From 54d42dade93602f4753a36fb146e6ad78c207335 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 26 Sep 2026 13:41:07 +0200 Subject: [PATCH] Add per-pod orientation gizmo to the API demo. Shows live accel attitude with a front-view cylinder (flat / facing / steer) and a TOP marker that rotates with the mesh. Co-authored-by: Cursor --- project.godot | 2 +- scenes/orientation_gizmo.tscn | 76 ++++++++++++++++++++++++++++++++ scripts/demo.gd | 27 ++++++++++-- scripts/orientation_gizmo.gd | 71 +++++++++++++++++++++++++++++ scripts/orientation_gizmo.gd.uid | 1 + 5 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 scenes/orientation_gizmo.tscn create mode 100644 scripts/orientation_gizmo.gd create mode 100644 scripts/orientation_gizmo.gd.uid diff --git a/project.godot b/project.godot index 66ae2ed..5ee3978 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="alox_test_game" run/main_scene="res://scenes/menu.tscn" -config/features=PackedStringArray("4.6", "GL Compatibility") +config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://icon.svg" [display] diff --git a/scenes/orientation_gizmo.tscn b/scenes/orientation_gizmo.tscn new file mode 100644 index 0000000..0078f1b --- /dev/null +++ b/scenes/orientation_gizmo.tscn @@ -0,0 +1,76 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://scripts/orientation_gizmo.gd" id="1_gizmo"] + +[sub_resource type="CylinderMesh" id="CylinderMesh_body"] +top_radius = 0.35 +bottom_radius = 0.35 +height = 1.1 +radial_segments = 24 + +[sub_resource type="StandardMaterial3D" id="Mat_body"] +albedo_color = Color(0.42, 0.48, 0.58, 1) +roughness = 0.55 + +[sub_resource type="CylinderMesh" id="CylinderMesh_cap"] +top_radius = 0.38 +bottom_radius = 0.38 +height = 0.08 +radial_segments = 24 + +[sub_resource type="StandardMaterial3D" id="Mat_top"] +albedo_color = Color(1, 0.55, 0.12, 1) +roughness = 0.4 +emission_enabled = true +emission = Color(0.85, 0.4, 0.05, 1) +emission_energy_multiplier = 0.35 + +[node name="OrientationGizmo" type="SubViewportContainer"] +custom_minimum_size = Vector2(220, 220) +stretch = true +script = ExtResource("1_gizmo") + +[node name="SubViewport" type="SubViewport" parent="."] +own_world_3d = true +transparent_bg = true +handle_input_locally = false +size = Vector2i(220, 220) +render_target_update_mode = 4 + +[node name="Camera3D" type="Camera3D" parent="SubViewport"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3.2) +projection = 1 +size = 2.6 +near = 0.05 +far = 50.0 + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="SubViewport"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 4) +light_energy = 1.2 +shadow_enabled = false + +[node name="FillLight" type="DirectionalLight3D" parent="SubViewport"] +transform = Transform3D(-0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, 0.612372, -0.612372, 0, 0, 0) +light_energy = 0.4 +shadow_enabled = false + +[node name="Pod" type="Node3D" parent="SubViewport"] + +[node name="Body" type="MeshInstance3D" parent="SubViewport/Pod"] +mesh = SubResource("CylinderMesh_body") +surface_material_override/0 = SubResource("Mat_body") + +[node name="TopCap" type="MeshInstance3D" parent="SubViewport/Pod"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.59, 0) +mesh = SubResource("CylinderMesh_cap") +surface_material_override/0 = SubResource("Mat_top") + +[node name="TopLabel" type="Label3D" parent="SubViewport/Pod"] +transform = Transform3D(1, 0, 0, 0, 0, -1, 0, 1, 0, 0, -0.66, 0) +pixel_size = 0.01 +billboard = 0 +text = "TOP" +font_size = 42 +outline_size = 8 +modulate = Color(1, 0.95, 0.85, 1) +outline_modulate = Color(0.05, 0.05, 0.08, 1) diff --git a/scripts/demo.gd b/scripts/demo.gd index ff0946f..0db7a5f 100644 --- a/scripts/demo.gd +++ b/scripts/demo.gd @@ -12,6 +12,7 @@ const PANEL_BORDER_FLASH := Color(1.0, 0.82, 0.15, 1.0) const LED_MODES: PackedStringArray = ["color", "clear", "progress", "digit", "blink", "find-me"] const LED_MODES_COLOR_INTENSITY: PackedStringArray = ["color", "progress", "digit", "blink"] +const ORIENTATION_GIZMO_SCENE := preload("res://scenes/orientation_gizmo.tscn") @onready var status_label: Label = $UiLayer/TopBar/StatusLabel @onready var log_output: TextEdit = $UiLayer/MainSplit/LogPanel/LogOutput @@ -122,6 +123,7 @@ func _stop_stream() -> void: "triple": false, }) ui["values_label"].text = "—" + ui["orient_gizmo"].clear() _streaming = false start_stream_btn.disabled = _client_ui.is_empty() @@ -328,14 +330,26 @@ func _populate_clients(clients: Array) -> void: _add_led_preset_btn(led_presets, "Blau", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(0.2, 0.4, 1), 128)) _add_led_preset_btn(led_presets, "Find", cid, available, func() -> void: _led_apply_preset(cid, "find-me")) + var accel_row := HBoxContainer.new() + accel_row.add_theme_constant_override("separation", 12) + accel_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + vbox.add_child(accel_row) + var values_label := Label.new() values_label.text = "—" values_label.add_theme_font_size_override("font_size", 12) values_label.add_theme_color_override("font_color", Color(0.65, 0.85, 0.75)) values_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - vbox.add_child(values_label) + values_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + values_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + accel_row.add_child(values_label) + + var orient_gizmo: OrientationGizmo = ORIENTATION_GIZMO_SCENE.instantiate() + accel_row.add_child(orient_gizmo) clients_list.add_child(panel) + # clear() after enter_tree so @onready nodes exist; _ready also clears. + orient_gizmo.clear() _client_ui[cid] = { "available": available, @@ -360,6 +374,7 @@ func _populate_clients(clients: Array) -> void: "led_digit_row": led_digit_row, "led_blink_row": led_blink_row, "values_label": values_label, + "orient_gizmo": orient_gizmo, } _update_led_fields(cid) @@ -443,14 +458,18 @@ func _on_input(data: Dictionary) -> void: var ui: Dictionary = _client_ui[cid] var lines: PackedStringArray = [] if client.get("valid", false): + var ax := int(client.get("x", 0)) + var ay := int(client.get("y", 0)) + var az := int(client.get("z", 0)) lines.append( "x=%d y=%d z=%d (age %sms)" % [ - int(client.get("x", 0)), - int(client.get("y", 0)), - int(client.get("z", 0)), + ax, ay, az, str(client.get("accel_age_ms", "?")), ] ) + ui["orient_gizmo"].set_accel(Vector3(float(ax), float(ay), float(az))) + else: + ui["orient_gizmo"].clear() var tap_kind := str(client.get("tap_kind", "none")) if tap_kind != "none": diff --git a/scripts/orientation_gizmo.gd b/scripts/orientation_gizmo.gd new file mode 100644 index 0000000..3fdf661 --- /dev/null +++ b/scripts/orientation_gizmo.gd @@ -0,0 +1,71 @@ +extends SubViewportContainer +class_name OrientationGizmo + +const MIN_ACCEL_LEN := 1000.0 +const SMOOTH := 0.25 +const VIEW_SIZE := Vector2(220, 220) +const ACTIVE_MODULATE := Color(1, 1, 1, 1) +const IDLE_MODULATE := Color(0.55, 0.58, 0.65, 0.85) + +## Mesh TOP = local −Y. +## Flat accel ≈ +Z → side view, TOP at top of screen +## Facing accel ≈ −Y → TOP face toward camera +## Steer accel ≈ −X → facing + roll left (steering wheel) +var _basis_flat: Basis = Basis(Quaternion(Vector3.RIGHT, PI)) +var _basis_facing: Basis = Basis(Quaternion(Vector3.RIGHT, -PI * 0.5)) + +@onready var _viewport: SubViewport = $SubViewport +@onready var _camera: Camera3D = $SubViewport/Camera3D +@onready var _pod: Node3D = $SubViewport/Pod + +var _has_sample := false +var _target_basis := Basis.IDENTITY + + +func _ready() -> void: + custom_minimum_size = VIEW_SIZE + stretch = true + _viewport.own_world_3d = true + _viewport.transparent_bg = true + _viewport.size = Vector2i(VIEW_SIZE) + _viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS + _camera.projection = Camera3D.PROJECTION_ORTHOGONAL + _camera.size = 2.6 + _camera.near = 0.05 + _camera.far = 50.0 + _camera.position = Vector3(0.0, 0.0, 3.2) + _camera.look_at(Vector3.ZERO, Vector3.UP) + clear() + + +func set_accel(v: Vector3) -> void: + if v.length() < MIN_ACCEL_LEN: + return + _has_sample = true + modulate = ACTIVE_MODULATE + _target_basis = _basis_from_accel(v) + + +func clear() -> void: + _has_sample = false + _target_basis = _basis_flat + if _pod != null: + _pod.basis = _basis_flat + modulate = IDLE_MODULATE + + +func _process(_delta: float) -> void: + if not _has_sample: + return + _pod.basis = _pod.basis.slerp(_target_basis, SMOOTH) + + +func _basis_from_accel(accel: Vector3) -> Basis: + var n := accel.normalized() + # 0 = liegend (+Z), 1 = aufgerichtet / gedreht (wenig +Z) + var face_t := clampf(1.0 - maxf(n.z, 0.0), 0.0, 1.0) + var b := _basis_flat.slerp(_basis_facing, face_t) + # Lenkrad-Roll in der Sensor-XY-Ebene: + # facing (0,−1) → 0; links (−1,0) → +π/2; rechts (+1,0) → −π/2 + var roll := atan2(-n.x, -n.y) + return Basis(Quaternion(Vector3.BACK, roll * face_t)) * b diff --git a/scripts/orientation_gizmo.gd.uid b/scripts/orientation_gizmo.gd.uid new file mode 100644 index 0000000..3b596e7 --- /dev/null +++ b/scripts/orientation_gizmo.gd.uid @@ -0,0 +1 @@ +uid://djoqip111hnbd