use std::collections::{HashMap, HashSet};
use crate::gf;
use crate::sdf::schema::FieldKey;
use crate::sdf::{self, AssetPath, LayerOffset, Path, Value};
use crate::tf;
use super::asset_resolve::{self, AssetSite};
use super::clip_manifest::{self, ClipSetKey};
use super::diagnostics::Diagnostics;
use super::index_cache::block_to_none;
use super::layer_graph::LayerGraph;
use super::prim_graph::Node;
use super::value_resolve::ValueState;
use super::{ClipLoad, CompositionDiagnostic, LayerId, LayerStackId, QueryError};
pub(crate) mod keys {
pub const ASSET_PATHS: &str = "assetPaths";
pub const MANIFEST_ASSET_PATH: &str = "manifestAssetPath";
pub const PRIM_PATH: &str = "primPath";
pub const ACTIVE: &str = "active";
pub const TIMES: &str = "times";
pub const INTERPOLATE_MISSING: &str = "interpolateMissingClipValues";
pub const TEMPLATE_ASSET_PATH: &str = "templateAssetPath";
pub const TEMPLATE_START_TIME: &str = "templateStartTime";
pub const TEMPLATE_END_TIME: &str = "templateEndTime";
pub const TEMPLATE_STRIDE: &str = "templateStride";
pub const TEMPLATE_ACTIVE_OFFSET: &str = "templateActiveOffset";
}
pub(crate) const CLIP_FIELDS: [FieldKey; 2] = [FieldKey::Clips, FieldKey::ClipSets];
pub(crate) fn is_clip_field(field: &str) -> bool {
CLIP_FIELDS.iter().any(|key| key.as_str() == field)
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ClipSet {
pub name: String,
pub prim_path: Option<Path>,
pub manifest_asset: Option<String>,
pub asset_paths: Vec<AssetPath>,
pub active: Vec<(f64, usize)>,
pub times: Vec<gf::Vec2d>,
pub interpolate_missing: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ResolvedClipSet {
pub set: ClipSet,
pub source: ClipAnchor,
pub manifest_layer: Option<LayerId>,
pub active_offset: LayerOffset,
}
impl ResolvedClipSet {
pub(super) fn key(&self, anchor: &Path) -> ClipSetKey {
ClipSetKey {
prim: anchor.clone(),
clip_set: self.set.name.clone(),
}
}
pub(super) fn same_resolution(&self, other: &Self) -> bool {
self == other && self.set.resolution_paths().eq(other.set.resolution_paths())
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ClipAnchor {
pub layer: LayerId,
pub prim_path: Path,
pub stack: LayerStackId,
}
impl ClipAnchor {
pub(super) fn applies_at(&self, node: &Node, layer: LayerId) -> bool {
self.layer == layer && node.layer_stack_id() == self.stack && node.path().has_prefix(&self.prim_path)
}
}
impl ClipSet {
pub(super) fn resolution_paths(&self) -> impl Iterator<Item = &str> {
self.asset_paths.iter().map(AssetPath::asset_path)
}
pub(crate) fn parse(clips: &Value, clip_sets_order: Option<&[String]>) -> Vec<ClipSet> {
let Value::Dictionary(sets) = clips else {
return Vec::new();
};
effective_set_names(sets, clip_sets_order)
.into_iter()
.filter_map(|name| match sets.get(name) {
Some(Value::Dictionary(set)) => Self::parse_set(name, set),
_ => None,
})
.collect()
}
fn parse_set(name: &str, set: &HashMap<String, Value>) -> Option<ClipSet> {
let prim_path = set
.get(keys::PRIM_PATH)
.and_then(Value::as_str)
.and_then(|s| Path::new(s).ok());
let manifest_asset = asset_input(set, keys::MANIFEST_ASSET_PATH).map(str::to_owned);
let (asset_paths, active, times) = match explicit_asset_paths(set) {
Some(asset_paths) => {
let mut active: Vec<(f64, usize)> = get::<Vec<gf::Vec2d>>(set, keys::ACTIVE)
.unwrap_or_default()
.into_iter()
.map(|p| Some((p.x.is_finite().then_some(p.x)?, clip_index(p.y)?)))
.collect::<Option<_>>()?;
active.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut times = get::<Vec<gf::Vec2d>>(set, keys::TIMES).unwrap_or_default();
if times.iter().any(|k| !k.x.is_finite() || !k.y.is_finite()) {
return None;
}
times.sort_by(|a, b| a.x.total_cmp(&b.x));
(asset_paths, active, times)
}
None => expand_template(set)?,
};
let interpolate_missing = get::<bool>(set, keys::INTERPOLATE_MISSING).unwrap_or(false);
if active.iter().any(|&(_, index)| index >= asset_paths.len()) {
return None;
}
Some(ClipSet {
name: name.to_string(),
prim_path,
manifest_asset,
asset_paths,
active,
times,
interpolate_missing,
})
}
pub(crate) fn clip_prim_path(&self, anchor: &Path) -> Path {
self.prim_path.clone().unwrap_or_else(|| anchor.clone())
}
pub(crate) fn active_entry(&self, stage_time: f64) -> Option<(f64, usize)> {
let mut chosen = *self.active.first()?;
for &entry in &self.active {
if entry.0 <= stage_time {
chosen = entry;
} else {
break;
}
}
Some(chosen)
}
pub(crate) fn map_stage_to_clip(&self, stage_time: f64) -> f64 {
map_stage_to_clip(&self.times, stage_time)
}
pub(crate) fn retime_stage_times(&mut self, offset: LayerOffset) {
if offset.is_identity() {
return;
}
for (stage, _) in &mut self.active {
*stage = offset.apply(*stage);
}
for knot in &mut self.times {
knot.x = offset.apply(knot.x);
}
if offset.scale < 0.0 {
self.active.sort_by(|a, b| a.0.total_cmp(&b.0));
self.times.sort_by(|a, b| a.x.total_cmp(&b.x));
}
}
pub(crate) fn stage_sample_times(&self, per_clip: &[Vec<f64>]) -> Vec<f64> {
let mut out: Vec<f64> = Vec::new();
for (k, &(start, clip_index)) in self.active.iter().enumerate() {
let samples = per_clip.get(clip_index).map_or(&[][..], Vec::as_slice);
let end = self.active.get(k + 1).map(|next| next.0);
let lower = (k > 0).then_some(start);
let in_interval = |t: f64| lower.is_none_or(|l| t >= l) && end.is_none_or(|e| t < e);
out.push(start);
out.extend(self.times.iter().map(|knot| knot.x).filter(|&x| in_interval(x)));
for &clip_time in samples {
out.extend(
self.stage_times_for_clip_time(clip_time)
.into_iter()
.filter(|&t| in_interval(t)),
);
}
}
out.retain(|t| t.is_finite());
out.sort_by(f64::total_cmp);
out.dedup();
out
}
pub(crate) fn may_be_time_varying(&self) -> bool {
self.active.len() > 1
}
fn stage_times_for_clip_time(&self, clip_time: f64) -> Vec<f64> {
if self.times.is_empty() {
return vec![clip_time];
}
self.times
.windows(2)
.filter_map(|seg| {
let (sa, ca) = (seg[0].x, seg[0].y);
let (sb, cb) = (seg[1].x, seg[1].y);
let (lo, hi) = if ca <= cb { (ca, cb) } else { (cb, ca) };
if ca == cb || clip_time < lo || clip_time > hi {
return None;
}
Some(sa + (clip_time - ca) / (cb - ca) * (sb - sa))
})
.collect()
}
}
fn clip_index(value: f64) -> Option<usize> {
(value.is_finite() && value >= 0.0 && value.fract() == 0.0).then_some(value as usize)
}
fn map_stage_to_clip(times: &[gf::Vec2d], stage_time: f64) -> f64 {
let (Some(first), Some(last)) = (times.first(), times.last()) else {
return stage_time;
};
if stage_time < first.x {
return first.y;
}
if stage_time >= last.x {
return last.y;
}
let lo = times.iter().rposition(|knot| knot.x <= stage_time).unwrap_or(0);
let (lo_knot, hi_knot) = (times[lo], times[lo + 1]);
let (stage0, clip0) = (lo_knot.x, lo_knot.y);
let (stage1, clip1) = (hi_knot.x, hi_knot.y);
if stage0 == stage1 {
return clip1;
}
if stage_time == stage0 {
return clip0;
}
let ratio = (stage_time - stage0) / (stage1 - stage0);
gf::lerp(clip0, clip1, ratio)
}
type TemplateExpansion = (Vec<AssetPath>, Vec<(f64, usize)>, Vec<gf::Vec2d>);
fn expand_template(set: &HashMap<String, Value>) -> Option<TemplateExpansion> {
let template = asset_input(set, keys::TEMPLATE_ASSET_PATH).map(str::to_owned)?;
let start = get::<f64>(set, keys::TEMPLATE_START_TIME)?;
let end = get::<f64>(set, keys::TEMPLATE_END_TIME)?;
let stride = get::<f64>(set, keys::TEMPLATE_STRIDE)?;
let active_offset = get::<f64>(set, keys::TEMPLATE_ACTIVE_OFFSET);
if stride.is_nan() || stride <= 0.0 || end < start {
return None;
}
if active_offset.is_some_and(|off| off.abs() > stride) {
return None;
}
let pattern = HashPattern::parse(&template)?;
const PROMOTION: f64 = 10000.0;
let end_p = end * PROMOTION;
let stride_p = stride * PROMOTION;
let mut asset_paths = Vec::new();
let mut active = Vec::new();
let mut times = Vec::new();
if let Some(off) = active_offset {
let front = start - off.abs();
times.push(gf::vec2d(front, front));
}
let mut t = start * PROMOTION;
let mut index = 0usize;
while t <= end_p + 0.5 {
let clip_time = t / PROMOTION;
asset_paths.push(pattern.format(clip_time).into());
times.push(gf::vec2d(clip_time, clip_time));
let stage_time = match active_offset {
Some(off) => (t + off * PROMOTION) / PROMOTION,
None => clip_time,
};
active.push((stage_time, index));
index += 1;
t += stride_p;
}
if let Some(off) = active_offset {
let back = end + off.abs();
times.push(gf::vec2d(back, back));
}
if asset_paths.is_empty() {
return None;
}
active.sort_by(|a, b| a.0.total_cmp(&b.0));
times.sort_by(|a, b| a.x.total_cmp(&b.x));
Some((asset_paths, active, times))
}
struct HashPattern {
prefix: String,
int_width: usize,
frac_width: Option<usize>,
suffix: String,
}
impl HashPattern {
fn parse(template: &str) -> Option<HashPattern> {
let first = template.find('#')?;
let prefix = template[..first].to_string();
let rest = &template[first..];
let int_width = rest.chars().take_while(|&c| c == '#').count();
let after_int = &rest[int_width..];
let (frac_width, suffix) = if let Some(dot_rest) = after_int.strip_prefix('.') {
if dot_rest.starts_with('#') {
let frac_width = dot_rest.chars().take_while(|&c| c == '#').count();
(Some(frac_width), dot_rest[frac_width..].to_string())
} else {
(None, after_int.to_string())
}
} else {
(None, after_int.to_string())
};
if suffix.contains('#') {
return None;
}
Some(HashPattern {
prefix,
int_width,
frac_width,
suffix,
})
}
fn format(&self, time: f64) -> String {
let body = match self.frac_width {
Some(frac_width) => {
let rendered = format!("{:.*}", frac_width, time);
let (int_part, frac_part) = rendered.split_once('.').unwrap_or((rendered.as_str(), ""));
let neg = int_part.starts_with('-');
let digits = int_part.trim_start_matches('-');
let padded = format!("{:0>width$}", digits, width = self.int_width);
let sign = if neg { "-" } else { "" };
format!("{sign}{padded}.{frac_part}")
}
None => format!("{:0width$}", time as i64, width = self.int_width),
};
format!("{}{}{}", self.prefix, body, self.suffix)
}
}
fn get<T: TryFrom<Value>>(set: &HashMap<String, Value>, key: &str) -> Option<T> {
set.get(key).cloned().and_then(|v| T::try_from(v).ok())
}
#[derive(Default)]
pub(crate) struct ClipCache {
clip_layers: HashMap<String, sdf::Layer>,
manifests: HashMap<ClipSetKey, ManifestEntry>,
set_sources: HashMap<Path, HashMap<String, SetSources>>,
dependents: HashMap<String, HashSet<ClipSetKey>>,
}
struct SetSources {
from: ResolvedClipSet,
identifiers: HashSet<String>,
}
struct ManifestEntry {
generated_from: ResolvedClipSet,
layer: String,
complete: bool,
}
pub(crate) struct ClipQuery<'a> {
pub anchor: &'a Path,
pub attr_prim: &'a Path,
pub suffix: &'a str,
}
impl ClipCache {
pub(super) fn value_in_set(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
query: &ClipQuery<'_>,
time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<Value>, QueryError> {
let set = &resolved.set;
let Some((activation, active)) = set.active_entry(time) else {
return Ok(None);
};
let clip_path = clip_attr_path(query, &set.clip_prim_path(query.anchor))?;
let manifest = self.manifest_id(graph, diagnostics, resolved, query.anchor)?;
if !self.manifest_declares(graph, manifest.as_deref(), &clip_path)? {
return Ok(None);
}
let Some(asset) = set.asset_paths.get(active) else {
return Ok(None);
};
let clip_time = set.map_stage_to_clip(time);
let contributes = !set.interpolate_missing
|| !self.manifest_blocks(graph, resolved, manifest.as_deref(), &clip_path, activation);
if contributes
&& let Some((clip_id, value)) =
self.clip_sample_at(graph, resolved, asset.asset_path(), &clip_path, clip_time, interp)?
{
return Ok(Some(self.resolve_asset_in(
graph,
diagnostics,
&clip_id,
resolved,
&clip_path,
value,
)));
}
if let Some(manifest) = manifest.as_deref()
&& let Some(value) = self.manifest_default(graph, manifest, &clip_path)?
{
return Ok(Some(self.resolve_asset_in(
graph,
diagnostics,
manifest,
resolved,
&clip_path,
value,
)));
}
if set.interpolate_missing
&& let Some(value) = self.interpolate_missing_value(
graph,
diagnostics,
resolved,
manifest.as_deref(),
&clip_path,
time,
interp,
)?
{
return Ok(Some(value));
}
Ok(Some(Value::ValueBlock))
}
pub(super) fn clip_spec_site_in_set(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
query: &ClipQuery<'_>,
time: f64,
) -> Result<Option<(String, Path)>, QueryError> {
let set = &resolved.set;
let Some((activation, active)) = set.active_entry(time) else {
return Ok(None);
};
let clip_path = clip_attr_path(query, &set.clip_prim_path(query.anchor))?;
let manifest = self.manifest_id(graph, diagnostics, resolved, query.anchor)?;
if !self.manifest_declares(graph, manifest.as_deref(), &clip_path)? {
return Ok(None);
}
let Some(asset) = set.asset_paths.get(active) else {
return Ok(None);
};
let contributes = !set.interpolate_missing
|| !self.manifest_blocks(graph, resolved, manifest.as_deref(), &clip_path, activation);
if contributes
&& let Some((clip_id, samples)) =
self.clip_time_samples(graph, asset.asset_path(), resolved.source.layer, &clip_path)?
&& !samples.is_empty()
{
return Ok(Some((clip_id, clip_path)));
}
Ok(manifest.map(|id| (id, clip_path)))
}
pub(super) fn untimed_answer_in_set(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
query: &ClipQuery<'_>,
) -> Result<ValueState, QueryError> {
let clip_path = clip_attr_path(query, &resolved.set.clip_prim_path(query.anchor))?;
let Some(per_clip) = self.clip_set_participates(graph, diagnostics, resolved, query.anchor, &clip_path)? else {
return Ok(ValueState::Absent);
};
if per_clip.iter().any(|times| !times.is_empty()) {
return Ok(ValueState::Present);
}
let manifest = self.manifest_id(graph, diagnostics, resolved, query.anchor)?;
let default = match manifest.as_deref() {
Some(manifest) => self.manifest_default(graph, manifest, &clip_path)?,
None => None,
};
match default {
Some(_) => Ok(ValueState::Present),
None => Ok(ValueState::Blocked),
}
}
pub(super) fn clip_introspection_in_set(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
query: &ClipQuery<'_>,
) -> Result<Option<(Vec<f64>, bool)>, QueryError> {
let clip_path = clip_attr_path(query, &resolved.set.clip_prim_path(query.anchor))?;
let Some(per_clip) = self.clip_set_participates(graph, diagnostics, resolved, query.anchor, &clip_path)? else {
return Ok(None);
};
let set = &resolved.set;
Ok(Some((set.stage_sample_times(&per_clip), set.may_be_time_varying())))
}
pub(super) fn generate_manifest(
&mut self,
graph: &LayerGraph,
resolved: &ResolvedClipSet,
prim: &Path,
tag: &str,
write_blocks: bool,
) -> Result<(sdf::Layer, Diagnostics), QueryError> {
let mut scheduled: Vec<(String, Option<f64>)> = Vec::with_capacity(resolved.set.active.len());
let mut unread = Diagnostics::default();
for &(stage_time, index) in &resolved.set.active {
let Some(asset) = resolved.set.asset_paths.get(index) else {
continue;
};
let reason = match self.ensure_clip_layer(graph, asset.asset_path(), resolved.source.layer) {
Ok(Some(id)) => {
scheduled.push((id, write_blocks.then_some(stage_time)));
continue;
}
Ok(None) => "asset path did not resolve".to_owned(),
Err(error) => tf::error_chain(&error),
};
unread.report(CompositionDiagnostic::UnreadableClip {
asset_path: asset.asset_path().to_owned(),
clip_set: resolved.set.name.clone(),
prim_path: prim.clone(),
reason,
});
}
let clips: Vec<(&sdf::Layer, Option<f64>)> = scheduled
.iter()
.filter_map(|(id, active)| self.layer(graph, id).map(|layer| (layer, *active)))
.collect();
let clip_prim_path = resolved.set.clip_prim_path(prim);
Ok((clip_manifest::generate_manifest(&clips, &clip_prim_path, tag)?, unread))
}
fn manifest_id(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
prim: &Path,
) -> Result<Option<String>, QueryError> {
if let Some(asset) = resolved.set.manifest_asset.as_deref() {
let anchor = resolved.manifest_layer.unwrap_or(resolved.source.layer);
return self.ensure_clip_layer(graph, asset, anchor);
}
let key = resolved.key(prim);
if let Some(entry) = self.manifests.get(&key)
&& entry.complete
&& entry.generated_from.same_resolution(resolved)
{
return Ok(Some(entry.layer.clone()));
}
let (manifest, unread) =
self.generate_manifest(graph, resolved, prim, clip_manifest::GENERATED_MANIFEST_TAG, false)?;
let layer = manifest.identifier().to_owned();
self.clip_layers.insert(layer.clone(), manifest);
let entry = ManifestEntry {
generated_from: resolved.clone(),
layer: layer.clone(),
complete: unread.is_empty(),
};
diagnostics.extend(unread);
if let Some(superseded) = self.manifests.insert(key, entry) {
self.clip_layers.remove(&superseded.layer);
}
Ok(Some(layer))
}
pub(super) fn register_set_sources(&mut self, graph: &LayerGraph, anchor: &Path, resolved: &ResolvedClipSet) {
if self
.set_sources
.get(anchor)
.and_then(|sets| sets.get(&resolved.set.name))
.is_some_and(|registered| registered.from.same_resolution(resolved))
{
return;
}
let identifiers = Self::set_identifiers(graph, resolved);
let key = resolved.key(anchor);
let previous = self
.set_sources
.entry(anchor.clone())
.or_default()
.insert(
resolved.set.name.clone(),
SetSources {
from: resolved.clone(),
identifiers: identifiers.clone(),
},
)
.map(|entry| entry.identifiers)
.unwrap_or_default();
Self::rewire_dependents(&mut self.dependents, &key, &previous, &identifiers);
}
fn set_identifiers(graph: &LayerGraph, resolved: &ResolvedClipSet) -> HashSet<String> {
let mut identifiers: HashSet<String> = resolved
.set
.resolution_paths()
.map(|path| clip_identifier(graph, path, resolved.source.layer))
.collect();
if let Some(asset) = resolved.set.manifest_asset.as_deref() {
let anchor_layer = resolved.manifest_layer.unwrap_or(resolved.source.layer);
identifiers.insert(clip_identifier(graph, asset, anchor_layer));
}
identifiers
}
pub(super) fn invalidate_layer(&mut self, identifier: &str) -> Vec<Path> {
let Some(dependents) = self.dependents.get(identifier) else {
return Vec::new();
};
let keys: Vec<ClipSetKey> = dependents.iter().cloned().collect();
let mut anchors: Vec<Path> = keys.iter().map(|key| key.prim.clone()).collect();
anchors.sort();
anchors.dedup();
for key in &keys {
self.drop_generated_manifest(key);
}
anchors
}
pub(super) fn has_clip_sources(&self) -> bool {
!self.dependents.is_empty()
}
fn drop_generated_manifest(&mut self, key: &ClipSetKey) {
if let Some(entry) = self.manifests.remove(key) {
self.clip_layers.remove(&entry.layer);
}
}
fn rewire_dependents(
dependents: &mut HashMap<String, HashSet<ClipSetKey>>,
key: &ClipSetKey,
previous: &HashSet<String>,
current: &HashSet<String>,
) {
for identifier in previous.difference(current) {
let Some(entry) = dependents.get_mut(identifier) else {
continue;
};
entry.remove(key);
if entry.is_empty() {
dependents.remove(identifier);
}
}
for identifier in current.difference(previous) {
dependents.entry(identifier.clone()).or_default().insert(key.clone());
}
}
fn manifest_declares(
&self,
graph: &LayerGraph,
manifest: Option<&str>,
clip_path: &Path,
) -> Result<bool, sdf::PathParseError> {
let Some(layer) = manifest.and_then(|id| self.layer(graph, id)) else {
return Ok(false);
};
Ok(layer
.attribute(clip_path)?
.is_some_and(|attr| attr.variability() == sdf::Variability::Varying))
}
fn manifest_blocks(
&self,
graph: &LayerGraph,
resolved: &ResolvedClipSet,
manifest: Option<&str>,
clip_path: &Path,
stage_time: f64,
) -> bool {
let Some(layer) = manifest.and_then(|id| self.layer(graph, id)) else {
return false;
};
let Ok(Some(field)) = layer.data().try_field(clip_path, FieldKey::TimeSamples.as_str()) else {
return false;
};
let Value::TimeSamples(samples) = &*field else {
return false;
};
samples.iter().any(|(time, value)| {
*value == Value::ValueBlock
&& sdf::compare_sample_times(resolved.active_offset.apply(*time), stage_time).is_eq()
})
}
fn ensure_clip_layer(
&mut self,
graph: &LayerGraph,
asset_path: &str,
anchor_layer: LayerId,
) -> Result<Option<String>, QueryError> {
let clip_id = clip_identifier(graph, asset_path, anchor_layer);
if graph.id_of(&clip_id).is_none() && !self.clip_layers.contains_key(&clip_id) {
let opened = graph
.layer_registry()
.open(&clip_id)
.map_err(|error| ClipLoad::new(clip_id.clone(), error))?;
let Some((resolved, data)) = opened else {
return Ok(None);
};
self.clip_layers.insert(
clip_id.clone(),
sdf::Layer::new_resolved(clip_id.clone(), &resolved, data),
);
}
Ok(Some(clip_id))
}
fn layer<'a>(&'a self, graph: &'a LayerGraph, id: &str) -> Option<&'a sdf::Layer> {
match graph.id_of(id) {
Some(interned) => Some(graph.layer(interned)),
None => self.clip_layers.get(id),
}
}
fn clip_set_participates(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
prim: &Path,
clip_path: &Path,
) -> Result<Option<Vec<Vec<f64>>>, QueryError> {
let set = &resolved.set;
if set.active.is_empty() {
return Ok(None);
}
let manifest = self.manifest_id(graph, diagnostics, resolved, prim)?;
if !self.manifest_declares(graph, manifest.as_deref(), clip_path)? {
return Ok(None);
}
let mut per_clip: Vec<Vec<f64>> = Vec::with_capacity(set.asset_paths.len());
for asset in &set.asset_paths {
per_clip.push(self.clip_in_clip_times(graph, asset.asset_path(), resolved.source.layer, clip_path)?);
}
Ok(Some(per_clip))
}
fn clip_in_clip_times(
&mut self,
graph: &LayerGraph,
asset: &str,
anchor_layer: LayerId,
clip_path: &Path,
) -> Result<Vec<f64>, QueryError> {
Ok(self
.clip_time_samples(graph, asset, anchor_layer, clip_path)?
.map(|(_, samples)| samples.iter().map(|(t, _)| *t).collect())
.unwrap_or_default())
}
fn clip_time_samples(
&mut self,
graph: &LayerGraph,
asset: &str,
anchor_layer: LayerId,
clip_path: &Path,
) -> Result<Option<(String, sdf::TimeSampleMap)>, QueryError> {
let Some(id) = self.ensure_clip_layer(graph, asset, anchor_layer)? else {
return Ok(None);
};
let Some(layer) = self.layer(graph, &id) else {
return Ok(None);
};
Ok(
match layer.data().try_field(clip_path, FieldKey::TimeSamples.as_str())? {
Some(value) => match value.into_owned() {
Value::TimeSamples(samples) => Some((id, samples)),
_ => None,
},
None => None,
},
)
}
fn clip_sample_at(
&mut self,
graph: &LayerGraph,
resolved: &ResolvedClipSet,
asset: &str,
clip_path: &Path,
clip_time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<(String, Value)>, QueryError> {
let Some((clip_id, samples)) = self.clip_time_samples(graph, asset, resolved.source.layer, clip_path)? else {
return Ok(None);
};
Ok(interp(&samples, clip_time).map(|value| (clip_id, value)))
}
fn resolve_asset_in(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
identifier: &str,
resolved: &ResolvedClipSet,
clip_path: &Path,
value: Value,
) -> Value {
if !value.is_asset_valued() {
return value;
}
let Some(layer) = self.layer(graph, identifier) else {
return value;
};
let site = AssetSite::in_clip(layer, resolved.source.stack, clip_path);
asset_resolve::resolve_values(graph, value, Some(&site), diagnostics)
}
fn manifest_default(
&self,
graph: &LayerGraph,
manifest: &str,
clip_path: &Path,
) -> Result<Option<Value>, QueryError> {
let Some(layer) = self.layer(graph, manifest) else {
return Ok(None);
};
Ok(layer
.data()
.try_field(clip_path, FieldKey::Default.as_str())?
.map(|value| value.into_owned())
.and_then(block_to_none))
}
#[allow(clippy::too_many_arguments)]
fn interpolate_missing_value(
&mut self,
graph: &LayerGraph,
diagnostics: &mut Diagnostics,
resolved: &ResolvedClipSet,
manifest: Option<&str>,
clip_path: &Path,
time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<Value>, QueryError> {
let set = &resolved.set;
let active_pos = set.active.iter().rposition(|&(stage, _)| stage <= time).unwrap_or(0);
let mut upper = None;
for &(stage, idx) in set.active.iter().skip(active_pos + 1) {
if self.manifest_blocks(graph, resolved, manifest, clip_path, stage) {
continue;
}
if let Some(asset) = set.asset_paths.get(idx) {
let clip_time = set.map_stage_to_clip(stage);
if let Some(sample) =
self.clip_sample_at(graph, resolved, asset.asset_path(), clip_path, clip_time, interp)?
{
upper = Some((stage, sample));
break;
}
}
}
let mut lower = None;
for &(stage, idx) in set.active[..active_pos].iter().rev() {
if self.manifest_blocks(graph, resolved, manifest, clip_path, stage) {
continue;
}
if let Some(asset) = set.asset_paths.get(idx) {
let clip_time = set.map_stage_to_clip(stage);
if let Some(sample) =
self.clip_sample_at(graph, resolved, asset.asset_path(), clip_path, clip_time, interp)?
{
lower = Some((stage, sample));
break;
}
}
}
Ok(match (lower, upper) {
(Some((lt, (lid, lv))), Some((ut, (_, uv)))) => {
if lv.is_asset_valued() || uv.is_asset_valued() {
Some(self.resolve_asset_in(graph, diagnostics, &lid, resolved, clip_path, lv))
} else {
interp(&vec![(lt, lv), (ut, uv)], time)
}
}
(Some((_, (id, value))), None) | (None, Some((_, (id, value)))) => {
Some(self.resolve_asset_in(graph, diagnostics, &id, resolved, clip_path, value))
}
(None, None) => None,
})
}
}
pub(crate) fn has_explicit_assets(set: &HashMap<String, Value>) -> bool {
matches!(set.get(keys::ASSET_PATHS), Some(Value::AssetPathVec(_)))
}
fn explicit_asset_paths(set: &HashMap<String, Value>) -> Option<Vec<AssetPath>> {
has_explicit_assets(set).then(|| get::<Vec<AssetPath>>(set, keys::ASSET_PATHS))?
}
pub(crate) fn effective_set_names<'a, T>(sets: &'a HashMap<String, T>, order: Option<&'a [String]>) -> Vec<&'a String> {
if let Some(order) = order {
return order.iter().filter(|name| sets.contains_key(*name)).collect();
}
let mut names: Vec<&String> = sets.keys().collect();
names.sort();
names
}
pub(crate) fn as_asset_field(value: Value) -> Value {
match value {
Value::String(path) => Value::AssetPath(AssetPath::new(path)),
Value::Token(path) => Value::AssetPath(AssetPath::new(path.as_str())),
other => other,
}
}
pub(crate) fn asset_input<'a>(set: &'a HashMap<String, Value>, field: &str) -> Option<&'a str> {
match set.get(field)? {
Value::AssetPath(asset) => Some(asset.asset_path()),
other => other.as_str(),
}
}
fn clip_identifier(graph: &LayerGraph, asset_path: &str, anchor_layer: LayerId) -> String {
let anchor = graph.anchor_location(Some(anchor_layer));
let anchored = graph.layer_registry().create_identifier(asset_path, anchor.as_ref());
if graph.id_of(&anchored).is_some() {
return anchored;
}
match graph.find_relative(asset_path, anchor_layer) {
Some(interned) => graph.identifier(interned).to_owned(),
None => anchored,
}
}
fn clip_attr_path(query: &ClipQuery<'_>, base: &Path) -> Result<Path, QueryError> {
let attr = Path::new(&format!("{}{}", query.attr_prim, query.suffix))?;
Ok(attr.replace_prefix(query.anchor, base).unwrap_or(attr))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf;
#[test]
fn loads_and_caches_clip_layer() -> crate::Result<()> {
let root = format!(
"{}vendor/core-spec-supplemental-release_dec2025/value_resolution/tests/assets/clip_basic/usda/root.usda",
env!("CARGO_WORKSPACE_DIR")
);
let registry = sdf::LayerRegistry::default();
let id = registry.create_identifier(&root, None);
let (_, data) = registry.open(&root).expect("open root").expect("root resolves");
let graph = LayerGraph::from_layers(vec![sdf::Layer::new(id, data)], 0, registry);
let root_id = graph.root_id().expect("root layer");
let mut clips = ClipCache::default();
let id = clips
.ensure_clip_layer(&graph, "./clip.usda", root_id)?
.expect("clip resolves");
{
let clip = clips.layer(&graph, &id).expect("the clip is held");
assert!(clip.identifier.contains("clip.usda"));
assert!(clip.data().has_spec(&sdf::path("/Model.size")?));
}
assert!(clips.ensure_clip_layer(&graph, "./clip.usda", root_id)?.is_some());
assert!(
clips
.ensure_clip_layer(&graph, "./does_not_exist.usda", root_id)?
.is_none()
);
Ok(())
}
#[test]
fn in_memory_clip_resolves() -> crate::Result<()> {
let root = format!(
"{}/fixtures/clip_manifestless_held/root.usda",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
);
let registry = sdf::LayerRegistry::default();
let id = registry.create_identifier(&root, None);
let (_, data) = registry.open(&root).expect("open root").expect("root resolves");
let clip = sdf::Layer::new_in_memory("in_memory_clip.usda");
let graph = LayerGraph::from_layers(vec![sdf::Layer::new(id, data), clip], 0, registry);
let root_id = graph.root_id().expect("root layer");
let identifier = clip_identifier(&graph, "in_memory_clip.usda", root_id);
assert_eq!(
graph.id_of(&identifier),
graph.id_of("in_memory_clip.usda"),
"the clip must be keyed under the identifier the graph interns it by",
);
let mut clips = ClipCache::default();
assert_eq!(
clips
.ensure_clip_layer(&graph, "in_memory_clip.usda", root_id)?
.as_deref(),
Some(identifier.as_str()),
"resolving must find the interned layer rather than opening a file",
);
assert!(
clips.layer(&graph, &identifier).is_some(),
"and reading it must reach the graph's layer",
);
Ok(())
}
#[test]
fn synthesized_manifest_is_memoized() -> crate::Result<()> {
let root = format!(
"{}/fixtures/clip_manifestless_held/root.usda",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
);
let registry = sdf::LayerRegistry::default();
let id = registry.create_identifier(&root, None);
let (_, data) = registry.open(&root).expect("open root").expect("root resolves");
let graph = LayerGraph::from_layers(vec![sdf::Layer::new(id, data)], 0, registry);
let root_id = graph.root_id().expect("root layer");
let model = sdf::path("/Model")?;
let resolved = ResolvedClipSet {
set: ClipSet {
name: "default".into(),
prim_path: Some(model.clone()),
manifest_asset: None,
asset_paths: vec![AssetPath::new("./clip0.usda"), AssetPath::new("./clip1.usda")],
active: vec![(0.0, 0), (10.0, 1)],
times: Vec::new(),
interpolate_missing: false,
},
source: ClipAnchor {
layer: root_id,
prim_path: model.clone(),
stack: LayerStackId::ROOT,
},
manifest_layer: None,
active_offset: LayerOffset::IDENTITY,
};
let mut clips = ClipCache::default();
let first = clips
.manifest_id(&graph, &mut Diagnostics::default(), &resolved, &model)?
.expect("synthesized");
let layers = clips.clip_layers.len();
assert_eq!(
clips
.manifest_id(&graph, &mut Diagnostics::default(), &resolved, &model)?
.as_deref(),
Some(&*first)
);
assert_eq!(clips.clip_layers.len(), layers);
assert!(clips.manifest_declares(&graph, Some(&first), &sdf::path("/Model.size")?)?);
assert!(!clips.manifest_declares(&graph, Some(&first), &sdf::path("/Model.absent")?)?);
let mut retimed = resolved.clone();
retimed.set.active = vec![(0.0, 0), (5.0, 1)];
let second = clips
.manifest_id(&graph, &mut Diagnostics::default(), &retimed, &model)?
.expect("synthesized");
assert_ne!(second, first);
assert!(!clips.clip_layers.contains_key(&first));
assert_eq!(clips.clip_layers.len(), layers);
let mut repathed = retimed.clone();
repathed.set.prim_path = Some(sdf::path("/Other")?);
let third = clips
.manifest_id(&graph, &mut Diagnostics::default(), &repathed, &model)?
.expect("synthesized");
assert_ne!(third, second);
assert!(!clips.clip_layers.contains_key(&second));
assert_eq!(clips.clip_layers.len(), layers);
let other_prim = sdf::path("/Other")?;
let mut elsewhere = resolved.clone();
elsewhere.set.asset_paths = vec![AssetPath::new("./clip1.usda")];
elsewhere.set.active = vec![(0.0, 0)];
let mine = clips
.manifest_id(&graph, &mut Diagnostics::default(), &resolved, &model)?
.expect("synthesized");
let theirs = clips
.manifest_id(&graph, &mut Diagnostics::default(), &elsewhere, &other_prim)?
.expect("synthesized");
assert_ne!(mine, theirs);
assert_eq!(
clips
.manifest_id(&graph, &mut Diagnostics::default(), &resolved, &model)?
.as_deref(),
Some(&*mine)
);
assert_eq!(
clips
.manifest_id(&graph, &mut Diagnostics::default(), &elsewhere, &other_prim)?
.as_deref(),
Some(&*theirs)
);
Ok(())
}
fn knots(pairs: &[(f64, f64)]) -> Vec<gf::Vec2d> {
pairs.iter().map(|&(x, y)| gf::vec2d(x, y)).collect()
}
#[test]
fn hash_substitution_integer() {
assert_eq!(HashPattern::parse("foo.##.usd").unwrap().format(12.0), "foo.12.usd");
assert_eq!(HashPattern::parse("foo.###.usd").unwrap().format(12.0), "foo.012.usd");
assert_eq!(HashPattern::parse("foo.#.usd").unwrap().format(333.0), "foo.333.usd");
assert_eq!(HashPattern::parse("foo.#.usd").unwrap().format(1.6), "foo.1.usd");
}
#[test]
fn hash_substitution_subinteger() {
assert_eq!(
HashPattern::parse("foo.#.###.usd").unwrap().format(1.15),
"foo.1.150.usd"
);
assert_eq!(HashPattern::parse("foo.#.##.usd").unwrap().format(1.1), "foo.1.10.usd");
}
#[test]
fn hash_pattern_rejects_three_groups() {
assert!(HashPattern::parse("foo.#.#.#.usd").is_none());
assert!(HashPattern::parse("foo.usd").is_none());
}
#[test]
fn template_expands_to_explicit_clip_set() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("clip.##.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(101.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(103.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
let parsed = ClipSet::parse_set("default", &set).expect("template set");
assert_eq!(
parsed.asset_paths,
vec![
"clip.101.usd".to_string(),
"clip.102.usd".to_string(),
"clip.103.usd".to_string()
],
);
assert_eq!(parsed.active, vec![(101.0, 0), (102.0, 1), (103.0, 2)]);
assert_eq!(parsed.times, knots(&[(101.0, 101.0), (102.0, 102.0), (103.0, 103.0)]));
}
#[test]
fn template_active_offset_shifts_active_times() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
set.insert(keys::TEMPLATE_ACTIVE_OFFSET.to_string(), Value::Double(-0.5));
let parsed = ClipSet::parse_set("default", &set).expect("template set");
assert_eq!(parsed.active, vec![(-0.5, 0), (0.5, 1), (1.5, 2)]);
assert_eq!(
parsed.times,
knots(&[(-0.5, -0.5), (0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (2.5, 2.5)])
);
}
#[test]
fn template_rejects_invalid_metadata() {
use std::collections::HashMap;
let base = |off: f64, stride: f64| {
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(stride));
set.insert(keys::TEMPLATE_ACTIVE_OFFSET.to_string(), Value::Double(off));
set
};
assert!(ClipSet::parse_set("default", &base(2.0, 1.0)).is_none());
assert!(ClipSet::parse_set("default", &base(0.0, 0.0)).is_none());
}
#[test]
fn active_rejects_unusable_index() {
use std::collections::HashMap;
let parse = |active: Vec<gf::Vec2d>| {
let mut set = HashMap::new();
set.insert(
keys::ASSET_PATHS.to_string(),
Value::AssetPathVec(vec!["a.usd".into(), "b.usd".into()]),
);
set.insert(keys::ACTIVE.to_string(), Value::Vec2dVec(active));
ClipSet::parse_set("default", &set)
};
assert_eq!(
parse(knots(&[(0.0, 0.0), (10.0, 1.0)])).expect("valid set").active,
vec![(0.0, 0), (10.0, 1)]
);
assert!(parse(knots(&[(0.0, 0.0), (5.0, 7.0)])).is_none());
assert!(parse(knots(&[(0.0, 0.0), (5.0, -1.0)])).is_none());
assert!(parse(knots(&[(0.0, 0.0), (5.0, 0.5)])).is_none());
assert!(parse(vec![gf::vec2d(0.0, f64::NAN)]).is_none());
assert!(parse(vec![gf::vec2d(f64::NAN, 0.0)]).is_none());
}
#[test]
fn explicit_asset_paths_win_over_template() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::ASSET_PATHS.to_string(),
Value::AssetPathVec(vec!["explicit.usd".into()]),
);
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
let parsed = ClipSet::parse_set("default", &set).expect("explicit set");
assert_eq!(parsed.asset_paths, vec!["explicit.usd".to_string()]);
}
fn clip_set(active: Vec<(f64, usize)>, times: Vec<gf::Vec2d>) -> ClipSet {
ClipSet {
name: "default".into(),
prim_path: None,
manifest_asset: None,
asset_paths: Vec::new(),
active,
times,
interpolate_missing: false,
}
}
#[test]
fn active_clip_ranges() {
let cs = clip_set(vec![(0.0, 0), (1.0, 1), (2.0, 2)], vec![]);
assert_eq!(cs.active_entry(-5.0).map(|(_, i)| i), Some(0)); assert_eq!(cs.active_entry(0.0).map(|(_, i)| i), Some(0));
assert_eq!(cs.active_entry(1.5).map(|(_, i)| i), Some(1));
assert_eq!(cs.active_entry(2.0).map(|(_, i)| i), Some(2));
assert_eq!(cs.active_entry(100.0).map(|(_, i)| i), Some(2)); }
#[test]
fn active_clip_empty() {
assert_eq!(clip_set(vec![], vec![]).active_entry(0.0), None);
}
#[test]
fn map_times_linear() {
let cs = clip_set(vec![], knots(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]));
assert_eq!(cs.map_stage_to_clip(0.0), 1.0);
assert_eq!(cs.map_stage_to_clip(1.0), 2.0);
assert_eq!(cs.map_stage_to_clip(1.5), 2.5); assert_eq!(cs.map_stage_to_clip(-3.0), 1.0); assert_eq!(cs.map_stage_to_clip(9.0), 3.0); }
#[test]
fn map_times_identity() {
assert_eq!(clip_set(vec![], vec![]).map_stage_to_clip(7.5), 7.5);
}
#[test]
fn stage_times_identity() {
let cs = clip_set(vec![(0.0, 0), (10.0, 1)], vec![]);
let times = cs.stage_sample_times(&[vec![0.0, 5.0, 12.0], vec![10.0, 15.0]]);
assert_eq!(times, vec![0.0, 5.0, 10.0, 15.0]);
}
#[test]
fn stage_times_empty_window_boundary() {
let cs = clip_set(vec![(0.0, 0), (10.0, 1)], vec![]);
let times = cs.stage_sample_times(&[vec![0.0, 5.0], vec![]]);
assert_eq!(times, vec![0.0, 5.0, 10.0]);
}
#[test]
fn stage_times_linear_timing() {
let cs = clip_set(vec![(0.0, 0)], knots(&[(0.0, 0.0), (10.0, 5.0)]));
let times = cs.stage_sample_times(&[vec![0.0, 2.5, 5.0]]);
assert_eq!(times, vec![0.0, 5.0, 10.0]);
}
#[test]
fn stage_times_before_first_active() {
let cs = clip_set(vec![(10.0, 0)], vec![]);
let times = cs.stage_sample_times(&[vec![5.0, 15.0]]);
assert_eq!(times, vec![5.0, 10.0, 15.0]);
}
#[test]
fn stage_times_no_active() {
assert!(
clip_set(vec![], vec![])
.stage_sample_times(&[vec![0.0, 1.0]])
.is_empty()
);
}
#[test]
fn map_times_jump_discontinuity() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (10.0, 10.0), (10.0, 25.0), (20.0, 35.0)]));
assert_eq!(cs.map_stage_to_clip(5.0), 5.0);
assert!((cs.map_stage_to_clip(9.999) - 9.999).abs() < 1e-6); assert_eq!(cs.map_stage_to_clip(10.0), 25.0); assert_eq!(cs.map_stage_to_clip(15.0), 30.0); assert_eq!(cs.map_stage_to_clip(20.0), 35.0);
}
#[test]
fn map_times_initial_jump() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (0.0, 25.0), (10.0, 35.0)]));
assert_eq!(cs.map_stage_to_clip(-1.0), 0.0);
assert_eq!(cs.map_stage_to_clip(0.0), 25.0);
assert_eq!(cs.map_stage_to_clip(5.0), 30.0);
}
#[test]
fn map_times_looping() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (25.0, 25.0), (25.0, 0.0), (50.0, 25.0)]));
assert_eq!(cs.map_stage_to_clip(20.0), 20.0);
assert_eq!(cs.map_stage_to_clip(45.0), 20.0); }
#[test]
fn parse_explicit_from_usda() {
use crate::sdf::AbstractData;
let parsed = crate::usda::parser::Parser::new(
r#"#usda 1.0
def Xform "Geo" (
clips = {
dictionary default = {
double2[] active = [(0, 0), (1, 1), (2, 2)]
asset[] assetPaths = [@./quad_1.usda@, @./quad_2.usda@, @./quad_3.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Geo"
double2[] times = [(0, 1), (1, 2), (2, 3)]
}
}
)
{
}
"#,
)
.parse()
.expect("parse usda");
let data = sdf::Data::from_specs(parsed);
let clips = data
.try_field(&Path::new("/Geo").unwrap(), "clips")
.expect("try_field")
.expect("clips authored")
.into_owned();
let sets = ClipSet::parse(&clips, None);
assert_eq!(sets.len(), 1);
let cs = &sets[0];
assert_eq!(cs.name, "default");
assert_eq!(cs.prim_path, Some(Path::new("/Geo").unwrap()));
assert_eq!(cs.manifest_asset.as_deref(), Some("./manifest.usda"));
assert_eq!(cs.asset_paths, vec!["./quad_1.usda", "./quad_2.usda", "./quad_3.usda"]);
assert_eq!(cs.active, vec![(0.0, 0), (1.0, 1), (2.0, 2)]);
assert_eq!(cs.times, knots(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]));
}
}