leafBounds method

({Rect2D layout, Rect2D? paint})? leafBounds(
  1. Node n, {
  2. Map<ElementId, PathIR>? pathIRById,
  3. Map<ElementId, ImagePlacement>? imagePlacementById,
  4. Map<ElementId, ResolvedIconText>? iconTextById,
  5. Map<ElementId, PathIR>? iconPathIRById,
})

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;
  }
}