mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3269 from MiQ71/fix/issue-3219-show-hide-guides
Fix Show and Hide Guides (#3219)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<Node>) -> 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) {
|
||||
@@ -1382,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<Node>) {
|
||||
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) {
|
||||
|
||||
@@ -166,3 +166,71 @@ 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");
|
||||
}
|
||||
|
||||
/// #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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>(
|
||||
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)
|
||||
}
|
||||
@@ -5628,8 +5630,43 @@ 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
|
||||
/// 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());
|
||||
|
||||
@@ -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<u64>,
|
||||
pub rot: f64,
|
||||
|
||||
@@ -1250,9 +1250,18 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
"view.boundingBox" => 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
|
||||
|
||||
Reference in New Issue
Block a user