421 lines
6.3 KiB
GDScript
421 lines
6.3 KiB
GDScript
class_name RoomEditSession
|
|
extends RefCounted
|
|
|
|
|
|
signal room_changed(room: RoomDefinition)
|
|
signal dirty_changed(is_dirty: bool)
|
|
signal saved
|
|
signal reverted
|
|
|
|
|
|
var source_room: RoomDefinition
|
|
var working_room: RoomDefinition
|
|
|
|
var is_dirty: bool = false
|
|
|
|
var saved_state: Dictionary = {}
|
|
|
|
|
|
func begin(room: RoomDefinition) -> bool:
|
|
if room == null:
|
|
push_error(
|
|
"RoomEditSession cannot begin with a null room."
|
|
)
|
|
return false
|
|
|
|
source_room = room
|
|
_create_working_copy()
|
|
|
|
return true
|
|
|
|
|
|
func mark_dirty() -> void:
|
|
|
|
if working_room == null:
|
|
_set_dirty(
|
|
false
|
|
)
|
|
return
|
|
|
|
var current_state := (
|
|
_capture_room_state(
|
|
working_room
|
|
)
|
|
)
|
|
|
|
_set_dirty(
|
|
current_state != saved_state
|
|
)
|
|
|
|
|
|
func save() -> bool:
|
|
if source_room == null:
|
|
push_error(
|
|
"RoomEditSession has no source room."
|
|
)
|
|
return false
|
|
|
|
if working_room == null:
|
|
push_error(
|
|
"RoomEditSession has no working room."
|
|
)
|
|
return false
|
|
|
|
_apply_working_copy_to_source()
|
|
|
|
var path := source_room.resource_path
|
|
|
|
if path.is_empty():
|
|
push_error(
|
|
"Source RoomDefinition has no resource path."
|
|
)
|
|
return false
|
|
|
|
var error := ResourceSaver.save(
|
|
source_room,
|
|
path
|
|
)
|
|
|
|
if error != OK:
|
|
push_error(
|
|
"Failed to save RoomDefinition. Error: %s"
|
|
% error
|
|
)
|
|
return false
|
|
|
|
print(
|
|
"[ROOM EDITOR] Saved room to '%s'."
|
|
% path
|
|
)
|
|
|
|
saved_state = (
|
|
_capture_room_state(
|
|
working_room
|
|
)
|
|
)
|
|
|
|
_set_dirty(
|
|
false
|
|
)
|
|
|
|
saved.emit()
|
|
|
|
return true
|
|
|
|
|
|
func revert() -> void:
|
|
|
|
if source_room == null:
|
|
return
|
|
|
|
_create_working_copy()
|
|
|
|
reverted.emit()
|
|
|
|
print(
|
|
"[ROOM EDITOR] Reverted unsaved changes."
|
|
)
|
|
|
|
func reload_from_disk() -> bool:
|
|
|
|
if source_room == null:
|
|
return false
|
|
|
|
var path := (
|
|
source_room.resource_path
|
|
)
|
|
|
|
if path.is_empty():
|
|
push_error(
|
|
"Source RoomDefinition has no resource path."
|
|
)
|
|
return false
|
|
|
|
var reloaded := (
|
|
ResourceLoader.load(
|
|
path,
|
|
"",
|
|
ResourceLoader.CACHE_MODE_REPLACE
|
|
)
|
|
as RoomDefinition
|
|
)
|
|
|
|
if reloaded == null:
|
|
push_error(
|
|
"Failed to reload RoomDefinition from '%s'."
|
|
% path
|
|
)
|
|
return false
|
|
|
|
source_room = reloaded
|
|
|
|
_create_working_copy()
|
|
|
|
print(
|
|
"[ROOM EDITOR] Reloaded room from disk: '%s'."
|
|
% path
|
|
)
|
|
|
|
return true
|
|
|
|
func _create_working_copy() -> void:
|
|
|
|
working_room = (
|
|
_duplicate_room(
|
|
source_room
|
|
)
|
|
)
|
|
|
|
saved_state = (
|
|
_capture_room_state(
|
|
working_room
|
|
)
|
|
)
|
|
|
|
_set_dirty(
|
|
false
|
|
)
|
|
|
|
room_changed.emit(
|
|
working_room
|
|
)
|
|
|
|
func _apply_working_copy_to_source() -> void:
|
|
|
|
for working_placement in working_room.cabinet_placements:
|
|
if working_placement == null:
|
|
continue
|
|
|
|
var source_placement := (
|
|
source_room.get_cabinet_placement(
|
|
working_placement.cabinet_id
|
|
)
|
|
)
|
|
|
|
if source_placement == null:
|
|
continue
|
|
|
|
source_placement.position = (
|
|
working_placement.position
|
|
)
|
|
|
|
source_placement.rotation_degrees = (
|
|
working_placement.rotation_degrees
|
|
)
|
|
|
|
for working_device in working_room.devices:
|
|
if working_device == null:
|
|
continue
|
|
|
|
var source_device := (
|
|
source_room.get_device(
|
|
working_device.device_id
|
|
)
|
|
)
|
|
|
|
if source_device == null:
|
|
continue
|
|
|
|
source_device.position = (
|
|
working_device.position
|
|
)
|
|
|
|
for working_binding in working_room.bindings:
|
|
|
|
if working_binding == null:
|
|
continue
|
|
|
|
var source_binding: RoomDeviceBinding = null
|
|
|
|
for candidate in source_room.bindings:
|
|
|
|
if candidate == null:
|
|
continue
|
|
|
|
if (
|
|
candidate.cabinet_id
|
|
== working_binding.cabinet_id
|
|
and candidate.hardware_action_id
|
|
== working_binding.hardware_action_id
|
|
and candidate.device_id
|
|
== working_binding.device_id
|
|
):
|
|
source_binding = candidate
|
|
break
|
|
|
|
if source_binding == null:
|
|
continue
|
|
|
|
source_binding.route_points = (
|
|
working_binding.route_points.duplicate()
|
|
)
|
|
|
|
#
|
|
# Copy authored room geometry.
|
|
#
|
|
source_room.boundary_points = (
|
|
working_room.boundary_points.duplicate()
|
|
)
|
|
|
|
source_room.walls = _duplicate_walls(
|
|
working_room.walls
|
|
)
|
|
|
|
func _duplicate_room(
|
|
source: RoomDefinition
|
|
) -> RoomDefinition:
|
|
|
|
var copy := (
|
|
source.duplicate(true)
|
|
as RoomDefinition
|
|
)
|
|
|
|
if copy == null:
|
|
return null
|
|
|
|
#
|
|
# Explicitly duplicate authored geometry so
|
|
# wall Resources are never shared with source_room.
|
|
#
|
|
copy.boundary_points = (
|
|
source.boundary_points.duplicate()
|
|
)
|
|
|
|
copy.walls = _duplicate_walls(
|
|
source.walls
|
|
)
|
|
|
|
return copy
|
|
|
|
|
|
func _duplicate_walls(
|
|
source: Array[RoomWallDefinition]
|
|
) -> Array[RoomWallDefinition]:
|
|
|
|
var result: Array[RoomWallDefinition] = []
|
|
|
|
for wall in source:
|
|
if wall == null:
|
|
continue
|
|
|
|
var copy := (
|
|
wall.duplicate(true)
|
|
as RoomWallDefinition
|
|
)
|
|
|
|
if copy != null:
|
|
result.append(
|
|
copy
|
|
)
|
|
|
|
return result
|
|
|
|
func _capture_room_state(
|
|
room: RoomDefinition
|
|
) -> Dictionary:
|
|
|
|
if room == null:
|
|
return {}
|
|
|
|
var state := {
|
|
"room_id": room.room_id,
|
|
"display_name": room.display_name,
|
|
"boundary_points": room.boundary_points.duplicate(),
|
|
"cabinet_placements": [],
|
|
"devices": [],
|
|
"bindings": [],
|
|
"walls": [],
|
|
}
|
|
|
|
for placement in room.cabinet_placements:
|
|
|
|
if placement == null:
|
|
state["cabinet_placements"].append(
|
|
null
|
|
)
|
|
continue
|
|
|
|
state["cabinet_placements"].append(
|
|
{
|
|
"cabinet_id": placement.cabinet_id,
|
|
"position": placement.position,
|
|
"rotation_degrees": (
|
|
placement.rotation_degrees
|
|
),
|
|
}
|
|
)
|
|
|
|
for device in room.devices:
|
|
|
|
if device == null:
|
|
state["devices"].append(
|
|
null
|
|
)
|
|
continue
|
|
|
|
state["devices"].append(
|
|
{
|
|
"device_id": device.device_id,
|
|
"display_name": device.display_name,
|
|
"device_type": device.device_type,
|
|
"behavior": device.behavior,
|
|
"initial_active": (
|
|
device.initial_active
|
|
),
|
|
"position": device.position,
|
|
}
|
|
)
|
|
|
|
for binding in room.bindings:
|
|
|
|
if binding == null:
|
|
state["bindings"].append(
|
|
null
|
|
)
|
|
continue
|
|
|
|
state["bindings"].append(
|
|
{
|
|
"cabinet_id": binding.cabinet_id,
|
|
"hardware_action_id": (
|
|
binding.hardware_action_id
|
|
),
|
|
"device_id": binding.device_id,
|
|
"route_points": (
|
|
binding.route_points.duplicate()
|
|
),
|
|
}
|
|
)
|
|
|
|
for wall in room.walls:
|
|
|
|
if wall == null:
|
|
state["walls"].append(
|
|
null
|
|
)
|
|
continue
|
|
|
|
state["walls"].append(
|
|
{
|
|
"wall_id": wall.wall_id,
|
|
"start": wall.start,
|
|
"end": wall.end,
|
|
"thickness": wall.thickness,
|
|
}
|
|
)
|
|
|
|
return state
|
|
|
|
func _set_dirty(
|
|
value: bool
|
|
) -> void:
|
|
|
|
if is_dirty == value:
|
|
return
|
|
|
|
is_dirty = value
|
|
|
|
dirty_changed.emit(
|
|
is_dirty
|
|
)
|