use std::f32::consts::FRAC_PI_2;
use scena::{
Assets, Color, GeometryDesc, GizmoAxis, GizmoConstraint, GizmoMode, GizmoRay, GizmoSpace,
MaterialDesc, Scene, Transform, TransformGizmo, Vec3,
};
fn assert_vec3_close(actual: Vec3, expected: Vec3) {
assert!(
actual.abs_diff_eq(expected, 1.0e-5),
"expected {expected:?}, got {actual:?}"
);
}
#[test]
fn transform_gizmo_translates_along_world_axis_from_pointer_rays() {
let gizmo = TransformGizmo::new(GizmoMode::Translate)
.with_constraint(GizmoConstraint::Axis(GizmoAxis::X));
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(3.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("axis drag resolves");
assert_vec3_close(transformed.translation, Vec3::new(2.0, 0.0, 0.0));
assert_eq!(transformed.rotation, Transform::IDENTITY.rotation);
assert_eq!(transformed.scale, Vec3::ONE);
}
#[test]
fn transform_gizmo_translates_on_world_plane_from_pointer_rays() {
let gizmo = TransformGizmo::new(GizmoMode::Translate)
.with_constraint(GizmoConstraint::Plane(GizmoAxis::Z));
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 2.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(3.0, 4.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("plane drag resolves");
assert_vec3_close(transformed.translation, Vec3::new(2.0, 2.0, 0.0));
}
#[test]
fn transform_gizmo_uses_local_axis_when_requested() {
let gizmo = TransformGizmo::new(GizmoMode::Translate)
.with_space(GizmoSpace::Local)
.with_constraint(GizmoConstraint::Axis(GizmoAxis::Y));
let start = Transform::IDENTITY.rotate_z_deg(-90.0);
let start_ray = GizmoRay::new(Vec3::new(1.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(3.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("local axis drag resolves");
assert_vec3_close(transformed.translation, Vec3::new(2.0, 0.0, 0.0));
}
#[test]
fn transform_gizmo_rotates_around_constrained_axis() {
let gizmo =
TransformGizmo::new(GizmoMode::Rotate).with_constraint(GizmoConstraint::Axis(GizmoAxis::Z));
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(0.0, 1.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("rotation drag resolves");
let rotated_x = transformed.rotation * Vec3::X;
assert_vec3_close(rotated_x, Vec3::Y);
}
#[test]
fn transform_gizmo_scales_along_constrained_axis() {
let gizmo =
TransformGizmo::new(GizmoMode::Scale).with_constraint(GizmoConstraint::Axis(GizmoAxis::X));
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(2.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("scale drag resolves");
assert_vec3_close(transformed.scale, Vec3::new(2.0, 1.0, 1.0));
}
#[test]
fn transform_gizmo_view_aligned_translate_uses_pointer_ray_plane() {
let gizmo = TransformGizmo::new(GizmoMode::Translate).with_space(GizmoSpace::ViewAligned);
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 2.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(3.0, 4.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("view plane drag resolves");
assert_vec3_close(transformed.translation, Vec3::new(2.0, 2.0, 0.0));
}
#[test]
fn transform_gizmo_helper_geometry_is_parented_and_on_top() {
let assets = Assets::new();
let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
let mut scene = Scene::new();
let target = scene
.mesh(geometry, material)
.transform(Transform::at(Vec3::new(1.0, 2.0, 3.0)))
.add()
.expect("target inserts");
let helpers = TransformGizmo::new(GizmoMode::Translate)
.add_helpers(&mut scene, &assets, target)
.expect("helpers insert");
assert_eq!(helpers.nodes().len(), 3);
for node in helpers.nodes() {
assert_eq!(
scene.node(*node).expect("helper exists").parent(),
Some(target)
);
assert_eq!(scene.helper_on_top(*node), Some(true));
}
}
#[test]
fn transform_gizmo_invalid_ray_is_rejected() {
assert!(GizmoRay::new(Vec3::ZERO, Vec3::ZERO).is_none());
assert!(GizmoRay::new(Vec3::ZERO, Vec3::new(f32::NAN, 0.0, -1.0)).is_none());
}
#[cfg(feature = "scene-host")]
#[test]
fn transform_gizmo_visual_patch_reports_stale_target_handles() {
use scena::{SceneHostCore, SceneHostErrorCode, VisualPatchResultV1};
let mut host = SceneHostCore::headless(64, 64).expect("host builds");
let patch =
TransformGizmo::new(GizmoMode::Translate).to_visual_patch(999_999, Transform::IDENTITY);
let result = host.apply_patch(&patch).expect("patch schema is valid");
assert_eq!(result.applied.transforms, 0);
assert_eq!(result.failed.len(), 1);
assert_eq!(result.failed[0].channel, "transforms");
assert_eq!(result.failed[0].handle, Some(999_999));
assert_eq!(
result.failed[0].code,
SceneHostErrorCode::NodeHandleNotFound
);
let json = serde_json::to_string(&result).expect("result serializes");
let decoded: VisualPatchResultV1 = serde_json::from_str(&json).expect("result decodes");
assert_eq!(decoded.failed[0].handle, Some(999_999));
}
#[cfg(feature = "scene-host")]
#[test]
fn scene_host_gizmo_drag_json_applies_translate_and_rotate_visual_patches() {
use scena::{
SCENE_HOST_GIZMO_DRAG_SCHEMA_V1, SceneHostCore, SceneHostErrorCode,
SceneInspectionReportV1, VisualPatchResultV1,
};
use serde_json::json;
let mut host = SceneHostCore::headless(64, 64).expect("host builds");
let target = host
.add_empty(
Some(host.root_handle()),
Transform::IDENTITY,
Some("editable"),
)
.expect("target inserts");
let translate = json!({
"schema": SCENE_HOST_GIZMO_DRAG_SCHEMA_V1,
"mode": "translate",
"space": "world",
"constraint": { "kind": "axis", "axis": "x" },
"start_transform": {
"translation": [0.0, 0.0, 0.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
},
"start_ray": {
"origin": [1.0, 0.0, 5.0],
"direction": [0.0, 0.0, -1.0]
},
"current_ray": {
"origin": [3.0, 0.0, 5.0],
"direction": [0.0, 0.0, -1.0]
}
});
let translate_result: VisualPatchResultV1 = serde_json::from_str(
&host
.apply_gizmo_drag_json(target, &translate.to_string())
.unwrap(),
)
.expect("translate result decodes");
assert_eq!(translate_result.applied.transforms, 1);
assert!(translate_result.failed.is_empty());
let translated = inspected_node(&host, target).local_transform;
assert_vec3_close(translated.translation, Vec3::new(2.0, 0.0, 0.0));
let rotate = json!({
"schema": SCENE_HOST_GIZMO_DRAG_SCHEMA_V1,
"mode": "rotate",
"space": "world",
"constraint": { "kind": "axis", "axis": "z" },
"start_transform": translated,
"start_ray": {
"origin": [3.0, 0.0, 5.0],
"direction": [0.0, 0.0, -1.0]
},
"current_ray": {
"origin": [2.0, 1.0, 5.0],
"direction": [0.0, 0.0, -1.0]
}
});
let rotate_result: VisualPatchResultV1 = serde_json::from_str(
&host
.apply_gizmo_drag_json(target, &rotate.to_string())
.unwrap(),
)
.expect("rotate result decodes");
assert_eq!(rotate_result.applied.transforms, 1);
assert!(rotate_result.failed.is_empty());
let rotated = inspected_node(&host, target).local_transform;
assert_vec3_close(rotated.translation, Vec3::new(2.0, 0.0, 0.0));
assert_vec3_close(rotated.rotation * Vec3::X, Vec3::Y);
let stale_result: VisualPatchResultV1 = serde_json::from_str(
&host
.apply_gizmo_drag_json(999_999, &translate.to_string())
.unwrap(),
)
.expect("stale-target result decodes");
assert_eq!(stale_result.applied.transforms, 0);
assert_eq!(stale_result.failed.len(), 1);
assert_eq!(stale_result.failed[0].channel, "transforms");
assert_eq!(stale_result.failed[0].handle, Some(999_999));
assert_eq!(
stale_result.failed[0].code,
SceneHostErrorCode::NodeHandleNotFound
);
let mut invalid_ray = translate;
invalid_ray["current_ray"]["direction"] = json!([0.0, 0.0, 0.0]);
let error = host
.apply_gizmo_drag_json(target, &invalid_ray.to_string())
.expect_err("zero-length ray should fail closed");
assert_eq!(error.code(), SceneHostErrorCode::InvalidInput);
fn inspected_node(host: &SceneHostCore, handle: u64) -> scena::SceneNodeInspectionV1 {
let report: SceneInspectionReportV1 =
serde_json::from_str(&host.inspect_json().expect("inspection serializes"))
.expect("inspection decodes");
report
.node_by_handle(handle)
.expect("target remains inspectable")
.clone()
}
}
#[test]
fn transform_gizmo_rotation_keeps_finite_quaternion() {
let gizmo =
TransformGizmo::new(GizmoMode::Rotate).with_constraint(GizmoConstraint::Axis(GizmoAxis::Z));
let start = Transform::IDENTITY;
let start_ray = GizmoRay::new(Vec3::new(1.0, 0.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("start ray normalizes");
let current_ray = GizmoRay::new(Vec3::new(0.0, -1.0, 5.0), Vec3::new(0.0, 0.0, -1.0))
.expect("current ray normalizes");
let transformed = gizmo
.drag_transform(start, start_ray, current_ray)
.expect("rotation drag resolves");
assert!(transformed.rotation.is_finite());
let rotated_x = transformed.rotation * Vec3::X;
assert_vec3_close(rotated_x, -Vec3::Y);
assert!((transformed.rotation.to_axis_angle().1.abs() - FRAC_PI_2).abs() < 1.0e-5);
}