1use crate::animation::opaque;
28use crate::{
29 Animated, ClipBlendMode, ClipEffect, CompositionId, EffectValue, Generator, MatteMode, NodeId,
30 ShapeMask, VisualProperties,
31};
32use alpha2_media::SourceId;
33use alpha2_time::MediaTime;
34use serde::{Deserialize, Serialize};
35use std::collections::BTreeMap;
36use std::collections::{HashMap, HashSet};
37
38pub const MAX_COMPOSITIONS: usize = 10_000;
40pub const MAX_COMPOSITION_NODES: usize = 256;
42pub const MAX_EFFECT_INPUTS: usize = 8;
45pub const MAX_MERGE_LAYERS: usize = 32;
47pub const MAX_GROUP_INPUTS: usize = 16;
49pub const MAX_COMPOSITION_CONTROLS: usize = 64;
51pub const MAX_COMPOSITION_DEPTH: usize = 64;
56pub const MAX_INLINED_NODES: usize = 65_536;
62pub const MAX_FRAME_INLINED_NODES: usize = 65_536;
70
71#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[serde(deny_unknown_fields)]
75pub struct Composition {
76 pub id: CompositionId,
78 #[serde(default)]
81 pub nodes: Vec<CompositionNode>,
82 #[serde(default)]
85 pub output: Option<NodeId>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub inputs: Vec<String>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub controls: Vec<CompositionControl>,
93}
94
95#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
98#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
99#[serde(deny_unknown_fields)]
100pub struct CompositionControl {
101 pub name: String,
103 pub node: NodeId,
105 pub param: String,
107}
108
109impl Composition {
110 #[must_use]
112 pub const fn new(id: CompositionId) -> Self {
113 Self {
114 id,
115 nodes: Vec::new(),
116 output: None,
117 inputs: Vec::new(),
118 controls: Vec::new(),
119 }
120 }
121
122 #[must_use]
124 pub fn node(&self, id: &NodeId) -> Option<&CompositionNode> {
125 self.nodes.iter().find(|node| &node.id == id)
126 }
127}
128
129#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(deny_unknown_fields)]
133pub struct CompositionNode {
134 pub id: NodeId,
136 #[serde(default)]
140 pub bypass: bool,
141 pub op: NodeOp,
143}
144
145impl CompositionNode {
146 #[must_use]
148 pub const fn new(id: NodeId, op: NodeOp) -> Self {
149 Self {
150 id,
151 bypass: false,
152 op,
153 }
154 }
155
156 #[must_use]
161 pub fn needed_inputs(&self) -> Vec<NodeInput<'_>> {
162 if self.bypass {
163 return self.op.bypass_input().into_iter().collect();
164 }
165 self.op.inputs()
166 }
167}
168
169#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
176pub enum NodeOp {
177 Clip {},
181 Media {
184 source_id: SourceId,
186 #[serde(default)]
188 start: MediaTime,
189 },
190 Generator {
192 generator: Generator,
194 },
195 Effect {
198 #[serde(default)]
200 input: Option<NodeId>,
201 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
207 inputs: BTreeMap<String, Option<NodeId>>,
208 effect: ClipEffect,
210 },
211 Transition {
213 #[serde(default)]
215 from: Option<NodeId>,
216 #[serde(default)]
218 to: Option<NodeId>,
219 effect: ClipEffect,
221 progress: Animated<f32>,
223 },
224 Transform {
226 #[serde(default)]
228 input: Option<NodeId>,
229 #[serde(default)]
231 transform: VisualProperties,
232 },
233 Merge {
235 #[serde(default)]
237 layers: Vec<MergeLayer>,
238 },
239 Matte {
241 #[serde(default)]
243 input: Option<NodeId>,
244 #[serde(default)]
246 matte: Option<NodeId>,
247 mode: MatteMode,
249 },
250 ShapeMask {
252 #[serde(default)]
254 input: Option<NodeId>,
255 masks: Vec<ShapeMask>,
258 },
259 Group {
263 composition: CompositionId,
265 #[serde(default)]
267 inputs: Vec<Option<NodeId>>,
268 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
271 controls: BTreeMap<String, Animated<EffectValue>>,
272 },
273 GroupInput {
276 index: u32,
278 },
279}
280
281#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[serde(deny_unknown_fields)]
285pub struct MergeLayer {
286 #[serde(default)]
288 pub input: Option<NodeId>,
289 #[serde(default)]
291 pub blend_mode: ClipBlendMode,
292 #[serde(default = "opaque")]
294 pub opacity: Animated<f32>,
295}
296
297impl MergeLayer {
298 #[must_use]
300 pub const fn new(input: Option<NodeId>) -> Self {
301 Self {
302 input,
303 blend_mode: ClipBlendMode::Normal,
304 opacity: Animated::Constant { value: 1.0 },
305 }
306 }
307}
308
309#[derive(Clone, Debug, Default)]
320pub struct CompositionReads<'a> {
321 read: HashMap<&'a CompositionId, Reads>,
322}
323
324#[derive(Clone, Debug)]
326struct Reads {
327 ports: Vec<bool>,
329 clip: bool,
331}
332
333impl<'a> CompositionReads<'a> {
334 #[must_use]
336 pub fn new(compositions: &HashMap<&'a CompositionId, &'a Composition>) -> Self {
337 let mut reads = Self::default();
338 let mut walking = HashSet::new();
339 let group = |node: &'a CompositionNode| match &node.op {
340 NodeOp::Group { composition, .. } => compositions.get(composition).copied(),
341 _ => None,
342 };
343 for root in compositions.values() {
344 let mut stack = vec![(*root, false)];
345 while let Some((composition, expanded)) = stack.pop() {
346 if reads.read.contains_key(&composition.id) {
347 continue;
348 }
349 if !expanded {
350 if !walking.insert(&composition.id) {
351 continue;
352 }
353 stack.push((composition, true));
354 stack.extend(
355 composition
356 .nodes
357 .iter()
358 .filter_map(group)
359 .filter(|inner| {
360 !reads.read.contains_key(&inner.id) && !walking.contains(&inner.id)
361 })
362 .map(|inner| (inner, false)),
363 );
364 continue;
365 }
366 walking.remove(&composition.id);
367 let read = reads.walk(composition);
368 reads.read.insert(&composition.id, read);
369 }
370 }
371 reads
372 }
373
374 #[must_use]
377 pub fn reads_port(&self, composition: &CompositionId, port: usize) -> bool {
378 self.read
379 .get(composition)
380 .is_none_or(|read| read.ports.get(port).copied().unwrap_or(false))
381 }
382
383 #[must_use]
387 pub fn draws_clip(&self, composition: &CompositionId) -> bool {
388 self.read.get(composition).is_none_or(|read| read.clip)
389 }
390
391 #[must_use]
395 pub fn needed_inputs<'n>(&self, node: &'n CompositionNode) -> Vec<NodeInput<'n>> {
396 let mut needed = node.needed_inputs();
397 if let (false, NodeOp::Group { composition, .. }) = (node.bypass, &node.op) {
398 needed.retain(|input| match input {
399 NodeInput::Port(port, _) => self.reads_port(composition, *port),
400 _ => true,
401 });
402 }
403 needed
404 }
405
406 fn walk(&self, composition: &'a Composition) -> Reads {
409 let nodes: HashMap<&NodeId, &CompositionNode> = composition
410 .nodes
411 .iter()
412 .map(|node| (&node.id, node))
413 .collect();
414 let mut read = Reads {
415 ports: vec![false; composition.inputs.len()],
416 clip: false,
417 };
418 let mut reached = HashSet::new();
419 let mut pending: Vec<&NodeId> = composition.output.iter().collect();
420 while let Some(id) = pending.pop() {
421 let Some(node) = nodes.get(id).filter(|_| reached.insert(id)) else {
422 continue;
423 };
424 match (node.bypass, &node.op) {
425 (false, NodeOp::GroupInput { index }) => {
426 if let Some(port) = usize::try_from(*index)
427 .ok()
428 .and_then(|index| read.ports.get_mut(index))
429 {
430 *port = true;
431 }
432 }
433 (false, NodeOp::Clip {}) => read.clip = true,
434 (false, NodeOp::Group { composition, .. }) => {
435 read.clip |= self.draws_clip(composition);
436 }
437 _ => {}
438 }
439 pending.extend(
440 self.needed_inputs(node)
441 .into_iter()
442 .filter_map(NodeInput::node),
443 );
444 }
445 read
446 }
447}
448
449#[derive(Clone, Copy, Debug, Eq, PartialEq)]
451pub enum NodeInput<'a> {
452 Named(&'static str, Option<&'a NodeId>),
454 Layer(usize, Option<&'a NodeId>),
456 Port(usize, Option<&'a NodeId>),
458 EffectInput(&'a str, Option<&'a NodeId>),
460}
461
462impl<'a> NodeInput<'a> {
463 #[must_use]
465 pub const fn node(self) -> Option<&'a NodeId> {
466 match self {
467 Self::Named(_, node)
468 | Self::Layer(_, node)
469 | Self::Port(_, node)
470 | Self::EffectInput(_, node) => node,
471 }
472 }
473
474 #[must_use]
477 pub fn name(self) -> String {
478 match self {
479 Self::Named(name, _) => name.to_owned(),
480 Self::Layer(index, _) => format!("layers[{index}]"),
481 Self::Port(index, _) => format!("inputs[{index}]"),
482 Self::EffectInput(name, _) => format!("inputs.{name}"),
483 }
484 }
485}
486
487impl NodeOp {
488 #[must_use]
490 pub fn inputs(&self) -> Vec<NodeInput<'_>> {
491 match self {
492 Self::Clip {}
493 | Self::Media { .. }
494 | Self::Generator { .. }
495 | Self::GroupInput { .. } => Vec::new(),
496 Self::Group { inputs, .. } => inputs
497 .iter()
498 .enumerate()
499 .map(|(index, input)| NodeInput::Port(index, input.as_ref()))
500 .collect(),
501 Self::Effect { input, inputs, .. } => {
502 std::iter::once(NodeInput::Named("input", input.as_ref()))
503 .chain(
504 inputs
505 .iter()
506 .map(|(name, node)| NodeInput::EffectInput(name, node.as_ref())),
507 )
508 .collect()
509 }
510 Self::Transform { input, .. } | Self::ShapeMask { input, .. } => {
511 vec![NodeInput::Named("input", input.as_ref())]
512 }
513 Self::Transition { from, to, .. } => vec![
514 NodeInput::Named("from", from.as_ref()),
515 NodeInput::Named("to", to.as_ref()),
516 ],
517 Self::Matte { input, matte, .. } => vec![
518 NodeInput::Named("input", input.as_ref()),
519 NodeInput::Named("matte", matte.as_ref()),
520 ],
521 Self::Merge { layers } => layers
522 .iter()
523 .enumerate()
524 .map(|(index, layer)| NodeInput::Layer(index, layer.input.as_ref()))
525 .collect(),
526 }
527 }
528
529 #[must_use]
534 pub fn bypass_input(&self) -> Option<NodeInput<'_>> {
535 match self {
536 Self::Clip {}
537 | Self::Media { .. }
538 | Self::Generator { .. }
539 | Self::GroupInput { .. } => None,
540 Self::Merge { layers } => layers
541 .first()
542 .map(|layer| NodeInput::Layer(0, layer.input.as_ref())),
543 _ => self.inputs().into_iter().next(),
544 }
545 }
546
547 #[must_use]
549 pub const fn has_inputs(&self) -> bool {
550 match self {
551 Self::Clip {}
552 | Self::Media { .. }
553 | Self::Generator { .. }
554 | Self::GroupInput { .. } => false,
555 Self::Group { inputs, .. } => !inputs.is_empty(),
556 _ => true,
557 }
558 }
559
560 #[must_use]
562 pub const fn effect(&self) -> Option<&ClipEffect> {
563 match self {
564 Self::Effect { effect, .. } | Self::Transition { effect, .. } => Some(effect),
565 _ => None,
566 }
567 }
568}
569
570#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
580#[serde(deny_unknown_fields)]
581pub struct ClipComposition {
582 pub id: CompositionId,
584 #[serde(default)]
586 pub offset: MediaTime,
587 #[serde(default)]
589 pub scale: TimeScale,
590}
591
592impl ClipComposition {
593 #[must_use]
595 pub const fn new(id: CompositionId) -> Self {
596 Self {
597 id,
598 offset: MediaTime::ZERO,
599 scale: TimeScale::ONE,
600 }
601 }
602
603 #[must_use]
607 pub fn local_time(&self, clip_start: MediaTime, time: MediaTime) -> Option<MediaTime> {
608 let elapsed = i128::from(time.ticks()) - i128::from(clip_start.ticks());
609 let scaled = (elapsed * i128::from(self.scale.num))
610 .checked_div_euclid(i128::from(self.scale.den))?;
611 let local = i128::from(self.offset.ticks()).checked_add(scaled)?;
612 i64::try_from(local).ok().map(MediaTime)
613 }
614}
615
616#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
618#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
619#[serde(deny_unknown_fields)]
620pub struct TimeScale {
621 pub num: u32,
623 pub den: u32,
625}
626
627impl TimeScale {
628 pub const ONE: Self = Self { num: 1, den: 1 };
630
631 #[must_use]
633 pub const fn is_valid(self) -> bool {
634 self.num > 0 && self.den > 0
635 }
636}
637
638impl Default for TimeScale {
639 fn default() -> Self {
640 Self::ONE
641 }
642}
643
644pub(crate) fn validate_compositions<'a>(
649 compositions: &'a [Composition],
650 sources: &'a [crate::MediaSource],
651 by_source: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
652 effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
653 canvas: (u32, u32),
654) -> Result<InlinedNodes<'a>, crate::ModelError> {
655 use crate::ModelError;
656 use std::collections::{HashMap, HashSet};
657
658 if compositions.len() > MAX_COMPOSITIONS {
659 return Err(ModelError::TooManyCompositions {
660 found: compositions.len(),
661 maximum: MAX_COMPOSITIONS,
662 });
663 }
664 let mut ids = HashSet::with_capacity(compositions.len());
665 for composition in compositions {
666 if !ids.insert(&composition.id) {
667 return Err(ModelError::DuplicateCompositionId(composition.id.clone()));
668 }
669 }
670 let by_id: HashMap<&CompositionId, &Composition> = compositions
671 .iter()
672 .map(|composition| (&composition.id, composition))
673 .collect();
674 if let Some(source) = sources.iter().find(|source| {
675 source
676 .composition
677 .as_ref()
678 .is_some_and(|id| !by_id.contains_key(id))
679 }) {
680 return Err(ModelError::InvalidCompositionSource {
681 source: source.id.clone(),
682 });
683 }
684 for composition in compositions {
685 if composition.nodes.len() > MAX_COMPOSITION_NODES {
686 return Err(ModelError::TooManyCompositionNodes {
687 composition: composition.id.clone(),
688 found: composition.nodes.len(),
689 maximum: MAX_COMPOSITION_NODES,
690 });
691 }
692 let mut positions = HashMap::with_capacity(composition.nodes.len());
693 for (position, node) in composition.nodes.iter().enumerate() {
694 if node.id.as_str().contains('/') {
695 return Err(ModelError::InvalidNodeId {
696 composition: composition.id.clone(),
697 node: node.id.clone(),
698 });
699 }
700 if positions.insert(&node.id, position).is_some() {
701 return Err(ModelError::DuplicateNodeId {
702 composition: composition.id.clone(),
703 node: node.id.clone(),
704 });
705 }
706 }
707 validate_ports(composition, &positions)?;
708 for node in &composition.nodes {
709 validate_node(
710 composition,
711 node,
712 by_source,
713 &by_id,
714 effect_resources,
715 canvas,
716 )?;
717 for input in node.op.inputs() {
718 if let Some(target) = input.node()
719 && !positions.contains_key(target)
720 {
721 return Err(ModelError::UnknownNodeInput {
722 composition: composition.id.clone(),
723 node: node.id.clone(),
724 input: input.name(),
725 target: target.clone(),
726 });
727 }
728 }
729 }
730 if let Some(output) = &composition.output
731 && !positions.contains_key(output)
732 {
733 return Err(ModelError::UnknownCompositionOutput {
734 composition: composition.id.clone(),
735 node: output.clone(),
736 });
737 }
738 if let Some(node) = cycle_member(composition, &positions) {
739 return Err(ModelError::CompositionCycle {
740 composition: composition.id.clone(),
741 node: node.clone(),
742 });
743 }
744 }
745 if let Some(composition) = reference_cycle_member(compositions, &by_id, by_source) {
746 return Err(ModelError::CompositionUsesItself {
747 composition: composition.clone(),
748 });
749 }
750 validate_expansion(compositions, &by_id, sources)
751}
752
753pub(crate) type InlinedNodes<'a> = std::collections::HashMap<&'a CompositionId, u64>;
756
757fn validate_expansion<'a>(
763 compositions: &'a [Composition],
764 by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
765 sources: &'a [crate::MediaSource],
766) -> Result<InlinedNodes<'a>, crate::ModelError> {
767 use std::collections::HashMap;
768 let composition_sources: HashMap<&SourceId, &CompositionId> = sources
769 .iter()
770 .filter_map(|source| Some((&source.id, source.composition.as_ref()?)))
771 .collect();
772 let draws = |node: &'a CompositionNode| -> Option<&'a CompositionId> {
774 match &node.op {
775 NodeOp::Group { composition, .. } => Some(composition),
776 NodeOp::Media { source_id, .. } => composition_sources.get(source_id).copied(),
777 _ => None,
778 }
779 .filter(|id| by_id.contains_key(id))
780 };
781 let mut walked: HashMap<&CompositionId, (usize, u64)> = HashMap::new();
783 let mut total = 0_u64;
784 for root in compositions {
785 let mut stack = vec![(root, false)];
786 while let Some((composition, expanded)) = stack.pop() {
787 if walked.contains_key(&composition.id) {
788 continue;
789 }
790 if !expanded {
791 stack.push((composition, true));
792 stack.extend(
793 composition
794 .nodes
795 .iter()
796 .filter_map(draws)
797 .filter(|id| !walked.contains_key(id))
798 .map(|id| (by_id[id], false)),
799 );
800 continue;
801 }
802 let mut depth = 1;
803 let mut nodes = 0_u64;
804 for node in &composition.nodes {
805 let (inner, count) = draws(node).map_or((0, 1), |id| walked[id]);
806 depth = depth.max(inner + 1);
807 nodes = nodes.saturating_add(count);
808 }
809 walked.insert(&composition.id, (depth, nodes));
810 }
811 let (depth, nodes) = walked[&root.id];
812 if depth > MAX_COMPOSITION_DEPTH {
813 return Err(crate::ModelError::CompositionNestedTooDeep {
814 composition: root.id.clone(),
815 maximum: MAX_COMPOSITION_DEPTH,
816 });
817 }
818 total = total.saturating_add(nodes);
819 if total > MAX_INLINED_NODES as u64 {
820 return Err(crate::ModelError::TooManyInlinedNodes {
821 maximum: MAX_INLINED_NODES,
822 });
823 }
824 }
825 Ok(walked
826 .into_iter()
827 .map(|(id, (_, nodes))| (id, nodes))
828 .collect())
829}
830
831fn validate_ports(
834 composition: &Composition,
835 positions: &std::collections::HashMap<&NodeId, usize>,
836) -> Result<(), crate::ModelError> {
837 use std::collections::HashSet;
838 let invalid = || crate::ModelError::InvalidCompositionPorts {
839 composition: composition.id.clone(),
840 };
841 let mut names = HashSet::new();
842 if composition.inputs.len() > MAX_GROUP_INPUTS
843 || composition
844 .inputs
845 .iter()
846 .any(|name| name.is_empty() || !names.insert(name))
847 {
848 return Err(invalid());
849 }
850 let mut controls = HashSet::new();
851 let mut settings = HashSet::new();
852 if composition.controls.len() > MAX_COMPOSITION_CONTROLS {
853 return Err(invalid());
854 }
855 for control in &composition.controls {
856 let node = positions
857 .get(&control.node)
858 .map(|position| &composition.nodes[*position]);
859 if control.name.is_empty()
860 || control.param.is_empty()
861 || !controls.insert(&control.name)
862 || !settings.insert((&control.node, &control.param))
863 || node.is_none_or(|node| node.op.effect().is_none())
864 {
865 return Err(invalid());
866 }
867 }
868 Ok(())
869}
870
871fn reference_cycle_member<'a>(
875 compositions: &'a [Composition],
876 by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
877 sources: &std::collections::HashMap<&SourceId, &'a crate::MediaSource>,
878) -> Option<&'a CompositionId> {
879 #[derive(Clone, Copy, PartialEq)]
880 enum Mark {
881 New,
882 Open,
883 Done,
884 }
885 let uses = |composition: &'a Composition| -> Vec<&'a CompositionId> {
886 composition
887 .nodes
888 .iter()
889 .filter_map(|node| match &node.op {
890 NodeOp::Group { composition, .. } => Some(composition),
891 NodeOp::Media { source_id, .. } => sources
892 .get(source_id)
893 .and_then(|source| source.composition.as_ref()),
894 _ => None,
895 })
896 .filter(|id| by_id.contains_key(id))
897 .collect()
898 };
899 let mut marks: std::collections::HashMap<&CompositionId, Mark> = compositions
900 .iter()
901 .map(|composition| (&composition.id, Mark::New))
902 .collect();
903 for root in compositions {
904 if marks[&root.id] != Mark::New {
905 continue;
906 }
907 let mut stack = vec![(&root.id, uses(root), 0_usize)];
908 marks.insert(&root.id, Mark::Open);
909 while let Some((current, next, at)) = stack.last_mut() {
910 let Some(target) = next.get(*at).copied() else {
911 marks.insert(current, Mark::Done);
912 stack.pop();
913 continue;
914 };
915 *at += 1;
916 match marks[target] {
917 Mark::Open => return Some(target),
918 Mark::New => {
919 marks.insert(target, Mark::Open);
920 stack.push((target, uses(by_id[target]), 0));
921 }
922 Mark::Done => {}
923 }
924 }
925 }
926 None
927}
928
929fn shape_error(
931 composition: &Composition,
932 node: &CompositionNode,
933 issue: crate::ShapeIssue,
934) -> crate::ModelError {
935 crate::ModelError::InvalidShape {
936 owner: crate::ShapeOwner::Node {
937 composition: composition.id.clone(),
938 node: node.id.clone(),
939 },
940 at: issue.at,
941 problem: issue.problem,
942 }
943}
944
945fn layout_error(
947 composition: &Composition,
948 node: &CompositionNode,
949 issue: crate::LayoutIssue,
950) -> crate::ModelError {
951 crate::ModelError::InvalidLayout {
952 owner: crate::ShapeOwner::Node {
953 composition: composition.id.clone(),
954 node: node.id.clone(),
955 },
956 at: issue.at,
957 problem: issue.problem,
958 }
959}
960
961fn validate_node(
962 composition: &Composition,
963 node: &CompositionNode,
964 sources: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
965 compositions: &std::collections::HashMap<&CompositionId, &Composition>,
966 effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
967 canvas: (u32, u32),
968) -> Result<(), crate::ModelError> {
969 use crate::ModelError;
970
971 let invalid = || ModelError::InvalidNode {
972 composition: composition.id.clone(),
973 node: node.id.clone(),
974 };
975 let unit = |animation: &Animated<f32>| {
976 crate::animation::valid_scalar_values(animation, |value| (0.0..=1.0).contains(&value))
977 };
978 let shape_error = |issue| shape_error(composition, node, issue);
979 if node.bypass && !node.op.has_inputs() {
980 return Err(invalid());
981 }
982 let valid = match &node.op {
983 NodeOp::Clip {} | NodeOp::Matte { .. } => true,
984 NodeOp::Media { source_id, start } => {
985 let visual = sources.get(source_id).is_some_and(|source| {
986 matches!(
987 source.kind,
988 crate::MediaKind::Video
989 | crate::MediaKind::Image
990 | crate::MediaKind::Generated
991 | crate::MediaKind::Composition
992 )
993 });
994 if !visual {
995 return Err(ModelError::InvalidNodeSource {
996 composition: composition.id.clone(),
997 node: node.id.clone(),
998 source: source_id.clone(),
999 });
1000 }
1001 start.ticks() >= 0
1002 }
1003 NodeOp::Generator { generator } => match generator.validate_on(canvas.0, canvas.1) {
1004 Ok(()) => true,
1005 Err(crate::GeneratorIssue::Shape(issue)) => return Err(shape_error(issue)),
1006 Err(crate::GeneratorIssue::Layout(issue)) => {
1007 return Err(layout_error(composition, node, issue));
1008 }
1009 Err(crate::GeneratorIssue::Invalid) => false,
1010 },
1011 NodeOp::Effect { effect, inputs, .. } => {
1012 effect.is_valid()
1013 && inputs.len() <= MAX_EFFECT_INPUTS
1014 && inputs
1015 .keys()
1016 .all(|name| crate::effect::valid_binding_name(name))
1017 }
1018 NodeOp::Transition {
1019 effect, progress, ..
1020 } => effect.is_valid() && unit(progress),
1021 NodeOp::Transform { transform, .. } => transform.validate(),
1022 NodeOp::Merge { layers } => {
1023 layers.len() <= MAX_MERGE_LAYERS && layers.iter().all(|layer| unit(&layer.opacity))
1024 }
1025 NodeOp::ShapeMask { masks, .. } => {
1026 crate::mask::validate_masks(masks).map_err(shape_error)?;
1027 masks.len() <= crate::MAX_CLIP_MASKS
1028 }
1029 NodeOp::Group {
1030 composition: target,
1031 inputs,
1032 controls,
1033 } => {
1034 let Some(target) = compositions.get(target) else {
1035 return Err(ModelError::UnknownGroupComposition {
1036 composition: composition.id.clone(),
1037 node: node.id.clone(),
1038 target: target.clone(),
1039 });
1040 };
1041 inputs.len() == target.inputs.len()
1042 && controls.iter().all(|(name, value)| {
1043 target.controls.iter().any(|control| &control.name == name)
1044 && crate::effect::valid_param(value)
1045 })
1046 }
1047 NodeOp::GroupInput { index } => {
1048 usize::try_from(*index).is_ok_and(|index| index < composition.inputs.len())
1049 }
1050 };
1051 if !valid {
1052 return Err(invalid());
1053 }
1054 if let Some(effect) = node.op.effect()
1055 && let Some(resource) = effect
1056 .resources
1057 .values()
1058 .find(|resource| !effect_resources.contains(resource))
1059 {
1060 return Err(ModelError::UnknownNodeResource {
1061 composition: composition.id.clone(),
1062 node: node.id.clone(),
1063 resource: resource.clone(),
1064 });
1065 }
1066 Ok(())
1067}
1068
1069fn cycle_member<'a>(
1072 composition: &'a Composition,
1073 positions: &std::collections::HashMap<&'a NodeId, usize>,
1074) -> Option<&'a NodeId> {
1075 #[derive(Clone, Copy, PartialEq)]
1076 enum Mark {
1077 New,
1078 Open,
1079 Done,
1080 }
1081 let nodes = &composition.nodes;
1082 let mut marks = vec![Mark::New; nodes.len()];
1083 for root in 0..nodes.len() {
1084 if marks[root] != Mark::New {
1085 continue;
1086 }
1087 let mut stack = vec![(root, 0_usize)];
1089 marks[root] = Mark::Open;
1090 while let Some((current, next)) = stack.last_mut() {
1091 let inputs = nodes[*current].op.inputs();
1092 let Some(input) = inputs.get(*next) else {
1093 marks[*current] = Mark::Done;
1094 stack.pop();
1095 continue;
1096 };
1097 *next += 1;
1098 let Some(target) = input.node().and_then(|id| positions.get(id)) else {
1099 continue;
1100 };
1101 match marks[*target] {
1102 Mark::Open => return Some(&nodes[*target].id),
1103 Mark::New => {
1104 marks[*target] = Mark::Open;
1105 stack.push((*target, 0));
1106 }
1107 Mark::Done => {}
1108 }
1109 }
1110 }
1111 None
1112}
1113
1114#[cfg(test)]
1115#[path = "composition_tests.rs"]
1116mod tests;