Add sandbox stage builder scripts and functionality
- Introduced StageGizmos for hover highlighting, selection outlines, and rotation handles in the sandbox stage builder. - Added StageGrid for an optional world-space grid overlay that adjusts with camera panning and zooming. - Implemented StageSelection for geometric hit-testing and selection management of nodes in the sandbox. - Created StageSpawner as a registry-driven factory for spawning terrain, props, and stickmen, allowing for dynamic template management. - Each script includes necessary constants, state management, and public API methods for interaction.
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
class_name SandboxStage
|
||||
extends Node2D
|
||||
## SandboxStage - Kid-friendly director sandbox stage (Phase 2).
|
||||
##
|
||||
## A self-contained visual stage for placing terrain, props and stickmen from a
|
||||
## palette, with EDIT (build) and PLAY (simulate) modes. Selection uses geometric
|
||||
## hit-testing; objects are dragged directly (no move handle) and rotated via a
|
||||
## ring handle. A placement ghost previews the object under the cursor, and an
|
||||
## optional snap-to-grid + grid overlay ease placement. NOT wired into the editor
|
||||
## - run standalone via F6 on res://scenes/sandbox_stage.tscn.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preloaded helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd")
|
||||
const STAGE_SELECTION := preload("res://scripts/stage_selection.gd")
|
||||
const STAGE_GIZMOS := preload("res://scripts/stage_gizmos.gd")
|
||||
const STAGE_GRID := preload("res://scripts/stage_grid.gd")
|
||||
const STICKMAN_RIG := preload("res://scripts/stickman_rig.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
enum StageMode { EDIT, PLAY }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
signal mode_changed(mode: int)
|
||||
signal object_placed(node: Node2D)
|
||||
signal object_selected(nodes: Array[Node2D])
|
||||
signal object_deselected()
|
||||
signal object_deleted(nodes: Array[Node2D])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ZOOM_STEP: float = 1.10
|
||||
const MIN_BOX_AREA: float = 16.0
|
||||
const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer"
|
||||
|
||||
const SETTINGS_PATH := "user://sandbox_settings.json"
|
||||
const DEFAULT_GRID_SIZE := 15.0
|
||||
const MIN_GRID_SIZE := 1.0
|
||||
const MAX_GRID_SIZE := 100.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported properties
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@export var min_zoom: float = 0.1
|
||||
@export var max_zoom: float = 6.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node references
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@onready var _camera: Camera2D = $Camera2D
|
||||
@onready var _world: Node2D = $World
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var current_mode: StageMode = StageMode.EDIT
|
||||
|
||||
var _spawner: StageSpawner
|
||||
var _selection: StageSelection
|
||||
var _gizmos: StageGizmos
|
||||
var _grid: StageGrid
|
||||
|
||||
var _placement_id: String = ""
|
||||
|
||||
var _mode_button: Button
|
||||
var _palette_buttons: Dictionary = {}
|
||||
var _status_label: Label
|
||||
var _grid_check: CheckBox
|
||||
var _snap_check: CheckBox
|
||||
var _grid_size_spin: SpinBox
|
||||
|
||||
var _grid_size: float = DEFAULT_GRID_SIZE
|
||||
var _snap_enabled: bool = false
|
||||
var _show_grid: bool = true
|
||||
|
||||
var _panning: bool = false
|
||||
var _pan_last: Vector2 = Vector2.ZERO
|
||||
|
||||
var _box_selecting: bool = false
|
||||
var _box_start: Vector2 = Vector2.ZERO
|
||||
var _box_rect: Rect2 = Rect2()
|
||||
|
||||
var _ghost: Node2D = null
|
||||
var _ghost_holder: Node2D = null
|
||||
|
||||
## Authored object state (instance id -> {node, position, rotation}), updated on
|
||||
## every place/move/rotate and restored when returning to EDIT.
|
||||
var _authored: Dictionary = {}
|
||||
|
||||
## Physics frames remaining before re-asserting the authored restore (a safety
|
||||
## net for engines where a same-frame freeze + teleport does not stick).
|
||||
var _restore_frames_left: int = 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_camera.make_current()
|
||||
_load_settings()
|
||||
|
||||
_spawner = STAGE_SPAWNER.new(_world)
|
||||
_selection = STAGE_SELECTION.new(_world, _camera)
|
||||
_selection.selection_changed.connect(_on_selection_changed)
|
||||
_selection.hover_changed.connect(_on_hover_changed)
|
||||
|
||||
_build_grid_layer()
|
||||
_build_gizmo_layer()
|
||||
_build_ghost_holder()
|
||||
_build_ui()
|
||||
_apply_grid_settings()
|
||||
_refresh_status()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _ghost != null and is_instance_valid(_ghost) and current_mode == StageMode.EDIT:
|
||||
_update_ghost_position()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if _restore_frames_left > 0:
|
||||
_restore_frames_left -= 1
|
||||
_restore_authored_state()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
_handle_mouse_button(event)
|
||||
elif event is InputEventMouseMotion:
|
||||
_handle_mouse_motion(event)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
_handle_world_click(event)
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not (event is InputEventKey) or not event.pressed or event.echo:
|
||||
return
|
||||
match event.keycode:
|
||||
KEY_DELETE, KEY_BACKSPACE:
|
||||
if current_mode == StageMode.EDIT:
|
||||
delete_selected()
|
||||
KEY_ESCAPE:
|
||||
if _placement_id != "":
|
||||
set_placement_mode("")
|
||||
elif not _selection.get_selected().is_empty():
|
||||
_selection.clear_selection()
|
||||
|
||||
|
||||
func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
match mb.button_index:
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
if mb.pressed:
|
||||
_set_zoom(_camera.zoom.x * ZOOM_STEP)
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
if mb.pressed:
|
||||
_set_zoom(_camera.zoom.x / ZOOM_STEP)
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
_panning = mb.pressed
|
||||
_pan_last = mb.position
|
||||
|
||||
|
||||
func _handle_world_click(mb: InputEventMouseButton) -> void:
|
||||
if mb.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
if current_mode != StageMode.EDIT:
|
||||
return
|
||||
var world_pos := _camera.get_global_mouse_position()
|
||||
if mb.pressed:
|
||||
if _gizmos.hit_test(world_pos) != STAGE_GIZMOS.Handle.NONE:
|
||||
_gizmos.begin_drag(world_pos)
|
||||
elif _placement_id != "":
|
||||
_place_at(world_pos)
|
||||
else:
|
||||
_begin_click_select(world_pos, mb.shift_pressed)
|
||||
else:
|
||||
if _gizmos.is_dragging():
|
||||
_gizmos.end_drag()
|
||||
elif _box_selecting:
|
||||
_finish_box_select(mb.shift_pressed)
|
||||
|
||||
|
||||
func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
|
||||
if _panning:
|
||||
_camera.position -= mm.relative / _camera.zoom.x
|
||||
return
|
||||
if current_mode != StageMode.EDIT:
|
||||
return
|
||||
var world_pos := _camera.get_global_mouse_position()
|
||||
if _gizmos.is_dragging():
|
||||
_gizmos.drag_to(world_pos)
|
||||
elif _box_selecting:
|
||||
_box_rect = Rect2(_box_start, world_pos - _box_start)
|
||||
_gizmos.set_box_rect(_box_rect)
|
||||
elif not _is_mouse_over_ui():
|
||||
_selection.update_hover(world_pos)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mode management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func set_mode(mode: StageMode) -> void:
|
||||
if mode == current_mode:
|
||||
return
|
||||
current_mode = mode
|
||||
if mode == StageMode.EDIT:
|
||||
_enter_edit_mode()
|
||||
else:
|
||||
_enter_play_mode()
|
||||
mode_changed.emit(int(mode))
|
||||
_refresh_status()
|
||||
|
||||
|
||||
func _enter_edit_mode() -> void:
|
||||
# Snap stickmen straight back to their standing pose/position (no stand-up
|
||||
# tween glide).
|
||||
for node: Node2D in _world_children_selectable():
|
||||
if node is STICKMAN_RIG:
|
||||
(node as STICKMAN_RIG).snap_to_standing()
|
||||
# Freeze every prop (kinematic) so nothing keeps falling in EDIT — including
|
||||
# any object that may not be in the authored map. freeze_mode is set BEFORE
|
||||
# freeze so the body freezes directly as kinematic, never via the static
|
||||
# layer (whose transform sync can drop a subsequent position/rotation set).
|
||||
for node: Node2D in _world_children_selectable():
|
||||
if node is RigidBody2D:
|
||||
var body := node as RigidBody2D
|
||||
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
||||
body.freeze = true
|
||||
_restore_authored_state()
|
||||
# The freeze/teleport can take a physics frame or two to settle in the engine,
|
||||
# so re-assert the authored transform on the next few physics frames.
|
||||
_restore_frames_left = 3
|
||||
_gizmos.set_enabled(true)
|
||||
|
||||
|
||||
func _enter_play_mode() -> void:
|
||||
_gizmos.set_enabled(false)
|
||||
_selection.clear_selection()
|
||||
set_placement_mode("")
|
||||
for node: Node2D in _world_children_selectable():
|
||||
if node is RigidBody2D:
|
||||
(node as RigidBody2D).freeze = false
|
||||
for node: Node2D in _world_children_selectable():
|
||||
if node is STICKMAN_RIG:
|
||||
var rig := node as STICKMAN_RIG
|
||||
rig.auto_recover = false
|
||||
rig.set_ragdoll(true)
|
||||
|
||||
|
||||
func _save_object_state(node: Node2D) -> void:
|
||||
_authored[node.get_instance_id()] = {
|
||||
"node": node,
|
||||
"position": node.global_position,
|
||||
"rotation": node.global_rotation,
|
||||
}
|
||||
|
||||
|
||||
func _clear_object_state(node: Node2D) -> void:
|
||||
_authored.erase(node.get_instance_id())
|
||||
|
||||
|
||||
func _restore_authored_state() -> void:
|
||||
for id: int in _authored.keys():
|
||||
var entry: Dictionary = _authored[id]
|
||||
var node := entry.get("node") as Node2D
|
||||
if node == null or not is_instance_valid(node):
|
||||
_authored.erase(id)
|
||||
continue
|
||||
node.global_position = entry.get("position", node.global_position)
|
||||
node.global_rotation = entry.get("rotation", node.global_rotation)
|
||||
if node is RigidBody2D:
|
||||
var body := node as RigidBody2D
|
||||
body.linear_velocity = Vector2.ZERO
|
||||
body.angular_velocity = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Placement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func set_placement_mode(id: String) -> void:
|
||||
_placement_id = id
|
||||
for pid: String in _palette_buttons:
|
||||
var btn: Button = _palette_buttons[pid]
|
||||
btn.set_pressed_no_signal(pid == id)
|
||||
if id == "":
|
||||
_free_ghost()
|
||||
else:
|
||||
_spawn_ghost()
|
||||
|
||||
|
||||
func _place_at(world_pos: Vector2) -> void:
|
||||
if _snap_enabled:
|
||||
world_pos = _snap_to_grid(world_pos)
|
||||
var node := _spawner.spawn(_placement_id, world_pos)
|
||||
if node == null:
|
||||
return
|
||||
if current_mode == StageMode.EDIT and node is RigidBody2D:
|
||||
var body := node as RigidBody2D
|
||||
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
||||
body.freeze = true
|
||||
_save_object_state(node)
|
||||
object_placed.emit(node)
|
||||
_refresh_status()
|
||||
_spawn_ghost()
|
||||
|
||||
|
||||
func _spawn_ghost() -> void:
|
||||
_free_ghost()
|
||||
if _placement_id == "":
|
||||
return
|
||||
var ghost := _spawner.spawn(_placement_id, Vector2.ZERO)
|
||||
if ghost == null:
|
||||
return
|
||||
# Reparent out of World so the ghost is not selectable/counted.
|
||||
if ghost.get_parent() == _world:
|
||||
_world.remove_child(ghost)
|
||||
_ghost_holder.add_child(ghost)
|
||||
ghost.modulate = Color(1.0, 1.0, 1.0, 0.5)
|
||||
if ghost is RigidBody2D:
|
||||
var body := ghost as RigidBody2D
|
||||
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
||||
body.freeze = true
|
||||
body.collision_layer = 0
|
||||
body.collision_mask = 0
|
||||
elif ghost is StaticBody2D:
|
||||
var sb := ghost as StaticBody2D
|
||||
sb.collision_layer = 0
|
||||
sb.collision_mask = 0
|
||||
elif ghost is STICKMAN_RIG:
|
||||
# Freeze the ghost rig into a static standing figure: disable IK solving
|
||||
# and animation so its limbs don't flex/follow anything while the ghost
|
||||
# tracks the cursor.
|
||||
var rig := ghost as STICKMAN_RIG
|
||||
var skeleton := rig.get_node_or_null(NodePath("Skeleton2D")) as Skeleton2D
|
||||
if skeleton != null and skeleton.modification_stack != null:
|
||||
skeleton.modification_stack.enabled = false
|
||||
var anim := rig.get_node_or_null(NodePath("AnimationPlayer")) as AnimationPlayer
|
||||
if anim != null:
|
||||
anim.stop()
|
||||
_ghost = ghost
|
||||
_update_ghost_position()
|
||||
|
||||
|
||||
func _free_ghost() -> void:
|
||||
if _ghost != null and is_instance_valid(_ghost):
|
||||
_ghost.queue_free()
|
||||
_ghost = null
|
||||
|
||||
|
||||
func _update_ghost_position() -> void:
|
||||
if _ghost == null or not is_instance_valid(_ghost):
|
||||
return
|
||||
var pos := _camera.get_global_mouse_position() + _spawner.get_spawn_offset(_placement_id)
|
||||
if _snap_enabled:
|
||||
pos = _snap_to_grid(pos)
|
||||
_ghost.position = pos
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Selection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _begin_click_select(world_pos: Vector2, shift: bool) -> void:
|
||||
var hit := _selection.hit_test(world_pos)
|
||||
if hit != null:
|
||||
if shift:
|
||||
_selection.toggle_selection(hit)
|
||||
if _selection.is_selected(hit):
|
||||
_gizmos.begin_translate_drag(hit, world_pos)
|
||||
else:
|
||||
_selection.select_only(hit)
|
||||
_gizmos.begin_translate_drag(hit, world_pos)
|
||||
else:
|
||||
_box_selecting = true
|
||||
_box_start = world_pos
|
||||
_box_rect = Rect2(world_pos, Vector2.ZERO)
|
||||
_gizmos.set_box_rect(_box_rect)
|
||||
|
||||
|
||||
func _finish_box_select(shift: bool) -> void:
|
||||
_box_selecting = false
|
||||
_gizmos.set_box_rect(Rect2())
|
||||
var rect := _box_rect.abs()
|
||||
if rect.get_area() < MIN_BOX_AREA:
|
||||
if not shift:
|
||||
_selection.clear_selection()
|
||||
else:
|
||||
_selection.box_select(rect, shift)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deletion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func delete_selected() -> void:
|
||||
var selected := _selection.get_selected().duplicate()
|
||||
if selected.is_empty():
|
||||
return
|
||||
for node: Node2D in selected:
|
||||
if is_instance_valid(node):
|
||||
_clear_object_state(node)
|
||||
node.queue_free()
|
||||
_selection.clear_selection()
|
||||
object_deleted.emit(selected)
|
||||
_refresh_status()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status / UI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _refresh_status() -> void:
|
||||
if _status_label == null:
|
||||
return
|
||||
var mode_text := "EDIT" if current_mode == StageMode.EDIT else "PLAY"
|
||||
var count := _world_children_selectable().size()
|
||||
var sel := _selection.get_selected()
|
||||
var sel_text: String
|
||||
if sel.is_empty():
|
||||
sel_text = "none"
|
||||
elif sel.size() == 1:
|
||||
sel_text = String(sel[0].name)
|
||||
else:
|
||||
sel_text = "%d objects" % sel.size()
|
||||
_status_label.text = "Mode: %s | Objects: %d | Selected: %s" % [mode_text, count, sel_text]
|
||||
if _mode_button != null:
|
||||
_mode_button.set_pressed_no_signal(current_mode == StageMode.PLAY)
|
||||
_mode_button.text = "Play" if current_mode == StageMode.EDIT else "Edit"
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var ui := CanvasLayer.new()
|
||||
ui.name = "UI"
|
||||
add_child(ui)
|
||||
|
||||
var top_bar := PanelContainer.new()
|
||||
top_bar.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
top_bar.offset_bottom = 40.0
|
||||
ui.add_child(top_bar)
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
top_bar.add_child(hbox)
|
||||
|
||||
_mode_button = Button.new()
|
||||
_mode_button.toggle_mode = true
|
||||
_mode_button.toggled.connect(_on_mode_toggled)
|
||||
hbox.add_child(_mode_button)
|
||||
|
||||
for id: String in _spawner.get_spawnable_ids():
|
||||
var btn := Button.new()
|
||||
btn.text = _spawner.get_label(id)
|
||||
btn.toggle_mode = true
|
||||
btn.toggled.connect(_on_palette_toggled.bind(id))
|
||||
hbox.add_child(btn)
|
||||
_palette_buttons[id] = btn
|
||||
|
||||
_grid_check = CheckBox.new()
|
||||
_grid_check.text = "Grid"
|
||||
_grid_check.button_pressed = _show_grid
|
||||
_grid_check.toggled.connect(_on_grid_toggled)
|
||||
hbox.add_child(_grid_check)
|
||||
|
||||
_snap_check = CheckBox.new()
|
||||
_snap_check.text = "Snap"
|
||||
_snap_check.button_pressed = _snap_enabled
|
||||
_snap_check.toggled.connect(_on_snap_toggled)
|
||||
hbox.add_child(_snap_check)
|
||||
|
||||
var grid_label := Label.new()
|
||||
grid_label.text = "Size"
|
||||
hbox.add_child(grid_label)
|
||||
|
||||
_grid_size_spin = SpinBox.new()
|
||||
_grid_size_spin.min_value = MIN_GRID_SIZE
|
||||
_grid_size_spin.max_value = MAX_GRID_SIZE
|
||||
_grid_size_spin.step = 1.0
|
||||
_grid_size_spin.value = _grid_size
|
||||
_grid_size_spin.rounded = true
|
||||
_grid_size_spin.value_changed.connect(_on_grid_size_changed)
|
||||
hbox.add_child(_grid_size_spin)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
hbox.add_child(_status_label)
|
||||
|
||||
|
||||
func _build_gizmo_layer() -> void:
|
||||
_gizmos = STAGE_GIZMOS.new()
|
||||
_gizmos.name = "GizmoLayer"
|
||||
_gizmos.camera = _camera
|
||||
_gizmos.transform_committed.connect(_on_transform_committed)
|
||||
add_child(_gizmos)
|
||||
|
||||
|
||||
func _build_grid_layer() -> void:
|
||||
_grid = STAGE_GRID.new()
|
||||
_grid.name = "GridLayer"
|
||||
_grid.camera = _camera
|
||||
add_child(_grid)
|
||||
move_child(_grid, 0)
|
||||
|
||||
|
||||
func _build_ghost_holder() -> void:
|
||||
_ghost_holder = Node2D.new()
|
||||
_ghost_holder.name = "PlacementGhost"
|
||||
add_child(_ghost_holder)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _on_mode_toggled(pressed: bool) -> void:
|
||||
set_mode(StageMode.PLAY if pressed else StageMode.EDIT)
|
||||
|
||||
|
||||
func _on_palette_toggled(pressed: bool, id: String) -> void:
|
||||
if pressed:
|
||||
set_placement_mode(id)
|
||||
elif _placement_id == id:
|
||||
set_placement_mode("")
|
||||
|
||||
|
||||
func _on_grid_toggled(pressed: bool) -> void:
|
||||
_show_grid = pressed
|
||||
_apply_grid_settings()
|
||||
_save_settings()
|
||||
|
||||
|
||||
func _on_snap_toggled(pressed: bool) -> void:
|
||||
_snap_enabled = pressed
|
||||
_apply_grid_settings()
|
||||
_save_settings()
|
||||
|
||||
|
||||
func _on_grid_size_changed(value: float) -> void:
|
||||
_grid_size = clampf(value, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
_apply_grid_settings()
|
||||
_save_settings()
|
||||
|
||||
|
||||
func _on_selection_changed(nodes: Array) -> void:
|
||||
_gizmos.set_target(_selection.get_primary())
|
||||
if nodes.is_empty():
|
||||
object_deselected.emit()
|
||||
else:
|
||||
object_selected.emit(nodes)
|
||||
_refresh_status()
|
||||
|
||||
|
||||
func _on_hover_changed(node: Node2D) -> void:
|
||||
_gizmos.set_hover(node)
|
||||
|
||||
|
||||
func _on_transform_committed(node: Node2D) -> void:
|
||||
_save_object_state(node)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _set_zoom(value: float) -> void:
|
||||
var z := clampf(value, min_zoom, max_zoom)
|
||||
_camera.zoom = Vector2(z, z)
|
||||
|
||||
|
||||
func _is_mouse_over_ui() -> bool:
|
||||
return get_viewport().gui_get_hovered_control() != null
|
||||
|
||||
|
||||
func _snap_to_grid(v: Vector2) -> Vector2:
|
||||
if _grid_size <= 0.0:
|
||||
return v
|
||||
return Vector2(roundf(v.x / _grid_size) * _grid_size, roundf(v.y / _grid_size) * _grid_size)
|
||||
|
||||
|
||||
func _apply_grid_settings() -> void:
|
||||
if _grid != null:
|
||||
_grid.grid_size = _grid_size
|
||||
_grid.enabled = _show_grid
|
||||
if _gizmos != null:
|
||||
_gizmos.snap_size = _grid_size if _snap_enabled else 0.0
|
||||
|
||||
|
||||
## Direct Node2D children of World, excluding the ragdoll body container.
|
||||
func _world_children_selectable() -> Array[Node2D]:
|
||||
var result: Array[Node2D] = []
|
||||
for child: Node in _world.get_children():
|
||||
var node := child as Node2D
|
||||
if node == null:
|
||||
continue
|
||||
if node.name == RAGDOLL_CONTAINER_NAME:
|
||||
continue
|
||||
result.append(node)
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings persistence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_settings() -> void:
|
||||
if not FileAccess.file_exists(SETTINGS_PATH):
|
||||
return
|
||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
return
|
||||
var json: Variant = JSON.parse_string(file.get_as_text())
|
||||
file.close()
|
||||
if not json is Dictionary:
|
||||
return
|
||||
var d := json as Dictionary
|
||||
_grid_size = float(d.get("grid_size", DEFAULT_GRID_SIZE))
|
||||
_grid_size = clampf(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
_snap_enabled = bool(d.get("snap_to_grid", false))
|
||||
_show_grid = bool(d.get("show_grid", true))
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
var data := {
|
||||
"version": "1.0",
|
||||
"grid_size": _grid_size,
|
||||
"snap_to_grid": _snap_enabled,
|
||||
"show_grid": _show_grid,
|
||||
}
|
||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(JSON.stringify(data, "\t", false))
|
||||
file.close()
|
||||
Reference in New Issue
Block a user