Merge pull request #6124 from dotCooCoo/fix/envelope-params

Envelopes: commands reject a style, orientation or preserveShape of the wrong type
This commit is contained in:
BFlat
2026-11-08 14:13:33 +08:00
committed by GitHub
6 changed files with 96 additions and 14 deletions
+2 -2
View File
@@ -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;
+9 -10
View File
@@ -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<Envelop
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);
}
if let Some(s) = str_param(p, "style") {
if let Some(s) = text_param(p, "style", cmd)? {
let s = s.strip_prefix("warp.").unwrap_or(s);
if live::WarpStyle::from_id(s).is_none() {
return Err(bad(cmd, format!("unknown warp style `{s}`")));
@@ -28,10 +28,9 @@ fn warp_kind(p: &Value, cmd: &str, cur: Option<&EnvelopeKind>) -> Result<Envelop
h = f64_or(p, "h", f64_or(p, "horizontalDistortion", h)).clamp(-100.0, 100.0);
v = f64_or(p, "v", f64_or(p, "verticalDistortion", v)).clamp(-100.0, 100.0);
let given = switch_or(p, "horizontal", horizontal, cmd)?;
horizontal = match str_param(p, "orientation") {
Some("vertical") => false,
Some("horizontal") => true,
_ => 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<EnvelopeOptions> {
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<Value> {
pub(super) fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.envelope.options";
check_numbers(p, &["fidelity"], C)?;
check_numbers(p, WARP_NUMBERS, C)?;
check_switches(p, &["horizontal"], C)?;
// 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"));
+1 -1
View File
@@ -66,7 +66,7 @@ fn measure(s: &Session, p: &Value, cmd: &str) -> Result<Measure> {
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<Option<&'a str>> {
let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(None) };
+74 -1
View File
@@ -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();
+8
View File
@@ -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`
+2
View File
@@ -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.<id>`, 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.