From 699e6e9af482f0913bb93a0e1c80b349eb8b6f0d Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Mon, 19 Oct 2026 08:40:19 +0300 Subject: [PATCH] G1: Grid tool dialogs add reference point locator (#1377) Rectangular and Polar Grid click dialogs show the nine-point locator and resize width/height about it using the same artboard_rect logic as Artboard Options. Co-authored-by: Cursor --- crates/ui-egui/src/dialogs/shapes.rs | 47 +++++++++++++++++-- .../src/dialogs/tests_rectangular_grid.rs | 14 ++++++ crates/ui-egui/src/dialogs/tools.rs | 8 +++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dialogs/shapes.rs b/crates/ui-egui/src/dialogs/shapes.rs index 9df9280cc..ddc09a366 100644 --- a/crates/ui-egui/src/dialogs/shapes.rs +++ b/crates/ui-egui/src/dialogs/shapes.rs @@ -2,8 +2,11 @@ //! Star, Line Segment Tool Options and the two grids (size, divider counts and skews, switches). use serde_json::{Value, json}; +use vectorcraft_geom::Rect; use super::{DialogSpec, form, run_and_close}; +use crate::panels::transform::artboard_rect; +use crate::widgets; use crate::VectorcraftApp; use crate::state::Dialog; @@ -27,9 +30,46 @@ fn grid_keys(kind: &str) -> ([&'static str; 2], [&'static str; 2]) { if kind == "polarGrid" { (["concentric", "radial"], ["concentricSkew", "radialSkew"]) } else { (["rows", "columns"], ["rowSkew", "columnSkew"]) } } +/// The 9-point locator index for a grid dialog (`reference`, default top-left). +fn grid_reference(d: &Dialog) -> usize { + d.fields.get("reference").and_then(|v| v.as_u64()).map(|n| (n as usize).min(8)).unwrap_or(0) +} + +/// A grid dialog's bounding box from its top-left `x`/`y` and size fields. +fn grid_placed(d: &Dialog) -> Rect { + let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0)); + Rect::new(x, y, x + d.f64("width", 100.0), y + d.f64("height", 100.0)) +} + +fn set_grid_placed(d: &mut Dialog, r: Rect) { + d.fields.insert("x".into(), json!(r.x0)); + d.fields.insert("y".into(), json!(r.y0)); + d.fields.insert("width".into(), json!(r.width())); + d.fields.insert("height".into(), json!(r.height())); +} + /// The form's fields, then a grid's switches. fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - form::grid(ui, d, app.session.general_unit()); + let unit = app.session.general_unit(); + if matches!(d.kind.as_str(), "rectangularGrid" | "polarGrid") { + ui.horizontal(|ui| { + let refi = grid_reference(d); + if let Some(i) = widgets::reference_point(ui, refi) { + d.fields.insert("reference".into(), json!(i)); + } + }); + ui.add_space(4.0); + let refi = grid_reference(d); + let (was, w0, h0) = (grid_placed(d), d.f64("width", 100.0), d.f64("height", 100.0)); + form::grid_then(ui, d, unit, |_, d| { + let (w, h) = (d.f64("width", 100.0), d.f64("height", 100.0)); + if (w - w0).abs() > 1e-9 || (h - h0).abs() > 1e-9 { + set_grid_placed(d, artboard_rect(was, refi, None, None, Some(w), Some(h), false)); + } + }); + } else { + form::grid(ui, d, unit); + } let own = match d.kind.as_str() { "rectangularGrid" => ("frame", tl!("Use Outside Rectangle As Frame")), "polarGrid" => ("compound", tl!("Create Compound Path From Ellipses")), @@ -54,8 +94,9 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { kind @ ("rectangularGrid" | "polarGrid") => { // The switches as they are; the typed fields as numbers. let (mut p, (counts, skews)) = (form::params(d), grid_keys(kind)); - for (k, default) in [("width", 100.0), ("height", 100.0)] { - p[k] = json!(d.f64(k, default)); + let r = grid_placed(d); + for (k, v) in [("x", r.x0), ("y", r.y0), ("width", r.width()), ("height", r.height())] { + p[k] = json!(v); } for (count, skew) in counts.into_iter().zip(skews) { p[count] = json!((d.f64(count, 5.0) as u64).min(999)); diff --git a/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs b/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs index 8d9f8c56b..88a0c5ab8 100644 --- a/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs +++ b/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs @@ -159,6 +159,20 @@ fn key_frame(app: &mut VectorcraftApp, key: egui::Key) { out.textures_delta.clear(); } +/// #1377: OK uses the bounding box after a reference-point resize (`artboard_rect` keeps the +/// center pinned when width doubles). +#[test] +fn rectangular_grid_reference_point_pins_center_when_width_changes() { + let mut app = app(); + click(&mut app, Point::new(100.0, 100.0)); + assert_eq!(field(&app, "reference"), json!(0)); + for (key, value) in [("reference", json!(4)), ("x", json!(50.0)), ("y", json!(50.0)), ("width", json!(200)), ("height", json!(100))] { + set(&mut app, key, value); + } + let id = confirm(&mut app); + assert_grid(&app, id, Rect::new(50.0, 50.0, 250.0, 150.0), 5, 5); +} + #[test] fn rectangular_grid_canvas_click_opens_registered_dialog_with_saved_counts_and_point_defaults() { let mut app = app(); diff --git a/crates/ui-egui/src/dialogs/tools.rs b/crates/ui-egui/src/dialogs/tools.rs index 034295ff3..999dd9c2e 100644 --- a/crates/ui-egui/src/dialogs/tools.rs +++ b/crates/ui-egui/src/dialogs/tools.rs @@ -19,7 +19,13 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) { // The grids: the tool's stored options (divider counts and skews, switches) at the default size. "rectangularGrid" | "polarGrid" => { let mut fields = app.session.tool_options_of(kind); - for (key, value) in [("x", json!(x)), ("y", json!(y)), ("width", json!("100 pt")), ("height", json!("100 pt"))] { + for (key, value) in [ + ("x", json!(x)), + ("y", json!(y)), + ("width", json!("100 pt")), + ("height", json!("100 pt")), + ("reference", json!(0)), + ] { fields[key] = value; } Dialog::new(kind, fields)