Skip to main content

animsmith_core/
source_timing.rs

1//! Exact, bounded source-timing evidence retained beside normalized source facts.
2//!
3//! This contract is intentionally in-memory only. It does not widen the V1 raw-source
4//! facts or prediction wire contracts, and it never treats floating-point seconds or
5//! decimal frame rates as exact frame coordinates.
6
7use crate::{
8    RAW_SOURCE_V1_MAX_CLIPS, SourceLoaderDispositionV1, SourceProvenanceV1, SourceSetCoverageV1,
9};
10
11/// Semantic identity of the exact in-memory source-timing vocabulary.
12pub const EXACT_SOURCE_TIMING_V1_ID: &str = "urn:animsmith:exact-source-timing:1";
13
14/// Maximum retained source-clip timing rows.
15pub const EXACT_SOURCE_TIMING_V1_MAX_CLIPS: usize = RAW_SOURCE_V1_MAX_CLIPS;
16
17/// Why one exact source timing observation cannot be established.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub enum ExactSourceTimingUnavailableReasonV1 {
20    /// A source property or resolved span is structurally invalid.
21    Malformed,
22    /// The loader exposes a custom frame rate only through floating-point data.
23    CustomFrameRateNotExact,
24    /// The source timeline mode has no exact period rule in the loader projection.
25    UnsupportedTimeMode,
26    /// The source time basis cannot exactly represent the requested frame mode.
27    UnsupportedTimeBasis,
28    /// The parser did not make the required exact evidence available.
29    ParserUnavailable,
30}
31
32/// Availability of one exact source timing observation.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ExactSourceTimingObservationStateV1<T> {
35    /// The exact value was retained.
36    Observed(T),
37    /// Complete parser evidence proves that no declaration exists.
38    ProvenAbsent,
39    /// Exact evidence cannot be established.
40    Unavailable(ExactSourceTimingUnavailableReasonV1),
41}
42
43/// One exact source timing value with orthogonal provenance and loader treatment.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ExactSourceTimingObservationV1<T> {
46    state: ExactSourceTimingObservationStateV1<T>,
47    disposition: SourceLoaderDispositionV1,
48    provenance: Option<SourceProvenanceV1>,
49}
50
51impl<T> ExactSourceTimingObservationV1<T> {
52    /// Retain an exact observed value.
53    pub fn observed(
54        value: T,
55        provenance: SourceProvenanceV1,
56        disposition: SourceLoaderDispositionV1,
57    ) -> Self {
58        Self {
59            state: ExactSourceTimingObservationStateV1::Observed(value),
60            disposition,
61            provenance: Some(provenance),
62        }
63    }
64
65    /// Record proven source absence.
66    pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
67        Self {
68            state: ExactSourceTimingObservationStateV1::ProvenAbsent,
69            disposition: SourceLoaderDispositionV1::NotApplicable,
70            provenance: Some(provenance),
71        }
72    }
73
74    /// Record a typed exact-evidence failure.
75    pub fn unavailable(
76        reason: ExactSourceTimingUnavailableReasonV1,
77        provenance: Option<SourceProvenanceV1>,
78        disposition: SourceLoaderDispositionV1,
79    ) -> Self {
80        Self {
81            state: ExactSourceTimingObservationStateV1::Unavailable(reason),
82            disposition,
83            provenance,
84        }
85    }
86
87    /// Availability and exact value state.
88    pub const fn state(&self) -> &ExactSourceTimingObservationStateV1<T> {
89        &self.state
90    }
91
92    /// Loader treatment of this source domain.
93    pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
94        self.disposition
95    }
96
97    /// Source or parser provenance, when retained.
98    pub const fn provenance(&self) -> Option<&SourceProvenanceV1> {
99        self.provenance.as_ref()
100    }
101}
102
103/// Positive exact source-time units in one second.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
105pub struct ExactSourceTimeBasisV1(i64);
106
107impl ExactSourceTimeBasisV1 {
108    /// Validate an exact positive source-time basis.
109    pub fn new(units_per_second: i64) -> Result<Self, ExactSourceTimingContractError> {
110        if units_per_second <= 0 {
111            return Err(ExactSourceTimingContractError::InvalidTimeBasis);
112        }
113        Ok(Self(units_per_second))
114    }
115
116    /// Exact source-time units in one second.
117    pub const fn units_per_second(self) -> i64 {
118        self.0
119    }
120}
121
122/// Source timeline mode retained without converting it to decimal FPS.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
124pub enum SourceTimelineModeV1 {
125    /// Explicit source default mode, physically 30 fps.
126    Default,
127    /// 120 fps.
128    Fps120,
129    /// 100 fps.
130    Fps100,
131    /// 60 fps.
132    Fps60,
133    /// 50 fps.
134    Fps50,
135    /// 48 fps.
136    Fps48,
137    /// 30 fps.
138    Fps30,
139    /// 30 fps with drop-style display semantics.
140    Fps30Drop,
141    /// NTSC approximately 29.97 fps with drop-frame numbering.
142    NtscDropFrame,
143    /// NTSC approximately 29.97 fps with full-frame numbering.
144    NtscFullFrame,
145    /// PAL 25 fps.
146    Pal,
147    /// 24 fps.
148    Fps24,
149    /// 1000 fps.
150    Fps1000,
151    /// Film approximately 23.976 fps.
152    FilmFullFrame,
153    /// Source-defined custom frame rate.
154    Custom,
155    /// 96 fps.
156    Fps96,
157    /// 72 fps.
158    Fps72,
159    /// NTSC approximately 59.94 fps.
160    Fps59Dot94,
161}
162
163/// Source timecode display protocol.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
165pub enum SourceTimeDisplayProtocolV1 {
166    /// SMPTE timecode display.
167    Smpte,
168    /// Frame-count display.
169    FrameCount,
170    /// Parser-resolved default protocol marker.
171    Default,
172}
173
174/// Exact binary64 payload exposed by a loader for a finite positive custom frame rate.
175///
176/// This preserves the parser projection for evidence and diagnostics. It is not an
177/// exact rational rate and cannot authorize an integer frame lattice.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
179pub struct ParserFrameRateProjectionV1(u64);
180
181impl ParserFrameRateProjectionV1 {
182    /// Retain the exact finite positive binary64 payload.
183    pub fn new(value: f64) -> Result<Self, ExactSourceTimingContractError> {
184        if !value.is_finite() || value <= 0.0 {
185            return Err(ExactSourceTimingContractError::InvalidFrameRateProjection);
186        }
187        Ok(Self(value.to_bits()))
188    }
189
190    /// Exact IEEE-754 binary64 bit pattern exposed by the parser.
191    pub const fn binary64_bits(self) -> u64 {
192        self.0
193    }
194
195    /// Parser-projected floating-point value for display or diagnostics only.
196    pub const fn parser_value(self) -> f64 {
197        f64::from_bits(self.0)
198    }
199}
200
201/// Positive exact source-time units in one physical source frame.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
203pub struct ExactSourceFramePeriodV1(i64);
204
205impl ExactSourceFramePeriodV1 {
206    /// Validate a positive exact frame period.
207    pub fn new(units_per_frame: i64) -> Result<Self, ExactSourceTimingContractError> {
208        if units_per_frame <= 0 {
209            return Err(ExactSourceTimingContractError::InvalidFramePeriod);
210        }
211        Ok(Self(units_per_frame))
212    }
213
214    /// Exact source-time units in one physical source frame.
215    pub const fn units_per_frame(self) -> i64 {
216        self.0
217    }
218
219    /// Whether an absolute signed source-time coordinate lies on the frame lattice.
220    pub fn is_whole_frame(self, coordinate_units: i64) -> bool {
221        coordinate_units.rem_euclid(self.0) == 0
222    }
223
224    /// Exact signed frame index when the coordinate lies on the frame lattice.
225    pub fn frame_index(self, coordinate_units: i64) -> Option<i64> {
226        self.is_whole_frame(coordinate_units)
227            .then_some(coordinate_units / self.0)
228    }
229}
230
231/// Parser-selected source time-span property pair.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
233pub enum ExactSourceRangeSelectionV1 {
234    /// The loader's preferred complete pair.
235    Primary,
236    /// The loader's complete fallback pair.
237    Fallback,
238}
239
240/// Exact signed begin/end source-time coordinates for one animation clip.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct ExactSourceClipTimeRangeV1 {
243    selection: ExactSourceRangeSelectionV1,
244    begin_units: i64,
245    end_units: i64,
246}
247
248impl ExactSourceClipTimeRangeV1 {
249    /// Validate a parser-selected, ordered exact clip range.
250    pub fn new(
251        selection: ExactSourceRangeSelectionV1,
252        begin_units: i64,
253        end_units: i64,
254    ) -> Result<Self, ExactSourceTimingContractError> {
255        if begin_units > end_units {
256            return Err(ExactSourceTimingContractError::ReversedClipRange);
257        }
258        Ok(Self {
259            selection,
260            begin_units,
261            end_units,
262        })
263    }
264
265    /// Property pair selected by parser semantics.
266    pub const fn selection(self) -> ExactSourceRangeSelectionV1 {
267        self.selection
268    }
269
270    /// Exact signed source begin coordinate.
271    pub const fn begin_units(self) -> i64 {
272        self.begin_units
273    }
274
275    /// Exact signed source end coordinate.
276    pub const fn end_units(self) -> i64 {
277        self.end_units
278    }
279}
280
281/// Exact timing evidence for one retained source animation clip.
282#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct ExactSourceClipTimingV1 {
284    source_clip_index: usize,
285    source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
286}
287
288impl ExactSourceClipTimingV1 {
289    /// Construct one source-indexed exact clip row.
290    pub fn new(
291        source_clip_index: usize,
292        source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
293    ) -> Self {
294        Self {
295            source_clip_index,
296            source_time_range,
297        }
298    }
299
300    /// Stable zero-based source clip index.
301    pub const fn source_clip_index(&self) -> usize {
302        self.source_clip_index
303    }
304
305    /// Exact parser-selected source time range.
306    pub const fn source_time_range(
307        &self,
308    ) -> &ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1> {
309        &self.source_time_range
310    }
311}
312
313/// Bounded exact source timing evidence from one successful loader parse.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct ExactSourceTimingV1 {
316    time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
317    declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
318    effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
319    declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
320    frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
321    declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
322    effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
323    clip_coverage: SourceSetCoverageV1,
324    clips: Vec<ExactSourceClipTimingV1>,
325}
326
327impl ExactSourceTimingV1 {
328    /// Construct and validate one bounded exact timing projection.
329    #[allow(clippy::too_many_arguments)]
330    pub fn new(
331        time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
332        declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
333        effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
334        declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
335        frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
336        declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
337        effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
338        clip_coverage: SourceSetCoverageV1,
339        clips: Vec<ExactSourceClipTimingV1>,
340    ) -> Result<Self, ExactSourceTimingContractError> {
341        if clips.len() > EXACT_SOURCE_TIMING_V1_MAX_CLIPS {
342            return Err(ExactSourceTimingContractError::TooManyClips {
343                count: clips.len(),
344                limit: EXACT_SOURCE_TIMING_V1_MAX_CLIPS,
345            });
346        }
347        for (expected, clip) in clips.iter().enumerate() {
348            if clip.source_clip_index != expected {
349                return Err(ExactSourceTimingContractError::NonCanonicalClipIndex {
350                    expected,
351                    actual: clip.source_clip_index,
352                });
353            }
354        }
355        Ok(Self {
356            time_basis,
357            declared_time_mode,
358            effective_time_mode,
359            declared_custom_frame_rate,
360            frame_period,
361            declared_time_protocol,
362            effective_time_protocol,
363            clip_coverage,
364            clips,
365        })
366    }
367
368    /// Semantic identity of this in-memory contract.
369    pub const fn contract_id(&self) -> &'static str {
370        EXACT_SOURCE_TIMING_V1_ID
371    }
372
373    /// Exact parser-resolved source-time basis.
374    pub const fn time_basis(&self) -> &ExactSourceTimingObservationV1<ExactSourceTimeBasisV1> {
375        &self.time_basis
376    }
377
378    /// Raw source declaration state for the timeline mode.
379    pub const fn declared_time_mode(
380        &self,
381    ) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
382        &self.declared_time_mode
383    }
384
385    /// Loader-effective time mode, including parser fallback.
386    pub const fn effective_time_mode(
387        &self,
388    ) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
389        &self.effective_time_mode
390    }
391
392    /// Raw direct-property state for parser-projected `CustomFrameRate` binary64 evidence.
393    pub const fn declared_custom_frame_rate(
394        &self,
395    ) -> &ExactSourceTimingObservationV1<ParserFrameRateProjectionV1> {
396        &self.declared_custom_frame_rate
397    }
398
399    /// Exact integer physical frame period, when supported.
400    pub const fn frame_period(&self) -> &ExactSourceTimingObservationV1<ExactSourceFramePeriodV1> {
401        &self.frame_period
402    }
403
404    /// Raw source declaration state for the time-display protocol.
405    pub const fn declared_time_protocol(
406        &self,
407    ) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
408        &self.declared_time_protocol
409    }
410
411    /// Loader-effective time protocol, including parser fallback.
412    pub const fn effective_time_protocol(
413        &self,
414    ) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
415        &self.effective_time_protocol
416    }
417
418    /// Coverage of the independently enumerable source-clip domain.
419    pub const fn clip_coverage(&self) -> SourceSetCoverageV1 {
420        self.clip_coverage
421    }
422
423    /// Retained deterministic source-clip-prefix rows.
424    pub fn clips(&self) -> &[ExactSourceClipTimingV1] {
425        &self.clips
426    }
427}
428
429/// Invalid exact source timing value or attachment invariant.
430#[derive(Debug, thiserror::Error, PartialEq, Eq)]
431#[non_exhaustive]
432pub enum ExactSourceTimingContractError {
433    /// Source-time units per second must be positive.
434    #[error("source-time units per second must be positive")]
435    InvalidTimeBasis,
436    /// Source-time units per frame must be positive.
437    #[error("source-time units per frame must be positive")]
438    InvalidFramePeriod,
439    /// A retained frame-rate projection must be finite and positive.
440    #[error("parser frame-rate projection must be finite and positive")]
441    InvalidFrameRateProjection,
442    /// An observed clip range is reversed.
443    #[error("exact source clip range must satisfy begin_units <= end_units")]
444    ReversedClipRange,
445    /// The bounded clip limit was exceeded.
446    #[error("exact source timing has {count} clips, exceeding the limit of {limit}")]
447    TooManyClips {
448        /// Retained row count.
449        count: usize,
450        /// Public contract limit.
451        limit: usize,
452    },
453    /// Clip rows do not form a canonical zero-based prefix.
454    #[error("exact source clip index {actual} is not expected prefix index {expected}")]
455    NonCanonicalClipIndex {
456        /// Expected zero-based index.
457        expected: usize,
458        /// Actual retained index.
459        actual: usize,
460    },
461    /// Exact and raw clip prefixes have different lengths.
462    #[error(
463        "exact source clip count {exact} does not match retained raw clip count {source_count}"
464    )]
465    ClipCountMismatch {
466        /// Exact timing clip rows.
467        exact: usize,
468        /// Existing V1 source clip rows.
469        source_count: usize,
470    },
471    /// Exact and raw clip domains have different coverage.
472    #[error("exact source clip coverage does not match retained raw clip coverage")]
473    ClipCoverageMismatch,
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn whole_frame_predicate_is_signed_and_unit_exact() {
482        let period = ExactSourceFramePeriodV1::new(100).unwrap();
483        for coordinate in [-300, 0, 700] {
484            assert!(period.is_whole_frame(coordinate));
485        }
486        for coordinate in [-301, -299, 699, 701] {
487            assert!(!period.is_whole_frame(coordinate));
488        }
489        assert_eq!(period.frame_index(-300), Some(-3));
490        assert_eq!(period.frame_index(701), None);
491    }
492
493    #[test]
494    fn long_timeline_remains_unit_exact_beyond_binary64_integer_precision() {
495        let period = ExactSourceFramePeriodV1::new(4_708_704).unwrap();
496        let whole = period.units_per_frame() * 3_000_000_000;
497        assert!(whole > (1i64 << 53));
498        assert!(period.is_whole_frame(whole));
499        assert!(!period.is_whole_frame(whole - 1));
500        assert!(!period.is_whole_frame(whole + 1));
501        assert_eq!(period.frame_index(whole), Some(3_000_000_000));
502
503        assert_eq!(whole as f64, (whole - 1) as f64);
504        assert_eq!(whole as f64, (whole + 1) as f64);
505    }
506}