1use crate::error::PredictionRuleError;
4use crate::{SettingIdV2, profiles_v2};
5use animsmith_core::engine_contract::{EngineSettingIdV2, EngineSettingValueV2};
6use animsmith_core::measure::{
7 AssetMeasurements, LinearTransformClassification, SkeletonNodeMeasurements, measure_assets,
8};
9use animsmith_core::prediction::{
10 EnginePredictionBasisV4, PredictionBasisReferenceV4, RawSceneAttachmentBasisDomainV1,
11 RawSceneAttachmentBasisReferenceV1,
12};
13use animsmith_core::{
14 Applicability, Check, CheckCtx, CheckOutput, DependencyClosureCoverageV1,
15 EngineMachineResultV1, EnginePredictionFacetV4, EnginePredictionV4, EvaluationScope,
16 EvaluationScopeCode, ImporterSubjectCreationV1, InventoryCoverageResultV1, LoadedSource,
17 MeasurementPointerV1, PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE, PredictionBasisReferenceV1,
18 PredictionBasisReferenceV2, PredictionFacetDemandV2, PredictionInventoryCoverageStateV1,
19 PredictionInventoryDomainV1, PredictionProvenanceV4, PredictionProvenanceV5,
20 PredictionRuleAllocationV2, PredictionScalarV1, PredictionUnavailableReasonV2,
21 RawSceneAttachmentCoverageV1, RawSceneAttachmentInventoryV1, RawSourceBasisReferenceV1,
22 RawSourceDomainV1, RawSourceFieldIdV1, RawSourceKeyV1, ResolvedSettingLocationV1,
23 SourceFormatV1, SourceNodeLocalRest, SourceSkeletonCoverage, SourceSkeletonRowKindV1,
24 TransformScaleDomainV1, TransformScaleResultV1, TransformScaleSubjectKindV1,
25 UnitMappingResultV1,
26};
27use std::collections::{BTreeMap, BTreeSet};
28
29pub const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
31
32const BEVY_FAMILY: &str = "bevy";
33const BEVY_PROFILE_REVISION: u32 = 2;
34const BEVY_ENGINE_VERSION: &str = "0.19.0";
35const BEVY_IMPORTER: &str = "gltf-asset-loader";
36
37const BEVY_LOADER_SOURCE: &str = "bevy-gltf-loader-0.19.0-c6f634ca";
38const BEVY_COORDINATE_SOURCE: &str = "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca";
39const BEVY_RENDER_ASSET_SOURCE: &str = "bevy-render-asset-usages-0.19.0-c6f634ca";
40const GLTF_UNIT_SOURCE: &str = "khronos-gltf-2.0-coordinate-units";
41
42const FILE_SCOPE: &str = "engine-unit-scale:file-unit";
43const SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
44const SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
45const MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
46const MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
47const SELECTED_NODE_SCOPE: &str = "engine-unit-scale:selected-source-node";
48const SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
49 "animsmith:selected_node_scene_reachability_unavailable";
50
51const SELECTED_ANCESTRY_MAX_NODES: usize = 128;
55
56pub struct EngineUnitScaleCheck<'a> {
58 source: &'a LoadedSource,
59 provenance: Option<&'a PredictionProvenanceV4>,
60 successor_provenance: Option<&'a PredictionProvenanceV5>,
61}
62
63impl<'a> EngineUnitScaleCheck<'a> {
64 pub fn new(
72 source: &'a LoadedSource,
73 provenance: Option<&'a PredictionProvenanceV4>,
74 ) -> Result<Self, PredictionRuleError> {
75 if let Some(provenance) = provenance {
76 provenance
77 .validate()
78 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
79 let raw_source = animsmith_core::RawSourceBindingV2::from_source(
80 source.source_facts(),
81 source.exact_source_timing(),
82 )
83 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
84 let raw_inventory_matches = match (
85 provenance.raw_scene_attachment().inventory(),
86 source.raw_scene_attachment_inventory(),
87 ) {
88 (Some(left), Some(right)) => left == right,
89 (None, None) => true,
90 _ => false,
91 };
92 if &raw_source != provenance.raw_source()
93 || !raw_inventory_matches
94 || source.dependency_closure() != provenance.dependency_closure()
95 {
96 return Err(PredictionRuleError::SourceProvenanceMismatch);
97 }
98 if is_bevy_tuple(provenance) && !has_frozen_bevy_profile(provenance) {
99 return Err(PredictionRuleError::FrozenProfileMismatch);
100 }
101 }
102 Ok(Self {
103 source,
104 provenance,
105 successor_provenance: None,
106 })
107 }
108
109 pub fn new_v5(
112 source: &'a LoadedSource,
113 provenance: Option<&'a PredictionProvenanceV5>,
114 ) -> Result<Self, PredictionRuleError> {
115 let mut check = Self::new(source, provenance.map(PredictionProvenanceV5::base))?;
116 if let Some(provenance) = provenance {
117 provenance
118 .validate()
119 .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
120 if is_bevy_tuple_revision(provenance.base(), 3)
121 && !has_frozen_bevy_profile_revision(provenance.base(), 3)
122 {
123 return Err(PredictionRuleError::FrozenProfileMismatch);
124 }
125 }
126 check.successor_provenance = provenance;
127 Ok(check)
128 }
129
130 fn exact_applicable(&self) -> bool {
131 match (self.provenance, self.successor_provenance) {
132 (Some(provenance), Some(_)) => {
133 is_exact_bevy_gltf_revision(self.source, provenance, 2)
134 || is_exact_bevy_gltf_revision(self.source, provenance, 3)
135 }
136 (Some(provenance), None) => is_exact_bevy_gltf(self.source, provenance),
137 (None, _) => false,
138 }
139 }
140}
141
142impl Check for EngineUnitScaleCheck<'_> {
143 fn id(&self) -> &'static str {
144 ENGINE_UNIT_SCALE_CHECK_ID
145 }
146
147 fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
148 if self.exact_applicable() {
149 Applicability::Applicable
150 } else {
151 Applicability::NotApplicable
152 }
153 }
154
155 fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
156 let Some(provenance) = self.provenance else {
157 return empty_output();
158 };
159 if !self.exact_applicable() {
160 return empty_output();
161 }
162 let plan = plan(self.source, provenance);
163 let (capacity, summary_required) = match plan.demand {
164 PredictionFacetDemandV2::Exact(count) => (count, false),
165 PredictionFacetDemandV2::NPlusOne => (
166 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(1),
167 true,
168 ),
169 };
170 evaluate_allocated(
171 self.source,
172 provenance,
173 self.successor_provenance,
174 &plan,
175 capacity,
176 summary_required,
177 )
178 }
179
180 fn prediction_facet_demand_v2(&self, _ctx: &CheckCtx<'_>) -> PredictionFacetDemandV2 {
181 match self.provenance {
182 Some(provenance) if self.exact_applicable() => plan(self.source, provenance).demand,
183 _ => PredictionFacetDemandV2::Exact(0),
184 }
185 }
186
187 fn evaluate_with_prediction_allocation_v2(
188 &self,
189 _ctx: &CheckCtx<'_>,
190 allocation: PredictionRuleAllocationV2<'_>,
191 ) -> CheckOutput {
192 let Some(provenance) = self.provenance else {
193 return empty_output();
194 };
195 if !self.exact_applicable() {
196 return empty_output();
197 }
198 let plan = plan(self.source, provenance);
199 evaluate_allocated(
200 self.source,
201 provenance,
202 self.successor_provenance,
203 &plan,
204 allocation.candidate_capacity(),
205 allocation.summary_required(),
206 )
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
211enum ScenePlan {
212 Detailed(usize),
213 Incomplete,
214}
215
216#[derive(Debug, Clone)]
217enum MeshPlan {
218 Detailed(Vec<MeshJoinRow>),
219 CompleteEmpty,
220 Incomplete,
221 JoinOverflow,
222}
223
224#[derive(Debug, Clone, Copy)]
226struct MeshJoinRow {
227 source_scene_index: u64,
228 source_root_ordinal: u64,
229 root_node_index: u64,
230 source_node_index: u64,
231 source_mesh_index: u64,
232 source_primitive_index: u64,
233}
234
235#[derive(Debug, Clone)]
236struct RulePlan {
237 demand: PredictionFacetDemandV2,
238 scenes: ScenePlan,
239 meshes: MeshPlan,
240 selectors: usize,
241}
242
243fn plan(source: &LoadedSource, provenance: &PredictionProvenanceV4) -> RulePlan {
244 let inventory = provenance.raw_scene_attachment().inventory();
245 let scenes = match inventory {
246 Some(inventory)
247 if inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete =>
248 {
249 ScenePlan::Detailed(inventory.scenes().rows().len())
250 }
251 _ => ScenePlan::Incomplete,
252 };
253 let meshes = plan_meshes(source, inventory);
254 let selectors = selected_facet_count(source, inventory, provenance);
255
256 let scene_facets = match scenes {
257 ScenePlan::Detailed(count) => count,
258 ScenePlan::Incomplete => 1,
259 };
260 let mesh_facets = match &meshes {
261 MeshPlan::Detailed(rows) => rows.len(),
262 MeshPlan::CompleteEmpty | MeshPlan::Incomplete | MeshPlan::JoinOverflow => 1,
263 };
264 let exact = 1usize
265 .checked_add(scene_facets)
266 .and_then(|count| count.checked_add(mesh_facets))
267 .and_then(|count| count.checked_add(selectors));
268 let demand = match exact {
269 Some(count) if count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
270 PredictionFacetDemandV2::Exact(count)
271 }
272 _ => PredictionFacetDemandV2::NPlusOne,
273 };
274 RulePlan {
275 demand,
276 scenes,
277 meshes,
278 selectors,
279 }
280}
281
282fn plan_meshes(
283 source: &LoadedSource,
284 inventory: Option<&RawSceneAttachmentInventoryV1>,
285) -> MeshPlan {
286 let Some(inventory) = inventory else {
287 return MeshPlan::Incomplete;
288 };
289 if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
290 || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
291 || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
292 || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
293 || source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete
294 {
295 return MeshPlan::Incomplete;
296 }
297 checked_mesh_join(source, inventory)
298}
299
300fn selected_facet_count(
301 source: &LoadedSource,
302 inventory: Option<&RawSceneAttachmentInventoryV1>,
303 provenance: &PredictionProvenanceV4,
304) -> usize {
305 let selectors = provenance.rule_inputs().runtime_node_selectors();
306 if selectors.is_empty() {
307 return 0;
308 }
309 if source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete {
310 return selectors.len();
311 }
312 let nodes = &source.document().assets.source_skeleton.nodes;
313 let parents = selected_parent_index(source);
314 let complete_scenes = inventory.filter(|inventory| {
315 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
316 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
317 });
318 let mut count = 0usize;
319 for selector in selectors {
320 let mut matches = nodes.iter().filter(|node| {
321 node.name
322 .as_deref()
323 .is_some_and(|name| animsmith_core::config::glob_match(selector, name))
324 });
325 let first = matches.next();
326 let ambiguous = matches.next().is_some();
327 let facets = match (first, ambiguous, complete_scenes) {
328 (Some(node), false, Some(inventory)) => {
329 let mut work = 0usize;
330 let mut reachable = 0usize;
331 for scene in inventory.scenes().rows() {
332 match selected_scene_reachability(
333 node.source_node_index as u64,
334 scene.root_node_indices(),
335 &parents,
336 &mut work,
337 ) {
338 SelectedSceneReachability::Reachable(_, _) => {
339 reachable = reachable.saturating_add(1);
340 }
341 SelectedSceneReachability::Unreachable => {}
342 SelectedSceneReachability::Unavailable
343 | SelectedSceneReachability::WorkBudgetExceeded => {
344 reachable = 0;
345 break;
346 }
347 }
348 }
349 reachable.max(1)
350 }
351 _ => 1,
352 };
353 count = count.saturating_add(facets);
354 if count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
355 return count;
356 }
357 }
358 count
359}
360
361fn checked_mesh_join(source: &LoadedSource, inventory: &RawSceneAttachmentInventoryV1) -> MeshPlan {
368 let parents = source
369 .document()
370 .assets
371 .source_skeleton
372 .nodes
373 .iter()
374 .map(|node| {
375 (
376 node.source_node_index as u64,
377 node.parent_source_node_index.map(|parent| parent as u64),
378 )
379 })
380 .collect::<BTreeMap<_, _>>();
381 let scene_roots = inventory
382 .scenes()
383 .rows()
384 .iter()
385 .map(|scene| {
386 (
387 scene.source_scene_index(),
388 scene
389 .root_node_indices()
390 .iter()
391 .copied()
392 .enumerate()
393 .map(|(ordinal, node)| (node, ordinal as u64))
394 .collect::<BTreeMap<_, _>>(),
395 )
396 })
397 .collect::<Vec<_>>();
398 let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
399 BTreeMap::<u64, Vec<u64>>::new(),
400 |mut grouped, primitive| {
401 grouped
402 .entry(primitive.source_mesh_index())
403 .or_default()
404 .push(primitive.source_primitive_index());
405 grouped
406 },
407 );
408 let mut work = 0usize;
409 let mut rows = Vec::new();
410 for (source_scene_index, roots) in scene_roots {
411 for attachment in inventory.node_mesh_attachments().rows() {
412 if join_budget_exceeded(&mut work) {
413 return MeshPlan::JoinOverflow;
414 }
415 let Ok(reachable) = reachable_scene_root_indexed(
416 attachment.source_node_index(),
417 &roots,
418 &parents,
419 &mut work,
420 ) else {
421 return MeshPlan::JoinOverflow;
422 };
423 let Some((source_root_ordinal, root_node_index)) = reachable else {
424 continue;
425 };
426 for &source_primitive_index in primitives_by_mesh
427 .get(&attachment.source_mesh_index())
428 .into_iter()
429 .flatten()
430 {
431 if join_budget_exceeded(&mut work) {
432 return MeshPlan::JoinOverflow;
433 }
434 rows.push(MeshJoinRow {
435 source_scene_index,
436 source_root_ordinal,
437 root_node_index,
438 source_node_index: attachment.source_node_index(),
439 source_mesh_index: attachment.source_mesh_index(),
440 source_primitive_index,
441 });
442 }
443 }
444 }
445 if rows.is_empty() {
446 MeshPlan::CompleteEmpty
447 } else {
448 MeshPlan::Detailed(rows)
449 }
450}
451
452fn join_budget_exceeded(work: &mut usize) -> bool {
453 *work = work.saturating_add(1);
454 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
455}
456
457fn evaluate_allocated(
458 source: &LoadedSource,
459 provenance: &PredictionProvenanceV4,
460 successor_provenance: Option<&PredictionProvenanceV5>,
461 plan: &RulePlan,
462 candidate_capacity: usize,
463 summary_required: bool,
464) -> CheckOutput {
465 let measurements = measure_assets(source.document());
466 let dependency_complete = matches!(
467 provenance.dependency_closure().coverage(),
468 DependencyClosureCoverageV1::Complete
469 );
470 let mut candidates = Vec::with_capacity(plan.demand.bounded_count());
471
472 if candidate_capacity != 0 {
473 candidates.push(file_facet(provenance, dependency_complete));
474 }
475 append_scene_facets(
476 &mut candidates,
477 provenance,
478 plan.scenes,
479 dependency_complete,
480 candidate_capacity,
481 );
482 append_mesh_facets(
483 &mut candidates,
484 provenance,
485 &plan.meshes,
486 dependency_complete,
487 candidate_capacity,
488 );
489 append_selected_node_facets(
490 &mut candidates,
491 plan.selectors,
492 SelectedEvaluation {
493 source,
494 provenance,
495 measurements: &measurements,
496 dependency_complete,
497 candidate_capacity,
498 },
499 );
500
501 let mut facets = candidates;
502 if summary_required {
503 facets.push(unavailable_facet(
504 budget_scope(),
505 static_basis(&[BEVY_LOADER_SOURCE]),
506 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
507 ));
508 }
509 let evaluated_scopes = facets
510 .iter()
511 .filter(|facet| facet.result().is_some())
512 .map(|facet| facet.scope().clone())
513 .collect();
514 let prediction = EnginePredictionV4::new(provenance.identity().clone(), facets)
515 .expect("allocated unit/scale facets satisfy V4 bounds");
516 let output = CheckOutput::from_coverage(Vec::new(), evaluated_scopes, Vec::new());
517 match successor_provenance {
518 Some(successor) => output.with_engine_prediction_v5(
519 animsmith_core::EnginePredictionV5::new(successor, prediction)
520 .expect("unit-scale V5 prediction binds the same immutable V4 authority"),
521 ),
522 None => output.with_engine_prediction_v4(prediction),
523 }
524}
525
526fn file_facet(
527 _provenance: &PredictionProvenanceV4,
528 dependency_complete: bool,
529) -> EnginePredictionFacetV4 {
530 let scope = EvaluationScope::new(EvaluationScopeCode::custom(FILE_SCOPE));
531 let basis = unit_basis();
532 if !dependency_complete {
533 return unavailable_facet(
534 scope,
535 basis,
536 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
537 );
538 }
539 EnginePredictionFacetV4::available(
540 scope,
541 basis,
542 EngineMachineResultV1::UnitMapping(UnitMappingResultV1::gltf_to_engine_world_length_unit()),
543 )
544 .expect("the frozen Bevy file mapping is valid")
545}
546
547fn append_scene_facets(
548 facets: &mut Vec<EnginePredictionFacetV4>,
549 provenance: &PredictionProvenanceV4,
550 plan: ScenePlan,
551 dependency_complete: bool,
552 candidate_capacity: usize,
553) {
554 if facets.len() >= candidate_capacity {
555 return;
556 }
557 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
558 facets.push(unavailable_facet(
559 EvaluationScope::new(EvaluationScopeCode::custom(SCENE_INVENTORY_SCOPE)),
560 scene_inventory_basis(None),
561 unavailable_with_dependency(
562 PredictionUnavailableReasonV2::RawSourceIncomplete,
563 dependency_complete,
564 ),
565 ));
566 return;
567 };
568 match plan {
569 ScenePlan::Incomplete => facets.push(unavailable_facet(
570 EvaluationScope::new(EvaluationScopeCode::custom(SCENE_INVENTORY_SCOPE)),
571 scene_inventory_basis(Some(inventory)),
572 unavailable_with_dependency(
573 PredictionUnavailableReasonV2::RawSourceIncomplete,
574 dependency_complete,
575 ),
576 )),
577 ScenePlan::Detailed(expected) => {
578 debug_assert_eq!(expected, inventory.scenes().rows().len());
579 for scene in inventory.scenes().rows() {
580 if facets.len() >= candidate_capacity {
581 break;
582 }
583 let scope = EvaluationScope::new(EvaluationScopeCode::custom(SCENE_SCOPE))
584 .subject(format!("source_scene:{}", scene.source_scene_index()));
585 let basis = scene_row_basis(scene.source_scene_index());
586 if dependency_complete {
587 facets.push(available_transform(
588 scope,
589 basis,
590 TransformScaleSubjectKindV1::LoaderSceneEntity,
591 ImporterSubjectCreationV1::Created,
592 TransformScaleDomainV1::Local,
593 LinearTransformClassification::UnitOrthonormal,
594 ));
595 } else {
596 facets.push(unavailable_facet(
597 scope,
598 basis,
599 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
600 ));
601 }
602 }
603 }
604 }
605}
606
607fn append_mesh_facets(
608 facets: &mut Vec<EnginePredictionFacetV4>,
609 provenance: &PredictionProvenanceV4,
610 plan: &MeshPlan,
611 dependency_complete: bool,
612 candidate_capacity: usize,
613) {
614 if facets.len() >= candidate_capacity {
615 return;
616 }
617 match plan {
618 MeshPlan::Incomplete => {
619 facets.push(unavailable_facet(
620 EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE)),
621 mesh_inventory_basis(provenance.raw_scene_attachment().inventory()),
622 unavailable_with_dependency(
623 PredictionUnavailableReasonV2::RawSourceIncomplete,
624 dependency_complete,
625 ),
626 ));
627 }
628 MeshPlan::JoinOverflow => {
629 facets.push(unavailable_facet(
630 EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE)),
631 mesh_inventory_basis(provenance.raw_scene_attachment().inventory()),
632 vec![custom_reason("animsmith:mesh_join_work_budget_exceeded")],
633 ));
634 }
635 MeshPlan::CompleteEmpty => {
636 let scope = EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE));
637 let basis = mesh_inventory_basis(provenance.raw_scene_attachment().inventory());
638 if dependency_complete {
639 facets.push(
640 EnginePredictionFacetV4::available(
641 scope,
642 basis,
643 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
644 domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
645 coverage: PredictionInventoryCoverageStateV1::Complete,
646 retained_rows: 0,
647 }),
648 )
649 .expect("complete-empty joined mesh subjects form an available result"),
650 );
651 } else {
652 facets.push(unavailable_facet(
653 scope,
654 basis,
655 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
656 ));
657 }
658 }
659 MeshPlan::Detailed(rows) => {
660 for row in rows {
661 if facets.len() >= candidate_capacity {
662 break;
663 }
664 let scope =
665 EvaluationScope::new(EvaluationScopeCode::custom(MESH_SCOPE)).subject(format!(
666 "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
667 row.source_scene_index,
668 row.source_node_index,
669 row.source_mesh_index,
670 row.source_primitive_index,
671 ));
672 let basis = mesh_row_basis(
673 row.source_scene_index,
674 row.source_root_ordinal,
675 row.root_node_index,
676 row.source_node_index,
677 row.source_mesh_index,
678 row.source_primitive_index,
679 );
680 if dependency_complete {
681 facets.push(available_transform(
682 scope,
683 basis,
684 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
685 if load_meshes(provenance) {
686 ImporterSubjectCreationV1::Created
687 } else {
688 ImporterSubjectCreationV1::SuppressedBySetting
689 },
690 TransformScaleDomainV1::Local,
691 LinearTransformClassification::UnitOrthonormal,
692 ));
693 } else {
694 facets.push(unavailable_facet(
695 scope,
696 basis,
697 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
698 ));
699 }
700 }
701 }
702 }
703}
704
705#[derive(Clone, Copy)]
706struct SelectedEvaluation<'a> {
707 source: &'a LoadedSource,
708 provenance: &'a PredictionProvenanceV4,
709 measurements: &'a AssetMeasurements,
710 dependency_complete: bool,
711 candidate_capacity: usize,
712}
713
714fn append_selected_node_facets(
715 facets: &mut Vec<EnginePredictionFacetV4>,
716 expected_facets: usize,
717 evaluation: SelectedEvaluation<'_>,
718) {
719 let SelectedEvaluation {
720 source,
721 provenance,
722 measurements: _,
723 dependency_complete,
724 candidate_capacity,
725 } = evaluation;
726 let selectors = provenance.rule_inputs().runtime_node_selectors();
727 if selectors.is_empty() {
728 debug_assert_eq!(expected_facets, 0);
729 return;
730 }
731 let inventory = provenance.raw_scene_attachment().inventory();
732 let inventory_complete = inventory.is_some_and(|inventory| {
733 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
734 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
735 });
736 let start_len = facets.len();
737 if source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete
738 || !inventory_complete
739 {
740 for selector in selectors {
741 if facets.len() >= candidate_capacity {
742 break;
743 }
744 facets.push(unavailable_facet(
745 selected_scope(selector),
746 selected_resolution_basis(source, inventory, selector, &[]),
747 unavailable_with_dependency(
748 PredictionUnavailableReasonV2::RawSourceIncomplete,
749 dependency_complete,
750 ),
751 ));
752 }
753 return;
754 }
755
756 let inventory = inventory.expect("complete selected-node inventory exists");
757 for selector in selectors {
758 if facets.len() >= candidate_capacity {
759 break;
760 }
761 let mut matches = source
762 .document()
763 .assets
764 .source_skeleton
765 .nodes
766 .iter()
767 .filter(|node| {
768 node.name
769 .as_deref()
770 .is_some_and(|name| animsmith_core::config::glob_match(selector, name))
771 });
772 let first = matches.next();
773 let second = matches.next();
774 match (first, second) {
775 (None, _) => facets.push(unavailable_facet(
776 selected_scope(selector),
777 selected_resolution_basis(source, Some(inventory), selector, &[]),
778 unavailable_with_dependency(
779 PredictionUnavailableReasonV2::SourceSelectorNoMatch,
780 dependency_complete,
781 ),
782 )),
783 (Some(first), Some(second)) => facets.push(unavailable_facet(
784 selected_scope(selector),
785 selected_resolution_basis(
786 source,
787 Some(inventory),
788 selector,
789 &[first.source_node_index, second.source_node_index],
790 ),
791 unavailable_with_dependency(
792 PredictionUnavailableReasonV2::SourceSelectorAmbiguous,
793 dependency_complete,
794 ),
795 )),
796 (Some(node), None) => append_resolved_selected_node(
797 facets,
798 inventory,
799 selector,
800 node.source_node_index,
801 evaluation,
802 ),
803 }
804 }
805 if candidate_capacity >= start_len.saturating_add(expected_facets) {
806 debug_assert_eq!(facets.len() - start_len, expected_facets);
807 }
808}
809
810fn append_resolved_selected_node(
811 facets: &mut Vec<EnginePredictionFacetV4>,
812 inventory: &RawSceneAttachmentInventoryV1,
813 selector: &str,
814 node_index: usize,
815 evaluation: SelectedEvaluation<'_>,
816) {
817 let SelectedEvaluation {
818 source,
819 measurements,
820 dependency_complete,
821 candidate_capacity,
822 ..
823 } = evaluation;
824 let parents = selected_parent_index(source);
825 let mut reachable_scenes = Vec::new();
826 let mut work = 0usize;
827 for scene in inventory.scenes().rows() {
828 match selected_scene_reachability(
829 node_index as u64,
830 scene.root_node_indices(),
831 &parents,
832 &mut work,
833 ) {
834 SelectedSceneReachability::Reachable(root_ordinal, root_node_index) => {
835 reachable_scenes.push((scene, (root_ordinal, root_node_index)));
836 }
837 SelectedSceneReachability::Unreachable => {}
838 SelectedSceneReachability::Unavailable
839 | SelectedSceneReachability::WorkBudgetExceeded => {
840 facets.push(unavailable_facet(
841 selected_scope(selector),
842 selected_node_basis(
843 source,
844 Some(inventory),
845 selector,
846 node_index,
847 &[],
848 None,
849 None,
850 ),
851 unavailable_with_dependency(
852 custom_reason(SELECTED_REACHABILITY_UNAVAILABLE_REASON),
853 dependency_complete,
854 ),
855 ));
856 return;
857 }
858 }
859 }
860 let ancestry = selected_ancestry(source, node_index);
861 let measurement = measurements
862 .skeleton_nodes
863 .iter()
864 .enumerate()
865 .find(|(_, node)| node.node_index == node_index);
866 if reachable_scenes.is_empty() {
867 facets.push(unavailable_facet(
868 selected_scope(selector),
869 selected_node_basis(
870 source,
871 Some(inventory),
872 selector,
873 node_index,
874 &ancestry.rows,
875 None,
876 None,
877 ),
878 unavailable_with_dependency(
879 custom_reason("animsmith:selected_node_unreachable"),
880 dependency_complete,
881 ),
882 ));
883 return;
884 }
885
886 for (scene, (root_ordinal, root_node_index)) in reachable_scenes {
887 if facets.len() >= candidate_capacity {
888 break;
889 }
890 let scene_witness = Some(SelectedSceneWitness {
891 source_scene_index: scene.source_scene_index(),
892 source_root_ordinal: root_ordinal,
893 root_node_index,
894 });
895 let basis = selected_node_basis(
896 source,
897 Some(inventory),
898 selector,
899 node_index,
900 &ancestry.rows,
901 scene_witness,
902 measurement,
903 );
904 let scope = selected_scene_scope(selector, scene.source_scene_index(), node_index);
905 let unavailable_reason = match ancestry.representation {
906 SelectedRepresentation::AllTrs => measurement.and_then(|(_, node)| {
907 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
908 || node.rest_world_matrix.is_none())
909 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
910 }),
911 SelectedRepresentation::MatrixAuthored => Some(custom_reason(
912 "animsmith:matrix_authored_selected_node_or_ancestry",
913 )),
914 SelectedRepresentation::Unavailable => Some(custom_reason(
915 "animsmith:selected_node_ancestry_unavailable",
916 )),
917 }
918 .or_else(|| {
919 measurement
920 .is_none()
921 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
922 });
923 if let Some(reason) = unavailable_reason {
924 facets.push(unavailable_facet(
925 scope,
926 basis,
927 unavailable_with_dependency(reason, dependency_complete),
928 ));
929 } else if dependency_complete {
930 let node = measurement
931 .expect("available selected node has a measurement")
932 .1;
933 facets.push(available_transform(
937 scope,
938 basis,
939 TransformScaleSubjectKindV1::SelectedSourceNode,
940 ImporterSubjectCreationV1::Created,
941 TransformScaleDomainV1::LoaderRootToSubject,
942 node.rest_world_linear.classification,
943 ));
944 } else {
945 facets.push(unavailable_facet(
946 scope,
947 basis,
948 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
949 ));
950 }
951 }
952}
953
954#[derive(Debug, Clone, Copy, PartialEq, Eq)]
955enum SelectedRepresentation {
956 AllTrs,
957 MatrixAuthored,
958 Unavailable,
959}
960
961struct SelectedAncestry {
962 representation: SelectedRepresentation,
963 rows: Vec<usize>,
964}
965
966fn selected_ancestry(source: &LoadedSource, start: usize) -> SelectedAncestry {
967 let nodes = source
968 .document()
969 .assets
970 .source_skeleton
971 .nodes
972 .iter()
973 .map(|node| (node.source_node_index, node))
974 .collect::<BTreeMap<_, _>>();
975 let mut seen = BTreeSet::new();
976 let mut current = start;
977 let mut rows = Vec::new();
978 for _ in 0..SELECTED_ANCESTRY_MAX_NODES {
979 let index = current;
980 if !seen.insert(index) {
981 return SelectedAncestry {
982 representation: SelectedRepresentation::Unavailable,
983 rows,
984 };
985 }
986 let Some(node) = nodes.get(&index) else {
987 return SelectedAncestry {
988 representation: SelectedRepresentation::Unavailable,
989 rows,
990 };
991 };
992 rows.push(index);
993 if matches!(node.local_rest, SourceNodeLocalRest::Matrix(_)) {
994 return SelectedAncestry {
995 representation: SelectedRepresentation::MatrixAuthored,
996 rows,
997 };
998 }
999 let Some(parent) = node.parent_source_node_index else {
1000 return SelectedAncestry {
1001 representation: SelectedRepresentation::AllTrs,
1002 rows,
1003 };
1004 };
1005 current = parent;
1006 }
1007 SelectedAncestry {
1008 representation: SelectedRepresentation::Unavailable,
1009 rows,
1010 }
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014enum SelectedSceneReachability {
1015 Reachable(u64, u64),
1016 Unreachable,
1017 Unavailable,
1018 WorkBudgetExceeded,
1019}
1020
1021fn selected_reachability_budget_exceeded(work: &mut usize) -> bool {
1022 *work = work.saturating_add(1);
1023 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
1024}
1025
1026fn selected_parent_index(source: &LoadedSource) -> BTreeMap<u64, Option<u64>> {
1027 source
1028 .document()
1029 .assets
1030 .source_skeleton
1031 .nodes
1032 .iter()
1033 .map(|node| {
1034 (
1035 node.source_node_index as u64,
1036 node.parent_source_node_index.map(|parent| parent as u64),
1037 )
1038 })
1039 .collect()
1040}
1041
1042fn selected_scene_reachability(
1043 start: u64,
1044 roots: &[u64],
1045 parents: &BTreeMap<u64, Option<u64>>,
1046 work: &mut usize,
1047) -> SelectedSceneReachability {
1048 if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
1049 return SelectedSceneReachability::WorkBudgetExceeded;
1050 }
1051 let roots = roots
1052 .iter()
1053 .copied()
1054 .enumerate()
1055 .map(|(ordinal, node)| (node, ordinal as u64))
1056 .collect::<BTreeMap<_, _>>();
1057 let mut seen = BTreeSet::new();
1058 let mut current = Some(start);
1059 for _ in 0..SELECTED_ANCESTRY_MAX_NODES {
1060 let Some(index) = current else {
1061 return SelectedSceneReachability::Unreachable;
1062 };
1063 if selected_reachability_budget_exceeded(work) {
1064 return SelectedSceneReachability::WorkBudgetExceeded;
1065 }
1066 if let Some(ordinal) = roots.get(&index) {
1067 return SelectedSceneReachability::Reachable(*ordinal, index);
1068 }
1069 if !seen.insert(index) {
1070 return SelectedSceneReachability::Unavailable;
1071 }
1072 current = match parents.get(&index) {
1073 Some(parent) => *parent,
1074 None => return SelectedSceneReachability::Unavailable,
1075 };
1076 }
1077 match current {
1078 Some(_) => SelectedSceneReachability::Unavailable,
1079 None => SelectedSceneReachability::Unreachable,
1080 }
1081}
1082
1083fn reachable_scene_root_indexed(
1084 start: u64,
1085 roots: &BTreeMap<u64, u64>,
1086 parents: &BTreeMap<u64, Option<u64>>,
1087 work: &mut usize,
1088) -> Result<Option<(u64, u64)>, ()> {
1089 let mut seen = BTreeSet::new();
1090 let mut current = Some(start);
1091 while let Some(index) = current {
1092 if join_budget_exceeded(work) {
1093 return Err(());
1094 }
1095 if let Some(ordinal) = roots.get(&index) {
1096 return Ok(Some((*ordinal, index)));
1097 }
1098 if !seen.insert(index) {
1099 return Ok(None);
1100 }
1101 current = match parents.get(&index) {
1102 Some(parent) => *parent,
1103 None => return Ok(None),
1104 };
1105 }
1106 Ok(None)
1107}
1108
1109fn available_transform(
1110 scope: EvaluationScope,
1111 basis: EnginePredictionBasisV4,
1112 subject_kind: TransformScaleSubjectKindV1,
1113 creation: ImporterSubjectCreationV1,
1114 domain: TransformScaleDomainV1,
1115 classification: LinearTransformClassification,
1116) -> EnginePredictionFacetV4 {
1117 EnginePredictionFacetV4::available(
1118 scope,
1119 basis,
1120 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
1121 subject_kind,
1122 creation,
1123 domain,
1124 classification: (creation == ImporterSubjectCreationV1::Created)
1125 .then_some(classification),
1126 }),
1127 )
1128 .expect("the exact Bevy transform result is valid")
1129}
1130
1131fn unavailable_facet(
1132 scope: EvaluationScope,
1133 basis: EnginePredictionBasisV4,
1134 reasons: Vec<PredictionUnavailableReasonV2>,
1135) -> EnginePredictionFacetV4 {
1136 EnginePredictionFacetV4::required_unavailable(scope, basis, reasons)
1137 .expect("typed unit/scale unavailability is valid")
1138}
1139
1140fn unavailable_with_dependency(
1141 reason: PredictionUnavailableReasonV2,
1142 dependency_complete: bool,
1143) -> Vec<PredictionUnavailableReasonV2> {
1144 let mut reasons = vec![reason];
1145 if !dependency_complete {
1146 reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
1147 }
1148 reasons
1149}
1150
1151fn unit_basis() -> EnginePredictionBasisV4 {
1152 let mut references = [
1153 "application_world_unit_policy",
1154 "importer_scale_conversion",
1155 "physical_dimensions_preserved",
1156 "source_to_target_unit_mapping",
1157 "target_linear_unit",
1158 ]
1159 .into_iter()
1160 .map(|fact| {
1161 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1162 PredictionBasisReferenceV1::profile_fact(fact).expect("static fact id is valid"),
1163 ))
1164 })
1165 .collect::<Vec<_>>();
1166 for source in [BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE, GLTF_UNIT_SOURCE] {
1167 references.push(primary_source_reference(source));
1168 }
1169 EnginePredictionBasisV4::new(references).expect("the frozen unit basis is valid")
1170}
1171
1172fn scene_inventory_basis(
1173 inventory: Option<&RawSceneAttachmentInventoryV1>,
1174) -> EnginePredictionBasisV4 {
1175 let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1176 references.push(transform_profile_fact_reference());
1177 references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1178 references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1179 if inventory.is_some() {
1180 references.push(raw_inventory_reference(
1181 RawSceneAttachmentBasisReferenceV1::Coverage {
1182 domain: RawSceneAttachmentBasisDomainV1::Scenes,
1183 },
1184 ));
1185 }
1186 EnginePredictionBasisV4::new(references).expect("the frozen scene basis is valid")
1187}
1188
1189fn scene_row_basis(source_scene_index: u64) -> EnginePredictionBasisV4 {
1190 let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1191 references.push(transform_profile_fact_reference());
1192 references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1193 references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1194 references.push(raw_inventory_reference(
1195 RawSceneAttachmentBasisReferenceV1::Coverage {
1196 domain: RawSceneAttachmentBasisDomainV1::Scenes,
1197 },
1198 ));
1199 references.push(raw_inventory_reference(
1200 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
1201 ));
1202 EnginePredictionBasisV4::new(references).expect("the scene-row basis is valid")
1203}
1204
1205fn mesh_inventory_basis(
1206 inventory: Option<&RawSceneAttachmentInventoryV1>,
1207) -> EnginePredictionBasisV4 {
1208 let mut references = static_references(&[
1209 BEVY_LOADER_SOURCE,
1210 BEVY_COORDINATE_SOURCE,
1211 BEVY_RENDER_ASSET_SOURCE,
1212 ]);
1213 references.push(transform_profile_fact_reference());
1214 references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1215 references.push(setting_reference(SettingIdV2::LoadMeshes));
1216 references.push(setting_reference(SettingIdV2::RotateMeshes));
1217 if inventory.is_some() {
1218 for domain in [
1219 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1220 RawSceneAttachmentBasisDomainV1::Scenes,
1221 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
1222 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
1223 ] {
1224 references.push(raw_inventory_reference(
1225 RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1226 ));
1227 }
1228 }
1229 EnginePredictionBasisV4::new(references).expect("the frozen mesh basis is valid")
1230}
1231
1232fn mesh_row_basis(
1233 source_scene_index: u64,
1234 source_root_ordinal: u64,
1235 root_node_index: u64,
1236 source_node_index: u64,
1237 source_mesh_index: u64,
1238 source_primitive_index: u64,
1239) -> EnginePredictionBasisV4 {
1240 let mut references = static_references(&[
1241 BEVY_LOADER_SOURCE,
1242 BEVY_COORDINATE_SOURCE,
1243 BEVY_RENDER_ASSET_SOURCE,
1244 ]);
1245 references.push(transform_profile_fact_reference());
1246 references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1247 references.push(setting_reference(SettingIdV2::LoadMeshes));
1248 references.push(setting_reference(SettingIdV2::RotateMeshes));
1249 for domain in [
1250 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1251 RawSceneAttachmentBasisDomainV1::Scenes,
1252 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
1253 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
1254 ] {
1255 references.push(raw_inventory_reference(
1256 RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1257 ));
1258 }
1259 for reference in [
1260 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
1261 RawSceneAttachmentBasisReferenceV1::SceneRoot {
1262 source_scene_index,
1263 source_root_ordinal,
1264 source_node_index: root_node_index,
1265 },
1266 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
1267 source_node_index,
1268 source_mesh_index,
1269 },
1270 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
1271 source_mesh_index,
1272 source_primitive_index,
1273 },
1274 ] {
1275 references.push(raw_inventory_reference(reference));
1276 }
1277 EnginePredictionBasisV4::new(references).expect("the mesh-row basis is valid")
1278}
1279
1280fn raw_inventory_reference(
1281 reference: RawSceneAttachmentBasisReferenceV1,
1282) -> PredictionBasisReferenceV4 {
1283 PredictionBasisReferenceV4::raw_scene_attachment(reference)
1284}
1285
1286fn selected_static_references(selector: &str) -> Vec<PredictionBasisReferenceV4> {
1287 let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1288 references.push(transform_profile_fact_reference());
1289 references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1290 references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1291 references.push(PredictionBasisReferenceV4::v2(
1292 PredictionBasisReferenceV2::v1(
1293 PredictionBasisReferenceV1::project_field(
1294 "runtime_nodes.selector",
1295 PredictionScalarV1::text(selector).expect("validated selector text is bounded"),
1296 )
1297 .expect("the runtime-node selector field id is valid"),
1298 ),
1299 ));
1300 references
1301}
1302
1303fn selected_resolution_basis(
1304 source: &LoadedSource,
1305 inventory: Option<&RawSceneAttachmentInventoryV1>,
1306 selector: &str,
1307 matched_node_indices: &[usize],
1308) -> EnginePredictionBasisV4 {
1309 let mut references = selected_static_references(selector);
1310 if inventory.is_some() {
1311 for domain in [
1312 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1313 RawSceneAttachmentBasisDomainV1::Scenes,
1314 ] {
1315 references.push(raw_inventory_reference(
1316 RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1317 ));
1318 }
1319 }
1320 for &node_index in matched_node_indices {
1321 references.push(raw_source_node_reference(source, node_index, "name"));
1322 }
1323 EnginePredictionBasisV4::new(references).expect("the selected-node resolution basis is valid")
1324}
1325
1326#[derive(Debug, Clone, Copy)]
1327struct SelectedSceneWitness {
1328 source_scene_index: u64,
1329 source_root_ordinal: u64,
1330 root_node_index: u64,
1331}
1332
1333fn selected_node_basis(
1334 source: &LoadedSource,
1335 inventory: Option<&RawSceneAttachmentInventoryV1>,
1336 selector: &str,
1337 node_index: usize,
1338 ancestry_rows: &[usize],
1339 scene: Option<SelectedSceneWitness>,
1340 measurement: Option<(usize, &SkeletonNodeMeasurements)>,
1341) -> EnginePredictionBasisV4 {
1342 let mut references = selected_resolution_basis(source, inventory, selector, &[node_index])
1343 .references()
1344 .to_vec();
1345 for &ancestry_node_index in ancestry_rows {
1346 references.push(raw_source_node_reference(
1347 source,
1348 ancestry_node_index,
1349 "local_rest.kind",
1350 ));
1351 references.push(raw_source_node_reference(
1352 source,
1353 ancestry_node_index,
1354 "parent_source_node_index",
1355 ));
1356 }
1357 if let Some(scene) = scene {
1358 references.push(raw_inventory_reference(
1359 RawSceneAttachmentBasisReferenceV1::SceneRow {
1360 source_scene_index: scene.source_scene_index,
1361 },
1362 ));
1363 references.push(raw_inventory_reference(
1364 RawSceneAttachmentBasisReferenceV1::SceneRoot {
1365 source_scene_index: scene.source_scene_index,
1366 source_root_ordinal: scene.source_root_ordinal,
1367 source_node_index: scene.root_node_index,
1368 },
1369 ));
1370 }
1371 if let Some((measurement_ordinal, node)) = measurement {
1372 references.push(PredictionBasisReferenceV4::v2(
1373 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::measurement_v16(
1374 MeasurementPointerV1::new(format!(
1375 "/measurements/skeleton_nodes/{measurement_ordinal}/rest_world_linear/classification"
1376 ))
1377 .expect("static measurement path shape is valid"),
1378 PredictionScalarV1::token(classification_name(
1379 node.rest_world_linear.classification,
1380 ))
1381 .expect("classification spelling is a token"),
1382 )),
1383 ));
1384 }
1385 EnginePredictionBasisV4::new(references).expect("the selected-node evidence basis is valid")
1386}
1387
1388fn raw_source_node_reference(
1389 source: &LoadedSource,
1390 node_index: usize,
1391 field: &'static str,
1392) -> PredictionBasisReferenceV4 {
1393 let raw = RawSourceBasisReferenceV1::from_source(
1394 RawSourceDomainV1::SourceNode,
1395 RawSourceKeyV1::SourceSkeleton {
1396 row_kind: SourceSkeletonRowKindV1::SourceNode,
1397 source_index: node_index as u64,
1398 },
1399 RawSourceFieldIdV1::new(field).expect("static raw field is valid"),
1400 source.source_facts(),
1401 )
1402 .expect("resolved selected source node has the cited raw field");
1403 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1404 PredictionBasisReferenceV1::raw_source(raw),
1405 ))
1406}
1407
1408fn transform_profile_fact_reference() -> PredictionBasisReferenceV4 {
1409 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1410 PredictionBasisReferenceV1::profile_fact("resulting_transform_scale")
1411 .expect("static profile fact id is valid"),
1412 ))
1413}
1414
1415fn static_basis(sources: &[&str]) -> EnginePredictionBasisV4 {
1416 EnginePredictionBasisV4::new(static_references(sources))
1417 .expect("the frozen static basis is valid")
1418}
1419
1420fn static_references(sources: &[&str]) -> Vec<PredictionBasisReferenceV4> {
1421 sources
1422 .iter()
1423 .map(|source| primary_source_reference(source))
1424 .collect()
1425}
1426
1427fn primary_source_reference(source: &str) -> PredictionBasisReferenceV4 {
1428 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1429 PredictionBasisReferenceV1::primary_source(source)
1430 .expect("static primary-source id is valid"),
1431 ))
1432}
1433
1434fn setting_reference(setting: SettingIdV2) -> PredictionBasisReferenceV4 {
1435 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1436 PredictionBasisReferenceV1::resolved_setting(
1437 ResolvedSettingLocationV1::Document,
1438 setting.as_str(),
1439 )
1440 .expect("static setting id is valid"),
1441 ))
1442}
1443
1444fn selected_scope(selector: &str) -> EvaluationScope {
1445 EvaluationScope::new(EvaluationScopeCode::custom(SELECTED_NODE_SCOPE))
1446 .subject(format!("selector:{selector}"))
1447}
1448
1449fn selected_scene_scope(
1450 selector: &str,
1451 source_scene_index: u64,
1452 node_index: usize,
1453) -> EvaluationScope {
1454 EvaluationScope::new(EvaluationScopeCode::custom(SELECTED_NODE_SCOPE)).subject(format!(
1455 "selector:{selector}:source_scene:{source_scene_index}:source_node:{node_index}"
1456 ))
1457}
1458
1459fn budget_scope() -> EvaluationScope {
1460 EvaluationScope::new(EvaluationScopeCode::custom(
1461 "engine-unit-scale:facet-budget",
1462 ))
1463}
1464
1465fn custom_reason(code: &'static str) -> PredictionUnavailableReasonV2 {
1466 PredictionUnavailableReasonV2::custom(code).expect("static reason code is valid")
1467}
1468
1469fn classification_name(classification: LinearTransformClassification) -> &'static str {
1470 match classification {
1471 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
1472 LinearTransformClassification::UniformScaled => "uniform_scaled",
1473 LinearTransformClassification::NonUniform => "non_uniform",
1474 LinearTransformClassification::Sheared => "sheared",
1475 LinearTransformClassification::Reflected => "reflected",
1476 LinearTransformClassification::Singular => "singular",
1477 LinearTransformClassification::NonFinite => "non_finite",
1478 _ => "unknown",
1479 }
1480}
1481
1482fn load_meshes(provenance: &PredictionProvenanceV4) -> bool {
1483 matches!(
1484 provenance
1485 .settings()
1486 .document_setting(EngineSettingIdV2::LoadMeshes)
1487 .map(|setting| setting.value()),
1488 Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
1489 )
1490}
1491
1492fn is_bevy_tuple(provenance: &PredictionProvenanceV4) -> bool {
1493 is_bevy_tuple_revision(provenance, BEVY_PROFILE_REVISION)
1494}
1495
1496fn is_bevy_tuple_revision(provenance: &PredictionProvenanceV4, revision: u32) -> bool {
1497 let selection = provenance.profile().selection();
1498 selection.family() == BEVY_FAMILY
1499 && selection.profile_revision() == revision
1500 && selection.engine_version() == BEVY_ENGINE_VERSION
1501 && selection.importer() == BEVY_IMPORTER
1502}
1503
1504fn has_frozen_bevy_profile(provenance: &PredictionProvenanceV4) -> bool {
1505 has_frozen_bevy_profile_revision(provenance, BEVY_PROFILE_REVISION)
1506}
1507
1508fn has_frozen_bevy_profile_revision(provenance: &PredictionProvenanceV4, revision: u32) -> bool {
1509 profiles_v2().iter().any(|profile| {
1510 let selection = profile.selection();
1511 selection.family() == BEVY_FAMILY
1512 && selection.profile_revision() == revision
1513 && selection.engine_version() == BEVY_ENGINE_VERSION
1514 && selection.importer() == BEVY_IMPORTER
1515 && crate::project_engine_profile_v2(profile)
1516 .is_ok_and(|projected| &projected == provenance.profile())
1517 })
1518}
1519
1520fn is_exact_bevy_gltf(source: &LoadedSource, provenance: &PredictionProvenanceV4) -> bool {
1521 is_exact_bevy_gltf_revision(source, provenance, BEVY_PROFILE_REVISION)
1522}
1523
1524fn is_exact_bevy_gltf_revision(
1525 source: &LoadedSource,
1526 provenance: &PredictionProvenanceV4,
1527 revision: u32,
1528) -> bool {
1529 matches!(
1530 source.source_facts().format(),
1531 SourceFormatV1::GltfJson | SourceFormatV1::Glb
1532 ) && source.source_facts().format() == provenance.source_format()
1533 && is_bevy_tuple_revision(provenance, revision)
1534 && has_frozen_bevy_profile_revision(provenance, revision)
1535}
1536
1537fn empty_output() -> CheckOutput {
1538 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
1539}