use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_core::components::Transform;
use concinnity_host::thread::asset_id;
use crate::frame_hook::FrameHook;
use crate::editor::hook::drag::content::{drag_has_effect, placement_args};
use crate::editor::hook::tests::fixtures::{click_at, drag_input, hook, pick_world, set_input};
use crate::editor::hook::{declared_id, entry_type};
use crate::editor::hook::tests::fixtures::active;
use crate::editor::panels::registry::PanelKey;
#[test]
fn drag_out_places_a_prop_where_the_ghost_lands() {
asset_id::reset_interner();
let ground = asset_id::intern("ground");
let mut world = pick_world(
[0.0; 3],
vec![(ground, [-20.0, -1.0, -20.0], [20.0, 0.0, 20.0])],
);
let mut h = hook(vec![
serde_json::json!({
"type": "ProceduralMesh",
"args": { "$id": "demo_ball", "generator": "sphere" }
}),
serde_json::json!({
"type": "Prop",
"args": { "$id": "ground", "mesh": "demo_ball", "position": [0.0, -0.5, 0.0] }
}),
]);
h.open[PanelKey::Content] = true;
h.assets.stale = true;
h.refresh_tree_if_needed();
let o = h.origin(PanelKey::Content, [1280.0, 720.0]);
let cell = crate::editor::panels::content_panel::cell_rect(o, 0);
click_at(&mut world, &mut h, [cell[0] + 10.0, cell[1] + 10.0]);
assert!(h.content.drag.is_some(), "the cell press arms a drag");
assert_eq!(active(&h).as_deref(), Some("demo_ball"));
let before = h.entries.len();
set_input(&mut world, drag_input([640.0, 500.0], true));
h.tick(&mut world);
let pose = h
.content_ghost_pose()
.expect("the ghost has a landing point");
assert!(
pose.position[1].abs() < 1e-3,
"landed on the ground top: {pose:?}"
);
set_input(&mut world, drag_input([640.0, 500.0], false));
h.tick(&mut world);
assert!(h.content.drag.is_none());
assert_eq!(h.entries.len(), before + 1);
let placed = &h.entries[before];
assert_eq!(entry_type(placed), Some("Prop"));
assert_eq!(placed["args"]["mesh"], "demo_ball");
assert!(
placed["args"]["position"][1].as_f64().unwrap().abs() < 1e-3,
"{placed}"
);
assert_eq!(declared_id(placed), None, "a placed prop is anonymous");
assert_eq!(active(&h).as_deref(), Some("Prop#0"));
assert!(h.dirty);
h.undo(&mut world);
assert_eq!(h.entries.len(), before, "one undo removes the placement");
}
#[test]
fn align_rotation_maps_up_onto_every_face_normal() {
for (axis, sign) in [
(0, 1.0f32),
(0, -1.0),
(1, 1.0),
(1, -1.0),
(2, 1.0),
(2, -1.0),
] {
let rotation_deg = crate::editor::hook::drag::content::align_rotation(axis, sign);
let m = Transform {
position: [0.0; 3],
rotation_deg,
scale: [1.0; 3],
}
.model_matrix();
let up = [m[1][0], m[1][1], m[1][2]];
let mut normal = [0.0f32; 3];
normal[axis] = sign;
for c in 0..3 {
assert!(
(up[c] - normal[c]).abs() < 1e-5,
"axis {axis} sign {sign}: +Y maps to {up:?}, want {normal:?}"
);
}
}
}
#[test]
fn aligned_drag_out_orients_the_drop_to_the_struck_face() {
asset_id::reset_interner();
let wall = asset_id::intern("wall");
let mut world = pick_world([0.0; 3], vec![(wall, [-2.0, -2.0, -6.0], [2.0, 2.0, -4.0])]);
let mut h = hook(vec![
serde_json::json!({
"type": "ProceduralMesh",
"args": { "$id": "demo_ball", "generator": "sphere" }
}),
serde_json::json!({
"type": "Prop",
"args": { "$id": "wall", "mesh": "demo_ball", "position": [0.0, 0.0, -5.0] }
}),
]);
h.open[PanelKey::Content] = true;
h.align_to_surface = true;
h.assets.stale = true;
h.refresh_tree_if_needed();
let o = h.origin(PanelKey::Content, [1280.0, 720.0]);
let cell = crate::editor::panels::content_panel::cell_rect(o, 0);
click_at(&mut world, &mut h, [cell[0] + 10.0, cell[1] + 10.0]);
let before = h.entries.len();
set_input(&mut world, drag_input([640.0, 360.0], true));
h.tick(&mut world);
let pose = h.content_ghost_pose().expect("ghost up");
assert_eq!(pose.normal, [0.0, 0.0, 1.0], "the +Z face was struck");
assert_eq!(pose.rotation_deg, [90.0, 0.0, 0.0]);
set_input(&mut world, drag_input([640.0, 360.0], false));
h.tick(&mut world);
let placed = &h.entries[before];
assert_eq!(
placed["args"]["rotation_deg"],
serde_json::json!([90.0, 0.0, 0.0]),
"{placed}"
);
assert!(
(placed["args"]["position"][2].as_f64().unwrap() - (-4.0)).abs() < 1e-3,
"landed on the face: {placed}"
);
h.undo(&mut world);
h.align_to_surface = false;
click_at(&mut world, &mut h, [cell[0] + 10.0, cell[1] + 10.0]);
set_input(&mut world, drag_input([640.0, 360.0], true));
h.tick(&mut world);
set_input(&mut world, drag_input([640.0, 360.0], false));
h.tick(&mut world);
let placed = &h.entries[before];
assert!(
placed["args"].get("rotation_deg").is_none(),
"no rotation written while align is off: {placed}"
);
}
#[test]
fn a_still_cell_press_places_nothing() {
asset_id::reset_interner();
let mut world = pick_world([0.0; 3], vec![]);
let mut h = hook(vec![serde_json::json!({
"type": "ProceduralMesh", "args": { "$id": "demo_ball", "generator": "box" }
})]);
h.open[PanelKey::Content] = true;
h.assets.stale = true;
h.refresh_tree_if_needed();
let o = h.origin(PanelKey::Content, [1280.0, 720.0]);
let cell = crate::editor::panels::content_panel::cell_rect(o, 0);
let at = [cell[0] + 10.0, cell[1] + 10.0];
click_at(&mut world, &mut h, at);
set_input(&mut world, drag_input(at, false));
h.tick(&mut world);
assert!(h.content.drag.is_none());
assert_eq!(h.entries.len(), 1, "no placement from a plain click");
assert!(!h.dirty);
assert_eq!(active(&h).as_deref(), Some("demo_ball"), "still selected");
}
#[test]
fn material_drag_assigns_to_the_prop_under_the_cursor() {
asset_id::reset_interner();
let crate_id = asset_id::intern("crate_prop");
let mut world = pick_world(
[0.0; 3],
vec![(crate_id, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])],
);
let mut h = hook(vec![
serde_json::json!({
"type": "Material", "args": { "$id": "wood", "roughness": 0.7 }
}),
serde_json::json!({
"type": "Prop",
"args": { "$id": "crate_prop", "mesh": "demo", "position": [0.0, 0.0, -5.0] }
}),
]);
h.open[PanelKey::Content] = true;
h.arm_content_drag(
"wood".to_string(),
RegisteredType::Material,
[1000.0, 300.0],
);
set_input(&mut world, drag_input([640.0, 360.0], true));
h.tick(&mut world);
set_input(&mut world, drag_input([640.0, 360.0], false));
h.tick(&mut world);
assert_eq!(
h.entries[1]["args"]["material"], "wood",
"the hovered Prop gains the material"
);
assert_eq!(h.entries.len(), 2, "nothing was placed");
assert!(h.dirty);
h.undo(&mut world);
assert!(h.entries[1]["args"].get("material").is_none());
}
#[test]
fn only_the_placing_types_produce_placement_args() {
for &ty in RegisteredType::all() {
let field = match ty {
RegisteredType::Mesh | RegisteredType::ProceduralMesh => Some("mesh"),
RegisteredType::Model => Some("model"),
RegisteredType::Prefab => Some("prefab"),
_ => None,
};
let expected = field.map(|f| serde_json::json!({ f: "rock", "position": [1.0, 2.0, 3.0] }));
assert_eq!(
placement_args(ty, "rock", [1.0, 2.0, 3.0]),
expected,
"{}",
ty.as_str()
);
assert_eq!(
drag_has_effect(ty),
field.is_some() || ty == RegisteredType::Material,
"{}",
ty.as_str()
);
}
}