mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Merge pull request #2851 from dotCooCoo/fix/live-commands-refuse-text-numbers
Blends, meshes and envelopes: a number given as text is refused
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