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:
@@ -22,6 +22,21 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
|||||||
Writes `FILE_VERSION "1.2"`; auto-migrates `"1.0"`/`"1.1"` files on load. Coordinates cross-panel
|
Writes `FILE_VERSION "1.2"`; auto-migrates `"1.0"`/`"1.1"` files on load. Coordinates cross-panel
|
||||||
selection so only one shape is selected at a time (`shape_selected` → deselect others).
|
selection so only one shape is selected at a time (`shape_selected` → deselect others).
|
||||||
Collects per-part `{shapes[], position, rotation, scale}` for save/load.
|
Collects per-part `{shapes[], position, rotation, scale}` for save/load.
|
||||||
|
- **Phase 6 recent colors:** stores `_recent_colors: Array[String]` (max 8,
|
||||||
|
most-recent-first), loads from `settings.json` (`recent_colors` key) in
|
||||||
|
`_load_settings()`, saves on each color selection via `_save_settings()`, and
|
||||||
|
broadcasts to all 10 panels via `_broadcast_recent_colors()`.
|
||||||
|
- `_on_color_selected(color, part_name)`: deduplicates, inserts the hex string at the
|
||||||
|
front, trims to 8 entries, saves settings, then broadcasts to all panels.
|
||||||
|
- `_broadcast_recent_colors()`: pushes `_recent_colors` to every panel via
|
||||||
|
`BodyPartPanel.set_recent_colors(_recent_colors)`.
|
||||||
|
- **Phase 6 status bar (cursor coords):** `_process()` polls
|
||||||
|
`get_global_mouse_position()` each frame, checks each panel via
|
||||||
|
`is_cursor_over_drawing(global_pos)` and the preview via
|
||||||
|
`is_cursor_over_preview(global_pos)`, converts to world space via
|
||||||
|
`global_to_world(global_pos)`, and writes `"X: ### Y: ###"` to `_status_cursor_coords`.
|
||||||
|
- **Phase 6 snap status:** `_status_snap_status` displays `"SNAP: ON"` / `"SNAP: OFF"`,
|
||||||
|
set in `_ready()` and re-synced when snap is toggled (`_on_edit_menu_id_pressed`).
|
||||||
- `scripts/body_part_panel.gd` — `class_name BodyPartPanel`, `extends PanelContainer`.
|
- `scripts/body_part_panel.gd` — `class_name BodyPartPanel`, `extends PanelContainer`.
|
||||||
Reusable per-part editor. Public API:
|
Reusable per-part editor. Public API:
|
||||||
- `set_shape_data(data: Variant)` — import shape data (Array or single Dictionary; used on Load/Clear)
|
- `set_shape_data(data: Variant)` — import shape data (Array or single Dictionary; used on Load/Clear)
|
||||||
@@ -31,6 +46,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
|||||||
- `deselect()` — clear selection and cancel any in-progress vertex drag
|
- `deselect()` — clear selection and cancel any in-progress vertex drag
|
||||||
- `signal shape_changed(shapes: Array)` — emitted when any shape is created, modified, deleted, or reordered
|
- `signal shape_changed(shapes: Array)` — emitted when any shape is created, modified, deleted, or reordered
|
||||||
- `signal shape_selected()` — emitted on left-click; the editor deselects all other panels
|
- `signal shape_selected()` — emitted on left-click; the editor deselects all other panels
|
||||||
|
- `signal color_selected(color: Color)` — emitted when the color is confirmed (OK button)
|
||||||
|
- `set_recent_colors(colors_hex: Array)` — clears existing ColorPicker presets and populates with the given hex colors
|
||||||
|
- `is_cursor_over_drawing(global_pos: Vector2) -> bool` — true if `global_pos` is over the drawing surface
|
||||||
|
- `global_to_world(global_pos: Vector2) -> Vector2` — maps a global position to drawing ("world") space
|
||||||
- **Phase 2 vertex editing:** left-click on a shape selects it; drag a vertex handle to
|
- **Phase 2 vertex editing:** left-click on a shape selects it; drag a vertex handle to
|
||||||
reshape in real time; with a shape selected, right-click near an outline edge offers
|
reshape in real time; with a shape selected, right-click near an outline edge offers
|
||||||
"Create Point" (inserts a vertex flagged `1` at the edge midpoint). Original vertices
|
"Create Point" (inserts a vertex flagged `1` at the edge midpoint). Original vertices
|
||||||
@@ -41,27 +60,23 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
|||||||
is active for vertex editing.
|
is active for vertex editing.
|
||||||
- **Per-panel zoom:** mouse wheel multiplies `_zoom` by 1.10, clamped to `[0.3, 3.0]`;
|
- **Per-panel zoom:** mouse wheel multiplies `_zoom` by 1.10, clamped to `[0.3, 3.0]`;
|
||||||
drawing and input hit-testing both run in world space via `draw_set_transform`.
|
drawing and input hit-testing both run in world space via `draw_set_transform`.
|
||||||
- **Drawing approach:** closedness is read from the `closed` flag (Phase 2), not
|
- **Phase 6 touchpad:** `_gui_input` handles `InputEventMagnifyGesture` (pinch zoom)
|
||||||
`shape_type`. Closed shapes are filled via `draw_colored_polygon(pts, color)` before a
|
and `InputEventPanGesture` (2-finger drag panning), both checked before
|
||||||
2 px outline is drawn with `_draw_polyline(..., closed=true)`. Open shapes render
|
`InputEventMouseButton`. Pan gesture delta is multiplied by 3.0 for speed
|
||||||
outline-only (`closed=false`). Shapes draw in array order (z-order).
|
parity with mouse panning.
|
||||||
- `scripts/whole_stickman_preview.gd` — `class_name WholeStickmanPreview`,
|
- **Phase 6 cursor-centered zoom:** both mouse wheel and pinch zoom adjust
|
||||||
`extends PanelContainer`. Assembles all parts and handles drag-to-reposition, rotation, scale.
|
`_pan_offset` so the world point under the cursor stays fixed during zoom.
|
||||||
- `set_body_parts(parts_data)`, `set_all_part_positions(pos)`, `get_part_position(name)`
|
- **Selection gizmos always on top:** bounding box, rotation circle, and scale
|
||||||
- `get_part_rotation(name) -> float`, `set_part_rotation(name, degrees)`
|
crosses for the selected part are drawn in a second pass after all parts,
|
||||||
- `get_part_scale(name) -> Vector2`, `set_part_scale(name, scale)`
|
via `_selected_gizmo_bounds`, so they always render in front.
|
||||||
- `_selected_part` (String) — part with white bounding box selected
|
|
||||||
- `_interaction` (enum: NONE, TRANSLATE, ROTATE, SCALE) — current gizmo interaction
|
|
||||||
- Rotation gizmo: filled circle centered below bounding box; Ctrl = 15° snap
|
|
||||||
- Scale gizmo: crosses at 4 corners; Ctrl = aspect ratio lock
|
|
||||||
- `signal part_moved(part_name, new_position)`
|
|
||||||
- Per-panel zoom via mouse wheel (×1.10, clamped to `[0.3, 3.0]`); drag hit-tests and
|
|
||||||
rendering run in world space.
|
|
||||||
- Scenes:
|
- Scenes:
|
||||||
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
||||||
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
||||||
`%CenterColumn`, `%WholeStickmanPreview`, `%SaveDialog`, `%LoadDialog`,
|
`%CenterColumn`, `%WholeStickmanPreview`, `%SaveDialog`, `%LoadDialog`,
|
||||||
`%ClearConfirmDialog`, `%ErrorDialog`.
|
`%ClearConfirmDialog`, `%ErrorDialog`, `%StatusBar`, `%CursorCoords`, `%SnapStatus`.
|
||||||
|
- **Phase 6 status bar:** `StatusBar` is an `HBoxContainer` bottom-anchored at 28 px
|
||||||
|
height containing `CursorCoords` (left) and `SnapStatus` (right); `MainLayout`
|
||||||
|
`offset_bottom` is `-28.0` to leave room for it.
|
||||||
- `scenes/body_part_panel.tscn` — instantiated 10× at runtime (5 per column). Each panel
|
- `scenes/body_part_panel.tscn` — instantiated 10× at runtime (5 per column). Each panel
|
||||||
sets `size_flags_vertical = SIZE_EXPAND_FILL` so the panels expand to fill the column
|
sets `size_flags_vertical = SIZE_EXPAND_FILL` so the panels expand to fill the column
|
||||||
height in their parent VBoxContainer.
|
height in their parent VBoxContainer.
|
||||||
@@ -83,6 +98,13 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
|||||||
- The JSON `.stk` format is defined in `README.md` (versioned `"1.2"`, extensible;
|
- The JSON `.stk` format is defined in `README.md` (versioned `"1.2"`, extensible;
|
||||||
`"1.0"`/`"1.1"` files auto-migrate on load).
|
`"1.0"`/`"1.1"` files auto-migrate on load).
|
||||||
|
|
||||||
|
### settings.json (Phase 6)
|
||||||
|
- Persisted editor preferences written to `settings.json` via `_save_settings()` and
|
||||||
|
loaded in `_load_settings()`.
|
||||||
|
- Keys: `version`, `grid_size`, `snap_to_grid`, `recent_colors`.
|
||||||
|
- `recent_colors: Array[String]` — the last up-to-8 selected hex colors, most-recent-first.
|
||||||
|
Populated on load and flushed on every color selection.
|
||||||
|
|
||||||
### Legacy scene (do not delete)
|
### Legacy scene (do not delete)
|
||||||
- `stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets +
|
- `stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets +
|
||||||
`RemoteTransform2D` + `Line2D` limbs, plus an embedded `@tool` script drawing the head
|
`RemoteTransform2D` + `Line2D` limbs, plus an embedded `@tool` script drawing the head
|
||||||
|
|||||||
@@ -35,3 +35,14 @@
|
|||||||
### Snap to grid when moving an object
|
### Snap to grid when moving an object
|
||||||
|
|
||||||
1. Currently snapping to the grid snaps the mouse cursor to the grid, however, that can lead to object still not lining up properly. The object's bounding box should be what snaps to the grid. If a reference point is needed for snapping, use the object's upper left point of the bounding box.
|
1. Currently snapping to the grid snaps the mouse cursor to the grid, however, that can lead to object still not lining up properly. The object's bounding box should be what snaps to the grid. If a reference point is needed for snapping, use the object's upper left point of the bounding box.
|
||||||
|
|
||||||
|
## Stickman editor (Phase 6 Round 1)
|
||||||
|
|
||||||
|
### Seletion bounding box as well as rotation 'dot' should stay in front of all other objects.
|
||||||
|
If an object is selected, it's bounding box and rotation dot should be in front of all other objects.
|
||||||
|
|
||||||
|
### Touchpad panning is too slow
|
||||||
|
Panning using the touchpad does not moves too slowly. We need to increase the distance the panning moves through touchpad.
|
||||||
|
|
||||||
|
### Zooming in/out
|
||||||
|
When zooming in, we should use the mouse cursor as the zoom in point. If the cursor is in the window should determine where the zoom should happen.
|
||||||
+21
@@ -265,6 +265,27 @@ The .stk file should account for the new attributes introduced in this phase.
|
|||||||
- Object's mirror status?
|
- Object's mirror status?
|
||||||
- Shape's mirror status?
|
- Shape's mirror status?
|
||||||
|
|
||||||
|
## Stickman editor (Phase 6)
|
||||||
|
|
||||||
|
### Touchpad controls
|
||||||
|
Currently the mouse handles most of the controls. We need to adapt the controls to a touchpad as well. We should make the following adaptions:
|
||||||
|
- Panning in each window is handled by holding down the middle mouse button and moving the mouse. This should be done on a touchpad by using a 2 finger drag.
|
||||||
|
- Zoom in handled by the scroll wheel on the mouse. It should also be handled by pinch zooming on the touchpad.
|
||||||
|
|
||||||
|
### Color picker recent colors
|
||||||
|
The color picker currently is not storing recent colors. It should keep track of at least 8 colors that were used previously in the project so that the user can reuse colors.
|
||||||
|
|
||||||
|
### Bottom of screen status bar
|
||||||
|
Just like the menu bar at the top of the screen, the bottom of the screen should have a status bar that will hold information about the project, current operation, etc. It should be easily readable
|
||||||
|
|
||||||
|
### Pixel (x, y) display on the status bar
|
||||||
|
In the status bar described above, we should display the current coordinates of the cursor in the current window in pixels. The coordinates should be based off of which window the cursor is currently over. Make sure to take into account panning and zooming, so the windows should probably have a set coordinate limit. Maybe (0, 0) to (large x, large y) This coordinate display should be on the left side of the status bar.
|
||||||
|
|
||||||
|
### Snap status
|
||||||
|
In the status bar mentioned above, there should a display that shows if snapping is on or off. Label it as "SNAP: OFF" or "SNAP: ON". That display should be on the right side of the status bar.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. Try to use standard Godot controls when applicable
|
1. Try to use standard Godot controls when applicable
|
||||||
|
|||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dr5ef3l2var2s"]
|
||||||
|
|
||||||
|
[sub_resource type="GDScript" id="GDScript_f0s26"]
|
||||||
|
script/source = "@tool
|
||||||
|
extends Node2D
|
||||||
|
@export var radius : float = 100.0:
|
||||||
|
set(value):
|
||||||
|
radius = value
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
@export var color : Color = Color.WHITE:
|
||||||
|
set(value):
|
||||||
|
color = value
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw():
|
||||||
|
draw_circle(Vector2(0,0), radius, color)
|
||||||
|
"
|
||||||
|
|
||||||
|
[sub_resource type="SkeletonModification2DTwoBoneIK" id="SkeletonModification2DTwoBoneIK_yvxej"]
|
||||||
|
target_nodepath = NodePath("../IK_Targets/Right_Hand")
|
||||||
|
flip_bend_direction = true
|
||||||
|
joint_one_bone_idx = 4
|
||||||
|
joint_one_bone2d_node = NodePath("Torso/RightUpperArm")
|
||||||
|
joint_two_bone_idx = 5
|
||||||
|
joint_two_bone2d_node = NodePath("Torso/RightUpperArm/RightLowerArm")
|
||||||
|
|
||||||
|
[sub_resource type="SkeletonModification2DTwoBoneIK" id="SkeletonModification2DTwoBoneIK_f0s26"]
|
||||||
|
target_nodepath = NodePath("../IK_Targets/Left_Hand")
|
||||||
|
joint_one_bone_idx = 2
|
||||||
|
joint_one_bone2d_node = NodePath("Torso/LeftUpperArm")
|
||||||
|
joint_two_bone_idx = 3
|
||||||
|
joint_two_bone2d_node = NodePath("Torso/LeftUpperArm/LeftLowerArm")
|
||||||
|
|
||||||
|
[sub_resource type="SkeletonModificationStack2D" id="SkeletonModificationStack2D_j4hao"]
|
||||||
|
enabled = true
|
||||||
|
modification_count = 2
|
||||||
|
modifications/0 = SubResource("SkeletonModification2DTwoBoneIK_yvxej")
|
||||||
|
modifications/1 = SubResource("SkeletonModification2DTwoBoneIK_f0s26")
|
||||||
|
|
||||||
|
[node name="Master" type="Node2D" unique_id=319103476]
|
||||||
|
|
||||||
|
[node name="Body" type="Node2D" parent="." unique_id=327811712]
|
||||||
|
|
||||||
|
[node name="Head" type="Node2D" parent="Body" unique_id=864822355]
|
||||||
|
position = Vector2(704, 104)
|
||||||
|
scale = Vector2(0.99999964, 0.99999964)
|
||||||
|
script = SubResource("GDScript_f0s26")
|
||||||
|
|
||||||
|
[node name="Body" type="Line2D" parent="Body" unique_id=1290443463]
|
||||||
|
position = Vector2(704, 184)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 400)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="LeftUpperLeg" type="Line2D" parent="Body" unique_id=199373156]
|
||||||
|
position = Vector2(703.99994, 576)
|
||||||
|
rotation = 0.5235988
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="RightUpperLeg" type="Line2D" parent="Body" unique_id=1396556308]
|
||||||
|
position = Vector2(703.99994, 576)
|
||||||
|
rotation = -0.5235988
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="LeftLowerLeg" type="Line2D" parent="Body" unique_id=1590485904]
|
||||||
|
position = Vector2(608, 744)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="RightLowerLeg" type="Line2D" parent="Body" unique_id=29203948]
|
||||||
|
position = Vector2(800, 744)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="LeftUpperArm" type="Line2D" parent="Body" unique_id=744524157]
|
||||||
|
position = Vector2(704, 328)
|
||||||
|
rotation = 1.4909883
|
||||||
|
scale = Vector2(0.9999922, 0.9999922)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 175)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="RightUpperArm" type="Line2D" parent="Body" unique_id=1593646765]
|
||||||
|
position = Vector2(704, 328)
|
||||||
|
rotation = -1.4639877
|
||||||
|
scale = Vector2(0.99998343, 0.99998343)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 175)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="LeftLowerArm" type="Line2D" parent="Body" unique_id=2142117097]
|
||||||
|
position = Vector2(536.536, 341.3934)
|
||||||
|
rotation = 3.0600982
|
||||||
|
scale = Vector2(0.99998534, 0.99998534)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="RightLowerArm" type="Line2D" parent="Body" unique_id=1200980480]
|
||||||
|
position = Vector2(871.03986, 345.90945)
|
||||||
|
rotation = -2.8942726
|
||||||
|
scale = Vector2(0.9999745, 0.9999745)
|
||||||
|
points = PackedVector2Array(0, 0, 0, 200)
|
||||||
|
width = 16.0
|
||||||
|
default_color = Color(0.445488, 0.445488, 0.445488, 1)
|
||||||
|
|
||||||
|
[node name="Skeleton2D" type="Skeleton2D" parent="." unique_id=1854735445]
|
||||||
|
modification_stack = SubResource("SkeletonModificationStack2D_j4hao")
|
||||||
|
|
||||||
|
[node name="Torso" type="Bone2D" parent="Skeleton2D" unique_id=744339938]
|
||||||
|
position = Vector2(704, 576)
|
||||||
|
rest = Transform2D(1, 0, 0, 1, 704, 576)
|
||||||
|
|
||||||
|
[node name="Head" type="Bone2D" parent="Skeleton2D/Torso" unique_id=1487357366]
|
||||||
|
position = Vector2(0, -392)
|
||||||
|
rest = Transform2D(1, 0, 0, 1, 0, -392)
|
||||||
|
|
||||||
|
[node name="LeftUpperArm" type="Bone2D" parent="Skeleton2D/Torso" unique_id=1840957808]
|
||||||
|
position = Vector2(0, -248)
|
||||||
|
rotation = -0.7853982
|
||||||
|
scale = Vector2(0.9999999, 0.9999999)
|
||||||
|
rest = Transform2D(1, 0, 0, 1, -168, -248)
|
||||||
|
metadata/_local_pose_override_enabled_ = true
|
||||||
|
|
||||||
|
[node name="LeftLowerArm" type="Bone2D" parent="Skeleton2D/Torso/LeftUpperArm" unique_id=721380545]
|
||||||
|
position = Vector2(-118.79398, -118.793945)
|
||||||
|
rotation = -1.5707964
|
||||||
|
scale = Vector2(0.9999999, 0.9999999)
|
||||||
|
rest = Transform2D(1, 0, 0, 1, -136, -136)
|
||||||
|
auto_calculate_length_and_angle = false
|
||||||
|
length = 200.0
|
||||||
|
bone_angle = 0.0
|
||||||
|
metadata/_local_pose_override_enabled_ = true
|
||||||
|
|
||||||
|
[node name="RemoteTransform2D" type="RemoteTransform2D" parent="Skeleton2D/Torso/LeftUpperArm/LeftLowerArm" unique_id=1189277122]
|
||||||
|
position = Vector2(3.0517578e-05, 1.9073486e-06)
|
||||||
|
rotation = 4.712389
|
||||||
|
remote_path = NodePath("../../../../../Body/LeftLowerArm")
|
||||||
|
|
||||||
|
[node name="RemoteTransform2D" type="RemoteTransform2D" parent="Skeleton2D/Torso/LeftUpperArm" unique_id=1110605862]
|
||||||
|
rotation = 2.3561945
|
||||||
|
remote_path = NodePath("../../../../Body/LeftUpperArm")
|
||||||
|
|
||||||
|
[node name="RightUpperArm" type="Bone2D" parent="Skeleton2D/Torso" unique_id=36616186]
|
||||||
|
position = Vector2(0, -248)
|
||||||
|
scale = Vector2(0.9999998, 0.9999998)
|
||||||
|
rest = Transform2D(0.9999999, 0, 0, 0.9999999, 0, -248)
|
||||||
|
metadata/_local_pose_override_enabled_ = true
|
||||||
|
|
||||||
|
[node name="RightLowerArm" type="Bone2D" parent="Skeleton2D/Torso/RightUpperArm" unique_id=1018820563]
|
||||||
|
position = Vector2(168, 0)
|
||||||
|
rotation = -0.7853978
|
||||||
|
scale = Vector2(0.9999999, 0.9999999)
|
||||||
|
rest = Transform2D(-0.9999999, 1.5099579e-07, -1.5099579e-07, -0.9999999, 168, 0)
|
||||||
|
auto_calculate_length_and_angle = false
|
||||||
|
length = 200.0
|
||||||
|
bone_angle = 0.0
|
||||||
|
metadata/_local_pose_override_enabled_ = true
|
||||||
|
|
||||||
|
[node name="RemoteTransform2D" type="RemoteTransform2D" parent="Skeleton2D/Torso/RightUpperArm/RightLowerArm" unique_id=1282597655]
|
||||||
|
rotation = -1.5707964
|
||||||
|
remote_path = NodePath("../../../../../Body/RightLowerArm")
|
||||||
|
|
||||||
|
[node name="RemoteTransform2D" type="RemoteTransform2D" parent="Skeleton2D/Torso/RightUpperArm" unique_id=975221467]
|
||||||
|
rotation = -1.5707964
|
||||||
|
remote_path = NodePath("../../../../Body/RightUpperArm")
|
||||||
|
|
||||||
|
[node name="IK_Targets" type="Node2D" parent="." unique_id=1089334064]
|
||||||
|
|
||||||
|
[node name="Right_Hand" type="Marker2D" parent="IK_Targets" unique_id=1785254130]
|
||||||
|
position = Vector2(920, 152)
|
||||||
|
|
||||||
|
[node name="Left_Hand" type="Marker2D" parent="IK_Targets" unique_id=1085197042]
|
||||||
|
position = Vector2(496, 144)
|
||||||
+5
-1
@@ -8,9 +8,13 @@
|
|||||||
|
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
|
[animation]
|
||||||
|
|
||||||
|
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="stickman"
|
config/name="stickman"
|
||||||
run/main_scene="uid://c5jkoyu1ik6fo"
|
run/main_scene="uid://c5jkoyu1ik6fo"
|
||||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ anchors_preset = 15
|
|||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_top = 68.0
|
offset_top = 68.0
|
||||||
|
offset_bottom = -28.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
@@ -149,3 +150,29 @@ max_value = 100.0
|
|||||||
value = 15.0
|
value = 15.0
|
||||||
step = 1.0
|
step = 1.0
|
||||||
rounded = true
|
rounded = true
|
||||||
|
|
||||||
|
[node name="StatusBar" type="HBoxContainer" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchor_left = 0.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = -28.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
|
||||||
|
[node name="CursorCoords" type="Label" parent="StatusBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = ""
|
||||||
|
horizontal_alignment = 0
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="SnapStatus" type="Label" parent="StatusBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "SNAP: OFF"
|
||||||
|
horizontal_alignment = 2
|
||||||
|
vertical_alignment = 1
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ signal shape_changed(shape_data: Array)
|
|||||||
signal shape_selected()
|
signal shape_selected()
|
||||||
signal shape_copy_requested(shape_dict: Dictionary)
|
signal shape_copy_requested(shape_dict: Dictionary)
|
||||||
signal shape_paste_requested(world_pos: Vector2)
|
signal shape_paste_requested(world_pos: Vector2)
|
||||||
|
signal color_selected(color: Color)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Exported / public properties
|
# Exported / public properties
|
||||||
@@ -269,6 +270,13 @@ func set_has_clipboard(state: bool) -> void:
|
|||||||
_has_clipboard = state
|
_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:
|
func paste_shape(source: Dictionary, target_pos: Vector2) -> void:
|
||||||
## Insert a deep copy of the clipboard shape at the given world position.
|
## 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.
|
## 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()
|
drawing_area.queue_redraw()
|
||||||
_emit_changed()
|
_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
|
# 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:
|
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:
|
if event is InputEventMouseButton:
|
||||||
var mb := event as InputEventMouseButton
|
var mb := event as InputEventMouseButton
|
||||||
|
|
||||||
@@ -442,11 +474,17 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
# 2. Mouse wheel -> zoom
|
# 2. Mouse wheel -> zoom
|
||||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
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)
|
_zoom = clampf(_zoom * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
_pan_offset = cursor_local - world * _zoom
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
return
|
return
|
||||||
if mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
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)
|
_zoom = clampf(_zoom / ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
_pan_offset = cursor_local - world * _zoom
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -468,7 +506,7 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
|||||||
var mm := event as InputEventMouseMotion
|
var mm := event as InputEventMouseMotion
|
||||||
|
|
||||||
if _is_panning:
|
if _is_panning:
|
||||||
_pan_offset += (mm.position - _pan_start) / _zoom
|
_pan_offset += (mm.position - _pan_start)
|
||||||
_pan_start = mm.position
|
_pan_start = mm.position
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
return
|
return
|
||||||
@@ -878,6 +916,7 @@ func _on_color_picker_changed(new_color: Color) -> void:
|
|||||||
|
|
||||||
func _on_color_picker_ok() -> void:
|
func _on_color_picker_ok() -> void:
|
||||||
color_picker_popup.hide()
|
color_picker_popup.hide()
|
||||||
|
color_selected.emit(color_picker.color)
|
||||||
_emit_changed()
|
_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 _error_dialog: AcceptDialog = %ErrorDialog
|
||||||
@onready var _grid_config_dialog: ConfirmationDialog = %GridConfigDialog
|
@onready var _grid_config_dialog: ConfirmationDialog = %GridConfigDialog
|
||||||
@onready var _grid_size_spin_box: SpinBox = %GridSizeSpinBox
|
@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
|
# Grid / snap state
|
||||||
@@ -84,6 +86,9 @@ var _snap_enabled: bool = false
|
|||||||
# Phase 5: shape clipboard
|
# Phase 5: shape clipboard
|
||||||
var _shape_clipboard: Dictionary = {}
|
var _shape_clipboard: Dictionary = {}
|
||||||
|
|
||||||
|
# Phase 6: recent colors
|
||||||
|
var _recent_colors: Array[String] = []
|
||||||
|
|
||||||
var _edit_menu: PopupMenu
|
var _edit_menu: PopupMenu
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -98,11 +103,31 @@ func _ready() -> void:
|
|||||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||||
|
|
||||||
_load_settings()
|
_load_settings()
|
||||||
|
_update_snap_status_label()
|
||||||
_broadcast_settings()
|
_broadcast_settings()
|
||||||
|
_broadcast_recent_colors()
|
||||||
|
|
||||||
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
||||||
_grid_config_dialog.confirmed.connect(_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
|
# 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_selected.connect(_on_body_part_shape_selected.bind(part_name))
|
||||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.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.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
|
_body_part_panels[part_name] = panel
|
||||||
left_col.add_child(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_selected.connect(_on_body_part_shape_selected.bind(part_name))
|
||||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.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.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
|
_body_part_panels[part_name] = panel
|
||||||
center_col.add_child(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())
|
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||||
_save_settings()
|
_save_settings()
|
||||||
_broadcast_settings()
|
_broadcast_settings()
|
||||||
|
_update_snap_status_label()
|
||||||
|
|
||||||
|
|
||||||
func _on_edit_menu_about_to_popup() -> void:
|
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())
|
(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:
|
func _paste_shape_into_panel(part_name: String, world_pos: Vector2) -> void:
|
||||||
if _shape_clipboard.is_empty():
|
if _shape_clipboard.is_empty():
|
||||||
return
|
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:
|
func _on_shape_paste_requested(world_pos: Vector2, part_name: String) -> void:
|
||||||
_paste_shape_into_panel(part_name, world_pos)
|
_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
|
# Phase 3: Settings persistence
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -483,6 +529,15 @@ func _load_settings() -> void:
|
|||||||
_snap_enabled = bool(d.get("snap_to_grid", false))
|
_snap_enabled = bool(d.get("snap_to_grid", false))
|
||||||
_grid_size = clampi(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
_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:
|
if _edit_menu:
|
||||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||||
|
|
||||||
@@ -492,6 +547,7 @@ func _save_settings() -> void:
|
|||||||
"version": SETTINGS_VERSION,
|
"version": SETTINGS_VERSION,
|
||||||
"grid_size": _grid_size,
|
"grid_size": _grid_size,
|
||||||
"snap_to_grid": _snap_enabled,
|
"snap_to_grid": _snap_enabled,
|
||||||
|
"recent_colors": _recent_colors,
|
||||||
}
|
}
|
||||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||||
if file:
|
if file:
|
||||||
@@ -511,3 +567,7 @@ func _broadcast_settings() -> void:
|
|||||||
|
|
||||||
func _snap_menu_label() -> String:
|
func _snap_menu_label() -> String:
|
||||||
return "[√] Snap to Grid" if _snap_enabled else "Snap to Grid"
|
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 _part_order: Array[String] = []
|
||||||
var _context_menu: PopupMenu
|
var _context_menu: PopupMenu
|
||||||
|
|
||||||
|
# Phase 6: selection gizmos rendered on top
|
||||||
|
var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public API
|
# Public API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -215,6 +218,14 @@ func reset_view() -> void:
|
|||||||
preview_area.queue_redraw()
|
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:
|
func get_part_order() -> Array:
|
||||||
return _part_order.duplicate()
|
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))
|
preview_area.draw_set_transform(_pan_offset, 0.0, Vector2(_zoom, _zoom))
|
||||||
|
|
||||||
_draw_grid()
|
_draw_grid()
|
||||||
|
_selected_gizmo_bounds = Rect2()
|
||||||
|
|
||||||
for part_name: String in _part_order:
|
for part_name: String in _part_order:
|
||||||
if not _part_shapes.has(part_name):
|
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)
|
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)
|
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:
|
if part_name == _selected_part:
|
||||||
var bounds := _compute_transformed_bounds_from_points(all_raw, center, rot_deg, scl)
|
var bounds := _compute_transformed_bounds_from_points(all_raw, center, rot_deg, scl)
|
||||||
if bounds.has_area():
|
if bounds.has_area():
|
||||||
# White bounding box
|
_selected_gizmo_bounds = bounds
|
||||||
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)
|
|
||||||
|
|
||||||
# Highlight dragged part
|
# Highlight dragged part
|
||||||
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
||||||
@@ -337,6 +329,24 @@ func _on_preview_draw() -> void:
|
|||||||
if bounds is Rect2:
|
if bounds is Rect2:
|
||||||
preview_area.draw_rect(bounds as Rect2, Color(1.0, 0.8, 0.0, 0.35), false, 1.5)
|
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:
|
func _draw_grid() -> void:
|
||||||
var gs := float(_grid_size)
|
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:
|
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:
|
if event is InputEventMouseButton:
|
||||||
var mb := event as InputEventMouseButton
|
var mb := event as InputEventMouseButton
|
||||||
|
|
||||||
@@ -470,11 +495,17 @@ func _on_preview_gui_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
# Mouse wheel -> zoom
|
# Mouse wheel -> zoom
|
||||||
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
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)
|
_zoom = clampf(_zoom * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
_pan_offset = cursor_local - world * _zoom
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
return
|
return
|
||||||
if mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
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)
|
_zoom = clampf(_zoom / ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
|
_pan_offset = cursor_local - world * _zoom
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -495,7 +526,7 @@ func _on_preview_gui_input(event: InputEvent) -> void:
|
|||||||
var mm := event as InputEventMouseMotion
|
var mm := event as InputEventMouseMotion
|
||||||
|
|
||||||
if _is_panning:
|
if _is_panning:
|
||||||
_pan_offset += (mm.position - _pan_start) / _zoom
|
_pan_offset += (mm.position - _pan_start)
|
||||||
_pan_start = mm.position
|
_pan_start = mm.position
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user