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 bone,
380 ..
381 },
382 rule @ (ScaleRewriteRule::RestBindParentBasis
383 | ScaleRewriteRule::RestBindLocalScale),
384 ) => match rule {
385 ScaleRewriteRule::RestBindParentBasis => {
386 let s_parent = parent_factor(bone)?;
387 if let TrackValues::Vec3s(values) =
388 &mut candidate.clips[clip_index].tracks[track_index].values
389 {
390 for value in values.iter_mut() {
391 *value *= s_parent;
392 }
393 }
394 }
395 ScaleRewriteRule::RestBindLocalScale => {
396 let multiplier =
397 scale_animation_multiplier(document, bone, &affected, plan.common_factor());
398 if let TrackValues::Vec3s(values) =
399 &mut candidate.clips[clip_index].tracks[track_index].values
400 {
401 for value in values {
402 *value = (value.as_dvec3() * multiplier).as_vec3();
405 }
406 }
407 }
408 ScaleRewriteRule::WholeDocumentLength
409 | ScaleRewriteRule::RestBindNodeBasis
410 | ScaleRewriteRule::RestBindSourceLocal { .. } => {
411 unreachable!("outer pattern limits animation rules")
412 }
413 },
414 (
415 ScaleFieldTarget::InstanceInverseBind {
416 instance_index,
417 slot,
418 joint,
419 },
420 ScaleRewriteRule::RestBindNodeBasis,
421 ) => {
422 let source_instance = &document.assets.instances[instance_index];
423 let binds = materialized_binds
424 .entry(instance_index)
425 .or_insert_with(|| Vec::with_capacity(source_instance.skin_joints.len()));
426 if slot != binds.len() {
427 return Err(ScaleError::PlanDocumentMismatch {
428 reason: "compiled_inverse_bind_slot_order_mismatch",
429 });
430 }
431 let before = instance_bind(document, source_instance, slot, joint)?;
432 binds.push(scale_rows(before, node_factor(joint)));
433 }
434 _ => {
435 return Err(ScaleError::PlanDocumentMismatch {
436 reason: "invalid_rest_bind_write_target",
437 });
438 }
439 }
440 }
441 for (instance_index, binds) in materialized_binds {
442 candidate.assets.instances[instance_index].skin_ibms = binds;
443 }
444 Ok(candidate)
445}
446
447fn rebase_source_local_rest(
455 local_rest: &SourceNodeLocalRest,
456 s_parent: f32,
457 s_node: f32,
458 bridge_offset: Option<DVec3>,
459) -> SourceNodeLocalRest {
460 match local_rest {
461 SourceNodeLocalRest::Trs {
462 translation,
463 rotation,
464 scale,
465 } => {
466 let translation = match bridge_offset {
467 Some(offset) => (translation.as_dvec3() * f64::from(s_parent) + offset).as_vec3(),
468 None => *translation * s_parent,
469 };
470 let scale = match bridge_offset {
471 Some(_) => {
472 let ratio = f64::from(s_parent) / f64::from(s_node);
473 (scale.as_dvec3() * ratio).as_vec3()
474 }
475 None => *scale * (s_parent / s_node),
476 };
477 SourceNodeLocalRest::Trs {
478 translation,
479 rotation: *rotation,
480 scale,
481 }
482 }
483 SourceNodeLocalRest::Matrix(matrix) => {
484 let rebased = if let Some(offset) = bridge_offset {
485 let ratio = f64::from(s_parent) / f64::from(s_node);
486 let rebase_linear_column = |column: Vec4| {
487 (column.truncate().as_dvec3() * ratio)
488 .as_vec3()
489 .extend(column.w)
490 };
491 let translation =
492 (matrix.w_axis.truncate().as_dvec3() * f64::from(s_parent) + offset).as_vec3();
493 Mat4::from_cols(
494 rebase_linear_column(matrix.x_axis),
495 rebase_linear_column(matrix.y_axis),
496 rebase_linear_column(matrix.z_axis),
497 translation.extend(matrix.w_axis.w),
498 )
499 } else {
500 rebase_matrix(*matrix, s_parent, s_node)
501 };
502 SourceNodeLocalRest::Matrix(rebased)
503 }
504 }
505}
506
507fn source_rest_with_rewritten_field(
514 original: &SourceNodeLocalRest,
515 rewritten: &SourceNodeLocalRest,
516 field: ScaleSourceRestField,
517) -> Result<SourceNodeLocalRest, ScaleError> {
518 match (original, rewritten, field) {
519 (
520 SourceNodeLocalRest::Trs {
521 translation: _,
522 rotation,
523 scale,
524 },
525 SourceNodeLocalRest::Trs {
526 translation: rewritten,
527 ..
528 },
529 ScaleSourceRestField::Translation,
530 ) => Ok(SourceNodeLocalRest::Trs {
531 translation: *rewritten,
532 rotation: *rotation,
533 scale: *scale,
534 }),
535 (
536 SourceNodeLocalRest::Trs {
537 translation,
538 rotation,
539 scale: _,
540 },
541 SourceNodeLocalRest::Trs {
542 scale: rewritten, ..
543 },
544 ScaleSourceRestField::Scale,
545 ) => Ok(SourceNodeLocalRest::Trs {
546 translation: *translation,
547 rotation: *rotation,
548 scale: *rewritten,
549 }),
550 (
551 SourceNodeLocalRest::Matrix(original),
552 SourceNodeLocalRest::Matrix(rewritten),
553 ScaleSourceRestField::MatrixLinear,
554 ) => Ok(SourceNodeLocalRest::Matrix(Mat4::from_cols(
555 rewritten.x_axis.truncate().extend(original.x_axis.w),
556 rewritten.y_axis.truncate().extend(original.y_axis.w),
557 rewritten.z_axis.truncate().extend(original.z_axis.w),
558 original.w_axis,
559 ))),
560 (
561 SourceNodeLocalRest::Matrix(original),
562 SourceNodeLocalRest::Matrix(rewritten),
563 ScaleSourceRestField::MatrixTranslation,
564 ) => Ok(SourceNodeLocalRest::Matrix(Mat4::from_cols(
565 original.x_axis,
566 original.y_axis,
567 original.z_axis,
568 rewritten.w_axis.truncate().extend(original.w_axis.w),
569 ))),
570 _ => Err(ScaleError::PlanDocumentMismatch {
571 reason: "source_local_field_variant_mismatch",
572 }),
573 }
574}
575
576fn rebase_source_local_through_connector_bridge(
584 local_rest: &SourceNodeLocalRest,
585 connector_tail: usize,
586 by_source_index: &BTreeMap<usize, &SourceNodeAsset>,
587 connector_product_by_tail: &mut BTreeMap<usize, DMat4>,
588 s_parent: f32,
589 s_node: f32,
590 bone: BoneId,
591) -> Result<SourceNodeLocalRest, ScaleError> {
592 if s_parent == 1.0 && s_node == 1.0 {
593 return Ok(local_rest.clone());
594 }
595 let connector =
596 memoized_connector_product(connector_tail, by_source_index, connector_product_by_tail)?;
597 let connector_linear_inverse = DMat3::from_cols(
598 connector.x_axis.truncate(),
599 connector.y_axis.truncate(),
600 connector.z_axis.truncate(),
601 )
602 .inverse();
603 let bridge_offset =
604 connector_linear_inverse * (connector.w_axis.truncate() * (f64::from(s_parent) - 1.0));
605 if !connector_linear_inverse.x_axis.is_finite()
606 || !connector_linear_inverse.y_axis.is_finite()
607 || !connector_linear_inverse.z_axis.is_finite()
608 || !bridge_offset.is_finite()
609 {
610 return Err(ScaleError::NonFiniteTransform { node: bone });
611 }
612 let rebased = rebase_source_local_rest(local_rest, s_parent, s_node, Some(bridge_offset));
618 if !mat4_is_finite(local_rest_matrix(&rebased)) {
619 return Err(ScaleError::NonFiniteTransform { node: bone });
620 }
621 Ok(rebased)
622}
623
624fn memoized_connector_product(
627 connector_tail: usize,
628 by_source_index: &BTreeMap<usize, &SourceNodeAsset>,
629 connector_product_by_tail: &mut BTreeMap<usize, DMat4>,
630) -> Result<DMat4, ScaleError> {
631 let mut pending = Vec::new();
632 let mut visited = BTreeSet::new();
633 let mut cursor = connector_tail;
634 let mut product = loop {
635 if let Some(&cached) = connector_product_by_tail.get(&cursor) {
636 break cached;
637 }
638 if !visited.insert(cursor) {
639 return Err(ScaleError::IncompleteClosure {
640 reason: "cyclic_connector_source_parent_chain",
641 });
642 }
643 let asset = by_source_index
644 .get(&cursor)
645 .ok_or(ScaleError::IncompleteClosure {
646 reason: "dangling_connector_source_node_index",
647 })?;
648 if asset.bone.is_some() {
649 break DMat4::IDENTITY;
650 }
651 pending.push(cursor);
652 cursor = asset
653 .parent_source_node_index
654 .ok_or(ScaleError::IncompleteClosure {
655 reason: "connector_without_projected_ancestor",
656 })?;
657 };
658 while let Some(source) = pending.pop() {
659 let asset = by_source_index
660 .get(&source)
661 .ok_or(ScaleError::IncompleteClosure {
662 reason: "dangling_connector_source_node_index",
663 })?;
664 product *= local_rest_matrix(&asset.local_rest).as_dmat4();
665 connector_product_by_tail.insert(source, product);
666 }
667 connector_product_by_tail
668 .get(&connector_tail)
669 .copied()
670 .ok_or(ScaleError::IncompleteClosure {
671 reason: "empty_connector_bridge",
672 })
673}
674
675fn rebase_matrix(matrix: Mat4, s_parent: f32, s_node: f32) -> Mat4 {
683 let scaled = scale_rows(matrix, s_parent);
684 let inverse_node = 1.0 / s_node;
685 Mat4::from_cols(
686 scaled.x_axis * inverse_node,
687 scaled.y_axis * inverse_node,
688 scaled.z_axis * inverse_node,
689 scaled.w_axis,
690 )
691}
692
693fn scale_animation_multiplier(
702 document: &Document,
703 node: BoneId,
704 affected: &BTreeSet<BoneId>,
705 common_factor: f64,
706) -> f64 {
707 if !affected.contains(&node) {
708 return 1.0;
709 }
710 match document
711 .skeleton
712 .bones
713 .get(node)
714 .and_then(|bone| bone.parent)
715 {
716 Some(parent) if affected.contains(&parent) => 1.0,
717 _ => 1.0 / common_factor,
718 }
719}