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, "is_doorway": wall.is_doorway, "swing_reversed": wall.swing_reversed, } ) return state func _set_dirty( value: bool ) -> void: if is_dirty == value: return is_dirty = value dirty_changed.emit( is_dirty )