scena 1.0.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use super::super::transforms::rotate_vec3;
use super::super::{ConnectorKey, NodeKey, NodeKind, Quat, Transform, Vec3};
use super::{ConnectOptions, ConnectionError, ConnectorFrame};

pub(super) fn validate_connector_live(
    connector: &ConnectorFrame,
    key: Option<ConnectorKey>,
) -> Result<(), ConnectionError> {
    if connector.is_live() {
        Ok(())
    } else {
        Err(ConnectionError::StaleConnectorHandle {
            connector: key,
            name: connector.name.clone(),
        })
    }
}

pub(super) fn validate_connector_kinds(
    source: &ConnectorFrame,
    target: &ConnectorFrame,
) -> Result<(), ConnectionError> {
    match (source.kind(), target.kind()) {
        (Some(source_kind), Some(target_kind))
            if source_kind != target_kind
                && !connectors_explicitly_allow_mate(source, source_kind, target, target_kind) =>
        {
            Err(ConnectionError::IncompatibleConnector {
                source_kind: source_kind.to_string(),
                target_kind: target_kind.to_string(),
            })
        }
        (Some(_), Some(_)) | (Some(_), None) | (None, Some(_)) | (None, None) => Ok(()),
    }
}

pub(super) fn validate_connector_source_metadata(
    source: &ConnectorFrame,
    target: &ConnectorFrame,
) -> Result<(), ConnectionError> {
    if source.import_live.is_none()
        && target.import_live.is_none()
        && source.source_units() != target.source_units()
    {
        return Err(ConnectionError::UnitMismatch {
            source_units: source.source_units(),
            target_units: target.source_units(),
        });
    }
    if source.import_live.is_none()
        && target.import_live.is_none()
        && source.source_coordinate_system() != target.source_coordinate_system()
    {
        return Err(ConnectionError::CoordinateSystemMismatch {
            source_coordinate_system: source.source_coordinate_system(),
            target_coordinate_system: target.source_coordinate_system(),
        });
    }
    Ok(())
}

fn connectors_explicitly_allow_mate(
    source: &ConnectorFrame,
    source_kind: &str,
    target: &ConnectorFrame,
    target_kind: &str,
) -> bool {
    source.allowed_mates.iter().any(|kind| kind == target_kind)
        || target.allowed_mates.iter().any(|kind| kind == source_kind)
}

pub(super) fn validate_connector_handedness(
    connector: &ConnectorFrame,
) -> Result<(), ConnectionError> {
    if connector.source_coordinate_system().is_left_handed() {
        return Err(ConnectionError::HandednessMismatch {
            connector: connector.name.clone(),
            coordinate_system: connector.source_coordinate_system(),
        });
    }
    Ok(())
}

pub(super) fn validate_connector_host_prepared(
    connector: &ConnectorFrame,
    kind: &NodeKind,
) -> Result<(), ConnectionError> {
    if matches!(kind, NodeKind::Model(_)) {
        return Err(ConnectionError::ConnectorHostNotPrepared {
            node: connector.node,
            connector: connector.name.clone(),
        });
    }
    Ok(())
}

pub(super) fn validate_connector_transform(
    connector: &ConnectorFrame,
    options: ConnectOptions,
) -> Result<(), ConnectionError> {
    if !is_valid_scale(connector.local_transform.scale)
        || !is_valid_rotation(connector.local_transform.rotation)
    {
        return Err(ConnectionError::DegenerateConnectorFrame {
            connector: connector.name.clone(),
        });
    }
    if has_negative_determinant(connector.local_transform.scale) {
        return Err(ConnectionError::FlippedConnection {
            connector: connector.name.clone(),
            node: None,
        });
    }
    if !options.allow_non_uniform_scale && !is_uniform_scale(connector.local_transform.scale) {
        return Err(ConnectionError::NonUniformScaleConnectionRisk {
            node: connector.node,
        });
    }
    Ok(())
}

pub(super) fn validate_node_transform(
    node: NodeKey,
    transform: Transform,
    options: ConnectOptions,
) -> Result<(), ConnectionError> {
    validate_transform_scale(node, transform, options)
}

