Compare commits

..
7 Commits
103 changed files with 6345 additions and 1628 deletions

No files matched your search

Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://1sjxuy7kgxy7"
path="res://.godot/imported/link.svg-f8724c7cad57bde98ee0cda284f3ebbb.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/vector/status/link.svg"
dest_files=["res://.godot/imported/link.svg-f8724c7cad57bde98ee0cda284f3ebbb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3kgbws2ejds6"
path="res://.godot/imported/rov.svg-85cd68e18cc64f6002858c870af4fd81.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/vector/status/rov.svg"
dest_files=["res://.godot/imported/rov.svg-85cd68e18cc64f6002858c870af4fd81.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
Binary file not shown.
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc2m1gknloa5i"
path="res://.godot/imported/stationkeep.svg-ba46234e5cd35725d5ce1d66d82e2fa0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/vector/status/stationkeep.svg"
dest_files=["res://.godot/imported/stationkeep.svg-ba46234e5cd35725d5ce1d66d82e2fa0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+35
View File
@@ -0,0 +1,35 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/camera_layout_frame.gd" id="1"]
[node name="CameraSlot" type="VBoxContainer"]
theme_override_constants/separation = 8
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Title" type="Label" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
text = "CAMERA / PRIMARY"
theme_override_font_sizes/font_size = 20
[node name="State" type="Label" parent="Header"]
layout_mode = 2
text = "NO STREAM"
[node name="Frame" type="AspectRatioContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
ratio = 1.7777778
script = ExtResource("1")
[node name="Placeholder" type="PanelContainer" parent="Frame"]
layout_mode = 2
[node name="Label" type="Label" parent="Frame/Placeholder"]
layout_mode = 2
text = "16:9 CAMERA\nReserved video surface"
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 24
+51
View File
@@ -0,0 +1,51 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/selectable_container.tscn" id="1"]
[ext_resource type="Script" path="res://scripts/console_panel.gd" id="2"]
[ext_resource type="PackedScene" path="res://components/console_session.tscn" id="3"]
[node name="ConsolePanel" instance=ExtResource("1")]
custom_minimum_size = Vector2(720, 420)
script = ExtResource("2")
title = "CONSOLE"
focus_key = 0
initial_focus = NodePath("Tabs/ROV/Input/Command")
[node name="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
label_settings = null
theme_override_font_sizes/font_size = 18
text = "CONSOLE"
[node name="Tabs" type="TabContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
current_tab = 0
[node name="ROV" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
[node name="Host" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "host"
tab_label = "Host"
shortcut = 4194337
prompt = "host $"
introduction = "Host shell disconnected."
[node name="Logs" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "logs"
tab_label = "Logs"
shortcut = 4194338
read_only = true
introduction = "ROV journal stream disconnected."
[node name="Server" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "server"
tab_label = "Server"
shortcut = 4194339
prompt = "server >"
introduction = "Server shell disconnected."
+73
View File
@@ -0,0 +1,73 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/console_session.gd" id="1"]
[ext_resource type="Font" path="res://themes/console_mono.tres" id="2_mono"]
[node name="ConsoleSession" type="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 6
script = ExtResource("1")
[node name="Status" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Connection" type="Label" parent="Status"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 14
text = "Not connected"
[node name="Follow" type="CheckButton" parent="Status"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Follow"
button_pressed = true
[node name="Clear" type="Button" parent="Status"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Clear"
[node name="Output" type="RichTextLabel" parent="."]
custom_minimum_size = Vector2(0, 160)
layout_mode = 2
size_flags_vertical = 3
focus_mode = 2
theme_override_fonts/normal_font = ExtResource("2_mono")
theme_override_font_sizes/normal_font_size = 18
bbcode_enabled = false
scroll_following = true
selection_enabled = true
context_menu_enabled = true
[node name="Filter" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="Label" type="Label" parent="Filter"]
layout_mode = 2
text = "Filter"
[node name="Text" type="LineEdit" parent="Filter"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
placeholder_text = "Filter log text"
[node name="Input" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Prompt" type="Label" parent="Input"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
text = "rov $"
[node name="Command" type="LineEdit" parent="Input"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
placeholder_text = "Enter command…"
+136
View File
@@ -0,0 +1,136 @@
[gd_scene format=3 uid="uid://b75nt1kn2sobp"]
[ext_resource type="Script" uid="uid://dnafkcowopwod" path="res://scripts/environment_panel.gd" id="1"]
[ext_resource type="Script" uid="uid://cbobdfddbbddo" path="res://scripts/depth_scale.gd" id="2"]
[node name="EnvironmentPanel" type="PanelContainer" unique_id=1120688022]
custom_minimum_size = Vector2(360, 240)
offset_right = 360.0
offset_bottom = 240.0
theme_type_variation = &"VehicleBackground"
script = ExtResource("1")
[node name="Margin" type="MarginContainer" parent="." unique_id=1435834688]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 10
[node name="Body" type="VBoxContainer" parent="Margin" unique_id=826880208]
layout_mode = 2
theme_override_constants/separation = 6
[node name="Header" type="HBoxContainer" parent="Margin/Body" unique_id=1188782081]
layout_mode = 2
[node name="Title" type="Label" parent="Margin/Body/Header" unique_id=1435423541]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 18
text = "ENVIRONMENT"
[node name="State" type="Label" parent="Margin/Body/Header" unique_id=1387248588]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"InstrumentMuted"
theme_override_font_sizes/font_size = 14
text = "NO TELEMETRY"
horizontal_alignment = 2
[node name="Measurements" type="HBoxContainer" parent="Margin/Body" unique_id=354890970]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 16
[node name="Readouts" type="VBoxContainer" parent="Margin/Body/Measurements" unique_id=766009790]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="DepthTitle" type="Label" parent="Margin/Body/Measurements/Readouts" unique_id=1777992876]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 14
text = "DEPTH"
[node name="DepthValue" type="Label" parent="Margin/Body/Measurements/Readouts" unique_id=1937430512]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 40
text = "— m"
[node name="DepthReference" type="Label" parent="Margin/Body/Measurements/Readouts" unique_id=1946013161]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"InstrumentMuted"
theme_override_font_sizes/font_size = 14
text = "/ 1000 m"
[node name="Spacer" type="Control" parent="Margin/Body/Measurements/Readouts" unique_id=1016695424]
layout_mode = 2
size_flags_vertical = 3
[node name="Supporting" type="HBoxContainer" parent="Margin/Body/Measurements/Readouts" unique_id=1473880932]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Pressure" type="VBoxContainer" parent="Margin/Body/Measurements/Readouts/Supporting" unique_id=1964987657]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="PressureTitle" type="Label" parent="Margin/Body/Measurements/Readouts/Supporting/Pressure" unique_id=1727076495]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 13
text = "PRESSURE"
[node name="PressureValue" type="Label" parent="Margin/Body/Measurements/Readouts/Supporting/Pressure" unique_id=1479127186]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "— MPa"
[node name="PressureReference" type="Label" parent="Margin/Body/Measurements/Readouts/Supporting/Pressure" unique_id=1835332085]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"InstrumentMuted"
theme_override_font_sizes/font_size = 14
text = "/ 10 MPa"
[node name="Temperature" type="VBoxContainer" parent="Margin/Body/Measurements/Readouts/Supporting" unique_id=1827823179]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Title" type="Label" parent="Margin/Body/Measurements/Readouts/Supporting/Temperature" unique_id=1829138035]
layout_mode = 2
theme_override_font_sizes/font_size = 13
text = "WATER TEMP"
[node name="TemperatureValue" type="Label" parent="Margin/Body/Measurements/Readouts/Supporting/Temperature" unique_id=1337672100]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "— °C"
[node name="Scale" type="VBoxContainer" parent="Margin/Body/Measurements" unique_id=1235543623]
layout_mode = 2
theme_override_constants/separation = 0
[node name="DepthScale" type="Control" parent="Margin/Body/Measurements/Scale" unique_id=223719936]
unique_name_in_owner = true
custom_minimum_size = Vector2(74, 140)
layout_mode = 2
size_flags_vertical = 3
tooltip_text = "Shallow depths expanded: log(1 + depth / knee). Includes zero."
script = ExtResource("2")
[node name="Caption" type="Label" parent="Margin/Body/Measurements/Scale" unique_id=1491620694]
layout_mode = 2
theme_type_variation = &"InstrumentMuted"
theme_override_font_sizes/font_size = 12
text = "LOG(n) -1"
horizontal_alignment = 1
+52
View File
@@ -0,0 +1,52 @@
[gd_scene format=3 uid="uid://dscdhba74otou"]
[ext_resource type="Script" uid="uid://bqrapdh6yddl8" path="res://scripts/layout_canvas.gd" id="1"]
[ext_resource type="Script" uid="uid://cwbk5g4yhdfd6" path="res://scripts/layout_edit_overlay.gd" id="2"]
[node name="LayoutCanvas" type="Control" unique_id=325333537]
clip_contents = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1")
legacy_config_path = null
[node name="EditOverlay" type="Control" parent="." unique_id=162710218]
visible = false
z_index = 120
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
script = ExtResource("2")
[node name="Actions" type="HBoxContainer" parent="EditOverlay" unique_id=846269020]
visible = false
layout_mode = 0
offset_right = 112.0
offset_bottom = 36.0
theme_override_constants/separation = 2
[node name="Settings" type="Button" parent="EditOverlay/Actions" unique_id=1387368158]
custom_minimum_size = Vector2(36, 36)
layout_mode = 2
tooltip_text = "Settings (not available yet)"
disabled = true
text = "⚙"
[node name="Resize" type="Button" parent="EditOverlay/Actions" unique_id=1301115821]
custom_minimum_size = Vector2(36, 36)
layout_mode = 2
tooltip_text = "Drag to resize"
text = "↘"
[node name="Delete" type="Button" parent="EditOverlay/Actions" unique_id=1936216385]
custom_minimum_size = Vector2(36, 36)
layout_mode = 2
tooltip_text = "Remove panel from layout"
text = "×"
+12
View File
@@ -0,0 +1,12 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/layout_item.gd" id="1"]
[node name="LayoutItem" type="Control"]
layout_mode = 0
script = ExtResource("1")
[node name="Background" type="Panel" parent="."]
layout_mode = 0
mouse_filter = 2
theme_type_variation = &"LayoutItemBackground"
+66
View File
@@ -0,0 +1,66 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/layout_library.gd" id="1"]
[node name="LayoutLibrary" type="PanelContainer"]
visible = false
z_index = 300
layout_mode = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -424.0
offset_top = 80.0
offset_right = -24.0
offset_bottom = 720.0
theme_type_variation = &"VehicleBackground"
script = ExtResource("1")
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_bottom = 12
[node name="Body" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Header" type="HBoxContainer" parent="Margin/Body"]
layout_mode = 2
[node name="Title" type="Label" parent="Margin/Body/Header"]
layout_mode = 2
size_flags_horizontal = 3
text = "ELEMENT LIBRARY"
theme_override_font_sizes/font_size = 20
[node name="Close" type="Button" parent="Margin/Body/Header"]
layout_mode = 2
text = "×"
tooltip_text = "Close library"
[node name="List" type="ItemList" parent="Margin/Body"]
custom_minimum_size = Vector2(0, 180)
layout_mode = 2
size_flags_vertical = 3
theme_type_variation = &"LayoutLibraryList"
same_column_width = true
fixed_icon_size = Vector2i(24, 24)
[node name="Description" type="Label" parent="Margin/Body"]
custom_minimum_size = Vector2(0, 78)
layout_mode = 2
autowrap_mode = 3
[node name="Size" type="Label" parent="Margin/Body"]
layout_mode = 2
[node name="Status" type="Label" parent="Margin/Body"]
layout_mode = 2
autowrap_mode = 3
[node name="Add" type="Button" parent="Margin/Body"]
layout_mode = 2
text = "Add element"
tooltip_text = "Place in the first available space, using the minimum size if needed"
+81
View File
@@ -0,0 +1,81 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/layout_overview.gd" id="1"]
[ext_resource type="Script" path="res://scripts/layout_grid.gd" id="2"]
[ext_resource type="Shader" path="res://shaders/layout_grid.gdshader" id="3"]
[ext_resource type="PackedScene" path="res://components/layout_library.tscn" id="library"]
[sub_resource type="ShaderMaterial" id="GridMaterial"]
resource_local_to_scene = true
shader = ExtResource("3")
[node name="LayoutOverview" type="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="Dashboard" type="Control" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Grid" type="ColorRect" parent="Dashboard"]
visible = false
material = SubResource("GridMaterial")
z_index = 100
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
script = ExtResource("2")
[node name="Toolbar" type="HBoxContainer" parent="."]
visible = false
z_index = 200
layout_mode = 1
anchor_right = 1.0
offset_left = 24.0
offset_top = 16.0
offset_right = -24.0
offset_bottom = 60.0
mouse_filter = 2
theme_override_constants/separation = 20
[node name="Title" type="Label" parent="Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
theme_override_font_sizes/font_size = 20
text = "LAYOUT OVERVIEW"
[node name="Hint" type="Label" parent="Toolbar"]
layout_mode = 2
mouse_filter = 2
text = "Esc to return"
[node name="Add" type="Button" parent="Toolbar"]
layout_mode = 2
text = "Add element"
[node name="Reset" type="Button" parent="Toolbar"]
layout_mode = 2
text = "Reset layout"
[node name="Return" type="Button" parent="Toolbar"]
layout_mode = 2
text = "Return to dashboard"
[node name="Library" parent="." instance=ExtResource("library")]
layout_mode = 1
+30
View File
@@ -0,0 +1,30 @@
[gd_scene format=3 uid="uid://u014ihs685mq"]
[ext_resource type="Script" uid="uid://btaf1ke22k5ij" path="res://scripts/layout_slot.gd" id="1"]
[node name="LayoutSlot" type="PanelContainer" unique_id=241401501]
custom_minimum_size = Vector2(0, 100)
theme_type_variation = &"VehicleBackground"
script = ExtResource("1")
[node name="Margin" type="MarginContainer" parent="." unique_id=1948608899]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 12
[node name="Text" type="VBoxContainer" parent="Margin" unique_id=764454736]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Text" unique_id=806114057]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "RESERVED"
[node name="Description" type="Label" parent="Margin/Text" unique_id=592619421]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 3
+103
View File
@@ -0,0 +1,103 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/link_panel.gd" id="1"]
[ext_resource type="PackedScene" path="res://components/link_rate.tscn" id="2"]
[ext_resource type="Texture2D" path="res://assets/vector/status/link.svg" id="3"]
[node name="LinkPanel" type="PanelContainer"]
custom_minimum_size = Vector2(360, 240)
offset_right = 360.0
offset_bottom = 240.0
theme_type_variation = &"VehicleBackground"
script = ExtResource("1")
link_icon = ExtResource("3")
[node name="Backdrop" type="Control" parent="."]
layout_mode = 2
clip_contents = true
mouse_filter = 2
[node name="LinkIcon" type="TextureRect" parent="Backdrop"]
unique_name_in_owner = true
layout_mode = 1
anchor_left = 0.65
anchor_right = 0.65
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -90.0
offset_right = 90.0
offset_top = -90.0
offset_bottom = 90.0
rotation = -0.20943952
pivot_offset = Vector2(90, 90)
mouse_filter = 2
texture = ExtResource("3")
expand_mode = 1
stretch_mode = 5
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_top = 10
theme_override_constants/margin_bottom = 10
[node name="Body" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="Header" type="HBoxContainer" parent="Margin/Body"]
layout_mode = 2
[node name="Title" type="Label" parent="Margin/Body/Header"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 18
text = "LINK"
[node name="State" type="Label" parent="Margin/Body/Header"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "NO TELEMETRY"
horizontal_alignment = 2
[node name="Rates" type="HBoxContainer" parent="Margin/Body"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 16
[node name="Downlink" parent="Margin/Body/Rates" instance=ExtResource("2")]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="Uplink" parent="Margin/Body/Rates" instance=ExtResource("2")]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
direction = "↑ UPLINK"
[node name="Loss" type="HBoxContainer" parent="Margin/Body"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Title" type="Label" parent="Margin/Body/Loss"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "PACKET LOSS"
[node name="LossValue" type="Label" parent="Margin/Body/Loss"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 20
text = "—"
[node name="LossWindow" type="Label" parent="Margin/Body/Loss"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"LinkMuted"
theme_override_font_sizes/font_size = 14
text = "10 s window"
horizontal_alignment = 2
+57
View File
@@ -0,0 +1,57 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/link_rate.gd" id="1"]
[node name="LinkRate" type="Control"]
custom_minimum_size = Vector2(140, 148)
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("1")
[node name="Readouts" type="VBoxContainer" parent="."]
layout_mode = 1
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -70.0
offset_right = 70.0
offset_top = -44.0
offset_bottom = 44.0
theme_override_constants/separation = 0
[node name="Direction" type="Label" parent="Readouts"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"LinkRateNormal"
theme_override_font_sizes/font_size = 15
text = "↓ DOWNLINK"
horizontal_alignment = 1
[node name="Value" type="Label" parent="Readouts"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "—"
horizontal_alignment = 1
[node name="Units" type="Label" parent="Readouts"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Gbps"
horizontal_alignment = 1
[node name="Capacity" type="Label" parent="."]
unique_name_in_owner = true
layout_mode = 1
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -62.0
offset_right = 62.0
offset_top = -23.0
offset_bottom = -3.0
theme_override_font_sizes/font_size = 14
text = "10 Gbps max"
horizontal_alignment = 1
+332
View File
@@ -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
+274
View File
@@ -0,0 +1,274 @@
[gd_scene format=3 uid="uid://c6xqxnk0pqhg1"]
[ext_resource type="Script" uid="uid://d3jrypgr15n4x" path="res://scripts/vehicle_status_panel.gd" id="1"]
[ext_resource type="Script" uid="uid://rh8gwu8mfqx2" path="res://scripts/vehicle_status_art.gd" id="2"]
[ext_resource type="Script" uid="uid://bqvhrignal143" path="res://scripts/status_gauge.gd" id="3"]
[ext_resource type="Texture2D" uid="uid://bc2m1gknloa5i" path="res://assets/vector/status/stationkeep.svg" id="4"]
[node name="VehicleStatusPanel" type="PanelContainer" unique_id=489606862]
custom_minimum_size = Vector2(600, 440)
offset_right = 600.0
offset_bottom = 440.0
theme_type_variation = &"VehicleBackground"
script = ExtResource("1")
mode_icon = ExtResource("4")
[node name="Margin" type="MarginContainer" parent="." unique_id=1101194281]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 12
[node name="Layout" type="VBoxContainer" parent="Margin" unique_id=221437014]
layout_mode = 2
theme_override_constants/separation = 12
[node name="StatusArea" type="MarginContainer" parent="Margin/Layout" unique_id=1107401965]
custom_minimum_size = Vector2(0, 164)
clip_contents = true
layout_mode = 2
size_flags_vertical = 3
[node name="StatusArt" type="Control" parent="Margin/Layout/StatusArea" unique_id=649910411]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
script = ExtResource("2")
[node name="ModeIcon" type="TextureRect" parent="Margin/Layout/StatusArea/StatusArt" unique_id=728690011]
unique_name_in_owner = true
visible = false
self_modulate = Color(1, 0.61176467, 0.25, 0.09)
layout_mode = 1
anchors_preset = -1
anchor_left = 0.68
anchor_top = 0.5
anchor_right = 0.68
anchor_bottom = 0.5
offset_left = -105.0
offset_top = -105.0
offset_right = 105.0
offset_bottom = 105.0
rotation = -0.20943952
pivot_offset = Vector2(105, 105)
mouse_filter = 2
texture = ExtResource("4")
expand_mode = 1
stretch_mode = 5
[node name="Inset" type="MarginContainer" parent="Margin/Layout/StatusArea" unique_id=2050290664]
layout_mode = 2
theme_override_constants/margin_left = 12
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 12
theme_override_constants/margin_bottom = 8
[node name="Text" type="VBoxContainer" parent="Margin/Layout/StatusArea/Inset" unique_id=905192152]
layout_mode = 2
theme_override_constants/separation = 4
[node name="Top" type="HBoxContainer" parent="Margin/Layout/StatusArea/Inset/Text" unique_id=424687654]
layout_mode = 2
[node name="Code" type="Label" parent="Margin/Layout/StatusArea/Inset/Text/Top" unique_id=1099636858]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 18
[node name="Severity" type="Label" parent="Margin/Layout/StatusArea/Inset/Text/Top" unique_id=1356897068]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 18
text = "NO DATA"
horizontal_alignment = 2
[node name="StatusName" type="Label" parent="Margin/Layout/StatusArea/Inset/Text" unique_id=1189663888]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 30
text = "Waiting for telemetry"
autowrap_mode = 3
[node name="Detail" type="Label" parent="Margin/Layout/StatusArea/Inset/Text" unique_id=703466821]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 18
autowrap_mode = 3
[node name="Action" type="Label" parent="Margin/Layout/StatusArea/Inset/Text" unique_id=514634651]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Label"
theme_override_font_sizes/font_size = 18
autowrap_mode = 3
[node name="Readouts" type="VBoxContainer" parent="Margin/Layout" unique_id=1999758637]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Voltage" type="VBoxContainer" parent="Margin/Layout/Readouts" unique_id=1002742281]
layout_mode = 2
theme_override_constants/separation = 2
[node name="Numbers" type="HBoxContainer" parent="Margin/Layout/Readouts/Voltage" unique_id=1312021033]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Layout/Readouts/Voltage/Numbers" unique_id=264983620]
custom_minimum_size = Vector2(124, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "VOLTAGE"
[node name="VoltageValue" type="Label" parent="Margin/Layout/Readouts/Voltage/Numbers" unique_id=1230347145]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 26
text = "—"
horizontal_alignment = 2
[node name="VoltageReference" type="Label" parent="Margin/Layout/Readouts/Voltage/Numbers" unique_id=264381298]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "/ 48 V"
vertical_alignment = 1
[node name="VoltageGauge" type="Control" parent="Margin/Layout/Readouts/Voltage" unique_id=446082505]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
script = ExtResource("3")
minimum = 42.0
maximum = 54.0
warning_low = 47.0
warning_high = 49.0
fault_low = 45.0
fault_high = 51.0
nominal = 48.0
[node name="Current" type="VBoxContainer" parent="Margin/Layout/Readouts" unique_id=1982270337]
layout_mode = 2
theme_override_constants/separation = 2
[node name="Numbers" type="HBoxContainer" parent="Margin/Layout/Readouts/Current" unique_id=619271939]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Layout/Readouts/Current/Numbers" unique_id=137700803]
custom_minimum_size = Vector2(124, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "CURRENT"
[node name="CurrentValue" type="Label" parent="Margin/Layout/Readouts/Current/Numbers" unique_id=2147417420]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 26
text = "—"
horizontal_alignment = 2
[node name="CurrentReference" type="Label" parent="Margin/Layout/Readouts/Current/Numbers" unique_id=1995980746]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "/ 30 A"
vertical_alignment = 1
[node name="CurrentGauge" type="Control" parent="Margin/Layout/Readouts/Current" unique_id=484645650]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
script = ExtResource("3")
nominal = nan
[node name="Power" type="VBoxContainer" parent="Margin/Layout/Readouts" unique_id=1074490654]
layout_mode = 2
theme_override_constants/separation = 2
[node name="Numbers" type="HBoxContainer" parent="Margin/Layout/Readouts/Power" unique_id=111236353]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Layout/Readouts/Power/Numbers" unique_id=213594380]
custom_minimum_size = Vector2(124, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "POWER"
[node name="PowerValue" type="Label" parent="Margin/Layout/Readouts/Power/Numbers" unique_id=1921756191]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 26
text = "—"
horizontal_alignment = 2
[node name="PowerReference" type="Label" parent="Margin/Layout/Readouts/Power/Numbers" unique_id=1441937096]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "/ 1440 W"
vertical_alignment = 1
[node name="PowerGauge" type="Control" parent="Margin/Layout/Readouts/Power" unique_id=82393019]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
script = ExtResource("3")
maximum = 1728.0
warning_high = 1152.0
fault_high = 1440.0
nominal = nan
[node name="Temperature" type="VBoxContainer" parent="Margin/Layout/Readouts" unique_id=658026844]
layout_mode = 2
theme_override_constants/separation = 2
[node name="Numbers" type="HBoxContainer" parent="Margin/Layout/Readouts/Temperature" unique_id=567006533]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Layout/Readouts/Temperature/Numbers" unique_id=1987975109]
custom_minimum_size = Vector2(124, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "INTERNAL"
[node name="TemperatureValue" type="Label" parent="Margin/Layout/Readouts/Temperature/Numbers" unique_id=1041616611]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 26
text = "—"
horizontal_alignment = 2
[node name="TemperatureReference" type="Label" parent="Margin/Layout/Readouts/Temperature/Numbers" unique_id=1032238631]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "/ 65 °C"
vertical_alignment = 1
[node name="TemperatureGauge" type="Control" parent="Margin/Layout/Readouts/Temperature" unique_id=214013672]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
script = ExtResource("3")
maximum = 90.0
warning_high = 55.0
fault_high = 65.0
nominal = nan
-7
View File
@@ -1,20 +1,13 @@
To do: To do:
- Pitch/yaw/tilt relative to magnetic north
- Motor information - Motor information
- RPM - RPM
- Torque - Torque
- Power Consumption - Power Consumption
- Overall power consumption
- Command line - Command line
- Inputs - Inputs
- Camera Status - Camera Status
- VR Peripherals and ROV connection status - VR Peripherals and ROV connection status
- Uplink/Downlink Data Rate
- Manipulator Pos/Rot - Manipulator Pos/Rot
- 3D model of arm rotation - 3D model of arm rotation
- Status
- Logging - Logging
- Enclosure Temperature
- Depth (Pressure)
- Tether Latency
+13
View File
@@ -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.
+27
View File
@@ -0,0 +1,27 @@
# Console
Run `scenes/console_demo.tscn` for simulated commands and a journal feed, or add **Console** from the layout library.
| Tab | Shortcut | Input |
| --- | --- | --- |
| ROV | F5 | ROV shell command |
| Host | F6 | Host shell command |
| Logs | F7 | Case-insensitive log filter |
| Server | F8 | Server command |
A shortcut selects its tab and focuses its input across either dashboard window. Escape releases focus. Ctrl+Tab / Ctrl+Shift+Tab cycle tabs, Ctrl+L clears the current output, and Up/Down recall commands. Each tab retains its draft, history and output. Follow controls automatic scrolling; Clear keeps command history. Output supports selection and copying.
Shortcuts are exported on the session nodes in `components/console_panel.tscn`. Conflicting shortcuts show “in use”; the first visible panel owns them. Layout editing suspends console input and shortcuts. Placement persists with the layout; console contents and command history do not.
## Transport hooks
`components/console_panel.tscn` owns four instances of `console_session.tscn`. Controls and tabs live in the scene tree and inherit the shared theme. Output, prompts and inputs share `themes/console_mono.tres`, with full hinting, 2× oversampling and slightly strengthened strokes for legibility under scaling.
- Connect `command_requested(session_id, command)` to a transport.
- Call `set_connection(session_id, connected, message)` to update availability and status.
- Call `append_output(session_id, text)` to deliver output, including trailing newlines where appropriate.
- Session IDs are `rov`, `host`, `logs`, and `server`.
The component starts disconnected and rejects command dispatch until connected with a handler attached. Logs are read-only. Scrollback is bounded by each session's `max_scrollback_characters`.
This is a line-oriented frontend. SSH, local processes, journal streaming, ANSI/VT terminal emulation and interactive full-screen programs are not connected. The demo only simulates responses and never executes system commands.
+48
View File
@@ -0,0 +1,48 @@
# Display layouts
Run either scene with F6:
- `scenes/layout_landscape.tscn`: 1920×1080, 32×18 grid.
- `scenes/layout_dual_portrait.tscn`: two independent 1080×1920 windows, each 18×32. With two portrait monitors detected, each window is maximized on its own screen; otherwise, smaller windows open side by side. Disable `use_portrait_monitors` to force the windowed preview.
The reference cell size is 60px. Window scaling is uniform and independent of the project's stretch scale. Physical monitor placement still needs checking on the target hardware.
## Editing
Hover briefly in the top-left corner to enter overview. The trigger uses a 20-pixel window area, including letterboxing, independently of UI scale. In dual portrait, only the left window owns the hot corner; both windows enter overview together.
**Add element** opens a scrollable library. Select an entry and click Add element (or double-click it). Panels use the first free space, falling back to their minimum size if needed. Multiple instances are supported; a full grid leaves the library open with a no-space message.
Select and drag a panel to move it. Selected panels show three buttons:
- **↘**: drag to resize from the bottom-right edge.
- **×**: remove the panel from this layout.
- **⚙**: settings, currently disabled.
Moves and sizes snap to cells. Occupied space, minimum sizes, and window boundaries block invalid placements; invalid drops return to their starting position. Escape or leaving overview cancels an unfinished drag. Panels stay within their own window.
Changes save automatically as JSON under `user://layouts/`: `landscape.json`, `portrait_left.json`, and `portrait_right.json`. Godot resolves `user://` to the application's user-data directory (the XDG data location on Linux), available from `OS.get_user_data_dir()`. To store files beside a writable application instead, set each canvas's `config_path` to an absolute path in that folder. Existing `.cfg` layouts migrate on first load when no JSON file exists; the originals are retained. **Reset layout** restores default positions, sizes, and deleted panels. Exit with Escape, Return to dashboard, or a click outside the preview.
## JSON format
```json
{
"version": 1,
"grid": [32, 18],
"elements": [
{"id": "camera", "type": "camera", "rect": [0, 0, 16, 10]}
]
}
```
The elements array is the complete active layout; omitted default panels stay deleted. IDs distinguish repeated instances. Writes use a temporary file followed by rename. Invalid types, duplicate IDs, overlaps, and out-of-bounds rectangles are rejected before changing the displayed layout.
## Scene structure
`layout_overview.tscn` owns the shader grid and toolbar. Each `layout_canvas.tscn` places its panel children using `layout_item.gd`; set their IDs, cell rectangles, and minimum sizes in the Inspector. Existing UI scenes remain under each item's `Content` node. Each allocation has a full-size panel background with a shared `panel_gap` (16 logical pixels by default). Instruments scale uniformly inside that frame; reserved slots fill it. Embedded component backgrounds are transparent so their scaling does not change the visible gutters. The camera retains its 16:9 picture inside the camera/header allocation.
The initial layouts deliberately leave free cells for rearranging. VR/video and operator tools are placeholders. The Link and Environment panels have separate [Link](link_panel.md) and [Environment](environment_panel.md) telemetry demos. The [Console](console_panel.md) is available in the library with four tabs and direct focus shortcuts. Camera and telemetry feeds are not connected.
The library registry is `scripts/layout_catalog.gd`. Each entry supplies a scene, display text, preferred/minimum cells and sizing behavior. JSON stores registered type IDs rather than resource paths. `components/layout_library.tscn` owns the drawer; selection button order is unchanged.
`LayoutItemBackground` supplies the shared outer frame. Components with a separate background node can specify `background_path` on their item (or `background` in the catalog entry); root PanelContainers are embedded automatically. Standalone component scenes retain their own styling.
+16
View File
@@ -0,0 +1,16 @@
# Environment panel
Run `scenes/environment_demo.tscn` with F6. It previews surface/zero, shallow operation, deeper readings, independent depth/pressure warnings, stale data and both themes.
```gdscript
$EnvironmentPanel.set_telemetry(248.6, 2.54, 7.4)
# depth: metres; outside pressure: MPa; water temperature: Celsius
```
The depth indicator uses `log(1 + depth / knee)`, normalized to the maximum depth. This includes zero and expands shallow readings; the default knee is 1 m. Tick labels show the non-linear spacing. Numeric readings are untransformed; negative depth remains visible numerically while the marker stays at zero. Above-limit readings remain visible while the marker clamps to the bottom.
Limits default to 1000 m and 10 MPa. Warning/saturation colors start at 80%/95% of each actual measurement's limit, independently of the scale transform. Temperature has no limit or gauge. Pressure and depth are supplied independently; no conversion is performed, and the pressure datum should match the configured pressure limit. Demo samples are illustrative.
Missing data shows dashes. After two seconds without a sample, values are marked STALE / HELD. Non-finite values and negative pressure are rejected. `clear_telemetry()` restores the missing-data state.
Labels and containers remain editable scene nodes; `depth_scale.gd` draws the indicator. There is no background icon. Colors use shared theme roles. The panel replaces the Environment placeholder in the library and both layouts without changing saved JSON type IDs.
+16
View File
@@ -0,0 +1,16 @@
# Link panel
Run `scenes/link_demo.tscn` with F6. The controls preview connected, near-capacity, saturated, degraded, disconnected, stale and missing telemetry, plus both themes.
```gdscript
$LinkPanel.set_telemetry(6.42, 1.08, 0.02, 0)
# down/up: Gbps; packet loss: percent; state: 0 connected, 1 degraded, 2 disconnected
```
Capacity defaults to 10 Gbps per direction. Arc and direction colors change at 80% and 95%; these thresholds and capacities are configurable. Numeric readings stay in the normal text color and remain unclamped above capacity. Utilization does not change the reported connection state.
`loss_window_seconds` describes the supplied loss measurement (default 10 s); compute that measurement upstream. Disconnected and missing telemetry show dashes. After `stale_after_seconds` (default 2), values are held with a STALE label. Invalid or non-finite samples are rejected. `clear_telemetry()` restores the missing-data state.
Layout and text are scene nodes; `link_rate.gd` only draws the utilization arcs. `link_icon` selects the subdued SVG watermark. The current local chain-link drawing is a draft. Colors come from the shared theme.
The panel replaces the Link placeholder in both layouts and the element library. Existing JSON layouts keep their `link` type IDs. Camera and telemetry transports are not connected by this prototype.
+20
View File
@@ -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).
+30
View File
@@ -0,0 +1,30 @@
# Vehicle status prototype
Run `scenes/vehicle_status_demo.tscn` with F6. The controls switch between normal, warning, fault, and a stopped feed, plus high contrast and alert pulsing.
Instance `components/vehicle_status_panel.tscn` for integration:
```gdscript
$VehicleStatusPanel.set_status("STATIONKEEP", 0, "", "Position hold active", "Thrusters enabled")
$VehicleStatusPanel.set_telemetry(48.0, 18.2, 874.0, 34.0)
# severity: 0 normal, 1 warning, 2 fault
$VehicleStatusPanel.set_status("Enclosure overtemperature", 2, "TMP-003",
"68 °C · trip 65 °C · thrusters inhibited", "Inspect cooling before reset")
```
All sample fault codes, responses, and thresholds are illustrative. The UI displays reported vehicle status; gauge zones do not control the ROV or generate its faults.
Configure each gauge in the scene Inspector. The numeric references use the voltage nominal or the other gauges' upper fault thresholds. Voltage marks 48 V at center; any deviation enters a yellow region.
| Gauge | Display range | Warning zone | Fault zone |
| --- | --- | --- | --- |
| Voltage | 42–54 V | Either side of 48 V | Below 45 / above 51 V |
| Current | 0–36 A | 24–30 A | Above 30 A |
| Power | 0–1728 W | 1152–1440 W | Above 1440 W |
| Internal temperature | 0–90 °C | 55–65 °C | Above 65 °C |
Only 48 V nominal and 30 A were supplied. The other thresholds need confirmation; 1440 W is the provisional 48 × 30 reference. Markers clamp to the track, but numeric readings remain unclamped.
Labels and containers own layout. `status_gauge.gd` draws tracks and markers; `vehicle_status_art.gd` draws the watermark and status background. Warning/fault backgrounds alternate shades once per second; `pulse_alerts` disables that behavior. After two seconds without telemetry, the status is marked STALE and readings are held. Colors come from the shared palette and generated themes.
`mode_icon` selects the SVG watermark (stationkeep and ROV samples are in `assets/vector/status/`). Edit `ModeIcon` anchors, rotation, and size in the scene. Readings are right-aligned beside a fixed-width, left-aligned limits column. Off-phase warning/fault shades are derived in the shared palette.
+2 -1
View File
@@ -11,6 +11,7 @@ config_version=5
[application] [application]
config/name="SurfaceSide-UI" config/name="SurfaceSide-UI"
run/main_scene="res://scenes/layout_landscape.tscn"
config/features=PackedStringArray("4.7", "Forward Plus") config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
@@ -31,4 +32,4 @@ theme/custom="res://themes/theme.tres"
[rendering] [rendering]
rendering_device/driver.windows="d3d12" rendering_device/driver.windows="d3d12"
environment/defaults/default_clear_color=Color(0.067, 0.055, 0.067, 1) environment/defaults/default_clear_color=Color(0.069019616, 0.058235295, 0.06686275, 1)
+2 -9
View File
@@ -1,19 +1,12 @@
[gd_scene format=3 uid="uid://j2vmxn1p5hej"] [gd_scene format=3 uid="uid://j2vmxn1p5hej"]
[ext_resource type="Script" uid="uid://cgcgn8lwigumx" path="res://scripts/angle_display_3d.gd" id="1_8a3he"] [ext_resource type="Script" uid="uid://cgcgn8lwigumx" path="res://scripts/angle_display_3d.gd" id="1_8a3he"]
[ext_resource type="Texture2D" uid="uid://cjw3n47kehnfj" path="res://assets/vector/dial.svg" id="1_vggt3"] [ext_resource type="Material" path="res://themes/compass_material.tres" id="1_material"]
[ext_resource type="PackedScene" uid="uid://0qb2f8igltax" path="res://assets/models/grey-seal-halichoerus-grypus/grey_seal_clean.glb" id="2_8a3he"] [ext_resource type="PackedScene" uid="uid://0qb2f8igltax" path="res://assets/models/grey-seal-halichoerus-grypus/grey_seal_clean.glb" id="2_8a3he"]
[ext_resource type="Script" uid="uid://cv5cmdlub5gtx" path="res://scenes/node_3d.gd" id="3_mut2o"] [ext_resource type="Script" uid="uid://cv5cmdlub5gtx" path="res://scenes/node_3d.gd" id="3_mut2o"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mut2o"]
transparency = 1
cull_mode = 2
shading_mode = 0
albedo_color = Color(0.44361365, 0.72118443, 0, 1)
albedo_texture = ExtResource("1_vggt3")
[sub_resource type="PlaneMesh" id="PlaneMesh_wivqi"] [sub_resource type="PlaneMesh" id="PlaneMesh_wivqi"]
material = SubResource("StandardMaterial3D_mut2o") material = ExtResource("1_material")
[node name="Compass" type="Node3D" unique_id=1678590387] [node name="Compass" type="Node3D" unique_id=1678590387]
+36
View File
@@ -0,0 +1,36 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/console_demo.gd" id="1"]
[ext_resource type="PackedScene" path="res://components/console_panel.tscn" id="2"]
[node name="ConsoleDemo" type="VBoxContainer"]
offset_left = 24.0
offset_top = 24.0
offset_right = 924.0
offset_bottom = 700.0
theme_override_constants/separation = 12
script = ExtResource("1")
[node name="Notice" type="Label" parent="."]
layout_mode = 2
text = "SIMULATED / CONSOLE · F5–F8 choose a tab · Esc releases focus"
[node name="ConsolePanel" parent="." instance=ExtResource("2")]
layout_mode = 2
custom_minimum_size = Vector2(900, 520)
[node name="Controls" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Contrast" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "High contrast"
[node name="Feed" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "Simulated journal feed"
button_pressed = true
[node name="LogTimer" type="Timer" parent="."]
wait_time = 1.0
autostart = true
+62
View File
@@ -0,0 +1,62 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/layout_canvas.tscn" id="canvas"]
[ext_resource type="PackedScene" path="res://components/layout_item.tscn" id="item"]
[ext_resource type="PackedScene" path="res://components/camera_layout_slot.tscn" id="camera"]
[ext_resource type="PackedScene" path="res://components/vehicle_status_panel.tscn" id="vehicle"]
[ext_resource type="PackedScene" path="res://components/motion_panel.tscn" id="motion"]
[ext_resource type="PackedScene" path="res://components/environment_panel.tscn" id="environment"]
[ext_resource type="PackedScene" path="res://components/link_panel.tscn" id="link"]
[ext_resource type="PackedScene" path="res://components/layout_slot.tscn" id="slot"]
[node name="DisplayLeft" instance=ExtResource("canvas")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
grid_size = Vector2i(18, 32)
config_path = "user://layouts/portrait_left.json"
legacy_config_path = "user://layout_portrait_left.cfg"
[node name="Camera" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "camera"
cell_rect = Rect2i(0, 0, 18, 11)
minimum_cells = Vector2i(8, 5)
reference_size = Vector2(960, 600)
[node name="Content" parent="Camera" instance=ExtResource("camera")]
layout_mode = 0
[node name="Vehicle" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "vehicle"
cell_rect = Rect2i(0, 11, 10, 8)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 440)
[node name="Content" parent="Vehicle" instance=ExtResource("vehicle")]
layout_mode = 0
[node name="Motion" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "motion"
cell_rect = Rect2i(0, 19, 10, 7)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 410)
background_path = NodePath("Content/Background")
[node name="Content" parent="Motion" instance=ExtResource("motion")]
layout_mode = 0
[node name="Environment" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "environment"
cell_rect = Rect2i(10, 11, 8, 8)
minimum_cells = Vector2i(4, 3)
reference_size = Vector2(360, 240)
[node name="Content" parent="Environment" instance=ExtResource("environment")]
layout_mode = 0
+57
View File
@@ -0,0 +1,57 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/layout_canvas.tscn" id="canvas"]
[ext_resource type="PackedScene" path="res://components/layout_item.tscn" id="item"]
[ext_resource type="PackedScene" path="res://components/camera_layout_slot.tscn" id="camera"]
[ext_resource type="PackedScene" path="res://components/vehicle_status_panel.tscn" id="vehicle"]
[ext_resource type="PackedScene" path="res://components/motion_panel.tscn" id="motion"]
[ext_resource type="PackedScene" path="res://components/environment_panel.tscn" id="environment"]
[ext_resource type="PackedScene" path="res://components/link_panel.tscn" id="link"]
[ext_resource type="PackedScene" path="res://components/layout_slot.tscn" id="slot"]
[node name="DisplayRight" instance=ExtResource("canvas")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
grid_size = Vector2i(18, 32)
config_path = "user://layouts/portrait_right.json"
legacy_config_path = "user://layout_portrait_right.cfg"
[node name="VideoVR" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "videovr"
cell_rect = Rect2i(0, 0, 18, 17)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 400)
fill_allocation = true
[node name="Content" parent="VideoVR" instance=ExtResource("slot")]
layout_mode = 0
title = "VIDEO / VR"
description = "Latency history and breakdown · headset connection · device · resolution"
[node name="Link" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "link"
cell_rect = Rect2i(0, 17, 18, 5)
minimum_cells = Vector2i(4, 3)
reference_size = Vector2(360, 240)
[node name="Content" parent="Link" instance=ExtResource("link")]
layout_mode = 0
[node name="OperatorTools" parent="." instance=ExtResource("item")]
layout_mode = 0
panel_id = "operatortools"
cell_rect = Rect2i(0, 22, 18, 10)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 400)
fill_allocation = true
[node name="Content" parent="OperatorTools" instance=ExtResource("slot")]
layout_mode = 0
title = "OPERATOR TOOLS / RESERVED"
description = "Recording · manipulator · terminal / events"
+52
View File
@@ -0,0 +1,52 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/environment_demo.gd" id="1"]
[ext_resource type="PackedScene" path="res://components/environment_panel.tscn" id="2"]
[node name="EnvironmentDemo" type="VBoxContainer"]
offset_left = 24.0
offset_top = 24.0
offset_right = 624.0
offset_bottom = 420.0
theme_override_constants/separation = 16
script = ExtResource("1")
[node name="Notice" type="Label" parent="."]
layout_mode = 2
text = "SIMULATED / ENVIRONMENT INSTRUMENT"
[node name="EnvironmentPanel" parent="." instance=ExtResource("2")]
layout_mode = 2
size_flags_horizontal = 4
[node name="Controls" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="State" type="OptionButton" parent="Controls"]
layout_mode = 2
selected = 1
item_count = 8
popup/item_0/text = "Surface / zero"
popup/item_0/id = 0
popup/item_1/text = "Shallow operation"
popup/item_1/id = 1
popup/item_2/text = "Operating depth"
popup/item_2/id = 2
popup/item_3/text = "Near depth limit"
popup/item_3/id = 3
popup/item_4/text = "Pressure near limit"
popup/item_4/id = 4
popup/item_5/text = "Over limits"
popup/item_5/id = 5
popup/item_6/text = "Stop feed / stale"
popup/item_6/id = 6
popup/item_7/text = "No telemetry"
popup/item_7/id = 7
[node name="Contrast" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "High contrast"
[node name="Note" type="Label" parent="."]
layout_mode = 2
text = "Zero-inclusive log scale · shallow depths expanded"
+49
View File
@@ -0,0 +1,49 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://scenes/display_left.tscn" id="left"]
[ext_resource type="PackedScene" path="res://scenes/display_right.tscn" id="right"]
[ext_resource type="Script" path="res://scripts/layout_preview.gd" id="preview"]
[ext_resource type="PackedScene" path="res://components/layout_overview.tscn" id="overview"]
[node name="DualPortraitLayout" type="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("preview")
configuration = 1
[node name="Overview" parent="." instance=ExtResource("overview")]
layout_mode = 1
grid_size = Vector2i(18, 32)
[node name="Canvas" parent="Overview/Dashboard" instance=ExtResource("left")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="RightWindow" type="Window" parent="."]
title = "ROV / right portrait"
visible = false
size = Vector2i(540, 960)
content_scale_size = Vector2i(1080, 1920)
content_scale_mode = 1
content_scale_aspect = 1
[node name="RightOverview" parent="RightWindow" instance=ExtResource("overview")]
layout_mode = 1
hot_corner_enabled = false
grid_size = Vector2i(18, 32)
[node name="Canvas" parent="RightWindow/RightOverview/Dashboard" instance=ExtResource("right")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
+106
View File
@@ -0,0 +1,106 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/layout_canvas.tscn" id="canvas"]
[ext_resource type="PackedScene" path="res://components/layout_item.tscn" id="item"]
[ext_resource type="PackedScene" path="res://components/camera_layout_slot.tscn" id="camera"]
[ext_resource type="PackedScene" path="res://components/vehicle_status_panel.tscn" id="vehicle"]
[ext_resource type="PackedScene" path="res://components/motion_panel.tscn" id="motion"]
[ext_resource type="PackedScene" path="res://components/layout_slot.tscn" id="slot"]
[ext_resource type="Script" path="res://scripts/layout_preview.gd" id="preview"]
[ext_resource type="PackedScene" path="res://components/layout_overview.tscn" id="overview"]
[ext_resource type="PackedScene" path="res://components/link_panel.tscn" id="link"]
[ext_resource type="PackedScene" path="res://components/environment_panel.tscn" id="environment"]
[node name="LandscapeLayout" type="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("preview")
[node name="Overview" parent="." instance=ExtResource("overview")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Canvas" parent="Overview/Dashboard" instance=ExtResource("canvas")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
config_path = "user://layouts/landscape.json"
legacy_config_path = "user://layout_landscape.cfg"
[node name="Camera" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "camera"
cell_rect = Rect2i(0, 0, 16, 10)
minimum_cells = Vector2i(8, 5)
reference_size = Vector2(960, 600)
[node name="Content" parent="Overview/Dashboard/Canvas/Camera" instance=ExtResource("camera")]
layout_mode = 0
[node name="Vehicle" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "vehicle"
cell_rect = Rect2i(16, 0, 10, 8)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 440)
[node name="Content" parent="Overview/Dashboard/Canvas/Vehicle" instance=ExtResource("vehicle")]
layout_mode = 0
[node name="Motion" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "motion"
cell_rect = Rect2i(16, 8, 10, 7)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 410)
background_path = NodePath("Content/Background")
[node name="Content" parent="Overview/Dashboard/Canvas/Motion" instance=ExtResource("motion")]
layout_mode = 0
[node name="Environment" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "environment"
cell_rect = Rect2i(26, 0, 6, 8)
minimum_cells = Vector2i(4, 3)
reference_size = Vector2(360, 240)
[node name="Content" parent="Overview/Dashboard/Canvas/Environment" instance=ExtResource("environment")]
layout_mode = 0
[node name="VideoVR" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "videovr"
cell_rect = Rect2i(0, 10, 16, 8)
minimum_cells = Vector2i(6, 4)
reference_size = Vector2(600, 400)
fill_allocation = true
[node name="Content" parent="Overview/Dashboard/Canvas/VideoVR" instance=ExtResource("slot")]
layout_mode = 0
title = "VIDEO / VR"
description = "Latency breakdown · headset · resolution"
[node name="Link" parent="Overview/Dashboard/Canvas" instance=ExtResource("item")]
layout_mode = 0
panel_id = "link"
cell_rect = Rect2i(26, 8, 6, 4)
minimum_cells = Vector2i(4, 3)
reference_size = Vector2(360, 240)
[node name="Content" parent="Overview/Dashboard/Canvas/Link" instance=ExtResource("link")]
layout_mode = 0
+50
View File
@@ -0,0 +1,50 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/link_demo.gd" id="1"]
[ext_resource type="PackedScene" path="res://components/link_panel.tscn" id="2"]
[node name="LinkDemo" type="VBoxContainer"]
offset_left = 24.0
offset_top = 24.0
offset_right = 624.0
offset_bottom = 420.0
theme_override_constants/separation = 16
script = ExtResource("1")
[node name="Notice" type="Label" parent="."]
layout_mode = 2
text = "SIMULATED / LINK INSTRUMENT"
[node name="LinkPanel" parent="." instance=ExtResource("2")]
layout_mode = 2
size_flags_horizontal = 4
[node name="Controls" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="State" type="OptionButton" parent="Controls"]
layout_mode = 2
selected = 0
item_count = 7
popup/item_0/text = "Connected"
popup/item_0/id = 0
popup/item_1/text = "Near capacity"
popup/item_1/id = 1
popup/item_2/text = "Saturated"
popup/item_2/id = 2
popup/item_3/text = "Degraded"
popup/item_3/id = 3
popup/item_4/text = "Disconnected"
popup/item_4/id = 4
popup/item_5/text = "Stop feed / stale"
popup/item_5/id = 5
popup/item_6/text = "No telemetry"
popup/item_6/id = 6
[node name="Contrast" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "High contrast"
[node name="Thresholds" type="Label" parent="."]
layout_mode = 2
text = "Arc thresholds: 80% warning · 95% saturation"
+22
View File
@@ -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)"
+44
View File
@@ -0,0 +1,44 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/vehicle_status_panel.tscn" id="1"]
[ext_resource type="Script" path="res://scripts/vehicle_status_demo.gd" id="2"]
[node name="VehicleStatusDemo" type="VBoxContainer"]
offset_left = 24.0
offset_top = 24.0
offset_right = 624.0
offset_bottom = 560.0
theme_override_constants/separation = 12
script = ExtResource("2")
[node name="Notice" type="Label" parent="."]
layout_mode = 2
text = "SIMULATED / gauge thresholds are provisional"
[node name="VehicleStatusPanel" parent="." instance=ExtResource("1")]
layout_mode = 2
[node name="Controls" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="State" type="OptionButton" parent="Controls"]
layout_mode = 2
item_count = 4
selected = 0
popup/item_0/text = "Normal"
popup/item_0/id = 0
popup/item_1/text = "Warning"
popup/item_1/id = 1
popup/item_2/text = "Fault"
popup/item_2/id = 2
popup/item_3/text = "Stop feed / stale"
popup/item_3/id = 3
[node name="Contrast" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "High contrast"
[node name="Pulse" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "Pulse"
button_pressed = true
+9
View File
@@ -0,0 +1,9 @@
@tool
extends AspectRatioContainer
func _ready() -> void:
resized.connect(_update_height)
_update_height()
func _update_height() -> void:
custom_minimum_size.y = ceilf(size.x / ratio)
+1
View File
@@ -0,0 +1 @@
uid://k6buaitag78o
+32
View File
@@ -0,0 +1,32 @@
extends VBoxContainer
@onready var console = $ConsolePanel
var _log_index := 0
func _ready() -> void:
console.command_requested.connect(_demo_command)
for id in ["rov", "host", "logs", "server"]:
console.set_connection(id, true, "SIMULATED · " + id)
console.append_output("rov", "[demo] ROV shell ready. Try help, pwd or status.\n")
console.append_output("host", "[demo] Host shell ready. Try help or pwd.\n")
console.append_output("server", "[demo] Server shell ready. Try help or status.\n")
$Controls/Contrast.toggled.connect(func(high: bool) -> void:
theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres")
)
$Controls/Feed.toggled.connect(func(enabled: bool) -> void: $LogTimer.paused = not enabled)
$LogTimer.timeout.connect(_append_log)
_append_log()
func _demo_command(id: String, command: String) -> void:
match command.strip_edges():
"help": console.append_output(id, "Demo commands: help, status, pwd, clear\n")
"status": console.append_output(id, "[demo] Vehicle ready · stationkeep · no live transport\n")
"pwd": console.append_output(id, "/" + id + " (simulated)\n")
"clear": console.session(id).clear_output()
_: console.append_output(id, "[demo] Received: " + command + "\n")
func _append_log() -> void:
_log_index += 1
var level := "WARN" if _log_index % 5 == 0 else "INFO"
var message := "sample telemetry delayed" if level == "WARN" else "sample telemetry received"
console.append_output("logs", "%04d %s rov-server: %s\n" % [_log_index, level, message])
+1
View File
@@ -0,0 +1 @@
uid://blo11s7dqe2b
+104
View File
@@ -0,0 +1,104 @@
@tool
extends SelectableContainer
signal command_requested(session_id: String, command: String)
@onready var tabs: TabContainer = %Tabs
func _ready() -> void:
super._ready()
_refresh_header()
if Engine.is_editor_hint():
return
tabs.get_tab_bar().focus_mode = Control.FOCUS_NONE
tabs.get_tab_bar().gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed: activate()
)
tabs.tab_changed.connect(func(_index: int) -> void:
_last_focus = null
activate()
)
for page in _pages():
page.command_submitted.connect(_route_command.bind(page.session_id))
page.focus_requested.connect(activate)
func _pages() -> Array[Control]:
var result: Array[Control] = []
var node := get_node_or_null("%Tabs")
if node != null:
for child in node.get_children():
if child.has_method("submit_command"): result.append(child)
return result
func get_focus_shortcuts() -> Array[Key]:
var keys: Array[Key] = []
for page in _pages():
if page.shortcut != KEY_NONE: keys.append(page.shortcut)
return keys
func _refresh_header() -> void:
super._refresh_header()
var node := get_node_or_null("%Tabs") as TabContainer
if node == null:
return
var seen: Dictionary = {}
for index in range(node.get_tab_count()):
var page := node.get_tab_control(index)
if not page.has_method("submit_command"): continue
var key: Key = page.shortcut
var available := is_shortcut_available(key) and not seen.has(key)
var caption: String = page.tab_label
if key != KEY_NONE:
caption += " · " + OS.get_keycode_string(key) + (" (in use)" if not available else "")
seen[key] = true
node.set_tab_title(index, caption)
node.set_tab_tooltip(index, "Focus " + page.tab_label if available else "Shortcut already used by another panel or tab")
func activate() -> void:
super.activate()
if is_active:
var page := tabs.get_current_tab_control()
if page != null:
var target: Control = page.preferred_input()
if target.is_visible_in_tree(): target.grab_focus()
func activate_shortcut(key: Key) -> void:
for index in range(tabs.get_tab_count()):
if tabs.get_tab_control(index).shortcut == key:
tabs.current_tab = index
_last_focus = null
activate()
return
func _input(event: InputEvent) -> void:
if not Engine.is_editor_hint() and is_active and event is InputEventKey:
if event.ctrl_pressed and not event.alt_pressed and not event.meta_pressed and event.keycode in [KEY_TAB, KEY_L]:
get_viewport().set_input_as_handled()
if event.pressed and not event.echo:
if event.keycode == KEY_TAB:
tabs.current_tab = wrapi(tabs.current_tab + (-1 if event.shift_pressed else 1), 0, tabs.get_tab_count())
else:
tabs.get_current_tab_control().clear_output()
return
super._input(event)
func session(id: String) -> Control:
for page in _pages():
if page.session_id == id: return page
return null
func append_output(id: String, text: String) -> void:
var page := session(id)
if page != null: page.append_output(text)
func set_connection(id: String, connected: bool, message: String = "") -> void:
var page := session(id)
if page != null: page.set_connection(connected, message)
func _route_command(command: String, id: String) -> void:
var page := session(id)
if page == null: return
if not page.connected or command_requested.get_connections().is_empty():
page.append_output("Not connected; command was not sent.\n")
else:
command_requested.emit(id, command)
+1
View File
@@ -0,0 +1 @@
uid://dcxkla5mcrxmx
+106
View File
@@ -0,0 +1,106 @@
@tool
extends VBoxContainer
signal command_submitted(command: String)
signal focus_requested
@export var session_id := "rov"
@export var tab_label := "ROV"
@export var shortcut: Key = KEY_F5
@export var prompt := "rov $"
@export var read_only := false
@export var introduction := "ROV shell disconnected."
@export var max_scrollback_characters := 128000
var connected := false
var _buffer := ""
var _history: Array[String] = []
var _history_index := 0
var _draft := ""
@onready var output: RichTextLabel = $Output
@onready var command_input: LineEdit = $Input/Command
@onready var filter_input: LineEdit = $Filter/Text
func _ready() -> void:
$Input.visible = not read_only
$Filter.visible = read_only
$Input/Prompt.text = prompt
_buffer = introduction + "\n"
output.text = _buffer
if Engine.is_editor_hint():
return
command_input.text_submitted.connect(submit_command)
command_input.gui_input.connect(_history_input)
filter_input.text_changed.connect(func(_text: String) -> void: _render_output())
$Status/Clear.pressed.connect(clear_output)
$Status/Follow.toggled.connect(func(enabled: bool) -> void:
output.scroll_following = enabled
if enabled: output.scroll_to_line(maxi(output.get_line_count() - 1, 0))
)
for control in [output, command_input, filter_input, $Status/Clear, $Status/Follow]:
control.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
focus_requested.emit()
)
func preferred_input() -> Control:
return filter_input if read_only else command_input
func set_connection(value: bool, message: String = "") -> void:
connected = value
$Status/Connection.text = message if not message.is_empty() else ("Connected" if value else "Not connected")
func submit_command(command: String) -> bool:
if read_only or command.strip_edges().is_empty():
return false
if _history.is_empty() or _history.back() != command:
_history.append(command)
if _history.size() > 200: _history.pop_front()
_history_index = _history.size()
_draft = ""
append_output(prompt + " " + command + "\n")
command_input.clear()
command_submitted.emit(command)
return true
func append_output(text: String) -> void:
_buffer += text
var truncated := _buffer.length() > max_scrollback_characters
if truncated:
_buffer = _buffer.right(max_scrollback_characters)
var newline := _buffer.find("\n")
if newline >= 0 and newline < _buffer.length() - 1:
_buffer = _buffer.substr(newline + 1)
if truncated or (read_only and not filter_input.text.is_empty()):
_render_output()
else:
output.add_text(text)
func clear_output() -> void:
_buffer = ""
output.clear()
func _render_output() -> void:
var scroll := output.get_v_scroll_bar().value
if read_only and not filter_input.text.is_empty():
var matches := PackedStringArray()
for line in _buffer.split("\n"):
if line.to_lower().contains(filter_input.text.to_lower()):
matches.append(line)
output.text = "\n".join(matches)
else:
output.text = _buffer
if not $Status/Follow.button_pressed:
output.get_v_scroll_bar().set_deferred("value", scroll)
func _history_input(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.ctrl_pressed or event.alt_pressed or event.meta_pressed:
return
if event.keycode not in [KEY_UP, KEY_DOWN] or _history.is_empty():
return
if _history_index == _history.size():
_draft = command_input.text
_history_index = clampi(_history_index + (-1 if event.keycode == KEY_UP else 1), 0, _history.size())
command_input.text = _draft if _history_index == _history.size() else _history[_history_index]
command_input.caret_column = command_input.text.length()
command_input.accept_event()
+1
View File
@@ -0,0 +1 @@
uid://bvo8xa0w47jy6
+64
View File
@@ -0,0 +1,64 @@
@tool
extends Control
var depth_m := 0.0
var maximum_depth_m := 1000.0
var knee_m := 1.0
var warning_fraction := 0.8
var fault_fraction := 0.95
var available := false
func _ready() -> void:
resized.connect(queue_redraw)
theme_changed.connect(queue_redraw)
# log(1 + depth / knee) includes zero and is nearly linear close to the surface.
func normalized_depth(value: float) -> float:
var knee := maxf(knee_m, 0.001)
var maximum := maxf(maximum_depth_m, 0.001)
return log(1 + clampf(value, 0, maximum) / knee) / log(1 + maximum / knee)
func severity() -> int:
var ratio := depth_m / maxf(maximum_depth_m, 0.001)
return 2 if ratio >= fault_fraction else (1 if ratio >= warning_fraction else 0)
func _draw() -> void:
var top := 9.0
var bottom := maxf(top + 1, size.y - 10)
var track := get_theme_color("instrument_grid", "SurfaceSide")
var ink := get_theme_color("font_color", "Label")
var font := get_theme_default_font()
draw_line(Vector2(17, top), Vector2(17, bottom), track, 2, true)
var decade := maxf(knee_m, 0.001)
while decade < maximum_depth_m:
for multiplier in [1.0, 2.0, 5.0]:
var tick: float = decade * multiplier
if tick > 0 and tick < maximum_depth_m:
var y: float = lerpf(top, bottom, normalized_depth(tick))
draw_line(Vector2(14, y), Vector2(21, y), track, 1, true)
decade *= 10
var last_label_y := -100.0
var last_label_value := -1.0
var ticks: Array[float] = [0, maxf(knee_m, 0.001) * 2]
decade = maxf(knee_m, 0.001) * 10
while decade < maximum_depth_m:
ticks.append(decade)
decade *= 10
ticks.append(maximum_depth_m)
for tick in ticks:
if tick < 0 or tick > maximum_depth_m or is_equal_approx(tick, last_label_value):
continue
var y := lerpf(top, bottom, normalized_depth(tick))
if tick != maximum_depth_m and (y - last_label_y < 20 or (bottom - y < 20 and tick != 0)):
continue
draw_line(Vector2(12, y), Vector2(23, y), ink, 1, true)
draw_string(font, Vector2(29, y + 5), String.num(tick, 3).trim_suffix(".0"), HORIZONTAL_ALIGNMENT_LEFT, -1, 14, ink)
last_label_y = y
last_label_value = tick
if available:
var y := lerpf(top, bottom, normalized_depth(depth_m))
var role: String = ["instrument_accent", "warning", "fault"][severity()]
var accent := get_theme_color(role, "SurfaceSide")
draw_line(Vector2(17, top), Vector2(17, y), accent, 3, true)
draw_colored_polygon(PackedVector2Array([Vector2(1, y - 5), Vector2(1, y + 5), Vector2(12, y)]), accent)
draw_line(Vector2(12, y), Vector2(23, y), accent, 3, true)
+1
View File
@@ -0,0 +1 @@
uid://cbobdfddbbddo
+23
View File
@@ -0,0 +1,23 @@
extends VBoxContainer
var elapsed := 0.0
@onready var panel = $EnvironmentPanel
func _ready() -> void:
$Controls/Contrast.toggled.connect(func(high: bool) -> void:
theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres")
)
$Controls/State.item_selected.connect(func(index: int) -> void:
if index == 7: panel.clear_telemetry()
)
func _process(delta: float) -> void:
elapsed += delta
match $Controls/State.selected:
0: panel.set_telemetry(0, 0.10, 12.4)
1: panel.set_telemetry(0.75 + sin(elapsed) * 0.25, 0.11, 12.4)
2: panel.set_telemetry(248.6, 2.54, 7.4)
3: panel.set_telemetry(850, 8.6, 4.2)
4: panel.set_telemetry(400, 9.6, -1.5)
5: panel.set_telemetry(1010, 10.1, 3.2)
6, 7: pass
+1
View File
@@ -0,0 +1 @@
uid://5qywekq0w4ne
+67
View File
@@ -0,0 +1,67 @@
@tool
extends PanelContainer
@export var maximum_depth_m := 1000.0
@export var maximum_pressure_mpa := 10.0
@export var shallow_scale_knee_m := 1.0
@export_range(0, 1) var warning_fraction := 0.8
@export_range(0, 1) var fault_fraction := 0.95
@export var stale_after_seconds := 2.0
var _depth := 0.0
var _pressure := 0.0
var _temperature := 0.0
var _received_ms := -1
var _stale := false
func _ready() -> void:
_refresh()
## Depth in metres, pressure in MPa, water temperature in Celsius; supplied independently.
func set_telemetry(depth_m: float, pressure_mpa: float, water_temperature_c: float) -> bool:
if not is_finite(depth_m) or not is_finite(pressure_mpa) or not is_finite(water_temperature_c) or pressure_mpa < 0:
return false
_depth = depth_m
_pressure = pressure_mpa
_temperature = water_temperature_c
_received_ms = Time.get_ticks_msec()
_refresh()
return true
func clear_telemetry() -> void:
_received_ms = -1
_refresh()
func _severity(ratio: float) -> int:
return 2 if ratio >= maxf(warning_fraction, fault_fraction) else (1 if ratio >= warning_fraction else 0)
func _process(_delta: float) -> void:
var stale := _received_ms >= 0 and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
if stale != _stale:
_refresh()
func _refresh() -> void:
if not is_node_ready():
return
var measured := _received_ms >= 0
_stale = measured and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
var depth_ratio := _depth / maxf(maximum_depth_m, 0.001)
var pressure_ratio := _pressure / maxf(maximum_pressure_mpa, 0.001)
var worst_ratio := maxf(depth_ratio, pressure_ratio)
var variants := ["Label", "InstrumentWarning", "InstrumentFault"]
%DepthValue.text = "%.2f m" % _depth if measured else "— m"
%PressureValue.text = "%.2f MPa" % _pressure if measured else "— MPa"
%TemperatureValue.text = "%.1f °C" % _temperature if measured else "— °C"
%DepthReference.text = "/ %s m" % String.num(maximum_depth_m, 2).trim_suffix(".0")
%PressureReference.text = "/ %s MPa" % String.num(maximum_pressure_mpa, 2).trim_suffix(".0")
%DepthTitle.theme_type_variation = variants[_severity(depth_ratio)] if measured else "Label"
%PressureTitle.theme_type_variation = variants[_severity(pressure_ratio)] if measured else "Label"
%State.text = "NO TELEMETRY" if not measured else ("STALE" if _stale else ("OVER LIMIT" if worst_ratio > 1 else ("NEAR LIMIT" if _severity(worst_ratio) > 0 else "")))
%State.theme_type_variation = "InstrumentMuted" if not measured else ("InstrumentWarning" if _stale else variants[_severity(worst_ratio)])
%DepthScale.depth_m = _depth
%DepthScale.maximum_depth_m = maxf(maximum_depth_m, 0.001)
%DepthScale.knee_m = maxf(shallow_scale_knee_m, 0.001)
%DepthScale.warning_fraction = warning_fraction
%DepthScale.fault_fraction = maxf(warning_fraction, fault_fraction)
%DepthScale.available = measured
%DepthScale.queue_redraw()
+1
View File
@@ -0,0 +1 @@
uid://dnafkcowopwod
+345
View File
@@ -0,0 +1,345 @@
@tool
extends Control
signal layout_saved
signal save_failed
signal load_failed
const CATALOG = preload("res://scripts/layout_catalog.gd")
const STORE = preload("res://scripts/layout_store.gd")
@export var grid_size := Vector2i(32, 18)
@export var panel_gap := 16.0:
set(value):
panel_gap = maxf(value, 0.0)
if is_node_ready():
arrange_items()
@export var config_path := ""
@export var legacy_config_path := ""
var edit_mode := false
var selected: Control
var _items: Array[Control] = []
var _defaults: Dictionary = {}
var _operation := ""
var _origin := Rect2i()
var _candidate := Rect2i()
var _offset := Vector2.ZERO
@onready var overlay: Control = $EditOverlay
func _ready() -> void:
for child in get_children():
if child.has_method("arrange_content"):
if child.element_type.is_empty():
child.element_type = child.panel_id
_items.append(child)
_defaults[child.panel_id] = child.cell_rect
resized.connect(arrange_items)
get_window().focus_exited.connect(cancel_interaction)
$EditOverlay/Actions/Resize.gui_input.connect(_resize_input)
$EditOverlay/Actions/Delete.pressed.connect(delete_selected)
arrange_items()
func cell_size() -> Vector2:
return size / Vector2(grid_size)
func arrange_items() -> void:
for item in _items:
item.padding = panel_gap / 2
item.position = Vector2(item.cell_rect.position) * cell_size()
item.size = Vector2(item.cell_rect.size) * cell_size()
item.arrange_content()
_update_overlay()
func set_edit_mode(active: bool) -> void:
cancel_interaction()
edit_mode = active
for item in _items:
_set_item_interaction(item)
if not active:
selected = null
overlay.visible = active
_update_overlay()
func handle_grid_input(event: InputEvent) -> void:
if not edit_mode:
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
var point: Vector2 = event.position
selected = null
for item in _items:
if item.visible and Rect2(item.position, item.size).has_point(point):
selected = item
break
if selected != null:
_begin_interaction("move", point)
_update_overlay()
func _input(event: InputEvent) -> void:
if not edit_mode or _operation.is_empty():
return
if event is InputEventMouseMotion:
update_interaction(get_global_transform_with_canvas().affine_inverse() * event.position)
get_viewport().set_input_as_handled()
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
finish_interaction()
func _resize_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed and selected != null:
var button: Control = $EditOverlay/Actions/Resize
var point: Vector2 = get_global_transform_with_canvas().affine_inverse() * button.get_global_transform_with_canvas() * event.position
_begin_interaction("resize", point)
$EditOverlay/Actions/Resize.accept_event()
func _begin_interaction(operation: String, point: Vector2) -> void:
_operation = operation
_origin = selected.cell_rect
_candidate = _origin
_offset = point / cell_size()
_update_overlay()
func update_interaction(point: Vector2) -> void:
if selected == null or _operation.is_empty():
return
var delta := Vector2i((point / cell_size() - _offset).round())
_candidate = Rect2i(_origin.position + delta, _origin.size) if _operation == "move" else Rect2i(_origin.position, _origin.size + delta)
if can_place(selected, _candidate):
selected.cell_rect = _candidate
arrange_items()
_update_overlay()
func can_place(item: Control, rect: Rect2i) -> bool:
if rect.position.x < 0 or rect.position.y < 0 or rect.end.x > grid_size.x or rect.end.y > grid_size.y:
return false
if rect.size.x < item.minimum_cells.x or rect.size.y < item.minimum_cells.y:
return false
for other in _items:
if other != item and other.visible and rect.intersects(other.cell_rect):
return false
return true
func finish_interaction() -> void:
if selected == null or _operation.is_empty():
return
selected.cell_rect = _candidate if can_place(selected, _candidate) else _origin
var changed: bool = selected.cell_rect != _origin
_operation = ""
arrange_items()
if changed:
save_layout()
func cancel_interaction() -> void:
if selected != null and not _operation.is_empty():
selected.cell_rect = _origin
_operation = ""
if is_node_ready():
arrange_items()
func delete_selected() -> void:
if selected == null:
return
cancel_interaction()
selected.hide()
selected = null
_update_overlay()
save_layout()
func reset_layout() -> void:
cancel_interaction()
selected = null
_remove_dynamic_items()
for item in _items:
item.cell_rect = _defaults[item.panel_id]
item.show()
arrange_items()
save_layout()
func add_element(element_type: String) -> Control:
if not edit_mode or not CATALOG.ENTRIES.has(element_type):
return null
cancel_interaction()
var entry: Dictionary = CATALOG.ENTRIES[element_type]
var item: Control
for candidate in _items:
if not candidate.visible and candidate.element_type == element_type:
item = candidate
break
var minimum: Vector2i = item.minimum_cells if item != null else entry.minimum
var preferred: Vector2i = Vector2i(entry.size).max(minimum)
var rect := _first_free(preferred)
if rect.size == Vector2i.ZERO:
rect = _first_free(minimum)
if rect.size == Vector2i.ZERO:
return null
if item == null:
var id := element_type
var suffix := 2
while _find_item(id) != null:
id = "%s_%d" % [element_type, suffix]
suffix += 1
item = CATALOG.create_item(element_type, id, rect)
add_child(item)
_items.append(item)
item.cell_rect = rect
item.show()
_set_item_interaction(item)
selected = item
arrange_items()
save_layout()
return item
func _first_free(dimensions: Vector2i) -> Rect2i:
for y in range(grid_size.y - dimensions.y + 1):
for x in range(grid_size.x - dimensions.x + 1):
var rect := Rect2i(Vector2i(x, y), dimensions)
var free := true
for item in _items:
if item.visible and rect.intersects(item.cell_rect):
free = false
break
if free:
return rect
return Rect2i()
func _find_item(id: String) -> Control:
for item in _items:
if item.panel_id == id:
return item
return null
func _remove_dynamic_items() -> void:
for item in _items.duplicate():
if not _defaults.has(item.panel_id):
_items.erase(item)
remove_child(item)
item.queue_free()
func _record(item: Control, rect: Rect2i) -> Dictionary:
return {"id": item.panel_id, "type": item.element_type,
"rect": [rect.position.x, rect.position.y, rect.size.x, rect.size.y]}
func save_layout() -> void:
if config_path.is_empty():
return
var elements: Array = []
for item in _items:
if item.visible:
elements.append(_record(item, item.cell_rect))
var document := {"version": 1, "grid": [grid_size.x, grid_size.y], "elements": elements}
if STORE.write_json(config_path, document) != OK:
save_failed.emit()
else:
layout_saved.emit()
func load_layout() -> void:
if config_path.is_empty():
return
if not FileAccess.file_exists(config_path):
_migrate_legacy()
return
var decoded := _decode_document(STORE.read_json(config_path))
if decoded.is_empty():
load_failed.emit()
return
_apply_records(decoded.records)
func _decode_document(document: Variant) -> Dictionary:
if not document is Dictionary or document.get("version") != 1:
return {}
var dimensions = document.get("grid")
if not STORE.integer_array(dimensions, 2) or (dimensions[0] != grid_size.x or dimensions[1] != grid_size.y):
return {}
var elements = document.get("elements")
if not elements is Array or elements.size() > grid_size.x * grid_size.y:
return {}
var ids: Dictionary = {}
var occupied: Array[Rect2i] = []
var records: Array = []
for record in elements:
if not record is Dictionary:
return {}
var id = record.get("id")
var element_type = record.get("type")
var coordinates = record.get("rect")
if not id is String or id.length() > 64 or not id.is_valid_identifier() or ids.has(id):
return {}
if not element_type is String or not CATALOG.ENTRIES.has(element_type) or not STORE.integer_array(coordinates, 4):
return {}
for coordinate in coordinates:
if absf(float(coordinate)) > maxi(grid_size.x, grid_size.y):
return {}
var rect := Rect2i(coordinates[0], coordinates[1], coordinates[2], coordinates[3])
var existing := _find_item(id)
if existing != null and existing.element_type != element_type:
return {}
var minimum: Vector2i = existing.minimum_cells if existing != null else CATALOG.ENTRIES[element_type].minimum
if rect.position.x < 0 or rect.position.y < 0 or rect.end.x > grid_size.x or rect.end.y > grid_size.y or rect.size.x < minimum.x or rect.size.y < minimum.y:
return {}
for other in occupied:
if rect.intersects(other):
return {}
occupied.append(rect)
ids[id] = true
records.append({"id": id, "type": element_type, "rect": rect})
return {"records": records}
func _apply_records(records: Array) -> void:
cancel_interaction()
selected = null
_remove_dynamic_items()
for item in _items:
item.hide()
for record in records:
var item := _find_item(record.id)
if item == null:
item = CATALOG.create_item(record.type, record.id, record.rect)
add_child(item)
_items.append(item)
item.cell_rect = record.rect
item.show()
_set_item_interaction(item)
arrange_items()
func _migrate_legacy() -> void:
if legacy_config_path.is_empty() or not FileAccess.file_exists(legacy_config_path):
return
var config := ConfigFile.new()
if config.load(legacy_config_path) != OK or config.get_value("layout", "grid") != grid_size:
load_failed.emit()
return
var elements: Array = []
for item in _items:
if not _defaults.has(item.panel_id):
continue
var rect = config.get_value(item.panel_id, "rect", _defaults[item.panel_id])
var enabled = config.get_value(item.panel_id, "visible", true)
if not rect is Rect2i or not enabled is bool:
load_failed.emit()
return
if enabled:
elements.append(_record(item, rect))
var decoded := _decode_document({"version": 1, "grid": [grid_size.x, grid_size.y], "elements": elements})
if decoded.is_empty():
load_failed.emit()
return
_apply_records(decoded.records)
save_layout()
func _update_overlay() -> void:
if not is_instance_valid(overlay):
return
var actions: Control = $EditOverlay/Actions
actions.visible = edit_mode and selected != null
if selected != null:
overlay.selected_rect = Rect2(selected.position, selected.size)
overlay.candidate_rect = Rect2(Vector2(_candidate.position) * cell_size(), Vector2(_candidate.size) * cell_size())
overlay.candidate_valid = can_place(selected, _candidate)
actions.position = selected.position + Vector2(selected.size.x - 116, 4)
overlay.show_candidate = not _operation.is_empty()
overlay.queue_redraw()
func _set_item_interaction(item: Control) -> void:
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if edit_mode else Control.MOUSE_BEHAVIOR_INHERITED
var content := item.get_node_or_null("Content")
if content != null and content.has_method("set_layout_editing"):
content.set_layout_editing(edit_mode)
+1
View File
@@ -0,0 +1 @@
uid://bqrapdh6yddl8
+32
View File
@@ -0,0 +1,32 @@
extends RefCounted
const ITEM = preload("res://components/layout_item.tscn")
const ENTRIES = {
"camera": {"title": "Camera", "description": "16:9 video surface with a stream header.", "scene": preload("res://components/camera_layout_slot.tscn"), "size": Vector2i(16, 10), "minimum": Vector2i(8, 5), "reference": Vector2(960, 600)},
"vehicle": {"title": "Vehicle status", "description": "Mode, faults, electrical readings and internal temperature.", "scene": preload("res://components/vehicle_status_panel.tscn"), "size": Vector2i(10, 8), "minimum": Vector2i(6, 4), "reference": Vector2(600, 440)},
"motion": {"title": "Motion", "description": "Attitude, heading, acceleration and angular momentum.", "scene": preload("res://components/motion_panel.tscn"), "size": Vector2i(10, 7), "minimum": Vector2i(6, 4), "reference": Vector2(600, 410), "background": NodePath("Content/Background")},
"environment": {"title": "Environment", "description": "Depth with a shallow-expanded scale, outside pressure and water temperature.", "scene": preload("res://components/environment_panel.tscn"), "size": Vector2i(6, 5), "minimum": Vector2i(4, 3), "reference": Vector2(360, 240)},
"link": {"title": "Link", "description": "Connection state, traffic utilization and packet loss.", "scene": preload("res://components/link_panel.tscn"), "size": Vector2i(6, 4), "minimum": Vector2i(4, 3), "reference": Vector2(360, 240)},
"videovr": {"title": "Video / VR", "description": "Reserved for latency, headset connection and resolution.", "scene": preload("res://components/layout_slot.tscn"), "size": Vector2i(12, 8), "minimum": Vector2i(6, 4), "reference": Vector2(600, 400), "fill": true},
"console": {"title": "Console", "description": "Tabbed ROV shell, host shell, journal logs and server shell. F5–F8 select tabs.", "scene": preload("res://components/console_panel.tscn"), "size": Vector2i(16, 9), "minimum": Vector2i(10, 6), "reference": Vector2(900, 520)},
"operatortools": {"title": "Operator tools", "description": "Reserved for recording, manipulator controls and terminal.", "scene": preload("res://components/layout_slot.tscn"), "size": Vector2i(10, 6), "minimum": Vector2i(6, 4), "reference": Vector2(600, 400), "fill": true},
}
static func create_item(element_type: String, id: String, rect: Rect2i) -> Control:
var entry: Dictionary = ENTRIES[element_type]
var item = ITEM.instantiate()
item.name = id
item.panel_id = id
item.element_type = element_type
item.cell_rect = rect
item.minimum_cells = entry.minimum
item.reference_size = entry.reference
item.fill_allocation = entry.get("fill", false)
item.background_path = entry.get("background", NodePath(""))
var content = entry.scene.instantiate()
content.name = "Content"
if item.fill_allocation:
content.title = entry.title.to_upper()
content.description = entry.description
item.add_child(content)
return item
+1
View File
@@ -0,0 +1 @@
uid://0hiij8bwyn7w
+19
View File
@@ -0,0 +1,19 @@
@tool
extends Control
var selected_rect := Rect2()
var candidate_rect := Rect2()
var show_candidate := false
var candidate_valid := true
func _draw() -> void:
if not $Actions.visible:
return
var accent := get_theme_color("accent", "SurfaceSide")
draw_rect(selected_rect.grow(-1), accent, false, 2)
if show_candidate:
var ink := accent if candidate_valid else get_theme_color("fault", "SurfaceSide")
var fill := ink
fill.a = 0.12
draw_rect(candidate_rect.abs(), fill)
draw_rect(candidate_rect.abs(), ink, false, 3)
+1
View File
@@ -0,0 +1 @@
uid://cwbk5g4yhdfd6
+27
View File
@@ -0,0 +1,27 @@
@tool
extends ColorRect
@export var grid_size := Vector2i(32, 18):
set(value):
grid_size = value
if is_node_ready():
_update_material()
@export var monitor_boundary := 0:
set(value):
monitor_boundary = value
if is_node_ready():
_update_material()
func _ready() -> void:
theme_changed.connect(_update_material)
_update_material()
func _gui_input(_event: InputEvent) -> void:
accept_event()
func _update_material() -> void:
var shader_material := material as ShaderMaterial
shader_material.set_shader_parameter("grid_size", Vector2(grid_size))
shader_material.set_shader_parameter("monitor_boundary", float(monitor_boundary))
shader_material.set_shader_parameter("line_color", get_theme_color("instrument_grid", "SurfaceSide"))
shader_material.set_shader_parameter("boundary_color", get_theme_color("accent", "SurfaceSide"))
+1
View File
@@ -0,0 +1 @@
uid://n4eb1a6j5j88
+44
View File
@@ -0,0 +1,44 @@
@tool
extends Control
const EMBEDDED_PANEL = preload("res://themes/layout_embedded_panel.tres")
@export var panel_id := "panel"
@export var element_type := ""
@export var cell_rect := Rect2i(0, 0, 6, 4)
@export var minimum_cells := Vector2i(4, 3)
@export var reference_size := Vector2(600, 440)
@export var fill_allocation := false
@export var padding := 8.0
@export_node_path("Control") var background_path := NodePath("")
func _ready() -> void:
clip_contents = true
resized.connect(arrange_content)
if has_node("Content") and $Content is PanelContainer:
$Content.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
if not background_path.is_empty():
var background := get_node_or_null(background_path) as Control
if background != null:
background.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
if has_node("Content") and $Content.has_method("set_frame_host"):
$Content.set_frame_host($Background)
arrange_content.call_deferred()
func arrange_content() -> void:
if not has_node("Content"):
return
var content: Control = $Content
var available := (size - Vector2.ONE * padding * 2).max(Vector2.ZERO)
$Background.position = Vector2.ONE * padding
$Background.size = available
content.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
if fill_allocation:
content.scale = Vector2.ONE
content.position = Vector2.ONE * padding
content.size = available
else:
content.size = reference_size
var factor := maxf(0.01, minf(available.x / reference_size.x, available.y / reference_size.y))
content.scale = Vector2.ONE * factor
content.position = (size - reference_size * factor) / 2
+1
View File
@@ -0,0 +1 @@
uid://byjnf0ae0jr2y
+30
View File
@@ -0,0 +1,30 @@
extends PanelContainer
signal element_requested(element_type: String)
const CATALOG = preload("res://scripts/layout_catalog.gd")
var _types: Array = []
func _ready() -> void:
_types = CATALOG.ENTRIES.keys()
for element_type in _types:
$Margin/Body/List.add_item(CATALOG.ENTRIES[element_type].title)
$Margin/Body/List.item_selected.connect(_select)
$Margin/Body/List.item_activated.connect(func(_index: int) -> void: _request())
$Margin/Body/Add.pressed.connect(_request)
$Margin/Body/Header/Close.pressed.connect(hide)
$Margin/Body/List.select(0)
_select(0)
func _select(index: int) -> void:
var entry: Dictionary = CATALOG.ENTRIES[_types[index]]
$Margin/Body/Description.text = entry.description
$Margin/Body/Size.text = "%d × %d cells · minimum %d × %d" % [entry.size.x, entry.size.y, entry.minimum.x, entry.minimum.y]
$Margin/Body/Status.text = ""
func _request() -> void:
var indices: PackedInt32Array = $Margin/Body/List.get_selected_items()
if not indices.is_empty():
element_requested.emit(_types[indices[0]])
func show_no_space() -> void:
$Margin/Body/Status.text = "No space available. Move, resize or remove a panel first."
+1
View File
@@ -0,0 +1 @@
uid://cwdwpabj62mhl
+140
View File
@@ -0,0 +1,140 @@
extends Control
signal overview_changed(active: bool)
const FOCUS_SCOPES = preload("res://scripts/selectable_container.gd")
@export var grid_size := Vector2i(32, 18):
set(value):
grid_size = Vector2i(maxi(1, value.x), maxi(1, value.y))
if is_node_ready():
_update_grid()
@export var monitor_boundary := 0
@export_range(0.5, 0.95) var overview_scale := 0.82
@export var transition_seconds := 0.22
@export var hot_corner_enabled := true
@export var corner_size := 20.0
@export var corner_dwell := 0.18
var overview_active := false
var _pointer_in_window := false
var _corner_hover := false
var _corner_armed := true
var _dwell := 0.0
var _tween: Tween
@onready var dashboard: Control = $Dashboard
@onready var grid: Control = $Dashboard/Grid
@onready var canvas: Control = $Dashboard/Canvas
func _ready() -> void:
grid.gui_input.connect(canvas.handle_grid_input)
$Toolbar/Add.pressed.connect(func() -> void: $Library.visible = not $Library.visible)
$Library.element_requested.connect(_add_element)
canvas.layout_saved.connect(func() -> void: $Toolbar/Hint.text = "Esc to return")
canvas.load_failed.connect(func() -> void: $Toolbar/Hint.text = "Could not load saved layout")
$Toolbar/Reset.pressed.connect(canvas.reset_layout)
canvas.save_failed.connect(func() -> void: $Toolbar/Hint.text = "Could not save layout")
$Toolbar/Return.pressed.connect(func() -> void: set_overview(false))
resized.connect(_resize)
get_window().mouse_entered.connect(func() -> void: _pointer_in_window = true)
get_window().mouse_exited.connect(_leave_corner)
theme_changed.connect(queue_redraw)
_update_grid()
canvas.load_layout()
_resize()
func _input(event: InputEvent) -> void:
if FOCUS_SCOPES.dispatch_focus_shortcut(event, get_tree()):
get_viewport().set_input_as_handled()
return
if event is InputEventMouseMotion and hot_corner_enabled:
_pointer_in_window = true
_update_corner(get_window().get_final_transform() * event.position)
elif overview_active and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
if $Library.visible:
$Library.hide()
else:
set_overview(false)
get_viewport().set_input_as_handled()
elif overview_active and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var pointer: Vector2 = get_global_transform_with_canvas().affine_inverse() * event.position
var in_library: bool = $Library.visible and $Library.get_global_rect().has_point(event.position)
if not in_library and not preview_rect().has_point(pointer) and not $Toolbar.get_global_rect().has_point(event.position):
set_overview(false)
get_viewport().set_input_as_handled()
func _process(delta: float) -> void:
var window := get_window()
# Letterbox regions may not send canvas mouse-motion events.
if hot_corner_enabled and window.visible and (_pointer_in_window or window.has_focus()):
_update_corner(window.get_final_transform() * window.get_mouse_position())
else:
_leave_corner()
if hot_corner_enabled and _corner_hover and _corner_armed and not overview_active:
_dwell += delta
if _dwell >= corner_dwell:
_corner_armed = false
set_overview(true)
func _update_corner(window_position: Vector2) -> void:
_corner_hover = Rect2(Vector2.ZERO, Vector2.ONE * corner_size).has_point(window_position)
if not _corner_hover:
_corner_armed = true
_dwell = 0
func _leave_corner() -> void:
_pointer_in_window = false
_corner_hover = false
_dwell = 0
func set_overview(active: bool) -> void:
if overview_active == active:
return
if not active:
$Library.hide()
canvas.set_edit_mode(active)
overview_active = active
_corner_armed = false
_dwell = 0
if is_instance_valid(_tween):
_tween.kill()
grid.visible = active
$Toolbar.visible = active
queue_redraw()
var target := Vector2.ONE * overview_scale if active else Vector2.ONE
if transition_seconds <= 0:
dashboard.scale = target
else:
_tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
_tween.tween_property(dashboard, "scale", target, transition_seconds)
overview_changed.emit(active)
func _add_element(element_type: String) -> void:
if canvas.add_element(element_type) == null:
$Library.show_no_space()
else:
$Library.hide()
func preview_rect() -> Rect2:
return Rect2((size - size * dashboard.scale) / 2, size * dashboard.scale)
func _resize() -> void:
_dwell = 0
if is_instance_valid(_tween):
_tween.kill()
dashboard.pivot_offset = size / 2
dashboard.scale = Vector2.ONE * overview_scale if overview_active else Vector2.ONE
queue_redraw()
func _update_grid() -> void:
canvas.grid_size = grid_size
canvas.arrange_items()
grid.grid_size = grid_size
grid.monitor_boundary = monitor_boundary
grid.queue_redraw()
$Toolbar/Title.text = "LAYOUT OVERVIEW %d × %d" % [grid_size.x, grid_size.y]
func _draw() -> void:
if overview_active:
draw_rect(Rect2(Vector2.ZERO, size), get_theme_color("instrument_empty", "SurfaceSide"))
+1
View File
@@ -0,0 +1 @@
uid://b4g15anvgpot8
+56
View File
@@ -0,0 +1,56 @@
extends Control
@export_enum("Landscape", "Dual portrait") var configuration := 0
@export var use_portrait_monitors := true
func _ready() -> void:
if configuration == 1:
$Overview.overview_changed.connect($RightWindow/RightOverview.set_overview)
$RightWindow/RightOverview.overview_changed.connect($Overview.set_overview)
$RightWindow.close_requested.connect(get_tree().quit)
if get_parent() == get_tree().root:
_configure_windows.call_deferred()
func _configure_windows() -> void:
var window := get_window()
var reference := Vector2i(1920, 1080) if configuration == 0 else Vector2i(1080, 1920)
_prepare_window(window, reference)
if configuration == 0:
var usable := DisplayServer.screen_get_usable_rect()
window.size = _preview_size(reference, usable.size, 1)
window.title = "Layout / landscape"
return
var right: Window = $RightWindow
_prepare_window(right, reference)
window.title = "ROV / left portrait"
var portrait_screens: Array[int] = []
for screen in range(DisplayServer.get_screen_count()):
var dimensions := DisplayServer.screen_get_size(screen)
if dimensions.y > dimensions.x:
portrait_screens.append(screen)
if use_portrait_monitors and portrait_screens.size() >= 2:
window.current_screen = portrait_screens[0]
window.mode = Window.MODE_MAXIMIZED
right.current_screen = portrait_screens[1]
right.show()
right.mode = Window.MODE_MAXIMIZED
else:
var usable := DisplayServer.screen_get_usable_rect()
var preview := _preview_size(reference, usable.size, 2)
window.size = preview
right.size = preview
var origin := usable.position + (usable.size - Vector2i(preview.x * 2 + 16, preview.y)) / 2
window.position = origin
right.position = origin + Vector2i(preview.x + 16, 0)
right.show()
func _prepare_window(window: Window, reference: Vector2i) -> void:
window.content_scale_factor = 1.0
window.content_scale_size = reference
window.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
window.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_KEEP
window.mode = Window.MODE_WINDOWED
func _preview_size(reference: Vector2i, usable: Vector2i, count: int) -> Vector2i:
var factor := minf(float(usable.x) * 0.85 / (reference.x * count + 16 * (count - 1)), float(usable.y) * 0.85 / reference.y)
return Vector2i(Vector2(reference) * clampf(factor, 0.1, 1.0))
+1
View File
@@ -0,0 +1 @@
uid://bxj3puinklg62
+17
View File
@@ -0,0 +1,17 @@
@tool
extends PanelContainer
@export var title := "RESERVED":
set(value):
title = value
if is_node_ready():
%Title.text = value
@export_multiline var description := "":
set(value):
description = value
if is_node_ready():
%Description.text = value
func _ready() -> void:
%Title.text = title
%Description.text = description
+1
View File
@@ -0,0 +1 @@
uid://btaf1ke22k5ij
+35
View File
@@ -0,0 +1,35 @@
extends RefCounted
static func read_json(path: String) -> Variant:
var file := FileAccess.open(path, FileAccess.READ)
if file == null or file.get_length() > 1048576:
return null
var parser := JSON.new()
if parser.parse(file.get_as_text()) != OK:
return null
return parser.data
static func write_json(path: String, document: Dictionary) -> Error:
var absolute := ProjectSettings.globalize_path(path)
var error := DirAccess.make_dir_recursive_absolute(absolute.get_base_dir())
if error != OK:
return error
var temporary := absolute + ".tmp"
var file := FileAccess.open(temporary, FileAccess.WRITE)
if file == null:
return FileAccess.get_open_error()
file.store_string(JSON.stringify(document, "\t") + "\n")
file.flush()
error = file.get_error()
file.close()
if error != OK:
return error
return DirAccess.rename_absolute(temporary, absolute)
static func integer_array(value: Variant, length: int) -> bool:
if not value is Array or value.size() != length:
return false
for number in value:
if not (number is int or number is float) or not is_finite(float(number)) or float(number) != floorf(float(number)):
return false
return true
+1
View File
@@ -0,0 +1 @@
uid://ceg5mij8na3qr
+22
View File
@@ -0,0 +1,22 @@
extends VBoxContainer
var elapsed := 0.0
@onready var panel = $LinkPanel
func _ready() -> void:
$Controls/Contrast.toggled.connect(func(high: bool) -> void:
theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres")
)
$Controls/State.item_selected.connect(func(index: int) -> void:
if index == 6: panel.clear_telemetry()
)
func _process(delta: float) -> void:
elapsed += delta
match $Controls/State.selected:
0: panel.set_telemetry(6.42 + sin(elapsed) * 0.12, 1.08, 0.02)
1: panel.set_telemetry(8.65, 7.9, 0.02)
2: panel.set_telemetry(9.72, 10.2, 0.02)
3: panel.set_telemetry(3.2, 0.4, 2.3, 1)
4: panel.set_telemetry(0, 0, 0, 2)
5, 6: pass
+1
View File
@@ -0,0 +1 @@
uid://i5dbrwr8d2gr
+74
View File
@@ -0,0 +1,74 @@
@tool
extends PanelContainer
enum Connection { CONNECTED, DEGRADED, DISCONNECTED }
@export var downlink_capacity_gbps := 10.0
@export var uplink_capacity_gbps := 10.0
@export_range(0, 1) var warning_fraction := 0.8
@export_range(0, 1) var fault_fraction := 0.95
@export var loss_window_seconds := 10.0
@export var stale_after_seconds := 2.0
@export var link_icon: Texture2D:
set(value):
link_icon = value
if is_node_ready():
%LinkIcon.texture = value
var _received_ms := -1
var _state := Connection.CONNECTED
var _down := 0.0
var _up := 0.0
var _loss := 0.0
var _stale := false
func _ready() -> void:
%LinkIcon.texture = link_icon
theme_changed.connect(_update_icon)
_update_icon()
_refresh()
## Rates in Gbps; loss in percent over loss_window_seconds.
func set_telemetry(down: float, up: float, loss_percent: float, state: int = Connection.CONNECTED) -> bool:
if not is_finite(down) or not is_finite(up) or not is_finite(loss_percent) or down < 0 or up < 0 or loss_percent < 0 or loss_percent > 100 or state < 0 or state > 2:
return false
_down = down
_up = up
_loss = loss_percent
_state = state
_received_ms = Time.get_ticks_msec()
_refresh()
return true
func clear_telemetry() -> void:
_received_ms = -1
_refresh()
func _process(_delta: float) -> void:
var stale := _received_ms >= 0 and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
if stale != _stale:
_refresh()
func _refresh() -> void:
if not is_node_ready():
return
var missing := _received_ms < 0
_stale = not missing and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
var measured := not missing and _state != Connection.DISCONNECTED
%State.text = "NO TELEMETRY" if missing else ("STALE" if _stale else ["CONNECTED", "DEGRADED", "DISCONNECTED"][_state])
%State.theme_type_variation = "LinkMuted" if missing else ("LinkRateWarning" if _stale or _state == Connection.DEGRADED else ("LinkRateFault" if _state == Connection.DISCONNECTED else "Label"))
var gauges := [%Downlink, %Uplink]
for index in range(2):
gauges[index].capacity_gbps = maxf([downlink_capacity_gbps, uplink_capacity_gbps][index], 0.001)
gauges[index].warning_fraction = warning_fraction
gauges[index].fault_fraction = maxf(warning_fraction, fault_fraction)
gauges[index].rate_gbps = [_down, _up][index]
gauges[index].available = measured
gauges[index].refresh()
%LossValue.text = "%.2f%%" % _loss if measured else "—"
%LossWindow.text = "%.0f s window" % loss_window_seconds + (" · held" if _stale else "")
func _update_icon() -> void:
var tint := get_theme_color("status_icon", "SurfaceSide")
tint.a = 0.06
%LinkIcon.self_modulate = tint
+1
View File
@@ -0,0 +1 @@
uid://84jt1vgxm2ei
+44
View File
@@ -0,0 +1,44 @@
@tool
extends Control
@export var direction := "↓ DOWNLINK"
var capacity_gbps := 10.0
var warning_fraction := 0.8
var fault_fraction := 0.95
var rate_gbps := 0.0
var available := false
func _ready() -> void:
resized.connect(queue_redraw)
theme_changed.connect(queue_redraw)
refresh()
func utilization() -> float:
return rate_gbps / maxf(capacity_gbps, 0.001)
func severity() -> int:
if not available:
return 0
return 2 if utilization() >= fault_fraction else (1 if utilization() >= warning_fraction else 0)
func refresh() -> void:
if not is_node_ready():
return
%Direction.text = direction
%Direction.theme_type_variation = "LinkMuted" if not available else ["LinkRateNormal", "LinkRateWarning", "LinkRateFault"][severity()]
%Value.text = "%.2f" % rate_gbps if available else "—"
%Capacity.text = "%s Gbps max" % String.num(capacity_gbps, 2)
queue_redraw()
func _draw() -> void:
var radius := maxf(1.0, minf(size.x, size.y) / 2 - 6)
var center := size / 2
var start := deg_to_rad(140)
var sweep := deg_to_rad(260)
draw_arc(center, radius, start, start + sweep, 96, get_theme_color("instrument_grid", "SurfaceSide"), 3, true)
for fraction in [warning_fraction, fault_fraction]:
var radial := Vector2.from_angle(start + sweep * fraction)
draw_line(center + radial * (radius - 4), center + radial * (radius + 4), get_theme_color("instrument_axis", "SurfaceSide"), 1, true)
if available and rate_gbps > 0:
var role: String = ["instrument_accent", "warning", "fault"][severity()]
draw_arc(center, radius, start, start + sweep * clampf(utilization(), 0, 1), 96, get_theme_color(role, "SurfaceSide"), 4, true)
+1
View File
@@ -0,0 +1 @@
uid://b1pcqygbaipjh
+18
View File
@@ -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))
)
+1
View File
@@ -0,0 +1 @@
uid://c5mncovlnarve
+133
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://1dn362pyibkx
+63
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dh2vnk35potq3
+91 -18
View File
@@ -16,8 +16,12 @@ signal deactivated
@export_node_path("Control") var initial_focus: NodePath @export_node_path("Control") var initial_focus: NodePath
var is_active: bool = false var is_active: bool = false
var _layout_editing := false
var _registered := false
var _blocked_shortcuts: Dictionary = {}
var _frame_host: Control
const EMBEDDED_PANEL = preload("res://themes/layout_embedded_panel.tres")
var _last_focus: Control var _last_focus: Control
var _shortcut_available: bool = true
var _initial_focus_control: Control var _initial_focus_control: Control
@onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content @onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content
@onready var _panel: PanelContainer = $PanelContainer @onready var _panel: PanelContainer = $PanelContainer
@@ -33,16 +37,13 @@ func _ready() -> void:
_route_content_child(child) _route_content_child(child)
child_entered_tree.connect(_on_child_entered_tree) child_entered_tree.connect(_on_child_entered_tree)
add_to_group("keyboard_focus_containers") add_to_group("keyboard_focus_containers")
_registered = true
focus_mode = Control.FOCUS_NONE focus_mode = Control.FOCUS_NONE
_shortcut.focus_mode = Control.FOCUS_NONE _shortcut.focus_mode = Control.FOCUS_NONE
_shortcut.pressed.connect(activate) _shortcut.pressed.connect(activate)
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"): refresh_shortcuts(get_tree())
if other != self and other.get_viewport() == get_viewport() and other.focus_key == focus_key and other._shortcut_available: get_window().focus_exited.connect(deactivate)
_shortcut_available = false theme_changed.connect(_update_style)
_shortcut.disabled = true
_shortcut.tooltip_text = "Shortcut already assigned to " + other.title
push_warning("Duplicate panel shortcut: " + OS.get_keycode_string(focus_key))
break
get_viewport().gui_focus_changed.connect(_on_focus_changed) get_viewport().gui_focus_changed.connect(_on_focus_changed)
visibility_changed.connect(_on_visibility_changed) visibility_changed.connect(_on_visibility_changed)
_update_style() _update_style()
@@ -54,7 +55,10 @@ func _refresh_header() -> void:
if label != null: if label != null:
label.text = title label.text = title
if badge != null: if badge != null:
badge.visible = focus_key != KEY_NONE
badge.disabled = not is_shortcut_available(focus_key)
badge.text = OS.get_keycode_string(focus_key) + (" · ACTIVE" if is_active else "") badge.text = OS.get_keycode_string(focus_key) + (" · ACTIVE" if is_active else "")
badge.tooltip_text = "Shortcut already used by " + str(_blocked_shortcuts.get(focus_key, "")) if _blocked_shortcuts.has(focus_key) else "Focus this panel"
func _on_child_entered_tree(child: Node) -> void: func _on_child_entered_tree(child: Node) -> void:
# SceneTree may still be adding the node when this signal fires. # SceneTree may still be adding the node when this signal fires.
@@ -68,11 +72,12 @@ func _route_content_child(child: Node) -> void:
child.reparent(content, false) child.reparent(content, false)
func activate() -> void: func activate() -> void:
if Engine.is_editor_hint() or not is_visible_in_tree() or not _shortcut_available: if Engine.is_editor_hint() or not focus_available():
return return
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"): for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
if other != self and other.get_viewport() == get_viewport(): if other != self:
other.deactivate() other.deactivate()
get_window().grab_focus()
var was_active := is_active var was_active := is_active
is_active = true is_active = true
focus_mode = Control.FOCUS_ALL focus_mode = Control.FOCUS_ALL
@@ -105,15 +110,14 @@ func deactivate() -> void:
deactivated.emit() deactivated.emit()
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if Engine.is_editor_hint() or not event is InputEventKey or not is_visible_in_tree(): if Engine.is_editor_hint() or not is_visible_in_tree() or _layout_editing:
return
if dispatch_focus_shortcut(event, get_tree()):
get_viewport().set_input_as_handled()
return
if not event is InputEventKey:
return return
var key := event as InputEventKey var key := event as InputEventKey
if key.keycode == focus_key and not key.alt_pressed and not key.ctrl_pressed and not key.meta_pressed and not key.shift_pressed and _shortcut_available:
# Consume releases and repeats as well, so shortcuts never become input.
get_viewport().set_input_as_handled()
if key.pressed and not key.echo:
activate()
return
if not is_active or not key.pressed: if not is_active or not key.pressed:
return return
if key.keycode == KEY_ESCAPE: if key.keycode == KEY_ESCAPE:
@@ -145,8 +149,77 @@ func _on_focus_changed(control: Control) -> void:
func _on_visibility_changed() -> void: func _on_visibility_changed() -> void:
if not is_visible_in_tree(): if not is_visible_in_tree():
deactivate() deactivate()
if is_inside_tree():
refresh_shortcuts(get_tree())
func _update_style() -> void: func _update_style() -> void:
var style_name := "panel_active" if is_active else "panel_inactive" var style_name := "panel_active" if is_active else "panel_inactive"
_panel.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide")) if is_instance_valid(_frame_host):
_panel.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
_frame_host.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
else:
_panel.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
_refresh_header() _refresh_header()
func get_focus_shortcuts() -> Array[Key]:
return [focus_key] if focus_key != KEY_NONE else []
func is_shortcut_available(key: Key) -> bool:
return key != KEY_NONE and not _blocked_shortcuts.has(key)
func focus_available() -> bool:
return _registered and is_inside_tree() and is_visible_in_tree() and not _layout_editing
func activate_shortcut(_key: Key) -> void:
activate()
func set_layout_editing(editing: bool) -> void:
_layout_editing = editing
if editing:
deactivate()
if is_inside_tree():
refresh_shortcuts(get_tree())
func set_frame_host(host: Control) -> void:
_frame_host = host
if is_node_ready():
_update_style()
func _exit_tree() -> void:
_registered = false
if not Engine.is_editor_hint() and get_tree() != null:
refresh_shortcuts(get_tree())
static func refresh_shortcuts(tree: SceneTree) -> void:
var scopes := tree.get_nodes_in_group("keyboard_focus_containers")
scopes.sort_custom(func(a: Node, b: Node) -> bool: return a.get_instance_id() < b.get_instance_id())
var claimed: Dictionary = {}
for scope in scopes:
scope._blocked_shortcuts.clear()
if scope.focus_available():
for key in scope.get_focus_shortcuts():
if key == KEY_NONE:
continue
if claimed.has(key) and claimed[key] != scope:
scope._blocked_shortcuts[key] = claimed[key].title
else:
claimed[key] = scope
scope._refresh_header()
static func dispatch_focus_shortcut(event: InputEvent, tree: SceneTree) -> bool:
if not event is InputEventKey or event.alt_pressed or event.ctrl_pressed or event.meta_pressed or event.shift_pressed:
return false
var matches := false
for scope in tree.get_nodes_in_group("keyboard_focus_containers"):
if scope.focus_available() and event.keycode in scope.get_focus_shortcuts():
matches = true
break
if not matches:
return false
refresh_shortcuts(tree)
for scope in tree.get_nodes_in_group("keyboard_focus_containers"):
if scope.focus_available() and event.keycode in scope.get_focus_shortcuts() and scope.is_shortcut_available(event.keycode):
if event.pressed and not event.echo:
scope.activate_shortcut(event.keycode)
return true
return false
+40
View File
@@ -0,0 +1,40 @@
@tool
extends Control
@export var minimum := 0.0
@export var maximum := 36.0
@export var warning_low := 0.0
@export var warning_high := 24.0
@export var fault_low := 0.0
@export var fault_high := 30.0
@export var nominal := NAN
var value := 0.0
var available := false
func _ready() -> void:
resized.connect(queue_redraw)
theme_changed.connect(queue_redraw)
func _x(number: float) -> float:
return 4 + clampf((number - minimum) / maxf(maximum - minimum, 0.001), 0, 1) * (size.x - 8)
func _zone(low: float, high: float, role: String) -> void:
if high <= low:
return
var color := get_theme_color(role, "SurfaceSide")
color.a = 0.65
draw_rect(Rect2(_x(low), 4, _x(high) - _x(low), 6), color)
func _draw() -> void:
draw_rect(Rect2(4, 4, maxf(size.x - 8, 0), 6), get_theme_color("instrument_grid", "SurfaceSide"))
_zone(minimum, fault_low, "fault")
_zone(fault_low, warning_low, "warning")
_zone(warning_high, fault_high, "warning")
_zone(fault_high, maximum, "fault")
if is_finite(nominal):
draw_line(Vector2(_x(nominal), 0), Vector2(_x(nominal), 16), get_theme_color("instrument_accent", "SurfaceSide"), 2)
if available:
var x := _x(value)
var ink := get_theme_color("font_color", "Label")
draw_line(Vector2(x, 0), Vector2(x, 14), ink, 3)
draw_colored_polygon(PackedVector2Array([Vector2(x - 5, 0), Vector2(x + 5, 0), Vector2(x, 4)]), ink)
+1
View File
@@ -0,0 +1 @@
uid://bqvhrignal143
+29
View File
@@ -0,0 +1,29 @@
@tool
extends Control
@export_enum("Normal", "Warning", "Fault", "No data") var severity := 3
@export var pulse_alerts := true
var _phase := -1
func _ready() -> void:
resized.connect(queue_redraw)
theme_changed.connect(_update_icon)
_update_icon()
func _process(_delta: float) -> void:
var phase := int(Time.get_ticks_msec() / 1000.0) % 2 if pulse_alerts else 0
if phase != _phase:
_phase = phase
queue_redraw()
func _draw() -> void:
if severity in [1, 2]:
var role := "warning" if severity == 1 else "fault"
if _phase == 1:
role += "_alternate"
draw_rect(Rect2(Vector2.ZERO, size), get_theme_color(role, "SurfaceSide"))
func _update_icon() -> void:
var tint := get_theme_color("status_icon", "SurfaceSide")
tint.a = 0.09
$ModeIcon.self_modulate = tint
queue_redraw()
+1
View File
@@ -0,0 +1 @@
uid://rh8gwu8mfqx2
+27
View File
@@ -0,0 +1,27 @@
extends VBoxContainer
@onready var panel = $VehicleStatusPanel
var elapsed := 0.0
func _ready() -> void:
$Controls/State.item_selected.connect(_select_state)
$Controls/Contrast.toggled.connect(func(high: bool) -> void:
theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres")
)
$Controls/Pulse.toggled.connect(func(enabled: bool) -> void: panel.pulse_alerts = enabled)
_select_state(0)
func _select_state(index: int) -> void:
match index:
0: panel.set_status("STATIONKEEP", 0, "", "Position hold active", "Thrusters enabled")
1: panel.set_status("Supply voltage low", 1, "PWR-012", "46.5 V · below 48 V nominal", "Check topside supply and tether load")
2: panel.set_status("Enclosure overtemperature", 2, "TMP-003", "68 °C · trip 65 °C · thrusters inhibited", "Inspect cooling before reset")
3: panel.set_status("Enclosure overtemperature", 2, "TMP-003", "68 °C · trip 65 °C · thrusters inhibited", "Inspect cooling before reset")
func _process(delta: float) -> void:
elapsed += delta
match $Controls/State.selected:
0: panel.set_telemetry(48.0, 18.2 + sin(elapsed) * 0.6, 874 + sin(elapsed) * 29, 34)
1: panel.set_telemetry(46.5, 25.5, 1186, 42)
2: panel.set_telemetry(48.0, 2.1, 101, 68)
3: pass
+1
View File
@@ -0,0 +1 @@
uid://qipln371l5is
+83
View File
@@ -0,0 +1,83 @@
@tool
extends PanelContainer
@export var mode_icon: Texture2D:
set(value):
mode_icon = value
if is_node_ready():
%ModeIcon.texture = value
@export var stale_after_seconds := 2.0
@export var pulse_alerts := true:
set(value):
pulse_alerts = value
if is_node_ready():
%StatusArt.pulse_alerts = value
var _received_ms := -1
var _severity := 0
var _name := ""
var _code := ""
var _detail := ""
var _action := ""
var _stale := false
func _ready() -> void:
%ModeIcon.texture = mode_icon
_refresh_status()
_refresh_references()
## Severity and fault explanations come from vehicle telemetry, not gauge zones.
func set_status(name_text: String, severity: int = 0, code: String = "", detail: String = "", action: String = "") -> void:
_name = name_text
_severity = clampi(severity, 0, 2)
_code = code
_detail = detail
_action = action
_refresh_status()
func set_telemetry(voltage: float, current: float, power: float, internal_temperature: float) -> void:
for number in [voltage, current, power, internal_temperature]:
if not is_finite(number):
return
_refresh_references()
_received_ms = Time.get_ticks_msec()
var values := [voltage, current, power, internal_temperature]
var labels := [%VoltageValue, %CurrentValue, %PowerValue, %TemperatureValue]
var gauges := [%VoltageGauge, %CurrentGauge, %PowerGauge, %TemperatureGauge]
for index in range(4):
labels[index].text = (["%.1f V", "%.1f A", "%.0f W", "%.1f °C"][index]) % values[index]
gauges[index].value = values[index]
gauges[index].available = true
gauges[index].queue_redraw()
_refresh_status()
func _process(_delta: float) -> void:
var stale := _received_ms >= 0 and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
if stale != _stale:
_refresh_status()
func _refresh_status() -> void:
if not is_node_ready():
return
_stale = _received_ms >= 0 and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000
var missing := _received_ms < 0
var unavailable := missing or _stale
%StatusArt.severity = 3 if unavailable else _severity
%StatusArt.pulse_alerts = pulse_alerts
%StatusArt.queue_redraw()
%ModeIcon.visible = not unavailable and _severity == 0
%Severity.text = "NO DATA" if missing else ("STALE" if _stale else ["", "WARNING", "FAULT"][_severity])
%StatusName.text = "Waiting for telemetry" if missing else (_name if not _name.is_empty() else "Mode unavailable")
%Code.text = _code
%Code.visible = not _code.is_empty() and not missing
%Detail.text = "" if missing else _detail
%Action.text = "Values held · connection overdue" if _stale else ("" if missing else _action)
var variant := ("VehicleWarningText" if _severity == 1 else "VehicleFaultText") if _severity in [1, 2] and not unavailable else "Label"
for label in [%Severity, %StatusName, %Code, %Detail, %Action]:
label.theme_type_variation = variant
func _refresh_references() -> void:
%VoltageReference.text = "/ %.0f V" % %VoltageGauge.nominal
%CurrentReference.text = "/ %.0f A" % %CurrentGauge.fault_high
%PowerReference.text = "/ %.0f W" % %PowerGauge.fault_high
%TemperatureReference.text = "/ %.0f °C" % %TemperatureGauge.fault_high
+1
View File
@@ -0,0 +1 @@
uid://d3jrypgr15n4x
+30
View File
@@ -0,0 +1,30 @@
shader_type canvas_item;
render_mode unshaded;
uniform vec2 grid_size = vec2(32.0, 18.0);
uniform vec4 line_color : source_color;
uniform vec4 boundary_color : source_color;
uniform float monitor_boundary = 0.0;
float grid_coverage(vec2 coordinate, float width) {
vec2 distance_to_line = abs(fract(coordinate + 0.5) - 0.5);
vec2 pixels = distance_to_line / max(fwidth(coordinate), vec2(0.00001));
vec2 coverage = 1.0 - smoothstep(vec2(width - 0.5), vec2(width + 0.5), pixels);
return max(coverage.x, coverage.y);
}
void fragment() {
vec2 cells = UV * grid_size;
float minor = grid_coverage(cells, 0.6) * 0.4;
float major = grid_coverage(cells / 4.0, 0.8) * 0.85;
vec2 edge_pixels = min(UV, 1.0 - UV) / max(fwidth(UV), vec2(0.00001));
float border = 1.0 - smoothstep(0.5, 1.5, min(edge_pixels.x, edge_pixels.y));
float alpha = max(max(minor, major), border);
vec4 result = vec4(line_color.rgb, line_color.a * alpha);
if (monitor_boundary > 0.0 && monitor_boundary < grid_size.x) {
float distance_pixels = abs(cells.x - monitor_boundary) / max(fwidth(cells.x), 0.00001);
float boundary = 1.0 - smoothstep(1.0, 2.0, distance_pixels);
result = mix(result, boundary_color, boundary);
}
COLOR = result;
}
+1
View File
@@ -0,0 +1 @@
uid://beaucnaf4ol72
+86 -34
View File
@@ -1,39 +1,36 @@
extends SceneTree extends SceneTree
const ACCENT = Color("ff7b00") const COLORS = preload("res://themes/palette.gd")
const SECONDARY = Color("402e32")
const CLEAR = Color.TRANSPARENT
const HIGH_CONTRAST = {
"text": Color("f5f1ed"), "muted_text": Color("b9b0b3"),
"active_text": Color("141114"), "surface": Color("201b1f"),
"panel": Color("201b1f"), "input": Color("141114"),
"hover": SECONDARY, "pressed": ACCENT, "fill": ACCENT,
"disabled": Color("191619"), "border": Color("817278"),
"disabled_border": Color("51464c"), "focus": ACCENT,
"separator": SECONDARY, "thumb": Color("b9b0b3"),
"fill_text": Color("141114"),
}
const SOFT = {
"text": Color("e7e0dd"), "muted_text": Color("ada2a8"),
"active_text": Color("f5f1ed"), "surface": Color("292126"),
"panel": Color("201b1f"), "input": Color("141114"),
"hover": SECONDARY, "pressed": SECONDARY,
"fill": ACCENT, "disabled": Color("211c20"),
"border": CLEAR, "disabled_border": CLEAR,
"focus": ACCENT, "separator": SECONDARY,
"thumb": Color("d2c6c9"), "fill_text": Color("141114"),
}
func _initialize() -> void: func _initialize() -> void:
for high_contrast in [false, true]: for high_contrast in [false, true]:
var palette: Dictionary = HIGH_CONTRAST if high_contrast else SOFT var palette: Dictionary = COLORS.HIGH_CONTRAST if high_contrast else COLORS.SOFT
var path := "res://themes/theme_highcontrast.tres" if high_contrast else "res://themes/theme.tres" var path := "res://themes/theme_highcontrast.tres" if high_contrast else "res://themes/theme.tres"
var error := ResourceSaver.save(_build_theme(palette, high_contrast), path) var error := ResourceSaver.save(_build_theme(palette, high_contrast), path)
if error != OK: if error != OK:
push_error("Could not save %s: %s" % [path, error_string(error)]) push_error("Could not save %s: %s" % [path, error_string(error)])
quit(error) quit(error)
return return
var material := StandardMaterial3D.new()
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = COLORS.COMPASS_TEST
material.albedo_texture = load("res://assets/vector/dial.svg")
var material_error := ResourceSaver.save(material, "res://themes/compass_material.tres")
if material_error != OK:
push_error("Could not save compass material: " + error_string(material_error))
quit(material_error)
return
var config := ConfigFile.new()
var config_error := config.load("res://project.godot")
if config_error == OK and config.get_value("rendering", "environment/defaults/default_clear_color") != COLORS.VIEWPORT_BACKGROUND:
config.set_value("rendering", "environment/defaults/default_clear_color", COLORS.VIEWPORT_BACKGROUND)
config_error = config.save("res://project.godot")
if config_error != OK:
push_error("Could not update viewport background: " + error_string(config_error))
quit(config_error)
return
quit() quit()
func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme: func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme:
@@ -48,7 +45,7 @@ func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme:
if "disabled" in name or "placeholder" in name or "readonly" in name: if "disabled" in name or "placeholder" in name or "readonly" in name:
color = palette.muted_text color = palette.muted_text
elif "selection" in name: elif "selection" in name:
color = SECONDARY color = COLORS.SECONDARY
elif name in ["font_pressed_color", "font_hover_pressed_color"] and type in ["Button", "OptionButton", "MenuButton"]: elif name in ["font_pressed_color", "font_hover_pressed_color"] and type in ["Button", "OptionButton", "MenuButton"]:
color = palette.active_text color = palette.active_text
elif name == "font_selected_color" and type == "TabContainer": elif name == "font_selected_color" and type == "TabContainer":
@@ -56,11 +53,11 @@ func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme:
elif name == "font_color" and type == "ProgressBar": elif name == "font_color" and type == "ProgressBar":
color = palette.fill_text color = palette.fill_text
elif "background" in name: elif "background" in name:
color = CLEAR color = COLORS.CLEAR
elif "caret" in name or "accent" in name: elif "caret" in name or "accent" in name:
color = ACCENT color = COLORS.ACCENT
elif "shadow" in name or "outline" in name: elif "shadow" in name or "outline" in name:
color = CLEAR color = COLORS.CLEAR
theme.set_color(name, type, color) theme.set_color(name, type, color)
for name in theme.get_stylebox_list(type): for name in theme.get_stylebox_list(type):
var original := theme.get_stylebox(name, type) var original := theme.get_stylebox(name, type)
@@ -99,13 +96,11 @@ func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme:
style.bg_color = palette.separator style.bg_color = palette.separator
style.border_color = palette.separator style.border_color = palette.separator
theme.set_stylebox(name, type, style) theme.set_stylebox(name, type, style)
# Slider thumbs use square textures instead of the engine's round icons.
for type in ["HSlider", "VSlider"]: for type in ["HSlider", "VSlider"]:
for name in ["grabber", "grabber_highlight", "grabber_disabled"]: for name in ["grabber", "grabber_highlight", "grabber_disabled"]:
var bitmap := Image.create(14, 18, false, Image.FORMAT_RGBA8) var bitmap := Image.create(14, 18, false, Image.FORMAT_RGBA8)
bitmap.fill(ACCENT if name == "grabber_highlight" else palette.thumb) bitmap.fill(COLORS.ACCENT if name == "grabber_highlight" else palette.thumb)
theme.set_icon(name, type, ImageTexture.create_from_image(bitmap)) theme.set_icon(name, type, ImageTexture.create_from_image(bitmap))
# Dedicated panel states used by the keyboard focus containers.
var panel := theme.get_stylebox("panel", "PanelContainer").duplicate() as StyleBoxFlat var panel := theme.get_stylebox("panel", "PanelContainer").duplicate() as StyleBoxFlat
theme.set_stylebox("panel_inactive", "SurfaceSide", panel) theme.set_stylebox("panel_inactive", "SurfaceSide", panel)
var active := panel.duplicate() as StyleBoxFlat var active := panel.duplicate() as StyleBoxFlat
@@ -114,6 +109,63 @@ func _build_theme(palette: Dictionary, high_contrast: bool) -> Theme:
if not high_contrast: if not high_contrast:
active.border_width_top = 2 active.border_width_top = 2
theme.set_stylebox("panel_active", "SurfaceSide", active) theme.set_stylebox("panel_active", "SurfaceSide", active)
theme.set_color("accent", "SurfaceSide", ACCENT) theme.set_color("accent", "SurfaceSide", COLORS.ACCENT)
theme.set_color("secondary", "SurfaceSide", SECONDARY) theme.set_color("secondary", "SurfaceSide", COLORS.SECONDARY)
_add_instrument_theme(theme, palette)
for role in COLORS.STATUS:
theme.set_color(role, "SurfaceSide", COLORS.STATUS[role])
theme.set_type_variation("VehicleWarningText", "Label")
theme.set_color("font_color", "VehicleWarningText", COLORS.STATUS.status_ink)
theme.set_type_variation("VehicleFaultText", "Label")
theme.set_color("font_color", "VehicleFaultText", COLORS.WHITE)
theme.set_type_variation("VehicleBackground", "PanelContainer")
var vehicle_background := StyleBoxFlat.new()
vehicle_background.bg_color = palette.panel
vehicle_background.border_color = palette.border
vehicle_background.set_border_width_all(1 if high_contrast else 0)
theme.set_stylebox("panel", "VehicleBackground", vehicle_background)
theme.set_type_variation("LayoutItemBackground", "Panel")
theme.set_stylebox("panel", "LayoutItemBackground", vehicle_background)
theme.set_type_variation("LayoutLibraryList", "ItemList")
theme.set_color("guide_color", "LayoutLibraryList", COLORS.SECONDARY if high_contrast else COLORS.CLEAR)
theme.set_constant("v_separation", "LayoutLibraryList", 8)
for variant in ["LinkRateNormal", "LinkRateWarning", "LinkRateFault", "LinkMuted"]:
theme.set_type_variation(variant, "Label")
theme.set_color("font_color", "LinkRateNormal", COLORS.INSTRUMENT.instrument_accent)
theme.set_color("font_color", "LinkRateWarning", COLORS.WARNING)
theme.set_color("font_color", "LinkRateFault", COLORS.FAULT)
theme.set_color("font_color", "LinkMuted", palette.muted_text)
for variant in ["InstrumentWarning", "InstrumentFault", "InstrumentMuted"]:
theme.set_type_variation(variant, "Label")
theme.set_color("font_color", "InstrumentWarning", COLORS.WARNING)
theme.set_color("font_color", "InstrumentFault", COLORS.FAULT)
theme.set_color("font_color", "InstrumentMuted", palette.muted_text)
return theme return theme
func _add_instrument_theme(theme: Theme, palette: Dictionary) -> void:
for role in COLORS.INSTRUMENT:
theme.set_color(role, "SurfaceSide", COLORS.INSTRUMENT[role])
theme.set_color("instrument_empty", "SurfaceSide", palette.input)
theme.set_color("instrument_axis", "SurfaceSide", palette.muted_text)
theme.set_type_variation("MotionBackground", "Panel")
var background := StyleBoxFlat.new()
background.bg_color = palette.panel
theme.set_stylebox("panel", "MotionBackground", background)
for vertical in [false, true]:
var type := "MotionVSeparator" if vertical else "MotionHSeparator"
theme.set_type_variation(type, "VSeparator" if vertical else "HSeparator")
var separator := StyleBoxLine.new()
separator.color = COLORS.INSTRUMENT.instrument_grid
separator.thickness = 1
separator.vertical = vertical
theme.set_stylebox("separator", type, separator)
theme.set_type_variation("MotionAlertPanel", "PanelContainer")
var alert := StyleBoxFlat.new()
alert.bg_color = COLORS.INSTRUMENT.alert_background
alert.content_margin_top = 7
alert.content_margin_bottom = 7
theme.set_stylebox("panel", "MotionAlertPanel", alert)
theme.set_type_variation("MotionAlertText", "Label")
theme.set_color("font_color", "MotionAlertText", COLORS.INSTRUMENT.alert_text)
theme.set_type_variation("MotionWarning", "Label")
theme.set_color("font_color", "MotionWarning", COLORS.INSTRUMENT.instrument_accent)
+10
View File
@@ -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")
+10
View File
@@ -0,0 +1,10 @@
[gd_resource type="FontVariation" format=3]
[sub_resource type="SystemFont" id="SystemFont_mono"]
font_names = PackedStringArray("DejaVu Sans Mono", "Liberation Mono", "monospace")
hinting = 2
oversampling = 2.0
[resource]
base_font = SubResource("SystemFont_mono")
variation_embolden = 0.35
+7
View File
@@ -0,0 +1,7 @@
[gd_resource type="StyleBoxEmpty" format=3]
[resource]
content_margin_left = 0.0
content_margin_top = 0.0
content_margin_right = 0.0
content_margin_bottom = 0.0
+68
View File
@@ -0,0 +1,68 @@
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 WARNING = Color("f5c542")
const FAULT = Color("dc414b")
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,
}
static var STATUS: Dictionary = {
"warning": WARNING,
"warning_alternate": WARNING.darkened(0.36),
"fault": FAULT,
"fault_alternate": FAULT.darkened(0.50),
"status_ink": INPUT,
"status_icon": ACCENT_LIGHT,
}
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more