collective

Browse / github.com/storytold/photocraft / patch

feat(atn): gradientLayer import and Blnd blend-if support

feat(atn): gradientLayer import and Blnd blend-if support Fixes two engine gaps identified in ARTCRAFT-PC-2026-001: 1. gradientLayer in make_from / set_from (Spotlight, Vignette — BLOCKED → PASS) - Add gradientLayer arm in make_from contentLayer branch: maps to layer.newFillLayer.gradient + gradient.fill.set (two steps via steps()) - Add gradientLayer arm in set_from contentLayer branch: maps to gradient.fill.set - Both use existing commands; no new commands or data models required - New helpers: gradient_layer_params, gradient_layer_set_params, gradient_color_stops, gradient_transparency_stops, GRADIENT_STYLES 2. Blnd (blend-

Status
open
Score
0.00
Adopters
0 other operators
Author
jacabus (imported from pull request #9812, open)
Base
a0ce166acd2a64ff1200f793d4006db61c09f4d6
Size
+113 −0 in 1 file
Review
pending
Shared
2026-10-28
Id
dcd21800b442b9ddd655ac03

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/atn_cmds.rs

diff --git a/crates/engine/src/atn_cmds.rs b/crates/engine/src/atn_cmds.rs--- a/crates/engine/src/atn_cmds.rs+++ b/crates/engine/src/atn_cmds.rs@@ -1159,6 +1159,79 @@ fn pattern_desc(p: &Value) -> Option<Descriptor> {     Some(t) } +const GRADIENT_STYLES: &[(&str, &str)] = &[+    ("Rdl", "radial"), ("Lnr", "linear"), ("Angl", "angle"),+    ("Rflt", "reflected"), ("Dmnd", "diamond"),+];++/// Color stops from a `gradientLayer` Grad descriptor: `[[location_0_to_1, "#rrggbb"], …]`.+fn gradient_color_stops(grad: &Descriptor) -> Option<Vec<Value>> {+    let clrs = get_list(grad, "Clrs")?;+    clrs.iter().map(|item| {+        let D::Descriptor(stop) = item else { return None };+        let hex = color_hex(get_obj(stop, "Clr ")?)?;+        let loc = get_num(stop, "Lctn")? / 4096.0;+        Some(json!([loc, hex]))+    }).collect()+}++/// Transparency stops from a `gradientLayer` Grad descriptor: `[[location_0_to_1, opacity_0_to_100], …]`.+fn gradient_transparency_stops(grad: &Descriptor) -> Vec<Value> {+    get_list(grad, "Trns").unwrap_or(&[]).iter().filter_map(|item| {+        let D::Descriptor(stop) = item else { return None };+        let opct = get_num(stop, "Opct")?;+        let loc = get_num(stop, "Lctn")? / 4096.0;+        Some(json!([loc, opct]))+    }).collect()+}++/// Set params for a `gradientLayer` descriptor → `gradient.fill.set`.+fn gradient_layer_set_params(t: &Descriptor) -> Option<Value> {+    let grad = get_obj(t, "Grad")?;+    let style = lookup(GRADIENT_STYLES, &get_enum(t, "Type")?)?;+    let angle = get_num(t, "Angl").unwrap_or(90.0);+    let stops = gradient_color_stops(grad)?;+    let mut p = Map::new();+    p.insert("style".into(), style.into());+    p.insert("angle".into(), angle.into());+    p.insert("stops".into(), Value::Array(stops));+    let trns = gradient_transparency_stops(grad);+    if trns.iter().any(|s| s[1].as_f64() != Some(100.0)) {+        p.insert("transparency".into(), Value::Array(trns));+    }+    if get_bool(t, "Rvrs") == Some(true) { p.insert("reverse".into(), true.into()); }+    if get_bool(t, "Dthr") == Some(true) { p.insert("dither".into(), true.into()); }+    if let Some(s) = get_num(t, "Scl ").filter(|&s| (s - 100.0).abs() > 0.01) {+        p.insert("scale".into(), s.into());+    }+    if let Some(ofst) = get_obj(t, "Ofst") {+        let h = get_num(ofst, "Hrzn").unwrap_or(0.0);+        let v = get_num(ofst, "Vrtc").unwrap_or(0.0);+        if h.abs() > 0.001 || v.abs() > 0.001 {+            p.insert("offset".into(), json!([h, v]));+        }+    }+    Some(Value::Object(p))+}++/// Create + set params for a `gradientLayer` descriptor → `(layer.newFillLayer.gradient, gradient.fill.set)`.+fn gradient_layer_params(t: &Descriptor) -> Option<(Value, Value)> {+    let set_p = gradient_layer_set_params(t)?;+    let grad = get_obj(t, "Grad")?;+    let stops = gradient_color_stops(grad)?;+    let from = stops.first().and_then(|s| s[1].as_str()).map(str::to_string)?;+    let to = stops.last().and_then(|s| s[1].as_str()).map(str::to_string)?;+    let style = lookup(GRADIENT_STYLES, &get_enum(t, "Type")?)?;+    let angle = get_num(t, "Angl").unwrap_or(90.0);+    let mut create_p = Map::new();+    create_p.insert("from".into(), from.into());+    create_p.insert("to".into(), to.into());+    create_p.insert("style".into(), style.into());+    create_p.insert("angle".into(), angle.into());+    if get_bool(t, "Rvrs") == Some(true) { create_p.insert("reverse".into(), true.into()); }+    Some((Value::Object(create_p), set_p))+}+ /// Layer › Layer Mask: the `UsrM` value of an Add Layer Mask `Mk` ↔ its command. const MASK_USING: &[(&str, &str)] = &[     ("RvlA", "layer.layerMask.revealAll"),@@ -1276,6 +1349,13 @@ fn make_from(d: &Descriptor) -> Mapped {                 Some(p) => step("layer.newFillLayer.pattern", Value::Object(p)),                 None => Mapped::Unsupported { harmless: false },             },+            Some(t) if t.class_id.is("gradientLayer") => match gradient_layer_params(t) {+                Some((create_p, set_p)) => steps(vec![+                    ("layer.newFillLayer.gradient", create_p),+                    ("gradient.fill.set", set_p),+                ]),+                None => Mapped::Unsupported { harmless: false },+            },             _ => Mapped::Unsupported { harmless: false },         },         Some("layerSection") => {@@ -1535,6 +1615,35 @@ fn set_from(d: &Descriptor) -> Mapped {             if p.keys().any(|k| k != "layer") {                 v.push(("layer.setProps", Value::Object(p)));             }+            if let Some(blnd_list) = get_list(t, "Blnd") {+                let entries: Option<Vec<Value>> = blnd_list.iter().map(|item| {+                    let D::Descriptor(blnd) = item else { return None };+                    let chnl_ref = get_ref(blnd, "Chnl")?;+                    let ch_name = match target(chnl_ref) {+                        Target::Ordinal(o) if o == "Trgt" || o == "Cmps" => "gray",+                        Target::Ordinal(o) => lookup(CHANNELS, &o)?,+                        _ => return None,+                    };+                    let src_b = get_num(blnd, "SrcB")? as i64;+                    let src_l = get_num(blnd, "Srcl")? as i64;+                    let src_w = get_num(blnd, "SrcW")? as i64;+                    let src_m = get_num(blnd, "Srcm")? as i64;+                    let mut entry = json!({"channel": ch_name, "thisLayer": [src_b, src_l, src_w, src_m]});+                    if let (Some(db), Some(dl), Some(dw), Some(dt)) = (+                        get_num(blnd, "DstB"), get_num(blnd, "Dstl"),+                        get_num(blnd, "DstW"), get_num(blnd, "Dstt"),+                    ) {+                        entry["underlying"] = json!([db as i64, dl as i64, dw as i64, dt as i64]);+                    }+                    Some(entry)+                }).collect();+                if let Some(entries) = entries {+                    if let Some(mut bp) = layer_target(r) {+                        bp.insert("blendIf".into(), Value::Array(entries));+                        v.push(("layer.setProps", Value::Object(bp)));+                    }+                }+            }             if v.is_empty() { unsupported } else { steps(v) }         }         // A Pattern Fill layer's pattern (double-click its thumbnail).@@ -1543,6 +1652,10 @@ fn set_from(d: &Descriptor) -> Mapped {                 Some(p) if !p.is_empty() => step("layer.fill.pattern.set", Value::Object(p)),                 _ => unsupported,             },+            Some(t) if t.class_id.is("gradientLayer") => match gradient_layer_set_params(t) {+                Some(p) => step("gradient.fill.set", p),+                None => unsupported,+            },             _ => unsupported,         },         (Some("AdjL"), Target::Ordinal(o)) if o == "Trgt" => {