From b9c3dd45192d219b7d2b9436a744db1c5e77d996 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 16 Oct 2026 20:35:44 -0700 Subject: [PATCH] Effects: effect.apply and effect.setParams refuse a color the effect can't draw effect.apply and effect.setParams stored any string as the color of Drop Shadow, Inner Glow, Outer Glow and Shift to Color. When the effect could not read it, it drew its default color with no message, so a typo such as #ff00 in the Drop Shadow dialog turned the shadow black. The commands now refuse such a color before they edit, with "`color` must be a #rrggbb color", and the document and its undo history stay as they were. Hex colors, the five named colors, [r, g, b] and color objects still pass. The check runs in the commands, so the app, command.batch steps and replayed journals get it too. The effect dialog's OK shows the error and keeps the dialog open, with Preview on or off. Saved documents open as before. Docs: docs/mcp.md (effect params). --- crates/engine/src/cmd/effectcmd.rs | 7 +++- crates/engine/src/cmd/params.rs | 43 +++++++++++++++++++++-- crates/engine/tests/param_keys.rs | 52 ++++++++++++++++++++++++++++ crates/ui-egui/src/dialogs/effect.rs | 20 +++-------- crates/ui-egui/src/dialogs/tests.rs | 21 +++++++++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tests.rs | 14 ++++++++ crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + docs/mcp.md | 20 +++++++++-- 24 files changed, 172 insertions(+), 22 deletions(-) diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index abffe675c..c9ebf4214 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -138,6 +138,7 @@ pub(crate) fn apply(s: &mut Session, p: &Value) -> Result { if !params.is_null() && !params.is_object() { return Err(bad(C, "`params` must be an object")); } + super::params::check_effect_color(C, id, ¶ms)?; // Crop Marks take their style from the preferences when applied. if id == effects::CROP_MARKS && params.get("style").is_none() { params["style"] = json!(s.crop_mark_style().id()); @@ -313,7 +314,11 @@ fn set_params(s: &mut Session, p: &Value) -> Result { .transpose()?; let visible = p.get("visible").and_then(Value::as_bool); let item = item_target(s, p, C)?; - if set_params_effects(s, p).iter().any(|e| e == GLASS) { + let edited = set_params_effects(s, p); + for effect in &edited { + super::params::check_effect_color(C, effect, ¶ms)?; + } + if edited.iter().any(|e| e == GLASS) { load_texture(&mut params, C)?; } for id in appearance_targets(s, p)? { diff --git a/crates/engine/src/cmd/params.rs b/crates/engine/src/cmd/params.rs index 7989071f0..5d70a9fa5 100644 --- a/crates/engine/src/cmd/params.rs +++ b/crates/engine/src/cmd/params.rs @@ -13,7 +13,9 @@ //! command with its default. So are the `params` of an effect (#1592): `effect.apply` and //! `effect.setParams` answer a key the effect doesn't take, or a value of another type than its //! default, with a parameter error ([`check_effect_params`]). [`Session::execute`] itself takes -//! what it is given, so the journal of an older version still replays. +//! what it is given, so the journal of an older version still replays, except for an effect's +//! `color` that the effect can't read, which `effect.apply` and `effect.setParams` refuse +//! themselves ([`check_effect_color`]). use serde_json::Value; @@ -212,8 +214,9 @@ fn unknown(id: &str, key: &str, keys: &[&str]) -> crate::EngineError { /// `params` of built-in effect `effect`, as command `id` was given them (#1592): only keys the /// effect takes (those of its dialog defaults and of its params description), each of its -/// default's type. Numbers out of range pass: the effect clamps them when it draws. An effect -/// plug-in resolves its own against its manifest, and an unknown effect is the command's to report. +/// default's type, and a `color` the effect can draw ([`check_effect_color`]). Numbers out of +/// range pass: the effect clamps them when it draws. An effect plug-in resolves its own against +/// its manifest, and an unknown effect is the command's to report. pub fn check_effect_params(id: &str, effect: &str, params: &Value) -> Result<()> { let Value::Object(given) = params else { return Ok(()) }; if effect.starts_with(vectorcraft_plugins::effect::PREFIX) { @@ -240,6 +243,40 @@ pub fn check_effect_params(id: &str, effect: &str, params: &Value) -> Result<()> _ => {} } } + check_effect_color(id, effect, params) +} + +/// Can an effect read `v` as its `color`: a string [`Color::from_hex`] reads (`#rrggbb`, `#rgb`, +/// `#rrggbbaa`, black, white, red, green, blue), `[r, g, b]` whose first three entries are numbers +/// (read on a 0..1 scale) or a color object (`{"model": "rgb", "r": …}`)? The effect draws its +/// default color for any other value, except that an array of three or more entries draws an +/// entry that isn't a number as 0. +/// +/// [`Color::from_hex`]: vectorcraft_color::Color::from_hex +fn is_effect_color(v: &Value) -> bool { + match v { + Value::String(s) => vectorcraft_color::Color::from_hex(s).is_some(), + Value::Array(a) => a.len() >= 3 && a.iter().take(3).all(Value::is_number), + Value::Object(_) => serde_json::from_value::(v.clone()).is_ok(), + _ => false, + } +} + +/// The `color` in `params` of built-in effect `effect`, when the effect has a `color` parameter, +/// as command `id` was given it: a parameter error naming it unless the effect can read it +/// ([`is_effect_color`]). `effect.apply` and `effect.setParams` run this themselves before they +/// edit, so the app's own edits, the steps of a `command.batch` and a replayed journal are held to +/// it too. +pub(crate) fn check_effect_color(id: &str, effect: &str, params: &Value) -> Result<()> { + let key = "color"; + let Some(v) = params.get(key) else { return Ok(()) }; + if effect.starts_with(vectorcraft_plugins::effect::PREFIX) { + return Ok(()); + } + let takes_color = vectorcraft_render::effects::effect_info(effect).is_some_and(|info| info.defaults.get(key).is_some_and(Value::is_string)); + if takes_color && !is_effect_color(v) { + return Err(bad(id, format!("`{key}` must be a #rrggbb color"))); + } Ok(()) } diff --git a/crates/engine/tests/param_keys.rs b/crates/engine/tests/param_keys.rs index cc0551e8f..b8e00bea6 100644 --- a/crates/engine/tests/param_keys.rs +++ b/crates/engine/tests/param_keys.rs @@ -44,6 +44,58 @@ fn effect_params_no_longer_taken_still_replay_and_open() { assert_eq!(stored(&back)["blurr"], json!(3)); } +/// An effect's `color` that the effect can't read (a string `Color::from_hex` doesn't read, an +/// `[r, g, b]` without three numbers, an object that isn't a color, `null`) is refused by +/// `effect.apply`, `appearance.addEffect` and `effect.setParams` themselves, not only by the check, +/// and the document and its undo history stay as they were. The forms the effect reads pass. A +/// document saved with an unreadable color still opens with it. +#[test] +fn an_effect_color_it_cannot_draw_is_refused() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap(); + s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "params": {"color": "#ff0000"}})).unwrap(); + let state = |s: &Session| { + let st = s.active().unwrap(); + (st.doc.clone(), st.history.undo.len()) + }; + let before = state(&s); + let why = "`color` must be a #rrggbb color"; + for bad in [json!("not-a-color"), json!("#ff00"), json!([1, 0]), json!(["#ff0000", 0, 0]), json!({"gray": 50}), json!(null)] { + for (cmd, params) in [ + ("effect.setParams", json!({"index": 0, "params": {"color": bad}})), + ("effect.apply", json!({"effect": "stylize.outerGlow", "params": {"color": bad}})), + ("appearance.addEffect", json!({"effect": "adjust.shiftToColor", "params": {"color": bad}})), + ] { + let e = s.execute(cmd, ¶ms).unwrap_err().to_string(); + assert!(e.contains(why), "{cmd} {params}: {e}"); + let checked = s.check_params(cmd, ¶ms).unwrap_err().to_string(); + assert!(checked.contains(why), "{cmd} {params}: {checked}"); + assert!(state(&s) == before, "{cmd} {params} changed the document or its undo history"); + } + } + let stored = |s: &Session| s.active().unwrap().doc.layers[0].children().unwrap()[0].appearance.effects[0].params["color"].clone(); + for good in [ + json!("#00ff00"), + json!("#0f0"), + json!("00ff00"), + json!("#00ff00ff"), + json!("green"), + json!([0, 1, 0]), + json!({"model": "rgb", "r": 0.0, "g": 1.0, "b": 0.0}), + json!({"model": "cmyk", "c": 1.0, "m": 0.0, "y": 1.0, "k": 0.0}), + ] { + let params = json!({"index": 0, "params": {"color": good}}); + s.check_params("effect.setParams", ¶ms).unwrap_or_else(|e| panic!("{good}: {e}")); + s.execute("effect.setParams", ¶ms).unwrap_or_else(|e| panic!("{good}: {e}")); + assert_eq!(stored(&s), good); + } + let mut doc = (*s.active().unwrap().doc).clone(); + doc.node_mut(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params["color"] = json!("not-a-color"); + let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap(); + assert_eq!(back.layers[0].children().unwrap()[0].appearance.effects[0].params["color"], json!("not-a-color")); +} + /// What a command adds to its own journal entry passes the check, so a recorded journal can be /// sent back through the control channel or MCP. #[test] diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index 669532ef2..95fdabd23 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -604,26 +604,14 @@ fn command(d: &Dialog) -> (&'static str, Value) { fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let (cmd, p) = command(d); - if super::warp_effect::is_warp(&d.str("__effect")) { - let r = form::commit_preview(app, cmd, p.clone())?; - if cmd == "effect.apply" { - app.last_effect = Some((d.str("__effect"), p["params"].clone())); - } - return Ok(r); - } - let r = if app.session.in_interaction() { - // Live preview already applied: keep it (the interaction commits as one undo step). - let _ = app.session.preview(cmd, &p); - app.session.commit_interaction().map(|_| Value::Null).map_err(|e| e.to_string()) - } else { - app.run(cmd, p.clone()) - }; + // A live preview commits as one undo step. When the command refuses the values (a Color the + // effect can't draw), the dialog stays open and the error is returned. + let r = form::commit_preview(app, cmd, p.clone())?; // Last Effect repeats applied effects, not edits. if cmd == "effect.apply" { app.last_effect = Some((d.str("__effect"), p["params"].clone())); } - app.ui.dialog = None; - r + Ok(r) } #[cfg(test)] diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 18dbd2b3c..32ff56b2b 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -174,6 +174,27 @@ fn effect_preview_is_kept_as_one_undo_step() { assert_eq!(app.session.doc().unwrap().doc, before); } +/// Drop Shadow's Color typed as text the effect can't draw: OK returns the error and keeps the +/// dialog open, with Preview on or off, and the document stays as it was. +#[test] +fn effect_dialog_refuses_a_color_it_cannot_draw() { + for preview in [true, false] { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + app.run("select.all", json!({})).unwrap(); + let before = app.session.doc().unwrap().doc.clone(); + app.run("effect.dialog", json!({"effect": "stylize.dropShadow"})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("preview".into(), json!(preview)); + d.fields.insert("color".into(), json!("not-a-color")); + frame(&mut app, Default::default()); + let e = confirm(&mut app).unwrap_err(); + assert!(e.contains("`color` must be a #rrggbb color"), "preview {preview}: {e}"); + assert!(app.ui.dialog.is_some() && app.last_effect.is_none(), "preview {preview}"); + assert_eq!(app.session.doc().unwrap().doc, before, "preview {preview}"); + } +} + #[test] fn raster_filter_dialogs_preview_and_apply() { for (effect, key, value) in [ diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 5f93a3fd1..fe4f3457c 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3605,6 +3605,7 @@ axis Both И двете @msg `{id}` has no envelope dialog `{id}` няма диалогов прозорец за обвивка @msg `{keep}` is not a side of this axis `{keep}` не е страна на тази ос @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` трябва да бъде `auto` или езиков код като `en` или `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` трябва да бъде цвят във формат #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` трябва да бъде цвят във формат #rrggbb @msg `{key}` must be a number `{key}` трябва да бъде число @msg `{key}` must be a string `{key}` трябва да бъде низ diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 6ca2a3d9c..42decb52a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3976,6 +3976,7 @@ imageTrace Outline Obrys @msg `{id}` has no envelope dialog `{id}` nemá dialogové okno obálky @msg `{keep}` is not a side of this axis `{keep}` není strana této osy @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` musí být `auto` nebo kód jazyka, například `en` nebo `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` musí být barva #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` musí být barva #rrggbb @msg `{key}` must be a number `{key}` musí být číslo @msg `{key}` must be a string `{key}` musí být řetězec diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 9f7ee9465..e0aeefc7a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3579,6 +3579,7 @@ theme Appearance Erscheinungsbild @msg `{id}` has no envelope dialog `{id}` hat kein Hüllendialogfeld @msg `{keep}` is not a side of this axis `{keep}` ist keine Seite dieser Achse @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` muss `auto` oder ein Sprachcode wie `en` oder `zh-hant` sein +@msg `{key}` must be a #rrggbb color `{key}` muss eine Farbe der Form #rrggbb sein @msg `{key}` must be a #rrggbb colour `{key}` muss eine Farbe der Form #rrggbb sein @msg `{key}` must be a number `{key}` muss eine Zahl sein @msg `{key}` must be a string `{key}` muss eine Zeichenfolge sein diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 66977f54d..77168a212 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1988,6 +1988,7 @@ axis Both Και τα δύο @msg `{id}` has no envelope dialog Το `{id}` δεν έχει παράθυρο διαλόγου περιβλήματος @msg `{keep}` is not a side of this axis Το `{keep}` δεν είναι πλευρά αυτού του άξονα @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` Το `{key}` πρέπει να είναι `auto` ή κωδικός γλώσσας όπως `en` ή `zh-hant` +@msg `{key}` must be a #rrggbb color Το `{key}` πρέπει να είναι χρώμα #rrggbb @msg `{key}` must be a #rrggbb colour Το `{key}` πρέπει να είναι χρώμα #rrggbb @msg `{key}` must be a number Το `{key}` πρέπει να είναι αριθμός @msg `{key}` must be a string Το `{key}` πρέπει να είναι συμβολοσειρά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 1067afa8b..631f92491 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3676,6 +3676,7 @@ axis Both Ambos @msg `{id}` has no envelope dialog `{id}` no tiene cuadro de diálogo de envolvente @msg `{keep}` is not a side of this axis `{keep}` no es un lado de este eje @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` debe ser `auto` o un código de idioma como `en` o `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` debe ser un color #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` debe ser un color #rrggbb @msg `{key}` must be a number `{key}` debe ser un número @msg `{key}` must be a string `{key}` debe ser un texto diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index c357563e5..a2a954356 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3903,6 +3903,7 @@ imageTrace Outline Ääriviiva @msg `{id}` has no envelope dialog `{id}`: ei vääristyskuoren valintaikkunaa @msg `{keep}` is not a side of this axis `{keep}` ei ole tämän akselin puoli @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` on oltava `auto` tai kielikoodi, kuten `en` tai `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` on oltava #rrggbb-väri @msg `{key}` must be a #rrggbb colour `{key}` on oltava #rrggbb-väri @msg `{key}` must be a number `{key}` on oltava luku @msg `{key}` must be a string `{key}` on oltava merkkijono diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index e74016c88..536305f24 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3673,6 +3673,7 @@ axis Both Les deux @msg `{id}` has no envelope dialog `{id}` n’a pas de boîte de dialogue d’enveloppe @msg `{keep}` is not a side of this axis `{keep}` n’est pas un côté de cet axe @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` doit être `auto` ou un code de langue comme `en` ou `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` doit être une couleur #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` doit être une couleur #rrggbb @msg `{key}` must be a number `{key}` doit être un nombre @msg `{key}` must be a string `{key}` doit être une chaîne diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8e28c70a6..bbf81ef0d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4169,6 +4169,7 @@ imageTrace Outline Körvonal @msg `{id}` has no envelope dialog ennek nincs burkoló-párbeszédablaka: `{id}` @msg `{keep}` is not a side of this axis `{keep}` nem ennek a tengelynek az oldala @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}`: `auto` vagy nyelvkód kell, például `en` vagy `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}`: #rrggbb szín kell @msg `{key}` must be a #rrggbb colour `{key}`: #rrggbb szín kell @msg `{key}` must be a number `{key}`: szám kell @msg `{key}` must be a string `{key}`: szöveg kell diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index e781a57e6..5923a3518 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3679,6 +3679,7 @@ axis Both Entrambi @msg `{id}` has no envelope dialog `{id}` non ha una finestra di dialogo per l'involucro @msg `{keep}` is not a side of this axis `{keep}` non è un lato di questo asse @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` deve essere `auto` o un codice lingua come `en` o `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` deve essere un colore #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` deve essere un colore #rrggbb @msg `{key}` must be a number `{key}` deve essere un numero @msg `{key}` must be a string `{key}` deve essere una stringa diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e925da051..52e746d9e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3857,6 +3857,7 @@ swatch Bright Violet ビビッドバイオレット @msg no artboard アートボードがありません @msg installing from a path is not available on the web; pass the module as `dataBase64` Web 版ではファイルパスからはインストールできないため、モジュールは `dataBase64` で渡してください @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` は `auto` か、`en` や `zh-hant` などの言語コードにしてください +@msg `{key}` must be a #rrggbb color `{key}` は #rrggbb 形式のカラーにしてください @msg `{key}` must be a #rrggbb colour `{key}` は #rrggbb 形式のカラーにしてください @msg `{key}` must be a string `{key}` は文字列にしてください @msg `{key}` must be a whole number `{key}` は整数にしてください diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index c22818da9..6b1c35f4c 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3789,6 +3789,7 @@ theme Appearance 외관 @msg no artboard 아트보드가 없습니다 @msg installing from a path is not available on the web; pass the module as `dataBase64` 웹에서는 경로로 설치할 수 없습니다. 모듈을 `dataBase64`로 전달하세요 @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}`은(는) `auto` 또는 `en`, `zh-hant` 같은 언어 코드여야 합니다 +@msg `{key}` must be a #rrggbb color `{key}`은(는) #rrggbb 색상이어야 합니다 @msg `{key}` must be a #rrggbb colour `{key}`은(는) #rrggbb 색상이어야 합니다 @msg `{key}` must be a string `{key}`은(는) 문자열이어야 합니다 @msg `{key}` must be a whole number `{key}`은(는) 정수여야 합니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 42801725d..412463bf0 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3791,6 +3791,7 @@ imageTrace Outline Kontur @msg `{id}` has no envelope dialog `{id}` nie ma okna dialogowego koperty @msg `{keep}` is not a side of this axis `{keep}` nie jest stroną tej osi @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` musi mieć wartość `auto` lub kod języka, np. `en` albo `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` musi być kolorem #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` musi być kolorem #rrggbb @msg `{key}` must be a number `{key}` musi być liczbą @msg `{key}` must be a string `{key}` musi być ciągiem znaków diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index ea126556b..6b4bbca09 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3275,6 +3275,7 @@ axis Both Ambos @msg `{id}` has no envelope dialog `{id}` não tem caixa de diálogo de envelope @msg `{keep}` is not a side of this axis `{keep}` não é um lado deste eixo @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` deve ser `auto` ou um código de idioma como `en` ou `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` deve ser uma cor #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` deve ser uma cor #rrggbb @msg `{key}` must be a number `{key}` deve ser um número @msg `{key}` must be a string `{key}` deve ser uma string diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c9c9a4055..a6586cc69 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3675,6 +3675,7 @@ axis Both Оба @msg `{id}` has no envelope dialog `{id}` не имеет диалогового окна оболочки @msg `{keep}` is not a side of this axis `{keep}` не является стороной этой оси @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` должно быть `auto` или кодом языка, например `en` или `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` должно быть цветом #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` должно быть цветом #rrggbb @msg `{key}` must be a number `{key}` должно быть числом @msg `{key}` must be a string `{key}` должно быть строкой diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index f182c945a..b52320ed5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3605,6 +3605,7 @@ axis Both Båda @msg `{id}` has no envelope dialog `{id}` har ingen dialogruta för kuvert @msg `{keep}` is not a side of this axis `{keep}` är inte en sida av den här axeln @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` måste vara `auto` eller en språkkod som `en` eller `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` måste vara en färg i formatet #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` måste vara en färg i formatet #rrggbb @msg `{key}` must be a number `{key}` måste vara ett tal @msg `{key}` must be a string `{key}` måste vara en sträng diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index a17831fbe..c06c193ff 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -1251,6 +1251,20 @@ fn unknown_parameter_errors_are_translated() { } } +/// The error for an effect `color` the effect can't draw, whose text the scan above doesn't read, +/// is translated whole in each language of [`COMPLETE_MESSAGES`] and in Bulgarian. +#[test] +fn effect_color_param_error_is_translated() { + use serde_json::json; + let p = json!({"effect": "stylize.dropShadow", "params": {"color": "not-a-color"}}); + let e = vectorcraft_engine::check_params("effect.apply", &p).unwrap_err(); + let vectorcraft_engine::EngineError::BadParams { msg, .. } = &e else { panic!("{e}") }; + for code in COMPLETE_MESSAGES.iter().chain(&["bg"]) { + let shown = message(Lang::from_code(code).unwrap(), &e.to_string()); + assert!(!shown.contains(msg.as_str()) && shown.contains("`color`") && shown.contains("effect.apply"), "{code}: {shown}"); + } +} + /// The notes of an EPS or `.ai` whose layers weren't read, which the importer pushes as warnings /// (the scan above doesn't read them), are translated whole, reason included, as the status bar /// shows them after opening (#2035). diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a5299733a..668669767 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3663,6 +3663,7 @@ axis Both Обидві @msg `{id}` has no envelope dialog `{id}` не має діалогового вікна оболонки @msg `{keep}` is not a side of this axis `{keep}` не є стороною цієї осі @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` має бути `auto` або кодом мови, наприклад `en` або `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` має бути кольором #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` має бути кольором #rrggbb @msg `{key}` must be a number `{key}` має бути числом @msg `{key}` must be a string `{key}` має бути рядком diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 149cb4702..899e9aff9 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3800,6 +3800,7 @@ theme Appearance Giao diện @msg `{id}` has no envelope dialog `{id}` không có hộp thoại Envelope @msg `{keep}` is not a side of this axis `{keep}` không phải là một cạnh của trục này @msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` phải là `auto` hoặc mã ngôn ngữ như `en` hoặc `zh-hant` +@msg `{key}` must be a #rrggbb color `{key}` phải là màu #rrggbb @msg `{key}` must be a #rrggbb colour `{key}` phải là màu #rrggbb @msg `{key}` must be a number `{key}` phải là một số @msg `{key}` must be a string `{key}` phải là một chuỗi diff --git a/docs/mcp.md b/docs/mcp.md index ea9386749..9fc88c525 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -815,13 +815,29 @@ is refused the same way: → invalid parameters for `effect.apply`: unknown parameter `blurr`; the command takes: radius ``` +The `color` of Drop Shadow, Inner Glow, Outer Glow and Shift to Color must be one the effect can read: a hex color +(`#rrggbb`, `#rgb` or `#rrggbbaa`, with or without the `#`), `black`, `white`, `red`, `green` or `blue`, `[r, g, b]` +(three numbers, each read on a 0 to 1 scale), or a color object (`{"model": "rgb", "r": 1, "g": 0, "b": 0}`). +`effect.apply` and `effect.setParams` refuse any other value themselves (where the parameter check runs first, a number +or a boolean is refused as not a string), so the app's effect dialogs and Appearance panel, a step of +`command.batch` and a replayed journal get the same error, and the document and its undo history stay as they were: + +```json +{"id": "effect.setParams", "params": {"index": 0, "params": {"color": "#ff00"}}} +→ invalid parameters for `effect.setParams`: `color` must be a #rrggbb color +``` + +A document saved with such a color opens as before, and the effect draws its default color (an array of three or more +entries reads an entry that isn't a number as 0). + A number outside the range the effect documents is still stored as given and clamped when the effect draws. A plug-in effect's parameters are resolved against its manifest instead, and `effect.setParams` as a step of -`command.batch` isn't checked (the effect it edits may only exist once the steps before it ran). A description with a `…` entry takes more keys than it lists: those of the command it names +`command.batch` isn't checked before the batch runs (the effect it edits may only exist once the steps before it ran); +the command still refuses an effect `color` it can't read. A description with a `…` entry takes more keys than it lists: those of the command it names (`…settings as file.new`), or any key when it names none, as `document.export` and `document.save` do for the options of the format they write (one object can carry several formats' options). A command that takes its own reply back (`document.setup`, `text.areaOptions`) says so the same way. The app's own journal isn't checked: an action or -journal recorded by an older version replays as it did. +journal recorded by an older version replays as it did, except for an effect `color` that the effect can't read. 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