Merge pull request #1488 from storytold/fix/1462-split-into-grid
Fix #1462: Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview
This commit is contained in:
commit
3dbb551db3
|
|
@ -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