1use super::{
4 ScaleError, ScaleOperation, ScalePlan, ScaleProjectedRole, ScaleSourceNodeKind,
5 ScaleTolerancePolicy,
6};
7use crate::model::{Document, SourceNodeLocalRest};
8use serde::Serialize;
9use std::collections::BTreeSet;
10
11pub const ASSEMBLY_SCALE_BASIS_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct AssemblyScaleNamedNode {
17 pub name: String,
19 pub parent: Option<String>,
21 pub translation_bits: [u32; 3],
23 pub rotation_bits: [u32; 4],
25 pub scale_bits: [u32; 3],
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31pub struct AssemblyScaleSourceNode {
32 pub source_node_index: usize,
34 pub parent_source_node_index: Option<usize>,
36 pub name: Option<String>,
38 pub role: String,
40 pub local_rest: AssemblyScaleSourceRest,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46#[serde(tag = "kind", rename_all = "snake_case")]
47pub enum AssemblyScaleSourceRest {
48 Trs {
50 translation_bits: [u32; 3],
52 rotation_bits: [u32; 4],
54 scale_bits: [u32; 3],
56 },
57 Matrix {
59 matrix_bits: [u32; 16],
61 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct AssemblyScaleTargetPath {
67 pub clip_index: usize,
69 pub track_index: usize,
71 pub bone: String,
73 pub property: &'static str,
75 pub factor_bits: u64,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct AssemblyScaleBasis {
82 pub version: u32,
84 pub coordinate_convention: &'static str,
86 pub tolerance_policy_id: &'static str,
88 pub source_skin_index: usize,
90 pub source_root_node_index: usize,
92 pub expected_factor_bits: u64,
94 pub named_nodes: Vec<AssemblyScaleNamedNode>,
96 pub source_nodes: Vec<AssemblyScaleSourceNode>,
98 pub target_paths: Vec<AssemblyScaleTargetPath>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[error("assembly scale basis mismatch ({reason})")]
105pub struct AssemblyScaleCompatibilityError {
106 pub reason: &'static str,
108}
109
110#[derive(Debug, Clone, Copy)]
116#[non_exhaustive]
117pub enum AssemblyScaleSelectorRequest<'a> {
118 Indexed,
120 Named {
122 root_node_name: &'a str,
124 },
125}
126
127#[derive(Debug, Clone)]
128enum AssemblyScaleSelectorIdentity {
129 Indexed,
130 Named {
131 root_node_name: String,
132 skin_joint_names: Vec<String>,
133 },
134}
135
136#[derive(Debug, Clone)]
141pub struct AssemblyScaleCompatibilityBasis {
142 basis: AssemblyScaleBasis,
143 selector: AssemblyScaleSelectorIdentity,
144}
145
146impl AssemblyScaleCompatibilityBasis {
147 #[must_use]
149 pub fn basis(&self) -> &AssemblyScaleBasis {
150 &self.basis
151 }
152}
153
154pub fn assembly_scale_basis(
161 document: &Document,
162 plan: &ScalePlan,
163) -> Result<AssemblyScaleBasis, ScaleError> {
164 plan.validate_document_inventory(document)?;
165 let ScaleOperation::RestBindUniformScale {
166 source_skin_index,
167 source_root_node_index,
168 expected_factor,
169 } = plan.operation()
170 else {
171 return Err(ScaleError::PlanDocumentMismatch {
172 reason: "assembly_basis_requires_rest_bind",
173 });
174 };
175 let mut names = BTreeSet::new();
176 let mut named_nodes = Vec::with_capacity(document.skeleton.bones.len());
177 for (index, bone) in document.skeleton.bones.iter().enumerate() {
178 if bone.name.is_empty() || !names.insert(bone.name.as_str()) {
179 return Err(ScaleError::PlanDocumentMismatch {
180 reason: "assembly_basis_requires_unique_named_nodes",
181 });
182 }
183 named_nodes.push(AssemblyScaleNamedNode {
184 name: bone.name.clone(),
185 parent: bone
186 .parent
187 .and_then(|parent| document.skeleton.bones.get(parent))
188 .map(|parent| parent.name.clone()),
189 translation_bits: bone.rest.translation.to_array().map(f32::to_bits),
190 rotation_bits: bone.rest.rotation.to_array().map(f32::to_bits),
191 scale_bits: bone.rest.scale.to_array().map(f32::to_bits),
192 });
193 if bone.parent.is_some_and(|parent| parent >= index) {
194 return Err(ScaleError::PlanDocumentMismatch {
195 reason: "assembly_basis_parent_order",
196 });
197 }
198 }
199 let source_by_index = document
200 .assets
201 .source_skeleton
202 .nodes
203 .iter()
204 .map(|node| (node.source_node_index, node))
205 .collect::<std::collections::BTreeMap<_, _>>();
206 let mut source_nodes = Vec::new();
207 for row in plan.ledger().source_topology() {
208 let source = source_by_index.get(&row.source_node_index()).ok_or(
209 ScaleError::PlanDocumentMismatch {
210 reason: "assembly_basis_source_node_missing",
211 },
212 )?;
213 let local_rest = match source.local_rest {
214 SourceNodeLocalRest::Trs {
215 translation,
216 rotation,
217 scale,
218 } => AssemblyScaleSourceRest::Trs {
219 translation_bits: translation.to_array().map(f32::to_bits),
220 rotation_bits: rotation.to_array().map(f32::to_bits),
221 scale_bits: scale.to_array().map(f32::to_bits),
222 },
223 SourceNodeLocalRest::Matrix(matrix) => AssemblyScaleSourceRest::Matrix {
224 matrix_bits: matrix.to_cols_array().map(f32::to_bits),
225 },
226 };
227 let role = match row.kind() {
228 ScaleSourceNodeKind::Projected { role, .. } => match role {
229 ScaleProjectedRole::Root => "projected-root",
230 ScaleProjectedRole::Joint => "projected-joint",
231 ScaleProjectedRole::TransformOnly => "projected-transform-only",
232 },
233 ScaleSourceNodeKind::Connector => "connector",
234 ScaleSourceNodeKind::OutsideDomain { bone: Some(_) } => "outside-projected",
235 ScaleSourceNodeKind::OutsideDomain { bone: None } => "outside-helper",
236 };
237 source_nodes.push(AssemblyScaleSourceNode {
238 source_node_index: row.source_node_index(),
239 parent_source_node_index: row.parent_source_node_index(),
240 name: source.name.clone(),
241 role: role.to_owned(),
242 local_rest,
243 });
244 }
245 let mut target_paths = Vec::new();
246 for (clip_index, clip) in document.clips.iter().enumerate() {
247 for (track_index, track) in clip.tracks.iter().enumerate() {
248 let bone = document
249 .skeleton
250 .bones
251 .get(track.bone)
252 .ok_or(ScaleError::BoneIndexOutOfRange { index: track.bone })?;
253 target_paths.push(AssemblyScaleTargetPath {
254 clip_index,
255 track_index,
256 bone: bone.name.clone(),
257 property: track.property.as_str(),
258 factor_bits: plan
259 .animation_target_factor_unchecked(document, track.bone, track.property)?
260 .to_bits(),
261 });
262 }
263 }
264 Ok(AssemblyScaleBasis {
265 version: ASSEMBLY_SCALE_BASIS_VERSION,
266 coordinate_convention: "right-handed-y-up-metres",
267 tolerance_policy_id: plan.tolerance_policy().id,
268 source_skin_index,
269 source_root_node_index,
270 expected_factor_bits: expected_factor.to_bits(),
271 named_nodes,
272 source_nodes,
273 target_paths,
274 })
275}
276
277pub fn assembly_scale_compatibility_basis(
286 document: &Document,
287 plan: &ScalePlan,
288 selector: AssemblyScaleSelectorRequest<'_>,
289) -> Result<AssemblyScaleCompatibilityBasis, ScaleError> {
290 let basis = assembly_scale_basis(document, plan)?;
293 let selector = match selector {
294 AssemblyScaleSelectorRequest::Indexed => AssemblyScaleSelectorIdentity::Indexed,
295 AssemblyScaleSelectorRequest::Named { root_node_name } => {
296 let root_matches = document
297 .assets
298 .source_skeleton
299 .nodes
300 .iter()
301 .filter(|node| {
302 node.bone
303 .and_then(|bone| document.skeleton.bones.get(bone))
304 .is_some_and(|bone| bone.name == root_node_name)
305 })
306 .collect::<Vec<_>>();
307 let [root] = root_matches.as_slice() else {
308 return Err(ScaleError::PlanDocumentMismatch {
309 reason: "assembly_basis_named_selector_root_not_unique",
310 });
311 };
312 if root.source_node_index != basis.source_root_node_index {
313 return Err(ScaleError::PlanDocumentMismatch {
314 reason: "assembly_basis_named_selector_root_disagrees_with_plan",
315 });
316 }
317 let skin_matches = document
318 .assets
319 .source_skeleton
320 .skins
321 .iter()
322 .filter(|skin| {
323 skin.joint_source_node_indices
324 .contains(&root.source_node_index)
325 })
326 .collect::<Vec<_>>();
327 let [skin] = skin_matches.as_slice() else {
328 return Err(ScaleError::PlanDocumentMismatch {
329 reason: "assembly_basis_named_selector_skin_not_unique",
330 });
331 };
332 if skin.source_skin_index != basis.source_skin_index {
333 return Err(ScaleError::PlanDocumentMismatch {
334 reason: "assembly_basis_named_selector_skin_disagrees_with_plan",
335 });
336 }
337 let source_nodes = document
338 .assets
339 .source_skeleton
340 .nodes
341 .iter()
342 .map(|node| (node.source_node_index, node))
343 .collect::<std::collections::BTreeMap<_, _>>();
344 let skin_joint_names = skin
345 .joint_source_node_indices
346 .iter()
347 .map(|source_index| {
348 source_nodes
349 .get(source_index)
350 .and_then(|node| node.bone)
351 .and_then(|bone| document.skeleton.bones.get(bone))
352 .map(|bone| bone.name.clone())
353 .ok_or(ScaleError::PlanDocumentMismatch {
354 reason: "assembly_basis_named_selector_joint_has_no_name",
355 })
356 })
357 .collect::<Result<Vec<_>, _>>()?;
358 AssemblyScaleSelectorIdentity::Named {
359 root_node_name: root_node_name.to_owned(),
360 skin_joint_names,
361 }
362 }
363 };
364 Ok(AssemblyScaleCompatibilityBasis { basis, selector })
365}
366
367pub fn require_assembly_scale_compatibility(
377 base: &AssemblyScaleBasis,
378 input: &AssemblyScaleBasis,
379) -> Result<(), AssemblyScaleCompatibilityError> {
380 require_assembly_scale_compatibility_inner(
381 base,
382 &AssemblyScaleSelectorIdentity::Indexed,
383 input,
384 &AssemblyScaleSelectorIdentity::Indexed,
385 )
386}
387
388pub fn require_assembly_scale_compatibility_with_selectors(
395 base: &AssemblyScaleCompatibilityBasis,
396 input: &AssemblyScaleCompatibilityBasis,
397) -> Result<(), AssemblyScaleCompatibilityError> {
398 require_assembly_scale_compatibility_inner(
399 &base.basis,
400 &base.selector,
401 &input.basis,
402 &input.selector,
403 )
404}
405
406fn require_assembly_scale_compatibility_inner(
407 base: &AssemblyScaleBasis,
408 base_selector: &AssemblyScaleSelectorIdentity,
409 input: &AssemblyScaleBasis,
410 input_selector: &AssemblyScaleSelectorIdentity,
411) -> Result<(), AssemblyScaleCompatibilityError> {
412 let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
413 let named_selectors = match (base_selector, input_selector) {
414 (AssemblyScaleSelectorIdentity::Indexed, AssemblyScaleSelectorIdentity::Indexed) => None,
415 (
416 AssemblyScaleSelectorIdentity::Named {
417 root_node_name: base_root,
418 skin_joint_names: base_joints,
419 },
420 AssemblyScaleSelectorIdentity::Named {
421 root_node_name: input_root,
422 skin_joint_names: input_joints,
423 },
424 ) => Some((base_root, base_joints, input_root, input_joints)),
425 _ => {
426 return Err(AssemblyScaleCompatibilityError {
427 reason: "source-selector-mode",
428 });
429 }
430 };
431 let mismatch = if base.version != input.version {
432 Some("basis-version")
433 } else if base.coordinate_convention != input.coordinate_convention {
434 Some("coordinate-convention")
435 } else if base.tolerance_policy_id != input.tolerance_policy_id
436 || base.tolerance_policy_id != tolerance.id
437 {
438 Some("tolerance-policy")
439 } else if named_selectors.is_none() && base.source_skin_index != input.source_skin_index {
440 Some("source-skin-selector")
441 } else if named_selectors.is_none()
442 && base.source_root_node_index != input.source_root_node_index
443 {
444 Some("source-root-selector")
445 } else if named_selectors.is_some_and(|(base_root, base_joints, input_root, input_joints)| {
446 base_root != input_root || base_joints != input_joints
447 }) {
448 Some("source-name-selector")
449 } else if base.expected_factor_bits != input.expected_factor_bits {
450 Some("expected-factor")
451 } else if !same_named_topology(&base.named_nodes, &input.named_nodes) {
452 Some("named-topology")
453 } else if !same_named_rest(&base.named_nodes, &input.named_nodes, &tolerance) {
454 Some("named-rest-basis")
455 } else if !same_named_orientations(&base.named_nodes, &input.named_nodes, &tolerance) {
456 Some("named-orientation")
457 } else if (named_selectors.is_some()
458 && !same_named_source_layout(&base.source_nodes, &input.source_nodes))
459 || (named_selectors.is_none()
460 && !same_source_layout(&base.source_nodes, &input.source_nodes))
461 {
462 Some("source-helper-layout")
463 } else if (named_selectors.is_some()
464 && !same_named_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
465 || (named_selectors.is_none()
466 && !same_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
467 {
468 Some("source-helper-rest-basis")
469 } else {
470 None
471 };
472 mismatch.map_or(Ok(()), |reason| {
473 Err(AssemblyScaleCompatibilityError { reason })
474 })
475}
476
477fn same_named_topology(base: &[AssemblyScaleNamedNode], input: &[AssemblyScaleNamedNode]) -> bool {
478 base.len() == input.len()
479 && base
480 .iter()
481 .zip(input)
482 .all(|(base, input)| base.name == input.name && base.parent == input.parent)
483}
484
485fn same_named_rest(
486 base: &[AssemblyScaleNamedNode],
487 input: &[AssemblyScaleNamedNode],
488 tolerance: &ScaleTolerancePolicy,
489) -> bool {
490 base.iter().zip(input).all(|(base, input)| {
491 close_f32_bits(&base.translation_bits, &input.translation_bits, tolerance)
492 && close_f32_bits(&base.scale_bits, &input.scale_bits, tolerance)
493 })
494}
495
496fn same_named_orientations(
497 base: &[AssemblyScaleNamedNode],
498 input: &[AssemblyScaleNamedNode],
499 tolerance: &ScaleTolerancePolicy,
500) -> bool {
501 base.iter()
502 .zip(input)
503 .all(|(base, input)| same_quaternion(&base.rotation_bits, &input.rotation_bits, tolerance))
504}
505
506fn same_source_layout(base: &[AssemblyScaleSourceNode], input: &[AssemblyScaleSourceNode]) -> bool {
507 base.len() == input.len()
508 && base.iter().zip(input).all(|(base, input)| {
509 base.source_node_index == input.source_node_index
510 && base.parent_source_node_index == input.parent_source_node_index
511 && base.name == input.name
512 && base.role == input.role
513 && std::mem::discriminant(&base.local_rest)
514 == std::mem::discriminant(&input.local_rest)
515 })
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
519struct NamedSourcePath(Vec<(Option<String>, String, bool)>);
520
521fn named_source_paths(nodes: &[AssemblyScaleSourceNode]) -> Option<Vec<NamedSourcePath>> {
522 let by_index = nodes
523 .iter()
524 .enumerate()
525 .map(|(position, node)| (node.source_node_index, position))
526 .collect::<std::collections::BTreeMap<_, _>>();
527 nodes
528 .iter()
529 .map(|node| {
530 let mut path = Vec::new();
531 let mut current = Some(node.source_node_index);
532 for _ in 0..=nodes.len() {
533 let Some(index) = current else {
534 path.reverse();
535 return Some(NamedSourcePath(path));
536 };
537 let row = nodes.get(*by_index.get(&index)?)?;
538 path.push((
539 row.name.clone(),
540 row.role.clone(),
541 matches!(&row.local_rest, AssemblyScaleSourceRest::Matrix { .. }),
542 ));
543 current = row.parent_source_node_index;
544 }
545 None
546 })
547 .collect()
548}
549
550fn same_named_source_layout(
551 base: &[AssemblyScaleSourceNode],
552 input: &[AssemblyScaleSourceNode],
553) -> bool {
554 let (Some(mut base), Some(mut input)) = (named_source_paths(base), named_source_paths(input))
555 else {
556 return false;
557 };
558 base.sort();
559 input.sort();
560 base == input
561}
562
563fn same_named_source_rest(
564 base: &[AssemblyScaleSourceNode],
565 input: &[AssemblyScaleSourceNode],
566 tolerance: &ScaleTolerancePolicy,
567) -> bool {
568 let (Some(base_paths), Some(input_paths)) =
569 (named_source_paths(base), named_source_paths(input))
570 else {
571 return false;
572 };
573 let mut matched = vec![false; input.len()];
574 base.iter().zip(base_paths).all(|(base_node, base_path)| {
575 input
576 .iter()
577 .zip(&input_paths)
578 .enumerate()
579 .find(|(index, (input_node, input_path))| {
580 !matched[*index]
581 && **input_path == base_path
582 && same_source_rest_node(base_node, input_node, tolerance)
583 })
584 .is_some_and(|(index, _)| {
585 matched[index] = true;
586 true
587 })
588 })
589}
590
591fn same_source_rest_node(
592 base: &AssemblyScaleSourceNode,
593 input: &AssemblyScaleSourceNode,
594 tolerance: &ScaleTolerancePolicy,
595) -> bool {
596 same_source_rest(
597 std::slice::from_ref(base),
598 std::slice::from_ref(input),
599 tolerance,
600 )
601}
602
603fn same_source_rest(
604 base: &[AssemblyScaleSourceNode],
605 input: &[AssemblyScaleSourceNode],
606 tolerance: &ScaleTolerancePolicy,
607) -> bool {
608 base.iter().zip(input).all(
609 |(base, input)| match (&base.local_rest, &input.local_rest) {
610 (
611 AssemblyScaleSourceRest::Trs {
612 translation_bits: base_translation,
613 rotation_bits: base_rotation,
614 scale_bits: base_scale,
615 },
616 AssemblyScaleSourceRest::Trs {
617 translation_bits: input_translation,
618 rotation_bits: input_rotation,
619 scale_bits: input_scale,
620 },
621 ) => {
622 close_f32_bits(base_translation, input_translation, tolerance)
623 && close_f32_bits(base_scale, input_scale, tolerance)
624 && same_quaternion(base_rotation, input_rotation, tolerance)
625 }
626 (
627 AssemblyScaleSourceRest::Matrix {
628 matrix_bits: base_matrix,
629 },
630 AssemblyScaleSourceRest::Matrix {
631 matrix_bits: input_matrix,
632 },
633 ) => close_f32_bits(base_matrix, input_matrix, tolerance),
634 _ => false,
635 },
636 )
637}
638
639fn close_f32_bits<const N: usize>(
640 base: &[u32; N],
641 input: &[u32; N],
642 tolerance: &ScaleTolerancePolicy,
643) -> bool {
644 base.iter().zip(input).all(|(&base, &input)| {
645 close_f64(
646 f32::from_bits(base) as f64,
647 f32::from_bits(input) as f64,
648 tolerance,
649 )
650 })
651}
652
653fn close_f64(base: f64, input: f64, tolerance: &ScaleTolerancePolicy) -> bool {
654 base.is_finite()
655 && input.is_finite()
656 && (base - input).abs()
657 <= tolerance.scalar_absolute + tolerance.scalar_relative * base.abs().max(input.abs())
658}
659
660fn same_quaternion(base: &[u32; 4], input: &[u32; 4], tolerance: &ScaleTolerancePolicy) -> bool {
661 let base = base.map(|bits| f32::from_bits(bits) as f64);
662 let input = input.map(|bits| f32::from_bits(bits) as f64);
663 if !base
664 .iter()
665 .chain(input.iter())
666 .all(|value| value.is_finite())
667 {
668 return false;
669 }
670 let base_norm = base.iter().map(|value| value * value).sum::<f64>().sqrt();
671 let input_norm = input.iter().map(|value| value * value).sum::<f64>().sqrt();
672 if base_norm == 0.0 || input_norm == 0.0 {
673 return false;
674 }
675 let dot = base
676 .iter()
677 .zip(input)
678 .map(|(base, input)| base * input)
679 .sum::<f64>()
680 / (base_norm * input_norm);
681 2.0 * dot.abs().clamp(-1.0, 1.0).acos() <= tolerance.rotation_residual_radians
682}