tscircuit / tscircuit/image-utils
Nonzero SVG fills incorrectly acquire holes for same-direction nested subpaths
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 10
- PR merge metrics
- No merged PRs in 30d
Description
On v0.0.10 / 81a79bc3665a4fedab02d3a8e273b9ac5d1af906, getSvgBRepShapes treats nesting depth as even/odd regardless of SVG fill-rule. A single path with nested subpaths wound in the same direction acquires a hole under the default nonzero rule, although its center should remain filled.
Minimal reproduction (Bun, from repository root):
import { identity } from "transformation-matrix"
import { getSvgBRepShapes } from "./lib/svg-to-brep-shapes"
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path d="M0 0 H10 V10 H0 Z M2 2 H8 V8 H2 Z"/>
</svg>`
const shapes = getSvgBRepShapes({ svg, width: 10, height: 10, transform: identity() })
console.log(shapes.length, shapes[0]?.inner_rings.length) // 1, 1
Expected: one filled outer square with no hole, area 100. Actual: one outer square and a 6-by-6 hole, net filled area 64. Adding fill-rule="nonzero" produces the same incorrect hole.
Control cases verified locally:
| Rule / inner direction | Expected hole | Actual hole | Canvas center alpha |
|---|---|---|---|
| omitted / same | no | yes | 255 |
| nonzero / same | no | yes | 255 |
| evenodd / same | yes | yes | 0 |
| nonzero / opposite | yes | yes | 0 |
The Canvas check used the installed @napi-rs/canvas Path2D with the same path data and an explicit corresponding fill rule; it is an independent path-fill check, not a browser screenshot. Filled BRep areas were calculated from the returned rings with the shoelace formula.
SVG 2 section 13.4.2 specifies nonzero as the initial fill rule and distinguishes winding-number filling from evenodd. The converter currently does not read fill-rule or account for winding when choosing holes.
This is distinct from #27 / #28: there is only one path element here, and I also reproduced the unwanted hole against the local #28 implementation that isolates paths. A fix should preserve each path's effective fill rule (including inheritance) and winding relationships rather than infer all holes solely from containment parity.
Prepared with Codex (Astra) assistance. Reproduction and control cases executed locally.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/svg-to-brep-shapes and run the Bun reproduction from the issue against the current implementation. Trace how nested subpaths become inner_rings, then account for each path’s effective fill-rule and winding relationships rather than containment parity alone. Done means the same-direction nonzero case has no hole while the evenodd and opposite-direction nonzero controls retain theirs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- computer-graphics
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100