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:
parent
974325b62a
commit
cb9d93b647
|
|
@ -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 ----------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<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)?;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue