From 0ec4605a324d77a78ebbdb9c135a1bff104aac90 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 14:32:21 -0400 Subject: [PATCH] M0.6: content bounds helpers for viewer handles Co-Authored-By: Claude Opus 5.5 --- crates/render/src/lib.rs | 19 +++++++++++++++++++ crates/render/src/shapes.rs | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 44e48b457..944c858ee 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -332,3 +332,22 @@ pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Ima #[cfg(test)] mod tests; + +/// Layer-space bounds `[x0, y0, x1, y1]` of a layer's content at the context time (source size +/// for solids/footage/precomps, glyph bounds for text, painted bounds for shapes). Used for viewer +/// handles and hit testing. +pub fn content_bounds(ctx: &EvalCtx, layer: &effectcraft_project::Layer) -> Option<[f64; 4]> { + match &layer.source { + LayerSource::Text => { + let glyphs = text::glyph_paths(ctx, layer); + let paths: Vec<_> = glyphs.into_iter().map(|g| g.0).collect(); + effectcraft_path::bounds(&paths).map(|r| [r.x0, r.y0, r.x1, r.y1]) + } + LayerSource::Shape => layer.props.sub("contents").and_then(|c| shapes::content_bounds(ctx, layer, c)).map(|r| [r.x0, r.y0, r.x1, r.y1]), + LayerSource::Camera | LayerSource::Light { .. } => None, + _ => { + let (w, h) = source_size(ctx.project, layer); + (w > 0 && h > 0).then_some([0.0, 0.0, w as f64, h as f64]) + } + } +} diff --git a/crates/render/src/shapes.rs b/crates/render/src/shapes.rs index f322fe81d..dfcbe8d82 100644 --- a/crates/render/src/shapes.rs +++ b/crates/render/src/shapes.rs @@ -485,3 +485,17 @@ pub fn render(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, s: f64) -> Buf #[cfg(test)] mod tests; + +/// Layer-space bounds of a shape layer's painted contents (strokes included), without +/// rasterising. +pub fn content_bounds(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup) -> Option { + let mut arena = Vec::new(); + let (draws, _) = collect(ctx, layer, contents, &mut arena); + draws + .iter() + .filter_map(|d| { + let paths: Vec = d.slots.iter().map(|&i| arena[i].clone()).filter(|p| !p.elements().is_empty()).collect(); + draw_bounds(d, &paths) + }) + .reduce(|a, b| a.union(b)) +}