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