mocari 0.2.5

A pure Rust Live2D/Cubism-compatible runtime
Documentation
mod ids {
    use mocari::{DrawableId, Error, Id, ParameterId, PartId};

    #[test]
    fn id_rejects_empty_strings() {
        let error = Id::new("").unwrap_err();
        assert!(matches!(error, Error::EmptyId));
    }

    #[test]
    fn id_trims_nothing_and_preserves_source_text() {
        let id = Id::new(" ParamAngleX ").unwrap();
        assert_eq!(id.as_str(), " ParamAngleX ");
        assert_eq!(id.to_string(), " ParamAngleX ");
    }

    #[test]
    fn typed_ids_expose_their_inner_text() {
        let parameter = ParameterId::new("ParamAngleX").unwrap();
        let part = PartId::new("PartSegmentA").unwrap();
        let drawable = DrawableId::new("DrawableBody").unwrap();

        assert_eq!(parameter.as_str(), "ParamAngleX");
        assert_eq!(part.as_str(), "PartSegmentA");
        assert_eq!(drawable.as_str(), "DrawableBody");
    }
}

mod core_parameters {
    use mocari::core::{clamp_parameter_value, core_repeat_fold, parameter_dirty};

    fn assert_close(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 0.00001,
            "expected {expected}, got {actual}"
        );
    }

    #[test]
    fn clamps_parameter_values_to_min_max() {
        assert_eq!(clamp_parameter_value(-2.0, -1.0, 1.0), -1.0);
        assert_eq!(clamp_parameter_value(2.0, -1.0, 1.0), 1.0);
        assert_eq!(clamp_parameter_value(0.25, -1.0, 1.0), 0.25);
    }

    #[test]
    fn repeat_fold_uses_core_floor_like_correction() {
        assert_close(core_repeat_fold(370.0, 0.0, 360.0), 10.0);
        assert_close(core_repeat_fold(-10.0, 0.0, 360.0), 350.0);
        assert_close(core_repeat_fold(-360.0, 0.0, 360.0), 0.0);
    }

    #[test]
    fn repeat_fold_keeps_large_q_path_truncated() {
        let value = 8_388_609.25_f32;

        assert_eq!(core_repeat_fold(value, 0.0, 1.0), 0.0);
    }

    #[test]
    fn dirty_flag_treats_nan_as_dirty() {
        assert!(!parameter_dirty(1.0, 1.0));
        assert!(parameter_dirty(1.0, 2.0));
        assert!(parameter_dirty(f32::NAN, 1.0));
        assert!(parameter_dirty(1.0, f32::NAN));
    }
}

mod math {
    use std::collections::BTreeMap;

    use mocari::core::{Matrix44, ModelMatrix};

    #[test]
    fn matrix44_transforms_scaled_and_translated_points() {
        let mut matrix = Matrix44::identity();
        matrix.scale(2.0, 3.0);
        matrix.translate(10.0, -4.0);

        assert_eq!(matrix.transform_x(5.0), 20.0);
        assert_eq!(matrix.transform_y(2.0), 2.0);
        assert_eq!(matrix.invert_transform_x(20.0), 5.0);
        assert_eq!(matrix.invert_transform_y(2.0), 2.0);
    }

    #[test]
    fn model_matrix_initializes_to_height_two() {
        let matrix = ModelMatrix::new(4.0, 8.0);

        assert_eq!(matrix.transform_x(4.0), 1.0);
        assert_eq!(matrix.transform_y(8.0), 2.0);
    }

    #[test]
    fn model_matrix_applies_layout_size_before_position() {
        let mut matrix = ModelMatrix::new(4.0, 8.0);
        let mut layout = BTreeMap::new();
        layout.insert("width".to_string(), 4.0);
        layout.insert("center_x".to_string(), 0.0);
        layout.insert("top".to_string(), 1.0);

        matrix.setup_from_layout(&layout);

        assert_eq!(matrix.transform_x(0.0), -2.0);
        assert_eq!(matrix.transform_x(4.0), 2.0);
        assert_eq!(matrix.transform_y(0.0), 1.0);
        assert_eq!(matrix.transform_y(8.0), 9.0);
    }
}

