added a SVG importer, closes #39

This commit is contained in:
hneemann 2018-12-01 15:23:39 +01:00
commit c3b43b753a
28 changed files with 1955 additions and 157 deletions

View File

@ -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.

View File

@ -4,7 +4,7 @@
<attributes>
<entry>
<string>shapeType</string>
<de.neemann.digital.draw.shapes.CustomCircuitShapeType>CUSTOM</de.neemann.digital.draw.shapes.CustomCircuitShapeType>
<shapeType>CUSTOM</shapeType>
</entry>
<entry>
<string>Description</string>
@ -85,7 +85,7 @@ bei der Speicheradressierung verwendet.}}</string>
</pins>
<drawables>
<poly>
<poly path="M 0.0 -30.0 L 80.0 10.0 L 80.0 90.0 L 0.0 130.0 L 0.0 65.0 L 30.0 50.0 L 0.0 35.0 z"/>
<poly path="M 0,-30 L 80,10 L 80,90 L 0,130 L 0,65 L 30,50 L 0,35 Z" evenOdd="false"/>
<thickness>1</thickness>
<filled>true</filled>
<color>
@ -96,7 +96,7 @@ bei der Speicheradressierung verwendet.}}</string>
</color>
</poly>
<poly>
<poly path="M 0.0 -30.0 L 80.0 10.0 L 80.0 90.0 L 0.0 130.0 L 0.0 65.0 L 30.0 50.0 L 0.0 35.0 z"/>
<poly reference="../../poly/poly"/>
<thickness>4</thickness>
<filled>false</filled>
<color>

View File

@ -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

View File

@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<!-- Created with Digital by H.Neemann -->\n");
w.write("<!-- created: " + new Date() + " -->\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("<path d=\"M " + str(p.get(0)));
for (int i = 1; i < p.size(); i++)
if (p.isBezierStart(i)) {
w.write(" C " + str(p.get(i)) + " " + str(p.get(i + 1)) + " " + str(p.get(i + 2)));
i += 2;
} else
w.write(" L " + str(p.get(i)));
//CHECKSTYLE.ON: ModifiedControlVariable
if (p.isClosed())
w.write(" Z");
w.write("\"");
w.write("<path d=\"" + p + "\"");
addStrokeDash(w, style.getDash());
if (p.getEvenOdd() && style.isFilled())
w.write(" fill-rule=\"evenodd\"");
if (style.isFilled() && p.isClosed())
w.write(" stroke=\"" + getColor(style) + "\" stroke-width=\"" + getStrokeWidth(style) + "\" fill=\"" + getColor(style) + "\" fill-opacity=\"" + getOpacity(style) + "\"/>\n");
else

View File

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

View File

