feat(svg): marker/arrowhead rendering for the drawing-card renderer (T-320)

Collect <marker> defs (refX/refY, orient=auto, markerUnits) and the path
marker-start/mid/end refs in the builder, then paint them at the path
endpoints rotated to the path tangent (orient=auto). The viewBox→viewport
scale is approximated 1:1 (holds for d2's markers). d2 edges now render
with arrowheads. Covered by dart test (builder) + a flutter-test pixel
probe (the green arrowhead draws). image painting remains the last T-320
follow-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:18:53 +02:00
co-authored by Claude Opus 4.8
parent 91607d54d1
commit 718d2b39c2
7 changed files with 248 additions and 14 deletions
+35
View File
@@ -8108,3 +8108,38 @@ CSS DECISION — inline-normalize (option A): d2 emits a <style> block of FLAT s
BUILD SPLIT: (1) inline-style normalizer; (2) path-data parser; (3) the CustomPaint painter (shapes/text/image/marker/transform); (4) injected image-href resolver (like image.show). Fixtures: real d2 + graphviz output.', NULL, '2026-06-28 17:54:38', '2026-06-28 17:54:38.871', '2026-06-28 17:54:38.871', NULL, 'afedea62e1486510e317fc4afb0df528', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYS06SDJGZ4W3Z9V9HDEATW', 'description', NULL, 'The pre-commit hook re-exports + stages .pql/changelog on commit but only catches the tickets + ticket_history tables — it leaves ticket_deps (blocker edges) and ticket_idmap (new T-NNN <-> record_id mappings) UNSTAGED. Seen repeatedly this session (D-103 re-sequencing blockers; T-495 id mapping), each needing a manual follow-up commit; otherwise the planning state silently doesn''t persist and a later branch switch drops it — exactly the failure the hook exists to prevent. Fix: stage ALL of .pql/changelog/ (git add .pql/changelog/), not just the tables the export rewrote. Repro: pql ticket block X --by Y + create a ticket, commit unrelated files, observe ticket_deps/ticket_idmap still modified afterward.', NULL, '2026-06-28 18:00:41', '2026-06-28 18:00:41.453', '2026-06-28 18:00:41.453', NULL, '81bf67c8003a8e48eb20c5c0b16fc90c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 18:04:13', '2026-06-28 18:04:13.608', '2026-06-28 18:04:13.608', NULL, 'd1705e1dbede4e9b9cbe8d1fa6e4352e', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'description', 'Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).
PROMOTED TO ENGINE (D-103, 2026-06-28): this is now the drawing-card''s rendering FOUNDATION, not one template among many. Build the clide-owned CustomPaint SVG renderer over a BOUNDED subset (rect/line/polyline/polygon/circle/ellipse/path, text incl. the Phosphor font, image, g + transform, basic fill/stroke/opacity/rx). T-318 (envelope + dispatch + Flutter overlay) and every template lower onto this; d2/graphviz/mermaid output renders for free. No longer blocked by T-318 (dependency inverted). Open: pin the subset to what our own templates + d2/graphviz emit. Schema: docs/design/drawing-card-schema.md.
SVG SUBSET FINALIZED (2026-06-28, grounded in a real d2 sample + our own templates):
STRUCTURE: <svg> (viewBox/width/height, incl. nested svg), <g> (transform, opacity, class), <defs>, <marker> (+ marker-start/mid/end; orient=auto, refX/refY, viewBox) edge arrowheads.
SHAPES: rect (rx/ry), circle, ellipse, line, polyline, polygon, path (full data: M L H V C S Q T A Z + relatives).
TEXT: text + tspan (x/y/dx/dy, font-family incl. Phosphor, font-size/weight, text-anchor, dominant-baseline).
RASTER: image (href/xlink:href, x/y/w/h, preserveAspectRatio) our image/compare templates.
STYLING: presentation attrs (fill, fill-opacity, stroke, stroke-width, stroke-linecap/linejoin, stroke-dasharray, opacity, color), transform (translate/scale/rotate/matrix), class= resolved via the normalizer below.
DEFERRED v1: <mask> (d2 masks connections for clean edge/node joins) ignore and rely on node-over-edge paint order; add basic mask only if d2 output looks wrong.
OUT: foreignObject, filters, <animate>/SMIL, scripting, <use>/<symbol>, gradients, patterns, clipPath. (So mermaid, which leans on foreignObject, is not a launch target.)
CSS DECISION inline-normalize (option A): d2 emits a <style> block of FLAT single-class selectors (.fill-B1, .color-N1, .shape, .connection, .text-bold presentation props). A preprocessing normalizer parses <style> into class->props, merges each element''s class props into inline presentation attributes (inline wins), and drops <style>; the painter then only ever sees inline attrs a pure, testable presentation-attribute renderer. Chosen over a render-time CSS resolver to keep the painter free of any CSS concept.
BUILD SPLIT: (1) inline-style normalizer; (2) path-data parser; (3) the CustomPaint painter (shapes/text/image/marker/transform); (4) injected image-href resolver (like image.show). Fixtures: real d2 + graphviz output.', 'Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).
PROMOTED TO ENGINE (D-103, 2026-06-28): this is now the drawing-card''s rendering FOUNDATION, not one template among many. Build the clide-owned CustomPaint SVG renderer over a BOUNDED subset (rect/line/polyline/polygon/circle/ellipse/path, text incl. the Phosphor font, image, g + transform, basic fill/stroke/opacity/rx). T-318 (envelope + dispatch + Flutter overlay) and every template lower onto this; d2/graphviz/mermaid output renders for free. No longer blocked by T-318 (dependency inverted). Open: pin the subset to what our own templates + d2/graphviz emit. Schema: docs/design/drawing-card-schema.md.
SVG SUBSET FINALIZED (2026-06-28, grounded in a real d2 sample + our own templates):
STRUCTURE: <svg> (viewBox/width/height, incl. nested svg), <g> (transform, opacity, class), <defs>, <marker> (+ marker-start/mid/end; orient=auto, refX/refY, viewBox) edge arrowheads.
SHAPES: rect (rx/ry), circle, ellipse, line, polyline, polygon, path (full data: M L H V C S Q T A Z + relatives).
TEXT: text + tspan (x/y/dx/dy, font-family incl. Phosphor, font-size/weight, text-anchor, dominant-baseline).
RASTER: image (href/xlink:href, x/y/w/h, preserveAspectRatio) our image/compare templates.
STYLING: presentation attrs (fill, fill-opacity, stroke, stroke-width, stroke-linecap/linejoin, stroke-dasharray, opacity, color), transform (translate/scale/rotate/matrix), class= resolved via the normalizer below.
DEFERRED v1: <mask> (d2 masks connections for clean edge/node joins) ignore and rely on node-over-edge paint order; add basic mask only if d2 output looks wrong.
OUT: foreignObject, filters, <animate>/SMIL, scripting, <use>/<symbol>, gradients, patterns, clipPath. (So mermaid, which leans on foreignObject, is not a launch target.)
CSS DECISION inline-normalize (option A): d2 emits a <style> block of FLAT single-class selectors (.fill-B1, .color-N1, .shape, .connection, .text-bold presentation props). A preprocessing normalizer parses <style> into class->props, merges each element''s class props into inline presentation attributes (inline wins), and drops <style>; the painter then only ever sees inline attrs a pure, testable presentation-attribute renderer. Chosen over a render-time CSS resolver to keep the painter free of any CSS concept.
BUILD SPLIT: (1) inline-style normalizer; (2) path-data parser; (3) the CustomPaint painter (shapes/text/image/marker/transform); (4) injected image-href resolver (like image.show). Fixtures: real d2 + graphviz output.
RENDERER CORE DONE (2026-06-28): the pure-Dart pipeline (5 parsers + typed model + builder; 82 dart-test cases incl. a real-d2 fixture) plus the CustomPaint painter (lib/widgets/src/svg/svg_painter.dart; 8 pixel-probe cases under flutter test) render real d2 output end to end shapes/text/transforms/group-opacity/viewBox all draw. Exposes paintSvg + SvgScenePainter + SvgView. REMAINING for T-320: (1) markers/arrowheads collect <marker> defs in the builder + paint marker-end at path endpoints (orient=auto); (2) image painting async href ui.Image via an injected resolver (like image.show), currently skipped. After those, T-318 (envelope/dispatch/overlay) consumes SvgView.', NULL, '2026-06-28 18:59:56', '2026-06-28 18:59:56.371', '2026-06-28 18:59:56.371', NULL, '46585e90df499aa7dce4886336e56752', 2) ON CONFLICT(hash) DO NOTHING;
+19
View File
@@ -9980,3 +9980,22 @@ OUT: foreignObject, filters, <animate>/SMIL, scripting, <use>/<symbol>, gradient
CSS DECISION inline-normalize (option A): d2 emits a <style> block of FLAT single-class selectors (.fill-B1, .color-N1, .shape, .connection, .text-bold presentation props). A preprocessing normalizer parses <style> into class->props, merges each element''s class props into inline presentation attributes (inline wins), and drops <style>; the painter then only ever sees inline attrs a pure, testable presentation-attribute renderer. Chosen over a render-time CSS resolver to keep the painter free of any CSS concept.
BUILD SPLIT: (1) inline-style normalizer; (2) path-data parser; (3) the CustomPaint painter (shapes/text/image/marker/transform); (4) injected image-href resolver (like image.show). Fixtures: real d2 + graphviz output.', 'in_progress', 'low', NULL, NULL, NULL, '2026-06-10 11:11:55', '2026-06-28 18:04:13.608', NULL, 'ea0df82b234d48fcb6008bb09c927578', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: SVG render', 'Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).
PROMOTED TO ENGINE (D-103, 2026-06-28): this is now the drawing-card''s rendering FOUNDATION, not one template among many. Build the clide-owned CustomPaint SVG renderer over a BOUNDED subset (rect/line/polyline/polygon/circle/ellipse/path, text incl. the Phosphor font, image, g + transform, basic fill/stroke/opacity/rx). T-318 (envelope + dispatch + Flutter overlay) and every template lower onto this; d2/graphviz/mermaid output renders for free. No longer blocked by T-318 (dependency inverted). Open: pin the subset to what our own templates + d2/graphviz emit. Schema: docs/design/drawing-card-schema.md.
SVG SUBSET FINALIZED (2026-06-28, grounded in a real d2 sample + our own templates):
STRUCTURE: <svg> (viewBox/width/height, incl. nested svg), <g> (transform, opacity, class), <defs>, <marker> (+ marker-start/mid/end; orient=auto, refX/refY, viewBox) edge arrowheads.
SHAPES: rect (rx/ry), circle, ellipse, line, polyline, polygon, path (full data: M L H V C S Q T A Z + relatives).
TEXT: text + tspan (x/y/dx/dy, font-family incl. Phosphor, font-size/weight, text-anchor, dominant-baseline).
RASTER: image (href/xlink:href, x/y/w/h, preserveAspectRatio) our image/compare templates.
STYLING: presentation attrs (fill, fill-opacity, stroke, stroke-width, stroke-linecap/linejoin, stroke-dasharray, opacity, color), transform (translate/scale/rotate/matrix), class= resolved via the normalizer below.
DEFERRED v1: <mask> (d2 masks connections for clean edge/node joins) ignore and rely on node-over-edge paint order; add basic mask only if d2 output looks wrong.
OUT: foreignObject, filters, <animate>/SMIL, scripting, <use>/<symbol>, gradients, patterns, clipPath. (So mermaid, which leans on foreignObject, is not a launch target.)
CSS DECISION inline-normalize (option A): d2 emits a <style> block of FLAT single-class selectors (.fill-B1, .color-N1, .shape, .connection, .text-bold presentation props). A preprocessing normalizer parses <style> into class->props, merges each element''s class props into inline presentation attributes (inline wins), and drops <style>; the painter then only ever sees inline attrs a pure, testable presentation-attribute renderer. Chosen over a render-time CSS resolver to keep the painter free of any CSS concept.
BUILD SPLIT: (1) inline-style normalizer; (2) path-data parser; (3) the CustomPaint painter (shapes/text/image/marker/transform); (4) injected image-href resolver (like image.show). Fixtures: real d2 + graphviz output.
RENDERER CORE DONE (2026-06-28): the pure-Dart pipeline (5 parsers + typed model + builder; 82 dart-test cases incl. a real-d2 fixture) plus the CustomPaint painter (lib/widgets/src/svg/svg_painter.dart; 8 pixel-probe cases under flutter test) render real d2 output end to end shapes/text/transforms/group-opacity/viewBox all draw. Exposes paintSvg + SvgScenePainter + SvgView. REMAINING for T-320: (1) markers/arrowheads collect <marker> defs in the builder + paint marker-end at path endpoints (orient=auto); (2) image painting async href ui.Image via an injected resolver (like image.show), currently skipped. After those, T-318 (envelope/dispatch/overlay) consumes SvgView.', 'in_progress', 'low', NULL, NULL, NULL, '2026-06-10 11:11:55', '2026-06-28 18:59:56.371', NULL, '3748efc57b0f8cd46f1665d805f491c6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);