Skip to main content

animsmith_engine/
canonical.rs

1use crate::types::UnrealSampleRateV2;
2use crate::{
3    AnimationAddressability, BakeOrExtract, ConversionControl, DefaultStatus, EngineProfile,
4    EngineProfileV2, FactState, FactValue, ForwardAxis, Handedness, ImportHandling, LinearUnit,
5    ResolvedClipCoverageV2, ResolvedProfile, ResolvedProfileSettingsV2, ResolvedProfileV2,
6    ResolvedSettingOriginV2, RootMotionAddressability, SettingApplicability, SettingDefaultV2,
7    SettingDomainV2, SettingId, SettingIdV2, SettingScope, SettingValue, SettingValueV2,
8    TargetAddressability, UpAxis,
9};
10use animsmith_core::engine_contract::{
11    EngineAnimationAddressabilityV1, EngineBakeOrExtractV1, EngineClipSettingsV1,
12    EngineContractError, EngineConversionControlV1, EngineCoordinateBasisV1, EngineDefaultStatusV1,
13    EngineFactIdV1, EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
14    EngineImportHandlingV1, EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1,
15    EngineProfileSelectionV1, EngineRootMotionAddressabilityV1, EngineSettingApplicabilityV1,
16    EngineSettingDescriptorV1, EngineSettingDomainV1, EngineSettingIdV1, EngineSettingRowV1,
17    EngineSettingScopeV1, EngineSettingValueV1, EngineTargetAddressabilityV1, EngineUpAxisV1,
18    ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2, ResolvedEngineSettingsV1,
19    ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
20};
21use animsmith_core::engine_contract::{
22    EngineClipSettingsV3 as CoreClipSettingsV3, EnginePrimarySourceV2 as CorePrimarySourceV2,
23    EngineSampleRateV2 as CoreSampleRateV2, EngineSettingDescriptorV2 as CoreSettingDescriptorV2,
24    EngineSettingDomainV2 as CoreSettingDomainV2, EngineSettingIdV2 as CoreSettingIdV2,
25    EngineSettingRowV3, EngineSettingValueOriginV3, EngineSettingValueV2 as CoreSettingValueV2,
26    ResolvedEngineProfileV2 as CoreEngineProfileV2,
27    ResolvedEngineSettingsCoverageV2 as CoreSettingsCoverageV2,
28    ResolvedEngineSettingsV3 as CoreSettingsV3, ResolvedEngineSettingsWorkV2 as CoreSettingsWorkV2,
29};
30use animsmith_core::{InputIdentity, SourceFormatV1};
31use std::collections::BTreeMap;
32
33pub(crate) fn facts_identity(profile: &EngineProfile) -> InputIdentity {
34    project_profile(profile)
35        .expect("the frozen engine registry must project into its core-owned V1 contract")
36        .facts_identity()
37        .clone()
38}
39
40pub(crate) fn settings_identity<'a>(
41    profile: &EngineProfile,
42    document: &BTreeMap<SettingId, SettingValue>,
43    clips: impl IntoIterator<Item = (&'a str, &'a BTreeMap<SettingId, SettingValue>)>,
44) -> Result<InputIdentity, EngineContractError> {
45    let profile = project_profile(profile)?;
46    Ok(project_settings(&profile, document, clips)?
47        .settings_identity()
48        .clone())
49}
50
51pub(crate) fn project_resolved_profile(
52    resolved: &ResolvedProfile,
53) -> Result<(ResolvedEngineProfileV1, ResolvedEngineSettingsV1), EngineContractError> {
54    let profile = project_profile(resolved.profile())?;
55    let settings = project_settings(
56        &profile,
57        resolved.document_settings(),
58        resolved
59            .clip_settings()
60            .iter()
61            .map(|clip| (clip.clip_name(), clip.settings())),
62    )?;
63    Ok((profile, settings))
64}
65
66/// Project one bounded V2 resolution into core-owned V2 settings.
67pub(crate) fn project_resolved_profile_v2(
68    resolved: &ResolvedProfileV2,
69) -> Result<(ResolvedEngineProfileV1, ResolvedEngineSettingsV2), EngineContractError> {
70    let profile = project_profile(resolved.profile())?;
71    let coverage = match resolved.clip_coverage() {
72        ResolvedClipCoverageV2::Complete => ResolvedEngineSettingsCoverageV2::complete(),
73        ResolvedClipCoverageV2::Partial { .. } => {
74            ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded()
75        }
76    };
77    let work = resolved.work();
78    let settings = ResolvedEngineSettingsV2::new(
79        &profile,
80        resolved
81            .document_settings()
82            .iter()
83            .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
84            .collect(),
85        resolved
86            .clip_settings()
87            .iter()
88            .map(|clip| {
89                EngineClipSettingsV1::new(
90                    clip.clip_name(),
91                    clip.settings()
92                        .iter()
93                        .map(|(id, value)| {
94                            EngineSettingRowV1::new(setting_id(*id), setting_value(value))
95                        })
96                        .collect(),
97                )
98            })
99            .collect::<Result<Vec<_>, _>>()?,
100        coverage,
101        ResolvedEngineSettingsWorkV2::new(
102            work.actual_clip_rows_inspected(),
103            work.materialized_clip_rows(),
104            work.retained_clip_rows(),
105        ),
106    )?;
107    Ok((profile, settings))
108}
109
110/// Project one engine-owned revision-2 profile into the core-owned V2 wire
111/// contract without changing the historical V1 projection.
112pub fn project_engine_profile_v2(
113    profile: &EngineProfileV2,
114) -> Result<CoreEngineProfileV2, EngineContractError> {
115    let selection = profile.selection();
116    CoreEngineProfileV2::new(
117        EngineProfileSelectionV1::new(
118            selection.family(),
119            selection.profile_revision(),
120            selection.engine_version(),
121            selection.importer(),
122        )?,
123        profile.profile_urn(),
124        profile.facts().to_vec(),
125        profile
126            .setting_descriptors()
127            .iter()
128            .map(|descriptor| {
129                CoreSettingDescriptorV2::new(
130                    setting_id_v2(descriptor.id()),
131                    match descriptor.scope() {
132                        SettingScope::Document => EngineSettingScopeV1::Document,
133                        SettingScope::Clip => EngineSettingScopeV1::Clip,
134                    },
135                    match descriptor.domain() {
136                        SettingDomainV2::Boolean => CoreSettingDomainV2::Boolean,
137                        SettingDomainV2::BakeOrExtract => CoreSettingDomainV2::BakeOrExtract,
138                        SettingDomainV2::SourceTransformPath => {
139                            CoreSettingDomainV2::SourceTransformPath
140                        }
141                        SettingDomainV2::AnimationType | SettingDomainV2::AvatarSetup => {
142                            CoreSettingDomainV2::Token
143                        }
144                        SettingDomainV2::LoadMeshesState | SettingDomainV2::HandlerEnvironment => {
145                            CoreSettingDomainV2::Token
146                        }
147                        SettingDomainV2::PositiveInteger => CoreSettingDomainV2::PositiveInteger,
148                        SettingDomainV2::SampleRate => CoreSettingDomainV2::SampleRate,
149                    },
150                    profile.accepted_inputs().to_vec(),
151                    match descriptor.default() {
152                        SettingDefaultV2::RequiredExplicit => None,
153                        SettingDefaultV2::Verified(value) => Some(setting_value_v2(value)),
154                    },
155                )
156            })
157            .collect::<Result<Vec<_>, _>>()?,
158        profile
159            .sources()
160            .iter()
161            .map(|source| {
162                CorePrimarySourceV2::new(
163                    source.id(),
164                    source.target_version(),
165                    source.url(),
166                    source.verified_on(),
167                    source.supported_facts().to_vec(),
168                    source
169                        .supported_settings()
170                        .iter()
171                        .copied()
172                        .map(setting_id_v2)
173                        .collect(),
174                )
175            })
176            .collect::<Result<Vec<_>, _>>()?,
177    )
178}
179
180/// Project input-validated revision-2 settings into the core-owned V3
181/// origin-bearing contract.
182pub fn project_resolved_engine_settings_v3(
183    resolved: &ResolvedProfileSettingsV2,
184) -> Result<(CoreEngineProfileV2, CoreSettingsV3), EngineContractError> {
185    let profile = project_engine_profile_v2(resolved.profile())?;
186    let document_settings = resolved
187        .document_settings()
188        .iter()
189        .map(|(id, resolved)| {
190            EngineSettingRowV3::new(
191                setting_id_v2(*id),
192                setting_value_v2(resolved.value()),
193                match resolved.origin() {
194                    ResolvedSettingOriginV2::ExplicitConfig => {
195                        EngineSettingValueOriginV3::ExplicitConfig
196                    }
197                    ResolvedSettingOriginV2::ProfileDefault => {
198                        EngineSettingValueOriginV3::ProfileDefault
199                    }
200                },
201            )
202        })
203        .collect::<Vec<_>>();
204    let clips = resolved
205        .clip_settings()
206        .iter()
207        .map(|clip| {
208            CoreClipSettingsV3::new(
209                clip.clip_ordinal(),
210                clip.clip_name(),
211                clip.settings()
212                    .iter()
213                    .map(|(id, resolved)| {
214                        EngineSettingRowV3::new(
215                            setting_id_v2(*id),
216                            setting_value_v2(resolved.value()),
217                            match resolved.origin() {
218                                ResolvedSettingOriginV2::ExplicitConfig => {
219                                    EngineSettingValueOriginV3::ExplicitConfig
220                                }
221                                ResolvedSettingOriginV2::ProfileDefault => {
222                                    EngineSettingValueOriginV3::ProfileDefault
223                                }
224                            },
225                        )
226                    })
227                    .collect(),
228            )
229        })
230        .collect::<Result<Vec<_>, _>>()?;
231    let coverage = match resolved.clip_coverage() {
232        ResolvedClipCoverageV2::Complete => CoreSettingsCoverageV2::complete(),
233        ResolvedClipCoverageV2::Partial { .. } => {
234            CoreSettingsCoverageV2::actual_clip_rows_exceeded()
235        }
236    };
237    let work = resolved.work();
238    let settings = CoreSettingsV3::new(
239        &profile,
240        resolved.source_format(),
241        document_settings,
242        clips,
243        coverage,
244        CoreSettingsWorkV2::new(
245            work.actual_clip_rows_inspected(),
246            work.materialized_clip_rows(),
247            work.retained_clip_rows(),
248        ),
249    )?;
250    Ok((profile, settings))
251}
252
253fn project_profile(
254    profile: &EngineProfile,
255) -> Result<ResolvedEngineProfileV1, EngineContractError> {
256    let selection = profile.selection();
257    ResolvedEngineProfileV1::new(
258        EngineProfileSelectionV1::new(
259            selection.family(),
260            selection.profile_revision(),
261            selection.engine_version(),
262            selection.importer(),
263        )?,
264        profile.fact_bundle_urn(),
265        profile
266            .facts()
267            .iter()
268            .map(|fact| EngineProfileFactV1::new(fact_id(fact.id()), fact_state(fact.state())))
269            .collect(),
270        profile
271            .setting_descriptors()
272            .iter()
273            .map(|descriptor| {
274                EngineSettingDescriptorV1::new(
275                    setting_id(descriptor.id()),
276                    match descriptor.scope() {
277                        SettingScope::Document => EngineSettingScopeV1::Document,
278                        SettingScope::Clip => EngineSettingScopeV1::Clip,
279                    },
280                    match descriptor.domain() {
281                        crate::SettingDomain::Boolean => EngineSettingDomainV1::Boolean,
282                        crate::SettingDomain::BakeOrExtract => EngineSettingDomainV1::BakeOrExtract,
283                        crate::SettingDomain::SourceTransformPath => {
284                            EngineSettingDomainV1::SourceTransformPath
285                        }
286                    },
287                    match descriptor.applicability() {
288                        SettingApplicability::Applicable => {
289                            EngineSettingApplicabilityV1::Applicable
290                        }
291                        SettingApplicability::NotApplicable => {
292                            EngineSettingApplicabilityV1::NotApplicable
293                        }
294                    },
295                    match descriptor.default_status() {
296                        DefaultStatus::RequiredWithoutDefault => {
297                            EngineDefaultStatusV1::RequiredWithoutDefault
298                        }
299                        DefaultStatus::NotApplicable => EngineDefaultStatusV1::NotApplicable,
300                    },
301                )
302            })
303            .collect(),
304        profile
305            .sources()
306            .iter()
307            .map(|source| {
308                EnginePrimarySourceV1::new(
309                    source.id(),
310                    source.target_version(),
311                    source.url(),
312                    source.verified_on(),
313                    source
314                        .supported_facts()
315                        .iter()
316                        .copied()
317                        .map(fact_id)
318                        .collect(),
319                    source
320                        .supported_settings()
321                        .iter()
322                        .copied()
323                        .map(setting_id)
324                        .collect(),
325                )
326            })
327            .collect::<Result<Vec<_>, _>>()?,
328    )
329}
330
331fn project_settings<'a>(
332    profile: &ResolvedEngineProfileV1,
333    document: &BTreeMap<SettingId, SettingValue>,
334    clips: impl IntoIterator<Item = (&'a str, &'a BTreeMap<SettingId, SettingValue>)>,
335) -> Result<ResolvedEngineSettingsV1, EngineContractError> {
336    ResolvedEngineSettingsV1::new(
337        profile,
338        document
339            .iter()
340            .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
341            .collect(),
342        clips
343            .into_iter()
344            .map(|(clip_name, values)| {
345                EngineClipSettingsV1::new(
346                    clip_name,
347                    values
348                        .iter()
349                        .map(|(id, value)| {
350                            EngineSettingRowV1::new(setting_id(*id), setting_value(value))
351                        })
352                        .collect(),
353                )
354            })
355            .collect::<Result<Vec<_>, _>>()?,
356    )
357}
358
359/// Validate one materialized clip row before a bounded resolver advances.
360pub(crate) fn validate_clip_settings(
361    clip_name: &str,
362    values: &BTreeMap<SettingId, SettingValue>,
363) -> Result<(), EngineContractError> {
364    EngineClipSettingsV1::new(
365        clip_name,
366        values
367            .iter()
368            .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
369            .collect(),
370    )?;
371    Ok(())
372}
373
374fn fact_state(state: &FactState) -> EngineFactStateV1 {
375    match state {
376        FactState::Known(value) => EngineFactStateV1::Known(fact_value(value)),
377        FactState::Unknown => EngineFactStateV1::Unknown,
378        FactState::NotApplicable => EngineFactStateV1::NotApplicable,
379    }
380}
381
382fn fact_value(value: &FactValue) -> EngineFactValueV1 {
383    match value {
384        FactValue::AcceptedFormats(formats) => EngineFactValueV1::AcceptedFormats(formats.clone()),
385        FactValue::AnimationAddressability(AnimationAddressability::GltfAssetLabel) => {
386            EngineFactValueV1::AnimationAddressability(
387                EngineAnimationAddressabilityV1::GltfAssetLabel,
388            )
389        }
390        FactValue::CoordinateBasis(value) => {
391            EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
392                handedness: match value.handedness {
393                    Handedness::Left => EngineHandednessV1::Left,
394                    Handedness::Right => EngineHandednessV1::Right,
395                },
396                up_axis: match value.up_axis {
397                    UpAxis::X => EngineUpAxisV1::X,
398                    UpAxis::Y => EngineUpAxisV1::Y,
399                    UpAxis::Z => EngineUpAxisV1::Z,
400                },
401                forward_axis: match value.forward_axis {
402                    ForwardAxis::PositiveX => EngineForwardAxisV1::PositiveX,
403                    ForwardAxis::NegativeX => EngineForwardAxisV1::NegativeX,
404                    ForwardAxis::PositiveY => EngineForwardAxisV1::PositiveY,
405                    ForwardAxis::NegativeY => EngineForwardAxisV1::NegativeY,
406                    ForwardAxis::PositiveZ => EngineForwardAxisV1::PositiveZ,
407                    ForwardAxis::NegativeZ => EngineForwardAxisV1::NegativeZ,
408                },
409            })
410        }
411        FactValue::LinearUnit(value) => EngineFactValueV1::LinearUnit(match value {
412            LinearUnit::Metre => EngineLinearUnitV1::Metre,
413            LinearUnit::Centimetre => EngineLinearUnitV1::Centimetre,
414        }),
415        FactValue::ConversionControl(value) => EngineFactValueV1::ConversionControl(match value {
416            ConversionControl::ProfileSetting(id) => {
417                EngineConversionControlV1::ProfileSetting(setting_id(*id))
418            }
419            ConversionControl::ImporterOption => EngineConversionControlV1::ImporterOption,
420        }),
421        FactValue::Boolean(value) => EngineFactValueV1::Boolean(*value),
422        FactValue::ImportHandling(value) => EngineFactValueV1::ImportHandling(match value {
423            ImportHandling::Preserved => EngineImportHandlingV1::Preserved,
424            ImportHandling::Converted => EngineImportHandlingV1::Converted,
425            ImportHandling::Discarded => EngineImportHandlingV1::Discarded,
426            ImportHandling::Unsupported => EngineImportHandlingV1::Unsupported,
427        }),
428        FactValue::TargetAddressability(TargetAddressability::NamePathDerivedId) => {
429            EngineFactValueV1::TargetAddressability(EngineTargetAddressabilityV1::NamePathDerivedId)
430        }
431        FactValue::RootMotionAddressability(value) => {
432            EngineFactValueV1::RootMotionAddressability(match value {
433                RootMotionAddressability::ExactSourceTransformPath => {
434                    EngineRootMotionAddressabilityV1::ExactSourceTransformPath
435                }
436                RootMotionAddressability::HumanoidAvatarBody => {
437                    EngineRootMotionAddressabilityV1::HumanoidAvatarBody
438                }
439            })
440        }
441    }
442}
443
444fn setting_value(value: &SettingValue) -> EngineSettingValueV1 {
445    match value {
446        SettingValue::Boolean(value) => EngineSettingValueV1::Boolean(*value),
447        SettingValue::BakeOrExtract(value) => EngineSettingValueV1::BakeOrExtract(match value {
448            BakeOrExtract::Bake => EngineBakeOrExtractV1::Bake,
449            BakeOrExtract::Extract => EngineBakeOrExtractV1::Extract,
450        }),
451        SettingValue::SourceTransformPath(value) => {
452            EngineSettingValueV1::SourceTransformPath(value.clone())
453        }
454    }
455}
456
457const fn fact_id(id: crate::FactId) -> EngineFactIdV1 {
458    match id {
459        crate::FactId::AcceptedInputs => EngineFactIdV1::AcceptedInputs,
460        crate::FactId::AnimationAddressability => EngineFactIdV1::AnimationAddressability,
461        crate::FactId::TargetCoordinateBasis => EngineFactIdV1::TargetCoordinateBasis,
462        crate::FactId::TargetLinearUnit => EngineFactIdV1::TargetLinearUnit,
463        crate::FactId::UnitConversionControl => EngineFactIdV1::UnitConversionControl,
464        crate::FactId::AxisConversionControl => EngineFactIdV1::AxisConversionControl,
465        crate::FactId::ExactAxisConversion => EngineFactIdV1::ExactAxisConversion,
466        crate::FactId::ResultingHierarchyScale => EngineFactIdV1::ResultingHierarchyScale,
467        crate::FactId::WholeEndFrameRequired => EngineFactIdV1::WholeEndFrameRequired,
468        crate::FactId::AnimationChannelHandling => EngineFactIdV1::AnimationChannelHandling,
469        crate::FactId::ExtensionHandling => EngineFactIdV1::ExtensionHandling,
470        crate::FactId::ConstructHandling => EngineFactIdV1::ConstructHandling,
471        crate::FactId::AnimationTargetAddressability => {
472            EngineFactIdV1::AnimationTargetAddressability
473        }
474        crate::FactId::RootMotionAddressability => EngineFactIdV1::RootMotionAddressability,
475    }
476}
477
478const fn setting_id(id: SettingId) -> EngineSettingIdV1 {
479    match id {
480        SettingId::ConvertUnits => EngineSettingIdV1::ConvertUnits,
481        SettingId::BakeAxisConversion => EngineSettingIdV1::BakeAxisConversion,
482        SettingId::RootMotionSource => EngineSettingIdV1::RootMotionSource,
483        SettingId::RootRotation => EngineSettingIdV1::RootRotation,
484        SettingId::RootPositionY => EngineSettingIdV1::RootPositionY,
485        SettingId::RootPositionXz => EngineSettingIdV1::RootPositionXz,
486    }
487}
488
489const fn setting_id_v2(id: SettingIdV2) -> CoreSettingIdV2 {
490    match id {
491        SettingIdV2::AnimationType => CoreSettingIdV2::AnimationType,
492        SettingIdV2::AvatarSetup => CoreSettingIdV2::AvatarSetup,
493        SettingIdV2::ImportAnimation => CoreSettingIdV2::ImportAnimation,
494        SettingIdV2::RootMotionSource => CoreSettingIdV2::RootMotionSource,
495        SettingIdV2::RootRotation => CoreSettingIdV2::RootRotation,
496        SettingIdV2::RootPositionY => CoreSettingIdV2::RootPositionY,
497        SettingIdV2::RootPositionXz => CoreSettingIdV2::RootPositionXz,
498        SettingIdV2::RotateSceneEntity => CoreSettingIdV2::RotateSceneEntity,
499        SettingIdV2::RotateMeshes => CoreSettingIdV2::RotateMeshes,
500        SettingIdV2::LoadMeshes => CoreSettingIdV2::LoadMeshes,
501        SettingIdV2::ExtensionHandlerEnvironment => CoreSettingIdV2::ExtensionHandlerEnvironment,
502        SettingIdV2::BevyAnimationFeature => CoreSettingIdV2::BevyAnimationFeature,
503        SettingIdV2::LoadAnimations => CoreSettingIdV2::LoadAnimations,
504        SettingIdV2::AnimationFps => CoreSettingIdV2::AnimationFps,
505        SettingIdV2::AnimationTrimming => CoreSettingIdV2::AnimationTrimming,
506        SettingIdV2::SampleRate => CoreSettingIdV2::SampleRate,
507    }
508}
509
510fn setting_value_v2(value: &SettingValueV2) -> CoreSettingValueV2 {
511    match value {
512        SettingValueV2::Boolean(value) => CoreSettingValueV2::Boolean(*value),
513        SettingValueV2::BakeOrExtract(value) => CoreSettingValueV2::BakeOrExtract(match value {
514            crate::BakeOrExtract::Bake => {
515                animsmith_core::engine_contract::EngineBakeOrExtractV1::Bake
516            }
517            crate::BakeOrExtract::Extract => {
518                animsmith_core::engine_contract::EngineBakeOrExtractV1::Extract
519            }
520        }),
521        SettingValueV2::SourceTransformPath(value) => {
522            CoreSettingValueV2::SourceTransformPath(value.clone())
523        }
524        SettingValueV2::AnimationType(value) => CoreSettingValueV2::Token(value.as_str().into()),
525        SettingValueV2::AvatarSetup(value) => CoreSettingValueV2::Token(value.as_str().into()),
526        SettingValueV2::LoadMeshesState(value) => CoreSettingValueV2::Token(
527            match value {
528                crate::BevyLoadMeshesStateV2::Empty => "empty",
529                crate::BevyLoadMeshesStateV2::Nonempty => "nonempty",
530            }
531            .into(),
532        ),
533        SettingValueV2::HandlerEnvironment(value) => {
534            CoreSettingValueV2::Token(value.as_str().into())
535        }
536        SettingValueV2::PositiveInteger(value) => CoreSettingValueV2::PositiveInteger(*value),
537        SettingValueV2::SampleRate(value) => CoreSettingValueV2::SampleRate(match value {
538            UnrealSampleRateV2::Default30 => CoreSampleRateV2::Default30,
539            UnrealSampleRateV2::SourceDetermined => CoreSampleRateV2::SourceDetermined,
540            UnrealSampleRateV2::CustomHz(value) => CoreSampleRateV2::CustomHz(*value),
541        }),
542    }
543}
544
545pub(crate) const fn format_name(format: SourceFormatV1) -> &'static str {
546    match format {
547        SourceFormatV1::GltfJson => "gltf_json",
548        SourceFormatV1::Glb => "glb",
549        SourceFormatV1::Fbx => "fbx",
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::{
557        FactId, ProfileFact, SettingDescriptor, SettingDomainV2, SettingValueV2,
558        types::UnrealSampleRateV2,
559    };
560
561    #[test]
562    fn advice_setting_values_map_to_the_closed_core_vocabulary_and_bounds() {
563        assert_eq!(
564            setting_value_v2(&SettingValueV2::PositiveInteger(120)),
565            CoreSettingValueV2::PositiveInteger(120)
566        );
567        assert!(
568            !SettingValueV2::PositiveInteger(0).matches_domain(SettingDomainV2::PositiveInteger)
569        );
570        assert!(
571            !SettingValueV2::PositiveInteger(121).matches_domain(SettingDomainV2::PositiveInteger)
572        );
573        for (public, core) in [
574            (UnrealSampleRateV2::Default30, CoreSampleRateV2::Default30),
575            (
576                UnrealSampleRateV2::SourceDetermined,
577                CoreSampleRateV2::SourceDetermined,
578            ),
579            (
580                UnrealSampleRateV2::CustomHz(48_000),
581                CoreSampleRateV2::CustomHz(48_000),
582            ),
583        ] {
584            assert!(SettingValueV2::SampleRate(public).matches_domain(SettingDomainV2::SampleRate));
585            assert_eq!(
586                setting_value_v2(&SettingValueV2::SampleRate(public)),
587                CoreSettingValueV2::SampleRate(core)
588            );
589        }
590        for invalid in [0, 48_001] {
591            assert!(
592                !SettingValueV2::SampleRate(UnrealSampleRateV2::CustomHz(invalid))
593                    .matches_domain(SettingDomainV2::SampleRate)
594            );
595        }
596    }
597
598    #[test]
599    fn facts_encoding_sorts_every_repeated_stable_id() {
600        for original in crate::profiles_v1() {
601            let expected = facts_identity(original);
602            let mut reordered = original.clone();
603            let accepted = reordered
604                .facts
605                .iter_mut()
606                .find(|fact| fact.id() == FactId::AcceptedInputs)
607                .unwrap();
608            if let FactState::Known(FactValue::AcceptedFormats(formats)) = accepted.state() {
609                let mut formats = formats.clone();
610                formats.reverse();
611                *accepted = ProfileFact::new(
612                    FactId::AcceptedInputs,
613                    FactState::Known(FactValue::AcceptedFormats(formats)),
614                );
615            }
616            reordered.facts.reverse();
617            reordered.settings.reverse();
618            reordered.sources.reverse();
619            for source in &mut reordered.sources {
620                source.reverse_supported_ids_for_test();
621            }
622            assert_eq!(
623                facts_identity(&reordered),
624                expected,
625                "{:?}",
626                original.selection()
627            );
628        }
629    }
630
631    #[test]
632    fn facts_encoding_is_sensitive_to_each_record_domain() {
633        let original = crate::profiles_v1()
634            .iter()
635            .find(|profile| profile.selection.family() == "unity-generic")
636            .unwrap()
637            .clone();
638        let expected = facts_identity(&original);
639        let changed = |profile: &EngineProfile| {
640            if let Ok(projected) = project_profile(profile) {
641                assert_ne!(projected.facts_identity(), &expected);
642            }
643        };
644
645        let mut profile = original.clone();
646        profile.selection = crate::ProfileSelection::new(
647            "unity-generic-changed",
648            1,
649            "6000.3",
650            "fbx-model-importer",
651        );
652        changed(&profile);
653
654        let mut profile = original.clone();
655        profile.selection =
656            crate::ProfileSelection::new("unity-generic", 2, "6000.3", "fbx-model-importer");
657        changed(&profile);
658
659        let mut profile = original.clone();
660        profile.selection =
661            crate::ProfileSelection::new("unity-generic", 1, "6000.4", "fbx-model-importer");
662        changed(&profile);
663
664        let mut profile = original.clone();
665        profile.selection =
666            crate::ProfileSelection::new("unity-generic", 1, "6000.3", "other-importer");
667        changed(&profile);
668
669        let mut profile = original.clone();
670        profile.fact_bundle_urn = "urn:animsmith:engine-profile:unity-generic:changed";
671        changed(&profile);
672
673        let mut profile = original.clone();
674        let fact = profile
675            .facts
676            .iter_mut()
677            .find(|fact| fact.id() == FactId::ExactAxisConversion)
678            .unwrap();
679        *fact = ProfileFact::new(FactId::ExactAxisConversion, FactState::NotApplicable);
680        changed(&profile);
681
682        let mut profile = original.clone();
683        let fact = profile
684            .facts
685            .iter_mut()
686            .find(|fact| fact.id() == FactId::AcceptedInputs)
687            .unwrap();
688        *fact = ProfileFact::new(
689            FactId::AcceptedInputs,
690            FactState::Known(FactValue::AcceptedFormats(vec![
691                SourceFormatV1::Fbx,
692                SourceFormatV1::Glb,
693            ])),
694        );
695        changed(&profile);
696
697        let mut profile = original.clone();
698        let descriptor = profile.settings[0].clone();
699        profile.settings[0] = SettingDescriptor::new(
700            descriptor.id(),
701            descriptor.scope(),
702            crate::SettingDomain::BakeOrExtract,
703            descriptor.applicability(),
704            descriptor.default_status(),
705        );
706        changed(&profile);
707
708        let mut profile = original.clone();
709        let descriptor = profile.settings[0].clone();
710        profile.settings[0] = SettingDescriptor::new(
711            descriptor.id(),
712            crate::SettingScope::Clip,
713            descriptor.domain(),
714            descriptor.applicability(),
715            descriptor.default_status(),
716        );
717        changed(&profile);
718
719        let mut profile = original.clone();
720        let descriptor = profile.settings[0].clone();
721        profile.settings[0] = SettingDescriptor::new(
722            descriptor.id(),
723            descriptor.scope(),
724            descriptor.domain(),
725            crate::SettingApplicability::NotApplicable,
726            descriptor.default_status(),
727        );
728        changed(&profile);
729
730        let mut profile = original.clone();
731        let descriptor = profile.settings[0].clone();
732        profile.settings[0] = SettingDescriptor::new(
733            descriptor.id(),
734            descriptor.scope(),
735            descriptor.domain(),
736            descriptor.applicability(),
737            crate::DefaultStatus::NotApplicable,
738        );
739        changed(&profile);
740
741        let mut profile = original.clone();
742        let source = profile.sources[0].clone();
743        profile.sources[0] = crate::PrimarySource::new(
744            "changed-source-id",
745            source.target_version(),
746            source.url(),
747            source.verified_on(),
748            source.supported_facts().to_vec(),
749            source.supported_settings().to_vec(),
750        );
751        changed(&profile);
752
753        let replace_source = |profile: &mut EngineProfile,
754                              target_version,
755                              url,
756                              verified_on,
757                              facts: Vec<_>,
758                              settings: Vec<_>| {
759            let source = profile.sources[0].clone();
760            profile.sources[0] = crate::PrimarySource::new(
761                source.id(),
762                target_version,
763                url,
764                verified_on,
765                facts,
766                settings,
767            );
768        };
769
770        let mut profile = original.clone();
771        let source = profile.sources[0].clone();
772        replace_source(
773            &mut profile,
774            "changed-version",
775            source.url(),
776            source.verified_on(),
777            source.supported_facts().to_vec(),
778            source.supported_settings().to_vec(),
779        );
780        changed(&profile);
781
782        let mut profile = original.clone();
783        let source = profile.sources[0].clone();
784        replace_source(
785            &mut profile,
786            source.target_version(),
787            "https://example.invalid/changed",
788            source.verified_on(),
789            source.supported_facts().to_vec(),
790            source.supported_settings().to_vec(),
791        );
792        changed(&profile);
793
794        let mut profile = original.clone();
795        let source = profile.sources[0].clone();
796        replace_source(
797            &mut profile,
798            source.target_version(),
799            source.url(),
800            "2099-01-01",
801            source.supported_facts().to_vec(),
802            source.supported_settings().to_vec(),
803        );
804        changed(&profile);
805
806        let mut profile = original.clone();
807        let source = profile.sources[0].clone();
808        let mut facts = source.supported_facts().to_vec();
809        facts.push(FactId::AcceptedInputs);
810        replace_source(
811            &mut profile,
812            source.target_version(),
813            source.url(),
814            source.verified_on(),
815            facts,
816            source.supported_settings().to_vec(),
817        );
818        changed(&profile);
819
820        let mut profile = original.clone();
821        let source = profile.sources[0].clone();
822        let mut settings = source.supported_settings().to_vec();
823        settings.pop();
824        replace_source(
825            &mut profile,
826            source.target_version(),
827            source.url(),
828            source.verified_on(),
829            source.supported_facts().to_vec(),
830            settings,
831        );
832        changed(&profile);
833
834        let unreal = crate::profiles_v1()
835            .iter()
836            .find(|profile| profile.selection.family() == "unreal")
837            .unwrap();
838        let expected = facts_identity(unreal);
839        let mut profile = unreal.clone();
840        let fact = profile
841            .facts
842            .iter_mut()
843            .find(|fact| fact.id() == FactId::TargetCoordinateBasis)
844            .unwrap();
845        *fact = ProfileFact::new(
846            FactId::TargetCoordinateBasis,
847            FactState::Known(FactValue::CoordinateBasis(crate::CoordinateBasis {
848                handedness: crate::Handedness::Left,
849                up_axis: crate::UpAxis::Z,
850                forward_axis: crate::ForwardAxis::NegativeX,
851            })),
852        );
853        assert_ne!(facts_identity(&profile), expected);
854    }
855}