diff --git a/distribution/ReleaseNotes.txt b/distribution/ReleaseNotes.txt index 08dc728ca..9e714af28 100644 --- a/distribution/ReleaseNotes.txt +++ b/distribution/ReleaseNotes.txt @@ -2,6 +2,7 @@ Release Notes HEAD, planned as v0.21 +- Added a simple SVG importer to define custom shapes. - Added an FSM editor, which allows to input a fsm, creating a associated truth table and finally allows to create a circuit which implements the FSM. - Added a divider component. diff --git a/src/main/dig/processor/ALU/ALU.dig b/src/main/dig/processor/ALU/ALU.dig index 8502dd44c..80258120d 100644 --- a/src/main/dig/processor/ALU/ALU.dig +++ b/src/main/dig/processor/ALU/ALU.dig @@ -4,7 +4,7 @@ shapeType - CUSTOM + CUSTOM Description @@ -85,7 +85,7 @@ bei der Speicheradressierung verwendet.}} - + 1 true @@ -96,7 +96,7 @@ bei der Speicheradressierung verwendet.}} - + 4 false diff --git a/src/main/java/de/neemann/digital/draw/graphics/GraphicMinMax.java b/src/main/java/de/neemann/digital/draw/graphics/GraphicMinMax.java index 103369a8b..962edacae 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/GraphicMinMax.java +++ b/src/main/java/de/neemann/digital/draw/graphics/GraphicMinMax.java @@ -57,8 +57,7 @@ public class GraphicMinMax implements Graphic { @Override public void drawPolygon(Polygon p, Style style) { - for (VectorInterface v : p) - check(v); + p.traverse(this::check); } @Override diff --git a/src/main/java/de/neemann/digital/draw/graphics/GraphicSVG.java b/src/main/java/de/neemann/digital/draw/graphics/GraphicSVG.java index 95202874b..3b43c3c84 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/GraphicSVG.java +++ b/src/main/java/de/neemann/digital/draw/graphics/GraphicSVG.java @@ -6,10 +6,9 @@ package de.neemann.digital.draw.graphics; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.Date; -import static java.lang.System.out; - /** * Used to create a SVG representation of the circuit. * Don't use this implementation directly. Use {@link GraphicSVGIndex} to create plain SVG or @@ -61,7 +60,7 @@ public class GraphicSVG implements Graphic { @Override public Graphic setBoundingBox(VectorInterface min, VectorInterface max) { try { - w = new BufferedWriter(new OutputStreamWriter(out, "utf-8")); + w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); w.write("\n" + "\n"); w.write("\n"); @@ -113,22 +112,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("\n"); else diff --git a/src/main/java/de/neemann/digital/draw/graphics/GraphicSwing.java b/src/main/java/de/neemann/digital/draw/graphics/GraphicSwing.java index b051fbb57..638b970b1 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/GraphicSwing.java +++ b/src/main/java/de/neemann/digital/draw/graphics/GraphicSwing.java @@ -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); diff --git a/src/main/java/de/neemann/digital/draw/graphics/Polygon.java b/src/main/java/de/neemann/digital/draw/graphics/Polygon.java index ac8465daf..13150d53d 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/Polygon.java +++ b/src/main/java/de/neemann/digital/draw/graphics/Polygon.java @@ -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 { +public class Polygon implements Iterable { - private final ArrayList points; - private final HashSet isBezierStart; + private final ArrayList 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 { * @param closed true if polygon is closed */ public Polygon(ArrayList 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 { * @return this for chained calls */ public Polygon add(VectorInterface p) { - points.add(p); + if (path.isEmpty()) + add(new MoveTo(p)); + else + 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,60 @@ public class Polygon implements Iterable { * @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); + add(new CurveTo(c1, c2, p)); + hasSpecialElements = true; return this; } /** - * Returns true if the point with the given index is a bezier start point + * Adds a new quadratic bezier curve to the polygon. * - * @param n the index - * @return true if point is bezier start + * @param c the control point to add + * @param p the end point to add + * @return this for chained calls */ - public boolean isBezierStart(int n) { - return isBezierStart.contains(n); + public Polygon add(VectorInterface c, VectorInterface p) { + if (path.size() == 0) + throw new RuntimeException("quadratic bezier curve is not allowed to be the first path element"); + add(new QuadTo(c, p)); + hasSpecialElements = true; + return this; } /** - * @return the number of points + * Closes the actual path */ - public int size() { - return points.size(); + public void addClosePath() { + add(new ClosePath()); } /** - * Returns one of the points + * Adds a moveto to the path * - * @param i the index - * @return the i'th point + * @param p the point to move to */ - public VectorInterface get(int i) { - return points.get(i); + public void addMoveTo(VectorFloat p) { + add(new MoveTo(p)); } - @Override - public Iterator iterator() { - return points.iterator(); + /** + * @return true if filled in even odd mode + */ + public boolean getEvenOdd() { + return evenOdd; + } + + /** + * Sets the even odd mode used to fill the polygon + * + * @param evenOdd true is even odd mode is needed + * @return this for chained calls + */ + public Polygon setEvenOdd(boolean evenOdd) { + this.evenOdd = evenOdd; + return this; } /** @@ -140,28 +164,45 @@ public class Polygon implements Iterable { } 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() { + if (!(path.get(0) instanceof MoveTo)) + throw new RuntimeException("initial path element is not a MoveTo!"); + 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 +212,17 @@ public class Polygon implements Iterable { * @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 +232,11 @@ public class Polygon implements Iterable { * @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 +250,24 @@ public class Polygon implements Iterable { 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 +287,260 @@ public class Polygon implements Iterable { void setClosed(boolean closed) { this.closed = closed; } + + @Override + public Iterator iterator() { + return path.iterator(); + } + + /** + * 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); + } + + /** + * Traverses all points + * + * @param v the visitor to use + */ + public void traverse(PointVisitor v) { + for (PathElement pe : path) + pe.traverse(v); + } + + /** + * Visitor used to traverse all points + */ + public interface PointVisitor { + /** + * Called with every point + * + * @param p the point + */ + void visit(VectorInterface p); + } + + /** + * A element of the path + */ + public interface PathElement { + /** + * @return the coordinate of this path element + */ + VectorInterface getPoint(); + + /** + * Returns the transformed 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); + + /** + * Traverses all points + * + * @param v the visitor to use + */ + void traverse(PointVisitor v); + } + + 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); + } + + @Override + public void traverse(PointVisitor v) { + v.visit(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()); + } + + @Override + public void traverse(PointVisitor v) { + v.visit(c1); + v.visit(c2); + v.visit(p); + } + } + + private static final class QuadTo implements PathElement { + private final VectorInterface c; + private final VectorInterface p; + + private QuadTo(VectorInterface c, VectorInterface p) { + this.c = c; + this.p = p; + } + + @Override + public VectorInterface getPoint() { + return p; + } + + @Override + public PathElement transform(Transform transform) { + return new QuadTo( + c.transform(transform), + p.transform(transform) + ); + } + + @Override + public String toString() { + return "Q " + str(c) + " " + str(p); + } + + @Override + public void drawTo(Path2D path2d) { + path2d.quadTo(c.getXFloat(), c.getYFloat(), + p.getXFloat(), p.getYFloat()); + } + + @Override + public void traverse(PointVisitor v) { + v.visit(c); + v.visit(p); + } + } + + 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"; + } + + @Override + public void traverse(PointVisitor v) { + } + } } diff --git a/src/main/java/de/neemann/digital/draw/graphics/PolygonConverter.java b/src/main/java/de/neemann/digital/draw/graphics/PolygonConverter.java index 35a15b72f..772705f8c 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/PolygonConverter.java +++ b/src/main/java/de/neemann/digital/draw/graphics/PolygonConverter.java @@ -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; } } 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 da4a49117..e9a2caf87 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java +++ b/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java @@ -26,7 +26,7 @@ public class PolygonParser { * * @param path the path to parse */ - PolygonParser(String path) { + public PolygonParser(String path) { this.path = path; pos = 0; } @@ -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': @@ -169,10 +178,10 @@ public class PolygonParser { p.add(nextVector(), setLastC3(nextVector()), nextVector()); break; case 'q': - addQuadratic(p, getCurrent(), setLastC2(nextVectorRel()), nextVectorInc()); + p.add(setLastC2(nextVectorRel()), nextVectorInc()); break; case 'Q': - addQuadratic(p, getCurrent(), setLastC2(nextVector()), nextVector()); + p.add(setLastC2(nextVector()), nextVector()); break; case 's': addCubicWithReflect(p, getCurrent(), nextVectorRel(), nextVectorInc()); @@ -187,22 +196,24 @@ 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': case 'z': - p.setClosed(true); + closedPending = true; clearControl(); break; default: throw new ParserException("unsupported path command " + command); } } + if (closedPending) + p.setClosed(true); return p; } @@ -239,18 +250,107 @@ 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 void addQuadratic(Polygon poly, VectorInterface start, VectorInterface c, VectorInterface p) { - c = c.mul(2.0f / 3); - poly.add(start.mul(1f / 3).add(c), p.mul(1f / 3).add(c), p); + 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) { VectorInterface c = start.add(start.sub(getLastC2())); - addQuadratic(poly, start, setLastC2(c), p); + poly.add(setLastC2(c), p); } private void addCubicWithReflect(Polygon poly, VectorInterface start, VectorInterface c2, VectorInterface p) { @@ -266,4 +366,32 @@ public class PolygonParser { super(message); } } + + /** + * Parses a polygon. + * + * @return the polygon + * @throws ParserException ParserException + */ + public Polygon parsePolygon() throws ParserException { + return parsePolygonPolyline(true); + } + + /** + * Parses a polyline. + * + * @return the polygon + * @throws ParserException ParserException + */ + public Polygon parsePolyline() throws ParserException { + return parsePolygonPolyline(false); + } + + private Polygon parsePolygonPolyline(boolean closed) throws ParserException { + Polygon p = new Polygon(closed); + while (next() != Token.EOF) + 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/main/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollector.java b/src/main/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollector.java index 28a79c770..470305095 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollector.java +++ b/src/main/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollector.java @@ -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); diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/CustomShapeDescription.java b/src/main/java/de/neemann/digital/draw/shapes/custom/CustomShapeDescription.java index cd6ef32e8..cf6a55371 100644 --- a/src/main/java/de/neemann/digital/draw/shapes/custom/CustomShapeDescription.java +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/CustomShapeDescription.java @@ -147,6 +147,13 @@ public class CustomShapeDescription implements Iterable { .addText(new Vector(20, -25), new Vector(21, -25), "Hi!", Orientation.LEFTCENTER, 20, Color.BLACK); } + /** + * @return the number of pins in this shape + */ + public int getPinCount() { + return pins.size(); + } + /** * Stores a line. */ @@ -167,6 +174,20 @@ public class CustomShapeDescription implements Iterable { public void drawTo(Graphic graphic, Style highLight) { graphic.drawLine(p1, p2, Style.NORMAL.deriveStyle(thickness, false, color)); } + + /** + * @return first coordinate + */ + public Vector getP1() { + return p1; + } + + /** + * @return second coordinate + */ + public Vector getP2() { + return p2; + } } /** @@ -214,6 +235,13 @@ public class CustomShapeDescription implements Iterable { public void drawTo(Graphic graphic, Style highLight) { graphic.drawPolygon(poly, Style.NORMAL.deriveStyle(thickness, filled, color)); } + + /** + * @return the stored polygon + */ + public Polygon getPolygon() { + return poly; + } } /** diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java new file mode 100644 index 000000000..eb82da17c --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java @@ -0,0 +1,196 @@ +/* + * 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.shapes.custom.svg; + +import de.neemann.digital.draw.graphics.*; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.awt.*; +import java.util.HashMap; +import java.util.StringTokenizer; + +class Context { + + private static final HashMap PARSER = new HashMap<>(); + + + static { + PARSER.put("transform", Context::readTransform); + PARSER.put("fill", (c, value) -> c.fill = getColorFromString(value)); + PARSER.put("stroke", (c, value) -> c.color = getColorFromString(value)); + PARSER.put("stroke-width", (c, value) -> c.thickness = getFloatFromString(value) + 1); + PARSER.put("font-size", (c, value) -> c.fontSize = getFloatFromString(value) + 1); + PARSER.put("style", Context::readStyle); + PARSER.put("text-anchor", (c, value) -> c.textAnchor = value); + PARSER.put("fill-rule", (c, value) -> c.fillRuleEvenOdd = value.equalsIgnoreCase("evenodd")); + } + + private Transform tr; + private Color fill; + private Color color; + private float thickness; + private float fontSize; + private String textAnchor; + private boolean fillRuleEvenOdd; + + Context() { + tr = Transform.IDENTITY; + thickness = 1; + color = Color.BLACK; + } + + private Context(Context parent) { + tr = parent.tr; + fill = parent.fill; + color = parent.color; + thickness = parent.thickness; + fontSize = parent.fontSize; + fillRuleEvenOdd = parent.fillRuleEvenOdd; + } + + Context(Context parent, Element element) throws SvgException { + this(parent); + final NamedNodeMap attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + final Node item = attributes.item(i); + AttrParser p = PARSER.get(item.getNodeName()); + if (p != null) + p.parse(this, item.getNodeValue().trim()); + } + } + + private static void readStyle(Context context, String style) throws SvgException { + StringTokenizer st = new StringTokenizer(style, ";"); + while (st.hasMoreTokens()) { + String[] t = st.nextToken().split(":"); + if (t.length == 2) { + AttrParser p = PARSER.get(t[0].trim()); + if (p != null) + p.parse(context, t[1].trim()); + } + } + } + + Transform getTransform() { + return tr; + } + + public Color getColor() { + return color; + } + + public Color getFilled() { + return fill; + } + + public int getThickness() { + return (int) thickness; + } + + public boolean isFillRuleEvenOdd() { + return fillRuleEvenOdd; + } + + public Orientation getTextOrientation() { + if (textAnchor == null) + return Orientation.LEFTBOTTOM; + + switch (textAnchor) { + case "end": + return Orientation.RIGHTBOTTOM; + case "middle": + return Orientation.CENTERBOTTOM; + default: + return Orientation.LEFTBOTTOM; + } + } + + public VectorInterface tr(VectorInterface vector) { + return vector.transform(tr); + } + + public VectorInterface v(float x, float y) { + return new VectorFloat(x, y).transform(tr); + } + + public VectorInterface v(String xStr, String yStr) { + float x = xStr.isEmpty() ? 0 : Float.parseFloat(xStr); + float y = yStr.isEmpty() ? 0 : Float.parseFloat(yStr); + return v(x, y); + } + + public float getFontSize() { + return fontSize; + } + + + private interface AttrParser { + void parse(Context c, String value) throws SvgException; + } + + private static void readTransform(Context c, String value) throws SvgException { + StringTokenizer st = new StringTokenizer(value, "(),"); + Transform t = null; + final String trans = st.nextToken(); + switch (trans) { + case "translate": + t = new TransformTranslate(new VectorFloat(Float.parseFloat(st.nextToken()), Float.parseFloat(st.nextToken()))); + break; + case "scale": + final float xs = Float.parseFloat(st.nextToken()); + final float ys = Float.parseFloat(st.nextToken()); + t = new TransformMatrix(xs, 0, 0, ys, 0, 0); + break; + case "matrix": + t = new TransformMatrix( + Float.parseFloat(st.nextToken()), + Float.parseFloat(st.nextToken()), + Float.parseFloat(st.nextToken()), + Float.parseFloat(st.nextToken()), + Float.parseFloat(st.nextToken()), + Float.parseFloat(st.nextToken())); + break; + case "rotate": + float w = Float.parseFloat(st.nextToken()); + if (st.hasMoreTokens()) { + t = TransformMatrix.rotate(w); + float xc = Float.parseFloat(st.nextToken()); + float yc = Float.parseFloat(st.nextToken()); + t = Transform.mul(new TransformTranslate(xc, yc), t); + t = Transform.mul(t, new TransformTranslate(-xc, -yc)); + } else + t = TransformMatrix.rotate(w); + break; + default: + throw new SvgException("unknown transform: " + value, null); + } + c.tr = Transform.mul(c.tr, t); + } + + private static Color getColorFromString(String v) { + if (v.equalsIgnoreCase("none")) + return null; + + if (v.startsWith("#")) + return Color.decode(v); + + try { + return (Color) Color.class.getField(v).get(null); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + return Color.BLACK; + } + } + + private static float getFloatFromString(String inp) { + inp = inp.replaceAll("[^0-9.]", ""); + if (inp.isEmpty()) + return 1; + return Float.parseFloat(inp); + } + +} diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgException.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgException.java new file mode 100644 index 000000000..ed9dbd95e --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgException.java @@ -0,0 +1,21 @@ +/* + * 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.shapes.custom.svg; + +/** + * Exception thrown if svg could not be parsed. + */ +public class SvgException extends Exception { + /** + * Creates a new instance + * + * @param message the message + * @param cause the cause + */ + public SvgException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgImporter.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgImporter.java new file mode 100644 index 000000000..257cc6464 --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgImporter.java @@ -0,0 +1,267 @@ +/* + * 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.shapes.custom.svg; + +import de.neemann.digital.draw.graphics.*; +import de.neemann.digital.draw.shapes.custom.CustomShapeDescription; +import de.neemann.digital.lang.Lang; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import static de.neemann.digital.draw.shapes.GenericShape.SIZE; + +/** + * Helper to import an SVG file + */ +public class SvgImporter { + private final Document svg; + + /** + * Create a new importer instance + * + * @param in the svg file to import + * @throws IOException IOException + */ + public SvgImporter(InputStream in) throws IOException { + try { + svg = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); + } catch (Exception e) { + throw new IOException(Lang.get("err_parsingSVG"), e); + } + } + + /** + * Create a new importer instance + * + * @param svgFile the svg file to import + * @throws IOException IOException + */ + public SvgImporter(File svgFile) throws IOException { + if (!svgFile.exists()) + throw new FileNotFoundException(svgFile.getPath()); + try { + svg = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(svgFile); + } catch (Exception e) { + throw new IOException(Lang.get("err_parsingSVG"), e); + } + } + + /** + * Parses and draws the svg file. + * + * @return the custom shape description + * @throws SvgException SvgException + */ + public CustomShapeDescription create() throws SvgException { + NodeList gList = svg.getElementsByTagName("svg").item(0).getChildNodes(); + Context c = new Context(); + try { + CustomShapeDescription csd = new CustomShapeDescription(); + create(csd, gList, c); + return csd; + } catch (RuntimeException e) { + throw new SvgException(Lang.get("err_parsingSVG"), e); + } + } + + private void create(CustomShapeDescription csd, NodeList gList, Context c) throws SvgException { + for (int i = 0; i < gList.getLength(); i++) { + final Node node = gList.item(i); + if (node instanceof Element) + create(csd, (Element) node, c); + } + } + + private void create(CustomShapeDescription csd, Element element, Context parent) throws SvgException { + Context c = new Context(parent, element); + switch (element.getNodeName()) { + case "a": + case "g": + create(csd, element.getChildNodes(), c); + break; + case "line": + csd.addLine( + c.v(element.getAttribute("x1"), element.getAttribute("y1")).round(), + c.v(element.getAttribute("x2"), element.getAttribute("y2")).round(), + c.getThickness(), c.getColor()); + break; + case "rect": + drawRect(csd, element, c); + break; + case "path": + try { + final Polygon d = new PolygonParser(element.getAttribute("d")).create(); + if (d != null) + d.setEvenOdd(c.isFillRuleEvenOdd()); + + drawPolygon(csd, c, d); + } catch (PolygonParser.ParserException e) { + throw new SvgException("invalid path", e); + } + break; + case "polygon": + try { + drawPolygon(csd, c, new PolygonParser(element.getAttribute("points")).parsePolygon()); + } catch (PolygonParser.ParserException e) { + throw new SvgException("invalid points", e); + } + break; + case "polyline": + try { + drawPolygon(csd, c, new PolygonParser(element.getAttribute("points")).parsePolyline()); + } catch (PolygonParser.ParserException e) { + throw new SvgException("invalid points", e); + } + break; + case "circle": + case "ellipse": + drawCircle(csd, element, c); + break; + case "text": + drawText(csd, c, element); + break; + } + } + + private void drawPolygon(CustomShapeDescription csd, Context c, Polygon polygon) { + if (c.getFilled() != null) + csd.addPolygon(polygon.transform(c.getTransform()), c.getThickness(), c.getFilled(), true); + if (c.getColor() != null) + csd.addPolygon(polygon.transform(c.getTransform()), c.getThickness(), c.getColor(), false); + } + + private VectorFloat vec(String xStr, String yStr) { + float x = xStr.isEmpty() ? 0 : Float.parseFloat(xStr); + float y = yStr.isEmpty() ? 0 : Float.parseFloat(yStr); + return new VectorFloat(x, y); + } + + private void drawRect(CustomShapeDescription csd, Element element, Context c) { + VectorInterface size = vec(element.getAttribute("width"), element.getAttribute("height")); + VectorInterface pos = vec(element.getAttribute("x"), element.getAttribute("y")); + VectorInterface rad = vec(element.getAttribute("rx"), element.getAttribute("ry")); + + final Polygon polygon; + + float x = pos.getXFloat(); + float y = pos.getYFloat(); + float width = size.getXFloat(); + float height = size.getYFloat(); + if (rad.getXFloat() * rad.getYFloat() != 0) { + float rx = rad.getXFloat(); + float ry = rad.getYFloat(); + float w = size.getXFloat() - 2 * rx; + float h = size.getYFloat() - 2 * ry; + + double f = 4 * (Math.sqrt(2) - 1) / 3; + float cx = (float) (f * rx); + float cy = (float) (f * ry); + + polygon = new Polygon(true) + .add(c.v(x + rx + w, y)) + .add(c.v(x + rx + w + cx, y), c.v(x + width, y + ry - cy), c.v(x + width, y + ry)) + .add(c.v(x + width, y + ry + h)) + .add(c.v(x + width, y + ry + h + cy), c.v(x + rx + w + cx, y + height), c.v(x + rx + w, y + height)) + .add(c.v(x + rx, y + height)) + .add(c.v(x + rx - cx, y + height), c.v(x, y + ry + h + cy), c.v(x, y + ry + h)) + .add(c.v(x, y + ry)) + .add(c.v(x, y + ry - cy), c.v(x + rx - cx, y), c.v(x + rx, y)); + } else + polygon = new Polygon(true) + .add(c.v(x, y)) + .add(c.v(x + width, y)) + .add(c.v(x + width, y + height)) + .add(c.v(x, y + height)); + + if (c.getFilled() != null) + csd.addPolygon(polygon, c.getThickness(), c.getFilled(), true); + if (c.getColor() != null) + csd.addPolygon(polygon, c.getThickness(), c.getColor(), false); + } + + private void drawCircle(CustomShapeDescription csd, Element element, Context c) { + if (element.hasAttribute("id")) { + VectorInterface pos = c.v(element.getAttribute("cx"), element.getAttribute("cy")); + String id = element.getAttribute("id"); + if (id.startsWith("pin:")) { + csd.addPin(id.substring(4).trim(), toGrid(pos), false); + return; + } else if (id.startsWith("pin+:")) { + csd.addPin(id.substring(5).trim(), toGrid(pos), true); + return; + } + } + + VectorFloat r = null; + if (element.hasAttribute("r")) { + final String rad = element.getAttribute("r"); + r = vec(rad, rad); + } + if (element.hasAttribute("rx")) { + r = vec(element.getAttribute("rx"), element.getAttribute("ry")); + } + if (r != null) { + VectorFloat pos = vec(element.getAttribute("cx"), element.getAttribute("cy")); + float x = pos.getXFloat(); + float y = pos.getYFloat(); + float rx = r.getXFloat(); + float ry = r.getYFloat(); + + double f = 4 * (Math.sqrt(2) - 1) / 3; + float cx = (float) (f * rx); + float cy = (float) (f * ry); + + Polygon poly = new Polygon(true) + .add(c.v(x - rx, y)) + .add(c.v(x - rx, y + cy), c.v(x - cx, y + ry), c.v(x, y + ry)) + .add(c.v(x + cx, y + ry), c.v(x + rx, y + cy), c.v(x + rx, y)) + .add(c.v(x + rx, y - cy), c.v(x + cx, y - ry), c.v(x, y - ry)) + .add(c.v(x - cx, y - ry), c.v(x - rx, y - cy), c.v(x - rx, y)); + + if (c.getFilled() != null) + csd.addPolygon(poly, c.getThickness(), c.getFilled(), true); + if (c.getColor() != null) + csd.addPolygon(poly, c.getThickness(), c.getColor(), false); + } + } + + private Vector toGrid(VectorInterface pos) { + return new Vector(Math.round(pos.getXFloat() / SIZE) * SIZE, Math.round(pos.getYFloat() / SIZE) * SIZE); + } + + private void drawText(CustomShapeDescription csd, Context c, Element element) throws SvgException { + VectorFloat p = vec(element.getAttribute("x"), element.getAttribute("y")); + VectorInterface pos0 = p.transform(c.getTransform()); + VectorInterface pos1 = p.add(new VectorFloat(1, 0)).transform(c.getTransform()); + + drawTextElement(csd, c, element, pos0, pos1); + } + + private void drawTextElement(CustomShapeDescription csd, Context c, Element element, VectorInterface pos0, VectorInterface pos1) throws SvgException { + NodeList nodes = element.getElementsByTagName("*"); + if (nodes.getLength() == 0) { + String text = element.getTextContent(); + csd.addText(pos0.round(), pos1.round(), text, c.getTextOrientation(), (int) c.getFontSize(), c.getColor()); + } else { + for (int i = 0; i < nodes.getLength(); i++) { + Node n = nodes.item(i); + if (n instanceof Element) { + Element el = (Element) n; + drawTextElement(csd, new Context(c, el), el, pos0, pos1); + } + } + } + } + +} diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgTemplate.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgTemplate.java new file mode 100644 index 000000000..215f23636 --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/SvgTemplate.java @@ -0,0 +1,100 @@ +/* + * 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.shapes.custom.svg; + +import de.neemann.digital.core.ObservableValues; +import de.neemann.digital.core.element.Keys; +import de.neemann.digital.core.element.PinDescription; +import de.neemann.digital.draw.elements.Circuit; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +import static de.neemann.digital.draw.shapes.GenericShape.SIZE; + +/** + * Used to create a SVG template + */ +public class SvgTemplate implements Closeable { + private final Writer w; + private final PinDescription[] inputs; + private final ObservableValues outputs; + private final int width; + private final int height; + + /** + * Creates a new instance + * + * @param file the file to create + * @param circuit the circuit + * @throws Exception Exception + */ + public SvgTemplate(File file, Circuit circuit) throws Exception { + this(new FileOutputStream(file), circuit); + } + + /** + * Creates a new instance + * + * @param outputStream the stream to write to + * @param circuit the circuit + * @throws Exception Exception + */ + public SvgTemplate(OutputStream outputStream, Circuit circuit) throws Exception { + width = circuit.getAttributes().get(Keys.WIDTH) * SIZE; + inputs = circuit.getInputNames(); + outputs = circuit.getOutputNames(); + height = Math.max(inputs.length, outputs.size()) * SIZE; + int border = SIZE * 4; + + + w = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + w.write("\n" + + "\n" + + " \n" + + " \n" + + " \n"); + } + + /** + * Creates the template + * + * @throws Exception Exception + */ + public void create() throws Exception { + w.write(" \n"); + + int y = 0; + for (PinDescription i : inputs) { + w.write(" \n"); + y += 20; + } + y = 0; + for (PinDescription o : outputs) { + w.write(" \n"); + y += 20; + } + } + + @Override + public void close() throws IOException { + w.write("\n"); + w.close(); + } +} diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/package-info.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/package-info.java new file mode 100644 index 000000000..d0892bfc2 --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/package-info.java @@ -0,0 +1,13 @@ +/* + * 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. + */ + +/** + * Simple importer for svg files. + * A pin is specified by a circle which has an id of the form "pin:[name]" or "pin+:[name]". + * The later one enables the pin label in the shape. + * In this case the circle itself is ignored. + */ +package de.neemann.digital.draw.shapes.custom.svg; diff --git a/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java b/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java index a4a7267c5..90c99be9d 100644 --- a/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java +++ b/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java @@ -65,8 +65,7 @@ public class CircuitComponent extends JComponent implements Circuit.ChangedListe static { ATTR_LIST.add(Keys.WIDTH); ATTR_LIST.add(Keys.SHAPE_TYPE); - if (Main.isExperimentalMode()) - ATTR_LIST.add(Keys.CUSTOM_SHAPE); + ATTR_LIST.add(Keys.CUSTOM_SHAPE); ATTR_LIST.add(Keys.HEIGHT); ATTR_LIST.add(Keys.PINCOUNT); ATTR_LIST.add(Keys.BACKGROUND_COLOR); diff --git a/src/main/java/de/neemann/digital/gui/components/CustomShapeEditor.java b/src/main/java/de/neemann/digital/gui/components/CustomShapeEditor.java index cb5457f64..f14fcbb31 100644 --- a/src/main/java/de/neemann/digital/gui/components/CustomShapeEditor.java +++ b/src/main/java/de/neemann/digital/gui/components/CustomShapeEditor.java @@ -8,12 +8,21 @@ package de.neemann.digital.gui.components; import de.neemann.digital.core.element.ElementAttributes; import de.neemann.digital.core.element.Key; import de.neemann.digital.draw.shapes.custom.CustomShapeDescription; +import de.neemann.digital.draw.shapes.custom.svg.SvgException; +import de.neemann.digital.draw.shapes.custom.svg.SvgImporter; +import de.neemann.digital.draw.shapes.custom.svg.SvgTemplate; +import de.neemann.digital.gui.Main; +import de.neemann.digital.gui.SaveAsHelper; import de.neemann.digital.lang.Lang; +import de.neemann.gui.ErrorMessage; +import de.neemann.gui.MyFileChooser; import de.neemann.gui.ToolTipAction; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; +import java.io.File; +import java.io.IOException; /** * Editor used to define a custom shape. @@ -23,6 +32,8 @@ public class CustomShapeEditor extends EditorFactory.LabelEditorSetzen Bearbeitet den Inhalt des ausgewählten ROM/EEPROM. Entfernt die hier gespeicherten Daten des ausgewählten ROM. Es wird stattdessen der direkt im ROM gespeicherte Inhalt verwendet. + Template + Ein SVG Template erzeugen, welches dann mit z.B. Inkscape bearbeitet werden kann. + Import + Importieren einer SVG-Datei. Um eine geeignete SVG Datei zu erstellen, ist es + am einfachsten zunächst ein Template zu erzeugen und dieses dann zu bearbeiten. Warnung Abbrechen Digital @@ -957,6 +962,7 @@ Sind evtl. die Namen der Variablen nicht eindeutig? Kein Programmspeicher im Modell gefunden! Ein Programmspeicher muss gewählt werden! Mehr als einen Programmspeicher gefunden. Es darf nur einen Programmspeicher geben. Fehler beim Laden des Programmspeichers. + Fehler beim Laden der SVG-Datei. Adress-Bits Anzahl der Adress-Bits, die verwendet werden. @@ -1582,6 +1588,8 @@ Stellen Sie sicher, dass der Flash-Vorgang abgeschlossen ist, bevor Sie diesen D {0} Testzeilen überprüft Testdatei Wahrheitstabelle + Fehler beim Import der SVG-Datei. + Fehler beim Erzeugen der SVG-Datei. Ok diff --git a/src/main/resources/lang/lang_en.xml b/src/main/resources/lang/lang_en.xml index acd44f84a..b50b8ffbe 100644 --- a/src/main/resources/lang/lang_en.xml +++ b/src/main/resources/lang/lang_en.xml @@ -45,6 +45,11 @@ Apply Edits the content of the selected ROM/EEPROM Removes the stored data for the selected ROM. The content which is stored in the ROM directly is used instead. + Template + Creates an SVG template which can then be edited with Inkscape. + Import + Import an SVG file. To create a suitable SVG file, it is easiest to first create + a SVG template and then edit it. Warning Cancel Digital @@ -954,6 +959,7 @@ No program memory found! The program memory needs to be flagged as such. More then one program memories found! Only one program memory must be flages as such. Error loading the program memory. + Error while reading the SVG file. Address Bits Number of address bits used. @@ -1570,6 +1576,8 @@ Make sure the flash process is complete before closing this dialog! {0} test rows passed File Tested Truth Table + Error while importing the SVG file. + Error creating the SVG template. OK diff --git a/src/test/java/de/neemann/digital/draw/graphics/PolygonTest.java b/src/test/java/de/neemann/digital/draw/graphics/PolygonTest.java index 03c52bba9..43e81df3c 100644 --- a/src/test/java/de/neemann/digital/draw/graphics/PolygonTest.java +++ b/src/test/java/de/neemann/digital/draw/graphics/PolygonTest.java @@ -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 Q 30,30 60,0 Q 90,-30 120,0 Q 150,30 180,0 Q 210,-30 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 Q 0,60 60,60 Q 120,60 120,0 Q 120,-60 60,-60 Q 0,-60 0,0 Z M 30,0 Q 30,30 60,30 Q 90,30 90,0 Q 90,-30 60,-30 Q 30,-30 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()); + } + } \ No newline at end of file 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..4632f0db9 --- /dev/null +++ b/src/test/java/de/neemann/digital/draw/graphics/TransformMatrixTest.java @@ -0,0 +1,44 @@ +/* + * 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; + +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/graphics/linemerger/GraphicLineCollectorTest.java b/src/test/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollectorTest.java new file mode 100644 index 000000000..4c174b3fd --- /dev/null +++ b/src/test/java/de/neemann/digital/draw/graphics/linemerger/GraphicLineCollectorTest.java @@ -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 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 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 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 poly; + + private MyGraphic(ArrayList 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) { + } + } +} \ 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 new file mode 100644 index 000000000..4b1ff7a37 --- /dev/null +++ b/src/test/java/de/neemann/digital/draw/shapes/custom/SvgImporterTest.java @@ -0,0 +1,444 @@ +/* + * 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.shapes.custom; + +import de.neemann.digital.draw.elements.PinException; +import de.neemann.digital.draw.graphics.Polygon; +import de.neemann.digital.draw.graphics.PolygonParser; +import de.neemann.digital.draw.graphics.VectorInterface; +import de.neemann.digital.draw.shapes.Drawable; +import de.neemann.digital.draw.shapes.custom.svg.SvgException; +import de.neemann.digital.draw.shapes.custom.svg.SvgImporter; +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +public class SvgImporterTest extends TestCase { + + public void testPin() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + " \n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0") + .checkPin(20, 20, "test", false) + .checkPin(0, 40, "test2", true) + .check(); + } + + public void testPath() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0") + .check(); + } + + public void testPathEvenOdd() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0", true) + .check(); + } + + public void testPathEvenOdd2() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0", true) + .check(); + } + + public void testPolyline() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0") + .check(); + } + + public void testPolygon() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L 0,100 L 100,100 L 100,0 Z") + .check(); + } + + public void testPolygonTranslated() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 10,20 L 10,120 L 110,120 L 110,20 Z") + .check(); + } + + public void testPolygonTranslated2() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + " \n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 10,20 L 10,120 L 110,120 L 110,20 Z") + .check(); + } + + public void testPolygonTranslated3() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + " \n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 10,20 L 10,120 L 110,120 L 110,20 Z") + .check(); + } + + public void testPolygonRotate() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 0,0 L -50,86.60254 L 36.60254,136.60254 L 86.60254,50 Z") + .check(); + } + + public void testPolygonRotate2() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 50,-20.710678 L -20.710678,50 L 50,120.71068 L 120.71068,50 Z") + .check(); + } + + public void testPolygonMatrix() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 5,10 L 15,100 L 95,120 L 85,30 Z") + .check(); + } + + public void testRect() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 10,20 L 80,20 L 80,100 L 10,100 Z") + .check(); + } + + public void testRectRound() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 70,20 C 75.52285,20 80,28.954304 80,40 L 80,80 C 80,91.04569 75.52285,100 70,100 L 20,100 C 14.477152,100 10,91.04569 10,80 L 10,40 C 10,28.954304 14.477152,20 20,20 Z") + .check(); + } + + public void testCircle() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 27,68 C 28.656855,82.91169 40.745167,97.686295 54,101 C 67.25484,104.313705 76.65685,94.91169 75,80 C 73.34315,65.08831 61.254833,50.31371 48,47 C 34.745167,43.68629 25.343145,53.08831 27,68 Z") + .check(); + } + + public void testScale() throws IOException, SvgException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + "\n" + + "\n" + + "\n" + + "")).create(); + + new CSDChecker(custom) + .checkLine(20, 60, 160, 210) + .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(); + } + + public void testInkscape3() throws IOException, SvgException, PolygonParser.ParserException, PinException { + CustomShapeDescription custom = new SvgImporter( + in("\n" + + " \n" + + "")).create(); + + new CSDChecker(custom) + .checkPolygon("M 11.217981,5.540678 Q 19.667194,15.0592 25.481504,24.63657 Q 31.29581,34.213936 32.91727,41.2839 Q 33.082184,42.00294 33.19799,42.68264 Q 34.336685,49.36582 30.875134,51.51164 Q 27.413582,53.657448 20.279312,50.690926 Q 19.55373,50.389217 18.799128,50.03573 L -1.9073486E-6,15.498432 Z") + .check(); + } + + + //***************************************************************************************************** + + + private InputStream in(String s) { + return new ByteArrayInputStream(s.getBytes()); + } + + private static class CSDChecker { + private final CustomShapeDescription csd; + private final ArrayList checker; + private final ArrayList pins; + + private CSDChecker(CustomShapeDescription csd) { + this.csd = csd; + this.checker = new ArrayList<>(); + this.pins = new ArrayList<>(); + } + + public void check() throws PinException { + checkShape(); + checkPins(); + } + + private void checkPins() throws PinException { + assertEquals("wrong number of pins", pins.size(), csd.getPinCount()); + for (TestPin tp : pins) { + CustomShapeDescription.Pin p = csd.getPin(tp.name); + assertEquals("wrong pin x coordinate", tp.x, p.getPos().x); + assertEquals("wrong pin y coordinate", tp.y, p.getPos().y); + assertEquals("wrong pin label", tp.showLabel, p.isShowLabel()); + } + } + + private void checkShape() { + int i = 0; + for (Drawable d : csd) { + if (i >= checker.size()) + fail("to much elements found in the csd"); + checker.get(i).check(d); + i++; + } + if (i != checker.size()) + fail("not enough elements found in the csd"); + } + + private CSDChecker checkPolygon(String s) throws PolygonParser.ParserException { + checker.add(new CheckPolygon(new PolygonParser(s).create())); + return this; + } + + private CSDChecker checkPolygon(String s, boolean evenOdd) throws PolygonParser.ParserException { + checker.add(new CheckPolygon(new PolygonParser(s).create().setEvenOdd(evenOdd))); + return this; + } + + private CSDChecker checkPin(int x, int y, String name, boolean showLabel) { + pins.add(new TestPin(x, y, name, showLabel)); + return this; + } + + private CSDChecker checkLine(int x1, int y1, int x2, int y2) { + checker.add(new Checker() { + @Override + public void check(Drawable d) { + 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); + assertEquals(x2, l.getP2().x); + assertEquals(y2, l.getP2().y); + } + }); + return this; + } + + private static class TestPin { + private final int x; + private final int y; + private final String name; + private final boolean showLabel; + + private TestPin(int x, int y, String name, boolean showLabel) { + this.x = x; + this.y = y; + this.name = name; + this.showLabel = showLabel; + } + } + } + + private interface Checker { + void check(Drawable d); + } + + private static class CheckPolygon implements Checker { + + private final Polygon should; + + private CheckPolygon(Polygon should) { + this.should = should; + } + + @Override + public void check(Drawable d) { + assertTrue("no polygon found", d instanceof CustomShapeDescription.PolygonHolder); + final Polygon polygon = ((CustomShapeDescription.PolygonHolder) d).getPolygon(); + assertEquals("wrong evanOdd mode", should.getEvenOdd(), polygon.getEvenOdd()); + + ArrayList shouldPoints = new ArrayList<>(); + should.traverse(shouldPoints::add); + ArrayList isPoints = new ArrayList<>(); + polygon.traverse(isPoints::add); + + //System.out.println(polygon); + + assertEquals("not the correct polygon size", shouldPoints.size(), isPoints.size()); + for (int i = 0; i < shouldPoints.size(); i++) { + VectorInterface sh = shouldPoints.get(i); + VectorInterface is = isPoints.get(i); + assertEquals("x coordinate " + i, sh.getXFloat(), is.getXFloat(), 1e-4); + assertEquals("y coordinate " + i, sh.getYFloat(), is.getYFloat(), 1e-4); + } + } + } + +} \ No newline at end of file diff --git a/src/test/resources/docu/static_de.xml b/src/test/resources/docu/static_de.xml index 0f2c1332c..d41dab9f3 100644 --- a/src/test/resources/docu/static_de.xml +++ b/src/test/resources/docu/static_de.xml @@ -405,7 +405,7 @@ Soll eine Schaltung auf einem BASYS3 Board betrieben werden, wird eine Vivado Projektdatei angelegt, - die direkt mit Vivado geöffnet werden kann. Es läßt sich dann der Bitstream erzeugen und mit dem + die direkt mit Vivado geöffnet werden kann. Es lässt sich dann der Bitstream erzeugen und mit dem Hardware-Manager kann dieser in ein BASYS3 Board übertragen werden. @@ -414,6 +414,49 @@ + + + Digital bietet zwar einige Optionen, die die Erscheinungsform einer Schaltung festlegen, wenn diese + in eine andere eingebettet wird, in manchen Fällen kann es jedoch sinnvoll sein, ein spezielles + eigenes Symbol für eine Teilschaltung zu verwenden. + Ein Beispiel ist die Darstellung der ALU in dem Prozessor, welcher in den Beispielen enthalten ist. + In diesem Kapitel wird erklärt, wie eine solches spezielles Symbol für eine Schaltung definiert wird. + + + Digital bietet keinen Editor für das Erstellen eines speziellen Symbols an. Statt dessen ist ein kleiner + Umweg für das Erstellen von Schaltungssymbolen erforderlich: + Zunächst wird die Schaltung geöffnet, welche durch ein spezielles Symbol repräsentiert werden soll. + Dann wird für diese Schaltung ein SVG-Template erstellt. In diesem Template wird die Schaltung durch + ein einfaches Rechteck repräsentiert. Es finden sich darin auch alle Pins der Schaltung, + repräsentiert durch blaue (Eingänge) und rote (Ausgänge) Kreise. + Um zu sehen, welcher Kreis zu welchem Pin gehört, kann man sich in den Objekteigenschaften die ID des + Kreises ansehen. Diese ID hat die Form pin:[Name] oder pin+:[Name]. Bei der letzteren + Variante wird der Pin beim Reimport zu Digital mit einem Label versehen. Wünscht man kein solches Label, + kann das + entfernt werden. + + + Diese SVG-Datei kann jetzt bearbeitet werden. Am besten geeignet ist das Open-Source Programm + Inkscape welches kostenlos verfügbar ist. + Die Pins können frei verschoben werden, werden jedoch beim Reimport auf den nächstgelegenen Rasterpunkt + verschoben. + + + Wenn schon existierende SVG Dateien verwendet werden sollen, ist es das einfachste das erstellte + Template zu öffnen, und die bereits vorhandene Grafik per Copy&Paste in das Template einzufügen. + + + Wenn die Datei gespeichert wurde, kann diese mit Digital importiert werden. Dabei wird die Datei + eingelesen und alle notwendigen Informationen werden extrahiert und in der Schaltung gespeichert. + Für die weitere Verwendung der Schaltung ist die SVG-Datei also nicht mehr erforderlich. + + + Noch eine Anmerkung: SVG ist ein sehr mächtiges und flexibles Dateiformat. Es können extrem + komplexe Grafiken damit beschrieben werden. Der in Digital integrierte Importer ist nicht in der + Lage alle denkbaren SVG-Dateien fehlerfrei zu importieren. Wenn eine Datei nicht importiert werden kann, + oder nicht so erscheint wie erwartet, ist evtl. einiges Experimentieren erforderlich, bevor das + gewünschte Ergebnis erreicht ist. + + Wie kann ich eine Leitung verschieben? diff --git a/src/test/resources/docu/static_en.xml b/src/test/resources/docu/static_en.xml index 4109fe3e3..ae3b5acbb 100644 --- a/src/test/resources/docu/static_en.xml +++ b/src/test/resources/docu/static_en.xml @@ -388,6 +388,44 @@ + + + Although Digital has some options that determine the appearance of a circuit when it is embedded in + another, in some cases it may be useful to use a very special shape for a subcircuit. An example is + the representation of the ALU in the processor included in the examples. This chapter explains how to + define such a special shape for a circuit. + + + Digital does not provide an editor for creating a special shape. Instead, a small detour is required + for creating circuit shapes: First, the circuit is opened, which is to be represented by a special shape. + Then an SVG template is created for this circuit. In this template, the circuit is represented by a + simple rectangle. It also contains all the pins of the circuit, represented by blue (inputs) and + red (outputs) circuits. To see which circle belongs to which pin, you can look at the ID of the + circle in the object properties. This ID has the form pin:[name] or pin+:[name]. + In the latter variant, the pin is provided with a label if reimported to digital. + If you do not want such a label, the + can be removed. + + + This SVG file can now be edited. The most suitable is the open source program + Inkscape which is available for free. + The pins can be moved freely, but are moved to the next grid point during the reimport. + + + If existing SVG files are to be used, it is easiest to open the created template and paste the + existing graphic into the template via Copy&Paste. + + + If the file was saved, it can be imported with Digital. The file is read in and all necessary + information is extracted and stored in the circuit. For further use of the circuit, the SVG + file is no longer required. + + + A final remark: SVG is a very powerful and flexible file format. + It can be used to describe extremely complex graphics. The Digital importer is not able to import all + possible SVG files without errors. If a file can not be imported, or does not appear as expected, + some experimentation may be required before the desired result is achieved. + + How to move a wire?