Improved the Polygon and the PolygonParser to deal with compound paths.

This commit is contained in:
hneemann 2018-11-27 21:15:17 +01:00
parent b23c430f78
commit b1a82601c9
9 changed files with 400 additions and 138 deletions

View File

@ -57,8 +57,13 @@ public class GraphicMinMax implements Graphic {
@Override
public void drawPolygon(Polygon p, Style style) {
for (VectorInterface v : p)
check(v);
for (Polygon.PathElement v : p) {
if (v==null)
System.out.println(v);
final VectorInterface point = v.getPoint();
if (point != null)
check(point);
}
}
@Override

View File

@ -113,22 +113,11 @@ public class GraphicSVG implements Graphic {
@Override
public void drawPolygon(Polygon p, Style style) {
try {
//modification of loop variable i is intended!
//CHECKSTYLE.OFF: ModifiedControlVariable
w.write("<path d=\"M " + str(p.get(0)));
for (int i = 1; i < p.size(); i++)
if (p.isBezierStart(i)) {
w.write(" C " + str(p.get(i)) + " " + str(p.get(i + 1)) + " " + str(p.get(i + 2)));
i += 2;
} else
w.write(" L " + str(p.get(i)));
//CHECKSTYLE.ON: ModifiedControlVariable
if (p.isClosed())
w.write(" Z");
w.write("\"");
w.write("<path d=\"" + p + "\"");
addStrokeDash(w, style.getDash());
if (p.getEvenOdd() && style.isFilled())
w.write(" fill-rule=\"evenodd\"");
if (style.isFilled() && p.isClosed())
w.write(" stroke=\"" + getColor(style) + "\" stroke-width=\"" + getStrokeWidth(style) + "\" fill=\"" + getColor(style) + "\" fill-opacity=\"" + getOpacity(style) + "\"/>\n");
else

View File

@ -64,25 +64,7 @@ public class GraphicSwing implements Graphic {
public void drawPolygon(Polygon p, Style style) {
applyStyle(style);
Path2D path = new GeneralPath();
//modification of loop variable i is intended!
//CHECKSTYLE.OFF: ModifiedControlVariable
for (int i = 0; i < p.size(); i++) {
if (i == 0) {
path.moveTo(p.get(i).getXFloat(), p.get(i).getYFloat());
} else {
if (p.isBezierStart(i)) {
path.curveTo(p.get(i).getXFloat(), p.get(i).getYFloat(),
p.get(i + 1).getXFloat(), p.get(i + 1).getYFloat(),
p.get(i + 2).getXFloat(), p.get(i + 2).getYFloat());
i += 2;
} else
path.lineTo(p.get(i).getXFloat(), p.get(i).getYFloat());
}
}
//CHECKSTYLE.ON: ModifiedControlVariable
if (p.isClosed())
path.closePath();
p.drawTo(path);
if (style.isFilled() && p.isClosed())
gr.fill(path);

View File

@ -5,18 +5,19 @@
*/
package de.neemann.digital.draw.graphics;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
/**
* A polygon representation used by the {@link Graphic} interface.
*/
public class Polygon implements Iterable<VectorInterface> {
public class Polygon implements Iterable<Polygon.PathElement> {
private final ArrayList<VectorInterface> points;
private final HashSet<Integer> isBezierStart;
private final ArrayList<PathElement> path;
private boolean closed;
private boolean hasSpecialElements = false;
private boolean evenOdd;
/**
* Creates e new closed polygon
@ -41,9 +42,10 @@ public class Polygon implements Iterable<VectorInterface> {
* @param closed true if polygon is closed
*/
public Polygon(ArrayList<VectorInterface> points, boolean closed) {
this.points = points;
this.closed = closed;
isBezierStart = new HashSet<>();
this.path = new ArrayList<>();
for (VectorInterface p : points)
add(p);
}
/**
@ -71,10 +73,17 @@ public class Polygon implements Iterable<VectorInterface> {
* @return this for chained calls
*/
public Polygon add(VectorInterface p) {
points.add(p);
if (path.isEmpty())
path.add(new MoveTo(p));
else
path.add(new LineTo(p));
return this;
}
private void add(PathElement pe) {
path.add(pe);
}
/**
* Adds a new cubic bezier curve to the polygon.
*
@ -84,45 +93,29 @@ public class Polygon implements Iterable<VectorInterface> {
* @return this for chained calls
*/
public Polygon add(VectorInterface c1, VectorInterface c2, VectorInterface p) {
if (points.size() == 0)
if (path.size() == 0)
throw new RuntimeException("cubic bezier curve is not allowed to be the first path element");
isBezierStart.add(points.size());
points.add(c1);
points.add(c2);
points.add(p);
path.add(new CurveTo(c1, c2, p));
hasSpecialElements = true;
return this;
}
/**
* Returns true if the point with the given index is a bezier start point
*
* @param n the index
* @return true if point is bezier start
* @return true if filled in even odd mode
*/
public boolean isBezierStart(int n) {
return isBezierStart.contains(n);
public boolean getEvenOdd() {
return evenOdd;
}
/**
* @return the number of points
*/
public int size() {
return points.size();
}
/**
* Returns one of the points
* Sets the even odd mode used to fill the polygon
*
* @param i the index
* @return the i'th point
* @param evenOdd true is even odd mode is needed
* @return this for chained calls
*/
public VectorInterface get(int i) {
return points.get(i);
}
@Override
public Iterator<VectorInterface> iterator() {
return points.iterator();
public Polygon setEvenOdd(boolean evenOdd) {
this.evenOdd = evenOdd;
return this;
}
/**
@ -140,28 +133,43 @@ public class Polygon implements Iterable<VectorInterface> {
}
private boolean check(VectorInterface p1, VectorInterface p2) {
if (closed)
return false;
if (p1.equals(getFirst())) {
points.add(0, p2);
if (p2.equals(getLast()))
closed = true;
else {
removeInitialMoveTo();
path.add(0, new MoveTo(p2));
}
return true;
} else if (p1.equals(getLast())) {
points.add(p2);
if (p2.equals(getFirst()))
closed = true;
else
path.add(new LineTo(p2));
return true;
} else
return false;
}
private void removeInitialMoveTo() {
path.set(0, new LineTo(path.get(0)));
}
/**
* @return the first point of the polygon
*/
public VectorInterface getFirst() {
return points.get(0);
return path.get(0).getPoint();
}
/**
* @return the last point of the polygon
*/
public VectorInterface getLast() {
return points.get(points.size() - 1);
return path.get(path.size() - 1).getPoint();
}
/**
@ -171,10 +179,17 @@ public class Polygon implements Iterable<VectorInterface> {
* @return this for chained calls
*/
public Polygon append(Polygon p2) {
if (!p2.isBezierStart.isEmpty())
if (hasSpecialElements || p2.hasSpecialElements)
throw new RuntimeException("append of bezier not supported");
for (int i = 1; i < p2.points.size(); i++)
points.add(p2.points.get(i));
if (p2.getLast().equals(getFirst())) {
for (int i = 1; i < p2.path.size() - 1; i++)
add(p2.path.get(i).getPoint());
closed = true;
} else {
for (int i = 1; i < p2.path.size(); i++)
add(p2.path.get(i).getPoint());
}
return this;
}
@ -184,11 +199,11 @@ public class Polygon implements Iterable<VectorInterface> {
* @return returns this polygon with reverse order of points
*/
public Polygon reverse() {
if (!isBezierStart.isEmpty())
throw new RuntimeException("reverse of bezier not supported");
if (hasSpecialElements)
throw new RuntimeException("append of bezier not supported");
Polygon p = new Polygon(closed);
for (int i = points.size() - 1; i >= 0; i--)
p.add(points.get(i));
for (int i = path.size() - 1; i >= 0; i--)
p.add(path.get(i).getPoint());
return p;
}
@ -202,43 +217,24 @@ public class Polygon implements Iterable<VectorInterface> {
if (transform == Transform.IDENTITY)
return this;
Polygon p = new Polygon(closed);
for (VectorInterface v : points)
p.add(v.transform(transform));
p.isBezierStart.addAll(isBezierStart);
Polygon p = new Polygon(closed).setEvenOdd(evenOdd);
for (PathElement pe : path)
p.add(pe.transform(transform));
return p;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("M ");
VectorInterface v = points.get(0);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
//modification of loop variable i is intended!
//CHECKSTYLE.OFF: ModifiedControlVariable
for (int i = 1; i < points.size(); i++) {
v = points.get(i);
if (isBezierStart.contains(i)) {
sb.append("C ").append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
v = points.get(i + 1);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
v = points.get(i + 2);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
i += 2;
} else
sb.append("L ").append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
StringBuilder sb = new StringBuilder();
for (PathElement pe : path) {
if (sb.length() > 0)
sb.append(' ');
sb.append(pe.toString());
}
//CHECKSTYLE.ON: ModifiedControlVariable
if (closed)
sb.append("z");
return sb.toString();
}
private static String str(float f) {
if (f == Math.round(f))
return Integer.toString(Math.round(f));
else
return Float.toString(f);
if (closed)
sb.append(" Z");
return sb.toString();
}
/**
@ -258,4 +254,191 @@ public class Polygon implements Iterable<VectorInterface> {
void setClosed(boolean closed) {
this.closed = closed;
}
@Override
public Iterator<PathElement> iterator() {
return path.iterator();
}
/**
* Closes the actual path
*/
public void addClosePath() {
add(new ClosePath());
}
/**
* Adds a moveto to the path
*
* @param p the point to move to
*/
public void addMoveTo(VectorFloat p) {
add(new MoveTo(p));
}
/**
* Draw this polygon to a {@link Path2D} instance.
*
* @param path2d the Path2d instance.
*/
public void drawTo(Path2D path2d) {
for (PathElement pe : path)
pe.drawTo(path2d);
if (closed)
path2d.closePath();
if (evenOdd)
path2d.setWindingRule(Path2D.WIND_EVEN_ODD);
}
/**
* A element of the path
*/
public interface PathElement {
/**
* @return the coordinate of this path element
*/
VectorInterface getPoint();
/**
* Returns the transormated path element
*
* @param transform the transformation
* @return the transormated path element
*/
PathElement transform(Transform transform);
/**
* Draws this path element to a Path2D instance.
*
* @param path2d the a Path2D instance
*/
void drawTo(Path2D path2d);
}
private static String str(float f) {
if (f == Math.round(f))
return Integer.toString(Math.round(f));
else
return Float.toString(f);
}
private static String str(VectorInterface p) {
return str(p.getXFloat()) + "," + str(p.getYFloat());
}
//LineTo can not be final because its overridden. Maybe checkstyle has a bug?
//CHECKSTYLE.OFF: FinalClass
private static class LineTo implements PathElement {
protected final VectorInterface p;
private LineTo(VectorInterface p) {
this.p = p;
}
private LineTo(PathElement pathElement) {
this(pathElement.getPoint());
}
@Override
public VectorInterface getPoint() {
return p;
}
@Override
public PathElement transform(Transform transform) {
return new LineTo(p.transform(transform));
}
@Override
public void drawTo(Path2D path2d) {
path2d.lineTo(p.getXFloat(), p.getYFloat());
}
@Override
public String toString() {
return "L " + str(p);
}
}
//CHECKSTYLE.ON: FinalClass
private static final class MoveTo extends LineTo {
private MoveTo(VectorInterface p) {
super(p);
}
@Override
public String toString() {
return "M " + str(p);
}
@Override
public void drawTo(Path2D path2d) {
path2d.moveTo(p.getXFloat(), p.getYFloat());
}
@Override
public PathElement transform(Transform transform) {
return new MoveTo(p.transform(transform));
}
}
private static final class CurveTo implements PathElement {
private final VectorInterface c1;
private final VectorInterface c2;
private final VectorInterface p;
private CurveTo(VectorInterface c1, VectorInterface c2, VectorInterface p) {
this.c1 = c1;
this.c2 = c2;
this.p = p;
}
@Override
public VectorInterface getPoint() {
return p;
}
@Override
public PathElement transform(Transform transform) {
return new CurveTo(
c1.transform(transform),
c2.transform(transform),
p.transform(transform)
);
}
@Override
public String toString() {
return "C " + str(c1) + " " + str(c2) + " " + str(p);
}
@Override
public void drawTo(Path2D path2d) {
path2d.curveTo(c1.getXFloat(), c1.getYFloat(),
c2.getXFloat(), c2.getYFloat(),
p.getXFloat(), p.getYFloat());
}
}
private class ClosePath implements PathElement {
@Override
public VectorInterface getPoint() {
return null;
}
@Override
public PathElement transform(Transform transform) {
return this;
}
@Override
public void drawTo(Path2D path2d) {
path2d.closePath();
}
@Override
public String toString() {
return "Z";
}
}
}

View File

@ -25,12 +25,17 @@ public class PolygonConverter implements Converter {
public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext marshallingContext) {
Polygon p = (Polygon) o;
writer.addAttribute("path", p.toString());
writer.addAttribute("evenOdd", Boolean.toString(p.getEvenOdd()));
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext unmarshallingContext) {
String path = reader.getAttribute("path");
return Polygon.createFromPath(path);
boolean evenOdd = Boolean.parseBoolean(reader.getAttribute("evenOdd"));
final Polygon polygon = Polygon.createFromPath(path);
if (polygon != null)
polygon.setEvenOdd(evenOdd);
return polygon;
}
}

View File

@ -117,6 +117,7 @@ public class PolygonParser {
public Polygon create() throws ParserException {
Polygon p = new Polygon(false);
Token tok;
boolean closedPending = false;
while ((tok = next()) != Token.EOF) {
if (tok == Token.NUMBER) {
unreadToken();
@ -127,11 +128,19 @@ public class PolygonParser {
}
switch (command) {
case 'M':
p.add(nextVector());
if (closedPending) {
closedPending = false;
p.addClosePath();
}
p.addMoveTo(nextVector());
clearControl();
break;
case 'm':
p.add(nextVectorInc());
if (closedPending) {
closedPending = false;
p.addClosePath();
}
p.addMoveTo(nextVectorInc());
clearControl();
break;
case 'V':
@ -196,13 +205,15 @@ public class PolygonParser {
break;
case 'Z':
case 'z':
p.setClosed(true);
closedPending = true;
clearControl();
break;
default:
throw new ParserException("unsupported path command " + command);
}
}
if (closedPending)
p.setClosed(true);
return p;
}

View File

@ -75,7 +75,7 @@ public class GraphicLineCollector implements Graphic {
private void tryMerge(Polygon p1) {
for (Polygon p2 : polyList)
if (p1 != p2) {
if (p1 != p2 && !p1.isClosed() && !p2.isClosed()) {
if (p1.getLast().equals(p2.getFirst())) {
p1.append(p2);
polyList.remove(p2);

View File

@ -10,42 +10,46 @@ import junit.framework.TestCase;
public class PolygonTest extends TestCase {
public void testPath() {
checkLine(Polygon.createFromPath("m 10,10 L 20,10 20,20 10,20 z"));
checkLine(Polygon.createFromPath("m 10,10 l 10,0 0,10 -10,0 z"));
checkLine(Polygon.createFromPath("m 10,10 h 10 v 10 h -10 z"));
checkLine(Polygon.createFromPath("m 10,10 H 20 V 20 H 10 z"));
checkLine(Polygon.createFromPath("m 10,10 L 20,10 20,20 10,20 Z"));
checkLine(Polygon.createFromPath("m 10,10 l 10,0 0,10 -10,0 Z"));
checkLine(Polygon.createFromPath("m 10,10 h 10 v 10 h -10 Z"));
checkLine(Polygon.createFromPath("m 10,10 H 20 V 20 H 10 Z"));
}
private void checkLine(Polygon p) {
checkCoor(p);
assertEquals("M 10,10 L 20,10 L 20,20 L 10,20 z", p.toString());
}
private void checkCoor(Polygon p) {
assertEquals(4, p.size());
assertEquals(new VectorFloat(10, 10), p.get(0));
assertEquals(new VectorFloat(20, 10), p.get(1));
assertEquals(new VectorFloat(20, 20), p.get(2));
assertEquals(new VectorFloat(10, 20), p.get(3));
assertNotNull(p);
assertEquals("M 10,10 L 20,10 L 20,20 L 10,20 Z", p.toString());
}
private void checkBezier(Polygon p) {
checkCoor(p);
assertEquals("M 10,10 C 20,10 20,20 10,20 z", p.toString());
assertEquals("M 10,10 C 20,10 20,20 10,20 Z", p.toString());
}
public void testBezierPath() {
checkBezier(Polygon.createFromPath("m 10,10 C 20 10 20 20 10 20 z"));
checkBezier(Polygon.createFromPath("m 10,10 c 10 0 10 10 0 10 z"));
checkBezier(Polygon.createFromPath("m 10,10 C 20 10 20 20 10 20 Z"));
checkBezier(Polygon.createFromPath("m 10,10 c 10 0 10 10 0 10 Z"));
}
public void testCubicReflect() {
assertEquals("M 0,0 C 0,10 10,10 10,0 C 10,-10 20,-10 20,0 C 20,10 30,10 30,0 z",
Polygon.createFromPath("m 0,0 c 0,10 10,10 10,0 s 10,-10 10,0 s 10,10 10,0 z").toString());
assertEquals("M 0,0 C 0,10 10,10 10,0 C 10,-10 20,-10 20,0 C 20,10 30,10 30,0 Z",
Polygon.createFromPath("m 0,0 c 0,10 10,10 10,0 s 10,-10 10,0 s 10,10 10,0 Z").toString());
}
public void testQuadraticReflect() {
assertEquals("M 0,0 C 20,20 40,20 60,0 C 80,-20 100,-20 120,0 C 140,20 160,20 180,0 C 200,-20 220,-20 240,0 ",
assertEquals("M 0,0 C 20,20 40,20 60,0 C 80,-20 100,-20 120,0 C 140,20 160,20 180,0 C 200,-20 220,-20 240,0",
Polygon.createFromPath("m 0,0 Q 30,30 60,0 t 60,0 t 60,0 t 60,0").toString());
}
public void testMultiPath() {
assertEquals("M 0,0 C 0,40 20,60 60,60 C 100,60 120,40 120,0 C 120,-40 100,-60 60,-60 C 20,-60 0,-40 0,0 Z M 30,0 C 30,20 40,30 60,30 C 80,30 90,20 90,0 C 90,-20 80,-30 60,-30 C 40,-30 30,-20 30,0 Z",
Polygon.createFromPath("M 0,0 Q 0,60 60,60 T 120,0 T 60,-60 T 0,0 z M 30,0 Q 30,30 60,30 T 90,0 T 60,-30 T 30,0 Z").toString());
}
public void testAppend() {
Polygon p1 = new Polygon(false).add(0, 0).add(0, 10).add(10, 10);
Polygon p2 = new Polygon(false).add(10, 10).add(10, 0).add(0, 0);
assertEquals("M 0,0 L 0,10 L 10,10 L 10,0 Z", p1.append(p2).toString());
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2018 Helmut Neemann
* Use of this source code is governed by the GPL v3 license
* that can be found in the LICENSE file.
*/
package de.neemann.digital.draw.graphics.linemerger;
import de.neemann.digital.draw.graphics.*;
import junit.framework.TestCase;
import java.util.ArrayList;
public class GraphicLineCollectorTest extends TestCase {
public void testOpenSquare() {
GraphicLineCollector col = new GraphicLineCollector();
col.drawLine(new Vector(0, 0), new Vector(10, 0), Style.NORMAL);
col.drawLine(new Vector(0, 10), new Vector(10, 10), Style.NORMAL);
col.drawLine(new Vector(0, 0), new Vector(0, 10), Style.NORMAL);
ArrayList<Polygon> poly = new ArrayList<>();
col.drawTo(new MyGraphic(poly));
assertEquals(1, poly.size());
assertEquals("M 10,0 L 0,0 L 0,10 L 10,10", poly.get(0).toString());
}
public void testClosedSquare() {
GraphicLineCollector col = new GraphicLineCollector();
col.drawLine(new Vector(0, 0), new Vector(10, 0), Style.NORMAL);
col.drawLine(new Vector(0, 10), new Vector(10, 10), Style.NORMAL);
col.drawLine(new Vector(0, 0), new Vector(0, 10), Style.NORMAL);
col.drawLine(new Vector(10, 0), new Vector(10, 10), Style.NORMAL);
ArrayList<Polygon> poly = new ArrayList<>();
col.drawTo(new MyGraphic(poly));
assertEquals(1, poly.size());
assertEquals("M 10,0 L 0,0 L 0,10 L 10,10 Z", poly.get(0).toString());
}
public void testClosedSquare2() {
GraphicLineCollector col = new GraphicLineCollector();
col.drawLine(new Vector(0, 0), new Vector(10, 0), Style.NORMAL);
col.drawLine(new Vector(0, 10), new Vector(10, 10), Style.NORMAL);
col.drawLine(new Vector(0, 0), new Vector(0, 10), Style.NORMAL);
col.drawLine(new Vector(10, 0), new Vector(10, 10), Style.NORMAL);
ArrayList<Polygon> poly = new ArrayList<>();
col.drawTo(new MyGraphic(poly));
assertEquals(1, poly.size());
assertEquals("M 10,0 L 0,0 L 0,10 L 10,10 Z", poly.get(0).toString());
}
private static class MyGraphic implements Graphic {
private final ArrayList<Polygon> poly;
private MyGraphic(ArrayList<Polygon> poly) {
this.poly = poly;
}
@Override
public void drawLine(VectorInterface p1, VectorInterface p2, Style style) {
}
@Override
public void drawPolygon(Polygon p, Style style) {
poly.add(p);
}
@Override
public void drawCircle(VectorInterface p1, VectorInterface p2, Style style) {
}
@Override
public void drawText(VectorInterface p1, VectorInterface p2, String text, Orientation orientation, Style style) {
}
}
}