Skip to content

fix(import): carry Figma strokes through real stroke channels instead of dropping them#6419

Merged
shipyard-local[bot] merged 8 commits into
mainfrom
fix/svg-stroke-survival
Jul 23, 2026
Merged

fix(import): carry Figma strokes through real stroke channels instead of dropping them#6419
shipyard-local[bot] merged 8 commits into
mainfrom
fix/svg-stroke-survival

Conversation

@shipyard-local

@shipyard-local shipyard-local Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Item #2 of the Figma-import hardening plan. Stacked on #6416 (fill-rule) — its diff reduces to only the stroke changes once #6416 merges.

Strokes were the only material property the audit showed losing (96 across the 3 files). Ground truth differed from the plan in useful ways:

  • Gradient rims (knob "Oval"/"knob base") — two gaps: the Oval rim had NO baked geometry (only an authored vector network), so it fell to a plain box. Added a vector-network decoder (paths.mjs, self-validated by exact byte consumption) + a full gradient-stroke channel (SvgPathWidget::set_stroke_gradientCanvas::set_stroke_gradient_linear, setSvgStrokeGradient bridge + manifest + oracle, codegen, IR capture, native, decoder lowering). Caveat: all 20 rims are paint-opacity-0 (designer-disabled; Figma renders them invisible too) — verified by audit + Skia raster proof.
  • Sequencer step cells (48 "findings") — were NEVER dropped; they render correctly. The findings were an audit blind-spot + a diagnostics-join bug (pushDiag logged the master guid, not the instance) — both fixed. The real drop was 1 fill+stroke node per FX file, now riding the same path (exact for CENTER align — no sibling vector needed).
  • XY-pad triangle — Figma bakes INSIDE stroke bands unclipped (a 2px INSIDE stroke → 4px band that render-time clipping trims; we didn't clip), so it painted fat and bright. Decoder now prefers the fill outline + a centered stroke channel — the triangle renders as a thin dim line matching Figma (before/after crops).

Verification

  • Decode-stage stroke counts fully close: A Channel FX 42/65 → 65/65, designers-pick 42/65 → 65/65, sequencer 33/83 → 83/83 (zero unaccounted). thumb_parity (1006,189) block clears in both FX files; sequencer unchanged; mixer reference 43/43 (only sub-pixel AA shifts).
  • ctest svg|path|import|design|native|canvas all pass except the known pre-existing PulpSampler SEGFAULT. JS 136/136. New tests: gradient-stroke unit ×3 + Skia raster proof + bridge/IR round-trips + ellipse-synthesis e2e + vector-network/INSIDE-band/instance-diag scene tests.

🤖 Generated with Claude Code


🔎 Provenance

Agent codex
Machine m3
Tab design PRs
Directory /Volumes/Workshop/Code/pulp
Session 019f8d7d-1d7b-7b00-b48a-e4c4213b1161

Resume

codex resume 019f8d7d-1d7b-7b00-b48a-e4c4213b1161

Jump to this tab

cmux surface focus FD52547A-6433-47C0-BF1A-EC0CFDD0DD55

Relaunch (any agent)

cmux surface resume get --surface FD52547A-6433-47C0-BF1A-EC0CFDD0DD55

stamped 2026-07-23 14:37 UTC

… of dropping them

The material audit showed strokes as the only property family losing
material on the Triaz files (decode-stage emitted/declared 42/65, 42/65,
33/83). Three root causes, all fixed generally:

1. Gradient strokes dropped. GRADIENT_LINEAR strokes ("Oval" / "knob
   base" rims) had no channel to ride: the box border is solid-only and
   SvgPathWidget had no gradient stroke. Added the full mirror of the
   fill-gradient path: SvgPathWidget::set_stroke_gradient (parsed
   per-frame, solid stroke as the parse-failure fallback, painted via
   Canvas::set_stroke_gradient_linear), a setSvgStrokeGradient bridge fn
   + manifest/oracle rows, codegen emission from svg_stroke_gradient,
   native-materializer wiring, and decoder lowering to a new envelope
   strokeGradient channel. design_ir_json captures the stroke channels
   unconditionally (not path_data-gated) so an ellipse whose path is
   synthesized later keeps its rim.

2. No geometry at all for the FX knobs' "Oval" rim: master AND every
   instance carry EMPTY fillGeometry/strokeGeometry, leaving only the
   authored vector network. paths.mjs now decodes
   vectorData.vectorNetworkBlob (layout self-validated by exact byte
   consumption; refused for regions, branching vertices, or
   BOOLEAN_OPERATIONs) into the shape's centerline, which the stroke
   channels above paint at the declared weight.

3. INSIDE/OUTSIDE stroke bands render double-width. Figma bakes those
   bands UNCLIPPED — the boundary outlined at ±weight (verified: a
   weight-2 INSIDE "Polygon 5" carries a 4px band) — and painting the
   blob verbatim drew the XY-pad triangle fat and bright (#373737 vs
   Figma's #2b2b2b, dE 3.84). The decoder now prefers the fill outline
   with a centered stroke channel (right width/color, half a weight off
   in position) and raises stroke-align-approximated.

A filled vector's separate stroke also rides the same path as stroke
channels now (SvgPathWidget fills then strokes one path; exact for
CENTER align) instead of a whole-px CSS border.

Audit accuracy fixes that the counts depended on: pushDiag records the
instance-path node id (the master guid never joined against the
per-instance materials sidecar, so the diagnosed column read 0), and
the stroke tally recognizes every legitimate survival channel — border,
stroke/strokeGradient, and a stroke-only vector's band fill (48
sequencer step-cell ticks were rendering correctly while counted as
silent drops).

Decode-stage stroke counts on the three Triaz files close to 65/65,
65/65, 83/83 (was 42/65, 42/65, 33/83) with codegen-stage 46+20, 46+20,
33 of the same — zero unaccounted. thumb_parity: the (1006,189)
triangle block clears in both FX files; no block regresses; the
reference mixer frame stays at 43/43 with only sub-pixel AA shifts from
fractional stroke widths.

Hotspot-Grow: test/test_design_import_codegen.cpp reason="new end-to-end test: synthesized ellipse carries its gradient stroke onto the path"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e8kbMn8Sf4mF8ozsy1Jot
shipyard-local Bot and others added 2 commits July 21, 2026 17:37
# Conflicts:
#	tools/import-design/fig/paths.mjs
#	tools/import-design/fig/paths.test.mjs
@danielraffel
danielraffel self-requested a review as a code owner July 23, 2026 07:00
@shipyard-local shipyard-local Bot closed this Jul 23, 2026
auto-merge was automatically disabled July 23, 2026 08:40

Pull request was closed

@shipyard-local shipyard-local Bot reopened this Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@shipyard-local
shipyard-local Bot removed this pull request from the merge queue due to a manual request Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Jul 23, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Jul 23, 2026
@danielraffel
danielraffel force-pushed the fix/svg-stroke-survival branch from 7de2d11 to 6f9c5bf Compare July 23, 2026 14:37
@shipyard-local shipyard-local Bot closed this Jul 23, 2026
@shipyard-local shipyard-local Bot reopened this Jul 23, 2026
Hotspot-Grow: test/test_design_import_ir.cpp reason="focused DesignIR stroke channel round-trip regression coverage"

Hotspot-Grow: core/view/src/design_ir_json.cpp reason="serialize and parse SVG stroke channels"

Hotspot-Grow: core/view/src/design_import_native_common.cpp reason="materialize SVG stroke channels in native imports"

Hotspot-Grow: core/view/src/design_codegen.cpp reason="emit SVG stroke channel bridge calls"
@danielraffel
danielraffel force-pushed the fix/svg-stroke-survival branch from 6f9c5bf to 9066bc1 Compare July 23, 2026 14:57
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit fd60b48 Jul 23, 2026
23 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant