From 2ef3d977a66d596b93cc2d7e039916fd4924ee61 Mon Sep 17 00:00:00 2001 From: Kristian Myram Date: Mon, 12 Oct 2026 19:19:13 +0200 Subject: [PATCH] #808: sourcePointToComp and compPointToSource follow the layer's transform Both returned their input unchanged. They now go through the layer's transform and its parents' at comp time, and the inverse. Co-Authored-By: Claude Haiku 5.5 --- crates/script/src/model.rs | 24 ++++++++++++++++++++++++ crates/script/src/prelude.js | 8 ++++++-- crates/script/src/tests.rs | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/script/src/model.rs b/crates/script/src/model.rs index 4445fbbf..d3c084af 100644 --- a/crates/script/src/model.rs +++ b/crates/script/src/model.rs @@ -44,6 +44,7 @@ pub fn query(s: &mut Session, kind: &str, a: &J) -> R { "textdoc" => textdoc(s, a), "markers" => markers(s, a), "sourceRect" => source_rect(s, a), + "pointConvert" => point_convert(s, a), "resolveAdd" => resolve_add(s, a), "menuCommandId" => Ok(menu_command_id(a.get("name").and_then(J::as_str).unwrap_or(""))), "commandAt" => { @@ -577,6 +578,29 @@ fn markers(s: &Session, a: &J) -> R { Ok(J::Array(out)) } +/// A layer's source point in comp space (`toComp`) or the reverse, through the layer's transform +/// and its parents' at comp time (#808: `sourcePointToComp`, `compPointToSource`). +fn point_convert(s: &Session, a: &J) -> R { + let (cid, c, l) = layer_of(s, a)?; + let t = f(a, "time").map(Tick::from_seconds_f64).unwrap_or_else(|| s.time_of(cid)); + let p = a.get("point").and_then(J::as_array).ok_or("missing point")?; + let x = p.first().and_then(J::as_f64).unwrap_or(0.0); + let y = p.get(1).and_then(J::as_f64).unwrap_or(0.0); + let mut ctx = EvalCtx::new(&s.project, cid, c, t); + ctx.expr = s.expr.as_deref(); + let (layer_space, _) = ctx.layer_to_comp(l); + let m = if a.get("toComp").and_then(J::as_bool).unwrap_or(true) { + layer_space + } else { + layer_space.inverse().ok_or("the layer's transform can't be inverted")? + }; + let k = m.0; + let w = k[2][0] * x + k[2][1] * y + k[2][2]; + let (ox, oy) = (k[0][0] * x + k[0][1] * y + k[0][2], k[1][0] * x + k[1][1] * y + k[1][2]); + let (ox, oy) = if w != 0.0 && w.is_finite() { (ox / w, oy / w) } else { (ox, oy) }; + Ok(json!([ox, oy])) +} + fn source_rect(s: &Session, a: &J) -> R { let (cid, c, l) = layer_of(s, a)?; let t = f(a, "time").map(Tick::from_seconds_f64).unwrap_or_else(|| s.time_of(cid)); diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 9daac6ab..1956ce32 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -1285,8 +1285,12 @@ AVLayer.prototype.constructor = AVLayer; P.sourceRectAtTime = function (t, extents) { return __get("sourceRect", { comp: this.__comp, layer: this.__id, time: __num(t, "time"), extents: !!extents }); }; - P.sourcePointToComp = function (p) { return p; }; - P.compPointToSource = function (p) { return p; }; + // Source ↔ comp points, through the layer's transform and its parents' at the comp's time (#808). + function pointIn(layer, p, toComp) { + return __get("pointConvert", { comp: layer.__comp, layer: layer.__id, point: [__num(p[0], "point"), __num(p[1], "point")], toComp: toComp, time: layer.containingComp.time }); + } + P.sourcePointToComp = function (p) { return pointIn(this, p, true); }; + P.compPointToSource = function (p) { return pointIn(this, p, false); }; P.replaceSource = function () { throw __err("replaceSource is not supported yet"); }; P.openInViewer = function () { __call("layer.openLayer", this.__p1()); return null; }; })(); diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 303bd820..986dc136 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -1010,3 +1010,25 @@ fn scripted_text_range_selector_resolves_and_is_the_only_selector() { ); assert_eq!(o.result, json!([1, "ADBE Text Selector", 50, 100, "ADBE Text Percent Offset"])); } + +/// #808: sourcePointToComp and compPointToSource follow the layer's transform. +#[test] +fn source_point_conversions_follow_the_layer_transform() { + let mut s = session(); + let o = ok( + &mut s, + r#" + var comp = app.project.items.addComp("Repro", 1920, 1080, 1, 10, 25); + var layer = comp.layers.addSolid([1, 0, 0], "S", 200, 100, 1); + layer.transform.position.setValue([400, 300]); + layer.transform.scale.setValue([50, 50]); + var a = layer.sourcePointToComp([0, 0]); + var b = layer.compPointToSource([400, 300]); + [a[0], a[1], b[0], b[1]] + "#, + ); + let v = o.result; + let near = |x: &Value, want: f64| x.as_f64().is_some_and(|g| (g - want).abs() < 1e-6); + assert!(near(&v[0], 350.0) && near(&v[1], 275.0), "{v}"); + assert!(near(&v[2], 100.0) && near(&v[3], 50.0), "{v}"); +}