1use super::numeric::{scale_rows, scale_translation_only};
10use super::planning::{check_factor_narrows, validate_plan_document_inventory};
11use super::validation::{
12 instance_bind, local_rest_matrix, source_node_index_map, validate_scale_input,
13};
14use super::{
15 ScaleBoneRestField, ScaleError, ScaleFieldDisposition, ScaleFieldTarget, ScaleOperation,
16 ScalePlan, ScaleRewriteRule, ScaleSourceRestField,
17};
18use crate::model::{
19 BoneId, Document, Property, SourceNodeAsset, SourceNodeLocalRest, SourceSkeletonCoverage,
20 TrackValues, mat4_is_finite,
21};
22use glam::{DMat3, DMat4, DVec3, Mat4, Vec4};
23use std::collections::{BTreeMap, BTreeSet};
24
25#[derive(Debug, Clone)]
33#[non_exhaustive]
34pub struct ScaleCandidate {
35 pub(in crate::scale) document: Document,
36}
37
38impl ScaleCandidate {
39 pub fn from_document(document: Document) -> Self {
57 Self { document }
58 }
59
60 pub fn document(&self) -> &Document {
62 &self.document
63 }
64
65 pub fn into_document(self) -> Document {
67 self.document
68 }
69}
70
71#[cfg(doctest)]
72mod candidate_api_contract {
73 struct RemovedPublicBuilder;
107}
108
109#[cfg_attr(not(any(test, feature = "fixtures")), allow(dead_code))]
133pub(crate) fn build_scale_candidate(
134 document: &Document,
135 plan: &ScalePlan,
136) -> Result<ScaleCandidate, ScaleError> {
137 validate_plan_document_inventory(document, plan)?;
138 let candidate = match plan.operation() {
139 ScaleOperation::WholeDocumentLinearUnits { .. } => build_whole_document(document, plan)?,
140 ScaleOperation::RestBindUniformScale { .. } => build_rest_bind(document, plan)?,
141 };
142 validate_scale_input(&candidate)?;
149 Ok(ScaleCandidate {
150 document: candidate,
151 })
152}
153
154pub(in crate::scale) fn build_whole_document(
155 document: &Document,
156 plan: &ScalePlan,
157) -> Result<Document, ScaleError> {
158 let q = check_factor_narrows(plan.common_factor(), plan.common_factor())?;
159 let mut candidate = document.clone();
160 let source_positions: BTreeMap<_, _> = candidate
161 .assets
162 .source_skeleton
163 .nodes
164 .iter()
165 .enumerate()
166 .map(|(position, node)| (node.source_node_index, position))
167 .collect();
168 for row in plan.field_rows() {
169 let ScaleFieldDisposition::Rewrite(rule) = row.disposition else {
170 continue;
171 };
172 if rule != ScaleRewriteRule::WholeDocumentLength {
173 return Err(ScaleError::PlanDocumentMismatch {
174 reason: "invalid_whole_document_rewrite_rule",
175 });
176 }
177 match row.target {
178 ScaleFieldTarget::BoneRest {
179 bone,
180 field: ScaleBoneRestField::Translation,
181 } => candidate.skeleton.bones[bone].rest.translation *= q,
182 ScaleFieldTarget::BoneInverseBind { bone } => {
183 let inverse_bind = candidate.skeleton.bones[bone].inverse_bind.as_mut().ok_or(
184 ScaleError::PlanDocumentMismatch {
185 reason: "compiled_bone_inverse_bind_missing",
186 },
187 )?;
188 *inverse_bind = scale_translation_only(*inverse_bind, q);
189 }
190 ScaleFieldTarget::SourceNodeRest {
191 source_node_index,
192 field: ScaleSourceRestField::Translation | ScaleSourceRestField::MatrixTranslation,
193 } => {
194 let position = *source_positions.get(&source_node_index).ok_or(
195 ScaleError::PlanDocumentMismatch {
196 reason: "compiled_source_node_missing",
197 },
198 )?;
199 let node = &mut candidate.assets.source_skeleton.nodes[position];
200 node.local_rest = match &node.local_rest {
201 SourceNodeLocalRest::Trs {
202 translation,
203 rotation,
204 scale,
205 } => SourceNodeLocalRest::Trs {
206 translation: *translation * q,
207 rotation: *rotation,
208 scale: *scale,
209 },
210 SourceNodeLocalRest::Matrix(matrix) => {
211 SourceNodeLocalRest::Matrix(scale_translation_only(*matrix, q))
212 }
213 };
214 }
215 ScaleFieldTarget::AnimationValues {
216 clip_index,
217 track_index,
218 property: Property::Translation,
219 ..
220 } => {
221 if let TrackValues::Vec3s(values) =
222 &mut candidate.clips[clip_index].tracks[track_index].values
223 {
224 for value in values {
225 *value *= q;
226 }
227 }
228 }
229 ScaleFieldTarget::MeshPositions {
230 mesh_index,
231 primitive_index,
232 } => {
233 for position in
234 &mut candidate.assets.meshes[mesh_index].primitives[primitive_index].positions
235 {
236 *position *= q;
237 }
238 }
239 ScaleFieldTarget::InstanceInverseBind {
240 instance_index,
241 slot,
242 ..
243 } => {
244 let inverse_bind = &mut candidate.assets.instances[instance_index].skin_ibms[slot];
245 *inverse_bind = scale_translation_only(*inverse_bind, q);
246 }
247 _ => {
248 return Err(ScaleError::PlanDocumentMismatch {
249 reason: "invalid_whole_document_write_target",
250 });
251 }
252 }
253 }
254 if candidate.assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete {
260 for node in &mut candidate.assets.source_skeleton.nodes {
261 node.local_rest = match &node.local_rest {
262 SourceNodeLocalRest::Trs {
263 translation,
264 rotation,
265 scale,
266 } => SourceNodeLocalRest::Trs {
267 translation: *translation * q,
268 rotation: *rotation,
269 scale: *scale,
270 },
271 SourceNodeLocalRest::Matrix(matrix) => {
272 SourceNodeLocalRest::Matrix(scale_translation_only(*matrix, q))
273 }
274 };
275 }
276 }
277 Ok(candidate)
278}
279
280pub(in crate::scale) fn build_rest_bind(
281 document: &Document,
282 plan: &ScalePlan,
283) -> Result<Document, ScaleError> {
284 let affected = plan.affected_set();
285 let s = check_factor_narrows(plan.common_factor(), plan.common_factor())?;
286 let by_source_index = source_node_index_map(document);
287 let mut connector_product_by_tail = BTreeMap::new();
288 let parent_factor = |node: BoneId| -> Result<f32, ScaleError> {
289 let bone = document
290 .skeleton
291 .bones
292 .get(node)
293 .ok_or(ScaleError::BoneIndexOutOfRange { index: node })?;
294 Ok(match bone.parent {
295 Some(parent) if affected.contains(&parent) => s,
296 _ => 1.0,
297 })
298 };
299 let node_factor = |node: BoneId| -> f32 { if affected.contains(&node) { s } else { 1.0 } };
300
301 let mut candidate = document.clone();
302 let source_positions: BTreeMap<_, _> = candidate
303 .assets
304 .source_skeleton
305 .nodes
306 .iter()
307 .enumerate()
308 .map(|(position, node)| (node.source_node_index, position))
309 .collect();
310 let mut materialized_binds: BTreeMap<usize, Vec<Mat4>> = BTreeMap::new();
311 for row in plan.field_rows() {
312 let ScaleFieldDisposition::Rewrite(rule) = row.disposition else {
313 continue;
314 };
315 match (row.target, rule) {
316 (
317 ScaleFieldTarget::BoneRest {
318 bone,
319 field: ScaleBoneRestField::Translation,
320 },
321 ScaleRewriteRule::RestBindParentBasis,
322 ) => candidate.skeleton.bones[bone].rest.translation *= parent_factor(bone)?,
323 (
324 ScaleFieldTarget::BoneRest {
325 bone,
326 field: ScaleBoneRestField::Scale,
327 },
328 ScaleRewriteRule::RestBindLocalScale,
329 ) => {
330 candidate.skeleton.bones[bone].rest.scale *=
331 parent_factor(bone)? / node_factor(bone);
332 }
333 (ScaleFieldTarget::BoneInverseBind { bone }, ScaleRewriteRule::RestBindNodeBasis) => {
334 let inverse_bind = candidate.skeleton.bones[bone].inverse_bind.as_mut().ok_or(
335 ScaleError::PlanDocumentMismatch {
336 reason: "compiled_bone_inverse_bind_missing",
337 },
338 )?;
339 *inverse_bind = scale_rows(*inverse_bind, node_factor(bone));
340 }
341 (
342 ScaleFieldTarget::SourceNodeRest {
343 source_node_index,
344 field,
345 },
346 ScaleRewriteRule::RestBindSourceLocal { connector_tail },
347 ) => {
348 let position = *source_positions.get(&source_node_index).ok_or(
349 ScaleError::PlanDocumentMismatch {
350 reason: "compiled_source_node_missing",
351 },
352 )?;
353 let bone = candidate.assets.source_skeleton.nodes[position]
354 .bone
355 .ok_or(ScaleError::SourceNodeNotNormalized { source_node_index })?;
356 let local_rest = &candidate.assets.source_skeleton.nodes[position].local_rest;
357 let s_parent = parent_factor(bone)?;
358 let s_node = node_factor(bone);
359 let rebased = if let Some(connector_tail) = connector_tail {
360 rebase_source_local_through_connector_bridge(
361 local_rest,
362 connector_tail,
363 &by_source_index,
364 &mut connector_product_by_tail,
365 s_parent,
366 s_node,
367 bone,
368 )?
369 } else {
370 rebase_source_local_rest(local_rest, s_parent, s_node, None)
371 };
372 candidate.assets.source_skeleton.nodes[position].local_rest =
373 source_rest_with_rewritten_field(local_rest, &rebased, field)?;
374 }
375 (
376 ScaleFieldTarget::AnimationValues {
377 clip_index,
378 track_index,
379 ..
380 },
381 rule @ (ScaleRewriteRule::RestBindParentBasis
382 | ScaleRewriteRule::RestBindLocalScale),
383 ) => match rule {
384 ScaleRewriteRule::RestBindParentBasis => {
385 let property = document.clips[clip_index].tracks[track_index].property;
386 let bone = document.clips[clip_index].tracks[track_index].bone;
387 let s_parent =
388 plan.animation_target_factor_unchecked(document, bone, property)? as f32;
389 if let TrackValues::Vec3s(values) =
390 &mut candidate.clips[clip_index].tracks[track_index].values
391 {
392 for value in values.iter_mut() {
393 *value *= s_parent;
394 }
395 }
396 }
397 ScaleRewriteRule::RestBindLocalScale => {
398 let property = document.clips[clip_index].tracks[track_index].property;
399 let bone = document.clips[clip_index].tracks[track_index].bone;
400 let multiplier =
401 plan.animation_target_factor_unchecked(document, bone, property)?;
402 if let TrackValues::Vec3s(values) =
403 &mut candidate.clips[clip_index].tracks[track_index].values
404 {
405 for value in values {
406 *value = (value.as_dvec3() * multiplier).as_vec3();
409 }
410 }
411 }
412 ScaleRewriteRule::WholeDocumentLength
413 | ScaleRewriteRule::RestBindNodeBasis
414 | ScaleRewriteRule::RestBindSourceLocal { .. } => {
415 unreachable!("outer pattern limits animation rules")
416 }
417 },
418 (
419 ScaleFieldTarget::InstanceInverseBind {
420 instance_index,
421 slot,
422 joint,
423 },
424 ScaleRewriteRule::RestBindNodeBasis,
425 ) => {
426 let source_instance = &document.assets.instances[instance_index];
427 let binds = materialized_binds
428 .entry(instance_index)
429 .or_insert_with(|| Vec::with_capacity(source_instance.skin_joints.len()));
430 if slot != binds.len() {
431 return Err(ScaleError::PlanDocumentMismatch {
432 reason: "compiled_inverse_bind_slot_order_mismatch",
433 });
434 }
435 let before = instance_bind(document, source_instance, slot, joint)?;
436 binds.push(scale_rows(before, node_factor(joint)));
437 }
438 _ => {
439 return Err(ScaleError::PlanDocumentMismatch {
440 reason: "invalid_rest_bind_write_target",
441 });
442 }
443 }
444 }
445 for (instance_index, binds) in materialized_binds {
446 candidate.assets.instances[instance_index].skin_ibms = binds;
447 }
448 Ok(candidate)
449}
450
451fn rebase_source_local_rest(
459 local_rest: &SourceNodeLocalRest,
460 s_parent: f32,
461 s_node: f32,
462 bridge_offset: Option<DVec3>,
463) -> SourceNodeLocalRest {
464 match local_rest {
465 SourceNodeLocalRest::Trs {
466 translation,
467 rotation,
468 scale,
469 } => {
470 let translation = match bridge_offset {
471 Some(offset) => (translation.as_dvec3() * f64::from(s_parent) + offset).as_vec3(),
472 None => *translation * s_parent,
473 };
474 let scale = match bridge_offset {
475 Some(_) => {
476 let ratio = f64::from(s_parent) / f64::from(s_node);
477 (scale.as_dvec3() * ratio).as_vec3()
478 }
479 None => *scale * (s_parent / s_node),
480 };
481 SourceNodeLocalRest::Trs {
482 translation,
483 rotation: *rotation,
484 scale,
485 }
486 }
487 SourceNodeLocalRest::Matrix(matrix) => {
488 let rebased = if let Some(offset) = bridge_offset {
489 let ratio = f64::from(s_parent) / f64::from(s_node);
490 let rebase_linear_column = |column: Vec4| {
491 (column.truncate().as_dvec3() * ratio)
492 .as_vec3()
493 .extend(column.w)
494 };
495 let translation =
496 (matrix.w_axis.truncate().as_dvec3() * f64::from(s_parent) + offset).as_vec3();
497 Mat4::from_cols(
498 rebase_linear_column(matrix.x_axis),
499 rebase_linear_column(matrix.y_axis),
500 rebase_linear_column(matrix.z_axis),
501 translation.extend(matrix.w_axis.w),
502 )
503 } else {
504 rebase_matrix(*matrix, s_parent, s_node)
505 };
506 SourceNodeLocalRest::Matrix(rebased)
507 }
508 }
509}
510
511fn source_rest_with_rewritten_field(
518 original: &SourceNodeLocalRest,
519 rewritten: &SourceNodeLocalRest,
520 field: ScaleSourceRestField,
521) -> Result<SourceNodeLocalRest, ScaleError> {
522 match (original, rewritten, field) {
523 (
524 SourceNodeLocalRest::Trs {
525 translation: _,
526 rotation,
527 scale,
528 },
529 SourceNodeLocalRest::Trs {
530 translation: rewritten,
531 ..
532 },
533 ScaleSourceRestField::Translation,
534 ) => Ok(SourceNodeLocalRest::Trs {
535 translation: *rewritten,
536 rotation: *rotation,
537 scale: *scale,
538 }),
539 (
540 SourceNodeLocalRest::Trs {
541 translation,
542 rotation,
543 scale: _,
544 },
545 SourceNodeLocalRest::Trs {
546 scale: rewritten, ..
547 },
548 ScaleSourceRestField::Scale,
549 ) => Ok(SourceNodeLocalRest::Trs {
550 translation: *translation,
551 rotation: *rotation,
552 scale: *rewritten,
553 }),
554 (
555 SourceNodeLocalRest::Matrix(original),
556 SourceNodeLocalRest::Matrix(rewritten),
557 ScaleSourceRestField::MatrixLinear,
558 ) => Ok(SourceNodeLocalRest::Matrix(Mat4::from_cols(
559 rewritten.x_axis.truncate().extend(original.x_axis.w),
560 rewritten.y_axis.truncate().extend(original.y_axis.w),
561 rewritten.z_axis.truncate().extend(original.z_axis.w),
562 original.w_axis,
563 ))),
564 (
565 SourceNodeLocalRest::Matrix(original),
566 SourceNodeLocalRest::Matrix(rewritten),
567 ScaleSourceRestField::MatrixTranslation,
568 ) => Ok(SourceNodeLocalRest::Matrix(Mat4::from_cols(
569 original.x_axis,
570 original.y_axis,
571 original.z_axis,
572 rewritten.w_axis.truncate().extend(original.w_axis.w),
573 ))),
574 _ => Err(ScaleError::PlanDocumentMismatch {
575 reason: "source_local_field_variant_mismatch",
576 }),
577 }
578}
579
580fn rebase_source_local_through_connector_bridge(
588 local_rest: &SourceNodeLocalRest,
589 connector_tail: usize,
590 by_source_index: &BTreeMap<usize, &SourceNodeAsset>,
591 connector_product_by_tail: &mut BTreeMap<usize, DMat4>,
592 s_parent: f32,
593 s_node: f32,
594 bone: BoneId,
595) -> Result<SourceNodeLocalRest, ScaleError> {
596 if s_parent == 1.0 && s_node == 1.0 {
597 return Ok(local_rest.clone());
598 }
599 let connector =
600 memoized_connector_product(connector_tail, by_source_index, connector_product_by_tail)?;
601 let connector_linear_inverse = DMat3::from_cols(
602 connector.x_axis.truncate(),
603 connector.y_axis.truncate(),
604 connector.z_axis.truncate(),
605 )
606 .inverse();
607 let bridge_offset =
608 connector_linear_inverse * (connector.w_axis.truncate() * (f64::from(s_parent) - 1.0));
609 if !connector_linear_inverse.x_axis.is_finite()
610 || !connector_linear_inverse.y_axis.is_finite()
611 || !connector_linear_inverse.z_axis.is_finite()
612 || !bridge_offset.is_finite()
613 {
614 return Err(ScaleError::NonFiniteTransform { node: bone });
615 }
616 let rebased = rebase_source_local_rest(local_rest, s_parent, s_node, Some(bridge_offset));
622 if !mat4_is_finite(local_rest_matrix(&rebased)) {
623 return Err(ScaleError::NonFiniteTransform { node: bone });
624 }
625 Ok(rebased)
626}
627
628fn memoized_connector_product(
631 connector_tail: usize,
632 by_source_index: &BTreeMap<usize, &SourceNodeAsset>,
633 connector_product_by_tail: &mut BTreeMap<usize, DMat4>,
634) -> Result<DMat4, ScaleError> {
635 let mut pending = Vec::new();
636 let mut visited = BTreeSet::new();
637 let mut cursor = connector_tail;
638 let mut product = loop {
639 if let Some(&cached) = connector_product_by_tail.get(&cursor) {
640 break cached;
641 }
642 if !visited.insert(cursor) {
643 return Err(ScaleError::IncompleteClosure {
644 reason: "cyclic_connector_source_parent_chain",
645 });
646 }
647 let asset = by_source_index
648 .get(&cursor)
649 .ok_or(ScaleError::IncompleteClosure {
650 reason: "dangling_connector_source_node_index",
651 })?;
652 if asset.bone.is_some() {
653 break DMat4::IDENTITY;
654 }
655 pending.push(cursor);
656 cursor = asset
657 .parent_source_node_index
658 .ok_or(ScaleError::IncompleteClosure {
659 reason: "connector_without_projected_ancestor",
660 })?;
661 };
662 while let Some(source) = pending.pop() {
663 let asset = by_source_index
664 .get(&source)
665 .ok_or(ScaleError::IncompleteClosure {
666 reason: "dangling_connector_source_node_index",
667 })?;
668 product *= local_rest_matrix(&asset.local_rest).as_dmat4();
669 connector_product_by_tail.insert(source, product);
670 }
671 connector_product_by_tail
672 .get(&connector_tail)
673 .copied()
674 .ok_or(ScaleError::IncompleteClosure {
675 reason: "empty_connector_bridge",
676 })
677}
678
679fn rebase_matrix(matrix: Mat4, s_parent: f32, s_node: f32) -> Mat4 {
687 let scaled = scale_rows(matrix, s_parent);
688 let inverse_node = 1.0 / s_node;
689 Mat4::from_cols(
690 scaled.x_axis * inverse_node,
691 scaled.y_axis * inverse_node,
692 scaled.z_axis * inverse_node,
693 scaled.w_axis,
694 )
695}