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
+15 -3
View File
@@ -104,9 +104,21 @@ void main() {
expect(p.segments.first.op, SvgPathOp.moveTo);
});
test('defs and marker are skipped this slice', () {
final k = kids('<svg><defs><marker id="m"><polygon points="0,0 1,1"/></marker></defs><rect/></svg>');
expect(k.map((n) => n.runtimeType.toString()), ['SvgRect']);
test('marker defs are collected (not drawn inline) with the path marker-end ref', () {
final d = buildSvgDocument(
'<svg><defs><marker id="arrow" refX="7" refY="6" orient="auto" markerUnits="userSpaceOnUse">'
'<polygon points="0,0 10,6 0,12" fill="#0D32B2"/></marker></defs>'
'<path d="M0 0 L10 0" marker-end="url(#arrow)"/></svg>',
);
// defs/marker are not drawable children — only the path is.
expect(d.root.children.map((n) => n.runtimeType.toString()), ['SvgPath']);
// …but the marker is collected, with its content style resolved.
final m = d.markers['arrow']!;
expect(m.refX, 7);
expect(m.orientAuto, isTrue);
expect(m.strokeScaled, isFalse); // markerUnits=userSpaceOnUse
expect((m.children.single as SvgPolyline).style.fill, 0xFF0D32B2);
expect((d.root.children.single as SvgPath).markerEnd, 'arrow');
});
});
}
+19
View File
@@ -63,6 +63,25 @@ void main() {
expect(alpha(await argbAt(img, 5, 5)), closeTo(128, 4));
});
test('draws a marker-end arrowhead, rotated to the path direction', () async {
// A rightward path with a green right-pointing triangle marker at its end.
const svg =
'<svg viewBox="0 0 20 10">'
'<defs><marker id="a" refX="0" refY="2" orient="auto" markerUnits="userSpaceOnUse">'
'<polygon points="0,0 4,2 0,4" fill="#00FF00"/></marker></defs>'
'<path d="M2 5 L12 5" stroke="#000000" marker-end="url(#a)"/></svg>';
final data = (await (await render(svg, 40, 20)).toByteData())!;
var greenDrawn = false;
for (var i = 0; i < data.lengthInBytes; i += 4) {
final r = data.getUint8(i), g = data.getUint8(i + 1), b = data.getUint8(i + 2), a = data.getUint8(i + 3);
if (a > 200 && g > 200 && r < 80 && b < 80) {
greenDrawn = true;
break;
}
}
expect(greenDrawn, isTrue, reason: 'the green arrowhead should have been painted');
});
test('renders the real d2 fixture without error and draws ink', () async {
final svg = File('test/svg/fixtures/d2_pipeline.svg').readAsStringSync();
final img = await render(svg, 200, 120);