diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index cc95cb9f6..f078d235c 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -2065,6 +2065,8 @@ impl Renderer { let doc = self.doc_inputs(f.doc); let mut h = DefaultHasher::new(); (doc.cms, f.opts.anti_alias.threshold(), self.knockout, f.doc.raster_effects_ppi.to_bits()).hash(&mut h); + // Cached effects on containers can include paths made into guides. + f.opts.hide_guides.hash(&mut h); if let Some((isolated, path)) = self.isolation_path(f) && n.id != isolated && path.contains(&n.id) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c8f800693..6855cc872 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1386,7 +1386,7 @@ impl Renderer { /// Outline view, culled by its cached bounds ([`Self::bounds_of`]) and drawn with `c` as /// `fx_owner`, so its path and live-effect geometry come from the caches. fn draw_outline_member(&mut self, ctx: &mut RenderContext, f: &Frame, c: &Arc) { - if !c.visible || f.opts.hidden.contains(&c.id) { + if !c.visible || f.opts.hidden.contains(&c.id) || f.opts.hide_guides && matches!(c.kind, NodeKind::Path { guide: true, .. }) { return; } match self.bounds_of(f.symbols, c) { diff --git a/crates/render/src/tests_layeropts.rs b/crates/render/src/tests_layeropts.rs index 7130d654b..6555e827f 100644 --- a/crates/render/src/tests_layeropts.rs +++ b/crates/render/src/tests_layeropts.rs @@ -186,3 +186,51 @@ fn hide_guides_hides_guide_paths_on_screen() { assert!(pixels(false) > 0, "Show Guides draws the guide path"); assert_eq!(pixels(true), 0, "Hide Guides leaves the guide path out"); } + +/// #3219: hiding guides omits guide paths, including those inside cached group effects, without +/// hiding ordinary artwork. The same renderer must show them again when the view is toggled back. +#[test] +fn hidden_guide_paths_leave_the_artwork_and_refresh_cached_effects() { + for effect in [false, true] { + let mut d = Document::new(120.0, 120.0); + let layer = d.layers[0].id; + let mut guide = + Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.5, 20.5, 60.5, 60.5)), Appearance::basic(Paint::None, Paint::None, 0.0)); + if let NodeKind::Path { guide, .. } = &mut guide.kind { + *guide = true; + } + let id = guide.id; + let art = Node::path( + d.alloc_id(), + shapes::rectangle(Rect::new(80.0, 80.0, 100.0, 100.0)), + Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0), + ); + let mut group = Node::group(d.alloc_id(), vec![Arc::new(guide), Arc::new(art)]); + if effect { + group.appearance.effects.push(vectorcraft_doc::Effect { + id: "blur.gaussian".into(), + params: serde_json::json!({"radius":2}), + visible: true, + }); + } + d.insert(Some(layer), 0, group).unwrap(); + let mut without_guide = d.clone(); + without_guide.remove(id).unwrap(); + let mut renderer = Renderer::new(); + for outline in [false, true] { + let opts = RenderOptions { outline, background: Some([255; 4]), ..Default::default() }; + let shown = renderer.render(&d, 120, 120, Affine::IDENTITY, &opts); + let hidden = RenderOptions { hide_guides: true, ..opts.clone() }; + let expected = Renderer::new().render(&without_guide, 120, 120, Affine::IDENTITY, &hidden); + assert_ne!(shown.pixels, expected.pixels, "the guide is initially visible, effect={effect}, outline={outline}"); + assert!( + renderer.render(&d, 120, 120, Affine::IDENTITY, &hidden).pixels == expected.pixels, + "only guides hide, effect={effect}, outline={outline}" + ); + assert!( + renderer.render(&d, 120, 120, Affine::IDENTITY, &opts).pixels == shown.pixels, + "guides show again, effect={effect}, outline={outline}" + ); + } + } +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d800b2713..8c989b2e9 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -5630,6 +5630,22 @@ mod tests { assert_eq!(dots(&mut app).len(), 2, "one anchor still shows them"); } + /// #3219: a selected path made into a guide is deselected when guides are hidden, so the + /// selection overlay cannot reveal the hidden path. + #[test] + fn hide_guides_deselects_selected_guide_paths() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap(); + app.run("view.guides.make", json!({})).unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + assert_eq!(app.session.active().unwrap().selection.objects, vec![vectorcraft_doc::NodeId(id)]); + + app.run("view.guides", json!({})).unwrap(); + + assert!(app.session.active().unwrap().selection.objects.is_empty()); + } + /// Smart Guides › Object Highlighting (#394): the outline of the object under the pointer does not /// show with View › Snap to Grid or View › Pixel Preview on, even with Smart Guides and the option on. /// #3219: guide paths hidden by View > Hide Guides are not hittable and cannot reveal diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 557ddfccd..b9318dfdd 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1250,9 +1250,18 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option flag(&mut app.ui.view.bounding_box), "view.guides" => { let r = flag(&mut app.ui.view.guides); - // Hidden guides can't stay selected (selected, they are all that is). - if !app.ui.view.guides && app.session.active().is_some_and(|d| !d.selection.guides.is_empty()) { - // Deselecting a document that is open can't fail. + // Hidden ruler guides and guide paths cannot stay selected: their selection overlay + // would otherwise reveal geometry that Hide Guides just hid. + let hidden_selection = !app.ui.view.guides + && app.session.active().is_some_and(|d| { + !d.selection.guides.is_empty() + || d.selection + .objects + .iter() + .any(|id| matches!(d.doc.node(*id).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Path { guide: true, .. }))) + }); + if hidden_selection { + // Deselecting a document that is open cannot fail. let _ = app.run("select.none", json!({})); } r