use animsmith_core::{
InputIdentity, RAW_SOURCE_V1_MAX_TEXT_BYTES, RawSourceFactsBuilderV1, SourceAxisV1,
SourceChannelFactV1, SourceChannelPropertyV1, SourceClipFactV1, SourceComponentMaskV1,
SourceConstructFactV1, SourceConstructKindV1, SourceCoordinateBasisV1, SourceFactDomainV1,
SourceFactSetV1, SourceFormatV1, SourceFramesPerSecondV1, SourceLinearUnitV1,
SourceLoaderDispositionV1, SourceLogicalLocatorV1, SourceObservationV1, SourceProvenanceV1,
SourceResourceKindV1, SourceResourceLocatorV1, SourceResourceReferenceV1, SourceTargetKindV1,
SourceTargetV1, SourceTextV1, SourceTimeRangeV1, SourceUnavailableReasonV1,
};
pub(crate) fn project(
scene: &ufbx::Scene,
construct_counts: SourceConstructCounts,
primary_bytes: &[u8],
) -> RawSourceFactsBuilderV1 {
let mut builder = RawSourceFactsBuilderV1::new(
SourceFormatV1::Fbx,
InputIdentity::from_bytes(primary_bytes),
);
project_coordinate_facts(scene, &mut builder);
project_clips(scene, &mut builder);
project_constructs(construct_counts, &mut builder);
project_resources(scene, &mut builder);
builder
}
fn project_coordinate_facts(scene: &ufbx::Scene, builder: &mut RawSourceFactsBuilderV1) {
let unit_provenance = parser_provenance("fbx:scene.settings.unit_meters");
builder.set_linear_unit(match SourceLinearUnitV1::new(scene.settings.unit_meters) {
Ok(unit) => SourceObservationV1::observed(
unit,
unit_provenance,
SourceLoaderDispositionV1::Normalized,
),
Err(_) => SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
Some(unit_provenance),
SourceLoaderDispositionV1::Normalized,
),
});
let basis_provenance = parser_provenance("fbx:scene.settings.axes");
let axes = scene.settings.axes;
let basis = axis(axes.right)
.zip(axis(axes.up))
.zip(axis(axes.front))
.and_then(|((right, up), forward)| SourceCoordinateBasisV1::new(right, up, forward).ok());
builder.set_coordinate_basis(match basis {
Some(basis) => SourceObservationV1::observed(
basis,
basis_provenance,
SourceLoaderDispositionV1::Normalized,
),
None => SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
Some(basis_provenance),
SourceLoaderDispositionV1::Normalized,
),
});
let fps_provenance = parser_provenance("fbx:scene.settings.frames_per_second");
builder.set_frames_per_second(
match SourceFramesPerSecondV1::new(scene.settings.frames_per_second) {
Ok(fps) => SourceObservationV1::observed(
fps,
fps_provenance,
SourceLoaderDispositionV1::Discarded,
),
Err(_) => SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
Some(fps_provenance),
SourceLoaderDispositionV1::Discarded,
),
},
);
}
fn axis(axis: ufbx::CoordinateAxis) -> Option<SourceAxisV1> {
match axis {
ufbx::CoordinateAxis::PositiveX => Some(SourceAxisV1::PositiveX),
ufbx::CoordinateAxis::NegativeX => Some(SourceAxisV1::NegativeX),
ufbx::CoordinateAxis::PositiveY => Some(SourceAxisV1::PositiveY),
ufbx::CoordinateAxis::NegativeY => Some(SourceAxisV1::NegativeY),
ufbx::CoordinateAxis::PositiveZ => Some(SourceAxisV1::PositiveZ),
ufbx::CoordinateAxis::NegativeZ => Some(SourceAxisV1::NegativeZ),
ufbx::CoordinateAxis::Unknown => None,
}
}
fn project_clips(scene: &ufbx::Scene, builder: &mut RawSourceFactsBuilderV1) {
for (stack_index, stack) in scene.anim_stacks.iter().enumerate() {
if builder.remaining_clip_rows() == 0 || builder.remaining_observation_rows() == 0 {
builder.mark_budget_exceeded(SourceFactDomainV1::Clips);
return;
}
let stack_locator_bytes = "fbx:anim_stacks/"
.len()
.saturating_add(decimal_len_usize(stack_index));
let name_locator_bytes = stack_locator_bytes.saturating_add("/name".len());
let range_locator_bytes = stack_locator_bytes.saturating_add("/time_range".len());
let retained_name_bytes = if stack.element.name.is_empty()
|| stack.element.name.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES
{
0
} else {
stack.element.name.len()
};
let fixed_text_bytes = name_locator_bytes
.saturating_add(retained_name_bytes)
.saturating_add(stack_locator_bytes)
.saturating_add(range_locator_bytes);
if fixed_text_bytes > builder.remaining_text_bytes() {
builder.mark_budget_exceeded(SourceFactDomainV1::Clips);
return;
}
let stack_locator = format!("fbx:anim_stacks/{stack_index}");
let name_locator = format!("{stack_locator}/name");
let range_locator = format!("{stack_locator}/time_range");
let channel_budget = builder.remaining_observation_rows().saturating_sub(1);
let channel_text_budget = builder
.remaining_text_bytes()
.saturating_sub(fixed_text_bytes);
let (channels, channels_truncated) =
project_channels(stack, channel_budget, channel_text_budget);
let channels = if channels_truncated {
SourceFactSetV1::partial(
channels,
SourceUnavailableReasonV1::ProjectionBudgetExceeded,
)
} else {
SourceFactSetV1::complete(channels)
};
let source_name = if stack.element.name.is_empty() {
SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
Some(parser_provenance(&name_locator)),
SourceLoaderDispositionV1::Preserved,
)
} else if stack.element.name.len() > RAW_SOURCE_V1_MAX_TEXT_BYTES {
SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ProjectionBudgetExceeded,
Some(parser_provenance(&name_locator)),
SourceLoaderDispositionV1::Preserved,
)
} else {
SourceObservationV1::observed(
SourceTextV1::new(stack.element.name.as_ref())
.expect("source name length checked before cloning"),
parser_provenance(&name_locator),
SourceLoaderDispositionV1::Preserved,
)
};
let normalized_clip_index = SourceObservationV1::observed(
stack_index,
derived_provenance(&stack_locator),
SourceLoaderDispositionV1::Baked,
);
let source_range = match SourceTimeRangeV1::new(stack.time_begin, stack.time_end) {
Ok(range) => SourceObservationV1::observed(
range,
parser_provenance(&range_locator),
SourceLoaderDispositionV1::Baked,
),
Err(_) => SourceObservationV1::unavailable(
SourceUnavailableReasonV1::Malformed,
Some(parser_provenance(&range_locator)),
SourceLoaderDispositionV1::Baked,
),
};
let sampler_range = SourceObservationV1::unavailable(
SourceUnavailableReasonV1::ParserUnavailable,
None,
SourceLoaderDispositionV1::NotApplicable,
);
if !builder.push_clip(SourceClipFactV1::new(
stack_index,
source_name,
normalized_clip_index,
source_range,
sampler_range,
channels,
)) {
return;
}
if channels_truncated {
return;
}
}
builder.mark_complete(SourceFactDomainV1::Clips);
}
fn project_channels(
stack: &ufbx::AnimStack,
channel_budget: usize,
total_text_budget: usize,
) -> (Vec<SourceChannelFactV1>, bool) {
let mut channels = Vec::new();
let mut retained_text_bytes = 0usize;
for layer in &stack.layers {
let layer_index = layer.element.typed_id as usize;
for (property_index, property) in layer.anim_props.as_ref().iter().enumerate() {
if channels.len() >= channel_budget {
return (channels, true);
}
let channel_index = channels.len();
let (kind, disposition) = property_kind(property.prop_name.as_ref());
let property_name_bytes = if kind == SourceChannelPropertyV1::Other {
property.prop_name.len()
} else {
0
};
if property_name_bytes > RAW_SOURCE_V1_MAX_TEXT_BYTES {
return (channels, true);
}
let locator_bytes = "fbx:anim_layers/"
.len()
.saturating_add(decimal_len_usize(layer_index))
.saturating_add("/anim_props/".len())
.saturating_add(decimal_len_usize(property_index));
let row_text_bytes = locator_bytes
.saturating_mul(2)
.saturating_add(property_name_bytes);
if retained_text_bytes
.checked_add(row_text_bytes)
.is_none_or(|bytes| bytes > total_text_budget)
{
return (channels, true);
}
let locator = format!("fbx:anim_layers/{layer_index}/anim_props/{property_index}");
let provenance = parser_provenance(&locator);
let interpolation = SourceObservationV1::unavailable(
SourceUnavailableReasonV1::BakedAway,
Some(provenance.clone()),
disposition,
);
let target = if property.element.type_ == ufbx::ElementType::Node {
SourceTargetV1::new(
SourceTargetKindV1::Node,
u64::from(property.element.typed_id),
)
} else {
SourceTargetV1::new(
SourceTargetKindV1::Element,
u64::from(property.element.element_id),
)
};
let curves = &property.anim_value.curves;
let mut row = SourceChannelFactV1::new(
channel_index,
target,
kind,
SourceComponentMaskV1::new(
curves[0].is_some(),
curves[1].is_some(),
curves[2].is_some(),
),
interpolation,
disposition,
provenance,
)
.with_source_layer_index(layer_index);
if kind == SourceChannelPropertyV1::Other && !property.prop_name.is_empty() {
row = row.with_property_name(
SourceTextV1::new(property.prop_name.as_ref())
.expect("source property length checked before cloning"),
);
}
channels.push(row);
retained_text_bytes = retained_text_bytes.saturating_add(row_text_bytes);
}
}
(channels, false)
}
fn property_kind(name: &str) -> (SourceChannelPropertyV1, SourceLoaderDispositionV1) {
match name {
"Lcl Translation" => (
SourceChannelPropertyV1::Translation,
SourceLoaderDispositionV1::Baked,
),
"Lcl Rotation" => (
SourceChannelPropertyV1::Rotation,
SourceLoaderDispositionV1::Baked,
),
"Lcl Scaling" => (
SourceChannelPropertyV1::Scale,
SourceLoaderDispositionV1::Baked,
),
_ => (
SourceChannelPropertyV1::Other,
SourceLoaderDispositionV1::Unsupported,
),
}
}
fn project_constructs(counts: SourceConstructCounts, builder: &mut RawSourceFactsBuilderV1) {
let mut source_order_index = 0usize;
for (name, kind, count, disposition, locator) in [
(
"fbx:user-defined-properties",
SourceConstructKindV1::CustomProperty,
counts.rest_bind.user_defined_property_count,
SourceLoaderDispositionV1::Unsupported,
"fbx:scene.elements.props",
),
(
"fbx:unmodeled-elements",
SourceConstructKindV1::UnknownElement,
counts.rest_bind.total_unmodeled_element_count(),
SourceLoaderDispositionV1::Unsupported,
"fbx:scene.elements",
),
(
"fbx:stackless-animation",
SourceConstructKindV1::UnknownElement,
counts.stackless_animation_count,
SourceLoaderDispositionV1::Unsupported,
"fbx:scene.animation",
),
] {
if count == 0 {
continue;
}
let required_text = name.len().saturating_add(locator.len());
if builder.remaining_observation_rows() == 0
|| required_text > builder.remaining_text_bytes()
{
builder.mark_budget_exceeded(SourceFactDomainV1::Constructs);
return;
}
let row = SourceConstructFactV1::new(
source_order_index,
kind,
SourceTextV1::new(name).expect("static construct name is bounded"),
false,
u64::try_from(count).unwrap_or(u64::MAX),
disposition,
parser_provenance(locator),
)
.expect("zero aggregate construct counts are skipped");
if !builder.push_construct(row) {
return;
}
source_order_index = source_order_index.saturating_add(1);
}
builder.mark_complete(SourceFactDomainV1::Constructs);
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SourceConstructCounts {
pub(crate) rest_bind: RestBindSourceConstructCounts,
stackless_animation_count: usize,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RestBindSourceConstructCounts {
pub(crate) user_defined_property_count: usize,
pub(crate) safe_texture_file_link_count: usize,
admitted_unmodeled_element_count: usize,
unsupported_unmodeled_element_counts: [(&'static str, usize); 23],
}
impl RestBindSourceConstructCounts {
pub(crate) fn total_unmodeled_element_count(self) -> usize {
self.safe_texture_file_link_count
.saturating_add(self.admitted_unmodeled_element_count)
.saturating_add(self.unsupported_unmodeled_element_count())
}
pub(crate) fn unsupported_unmodeled_element_count(self) -> usize {
self.unsupported_unmodeled_element_counts
.into_iter()
.fold(0usize, |total, (_, count)| total.saturating_add(count))
}
pub(crate) fn unsupported_kind_counts(self) -> impl Iterator<Item = (&'static str, usize)> {
self.unsupported_unmodeled_element_counts
.into_iter()
.filter(|(_, count)| *count > 0)
}
}
pub(crate) fn construct_counts(scene: &ufbx::Scene) -> SourceConstructCounts {
let user_defined_property_count = scene
.elements
.iter()
.flat_map(|element| element.props.props.iter())
.filter(|prop| prop.flags.has_any(ufbx::PropFlags::USER_DEFINED))
.count();
let mut rest_bind = rest_bind_unmodeled_element_counts(scene);
rest_bind.user_defined_property_count = user_defined_property_count;
let stackless_animation_count = if scene.anim_stacks.is_empty() {
scene
.anim_layers
.len()
.saturating_add(scene.anim_values.len())
.saturating_add(scene.anim_curves.len())
} else {
0
};
SourceConstructCounts {
rest_bind,
stackless_animation_count,
}
}
#[derive(Debug, Clone, Copy, Default)]
struct BindPoseReconciliationCounts {
admitted: usize,
non_bind: usize,
incomplete: usize,
ambiguous: usize,
non_finite: usize,
mismatched: usize,
allocation_budget_exceeded: usize,
}
const MAX_BIND_POSE_RECONCILIATION_NODES: usize = 65_536;
const MAX_BIND_POSE_RECONCILIATION_CLUSTERS: usize = 65_536;
#[derive(Debug, Clone, Copy)]
enum BindPoseReconciliation {
Admitted,
NonBind,
Incomplete,
Ambiguous,
NonFinite,
Mismatched,
}
fn reconcile_bind_poses(scene: &ufbx::Scene) -> BindPoseReconciliationCounts {
let bind_pose_count = scene.poses.iter().filter(|pose| pose.is_bind_pose).count();
let non_bind_pose_count = scene.poses.len().saturating_sub(bind_pose_count);
if bind_pose_count == 0 {
return BindPoseReconciliationCounts {
non_bind: non_bind_pose_count,
..BindPoseReconciliationCounts::default()
};
}
if !bind_pose_reconciliation_allocation_within_budget(
scene.nodes.len(),
scene.skin_clusters.len(),
) {
return BindPoseReconciliationCounts {
non_bind: non_bind_pose_count,
allocation_budget_exceeded: bind_pose_count,
..BindPoseReconciliationCounts::default()
};
}
let mut bind_pose_coverage = vec![0u8; scene.nodes.len()];
for pose in &scene.poses {
if !pose.is_bind_pose {
continue;
}
for bone_pose in &pose.bone_poses {
let Ok(node_index) = usize::try_from(bone_pose.bone_node.element.typed_id) else {
continue;
};
if let Some(coverage) = bind_pose_coverage.get_mut(node_index) {
*coverage = coverage.saturating_add(1);
}
}
}
let mut clusters_by_node = vec![Vec::new(); scene.nodes.len()];
for (cluster_index, cluster) in scene.skin_clusters.iter().enumerate() {
let Some(bone_node) = &cluster.bone_node else {
continue;
};
let Ok(node_index) = usize::try_from(bone_node.element.typed_id) else {
continue;
};
if let Some(indices) = clusters_by_node.get_mut(node_index) {
indices.push(cluster_index);
}
}
let mut counts = BindPoseReconciliationCounts::default();
for pose in &scene.poses {
let outcome = reconcile_bind_pose(scene, pose, &bind_pose_coverage, &clusters_by_node);
match outcome {
BindPoseReconciliation::Admitted => counts.admitted = counts.admitted.saturating_add(1),
BindPoseReconciliation::NonBind => counts.non_bind = counts.non_bind.saturating_add(1),
BindPoseReconciliation::Incomplete => {
counts.incomplete = counts.incomplete.saturating_add(1)
}
BindPoseReconciliation::Ambiguous => {
counts.ambiguous = counts.ambiguous.saturating_add(1)
}
BindPoseReconciliation::NonFinite => {
counts.non_finite = counts.non_finite.saturating_add(1)
}
BindPoseReconciliation::Mismatched => {
counts.mismatched = counts.mismatched.saturating_add(1)
}
}
}
counts
}
fn bind_pose_reconciliation_allocation_within_budget(
node_count: usize,
cluster_count: usize,
) -> bool {
node_count <= MAX_BIND_POSE_RECONCILIATION_NODES
&& cluster_count <= MAX_BIND_POSE_RECONCILIATION_CLUSTERS
}
fn reconcile_bind_pose(
scene: &ufbx::Scene,
pose: &ufbx::Pose,
bind_pose_coverage: &[u8],
clusters_by_node: &[Vec<usize>],
) -> BindPoseReconciliation {
if !pose.is_bind_pose {
return BindPoseReconciliation::NonBind;
}
if pose.bone_poses.is_empty() {
return BindPoseReconciliation::Incomplete;
}
for skin in &scene.skin_deformers {
if skin.clusters.is_empty() {
continue;
}
let covered = skin
.clusters
.iter()
.filter(|cluster| {
cluster
.bone_node
.as_ref()
.and_then(|node| ufbx::get_bone_pose(pose, node))
.is_some()
})
.count();
if covered > 0 && covered != skin.clusters.len() {
return BindPoseReconciliation::Incomplete;
}
}
let mut previous_node = None;
for bone_pose in &pose.bone_poses {
let Ok(node_index) = usize::try_from(bone_pose.bone_node.element.typed_id) else {
return BindPoseReconciliation::Ambiguous;
};
if previous_node == Some(node_index)
|| bind_pose_coverage.get(node_index).copied() != Some(1)
{
return BindPoseReconciliation::Ambiguous;
}
previous_node = Some(node_index);
if !matrix_is_finite(&bone_pose.bone_to_world) {
return BindPoseReconciliation::NonFinite;
}
let Some(node) = scene.nodes.get(node_index) else {
return BindPoseReconciliation::Ambiguous;
};
let Some(cluster_indices) = clusters_by_node.get(node_index) else {
return BindPoseReconciliation::Ambiguous;
};
if cluster_indices.is_empty() {
let outcome = compare_bind_pose_matrix(&bone_pose.bone_to_world, &node.node_to_world);
if !matches!(outcome, BindPoseReconciliation::Admitted) {
return outcome;
}
continue;
}
for cluster_index in cluster_indices {
let Some(cluster) = scene.skin_clusters.get(*cluster_index) else {
return BindPoseReconciliation::Ambiguous;
};
let outcome =
compare_bind_pose_matrix(&bone_pose.bone_to_world, &cluster.bind_to_world);
if !matches!(outcome, BindPoseReconciliation::Admitted) {
return outcome;
}
}
}
BindPoseReconciliation::Admitted
}
fn compare_bind_pose_matrix(
actual: &ufbx::Matrix,
expected: &ufbx::Matrix,
) -> BindPoseReconciliation {
if !matrix_is_finite(expected) {
BindPoseReconciliation::NonFinite
} else if !matrices_approximately_equal(actual, expected) {
BindPoseReconciliation::Mismatched
} else {
BindPoseReconciliation::Admitted
}
}
fn matrix_is_finite(matrix: &ufbx::Matrix) -> bool {
matrix_components(matrix).into_iter().all(f64::is_finite)
}
fn matrices_approximately_equal(left: &ufbx::Matrix, right: &ufbx::Matrix) -> bool {
matrix_components(left)
.into_iter()
.zip(matrix_components(right))
.all(|(left, right)| {
left.is_finite()
&& right.is_finite()
&& scalar_difference_within_tolerance(
(left - right).abs(),
left.abs().max(right.abs()),
)
})
}
fn scalar_difference_within_tolerance(difference: f64, magnitude: f64) -> bool {
let policy = animsmith_core::scale::ScaleTolerancePolicy::APPENDIX_D_V6;
difference <= policy.scalar_absolute + policy.scalar_relative * magnitude
}
fn matrix_components(matrix: &ufbx::Matrix) -> [f64; 12] {
[
matrix.m00, matrix.m10, matrix.m20, matrix.m01, matrix.m11, matrix.m21, matrix.m02,
matrix.m12, matrix.m22, matrix.m03, matrix.m13, matrix.m23,
]
}
fn rest_bind_unmodeled_element_counts(scene: &ufbx::Scene) -> RestBindSourceConstructCounts {
let bind_poses = reconcile_bind_poses(scene);
let ufbx::Scene {
metadata: _,
settings: _,
root_node: _,
anim: _,
unknowns,
nodes: _,
meshes: _,
lights: _,
cameras: _,
bones: _,
empties: _,
line_curves,
nurbs_curves,
nurbs_surfaces,
nurbs_trim_surfaces,
nurbs_trim_boundaries,
procedural_geometries,
stereo_cameras,
camera_switchers,
markers,
lod_groups,
skin_deformers: _,
skin_clusters: _,
blend_deformers: _,
blend_channels,
blend_shapes,
cache_deformers: _,
cache_files,
materials: _,
textures: _,
videos: _,
shaders,
shader_bindings,
anim_stacks: _,
anim_layers: _,
anim_values: _,
anim_curves: _,
display_layers,
selection_sets,
selection_nodes,
characters,
constraints,
audio_layers,
audio_clips,
poses: _,
metadata_objects,
texture_files,
elements: _,
connections_src: _,
connections_dst: _,
elements_by_name: _,
dom_root: _,
} = scene;
RestBindSourceConstructCounts {
user_defined_property_count: 0,
safe_texture_file_link_count: texture_files.len(),
admitted_unmodeled_element_count: stereo_cameras
.len()
.saturating_add(camera_switchers.len())
.saturating_add(markers.len())
.saturating_add(lod_groups.len())
.saturating_add(shaders.len())
.saturating_add(shader_bindings.len())
.saturating_add(display_layers.len())
.saturating_add(bind_poses.admitted),
unsupported_unmodeled_element_counts: [
("unknowns", unknowns.len()),
("line_curves", line_curves.len()),
("nurbs_curves", nurbs_curves.len()),
("nurbs_surfaces", nurbs_surfaces.len()),
("nurbs_trim_surfaces", nurbs_trim_surfaces.len()),
("nurbs_trim_boundaries", nurbs_trim_boundaries.len()),
("procedural_geometries", procedural_geometries.len()),
("blend_channels", blend_channels.len()),
("blend_shapes", blend_shapes.len()),
("cache_files", cache_files.len()),
("selection_sets", selection_sets.len()),
("selection_nodes", selection_nodes.len()),
("characters", characters.len()),
("constraints", constraints.len()),
("audio_layers", audio_layers.len()),
("audio_clips", audio_clips.len()),
("non_bind_poses", bind_poses.non_bind),
("incomplete_bind_poses", bind_poses.incomplete),
("ambiguous_bind_poses", bind_poses.ambiguous),
("non_finite_bind_poses", bind_poses.non_finite),
("mismatched_bind_poses", bind_poses.mismatched),
(
"bind_pose_reconciliation_budget_exceeded",
bind_poses.allocation_budget_exceeded,
),
("metadata_objects", metadata_objects.len()),
],
}
}
fn project_resources(scene: &ufbx::Scene, builder: &mut RawSourceFactsBuilderV1) {
for resource in resource_declarations(scene) {
if builder.remaining_resource_rows() == 0 || builder.remaining_observation_rows() == 0 {
builder.mark_budget_exceeded(SourceFactDomainV1::Resources);
return;
}
let (value, field, redacted_locator) = resource.source_locator();
let retained_locator_bytes =
value.map_or(0, SourceResourceLocatorV1::retained_relative_bytes);
let provenance_locator_bytes = "fbx:"
.len()
.saturating_add(resource.type_name.len())
.saturating_add(1)
.saturating_add(decimal_len_u64(resource.source_index()))
.saturating_add(1)
.saturating_add(field.len());
if provenance_locator_bytes.saturating_add(retained_locator_bytes)
> builder.remaining_text_bytes()
{
builder.mark_budget_exceeded(SourceFactDomainV1::Resources);
return;
}
let provenance_locator = format!(
"fbx:{}/{}/{field}",
resource.type_name,
resource.source_index()
);
let locator = redacted_locator.unwrap_or_else(|| {
value.map_or(SourceResourceLocatorV1::Missing, |value| {
SourceResourceLocatorV1::classify(value)
})
});
let row = SourceResourceReferenceV1::new(
resource.source_order_index(),
resource.kind(),
resource.source_index(),
locator,
resource.disposition,
parser_provenance(&provenance_locator),
);
if !builder.push_resource(row) {
return;
}
}
builder.mark_complete(SourceFactDomainV1::Resources);
}
#[derive(Clone, Copy)]
enum FbxResourceList {
Texture,
Video,
Cache,
}
impl FbxResourceList {
const fn tie_breaker(self) -> u8 {
match self {
Self::Texture => 0,
Self::Video => 1,
Self::Cache => 2,
}
}
}
fn next_resource_list(
scene: &ufbx::Scene,
texture_index: usize,
video_index: usize,
cache_index: usize,
) -> Option<FbxResourceList> {
[
scene
.textures
.get(texture_index)
.map(|texture| (texture.element.element_id, FbxResourceList::Texture)),
scene
.videos
.get(video_index)
.map(|video| (video.element.element_id, FbxResourceList::Video)),
scene
.cache_files
.get(cache_index)
.map(|cache| (cache.element.element_id, FbxResourceList::Cache)),
]
.into_iter()
.flatten()
.min_by_key(|(element_id, list)| (*element_id, list.tie_breaker()))
.map(|(_, list)| list)
}
pub(crate) struct ResourceDeclaration<'a> {
source_order_index: usize,
kind: SourceResourceKindV1,
source_index: u64,
embedded: bool,
relative_filename: &'a str,
filename: &'a str,
absolute_filename_present: bool,
disposition: SourceLoaderDispositionV1,
type_name: &'static str,
}
impl ResourceDeclaration<'_> {
pub(crate) const fn source_order_index(&self) -> usize {
self.source_order_index
}
pub(crate) const fn kind(&self) -> SourceResourceKindV1 {
self.kind
}
pub(crate) const fn source_index(&self) -> u64 {
self.source_index
}
pub(crate) const fn is_embedded(&self) -> bool {
self.embedded
}
fn source_locator(&self) -> (Option<&str>, &'static str, Option<SourceResourceLocatorV1>) {
if self.is_embedded() {
(None, "content", Some(SourceResourceLocatorV1::Embedded))
} else if !self.relative_filename.is_empty() {
(Some(self.relative_filename), "relative_filename", None)
} else if !self.filename.is_empty() {
(Some(self.filename), "filename", None)
} else if self.absolute_filename_present {
(
None,
"absolute_filename",
Some(SourceResourceLocatorV1::Absolute),
)
} else {
(None, "filename", Some(SourceResourceLocatorV1::Missing))
}
}
}
pub(crate) struct ResourceDeclarations<'a> {
scene: &'a ufbx::Scene,
source_order_index: usize,
texture_index: usize,
video_index: usize,
cache_index: usize,
}
pub(crate) fn resource_declarations(scene: &ufbx::Scene) -> ResourceDeclarations<'_> {
ResourceDeclarations {
scene,
source_order_index: 0,
texture_index: 0,
video_index: 0,
cache_index: 0,
}
}
impl<'a> Iterator for ResourceDeclarations<'a> {
type Item = ResourceDeclaration<'a>;
fn next(&mut self) -> Option<Self::Item> {
let list = next_resource_list(
self.scene,
self.texture_index,
self.video_index,
self.cache_index,
)?;
let source_order_index = self.source_order_index;
self.source_order_index = self.source_order_index.saturating_add(1);
Some(match list {
FbxResourceList::Texture => {
let texture = &self.scene.textures[self.texture_index];
self.texture_index = self.texture_index.saturating_add(1);
ResourceDeclaration {
source_order_index,
kind: SourceResourceKindV1::Texture,
source_index: u64::from(texture.element.typed_id),
embedded: !texture.content.is_empty(),
relative_filename: texture.relative_filename.as_ref(),
filename: texture.filename.as_ref(),
absolute_filename_present: !texture.absolute_filename.is_empty(),
disposition: SourceLoaderDispositionV1::Unknown,
type_name: "textures",
}
}
FbxResourceList::Video => {
let video = &self.scene.videos[self.video_index];
self.video_index = self.video_index.saturating_add(1);
ResourceDeclaration {
source_order_index,
kind: SourceResourceKindV1::Video,
source_index: u64::from(video.element.typed_id),
embedded: !video.content.is_empty(),
relative_filename: video.relative_filename.as_ref(),
filename: video.filename.as_ref(),
absolute_filename_present: !video.absolute_filename.is_empty(),
disposition: SourceLoaderDispositionV1::Discarded,
type_name: "videos",
}
}
FbxResourceList::Cache => {
let cache = &self.scene.cache_files[self.cache_index];
self.cache_index = self.cache_index.saturating_add(1);
ResourceDeclaration {
source_order_index,
kind: SourceResourceKindV1::Cache,
source_index: u64::from(cache.element.typed_id),
embedded: false,
relative_filename: cache.relative_filename.as_ref(),
filename: cache.filename.as_ref(),
absolute_filename_present: !cache.absolute_filename.is_empty(),
disposition: SourceLoaderDispositionV1::Unsupported,
type_name: "cache_files",
}
}
})
}
}
fn parser_provenance(locator: &str) -> SourceProvenanceV1 {
SourceProvenanceV1::parser_projected(
SourceLogicalLocatorV1::fbx_parser_path(locator)
.expect("generated FBX logical locator is bounded and structural"),
)
}
fn derived_provenance(locator: &str) -> SourceProvenanceV1 {
SourceProvenanceV1::derived_from_source(
SourceLogicalLocatorV1::fbx_parser_path(locator)
.expect("generated FBX logical locator is bounded and structural"),
)
}
fn decimal_len_usize(mut value: usize) -> usize {
let mut digits = 1usize;
while value >= 10 {
value /= 10;
digits += 1;
}
digits
}
fn decimal_len_u64(mut value: u64) -> usize {
let mut digits = 1usize;
while value >= 10 {
value /= 10;
digits += 1;
}
digits
}
#[cfg(test)]
mod tests {
use super::*;
fn matrix_with_component(index: usize, value: f64) -> ufbx::Matrix {
let mut matrix = ufbx::Matrix::default();
match index {
0 => matrix.m00 = value,
1 => matrix.m10 = value,
2 => matrix.m20 = value,
3 => matrix.m01 = value,
4 => matrix.m11 = value,
5 => matrix.m21 = value,
6 => matrix.m02 = value,
7 => matrix.m12 = value,
8 => matrix.m22 = value,
9 => matrix.m03 = value,
10 => matrix.m13 = value,
11 => matrix.m23 = value,
_ => panic!("matrix component index out of range"),
}
matrix
}
#[test]
fn matrix_reconciliation_checks_all_twelve_affine_components() {
for index in 0..12 {
assert!(matrices_approximately_equal(
&matrix_with_component(index, 5.0e-7),
&ufbx::Matrix::default(),
));
assert!(!matrices_approximately_equal(
&matrix_with_component(index, 2.0e-6),
&ufbx::Matrix::default(),
));
}
}
#[test]
fn scalar_reconciliation_includes_the_exact_absolute_and_relative_boundary() {
let policy = animsmith_core::scale::ScaleTolerancePolicy::APPENDIX_D_V6;
for magnitude in [0.0, 100.0] {
let boundary = policy.scalar_absolute + policy.scalar_relative * magnitude;
assert!(scalar_difference_within_tolerance(boundary, magnitude));
assert!(!scalar_difference_within_tolerance(
f64::from_bits(boundary.to_bits() + 1),
magnitude,
));
}
}
#[test]
fn bind_pose_reconciliation_allocation_budget_has_a_fixed_boundary() {
assert!(bind_pose_reconciliation_allocation_within_budget(
MAX_BIND_POSE_RECONCILIATION_NODES,
MAX_BIND_POSE_RECONCILIATION_CLUSTERS,
));
assert!(!bind_pose_reconciliation_allocation_within_budget(
MAX_BIND_POSE_RECONCILIATION_NODES + 1,
0,
));
assert!(!bind_pose_reconciliation_allocation_within_budget(
0,
MAX_BIND_POSE_RECONCILIATION_CLUSTERS + 1,
));
}
}