From 2305ebb931cd16c3e2a3bed2ded4f1b735d624b1 Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Tue, 20 Oct 2026 21:01:51 +0200 Subject: [PATCH 1/2] Fix Show and Hide Guides (#3219) --- crates/render/src/lib.rs | 10 +++++++--- crates/render/src/tests_layeropts.rs | 20 ++++++++++++++++++++ crates/ui-egui/src/canvas.rs | 23 ++++++++++++++++++++++- crates/ui-egui/src/lib.rs | 4 +++- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 442e156cb..c8f800693 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -125,6 +125,9 @@ pub struct RenderOptions { pub skip_templates: bool, /// View › Hide Template: template layers left out of the screen view (guides stay). pub hide_templates: bool, + /// View › Hide Guides: paths made into guides left out of the screen view. Exports use + /// `skip_templates`, which independently leaves both templates and guides out. + pub hide_guides: bool, /// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge /// Color), RGB. pub tile_edge: [u8; 3], @@ -246,6 +249,7 @@ impl Default for RenderOptions { trim: false, skip_templates: false, hide_templates: false, + hide_guides: false, tile_edge: vectorcraft_doc::LAYER_COLORS[0].1, guide: ([0x4a, 0xd8, 0xff], false), mask_view: None, @@ -1036,9 +1040,9 @@ impl Renderer { /// Whether `a` is left out of this frame: hidden, a skipped template or guide, or culled. fn skipped(&mut self, f: &Frame, a: &Arc) -> bool { - let skipped_template = (f.opts.skip_templates || f.opts.hide_templates) && matches!(a.kind, NodeKind::Layer { template: true, .. }) - || f.opts.skip_templates && matches!(a.kind, NodeKind::Path { guide: true, .. }); - if !a.visible || skipped_template || f.opts.hidden.contains(&a.id) { + let skipped_view_item = (f.opts.skip_templates || f.opts.hide_templates) && matches!(a.kind, NodeKind::Layer { template: true, .. }) + || (f.opts.skip_templates || f.opts.hide_guides) && matches!(a.kind, NodeKind::Path { guide: true, .. }); + if !a.visible || skipped_view_item || f.opts.hidden.contains(&a.id) { return true; } match self.bounds_of(f.symbols, a) { diff --git a/crates/render/src/tests_layeropts.rs b/crates/render/src/tests_layeropts.rs index ffac0cd3c..7130d654b 100644 --- a/crates/render/src/tests_layeropts.rs +++ b/crates/render/src/tests_layeropts.rs @@ -166,3 +166,23 @@ fn guides_draw_on_screen_only() { assert!(drawn(false) > 0, "on screen the guide shows"); assert_eq!(drawn(true), 0, "exports leave it out"); } + +/// #3219: View > Hide Guides hides paths made into guides without using the export/template +/// filtering switch; Show Guides (the default) draws them again. +#[test] +fn hide_guides_hides_guide_paths_on_screen() { + let mut d = Document::new(100.0, 100.0); + let l = d.layers[0].id; + let mut g = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(70.0, 70.0, 90.0, 90.0)), Appearance::default()); + if let NodeKind::Path { guide, .. } = &mut g.kind { + *guide = true; + } + d.insert(Some(l), 0, g).unwrap(); + let pixels = |hide_guides: bool| { + let opts = RenderOptions { background: Some([255; 4]), hide_guides, ..Default::default() }; + let r = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts); + (60..100).flat_map(|y| (60..100).map(move |x| (x, y))).filter(|&(x, y)| r.pixel(x, y) != [255; 4]).count() + }; + assert!(pixels(false) > 0, "Show Guides draws the guide path"); + assert_eq!(pixels(true), 0, "Hide Guides leaves the guide path out"); +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index b87ff3f60..d800b2713 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -330,6 +330,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { outline: app.ui.view.outline, trim: app.ui.view.trim_view, templates: app.ui.view.templates, + guides: app.ui.view.guides, ppp, hidden: vec![], rot: v.rotation, @@ -383,6 +384,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { overprint_preview: vectorcraft_render::proof::overprint_preview_on(), trim: app.ui.view.trim_view, hide_templates: !app.ui.view.templates, + hide_guides: !app.ui.view.guides, tile_edge: vectorcraft_color::Color::from_hex(&app.session.prefs.pattern_tile_edge_color).map_or(opts.tile_edge, |c| { let [r, g, b, _] = c.to_rgba8(1.0); [r, g, b] @@ -1951,7 +1953,7 @@ fn hit_with( path_only: prefs.object_selection_by_path_only, type_path_only: prefs.type_selection_by_path_only, scope: app.session.active().and_then(|st| st.isolation), - ..Default::default() + skip_guides: !app.ui.view.guides, }; test(&app.session.active()?.doc, p, opt) } @@ -5630,6 +5632,25 @@ mod tests { /// 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 + /// themselves through Smart Guides object highlighting. + #[test] + fn hidden_guide_paths_have_no_hover_highlight() { + 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.none", json!({})).unwrap(); + app.select_tool("selection"); + app.ui.view.guides = false; + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let color = c32(app.session.active().unwrap().doc.layer_color(vectorcraft_doc::NodeId(id))); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(xf.to_screen(Point::new(100.0, 150.0)))]); + assert!(drawn_in(&mut app, &ctx, color, 1.5).is_empty(), "a hidden guide has no hover highlight"); + } + #[test] fn snap_to_grid_and_pixel_preview_hide_the_hover_outline() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 7d63c21a8..59efd1bcd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -354,8 +354,10 @@ pub struct CacheKey { pub h: u32, pub outline: bool, pub trim: bool, - /// View › Hide Template: template layers left out ([`vectorcraft_render::RenderOptions::skip_templates`]). + /// View › Hide Template: template layers left out ([`vectorcraft_render::RenderOptions::hide_templates`]). pub templates: bool, + /// View › Show/Hide Guides: whether guide paths are visible in the artwork raster. + pub guides: bool, pub ppp: f32, pub hidden: Vec, pub rot: f64, From d2625ec10845df50ba85e0163840d02e60085084 Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Tue, 20 Oct 2026 22:42:57 +0200 Subject: [PATCH 2/2] 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