use crate::{
Animated, ClipBlendMode, ClipEffect, CompositionId, EffectValue, Generator, MatteMode, NodeId,
ShapeMask, VisualProperties,
};
use alpha2_media::SourceId;
use alpha2_time::MediaTime;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{HashMap, HashSet};
pub const MAX_COMPOSITIONS: usize = 10_000;
pub const MAX_COMPOSITION_NODES: usize = 256;
pub const MAX_EFFECT_INPUTS: usize = 8;
pub const MAX_MERGE_LAYERS: usize = 32;
pub const MAX_GROUP_INPUTS: usize = 16;
pub const MAX_COMPOSITION_CONTROLS: usize = 64;
pub const MAX_COMPOSITION_DEPTH: usize = 64;
pub const MAX_INLINED_NODES: usize = 65_536;
pub const MAX_FRAME_INLINED_NODES: usize = 65_536;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Composition {
pub id: CompositionId,
#[serde(default)]
pub nodes: Vec<CompositionNode>,
#[serde(default)]
pub output: Option<NodeId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub controls: Vec<CompositionControl>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CompositionControl {
pub name: String,
pub node: NodeId,
pub param: String,
}
impl Composition {
#[must_use]
pub const fn new(id: CompositionId) -> Self {
Self {
id,
nodes: Vec::new(),
output: None,
inputs: Vec::new(),
controls: Vec::new(),
}
}
#[must_use]
pub fn node(&self, id: &NodeId) -> Option<&CompositionNode> {
self.nodes.iter().find(|node| &node.id == id)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CompositionNode {
pub id: NodeId,
#[serde(default)]
pub bypass: bool,
pub op: NodeOp,
}
impl CompositionNode {
#[must_use]
pub const fn new(id: NodeId, op: NodeOp) -> Self {
Self {
id,
bypass: false,
op,
}
}
#[must_use]
pub fn needed_inputs(&self) -> Vec<NodeInput<'_>> {
if self.bypass {
return self.op.bypass_input().into_iter().collect();
}
self.op.inputs()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum NodeOp {
Clip {},
Media {
source_id: SourceId,
#[serde(default)]
start: MediaTime,
},
Generator {
generator: Generator,
},
Effect {
#[serde(default)]
input: Option<NodeId>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
inputs: BTreeMap<String, Option<NodeId>>,
effect: ClipEffect,
},
Transition {
#[serde(default)]
from: Option<NodeId>,
#[serde(default)]
to: Option<NodeId>,
effect: ClipEffect,
progress: Animated<f32>,
},
Transform {
#[serde(default)]
input: Option<NodeId>,
#[serde(default)]
transform: VisualProperties,
},
Merge {
#[serde(default)]
layers: Vec<MergeLayer>,
},
Matte {
#[serde(default)]
input: Option<NodeId>,
#[serde(default)]
matte: Option<NodeId>,
mode: MatteMode,
},
ShapeMask {
#[serde(default)]
input: Option<NodeId>,
masks: Vec<ShapeMask>,
},
Group {
composition: CompositionId,
#[serde(default)]
inputs: Vec<Option<NodeId>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
controls: BTreeMap<String, Animated<EffectValue>>,
},
GroupInput {
index: u32,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MergeLayer {
#[serde(default)]
pub input: Option<NodeId>,
#[serde(default)]
pub blend_mode: ClipBlendMode,
#[serde(default = "opaque")]
pub opacity: Animated<f32>,
}
impl MergeLayer {
#[must_use]
pub const fn new(input: Option<NodeId>) -> Self {
Self {
input,
blend_mode: ClipBlendMode::Normal,
opacity: Animated::Constant { value: 1.0 },
}
}
}
const fn opaque() -> Animated<f32> {
Animated::Constant { value: 1.0 }
}
#[derive(Clone, Debug, Default)]
pub struct CompositionReads<'a> {
read: HashMap<&'a CompositionId, Reads>,
}
#[derive(Clone, Debug)]
struct Reads {
ports: Vec<bool>,
clip: bool,
}
impl<'a> CompositionReads<'a> {
#[must_use]
pub fn new(compositions: &HashMap<&'a CompositionId, &'a Composition>) -> Self {
let mut reads = Self::default();
let mut walking = HashSet::new();
let group = |node: &'a CompositionNode| match &node.op {
NodeOp::Group { composition, .. } => compositions.get(composition).copied(),
_ => None,
};
for root in compositions.values() {
let mut stack = vec![(*root, false)];
while let Some((composition, expanded)) = stack.pop() {
if reads.read.contains_key(&composition.id) {
continue;
}
if !expanded {
if !walking.insert(&composition.id) {
continue;
}
stack.push((composition, true));
stack.extend(
composition
.nodes
.iter()
.filter_map(group)
.filter(|inner| {
!reads.read.contains_key(&inner.id) && !walking.contains(&inner.id)
})
.map(|inner| (inner, false)),
);
continue;
}
walking.remove(&composition.id);
let read = reads.walk(composition);
reads.read.insert(&composition.id, read);
}
}
reads
}
#[must_use]
pub fn reads_port(&self, composition: &CompositionId, port: usize) -> bool {
self.read
.get(composition)
.is_none_or(|read| read.ports.get(port).copied().unwrap_or(false))
}
#[must_use]
pub fn draws_clip(&self, composition: &CompositionId) -> bool {
self.read.get(composition).is_none_or(|read| read.clip)
}
#[must_use]
pub fn needed_inputs<'n>(&self, node: &'n CompositionNode) -> Vec<NodeInput<'n>> {
let mut needed = node.needed_inputs();
if let (false, NodeOp::Group { composition, .. }) = (node.bypass, &node.op) {
needed.retain(|input| match input {
NodeInput::Port(port, _) => self.reads_port(composition, *port),
_ => true,
});
}
needed
}
fn walk(&self, composition: &'a Composition) -> Reads {
let nodes: HashMap<&NodeId, &CompositionNode> = composition
.nodes
.iter()
.map(|node| (&node.id, node))
.collect();
let mut read = Reads {
ports: vec![false; composition.inputs.len()],
clip: false,
};
let mut reached = HashSet::new();
let mut pending: Vec<&NodeId> = composition.output.iter().collect();
while let Some(id) = pending.pop() {
let Some(node) = nodes.get(id).filter(|_| reached.insert(id)) else {
continue;
};
match (node.bypass, &node.op) {
(false, NodeOp::GroupInput { index }) => {
if let Some(port) = usize::try_from(*index)
.ok()
.and_then(|index| read.ports.get_mut(index))
{
*port = true;
}
}
(false, NodeOp::Clip {}) => read.clip = true,
(false, NodeOp::Group { composition, .. }) => {
read.clip |= self.draws_clip(composition);
}
_ => {}
}
pending.extend(
self.needed_inputs(node)
.into_iter()
.filter_map(NodeInput::node),
);
}
read
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NodeInput<'a> {
Named(&'static str, Option<&'a NodeId>),
Layer(usize, Option<&'a NodeId>),
Port(usize, Option<&'a NodeId>),
EffectInput(&'a str, Option<&'a NodeId>),
}
impl<'a> NodeInput<'a> {
#[must_use]
pub const fn node(self) -> Option<&'a NodeId> {
match self {
Self::Named(_, node)
| Self::Layer(_, node)
| Self::Port(_, node)
| Self::EffectInput(_, node) => node,
}
}
#[must_use]
pub fn name(self) -> String {
match self {
Self::Named(name, _) => name.to_owned(),
Self::Layer(index, _) => format!("layers[{index}]"),
Self::Port(index, _) => format!("inputs[{index}]"),
Self::EffectInput(name, _) => format!("inputs.{name}"),
}
}
}
impl NodeOp {
#[must_use]
pub fn inputs(&self) -> Vec<NodeInput<'_>> {
match self {
Self::Clip {}
| Self::Media { .. }
| Self::Generator { .. }
| Self::GroupInput { .. } => Vec::new(),
Self::Group { inputs, .. } => inputs
.iter()
.enumerate()
.map(|(index, input)| NodeInput::Port(index, input.as_ref()))
.collect(),
Self::Effect { input, inputs, .. } => {
std::iter::once(NodeInput::Named("input", input.as_ref()))
.chain(
inputs
.iter()
.map(|(name, node)| NodeInput::EffectInput(name, node.as_ref())),
)
.collect()
}
Self::Transform { input, .. } | Self::ShapeMask { input, .. } => {
vec![NodeInput::Named("input", input.as_ref())]
}
Self::Transition { from, to, .. } => vec![
NodeInput::Named("from", from.as_ref()),
NodeInput::Named("to", to.as_ref()),
],
Self::Matte { input, matte, .. } => vec![
NodeInput::Named("input", input.as_ref()),
NodeInput::Named("matte", matte.as_ref()),
],
Self::Merge { layers } => layers
.iter()
.enumerate()
.map(|(index, layer)| NodeInput::Layer(index, layer.input.as_ref()))
.collect(),
}
}
#[must_use]
pub fn bypass_input(&self) -> Option<NodeInput<'_>> {
match self {
Self::Clip {}
| Self::Media { .. }
| Self::Generator { .. }
| Self::GroupInput { .. } => None,
Self::Merge { layers } => layers
.first()
.map(|layer| NodeInput::Layer(0, layer.input.as_ref())),
_ => self.inputs().into_iter().next(),
}
}
#[must_use]
pub const fn has_inputs(&self) -> bool {
match self {
Self::Clip {}
| Self::Media { .. }
| Self::Generator { .. }
| Self::GroupInput { .. } => false,
Self::Group { inputs, .. } => !inputs.is_empty(),
_ => true,
}
}
#[must_use]
pub const fn effect(&self) -> Option<&ClipEffect> {
match self {
Self::Effect { effect, .. } | Self::Transition { effect, .. } => Some(effect),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ClipComposition {
pub id: CompositionId,
#[serde(default)]
pub offset: MediaTime,
#[serde(default)]
pub scale: TimeScale,
}
impl ClipComposition {
#[must_use]
pub const fn new(id: CompositionId) -> Self {
Self {
id,
offset: MediaTime::ZERO,
scale: TimeScale::ONE,
}
}
#[must_use]
pub fn local_time(&self, clip_start: MediaTime, time: MediaTime) -> Option<MediaTime> {
let elapsed = i128::from(time.ticks()) - i128::from(clip_start.ticks());
let scaled = (elapsed * i128::from(self.scale.num))
.checked_div_euclid(i128::from(self.scale.den))?;
let local = i128::from(self.offset.ticks()).checked_add(scaled)?;
i64::try_from(local).ok().map(MediaTime)
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TimeScale {
pub num: u32,
pub den: u32,
}
impl TimeScale {
pub const ONE: Self = Self { num: 1, den: 1 };
#[must_use]
pub const fn is_valid(self) -> bool {
self.num > 0 && self.den > 0
}
}
impl Default for TimeScale {
fn default() -> Self {
Self::ONE
}
}
pub(crate) fn validate_compositions<'a>(
compositions: &'a [Composition],
sources: &'a [crate::MediaSource],
by_source: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
canvas: (u32, u32),
) -> Result<InlinedNodes<'a>, crate::ModelError> {
use crate::ModelError;
use std::collections::{HashMap, HashSet};
if compositions.len() > MAX_COMPOSITIONS {
return Err(ModelError::TooManyCompositions {
found: compositions.len(),
maximum: MAX_COMPOSITIONS,
});
}
let mut ids = HashSet::with_capacity(compositions.len());
for composition in compositions {
if !ids.insert(&composition.id) {
return Err(ModelError::DuplicateCompositionId(composition.id.clone()));
}
}
let by_id: HashMap<&CompositionId, &Composition> = compositions
.iter()
.map(|composition| (&composition.id, composition))
.collect();
if let Some(source) = sources.iter().find(|source| {
source
.composition
.as_ref()
.is_some_and(|id| !by_id.contains_key(id))
}) {
return Err(ModelError::InvalidCompositionSource {
source: source.id.clone(),
});
}
for composition in compositions {
if composition.nodes.len() > MAX_COMPOSITION_NODES {
return Err(ModelError::TooManyCompositionNodes {
composition: composition.id.clone(),
found: composition.nodes.len(),
maximum: MAX_COMPOSITION_NODES,
});
}
let mut positions = HashMap::with_capacity(composition.nodes.len());
for (position, node) in composition.nodes.iter().enumerate() {
if node.id.as_str().contains('/') {
return Err(ModelError::InvalidNodeId {
composition: composition.id.clone(),
node: node.id.clone(),
});
}
if positions.insert(&node.id, position).is_some() {
return Err(ModelError::DuplicateNodeId {
composition: composition.id.clone(),
node: node.id.clone(),
});
}
}
validate_ports(composition, &positions)?;
for node in &composition.nodes {
validate_node(
composition,
node,
by_source,
&by_id,
effect_resources,
canvas,
)?;
for input in node.op.inputs() {
if let Some(target) = input.node()
&& !positions.contains_key(target)
{
return Err(ModelError::UnknownNodeInput {
composition: composition.id.clone(),
node: node.id.clone(),
input: input.name(),
target: target.clone(),
});
}
}
}
if let Some(output) = &composition.output
&& !positions.contains_key(output)
{
return Err(ModelError::UnknownCompositionOutput {
composition: composition.id.clone(),
node: output.clone(),
});
}
if let Some(node) = cycle_member(composition, &positions) {
return Err(ModelError::CompositionCycle {
composition: composition.id.clone(),
node: node.clone(),
});
}
}
if let Some(composition) = reference_cycle_member(compositions, &by_id, by_source) {
return Err(ModelError::CompositionUsesItself {
composition: composition.clone(),
});
}
validate_expansion(compositions, &by_id, sources)
}
pub(crate) type InlinedNodes<'a> = std::collections::HashMap<&'a CompositionId, u64>;
fn validate_expansion<'a>(
compositions: &'a [Composition],
by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
sources: &'a [crate::MediaSource],
) -> Result<InlinedNodes<'a>, crate::ModelError> {
use std::collections::HashMap;
let composition_sources: HashMap<&SourceId, &CompositionId> = sources
.iter()
.filter_map(|source| Some((&source.id, source.composition.as_ref()?)))
.collect();
let draws = |node: &'a CompositionNode| -> Option<&'a CompositionId> {
match &node.op {
NodeOp::Group { composition, .. } => Some(composition),
NodeOp::Media { source_id, .. } => composition_sources.get(source_id).copied(),
_ => None,
}
.filter(|id| by_id.contains_key(id))
};
let mut walked: HashMap<&CompositionId, (usize, u64)> = HashMap::new();
let mut total = 0_u64;
for root in compositions {
let mut stack = vec![(root, false)];
while let Some((composition, expanded)) = stack.pop() {
if walked.contains_key(&composition.id) {
continue;
}
if !expanded {
stack.push((composition, true));
stack.extend(
composition
.nodes
.iter()
.filter_map(draws)
.filter(|id| !walked.contains_key(id))
.map(|id| (by_id[id], false)),
);
continue;
}
let mut depth = 1;
let mut nodes = 0_u64;
for node in &composition.nodes {
let (inner, count) = draws(node).map_or((0, 1), |id| walked[id]);
depth = depth.max(inner + 1);
nodes = nodes.saturating_add(count);
}
walked.insert(&composition.id, (depth, nodes));
}
let (depth, nodes) = walked[&root.id];
if depth > MAX_COMPOSITION_DEPTH {
return Err(crate::ModelError::CompositionNestedTooDeep {
composition: root.id.clone(),
maximum: MAX_COMPOSITION_DEPTH,
});
}
total = total.saturating_add(nodes);
if total > MAX_INLINED_NODES as u64 {
return Err(crate::ModelError::TooManyInlinedNodes {
maximum: MAX_INLINED_NODES,
});
}
}
Ok(walked
.into_iter()
.map(|(id, (_, nodes))| (id, nodes))
.collect())
}
fn validate_ports(
composition: &Composition,
positions: &std::collections::HashMap<&NodeId, usize>,
) -> Result<(), crate::ModelError> {
use std::collections::HashSet;
let invalid = || crate::ModelError::InvalidCompositionPorts {
composition: composition.id.clone(),
};
let mut names = HashSet::new();
if composition.inputs.len() > MAX_GROUP_INPUTS
|| composition
.inputs
.iter()
.any(|name| name.is_empty() || !names.insert(name))
{
return Err(invalid());
}
let mut controls = HashSet::new();
let mut settings = HashSet::new();
if composition.controls.len() > MAX_COMPOSITION_CONTROLS {
return Err(invalid());
}
for control in &composition.controls {
let node = positions
.get(&control.node)
.map(|position| &composition.nodes[*position]);
if control.name.is_empty()
|| control.param.is_empty()
|| !controls.insert(&control.name)
|| !settings.insert((&control.node, &control.param))
|| node.is_none_or(|node| node.op.effect().is_none())
{
return Err(invalid());
}
}
Ok(())
}
fn reference_cycle_member<'a>(
compositions: &'a [Composition],
by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
sources: &std::collections::HashMap<&SourceId, &'a crate::MediaSource>,
) -> Option<&'a CompositionId> {
#[derive(Clone, Copy, PartialEq)]
enum Mark {
New,
Open,
Done,
}
let uses = |composition: &'a Composition| -> Vec<&'a CompositionId> {
composition
.nodes
.iter()
.filter_map(|node| match &node.op {
NodeOp::Group { composition, .. } => Some(composition),
NodeOp::Media { source_id, .. } => sources
.get(source_id)
.and_then(|source| source.composition.as_ref()),
_ => None,
})
.filter(|id| by_id.contains_key(id))
.collect()
};
let mut marks: std::collections::HashMap<&CompositionId, Mark> = compositions
.iter()
.map(|composition| (&composition.id, Mark::New))
.collect();
for root in compositions {
if marks[&root.id] != Mark::New {
continue;
}
let mut stack = vec![(&root.id, uses(root), 0_usize)];
marks.insert(&root.id, Mark::Open);
while let Some((current, next, at)) = stack.last_mut() {
let Some(target) = next.get(*at).copied() else {
marks.insert(current, Mark::Done);
stack.pop();
continue;
};
*at += 1;
match marks[target] {
Mark::Open => return Some(target),
Mark::New => {
marks.insert(target, Mark::Open);
stack.push((target, uses(by_id[target]), 0));
}
Mark::Done => {}
}
}
}
None
}
fn shape_error(
composition: &Composition,
node: &CompositionNode,
issue: crate::ShapeIssue,
) -> crate::ModelError {
crate::ModelError::InvalidShape {
owner: crate::ShapeOwner::Node {
composition: composition.id.clone(),
node: node.id.clone(),
},
at: issue.at,
problem: issue.problem,
}
}
fn layout_error(
composition: &Composition,
node: &CompositionNode,
issue: crate::LayoutIssue,
) -> crate::ModelError {
crate::ModelError::InvalidLayout {
owner: crate::ShapeOwner::Node {
composition: composition.id.clone(),
node: node.id.clone(),
},
at: issue.at,
problem: issue.problem,
}
}
fn validate_node(
composition: &Composition,
node: &CompositionNode,
sources: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
compositions: &std::collections::HashMap<&CompositionId, &Composition>,
effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
canvas: (u32, u32),
) -> Result<(), crate::ModelError> {
use crate::ModelError;
let invalid = || ModelError::InvalidNode {
composition: composition.id.clone(),
node: node.id.clone(),
};
let unit = |animation: &Animated<f32>| {
crate::animation::valid_scalar_values(animation, |value| (0.0..=1.0).contains(&value))
};
let shape_error = |issue| shape_error(composition, node, issue);
if node.bypass && !node.op.has_inputs() {
return Err(invalid());
}
let valid = match &node.op {
NodeOp::Clip {} | NodeOp::Matte { .. } => true,
NodeOp::Media { source_id, start } => {
let visual = sources.get(source_id).is_some_and(|source| {
matches!(
source.kind,
crate::MediaKind::Video
| crate::MediaKind::Image
| crate::MediaKind::Generated
| crate::MediaKind::Composition
)
});
if !visual {
return Err(ModelError::InvalidNodeSource {
composition: composition.id.clone(),
node: node.id.clone(),
source: source_id.clone(),
});
}
start.ticks() >= 0
}
NodeOp::Generator { generator } => match generator.validate_on(canvas.0, canvas.1) {
Ok(()) => true,
Err(crate::GeneratorIssue::Shape(issue)) => return Err(shape_error(issue)),
Err(crate::GeneratorIssue::Layout(issue)) => {
return Err(layout_error(composition, node, issue));
}
Err(crate::GeneratorIssue::Invalid) => false,
},
NodeOp::Effect { effect, inputs, .. } => {
effect.is_valid()
&& inputs.len() <= MAX_EFFECT_INPUTS
&& inputs
.keys()
.all(|name| crate::effect::valid_binding_name(name))
}
NodeOp::Transition {
effect, progress, ..
} => effect.is_valid() && unit(progress),
NodeOp::Transform { transform, .. } => transform.validate(),
NodeOp::Merge { layers } => {
layers.len() <= MAX_MERGE_LAYERS && layers.iter().all(|layer| unit(&layer.opacity))
}
NodeOp::ShapeMask { masks, .. } => {
crate::mask::validate_masks(masks).map_err(shape_error)?;
masks.len() <= crate::MAX_CLIP_MASKS
}
NodeOp::Group {
composition: target,
inputs,
controls,
} => {
let Some(target) = compositions.get(target) else {
return Err(ModelError::UnknownGroupComposition {
composition: composition.id.clone(),
node: node.id.clone(),
target: target.clone(),
});
};
inputs.len() == target.inputs.len()
&& controls.iter().all(|(name, value)| {
target.controls.iter().any(|control| &control.name == name)
&& crate::effect::valid_param(value)
})
}
NodeOp::GroupInput { index } => {
usize::try_from(*index).is_ok_and(|index| index < composition.inputs.len())
}
};
if !valid {
return Err(invalid());
}
if let Some(effect) = node.op.effect()
&& let Some(resource) = effect
.resources
.values()
.find(|resource| !effect_resources.contains(resource))
{
return Err(ModelError::UnknownNodeResource {
composition: composition.id.clone(),
node: node.id.clone(),
resource: resource.clone(),
});
}
Ok(())
}
fn cycle_member<'a>(
composition: &'a Composition,
positions: &std::collections::HashMap<&'a NodeId, usize>,
) -> Option<&'a NodeId> {
#[derive(Clone, Copy, PartialEq)]
enum Mark {
New,
Open,
Done,
}
let nodes = &composition.nodes;
let mut marks = vec![Mark::New; nodes.len()];
for root in 0..nodes.len() {
if marks[root] != Mark::New {
continue;
}
let mut stack = vec![(root, 0_usize)];
marks[root] = Mark::Open;
while let Some((current, next)) = stack.last_mut() {
let inputs = nodes[*current].op.inputs();
let Some(input) = inputs.get(*next) else {
marks[*current] = Mark::Done;
stack.pop();
continue;
};
*next += 1;
let Some(target) = input.node().and_then(|id| positions.get(id)) else {
continue;
};
match marks[*target] {
Mark::Open => return Some(&nodes[*target].id),
Mark::New => {
marks[*target] = Mark::Open;
stack.push((*target, 0));
}
Mark::Done => {}
}
}
}
None
}
#[cfg(test)]
#[path = "composition_tests.rs"]
mod tests;