Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #720: Bounding box calculation for paths with transform #727

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(), 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);
}
}
Loading