pub(super) fn validate_transform_scale(
    node: NodeKey,
    transform: Transform,
    options: ConnectOptions,
) -> Result<(), ConnectionError> {
    if !is_valid_scale(transform.scale) || !is_valid_rotation(transform.rotation) {
        return Err(ConnectionError::DegenerateConnectorFrame { connector: None });
    }
    if has_negative_determinant(transform.scale) {
        return Err(ConnectionError::FlippedConnection {
            connector: None,
            node: Some(node),
        });
    }
    if !options.allow_non_uniform_scale && !is_uniform_scale(transform.scale) {
        return Err(ConnectionError::NonUniformScaleConnectionRisk { node });
    }
    Ok(())
}

pub(super) fn inverse_transform(transform: Transform) -> Option<Transform> {
    if !is_valid_scale(transform.scale) || !is_uniform_scale(transform.scale) {
        return None;
    }
    let uniform_scale = transform.scale.x;
    let inverse_scale = uniform_scale.recip();
    let inverse_rotation = inverse_quat(transform.rotation)?;
    let inverse_translation = scale_vec3(
        rotate_vec3(inverse_rotation, negate_vec3(transform.translation)),
        inverse_scale,
    );
    Some(Transform {
        translation: inverse_translation,
        rotation: inverse_rotation,
        scale: Vec3::new(inverse_scale, inverse_scale, inverse_scale),
    })
}

fn inverse_quat(rotation: Quat) -> Option<Quat> {
    let length_squared = rotation.x * rotation.x
        + rotation.y * rotation.y
        + rotation.z * rotation.z
        + rotation.w * rotation.w;
    if length_squared <= f32::EPSILON || !length_squared.is_finite() {
        return None;
    }
    let inverse_length_squared = length_squared.recip();
    Some(Quat::from_xyzw(
        -rotation.x * inverse_length_squared,
        -rotation.y * inverse_length_squared,
        -rotation.z * inverse_length_squared,
        rotation.w * inverse_length_squared,
    ))
}

fn is_valid_scale(scale: Vec3) -> bool {
    scale.x.is_finite()
        && scale.y.is_finite()
        && scale.z.is_finite()
        && scale.x.abs() > f32::EPSILON
        && scale.y.abs() > f32::EPSILON
        && scale.z.abs() > f32::EPSILON
}

fn is_uniform_scale(scale: Vec3) -> bool {
    (scale.x - scale.y).abs() <= 1.0e-5 && (scale.x - scale.z).abs() <= 1.0e-5
}

fn has_negative_determinant(scale: Vec3) -> bool {
    scale.x * scale.y * scale.z < 0.0
}

fn is_valid_rotation(rotation: Quat) -> bool {
    let length_squared = rotation.x * rotation.x
        + rotation.y * rotation.y
        + rotation.z * rotation.z
        + rotation.w * rotation.w;
    length_squared.is_finite() && length_squared > f32::EPSILON
}

const fn negate_vec3(value: Vec3) -> Vec3 {
    Vec3::new(-value.x, -value.y, -value.z)
}

const fn scale_vec3(value: Vec3, scale: f32) -> Vec3 {
    Vec3::new(value.x * scale, value.y * scale, value.z * scale)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scene::transforms::{compose_transform, multiply_quat};

    #[test]
    fn inverse_round_trips_uniform_transform() {
        let transform = Transform::at(Vec3::new(2.0, 3.0, 4.0))
            .rotate_z_deg(90.0)
            .scale_by(2.0);
        let inverse = inverse_transform(transform).expect("uniform transform inverts");
        let round_trip = compose_transform(transform, inverse);

        assert!((round_trip.translation.x).abs() < 1.0e-5);
        assert!((round_trip.translation.y).abs() < 1.0e-5);
        assert!((round_trip.translation.z).abs() < 1.0e-5);
        assert!((round_trip.scale.x - 1.0).abs() < 1.0e-5);
        assert!((round_trip.scale.y - 1.0).abs() < 1.0e-5);
        assert!((round_trip.scale.z - 1.0).abs() < 1.0e-5);
        let identity_rotation = multiply_quat(transform.rotation, inverse.rotation);
        assert!(identity_rotation.x.abs() < 1.0e-5);
        assert!(identity_rotation.y.abs() < 1.0e-5);
        assert!(identity_rotation.z.abs() < 1.0e-5);
        assert!((identity_rotation.w - 1.0).abs() < 1.0e-5);
    }
}