From 6939eff2f97b0ee513c4feff932faf267e0769a0 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 07:24:43 +0700 Subject: [PATCH 1/2] Fix #1534: paths made into guides take the guide colour and style of Preferences --- crates/render/src/lib.rs | 16 +++++++++++++++- crates/render/src/tests_tileedge.rs | 28 ++++++++++++++++++++++++++++ crates/ui-egui/src/canvas.rs | 4 ++++ crates/ui-egui/src/lib.rs | 2 ++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 7d6d9489..cf09ef8a 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -96,6 +96,9 @@ pub struct RenderOptions { /// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge /// Color), RGB. pub tile_edge: [u8; 3], + /// Colour and style of the paths made into guides (Preferences › Guides & Grid, as the ruler + /// guides the UI draws): RGB, and whether they are dotted. + pub guide: ([u8; 3], bool), /// View Opacity Mask (Alt-click the mask thumbnail): instead of the artwork, show this object's /// opacity mask alone as its coverage in greyscale (white = opaque, black = transparent). pub mask_view: Option, @@ -201,6 +204,7 @@ impl Default for RenderOptions { trim: false, skip_templates: false, tile_edge: vectorcraft_doc::LAYER_COLORS[0].1, + guide: ([0x4a, 0xd8, 0xff], false), mask_view: None, highlight_substitutions: false, anti_alias: AntiAlias::Art, @@ -1158,7 +1162,17 @@ impl Renderer { NodeKind::Path { path, rule, guide, .. } => { let bp = path.to_bezpath(); if *guide { - self.hairline(ctx, f, &bp, [0x4a, 0xd8, 0xff, 255]); + let ([r, g, b], dots) = f.opts.guide; + if dots { + let mut screen = bp.clone(); + screen.apply_affine(f.view); + ctx.set_transform(Affine::IDENTITY); + ctx.set_stroke(kurbo::Stroke::new(1.5).with_dashes(0.0, [1.5, 2.5])); + ctx.set_paint(f.ink.fixed([r, g, b, 255])); + ctx.stroke_path(&screen); + } else { + self.hairline(ctx, f, &bp, [r, g, b, 255]); + } } else { self.draw_shape(ctx, f, n, &bp, *rule); } diff --git a/crates/render/src/tests_tileedge.rs b/crates/render/src/tests_tileedge.rs index 3657f0b3..2c487ff8 100644 --- a/crates/render/src/tests_tileedge.rs +++ b/crates/render/src/tests_tileedge.rs @@ -67,3 +67,31 @@ fn swatch_bounds_outline_the_period_dashed() { let off = render(&editing(tt, false, false), Some([0, 200, 0])); assert!((51..70).all(|x| is_white(off.pixel(x, 90)))); } + +/// #1534: a path made into a guide is drawn in the guide colour and style (Preferences › Guides & +/// Grid), as ruler guides are: a line, or dots with gaps between them. +#[test] +fn guide_paths_take_the_guide_color_and_style() { + let mut d = Document::new(200.0, 200.0); + let (layer, id) = (d.layers[0].id, d.alloc_id()); + let mut n = Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 60.0, 60.0)), Appearance::basic(Paint::None, Paint::None, 0.0)); + if let NodeKind::Path { guide, .. } = &mut n.kind { + *guide = true; + } + d.insert(Some(layer), 0, n).unwrap(); + let draw = |guide| { + let opts = RenderOptions { background: Some([255; 4]), guide, ..Default::default() }; + Renderer::new().render(&d, 120, 120, Affine::translate((50.5, 50.5)), &opts) + }; + // Along the top edge of the square (y = 50), between its corners: the pixels the red covers. + let inked = |r: &Rendered| (55..105).filter(|x| r.pixel(*x, 50)[1] < 64).count(); + let line = draw(([200, 0, 0], false)); + let p = line.pixel(80, 50); + assert!(p[0] > 150 && p[1] < 100 && p[2] < 100, "{p:?}"); + assert_eq!(inked(&line), 50); + let dotted = draw(([200, 0, 0], true)); + let dots = inked(&dotted); + assert!((10..40).contains(&dots), "{dots}: {:?}", (45..70).map(|x| dotted.pixel(x, 50)[1]).collect::>()); + let default = draw(RenderOptions::default().guide); + assert!((55..105).all(|x| !is_white(default.pixel(x, 50))), "a line by default"); +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 373e6939..8d3ac45f 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -234,7 +234,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview: off, images show // their pixels as they rasterize, hard-edged. let smooth_images = pixel.is_none() || app.session.prefs.anti_aliased_bitmaps; + // Paths made into guides are drawn as the ruler guides are (Preferences › Guides & Grid). + let guide_look = LineLook::guides(&app.session.prefs, &t); let key = CacheKey { + guide: ([guide_look.color.r(), guide_look.color.g(), guide_look.color.b()], guide_look.dots), doc: st.uid as usize, revision: st.revision, interacting: st.interaction.is_some(), @@ -297,6 +300,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let [r, g, b, _] = c.to_rgba8(1.0); [r, g, b] }), + guide: key.guide, mask_view, highlight_substitutions: true, // General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1648279f..1f165d59 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -321,6 +321,8 @@ pub struct CacheKey { /// Appearance of Black › On Screen › Display All Blacks as Rich Black /// ([`vectorcraft_render::RenderOptions::rich_black`]). pub rich_black: bool, + /// Colour and style of guide paths ([`vectorcraft_render::RenderOptions::guide`]). + pub guide: ([u8; 3], bool), } #[derive(Clone, Copy, Debug, Default)] From 50911d860864b38c0565ad0f55437bfc2792f1af Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 07:52:00 +0700 Subject: [PATCH 2/2] Fix #1534: a dragged selection snaps to ruler guides with Smart Guides on too --- crates/tools/src/guides.rs | 6 ++++++ crates/tools/src/select.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/crates/tools/src/guides.rs b/crates/tools/src/guides.rs index 64bd8992..3e7aa338 100644 --- a/crates/tools/src/guides.rs +++ b/crates/tools/src/guides.rs @@ -384,6 +384,12 @@ impl Targets { self } + /// The ruler guides alone ([`Self::with_rulers`]): what a dragged selection's grabbed point + /// still snaps to with View → Snap to Point when Smart Guides line the selection up. + pub(crate) fn rulers(cx: &ToolContext) -> Self { + Self::default().styled(cx).with_rulers(cx) + } + /// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart /// Guides off: the anchors of the art but `exclude` and the ruler guides. pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option { diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 33e1099e..e828364e 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -90,6 +90,9 @@ pub(crate) struct MoveSnap { /// original. copy_targets: Option, copy_points: Option, + /// With Smart Guides on, the ruler guides View › Snap to Point still pulls the grabbed point + /// onto (#1534); `points` holds them with Smart Guides off. + rulers: Option, } impl MoveSnap { @@ -102,6 +105,7 @@ impl MoveSnap { points: points(&cx.selection.objects), copy_targets: guides.then(|| Targets::for_move(cx, &[])), copy_points: points(&[]), + rulers: (guides && cx.snap_to_point).then(|| Targets::rulers(cx)), } } @@ -133,6 +137,14 @@ impl MoveSnap { guides = ov; } } + // A ruler guide within reach of the grabbed point wins over lining the bounds up. + if let Some(t) = &self.rulers { + let (q, ov) = t.snap_point(start + d, cx.tol(cx.snap_tolerance)); + if q != start + d { + d = q - start; + guides = ov; + } + } if cx.snap_to_pixel && let Some(b) = self.bounds { @@ -733,6 +745,29 @@ mod tests { assert_eq!(a, vec![Action::Commit]); } + /// #1534: with Smart Guides on (the default), the point a selection is dragged by still snaps + /// to a ruler guide within the Snap to Point distance; not with Snap to Point off. + #[test] + fn a_move_snaps_the_grabbed_point_to_a_ruler_guide_with_smart_guides_on() { + let (mut d, id) = doc_with_rect(); + d.guides = vec![vectorcraft_doc::Guide::new(true, 173.4)]; + let mut s = Selection::default(); + s.add(id); + let p = paint(); + let dx = |c: &ToolContext, to: f64| { + let mut t = SelectionTool::default(); + t.pointer(c, &ev(PointerKind::Down, 150.0, 150.0)); + let a = t.pointer(c, &ev(PointerKind::Drag, to, 150.0)); + let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") }; + v["matrix"][4].as_f64().unwrap() + }; + let c = cx(&d, &s, &p); + assert!(c.smart_guides && c.snap_to_point); + assert!((dx(&c, 172.0) - 23.4).abs() < 1e-9, "onto the guide"); + assert_eq!(dx(&c, 165.0), 15.0, "out of reach"); + assert_eq!(dx(&ToolContext { snap_to_point: false, ..cx(&d, &s, &p) }, 172.0), 22.0); + } + /// Alt pressed during a move makes a copy, which snaps to the original it leaves behind (as /// gathered before the previews moved it); released, the move doesn't. #[test]