From badcf3f63f682800464ca8ffd07cc45736eaaf0a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 23:07:27 -0800 Subject: [PATCH] Symbols: an instance that only moves holds art its 9-slice guides move A symbol whose natural size is not 20 x 20 is drawn through its 9-slice guides by an instance that only moves, so art the guides move can draw outside the art box moved by the instance. The visual box took that moved art box alone, and the renderer culled the instance when only the moved art was in view. The visual box now measures such an instance as it does a scaled one. --- crates/doc/src/node/ops.rs | 5 ++++- crates/render/src/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/node/ops.rs b/crates/doc/src/node/ops.rs index f76957258..eb7e0d39b 100644 --- a/crates/doc/src/node/ops.rs +++ b/crates/doc/src/node/ops.rs @@ -373,7 +373,10 @@ impl Node { NodeKind::SymbolInstance { symbol, xf } => { let b = match symbols.and_then(|s| Some((s, s.art(symbol)?))) { Some((s, art)) if !s.art_strokes() => xf.transform_rect_bbox(art.geometric), - Some((_, art)) if moves_only(*xf) => xf.transform_rect_bbox(art.visual), + // A symbol drawn at other than its natural size goes through its 9-slice guides. + Some((s, art)) if moves_only(*xf) && s.document().symbol_slice_map_of(symbol, art.geometric, *xf).is_none() => { + xf.transform_rect_bbox(art.visual) + } Some((s, art)) => { let placed = xf.transform_rect_bbox(art.geometric); match s.placed_visual(symbol, *xf).or_else(|| s.sliced_visual(symbol, *xf)) { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index dd7198d53..d5c129e5c 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -425,6 +425,29 @@ fn an_instance_is_culled_by_the_art_nested_9_slice_guides_move() { assert_eq!(img.pixel(95, 50), [255, 0, 0, 255]); } +#[test] +fn an_instance_that_only_moves_is_culled_by_the_art_its_9_slice_guides_move() { + // A symbol made in the app: art in the 20 × 20 square, a natural size of 2 × 2, and guides a + // quarter in. An instance that only moves draws it at 20 × 20, ten times its natural size. The + // bar from x −9.9 to 0.1 crosses the left guide and is placed by the middle column's map, + // x → 1.9x: moved 115 pt right, it draws from x 96.19 to 115.19 while its art box starts at + // x 105, out of view. + let mut d = Document::new(100.0, 100.0); + let bar = rect_node(&mut d, Rect::new(-9.9, -1.0, 0.1, 1.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Bar".into(), art: Arc::new(bar) }); + let frame = rect_node(&mut d, Rect::new(-10.0, -10.0, 10.0, 10.0), Paint::None, Paint::None, 0.0); + let inner = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Bar".into(), xf: Affine::IDENTITY }); + let art = Node::group(d.alloc_id(), vec![Arc::new(frame), Arc::new(inner)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Panel".into(), art: Arc::new(art) }); + d.unknown.insert(vectorcraft_doc::SYMBOL_SIZES.into(), serde_json::json!({ "Panel": [2.0, 2.0] })); + d.set_symbol_nine_slice("Panel", Some(vectorcraft_doc::SymbolNineSlice::default())); + let (id, l) = (d.alloc_id(), d.layers[0].id); + let xf = Affine::translate((115.0, 50.0)); + d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Panel".into(), xf })).unwrap(); + let img = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default()); + assert_eq!(img.pixel(98, 50), [255, 0, 0, 255]); +} + #[test] fn an_instance_with_a_geometry_effect_is_culled_by_its_reshaped_art() { // The art, (0, 0) to (20, 20), drawn from (-100, 10) is out of view, and the Transform effect