mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
Merge main into M13.5 long-tail polish
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
//! Essential Graphics: a composition's exposed controls (the Essential Graphics panel) and the
|
||||
//! "Essential Properties" of precomp layers that nest it (per-instance override values, After
|
||||
//! Effects' Master Properties).
|
||||
//!
|
||||
//! - A comp's [`EssentialGraphics`] lists controls: properties of its layers (shown as text,
|
||||
//! colour, slider, checkbox, point, angle or dropdown controls), Media Replacement slots for
|
||||
//! footage layers, comments and groups.
|
||||
//! - Every layer that nests such a comp gets an **Essential Properties** group (match id
|
||||
//! `essential`, [`GroupKind::Essential`]) mirroring the controls, one child per control with
|
||||
//! match id `eg<control id>`. Changing a child marks it *overridden* for that instance only;
|
||||
//! [`sync_project`] keeps the groups in step with the controls (and refreshes the values of
|
||||
//! controls that are not overridden), [`with_overrides`] builds the project an instance renders.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::props::{GroupKind, Node, ParamUi, PropGroup, Property, Uid};
|
||||
use crate::{ItemId, ItemKind, Layer, LayerId, LayerSource, Project, Value};
|
||||
|
||||
/// Match id of the Essential Properties group on precomp layers.
|
||||
pub const GROUP: &str = "essential";
|
||||
|
||||
/// A composition's Essential Graphics definition.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EssentialGraphics {
|
||||
/// Template name (Essential Graphics panel ▸ name field; the exported template's title).
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub controls: Vec<EgControl>,
|
||||
}
|
||||
|
||||
/// One entry of the Essential Graphics panel.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EgControl {
|
||||
pub id: u64,
|
||||
/// Display name (renamable; defaults to the property's name).
|
||||
pub name: String,
|
||||
pub kind: EgKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EgKind {
|
||||
/// A property of a layer of the comp.
|
||||
Property { layer: LayerId, prop: Uid },
|
||||
/// Media Replacement: the footage of a footage layer can be swapped per instance.
|
||||
Media { layer: LayerId },
|
||||
/// A text note shown in the panel.
|
||||
Comment { text: String },
|
||||
/// A named, collapsible group of controls.
|
||||
Group {
|
||||
#[serde(default)]
|
||||
children: Vec<EgControl>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The kind of control a property becomes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ControlType {
|
||||
Text,
|
||||
Color,
|
||||
Slider,
|
||||
Checkbox,
|
||||
Point,
|
||||
Angle,
|
||||
Dropdown,
|
||||
Media,
|
||||
}
|
||||
|
||||
impl ControlType {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ControlType::Text => "Text",
|
||||
ControlType::Color => "Color",
|
||||
ControlType::Slider => "Slider",
|
||||
ControlType::Checkbox => "Checkbox",
|
||||
ControlType::Point => "Point",
|
||||
ControlType::Angle => "Angle",
|
||||
ControlType::Dropdown => "Dropdown",
|
||||
ControlType::Media => "Media",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The control a property is shown as, `None` when Essential Graphics can't expose it (paths,
|
||||
/// gradients, layer pickers, hidden data).
|
||||
pub fn control_type(p: &Property) -> Option<ControlType> {
|
||||
if matches!(p.ui, ParamUi::Hidden) {
|
||||
return None;
|
||||
}
|
||||
Some(match (&p.value, &p.ui) {
|
||||
(Value::Text(_) | Value::Str(_), _) => ControlType::Text,
|
||||
(Value::Color(_), _) => ControlType::Color,
|
||||
(Value::Bool(_), _) | (_, ParamUi::Checkbox) => ControlType::Checkbox,
|
||||
(Value::Enum(_), _) | (_, ParamUi::Popup { .. }) => ControlType::Dropdown,
|
||||
(_, ParamUi::Angle) => ControlType::Angle,
|
||||
(Value::Vec2(_) | Value::Vec3(_), _) => ControlType::Point,
|
||||
(Value::Scalar(_), _) => ControlType::Slider,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
impl EssentialGraphics {
|
||||
/// Every control, depth first (groups before their children).
|
||||
pub fn flat(&self) -> Vec<&EgControl> {
|
||||
fn go<'a>(v: &'a [EgControl], out: &mut Vec<&'a EgControl>) {
|
||||
for c in v {
|
||||
out.push(c);
|
||||
if let EgKind::Group { children } = &c.kind {
|
||||
go(children, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
go(&self.controls, &mut out);
|
||||
out
|
||||
}
|
||||
pub fn find(&self, id: u64) -> Option<&EgControl> {
|
||||
self.flat().into_iter().find(|c| c.id == id)
|
||||
}
|
||||
pub fn find_mut(&mut self, id: u64) -> Option<&mut EgControl> {
|
||||
fn go(v: &mut [EgControl], id: u64) -> Option<&mut EgControl> {
|
||||
for c in v {
|
||||
if c.id == id {
|
||||
return Some(c);
|
||||
}
|
||||
if let EgKind::Group { children } = &mut c.kind
|
||||
&& let Some(x) = go(children, id)
|
||||
{
|
||||
return Some(x);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
go(&mut self.controls, id)
|
||||
}
|
||||
/// Remove a control (anywhere) and return it.
|
||||
pub fn remove(&mut self, id: u64) -> Option<EgControl> {
|
||||
fn go(v: &mut Vec<EgControl>, id: u64) -> Option<EgControl> {
|
||||
if let Some(i) = v.iter().position(|c| c.id == id) {
|
||||
return Some(v.remove(i));
|
||||
}
|
||||
v.iter_mut().find_map(|c| if let EgKind::Group { children } = &mut c.kind { go(children, id) } else { None })
|
||||
}
|
||||
go(&mut self.controls, id)
|
||||
}
|
||||
/// The list a control is inserted into: a group's children or the top level.
|
||||
pub fn list_mut(&mut self, group: Option<u64>) -> Option<&mut Vec<EgControl>> {
|
||||
match group {
|
||||
None => Some(&mut self.controls),
|
||||
Some(g) => match &mut self.find_mut(g)?.kind {
|
||||
EgKind::Group { children } => Some(children),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The control exposing property `prop` of `layer`, if any.
|
||||
pub fn control_for(&self, layer: LayerId, prop: Uid) -> Option<&EgControl> {
|
||||
self.flat().into_iter().find(|c| matches!(c.kind, EgKind::Property { layer: l, prop: p } if l == layer && p == prop))
|
||||
}
|
||||
/// Controls an instance can override (properties and media; not comments or groups).
|
||||
pub fn has_instance_controls(&self) -> bool {
|
||||
self.flat().iter().any(|c| matches!(c.kind, EgKind::Property { .. } | EgKind::Media { .. }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Match id of an instance property / group for a control.
|
||||
pub fn match_id(control: u64) -> String {
|
||||
format!("eg{control}")
|
||||
}
|
||||
|
||||
/// Control id of an Essential Properties child (`eg42` → 42).
|
||||
pub fn control_of(match_id: &str) -> Option<u64> {
|
||||
match_id.strip_prefix("eg")?.parse().ok()
|
||||
}
|
||||
|
||||
/// The Essential Properties group of a layer.
|
||||
pub fn group(layer: &Layer) -> Option<&PropGroup> {
|
||||
layer.props.sub(GROUP)
|
||||
}
|
||||
|
||||
/// Uids of the overridden master properties of a layer.
|
||||
pub fn overridden(layer: &Layer) -> Vec<Uid> {
|
||||
match group(layer).map(|g| &g.kind) {
|
||||
Some(GroupKind::Essential { overridden }) => overridden.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// The source property a control exposes, in `project`.
|
||||
pub fn source_prop<'a>(project: &'a Project, comp: ItemId, c: &EgControl) -> Option<(&'a Layer, &'a Property)> {
|
||||
let EgKind::Property { layer, prop } = c.kind else { return None };
|
||||
let l = project.comp(comp)?.layer(layer)?;
|
||||
Some((l, l.props.find(prop)?))
|
||||
}
|
||||
|
||||
/// The footage item a media control shows by default (the layer's own source).
|
||||
pub fn source_media(project: &Project, comp: ItemId, c: &EgControl) -> Option<ItemId> {
|
||||
let EgKind::Media { layer } = c.kind else { return None };
|
||||
match project.comp(comp)?.layer(layer)?.source {
|
||||
LayerSource::Footage { item } => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the Essential Properties group a layer nesting `comp` should have, reusing uids and
|
||||
/// override values from `old`. Uids for new nodes come from `next`.
|
||||
fn build_group(project: &Project, comp: ItemId, eg: &EssentialGraphics, old: Option<&PropGroup>, next: &mut u64) -> PropGroup {
|
||||
let over = match old.map(|g| &g.kind) {
|
||||
Some(GroupKind::Essential { overridden }) => overridden.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let mut alloc = || {
|
||||
let v = *next;
|
||||
*next += 1;
|
||||
v
|
||||
};
|
||||
fn find_old<'a>(old: Option<&'a PropGroup>, m: &str) -> Option<&'a Node> {
|
||||
let g = old?;
|
||||
g.children.iter().find(|c| c.match_id() == m).or_else(|| g.groups().find_map(|sub| find_old(Some(sub), m)))
|
||||
}
|
||||
fn build(
|
||||
project: &Project,
|
||||
comp: ItemId,
|
||||
list: &[EgControl],
|
||||
old: Option<&PropGroup>,
|
||||
over: &[Uid],
|
||||
alloc: &mut dyn FnMut() -> u64,
|
||||
keep: &mut Vec<Uid>,
|
||||
) -> Vec<Node> {
|
||||
let mut out = vec![];
|
||||
for c in list {
|
||||
let m = match_id(c.id);
|
||||
let prev = find_old(old, &m);
|
||||
match &c.kind {
|
||||
EgKind::Comment { .. } => {}
|
||||
EgKind::Group { children } => {
|
||||
let uid = prev.map(Node::uid).unwrap_or_else(&mut *alloc);
|
||||
let mut g = PropGroup::new(uid, &m, &c.name);
|
||||
g.children = build(project, comp, children, old, over, alloc, keep);
|
||||
out.push(Node::Group(g));
|
||||
}
|
||||
EgKind::Property { .. } => {
|
||||
let Some((_, src)) = source_prop(project, comp, c) else { continue };
|
||||
let prev = prev.and_then(Node::as_prop);
|
||||
let uid = prev.map(|p| p.uid).unwrap_or_else(&mut *alloc);
|
||||
let mut p = Property { uid, match_id: m, name: c.name.clone(), keys: vec![], expr: None, ..src.clone() };
|
||||
// Instance properties follow the source's look but are always editable.
|
||||
p.static_only = false;
|
||||
if let Some(prev) = prev.filter(|pp| over.contains(&pp.uid)) {
|
||||
p.value = prev.value.clone();
|
||||
p.keys = prev.keys.clone();
|
||||
p.expr = prev.expr.clone();
|
||||
keep.push(uid);
|
||||
} else {
|
||||
p.value = src.value_at(effectcraft_time::Tick::ZERO);
|
||||
}
|
||||
out.push(Node::Prop(p));
|
||||
}
|
||||
EgKind::Media { .. } => {
|
||||
let Some(item) = source_media(project, comp, c) else { continue };
|
||||
let prev = prev.and_then(Node::as_prop);
|
||||
let uid = prev.map(|p| p.uid).unwrap_or_else(&mut *alloc);
|
||||
let mut p = Property::new(uid, &m, &c.name, Value::Scalar(item.0 as f64)).with_ui(ParamUi::Hidden);
|
||||
p.static_only = true;
|
||||
if let Some(prev) = prev.filter(|pp| over.contains(&pp.uid)) {
|
||||
p.value = prev.value.clone();
|
||||
keep.push(uid);
|
||||
}
|
||||
out.push(Node::Prop(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
let uid = old.map(|g| g.uid).unwrap_or_else(&mut alloc);
|
||||
let mut keep = vec![];
|
||||
let children = build(project, comp, &eg.controls, old, &over, &mut alloc, &mut keep);
|
||||
PropGroup { uid, match_id: GROUP.into(), name: "Essential Properties".into(), kind: GroupKind::Essential { overridden: keep }, enabled: true, children }
|
||||
}
|
||||
|
||||
/// Master properties the user changed in this edit (`before` → `after`) become overridden.
|
||||
fn mark_overrides(before: &Project, after: &mut Project) {
|
||||
let ids: Vec<ItemId> = after.comps().map(|(i, _)| *i).collect();
|
||||
for cid in ids {
|
||||
let Some(comp) = after.comp(cid) else { continue };
|
||||
let Some(old_comp) = before.comp(cid) else { continue };
|
||||
let mut changes: Vec<(LayerId, Vec<Uid>)> = vec![];
|
||||
for l in &comp.layers {
|
||||
let Some(g) = group(l) else { continue };
|
||||
let Some(ol) = old_comp.layer(l.id) else { continue };
|
||||
let Some(og) = group(ol) else { continue };
|
||||
// Properties overridden before this edit stay as the edit left them (Revert and
|
||||
// Push to Comp clear overrides by editing them).
|
||||
let mut over = overridden(l);
|
||||
over.extend(overridden(ol));
|
||||
let mut add = vec![];
|
||||
g.walk("", &mut |_, p| {
|
||||
if over.contains(&p.uid) {
|
||||
return;
|
||||
}
|
||||
if let Some(op) = og.find(p.uid)
|
||||
&& (op.value != p.value || op.keys != p.keys || op.expr != p.expr)
|
||||
{
|
||||
add.push(p.uid);
|
||||
}
|
||||
});
|
||||
if !add.is_empty() {
|
||||
changes.push((l.id, add));
|
||||
}
|
||||
}
|
||||
if changes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(c) = after.comp_mut(cid) else { continue };
|
||||
for (lid, add) in changes {
|
||||
if let Some(g) = c.layer_mut(lid).and_then(|l| l.props.sub_mut(GROUP))
|
||||
&& let GroupKind::Essential { overridden } = &mut g.kind
|
||||
{
|
||||
overridden.extend(add);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// After an edit: mark changed master properties as overridden, then add, update or remove the
|
||||
/// Essential Properties groups of every precomp layer so they match their comp's controls.
|
||||
pub fn sync_project(before: &Project, after: &mut Project) {
|
||||
mark_overrides(before, after);
|
||||
let ids: Vec<ItemId> = after.comps().map(|(i, _)| *i).collect();
|
||||
let mut next = after.next_id;
|
||||
for cid in ids {
|
||||
let Some(comp) = after.comp(cid) else { continue };
|
||||
let mut updates: Vec<(usize, Option<PropGroup>)> = vec![];
|
||||
for (i, l) in comp.layers.iter().enumerate() {
|
||||
let want = match l.source {
|
||||
LayerSource::Comp { item } => after.comp(item).and_then(|c| c.essential.as_ref()).filter(|e| e.has_instance_controls()).map(|e| (item, e)),
|
||||
_ => None,
|
||||
};
|
||||
let old = group(l);
|
||||
match want {
|
||||
Some((item, eg)) => {
|
||||
let g = build_group(after, item, eg, old, &mut next);
|
||||
if old != Some(&g) {
|
||||
updates.push((i, Some(g)));
|
||||
}
|
||||
}
|
||||
None if old.is_some() => updates.push((i, None)),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if updates.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(c) = after.comp_mut(cid) else { continue };
|
||||
for (i, g) in updates {
|
||||
let l = &mut c.layers[i];
|
||||
let pos = l.props.children.iter().position(|n| n.match_id() == GROUP);
|
||||
match (pos, g) {
|
||||
(Some(p), Some(g)) => l.props.children[p] = Node::Group(g),
|
||||
(None, Some(g)) => l.props.children.insert(0, Node::Group(g)),
|
||||
(Some(p), None) => {
|
||||
l.props.children.remove(p);
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
after.next_id = after.next_id.max(next);
|
||||
}
|
||||
|
||||
/// One instance override: the control and the value it takes in this instance.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Override {
|
||||
pub control: u64,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// The project as an instance of `comp` renders it: each overridden control's source property
|
||||
/// set to the instance value (keyframes dropped; its expression, if any, still applies) and
|
||||
/// replaced media swapped in. `None` when nothing applies.
|
||||
pub fn with_overrides(project: &Project, comp: ItemId, overrides: &[Override]) -> Option<Project> {
|
||||
let eg = project.comp(comp)?.essential.clone()?;
|
||||
let mut p = project.clone();
|
||||
let c = p.comp_mut(comp)?;
|
||||
let mut any = false;
|
||||
for o in overrides {
|
||||
let Some(ctl) = eg.find(o.control) else { continue };
|
||||
match ctl.kind {
|
||||
EgKind::Property { layer, prop } => {
|
||||
if let Some(pr) = c.layer_mut(layer).and_then(|l| l.props.find_mut(prop)) {
|
||||
pr.keys.clear();
|
||||
pr.value = o.value.clone();
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
EgKind::Media { layer } => {
|
||||
let item = ItemId(o.value.as_f64().max(0.0) as u64);
|
||||
// Replacement media must be footage.
|
||||
if !matches!(project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(_))) {
|
||||
continue;
|
||||
}
|
||||
if let Some(l) = c.layer_mut(layer) {
|
||||
l.source = LayerSource::Footage { item };
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
any.then_some(p)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------ id remapping
|
||||
|
||||
/// Shift every id (items, layers, property uids, Essential Graphics controls) of `p` by
|
||||
/// `offset`, keeping all references consistent: used when merging a template project into
|
||||
/// another project.
|
||||
pub fn offset_ids(p: &mut Project, offset: u64) {
|
||||
let items = std::mem::take(&mut p.items);
|
||||
for (_, mut it) in items {
|
||||
it.id = ItemId(it.id.0 + offset);
|
||||
it.parent = it.parent.map(|f| ItemId(f.0 + offset));
|
||||
if let ItemKind::Comp(c) = &mut it.kind {
|
||||
let c = std::sync::Arc::make_mut(c);
|
||||
for l in &mut c.layers {
|
||||
offset_layer(l, offset);
|
||||
}
|
||||
if let Some(eg) = &mut c.essential {
|
||||
fn go(v: &mut [EgControl], o: u64) {
|
||||
for c in v {
|
||||
c.id += o;
|
||||
match &mut c.kind {
|
||||
EgKind::Property { layer, prop } => {
|
||||
*layer = LayerId(layer.0 + o);
|
||||
*prop += o;
|
||||
}
|
||||
EgKind::Media { layer } => *layer = LayerId(layer.0 + o),
|
||||
EgKind::Group { children } => go(children, o),
|
||||
EgKind::Comment { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
go(&mut eg.controls, offset);
|
||||
}
|
||||
}
|
||||
p.items.insert(it.id, it);
|
||||
}
|
||||
p.render_queue.clear();
|
||||
p.next_id += offset;
|
||||
}
|
||||
|
||||
fn offset_layer(l: &mut Layer, o: u64) {
|
||||
l.id = LayerId(l.id.0 + o);
|
||||
l.parent = l.parent.map(|x| LayerId(x.0 + o));
|
||||
if let Some(tm) = &mut l.track_matte {
|
||||
tm.layer = LayerId(tm.layer.0 + o);
|
||||
}
|
||||
match &mut l.source {
|
||||
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => *item = ItemId(item.0 + o),
|
||||
_ => {}
|
||||
}
|
||||
fn group(g: &mut PropGroup, o: u64, essential_parent: bool) {
|
||||
g.uid += o;
|
||||
if let GroupKind::Essential { overridden } = &mut g.kind {
|
||||
for u in overridden.iter_mut() {
|
||||
*u += o;
|
||||
}
|
||||
}
|
||||
let is_essential = matches!(g.kind, GroupKind::Essential { .. }) || essential_parent;
|
||||
for c in &mut g.children {
|
||||
match c {
|
||||
Node::Prop(p) => {
|
||||
p.uid += o;
|
||||
let fix = |v: &mut Value| {
|
||||
if let Value::Layer(Some(id)) = v {
|
||||
*id += o;
|
||||
}
|
||||
};
|
||||
fix(&mut p.value);
|
||||
for k in &mut p.keys {
|
||||
fix(&mut k.value);
|
||||
}
|
||||
if is_essential && control_of(&p.match_id).is_some() {
|
||||
p.match_id = match_id(control_of(&p.match_id).unwrap_or(0) + o);
|
||||
// Media replacement values are item ids.
|
||||
if matches!(p.ui, ParamUi::Hidden)
|
||||
&& let Value::Scalar(x) = &mut p.value
|
||||
{
|
||||
*x += o as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Group(sub) => {
|
||||
if is_essential && let Some(id) = control_of(&sub.match_id) {
|
||||
sub.match_id = match_id(id + o);
|
||||
}
|
||||
group(sub, o, is_essential);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
group(&mut l.props, o, false);
|
||||
}
|
||||
|
||||
/// The items `comp` needs (itself, nested comps, footage, solids), transitively.
|
||||
pub fn dependencies(project: &Project, comp: ItemId) -> Vec<ItemId> {
|
||||
let mut out = vec![];
|
||||
let mut stack = vec![comp];
|
||||
while let Some(id) = stack.pop() {
|
||||
if out.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
let Some(it) = project.item(id) else { continue };
|
||||
out.push(id);
|
||||
if let ItemKind::Comp(c) = &it.kind {
|
||||
for l in &c.layers {
|
||||
if let Some(i) = l.source.item() {
|
||||
stack.push(i);
|
||||
}
|
||||
// Media replacement candidates referenced by instances inside.
|
||||
if let Some(g) = group(l) {
|
||||
g.walk("", &mut |_, p| {
|
||||
if matches!(p.ui, ParamUi::Hidden)
|
||||
&& control_of(&p.match_id).is_some()
|
||||
&& let Value::Scalar(x) = p.value
|
||||
{
|
||||
stack.push(ItemId(x as u64));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
+129
-1
@@ -5,6 +5,7 @@
|
||||
//! change.
|
||||
|
||||
pub mod build;
|
||||
pub mod essential;
|
||||
pub mod props;
|
||||
pub mod render_queue;
|
||||
pub mod styles;
|
||||
@@ -219,6 +220,9 @@ pub struct Comp {
|
||||
/// Viewer guides (View ▸ Add Guide…), in comp pixels.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub guides: Vec<Guide>,
|
||||
/// Essential Graphics: the controls this comp exposes to instances and templates.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub essential: Option<essential::EssentialGraphics>,
|
||||
}
|
||||
|
||||
/// A viewer guide line: vertical guides sit at an x position, horizontal ones at a y position.
|
||||
@@ -261,6 +265,7 @@ impl Comp {
|
||||
poster_time: Tick::ZERO,
|
||||
guides: vec![],
|
||||
global_light: styles::GlobalLight::default(),
|
||||
essential: None,
|
||||
}
|
||||
}
|
||||
pub fn layer(&self, id: LayerId) -> Option<&Layer> {
|
||||
@@ -600,6 +605,38 @@ pub enum FootageKind {
|
||||
Audio,
|
||||
/// A 3D model (glTF 2.0 / OBJ), placed as a model layer.
|
||||
Model,
|
||||
/// A data file (JSON, CSV, TSV) for data-driven animation: read by expressions through
|
||||
/// `footage(name).sourceData` / `sourceText` / `dataValue`; it has no pixels.
|
||||
Data,
|
||||
}
|
||||
|
||||
/// Interpret Footage ▸ Fields and Pulldown ▸ Separate Fields.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum FieldOrder {
|
||||
/// Progressive frames.
|
||||
#[default]
|
||||
Off,
|
||||
/// Interlaced, upper (odd) field first: each field becomes a frame at twice the rate.
|
||||
UpperFirst,
|
||||
LowerFirst,
|
||||
}
|
||||
|
||||
impl FieldOrder {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
FieldOrder::Off => "Off",
|
||||
FieldOrder::UpperFirst => "Upper Field First",
|
||||
FieldOrder::LowerFirst => "Lower Field First",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Option<FieldOrder> {
|
||||
match s.to_ascii_lowercase().replace([' ', '_', '-'], "").as_str() {
|
||||
"off" | "none" | "progressive" => Some(FieldOrder::Off),
|
||||
"upper" | "upperfirst" | "upperfieldfirst" => Some(FieldOrder::UpperFirst),
|
||||
"lower" | "lowerfirst" | "lowerfieldfirst" => Some(FieldOrder::LowerFirst),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -639,6 +676,93 @@ pub struct Footage {
|
||||
/// Colour profile (from the file's metadata, or Interpret Footage). `None` = sRGB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color_profile: Option<ColorSpace>,
|
||||
/// One layer of a layered file (Photoshop) instead of its merged image.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub layer: Option<SourceLayer>,
|
||||
/// Interpret Footage ▸ Fields and Pulldown ▸ Separate Fields.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub fields: FieldOrder,
|
||||
/// Interpret Footage ▸ Alpha ▸ Invert Alpha.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub invert_alpha: bool,
|
||||
/// Interpret Footage ▸ Color ▸ Interpret As Linear Light: the file's values are linear.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub linear_light: bool,
|
||||
/// Data footage ([`FootageKind::Data`]): the file's text (JSON, CSV or TSV), kept in the
|
||||
/// project so expressions read it everywhere (and on the web).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<String>,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
|
||||
*v == T::default()
|
||||
}
|
||||
|
||||
impl Default for Footage {
|
||||
fn default() -> Self {
|
||||
Footage {
|
||||
path: String::new(),
|
||||
kind: FootageKind::Still,
|
||||
width: 0,
|
||||
height: 0,
|
||||
pixel_aspect: 1.0,
|
||||
frame_rate: FrameRate::FPS_30,
|
||||
native_rate: None,
|
||||
duration: Tick::ZERO,
|
||||
has_video: false,
|
||||
has_audio: false,
|
||||
alpha: AlphaMode::Straight,
|
||||
premul_color: [0.0; 3],
|
||||
loop_count: 1,
|
||||
codec: String::new(),
|
||||
missing: false,
|
||||
sequence: vec![],
|
||||
color_profile: None,
|
||||
fields: FieldOrder::Off,
|
||||
invert_alpha: false,
|
||||
linear_light: false,
|
||||
data: None,
|
||||
layer: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Footage {
|
||||
/// The data format of data footage: `json`, `csv` or `tsv` (from the file extension).
|
||||
pub fn data_format(&self) -> Option<&'static str> {
|
||||
if self.kind != FootageKind::Data {
|
||||
return None;
|
||||
}
|
||||
let ext = std::path::Path::new(&self.path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
|
||||
Some(match ext.as_str() {
|
||||
"csv" => "csv",
|
||||
"tsv" | "tab" => "tsv",
|
||||
_ => "json",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A proxy: a stand-in file used instead of a footage item or composition (File ▸ Set Proxy /
|
||||
/// Create Proxy), when its Use Proxy switch is on and the render's Proxy Use allows it.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Proxy {
|
||||
/// The proxy's footage (path, size, rate…; File ▸ Interpret Footage ▸ Proxy edits it).
|
||||
pub footage: Footage,
|
||||
/// Use Proxy (the Project panel's proxy indicator: filled = in use, hollow = set but off).
|
||||
#[serde(default = "yes")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// A layer of a layered still (a Photoshop document) used as footage
|
||||
/// (File ▸ Import ▸ Composition / Composition – Retain Layer Sizes, or Choose Layer).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SourceLayer {
|
||||
/// Layer record index in the file (bottom of the stack = 0).
|
||||
pub index: u32,
|
||||
pub name: String,
|
||||
/// The footage is the layer's own bounds (Retain Layer Sizes) rather than the document size.
|
||||
#[serde(default)]
|
||||
pub layer_size: bool,
|
||||
}
|
||||
|
||||
fn one() -> u32 {
|
||||
@@ -673,6 +797,9 @@ pub struct Item {
|
||||
#[serde(default)]
|
||||
pub parent: Option<ItemId>,
|
||||
pub kind: ItemKind,
|
||||
/// The item's proxy (footage and compositions).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proxy: Option<Box<Proxy>>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
@@ -686,6 +813,7 @@ impl Item {
|
||||
FootageKind::Sequence => "Image Sequence",
|
||||
FootageKind::Audio => "Audio",
|
||||
FootageKind::Model => "3D Model",
|
||||
FootageKind::Data => "Data",
|
||||
},
|
||||
ItemKind::Solid(_) => "Solid",
|
||||
}
|
||||
@@ -763,7 +891,7 @@ impl Project {
|
||||
}
|
||||
pub fn add_item(&mut self, name: &str, label: Label, parent: Option<ItemId>, kind: ItemKind) -> ItemId {
|
||||
let id = ItemId(self.alloc());
|
||||
self.items.insert(id, Item { id, name: name.into(), label, comment: String::new(), parent, kind });
|
||||
self.items.insert(id, Item { id, name: name.into(), label, comment: String::new(), parent, kind, proxy: None });
|
||||
id
|
||||
}
|
||||
/// Children of a folder (None = root), folders first then by name.
|
||||
|
||||
@@ -181,6 +181,13 @@ pub enum GroupKind {
|
||||
},
|
||||
/// A motion tracker (Motion Trackers ▸ Tracker n) with its Tracker panel settings.
|
||||
Tracker { settings: Box<crate::tracking::TrackerSettings> },
|
||||
/// The Essential Properties of a precomp layer (master properties): one child per control
|
||||
/// of the nested comp's Essential Graphics; `overridden` lists the children (uids) whose
|
||||
/// values this instance overrides.
|
||||
Essential {
|
||||
#[serde(default)]
|
||||
overridden: Vec<Uid>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Mask motion blur (sub-frame samples of an animated mask path).
|
||||
|
||||
@@ -39,6 +39,51 @@ pub enum TimeSpan {
|
||||
Custom { start: Tick, end: Tick },
|
||||
}
|
||||
|
||||
/// Render Settings ▸ Proxy Use: which proxies a render uses.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ProxyUse {
|
||||
/// Each item's Use Proxy switch (what the viewer shows).
|
||||
#[default]
|
||||
CurrentSettings,
|
||||
/// Every item that has a proxy uses it.
|
||||
UseAll,
|
||||
/// Only compositions' proxies.
|
||||
UseCompOnly,
|
||||
/// No proxies (full-resolution sources).
|
||||
UseNone,
|
||||
}
|
||||
|
||||
impl ProxyUse {
|
||||
pub const ALL: [ProxyUse; 4] = [ProxyUse::CurrentSettings, ProxyUse::UseAll, ProxyUse::UseCompOnly, ProxyUse::UseNone];
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ProxyUse::CurrentSettings => "Current Settings",
|
||||
ProxyUse::UseAll => "Use All Proxies",
|
||||
ProxyUse::UseCompOnly => "Use Comp Proxies Only",
|
||||
ProxyUse::UseNone => "Use No Proxies",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Option<ProxyUse> {
|
||||
match s.to_ascii_lowercase().replace([' ', '_', '-'], "").as_str() {
|
||||
"current" | "currentsettings" => Some(ProxyUse::CurrentSettings),
|
||||
"all" | "useall" | "useallproxies" => Some(ProxyUse::UseAll),
|
||||
"comp" | "componly" | "usecompproxiesonly" | "usecomponly" => Some(ProxyUse::UseCompOnly),
|
||||
"none" | "usenone" | "usenoproxies" => Some(ProxyUse::UseNone),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Whether an item's proxy is used: `comp` = the item is a composition, `enabled` = its Use
|
||||
/// Proxy switch.
|
||||
pub fn uses(self, comp: bool, enabled: bool) -> bool {
|
||||
match self {
|
||||
ProxyUse::CurrentSettings => enabled,
|
||||
ProxyUse::UseAll => true,
|
||||
ProxyUse::UseCompOnly => comp,
|
||||
ProxyUse::UseNone => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RenderSettings {
|
||||
@@ -52,6 +97,8 @@ pub struct RenderSettings {
|
||||
pub motion_blur: bool,
|
||||
/// Image sequences: skip frames whose file already exists.
|
||||
pub skip_existing: bool,
|
||||
/// Proxy Use (Best Settings: Use No Proxies).
|
||||
pub proxy_use: ProxyUse,
|
||||
}
|
||||
|
||||
impl Default for RenderSettings {
|
||||
@@ -63,6 +110,7 @@ impl Default for RenderSettings {
|
||||
frame_rate: None,
|
||||
motion_blur: true,
|
||||
skip_existing: false,
|
||||
proxy_use: ProxyUse::UseNone,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,17 +184,26 @@ pub enum OutputFormat {
|
||||
ExrSequence,
|
||||
/// Animated GIF.
|
||||
Gif,
|
||||
/// WebM: VP9 video (alpha with RGB + Alpha) + Opus audio.
|
||||
WebM,
|
||||
/// Audio only: WAV (16-bit PCM).
|
||||
Wav,
|
||||
/// Audio only: AIFF (16-bit PCM).
|
||||
Aiff,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub const ALL: [OutputFormat; 7] = [
|
||||
pub const ALL: [OutputFormat; 10] = [
|
||||
OutputFormat::H264,
|
||||
OutputFormat::ProRes,
|
||||
OutputFormat::WebM,
|
||||
OutputFormat::PngSequence,
|
||||
OutputFormat::JpegSequence,
|
||||
OutputFormat::TiffSequence,
|
||||
OutputFormat::ExrSequence,
|
||||
OutputFormat::Gif,
|
||||
OutputFormat::Wav,
|
||||
OutputFormat::Aiff,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
@@ -158,6 +215,9 @@ impl OutputFormat {
|
||||
OutputFormat::TiffSequence => "TIFF Sequence",
|
||||
OutputFormat::ExrSequence => "OpenEXR Sequence",
|
||||
OutputFormat::Gif => "Animated GIF",
|
||||
OutputFormat::WebM => "WebM (VP9 + Opus)",
|
||||
OutputFormat::Wav => "WAV",
|
||||
OutputFormat::Aiff => "AIFF",
|
||||
}
|
||||
}
|
||||
pub fn extension(self) -> &'static str {
|
||||
@@ -169,19 +229,26 @@ impl OutputFormat {
|
||||
OutputFormat::TiffSequence => "tif",
|
||||
OutputFormat::ExrSequence => "exr",
|
||||
OutputFormat::Gif => "gif",
|
||||
OutputFormat::WebM => "webm",
|
||||
OutputFormat::Wav => "wav",
|
||||
OutputFormat::Aiff => "aif",
|
||||
}
|
||||
}
|
||||
pub fn is_sequence(self) -> bool {
|
||||
matches!(self, OutputFormat::PngSequence | OutputFormat::JpegSequence | OutputFormat::TiffSequence | OutputFormat::ExrSequence)
|
||||
}
|
||||
pub fn is_movie(self) -> bool {
|
||||
matches!(self, OutputFormat::H264 | OutputFormat::ProRes)
|
||||
matches!(self, OutputFormat::H264 | OutputFormat::ProRes | OutputFormat::WebM)
|
||||
}
|
||||
/// Audio-only outputs (no video is rendered).
|
||||
pub fn is_audio_only(self) -> bool {
|
||||
matches!(self, OutputFormat::Wav | OutputFormat::Aiff)
|
||||
}
|
||||
pub fn supports_alpha(self) -> bool {
|
||||
!matches!(self, OutputFormat::H264 | OutputFormat::JpegSequence)
|
||||
!matches!(self, OutputFormat::H264 | OutputFormat::JpegSequence | OutputFormat::Wav | OutputFormat::Aiff)
|
||||
}
|
||||
pub fn supports_audio(self) -> bool {
|
||||
self.is_movie()
|
||||
self.is_movie() || self.is_audio_only()
|
||||
}
|
||||
/// The written frame size for a rendered size: H.264 needs even dimensions (rounded down,
|
||||
/// at least 2).
|
||||
@@ -201,6 +268,9 @@ impl OutputFormat {
|
||||
"tif" | "tiff" | "tiffsequence" => OutputFormat::TiffSequence,
|
||||
"exr" | "openexr" | "openexrsequence" => OutputFormat::ExrSequence,
|
||||
"gif" | "animatedgif" => OutputFormat::Gif,
|
||||
"webm" | "vp9" | "webmvp9opus" => OutputFormat::WebM,
|
||||
"wav" | "wave" => OutputFormat::Wav,
|
||||
"aif" | "aiff" | "aifc" => OutputFormat::Aiff,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -419,6 +489,8 @@ pub enum PostRenderAction {
|
||||
Import,
|
||||
/// Import it and replace every use of the rendered composition (Composition ▸ Pre-render).
|
||||
ImportAndReplace,
|
||||
/// Set the rendered file as the composition's proxy (File ▸ Create Proxy).
|
||||
SetProxy,
|
||||
}
|
||||
|
||||
impl PostRenderAction {
|
||||
@@ -430,6 +502,7 @@ impl PostRenderAction {
|
||||
PostRenderAction::None => "None",
|
||||
PostRenderAction::Import => "Import",
|
||||
PostRenderAction::ImportAndReplace => "Import & Replace Usage",
|
||||
PostRenderAction::SetProxy => "Set Proxy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user