From 4fddf66d2c127fc3604ec194582859335c49d61a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 06:44:44 -0800 Subject: [PATCH] Envelopes: commands reject a style, orientation or preserveShape of the wrong type object.envelope.makeWithWarp, resetWithWarp and options read `style` and `orientation`, and options read `preserveShape`, as text and ignored any other value, so `{"style": 5}` kept the default style and `{"orientation": "diagonal"}` kept the current orientation. They now return "`style` must be a string" or "`preserveShape` must be a string" for a value other than text or null, and "`orientation` must be one of horizontal, vertical" for any other orientation, through the shared `choice` helper. object.envelope.options checks the warp keys whatever is selected, so a call with no envelope or only mesh envelopes selected gets the same errors, an unknown style name included. Omitted and null values keep today's defaults. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/live.rs | 4 +- crates/engine/src/cmd/live/envelope.rs | 19 ++++--- crates/engine/src/cmd/object/align.rs | 2 +- crates/engine/src/tests/live.rs | 75 +++++++++++++++++++++++++- docs/mcp.md | 8 +++ docs/target-app-parity.md | 2 + 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/crates/engine/src/cmd/live.rs b/crates/engine/src/cmd/live.rs index be02c16b1..464f24115 100644 --- a/crates/engine/src/cmd/live.rs +++ b/crates/engine/src/cmd/live.rs @@ -20,8 +20,8 @@ use super::edit::selected_roots; use super::expand::gradient_sampler; use super::pathops::Beside; use super::{ - CommandSpec, EngineError, Result, Session, always, bad, check_numbers, check_switches, cmd, color_param, f64_or, f64_req, finite_numbers, - has_doc, has_multi, has_selection, id_param, named_ids, number_or, ok, str_param, switch_or, + CommandSpec, EngineError, Result, Session, always, bad, check_numbers, cmd, color_param, f64_or, f64_req, finite_numbers, has_doc, has_multi, + has_selection, id_param, named_ids, number_or, ok, str_param, switch_or, text_param, }; mod blend; diff --git a/crates/engine/src/cmd/live/envelope.rs b/crates/engine/src/cmd/live/envelope.rs index 502ab0a7d..7583cc52f 100644 --- a/crates/engine/src/cmd/live/envelope.rs +++ b/crates/engine/src/cmd/live/envelope.rs @@ -1,9 +1,9 @@ +use super::super::object::choice; use super::common::{fix_ids, ids_json, is_envelope, roots_param, selected_of, wrap}; use super::mesh::{handle_param, index_param, whole}; use super::{ Affine, Appearance, Arc, Beside, Color, Document, EngineError, EnvelopeKind, EnvelopeMap, EnvelopeOptions, Node, NodeId, NodeKind, Paint, - PathData, Point, PreserveShape, Result, Session, Value, bad, check_numbers, check_switches, f64_or, f64_req, id_param, json, live, ok, str_param, - switch_or, + PathData, Point, PreserveShape, Result, Session, Value, bad, check_numbers, f64_or, f64_req, id_param, json, live, ok, switch_or, text_param, }; // ---------- Envelope ---------- @@ -17,7 +17,7 @@ fn warp_kind(p: &Value, cmd: &str, cur: Option<&EnvelopeKind>) -> Result) -> Result false, - Some("horizontal") => true, - _ => given, + horizontal = match choice(p, cmd, "orientation", &["horizontal", "vertical"])? { + Some(o) => o == "horizontal", + None => given, }; Ok(EnvelopeKind::Warp { style, bend, h, v, horizontal }) } @@ -185,7 +184,7 @@ fn envelope_options_json(mut v: Value, o: &EnvelopeOptions, fidelity: f64) -> Va /// `cur` with the options `p` sets. fn options_param(p: &Value, cmd: &str, cur: EnvelopeOptions) -> Result { - let preserve_shape = match str_param(p, "preserveShape") { + let preserve_shape = match text_param(p, "preserveShape", cmd)? { Some(v) => PreserveShape::parse(v).ok_or_else(|| bad(cmd, "preserveShape must be clippingMask|transparency"))?, None => cur.preserve_shape, }; @@ -289,8 +288,8 @@ pub(super) fn env_release(s: &mut Session, _: &Value) -> Result { pub(super) fn env_options(s: &mut Session, p: &Value) -> Result { const C: &str = "object.envelope.options"; check_numbers(p, &["fidelity"], C)?; - check_numbers(p, WARP_NUMBERS, C)?; - check_switches(p, &["horizontal"], C)?; + // The warp keys are checked whatever is selected: with nothing or only mesh envelopes, too. + warp_kind(p, C, None)?; 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")); diff --git a/crates/engine/src/cmd/object/align.rs b/crates/engine/src/cmd/object/align.rs index fe350a144..156b35824 100644 --- a/crates/engine/src/cmd/object/align.rs +++ b/crates/engine/src/cmd/object/align.rs @@ -66,7 +66,7 @@ fn measure(s: &Session, p: &Value, cmd: &str) -> Result { Ok(Measure { preview, point: s.prefs.align_glyph_bounds_point, area: s.prefs.align_glyph_bounds_area }) } -/// Param `key` of the Align commands, Shear, Unlock All, Show All and `text.setFormat`'s lists: one +/// Param `key` of the Align commands, Shear, Unlock All, Show All, a warp's `orientation` and `text.setFormat`'s lists: one /// of `values`, or `None` when it is missing or null. Any other value is an error that lists `values`. pub(crate) fn choice<'a>(p: &'a Value, cmd: &str, key: &str, values: &[&str]) -> Result> { let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(None) }; diff --git a/crates/engine/src/tests/live.rs b/crates/engine/src/tests/live.rs index 1dc5f66ba..540ed218a 100644 --- a/crates/engine/src/tests/live.rs +++ b/crates/engine/src/tests/live.rs @@ -2,7 +2,7 @@ use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; -use vectorcraft_doc::live::{self, BlendOrientation, BlendSpacing, EnvelopeKind}; +use vectorcraft_doc::live::{self, BlendOrientation, BlendSpacing, EnvelopeKind, PreserveShape}; use vectorcraft_doc::{Node, NodeKind}; use vectorcraft_geom::{Point, Rect}; @@ -87,6 +87,12 @@ fn refuses_non_boolean(s: &mut Session, cmd: &str, p: &Value, key: &str) { assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if *msg == want), "{cmd} {p}: {r:?}"); } +/// `cmd` with `p` fails with the error `want`. +fn refuses_with(s: &mut Session, cmd: &str, p: &Value, want: &str) { + let r = s.execute(cmd, p); + assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if msg == want), "{cmd} {p}: {r:?}"); +} + // ---------- Blend ---------- #[test] @@ -745,6 +751,73 @@ fn envelope_options_refuse_a_boolean_given_as_text() { assert!(matches!(&node(&s, warp).kind, NodeKind::Envelope { options, .. } if !options.anti_alias && options.distort_appearance)); } +/// Make with Warp, Reset with Warp and Envelope Options refuse a warp `style` that is not text or +/// names no warp, and an `orientation` other than horizontal or vertical: no envelope is made, a +/// warp envelope keeps its warp, and no undo step is added. Null is read as the key left out. +#[test] +fn envelope_warps_refuse_an_invalid_style_or_orientation() { + let mut s = session(); + let a = text_rect(&mut s); + sel(&mut s, &[a]); + let n = undo_len(&s); + let calls = [ + (json!({"style": 5}), "`style` must be a string"), + (json!({"style": ["arch"], "bend": 40}), "`style` must be a string"), + (json!({"style": "foo"}), "unknown warp style `foo`"), + (json!({"orientation": 1}), "`orientation` must be one of horizontal, vertical"), + (json!({"orientation": true, "horizontal": true}), "`orientation` must be one of horizontal, vertical"), + (json!({"orientation": "diagonal"}), "`orientation` must be one of horizontal, vertical"), + ]; + for (p, want) in &calls { + refuses_with(&mut s, "object.envelope.makeWithWarp", p, want); + } + assert_eq!(undo_len(&s), n, "nothing changed"); + assert!(matches!(node(&s, a).kind, NodeKind::Path { .. }), "not enveloped"); + let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": null, "orientation": null, "bend": 30})).unwrap()); + assert!(matches!(&node(&s, e).kind, NodeKind::Envelope { kind, .. } + if matches!(&**kind, EnvelopeKind::Warp { style, horizontal: true, .. } if style == "arc"))); + let (before, n) = (node(&s, e), undo_len(&s)); + for cmd in ["object.envelope.resetWithWarp", "object.envelope.options"] { + for (p, want) in &calls { + refuses_with(&mut s, cmd, p, want); + } + } + assert_eq!((node(&s, e), undo_len(&s)), (before, n), "nothing changed"); + s.execute("select.none", &json!({})).unwrap(); + for (p, want) in &calls { + refuses_with(&mut s, "object.envelope.options", p, want); + } + sel(&mut s, &[e]); + s.execute("object.envelope.resetWithWarp", &json!({"style": "warp.flag", "orientation": "vertical"})).unwrap(); + assert!(matches!(&node(&s, e).kind, NodeKind::Envelope { kind, .. } + if matches!(&**kind, EnvelopeKind::Warp { style, horizontal: false, .. } if style == "flag"))); +} + +/// Envelope Options refuses a `preserveShape` that is not text, for the selected envelope and for +/// the options new envelopes get, and changes neither; null keeps the current value. +#[test] +fn envelope_options_refuse_a_preserve_shape_that_is_not_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 calls = [json!({"preserveShape": 1}), json!({"preserveShape": true, "antiAlias": false}), json!({"preserveShape": ["transparency"]})]; + for ids in [vec![e], vec![]] { + sel(&mut s, &ids); + let before = (node(&s, e), undo_len(&s)); + for p in &calls { + refuses_with(&mut s, "object.envelope.options", p, "`preserveShape` must be a string"); + } + assert_eq!((node(&s, e), undo_len(&s)), before, "nothing changed with {ids:?} selected"); + } + assert_eq!(s.envelope_defaults, None, "the options new envelopes get stayed"); + sel(&mut s, &[e]); + s.execute("object.envelope.options", &json!({"preserveShape": "transparency"})).unwrap(); + s.execute("object.envelope.options", &json!({"preserveShape": null, "antiAlias": false})).unwrap(); + assert!(matches!(&node(&s, e).kind, NodeKind::Envelope { options, .. } + if options.preserve_shape == PreserveShape::Transparency && !options.anti_alias)); +} + #[test] fn envelope_renders_distorted() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index 65a537ddd..5881c97cc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1227,6 +1227,14 @@ in `object.envelope.resetWithMesh`; `antiAlias`, `distortAppearance`, `distortLi `distortPatternFills` in `object.envelope.options`; and `editing` in `object.envelope.editContents`. A null is read as a key left out. +`object.envelope.makeWithWarp`, `resetWithWarp` and `options` refuse a `style` that is not a string with +`` `style` must be a string `` and a name that is no warp style with `` unknown warp style `foo` ``, and an +`orientation` other than `horizontal` or `vertical`, a number or `true` included, with +`` `orientation` must be one of horizontal, vertical ``. `object.envelope.options` refuses a `preserveShape` that is +not a string with `` `preserveShape` must be a string ``, and checks the warp keys when no envelope or only mesh +envelopes are selected, too. Each leaves the document, its undo history and the options new envelopes start with as +they were. A null is read as a key left out. + These commands refuse a string, a number or another value that is not true or false with `` `closed` must be true or false ``, naming the key, and leave the document and its undo history as they were: `clockwise` in `shape.spiral`; `closed` in `shape.arc` and `path.create`; `vertical` and `placeholder` in `text.create` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b91dcb3fd..934767da1 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1457,6 +1457,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Blend and envelope options given as text:** `object.blend.make` and `object.blend.options` (`smooth`), `object.blend.spine.moveAnchor` (`independent`) and `reshapeSegment` (`perpendicular`), `object.envelope.makeWithWarp`, `resetWithWarp` and `options` (`horizontal`), `object.envelope.resetWithMesh` (`maintainShape`), `object.envelope.options` (`antiAlias`, `distortAppearance`, `distortLinearGradients`, `distortPatternFills`) and `object.envelope.editContents` (`editing`) refuse a value other than true, false or null, such as `"false"`, and leave the document and its undo history as they were. +- **Envelope names given as another type:** `object.envelope.makeWithWarp`, `resetWithWarp` and `options` refuse a `style` or `orientation` other than text or null, such as `5`, and an `orientation` other than `horizontal` or `vertical`; `object.envelope.options` refuses such a `preserveShape`. `object.envelope.options` checks the warp keys with no envelope or only mesh envelopes selected, too. Each leaves the document, the options new envelopes get and the undo history as they were. + - **Exporter plug-ins (G17):** a WebAssembly plug-in of kind `exporter` adds a write-only file format. File › Export › Export As lists it after the built-in formats, with Use Artboards greyed out (the plug-in writes the whole document) and its parameters in a generated dialog once the file is picked; `document.export` writes it by its format id, `plugin.`, or by one of its extensions, and built-in formats keep theirs. The plug-in is given the native file's JSON (large images without their data) and `_context` (`mode`, `extension`, `name`), with 10 G instructions plus 4,000 per byte of input and 300 s, and Background Export's Cancel stops it between two slices of that budget. A decline or invalid output writes nothing; its warnings come back with the export's. Regressions: `crates/engine/tests/plugin_export.rs`, `crates/plugins/tests/abi.rs`, `crates/ui-egui/src/dialogs/tests_plugin_export.rs`. Scores and hours unchanged. - **Plug-in parameters as their manifest presents them (G17):** each parameter may have a `label`, a `unit` shown after its value, `help` on its label and a `helpUrl` page opened from a button, a `group` (sections that fold, after the parameters outside any group) and a `visibleWhen` that shows it only while the checkboxes and choices it names have one of the listed values (a hidden parameter still goes to the plug-in). `string` and `numberList` parameters join number, int, bool and choice, and a plug-in declares up to 256 parameters, at most 64 outside groups. The filter, live-effect and exporter dialogs use these keys, and the Appearance panel's effect editor the labels, units, help and conditions; hosts from before them ignore them. Regressions: `crates/plugins/src/dialog.rs`, `crates/plugins/src/manifest.rs`, `crates/ui-egui/src/dialogs/tests_plugin_fields.rs`, `crates/ui-egui/src/panels/appearance.rs`. Scores and hours unchanged.