animsmith_core/
raw_animation_inventory.rs1use crate::bounded_deserialize::{CappedSequence, deserialize_capped_sequence};
8use crate::prediction::RawSourceSetCoverageV1;
9use crate::source_facts::SourceFactsViewV1;
10use crate::{InputIdentity, SourceFormatV1};
11use serde::de::Error as _;
12use serde::{Deserialize, Deserializer, Serialize};
13
14pub const RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID: &str =
16 "urn:animsmith:raw-animation-channel-inventory:1";
17pub const RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES: usize = 4_097;
19
20fn deserialize_rows<'de, D>(deserializer: D) -> Result<Vec<RawAnimationChannelRowV1>, D::Error>
21where
22 D: Deserializer<'de>,
23{
24 let values: CappedSequence<RawAnimationChannelRowV1> = deserialize_capped_sequence(
25 deserializer,
26 RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES,
27 )?;
28 if values.overflowed {
29 return Err(D::Error::custom(
30 "raw animation/channel inventory exceeded its global candidate bound",
31 ));
32 }
33 Ok(values.values)
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
39pub enum RawAnimationChannelRowV1 {
40 Animation {
42 source_animation_index: u64,
44 channel_coverage: RawSourceSetCoverageV1,
46 },
47 AnimationChannel {
49 source_animation_index: u64,
51 source_channel_index: u64,
53 },
54}
55
56impl RawAnimationChannelRowV1 {
57 pub const fn source_animation_index(&self) -> u64 {
59 match self {
60 Self::Animation {
61 source_animation_index,
62 ..
63 }
64 | Self::AnimationChannel {
65 source_animation_index,
66 ..
67 } => *source_animation_index,
68 }
69 }
70 pub const fn source_channel_index(&self) -> Option<u64> {
72 match self {
73 Self::Animation { .. } => None,
74 Self::AnimationChannel {
75 source_channel_index,
76 ..
77 } => Some(*source_channel_index),
78 }
79 }
80 pub const fn channel_coverage(&self) -> Option<RawSourceSetCoverageV1> {
82 match self {
83 Self::Animation {
84 channel_coverage, ..
85 } => Some(*channel_coverage),
86 Self::AnimationChannel { .. } => None,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct RawAnimationChannelInventoryV1 {
95 schema: String,
96 primary_input: InputIdentity,
97 source_format: SourceFormatV1,
98 animation_coverage: RawSourceSetCoverageV1,
99 source_coverage_complete: bool,
100 candidate_prefix_saturated: bool,
101 #[serde(deserialize_with = "deserialize_rows")]
102 rows: Vec<RawAnimationChannelRowV1>,
103}
104
105impl RawAnimationChannelInventoryV1 {
106 pub fn from_source(facts: SourceFactsViewV1<'_>) -> Self {
108 let mut rows = Vec::new();
109 let mut candidate_prefix_saturated = false;
110 let mut source_coverage_complete =
111 facts.clips().coverage().state() == crate::SourceSetCoverageStateV1::Complete;
112 'animations: for animation in facts.clips().rows() {
113 if !source_coverage_complete {
114 break;
115 }
116 let channel_coverage: RawSourceSetCoverageV1 = animation.channels().coverage().into();
117 source_coverage_complete &=
118 channel_coverage.state() == crate::RawSourceSetCoverageStateV1::Complete;
119 rows.push(RawAnimationChannelRowV1::Animation {
120 source_animation_index: animation.source_clip_index() as u64,
121 channel_coverage,
122 });
123 if rows.len() == RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES {
124 candidate_prefix_saturated = true;
125 break;
126 }
127 if !source_coverage_complete {
128 break;
129 }
130 for channel in animation.channels().rows() {
131 rows.push(RawAnimationChannelRowV1::AnimationChannel {
132 source_animation_index: animation.source_clip_index() as u64,
133 source_channel_index: channel.source_channel_index() as u64,
134 });
135 if rows.len() == RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES {
136 candidate_prefix_saturated = true;
137 break 'animations;
138 }
139 }
140 }
141 Self {
142 schema: RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID.into(),
143 primary_input: facts.primary_identity().clone(),
144 source_format: facts.format(),
145 animation_coverage: facts.clips().coverage().into(),
146 source_coverage_complete,
147 candidate_prefix_saturated,
148 rows,
149 }
150 }
151
152 pub fn validate(&self) -> Result<(), &'static str> {
154 if self.schema != RAW_ANIMATION_CHANNEL_INVENTORY_V1_ID
155 || self.rows.len() > RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES
156 || self.candidate_prefix_saturated
157 != (self.rows.len() == RAW_ANIMATION_CHANNEL_INVENTORY_V1_MAX_CANDIDATES)
158 || !valid_coverage(self.animation_coverage)
159 {
160 return Err("invalid raw animation/channel inventory header or bound");
161 }
162 let mut expected_animation = 0u64;
163 let mut current_animation = None;
164 let mut expected_channel = 0u64;
165 for row in &self.rows {
166 match row {
167 RawAnimationChannelRowV1::Animation {
168 source_animation_index,
169 channel_coverage,
170 } => {
171 if *source_animation_index != expected_animation
172 || !valid_coverage(*channel_coverage)
173 {
174 return Err("raw animation rows are not contiguous or valid");
175 }
176 expected_animation = expected_animation.saturating_add(1);
177 current_animation = Some(*source_animation_index);
178 expected_channel = 0;
179 }
180 RawAnimationChannelRowV1::AnimationChannel {
181 source_animation_index,
182 source_channel_index,
183 } => {
184 if current_animation != Some(*source_animation_index)
185 || *source_channel_index != expected_channel
186 {
187 return Err("raw channel rows are not contiguous or attached");
188 }
189 expected_channel = expected_channel.saturating_add(1);
190 }
191 }
192 }
193 let retained_coverage_complete = self.animation_coverage.state()
194 == crate::RawSourceSetCoverageStateV1::Complete
195 && self.rows.iter().all(|row| {
196 row.channel_coverage().is_none_or(|coverage| {
197 coverage.state() == crate::RawSourceSetCoverageStateV1::Complete
198 })
199 });
200 if self.source_coverage_complete != retained_coverage_complete {
201 return Err("raw animation/channel aggregate coverage is contradictory");
202 }
203 Ok(())
204 }
205
206 pub const fn primary_input(&self) -> &InputIdentity {
208 &self.primary_input
209 }
210 pub const fn source_format(&self) -> SourceFormatV1 {
212 self.source_format
213 }
214 pub const fn animation_coverage(&self) -> RawSourceSetCoverageV1 {
216 self.animation_coverage
217 }
218 pub fn candidate_count(&self) -> u64 {
220 self.rows.len() as u64
221 }
222 pub const fn candidate_overflow(&self) -> bool {
224 self.candidate_prefix_saturated
225 }
226 pub fn rows(&self) -> &[RawAnimationChannelRowV1] {
228 &self.rows
229 }
230 pub fn is_complete_empty(&self) -> bool {
232 self.source_coverage_complete && self.rows.is_empty()
233 }
234 pub const fn source_coverage_complete(&self) -> bool {
236 self.source_coverage_complete
237 }
238}
239
240fn valid_coverage(coverage: RawSourceSetCoverageV1) -> bool {
241 matches!(
242 (coverage.state(), coverage.reason()),
243 (crate::RawSourceSetCoverageStateV1::Complete, None)
244 | (crate::RawSourceSetCoverageStateV1::Partial, Some(_))
245 | (crate::RawSourceSetCoverageStateV1::Unavailable, Some(_))
246 )
247}