mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Blends, meshes and envelopes: a number given as text is refused
The blend, envelope and gradient mesh commands read their optional
numbers with f64_or or as_f64, which fall back to a default for a value
that is not a JSON number. object.blend.make {"steps": "8"} made a
Smooth Color blend and reported success. Blend Options did the same
with steps, distance and value. Make with Warp, Reset with Warp and
Envelope Options did the same with bend, h and v (and their long
names). Make with Mesh, Reset with Mesh and Create Gradient Mesh did
the same with rows, cols and columns. Envelope Options did the same
with fidelity, Create Gradient Mesh did the same with highlight, and
Set Mesh Point Color did the same with opacity.
These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Envelope Options refuses the warp keys whether
or not a warp envelope is selected. Create Gradient Mesh refuses an
`at` that is not exactly two numbers with "`at` must be [x, y]".
Before, it took the first two entries of a longer array, and for any
other value it made a mesh with no lines through the point.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -410,6 +410,7 @@ const DEFAULT_STEPS: u32 = 5;
|
||||
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 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");
|
||||
@@ -971,7 +972,11 @@ fn blend_reverse_stack(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
// ---------- Envelope ----------
|
||||
|
||||
/// The number keys of a warp: `bend`, and the two distortions by their short and long names.
|
||||
const WARP_NUMBERS: &[&str] = &["bend", "h", "v", "horizontalDistortion", "verticalDistortion"];
|
||||
|
||||
fn warp_kind(p: &Value, cmd: &str, cur: Option<&EnvelopeKind>) -> Result<EnvelopeKind> {
|
||||
check_numbers(p, WARP_NUMBERS, cmd)?;
|
||||
let (mut style, mut bend, mut h, mut v, mut horizontal) = ("arc".to_string(), 50.0, 0.0, 0.0, true);
|
||||
if let Some(EnvelopeKind::Warp { style: s0, bend: b0, h: h0, v: v0, horizontal: z0 }) = cur {
|
||||
(style, bend, h, v, horizontal) = (s0.clone(), *b0, *h0, *v0, *z0);
|
||||
@@ -1005,6 +1010,7 @@ fn envelope(id: NodeId, content: Vec<Arc<Node>>, kind: EnvelopeKind, (options, f
|
||||
|
||||
/// `rows` and `cols` (or `columns`), whole numbers 1..50 each, defaulting to `default`.
|
||||
fn rows_cols(p: &Value, cmd: &str, default: (u32, u32)) -> Result<(u32, u32)> {
|
||||
check_numbers(p, &["rows", "cols", "columns"], cmd)?;
|
||||
let rows = f64_or(p, "rows", default.0 as f64);
|
||||
let cols_key = if p.get("cols").and_then(Value::as_f64).is_some() { "cols" } else { "columns" };
|
||||
let cols = f64_or(p, cols_key, default.1 as f64);
|
||||
@@ -1241,6 +1247,8 @@ fn env_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
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)?;
|
||||
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"));
|
||||
@@ -1386,8 +1394,12 @@ fn mesh_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(a) => MeshAppearance::parse(a).ok_or_else(|| bad(C, "appearance must be flat|center|edge"))?,
|
||||
None => MeshAppearance::Flat,
|
||||
};
|
||||
check_numbers(p, &["highlight"], C)?;
|
||||
let highlight = f64_or(p, "highlight", 100.0).clamp(0.0, 100.0);
|
||||
let at = point_param(p, "at");
|
||||
let at = match p.get("at") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(finite_numbers(v).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad(C, "`at` must be [x, y]"))?),
|
||||
};
|
||||
let (rows, cols) = if at.is_some() { (1, 1) } else { (rows, cols) };
|
||||
let roots = roots_param(s, p)?;
|
||||
let (ids, index) = s.edit("Create Gradient Mesh", |d, sel| {
|
||||
@@ -1522,6 +1534,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)?;
|
||||
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);
|
||||
|
||||
@@ -73,6 +73,13 @@ fn all_anchors(nodes: &[Node]) -> Vec<Point> {
|
||||
v
|
||||
}
|
||||
|
||||
/// `cmd` with `p` fails with the error for `key`, which `p` gives as something other than a number.
|
||||
fn refuses_non_number(s: &mut Session, cmd: &str, p: &Value, key: &str) {
|
||||
let r = s.execute(cmd, p);
|
||||
let want = format!("`{key}` must be a number, not {}", p[key]);
|
||||
assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if *msg == want), "{cmd} {p}: {r:?}");
|
||||
}
|
||||
|
||||
// ---------- Blend ----------
|
||||
|
||||
#[test]
|
||||
@@ -116,6 +123,54 @@ fn blend_steps_that_are_not_a_whole_number_are_refused() {
|
||||
assert_eq!(steps_of(&s, g).len(), 6);
|
||||
}
|
||||
|
||||
/// Make refuses Specified Steps or Distance given as text or as another value that is not a number,
|
||||
/// and makes no blend; `steps: 8` as a number makes 8 steps.
|
||||
#[test]
|
||||
fn blend_make_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let (a, b) = (rect(&mut s, 0.0, 0.0, 10.0, 10.0), rect(&mut s, 100.0, 0.0, 10.0, 10.0));
|
||||
sel(&mut s, &[a, b]);
|
||||
let n = undo_len(&s);
|
||||
for (key, p) in [
|
||||
("steps", json!({"steps": "8"})),
|
||||
("distance", json!({"distance": "10"})),
|
||||
("value", json!({"spacing": "steps", "value": "3"})),
|
||||
("value", json!({"spacing": "distance", "value": [5]})),
|
||||
("distance", json!({"spacing": "distance", "distance": true})),
|
||||
] {
|
||||
refuses_non_number(&mut s, "object.blend.make", &p, key);
|
||||
}
|
||||
assert_eq!(undo_len(&s), n, "nothing changed");
|
||||
assert!(matches!(node(&s, a).kind, NodeKind::Path { .. }), "not blended");
|
||||
let g = id_of(&s.execute("object.blend.make", &json!({"steps": 8})).unwrap());
|
||||
assert_eq!(steps_of(&s, g).len(), 10);
|
||||
}
|
||||
|
||||
/// Blend Options refuses Specified Steps or Distance given as text, for the selected blends and for
|
||||
/// the options new blends start with, and changes neither.
|
||||
#[test]
|
||||
fn blend_options_refuse_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let (_, _, g) = blend(&mut s, json!({"steps": 3}));
|
||||
let n = undo_len(&s);
|
||||
let calls = [
|
||||
("steps", json!({"steps": "4"})),
|
||||
("value", json!({"spacing": "steps", "value": "4"})),
|
||||
("distance", json!({"distance": "20"})),
|
||||
("distance", json!({"spacing": "distance", "value": null, "distance": "20"})),
|
||||
];
|
||||
for (key, p) in &calls {
|
||||
refuses_non_number(&mut s, "object.blend.options", p, key);
|
||||
}
|
||||
assert_eq!((undo_len(&s), steps_of(&s, g).len()), (n, 5), "the blend kept its options");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let defaults = s.prefs.blend_options;
|
||||
for (key, p) in &calls {
|
||||
refuses_non_number(&mut s, "object.blend.options", p, key);
|
||||
}
|
||||
assert_eq!(s.prefs.blend_options, defaults, "the options new blends start with stayed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_mid_step_interpolates_geometry_and_colour() {
|
||||
let mut s = session();
|
||||
@@ -479,6 +534,95 @@ fn envelope_options_and_edit_contents() {
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![e]);
|
||||
}
|
||||
|
||||
/// The warp's `bend`, and `h` and `v` by those keys or by their long names, given as text.
|
||||
fn warp_numbers_as_text() -> [(&'static str, Value); 5] {
|
||||
[
|
||||
("bend", json!({"bend": "40"})),
|
||||
("h", json!({"h": "10"})),
|
||||
("v", json!({"v": "-10"})),
|
||||
("horizontalDistortion", json!({"horizontalDistortion": "10"})),
|
||||
("verticalDistortion", json!({"verticalDistortion": "10%"})),
|
||||
]
|
||||
}
|
||||
|
||||
/// Make with Warp refuses a bend or distortion given as text, and makes no envelope.
|
||||
#[test]
|
||||
fn envelope_make_with_warp_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = text_rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let n = undo_len(&s);
|
||||
for (key, p) in warp_numbers_as_text() {
|
||||
refuses_non_number(&mut s, "object.envelope.makeWithWarp", &p, key);
|
||||
}
|
||||
assert_eq!(undo_len(&s), n, "nothing changed");
|
||||
assert!(matches!(node(&s, a).kind, NodeKind::Path { .. }), "not enveloped");
|
||||
}
|
||||
|
||||
/// Reset with Warp refuses a bend or distortion given as text, and the envelope keeps its warp.
|
||||
#[test]
|
||||
fn envelope_reset_with_warp_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = text_rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": "arc", "bend": 50})).unwrap());
|
||||
let (before, n) = (node(&s, e), undo_len(&s));
|
||||
for (key, p) in warp_numbers_as_text() {
|
||||
refuses_non_number(&mut s, "object.envelope.resetWithWarp", &p, key);
|
||||
}
|
||||
assert_eq!((node(&s, e), undo_len(&s)), (before, n), "nothing changed");
|
||||
}
|
||||
|
||||
/// Make with Mesh refuses rows or columns given as text, and makes no envelope.
|
||||
#[test]
|
||||
fn envelope_make_with_mesh_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = text_rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let n = undo_len(&s);
|
||||
for (key, p) in [("rows", json!({"rows": "3"})), ("cols", json!({"cols": "3"})), ("columns", json!({"rows": 2, "columns": "3"}))] {
|
||||
refuses_non_number(&mut s, "object.envelope.makeWithMesh", &p, key);
|
||||
}
|
||||
assert_eq!(undo_len(&s), n, "nothing changed");
|
||||
assert!(matches!(node(&s, a).kind, NodeKind::Path { .. }), "not enveloped");
|
||||
}
|
||||
|
||||
/// Reset with Mesh refuses rows or columns given as text, and the envelope keeps its mesh.
|
||||
#[test]
|
||||
fn envelope_reset_with_mesh_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = text_rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = id_of(&s.execute("object.envelope.makeWithMesh", &json!({"rows": 2, "cols": 2})).unwrap());
|
||||
let (before, n) = (node(&s, e), undo_len(&s));
|
||||
for (key, p) in [("rows", json!({"rows": "3"})), ("cols", json!({"cols": "3"})), ("columns", json!({"columns": "3", "maintainShape": false}))] {
|
||||
refuses_non_number(&mut s, "object.envelope.resetWithMesh", &p, key);
|
||||
}
|
||||
assert_eq!((node(&s, e), undo_len(&s)), (before, n), "nothing changed");
|
||||
}
|
||||
|
||||
/// Envelope Options refuses a fidelity, bend or distortion given as text, for the selected
|
||||
/// envelope and for the options new envelopes get, and changes neither.
|
||||
#[test]
|
||||
fn envelope_options_refuse_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = text_rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": "arc", "bend": 50})).unwrap());
|
||||
let (before, n) = (node(&s, e), undo_len(&s));
|
||||
let mut calls = vec![("fidelity", json!({"fidelity": "80", "antiAlias": false}))];
|
||||
calls.extend(warp_numbers_as_text());
|
||||
for (key, p) in &calls {
|
||||
refuses_non_number(&mut s, "object.envelope.options", p, key);
|
||||
}
|
||||
assert_eq!((node(&s, e), undo_len(&s)), (before, n), "nothing changed");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
for (key, p) in &calls {
|
||||
refuses_non_number(&mut s, "object.envelope.options", p, key);
|
||||
}
|
||||
assert_eq!(s.envelope_defaults, None, "the options new envelopes get stayed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_renders_distorted() {
|
||||
let mut s = session();
|
||||
@@ -634,6 +778,46 @@ fn mesh_commands_refuse_an_index_or_count_that_is_not_a_whole_number() {
|
||||
assert_eq!(mesh_of(&s, a).points[4].p, Point::new(40.0, 60.0));
|
||||
}
|
||||
|
||||
/// Create Gradient Mesh refuses rows, columns or a highlight given as text, and an `at` that is not
|
||||
/// two numbers, and makes no mesh; `at: [50, 40]` with a null highlight makes one.
|
||||
#[test]
|
||||
fn mesh_create_refuses_a_number_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a]);
|
||||
let n = undo_len(&s);
|
||||
for (key, p) in [
|
||||
("rows", json!({"rows": "3"})),
|
||||
("cols", json!({"cols": "2"})),
|
||||
("columns", json!({"columns": "2"})),
|
||||
("highlight", json!({"appearance": "center", "highlight": "50"})),
|
||||
] {
|
||||
refuses_non_number(&mut s, "object.mesh.create", &p, key);
|
||||
}
|
||||
for at in [json!(["50", "40"]), json!([50]), json!(50), json!({"x": 50, "y": 40})] {
|
||||
let r = s.execute("object.mesh.create", &json!({ "at": at }));
|
||||
assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if msg == "`at` must be [x, y]"), "{at}: {r:?}");
|
||||
}
|
||||
assert_eq!(undo_len(&s), n, "nothing changed");
|
||||
assert!(matches!(node(&s, a).kind, NodeKind::Path { .. }), "still a path");
|
||||
s.execute("object.mesh.create", &json!({"at": [50, 40], "highlight": null})).unwrap();
|
||||
assert_eq!((mesh_of(&s, a).rows, mesh_of(&s, a).cols), (2, 2));
|
||||
}
|
||||
|
||||
/// Set Mesh Point Color refuses an opacity given as text, and the point keeps its color and opacity.
|
||||
#[test]
|
||||
fn mesh_set_point_color_refuses_an_opacity_given_as_text() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("object.mesh.create", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
let (before, n) = (mesh_of(&s, a), undo_len(&s));
|
||||
for p in [json!({"id": a.0, "index": 3, "color": "#00ff00", "opacity": "0.5"}), json!({"id": a.0, "index": 3, "opacity": "50%"})] {
|
||||
refuses_non_number(&mut s, "object.mesh.setPointColor", &p, "opacity");
|
||||
}
|
||||
assert_eq!((mesh_of(&s, a), undo_len(&s)), (before, n), "nothing changed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_create_at_point_single_undo() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -880,6 +880,14 @@ journal recorded by an older version replays as it did, except for an effect `co
|
||||
`` `columns` must be a number, not "4" ``. Each message names the key that was given, and the command then changes
|
||||
nothing.
|
||||
|
||||
The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same
|
||||
message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in
|
||||
`object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in
|
||||
`object.envelope.makeWithWarp`, `resetWithWarp` and `options`; `rows`, `cols` and `columns` in
|
||||
`object.envelope.makeWithMesh`, `resetWithMesh` and `object.mesh.create`; `fidelity` in `object.envelope.options`;
|
||||
`highlight` in `object.mesh.create`; and `opacity` in `object.mesh.setPointColor`. A null is read as a key left out.
|
||||
`object.mesh.create` refuses an `at` that is not two numbers with `` `at` must be [x, y] ``.
|
||||
|
||||
A step can use an earlier step's result, in `run` as in the `command_batch` tool. A parameter string that is exactly
|
||||
`$N` or `$N.path` takes that value from the result of step `N` (counting from 0, in the order they ran; `run --in`'s
|
||||
open isn't a step), and `$last…` from the step before. A path goes into objects by key and arrays by index:
|
||||
|
||||
@@ -803,6 +803,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Blend, mesh and envelope points by number:** `object.blend.spine.moveAnchor`, `reshapeSegment` and `removeAnchor`, `object.mesh.setPointColor`, `movePoint` and `deletePoint`, and `object.envelope.setMeshPoint` refuse an `anchor`, `segment` or `index` that is not a whole number, and `object.envelope.setMeshPoint` refuses a negative `index`. Specified Steps (`object.blend.make`, `object.blend.options`) and the rows and columns of `object.mesh.create`, `object.envelope.makeWithMesh` and `resetWithMesh` must be whole numbers too.
|
||||
|
||||
- **Blend, mesh and envelope numbers given as text:** `object.blend.make` and `object.blend.options` (`steps`, `distance`, `value`), `object.envelope.makeWithWarp`, `resetWithWarp` and `options` (`bend`, `h`, `v`, `horizontalDistortion`, `verticalDistortion`), `object.envelope.makeWithMesh` and `resetWithMesh` (`rows`, `cols`, `columns`), `object.envelope.options` (`fidelity`), `object.mesh.create` (`rows`, `cols`, `columns`, `highlight`) and `object.mesh.setPointColor` (`opacity`) refuse a value other than a number or null, such as `"8"`, and change nothing. `object.mesh.create` refuses an `at` that is not `[x, y]`.
|
||||
|
||||
- **Plug-in effects after a plug-in changes:** when a plug-in is installed for the first time, installed again or removed (`plugin.install`, `plugin.reload`, `plugin.remove`, File › Open of a `.wasm` file, the Additional Plug-ins Folder preference), the next frame the canvas draws uses the plug-ins then installed for objects with a plug-in effect and their raster effects, for the blends, envelopes and repeats that show such objects, and for pattern tiles and symbol instances whose art holds one.
|
||||
|
||||
- **Full font names (#1258):** OpenType name ID 4 identifies its face alongside family/legacy/PostScript names, including localized names, for loaded fonts, cataloged files and Missing Fonts searches. Synthetic name-table regressions cover full names distinct from their PostScript name, real-family precedence and variable fonts' default faces. Windows font-service activation remains unverified; scores and hours are unchanged.
|
||||
|
||||
Reference in New Issue
Block a user