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
110pub fn assembly_scale_basis(
117 document: &Document,
118 plan: &ScalePlan,
119) -> Result<AssemblyScaleBasis, ScaleError> {
120 plan.validate_document_inventory(document)?;
121 let ScaleOperation::RestBindUniformScale {
122 source_skin_index,
123 source_root_node_index,
124 expected_factor,
125 } = plan.operation()
126 else {
127 return Err(ScaleError::PlanDocumentMismatch {
128 reason: "assembly_basis_requires_rest_bind",
129 });
130 };
131 let mut names = BTreeSet::new();
132 let mut named_nodes = Vec::with_capacity(document.skeleton.bones.len());
133 for (index, bone) in document.skeleton.bones.iter().enumerate() {
134 if bone.name.is_empty() || !names.insert(bone.name.as_str()) {
135 return Err(ScaleError::PlanDocumentMismatch {
136 reason: "assembly_basis_requires_unique_named_nodes",
137 });
138 }
139 named_nodes.push(AssemblyScaleNamedNode {
140 name: bone.name.clone(),
141 parent: bone
142 .parent
143 .and_then(|parent| document.skeleton.bones.get(parent))
144 .map(|parent| parent.name.clone()),
145 translation_bits: bone.rest.translation.to_array().map(f32::to_bits),
146 rotation_bits: bone.rest.rotation.to_array().map(f32::to_bits),
147 scale_bits: bone.rest.scale.to_array().map(f32::to_bits),
148 });
149 if bone.parent.is_some_and(|parent| parent >= index) {
150 return Err(ScaleError::PlanDocumentMismatch {
151 reason: "assembly_basis_parent_order",
152 });
153 }
154 }
155 let source_by_index = document
156 .assets
157 .source_skeleton
158 .nodes
159 .iter()
160 .map(|node| (node.source_node_index, node))
161 .collect::<std::collections::BTreeMap<_, _>>();
162 let mut source_nodes = Vec::new();
163 for row in plan.ledger().source_topology() {
164 let source = source_by_index.get(&row.source_node_index()).ok_or(
165 ScaleError::PlanDocumentMismatch {
166 reason: "assembly_basis_source_node_missing",
167 },
168 )?;
169 let local_rest = match source.local_rest {
170 SourceNodeLocalRest::Trs {
171 translation,
172 rotation,
173 scale,
174 } => AssemblyScaleSourceRest::Trs {
175 translation_bits: translation.to_array().map(f32::to_bits),
176 rotation_bits: rotation.to_array().map(f32::to_bits),
177 scale_bits: scale.to_array().map(f32::to_bits),
178 },
179 SourceNodeLocalRest::Matrix(matrix) => AssemblyScaleSourceRest::Matrix {
180 matrix_bits: matrix.to_cols_array().map(f32::to_bits),
181 },
182 };
183 let role = match row.kind() {
184 ScaleSourceNodeKind::Projected { role, .. } => match role {
185 ScaleProjectedRole::Root => "projected-root",
186 ScaleProjectedRole::Joint => "projected-joint",
187 ScaleProjectedRole::TransformOnly => "projected-transform-only",
188 },
189 ScaleSourceNodeKind::Connector => "connector",
190 ScaleSourceNodeKind::OutsideDomain { bone: Some(_) } => "outside-projected",
191 ScaleSourceNodeKind::OutsideDomain { bone: None } => "outside-helper",
192 };
193 source_nodes.push(AssemblyScaleSourceNode {
194 source_node_index: row.source_node_index(),
195 parent_source_node_index: row.parent_source_node_index(),
196 name: source.name.clone(),
197 role: role.to_owned(),
198 local_rest,
199 });
200 }
201 let mut target_paths = Vec::new();
202 for (clip_index, clip) in document.clips.iter().enumerate() {
203 for (track_index, track) in clip.tracks.iter().enumerate() {
204 let bone = document
205 .skeleton
206 .bones
207 .get(track.bone)
208 .ok_or(ScaleError::BoneIndexOutOfRange { index: track.bone })?;
209 target_paths.push(AssemblyScaleTargetPath {
210 clip_index,
211 track_index,
212 bone: bone.name.clone(),
213 property: track.property.as_str(),
214 factor_bits: plan
215 .animation_target_factor_unchecked(document, track.bone, track.property)?
216 .to_bits(),
217 });
218 }
219 }
220 Ok(AssemblyScaleBasis {
221 version: ASSEMBLY_SCALE_BASIS_VERSION,
222 coordinate_convention: "right-handed-y-up-metres",
223 tolerance_policy_id: plan.tolerance_policy().id,
224 source_skin_index,
225 source_root_node_index,
226 expected_factor_bits: expected_factor.to_bits(),
227 named_nodes,
228 source_nodes,
229 target_paths,
230 })
231}
232
233pub fn require_assembly_scale_compatibility(
243 base: &AssemblyScaleBasis,
244 input: &AssemblyScaleBasis,
245) -> Result<(), AssemblyScaleCompatibilityError> {
246 let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
247 let mismatch = if base.version != input.version {
248 Some("basis-version")
249 } else if base.coordinate_convention != input.coordinate_convention {
250 Some("coordinate-convention")
251 } else if base.tolerance_policy_id != input.tolerance_policy_id
252 || base.tolerance_policy_id != tolerance.id
253 {
254 Some("tolerance-policy")
255 } else if base.source_skin_index != input.source_skin_index {
256 Some("source-skin-selector")
257 } else if base.source_root_node_index != input.source_root_node_index {
258 Some("source-root-selector")
259 } else if base.expected_factor_bits != input.expected_factor_bits {
260 Some("expected-factor")
261 } else if !same_named_topology(&base.named_nodes, &input.named_nodes) {
262 Some("named-topology")
263 } else if !same_named_rest(&base.named_nodes, &input.named_nodes, &tolerance) {
264 Some("named-rest-basis")
265 } else if !same_named_orientations(&base.named_nodes, &input.named_nodes, &tolerance) {
266 Some("named-orientation")
267 } else if !same_source_layout(&base.source_nodes, &input.source_nodes) {
268 Some("source-helper-layout")
269 } else if !same_source_rest(&base.source_nodes, &input.source_nodes, &tolerance) {
270 Some("source-helper-rest-basis")
271 } else {
272 None
273 };
274 mismatch.map_or(Ok(()), |reason| {
275 Err(AssemblyScaleCompatibilityError { reason })
276 })
277}
278
279fn same_named_topology(base: &[AssemblyScaleNamedNode], input: &[AssemblyScaleNamedNode]) -> bool {
280 base.len() == input.len()
281 && base
282 .iter()
283 .zip(input)
284 .all(|(base, input)| base.name == input.name && base.parent == input.parent)
285}
286
287fn same_named_rest(
288 base: &[AssemblyScaleNamedNode],
289 input: &[AssemblyScaleNamedNode],
290 tolerance: &ScaleTolerancePolicy,
291) -> bool {
292 base.iter().zip(input).all(|(base, input)| {
293 close_f32_bits(&base.translation_bits, &input.translation_bits, tolerance)
294 && close_f32_bits(&base.scale_bits, &input.scale_bits, tolerance)
295 })
296}
297
298fn same_named_orientations(
299 base: &[AssemblyScaleNamedNode],
300 input: &[AssemblyScaleNamedNode],
301 tolerance: &ScaleTolerancePolicy,
302) -> bool {
303 base.iter()
304 .zip(input)
305 .all(|(base, input)| same_quaternion(&base.rotation_bits, &input.rotation_bits, tolerance))
306}
307
308fn same_source_layout(base: &[AssemblyScaleSourceNode], input: &[AssemblyScaleSourceNode]) -> bool {
309 base.len() == input.len()
310 && base.iter().zip(input).all(|(base, input)| {
311 base.source_node_index == input.source_node_index
312 && base.parent_source_node_index == input.parent_source_node_index
313 && base.name == input.name
314 && base.role == input.role
315 && std::mem::discriminant(&base.local_rest)
316 == std::mem::discriminant(&input.local_rest)
317 })
318}
319
320fn same_source_rest(
321 base: &[AssemblyScaleSourceNode],
322 input: &[AssemblyScaleSourceNode],
323 tolerance: &ScaleTolerancePolicy,
324) -> bool {
325 base.iter().zip(input).all(
326 |(base, input)| match (&base.local_rest, &input.local_rest) {
327 (
328 AssemblyScaleSourceRest::Trs {
329 translation_bits: base_translation,
330 rotation_bits: base_rotation,
331 scale_bits: base_scale,
332 },
333 AssemblyScaleSourceRest::Trs {
334 translation_bits: input_translation,
335 rotation_bits: input_rotation,
336 scale_bits: input_scale,
337 },
338 ) => {
339 close_f32_bits(base_translation, input_translation, tolerance)
340 && close_f32_bits(base_scale, input_scale, tolerance)
341 && same_quaternion(base_rotation, input_rotation, tolerance)
342 }
343 (
344 AssemblyScaleSourceRest::Matrix {
345 matrix_bits: base_matrix,
346 },
347 AssemblyScaleSourceRest::Matrix {
348 matrix_bits: input_matrix,
349 },
350 ) => close_f32_bits(base_matrix, input_matrix, tolerance),
351 _ => false,
352 },
353 )
354}
355
356fn close_f32_bits<const N: usize>(
357 base: &[u32; N],
358 input: &[u32; N],
359 tolerance: &ScaleTolerancePolicy,
360) -> bool {
361 base.iter().zip(input).all(|(&base, &input)| {
362 close_f64(
363 f32::from_bits(base) as f64,
364 f32::from_bits(input) as f64,
365 tolerance,
366 )
367 })
368}
369
370fn close_f64(base: f64, input: f64, tolerance: &ScaleTolerancePolicy) -> bool {
371 base.is_finite()
372 && input.is_finite()
373 && (base - input).abs()
374 <= tolerance.scalar_absolute + tolerance.scalar_relative * base.abs().max(input.abs())
375}
376
377fn same_quaternion(base: &[u32; 4], input: &[u32; 4], tolerance: &ScaleTolerancePolicy) -> bool {
378 let base = base.map(|bits| f32::from_bits(bits) as f64);
379 let input = input.map(|bits| f32::from_bits(bits) as f64);
380 if !base
381 .iter()
382 .chain(input.iter())
383 .all(|value| value.is_finite())
384 {
385 return false;
386 }
387 let base_norm = base.iter().map(|value| value * value).sum::<f64>().sqrt();
388 let input_norm = input.iter().map(|value| value * value).sum::<f64>().sqrt();
389 if base_norm == 0.0 || input_norm == 0.0 {
390 return false;
391 }
392 let dot = base
393 .iter()
394 .zip(input)
395 .map(|(base, input)| base * input)
396 .sum::<f64>()
397 / (base_norm * input_norm);
398 2.0 * dot.abs().clamp(-1.0, 1.0).acos() <= tolerance.rotation_residual_radians
399}