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:
2026-08-14 21:49:06 -04:00
parent b97bc145e4
commit 28e8798021
11 changed files with 647 additions and 42 deletions
+40 -1
View File
@@ -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()
+207
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://6bcl1j1yvpv4
+60
View File
@@ -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"
+54 -23
View File
@@ -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