Download scripts/studio/studio_ui.gd from geter62/monkeystudio: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/studio/studio_ui.gd
- Command line
-
hf download hf://geter62/monkeystudio/scripts/studio/studio_ui.gd
-
curl -L -o studio_ui.gd https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/studio/studio_ui.gd
12.1 kB
| 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 := | |
| 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 | |
| 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 | |