3990 lines
57 KiB
GDScript
3990 lines
57 KiB
GDScript
@tool
|
|
class_name RoomLayout
|
|
extends Control
|
|
|
|
signal item_selected(
|
|
item_id: StringName
|
|
)
|
|
|
|
signal wall_selected(
|
|
wall_id: StringName
|
|
)
|
|
|
|
signal layout_changed
|
|
|
|
signal wall_geometry_edit_requested(
|
|
wall_id: StringName,
|
|
old_start: Vector2,
|
|
old_end: Vector2,
|
|
new_start: Vector2,
|
|
new_end: Vector2
|
|
)
|
|
|
|
signal boundary_geometry_edit_requested(
|
|
old_points: PackedVector2Array,
|
|
new_points: PackedVector2Array
|
|
)
|
|
|
|
signal edit_rejected(message: String)
|
|
|
|
signal zoom_changed(
|
|
zoom_value: float
|
|
)
|
|
|
|
signal item_position_edit_requested(
|
|
item_id: StringName,
|
|
old_position: Vector2,
|
|
new_position: Vector2
|
|
)
|
|
|
|
signal refresh_requested
|
|
|
|
signal binding_route_edit_requested(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName,
|
|
old_points: PackedVector2Array,
|
|
new_points: PackedVector2Array
|
|
)
|
|
|
|
@export var item_scene: PackedScene
|
|
@export var snap_enabled: bool = true
|
|
|
|
@export_group("View")
|
|
@export_range(10.0, 500.0, 1.0)
|
|
var pixels_per_meter: float = 100.0
|
|
|
|
@export_range(0.01, 2.0, 0.001)
|
|
var grid_size_meters: float = 0.3048
|
|
|
|
var view_offset: Vector2 = Vector2(
|
|
80.0,
|
|
80.0
|
|
)
|
|
|
|
var selected_item_id: StringName = &""
|
|
var selected_wall_id: StringName = &""
|
|
|
|
var room: RoomDefinition
|
|
var scenario: ScenarioDefinition
|
|
|
|
var item_nodes: Dictionary = {}
|
|
|
|
#
|
|
# Runtime/editor simulation activity.
|
|
#
|
|
var active_bindings: Dictionary = {}
|
|
|
|
#
|
|
# Editor selection only.
|
|
#
|
|
var highlighted_bindings: Dictionary = {}
|
|
|
|
var binding_labels: Array[Label] = []
|
|
var show_connections: bool = true
|
|
|
|
var selected_binding_key: String = ""
|
|
|
|
var hovered_binding_route_point: int = -1
|
|
var active_binding_route_point: int = -1
|
|
|
|
var selected_binding_route_point: int = -1
|
|
|
|
var binding_route_drag_original_points := (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
|
|
|
|
enum EditMode {
|
|
SELECT,
|
|
DRAW_WALL,
|
|
EDIT_BOUNDARY,
|
|
ADD_BOUNDARY_POINT,
|
|
DELETE_BOUNDARY_POINT
|
|
}
|
|
|
|
var edit_mode: EditMode = EditMode.SELECT
|
|
|
|
enum WallHandle {
|
|
NONE,
|
|
START,
|
|
END,
|
|
}
|
|
|
|
var active_wall_handle: WallHandle = WallHandle.NONE
|
|
var hovered_wall_handle: WallHandle = WallHandle.NONE
|
|
|
|
var wall_handle_drag_wall_id: StringName = &""
|
|
var wall_handle_drag_original_start: Vector2
|
|
var wall_handle_drag_original_end: Vector2
|
|
|
|
var wall_draw_start: Vector2
|
|
var wall_draw_current: Vector2
|
|
var is_drawing_wall: bool = false
|
|
|
|
var zoom: float = 1.0
|
|
|
|
var is_panning: bool = false
|
|
var pan_last_mouse_position: Vector2
|
|
|
|
var hovered_boundary_vertex: int = -1
|
|
var active_boundary_vertex: int = -1
|
|
|
|
var boundary_drag_original_points: PackedVector2Array = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
var hovered_boundary_edge: int = -1
|
|
|
|
func _ready() -> void:
|
|
focus_mode = Control.FOCUS_ALL
|
|
|
|
func configure(
|
|
room_definition: RoomDefinition,
|
|
scenario_definition: ScenarioDefinition
|
|
) -> void:
|
|
|
|
room = room_definition
|
|
scenario = scenario_definition
|
|
|
|
_rebuild()
|
|
|
|
func _rebuild() -> void:
|
|
for child in get_children():
|
|
child.queue_free()
|
|
|
|
item_nodes.clear()
|
|
binding_labels.clear()
|
|
active_bindings.clear()
|
|
highlighted_bindings.clear()
|
|
|
|
selected_binding_key = ""
|
|
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
selected_binding_route_point = -1
|
|
|
|
binding_route_drag_original_points = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
selected_item_id = &""
|
|
|
|
if room == null:
|
|
return
|
|
|
|
_build_cabinets()
|
|
_build_devices()
|
|
_build_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
func _build_binding_labels() -> void:
|
|
if room == null:
|
|
return
|
|
|
|
for binding in room.bindings:
|
|
if binding == null:
|
|
continue
|
|
|
|
if not item_nodes.has(
|
|
binding.cabinet_id
|
|
):
|
|
continue
|
|
|
|
if not item_nodes.has(
|
|
binding.device_id
|
|
):
|
|
continue
|
|
|
|
var label := Label.new()
|
|
|
|
label.text = String(
|
|
binding.hardware_action_id
|
|
)
|
|
|
|
label.mouse_filter = (
|
|
Control.MOUSE_FILTER_IGNORE
|
|
)
|
|
|
|
add_child(label)
|
|
|
|
binding_labels.append(label)
|
|
|
|
_position_binding_label(
|
|
binding,
|
|
label
|
|
)
|
|
|
|
label.move_to_front()
|
|
|
|
func _position_binding_label(
|
|
binding: RoomDeviceBinding,
|
|
label: Label
|
|
) -> void:
|
|
|
|
var route_points := (
|
|
_get_binding_route_points(
|
|
binding
|
|
)
|
|
)
|
|
|
|
if route_points.size() < 2:
|
|
return
|
|
|
|
var midpoint := (
|
|
_get_polyline_midpoint(
|
|
route_points
|
|
)
|
|
)
|
|
|
|
label.position = (
|
|
midpoint
|
|
- Vector2(
|
|
label.size.x * 0.5,
|
|
18.0
|
|
)
|
|
)
|
|
|
|
func _get_polyline_midpoint(
|
|
points: PackedVector2Array
|
|
) -> Vector2:
|
|
|
|
if points.is_empty():
|
|
return Vector2.ZERO
|
|
|
|
if points.size() == 1:
|
|
return points[0]
|
|
|
|
var total_length: float = 0.0
|
|
|
|
for index in range(
|
|
points.size() - 1
|
|
):
|
|
|
|
total_length += (
|
|
points[index].distance_to(
|
|
points[index + 1]
|
|
)
|
|
)
|
|
|
|
if total_length <= 0.0:
|
|
return points[0]
|
|
|
|
var target_length := (
|
|
total_length * 0.5
|
|
)
|
|
|
|
var travelled: float = 0.0
|
|
|
|
for index in range(
|
|
points.size() - 1
|
|
):
|
|
|
|
var start := points[index]
|
|
var end := points[index + 1]
|
|
|
|
var segment_length := (
|
|
start.distance_to(
|
|
end
|
|
)
|
|
)
|
|
|
|
if (
|
|
travelled + segment_length
|
|
>= target_length
|
|
):
|
|
|
|
if segment_length <= 0.0:
|
|
return start
|
|
|
|
var amount := (
|
|
(
|
|
target_length
|
|
- travelled
|
|
)
|
|
/ segment_length
|
|
)
|
|
|
|
return start.lerp(
|
|
end,
|
|
amount
|
|
)
|
|
|
|
travelled += segment_length
|
|
|
|
return points[
|
|
points.size() - 1
|
|
]
|
|
|
|
func _build_cabinets() -> void:
|
|
if scenario == null:
|
|
return
|
|
|
|
for assignment in scenario.assignments:
|
|
if assignment == null:
|
|
continue
|
|
|
|
if assignment.cabinet == null:
|
|
continue
|
|
|
|
if assignment.game == null:
|
|
continue
|
|
|
|
var placement := room.get_cabinet_placement(
|
|
assignment.cabinet.cabinet_id
|
|
)
|
|
|
|
if placement == null:
|
|
continue
|
|
|
|
var item := _create_item()
|
|
|
|
if item == null:
|
|
continue
|
|
|
|
# IMPORTANT:
|
|
# Add it to the scene tree before calling configure().
|
|
add_child(item)
|
|
|
|
item.configure(
|
|
assignment.cabinet.cabinet_id,
|
|
assignment.cabinet.display_name,
|
|
assignment.game.display_name
|
|
)
|
|
|
|
var footprint_width_pixels := (
|
|
assignment.cabinet.footprint_width_m
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
var footprint_depth_pixels := (
|
|
assignment.cabinet.footprint_depth_m
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
item.configure_cabinet_footprint(
|
|
assignment.cabinet.footprint_width_m
|
|
* pixels_per_meter
|
|
* zoom,
|
|
assignment.cabinet.footprint_depth_m
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
item.configure_editor_texture(
|
|
assignment.cabinet.editor_texture,
|
|
assignment.cabinet.editor_texture_scale
|
|
)
|
|
|
|
var center_screen := (
|
|
world_to_screen(
|
|
placement.position
|
|
)
|
|
)
|
|
|
|
var center_offset := (
|
|
item.get_physical_center_offset()
|
|
)
|
|
|
|
item.position = (
|
|
center_screen
|
|
- center_offset
|
|
)
|
|
|
|
item.pivot_offset = (
|
|
center_offset
|
|
)
|
|
|
|
item.rotation_degrees = (
|
|
placement.rotation_degrees
|
|
)
|
|
|
|
item.moved.connect(
|
|
_on_cabinet_moved
|
|
)
|
|
|
|
item.selected.connect(
|
|
_on_item_selected
|
|
)
|
|
|
|
item_nodes[
|
|
assignment.cabinet.cabinet_id
|
|
] = item
|
|
|
|
item.moving.connect(
|
|
_on_item_moving
|
|
)
|
|
|
|
func _update_binding_labels() -> void:
|
|
if room == null:
|
|
return
|
|
|
|
var label_index := 0
|
|
|
|
for binding in room.bindings:
|
|
if binding == null:
|
|
continue
|
|
|
|
if label_index >= binding_labels.size():
|
|
break
|
|
|
|
var label := binding_labels[
|
|
label_index
|
|
]
|
|
|
|
_position_binding_label(
|
|
binding,
|
|
label
|
|
)
|
|
|
|
label_index += 1
|
|
|
|
func _build_devices() -> void:
|
|
for device in room.devices:
|
|
if device == null:
|
|
continue
|
|
|
|
var item := _create_item()
|
|
|
|
if item == null:
|
|
continue
|
|
|
|
# Same thing here.
|
|
add_child(item)
|
|
|
|
item.configure(
|
|
device.device_id,
|
|
device.display_name,
|
|
_device_type_to_text(
|
|
device.device_type
|
|
)
|
|
)
|
|
|
|
#
|
|
# Device dimensions are authored in meters.
|
|
#
|
|
item.configure_device_footprint(
|
|
device.physical_size_m.x
|
|
* pixels_per_meter
|
|
* zoom,
|
|
device.physical_size_m.y
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
item.configure_editor_texture(
|
|
device.editor_texture,
|
|
device.editor_texture_scale
|
|
)
|
|
|
|
#
|
|
# Device.position currently represents the
|
|
# top-left physical position in room space.
|
|
#
|
|
item.position = world_to_screen(
|
|
device.position
|
|
)
|
|
|
|
item.moved.connect(
|
|
_on_device_moved
|
|
)
|
|
|
|
item.selected.connect(
|
|
_on_item_selected
|
|
)
|
|
|
|
item_nodes[
|
|
device.device_id
|
|
] = item
|
|
|
|
item.moving.connect(
|
|
_on_item_moving
|
|
)
|
|
|
|
func _on_item_selected(
|
|
item_id: StringName
|
|
) -> void:
|
|
|
|
selected_binding_key = ""
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
selected_binding_route_point = -1
|
|
|
|
selected_item_id = item_id
|
|
selected_wall_id = &""
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.set_selected(
|
|
id == selected_item_id
|
|
)
|
|
|
|
item_selected.emit(
|
|
item_id
|
|
)
|
|
|
|
_update_relationship_highlight()
|
|
|
|
func _on_item_moving(
|
|
_item_id: StringName,
|
|
_new_position: Vector2
|
|
) -> void:
|
|
|
|
_update_binding_labels()
|
|
queue_redraw()
|
|
|
|
func _create_item() -> RoomLayoutItem:
|
|
if item_scene == null:
|
|
push_error(
|
|
"RoomLayout has no item_scene assigned."
|
|
)
|
|
return null
|
|
|
|
var instance := item_scene.instantiate()
|
|
|
|
if not instance is RoomLayoutItem:
|
|
push_error(
|
|
"RoomLayout item_scene root must inherit from "
|
|
+ "RoomLayoutItem. Got: %s"
|
|
% instance.get_class()
|
|
)
|
|
|
|
instance.queue_free()
|
|
return null
|
|
|
|
return instance as RoomLayoutItem
|
|
|
|
|
|
func set_device_active(
|
|
device_id: StringName,
|
|
active: bool
|
|
) -> void:
|
|
|
|
if not item_nodes.has(device_id):
|
|
return
|
|
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[device_id]
|
|
)
|
|
|
|
item.set_active(active)
|
|
|
|
|
|
func _on_cabinet_moved(
|
|
cabinet_id: StringName,
|
|
_old_screen_position: Vector2,
|
|
new_screen_position: Vector2
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
cabinet_id
|
|
)
|
|
)
|
|
|
|
if placement == null:
|
|
return
|
|
|
|
var item := (
|
|
item_nodes[
|
|
cabinet_id
|
|
] as RoomLayoutItem
|
|
)
|
|
|
|
var center_offset := (
|
|
item.get_physical_center_offset()
|
|
)
|
|
|
|
var new_center_screen := (
|
|
new_screen_position
|
|
+ center_offset
|
|
)
|
|
|
|
# Preserve the exact stored position for Undo, including off-grid placements.
|
|
var old_world_position := placement.position
|
|
|
|
var new_world_position := (
|
|
screen_to_world(
|
|
new_center_screen
|
|
)
|
|
)
|
|
|
|
new_world_position = (
|
|
snap_item_position_for_editor(
|
|
cabinet_id,
|
|
new_world_position
|
|
)
|
|
)
|
|
|
|
#
|
|
# Restore the visual item to the original position.
|
|
# Workspace will apply the actual change through UndoRedo.
|
|
#
|
|
if item_nodes.has(
|
|
cabinet_id
|
|
):
|
|
var cabinet_item: RoomLayoutItem = (
|
|
item_nodes[
|
|
cabinet_id
|
|
]
|
|
)
|
|
|
|
var restored_center_screen := (
|
|
world_to_screen(
|
|
old_world_position
|
|
)
|
|
)
|
|
|
|
cabinet_item.position = (
|
|
restored_center_screen
|
|
- cabinet_item.get_physical_center_offset()
|
|
)
|
|
|
|
item_position_edit_requested.emit(
|
|
cabinet_id,
|
|
old_world_position,
|
|
new_world_position
|
|
)
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _on_device_moved(
|
|
device_id: StringName,
|
|
old_screen_position: Vector2,
|
|
new_screen_position: Vector2
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var device := (
|
|
room.get_device(
|
|
device_id
|
|
)
|
|
)
|
|
|
|
if device == null:
|
|
return
|
|
|
|
var old_world_position := (
|
|
screen_to_world(
|
|
old_screen_position
|
|
)
|
|
)
|
|
|
|
var new_world_position := (
|
|
screen_to_world(
|
|
new_screen_position
|
|
)
|
|
)
|
|
|
|
old_world_position = (
|
|
_snap_world_position(
|
|
old_world_position
|
|
)
|
|
)
|
|
|
|
new_world_position = (
|
|
_snap_world_position(
|
|
new_world_position
|
|
)
|
|
)
|
|
|
|
if item_nodes.has(
|
|
device_id
|
|
):
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[
|
|
device_id
|
|
]
|
|
)
|
|
|
|
item.position = (
|
|
world_to_screen(
|
|
old_world_position
|
|
)
|
|
)
|
|
|
|
item_position_edit_requested.emit(
|
|
device_id,
|
|
old_world_position,
|
|
new_world_position
|
|
)
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
func get_selected_position() -> Vector2:
|
|
if selected_item_id == &"":
|
|
return Vector2.ZERO
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
selected_item_id
|
|
)
|
|
)
|
|
|
|
if placement != null:
|
|
return placement.position
|
|
|
|
var device := room.get_device(
|
|
selected_item_id
|
|
)
|
|
|
|
if device != null:
|
|
return device.position
|
|
|
|
return Vector2.ZERO
|
|
|
|
func get_selected_rotation() -> float:
|
|
if selected_item_id == &"":
|
|
return 0.0
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
selected_item_id
|
|
)
|
|
)
|
|
|
|
if placement != null:
|
|
return placement.rotation_degrees
|
|
|
|
return 0.0
|
|
|
|
func selected_item_is_cabinet() -> bool:
|
|
if room == null:
|
|
return false
|
|
|
|
return (
|
|
room.get_cabinet_placement(
|
|
selected_item_id
|
|
)
|
|
!= null
|
|
)
|
|
|
|
func set_selected_position(
|
|
new_position: Vector2
|
|
) -> void:
|
|
|
|
if selected_item_id == &"":
|
|
return
|
|
|
|
var final_position := (
|
|
snap_item_position_for_editor(
|
|
selected_item_id,
|
|
new_position
|
|
)
|
|
)
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
selected_item_id
|
|
)
|
|
)
|
|
|
|
if placement != null:
|
|
|
|
placement.position = (
|
|
final_position
|
|
)
|
|
|
|
else:
|
|
|
|
var device := (
|
|
room.get_device(
|
|
selected_item_id
|
|
)
|
|
)
|
|
|
|
if device == null:
|
|
return
|
|
|
|
device.position = (
|
|
final_position
|
|
)
|
|
|
|
if item_nodes.has(
|
|
selected_item_id
|
|
):
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[
|
|
selected_item_id
|
|
]
|
|
)
|
|
|
|
if placement != null:
|
|
|
|
var center_screen := (
|
|
world_to_screen(
|
|
final_position
|
|
)
|
|
)
|
|
|
|
item.position = (
|
|
center_screen
|
|
- item.get_physical_center_offset()
|
|
)
|
|
|
|
else:
|
|
|
|
item.position = (
|
|
world_to_screen(
|
|
final_position
|
|
)
|
|
)
|
|
|
|
layout_changed.emit()
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
func set_selected_rotation(
|
|
new_rotation: float
|
|
) -> void:
|
|
|
|
if selected_item_id == &"":
|
|
return
|
|
|
|
var placement := (
|
|
room.get_cabinet_placement(
|
|
selected_item_id
|
|
)
|
|
)
|
|
|
|
if placement == null:
|
|
return
|
|
|
|
placement.rotation_degrees = (
|
|
new_rotation
|
|
)
|
|
|
|
if item_nodes.has(
|
|
selected_item_id
|
|
):
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[
|
|
selected_item_id
|
|
]
|
|
)
|
|
|
|
item.rotation_degrees = (
|
|
new_rotation
|
|
)
|
|
|
|
layout_changed.emit()
|
|
|
|
func _device_type_to_text(
|
|
device_type: RoomDeviceDefinition.DeviceType
|
|
) -> String:
|
|
|
|
match device_type:
|
|
|
|
RoomDeviceDefinition.DeviceType.COIN_RELEASE:
|
|
return "Coin Release"
|
|
|
|
RoomDeviceDefinition.DeviceType.LIGHT:
|
|
return "Light"
|
|
|
|
RoomDeviceDefinition.DeviceType.DOOR_LOCK:
|
|
return "Door Lock"
|
|
|
|
RoomDeviceDefinition.DeviceType.DRAWER_LOCK:
|
|
return "Drawer Lock"
|
|
|
|
RoomDeviceDefinition.DeviceType.BUZZER:
|
|
return "Buzzer"
|
|
|
|
_:
|
|
return "Device"
|
|
|
|
func _make_binding_key(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> String:
|
|
|
|
return "%s::%s::%s" % [
|
|
cabinet_id,
|
|
action_id,
|
|
device_id
|
|
]
|
|
|
|
func set_binding_active(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName,
|
|
active: bool
|
|
) -> void:
|
|
|
|
var key := _make_binding_key(
|
|
cabinet_id,
|
|
action_id,
|
|
device_id
|
|
)
|
|
|
|
if active:
|
|
active_bindings[key] = true
|
|
else:
|
|
active_bindings.erase(key)
|
|
|
|
queue_redraw()
|
|
|
|
func _draw() -> void:
|
|
|
|
_draw_grid()
|
|
_draw_room_boundary()
|
|
_draw_walls()
|
|
|
|
if not show_connections:
|
|
return
|
|
|
|
if room == null:
|
|
return
|
|
|
|
for binding in room.bindings:
|
|
if binding == null:
|
|
continue
|
|
|
|
_draw_binding(
|
|
binding
|
|
)
|
|
|
|
if is_drawing_wall:
|
|
_draw_wall_preview()
|
|
|
|
func set_connections_visible(
|
|
visible: bool
|
|
) -> void:
|
|
|
|
show_connections = visible
|
|
|
|
for label in binding_labels:
|
|
label.visible = visible
|
|
|
|
queue_redraw()
|
|
|
|
func _draw_binding(
|
|
binding: RoomDeviceBinding
|
|
) -> void:
|
|
|
|
var route_points := (
|
|
_get_binding_route_points(
|
|
binding
|
|
)
|
|
)
|
|
|
|
if route_points.size() < 2:
|
|
return
|
|
|
|
var key := _make_binding_key(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id
|
|
)
|
|
|
|
var is_active := active_bindings.has(
|
|
key
|
|
)
|
|
|
|
var is_highlighted := (
|
|
highlighted_bindings.has(
|
|
key
|
|
)
|
|
)
|
|
|
|
var is_selected := (
|
|
key == selected_binding_key
|
|
)
|
|
|
|
var line_color := Color(
|
|
0.45,
|
|
0.45,
|
|
0.45,
|
|
0.8
|
|
)
|
|
|
|
var line_width: float = 2.0
|
|
|
|
#
|
|
# Editor selection.
|
|
#
|
|
if (
|
|
is_highlighted
|
|
or is_selected
|
|
):
|
|
|
|
line_color = Color(
|
|
0.35,
|
|
0.7,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
line_width = 4.0
|
|
|
|
#
|
|
# Live simulation always wins visually.
|
|
#
|
|
if is_active:
|
|
|
|
line_color = Color(
|
|
1.0,
|
|
0.8,
|
|
0.2,
|
|
1.0
|
|
)
|
|
|
|
line_width = 6.0
|
|
|
|
#
|
|
# Draw every segment of the authored cable route.
|
|
#
|
|
for index in range(
|
|
route_points.size() - 1
|
|
):
|
|
|
|
draw_line(
|
|
route_points[index],
|
|
route_points[index + 1],
|
|
line_color,
|
|
line_width,
|
|
true
|
|
)
|
|
|
|
#
|
|
# Keep the endpoint marker on the device end.
|
|
#
|
|
var end_position := (
|
|
route_points[
|
|
route_points.size() - 1
|
|
]
|
|
)
|
|
|
|
_draw_connection_endpoint(
|
|
end_position,
|
|
line_color,
|
|
is_active
|
|
or is_highlighted
|
|
or is_selected
|
|
)
|
|
|
|
if is_selected:
|
|
_draw_binding_route_handles(
|
|
binding
|
|
)
|
|
|
|
func _draw_binding_route_handles(
|
|
binding: RoomDeviceBinding
|
|
) -> void:
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
for index in range(
|
|
binding.route_points.size()
|
|
):
|
|
|
|
var screen_position := (
|
|
world_to_screen(
|
|
binding.route_points[index]
|
|
)
|
|
)
|
|
|
|
var radius: float = 7.0
|
|
|
|
var color := Color(
|
|
0.35,
|
|
0.7,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
if index == hovered_binding_route_point:
|
|
|
|
radius = 10.0
|
|
|
|
color = Color(
|
|
0.65,
|
|
0.85,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
if index == selected_binding_route_point:
|
|
|
|
radius = 9.0
|
|
|
|
color = Color(
|
|
1.0,
|
|
1.0,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
if index == active_binding_route_point:
|
|
|
|
radius = 11.0
|
|
|
|
color = Color(
|
|
1.0,
|
|
0.7,
|
|
0.2,
|
|
1.0
|
|
)
|
|
|
|
draw_circle(
|
|
screen_position,
|
|
radius,
|
|
color
|
|
)
|
|
|
|
draw_circle(
|
|
screen_position,
|
|
radius,
|
|
Color.WHITE,
|
|
false,
|
|
2.0
|
|
)
|
|
|
|
func _get_binding_route_points(
|
|
binding: RoomDeviceBinding
|
|
) -> PackedVector2Array:
|
|
|
|
var points := PackedVector2Array()
|
|
|
|
if binding == null:
|
|
return points
|
|
|
|
if not item_nodes.has(
|
|
binding.cabinet_id
|
|
):
|
|
return points
|
|
|
|
if not item_nodes.has(
|
|
binding.device_id
|
|
):
|
|
return points
|
|
|
|
var cabinet_item: RoomLayoutItem = (
|
|
item_nodes[
|
|
binding.cabinet_id
|
|
]
|
|
)
|
|
|
|
var device_item: RoomLayoutItem = (
|
|
item_nodes[
|
|
binding.device_id
|
|
]
|
|
)
|
|
|
|
#
|
|
# Start at the visual center of the cabinet.
|
|
#
|
|
var start_position := (
|
|
cabinet_item.position
|
|
+ cabinet_item.get_physical_center_offset()
|
|
)
|
|
|
|
points.append(
|
|
start_position
|
|
)
|
|
|
|
#
|
|
# Authored route points are stored in room/world meters,
|
|
# so convert them to screen coordinates here.
|
|
#
|
|
for room_point in binding.route_points:
|
|
|
|
points.append(
|
|
world_to_screen(
|
|
room_point
|
|
)
|
|
)
|
|
|
|
#
|
|
# Finish at the visual center of the device.
|
|
#
|
|
var end_position := (
|
|
device_item.position
|
|
+ device_item.get_physical_center_offset()
|
|
)
|
|
|
|
points.append(
|
|
end_position
|
|
)
|
|
|
|
return points
|
|
|
|
func _get_selected_binding() -> RoomDeviceBinding:
|
|
|
|
if room == null:
|
|
return null
|
|
|
|
if selected_binding_key.is_empty():
|
|
return null
|
|
|
|
for binding in room.bindings:
|
|
|
|
if binding == null:
|
|
continue
|
|
|
|
var key := _make_binding_key(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id
|
|
)
|
|
|
|
if key == selected_binding_key:
|
|
return binding
|
|
|
|
return null
|
|
|
|
|
|
func _find_binding_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> RoomDeviceBinding:
|
|
|
|
if room == null:
|
|
return null
|
|
|
|
if not show_connections:
|
|
return null
|
|
|
|
var hit_radius: float = 10.0
|
|
|
|
for binding in room.bindings:
|
|
|
|
if binding == null:
|
|
continue
|
|
|
|
var route_points := (
|
|
_get_binding_route_points(
|
|
binding
|
|
)
|
|
)
|
|
|
|
if route_points.size() < 2:
|
|
continue
|
|
|
|
for index in range(
|
|
route_points.size() - 1
|
|
):
|
|
|
|
var closest := (
|
|
Geometry2D.get_closest_point_to_segment(
|
|
screen_position,
|
|
route_points[index],
|
|
route_points[index + 1]
|
|
)
|
|
)
|
|
|
|
if (
|
|
closest.distance_to(
|
|
screen_position
|
|
)
|
|
<= hit_radius
|
|
):
|
|
return binding
|
|
|
|
return null
|
|
|
|
|
|
func _select_binding(
|
|
binding: RoomDeviceBinding
|
|
) -> void:
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
selected_binding_key = (
|
|
_make_binding_key(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id
|
|
)
|
|
)
|
|
|
|
selected_item_id = &""
|
|
selected_wall_id = &""
|
|
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
selected_binding_route_point = -1
|
|
|
|
for id in item_nodes:
|
|
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.set_selected(
|
|
false
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _find_binding_route_point_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> int:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding == null:
|
|
return -1
|
|
|
|
var best_index: int = -1
|
|
var best_distance: float = 12.0
|
|
|
|
for index in range(
|
|
binding.route_points.size()
|
|
):
|
|
|
|
var point_screen := (
|
|
world_to_screen(
|
|
binding.route_points[index]
|
|
)
|
|
)
|
|
|
|
var distance := (
|
|
point_screen.distance_to(
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
if distance <= best_distance:
|
|
|
|
best_distance = distance
|
|
best_index = index
|
|
|
|
return best_index
|
|
|
|
func _begin_binding_route_point_drag(
|
|
point_index: int
|
|
) -> void:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
if (
|
|
point_index < 0
|
|
or point_index >= binding.route_points.size()
|
|
):
|
|
return
|
|
|
|
active_binding_route_point = (
|
|
point_index
|
|
)
|
|
|
|
selected_binding_route_point = (
|
|
point_index
|
|
)
|
|
|
|
binding_route_drag_original_points = (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _update_binding_route_point_drag(
|
|
screen_position: Vector2
|
|
) -> void:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
if active_binding_route_point < 0:
|
|
return
|
|
|
|
if (
|
|
active_binding_route_point
|
|
>= binding.route_points.size()
|
|
):
|
|
return
|
|
|
|
var world_position := (
|
|
screen_to_world(
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
var snapped_position := (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
var points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
points[
|
|
active_binding_route_point
|
|
] = snapped_position
|
|
|
|
binding.route_points = points
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _finish_binding_route_point_drag() -> void:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding == null:
|
|
_cancel_binding_route_point_drag()
|
|
return
|
|
|
|
if active_binding_route_point < 0:
|
|
return
|
|
|
|
var new_points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
#
|
|
# Restore the original model before handing
|
|
# the completed change to Workspace UndoRedo.
|
|
#
|
|
binding.route_points = (
|
|
binding_route_drag_original_points.duplicate()
|
|
)
|
|
|
|
var old_points := (
|
|
binding_route_drag_original_points.duplicate()
|
|
)
|
|
|
|
binding_route_edit_requested.emit(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id,
|
|
old_points,
|
|
new_points
|
|
)
|
|
|
|
active_binding_route_point = -1
|
|
|
|
binding_route_drag_original_points = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _cancel_binding_route_point_drag() -> void:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding != null:
|
|
|
|
binding.route_points = (
|
|
binding_route_drag_original_points.duplicate()
|
|
)
|
|
|
|
active_binding_route_point = -1
|
|
|
|
binding_route_drag_original_points = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
func _draw_connection_endpoint(
|
|
position: Vector2,
|
|
color: Color,
|
|
is_active: bool
|
|
) -> void:
|
|
|
|
var radius := 5.0
|
|
|
|
if is_active:
|
|
radius = 9.0
|
|
|
|
draw_circle(
|
|
position,
|
|
radius,
|
|
color
|
|
)
|
|
|
|
func _request_selected_binding_route_point_delete() -> void:
|
|
|
|
var binding := (
|
|
_get_selected_binding()
|
|
)
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
if selected_binding_route_point < 0:
|
|
return
|
|
|
|
if (
|
|
selected_binding_route_point
|
|
>= binding.route_points.size()
|
|
):
|
|
return
|
|
|
|
var old_points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
var new_points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
new_points.remove_at(
|
|
selected_binding_route_point
|
|
)
|
|
|
|
binding_route_edit_requested.emit(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id,
|
|
old_points,
|
|
new_points
|
|
)
|
|
|
|
selected_binding_route_point = -1
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
queue_redraw()
|
|
|
|
func _update_relationship_highlight() -> void:
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.modulate = Color.WHITE
|
|
|
|
if selected_item_id == &"":
|
|
return
|
|
|
|
var related_ids: Dictionary = {}
|
|
related_ids[selected_item_id] = true
|
|
|
|
for binding in room.bindings:
|
|
if binding == null:
|
|
continue
|
|
|
|
if (
|
|
binding.cabinet_id
|
|
== selected_item_id
|
|
):
|
|
related_ids[
|
|
binding.device_id
|
|
] = true
|
|
|
|
elif (
|
|
binding.device_id
|
|
== selected_item_id
|
|
):
|
|
related_ids[
|
|
binding.cabinet_id
|
|
] = true
|
|
|
|
for id in item_nodes:
|
|
|
|
if related_ids.has(id):
|
|
continue
|
|
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.modulate = Color(
|
|
0.55,
|
|
0.55,
|
|
0.55,
|
|
1.0
|
|
)
|
|
|
|
func snap_position_for_editor(
|
|
position: Vector2
|
|
) -> Vector2:
|
|
|
|
return _snap_world_position(
|
|
position
|
|
)
|
|
|
|
func world_to_screen(
|
|
world_position: Vector2
|
|
) -> Vector2:
|
|
|
|
return (
|
|
world_position
|
|
* pixels_per_meter
|
|
* zoom
|
|
+ view_offset
|
|
)
|
|
|
|
func screen_to_world(
|
|
screen_position: Vector2
|
|
) -> Vector2:
|
|
|
|
return (
|
|
screen_position
|
|
- view_offset
|
|
) / (
|
|
pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
func _meters_to_feet_inches(
|
|
meters: float
|
|
) -> String:
|
|
|
|
var total_inches := roundi(
|
|
absf(meters) / 0.0254
|
|
)
|
|
|
|
var feet := (
|
|
total_inches / 12
|
|
)
|
|
|
|
var inches := (
|
|
total_inches % 12
|
|
)
|
|
|
|
if inches == 0:
|
|
return "%d'" % feet
|
|
|
|
if feet == 0:
|
|
return "%d\"" % inches
|
|
|
|
return "%d' %d\"" % [
|
|
feet,
|
|
inches
|
|
]
|
|
|
|
|
|
func _draw_dimension_label(
|
|
screen_position: Vector2,
|
|
text: String
|
|
) -> void:
|
|
|
|
if text.is_empty():
|
|
return
|
|
|
|
var font := (
|
|
get_theme_default_font()
|
|
)
|
|
|
|
var font_size := 14
|
|
|
|
var text_size := (
|
|
font.get_string_size(
|
|
text,
|
|
HORIZONTAL_ALIGNMENT_LEFT,
|
|
-1,
|
|
font_size
|
|
)
|
|
)
|
|
|
|
var padding := Vector2(
|
|
6.0,
|
|
3.0
|
|
)
|
|
|
|
var background_rect := Rect2(
|
|
screen_position
|
|
- text_size * 0.5
|
|
- padding,
|
|
text_size
|
|
+ padding * 2.0
|
|
)
|
|
|
|
draw_rect(
|
|
background_rect,
|
|
Color(
|
|
0.08,
|
|
0.08,
|
|
0.08,
|
|
0.90
|
|
),
|
|
true
|
|
)
|
|
|
|
draw_string(
|
|
font,
|
|
screen_position
|
|
- Vector2(
|
|
text_size.x * 0.5,
|
|
-text_size.y * 0.25
|
|
),
|
|
text,
|
|
HORIZONTAL_ALIGNMENT_LEFT,
|
|
-1,
|
|
font_size,
|
|
Color(
|
|
0.95,
|
|
0.95,
|
|
0.95,
|
|
1.0
|
|
)
|
|
)
|
|
|
|
func snap_item_position_for_editor(
|
|
item_id: StringName,
|
|
position: Vector2
|
|
) -> Vector2:
|
|
|
|
if not snap_enabled or room == null:
|
|
return position
|
|
|
|
var placement := room.get_cabinet_placement(item_id)
|
|
var cabinet := _get_cabinet_profile(item_id)
|
|
if placement == null or cabinet == null:
|
|
return _snap_world_position(position)
|
|
|
|
# Positions remain centers in meters. Snap the rotated footprint's
|
|
# minimum corner, then restore its center offset.
|
|
var angle := deg_to_rad(placement.rotation_degrees)
|
|
var half_size := Vector2(
|
|
cabinet.footprint_width_m,
|
|
cabinet.footprint_depth_m
|
|
) * 0.5
|
|
var half_bounds := Vector2(
|
|
abs(cos(angle)) * half_size.x + abs(sin(angle)) * half_size.y,
|
|
abs(sin(angle)) * half_size.x + abs(cos(angle)) * half_size.y
|
|
)
|
|
return _snap_world_position(position - half_bounds) + half_bounds
|
|
|
|
func _snap_world_position(
|
|
position: Vector2
|
|
) -> Vector2:
|
|
|
|
if not snap_enabled:
|
|
return position
|
|
|
|
return Vector2(
|
|
round(
|
|
position.x / grid_size_meters
|
|
) * grid_size_meters,
|
|
|
|
round(
|
|
position.y / grid_size_meters
|
|
) * grid_size_meters
|
|
)
|
|
|
|
func _draw_room_boundary() -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
if room.boundary_points.size() < 2:
|
|
return
|
|
|
|
#
|
|
# Approximate visual center. We use this only to decide
|
|
# which side of each wall is "outside" for the dimension.
|
|
#
|
|
var boundary_center := Vector2.ZERO
|
|
|
|
for point in room.boundary_points:
|
|
boundary_center += world_to_screen(
|
|
point
|
|
)
|
|
|
|
boundary_center /= float(
|
|
room.boundary_points.size()
|
|
)
|
|
|
|
for index in range(
|
|
room.boundary_points.size()
|
|
):
|
|
|
|
var next_index := (
|
|
(index + 1)
|
|
% room.boundary_points.size()
|
|
)
|
|
|
|
var start_world := (
|
|
room.boundary_points[index]
|
|
)
|
|
|
|
var end_world := (
|
|
room.boundary_points[next_index]
|
|
)
|
|
|
|
var start := world_to_screen(
|
|
start_world
|
|
)
|
|
|
|
var end := world_to_screen(
|
|
end_world
|
|
)
|
|
|
|
var line_color := Color(
|
|
0.85,
|
|
0.85,
|
|
0.85,
|
|
1.0
|
|
)
|
|
|
|
var line_width := 3.0
|
|
|
|
if (
|
|
edit_mode == EditMode.ADD_BOUNDARY_POINT
|
|
and index == hovered_boundary_edge
|
|
):
|
|
|
|
line_color = Color(
|
|
0.35,
|
|
0.7,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
line_width = 6.0
|
|
|
|
draw_line(
|
|
start,
|
|
end,
|
|
line_color,
|
|
line_width,
|
|
true
|
|
)
|
|
|
|
#
|
|
# Boundary dimension.
|
|
#
|
|
var edge_length_m := (
|
|
start_world.distance_to(
|
|
end_world
|
|
)
|
|
)
|
|
|
|
var midpoint := (
|
|
(start + end) * 0.5
|
|
)
|
|
|
|
var direction := (
|
|
end - start
|
|
)
|
|
|
|
if direction.length() > 0.001:
|
|
|
|
var normal := Vector2(
|
|
-direction.y,
|
|
direction.x
|
|
).normalized()
|
|
|
|
var offset_distance := 28.0
|
|
|
|
var candidate_a := (
|
|
midpoint
|
|
+ normal * offset_distance
|
|
)
|
|
|
|
var candidate_b := (
|
|
midpoint
|
|
- normal * offset_distance
|
|
)
|
|
|
|
var label_position := candidate_a
|
|
|
|
#
|
|
# Choose whichever candidate is farther
|
|
# from the room's center.
|
|
#
|
|
if (
|
|
candidate_b.distance_to(
|
|
boundary_center
|
|
)
|
|
>
|
|
candidate_a.distance_to(
|
|
boundary_center
|
|
)
|
|
):
|
|
label_position = candidate_b
|
|
|
|
_draw_dimension_label(
|
|
label_position,
|
|
_meters_to_feet_inches(
|
|
edge_length_m
|
|
)
|
|
)
|
|
|
|
#
|
|
# Existing boundary editing handles.
|
|
#
|
|
if (
|
|
edit_mode != EditMode.EDIT_BOUNDARY
|
|
and edit_mode != EditMode.ADD_BOUNDARY_POINT
|
|
and edit_mode != EditMode.DELETE_BOUNDARY_POINT
|
|
):
|
|
return
|
|
|
|
for index in range(
|
|
room.boundary_points.size()
|
|
):
|
|
|
|
var screen_position := world_to_screen(
|
|
room.boundary_points[index]
|
|
)
|
|
|
|
var radius := 6.0
|
|
|
|
var color := Color(
|
|
0.85,
|
|
0.85,
|
|
0.85,
|
|
1.0
|
|
)
|
|
|
|
if index == hovered_boundary_vertex:
|
|
|
|
radius = 9.0
|
|
|
|
color = Color(
|
|
0.35,
|
|
0.7,
|
|
1.0,
|
|
1.0
|
|
)
|
|
|
|
if index == active_boundary_vertex:
|
|
|
|
radius = 10.0
|
|
|
|
color = Color(
|
|
1.0,
|
|
0.7,
|
|
0.2,
|
|
1.0
|
|
)
|
|
|
|
draw_circle(
|
|
screen_position,
|
|
radius,
|
|
color
|
|
)
|
|
|
|
func _draw_grid() -> void:
|
|
|
|
if grid_size_meters <= 0.0:
|
|
return
|
|
|
|
var spacing := (
|
|
grid_size_meters
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
if spacing < 8.0:
|
|
return
|
|
|
|
var viewport_size := size
|
|
|
|
var start_x := fmod(
|
|
view_offset.x,
|
|
spacing
|
|
)
|
|
|
|
var start_y := fmod(
|
|
view_offset.y,
|
|
spacing
|
|
)
|
|
|
|
var grid_color := Color(
|
|
0.25,
|
|
0.25,
|
|
0.25,
|
|
0.35
|
|
)
|
|
|
|
var x := start_x
|
|
|
|
while x < viewport_size.x:
|
|
draw_line(
|
|
Vector2(
|
|
x,
|
|
0.0
|
|
),
|
|
Vector2(
|
|
x,
|
|
viewport_size.y
|
|
),
|
|
grid_color,
|
|
1.0
|
|
)
|
|
|
|
x += spacing
|
|
|
|
var y := start_y
|
|
|
|
while y < viewport_size.y:
|
|
draw_line(
|
|
Vector2(
|
|
0.0,
|
|
y
|
|
),
|
|
Vector2(
|
|
viewport_size.x,
|
|
y
|
|
),
|
|
grid_color,
|
|
1.0
|
|
)
|
|
|
|
y += spacing
|
|
|
|
func _draw_walls() -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
for wall in room.walls:
|
|
if wall == null:
|
|
continue
|
|
|
|
var start := world_to_screen(
|
|
wall.start
|
|
)
|
|
|
|
var end := world_to_screen(
|
|
wall.end
|
|
)
|
|
|
|
var width: float = maxf(
|
|
wall.thickness
|
|
* pixels_per_meter
|
|
* zoom,
|
|
2.0
|
|
)
|
|
|
|
var wall_color := Color(
|
|
0.75,
|
|
0.75,
|
|
0.75,
|
|
1.0
|
|
)
|
|
|
|
if wall.wall_id == selected_wall_id:
|
|
wall_color = Color(
|
|
1.0,
|
|
0.65,
|
|
0.15,
|
|
1.0
|
|
)
|
|
|
|
width += 3.0
|
|
|
|
draw_line(
|
|
start,
|
|
end,
|
|
wall_color,
|
|
width,
|
|
true
|
|
)
|
|
|
|
#
|
|
# Wall dimension.
|
|
#
|
|
var wall_length_m := (
|
|
wall.start.distance_to(
|
|
wall.end
|
|
)
|
|
)
|
|
|
|
var midpoint := (
|
|
(start + end) * 0.5
|
|
)
|
|
|
|
var direction := (
|
|
end - start
|
|
)
|
|
|
|
var label_position := (
|
|
midpoint
|
|
+ Vector2(
|
|
0.0,
|
|
-18.0
|
|
)
|
|
)
|
|
|
|
if direction.length() > 0.001:
|
|
|
|
var normal := Vector2(
|
|
-direction.y,
|
|
direction.x
|
|
).normalized()
|
|
|
|
#
|
|
# Keep the label beside the wall instead
|
|
# of directly over the line.
|
|
#
|
|
if normal.y > 0.0:
|
|
normal = -normal
|
|
|
|
label_position = (
|
|
midpoint
|
|
+ normal * 18.0
|
|
)
|
|
|
|
_draw_dimension_label(
|
|
label_position,
|
|
_meters_to_feet_inches(
|
|
wall_length_m
|
|
)
|
|
)
|
|
|
|
if wall.wall_id == selected_wall_id:
|
|
|
|
var start_radius: float = 7.0
|
|
var end_radius: float = 7.0
|
|
|
|
if hovered_wall_handle == WallHandle.START:
|
|
start_radius = 10.0
|
|
|
|
if hovered_wall_handle == WallHandle.END:
|
|
end_radius = 10.0
|
|
|
|
if active_wall_handle == WallHandle.START:
|
|
start_radius = 11.0
|
|
|
|
if active_wall_handle == WallHandle.END:
|
|
end_radius = 11.0
|
|
|
|
draw_circle(
|
|
start,
|
|
start_radius,
|
|
Color.WHITE
|
|
)
|
|
|
|
draw_circle(
|
|
end,
|
|
end_radius,
|
|
Color.WHITE
|
|
)
|
|
|
|
func set_edit_mode(
|
|
mode: EditMode
|
|
) -> void:
|
|
|
|
edit_mode = mode
|
|
|
|
hovered_boundary_vertex = -1
|
|
hovered_boundary_edge = -1
|
|
|
|
if active_boundary_vertex >= 0:
|
|
_cancel_boundary_vertex_drag()
|
|
|
|
_update_boundary_add_cursor()
|
|
|
|
queue_redraw()
|
|
|
|
signal wall_creation_requested(
|
|
start: Vector2,
|
|
end: Vector2
|
|
)
|
|
|
|
func _gui_input(
|
|
event: InputEvent
|
|
) -> void:
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.pressed
|
|
):
|
|
grab_focus()
|
|
|
|
if (
|
|
event is InputEventKey
|
|
and event.pressed
|
|
and not event.echo
|
|
):
|
|
if event.keycode == KEY_F:
|
|
focus_room()
|
|
accept_event()
|
|
return
|
|
|
|
if event.keycode == KEY_0:
|
|
reset_view()
|
|
accept_event()
|
|
return
|
|
|
|
if event.keycode == KEY_R:
|
|
|
|
refresh_requested.emit()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
if (
|
|
event.keycode == KEY_DELETE
|
|
or event.keycode == KEY_BACKSPACE
|
|
):
|
|
|
|
if (
|
|
edit_mode == EditMode.SELECT
|
|
and selected_binding_route_point >= 0
|
|
):
|
|
|
|
_request_selected_binding_route_point_delete()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
#
|
|
# Middle-mouse pan.
|
|
#
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_MIDDLE
|
|
):
|
|
if event.pressed:
|
|
_begin_pan(
|
|
event.position
|
|
)
|
|
else:
|
|
_end_pan()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
if (
|
|
event is InputEventMouseMotion
|
|
and is_panning
|
|
):
|
|
_update_pan(
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
#
|
|
# Mouse-wheel zoom.
|
|
#
|
|
if event is InputEventMouseButton:
|
|
if (
|
|
event.button_index == MOUSE_BUTTON_WHEEL_UP
|
|
or event.button_index == MOUSE_BUTTON_WHEEL_DOWN
|
|
):
|
|
_handle_zoom(
|
|
event
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
#
|
|
# SELECT mode
|
|
#
|
|
if edit_mode == EditMode.SELECT:
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_LEFT
|
|
):
|
|
|
|
if event.pressed:
|
|
|
|
if event.double_click:
|
|
|
|
var double_click_binding := (
|
|
_find_binding_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if double_click_binding != null:
|
|
|
|
_request_binding_route_point_insert(
|
|
double_click_binding,
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
var route_point_index := (
|
|
_find_binding_route_point_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if route_point_index >= 0:
|
|
|
|
_begin_binding_route_point_drag(
|
|
route_point_index
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
|
|
var binding := (
|
|
_find_binding_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if binding != null:
|
|
|
|
_select_binding(
|
|
binding
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
var handle := (
|
|
_find_wall_handle_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if handle != WallHandle.NONE:
|
|
_begin_wall_handle_drag(
|
|
handle
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
var wall := (
|
|
_find_wall_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if wall != null:
|
|
_select_wall(
|
|
wall.wall_id
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
else:
|
|
|
|
if active_binding_route_point >= 0:
|
|
|
|
_finish_binding_route_point_drag()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
if (
|
|
active_wall_handle
|
|
!= WallHandle.NONE
|
|
):
|
|
_finish_wall_handle_drag()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
if active_binding_route_point >= 0:
|
|
|
|
_update_binding_route_point_drag(
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
if active_wall_handle != WallHandle.NONE:
|
|
_update_wall_handle_drag(
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
var new_route_hover := (
|
|
_find_binding_route_point_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if (
|
|
new_route_hover
|
|
!= hovered_binding_route_point
|
|
):
|
|
|
|
hovered_binding_route_point = (
|
|
new_route_hover
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
var new_hover := (
|
|
_find_wall_handle_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if new_hover != hovered_wall_handle:
|
|
hovered_wall_handle = new_hover
|
|
queue_redraw()
|
|
|
|
return
|
|
|
|
#
|
|
# ADD BOUNDARY POINT mode
|
|
#
|
|
if edit_mode == EditMode.ADD_BOUNDARY_POINT:
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_LEFT
|
|
and event.pressed
|
|
):
|
|
var edge_index := (
|
|
_find_boundary_edge_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if edge_index >= 0:
|
|
_request_boundary_point_insert(
|
|
edge_index,
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
var new_hover_edge := (
|
|
_find_boundary_edge_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
var new_hover_vertex := (
|
|
_find_boundary_vertex_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if (
|
|
new_hover_edge != hovered_boundary_edge
|
|
or new_hover_vertex != hovered_boundary_vertex
|
|
):
|
|
hovered_boundary_edge = (
|
|
new_hover_edge
|
|
)
|
|
|
|
hovered_boundary_vertex = (
|
|
new_hover_vertex
|
|
)
|
|
|
|
_update_boundary_add_cursor()
|
|
|
|
queue_redraw()
|
|
|
|
return
|
|
|
|
#
|
|
# EDIT BOUNDARY mode
|
|
#
|
|
if edit_mode == EditMode.EDIT_BOUNDARY:
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_LEFT
|
|
):
|
|
if event.pressed:
|
|
|
|
var vertex_index := (
|
|
_find_boundary_vertex_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if vertex_index >= 0:
|
|
_begin_boundary_vertex_drag(
|
|
vertex_index
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
else:
|
|
if active_boundary_vertex >= 0:
|
|
_finish_boundary_vertex_drag()
|
|
|
|
accept_event()
|
|
return
|
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
if active_boundary_vertex >= 0:
|
|
_update_boundary_vertex_drag(
|
|
event.position
|
|
)
|
|
|
|
accept_event()
|
|
return
|
|
|
|
var new_hover := (
|
|
_find_boundary_vertex_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if (
|
|
new_hover
|
|
!= hovered_boundary_vertex
|
|
):
|
|
hovered_boundary_vertex = (
|
|
new_hover
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
return
|
|
|
|
#
|
|
# DELETE BOUNDARY POINT mode
|
|
#
|
|
if edit_mode == EditMode.DELETE_BOUNDARY_POINT:
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_LEFT
|
|
and event.pressed
|
|
):
|
|
var vertex_index := (
|
|
_find_boundary_vertex_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if vertex_index >= 0:
|
|
_request_boundary_point_delete(
|
|
vertex_index
|
|
)
|
|
|
|
accept_event()
|
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
var new_hover := (
|
|
_find_boundary_vertex_at_screen_position(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if new_hover != hovered_boundary_vertex:
|
|
hovered_boundary_vertex = new_hover
|
|
queue_redraw()
|
|
|
|
return
|
|
|
|
#
|
|
# DRAW WALL mode
|
|
#
|
|
if edit_mode != EditMode.DRAW_WALL:
|
|
return
|
|
|
|
if (
|
|
event is InputEventMouseButton
|
|
and event.button_index == MOUSE_BUTTON_LEFT
|
|
):
|
|
if event.pressed:
|
|
var world_position := screen_to_world(
|
|
event.position
|
|
)
|
|
|
|
wall_draw_start = (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
wall_draw_current = wall_draw_start
|
|
is_drawing_wall = true
|
|
|
|
queue_redraw()
|
|
|
|
accept_event()
|
|
|
|
else:
|
|
if not is_drawing_wall:
|
|
return
|
|
|
|
var world_position := screen_to_world(
|
|
event.position
|
|
)
|
|
|
|
var wall_end := (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
is_drawing_wall = false
|
|
|
|
if (
|
|
wall_draw_start.distance_to(
|
|
wall_end
|
|
)
|
|
> 0.01
|
|
):
|
|
wall_creation_requested.emit(
|
|
wall_draw_start,
|
|
wall_end
|
|
)
|
|
|
|
queue_redraw()
|
|
accept_event()
|
|
|
|
elif (
|
|
event is InputEventMouseMotion
|
|
and is_drawing_wall
|
|
):
|
|
var world_position := screen_to_world(
|
|
event.position
|
|
)
|
|
|
|
wall_draw_current = (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
queue_redraw()
|
|
accept_event()
|
|
|
|
func _draw_wall_preview() -> void:
|
|
|
|
draw_line(
|
|
world_to_screen(
|
|
wall_draw_start
|
|
),
|
|
world_to_screen(
|
|
wall_draw_current
|
|
),
|
|
Color(
|
|
1.0,
|
|
0.7,
|
|
0.2,
|
|
1.0
|
|
),
|
|
4.0,
|
|
true
|
|
)
|
|
|
|
func _handle_zoom(
|
|
event: InputEventMouseButton
|
|
) -> void:
|
|
|
|
var old_world_position := (
|
|
screen_to_world(
|
|
event.position
|
|
)
|
|
)
|
|
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
|
zoom *= 1.1
|
|
|
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
|
zoom /= 1.1
|
|
|
|
zoom = clamp(
|
|
zoom,
|
|
0.25,
|
|
4.0
|
|
)
|
|
|
|
var new_screen_position := (
|
|
world_to_screen(
|
|
old_world_position
|
|
)
|
|
)
|
|
|
|
view_offset += (
|
|
event.position
|
|
- new_screen_position
|
|
)
|
|
|
|
_reposition_items()
|
|
|
|
zoom_changed.emit(
|
|
zoom
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func _reposition_items() -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
#
|
|
# Cabinets.
|
|
#
|
|
for placement in room.cabinet_placements:
|
|
|
|
if placement == null:
|
|
continue
|
|
|
|
if not item_nodes.has(
|
|
placement.cabinet_id
|
|
):
|
|
continue
|
|
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[
|
|
placement.cabinet_id
|
|
]
|
|
)
|
|
|
|
var cabinet := (
|
|
_get_cabinet_profile(
|
|
placement.cabinet_id
|
|
)
|
|
)
|
|
|
|
if cabinet != null:
|
|
|
|
item.configure_cabinet_footprint(
|
|
cabinet.footprint_width_m
|
|
* pixels_per_meter
|
|
* zoom,
|
|
cabinet.footprint_depth_m
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
item.configure_editor_texture(
|
|
cabinet.editor_texture,
|
|
cabinet.editor_texture_scale
|
|
)
|
|
|
|
var center_screen := (
|
|
world_to_screen(
|
|
placement.position
|
|
)
|
|
)
|
|
|
|
var center_offset := (
|
|
item.get_physical_center_offset()
|
|
)
|
|
|
|
item.position = (
|
|
center_screen
|
|
- center_offset
|
|
)
|
|
|
|
item.pivot_offset = (
|
|
center_offset
|
|
)
|
|
|
|
item.rotation_degrees = (
|
|
placement.rotation_degrees
|
|
)
|
|
|
|
#
|
|
# Devices.
|
|
#
|
|
for device in room.devices:
|
|
|
|
if device == null:
|
|
continue
|
|
|
|
if not item_nodes.has(
|
|
device.device_id
|
|
):
|
|
continue
|
|
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[
|
|
device.device_id
|
|
]
|
|
)
|
|
|
|
item.configure_device_footprint(
|
|
device.physical_size_m.x
|
|
* pixels_per_meter
|
|
* zoom,
|
|
device.physical_size_m.y
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
item.position = (
|
|
world_to_screen(
|
|
device.position
|
|
)
|
|
)
|
|
|
|
_update_binding_labels()
|
|
|
|
func _find_wall_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> RoomWallDefinition:
|
|
|
|
if room == null:
|
|
return null
|
|
|
|
var hit_radius_pixels: float = 10.0
|
|
|
|
for wall in room.walls:
|
|
if wall == null:
|
|
continue
|
|
|
|
var start_screen := world_to_screen(
|
|
wall.start
|
|
)
|
|
|
|
var end_screen := world_to_screen(
|
|
wall.end
|
|
)
|
|
|
|
var closest := Geometry2D.get_closest_point_to_segment(
|
|
screen_position,
|
|
start_screen,
|
|
end_screen
|
|
)
|
|
|
|
if (
|
|
closest.distance_to(screen_position)
|
|
<= hit_radius_pixels
|
|
):
|
|
return wall
|
|
|
|
return null
|
|
|
|
func _find_binding_segment_at_screen_position(
|
|
binding: RoomDeviceBinding,
|
|
screen_position: Vector2
|
|
) -> int:
|
|
|
|
if binding == null:
|
|
return -1
|
|
|
|
var route_points := (
|
|
_get_binding_route_points(
|
|
binding
|
|
)
|
|
)
|
|
|
|
if route_points.size() < 2:
|
|
return -1
|
|
|
|
var best_segment: int = -1
|
|
var best_distance: float = 12.0
|
|
|
|
for index in range(
|
|
route_points.size() - 1
|
|
):
|
|
|
|
var closest := (
|
|
Geometry2D.get_closest_point_to_segment(
|
|
screen_position,
|
|
route_points[index],
|
|
route_points[index + 1]
|
|
)
|
|
)
|
|
|
|
var distance := (
|
|
closest.distance_to(
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
if distance <= best_distance:
|
|
|
|
best_distance = distance
|
|
best_segment = index
|
|
|
|
return best_segment
|
|
|
|
func _request_binding_route_point_insert(
|
|
binding: RoomDeviceBinding,
|
|
screen_position: Vector2
|
|
) -> void:
|
|
|
|
if binding == null:
|
|
return
|
|
|
|
var segment_index := (
|
|
_find_binding_segment_at_screen_position(
|
|
binding,
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
if segment_index < 0:
|
|
return
|
|
|
|
var old_points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
var new_points := (
|
|
binding.route_points.duplicate()
|
|
)
|
|
|
|
var world_position := (
|
|
screen_to_world(
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
var snapped_position := (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
#
|
|
# The complete polyline is:
|
|
#
|
|
# cabinet endpoint
|
|
# route_points[0]
|
|
# route_points[1]
|
|
# ...
|
|
# device endpoint
|
|
#
|
|
# Therefore the polyline segment index is also
|
|
# the correct insertion index in route_points.
|
|
#
|
|
var insertion_index := clampi(
|
|
segment_index,
|
|
0,
|
|
new_points.size()
|
|
)
|
|
|
|
new_points.insert(
|
|
insertion_index,
|
|
snapped_position
|
|
)
|
|
|
|
selected_binding_key = (
|
|
_make_binding_key(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id
|
|
)
|
|
)
|
|
|
|
hovered_binding_route_point = (
|
|
insertion_index
|
|
)
|
|
|
|
binding_route_edit_requested.emit(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id,
|
|
old_points,
|
|
new_points
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func _select_wall(
|
|
wall_id: StringName
|
|
) -> void:
|
|
|
|
selected_binding_key = ""
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
selected_binding_route_point = -1
|
|
|
|
selected_item_id = &""
|
|
selected_wall_id = wall_id
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.set_selected(false)
|
|
|
|
wall_selected.emit(
|
|
wall_id
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func get_selected_wall() -> RoomWallDefinition:
|
|
|
|
if room == null:
|
|
return null
|
|
|
|
if selected_wall_id == &"":
|
|
return null
|
|
|
|
return room.get_wall(
|
|
selected_wall_id
|
|
)
|
|
|
|
func _find_wall_handle_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> WallHandle:
|
|
|
|
var wall := get_selected_wall()
|
|
|
|
if wall == null:
|
|
return WallHandle.NONE
|
|
|
|
var start_screen := world_to_screen(
|
|
wall.start
|
|
)
|
|
|
|
var end_screen := world_to_screen(
|
|
wall.end
|
|
)
|
|
|
|
var hit_radius: float = 12.0
|
|
|
|
if (
|
|
start_screen.distance_to(
|
|
screen_position
|
|
)
|
|
<= hit_radius
|
|
):
|
|
return WallHandle.START
|
|
|
|
if (
|
|
end_screen.distance_to(
|
|
screen_position
|
|
)
|
|
<= hit_radius
|
|
):
|
|
return WallHandle.END
|
|
|
|
return WallHandle.NONE
|
|
|
|
func _begin_wall_handle_drag(
|
|
handle: WallHandle
|
|
) -> void:
|
|
|
|
var wall := get_selected_wall()
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
active_wall_handle = handle
|
|
|
|
wall_handle_drag_wall_id = wall.wall_id
|
|
|
|
wall_handle_drag_original_start = wall.start
|
|
wall_handle_drag_original_end = wall.end
|
|
|
|
func _update_wall_handle_drag(
|
|
screen_position: Vector2
|
|
) -> void:
|
|
|
|
if active_wall_handle == WallHandle.NONE:
|
|
return
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var wall := room.get_wall(
|
|
wall_handle_drag_wall_id
|
|
)
|
|
|
|
if wall == null:
|
|
return
|
|
|
|
var world_position := screen_to_world(
|
|
screen_position
|
|
)
|
|
|
|
var snapped_position := (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
match active_wall_handle:
|
|
WallHandle.START:
|
|
wall.start = snapped_position
|
|
|
|
WallHandle.END:
|
|
wall.end = snapped_position
|
|
|
|
_:
|
|
return
|
|
|
|
wall_selected.emit(
|
|
wall.wall_id
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func _finish_wall_handle_drag() -> void:
|
|
|
|
if active_wall_handle == WallHandle.NONE:
|
|
return
|
|
|
|
if room == null:
|
|
_cancel_wall_handle_drag()
|
|
return
|
|
|
|
var wall := room.get_wall(
|
|
wall_handle_drag_wall_id
|
|
)
|
|
|
|
if wall == null:
|
|
_cancel_wall_handle_drag()
|
|
return
|
|
|
|
var new_start := wall.start
|
|
var new_end := wall.end
|
|
|
|
#
|
|
# Restore the original state before handing the command
|
|
# to the Workspace. UndoRedo will apply the final state.
|
|
#
|
|
wall.start = wall_handle_drag_original_start
|
|
wall.end = wall_handle_drag_original_end
|
|
|
|
wall_geometry_edit_requested.emit(
|
|
wall.wall_id,
|
|
wall_handle_drag_original_start,
|
|
wall_handle_drag_original_end,
|
|
new_start,
|
|
new_end
|
|
)
|
|
|
|
active_wall_handle = WallHandle.NONE
|
|
wall_handle_drag_wall_id = &""
|
|
|
|
queue_redraw()
|
|
|
|
func _cancel_wall_handle_drag() -> void:
|
|
|
|
if room != null:
|
|
var wall := room.get_wall(
|
|
wall_handle_drag_wall_id
|
|
)
|
|
|
|
if wall != null:
|
|
wall.start = (
|
|
wall_handle_drag_original_start
|
|
)
|
|
|
|
wall.end = (
|
|
wall_handle_drag_original_end
|
|
)
|
|
|
|
active_wall_handle = WallHandle.NONE
|
|
wall_handle_drag_wall_id = &""
|
|
|
|
queue_redraw()
|
|
|
|
func _begin_pan(
|
|
mouse_position: Vector2
|
|
) -> void:
|
|
|
|
is_panning = true
|
|
pan_last_mouse_position = mouse_position
|
|
|
|
func _update_pan(
|
|
mouse_position: Vector2
|
|
) -> void:
|
|
|
|
if not is_panning:
|
|
return
|
|
|
|
var delta := (
|
|
mouse_position
|
|
- pan_last_mouse_position
|
|
)
|
|
|
|
view_offset += delta
|
|
|
|
pan_last_mouse_position = mouse_position
|
|
|
|
_reposition_items()
|
|
queue_redraw()
|
|
|
|
func _end_pan() -> void:
|
|
|
|
is_panning = false
|
|
|
|
func apply_editor_selection(
|
|
selection: EscapeRoomEditorSelection
|
|
) -> void:
|
|
|
|
if selection == null:
|
|
return
|
|
|
|
clear_editor_highlight()
|
|
|
|
if (
|
|
selection.cabinet_id != &""
|
|
and selection.action_id != &""
|
|
):
|
|
selected_wall_id = &""
|
|
selected_item_id = selection.cabinet_id
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.set_selected(
|
|
id == selection.cabinet_id
|
|
)
|
|
|
|
highlight_logic_path(
|
|
selection.cabinet_id,
|
|
selection.action_id,
|
|
selection.device_id
|
|
)
|
|
|
|
return
|
|
|
|
if selection.wall_id != &"":
|
|
selected_item_id = &""
|
|
selected_wall_id = selection.wall_id
|
|
queue_redraw()
|
|
return
|
|
|
|
selected_wall_id = &""
|
|
|
|
if selection.cabinet_id != &"":
|
|
selected_item_id = selection.cabinet_id
|
|
|
|
elif selection.device_id != &"":
|
|
selected_item_id = selection.device_id
|
|
|
|
else:
|
|
selected_item_id = &""
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.set_selected(
|
|
id == selected_item_id
|
|
)
|
|
|
|
_update_relationship_highlight()
|
|
queue_redraw()
|
|
|
|
func highlight_logic_path(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName
|
|
) -> void:
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.modulate = Color(
|
|
0.45,
|
|
0.45,
|
|
0.45,
|
|
1.0
|
|
)
|
|
|
|
if item_nodes.has(cabinet_id):
|
|
var cabinet_item: RoomLayoutItem = (
|
|
item_nodes[cabinet_id]
|
|
)
|
|
|
|
cabinet_item.modulate = Color.WHITE
|
|
|
|
if (
|
|
device_id != &""
|
|
and item_nodes.has(device_id)
|
|
):
|
|
var device_item: RoomLayoutItem = (
|
|
item_nodes[device_id]
|
|
)
|
|
|
|
device_item.modulate = Color.WHITE
|
|
|
|
for binding in room.bindings:
|
|
if binding == null:
|
|
continue
|
|
|
|
if (
|
|
binding.cabinet_id == cabinet_id
|
|
and binding.hardware_action_id == action_id
|
|
):
|
|
if (
|
|
device_id == &""
|
|
or binding.device_id == device_id
|
|
):
|
|
set_binding_highlighted(
|
|
binding.cabinet_id,
|
|
binding.hardware_action_id,
|
|
binding.device_id,
|
|
true
|
|
)
|
|
|
|
func clear_editor_highlight() -> void:
|
|
|
|
for id in item_nodes:
|
|
var item: RoomLayoutItem = (
|
|
item_nodes[id]
|
|
)
|
|
|
|
item.modulate = Color.WHITE
|
|
item.set_selected(false)
|
|
|
|
selected_binding_key = ""
|
|
hovered_binding_route_point = -1
|
|
active_binding_route_point = -1
|
|
|
|
selected_binding_route_point = -1
|
|
|
|
selected_item_id = &""
|
|
selected_wall_id = &""
|
|
|
|
clear_binding_highlights()
|
|
|
|
queue_redraw()
|
|
|
|
func set_binding_highlighted(
|
|
cabinet_id: StringName,
|
|
action_id: StringName,
|
|
device_id: StringName,
|
|
highlighted: bool
|
|
) -> void:
|
|
|
|
var key := _make_binding_key(
|
|
cabinet_id,
|
|
action_id,
|
|
device_id
|
|
)
|
|
|
|
if highlighted:
|
|
highlighted_bindings[
|
|
key
|
|
] = true
|
|
|
|
else:
|
|
highlighted_bindings.erase(
|
|
key
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func clear_binding_highlights() -> void:
|
|
|
|
highlighted_bindings.clear()
|
|
queue_redraw()
|
|
|
|
func _find_boundary_vertex_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> int:
|
|
|
|
if room == null:
|
|
return -1
|
|
|
|
var best_index := -1
|
|
var best_distance := 12.0
|
|
|
|
for index in range(
|
|
room.boundary_points.size()
|
|
):
|
|
var vertex_screen_position := (
|
|
world_to_screen(
|
|
room.boundary_points[index]
|
|
)
|
|
)
|
|
|
|
var distance := (
|
|
vertex_screen_position.distance_to(
|
|
screen_position
|
|
)
|
|
)
|
|
|
|
if distance <= best_distance:
|
|
best_distance = distance
|
|
best_index = index
|
|
|
|
return best_index
|
|
|
|
func _begin_boundary_vertex_drag(
|
|
vertex_index: int
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
if (
|
|
vertex_index < 0
|
|
or vertex_index >= room.boundary_points.size()
|
|
):
|
|
return
|
|
|
|
active_boundary_vertex = vertex_index
|
|
|
|
boundary_drag_original_points = (
|
|
room.boundary_points.duplicate()
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _update_boundary_vertex_drag(
|
|
screen_position: Vector2
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
if active_boundary_vertex < 0:
|
|
return
|
|
|
|
if (
|
|
active_boundary_vertex
|
|
>= room.boundary_points.size()
|
|
):
|
|
return
|
|
|
|
var world_position := screen_to_world(
|
|
screen_position
|
|
)
|
|
|
|
var snapped_position := (
|
|
_snap_world_position(
|
|
world_position
|
|
)
|
|
)
|
|
|
|
var points := (
|
|
room.boundary_points.duplicate()
|
|
)
|
|
|
|
points[
|
|
active_boundary_vertex
|
|
] = snapped_position
|
|
|
|
room.boundary_points = points
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _finish_boundary_vertex_drag() -> void:
|
|
|
|
if room == null:
|
|
_cancel_boundary_vertex_drag()
|
|
return
|
|
|
|
if active_boundary_vertex < 0:
|
|
return
|
|
|
|
var new_points := (
|
|
room.boundary_points.duplicate()
|
|
)
|
|
|
|
#
|
|
# Restore the original before handing the change
|
|
# to Workspace UndoRedo.
|
|
#
|
|
room.boundary_points = (
|
|
boundary_drag_original_points.duplicate()
|
|
)
|
|
|
|
boundary_geometry_edit_requested.emit(
|
|
boundary_drag_original_points.duplicate(),
|
|
new_points
|
|
)
|
|
|
|
active_boundary_vertex = -1
|
|
boundary_drag_original_points = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
|
|
func _cancel_boundary_vertex_drag() -> void:
|
|
|
|
if room != null:
|
|
room.boundary_points = (
|
|
boundary_drag_original_points.duplicate()
|
|
)
|
|
|
|
active_boundary_vertex = -1
|
|
boundary_drag_original_points = (
|
|
PackedVector2Array()
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func focus_room() -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
var points: Array[Vector2] = []
|
|
|
|
#
|
|
# Room boundary.
|
|
#
|
|
for point in room.boundary_points:
|
|
points.append(
|
|
point
|
|
)
|
|
|
|
#
|
|
# Walls.
|
|
#
|
|
for wall in room.walls:
|
|
if wall == null:
|
|
continue
|
|
|
|
points.append(
|
|
wall.start
|
|
)
|
|
|
|
points.append(
|
|
wall.end
|
|
)
|
|
|
|
#
|
|
# Cabinet placements.
|
|
#
|
|
for placement in room.cabinet_placements:
|
|
if placement == null:
|
|
continue
|
|
|
|
points.append(
|
|
placement.position
|
|
)
|
|
|
|
#
|
|
# Room devices.
|
|
#
|
|
for device in room.devices:
|
|
if device == null:
|
|
continue
|
|
|
|
points.append(
|
|
device.position
|
|
)
|
|
|
|
if points.is_empty():
|
|
return
|
|
|
|
var min_point := points[0]
|
|
var max_point := points[0]
|
|
|
|
for point in points:
|
|
|
|
min_point.x = minf(
|
|
min_point.x,
|
|
point.x
|
|
)
|
|
|
|
min_point.y = minf(
|
|
min_point.y,
|
|
point.y
|
|
)
|
|
|
|
max_point.x = maxf(
|
|
max_point.x,
|
|
point.x
|
|
)
|
|
|
|
max_point.y = maxf(
|
|
max_point.y,
|
|
point.y
|
|
)
|
|
|
|
var world_size := (
|
|
max_point
|
|
- min_point
|
|
)
|
|
|
|
#
|
|
# Prevent divide-by-zero for extremely
|
|
# small or single-point layouts.
|
|
#
|
|
world_size.x = maxf(
|
|
world_size.x,
|
|
0.5
|
|
)
|
|
|
|
world_size.y = maxf(
|
|
world_size.y,
|
|
0.5
|
|
)
|
|
|
|
var margin_pixels := 80.0
|
|
|
|
var available_size := Vector2(
|
|
maxf(
|
|
size.x
|
|
- margin_pixels * 2.0,
|
|
1.0
|
|
),
|
|
maxf(
|
|
size.y
|
|
- margin_pixels * 2.0,
|
|
1.0
|
|
)
|
|
)
|
|
|
|
var zoom_x := (
|
|
available_size.x
|
|
/ (
|
|
world_size.x
|
|
* pixels_per_meter
|
|
)
|
|
)
|
|
|
|
var zoom_y := (
|
|
available_size.y
|
|
/ (
|
|
world_size.y
|
|
* pixels_per_meter
|
|
)
|
|
)
|
|
|
|
zoom = clampf(
|
|
minf(
|
|
zoom_x,
|
|
zoom_y
|
|
),
|
|
0.25,
|
|
4.0
|
|
)
|
|
|
|
var world_center := (
|
|
min_point
|
|
+ max_point
|
|
) * 0.5
|
|
|
|
view_offset = (
|
|
size * 0.5
|
|
- world_center
|
|
* pixels_per_meter
|
|
* zoom
|
|
)
|
|
|
|
_reposition_items()
|
|
|
|
zoom_changed.emit(
|
|
zoom
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func _closest_point_on_segment(
|
|
point: Vector2,
|
|
start: Vector2,
|
|
end: Vector2
|
|
) -> Vector2:
|
|
|
|
var segment := (
|
|
end - start
|
|
)
|
|
|
|
var length_squared := (
|
|
segment.length_squared()
|
|
)
|
|
|
|
if length_squared <= 0.000001:
|
|
return start
|
|
|
|
var t := (
|
|
(point - start).dot(
|
|
segment
|
|
)
|
|
/ length_squared
|
|
)
|
|
|
|
t = clampf(
|
|
t,
|
|
0.0,
|
|
1.0
|
|
)
|
|
|
|
return (
|
|
start
|
|
+ segment * t
|
|
)
|
|
|
|
func _find_boundary_edge_at_screen_position(
|
|
screen_position: Vector2
|
|
) -> int:
|
|
|
|
if room == null:
|
|
return -1
|
|
|
|
if room.boundary_points.size() < 2:
|
|
return -1
|
|
|
|
var best_index := -1
|
|
var best_distance := 12.0
|
|
|
|
for index in range(
|
|
room.boundary_points.size()
|
|
):
|
|
var next_index := (
|
|
(index + 1)
|
|
% room.boundary_points.size()
|
|
)
|
|
|
|
var start_screen := world_to_screen(
|
|
room.boundary_points[index]
|
|
)
|
|
|
|
var end_screen := world_to_screen(
|
|
room.boundary_points[next_index]
|
|
)
|
|
|
|
var closest := (
|
|
_closest_point_on_segment(
|
|
screen_position,
|
|
start_screen,
|
|
end_screen
|
|
)
|
|
)
|
|
|
|
var distance := (
|
|
screen_position.distance_to(
|
|
closest
|
|
)
|
|
)
|
|
|
|
if distance <= best_distance:
|
|
best_distance = distance
|
|
best_index = index
|
|
|
|
return best_index
|
|
|
|
func _request_boundary_point_insert(
|
|
edge_index: int,
|
|
screen_position: Vector2
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
if (
|
|
edge_index < 0
|
|
or edge_index >= room.boundary_points.size()
|
|
):
|
|
return
|
|
|
|
var next_index := (
|
|
(edge_index + 1)
|
|
% room.boundary_points.size()
|
|
)
|
|
|
|
var start_screen := world_to_screen(
|
|
room.boundary_points[
|
|
edge_index
|
|
]
|
|
)
|
|
|
|
var end_screen := world_to_screen(
|
|
room.boundary_points[
|
|
next_index
|
|
]
|
|
)
|
|
|
|
var closest_screen := (
|
|
_closest_point_on_segment(
|
|
screen_position,
|
|
start_screen,
|
|
end_screen
|
|
)
|
|
)
|
|
|
|
var new_world_point := (
|
|
screen_to_world(
|
|
closest_screen
|
|
)
|
|
)
|
|
|
|
new_world_point = (
|
|
_snap_world_position(
|
|
new_world_point
|
|
)
|
|
)
|
|
|
|
var old_points := (
|
|
room.boundary_points.duplicate()
|
|
)
|
|
|
|
var new_points := PackedVector2Array()
|
|
|
|
for index in range(
|
|
old_points.size()
|
|
):
|
|
new_points.append(
|
|
old_points[index]
|
|
)
|
|
|
|
if index == edge_index:
|
|
new_points.append(
|
|
new_world_point
|
|
)
|
|
|
|
boundary_geometry_edit_requested.emit(
|
|
old_points,
|
|
new_points
|
|
)
|
|
|
|
hovered_boundary_vertex = -1
|
|
hovered_boundary_edge = -1
|
|
|
|
_update_boundary_add_cursor()
|
|
|
|
queue_redraw()
|
|
|
|
func _request_boundary_point_delete(
|
|
vertex_index: int
|
|
) -> void:
|
|
|
|
if room == null:
|
|
return
|
|
|
|
if room.boundary_points.size() <= 3:
|
|
edit_rejected.emit(
|
|
"A room boundary must contain at least 3 points."
|
|
)
|
|
return
|
|
|
|
if (
|
|
vertex_index < 0
|
|
or vertex_index >= room.boundary_points.size()
|
|
):
|
|
return
|
|
|
|
var old_points := (
|
|
room.boundary_points.duplicate()
|
|
)
|
|
|
|
var new_points := PackedVector2Array()
|
|
|
|
for index in range(
|
|
old_points.size()
|
|
):
|
|
if index == vertex_index:
|
|
continue
|
|
|
|
new_points.append(
|
|
old_points[index]
|
|
)
|
|
|
|
boundary_geometry_edit_requested.emit(
|
|
old_points,
|
|
new_points
|
|
)
|
|
|
|
hovered_boundary_vertex = -1
|
|
queue_redraw()
|
|
|
|
func _update_boundary_add_cursor() -> void:
|
|
|
|
if (
|
|
edit_mode == EditMode.ADD_BOUNDARY_POINT
|
|
and hovered_boundary_edge >= 0
|
|
):
|
|
mouse_default_cursor_shape = (
|
|
Control.CURSOR_POINTING_HAND
|
|
)
|
|
else:
|
|
mouse_default_cursor_shape = (
|
|
Control.CURSOR_ARROW
|
|
)
|
|
|
|
func reset_view() -> void:
|
|
|
|
zoom = 1.0
|
|
|
|
view_offset = Vector2(
|
|
80.0,
|
|
80.0
|
|
)
|
|
|
|
_reposition_items()
|
|
|
|
zoom_changed.emit(
|
|
zoom
|
|
)
|
|
|
|
queue_redraw()
|
|
|
|
func refresh_item_positions() -> void:
|
|
|
|
_reposition_items()
|
|
|
|
_update_binding_labels()
|
|
|
|
queue_redraw()
|
|
|
|
func refresh_from_model() -> void:
|
|
|
|
_rebuild()
|
|
|
|
func _get_cabinet_profile(
|
|
cabinet_id: StringName
|
|
) -> CabinetProfile:
|
|
|
|
if scenario == null:
|
|
return null
|
|
|
|
for assignment in scenario.assignments:
|
|
|
|
if assignment == null:
|
|
continue
|
|
|
|
if assignment.cabinet == null:
|
|
continue
|
|
|
|
if (
|
|
assignment.cabinet.cabinet_id
|
|
== cabinet_id
|
|
):
|
|
return assignment.cabinet
|
|
|
|
return null
|