mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 11:11:30 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user