Fix #1462: Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview

Object > Path > Split Into Grid took a number of rows and columns and one gutter, and split each
selected path on its own. The dialog now has the documented options, the rows beside the columns:
number, height or width, gutter and total, with Add Guides and Preview. Several selected paths
become one grid over them all, painted as the topmost one, as documented.

object.path.splitIntoGrid takes rowGutter and columnGutter (gutter still sets both), totalHeight
and totalWidth, height and width (the size of each row and column, which sets the total) and
addGuides, which adds ruler guides along the row and column edges on the grid's layer.

Behaviour reference:
https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/divide-or-split-objects.html
This commit is contained in:
bflatastic 2026-10-13 01:59:13 +07:00
parent 974325b62a
commit cb9d93b647
23 changed files with 255 additions and 48 deletions

View File

@ -149,7 +149,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<Value> {
// ---------- 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<Value> {
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::<Vec<_>>())) 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::<Vec<_>>(), "guides": edges.len() }))
}
// ---------- Clean Up ----------

View File

@ -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<Rect> = 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::<Vec<_>>(), [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();

View File

@ -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<PathData> {
/// 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<PathData> {
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,

View File

@ -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]

View File

@ -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);

View File

@ -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)),

View File

@ -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;

View File

@ -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(&params) || !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<Value, String> {
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)?;

View File

@ -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.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2522,6 +2522,8 @@
Spiral Tool スパイラルツール
Split Freeform Line フリーフォームラインを分割
Split Into Grid グリッドに分割
Total: 合計:
Add Guides ガイドを追加
Split Path パスを分割
Spot Color 特色
Spot Colors 特色

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2516,6 +2516,8 @@
Spiral Tool 나선 도구
Split Freeform Line 자유형 선 분할
Split Into Grid 격자로 분할
Total: 합계:
Add Guides 안내선 추가
Split Path 패스 분할
Spot Color 별색
Spot Colors 별색

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2737,6 +2737,8 @@
Spiral Tool Инструмент «Спираль»
Split Freeform Line Разделить произвольную линию
Split Into Grid Разделить на сетку
Total: Всего:
Add Guides Добавить направляющие
Split Into Grid… Разделить на сетку…
Split Path Разделить контур
spot плашечный

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2734,6 +2734,8 @@ effect Points Точки
Spiral Tool Інструмент «Спіраль»
Split Freeform Line Розділити довільну лінію
Split Into Grid Розділити на сітку
Total: Усього:
Add Guides Додати напрямні
Split Into Grid… Розділити на сітку…
Split Path Розділити контур
spot плашковий

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -2628,6 +2628,8 @@
Spiral Tool 螺旋工具
Split Freeform Line 分割自由曲线
Split Into Grid 分割为网格线
Total: 总计:
Add Guides 添加参考线
Split Into Grid… 分割为网格…
Split Path 分割路径
spot 专色

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2719,6 +2719,8 @@
Spiral Tool 螺旋工具
Split Freeform Line 分割任意形狀線條
Split Into Grid 分割為格線
Total: 總計:
Add Guides 增加參考線
Split Into Grid… 分割為格線…
Split Path 分割路徑
spot 特別色

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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,

View File

@ -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