diff --git a/crates/expr/src/host.rs b/crates/expr/src/host.rs index f98b895a5..a25fbc4f0 100644 --- a/crates/expr/src/host.rs +++ b/crates/expr/src/host.rs @@ -313,6 +313,32 @@ impl<'a> Resolver<'a> { self.try_resolve(req).unwrap_or(Resp::Null) } + /// Resolve an ancestor from its UID path while preserving child indexing. + fn ancestor(&self, comp: u64, layer: u64, path: &str, up: i64) -> Option { + let l = self.layer(comp, layer)?; + // The node's full `@uid/@uid/…` path from the layer: its ancestors are its prefixes. + let uid = match self.prop(comp, layer, path) { + Some((_, p)) => p.uid, + None => l.props.node(path)?.uid(), + }; + let full = l.props.path_of(uid)?; + let segs: Vec<&str> = full.split('/').collect(); + let up = usize::try_from(up).ok().filter(|u| *u >= 1 && *u <= segs.len())?; + if up == segs.len() { + return Some(Resp::Str("layer".into())); + } + let keep = segs.len() - up; + let group_path = segs.get(..keep)?.join("/"); + let parent = match keep { + 1 => &l.props, + _ => l.props.group(&segs.get(..keep - 1)?.join("/"))?, + }; + let uid = segs.get(keep - 1)?.strip_prefix('@')?.parse::().ok()?; + let index = parent.children.iter().position(|c| c.uid() == uid)?; + let n = parent.children.get(index)?; + Some(node_resp(group_path, n, index + 1)) + } + fn try_resolve(&self, req: &Req) -> Option { let project = self.ctx.project; Some(match req { @@ -373,30 +399,7 @@ impl<'a> Resolver<'a> { let path = if group.is_empty() { format!("@{}", n.uid()) } else { format!("{group}/@{}", n.uid()) }; node_resp(path, n, index) } - Req::Ancestor { comp, layer, path, up } => { - let l = self.layer(*comp, *layer)?; - // The node's full `@uid/@uid/…` path from the layer: its ancestors are its prefixes. - let uid = match self.prop(*comp, *layer, path) { - Some((_, p)) => p.uid, - None => l.props.node(path)?.uid(), - }; - let full = l.props.path_of(uid)?; - let segs: Vec<&str> = full.split('/').collect(); - let up = usize::try_from(*up).ok().filter(|u| *u >= 1 && *u <= segs.len())?; - if up == segs.len() { - return Some(Resp::Str("layer".into())); - } - let keep = segs.len() - up; - let group_path = segs.get(..keep)?.join("/"); - let parent = match keep { - 1 => &l.props, - _ => l.props.group(&segs.get(..keep - 1)?.join("/"))?, - }; - let uid = segs.get(keep - 1)?.strip_prefix('@')?.parse::().ok()?; - let index = parent.children.iter().position(|c| c.uid() == uid)?; - let n = parent.children.get(index)?; - node_resp(group_path, n, index + 1) - } + Req::Ancestor { comp, layer, path, up } => return self.ancestor(*comp, *layer, path, *up), Req::PropInfo { comp, layer, path } => { let (l, p) = self.prop(*comp, *layer, path)?; let dims = prop_dims(l, p); diff --git a/crates/expr/src/lib.rs b/crates/expr/src/lib.rs index 4963810c4..c77af5b37 100644 --- a/crates/expr/src/lib.rs +++ b/crates/expr/src/lib.rs @@ -93,7 +93,7 @@ impl ExprHost for Expressions { return Ok([v.first().copied().unwrap_or(100.0), v.get(1).copied().unwrap_or(100.0), v.get(2).copied().unwrap_or(100.0)]); }; let value = prop.value_at(layer.layer_time(ctx.time)); - let out = evaluate_out(self, ctx, layer, prop, &value, &e.text, [index as f64 + 1.0, total as f64], selector)?; + let out = evaluate_out(self, ctx, layer, prop, &value, &e.text, TextSelector { index_total: [index as f64 + 1.0, total as f64], value: selector })?; let num = |o: &runtime::Out| match o { runtime::Out::Num(x) => Some(*x), runtime::Out::Bool(b) => Some(if *b { 100.0 } else { 0.0 }), @@ -124,13 +124,17 @@ impl Drop for FrameGuard { /// Evaluate `text` as the expression of `prop` on `layer` at `ctx.time`, given its keyframed /// `value`. Other properties' expressions are evaluated through `host`. pub fn evaluate(host: &dyn ExprHost, ctx: &EvalCtx, layer: &Layer, prop: &Property, value: &Value, text: &str) -> Result { - let out = evaluate_out(host, ctx, layer, prop, value, text, [1.0, 1.0], [100.0; 3])?; + let out = evaluate_out(host, ctx, layer, prop, value, text, TextSelector { index_total: [1.0, 1.0], value: [100.0; 3] })?; runtime::to_value(&out, value) } -/// Run an expression and return the raw script result. `text_sel` / `selector` are the text -/// Expression Selector's `[textIndex, textTotal]` and `selectorValue`. -#[allow(clippy::too_many_arguments)] +/// Expression Selector state: `[textIndex, textTotal]` and `selectorValue`. +struct TextSelector { + index_total: [f64; 2], + value: [f64; 3], +} + +/// Run an expression and return the raw script result. fn evaluate_out( host: &dyn ExprHost, ctx: &EvalCtx, @@ -138,9 +142,9 @@ fn evaluate_out( prop: &Property, value: &Value, text: &str, - text_sel: [f64; 2], - selector: [f64; 3], + text_selector: TextSelector, ) -> Result { + let TextSelector { index_total: text_sel, value: selector } = text_selector; let owner = Owner { comp: ctx.comp_id.0, layer: layer.id.0, uid: prop.uid, time: ctx.time, text: text.to_string() }; let (depth, cycle) = FRAMES.with(|f| { let f = f.borrow();