leafBounds method

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

Layout describes intrinsic geometry independently of appearance. Paint is an estimate; null means no visible paint is expected. 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, ComputedIcon>? iconById,
}) {
  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: fillContributesToPaint(appearance.foreground),
          underlays: appearance.underlays,
        ),
      );

    case IconNode(id: final id, data: final d):
      final size = d.sizePx;
      if (!size.isFinite || size <= 0) {
        throw ArgumentError.value(
          size,
          'sizePx',
          'Must be finite and positive.',
        );
      }
      final layout = Rect2D.fromLTWH(-size / 2, -size / 2, size, size);
      final resolved = s.icons?.resolve(d.iconRef);

      if (resolved == null) return (layout: layout, paint: null);
      final icon = ComputedIcon.fit(resolved, size);
      iconById?[id] = icon;
      final sourceBounds = icon.pathIR.cmds.isEmpty
          ? null
          : aabbOfTransformedRect(
              icon.pathIR.localBounds(),
              icon.sourceToLocal,
            );
      final appearance = d.appearance;
      return (
        layout: layout,
        paint: estimateAppearancePaintBounds(
          sourceBounds: sourceBounds,
          foregroundPresent: fillContributesToPaint(appearance.foreground),
          underlays: appearance.underlays,
        ),
      );

    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 geometryBounds = ir.localBounds();
      final hasSegments = ir.cmds.any(
        (cmd) => cmd.verb != PathVerb.moveTo && cmd.verb != PathVerb.close,
      );
      return (
        layout: geometryBounds,
        paint: estimatePathPaintBounds(
          sourceBounds: hasSegments ? geometryBounds : null,
          appearance: d.appearance,
        ),
      );

    default:
      return null;
  }
}