leafBounds method
Layout preserves existing pivot and interaction geometry. Paint is an estimate; null means no resolved source will be drawn. Text estimates use layout metrics, not guaranteed glyph-pixel bounds.
Implementation
({Rect2D layout, Rect2D? paint})? leafBounds(
Node n, {
Map<ElementId, PathIR>? pathIRById,
Map<ElementId, ImagePlacement>? imagePlacementById,
Map<ElementId, ResolvedIconText>? iconTextById,
Map<ElementId, PathIR>? iconPathIRById,
}) {
final s = services;
switch (n) {
case TextNode(:final data):
final m = s.textMeasurer.measure(
text: data.text,
fontFamily: data.fontFamily,
fontWeight: data.fontWeight,
fontSize: data.fontSize,
letterSpacing: data.letterSpacing,
);
final layout = Rect2D.fromLTWH(-m.w / 2, -m.h / 2, m.w, m.h);
final sourceBounds = data.text.isEmpty ? null : layout;
final appearance = data.appearance;
return (
layout: layout,
paint: estimateAppearancePaintBounds(
sourceBounds: sourceBounds,
foregroundPresent: appearance.foreground is! CanvasFillNone,
underlays: appearance.underlays,
),
);
case IconNode(id: final id, data: final d):
final size = d.sizePx;
final layout = Rect2D.fromLTWH(-size / 2, -size / 2, size, size);
final resolved = s.icons?.resolve(d.iconRef);
switch (resolved) {
case ResolvedIconText():
iconTextById?[id] = resolved;
if (resolved.glyph.isEmpty) {
return (layout: layout, paint: null);
}
// Match DrawTextOp's glyph metrics and center-origin painting.
// The stable icon square remains the layout/pivot geometry.
final m = s.textMeasurer.measure(
text: resolved.glyph,
fontFamily: resolved.fontFamily,
fontWeight: resolved.fontWeight,
fontSize: size,
letterSpacing: 0,
);
final sourceBounds = Rect2D.fromLTWH(-m.w / 2, -m.h / 2, m.w, m.h);
final appearance = d.appearance;
return (
layout: layout,
paint: estimateAppearancePaintBounds(
sourceBounds: sourceBounds,
foregroundPresent: appearance.foreground is! CanvasFillNone,
underlays: appearance.underlays,
),
);
case ResolvedIconPath(:final path):
final ir = compilePath(path);
iconPathIRById?[id] = ir;
final sourceBounds = ir.cmds.isEmpty
? null
: ir.localBounds(includeStroke: true);
final appearance = d.appearance;
return (
layout: layout,
paint: estimateAppearancePaintBounds(
sourceBounds: sourceBounds,
foregroundPresent: appearance.foreground is! CanvasFillNone,
underlays: appearance.underlays,
),
);
default:
return (layout: layout, paint: null);
}
case ImageNode(id: final id, data: final d):
final intrinsic = s.images?.intrinsicSize(id);
final size = d.size;
if (size.w <= 0 || size.h <= 0) return null;
final placement = imageSrcDst(
intrinsic: intrinsic ?? size,
layout: size,
fit: d.fit,
align: d.align,
);
imagePlacementById?[id] = placement;
// Preserve contain-fit destination geometry, including its offset.
return (layout: placement.dst, paint: placement.dst);
case PathNode(id: final id, data: final d):
final ir = compilePath(d);
pathIRById?[id] = ir;
final layout = ir.localBounds(includeStroke: true);
// Retain the existing path/stroke estimate in this PR.
return (layout: layout, paint: ir.cmds.isEmpty ? null : layout);
default:
return null;
}
}