mod core_blend {
    use mocari::core::{
        BlendSlot, Rgb, blend_scalar_slots, blend_scalar_slots_clamped, multiply_rgb, screen_rgb,
    };

    fn assert_close(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 0.00001,
            "expected {expected}, got {actual}"
        );
    }

    #[test]
    fn blends_scalar_slots_with_single_and_pair_kinds() {
        let values = [10.0, 20.0, 30.0, 40.0];
        let slots = [
            BlendSlot::Skip,
            BlendSlot::Single {
                base: 0,
                index: 1,
                weight: 0.5,
                final_weight: 0.25,
            },
            BlendSlot::Pair {
                base: 0,
                index0: 2,
                weight0: 0.75,
                index1: 3,
                weight1: 0.25,
                final_weight: 0.5,
            },
        ];

        assert_close(blend_scalar_slots(&slots, &values, 1.0).unwrap(), 19.75);
    }

    #[test]
    fn clamps_scalar_blend_result() {
        let slots = [BlendSlot::Single {
            base: 0,
            index: 0,
            weight: 2.0,
            final_weight: 1.0,
        }];

        assert_eq!(
            blend_scalar_slots_clamped(&slots, &[0.75], 0.0, 0.0, 1.0).unwrap(),
            1.0
        );
    }

    #[test]
    fn blends_multiply_and_screen_rgb() {
        let local = Rgb::new(0.25, 0.5, 1.25);
        let parent = Rgb::new(0.5, 0.25, 0.5);

        assert_eq!(multiply_rgb(local, parent), Rgb::new(0.125, 0.125, 0.625));
        assert_eq!(screen_rgb(local, parent), Rgb::new(0.625, 0.625, 1.0));
    }
}

mod core_keyforms {
    use mocari::core::{KeyformAxis, compute_keyform_axis_interval, expand_keyform_runtime_slots};

    fn assert_close(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 0.00001,
            "expected {expected}, got {actual}"
        );
    }

    #[test]
    fn computes_keyform_axis_interval() {
        let keys = [0.0, 10.0, 20.0];

        let below = compute_keyform_axis_interval(&keys, -1.0).unwrap();
        assert_eq!(below.left_index(), 0);
        assert_eq!(below.t(), 0.0);

        let middle = compute_keyform_axis_interval(&keys, 12.5).unwrap();
        assert_eq!(middle.left_index(), 1);
        assert_close(middle.t(), 0.25);

        let above = compute_keyform_axis_interval(&keys, 25.0).unwrap();
        assert_eq!(above.left_index(), 2);
        assert_eq!(above.t(), 0.0);

        assert!(compute_keyform_axis_interval(&[], 1.0).is_none());
    }

    #[test]
    fn expands_runtime_slots_from_active_axes() {
        let axes = [
            KeyformAxis::new(1, 0.25, 1),
            KeyformAxis::new(2, 0.0, 4),
            KeyformAxis::new(0, 0.5, 12),
        ];

        let slots = expand_keyform_runtime_slots(&axes);

        assert_eq!(slots.len(), 4);
        assert_eq!(slots[0].flat_index(), 9);
        assert_close(slots[0].weight(), 0.375);
        assert_eq!(slots[1].flat_index(), 10);
        assert_close(slots[1].weight(), 0.125);
        assert_eq!(slots[2].flat_index(), 21);
        assert_close(slots[2].weight(), 0.375);
        assert_eq!(slots[3].flat_index(), 22);
        assert_close(slots[3].weight(), 0.125);
    }
}

mod core_update_order {
    use mocari::core::{
        ModelUpdateStep, semantic_model_update_order, should_affect_glues, should_blend_glues,
        should_run_offscreen_stage,
    };

