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>
This commit is contained in:
@@ -8808,3 +8808,22 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
- CI validates both code paths
|
||||
- Performance baseline established
|
||||
', NULL, '2026-06-28 19:58:30', '2026-06-28 19:58:30.563', '2026-06-28 19:58:30.563', NULL, 'c1a3467c9be21b5b1d57ef7873a8f678', 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 ('06FB2ERREMEEF26KKHGNZBWW64', 'description', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', NULL, '2026-06-28 20:25:50', '2026-06-28 20:25:50.683', '2026-06-28 20:25:50.683', NULL, 'e8481d8539f90b8d5fd2dddb1932e2a0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -10603,3 +10603,14 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
- CI validates both code paths
|
||||
- Performance baseline established
|
||||
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-28 19:56:47.977', '2026-06-28 19:58:30.563', NULL, 'f8afde31947ae623adf0d6fcf226f5a2', 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 ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:25:50.683', NULL, 'a5a8e71c0e31375e52004b37b181b484', 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);
|
||||
|
||||
@@ -28,13 +28,17 @@ SvgDocument buildSvgDocument(String src) {
|
||||
final markers = <String, SvgMarker>{};
|
||||
_collectMarkers(root, markers);
|
||||
|
||||
final annotations = <SvgAnnotation>[];
|
||||
final style = _resolveStyle(root.attrs, SvgStyle.initial);
|
||||
final rootTf = _transform(root.attrs['transform']);
|
||||
final children = _children(root, style, rootTf ?? Affine.identity, annotations);
|
||||
return SvgDocument(
|
||||
width: _lenN(root.attrs['width']),
|
||||
height: _lenN(root.attrs['height']),
|
||||
viewBox: _viewBox(root.attrs['viewBox']),
|
||||
root: SvgGroup(style, _transform(root.attrs['transform']), _children(root, style)),
|
||||
root: SvgGroup(style, rootTf, children),
|
||||
markers: markers,
|
||||
annotations: annotations,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +62,7 @@ SvgMarker _marker(XmlElement el) {
|
||||
orientAngle: (orient != null && !auto) ? (_stripNum(orient) ?? 0) : 0,
|
||||
strokeScaled: el.attrs['markerUnits'] != 'userSpaceOnUse', // default = strokeWidth
|
||||
viewBox: _viewBox(el.attrs['viewBox']),
|
||||
children: _children(el, SvgStyle.initial),
|
||||
children: _children(el, SvgStyle.initial, Affine.identity, []), // markers don't carry annotations
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,26 +73,33 @@ String? _markerRef(String? v) {
|
||||
return m?.group(1) ?? (v.startsWith('#') ? v.substring(1) : null);
|
||||
}
|
||||
|
||||
List<SvgNode> _children(XmlElement el, SvgStyle inherited) {
|
||||
List<SvgNode> _children(XmlElement el, SvgStyle inherited, Affine accumulated, List<SvgAnnotation> annotations) {
|
||||
final out = <SvgNode>[];
|
||||
for (final c in el.children) {
|
||||
if (c is XmlElement) {
|
||||
final n = _node(c, inherited);
|
||||
final n = _node(c, inherited, accumulated, annotations);
|
||||
if (n != null) out.add(n);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
SvgNode? _node(XmlElement el, SvgStyle inherited) {
|
||||
SvgNode? _node(XmlElement el, SvgStyle inherited, Affine accumulated, List<SvgAnnotation> annotations) {
|
||||
final style = _resolveStyle(el.attrs, inherited);
|
||||
final tf = _transform(el.attrs['transform']);
|
||||
final acc = tf == null ? accumulated : accumulated.multiply(tf);
|
||||
final node = _build(el, style, tf, acc, annotations);
|
||||
if (node != null) _maybeAnnotate(el, node, acc, annotations);
|
||||
return node;
|
||||
}
|
||||
|
||||
SvgNode? _build(XmlElement el, SvgStyle style, Affine? tf, Affine acc, List<SvgAnnotation> annotations) {
|
||||
final a = el.attrs;
|
||||
switch (el.name) {
|
||||
case 'g':
|
||||
case 'a':
|
||||
case 'svg':
|
||||
return SvgGroup(style, tf, _children(el, style));
|
||||
return SvgGroup(style, tf, _children(el, style, acc, annotations));
|
||||
case 'rect':
|
||||
final rx = _numN(a['rx']), ry = _numN(a['ry']);
|
||||
return SvgRect(style, tf, _num(a['x']), _num(a['y']), _num(a['width']), _num(a['height']), rx ?? ry ?? 0, ry ?? rx ?? 0);
|
||||
@@ -121,6 +132,88 @@ SvgNode? _node(XmlElement el, SvgStyle inherited) {
|
||||
}
|
||||
}
|
||||
|
||||
void _maybeAnnotate(XmlElement el, SvgNode node, Affine acc, List<SvgAnnotation> annotations) {
|
||||
final label = el.attrs['data-label'];
|
||||
final desc = el.attrs['data-description'];
|
||||
final lightbox = el.attrs.containsKey('data-lightbox');
|
||||
if (label == null && desc == null && !lightbox) return;
|
||||
final box = _localBBox(node);
|
||||
if (box == null) return;
|
||||
final r = _transformedAABB(box, acc);
|
||||
annotations.add(
|
||||
SvgAnnotation(x: r[0], y: r[1], width: r[2], height: r[3], label: label, description: desc, lightbox: lightbox, href: node is SvgImage ? node.href : null),
|
||||
);
|
||||
}
|
||||
|
||||
/// Axis-aligned bounding box of [node] in its own local coordinates. Groups are
|
||||
/// not bounded (anchor leaf shapes); text degenerates to its anchor point.
|
||||
List<double>? _localBBox(SvgNode node) {
|
||||
switch (node) {
|
||||
case SvgRect r:
|
||||
return [r.x, r.y, r.width, r.height];
|
||||
case SvgImage i:
|
||||
return [i.x, i.y, i.width, i.height];
|
||||
case SvgEllipse e:
|
||||
return [e.cx - e.rx, e.cy - e.ry, e.rx * 2, e.ry * 2];
|
||||
case SvgLine l:
|
||||
return _aabbOfPoints([l.x1, l.y1, l.x2, l.y2]);
|
||||
case SvgPolyline p:
|
||||
return _aabbOfPoints(p.points);
|
||||
case SvgPath p:
|
||||
return _aabbOfPoints(_pathPoints(p.segments));
|
||||
case SvgText t:
|
||||
return [t.x, t.y, 0, 0];
|
||||
case SvgGroup _:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<double> _pathPoints(List<SvgPathSeg> segs) {
|
||||
final pts = <double>[];
|
||||
for (final s in segs) {
|
||||
switch (s.op) {
|
||||
case SvgPathOp.moveTo:
|
||||
case SvgPathOp.lineTo:
|
||||
pts.addAll([s.args[0], s.args[1]]);
|
||||
case SvgPathOp.cubicTo:
|
||||
pts.addAll([s.args[0], s.args[1], s.args[2], s.args[3], s.args[4], s.args[5]]);
|
||||
case SvgPathOp.quadTo:
|
||||
pts.addAll([s.args[0], s.args[1], s.args[2], s.args[3]]);
|
||||
case SvgPathOp.arcTo:
|
||||
pts.addAll([s.args[5], s.args[6]]);
|
||||
case SvgPathOp.close:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
List<double>? _aabbOfPoints(List<double> pts) {
|
||||
if (pts.length < 2) return null;
|
||||
var minX = pts[0], minY = pts[1], maxX = pts[0], maxY = pts[1];
|
||||
for (var i = 0; i + 1 < pts.length; i += 2) {
|
||||
minX = pts[i] < minX ? pts[i] : minX;
|
||||
maxX = pts[i] > maxX ? pts[i] : maxX;
|
||||
minY = pts[i + 1] < minY ? pts[i + 1] : minY;
|
||||
maxY = pts[i + 1] > maxY ? pts[i + 1] : maxY;
|
||||
}
|
||||
return [minX, minY, maxX - minX, maxY - minY];
|
||||
}
|
||||
|
||||
/// Transform [box] = `[x, y, w, h]` by [m]; return the AABB `[x, y, w, h]`.
|
||||
List<double> _transformedAABB(List<double> box, Affine m) {
|
||||
final x = box[0], y = box[1], w = box[2], h = box[3];
|
||||
final corners = [m.apply(x, y), m.apply(x + w, y), m.apply(x, y + h), m.apply(x + w, y + h)];
|
||||
var minX = corners[0].$1, minY = corners[0].$2, maxX = corners[0].$1, maxY = corners[0].$2;
|
||||
for (final c in corners) {
|
||||
minX = c.$1 < minX ? c.$1 : minX;
|
||||
maxX = c.$1 > maxX ? c.$1 : maxX;
|
||||
minY = c.$2 < minY ? c.$2 : minY;
|
||||
maxY = c.$2 > maxY ? c.$2 : maxY;
|
||||
}
|
||||
return [minX, minY, maxX - minX, maxY - minY];
|
||||
}
|
||||
|
||||
SvgStyle _resolveStyle(Map<String, String> a, SvgStyle inh) {
|
||||
int? color(String k, int? fb) {
|
||||
final v = a[k];
|
||||
|
||||
@@ -141,10 +141,35 @@ class SvgViewBox {
|
||||
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], and the [root] group.
|
||||
/// [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 {}});
|
||||
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, []));
|
||||
|
||||
@@ -154,4 +179,7 @@ class SvgDocument {
|
||||
|
||||
/// `<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;
|
||||
}
|
||||
|
||||
@@ -120,5 +120,40 @@ void main() {
|
||||
expect((m.children.single as SvgPolyline).style.fill, 0xFF0D32B2);
|
||||
expect((d.root.children.single as SvgPath).markerEnd, 'arrow');
|
||||
});
|
||||
|
||||
test('a rect with data-label/data-description yields one annotation at its bbox', () {
|
||||
final d = buildSvgDocument('<svg viewBox="0 0 100 100"><rect x="10" y="20" width="30" height="40" data-label="Box" data-description="a box"/></svg>');
|
||||
final a = d.annotations.single;
|
||||
expect([a.x, a.y, a.width, a.height], [10, 20, 30, 40]);
|
||||
expect(a.label, 'Box');
|
||||
expect(a.description, 'a box');
|
||||
expect(a.lightbox, isFalse);
|
||||
});
|
||||
|
||||
test('data-lightbox on an image yields a lightbox annotation carrying the href', () {
|
||||
final d = buildSvgDocument('<svg><image x="0" y="0" width="8" height="8" href="pic.png" data-lightbox=""/></svg>');
|
||||
final a = d.annotations.single;
|
||||
expect(a.lightbox, isTrue);
|
||||
expect(a.href, 'pic.png');
|
||||
});
|
||||
|
||||
test('the annotation bbox respects the element transform', () {
|
||||
final d = buildSvgDocument('<svg><rect x="0" y="0" width="10" height="10" transform="translate(5,5)" data-label="x"/></svg>');
|
||||
expect([d.annotations.single.x, d.annotations.single.y], [5, 5]);
|
||||
});
|
||||
|
||||
test('the annotation bbox respects an inherited group transform', () {
|
||||
final d = buildSvgDocument('<svg><g transform="translate(100,0)"><rect x="0" y="0" width="10" height="10" data-label="x"/></g></svg>');
|
||||
final a = d.annotations.single;
|
||||
expect([a.x, a.y, a.width, a.height], [100, 0, 10, 10]);
|
||||
});
|
||||
|
||||
test('elements without data-* produce no annotations', () {
|
||||
expect(buildSvgDocument('<svg><rect width="10" height="10"/></svg>').annotations, isEmpty);
|
||||
});
|
||||
|
||||
test('a group with data-label is skipped — annotations anchor leaf shapes', () {
|
||||
expect(buildSvgDocument('<svg><g data-label="grp"><rect width="10" height="10"/></g></svg>').annotations, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user