Files
effectcraft/crates/engine/src/commands/track.rs
T
Brandon ThomasandClaude Opus 5.5 587d4803fe Never crash: project crate without expect()
Layer::motion_trackers_mut expect()ed the group it had just inserted; it returns an Option
now and track.motion reports an error if the group is somehow missing. Denies the panic
lints in the crate (the project model behind .ecproj files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:58:51 -04:00

593 lines
26 KiB
Rust

//! Motion tracking commands (Animation ▸ Track Motion / Stabilize Motion / Track this Property and
//! the Tracker panel): create trackers, set type / target / options, move track points, analyze
//! (background job or blocking), apply, reset.
//!
//! A tracker is addressed by `layer` (the tracked layer, a.k.a. Motion Source) and `tracker`
//! (group uid, name or 1-based index); both default to the Tracker panel's Current Track.
use effectcraft_project::build::Ids;
use effectcraft_project::tracking::{self, LowConfidence, TrackChannel, TrackKind, TrackerSettings};
use effectcraft_project::{ItemId, Layer, LayerId, Node, PropGroup, Uid, Value};
use effectcraft_time::Tick;
use serde_json::json;
use super::{CommandSpec, b_p, bad, comp_id, f_p, frontend, has_comp, layer_mut, layer_p, resolve_layer, str_p};
use crate::tracking::{Dims, Direction, Work, point_values};
use crate::{EngineError, Result, Session, cmd, query};
type V = serde_json::Value;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("track.motion", "Track Motion", ["Animation"], None, "{layer?}", can_track, |s, p| new_with(s, p, TrackKind::Transform, "track.motion")),
cmd!("track.stabilize", "Stabilize Motion", ["Animation"], None, "{layer?}", can_track, |s, p| new_with(s, p, TrackKind::Stabilize, "track.stabilize")),
cmd!(
"track.new",
"New Tracker",
[],
None,
"{layer?, kind?: transform|stabilize|affine|perspective|raw, position?, rotation?, scale?, target?: layer}",
can_track,
new_tracker
),
cmd!("track.property", "Track this Property", ["Animation"], None, "{layer?, source?: layer}", has_prop_selected, track_property),
cmd!("track.select", "Current Track", [], None, "{layer?, tracker?: uid|name|#n}", has_comp, select),
cmd!(
"track.setType",
"Track Type",
[],
None,
"{layer?, tracker?, kind?: transform|stabilize|affine|perspective|raw, position?, rotation?, scale?}",
has_track,
set_type
),
cmd!("track.setTarget", "Edit Target", [], None, "{layer?, tracker?, target: layer|null}", has_track, set_target),
cmd!(
"track.options",
"Motion Tracker Options",
[],
None,
"{layer?, tracker?, name?, channel?: rgb|luminance|saturation, blur? (px, 0 = off), enhance?, subpixel?, adaptEveryFrame?, threshold? (%), action?: continue|stop|extrapolate|adapt, trackShape?}",
has_track,
options
),
cmd!(
"track.setPoint",
"Move Track Point",
[],
None,
"{layer?, tracker?, point: n (1-based), center?: [x,y], featureSize?: [w,h], searchOffset?: [x,y], searchSize?: [w,h], attachOffset?: [x,y], move?: [dx,dy], time? (s)}",
has_track,
set_point
),
cmd!(
"track.analyze",
"Analyze",
[],
None,
"{layer?, tracker?, direction?: forward|backward|frameForward|frameBackward, start? (s), end? (s), wait?: block until done}",
has_track,
analyze
),
cmd!("track.stop", "Stop Analysis", [], None, "{}", is_tracking, |s, _| Ok(json!({"stopped": s.stop_track() | s.stop_mask_track() | s.stop_warp()}))),
cmd!("track.apply", "Apply", [], None, "{layer?, tracker?, dimensions?: xy|x|y}", has_track, apply),
cmd!("track.reset", "Reset", [], None, "{layer?, tracker?}", has_track, reset),
cmd!("track.delete", "Delete Tracker", [], None, "{layer?, tracker?}", has_track, delete),
cmd!("track.editTargetDialog", "Edit Target...", [], None, "{}", has_track, |s, p| frontend(s, "track.editTargetDialog", p)),
cmd!("track.optionsDialog", "Options...", [], None, "{}", has_track, |s, p| frontend(s, "track.optionsDialog", p)),
query!("track.status", "Tracker Status", "{layer?, tracker?}", status),
]
}
// ---------- enablement ----------
fn trackable(l: &Layer) -> bool {
l.has_video() && !matches!(l.source, effectcraft_project::LayerSource::Text | effectcraft_project::LayerSource::Shape)
}
fn can_track(s: &Session) -> std::result::Result<(), String> {
has_comp(s)?;
let c = s.active_comp().ok_or("no composition is open")?;
match s.state.selected_layers.first().and_then(|l| c.layer(*l)) {
Some(l) if trackable(l) => Ok(()),
Some(_) => Err("select a footage, composition or solid layer to track".into()),
None => Err("select a layer to track".into()),
}
}
fn has_prop_selected(s: &Session) -> std::result::Result<(), String> {
has_comp(s)?;
if s.state.selected_props.is_empty() { Err("select a property to track".into()) } else { Ok(()) }
}
fn has_track(s: &Session) -> std::result::Result<(), String> {
current(s, &json!({}), "track").map(|_| ()).map_err(|e| e.to_string().replace("invalid parameters for `track`: ", ""))
}
fn is_tracking(s: &Session) -> std::result::Result<(), String> {
if s.is_tracking() || s.is_mask_tracking() || s.is_warp_analyzing() { Ok(()) } else { Err("no track analysis is running".into()) }
}
// ---------- addressing ----------
/// (comp, tracked layer, tracker uid) from `layer` / `tracker` or the Current Track.
pub(crate) fn current(s: &Session, p: &V, cmd: &str) -> Result<(ItemId, LayerId, Uid)> {
let cid = comp_id(s, p)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let lid = match p.get("layer") {
Some(v) => resolve_layer(comp, v).ok_or_else(|| bad(cmd, format!("no layer {v}")))?,
None => match s.state.current_track {
Some((l, _)) if comp.layer(l).is_some() => l,
_ => s
.state
.selected_layers
.iter()
.copied()
.find(|l| comp.layer(*l).is_some_and(|l| l.trackers().next().is_some()))
.ok_or_else(|| bad(cmd, "no current track: use Track Motion first"))?,
},
};
let layer = comp.layer(lid).ok_or_else(|| bad(cmd, "no layer"))?;
let uid = match p.get("tracker") {
Some(V::Number(n)) => {
let n = n.as_u64().unwrap_or(0);
layer.tracker(n).map(|(g, _)| g.uid).or_else(|| layer.trackers().nth((n as usize).saturating_sub(1)).map(|(g, _)| g.uid))
}
Some(V::String(name)) => match name.strip_prefix('#').and_then(|i| i.parse::<usize>().ok()) {
Some(i) => layer.trackers().nth(i.saturating_sub(1)).map(|(g, _)| g.uid),
None => layer.trackers().find(|(g, _)| &g.name == name).map(|(g, _)| g.uid),
},
_ => match s.state.current_track {
Some((l, t)) if l == lid && layer.tracker(t).is_some() => Some(t),
_ => layer.trackers().next().map(|(g, _)| g.uid),
},
}
.ok_or_else(|| bad(cmd, format!("layer `{}` has no such tracker", layer.name)))?;
Ok((cid, lid, uid))
}
fn tracker_mut(proj: &mut effectcraft_project::Project, cid: ItemId, lid: LayerId, uid: Uid) -> Result<&mut PropGroup> {
layer_mut(proj, cid, lid)?.props.find_group_mut(uid).ok_or_else(|| EngineError::Other("tracker gone".into()))
}
fn layer_size(s: &Session, cid: ItemId, l: &Layer) -> [f64; 2] {
let (w, h) = effectcraft_render::source_size(&s.project, l);
if w == 0 {
let c = s.project.comp(cid);
c.map(|c| [c.width as f64, c.height as f64]).unwrap_or([1920.0, 1080.0])
} else {
[w as f64, h as f64]
}
}
/// The layer directly above `lid` (the default Motion Target).
fn layer_above(comp: &effectcraft_project::Comp, lid: LayerId) -> Option<LayerId> {
let i = comp.layers.iter().position(|l| l.id == lid)?;
i.checked_sub(1).map(|i| comp.layers[i].id)
}
fn kind_p(p: &V, cmd: &str) -> Result<Option<TrackKind>> {
match str_p(p, "kind") {
Some(k) => TrackKind::from_name(k).map(Some).ok_or_else(|| bad(cmd, format!("unknown kind `{k}` (transform|stabilize|affine|perspective|raw)"))),
None => Ok(None),
}
}
// ---------- create ----------
fn new_with(s: &mut Session, p: &V, kind: TrackKind, cmd: &str) -> Result<V> {
let mut q = p.clone();
q["kind"] = json!(kind.id());
create(s, &q, cmd)
}
fn new_tracker(s: &mut Session, p: &V) -> Result<V> {
create(s, p, "track.new")
}
fn create(s: &mut Session, p: &V, cmd: &str) -> Result<V> {
let (cid, lid) = layer_p(s, p, cmd)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
if !trackable(layer) {
return Err(bad(cmd, format!("layer `{}` has no pixels to track", layer.name)));
}
let kind = kind_p(p, cmd)?.unwrap_or_default();
let mut st = TrackerSettings::new(kind);
st.position = b_p(p, "position").unwrap_or(true);
st.rotation = b_p(p, "rotation").unwrap_or(false);
st.scale = b_p(p, "scale").unwrap_or(false);
// Motion Target: the layer above the tracked layer (Stabilize applies to the layer itself).
st.target = match p.get("target") {
Some(V::Null) => None,
Some(v) => Some(resolve_layer(comp, v).ok_or_else(|| bad(cmd, format!("no target layer {v}")))?),
None if kind == TrackKind::Stabilize => Some(lid),
None => layer_above(comp, lid),
};
let size = layer_size(s, cid, layer);
let n_existing = layer.trackers().count();
let name = format!("Tracker {}", n_existing + 1);
let pts = tracking::default_points(kind, st.point_count(), size);
let uid = s.edit(&format!("New {}", name), None, |proj, state| {
let mut next = proj.next_id;
let l = layer_mut(proj, cid, lid)?;
let mut ids = Ids(&mut next);
let g = tracking::tracker(&mut ids, &name, st, &pts);
let uid = g.uid;
l.motion_trackers_mut(&mut ids).ok_or_else(|| bad(cmd, "the layer has no Motion Trackers group"))?.children.push(g.into());
proj.next_id = next;
state.current_track = Some((lid, uid));
state.selected_layers = vec![lid];
Ok(uid)
})?;
let _ = frontend(s, "window.panel", &json!({"panel": "tracker"}));
Ok(json!({"tracker": uid, "name": name, "layer": lid.0, "points": pts.len()}))
}
/// Track this Property: track another layer and drive the selected property's layer.
fn track_property(s: &mut Session, p: &V) -> Result<V> {
let cid = comp_id(s, p)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let target = match p.get("layer") {
Some(v) => resolve_layer(comp, v).ok_or_else(|| bad("track.property", format!("no layer {v}")))?,
None => s.state.selected_props.first().map(|(l, _)| *l).ok_or_else(|| bad("track.property", "select a property first"))?,
};
let source = match p.get("source") {
Some(v) => resolve_layer(comp, v).ok_or_else(|| bad("track.property", format!("no layer {v}")))?,
None => {
let ti = comp.layers.iter().position(|l| l.id == target).unwrap_or(0);
comp.layers
.iter()
.skip(ti + 1)
.chain(comp.layers.iter().take(ti))
.find(|l| l.id != target && trackable(l))
.map(|l| l.id)
.ok_or_else(|| bad("track.property", "no layer to track"))?
}
};
create(s, &json!({"layer": source.0, "kind": "transform", "target": target.0, "comp": cid.0}), "track.property")
}
// ---------- settings ----------
fn select(s: &mut Session, p: &V) -> Result<V> {
let (_, lid, uid) = current(s, p, "track.select")?;
s.state.current_track = Some((lid, uid));
s.bump();
Ok(json!({"layer": lid.0, "tracker": uid}))
}
fn set_type(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.setType")?;
let kind = kind_p(p, "track.setType")?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
let size = layer_size(s, cid, layer);
let above = layer_above(comp, lid);
let n = s.edit("Track Type", None, |proj, _| {
let mut next = proj.next_id;
let g = tracker_mut(proj, cid, lid, uid)?;
let st = g.tracker_settings_mut().ok_or_else(|| bad("track.setType", "not a tracker"))?;
if let Some(k) = kind {
if k == TrackKind::Stabilize {
st.target = Some(lid);
} else if st.kind == TrackKind::Stabilize && st.target == Some(lid) {
st.target = above;
}
st.kind = k;
}
for (k, f) in [("position", &mut st.position), ("rotation", &mut st.rotation), ("scale", &mut st.scale)] {
if let Some(v) = b_p(p, k) {
*f = v;
}
}
let (kind, n) = (st.kind, st.point_count());
// Add or remove Track Points to match the type.
let have = g.track_points().count();
if have > n {
let mut seen = 0;
g.children.retain(|c| match c {
Node::Group(tp) if tp.match_id == tracking::TRACK_POINT => {
seen += 1;
seen <= n
}
_ => true,
});
} else if have < n {
let defaults = tracking::default_points(kind, n, size);
let mut ids = Ids(&mut next);
for (i, d) in defaults.iter().enumerate().skip(have) {
g.children.push(tracking::track_point(&mut ids, &format!("Track Point {}", i + 1), d).into());
}
}
proj.next_id = next;
Ok(n)
})?;
Ok(json!({"points": n}))
}
fn set_target(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.setTarget")?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let target = match p.get("target") {
None | Some(V::Null) => None,
Some(v) => Some(resolve_layer(comp, v).ok_or_else(|| bad("track.setTarget", format!("no layer {v}")))?),
};
s.edit("Edit Target", None, |proj, _| {
let st = tracker_mut(proj, cid, lid, uid)?.tracker_settings_mut().ok_or_else(|| bad("track.setTarget", "not a tracker"))?;
if st.kind == TrackKind::Stabilize && target.is_some_and(|t| t != lid) {
return Err(bad("track.setTarget", "Stabilize applies to the tracked layer"));
}
st.target = target;
Ok(())
})?;
Ok(json!({"target": target.map(|t| t.0)}))
}
fn options(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.options")?;
let channel = match str_p(p, "channel").map(|c| c.to_ascii_lowercase()) {
None => None,
Some(c) => Some(match c.as_str() {
"rgb" => TrackChannel::Rgb,
"luminance" | "luma" => TrackChannel::Luminance,
"saturation" => TrackChannel::Saturation,
_ => return Err(bad("track.options", "channel: rgb|luminance|saturation")),
}),
};
let action = match str_p(p, "action") {
None => None,
Some(a) => Some(match effectcraft_track::ConfidenceAction::from_name(a) {
Some(effectcraft_track::ConfidenceAction::Continue) => LowConfidence::Continue,
Some(effectcraft_track::ConfidenceAction::Stop) => LowConfidence::Stop,
Some(effectcraft_track::ConfidenceAction::Extrapolate) => LowConfidence::Extrapolate,
Some(effectcraft_track::ConfidenceAction::Adapt) => LowConfidence::Adapt,
None => return Err(bad("track.options", "action: continue|stop|extrapolate|adapt")),
}),
};
let r = s.edit("Motion Tracker Options", None, |proj, _| {
let g = tracker_mut(proj, cid, lid, uid)?;
if let Some(n) = str_p(p, "name") {
g.name = n.to_string();
}
let o = &mut g.tracker_settings_mut().ok_or_else(|| bad("track.options", "not a tracker"))?.options;
if let Some(c) = channel {
o.channel = c;
}
if let Some(b) = f_p(p, "blur") {
o.blur = b.clamp(0.0, 100.0);
}
if let Some(v) = b_p(p, "enhance") {
o.enhance = v;
}
if let Some(v) = b_p(p, "subpixel") {
o.subpixel = v;
}
if let Some(v) = b_p(p, "adaptEveryFrame") {
o.adapt_every_frame = v;
}
if let Some(v) = f_p(p, "threshold") {
o.threshold = v.clamp(0.0, 100.0);
}
if let Some(a) = action {
o.action = a;
}
if let Some(v) = b_p(p, "trackShape") {
o.track_shape = v;
}
Ok(serde_json::to_value(&*o).unwrap_or(V::Null))
})?;
Ok(r)
}
fn vec2_p(p: &V, k: &str) -> Option<[f64; 2]> {
let a = p.get(k)?.as_array()?;
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])
}
fn set_point(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.setPoint")?;
let n = p.get("point").and_then(V::as_u64).unwrap_or(1).max(1) as usize;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
let t = f_p(p, "time").map(Tick::from_seconds_f64).map(|t| comp.frame_rate.snap_nearest(t)).unwrap_or_else(|| s.time());
let lt = layer.layer_time(t);
let merge = super::merge_p(p).map(str::to_string);
s.edit("Move Track Point", merge.as_deref(), |proj, _| {
let g = tracker_mut(proj, cid, lid, uid)?;
let tp_uid = g.track_points().nth(n - 1).map(|g| g.uid).ok_or_else(|| bad("track.setPoint", format!("no track point {n}")))?;
let tp = g.find_group_mut(tp_uid).ok_or_else(|| bad("track.setPoint", "no track point"))?;
let cur = point_values(tp, lt);
let mut center = vec2_p(p, "center").unwrap_or(cur.spec.center);
if let Some(d) = vec2_p(p, "move") {
center = [center[0] + d[0], center[1] + d[1]];
}
let set = |tp: &mut PropGroup, m: &str, v: [f64; 2]| {
if let Some(pr) = tp.get_mut(m) {
pr.set_value_at(lt, Value::Vec2(v));
}
};
set(tp, "featureCenter", center);
for (k, m) in [("featureSize", "featureSize"), ("searchOffset", "searchOffset"), ("searchSize", "searchSize"), ("attachOffset", "attachPointOffset")] {
if let Some(v) = vec2_p(p, k) {
let v = if k.ends_with("Size") { [v[0].max(4.0), v[1].max(4.0)] } else { v };
set(tp, m, v);
}
}
let off = tp.get("attachPointOffset").map(|pr| pr.value_at(lt).as_vec2()).unwrap_or([0.0; 2]);
set(tp, "attachPoint", [center[0] + off[0], center[1] + off[1]]);
Ok(())
})?;
Ok(V::Null)
}
// ---------- analyze / apply / reset ----------
fn analyze(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.analyze")?;
let dir = match str_p(p, "direction") {
Some(d) => Direction::from_name(d).ok_or_else(|| bad("track.analyze", "direction: forward|backward|frameForward|frameBackward"))?,
None => Direction::Forward,
};
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
let (tg, settings) = layer.tracker(uid).ok_or_else(|| bad("track.analyze", "no tracker"))?;
let now = comp.frame_rate.snap_nearest(if s.state.active_comp == Some(cid) { s.time() } else { Tick::ZERO });
let start = f_p(p, "start").map(|v| comp.frame_rate.snap_nearest(Tick::from_seconds_f64(v)));
let end = f_p(p, "end").map(|v| comp.frame_rate.snap_nearest(Tick::from_seconds_f64(v)));
let times = analysis_times(comp, layer, dir, now, start, end);
if times.len() < 2 {
return Err(bad("track.analyze", "nothing to analyze in that direction (at the layer's end)"));
}
let lt0 = layer.layer_time(times[0]);
let points: Vec<_> = tg.track_points().map(|g| point_values(g, lt0)).collect();
if points.is_empty() {
return Err(bad("track.analyze", "the tracker has no track points"));
}
let work = Work {
project: s.project.clone(),
footage: s.footage.clone(),
expr: s.expr.clone(),
cache: s.layer_cache.clone(),
comp: cid,
layer: lid,
settings: settings.clone(),
points,
times: times.clone(),
};
s.state.current_track = Some((lid, uid));
let wait = b_p(p, "wait").unwrap_or(false);
s.start_track(work, uid, dir, wait).map_err(EngineError::Other)?;
let prog = s.track_progress();
Ok(json!({
"frames": times.len() - 1,
"start": times[0].seconds(),
"end": times.last().map(|t| t.seconds()),
"running": s.is_tracking(),
"progress": prog,
}))
}
/// Comp times to analyse from the CTI (`now`) in `dir`, bounded by `start` / `end` and the
/// layer's In/Out points; the first is the start frame.
pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer, dir: Direction, now: Tick, start: Option<Tick>, end: Option<Tick>) -> Vec<Tick> {
let fd = comp.frame_duration();
let lo = layer.in_point.max(Tick::ZERO);
let hi = layer.out_point.min(comp.duration) - fd;
let mut times = vec![];
match dir {
Direction::Forward | Direction::FrameForward => {
let mut t = start.unwrap_or(now).clamp(lo, hi);
let stop = if dir == Direction::FrameForward { t + fd } else { end.unwrap_or(hi).min(hi) };
while t <= stop && t <= hi {
times.push(t);
t += fd;
}
}
Direction::Backward | Direction::FrameBackward => {
let mut t = end.unwrap_or(now).clamp(lo, hi);
let stop = if dir == Direction::FrameBackward { t - fd } else { start.unwrap_or(lo).max(lo) };
while t >= stop && t >= lo {
times.push(t);
t -= fd;
}
}
}
times
}
fn apply(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.apply")?;
let dims = match str_p(p, "dimensions") {
Some(d) => Dims::from_name(d).ok_or_else(|| bad("track.apply", "dimensions: xy|x|y"))?,
None => Dims::XY,
};
if s.is_tracking() {
return Err(bad("track.apply", "wait for the analysis to finish"));
}
let n = s.edit("Apply Track", None, |proj, _| crate::tracking::apply(proj, cid, lid, uid, dims).map_err(|e| bad("track.apply", e)))?;
Ok(json!({"frames": n}))
}
fn reset(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.reset")?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
let size = layer_size(s, cid, layer);
s.edit("Reset Track", None, |proj, _| {
let g = tracker_mut(proj, cid, lid, uid)?;
let st = g.tracker_settings().cloned().unwrap_or_default();
let n = g.track_points().count();
let defaults = tracking::default_points(st.kind, n, size);
let mut k = 0;
for c in &mut g.children {
let Node::Group(tp) = c else { continue };
if tp.match_id != tracking::TRACK_POINT {
continue;
}
let d = defaults.get(k).copied().unwrap_or(defaults[0]);
k += 1;
for (m, v) in [
("featureCenter", Value::Vec2(d.center)),
("featureSize", Value::Vec2(d.feature_size)),
("searchOffset", Value::Vec2(d.search_offset)),
("searchSize", Value::Vec2(d.search_size)),
("confidence", Value::Scalar(0.0)),
("attachPoint", Value::Vec2(d.center)),
("attachPointOffset", Value::Vec2(d.attach_offset)),
] {
if let Some(pr) = tp.get_mut(m) {
pr.keys.clear();
pr.value = v;
}
}
}
Ok(())
})?;
Ok(V::Null)
}
fn delete(s: &mut Session, p: &V) -> Result<V> {
let (cid, lid, uid) = current(s, p, "track.delete")?;
s.edit("Delete Tracker", None, |proj, st| {
let l = layer_mut(proj, cid, lid)?;
if let Some(mt) = l.props.sub_mut(tracking::MOTION_TRACKERS) {
mt.children.retain(|c| c.uid() != uid);
if mt.children.is_empty() {
l.props.children.retain(|c| c.match_id() != tracking::MOTION_TRACKERS);
}
}
if st.current_track == Some((lid, uid)) {
st.current_track = None;
}
Ok(())
})?;
Ok(V::Null)
}
fn status(s: &mut Session, p: &V) -> Result<V> {
s.poll_track();
s.poll_mask_track();
let progress = s.track_progress();
let cur = current(s, p, "track.status").ok();
let mut out = json!({"running": s.is_tracking() || s.is_mask_tracking(), "progress": progress, "mask": s.mask_track_progress(), "maskMethod": s.state.mask_track_method.id()});
if let Some((cid, lid, uid)) = cur
&& let Some(layer) = s.project.comp(cid).and_then(|c| c.layer(lid))
&& let Some((g, st)) = layer.tracker(uid)
{
let points: Vec<V> = g
.track_points()
.map(|tp| {
let keys = tp.get("featureCenter").map(|p| p.keys.len()).unwrap_or(0);
let v = point_values(tp, layer.layer_time(s.time()));
json!({"name": tp.name, "uid": tp.uid, "center": v.spec.center, "featureSize": v.spec.feature_size, "searchOffset": v.spec.search_offset, "searchSize": v.spec.search_size, "attachOffset": v.attach_offset, "keys": keys})
})
.collect();
let trackers: Vec<V> = layer.trackers().map(|(g, _)| json!({"uid": g.uid, "name": g.name})).collect();
out["layer"] = json!(lid.0);
out["tracker"] = json!({"uid": uid, "name": g.name, "settings": st, "points": points});
out["trackers"] = json!(trackers);
}
Ok(out)
}