diff --git a/components/camera_layout_slot.tscn b/components/camera_layout_slot.tscn new file mode 100644 index 0000000..6f9f0d2 --- /dev/null +++ b/components/camera_layout_slot.tscn @@ -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 diff --git a/components/layout_canvas.tscn b/components/layout_canvas.tscn new file mode 100644 index 0000000..3ecb2b2 --- /dev/null +++ b/components/layout_canvas.tscn @@ -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 = "×" diff --git a/components/layout_item.tscn b/components/layout_item.tscn new file mode 100644 index 0000000..4935004 --- /dev/null +++ b/components/layout_item.tscn @@ -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" diff --git a/components/layout_library.tscn b/components/layout_library.tscn new file mode 100644 index 0000000..5ace60e --- /dev/null +++ b/components/layout_library.tscn @@ -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" diff --git a/components/layout_overview.tscn b/components/layout_overview.tscn new file mode 100644 index 0000000..295ac1f --- /dev/null +++ b/components/layout_overview.tscn @@ -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 diff --git a/components/layout_slot.tscn b/components/layout_slot.tscn new file mode 100644 index 0000000..e3babaf --- /dev/null +++ b/components/layout_slot.tscn @@ -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 diff --git a/docs/display_layouts.md b/docs/display_layouts.md new file mode 100644 index 0000000..6045287 --- /dev/null +++ b/docs/display_layouts.md @@ -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. Environment, VR/video, and operator tools are placeholders. The Link panel has a separate [telemetry demo](link_panel.md). 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. diff --git a/scenes/layout_dual_portrait.tscn b/scenes/layout_dual_portrait.tscn new file mode 100644 index 0000000..ed6960f --- /dev/null +++ b/scenes/layout_dual_portrait.tscn @@ -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 diff --git a/scenes/layout_landscape.tscn b/scenes/layout_landscape.tscn new file mode 100644 index 0000000..737c30d --- /dev/null +++ b/scenes/layout_landscape.tscn @@ -0,0 +1,107 @@ +[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"] + +[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) +fill_allocation = true + +[node name="Content" parent="Overview/Dashboard/Canvas/Environment" instance=ExtResource("slot")] +layout_mode = 0 +title = "ENVIRONMENT" +description = "Depth · pressure · water temperature" + +[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 diff --git a/scripts/camera_layout_frame.gd b/scripts/camera_layout_frame.gd new file mode 100644 index 0000000..c025ab8 --- /dev/null +++ b/scripts/camera_layout_frame.gd @@ -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) diff --git a/scripts/camera_layout_frame.gd.uid b/scripts/camera_layout_frame.gd.uid new file mode 100644 index 0000000..b1aeb19 --- /dev/null +++ b/scripts/camera_layout_frame.gd.uid @@ -0,0 +1 @@ +uid://k6buaitag78o diff --git a/scripts/layout_canvas.gd b/scripts/layout_canvas.gd new file mode 100644 index 0000000..42119dd --- /dev/null +++ b/scripts/layout_canvas.gd @@ -0,0 +1,339 @@ +@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: + item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if active else Control.MOUSE_BEHAVIOR_INHERITED + 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() + item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED + 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() + item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if edit_mode else Control.MOUSE_BEHAVIOR_INHERITED + 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() diff --git a/scripts/layout_canvas.gd.uid b/scripts/layout_canvas.gd.uid new file mode 100644 index 0000000..2440ab4 --- /dev/null +++ b/scripts/layout_canvas.gd.uid @@ -0,0 +1 @@ +uid://bqrapdh6yddl8 diff --git a/scripts/layout_catalog.gd b/scripts/layout_catalog.gd new file mode 100644 index 0000000..502b7b1 --- /dev/null +++ b/scripts/layout_catalog.gd @@ -0,0 +1,31 @@ +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": "Reserved for depth, pressure and water temperature.", "scene": preload("res://components/layout_slot.tscn"), "size": Vector2i(6, 5), "minimum": Vector2i(4, 3), "reference": Vector2(360, 240), "fill": true}, + "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}, + "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 diff --git a/scripts/layout_catalog.gd.uid b/scripts/layout_catalog.gd.uid new file mode 100644 index 0000000..d96faca --- /dev/null +++ b/scripts/layout_catalog.gd.uid @@ -0,0 +1 @@ +uid://0hiij8bwyn7w diff --git a/scripts/layout_edit_overlay.gd b/scripts/layout_edit_overlay.gd new file mode 100644 index 0000000..71fb7d8 --- /dev/null +++ b/scripts/layout_edit_overlay.gd @@ -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) diff --git a/scripts/layout_edit_overlay.gd.uid b/scripts/layout_edit_overlay.gd.uid new file mode 100644 index 0000000..c099c55 --- /dev/null +++ b/scripts/layout_edit_overlay.gd.uid @@ -0,0 +1 @@ +uid://cwbk5g4yhdfd6 diff --git a/scripts/layout_grid.gd b/scripts/layout_grid.gd new file mode 100644 index 0000000..11c547b --- /dev/null +++ b/scripts/layout_grid.gd @@ -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")) diff --git a/scripts/layout_grid.gd.uid b/scripts/layout_grid.gd.uid new file mode 100644 index 0000000..fc6e651 --- /dev/null +++ b/scripts/layout_grid.gd.uid @@ -0,0 +1 @@ +uid://n4eb1a6j5j88 diff --git a/scripts/layout_item.gd b/scripts/layout_item.gd new file mode 100644 index 0000000..25f65ed --- /dev/null +++ b/scripts/layout_item.gd @@ -0,0 +1,42 @@ +@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) + 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 diff --git a/scripts/layout_item.gd.uid b/scripts/layout_item.gd.uid new file mode 100644 index 0000000..2672ac9 --- /dev/null +++ b/scripts/layout_item.gd.uid @@ -0,0 +1 @@ +uid://byjnf0ae0jr2y diff --git a/scripts/layout_library.gd b/scripts/layout_library.gd new file mode 100644 index 0000000..43d84a6 --- /dev/null +++ b/scripts/layout_library.gd @@ -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." diff --git a/scripts/layout_library.gd.uid b/scripts/layout_library.gd.uid new file mode 100644 index 0000000..0175c64 --- /dev/null +++ b/scripts/layout_library.gd.uid @@ -0,0 +1 @@ +uid://cwdwpabj62mhl diff --git a/scripts/layout_overview.gd b/scripts/layout_overview.gd new file mode 100644 index 0000000..dc56fdb --- /dev/null +++ b/scripts/layout_overview.gd @@ -0,0 +1,135 @@ +extends Control + +signal overview_changed(active: bool) + +@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 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")) diff --git a/scripts/layout_overview.gd.uid b/scripts/layout_overview.gd.uid new file mode 100644 index 0000000..5362660 --- /dev/null +++ b/scripts/layout_overview.gd.uid @@ -0,0 +1 @@ +uid://b4g15anvgpot8 diff --git a/scripts/layout_preview.gd b/scripts/layout_preview.gd new file mode 100644 index 0000000..2523372 --- /dev/null +++ b/scripts/layout_preview.gd @@ -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)) diff --git a/scripts/layout_preview.gd.uid b/scripts/layout_preview.gd.uid new file mode 100644 index 0000000..931bb70 --- /dev/null +++ b/scripts/layout_preview.gd.uid @@ -0,0 +1 @@ +uid://bxj3puinklg62 diff --git a/scripts/layout_slot.gd b/scripts/layout_slot.gd new file mode 100644 index 0000000..937578c --- /dev/null +++ b/scripts/layout_slot.gd @@ -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 diff --git a/scripts/layout_slot.gd.uid b/scripts/layout_slot.gd.uid new file mode 100644 index 0000000..d53535b --- /dev/null +++ b/scripts/layout_slot.gd.uid @@ -0,0 +1 @@ +uid://btaf1ke22k5ij diff --git a/scripts/layout_store.gd b/scripts/layout_store.gd new file mode 100644 index 0000000..fad768b --- /dev/null +++ b/scripts/layout_store.gd @@ -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 diff --git a/scripts/layout_store.gd.uid b/scripts/layout_store.gd.uid new file mode 100644 index 0000000..48870a6 --- /dev/null +++ b/scripts/layout_store.gd.uid @@ -0,0 +1 @@ +uid://ceg5mij8na3qr diff --git a/shaders/layout_grid.gdshader b/shaders/layout_grid.gdshader new file mode 100644 index 0000000..ca27897 --- /dev/null +++ b/shaders/layout_grid.gdshader @@ -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; +} diff --git a/shaders/layout_grid.gdshader.uid b/shaders/layout_grid.gdshader.uid new file mode 100644 index 0000000..6a026ab --- /dev/null +++ b/shaders/layout_grid.gdshader.uid @@ -0,0 +1 @@ +uid://beaucnaf4ol72 diff --git a/themes/layout_embedded_panel.tres b/themes/layout_embedded_panel.tres new file mode 100644 index 0000000..ba5eb4d --- /dev/null +++ b/themes/layout_embedded_panel.tres @@ -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