class_name StudioMain extends Control ## Standalone ModBison Studio Application Shell. ## Implements multi-panel docking layout, top menu bar with keyboard shortcuts, ## 3D interactive viewport with gizmos, hierarchy, inspector, collections dock, ## and simulation harness. const MBWorldProjectIOClass = preload("res://scripts/studio/mbworld_project_io.gd") const StudioViewportCameraClass = preload("res://scripts/studio/studio_viewport_camera.gd") const TransformGizmo3DClass = preload("res://scripts/studio/gizmos/transform_gizmo_3d.gd") const StudioHierarchyDockClass = preload("res://scripts/studio/studio_hierarchy_dock.gd") const StudioInspectorDockClass = preload("res://scripts/studio/studio_inspector_dock.gd") const StudioCollectionsDockClass = preload("res://scripts/studio/studio_collections_dock.gd") const StudioSimulationManagerClass = preload("res://scripts/studio/studio_simulation_manager.gd") const StaticObjectNodeClass = preload("res://scripts/static_object_node.gd") const WorldObjectClass = preload("res://scripts/world_object.gd") # Core state var core: EditorCore = null var current_project_path: String = "" var project_title: String = "Untitled World" # Top Menu & Toolbar var _menu_bar: MenuBar var _toolbar: HBoxContainer var _btn_select: Button var _btn_move: Button var _btn_rotate: Button var _btn_scale: Button var _btn_play: Button var _btn_stop: Button var _snap_spin: SpinBox # Main Viewport & 3D Elements var _sub_viewport: SubViewport var _camera_3d: Camera3D var _world_container: Node3D var _gizmo_3d: Node3D # Docks var _hierarchy_dock = null var _inspector_dock = null var _collections_dock = null var _simulation_mgr = null var _status_bar_label: Label var _is_built: bool = false func _init() -> void: name = "StudioMain" if not _is_built: _init_core_model() _build_studio_layout() func _ready() -> void: if not _is_built: _init_core_model() _build_studio_layout() _setup_shortcuts() _parse_cmdline_arguments() func _init_core_model() -> void: var payload := ChunkDataDecoder.ChunkPayload.new() var layer := ChunkDataDecoder.TileLayerData.new() layer.name = "ground" layer.layer_id = 0 layer.width = 32 layer.height = 32 layer.depth = 1 layer.tiles = PackedInt32Array() layer.tiles.resize(32 * 32) payload.tile_layers.append(layer) var model := ChunkEditorModel.new(payload) var catalog := EditorObjectCatalog.new() catalog.load_catalog() core = EditorCore.new(model, catalog) func _build_studio_layout() -> void: if _is_built: return _is_built = true set_anchors_preset(Control.PRESET_FULL_RECT) var bg := ColorRect.new() bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.color = Color(0.14, 0.15, 0.18) add_child(bg) var main_vbox := VBoxContainer.new() main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(main_vbox) # 1. Top Menu Bar _build_top_menu_bar(main_vbox) # 2. Main Action Toolbar _build_toolbar(main_vbox) # 3. Center Workspace (Horizontal Split: Left Dock | Center Viewport | Right Docks) var workspace_hbox := HBoxContainer.new() workspace_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL main_vbox.add_child(workspace_hbox) # Left Dock: Hierarchy var left_panel := PanelContainer.new() left_panel.custom_minimum_size = Vector2(240, 0) left_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL _hierarchy_dock = StudioHierarchyDockClass.new() _hierarchy_dock.setup(core) left_panel.add_child(_hierarchy_dock) workspace_hbox.add_child(left_panel) # Center: 3D Viewport var vp_container := SubViewportContainer.new() vp_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL vp_container.size_flags_vertical = Control.SIZE_EXPAND_FILL vp_container.stretch = true workspace_hbox.add_child(vp_container) _sub_viewport = SubViewport.new() _sub_viewport.handle_input_locally = true vp_container.add_child(_sub_viewport) _setup_3d_viewport_scene(_sub_viewport) # Right Dock: TabContainer with Inspector & Collections var right_panel := PanelContainer.new() right_panel.custom_minimum_size = Vector2(280, 0) right_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL var tabs := TabContainer.new() tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL _inspector_dock = StudioInspectorDockClass.new() _inspector_dock.setup(core) tabs.add_child(_inspector_dock) tabs.set_tab_title(0, "Inspector") _collections_dock = StudioCollectionsDockClass.new() _collections_dock.model_selected.connect(_on_collection_model_selected) tabs.add_child(_collections_dock) tabs.set_tab_title(1, "Collections") right_panel.add_child(tabs) workspace_hbox.add_child(right_panel) # 4. Bottom Status Bar var status_panel := PanelContainer.new() status_panel.custom_minimum_size = Vector2(0, 24) _status_bar_label = Label.new() _status_bar_label.text = "ModBison Studio Ready | Grid: 1.0m | Mode: Select" _status_bar_label.add_theme_font_size_override("font_size", 10) status_panel.add_child(_status_bar_label) main_vbox.add_child(status_panel) # Simulation Manager _simulation_mgr = StudioSimulationManagerClass.new() _simulation_mgr.setup(core, _world_container) _simulation_mgr.simulation_started.connect(func(_m): _update_simulation_ui(true)) _simulation_mgr.simulation_stopped.connect(func(): _update_simulation_ui(false)) add_child(_simulation_mgr) func _build_top_menu_bar(parent: Control) -> void: _menu_bar = MenuBar.new() _menu_bar.custom_minimum_size = Vector2(0, 28) # File Menu var file_menu := PopupMenu.new() file_menu.name = "File" file_menu.add_item("New Project...", 0, KEY_MASK_CTRL | KEY_N) file_menu.add_item("Open Project...", 1, KEY_MASK_CTRL | KEY_O) file_menu.add_separator() file_menu.add_item("Save (Ctrl+S)", 2, KEY_MASK_CTRL | KEY_S) file_menu.add_item("Save As...", 3) file_menu.add_separator() file_menu.add_item("Publish to Cloud...", 4) file_menu.add_separator() file_menu.add_item("Exit Studio", 5) file_menu.id_pressed.connect(_on_file_menu_pressed) _menu_bar.add_child(file_menu) # Edit Menu var edit_menu := PopupMenu.new() edit_menu.name = "Edit" edit_menu.add_item("Undo", 0, KEY_MASK_CTRL | KEY_Z) edit_menu.add_item("Redo", 1, KEY_MASK_CTRL | KEY_Y) edit_menu.add_separator() edit_menu.add_item("Duplicate", 2, KEY_MASK_CTRL | KEY_D) edit_menu.add_item("Delete", 3, KEY_DELETE) edit_menu.id_pressed.connect(_on_edit_menu_pressed) _menu_bar.add_child(edit_menu) # View Menu var view_menu := PopupMenu.new() view_menu.name = "View" view_menu.add_item("Perspective", 0, KEY_P) view_menu.add_item("Top View", 1, KEY_7) view_menu.add_item("Front View", 2, KEY_1) view_menu.add_item("Side View", 3, KEY_3) view_menu.add_separator() view_menu.add_item("Focus Selection", 4, KEY_F) view_menu.id_pressed.connect(_on_view_menu_pressed) _menu_bar.add_child(view_menu) # Model Menu var model_menu := PopupMenu.new() model_menu.name = "Model" model_menu.add_item("Add Crate Prop", 0) model_menu.add_item("Add Lamp Post", 1) model_menu.add_item("Add Pine Tree", 2) model_menu.add_item("Add ModBison", 3) model_menu.id_pressed.connect(_on_model_menu_pressed) _menu_bar.add_child(model_menu) # Test Menu var test_menu := PopupMenu.new() test_menu.name = "Test" test_menu.add_item("Play Solo", 0, KEY_F5) test_menu.add_item("Run Simulation", 1, KEY_F6) test_menu.add_item("Stop Playtest", 2, KEY_MASK_SHIFT | KEY_F5) test_menu.id_pressed.connect(_on_test_menu_pressed) _menu_bar.add_child(test_menu) # Publish Menu var pub_menu := PopupMenu.new() pub_menu.name = "Publish" pub_menu.add_item("Export .mbworld Package", 0) pub_menu.add_item("Sync to Game Server", 1) pub_menu.id_pressed.connect(_on_publish_menu_pressed) _menu_bar.add_child(pub_menu) parent.add_child(_menu_bar) func _build_toolbar(parent: Control) -> void: _toolbar = HBoxContainer.new() _toolbar.custom_minimum_size = Vector2(0, 36) _toolbar.add_theme_constant_override("separation", 6) _btn_select = _add_tool_btn("↖ Select", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.NONE)) _btn_move = _add_tool_btn("✛ Move", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.TRANSLATE)) _btn_rotate = _add_tool_btn("↻ Rotate", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.ROTATE)) _btn_scale = _add_tool_btn("⤢ Scale", func(): _set_gizmo_mode(TransformGizmo3DClass.Mode.SCALE)) var vsep := VSeparator.new() _toolbar.add_child(vsep) var snap_lbl := Label.new() snap_lbl.text = "Snap:" snap_lbl.add_theme_font_size_override("font_size", 10) _toolbar.add_child(snap_lbl) _snap_spin = SpinBox.new() _snap_spin.min_value = 0.1 _snap_spin.max_value = 10.0 _snap_spin.step = 0.5 _snap_spin.value = 1.0 _snap_spin.value_changed.connect(func(v): if _gizmo_3d != null: _gizmo_3d.snap_translate = v ) _toolbar.add_child(_snap_spin) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL _toolbar.add_child(spacer) _btn_play = Button.new() _btn_play.text = "▶ Play (F5)" _btn_play.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4)) _btn_play.pressed.connect(func(): _simulation_mgr.start_play(true)) _toolbar.add_child(_btn_play) _btn_stop = Button.new() _btn_stop.text = "⏹ Stop" _btn_stop.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) _btn_stop.disabled = true _btn_stop.pressed.connect(func(): _simulation_mgr.stop_simulation()) _toolbar.add_child(_btn_stop) parent.add_child(_toolbar) func _add_tool_btn(text: String, callback: Callable) -> Button: var btn := Button.new() btn.text = text btn.pressed.connect(callback) _toolbar.add_child(btn) return btn func _setup_3d_viewport_scene(vp: SubViewport) -> void: # World Environment var env := WorldEnvironment.new() var environment := Environment.new() environment.background_mode = Environment.BG_COLOR environment.background_color = Color(0.18, 0.22, 0.28) environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.ambient_light_color = Color(0.6, 0.6, 0.65) env.environment = environment vp.add_child(env) # Directional Sun Light var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-45, 30, 0) sun.shadow_enabled = true vp.add_child(sun) # 3D Grid floor var grid := _create_3d_grid(32, 1.0) vp.add_child(grid) # Camera _camera_3d = StudioViewportCameraClass.new() vp.add_child(_camera_3d) # World Object Container _world_container = Node3D.new() _world_container.name = "WorldContainer" vp.add_child(_world_container) # Interactive Gizmo _gizmo_3d = TransformGizmo3DClass.new() _gizmo_3d.name = "TransformGizmo3D" vp.add_child(_gizmo_3d) core.selection.selection_changed.connect(_on_selection_changed) func _create_3d_grid(size: int, step: float) -> Node3D: var mi := MeshInstance3D.new() var imm := ImmediateMesh.new() var mat := StandardMaterial3D.new() mat.albedo_color = Color(0.35, 0.4, 0.45, 0.5) mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED imm.surface_begin(Mesh.PRIMITIVE_LINES, mat) var half := (size * step) / 2.0 for i in range(-size / 2, size / 2 + 1): var p := i * step imm.surface_add_vertex(Vector3(p, 0, -half)) imm.surface_add_vertex(Vector3(p, 0, half)) imm.surface_add_vertex(Vector3(-half, 0, p)) imm.surface_add_vertex(Vector3(half, 0, p)) imm.surface_end() mi.mesh = imm return mi func _setup_shortcuts() -> void: set_process_unhandled_key_input(true) func _unhandled_key_input(event: InputEvent) -> void: var k := event as InputEventKey if not k.pressed or k.echo: return if k.is_command_or_control_pressed(): if k.keycode == KEY_Z: core.undo() _refresh_scene_visuals() elif k.keycode == KEY_Y: core.redo() _refresh_scene_visuals() elif k.keycode == KEY_S: save_current_project() elif k.keycode == KEY_D: _hierarchy_dock._on_duplicate_pressed() elif k.keycode == KEY_F5: if _simulation_mgr.is_playing(): _simulation_mgr.stop_simulation() else: _simulation_mgr.start_play(true) func _set_gizmo_mode(mode: TransformGizmo3DClass.Mode) -> void: if _gizmo_3d != null: _gizmo_3d.set_mode(mode) _status_bar_label.text = "Gizmo Mode: %s" % str(mode) func _on_selection_changed(obj_id: String) -> void: if obj_id == "": _gizmo_3d.attach_to_node(null) return var obj_node = _world_container.get_node_or_null(obj_id) if obj_node is Node3D: _gizmo_3d.attach_to_node(obj_node) _camera_3d.focus_on(obj_node.global_position) func _on_collection_model_selected(model_data: Dictionary) -> void: var cat_id := str(model_data.get("catalog_id", "crate_small")) var new_pos := Vector3(0, 0, 0) var new_id := core.place_object(cat_id, new_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) _hierarchy_dock.refresh_tree() func _spawn_visual_node(obj_id: String) -> void: var obj = core.model.find_object(obj_id) if obj == null: return var wo := WorldObjectClass.new() wo.id = obj.id wo.type = obj.type wo.asset_id = obj.asset_id wo.position = obj.position wo.properties = obj.properties.duplicate(true) var node = StaticObjectNodeClass.new() node.name = obj.id node.world_object = wo node.position = Vector3(obj.position.x, obj.position.z, obj.position.y) _world_container.add_child(node) func _refresh_scene_visuals() -> void: for c in _world_container.get_children(): c.queue_free() for obj in core.model.payload.objects: _spawn_visual_node(obj.id) _hierarchy_dock.refresh_tree() func load_template(template_name: String) -> void: var data := MBWorldProjectIOClass.create_template(template_name) project_title = data.get("title", "Untitled World") for obj in data.get("objects", []): var p: Array = obj.get("position", [0, 0, 0]) var r: Array = obj.get("rotation", [0, 0, 0, 1]) var s: Array = obj.get("scale", [1, 1, 1]) core.place_object( obj.get("catalog_id", "crate_small"), Vector3(p[0], p[1], p[2]), Vector4(r[0], r[1], r[2], r[3]), Vector3(s[0], s[1], s[2]), obj.get("properties", {}) ) _refresh_scene_visuals() func save_current_project(path: String = "") -> bool: if path == "": path = "user://saved_world.mbworld" var data := { "title": project_title, "objects": [] } for obj in core.model.payload.objects: data["objects"].append({ "id": obj.id, "catalog_id": obj.properties.get("catalog_id", "crate_small"), "asset_id": obj.asset_id, "position": [obj.position.x, obj.position.y, obj.position.z], "rotation": [obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w], "scale": [obj.scale.x, obj.scale.y, obj.scale.z], "properties": obj.properties }) var ok := MBWorldProjectIOClass.save_project(path, data) if ok: _status_bar_label.text = "Saved world project to %s" % path return ok 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)" func _on_file_menu_pressed(id: int) -> void: match id: 0: load_template("baseplate") 2: save_current_project() 5: get_tree().quit(0) func _on_edit_menu_pressed(id: int) -> void: match id: 0: core.undo(); _refresh_scene_visuals() 1: core.redo(); _refresh_scene_visuals() 2: _hierarchy_dock._on_duplicate_pressed() 3: _hierarchy_dock._on_delete_pressed() func _on_view_menu_pressed(id: int) -> void: match id: 0: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.PERSPECTIVE) 1: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.TOP) 2: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.FRONT) 3: _camera_3d.set_view_preset(StudioViewportCameraClass.ViewPreset.SIDE) func _on_model_menu_pressed(id: int) -> void: var cat_ids := ["crate_small", "lamp_post", "tree_pine", "bison"] if id >= 0 and id < cat_ids.size(): _on_collection_model_selected({"catalog_id": cat_ids[id], "name": cat_ids[id].capitalize()}) func _on_test_menu_pressed(id: int) -> void: match id: 0: _simulation_mgr.start_play(true) 1: _simulation_mgr.start_play(false) 2: _simulation_mgr.stop_simulation() func _on_publish_menu_pressed(id: int) -> void: if id == 0: save_current_project("user://exported_world.mbworld") func _parse_cmdline_arguments() -> void: var args := OS.get_cmdline_args() for i in range(args.size()): var arg := args[i] if arg == "--template" and i + 1 < args.size(): load_template(args[i + 1]) elif arg.ends_with(".mbworld") and FileAccess.file_exists(arg): var d := MBWorldProjectIOClass.load_project(arg) if not d.is_empty(): project_title = d.get("title", "Loaded World")