Skip to content

Commit

Permalink
Fix #720: Bounding box calculation for paths with transform
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mgmax committed Apr 27, 2024
1 parent b407646 commit fc8305c
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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: <path transform="scale(123)" style="stroke-width:4">
// --> 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)
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* This file is part of VisiCut.
* Copyright (C) 2011 - 2024 Thomas Oster <[email protected]>
* 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 <http://www.gnu.org/licenses/>.
**/
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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<svg width=\"40mm\" height=\"30mm\" viewBox=\"0 0 40 30\">\n" +
" <path\n" +
" d=\"M 0,0 H 20000 L 5000,20000 h 10000\"\n" +
" style=\"fill:none;stroke:#0000ff;stroke-width:100\"\n" +
" transform=\"scale(0.001,0.001)\"\n" +
" id=\"path4\" />\n" +
"</svg>";
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(), 0);
// other values are identical (partly because approximation errors cancel out)
assertEquals(-0.05, bb.getMinY(), 0);
assertEquals(20.1, bb.getHeight(), 0);
assertEquals(20.1, bb.getWidth(), 0);
}
}

0 comments on commit fc8305c

Please sign in to comment.