extends SceneTree ## Headless smoke test verifying Studio floor collision, selection, ## gizmo ray picking, inspector property sync, and transformation manipulation. var _failures: int = 0 var _checks: int = 0 const StudioMainClass = preload("res://scripts/studio/studio_main.gd") const TransformGizmo3DClass = preload("res://scripts/studio/gizmos/transform_gizmo_3d.gd") func _init() -> void: print("--- Running Smoke Test: Studio Viewport Interaction & Floor Collision ---") test_floor_collision_body() test_inspector_sync_and_visual_transform() test_gizmo_picking_and_dragging() test_nudge_shortcuts_and_delete() if _failures == 0: print("--- PASS: All %d studio interaction checks passed! ---" % _checks) quit(0) else: printerr("--- FAIL: %d failures out of %d checks ---" % [_failures, _checks]) quit(1) func _check(desc: String, cond: bool) -> void: _checks += 1 if cond: print(" ✓ %s" % desc) else: _failures += 1 printerr(" ✗ FAIL: %s" % desc) func test_floor_collision_body() -> void: print("Testing Studio floor collision setup...") var studio := StudioMainClass.new() root.add_child(studio) var floor_body = studio._sub_viewport.find_child("StudioFloorBody", true, false) _check("StudioFloorBody exists in 3D viewport", floor_body != null) if floor_body is StaticBody3D: var sb := floor_body as StaticBody3D _check("Floor body has collision layer 1 enabled", sb.get_collision_layer_value(1)) _check("Floor body has collision mask 1 enabled", sb.get_collision_mask_value(1)) var col = sb.find_child("*", true, false) as CollisionShape3D _check("Floor body has CollisionShape3D", col != null and col.shape is BoxShape3D) if col != null and col.shape is BoxShape3D: var box := col.shape as BoxShape3D _check("Floor collider spans at least 100x100m", box.size.x >= 100.0 and box.size.z >= 100.0) studio.queue_free() func test_inspector_sync_and_visual_transform() -> void: print("Testing Inspector property sync to 3D visual nodes...") var studio := StudioMainClass.new() root.add_child(studio) # Place an object var obj_id := studio.core.place_object("crate_small", Vector3(10, 20, 0)) studio._spawn_visual_node(obj_id) studio._on_selection_changed(obj_id) var visual_node := studio._world_container.get_node_or_null(obj_id) as Node3D _check("Visual node exists in world container", visual_node != null) # Simulate inspector position change (moving object via Inspector) studio._inspector_dock._pos_x.value = 15.0 studio._inspector_dock._pos_y.value = 3.0 studio._inspector_dock._pos_z.value = 25.0 studio._inspector_dock._on_pos_changed(0.0) _check("Visual node position updated to match Inspector X", is_equal_approx(visual_node.position.x, 15.0)) _check("Visual node position updated to match Inspector Y (height)", is_equal_approx(visual_node.position.y, 3.0)) _check("Visual node position updated to match Inspector Z (depth)", is_equal_approx(visual_node.position.z, 25.0)) # Simulate inspector yaw rotation change studio._inspector_dock._rot_y.value = 90.0 studio._inspector_dock._on_rot_changed(90.0) _check("Visual node rotated to match Inspector Yaw", absf(visual_node.rotation_degrees.y - 90.0) < 0.1) # Simulate inspector scale change studio._inspector_dock._scale_x.value = 2.0 studio._inspector_dock._scale_y.value = 1.5 studio._inspector_dock._scale_z.value = 2.0 studio._inspector_dock._on_scale_changed(0.0) _check("Visual node scale updated to match Inspector", is_equal_approx(visual_node.scale.x, 2.0) and is_equal_approx(visual_node.scale.y, 1.5)) studio.queue_free() func test_gizmo_picking_and_dragging() -> void: print("Testing Gizmo geometric picking and axis detection...") var studio := StudioMainClass.new() root.add_child(studio) var obj_id := studio.core.place_object("bison", Vector3(0, 0, 0)) studio._spawn_visual_node(obj_id) studio._on_selection_changed(obj_id) var gizmo = studio._gizmo_3d _check("Gizmo is attached and visible on selection", gizmo != null and gizmo.visible) # Test X axis picking (ray aimed near +X arrow at (1.0, 0, 0) relative to gizmo) var ray_origin: Vector3 = gizmo.position + Vector3(1.0, 5.0, 0.0) var ray_dir: Vector3 = Vector3(0.0, -1.0, 0.0) var picked_axis = gizmo.pick_axis(ray_origin, ray_dir) _check("Gizmo ray picking correctly identified X axis", picked_axis == TransformGizmo3DClass.Axis.X) # Test Y axis picking (ray aimed near +Y arrow at (0, 1.0, 0) relative to gizmo) ray_origin = gizmo.position + Vector3(0.0, 1.0, 5.0) ray_dir = Vector3(0.0, 0.0, -1.0) picked_axis = gizmo.pick_axis(ray_origin, ray_dir) _check("Gizmo ray picking correctly identified Y axis", picked_axis == TransformGizmo3DClass.Axis.Y) # Test Z axis picking (ray aimed near +Z arrow at (0, 0, 1.0) relative to gizmo) ray_origin = gizmo.position + Vector3(0.0, 5.0, 1.0) ray_dir = Vector3(0.0, -1.0, 0.0) picked_axis = gizmo.pick_axis(ray_origin, ray_dir) _check("Gizmo ray picking correctly identified Z axis", picked_axis == TransformGizmo3DClass.Axis.Z) # Test Center picking (ray aimed at gizmo origin) ray_origin = gizmo.position + Vector3(0.0, 5.0, 0.0) ray_dir = Vector3(0.0, -1.0, 0.0) picked_axis = gizmo.pick_axis(ray_origin, ray_dir) _check("Gizmo ray picking correctly identified Center/Plane handle", picked_axis == TransformGizmo3DClass.Axis.XY) studio.queue_free() func test_nudge_shortcuts_and_delete() -> void: print("Testing keyboard nudge shortcuts and object deletion...") var studio := StudioMainClass.new() root.add_child(studio) var obj_id := studio.core.place_object("crate_small", Vector3(0, 0, 0)) studio._spawn_visual_node(obj_id) studio.core.select_object(obj_id) studio._on_selection_changed(obj_id) var visual_node := studio._world_container.get_node_or_null(obj_id) as Node3D # Nudge position right (+X) var old_x = visual_node.position.x studio._nudge_selected_position(Vector3(1, 0, 0)) _check("Nudge right moved visual node +X by 1.0", is_equal_approx(visual_node.position.x, old_x + 1.0)) # Nudge rotation studio._nudge_selected_rotation(45.0) _check("Nudge rotation rotated visual node by 45 degrees", is_equal_approx(visual_node.rotation_degrees.y, 45.0)) # Delete object studio._delete_selected_object(obj_id) _check("Object deleted from Core model", studio.core.model.find_object(obj_id) == null) _check("Selection cleared after delete", studio.core.selection.get_selected() == "") studio.queue_free()