Files
clide/lib/src/svg/svg_node.dart
T
jpmschweitzerandClaude Opus 4.8 78b0a7b8a4 feat(svg): extract per-object data-* annotations for the drawing-card overlay (T-318)
The builder captures data-label / data-description / data-lightbox on SVG
elements into SvgDocument.annotations, each with the element's AABB in
viewBox coordinates (accumulated transform applied — rect/ellipse/image/
line/poly/path bounded; groups skipped, text degenerates to its anchor
point). Feeds the Flutter caption/lightbox overlay next. Flutter-free,
covered by dart test (6 new cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:31:04 +02:00

186 lines
5.8 KiB
Dart

/// The typed SVG scene model the painter draws (T-320 / D-103).
///
/// The document builder ([buildSvgDocument]) lowers a normalized XML tree into
/// this model: every node carries a resolved [SvgStyle] (inheritance already
/// flattened), an optional [Affine] transform, and typed geometry. Colours are
/// packed ARGB ints, not `dart:ui` Colors, so the model stays Flutter-free and
/// `dart test`-able; the painter converts.
///
/// `null` style fields mean "unspecified" — the painter applies the SVG default
/// (fill black, stroke none, stroke-width 1).
library;
import 'svg_path.dart';
import 'svg_transform.dart';
enum SvgLineCap { butt, round, square }
enum SvgLineJoin { miter, round, bevel }
enum SvgTextAnchor { start, middle, end }
enum SvgBaseline { auto, middle, hanging }
/// Resolved presentation style for a node, with inheritance already applied.
class SvgStyle {
const SvgStyle({
this.fill,
this.stroke,
this.strokeWidth,
this.opacity = 1.0,
this.fillOpacity,
this.strokeOpacity,
this.dashArray,
this.lineCap,
this.lineJoin,
this.fontFamily,
this.fontSize,
this.fontWeight,
this.textAnchor,
this.baseline,
});
/// The root inheritance context: nothing specified, full opacity.
static const initial = SvgStyle();
final int? fill; // ARGB; 0x00000000 = explicit none
final int? stroke; // ARGB
final double? strokeWidth;
final double opacity; // element/group opacity — NOT inherited
final double? fillOpacity;
final double? strokeOpacity;
final List<double>? dashArray;
final SvgLineCap? lineCap;
final SvgLineJoin? lineJoin;
final String? fontFamily;
final double? fontSize;
final int? fontWeight; // 400, 700, …
final SvgTextAnchor? textAnchor;
final SvgBaseline? baseline;
}
/// A node in the typed scene — group or leaf. [style] is fully resolved;
/// [transform] is `null` when identity.
sealed class SvgNode {
const SvgNode(this.style, this.transform);
final SvgStyle style;
final Affine? transform;
}
class SvgGroup extends SvgNode {
const SvgGroup(super.style, super.transform, this.children);
final List<SvgNode> children;
}
class SvgRect extends SvgNode {
const SvgRect(super.style, super.transform, this.x, this.y, this.width, this.height, this.rx, this.ry);
final double x, y, width, height, rx, ry;
}
/// Circle is an ellipse with `rx == ry`.
class SvgEllipse extends SvgNode {
const SvgEllipse(super.style, super.transform, this.cx, this.cy, this.rx, this.ry);
final double cx, cy, rx, ry;
}
class SvgLine extends SvgNode {
const SvgLine(super.style, super.transform, this.x1, this.y1, this.x2, this.y2);
final double x1, y1, x2, y2;
}
/// Polyline (open) or polygon (`closed == true`). [points] is `[x0,y0,x1,y1,…]`.
class SvgPolyline extends SvgNode {
const SvgPolyline(super.style, super.transform, this.points, this.closed);
final List<double> points;
final bool closed;
}
class SvgPath extends SvgNode {
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 {
const SvgText(super.style, super.transform, this.x, this.y, this.text);
final double x, y;
final String text;
}
class SvgImage extends SvgNode {
const SvgImage(super.style, super.transform, this.x, this.y, this.width, this.height, this.href);
final double x, y, width, height;
final String href;
}
/// `viewBox="minX minY width height"`.
class SvgViewBox {
const SvgViewBox(this.minX, this.minY, this.width, this.height);
final double minX, minY, width, height;
}
/// A per-object overlay annotation (T-318, D-103): a caption and/or lightbox
/// affordance anchored to an SVG element carrying `data-label` /
/// `data-description` / `data-lightbox`. The rect is the element's axis-aligned
/// bounding box in viewBox (user-space) coordinates with its transform applied;
/// the Flutter overlay maps it through the same viewBox→size fit the painter
/// uses, so captions sit under the right spot.
class SvgAnnotation {
const SvgAnnotation({
required this.x,
required this.y,
required this.width,
required this.height,
this.label,
this.description,
this.lightbox = false,
this.href,
});
final double x, y, width, height;
final String? label, description;
final bool lightbox;
final String? href; // image href, for a lightbox target
}
/// A parsed SVG document: optional intrinsic [width]/[height] (px), optional
/// [viewBox], the [root] group, marker defs, and per-object overlay
/// [annotations].
class SvgDocument {
const SvgDocument({this.width, this.height, this.viewBox, required this.root, this.markers = const {}, this.annotations = 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;
/// Overlay captions/lightbox anchors extracted from `data-*` attributes.
final List<SvgAnnotation> annotations;
}