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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user