Merge pull request #3271 from FractalEncrypt/fix/live-trace-bounds

Exclude retained trace bitmaps from result bounds and highlights
This commit is contained in:
BFlat
2026-10-21 16:17:22 +07:00
committed by GitHub
8 changed files with 121 additions and 21 deletions
+15 -8
View File
@@ -1159,6 +1159,17 @@ impl Node {
pub fn geometric_bounds_with(&self, symbols: &SymbolBoxes) -> Option<Rect> {
self.geometric(Some(symbols))
}
/// Children that contribute to a container's editing bounds and selection highlights. A live trace retains a
/// hidden bitmap for re-tracing/release; only source-image views measure that bitmap.
pub fn editing_children(&self) -> &[Arc<Node>] {
let children = self.children().map(Vec::as_slice).unwrap_or_default();
let trace_source = self.extras().trace.is_some()
&& !self.trace_view().shows_source()
&& children.first().is_some_and(|n| !n.visible && matches!(n.kind, NodeKind::Image(_)));
let skip = usize::from(self.extras().shaper.is_some() || trace_source);
children.get(skip..).unwrap_or_default()
}
/// The box of [`Node::geometric_bounds`], or of [`Node::geometric_bounds_with`] when `symbols`
/// is given.
pub(crate) fn geometric(&self, symbols: Option<&SymbolBoxes>) -> Option<Rect> {
@@ -1168,9 +1179,9 @@ impl Node {
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
children.first().and_then(|c| c.geometric(symbols))
}
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => children
NodeKind::Layer { .. } | NodeKind::Group { .. } | NodeKind::Compound { .. } => self
.editing_children()
.iter()
.skip(usize::from(self.extras().shaper.is_some()))
.filter(|c| c.visible || !matches!(self.kind, NodeKind::Layer { .. }))
.fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric(symbols))),
NodeKind::Text(t) => self.projected(t.bounds()),
@@ -1223,14 +1234,10 @@ impl Node {
}
}
NodeKind::Intertwine { children } => children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual(symbols))),
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => {
NodeKind::Layer { .. } | NodeKind::Group { .. } => {
// The container's own strokes paint around its members.
let o = self.appearance.outset();
children
.iter()
.skip(usize::from(self.extras().shaper.is_some()))
.fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual(symbols)))
.map(|b| b.inflate(o, o))
self.editing_children().iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual(symbols))).map(|b| b.inflate(o, o))
}
NodeKind::Blend { children, spec } => {
let b = children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual(symbols)));
+2 -2
View File
@@ -90,8 +90,8 @@ impl Node {
/// ([`Node::geometric_bounds_with`]).
pub fn bounds_in(&self, symbols: &SymbolBoxes, a: Affine, visual: bool) -> Option<Rect> {
match &self.kind {
NodeKind::Group { children, clip: false } => {
let b = children.iter().fold(None, |acc, c| union_opt(acc, c.bounds_in(symbols, a, visual)));
NodeKind::Group { clip: false, .. } => {
let b = self.editing_children().iter().fold(None, |acc, c| union_opt(acc, c.bounds_in(symbols, a, visual)));
// The group's own strokes paint around its members (as in `visual_bounds`).
let o = if visual { self.appearance.outset() } else { 0.0 };
b.map(|b| b.inflate(o, o))
+49
View File
@@ -89,4 +89,53 @@ mod tests {
g.extras_mut().trace = Some(Box::new(serde_json::json!({"view": "sepia"})));
assert_eq!(g.trace_view(), TraceView::Result);
}
#[test]
fn live_trace_result_bounds_exclude_the_retained_bitmap_at_any_angle() {
use crate::{Appearance, Document, ImageObject, NodeId, NodeKind};
use std::sync::Arc;
use vectorcraft_geom::{Affine, PathData, Point, Rect, SubPath};
let mut source = Node::new(
NodeId(2),
NodeKind::Image(Box::new(ImageObject {
key: "synthetic".into(),
width: 200,
height: 200,
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
})),
);
source.visible = false;
let art = Node::path(
NodeId(3),
PathData::single(SubPath::polyline(
&[Point::new(20.0, 30.0), Point::new(80.0, 30.0), Point::new(80.0, 90.0), Point::new(20.0, 90.0)],
true,
)),
Appearance::basic(vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), vectorcraft_color::Paint::None, 0.0),
);
let mut group = Node::group(NodeId(1), vec![Arc::new(source), Arc::new(art.clone())]);
group.extras_mut().trace = Some(Box::new(serde_json::json!({})));
let doc = Document::new(300.0, 300.0);
for view in TraceView::ALL {
group.extras_mut().trace = Some(Box::new(serde_json::json!({"view":view.id()})));
let want = if view.shows_source() { Rect::new(0.0, 0.0, 200.0, 200.0) } else { Rect::new(20.0, 30.0, 80.0, 90.0) };
assert_eq!(group.geometric_bounds(), Some(want));
assert_eq!(group.visual_bounds(), Some(want));
if !view.shows_source() {
for angle in [0.0_f64, 30.0, 90.0] {
let xf = Affine::rotate(angle.to_radians());
for visual in [false, true] {
assert_eq!(group.bounds_in(&doc.symbol_boxes(), xf, visual), art.bounds_in(&doc.symbol_boxes(), xf, visual));
}
}
}
}
group.extras_mut().trace = None;
assert_eq!(
group.geometric_bounds(),
Some(Rect::new(0.0, 0.0, 200.0, 200.0)),
"ordinary hidden group members retain their existing bounds policy"
);
}
}
+38 -3
View File
@@ -1884,8 +1884,7 @@ fn too_many_anchors<'a>(nodes: impl IntoIterator<Item = &'a Node>) -> bool {
if n.clips() {
return n.children().and_then(|children| children.first()).is_some_and(|clip| over(clip, left));
}
!matches!(n.kind, NodeKind::Envelope { .. })
&& n.children().into_iter().flatten().skip(usize::from(n.extras().shaper.is_some())).any(|c| over(c, left))
!matches!(n.kind, NodeKind::Envelope { .. }) && n.editing_children().iter().any(|c| over(c, left))
}
let mut left = OVERLAY_MAX_ANCHORS;
nodes.into_iter().any(|n| over(n, &mut left))
@@ -1929,7 +1928,7 @@ fn walk_drawn<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
}
return;
}
for c in n.children().into_iter().flatten().skip(usize::from(n.extras().shaper.is_some())) {
for c in n.editing_children().iter() {
walk_drawn(c, f);
}
}
@@ -8245,4 +8244,40 @@ mod tests {
assert!((after - want).length() < 0.5, "bar {k}: {before:?} -> {after:?}, want {want:?}");
}
}
#[test]
fn live_trace_selection_highlight_excludes_the_retained_source_rectangle() {
use std::sync::Arc;
use vectorcraft_doc::{Appearance, ImageObject, NodeId, TraceView};
let mut source = Node::new(
NodeId(2),
NodeKind::Image(Box::new(ImageObject {
key: "synthetic".into(),
width: 200,
height: 200,
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
})),
);
source.visible = false;
let art =
Node::path(NodeId(3), vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(20.0, 30.0, 80.0, 90.0)), Appearance::default());
let mut group = Node::group(NodeId(1), vec![Arc::new(source), Arc::new(art)]);
let doc = vectorcraft_doc::Document::new(300.0, 300.0);
let symbols = doc.symbol_boxes();
for view in TraceView::ALL {
group.extras_mut().trace = Some(Box::new(json!({"view":view.id()})));
let mut visited = vec![];
walk_drawn(&group, &mut |n| visited.push(n.id));
assert_eq!(visited.contains(&NodeId(2)), view.shows_source());
let want = if view.shows_source() {
vectorcraft_geom::Rect::new(0.0, 0.0, 200.0, 200.0)
} else {
vectorcraft_geom::Rect::new(20.0, 30.0, 80.0, 90.0)
};
assert_eq!(node_outline(&group, &symbols).bounding_box(), want);
}
group.extras_mut().trace = None;
assert_eq!(node_outline(&group, &symbols).bounding_box(), vectorcraft_geom::Rect::new(0.0, 0.0, 200.0, 200.0));
}
}
+4 -1
View File
File diff suppressed because one or more lines are too long
+5 -4
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long