From f05a6148fb093f6ba9f4cf802121e20757db0a4f Mon Sep 17 00:00:00 2001 From: RedNicStone Date: Sat, 7 Nov 2026 21:31:14 +0100 Subject: [PATCH] Added motion planel for raw/pitch/roll and acceleration. Consolidated all colors into one stylesheet in themes/palette.gd. --- components/motion_panel.tscn | 332 +++++++++++++++++++++++++++++++ docs/colors.md | 13 ++ docs/motion_panel.md | 20 ++ scenes/motion_demo.tscn | 22 ++ scripts/motion_demo.gd | 18 ++ scripts/motion_demo.gd.uid | 1 + scripts/motion_instrument.gd | 133 +++++++++++++ scripts/motion_instrument.gd.uid | 1 + scripts/motion_panel.gd | 63 ++++++ scripts/motion_panel.gd.uid | 1 + themes/compass_material.tres | 10 + themes/palette.gd | 57 ++++++ themes/palette.gd.uid | 1 + 13 files changed, 672 insertions(+) create mode 100644 components/motion_panel.tscn create mode 100644 docs/colors.md create mode 100644 docs/motion_panel.md create mode 100644 scenes/motion_demo.tscn create mode 100644 scripts/motion_demo.gd create mode 100644 scripts/motion_demo.gd.uid create mode 100644 scripts/motion_instrument.gd create mode 100644 scripts/motion_instrument.gd.uid create mode 100644 scripts/motion_panel.gd create mode 100644 scripts/motion_panel.gd.uid create mode 100644 themes/compass_material.tres create mode 100644 themes/palette.gd create mode 100644 themes/palette.gd.uid diff --git a/components/motion_panel.tscn b/components/motion_panel.tscn new file mode 100644 index 0000000..dc2c5fb --- /dev/null +++ b/components/motion_panel.tscn @@ -0,0 +1,332 @@ +[gd_scene format=3 uid="uid://dvljqi38sjimi"] + +[ext_resource type="Script" uid="uid://dh2vnk35potq3" path="res://scripts/motion_panel.gd" id="1"] +[ext_resource type="Script" uid="uid://1dn362pyibkx" path="res://scripts/motion_instrument.gd" id="2"] + +[node name="MotionPanel" type="Control" unique_id=660733185] +custom_minimum_size = Vector2(600, 410) +custom_maximum_size = Vector2(600, 410) +layout_mode = 3 +anchors_preset = 0 +offset_right = 600.0 +offset_bottom = 440.0 +tooltip_text = "Orientation: roll right side down, pitch nose up, heading clockwise from north. +Body axes: X forward, Y right, Z down. Acceleration excludes gravity." +script = ExtResource("1") + +[node name="Background" type="Panel" parent="." unique_id=144412302] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_type_variation = &"MotionBackground" + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=246886683] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 16.0 +offset_top = 8.0 +offset_right = -16.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 4 + +[node name="GridContainer" type="GridContainer" parent="VBoxContainer" unique_id=682224599] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 8 +columns = 3 + +[node name="Attitude" type="VBoxContainer" parent="VBoxContainer/GridContainer" unique_id=200553262] +custom_minimum_size = Vector2(244, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 2 + +[node name="MarginContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/Attitude" unique_id=1592037194] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/GridContainer/Attitude/MarginContainer" unique_id=678832939] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "ATTITUDE deg" + +[node name="DialAspect" type="MarginContainer" parent="VBoxContainer/GridContainer/Attitude" unique_id=1404917279] +custom_minimum_size = Vector2(0, 264) +layout_mode = 2 +size_flags_vertical = 0 + +[node name="Horizon" type="Control" parent="VBoxContainer/GridContainer/Attitude/DialAspect" unique_id=1698568859] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 2 +script = ExtResource("2") + +[node name="OrientationReadouts" type="HBoxContainer" parent="VBoxContainer/GridContainer/Attitude" unique_id=185739847] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Roll" type="Label" parent="VBoxContainer/GridContainer/Attitude/OrientationReadouts" unique_id=818199825] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 19 +text = "R —°" + +[node name="Pitch" type="Label" parent="VBoxContainer/GridContainer/Attitude/OrientationReadouts" unique_id=25151996] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 19 +text = "P —°" +horizontal_alignment = 2 + +[node name="HeadingTape" type="Control" parent="VBoxContainer/GridContainer/Attitude" unique_id=736154972] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +mouse_filter = 2 +script = ExtResource("2") +instrument = 1 + +[node name="Heading" type="Label" parent="VBoxContainer/GridContainer/Attitude" unique_id=1630510362] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "H —" +horizontal_alignment = 1 + +[node name="CategorySeparator" type="VSeparator" parent="VBoxContainer/GridContainer" unique_id=1838523344] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 1 +theme_type_variation = &"MotionVSeparator" + +[node name="Acceleration" type="VBoxContainer" parent="VBoxContainer/GridContainer" unique_id=1201786358] +custom_minimum_size = Vector2(307, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 2 + +[node name="MarginContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration" unique_id=1329156619] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/GridContainer/Acceleration/MarginContainer" unique_id=2085073917] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 18 +text = "ACCEL m/s²" + +[node name="FullScale" type="Label" parent="VBoxContainer/GridContainer/Acceleration/MarginContainer" unique_id=248494772] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "±5.0" + +[node name="Limit" type="Label" parent="VBoxContainer/GridContainer/Acceleration/MarginContainer" unique_id=1467570655] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_type_variation = &"MotionWarning" +theme_override_font_sizes/font_size = 18 +text = "LIMIT" + +[node name="Plots" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration" unique_id=127821710] +custom_minimum_size = Vector2(0, 264) +layout_mode = 2 +size_flags_vertical = 0 +theme_override_constants/separation = 4 + +[node name="AccelerationPlot" type="Control" parent="VBoxContainer/GridContainer/Acceleration/Plots" unique_id=383346065 node_paths=PackedStringArray("radius_source")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 2 +script = ExtResource("2") +instrument = 2 +radius_source = NodePath("../../../Attitude/DialAspect/Horizon") + +[node name="VerticalAxis" type="Control" parent="VBoxContainer/GridContainer/Acceleration/Plots" unique_id=1631611633] +custom_minimum_size = Vector2(24, 0) +layout_mode = 2 + +[node name="Axis" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Plots/VerticalAxis" unique_id=1652971232] +layout_mode = 0 +offset_right = 24.0 +offset_bottom = 26.0 +theme_override_font_sizes/font_size = 18 +text = "−Z" +horizontal_alignment = 1 + +[node name="VerticalAcceleration" type="Control" parent="VBoxContainer/GridContainer/Acceleration/Plots/VerticalAxis" unique_id=786328417 node_paths=PackedStringArray("radius_source")] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 2 +script = ExtResource("2") +instrument = 3 +radius_source = NodePath("../../AccelerationPlot") + +[node name="Readouts" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration" unique_id=1041397004] +custom_minimum_size = Vector2(260, 0) +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_constants/separation = 8 + +[node name="X" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Readouts" unique_id=1850756267] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="AxisX" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/X" unique_id=1279485247] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "X" + +[node name="AccelX" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/X" unique_id=528426129] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="Y" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Readouts" unique_id=1540446279] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="AxisY" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/Y" unique_id=1789229948] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "Y" + +[node name="AccelY" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/Y" unique_id=1811878137] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="Z" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Readouts" unique_id=449449369] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="AxisZ" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/Z" unique_id=1812289041] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "Z" + +[node name="AccelZ" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Readouts/Z" unique_id=648512841] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="MomentumSeparator" type="HSeparator" parent="VBoxContainer/GridContainer/Acceleration" unique_id=498814282] +custom_minimum_size = Vector2(0, 12) +layout_mode = 2 +theme_override_constants/separation = 4 +theme_type_variation = &"MotionHSeparator" + +[node name="MomentumTitle" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration" unique_id=1176164218] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="VBoxContainer/GridContainer/Acceleration/MomentumTitle" unique_id=1226461986] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "ANG. MOMENTUM" + +[node name="Units" type="Label" parent="VBoxContainer/GridContainer/Acceleration/MomentumTitle" unique_id=1830199918] +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "kg·m²/s" + +[node name="Momentum" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration" unique_id=276201290] +custom_minimum_size = Vector2(260, 0) +layout_mode = 2 +size_flags_horizontal = 4 +theme_override_constants/separation = 8 + +[node name="X" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Momentum" unique_id=166270987] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="Axis" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/X" unique_id=243294341] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "X" + +[node name="MomentumX" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/X" unique_id=210690849] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="Y" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Momentum" unique_id=722649907] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="Axis" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/Y" unique_id=1590278938] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "Y" + +[node name="MomentumY" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/Y" unique_id=349989084] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="Z" type="HBoxContainer" parent="VBoxContainer/GridContainer/Acceleration/Momentum" unique_id=517849282] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="Axis" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/Z" unique_id=1648789922] +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "Z" + +[node name="MomentumZ" type="Label" parent="VBoxContainer/GridContainer/Acceleration/Momentum/Z" unique_id=1970978687] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 19 +text = "—" + +[node name="StatusBanner" type="PanelContainer" parent="." unique_id=1814858071] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 44.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"MotionAlertPanel" + +[node name="Status" type="Label" parent="StatusBanner" unique_id=1561328163] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"MotionAlertText" +theme_override_font_sizes/font_size = 22 +text = "NO DATA" +horizontal_alignment = 1 diff --git a/docs/colors.md b/docs/colors.md new file mode 100644 index 0000000..4f6eba3 --- /dev/null +++ b/docs/colors.md @@ -0,0 +1,13 @@ +# Colors + +Edit `themes/palette.gd`: base swatches define the hues; derived shades use `lightened()`, `darkened()`, and `lerp()`. `SOFT`, `HIGH_CONTRAST`, and `INSTRUMENT` map them to theme roles. Treat derived static values as read-only. + +Regenerate after palette edits: + +```sh +godot --headless --path . --script themes/build_theme.gd +``` + +Commit the generated themes and compass material alongside palette changes. The generator also updates the viewport background when needed. + +Controls inherit the active theme. Custom graphics use `get_theme_color(role, "SurfaceSide")` for instrument roles and `get_theme_color("font_color", "Label")` for text. Prefer existing roles before adding new ones. diff --git a/docs/motion_panel.md b/docs/motion_panel.md new file mode 100644 index 0000000..3d48d14 --- /dev/null +++ b/docs/motion_panel.md @@ -0,0 +1,20 @@ +# Motion instrument + +Instance `components/motion_panel.tscn`. Run `scenes/motion_demo.tscn` with F6 for simulated telemetry; pause the feed to preview stale status. + +```gdscript +$MotionPanel.set_telemetry( + Vector3(roll_degrees, pitch_degrees, heading_degrees), + Vector3(momentum_x, momentum_y, momentum_z), + Vector3(accel_x, accel_y, accel_z) +) +``` + +- Orientation: positive roll right side down, positive pitch nose up, heading clockwise from north (0–360°). +- Body axes: X forward, Y right, Z down. +- Angular momentum: kg·m²/s, distinct from gyroscope angular velocity. +- Acceleration: m/s², with gravity removed upstream. + +Missing values show dashes; non-finite samples are rejected. After `stale_after_seconds` (default 2), the banner marks held values STALE. `acceleration_full_scale` sets plot limits; numeric values remain unclamped. + +Edit layout and Labels in the scene tree. Preserve scene-unique names used by `motion_panel.gd`. The four graphics use `motion_instrument.gd`; `radius_source` keeps the circles and Z scale aligned. Colors inherit the active theme; see [colors.md](colors.md). diff --git a/scenes/motion_demo.tscn b/scenes/motion_demo.tscn new file mode 100644 index 0000000..946c6bb --- /dev/null +++ b/scenes/motion_demo.tscn @@ -0,0 +1,22 @@ +[gd_scene format=3 uid="uid://b8u5uwdoxxvb1"] + +[ext_resource type="PackedScene" uid="uid://dvljqi38sjimi" path="res://components/motion_panel.tscn" id="1"] +[ext_resource type="Script" uid="uid://c5mncovlnarve" path="res://scripts/motion_demo.gd" id="2"] + +[node name="MotionDemo" type="VBoxContainer" unique_id=1608361665] +offset_left = 24.0 +offset_top = 24.0 +offset_right = 624.0 +offset_bottom = 530.0 +script = ExtResource("2") + +[node name="DemoNotice" type="Label" parent="." unique_id=937059766] +layout_mode = 2 +text = "SIMULATED TELEMETRY / motion instrument preview" + +[node name="MotionPanel" parent="." unique_id=843107870 instance=ExtResource("1")] +layout_mode = 2 + +[node name="Pause" type="CheckButton" parent="." unique_id=103027158] +layout_mode = 2 +text = "Pause feed (shows stale state after 2 seconds)" diff --git a/scripts/motion_demo.gd b/scripts/motion_demo.gd new file mode 100644 index 0000000..4039717 --- /dev/null +++ b/scripts/motion_demo.gd @@ -0,0 +1,18 @@ +extends VBoxContainer + +var elapsed := 0.0 +var running := true +@onready var instrument = $MotionPanel + +func _ready() -> void: + $Pause.toggled.connect(func(paused: bool) -> void: running = not paused) + +func _process(delta: float) -> void: + if not running: + return + elapsed += delta + instrument.set_telemetry( + Vector3(24 * sin(elapsed * 0.5), 18 * sin(elapsed * 0.3), fposmod(350 + elapsed * 4, 360)), + Vector3(1.8 * cos(elapsed * 0.5), 0.9 * cos(elapsed * 0.3), 0.6), + Vector3(2.8 * sin(elapsed * 0.7), 1.9 * cos(elapsed * 0.5), 1.2 * sin(elapsed * 0.4)) + ) diff --git a/scripts/motion_demo.gd.uid b/scripts/motion_demo.gd.uid new file mode 100644 index 0000000..ddcd451 --- /dev/null +++ b/scripts/motion_demo.gd.uid @@ -0,0 +1 @@ +uid://c5mncovlnarve diff --git a/scripts/motion_instrument.gd b/scripts/motion_instrument.gd new file mode 100644 index 0000000..ad1d1ca --- /dev/null +++ b/scripts/motion_instrument.gd @@ -0,0 +1,133 @@ +@tool +extends Control +@export_enum("Attitude", "Heading", "Acceleration", "Vertical acceleration") var instrument: int = 0 +## Shares the circle radius with another instrument. +@export var radius_source: Control + +var attitude := Vector3.ZERO +var acceleration := Vector3.ZERO +var available := false +var full_scale := 5.0 + +func _ready() -> void: + resized.connect(queue_redraw) + theme_changed.connect(queue_redraw) + if is_instance_valid(radius_source): + radius_source.resized.connect(queue_redraw) + +func _text(at: Vector2, value: String, font_size: int = 16, color: Variant = null) -> void: + draw_string(get_theme_default_font(), at, value, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, _ink() if color == null else color) + +func _line(a: Vector2, b: Vector2, color: Variant = null, width: float = 1.0) -> void: + draw_line(a, b, _role("instrument_grid") if color == null else color, width, true) + +func _draw() -> void: + match instrument: + 0: _draw_attitude(available) + 1: _draw_heading() + 2: _draw_acceleration() + 3: _draw_vertical() + +func _horizon_half(center: Vector2, radius: float, normal: Vector2, displacement: float) -> PackedVector2Array: + var polygon := PackedVector2Array() + var previous := Vector2(radius, 0) + var previous_distance := previous.dot(normal) - displacement + for step in range(1, 129): + var point := Vector2.from_angle(step * TAU / 128.0) * radius + var distance := point.dot(normal) - displacement + if (distance >= 0) != (previous_distance >= 0): + polygon.append(center + previous.lerp(point, previous_distance / (previous_distance - distance))) + if distance >= 0: + polygon.append(center + point) + previous = point + previous_distance = distance + return polygon + +func _draw_attitude(available: bool) -> void: + var center := circle_center() + var radius := circle_radius() + draw_circle(center, radius, _role("horizon_sky") if available else _role("instrument_empty")) + if available: + var rotation := deg_to_rad(-attitude.x) + var normal := Vector2.DOWN.rotated(rotation) + var direction := Vector2.RIGHT.rotated(rotation) + var displacement := attitude.y * radius / 60.0 + var ground := _horizon_half(center, radius, normal, displacement) + if ground.size() >= 3: + draw_colored_polygon(ground, _role("horizon_ground")) + if absf(displacement) < radius: + var half_chord := sqrt(radius * radius - displacement * displacement) + _line(center + normal * displacement - direction * half_chord, center + normal * displacement + direction * half_chord, _ink(), 3) + for pitch_mark in range(-90, 91, 10): + var y: float = (attitude.y - pitch_mark) * radius / 60.0 + if pitch_mark != 0 and absf(y) < radius - 25: + var p := center + normal * y + var width := 26.0 if pitch_mark % 20 == 0 else 15.0 + _line(p - direction * width, p + direction * width, _ink(), 2) + if pitch_mark % 20 == 0: + _text(p + direction * (width + 7) + Vector2(0, 6), str(absi(pitch_mark)), 18) + for bank in [-60, -30, 0, 30, 60]: + var radial := Vector2.UP.rotated(deg_to_rad(bank)) + _line(center + radial * (radius - 10), center + radial * (radius - 1), _ink(), 2) + var pointer := Vector2.UP.rotated(rotation) + draw_colored_polygon(PackedVector2Array([center + pointer * (radius - 17), center + pointer.rotated(0.07) * (radius - 29), center + pointer.rotated(-0.07) * (radius - 29)]), _role("instrument_accent")) + draw_arc(center, radius, 0, TAU, 128, _role("instrument_grid"), 2, true) + _line(center + Vector2(-47, 0), center + Vector2(-14, 0), _role("instrument_accent"), 4) + _line(center + Vector2(14, 0), center + Vector2(47, 0), _role("instrument_accent"), 4) + _line(center + Vector2(-14, 0), center + Vector2(-14, 8), _role("instrument_accent"), 3) + _line(center + Vector2(14, 0), center + Vector2(14, 8), _role("instrument_accent"), 3) + draw_circle(center, 4, _role("instrument_accent")) +func _heading_tick_x(degrees: float) -> float: + return size.x / 2 + (degrees - attitude.z) * size.x / 100.0 + +func _draw_heading() -> void: + if available: + var first_tick := int(floor((attitude.z - 50.0) / 10.0)) * 10 + for degrees in range(first_tick, first_tick + 120, 10): + var x := _heading_tick_x(degrees) + if x < 18 or x > size.x - 18: + continue + _line(Vector2(x, 7), Vector2(x, 16), _ink(), 2) + if degrees % 20 == 0: + _text(Vector2(x - 17, 36), "%03d" % int(fposmod(degrees, 360)), 18, _ink()) + _line(Vector2(size.x / 2, 0), Vector2(size.x / 2, 20), _role("instrument_accent"), 3) + +func _draw_acceleration() -> void: + var radius := circle_radius() + var center := circle_center() + for fraction in [0.5, 1.0]: + draw_arc(center, radius * fraction, 0, TAU, 96, _role("instrument_grid"), 2, true) + _line(center - Vector2(radius, 0), center + Vector2(radius, 0), _role("instrument_axis")) + _line(center - Vector2(0, radius), center + Vector2(0, radius), _role("instrument_axis")) + _text(center + Vector2(-14, -radius - 8), "+X", 18) + _text(center + Vector2(radius + 2, 6), "+Y", 18) + if available: + var planar := Vector2(acceleration.y, -acceleration.x) / maxf(full_scale, 0.1) + var endpoint := center + planar.limit_length(1.0) * radius + _line(center, endpoint, _role("instrument_accent"), 4) + draw_circle(endpoint, 7, _role("instrument_accent")) + +func _draw_vertical() -> void: + var center := circle_center() + var radius := circle_radius() + _line(center - Vector2(0, radius), center + Vector2(0, radius), _role("instrument_axis"), 2) + for fraction in [-1.0, -0.5, 0.0, 0.5, 1.0]: + var y: float = center.y + fraction * radius + _line(Vector2(center.x - 6, y), Vector2(center.x + 6, y), _ink(), 2) + if available: + var y := center.y + clampf(acceleration.z / maxf(full_scale, 0.1), -1, 1) * radius + _line(Vector2(center.x - 10, y), Vector2(center.x + 10, y), _role("instrument_accent"), 5) + +func circle_radius() -> float: + if is_instance_valid(radius_source): + return radius_source.circle_radius() + return maxf(1.0, minf(size.x - 24, size.y - 44) / 2) + +func circle_center() -> Vector2: + return size / 2 + Vector2(0, 10) + +func _ink() -> Color: + return get_theme_color("font_color", "Label") + +func _role(name: StringName) -> Color: + return get_theme_color(name, "SurfaceSide") diff --git a/scripts/motion_instrument.gd.uid b/scripts/motion_instrument.gd.uid new file mode 100644 index 0000000..97cf2bd --- /dev/null +++ b/scripts/motion_instrument.gd.uid @@ -0,0 +1 @@ +uid://1dn362pyibkx diff --git a/scripts/motion_panel.gd b/scripts/motion_panel.gd new file mode 100644 index 0000000..5fa4bfb --- /dev/null +++ b/scripts/motion_panel.gd @@ -0,0 +1,63 @@ +@tool +extends Control +## Telemetry conventions: docs/motion_panel.md. + +@export var stale_after_seconds: float = 2.0 +@export_range(0.1, 100.0) var acceleration_full_scale: float = 5.0: + set(value): + acceleration_full_scale = maxf(value, 0.1) + _refresh() + +var attitude := Vector3.ZERO # roll, pitch, heading in degrees +var momentum := Vector3.ZERO +var acceleration := Vector3.ZERO +var _received_ms: int = -1 + +var _last_stale := false + +func _ready() -> void: + _refresh() + +func set_telemetry(orientation_degrees: Vector3, angular_momentum: Vector3, linear_acceleration: Vector3) -> void: + if not orientation_degrees.is_finite() or not angular_momentum.is_finite() or not linear_acceleration.is_finite(): + return + attitude = Vector3(wrapf(orientation_degrees.x, -180, 180), wrapf(orientation_degrees.y, -180, 180), fposmod(orientation_degrees.z, 360)) + momentum = angular_momentum + acceleration = linear_acceleration + _received_ms = Time.get_ticks_msec() + _refresh() + +func _process(_delta: float) -> void: + var stale := _is_stale() + if stale != _last_stale: + _refresh() + +func _is_stale() -> bool: + return _received_ms >= 0 and (Time.get_ticks_msec() - _received_ms) / 1000.0 > stale_after_seconds + +func _number(value: float, precision: int = 2) -> String: + return "—" if _received_ms < 0 else ("%+.*f" % [precision, value]) + +func _refresh() -> void: + if not is_node_ready(): + return + _last_stale = _is_stale() + %Status.text = "STALE" if _last_stale else "NO DATA" + %StatusBanner.visible = _last_stale or _received_ms < 0 + %Status.visible = true + %Roll.text = "R " + _number(attitude.x, 1) + "°" + %Pitch.text = "P " + _number(attitude.y, 1) + "°" + %Heading.text = "H " + ("%05.1f°" % attitude.z if _received_ms >= 0 else "—") + var accel_labels := [%AccelX, %AccelY, %AccelZ] + var momentum_labels := [%MomentumX, %MomentumY, %MomentumZ] + for axis in range(3): + accel_labels[axis].text = _number(acceleration[axis]) + momentum_labels[axis].text = _number(momentum[axis]) + %FullScale.text = "±%.1f" % acceleration_full_scale + %Limit.visible = _received_ms >= 0 and (Vector2(acceleration.x, acceleration.y).length() > acceleration_full_scale or absf(acceleration.z) > acceleration_full_scale) + for graphic in [%Horizon, %HeadingTape, %AccelerationPlot, %VerticalAcceleration]: + graphic.attitude = attitude + graphic.acceleration = acceleration + graphic.available = _received_ms >= 0 + graphic.full_scale = acceleration_full_scale + graphic.queue_redraw() diff --git a/scripts/motion_panel.gd.uid b/scripts/motion_panel.gd.uid new file mode 100644 index 0000000..365a348 --- /dev/null +++ b/scripts/motion_panel.gd.uid @@ -0,0 +1 @@ +uid://dh2vnk35potq3 diff --git a/themes/compass_material.tres b/themes/compass_material.tres new file mode 100644 index 0000000..9716adc --- /dev/null +++ b/themes/compass_material.tres @@ -0,0 +1,10 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[ext_resource type="Texture2D" path="res://assets/vector/dial.svg" id="1_7hb6v"] + +[resource] +transparency = 1 +cull_mode = 2 +shading_mode = 0 +albedo_color = Color(0.44361365, 0.72118443, 0, 1) +albedo_texture = ExtResource("1_7hb6v") diff --git a/themes/palette.gd b/themes/palette.gd new file mode 100644 index 0000000..e0fc860 --- /dev/null +++ b/themes/palette.gd @@ -0,0 +1,57 @@ +extends RefCounted +## Rebuild: godot --headless --path . --script themes/build_theme.gd + +const ACCENT = Color("ff7b00") +const SECONDARY = Color("402e32") +const TEXT = Color("f5f1ed") +const PANEL = Color("201b1f") +const SKY = Color("172f40") +const GROUND = Color("4b3930") +const COMPASS_TEST = Color(0.44361365, 0.72118443, 0, 1) +const WHITE = Color.WHITE +const CLEAR = Color.TRANSPARENT + +# Color methods cannot initialize constants; treat these static values as read-only. +static var ACCENT_LIGHT: Color = ACCENT.lightened(0.25) +static var ACCENT_DARK: Color = ACCENT.darkened(0.25) +static var INPUT: Color = PANEL.darkened(0.40) +static var SURFACE: Color = PANEL.lightened(0.04) +static var PANEL_DIM: Color = PANEL.darkened(0.20) +static var TEXT_SOFT: Color = TEXT.darkened(0.055) +static var TEXT_MUTED: Color = TEXT.lerp(SECONDARY, 0.34) +static var TEXT_MUTED_SOFT: Color = TEXT.lerp(SECONDARY, 0.40) +static var BORDER: Color = TEXT.lerp(SECONDARY, 0.64) +static var BORDER_DISABLED: Color = SECONDARY.lightened(0.08) +static var GRID: Color = SECONDARY.lightened(0.18) +static var THUMB_SOFT: Color = TEXT.lerp(SECONDARY, 0.18) +static var VIEWPORT_BACKGROUND: Color = PANEL.darkened(0.45) + +static var HIGH_CONTRAST: Dictionary = { + "text": TEXT, "muted_text": TEXT_MUTED, + "active_text": INPUT, "surface": PANEL, + "panel": PANEL, "input": INPUT, + "hover": SECONDARY, "pressed": ACCENT, "fill": ACCENT, + "disabled": PANEL_DIM, "border": BORDER, + "disabled_border": BORDER_DISABLED, "focus": ACCENT, + "separator": SECONDARY, "thumb": TEXT_MUTED, + "fill_text": INPUT, +} +static var SOFT: Dictionary = { + "text": TEXT_SOFT, "muted_text": TEXT_MUTED_SOFT, + "active_text": TEXT, "surface": SURFACE, + "panel": PANEL, "input": INPUT, + "hover": SECONDARY, "pressed": SECONDARY, + "fill": ACCENT, "disabled": PANEL, + "border": CLEAR, "disabled_border": CLEAR, + "focus": ACCENT, "separator": SECONDARY, + "thumb": THUMB_SOFT, "fill_text": INPUT, +} + +static var INSTRUMENT: Dictionary = { + "instrument_accent": ACCENT_LIGHT, + "instrument_grid": GRID, + "horizon_sky": SKY, + "horizon_ground": GROUND, + "alert_background": ACCENT_DARK, + "alert_text": WHITE, +} diff --git a/themes/palette.gd.uid b/themes/palette.gd.uid new file mode 100644 index 0000000..4f25513 --- /dev/null +++ b/themes/palette.gd.uid @@ -0,0 +1 @@ +uid://deuuvf6y2c031