ui(ui): 💄 Refactor and update UI system in Godot engine to improve layout consistency across ScreenLayoutControl and SettingsWindow classes
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
47eeb4cbe7
commit
2ddedcb070
6 changed files with 322 additions and 234 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
280
shared/godot/ui/settings_page_gaze_demo.gd
Normal file
280
shared/godot/ui/settings_page_gaze_demo.gd
Normal file
|
|
@ -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)
|
||||
1
shared/godot/ui/settings_page_gaze_demo.gd.uid
Normal file
1
shared/godot/ui/settings_page_gaze_demo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://beup06a8fn3j1
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue