Operator hub - Introduce Escape Gallery branding and Glitch: the Dark Machine room views - Add cabinet illustrations, Pac-Man marquee/gameplay artwork, and waiting states - Add device feedback panels with RFID readings, input/output states, and stale status - Add logged operator hardware interventions with request deduplication Room and cabinet authoring - Add a visual custom-PCB connection editor with per-cabinet device inventory - Support device IDs, bindings, wiring notes, input polarity, and validation - Add physical-room label/dimension toggles and editable doorway annotations - Preserve doorway changes through undo, save, and revert - Expand cabinet profiles, room layout, and Glitch scenario resources Scenarios and gameplay - Add per-assignment game configuration and runtime feature validation - Add timed test wins and Frogger individual-home objectives - Add the three-cabinet timed/score/Frogger test scenario - Add Cabinet 1 Pac-Man RFID credits, credit sound, and CREDIT 0/1 display - Require physical Start after an accepted scan and prevent duplicate credits Pi hardware and deployment - Add GPIO feedback sampling and a separate multiplexed RFID helper - Validate reader pin ownership and input/output conflicts - Add SSH cabinet startup and Pi desktop-session service support - Add master packaging, site setup, SSH enrollment, and staged cabinet releases - Verify deployment manifests and support explicit activation/status/log commands Documentation and verification - Consolidate the root README around the current architecture and setup workflow - Add the complete master/Pi/RFID export and deployment walkthrough - Document network isolation, credentials, card identification, and rollback - Add PCB, room-editor, feedback, credit, deployment, and controller tests - Isolate protocol tests from designer-edited cabinet configurations Known limitations - RFID helper compilation and physical Pi commissioning remain unverified - Fleet provisioning does not yet configure RFID-only installations completely - Broader Pac-Man tests retain a ghost-capture assertion failure - Staged exports must be rebuilt and checked before deploying runtime 2.3
3083 lines
52 KiB
GDScript
3083 lines
52 KiB
GDScript
@tool
|
|
class_name EscapeRoomWorkspace
|
|
extends Control
|
|
|
|
|
|
signal scenario_selected(
|
|
scenario: ScenarioDefinition
|
|
)
|
|
|
|
|
|
@onready var room_simulator: RoomSimulator = (
|
|
$RoomSimulator
|
|
)
|
|
|
|
@onready var scenario_select: OptionButton = (
|
|
$VBoxContainer/Toolbar/ScenarioSelect
|
|
)
|
|
|
|
@onready var status_label: Label = (
|
|
$VBoxContainer/Toolbar/StatusLabel
|
|
)
|
|
|
|
@onready var room_layout: RoomLayout = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
RoomLayoutPanel/RoomLayout
|
|
)
|
|
|
|
@onready var logic_graph: LogicGraph = (
|
|
$VBoxContainer/MainViews/
|
|
Logic/VBoxContainer/LogicGraph
|
|
)
|
|
|
|
@onready var test_cabinet_select: OptionButton = (
|
|
$VBoxContainer/
|
|
SimulationBar/TestCabinetSelect
|
|
)
|
|
|
|
@onready var test_event_select: OptionButton = (
|
|
$VBoxContainer/
|
|
SimulationBar/TestEventSelect
|
|
)
|
|
|
|
@onready var test_event_button: Button = (
|
|
$VBoxContainer/
|
|
SimulationBar/TestEventButton
|
|
)
|
|
|
|
@onready var selected_label: Label = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/SelectedLabel
|
|
)
|
|
|
|
@onready var x_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/XContainer/XSpinBox
|
|
)
|
|
|
|
@onready var y_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/YContainer/YSpinBox
|
|
)
|
|
|
|
@onready var rotation_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/RotationContainer/RotationSpinBox
|
|
)
|
|
|
|
@onready var snap_check_box: CheckBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/SnapCheckBox
|
|
)
|
|
|
|
@onready var grid_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/GridContainer/GridSpinBox
|
|
)
|
|
|
|
@onready var room_dirty_label: Label = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/DirtyLabel
|
|
)
|
|
|
|
@onready var scenario_dirty_label: Label = (
|
|
$VBoxContainer/MainViews/
|
|
Logic/VBoxContainer/
|
|
EditorBar/ScenarioDirtyLabel
|
|
)
|
|
|
|
@onready var show_connections_check_box: CheckBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/ShowConnectionsCheckBox
|
|
)
|
|
|
|
@onready var edit_boundary_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/EditBoundaryButton
|
|
)
|
|
|
|
@onready var select_tool_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/SelectButton
|
|
)
|
|
|
|
@onready var draw_wall_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/DrawWallButton
|
|
)
|
|
|
|
@onready var add_boundary_point_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/AddBoundaryPointButton
|
|
)
|
|
|
|
@onready var delete_boundary_point_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/DeleteBoundaryPointButton
|
|
)
|
|
|
|
@onready var focus_room_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/FocusRoomButton
|
|
)
|
|
|
|
@onready var reset_view_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/ResetViewButton
|
|
)
|
|
|
|
@onready var zoom_label: Label = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/
|
|
PhysicalToolbar/ZoomLabel
|
|
)
|
|
|
|
@onready var wall_start_x_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/WallStartXContainer/WallStartXSpinBox
|
|
)
|
|
|
|
@onready var wall_start_y_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/WallStartYContainer/WallStartYSpinBox
|
|
)
|
|
|
|
@onready var wall_end_x_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/WallEndXContainer/WallEndXSpinBox
|
|
)
|
|
|
|
@onready var wall_end_y_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/WallEndYContainer/WallEndYSpinBox
|
|
)
|
|
|
|
@onready var wall_thickness_spin_box: SpinBox = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/WallThicknessContainer/WallThicknessSpinBox
|
|
)
|
|
|
|
@onready var delete_wall_button: Button = (
|
|
$VBoxContainer/MainViews/
|
|
Physical/VBoxContainer/LayoutWorkspace/
|
|
PropertyPanel/MarginContainer/
|
|
VBoxContainer/DeleteWallButton
|
|
)
|
|
|
|
@onready var main_views: TabContainer = (
|
|
$VBoxContainer/MainViews
|
|
)
|
|
|
|
@onready
|
|
var simulation_timeline: SimulationTimeline = (
|
|
$VBoxContainer/SimulationTimeline
|
|
)
|
|
|
|
# Physical controls use feet; model values and UndoRedo remain in meters.
|
|
const FEET_TO_METERS: float = 0.3048
|
|
const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
|
|
|
|
const TEST_SEQUENCE_DIRECTORY := (
|
|
"res://data/test_sequences"
|
|
)
|
|
|
|
var test_sequences: Array[TestSequence] = []
|
|
var selected_test_sequence_index: int = -1
|
|
|
|
var undo_redo: EditorUndoRedoManager
|
|
|
|
var test_cabinet_ids: Array[StringName] = []
|
|
var test_event_ids: Array[StringName] = []
|
|
|
|
var context: EscapeRoomEditorContext
|
|
|
|
var scenario_ids: Array[StringName] = []
|
|
|
|
var simulation_driver := EditorSimulationDriver.new()
|
|
|
|
var editor_selection := EscapeRoomEditorSelection.new()
|
|
|
|
var doorway_tool: Button
|
|
var doorway_check: CheckBox
|
|
var swing_check: CheckBox
|
|
|
|
func _ready() -> void:
|
|
_setup_view_controls()
|
|
|
|
select_tool_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.SELECT
|
|
)
|
|
|
|
if not logic_graph.scenario_mapping_changed.is_connected(
|
|
_on_logic_mapping_changed
|
|
):
|
|
logic_graph.scenario_mapping_changed.connect(
|
|
_on_logic_mapping_changed
|
|
)
|
|
|
|
if not logic_graph.edit_rejected.is_connected(
|
|
_on_logic_edit_rejected
|
|
):
|
|
logic_graph.edit_rejected.connect(
|
|
_on_logic_edit_rejected
|
|
)
|
|
|
|
if not room_layout.layout_changed.is_connected(
|
|
_on_room_layout_changed
|
|
):
|
|
room_layout.layout_changed.connect(
|
|
_on_room_layout_changed
|
|
)
|
|
|
|
if not simulation_driver.action_activated.is_connected(
|
|
_on_editor_action_activated
|
|
):
|
|
simulation_driver.action_activated.connect(
|
|
_on_editor_action_activated
|
|
)
|
|
|
|
if not simulation_driver.action_deactivated.is_connected(
|
|
_on_editor_action_deactivated
|
|
):
|
|
simulation_driver.action_deactivated.connect(
|
|
_on_editor_action_deactivated
|
|
)
|
|
|
|
if not room_layout.item_selected.is_connected(
|
|
_on_room_layout_item_selected
|
|
):
|
|
room_layout.item_selected.connect(
|
|
_on_room_layout_item_selected
|
|
)
|
|
|
|
wall_thickness_spin_box.min_value = 0.01
|
|
wall_thickness_spin_box.max_value = 4.0
|
|
wall_thickness_spin_box.step = 0.01
|
|
wall_thickness_spin_box.suffix = " ft"
|
|
|
|
if not x_spin_box.value_changed.is_connected(
|
|
_on_x_spin_box_value_changed
|
|
):
|
|
x_spin_box.value_changed.connect(
|
|
_on_x_spin_box_value_changed
|
|
)
|
|
|
|
if not y_spin_box.value_changed.is_connected(
|
|
_on_y_spin_box_value_changed
|
|
):
|
|
y_spin_box.value_changed.connect(
|
|
_on_y_spin_box_value_changed
|
|
)
|
|
|
|
if not rotation_spin_box.value_changed.is_connected(
|
|
_on_rotation_spin_box_value_changed
|
|
):
|
|
rotation_spin_box.value_changed.connect(
|
|
_on_rotation_spin_box_value_changed
|
|
)
|
|
|
|
snap_check_box.set_pressed_no_signal(
|
|
room_layout.snap_enabled
|
|
)
|
|
|
|
grid_spin_box.set_value_no_signal(
|
|
room_layout.grid_size_meters * METERS_TO_FEET
|
|
)
|
|
|
|
if not snap_check_box.toggled.is_connected(
|
|
_on_snap_check_box_toggled
|
|
):
|
|
snap_check_box.toggled.connect(
|
|
_on_snap_check_box_toggled
|
|
)
|
|
|
|
if not grid_spin_box.value_changed.is_connected(
|
|
_on_grid_spin_box_value_changed
|
|
):
|
|
grid_spin_box.value_changed.connect(
|
|
_on_grid_spin_box_value_changed
|
|
)
|
|
|
|
show_connections_check_box.set_pressed_no_signal(
|
|
room_layout.show_connections
|
|
)
|
|
|
|
if not show_connections_check_box.toggled.is_connected(
|
|
_on_show_connections_check_box_toggled
|
|
):
|
|
show_connections_check_box.toggled.connect(
|
|
_on_show_connections_check_box_toggled
|
|
)
|
|
|
|
if not test_cabinet_select.item_selected.is_connected(
|
|
_on_test_cabinet_select_item_selected
|
|
):
|
|
test_cabinet_select.item_selected.connect(
|
|
_on_test_cabinet_select_item_selected
|
|
)
|
|
|
|
if not test_event_button.pressed.is_connected(
|
|
_on_test_event_button_pressed
|
|
):
|
|
test_event_button.pressed.connect(
|
|
_on_test_event_button_pressed
|
|
)
|
|
|
|
if not room_layout.wall_creation_requested.is_connected(
|
|
_on_wall_creation_requested
|
|
):
|
|
room_layout.wall_creation_requested.connect(
|
|
_on_wall_creation_requested
|
|
)
|
|
|
|
if not room_layout.wall_selected.is_connected(
|
|
_on_room_wall_selected
|
|
):
|
|
room_layout.wall_selected.connect(
|
|
_on_room_wall_selected
|
|
)
|
|
|
|
if not room_layout.wall_geometry_edit_requested.is_connected(
|
|
_on_wall_geometry_edit_requested
|
|
):
|
|
room_layout.wall_geometry_edit_requested.connect(
|
|
_on_wall_geometry_edit_requested
|
|
)
|
|
|
|
if not editor_selection.changed.is_connected(
|
|
_on_editor_selection_changed
|
|
):
|
|
editor_selection.changed.connect(
|
|
_on_editor_selection_changed
|
|
)
|
|
|
|
if not scenario_select.item_selected.is_connected(
|
|
_on_scenario_select_item_selected
|
|
):
|
|
scenario_select.item_selected.connect(
|
|
_on_scenario_select_item_selected
|
|
)
|
|
|
|
if not edit_boundary_button.pressed.is_connected(
|
|
_on_edit_boundary_button_pressed
|
|
):
|
|
edit_boundary_button.pressed.connect(
|
|
_on_edit_boundary_button_pressed
|
|
)
|
|
|
|
if not room_layout.boundary_geometry_edit_requested.is_connected(
|
|
_on_boundary_geometry_edit_requested
|
|
):
|
|
room_layout.boundary_geometry_edit_requested.connect(
|
|
_on_boundary_geometry_edit_requested
|
|
)
|
|
|
|
if not focus_room_button.pressed.is_connected(
|
|
_on_focus_room_button_pressed
|
|
):
|
|
focus_room_button.pressed.connect(
|
|
_on_focus_room_button_pressed
|
|
)
|
|
|
|
if not add_boundary_point_button.pressed.is_connected(
|
|
_on_add_boundary_point_button_pressed
|
|
):
|
|
add_boundary_point_button.pressed.connect(
|
|
_on_add_boundary_point_button_pressed
|
|
)
|
|
|
|
if not delete_boundary_point_button.pressed.is_connected(
|
|
_on_delete_boundary_point_button_pressed
|
|
):
|
|
delete_boundary_point_button.pressed.connect(
|
|
_on_delete_boundary_point_button_pressed
|
|
)
|
|
|
|
if not room_layout.edit_rejected.is_connected(
|
|
_on_room_layout_edit_rejected
|
|
):
|
|
room_layout.edit_rejected.connect(
|
|
_on_room_layout_edit_rejected
|
|
)
|
|
|
|
if not reset_view_button.pressed.is_connected(
|
|
_on_reset_view_button_pressed
|
|
):
|
|
reset_view_button.pressed.connect(
|
|
_on_reset_view_button_pressed
|
|
)
|
|
|
|
if not room_layout.zoom_changed.is_connected(
|
|
_on_room_layout_zoom_changed
|
|
):
|
|
room_layout.zoom_changed.connect(
|
|
_on_room_layout_zoom_changed
|
|
)
|
|
|
|
_on_room_layout_zoom_changed(
|
|
room_layout.zoom
|
|
)
|
|
|
|
if not room_layout.item_position_edit_requested.is_connected(
|
|
_on_room_layout_item_position_edit_requested
|
|
):
|
|
room_layout.item_position_edit_requested.connect(
|
|
_on_room_layout_item_position_edit_requested
|
|
)
|
|
|
|
if not simulation_driver.trace_entry_created.is_connected(
|
|
_on_simulation_trace_entry_created
|
|
):
|
|
simulation_driver.trace_entry_created.connect(
|
|
_on_simulation_trace_entry_created
|
|
)
|
|
|
|
if not simulation_timeline.pause_requested.is_connected(
|
|
_on_simulation_pause_requested
|
|
):
|
|
simulation_timeline.pause_requested.connect(
|
|
_on_simulation_pause_requested
|
|
)
|
|
|
|
if not simulation_timeline.resume_requested.is_connected(
|
|
_on_simulation_resume_requested
|
|
):
|
|
simulation_timeline.resume_requested.connect(
|
|
_on_simulation_resume_requested
|
|
)
|
|
|
|
if not simulation_timeline.step_requested.is_connected(
|
|
_on_simulation_step_requested
|
|
):
|
|
simulation_timeline.step_requested.connect(
|
|
_on_simulation_step_requested
|
|
)
|
|
|
|
if not simulation_timeline.reset_requested.is_connected(
|
|
_on_simulation_reset_requested
|
|
):
|
|
simulation_timeline.reset_requested.connect(
|
|
_on_simulation_reset_requested
|
|
)
|
|
|
|
if not simulation_timeline.sequence_selected.is_connected(
|
|
_on_test_sequence_selected
|
|
):
|
|
simulation_timeline.sequence_selected.connect(
|
|
_on_test_sequence_selected
|
|
)
|
|
|
|
if not simulation_timeline.run_sequence_requested.is_connected(
|
|
_on_run_test_sequence_requested
|
|
):
|
|
simulation_timeline.run_sequence_requested.connect(
|
|
_on_run_test_sequence_requested
|
|
)
|
|
|
|
if not simulation_timeline.stop_sequence_requested.is_connected(
|
|
_on_stop_test_sequence_requested
|
|
):
|
|
simulation_timeline.stop_sequence_requested.connect(
|
|
_on_stop_test_sequence_requested
|
|
)
|
|
|
|
if not simulation_timeline.replay_sequence_requested.is_connected(
|
|
_on_replay_test_sequence_requested
|
|
):
|
|
simulation_timeline.replay_sequence_requested.connect(
|
|
_on_replay_test_sequence_requested
|
|
)
|
|
|
|
if not simulation_driver.sequence_started.is_connected(
|
|
_on_test_sequence_started
|
|
):
|
|
simulation_driver.sequence_started.connect(
|
|
_on_test_sequence_started
|
|
)
|
|
|
|
if not simulation_driver.sequence_finished.is_connected(
|
|
_on_test_sequence_finished
|
|
):
|
|
simulation_driver.sequence_finished.connect(
|
|
_on_test_sequence_finished
|
|
)
|
|
|
|
if not simulation_driver.sequence_stopped.is_connected(
|
|
_on_test_sequence_stopped
|
|
):
|
|
simulation_driver.sequence_stopped.connect(
|
|
_on_test_sequence_stopped
|
|
)
|
|
|
|
if not room_layout.refresh_requested.is_connected(
|
|
_on_room_layout_refresh_requested
|
|
):
|
|
room_layout.refresh_requested.connect(
|
|
_on_room_layout_refresh_requested
|
|
)
|
|
|
|
if not room_layout.binding_route_edit_requested.is_connected(
|
|
_on_room_layout_binding_route_edit_requested
|
|
):
|
|
room_layout.binding_route_edit_requested.connect(
|
|
_on_room_layout_binding_route_edit_requested
|
|
)
|
|
|
|
_load_test_sequences()
|
|
|
|
func _process(
|
|
delta: float
|
|
) -> void:
|
|
|
|
if simulation_driver == null:
|
|
return
|
|
|
|
simulation_driver.advance(
|
|
delta
|
|
)
|
|
|
|
func configure(
|
|
project_database: ProjectDatabase,
|
|
room: RoomDefinition
|
|
) -> bool:
|
|
|
|
context = EscapeRoomEditorContext.new()
|
|
|
|
context.room_changed.connect(
|
|
_on_context_room_changed
|
|
)
|
|
|
|
context.scenario_changed.connect(
|
|
_on_context_scenario_changed
|
|
)
|
|
|
|
context.room_edit_session.dirty_changed.connect(
|
|
_on_room_dirty_changed
|
|
)
|
|
|
|
context.scenario_edit_session.dirty_changed.connect(
|
|
_on_scenario_dirty_changed
|
|
)
|
|
|
|
#
|
|
# Connect simulator output before configuring the context,
|
|
# because context.configure() may immediately emit room_changed.
|
|
#
|
|
_connect_simulation_signals()
|
|
|
|
if not context.configure(
|
|
project_database,
|
|
room
|
|
):
|
|
return false
|
|
|
|
#
|
|
# Opening the first scenario will trigger
|
|
# _on_context_scenario_changed(), which configures
|
|
# the simulation driver, layout, and graph.
|
|
#
|
|
_populate_scenarios()
|
|
|
|
return true
|
|
|
|
func _populate_scenarios() -> void:
|
|
scenario_select.clear()
|
|
scenario_ids.clear()
|
|
|
|
if context == null:
|
|
return
|
|
|
|
for scenario in context.database.scenarios:
|
|
if scenario == null:
|
|
continue
|
|
|
|
scenario_select.add_item(
|
|
scenario.display_name
|
|
)
|
|
|
|
scenario_ids.append(
|
|
scenario.scenario_id
|
|
)
|
|
|
|
if scenario_ids.is_empty():
|
|
status_label.text = "No scenarios"
|
|
return
|
|
|
|
scenario_select.set_block_signals(
|
|
true
|
|
)
|
|
|
|
scenario_select.select(
|
|
0
|
|
)
|
|
|
|
scenario_select.set_block_signals(
|
|
false
|
|
)
|
|
|
|
_open_selected_scenario()
|
|
|
|
func _open_selected_scenario() -> void:
|
|
var index := scenario_select.selected
|
|
|
|
if (
|
|
index < 0
|
|
or index >= scenario_ids.size()
|
|
):
|
|
return
|
|
|
|
var scenario := (
|
|
context.database.get_scenario(
|
|
scenario_ids[index]
|
|
)
|
|
)
|
|
|
|
if scenario == null:
|
|
return
|
|
|
|
if context.open_scenario(
|
|
scenario
|
|
):
|
|
scenario_selected.emit(
|
|
context.get_scenario()
|
|
)
|
|
|
|
func _on_scenario_select_item_selected(
|
|
_index: int
|
|
) -> void:
|
|
|
|
_open_selected_scenario()
|
|
|
|
func _on_context_scenario_changed(
|
|
scenario: ScenarioDefinition
|
|
) -> void:
|
|
|
|
if scenario == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if simulation_timeline != null:
|
|
simulation_timeline.clear()
|
|
|
|
simulation_driver.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
room_layout.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
_clear_property_panel()
|
|
|
|
logic_graph.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
status_label.text = (
|
|
"%s v%d"
|
|
% [
|
|
scenario.display_name,
|
|
scenario.version
|
|
]
|
|
)
|
|
|
|
_populate_test_cabinets()
|
|
|
|
print(
|
|
"[SIM UI] cabinets=%d events=%d"
|
|
% [
|
|
test_cabinet_select.item_count,
|
|
test_event_select.item_count
|
|
]
|
|
)
|
|
|
|
func _on_simulation_trace_entry_created(
|
|
entry: Dictionary
|
|
) -> void:
|
|
|
|
if simulation_timeline == null:
|
|
return
|
|
|
|
simulation_timeline.add_trace_entry(
|
|
entry
|
|
)
|
|
|
|
func _on_context_room_changed(
|
|
room: RoomDefinition
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
room_simulator.configure(
|
|
room
|
|
)
|
|
|
|
var scenario := context.get_scenario()
|
|
|
|
simulation_driver.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
if scenario != null:
|
|
room_layout.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
_clear_property_panel()
|
|
|
|
logic_graph.configure(
|
|
room,
|
|
scenario
|
|
)
|
|
|
|
func _on_logic_mapping_changed() -> void:
|
|
if context == null:
|
|
return
|
|
|
|
context.scenario_edit_session.mark_dirty()
|
|
|
|
validate_setup()
|
|
|
|
func _on_room_layout_changed() -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
_refresh_selected_property_values()
|
|
|
|
func _on_logic_edit_rejected(
|
|
message: String
|
|
) -> void:
|
|
|
|
status_label.text = (
|
|
"Edit rejected: %s"
|
|
% message
|
|
)
|
|
|
|
func validate_setup() -> ValidationResult:
|
|
if context == null:
|
|
var result := ValidationResult.new()
|
|
|
|
result.add_error(
|
|
"Editor workspace is not configured."
|
|
)
|
|
|
|
return result
|
|
|
|
if context.get_room() == null:
|
|
var result := ValidationResult.new()
|
|
|
|
result.add_error(
|
|
"No room is currently open."
|
|
)
|
|
|
|
_display_validation_result(
|
|
result
|
|
)
|
|
|
|
return result
|
|
|
|
if context.get_scenario() == null:
|
|
var result := ValidationResult.new()
|
|
|
|
result.add_error(
|
|
"No scenario is currently open."
|
|
)
|
|
|
|
_display_validation_result(
|
|
result
|
|
)
|
|
|
|
return result
|
|
|
|
var result := (
|
|
EscapeRoomSetupValidator.validate(
|
|
context.get_room(),
|
|
context.get_scenario(),
|
|
context.database
|
|
)
|
|
)
|
|
|
|
_display_validation_result(
|
|
result
|
|
)
|
|
|
|
return result
|
|
|
|
func _display_validation_result(
|
|
result: ValidationResult
|
|
) -> void:
|
|
|
|
if result.is_valid():
|
|
status_label.text = (
|
|
"✓ " + result.get_summary()
|
|
)
|
|
else:
|
|
status_label.text = (
|
|
"⚠ " + result.get_summary()
|
|
)
|
|
|
|
for error in result.errors:
|
|
print(
|
|
"[EDITOR VALIDATION] ERROR: ",
|
|
error
|
|
)
|
|
|
|
for warning in result.warnings:
|
|
print(
|
|
"[EDITOR VALIDATION] WARNING: ",
|
|
warning
|
|
)
|
|
|
|
func _on_save_room_button_pressed() -> void:
|
|
if context == null:
|
|
return
|
|
|
|
if context.save_room():
|
|
|
|
if Engine.is_editor_hint():
|
|
EditorInterface.get_resource_filesystem().scan()
|
|
|
|
status_label.text = (
|
|
"Room saved."
|
|
)
|
|
|
|
func _on_validate_button_pressed() -> void:
|
|
validate_setup()
|
|
|
|
func _on_revert_room_button_pressed() -> void:
|
|
if context == null:
|
|
return
|
|
|
|
context.revert_room()
|
|
|
|
|
|
func _on_revert_scenario_button_pressed() -> void:
|
|
if context == null:
|
|
return
|
|
|
|
context.revert_scenario()
|
|
|
|
func _on_save_scenario_button_pressed() -> void:
|
|
if context == null:
|
|
return
|
|
|
|
if context.save_scenario():
|
|
|
|
if Engine.is_editor_hint():
|
|
EditorInterface.get_resource_filesystem().scan()
|
|
|
|
status_label.text = (
|
|
"Scenario saved."
|
|
)
|
|
|
|
func _on_validate_scenario_button_pressed() -> void:
|
|
validate_setup()
|
|
|
|
func _on_room_dirty_changed(
|
|
dirty: bool
|
|
) -> void:
|
|
|
|
if dirty:
|
|
room_dirty_label.text = (
|
|
"● UNSAVED ROOM CHANGES"
|
|
)
|
|
else:
|
|
room_dirty_label.text = (
|
|
"✓ ROOM SAVED"
|
|
)
|
|
|
|
func _on_scenario_dirty_changed(
|
|
dirty: bool
|
|
) -> void:
|
|
|
|
if dirty:
|
|
scenario_dirty_label.text = (
|
|
"● UNSAVED SCENARIO CHANGES"
|
|
)
|
|
else:
|
|
scenario_dirty_label.text = (
|
|
"✓ SCENARIO SAVED"
|
|
)
|
|
|
|
func _connect_simulation_signals() -> void:
|
|
|
|
if not room_simulator.device_state_changed.is_connected(
|
|
_on_device_state_changed
|
|
):
|
|
room_simulator.device_state_changed.connect(
|
|
_on_device_state_changed
|
|
)
|
|
|
|
if not room_simulator.binding_activated.is_connected(
|
|
_on_binding_activated
|
|
):
|
|
room_simulator.binding_activated.connect(
|
|
_on_binding_activated
|
|
)
|
|
|
|
if not room_simulator.binding_deactivated.is_connected(
|
|
_on_binding_deactivated
|
|
):
|
|
room_simulator.binding_deactivated.connect(
|
|
_on_binding_deactivated
|
|
)
|
|
|
|
if not room_simulator.path_activated.is_connected(
|
|
_on_path_activated
|
|
):
|
|
room_simulator.path_activated.connect(
|
|
_on_path_activated
|
|
)
|
|
|
|
if not room_simulator.path_deactivated.is_connected(
|
|
_on_path_deactivated
|
|
):
|
|
room_simulator.path_deactivated.connect(
|
|
_on_path_deactivated
|
|
)
|
|
|
|
if not logic_graph.logic_path_selected.is_connected(
|
|
_on_logic_path_selected
|
|
):
|
|
logic_graph.logic_path_selected.connect(
|
|
_on_logic_path_selected
|
|
)
|
|
|
|
func _on_device_state_changed(
|
|
device_id: StringName,
|
|
active: bool
|
|
) -> void:
|
|
|
|
room_layout.set_device_active(
|
|
device_id,
|
|
active
|
|
)
|
|
|
|
func _on_binding_activated(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
room_layout.set_binding_active(
|
|
cabinet_id,
|
|
action_id,
|
|
device_id,
|
|
true
|
|
)
|
|
|
|
func _on_binding_deactivated(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
room_layout.set_binding_active(
|
|
cabinet_id,
|
|
action_id,
|
|
device_id,
|
|
false
|
|
)
|
|
|
|
func _on_path_activated(
|
|
_path_id: StringName,
|
|
cabinet_id: StringName,
|
|
event_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
print(
|
|
"[EDITOR PATH] %s :: %s -> %s -> %s"
|
|
% [
|
|
cabinet_id,
|
|
event_id,
|
|
action_id,
|
|
device_id
|
|
]
|
|
)
|
|
|
|
logic_graph.set_path_active(
|
|
cabinet_id,
|
|
event_id,
|
|
action_id,
|
|
device_id,
|
|
true
|
|
)
|
|
|
|
func _on_path_deactivated(
|
|
_path_id: StringName,
|
|
cabinet_id: StringName,
|
|
event_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
logic_graph.set_path_active(
|
|
cabinet_id,
|
|
event_id,
|
|
action_id,
|
|
device_id,
|
|
false
|
|
)
|
|
|
|
func simulate_game_event(
|
|
cabinet_id: StringName,
|
|
event_id: StringName
|
|
) -> void:
|
|
|
|
simulation_driver.simulate_game_event(
|
|
cabinet_id,
|
|
event_id
|
|
)
|
|
|
|
func _on_editor_action_activated(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
context_data: Dictionary
|
|
) -> void:
|
|
|
|
room_simulator.simulate_action_activated(
|
|
cabinet_id,
|
|
action_id,
|
|
context_data
|
|
)
|
|
|
|
func _on_editor_action_deactivated(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
context_data: Dictionary
|
|
) -> void:
|
|
|
|
room_simulator.simulate_action_deactivated(
|
|
cabinet_id,
|
|
action_id,
|
|
context_data
|
|
)
|
|
|
|
func _populate_test_cabinets() -> void:
|
|
|
|
test_cabinet_select.clear()
|
|
test_cabinet_ids.clear()
|
|
|
|
var scenario := context.get_scenario()
|
|
|
|
if scenario == null:
|
|
return
|
|
|
|
for assignment in scenario.assignments:
|
|
if assignment == null:
|
|
continue
|
|
|
|
if assignment.cabinet == null:
|
|
continue
|
|
|
|
var label := String(
|
|
assignment.cabinet.display_name
|
|
)
|
|
|
|
if label.is_empty():
|
|
label = String(
|
|
assignment.cabinet.cabinet_id
|
|
)
|
|
|
|
test_cabinet_select.add_item(
|
|
label
|
|
)
|
|
|
|
test_cabinet_ids.append(
|
|
assignment.cabinet.cabinet_id
|
|
)
|
|
|
|
if test_cabinet_ids.is_empty():
|
|
test_event_select.clear()
|
|
test_event_ids.clear()
|
|
return
|
|
|
|
test_cabinet_select.set_block_signals(
|
|
true
|
|
)
|
|
|
|
test_cabinet_select.select(
|
|
0
|
|
)
|
|
|
|
test_cabinet_select.set_block_signals(
|
|
false
|
|
)
|
|
|
|
_populate_test_events(
|
|
0
|
|
)
|
|
|
|
func _populate_test_events(
|
|
cabinet_index: int
|
|
) -> void:
|
|
|
|
test_event_select.clear()
|
|
test_event_ids.clear()
|
|
|
|
if (
|
|
cabinet_index < 0
|
|
or cabinet_index >= test_cabinet_ids.size()
|
|
):
|
|
return
|
|
|
|
var scenario := context.get_scenario()
|
|
|
|
if scenario == null:
|
|
return
|
|
|
|
var cabinet_id := (
|
|
test_cabinet_ids[
|
|
cabinet_index
|
|
]
|
|
)
|
|
|
|
var assignment := (
|
|
scenario.get_assignment_for_cabinet(
|
|
cabinet_id
|
|
)
|
|
)
|
|
|
|
if assignment == null:
|
|
return
|
|
|
|
if assignment.game == null:
|
|
return
|
|
|
|
for event_definition in assignment.game.events:
|
|
if event_definition == null:
|
|
continue
|
|
|
|
var label := String(
|
|
event_definition.display_name
|
|
)
|
|
|
|
if label.is_empty():
|
|
label = String(
|
|
event_definition.event_id
|
|
)
|
|
|
|
test_event_select.add_item(
|
|
label
|
|
)
|
|
|
|
test_event_ids.append(
|
|
event_definition.event_id
|
|
)
|
|
|
|
if not test_event_ids.is_empty():
|
|
|
|
test_event_select.set_block_signals(
|
|
true
|
|
)
|
|
|
|
test_event_select.select(
|
|
0
|
|
)
|
|
|
|
test_event_select.set_block_signals(
|
|
false
|
|
)
|
|
|
|
func _on_test_cabinet_select_item_selected(
|
|
index: int
|
|
) -> void:
|
|
|
|
_populate_test_events(
|
|
index
|
|
)
|
|
|
|
func _on_test_event_button_pressed() -> void:
|
|
|
|
var cabinet_index := (
|
|
test_cabinet_select.selected
|
|
)
|
|
|
|
var event_index := (
|
|
test_event_select.selected
|
|
)
|
|
|
|
#
|
|
# The editor can temporarily report -1 even when
|
|
# an OptionButton contains entries. Fall back to
|
|
# the first available item.
|
|
#
|
|
if (
|
|
cabinet_index < 0
|
|
and not test_cabinet_ids.is_empty()
|
|
):
|
|
cabinet_index = 0
|
|
|
|
if (
|
|
event_index < 0
|
|
and not test_event_ids.is_empty()
|
|
):
|
|
event_index = 0
|
|
|
|
if (
|
|
cabinet_index < 0
|
|
or cabinet_index >= test_cabinet_ids.size()
|
|
):
|
|
return
|
|
|
|
if (
|
|
event_index < 0
|
|
or event_index >= test_event_ids.size()
|
|
):
|
|
return
|
|
|
|
var cabinet_id := (
|
|
test_cabinet_ids[
|
|
cabinet_index
|
|
]
|
|
)
|
|
|
|
var event_id := (
|
|
test_event_ids[
|
|
event_index
|
|
]
|
|
)
|
|
|
|
print(
|
|
"[EDITOR SIMULATION] %s -> %s"
|
|
% [
|
|
cabinet_id,
|
|
event_id
|
|
]
|
|
)
|
|
|
|
simulate_game_event(
|
|
cabinet_id,
|
|
event_id
|
|
)
|
|
|
|
func _on_room_layout_item_selected(
|
|
item_id: StringName
|
|
) -> void:
|
|
|
|
_refresh_property_panel(
|
|
item_id
|
|
)
|
|
|
|
if room_layout.selected_item_is_cabinet():
|
|
editor_selection.select_cabinet(
|
|
item_id
|
|
)
|
|
else:
|
|
editor_selection.select_device(
|
|
item_id
|
|
)
|
|
|
|
func _refresh_property_panel(
|
|
item_id: StringName
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var position := (
|
|
room_layout.get_selected_position()
|
|
)
|
|
|
|
x_spin_box.set_value_no_signal(
|
|
position.x * METERS_TO_FEET
|
|
)
|
|
|
|
y_spin_box.set_value_no_signal(
|
|
position.y * METERS_TO_FEET
|
|
)
|
|
|
|
if room_layout.selected_item_is_cabinet():
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
item_id
|
|
)
|
|
)
|
|
|
|
selected_label.text = (
|
|
"Cabinet: %s"
|
|
% item_id
|
|
)
|
|
|
|
rotation_spin_box.editable = true
|
|
|
|
if placement != null:
|
|
rotation_spin_box.set_value_no_signal(
|
|
placement.rotation_degrees
|
|
)
|
|
|
|
else:
|
|
var device := (
|
|
room.get_device(
|
|
item_id
|
|
)
|
|
)
|
|
|
|
if device != null:
|
|
selected_label.text = (
|
|
"Device: %s"
|
|
% device.display_name
|
|
)
|
|
else:
|
|
selected_label.text = (
|
|
"Device: %s"
|
|
% item_id
|
|
)
|
|
|
|
#
|
|
# Room devices currently have position,
|
|
# but no rotation property.
|
|
#
|
|
rotation_spin_box.set_value_no_signal(
|
|
0.0
|
|
)
|
|
|
|
rotation_spin_box.editable = false
|
|
|
|
func _on_x_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
if room_layout.selected_item_id == &"":
|
|
return
|
|
|
|
var old_position := (
|
|
room_layout.get_selected_position()
|
|
)
|
|
|
|
var requested_position := Vector2(
|
|
value_ft * FEET_TO_METERS,
|
|
old_position.y
|
|
)
|
|
|
|
var new_position := (
|
|
room_layout.snap_item_position_for_editor(
|
|
room_layout.selected_item_id,
|
|
requested_position
|
|
)
|
|
)
|
|
|
|
_commit_position_change(
|
|
old_position,
|
|
new_position
|
|
)
|
|
|
|
func _on_y_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
if room_layout.selected_item_id == &"":
|
|
return
|
|
|
|
var old_position := (
|
|
room_layout.get_selected_position()
|
|
)
|
|
|
|
var requested_position := Vector2(
|
|
old_position.x,
|
|
value_ft * FEET_TO_METERS
|
|
)
|
|
|
|
var new_position := (
|
|
room_layout.snap_item_position_for_editor(
|
|
room_layout.selected_item_id,
|
|
requested_position
|
|
)
|
|
)
|
|
|
|
_commit_position_change(
|
|
old_position,
|
|
new_position
|
|
)
|
|
|
|
func _on_rotation_spin_box_value_changed(
|
|
value: float
|
|
) -> void:
|
|
|
|
if room_layout.selected_item_id == &"":
|
|
return
|
|
|
|
if not room_layout.selected_item_is_cabinet():
|
|
return
|
|
|
|
var old_rotation := (
|
|
room_layout.get_selected_rotation()
|
|
)
|
|
|
|
_commit_rotation_change(
|
|
old_rotation,
|
|
value
|
|
)
|
|
|
|
func _refresh_selected_property_values() -> void:
|
|
|
|
if room_layout.selected_item_id == &"":
|
|
return
|
|
|
|
var position := (
|
|
room_layout.get_selected_position()
|
|
)
|
|
|
|
x_spin_box.set_value_no_signal(
|
|
position.x * METERS_TO_FEET
|
|
)
|
|
|
|
y_spin_box.set_value_no_signal(
|
|
position.y * METERS_TO_FEET
|
|
)
|
|
|
|
rotation_spin_box.set_value_no_signal(
|
|
room_layout.get_selected_rotation()
|
|
)
|
|
|
|
func _on_snap_check_box_toggled(
|
|
enabled: bool
|
|
) -> void:
|
|
|
|
room_layout.snap_enabled = enabled
|
|
|
|
func _on_grid_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
room_layout.grid_size_meters = max(
|
|
value_ft * FEET_TO_METERS,
|
|
0.01
|
|
)
|
|
room_layout.queue_redraw()
|
|
|
|
func _on_show_connections_check_box_toggled(
|
|
enabled: bool
|
|
) -> void:
|
|
|
|
room_layout.set_connections_visible(
|
|
enabled
|
|
)
|
|
|
|
func _clear_property_panel() -> void:
|
|
if doorway_check != null:
|
|
doorway_check.disabled = true
|
|
swing_check.disabled = true
|
|
|
|
selected_label.text = (
|
|
"Nothing selected"
|
|
)
|
|
|
|
x_spin_box.set_value_no_signal(
|
|
0.0
|
|
)
|
|
|
|
y_spin_box.set_value_no_signal(
|
|
0.0
|
|
)
|
|
|
|
rotation_spin_box.set_value_no_signal(
|
|
0.0
|
|
)
|
|
|
|
rotation_spin_box.editable = false
|
|
|
|
func set_undo_redo_manager(
|
|
manager: EditorUndoRedoManager
|
|
) -> void:
|
|
|
|
undo_redo = manager
|
|
|
|
func _apply_selected_position(
|
|
position: Vector2
|
|
) -> void:
|
|
|
|
room_layout.set_selected_position(
|
|
position
|
|
)
|
|
|
|
_refresh_selected_property_values()
|
|
|
|
func _commit_position_change(
|
|
old_position: Vector2,
|
|
new_position: Vector2
|
|
) -> void:
|
|
|
|
if old_position == new_position:
|
|
_refresh_selected_property_values()
|
|
return
|
|
|
|
var item_id := (
|
|
room_layout.selected_item_id
|
|
)
|
|
|
|
if item_id == &"":
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_apply_item_position(
|
|
item_id,
|
|
new_position
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Move Room Item",
|
|
UndoRedo.MERGE_ENDS,
|
|
context.get_room()
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_item_position",
|
|
item_id,
|
|
new_position
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_item_position",
|
|
item_id,
|
|
old_position
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _apply_selected_rotation(
|
|
rotation: float
|
|
) -> void:
|
|
|
|
room_layout.set_selected_rotation(
|
|
rotation
|
|
)
|
|
|
|
_refresh_selected_property_values()
|
|
|
|
func _commit_rotation_change(
|
|
old_rotation: float,
|
|
new_rotation: float
|
|
) -> void:
|
|
|
|
if is_equal_approx(
|
|
old_rotation,
|
|
new_rotation
|
|
):
|
|
return
|
|
|
|
var cabinet_id := (
|
|
room_layout.selected_item_id
|
|
)
|
|
|
|
if cabinet_id == &"":
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_apply_cabinet_rotation(
|
|
cabinet_id,
|
|
new_rotation
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Rotate Cabinet",
|
|
UndoRedo.MERGE_ENDS,
|
|
context.get_room()
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_cabinet_rotation",
|
|
cabinet_id,
|
|
new_rotation
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_cabinet_rotation",
|
|
cabinet_id,
|
|
old_rotation
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_wall_creation_requested(
|
|
start: Vector2,
|
|
end: Vector2
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var target_room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
var wall := (
|
|
RoomWallDefinition.new()
|
|
)
|
|
|
|
wall.wall_id = (
|
|
_generate_wall_id(
|
|
target_room
|
|
)
|
|
)
|
|
|
|
wall.start = start
|
|
wall.end = end
|
|
wall.thickness = 0.10
|
|
wall.is_doorway = doorway_tool.button_pressed
|
|
|
|
if undo_redo == null:
|
|
_add_wall(
|
|
target_room,
|
|
wall
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Add Room Wall",
|
|
UndoRedo.MERGE_DISABLE,
|
|
target_room
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_add_wall",
|
|
target_room,
|
|
wall
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_remove_wall",
|
|
target_room,
|
|
wall
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _generate_wall_id(
|
|
room: RoomDefinition
|
|
) -> StringName:
|
|
|
|
var index := 1
|
|
|
|
while true:
|
|
var candidate := StringName(
|
|
"wall_%03d"
|
|
% index
|
|
)
|
|
|
|
if not room.has_wall(
|
|
candidate
|
|
):
|
|
return candidate
|
|
|
|
index += 1
|
|
|
|
return &"wall"
|
|
|
|
func _add_wall(
|
|
target_room: RoomDefinition,
|
|
wall: RoomWallDefinition
|
|
) -> void:
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
if target_room.walls.has(
|
|
wall
|
|
):
|
|
return
|
|
|
|
target_room.walls.append(
|
|
wall
|
|
)
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.queue_redraw()
|
|
|
|
|
|
func _remove_wall(
|
|
target_room: RoomDefinition,
|
|
wall: RoomWallDefinition
|
|
) -> void:
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
target_room.walls.erase(
|
|
wall
|
|
)
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
if (
|
|
room_layout.selected_wall_id
|
|
== wall.wall_id
|
|
):
|
|
room_layout.selected_wall_id = &""
|
|
_clear_property_panel()
|
|
|
|
room_layout.queue_redraw()
|
|
|
|
func _on_select_tool_button_pressed() -> void:
|
|
|
|
select_tool_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.SELECT
|
|
)
|
|
|
|
|
|
func _on_draw_wall_button_pressed() -> void:
|
|
|
|
draw_wall_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.DRAW_WALL
|
|
)
|
|
|
|
|
|
func _on_edit_boundary_button_pressed() -> void:
|
|
|
|
edit_boundary_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.EDIT_BOUNDARY
|
|
)
|
|
|
|
func _on_room_wall_selected(
|
|
wall_id: StringName
|
|
) -> void:
|
|
|
|
_refresh_wall_property_panel(
|
|
wall_id
|
|
)
|
|
|
|
editor_selection.select_wall(
|
|
wall_id
|
|
)
|
|
|
|
func _refresh_wall_property_panel(
|
|
wall_id: StringName
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var wall := room.get_wall(
|
|
wall_id
|
|
)
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
selected_label.text = (
|
|
("Doorway: %s" if wall.is_doorway else "Wall: %s")
|
|
% wall.wall_id
|
|
)
|
|
|
|
doorway_check.disabled = false
|
|
doorway_check.set_pressed_no_signal(wall.is_doorway)
|
|
swing_check.set_pressed_no_signal(wall.swing_reversed)
|
|
swing_check.disabled = not wall.is_doorway
|
|
|
|
wall_start_x_spin_box.set_value_no_signal(
|
|
wall.start.x * METERS_TO_FEET
|
|
)
|
|
|
|
wall_start_y_spin_box.set_value_no_signal(
|
|
wall.start.y * METERS_TO_FEET
|
|
)
|
|
|
|
wall_end_x_spin_box.set_value_no_signal(
|
|
wall.end.x * METERS_TO_FEET
|
|
)
|
|
|
|
wall_end_y_spin_box.set_value_no_signal(
|
|
wall.end.y * METERS_TO_FEET
|
|
)
|
|
|
|
wall_thickness_spin_box.set_value_no_signal(
|
|
wall.thickness_ft
|
|
)
|
|
|
|
func _apply_wall_geometry(
|
|
target_room: RoomDefinition,
|
|
wall: RoomWallDefinition,
|
|
start: Vector2,
|
|
end: Vector2
|
|
) -> void:
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
if not target_room.walls.has(
|
|
wall
|
|
):
|
|
return
|
|
|
|
wall.start = start
|
|
wall.end = end
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.queue_redraw()
|
|
|
|
if (
|
|
room_layout.selected_wall_id
|
|
== wall.wall_id
|
|
):
|
|
_refresh_wall_property_panel(
|
|
wall.wall_id
|
|
)
|
|
|
|
func _commit_wall_geometry_change(
|
|
wall: RoomWallDefinition,
|
|
old_start: Vector2,
|
|
old_end: Vector2,
|
|
new_start: Vector2,
|
|
new_end: Vector2
|
|
) -> void:
|
|
|
|
if (
|
|
old_start == new_start
|
|
and old_end == new_end
|
|
):
|
|
_refresh_wall_property_panel(wall.wall_id)
|
|
return
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var target_room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_apply_wall_geometry(
|
|
target_room,
|
|
wall,
|
|
new_start,
|
|
new_end
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Edit Wall",
|
|
UndoRedo.MERGE_ENDS,
|
|
target_room
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_wall_geometry",
|
|
target_room,
|
|
wall,
|
|
new_start,
|
|
new_end
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_wall_geometry",
|
|
target_room,
|
|
wall,
|
|
old_start,
|
|
old_end
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_wall_start_x_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
var wall := room_layout.get_selected_wall()
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
var old_start := wall.start
|
|
var old_end := wall.end
|
|
|
|
var new_start := Vector2(
|
|
value_ft * FEET_TO_METERS,
|
|
old_start.y
|
|
)
|
|
|
|
new_start = room_layout.snap_position_for_editor(
|
|
new_start
|
|
)
|
|
|
|
_commit_wall_geometry_change(
|
|
wall,
|
|
old_start,
|
|
old_end,
|
|
new_start,
|
|
old_end
|
|
)
|
|
|
|
func _on_wall_start_y_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
var wall := room_layout.get_selected_wall()
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
var old_start := wall.start
|
|
var old_end := wall.end
|
|
|
|
var new_start := Vector2(
|
|
old_start.x,
|
|
value_ft * FEET_TO_METERS
|
|
)
|
|
|
|
new_start = room_layout.snap_position_for_editor(
|
|
new_start
|
|
)
|
|
|
|
_commit_wall_geometry_change(
|
|
wall,
|
|
old_start,
|
|
old_end,
|
|
new_start,
|
|
old_end
|
|
)
|
|
|
|
func _on_wall_end_x_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
var wall := room_layout.get_selected_wall()
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
var old_start := wall.start
|
|
var old_end := wall.end
|
|
|
|
var new_end := Vector2(
|
|
value_ft * FEET_TO_METERS,
|
|
old_end.y
|
|
)
|
|
|
|
new_end = room_layout.snap_position_for_editor(
|
|
new_end
|
|
)
|
|
|
|
_commit_wall_geometry_change(
|
|
wall,
|
|
old_start,
|
|
old_end,
|
|
old_start,
|
|
new_end
|
|
)
|
|
|
|
func _on_wall_end_y_spin_box_value_changed(
|
|
value_ft: float
|
|
) -> void:
|
|
|
|
var wall := room_layout.get_selected_wall()
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
var old_start := wall.start
|
|
var old_end := wall.end
|
|
|
|
var new_end := Vector2(
|
|
old_end.x,
|
|
value_ft * FEET_TO_METERS
|
|
)
|
|
|
|
new_end = room_layout.snap_position_for_editor(
|
|
new_end
|
|
)
|
|
|
|
_commit_wall_geometry_change(
|
|
wall,
|
|
old_start,
|
|
old_end,
|
|
old_start,
|
|
new_end
|
|
)
|
|
|
|
func _apply_wall_thickness(
|
|
target_room: RoomDefinition,
|
|
wall: RoomWallDefinition,
|
|
thickness_ft: float
|
|
) -> void:
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
if not target_room.walls.has(
|
|
wall
|
|
):
|
|
return
|
|
|
|
wall.thickness_ft = thickness_ft
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.queue_redraw()
|
|
|
|
if (
|
|
room_layout.selected_wall_id
|
|
== wall.wall_id
|
|
):
|
|
_refresh_wall_property_panel(
|
|
wall.wall_id
|
|
)
|
|
|
|
func _on_wall_thickness_spin_box_value_changed(
|
|
value: float
|
|
) -> void:
|
|
|
|
var wall := (
|
|
room_layout.get_selected_wall()
|
|
)
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var target_room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
var old_value_ft := (
|
|
wall.thickness_ft
|
|
)
|
|
|
|
if is_equal_approx(
|
|
old_value_ft,
|
|
value
|
|
):
|
|
return
|
|
|
|
if undo_redo == null:
|
|
|
|
_apply_wall_thickness(
|
|
target_room,
|
|
wall,
|
|
value
|
|
)
|
|
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Change Wall Thickness",
|
|
UndoRedo.MERGE_ENDS,
|
|
target_room
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_wall_thickness",
|
|
target_room,
|
|
wall,
|
|
value
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_wall_thickness",
|
|
target_room,
|
|
wall,
|
|
old_value_ft
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_delete_wall_button_pressed() -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var target_room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if target_room == null:
|
|
return
|
|
|
|
var wall := (
|
|
room_layout.get_selected_wall()
|
|
)
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_remove_wall(
|
|
target_room,
|
|
wall
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Delete Room Wall",
|
|
UndoRedo.MERGE_DISABLE,
|
|
target_room
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_remove_wall",
|
|
target_room,
|
|
wall
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_add_wall",
|
|
target_room,
|
|
wall
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_wall_geometry_edit_requested(
|
|
wall_id: StringName,
|
|
old_start: Vector2,
|
|
old_end: Vector2,
|
|
new_start: Vector2,
|
|
new_end: Vector2
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var wall := room.get_wall(
|
|
wall_id
|
|
)
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
_commit_wall_geometry_change(
|
|
wall,
|
|
old_start,
|
|
old_end,
|
|
new_start,
|
|
new_end
|
|
)
|
|
|
|
func _on_editor_selection_changed() -> void:
|
|
|
|
room_layout.apply_editor_selection(
|
|
editor_selection
|
|
)
|
|
|
|
logic_graph.apply_editor_selection(
|
|
editor_selection
|
|
)
|
|
|
|
func _on_logic_path_selected(
|
|
cabinet_id: StringName,
|
|
event_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
editor_selection.select_logic_path(
|
|
cabinet_id,
|
|
event_id,
|
|
action_id,
|
|
device_id
|
|
)
|
|
|
|
func _apply_boundary_geometry(
|
|
points: PackedVector2Array
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if room == null:
|
|
return
|
|
|
|
room.boundary_points = (
|
|
points.duplicate()
|
|
)
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.queue_redraw()
|
|
|
|
func _on_boundary_geometry_edit_requested(
|
|
old_points: PackedVector2Array,
|
|
new_points: PackedVector2Array
|
|
) -> void:
|
|
|
|
if old_points == new_points:
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_apply_boundary_geometry(
|
|
new_points
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Edit Room Boundary",
|
|
UndoRedo.MERGE_ENDS,
|
|
context.get_room()
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_boundary_geometry",
|
|
new_points
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_boundary_geometry",
|
|
old_points
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_focus_room_button_pressed() -> void:
|
|
|
|
room_layout.focus_room()
|
|
|
|
func _on_add_boundary_point_button_pressed() -> void:
|
|
|
|
add_boundary_point_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.ADD_BOUNDARY_POINT
|
|
)
|
|
|
|
func _on_delete_boundary_point_button_pressed() -> void:
|
|
|
|
delete_boundary_point_button.button_pressed = true
|
|
|
|
room_layout.set_edit_mode(
|
|
RoomLayout.EditMode.DELETE_BOUNDARY_POINT
|
|
)
|
|
|
|
func _on_room_layout_edit_rejected(
|
|
message: String
|
|
) -> void:
|
|
|
|
status_label.text = (
|
|
"Edit rejected: %s"
|
|
% message
|
|
)
|
|
|
|
func _on_reset_view_button_pressed() -> void:
|
|
|
|
room_layout.reset_view()
|
|
|
|
func _on_room_layout_zoom_changed(
|
|
zoom_value: float
|
|
) -> void:
|
|
|
|
zoom_label.text = (
|
|
"%d%%"
|
|
% roundi(
|
|
zoom_value * 100.0
|
|
)
|
|
)
|
|
|
|
func _apply_cabinet_rotation(
|
|
cabinet_id: StringName,
|
|
rotation: float
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := context.get_room()
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
cabinet_id
|
|
)
|
|
)
|
|
|
|
if placement == null:
|
|
return
|
|
|
|
placement.rotation_degrees = rotation
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.queue_redraw()
|
|
room_layout._reposition_items()
|
|
|
|
func _unhandled_key_input(
|
|
event: InputEvent
|
|
) -> void:
|
|
|
|
if not Engine.is_editor_hint():
|
|
return
|
|
|
|
if not (
|
|
event is InputEventKey
|
|
):
|
|
return
|
|
|
|
if not event.pressed:
|
|
return
|
|
|
|
if event.echo:
|
|
return
|
|
|
|
var ctrl_pressed: bool = (
|
|
event.ctrl_pressed
|
|
or event.meta_pressed
|
|
)
|
|
|
|
if not ctrl_pressed:
|
|
return
|
|
|
|
if (
|
|
event.keycode == KEY_Z
|
|
and event.shift_pressed
|
|
):
|
|
redo_escape_room_action()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
if event.keycode == KEY_Z:
|
|
undo_escape_room_action()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
if event.keycode == KEY_Y:
|
|
redo_escape_room_action()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
func _get_active_escape_room_undo_redo() -> UndoRedo:
|
|
|
|
if undo_redo == null:
|
|
return null
|
|
|
|
if context == null:
|
|
return null
|
|
|
|
var target: Object = null
|
|
|
|
var current_tab_name := (
|
|
main_views.get_tab_title(
|
|
main_views.current_tab
|
|
)
|
|
)
|
|
|
|
if current_tab_name == "Logic":
|
|
target = context.get_scenario()
|
|
else:
|
|
target = context.get_room()
|
|
|
|
if target == null:
|
|
return null
|
|
|
|
var history_id := (
|
|
undo_redo.get_object_history_id(
|
|
target
|
|
)
|
|
)
|
|
|
|
return (
|
|
undo_redo.get_history_undo_redo(
|
|
history_id
|
|
)
|
|
)
|
|
|
|
func undo_escape_room_action() -> void:
|
|
|
|
var history := (
|
|
_get_active_escape_room_undo_redo()
|
|
)
|
|
|
|
if history == null:
|
|
print("[UNDO] No history")
|
|
return
|
|
|
|
print(
|
|
"[UNDO] has_undo=",
|
|
history.has_undo()
|
|
)
|
|
|
|
if history.has_undo():
|
|
history.undo()
|
|
|
|
|
|
func redo_escape_room_action() -> void:
|
|
|
|
var history := (
|
|
_get_active_escape_room_undo_redo()
|
|
)
|
|
|
|
if history == null:
|
|
return
|
|
|
|
if not history.has_redo():
|
|
return
|
|
|
|
history.redo()
|
|
|
|
func _on_room_layout_item_position_edit_requested(
|
|
item_id: StringName,
|
|
old_position: Vector2,
|
|
new_position: Vector2
|
|
) -> void:
|
|
|
|
if old_position == new_position:
|
|
return
|
|
|
|
if undo_redo == null:
|
|
_apply_item_position(
|
|
item_id,
|
|
new_position
|
|
)
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Move Room Item",
|
|
UndoRedo.MERGE_DISABLE,
|
|
context.get_room()
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_item_position",
|
|
item_id,
|
|
new_position
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_item_position",
|
|
item_id,
|
|
old_position
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
func _on_room_layout_binding_route_edit_requested(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName,
|
|
old_points: PackedVector2Array,
|
|
new_points: PackedVector2Array
|
|
) -> void:
|
|
|
|
if old_points == new_points:
|
|
return
|
|
|
|
if context == null:
|
|
return
|
|
|
|
if undo_redo == null:
|
|
|
|
_apply_binding_route_points(
|
|
cabinet_id,
|
|
action_id,
|
|
device_id,
|
|
new_points
|
|
)
|
|
|
|
return
|
|
|
|
undo_redo.create_action(
|
|
"Move Cable Route Point",
|
|
UndoRedo.MERGE_DISABLE,
|
|
context.get_room()
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
"_apply_binding_route_points",
|
|
cabinet_id,
|
|
action_id,
|
|
device_id,
|
|
new_points.duplicate()
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
"_apply_binding_route_points",
|
|
cabinet_id,
|
|
action_id,
|
|
device_id,
|
|
old_points.duplicate()
|
|
)
|
|
|
|
undo_redo.commit_action()
|
|
|
|
|
|
func _apply_binding_route_points(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName,
|
|
points: PackedVector2Array
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var target_binding: RoomDeviceBinding = null
|
|
|
|
for binding in room.bindings:
|
|
|
|
if binding == null:
|
|
continue
|
|
|
|
if (
|
|
binding.cabinet_id == cabinet_id
|
|
and binding.hardware_action_id == action_id
|
|
and binding.device_id == device_id
|
|
):
|
|
|
|
target_binding = binding
|
|
break
|
|
|
|
if target_binding == null:
|
|
return
|
|
|
|
target_binding.route_points = (
|
|
points.duplicate()
|
|
)
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.refresh_item_positions()
|
|
|
|
func _apply_item_position(
|
|
item_id: StringName,
|
|
position: Vector2
|
|
) -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
item_id
|
|
)
|
|
)
|
|
|
|
if placement != null:
|
|
|
|
placement.position = (
|
|
position
|
|
)
|
|
|
|
else:
|
|
|
|
var device := (
|
|
room.get_device(
|
|
item_id
|
|
)
|
|
)
|
|
|
|
if device == null:
|
|
return
|
|
|
|
device.position = (
|
|
position
|
|
)
|
|
|
|
context.room_edit_session.mark_dirty()
|
|
|
|
room_layout.refresh_item_positions()
|
|
|
|
if room_layout.selected_item_id == item_id:
|
|
_refresh_selected_property_values()
|
|
|
|
func _on_simulation_pause_requested() -> void:
|
|
|
|
simulation_driver.set_paused(
|
|
true
|
|
)
|
|
|
|
simulation_timeline.set_paused(
|
|
true
|
|
)
|
|
|
|
|
|
func _on_simulation_resume_requested() -> void:
|
|
|
|
simulation_driver.set_paused(
|
|
false
|
|
)
|
|
|
|
simulation_timeline.set_paused(
|
|
false
|
|
)
|
|
|
|
|
|
func _on_simulation_step_requested(
|
|
seconds: float
|
|
) -> void:
|
|
|
|
simulation_driver.step(
|
|
seconds
|
|
)
|
|
|
|
|
|
func _on_simulation_reset_requested() -> void:
|
|
|
|
simulation_driver.reset_simulation()
|
|
|
|
_reset_simulation_visuals()
|
|
|
|
simulation_timeline.clear()
|
|
|
|
simulation_timeline.set_paused(
|
|
false
|
|
)
|
|
|
|
simulation_timeline.set_sequence_running(
|
|
false
|
|
)
|
|
|
|
func _load_test_sequences() -> void:
|
|
|
|
test_sequences.clear()
|
|
|
|
var directory := DirAccess.open(
|
|
TEST_SEQUENCE_DIRECTORY
|
|
)
|
|
|
|
if directory == null:
|
|
|
|
push_warning(
|
|
"Could not open test sequence directory: %s"
|
|
% TEST_SEQUENCE_DIRECTORY
|
|
)
|
|
|
|
simulation_timeline.set_sequences(
|
|
test_sequences
|
|
)
|
|
|
|
return
|
|
|
|
directory.list_dir_begin()
|
|
|
|
while true:
|
|
|
|
var file_name := (
|
|
directory.get_next()
|
|
)
|
|
|
|
if file_name.is_empty():
|
|
break
|
|
|
|
if directory.current_is_dir():
|
|
continue
|
|
|
|
if not file_name.ends_with(
|
|
".tres"
|
|
):
|
|
continue
|
|
|
|
var path := (
|
|
TEST_SEQUENCE_DIRECTORY
|
|
.path_join(
|
|
file_name
|
|
)
|
|
)
|
|
|
|
var resource := (
|
|
ResourceLoader.load(
|
|
path
|
|
)
|
|
)
|
|
|
|
var sequence := (
|
|
resource as TestSequence
|
|
)
|
|
|
|
if sequence == null:
|
|
|
|
push_warning(
|
|
"Resource is not a TestSequence: %s"
|
|
% path
|
|
)
|
|
|
|
continue
|
|
|
|
test_sequences.append(
|
|
sequence
|
|
)
|
|
|
|
directory.list_dir_end()
|
|
|
|
test_sequences.sort_custom(
|
|
func(
|
|
a: TestSequence,
|
|
b: TestSequence
|
|
) -> bool:
|
|
|
|
return (
|
|
a.display_name
|
|
< b.display_name
|
|
)
|
|
)
|
|
|
|
simulation_timeline.set_sequences(
|
|
test_sequences
|
|
)
|
|
|
|
if test_sequences.is_empty():
|
|
selected_test_sequence_index = -1
|
|
else:
|
|
selected_test_sequence_index = 0
|
|
|
|
func _on_test_sequence_selected(
|
|
index: int
|
|
) -> void:
|
|
|
|
if (
|
|
index < 0
|
|
or index >= test_sequences.size()
|
|
):
|
|
selected_test_sequence_index = -1
|
|
return
|
|
|
|
selected_test_sequence_index = index
|
|
|
|
|
|
func _on_run_test_sequence_requested() -> void:
|
|
|
|
if (
|
|
selected_test_sequence_index < 0
|
|
or selected_test_sequence_index
|
|
>= test_sequences.size()
|
|
):
|
|
return
|
|
|
|
var sequence := (
|
|
test_sequences[
|
|
selected_test_sequence_index
|
|
]
|
|
)
|
|
|
|
if sequence == null:
|
|
return
|
|
|
|
var current_scenario := (
|
|
context.get_scenario()
|
|
)
|
|
|
|
if current_scenario == null:
|
|
|
|
status_label.text = (
|
|
"Cannot run rehearsal: no scenario is open."
|
|
)
|
|
|
|
return
|
|
|
|
if (
|
|
sequence.scenario_id != &""
|
|
and sequence.scenario_id
|
|
!= current_scenario.scenario_id
|
|
):
|
|
|
|
status_label.text = (
|
|
"Rehearsal '%s' requires scenario '%s'."
|
|
% [
|
|
sequence.display_name,
|
|
sequence.scenario_id
|
|
]
|
|
)
|
|
|
|
push_warning(
|
|
(
|
|
"Cannot run rehearsal '%s': "
|
|
+ "current scenario is '%s', "
|
|
+ "but rehearsal requires '%s'."
|
|
)
|
|
% [
|
|
sequence.display_name,
|
|
current_scenario.scenario_id,
|
|
sequence.scenario_id
|
|
]
|
|
)
|
|
|
|
return
|
|
|
|
_reset_simulation_visuals()
|
|
|
|
simulation_timeline.clear()
|
|
|
|
simulation_driver.start_sequence(
|
|
sequence
|
|
)
|
|
|
|
|
|
func _on_stop_test_sequence_requested() -> void:
|
|
|
|
simulation_driver.stop_sequence()
|
|
|
|
|
|
func _on_replay_test_sequence_requested() -> void:
|
|
|
|
_on_run_test_sequence_requested()
|
|
|
|
|
|
func _on_test_sequence_started(
|
|
_sequence: TestSequence
|
|
) -> void:
|
|
|
|
simulation_timeline.set_sequence_running(
|
|
true
|
|
)
|
|
|
|
|
|
func _on_test_sequence_finished(
|
|
_sequence: TestSequence
|
|
) -> void:
|
|
|
|
simulation_timeline.set_sequence_running(
|
|
false
|
|
)
|
|
|
|
|
|
func _on_test_sequence_stopped() -> void:
|
|
|
|
simulation_timeline.set_sequence_running(
|
|
false
|
|
)
|
|
|
|
func _reset_simulation_visuals() -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
var room := (
|
|
context.get_room()
|
|
)
|
|
|
|
if room == null:
|
|
return
|
|
|
|
room_simulator.configure(
|
|
room
|
|
)
|
|
|
|
for device in room.devices:
|
|
|
|
if device == null:
|
|
continue
|
|
|
|
room_layout.set_device_active(
|
|
device.device_id,
|
|
device.initial_active
|
|
)
|
|
|
|
for binding in room.bindings:
|
|
|
|
if binding == null:
|
|
continue
|
|
|
|
room_layout.set_binding_active(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id,
|
|
false
|
|
)
|
|
|
|
func _on_room_layout_refresh_requested() -> void:
|
|
|
|
if context == null:
|
|
return
|
|
|
|
if context.room_edit_session.is_dirty:
|
|
|
|
status_label.text = (
|
|
"Cannot refresh: save or revert "
|
|
+ "unsaved room changes first."
|
|
)
|
|
|
|
return
|
|
|
|
if not context.room_edit_session.reload_from_disk():
|
|
|
|
status_label.text = (
|
|
"Failed to refresh room."
|
|
)
|
|
|
|
return
|
|
|
|
status_label.text = (
|
|
"Physical room refreshed."
|
|
)
|
|
|
|
|
|
func _setup_view_controls() -> void:
|
|
var toolbar := draw_wall_button.get_parent()
|
|
doorway_tool = Button.new()
|
|
doorway_tool.text = "DRAW DOORWAY"
|
|
doorway_tool.toggle_mode = true
|
|
doorway_tool.button_group = draw_wall_button.button_group
|
|
doorway_tool.tooltip_text = "Drag from hinge to opposite jamb. Align with a wall to create an opening."
|
|
toolbar.add_child(doorway_tool)
|
|
toolbar.move_child(doorway_tool, draw_wall_button.get_index() + 1)
|
|
doorway_tool.pressed.connect(func(): room_layout.set_edit_mode(RoomLayout.EditMode.DRAW_WALL))
|
|
var view_bar := HBoxContainer.new()
|
|
toolbar.get_parent().add_child(view_bar)
|
|
toolbar.get_parent().move_child(view_bar, toolbar.get_index() + 1)
|
|
for title in ["Labels", "Dimensions"]:
|
|
var toggle := CheckBox.new()
|
|
toggle.text = title
|
|
toggle.button_pressed = true
|
|
view_bar.add_child(toggle)
|
|
if title == "Labels":
|
|
toggle.toggled.connect(room_layout.set_labels_visible)
|
|
else:
|
|
toggle.toggled.connect(room_layout.set_dimensions_visible)
|
|
doorway_check = CheckBox.new()
|
|
doorway_check.text = "Doorway opening"
|
|
doorway_check.disabled = true
|
|
doorway_check.toggled.connect(func(value: bool): _commit_door_setting("is_doorway", value))
|
|
swing_check = CheckBox.new()
|
|
swing_check.text = "Reverse door swing"
|
|
swing_check.disabled = true
|
|
swing_check.toggled.connect(func(value: bool): _commit_door_setting("swing_reversed", value))
|
|
var properties := delete_wall_button.get_parent()
|
|
properties.add_child(doorway_check)
|
|
properties.add_child(swing_check)
|
|
properties.move_child(doorway_check, delete_wall_button.get_index())
|
|
properties.move_child(swing_check, delete_wall_button.get_index())
|
|
|
|
func _commit_door_setting(property: String, value: bool) -> void:
|
|
if context == null:
|
|
return
|
|
var target := context.get_room()
|
|
var wall := target.get_wall(room_layout.selected_wall_id)
|
|
if wall == null:
|
|
doorway_check.set_pressed_no_signal(false)
|
|
swing_check.set_pressed_no_signal(false)
|
|
return
|
|
var old: bool = wall.get(property)
|
|
if old == value:
|
|
return
|
|
if undo_redo == null:
|
|
_apply_door_setting(target, wall, property, value)
|
|
return
|
|
undo_redo.create_action("Edit doorway", UndoRedo.MERGE_DISABLE, target)
|
|
undo_redo.add_do_method(self, "_apply_door_setting", target, wall, property, value)
|
|
undo_redo.add_undo_method(self, "_apply_door_setting", target, wall, property, old)
|
|
undo_redo.commit_action()
|
|
|
|
func _apply_door_setting(target: RoomDefinition, wall: RoomWallDefinition, property: String, value: bool) -> void:
|
|
if not target.walls.has(wall):
|
|
return
|
|
wall.set(property, value)
|
|
context.room_edit_session.mark_dirty()
|
|
room_layout.queue_redraw()
|
|
_refresh_wall_property_panel(wall.wall_id)
|