feat(studio): Add Quick Start Guide, Viewport HUD, 3D raycast picking, quick add toolbar, and starter Baseplate
checkpoint / checkpoint (push) Successful in 6m20s
checkpoint / checkpoint (push) Successful in 6m20s
This commit is contained in:
@@ -90,7 +90,8 @@ func place_object(catalog_id: String, position: Vector3,
|
|||||||
|
|
||||||
func place_object_stacked(catalog_id: String, target_object_id: String = "",
|
func place_object_stacked(catalog_id: String, target_object_id: String = "",
|
||||||
hit_point: Vector3 = Vector3.ZERO, surface_normal: Vector3 = Vector3(0, 0, 1),
|
hit_point: Vector3 = Vector3.ZERO, surface_normal: Vector3 = Vector3(0, 0, 1),
|
||||||
elevation_offset: float = 0.0, align_to_normal: bool = false) -> String:
|
elevation_offset: float = 0.0, align_to_normal: bool = false,
|
||||||
|
extra_properties: Dictionary = {}) -> String:
|
||||||
var target_obj: ChunkDataDecoder.WorldObjectData = null
|
var target_obj: ChunkDataDecoder.WorldObjectData = null
|
||||||
if target_object_id != "":
|
if target_object_id != "":
|
||||||
target_obj = model.find_object(target_object_id)
|
target_obj = model.find_object(target_object_id)
|
||||||
@@ -103,6 +104,7 @@ func place_object_stacked(catalog_id: String, target_object_id: String = "",
|
|||||||
props["stacked_on"] = stack_info["parent_id"]
|
props["stacked_on"] = stack_info["parent_id"]
|
||||||
if elevation_offset != 0.0:
|
if elevation_offset != 0.0:
|
||||||
props["elevation_offset"] = elevation_offset
|
props["elevation_offset"] = elevation_offset
|
||||||
|
props.merge(extra_properties)
|
||||||
|
|
||||||
return place_object(catalog_id, stack_info["position"], stack_info["rotation"], Vector3.ONE, props)
|
return place_object(catalog_id, stack_info["position"], stack_info["rotation"], Vector3.ONE, props)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ func resolve(id: String) -> Dictionary:
|
|||||||
return _catalog[id]
|
return _catalog[id]
|
||||||
if (id.contains("bizon") or id.contains("bison")) and _catalog.has("bison"):
|
if (id.contains("bizon") or id.contains("bison")) and _catalog.has("bison"):
|
||||||
return _catalog["bison"]
|
return _catalog["bison"]
|
||||||
|
if (id == "tree_pine" or id.begins_with("tree")) and _catalog.has("tree_pine_01"):
|
||||||
|
return _catalog["tree_pine_01"]
|
||||||
|
if (id == "lamp_post" or id.begins_with("lamp")) and _catalog.has("lamp_post_01"):
|
||||||
|
return _catalog["lamp_post_01"]
|
||||||
|
if (id == "chest" or id.begins_with("chest")) and _catalog.has("chest_01"):
|
||||||
|
return _catalog["chest_01"]
|
||||||
|
if (id == "rock" or id.begins_with("rock")) and _catalog.has("rock_01"):
|
||||||
|
return _catalog["rock_01"]
|
||||||
|
if (id == "spawn_pad" or id.begins_with("spawn")) and _catalog.has("spawn_point"):
|
||||||
|
return _catalog["spawn_point"]
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,48 @@ static func create_template(template_name: String) -> Dictionary:
|
|||||||
base["objects"] = [
|
base["objects"] = [
|
||||||
{
|
{
|
||||||
"id": "spawn_pad",
|
"id": "spawn_pad",
|
||||||
"catalog_id": "spawn_pad",
|
"catalog_id": "crate_small",
|
||||||
"asset_id": "props/crate_small.glb",
|
"asset_id": "props/crate_small.glb",
|
||||||
"position": [0, 0, 0],
|
"position": [0, 0, 0],
|
||||||
"rotation": [0, 0, 0, 1],
|
"rotation": [0, 0, 0, 1],
|
||||||
"scale": [2, 2, 0.2],
|
"scale": [2, 2, 0.2],
|
||||||
"properties": {"display_name": "Spawn Pad"}
|
"properties": {"display_name": "Spawn Pad", "catalog_id": "crate_small"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "modbison_mascot",
|
||||||
|
"catalog_id": "bison",
|
||||||
|
"asset_id": "props/bison.glb",
|
||||||
|
"position": [0, 4, 0],
|
||||||
|
"rotation": [0, 0, 0, 1],
|
||||||
|
"scale": [1, 1, 1],
|
||||||
|
"properties": {"display_name": "ModBison", "catalog_id": "bison"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crate_base",
|
||||||
|
"catalog_id": "crate_small",
|
||||||
|
"asset_id": "props/crate_small.glb",
|
||||||
|
"position": [4, 0, 0],
|
||||||
|
"rotation": [0, 0, 0, 1],
|
||||||
|
"scale": [1, 1, 1],
|
||||||
|
"properties": {"display_name": "Wooden Crate", "catalog_id": "crate_small"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crate_stacked",
|
||||||
|
"catalog_id": "crate_small",
|
||||||
|
"asset_id": "props/crate_small.glb",
|
||||||
|
"position": [4, 0, 1],
|
||||||
|
"rotation": [0, 0, 0, 1],
|
||||||
|
"scale": [1, 1, 1],
|
||||||
|
"properties": {"display_name": "Stacked Crate", "catalog_id": "crate_small", "parent_id": "crate_base"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lamp_01",
|
||||||
|
"catalog_id": "lamp_post_01",
|
||||||
|
"asset_id": "props/lamp_post_01.glb",
|
||||||
|
"position": [-3, 3, 0],
|
||||||
|
"rotation": [0, 0, 0, 1],
|
||||||
|
"scale": [1, 1, 1],
|
||||||
|
"properties": {"display_name": "Street Lamp", "catalog_id": "lamp_post_01"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"terrain":
|
"terrain":
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
class_name StudioHelpDialog
|
||||||
|
extends PanelContainer
|
||||||
|
## Interactive Welcome, Quick Start Guide & Controls Cheatsheet for ModBison Studio.
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
const SETTINGS_PATH := "user://studio_settings.json"
|
||||||
|
|
||||||
|
var _check_show_on_start: CheckBox
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
name = "StudioHelpDialog"
|
||||||
|
custom_minimum_size = Vector2(720, 520)
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
static func should_show_on_startup() -> bool:
|
||||||
|
if not FileAccess.file_exists(SETTINGS_PATH):
|
||||||
|
return true
|
||||||
|
var fa := FileAccess.open(SETTINGS_PATH, FileAccess.READ)
|
||||||
|
if fa == null:
|
||||||
|
return true
|
||||||
|
var parsed = JSON.parse_string(fa.get_as_text())
|
||||||
|
fa.close()
|
||||||
|
if parsed is Dictionary and parsed.has("show_help_on_startup"):
|
||||||
|
return bool(parsed["show_help_on_startup"])
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func save_show_on_startup(show_help: bool) -> void:
|
||||||
|
var data := {"show_help_on_startup": show_help}
|
||||||
|
var fa := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||||
|
if fa != null:
|
||||||
|
fa.store_string(JSON.stringify(data))
|
||||||
|
fa.close()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
# Style panel
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.14, 0.16, 0.20, 0.98)
|
||||||
|
style.border_color = Color(0.9, 0.75, 0.25)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 24
|
||||||
|
style.content_margin_top = 20
|
||||||
|
style.content_margin_right = 24
|
||||||
|
style.content_margin_bottom = 20
|
||||||
|
add_theme_stylebox_override("panel", style)
|
||||||
|
|
||||||
|
var root_vbox := VBoxContainer.new()
|
||||||
|
root_vbox.add_theme_constant_override("separation", 14)
|
||||||
|
add_child(root_vbox)
|
||||||
|
|
||||||
|
# Header Row
|
||||||
|
var header_hbox := HBoxContainer.new()
|
||||||
|
var title_lbl := Label.new()
|
||||||
|
title_lbl.text = "🦬 Welcome to ModBison Studio!"
|
||||||
|
title_lbl.add_theme_font_size_override("font_size", 20)
|
||||||
|
title_lbl.add_theme_color_override("font_color", Color(1.0, 0.88, 0.35))
|
||||||
|
header_hbox.add_child(title_lbl)
|
||||||
|
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header_hbox.add_child(spacer)
|
||||||
|
|
||||||
|
var close_x := Button.new()
|
||||||
|
close_x.text = "✕"
|
||||||
|
close_x.custom_minimum_size = Vector2(32, 28)
|
||||||
|
close_x.pressed.connect(_on_close_pressed)
|
||||||
|
header_hbox.add_child(close_x)
|
||||||
|
root_vbox.add_child(header_hbox)
|
||||||
|
|
||||||
|
var subtitle := Label.new()
|
||||||
|
subtitle.text = "Your creator workbench for designing 3D worlds, placing interactive props, and playtesting in real-time."
|
||||||
|
subtitle.add_theme_font_size_override("font_size", 12)
|
||||||
|
subtitle.add_theme_color_override("font_color", Color(0.75, 0.78, 0.82))
|
||||||
|
root_vbox.add_child(subtitle)
|
||||||
|
|
||||||
|
# Tab Container for Guide & Shortcuts
|
||||||
|
var tabs := TabContainer.new()
|
||||||
|
tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
# Tab 1: Quick Start Guide
|
||||||
|
var guide_scroll := ScrollContainer.new()
|
||||||
|
guide_scroll.name = "Quick Start Guide"
|
||||||
|
guide_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
var guide_content := VBoxContainer.new()
|
||||||
|
guide_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
guide_content.add_theme_constant_override("separation", 14)
|
||||||
|
|
||||||
|
_add_guide_step(guide_content, "🎥 1. Navigate the 3D World",
|
||||||
|
"• Fly Camera: Hold Right Mouse Button (RMB) + W, A, S, D, Q, E\n" +
|
||||||
|
"• Orbit Camera: Hold Middle Mouse Button (MMB) and move mouse\n" +
|
||||||
|
"• Zoom: Scroll mouse wheel up/down\n" +
|
||||||
|
"• Focus View: Press [F] to instantly center and zoom onto the selected object")
|
||||||
|
|
||||||
|
_add_guide_step(guide_content, "🖱️ 2. Select & Transform Props",
|
||||||
|
"• Click any 3D model directly in the viewport (or in the Scene Hierarchy on the left)\n" +
|
||||||
|
"• Switch transformation tools via top Toolbar or hotkeys:\n" +
|
||||||
|
" [1] ↖ Select Mode [2] ✛ Move (drag arrows) [3] ↻ Rotate [4] ⤢ Scale\n" +
|
||||||
|
"• Snapping: Set grid snap step (0.5m, 1.0m, etc.) from the Toolbar snap box\n" +
|
||||||
|
"• Duplicate: Press [Ctrl + D] to duplicate, [Delete] to remove")
|
||||||
|
|
||||||
|
_add_guide_step(guide_content, "📦 3. Add Models & Stacking",
|
||||||
|
"• Quick Insert: Click [+ Bison], [+ Crate], [+ Tree], or [+ Lamp] on the top toolbar\n" +
|
||||||
|
"• Collections Dock: Open the Collections tab on the right to browse theme packs and drag & drop models directly onto the ground!\n" +
|
||||||
|
"• Object Stacking: Place items directly on top of other items — Studio automatically snaps surfaces so they stick together!")
|
||||||
|
|
||||||
|
_add_guide_step(guide_content, "▶️ 4. Playtest Your World (F5)",
|
||||||
|
"• Hit [F5] or click the green '▶ Play' button on the toolbar\n" +
|
||||||
|
"• Your player avatar spawns immediately with full walking, jumping, and riding physics!\n" +
|
||||||
|
"• Hit [F5] again or click '⏹ Stop' to cleanly return to Edit mode (all objects roll back to their original positions)")
|
||||||
|
|
||||||
|
_add_guide_step(guide_content, "💾 5. Save & Return to Game",
|
||||||
|
"• Save: Press [Ctrl + S] or File -> Save Project\n" +
|
||||||
|
"• Return: Click '🏠 Main Menu' in the top-right corner to return to the game menu anytime")
|
||||||
|
|
||||||
|
guide_scroll.add_child(guide_content)
|
||||||
|
tabs.add_child(guide_scroll)
|
||||||
|
|
||||||
|
# Tab 2: Keyboard & Mouse Reference
|
||||||
|
var keys_scroll := ScrollContainer.new()
|
||||||
|
keys_scroll.name = "Shortcuts & Controls"
|
||||||
|
keys_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
var keys_vbox := VBoxContainer.new()
|
||||||
|
keys_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
keys_vbox.add_theme_constant_override("separation", 10)
|
||||||
|
|
||||||
|
_add_shortcut_row(keys_vbox, "RMB + WASDQE", "Fly camera through scene (Hold Shift to fly faster, Ctrl for precision)")
|
||||||
|
_add_shortcut_row(keys_vbox, "MMB + Drag", "Pan camera across scene")
|
||||||
|
_add_shortcut_row(keys_vbox, "Mouse Wheel", "Zoom camera in / out")
|
||||||
|
_add_shortcut_row(keys_vbox, "LMB (Left Click)", "Select object in 3D viewport or click gizmo handles")
|
||||||
|
_add_shortcut_row(keys_vbox, "F Key", "Focus camera on selected object")
|
||||||
|
_add_shortcut_row(keys_vbox, "1, 2, 3, 4 Keys", "Switch tool: [1] Select, [2] Move, [3] Rotate, [4] Scale")
|
||||||
|
_add_shortcut_row(keys_vbox, "7, 1, 3 Keys", "View Presets: [7] Top Ortho, [1] Front Ortho, [3] Side Ortho")
|
||||||
|
_add_shortcut_row(keys_vbox, "5 Key", "Toggle Perspective / Orthographic projection")
|
||||||
|
_add_shortcut_row(keys_vbox, "F5 Key", "Toggle Playtest (Play Solo / Stop Playtest)")
|
||||||
|
_add_shortcut_row(keys_vbox, "Ctrl + Z / Ctrl + Y", "Undo / Redo world modifications")
|
||||||
|
_add_shortcut_row(keys_vbox, "Ctrl + D", "Duplicate selected object")
|
||||||
|
_add_shortcut_row(keys_vbox, "Delete / Backspace", "Delete selected object")
|
||||||
|
_add_shortcut_row(keys_vbox, "Ctrl + S", "Save project to disk (.mbworld)")
|
||||||
|
_add_shortcut_row(keys_vbox, "F1 Key", "Toggle this Help & Controls guide")
|
||||||
|
|
||||||
|
keys_scroll.add_child(keys_vbox)
|
||||||
|
tabs.add_child(keys_scroll)
|
||||||
|
|
||||||
|
root_vbox.add_child(tabs)
|
||||||
|
|
||||||
|
# Footer Row
|
||||||
|
var footer_hbox := HBoxContainer.new()
|
||||||
|
_check_show_on_start = CheckBox.new()
|
||||||
|
_check_show_on_start.text = "Show this guide when opening Studio"
|
||||||
|
_check_show_on_start.button_pressed = should_show_on_startup()
|
||||||
|
_check_show_on_start.toggled.connect(func(val): save_show_on_startup(val))
|
||||||
|
footer_hbox.add_child(_check_show_on_start)
|
||||||
|
|
||||||
|
var f_spacer := Control.new()
|
||||||
|
f_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
footer_hbox.add_child(f_spacer)
|
||||||
|
|
||||||
|
var btn_close := Button.new()
|
||||||
|
btn_close.text = "✨ Let's Build! (Close)"
|
||||||
|
btn_close.add_theme_color_override("font_color", Color(1.0, 0.9, 0.35))
|
||||||
|
btn_close.custom_minimum_size = Vector2(160, 34)
|
||||||
|
btn_close.pressed.connect(_on_close_pressed)
|
||||||
|
footer_hbox.add_child(btn_close)
|
||||||
|
|
||||||
|
root_vbox.add_child(footer_hbox)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_guide_step(parent: VBoxContainer, title: String, text: String) -> void:
|
||||||
|
var box := VBoxContainer.new()
|
||||||
|
var t := Label.new()
|
||||||
|
t.text = title
|
||||||
|
t.add_theme_font_size_override("font_size", 13)
|
||||||
|
t.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
||||||
|
box.add_child(t)
|
||||||
|
|
||||||
|
var b := Label.new()
|
||||||
|
b.text = text
|
||||||
|
b.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
b.add_theme_font_size_override("font_size", 11)
|
||||||
|
b.add_theme_color_override("font_color", Color(0.85, 0.87, 0.9))
|
||||||
|
box.add_child(b)
|
||||||
|
parent.add_child(box)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_shortcut_row(parent: VBoxContainer, key: String, desc: String) -> void:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
var k_lbl := Label.new()
|
||||||
|
k_lbl.text = key
|
||||||
|
k_lbl.custom_minimum_size = Vector2(180, 0)
|
||||||
|
k_lbl.add_theme_font_size_override("font_size", 11)
|
||||||
|
k_lbl.add_theme_color_override("font_color", Color(1.0, 0.85, 0.4))
|
||||||
|
row.add_child(k_lbl)
|
||||||
|
|
||||||
|
var d_lbl := Label.new()
|
||||||
|
d_lbl.text = desc
|
||||||
|
d_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
d_lbl.add_theme_font_size_override("font_size", 11)
|
||||||
|
d_lbl.add_theme_color_override("font_color", Color(0.82, 0.85, 0.88))
|
||||||
|
row.add_child(d_lbl)
|
||||||
|
parent.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_pressed() -> void:
|
||||||
|
visible = false
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
if event is InputEventKey:
|
||||||
|
var k := event as InputEventKey
|
||||||
|
if k.pressed and not k.echo:
|
||||||
|
if k.keycode == KEY_ESCAPE or k.keycode == KEY_F1:
|
||||||
|
_on_close_pressed()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
@@ -138,7 +138,7 @@ func _on_core_selection_changed(obj_id: String) -> void:
|
|||||||
_tree.deselect_all()
|
_tree.deselect_all()
|
||||||
if obj_id != "" and _items_by_id.has(obj_id):
|
if obj_id != "" and _items_by_id.has(obj_id):
|
||||||
_items_by_id[obj_id].select(0)
|
_items_by_id[obj_id].select(0)
|
||||||
_items_by_id[obj_id].scroll_to_visible()
|
_tree.scroll_to_item(_items_by_id[obj_id])
|
||||||
_updating = false
|
_updating = false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ func _build_ui() -> void:
|
|||||||
add_child(_title_label)
|
add_child(_title_label)
|
||||||
|
|
||||||
_empty_label = Label.new()
|
_empty_label = Label.new()
|
||||||
_empty_label.text = "(No selection)"
|
_empty_label.text = "\n↖ No Object Selected\n\nClick any 3D model in the viewport or Scene Hierarchy to inspect its transform, animations, and movement.\n\n💡 Tip:\nUse '+ Add' on the top toolbar or open the 'Collections' tab to place props!"
|
||||||
|
_empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
_empty_label.add_theme_font_size_override("font_size", 11)
|
_empty_label.add_theme_font_size_override("font_size", 11)
|
||||||
_empty_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
_empty_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.75))
|
||||||
add_child(_empty_label)
|
add_child(_empty_label)
|
||||||
|
|
||||||
_scroll = ScrollContainer.new()
|
_scroll = ScrollContainer.new()
|
||||||
|
|||||||
+357
-41
@@ -2,8 +2,8 @@ class_name StudioMain
|
|||||||
extends Control
|
extends Control
|
||||||
## Standalone ModBison Studio Application Shell.
|
## Standalone ModBison Studio Application Shell.
|
||||||
## Implements multi-panel docking layout, top menu bar with keyboard shortcuts,
|
## Implements multi-panel docking layout, top menu bar with keyboard shortcuts,
|
||||||
## 3D interactive viewport with gizmos, hierarchy, inspector, collections dock,
|
## 3D interactive viewport with raycast picking, gizmos, hierarchy, inspector,
|
||||||
## and simulation harness.
|
## collections dock, quick insert toolbar, instructions HUD, and simulation harness.
|
||||||
|
|
||||||
const MBWorldProjectIOClass = preload("res://scripts/studio/mbworld_project_io.gd")
|
const MBWorldProjectIOClass = preload("res://scripts/studio/mbworld_project_io.gd")
|
||||||
const StudioViewportCameraClass = preload("res://scripts/studio/studio_viewport_camera.gd")
|
const StudioViewportCameraClass = preload("res://scripts/studio/studio_viewport_camera.gd")
|
||||||
@@ -12,9 +12,26 @@ const StudioHierarchyDockClass = preload("res://scripts/studio/studio_hierarchy_
|
|||||||
const StudioInspectorDockClass = preload("res://scripts/studio/studio_inspector_dock.gd")
|
const StudioInspectorDockClass = preload("res://scripts/studio/studio_inspector_dock.gd")
|
||||||
const StudioCollectionsDockClass = preload("res://scripts/studio/studio_collections_dock.gd")
|
const StudioCollectionsDockClass = preload("res://scripts/studio/studio_collections_dock.gd")
|
||||||
const StudioSimulationManagerClass = preload("res://scripts/studio/studio_simulation_manager.gd")
|
const StudioSimulationManagerClass = preload("res://scripts/studio/studio_simulation_manager.gd")
|
||||||
|
const StudioHelpDialogClass = preload("res://scripts/studio/studio_help_dialog.gd")
|
||||||
const StaticObjectNodeClass = preload("res://scripts/static_object_node.gd")
|
const StaticObjectNodeClass = preload("res://scripts/static_object_node.gd")
|
||||||
const WorldObjectClass = preload("res://scripts/world_object.gd")
|
const WorldObjectClass = preload("res://scripts/world_object.gd")
|
||||||
|
|
||||||
|
# Custom SubViewportContainer supporting drag & drop model placement and click picking
|
||||||
|
class StudioViewportContainer extends SubViewportContainer:
|
||||||
|
var studio_main: Node = null
|
||||||
|
|
||||||
|
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
|
return data is Dictionary and data.get("type") == "model_placement"
|
||||||
|
|
||||||
|
func _drop_data(at_position: Vector2, data: Variant) -> void:
|
||||||
|
if studio_main != null and studio_main.has_method("handle_viewport_drop"):
|
||||||
|
studio_main.handle_viewport_drop(at_position, data)
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if studio_main != null and studio_main.has_method("handle_viewport_input"):
|
||||||
|
studio_main.handle_viewport_input(event)
|
||||||
|
|
||||||
|
|
||||||
# Core state
|
# Core state
|
||||||
var core: EditorCore = null
|
var core: EditorCore = null
|
||||||
var current_project_path: String = ""
|
var current_project_path: String = ""
|
||||||
@@ -32,16 +49,19 @@ var _btn_stop: Button
|
|||||||
var _snap_spin: SpinBox
|
var _snap_spin: SpinBox
|
||||||
|
|
||||||
# Main Viewport & 3D Elements
|
# Main Viewport & 3D Elements
|
||||||
|
var _vp_container: StudioViewportContainer
|
||||||
var _sub_viewport: SubViewport
|
var _sub_viewport: SubViewport
|
||||||
var _camera_3d: Camera3D
|
var _camera_3d: Camera3D
|
||||||
var _world_container: Node3D
|
var _world_container: Node3D
|
||||||
var _gizmo_3d: Node3D
|
var _gizmo_3d: Node3D
|
||||||
|
var _baseplate_mesh: MeshInstance3D
|
||||||
|
|
||||||
# Docks
|
# Docks & Dialogs
|
||||||
var _hierarchy_dock = null
|
var _hierarchy_dock = null
|
||||||
var _inspector_dock = null
|
var _inspector_dock = null
|
||||||
var _collections_dock = null
|
var _collections_dock = null
|
||||||
var _simulation_mgr = null
|
var _simulation_mgr = null
|
||||||
|
var _help_dialog = null
|
||||||
var _status_bar_label: Label
|
var _status_bar_label: Label
|
||||||
var _is_built: bool = false
|
var _is_built: bool = false
|
||||||
|
|
||||||
@@ -60,6 +80,15 @@ func _ready() -> void:
|
|||||||
_setup_shortcuts()
|
_setup_shortcuts()
|
||||||
_parse_cmdline_arguments()
|
_parse_cmdline_arguments()
|
||||||
|
|
||||||
|
# If the world has no objects yet, load the Baseplate starter template automatically
|
||||||
|
if core != null and core.model != null and core.model.payload.objects.is_empty():
|
||||||
|
load_template("baseplate")
|
||||||
|
|
||||||
|
# Show Help & Quick Start Guide on first launch if enabled in settings
|
||||||
|
if _help_dialog != null and StudioHelpDialogClass.should_show_on_startup():
|
||||||
|
_help_dialog.visible = true
|
||||||
|
_help_dialog.move_to_front()
|
||||||
|
|
||||||
|
|
||||||
func _init_core_model() -> void:
|
func _init_core_model() -> void:
|
||||||
var payload := ChunkDataDecoder.ChunkPayload.new()
|
var payload := ChunkDataDecoder.ChunkPayload.new()
|
||||||
@@ -87,7 +116,7 @@ func _build_studio_layout() -> void:
|
|||||||
|
|
||||||
var bg := ColorRect.new()
|
var bg := ColorRect.new()
|
||||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
bg.color = Color(0.14, 0.15, 0.18)
|
bg.color = Color(0.13, 0.14, 0.17)
|
||||||
add_child(bg)
|
add_child(bg)
|
||||||
|
|
||||||
var main_vbox := VBoxContainer.new()
|
var main_vbox := VBoxContainer.new()
|
||||||
@@ -97,7 +126,7 @@ func _build_studio_layout() -> void:
|
|||||||
# 1. Top Menu Bar
|
# 1. Top Menu Bar
|
||||||
_build_top_menu_bar(main_vbox)
|
_build_top_menu_bar(main_vbox)
|
||||||
|
|
||||||
# 2. Main Action Toolbar
|
# 2. Main Action Toolbar (Tools, Snap, Quick Insert, Simulation, Help)
|
||||||
_build_toolbar(main_vbox)
|
_build_toolbar(main_vbox)
|
||||||
|
|
||||||
# 3. Center Workspace (Horizontal Split: Left Dock | Center Viewport | Right Docks)
|
# 3. Center Workspace (Horizontal Split: Left Dock | Center Viewport | Right Docks)
|
||||||
@@ -105,7 +134,7 @@ func _build_studio_layout() -> void:
|
|||||||
workspace_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
workspace_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
main_vbox.add_child(workspace_hbox)
|
main_vbox.add_child(workspace_hbox)
|
||||||
|
|
||||||
# Left Dock: Hierarchy
|
# Left Dock: Scene Hierarchy
|
||||||
var left_panel := PanelContainer.new()
|
var left_panel := PanelContainer.new()
|
||||||
left_panel.custom_minimum_size = Vector2(240, 0)
|
left_panel.custom_minimum_size = Vector2(240, 0)
|
||||||
left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
@@ -114,22 +143,24 @@ func _build_studio_layout() -> void:
|
|||||||
left_panel.add_child(_hierarchy_dock)
|
left_panel.add_child(_hierarchy_dock)
|
||||||
workspace_hbox.add_child(left_panel)
|
workspace_hbox.add_child(left_panel)
|
||||||
|
|
||||||
# Center: 3D Viewport
|
# Center: 3D Viewport with interactive picking & drop support
|
||||||
var vp_container := SubViewportContainer.new()
|
_vp_container = StudioViewportContainer.new()
|
||||||
vp_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
_vp_container.studio_main = self
|
||||||
vp_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_vp_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
vp_container.stretch = true
|
_vp_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
workspace_hbox.add_child(vp_container)
|
_vp_container.stretch = true
|
||||||
|
workspace_hbox.add_child(_vp_container)
|
||||||
|
|
||||||
_sub_viewport = SubViewport.new()
|
_sub_viewport = SubViewport.new()
|
||||||
_sub_viewport.handle_input_locally = true
|
_sub_viewport.handle_input_locally = true
|
||||||
vp_container.add_child(_sub_viewport)
|
_vp_container.add_child(_sub_viewport)
|
||||||
|
|
||||||
_setup_3d_viewport_scene(_sub_viewport)
|
_setup_3d_viewport_scene(_sub_viewport)
|
||||||
|
_setup_viewport_hud_overlay(_vp_container)
|
||||||
|
|
||||||
# Right Dock: TabContainer with Inspector & Collections
|
# Right Dock: TabContainer with Inspector & Collections
|
||||||
var right_panel := PanelContainer.new()
|
var right_panel := PanelContainer.new()
|
||||||
right_panel.custom_minimum_size = Vector2(280, 0)
|
right_panel.custom_minimum_size = Vector2(300, 0)
|
||||||
right_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
right_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
var tabs := TabContainer.new()
|
var tabs := TabContainer.new()
|
||||||
tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
@@ -152,7 +183,7 @@ func _build_studio_layout() -> void:
|
|||||||
var status_panel := PanelContainer.new()
|
var status_panel := PanelContainer.new()
|
||||||
status_panel.custom_minimum_size = Vector2(0, 24)
|
status_panel.custom_minimum_size = Vector2(0, 24)
|
||||||
_status_bar_label = Label.new()
|
_status_bar_label = Label.new()
|
||||||
_status_bar_label.text = "ModBison Studio Ready | Grid: 1.0m | Mode: Select"
|
_status_bar_label.text = "ModBison Studio Ready | Baseplate Loaded | Press [F1] for Quick Start Guide"
|
||||||
_status_bar_label.add_theme_font_size_override("font_size", 10)
|
_status_bar_label.add_theme_font_size_override("font_size", 10)
|
||||||
status_panel.add_child(_status_bar_label)
|
status_panel.add_child(_status_bar_label)
|
||||||
main_vbox.add_child(status_panel)
|
main_vbox.add_child(status_panel)
|
||||||
@@ -164,6 +195,12 @@ func _build_studio_layout() -> void:
|
|||||||
_simulation_mgr.simulation_stopped.connect(func(): _update_simulation_ui(false))
|
_simulation_mgr.simulation_stopped.connect(func(): _update_simulation_ui(false))
|
||||||
add_child(_simulation_mgr)
|
add_child(_simulation_mgr)
|
||||||
|
|
||||||
|
# Help Dialog Overlay
|
||||||
|
_help_dialog = StudioHelpDialogClass.new()
|
||||||
|
_help_dialog.visible = false
|
||||||
|
_help_dialog.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
add_child(_help_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _build_top_menu_bar(parent: Control) -> void:
|
func _build_top_menu_bar(parent: Control) -> void:
|
||||||
_menu_bar = MenuBar.new()
|
_menu_bar = MenuBar.new()
|
||||||
@@ -172,15 +209,16 @@ func _build_top_menu_bar(parent: Control) -> void:
|
|||||||
# File Menu
|
# File Menu
|
||||||
var file_menu := PopupMenu.new()
|
var file_menu := PopupMenu.new()
|
||||||
file_menu.name = "File"
|
file_menu.name = "File"
|
||||||
file_menu.add_item("New Project...", 0, KEY_MASK_CTRL | KEY_N)
|
file_menu.add_item("New Project (Baseplate)", 0, KEY_MASK_CTRL | KEY_N)
|
||||||
file_menu.add_item("Open Project...", 1, KEY_MASK_CTRL | KEY_O)
|
file_menu.add_item("Open Project...", 1, KEY_MASK_CTRL | KEY_O)
|
||||||
file_menu.add_separator()
|
file_menu.add_separator()
|
||||||
file_menu.add_item("Save (Ctrl+S)", 2, KEY_MASK_CTRL | KEY_S)
|
file_menu.add_item("Save Project (Ctrl+S)", 2, KEY_MASK_CTRL | KEY_S)
|
||||||
file_menu.add_item("Save As...", 3)
|
file_menu.add_item("Save As...", 3)
|
||||||
file_menu.add_separator()
|
file_menu.add_separator()
|
||||||
file_menu.add_item("Publish to Cloud...", 4)
|
file_menu.add_item("Publish to Cloud...", 4)
|
||||||
file_menu.add_separator()
|
file_menu.add_separator()
|
||||||
file_menu.add_item("Exit Studio", 5)
|
file_menu.add_item("Return to Main Menu", 5)
|
||||||
|
file_menu.add_item("Exit Studio (Quit)", 6)
|
||||||
file_menu.id_pressed.connect(_on_file_menu_pressed)
|
file_menu.id_pressed.connect(_on_file_menu_pressed)
|
||||||
_menu_bar.add_child(file_menu)
|
_menu_bar.add_child(file_menu)
|
||||||
|
|
||||||
@@ -210,10 +248,12 @@ func _build_top_menu_bar(parent: Control) -> void:
|
|||||||
# Model Menu
|
# Model Menu
|
||||||
var model_menu := PopupMenu.new()
|
var model_menu := PopupMenu.new()
|
||||||
model_menu.name = "Model"
|
model_menu.name = "Model"
|
||||||
model_menu.add_item("Add Crate Prop", 0)
|
model_menu.add_item("Add ModBison", 0)
|
||||||
model_menu.add_item("Add Lamp Post", 1)
|
model_menu.add_item("Add Wooden Crate", 1)
|
||||||
model_menu.add_item("Add Pine Tree", 2)
|
model_menu.add_item("Add Pine Tree", 2)
|
||||||
model_menu.add_item("Add ModBison", 3)
|
model_menu.add_item("Add Street Lamp", 3)
|
||||||
|
model_menu.add_item("Add Treasure Chest", 4)
|
||||||
|
model_menu.add_item("Add Buffalo Chip Coin", 5)
|
||||||
model_menu.id_pressed.connect(_on_model_menu_pressed)
|
model_menu.id_pressed.connect(_on_model_menu_pressed)
|
||||||
_menu_bar.add_child(model_menu)
|
_menu_bar.add_child(model_menu)
|
||||||
|
|
||||||
@@ -234,6 +274,16 @@ func _build_top_menu_bar(parent: Control) -> void:
|
|||||||
pub_menu.id_pressed.connect(_on_publish_menu_pressed)
|
pub_menu.id_pressed.connect(_on_publish_menu_pressed)
|
||||||
_menu_bar.add_child(pub_menu)
|
_menu_bar.add_child(pub_menu)
|
||||||
|
|
||||||
|
# Help Menu
|
||||||
|
var help_menu := PopupMenu.new()
|
||||||
|
help_menu.name = "Help"
|
||||||
|
help_menu.add_item("Quick Start Guide (F1)", 0, KEY_F1)
|
||||||
|
help_menu.add_item("Keyboard & Mouse Controls", 1)
|
||||||
|
help_menu.add_separator()
|
||||||
|
help_menu.add_item("Reset Camera to Center", 2)
|
||||||
|
help_menu.id_pressed.connect(_on_help_menu_pressed)
|
||||||
|
_menu_bar.add_child(help_menu)
|
||||||
|
|
||||||
parent.add_child(_menu_bar)
|
parent.add_child(_menu_bar)
|
||||||
|
|
||||||
|
|
||||||
@@ -242,13 +292,13 @@ func _build_toolbar(parent: Control) -> void:
|
|||||||
_toolbar.custom_minimum_size = Vector2(0, 36)
|
_toolbar.custom_minimum_size = Vector2(0, 36)
|
||||||
_toolbar.add_theme_constant_override("separation", 6)
|
_toolbar.add_theme_constant_override("separation", 6)
|
||||||
|
|
||||||
_btn_select = _add_tool_btn("↖ Select", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.NONE))
|
_btn_select = _add_tool_btn("↖ [1] Select", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.NONE))
|
||||||
_btn_move = _add_tool_btn("✛ Move", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE))
|
_btn_move = _add_tool_btn("✛ [2] Move", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE))
|
||||||
_btn_rotate = _add_tool_btn("↻ Rotate", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.ROTATE))
|
_btn_rotate = _add_tool_btn("↻ [3] Rotate", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.ROTATE))
|
||||||
_btn_scale = _add_tool_btn("⤢ Scale", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE))
|
_btn_scale = _add_tool_btn("⤢ [4] Scale", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE))
|
||||||
|
|
||||||
var vsep := VSeparator.new()
|
var vsep1 := VSeparator.new()
|
||||||
_toolbar.add_child(vsep)
|
_toolbar.add_child(vsep1)
|
||||||
|
|
||||||
var snap_lbl := Label.new()
|
var snap_lbl := Label.new()
|
||||||
snap_lbl.text = "Snap:"
|
snap_lbl.text = "Snap:"
|
||||||
@@ -266,10 +316,26 @@ func _build_toolbar(parent: Control) -> void:
|
|||||||
)
|
)
|
||||||
_toolbar.add_child(_snap_spin)
|
_toolbar.add_child(_snap_spin)
|
||||||
|
|
||||||
|
var vsep2 := VSeparator.new()
|
||||||
|
_toolbar.add_child(vsep2)
|
||||||
|
|
||||||
|
# Quick Insert Bar
|
||||||
|
var add_lbl := Label.new()
|
||||||
|
add_lbl.text = "+ Quick Add:"
|
||||||
|
add_lbl.add_theme_font_size_override("font_size", 10)
|
||||||
|
add_lbl.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
|
||||||
|
_toolbar.add_child(add_lbl)
|
||||||
|
|
||||||
|
_add_quick_insert_btn("🦬 Bison", "bison", "ModBison")
|
||||||
|
_add_quick_insert_btn("📦 Crate", "crate_small", "Wooden Crate")
|
||||||
|
_add_quick_insert_btn("🌲 Tree", "tree_pine_01", "Pine Tree")
|
||||||
|
_add_quick_insert_btn("💡 Lamp", "lamp_post_01", "Street Lamp")
|
||||||
|
|
||||||
var spacer := Control.new()
|
var spacer := Control.new()
|
||||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_toolbar.add_child(spacer)
|
_toolbar.add_child(spacer)
|
||||||
|
|
||||||
|
# Simulation Controls
|
||||||
_btn_play = Button.new()
|
_btn_play = Button.new()
|
||||||
_btn_play.text = "▶ Play (F5)"
|
_btn_play.text = "▶ Play (F5)"
|
||||||
_btn_play.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
|
_btn_play.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
|
||||||
@@ -283,6 +349,24 @@ func _build_toolbar(parent: Control) -> void:
|
|||||||
_btn_stop.pressed.connect(func(): _simulation_mgr.stop_simulation())
|
_btn_stop.pressed.connect(func(): _simulation_mgr.stop_simulation())
|
||||||
_toolbar.add_child(_btn_stop)
|
_toolbar.add_child(_btn_stop)
|
||||||
|
|
||||||
|
var vsep3 := VSeparator.new()
|
||||||
|
_toolbar.add_child(vsep3)
|
||||||
|
|
||||||
|
# Help & Return to Main Menu
|
||||||
|
var btn_help := Button.new()
|
||||||
|
btn_help.text = "❓ Help & Guide (F1)"
|
||||||
|
btn_help.add_theme_color_override("font_color", Color(1.0, 0.9, 0.3))
|
||||||
|
btn_help.tooltip_text = "Open Quick Start Guide and Controls Reference (F1)"
|
||||||
|
btn_help.pressed.connect(func(): _toggle_help_dialog())
|
||||||
|
_toolbar.add_child(btn_help)
|
||||||
|
|
||||||
|
var btn_home := Button.new()
|
||||||
|
btn_home.text = "🏠 Main Menu"
|
||||||
|
btn_home.add_theme_color_override("font_color", Color(0.82, 0.86, 0.95))
|
||||||
|
btn_home.tooltip_text = "Return to ModBison Main Menu"
|
||||||
|
btn_home.pressed.connect(_return_to_main_menu)
|
||||||
|
_toolbar.add_child(btn_home)
|
||||||
|
|
||||||
parent.add_child(_toolbar)
|
parent.add_child(_toolbar)
|
||||||
|
|
||||||
|
|
||||||
@@ -294,6 +378,33 @@ func _add_tool_btn(text: String, callback: Callable) -> Button:
|
|||||||
return btn
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _add_quick_insert_btn(btn_text: String, cat_id: String, display_name: String) -> Button:
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = btn_text
|
||||||
|
btn.custom_minimum_size = Vector2(0, 26)
|
||||||
|
btn.tooltip_text = "Spawn %s into the world at camera focus point" % display_name
|
||||||
|
btn.pressed.connect(func(): _quick_insert(cat_id, display_name))
|
||||||
|
_toolbar.add_child(btn)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _quick_insert(cat_id: String, display_name: String) -> void:
|
||||||
|
var spawn_pos: Vector3 = Vector3.ZERO
|
||||||
|
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, {
|
||||||
|
"display_name": display_name,
|
||||||
|
"catalog_id": cat_id
|
||||||
|
})
|
||||||
|
if new_id != "":
|
||||||
|
core.select_object(new_id)
|
||||||
|
_spawn_visual_node(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]
|
||||||
|
|
||||||
|
|
||||||
func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
||||||
# World Environment
|
# World Environment
|
||||||
var env := WorldEnvironment.new()
|
var env := WorldEnvironment.new()
|
||||||
@@ -301,7 +412,7 @@ func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
|||||||
environment.background_mode = Environment.BG_COLOR
|
environment.background_mode = Environment.BG_COLOR
|
||||||
environment.background_color = Color(0.18, 0.22, 0.28)
|
environment.background_color = Color(0.18, 0.22, 0.28)
|
||||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||||
environment.ambient_light_color = Color(0.6, 0.6, 0.65)
|
environment.ambient_light_color = Color(0.65, 0.65, 0.7)
|
||||||
env.environment = environment
|
env.environment = environment
|
||||||
vp.add_child(env)
|
vp.add_child(env)
|
||||||
|
|
||||||
@@ -311,7 +422,17 @@ func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
|||||||
sun.shadow_enabled = true
|
sun.shadow_enabled = true
|
||||||
vp.add_child(sun)
|
vp.add_child(sun)
|
||||||
|
|
||||||
# 3D Grid floor
|
# Studio Baseplate Ground Mesh
|
||||||
|
_baseplate_mesh = MeshInstance3D.new()
|
||||||
|
var plane := PlaneMesh.new()
|
||||||
|
plane.size = Vector2(64, 64)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
@@ -332,21 +453,45 @@ func _setup_3d_viewport_scene(vp: SubViewport) -> void:
|
|||||||
core.selection.selection_changed.connect(_on_selection_changed)
|
core.selection.selection_changed.connect(_on_selection_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_viewport_hud_overlay(parent: Control) -> void:
|
||||||
|
var hud_bar := PanelContainer.new()
|
||||||
|
var h_style := StyleBoxFlat.new()
|
||||||
|
h_style.bg_color = Color(0.08, 0.10, 0.14, 0.88)
|
||||||
|
h_style.border_color = Color(0.3, 0.45, 0.6, 0.6)
|
||||||
|
h_style.set_border_width_all(1)
|
||||||
|
h_style.set_corner_radius_all(6)
|
||||||
|
h_style.content_margin_left = 12
|
||||||
|
h_style.content_margin_right = 12
|
||||||
|
h_style.content_margin_top = 6
|
||||||
|
h_style.content_margin_bottom = 6
|
||||||
|
hud_bar.add_theme_stylebox_override("panel", h_style)
|
||||||
|
hud_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
hud_bar.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||||
|
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.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)
|
||||||
|
parent.add_child(hud_bar)
|
||||||
|
|
||||||
|
|
||||||
func _create_3d_grid(size: int, step: float) -> Node3D:
|
func _create_3d_grid(size: int, step: float) -> Node3D:
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
var imm := ImmediateMesh.new()
|
var imm := ImmediateMesh.new()
|
||||||
var mat := StandardMaterial3D.new()
|
var mat := StandardMaterial3D.new()
|
||||||
mat.albedo_color = Color(0.35, 0.4, 0.45, 0.5)
|
mat.albedo_color = Color(0.35, 0.42, 0.5, 0.6)
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
|
||||||
imm.surface_begin(Mesh.PRIMITIVE_LINES, mat)
|
imm.surface_begin(Mesh.PRIMITIVE_LINES, mat)
|
||||||
var half := (size * step) / 2.0
|
var half := (size * step) / 2.0
|
||||||
for i in range(-size / 2, size / 2 + 1):
|
for i in range(-size / 2, size / 2 + 1):
|
||||||
var p := i * step
|
var p := i * step
|
||||||
imm.surface_add_vertex(Vector3(p, 0, -half))
|
imm.surface_add_vertex(Vector3(p, 0.01, -half))
|
||||||
imm.surface_add_vertex(Vector3(p, 0, half))
|
imm.surface_add_vertex(Vector3(p, 0.01, half))
|
||||||
imm.surface_add_vertex(Vector3(-half, 0, p))
|
imm.surface_add_vertex(Vector3(-half, 0.01, p))
|
||||||
imm.surface_add_vertex(Vector3(half, 0, p))
|
imm.surface_add_vertex(Vector3(half, 0.01, p))
|
||||||
imm.surface_end()
|
imm.surface_end()
|
||||||
mi.mesh = imm
|
mi.mesh = imm
|
||||||
return mi
|
return mi
|
||||||
@@ -372,19 +517,152 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
save_current_project()
|
save_current_project()
|
||||||
elif k.keycode == KEY_D:
|
elif k.keycode == KEY_D:
|
||||||
_hierarchy_dock._on_duplicate_pressed()
|
_hierarchy_dock._on_duplicate_pressed()
|
||||||
|
elif k.keycode == KEY_F1:
|
||||||
|
_toggle_help_dialog()
|
||||||
elif k.keycode == KEY_F5:
|
elif k.keycode == KEY_F5:
|
||||||
if _simulation_mgr.is_playing():
|
if _simulation_mgr.is_playing():
|
||||||
_simulation_mgr.stop_simulation()
|
_simulation_mgr.stop_simulation()
|
||||||
else:
|
else:
|
||||||
_simulation_mgr.start_play(true)
|
_simulation_mgr.start_play(true)
|
||||||
|
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():
|
||||||
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
|
elif k.keycode == KEY_3 and not k.is_command_or_control_pressed():
|
||||||
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.ROTATE)
|
||||||
|
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 ""
|
||||||
|
if 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 _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)
|
||||||
_status_bar_label.text = "Gizmo Mode: %s" % str(mode)
|
var mode_name := "Select"
|
||||||
|
match mode:
|
||||||
|
TransformGizmo3DClass.Mode.TRANSLATE: mode_name = "Move (✛)"
|
||||||
|
TransformGizmo3DClass.Mode.ROTATE: mode_name = "Rotate (↻)"
|
||||||
|
TransformGizmo3DClass.Mode.SCALE: mode_name = "Scale (⤢)"
|
||||||
|
_status_bar_label.text = "Tool: %s | Snap: %.1fm | [F1] Help" % [mode_name, _snap_spin.value if _snap_spin else 1.0]
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_help_dialog() -> void:
|
||||||
|
if _help_dialog == null:
|
||||||
|
return
|
||||||
|
_help_dialog.visible = not _help_dialog.visible
|
||||||
|
if _help_dialog.visible:
|
||||||
|
_help_dialog.move_to_front()
|
||||||
|
|
||||||
|
|
||||||
|
func _return_to_main_menu() -> void:
|
||||||
|
if _simulation_mgr != null and _simulation_mgr.is_playing():
|
||||||
|
_simulation_mgr.stop_simulation()
|
||||||
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Interactive 3D Viewport Input & Raycast 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
|
||||||
|
else:
|
||||||
|
core.select_object("")
|
||||||
|
_status_bar_label.text = "Deselected | Click an object or [+ Add] to build"
|
||||||
|
|
||||||
|
|
||||||
|
func handle_viewport_drop(at_pos: Vector2, data: Variant) -> void:
|
||||||
|
if not (data is Dictionary):
|
||||||
|
return
|
||||||
|
var cat_id := str(data.get("catalog_id", "crate_small"))
|
||||||
|
var drop_pos := _raycast_ground_plane(at_pos)
|
||||||
|
var target_obj := _raycast_find_object(at_pos)
|
||||||
|
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, {
|
||||||
|
"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, {
|
||||||
|
"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)
|
||||||
|
_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]
|
||||||
|
|
||||||
|
|
||||||
|
func _raycast_find_object(screen_pos: Vector2) -> Node3D:
|
||||||
|
if _camera_3d == null or _world_container == null:
|
||||||
|
return null
|
||||||
|
var from: Vector3 = _camera_3d.project_ray_origin(screen_pos)
|
||||||
|
var dir: Vector3 = _camera_3d.project_ray_normal(screen_pos)
|
||||||
|
|
||||||
|
# 1. Physics raycast against colliders in viewport world
|
||||||
|
if _sub_viewport != null and _sub_viewport.world_3d != null:
|
||||||
|
var space = _sub_viewport.world_3d.direct_space_state
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(from, from + dir * 500.0)
|
||||||
|
var res := space.intersect_ray(query)
|
||||||
|
if not res.is_empty() and res.collider != null:
|
||||||
|
var curr: Node = res.collider
|
||||||
|
while curr != null and curr != _world_container:
|
||||||
|
if curr.get_parent() == _world_container and curr is Node3D:
|
||||||
|
return curr as Node3D
|
||||||
|
curr = curr.get_parent()
|
||||||
|
|
||||||
|
# 2. Geometric fallback (ray-to-object closest distance)
|
||||||
|
var best_node: Node3D = null
|
||||||
|
var best_dist: float = 1e9
|
||||||
|
for child in _world_container.get_children():
|
||||||
|
if not (child is Node3D):
|
||||||
|
continue
|
||||||
|
var obj_pos: Vector3 = (child as Node3D).global_position
|
||||||
|
var to_obj: Vector3 = obj_pos - from
|
||||||
|
var proj: float = to_obj.dot(dir)
|
||||||
|
if proj < 0.2:
|
||||||
|
continue
|
||||||
|
var closest: Vector3 = from + dir * proj
|
||||||
|
var dist: float = closest.distance_to(obj_pos)
|
||||||
|
if dist < 1.8 and proj < best_dist:
|
||||||
|
best_dist = proj
|
||||||
|
best_node = child as Node3D
|
||||||
|
return best_node
|
||||||
|
|
||||||
|
|
||||||
|
func _raycast_ground_plane(screen_pos: Vector2) -> Vector3:
|
||||||
|
if _camera_3d == null:
|
||||||
|
return Vector3.ZERO
|
||||||
|
var from: Vector3 = _camera_3d.project_ray_origin(screen_pos)
|
||||||
|
var dir: Vector3 = _camera_3d.project_ray_normal(screen_pos)
|
||||||
|
if absf(dir.y) < 0.0001:
|
||||||
|
return from + dir * 10.0
|
||||||
|
var t: float = -from.y / dir.y
|
||||||
|
if t < 0.0:
|
||||||
|
return from + dir * 10.0
|
||||||
|
return from + dir * t
|
||||||
|
|
||||||
|
|
||||||
|
# ── Selection & Scene Visual Sync ─────────────────────────────────────
|
||||||
|
|
||||||
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)
|
_gizmo_3d.attach_to_node(null)
|
||||||
@@ -398,8 +676,11 @@ func _on_selection_changed(obj_id: String) -> void:
|
|||||||
|
|
||||||
func _on_collection_model_selected(model_data: Dictionary) -> void:
|
func _on_collection_model_selected(model_data: Dictionary) -> void:
|
||||||
var cat_id := str(model_data.get("catalog_id", "crate_small"))
|
var cat_id := str(model_data.get("catalog_id", "crate_small"))
|
||||||
var new_pos := Vector3(0, 0, 0)
|
var spawn_pos: Vector3 = Vector3.ZERO
|
||||||
var new_id := core.place_object(cat_id, new_pos, Vector4(0, 0, 0, 1), Vector3.ONE, {
|
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, {
|
||||||
"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", "")
|
||||||
@@ -408,6 +689,8 @@ func _on_collection_model_selected(model_data: Dictionary) -> void:
|
|||||||
core.select_object(new_id)
|
core.select_object(new_id)
|
||||||
_spawn_visual_node(new_id)
|
_spawn_visual_node(new_id)
|
||||||
_hierarchy_dock.refresh_tree()
|
_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]
|
||||||
|
|
||||||
|
|
||||||
func _spawn_visual_node(obj_id: String) -> void:
|
func _spawn_visual_node(obj_id: String) -> void:
|
||||||
@@ -436,6 +719,8 @@ func _refresh_scene_visuals() -> void:
|
|||||||
_hierarchy_dock.refresh_tree()
|
_hierarchy_dock.refresh_tree()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Templates & Project IO ───────────────────────────────────────────
|
||||||
|
|
||||||
func load_template(template_name: String) -> void:
|
func load_template(template_name: String) -> void:
|
||||||
var data := MBWorldProjectIOClass.create_template(template_name)
|
var data := MBWorldProjectIOClass.create_template(template_name)
|
||||||
project_title = data.get("title", "Untitled World")
|
project_title = data.get("title", "Untitled World")
|
||||||
@@ -451,6 +736,11 @@ 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():
|
||||||
|
core.select_object(core.model.payload.objects[0].id)
|
||||||
|
_set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)
|
||||||
|
_status_bar_label.text = "Loaded template: %s | Press [F1] for Controls" % project_title
|
||||||
|
|
||||||
|
|
||||||
func save_current_project(path: String = "") -> bool:
|
func save_current_project(path: String = "") -> bool:
|
||||||
@@ -482,11 +772,16 @@ func _update_simulation_ui(playing: bool) -> void:
|
|||||||
_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)"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Menu Bar Dispatch ────────────────────────────────────────────────
|
||||||
|
|
||||||
func _on_file_menu_pressed(id: int) -> void:
|
func _on_file_menu_pressed(id: int) -> void:
|
||||||
match id:
|
match id:
|
||||||
0: load_template("baseplate")
|
0: load_template("baseplate")
|
||||||
2: save_current_project()
|
2: save_current_project()
|
||||||
5: get_tree().quit(0)
|
3: save_current_project("user://saved_world_as.mbworld")
|
||||||
|
4: save_current_project("user://published_world.mbworld")
|
||||||
|
5: _return_to_main_menu()
|
||||||
|
6: get_tree().quit(0)
|
||||||
|
|
||||||
|
|
||||||
func _on_edit_menu_pressed(id: int) -> void:
|
func _on_edit_menu_pressed(id: int) -> void:
|
||||||
@@ -503,12 +798,25 @@ func _on_view_menu_pressed(id: int) -> void:
|
|||||||
1: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.TOP)
|
1: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.TOP)
|
||||||
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:
|
||||||
|
var sel_id = core.selection.get_selected_id() if core.selection.has_method("get_selected_id") else ""
|
||||||
|
if 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 _on_model_menu_pressed(id: int) -> void:
|
func _on_model_menu_pressed(id: int) -> void:
|
||||||
var cat_ids := ["crate_small", "lamp_post", "tree_pine", "bison"]
|
var cat_info := [
|
||||||
if id >= 0 and id < cat_ids.size():
|
{"catalog_id": "bison", "name": "ModBison"},
|
||||||
_on_collection_model_selected({"catalog_id": cat_ids[id], "name": cat_ids[id].capitalize()})
|
{"catalog_id": "crate_small", "name": "Wooden Crate"},
|
||||||
|
{"catalog_id": "tree_pine_01", "name": "Pine Tree"},
|
||||||
|
{"catalog_id": "lamp_post_01", "name": "Street Lamp"},
|
||||||
|
{"catalog_id": "chest_01", "name": "Treasure Chest"},
|
||||||
|
{"catalog_id": "buffalo_chip_coin", "name": "Buffalo Chip Coin"}
|
||||||
|
]
|
||||||
|
if id >= 0 and id < cat_info.size():
|
||||||
|
_quick_insert(cat_info[id]["catalog_id"], cat_info[id]["name"])
|
||||||
|
|
||||||
|
|
||||||
func _on_test_menu_pressed(id: int) -> void:
|
func _on_test_menu_pressed(id: int) -> void:
|
||||||
@@ -523,6 +831,14 @@ func _on_publish_menu_pressed(id: int) -> void:
|
|||||||
save_current_project("user://exported_world.mbworld")
|
save_current_project("user://exported_world.mbworld")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_help_menu_pressed(id: int) -> void:
|
||||||
|
match id:
|
||||||
|
0, 1: _toggle_help_dialog()
|
||||||
|
2:
|
||||||
|
if _camera_3d != null:
|
||||||
|
_camera_3d.focus_on(Vector3.ZERO, 5.0)
|
||||||
|
|
||||||
|
|
||||||
func _parse_cmdline_arguments() -> void:
|
func _parse_cmdline_arguments() -> void:
|
||||||
var args := OS.get_cmdline_args()
|
var args := OS.get_cmdline_args()
|
||||||
for i in range(args.size()):
|
for i in range(args.size()):
|
||||||
|
|||||||
Reference in New Issue
Block a user