From 678de9462d5965b63ccc0d3c4c29a3fc5cf3e5dd Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 14 Oct 2026 09:36:12 +0200 Subject: [PATCH] =?UTF-8?q?Gradient=20stops:=20parse=5Fcolor=20reads=20the?= =?UTF-8?q?=20{model,=20=E2=80=A6}=20form=20the=20panel=20now=20sends,=20a?= =?UTF-8?q?nd=20still=20takes=20{gray:=200=E2=80=93100}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to aiongg's ba95ae2d on this branch, where main's parse_color has neither form. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/style.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 57169ee7a..04aa8cd14 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -144,7 +144,7 @@ pub fn specs() -> Vec { "Gradient", [], None, - "{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}(0..100)|[r,g,b](0..255)|{model: rgb|cmyk|gray, …}(0..1) | swatch: name (its colour, as dropped on the Gradient panel), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), ids?} → {swatch, kind, stops} — edits the fill's gradient (an unnamed gradient unless it equals a gradient swatch)", + "{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}(0..100)|{gray: 0–100}|[r,g,b](0..255)|{model: rgb|cmyk|gray, …}(0..1) | swatch: name (its colour, as dropped on the Gradient panel), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), ids?} → {swatch, kind, stops} — edits the fill's gradient (an unnamed gradient unless it equals a gradient swatch)", super::has_selection, apply_gradient ), @@ -747,6 +747,8 @@ pub fn parse_color(v: &Value) -> Option { use designcraft_color::Color; match v { Value::String(s) => Color::from_hex(s), + // `{model, …}` is the colour's own form (as `color.convert` takes it): fractions, never guessed. + Value::Object(o) if o.contains_key("model") => model_color(v), // One scale per colour, not per component: {c: 0.7, y: 12.2} is C 0.7 %, Y 12.2 %. Only a colour whose // components are all within 0..1 is read as fractions (the older form). Value::Array(a) if a.len() >= 3 => { @@ -756,6 +758,10 @@ pub fn parse_color(v: &Value) -> Option { } Value::Object(o) => { let g = |k: &str| o.get(k).and_then(Value::as_f64); + // {gray: 0–100} is a grey with that much black. + if let Some(k) = g("gray") { + return Some(Color::gray((k.clamp(0.0, 100.0) / 100.0) as f32)); + } let v = [g("c")?, g("m")?, g("y")?, g("k")?]; let scale = if v.iter().any(|&x| x > 1.0) { 100.0 } else { 1.0 }; Some(Color::cmyk((v[0] / scale) as f32, (v[1] / scale) as f32, (v[2] / scale) as f32, (v[3] / scale) as f32)) @@ -764,6 +770,20 @@ pub fn parse_color(v: &Value) -> Option { } } +/// `{model: rgb|cmyk|gray, …}` with every component in 0..=1; anything else is not a colour. +fn model_color(v: &Value) -> Option { + use designcraft_color::Color; + let color: Color = serde_json::from_value(v.clone()).ok()?; + // A float's last bit (100.0 / 100.0 ≠ 1.0 after an f32 round trip) is not out of range. + let unit = |x: f32| x.is_finite() && (-1e-4..=1.0 + 1e-4).contains(&x); + let q = |x: f32| x.clamp(0.0, 1.0); + match color { + Color::Rgb { r, g, b } => [r, g, b].into_iter().all(unit).then(|| Color::rgb(q(r), q(g), q(b))), + Color::Cmyk { c, m, y, k } => [c, m, y, k].into_iter().all(unit).then(|| Color::cmyk(q(c), q(m), q(y), q(k))), + Color::Gray { k } => unit(k).then_some(Color::Gray { k: q(k) }), + } +} + fn apply_object(s: &mut Session, p: &Value) -> Result { let name = str_param(p, "name").ok_or_else(|| bad("style.object.apply", "missing name"))?.to_string(); let os = s.doc()?.doc.styles.object_style(&name).cloned().ok_or_else(|| bad("style.object.apply", format!("no object style `{name}`")))?;