Skip to content

Commit cc3689d

Browse files
committed
Fix #720: Bounding box calculation for paths with transform
For `<path transform="..." style="stroke-width:...">`, 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.
1 parent 1357f64 commit cc3689d

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

src/main/java/de/thomas_oster/visicut/model/graphicelements/GraphicObject.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@
2828
*/
2929
public interface GraphicObject
3030
{
31+
/**
32+
* Get bounding box.
33+
*
34+
* The stroke width of lines is included in the bounding box (at least for SVG;
35+
* the implementation status for other formats is unclear.)
36+
* This may be done as a simplified approximation by adding half the stroke width at every boundary,
37+
* even if the rendered path behaves differently (e.g., ignoring the SVG stroke-linejoin setting).
38+
*
39+
* TODO: add a parameter to include/exclude stroke width in the bounding box calculation
40+
* (stroke width should be included for engrave but excluded for cutting)
41+
*
42+
* @return bounding rectangle in raw units (e.g., SVG pixels)
43+
*/
3144
public Rectangle2D getBoundingBox();
3245
/**
3346
* Returns a list of attribute values for the given

src/main/java/de/thomas_oster/visicut/model/graphicelements/svgsupport/SVGShape.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private StyleAttribute getStyleAttributeRecursive(String name)
8686
*
8787
* @return stroke width or 0 if the stroke is disabled.
8888
*/
89-
private double getEffectiveStrokeWidthMm()
89+
public double getEffectiveStrokeWidthMm()
9090
{
9191
// If "stroke:none" is set, the stroke is disabled regardless of stroke-width.
9292
StyleAttribute strokeStyle = this.getStyleAttributeRecursive("stroke");
@@ -109,7 +109,13 @@ private double getEffectiveStrokeWidthMm()
109109
double width = SVGImporter.numberWithUnitsToMm(strokeWidth, this.svgResolution);
110110
try
111111
{
112+
// 1. transformation of the group(s) that the shape is inside
112113
AffineTransform t = this.getAbsoluteTransformation();
114+
// 2. transform attribute of the shape itself
115+
// example: <path transform="scale(123)" style="stroke-width:4">
116+
// --> effective stroke width is 123 * 4
117+
// see https://github.com/t-oster/VisiCut/issues/720
118+
t.concatenate(this.getDecoratee().getXForm());
113119
width *= (Math.abs(t.getScaleX()) + Math.abs(t.getScaleY())) / 2;
114120
}
115121
catch (SVGException ex)
@@ -241,6 +247,8 @@ public Rectangle2D getShapeBoundingBox()
241247

242248
/**
243249
* get bounding box in SVG pixels
250+
*
251+
* stroke width is included in a simplified approximation
244252
*/
245253
@Override
246254
public Rectangle2D getBoundingBox()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* This file is part of VisiCut.
3+
* Copyright (C) 2011 - 2024 Thomas Oster <[email protected]>
4+
* RWTH Aachen University - 52062 Aachen, Germany
5+
*
6+
* VisiCut is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* VisiCut is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with VisiCut. If not, see <http://www.gnu.org/licenses/>.
18+
**/
19+
package de.thomas_oster.visicut.model.graphicelements;
20+
21+
import org.junit.Test;
22+
import static org.junit.Assert.*;
23+
import de.thomas_oster.visicut.model.graphicelements.svgsupport.SVGImporter;
24+
import de.thomas_oster.visicut.model.graphicelements.svgsupport.SVGShape;
25+
import java.awt.geom.Rectangle2D;
26+
import java.io.File;
27+
import java.io.FileWriter;
28+
import java.io.IOException;
29+
import java.util.ArrayList;
30+
31+
public class SVGImportTest
32+
{
33+
34+
@Test
35+
public void PathWithLocalTransform() throws ImportException, IOException
36+
{
37+
// Regression test: Bounding box calculated wrong when path has transform attribute.
38+
// https://github.com/t-oster/VisiCut/issues/720
39+
final String exampleSVG = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
40+
"<svg width=\"40mm\" height=\"30mm\" viewBox=\"0 0 40 30\">\n" +
41+
" <path\n" +
42+
" d=\"M 0,0 H 20000 L 5000,20000 h 10000\"\n" +
43+
" style=\"fill:none;stroke:#0000ff;stroke-width:100\"\n" +
44+
" transform=\"scale(0.001,0.001)\"\n" +
45+
" id=\"path4\" />\n" +
46+
"</svg>";
47+
File tempFile = File.createTempFile("example", ".svg");
48+
tempFile.deleteOnExit();
49+
try (FileWriter s = new FileWriter(tempFile)) {
50+
s.write(exampleSVG);
51+
}
52+
SVGImporter imp = new SVGImporter();
53+
GraphicSet result = imp.importSetFromFile(tempFile.getAbsoluteFile(), new ArrayList<>());
54+
assertEquals(result.size(), 1);
55+
// stroke width = 100 * 0.001 local transform * 1 mm width per 1 unit viewbox = 0.1
56+
assertEquals(0.1, ((SVGShape) result.get(0)).getEffectiveStrokeWidthMm(), 0);
57+
// "visual bounding box" in SVG pixels including stroke width
58+
// (expected values were determined in Inkscape)
59+
// Note: here, SVG pixels are the same as millimeters
60+
Rectangle2D bb = result.get(0).getBoundingBox();
61+
// left X = 0.0 mm according to Inkscape, but -0.05mm due to simplified approximation in VisiCut
62+
assertEquals(-0.05, bb.getMinX(), 1e-9);
63+
// other values are identical (partly because approximation errors cancel out)
64+
assertEquals(-0.05, bb.getMinY(), 1e-9);
65+
assertEquals(20.1, bb.getHeight(), 1e-9);
66+
assertEquals(20.1, bb.getWidth(), 1e-9);
67+
}
68+
}

0 commit comments

Comments
 (0)