Commands: a boolean or number given as text is refused

Squashed from #3428 onto main.
This commit is contained in:
bflatastic
2026-10-22 00:10:57 +07:00
parent 14f804cdee
commit e7cdf154ca
32 changed files with 1467 additions and 105 deletions
+1
View File
@@ -548,6 +548,7 @@ fn reduce_basic(s: &mut Session, p: &Value) -> Result<Value> {
fn set_item(s: &mut Session, p: &Value) -> Result<Value> {
let idx = index_param(p, "index", "appearance.setItem")?;
super::paint::check_keep_model(p, "appearance.setItem")?;
let paint = paint_from(s, p)?;
let ids = appearance_targets(s, p)?;
if ids.is_empty() {
+1
View File
@@ -1301,6 +1301,7 @@ fn symbol_adjust(s: &mut Session, p: &Value) -> Result<Value> {
if !["shift", "scrunch", "size", "spin", "stain", "screen", "style"].contains(&tool.as_str()) {
return Err(bad(C, format!("unknown tool `{tool}`")));
}
super::paint::check_keep_model(p, C)?;
let pts = points(p, C)?;
let radius = f64_or(p, "radius", 40.0).clamp(1.0, 2000.0);
let intensity = f64_or(p, "intensity", 5.0).clamp(1.0, 10.0) / 10.0;
+2
View File
@@ -752,6 +752,7 @@ fn lp_gap_options(s: &mut Session, p: &Value) -> Result<Value> {
fn lp_fill(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "livePaint.fill";
let pt = point_param(p, "point").filter(|q| finite(*q)).ok_or_else(|| bad(C, "missing point [x,y]"))?;
super::paint::check_keep_model(p, C)?;
let paint = paint_from(s, p)?.unwrap_or_else(|| s.paint.fill.clone());
let group = id_param(p, "group");
let roots = if group.is_none() { root_ids(s, p)? } else { vec![] };
@@ -781,6 +782,7 @@ fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "livePaint.strokeEdge";
let pt = point_param(p, "point").filter(|q| finite(*q)).ok_or_else(|| bad(C, "missing point [x,y]"))?;
let g = id_param(p, "group").ok_or_else(|| bad(C, "missing group"))?;
super::paint::check_keep_model(p, C)?;
let paint = paint_from(s, p)?.unwrap_or_else(|| s.paint.stroke.clone());
let width = p.get("width").and_then(Value::as_f64).unwrap_or(s.paint.stroke_width.max(0.0));
if !(0.0..=1000.0).contains(&width) {
+1
View File
@@ -494,6 +494,7 @@ fn path_create(s: &mut Session, p: &Value) -> Result<Value> {
fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
let x = f64_req(p, "x", "text.create")?;
let y = f64_req(p, "y", "text.create")?;
super::paint::check_keep_model(p, "text.create")?;
let text = str_param(p, "text").unwrap_or("");
let mut t = TextObject::point(Point::new(x, y), text, new_type_style(s, p));
t.vertical = p.get("vertical").and_then(Value::as_bool).unwrap_or(false);
+43 -21
View File
@@ -60,7 +60,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Liquify",
[],
None,
"{tool: warp|twirl|pucker|bloat|scallop|crystallize|wrinkle, points: [[x,y,pressure?]…] (the brush stroke; pen pressure 0..1, default 1), diameter?|width?, height? (pt, default 100), angle?, intensity? (0..1 or %), usePressure? (each point's pressure is the intensity there), detail? (1..10), simplify?: 0..100, simplifyOn?: bool (default true; both for warp, twirl, pucker, bloat), rate? (twirl °), complexity? (0..15), horizontal?, vertical? (wrinkle, 0..1 or %), affectAnchors?, affectIn?, affectOut? (scallop, crystallize, wrinkle; default true), ids?|id? (default: selection, else every path the brush touches)}",
"{tool: warp|twirl|pucker|bloat|scallop|crystallize|wrinkle, points: [[x,y,pressure?]…] (the brush stroke; pen pressure 0..1, default 1), diameter?|width?, height? (pt, default 100), angle?, intensity? (0..1 or %), usePressure?: bool (each point's pressure is the intensity there), detail? (1..10), simplify?: 0..100, simplifyOn?: bool (default true; both for warp, twirl, pucker, bloat), rate? (twirl °), complexity? (0..15), horizontal?, vertical? (wrinkle, 0..1 or %), affectAnchors?, affectIn?, affectOut?: bool (scallop, crystallize, wrinkle; default true), ids?|id? (default: selection, else every path the brush touches)}",
has_doc,
liquify
),
@@ -300,7 +300,7 @@ fn width_point_set(s: &mut Session, p: &Value) -> Result<Value> {
if !(left.is_finite() && right.is_finite()) || left < 0.0 || right < 0.0 || left.max(right) > 1.0e5 {
return Err(bad(C, "left/right must be non-negative widths"));
}
let adjust = bool_or(p, "adjustAdjoining", false);
let adjust = switch_or(p, "adjustAdjoining", false, C)?;
// When the profile comes out as it was, within a rounding error (1e-9), the document and its undo
// history stay as they are.
let mut pos = 0;
@@ -405,7 +405,7 @@ fn width_profile_set(s: &mut Session, p: &Value) -> Result<Value> {
Some(Value::Array(a)) => {
let mut pts = vec![];
for q in a {
let v: Vec<f64> = q.as_array().map(|x| x.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
let v: Vec<f64> = q.as_array().and_then(|x| x.iter().map(Value::as_f64).collect()).unwrap_or_default();
if v.len() != 3 || v.iter().any(|x| !x.is_finite()) || v[1] < 0.0 || v[2] < 0.0 {
return Err(bad(C, "points must be [t, left, right] with non-negative widths"));
}
@@ -632,9 +632,17 @@ impl LiquifyStroke {
}
}
/// The number keys of `object.liquify`. [`LiquifyParams::from_json`] reads a value that is not a
/// number as the key left out.
const LIQUIFY_NUMBERS: &[&str] =
&["diameter", "width", "height", "angle", "intensity", "detail", "simplify", "rate", "complexity", "horizontal", "vertical"];
fn liquify(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.liquify";
check_numbers(p, LIQUIFY_NUMBERS, C)?;
let prm = LiquifyParams::from_json(p).ok_or_else(|| bad(C, "missing or unknown `tool`"))?;
// `LiquifyParams::from_json` uses a key's default for a value that is not true or false.
check_switches(p, &["usePressure", "simplifyOn", "affectAnchors", "affectIn", "affectOut"], C)?;
let pts = samples_of(p, "points").filter(|v| !v.is_empty()).ok_or_else(|| bad(C, "points must be a non-empty [[x,y,pressure?]…] list"))?;
if pts.iter().any(|(q, f)| !q.x.is_finite() || !q.y.is_finite() || !f.is_finite()) {
return Err(bad(C, "points must be finite"));
@@ -701,9 +709,10 @@ fn puppet_ids(p: &Value, default: impl FnOnce() -> Result<Vec<NodeId>>) -> Resul
fn puppet_warp(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.puppetWarp";
check_numbers(p, &["expand"], C)?;
let pins = points_of(p, "pins").ok_or_else(|| bad(C, "pins must be [[x,y]…]"))?;
let moved = points_of(p, "moved").ok_or_else(|| bad(C, "moved must be [[x,y]…]"))?;
let rest = bool_or(p, "rest", false);
let rest = switch_or(p, "rest", false, C)?;
if (pins.is_empty() && !rest) || pins.len() != moved.len() {
return Err(bad(C, "pins and moved must be non-empty lists of the same length"));
}
@@ -715,7 +724,9 @@ fn puppet_warp(s: &mut Session, p: &Value) -> Result<Value> {
let expand = f64_or(p, "expand", 3.0).clamp(0.0, 1000.0);
let angles: Vec<Option<f64>> = match p.get("angles") {
None | Some(Value::Null) => vec![None; pins.len()],
Some(Value::Array(a)) if a.len() == pins.len() => a.iter().map(|v| v.as_f64().filter(|d| d.is_finite()).map(f64::to_radians)).collect(),
Some(Value::Array(a)) if a.len() == pins.len() && a.iter().all(|v| v.is_null() || v.is_number()) => {
a.iter().map(|v| v.as_f64().filter(|d| d.is_finite()).map(f64::to_radians)).collect()
}
Some(_) => return Err(bad(C, "angles must be a list as long as pins (degrees or null)")),
};
let ids_json = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() });
@@ -756,6 +767,7 @@ pub(crate) fn drop_puppet_pins(st: &mut crate::DocState) {
}
fn puppet_pins(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["expand"], "object.puppetWarp.pins")?;
let ids = puppet_ids(p, || Ok(s.doc()?.selection.objects.clone()))?;
let expand = f64_or(p, "expand", 3.0).clamp(0.0, 1000.0);
let set = PinSet::of(&s.doc()?.doc, &ids, expand).ok_or_else(|| EngineError::Other("nothing to warp".into()))?;
@@ -798,9 +810,10 @@ pub(crate) fn silent(s: &mut Session, f: impl FnOnce(&mut PerspectiveGrid)) -> R
}
fn grid_set(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "perspective.grid.set";
let old = grid_of(&s.doc()?.doc);
let g = old.merged(p).map_err(|e| bad("perspective.grid.set", e))?;
let reproject = bool_or(p, "reproject", false);
let g = old.merged(p).map_err(|e| bad(C, e))?;
let reproject = switch_or(p, "reproject", false, C)?;
// When the document holds this grid already, no object moves, and the document and its undo
// history stay as they are.
if grid_kept(&s.doc()?.doc, &g) {
@@ -823,7 +836,12 @@ fn grid_set(s: &mut Session, p: &Value) -> Result<Value> {
}
fn grid_show(s: &mut Session, p: &Value) -> Result<Value> {
let want = p.get("visible").and_then(Value::as_bool);
const C: &str = "perspective.grid.show";
// A missing or null `visible` toggles the grid.
let want = match p.get("visible") {
None | Some(Value::Null) => None,
Some(_) => Some(switch_or(p, "visible", false, C)?),
};
let g = silent(s, |g| g.visible = want.unwrap_or(!g.visible))?;
Ok(json!({ "visible": g.visible }))
}
@@ -1037,7 +1055,9 @@ fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(C, "from and to must be finite"));
}
let fallback = plane_param(&grid_of(&s.doc()?.doc), p, C).ok();
let (perp, copy) = (bool_or(p, "perpendicular", false), bool_or(p, "copy", false));
let (perp, copy) = (switch_or(p, "perpendicular", false, C)?, switch_or(p, "copy", false, C)?);
// `snap_fix` uses Snap to Grid's setting for a `snap` that is not true or false.
check_switches(p, &["snap"], C)?;
// Snap to Grid: a correction for the objects on the first one's plane and depth.
let fix = if perp { None } else { super::perspgrid::snap_fix(&s.doc()?.doc, &ids, p, (from, to)) };
let snapped = move |pl: Plane, depth: f64, dv: vectorcraft_geom::Vec2| {
@@ -1088,13 +1108,13 @@ fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
fn persp_transform(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "perspective.transform";
let m = matrix_param(p, "matrix").ok_or_else(|| bad(C, "missing matrix [a,b,c,d,e,f]"))?;
let dz = f64_or(p, "depth", 0.0);
let dz = number_or(p, "depth", 0.0, C)?;
if !m.as_coeffs().iter().all(|v| v.is_finite()) || m.determinant().abs() < 1e-12 || !dz.is_finite() {
return Err(bad(C, "matrix must be finite and invertible, depth finite"));
}
let ids = roots(s, p)?;
let fallback = plane_param(&grid_of(&s.doc()?.doc), p, C).ok();
let copy = bool_or(p, "copy", false);
let copy = switch_or(p, "copy", false, C)?;
let step = |g: &PerspectiveGrid, pl: Plane, depth: f64| -> Result<(Homography, f64)> {
let to = depth + dz;
if to.abs() > persp::MAX_DEPTH {
@@ -1126,6 +1146,7 @@ pub(crate) fn transform_again(s: &mut Session, again: &Value) -> Result<Value> {
fn plane_move(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "perspective.plane.move";
check_numbers(p, &["offset", "by"], C)?;
let old = grid_of(&s.doc()?.doc);
let plane = plane_param(&old, p, C)?;
let from = old.offset(plane);
@@ -1223,11 +1244,12 @@ fn plane_match(s: &mut Session, p: &Value) -> Result<Value> {
fn persp_nudge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "perspective.nudge";
let (dx, dy) = (f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let (dx, dy) = (number_or(p, "dx", 0.0, C)?, number_or(p, "dy", 0.0, C)?);
if !(dx.is_finite() && dy.is_finite()) || (dx == 0.0 && dy == 0.0) {
return Err(bad(C, "dx and dy give the arrow's direction"));
}
let k = s.prefs.keyboard_increment * if bool_or(p, "big", false) { 10.0 } else { 1.0 };
let k = s.prefs.keyboard_increment * if switch_or(p, "big", false, C)? { 10.0 } else { 1.0 };
let copy = switch_or(p, "copy", false, C)?;
let ids = selected_roots(s)?;
// The step is measured on the page at the centre of the selection's perspective box.
let st = s.doc()?;
@@ -1235,10 +1257,7 @@ fn persp_nudge(s: &mut Session, p: &Value) -> Result<Value> {
let (plane, depth, rect) = g.plane_bounds(&st.doc, &ids).ok_or_else(|| EngineError::Other("the selection isn't in perspective".into()))?;
let from = g.homography_at(plane, depth).and_then(|h| h.apply(rect.center())).ok_or_else(beyond_horizon)?;
let to = from + vectorcraft_geom::Vec2::new(dx.clamp(-1.0, 1.0), dy.clamp(-1.0, 1.0)) * k;
persp_move(
s,
&json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "from": [from.x, from.y], "to": [to.x, to.y], "copy": bool_or(p, "copy", false)}),
)
persp_move(s, &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "from": [from.x, from.y], "to": [to.x, to.y], "copy": copy}))
}
fn persp_draw(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1247,11 +1266,14 @@ fn persp_draw(s: &mut Session, p: &Value) -> Result<Value> {
let params = p.get("params").cloned().unwrap_or_else(|| json!({}));
let mut g = grid_of(&s.doc()?.doc);
let plane = plane_param(&g, p, C)?;
let snap = bool_or(p, "snap", g.snap);
let snap = switch_or(p, "snap", g.snap, C)?;
// A click-to-size dialog's shape keeps its sizes in plane units from the click.
let map = match point_param(p, "at") {
Some(at) => Some(g.size_homography(plane, at, snap).ok_or_else(beyond_horizon)?),
None => None,
let map = match p.get("at") {
None | Some(Value::Null) => None,
Some(v) => {
let at = finite_numbers(v).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad(C, "`at` must be [x, y]"))?;
Some(g.size_homography(plane, at, snap).ok_or_else(beyond_horizon)?)
}
};
let undo_before = s.doc()?.history.undo.len();
let r = s.execute(&command, &params)?;
+40 -29
View File
@@ -152,24 +152,40 @@ pub fn specs() -> Vec<CommandSpec> {
// ---------- helpers ----------
/// `[[x,y]…]` or `[{x,y}…]`; non-finite points are dropped.
/// `[[x,y]…]` or `[{x,y}…]`.
fn points_param(p: &Value, key: &str, cmd: &str) -> Result<Vec<Point>> {
Ok(samples_param(p, key, cmd)?.into_iter().map(|(pt, _)| pt).collect())
}
/// The point `[x, y, …]` or `{x, y, …}` that `v` holds, or an error when its x or y is not a finite
/// number.
fn point_entry(v: &Value, cmd: &str) -> Result<Point> {
let xy = match v {
Value::Array(xy) => xy.first().and_then(Value::as_f64).zip(xy.get(1).and_then(Value::as_f64)),
Value::Object(_) => v.get("x").and_then(Value::as_f64).zip(v.get("y").and_then(Value::as_f64)),
_ => None,
};
xy.filter(|(x, y)| x.is_finite() && y.is_finite()).map(|(x, y)| Point::new(x, y)).ok_or_else(|| bad(cmd, "x and y must both be numbers"))
}
/// [`points_param`] with each point's pen pressure, if it has one: `[[x,y,pressure]…]` or
/// `[{x,y,pressure}…]` (0..1).
/// `[{x,y,pressure}…]` (0..1). A point whose x or y is not a number, or whose pressure is neither a
/// number nor null, is an error.
fn samples_param(p: &Value, key: &str, cmd: &str) -> Result<Vec<(Point, Option<f64>)>> {
let a = p.get(key).and_then(Value::as_array).ok_or_else(|| bad(cmd, format!("missing array `{key}`")))?;
let pts: Vec<(Point, Option<f64>)> = a
.iter()
.filter_map(|v| match v {
Value::Array(xy) => Some((Point::new(xy.first()?.as_f64()?, xy.get(1)?.as_f64()?), xy.get(2).and_then(Value::as_f64))),
Value::Object(_) => Some((Point::new(v.get("x")?.as_f64()?, v.get("y")?.as_f64()?), v.get("pressure").and_then(Value::as_f64))),
_ => None,
.map(|v| {
let pressure = match v {
Value::Array(xy) => xy.get(2),
_ => v.get("pressure"),
};
match pressure {
None | Some(Value::Null) | Some(Value::Number(_)) => Ok((point_entry(v, cmd)?, pressure.and_then(Value::as_f64))),
Some(f) => Err(bad(cmd, format!("`pressure` must be a number, not {f}"))),
}
})
.filter(|(p, _)| p.x.is_finite() && p.y.is_finite())
.collect();
.collect::<Result<_>>()?;
if pts.is_empty() {
return Err(bad(cmd, "need at least one point"));
}
@@ -648,7 +664,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
}
let pressure = stroke_pressure(&samples);
let mut pts: Vec<Point> = samples.into_iter().map(|(pt, _)| pt).collect();
let tol = f64_or(p, "fidelity", 1.5).clamp(0.05, 100.0);
let tol = number_or(p, "fidelity", 1.5, C)?.clamp(0.05, 100.0);
let closed = bool_or(p, "closed", false);
let brush = str_param(p, "style") == Some("brush");
let keep_selected = bool_or(p, "keepSelected", true);
@@ -724,7 +740,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
}
if let Some(rd) = p.get("redraw").filter(|v| !v.is_null()) {
let id = id_param(rd, "id").ok_or_else(|| bad(C, "redraw needs id"))?;
let within = f64_or(rd, "within", 12.0);
let within = number_or(rd, "within", 12.0, C)?;
if !within.is_finite() || within < 0.0 {
return Err(bad(C, "redraw within must be a finite distance of 0 or more"));
}
@@ -764,16 +780,8 @@ fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
fn curvature(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.curvature";
let arr = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(C, "missing points"))?;
let pts: Vec<(Point, bool)> = arr
.iter()
.filter_map(|v| {
let pt = match v {
Value::Array(xy) => Point::new(xy.first()?.as_f64()?, xy.get(1)?.as_f64()?),
_ => Point::new(v.get("x")?.as_f64()?, v.get("y")?.as_f64()?),
};
(pt.x.is_finite() && pt.y.is_finite()).then(|| (pt, v.get("corner").and_then(Value::as_bool).unwrap_or(false)))
})
.collect();
let pts: Vec<(Point, bool)> =
arr.iter().map(|v| Ok((point_entry(v, C)?, v.get("corner").and_then(Value::as_bool).unwrap_or(false)))).collect::<Result<_>>()?;
if pts.is_empty() {
return Err(bad(C, "need at least one point"));
}
@@ -807,6 +815,7 @@ fn curvature_edit(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.curvatureEdit";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let si = usize_opt(p, "subpath", C)?.unwrap_or(0);
check_numbers(p, &["x", "y", "t"], C)?;
let at = match (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) {
(Some(x), Some(y)) if x.is_finite() && y.is_finite() => Some(Point::new(x, y)),
(None, None) => None,
@@ -980,9 +989,11 @@ fn convert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let si = usize_opt(p, "subpath", C)?.unwrap_or(0);
let ai = usize_req(p, "anchor", C)?;
let smooth = str_param(p, "to") == Some("smooth");
check_numbers(p, &["x", "y"], C)?;
let target = match (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) {
(Some(x), Some(y)) => Some(Point::new(x, y)),
_ => None,
(None, None) => None,
_ => return Err(bad(C, "x and y must be given together")),
};
if target.is_some_and(|q| !within_input_bound(q)) {
return Err(bad(C, "x and y must be finite"));
@@ -1014,8 +1025,8 @@ fn reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let si = usize_opt(p, "subpath", C)?.unwrap_or(0);
let seg = usize_req(p, "segment", C)?;
let t = f64_or(p, "t", 0.5).clamp(0.05, 0.95);
let dv = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let t = number_or(p, "t", 0.5, C)?.clamp(0.05, 0.95);
let dv = Vec2::new(number_or(p, "dx", 0.0, C)?, number_or(p, "dy", 0.0, C)?);
if !(dv.x.is_finite() && dv.y.is_finite() && dv.hypot() < MAX_INPUT) {
return Err(bad(C, "invalid delta"));
}
@@ -1132,7 +1143,7 @@ fn knife(s: &mut Session, p: &Value) -> Result<Value> {
fn erase_region(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.eraseRegion";
let pts = points_param(p, "points", C)?;
let size = f64_or(p, "size", 10.0);
let size = number_or(p, "size", 10.0, C)?;
if !(size.is_finite() && size > 0.0) {
return Err(bad(C, "size must be positive"));
}
@@ -1174,7 +1185,7 @@ fn apply_plan(s: &mut Session, label: &str, plan: Vec<(NodeId, Vec<PathData>)>)
fn erase_segments(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.eraseSegments";
let pts = points_param(p, "points", C)?;
let w = f64_or(p, "width", 8.0);
let w = number_or(p, "width", 8.0, C)?;
if !(w.is_finite() && w > 0.0) {
return Err(bad(C, "width must be positive"));
}
@@ -1317,8 +1328,8 @@ fn smooth_open(anchors: &[Anchor], marked: &[bool], near: &dyn Fn(Point) -> bool
fn smooth_region(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.smoothRegion";
let pts = points_param(p, "points", C)?;
let radius = f64_or(p, "radius", 12.0).max(0.0);
let tol = f64_or(p, "fidelity", 2.5).clamp(0.05, 100.0);
let radius = number_or(p, "radius", 12.0, C)?.max(0.0);
let tol = number_or(p, "fidelity", 2.5, C)?.clamp(0.05, 100.0);
let st = s.doc()?;
let near = |q: Point| dist_to_polyline(&pts, q) <= radius;
let mut plan = vec![];
@@ -1365,7 +1376,7 @@ fn smooth_region(s: &mut Session, p: &Value) -> Result<Value> {
fn join_scrub(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.joinScrub";
let pts = points_param(p, "points", C)?;
let tol = f64_or(p, "tolerance", 12.0).max(0.0);
let tol = number_or(p, "tolerance", 12.0, C)?.max(0.0);
let st = s.doc()?;
// Open endpoints near the scrub, ordered along it: (order, id, subpath, at_start).
let poly = {
@@ -1464,7 +1475,7 @@ fn join_scrub(s: &mut Session, p: &Value) -> Result<Value> {
fn blob(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.blob";
let pts = points_param(p, "points", C)?;
let size = f64_or(p, "size", 10.0);
let size = number_or(p, "size", 10.0, C)?;
if !(size.is_finite() && size > 0.0) {
return Err(bad(C, "size must be positive"));
}
+2
View File
@@ -244,6 +244,7 @@ fn add_point(s: &mut Session, p: &Value) -> Result<Value> {
Some(Value::Bool(true)) => s.selected_freeform_point(),
Some(_) => return Err(bad(C, "`line` must be true or false")),
};
super::paint::check_keep_model(p, C)?;
let given = given_color(s, p);
let index = edit(s, p, C, |f, sp| {
let q = sp.to_paint * at;
@@ -266,6 +267,7 @@ fn add_point(s: &mut Session, p: &Value) -> Result<Value> {
fn set_point(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "paint.freeform.setPoint";
let i = point_index(s, p, C)?;
super::paint::check_keep_model(p, C)?;
let given = given_color(s, p);
edit(s, p, C, |f, sp| {
check_index(f, i)?;
+1
View File
@@ -381,6 +381,7 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result<Value> {
Some(_) if !stroke => return Err(bad(C, "`strokeMode` applies to strokes (stroke: true)")),
Some(m) => Some(StrokeGradientMode::parse(m).ok_or_else(|| bad(C, format!("strokeMode must be within|along|across, got {m}")))?),
};
super::paint::check_keep_model(p, C)?;
// New RGB stop colors take the document's model; those of the swatches the stops name don't.
let stored = stop_colors(s, p, p);
// Stops naming swatches take their colours from them.
+18 -12
View File
@@ -411,6 +411,7 @@ const DEFAULT_DISTANCE: f64 = 10.0;
fn spacing_param(p: &Value, cmd: &str, current: BlendSpacing) -> Result<BlendSpacing> {
check_numbers(p, &["value", "steps", "distance"], cmd)?;
let smooth = switch_or(p, "smooth", false, cmd)?;
let num = |k: &str| p.get(k).and_then(Value::as_f64);
let steps = |k: &str, n: f64| whole(n, k, cmd).map(|n| BlendSpacing::Steps(n as u32));
let mode = str_param(p, "spacing");
@@ -428,7 +429,7 @@ fn spacing_param(p: &Value, cmd: &str, current: BlendSpacing) -> Result<BlendSpa
steps("steps", n)?
} else if let Some(d) = num("distance") {
BlendSpacing::Distance(d)
} else if bool_or(p, "smooth", false) {
} else if smooth {
BlendSpacing::SmoothColor
} else {
current
@@ -643,7 +644,7 @@ fn spine_move_anchor(s: &mut Session, p: &Value) -> Result<Value> {
Some("in") => Some(false),
Some(_) => return Err(bad(C, "handle must be in or out")),
};
let independent = bool_or(p, "independent", false);
let independent = switch_or(p, "independent", false, C)?;
edit_spine(s, p, C, "Reshape Spine", |sp, anchors, keys| {
let a = sp.anchors.get_mut(index).ok_or_else(|| bad(C, "no such spine point"))?;
if let Some(out) = handle {
@@ -697,12 +698,12 @@ fn spine_convert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
fn spine_reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.blend.spine.reshapeSegment";
let seg = index_param(p, "segment", C)?;
let t = f64_or(p, "t", 0.5).clamp(0.05, 0.95);
let dv = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let t = number_or(p, "t", 0.5, C)?.clamp(0.05, 0.95);
let dv = Vec2::new(number_or(p, "dx", 0.0, C)?, number_or(p, "dy", 0.0, C)?);
if !(t.is_finite() && dv.x.is_finite() && dv.y.is_finite() && dv.hypot() < 1e9) {
return Err(bad(C, "t, dx and dy must be finite"));
}
let perpendicular = bool_or(p, "perpendicular", false);
let perpendicular = switch_or(p, "perpendicular", false, C)?;
edit_spine(s, p, C, "Reshape Spine", |sp, _, _| {
let bent = if perpendicular { sp.bend_segment_perpendicular(seg, t, dv) } else { sp.bend_segment(seg, t, dv) };
if bent { Ok(()) } else { Err(bad(C, "no such spine segment")) }
@@ -991,10 +992,11 @@ fn warp_kind(p: &Value, cmd: &str, cur: Option<&EnvelopeKind>) -> Result<Envelop
bend = f64_or(p, "bend", bend).clamp(-100.0, 100.0);
h = f64_or(p, "h", f64_or(p, "horizontalDistortion", h)).clamp(-100.0, 100.0);
v = f64_or(p, "v", f64_or(p, "verticalDistortion", v)).clamp(-100.0, 100.0);
let given = switch_or(p, "horizontal", horizontal, cmd)?;
horizontal = match str_param(p, "orientation") {
Some("vertical") => false,
Some("horizontal") => true,
_ => bool_or(p, "horizontal", horizontal),
_ => given,
};
Ok(EnvelopeKind::Warp { style, bend, h, v, horizontal })
}
@@ -1119,7 +1121,7 @@ fn env_reset_warp(s: &mut Session, p: &Value) -> Result<Value> {
fn env_reset_mesh(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.envelope.resetWithMesh";
let maintain = bool_or(p, "maintainShape", true);
let maintain = switch_or(p, "maintainShape", true, C)?;
let kinds = reshaped(s, |env, kind| {
let own = match kind {
EnvelopeKind::Mesh { rows, cols, .. } => (*rows, *cols),
@@ -1150,11 +1152,11 @@ fn options_param(p: &Value, cmd: &str, cur: EnvelopeOptions) -> Result<EnvelopeO
None => cur.preserve_shape,
};
Ok(EnvelopeOptions {
anti_alias: bool_or(p, "antiAlias", cur.anti_alias),
anti_alias: switch_or(p, "antiAlias", cur.anti_alias, cmd)?,
preserve_shape,
distort_appearance: bool_or(p, "distortAppearance", cur.distort_appearance),
distort_linear_gradients: bool_or(p, "distortLinearGradients", cur.distort_linear_gradients),
distort_pattern_fills: bool_or(p, "distortPatternFills", cur.distort_pattern_fills),
distort_appearance: switch_or(p, "distortAppearance", cur.distort_appearance, cmd)?,
distort_linear_gradients: switch_or(p, "distortLinearGradients", cur.distort_linear_gradients, cmd)?,
distort_pattern_fills: switch_or(p, "distortPatternFills", cur.distort_pattern_fills, cmd)?,
})
}
@@ -1249,6 +1251,7 @@ fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.envelope.options";
check_numbers(p, &["fidelity"], C)?;
check_numbers(p, WARP_NUMBERS, C)?;
check_switches(p, &["horizontal"], C)?;
let fid = p.get("fidelity").and_then(Value::as_f64);
if fid.is_some_and(|f| !(0.0..=100.0).contains(&f)) {
return Err(bad(C, "fidelity must be 0..100"));
@@ -1330,9 +1333,10 @@ fn env_expand(s: &mut Session, _: &Value) -> Result<Value> {
}
fn env_edit_contents(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.envelope.editContents";
let envs = selected_of(s, is_envelope);
let cur = matches!(envs.first().and_then(|e| s.doc().ok()?.doc.node(*e)).map(|n| &n.kind), Some(NodeKind::Envelope { editing: true, .. }));
let want = bool_or(p, "editing", !cur);
let want = switch_or(p, "editing", !cur, C)?;
s.edit(if want { "Edit Contents" } else { "Edit Envelope" }, |d, sel| {
let mut content_ids = vec![];
for e in &envs {
@@ -1535,6 +1539,7 @@ fn whole(x: f64, key: &str, cmd: &str) -> Result<f64> {
fn mesh_set_color(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.mesh.setPointColor";
check_numbers(p, &["opacity"], C)?;
super::paint::check_keep_model(p, C)?;
let index = index_param(p, "index", C)?;
let color = p.get("color").and_then(color_value).map(|c| super::paint::new_color(s, p, c));
let opacity = p.get("opacity").and_then(Value::as_f64);
@@ -1573,6 +1578,7 @@ fn mesh_move_point(s: &mut Session, p: &Value) -> Result<Value> {
fn mesh_add_line(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.mesh.addLine";
let q = Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?);
super::paint::check_keep_model(p, C)?;
let color = p.get("color").and_then(color_value).map(|c| super::paint::new_color(s, p, c));
let index = with_mesh(s, p, C, "Add Mesh Line", true, |m| {
let i = m.add_lines_at(q).ok_or_else(|| bad(C, "point is outside the mesh"))?;
+14 -13
View File
@@ -348,11 +348,11 @@ fn transform_each_as(s: &mut Session, p: &Value, label: &str) -> Result<Value> {
let mh = f64_or(p, "moveH", 0.0).clamp(-1.0e7, 1.0e7);
let mv = f64_or(p, "moveV", 0.0).clamp(-1.0e7, 1.0e7);
let rot = f64_or(p, "rotate", 0.0);
let rx = bool_or(p, "reflectX", false);
let ry = bool_or(p, "reflectY", false);
let random = bool_or(p, "random", false);
let rx = switch_or(p, "reflectX", false, "object.transformEach")?;
let ry = switch_or(p, "reflectY", false, "object.transformEach")?;
let random = switch_or(p, "random", false, "object.transformEach")?;
let refi = whole_or(p, "reference", 4, "object.transformEach")?.min(8) as usize;
let copy = bool_or(p, "copy", false);
let copy = switch_or(p, "copy", false, "object.transformEach")?;
let objects = super::object::transforms_objects(p);
if sh == 0.0 || sv == 0.0 {
return Err(bad("object.transformEach", "scale must be non-zero"));
@@ -598,7 +598,7 @@ fn object_mosaic(s: &mut Session, p: &Value) -> Result<Value> {
let raster = vectorcraft_trace::Raster::decode(&blob.bytes).map_err(|e| bad(C, e.to_string()))?;
let cols = whole_or(p, "columns", 10, C)?.clamp(1, 1000) as u32;
let rows = whole_or(p, "rows", 10, C)?.clamp(1, 1000) as u32;
let gray = bool_or(p, "gray", false);
let gray = switch_or(p, "gray", false, C)?;
let colors = vectorcraft_trace::mosaic(&raster, cols, rows);
// Tiles in image space, placed by the image transform (scaled to a new size if asked).
let b = node.geometric_bounds().ok_or_else(|| bad(C, "image has no bounds"))?;
@@ -613,7 +613,7 @@ fn object_mosaic(s: &mut Session, p: &Value) -> Result<Value> {
let ppx = (xf * Point::new(1.0, 0.0) - xf * Point::ZERO).hypot().max(1e-9);
let ppy = (xf * Point::new(0.0, 1.0) - xf * Point::ZERO).hypot().max(1e-9);
let (gx, gy) = ((f64_or(p, "spacingX", 0.0).max(0.0) / ppx).min(tw * 0.9), (f64_or(p, "spacingY", 0.0).max(0.0) / ppy).min(th * 0.9));
let delete = bool_or(p, "deleteRaster", false);
let delete = switch_or(p, "deleteRaster", false, C)?;
let (gid, n) = s.edit("Create Object Mosaic", |d, sel| {
let mut tiles = vec![];
for r in 0..rows {
@@ -670,7 +670,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
.ok_or_else(|| bad(C, "select an image"))?;
let node = st.doc.node(id).cloned().ok_or(EngineError::NoNode(id))?;
let NodeKind::Image(im) = &node.kind else { return Err(bad(C, "select an image")) };
if p.get("trim").and_then(Value::as_bool) == Some(true) {
if switch_or(p, "trim", false, C)? {
return trim_image(s, id, (**im).clone());
}
let ib = node.geometric_bounds().ok_or_else(|| bad(C, "image has no bounds"))?;
@@ -836,9 +836,9 @@ impl Session {
fn trim_marks(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.createTrimMarks";
check_numbers(p, &["offset", "length", "bleed", "weight"], C)?;
let style = match str_param(p, "style") {
Some(v) => MarkStyle::parse(v).ok_or_else(|| bad(C, "`style` must be roman or japanese"))?,
None => s.crop_mark_style(),
let style = match p.get("style") {
None | Some(Value::Null) => s.crop_mark_style(),
Some(v) => v.as_str().and_then(MarkStyle::parse).ok_or_else(|| bad(C, "`style` must be roman or japanese"))?,
};
let d = TrimMarks::default();
let marks = TrimMarks {
@@ -849,7 +849,7 @@ fn trim_marks(s: &mut Session, p: &Value) -> Result<Value> {
weight: f64_or(p, "weight", d.weight).max(0.01),
};
let st = s.doc()?;
let rects: Vec<Rect> = if bool_or(p, "allArtboards", st.selection.is_empty()) {
let rects: Vec<Rect> = if switch_or(p, "allArtboards", st.selection.is_empty(), C)? {
st.doc.artboards.iter().map(|a| a.rect).collect()
} else {
st.doc.bounds_of(&selected_roots(s)?, true).into_iter().collect()
@@ -993,9 +993,10 @@ fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
use super::newdoc::ArtboardLayout as L;
// A value given (not null) must be one of the names.
let named = |key: &str| p.get(key).filter(|v| !v.is_null()).map(|v| v.as_str().unwrap_or_default());
let by_column = switch_or(p, "byColumn", false, CMD)?;
let layout = match named("layout") {
Some(l) => L::parse(l).ok_or_else(|| bad(CMD, "layout must be gridByRow, gridByColumn, row or column"))?,
None if bool_or(p, "byColumn", false) => L::GridByColumn,
None if by_column => L::GridByColumn,
None => L::GridByRow,
};
let rtl = match named("order") {
@@ -1016,7 +1017,7 @@ fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
};
check_numbers(p, &["spacing"], CMD)?;
let spacing = f64_or(p, "spacing", 20.0).clamp(-1.0e5, 1.0e5);
let move_art = bool_or(p, "moveArtwork", true);
let move_art = switch_or(p, "moveArtwork", true, CMD)?;
let locked_and_hidden = s.prefs.move_locked_with_artboard;
let d = &s.doc()?.doc;
let rects: Vec<Rect> = d.artboards.iter().map(|a| a.rect).collect();
+28 -6
View File
@@ -318,20 +318,42 @@ pub(crate) fn f64_or(p: &Value, key: &str, default: f64) -> f64 {
pub(crate) fn f64_req(p: &Value, key: &str, cmd: &str) -> Result<f64> {
p.get(key).and_then(Value::as_f64).ok_or_else(|| bad(cmd, format!("missing number `{key}`")))
}
/// Error unless each of `keys` that `p` gives is a number (or null): an optional number read with
/// [`f64_or`] would otherwise fall back to its default for a string such as `"60 mm"` (#1952).
/// The number `p` gives for `key`, or `default` when `p` leaves it out or gives null. Any other
/// value, such as `"40"`, is an error: "`{key}` must be a number, not {v}".
pub(crate) fn number_or(p: &Value, key: &str, default: f64, cmd: &str) -> Result<f64> {
match p.get(key) {
None | Some(Value::Null) => Ok(default),
Some(v) => v.as_f64().ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))),
}
}
/// Error unless each of `keys` that `p` gives is a number (or null), as [`number_or`] reads them: an
/// optional number read with [`f64_or`] would otherwise fall back to its default for a string such as
/// `"60 mm"` (#1952).
pub(crate) fn check_numbers(p: &Value, keys: &[&str], cmd: &str) -> Result<()> {
for key in keys {
match p.get(*key) {
None | Some(Value::Null) | Some(Value::Number(_)) => {}
Some(v) => return Err(bad(cmd, format!("`{key}` must be a number, not {v}"))),
}
number_or(p, key, 0.0, cmd)?;
}
Ok(())
}
pub(crate) fn bool_or(p: &Value, key: &str, default: bool) -> bool {
p.get(key).and_then(Value::as_bool).unwrap_or(default)
}
/// The true or false `p` gives for `key`, or `default` when `p` leaves it out or gives null. Any
/// other value, such as `"false"`, is an error: "`{key}` must be true or false".
pub(crate) fn switch_or(p: &Value, key: &str, default: bool, cmd: &str) -> Result<bool> {
match p.get(key) {
None | Some(Value::Null) => Ok(default),
Some(v) => v.as_bool().ok_or_else(|| bad(cmd, format!("`{key}` must be true or false"))),
}
}
/// Error unless each of `keys` that `p` gives is true or false (or null), as [`switch_or`] reads
/// them.
pub(crate) fn check_switches(p: &Value, keys: &[&str], cmd: &str) -> Result<()> {
for key in keys {
switch_or(p, key, false, cmd)?;
}
Ok(())
}
pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> {
p.get(key).and_then(Value::as_str)
}
+23
View File
@@ -356,6 +356,11 @@ pub(crate) fn transforms_objects(p: &Value) -> bool {
bool_or(p, "objects", true)
}
/// The on/off params of the transform commands that [`apply_transform`], [`scaling`],
/// [`transform_patterns`] and [`transforms_objects`] read. The transform commands in this file
/// check them with [`check_switches`] before they change anything.
const TRANSFORM_SWITCHES: [&str; 6] = ["copy", "strokes", "corners", "patterns", "objects", "typeAreas"];
/// `xf` on `n` with Transform Objects (`objects`, see [`transforms_objects`]) on or off.
pub(crate) fn transform_node(n: &mut Node, xf: Affine, sc: Scaling, objects: bool) {
if objects {
@@ -505,6 +510,8 @@ fn resize_type_area(n: &mut Node, xf: Affine) -> bool {
}
fn transform(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &TRANSFORM_SWITCHES, "object.transform")?;
check_switches(p, &["anchors"], "object.transform")?;
let m = matrix_param(p, "matrix").ok_or_else(|| bad("object.transform", "missing matrix [a,b,c,d,e,f]"))?;
let (ids, anchors) = match ids_param(p, "ids") {
Some(v) => (outermost(&s.doc()?.doc, v), false),
@@ -515,12 +522,15 @@ fn transform(s: &mut Session, p: &Value) -> Result<Value> {
fn move_cmd(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["dx", "dy"], "object.move")?;
check_switches(p, &TRANSFORM_SWITCHES, "object.move")?;
let ids = selected_roots_to_transform(s)?;
apply_transform(s, "Move", ids, Affine::translate((f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0))), p, true)
}
fn nudge(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["dx", "dy"], "object.nudge")?;
check_switches(p, &TRANSFORM_SWITCHES, "object.nudge")?;
check_switches(p, &["big"], "object.nudge")?;
let k = s.prefs.keyboard_increment * if bool_or(p, "big", false) { 10.0 } else { 1.0 };
let dx = f64_or(p, "dx", 0.0) * k;
let dy = f64_or(p, "dy", 0.0) * k;
@@ -549,6 +559,8 @@ fn about(o: Point, a: Affine) -> Affine {
fn rotate(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["angle"], "object.rotate")?;
check_switches(p, &TRANSFORM_SWITCHES, "object.rotate")?;
check_switches(p, &["absolute"], "object.rotate")?;
let ids = selected_roots_to_transform(s)?;
let o = origin_of(s, p, &ids)?;
let mut angle = f64_or(p, "angle", 0.0);
@@ -562,6 +574,7 @@ fn rotate(s: &mut Session, p: &Value) -> Result<Value> {
fn scale(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["sy"], "object.scale")?;
check_switches(p, &TRANSFORM_SWITCHES, "object.scale")?;
let ids = selected_roots_to_transform(s)?;
let o = origin_of(s, p, &ids)?;
let sx = f64_req(p, "sx", "object.scale")? / 100.0;
@@ -573,6 +586,7 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
}
fn reflect(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &TRANSFORM_SWITCHES, "object.reflect")?;
let ids = selected_roots_to_transform(s)?;
let o = origin_of(s, p, &ids)?;
let m = match p.get("axis") {
@@ -593,6 +607,7 @@ fn reflect(s: &mut Session, p: &Value) -> Result<Value> {
fn shear(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["angle"], "object.shear")?;
check_switches(p, &TRANSFORM_SWITCHES, "object.shear")?;
let ids = selected_roots_to_transform(s)?;
let o = origin_of(s, p, &ids)?;
let t = f64_or(p, "angle", 0.0).clamp(-89.0, 89.0).to_radians().tan();
@@ -605,6 +620,7 @@ fn shear(s: &mut Session, p: &Value) -> Result<Value> {
/// that `p` sets to true or false (a recorded Transform Again's) takes precedence. A Transform
/// Each is repeated by running it again (its transform is per object, about each one's bounds).
fn transform_again(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &TRANSFORM_SWITCHES, "object.transformAgain")?;
if let Some(again) = s.doc()?.last_perspective.clone() {
return super::distortcmds::transform_again(s, &again);
}
@@ -1138,6 +1154,7 @@ fn data_param(m: &serde_json::Map<String, Value>) -> Result<Vec<(String, Option<
}
fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["visible", "locked", "isolate", "knockoutShape"], "object.setProps")?;
let ids = targets(s, p)?;
let opacity = p.get("opacity").and_then(Value::as_f64).map(percent);
let blend = match str_param(p, "blend") {
@@ -1389,6 +1406,7 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
let preview = preview_bounds(s, p, "object.align")?;
let (h, v) = direction(p, "object.align")?;
choice(p, "object.align", "to", &["selection", "artboard", "key"])?;
check_switches(p, &["patterns"], "object.align")?;
if h.is_none() && v.is_none() {
return Err(bad("object.align", "give horizontal or vertical"));
}
@@ -1607,6 +1625,7 @@ fn distribute_anchors(s: &mut Session, label: &str, horiz: bool, gap: Option<f64
}
fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "object.distribute")?;
let ids = selected_roots(s)?;
let preview = preview_bounds(s, p, "object.distribute")?;
let board = distribute_to(s, p, &ids, preview, "object.distribute")?;
@@ -1680,6 +1699,7 @@ fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
}
fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "object.distributeSpacing")?;
let ids = selected_roots(s)?;
let horiz = choice(p, "object.distributeSpacing", "axis", &["horizontal", "vertical"])? != Some("vertical");
let preview = preview_bounds(s, p, "object.distributeSpacing")?;
@@ -1768,6 +1788,8 @@ pub(crate) fn bounds_rewrite(s: &Session, cmd: &str, params: &Value) -> Option<(
fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64) -> Result<Bounds> {
check_numbers(p, &["x", "y", "width", "height"], "object.setBounds")?;
let st = s.doc()?;
check_switches(p, &TRANSFORM_SWITCHES, "object.setBounds")?;
check_switches(p, &["proportional"], "object.setBounds")?;
if transforms_objects(p)
&& !bool_or(p, "copy", false)
&& let Some(b) = effects::preview::anchor_bounds(&st.selection, &st.doc, s.prefs.use_preview_bounds)
@@ -2299,6 +2321,7 @@ fn corners_param(p: &Value) -> Result<Option<BTreeSet<usize>>> {
}
fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["invertPie", "makeSidesEqual"], "object.setLiveShape")?;
// `items`: objects each with its own `corners`; else `ids` (or the selection) sharing them.
let items = match p.get("items").filter(|v| !v.is_null()) {
None => None,
+23 -12
View File
@@ -55,7 +55,7 @@ pub fn specs() -> Vec<CommandSpec> {
"{fill?: bool (true: bring the Fill proxy forward, false: the Stroke proxy; omitted: swap which is in front)} → {fillActive}",
always,
|s, p| {
s.fill_active = p.get("fill").and_then(Value::as_bool).unwrap_or(!s.fill_active);
s.fill_active = switch_or(p, "fill", !s.fill_active, "paint.toggleActive")?;
Ok(json!({ "fillActive": s.fill_active }))
}
),
@@ -105,7 +105,7 @@ pub fn proxy_specs() -> Vec<CommandSpec> {
has_doc,
|s, p| {
let paint = Paint::solid(s.last_solid);
apply_to_proxy(s, p, paint)
apply_to_proxy(s, p, paint, "paint.lastColor")
}
),
cmd!(
@@ -117,7 +117,7 @@ pub fn proxy_specs() -> Vec<CommandSpec> {
has_doc,
|s, p| {
let paint = super::gradient::unplaced(&Paint::Gradient(Box::new(s.last_gradient.clone())));
apply_to_proxy(s, p, paint)
apply_to_proxy(s, p, paint, "paint.lastGradient")
}
),
cmd!(
@@ -287,8 +287,15 @@ fn proxies(s: &mut Session, _: &Value) -> Result<Value> {
}
/// `stroke` param, defaulting to the proxy that is in front.
fn stroke_param(s: &Session, p: &Value) -> bool {
p.get("stroke").and_then(Value::as_bool).unwrap_or(!s.fill_active)
fn stroke_param(s: &Session, p: &Value, cmd: &str) -> Result<bool> {
switch_or(p, "stroke", !s.fill_active, cmd)
}
/// Error unless `p` gives `keepModel` as true, false or null, or leaves it out ("`keepModel` must
/// be true or false"). [`paint_from`] calls it first, and each command whose params text lists
/// `keepModel` calls it before it changes anything.
pub(crate) fn check_keep_model(p: &Value, cmd: &str) -> Result<()> {
check_switches(p, &["keepModel"], cmd)
}
/// The model RGB colours given in `p` are stored in: the document's in a CMYK document (`None` in
@@ -319,11 +326,13 @@ pub(crate) fn new_color(s: &Session, p: &Value, c: Color) -> Color {
/// Parse a paint from params (color / none / swatch / gradient). None = no paint keys given. A
/// color or gradient (its stops and freeform points) given in RGB takes the document's color
/// model ([`new_color_model`]), converted through the color settings.
/// model ([`new_color_model`]), converted through the color settings. A `none` or `keepModel`
/// other than true, false or null is an error.
pub(crate) fn paint_from(s: &Session, p: &Value) -> Result<Option<Paint>> {
check_keep_model(p, "paint")?;
let model = new_color_model(s, p);
let new_color = |c: Color| stored_color(model, c);
if bool_or(p, "none", false) {
if switch_or(p, "none", false, "paint")? {
return Ok(Some(Paint::None));
}
if let Some(name) = str_param(p, "swatch") {
@@ -391,13 +400,14 @@ fn set_paint(s: &mut Session, p: &Value, fill: bool) -> Result<Value> {
if p.get("tint").is_some() && p.get("swatch").is_none() {
return Err(bad(cmd, "`tint` goes with a global or spot `swatch`"));
}
check_keep_model(p, cmd)?;
let paint = paint_from(s, p)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
apply_paint(s, p, paint, fill)
}
/// [`apply_paint`] on the proxy named by `stroke` (default: the active one), focusing it.
fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint) -> Result<Value> {
let fill = !stroke_param(s, p);
fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint, cmd: &str) -> Result<Value> {
let fill = !stroke_param(s, p, cmd)?;
apply_paint(s, p, paint, fill)
}
@@ -406,6 +416,7 @@ fn apply_to_proxy(s: &mut Session, p: &Value, paint: Paint) -> Result<Value> {
/// remember the paint.
fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<Value> {
let cmd = if fill { "paint.setFill" } else { "paint.setStroke" };
let focus = switch_or(p, "focus", true, cmd)?;
let span = spans_targets(p, &paint).map_err(|e| bad(cmd, e))?;
// Characters selected with the Type tool take the paint, not the whole type object (#1063).
if p.get("ids").is_none()
@@ -431,7 +442,7 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
super::typecmd::refresh_bounds(t);
Ok(super::appearance::count_changed(d, &before, &[id]))
})?;
if bool_or(p, "focus", true) {
if focus {
s.fill_active = fill;
}
s.remember_paint(&paint);
@@ -514,7 +525,7 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result<V
}
s.paint.stroke = unplaced(&paint);
}
if bool_or(p, "focus", true) {
if focus {
s.fill_active = fill;
}
s.remember_paint(&paint);
@@ -666,7 +677,7 @@ fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
/// default when there are none) and remember the first target's new paint. The Appearance panel's
/// active item of the proxy's kind is recoloured in each selected object's own stack instead.
fn proxy_recolor(s: &mut Session, p: &Value, cmd: &str, label: &str, f: &dyn Fn(Color) -> Color) -> Result<Value> {
let stroke = stroke_param(s, p);
let stroke = stroke_param(s, p, cmd)?;
let ids = match ids_param(p, "ids") {
Some(ids) => ids,
None => super::edit::selected_roots(s)?,
+7 -3
View File
@@ -158,7 +158,10 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
fn close(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.close", "missing id"))?;
let h_in = point_param(p, "in");
let h_in = match p.get("in").filter(|h| !h.is_null()) {
None => None,
Some(h) => Some(finite_numbers(h).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad("path.close", "`in` must be [x, y]"))?),
};
let independent = bool_or(p, "independent", false);
s.edit("Close Path", |d, sel| {
let path = path_mut(d, id)?;
@@ -178,7 +181,7 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
}
pub(crate) fn move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
let d = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let d = Vec2::new(number_or(p, "dx", 0.0, "path.moveAnchors")?, number_or(p, "dy", 0.0, "path.moveAnchors")?);
// Objects selected whole move as objects, and their pattern tiles move with them when
// `patterns` (default: General › Transform Pattern Tiles) is on.
let whole = s.doc()?.selection.whole();
@@ -725,6 +728,7 @@ fn open_run(mut anchors: Vec<vectorcraft_geom::Anchor>) -> Option<SubPath> {
fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.reshape", "missing id"))?;
let at = Point::new(f64_req(p, "x", "path.reshape")?, f64_req(p, "y", "path.reshape")?);
check_numbers(p, &["dx", "dy", "tol", "radius"], "path.reshape")?;
let delta = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
let tol = f64_or(p, "tol", 3.0).max(0.0);
let radius = p.get("radius").and_then(Value::as_f64);
@@ -773,7 +777,7 @@ fn insert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.insertAnchor", "missing id"))?;
let si = usize_opt(p, "subpath", "path.insertAnchor")?.unwrap_or(0);
let seg = p.get("segment").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.insertAnchor", "missing segment"))?;
let t = f64_or(p, "t", 0.5).clamp(0.0, 1.0);
let t = number_or(p, "t", 0.5, "path.insertAnchor")?.clamp(0.0, 1.0);
let idx = s.edit("Add Anchor Point", |d, _| {
let path = path_mut(d, id)?;
let sp = path.subpaths.get_mut(si).ok_or_else(|| EngineError::Other("no such subpath".into()))?;
+12 -8
View File
@@ -366,7 +366,7 @@ fn presets_import(s: &mut Session, p: &Value) -> Result<Value> {
let base = GridDefinition::default();
let check = |v: &Value| base.merged(v).map_err(|e| bad(C, format!("`{}`: {e}", v["name"].as_str().unwrap_or("?"))));
let presets = file.presets.iter().map(check).collect::<Result<Vec<_>>>()?;
let replace = bool_or(p, "replace", false);
let replace = switch_or(p, "replace", false, C)?;
let mut imported = vec![];
for mut preset in presets {
// A built-in view comes in as a saved copy ("2P-Normal View"…): the built-in one stays.
@@ -390,9 +390,13 @@ fn presets_import(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "imported": imported }))
}
/// Toggle (or set, with `on`) a view option of the grid.
fn view_option(s: &mut Session, p: &Value, field: fn(&mut PerspectiveGrid) -> &mut bool) -> Result<Value> {
let want = p.get("on").and_then(Value::as_bool);
/// Toggle (or set, with `on`) a view option of the grid for the command `cmd`.
fn view_option(s: &mut Session, p: &Value, cmd: &str, field: fn(&mut PerspectiveGrid) -> &mut bool) -> Result<Value> {
// A missing or null `on` toggles the option.
let want = match p.get("on") {
None | Some(Value::Null) => None,
Some(_) => Some(switch_or(p, "on", false, cmd)?),
};
let mut g = silent(s, |g| {
let f = field(g);
*f = want.unwrap_or(!*f);
@@ -401,19 +405,19 @@ fn view_option(s: &mut Session, p: &Value, field: fn(&mut PerspectiveGrid) -> &m
}
fn lock(s: &mut Session, p: &Value) -> Result<Value> {
view_option(s, p, |g| &mut g.locked)
view_option(s, p, "perspective.grid.lock", |g| &mut g.locked)
}
fn lock_station(s: &mut Session, p: &Value) -> Result<Value> {
view_option(s, p, |g| &mut g.lock_station)
view_option(s, p, "perspective.grid.lockStation", |g| &mut g.lock_station)
}
fn snap(s: &mut Session, p: &Value) -> Result<Value> {
view_option(s, p, |g| &mut g.snap)
view_option(s, p, "perspective.grid.snap", |g| &mut g.snap)
}
fn rulers(s: &mut Session, p: &Value) -> Result<Value> {
view_option(s, p, |g| &mut g.rulers)
view_option(s, p, "perspective.grid.rulers", |g| &mut g.rulers)
}
/// Snap to Grid for a move of `ids` from page point `from` to `to` (unless `p` says `snap: false`):
+7
View File
@@ -137,6 +137,7 @@ pub fn specs() -> Vec<CommandSpec> {
fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.new";
let mode = str_param(p, "mode");
super::paint::check_keep_model(p, C)?;
let paints = match (p.get("colors").and_then(Value::as_array), str_param(p, "pattern")) {
(Some(cs), _) => {
let stored = |c: Color| if mode.is_some() { c } else { super::paint::new_color(s, p, c) };
@@ -302,6 +303,7 @@ fn link_colors(d: &mut Document, ids: &[NodeId], links: &[(Color, String)]) -> u
fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.newGroup";
let mut names = str_list(p, "swatches");
super::paint::check_keep_model(p, C)?;
let colors: Vec<Color> = p
.get("colors")
.and_then(Value::as_array)
@@ -538,6 +540,10 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
{
return Err(bad(C, format!("`{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name)")));
}
super::paint::check_keep_model(p, C)?;
if let Some(v) = p.get("paint") {
super::paint::check_keep_model(v, C)?;
}
// A replacement paint of the swatch's kind (a dropped colour or edited gradient).
let paint = match p.get("paint").filter(|v| !v.is_null()) {
Some(v) => match (if given("mode") { paint_from(s, &keeping_model(v))? } else { paint_from(s, v)? }, &sw.paint) {
@@ -875,6 +881,7 @@ fn swatch_edit_group(s: &mut Session, p: &Value) -> Result<Value> {
.iter()
.map(|v| color_value(v).ok_or_else(|| bad(C, format!("bad colour {v}"))))
.collect::<Result<_>>()?;
super::paint::check_keep_model(p, C)?;
// A color its swatch already has keeps its model. The others take the document's.
let old: Vec<Option<Color>> =
s.doc()?.doc.swatch_groups.iter().find(|g| g.name == group).map(|g| g.swatches.iter().map(|w| w.paint.color()).collect()).unwrap_or_default();
+2
View File
@@ -619,6 +619,7 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
},
};
let mut change = CharChange::parse(p, C, &s.doc()?.doc.setup)?;
super::paint::check_keep_model(p, C)?;
change.store_colors(s, p);
if change.is_empty() {
return Err(bad(C, "nothing to change"));
@@ -839,6 +840,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
if !on_path && !path.is_closed() && path.bounds().is_none_or(|b| b.width() < 1.0 || b.height() < 1.0) {
return Err(bad(C, "area type needs a path that encloses an area"));
}
super::paint::check_keep_model(p, C)?;
let style = super::create::new_type_style(s, p);
let text = str_param(p, "text").unwrap_or("").to_string();
let start = match point_param(p, "at") {
+1
View File
@@ -691,6 +691,7 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
Some(j) => Some(justify_param(j).ok_or_else(|| bad(C, "justify must be auto|left|center|right|justifyAll"))?),
None => None,
};
super::paint::check_keep_model(p, C)?;
let fill = match p.get("fill") {
None | Some(Value::Null) => None,
Some(Value::String(n)) if n.eq_ignore_ascii_case("none") => Some(vectorcraft_color::Paint::None),