Skip to main content

alpha2_model/
error.rs

1use crate::{ClipId, CompositionId, EffectResourceId, NodeId, TrackId};
2use alpha2_media::SourceId;
3use std::fmt;
4
5/// What holds the shape or layout a [`ModelError::InvalidShape`] or
6/// [`ModelError::InvalidLayout`] names.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ShapeOwner {
9    /// A generated source's shapes or layout generator.
10    Source(SourceId),
11    /// A clip's masks.
12    Clip(ClipId),
13    /// A composition node's shapes or layout generator, or its masks.
14    Node {
15        /// The composition.
16        composition: CompositionId,
17        /// The node.
18        node: NodeId,
19    },
20}
21
22impl fmt::Display for ShapeOwner {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Source(source) => write!(formatter, "source {source}"),
26            Self::Clip(clip) => write!(formatter, "clip {clip}"),
27            Self::Node { composition, node } => {
28                write!(formatter, "composition {composition} node {node}")
29            }
30        }
31    }
32}
33
34/// A document invariant violation.
35#[derive(Clone, Debug, Eq, PartialEq)]
36#[non_exhaustive]
37pub enum ModelError {
38    /// This build cannot interpret the document version.
39    UnsupportedSchemaVersion {
40        /// Version found in the decoded document.
41        found: u32,
42    },
43    /// At least one ID was empty.
44    EmptyIdentifier,
45    /// Two sources share one ID.
46    DuplicateSourceId(SourceId),
47    /// Two effect resources share one ID.
48    DuplicateEffectResourceId(EffectResourceId),
49    /// Two tracks share one ID.
50    DuplicateTrackId(TrackId),
51    /// Two clips share one ID.
52    DuplicateClipId(ClipId),
53    /// A source locator was empty.
54    EmptyLocator {
55        /// Source whose locator was empty.
56        source: SourceId,
57    },
58    /// A still image source says it has audio, which no image holds.
59    AudioOnImage {
60        /// The image source.
61        source: SourceId,
62    },
63    /// A source's provider does not fit it (wrong source kind, audio, or incoherent settings),
64    /// or a generated source has one.
65    InvalidProvider {
66        /// The source.
67        source: SourceId,
68    },
69    /// A source's fingerprint is malformed, or a generated source has one.
70    InvalidFingerprint {
71        /// The source.
72        source: SourceId,
73    },
74    /// A source declares a color whose dynamic range contradicts its transfer (HDR is exactly a
75    /// PQ or HLG transfer).
76    ContradictoryColor {
77        /// The source.
78        source: SourceId,
79    },
80    /// A generated source has no valid generator, or has a locator, color metadata or audio; or
81    /// another source has a generator.
82    InvalidGenerator {
83        /// The source.
84        source: SourceId,
85    },
86    /// A shapes content tree or mask outline breaks a limit, has a value out of range, or has
87    /// keyframes that cannot interpolate.
88    InvalidShape {
89        /// What holds the shape.
90        owner: ShapeOwner,
91        /// Where the problem lies, like `items[2].items[0].path` or `masks[1].shape.size`.
92        at: String,
93        /// What is wrong there.
94        problem: crate::ShapeProblem,
95    },
96    /// A layout tree breaks a limit or structure rule, has a value out of range, or has
97    /// keyframes that cannot interpolate.
98    InvalidLayout {
99        /// What holds the layout.
100        owner: ShapeOwner,
101        /// Where in it the problem lies, like `root.children[2].padding`.
102        at: String,
103        /// What is wrong there.
104        problem: crate::LayoutProblem,
105    },
106    /// An effect resource locator was empty.
107    EmptyEffectResourceLocator {
108        /// Resource whose locator was empty.
109        resource: EffectResourceId,
110    },
111    /// A clip effect references an absent effect resource.
112    UnknownEffectResource {
113        /// Clip containing the reference.
114        clip: ClipId,
115        /// Missing resource identity.
116        resource: EffectResourceId,
117    },
118    /// A clip has an empty source or timeline range.
119    EmptyClipRange {
120        /// Clip with an empty range.
121        clip: ClipId,
122    },
123    /// A clip starts before the project or source timeline origin.
124    NegativeClipRange {
125        /// Clip with a negative range start.
126        clip: ClipId,
127    },
128    /// Two clips overlap without one transition covering their intersection.
129    OverlappingClips {
130        /// Track containing the overlap.
131        track: TrackId,
132        /// First overlapping clip.
133        first: ClipId,
134        /// Second overlapping clip.
135        second: ClipId,
136    },
137    /// A clip refers to an absent source.
138    UnknownClipSource {
139        /// Clip containing the absent source reference.
140        clip: ClipId,
141        /// Referenced source that was not in the project.
142        source: SourceId,
143    },
144    /// A source cannot be placed on the selected track kind.
145    IncompatibleClipSource {
146        /// Clip containing the incompatible reference.
147        clip: ClipId,
148        /// Track that cannot accept the source kind.
149        track: TrackId,
150        /// Source with the incompatible kind.
151        source: SourceId,
152    },
153    /// A visual clip has non-finite values, invalid opacity, or unordered keyframes.
154    InvalidVisualProperties {
155        /// Clip containing the invalid presentation data.
156        clip: ClipId,
157    },
158    /// A clip has an invalid gain, pan, fade, or audio effect parameter.
159    InvalidAudioProperties {
160        /// Clip containing the invalid audio data.
161        clip: ClipId,
162    },
163    /// A track bus has an invalid gain, pan, effect, or ducking parameter.
164    InvalidTrackAudio {
165        /// Track containing the invalid bus data.
166        track: TrackId,
167    },
168    /// The project master bus has an invalid gain, effect, or limiter parameter.
169    InvalidMasterAudio,
170    /// A ducking sidechain names a missing track, itself, or forms a cycle.
171    InvalidDuckingSidechain {
172        /// Track whose ducking reference is unusable.
173        track: TrackId,
174        /// Referenced sidechain track.
175        sidechain: TrackId,
176    },
177    /// A clip names an absent composition.
178    UnknownComposition {
179        /// The clip.
180        clip: ClipId,
181        /// The composition it names.
182        composition: CompositionId,
183    },
184    /// A clip's composition has a non-positive scale, reaches a composition time past
185    /// [`alpha2_time::MAX_SERIALIZED_TICKS`] before the clip ends, or is on an audio clip.
186    InvalidClipComposition {
187        /// The clip.
188        clip: ClipId,
189    },
190    /// Two compositions share one ID.
191    DuplicateCompositionId(CompositionId),
192    /// A node ID contains `/`, which is reserved for joining the IDs of inlined group and media
193    /// nodes.
194    InvalidNodeId {
195        /// The composition.
196        composition: CompositionId,
197        /// The node ID.
198        node: NodeId,
199    },
200    /// Two nodes of one composition share an ID.
201    DuplicateNodeId {
202        /// The composition.
203        composition: CompositionId,
204        /// The repeated node ID.
205        node: NodeId,
206    },
207    /// A composition has more nodes than the renderer will schedule.
208    TooManyCompositionNodes {
209        /// The composition.
210        composition: CompositionId,
211        /// Number supplied by the document.
212        found: usize,
213        /// Largest supported count.
214        maximum: usize,
215    },
216    /// A node has an invalid setting: effect identity, version, parameter or value; transform,
217    /// progress, opacity or mask out of range; too many merge layers or masks; or a bypass on a
218    /// node without inputs.
219    InvalidNode {
220        /// The composition.
221        composition: CompositionId,
222        /// The node.
223        node: NodeId,
224    },
225    /// A node input names a node its composition does not have.
226    UnknownNodeInput {
227        /// The composition.
228        composition: CompositionId,
229        /// The node whose input it is.
230        node: NodeId,
231        /// The input: `input`, `from`, `to`, `matte`, `layers[i]`, or `inputs[i]`.
232        input: String,
233        /// The node named.
234        target: NodeId,
235    },
236    /// A node depends on its own output.
237    CompositionCycle {
238        /// The composition.
239        composition: CompositionId,
240        /// A node on the cycle.
241        node: NodeId,
242    },
243    /// A composition's output names a node it does not have.
244    UnknownCompositionOutput {
245        /// The composition.
246        composition: CompositionId,
247        /// The node named.
248        node: NodeId,
249    },
250    /// A media node names an absent source or one that is not visual.
251    InvalidNodeSource {
252        /// The composition.
253        composition: CompositionId,
254        /// The node.
255        node: NodeId,
256        /// The source named.
257        source: SourceId,
258    },
259    /// A composition's group inputs are unnamed, repeated or too many, or a control names no
260    /// effect setting of its nodes or repeats one.
261    InvalidCompositionPorts {
262        /// The composition.
263        composition: CompositionId,
264    },
265    /// A group node names an absent composition.
266    UnknownGroupComposition {
267        /// The composition holding the group node.
268        composition: CompositionId,
269        /// The group node.
270        node: NodeId,
271        /// The composition it names.
272        target: CompositionId,
273    },
274    /// A composition uses itself through group nodes or composition sources.
275    CompositionUsesItself {
276        /// A composition on the loop.
277        composition: CompositionId,
278    },
279    /// Compositions nest, through group nodes or composition sources, more than
280    /// [`crate::MAX_COMPOSITION_DEPTH`] deep.
281    CompositionNestedTooDeep {
282        /// The first such composition, in document order.
283        composition: CompositionId,
284        /// Deepest supported nesting.
285        maximum: usize,
286    },
287    /// The compositions draw more than [`crate::MAX_INLINED_NODES`] nodes, counting each group
288    /// node and composition-source media node as the nodes it draws.
289    TooManyInlinedNodes {
290        /// Largest supported count.
291        maximum: usize,
292    },
293    /// The clips at one instant draw more than [`crate::MAX_FRAME_INLINED_NODES`] composition
294    /// nodes together.
295    FrameDrawsTooManyNodes {
296        /// The first clip, in document order, starting at the first such instant.
297        clip: ClipId,
298        /// The nodes the clips at that instant draw.
299        found: u64,
300        /// Largest supported count.
301        maximum: usize,
302    },
303    /// A composition source names an absent composition, or has what only a host-supplied
304    /// source has.
305    InvalidCompositionSource {
306        /// The source.
307        source: SourceId,
308    },
309    /// A node's effect references an absent effect resource.
310    UnknownNodeResource {
311        /// The composition.
312        composition: CompositionId,
313        /// The node.
314        node: NodeId,
315        /// The resource named.
316        resource: EffectResourceId,
317    },
318    /// A clip has more than [`crate::MAX_CLIP_MASKS`] masks, a mask with a negative or
319    /// non-finite value, or masks on an audio track.
320    InvalidClipMask {
321        /// Clip carrying the invalid mask.
322        clip: ClipId,
323    },
324    /// A caption track has no captions, has clips or transitions, or has invalid cues or style;
325    /// or another track has captions.
326    InvalidCaptions {
327        /// The track.
328        track: TrackId,
329    },
330    /// A caption cue names a style its track does not have.
331    UnknownCaptionStyle {
332        /// The track.
333        track: TrackId,
334        /// The cue's position among the track's cues.
335        cue: usize,
336        /// The style it names.
337        name: String,
338    },
339    /// A caption cue's style (its track's with the cue's patch applied) is out of range.
340    InvalidCaptionCueStyle {
341        /// The track.
342        track: TrackId,
343        /// The cue's position among the track's cues.
344        cue: usize,
345    },
346    /// A clip freezes on an instant outside its source range.
347    InvalidTimeMap {
348        /// The clip.
349        clip: ClipId,
350    },
351    /// A clip's matte names no visual clip, itself, a clip with its own matte, or a clip in a
352    /// transition; or an audio clip has a matte.
353    InvalidMatte {
354        /// Clip whose matte is invalid.
355        clip: ClipId,
356    },
357    /// A transition is not contained in two distinct visual clips on its track.
358    InvalidClipTransition {
359        /// Track containing the invalid transition.
360        track: TrackId,
361    },
362    /// Transitions active at one instant lead from clip to clip in a circle, leaving no clip to
363    /// show.
364    TransitionCycle {
365        /// Track containing the transitions.
366        track: TrackId,
367        /// An instant they are all active.
368        at: alpha2_time::MediaTime,
369    },
370    /// Two clips have more than one transition between them, in either direction.
371    DuplicateTransition {
372        /// Track containing the transitions.
373        track: TrackId,
374        /// The transition's first clip.
375        first: ClipId,
376        /// The transition's second clip.
377        second: ClipId,
378    },
379    /// The project contains too many compositions.
380    TooManyCompositions {
381        /// Number supplied by the document.
382        found: usize,
383        /// Largest supported count.
384        maximum: usize,
385    },
386    /// The source table exceeds the bounded project resource budget.
387    TooManySources {
388        /// Number supplied by the document.
389        found: usize,
390        /// Largest supported count.
391        maximum: usize,
392    },
393    /// The timeline contains too many tracks.
394    TooManyTracks {
395        /// Number supplied by the document.
396        found: usize,
397        /// Largest supported count.
398        maximum: usize,
399    },
400    /// The timeline contains too many clips in total.
401    TooManyClips {
402        /// Number supplied by the document.
403        found: usize,
404        /// Largest supported count.
405        maximum: usize,
406    },
407    /// More audio effects hold state at once than [`crate::MAX_AUDIO_EFFECTS`] (see
408    /// [`crate::Project::audio_effect_load`]).
409    TooManyAudioEffects {
410        /// The most effects holding state at once.
411        found: usize,
412        /// Largest supported count.
413        maximum: usize,
414        /// The earliest timeline position, in ticks, at which `found` hold state.
415        at_ticks: i64,
416    },
417    /// The audio effects hold more than [`crate::MAX_AUDIO_DELAY_MS`] of delay at once (see
418    /// [`crate::Project::audio_effect_load`]).
419    TooMuchAudioDelay {
420        /// The most milliseconds held at once, rounded up.
421        found_ms: u64,
422        /// Largest supported total.
423        maximum_ms: u32,
424        /// The earliest timeline position, in ticks, at which `found_ms` is held.
425        at_ticks: i64,
426    },
427    /// One track contains too many clips.
428    TooManyClipsOnTrack {
429        /// Track containing the excessive clip list.
430        track: TrackId,
431        /// Number supplied by the document.
432        found: usize,
433        /// Largest supported count.
434        maximum: usize,
435    },
436    /// A caption track holds more than [`crate::MAX_CLIPS_PER_TRACK`] cues.
437    TooManyCaptionCues {
438        /// The caption track.
439        track: TrackId,
440        /// Number supplied by the document.
441        found: usize,
442        /// Largest supported count.
443        maximum: usize,
444    },
445    /// One track contains too many transitions.
446    TooManyTransitions {
447        /// Track containing the excessive transition list.
448        track: TrackId,
449        /// Number supplied by the document.
450        found: usize,
451        /// Largest supported count.
452        maximum: usize,
453    },
454    /// Width or height was zero.
455    ZeroCanvasDimension,
456    /// A canvas edge exceeds the portable GPU-independent limit.
457    CanvasDimensionTooLarge {
458        /// Supplied width.
459        width: u32,
460        /// Supplied height.
461        height: u32,
462        /// Largest supported edge.
463        maximum: u32,
464    },
465    /// The canvas area exceeds the bounded pixel budget.
466    CanvasPixelBudgetExceeded {
467        /// Supplied pixel area.
468        pixels: u64,
469        /// Largest supported pixel area.
470        maximum: u64,
471    },
472    /// A background component is non-finite or outside `0..=1`.
473    InvalidBackground,
474    /// The color pipeline's HDR peak is outside [`crate::HDR_PEAK_NITS_RANGE`].
475    InvalidColorPipeline,
476    /// The project sample rate is outside the supported DSP interval.
477    UnsupportedSampleRate {
478        /// Supplied rate.
479        found: u32,
480        /// Smallest supported rate.
481        minimum: u32,
482        /// Largest supported rate.
483        maximum: u32,
484    },
485}
486
487impl fmt::Display for ModelError {
488    #[allow(clippy::too_many_lines)]
489    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
490        match self {
491            Self::UnsupportedSchemaVersion { found } => {
492                write!(formatter, "unsupported schema version {found}")
493            }
494            Self::EmptyIdentifier => formatter.write_str("document contains an empty identifier"),
495            Self::DuplicateSourceId(id) => write!(formatter, "duplicate source ID {id}"),
496            Self::DuplicateEffectResourceId(id) => {
497                write!(formatter, "duplicate effect resource ID {id}")
498            }
499            Self::DuplicateTrackId(id) => write!(formatter, "duplicate track ID {id}"),
500            Self::DuplicateClipId(id) => write!(formatter, "duplicate clip ID {id}"),
501            Self::EmptyLocator { source } => {
502                write!(formatter, "source {source} has an empty locator")
503            }
504            Self::AudioOnImage { source } => {
505                write!(
506                    formatter,
507                    "source {source} is a still image and has no audio"
508                )
509            }
510            Self::InvalidGenerator { source } => {
511                write!(formatter, "source {source} has an invalid generator")
512            }
513            Self::InvalidShape { owner, at, problem } => {
514                write!(formatter, "{owner} has an invalid shape: {at} {problem}")
515            }
516            Self::InvalidLayout { owner, at, problem } => {
517                write!(formatter, "{owner} has an invalid layout: {at} {problem}")
518            }
519            Self::ContradictoryColor { source } => write!(
520                formatter,
521                "source {source} declares a dynamic range its transfer contradicts"
522            ),
523            Self::InvalidFingerprint { source } => {
524                write!(formatter, "source {source} has an invalid fingerprint")
525            }
526            Self::InvalidProvider { source } => {
527                write!(formatter, "source {source} has an invalid provider")
528            }
529            Self::EmptyEffectResourceLocator { resource } => {
530                write!(formatter, "effect resource {resource} has an empty locator")
531            }
532            Self::UnknownEffectResource { clip, resource } => {
533                write!(
534                    formatter,
535                    "clip {clip} references unknown effect resource {resource}"
536                )
537            }
538            Self::EmptyClipRange { clip } => write!(formatter, "clip {clip} has an empty range"),
539            Self::NegativeClipRange { clip } => {
540                write!(formatter, "clip {clip} has a range before timeline origin")
541            }
542            Self::OverlappingClips {
543                track,
544                first,
545                second,
546            } => write!(
547                formatter,
548                "clips {first} and {second} overlap without a covering transition on track {track}"
549            ),
550            Self::UnknownClipSource { clip, source } => {
551                write!(formatter, "clip {clip} references unknown source {source}")
552            }
553            Self::IncompatibleClipSource {
554                clip,
555                track,
556                source,
557            } => {
558                write!(
559                    formatter,
560                    "clip {clip} cannot use source {source} on track {track}"
561                )
562            }
563            Self::InvalidVisualProperties { clip } => {
564                write!(formatter, "clip {clip} has invalid visual properties")
565            }
566            Self::InvalidAudioProperties { clip } => {
567                write!(formatter, "clip {clip} has invalid audio properties")
568            }
569            Self::InvalidTrackAudio { track } => {
570                write!(formatter, "track {track} has invalid audio bus settings")
571            }
572            Self::InvalidMasterAudio => {
573                write!(formatter, "the project master bus is invalid")
574            }
575            Self::InvalidDuckingSidechain { track, sidechain } => write!(
576                formatter,
577                "track {track} cannot duck from sidechain track {sidechain}"
578            ),
579            Self::UnknownComposition { clip, composition } => {
580                write!(
581                    formatter,
582                    "clip {clip} references unknown composition {composition}"
583                )
584            }
585            Self::InvalidClipComposition { clip } => {
586                write!(
587                    formatter,
588                    "clip {clip} has an invalid composition reference"
589                )
590            }
591            Self::DuplicateCompositionId(id) => write!(formatter, "duplicate composition ID {id}"),
592            Self::InvalidNodeId { composition, node } => {
593                write!(
594                    formatter,
595                    "composition {composition} has node ID {node}, which has a `/`"
596                )
597            }
598            Self::DuplicateNodeId { composition, node } => {
599                write!(
600                    formatter,
601                    "composition {composition} has duplicate node ID {node}"
602                )
603            }
604            Self::TooManyCompositionNodes {
605                composition,
606                found,
607                maximum,
608            } => write!(
609                formatter,
610                "composition {composition} has {found} nodes; the maximum is {maximum}"
611            ),
612            Self::InvalidNode { composition, node } => {
613                write!(
614                    formatter,
615                    "node {node} of composition {composition} has an invalid setting"
616                )
617            }
618            Self::UnknownNodeInput {
619                composition,
620                node,
621                input,
622                target,
623            } => write!(
624                formatter,
625                "input {input} of node {node} in composition {composition} names unknown node {target}"
626            ),
627            Self::CompositionCycle { composition, node } => write!(
628                formatter,
629                "node {node} of composition {composition} depends on its own output"
630            ),
631            Self::UnknownCompositionOutput { composition, node } => write!(
632                formatter,
633                "composition {composition} outputs unknown node {node}"
634            ),
635            Self::InvalidNodeSource {
636                composition,
637                node,
638                source,
639            } => write!(
640                formatter,
641                "node {node} of composition {composition} cannot show source {source}"
642            ),
643            Self::InvalidCompositionPorts { composition } => write!(
644                formatter,
645                "composition {composition} has invalid group inputs or controls"
646            ),
647            Self::UnknownGroupComposition {
648                composition,
649                node,
650                target,
651            } => write!(
652                formatter,
653                "group node {node} of composition {composition} names unknown composition {target}"
654            ),
655            Self::CompositionUsesItself { composition } => {
656                write!(formatter, "composition {composition} uses itself")
657            }
658            Self::CompositionNestedTooDeep {
659                composition,
660                maximum,
661            } => write!(
662                formatter,
663                "composition {composition} nests compositions more than {maximum} deep"
664            ),
665            Self::TooManyInlinedNodes { maximum } => write!(
666                formatter,
667                "compositions draw more than {maximum} nodes, counting each group and composition source as the nodes it draws"
668            ),
669            Self::FrameDrawsTooManyNodes {
670                clip,
671                found,
672                maximum,
673            } => write!(
674                formatter,
675                "the clips on the timeline where clip {clip} starts draw {found} composition nodes; a frame draws at most {maximum}"
676            ),
677            Self::InvalidCompositionSource { source } => {
678                write!(
679                    formatter,
680                    "source {source} is not a valid composition source"
681                )
682            }
683            Self::UnknownNodeResource {
684                composition,
685                node,
686                resource,
687            } => write!(
688                formatter,
689                "node {node} of composition {composition} references unknown effect resource {resource}"
690            ),
691            Self::InvalidClipMask { clip } => write!(formatter, "clip {clip} has an invalid mask"),
692            Self::InvalidMatte { clip } => write!(formatter, "clip {clip} has an invalid matte"),
693            Self::InvalidCaptions { track } => {
694                write!(formatter, "track {track} has invalid captions")
695            }
696            Self::UnknownCaptionStyle { track, cue, name } => write!(
697                formatter,
698                "cue {cue} of track {track} names style {name:?}, which the track does not have"
699            ),
700            Self::InvalidCaptionCueStyle { track, cue } => write!(
701                formatter,
702                "cue {cue} of track {track} has a style out of range once its patch is applied"
703            ),
704            Self::InvalidTimeMap { clip } => {
705                write!(formatter, "clip {clip} freezes outside its source range")
706            }
707            Self::InvalidClipTransition { track } => {
708                write!(formatter, "track {track} has an invalid clip transition")
709            }
710            Self::TransitionCycle { track, at } => write!(
711                formatter,
712                "transitions on track {track} active at {} ticks lead from clip to clip in a circle",
713                at.ticks()
714            ),
715            Self::DuplicateTransition {
716                track,
717                first,
718                second,
719            } => write!(
720                formatter,
721                "clips {first} and {second} have more than one transition on track {track}"
722            ),
723            Self::TooManyCompositions { found, maximum } => write!(
724                formatter,
725                "project has {found} compositions; the maximum is {maximum}"
726            ),
727            Self::TooManySources { found, maximum } => {
728                write!(
729                    formatter,
730                    "project has {found} sources; the maximum is {maximum}"
731                )
732            }
733            Self::TooManyTracks { found, maximum } => {
734                write!(
735                    formatter,
736                    "project has {found} tracks; the maximum is {maximum}"
737                )
738            }
739            Self::TooManyClips { found, maximum } => {
740                write!(
741                    formatter,
742                    "project has {found} clips; the maximum is {maximum}"
743                )
744            }
745            Self::TooManyAudioEffects {
746                found,
747                maximum,
748                at_ticks,
749            } => write!(
750                formatter,
751                "project has {found} audio effects holding state at tick {at_ticks}; the maximum at once is {maximum}"
752            ),
753            Self::TooMuchAudioDelay {
754                found_ms,
755                maximum_ms,
756                at_ticks,
757            } => write!(
758                formatter,
759                "project audio effects hold {found_ms} ms of delay at tick {at_ticks}; the maximum at once is {maximum_ms} ms"
760            ),
761            Self::TooManyClipsOnTrack {
762                track,
763                found,
764                maximum,
765            } => write!(
766                formatter,
767                "track {track} has {found} clips; the maximum is {maximum}"
768            ),
769            Self::TooManyCaptionCues {
770                track,
771                found,
772                maximum,
773            } => write!(
774                formatter,
775                "track {track} has {found} caption cues; the maximum is {maximum}"
776            ),
777            Self::TooManyTransitions {
778                track,
779                found,
780                maximum,
781            } => write!(
782                formatter,
783                "track {track} has {found} transitions; the maximum is {maximum}"
784            ),
785            Self::ZeroCanvasDimension => formatter.write_str("canvas dimensions must be positive"),
786            Self::CanvasDimensionTooLarge {
787                width,
788                height,
789                maximum,
790            } => write!(
791                formatter,
792                "canvas {width}x{height} exceeds the maximum edge {maximum}"
793            ),
794            Self::CanvasPixelBudgetExceeded { pixels, maximum } => write!(
795                formatter,
796                "canvas has {pixels} pixels; the maximum is {maximum}"
797            ),
798            Self::InvalidBackground => {
799                formatter.write_str("background color components must be finite and within 0..=1")
800            }
801            Self::InvalidColorPipeline => {
802                formatter.write_str("the HDR peak must be within 100..=10000 nits")
803            }
804            Self::UnsupportedSampleRate {
805                found,
806                minimum,
807                maximum,
808            } => write!(
809                formatter,
810                "sample rate {found} is outside {minimum}..={maximum}"
811            ),
812        }
813    }
814}
815
816impl std::error::Error for ModelError {}
817
818/// Failure while decoding a serialized project document.
819#[derive(Debug)]
820#[non_exhaustive]
821pub enum ProjectLoadError {
822    /// The JSON input exceeded the parser byte budget.
823    TooLarge {
824        /// Supplied UTF-8 byte length.
825        bytes: usize,
826        /// Largest accepted byte length.
827        maximum: usize,
828    },
829    /// JSON syntax or shape was invalid.
830    Json(serde_json::Error),
831    /// The decoded document violated a model invariant.
832    InvalidProject(ModelError),
833}
834
835impl fmt::Display for ProjectLoadError {
836    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
837        match self {
838            Self::TooLarge { bytes, maximum } => {
839                write!(
840                    formatter,
841                    "project JSON is {bytes} bytes; the maximum is {maximum}"
842                )
843            }
844            Self::Json(error) => write!(formatter, "invalid project JSON: {error}"),
845            Self::InvalidProject(error) => write!(formatter, "invalid project: {error}"),
846        }
847    }
848}
849
850impl std::error::Error for ProjectLoadError {
851    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
852        match self {
853            Self::TooLarge { .. } => None,
854            Self::Json(error) => Some(error),
855            Self::InvalidProject(error) => Some(error),
856        }
857    }
858}