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:
+362
-32
@@ -56,6 +56,18 @@ var _world_container: Node3D
|
||||
var _gizmo_3d: Node3D
|
||||
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
|
||||
var _hierarchy_dock = null
|
||||
var _inspector_dock = null
|
||||
@@ -140,6 +152,7 @@ func _build_studio_layout() -> void:
|
||||
left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_hierarchy_dock = StudioHierarchyDockClass.new()
|
||||
_hierarchy_dock.setup(core)
|
||||
_hierarchy_dock.delete_requested.connect(_delete_selected_object)
|
||||
left_panel.add_child(_hierarchy_dock)
|
||||
workspace_hbox.add_child(left_panel)
|
||||
|
||||
@@ -168,6 +181,8 @@ func _build_studio_layout() -> void:
|
||||
|
||||
_inspector_dock = StudioInspectorDockClass.new()
|
||||
_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.set_tab_title(0, "Inspector")
|
||||
|
||||
@@ -306,7 +321,7 @@ func _build_toolbar(parent: Control) -> void:
|
||||
_toolbar.add_child(snap_lbl)
|
||||
|
||||
_snap_spin = SpinBox.new()
|
||||
_snap_spin.min_value = 0.1
|
||||
_snap_spin.min_value = 0.5
|
||||
_snap_spin.max_value = 10.0
|
||||
_snap_spin.step = 0.5
|
||||
_snap_spin.value = 1.0
|
||||
@@ -393,13 +408,15 @@ func _quick_insert(cat_id: String, display_name: String) -> void:
|
||||
if _camera_3d != null:
|
||||
spawn_pos = _camera_3d.focal_point
|
||||
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,
|
||||
"catalog_id": cat_id
|
||||
})
|
||||
if new_id != "":
|
||||
core.select_object(new_id)
|
||||
_spawn_visual_node(new_id)
|
||||
core.select_object(new_id)
|
||||
_on_selection_changed(new_id)
|
||||
_hierarchy_dock.refresh_tree()
|
||||
_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]
|
||||
@@ -425,13 +442,26 @@ func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
||||
# Studio Baseplate Ground Mesh
|
||||
_baseplate_mesh = MeshInstance3D.new()
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(64, 64)
|
||||
plane.size = Vector2(128, 128)
|
||||
var gmat := StandardMaterial3D.new()
|
||||
gmat.albedo_color = Color(0.20, 0.23, 0.27)
|
||||
gmat.roughness = 0.85
|
||||
_baseplate_mesh.mesh = plane
|
||||
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
|
||||
var grid := _create_3d_grid(32, 1.0)
|
||||
vp.add_child(grid)
|
||||
@@ -470,7 +500,7 @@ func _setup_viewport_hud_overlay(parent: Control) -> void:
|
||||
hud_bar.position = Vector2(16, -42)
|
||||
|
||||
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_color_override("font_color", Color(0.9, 0.92, 0.96))
|
||||
hud_bar.add_child(hud_text)
|
||||
@@ -497,6 +527,8 @@ func _create_3d_grid(size: int, step: float) -> Node3D:
|
||||
return mi
|
||||
|
||||
|
||||
# ── Keyboard Shortcuts & Nudge ────────────────────────────────────────
|
||||
|
||||
func _setup_shortcuts() -> void:
|
||||
set_process_unhandled_key_input(true)
|
||||
|
||||
@@ -524,6 +556,26 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
_simulation_mgr.stop_simulation()
|
||||
else:
|
||||
_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():
|
||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.NONE)
|
||||
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():
|
||||
_set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE)
|
||||
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 != "":
|
||||
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:
|
||||
_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:
|
||||
if _gizmo_3d != null:
|
||||
_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")
|
||||
|
||||
|
||||
# ── Interactive 3D Viewport Input & Raycast Picking ─────────────────────
|
||||
# ── Interactive 3D Viewport Input & Picking ───────────────────────────
|
||||
|
||||
func handle_viewport_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
||||
var hit_obj := _raycast_find_object(mb.position)
|
||||
if hit_obj != null:
|
||||
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
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
_handle_left_press(mb)
|
||||
else:
|
||||
core.select_object("")
|
||||
_status_bar_label.text = "Deselected | Click an object or [+ Add] to build"
|
||||
_handle_left_release(mb)
|
||||
|
||||
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:
|
||||
@@ -591,21 +863,24 @@ func handle_viewport_drop(at_pos: Vector2, data: Variant) -> void:
|
||||
var new_id: String = ""
|
||||
|
||||
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),
|
||||
"catalog_id": cat_id,
|
||||
"asset_id": data.get("asset_id", "")
|
||||
})
|
||||
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),
|
||||
"catalog_id": cat_id,
|
||||
"asset_id": data.get("asset_id", "")
|
||||
})
|
||||
|
||||
if new_id != "":
|
||||
core.select_object(new_id)
|
||||
_spawn_visual_node(new_id)
|
||||
core.select_object(new_id)
|
||||
_on_selection_changed(new_id)
|
||||
_hierarchy_dock.refresh_tree()
|
||||
_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]
|
||||
@@ -661,17 +936,63 @@ func _raycast_ground_plane(screen_pos: Vector2) -> Vector3:
|
||||
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 ─────────────────────────────────────
|
||||
|
||||
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:
|
||||
if obj_id == "":
|
||||
_gizmo_3d.attach_to_node(null)
|
||||
if _gizmo_3d != null:
|
||||
_gizmo_3d.attach_to_node(null)
|
||||
return
|
||||
|
||||
var obj_node = _world_container.get_node_or_null(obj_id)
|
||||
if obj_node is Node3D:
|
||||
var obj_node := _world_container.get_node_or_null(obj_id)
|
||||
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)
|
||||
_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:
|
||||
@@ -680,14 +1001,16 @@ func _on_collection_model_selected(model_data: Dictionary) -> void:
|
||||
if _camera_3d != null:
|
||||
spawn_pos = _camera_3d.focal_point
|
||||
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),
|
||||
"catalog_id": cat_id,
|
||||
"asset_id": model_data.get("asset_id", "")
|
||||
})
|
||||
if new_id != "":
|
||||
core.select_object(new_id)
|
||||
_spawn_visual_node(new_id)
|
||||
core.select_object(new_id)
|
||||
_on_selection_changed(new_id)
|
||||
_hierarchy_dock.refresh_tree()
|
||||
_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]
|
||||
@@ -707,7 +1030,9 @@ func _spawn_visual_node(obj_id: String) -> void:
|
||||
var node = StaticObjectNodeClass.new()
|
||||
node.name = obj.id
|
||||
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)
|
||||
|
||||
|
||||
@@ -736,9 +1061,10 @@ func load_template(template_name: String) -> void:
|
||||
obj.get("properties", {})
|
||||
)
|
||||
_refresh_scene_visuals()
|
||||
# Select mascot or first object to showcase inspector & gizmo
|
||||
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)
|
||||
_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_stop.disabled = not playing
|
||||
_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 ────────────────────────────────────────────────
|
||||
@@ -789,7 +1119,7 @@ func _on_edit_menu_pressed(id: int) -> void:
|
||||
0: core.undo(); _refresh_scene_visuals()
|
||||
1: core.redo(); _refresh_scene_visuals()
|
||||
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:
|
||||
@@ -799,9 +1129,9 @@ func _on_view_menu_pressed(id: int) -> void:
|
||||
2: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.FRONT)
|
||||
3: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.SIDE)
|
||||
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 != "":
|
||||
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:
|
||||
_camera_3d.focus_on(n.global_position)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user