//! Expression tools: the error list behind the expression error bar, and the Expression //! Language menu (the ⓕ button next to an expression), written from the public Expression //! Language Reference. use effectcraft_project::{Expression, ItemId, Layer, Property}; use effectcraft_render::EvalCtx; use effectcraft_time::Tick; use serde::Serialize; use serde_json::{Value, json}; use super::{CommandSpec, comp_id, f_p}; use crate::{EngineError, Result, Session, query}; /// One failing expression. #[derive(Clone, Debug, PartialEq, Serialize)] pub struct ExprError { pub comp: u64, pub layer: u64, #[serde(rename = "layerIndex")] pub layer_index: usize, #[serde(rename = "layerName")] pub layer_name: String, pub prop: u64, /// Match path (`transform/opacity`). pub path: String, /// Property name path as shown in the timeline (`Transform/Opacity`). pub name: String, pub message: String, /// The expression is disabled (it failed to compile). pub disabled: bool, } impl ExprError { /// The error bar's text: `Error at line 1 in property 'Opacity' of layer 1 ('Solid') in comp 'Main'. …` pub fn describe(&self, comp_name: &str) -> String { let prop = self.name.rsplit('/').next().unwrap_or(&self.name); format!("{} — property '{}' of layer {} ('{}') in comp '{}'", self.message, prop, self.layer_index, self.layer_name, comp_name) } } /// Every failing expression of comp `cid` at comp time `t`: evaluation errors of enabled /// expressions and the syntax errors that disabled others. pub fn errors(s: &Session, cid: ItemId, t: Tick) -> Vec { let Some(comp) = s.project.comp(cid) else { return vec![] }; let ctx = EvalCtx { footage: Some(s.footage.as_ref()), expr: s.expr.as_deref(), ..EvalCtx::new(&s.project, cid, comp, t) }; let mut out = vec![]; for (i, l) in comp.layers.iter().enumerate() { let mut props: Vec<(&Property, &Expression)> = vec![]; l.props.walk("", &mut |_, p| { if let Some(e) = p.expr.as_ref().filter(|e| !e.text.trim().is_empty()) { props.push((p, e)); } }); for (p, e) in props { let msg = if e.enabled { match &ctx.expr { Some(h) => h.eval(&ctx, l, p, &p.value_at(l.layer_time(t))).err(), None => None, } } else { s.expr_check.and_then(|check| check(&e.text).err()) }; if let Some(message) = msg { out.push(error_of(cid, i, l, p, message, !e.enabled)); } } } out } fn error_of(cid: ItemId, i: usize, l: &Layer, p: &Property, message: String, disabled: bool) -> ExprError { ExprError { comp: cid.0, layer: l.id.0, layer_index: i + 1, layer_name: l.name.clone(), prop: p.uid, path: super::match_path_of(&l.props, p.uid).unwrap_or_default(), name: l.props.name_path_of(p.uid).unwrap_or_else(|| p.name.clone()), message, disabled, } } fn errors_cmd(s: &mut Session, p: &Value) -> Result { let cid = comp_id(s, p)?; let t = f_p(p, "time").map(Tick::from_seconds_f64).unwrap_or_else(|| s.time_of(cid)); let name = s.project.item(cid).map(|i| i.name.clone()).ok_or(EngineError::NoComp)?; let list = errors(s, cid, t); let text: Vec = list.iter().map(|e| e.describe(&name)).collect(); Ok(json!({"comp": cid.0, "time": t.seconds(), "count": list.len(), "errors": list, "text": text})) } /// The Expression Language menu: (category, [(label, inserted text)]). Submenus are nested /// with `▸` in the category name. pub fn language_menu() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { vec![ ( "Global", vec![ ("comp(name)", "comp(\"name\")"), ("footage(name)", "footage(\"name\")"), ("thisComp", "thisComp"), ("thisLayer", "thisLayer"), ("thisProperty", "thisProperty"), ("thisProject", "thisProject"), ("time", "time"), ("colorDepth", "colorDepth"), ("posterizeTime(framesPerSecond)", "posterizeTime(framesPerSecond)"), ("value", "value"), ], ), ( "Vector Math", vec![ ("add(vec1, vec2)", "add(vec1, vec2)"), ("sub(vec1, vec2)", "sub(vec1, vec2)"), ("mul(vec, amount)", "mul(vec, amount)"), ("div(vec, amount)", "div(vec, amount)"), ("clamp(value, limit1, limit2)", "clamp(value, limit1, limit2)"), ("dot(vec1, vec2)", "dot(vec1, vec2)"), ("cross(vec1, vec2)", "cross(vec1, vec2)"), ("normalize(vec)", "normalize(vec)"), ("length(vec)", "length(vec)"), ("length(point1, point2)", "length(point1, point2)"), ("lookAt(fromPoint, atPoint)", "lookAt(fromPoint, atPoint)"), ], ), ( "Random Numbers", vec![ ("seedRandom(offset, timeless=false)", "seedRandom(offset, timeless=false)"), ("random()", "random()"), ("random(maxValOrArray)", "random(maxValOrArray)"), ("random(minValOrArray, maxValOrArray)", "random(minValOrArray, maxValOrArray)"), ("gaussRandom()", "gaussRandom()"), ("gaussRandom(maxValOrArray)", "gaussRandom(maxValOrArray)"), ("gaussRandom(minValOrArray, maxValOrArray)", "gaussRandom(minValOrArray, maxValOrArray)"), ("noise(valOrArray)", "noise(valOrArray)"), ], ), ( "Interpolation", vec![ ("linear(t, value1, value2)", "linear(t, value1, value2)"), ("linear(t, tMin, tMax, value1, value2)", "linear(t, tMin, tMax, value1, value2)"), ("ease(t, value1, value2)", "ease(t, value1, value2)"), ("ease(t, tMin, tMax, value1, value2)", "ease(t, tMin, tMax, value1, value2)"), ("easeIn(t, value1, value2)", "easeIn(t, value1, value2)"), ("easeIn(t, tMin, tMax, value1, value2)", "easeIn(t, tMin, tMax, value1, value2)"), ("easeOut(t, value1, value2)", "easeOut(t, value1, value2)"), ("easeOut(t, tMin, tMax, value1, value2)", "easeOut(t, tMin, tMax, value1, value2)"), ], ), ( "Color Conversion", vec![ ("rgbToHsl(rgbaArray)", "rgbToHsl(rgbaArray)"), ("hslToRgb(hslaArray)", "hslToRgb(hslaArray)"), ("hexToRgb(hexString)", "hexToRgb(hexString)"), ], ), ("Other Math", vec![("degreesToRadians(degrees)", "degreesToRadians(degrees)"), ("radiansToDegrees(radians)", "radiansToDegrees(radians)")]), ( "JavaScript Math", vec![ ("Math.cos(value)", "Math.cos(value)"), ("Math.sin(value)", "Math.sin(value)"), ("Math.atan2(y, x)", "Math.atan2(y, x)"), ("Math.sqrt(value)", "Math.sqrt(value)"), ("Math.abs(value)", "Math.abs(value)"), ("Math.round(value)", "Math.round(value)"), ("Math.floor(value)", "Math.floor(value)"), ("Math.min(value1, value2)", "Math.min(value1, value2)"), ("Math.max(value1, value2)", "Math.max(value1, value2)"), ("Math.PI", "Math.PI"), ], ), ( "Comp", vec![ ("layer(index)", "layer(index)"), ("layer(name)", "layer(\"name\")"), ("layer(otherLayer, relIndex)", "layer(otherLayer, relIndex)"), ("marker", "marker"), ("numLayers", "numLayers"), ("activeCamera", "activeCamera"), ("width", "width"), ("height", "height"), ("duration", "duration"), ("frameDuration", "frameDuration"), ("pixelAspect", "pixelAspect"), ("name", "name"), ("bgColor", "bgColor"), ("displayStartTime", "displayStartTime"), ], ), ( "Footage", vec![ ("width", "width"), ("height", "height"), ("duration", "duration"), ("frameDuration", "frameDuration"), ("pixelAspect", "pixelAspect"), ("name", "name"), ("sourceText", "sourceText"), ("sourceData", "sourceData"), ("dataValue(dataPath)", "dataValue(dataPath)"), ("dataKeyCount", "dataKeyCount"), ], ), ( "Layer > Sub-objects", vec![ ("source", "source"), ("effect(name)", "effect(\"name\")"), ("effect(index)", "effect(index)"), ("mask(name)", "mask(\"name\")"), ("mask(index)", "mask(index)"), ], ), ( "Layer > General", vec![ ("width", "width"), ("height", "height"), ("index", "index"), ("parent", "parent"), ("hasParent", "hasParent"), ("inPoint", "inPoint"), ("outPoint", "outPoint"), ("startTime", "startTime"), ("hasVideo", "hasVideo"), ("enabled", "enabled"), ("active", "active"), ], ), ( "Layer > Properties", vec![ ("anchorPoint", "anchorPoint"), ("position", "position"), ("scale", "scale"), ("rotation", "rotation"), ("opacity", "opacity"), ("marker", "marker"), ("name", "name"), ], ), ( "Layer > Space Transforms", vec![ ("toComp(point, t = time)", "toComp(point, t = time)"), ("fromComp(point, t = time)", "fromComp(point, t = time)"), ("toWorld(point, t = time)", "toWorld(point, t = time)"), ("fromWorld(point, t = time)", "fromWorld(point, t = time)"), ("toCompVec(vec, t = time)", "toCompVec(vec, t = time)"), ("fromCompVec(vec, t = time)", "fromCompVec(vec, t = time)"), ("toWorldVec(vec, t = time)", "toWorldVec(vec, t = time)"), ("fromWorldVec(vec, t = time)", "fromWorldVec(vec, t = time)"), ], ), ( "Layer > Other", vec![ ("sourceRectAtTime(t = time, includeExtents = false)", "sourceRectAtTime(t = time, includeExtents = false)"), ("sampleImage(point, radius = [.5, .5], postEffect = true, t = time)", "sampleImage(point, radius = [.5, .5], postEffect = true, t = time)"), ], ), ( "Property", vec![ ("value", "value"), ("valueAtTime(t)", "valueAtTime(t)"), ("velocity", "velocity"), ("velocityAtTime(t)", "velocityAtTime(t)"), ("speed", "speed"), ("speedAtTime(t)", "speedAtTime(t)"), ("wiggle(freq, amp, octaves = 1, amp_mult = .5, t = time)", "wiggle(freq, amp, octaves = 1, amp_mult = .5, t = time)"), ("temporalWiggle(freq, amp, octaves = 1, amp_mult = .5, t = time)", "temporalWiggle(freq, amp, octaves = 1, amp_mult = .5, t = time)"), ("smooth(width = .2, samples = 5, t = time)", "smooth(width = .2, samples = 5, t = time)"), ("loopIn(type = \"cycle\", numKeyframes = 0)", "loopIn(type = \"cycle\", numKeyframes = 0)"), ("loopOut(type = \"cycle\", numKeyframes = 0)", "loopOut(type = \"cycle\", numKeyframes = 0)"), ("loopInDuration(type = \"cycle\", duration = 0)", "loopInDuration(type = \"cycle\", duration = 0)"), ("loopOutDuration(type = \"cycle\", duration = 0)", "loopOutDuration(type = \"cycle\", duration = 0)"), ("key(index)", "key(index)"), ("key(markerName)", "key(markerName)"), ("nearestKey(t)", "nearestKey(t)"), ("numKeys", "numKeys"), ("name", "name"), ("propertyIndex", "propertyIndex"), ], ), ( "Path Property", vec![ ("points(t = time)", "points(t = time)"), ("inTangents(t = time)", "inTangents(t = time)"), ("outTangents(t = time)", "outTangents(t = time)"), ("isClosed()", "isClosed()"), ( "createPath(points, inTangents = [], outTangents = [], is_closed = true)", "createPath(points, inTangents = [], outTangents = [], is_closed = true)", ), ], ), ( "Text > Style", vec![ ("text.sourceText.style", "text.sourceText.style"), ("getStyleAt(charIndex, t = time)", "getStyleAt(charIndex, t = time)"), ("createStyle()", "createStyle()"), ("setFontSize(value)", "setFontSize(value)"), ("setFillColor(value)", "setFillColor(value)"), ("setText(value)", "setText(value)"), ], ), ("Text > Expression Selector", vec![("textIndex", "textIndex"), ("textTotal", "textTotal"), ("selectorValue", "selectorValue")]), ("Key", vec![("value", "value"), ("time", "time"), ("index", "index")]), ( "Marker Key", vec![ ("duration", "duration"), ("comment", "comment"), ("chapter", "chapter"), ("url", "url"), ("frameTarget", "frameTarget"), ("eventCuePoint", "eventCuePoint"), ("cuePointName", "cuePointName"), ("parameters", "parameters"), ("protectedRegion", "protectedRegion"), ], ), ( "Time Conversion", vec![ ( "timeToFrames(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, isDuration = false)", "timeToFrames(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, isDuration = false)", ), ("framesToTime(frames, fps = 1.0 / thisComp.frameDuration)", "framesToTime(frames, fps = 1.0 / thisComp.frameDuration)"), ( "timeToTimecode(t = time + thisComp.displayStartTime, timecodeBase = 30, isDuration = false)", "timeToTimecode(t = time + thisComp.displayStartTime, timecodeBase = 30, isDuration = false)", ), ( "timeToFeetAndFrames(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, framesPerFoot = 16, isDuration = false)", "timeToFeetAndFrames(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, framesPerFoot = 16, isDuration = false)", ), ( "timeToCurrentFormat(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, isDuration = false)", "timeToCurrentFormat(t = time + thisComp.displayStartTime, fps = 1.0 / thisComp.frameDuration, isDuration = false)", ), ], ), ] } fn menu_cmd(_s: &mut Session, _p: &Value) -> Result { Ok(json!( language_menu() .into_iter() .map(|(c, items)| json!({"category": c, "items": items.into_iter().map(|(l, t)| json!({"label": l, "text": t})).collect::>()})) .collect::>() )) } pub fn specs() -> Vec { vec![ query!("expr.errors", "Expression errors (the error bar)", "{comp?, time? (s)}", errors_cmd), query!("expr.languageMenu", "Expression Language menu", "{}", menu_cmd), ] }