From 2ddedcb07091f4ecc89deb7fdc278fbf7fed1044 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 29 Mar 2026 23:25:08 -0700 Subject: [PATCH] =?UTF-8?q?ui(ui):=20=F0=9F=92=84=20Refactor=20and=20updat?= =?UTF-8?q?e=20UI=20system=20in=20Godot=20engine=20to=20improve=20layout?= =?UTF-8?q?=20consistency=20across=20ScreenLayoutControl=20and=20SettingsW?= =?UTF-8?q?indow=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- shared/godot/ui/screen_layout_control.gd | 16 + shared/godot/ui/settings_page_camera.gd | 228 +------------- shared/godot/ui/settings_page_gaze_demo.gd | 280 ++++++++++++++++++ .../godot/ui/settings_page_gaze_demo.gd.uid | 1 + shared/godot/ui/settings_page_general.gd | 6 +- shared/godot/ui/settings_window.gd | 25 +- 6 files changed, 322 insertions(+), 234 deletions(-) create mode 100644 shared/godot/ui/settings_page_gaze_demo.gd create mode 100644 shared/godot/ui/settings_page_gaze_demo.gd.uid diff --git a/shared/godot/ui/screen_layout_control.gd b/shared/godot/ui/screen_layout_control.gd index dfb8f54..1cdb149 100644 --- a/shared/godot/ui/screen_layout_control.gd +++ b/shared/godot/ui/screen_layout_control.gd @@ -27,6 +27,10 @@ var _tf_valid: bool = false ## Companion node — used to get the companion window position accurately. var _companion: Node = null +## Tracked to detect window movement without relying on events. +var _last_win_pos: Vector2 = Vector2.ZERO +var _last_win_size: Vector2 = Vector2.ZERO + func _ready() -> void: mouse_filter = MOUSE_FILTER_STOP @@ -38,6 +42,18 @@ func _ready() -> void: EventBus.face_lost.connect(_on_face_lost) +func _process(_delta: float) -> void: + if _companion == null or not is_instance_valid(_companion): + return + var win: Window = _companion.get_window() + var pos: Vector2 = Vector2(win.position) + var sz: Vector2 = Vector2(win.size) + if pos != _last_win_pos or sz != _last_win_size: + _last_win_pos = pos + _last_win_size = sz + queue_redraw() + + ## Apply a named preset for the camera's physical position. ## Presets: "top-center", "top-left", "top-right", "left", "right", "center" func setup(companion: Node) -> void: diff --git a/shared/godot/ui/settings_page_camera.gd b/shared/godot/ui/settings_page_camera.gd index 40168dd..d575d3f 100644 --- a/shared/godot/ui/settings_page_camera.gd +++ b/shared/godot/ui/settings_page_camera.gd @@ -1,26 +1,13 @@ extends "res://addons/godot-ui/core/page_base.gd" -## Camera settings page — face tracking controls, live status, screen layout diagram, -## and inline WebSocket camera preview streamed from the vision sidecar. +## Camera settings page — face tracking enable/device, and gaze behavior tuning. +## Visualization (preview, status, screen layout) lives in the Gaze Demo page. -const ScreenLayoutControlScript = preload("res://src/ui/screen_layout_control.gd") -const StatusLabelScript = preload("res://addons/godot-ui/feedback/status_label.gd") const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd") const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd") const CheckToggleScript = preload("res://addons/godot-ui/form/check_toggle.gd") const SpinRowScript = preload("res://addons/godot-ui/form/spin_row.gd") const OptionRowScript = preload("res://addons/godot-ui/form/option_row.gd") -const PREVIEW_WS_URL := "ws://127.0.0.1:19703" - - -class _WsPoller: - extends Node - var page: RefCounted - - func _process(_delta: float) -> void: - page._ws_tick() - - var _companion: Node var _gaze_toggle: CheckButton @@ -29,17 +16,6 @@ var _duration_spin: HBoxContainer var _margin_spin: HBoxContainer var _camera_option: OptionButton var _camera_name_label: Label -var _status_row: Node -var _pose_label: Label -var _iris_label: Label -var _layout_control: Control - -var _preview_btn: Button -var _preview_texture_rect: TextureRect -var _preview_texture: ImageTexture -var _ws: WebSocketPeer -var _ws_active: bool = false -var _ws_poller: Node var _cameras_cache: Array = [] @@ -64,60 +40,14 @@ func build() -> Control: _add_header(vbox, "GAZE BEHAVIOR") vbox.add_child(_build_behavior_section()) - vbox.add_child(_thin_sep()) - _add_header(vbox, "STATUS") - vbox.add_child(_build_status_section()) - vbox.add_child(_thin_sep()) - - _add_header(vbox, "PREVIEW") - vbox.add_child(_build_preview_section()) - vbox.add_child(_thin_sep()) - - _add_header(vbox, "SCREEN LAYOUT") - var presets: Array[String] = [ - "Top Center", - "Top Left", - "Top Right", - "Left Side", - "Right Side", - "Center (eye-level)", - ] - var preset_row: HBoxContainer = OptionRowScript.new() - preset_row.setup("Camera Position", presets, 0, false, 130.0) - preset_row.item_selected.connect(_on_preset_selected) - vbox.add_child(preset_row) - - _layout_control = ScreenLayoutControlScript.new() - vbox.add_child(_layout_control) - _layout_control.setup(_companion) - if AppState.get_camera_rect().is_empty(): - _layout_control.apply_preset("top-center") - - _ws_poller = _WsPoller.new() - (_ws_poller as _WsPoller).page = self - page.add_child(_ws_poller) - - EventBus.attention_changed.connect(_on_attention_changed) - EventBus.face_lost.connect(_on_face_lost) - EventBus.face_pose_updated.connect(_on_pose_updated) EventBus.camera_list_updated.connect(_on_camera_list_updated) - _refresh_status() - if AppState.get_camera_enabled(): - _ws_start() _send_tray_msg({"cmd": "list_cameras"}) return page func cleanup() -> void: - _ws_stop() - if EventBus.attention_changed.is_connected(_on_attention_changed): - EventBus.attention_changed.disconnect(_on_attention_changed) - if EventBus.face_lost.is_connected(_on_face_lost): - EventBus.face_lost.disconnect(_on_face_lost) - if EventBus.face_pose_updated.is_connected(_on_pose_updated): - EventBus.face_pose_updated.disconnect(_on_pose_updated) if EventBus.camera_list_updated.is_connected(_on_camera_list_updated): EventBus.camera_list_updated.disconnect(_on_camera_list_updated) @@ -186,112 +116,6 @@ func _build_behavior_section() -> Control: return vbox -func _build_status_section() -> Control: - var vbox: VBoxContainer = VBoxContainer.new() - vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL - vbox.add_theme_constant_override("separation", 5) - - _status_row = StatusLabelScript.new() - (_status_row as StatusLabelScript).setup() - (_status_row as StatusLabelScript).set_status("absent", "No face detected") - vbox.add_child(_status_row) - - _pose_label = Label.new() - _pose_label.text = "Head pose: —" - _pose_label.add_theme_color_override("font_color", UiTheme.text_muted) - _pose_label.add_theme_font_size_override("font_size", 13) - vbox.add_child(_pose_label) - - _iris_label = Label.new() - _iris_label.text = "Iris gaze: —" - _iris_label.add_theme_color_override("font_color", UiTheme.text_muted) - _iris_label.add_theme_font_size_override("font_size", 13) - vbox.add_child(_iris_label) - - return vbox - - -func _build_preview_section() -> Control: - var vbox: VBoxContainer = VBoxContainer.new() - vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL - vbox.add_theme_constant_override("separation", 6) - - _preview_btn = Button.new() - _preview_btn.text = "Preview Camera" - _preview_btn.toggle_mode = true - _preview_btn.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN - _preview_btn.add_theme_font_size_override("font_size", 13) - _preview_btn.toggled.connect(_on_preview_toggled) - vbox.add_child(_preview_btn) - - _preview_texture_rect = TextureRect.new() - _preview_texture_rect.custom_minimum_size = Vector2(640, 360) - _preview_texture_rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _preview_texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL - _preview_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - _preview_texture_rect.visible = false - vbox.add_child(_preview_texture_rect) - - return vbox - - -func _refresh_status() -> void: - if not AppState.get_camera_enabled(): - (_status_row as StatusLabelScript).set_status("absent", "Face gaze disabled") - - -# -- WebSocket preview -------------------------------------------------------- - - -func _ws_start() -> void: - _ws = WebSocketPeer.new() - _ws.connect_to_url(PREVIEW_WS_URL) - _ws_active = true - _preview_texture_rect.visible = true - _preview_btn.text = "Stop Preview" - _preview_btn.set_pressed_no_signal(true) - FlightRecorder.record("camera.preview_started", "Camera preview stream opened") - - -func _on_preview_toggled(on: bool) -> void: - if on: - _ws_start() - else: - _ws_stop() - FlightRecorder.record("camera.preview_stopped", "Camera preview stream closed") - - -func _ws_tick() -> void: - if not _ws_active or _ws == null: - return - _ws.poll() - var state: int = _ws.get_ready_state() - if state == WebSocketPeer.STATE_OPEN: - while _ws.get_available_packet_count() > 0: - var data: PackedByteArray = _ws.get_packet() - var img: Image = Image.new() - if img.load_jpg_from_buffer(data) == OK: - if _preview_texture == null: - _preview_texture = ImageTexture.create_from_image(img) - _preview_texture_rect.texture = _preview_texture - else: - _preview_texture.update(img) - elif state == WebSocketPeer.STATE_CLOSED: - _ws_stop() - - -func _ws_stop() -> void: - _ws_active = false - if _ws != null: - _ws.close() - _ws = null - if _preview_texture_rect != null: - _preview_texture_rect.visible = false - if _preview_btn != null: - _preview_btn.text = "Preview Camera" - _preview_btn.set_pressed_no_signal(false) - - # -- Callbacks ----------------------------------------------------------------- @@ -300,10 +124,6 @@ func _on_gaze_toggled(on: bool) -> void: return AppState.set_camera_enabled(on) _send_tray_msg({"cmd": "set_camera_enabled", "enabled": on}) - if on: - _ws_start() - else: - _ws_stop() ( FlightRecorder . record( @@ -330,50 +150,6 @@ func _on_camera_option_selected(idx: int) -> void: ) -func _on_attention_changed(state: String, confidence: float) -> void: - (_status_row as StatusLabelScript).set_status( - state, "Attention: %s (%.0f%%)" % [state, confidence * 100.0] - ) - - -func _on_face_lost() -> void: - (_status_row as StatusLabelScript).set_status("absent", "No face detected") - _pose_label.text = "Head pose: —" - _pose_label.add_theme_color_override("font_color", UiTheme.text_muted) - _iris_label.text = "Iris gaze: —" - _iris_label.add_theme_color_override("font_color", UiTheme.text_muted) - - -func _on_pose_updated(yaw: float, pitch: float, iris_h: float, iris_v: float) -> void: - _pose_label.text = "Head pose: Yaw %+.1f° Pitch %+.1f°" % [yaw, pitch] - _pose_label.add_theme_color_override("font_color", UiTheme.text_primary) - _iris_label.text = "Iris gaze: H %.2f V %.2f" % [iris_h, iris_v] - _iris_label.add_theme_color_override("font_color", UiTheme.text_primary) - - -func _on_preset_selected(index: int) -> void: - if _layout_control == null: - return - const PRESETS := [ - "top-center", - "top-left", - "top-right", - "left", - "right", - "center", - ] - if index < PRESETS.size(): - _layout_control.apply_preset(PRESETS[index]) - ( - FlightRecorder - . record( - "camera.preset_selected", - "Camera position preset changed", - {"preset": PRESETS[index]}, - ) - ) - - func _on_camera_list_updated(cameras: Array) -> void: _cameras_cache = cameras if _camera_option == null: diff --git a/shared/godot/ui/settings_page_gaze_demo.gd b/shared/godot/ui/settings_page_gaze_demo.gd new file mode 100644 index 0000000..9ec6a9c --- /dev/null +++ b/shared/godot/ui/settings_page_gaze_demo.gd @@ -0,0 +1,280 @@ +extends "res://addons/godot-ui/core/page_base.gd" +## Gaze Demo page — live camera preview, face tracking status, screen layout diagram, +## and desktop gaze halo toggle. All the live visualization for the vision pipeline. + +const ScreenLayoutControlScript = preload("res://src/ui/screen_layout_control.gd") +const StatusLabelScript = preload("res://addons/godot-ui/feedback/status_label.gd") +const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd") +const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd") +const CheckToggleScript = preload("res://addons/godot-ui/form/check_toggle.gd") + +const PREVIEW_WS_URL := "ws://127.0.0.1:19703" +const WS_RETRY_INTERVAL: float = 3.0 + + +class _WsPoller: + extends Node + var page: RefCounted + + func _process(delta: float) -> void: + page._ws_tick(delta) + + +var _companion: Node + +var _preview_texture_rect: TextureRect +var _preview_placeholder: Label +var _preview_texture: ImageTexture +var _ws: WebSocketPeer +var _ws_active: bool = false +var _ws_retry_t: float = 0.0 +var _ws_poller: Node + +var _status_row: Node +var _pose_label: Label +var _iris_label: Label +var _layout_control: Control +var _halo_toggle: CheckButton + +var _page_control: Control + + +func setup(companion: Node) -> void: + _companion = companion + + +func build() -> Control: + var page: ScrollContainer = ScrollPageScript.new() + page.setup() + var vbox: VBoxContainer = page.content + _page_control = page + + _add_header(vbox, "CAMERA PREVIEW") + vbox.add_child(_build_preview_container()) + vbox.add_child(_thin_sep()) + + _add_header(vbox, "STATUS") + vbox.add_child(_build_status_section()) + vbox.add_child(_thin_sep()) + + _add_header(vbox, "SCREEN LAYOUT") + _layout_control = ScreenLayoutControlScript.new() + vbox.add_child(_layout_control) + _layout_control.setup(_companion) + vbox.add_child(_thin_sep()) + + _add_header(vbox, "GAZE HALO") + _halo_toggle = CheckToggleScript.new() + _halo_toggle.setup("Show gaze halo on screen", AppState.get_companion("gaze_halo", true)) + _halo_toggle.toggled.connect(_on_halo_toggled) + vbox.add_child(_halo_toggle) + + _ws_poller = _WsPoller.new() + (_ws_poller as _WsPoller).page = self + page.add_child(_ws_poller) + + EventBus.attention_changed.connect(_on_attention_changed) + EventBus.face_lost.connect(_on_face_lost) + EventBus.face_pose_updated.connect(_on_pose_updated) + + page.visibility_changed.connect(_on_visibility_changed) + + return page + + +func cleanup() -> void: + _ws_stop() + if EventBus.attention_changed.is_connected(_on_attention_changed): + EventBus.attention_changed.disconnect(_on_attention_changed) + if EventBus.face_lost.is_connected(_on_face_lost): + EventBus.face_lost.disconnect(_on_face_lost) + if EventBus.face_pose_updated.is_connected(_on_pose_updated): + EventBus.face_pose_updated.disconnect(_on_pose_updated) + + +# -- Helpers ------------------------------------------------------------------- + + +func _add_header(parent: VBoxContainer, text: String) -> void: + var header: Label = SectionHeaderScript.new() + header.setup(text) + parent.add_child(header) + + +func _build_preview_container() -> Control: + # Fixed-height container: dark background + placeholder label + video texture. + var container: Control = Control.new() + container.custom_minimum_size = Vector2(0, 360) + container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + var bg: ColorRect = ColorRect.new() + bg.color = Color(0.08, 0.08, 0.08, 1.0) + bg.set_anchors_preset(Control.PRESET_FULL_RECT) + container.add_child(bg) + + _preview_placeholder = Label.new() + _preview_placeholder.text = "Connecting to camera preview…" + _preview_placeholder.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _preview_placeholder.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _preview_placeholder.add_theme_color_override("font_color", UiTheme.text_muted) + _preview_placeholder.add_theme_font_size_override("font_size", 13) + _preview_placeholder.set_anchors_preset(Control.PRESET_FULL_RECT) + container.add_child(_preview_placeholder) + + _preview_texture_rect = TextureRect.new() + _preview_texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL + _preview_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + _preview_texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + _preview_texture_rect.visible = false + container.add_child(_preview_texture_rect) + + return container + + +func _build_status_section() -> Control: + var vbox: VBoxContainer = VBoxContainer.new() + vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + vbox.add_theme_constant_override("separation", 5) + + _status_row = StatusLabelScript.new() + (_status_row as StatusLabelScript).setup() + (_status_row as StatusLabelScript).set_status("absent", "No face detected") + vbox.add_child(_status_row) + + _pose_label = Label.new() + _pose_label.text = "Head pose: —" + _pose_label.add_theme_color_override("font_color", UiTheme.text_muted) + _pose_label.add_theme_font_size_override("font_size", 13) + vbox.add_child(_pose_label) + + _iris_label = Label.new() + _iris_label.text = "Iris gaze: —" + _iris_label.add_theme_color_override("font_color", UiTheme.text_muted) + _iris_label.add_theme_font_size_override("font_size", 13) + vbox.add_child(_iris_label) + + return vbox + + +# -- WebSocket preview --------------------------------------------------------- + + +func _ws_start() -> void: + if _ws_active: + return + _ws_retry_t = 0.0 + _ws = WebSocketPeer.new() + _ws.connect_to_url(PREVIEW_WS_URL) + _ws_active = true + _set_placeholder("Connecting to camera preview…") + FlightRecorder.record("gaze_demo.preview_started", "Gaze demo preview stream opened") + + +func _ws_tick(delta: float) -> void: + if not _ws_active: + if _ws_retry_t > 0.0: + _ws_retry_t -= delta + if _ws_retry_t <= 0.0 and _page_control != null and _page_control.visible: + _ws_start() + return + + _ws.poll() + var state: int = _ws.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + while _ws.get_available_packet_count() > 0: + var data: PackedByteArray = _ws.get_packet() + var img: Image = Image.new() + if img.load_jpg_from_buffer(data) == OK: + if _preview_texture == null: + _preview_texture = ImageTexture.create_from_image(img) + _preview_texture_rect.texture = _preview_texture + else: + _preview_texture.update(img) + # Show video, hide placeholder on first successful frame. + if not _preview_texture_rect.visible: + _preview_texture_rect.visible = true + _preview_placeholder.visible = false + elif state == WebSocketPeer.STATE_CLOSED: + _ws_closed() + + +func _ws_closed() -> void: + _ws_active = false + if _ws != null: + _ws.close() + _ws = null + _preview_texture_rect.visible = false + _preview_texture = null + if _preview_texture_rect != null: + _preview_texture_rect.texture = null + _ws_retry_t = WS_RETRY_INTERVAL + _set_placeholder("No camera signal — retrying…") + + +func _ws_stop() -> void: + _ws_active = false + _ws_retry_t = 0.0 + if _ws != null: + _ws.close() + _ws = null + if _preview_texture_rect != null: + _preview_texture_rect.visible = false + _preview_texture_rect.texture = null + _preview_texture = null + if _preview_placeholder != null: + _set_placeholder("Camera preview stopped.") + FlightRecorder.record("gaze_demo.preview_stopped", "Gaze demo preview stream closed") + + +func _set_placeholder(text: String) -> void: + if _preview_placeholder == null: + return + _preview_placeholder.text = text + _preview_placeholder.visible = true + + +# -- Callbacks ----------------------------------------------------------------- + + +func _on_visibility_changed() -> void: + if _page_control == null: + return + if _page_control.visible: + _ws_start() + _sync_face_state() + else: + _ws_stop() + + +func _sync_face_state() -> void: + if _companion == null: + return + var tray: Node = _companion.get_node_or_null("TrayListener") + if tray != null and tray.has_method("sync_face_state"): + tray.sync_face_state() + + +func _on_attention_changed(state: String, confidence: float) -> void: + (_status_row as StatusLabelScript).set_status( + state, "Attention: %s (%.0f%%)" % [state, confidence * 100.0] + ) + + +func _on_face_lost() -> void: + (_status_row as StatusLabelScript).set_status("absent", "No face detected") + _pose_label.text = "Head pose: —" + _pose_label.add_theme_color_override("font_color", UiTheme.text_muted) + _iris_label.text = "Iris gaze: —" + _iris_label.add_theme_color_override("font_color", UiTheme.text_muted) + + +func _on_pose_updated(yaw: float, pitch: float, iris_h: float, iris_v: float) -> void: + _pose_label.text = "Head pose: Yaw %+.1f° Pitch %+.1f°" % [yaw, pitch] + _pose_label.add_theme_color_override("font_color", UiTheme.text_primary) + _iris_label.text = "Iris gaze: H %.2f V %.2f" % [iris_h, iris_v] + _iris_label.add_theme_color_override("font_color", UiTheme.text_primary) + + +func _on_halo_toggled(on: bool) -> void: + AppState.set_companion("gaze_halo", on) + EventBus.gaze_halo_toggled.emit(on) diff --git a/shared/godot/ui/settings_page_gaze_demo.gd.uid b/shared/godot/ui/settings_page_gaze_demo.gd.uid new file mode 100644 index 0000000..5af1bae --- /dev/null +++ b/shared/godot/ui/settings_page_gaze_demo.gd.uid @@ -0,0 +1 @@ +uid://beup06a8fn3j1 diff --git a/shared/godot/ui/settings_page_general.gd b/shared/godot/ui/settings_page_general.gd index f28f671..fd10bb5 100644 --- a/shared/godot/ui/settings_page_general.gd +++ b/shared/godot/ui/settings_page_general.gd @@ -74,11 +74,7 @@ func _add_toggle(parent: VBoxContainer, label_text: String, initial: bool) -> Ch func _get_snap_enabled() -> bool: - if _companion != null: - var snap: Node = _companion.get_node_or_null("EdgeSnap") - if snap != null: - return snap.enabled - return false + return AppState.get_snap_enabled() func _get_zoom() -> float: diff --git a/shared/godot/ui/settings_window.gd b/shared/godot/ui/settings_window.gd index 0c5e594..b20d5ff 100644 --- a/shared/godot/ui/settings_window.gd +++ b/shared/godot/ui/settings_window.gd @@ -8,6 +8,7 @@ const BackendPageScript = preload("res://src/ui/settings_page_backend.gd") const TtsPageScript = preload("res://src/ui/settings_page_tts.gd") const SoundsPageScript = preload("res://src/ui/settings_page_sounds.gd") const CameraPageScript = preload("res://src/ui/settings_page_camera.gd") +const GazeDemoPageScript = preload("res://src/ui/settings_page_gaze_demo.gd") const PersonalityPageScript = preload("res://src/ui/settings_page_personality.gd") const AnimationsPageScript = preload("res://src/ui/settings_page_animations.gd") @@ -15,6 +16,9 @@ const _DEFAULT_SIZE := Vector2i(660, 520) const _DEFAULT_MIN := Vector2i(620, 480) const _CAMERA_SIZE := Vector2i(900, 580) const _CAMERA_MIN := Vector2i(860, 480) +## Gaze Demo needs room for the 640×360 preview + status + diagram + halo toggle. +const _GAZE_DEMO_SIZE := Vector2i(900, 940) +const _GAZE_DEMO_MIN := Vector2i(860, 860) var _companion: Node var _sound_config: Node @@ -23,6 +27,7 @@ var _general_page: RefCounted var _backend_page: RefCounted var _tts_page: RefCounted var _camera_page: RefCounted +var _gaze_demo_page: RefCounted var _sidebar_nav: PanelContainer var _pages: Dictionary = {} @@ -49,6 +54,8 @@ func _ready() -> void: func _on_theme_changed() -> void: if _camera_page != null: _camera_page.cleanup() + if _gaze_demo_page != null: + _gaze_demo_page.cleanup() super._on_theme_changed() @@ -85,6 +92,7 @@ func _build_ui() -> void: {"key": "sounds", "label": "Sounds"}, {"key": "animations", "label": "Animations"}, {"key": "camera", "label": "Camera"}, + {"key": "gaze_demo", "label": "Gaze Demo"}, ] _sidebar_nav.setup(tabs) _sidebar_nav.tab_selected.connect(_switch_tab) @@ -113,6 +121,7 @@ func _build_page_area() -> Control: _pages["sounds"] = _build_sounds_page() _pages["animations"] = _build_animations_page() _pages["camera"] = _build_camera_page() + _pages["gaze_demo"] = _build_gaze_demo_page() for key: String in _pages: var page: Control = _pages[key] @@ -192,6 +201,12 @@ func _build_camera_page() -> Control: return _camera_page.build() +func _build_gaze_demo_page() -> Control: + _gaze_demo_page = GazeDemoPageScript.new() + _gaze_demo_page.setup(_companion) + return _gaze_demo_page.build() + + # -- Tab switching ------------------------------------------------------------ @@ -206,7 +221,10 @@ func _switch_tab(tab_key: String) -> void: func _update_panel_size() -> void: - if _active_tab == "camera": + if _active_tab == "gaze_demo": + min_size = _GAZE_DEMO_MIN + size = _GAZE_DEMO_SIZE + elif _active_tab == "camera": min_size = _CAMERA_MIN size = _CAMERA_SIZE else: @@ -241,10 +259,11 @@ func _on_halo_toggled(on: bool) -> void: func _on_snap_toggled(on: bool) -> void: + AppState.set_snap_enabled(on) if _companion != null: - var snap := _companion.get_node_or_null("EdgeSnap") + var snap: Node = _companion.get_node_or_null("EdgeSnap") if snap != null: - snap.enabled = on + snap.set("enabled", on) func _on_zoom_changed(value: float) -> void: