From 4cde0668bef04fa0572e0bb77df2e053fbfc01bc Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 20 Oct 2026 21:03:34 +0200 Subject: [PATCH] Snapping: a drawn or resized edge snaps to the page's centre line, as InDesign's smart guides do Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/tools/src/snap.rs | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 0128924c0..060b5f169 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-20** · Smart guides: an edge being drawn or resized snaps to the page's centre line, as in InDesign, so a frame drawn from the top margin to the middle of the page stops there instead of at the nearest baseline grid line. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/tools/src/snap.rs b/crates/tools/src/snap.rs index 0cbc9aadd..2d5426cf5 100644 --- a/crates/tools/src/snap.rs +++ b/crates/tools/src/snap.rs @@ -1074,9 +1074,15 @@ fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> } let flags = axis_flags(req, axis); let mut best: Option = None; + // An edge being drawn or resized also meets the page's centre line, as InDesign's smart + // guides do: a frame drawn from the top margin down to the middle of the page stops there. + let edge_to_centre = cx.snap.align_centers && matches!(req.gesture, Gesture::Create | Gesture::Resize); if let Some(sp) = cx.doc.spread(req.spread) { for page in &sp.pages { offer_box(&mut best, req.rect, flags, axis, page.bounds(), tol, cx.snap.align_edges, cx.snap.align_centers); + if edge_to_centre { + offer_edges_to_centre(&mut best, req.rect, flags, axis, page.bounds(), tol); + } } for item in &sp.items { let Some(rect) = item_target_rect(cx.selection, req, item) else { continue }; @@ -1291,6 +1297,18 @@ fn offer_box(best: &mut Option, moving: Rect, flags: [bool; 3], axis: Axis } } +/// The moving rect's enabled edges against the target's centre line. +fn offer_edges_to_centre(best: &mut Option, moving: Rect, flags: [bool; 3], axis: Axis, target: Rect, tol: f64) { + let (m0, _, m1) = axis_triple(moving, axis); + let (_, t_mid, _) = axis_triple(target, axis); + if flags[0] { + offer(best, m0, t_mid, tol, Some(target)); + } + if flags[2] { + offer(best, m1, t_mid, tol, Some(target)); + } +} + fn offer(best: &mut Option, moving: f64, target: f64, tol: f64, span: Option) { if !moving.is_finite() || !target.is_finite() { return; @@ -1985,6 +2003,43 @@ mod tests { assert_eq!(hit.delta.x, 0.0); } + /// A frame drawn on an A4 landscape page from the top margin down to the middle of the page + /// stops at the page's centre line (297.6 pt), not at the hidden baseline grid line 1.4 pt + /// further down (300 pt), as InDesign's smart guides do. + #[test] + fn a_drawn_edge_snaps_to_the_page_centre() { + let a4 = NewDocument { width: 841.89, height: 595.276, ..NewDocument::default() }; + let doc = with_baseline_grid(Document::new(&a4)); + let cache = Cache::new(); + let layout = CanvasLayout::new(&doc, false); + let sel = Selection::default(); + let cx = ctx_on(&doc, &sel, &cache, &layout); + let create = |gesture, y_edges, rect| SnapRequest { + spread: SpreadRef::Doc(0), + gesture, + rect, + x_edges: [false, false, false], + y_edges, + exclude: &[], + copying: false, + lengths: [None, None], + angle: None, + radius: 0.0, + pointer: Point::new(0.0, 0.0), + }; + let centre = 595.276 / 2.0; + let hit = snap(&cx, create(Gesture::Create, [false, false, true], Rect::new(500.0, 42.52, 800.0, 298.6))); + assert!((hit.delta.y - (centre - 298.6)).abs() < 1e-6, "{}", hit.delta.y); + // Drawn upwards, the top edge meets it too; a resized edge does as well. + let hit = snap(&cx, create(Gesture::Create, [true, false, false], Rect::new(500.0, 296.0, 800.0, 552.0))); + assert!((hit.delta.y - (centre - 296.0)).abs() < 1e-6, "{}", hit.delta.y); + let hit = snap(&cx, create(Gesture::Resize, [false, false, true], Rect::new(500.0, 42.52, 800.0, 298.6))); + assert!((hit.delta.y - (centre - 298.6)).abs() < 1e-6, "{}", hit.delta.y); + // A moved frame still lines up by its centre only: its bottom edge goes to the baseline. + let hit = snap(&cx, create(Gesture::Move, [true, false, true], Rect::new(500.0, 42.52, 800.0, 298.6))); + assert!((hit.delta.y - 1.4).abs() < 1e-6, "{}", hit.delta.y); + } + #[test] fn create_does_not_match_spacing() { let mut doc = Document::new(&NewDocument::default());