use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use crate::gf;
use crate::sdf::schema::FieldKey;
use crate::sdf::{self, LayerOffset, Path, Specifier, Value};
use super::asset_resolve::{self, AssetSite};
use super::clip;
use super::diagnostics::Diagnostics;
use super::mapping::MapFunction;
use super::prim_graph::{ArcType, Node};
use super::prim_index::PrimIndex;
use super::value_resolve::{self, OpinionSite, Step};
use super::{LayerGraph, LayerId, QueryError};
struct Opinion<'a> {
node: &'a Node,
layer: LayerId,
query_path: Cow<'a, Path>,
value: Cow<'a, Value>,
offset: LayerOffset,
}
impl Opinion<'_> {
fn retimed(mut self) -> Self {
if !self.offset.is_identity() && self.value.holds_time_codes() {
self.offset.apply_to_value(self.value.to_mut());
}
self
}
}
pub(crate) struct Composing {
value: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Folded {
Composed,
Closed,
Skipped,
Blocked,
}
impl Folded {
pub(crate) fn step(self) -> Step {
match self {
Self::Composed | Self::Skipped => Step::NextSite,
Self::Closed | Self::Blocked => Step::Stop,
}
}
pub(crate) fn composed(self) -> bool {
matches!(self, Self::Composed | Self::Closed)
}
}
impl Composing {
pub(crate) fn new() -> Self {
Self { value: None }
}
pub(crate) fn resumed(value: Value) -> Self {
Self { value: Some(value) }
}
pub(crate) fn is_empty(&self) -> bool {
self.value.is_none()
}
pub(crate) fn fold(&mut self, value: Value, query_path: &Path, map_to_root: &MapFunction) -> Folded {
self.compose(value, &|expr| {
PrimIndex::map_expression_to_root(expr, &query_path.prim_path(), map_to_root)
})
}
pub(crate) fn fold_rooted(&mut self, value: Value) -> Folded {
self.compose(value, &|expr| expr)
}
fn compose(&mut self, value: Value, root: &dyn Fn(sdf::PathExpression) -> sdf::PathExpression) -> Folded {
if value_resolve::is_block(&value) {
return Folded::Blocked;
}
let Some(composed) = self.value.take() else {
let value = match value {
Value::PathExpression(expr) => Value::PathExpression(root(expr)),
Value::PathExpressionVec(exprs) => Value::PathExpressionVec(exprs.into_iter().map(root).collect()),
other => other,
};
let folded = match composes_over_weaker(&value) {
true => Folded::Composed,
false => Folded::Closed,
};
self.value = Some(value);
return folded;
};
match (composed, value) {
(Value::Dictionary(mut merged), Value::Dictionary(weaker)) => {
sdf::dictionary_over(&mut merged, weaker);
self.value = Some(Value::Dictionary(merged));
Folded::Composed
}
(Value::PathExpression(expr), weaker) => {
let Some(weaker) = weaker.into_path_expression() else {
self.value = Some(Value::PathExpression(expr));
return Folded::Skipped;
};
let expr = expr.compose_over(&root(weaker));
let folded = match expr.contains_weaker_reference() {
true => Folded::Composed,
false => Folded::Closed,
};
self.value = Some(Value::PathExpression(expr));
folded
}
(composed, _) => {
self.value = Some(composed);
Folded::Skipped
}
}
}
pub(crate) fn is_open(&self) -> bool {
self.value.as_ref().is_some_and(composes_over_weaker)
}
pub(crate) fn into_open(self) -> Option<Value> {
self.value
}
pub(crate) fn close(self) -> Option<Value> {
self.value.map(close_value)
}
}
#[derive(Debug, Clone)]
pub(crate) enum Supplied {
Composable(Value),
Opaque,
Blocked,
}
impl Supplied {
pub(crate) fn classify(value: &Value, root: &dyn Fn(Value) -> Value) -> Self {
if value_resolve::is_block(value) {
return Self::Blocked;
}
match reads_into_composition(value) {
true => Self::Composable(root(value.clone())),
false => Self::Opaque,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum Composed {
Open(Value),
Closed,
}
impl Composed {
pub(crate) fn source(supplied: &Supplied) -> Self {
let Supplied::Composable(value) = supplied else {
return Self::Closed;
};
match composes_over_weaker(value) {
true => Self::Open(value.clone()),
false => Self::Closed,
}
}
pub(crate) fn is_open(&self) -> bool {
matches!(self, Self::Open(_))
}
fn over(self, weaker: &Supplied) -> Self {
let Self::Open(value) = self else {
return Self::Closed;
};
let weaker = match weaker {
Supplied::Blocked => return Self::Closed,
Supplied::Opaque => return Self::Open(value),
Supplied::Composable(weaker) => weaker,
};
let mut composing = Composing::resumed(value);
let folded = composing.fold_rooted(weaker.clone());
match (folded, composing.into_open()) {
(Folded::Composed | Folded::Skipped, Some(value)) => Self::Open(value),
_ => Self::Closed,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Series<T> {
held: Option<T>,
entries: Vec<(f64, T)>,
}
impl<T> Default for Series<T> {
fn default() -> Self {
Self {
held: None,
entries: Vec::new(),
}
}
}
impl<T> Series<T> {
pub(crate) fn held(value: T) -> Self {
Self {
held: Some(value),
entries: Vec::new(),
}
}
pub(crate) fn sampled(entries: impl IntoIterator<Item = (f64, T)>) -> Self {
let mut entries: Vec<_> = entries.into_iter().collect();
entries.sort_by(|(a, _), (b, _)| a.total_cmp(b));
Self { held: None, entries }
}
pub(crate) fn is_empty(&self) -> bool {
self.held.is_none() && self.entries.is_empty()
}
pub(crate) fn times(&self) -> impl Iterator<Item = f64> + '_ {
self.entries.iter().map(|(time, _)| *time)
}
fn values(&self) -> impl Iterator<Item = &T> {
self.held.iter().chain(self.entries.iter().map(|(_, value)| value))
}
fn map<U>(&self, f: impl Fn(&T) -> U) -> Series<U> {
Series {
held: self.held.as_ref().map(&f),
entries: self.entries.iter().map(|(time, value)| (*time, f(value))).collect(),
}
}
fn at(&self, time: f64) -> Option<&T> {
if self.entries.is_empty() {
return self.held.as_ref();
}
let index = self.entries.partition_point(|(at, _)| *at <= time);
Some(&self.entries[index.saturating_sub(1)].1)
}
}
impl Series<Composed> {
pub(crate) fn is_open(&self) -> bool {
self.values().any(Composed::is_open)
}
pub(crate) fn fold(&mut self, weaker: &Series<Supplied>) {
if weaker.is_empty() {
return;
}
if self.is_empty() {
*self = weaker.map(Composed::source);
return;
}
if self.entries.is_empty() && weaker.entries.is_empty() {
self.held = self
.held
.take()
.zip(weaker.held.as_ref())
.map(|(composed, weaker)| composed.over(weaker));
return;
}
let mut times: Vec<_> = self.times().chain(weaker.times()).collect();
times.sort_by(f64::total_cmp);
times.dedup();
self.entries = times
.into_iter()
.map(|time| {
let composed = match (self.at(time), weaker.at(time)) {
(Some(composed), Some(weaker)) => composed.clone().over(weaker),
(Some(composed), None) => composed.clone(),
(None, Some(weaker)) => Composed::source(weaker),
(None, None) => Composed::Closed,
};
(time, composed)
})
.collect();
self.held = None;
}
}
pub(crate) fn rooted_at(value: Value, site: &OpinionSite<'_>) -> Value {
let mut composing = Composing::new();
composing.fold(value, &site.query_path, &site.node.map_to_root);
composing.into_open().unwrap_or(Value::ValueBlock)
}
pub(crate) fn reads_into_composition(value: &Value) -> bool {
composition_rewrites(value) || value.as_path_expression().is_some()
}
pub(crate) fn composition_rewrites(value: &Value) -> bool {
matches!(
value,
Value::Dictionary(_) | Value::PathExpression(_) | Value::PathExpressionVec(_)
)
}
pub(crate) fn composes_over_weaker(value: &Value) -> bool {
match value {
Value::Dictionary(_) => true,
Value::PathExpression(expr) => expr.contains_weaker_reference(),
_ => false,
}
}
fn close_value(value: Value) -> Value {
let close = |expr: sdf::PathExpression| expr.compose_over(&sdf::PathExpression::nothing());
match value {
Value::PathExpression(expr) => Value::PathExpression(close(expr)),
Value::PathExpressionVec(exprs) => Value::PathExpressionVec(exprs.into_iter().map(close).collect()),
other => other,
}
}
struct ContributingSite<'a> {
node: &'a Node,
layer: LayerId,
offset: LayerOffset,
query_path: Cow<'a, Path>,
}
pub(crate) enum InvalidTargetKind {
External,
Instance,
}
pub(crate) struct InvalidTarget {
pub kind: InvalidTargetKind,
pub target: Path,
pub property: Path,
pub layer: LayerId,
pub arc: ArcType,
pub arc_root: Path,
pub composed: Option<Path>,
}
impl PrimIndex {
pub(crate) fn resolve_field(
&self,
field: &str,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Result<Option<Value>, QueryError> {
if field == FieldKey::Specifier.as_str() {
return self.resolve_specifier(stack, prop_suffix);
}
if field == FieldKey::Variability.as_str() {
return self.resolve_variability(stack, prop_suffix);
}
if field == FieldKey::Custom.as_str() {
return self.resolve_custom(stack, prop_suffix);
}
if field == FieldKey::TimeSamples.as_str() {
return Ok(self.resolve_time_samples(stack, prop_suffix)?.map(Value::TimeSamples));
}
let composed = match self.resolve_strongest(field, stack, prop_suffix)? {
Some(strongest) if sdf::folds_list_ops(field) => {
Some(self.resolve_list_op(field, stack, prop_suffix, strongest)?)
}
other => other,
};
Ok(composed.map(close_value))
}
fn resolve_list_op(
&self,
field: &str,
stack: &LayerGraph,
prop_suffix: Option<&str>,
strongest: Value,
) -> Result<Value, QueryError> {
macro_rules! fold {
($variant:ident) => {{
let mut ops = Vec::new();
for opinion in self.opinions(field, stack, prop_suffix) {
match opinion?.value.into_owned() {
Value::ValueBlock => break,
Value::$variant(op) => {
let explicit = op.explicit;
ops.push(op);
if explicit {
break;
}
}
_ => {}
}
}
Value::$variant(sdf::ListOp::explicit(compose_list_ops(&ops)))
}};
}
Ok(match strongest {
Value::TokenListOp(_) => fold!(TokenListOp),
Value::StringListOp(_) => fold!(StringListOp),
Value::PathListOp(_) => fold!(PathListOp),
Value::ReferenceListOp(_) => fold!(ReferenceListOp),
Value::PayloadListOp(_) => fold!(PayloadListOp),
Value::IntListOp(_) => fold!(IntListOp),
Value::Int64ListOp(_) => fold!(Int64ListOp),
Value::UIntListOp(_) => fold!(UIntListOp),
Value::UInt64ListOp(_) => fold!(UInt64ListOp),
Value::UnregisteredValueListOp(_) => fold!(UnregisteredValueListOp),
_ if field == FieldKey::ApiSchemas.as_str() => fold!(TokenListOp),
other => other,
})
}
pub(crate) fn resolve_path_list_op_validated(
&self,
field: FieldKey,
stack: &LayerGraph,
prop_suffix: Option<&str>,
instance_targets: &HashSet<(Path, Path)>,
) -> Result<(Vec<Path>, Vec<InvalidTarget>), QueryError> {
let mut ops = Vec::new();
let mut invalid = Vec::new();
let mut deleted_composed: HashSet<Path> = HashSet::new();
let mut seen_explicit = false;
for opinion in self.opinions(field.as_str(), stack, prop_suffix) {
let Opinion {
node,
layer,
query_path,
value,
..
} = opinion?;
let list_op = match value.into_owned() {
Value::ValueBlock => break,
Value::PathListOp(op) => op,
Value::PathVec(paths) => sdf::PathListOp::explicit(paths),
_ => continue,
};
let is_explicit = list_op.explicit;
let map = &node.map_to_root;
let arc = node.arc;
let arc_root = node.parent.map_or_else(Path::abs_root, |p| self.node(p).path.clone());
let report = !seen_explicit;
let mut authored = |path: Path| {
let absolute = query_path.make_absolute(&path);
let mapped = map.translate_to_target(&absolute);
let (kind, composed) = match &mapped {
Some(t) if instance_targets.contains(&(absolute.clone(), query_path.as_ref().clone())) => {
(InvalidTargetKind::Instance, Some(t.clone()))
}
Some(_) => return mapped,
None => (InvalidTargetKind::External, None),
};
if report {
invalid.push(InvalidTarget {
kind,
target: absolute,
property: query_path.as_ref().clone(),
layer,
arc,
arc_root: arc_root.clone(),
composed,
});
}
None
};
let op = sdf::PathListOp {
explicit: list_op.explicit,
explicit_items: list_op.explicit_items.into_iter().filter_map(&mut authored).collect(),
added_items: list_op.added_items.into_iter().filter_map(&mut authored).collect(),
prepended_items: list_op.prepended_items.into_iter().filter_map(&mut authored).collect(),
appended_items: list_op.appended_items.into_iter().filter_map(&mut authored).collect(),
deleted_items: list_op
.deleted_items
.into_iter()
.filter_map(|p| {
let mapped = map.translate_to_target(&query_path.make_absolute(&p));
if let Some(d) = &mapped {
deleted_composed.insert(d.clone());
}
mapped
})
.collect(),
ordered_items: list_op
.ordered_items
.into_iter()
.filter_map(|p| map.translate_to_target(&query_path.make_absolute(&p)))
.collect(),
};
seen_explicit |= is_explicit;
ops.push(op);
}
invalid.retain(|inv| inv.composed.as_ref().is_none_or(|c| !deleted_composed.contains(c)));
Ok((compose_list_ops(&ops), invalid))
}
fn collect_path_list_ops(
&self,
field: FieldKey,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Result<Vec<sdf::PathListOp>, QueryError> {
let field = field.as_str();
let mut ops = Vec::new();
for opinion in self.opinions(field, stack, prop_suffix) {
let Opinion {
node,
query_path,
value,
..
} = opinion?;
let list_op = match value.into_owned() {
Value::ValueBlock => break,
Value::PathListOp(op) => op,
Value::PathVec(paths) => sdf::PathListOp::explicit(paths),
_ => continue,
};
ops.push(Self::map_path_list_op_to_root(list_op, &query_path, &node.map_to_root));
}
Ok(ops)
}
pub(crate) fn resolve_path_list_op_deleted(
&self,
field: FieldKey,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Result<Vec<Path>, QueryError> {
let ops = self.collect_path_list_ops(field, stack, prop_suffix)?;
let mut deleted = Vec::new();
for op in ops.iter().rev() {
if op.explicit {
deleted.clear();
continue;
}
deleted.extend(op.deleted_items.iter().cloned());
}
Ok(deleted)
}
fn map_path_list_op_to_root(op: sdf::PathListOp, anchor: &Path, map: &MapFunction) -> sdf::PathListOp {
fn map_paths(paths: Vec<Path>, anchor: &Path, map: &MapFunction) -> Vec<Path> {
paths
.into_iter()
.filter_map(|path| {
let mut absolute = anchor.make_absolute(&path);
if absolute.contains_prim_variant_selection() {
absolute = absolute.strip_all_variant_selections();
}
map.map_source_to_target(&absolute)
})
.collect()
}
sdf::PathListOp {
explicit: op.explicit,
explicit_items: map_paths(op.explicit_items, anchor, map),
added_items: map_paths(op.added_items, anchor, map),
prepended_items: map_paths(op.prepended_items, anchor, map),
appended_items: map_paths(op.appended_items, anchor, map),
deleted_items: map_paths(op.deleted_items, anchor, map),
ordered_items: map_paths(op.ordered_items, anchor, map),
}
}
fn map_expression_to_root(expr: sdf::PathExpression, anchor: &Path, map: &MapFunction) -> sdf::PathExpression {
expr.make_absolute(anchor).map_paths(|path| {
let mut absolute = path.clone();
if absolute.contains_prim_variant_selection() {
absolute = absolute.strip_all_variant_selections();
}
map.map_source_to_target(&absolute)
})
}
pub(super) fn query_path<'a>(node: &'a Node, prop_suffix: Option<&str>) -> Cow<'a, Path> {
match prop_suffix {
Some(suffix) => Cow::Owned(Path::from_str_unchecked(&format!("{}{suffix}", node.path))),
None => Cow::Borrowed(&node.path),
}
}
fn contributing_sites<'a>(
&'a self,
prop_suffix: Option<&'a str>,
) -> impl Iterator<Item = Result<ContributingSite<'a>, QueryError>> + 'a {
self.live_spec_sites().map(move |(site, node)| {
Ok(ContributingSite {
node,
layer: site.layer,
offset: site.offset,
query_path: Self::query_path(node, prop_suffix),
})
})
}
fn opinions<'a>(
&'a self,
field: &'a str,
stack: &'a LayerGraph,
prop_suffix: Option<&'a str>,
) -> impl Iterator<Item = Result<Opinion<'a>, QueryError>> + 'a {
self.opinions_in_layer_time(field, stack, prop_suffix)
.map(|opinion| opinion.map(Opinion::retimed))
}
fn opinions_in_layer_time<'a>(
&'a self,
field: &'a str,
stack: &'a LayerGraph,
prop_suffix: Option<&'a str>,
) -> impl Iterator<Item = Result<Opinion<'a>, QueryError>> + 'a {
self.contributing_sites(prop_suffix).filter_map(move |site| {
let site = match site {
Ok(site) => site,
Err(err) => return Some(Err(err)),
};
match stack.layer(site.layer).data().try_field(&site.query_path, field) {
Ok(Some(value)) => Some(Ok(Opinion {
node: site.node,
layer: site.layer,
query_path: site.query_path,
value,
offset: site.offset,
})),
Ok(None) => None,
Err(err) => Some(Err(err.into())),
}
})
}
pub(crate) fn strongest_opinion(
&self,
field: &str,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Option<AssetSite> {
for site in self.contributing_sites(prop_suffix) {
let Ok(site) = site else { continue };
if matches!(
stack.layer(site.layer).data().try_field(&site.query_path, field),
Ok(Some(_))
) {
return Some(AssetSite::in_graph(
stack,
site.node.layer_stack_id(),
site.layer,
&site.query_path,
));
}
}
None
}
pub(crate) fn resolve_strongest(
&self,
field: &str,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Result<Option<Value>, QueryError> {
let mut composing = Composing::new();
for opinion in self.opinions(field, stack, prop_suffix) {
let opinion = opinion?;
let anchor = opinion.query_path.prim_path();
let folded = composing.fold(opinion.value.into_owned(), &anchor, &opinion.node.map_to_root);
if folded.step().stop() {
break;
}
}
Ok(composing.into_open())
}
pub(crate) fn resolve_time_samples(
&self,
stack: &LayerGraph,
prop_suffix: Option<&str>,
) -> Result<Option<sdf::TimeSampleMap>, QueryError> {
self.first_time_samples(stack, prop_suffix, |map, opinion| {
let mut samples = map.clone();
opinion.offset.apply_to_samples(&mut samples);
samples
})
}
fn first_time_samples<R>(
&self,
stack: &LayerGraph,
prop_suffix: Option<&str>,
extract: impl FnOnce(&sdf::TimeSampleMap, &Opinion<'_>) -> R,
) -> Result<Option<R>, QueryError> {
let field = FieldKey::TimeSamples.as_str();
for opinion in self.opinions_in_layer_time(field, stack, prop_suffix) {
let opinion = opinion?;
match &*opinion.value {
Value::ValueBlock => return Ok(None),
Value::TimeSamples(map) => return Ok(Some(extract(map, &opinion))),
_ => {}
}
}
Ok(None)
}
pub(crate) fn clip_sets_order(&self, stack: &LayerGraph) -> Result<Option<Vec<String>>, QueryError> {
let ops = self.clip_sets_ops(stack)?;
if ops.is_empty() {
return Ok(None);
}
Ok(Some(compose_list_ops(&ops)))
}
pub(crate) fn clip_sets_list_op(&self, stack: &LayerGraph) -> Result<Option<sdf::StringListOp>, QueryError> {
Ok(self
.clip_sets_ops(stack)?
.into_iter()
.reduce(|stronger, weaker| stronger.combined_with(&weaker)))
}
fn clip_sets_ops(&self, stack: &LayerGraph) -> Result<Vec<sdf::StringListOp>, QueryError> {
let mut ops = Vec::new();
for opinion in self.opinions_in_layer_time(FieldKey::ClipSets.as_str(), stack, None) {
match opinion?.value.into_owned() {
Value::ValueBlock => break,
Value::StringListOp(op) => ops.push(op),
Value::TokenListOp(op) => ops.push(op.map(String::from)),
Value::StringVec(names) => ops.push(sdf::StringListOp::explicit(names)),
Value::TokenVec(names) => ops.push(sdf::StringListOp::explicit(
names.into_iter().map(String::from).collect::<Vec<_>>(),
)),
_ => {}
}
}
Ok(ops)
}
pub(crate) fn resolve_clip_sets(
&self,
stack: &LayerGraph,
errors: &mut Diagnostics,
) -> Result<Vec<clip::ResolvedClipSet>, QueryError> {
let mut sets: HashMap<String, HashMap<String, Value>> = HashMap::new();
let mut blocked_sets: HashSet<String> = HashSet::new();
let mut asset_sources: HashMap<String, clip::ClipAnchor> = HashMap::new();
let mut manifest_layers: HashMap<String, LayerId> = HashMap::new();
let mut explicit_sets: HashSet<String> = HashSet::new();
let mut asset_sites: HashMap<String, ClipAssetSites> = HashMap::new();
let mut template_offsets: HashMap<String, LayerOffset> = HashMap::new();
let mut active_offsets: HashMap<String, LayerOffset> = HashMap::new();
for opinion in self.opinions_in_layer_time(FieldKey::Clips.as_str(), stack, None) {
let Opinion {
node,
layer,
query_path,
value,
offset,
} = opinion?;
let node_stack = node.layer_stack_id();
match value.into_owned() {
Value::ValueBlock => break,
Value::Dictionary(dict) => {
for (set_name, set_value) in dict {
if blocked_sets.contains(&set_name) {
continue;
}
let Value::Dictionary(fields) = set_value else {
if !sets.contains_key(&set_name) {
blocked_sets.insert(set_name);
}
continue;
};
let composed = sets.entry(set_name.clone()).or_default();
for (field, value) in fields {
if composed.contains_key(&field) {
continue;
}
let value = if field == clip::keys::ACTIVE || field == clip::keys::TIMES {
retime_clip_stage_times(value, offset)
} else if field == clip::keys::TEMPLATE_ASSET_PATH
|| field == clip::keys::MANIFEST_ASSET_PATH
{
clip::as_asset_field(value)
} else {
value
};
let site = || {
sdf::holds_asset_expression(&value)
.then(|| AssetSite::in_graph(stack, node_stack, layer, &query_path))
};
if field == clip::keys::ACTIVE {
active_offsets.insert(set_name.clone(), offset);
}
if field == clip::keys::ASSET_PATHS {
asset_sources.insert(
set_name.clone(),
clip::ClipAnchor {
layer,
prim_path: query_path.clone().into_owned(),
stack: node_stack,
},
);
explicit_sets.insert(set_name.clone());
if let Some(site) = site() {
asset_sites.entry(set_name.clone()).or_default().asset_paths = Some(site);
}
} else if field == clip::keys::TEMPLATE_ASSET_PATH {
if !explicit_sets.contains(&set_name) {
asset_sources.insert(
set_name.clone(),
clip::ClipAnchor {
layer,
prim_path: query_path.clone().into_owned(),
stack: node_stack,
},
);
}
template_offsets.insert(set_name.clone(), offset);
if let Some(site) = site() {
asset_sites.entry(set_name.clone()).or_default().template = Some(site);
}
} else if field == clip::keys::MANIFEST_ASSET_PATH {
manifest_layers.insert(set_name.clone(), layer);
if let Some(site) = site() {
asset_sites.entry(set_name.clone()).or_default().manifest = Some(site);
}
}
composed.insert(field, value);
}
}
}
_ => {}
}
}
let order = self.clip_sets_order(stack)?;
evaluate_clip_assets(stack, &mut sets, order.as_deref(), &asset_sites, errors);
let clips = Value::Dictionary(
sets.into_iter()
.map(|(name, fields)| (name, Value::Dictionary(fields)))
.collect(),
);
Ok(clip::ClipSet::parse(&clips, order.as_deref())
.into_iter()
.filter_map(|mut set| {
let anchor = asset_sources.get(&set.name)?;
let manifest_layer = manifest_layers.get(&set.name).copied();
let template_offset = template_offsets.get(&set.name).copied();
if !explicit_sets.contains(&set.name)
&& let Some(offset) = template_offset
{
set.retime_stage_times(offset);
}
let active_offset = if explicit_sets.contains(&set.name) {
active_offsets.get(&set.name).copied().unwrap_or(LayerOffset::IDENTITY)
} else {
template_offset.unwrap_or(LayerOffset::IDENTITY)
};
Some(clip::ResolvedClipSet {
set,
source: anchor.clone(),
manifest_layer,
active_offset,
})
})
.collect())
}
fn resolve_variability(&self, stack: &LayerGraph, prop_suffix: Option<&str>) -> Result<Option<Value>, QueryError> {
let field = FieldKey::Variability.as_str();
let mut weakest = None;
for opinion in self.opinions(field, stack, prop_suffix) {
let value = opinion?.value;
if matches!(value.as_ref(), Value::ValueBlock) {
break;
}
if matches!(value.as_ref(), Value::Variability(_)) {
weakest = Some(value.into_owned());
}
}
Ok(weakest)
}
fn resolve_custom(&self, stack: &LayerGraph, prop_suffix: Option<&str>) -> Result<Option<Value>, QueryError> {
let field = FieldKey::Custom.as_str();
let mut saw_opinion = false;
for opinion in self.opinions(field, stack, prop_suffix) {
let value = opinion?.value;
if matches!(value.as_ref(), Value::ValueBlock) {
break;
}
saw_opinion = true;
if matches!(value.as_ref(), Value::Bool(true)) {
return Ok(Some(Value::Bool(true)));
}
}
Ok(saw_opinion.then_some(Value::Bool(false)))
}
fn resolve_specifier(&self, stack: &LayerGraph, prop_suffix: Option<&str>) -> Result<Option<Value>, QueryError> {
let field = FieldKey::Specifier.as_str();
let mut specs: Vec<(Specifier, ArcType)> = Vec::new();
for opinion in self.opinions(field, stack, prop_suffix) {
let Opinion { node, value, .. } = opinion?;
if matches!(value.as_ref(), Value::ValueBlock) {
break;
}
if let Value::Specifier(s) = value.into_owned() {
specs.push((s, node.arc));
}
}
if specs.is_empty() {
return Ok(None);
}
let strongest_defining = specs.iter().find(|(s, _)| *s != Specifier::Over).map(|(s, _)| *s);
let Some(strongest) = strongest_defining else {
return Ok(Some(Value::Specifier(Specifier::Over)));
};
let strongest_non_inherit_defining = specs
.iter()
.find(|(s, arc)| *s != Specifier::Over && *arc != ArcType::Inherit)
.map(|(s, _)| *s);
if strongest == Specifier::Def || strongest_non_inherit_defining == Some(Specifier::Def) {
return Ok(Some(Value::Specifier(Specifier::Def)));
}
let all_defining_class = specs
.iter()
.filter(|(s, _)| *s != Specifier::Over)
.all(|(s, _)| *s == Specifier::Class);
if strongest_non_inherit_defining == Some(Specifier::Class) || all_defining_class {
return Ok(Some(Value::Specifier(Specifier::Class)));
}
Ok(Some(Value::Specifier(strongest)))
}
}
#[derive(Default)]
struct ClipAssetSites {
asset_paths: Option<AssetSite>,
template: Option<AssetSite>,
manifest: Option<AssetSite>,
}
fn evaluate_clip_assets(
graph: &LayerGraph,
sets: &mut HashMap<String, HashMap<String, Value>>,
order: Option<&[String]>,
asset_sites: &HashMap<String, ClipAssetSites>,
errors: &mut Diagnostics,
) {
if asset_sites.is_empty() {
return;
}
let effective: Vec<String> = clip::effective_set_names(sets, order).into_iter().cloned().collect();
for name in effective {
let Some(sites) = asset_sites.get(&name) else { continue };
let Some(fields) = sets.get_mut(&name) else { continue };
let explicit = clip::has_explicit_assets(fields);
let reads_template = !explicit && clip::asset_input(fields, clip::keys::TEMPLATE_ASSET_PATH).is_some();
let reads_manifest = clip::asset_input(fields, clip::keys::MANIFEST_ASSET_PATH).is_some();
let fields_and_sites = [
(clip::keys::ASSET_PATHS, explicit, sites.asset_paths.as_ref()),
(clip::keys::TEMPLATE_ASSET_PATH, reads_template, sites.template.as_ref()),
(clip::keys::MANIFEST_ASSET_PATH, reads_manifest, sites.manifest.as_ref()),
];
let mut keep = true;
for (key, read, site) in fields_and_sites {
let (true, Some(site)) = (read, site) else { continue };
let Some((key, value)) = fields.remove_entry(key) else {
continue;
};
let (value, outcome) = asset_resolve::evaluate_values(graph, value, Some(site), errors);
match outcome {
sdf::AssetOutcome::Evaluated => {
fields.insert(key, value);
}
sdf::AssetOutcome::None if key == clip::keys::MANIFEST_ASSET_PATH => {}
_ => {
keep = false;
break;
}
}
}
if !keep {
sets.remove(&name);
}
}
}
fn compose_list_ops<T: Default + Clone + PartialEq>(ops: &[sdf::ListOp<T>]) -> Vec<T> {
let mut result = Vec::new();
for op in ops.iter().rev() {
result = op.compose_over(&result);
}
result
}
fn retime_clip_stage_times(value: Value, offset: LayerOffset) -> Value {
if offset.is_identity() {
return value;
}
match value {
Value::Vec2dVec(pairs) => {
Value::Vec2dVec(pairs.into_iter().map(|p| gf::vec2d(offset.apply(p.x), p.y)).collect())
}
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tf;
fn dict(key: &str) -> Value {
let mut d = sdf::Dictionary::new();
d.insert(key.to_string(), Value::Int(1));
Value::Dictionary(d)
}
fn expr(text: &str) -> Value {
Value::PathExpression(sdf::PathExpression::parse(text))
}
#[test]
fn dictionary_merges_weaker() {
let mut composing = Composing::new();
assert_eq!(composing.fold_rooted(dict("a")), Folded::Composed);
assert_eq!(composing.fold_rooted(Value::Int(7)), Folded::Skipped);
assert_eq!(composing.fold_rooted(dict("b")), Folded::Composed);
assert_eq!(composing.fold_rooted(Value::ValueBlock), Folded::Blocked);
let merged = composing
.close()
.expect("a value")
.try_as_dictionary()
.expect("a dictionary");
assert_eq!(merged.len(), 2, "{merged:?}");
}
#[test]
fn expression_composes_weaker() {
let mut composing = Composing::new();
assert_eq!(composing.fold_rooted(expr("/a// %_")), Folded::Composed);
assert_eq!(composing.fold_rooted(Value::Int(7)), Folded::Skipped);
assert_eq!(
composing.fold_rooted(Value::String("/b// %_".to_string())),
Folded::Composed
);
assert_eq!(composing.fold_rooted(expr("/c//")), Folded::Closed);
assert_eq!(composing.close(), Some(expr("/a// (/b// /c//)")));
}
#[test]
fn open_expression_closes() {
let mut composing = Composing::new();
composing.fold_rooted(expr("/a// %_"));
assert!(composing.is_open());
assert_eq!(composing.close(), Some(expr("/a//")));
}
#[test]
fn dense_value_wins_outright() {
let mut composing = Composing::new();
assert_eq!(composing.fold_rooted(Value::Int(7)), Folded::Closed);
assert!(!composing.is_open());
assert_eq!(composing.close(), Some(Value::Int(7)));
}
fn supplies(value: Value) -> Supplied {
Supplied::classify(&value, &|value| value)
}
fn held_source(value: Value) -> Series<Supplied> {
Series::held(supplies(value))
}
fn sampled_source(entries: impl IntoIterator<Item = (f64, Value)>) -> Series<Supplied> {
Series::sampled(entries.into_iter().map(|(time, value)| (time, supplies(value))))
}
fn reads(series: &Series<Composed>, time: f64) -> String {
match series.at(time) {
Some(Composed::Open(value)) => format!("{value:?}"),
Some(Composed::Closed) => "closed".to_string(),
None => "nothing".to_string(),
}
}
fn compose_sources(sources: impl IntoIterator<Item = Series<Supplied>>) -> Series<Composed> {
let mut composed = Series::<Composed>::default();
for source in sources {
composed.fold(&source);
}
composed
}
#[test]
fn weaker_source_outcomes() {
let open = || held_source(expr("/s// %_"));
let unchanged = format!("{:?}", expr("/s// %_"));
let cases = [
(expr("/w//"), false, "closed"),
(expr("/w// %_"), true, "/s// (/w// %_)"),
(Value::String("/w//".to_string()), false, "closed"),
(Value::Int(7), true, unchanged.as_str()),
(Value::ValueBlock, false, "closed"),
];
for (weaker, stays_open, effect) in cases {
let composed = compose_sources([open(), held_source(weaker.clone())]);
assert_eq!(composed.is_open(), stays_open, "{weaker:?}");
let read = reads(&composed, 0.0);
let effect = match effect.starts_with('/') {
true => format!("{:?}", expr(effect)),
false => effect.to_string(),
};
assert_eq!(read, effect, "{weaker:?}");
}
}
#[test]
fn source_tiers_compose_alike() {
for weaker in [expr("/w//"), Value::Int(7), Value::ValueBlock] {
let held = compose_sources([held_source(expr("/s// %_")), held_source(weaker.clone())]);
let sampled = compose_sources([held_source(expr("/s// %_")), sampled_source([(0.0, weaker.clone())])]);
assert_eq!(
held.is_open(),
sampled.is_open(),
"{weaker:?} answered differently as a sample than as a default"
);
assert_eq!(reads(&held, 0.0), reads(&sampled, 0.0), "{weaker:?}");
}
}
#[test]
fn outcomes_vary_over_time() {
let mixed = || sampled_source([(0.0, Value::ValueBlock), (1.0, Value::Int(7)), (2.0, expr("/w//"))]);
let composed = compose_sources([held_source(expr("/s// %_")), mixed()]);
assert_eq!(reads(&composed, 0.0), "closed", "the block withheld the value");
assert_eq!(
reads(&composed, 1.0),
format!("{:?}", expr("/s// %_")),
"the int was passed over"
);
assert_eq!(reads(&composed, 2.0), "closed", "the expression contributed");
assert!(composed.is_open(), "the time the int was passed over is still open");
let deeper = compose_sources([
held_source(expr("/s// %_")),
mixed(),
sampled_source([(3.0, expr("/z//"))]),
]);
assert_eq!(deeper.times().collect::<Vec<_>>(), vec![0.0, 1.0, 2.0, 3.0]);
assert_eq!(reads(&deeper, 1.0), "closed", "the weakest source composed in there");
assert!(!deeper.is_open());
}
#[test]
fn dense_sample_is_not_carried() {
assert!(matches!(supplies(Value::DoubleVec(vec![1.0; 128])), Supplied::Opaque));
}
#[test]
fn series_holds_first_sample_backwards() {
let composed = compose_sources([
sampled_source([(5.0, expr("/a// %_"))]),
sampled_source([(1.0, expr("/w//"))]),
]);
assert_eq!(composed.times().collect::<Vec<_>>(), vec![1.0, 5.0]);
assert!(!composed.is_open(), "the weaker expression composed in at both times");
}
#[test]
fn held_source_names_no_time() {
let held = compose_sources([held_source(expr("/a// %_"))]);
assert!(held.times().next().is_none());
let composed = compose_sources([held_source(expr("/a// %_")), sampled_source([(2.0, expr("/w//"))])]);
assert_eq!(composed.times().collect::<Vec<_>>(), vec![2.0]);
assert!(!composed.is_open(), "the sampled expression composed in");
}
#[test]
fn composition_questions_nest() {
let cases = [
dict("a"),
expr("/a// %_"),
expr("/a//"),
Value::PathExpressionVec(vec![sdf::PathExpression::parse("/a//")]),
Value::Int(7),
Value::String("/a//".to_string()),
Value::String("/a// %_".to_string()),
Value::Token(tf::Token::new("fixed")),
];
for value in cases {
assert!(
!composes_over_weaker(&value) || reads_into_composition(&value),
"{value:?} composes but is not read into a composition"
);
assert!(
!composes_over_weaker(&value) || composition_rewrites(&value),
"{value:?} composes but is reported as authored"
);
assert_eq!(
reads_into_composition(&value),
matches!(supplies(value.clone()), Supplied::Composable(_)),
"{value:?}"
);
}
}
#[test]
fn composing_kinds_agree() {
let cases = [
dict("a"),
expr("/a// %_"),
expr("/a//"),
Value::PathExpressionVec(vec![sdf::PathExpression::parse("/a// %_")]),
Value::Int(7),
Value::String("/a//".to_string()),
];
for value in cases {
let mut composing = Composing::new();
let folded = composing.fold_rooted(value.clone());
assert_eq!(
composes_over_weaker(&value),
folded == Folded::Composed,
"{value:?} composes but the fold does not read on, or the other way round"
);
assert_eq!(composes_over_weaker(&value), composing.is_open(), "{value:?}");
}
}
#[test]
fn weakest_closes_composition() {
let mut composing = Composing::resumed(expr("/a// %_"));
assert!(composing.is_open());
composing.fold_rooted(expr("/fallback//"));
assert_eq!(composing.close(), Some(expr("/a// /fallback//")));
let dense = Composing::resumed(Value::Int(7));
assert!(!dense.is_open());
assert_eq!(dense.close(), Some(Value::Int(7)));
}
}