monkeystudio / scripts /studio /studio_ui.gd
geter62's picture
Upload folder using huggingface_hub
84e8204 verified
Raw History Blame Contribute Delete
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 := [
["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