mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Commands: a boolean or number given as text is refused
Squashed from #3428 onto main.
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ¶ms)?;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -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`):
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user