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
+28 -2
View File
@@ -96,8 +96,31 @@ class SvgPolyline extends SvgNode {
}
class SvgPath extends SvgNode {
const SvgPath(super.style, super.transform, this.segments);
const SvgPath(super.style, super.transform, this.segments, {this.markerStart, this.markerMid, this.markerEnd});
final List<SvgPathSeg> segments;
/// Ids (sans `url(#…)`) of `marker-start`/`mid`/`end` definitions, if any.
final String? markerStart, markerMid, markerEnd;
}
/// A `<marker>` definition (e.g. an arrowhead), referenced by `marker-*`.
class SvgMarker {
const SvgMarker({
required this.refX,
required this.refY,
required this.orientAuto,
required this.orientAngle,
required this.strokeScaled,
required this.children,
this.viewBox,
});
final double refX, refY;
final bool orientAuto; // orient="auto"
final double orientAngle; // fixed angle (degrees) when not auto
final bool strokeScaled; // markerUnits=strokeWidth (default) vs userSpaceOnUse
final SvgViewBox? viewBox;
final List<SvgNode> children;
}
class SvgText extends SvgNode {
@@ -121,11 +144,14 @@ class SvgViewBox {
/// A parsed SVG document: optional intrinsic [width]/[height] (px), optional
/// [viewBox], and the [root] group.
class SvgDocument {
const SvgDocument({this.width, this.height, this.viewBox, required this.root});
const SvgDocument({this.width, this.height, this.viewBox, required this.root, this.markers = const {}});
static const empty = SvgDocument(root: SvgGroup(SvgStyle.initial, null, []));
final double? width, height;
final SvgViewBox? viewBox;
final SvgGroup root;
/// `<marker>` definitions by id, referenced by path `marker-*`.
final Map<String, SvgMarker> markers;
}