tscircuit / tscircuit/image-utils

Nonzero SVG fills incorrectly acquire holes for same-direction nested subpaths

Open
#33 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.