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