extends CanvasLayer ## ## StudioUI - dark material theme + floating, collapsible panels (Step 4). ## ## Layout (all built in code, resolution independent, scaled by a UI_SCALE ## constant so it fits phones in portrait/landscape and tablets): ## ## TopBar : project name, memory HUD, undo/redo ## LeftDock : tool rail (mesh / sculpt / rig / sim / anim) ## RightPanels : collapsible panels -> Tools, Layers, Timeline ## BottomBar : play/pause + timeline scrubber ## ## Touch targets are >= 48px. The theme is Material-ish dark to match the ## viewport and reduce OLED power draw on phones. ## const UI_SCALE := 1.0 const TOUCH_MIN := 48.0 var _touch_camera: Camera3D = null var _ui_root: Control var _panels: Dictionary = {} # panel_id -> PanelContainer var _tool_buttons: Dictionary = {} var _memory_label: Label var _fps_label: Label var _timeline: HSlider var _play_button: Button var _status: Label var _active_brush := "sculpt.draw" func _ready() -> void: layer = 10 _touch_camera = get_parent().get_node_or_null("Camera3D") as Camera3D _build_theme() _build_root() _build_top_bar() _build_tool_rail() _build_panels() _build_bottom_bar() _connect_bus() ## --- Theme ------------------------------------------------------------------ func _build_theme() -> void: var theme := Theme.new() theme.default_font_size = int(16 * UI_SCALE) var panel := StyleBoxFlat.new() panel.bg_color = Color(0.11, 0.12, 0.15, 0.94) panel.corner_radius_top_left = 10 panel.corner_radius_top_right = 10 panel.corner_radius_bottom_left = 10 panel.corner_radius_bottom_right = 10 panel.content_margin_left = 10 panel.content_margin_right = 10 panel.content_margin_top = 8 panel.content_margin_bottom = 8 theme.set_stylebox("panel", "PanelContainer", panel) var btn := StyleBoxFlat.new() btn.bg_color = Color(0.18, 0.2, 0.25) btn.corner_radius_top_left = 8 btn.corner_radius_top_right = 8 btn.corner_radius_bottom_left = 8 btn.corner_radius_bottom_right = 8 btn.content_margin_left = 12 btn.content_margin_right = 12 btn.content_margin_top = 8 btn.content_margin_bottom = 8 theme.set_stylebox("normal", "Button", btn) var btn_hover := btn.duplicate() as StyleBoxFlat btn_hover.bg_color = Color(0.24, 0.27, 0.33) theme.set_stylebox("hover", "Button", btn_hover) var btn_pressed := btn.duplicate() as StyleBoxFlat btn_pressed.bg_color = Color(0.31, 0.55, 0.86) theme.set_stylebox("pressed", "Button", btn_pressed) theme.set_color("font_color", "Label", Color(0.88, 0.9, 0.94)) theme.set_color("font_color", "Button", Color(0.88, 0.9, 0.94)) _ui_root_theme = theme var _ui_root_theme: Theme ## --- Layout ----------------------------------------------------------------- func _build_root() -> void: var root := Control.new() root.name = "Root" root.theme = _ui_root_theme root.set_anchors_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(root) _ui_root = root func _build_top_bar() -> void: var bar := PanelContainer.new() bar.name = "TopBar" bar.set_anchors_preset(Control.PRESET_TOP_WIDE) bar.custom_minimum_size = Vector2(0, 52 * UI_SCALE) bar.mouse_filter = Control.MOUSE_FILTER_STOP _ui_root.add_child(bar) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 10) bar.add_child(hbox) var title := Label.new() title.text = " MonkeyStudio" title.add_theme_font_size_override("font_size", int(20 * UI_SCALE)) hbox.add_child(title) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(spacer) _fps_label = Label.new() _fps_label.text = "FPS --" hbox.add_child(_fps_label) _memory_label = Label.new() _memory_label.text = "MEM --" hbox.add_child(_memory_label) func _build_tool_rail() -> void: var rail := PanelContainer.new() rail.name = "LeftDock" rail.set_anchors_preset(Control.PRESET_CENTER_LEFT) rail.position = Vector2(8, 0) rail.mouse_filter = Control.MOUSE_FILTER_STOP _ui_root.add_child(rail) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 8) rail.add_child(vbox) var tools := [ ["mesh", "Mesh", "mesh.cube"], ["sculpt", "Sculpt", "sculpt.draw"], ["rig", "Rig", "rig.skeleton"], ["sim", "Sim", "sim.cloth"], ["anim", "Anim", "anim.timeline"], ] for t in tools: var b := Button.new() b.text = t[1] b.custom_minimum_size = Vector2(96 * UI_SCALE, TOUCH_MIN * UI_SCALE) b.pressed.connect(_on_tool_pressed.bind(t[2])) vbox.add_child(b) _tool_buttons[t[0]] = b func _build_panels() -> void: var stack := VBoxContainer.new() stack.name = "RightPanels" stack.set_anchors_preset(Control.PRESET_TOP_RIGHT) stack.position = Vector2(-8, 60) stack.add_theme_constant_override("separation", 8) _ui_root.add_child(stack) _add_panel(stack, "tools", "Tools", _build_tools_panel()) _add_panel(stack, "layers", "Layers", _build_layers_panel()) _add_panel(stack, "timeline", "Timeline", _build_timeline_panel()) func _add_panel(parent: Control, id: String, title: String, content: Control) -> void: var panel := PanelContainer.new() panel.name = "Panel_" + id panel.custom_minimum_size = Vector2(220 * UI_SCALE, 0) parent.add_child(panel) var vbox := VBoxContainer.new() panel.add_child(vbox) var header := Button.new() header.text = "v " + title header.alignment = HORIZONTAL_ALIGNMENT_LEFT header.custom_minimum_size = Vector2(0, TOUCH_MIN * UI_SCALE * 0.8) header.pressed.connect(_toggle_panel.bind(id)) vbox.add_child(header) var body := MarginContainer.new() body.name = "Body" body.add_child(content) vbox.add_child(body) _panels[id] = panel func _build_tools_panel() -> Control: var box := VBoxContainer.new() box.add_theme_constant_override("separation", 6) var label := Label.new() label.text = "Brush" box.add_child(label) for b in ["draw", "grab", "pinch", "smooth", "flatten"]: var btn := Button.new() btn.text = b.capitalize() btn.custom_minimum_size = Vector2(0, TOUCH_MIN * UI_SCALE) btn.pressed.connect(_on_brush_selected.bind("sculpt." + b)) box.add_child(btn) box.add_child(_make_slider("Radius", 0.05, 2.0, 0.5, func(v): StudioBus.brush_radius_changed.emit(v))) box.add_child(_make_slider("Strength", 0.05, 1.0, 0.5, func(v): StudioBus.brush_strength_changed.emit(v))) return box func _build_layers_panel() -> Control: var box := VBoxContainer.new() var label := Label.new() label.text = "Scene objects appear here." box.add_child(label) return box func _build_timeline_panel() -> Control: var box := VBoxContainer.new() var label := Label.new() label.text = "Keyframe the selected object at the playhead." box.add_child(label) var key_btn := Button.new() key_btn.text = "Add Keyframe" key_btn.custom_minimum_size = Vector2(0, TOUCH_MIN * UI_SCALE) key_btn.pressed.connect(_on_add_keyframe) box.add_child(key_btn) return box func _make_slider(label_text: String, mn: float, mx: float, val: float, on_change: Callable) -> Control: var wrap := VBoxContainer.new() var l := Label.new() l.text = label_text wrap.add_child(l) var s := HSlider.new() s.min_value = mn s.max_value = mx s.step = 0.01 s.value = val s.custom_minimum_size = Vector2(180 * UI_SCALE, TOUCH_MIN * UI_SCALE) s.value_changed.connect(on_change) wrap.add_child(s) return wrap func _build_bottom_bar() -> void: var bar := PanelContainer.new() bar.name = "BottomBar" bar.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) bar.custom_minimum_size = Vector2(0, 56 * UI_SCALE) bar.mouse_filter = Control.MOUSE_FILTER_STOP _ui_root.add_child(bar) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 10) bar.add_child(hbox) _play_button = Button.new() _play_button.text = "Play" _play_button.custom_minimum_size = Vector2(84 * UI_SCALE, TOUCH_MIN * UI_SCALE) _play_button.pressed.connect(_on_play_pressed) hbox.add_child(_play_button) _timeline = HSlider.new() _timeline.min_value = 0.0 _timeline.max_value = 5.0 _timeline.step = 0.01 _timeline.size_flags_horizontal = Control.SIZE_EXPAND_FILL _timeline.custom_minimum_size = Vector2(0, TOUCH_MIN * UI_SCALE) _timeline.value_changed.connect(_on_timeline_scrub) hbox.add_child(_timeline) _status = Label.new() _status.text = "0.00s" hbox.add_child(_status) ## --- Behaviour -------------------------------------------------------------- func _connect_bus() -> void: StudioBus.timeline_playhead.connect(_on_playhead) StudioBus.playback_state_changed.connect(_on_playback_state) StudioBus.brush_changed.connect(_on_brush_changed) MemoryManager.memory_warning.connect(func(_u, _b): _refresh_hud()) func _process(_delta: float) -> void: _fps_label.text = "FPS %d" % Engine.get_frames_per_second() _refresh_hud() func _refresh_hud() -> void: var used: float = MemoryManager.get_loaded_mb() _memory_label.text = "MEM %.0f/%.0f MB" % [used, MemoryManager.asset_budget_mb] func _toggle_panel(id: String) -> void: var panel: PanelContainer = _panels.get(id) if panel == null: return var body := panel.get_node_or_null("VBoxContainer/Body") # PanelContainer -> VBox -> [Button, MarginContainer] var vbox := panel.get_child(0) var body_node := vbox.get_node_or_null("Body") as Control if body_node != null: body_node.visible = not body_node.visible StudioBus.panel_toggled.emit(id, body_node != null and body_node.visible) func _on_tool_pressed(feature_id: String) -> void: # Turning on a sculpt brush means the finger paints; navigation uses 2 fingers. var is_sculpt := feature_id.begins_with("sculpt.") if _touch_camera != null: _touch_camera.set("one_finger_orbit", not is_sculpt) if is_sculpt: var ok := FeatureRegistry.activate("sculpt.draw") if ok: # Ensure the sculpt module is wired to the selected object. var sculpt: Node = find_sculpt_module() if sculpt != null and StudioBus.has_signal("brush_changed"): StudioBus.brush_changed.emit(feature_id) _activate_feature_family(feature_id) _set_status("Tool: " + feature_id) func find_sculpt_module() -> Node: var host := get_parent().get_node_or_null("Features") if host == null: return null return host.get_node_or_null("Feature_sculpt.draw") func _activate_feature_family(feature_id: String) -> void: match feature_id: "mesh.cube": FeatureRegistry.activate("mesh.cube") "rig.skeleton": FeatureRegistry.activate("rig.skeleton") "sim.cloth": FeatureRegistry.activate("sim.cloth") StudioBus.sim_toggled.emit("sim.cloth", true) "anim.timeline": FeatureRegistry.activate("anim.timeline") _: pass func _on_brush_selected(id: String) -> void: _active_brush = id StudioBus.brush_changed.emit(id) _set_status("Brush: " + id) func _on_brush_changed(id: String) -> void: _set_status("Brush: " + id) func _on_play_pressed() -> void: var tl := _get_timeline_module() if tl == null: return if tl.call("is_playing"): tl.call("pause") else: tl.call("play") func _on_timeline_scrub(value: float) -> void: var tl := _get_timeline_module() if tl != null: tl.call("scrub", value) _status.text = "%.2fs" % value func _on_playhead(t: float) -> void: _timeline.set_value_no_signal(t) _status.text = "%.2fs" % t func _on_playback_state(playing: bool) -> void: _play_button.text = "Pause" if playing else "Play" func _on_add_keyframe() -> void: var tl := _get_timeline_module() if tl == null: FeatureRegistry.activate("anim.timeline") tl = _get_timeline_module() if tl == null: return var selected := _selected_object() if selected != null: tl.call("add_keyframe", selected, "position", _timeline.value, selected.position) _set_status("Keyframe added") func _get_timeline_module() -> Node: var host := get_parent().get_node_or_null("Features") if host == null: return null return host.get_node_or_null("Feature_anim.timeline") func _selected_object() -> Node3D: var geom := get_parent().get_node_or_null("Geometry") if geom == null: return null for child in geom.get_children(): if child is MeshInstance3D: return child return null func _set_status(text: String) -> void: if _status != null: _status.text = text