From d2625ec10845df50ba85e0163840d02e60085084 Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Tue, 20 Oct 2026 22:42:57 +0200 Subject: [PATCH] Fix remaining Hide Guides rendering cases (#3219) Include guide visibility in effect raster cache inputs, skip hidden guide paths in outline members, and clear selected guide paths when hiding guides. Add regression coverage for cached effects and selection overlays.\n\nThe cached-effect regression test is adapted from the overlapping #3264 implementation.\n\nCo-authored-by: Mahtab --- crates/render/src/fx.rs | 2 ++ crates/render/src/lib.rs | 2 +- crates/render/src/tests_layeropts.rs | 48 ++++++++++++++++++++++++++++ crates/ui-egui/src/canvas.rs | 16 ++++++++++ crates/ui-egui/src/menus.rs | 15 +++++++-- 5 files changed, 79 insertions(+), 4 deletions(-) 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