fix(studio): Add solid floor collision, live 3D gizmo dragging, inspector property sync, and nudge shortcuts
checkpoint / checkpoint (push) Successful in 6m23s
checkpoint / checkpoint (push) Successful in 6m23s
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
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()
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
class_name TransformGizmo3D
|
class_name TransformGizmo3D
|
||||||
extends Node3D
|
extends Node3D
|
||||||
## Interactive 3D manipulator gizmo supporting Translate, Rotate, and Scale modes with snapping.
|
## Interactive 3D manipulator gizmo supporting Translate, Rotate, and Scale modes with snapping.
|
||||||
|
## Renders visually clear axis handles (arrows for move, rings for rotate, cubes for scale)
|
||||||
|
## with priority depth rendering and geometric raycast picking.
|
||||||
|
|
||||||
signal transform_started
|
signal transform_started
|
||||||
signal transform_updated(mode: String, delta: Variant)
|
signal transform_updated(mode: String, delta: Variant)
|
||||||
@@ -24,17 +26,26 @@ var _drag_start_pos: Vector3 = Vector3.ZERO
|
|||||||
var _drag_start_rot: Vector3 = Vector3.ZERO
|
var _drag_start_rot: Vector3 = Vector3.ZERO
|
||||||
var _drag_start_scale: Vector3 = Vector3.ONE
|
var _drag_start_scale: Vector3 = Vector3.ONE
|
||||||
|
|
||||||
|
# Mode visual root nodes
|
||||||
|
var _translate_root: Node3D
|
||||||
|
var _rotate_root: Node3D
|
||||||
|
var _scale_root: Node3D
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_build_gizmo_visuals()
|
_build_gizmo_visuals()
|
||||||
|
_update_visual_mode()
|
||||||
|
|
||||||
|
|
||||||
func attach_to_node(node: Node3D) -> void:
|
func attach_to_node(node: Node3D) -> void:
|
||||||
target_node = node
|
target_node = node
|
||||||
if target_node != null:
|
if target_node != null and is_instance_valid(target_node):
|
||||||
visible = true
|
visible = (current_mode != Mode.NONE)
|
||||||
global_position = target_node.global_position
|
position = target_node.position
|
||||||
global_rotation = target_node.global_rotation
|
if current_mode == Mode.ROTATE or current_mode == Mode.SCALE:
|
||||||
|
quaternion = target_node.quaternion
|
||||||
|
else:
|
||||||
|
quaternion = Quaternion.IDENTITY
|
||||||
else:
|
else:
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
@@ -42,6 +53,8 @@ func attach_to_node(node: Node3D) -> void:
|
|||||||
func set_mode(new_mode: Mode) -> void:
|
func set_mode(new_mode: Mode) -> void:
|
||||||
current_mode = new_mode
|
current_mode = new_mode
|
||||||
_update_visual_mode()
|
_update_visual_mode()
|
||||||
|
if target_node != null and is_instance_valid(target_node):
|
||||||
|
attach_to_node(target_node)
|
||||||
|
|
||||||
|
|
||||||
func set_snap(enabled: bool, translate_step: float = 1.0, rotate_step: float = 15.0) -> void:
|
func set_snap(enabled: bool, translate_step: float = 1.0, rotate_step: float = 15.0) -> void:
|
||||||
@@ -50,63 +63,269 @@ func set_snap(enabled: bool, translate_step: float = 1.0, rotate_step: float = 1
|
|||||||
snap_rotate_deg = rotate_step
|
snap_rotate_deg = rotate_step
|
||||||
|
|
||||||
|
|
||||||
|
func _update_visual_mode() -> void:
|
||||||
|
if _translate_root != null:
|
||||||
|
_translate_root.visible = (current_mode == Mode.TRANSLATE)
|
||||||
|
if _rotate_root != null:
|
||||||
|
_rotate_root.visible = (current_mode == Mode.ROTATE)
|
||||||
|
if _scale_root != null:
|
||||||
|
_scale_root.visible = (current_mode == Mode.SCALE)
|
||||||
|
visible = (target_node != null and current_mode != Mode.NONE)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Visual Construction ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _make_gizmo_mat(color: Color) -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = color
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.no_depth_test = true
|
||||||
|
mat.render_priority = 10
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
func _build_gizmo_visuals() -> void:
|
func _build_gizmo_visuals() -> void:
|
||||||
# Build colored 3D axis handles for X (Red), Y (Green), Z (Blue)
|
var x_mat := _make_gizmo_mat(Color(0.95, 0.22, 0.22))
|
||||||
var x_mat := StandardMaterial3D.new()
|
var y_mat := _make_gizmo_mat(Color(0.22, 0.90, 0.25))
|
||||||
x_mat.albedo_color = Color(0.9, 0.2, 0.2)
|
var z_mat := _make_gizmo_mat(Color(0.25, 0.50, 0.98))
|
||||||
x_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
var center_mat := _make_gizmo_mat(Color(1.0, 0.88, 0.20))
|
||||||
|
|
||||||
var y_mat := StandardMaterial3D.new()
|
# 1. TRANSLATE Visuals (Arrows + Center Ground Plane Quad)
|
||||||
y_mat.albedo_color = Color(0.2, 0.85, 0.2)
|
_translate_root = Node3D.new()
|
||||||
y_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
_translate_root.name = "TranslateRoot"
|
||||||
|
add_child(_translate_root)
|
||||||
|
|
||||||
var z_mat := StandardMaterial3D.new()
|
_translate_root.add_child(_create_arrow(Vector3(1, 0, 0), x_mat, "ArrowX"))
|
||||||
z_mat.albedo_color = Color(0.2, 0.4, 0.95)
|
_translate_root.add_child(_create_arrow(Vector3(0, 1, 0), y_mat, "ArrowY"))
|
||||||
z_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
_translate_root.add_child(_create_arrow(Vector3(0, 0, 1), z_mat, "ArrowZ"))
|
||||||
|
|
||||||
# X axis line + arrow
|
# Center ground plane indicator
|
||||||
var x_axis := _create_axis_cylinder(Vector3(1, 0, 0), x_mat)
|
var plane_inst := MeshInstance3D.new()
|
||||||
x_axis.name = "AxisX"
|
var plane_mesh := BoxMesh.new()
|
||||||
add_child(x_axis)
|
plane_mesh.size = Vector3(0.4, 0.02, 0.4)
|
||||||
|
plane_inst.mesh = plane_mesh
|
||||||
|
plane_inst.material_override = center_mat
|
||||||
|
plane_inst.position = Vector3(0.2, 0.01, 0.2)
|
||||||
|
_translate_root.add_child(plane_inst)
|
||||||
|
|
||||||
# Y axis line + arrow
|
# 2. ROTATE Visuals (Rings)
|
||||||
var y_axis := _create_axis_cylinder(Vector3(0, 1, 0), y_mat)
|
_rotate_root = Node3D.new()
|
||||||
y_axis.name = "AxisY"
|
_rotate_root.name = "RotateRoot"
|
||||||
add_child(y_axis)
|
add_child(_rotate_root)
|
||||||
|
|
||||||
# Z axis line + arrow
|
_rotate_root.add_child(_create_torus(Vector3.UP, y_mat, "RingYaw"))
|
||||||
var z_axis := _create_axis_cylinder(Vector3(0, 0, 1), z_mat)
|
_rotate_root.add_child(_create_torus(Vector3.RIGHT, x_mat, "RingPitch"))
|
||||||
z_axis.name = "AxisZ"
|
_rotate_root.add_child(_create_torus(Vector3.FORWARD, z_mat, "RingRoll"))
|
||||||
add_child(z_axis)
|
|
||||||
|
# 3. SCALE Visuals (Lines + End Cubes)
|
||||||
|
_scale_root = Node3D.new()
|
||||||
|
_scale_root.name = "ScaleRoot"
|
||||||
|
add_child(_scale_root)
|
||||||
|
|
||||||
|
_scale_root.add_child(_create_scale_handle(Vector3(1, 0, 0), x_mat, "ScaleX"))
|
||||||
|
_scale_root.add_child(_create_scale_handle(Vector3(0, 1, 0), y_mat, "ScaleY"))
|
||||||
|
_scale_root.add_child(_create_scale_handle(Vector3(0, 0, 1), z_mat, "ScaleZ"))
|
||||||
|
|
||||||
|
# Center uniform scale cube
|
||||||
|
var center_cube := MeshInstance3D.new()
|
||||||
|
var c_box := BoxMesh.new()
|
||||||
|
c_box.size = Vector3(0.25, 0.25, 0.25)
|
||||||
|
center_cube.mesh = c_box
|
||||||
|
center_cube.material_override = center_mat
|
||||||
|
_scale_root.add_child(center_cube)
|
||||||
|
|
||||||
|
|
||||||
func _create_axis_cylinder(dir: Vector3, mat: Material) -> Node3D:
|
func _create_arrow(dir: Vector3, mat: Material, node_name: String) -> Node3D:
|
||||||
var container := Node3D.new()
|
var container := Node3D.new()
|
||||||
|
container.name = node_name
|
||||||
|
|
||||||
|
# Shaft
|
||||||
var shaft := MeshInstance3D.new()
|
var shaft := MeshInstance3D.new()
|
||||||
var cyl := CylinderMesh.new()
|
var cyl := CylinderMesh.new()
|
||||||
cyl.top_radius = 0.03
|
cyl.top_radius = 0.035
|
||||||
cyl.bottom_radius = 0.03
|
cyl.bottom_radius = 0.035
|
||||||
cyl.height = 1.5
|
cyl.height = 1.3
|
||||||
cyl.material = mat
|
|
||||||
shaft.mesh = cyl
|
shaft.mesh = cyl
|
||||||
|
shaft.material_override = mat
|
||||||
|
|
||||||
|
# Cone Head
|
||||||
|
var head := MeshInstance3D.new()
|
||||||
|
var cone := CylinderMesh.new()
|
||||||
|
cone.top_radius = 0.0
|
||||||
|
cone.bottom_radius = 0.12
|
||||||
|
cone.height = 0.35
|
||||||
|
head.mesh = cone
|
||||||
|
head.material_override = mat
|
||||||
|
|
||||||
# Point cylinder along direction
|
|
||||||
if dir == Vector3(1, 0, 0):
|
if dir == Vector3(1, 0, 0):
|
||||||
shaft.rotation_degrees = Vector3(0, 0, -90)
|
shaft.rotation_degrees = Vector3(0, 0, -90)
|
||||||
shaft.position = Vector3(0.75, 0, 0)
|
shaft.position = Vector3(0.65, 0, 0)
|
||||||
|
head.rotation_degrees = Vector3(0, 0, -90)
|
||||||
|
head.position = Vector3(1.45, 0, 0)
|
||||||
elif dir == Vector3(0, 1, 0):
|
elif dir == Vector3(0, 1, 0):
|
||||||
shaft.position = Vector3(0, 0.75, 0)
|
shaft.position = Vector3(0, 0.65, 0)
|
||||||
|
head.position = Vector3(0, 1.45, 0)
|
||||||
elif dir == Vector3(0, 0, 1):
|
elif dir == Vector3(0, 0, 1):
|
||||||
shaft.rotation_degrees = Vector3(90, 0, 0)
|
shaft.rotation_degrees = Vector3(90, 0, 0)
|
||||||
shaft.position = Vector3(0, 0, 0.75)
|
shaft.position = Vector3(0, 0, 0.65)
|
||||||
|
head.rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
head.position = Vector3(0, 0, 1.45)
|
||||||
|
|
||||||
container.add_child(shaft)
|
container.add_child(shaft)
|
||||||
|
container.add_child(head)
|
||||||
return container
|
return container
|
||||||
|
|
||||||
|
|
||||||
func _update_visual_mode() -> void:
|
func _create_torus(normal: Vector3, mat: Material, node_name: String) -> Node3D:
|
||||||
visible = (target_node != null and current_mode != Mode.NONE)
|
var inst := MeshInstance3D.new()
|
||||||
|
inst.name = node_name
|
||||||
|
var torus := TorusMesh.new()
|
||||||
|
torus.inner_radius = 1.25
|
||||||
|
torus.outer_radius = 1.35
|
||||||
|
torus.rings = 32
|
||||||
|
torus.ring_segments = 8
|
||||||
|
inst.mesh = torus
|
||||||
|
inst.material_override = mat
|
||||||
|
|
||||||
|
if normal == Vector3.RIGHT:
|
||||||
|
inst.rotation_degrees = Vector3(0, 0, 90)
|
||||||
|
elif normal == Vector3.FORWARD:
|
||||||
|
inst.rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
|
||||||
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
func _create_scale_handle(dir: Vector3, mat: Material, node_name: String) -> Node3D:
|
||||||
|
var container := Node3D.new()
|
||||||
|
container.name = node_name
|
||||||
|
|
||||||
|
# Shaft
|
||||||
|
var shaft := MeshInstance3D.new()
|
||||||
|
var cyl := CylinderMesh.new()
|
||||||
|
cyl.top_radius = 0.035
|
||||||
|
cyl.bottom_radius = 0.035
|
||||||
|
cyl.height = 1.3
|
||||||
|
shaft.mesh = cyl
|
||||||
|
shaft.material_override = mat
|
||||||
|
|
||||||
|
# Cube Tip
|
||||||
|
var tip := MeshInstance3D.new()
|
||||||
|
var box := BoxMesh.new()
|
||||||
|
box.size = Vector3(0.2, 0.2, 0.2)
|
||||||
|
tip.mesh = box
|
||||||
|
tip.material_override = mat
|
||||||
|
|
||||||
|
if dir == Vector3(1, 0, 0):
|
||||||
|
shaft.rotation_degrees = Vector3(0, 0, -90)
|
||||||
|
shaft.position = Vector3(0.65, 0, 0)
|
||||||
|
tip.position = Vector3(1.35, 0, 0)
|
||||||
|
elif dir == Vector3(0, 1, 0):
|
||||||
|
shaft.position = Vector3(0, 0.65, 0)
|
||||||
|
tip.position = Vector3(0, 1.35, 0)
|
||||||
|
elif dir == Vector3(0, 0, 1):
|
||||||
|
shaft.rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
shaft.position = Vector3(0, 0, 0.65)
|
||||||
|
tip.position = Vector3(0, 0, 1.35)
|
||||||
|
|
||||||
|
container.add_child(shaft)
|
||||||
|
container.add_child(tip)
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
|
# ── Geometric Axis Picking ────────────────────────────────────────────
|
||||||
|
|
||||||
|
func pick_axis(ray_origin: Vector3, ray_dir: Vector3) -> Axis:
|
||||||
|
if not visible or target_node == null:
|
||||||
|
return Axis.NONE
|
||||||
|
|
||||||
|
var origin := position
|
||||||
|
# 1. Center / Ground plane handle test (radius 0.45)
|
||||||
|
var d_center := _point_to_ray_dist(origin, ray_origin, ray_dir)
|
||||||
|
if d_center < 0.45:
|
||||||
|
return Axis.XY
|
||||||
|
|
||||||
|
match current_mode:
|
||||||
|
Mode.TRANSLATE:
|
||||||
|
var d_x := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(1.6, 0, 0))
|
||||||
|
var d_y := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(0, 1.6, 0))
|
||||||
|
var d_z := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(0, 0, 1.6))
|
||||||
|
var min_d := minf(d_x, minf(d_y, d_z))
|
||||||
|
if min_d < 0.35:
|
||||||
|
if min_d == d_y: return Axis.Y
|
||||||
|
if min_d == d_x: return Axis.X
|
||||||
|
if min_d == d_z: return Axis.Z
|
||||||
|
|
||||||
|
Mode.ROTATE:
|
||||||
|
var d_yaw := _ray_ring_distance(ray_origin, ray_dir, origin, Vector3.UP, 1.3)
|
||||||
|
var d_pitch := _ray_ring_distance(ray_origin, ray_dir, origin, Vector3.RIGHT, 1.3)
|
||||||
|
var d_roll := _ray_ring_distance(ray_origin, ray_dir, origin, Vector3.FORWARD, 1.3)
|
||||||
|
var min_r := minf(d_yaw, minf(d_pitch, d_roll))
|
||||||
|
if min_r < 0.40:
|
||||||
|
if min_r == d_yaw: return Axis.Y
|
||||||
|
if min_r == d_pitch: return Axis.X
|
||||||
|
if min_r == d_roll: return Axis.Z
|
||||||
|
|
||||||
|
Mode.SCALE:
|
||||||
|
var d_x := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(1.5, 0, 0))
|
||||||
|
var d_y := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(0, 1.5, 0))
|
||||||
|
var d_z := get_ray_segment_distance(ray_origin, ray_dir, origin, origin + Vector3(0, 0, 1.5))
|
||||||
|
var min_d := minf(d_x, minf(d_y, d_z))
|
||||||
|
if min_d < 0.35:
|
||||||
|
if min_d == d_y: return Axis.Y
|
||||||
|
if min_d == d_x: return Axis.X
|
||||||
|
if min_d == d_z: return Axis.Z
|
||||||
|
|
||||||
|
return Axis.NONE
|
||||||
|
|
||||||
|
|
||||||
|
static func _point_to_ray_dist(p: Vector3, ray_origin: Vector3, ray_dir: Vector3) -> float:
|
||||||
|
var u := ray_dir.normalized()
|
||||||
|
var to_p := p - ray_origin
|
||||||
|
var proj := to_p.dot(u)
|
||||||
|
if proj < 0.0:
|
||||||
|
return 1e9
|
||||||
|
var closest := ray_origin + u * proj
|
||||||
|
return p.distance_to(closest)
|
||||||
|
|
||||||
|
|
||||||
|
static func _ray_ring_distance(ray_origin: Vector3, ray_dir: Vector3, ring_center: Vector3, ring_normal: Vector3, ring_radius: float) -> float:
|
||||||
|
var denom := ray_dir.dot(ring_normal)
|
||||||
|
if absf(denom) < 1e-5:
|
||||||
|
return 1e9
|
||||||
|
var t := (ring_center - ray_origin).dot(ring_normal) / denom
|
||||||
|
if t < 0.0:
|
||||||
|
return 1e9
|
||||||
|
var hit := ray_origin + ray_dir * t
|
||||||
|
var d_from_center := hit.distance_to(ring_center)
|
||||||
|
return absf(d_from_center - ring_radius)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_ray_segment_distance(ray_origin: Vector3, ray_dir: Vector3, seg_a: Vector3, seg_b: Vector3) -> float:
|
||||||
|
var u := ray_dir.normalized()
|
||||||
|
var v := seg_b - seg_a
|
||||||
|
var w0 := ray_origin - seg_a
|
||||||
|
var a := u.dot(u)
|
||||||
|
var b := u.dot(v)
|
||||||
|
var c := v.dot(v)
|
||||||
|
var d := u.dot(w0)
|
||||||
|
var e := v.dot(w0)
|
||||||
|
var denom := a * c - b * b
|
||||||
|
var s: float = 0.0
|
||||||
|
var t: float = 0.0
|
||||||
|
if denom > 1e-6:
|
||||||
|
s = (b * e - c * d) / denom
|
||||||
|
t = (a * e - b * d) / denom
|
||||||
|
else:
|
||||||
|
s = 0.0
|
||||||
|
t = e / c if c > 1e-6 else 0.0
|
||||||
|
s = maxf(0.0, s)
|
||||||
|
t = clampf(t, 0.0, 1.0)
|
||||||
|
var pt_ray := ray_origin + u * s
|
||||||
|
var pt_seg := seg_a + v * t
|
||||||
|
return pt_ray.distance_to(pt_seg)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Drag Lifecycle (Backwards Compatibility) ──────────────────────────
|
||||||
|
|
||||||
func start_drag(axis: Axis) -> void:
|
func start_drag(axis: Axis) -> void:
|
||||||
if target_node == null:
|
if target_node == null:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class_name StudioInspectorDock
|
|||||||
extends VBoxContainer
|
extends VBoxContainer
|
||||||
## Property Inspector Dock for ModBison Studio.
|
## Property Inspector Dock for ModBison Studio.
|
||||||
## Renders editable properties for selected world object(s) with undo/redo support.
|
## Renders editable properties for selected world object(s) with undo/redo support.
|
||||||
|
## Synchronizes Godot 3D world transforms (X, Y_height, Z_depth) with Chunk storage.
|
||||||
|
|
||||||
signal property_changed(object_id: String, key: String, value: Variant)
|
signal property_changed(object_id: String, key: String, value: Variant)
|
||||||
signal delete_requested(object_id: String)
|
signal delete_requested(object_id: String)
|
||||||
@@ -15,7 +16,7 @@ var _empty_label: Label
|
|||||||
var _scroll: ScrollContainer
|
var _scroll: ScrollContainer
|
||||||
var _content_vbox: VBoxContainer
|
var _content_vbox: VBoxContainer
|
||||||
|
|
||||||
# Transform fields
|
# Transform fields (Godot 3D: X, Y_up, Z_depth)
|
||||||
var _pos_x: SpinBox
|
var _pos_x: SpinBox
|
||||||
var _pos_y: SpinBox
|
var _pos_y: SpinBox
|
||||||
var _pos_z: SpinBox
|
var _pos_z: SpinBox
|
||||||
@@ -27,6 +28,7 @@ var _scale_z: SpinBox
|
|||||||
# Metadata
|
# Metadata
|
||||||
var _name_edit: LineEdit
|
var _name_edit: LineEdit
|
||||||
var _asset_label: Label
|
var _asset_label: Label
|
||||||
|
var _btn_delete: Button
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
@@ -83,27 +85,40 @@ func _build_ui() -> void:
|
|||||||
|
|
||||||
# Transform header
|
# Transform header
|
||||||
var t_header := Label.new()
|
var t_header := Label.new()
|
||||||
t_header.text = "Transform"
|
t_header.text = "\nTransform (3D Space)"
|
||||||
t_header.add_theme_font_size_override("font_size", 11)
|
t_header.add_theme_font_size_override("font_size", 11)
|
||||||
t_header.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
t_header.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
||||||
_content_vbox.add_child(t_header)
|
_content_vbox.add_child(t_header)
|
||||||
|
|
||||||
_pos_x = _add_spin("Pos X", _on_pos_changed)
|
_pos_x = _add_spin("Pos X", _on_pos_changed)
|
||||||
_pos_y = _add_spin("Pos Y", _on_pos_changed)
|
_pos_y = _add_spin("Pos Y (Up)", _on_pos_changed, -50.0, 500.0, 0.5)
|
||||||
_pos_z = _add_spin("Pos Z", _on_pos_changed)
|
_pos_z = _add_spin("Pos Z", _on_pos_changed)
|
||||||
|
|
||||||
_rot_y = _add_spin("Yaw (°)", _on_rot_changed, -360.0, 360.0, 1.0)
|
_rot_y = _add_spin("Yaw (°)", _on_rot_changed, -360.0, 360.0, 15.0)
|
||||||
|
|
||||||
_scale_x = _add_spin("Scale X", _on_scale_changed, 0.05, 50.0, 0.1)
|
_scale_x = _add_spin("Scale X", _on_scale_changed, 0.1, 50.0, 0.1)
|
||||||
_scale_y = _add_spin("Scale Y", _on_scale_changed, 0.05, 50.0, 0.1)
|
_scale_y = _add_spin("Scale Y", _on_scale_changed, 0.1, 50.0, 0.1)
|
||||||
_scale_z = _add_spin("Scale Z", _on_scale_changed, 0.05, 50.0, 0.1)
|
_scale_z = _add_spin("Scale Z", _on_scale_changed, 0.1, 50.0, 0.1)
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
var act_sep := HSeparator.new()
|
||||||
|
_content_vbox.add_child(act_sep)
|
||||||
|
|
||||||
|
_btn_delete = Button.new()
|
||||||
|
_btn_delete.text = "🗑 Delete Object"
|
||||||
|
_btn_delete.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
_btn_delete.pressed.connect(func():
|
||||||
|
if _selected_id != "":
|
||||||
|
delete_requested.emit(_selected_id)
|
||||||
|
)
|
||||||
|
_content_vbox.add_child(_btn_delete)
|
||||||
|
|
||||||
|
|
||||||
func _add_spin(label: String, callback: Callable, min_val: float = -500.0, max_val: float = 500.0, step: float = 0.5) -> SpinBox:
|
func _add_spin(label: String, callback: Callable, min_val: float = -500.0, max_val: float = 500.0, step: float = 0.5) -> SpinBox:
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
var lbl := Label.new()
|
var lbl := Label.new()
|
||||||
lbl.text = label + ":"
|
lbl.text = label + ":"
|
||||||
lbl.custom_minimum_size = Vector2(70, 0)
|
lbl.custom_minimum_size = Vector2(80, 0)
|
||||||
lbl.add_theme_font_size_override("font_size", 10)
|
lbl.add_theme_font_size_override("font_size", 10)
|
||||||
row.add_child(lbl)
|
row.add_child(lbl)
|
||||||
|
|
||||||
@@ -118,6 +133,18 @@ func _add_spin(label: String, callback: Callable, min_val: float = -500.0, max_v
|
|||||||
return spin
|
return spin
|
||||||
|
|
||||||
|
|
||||||
|
func set_transform_values(godot_pos: Vector3, yaw_deg: float, godot_scale: Vector3) -> void:
|
||||||
|
_updating = true
|
||||||
|
if _pos_x: _pos_x.value = godot_pos.x
|
||||||
|
if _pos_y: _pos_y.value = godot_pos.y
|
||||||
|
if _pos_z: _pos_z.value = godot_pos.z
|
||||||
|
if _rot_y: _rot_y.value = yaw_deg
|
||||||
|
if _scale_x: _scale_x.value = godot_scale.x
|
||||||
|
if _scale_y: _scale_y.value = godot_scale.y
|
||||||
|
if _scale_z: _scale_z.value = godot_scale.z
|
||||||
|
_updating = false
|
||||||
|
|
||||||
|
|
||||||
func _on_selection_changed(obj_id: String) -> void:
|
func _on_selection_changed(obj_id: String) -> void:
|
||||||
_selected_id = obj_id
|
_selected_id = obj_id
|
||||||
if _selected_id == "" or _core == null:
|
if _selected_id == "" or _core == null:
|
||||||
@@ -141,16 +168,18 @@ func _on_selection_changed(obj_id: String) -> void:
|
|||||||
_name_edit.text = dname
|
_name_edit.text = dname
|
||||||
_asset_label.text = "Asset: %s" % obj.asset_id
|
_asset_label.text = "Asset: %s" % obj.asset_id
|
||||||
|
|
||||||
_pos_x.value = obj.position.x
|
# Map chunk coords (X, Y_ground, Z_height) to Godot 3D (X, Y_height, Z_depth)
|
||||||
_pos_y.value = obj.position.y
|
var godot_pos := Vector3(obj.position.x, obj.position.z, obj.position.y)
|
||||||
_pos_z.value = obj.position.z
|
_pos_x.value = godot_pos.x
|
||||||
|
_pos_y.value = godot_pos.y
|
||||||
|
_pos_z.value = godot_pos.z
|
||||||
|
|
||||||
var euler = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w).get_euler()
|
var euler = Quaternion(obj.rotation.x, obj.rotation.z, obj.rotation.y, obj.rotation.w).get_euler()
|
||||||
_rot_y.value = rad_to_deg(euler.y)
|
_rot_y.value = rad_to_deg(euler.y)
|
||||||
|
|
||||||
_scale_x.value = obj.scale.x
|
_scale_x.value = obj.scale.x
|
||||||
_scale_y.value = obj.scale.y
|
_scale_y.value = obj.scale.z
|
||||||
_scale_z.value = obj.scale.z
|
_scale_z.value = obj.scale.y
|
||||||
_updating = false
|
_updating = false
|
||||||
|
|
||||||
|
|
||||||
@@ -164,23 +193,29 @@ func _on_name_changed(new_name: String) -> void:
|
|||||||
func _on_pos_changed(_val: float) -> void:
|
func _on_pos_changed(_val: float) -> void:
|
||||||
if _updating or _core == null or _selected_id == "":
|
if _updating or _core == null or _selected_id == "":
|
||||||
return
|
return
|
||||||
var new_pos := Vector3(_pos_x.value, _pos_y.value, _pos_z.value)
|
var godot_pos := Vector3(_pos_x.value, _pos_y.value, _pos_z.value)
|
||||||
_core.move_object(_selected_id, new_pos)
|
# Convert Godot 3D (X, Y_up, Z_depth) to Chunk (X, Y_ground, Z_height)
|
||||||
property_changed.emit(_selected_id, "position", new_pos)
|
var chunk_pos := Vector3(godot_pos.x, godot_pos.z, godot_pos.y)
|
||||||
|
_core.move_object(_selected_id, chunk_pos)
|
||||||
|
property_changed.emit(_selected_id, "position", godot_pos)
|
||||||
|
|
||||||
|
|
||||||
func _on_rot_changed(_val: float) -> void:
|
func _on_rot_changed(_val: float) -> void:
|
||||||
if _updating or _core == null or _selected_id == "":
|
if _updating or _core == null or _selected_id == "":
|
||||||
return
|
return
|
||||||
var quat := Quaternion.from_euler(Vector3(0, deg_to_rad(_rot_y.value), 0))
|
var deg := _rot_y.value
|
||||||
var rot_vec := Vector4(quat.x, quat.y, quat.z, quat.w)
|
var godot_q := Quaternion(Vector3.UP, deg_to_rad(deg))
|
||||||
|
# Convert to chunk rotation quat
|
||||||
|
var chunk_q := Quaternion(godot_q.x, godot_q.z, godot_q.y, godot_q.w)
|
||||||
|
var rot_vec := Vector4(chunk_q.x, chunk_q.y, chunk_q.z, chunk_q.w)
|
||||||
_core.rotate_object(_selected_id, rot_vec)
|
_core.rotate_object(_selected_id, rot_vec)
|
||||||
property_changed.emit(_selected_id, "rotation", rot_vec)
|
property_changed.emit(_selected_id, "rotation", godot_q)
|
||||||
|
|
||||||
|
|
||||||
func _on_scale_changed(_val: float) -> void:
|
func _on_scale_changed(_val: float) -> void:
|
||||||
if _updating or _core == null or _selected_id == "":
|
if _updating or _core == null or _selected_id == "":
|
||||||
return
|
return
|
||||||
var new_scale := Vector3(_scale_x.value, _scale_y.value, _scale_z.value)
|
var godot_scale := Vector3(_scale_x.value, _scale_y.value, _scale_z.value)
|
||||||
_core.scale_object(_selected_id, new_scale)
|
var chunk_scale := Vector3(godot_scale.x, godot_scale.z, godot_scale.y)
|
||||||
property_changed.emit(_selected_id, "scale", new_scale)
|
_core.scale_object(_selected_id, chunk_scale)
|
||||||
|
property_changed.emit(_selected_id, "scale", godot_scale)
|
||||||
|
|||||||
+362
-32
@@ -56,6 +56,18 @@ var _world_container: Node3D
|
|||||||
var _gizmo_3d: Node3D
|
var _gizmo_3d: Node3D
|
||||||
var _baseplate_mesh: MeshInstance3D
|
var _baseplate_mesh: MeshInstance3D
|
||||||
|
|
||||||
|
# Drag & Drop / Transformation Interaction State
|
||||||
|
var _is_dragging: bool = false
|
||||||
|
var _drag_axis: int = 0
|
||||||
|
var _drag_start_mouse: Vector2 = Vector2.ZERO
|
||||||
|
var _drag_start_ground: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_start_node_pos: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_start_node_rot_deg: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_start_node_scale: Vector3 = Vector3.ONE
|
||||||
|
var _drag_accum_y: float = 0.0
|
||||||
|
var _drag_accum_rot: float = 0.0
|
||||||
|
var _drag_accum_scale: float = 0.0
|
||||||
|
|
||||||
# Docks & Dialogs
|
# Docks & Dialogs
|
||||||
var _hierarchy_dock = null
|
var _hierarchy_dock = null
|
||||||
var _inspector_dock = null
|
var _inspector_dock = null
|
||||||
@@ -140,6 +152,7 @@ func _build_studio_layout() -> void:
|
|||||||
left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
_hierarchy_dock = StudioHierarchyDockClass.new()
|
_hierarchy_dock = StudioHierarchyDockClass.new()
|
||||||
_hierarchy_dock.setup(core)
|
_hierarchy_dock.setup(core)
|
||||||
|
_hierarchy_dock.delete_requested.connect(_delete_selected_object)
|
||||||
left_panel.add_child(_hierarchy_dock)
|
left_panel.add_child(_hierarchy_dock)
|
||||||
workspace_hbox.add_child(left_panel)
|
workspace_hbox.add_child(left_panel)
|
||||||
|
|
||||||
@@ -168,6 +181,8 @@ func _build_studio_layout() -> void:
|
|||||||
|
|
||||||
_inspector_dock = StudioInspectorDockClass.new()
|
_inspector_dock = StudioInspectorDockClass.new()
|
||||||
_inspector_dock.setup(core)
|
_inspector_dock.setup(core)
|
||||||
|
_inspector_dock.property_changed.connect(_on_inspector_property_changed)
|
||||||
|
_inspector_dock.delete_requested.connect(_delete_selected_object)
|
||||||
tabs.add_child(_inspector_dock)
|
tabs.add_child(_inspector_dock)
|
||||||
tabs.set_tab_title(0, "Inspector")
|
tabs.set_tab_title(0, "Inspector")
|
||||||
|
|
||||||
@@ -306,7 +321,7 @@ func _build_toolbar(parent: Control) -> void:
|
|||||||
_toolbar.add_child(snap_lbl)
|
_toolbar.add_child(snap_lbl)
|
||||||
|
|
||||||
_snap_spin = SpinBox.new()
|
_snap_spin = SpinBox.new()
|
||||||
_snap_spin.min_value = 0.1
|
_snap_spin.min_value = 0.5
|
||||||
_snap_spin.max_value = 10.0
|
_snap_spin.max_value = 10.0
|
||||||
_snap_spin.step = 0.5
|
_snap_spin.step = 0.5
|
||||||
_snap_spin.value = 1.0
|
_snap_spin.value = 1.0
|
||||||
@@ -393,13 +408,15 @@ func _quick_insert(cat_id: String, display_name: String) -> void:
|
|||||||
if _camera_3d != null:
|
if _camera_3d != null:
|
||||||
spawn_pos = _camera_3d.focal_point
|
spawn_pos = _camera_3d.focal_point
|
||||||
spawn_pos.y = 0.0
|
spawn_pos.y = 0.0
|
||||||
var new_id := core.place_object(cat_id, spawn_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
var chunk_pos := _godot_to_chunk_pos(spawn_pos)
|
||||||
|
var new_id := core.place_object(cat_id, chunk_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"catalog_id": cat_id
|
"catalog_id": cat_id
|
||||||
})
|
})
|
||||||
if new_id != "":
|
if new_id != "":
|
||||||
core.select_object(new_id)
|
|
||||||
_spawn_visual_node(new_id)
|
_spawn_visual_node(new_id)
|
||||||
|
core.select_object(new_id)
|
||||||
|
_on_selection_changed(new_id)
|
||||||
_hierarchy_dock.refresh_tree()
|
_hierarchy_dock.refresh_tree()
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
_status_bar_label.text = "Spawned %s at (%.1f, 0, %.1f) | ✛ Move gizmo active" % [display_name, spawn_pos.x, spawn_pos.z]
|
_status_bar_label.text = "Spawned %s at (%.1f, 0, %.1f) | ✛ Move gizmo active" % [display_name, spawn_pos.x, spawn_pos.z]
|
||||||
@@ -425,13 +442,26 @@ func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
|||||||
# Studio Baseplate Ground Mesh
|
# Studio Baseplate Ground Mesh
|
||||||
_baseplate_mesh = MeshInstance3D.new()
|
_baseplate_mesh = MeshInstance3D.new()
|
||||||
var plane := PlaneMesh.new()
|
var plane := PlaneMesh.new()
|
||||||
plane.size = Vector2(64, 64)
|
plane.size = Vector2(128, 128)
|
||||||
var gmat := StandardMaterial3D.new()
|
var gmat := StandardMaterial3D.new()
|
||||||
gmat.albedo_color = Color(0.20, 0.23, 0.27)
|
gmat.albedo_color = Color(0.20, 0.23, 0.27)
|
||||||
gmat.roughness = 0.85
|
gmat.roughness = 0.85
|
||||||
_baseplate_mesh.mesh = plane
|
_baseplate_mesh.mesh = plane
|
||||||
vp.add_child(_baseplate_mesh)
|
vp.add_child(_baseplate_mesh)
|
||||||
|
|
||||||
|
# Solid Physics Floor Collider (layer 1, 500x500m surface at Y=0)
|
||||||
|
var floor_body := StaticBody3D.new()
|
||||||
|
floor_body.name = "StudioFloorBody"
|
||||||
|
floor_body.set_collision_layer_value(1, true)
|
||||||
|
floor_body.set_collision_mask_value(1, true)
|
||||||
|
var floor_col := CollisionShape3D.new()
|
||||||
|
var floor_box := BoxShape3D.new()
|
||||||
|
floor_box.size = Vector3(500.0, 2.0, 500.0)
|
||||||
|
floor_col.shape = floor_box
|
||||||
|
floor_col.position = Vector3(0, -1.0, 0)
|
||||||
|
floor_body.add_child(floor_col)
|
||||||
|
vp.add_child(floor_body)
|
||||||
|
|
||||||
# 3D Grid floor lines
|
# 3D Grid floor lines
|
||||||
var grid := _create_3d_grid(32, 1.0)
|
var grid := _create_3d_grid(32, 1.0)
|
||||||
vp.add_child(grid)
|
vp.add_child(grid)
|
||||||
@@ -470,7 +500,7 @@ func _setup_viewport_hud_overlay(parent: Control) -> void:
|
|||||||
hud_bar.position = Vector2(16, -42)
|
hud_bar.position = Vector2(16, -42)
|
||||||
|
|
||||||
var hud_text := Label.new()
|
var hud_text := Label.new()
|
||||||
hud_text.text = "🎥 RMB+WASD: Fly | 🖱️ LMB: Click to Select | ✛ [2] Move | ↻ [3] Rotate | [F] Focus | ▶ [F5] Play | ❓ [F1] Help"
|
hud_text.text = "🎥 RMB+WASD: Fly | 🖱️ LMB: Click/Drag to Move | ✛ [2] Move | ↻ [3] Rotate | [F] Focus | ▶ [F5] Play | ❓ [F1] Help"
|
||||||
hud_text.add_theme_font_size_override("font_size", 11)
|
hud_text.add_theme_font_size_override("font_size", 11)
|
||||||
hud_text.add_theme_color_override("font_color", Color(0.9, 0.92, 0.96))
|
hud_text.add_theme_color_override("font_color", Color(0.9, 0.92, 0.96))
|
||||||
hud_bar.add_child(hud_text)
|
hud_bar.add_child(hud_text)
|
||||||
@@ -497,6 +527,8 @@ func _create_3d_grid(size: int, step: float) -> Node3D:
|
|||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
|
||||||
|
# ── Keyboard Shortcuts & Nudge ────────────────────────────────────────
|
||||||
|
|
||||||
func _setup_shortcuts() -> void:
|
func _setup_shortcuts() -> void:
|
||||||
set_process_unhandled_key_input(true)
|
set_process_unhandled_key_input(true)
|
||||||
|
|
||||||
@@ -524,6 +556,26 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
_simulation_mgr.stop_simulation()
|
_simulation_mgr.stop_simulation()
|
||||||
else:
|
else:
|
||||||
_simulation_mgr.start_play(true)
|
_simulation_mgr.start_play(true)
|
||||||
|
elif k.keycode == KEY_DELETE or k.keycode == KEY_BACKSPACE:
|
||||||
|
var sel := _get_selected_id()
|
||||||
|
if sel != "":
|
||||||
|
_delete_selected_object(sel)
|
||||||
|
elif k.keycode == KEY_BRACKETLEFT and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_rotation(-45.0)
|
||||||
|
elif k.keycode == KEY_BRACKETRIGHT and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_rotation(45.0)
|
||||||
|
elif k.keycode == KEY_LEFT and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_position(Vector3(-1, 0, 0))
|
||||||
|
elif k.keycode == KEY_RIGHT and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_position(Vector3(1, 0, 0))
|
||||||
|
elif k.keycode == KEY_UP and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_position(Vector3(0, 0, -1))
|
||||||
|
elif k.keycode == KEY_DOWN and not k.is_command_or_control_pressed():
|
||||||
|
_nudge_selected_position(Vector3(0, 0, 1))
|
||||||
|
elif k.keycode == KEY_PAGEUP:
|
||||||
|
_nudge_selected_position(Vector3(0, 1, 0))
|
||||||
|
elif k.keycode == KEY_PAGEDOWN:
|
||||||
|
_nudge_selected_position(Vector3(0, -1, 0))
|
||||||
elif k.keycode == KEY_1 and not k.is_command_or_control_pressed():
|
elif k.keycode == KEY_1 and not k.is_command_or_control_pressed():
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.NONE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.NONE)
|
||||||
elif k.keycode == KEY_2 and not k.is_command_or_control_pressed():
|
elif k.keycode == KEY_2 and not k.is_command_or_control_pressed():
|
||||||
@@ -533,13 +585,66 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
elif k.keycode == KEY_4 and not k.is_command_or_control_pressed():
|
elif k.keycode == KEY_4 and not k.is_command_or_control_pressed():
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE)
|
||||||
elif k.keycode == KEY_F and not k.is_command_or_control_pressed():
|
elif k.keycode == KEY_F and not k.is_command_or_control_pressed():
|
||||||
var sel_id = core.selection.get_selected_id() if core.selection.has_method("get_selected_id") else ""
|
var sel_id := _get_selected_id()
|
||||||
if sel_id != "":
|
if sel_id != "":
|
||||||
var n = _world_container.get_node_or_null(sel_id)
|
var n := _world_container.get_node_or_null(sel_id)
|
||||||
if n is Node3D and _camera_3d != null:
|
if n is Node3D and _camera_3d != null:
|
||||||
_camera_3d.focus_on(n.global_position)
|
_camera_3d.focus_on(n.global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _nudge_selected_position(dir: Vector3) -> void:
|
||||||
|
var sel := _get_selected_id()
|
||||||
|
if sel == "":
|
||||||
|
return
|
||||||
|
var node := _world_container.get_node_or_null(sel) as Node3D
|
||||||
|
if node == null:
|
||||||
|
return
|
||||||
|
var step: float = _snap_spin.value if _snap_spin else 1.0
|
||||||
|
var new_pos := node.position + dir * step
|
||||||
|
new_pos.y = maxf(0.0, new_pos.y)
|
||||||
|
node.position = new_pos
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.position = node.position
|
||||||
|
var chunk_pos := _godot_to_chunk_pos(new_pos)
|
||||||
|
core.move_object(sel, chunk_pos)
|
||||||
|
if _inspector_dock != null:
|
||||||
|
_inspector_dock.set_transform_values(new_pos, node.rotation_degrees.y, node.scale)
|
||||||
|
_status_bar_label.text = "Nudged %s to (%.1f, %.1f, %.1f)" % [sel, new_pos.x, new_pos.y, new_pos.z]
|
||||||
|
|
||||||
|
|
||||||
|
func _nudge_selected_rotation(delta_deg: float) -> void:
|
||||||
|
var sel := _get_selected_id()
|
||||||
|
if sel == "":
|
||||||
|
return
|
||||||
|
var node := _world_container.get_node_or_null(sel) as Node3D
|
||||||
|
if node == null:
|
||||||
|
return
|
||||||
|
var new_yaw := fmod(node.rotation_degrees.y + delta_deg, 360.0)
|
||||||
|
node.rotation_degrees.y = new_yaw
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.quaternion = node.quaternion
|
||||||
|
var q := node.quaternion
|
||||||
|
var chunk_q := _godot_to_chunk_quat(q)
|
||||||
|
core.rotate_object(sel, Vector4(chunk_q.x, chunk_q.y, chunk_q.z, chunk_q.w))
|
||||||
|
if _inspector_dock != null:
|
||||||
|
_inspector_dock.set_transform_values(node.position, new_yaw, node.scale)
|
||||||
|
_status_bar_label.text = "Rotated %s to %.0f°" % [sel, new_yaw]
|
||||||
|
|
||||||
|
|
||||||
|
func _delete_selected_object(obj_id: String) -> void:
|
||||||
|
if obj_id == "":
|
||||||
|
obj_id = _get_selected_id()
|
||||||
|
if obj_id == "":
|
||||||
|
return
|
||||||
|
core.delete_object(obj_id)
|
||||||
|
var node := _world_container.get_node_or_null(obj_id)
|
||||||
|
if node != null:
|
||||||
|
node.queue_free()
|
||||||
|
core.select_object("")
|
||||||
|
_hierarchy_dock.refresh_tree()
|
||||||
|
_status_bar_label.text = "Deleted object '%s' | Undo with [Ctrl+Z]" % obj_id
|
||||||
|
|
||||||
|
|
||||||
func _set_gizmo_mode(mode: TransformGizmo3DClass.Mode) -> void:
|
func _set_gizmo_mode(mode: TransformGizmo3DClass.Mode) -> void:
|
||||||
if _gizmo_3d != null:
|
if _gizmo_3d != null:
|
||||||
_gizmo_3d.set_mode(mode)
|
_gizmo_3d.set_mode(mode)
|
||||||
@@ -565,21 +670,188 @@ func _return_to_main_menu() -> void:
|
|||||||
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
|
||||||
|
|
||||||
# ── Interactive 3D Viewport Input & Raycast Picking ─────────────────────
|
# ── Interactive 3D Viewport Input & Picking ───────────────────────────
|
||||||
|
|
||||||
func handle_viewport_input(event: InputEvent) -> void:
|
func handle_viewport_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton:
|
if event is InputEventMouseButton:
|
||||||
var mb := event as InputEventMouseButton
|
var mb := event as InputEventMouseButton
|
||||||
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||||
var hit_obj := _raycast_find_object(mb.position)
|
if mb.pressed:
|
||||||
if hit_obj != null:
|
_handle_left_press(mb)
|
||||||
core.select_object(hit_obj.name)
|
|
||||||
if mb.double_click and _camera_3d != null:
|
|
||||||
_camera_3d.focus_on(hit_obj.global_position)
|
|
||||||
_status_bar_label.text = "Selected: %s | ✛ Drag handles or [2-4] switch tools" % hit_obj.name
|
|
||||||
else:
|
else:
|
||||||
core.select_object("")
|
_handle_left_release(mb)
|
||||||
_status_bar_label.text = "Deselected | Click an object or [+ Add] to build"
|
|
||||||
|
elif event is InputEventMouseMotion and _is_dragging:
|
||||||
|
var mm := event as InputEventMouseMotion
|
||||||
|
_handle_drag_motion(mm)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_left_press(mb: InputEventMouseButton) -> void:
|
||||||
|
if _camera_3d == null or _world_container == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var ray_from: Vector3 = _camera_3d.project_ray_origin(mb.position)
|
||||||
|
var ray_dir: Vector3 = _camera_3d.project_ray_normal(mb.position)
|
||||||
|
var sel_id: String = _get_selected_id()
|
||||||
|
|
||||||
|
# 1. First priority: Check if clicking on Gizmo handle
|
||||||
|
if _gizmo_3d != null and _gizmo_3d.visible and sel_id != "":
|
||||||
|
var picked_axis: TransformGizmo3DClass.Axis = _gizmo_3d.pick_axis(ray_from, ray_dir)
|
||||||
|
if picked_axis != TransformGizmo3DClass.Axis.NONE:
|
||||||
|
_start_drag(picked_axis, mb.position)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Second priority: Check if clicking on a 3D world object
|
||||||
|
var hit_obj := _raycast_find_object(mb.position)
|
||||||
|
if hit_obj != null:
|
||||||
|
if hit_obj.name == sel_id and _gizmo_3d != null and _gizmo_3d.current_mode != TransformGizmo3DClass.Mode.NONE:
|
||||||
|
# Directly drag already selected object
|
||||||
|
_start_drag(TransformGizmo3DClass.Axis.XY, mb.position)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Select new object
|
||||||
|
core.select_object(hit_obj.name)
|
||||||
|
if mb.double_click and _camera_3d != null:
|
||||||
|
_camera_3d.focus_on(hit_obj.global_position)
|
||||||
|
_status_bar_label.text = "Selected: %s | ✛ Drag model or handles to move" % hit_obj.name
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Third: Clicked empty space -> deselect
|
||||||
|
_is_dragging = false
|
||||||
|
core.select_object("")
|
||||||
|
_status_bar_label.text = "Deselected | Click an object or [+ Add] to build"
|
||||||
|
|
||||||
|
|
||||||
|
func _start_drag(axis: TransformGizmo3DClass.Axis, mouse_pos: Vector2) -> void:
|
||||||
|
var sel_id := _get_selected_id()
|
||||||
|
if sel_id == "":
|
||||||
|
return
|
||||||
|
var node := _world_container.get_node_or_null(sel_id) as Node3D
|
||||||
|
if node == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_is_dragging = true
|
||||||
|
_drag_axis = axis
|
||||||
|
_drag_start_mouse = mouse_pos
|
||||||
|
_drag_start_ground = _raycast_ground_plane(mouse_pos)
|
||||||
|
_drag_start_node_pos = node.position
|
||||||
|
_drag_start_node_rot_deg = node.rotation_degrees
|
||||||
|
_drag_start_node_scale = node.scale
|
||||||
|
_drag_accum_y = 0.0
|
||||||
|
_drag_accum_rot = 0.0
|
||||||
|
_drag_accum_scale = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_drag_motion(mm: InputEventMouseMotion) -> void:
|
||||||
|
var sel_id := _get_selected_id()
|
||||||
|
if sel_id == "":
|
||||||
|
_is_dragging = false
|
||||||
|
return
|
||||||
|
var node := _world_container.get_node_or_null(sel_id) as Node3D
|
||||||
|
if node == null:
|
||||||
|
_is_dragging = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var snap_step: float = _snap_spin.value if _snap_spin else 1.0
|
||||||
|
|
||||||
|
match _gizmo_3d.current_mode if _gizmo_3d else TransformGizmo3DClass.Mode.TRANSLATE:
|
||||||
|
TransformGizmo3DClass.Mode.TRANSLATE:
|
||||||
|
if _drag_axis == TransformGizmo3DClass.Axis.Y:
|
||||||
|
var dy: float = -mm.relative.y * 0.05
|
||||||
|
_drag_accum_y += dy
|
||||||
|
var new_y := maxf(0.0, _drag_start_node_pos.y + _drag_accum_y)
|
||||||
|
if snap_step > 0.0:
|
||||||
|
new_y = snappedf(new_y, snap_step)
|
||||||
|
node.position.y = new_y
|
||||||
|
elif _drag_axis == TransformGizmo3DClass.Axis.X:
|
||||||
|
var curr_ground := _raycast_ground_plane(mm.position)
|
||||||
|
var dx := curr_ground.x - _drag_start_ground.x
|
||||||
|
if snap_step > 0.0:
|
||||||
|
dx = snappedf(dx, snap_step)
|
||||||
|
node.position.x = _drag_start_node_pos.x + dx
|
||||||
|
elif _drag_axis == TransformGizmo3DClass.Axis.Z:
|
||||||
|
var curr_ground := _raycast_ground_plane(mm.position)
|
||||||
|
var dz := curr_ground.z - _drag_start_ground.z
|
||||||
|
if snap_step > 0.0:
|
||||||
|
dz = snappedf(dz, snap_step)
|
||||||
|
node.position.z = _drag_start_node_pos.z + dz
|
||||||
|
else:
|
||||||
|
# Free ground plane movement (XZ)
|
||||||
|
var curr_ground := _raycast_ground_plane(mm.position)
|
||||||
|
var delta := curr_ground - _drag_start_ground
|
||||||
|
var new_x := _drag_start_node_pos.x + delta.x
|
||||||
|
var new_z := _drag_start_node_pos.z + delta.z
|
||||||
|
if snap_step > 0.0:
|
||||||
|
new_x = snappedf(new_x, snap_step)
|
||||||
|
new_z = snappedf(new_z, snap_step)
|
||||||
|
node.position.x = new_x
|
||||||
|
node.position.z = new_z
|
||||||
|
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.position = node.position
|
||||||
|
if _inspector_dock != null:
|
||||||
|
_inspector_dock.set_transform_values(node.position, node.rotation_degrees.y, node.scale)
|
||||||
|
_status_bar_label.text = "Moving %s to (%.1f, %.1f, %.1f)" % [sel_id, node.position.x, node.position.y, node.position.z]
|
||||||
|
|
||||||
|
TransformGizmo3DClass.Mode.ROTATE:
|
||||||
|
var rot_step: float = 15.0
|
||||||
|
_drag_accum_rot += mm.relative.x * 0.8
|
||||||
|
var new_yaw := _drag_start_node_rot_deg.y - _drag_accum_rot
|
||||||
|
if rot_step > 0.0:
|
||||||
|
new_yaw = snappedf(new_yaw, rot_step)
|
||||||
|
node.rotation_degrees.y = new_yaw
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.quaternion = node.quaternion
|
||||||
|
if _inspector_dock != null:
|
||||||
|
_inspector_dock.set_transform_values(node.position, new_yaw, node.scale)
|
||||||
|
_status_bar_label.text = "Rotating %s to %.0f°" % [sel_id, new_yaw]
|
||||||
|
|
||||||
|
TransformGizmo3DClass.Mode.SCALE:
|
||||||
|
var scale_step: float = 0.1
|
||||||
|
_drag_accum_scale += -mm.relative.y * 0.02
|
||||||
|
var factor := maxf(0.05, 1.0 + _drag_accum_scale)
|
||||||
|
var new_scale := _drag_start_node_scale * factor
|
||||||
|
if scale_step > 0.0:
|
||||||
|
new_scale.x = snappedf(new_scale.x, scale_step)
|
||||||
|
new_scale.y = snappedf(new_scale.y, scale_step)
|
||||||
|
new_scale.z = snappedf(new_scale.z, scale_step)
|
||||||
|
node.scale = new_scale
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.attach_to_node(node)
|
||||||
|
if _inspector_dock != null:
|
||||||
|
_inspector_dock.set_transform_values(node.position, node.rotation_degrees.y, new_scale)
|
||||||
|
_status_bar_label.text = "Scaling %s to (%.2f, %.2f, %.2f)" % [sel_id, new_scale.x, new_scale.y, new_scale.z]
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_left_release(_mb: InputEventMouseButton) -> void:
|
||||||
|
if not _is_dragging:
|
||||||
|
return
|
||||||
|
_is_dragging = false
|
||||||
|
var sel_id := _get_selected_id()
|
||||||
|
if sel_id == "":
|
||||||
|
return
|
||||||
|
var node := _world_container.get_node_or_null(sel_id) as Node3D
|
||||||
|
if node == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
match _gizmo_3d.current_mode if _gizmo_3d else TransformGizmo3DClass.Mode.TRANSLATE:
|
||||||
|
TransformGizmo3DClass.Mode.TRANSLATE:
|
||||||
|
var chunk_pos := _godot_to_chunk_pos(node.position)
|
||||||
|
core.move_object(sel_id, chunk_pos)
|
||||||
|
_status_bar_label.text = "Moved %s to (%.1f, %.1f, %.1f) | Undo with [Ctrl+Z]" % [sel_id, node.position.x, node.position.y, node.position.z]
|
||||||
|
|
||||||
|
TransformGizmo3DClass.Mode.ROTATE:
|
||||||
|
var q := node.quaternion
|
||||||
|
var chunk_q := _godot_to_chunk_quat(q)
|
||||||
|
core.rotate_object(sel_id, Vector4(chunk_q.x, chunk_q.y, chunk_q.z, chunk_q.w))
|
||||||
|
_status_bar_label.text = "Rotated %s to %.0f° | Undo with [Ctrl+Z]" % [sel_id, node.rotation_degrees.y]
|
||||||
|
|
||||||
|
TransformGizmo3DClass.Mode.SCALE:
|
||||||
|
var chunk_scale := _godot_to_chunk_scale(node.scale)
|
||||||
|
core.scale_object(sel_id, chunk_scale)
|
||||||
|
_status_bar_label.text = "Scaled %s to (%.2f, %.2f, %.2f) | Undo with [Ctrl+Z]" % [sel_id, node.scale.x, node.scale.y, node.scale.z]
|
||||||
|
|
||||||
|
_hierarchy_dock.refresh_tree()
|
||||||
|
|
||||||
|
|
||||||
func handle_viewport_drop(at_pos: Vector2, data: Variant) -> void:
|
func handle_viewport_drop(at_pos: Vector2, data: Variant) -> void:
|
||||||
@@ -591,21 +863,24 @@ func handle_viewport_drop(at_pos: Vector2, data: Variant) -> void:
|
|||||||
var new_id: String = ""
|
var new_id: String = ""
|
||||||
|
|
||||||
if target_obj != null and target_obj.name != "":
|
if target_obj != null and target_obj.name != "":
|
||||||
new_id = core.place_object_stacked(cat_id, target_obj.name, drop_pos, Vector3(0, 0, 1), 0.0, false, {
|
var target_chunk_pos := _godot_to_chunk_pos(drop_pos)
|
||||||
|
new_id = core.place_object_stacked(cat_id, target_obj.name, target_chunk_pos, Vector3(0, 0, 1), 0.0, false, {
|
||||||
"display_name": data.get("name", cat_id),
|
"display_name": data.get("name", cat_id),
|
||||||
"catalog_id": cat_id,
|
"catalog_id": cat_id,
|
||||||
"asset_id": data.get("asset_id", "")
|
"asset_id": data.get("asset_id", "")
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
new_id = core.place_object(cat_id, drop_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
var chunk_pos := _godot_to_chunk_pos(drop_pos)
|
||||||
|
new_id = core.place_object(cat_id, chunk_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
||||||
"display_name": data.get("name", cat_id),
|
"display_name": data.get("name", cat_id),
|
||||||
"catalog_id": cat_id,
|
"catalog_id": cat_id,
|
||||||
"asset_id": data.get("asset_id", "")
|
"asset_id": data.get("asset_id", "")
|
||||||
})
|
})
|
||||||
|
|
||||||
if new_id != "":
|
if new_id != "":
|
||||||
core.select_object(new_id)
|
|
||||||
_spawn_visual_node(new_id)
|
_spawn_visual_node(new_id)
|
||||||
|
core.select_object(new_id)
|
||||||
|
_on_selection_changed(new_id)
|
||||||
_hierarchy_dock.refresh_tree()
|
_hierarchy_dock.refresh_tree()
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
_status_bar_label.text = "Placed %s at (%.1f, %.1f, %.1f)" % [data.get("name", cat_id), drop_pos.x, drop_pos.y, drop_pos.z]
|
_status_bar_label.text = "Placed %s at (%.1f, %.1f, %.1f)" % [data.get("name", cat_id), drop_pos.x, drop_pos.y, drop_pos.z]
|
||||||
@@ -661,17 +936,63 @@ func _raycast_ground_plane(screen_pos: Vector2) -> Vector3:
|
|||||||
return from + dir * t
|
return from + dir * t
|
||||||
|
|
||||||
|
|
||||||
|
# ── Coordinate Conversions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
static func _godot_to_chunk_pos(g: Vector3) -> Vector3:
|
||||||
|
return Vector3(g.x, g.z, g.y)
|
||||||
|
|
||||||
|
static func _chunk_to_godot_pos(c: Vector3) -> Vector3:
|
||||||
|
return Vector3(c.x, c.z, c.y)
|
||||||
|
|
||||||
|
static func _godot_to_chunk_quat(q: Quaternion) -> Quaternion:
|
||||||
|
return Quaternion(q.x, q.z, q.y, q.w)
|
||||||
|
|
||||||
|
static func _chunk_to_godot_quat(q: Quaternion) -> Quaternion:
|
||||||
|
return Quaternion(q.x, q.z, q.y, q.w)
|
||||||
|
|
||||||
|
static func _godot_to_chunk_scale(s: Vector3) -> Vector3:
|
||||||
|
return Vector3(s.x, s.z, s.y)
|
||||||
|
|
||||||
|
static func _chunk_to_godot_scale(s: Vector3) -> Vector3:
|
||||||
|
return Vector3(s.x, s.z, s.y)
|
||||||
|
|
||||||
|
|
||||||
# ── Selection & Scene Visual Sync ─────────────────────────────────────
|
# ── Selection & Scene Visual Sync ─────────────────────────────────────
|
||||||
|
|
||||||
|
func _get_selected_id() -> String:
|
||||||
|
if core != null and core.selection != null:
|
||||||
|
return core.selection.get_selected()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func _on_selection_changed(obj_id: String) -> void:
|
func _on_selection_changed(obj_id: String) -> void:
|
||||||
if obj_id == "":
|
if obj_id == "":
|
||||||
_gizmo_3d.attach_to_node(null)
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.attach_to_node(null)
|
||||||
return
|
return
|
||||||
|
|
||||||
var obj_node = _world_container.get_node_or_null(obj_id)
|
var obj_node := _world_container.get_node_or_null(obj_id)
|
||||||
if obj_node is Node3D:
|
if obj_node is Node3D and _gizmo_3d != null:
|
||||||
|
if _gizmo_3d.current_mode == TransformGizmo3DClass.Mode.NONE:
|
||||||
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
_gizmo_3d.attach_to_node(obj_node)
|
_gizmo_3d.attach_to_node(obj_node)
|
||||||
_camera_3d.focus_on(obj_node.global_position)
|
|
||||||
|
|
||||||
|
func _on_inspector_property_changed(obj_id: String, prop_name: String, val: Variant) -> void:
|
||||||
|
var node := _world_container.get_node_or_null(obj_id) as Node3D
|
||||||
|
if node == null:
|
||||||
|
return
|
||||||
|
match prop_name:
|
||||||
|
"position":
|
||||||
|
node.position = val as Vector3
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.position = node.position
|
||||||
|
"rotation":
|
||||||
|
node.quaternion = val as Quaternion
|
||||||
|
if _gizmo_3d != null:
|
||||||
|
_gizmo_3d.quaternion = node.quaternion
|
||||||
|
"scale":
|
||||||
|
node.scale = val as Vector3
|
||||||
|
|
||||||
|
|
||||||
func _on_collection_model_selected(model_data: Dictionary) -> void:
|
func _on_collection_model_selected(model_data: Dictionary) -> void:
|
||||||
@@ -680,14 +1001,16 @@ func _on_collection_model_selected(model_data: Dictionary) -> void:
|
|||||||
if _camera_3d != null:
|
if _camera_3d != null:
|
||||||
spawn_pos = _camera_3d.focal_point
|
spawn_pos = _camera_3d.focal_point
|
||||||
spawn_pos.y = 0.0
|
spawn_pos.y = 0.0
|
||||||
var new_id := core.place_object(cat_id, spawn_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
var chunk_pos := _godot_to_chunk_pos(spawn_pos)
|
||||||
|
var new_id := core.place_object(cat_id, chunk_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
||||||
"display_name": model_data.get("name", cat_id),
|
"display_name": model_data.get("name", cat_id),
|
||||||
"catalog_id": cat_id,
|
"catalog_id": cat_id,
|
||||||
"asset_id": model_data.get("asset_id", "")
|
"asset_id": model_data.get("asset_id", "")
|
||||||
})
|
})
|
||||||
if new_id != "":
|
if new_id != "":
|
||||||
core.select_object(new_id)
|
|
||||||
_spawn_visual_node(new_id)
|
_spawn_visual_node(new_id)
|
||||||
|
core.select_object(new_id)
|
||||||
|
_on_selection_changed(new_id)
|
||||||
_hierarchy_dock.refresh_tree()
|
_hierarchy_dock.refresh_tree()
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
_status_bar_label.text = "Placed %s at (%.1f, 0, %.1f)" % [model_data.get("name", cat_id), spawn_pos.x, spawn_pos.z]
|
_status_bar_label.text = "Placed %s at (%.1f, 0, %.1f)" % [model_data.get("name", cat_id), spawn_pos.x, spawn_pos.z]
|
||||||
@@ -707,7 +1030,9 @@ func _spawn_visual_node(obj_id: String) -> void:
|
|||||||
var node = StaticObjectNodeClass.new()
|
var node = StaticObjectNodeClass.new()
|
||||||
node.name = obj.id
|
node.name = obj.id
|
||||||
node.world_object = wo
|
node.world_object = wo
|
||||||
node.position = Vector3(obj.position.x, obj.position.z, obj.position.y)
|
node.position = _chunk_to_godot_pos(obj.position)
|
||||||
|
node.quaternion = _chunk_to_godot_quat(Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w))
|
||||||
|
node.scale = _chunk_to_godot_scale(obj.scale)
|
||||||
_world_container.add_child(node)
|
_world_container.add_child(node)
|
||||||
|
|
||||||
|
|
||||||
@@ -736,9 +1061,10 @@ func load_template(template_name: String) -> void:
|
|||||||
obj.get("properties", {})
|
obj.get("properties", {})
|
||||||
)
|
)
|
||||||
_refresh_scene_visuals()
|
_refresh_scene_visuals()
|
||||||
# Select mascot or first object to showcase inspector & gizmo
|
|
||||||
if not core.model.payload.objects.is_empty():
|
if not core.model.payload.objects.is_empty():
|
||||||
core.select_object(core.model.payload.objects[0].id)
|
var first_id: String = core.model.payload.objects[0].id
|
||||||
|
core.select_object(first_id)
|
||||||
|
_on_selection_changed(first_id)
|
||||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
_status_bar_label.text = "Loaded template: %s | Press [F1] for Controls" % project_title
|
_status_bar_label.text = "Loaded template: %s | Press [F1] for Controls" % project_title
|
||||||
|
|
||||||
@@ -770,6 +1096,10 @@ func _update_simulation_ui(playing: bool) -> void:
|
|||||||
_btn_play.disabled = playing
|
_btn_play.disabled = playing
|
||||||
_btn_stop.disabled = not playing
|
_btn_stop.disabled = not playing
|
||||||
_status_bar_label.text = "Simulation: PLAYING (F5 to Stop)" if playing else "Simulation: STOPPED (Edit mode)"
|
_status_bar_label.text = "Simulation: PLAYING (F5 to Stop)" if playing else "Simulation: STOPPED (Edit mode)"
|
||||||
|
if not playing:
|
||||||
|
if _camera_3d != null:
|
||||||
|
_camera_3d.current = true
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
# ── Menu Bar Dispatch ────────────────────────────────────────────────
|
# ── Menu Bar Dispatch ────────────────────────────────────────────────
|
||||||
@@ -789,7 +1119,7 @@ func _on_edit_menu_pressed(id: int) -> void:
|
|||||||
0: core.undo(); _refresh_scene_visuals()
|
0: core.undo(); _refresh_scene_visuals()
|
||||||
1: core.redo(); _refresh_scene_visuals()
|
1: core.redo(); _refresh_scene_visuals()
|
||||||
2: _hierarchy_dock._on_duplicate_pressed()
|
2: _hierarchy_dock._on_duplicate_pressed()
|
||||||
3: _hierarchy_dock._on_delete_pressed()
|
3: _delete_selected_object(_get_selected_id())
|
||||||
|
|
||||||
|
|
||||||
func _on_view_menu_pressed(id: int) -> void:
|
func _on_view_menu_pressed(id: int) -> void:
|
||||||
@@ -799,9 +1129,9 @@ func _on_view_menu_pressed(id: int) -> void:
|
|||||||
2: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.FRONT)
|
2: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.FRONT)
|
||||||
3: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.SIDE)
|
3: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.SIDE)
|
||||||
4:
|
4:
|
||||||
var sel_id = core.selection.get_selected_id() if core.selection.has_method("get_selected_id") else ""
|
var sel_id := _get_selected_id()
|
||||||
if sel_id != "":
|
if sel_id != "":
|
||||||
var n = _world_container.get_node_or_null(sel_id)
|
var n := _world_container.get_node_or_null(sel_id)
|
||||||
if n is Node3D and _camera_3d != null:
|
if n is Node3D and _camera_3d != null:
|
||||||
_camera_3d.focus_on(n.global_position)
|
_camera_3d.focus_on(n.global_position)
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,18 @@ func _spawn_test_avatar() -> void:
|
|||||||
_spawned_avatar = p
|
_spawned_avatar = p
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(_delta: float) -> void:
|
||||||
|
if state == SimulationState.PLAYING and _spawned_avatar != null and is_instance_valid(_spawned_avatar):
|
||||||
|
if _spawned_avatar is Node3D and (_spawned_avatar as Node3D).global_position.y < -30.0:
|
||||||
|
(_spawned_avatar as Node3D).global_position = Vector3(0, 1.5, 0)
|
||||||
|
if _spawned_avatar is CharacterBody3D:
|
||||||
|
(_spawned_avatar as CharacterBody3D).velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
func is_playing() -> bool:
|
func is_playing() -> bool:
|
||||||
return state == SimulationState.PLAYING
|
return state == SimulationState.PLAYING
|
||||||
|
|
||||||
|
|
||||||
func is_editing() -> bool:
|
func is_editing() -> bool:
|
||||||
return state == SimulationState.EDIT
|
return state == SimulationState.EDIT
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user