diff --git a/pom.xml b/pom.xml index 84f8b0e665..021127df05 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 3.0.0-M5 1.3.0 3.2.4 - 1.14 + 1.16 0.1a 1.19.0 3.1.0 diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java index 9abf54d539..563e6a02f3 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java @@ -25,7 +25,7 @@ This file is part of Universal Gcode Sender (UGS). import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; -import java.awt.geom.Area; +import java.awt.geom.RectangularShape; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; @@ -44,6 +44,7 @@ public class EntityGroup extends AbstractEntity implements EntityListener { private double groupRotation = 0; private Point2D cachedCenter = new Point2D.Double(0, 0); + private Rectangle2D cachedBounds = new Rectangle2D.Double(0, 0, 0, 0); public EntityGroup() { super(); @@ -87,12 +88,34 @@ public void rotate(Point2D center, double angle) { @Override public Shape getShape() { - final Area area = new Area(); + return getBounds(); + } + + @Override + public Size getSize() { + Rectangle2D bounds = getBounds(); + return new Size(bounds.getWidth(), bounds.getHeight()); + } + + @Override + public Rectangle2D getBounds() { + if (cachedBounds != null) { + return cachedBounds; + } + List allChildren = getAllChildren(); - allChildren.stream() - .filter(c -> c != this) - .forEach(c -> area.add(new Area(c.getBounds()))); - return area.getBounds2D(); + double maxX = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMaxX).max().orElse(0); + double maxY = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMaxY).max().orElse(0); + double minX = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMinX).min().orElse(0); + double minY = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMinY).min().orElse(0); + cachedBounds = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + return cachedBounds; + } + + @Override + public Point2D getPosition(Anchor anchor) { + Rectangle2D bounds = getBounds(); + return new Point2D.Double(bounds.getX(), bounds.getY()); } @Override @@ -124,12 +147,13 @@ public void addChild(Entity entity, int index) { private void invalidateCenter() { cachedCenter = null; + cachedBounds = null; } @Override public Point2D getCenter() { if (cachedCenter == null) { - Rectangle2D bounds = getShape().getBounds2D(); + Rectangle2D bounds = getBounds(); cachedCenter = new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java index 9843bb39bc..fdb7a7143a 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java @@ -43,7 +43,7 @@ public Rectangle() { */ public Rectangle(double x, double y) { super(x, y); - this.shape = new Rectangle2D.Double(0, 0, 10, 10); + this.shape = new Rectangle2D.Double(0, 0, 1, 1); setName("Rectangle"); } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java index 83d576cf34..880b6d5960 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/gui/SelectionSettingsPanel.java @@ -316,11 +316,7 @@ public void changedUpdate(DocumentEvent e) { double width = Utils.parseDouble(widthTextField.getText()); double height = Utils.parseDouble(heightTextField.getText()); - if (width <= 0 || height <= 0) { - return; - } - - if (!lockRatioButton.isSelected()) { + if (width >= 0 && height >= 0 & !lockRatioButton.isSelected()) { double ratio = controller.getSelectionManager().getSize().getRatio(); if (e.getDocument() == widthTextField.getDocument()) { height = width / ratio; diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReader.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReader.java index 933c42e477..51685e87d6 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReader.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReader.java @@ -27,69 +27,49 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.io.DesignReaderException; import com.willwinder.ugs.nbp.designer.model.Design; import com.willwinder.ugs.nbp.designer.model.Size; -import com.willwinder.universalgcodesender.Utils; -import com.willwinder.universalgcodesender.utils.ThreadHelper; import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.anim.dom.SVGOMDocument; +import org.apache.batik.bridge.BridgeContext; +import org.apache.batik.bridge.DocumentLoader; +import org.apache.batik.bridge.GVTBuilder; +import org.apache.batik.bridge.UserAgent; +import org.apache.batik.bridge.UserAgentAdapter; import org.apache.batik.ext.awt.geom.ExtendedGeneralPath; import org.apache.batik.ext.awt.geom.ExtendedPathIterator; import org.apache.batik.gvt.CompositeGraphicsNode; import org.apache.batik.gvt.GraphicsNode; import org.apache.batik.gvt.ShapeNode; -import org.apache.batik.swing.JSVGCanvas; -import org.apache.batik.swing.svg.GVTTreeBuilderEvent; -import org.apache.batik.swing.svg.GVTTreeBuilderListener; -import org.apache.batik.swing.svg.JSVGComponent; import org.apache.batik.util.XMLResourceDescriptor; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.w3c.dom.svg.SVGDocument; -import org.w3c.dom.svg.SVGRect; -import org.yaml.snakeyaml.reader.ReaderException; import java.awt.*; import java.awt.geom.*; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.logging.Logger; /** * @author Joacim Breiler */ -public class SvgReader implements GVTTreeBuilderListener, DesignReader { +public class SvgReader implements DesignReader { private static final Logger LOGGER = Logger.getLogger(SvgReader.class.getSimpleName()); - private JSVGCanvas svgCanvas; - private Group result; - @Override public Optional read(File f) { if (EventQueue.isDispatchThread()) { throw new DesignReaderException("Method can not be executed in dispatch thread"); } - result = null; - svgCanvas = new JSVGCanvas(); - svgCanvas.setDocumentState(JSVGComponent.ALWAYS_DYNAMIC); - svgCanvas.addGVTTreeBuilderListener(this); - svgCanvas.setURI(f.toURI().toString()); - try { - // Wait for svg loader to finish processing the SVG - ThreadHelper.waitUntil(() -> result != null, 10, TimeUnit.SECONDS); - } catch (TimeoutException e) { - throw new DesignReaderException("It took to long to load file"); - // Never mind + return read(new FileInputStream(f)); + } catch (IOException ex) { + throw new RuntimeException("Couldn't load stream"); } - - Design design = new Design(); - design.setEntities(result != null ? result.getChildren() : new ArrayList<>()); - return Optional.of(design); } @Override @@ -100,93 +80,108 @@ public Optional read(InputStream inputStream) { String parser = XMLResourceDescriptor.getXMLParserClassName(); SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); - SVGDocument doc = null; + Group result; try { - doc = f.createSVGDocument(null, inputStream); + SVGOMDocument doc = (SVGOMDocument) f.createSVGDocument(null, inputStream); + UserAgent userAgent = new UserAgentAdapter(); + DocumentLoader loader = new DocumentLoader(userAgent); + BridgeContext bridgeContext = new BridgeContext(userAgent, loader); + bridgeContext.setDynamicState(BridgeContext.DYNAMIC); + + // Enable CSS- and SVG-specific enhancements. + (new GVTBuilder()).build(bridgeContext, doc); + result = parseGraphicsNode(doc, bridgeContext); } catch (IOException ex) { throw new DesignReaderException("Couldn't load stream"); } - result = null; - svgCanvas = new JSVGCanvas(); - svgCanvas.setDocumentState(JSVGComponent.ALWAYS_DYNAMIC); - svgCanvas.addGVTTreeBuilderListener(this); - svgCanvas.setSVGDocument(doc); + Design design = new Design(); + design.setEntities(result.getChildren()); + return Optional.of(design); + } + private Group parseGraphicsNode(SVGOMDocument node, BridgeContext bridgeContext) { + Group group = new Group(); + walk(node, group, 0, bridgeContext); - try { - // Wait for svg loader to finish processing the SVG - ThreadHelper.waitUntil(() -> result != null, 10, TimeUnit.SECONDS); - } catch (TimeoutException e) { - throw new DesignReaderException("It took to long to load file"); - } + // We need to invert the Y coordinate and apply global pixel scale + double pixelUnitToMM = node.getSVGContext().getPixelUnitToMillimeter(); + AffineTransform transform = new AffineTransform(); + transform.scale(1, -1); // Invert Y-coordinate + transform.scale(pixelUnitToMM, pixelUnitToMM); + group.applyTransform(transform); + group.move(new Point2D.Double(-group.getPosition().getX(), -group.getPosition().getY())); - Design design = new Design(); - design.setEntities(result != null ? result.getChildren() : new ArrayList<>()); - return Optional.of(design); + return group; } - private void walk(Node node, Group group, AffineTransform transform, int level) { - GraphicsNode graphicsNode = svgCanvas.getUpdateManager().getBridgeContext().getGraphicsNode(node); + private void walk(Node node, Group parent, int level, BridgeContext bridgeContext) { + GraphicsNode graphicsNode = bridgeContext.getGraphicsNode(node); if (graphicsNode != null) { LOGGER.finest(StringUtils.leftPad("", level, "\t") + graphicsNode); + if (graphicsNode instanceof CompositeGraphicsNode) { + parseGroupNode(node, parent, level, bridgeContext, (CompositeGraphicsNode) graphicsNode); + } else if (graphicsNode instanceof ShapeNode) { + parseShapeNode(node, parent, (ShapeNode) graphicsNode); + } + } + } - AffineTransform groupTransform = new AffineTransform(transform); - if (graphicsNode.getTransform() != null) { - if (!graphicsNode.getTransform().isIdentity()) { - groupTransform.concatenate(graphicsNode.getTransform()); - } else { + private void parseShapeNode(Node node, Group parent, ShapeNode shapeNode) { + Shape shape = shapeNode.getShape(); + AbstractEntity createdShape = null; + + if (shape instanceof ExtendedGeneralPath) { + createdShape = parsePath((ExtendedGeneralPath) shape); + } else if (shape instanceof Rectangle2D) { + createdShape = parseRectangle((Rectangle2D) shape); + } else if (shape instanceof Ellipse2D) { + createdShape = parseEllipse((Ellipse2D) shape); + } else { + LOGGER.finest(shape.toString()); + } - } + if (createdShape != null) { + Node id = node.getAttributes().getNamedItem("id"); + if (id != null) { + createdShape.setName(createdShape.getName() + " (" + id.getFirstChild().getNodeValue() + ")"); + } + Node desc = node.getAttributes().getNamedItem("desc"); + if (desc != null) { + createdShape.setDescription(desc.getFirstChild().getNodeValue()); } - if (graphicsNode instanceof CompositeGraphicsNode) { - NodeList childNodes = node.getChildNodes(); - - Group childGroup = group; - if (((CompositeGraphicsNode) graphicsNode).getChildren().size() > 1) { - childGroup = new Group(); - if (node.getAttributes() != null && node.getAttributes().getNamedItem("id") != null) { - Node id = node.getAttributes().getNamedItem("id"); - childGroup.setName(childGroup.getName() + " (" + id.getFirstChild().getNodeValue() + ")"); - } - group.addChild(childGroup); - } - for (int i = 0; i < childNodes.getLength(); i++) { - walk(childNodes.item(i), childGroup, groupTransform, level + 1); - } - } else if (graphicsNode instanceof ShapeNode) { - ShapeNode shapeNode = (ShapeNode) graphicsNode; - Shape shape = shapeNode.getShape(); - AbstractEntity createdShape = null; - - if (shape instanceof ExtendedGeneralPath) { - createdShape = parsePath((ExtendedGeneralPath) shape); - } else if (shape instanceof Rectangle2D) { - createdShape = parseRectangle((Rectangle2D) shape); - } else if (shape instanceof Ellipse2D) { - createdShape = parseEllipse((Ellipse2D) shape); - } else { - LOGGER.finest(shape.toString()); - } - - if (createdShape != null) { - Node id = node.getAttributes().getNamedItem("id"); - if (id != null) { - createdShape.setName(createdShape.getName() + " (" + id.getFirstChild().getNodeValue() + ")"); - } - Node desc = node.getAttributes().getNamedItem("desc"); - if (desc != null) { - createdShape.setDescription(desc.getFirstChild().getNodeValue()); - } - createdShape.setTransform(groupTransform); - group.addChild(createdShape); - } + if (shapeNode.getTransform() != null) { + createdShape.applyTransform(shapeNode.getTransform()); } + parent.addChild(createdShape); + } + } + + private void parseGroupNode(Node node, Group parent, int level, BridgeContext bridgeContext, CompositeGraphicsNode graphicsNode) { + NodeList childNodes = node.getChildNodes(); + + // Only add a child group if the group contains more than 1 child + Group group = parent; + if (graphicsNode.getChildren().size() > 1) { + group = new Group(); + if (node.getAttributes() != null && node.getAttributes().getNamedItem("id") != null) { + Node id = node.getAttributes().getNamedItem("id"); + group.setName(group.getName() + " (" + id.getFirstChild().getNodeValue() + ")"); + } + parent.addChild(group); + } + + for (int i = 0; i < childNodes.getLength(); i++) { + walk(childNodes.item(i), group, level + 1, bridgeContext); + } + + if (graphicsNode.getTransform() != null) { + group.applyTransform(graphicsNode.getTransform()); } } @@ -298,43 +293,4 @@ private AbstractEntity parsePath(ExtendedGeneralPath shape) { } return line; } - - @Override - public void gvtBuildStarted(GVTTreeBuilderEvent e) { - - } - - @Override - public void gvtBuildCompleted(GVTTreeBuilderEvent e) { - Group group = new Group(); - Dimension2D svgDocumentSize = svgCanvas.getSVGDocumentSize(); - - // If the width and height attributes are missing, try to fetch them from the viewport - SVGDocument svgDocument = svgCanvas.getSVGDocument(); - if (svgDocumentSize.getWidth() == 0 || svgDocumentSize.getHeight() == 0) { - SVGRect baseVal = svgDocument.getRootElement().getViewBox().getBaseVal(); - svgDocument.getRootElement().setAttributeNS(null, "width", Utils.formatter.format(baseVal.getWidth())); - svgDocument.getRootElement().setAttributeNS(null, "height", Utils.formatter.format(baseVal.getHeight())); - } - - walk(svgDocument, group, new AffineTransform(), 0); - - // We need to invert the Y coordinate - AffineTransform transform = new AffineTransform(); - transform.translate(0, group.getSize().getHeight()); - transform.scale(1, -1); - group.applyTransform(transform); - - this.result = group; - } - - @Override - public void gvtBuildCancelled(GVTTreeBuilderEvent e) { - - } - - @Override - public void gvtBuildFailed(GVTTreeBuilderEvent e) { - - } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/PathTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/PathTest.java new file mode 100644 index 0000000000..13473849de --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/PathTest.java @@ -0,0 +1,33 @@ +package com.willwinder.ugs.nbp.designer.entities.cuttable; + +import com.willwinder.ugs.nbp.designer.model.Size; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class PathTest { + + @Test + public void pathInOnlyOneDimensionShouldReturnCorrectWidth() { + Path path = new Path(); + path.moveTo(10, 0); + path.lineTo(20, 0); + path.close(); + + Size size = path.getSize(); + assertEquals(10, size.getWidth(), 0.1); + assertEquals(0, size.getHeight(), 0.1); + } + + @Test + public void pathInOnlyOneDimensionShouldReturnCorrectHeight() { + Path path = new Path(); + path.moveTo(0, 10); + path.lineTo(0, 20); + path.close(); + + Size size = path.getSize(); + assertEquals(0, size.getWidth(), 0.1); + assertEquals(10, size.getHeight(), 0.1); + } +} \ No newline at end of file diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/RectangleTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/RectangleTest.java new file mode 100644 index 0000000000..8244f2abda --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/cuttable/RectangleTest.java @@ -0,0 +1,18 @@ +package com.willwinder.ugs.nbp.designer.entities.cuttable; + +import com.willwinder.ugs.nbp.designer.model.Size; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class RectangleTest { + + @Test + public void setSize() { + Rectangle rectangle = new Rectangle(1, 1); + rectangle.setSize(new Size(30, 30)); + assertEquals(30, rectangle.getSize().getWidth(), 0.1); + assertEquals(30, rectangle.getSize().getHeight(), 0.1); + } + +} \ No newline at end of file diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReaderTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReaderTest.java new file mode 100644 index 0000000000..701471a9c6 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/io/svg/SvgReaderTest.java @@ -0,0 +1,44 @@ +package com.willwinder.ugs.nbp.designer.io.svg; + +import com.willwinder.ugs.nbp.designer.entities.Entity; +import com.willwinder.ugs.nbp.designer.model.Design; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SvgReaderTest { + + @Test + public void readShouldCorrectlyConvertToMM() { + SvgReader reader = new SvgReader(); + Design design = reader.read(SvgReaderTest.class.getResourceAsStream("/20x20mm.svg")).orElseThrow(() -> new RuntimeException("Could not find SVG")); + Entity entity = design.getEntities().get(0); + + assertEquals(20, entity.getSize().getWidth(), 0.1); + assertEquals(20, entity.getSize().getHeight(), 0.1); + assertEquals(0, entity.getPosition().getX(), 0.1); + assertEquals(0, entity.getPosition().getY(), 0.1); + } + + @Test + public void readShouldCorrectlyConvertToMMFromInch() { + SvgReader reader = new SvgReader(); + Design design = reader.read(SvgReaderTest.class.getResourceAsStream("/1x1inch.svg")).orElseThrow(() -> new RuntimeException("Could not find SVG")); + Entity entity = design.getEntities().get(0); + assertEquals(25.4, entity.getSize().getWidth(), 0.1); + assertEquals(25.4, entity.getSize().getHeight(), 0.1); + assertEquals(0, entity.getPosition().getX(), 0.1); + assertEquals(0, entity.getPosition().getY(), 0.1); + } + + @Test + public void readLinesShouldCorrectlyConvertToMMFromInch() { + SvgReader reader = new SvgReader(); + Design design = reader.read(SvgReaderTest.class.getResourceAsStream("/lines.svg")).orElseThrow(() -> new RuntimeException("Could not find SVG")); + Entity entity = design.getEntities().get(0); + assertEquals(50, entity.getSize().getHeight(), 0.1); + assertEquals(70, entity.getSize().getWidth(), 0.1); + assertEquals(0, entity.getPosition().getX(), 0.1); + assertEquals(0, entity.getPosition().getY(), 0.1); + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/resources/1x1inch.svg b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/1x1inch.svg new file mode 100644 index 0000000000..63ae8b1d47 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/1x1inch.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/resources/20x20mm.svg b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/20x20mm.svg new file mode 100644 index 0000000000..15a37ee135 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/20x20mm.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/resources/lines.svg b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/lines.svg new file mode 100644 index 0000000000..9f2bebcdfc --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/resources/lines.svg @@ -0,0 +1,33 @@ + + + + + + + + +