From d078e13ad8f76cecc0fadce3d37d48595daddeaf Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 16:42:12 -0800 Subject: [PATCH] Plug-ins: painted live effects refuse gradient stop plates without a spot link A gradient stop's process plates print beside the tint of the spot swatch that the stop itself links to. A painted live effect could return a stop with `plates` and no link, or a link to a process swatch or Registration, and the result was drawn with plates that no plate ever prints. - The painted-output decoder refuses `plates` on a stop with no `swatch` (absent or null). The gradient's own `swatch` does not link its stops. - The reference check refuses `plates` on a stop linked to a swatch that is not a spot swatch. It runs again on a cached result, so a result is refused once its swatch stops being a spot swatch. A missing swatch keeps the error that names the swatch. - Either fault refuses the whole result, and `lastError` names the stop's `plates`. Plates on a stop linked to a spot swatch, all-zero ones included, are kept. Filter and importer output keep clamping and unlinking plates as before. Docs: docs/plugins.md, docs/target-app-parity.md. The new message has a row in every complete message catalog. --- crates/plugins/src/effect.rs | 38 ++++++++++++ crates/plugins/src/painted.rs | 29 +++++++-- crates/plugins/src/painted/tests.rs | 94 +++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.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/hi.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/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.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/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + docs/plugins.md | 3 +- docs/target-app-parity.md | 1 + 29 files changed, 182 insertions(+), 7 deletions(-) diff --git a/crates/plugins/src/effect.rs b/crates/plugins/src/effect.rs index 6d549ed070..02b212cb23 100644 --- a/crates/plugins/src/effect.rs +++ b/crates/plugins/src/effect.rs @@ -620,6 +620,44 @@ mod tests { registry::remove(ID); } + /// A cached result whose gradient stop prints `plates` beside a spot swatch is refused once that + /// swatch is no longer a spot swatch, and used again when it is one again. + #[test] + fn cached_painted_plates_refuse_after_spot_becomes_process() { + let _lock = crate::safe_mode::TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + crate::safe_mode::set_active(None); + const ID: &str = "test.effect.spotplates"; + let manifest = format!(r#"{{"id":"{ID}","name":"Test","version":"1","kind":"effect","api":2,"paintOutput":true,"params":{{}}}}"#); + let gray = json!({"model": "gray", "k": 0}); + let stops = json!([{"offset": 0, "color": gray}, {"offset": 1, "color": gray, "swatch": "Ink", "plates": [0.1, 0.2, 0.3, 0.4]}]); + let fill = json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": stops}}); + let square = PathData::single(SubPath::polyline(&[(0.0, 0.0), (9.0, 0.0), (9.0, 9.0)].map(|(x, y)| Point::new(x, y)), true)); + let output = json!({"objects": [{"path": square, "fills": [fill]}]}).to_string(); + let src = crate::wat::with_imports(&manifest, "", "", &output); + registry::install(crate::Plugin::load(&wat::parse_str(&src).unwrap(), Limits::default()).unwrap()).unwrap(); + let with_ink = |spot: bool| { + let mut doc = Document::new(100.0, 100.0); + doc.swatches.push(vectorcraft_color::Swatch { + name: "Ink".into(), + paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), + global: true, + spot, + }); + doc + }; + let (spot, process) = (with_ink(true), with_ink(false)); + let path = line(2); + let draw = + |doc: &Document| apply_painted(ID, &json!({}), &path, Rect::new(0.0, 0.0, 1.0, 1.0), FillRule::NonZero, Paints::default(), Some(doc)); + assert!(draw(&spot).is_some()); + assert_eq!(last_error(ID), None); + assert!(draw(&process).is_none(), "from the cache, refused"); + let e = last_error(ID).unwrap(); + assert!(e.contains("objects[0].fills[0].gradient.stops[1].plates: `plates` requires a stop linked to a spot swatch"), "{e}"); + assert!(draw(&spot).is_some(), "from the cache, used again"); + registry::remove(ID); + } + /// A clock that reads `step` milliseconds later each time it is read. fn stepping(step: u64) -> crate::deferred::Clock { let t = Arc::new(std::sync::atomic::AtomicU64::new(0)); diff --git a/crates/plugins/src/painted.rs b/crates/plugins/src/painted.rs index 5b58318073..484e761677 100644 --- a/crates/plugins/src/painted.rs +++ b/crates/plugins/src/painted.rs @@ -7,8 +7,9 @@ //! An effect on a fill may return only `fills`, one on a stroke only `strokes`. //! //! The result is checked whole before any of it is used ([`decode`]): every number finite and in -//! range (nothing is clamped), every key known, and every swatch and pattern a paint names in the -//! document ([`check_references`]). One fault refuses the whole result, naming the field. +//! range (nothing is clamped), every key known, a gradient stop's `plates` only on a stop linked to +//! a spot swatch, and every swatch and pattern a paint names in the document ([`check_references`]). +//! One fault refuses the whole result, naming the field. use serde_json::Value; use vectorcraft_color::{Color, Paint}; @@ -236,7 +237,8 @@ fn paint_keys(v: &Value, at: &str) -> Result<()> { } /// A paint as the filter form has it (`null`: none), checked rather than clamped: colors, tints, -/// stops, opacities, midpoints and placements in range. +/// stops, opacities, midpoints and placements in range, and a gradient stop's `plates` only on a +/// stop linked to a swatch ([`check_references`] checks that the swatch is a spot swatch). fn paint(v: &Value, at: &str) -> Result { if v.is_null() { return Ok(Paint::None); @@ -265,6 +267,11 @@ fn paint(v: &Value, at: &str) -> Result { for x in s.plates.iter().flatten() { unit(*x, &format!("{at}.plates"), "a plate")?; } + // Plates print beside the tint of the spot swatch the stop itself links to; a + // gradient's own `swatch` does not link its stops. + if s.plates.is_some() && s.swatch.is_none() { + return Err(out_err(&format!("{at}.plates"), "`plates` requires a stop linked to a spot swatch")); + } } if stops.windows(2).any(|w| w[1].offset < w[0].offset) { return Err(out_err(&format!("{at}.gradient.stops"), "stops must be in order of `offset`")); @@ -308,8 +315,9 @@ fn paint(v: &Value, at: &str) -> Result { } /// Checks that every swatch a solid paint, gradient or gradient stop of `pieces` links to is a -/// swatch of `doc`, and every pattern a pattern paint names is one of its patterns. Without a -/// document (`None`), any swatch or pattern a piece names is refused. +/// swatch of `doc`, that a gradient stop with `plates` links to a spot swatch, and every pattern a +/// pattern paint names is one of its patterns. Without a document (`None`), any swatch or pattern a +/// piece names is refused. pub fn check_references(pieces: &[Piece], doc: Option<&Document>) -> Result<()> { let swatch = |name: &Option, at: &str| match name { Some(n) if doc.and_then(|d| d.swatch(n)).is_none() => Err(out_err(at, format!("the document has no swatch {n:?}"))), @@ -321,7 +329,16 @@ pub fn check_references(pieces: &[Piece], doc: Option<&Document>) -> Result<()> Paint::Solid { swatch: s, .. } => swatch(s, &format!("{at}.swatch")), Paint::Gradient(g) => { swatch(&g.swatch, &format!("{at}.swatch"))?; - g.gradient.stops.iter().enumerate().try_for_each(|(i, s)| swatch(&s.swatch, &format!("{at}.gradient.stops[{i}].swatch"))) + g.gradient.stops.iter().enumerate().try_for_each(|(i, s)| { + let at = format!("{at}.gradient.stops[{i}]"); + swatch(&s.swatch, &format!("{at}.swatch"))?; + match s.swatch.as_deref().and_then(|n| doc.and_then(|d| d.swatch(n))) { + Some(w) if s.plates.is_some() && !w.spot => { + Err(out_err(&format!("{at}.plates"), "`plates` requires a stop linked to a spot swatch")) + } + _ => Ok(()), + } + }) } Paint::Pattern { pattern, .. } if doc.and_then(|d| d.pattern(pattern)).is_none() => { Err(out_err(&format!("{at}.pattern"), format!("the document has no pattern {pattern:?}"))) diff --git a/crates/plugins/src/painted/tests.rs b/crates/plugins/src/painted/tests.rs index 3ed6dd653e..59acf18521 100644 --- a/crates/plugins/src/painted/tests.rs +++ b/crates/plugins/src/painted/tests.rs @@ -239,3 +239,97 @@ fn named_swatches_and_patterns_must_exist() { let e = check_references(&pieces(json!([{"path": square(), "fills": [{"type": "pattern", "pattern": "Dots"}]}])), None).unwrap_err().to_string(); assert!(e.contains("objects[0].fills[0].pattern"), "{e}"); } + +/// A gradient of two stops: a plain first one, then `stop`, with `extra` keys on the paint. +fn ramp(stop: Value, extra: Value) -> Value { + let mut g = json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": [{"offset": 0, "color": {"model": "gray", "k": 0}}, stop]}}); + g.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + g +} + +/// A second stop with process `plates`, and `extra` keys. +fn plated(extra: Value) -> Value { + let mut s = json!({"offset": 1, "color": {"model": "gray", "k": 1}, "plates": [0.1, 0.2, 0.3, 0.4]}); + s.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + s +} + +/// A gradient stop's process `plates` need the stop's own link to a swatch: a stop without one +/// (no `swatch` or a `null` one) refuses the whole result, naming the stop's `plates`, in a fill or +/// a stroke at every scope. +#[test] +fn unlinked_gradient_stop_plates_refuse_the_whole_painted_result() { + const WHY: &str = "`plates` requires a stop linked to a spot swatch"; + for orphan in [ramp(plated(json!({})), json!({})), ramp(plated(json!({"swatch": null})), json!({}))] { + let fills = out(json!([{"path": square(), "fills": [red()]}, {"path": square(), "fills": [red(), orphan]}])); + for scope in [Scope::Object, Scope::Fill] { + let e = decode(&fills, scope, FillRule::NonZero).unwrap_err(); + assert_eq!(e.to_string(), format!("plug-in output: objects[1].fills[1].gradient.stops[1].plates: {WHY}"), "{scope:?}"); + } + let strokes = out(json!([{"path": square(), "strokes": [{"paint": red()}]}, {"path": square(), "strokes": [{"paint": orphan, "width": 2}]}])); + for scope in [Scope::Object, Scope::Stroke] { + let e = decode(&strokes, scope, FillRule::NonZero).unwrap_err(); + assert_eq!(e.to_string(), format!("plug-in output: objects[1].strokes[0].paint.gradient.stops[1].plates: {WHY}"), "{scope:?}"); + } + } + // `plugin.info`'s `lastError` names the plug-in before it. + let e = decode(&out(json!([{"path": square(), "fills": [ramp(plated(json!({})), json!({}))]}])), Scope::Object, FillRule::NonZero).unwrap_err(); + assert_eq!(named("Test Tone", &e), format!("Test Tone: plug-in output: objects[0].fills[0].gradient.stops[1].plates: {WHY}")); + // A plate out of range keeps its own error. + let wide = ramp(json!({"offset": 1, "color": {"model": "gray", "k": 1}, "plates": [2, 0, 0, 0]}), json!({})); + let e = decode(&out(json!([{"path": square(), "fills": [wide]}])), Scope::Object, FillRule::NonZero).unwrap_err().to_string(); + assert!(e.contains("objects[0].fills[0].gradient.stops[1].plates: a plate must be from 0 to 1"), "{e}"); +} + +/// The gradient's own `swatch` does not link its stops, so it does not admit a stop's `plates`. +#[test] +fn gradient_swatch_does_not_link_its_stops_plates() { + let fill = ramp(plated(json!({})), json!({"swatch": "Ink"})); + let e = decode(&out(json!([{"path": square(), "fills": [fill]}])), Scope::Object, FillRule::NonZero).unwrap_err().to_string(); + assert!(e.contains("objects[0].fills[0].gradient.stops[1].plates: `plates` requires a stop linked to a spot swatch"), "{e}"); +} + +/// A stop's `plates` beside a link to a swatch that is not a spot swatch (a process swatch or +/// Registration) refuse the result at the reference check; a link to a missing swatch keeps the +/// error that names the swatch. +#[test] +fn process_swatch_cannot_back_stop_plates() { + let mut doc = Document::new(100.0, 100.0); + doc.swatches.push(Swatch { name: "Ink".into(), paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), global: true, spot: true }); + doc.swatches.push(Swatch { name: "Process".into(), paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), global: true, spot: false }); + let check = |pieces: Value| check_references(&decode(&out(pieces), Scope::Object, FillRule::NonZero).unwrap(), Some(&doc)); + for name in ["Process", vectorcraft_color::swatch::REGISTRATION] { + let fill = ramp(plated(json!({"swatch": name})), json!({})); + let e = check(json!([{"path": square(), "fills": [fill.clone()]}])).unwrap_err().to_string(); + assert!(e.contains("objects[0].fills[0].gradient.stops[1].plates: `plates` requires a stop linked to a spot swatch"), "{name}: {e}"); + let e = check(json!([{"path": square()}, {"path": square(), "strokes": [{"paint": fill}]}])).unwrap_err().to_string(); + assert!(e.contains("objects[1].strokes[0].paint.gradient.stops[1].plates:"), "{name}: {e}"); + // Without `plates`, a stop may link to any swatch of the document. + let plain = ramp(json!({"offset": 1, "color": {"model": "gray", "k": 1}, "swatch": name}), json!({})); + assert!(check(json!([{"path": square(), "fills": [plain]}])).is_ok(), "{name}"); + } + let gone = ramp(plated(json!({"swatch": "Gone"})), json!({})); + let e = check(json!([{"path": square(), "fills": [gone]}])).unwrap_err().to_string(); + assert!(e.contains("objects[0].fills[0].gradient.stops[1].swatch:"), "{e}"); +} + +/// A stop linked to a spot swatch keeps its `plates` as given, all-zero ones included, through the +/// reference check and the swatch colors. +#[test] +fn linked_spot_stop_plates_are_kept() { + let mut doc = Document::new(100.0, 100.0); + doc.swatches.push(Swatch { name: "Ink".into(), paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), global: true, spot: true }); + let zero = json!({"offset": 0, "color": {"model": "gray", "k": 0}, "swatch": "Ink", "plates": [0, 0, 0, 0]}); + let fill = json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": [zero, plated(json!({"swatch": "Ink"}))]}}); + let stroke = ramp(plated(json!({"swatch": "Ink"})), json!({})); + let pieces = + decode(&out(json!([{"path": square(), "fills": [fill], "strokes": [{"paint": stroke}]}])), Scope::Object, FillRule::NonZero).unwrap(); + assert!(check_references(&pieces, Some(&doc)).is_ok()); + let pieces = with_swatch_colors(std::sync::Arc::new(pieces), &doc); + let plates = |p: &Paint| match p { + Paint::Gradient(g) => g.gradient.stops.iter().map(|s| s.plates).collect::>(), + _ => panic!("{p:?}"), + }; + assert_eq!(plates(&pieces[0].fills.as_ref().unwrap()[0]), [Some([0.0; 4]), Some([0.1, 0.2, 0.3, 0.4])]); + assert_eq!(plates(&pieces[0].strokes.as_ref().unwrap()[0].paint), [None, Some([0.1, 0.2, 0.3, 0.4])]); +} diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ab50e14a58..c9dc8ae5ae 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6604,6 +6604,7 @@ digits Malayalam Malaiàlam @msg stops must be in order of `offset` les parades han d’estar en ordre d’`offset` @msg the document has no pattern {pattern} el document no té el motiu {pattern} @msg the document has no swatch {n} el document no té la mostra {n} +@msg `plates` requires a stop linked to a spot swatch `plates` requereix una parada vinculada a una mostra de color directe @msg the output la sortida @msg the path has no anchor points el traçat no té punts d’ancoratge @msg too many anchor points (limit {MAX_ANCHORS}) massa punts d’ancoratge (límit {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3b9c300980..c6ae415283 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6498,6 +6498,7 @@ axis Y Axis osa Y @msg stops must be in order of `offset` zarážky musí být seřazeny podle `offset` @msg the document has no pattern {pattern} dokument nemá vzorek {pattern} @msg the document has no swatch {n} dokument nemá vzorek barvy {n} +@msg `plates` requires a stop linked to a spot swatch `plates` vyžaduje zarážku propojenou se vzorkem přímé barvy @msg the output výstup @msg the path has no anchor points cesta nemá žádné kotevní body @msg too many anchor points (limit {MAX_ANCHORS}) příliš mnoho kotevních bodů (limit {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 59ce36d15f..7fa33efd42 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -478,6 +478,7 @@ @msg stops must be in order of `offset` farvestoppene skal stå i rækkefølge efter `offset` @msg the document has no pattern {pattern} dokumentet har intet mønster {pattern} @msg the document has no swatch {n} dokumentet har ingen farveprøve {n} +@msg `plates` requires a stop linked to a spot swatch `plates` kræver et gradientstop, der er knyttet til en spotfarveprøve @msg the output outputtet @msg the path has no anchor points kurven har ingen ankerpunkter @msg too many anchor points (limit {MAX_ANCHORS}) for mange ankerpunkter (grænse {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ddc01a8016..fa97c88997 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -6090,6 +6090,7 @@ axis Y Axis Y-Achse @msg stops must be in order of `offset` die Farbstopps müssen nach `offset` geordnet sein @msg the document has no pattern {pattern} das Dokument hat kein Muster {pattern} @msg the document has no swatch {n} das Dokument hat kein Farbfeld {n} +@msg `plates` requires a stop linked to a spot swatch `plates` erfordert einen Farbstopp, der mit einem Volltonfarbfeld verknüpft ist @msg the output die Ausgabe @msg the path has no anchor points der Pfad hat keine Ankerpunkte @msg too many anchor points (limit {MAX_ANCHORS}) zu viele Ankerpunkte (Grenze {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index a89d3c72e4..02cddf1577 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6451,6 +6451,7 @@ axis Y Axis Άξονας Y @msg stops must be in order of `offset` τα σημεία χρώματος πρέπει να είναι ταξινομημένα κατά `offset` @msg the document has no pattern {pattern} το έγγραφο δεν έχει μοτίβο {pattern} @msg the document has no swatch {n} το έγγραφο δεν έχει δείγμα χρώματος {n} +@msg `plates` requires a stop linked to a spot swatch το `plates` απαιτεί σημείο χρώματος συνδεδεμένο με δείγμα πλακάτου χρώματος @msg the output η έξοδος @msg the path has no anchor points η διαδρομή δεν έχει σημεία αγκύρωσης @msg too many anchor points (limit {MAX_ANCHORS}) πάρα πολλά σημεία αγκύρωσης (όριο {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4a912f8005..351257e8ae 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6442,6 +6442,7 @@ axis Y Axis Eje Y @msg stops must be in order of `offset` las paradas deben estar en orden de `offset` @msg the document has no pattern {pattern} el documento no tiene el motivo {pattern} @msg the document has no swatch {n} el documento no tiene la muestra {n} +@msg `plates` requires a stop linked to a spot swatch `plates` requiere una parada vinculada a una muestra de tinta plana @msg the output la salida @msg the path has no anchor points el trazado no tiene puntos de ancla @msg too many anchor points (limit {MAX_ANCHORS}) demasiados puntos de ancla (límite {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 06499d13ea..18e53b41bc 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6529,6 +6529,7 @@ axis Y Axis Y-akseli @msg stops must be in order of `offset` väripisteiden on oltava `offset`-järjestyksessä @msg the document has no pattern {pattern} asiakirjassa ei ole kuviota {pattern} @msg the document has no swatch {n} asiakirjassa ei ole värimallia {n} +@msg `plates` requires a stop linked to a spot swatch `plates` vaatii väripisteen, joka on linkitetty spottivärin värimalliin @msg the output tuloste @msg the path has no anchor points polulla ei ole ankkuripisteitä @msg too many anchor points (limit {MAX_ANCHORS}) liian monta ankkuripistettä (raja {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 90882cf105..4a4dff58e5 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6493,6 +6493,7 @@ axis Y Axis Axe Y @msg stops must be in order of `offset` les étapes doivent être classées par `offset` @msg the document has no pattern {pattern} le document n’a pas de motif {pattern} @msg the document has no swatch {n} le document n’a pas de nuance {n} +@msg `plates` requires a stop linked to a spot swatch `plates` exige une étape liée à une nuance de ton direct @msg the output la sortie @msg the path has no anchor points le tracé n’a aucun point d’ancrage @msg too many anchor points (limit {MAX_ANCHORS}) trop de points d’ancrage (limite {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 671052e82b..2bb6b50079 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -5654,6 +5654,7 @@ antiAlias Sharp शार्प @msg stops must be in order of `offset` स्टॉप `offset` के क्रम में होने चाहिए @msg the document has no pattern {pattern} डॉक्यूमेंट में पैटर्न {pattern} नहीं है @msg the document has no swatch {n} डॉक्यूमेंट में स्वॉच {n} नहीं है +@msg `plates` requires a stop linked to a spot swatch `plates` के लिए स्पॉट रंग स्वॉच से जुड़ा स्टॉप आवश्यक है @msg the output आउटपुट @msg the path has no anchor points पाथ में कोई एंकर पॉइंट नहीं है @msg too many anchor points (limit {MAX_ANCHORS}) बहुत अधिक एंकर पॉइंट (सीमा {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a218a2ce15..12c5981f8f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6481,6 +6481,7 @@ axis Y Axis Y tengely @msg stops must be in order of `offset` a színátmenet-pontok `offset` szerint rendezve legyenek @msg the document has no pattern {pattern} a dokumentumban nincs {pattern} mintázat @msg the document has no swatch {n} a dokumentumban nincs {n} színminta +@msg `plates` requires a stop linked to a spot swatch a `plates` direktszín-színmintához kapcsolt színátmenet-pontot igényel @msg the output a kimenet @msg the path has no anchor points az útvonalnak nincsenek horgonypontjai @msg too many anchor points (limit {MAX_ANCHORS}) túl sok horgonypont (korlát: {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2b5a6bf3da..450c97a877 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -6124,6 +6124,7 @@ axis Y Axis Asse Y @msg stops must be in order of `offset` i cursori colore devono essere in ordine di `offset` @msg the document has no pattern {pattern} il documento non ha il motivo {pattern} @msg the document has no swatch {n} il documento non ha il campione {n} +@msg `plates` requires a stop linked to a spot swatch `plates` richiede un cursore colore collegato a un campione di colore spot @msg the output l’output @msg the path has no anchor points il tracciato non ha punti di ancoraggio @msg too many anchor points (limit {MAX_ANCHORS}) troppi punti di ancoraggio (limite {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 87ec6c8b05..0e0b4dd782 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6190,6 +6190,7 @@ axis Y Axis Y 軸 @msg stops must be in order of `offset` 分岐点は `offset` の順に並べてください @msg the document has no pattern {pattern} ドキュメントにパターン {pattern} がありません @msg the document has no swatch {n} ドキュメントにスウォッチ {n} がありません +@msg `plates` requires a stop linked to a spot swatch `plates` には特色スウォッチにリンクされた分岐点が必要です @msg the output 出力 @msg the path has no anchor points パスにアンカーポイントがありません @msg too many anchor points (limit {MAX_ANCHORS}) アンカーポイントが多すぎます (上限 {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8c42eafaf1..7a016381d2 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6450,6 +6450,7 @@ axis Y Axis Y ღერძი @msg stops must be in order of `offset` გადასვლის წერტილები `offset`-ის მიხედვით უნდა იყოს დალაგებული @msg the document has no pattern {pattern} დოკუმენტს არ აქვს პატერნი {pattern} @msg the document has no swatch {n} დოკუმენტს არ აქვს ფერის ნიმუში {n} +@msg `plates` requires a stop linked to a spot swatch `plates` მოითხოვს გადასვლის წერტილს, რომელიც სპოტ-ფერის ნიმუშთანაა დაკავშირებული @msg the output გამოტანა @msg the path has no anchor points ტრაექტორიას საყრდენი წერტილები არ აქვს @msg too many anchor points (limit {MAX_ANCHORS}) ძალიან ბევრი საყრდენი წერტილი (ლიმიტი {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4fd8001ea7..642ab0928e 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -6085,6 +6085,7 @@ axis Y Axis Y축 @msg stops must be in order of `offset` 정지점은 `offset` 순서여야 합니다 @msg the document has no pattern {pattern} 문서에 패턴 {pattern}이(가) 없습니다 @msg the document has no swatch {n} 문서에 견본 {n}이(가) 없습니다 +@msg `plates` requires a stop linked to a spot swatch `plates`에는 별색 견본에 연결된 정지점이 필요합니다 @msg the output 출력 @msg the path has no anchor points 패스에 앵커 포인트가 없습니다 @msg too many anchor points (limit {MAX_ANCHORS}) 앵커 포인트가 너무 많습니다(한도 {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 31872f060e..99e720f0e8 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -374,6 +374,7 @@ @msg stops must be in order of `offset` kleurstops moeten op volgorde van `offset` staan @msg the document has no pattern {pattern} het document heeft geen patroon {pattern} @msg the document has no swatch {n} het document heeft geen staal {n} +@msg `plates` requires a stop linked to a spot swatch `plates` vereist een verloopstop die aan een steunkleurstaal is gekoppeld @msg the output de uitvoer @msg the path has no anchor points het pad heeft geen ankerpunten @msg too many anchor points (limit {MAX_ANCHORS}) te veel ankerpunten (limiet {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 77b1425e5f..44e5e983eb 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6353,6 +6353,7 @@ axis Y Axis Oś Y @msg stops must be in order of `offset` punkty koloru muszą być uporządkowane według `offset` @msg the document has no pattern {pattern} dokument nie ma wzorku {pattern} @msg the document has no swatch {n} dokument nie ma próbki {n} +@msg `plates` requires a stop linked to a spot swatch `plates` wymaga punktu koloru połączonego z próbką koloru dodatkowego @msg the output wyjście @msg the path has no anchor points ścieżka nie ma punktów kontrolnych @msg too many anchor points (limit {MAX_ANCHORS}) za dużo punktów kontrolnych (limit {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index caed79af2a..888d948b8c 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6413,6 +6413,7 @@ axis Y Axis Eixo Y @msg stops must be in order of `offset` as paradas devem estar em ordem de `offset` @msg the document has no pattern {pattern} o documento não tem o padrão {pattern} @msg the document has no swatch {n} o documento não tem a amostra {n} +@msg `plates` requires a stop linked to a spot swatch `plates` requer uma parada vinculada a uma amostra de cor spot @msg the output a saída @msg the path has no anchor points o caminho não tem pontos de ancoragem @msg too many anchor points (limit {MAX_ANCHORS}) pontos de ancoragem demais (limite {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 206b209ee2..05f8dff02f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6110,6 +6110,7 @@ axis Y Axis Ось Y @msg stops must be in order of `offset` точки перехода должны идти в порядке `offset` @msg the document has no pattern {pattern} в документе нет узора {pattern} @msg the document has no swatch {n} в документе нет образца {n} +@msg `plates` requires a stop linked to a spot swatch для `plates` нужна точка перехода, связанная с образцом плашечного цвета @msg the output вывод @msg the path has no anchor points у контура нет опорных точек @msg too many anchor points (limit {MAX_ANCHORS}) слишком много опорных точек (предел {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 82e63d1667..a80d7d0197 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6483,6 +6483,7 @@ axis Y Axis Y-axel @msg stops must be in order of `offset` stoppen måste vara ordnade efter `offset` @msg the document has no pattern {pattern} dokumentet har inget mönster {pattern} @msg the document has no swatch {n} dokumentet har ingen färgruta {n} +@msg `plates` requires a stop linked to a spot swatch `plates` kräver ett gradientstopp som är länkat till en dekorfärgruta @msg the output utdata @msg the path has no anchor points banan har inga ankarpunkter @msg too many anchor points (limit {MAX_ANCHORS}) för många ankarpunkter (gräns {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 237694bff5..80f7b33d75 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6573,6 +6573,7 @@ axis Y Axis Y ekseni @msg stops must be in order of `offset` duraklar `offset` sırasında olmalıdır @msg the document has no pattern {pattern} belgede {pattern} deseni yok @msg the document has no swatch {n} belgede {n} renk örneği yok +@msg `plates` requires a stop linked to a spot swatch `plates`, nokta rengi örneğine bağlı bir degrade durağı gerektirir @msg the output çıktı @msg the path has no anchor points yolun bağlantı noktası yok @msg too many anchor points (limit {MAX_ANCHORS}) çok fazla bağlantı noktası (sınır {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a74a03529d..f17d774b69 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -6104,6 +6104,7 @@ axis Y Axis Вісь Y @msg stops must be in order of `offset` точки переходу мають іти в порядку `offset` @msg the document has no pattern {pattern} у документі немає візерунка {pattern} @msg the document has no swatch {n} у документі немає зразка {n} +@msg `plates` requires a stop linked to a spot swatch для `plates` потрібна точка переходу, пов’язана зі зразком плашкового кольору @msg the output виведення @msg the path has no anchor points контур не має опорних точок @msg too many anchor points (limit {MAX_ANCHORS}) забагато опорних точок (межа {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 16c613874c..8d43a672c8 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -6106,6 +6106,7 @@ axis Y Axis Trục Y @msg stops must be in order of `offset` các điểm dừng phải theo thứ tự của `offset` @msg the document has no pattern {pattern} tài liệu không có họa tiết {pattern} @msg the document has no swatch {n} tài liệu không có mẫu màu {n} +@msg `plates` requires a stop linked to a spot swatch `plates` cần một điểm dừng chuyển sắc liên kết với mẫu màu đặc biệt @msg the output đầu ra @msg the path has no anchor points đường dẫn không có điểm neo @msg too many anchor points (limit {MAX_ANCHORS}) quá nhiều điểm neo (giới hạn {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b83bee650f..4943f3fe60 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6337,6 +6337,7 @@ axis Y Axis Y 轴 @msg stops must be in order of `offset` 色标必须按 `offset` 排序 @msg the document has no pattern {pattern} 文档中没有图案 {pattern} @msg the document has no swatch {n} 文档中没有色板 {n} +@msg `plates` requires a stop linked to a spot swatch `plates` 需要链接到专色色板的渐变色标 @msg the output 输出 @msg the path has no anchor points 路径没有锚点 @msg too many anchor points (limit {MAX_ANCHORS}) 锚点过多(上限 {MAX_ANCHORS}) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 4637ecb811..e97abacc3d 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -6144,6 +6144,7 @@ axis Y Axis Y 軸 @msg stops must be in order of `offset` 色標必須依 `offset` 排序 @msg the document has no pattern {pattern} 文件中沒有圖樣 {pattern} @msg the document has no swatch {n} 文件中沒有色票 {n} +@msg `plates` requires a stop linked to a spot swatch `plates` 需要連結到特別色色票的漸層色標 @msg the output 輸出 @msg the path has no anchor points 路徑沒有錨點 @msg too many anchor points (limit {MAX_ANCHORS}) 錨點過多(上限 {MAX_ANCHORS}) diff --git a/docs/plugins.md b/docs/plugins.md index dd3df7f7ac..3b6b730f59 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -804,7 +804,8 @@ is a piece drawn by itself, in the order returned, with paints of its own: anchors, 256 fills and 256 strokes per piece, 256 stops per gradient), finite numbers in range with nothing clamped (color components, tints, offsets, opacities, midpoints 0.13 to 0.87, stops in order of `offset`, stroke widths 0 to 1,000 pt, miter limits 1 to 500), and every swatch a solid paint, gradient or stop links to and every - pattern named in the document. One fault refuses the whole result: that step of the stack draws its input as it was, + pattern named in the document. A gradient stop's `plates` need that stop's own link to a spot swatch of the document; + the gradient's `swatch` does not link its stops. One fault refuses the whole result: that step of the stack draws its input as it was, the effect record stays, and `plugin.info`'s `lastError` names the plug-in and the field, for example `Test Tone: plug-in output: objects[1].strokes[0].width: …`. - The pieces are never written into the document while it is drawn, measured, previewed, saved or exported. PDF and SVG diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c6972468cc..5910eeedc3 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2074,6 +2074,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **The warning of a preview applied when a command ends a drag:** a preview that Real-time Drawing off left unapplied, and that Undo, Clear History or another command applies before it runs, keeps its warning: the command's result carries it as `previewWarning`, and as `warning` when the command has none of its own, and the status bar shows it, as releasing the drag shows it. Typing kept as its own step when a tool's drag begins, or when Cmd lends the Selection tool, shows its applied preview's warning in the status bar too. Regressions: `settling_a_deferred_preview_keeps_its_warning` and `settling_typing_for_a_tool_shows_its_preview_warning` in `crates/engine/src/tests/prefs.rs`, `settled_warnings_join_the_result` in `crates/engine/src/session/execute.rs`. Scores and hours unchanged. - **Plug-in progress, Cancel and log lines (G17):** at plug-in API level 2 a plug-in with the `progress` permission imports `env.vc_progress`, `env.vc_cancelled` and `env.vc_log`. A heavy filter's and an export's progress windows show its progress as a percentage bar, their Cancel reaches it through `vc_cancelled`, a light filter with `progress` and no `commandAllowlist` runs in the background with the same progress window when it runs at once or OK runs it with Preview off, and its log lines go to the app's log with its id, capped per line and per run. Installing checks every import and names the one it refuses. Regressions: `crates/plugins/src/host.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged. - **Live-effect plug-ins paint their output (G17, #121):** a live effect whose manifest has `"api": 2` and `"paintOutput": true` returns pieces, each drawn by itself in order with fills and strokes of its own in the filter's paint forms (a piece without them keeps the object's; an empty list paints none; an effect on a fill returns only fills, one on a stroke only strokes). The whole result is checked first: limits, numbers in range with nothing clamped, known keys only, and the swatches and patterns it names in the document; one fault draws that step of the stack unchanged, keeps the effect record and puts the plug-in's name and the field in `plugin.info`'s `lastError`. The pieces are drawn, measured and exported to PDF and SVG from the appearance stack without changing the document, and Expand Appearance writes them as ordinary paths in one undo step. Effects without the key behave as before. Regressions: `crates/plugins/src/painted/tests.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`. Scores and hours unchanged. +- **Painted effects refuse gradient stop plates without a spot link (G17):** a gradient stop with process `plates` in a painted live effect's fill or stroke must link to a spot swatch of the document itself; a stop with no link, a `null` link or only the gradient's own `swatch` refuses the whole result, and so does a link to a process swatch or Registration, also when a cached result's swatch stops being a spot swatch. `lastError` names the stop's `plates`. Regressions: `crates/plugins/src/painted/tests.rs`, `crates/plugins/src/effect.rs` (`cached_painted_plates_refuse_after_spot_becomes_process`). Scores and hours unchanged. - **Later effects on every fill or stroke a painted effect returns (G17):** when a painted effect on a fill or stroke returns several fills or strokes, a later live-effect plug-in in that fill's or stroke's stack, and a later geometry effect on strokes, runs once on each of them in paint order, given its own paint and its own `_context.item` among them. Each run's pieces replace only that fill or stroke. A run that fails, is refused, is deferred or would pass the effects' budget leaves only its own fill or stroke as it was, and the others still run; a deferred stage is listed once, at the source fill's or stroke's place. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/effects/src/tests/painted.rs`. Scores and hours unchanged. - **Painted pieces with brushed strokes are culled and measured by their brush art (G17):** a piece that keeps the object's brushed stroke draws the brush along the piece's own path at the piece's stroke width. Culling leaves VectorCraft's usual room for brush art (16 × the stroke width + 8 pt) past each piece, from the piece's width rather than the object's, and so does culling of a group, layer, symbol instance, blend, envelope or repeat that holds the object. A group or layer also leaves that room for each brushed path among its members, painted or not. Object › Rasterize takes the brush art drawn along each piece and leaves out the object's own brush art, which is not drawn. Pieces without a brush are culled as before. Regressions: `crates/render/src/tests/paintedbrush.rs`. Scores and hours unchanged. - **Brushed paths just off the page are exported and printed:** PDF and EPS export keep a path or compound path whose brush art reaches the page while the path lies just off it, alone or in a group, by leaving the canvas's room for brush art (16 × the widest stroke + 8 pt, VectorCraft's own value) past its box. Skip Blank Artboards keeps an artboard such brush art reaches, and Print with Ignore Artboards frames the page with that room around brushed paths and around instances whose art holds brushed strokes. A clip group leaves room only for its clipping path. Art without brushed strokes is written as before. Regressions: `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/print.rs`, `crates/eps/src/tests.rs`. Scores and hours unchanged.