    #[test]
    fn exposes_confirmed_semantic_model_update_order() {
        let order = semantic_model_update_order();

        assert_eq!(order.first(), Some(&ModelUpdateStep::PreUpdateDynamicFlags));
        assert_eq!(
            order.windows(4).position(|steps| {
                steps
                    == [
                        ModelUpdateStep::UpdateParameters,
                        ModelUpdateStep::UpdateParameterBindings,
                        ModelUpdateStep::UpdateBlendShapeParameterBindings,
                        ModelUpdateStep::UpdateKeyformBindings,
                    ]
            }),
            Some(1)
        );
        assert!(
            order
                .iter()
                .position(|step| *step == ModelUpdateStep::BlendArtMeshes)
                .unwrap()
                < order
                    .iter()
                    .position(|step| *step == ModelUpdateStep::TransformDeformers)
                    .unwrap()
        );
        assert_eq!(order.last(), Some(&ModelUpdateStep::PostUpdateDynamicFlags));
    }

    #[test]
    fn applies_confirmed_glue_and_offscreen_guards() {
        assert!(!should_affect_glues(0));
        assert!(should_affect_glues(1));
        assert!(!should_blend_glues(4));
        assert!(should_blend_glues(5));
        assert!(!should_run_offscreen_stage(5));
        assert!(should_run_offscreen_stage(6));
    }
}

mod core_art_mesh {
    use mocari::core::{
        Vector2, affect_art_mesh_pair, apply_art_mesh_blend_shape_delta, apply_parent_part_opacity,
        draw_order_from_raw, reverse_coordinate_y,
    };

    fn assert_vec_close(actual: Vector2, expected: Vector2) {
        assert!(
            (actual.x() - expected.x()).abs() < 0.00001,
            "expected x {}, got {}",
            expected.x(),
            actual.x()
        );
        assert!(
            (actual.y() - expected.y()).abs() < 0.00001,
            "expected y {}, got {}",
            expected.y(),
            actual.y()
        );
    }

    #[test]
    fn glue_affects_art_mesh_pair_with_directional_weights() {
        let (a, b) = affect_art_mesh_pair(
            Vector2::new(0.0, 0.0),
            Vector2::new(10.0, 20.0),
            0.25,
            0.5,
            0.8,
        );

        assert_vec_close(a, Vector2::new(2.0, 4.0));
        assert_vec_close(b, Vector2::new(6.0, 12.0));
    }

    #[test]
    fn vector_interpolation_helpers_cover_lerp_and_affine_offsets() {
        let origin = Vector2::new(1.0, 2.0);
        let horizontal = Vector2::new(5.0, 2.0);
        let vertical = Vector2::new(1.0, 8.0);

        assert_vec_close(origin.lerp(horizontal, 0.25), Vector2::new(2.0, 2.0));
        assert_vec_close(
            Vector2::affine2(origin, horizontal, 0.5, vertical, 0.25),
            Vector2::new(3.0, 3.5),
        );
    }

    #[test]
    fn reverse_coordinate_flips_y_only() {
        let mut vertices = [Vector2::new(1.0, 2.0), Vector2::new(-3.0, -4.0)];

        reverse_coordinate_y(&mut vertices);

        assert_eq!(vertices, [Vector2::new(1.0, -2.0), Vector2::new(-3.0, 4.0)]);
    }

    #[test]
    fn draw_order_uses_core_int_bias_and_clamps() {
        assert_eq!(draw_order_from_raw(12.999), 13);
        assert_eq!(draw_order_from_raw(-1.0), 0);
        assert_eq!(draw_order_from_raw(1001.0), 1000);
    }

    #[test]
    fn applies_art_mesh_blend_shape_delta() {
        let mut positions = [0.0, 1.0, 2.0, 3.0];

        apply_art_mesh_blend_shape_delta(&mut positions, &[10.0, -10.0, 4.0, -4.0], 0.25).unwrap();
        apply_art_mesh_blend_shape_delta(&mut positions, &[1.0, 1.0, 1.0, 1.0], 0.0).unwrap();

        assert_eq!(positions, [2.5, -1.5, 3.0, 2.0]);
        assert!(apply_art_mesh_blend_shape_delta(&mut positions, &[1.0], 1.0).is_none());
    }

    #[test]
    fn parent_part_opacity_multiplies_art_mesh_opacity() {
        assert_eq!(apply_parent_part_opacity(0.75, 0.5), 0.375);
    }
}