1use crate::addressability::{
4 GltfAnimationAddressabilityBevyAdapterV1, GltfAnimationAddressabilityInventoryV1,
5};
6use crate::error::PredictionRuleError;
7use crate::registry::profiles_v1;
8use animsmith_core::{
9 Applicability, Check, CheckCtx, CheckOutput, CheckSelection, EngineAnimationAddressabilityV1,
10 EngineFactIdV1, EngineFactStateV1, EngineFactValueV1, EnginePredictionBasisV1,
11 EnginePredictionBasisV2, EnginePredictionFacetV1, EnginePredictionFacetV2,
12 EnginePredictionFacetV3, EnginePredictionV1, EnginePredictionV2, EnginePredictionV3,
13 EvaluationScope, EvaluationScopeCode, LoadedSource, PredictionBasisReferenceV1,
14 PredictionBasisReferenceV2, PredictionFacetDemandV2, PredictionProvenanceV1,
15 PredictionProvenanceV2, PredictionProvenanceV3, PredictionRuleAllocationV2,
16 PredictionUnavailableReasonV1, RAW_SOURCE_V1_MAX_CLIPS, RawSourceBasisReferenceV1,
17 RawSourceBindingV1, RawSourceBindingV2, RawSourceDomainV1, RawSourceFieldIdV1, RawSourceKeyV1,
18 SourceFormatV1, SourceSetCoverageStateV1, evaluate_checks,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
24#[error("source animation index {source_clip_index} is outside the V1 limit of {limit} animations")]
25pub struct BevyAnimationAssetLabelError {
26 pub source_clip_index: usize,
28 pub limit: usize,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35#[non_exhaustive]
36pub enum GltfAnimationAddressabilityAdapterError {
37 #[error(transparent)]
39 PredictionRule(#[from] PredictionRuleError),
40 #[error(transparent)]
43 Evaluation(#[from] animsmith_core::EvaluationError),
44 #[error(transparent)]
47 Contract(#[from] crate::GltfAnimationAddressabilityError),
48}
49
50pub const ENGINE_ADDRESSABILITY_CHECK_ID: &str = "engine-addressability";
52
53pub const ENGINE_CHECK_IDS_V1: &[&str] = &[ENGINE_ADDRESSABILITY_CHECK_ID];
55
56pub const ENGINE_CHECK_IDS_V2: &[&str] = &[
62 ENGINE_ADDRESSABILITY_CHECK_ID,
63 crate::clip_boundary::ENGINE_CLIP_BOUNDARY_CHECK_ID,
64 crate::unit_scale::ENGINE_UNIT_SCALE_CHECK_ID,
65 crate::track_support::ENGINE_TRACK_SUPPORT_CHECK_ID,
66 crate::root_motion::ENGINE_ROOT_MOTION_CHECK_ID,
67];
68
69const BEVY_FAMILY: &str = "bevy";
70const BEVY_PROFILE_REVISION: u32 = 1;
71const BEVY_ENGINE_VERSION: &str = "0.19.0";
72const BEVY_IMPORTER: &str = "gltf-asset-loader";
73const BEVY_ANIMATION_LABEL_SOURCE: &str = "bevy-gltf-asset-label-0.19.0";
74
75#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct BevyAnimationAssetLabelV1 {
83 source_clip_index: usize,
84 display_selector: String,
85}
86
87impl BevyAnimationAssetLabelV1 {
88 pub fn new(source_clip_index: usize) -> Result<Self, BevyAnimationAssetLabelError> {
95 if source_clip_index >= RAW_SOURCE_V1_MAX_CLIPS {
96 return Err(BevyAnimationAssetLabelError {
97 source_clip_index,
98 limit: RAW_SOURCE_V1_MAX_CLIPS,
99 });
100 }
101 Ok(Self {
102 source_clip_index,
103 display_selector: format!("Animation{source_clip_index}"),
104 })
105 }
106
107 pub const fn source_clip_index(&self) -> usize {
109 self.source_clip_index
110 }
111
112 pub fn as_str(&self) -> &str {
114 &self.display_selector
115 }
116}
117
118pub struct EngineAddressabilityCheck<'a> {
124 source: &'a LoadedSource,
125 provenance: Option<&'a PredictionProvenanceV1>,
126}
127
128pub struct EngineAddressabilityCheckV2<'a> {
132 source: &'a LoadedSource,
133 provenance: Option<&'a PredictionProvenanceV2>,
134}
135
136pub struct EngineAddressabilityCheckV3<'a> {
142 source: &'a LoadedSource,
143 provenance: Option<&'a PredictionProvenanceV3>,
144}
145
146impl<'a> EngineAddressabilityCheckV3<'a> {
147 pub fn new(
149 source: &'a LoadedSource,
150 provenance: Option<&'a PredictionProvenanceV3>,
151 ) -> Result<Self, PredictionRuleError> {
152 if let Some(provenance) = provenance {
153 provenance
154 .validate()
155 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
156 let raw_source = RawSourceBindingV2::from_source(
157 source.source_facts(),
158 source.exact_source_timing(),
159 )
160 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
161 if &raw_source != provenance.raw_source()
162 || source.dependency_closure() != provenance.dependency_closure()
163 {
164 return Err(PredictionRuleError::SourceProvenanceMismatch);
165 }
166 let selection = provenance.profile().selection();
167 if selection.family() == BEVY_FAMILY
168 && selection.profile_revision() == BEVY_PROFILE_REVISION
169 && selection.engine_version() == BEVY_ENGINE_VERSION
170 && selection.importer() == BEVY_IMPORTER
171 && !profiles_v1().iter().any(|profile| {
172 let selection = profile.selection();
173 selection.family() == BEVY_FAMILY
174 && selection.profile_revision() == BEVY_PROFILE_REVISION
175 && selection.engine_version() == BEVY_ENGINE_VERSION
176 && selection.importer() == BEVY_IMPORTER
177 && profile.facts_identity() == provenance.profile().facts_identity()
178 })
179 {
180 return Err(PredictionRuleError::FrozenProfileMismatch);
181 }
182 }
183 Ok(Self { source, provenance })
184 }
185}
186
187impl Check for EngineAddressabilityCheckV3<'_> {
188 fn id(&self) -> &'static str {
189 ENGINE_ADDRESSABILITY_CHECK_ID
190 }
191
192 fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
193 match self.provenance {
194 Some(provenance)
195 if is_bevy_gltf_v3(self.source, provenance)
196 && !facts_are_complete_and_empty(self.source) =>
197 {
198 Applicability::Applicable
199 }
200 _ => Applicability::NotApplicable,
201 }
202 }
203
204 fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
205 let Some(provenance) = self.provenance else {
206 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
207 };
208 if !is_bevy_gltf_v3(self.source, provenance) || facts_are_complete_and_empty(self.source) {
209 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
210 }
211 evaluate_animation_asset_labels_v3(self.source, provenance)
212 }
213
214 fn prediction_facet_demand_v2(&self, _ctx: &CheckCtx<'_>) -> PredictionFacetDemandV2 {
215 match self.provenance {
216 Some(provenance)
217 if is_bevy_gltf_v3(self.source, provenance)
218 && !facts_are_complete_and_empty(self.source) =>
219 {
220 if self.source.source_facts().clips().coverage().state()
221 != SourceSetCoverageStateV1::Complete
222 || matches!(
223 provenance.settings().clip_coverage().state(),
224 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
225 )
226 {
227 PredictionFacetDemandV2::Exact(1)
228 } else {
229 PredictionFacetDemandV2::Exact(self.source.source_facts().clips().rows().len())
230 }
231 }
232 _ => PredictionFacetDemandV2::Exact(0),
233 }
234 }
235
236 fn evaluate_with_prediction_allocation_v2(
237 &self,
238 _ctx: &CheckCtx<'_>,
239 allocation: PredictionRuleAllocationV2<'_>,
240 ) -> CheckOutput {
241 let Some(provenance) = self.provenance else {
242 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
243 };
244 if !is_bevy_gltf_v3(self.source, provenance) || facts_are_complete_and_empty(self.source) {
245 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
246 }
247 evaluate_animation_asset_labels_v3_allocated(self.source, provenance, allocation)
248 }
249}
250
251impl<'a> EngineAddressabilityCheckV2<'a> {
252 pub fn new(
254 source: &'a LoadedSource,
255 provenance: Option<&'a PredictionProvenanceV2>,
256 ) -> Result<Self, PredictionRuleError> {
257 if let Some(provenance) = provenance {
258 provenance
259 .validate()
260 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
261 let raw_source = RawSourceBindingV1::from_source(source.source_facts());
262 if &raw_source != provenance.raw_source()
263 || source.dependency_closure() != provenance.dependency_closure()
264 {
265 return Err(PredictionRuleError::SourceProvenanceMismatch);
266 }
267 let selection = provenance.profile().selection();
268 if selection.family() == BEVY_FAMILY
269 && selection.profile_revision() == BEVY_PROFILE_REVISION
270 && selection.engine_version() == BEVY_ENGINE_VERSION
271 && selection.importer() == BEVY_IMPORTER
272 && !profiles_v1().iter().any(|profile| {
273 let selection = profile.selection();
274 selection.family() == BEVY_FAMILY
275 && selection.profile_revision() == BEVY_PROFILE_REVISION
276 && selection.engine_version() == BEVY_ENGINE_VERSION
277 && selection.importer() == BEVY_IMPORTER
278 && profile.facts_identity() == provenance.profile().facts_identity()
279 })
280 {
281 return Err(PredictionRuleError::FrozenProfileMismatch);
282 }
283 }
284 Ok(Self { source, provenance })
285 }
286}
287
288impl Check for EngineAddressabilityCheckV2<'_> {
289 fn id(&self) -> &'static str {
290 ENGINE_ADDRESSABILITY_CHECK_ID
291 }
292
293 fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
294 let Some(provenance) = self.provenance else {
295 return Applicability::NotApplicable;
296 };
297 if !is_bevy_gltf_v2(self.source, provenance) || facts_are_complete_and_empty(self.source) {
298 Applicability::NotApplicable
299 } else {
300 Applicability::Applicable
301 }
302 }
303
304 fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
305 let Some(provenance) = self.provenance else {
306 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
307 };
308 if !is_bevy_gltf_v2(self.source, provenance) || facts_are_complete_and_empty(self.source) {
309 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
310 }
311 evaluate_animation_asset_labels_v2(self.source, provenance)
312 }
313
314 fn prediction_facet_demand_v2(&self, _ctx: &CheckCtx<'_>) -> PredictionFacetDemandV2 {
315 match self.provenance {
316 Some(provenance)
317 if is_bevy_gltf_v2(self.source, provenance)
318 && !facts_are_complete_and_empty(self.source) =>
319 {
320 if self.source.source_facts().clips().coverage().state()
325 != SourceSetCoverageStateV1::Complete
326 || matches!(
327 provenance.settings().clip_coverage().state(),
328 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
329 )
330 {
331 PredictionFacetDemandV2::Exact(1)
332 } else {
333 PredictionFacetDemandV2::Exact(self.source.source_facts().clips().rows().len())
334 }
335 }
336 _ => PredictionFacetDemandV2::Exact(0),
337 }
338 }
339
340 fn evaluate_with_prediction_allocation_v2(
341 &self,
342 _ctx: &CheckCtx<'_>,
343 allocation: PredictionRuleAllocationV2<'_>,
344 ) -> CheckOutput {
345 let Some(provenance) = self.provenance else {
346 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
347 };
348 if !is_bevy_gltf_v2(self.source, provenance) || facts_are_complete_and_empty(self.source) {
349 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
350 }
351 evaluate_animation_asset_labels_v2_allocated(self.source, provenance, allocation)
352 }
353}
354
355impl<'a> EngineAddressabilityCheck<'a> {
356 pub fn new(
365 source: &'a LoadedSource,
366 provenance: Option<&'a PredictionProvenanceV1>,
367 ) -> Result<Self, PredictionRuleError> {
368 if let Some(provenance) = provenance {
369 let raw_source = RawSourceBindingV1::from_source(source.source_facts());
370 if &raw_source != provenance.raw_source()
371 || source.dependency_closure() != provenance.dependency_closure()
372 {
373 return Err(PredictionRuleError::SourceProvenanceMismatch);
374 }
375 if is_bevy_tuple(provenance)
376 && !profiles_v1().iter().any(|profile| {
377 let selection = profile.selection();
378 selection.family() == BEVY_FAMILY
379 && selection.profile_revision() == BEVY_PROFILE_REVISION
380 && selection.engine_version() == BEVY_ENGINE_VERSION
381 && selection.importer() == BEVY_IMPORTER
382 && profile.facts_identity() == provenance.profile().facts_identity()
383 })
384 {
385 return Err(PredictionRuleError::FrozenProfileMismatch);
386 }
387 }
388 Ok(Self { source, provenance })
389 }
390}
391
392impl Check for EngineAddressabilityCheck<'_> {
393 fn id(&self) -> &'static str {
394 ENGINE_ADDRESSABILITY_CHECK_ID
395 }
396
397 fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
398 borrowed_applicability(self.source, self.provenance)
399 }
400
401 fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
402 let Some(provenance) = self.provenance else {
403 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
404 };
405 if !is_bevy_gltf(self.source, provenance) || facts_are_complete_and_empty(self.source) {
406 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
407 }
408 evaluate_animation_asset_labels(self.source, provenance)
409 }
410}
411
412pub fn build_bevy_animation_addressability_adapter_v1(
427 source: &LoadedSource,
428 inventory: &GltfAnimationAddressabilityInventoryV1,
429 prediction_provenance: Option<PredictionProvenanceV1>,
430 ctx: &CheckCtx<'_>,
431) -> Result<Option<GltfAnimationAddressabilityBevyAdapterV1>, GltfAnimationAddressabilityAdapterError>
432{
433 let Some(prediction_provenance) = prediction_provenance else {
434 return Ok(None);
435 };
436 let check = EngineAddressabilityCheck::new(source, Some(&prediction_provenance))?;
437 if !is_bevy_gltf(source, &prediction_provenance) {
438 return Ok(None);
439 }
440
441 let evaluation = {
442 let checks: [Box<dyn Check + '_>; 1] = [Box::new(check)];
443 let mut evaluations = evaluate_checks(ctx, &checks, CheckSelection::All)?;
444 let evaluation = evaluations
445 .pop()
446 .expect("one valid static check catalog produces one evaluation");
447 debug_assert!(evaluations.is_empty());
448 evaluation
449 };
450 Ok(Some(GltfAnimationAddressabilityBevyAdapterV1::new(
451 prediction_provenance,
452 evaluation,
453 inventory,
454 )?))
455}
456
457fn borrowed_applicability(
458 source: &LoadedSource,
459 provenance: Option<&PredictionProvenanceV1>,
460) -> Applicability {
461 let Some(provenance) = provenance else {
462 return Applicability::NotApplicable;
463 };
464 if !is_bevy_gltf(source, provenance) {
465 return Applicability::NotApplicable;
466 }
467 if facts_are_complete_and_empty(source) {
468 Applicability::NotApplicable
469 } else {
470 Applicability::Applicable
471 }
472}
473
474fn facts_are_complete_and_empty(source: &LoadedSource) -> bool {
475 let clips = source.source_facts().clips();
476 clips.coverage().state() == SourceSetCoverageStateV1::Complete && clips.rows().is_empty()
477}
478
479fn is_bevy_gltf(source: &LoadedSource, provenance: &PredictionProvenanceV1) -> bool {
480 let facts = source.source_facts();
481 facts.format() == provenance.source_format()
482 && is_bevy_tuple(provenance)
483 && matches!(
484 provenance.source_format(),
485 SourceFormatV1::GltfJson | SourceFormatV1::Glb
486 )
487 && matches!(
488 provenance
489 .profile()
490 .fact(EngineFactIdV1::AnimationAddressability)
491 .map(|fact| fact.state()),
492 Some(EngineFactStateV1::Known(
493 EngineFactValueV1::AnimationAddressability(
494 EngineAnimationAddressabilityV1::GltfAssetLabel
495 )
496 ))
497 )
498 && provenance
499 .profile()
500 .source(BEVY_ANIMATION_LABEL_SOURCE)
501 .is_some()
502}
503
504fn is_bevy_gltf_v2(source: &LoadedSource, provenance: &PredictionProvenanceV2) -> bool {
505 let selection = provenance.profile().selection();
506 source.source_facts().format() == provenance.source_format()
507 && selection.family() == BEVY_FAMILY
508 && selection.profile_revision() == BEVY_PROFILE_REVISION
509 && selection.engine_version() == BEVY_ENGINE_VERSION
510 && selection.importer() == BEVY_IMPORTER
511 && matches!(
512 provenance
513 .profile()
514 .fact(EngineFactIdV1::AnimationAddressability)
515 .map(|fact| fact.state()),
516 Some(EngineFactStateV1::Known(
517 EngineFactValueV1::AnimationAddressability(
518 EngineAnimationAddressabilityV1::GltfAssetLabel
519 )
520 ))
521 )
522 && provenance
523 .profile()
524 .source(BEVY_ANIMATION_LABEL_SOURCE)
525 .is_some()
526}
527
528fn is_bevy_gltf_v3(source: &LoadedSource, provenance: &PredictionProvenanceV3) -> bool {
529 let selection = provenance.profile().selection();
530 source.source_facts().format() == provenance.source_format()
531 && selection.family() == BEVY_FAMILY
532 && selection.profile_revision() == BEVY_PROFILE_REVISION
533 && selection.engine_version() == BEVY_ENGINE_VERSION
534 && selection.importer() == BEVY_IMPORTER
535 && matches!(
536 provenance
537 .profile()
538 .fact(EngineFactIdV1::AnimationAddressability)
539 .map(|fact| fact.state()),
540 Some(EngineFactStateV1::Known(
541 EngineFactValueV1::AnimationAddressability(
542 EngineAnimationAddressabilityV1::GltfAssetLabel
543 )
544 ))
545 )
546 && provenance
547 .profile()
548 .source(BEVY_ANIMATION_LABEL_SOURCE)
549 .is_some()
550}
551
552fn is_bevy_tuple(provenance: &PredictionProvenanceV1) -> bool {
553 let selection = provenance.profile().selection();
554 selection.family() == BEVY_FAMILY
555 && selection.profile_revision() == BEVY_PROFILE_REVISION
556 && selection.engine_version() == BEVY_ENGINE_VERSION
557 && selection.importer() == BEVY_IMPORTER
558}
559
560fn evaluate_animation_asset_labels(
561 source: &LoadedSource,
562 provenance: &PredictionProvenanceV1,
563) -> CheckOutput {
564 let facts = source.source_facts();
565 let inventory_scope =
566 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY);
567 if facts.format() != provenance.source_format()
568 || facts.clips().coverage().state() != SourceSetCoverageStateV1::Complete
569 {
570 return unavailable_inventory(provenance, inventory_scope);
571 }
572
573 let mut scopes = Vec::with_capacity(facts.clips().rows().len());
574 let mut facets = Vec::with_capacity(facts.clips().rows().len());
575 for clip in facts.clips().rows() {
576 let source_index = clip.source_clip_index();
577 let label = BevyAnimationAssetLabelV1::new(source_index)
578 .expect("loader-valid retained clip indices satisfy the V1 raw-source bound");
579 let scope = EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
580 .subject(label.as_str().to_owned());
581 let source_name = RawSourceBasisReferenceV1::from_source(
582 RawSourceDomainV1::Clip,
583 RawSourceKeyV1::Clip {
584 source_clip_index: source_index as u64,
585 },
586 RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
587 facts,
588 )
589 .expect("loader-valid retained clip rows must resolve their own raw-source witness");
590 let basis = EnginePredictionBasisV1::new(vec![
591 PredictionBasisReferenceV1::profile_fact("animation_addressability")
592 .expect("static fact id is valid"),
593 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
594 .expect("static primary-source id is valid"),
595 PredictionBasisReferenceV1::raw_source(source_name),
596 ])
597 .expect("three distinct static basis references are valid");
598 let facet = EnginePredictionFacetV1::available(scope.clone(), basis)
599 .expect("complete source rows have unique bounded scopes");
600 scopes.push(scope);
601 facets.push(facet);
602 }
603
604 if facets.is_empty() {
605 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
606 }
607 let prediction = EnginePredictionV1::new(provenance.identity().clone(), facets)
608 .expect("complete bounded source rows form one valid canonical prediction");
609 CheckOutput::from_coverage(Vec::new(), scopes, Vec::new()).with_engine_prediction(prediction)
610}
611
612fn unavailable_inventory(
613 provenance: &PredictionProvenanceV1,
614 scope: EvaluationScope,
615) -> CheckOutput {
616 let basis = EnginePredictionBasisV1::new(vec![
617 PredictionBasisReferenceV1::profile_fact("animation_addressability")
618 .expect("static fact id is valid"),
619 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
620 .expect("static primary-source id is valid"),
621 ])
622 .expect("static basis is nonempty and canonical");
623 let facet = EnginePredictionFacetV1::required_unavailable(
624 scope,
625 basis,
626 vec![PredictionUnavailableReasonV1::RawSourceIncomplete],
627 )
628 .expect("static unavailable facet is valid");
629 let prediction = EnginePredictionV1::new(provenance.identity().clone(), vec![facet])
630 .expect("one static facet is valid");
631 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
632 .with_engine_prediction(prediction)
633}
634
635fn evaluate_animation_asset_labels_v2(
636 source: &LoadedSource,
637 provenance: &PredictionProvenanceV2,
638) -> CheckOutput {
639 let facts = source.source_facts();
640 let inventory_scope =
641 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY);
642 let settings_overflow = matches!(
643 provenance.settings().clip_coverage().state(),
644 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
645 );
646 if facts.clips().coverage().state() != SourceSetCoverageStateV1::Complete || settings_overflow {
647 let basis = EnginePredictionBasisV1::new(vec![
648 PredictionBasisReferenceV1::profile_fact("animation_addressability")
649 .expect("static fact id is valid"),
650 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
651 .expect("static primary-source id is valid"),
652 ])
653 .expect("static basis is canonical");
654 let mut reasons = Vec::new();
655 if facts.clips().coverage().state() != SourceSetCoverageStateV1::Complete {
656 reasons.push(animsmith_core::PredictionUnavailableReasonV2::RawSourceIncomplete);
657 }
658 if settings_overflow {
659 reasons.push(animsmith_core::PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
660 }
661 let facet = EnginePredictionFacetV2::required_unavailable(inventory_scope, basis, reasons)
662 .expect("static unavailable V2 facet is valid");
663 let prediction = EnginePredictionV2::new(provenance.identity().clone(), vec![facet])
664 .expect("one static V2 facet is valid");
665 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
666 .with_engine_prediction_v2(prediction);
667 }
668
669 let mut scopes = Vec::with_capacity(facts.clips().rows().len());
670 let mut facets = Vec::with_capacity(facts.clips().rows().len());
671 for clip in facts.clips().rows() {
672 let source_index = clip.source_clip_index();
673 let label = BevyAnimationAssetLabelV1::new(source_index)
674 .expect("loader-valid retained clip indices satisfy the V1 raw-source bound");
675 let scope = EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
676 .subject(label.as_str().to_owned());
677 let source_name = RawSourceBasisReferenceV1::from_source(
678 RawSourceDomainV1::Clip,
679 RawSourceKeyV1::Clip {
680 source_clip_index: source_index as u64,
681 },
682 RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
683 facts,
684 )
685 .expect("retained clip rows resolve their raw-source witness");
686 let basis = EnginePredictionBasisV1::new(vec![
687 PredictionBasisReferenceV1::profile_fact("animation_addressability")
688 .expect("static fact id is valid"),
689 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
690 .expect("static primary-source id is valid"),
691 PredictionBasisReferenceV1::raw_source(source_name),
692 ])
693 .expect("three static basis references are valid");
694 facets.push(
695 EnginePredictionFacetV2::available(scope.clone(), basis)
696 .expect("complete rows form V2 facets"),
697 );
698 scopes.push(scope);
699 }
700 if facets.is_empty() {
701 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
702 }
703 let prediction = EnginePredictionV2::new(provenance.identity().clone(), facets)
704 .expect("complete bounded source rows form V2 prediction");
705 CheckOutput::from_coverage(Vec::new(), scopes, Vec::new()).with_engine_prediction_v2(prediction)
706}
707
708fn evaluate_animation_asset_labels_v2_allocated(
709 source: &LoadedSource,
710 provenance: &PredictionProvenanceV2,
711 allocation: PredictionRuleAllocationV2<'_>,
712) -> CheckOutput {
713 let facts = source.source_facts();
714 let settings_partial = matches!(
715 provenance.settings().clip_coverage().state(),
716 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
717 );
718 let raw_partial = facts.clips().coverage().state() != SourceSetCoverageStateV1::Complete;
719 let mut scopes = Vec::with_capacity(allocation.candidate_capacity());
720 let mut facets = Vec::with_capacity(
721 allocation.candidate_capacity() + usize::from(allocation.summary_required()),
722 );
723
724 if raw_partial || settings_partial {
725 if allocation.candidate_capacity() != 0 {
728 let mut reasons = Vec::new();
729 if raw_partial {
730 reasons.push(animsmith_core::PredictionUnavailableReasonV2::RawSourceIncomplete);
731 }
732 if settings_partial {
733 reasons
734 .push(animsmith_core::PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
735 }
736 let basis = EnginePredictionBasisV1::new(vec![
737 PredictionBasisReferenceV1::profile_fact("animation_addressability")
738 .expect("static fact"),
739 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
740 .expect("static source"),
741 ])
742 .expect("static basis");
743 facets.push(
744 EnginePredictionFacetV2::required_unavailable(
745 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
746 basis,
747 reasons,
748 )
749 .expect("static unavailable V2 facet"),
750 );
751 }
752 } else {
753 for clip in facts
756 .clips()
757 .rows()
758 .iter()
759 .take(allocation.candidate_capacity())
760 {
761 let source_index = clip.source_clip_index();
762 let label = BevyAnimationAssetLabelV1::new(source_index)
763 .expect("loader-valid retained clip indices satisfy the V1 raw-source bound");
764 let scope = EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
765 .subject(label.as_str().to_owned());
766 let source_name = RawSourceBasisReferenceV1::from_source(
767 RawSourceDomainV1::Clip,
768 RawSourceKeyV1::Clip {
769 source_clip_index: source_index as u64,
770 },
771 RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
772 facts,
773 )
774 .expect("retained clip rows resolve their raw-source witness");
775 let basis = EnginePredictionBasisV1::new(vec![
776 PredictionBasisReferenceV1::profile_fact("animation_addressability")
777 .expect("static fact id is valid"),
778 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
779 .expect("static primary-source id is valid"),
780 PredictionBasisReferenceV1::raw_source(source_name),
781 ])
782 .expect("three static basis references are valid");
783 facets.push(
784 EnginePredictionFacetV2::available(scope.clone(), basis)
785 .expect("complete rows form V2 facets"),
786 );
787 scopes.push(scope);
788 }
789 }
790
791 if allocation.summary_required() {
792 let basis = EnginePredictionBasisV1::new(vec![
793 PredictionBasisReferenceV1::profile_fact("animation_addressability")
794 .expect("static fact"),
795 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
796 .expect("static source"),
797 ])
798 .expect("static basis");
799 facets.push(
800 EnginePredictionFacetV2::required_unavailable(
801 EvaluationScope::new(EvaluationScopeCode::custom(
802 "engine-addressability:facet-budget",
803 )),
804 basis,
805 vec![animsmith_core::PredictionUnavailableReasonV2::FacetBudgetExceeded],
806 )
807 .expect("budget summary facet"),
808 );
809 }
810 if facets.is_empty() {
811 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
812 }
813 CheckOutput::from_coverage(Vec::new(), scopes, Vec::new()).with_engine_prediction_v2(
814 EnginePredictionV2::new(provenance.identity().clone(), facets)
815 .expect("allocated V2 facets"),
816 )
817}
818
819fn evaluate_animation_asset_labels_v3(
820 source: &LoadedSource,
821 provenance: &PredictionProvenanceV3,
822) -> CheckOutput {
823 let raw_partial =
824 source.source_facts().clips().coverage().state() != SourceSetCoverageStateV1::Complete;
825 let settings_partial = matches!(
826 provenance.settings().clip_coverage().state(),
827 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
828 );
829 let candidate_capacity = if raw_partial || settings_partial {
830 1
831 } else {
832 source.source_facts().clips().rows().len()
833 };
834 evaluate_animation_asset_labels_v3_with_limits(source, provenance, candidate_capacity, false)
835}
836
837fn evaluate_animation_asset_labels_v3_allocated(
838 source: &LoadedSource,
839 provenance: &PredictionProvenanceV3,
840 allocation: PredictionRuleAllocationV2<'_>,
841) -> CheckOutput {
842 evaluate_animation_asset_labels_v3_with_limits(
843 source,
844 provenance,
845 allocation.candidate_capacity(),
846 allocation.summary_required(),
847 )
848}
849
850fn evaluate_animation_asset_labels_v3_with_limits(
851 source: &LoadedSource,
852 provenance: &PredictionProvenanceV3,
853 candidate_capacity: usize,
854 summary_required: bool,
855) -> CheckOutput {
856 let facts = source.source_facts();
857 let settings_partial = matches!(
858 provenance.settings().clip_coverage().state(),
859 animsmith_core::ResolvedEngineSettingsCoverageStateV2::Partial
860 );
861 let raw_partial = facts.clips().coverage().state() != SourceSetCoverageStateV1::Complete;
862 let mut scopes = Vec::with_capacity(candidate_capacity);
863 let mut facets = Vec::with_capacity(candidate_capacity + usize::from(summary_required));
864
865 if raw_partial || settings_partial {
866 if candidate_capacity != 0 {
867 let mut reasons = Vec::new();
868 if raw_partial {
869 reasons.push(animsmith_core::PredictionUnavailableReasonV2::RawSourceIncomplete);
870 }
871 if settings_partial {
872 reasons
873 .push(animsmith_core::PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
874 }
875 facets.push(
876 EnginePredictionFacetV3::required_unavailable(
877 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
878 addressability_basis_v3(facts, None),
879 reasons,
880 )
881 .expect("static unavailable V3 facet"),
882 );
883 }
884 } else {
885 for clip in facts.clips().rows().iter().take(candidate_capacity) {
886 let source_index = clip.source_clip_index();
887 let label = BevyAnimationAssetLabelV1::new(source_index)
888 .expect("loader-valid retained clip indices satisfy the V1 raw-source bound");
889 let scope = EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
890 .subject(label.as_str().to_owned());
891 facets.push(
892 EnginePredictionFacetV3::available(
893 scope.clone(),
894 addressability_basis_v3(facts, Some(source_index)),
895 )
896 .expect("complete rows form V3 facets"),
897 );
898 scopes.push(scope);
899 }
900 }
901
902 if summary_required {
903 facets.push(
904 EnginePredictionFacetV3::required_unavailable(
905 EvaluationScope::new(EvaluationScopeCode::custom(
906 "engine-addressability:facet-budget",
907 )),
908 addressability_basis_v3(facts, None),
909 vec![animsmith_core::PredictionUnavailableReasonV2::FacetBudgetExceeded],
910 )
911 .expect("budget summary V3 facet"),
912 );
913 }
914 if facets.is_empty() {
915 return CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new());
916 }
917 CheckOutput::from_coverage(Vec::new(), scopes, Vec::new()).with_engine_prediction_v3(
918 EnginePredictionV3::new(provenance.identity().clone(), facets)
919 .expect("allocated V3 facets"),
920 )
921}
922
923fn addressability_basis_v3(
924 facts: animsmith_core::SourceFactsViewV1<'_>,
925 source_index: Option<usize>,
926) -> EnginePredictionBasisV2 {
927 let mut references = vec![
928 PredictionBasisReferenceV2::v1(
929 PredictionBasisReferenceV1::profile_fact("animation_addressability")
930 .expect("static fact id is valid"),
931 ),
932 PredictionBasisReferenceV2::v1(
933 PredictionBasisReferenceV1::primary_source(BEVY_ANIMATION_LABEL_SOURCE)
934 .expect("static primary-source id is valid"),
935 ),
936 ];
937 if let Some(source_index) = source_index {
938 let source_name = RawSourceBasisReferenceV1::from_source(
939 RawSourceDomainV1::Clip,
940 RawSourceKeyV1::Clip {
941 source_clip_index: source_index as u64,
942 },
943 RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
944 facts,
945 )
946 .expect("retained clip rows resolve their raw-source witness");
947 references.push(PredictionBasisReferenceV2::v1(
948 PredictionBasisReferenceV1::raw_source(source_name),
949 ));
950 }
951 EnginePredictionBasisV2::new(references).expect("static addressability basis is canonical")
952}