1use super::validation::{
8 derive_rest_bind_plan_domain, source_node_index_map, source_skin_payload_shapes,
9 source_world_matrix, validate_scale_input, whole_document_source_topology,
10};
11use super::{
12 RestBindParams, ScaleBoneRestField, ScaleCompiledPlan, ScaleError, ScaleFieldDisposition,
13 ScaleFieldPlan, ScaleFieldTarget, ScaleLedger, ScaleOperation, ScalePayloadShapeRow, ScalePlan,
14 ScaleProjectedRole, ScaleProofObligation, ScaleRequest, ScaleRewriteRule, ScaleSourceNodeKind,
15 ScaleSourceRestField, ScaleSourceTopologyRow, ScaleTolerancePolicy, WholeDocumentParams,
16};
17use crate::model::{
18 AffineDomainViolation, BoneId, Document, Interpolation, PositiveUniformAffineTolerance,
19 Property, SourceNodeLocalRest, SourceSkeletonCoverage, classify_positive_uniform_affine,
20};
21use glam::Mat3;
22use std::collections::{BTreeMap, BTreeSet};
23
24pub fn plan_scale(request: &ScaleRequest<'_>) -> Result<ScalePlan, ScaleError> {
36 if !request.capability.is_supported_for(request.operation) {
37 return Err(ScaleError::IncompleteCapability);
38 }
39 validate_scale_input(request.document)?;
40 match request.operation {
41 ScaleOperation::WholeDocumentLinearUnits { factor } => {
42 plan_whole_document(request.document, factor)
43 }
44 ScaleOperation::RestBindUniformScale {
45 source_skin_index,
46 source_root_node_index,
47 expected_factor,
48 } => plan_rest_bind(
49 request.document,
50 source_skin_index,
51 source_root_node_index,
52 expected_factor,
53 ),
54 }
55}
56
57pub(in crate::scale) fn check_factor_narrows(
71 declared: f64,
72 factor: f64,
73) -> Result<f32, ScaleError> {
74 let narrowed = factor as f32;
75 if !narrowed.is_finite() || (narrowed == 0.0 && factor != 0.0) {
76 return Err(ScaleError::FactorNotRepresentable {
77 declared,
78 factor,
79 narrowed,
80 });
81 }
82 Ok(narrowed)
83}
84
85fn field_disposition(active: bool, rule: ScaleRewriteRule) -> ScaleFieldDisposition {
86 if active {
87 ScaleFieldDisposition::Rewrite(rule)
88 } else {
89 ScaleFieldDisposition::PreserveExact
90 }
91}
92
93fn compile_payload_shapes(document: &Document) -> Vec<ScalePayloadShapeRow> {
94 let source_authoritative =
95 document.assets.source_skeleton.coverage == SourceSkeletonCoverage::Complete;
96 let mut rows = vec![ScalePayloadShapeRow::Document {
97 bone_count: document.skeleton.bones.len(),
98 source_node_count: if source_authoritative {
99 document.assets.source_skeleton.nodes.len()
100 } else {
101 0
102 },
103 source_coverage: document.assets.source_skeleton.coverage,
104 clip_count: document.clips.len(),
105 instance_count: document.assets.instances.len(),
106 mesh_count: document.assets.meshes.len(),
107 }];
108 rows.extend(
109 document
110 .skeleton
111 .bones
112 .iter()
113 .enumerate()
114 .map(|(bone, value)| ScalePayloadShapeRow::Bone {
115 bone,
116 parent: value.parent,
117 }),
118 );
119 rows.extend(source_skin_payload_shapes(document));
120 for (clip_index, clip) in document.clips.iter().enumerate() {
121 rows.push(ScalePayloadShapeRow::Clip {
122 clip_index,
123 track_count: clip.tracks.len(),
124 });
125 rows.extend(clip.tracks.iter().enumerate().map(|(track_index, track)| {
126 ScalePayloadShapeRow::Track {
127 clip_index,
128 track_index,
129 bone: track.bone,
130 property: track.property,
131 interpolation: track.interpolation,
132 key_count: track.times.len(),
133 value_count: track.values.len(),
134 }
135 }));
136 }
137 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
138 rows.push(ScalePayloadShapeRow::Instance {
139 instance_index,
140 node: instance.node,
141 source_node_index: instance.source_node_index,
142 mesh: instance.mesh,
143 joint_count: instance.skin_joints.len(),
144 inverse_bind_count: instance.skin_ibms.len(),
145 });
146 rows.extend(
147 instance
148 .skin_joints
149 .iter()
150 .enumerate()
151 .map(|(slot, &joint)| ScalePayloadShapeRow::InstanceJoint {
152 instance_index,
153 slot,
154 joint,
155 }),
156 );
157 }
158 for (mesh_index, mesh) in document.assets.meshes.iter().enumerate() {
159 rows.push(ScalePayloadShapeRow::Mesh {
160 mesh_index,
161 source_mesh_index: mesh.source_mesh_index,
162 primitive_count: mesh.primitives.len(),
163 });
164 rows.extend(
165 mesh.primitives
166 .iter()
167 .enumerate()
168 .map(
169 |(primitive_index, primitive)| ScalePayloadShapeRow::Primitive {
170 mesh_index,
171 primitive_index,
172 position_count: primitive.positions.len(),
173 normal_count: primitive.normals.len(),
174 joint_count: primitive.joints.len(),
175 weight_count: primitive.weights.len(),
176 },
177 ),
178 );
179 }
180 rows
181}
182
183fn compile_scale_ledger(
184 document: &Document,
185 operation: ScaleOperation,
186 affected_nodes: &[BoneId],
187 transform_only_attachments: &[BoneId],
188 topology: &[ScaleSourceTopologyRow],
189) -> ScaleLedger {
190 let affected: BTreeSet<_> = affected_nodes.iter().copied().collect();
191 let factor = match operation {
192 ScaleOperation::WholeDocumentLinearUnits { factor } => factor,
193 ScaleOperation::RestBindUniformScale {
194 expected_factor, ..
195 } => expected_factor,
196 };
197 let factor_changes = factor != 1.0;
198 let whole_document = matches!(operation, ScaleOperation::WholeDocumentLinearUnits { .. });
199 let mut fields = Vec::new();
200
201 for (bone, value) in document.skeleton.bones.iter().enumerate() {
202 let in_domain = affected.contains(&bone);
203 let parent_in_domain = value
204 .parent
205 .is_some_and(|parent| affected.contains(&parent));
206 let translation = if whole_document {
207 field_disposition(factor_changes, ScaleRewriteRule::WholeDocumentLength)
208 } else {
209 field_disposition(
210 factor_changes && in_domain && parent_in_domain,
211 ScaleRewriteRule::RestBindParentBasis,
212 )
213 };
214 let scale = if whole_document {
215 ScaleFieldDisposition::PreserveExact
216 } else {
217 field_disposition(
218 factor_changes && in_domain && !parent_in_domain,
219 ScaleRewriteRule::RestBindLocalScale,
220 )
221 };
222 for (field, disposition) in [
223 (ScaleBoneRestField::Translation, translation),
224 (
225 ScaleBoneRestField::Rotation,
226 ScaleFieldDisposition::PreserveExact,
227 ),
228 (ScaleBoneRestField::Scale, scale),
229 ] {
230 fields.push(ScaleFieldPlan {
231 target: ScaleFieldTarget::BoneRest { bone, field },
232 disposition,
233 element_count: 1,
234 });
235 }
236 if value.inverse_bind.is_some() {
237 fields.push(ScaleFieldPlan {
238 target: ScaleFieldTarget::BoneInverseBind { bone },
239 disposition: if whole_document {
240 field_disposition(factor_changes, ScaleRewriteRule::WholeDocumentLength)
241 } else {
242 field_disposition(
243 factor_changes && in_domain,
244 ScaleRewriteRule::RestBindNodeBasis,
245 )
246 },
247 element_count: 1,
248 });
249 }
250 }
251
252 let source_nodes = source_node_index_map(document);
253 for topology_row in topology {
254 let node = source_nodes
255 .get(&topology_row.source_node_index)
256 .expect("validated topology row has a source node");
257 let (role, connector_tail) = match topology_row.kind {
258 ScaleSourceNodeKind::Projected {
259 role,
260 incoming_connector_tail,
261 ..
262 } => (Some(role), incoming_connector_tail),
263 ScaleSourceNodeKind::Connector | ScaleSourceNodeKind::OutsideDomain { .. } => {
264 (None, None)
265 }
266 };
267 let parent_rewrite = factor_changes
268 && (whole_document || role.is_some_and(|role| role != ScaleProjectedRole::Root));
269 let local_rewrite = factor_changes && role == Some(ScaleProjectedRole::Root);
270 let source_rule = if whole_document {
271 ScaleRewriteRule::WholeDocumentLength
272 } else {
273 ScaleRewriteRule::RestBindSourceLocal { connector_tail }
274 };
275 let push = |fields: &mut Vec<ScaleFieldPlan>, field: ScaleSourceRestField, active: bool| {
276 fields.push(ScaleFieldPlan {
277 target: ScaleFieldTarget::SourceNodeRest {
278 source_node_index: node.source_node_index,
279 field,
280 },
281 disposition: field_disposition(active, source_rule),
282 element_count: 1,
283 });
284 };
285 match node.local_rest {
286 SourceNodeLocalRest::Trs { .. } => {
287 push(
288 &mut fields,
289 ScaleSourceRestField::Translation,
290 parent_rewrite,
291 );
292 push(&mut fields, ScaleSourceRestField::Rotation, false);
293 push(&mut fields, ScaleSourceRestField::Scale, local_rewrite);
294 }
295 SourceNodeLocalRest::Matrix(_) => {
296 push(
297 &mut fields,
298 ScaleSourceRestField::MatrixLinear,
299 local_rewrite,
300 );
301 push(
302 &mut fields,
303 ScaleSourceRestField::MatrixTranslation,
304 parent_rewrite,
305 );
306 push(&mut fields, ScaleSourceRestField::MatrixHomogeneous, false);
307 }
308 }
309 }
310
311 let mut has_tracks = false;
312 for (clip_index, clip) in document.clips.iter().enumerate() {
313 for (track_index, track) in clip.tracks.iter().enumerate() {
314 has_tracks = true;
315 let parent_in_domain = document
316 .skeleton
317 .bones
318 .get(track.bone)
319 .and_then(|bone| bone.parent)
320 .is_some_and(|parent| affected.contains(&parent));
321 let disposition = match track.property {
322 Property::Translation if whole_document => {
323 field_disposition(factor_changes, ScaleRewriteRule::WholeDocumentLength)
324 }
325 Property::Translation if affected.contains(&track.bone) => field_disposition(
326 factor_changes && parent_in_domain,
327 ScaleRewriteRule::RestBindParentBasis,
328 ),
329 Property::Scale if !whole_document && affected.contains(&track.bone) => {
330 field_disposition(
331 factor_changes && !parent_in_domain,
332 ScaleRewriteRule::RestBindLocalScale,
333 )
334 }
335 _ => ScaleFieldDisposition::PreserveExact,
336 };
337 fields.push(ScaleFieldPlan {
338 target: ScaleFieldTarget::AnimationValues {
339 clip_index,
340 track_index,
341 bone: track.bone,
342 property: track.property,
343 },
344 disposition,
345 element_count: track.values.len(),
346 });
347 }
348 }
349
350 let mut has_affected_slots = false;
351 let mut has_unaffected_slots = false;
352 let mut has_skinned_instances = false;
353 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
354 let instance_affected = instance
355 .skin_joints
356 .iter()
357 .any(|joint| affected.contains(joint));
358 if instance_affected {
359 has_skinned_instances = true;
360 }
361 let slots: Vec<_> = if whole_document {
362 instance
363 .skin_ibms
364 .iter()
365 .enumerate()
366 .filter_map(|(slot, _)| {
367 instance
368 .skin_joints
369 .get(slot)
370 .copied()
371 .map(|joint| (slot, joint))
372 })
373 .collect()
374 } else {
375 instance.skin_joints.iter().copied().enumerate().collect()
376 };
377 for (slot, joint) in slots {
378 if whole_document || instance_affected {
379 has_affected_slots = true;
380 } else {
381 has_unaffected_slots = true;
382 }
383 fields.push(ScaleFieldPlan {
384 target: ScaleFieldTarget::InstanceInverseBind {
385 instance_index,
386 slot,
387 joint,
388 },
389 disposition: if whole_document {
390 field_disposition(factor_changes, ScaleRewriteRule::WholeDocumentLength)
391 } else if instance_affected {
392 ScaleFieldDisposition::Rewrite(ScaleRewriteRule::RestBindNodeBasis)
393 } else {
394 ScaleFieldDisposition::PreserveExact
395 },
396 element_count: 1,
397 });
398 }
399 }
400
401 let mut has_primitives = false;
402 for (mesh_index, mesh) in document.assets.meshes.iter().enumerate() {
403 for (primitive_index, primitive) in mesh.primitives.iter().enumerate() {
404 has_primitives = true;
405 fields.push(ScaleFieldPlan {
406 target: ScaleFieldTarget::MeshPositions {
407 mesh_index,
408 primitive_index,
409 },
410 disposition: if whole_document {
411 field_disposition(factor_changes, ScaleRewriteRule::WholeDocumentLength)
412 } else {
413 ScaleFieldDisposition::PreserveExact
414 },
415 element_count: primitive.positions.len(),
416 });
417 fields.push(ScaleFieldPlan {
418 target: ScaleFieldTarget::MeshNormals {
419 mesh_index,
420 primitive_index,
421 },
422 disposition: ScaleFieldDisposition::PreserveExact,
423 element_count: primitive.normals.len(),
424 });
425 }
426 }
427
428 let has_unaffected_nodes = affected.len() != document.skeleton.bones.len();
429 let sampled = sampled_evidence(document, &affected);
430 let has_connectors = topology
431 .iter()
432 .any(|row| matches!(row.kind, ScaleSourceNodeKind::Connector));
433 let mut obligations = vec![
434 ScaleProofObligation::ExactTopology,
435 ScaleProofObligation::ExactPayloadIdentity,
436 ];
437 if has_unaffected_nodes {
438 obligations.push(ScaleProofObligation::ExactUnchangedWorldRest);
439 }
440 if !affected_nodes.is_empty() {
441 obligations.push(if whole_document {
442 ScaleProofObligation::RestWorld
443 } else {
444 ScaleProofObligation::RestWorldAndUnitScale
445 });
446 }
447 if !transform_only_attachments.is_empty() {
448 obligations.push(ScaleProofObligation::TransformOnlyAffine);
449 }
450 if has_tracks {
451 obligations.push(ScaleProofObligation::TrackValues);
452 }
453 if has_primitives {
454 obligations.push(ScaleProofObligation::MeshPositions);
455 }
456 if sampled.key_translations {
457 obligations.push(ScaleProofObligation::KeyTranslations);
458 }
459 if sampled.cubic_interiors {
460 obligations.push(ScaleProofObligation::CubicInteriors);
461 }
462 if sampled.sample_times {
463 obligations.push(ScaleProofObligation::Trajectories);
464 }
465 if has_skinned_instances {
466 obligations.push(ScaleProofObligation::SkinAndBounds);
467 }
468 if has_affected_slots {
469 obligations.push(ScaleProofObligation::AffectedInverseBinds);
470 }
471 if has_unaffected_slots {
472 obligations.push(ScaleProofObligation::UnaffectedInverseBinds);
473 }
474 if has_connectors {
475 obligations.push(ScaleProofObligation::ExactConnectorProjection);
476 }
477 ScaleLedger {
478 field_rows: fields,
479 payload_shapes: compile_payload_shapes(document),
480 obligations,
481 }
482}
483
484fn plan_whole_document(document: &Document, factor: f64) -> Result<ScalePlan, ScaleError> {
485 if !factor.is_finite() || factor <= 0.0 {
486 return Err(ScaleError::InvalidFactor { factor });
487 }
488 check_factor_narrows(factor, factor)?;
489 let affected_nodes: Vec<BoneId> = (0..document.skeleton.bones.len()).collect();
490 let source_topology = whole_document_source_topology(document);
491 let ledger = compile_scale_ledger(
492 document,
493 ScaleOperation::WholeDocumentLinearUnits { factor },
494 &affected_nodes,
495 &[],
496 &source_topology,
497 );
498 Ok(ScalePlan {
499 tolerance_policy: ScaleTolerancePolicy::APPENDIX_D_V6,
500 observed_factor: factor,
502 affected_nodes,
503 source_topology,
504 ledger,
505 compiled: ScaleCompiledPlan::WholeDocument(WholeDocumentParams { factor }),
506 })
507}
508
509fn plan_rest_bind(
510 document: &Document,
511 source_skin_index: usize,
512 source_root_node_index: usize,
513 expected_factor: f64,
514) -> Result<ScalePlan, ScaleError> {
515 if !expected_factor.is_finite() || expected_factor <= 0.0 {
516 return Err(ScaleError::InvalidExpectedFactor {
517 factor: expected_factor,
518 });
519 }
520 check_factor_narrows(expected_factor, expected_factor)?;
523 check_factor_narrows(expected_factor, 1.0 / expected_factor)?;
524 let domain = derive_rest_bind_plan_domain(document, source_skin_index, source_root_node_index)?;
525 let by_source_index = source_node_index_map(document);
526 let bone_of_source = domain.bone_of_source();
527 let connector_sources = domain.connector_sources();
528
529 let tol = ScaleTolerancePolicy::APPENDIX_D_V6;
530 let mut world_cache = BTreeMap::new();
531 let mut node_factor: BTreeMap<BoneId, f64> = BTreeMap::new();
532 for (&source, &bone) in &bone_of_source {
533 let world = source_world_matrix(
534 source,
535 &by_source_index,
536 &connector_sources,
537 &mut world_cache,
538 )?;
539 let linear = Mat3::from_mat4(world);
540 let factor = classify_affine(linear, &tol)
541 .map_err(|reason| ScaleError::InvalidAffineDomain { node: bone, reason })?;
542 node_factor.insert(bone, factor);
543 }
544 let observed_common = node_factor[&domain.scaled_root_bone];
554 if !tol.relative(tol.common_factor, observed_common, expected_factor) {
555 return Err(ScaleError::FactorMismatch {
556 expected: expected_factor,
557 observed: observed_common,
558 });
559 }
560 for (&bone, &factor) in &node_factor {
561 if bone == domain.scaled_root_bone {
562 continue;
563 }
564 if !tol.relative(tol.common_factor, factor, observed_common) {
565 return Err(ScaleError::MixedFactor {
566 expected: observed_common,
567 observed: factor,
568 node: bone,
569 });
570 }
571 }
572
573 let affected_nodes = domain.affected_nodes();
574 let transform_only_attachments = domain.transform_only_attachments();
575 let operation = ScaleOperation::RestBindUniformScale {
576 source_skin_index,
577 source_root_node_index,
578 expected_factor,
579 };
580 let ledger = compile_scale_ledger(
581 document,
582 operation,
583 &affected_nodes,
584 &transform_only_attachments,
585 &domain.source_rows,
586 );
587 Ok(ScalePlan {
588 tolerance_policy: tol,
589 observed_factor: observed_common,
594 affected_nodes,
595 source_topology: domain.source_rows,
596 ledger,
597 compiled: ScaleCompiledPlan::RestBind(RestBindParams {
598 source_skin_index,
599 source_root_node_index,
600 expected_factor,
601 transform_only_attachments,
602 }),
603 })
604}
605
606pub(in crate::scale) fn validate_plan_document_inventory(
615 document: &Document,
616 plan: &ScalePlan,
617) -> Result<(), ScaleError> {
618 validate_scale_input(document)?;
619 let validate = |affected_nodes: &[BoneId],
620 source_topology: &[ScaleSourceTopologyRow],
621 transform_only_attachments: &[BoneId],
622 ledger: &ScaleLedger| {
623 let reason = if affected_nodes != plan.affected_nodes() {
624 Some("affected_nodes_mismatch")
625 } else if source_topology != plan.source_topology {
626 Some("affected_source_topology_mismatch")
627 } else if transform_only_attachments != plan.transform_only_attachments() {
628 Some("transform_only_attachments_mismatch")
629 } else if ledger.obligations != plan.obligations() {
630 Some("proof_obligations_mismatch")
631 } else if ledger.payload_shapes != plan.ledger.payload_shapes {
632 Some("payload_shape_inventory_mismatch")
633 } else if ledger.field_rows != plan.field_rows() {
634 Some("field_write_set_mismatch")
635 } else {
636 None
637 };
638 if let Some(reason) = reason {
639 return Err(ScaleError::PlanDocumentMismatch { reason });
640 }
641 Ok(())
642 };
643
644 match plan.operation() {
645 ScaleOperation::WholeDocumentLinearUnits { factor } => {
646 let derived = plan_whole_document(document, factor)?;
647 validate(
648 derived.affected_nodes(),
649 &derived.source_topology,
650 derived.transform_only_attachments(),
651 &derived.ledger,
652 )?;
653 Ok(())
654 }
655 ScaleOperation::RestBindUniformScale {
656 source_skin_index,
657 source_root_node_index,
658 ..
659 } => {
660 let domain =
661 derive_rest_bind_plan_domain(document, source_skin_index, source_root_node_index)?;
662 let affected_nodes = domain.affected_nodes();
663 let transform_only_attachments = domain.transform_only_attachments();
664 let ledger = compile_scale_ledger(
665 document,
666 plan.operation(),
667 &affected_nodes,
668 &transform_only_attachments,
669 &domain.source_rows,
670 );
671 validate(
672 &affected_nodes,
673 &domain.source_rows,
674 &transform_only_attachments,
675 &ledger,
676 )?;
677 Ok(())
678 }
679 }
680}
681
682pub(in crate::scale) fn classify_affine(
683 linear: Mat3,
684 tol: &ScaleTolerancePolicy,
685) -> Result<f64, AffineDomainViolation> {
686 classify_positive_uniform_affine(
687 linear,
688 PositiveUniformAffineTolerance {
689 equal_axis: tol.equal_axis,
690 relative_orthogonality: tol.relative_orthogonality,
691 singular_determinant_relative: tol.singular_determinant_relative,
692 },
693 )
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
708struct SampledEvidence {
709 key_translations: bool,
714 cubic_interiors: bool,
721 sample_times: bool,
726}
727
728fn sampled_evidence(document: &Document, affected: &BTreeSet<BoneId>) -> SampledEvidence {
730 let mut evidence = SampledEvidence::default();
731 for clip in &document.clips {
732 let mut translations = false;
733 let mut cubic_segments = false;
734 for track in &clip.tracks {
735 if !affected.contains(&track.bone) || track.times.is_empty() {
736 continue;
737 }
738 evidence.sample_times = true;
739 translations |= track.property == Property::Translation;
740 cubic_segments |=
741 track.interpolation == Interpolation::CubicSpline && track.times.len() >= 2;
742 }
743 evidence.key_translations |= translations;
744 evidence.cubic_interiors |= translations && cubic_segments;
745 }
746 evidence
747}