From 10ebfab78fb0ec5685cacce9bfea38a8047e1d5e Mon Sep 17 00:00:00 2001 From: Max Gaukler Date: Sat, 27 Apr 2024 19:17:46 +0200 Subject: [PATCH] Fix #720: Bounding box calculation for paths with transform For ``, VisiCut ignored the `transform` when calculating the effective stroke width that is used to determine the bounding box. As a result, some files showed way too large bounding boxes and could not be moved fully to the top left. --- .../model/graphicelements/GraphicObject.java | 13 ++++ .../graphicelements/svgsupport/SVGShape.java | 10 ++- .../model/graphicelements/SVGImportTest.java | 68 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/thomas_oster/visicut/model/graphicelements/SVGImportTest.java diff --git a/src/main/java/de/thomas_oster/visicut/model/graphicelements/GraphicObject.java b/src/main/java/de/thomas_oster/visicut/model/graphicelements/GraphicObject.java index e33088871..32bb7b193 100644 --- a/src/main/java/de/thomas_oster/visicut/model/graphicelements/GraphicObject.java +++ b/src/main/java/de/thomas_oster/visicut/model/graphicelements/GraphicObject.java @@ -28,6 +28,19 @@ */ public interface GraphicObject { + /** + * Get bounding box. + * + * The stroke width of lines is included in the bounding box (at least for SVG; + * the implementation status for other formats is unclear.) + * This may be done as a simplified approximation by adding half the stroke width at every boundary, + * even if the rendered path behaves differently (e.g., ignoring the SVG stroke-linejoin setting). + * + * TODO: add a parameter to include/exclude stroke width in the bounding box calculation + * (stroke width should be included for engrave but excluded for cutting) + * + * @return bounding rectangle in raw units (e.g., SVG pixels) + */ public Rectangle2D getBoundingBox(); /** * Returns a list of attribute values for the given diff --git a/src/main/java/de/thomas_oster/visicut/model/graphicelements/svgsupport/SVGShape.java b/src/main/java/de/thomas_oster/visicut/model/graphicelements/svgsupport/SVGShape.java index d584fb923..85a3537a2 100644 --- a/src/main/java/de/thomas_oster/visicut/model/graphicelements/svgsupport/SVGShape.java +++ b/src/main/java/de/thomas_oster/visicut/model/graphicelements/svgsupport/SVGShape.java @@ -86,7 +86,7 @@ private StyleAttribute getStyleAttributeRecursive(String name) * * @return stroke width or 0 if the stroke is disabled. */ - private double getEffectiveStrokeWidthMm() + public double getEffectiveStrokeWidthMm() { // If "stroke:none" is set, the stroke is disabled regardless of stroke-width. StyleAttribute strokeStyle = this.getStyleAttributeRecursive("stroke"); @@ -109,7 +109,13 @@ private double getEffectiveStrokeWidthMm() double width = SVGImporter.numberWithUnitsToMm(strokeWidth, this.svgResolution); try { + // 1. transformation of the group(s) that the shape is inside AffineTransform t = this.getAbsoluteTransformation(); + // 2. transform attribute of the shape itself + // example: + // --> effective stroke width is 123 * 4 + // see https://github.com/t-oster/VisiCut/issues/720 + t.concatenate(this.getDecoratee().getXForm()); width *= (Math.abs(t.getScaleX()) + Math.abs(t.getScaleY())) / 2; } catch (SVGException ex) @@ -241,6 +247,8 @@ public Rectangle2D getShapeBoundingBox() /** * get bounding box in SVG pixels + * + * stroke width is included in a simplified approximation */ @Override public Rectangle2D getBoundingBox() diff --git a/src/test/java/de/thomas_oster/visicut/model/graphicelements/SVGImportTest.java b/src/test/java/de/thomas_oster/visicut/model/graphicelements/SVGImportTest.java new file mode 100644 index 000000000..7f21d9afd --- /dev/null +++ b/src/test/java/de/thomas_oster/visicut/model/graphicelements/SVGImportTest.java @@ -0,0 +1,68 @@ +/** + * This file is part of VisiCut. + * Copyright (C) 2011 - 2024 Thomas Oster + * RWTH Aachen University - 52062 Aachen, Germany + * + * VisiCut is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VisiCut is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VisiCut. If not, see . + **/ +package de.thomas_oster.visicut.model.graphicelements; + +import org.junit.Test; +import static org.junit.Assert.*; +import de.thomas_oster.visicut.model.graphicelements.svgsupport.SVGImporter; +import de.thomas_oster.visicut.model.graphicelements.svgsupport.SVGShape; +import java.awt.geom.Rectangle2D; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +public class SVGImportTest +{ + + @Test + public void PathWithLocalTransform() throws ImportException, IOException + { + // Regression test: Bounding box calculated wrong when path has transform attribute. + // https://github.com/t-oster/VisiCut/issues/720 + final String exampleSVG = "\n" + + "\n" + + " \n" + + ""; + File tempFile = File.createTempFile("example", ".svg"); + tempFile.deleteOnExit(); + try (FileWriter s = new FileWriter(tempFile)) { + s.write(exampleSVG); + } + SVGImporter imp = new SVGImporter(); + GraphicSet result = imp.importSetFromFile(tempFile.getAbsoluteFile(), new ArrayList<>()); + assertEquals(result.size(), 1); + // stroke width = 100 * 0.001 local transform * 1 mm width per 1 unit viewbox = 0.1 + assertEquals(0.1, ((SVGShape) result.get(0)).getEffectiveStrokeWidthMm(), 0); + // "visual bounding box" in SVG pixels including stroke width + // (expected values were determined in Inkscape) + // Note: here, SVG pixels are the same as millimeters + Rectangle2D bb = result.get(0).getBoundingBox(); + // left X = 0.0 mm according to Inkscape, but -0.05mm due to simplified approximation in VisiCut + assertEquals(-0.05, bb.getMinX(), 1e-9); + // other values are identical (partly because approximation errors cancel out) + assertEquals(-0.05, bb.getMinY(), 1e-9); + assertEquals(20.1, bb.getHeight(), 1e-9); + assertEquals(20.1, bb.getWidth(), 1e-9); + } +} \ No newline at end of file