mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 20:58:00 -06:00
Commands: Transform Each, artboards and more refuse a boolean or number given as text
object.transformEach refused reflectX, reflectY, random and copy given
as text, but read strokes, corners, objects and patterns given as text
as the key left out. With Scale Strokes & Effects on,
{"scaleH": 200, "scaleV": 200, "strokes": "false"} scaled the stroke,
recorded an undo step and kept the params for Transform Again, which
repeated the call. artboard.rearrange, artboard.move and
artboard.duplicate read their patterns option the same way, and
artboard.setProps reads its patterns, strokes and corners options the
same way.
These commands now refuse such a value with "`<key>` must be true or
false" before they change anything. So do the other options and
numbers of artboard.setProps and artboard.move, the art of Duplicate,
Copy and Cut Artboard, the shape commands and path.create, path.close,
path.setHandle, path.moveAnchors, the Pencil, Curvature, Reshape
Segment and Blob Brush options, text.create, text.createInPath,
text.setRangeStyle, text.editRange, text.getRange, text.touchType,
text.areaOptions, type.step, the font size steps,
type.opticalMarginAlignment, type.systemLayout, type.fractionalWidths,
type.ruby, the symbol and swatch commands, the Appearance panel's
commands, the gradient and freeform gradient commands, Shape Builder,
Live Paint, Image Trace, the opacity of object.setProps, the radius,
sides and relativeRadii of object.setLiveShape, the reference point of
object.setBounds and the artboard of Align, Distribute and Distribute
Spacing. They refuse a number given as text with "`<key>` must be a
number, not <value>", and a gradient stop's tint with "stop <n>:
`tint` must be a number". A null still reads as the key left out.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -372,8 +372,10 @@ pub(crate) fn count_changed(d: &vectorcraft_doc::Document, before: &vectorcraft_
|
||||
}
|
||||
|
||||
/// Whether an edit aimed at `item` changes a stroke: the `stroke` param, else the kind of the
|
||||
/// targeted item (on the first target object), else `default`.
|
||||
pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bool) -> Result<bool> {
|
||||
/// targeted item (on the first target object), else `default`. A `stroke` other than true, false or
|
||||
/// null is an error of command `cmd`.
|
||||
pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bool, cmd: &str) -> Result<bool> {
|
||||
check_switches(p, &["stroke"], cmd)?;
|
||||
if let Some(b) = p.get("stroke").and_then(Value::as_bool) {
|
||||
return Ok(b);
|
||||
}
|
||||
@@ -486,7 +488,7 @@ fn transfer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if source == target {
|
||||
return Err(bad(C, "`source` and `target` are the same object"));
|
||||
}
|
||||
let copy = bool_or(p, "copy", false);
|
||||
let copy = switch_or(p, "copy", false, C)?;
|
||||
// When both objects come out as they were, the document and its undo history stay as they are.
|
||||
let before = s.doc()?.doc.clone();
|
||||
super::overprint::edit_counted(s, if copy { "Copy Appearance" } else { "Move Appearance" }, |d| {
|
||||
@@ -549,6 +551,8 @@ 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")?;
|
||||
check_numbers(p, &["weight", "opacity"], "appearance.setItem")?;
|
||||
check_switches(p, &["visible"], "appearance.setItem")?;
|
||||
let paint = paint_from(s, p)?;
|
||||
let ids = appearance_targets(s, p)?;
|
||||
if ids.is_empty() {
|
||||
@@ -762,7 +766,7 @@ fn target_contents(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn copy_from(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "appearance.copyFrom";
|
||||
let clicked = id_param(p, "source").ok_or_else(|| bad(C, "missing `source` id"))?;
|
||||
let (reverse, append) = (bool_or(p, "reverse", false), bool_or(p, "append", false));
|
||||
let (reverse, append) = (switch_or(p, "reverse", false, C)?, switch_or(p, "append", false, C)?);
|
||||
let attrs = s.prefs.eyedropper.attrs(p).map_err(|e| bad(C, e))?;
|
||||
let range = if !reverse && !append && p.get("ids").is_none() { super::textedit::editing_range(s)?.filter(|(_, a, b)| a != b) } else { None };
|
||||
let selected = match ids_param(p, "ids") {
|
||||
|
||||
@@ -970,7 +970,7 @@ fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let name = resolve_symbol(d, p, "symbol.place")?;
|
||||
let centre = d.artboards.first().map(|a| a.rect.center()).unwrap_or(Point::ZERO);
|
||||
let at = Point::new(f64_or(p, "x", centre.x), f64_or(p, "y", centre.y));
|
||||
let at = Point::new(number_or(p, "x", centre.x, "symbol.place")?, number_or(p, "y", centre.y, "symbol.place")?);
|
||||
let size = natural_size(d, &name);
|
||||
let reg = d.symbol_registration(&name);
|
||||
let parent = s.doc()?.target_parent()?;
|
||||
@@ -1099,7 +1099,7 @@ fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !s.doc()?.doc.symbols.iter().any(|x| x.name == name) {
|
||||
return Err(bad(C, format!("no symbol named `{name}`")));
|
||||
}
|
||||
let expand = bool_or(p, "expandInstances", true);
|
||||
let expand = switch_or(p, "expandInstances", true, C)?;
|
||||
s.edit("Delete Symbol", |d, _| {
|
||||
let inst = instances_of(d, Some(&name));
|
||||
if expand {
|
||||
@@ -1216,10 +1216,10 @@ fn symbol_spray(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pts = points(p, C)?;
|
||||
let st = s.doc()?;
|
||||
let name = resolve_symbol(&st.doc, p, C)?;
|
||||
let radius = f64_or(p, "radius", 40.0).clamp(1.0, 2000.0);
|
||||
let density = f64_or(p, "density", 5.0).clamp(1.0, 10.0);
|
||||
let scale = f64_or(p, "scale", 1.0).clamp(0.01, 100.0);
|
||||
let alt = bool_or(p, "alt", false);
|
||||
let radius = number_or(p, "radius", 40.0, C)?.clamp(1.0, 2000.0);
|
||||
let density = number_or(p, "density", 5.0, C)?.clamp(1.0, 10.0);
|
||||
let scale = number_or(p, "scale", 1.0, C)?.clamp(0.01, 100.0);
|
||||
let alt = switch_or(p, "alt", false, C)?;
|
||||
let size = natural_size(&st.doc, &name);
|
||||
let set = st.selection.objects.first().copied().filter(|id| st.selection.len() == 1 && st.doc.node(*id).is_some_and(is_symbol_set));
|
||||
// Only a new Symbol Set needs a layer that takes new art.
|
||||
@@ -1303,9 +1303,9 @@ fn symbol_adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
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;
|
||||
let alt = bool_or(p, "alt", false);
|
||||
let radius = number_or(p, "radius", 40.0, C)?.clamp(1.0, 2000.0);
|
||||
let intensity = number_or(p, "intensity", 5.0, C)?.clamp(1.0, 10.0) / 10.0;
|
||||
let alt = switch_or(p, "alt", false, C)?;
|
||||
let st = s.doc()?;
|
||||
let color = match p.get("color").and_then(color_value) {
|
||||
Some(c) => super::paint::new_color(s, p, c),
|
||||
|
||||
@@ -248,10 +248,11 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if pts.is_empty() || !pts.iter().all(|q| finite(*q)) {
|
||||
return Err(bad(C, "points must be a non-empty list of finite [x,y]"));
|
||||
}
|
||||
let erase = bool_or(p, "erase", false);
|
||||
let erase = switch_or(p, "erase", false, C)?;
|
||||
check_numbers(p, &["tolerance", "gap"], C)?;
|
||||
// Line pieces and edges are only deleted (erase mode), never merged.
|
||||
let piece_tol = erase.then(|| f64_or(p, "tolerance", 2.0).clamp(0.0, 1000.0));
|
||||
let open_filled_as_closed = bool_or(p, "openFilledAsClosed", true);
|
||||
let open_filled_as_closed = switch_or(p, "openFilledAsClosed", true, C)?;
|
||||
let swatches = match p.get("pickColorFrom") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(v) => match v.as_str() {
|
||||
@@ -382,9 +383,11 @@ fn replace_roots(d: &mut Document, roots: &[NodeId], nodes: Vec<Node>) -> Result
|
||||
}
|
||||
|
||||
fn sb_regions(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let open_filled_as_closed = switch_or(p, "openFilledAsClosed", true, "shapeBuilder.regions")?;
|
||||
check_numbers(p, &["gap"], "shapeBuilder.regions")?;
|
||||
let roots = root_ids(s, p)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let (map, leaves) = BuilderMap::with_gap(d, &roots, true, bool_or(p, "openFilledAsClosed", true), gap_param(p));
|
||||
let (map, leaves) = BuilderMap::with_gap(d, &roots, true, open_filled_as_closed, gap_param(p));
|
||||
let id_of = |i: usize| leaves.get(i).map(|n| n.id.0);
|
||||
let regions: Vec<Value> = map
|
||||
.regions
|
||||
@@ -714,7 +717,7 @@ fn lp_gap_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(v) => Some(super::layer::layer_color_param(v).map_err(|e| bad(C, e))?.rgb()),
|
||||
None => None,
|
||||
};
|
||||
let close = bool_or(p, "closeGaps", false);
|
||||
let close = switch_or(p, "closeGaps", false, C)?;
|
||||
let targets = live_targets(s, p)?;
|
||||
let (faces, gaps, closed) = s.edit("Gap Options", |d, sel| {
|
||||
let (mut faces, mut gaps, mut closed) = (0, vec![], 0);
|
||||
@@ -801,11 +804,11 @@ fn lp_stroke_edge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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));
|
||||
let width = number_or(p, "width", s.paint.stroke_width.max(0.0), C)?;
|
||||
if !(0.0..=1000.0).contains(&width) {
|
||||
return Err(bad(C, "width out of range"));
|
||||
}
|
||||
let tol = f64_or(p, "tolerance", 4.0).clamp(0.0, 1000.0);
|
||||
let tol = number_or(p, "tolerance", 4.0, C)?.clamp(0.0, 1000.0);
|
||||
let edge = s.edit("Live Paint Bucket", |d, _| {
|
||||
let n = d.node(g).filter(|n| is_live_paint(n)).ok_or_else(|| bad(C, format!("{g} is not a Live Paint group")))?;
|
||||
let eid = b::visible_edge_near(n, pt, tol).map(|e| e.id).ok_or_else(|| EngineError::Other("no Live Paint edge near that point".into()))?;
|
||||
@@ -863,7 +866,7 @@ fn view_param(cmd: &str, p: &Value) -> Result<Option<TraceView>> {
|
||||
|
||||
fn trace_params(p: &Value) -> Result<tr::TraceParams> {
|
||||
let name = str_param(p, "preset").unwrap_or("Default");
|
||||
let enhanced = p.get("enhanced").and_then(Value::as_bool).unwrap_or(false);
|
||||
let enhanced = switch_or(p, "enhanced", false, "imageTrace.make")?;
|
||||
// (A preset without an enhanced version is traced as it is.)
|
||||
let base = enhanced.then(|| tr::enhanced_preset(name)).flatten().or_else(|| tr::preset(name));
|
||||
let base = base.ok_or_else(|| bad("imageTrace.make", format!("unknown preset `{name}` (see imageTrace.presets)")))?;
|
||||
|
||||
@@ -207,6 +207,7 @@ fn rect_transform(r: Rect, p: &Value, cmd: &str) -> Result<Affine> {
|
||||
}
|
||||
|
||||
fn rectangle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_numbers(p, &["radius"], "shape.rectangle")?;
|
||||
let r = rect_of(p, "shape.rectangle")?;
|
||||
let radius = f64_or(p, "radius", 0.0).max(0.0);
|
||||
let live = LiveShape::Rectangle {
|
||||
@@ -230,6 +231,7 @@ fn ellipse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn polygon(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = Point::new(f64_req(p, "cx", "shape.polygon")?, f64_req(p, "cy", "shape.polygon")?);
|
||||
let r = f64_req(p, "radius", "shape.polygon")?.abs();
|
||||
check_numbers(p, &["sides", "rotation"], "shape.polygon")?;
|
||||
let sides = p.get("sides").and_then(Value::as_u64).unwrap_or(6).clamp(3, 1000) as u32;
|
||||
let rot = f64_or(p, "rotation", 0.0);
|
||||
let xf = Affine::translate(c.to_vec2()) * Affine::rotate(rot.to_radians());
|
||||
@@ -240,6 +242,7 @@ fn polygon(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn star(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = Point::new(f64_req(p, "cx", "shape.star")?, f64_req(p, "cy", "shape.star")?);
|
||||
let r1 = f64_req(p, "radius1", "shape.star")?.abs();
|
||||
check_numbers(p, &["radius2", "points", "rotation"], "shape.star")?;
|
||||
let r2 = f64_or(p, "radius2", r1 / 2.0).abs();
|
||||
let n = p.get("points").and_then(Value::as_u64).unwrap_or(5).clamp(2, 1000) as u32;
|
||||
let path = shapes::star(c, r1, r2, n, f64_or(p, "rotation", 0.0));
|
||||
@@ -251,6 +254,24 @@ fn star(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use vectorcraft_color::{BlendMode, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
|
||||
let c = Point::new(f64_req(p, "cx", "shape.flare")?, f64_req(p, "cy", "shape.flare")?);
|
||||
let numbers = [
|
||||
"diameter",
|
||||
"opacity",
|
||||
"brightness",
|
||||
"growth",
|
||||
"fuzziness",
|
||||
"rays",
|
||||
"longest",
|
||||
"rayFuzziness",
|
||||
"rings",
|
||||
"largest",
|
||||
"x2",
|
||||
"y2",
|
||||
"direction",
|
||||
"pathLength",
|
||||
"seed",
|
||||
];
|
||||
check_numbers(p, &numbers, "shape.flare")?;
|
||||
let pct = |k: &str, d: f64| (f64_or(p, k, d) / 100.0).clamp(0.0, 10.0);
|
||||
let r = (f64_or(p, "diameter", 100.0) / 2.0).max(0.5);
|
||||
let opacity = pct("opacity", 50.0).min(1.0) as f32;
|
||||
@@ -360,12 +381,13 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn spiral(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = Point::new(f64_req(p, "cx", "shape.spiral")?, f64_req(p, "cy", "shape.spiral")?);
|
||||
check_numbers(p, &["segments"], "shape.spiral")?;
|
||||
let path = shapes::spiral(
|
||||
c,
|
||||
f64_req(p, "radius", "shape.spiral")?,
|
||||
f64_or(p, "decay", 80.0),
|
||||
number_or(p, "decay", 80.0, "shape.spiral")?,
|
||||
p.get("segments").and_then(Value::as_u64).unwrap_or(10).clamp(2, 1000) as u32,
|
||||
bool_or(p, "clockwise", true),
|
||||
switch_or(p, "clockwise", true, "shape.spiral")?,
|
||||
);
|
||||
add_art(s, "Spiral", path_kind(path, None), None)
|
||||
}
|
||||
@@ -373,7 +395,7 @@ fn spiral(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn arc(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = Point::new(f64_req(p, "x1", "shape.arc")?, f64_req(p, "y1", "shape.arc")?);
|
||||
let b = Point::new(f64_req(p, "x2", "shape.arc")?, f64_req(p, "y2", "shape.arc")?);
|
||||
add_art(s, "Arc", path_kind(shapes::arc(a, b, 0.0, bool_or(p, "closed", false)), None), None)
|
||||
add_art(s, "Arc", path_kind(shapes::arc(a, b, 0.0, switch_or(p, "closed", false, "shape.arc")?), None), None)
|
||||
}
|
||||
|
||||
/// The grid `paths` (bottom first) as one selected group in the new-art stroke, filled with the
|
||||
@@ -480,7 +502,7 @@ fn path_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if anchors.is_empty() {
|
||||
return Err(bad("path.create", "need at least one anchor"));
|
||||
}
|
||||
PathData::single(SubPath::new(anchors, bool_or(p, "closed", false)))
|
||||
PathData::single(SubPath::new(anchors, switch_or(p, "closed", false, "path.create")?))
|
||||
};
|
||||
let mut look = s.new_art_look(s.paint.fill.clone(), s.paint.stroke.clone(), s.paint.stroke_width);
|
||||
// Open paths drawn with a stroke of None would be invisible: mimic Illustrator and keep what the user set.
|
||||
@@ -495,6 +517,11 @@ 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")?;
|
||||
check_numbers(p, &["size"], "text.create")?;
|
||||
check_switches(p, &["vertical", "placeholder"], "text.create")?;
|
||||
if let Some(a) = p.get("area") {
|
||||
check_numbers(a, &["width", "height"], "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);
|
||||
|
||||
@@ -665,9 +665,10 @@ 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 = number_or(p, "fidelity", 1.5, C)?.clamp(0.05, 100.0);
|
||||
let closed = bool_or(p, "closed", false);
|
||||
let closed = switch_or(p, "closed", false, C)?;
|
||||
let brush = str_param(p, "style") == Some("brush");
|
||||
let keep_selected = bool_or(p, "keepSelected", true);
|
||||
let keep_selected = switch_or(p, "keepSelected", true, C)?;
|
||||
let filled = switch_or(p, "fill", false, C)?;
|
||||
if let Some(ext) = p.get("extend").filter(|v| !v.is_null()) {
|
||||
let id = id_param(ext, "id").ok_or_else(|| bad(C, "extend needs id"))?;
|
||||
let at_start = str_param(ext, "end") == Some("start");
|
||||
@@ -764,7 +765,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, "need at least two points"));
|
||||
}
|
||||
let sp = fit_freehand(&pts, tol, closed);
|
||||
let fill = if bool_or(p, "fill", false) { s.paint.fill.clone() } else { Paint::None };
|
||||
let fill = if filled { s.paint.fill.clone() } else { Paint::None };
|
||||
// A pencil path with a fill keeps a stroke of None (#1679); only one that would show nothing
|
||||
// gets black. A brush is its stroke.
|
||||
let stroke = if brush || fill.is_none() { stroke_paint(s) } else { s.paint.stroke.clone() };
|
||||
@@ -780,12 +781,11 @@ 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().map(|v| Ok((point_entry(v, C)?, v.get("corner").and_then(Value::as_bool).unwrap_or(false)))).collect::<Result<_>>()?;
|
||||
let pts: Vec<(Point, bool)> = arr.iter().map(|v| Ok((point_entry(v, C)?, switch_or(v, "corner", false, C)?))).collect::<Result<_>>()?;
|
||||
if pts.is_empty() {
|
||||
return Err(bad(C, "need at least one point"));
|
||||
}
|
||||
let sp = vectorcraft_tools::draw2::curvature_spline(&pts, bool_or(p, "closed", false));
|
||||
let sp = vectorcraft_tools::draw2::curvature_spline(&pts, switch_or(p, "closed", false, C)?);
|
||||
if let Some(id) = id_param(p, "id") {
|
||||
s.edit("Curvature", |d, sel| {
|
||||
*path_mut(d, id)? = PathData::single(sp);
|
||||
@@ -831,7 +831,7 @@ fn curvature_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => EndCurve::Keep,
|
||||
Some(f) => EndCurve::parse(f).ok_or_else(|| bad(C, format!("unknown from {f:?}")))?,
|
||||
};
|
||||
let corner = bool_or(p, "corner", false);
|
||||
let corner = switch_or(p, "corner", false, C)?;
|
||||
let op = match str_param(p, "op") {
|
||||
Some("move") => CurvatureOp::Move(usize_req(p, "anchor", C)?, to()?),
|
||||
Some("insert") => CurvatureOp::Insert(usize_req(p, "segment", C)?, f64_or(p, "t", 0.5), at),
|
||||
@@ -1030,7 +1030,7 @@ fn reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !(dv.x.is_finite() && dv.y.is_finite() && dv.hypot() < MAX_INPUT) {
|
||||
return Err(bad(C, "invalid delta"));
|
||||
}
|
||||
let perpendicular = bool_or(p, "perpendicular", false);
|
||||
let perpendicular = switch_or(p, "perpendicular", false, C)?;
|
||||
s.edit("Reshape", |d, _| {
|
||||
let path = path_mut(d, id)?;
|
||||
let sp = path.subpaths.get_mut(si).ok_or_else(|| EngineError::Other("no such subpath".into()))?;
|
||||
@@ -1490,11 +1490,12 @@ fn blob(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
Paint::solid(Color::BLACK)
|
||||
};
|
||||
let keep_selected = bool_or(p, "keepSelected", false);
|
||||
let only_selection = bool_or(p, "mergeOnlySelection", false);
|
||||
let keep_selected = switch_or(p, "keepSelected", false, C)?;
|
||||
let only_selection = switch_or(p, "mergeOnlySelection", false, C)?;
|
||||
let merge = switch_or(p, "merge", true, C)?;
|
||||
let st = s.doc()?;
|
||||
let mut merge_ids = vec![];
|
||||
if bool_or(p, "merge", true) && !(only_selection && st.selection.is_empty()) {
|
||||
if merge && !(only_selection && st.selection.is_empty()) {
|
||||
let all = Selection::default();
|
||||
let from = if only_selection { &st.selection } else { &all };
|
||||
for id in target_paths(&st.doc, from, poly_bounds(&pts, size)) {
|
||||
|
||||
@@ -176,7 +176,7 @@ pub(crate) fn parse_mode(m: &str) -> std::result::Result<FreeformMode, String> {
|
||||
/// automatic points it shows first, and the stops follow the points' colours.
|
||||
fn edit<R>(s: &mut Session, p: &Value, cmd: &str, mut f: impl FnMut(&mut Freeform, Space) -> std::result::Result<R, String>) -> Result<R> {
|
||||
let item = item_target(s, p, cmd)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active, cmd)?;
|
||||
let item = item.of_kind(s, !stroke);
|
||||
let ids = item.targets(s, p)?;
|
||||
if ids.is_empty() {
|
||||
@@ -308,7 +308,7 @@ fn split_line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "paint.freeform.splitLine";
|
||||
let line = index_param(p, "line", C)?;
|
||||
let segment = index_param(p, "segment", C)?;
|
||||
let t = f64_or(p, "t", 0.5);
|
||||
let t = number_or(p, "t", 0.5, C)?;
|
||||
let index = edit(s, p, C, |f, _| f.split_line(line, segment, t).ok_or_else(|| format!("no segment {segment} on line {line}")))?;
|
||||
s.select_freeform_point(Some(index));
|
||||
Ok(json!({ "index": index }))
|
||||
@@ -332,7 +332,7 @@ fn select_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "paint.freeform.get";
|
||||
let stroke = bool_or(p, "stroke", !s.fill_active);
|
||||
let stroke = switch_or(p, "stroke", !s.fill_active, C)?;
|
||||
let (f, _) = s.proxy_freeform(stroke).ok_or_else(|| bad(C, NOT_FREEFORM))?;
|
||||
let selected = if stroke != s.fill_active { s.selected_freeform_point().filter(|i| *i < f.points.len()) } else { None };
|
||||
let mut v = vectorcraft_tools::params::freeform_json(&f);
|
||||
|
||||
@@ -117,7 +117,12 @@ pub(crate) fn link_stops(d: &Document, v: &Value) -> Parsed<Value> {
|
||||
if d.swatch(&name).is_none() && st.get("color").is_some() {
|
||||
continue;
|
||||
}
|
||||
let tint = st.get("tint").and_then(Value::as_f64).map(|t| (t / 100.0).clamp(0.0, 1.0) as f32);
|
||||
let tint = st
|
||||
.get("tint")
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|v| v.as_f64().ok_or_else(|| format!("stop {i}: `{k}` must be a number", k = "tint")))
|
||||
.transpose()?;
|
||||
let tint = tint.map(|t| (t / 100.0).clamp(0.0, 1.0) as f32);
|
||||
let Paint::Solid { color, swatch, tint } = swatch_solid(d, &name, tint).map_err(|e| format!("stop {i}: {e}"))? else {
|
||||
continue;
|
||||
};
|
||||
@@ -372,7 +377,9 @@ pub(crate) fn apply_gradient_edit_in(
|
||||
fn edit_gradient(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "paint.editGradient";
|
||||
let item = item_target(s, p, C)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active, C)?;
|
||||
check_switches(p, &["reverse"], C)?;
|
||||
check_numbers(p, &["angle"], C)?;
|
||||
let item = item.of_kind(s, !stroke);
|
||||
let ids = item.targets(s, p)?;
|
||||
// How the gradient lies on a stroke (the panel's Stroke buttons).
|
||||
@@ -554,7 +561,7 @@ fn set_gradient_geom(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let p = &p;
|
||||
let item = item_target(s, p, C)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active)?;
|
||||
let stroke = edits_stroke(s, p, item, !s.fill_active, C)?;
|
||||
let item = item.of_kind(s, !stroke);
|
||||
let targets = match item {
|
||||
ItemTarget::Top => leaf_targets(s, &ids_param(p, "ids").map_or_else(|| selected_roots(s), |ids| Ok(outermost(&s.doc()?.doc, ids)))?)?,
|
||||
|
||||
@@ -870,6 +870,11 @@ fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => vec![p],
|
||||
};
|
||||
check_switches(p, &["moveArt", "scaleArt", "strokes", "corners", "patterns", "lockedAndHidden"], "artboard.setProps")?;
|
||||
for q in &entries {
|
||||
check_switches(q, &["locked"], "artboard.setProps")?;
|
||||
check_numbers(q, &["x", "y", "width", "height"], "artboard.setProps")?;
|
||||
}
|
||||
// Scale Artwork with Artboard: resized, an artboard takes the art fully inside it (as Move
|
||||
// Artwork with Artboard gathers it) and its guides from the old rectangle onto the new one.
|
||||
let scale_art = bool_or(p, "scaleArt", false);
|
||||
|
||||
@@ -353,6 +353,7 @@ fn transform_each_as(s: &mut Session, p: &Value, label: &str) -> Result<Value> {
|
||||
let random = switch_or(p, "random", false, "object.transformEach")?;
|
||||
let refi = whole_or(p, "reference", 4, "object.transformEach")?.min(8) as usize;
|
||||
let copy = switch_or(p, "copy", false, "object.transformEach")?;
|
||||
check_switches(p, &["strokes", "corners", "objects", "patterns"], "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"));
|
||||
@@ -1018,6 +1019,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 = switch_or(p, "moveArtwork", true, CMD)?;
|
||||
check_switches(p, &["patterns"], 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();
|
||||
|
||||
@@ -1155,6 +1155,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")?;
|
||||
check_numbers(p, &["opacity"], "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") {
|
||||
@@ -1424,6 +1425,7 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (h, v) = direction(p, "object.align")?;
|
||||
choice(p, "object.align", "to", &["selection", "artboard", "key"])?;
|
||||
check_switches(p, &["patterns"], "object.align")?;
|
||||
check_numbers(p, &["artboard"], "object.align")?;
|
||||
if h.is_none() && v.is_none() {
|
||||
return Err(bad("object.align", "give horizontal or vertical"));
|
||||
}
|
||||
@@ -1651,6 +1653,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")?;
|
||||
check_numbers(p, &["artboard"], "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")?;
|
||||
@@ -1725,6 +1728,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")?;
|
||||
check_numbers(p, &["artboard"], "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")?;
|
||||
@@ -1811,7 +1815,7 @@ pub(crate) fn bounds_rewrite(s: &Session, cmd: &str, params: &Value) -> Option<(
|
||||
/// How `object.setBounds` with params `p` changes the objects `ids`. A scale that would leave the
|
||||
/// art with a width or height below `least` of the one it has keeps `least` of it.
|
||||
fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64) -> Result<Bounds> {
|
||||
check_numbers(p, &["x", "y", "width", "height"], "object.setBounds")?;
|
||||
check_numbers(p, &["x", "y", "width", "height", "reference"], "object.setBounds")?;
|
||||
let st = s.doc()?;
|
||||
check_switches(p, &TRANSFORM_SWITCHES, "object.setBounds")?;
|
||||
check_switches(p, &["proportional"], "object.setBounds")?;
|
||||
@@ -2346,7 +2350,8 @@ 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")?;
|
||||
check_switches(p, &["invertPie", "makeSidesEqual", "relativeRadii"], "object.setLiveShape")?;
|
||||
check_numbers(p, &["radius", "sides"], "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,
|
||||
|
||||
@@ -351,6 +351,7 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result<Option<Paint>> {
|
||||
};
|
||||
}
|
||||
if let Some(g) = p.get("gradient") {
|
||||
check_numbers(g, &["angle"], "paint")?;
|
||||
// A stop naming a process swatch keeps the swatch's model.
|
||||
let stored = super::gradient::stop_colors(s, p, g);
|
||||
let g = super::gradient::link_stops(&s.doc()?.doc, g).map_err(|e| bad("paint", e))?;
|
||||
|
||||
@@ -72,6 +72,7 @@ fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// copy commands: by default the artboards the Artboard tool has selected (its active one when none
|
||||
/// of them is in the document) and its Move/Copy Artwork with Artboard option.
|
||||
fn boards_and_art(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool)> {
|
||||
check_switches(p, &["art"], cmd)?;
|
||||
let t = s.tool_options_of("artboard");
|
||||
let d = &s.doc()?.doc;
|
||||
let indices = if p.get("index").is_some() || p.get("indices").is_some() {
|
||||
@@ -159,6 +160,7 @@ pub(crate) fn beside_artboards(d: &vectorcraft_doc::Document, rect: Rect) -> Vec
|
||||
/// Duplicate Artboards: a copy of each artboard with its art, in one undo step. The copies keep
|
||||
/// their places relative to each other, together right of the last artboard.
|
||||
fn artboard_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_switches(p, &["patterns"], "artboard.duplicate")?;
|
||||
let (indices, art) = boards_and_art(s, p, "artboard.duplicate")?;
|
||||
let arts = artboards_art(s, &indices, art, false)?;
|
||||
let all: Vec<NodeId> = arts.iter().flat_map(|(_, a)| a.iter().copied()).collect();
|
||||
|
||||
@@ -162,7 +162,7 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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);
|
||||
let independent = switch_or(p, "independent", false, "path.close")?;
|
||||
s.edit("Close Path", |d, sel| {
|
||||
let path = path_mut(d, id)?;
|
||||
let sp = path.subpaths.iter_mut().rev().find(|s| !s.closed).ok_or_else(|| EngineError::Other("path is already closed".into()))?;
|
||||
@@ -182,6 +182,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(number_or(p, "dx", 0.0, "path.moveAnchors")?, number_or(p, "dy", 0.0, "path.moveAnchors")?);
|
||||
check_switches(p, &["patterns"], "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();
|
||||
@@ -232,7 +233,7 @@ fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.setHandle", "missing anchor"))?;
|
||||
let out = str_param(p, "which") != Some("in");
|
||||
let pos = Point::new(f64_req(p, "x", "path.setHandle")?, f64_req(p, "y", "path.setHandle")?);
|
||||
let independent = bool_or(p, "independent", false);
|
||||
let independent = switch_or(p, "independent", false, "path.setHandle")?;
|
||||
s.edit("Reshape", |d, _| {
|
||||
let path = path_mut(d, id)?;
|
||||
let a = path.anchor_mut(si, ai).ok_or_else(|| EngineError::Other("no such anchor".into()))?;
|
||||
|
||||
@@ -138,6 +138,7 @@ 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)?;
|
||||
check_switches(p, &["spot", "global"], 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) };
|
||||
@@ -229,6 +230,7 @@ pub(super) fn names_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.delete";
|
||||
let names = names_param(p, C)?;
|
||||
check_switches(p, &["unlink"], C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
// Global swatches going away (named ones and those in named groups): their links are dropped.
|
||||
let mut global: Vec<String> = vec![];
|
||||
@@ -304,6 +306,7 @@ 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)?;
|
||||
check_switches(p, &["fromArtwork", "toGlobal", "includeTints"], C)?;
|
||||
let colors: Vec<Color> = p
|
||||
.get("colors")
|
||||
.and_then(Value::as_array)
|
||||
@@ -365,6 +368,7 @@ fn insert_before<T>(list: &mut Vec<T>, items: Vec<T>, anchor: Option<&str>, name
|
||||
|
||||
fn swatch_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.move";
|
||||
check_numbers(p, &["to"], C)?;
|
||||
let names = names_param(p, C)?;
|
||||
let to = p.get("to").and_then(Value::as_u64).map(index);
|
||||
let group = str_param(p, "group").map(str::to_string);
|
||||
@@ -530,6 +534,7 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if sw.is_reserved() {
|
||||
return Err(bad(C, format!("`{name}` can't be edited")));
|
||||
}
|
||||
check_switches(p, &["spot", "global"], C)?;
|
||||
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
|
||||
let solid = sw.paint.color();
|
||||
if solid.is_none() && (given("color") || given("mode") || bool_or(p, "spot", false)) {
|
||||
@@ -649,8 +654,8 @@ fn swatch_sort(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
fn swatch_add_used(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.addUsedColors";
|
||||
let global = bool_or(p, "global", false);
|
||||
let only_selection = bool_or(p, "selection", false);
|
||||
let global = switch_or(p, "global", false, C)?;
|
||||
let only_selection = switch_or(p, "selection", false, C)?;
|
||||
let (added, linked) = s.edit("Add Used Colors", |d, sel| {
|
||||
let ids: Vec<NodeId> = if only_selection { sel.objects.clone() } else { d.layers.iter().map(|l| l.id).collect() };
|
||||
if only_selection && ids.is_empty() {
|
||||
|
||||
@@ -83,7 +83,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Some("Cmd+Shift+."),
|
||||
"{by?: steps (1)} type.step {attribute: size}",
|
||||
has_selection,
|
||||
|s, p| type_step(s, &json!({"attribute": "size", "by": f64_or(p, "by", 1.0)}))
|
||||
|s, p| type_step(s, &json!({"attribute": "size", "by": number_or(p, "by", 1.0, "type.size.increase")?}))
|
||||
),
|
||||
cmd!(
|
||||
"type.size.decrease",
|
||||
@@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Some("Cmd+Shift+,"),
|
||||
"{by?: steps (1)} type.step {attribute: size, by: -by}",
|
||||
has_selection,
|
||||
|s, p| type_step(s, &json!({"attribute": "size", "by": -f64_or(p, "by", 1.0)}))
|
||||
|s, p| type_step(s, &json!({"attribute": "size", "by": -number_or(p, "by", 1.0, "type.size.decrease")?}))
|
||||
),
|
||||
cmd!(
|
||||
"type.size.increaseByFive",
|
||||
@@ -179,10 +179,13 @@ fn byte_param(p: &Value, k: &str) -> Option<usize> {
|
||||
p.get(k).and_then(Value::as_u64).map(index)
|
||||
}
|
||||
|
||||
fn range_of(p: &Value, len: usize) -> (usize, usize) {
|
||||
/// The bytes `start..end` of `p` in text of `len` bytes, clamped and ordered (without them, all of
|
||||
/// it). A `start` or `end` that is not a number or null, such as `"2"`, is an error.
|
||||
fn range_of(p: &Value, len: usize, cmd: &str) -> Result<(usize, usize)> {
|
||||
check_numbers(p, &["start", "end"], cmd)?;
|
||||
let a = byte_param(p, "start").unwrap_or(0).min(len);
|
||||
let b = byte_param(p, "end").unwrap_or(len).min(len);
|
||||
(a.min(b), a.max(b))
|
||||
Ok((a.min(b), a.max(b)))
|
||||
}
|
||||
|
||||
fn edit_range(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -190,7 +193,7 @@ fn edit_range(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
let t = text_ref(s, id)?;
|
||||
let len = edit::runs_len(&t.runs);
|
||||
let (a, b) = range_of(p, len);
|
||||
let (a, b) = range_of(p, len, C)?;
|
||||
let styled: Option<Vec<TextRun>> = match p.get("runs") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(serde_json::from_value(v.clone()).map_err(|e| bad(C, format!("bad `runs`: {e}")))?),
|
||||
@@ -221,9 +224,9 @@ fn get_range(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("text.getRange", "missing `id`"))?;
|
||||
let t = text_ref(s, id)?;
|
||||
let len = edit::runs_len(&t.runs);
|
||||
let (a, b) = range_of(p, len);
|
||||
let (a, b) = range_of(p, len, "text.getRange")?;
|
||||
let mut runs = edit::slice_runs(&t.runs, a, b);
|
||||
if p.get("effective").and_then(Value::as_bool).unwrap_or(false) {
|
||||
if switch_or(p, "effective", false, "text.getRange")? {
|
||||
for run in &mut runs {
|
||||
run.style = t.effective_char_style(&run.style).ok_or_else(|| bad("text.getRange", "text transform is collapsed or unrepresentable"))?;
|
||||
}
|
||||
@@ -398,6 +401,8 @@ fn auto_or_num(p: &Value, k: &str, cmd: &str) -> Result<Option<Option<f64>>> {
|
||||
impl CharChange {
|
||||
/// `setup` gives the superscript, subscript and small caps proportions.
|
||||
pub(crate) fn parse(p: &Value, cmd: &str, setup: &vectorcraft_doc::DocSetup) -> Result<Self> {
|
||||
check_numbers(p, &["size", "tracking", "baselineShift", "hScale", "vScale", "rotation", "strokeWidth"], cmd)?;
|
||||
check_switches(p, &["underline", "strikethrough", "allCaps", "proportionalMetrics", "tateChuYoko", "noBreak"], cmd)?;
|
||||
let num = |k: &str| p.get(k).and_then(Value::as_f64);
|
||||
let (position, small_caps) = super::docsetup::script_params(p, setup, cmd)?;
|
||||
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
|
||||
@@ -652,7 +657,7 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let protect = s.prefs.missing_glyph_protection && (change.font.is_some() || change.style.is_some());
|
||||
let t = text_ref(s, id)?;
|
||||
change.localize(&t, C)?;
|
||||
let (a, b) = range_of(p, edit::runs_len(&t.runs));
|
||||
let (a, b) = range_of(p, edit::runs_len(&t.runs), C)?;
|
||||
let n = s.edit("Character", |d, _| {
|
||||
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
|
||||
if a == b && !t.plain_text().is_empty() {
|
||||
@@ -682,6 +687,7 @@ fn glyph_em(t: &TextObject, g: &vectorcraft_text::PositionedGlyph) -> Option<f64
|
||||
fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.touchType";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
check_numbers(p, &["start"], C)?;
|
||||
let start = byte_param(p, "start").ok_or_else(|| bad(C, "missing `start` (the byte offset of a character)"))?;
|
||||
if !["dx", "dy", "hScale", "vScale", "rotation"].iter().any(|k| p.get(*k).is_some_and(|v| !v.is_null())) {
|
||||
return Err(bad(C, "nothing to change: give dx, dy, hScale, vScale or rotation"));
|
||||
@@ -866,6 +872,8 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, "area type needs a path that encloses an area"));
|
||||
}
|
||||
super::paint::check_keep_model(p, C)?;
|
||||
check_numbers(p, &["size"], C)?;
|
||||
check_switches(p, &["vertical", "placeholder"], 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") {
|
||||
@@ -952,7 +960,7 @@ fn type_ranges(
|
||||
) -> Result<Vec<(NodeId, usize, usize, bool)>> {
|
||||
// (text, its range; None: all of it).
|
||||
let targets: Vec<(NodeId, Option<(usize, usize)>)> = match id_param(p, "id") {
|
||||
Some(id) => vec![(id, Some(range_of(p, edit::runs_len(&text_ref(s, id)?.runs))))],
|
||||
Some(id) => vec![(id, Some(range_of(p, edit::runs_len(&text_ref(s, id)?.runs), c)?))],
|
||||
None => {
|
||||
let editing = editing_range(s)?.filter(|_| p.get("ids").filter(|v| !v.is_null()).is_none());
|
||||
match editing {
|
||||
@@ -990,7 +998,7 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some("baselineShift") => Step::BaselineShift,
|
||||
_ => return Err(bad(C, "attribute must be size|leading|tracking|kerning|baselineShift")),
|
||||
};
|
||||
let by = f64_or(p, "by", 1.0);
|
||||
let by = number_or(p, "by", 1.0, C)?;
|
||||
if !by.is_finite() {
|
||||
return Err(bad(C, "by must be a number"));
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ fn optical_margin_alignment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(NodeKind::Text(t)) => Some(t.optical_margin),
|
||||
_ => None,
|
||||
});
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!first.unwrap_or(false));
|
||||
let on = switch_or(p, "on", !first.unwrap_or(false), "type.opticalMarginAlignment")?;
|
||||
s.edit("Optical Margin Alignment", |d, _| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else {
|
||||
@@ -168,7 +168,7 @@ fn optical_margin_alignment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// setting) on the document and every text object, in one undo step. Off is the default.
|
||||
fn system_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let current = s.doc()?.doc.setup.system_layout;
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!current);
|
||||
let on = switch_or(p, "on", !current, "type.systemLayout")?;
|
||||
s.edit("System Layout", |d, _| {
|
||||
d.setup.system_layout = on;
|
||||
let mut ids = vec![];
|
||||
@@ -198,7 +198,7 @@ fn fractional_widths(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(NodeKind::Text(t)) => Some(t.fractional_widths),
|
||||
_ => None,
|
||||
});
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!first.unwrap_or(true));
|
||||
let on = switch_or(p, "on", !first.unwrap_or(true), "type.fractionalWidths")?;
|
||||
s.edit("Fractional Widths", |d, _| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else {
|
||||
@@ -219,7 +219,7 @@ fn ruby(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "type.ruby";
|
||||
let ids = text_targets(s, p, C)?;
|
||||
let range = TextRange::parse(p, C)?;
|
||||
let remove = p.get("remove").and_then(Value::as_bool).unwrap_or(false);
|
||||
let remove = switch_or(p, "remove", false, C)?;
|
||||
let reading = str_param(p, "text").map(str::trim).filter(|t| !t.is_empty()).map(str::to_string);
|
||||
if !remove && reading.is_none() {
|
||||
return Err(bad(C, "give `text` (the reading) or `remove`"));
|
||||
@@ -235,11 +235,11 @@ fn ruby(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some("end") | Some("right") | Some("bottom") => RubyAlign::End,
|
||||
Some(_) => return Err(bad(C, "`align` is start, center or end")),
|
||||
};
|
||||
let size = p.get("size").and_then(Value::as_f64).unwrap_or(50.0);
|
||||
let size = number_or(p, "size", 50.0, C)?;
|
||||
if !size.is_finite() || !(1.0..=100.0).contains(&size) {
|
||||
return Err(bad(C, "`size` is a percentage of the base text's size, 1 to 100"));
|
||||
}
|
||||
let overhang = p.get("overhang").and_then(Value::as_bool).unwrap_or(true);
|
||||
let overhang = switch_or(p, "overhang", true, C)?;
|
||||
let mut first: Option<Value> = None;
|
||||
s.edit("Ruby", |d, _| {
|
||||
for id in &ids {
|
||||
@@ -890,6 +890,7 @@ fn size_area(t: &mut TextObject, w: Option<f64>, h: Option<f64>) -> bool {
|
||||
/// `fitMinPercent`), starting from `cur`. None when `p` sets neither (or only `fitMinPercent`
|
||||
/// while the fit isn't Shrink Text).
|
||||
pub(crate) fn fit_param(p: &Value, cur: AreaFit, c: &str) -> Result<Option<AreaFit>> {
|
||||
check_numbers(p, &["fitMinPercent"], c)?;
|
||||
let min = p.get("fitMinPercent").and_then(Value::as_f64);
|
||||
let cur_min = match cur {
|
||||
AreaFit::ShrinkText { min_percent } => Some(min_percent),
|
||||
@@ -942,6 +943,7 @@ const AREA_KEYS: [&str; 7] = ["rows", "columns", "gutter", "inset", "firstBaseli
|
||||
|
||||
fn area_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.areaOptions";
|
||||
check_numbers(p, &["width", "height"], C)?;
|
||||
let ids: Vec<NodeId> = {
|
||||
let d = &s.doc()?.doc;
|
||||
text_targets(s, p, C)?.into_iter().filter(|i| area_text(d.node(*i)).is_some()).collect()
|
||||
@@ -1236,8 +1238,10 @@ mod area_tests {
|
||||
// New type inherits the document's setting.
|
||||
let made = s.execute("text.create", &json!({"x": 10, "y": 120, "text": "Z"})).unwrap()["id"].as_u64().unwrap();
|
||||
assert!(text(&s, made).system_layout);
|
||||
// A boolean given as text is ignored, so the setting toggles off.
|
||||
assert_eq!(s.execute("type.systemLayout", &json!({"on": "no"})).unwrap()["on"], false);
|
||||
// A boolean given as text is an error, and the setting stays on.
|
||||
assert!(s.execute("type.systemLayout", &json!({"on": "no"})).is_err());
|
||||
assert!(setup(&s).system_layout && text(&s, id).system_layout);
|
||||
assert_eq!(s.execute("type.systemLayout", &json!({"on": false})).unwrap()["on"], false);
|
||||
assert!(!setup(&s).system_layout && !text(&s, id).system_layout && !text(&s, made).system_layout);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(setup(&s).system_layout && text(&s, id).system_layout);
|
||||
@@ -1282,8 +1286,10 @@ mod area_tests {
|
||||
assert!(whole.iter().all(|a| *a >= 1.0 && (a - a.round()).abs() < 1e-9), "{whole:?}");
|
||||
assert!(fractional.iter().zip(&whole).any(|(a, b)| (a - b).abs() > 0.05), "{fractional:?} {whole:?}");
|
||||
assert_eq!(serde_json::to_value(text(&s)).unwrap()["fractionalWidths"], false);
|
||||
// A boolean given as text is ignored, so the flag toggles back on.
|
||||
assert_eq!(s.execute("type.fractionalWidths", &json!({"on": "no"})).unwrap()["on"], true);
|
||||
// A boolean given as text is an error, and the flag stays off.
|
||||
assert!(s.execute("type.fractionalWidths", &json!({"on": "no"})).is_err());
|
||||
assert!(!text(&s).fractional_widths);
|
||||
assert_eq!(s.execute("type.fractionalWidths", &json!({})).unwrap()["on"], true);
|
||||
assert!(text(&s).fractional_widths);
|
||||
assert_eq!(s.execute("type.fractionalWidths", &json!({"on": false})).unwrap()["on"], false);
|
||||
assert!(!text(&s).fractional_widths);
|
||||
|
||||
@@ -368,6 +368,8 @@ fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn artboard_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let indices = super::panelcmds::artboard_indices(&s.doc()?.doc, p, "artboard.move")?;
|
||||
check_numbers(p, &["dx", "dy"], "artboard.move")?;
|
||||
check_switches(p, &["moveArt", "copy", "patterns"], "artboard.move")?;
|
||||
let dv = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
let move_art = bool_or(p, "moveArt", false);
|
||||
let copy = bool_or(p, "copy", false);
|
||||
|
||||
Reference in New Issue
Block a user