getSvgBRepShapes combines rings from separate SVG path elements before classifying containment. A black square with another separately filled black square inside it becomes a square with a hole, even though the center is painted by both source elements.
Reproduced on 81a79bc (0.0.10). The test checks whether the center belongs to any output outer ring without being excluded by its inner rings:
import { expect, test } from "bun:test"
import { Polygon, point } from "@flatten-js/core"
import { identity } from "transformation-matrix"
import { getSvgBRepShapes } from "../lib/svg-to-brep-shapes"
test("a separately filled inner path does not cut a hole in an outer path", () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><path fill="black" d="M0 0H10V10H0Z"/><path fill="black" d="M3 3H7V7H3Z"/></svg>'
const shapes = getSvgBRepShapes({ svg, width: 10, height: 10, transform: identity() })
const containsCenter = (vertices: {x:number,y:number}[]) => {
const polygon = new Polygon()
polygon.addFace(vertices.map(p => point(p.x, p.y)))
return polygon.contains(point(0, 0))
}
const centerFilled = shapes.some(shape => containsCenter(shape.outer_ring.vertices) && !shape.inner_rings.some(ring => containsCenter(ring.vertices)))
console.log(JSON.stringify(shapes))
expect(centerFilled).toBe(true)
})
Run bun test tests/separate-filled-paths.test.ts. Expected centerFilled=true; actual=false. The output assigns the inner square to inner_rings of the outer square.
SVG fill operates on each graphical element; a separate path should not toggle another path's interior. See SVG 2 fill properties. Both elements here explicitly use the same opaque black fill, so this reproduction does not depend on color compositing or differing fill rules.
The path boundary is lost in getTransformedSvgPathRoutes through flatMap, then getSvgBRepShapes runs containment parity globally. Preserve per-element grouping when classifying holes; combine the resulting filled geometry without subtracting independent filled paths.
Related #24 improves whole-ring containment, but does not preserve element grouping; this reproduction uses strictly nested convex squares, so centroid/partial-overlap ambiguity is absent. This is a geometry-output reproduction, not a browser screenshot comparison.
Investigated with Codex (Astra) assistance.
getSvgBRepShapescombines rings from separate SVG path elements before classifying containment. A black square with another separately filled black square inside it becomes a square with a hole, even though the center is painted by both source elements.Reproduced on 81a79bc (0.0.10). The test checks whether the center belongs to any output outer ring without being excluded by its inner rings:
Run
bun test tests/separate-filled-paths.test.ts. Expected centerFilled=true; actual=false. The output assigns the inner square to inner_rings of the outer square.SVG fill operates on each graphical element; a separate path should not toggle another path's interior. See SVG 2 fill properties. Both elements here explicitly use the same opaque black fill, so this reproduction does not depend on color compositing or differing fill rules.
The path boundary is lost in
getTransformedSvgPathRoutesthrough flatMap, thengetSvgBRepShapesruns containment parity globally. Preserve per-element grouping when classifying holes; combine the resulting filled geometry without subtracting independent filled paths.Related #24 improves whole-ring containment, but does not preserve element grouping; this reproduction uses strictly nested convex squares, so centroid/partial-overlap ambiguity is absent. This is a geometry-output reproduction, not a browser screenshot comparison.
Investigated with Codex (Astra) assistance.