@ -5,18 +5,19 @@
*/
package de.neemann.digital.draw.graphics;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
/**
* A polygon representation used by the {@link Graphic} interface.
*/
public class Polygon implements Iterable<VectorInterface> {
public class Polygon implements Iterable<Polygon.PathElement> {
private final ArrayList<VectorInterface> points;
private final HashSet<Integer> isBezierStart;
private final ArrayList<PathElement> path;
private boolean closed;
private boolean hasSpecialElements = false;
private boolean evenOdd;
/**
* Creates e new closed polygon
@ -41,9 +42,10 @@ public class Polygon implements Iterable<VectorInterface> {
* @param closed true if polygon is closed
*/
public Polygon(ArrayList<VectorInterface> points, boolean closed) {
this.points = points;
this.closed = closed;
isBezierStart = new HashSet<>();
this.path = new ArrayList<>();
for (VectorInterface p : points)
add(p);
}
/**
@ -71,10 +73,17 @@ public class Polygon implements Iterable<VectorInterface> {
* @return this for chained calls
*/
public Polygon add(VectorInterface p) {
points.add(p);
if (path.isEmpty())
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<VectorInterface> {
* @return this for chained calls
*/
public Polygon add(VectorInterface c1, VectorInterface c2, VectorInterface p) {
if (points.size() == 0)
if (path.size() == 0)
throw new RuntimeException("cubic bezier curve is not allowed to be the first path element");
isBezierStart.add(points.size());
points.add(c1);
points.add(c2);
points.add(p);
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<VectorInterface> 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<VectorInterface> {
}
private boolean check(VectorInterface p1, VectorInterface p2) {
if (closed)
return false;
if (p1.equals(getFirst())) {
points.add(0, p2);
if (p2.equals(getLast()))
closed = true;
else {
removeInitialMoveTo();
path.add(0, new MoveTo(p2));
}
return true;
} else if (p1.equals(getLast())) {
points.add(p2);
if (p2.equals(getFirst()))
closed = true;
else
path.add(new LineTo(p2));
return true;
} else
return false;
}
private void removeInitialMoveTo() {
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<VectorInterface> {
* @return this for chained calls
*/
public Polygon append(Polygon p2) {
if (!p2.isBezierStart.isEmpty())
if (hasSpecialElements || p2.hasSpecialElements)
throw new RuntimeException("append of bezier not supported");
for (int i = 1; i < p2.points.size(); i++)
points.add(p2.points.get(i));
if (p2.getLast().equals(getFirst())) {
for (int i = 1; i < p2.path.size() - 1; i++)
add(p2.path.get(i).getPoint());
closed = true;
} else {
for (int i = 1; i < p2.path.size(); i++)
add(p2.path.get(i).getPoint());
}
return this;
}
@ -184,11 +232,11 @@ public class Polygon implements Iterable<VectorInterface> {
* @return returns this polygon with reverse order of points
*/
public Polygon reverse() {
if (!isBezierStart.isEmpty())
throw new RuntimeException("reverse of bezier not supported");
if (hasSpecialElements)
throw new RuntimeException("append of bezier not supported");
Polygon p = new Polygon(closed);
for (int i = points.size() - 1; i >= 0; i--)
p.add(points.get(i));
for (int i = path.size() - 1; i >= 0; i--)
p.add(path.get(i).getPoint());
return p;
}
@ -202,43 +250,24 @@ public class Polygon implements Iterable<VectorInterface> {
if (transform == Transform.IDENTITY)
return this;
Polygon p = new Polygon(closed);
for (VectorInterface v : points)
p.add(v.transform(transform));
p.isBezierStart.addAll(isBezierStart);
Polygon p = new Polygon(closed).setEvenOdd(evenOdd);
for (PathElement pe : path)
p.add(pe.transform(transform));
return p;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("M ");
VectorInterface v = points.get(0);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
//modification of loop variable i is intended!
//CHECKSTYLE.OFF: ModifiedControlVariable
for (int i = 1; i < points.size(); i++) {
v = points.get(i);
if (isBezierStart.contains(i)) {
sb.append("C ").append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
v = points.get(i + 1);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
v = points.get(i + 2);
sb.append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
i += 2;
} else
sb.append("L ").append(str(v.getXFloat())).append(",").append(str(v.getYFloat())).append(" ");
}
//CHECKSTYLE.ON: ModifiedControlVariable
if (closed)
sb.append("z");
return sb.toString();
StringBuilder sb = new StringBuilder();
for (PathElement pe : path) {
if (sb.length() > 0)
sb.append(' ');
sb.append(pe.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<VectorInterface> {
void setClosed(boolean closed) {
this.closed = closed;
}
@Override
public Iterator<PathElement> 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) {
}
}
}

View File

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

View File

@ -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;
}
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);
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) {
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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

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

View File

@ -147,6 +147,13 @@ public class CustomShapeDescription implements Iterable<Drawable> {
.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<Drawable> {
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<Drawable> {
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;
}
}
/**

View File

@ -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<String, AttrParser> 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<svg\n"
+ " xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"
+ " xmlns:cc=\"http://creativecommons.org/ns#\"\n"
+ " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
+ " xmlns:svg=\"http://www.w3.org/2000/svg\"\n"
+ " xmlns=\"http://www.w3.org/2000/svg\"\n"
+ " xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\n"
+ " xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"\n"
+ " viewBox=\"-" + border + " -" + border + " " + (width + border * 2) + " " + (height + border * 2) + "\"\n"
+ " version=\"1.1\">\n"
+ " <sodipodi:namedview showgrid=\"true\">\n"
+ " <inkscape:grid\n"
+ " type=\"xygrid\"\n"
+ " empspacing=\"4\"\n"
+ " spacingx=\"5\"\n"
+ " spacingy=\"5\" />\n"
+ " </sodipodi:namedview>\n");
}
/**
* Creates the template
*
* @throws Exception Exception
*/
public void create() throws Exception {
w.write(" <rect fill=\"none\" stroke=\"black\" stroke-width=\"3\" x=\"0\" y=\"-10\" width=\"" + width + "\" height=\"" + height + "\"/>\n");
int y = 0;
for (PinDescription i : inputs) {
w.write(" <circle fill=\"blue\" id=\"pin+:" + i.getName() + "\" cx=\"0\" cy=\"" + y + "\" r=\"3\"/>\n");
y += 20;
}
y = 0;
for (PinDescription o : outputs) {
w.write(" <circle fill=\"red\" id=\"pin+:" + o.getName() + "\" cx=\"" + width + "\" cy=\"" + y + "\" r=\"3\"/>\n");
y += 20;
}
}
@Override
public void close() throws IOException {
w.write("</svg>\n");
w.close();
}
}

View File

@ -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;

View File

@ -65,7 +65,6 @@ 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.HEIGHT);
ATTR_LIST.add(Keys.PINCOUNT);

View File

@ -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.LabelEditor<CustomShapeDesc
private CustomShapeDescription customShapeDescription;
private ToolTipAction clear;
private ToolTipAction load;
private ToolTipAction template;
private static File lastSVGFile;
/**
* Creates a new instance
@ -44,13 +55,51 @@ public class CustomShapeEditor extends EditorFactory.LabelEditor<CustomShapeDesc
}
};
panel.add(clear.createJButton());
load = new ToolTipAction(Lang.get("btn_load")) {
load = new ToolTipAction(Lang.get("btn_loadSvg")) {
@Override
public void actionPerformed(ActionEvent e) {
customShapeDescription = CustomShapeDescription.createDummy();
File path = null;
if (lastSVGFile != null)
path = lastSVGFile.getParentFile();
JFileChooser fc = new MyFileChooser(path);
if (lastSVGFile != null)
fc.setSelectedFile(lastSVGFile);
if (fc.showOpenDialog(getAttributeDialog()) == JFileChooser.APPROVE_OPTION) {
lastSVGFile = fc.getSelectedFile();
try {
customShapeDescription = new SvgImporter(fc.getSelectedFile()).create();
} catch (IOException | SvgException e1) {
new ErrorMessage(Lang.get("msg_errorImportingSvg")).addCause(e1).show(getAttributeDialog());
}
};
}
}
}.setToolTip(Lang.get("btn_loadSvg_tt"));
panel.add(load.createJButton());
template = new ToolTipAction(Lang.get("btn_saveTemplate")) {
@Override
public void actionPerformed(ActionEvent e) {
File path = null;
if (lastSVGFile != null)
path = lastSVGFile.getParentFile();
JFileChooser fc = new MyFileChooser(path);
if (fc.showSaveDialog(getAttributeDialog()) == JFileChooser.APPROVE_OPTION) {
try {
final Main main = getAttributeDialog().getMain();
if (main != null) {
File file = SaveAsHelper.checkSuffix(fc.getSelectedFile(), "svg");
lastSVGFile = file;
try (SvgTemplate tc = new SvgTemplate(file, main.getCircuitComponent().getCircuit())) {
tc.create();
}
}
} catch (Exception e1) {
new ErrorMessage(Lang.get("msg_errorCreatingSvgTemplate")).addCause(e1).show(getAttributeDialog());
}
}
}
}.setToolTip(Lang.get("btn_saveTemplate_tt"));
panel.add(template.createJButton());
return panel;
}
@ -59,6 +108,7 @@ public class CustomShapeEditor extends EditorFactory.LabelEditor<CustomShapeDesc
super.setEnabled(enabled);
load.setEnabled(enabled);
clear.setEnabled(enabled);
template.setEnabled(enabled);
}
@Override

View File

@ -45,6 +45,11 @@
<string name="btn_apply">Setzen</string>
<string name="btn_editRom_tt">Bearbeitet den Inhalt des ausgewählten ROM/EEPROM.</string>
<string name="btn_clearRom_tt">Entfernt die hier gespeicherten Daten des ausgewählten ROM. Es wird stattdessen der direkt im ROM gespeicherte Inhalt verwendet.</string>
<string name="btn_saveTemplate">Template</string>
<string name="btn_saveTemplate_tt">Ein SVG Template erzeugen, welches dann mit z.B. Inkscape bearbeitet werden kann.</string>
<string name="btn_loadSvg">Import</string>
<string name="btn_loadSvg_tt">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.</string>
<string name="msg_warning">Warnung</string>
<string name="cancel">Abbrechen</string>
<string name="digital">Digital</string>
@ -957,6 +962,7 @@ Sind evtl. die Namen der Variablen nicht eindeutig?</string>
<string name="err_noRomFound">Kein Programmspeicher im Modell gefunden! Ein Programmspeicher muss gewählt werden!</string>
<string name="err_moreThenOneRomFound">Mehr als einen Programmspeicher gefunden. Es darf nur einen Programmspeicher geben.</string>
<string name="err_errorLoadingRomData">Fehler beim Laden des Programmspeichers.</string>
<string name="err_parsingSVG">Fehler beim Laden der SVG-Datei.</string>
<string name="key_AddrBits">Adress-Bits</string><!-- ROM, RAMDualPort, RAMSinglePort, RAMSinglePortSel, EEPROM -->
<string name="key_AddrBits_tt">Anzahl der Adress-Bits, die verwendet werden.</string>
@ -1582,6 +1588,8 @@ Stellen Sie sicher, dass der Flash-Vorgang abgeschlossen ist, bevor Sie diesen D
<string name="msg_testPassed_N">{0} Testzeilen überprüft</string>
<string name="msg_testFile">Testdatei</string>
<string name="msg_truthTable">Wahrheitstabelle</string>
<string name="msg_errorImportingSvg">Fehler beim Import der SVG-Datei.</string>
<string name="msg_errorCreatingSvgTemplate">Fehler beim Erzeugen der SVG-Datei.</string>
<string name="ok">Ok</string>
<string name="rot_0"></string>

View File

@ -45,6 +45,11 @@
<string name="btn_apply">Apply</string>
<string name="btn_editRom_tt">Edits the content of the selected ROM/EEPROM</string>
<string name="btn_clearRom_tt">Removes the stored data for the selected ROM. The content which is stored in the ROM directly is used instead.</string>
<string name="btn_saveTemplate">Template</string>
<string name="btn_saveTemplate_tt">Creates an SVG template which can then be edited with Inkscape.</string>
<string name="btn_loadSvg">Import</string>
<string name="btn_loadSvg_tt">Import an SVG file. To create a suitable SVG file, it is easiest to first create
a SVG template and then edit it.</string>
<string name="msg_warning">Warning</string>
<string name="cancel">Cancel</string>
<string name="digital">Digital</string>
@ -954,6 +959,7 @@
<string name="err_noRomFound">No program memory found! The program memory needs to be flagged as such.</string>
<string name="err_moreThenOneRomFound">More then one program memories found! Only one program memory must be flages as such.</string>
<string name="err_errorLoadingRomData">Error loading the program memory.</string>
<string name="err_parsingSVG">Error while reading the SVG file.</string>
<string name="key_AddrBits">Address Bits</string><!-- ROM, RAMDualPort, RAMSinglePort, RAMSinglePortSel, EEPROM -->
<string name="key_AddrBits_tt">Number of address bits used.</string>
@ -1570,6 +1576,8 @@ Make sure the flash process is complete before closing this dialog!</string>
<string name="msg_testPassed_N">{0} test rows passed</string>
<string name="msg_testFile">File Tested</string>
<string name="msg_truthTable">Truth Table</string>
<string name="msg_errorImportingSvg">Error while importing the SVG file.</string>
<string name="msg_errorCreatingSvgTemplate">Error creating the SVG template.</string>
<string name="ok">OK</string>
<string name="rot_0"></string>

View File

@ -10,42 +10,46 @@ import junit.framework.TestCase;
public class PolygonTest extends TestCase {
public void testPath() {
checkLine(Polygon.createFromPath("m 10,10 L 20,10 20,20 10,20 z"));
checkLine(Polygon.createFromPath("m 10,10 l 10,0 0,10 -10,0 z"));
checkLine(Polygon.createFromPath("m 10,10 h 10 v 10 h -10 z"));
checkLine(Polygon.createFromPath("m 10,10 H 20 V 20 H 10 z"));
checkLine(Polygon.createFromPath("m 10,10 L 20,10 20,20 10,20 Z"));
checkLine(Polygon.createFromPath("m 10,10 l 10,0 0,10 -10,0 Z"));
checkLine(Polygon.createFromPath("m 10,10 h 10 v 10 h -10 Z"));
checkLine(Polygon.createFromPath("m 10,10 H 20 V 20 H 10 Z"));
}
private void checkLine(Polygon p) {
checkCoor(p);
assertEquals("M 10,10 L 20,10 L 20,20 L 10,20 z", p.toString());
}
private void checkCoor(Polygon p) {
assertEquals(4, p.size());
assertEquals(new VectorFloat(10, 10), p.get(0));
assertEquals(new VectorFloat(20, 10), p.get(1));
assertEquals(new VectorFloat(20, 20), p.get(2));
assertEquals(new VectorFloat(10, 20), p.get(3));
assertNotNull(p);
assertEquals("M 10,10 L 20,10 L 20,20 L 10,20 Z", p.toString());
}
private void checkBezier(Polygon p) {
checkCoor(p);
assertEquals("M 10,10 C 20,10 20,20 10,20 z", p.toString());
assertEquals("M 10,10 C 20,10 20,20 10,20 Z", p.toString());
}
public void testBezierPath() {
checkBezier(Polygon.createFromPath("m 10,10 C 20 10 20 20 10 20 z"));
checkBezier(Polygon.createFromPath("m 10,10 c 10 0 10 10 0 10 z"));
checkBezier(Polygon.createFromPath("m 10,10 C 20 10 20 20 10 20 Z"));
checkBezier(Polygon.createFromPath("m 10,10 c 10 0 10 10 0 10 Z"));
}
public void testCubicReflect() {
assertEquals("M 0,0 C 0,10 10,10 10,0 C 10,-10 20,-10 20,0 C 20,10 30,10 30,0 z",
Polygon.createFromPath("m 0,0 c 0,10 10,10 10,0 s 10,-10 10,0 s 10,10 10,0 z").toString());
assertEquals("M 0,0 C 0,10 10,10 10,0 C 10,-10 20,-10 20,0 C 20,10 30,10 30,0 Z",
Polygon.createFromPath("m 0,0 c 0,10 10,10 10,0 s 10,-10 10,0 s 10,10 10,0 Z").toString());
}
public void testQuadraticReflect() {
assertEquals("M 0,0 C 20,20 40,20 60,0 C 80,-20 100,-20 120,0 C 140,20 160,20 180,0 C 200,-20 220,-20 240,0 ",
assertEquals("M 0,0 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());
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <path d=\"M 0,0 l 0,100 l 100,0 l 0,-100\" />\n" +
" <circle\n" +
" id=\"pin:test\"\n" +
" cx=\"22\"\n" +
" cy=\"19\"\n" +
" r=\"6.0923076\" />\n" +
" <circle\n" +
" id=\"pin+:test2\"\n" +
" cx=\"0\"\n" +
" cy=\"45\"\n" +
" r=\"6.0923076\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <path d=\"M 0,0 l 0,100 l 100,0 l 0,-100\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <path fill-rule=\"evenOdd\" d=\"M 0,0 l 0,100 l 100,0 l 0,-100\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <path style=\"fill-rule:evenOdd\" d=\"M 0,0 l 0,100 l 100,0 l 0,-100\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <polyline points=\"0,0 0,100 100,100 100,0\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <polygon points=\"0,0 0,100 100,100 100,0\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <polygon transform=\"translate(10,20)\" points=\"0,0 0,100 100,100 100,0\" />\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <g transform=\"translate(10,20)\">\n" +
" <polygon points=\"0,0 0,100 100,100 100,0\" />\n" +
" </g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
" <g transform=\"translate(0,20)\">\n" +
" <polygon transform=\"translate(10,0)\" points=\"0,0 0,100 100,100 100,0\" />\n" +
" </g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<g transform=\"rotate(30)\">\n" +
"<path fill=\"none\" stroke=\"black\"\n" +
"d=\"M 0,0 L 0,100 L 100,100 L 100,0 Z\"/>\n" +
"</g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<g transform=\"rotate(45, 50, 50)\">\n" +
"<path fill=\"none\" stroke=\"black\"\n" +
"d=\"M 0,0 L 0,100 L 100,100 L 100,0 Z\"/>\n" +
"</g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<g transform=\"matrix(0.8,0.2,0.1,0.9,5,10)\">\n" +
"<path fill=\"none\" stroke=\"black\"\n" +
"d=\"M 0,0 L 0,100 L 100,100 L 100,0 Z\"/>\n" +
"</g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<rect fill=\"none\" stroke=\"black\" \n" +
"x=\"10\" y=\"20\" width=\"70\" height=\"80\"/>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<rect fill=\"none\" stroke=\"black\" \n" +
"x=\"10\" y=\"20\" rx=\"10\" ry=\"20\" width=\"70\" height=\"80\"/>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<g transform=\"matrix(0.8,0.2,0.1,0.9,5,10)\">\n" +
"<circle fill=\"none\" stroke=\"black\" \n" +
"cx=\"50\" cy=\"60\" r=\"30\" />\n" +
"</g>\n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<g transform=\"scale(2,3)\">\n" +
"<line fill=\"none\" stroke=\"black\" \n" +
"x1=\"10\" y1=\"20\" x2=\"80\" y2=\"70\" />\n" +
"</g>\n" +
"</svg>")).create();
new CSDChecker(custom)
.checkLine(20, 60, 160, 210)
.check();
}
public void testArc() throws IOException, SvgException, PolygonParser.ParserException, PinException {
CustomShapeDescription custom = new SvgImporter(
in("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 0,0 L 40,0 A 31,80,30,0,0,100,0 L 140,0\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 0,0 L 40,0 A 31,80,30,1,0,100,0 L 140,0\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 0,0 L 40,0 A 31,80,30,0,1,100,0 L 140,0\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 0,0 L 40,0 A 31,80,30,1,1,100,0 L 140,0\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"m 40,-40 h 20 c 11.08,0 20,8.92 20,20 V 20 C 80,31.08 71.08,40 60,40 H 40 C 28.92,40 20,31.08 20,20 v -40 c 0,-11.08 8.92,-20 20,-20 z\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 80,0 A 40,40 0 0 1 58.083689,35.678848 40,40 0 0 1 16.350991,32.26026 L 40,0 Z\"/> \n" +
"</svg>")).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("<svg viewBox=\"0 0 200 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
"<path fill=\"none\" stroke=\"black\" stroke-width=\"3\"\n" +
" d=\"M 4.71136,11.590742 A 15,47.5 0 0 1 -3.5072565,53.959374 15,47.5 0 0 1 -19.157018,49.899801 l 8.868378,-38.309059 z\"" +
" transform=\"rotate(-41.594188)\"/> \n" +
"</svg>")).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> checker;
private final ArrayList<TestPin> 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<VectorInterface> shouldPoints = new ArrayList<>();
should.traverse(shouldPoints::add);
ArrayList<VectorInterface> 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);
}
}
}
}

View File

@ -405,7 +405,7 @@
</par>
<par>
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.
</par>
<par>
@ -414,6 +414,49 @@
</par>
</subchapter>
</chapter>
<chapter name="Benutzerdefinierter Schaltungssymbole">
<par>
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.
</par>
<par>
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 <e>pin:[Name]</e> oder <e>pin+:[Name]</e>. Bei der letzteren
Variante wird der Pin beim Reimport zu Digital mit einem Label versehen. Wünscht man kein solches Label,
kann das <e>+</e> entfernt werden.
</par>
<par>
Diese SVG-Datei kann jetzt bearbeitet werden. Am besten geeignet ist das Open-Source Programm
<a href="https://inkscape.org/de/">Inkscape</a> welches kostenlos verfügbar ist.
Die Pins können frei verschoben werden, werden jedoch beim Reimport auf den nächstgelegenen Rasterpunkt
verschoben.
</par>
<par>
Wenn schon existierende SVG Dateien verwendet werden sollen, ist es das einfachste das erstellte
Template zu öffnen, und die bereits vorhandene Grafik per Copy&amp;Paste in das Template einzufügen.
</par>
<par>
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.
</par>
<par>
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.
</par>
</chapter>
<chapter name="Häufig gestellte Fragen">
<faq>
<question>Wie kann ich eine Leitung verschieben?</question>

View File

@ -388,6 +388,44 @@
</par>
</subchapter>
</chapter>
<chapter name="Custom Shapes">
<par>
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.
</par>
<par>
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 <e>pin:[name]</e> or <e>pin+:[name]</e>.
In the latter variant, the pin is provided with a label if reimported to digital.
If you do not want such a label, the <e>+</e> can be removed.
</par>
<par>
This SVG file can now be edited. The most suitable is the open source program
<a href="https://inkscape.org/en/">Inkscape</a> which is available for free.
The pins can be moved freely, but are moved to the next grid point during the reimport.
</par>
<par>
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&amp;Paste.
</par>
<par>
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.
</par>
<par>
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.
</par>
</chapter>
<chapter name="Frequently asked Questions">
<faq>
<question>How to move a wire?</question>