Implement Phase 6 features for Stickman Editor
- Updated BUGS.md with new issues related to selection bounding box, touchpad panning speed, and zooming behavior. - Enhanced PROJECT.md with detailed specifications for touchpad controls, recent colors in the color picker, and a status bar for project information. - Modified project.godot to update compatibility version to 4.7. - Added a status bar in stickman_editor.tscn to display cursor coordinates and snap status. - Updated body_part_panel.gd to handle recent colors and cursor detection over drawing areas. - Enhanced stickman_editor.gd to manage cursor coordinates display and recent colors tracking. - Improved whole_stickman_preview.gd to render selection gizmos on top of other parts. - Created master_rig.tscn for the stickman rig structure with bones and IK targets. - Developed master_rig_builder.gd to automate the rig building process with remote transforms and IK modifications.
This commit is contained in:
@@ -19,6 +19,7 @@ signal shape_changed(shape_data: Array)
|
||||
signal shape_selected()
|
||||
signal shape_copy_requested(shape_dict: Dictionary)
|
||||
signal shape_paste_requested(world_pos: Vector2)
|
||||
signal color_selected(color: Color)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported / public properties
|
||||
@@ -269,6 +270,13 @@ func set_has_clipboard(state: bool) -> void:
|
||||
_has_clipboard = state
|
||||
|
||||
|
||||
func set_recent_colors(colors_hex: Array) -> void:
|
||||
for preset in color_picker.get_presets():
|
||||
color_picker.erase_preset(preset)
|
||||
for hex_str in colors_hex:
|
||||
color_picker.add_preset(Color.from_string(str(hex_str), Color.BLACK))
|
||||
|
||||
|
||||
func paste_shape(source: Dictionary, target_pos: Vector2) -> void:
|
||||
## Insert a deep copy of the clipboard shape at the given world position.
|
||||
## The shape's points are translated so its center is at target_pos.
|
||||
@@ -293,6 +301,14 @@ func paste_shape(source: Dictionary, target_pos: Vector2) -> void:
|
||||
drawing_area.queue_redraw()
|
||||
_emit_changed()
|
||||
|
||||
|
||||
func is_cursor_over_drawing(global_pos: Vector2) -> bool:
|
||||
return drawing_area.get_global_rect().has_point(global_pos)
|
||||
|
||||
|
||||
func global_to_world(global_pos: Vector2) -> Vector2:
|
||||
return _screen_to_world(global_pos - drawing_area.global_position)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -427,6 +443,22 @@ func _draw_polyline(pts: PackedVector2Array, color: Color, closed: bool, width:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMagnifyGesture:
|
||||
var mag := event as InputEventMagnifyGesture
|
||||
var cursor_local := drawing_area.get_local_mouse_position()
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom * mag.factor, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
drawing_area.queue_redraw()
|
||||
return
|
||||
|
||||
if event is InputEventPanGesture:
|
||||
var pan := event as InputEventPanGesture
|
||||
_pan_offset -= pan.delta * 3.0
|
||||
_dragging_vertex = -1
|
||||
drawing_area.queue_redraw()
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
|
||||
@@ -442,11 +474,17 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
||||
|
||||
# 2. Mouse wheel -> zoom
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
||||
var cursor_local := mb.position
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
drawing_area.queue_redraw()
|
||||
return
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
||||
var cursor_local := mb.position
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom / ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
drawing_area.queue_redraw()
|
||||
return
|
||||
|
||||
@@ -468,7 +506,7 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
||||
var mm := event as InputEventMouseMotion
|
||||
|
||||
if _is_panning:
|
||||
_pan_offset += (mm.position - _pan_start) / _zoom
|
||||
_pan_offset += (mm.position - _pan_start)
|
||||
_pan_start = mm.position
|
||||
drawing_area.queue_redraw()
|
||||
return
|
||||
@@ -878,6 +916,7 @@ func _on_color_picker_changed(new_color: Color) -> void:
|
||||
|
||||
func _on_color_picker_ok() -> void:
|
||||
color_picker_popup.hide()
|
||||
color_selected.emit(color_picker.color)
|
||||
_emit_changed()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# res://scripts/rig_builder.gd
|
||||
@tool
|
||||
extends Node2D
|
||||
|
||||
## Builder script mirroring the exact bone layout, RemoteTransform2D links,
|
||||
## and IK modifications from the source scene.
|
||||
|
||||
@export var build_trigger: bool = false:
|
||||
set(val):
|
||||
if val:
|
||||
build_rig()
|
||||
build_trigger = false
|
||||
|
||||
func build_rig() -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
|
||||
name = "Node2D"
|
||||
|
||||
# 1. Main Hierarchy Containers
|
||||
var sticky := Node2D.new()
|
||||
sticky.name = "Sticky"
|
||||
add_child(sticky)
|
||||
|
||||
var stickman := Node2D.new()
|
||||
stickman.name = "Stickman"
|
||||
sticky.add_child(stickman)
|
||||
|
||||
# Container for visual nodes (Line2D / drawn elements)
|
||||
var body := Node2D.new()
|
||||
body.name = "Body"
|
||||
stickman.add_child(body)
|
||||
|
||||
# Container for skeleton
|
||||
var bones := Node2D.new()
|
||||
bones.name = "Bones"
|
||||
bones.visible = false
|
||||
stickman.add_child(bones)
|
||||
|
||||
var skeleton := Skeleton2D.new()
|
||||
skeleton.name = "Skeleton2D"
|
||||
bones.add_child(skeleton)
|
||||
|
||||
# --- 2. BONE HIERARCHY & REMOTE TRANSFORMS ---
|
||||
# Hip (Root Bone)
|
||||
var hip := Bone2D.new()
|
||||
hip.name = "Hip"
|
||||
hip.position = Vector2(-1, 27)
|
||||
hip.rest = Transform2D(0.0, Vector2(0, 32))
|
||||
skeleton.add_child(hip)
|
||||
|
||||
# Hip drives main Body visual
|
||||
_create_remote_transform("HipTransform", hip, "../../../../Body/Body", Vector2(0, -24))
|
||||
|
||||
# Left Leg
|
||||
var leg_u_l := _create_bone("Leg_Upper_Left", Vector2(-1, 1), hip)
|
||||
_create_remote_transform("RemoteTransform2D", leg_u_l, "../../../../../Body/Leg_Upper_Left", Vector2(1, 0), 2.35619)
|
||||
|
||||
var leg_l_l := _create_bone("Leg_Lower_Left", Vector2(-8, 8), leg_u_l)
|
||||
leg_l_l.auto_calculate_length_and_angle = false
|
||||
leg_l_l.length = 10.425
|
||||
leg_l_l.bone_angle = 89.9999
|
||||
_create_remote_transform("RemoteTransform2D", leg_l_l, "../../../../../../Body/Leg_Lower_Left", Vector2.ZERO, 1.57079)
|
||||
|
||||
# Right Leg
|
||||
var leg_u_r := _create_bone("Leg_Upper_Right", Vector2(2, 1), hip)
|
||||
_create_remote_transform("RemoteTransform2D", leg_u_r, "../../../../../Body/Leg_Upper_Right", Vector2(-2, 0), 0.785397)
|
||||
|
||||
var leg_l_r := _create_bone("Leg_Lower_Right", Vector2(7, 8), leg_u_r)
|
||||
leg_l_r.auto_calculate_length_and_angle = false
|
||||
leg_l_r.length = 10.425
|
||||
leg_l_r.bone_angle = 89.9999
|
||||
_create_remote_transform("RemoteTransform2D", leg_l_r, "../../../../../../Body/Leg_Lower_Right", Vector2.ZERO, 1.57079)
|
||||
|
||||
# Left Arm
|
||||
var arm_u_l := _create_bone("Arm_Upper_Left", Vector2(-2, -17), hip)
|
||||
_create_remote_transform("RemoteTransform2D", arm_u_l, "../../../../../Body/Arm_Upper_Left", Vector2(-11, 0))
|
||||
|
||||
var arm_l_l := _create_bone("Arm_Lower_Left", Vector2(-11, 0), arm_u_l)
|
||||
arm_l_l.auto_calculate_length_and_angle = false
|
||||
arm_l_l.length = 10.0
|
||||
arm_l_l.bone_angle = -180.0
|
||||
_create_remote_transform("RemoteTransform2D", arm_l_l, "../../../../../../Body/Arm_Lower_Left", Vector2(-10, 0))
|
||||
|
||||
# Right Arm
|
||||
var arm_u_r := _create_bone("Arm_Upper_Right", Vector2(2, -17), hip)
|
||||
_create_remote_transform("RemoteTransform2D", arm_u_r, "../../../../../Body/Arm_Upper_Right", Vector2.ZERO)
|
||||
|
||||
var arm_l_r := _create_bone("Arm_Lower_Right", Vector2(11, 0), arm_u_r)
|
||||
arm_l_r.auto_calculate_length_and_angle = false
|
||||
arm_l_r.length = 10.0
|
||||
arm_l_r.bone_angle = 0.0
|
||||
_create_remote_transform("RemoteTransform2D", arm_l_r, "../../../../../../Body/Arm_Lower_Right", Vector2.ZERO)
|
||||
|
||||
# Head
|
||||
var head := _create_bone("Head", Vector2(0, -23), hip)
|
||||
head.rotation = 0.0713012
|
||||
head.rest = Transform2D(
|
||||
Vector2(2.22127e-06, -1.0), # X-axis basis
|
||||
Vector2(1.0, 2.22127e-06), # Y-axis basis
|
||||
Vector2(0.0, -23.0) # Origin position
|
||||
)
|
||||
_create_remote_transform("RemoteTransform2D", head, "../../../../../Body/Head", Vector2(-0.498689, -6.98226))
|
||||
|
||||
# --- 3. IK TARGET CONTAINERS ---
|
||||
var ik_targets := Node2D.new()
|
||||
ik_targets.name = "IK Targets"
|
||||
sticky.add_child(ik_targets)
|
||||
|
||||
_create_target("Arm_Left", Vector2(0, 30), ik_targets)
|
||||
_create_target("Arm_Right", Vector2(4, 29), ik_targets)
|
||||
_create_target("Leg_Left", Vector2(-1, 50), ik_targets)
|
||||
_create_target("Leg_Right", Vector2(0, 51), ik_targets)
|
||||
_create_target("Head", Vector2(0, -10), ik_targets)
|
||||
|
||||
# Body target uses RemoteTransform2D to move Hip bone position
|
||||
var body_target := _create_target("Body", Vector2(-3, -4), ik_targets)
|
||||
_create_remote_transform("RemoteTransform2D", body_target, "../../../Stickman/Bones/Skeleton2D/Hip", Vector2(2, 31))
|
||||
|
||||
# --- 4. MODIFICATION STACK & SOLVERS ---
|
||||
var mod_stack := SkeletonModificationStack2D.new()
|
||||
|
||||
# Leg Left 2-Bone IK
|
||||
var leg_left_ik := SkeletonModification2DTwoBoneIK.new()
|
||||
leg_left_ik.target_nodepath = NodePath("../../../IK Targets/Leg_Left")
|
||||
leg_left_ik.joint_one_bone_idx = 1
|
||||
leg_left_ik.joint_one_bone2d_node = NodePath("Hip/Leg_Upper_Left")
|
||||
leg_left_ik.joint_two_bone_idx = 2
|
||||
leg_left_ik.joint_two_bone2d_node = NodePath("Hip/Leg_Upper_Left/Leg_Lower_Left")
|
||||
|
||||
# Leg Right 2-Bone IK
|
||||
var leg_right_ik := SkeletonModification2DTwoBoneIK.new()
|
||||
leg_right_ik.target_nodepath = NodePath("../../../IK Targets/Leg_Right")
|
||||
leg_right_ik.joint_one_bone_idx = 3
|
||||
leg_right_ik.joint_one_bone2d_node = NodePath("Hip/Leg_Upper_Right")
|
||||
leg_right_ik.joint_two_bone_idx = 4
|
||||
leg_right_ik.joint_two_bone2d_node = NodePath("Hip/Leg_Upper_Right/Leg_Lower_Right")
|
||||
|
||||
# Arm Right 2-Bone IK
|
||||
var arm_right_ik := SkeletonModification2DTwoBoneIK.new()
|
||||
arm_right_ik.target_nodepath = NodePath("../../../IK Targets/Arm_Right")
|
||||
arm_right_ik.flip_bend_direction = true
|
||||
arm_right_ik.joint_one_bone_idx = 7
|
||||
arm_right_ik.joint_one_bone2d_node = NodePath("Hip/Arm_Upper_Right")
|
||||
arm_right_ik.joint_two_bone_idx = 8
|
||||
arm_right_ik.joint_two_bone2d_node = NodePath("Hip/Arm_Upper_Right/Arm_Lower_Right")
|
||||
|
||||
# Arm Left 2-Bone IK
|
||||
var arm_left_ik := SkeletonModification2DTwoBoneIK.new()
|
||||
arm_left_ik.target_nodepath = NodePath("../../../IK Targets/Arm_Left")
|
||||
arm_left_ik.flip_bend_direction = true
|
||||
arm_left_ik.joint_one_bone_idx = 5
|
||||
arm_left_ik.joint_one_bone2d_node = NodePath("Hip/Arm_Upper_Left")
|
||||
arm_left_ik.joint_two_bone_idx = 6
|
||||
arm_left_ik.joint_two_bone2d_node = NodePath("Hip/Arm_Upper_Left/Arm_Lower_Left")
|
||||
|
||||
# Head LookAt Modifier
|
||||
var head_lookat := SkeletonModification2DLookAt.new()
|
||||
head_lookat.bone_index = 9
|
||||
head_lookat.bone2d_node = NodePath("Hip/Head")
|
||||
head_lookat.target_nodepath = NodePath("../../../IK Targets/Head")
|
||||
head_lookat.enable_constraint = true
|
||||
head_lookat.constraint_angle_min = 65.0
|
||||
head_lookat.constraint_angle_max = 295.0
|
||||
head_lookat.constraint_angle_invert = true
|
||||
head_lookat.constraint_in_localspace = true
|
||||
|
||||
mod_stack.add_modification(leg_left_ik)
|
||||
mod_stack.add_modification(leg_right_ik)
|
||||
mod_stack.add_modification(arm_right_ik)
|
||||
mod_stack.add_modification(arm_left_ik)
|
||||
mod_stack.add_modification(head_lookat)
|
||||
|
||||
skeleton.set_modification_stack(mod_stack)
|
||||
mod_stack.enabled = true
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
_set_owners(self, get_tree().edited_scene_root)
|
||||
|
||||
func _create_bone(b_name: String, pos: Vector2, parent_node: Node) -> Bone2D:
|
||||
var bone := Bone2D.new()
|
||||
bone.name = b_name
|
||||
bone.position = pos
|
||||
bone.rest = bone.transform
|
||||
parent_node.add_child(bone)
|
||||
return bone
|
||||
|
||||
func _create_target(t_name: String, pos: Vector2, parent_node: Node) -> Node2D:
|
||||
var target := Node2D.new()
|
||||
target.name = t_name
|
||||
target.position = pos
|
||||
parent_node.add_child(target)
|
||||
return target
|
||||
|
||||
func _create_remote_transform(rt_name: String, parent_node: Node, target_path: String, pos: Vector2 = Vector2.ZERO, rot: float = 0.0) -> RemoteTransform2D:
|
||||
var rt := RemoteTransform2D.new()
|
||||
rt.name = rt_name
|
||||
rt.position = pos
|
||||
rt.rotation = rot
|
||||
rt.remote_path = NodePath(target_path)
|
||||
parent_node.add_child(rt)
|
||||
return rt
|
||||
|
||||
func _set_owners(node: Node, scene_root: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.owner = scene_root
|
||||
_set_owners(child, scene_root)
|
||||
@@ -0,0 +1 @@
|
||||
uid://6bcl1j1yvpv4
|
||||
@@ -73,6 +73,8 @@ const MAX_GRID_SIZE := 100
|
||||
@onready var _error_dialog: AcceptDialog = %ErrorDialog
|
||||
@onready var _grid_config_dialog: ConfirmationDialog = %GridConfigDialog
|
||||
@onready var _grid_size_spin_box: SpinBox = %GridSizeSpinBox
|
||||
@onready var _status_cursor_coords: Label = %CursorCoords
|
||||
@onready var _status_snap_status: Label = %SnapStatus
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Grid / snap state
|
||||
@@ -84,6 +86,9 @@ var _snap_enabled: bool = false
|
||||
# Phase 5: shape clipboard
|
||||
var _shape_clipboard: Dictionary = {}
|
||||
|
||||
# Phase 6: recent colors
|
||||
var _recent_colors: Array[String] = []
|
||||
|
||||
var _edit_menu: PopupMenu
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -98,11 +103,31 @@ func _ready() -> void:
|
||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||
|
||||
_load_settings()
|
||||
_update_snap_status_label()
|
||||
_broadcast_settings()
|
||||
_broadcast_recent_colors()
|
||||
|
||||
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
||||
_grid_config_dialog.confirmed.connect(_on_grid_config_confirmed)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var mouse_pos := get_global_mouse_position()
|
||||
|
||||
for part_name: String in BODY_PART_NAMES:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel and panel.is_cursor_over_drawing(mouse_pos):
|
||||
var world_pos := panel.global_to_world(mouse_pos)
|
||||
_status_cursor_coords.text = "X: %d Y: %d" % [int(world_pos.x), int(world_pos.y)]
|
||||
return
|
||||
|
||||
if _whole_preview.is_cursor_over_preview(mouse_pos):
|
||||
var world_pos := _whole_preview.global_to_world(mouse_pos)
|
||||
_status_cursor_coords.text = "X: %d Y: %d" % [int(world_pos.x), int(world_pos.y)]
|
||||
return
|
||||
|
||||
_status_cursor_coords.text = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Menu & Dialog setup
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -162,6 +187,7 @@ func _populate_body_part_panels() -> void:
|
||||
panel.shape_selected.connect(_on_body_part_shape_selected.bind(part_name))
|
||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||
_body_part_panels[part_name] = panel
|
||||
left_col.add_child(panel)
|
||||
|
||||
@@ -173,6 +199,7 @@ func _populate_body_part_panels() -> void:
|
||||
panel.shape_selected.connect(_on_body_part_shape_selected.bind(part_name))
|
||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||
_body_part_panels[part_name] = panel
|
||||
center_col.add_child(panel)
|
||||
|
||||
@@ -201,6 +228,7 @@ func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||
_save_settings()
|
||||
_broadcast_settings()
|
||||
_update_snap_status_label()
|
||||
|
||||
|
||||
func _on_edit_menu_about_to_popup() -> void:
|
||||
@@ -448,6 +476,12 @@ func _broadcast_clipboard_state() -> void:
|
||||
(panel as BodyPartPanel).set_has_clipboard(not _shape_clipboard.is_empty())
|
||||
|
||||
|
||||
func _broadcast_recent_colors() -> void:
|
||||
for panel in _body_part_panels.values():
|
||||
if panel is BodyPartPanel:
|
||||
(panel as BodyPartPanel).set_recent_colors(_recent_colors)
|
||||
|
||||
|
||||
func _paste_shape_into_panel(part_name: String, world_pos: Vector2) -> void:
|
||||
if _shape_clipboard.is_empty():
|
||||
return
|
||||
@@ -459,6 +493,18 @@ func _paste_shape_into_panel(part_name: String, world_pos: Vector2) -> void:
|
||||
func _on_shape_paste_requested(world_pos: Vector2, part_name: String) -> void:
|
||||
_paste_shape_into_panel(part_name, world_pos)
|
||||
|
||||
|
||||
func _on_color_selected(color: Color, _part_name: String) -> void:
|
||||
var hex_str := color.to_html()
|
||||
var idx := _recent_colors.find(hex_str)
|
||||
if idx >= 0:
|
||||
_recent_colors.remove_at(idx)
|
||||
_recent_colors.insert(0, hex_str)
|
||||
while _recent_colors.size() > 8:
|
||||
_recent_colors.pop_back()
|
||||
_save_settings()
|
||||
_broadcast_recent_colors()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: Settings persistence
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -483,6 +529,15 @@ func _load_settings() -> void:
|
||||
_snap_enabled = bool(d.get("snap_to_grid", false))
|
||||
_grid_size = clampi(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
|
||||
var raw_colors: Variant = d.get("recent_colors", [])
|
||||
if raw_colors is Array:
|
||||
var arr: Array = raw_colors as Array
|
||||
for c in arr:
|
||||
if c is String:
|
||||
_recent_colors.append(c as String)
|
||||
while _recent_colors.size() > 8:
|
||||
_recent_colors.pop_back()
|
||||
|
||||
if _edit_menu:
|
||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||
|
||||
@@ -492,6 +547,7 @@ func _save_settings() -> void:
|
||||
"version": SETTINGS_VERSION,
|
||||
"grid_size": _grid_size,
|
||||
"snap_to_grid": _snap_enabled,
|
||||
"recent_colors": _recent_colors,
|
||||
}
|
||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
@@ -511,3 +567,7 @@ func _broadcast_settings() -> void:
|
||||
|
||||
func _snap_menu_label() -> String:
|
||||
return "[√] Snap to Grid" if _snap_enabled else "Snap to Grid"
|
||||
|
||||
|
||||
func _update_snap_status_label() -> void:
|
||||
_status_snap_status.text = "SNAP: ON" if _snap_enabled else "SNAP: OFF"
|
||||
|
||||
@@ -92,6 +92,9 @@ var _pan_start: Vector2 = Vector2.ZERO
|
||||
var _part_order: Array[String] = []
|
||||
var _context_menu: PopupMenu
|
||||
|
||||
# Phase 6: selection gizmos rendered on top
|
||||
var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -215,6 +218,14 @@ func reset_view() -> void:
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
func is_cursor_over_preview(global_pos: Vector2) -> bool:
|
||||
return preview_area.get_global_rect().has_point(global_pos)
|
||||
|
||||
|
||||
func global_to_world(global_pos: Vector2) -> Vector2:
|
||||
return _screen_to_world(global_pos - preview_area.global_position)
|
||||
|
||||
|
||||
func get_part_order() -> Array:
|
||||
return _part_order.duplicate()
|
||||
|
||||
@@ -249,6 +260,7 @@ func _on_preview_draw() -> void:
|
||||
preview_area.draw_set_transform(_pan_offset, 0.0, Vector2(_zoom, _zoom))
|
||||
|
||||
_draw_grid()
|
||||
_selected_gizmo_bounds = Rect2()
|
||||
|
||||
for part_name: String in _part_order:
|
||||
if not _part_shapes.has(part_name):
|
||||
@@ -305,31 +317,11 @@ func _on_preview_draw() -> void:
|
||||
var label_pos := Vector2(bounds.position.x, bounds.position.y - 14)
|
||||
preview_area.draw_string(font, label_pos, label, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color.WHITE)
|
||||
|
||||
# Phase 4: Draw gizmos for selected part
|
||||
# Store bounds for selected part (gizmos drawn on top later)
|
||||
if part_name == _selected_part:
|
||||
var bounds := _compute_transformed_bounds_from_points(all_raw, center, rot_deg, scl)
|
||||
if bounds.has_area():
|
||||
# White bounding box
|
||||
preview_area.draw_rect(bounds, Color.WHITE, false, 1.5 / _zoom)
|
||||
|
||||
# Rotation circle
|
||||
var rot_center := Vector2(bounds.position.x + bounds.size.x * 0.5, bounds.end.y + ROTATION_CIRCLE_OFFSET)
|
||||
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, Color.WHITE)
|
||||
|
||||
# Scale crosses at 4 corners
|
||||
var cross_half := SCALE_CROSS_SIZE / _zoom
|
||||
for i: int in range(4):
|
||||
var corner := _get_corner_position(bounds, i)
|
||||
# Horizontal line
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(-cross_half, 0),
|
||||
corner + Vector2(cross_half, 0),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
# Vertical line
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(0, -cross_half),
|
||||
corner + Vector2(0, cross_half),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
_selected_gizmo_bounds = bounds
|
||||
|
||||
# Highlight dragged part
|
||||
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
||||
@@ -337,6 +329,24 @@ func _on_preview_draw() -> void:
|
||||
if bounds is Rect2:
|
||||
preview_area.draw_rect(bounds as Rect2, Color(1.0, 0.8, 0.0, 0.35), false, 1.5)
|
||||
|
||||
# Draw selection gizmos on top of all parts
|
||||
if _selected_gizmo_bounds.has_area():
|
||||
var b: Rect2 = _selected_gizmo_bounds
|
||||
preview_area.draw_rect(b, Color.WHITE, false, 1.5 / _zoom)
|
||||
var rot_center := Vector2(b.position.x + b.size.x * 0.5, b.end.y + ROTATION_CIRCLE_OFFSET)
|
||||
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, Color.WHITE)
|
||||
var cross_half := SCALE_CROSS_SIZE / _zoom
|
||||
for i: int in range(4):
|
||||
var corner := _get_corner_position(b, i)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(-cross_half, 0),
|
||||
corner + Vector2(cross_half, 0),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(0, -cross_half),
|
||||
corner + Vector2(0, cross_half),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
|
||||
|
||||
func _draw_grid() -> void:
|
||||
var gs := float(_grid_size)
|
||||
@@ -455,6 +465,21 @@ func _compute_original_bounds_size(part_name: String) -> Vector2:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _on_preview_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMagnifyGesture:
|
||||
var mag := event as InputEventMagnifyGesture
|
||||
var cursor_local := preview_area.get_local_mouse_position()
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom * mag.factor, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
if event is InputEventPanGesture:
|
||||
var pan := event as InputEventPanGesture
|
||||
_pan_offset -= pan.delta * 3.0
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
|
||||
@@ -470,11 +495,17 @@ func _on_preview_gui_input(event: InputEvent) -> void:
|
||||
|
||||
# Mouse wheel -> zoom
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
||||
var cursor_local := mb.position
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
if mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
||||
var cursor_local := mb.position
|
||||
var world := _screen_to_world(cursor_local)
|
||||
_zoom = clampf(_zoom / ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||
_pan_offset = cursor_local - world * _zoom
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
@@ -495,7 +526,7 @@ func _on_preview_gui_input(event: InputEvent) -> void:
|
||||
var mm := event as InputEventMouseMotion
|
||||
|
||||
if _is_panning:
|
||||
_pan_offset += (mm.position - _pan_start) / _zoom
|
||||
_pan_offset += (mm.position - _pan_start)
|
||||
_pan_start = mm.position
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user