Files
effectcraft/crates/engine/src/commands/key_labels.rs
T
Brandon ThomasandClaude Opus 5.5 b03af6a979 M13.5: timeline/project column scrolling, keyframe label groups
Interface: Timeline outline columns wider than the pane scroll horizontally
(Shift+wheel/trackpad, scroll bar above the footer; headers clipped); the
Project panel's optional columns scroll under the frozen Name/Label columns.
ANM-1: keyframe colour labels (keys.setLabel, Label submenu, tinted key
icons) and Edit > Select Keyframe Label Group on selected/all layers and
visible keyframes (keys.selectLabelGroup replaces the stub). Alt-drag time
scaling of key groups already existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:48:33 -04:00

86 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Keyframe color labels: Label a keyframe (Keyframe ▸ Label ▸ colour) and Edit ▸ Select Keyframe
//! Label Group (On Selected Layers / On All Layers / Visible Keyframes on …).
use effectcraft_color::Label;
use serde_json::{Value, json};
use super::{CommandSpec, bad, has_comp, has_keys, str_p};
use crate::{EngineError, KeyRef, Result, Session, cmd};
fn label_p(p: &Value, c: &str) -> Result<u8> {
match p.get("label") {
Some(Value::String(s)) => {
let l = if s.eq_ignore_ascii_case("none") { Some(Label::None) } else { Label::from_name(s) };
let l = l.ok_or_else(|| bad(c, format!("unknown label `{s}`")))?;
Ok(Label::ALL.iter().position(|x| *x == l).unwrap_or(0) as u8)
}
Some(Value::Number(n)) => Ok(n.as_u64().filter(|i| (*i as usize) < Label::ALL.len()).ok_or_else(|| bad(c, "label index 0–16"))? as u8),
_ => Err(bad(c, "missing `label` (name or 0–16)")),
}
}
fn set_label(s: &mut Session, p: &Value) -> Result<Value> {
let c = "keys.setLabel";
let label = label_p(p, c)?;
let n = super::prop::edit_keys(s, "Keyframe Label", None, |keys, i, _| {
keys[i].label = label;
true
})?;
Ok(json!({"label": Label::ALL[label as usize].name(), "keys": n}))
}
/// Select every keyframe that shares a label with a selected keyframe, on the selected or all
/// layers. `visible` (property uids revealed in the Timeline) limits the "Visible Keyframes"
/// scopes; without it every property counts as visible.
fn select_label_group(s: &mut Session, p: &Value) -> Result<Value> {
let c = "keys.selectLabelGroup";
let scope = str_p(p, "scope").unwrap_or("selected");
let comp = s.active_comp().ok_or(EngineError::NoComp)?;
let labels: Vec<u8> =
s.state.selected_keys.iter().filter_map(|k| comp.layer(k.layer)?.props.find(k.prop)?.keys.iter().find(|x| x.time == k.time).map(|x| x.label)).collect();
if labels.is_empty() {
return Err(bad(c, "select a keyframe first"));
}
let only_selected = match scope {
"selected" | "visibleSelected" => true,
"all" | "visibleAll" => false,
_ => return Err(bad(c, "scope: selected|all|visibleSelected|visibleAll")),
};
let visible: Option<Vec<u64>> =
(scope.starts_with("visible")).then(|| p.get("visible").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).collect())).flatten();
let key_layers: Vec<_> = s.state.selected_keys.iter().map(|k| k.layer).collect();
let mut sel = Vec::new();
for l in &comp.layers {
if only_selected && !s.state.selected_layers.contains(&l.id) && !key_layers.contains(&l.id) {
continue;
}
l.props.walk("", &mut |_, pr| {
if visible.as_ref().is_some_and(|v| !v.contains(&pr.uid)) {
return;
}
for k in &pr.keys {
if labels.contains(&k.label) {
sel.push(KeyRef { layer: l.id, prop: pr.uid, time: k.time });
}
}
});
}
s.state.selected_keys = sel;
Ok(json!({"keys": s.state.selected_keys.len()}))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("keys.setLabel", "Keyframe Label", [], None, "{label: name|none|0–16}", has_keys, set_label),
cmd!(
"keys.selectLabelGroup",
"Select Keyframe Label Group",
[],
None,
"{scope: selected|all|visibleSelected|visibleAll, visible?: [prop uid]}",
has_comp,
select_label_group
),
]
}