diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index c91fced2..c7937a11 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -149,7 +149,7 @@ pub fn specs() -> Vec { "Split Into Grid…", ["Object", "Path"], None, - "{rows?: 2, columns?: 2, gutter?: pt (12)} → {ids}", + "{rows?: 2, columns?: 2, gutter?: pt (12; rowGutter?, columnGutter?: one of the two), totalHeight?, totalWidth?: pt (the grid's size; the selected paths' when left out), height?, width?: pt (each row's and column's size, which then sets the total), addGuides?: false (ruler guides along the row and column edges)} one grid of rectangles over the selected paths replaces them, painted as the topmost one → {ids, guides: count}", has_selection, split_into_grid ), @@ -717,40 +717,65 @@ fn add_anchor_points(s: &mut Session, _: &Value) -> Result { // ---------- Split Into Grid ---------- +/// One direction of the grid: the (gutter, total size) of `n` rows or columns. A `size` for each +/// makes the total; otherwise it is `total`, or `extent` (the selection's) when neither is given. +fn grid_axis(p: &Value, n: f64, [size, gutter, total]: [&str; 3], extent: f64) -> (f64, f64) { + let gutter = len_param(p, gutter).or_else(|| len_param(p, "gutter")).unwrap_or(12.0).max(0.0); + let total = match len_param(p, size) { + Some(size) => n * size + (n - 1.0) * gutter, + None => len_param(p, total).unwrap_or(extent), + }; + (gutter, total) +} + fn split_into_grid(s: &mut Session, p: &Value) -> Result { let rows = num_param(p, "rows").unwrap_or(2.0).round(); let cols = num_param(p, "columns").unwrap_or(2.0).round(); if !(1.0..=500.0).contains(&rows) || !(1.0..=500.0).contains(&cols) { return Err(bad("object.path.splitIntoGrid", "rows and columns must be 1..500")); } - let gutter = len_param(p, "gutter").unwrap_or(12.0).max(0.0); + let too_small = || EngineError::Other("Split Into Grid: select paths large enough for the grid".into()); + // The selected paths, back to front: one grid over them all replaces them, where the topmost + // one was and painted as it is. + let d = &s.doc()?.doc; let roots = selected_roots(s)?; + let paths: Vec<&Node> = + roots.iter().filter_map(|id| d.node(*id)).filter(|n| matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })).collect(); + let Some((top, below)) = paths.split_last().map(|(top, below)| (Node::clone(top), below.iter().map(|n| n.id).collect::>())) else { + return Err(too_small()); + }; + let Some(b) = paths.iter().filter_map(|n| n.geometric_bounds()).reduce(|a, b| a.union(b)) else { return Err(too_small()) }; + let (row_gutter, height) = grid_axis(p, rows, ["height", "rowGutter", "totalHeight"], b.height()); + let (col_gutter, width) = grid_axis(p, cols, ["width", "columnGutter", "totalWidth"], b.width()); + let (rows, cols) = (rows as usize, cols as usize); + let cells = po::split_into_grid(vectorcraft_geom::Rect::new(b.x0, b.y0, b.x0 + width, b.y0 + height), rows, cols, (row_gutter, col_gutter)); + if cells.is_empty() { + return Err(too_small()); + } + let mut edges: Vec<(bool, f64)> = Vec::new(); + if bool_or(p, "addGuides", false) { + // Row-major cells: the first row has the column edges, each row's first cell the row edges. + let bounds = cells.iter().filter_map(PathData::bounds); + edges.extend(bounds.clone().take(cols).flat_map(|c| [(true, c.x0), (true, c.x1)])); + edges.extend(bounds.step_by(cols).flat_map(|c| [(false, c.y0), (false, c.y1)])); + // Without a gutter, neighbours share an edge. + edges.dedup_by(|a, b| a.0 == b.0 && (a.1 - b.1).abs() < 1e-9); + } let ids = s.edit("Split Into Grid", |d, sel| { - let mut out = vec![]; - for id in &roots { - let Some(n) = d.node(*id).cloned() else { continue }; - if !matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) { - continue; - } - let Some(b) = n.geometric_bounds() else { continue }; - let cells = po::split_into_grid(b, rows as usize, cols as usize, gutter); - if cells.is_empty() { - continue; - } - let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?; - d.remove(*id)?; - for (k, c) in cells.into_iter().enumerate() { - let node = shape_node(d, c, Some(&n)); - out.push(d.insert(par, idx + k, node)?); - } - } - if out.is_empty() { - return Err(EngineError::Other("Split Into Grid: select paths large enough for the grid".into())); + d.remove_many(&below)?; + let (par, idx, _) = d.position(top.id).ok_or(EngineError::NoNode(top.id))?; + let layer = d.layer_containing(top.id); + d.remove(top.id)?; + let mut out = Vec::with_capacity(cells.len()); + for (k, c) in cells.into_iter().enumerate() { + let node = shape_node(d, c, Some(&top)); + out.push(d.insert(par, idx + k, node)?); } + d.guides.extend(edges.iter().map(|&(vertical, pos)| vectorcraft_doc::Guide { layer, ..vectorcraft_doc::Guide::new(vertical, pos) })); sel.set(out.iter().copied()); Ok(out) })?; - Ok(ids_json(&ids)) + Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>(), "guides": edges.len() })) } // ---------- Clean Up ---------- diff --git a/crates/engine/src/tests_pathops.rs b/crates/engine/src/tests_pathops.rs index c8081cb2..05036dd0 100644 --- a/crates/engine/src/tests_pathops.rs +++ b/crates/engine/src/tests_pathops.rs @@ -634,6 +634,60 @@ fn split_into_grid_cells() { assert!(s.execute("object.path.splitIntoGrid", &json!({"rows": 0})).is_err()); } +/// Split Into Grid's documented options: a gutter for the rows and one for the columns, the size +/// of each row and column or of the whole grid, and guides along the row and column edges. Several +/// selected paths give one grid over them all, painted as the topmost one, where it was. +#[test] +fn split_into_grid_options() { + let grid = |p: Value| { + let mut s = session(); + rect(&mut s, 10.0, 20.0, 110.0, 50.0); + let r = s.execute("object.path.splitIntoGrid", &p).unwrap(); + let cells: Vec = ids(&r).iter().map(|id| bounds(&s, *id)).collect(); + let guides: Vec<(bool, f64)> = s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect(); + assert_eq!(r["guides"], guides.len()); + (cells, guides) + }; + // A gutter each way; the grid keeps the path's size. + let (cells, guides) = grid(json!({"rows": 2, "columns": 3, "rowGutter": 10, "columnGutter": 4})); + assert!(close(cells[0], Rect::new(10.0, 20.0, 44.0, 40.0), 1e-9) && close(cells[5], Rect::new(86.0, 50.0, 120.0, 70.0), 1e-9), "{cells:?}"); + assert!(guides.is_empty()); + // The whole grid's size, from the path's top left corner. + let (cells, _) = grid(json!({"rows": 1, "columns": 2, "gutter": 0, "totalWidth": 60, "totalHeight": "1 in"})); + assert!(close(cells[1], Rect::new(40.0, 20.0, 70.0, 92.0), 1e-9), "{cells:?}"); + // The size of each row and column makes the total, whatever total is given. + let (cells, guides) = grid(json!({"rows": 2, "columns": 2, "gutter": 5, "width": 20, "height": 10, "totalWidth": 500, "addGuides": true})); + assert!(close(cells[3], Rect::new(35.0, 35.0, 55.0, 45.0), 1e-9), "{cells:?}"); + let edges = |vertical, at: [f64; 4]| at.map(|pos| (vertical, pos)); + assert_eq!(guides, [edges(true, [10.0, 30.0, 35.0, 55.0]), edges(false, [20.0, 30.0, 35.0, 45.0])].concat()); + // Without a gutter the rows share their edges: one guide for each. + let (_, guides) = grid(json!({"rows": 2, "columns": 1, "gutter": 0, "addGuides": true})); + assert_eq!(guides, [(true, 10.0), (true, 120.0), (false, 20.0), (false, 45.0), (false, 70.0)]); + + let mut s = session(); + let back = rect(&mut s, 0.0, 0.0, 40.0, 40.0); + let kept = rect(&mut s, 300.0, 300.0, 10.0, 10.0); + let front = rect(&mut s, 60.0, 20.0, 40.0, 40.0); + set_fill(&mut s, front, Color::rgb(0.1, 0.6, 0.3)); + s.execute("select.set", &json!({"ids": [front.0, back.0]})).unwrap(); + let undo = s.doc().unwrap().history.undo.len(); + let r = ids(&s.execute("object.path.splitIntoGrid", &json!({"rows": 1, "columns": 2, "gutter": 20, "addGuides": true})).unwrap()); + assert!(close(bounds(&s, r[0]), Rect::new(0.0, 0.0, 40.0, 60.0), 1e-9) && close(bounds(&s, r[1]), Rect::new(60.0, 0.0, 100.0, 60.0), 1e-9)); + let st = s.doc().unwrap(); + assert!(r.iter().all(|id| node(&s, *id).appearance == node(&s, r[0]).appearance) && st.doc.node(back).is_none() && st.doc.node(front).is_none()); + assert_eq!(st.doc.layers[0].children().unwrap().iter().map(|n| n.id).collect::>(), [kept, r[0], r[1]], "above the path left alone"); + assert_eq!(st.selection.objects, r); + assert!(st.doc.guides.iter().all(|g| g.layer == Some(st.doc.layers[0].id))); + assert_eq!(st.history.undo.len(), undo + 1); + s.execute("edit.undo", &json!({})).unwrap(); + let st = s.doc().unwrap(); + assert!(st.doc.node(back).is_some() && st.doc.node(front).is_some() && st.doc.guides.is_empty()); + // A grid with no room for its gutters is refused and changes nothing. + assert!(s.execute("object.path.splitIntoGrid", &json!({"columns": 4, "gutter": 50})).is_err()); + assert!(s.execute("object.path.splitIntoGrid", &json!({"width": -5})).is_err()); + assert!(s.doc().unwrap().doc.node(back).is_some()); +} + #[test] fn clean_up_removes_junk() { let mut s = session(); diff --git a/crates/pathops/src/edit.rs b/crates/pathops/src/edit.rs index 7e3451dd..5cb30bcd 100644 --- a/crates/pathops/src/edit.rs +++ b/crates/pathops/src/edit.rs @@ -422,22 +422,24 @@ pub fn join(paths: &[PathData], tolerance: f64) -> PathData { PathData::new(closed) } -/// Object → Path → Split Into Grid: `rows × cols` rectangles filling `rect` with `gutter` spacing -/// (row-major, top-left first). -pub fn split_into_grid(rect: Rect, rows: usize, cols: usize, gutter: f64) -> Vec { +/// Object → Path → Split Into Grid: `rows × cols` rectangles filling `rect` with a gutter between +/// the rows and one between the columns (row-major, top-left first). No cells when they would have +/// no size. +pub fn split_into_grid(rect: Rect, rows: usize, cols: usize, (row_gutter, col_gutter): (f64, f64)) -> Vec { if rows == 0 || cols == 0 { return Vec::new(); } - let w = (rect.width() - gutter * (cols as f64 - 1.0)) / cols as f64; - let h = (rect.height() - gutter * (rows as f64 - 1.0)) / rows as f64; - if w <= 0.0 || h <= 0.0 { + let w = (rect.width() - col_gutter * (cols as f64 - 1.0)) / cols as f64; + let h = (rect.height() - row_gutter * (rows as f64 - 1.0)) / rows as f64; + // Written so that a size that is not a number gives no cells either. + if !(w > 0.0 && h > 0.0 && w.is_finite() && h.is_finite()) { return Vec::new(); } let mut out = Vec::with_capacity(rows * cols); for r in 0..rows { for c in 0..cols { - let x = rect.x0 + c as f64 * (w + gutter); - let y = rect.y0 + r as f64 * (h + gutter); + let x = rect.x0 + c as f64 * (w + col_gutter); + let y = rect.y0 + r as f64 * (h + row_gutter); out.push(PathData::single(SubPath::polyline( &[Point::new(x, y), Point::new(x + w, y), Point::new(x + w, y + h), Point::new(x, y + h)], true, diff --git a/crates/pathops/tests/ops.rs b/crates/pathops/tests/ops.rs index 40bdfc2d..d65fb5b8 100644 --- a/crates/pathops/tests/ops.rs +++ b/crates/pathops/tests/ops.rs @@ -412,13 +412,14 @@ fn join_single_path_closes() { #[test] fn grid_split() { - let g = split_into_grid(Rect::new(0.0, 0.0, 100.0, 50.0), 2, 3, 5.0); + let g = split_into_grid(Rect::new(0.0, 0.0, 100.0, 50.0), 2, 3, (7.0, 5.0)); assert_eq!(g.len(), 6); let w = (100.0 - 10.0) / 3.0; - let h = (50.0 - 5.0) / 2.0; + let h = (50.0 - 7.0) / 2.0; assert!(g.iter().all(|c| close(ar(c), w * h, 1e-9))); assert_eq!(g[5].bounds().unwrap().x1, 100.0); - assert!(split_into_grid(Rect::new(0.0, 0.0, 10.0, 10.0), 0, 3, 1.0).is_empty()); + assert!(split_into_grid(Rect::new(0.0, 0.0, 10.0, 10.0), 0, 3, (1.0, 1.0)).is_empty()); + assert!(split_into_grid(Rect::new(0.0, 0.0, 10.0, f64::NAN), 2, 3, (1.0, 1.0)).is_empty()); } #[test] diff --git a/crates/pathops/tests/prop_pathops.rs b/crates/pathops/tests/prop_pathops.rs index 262e85e4..82feae9f 100644 --- a/crates/pathops/tests/prop_pathops.rs +++ b/crates/pathops/tests/prop_pathops.rs @@ -188,7 +188,7 @@ proptest! { let gutter_w = g * (cols - 1) as f64; let gutter_h = g * (rows - 1) as f64; prop_assume!(r.width() > gutter_w + 1.0 && r.height() > gutter_h + 1.0); - let cells = split_into_grid(r, rows, cols, g); + let cells = split_into_grid(r, rows, cols, (g, g)); prop_assert_eq!(cells.len(), rows * cols); let total: f64 = cells.iter().map(|c| area(c, NZ)).sum(); let want = (r.width() - gutter_w) * (r.height() - gutter_h); diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index 3c5f7198..acc70cba 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -23,7 +23,7 @@ pub(super) fn field_w(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, } /// The label cell of a grid row. -fn row_label(ui: &mut egui::Ui, label: &str) { +pub(super) fn row_label(ui: &mut egui::Ui, label: &str) { let t = Tokens::get(ui.ctx()); ui.label(egui::RichText::new(tl!(label)).color(t.text_dim)); } @@ -56,7 +56,6 @@ fn lengths(kind: &str) -> &'static [&'static str] { "lineSegment" => &["length"], "offsetPath" => &["offset"], "simplify" => &["tolerance"], - "splitIntoGrid" => &["gutter"], _ => &[], } } @@ -76,7 +75,6 @@ fn suffix(kind: &str, key: &str) -> Option<&'static str> { fn order(kind: &str) -> &'static [&'static str] { match kind { "offsetPath" => &["offset", "joins", "miterLimit"], - "splitIntoGrid" => &["rows", "columns", "gutter"], "rectangularGrid" => &["width", "height", "rows", "rowSkew", "columns", "columnSkew"], "polarGrid" => &["width", "height", "concentric", "concentricSkew", "radial", "radialSkew"], "artboardOptions" => &["name", "width", "height"], @@ -431,7 +429,7 @@ pub(super) fn humanize(k: &str) -> String { } /// [`humanize`] translated where it is painted: the ":" stays outside the translated text. -fn humanized(k: &str) -> String { +pub(super) fn humanized(k: &str) -> String { let h = humanize(k); match h.strip_suffix(':') { Some(base) => format!("{}:", tl!(base)), diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index cd8b38e8..101a29f2 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -52,7 +52,7 @@ mod new_document; pub mod new_swatch; pub mod office_export; pub mod package; -mod path_ops; +pub(crate) mod path_ops; pub mod pdf_presets; pub mod perspective_grid; pub mod perspective_options; diff --git a/crates/ui-egui/src/dialogs/path_ops.rs b/crates/ui-egui/src/dialogs/path_ops.rs index bc47850d..c2c35af3 100644 --- a/crates/ui-egui/src/dialogs/path_ops.rs +++ b/crates/ui-egui/src/dialogs/path_ops.rs @@ -1,6 +1,7 @@ -//! Path dialogs: Average, Offset Path and Simplify (with Preview), and Split Into Grid. +//! Path dialogs: Average, and Offset Path, Simplify and Split Into Grid with Preview. use serde_json::{Value, json}; +use vectorcraft_doc::Unit; use super::{DialogSpec, form, run_and_close}; use crate::VectorcraftApp; @@ -25,18 +26,79 @@ fn command(d: &Dialog) -> (&'static str, Value) { ("object.path.offsetPath", json!({"offset": d.f64("offset", 10.0), "joins": d.str("joins"), "miterLimit": d.f64("miterLimit", 4.0)})) } "simplify" => ("object.path.simplify", json!({"tolerance": d.f64("tolerance", 1.0)})), - _ => ("object.path.splitIntoGrid", json!({"rows": d.f64("rows", 2.0), "columns": d.f64("columns", 2.0), "gutter": d.f64("gutter", 12.0)})), + _ => ("object.path.splitIntoGrid", form::params(d)), } } +/// The undo label of the dialogs that preview on the canvas (all but Average). +fn previewed(kind: &str) -> Option<&'static str> { + match kind { + "offsetPath" => Some("Offset Path"), + "simplify" => Some("Simplify"), + "splitIntoGrid" => Some("Split Into Grid"), + _ => None, + } +} + +/// The fields Split Into Grid opens with for a selection `width` × `height` points. +pub(crate) fn split_fields(width: f64, height: f64) -> Value { + json!({"rows": 2, "columns": 2, "rowGutter": 12.0, "columnGutter": 12.0, "totalHeight": height, "totalWidth": width, "addGuides": false}) +} + +/// Split Into Grid's fields: the rows on the left and the columns on the right, each with its +/// number, size, gutter and total, and Add Guides. The size of a row or column is what the total +/// leaves each of them after the gutters, so it follows the other three; typing it sets the total. +fn split_grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { + const WIDTH: f32 = 84.0; + let axes = [ + ("rows", tl!("Rows:"), tl!("Height:"), "rowGutter", "totalHeight"), + ("columns", tl!("Columns:"), tl!("Width:"), "columnGutter", "totalWidth"), + ]; + let gutter_label = form::humanized("gutter"); + egui::Grid::new("dlg").num_columns(4).spacing([10.0, 8.0]).show(ui, |ui| { + for (number, label, ..) in axes { + form::row_label(ui, label); + if let Some(v) = crate::widgets::plain_field(ui, ("dlg-num", number), d.f64(number, 2.0), "", 0, WIDTH) { + d.fields.insert(number.into(), json!(v.round().clamp(1.0, 500.0))); + } + } + ui.end_row(); + for (number, _, label, gutter, total) in axes { + form::row_label(ui, label); + let n = d.f64(number, 2.0).max(1.0); + let gaps = d.f64(gutter, 0.0) * (n - 1.0); + if let Some(v) = crate::widgets::num_field(ui, ("dlg-size", number), Some((d.f64(total, 0.0) - gaps) / n), unit, WIDTH) { + d.fields.insert(total.into(), json!(n * v + gaps)); + } + } + ui.end_row(); + for (.., gutter, _) in axes { + form::row_label(ui, &gutter_label); + form::length(ui, d, gutter, unit, WIDTH); + } + ui.end_row(); + for (.., total) in axes { + form::row_label(ui, tl!("Total:")); + form::length(ui, d, total, unit, WIDTH); + } + ui.end_row(); + }); + ui.add_space(8.0); + form::check(ui, d, "addGuides", tl!("Add Guides")); +} + fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - form::grid(ui, d, app.session.general_unit()); - if matches!(d.kind.as_str(), "offsetPath" | "simplify") { + let unit = app.session.general_unit(); + if d.kind == "splitIntoGrid" { + split_grid(ui, d, unit); + } else { + form::grid(ui, d, unit); + } + if let Some(label) = previewed(&d.kind) { let (id, params) = command(d); if d.fields.get(form::PREVIEWED) != Some(¶ms) || !d.bool("preview") { d.fields.remove("__counts"); } - let label = if d.kind == "simplify" { "Simplify" } else { "Offset Path" }; if let Some(r) = form::preview_result(app, ui, d, label, id, params) && d.kind == "simplify" { @@ -55,7 +117,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let (id, params) = command(d); - if !matches!(d.kind.as_str(), "offsetPath" | "simplify") { + if previewed(&d.kind).is_none() { return run_and_close(app, id, params); } let r = form::commit_preview(app, id, params)?; diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index bfee917f..e3f6e072 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -379,6 +379,42 @@ fn a_form_dialog_is_as_wide_as_its_fields_not_the_window() { assert!(narrow.width() <= 360.0, "{:.0} wide in a 360-point window", narrow.width()); } +/// Object › Path › Split Into Grid opens on a grid the size of the selection, shows its rows and +/// columns side by side (number, size, gutter, total), previews on the canvas and, on OK, splits +/// the path and adds the guides as one undo step. +#[test] +fn split_into_grid_opens_at_the_selections_size_and_previews() { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 112, "height": 62})).unwrap(); + app.run("select.all", json!({})).unwrap(); + let undo = app.session.doc().unwrap().history.undo.len(); + crate::menus::invoke(&mut app, "object.path.splitIntoGrid", json!({})); + let d = app.ui.dialog.as_mut().unwrap(); + assert_eq!((d.f64("totalWidth", 0.0), d.f64("totalHeight", 0.0), d.bool("addGuides")), (112.0, 62.0, false)); + d.fields.extend([ + ("columns".to_string(), json!(3)), + ("columnGutter".to_string(), json!(8)), + ("addGuides".into(), json!(true)), + ("preview".into(), json!(true)), + ]); + frame(&mut app, Default::default()); + // The preview: 2 × 3 cells and the guides along their edges, not yet an undo step. + assert_eq!((objects(&app), app.session.doc().unwrap().doc.guides.len()), (6, 10)); + assert!(app.session.in_interaction()); + confirm(&mut app).unwrap(); + let st = app.session.doc().unwrap(); + assert_eq!((objects(&app), st.doc.guides.len(), st.history.undo.len()), (6, 10, undo + 1)); + assert!(app.ui.dialog.is_none() && !app.session.in_interaction()); + // Cancel rolls a preview back. + app.run("select.all", json!({})).unwrap(); + crate::menus::invoke(&mut app, "object.path.splitIntoGrid", json!({})); + app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true)); + frame(&mut app, Default::default()); + assert_eq!(objects(&app), 4, "one grid over the six cells"); + cancel(&mut app); + assert_eq!(objects(&app), 6); +} + #[test] fn menu_parameter_dialogs_are_compact() { // #292: Object › Path › Simplify (and its neighbours) spanned the whole window. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 9f8d8acb..94798738 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2974,6 +2974,8 @@ theme Appearance Motiv Offset Path Odsadit cestu Simplify Zjednodušit Split Into Grid Rozdělit do mřížky + Total: Celkem: + Add Guides Přidat vodítka Unite Sjednotit Delete Pattern Odstranit vzorek Done Hotovo diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index d00caa9f..949f4b84 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2691,6 +2691,8 @@ effect Points Punkte Spiral Tool Spirale-Werkzeug Split Freeform Line Freiformlinie teilen Split Into Grid In Raster teilen + Total: Gesamt: + Add Guides Hilfslinien hinzufügen Split Into Grid… In Raster teilen… Split Path Pfad teilen spot Vollton diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7467cec5..ae6ac751 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2741,6 +2741,8 @@ Spiral Tool Herramienta Espiral Split Freeform Line Dividir línea libre Split Into Grid Dividir en cuadrícula + Total: Total: + Add Guides Agregar guías Split Into Grid… Dividir en cuadrícula… Split Path Dividir trazado spot plana diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d70e57be..85242a40 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2738,6 +2738,8 @@ Spiral Tool Outil Spirale Split Freeform Line Scinder la ligne libre Split Into Grid Diviser en grille + Total: Total : + Add Guides Ajouter des repères Split Into Grid… Diviser en grille… Split Path Scinder le tracé spot ton direct diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 42a892de..3662f906 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3343,6 +3343,8 @@ Split Complementary Felosztott kiegészítő Split Freeform Line Szabad formájú vonal kettévágása Split Into Grid Felosztás rácsra + Total: Összesen: + Add Guides Segédvonalak hozzáadása Split Into Grid… Felosztás rácsra… Split Path Útvonal kettévágása spot direkt diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index cabf9da7..f1c529c7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2741,6 +2741,8 @@ Spiral Tool Strumento Spirale Split Freeform Line Dividi linea libera Split Into Grid Dividi in griglia + Total: Totale: + Add Guides Aggiungi guide Split Into Grid… Dividi in griglia… Split Path Dividi tracciato spot spot diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2500e047..3fdeeb5c 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2522,6 +2522,8 @@ Spiral Tool スパイラルツール Split Freeform Line フリーフォームラインを分割 Split Into Grid グリッドに分割 + Total: 合計: + Add Guides ガイドを追加 Split Path パスを分割 Spot Color 特色 Spot Colors 特色 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index d11ec669..cb1dad70 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2516,6 +2516,8 @@ Spiral Tool 나선 도구 Split Freeform Line 자유형 선 분할 Split Into Grid 격자로 분할 + Total: 합계: + Add Guides 안내선 추가 Split Path 패스 분할 Spot Color 별색 Spot Colors 별색 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 4111e53e..b22407a6 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2737,6 +2737,8 @@ Spiral Tool Инструмент «Спираль» Split Freeform Line Разделить произвольную линию Split Into Grid Разделить на сетку + Total: Всего: + Add Guides Добавить направляющие Split Into Grid… Разделить на сетку… Split Path Разделить контур spot плашечный diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2bd62953..293f79fa 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2734,6 +2734,8 @@ effect Points Точки Spiral Tool Інструмент «Спіраль» Split Freeform Line Розділити довільну лінію Split Into Grid Розділити на сітку + Total: Усього: + Add Guides Додати напрямні Split Into Grid… Розділити на сітку… Split Path Розділити контур spot плашковий diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index eaec2a43..95dd4b20 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2628,6 +2628,8 @@ Spiral Tool 螺旋工具 Split Freeform Line 分割自由曲线 Split Into Grid 分割为网格线 + Total: 总计: + Add Guides 添加参考线 Split Into Grid… 分割为网格… Split Path 分割路径 spot 专色 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 2c75bd15..84d609b0 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2719,6 +2719,8 @@ Spiral Tool 螺旋工具 Split Freeform Line 分割任意形狀線條 Split Into Grid 分割為格線 + Total: 總計: + Add Guides 增加參考線 Split Into Grid… 分割為格線… Split Path 分割路徑 spot 特別色 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index b9d78b21..47130d6a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3024,7 +3024,12 @@ fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Value)> "object.path.offsetPath" if app.ui.offset_path.is_object() => ("offsetPath", app.ui.offset_path.clone()), "object.path.offsetPath" => ("offsetPath", json!({"offset": "10 pt", "joins": "miter", "miterLimit": 4})), "object.path.simplify" => ("simplify", json!({"tolerance": "1 pt", "preview": true})), - "object.path.splitIntoGrid" => ("splitIntoGrid", json!({"rows": 2, "columns": 2, "gutter": "12 pt"})), + "object.path.splitIntoGrid" => { + // The grid starts at the size of the selection. + let size = + app.session.active().and_then(|st| st.doc.bounds_of(&st.selection.objects, false)).map_or((0.0, 0.0), |b| (b.width(), b.height())); + ("splitIntoGrid", crate::dialogs::path_ops::split_fields(size.0, size.1)) + } "object.vectorHalftone" => (crate::dialogs::halftone::KIND, crate::dialogs::halftone::fields()), "artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::fields()), _ => return None, diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 078dd52d..cb036d42 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -390,7 +390,7 @@ Perspective plane options: double-clicking a plane widget of the perspective gri opens the `perspectivePlane` dialog (fields `plane`: left, right or ground; `location`: points along the plane's normal; `objects`: none, move or copy). `ui.dialog.confirm` runs `perspective.plane.move` with them. -Units: dialog distance fields (Move's `dx`/`dy`, shape sizes, Offset Path's `offset`, Split Into Grid's `gutter`, +Units: dialog distance fields (Move's `dx`/`dy`, shape sizes, Offset Path's `offset`, Split Into Grid's gutters and totals, Artboard Options' sizes, effects' distances, `transformEach`'s `moveH`/`moveV`, New Document's `width`/`height`, the length preferences in `preferences`) show in the General unit (the active document's units, or `unitsGeneral` without a document). Through `ui.dialog.set` a number is in points and a string may carry its unit (`"10 mm"`; a bare number in a