From 36acbb7d963b9c2b795c2e5a9fe1e11af7ccf90a Mon Sep 17 00:00:00 2001 From: hneemann Date: Sat, 1 Dec 2018 14:01:14 +0100 Subject: [PATCH] added the arc path element --- .../digital/draw/graphics/PolygonParser.java | 104 +++++++++++++++++- .../digital/draw/graphics/Transform.java | 12 ++ .../draw/graphics/TransformMatrix.java | 53 ++++++++- .../draw/graphics/TransformTranslate.java | 5 + .../draw/graphics/TransformMatrixTest.java | 39 +++++++ .../draw/shapes/custom/SvgImporterTest.java | 92 ++++++++++++++-- 6 files changed, 287 insertions(+), 18 deletions(-) create mode 100644 src/test/java/de/neemann/digital/draw/graphics/TransformMatrixTest.java diff --git a/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java b/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java index 50e2ccf72..e9a2caf87 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java +++ b/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java @@ -196,11 +196,11 @@ public class PolygonParser { addQuadraticWithReflect(p, getCurrent(), nextVector()); break; case 'a': - addArc(p, nextVectorInc(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc()); + addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc()); clearControl(); break; case 'A': - addArc(p, nextVector(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector()); + addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector()); clearControl(); break; case 'Z': @@ -250,8 +250,102 @@ public class PolygonParser { return lastCubicControlPoint; } - private void addArc(Polygon p, VectorFloat rad, float rot, boolean large, boolean sweep, VectorFloat pos) { - p.add(pos); + /* + * Substitutes the arc by a number of quadratic bezier curves + */ + //CHECKSTYLE.OFF: ParameterNumberCheck + private void addArc(Polygon p, VectorInterface current, float rx, float ry, float rot, boolean large, boolean sweep, VectorFloat pos) { + Transform tr = Transform.IDENTITY; + if (rx != ry) + tr = TransformMatrix.scale(1, rx / ry); + + if (rot != 0) + tr = Transform.mul(TransformMatrix.rotate(-rot), tr); + + Transform invert = tr.invert(); + + VectorInterface p1 = current.transform(tr); + VectorInterface p2 = pos.transform(tr); + + // ellipse is transformed to a circle with radius r + float r = rx; + + double x1 = p1.getXFloat(); + double y1 = p1.getYFloat(); + double x2 = p2.getXFloat(); + double y2 = p2.getYFloat(); + + double x1q = x1 * x1; + double y1q = y1 * y1; + double x2q = x2 * x2; + double y2q = y2 * y2; + double rq = r * r; + + double x0A = (r * (y1 - y2) * sqrt(rq * (4 * rq - y1q + y2 * (2 * y1 - y2)) - rq * (x1q - 2 * x1 * x2 + x2q)) * sign(x1 - x2) + r * (x1 + x2) * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))) / (2 * r * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))); + double y0A = (r * (y1 + y2) * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q)) - r * sqrt(rq * (4 * rq - y1q + y2 * (2 * y1 - y2)) - rq * (x1q - 2 * x1 * x2 + x2q)) * abs(x1 - x2)) / (2 * r * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))); + double x0B = (r * (x1 + x2) * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q)) - r * (y1 - y2) * sqrt(rq * (4 * rq - y1q + y2 * (2 * y1 - y2)) - rq * (x1q - 2 * x1 * x2 + x2q)) * sign(x1 - x2)) / (2 * r * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))); + double y0B = (r * sqrt(rq * (4 * rq - y1q + y2 * (2 * y1 - y2)) - rq * (x1q - 2 * x1 * x2 + x2q)) * abs(x1 - x2) + r * (y1 + y2) * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))) / (2 * r * sqrt(rq * (y1q - 2 * y1 * y2 + y2q) + rq * (x1q - 2 * x1 * x2 + x2q))); + + double startA = Math.atan2(y1 - y0A, x1 - x0A); + double endA = Math.atan2(y2 - y0A, x2 - x0A); + + double startB = Math.atan2(y1 - y0B, x1 - x0B); + double endB = Math.atan2(y2 - y0B, x2 - x0B); + + double delta = 2 * Math.PI / 12; + if (!sweep) delta = -delta; + + if (delta > 0) { + if (endA < startA) endA += 2 * Math.PI; + if (endB < startB) endB += 2 * Math.PI; + } else { + if (endA > startA) endA -= 2 * Math.PI; + if (endB > startB) endB -= 2 * Math.PI; + } + + double sizeA = Math.abs(startA - endA); + double sizeB = Math.abs(startB - endB); + + double start = startA; + double end = endA; + double x0 = x0A; + double y0 = y0A; + if (large ^ (sizeA > sizeB)) { + start = startB; + end = endB; + x0 = x0B; + y0 = y0B; + } + + double lastStart = start; + start += delta; + while (delta < 0 ^ start < end) { + addArcPoint(p, lastStart, start, x0, y0, r, invert); + lastStart = start; + start += delta; + } + addArcPoint(p, lastStart, end, x0, y0, r, invert); + } + //CHECKSTYLE.ON: ParameterNumberCheck + + private void addArcPoint(Polygon p, double alpha0, double alpha1, double x0, double y0, float r, Transform tr) { + final double mean = (alpha0 + alpha1) / 2; + double rLong = r / Math.cos(Math.abs(alpha0 - alpha1) / 2); + final VectorInterface c = new VectorFloat((float) (x0 + rLong * Math.cos(mean)), (float) (y0 + rLong * Math.sin(mean))); + final VectorInterface p1 = new VectorFloat((float) (x0 + r * Math.cos(alpha1)), (float) (y0 + r * Math.sin(alpha1))); + p.add(c.transform(tr), p1.transform(tr)); + } + + private static double sqrt(double x) { + return Math.sqrt(x); + } + + private static double sign(double x) { + return Math.signum(x); + } + + private static double abs(double x) { + return Math.abs(x); } private void addQuadraticWithReflect(Polygon poly, VectorInterface start, VectorInterface p) { @@ -296,7 +390,7 @@ public class PolygonParser { private Polygon parsePolygonPolyline(boolean closed) throws ParserException { Polygon p = new Polygon(closed); while (next() != Token.EOF) - p.add(new VectorFloat(value, nextValue())); + p.add(new VectorFloat(value, nextValue())); return p; } diff --git a/src/main/java/de/neemann/digital/draw/graphics/Transform.java b/src/main/java/de/neemann/digital/draw/graphics/Transform.java index 7f39d323f..f839c0158 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/Transform.java +++ b/src/main/java/de/neemann/digital/draw/graphics/Transform.java @@ -28,6 +28,11 @@ public interface Transform { public TransformMatrix getMatrix() { return new TransformMatrix(1, 0, 0, 1, 0, 0); } + + @Override + public Transform invert() { + return IDENTITY; + } }; /** @@ -72,4 +77,11 @@ public interface Transform { * @return the transformed Transform */ TransformMatrix getMatrix(); + + /** + * @return the inverse transform + */ + default Transform invert() { + return getMatrix().invert(); + } } diff --git a/src/main/java/de/neemann/digital/draw/graphics/TransformMatrix.java b/src/main/java/de/neemann/digital/draw/graphics/TransformMatrix.java index 246714513..60ac21ccd 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/TransformMatrix.java +++ b/src/main/java/de/neemann/digital/draw/graphics/TransformMatrix.java @@ -13,17 +13,30 @@ public class TransformMatrix implements Transform { /** * Creates a rotation. + * Rotates in mathematically positive direction. Takes into account that + * in Digital the y-axis goes downwards. * - * @param w the angle in 360 grad + * @param w the angle in 360 grad units * @return the transformation */ - public static TransformMatrix rotate(float w) { + public static TransformMatrix rotate(double w) { final double phi = w / 180 * Math.PI; float cos = (float) Math.cos(phi); float sin = (float) Math.sin(phi); return new TransformMatrix(cos, sin, -sin, cos, 0, 0); } + /** + * Creates a scaling transformation + * + * @param sx scaling in x direction + * @param sy scaling in y direction + * @return the transformation + */ + public static TransformMatrix scale(float sx, float sy) { + return new TransformMatrix(sx, 0, 0, sy, 0, 0); + } + final float a; final float b; final float c; @@ -66,7 +79,8 @@ public class TransformMatrix implements Transform { /** - * Transforms a direction vector + * Transforms a direction vector. + * Ignores the translation part of the transformation. * * @param v the vector to transform * @return the transformed vector @@ -82,4 +96,37 @@ public class TransformMatrix implements Transform { return this; } + /** + * Returns the inverse transformation. + * + * @return the inverse transformation. + */ + public TransformMatrix invert() { + float q = a * d - b * c; + + return new TransformMatrix(d / q, -b / q, -c / q, a / q, + (b * y - d * x) / q, (c * x - a * y) / q); + } + } + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/de/neemann/digital/draw/graphics/TransformTranslate.java b/src/main/java/de/neemann/digital/draw/graphics/TransformTranslate.java index 6776b28d4..a5bb6f2de 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/TransformTranslate.java +++ b/src/main/java/de/neemann/digital/draw/graphics/TransformTranslate.java @@ -54,4 +54,9 @@ public class TransformTranslate implements Transform { public TransformMatrix getMatrix() { return new TransformMatrix(1, 0, 0, 1, trans.getXFloat(), trans.getYFloat()); } + + @Override + public Transform invert() { + return new TransformTranslate(trans.div(-1)); + } } diff --git a/src/test/java/de/neemann/digital/draw/graphics/TransformMatrixTest.java b/src/test/java/de/neemann/digital/draw/graphics/TransformMatrixTest.java new file mode 100644 index 000000000..3a4f4da81 --- /dev/null +++ b/src/test/java/de/neemann/digital/draw/graphics/TransformMatrixTest.java @@ -0,0 +1,39 @@ +package de.neemann.digital.draw.graphics; + +import junit.framework.TestCase; + +public class TransformMatrixTest extends TestCase { + + public void testScale() { + TransformMatrix tr = TransformMatrix.scale(2, 3); + VectorInterface p = new VectorFloat(3, 4).transform(tr); + assertEquals(6, p.getXFloat(), 1e-4); + assertEquals(12, p.getYFloat(), 1e-4); + } + + public void testRotate() { + TransformMatrix tr = TransformMatrix.rotate(45); + VectorInterface p = new VectorFloat(2, 0).transform(tr); + assertEquals(Math.sqrt(2), p.getXFloat(), 1e-4); + assertEquals(-Math.sqrt(2), p.getYFloat(), 1e-4); + } + + public void testRotateInverse() { + Transform tr = new TransformRotate(new Vector(2,3),1); + VectorInterface p = new VectorFloat(7, 8); + + VectorInterface t = p.transform(tr).transform(tr.invert()); + assertEquals(p.getXFloat(), t.getXFloat(), 1e-4); + assertEquals(p.getYFloat(), t.getYFloat(), 1e-4); + } + + public void testInverse() { + TransformMatrix tr = new TransformMatrix(1, 2, 3, 4, 5, 6); + VectorInterface p = new VectorFloat(7, 8); + + VectorInterface t = p.transform(tr).transform(tr.invert()); + assertEquals(p.getXFloat(), t.getXFloat(), 1e-4); + assertEquals(p.getYFloat(), t.getYFloat(), 1e-4); + } + +} \ No newline at end of file diff --git a/src/test/java/de/neemann/digital/draw/shapes/custom/SvgImporterTest.java b/src/test/java/de/neemann/digital/draw/shapes/custom/SvgImporterTest.java index 9ec83d3a7..eb9909bae 100644 --- a/src/test/java/de/neemann/digital/draw/shapes/custom/SvgImporterTest.java +++ b/src/test/java/de/neemann/digital/draw/shapes/custom/SvgImporterTest.java @@ -216,7 +216,7 @@ public class SvgImporterTest extends TestCase { .check(); } - public void testScale() throws IOException, SvgException, PolygonParser.ParserException, PinException { + public void testScale() throws IOException, SvgException, PinException { CustomShapeDescription custom = new SvgImporter( in("\n" + "\n" + @@ -230,6 +230,78 @@ public class SvgImporterTest extends TestCase { .check(); } + public void testArc() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 40,0 Q 37.161327,16.337067 40.99908,25.612656 Q 44.83683,34.888245 54.32269,34.616966 Q 63.80854,34.345688 76.40078,24.600235 Q 88.27738,15.408623 100,0 L 140,0") + .check(); + } + + public void testArc2() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 40,0 Q 27.571037,16.337067 18.5496,35.35811 Q 9.528162,54.37915 6.331539,70.987495 Q 3.1349144,87.59584 6.6196365,97.3413 Q 10.104355,107.086754 19.336689,107.358025 Q 28.569027,107.6293 41.075184,98.35372 Q 53.581345,89.078125 66.0103,72.74106 Q 78.43926,56.403996 87.46069,37.38295 Q 96.48214,18.36191 99.67876,1.7535629 Q 99.84891,0.86956406 100,0 L 140,0") + .check(); + } + + public void testArc3() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 40,0 Q 52.42896,-16.33707 64.93511,-25.612656 Q 77.44127,-34.88825 86.67361,-34.616966 Q 95.905945,-34.345688 99.39067,-24.600235 Q 102.67735,-15.408623 100,0 L 140,0") + .check(); + } + + public void testArc4() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 40,0 Q 42.83867,-16.337069 51.593147,-35.35811 Q 60.347626,-54.379158 72.67216,-70.987495 Q 84.99669,-87.59584 97.58891,-97.3413 Q 110.18115,-107.086754 119.66701,-107.35804 Q 129.15286,-107.62932 132.99062,-98.353714 Q 136.82837,-89.07814 133.98969,-72.74107 Q 131.15103,-56.403996 122.396545,-37.382957 Q 113.64207,-18.361908 101.317535,-1.7535667 Q 100.661545,-0.86956406 100,0 L 140,0") + .check(); + } + + public void testInkscape1() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 40,-40 L 60,-40 C 71.08,-40 80,-31.08 80,-20 L 80,20 C 80,31.08 71.08,40 60,40 L 40,40 C 28.92,40 20,31.08 20,20 L 20,-20 C 20,-31.08 28.92,-40 40,-40 Z") + .check(); + } + + public void testInkscape2() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 80,0 Q 80,10.717968 74.641014,20 Q 69.282036,29.282032 60,34.641018 Q 59.05599,35.186043 58.08369,35.67885 Q 48.52357,40.52436 37.82151,39.940636 Q 27.119452,39.35691 18.143057,33.500362 Q 17.230127,32.904728 16.35099,32.26026 L 40,0 Z") + .check(); + } + //***************************************************************************************************** @@ -238,12 +310,12 @@ public class SvgImporterTest extends TestCase { return new ByteArrayInputStream(s.getBytes()); } - public static class CSDChecker { + private static class CSDChecker { private final CustomShapeDescription csd; private final ArrayList checker; private final ArrayList pins; - public CSDChecker(CustomShapeDescription csd) { + private CSDChecker(CustomShapeDescription csd) { this.csd = csd; this.checker = new ArrayList<>(); this.pins = new ArrayList<>(); @@ -276,26 +348,26 @@ public class SvgImporterTest extends TestCase { fail("not enough elements found in the csd"); } - public CSDChecker checkPolygon(String s) throws PolygonParser.ParserException { + private CSDChecker checkPolygon(String s) throws PolygonParser.ParserException { checker.add(new CheckPolygon(new PolygonParser(s).create())); return this; } - public CSDChecker checkPolygon(String s, boolean evenOdd) throws PolygonParser.ParserException { + private CSDChecker checkPolygon(String s, boolean evenOdd) throws PolygonParser.ParserException { checker.add(new CheckPolygon(new PolygonParser(s).create().setEvenOdd(evenOdd))); return this; } - public CSDChecker checkPin(int x, int y, String name, boolean showLabel) { + private CSDChecker checkPin(int x, int y, String name, boolean showLabel) { pins.add(new TestPin(x, y, name, showLabel)); return this; } - public CSDChecker checkLine(int x1, int y1, int x2, int y2) { + private CSDChecker checkLine(int x1, int y1, int x2, int y2) { checker.add(new Checker() { @Override public void check(Drawable d) { - assertTrue(d instanceof CustomShapeDescription.LineHolder); + assertTrue("element is no line", d instanceof CustomShapeDescription.LineHolder); CustomShapeDescription.LineHolder l = (CustomShapeDescription.LineHolder) d; assertEquals(x1, l.getP1().x); assertEquals(y1, l.getP1().y); @@ -312,7 +384,7 @@ public class SvgImporterTest extends TestCase { private final String name; private final boolean showLabel; - public TestPin(int x, int y, String name, boolean showLabel) { + private TestPin(int x, int y, String name, boolean showLabel) { this.x = x; this.y = y; this.name = name; @@ -329,7 +401,7 @@ public class SvgImporterTest extends TestCase { private final Polygon should; - public CheckPolygon(Polygon should) { + private CheckPolygon(Polygon should) { this.should = should; }