From e43a5c6e6325699ce0f78db0f92564540e2c9489 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 21 Oct 2026 01:34:45 +0200 Subject: [PATCH 1/6] Canvas: an empty graphic frame's X joins its corners A four-cornered empty graphic frame draws its X from corner to corner, so a frame slanted with Direct Selection keeps the X on its corners, as InDesign draws it. Other shapes still span their bounds. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 52 +++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0128924c0..63713bc60 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-21** · An empty graphic frame's X joins its corners, so a frame slanted with Direct Selection keeps its X corner to corner, as InDesign draws it; it used to stay across the old rectangle. Found in Zero2Graphic's *How to Page Layout design in Adobe InDesign 2022 CC*. 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/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8d94f099f..64e544d78 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1149,14 +1149,27 @@ fn draw_item_edges(painter: &egui::Painter, xf: &Xf, doc: &Document, it: &Item, } // Empty graphic frame: the X. if it.object_style == designcraft_doc::BASIC_GRAPHICS_FRAME && matches!(it.content, Content::Unassigned) { - let r = it.inner_bounds(); let m = a * it.xf; - let p = |x: f64, y: f64| xf.to_screen(m * Point::new(x, y)); - painter.line_segment([p(r.x0, r.y0), p(r.x1, r.y1)], Stroke::new(hair(painter), col.gamma_multiply(0.75))); - painter.line_segment([p(r.x1, r.y0), p(r.x0, r.y1)], Stroke::new(hair(painter), col.gamma_multiply(0.75))); + for (p0, p1) in frame_x(it) { + painter.line_segment([xf.to_screen(m * p0), xf.to_screen(m * p1)], Stroke::new(hair(painter), col.gamma_multiply(0.75))); + } } } +/// The X of an empty graphic frame, in its own space: a four-cornered frame joins its opposite +/// corners, so a frame slanted with Direct Selection keeps its X corner to corner, as InDesign +/// draws it; other shapes span their bounds. +fn frame_x(it: &Item) -> [(Point, Point); 2] { + if let [sp] = it.path.subpaths.as_slice() + && sp.closed + && let [c0, c1, c2, c3] = sp.anchors.as_slice() + { + return [(c0.p, c2.p), (c1.p, c3.p)]; + } + let r = it.inner_bounds(); + [(Point::new(r.x0, r.y0), Point::new(r.x1, r.y1)), (Point::new(r.x1, r.y0), Point::new(r.x0, r.y1))] +} + /// Layer colour of the first selected item: live-corner marks are outlined in it. fn selection_color(app: &DesignApp) -> Color32 { let fallback = Color32::from_rgb(79, 153, 255); @@ -3081,6 +3094,37 @@ mod tests { } } + /// An empty graphic frame's X joins its corners: a rectangle's, and a frame slanted by + /// moving its top points, as in the Zero2Graphic tutorial's row of parallelograms. + #[test] + fn an_empty_frames_x_follows_its_corners() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [100, 100, 200, 160]})).unwrap()["id"].as_u64().unwrap(); + let x = |app: &DesignApp| { + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone(); + let m = it.xf; + frame_x(&it).map(|(a, b)| { + let (a, b) = (m * a, m * b); + [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] + }) + }; + let mut lines = x(&app).to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(100.0, 100.0), (200.0, 160.0)], [(100.0, 160.0), (200.0, 100.0)]], "corner to corner"); + // Slant it: the two top points move 30 pt right. + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone(); + let top: Vec = + it.path.subpaths[0].anchors.iter().enumerate().filter(|(_, a)| (it.xf * a.p).y < 130.0).map(|(k, _)| json!([0, k])).collect(); + assert_eq!(top.len(), 2); + app.run("path.moveAnchors", json!({"id": id, "anchors": top, "dx": 30, "dy": 0})).unwrap(); + let mut lines = x(&app).to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(100.0, 160.0), (230.0, 100.0)], [(130.0, 100.0), (200.0, 160.0)]], "the slanted corners"); + } + /// Each preview mode's shown area: Preview stops at the trim, Bleed reaches the bleed box and /// Slug the slug box. A slug narrower than the bleed never pulls the edge back in. #[test] From 5d6f79d3256f308214ac49bcc9a2af398d741ecb Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:55:46 +0200 Subject: [PATCH 2/6] An ellipse or polygon frame keeps the X across its bounds The corner-to-corner X is for rectangles (slanted or not): four corner points without handles, on a Rectangle or Path item. An ellipse frame (four smooth anchors) and a four-sided polygon (a diamond) would otherwise get a plus through their centre; they keep the X across their bounds. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 64e544d78..c98bdba99 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1160,9 +1160,13 @@ fn draw_item_edges(painter: &egui::Painter, xf: &Xf, doc: &Document, it: &Item, /// corners, so a frame slanted with Direct Selection keeps its X corner to corner, as InDesign /// draws it; other shapes span their bounds. fn frame_x(it: &Item) -> [(Point, Point); 2] { - if let [sp] = it.path.subpaths.as_slice() + // A rectangle (slanted or not: four corner points, no handles). Ellipses, polygons (a + // four-sided one is a diamond) and curved paths keep the X across their bounds. + if matches!(it.shape, designcraft_doc::Shape::Rectangle | designcraft_doc::Shape::Path) + && let [sp] = it.path.subpaths.as_slice() && sp.closed && let [c0, c1, c2, c3] = sp.anchors.as_slice() + && sp.anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p) { return [(c0.p, c2.p), (c1.p, c3.p)]; } @@ -3123,6 +3127,26 @@ mod tests { lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); assert_eq!(lines, vec![[(100.0, 160.0), (230.0, 100.0)], [(130.0, 100.0), (200.0, 160.0)]], "the slanted corners"); + // An ellipse frame (four smooth anchors) keeps the X across its bounds, not a plus. + let e = app.run("frame.create", json!({"rect": [300, 100, 400, 160], "shape": "ellipse"})).unwrap()["id"].as_u64().unwrap(); + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(e)).unwrap().clone(); + let mut lines: Vec<[(f64, f64); 2]> = frame_x(&it) + .map(|(a, b)| { + let (a, b) = (it.xf * a, it.xf * b); + [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] + }) + .to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(300.0, 100.0), (400.0, 160.0)], [(300.0, 160.0), (400.0, 100.0)]], "the ellipse's bounds"); + // So does a four-sided polygon frame (a diamond): its corners would cross as a plus. + let mut diamond = it.clone(); + diamond.shape = designcraft_doc::Shape::Polygon; + for a in &mut diamond.path.subpaths[0].anchors { + (a.h_in, a.h_out) = (a.p, a.p); + } + let lines: Vec<_> = frame_x(&diamond).to_vec(); + assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "diagonals, not a plus: {lines:?}"); } /// Each preview mode's shown area: Preview stops at the trim, Bleed reaches the bleed box and From 57ec8cad22f82694ab8e65bdc234f169ec9dcca6 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sun, 25 Oct 2026 17:19:30 +0100 Subject: [PATCH 3/6] Canvas: an edited diamond keeps its frame X across its bounds Moving a point makes a polygon a path (as the engine does for any point edit), and four handle-free corners took the rectangle's branch: a diamond's corners crossed as a plus. A path's corners are used only when both diagonals cross as an X; a test edits a four-sided polygon with path.moveAnchors. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 1a50aaa74..5636169ea 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1216,12 +1216,19 @@ fn draw_item_edges(painter: &egui::Painter, xf: &Xf, doc: &Document, it: &Item, /// draws it; other shapes span their bounds. fn frame_x(it: &Item) -> [(Point, Point); 2] { // A rectangle (slanted or not: four corner points, no handles). Ellipses, polygons (a - // four-sided one is a diamond) and curved paths keep the X across their bounds. + // four-sided one is a diamond) and curved paths keep the X across their bounds. Edited points + // make any of them a path: a four-cornered one whose corners would cross as a plus (a diamond, + // edited) keeps the bounds too. + let crosses = |a: Point, b: Point| { + let d = b - a; + d.x.abs() > 0.18 * d.hypot() && d.y.abs() > 0.18 * d.hypot() + }; if matches!(it.shape, designcraft_doc::Shape::Rectangle | designcraft_doc::Shape::Path) && let [sp] = it.path.subpaths.as_slice() && sp.closed && let [c0, c1, c2, c3] = sp.anchors.as_slice() && sp.anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p) + && (it.shape == designcraft_doc::Shape::Rectangle || (crosses(c0.p, c2.p) && crosses(c1.p, c3.p))) { return [(c0.p, c2.p), (c1.p, c3.p)]; } @@ -3745,6 +3752,13 @@ mod tests { } let lines: Vec<_> = frame_x(&diamond).to_vec(); assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "diagonals, not a plus: {lines:?}"); + // Still after one of its points moves (which makes it a path). + let p = app.run("frame.create", json!({"rect": [300, 200, 400, 260], "shape": "polygon", "sides": 4, "starInset": 0})).unwrap()["id"].as_u64().unwrap(); + app.run("path.moveAnchors", json!({"id": p, "anchors": [[0, 0]], "dx": 5, "dy": 0})).unwrap(); + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(p)).unwrap().clone(); + assert_eq!(it.shape, designcraft_doc::Shape::Path); + let lines: Vec<_> = frame_x(&it).to_vec(); + assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "an edited diamond: {lines:?}"); } /// Each preview mode's shown area: Preview stops at the trim, Bleed reaches the bleed box and From ec682809be487ffb59f6a7fa53fbcbb5a325304c Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sun, 25 Oct 2026 17:47:19 +0100 Subject: [PATCH 4/6] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 4d277c9a9..ee8b933b2 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3837,7 +3837,9 @@ mod tests { let lines: Vec<_> = frame_x(&diamond).to_vec(); assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "diagonals, not a plus: {lines:?}"); // Still after one of its points moves (which makes it a path). - let p = app.run("frame.create", json!({"rect": [300, 200, 400, 260], "shape": "polygon", "sides": 4, "starInset": 0})).unwrap()["id"].as_u64().unwrap(); + let p = app.run("frame.create", json!({"rect": [300, 200, 400, 260], "shape": "polygon", "sides": 4, "starInset": 0})).unwrap()["id"] + .as_u64() + .unwrap(); app.run("path.moveAnchors", json!({"id": p, "anchors": [[0, 0]], "dx": 5, "dy": 0})).unwrap(); let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(p)).unwrap().clone(); assert_eq!(it.shape, designcraft_doc::Shape::Path); From dccda2cd4ca67629cad35dd2c71debf715d0641a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 13:31:57 +0100 Subject: [PATCH 5/6] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas/tests.rs | 114 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 836fd37cd..b96133c76 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -86,65 +86,65 @@ fn bleed_and_slug_are_preview_modes_that_show_their_area() { } } - /// An empty graphic frame's X joins its corners: a rectangle's, and a frame slanted by - /// moving its top points, as in the Zero2Graphic tutorial's row of parallelograms. - #[test] - fn an_empty_frames_x_follows_its_corners() { - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - let id = app.run("frame.create", json!({"rect": [100, 100, 200, 160]})).unwrap()["id"].as_u64().unwrap(); - let x = |app: &DesignApp| { - let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone(); - let m = it.xf; - frame_x(&it).map(|(a, b)| { - let (a, b) = (m * a, m * b); - [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] - }) - }; - let mut lines = x(&app).to_vec(); - lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); - lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); - assert_eq!(lines, vec![[(100.0, 100.0), (200.0, 160.0)], [(100.0, 160.0), (200.0, 100.0)]], "corner to corner"); - // Slant it: the two top points move 30 pt right. +/// An empty graphic frame's X joins its corners: a rectangle's, and a frame slanted by +/// moving its top points, as in the Zero2Graphic tutorial's row of parallelograms. +#[test] +fn an_empty_frames_x_follows_its_corners() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [100, 100, 200, 160]})).unwrap()["id"].as_u64().unwrap(); + let x = |app: &DesignApp| { let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone(); - let top: Vec = - it.path.subpaths[0].anchors.iter().enumerate().filter(|(_, a)| (it.xf * a.p).y < 130.0).map(|(k, _)| json!([0, k])).collect(); - assert_eq!(top.len(), 2); - app.run("path.moveAnchors", json!({"id": id, "anchors": top, "dx": 30, "dy": 0})).unwrap(); - let mut lines = x(&app).to_vec(); - lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); - lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); - assert_eq!(lines, vec![[(100.0, 160.0), (230.0, 100.0)], [(130.0, 100.0), (200.0, 160.0)]], "the slanted corners"); - // An ellipse frame (four smooth anchors) keeps the X across its bounds, not a plus. - let e = app.run("frame.create", json!({"rect": [300, 100, 400, 160], "shape": "ellipse"})).unwrap()["id"].as_u64().unwrap(); - let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(e)).unwrap().clone(); - let mut lines: Vec<[(f64, f64); 2]> = frame_x(&it) - .map(|(a, b)| { - let (a, b) = (it.xf * a, it.xf * b); - [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] - }) - .to_vec(); - lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); - lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); - assert_eq!(lines, vec![[(300.0, 100.0), (400.0, 160.0)], [(300.0, 160.0), (400.0, 100.0)]], "the ellipse's bounds"); - // So does a four-sided polygon frame (a diamond): its corners would cross as a plus. - let mut diamond = it.clone(); - diamond.shape = designcraft_doc::Shape::Polygon; - for a in &mut diamond.path.subpaths[0].anchors { - (a.h_in, a.h_out) = (a.p, a.p); - } - let lines: Vec<_> = frame_x(&diamond).to_vec(); - assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "diagonals, not a plus: {lines:?}"); - // Still after one of its points moves (which makes it a path). - let p = app.run("frame.create", json!({"rect": [300, 200, 400, 260], "shape": "polygon", "sides": 4, "starInset": 0})).unwrap()["id"] - .as_u64() - .unwrap(); - app.run("path.moveAnchors", json!({"id": p, "anchors": [[0, 0]], "dx": 5, "dy": 0})).unwrap(); - let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(p)).unwrap().clone(); - assert_eq!(it.shape, designcraft_doc::Shape::Path); - let lines: Vec<_> = frame_x(&it).to_vec(); - assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "an edited diamond: {lines:?}"); + let m = it.xf; + frame_x(&it).map(|(a, b)| { + let (a, b) = (m * a, m * b); + [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] + }) + }; + let mut lines = x(&app).to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(100.0, 100.0), (200.0, 160.0)], [(100.0, 160.0), (200.0, 100.0)]], "corner to corner"); + // Slant it: the two top points move 30 pt right. + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().clone(); + let top: Vec = + it.path.subpaths[0].anchors.iter().enumerate().filter(|(_, a)| (it.xf * a.p).y < 130.0).map(|(k, _)| json!([0, k])).collect(); + assert_eq!(top.len(), 2); + app.run("path.moveAnchors", json!({"id": id, "anchors": top, "dx": 30, "dy": 0})).unwrap(); + let mut lines = x(&app).to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(100.0, 160.0), (230.0, 100.0)], [(130.0, 100.0), (200.0, 160.0)]], "the slanted corners"); + // An ellipse frame (four smooth anchors) keeps the X across its bounds, not a plus. + let e = app.run("frame.create", json!({"rect": [300, 100, 400, 160], "shape": "ellipse"})).unwrap()["id"].as_u64().unwrap(); + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(e)).unwrap().clone(); + let mut lines: Vec<[(f64, f64); 2]> = frame_x(&it) + .map(|(a, b)| { + let (a, b) = (it.xf * a, it.xf * b); + [(a.x.round(), a.y.round()), (b.x.round(), b.y.round())] + }) + .to_vec(); + lines.iter_mut().for_each(|l| l.sort_by(|a, b| a.partial_cmp(b).unwrap())); + lines.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert_eq!(lines, vec![[(300.0, 100.0), (400.0, 160.0)], [(300.0, 160.0), (400.0, 100.0)]], "the ellipse's bounds"); + // So does a four-sided polygon frame (a diamond): its corners would cross as a plus. + let mut diamond = it.clone(); + diamond.shape = designcraft_doc::Shape::Polygon; + for a in &mut diamond.path.subpaths[0].anchors { + (a.h_in, a.h_out) = (a.p, a.p); } + let lines: Vec<_> = frame_x(&diamond).to_vec(); + assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "diagonals, not a plus: {lines:?}"); + // Still after one of its points moves (which makes it a path). + let p = app.run("frame.create", json!({"rect": [300, 200, 400, 260], "shape": "polygon", "sides": 4, "starInset": 0})).unwrap()["id"] + .as_u64() + .unwrap(); + app.run("path.moveAnchors", json!({"id": p, "anchors": [[0, 0]], "dx": 5, "dy": 0})).unwrap(); + let it = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(p)).unwrap().clone(); + assert_eq!(it.shape, designcraft_doc::Shape::Path); + let lines: Vec<_> = frame_x(&it).to_vec(); + assert!(lines.iter().all(|(a, b)| (a.x - b.x).abs() > 1e-6 && (a.y - b.y).abs() > 1e-6), "an edited diamond: {lines:?}"); +} /// Each preview mode's shown area: Preview stops at the trim, Bleed reaches the bleed box and /// Slug the slug box. A slug narrower than the bleed never pulls the edge back in. From cb036e06e28ecd7634182a815db4bf3fcaed4a4c Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 15:28:38 +0100 Subject: [PATCH 6/6] Frame X test: the corner-to-corner lines from main's split canvas frames module Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/canvas/frames.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index de4331c7d..84b3cd0d6 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -30,7 +30,7 @@ mod view; use column_guides::{COLUMN_DRAG, ColumnDrag, column_guide_at, spread_x_across}; use frames::{draw_frames, draw_selection}; #[cfg(test)] -use frames::{item_canvas_xf, overset_frames, overset_marks, ported_frames, text_frames_in}; +use frames::{frame_x, item_canvas_xf, overset_frames, overset_marks, ported_frames, text_frames_in}; use guides::{GUIDE_DRAG, GUIDE_PRESS, GuideDrag, GuidePress, guide_readout, guide_spot, moving_guides, readout_at}; #[cfg(test)] use guides::{guide_at, guide_drag_delta, guide_measure}; diff --git a/crates/ui-egui/src/canvas/frames.rs b/crates/ui-egui/src/canvas/frames.rs index 1378ba255..4578b933b 100644 --- a/crates/ui-egui/src/canvas/frames.rs +++ b/crates/ui-egui/src/canvas/frames.rs @@ -135,7 +135,7 @@ fn draw_item_edges(painter: &egui::Painter, xf: &Xf, doc: &Document, it: &Item, /// The X of an empty graphic frame, in its own space: a four-cornered frame joins its opposite /// corners, so a frame slanted with Direct Selection keeps its X corner to corner, as InDesign /// draws it; other shapes span their bounds. -fn frame_x(it: &Item) -> [(Point, Point); 2] { +pub(super) fn frame_x(it: &Item) -> [(Point, Point); 2] { // A rectangle (slanted or not: four corner points, no handles). Ellipses, polygons (a // four-sided one is a diamond) and curved paths keep the X across their bounds. Edited points // make any of them a path: a four-cornered one whose corners would cross as a plus (a diamond,