feat: Implement facing profiles and bend direction toggles in test harness
- Added functionality for skeleton IK bone switches, allowing users to dynamically change the bend direction of joints via a context menu. - Introduced facing profiles (LEFT, RIGHT, FORWARD) that modify the bend direction of limbs based on the selected profile. - Implemented body part z-ordering based on the facing profile, ensuring correct rendering order of limbs relative to the torso. - Added a coordinates display panel that shows the position and rotation of the Skeleton2D and its bones, as well as IK target positions. - Created a new script to generate a walk animation for the stickman rig, including keyframe tracks for various IK targets. - Updated documentation to reflect the new features and their specifications.
This commit is contained in:
@@ -256,6 +256,78 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
overlay also draws a **null-guarded semi-transparent yellow aim line** from the Head bone
|
||||
origin to the head marker (`_draw_ik_handles`, width `1.5/zoom`, alpha `0.5`) — a visual aid
|
||||
for the LookAt test.
|
||||
- **Phase 9 Task 1 skeleton IK bone switches:** adds a "Facing" `MenuButton` (leftmost control
|
||||
in the top-bar `HBox`) and per-joint bend-direction toggles for the rig's TwoBoneIK "Flip
|
||||
Bend Direction" flags. `enum FacingProfile { LEFT, RIGHT, FORWARD }` (values used directly as
|
||||
menu item ids); `BEND_JOINTS: Array[String] = ["LeftArm","RightArm","LeftLeg","RightLeg"]`
|
||||
with `BEND_JOINT_BONE_PATHS` (each joint → its lower `Bone2D` NodePath relative to
|
||||
`Skeleton2D`, e.g. `Torso/LeftUpperArm/LeftLowerArm`); `PROFILE_FLAGS` maps each profile to a
|
||||
per-joint `flip_bend_direction` set (LEFT: arms off / legs on; RIGHT: arms on / legs off;
|
||||
FORWARD: LeftArm off, RightArm on, LeftLeg on, RightLeg off — matching `master_rig.tscn`'s
|
||||
authored defaults). State `_facing_profile` (default `FORWARD`, harness-level, persists across
|
||||
spawns), `_context_joint`, `_bend_joint_bones`, `_bend_modifications`, `_facing_button`/
|
||||
`_facing_menu`, `_context_menu`. The Facing popup (Left/Right/Forward) uses dynamic text-only
|
||||
labels with the current profile prefixed `[√] `, refreshed on `about_to_popup` and after
|
||||
selection (mirroring the editor's snap-menu pattern). Runtime resolution:
|
||||
`_resolve_bend_joints()` (called from `_resolve_rig_nodes()`) resolves the 4 lower-limb
|
||||
`Bone2D`s via `get_node_or_null` and matches each `SkeletonModification2DTwoBoneIK` in the
|
||||
modification stack by its `joint_two_bone2d_node` NodePath (no hardcoded stack index);
|
||||
`push_warning` on missing nodes/mods. Per-joint toggle: right-click inside the viewport on an
|
||||
elbow/knee (the upper↔lower limb connector, within `JOINT_HIT_RADIUS_PX := 14.0` screen px
|
||||
converted to world by `_camera.zoom.x`, nearest joint wins) pops a one-item context menu
|
||||
labeled **"Normal Bend"** (when `flip_bend_direction` is currently true) or **"Invert Bend"**
|
||||
(when false); selecting toggles that joint's `flip_bend_direction` on the live TwoBoneIK
|
||||
modification. Only the 4 elbows/knees are right-click targets — shoulders/hips/wrists/ankles/
|
||||
head/torso are not. Lifecycle: `_free_current_rig()` clears `_bend_joint_bones`/
|
||||
`_bend_modifications`/`_context_joint`; `_load_and_spawn()` re-applies
|
||||
`_apply_facing_profile(_facing_profile)` right after `_ensure_modification_stack_enabled()` so
|
||||
every fresh spawn matches the current profile. No persistence to disk.
|
||||
- **Phase 9 Task 2 body-part z-order:** `_apply_facing_profile()` now also sets the
|
||||
`_facing_profile` state itself (previously only the menu handler did) and calls
|
||||
`_apply_body_z_order()`, so bend flags + draw order stay in sync from one entry point.
|
||||
`const BODY_CONTAINER_PATH := "Body"` and `const Z_ORDER_BY_PROFILE: Dictionary` map each
|
||||
`FacingProfile` to the rig `Body/*` visual part node names in **back-to-front draw order**
|
||||
(Godot 4 `Node2D` draws siblings in tree order; all parts keep `z_index = 0`). FORWARD:
|
||||
torso → left/right upper legs → left/right lower legs → left/right upper arms → left/right
|
||||
lower arms → head (all limbs in front of the torso); LEFT: left arm pair then left leg
|
||||
pair **behind** the torso, right leg pair then right arm pair in front; RIGHT: mirrored.
|
||||
In every profile upper limbs stay behind lower limbs; far-side (behind-torso) arms draw
|
||||
behind the legs while near-side arms draw in front of the legs; the **head is always
|
||||
frontmost**. State `_body_container: Node2D` (resolved in
|
||||
`_resolve_rig_nodes()` via `get_node_or_null`, null-guarded with `push_warning`, cleared
|
||||
in `_free_current_rig()`). `_apply_body_z_order()` walks the profile array back-to-front
|
||||
and `move_child(part, count - 1)`s each existing child (missing parts skipped), which
|
||||
yields the profile order; unknown extra children stay at the back. Safe with the adapter:
|
||||
shapes are children of the part nodes, so moving a part moves its whole shape group. No
|
||||
persistence to disk.
|
||||
- **Phase 9 Task 3 coordinates display:** adds a "Show Coords" `CheckBox` in the top-bar
|
||||
`HBox` (after "Show IK Handles", before the status label), default ON, toggled via
|
||||
`_on_show_coords_toggled(pressed)` (sets `_show_coords: bool = true`, flips
|
||||
`_coords_panel.visible`). A code-built right-side readout (`_build_coords_panel()`, called
|
||||
from `_build_ui()` after the viewport container so it renders in front): `_coords_panel`
|
||||
(`PanelContainer`) + monospace selectable `_coords_label` (`RichTextLabel`,
|
||||
`selection_enabled` + `context_menu_enabled` + `fit_content`, autowrap off, scroll off,
|
||||
`FOCUS_CLICK`; `SystemFont`: Consolas/Menlo/DejaVu Sans Mono/Courier New via the
|
||||
`normal_font`/`normal_font_size` (18) theme overrides), anchored `PRESET_TOP_RIGHT`,
|
||||
`offset_top = 40.0`, `offset_right = -8.0`, `offset_left = -COORDS_PANEL_WIDTH` (320.0),
|
||||
`grow_vertical = GROW_DIRECTION_END` + `grow_horizontal = GROW_DIRECTION_BEGIN`
|
||||
(auto-height/width, grows left so text never runs off-screen), `mouse_filter =
|
||||
MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for selection);
|
||||
StyleBoxFlat
|
||||
bg `Color(0,0,0,0.55)`, border `Color(1,1,1,0.12)` w1, corner radius 4, content margin 8.
|
||||
Consts `COORDS_PANEL_WIDTH := 320.0` and `COORD_BONE_PATHS` (10 Skeleton2D-relative bone
|
||||
paths: Torso, Torso/Head, both upper/lower arms, both upper/lower legs). State
|
||||
`_coord_bones: Dictionary` (bone display name → `Bone2D`, keyed by `path.get_file()`),
|
||||
`_show_coords`. `_resolve_rig_nodes()` calls `_resolve_coord_bones()` (via
|
||||
`_skeleton.get_node_or_null`, `push_warning` on missing); `_free_current_rig()` clears
|
||||
`_coord_bones` (toggle persists across respawns). `_process(delta)` early-outs when hidden
|
||||
or label null, else `_update_coords_display()` — sections "Skeleton2D" pos + rot deg,
|
||||
"Bones" pos + rot deg, "IK Targets" pos only (reusing `IK_HANDLE_PATHS`/`_ik_handles`);
|
||||
"No rig loaded" fallback; every read guarded with `is_instance_valid`; the label text is
|
||||
only reassigned when the built string changes, so an active text selection survives idle
|
||||
frames. Values are
|
||||
world-space (`global_position`/`global_rotation`), rotation in degrees via
|
||||
`_fmt_deg(rad)` (1 decimal, `°`), `_fmt_vec2(v)` for positions. No persistence to disk.
|
||||
- Scenes:
|
||||
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
||||
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
||||
@@ -269,7 +341,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
height in their parent VBoxContainer.
|
||||
- `scenes/test_harness.tscn` — **standalone staging scene** (Phase 9, not wired into the
|
||||
editor; run via **F6**). Backed by `scripts/test_harness.gd`. Top UI bar with file open /
|
||||
quick-select, "Show Bones" / "Show IK Handles" toggles, and a loaded-filename status label;
|
||||
quick-select, "Show Bones" / "Show IK Handles" / "Show Coords" toggles, and a
|
||||
loaded-filename status label;
|
||||
`SubViewport` world with an enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on
|
||||
spawn); interactive limb IK via `SkeletonModificationStack2D` TwoBoneIK.
|
||||
|
||||
|
||||
@@ -260,6 +260,9 @@ The factory is the intended runtime API: `StickmanFactory.spawn("res://stickmen/
|
||||
- **Loaded filename** — status label showing the currently loaded file.
|
||||
- **Pan / zoom** — middle-mouse drag to pan, mouse-wheel to zoom the `Camera2D`; the camera recenters on each spawn.
|
||||
- **IK drag** — click and drag any of the **6** `Marker2D` IK handles (`IK_Targets/Left_Hand`, `Right_Hand`, `Left_Leg`, `Right_Leg` flex the limb via TwoBoneIK; `IK_Targets/Torso` translates the whole rig rigidly via its `RemoteTransform2D`; `IK_Targets/Head` drives the head's `SkeletonModification2DLookAt` aim rotation) (Phase 9 Round 7). The harness enables the modification stack after each spawn.
|
||||
- **Facing** — a `MenuButton` (leftmost in the top bar) applying a preset to the rig's TwoBoneIK **Flip Bend Direction** flags: **Left** (arms normal, legs inverted), **Right** (arms inverted, legs normal), **Forward** (left arm / right leg inverted — the authored defaults). The current profile is prefixed `[√] ` on the menu labels and persists across rig loads (Phase 9 Task 1).
|
||||
- **Bend-direction toggle** — right-click an elbow or knee joint in the viewport to pop a context menu that inverts that joint's TwoBoneIK bend direction ("Invert Bend" → "Normal Bend" and back). Only the 4 elbows/knees are targets (Phase 9 Task 1).
|
||||
- **Body-part z-order** — the Facing profile also reorders the rig's `Body/*` visual part nodes (tree order = draw order): **Forward** draws all limbs in front of the torso, **Left** tucks the left arm/leg pairs behind the torso (right pairs in front), **Right** tucks the right pairs behind; upper limbs sit behind lower limbs, far-side (behind-torso) arms draw behind the legs while near-side arms draw in front of them, and the **head is always frontmost** (Phase 9 Task 2).
|
||||
|
||||
Debug overlay (a world-space `Node2D` `_draw()`): true bone **segments** drawn between each `Bone2D` origin and its Bone2D children (color-coded left cyan / right orange / central white, with a joint dot per bone), with limb leaf bones drawn out to their IK targets so the forearm/shin segments and wrist/ankle joints are visible (Phase 9 Round 2) and the **Head** leaf drawn along the bone's own direction (~90 px, since its IK target is a LookAt aim point, not a joint) (Phase 9 Round 3), when **Show Bones** is on; colored markers at the six IK targets — hands green, feet blue, head **yellow**, torso **magenta** (Phase 9 Round 7) — plus a semi-transparent yellow aim line from the Head bone to the head marker, when **Show IK Handles** is on. Each load frees the previous rig and spawns a fresh one.
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# Stickman Rigging
|
||||
|
||||
We currently have a test harness to .stk files to be loaded, and figured to be tested by moving IK guides.
|
||||
We need to plan and implement several new features to the rigging and the harness.
|
||||
|
||||
## Task 1: Skeleton IK bone switches
|
||||
|
||||
The stickman will eventually be able to move in different 'directions', to make movement look better we need to build bone modification switches into the rig (master_rig.tscn)
|
||||
|
||||
### Bend direction
|
||||
|
||||
Each IK modifier in the skeleton has a 'Flip Bend Direction' flag. This flag needs to be made available to the user's stickman figure so that the switch can be changed dynamically.
|
||||
|
||||
- In the test harness, a user should be able to right click on a non-leaf joint (upper-lower limb connector) and select from a dropdown context menu 'Normal Bend' or 'Invert Bend'
|
||||
- The menu should look like the stickman editor's 'Hide Pose' vs 'Show Pose' item. When the normal bend is currently on, the menu will show invert bend and also the other way around.
|
||||
|
||||
### Rig bend direction profiles
|
||||
|
||||
Eventually, the stickman will be able to face different directions. There needs to be a selectable profile that can be set by the user. Each profile changes the 'flip bend direction' on the rig.
|
||||
|
||||
#### Left
|
||||
|
||||
The rig if 'facing' left, it will have the following bend profiles for these bones
|
||||
|
||||
- Left Arm (Upper/Lower Left Arm joint): Flip Bend Direction - off
|
||||
- Right Arm (Upper/Lower Right Arm joint): Flip Bend Direction - off
|
||||
- Left Leg (Upper/Lower Left Leg joint): Flip Bend Direction - on
|
||||
- Right Leg (Upper/Lower Right Leg joint): Flip Bend Direction - on
|
||||
|
||||
#### Right
|
||||
|
||||
The rig if 'facing' right, it will have the following bend profiles for these bones
|
||||
|
||||
- Left Arm (Upper/Lower Left Arm joint): Flip Bend Direction - on
|
||||
- Right Arm (Upper/Lower Right Arm joint): Flip Bend Direction - on
|
||||
- Left Leg (Upper/Lower Left Leg joint): Flip Bend Direction - off
|
||||
- Right Leg (Upper/Lower Right Leg joint): Flip Bend Direction - off
|
||||
|
||||
### Forward
|
||||
|
||||
The rig is looking 'at the user'
|
||||
|
||||
- Left Arm (Upper/Lower Left Arm joint): Flip Bend Direction - off
|
||||
- Right Arm (Upper/Lower Right Arm joint): Flip Bend Direction - on
|
||||
- Left Leg (Upper/Lower Left Leg joint): Flip Bend Direction - on
|
||||
- Right Leg (Upper/Lower Right Leg joint): Flip Bend Direction - off
|
||||
|
||||
The harness should have a control to set the direction profile. Maybe we should consider a menubar (Like the stickman editor)
|
||||
If a profile is selected, just set the flags corresponding to the profile. The user will still be free to change the bend direction by right clicking and changing them manually.
|
||||
Changing the profile WILL change the context menu item for each individual joint. Whether the context menu shows 'Normal' or 'Invert' bend depends on the current flag setting on that bone joint.
|
||||
|
||||
## Task 2: Body part z-order
|
||||
|
||||
**Implemented (Phase 9 Task 2)** — see `docs/phase9_task2_zorder_spec.md`; the harness reorders the rig's `Body/*` children from the Facing profile (test harness top-bar "Facing" menu).
|
||||
|
||||
When the stickman is facing certain directions, body parts need to be drawn in a certain order:
|
||||
|
||||
1. When 'facing' left, the left lower/upper arm and lower/upper leg need to be drawn 'behind' the torso, while others in front
|
||||
2. When 'facing' right, the right lower/upper arm and lower/upper leg need to be drawn 'behind' the torso, while others in front
|
||||
3. When 'facing' forward, the legs and arms should be drawn in front of the torso.
|
||||
4. On the far (behind-torso) side the arm draws behind the leg; on the near (in-front) side the arm draws in front of the leg.
|
||||
5. In all cases, the head should be drawn in 'front' of the torso.
|
||||
|
||||
## Task 3: Coordinates display
|
||||
|
||||
**Implemented (Phase 9 Task 3)** — see `docs/phase9_task3_coords_spec.md`; the harness adds a
|
||||
"Show Coords" checkbox (top bar, default ON) and a code-built top-right readout panel
|
||||
(`_coords_panel` + monospace `_coords_label`) showing, per frame, the Skeleton2D position and
|
||||
rotation, all 10 bone positions and rotations, and the 6 IK target positions (world-space
|
||||
values, rotation in degrees). "No rig loaded" when no rig is spawned.
|
||||
|
||||
The harness should have a display that can be shown or hidden that will display the following information:
|
||||
|
||||
1. Skeleton2D position and rotation
|
||||
2. Bone positions and rotations
|
||||
|
||||
- Torso
|
||||
- Head
|
||||
- LeftUpperArm
|
||||
- LeftLowerArm
|
||||
- RightUpperArm
|
||||
- RightLowerArm
|
||||
- LeftUpperLeg
|
||||
- LeftLowerLeg
|
||||
- RightUpperLeg
|
||||
- RightLowerLeg
|
||||
|
||||
3. IK target positions
|
||||
|
||||
- Head
|
||||
- Torso
|
||||
- Right_Hand
|
||||
- Left_Hand
|
||||
- Right_Leg
|
||||
- Left_Leg
|
||||
|
||||
The display should be easy to read to the right side of the screen. There should be a checkmark display like 'show ik bones' to show and hide the coordinate display.
|
||||
|
||||
## Task 4: Refactor facing direction / bone bend
|
||||
|
||||
We need to have facing direction and bone bend to be a part of the master_rig.tscn. This rig will be used for instances of stickmen which will be able to face different directions / have different bone bending at a time.
|
||||
So rather than keeping that data in the harness, we need it in the master rig. These should probably be exported controls too so that they can easily be accessed.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Phase 9 Task 2 — Feature: Body-Part Z-Order by Facing Profile
|
||||
|
||||
## Overview
|
||||
|
||||
RIGGING.md Task 2: when the stickman faces different directions, body parts must be drawn in
|
||||
the right order relative to the torso:
|
||||
|
||||
1. **Facing left** → left upper/lower arm **and** left upper/lower leg draw **behind** the
|
||||
torso; the right-side limbs draw in front. On the far (behind-torso) side the arm draws
|
||||
**behind** the leg; on the near (in-front) side the arm draws **in front of** the leg.
|
||||
2. **Facing right** → right upper/lower arm **and** right upper/lower leg draw **behind** the
|
||||
torso; the left-side limbs draw in front, with the same far-arm-behind-leg / near-arm-in-front-of-leg rule.
|
||||
3. **Facing forward** → all four limb pairs draw **in front of** the torso.
|
||||
4. In **all** cases the **head draws in front of the torso**.
|
||||
|
||||
Task 1 already built the harness-level `FacingProfile { LEFT, RIGHT, FORWARD }` state
|
||||
(`_facing_profile`, the "Facing" `MenuButton`, `_apply_facing_profile()`). Task 2 reuses that
|
||||
profile state to also reorder the rig's `Body/*` visual part nodes.
|
||||
|
||||
## 1. Mechanism
|
||||
|
||||
`master_rig.tscn`'s `Body` node is a plain `Node2D` container whose children are the per-part
|
||||
visual nodes: `Head`, `Body` (the torso visual), `LeftUpperLeg`, `RightUpperLeg`,
|
||||
`LeftLowerLeg`, `RightLowerLeg`, `LeftUpperArm`, `RightUpperArm`, `LeftLowerArm`,
|
||||
`RightLowerArm`. All have default `z_index = 0`, and Godot 4 `Node2D` draws siblings in tree
|
||||
order (first child = back, last child = front). The `RemoteTransform2D` drivers only push
|
||||
global transforms — they never touch tree order. Therefore **draw order = child order of the
|
||||
`Body` container**, and reordering is a pure `move_child()` operation. This is safe to do at
|
||||
any time: the adapter mounts shapes as children of each part node (`_mount_shape` adds
|
||||
`Line2D`/`Polygon2D` under the part node), so moving a part node moves its whole shape group
|
||||
and never disturbs intra-part shape order.
|
||||
|
||||
Per-part authored node names are fixed, so z-order tables are **static string arrays** — no
|
||||
runtime lookup by part key needed (the harness already owns the rig-facing logic for Task 1).
|
||||
|
||||
## 2. Z-order tables
|
||||
|
||||
Back-to-front (first entry = backmost, last = frontmost). Within each limb pair the **upper
|
||||
limb stays behind the lower limb** (the elbow/knee overlaps the lower limb). On the **far**
|
||||
(behind-torso) side the arm pair draws **behind** the leg pair; on the **near** (in-front)
|
||||
side the arm pair draws **in front of** the leg pair (per the user's per-direction spec). The
|
||||
head is always last.
|
||||
|
||||
`const Z_ORDER_BY_PROFILE := { FacingProfile.FORWARD: [torso, left upper leg, right upper leg,
|
||||
left lower leg, right lower leg, left upper arm, right upper arm, left lower arm, right lower
|
||||
arm, head], FacingProfile.LEFT: [left upper arm, left lower arm, left upper leg, left lower
|
||||
leg, torso, right upper leg, right lower leg, right upper arm, right lower arm, head],
|
||||
FacingProfile.RIGHT: [right upper arm, right lower arm, right upper leg, right lower leg,
|
||||
torso, left upper leg, left lower leg, left upper arm, left lower arm, head] }`
|
||||
|
||||
with node names from `master_rig.tscn` (`"Head"`, `"Body"`, `"LeftUpperArm"`, …).
|
||||
|
||||
- **FORWARD** matches the requirement "legs and arms drawn in front of the torso"; all limbs
|
||||
are on the near side, so arms draw in front of legs.
|
||||
- **LEFT** places both left limb pairs behind the torso (left arm behind left leg) and the
|
||||
right pairs in front (right arm in front of right leg) (requirement 1); **RIGHT** mirrors
|
||||
it (requirement 2).
|
||||
- The head is frontmost in all three (requirement 4).
|
||||
|
||||
## 3. Implementation — `scripts/test_harness.gd`
|
||||
|
||||
### 3a. Constants
|
||||
|
||||
- `const BODY_CONTAINER_PATH := "Body"` — rig-relative path of the visual container.
|
||||
- `const Z_ORDER_BY_PROFILE: Dictionary = { FacingProfile.FORWARD: [...], ... }` per §2.
|
||||
|
||||
### 3b. State
|
||||
|
||||
- `var _body_container: Node2D = null` — resolved at spawn, cleared in `_free_current_rig()`.
|
||||
|
||||
### 3c. Resolution
|
||||
|
||||
- `_resolve_rig_nodes()` additionally resolves `_body_container` via
|
||||
`_rig.get_node_or_null(NodePath(BODY_CONTAINER_PATH)) as Node2D` (null → `push_warning`).
|
||||
|
||||
### 3d. Reorder
|
||||
|
||||
New `_apply_body_z_order()`: when `_body_container` is valid, walk the profile's ordered part
|
||||
names **back-to-front** and call `_body_container.move_child(part, _body_container.get_child_count() - 1)`
|
||||
for each existing child (skip missing parts — a `.stk` may omit a part). Moving to the end in
|
||||
back-to-front sequence yields the profile order and leaves unknown/extra children untouched at
|
||||
the back.
|
||||
|
||||
### 3e. Hooks
|
||||
|
||||
- `_apply_facing_profile(profile)` calls `_apply_body_z_order()` after setting the
|
||||
`flip_bend_direction` flags (single entry point: menu selection and fresh spawns both flow
|
||||
through it).
|
||||
- No other changes: `_load_and_spawn()` already calls `_apply_facing_profile(_facing_profile)`
|
||||
after spawn, so every loaded rig gets the current profile's z-order.
|
||||
|
||||
## 4. Files modified
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `scripts/test_harness.gd` | `BODY_CONTAINER_PATH`, `Z_ORDER_BY_PROFILE`, `_body_container`, resolve + clear + `_apply_body_z_order()`, hook in `_apply_facing_profile()`. |
|
||||
| `docs/phase9_task2_zorder_spec.md` | This file. |
|
||||
| `AGENTS.md` | Test-harness section: "Phase 9 Task 2 body-part z-order" bullet. |
|
||||
| `README.md` | Test-harness bullet: facing profiles now also reorder `Body/*` draw order. |
|
||||
| `RIGGING.md` | Mark Task 2 implemented (Task 1 was documented in the same way). |
|
||||
|
||||
## 5. Edge cases
|
||||
|
||||
- **Missing `Body` container** (foreign rig): null-guarded; z-order silently skipped.
|
||||
- **Missing part nodes** (partial `.stk`): skipped by `get_node_or_null`; no warnings spam.
|
||||
- **Re-spawn**: `_body_container` is re-resolved per spawn; `_free_current_rig()` nulls it.
|
||||
- **`z_index`**: all authored `Body/*` nodes use default `z_index = 0`; nothing in the
|
||||
adapter or harness sets it, so tree order remains the sole depth source.
|
||||
|
||||
## 6. Design decisions
|
||||
|
||||
| # | Decision | Justification |
|
||||
|---|---|---|
|
||||
| D1 | Z-order lives in the **harness**, reusing `FacingProfile` state | The rig stays generic; Task 1 already made the harness the owner of facing state; the rig itself has no z-order concept. |
|
||||
| D2 | Upper limb behind lower limb in every profile | Elbow/knee joints overlap the lower limb; matching the authored scene order avoids joint seams. |
|
||||
| D3 | Far-side arms draw behind the legs; near-side arms draw in front of the legs (user-specified per direction) | Matches the user's Left/Right spec: e.g. facing left → left arm behind left leg, right arm in front of right leg. |
|
||||
| D4 | Head always frontmost (last in array) | Explicit requirement 4. |
|
||||
|
||||
## 7. Test plan
|
||||
|
||||
1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit`
|
||||
(user-requested; no errors).
|
||||
2. Temporary headless SceneTree script (pattern from the Round 7 spec; deleted afterwards):
|
||||
- Instantiate the harness scene, `add_child` it, call `_load_and_spawn("res://stickmen/basic.stk")`.
|
||||
- Assert `Body` children order equals `Z_ORDER_BY_PROFILE[FacingProfile.FORWARD]`.
|
||||
- Call `_apply_facing_profile(FacingProfile.LEFT)` → assert left limb pairs precede the
|
||||
torso node (left arm pair before left leg pair) and right pairs follow it (right leg
|
||||
pair before right arm pair); repeat for `RIGHT` (mirrored).
|
||||
- Assert `Body/Head` is the last child in all three profiles.
|
||||
3. Manual F6 check: load a `.stk`, switch Facing — far-side limbs visibly tuck behind the
|
||||
torso; head stays on top.
|
||||
|
||||
## 8. Implementation order
|
||||
|
||||
1. `scripts/test_harness.gd` — constants, state, resolution, reorder + hook.
|
||||
2. Parse check + headless verification script (temp, then removed).
|
||||
3. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`.
|
||||
@@ -0,0 +1,146 @@
|
||||
# Phase 9 Task 3 — Feature: Coordinates Display
|
||||
|
||||
## Overview
|
||||
|
||||
RIGGING.md Task 3: the test harness needs a show/hide readout on the right side of the screen
|
||||
displaying the current skeleton pose so bone/IK work can be inspected numerically:
|
||||
|
||||
1. **Skeleton2D** position and rotation.
|
||||
2. **Bone** positions and rotations — Torso, Head, Left/RightUpperArm, Left/RightLowerArm,
|
||||
Left/RightUpperLeg, Left/RightLowerLeg.
|
||||
3. **IK target** positions — Head, Torso, Right_Hand, Left_Hand, Right_Leg, Left_Leg.
|
||||
|
||||
The display is easy to read on the right side of the screen and is toggled by a checkbox like
|
||||
the existing "Show Bones" / "Show IK Handles" toggles. It is a **non-persistent** debug aid:
|
||||
the toggle defaults ON per launch, and the panel is rebuilt from scratch each run.
|
||||
|
||||
## 1. Mechanism
|
||||
|
||||
The readout is a `PanelContainer` + `RichTextLabel` pair built entirely in code
|
||||
(`_build_coords_panel()`), **not** authored in `test_harness.tscn`, and added as a child of the
|
||||
harness root **after** the viewport container so it renders in front of the `SubViewport`. It
|
||||
anchors to the top-right of the window below the top UI bar (`offset_top = 40.0`) with a fixed
|
||||
width and auto-height, and the panel is `MOUSE_FILTER_IGNORE` so it never intercepts viewport
|
||||
pan/zoom/drag input — the label itself stays interactive: text is **selectable** and copyable
|
||||
(mouse drag + Ctrl+C, plus the built-in right-click copy menu).
|
||||
|
||||
Values are read live each frame in `_process(delta)`, so the panel always mirrors the current
|
||||
pose. All values are **world-space** (`global_position` / `global_rotation`). Rotation is
|
||||
reported in degrees (`rad_to_deg`); positions are `Vector2` formatted to 1 decimal place.
|
||||
|
||||
## 2. Implementation — `scripts/test_harness.gd`
|
||||
|
||||
### 2a. Constants
|
||||
|
||||
- `const COORDS_PANEL_WIDTH: float = 320.0` — fixed panel width (also the `offset_left` extent).
|
||||
- `const COORD_BONE_PATHS: Array[String]` — 10 `Skeleton2D`-relative bone paths, in display
|
||||
order; the display name is the last path segment (`path.get_file()`):
|
||||
`Torso`, `Torso/Head`, `Torso/LeftUpperArm`, `Torso/LeftUpperArm/LeftLowerArm`,
|
||||
`Torso/RightUpperArm`, `Torso/RightUpperArm/RightLowerArm`, `Torso/LeftUpperLeg`,
|
||||
`Torso/LeftUpperLeg/LeftLowerLeg`, `Torso/RightUpperLeg`, `Torso/RightUpperLeg/RightLowerLeg`.
|
||||
|
||||
### 2b. State
|
||||
|
||||
- `var _coords_panel: PanelContainer = null` — the readout container.
|
||||
- `var _coords_label: RichTextLabel = null` — the monospace, selectable text readout.
|
||||
- `var _coord_bones: Dictionary = {}` — bone display name → `Bone2D`, resolved per spawn.
|
||||
- `var _show_coords: bool = true` — toggle state (default ON, harness-level, persists across
|
||||
spawns but not to disk).
|
||||
|
||||
### 2c. UI construction — `_build_coords_panel()`
|
||||
|
||||
Called from `_build_ui()` right after the viewport container. Builds:
|
||||
|
||||
- `_coords_panel` (`PanelContainer`):
|
||||
- `PRESET_TOP_RIGHT` anchors; `offset_top = 40.0`, `offset_right = -8.0`,
|
||||
`offset_left = -COORDS_PANEL_WIDTH`.
|
||||
- `grow_vertical = GROW_DIRECTION_END` + `grow_horizontal = GROW_DIRECTION_BEGIN`
|
||||
(auto-height/width from label content; grows leftward so text never runs off the right
|
||||
edge of the screen).
|
||||
- `mouse_filter = MOUSE_FILTER_IGNORE` on the panel (input passes through to the viewport;
|
||||
the label itself keeps mouse handling for text selection).
|
||||
- `StyleBoxFlat` override "panel": bg `Color(0,0,0,0.55)`, border `Color(1,1,1,0.12)` width 1,
|
||||
corner radius 4, content margin 8.
|
||||
- `_coords_label` (`RichTextLabel`): `selection_enabled = true`, `context_menu_enabled = true`,
|
||||
`fit_content = true`, `autowrap_mode = OFF`, `scroll_active = false`,
|
||||
`focus_mode = FOCUS_CLICK`; monospace via a `SystemFont` override on the `normal_font` theme
|
||||
item (`Consolas`, `Menlo`, `DejaVu Sans Mono`, `Courier New` fallbacks) at
|
||||
`normal_font_size = 18`.
|
||||
- `_coords_panel` added to the harness root (`add_child(_coords_panel)`).
|
||||
|
||||
### 2d. Toggle — `_on_show_coords_toggled(pressed)`
|
||||
|
||||
`CheckBox` "Show Coords" (top bar `HBox`, after "Show IK Handles", before the status label),
|
||||
`button_pressed = true` at build. Handler sets `_show_coords = pressed` and flips
|
||||
`_coords_panel.visible`. No persistence to disk.
|
||||
|
||||
### 2e. Update loop — `_process(_delta)` → `_update_coords_display()`
|
||||
|
||||
`_process(delta)` early-outs when `_show_coords` is false or `_coords_label` is null; otherwise
|
||||
calls `_update_coords_display()` each frame. `_update_coords_display()`:
|
||||
|
||||
- If `_rig` is null/invalid → label text `"No rig loaded"`, return.
|
||||
- `"Skeleton2D"` section: position + rotation (degrees) via `_skeleton.global_position` /
|
||||
`_skeleton.global_rotation` (guarded).
|
||||
- `"Bones"` section: iterates `COORD_BONE_PATHS`, looking each bone up in `_coord_bones` by
|
||||
display name; position + rotation (degrees). Missing/invalid bones skipped.
|
||||
- `"IK Targets"` section: iterates `IK_HANDLE_PATHS` and reads each handle from `_ik_handles`
|
||||
(the same 6 handle set used by the drag handles); **position only**. Missing/invalid handles
|
||||
skipped.
|
||||
- Lines joined with `\n` into `_coords_label.text`; the text is only reassigned when the built
|
||||
string differs from the current one, so an active text selection survives idle frames (it
|
||||
refreshes while bones are actively moving). Formatting helpers: `_fmt_vec2(v)`
|
||||
`(%8.1f, %8.1f)`, `_fmt_deg(rad)` `%7.1f°` (1 decimal place).
|
||||
|
||||
### 2f. Resolution & lifecycle
|
||||
|
||||
- `_resolve_rig_nodes()` additionally calls `_resolve_coord_bones()`.
|
||||
- `_resolve_coord_bones()` clears `_coord_bones`, then for each `COORD_BONE_PATHS` entry
|
||||
resolves via `_skeleton.get_node_or_null(NodePath(path)) as Bone2D`; valid bones are keyed by
|
||||
`path.get_file()`, missing ones emit `push_warning`.
|
||||
- `_free_current_rig()` clears `_coord_bones`. The `_show_coords` toggle itself persists across
|
||||
respawns (it lives at harness level, not per-rig).
|
||||
|
||||
## 3. Files modified
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `scripts/test_harness.gd` | `COORDS_PANEL_WIDTH`, `COORD_BONE_PATHS`, `_coords_panel`, `_coords_label`, `_coord_bones`, `_show_coords`, `_build_coords_panel()`, `_on_show_coords_toggled()`, `_process()`, `_update_coords_display()`, `_resolve_coord_bones()`, `_fmt_vec2()`, `_fmt_deg()`; "Show Coords" checkbox. |
|
||||
| `docs/phase9_task3_coords_spec.md` | This file. |
|
||||
| `AGENTS.md` | Test-harness section: "Phase 9 Task 3 coordinates display" bullet; `test_harness.tscn` scene description now lists the "Show Coords" toggle. |
|
||||
| `RIGGING.md` | Mark Task 3 implemented. |
|
||||
|
||||
## 4. Edge cases
|
||||
|
||||
- **No rig loaded**: `_update_coords_display()` shows `"No rig loaded"`.
|
||||
- **Missing bone nodes** (foreign/partial rig): `_resolve_coord_bones()` warns once, and the
|
||||
per-frame loop skips missing/invalid bones via `is_instance_valid`.
|
||||
- **Toggle off**: `_process()` early-outs; panel hidden.
|
||||
- **Re-spawn**: `_coord_bones` re-resolved per spawn; `_free_current_rig()` clears it. The
|
||||
`_show_coords` toggle stays at its user-set value across spawns (harness-level state).
|
||||
- **Input blocking**: `MOUSE_FILTER_IGNORE` keeps the panel from intercepting viewport pan/zoom/
|
||||
drag.
|
||||
- **World-space values**: `global_position`/`global_rotation` reflect the live pose regardless
|
||||
of `Camera2D` pan/zoom, so the readout tracks the actual rig transforms.
|
||||
|
||||
## 5. Design decisions
|
||||
|
||||
| # | Decision | Justification |
|
||||
|---|---|---|
|
||||
| D1 | Panel is code-built (`_build_coords_panel()`), not a scene node | Mirrors the code-built harness top bar; keeps `test_harness.tscn` unchanged and the panel trivial to rebuild. |
|
||||
| D2 | `MOUSE_FILTER_IGNORE` on the panel | The readout must never block viewport interaction (pan/zoom/IK dragging). |
|
||||
| D3 | World-space values + degrees | Matches what the debug overlay and drag handles use; degrees are the convention in the harness and the editor. |
|
||||
| D4 | IK targets report position only | IK targets carry no meaningful rotation for this readout (they are aim/IK markers). |
|
||||
| D5 | Toggle non-persistent, default ON | Debug aid only; no settings.json involvement, consistent with the other harness toggles. |
|
||||
|
||||
## 6. Test plan
|
||||
|
||||
1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit`
|
||||
(user-requested; no errors).
|
||||
2. Manual F6 check:
|
||||
- Launch harness; readout visible on the top-right with `"No rig loaded"`.
|
||||
- Load `stickmen/basic.stk`; Skeleton2D, 10 Bones, and 6 IK Target rows populate with
|
||||
numeric values.
|
||||
- Drag an IK handle → position/rotation values update live each frame.
|
||||
- Uncheck "Show Coords" → panel hides; recheck → reappears with current values.
|
||||
- Confirm the panel does not block middle-mouse pan / wheel zoom / IK drags beneath it.
|
||||
+126
-25
@@ -28,6 +28,7 @@ joint_two_bone2d_node = NodePath("Torso/RightUpperArm/RightLowerArm")
|
||||
|
||||
[sub_resource type="SkeletonModification2DTwoBoneIK" id="SkeletonModification2DTwoBoneIK_f0s26"]
|
||||
target_nodepath = NodePath("../IK_Targets/Left_Hand")
|
||||
flip_bend_direction = true
|
||||
joint_one_bone_idx = 2
|
||||
joint_one_bone2d_node = NodePath("Torso/LeftUpperArm")
|
||||
joint_two_bone_idx = 3
|
||||
@@ -42,7 +43,6 @@ joint_two_bone2d_node = NodePath("Torso/RightUpperLeg/RightLowerLeg")
|
||||
|
||||
[sub_resource type="SkeletonModification2DTwoBoneIK" id="SkeletonModification2DTwoBoneIK_2leu7"]
|
||||
target_nodepath = NodePath("../IK_Targets/Left_Leg")
|
||||
flip_bend_direction = true
|
||||
joint_one_bone_idx = 6
|
||||
joint_one_bone2d_node = NodePath("Torso/LeftUpperLeg")
|
||||
joint_two_bone_idx = 7
|
||||
@@ -66,38 +66,127 @@ modifications/2 = SubResource("SkeletonModification2DTwoBoneIK_ylko5")
|
||||
modifications/3 = SubResource("SkeletonModification2DTwoBoneIK_2leu7")
|
||||
modifications/4 = SubResource("SkeletonModification2DLookAt_j4hao")
|
||||
|
||||
[sub_resource type="Animation" id="Animation_2leu7"]
|
||||
length = 0.001
|
||||
|
||||
[sub_resource type="Animation" id="Animation_ylko5"]
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("IK_Targets/Torso:position")
|
||||
tracks/0/interp = 2
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(0, 10), Vector2(0, -15), Vector2(0, 10), Vector2(0, -15), Vector2(0, 10)]
|
||||
}
|
||||
tracks/1/type = "value"
|
||||
tracks/1/imported = false
|
||||
tracks/1/enabled = true
|
||||
tracks/1/path = NodePath("IK_Targets/Head:position")
|
||||
tracks/1/interp = 2
|
||||
tracks/1/loop_wrap = true
|
||||
tracks/1/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(100, -614), Vector2(100, -639), Vector2(100, -614), Vector2(100, -639), Vector2(100, -614)]
|
||||
}
|
||||
tracks/2/type = "value"
|
||||
tracks/2/imported = false
|
||||
tracks/2/enabled = true
|
||||
tracks/2/path = NodePath("IK_Targets/Right_Leg:position")
|
||||
tracks/2/interp = 2
|
||||
tracks/2/loop_wrap = true
|
||||
tracks/2/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390)]
|
||||
}
|
||||
tracks/3/type = "value"
|
||||
tracks/3/imported = false
|
||||
tracks/3/enabled = true
|
||||
tracks/3/path = NodePath("IK_Targets/Left_Leg:position")
|
||||
tracks/3/interp = 2
|
||||
tracks/3/loop_wrap = true
|
||||
tracks/3/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380)]
|
||||
}
|
||||
tracks/4/type = "value"
|
||||
tracks/4/imported = false
|
||||
tracks/4/enabled = true
|
||||
tracks/4/path = NodePath("IK_Targets/Right_Hand:position")
|
||||
tracks/4/interp = 2
|
||||
tracks/4/loop_wrap = true
|
||||
tracks/4/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110)]
|
||||
}
|
||||
tracks/5/type = "value"
|
||||
tracks/5/imported = false
|
||||
tracks/5/enabled = true
|
||||
tracks/5/path = NodePath("IK_Targets/Left_Hand:position")
|
||||
tracks/5/interp = 2
|
||||
tracks/5/loop_wrap = true
|
||||
tracks/5/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6, 0.8),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 0,
|
||||
"values": [Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110)]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_t75yq"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_2leu7"),
|
||||
&"walk_right": SubResource("Animation_ylko5")
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_6rw38"]
|
||||
|
||||
[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(-0.038591623, -463.50787)
|
||||
rotation = 0.000554086
|
||||
scale = Vector2(0.9999999, 0.9999999)
|
||||
position = Vector2(-0.03859164, -453.50787)
|
||||
rotation = 0.00055408623
|
||||
scale = Vector2(0.9999996, 0.9999996)
|
||||
script = SubResource("GDScript_f0s26")
|
||||
|
||||
[node name="Body" type="Line2D" parent="Body" unique_id=1290443463]
|
||||
position = Vector2(9.313226e-10, 10.000001)
|
||||
rotation = -3.1415925
|
||||
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(1.6097914e-05, 5.306938e-07)
|
||||
position = Vector2(1.6098846e-05, 10.000002)
|
||||
rotation = 0.4947604
|
||||
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(-2.3235021e-05, 2.1205284e-05)
|
||||
rotation = -0.4947779
|
||||
position = Vector2(-2.3252098e-05, 10.000022)
|
||||
rotation = -0.49392816
|
||||
scale = Vector2(0.99999994, 0.99999994)
|
||||
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(-94.96416, 176.0165)
|
||||
position = Vector2(-94.96416, 186.0165)
|
||||
rotation = 0.0051915743
|
||||
scale = Vector2(0.99999994, 0.99999994)
|
||||
points = PackedVector2Array(0, 0, 0, 200)
|
||||
@@ -105,36 +194,36 @@ 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(94.96724, 176.01485)
|
||||
rotation = -0.0051744953
|
||||
scale = Vector2(0.99999994, 0.99999994)
|
||||
position = Vector2(94.817635, 186.09546)
|
||||
rotation = -0.0034907677
|
||||
scale = Vector2(0.9999998, 0.9999998)
|
||||
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(0, -248)
|
||||
position = Vector2(9.313226e-10, -238)
|
||||
rotation = 1.6184965
|
||||
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(0, -248)
|
||||
position = Vector2(9.313226e-10, -238)
|
||||
rotation = -1.6184964
|
||||
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(-167.80888, -256.0106)
|
||||
position = Vector2(-167.80888, -246.01059)
|
||||
rotation = 3.1406152
|
||||
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(167.80891, -256.01056)
|
||||
position = Vector2(167.80891, -246.01057)
|
||||
rotation = -3.1406155
|
||||
points = PackedVector2Array(0, 0, 0, 200)
|
||||
width = 16.0
|
||||
@@ -145,13 +234,14 @@ rotation = -0.0006991282
|
||||
modification_stack = SubResource("SkeletonModificationStack2D_j4hao")
|
||||
|
||||
[node name="Torso" type="Bone2D" parent="Skeleton2D" unique_id=744339938]
|
||||
position = Vector2(-0.006991281, 9.999998)
|
||||
rotation = 0.0006991282
|
||||
rest = Transform2D(0.99999976, 0.00069912814, -0.00069912814, 0.99999976, 0, 0)
|
||||
|
||||
[node name="Head" type="Bone2D" parent="Skeleton2D/Torso" unique_id=1487357366]
|
||||
position = Vector2(-0.12882307, -391.5079)
|
||||
rotation = 0.000554086
|
||||
scale = Vector2(0.9999999, 0.9999999)
|
||||
rotation = 0.00055408623
|
||||
scale = Vector2(0.9999996, 0.9999996)
|
||||
rest = Transform2D(0.9998273, 0.0005539903, -0.0005539903, 0.9998273, -0.12882307, -391.5079)
|
||||
auto_calculate_length_and_angle = false
|
||||
length = 90.0
|
||||
@@ -239,15 +329,17 @@ position = Vector2(1.5258789e-05, -7.6293945e-06)
|
||||
remote_path = NodePath("../../../../Body/LeftUpperLeg")
|
||||
|
||||
[node name="RightUpperLeg" type="Bone2D" parent="Skeleton2D/Torso" unique_id=1125756606]
|
||||
rotation = -0.4947779
|
||||
rotation = -0.49392816
|
||||
scale = Vector2(0.99999994, 0.99999994)
|
||||
rest = Transform2D(0.8800552, -0.47482592, 0.47482592, 0.8800552, 0, 0)
|
||||
auto_calculate_length_and_angle = false
|
||||
length = 90.0
|
||||
length = 200.0
|
||||
bone_angle = 90.0
|
||||
|
||||
[node name="RightLowerLeg" type="Bone2D" parent="Skeleton2D/Torso/RightUpperLeg" unique_id=1819999778]
|
||||
position = Vector2(0, 200)
|
||||
rotation = 2.0603998
|
||||
rotation = 2.0612338
|
||||
scale = Vector2(0.9999999, 0.9999999)
|
||||
rest = Transform2D(-0.47026652, 0.8825017, -0.8825017, -0.47026652, 0, 200)
|
||||
auto_calculate_length_and_angle = false
|
||||
length = 200.0
|
||||
@@ -271,16 +363,16 @@ position = Vector2(0, 350)
|
||||
[node name="IK_Targets" type="Node2D" parent="." unique_id=1089334064]
|
||||
|
||||
[node name="Right_Hand" type="Marker2D" parent="IK_Targets" unique_id=1785254130]
|
||||
position = Vector2(168, -456)
|
||||
position = Vector2(-90, 110)
|
||||
|
||||
[node name="Left_Hand" type="Marker2D" parent="IK_Targets" unique_id=1085197042]
|
||||
position = Vector2(-168, -456)
|
||||
position = Vector2(90, 110)
|
||||
|
||||
[node name="Right_Leg" type="Marker2D" parent="IK_Targets" unique_id=720679106]
|
||||
position = Vector2(96, 376)
|
||||
position = Vector2(110, 390)
|
||||
|
||||
[node name="Left_Leg" type="Marker2D" parent="IK_Targets" unique_id=63598929]
|
||||
position = Vector2(-96, 376)
|
||||
position = Vector2(-110, 380)
|
||||
|
||||
[node name="RayCast_LeftLeg" type="RayCast2D" parent="IK_Targets" unique_id=2000000001]
|
||||
position = Vector2(-96, 200)
|
||||
@@ -291,9 +383,18 @@ position = Vector2(96, 200)
|
||||
target_position = Vector2(0, 200)
|
||||
|
||||
[node name="Head" type="Marker2D" parent="IK_Targets" unique_id=1347666160]
|
||||
position = Vector2(0, -624)
|
||||
position = Vector2(100, -614)
|
||||
|
||||
[node name="Torso" type="Marker2D" parent="IK_Targets" unique_id=809821395]
|
||||
position = Vector2(0, 10)
|
||||
|
||||
[node name="RemoteTransform2D" type="RemoteTransform2D" parent="IK_Targets/Torso" unique_id=1659291279]
|
||||
remote_path = NodePath("../../../Skeleton2D/Torso")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=679147993]
|
||||
libraries/ = SubResource("AnimationLibrary_t75yq")
|
||||
|
||||
[node name="AnimationTree" type="AnimationTree" parent="." unique_id=1526237266]
|
||||
active = false
|
||||
tree_root = SubResource("AnimationNodeStateMachine_6rw38")
|
||||
anim_player = NodePath("../AnimationPlayer")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
|
||||
@export var build_walk_animation: bool = false:
|
||||
set(value):
|
||||
if value:
|
||||
_create_walk_right_animation()
|
||||
|
||||
func _create_walk_right_animation() -> void:
|
||||
var anim_player: AnimationPlayer = get_node_or_null("AnimationPlayer")
|
||||
if not anim_player:
|
||||
push_error("AnimationPlayer node not found as direct child of Master!")
|
||||
return
|
||||
|
||||
var anim = Animation.new()
|
||||
anim.length = 0.8
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
var times = [0.0, 0.2, 0.4, 0.6, 0.8]
|
||||
|
||||
# Keyframe tracks mapped relative to root Master node
|
||||
var tracks = {
|
||||
"IK_Targets/Torso:position": [
|
||||
Vector2(0, 10), Vector2(0, -15), Vector2(0, 10), Vector2(0, -15), Vector2(0, 10)
|
||||
],
|
||||
"IK_Targets/Head:position": [
|
||||
Vector2(100, -614), Vector2(100, -639), Vector2(100, -614), Vector2(100, -639), Vector2(100, -614)
|
||||
],
|
||||
"IK_Targets/Right_Leg:position": [
|
||||
Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390)
|
||||
],
|
||||
"IK_Targets/Left_Leg:position": [
|
||||
Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380)
|
||||
],
|
||||
"IK_Targets/Right_Hand:position": [
|
||||
Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110)
|
||||
],
|
||||
"IK_Targets/Left_Hand:position": [
|
||||
Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110), Vector2(0, 115), Vector2(-90, 110)
|
||||
]
|
||||
}
|
||||
|
||||
for path in tracks:
|
||||
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
||||
anim.track_set_path(track_idx, path)
|
||||
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_CUBIC)
|
||||
for i in range(times.size()):
|
||||
anim.track_insert_key(track_idx, times[i], tracks[path][i])
|
||||
|
||||
# Add or retrieve default library
|
||||
var lib = anim_player.get_animation_library("")
|
||||
if not lib:
|
||||
lib = AnimationLibrary.new()
|
||||
anim_player.add_animation_library("", lib)
|
||||
|
||||
if lib.has_animation("walk_right"):
|
||||
lib.remove_animation("walk_right")
|
||||
|
||||
lib.add_animation("walk_right", anim)
|
||||
print("✓ Successfully generated 'walk_right' animation in AnimationPlayer!")
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0l8if18mvdvb
|
||||
@@ -28,6 +28,25 @@ const HANDLE_COLOR_HEAD := Color(1.00, 1.00, 0.00) # yellow
|
||||
const HANDLE_COLOR_TORSO := Color(1.00, 0.00, 1.00) # magenta
|
||||
|
||||
const SKELETON_PATH := "Skeleton2D"
|
||||
const BODY_CONTAINER_PATH := "Body"
|
||||
|
||||
## Width of the coordinates readout panel (Task 3).
|
||||
const COORDS_PANEL_WIDTH: float = 320.0
|
||||
|
||||
## Bone node paths (relative to Skeleton2D) shown in the coordinates panel, in
|
||||
## display order. The display name is the last path segment.
|
||||
const COORD_BONE_PATHS: Array[String] = [
|
||||
"Torso",
|
||||
"Torso/Head",
|
||||
"Torso/LeftUpperArm",
|
||||
"Torso/LeftUpperArm/LeftLowerArm",
|
||||
"Torso/RightUpperArm",
|
||||
"Torso/RightUpperArm/RightLowerArm",
|
||||
"Torso/LeftUpperLeg",
|
||||
"Torso/LeftUpperLeg/LeftLowerLeg",
|
||||
"Torso/RightUpperLeg",
|
||||
"Torso/RightUpperLeg/RightLowerLeg",
|
||||
]
|
||||
|
||||
## Quick-load buttons: [label, res:// path].
|
||||
const QUICK_LOADS: Array = [
|
||||
@@ -61,6 +80,64 @@ const LEAF_BONE_IK_PATHS: Dictionary = {
|
||||
"RightLowerLeg": "IK_Targets/Right_Leg",
|
||||
}
|
||||
|
||||
## Facing profiles for the rig's TwoBoneIK "Flip Bend Direction" flags (Phase
|
||||
## 9 / Task 1). LEFT = facing left, RIGHT = facing right, FORWARD = facing the
|
||||
## user. The enum values are used directly as the facing-menu item ids.
|
||||
enum FacingProfile { LEFT, RIGHT, FORWARD }
|
||||
|
||||
const JOINT_HIT_RADIUS_PX: float = 14.0 # screen-space grab radius for bend joints
|
||||
|
||||
## Bend joints (upper↔lower limb connectors) whose TwoBoneIK "Flip Bend
|
||||
## Direction" flag is user-controllable. Keyed by joint name; values are the
|
||||
## lower bone's NodePath relative to the Skeleton2D.
|
||||
const BEND_JOINTS: Array[String] = ["LeftArm", "RightArm", "LeftLeg", "RightLeg"]
|
||||
|
||||
const BEND_JOINT_BONE_PATHS: Dictionary = {
|
||||
"LeftArm": "Torso/LeftUpperArm/LeftLowerArm",
|
||||
"RightArm": "Torso/RightUpperArm/RightLowerArm",
|
||||
"LeftLeg": "Torso/LeftUpperLeg/LeftLowerLeg",
|
||||
"RightLeg": "Torso/RightUpperLeg/RightLowerLeg",
|
||||
}
|
||||
|
||||
## flip_bend_direction value per facing profile, keyed by bend-joint name.
|
||||
const PROFILE_FLAGS: Dictionary = {
|
||||
FacingProfile.LEFT: { "LeftArm": false, "RightArm": false, "LeftLeg": true, "RightLeg": true },
|
||||
FacingProfile.RIGHT: { "LeftArm": true, "RightArm": true, "LeftLeg": false, "RightLeg": false },
|
||||
FacingProfile.FORWARD: { "LeftArm": false, "RightArm": true, "LeftLeg": true, "RightLeg": false },
|
||||
}
|
||||
|
||||
## Body/* visual part node names in draw order (back-to-front) per facing
|
||||
## profile (Phase 9 / Task 2). First entry draws backmost, last frontmost.
|
||||
## Upper limbs stay behind lower limbs; on the far (behind-torso) side the arm
|
||||
## pair draws behind the leg pair, on the near (in-front) side the arm pair
|
||||
## draws in front of the leg pair; the head is always frontmost.
|
||||
const Z_ORDER_BY_PROFILE: Dictionary = {
|
||||
FacingProfile.FORWARD: [
|
||||
"Body",
|
||||
"LeftUpperLeg", "RightUpperLeg",
|
||||
"LeftLowerLeg", "RightLowerLeg",
|
||||
"LeftUpperArm", "RightUpperArm",
|
||||
"LeftLowerArm", "RightLowerArm",
|
||||
"Head",
|
||||
],
|
||||
FacingProfile.LEFT: [
|
||||
"LeftUpperArm", "LeftLowerArm",
|
||||
"LeftUpperLeg", "LeftLowerLeg",
|
||||
"Body",
|
||||
"RightUpperLeg", "RightLowerLeg",
|
||||
"RightUpperArm", "RightLowerArm",
|
||||
"Head",
|
||||
],
|
||||
FacingProfile.RIGHT: [
|
||||
"RightUpperArm", "RightLowerArm",
|
||||
"RightUpperLeg", "RightLowerLeg",
|
||||
"Body",
|
||||
"LeftUpperLeg", "LeftLowerLeg",
|
||||
"LeftUpperArm", "LeftLowerArm",
|
||||
"Head",
|
||||
],
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime-built node references
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,6 +149,8 @@ var _camera: Camera2D
|
||||
var _debug_overlay: Node2D
|
||||
var _status_label: Label
|
||||
var _file_dialog: FileDialog
|
||||
var _coords_panel: PanelContainer
|
||||
var _coords_label: RichTextLabel
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
@@ -79,15 +158,27 @@ var _file_dialog: FileDialog
|
||||
|
||||
var _rig: Node2D = null
|
||||
var _skeleton: Skeleton2D = null
|
||||
var _body_container: Node2D = null
|
||||
var _ik_handles: Dictionary = {} # { String : Marker2D }
|
||||
var _coord_bones: Dictionary = {} # { String : Bone2D }
|
||||
|
||||
var _show_bones: bool = true
|
||||
var _show_ik: bool = true
|
||||
var _show_coords: bool = true
|
||||
|
||||
var _is_panning: bool = false
|
||||
var _pan_last: Vector2 = Vector2.ZERO
|
||||
var _dragging_handle: Marker2D = null
|
||||
|
||||
# Task 1: IK bend-direction state (survives respawn; matches authored defaults).
|
||||
var _facing_profile: int = FacingProfile.FORWARD
|
||||
var _context_joint: String = ""
|
||||
var _bend_joint_bones: Dictionary = {} # { String : Bone2D }
|
||||
var _bend_modifications: Dictionary = {} # { String : SkeletonModification2DTwoBoneIK }
|
||||
var _facing_button: MenuButton
|
||||
var _facing_menu: PopupMenu
|
||||
var _context_menu: PopupMenu
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -128,6 +219,16 @@ func _build_ui() -> void:
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
top_bar.add_child(hbox)
|
||||
|
||||
_facing_button = MenuButton.new()
|
||||
_facing_button.text = "Facing"
|
||||
_facing_menu = _facing_button.get_popup()
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.LEFT), FacingProfile.LEFT)
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.RIGHT), FacingProfile.RIGHT)
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.FORWARD), FacingProfile.FORWARD)
|
||||
_facing_menu.id_pressed.connect(_on_facing_menu_id_pressed)
|
||||
_facing_menu.about_to_popup.connect(_update_facing_menu_labels)
|
||||
hbox.add_child(_facing_button)
|
||||
|
||||
var open_btn := Button.new()
|
||||
open_btn.text = "Open .stk…"
|
||||
open_btn.pressed.connect(_on_open_pressed)
|
||||
@@ -151,6 +252,12 @@ func _build_ui() -> void:
|
||||
ik_cb.toggled.connect(_on_show_ik_toggled)
|
||||
hbox.add_child(ik_cb)
|
||||
|
||||
var coords_cb := CheckBox.new()
|
||||
coords_cb.text = "Show Coords"
|
||||
coords_cb.button_pressed = true
|
||||
coords_cb.toggled.connect(_on_show_coords_toggled)
|
||||
hbox.add_child(coords_cb)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.text = "No file loaded"
|
||||
_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -184,6 +291,10 @@ func _build_ui() -> void:
|
||||
_debug_overlay.draw.connect(_on_debug_overlay_draw)
|
||||
_world.add_child(_debug_overlay)
|
||||
|
||||
# Coordinates readout panel (added after the viewport container so it
|
||||
# renders in front of it).
|
||||
_build_coords_panel()
|
||||
|
||||
# File dialog for "Open .stk…".
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.title = "Open .stk"
|
||||
@@ -193,6 +304,55 @@ func _build_ui() -> void:
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
# Right-click context menu for toggling a bend joint's bend direction.
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu.add_item("Invert Bend", 0)
|
||||
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
||||
add_child(_context_menu)
|
||||
|
||||
|
||||
func _build_coords_panel() -> void:
|
||||
_coords_panel = PanelContainer.new()
|
||||
_coords_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||
_coords_panel.offset_top = 40.0
|
||||
_coords_panel.offset_right = -8.0
|
||||
_coords_panel.offset_left = -COORDS_PANEL_WIDTH
|
||||
_coords_panel.grow_vertical = Control.GROW_DIRECTION_END
|
||||
_coords_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
_coords_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.0, 0.0, 0.0, 0.55)
|
||||
style.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
||||
style.border_width_left = 1
|
||||
style.border_width_top = 1
|
||||
style.border_width_right = 1
|
||||
style.border_width_bottom = 1
|
||||
style.corner_radius_top_left = 4
|
||||
style.corner_radius_top_right = 4
|
||||
style.corner_radius_bottom_right = 4
|
||||
style.corner_radius_bottom_left = 4
|
||||
style.content_margin_left = 8.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_right = 8.0
|
||||
style.content_margin_bottom = 8.0
|
||||
_coords_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
_coords_label = RichTextLabel.new()
|
||||
_coords_label.selection_enabled = true
|
||||
_coords_label.context_menu_enabled = true
|
||||
_coords_label.fit_content = true
|
||||
_coords_label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
_coords_label.scroll_active = false
|
||||
_coords_label.focus_mode = Control.FOCUS_CLICK
|
||||
var font := SystemFont.new()
|
||||
font.font_names = PackedStringArray(["Consolas", "Menlo", "DejaVu Sans Mono", "Courier New"])
|
||||
_coords_label.add_theme_font_override("normal_font", font)
|
||||
_coords_label.add_theme_font_size_override("normal_font_size", 18)
|
||||
|
||||
_coords_panel.add_child(_coords_label)
|
||||
add_child(_coords_panel)
|
||||
|
||||
|
||||
func _on_viewport_container_resized() -> void:
|
||||
var size := _viewport_container.size
|
||||
@@ -223,6 +383,11 @@ func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
_apply_zoom(mb.position, 1.0 / ZOOM_STEP)
|
||||
return
|
||||
|
||||
if mb.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if mb.pressed:
|
||||
_handle_right_click(mb.position)
|
||||
return
|
||||
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
_dragging_handle = _hit_test_handle(_viewport_to_world(mb.position))
|
||||
@@ -230,6 +395,33 @@ func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
_dragging_handle = null
|
||||
|
||||
|
||||
func _handle_right_click(screen_pos: Vector2) -> void:
|
||||
if _skeleton == null or not is_instance_valid(_skeleton):
|
||||
return
|
||||
var world_pos := _viewport_to_world(screen_pos)
|
||||
var joint := _hit_test_bend_joint(world_pos)
|
||||
if joint.is_empty():
|
||||
return
|
||||
_context_joint = joint
|
||||
_context_menu.set_item_text(0, _context_menu_label(joint))
|
||||
_context_menu.popup(Rect2i(Vector2i(screen_pos), Vector2i(1, 1)))
|
||||
|
||||
|
||||
func _hit_test_bend_joint(world_pos: Vector2) -> String:
|
||||
var hit_radius := JOINT_HIT_RADIUS_PX / _camera.zoom.x
|
||||
var best_joint := ""
|
||||
var best_dist := hit_radius
|
||||
for joint: String in BEND_JOINTS:
|
||||
var bone: Bone2D = _bend_joint_bones.get(joint) as Bone2D
|
||||
if bone == null or not is_instance_valid(bone):
|
||||
continue
|
||||
var dist := world_pos.distance_to(bone.global_position)
|
||||
if dist <= best_dist:
|
||||
best_joint = joint
|
||||
best_dist = dist
|
||||
return best_joint
|
||||
|
||||
|
||||
func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
|
||||
if _is_panning:
|
||||
var delta := mm.position - _pan_last
|
||||
@@ -377,6 +569,65 @@ func _bone_color(bone_name: String) -> Color:
|
||||
return BONE_COLOR_RIGHT
|
||||
return BONE_COLOR_CENTRAL
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Facing profile & bend-direction toggles
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _facing_menu_label(profile: int) -> String:
|
||||
return ("[√] " if profile == _facing_profile else "") + FacingProfile.keys()[profile].capitalize()
|
||||
|
||||
|
||||
func _update_facing_menu_labels() -> void:
|
||||
if _facing_menu == null:
|
||||
return
|
||||
for profile: int in [FacingProfile.LEFT, FacingProfile.RIGHT, FacingProfile.FORWARD]:
|
||||
var idx := _facing_menu.get_item_index(profile)
|
||||
if idx >= 0:
|
||||
_facing_menu.set_item_text(idx, _facing_menu_label(profile))
|
||||
|
||||
|
||||
func _on_facing_menu_id_pressed(id: int) -> void:
|
||||
_apply_facing_profile(id)
|
||||
_update_facing_menu_labels()
|
||||
|
||||
|
||||
func _apply_facing_profile(profile: int) -> void:
|
||||
_facing_profile = profile
|
||||
for joint: String in BEND_JOINTS:
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
continue
|
||||
mod.flip_bend_direction = bool(PROFILE_FLAGS[profile][joint])
|
||||
_apply_body_z_order()
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
|
||||
func _apply_body_z_order() -> void:
|
||||
if _body_container == null or not is_instance_valid(_body_container):
|
||||
return
|
||||
var order: Array = Z_ORDER_BY_PROFILE.get(_facing_profile, Z_ORDER_BY_PROFILE[FacingProfile.FORWARD])
|
||||
for part_name: String in order:
|
||||
var part := _body_container.get_node_or_null(NodePath(part_name))
|
||||
if part != null:
|
||||
_body_container.move_child(part, _body_container.get_child_count() - 1)
|
||||
|
||||
|
||||
func _context_menu_label(joint: String) -> String:
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
return "Invert Bend"
|
||||
return "Normal Bend" if mod.flip_bend_direction else "Invert Bend"
|
||||
|
||||
|
||||
func _on_context_menu_id_pressed(_id: int) -> void:
|
||||
if _context_joint.is_empty():
|
||||
return
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(_context_joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
return
|
||||
mod.flip_bend_direction = not mod.flip_bend_direction
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Spawn / swap
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -407,6 +658,7 @@ func _load_and_spawn(path: String) -> void:
|
||||
|
||||
_resolve_rig_nodes()
|
||||
_ensure_modification_stack_enabled()
|
||||
_apply_facing_profile(_facing_profile)
|
||||
|
||||
_status_label.text = path.get_file()
|
||||
_recenter_camera()
|
||||
@@ -418,7 +670,12 @@ func _free_current_rig() -> void:
|
||||
_rig.queue_free()
|
||||
_rig = null
|
||||
_skeleton = null
|
||||
_body_container = null
|
||||
_ik_handles.clear()
|
||||
_bend_joint_bones.clear()
|
||||
_bend_modifications.clear()
|
||||
_coord_bones.clear()
|
||||
_context_joint = ""
|
||||
_dragging_handle = null
|
||||
|
||||
|
||||
@@ -427,6 +684,10 @@ func _resolve_rig_nodes() -> void:
|
||||
if _skeleton == null:
|
||||
push_warning("TestHarness: missing '%s' node in rig." % SKELETON_PATH)
|
||||
|
||||
_body_container = _rig.get_node_or_null(NodePath(BODY_CONTAINER_PATH)) as Node2D
|
||||
if _body_container == null:
|
||||
push_warning("TestHarness: missing '%s' node in rig." % BODY_CONTAINER_PATH)
|
||||
|
||||
_ik_handles.clear()
|
||||
for handle_name: String in IK_HANDLE_PATHS:
|
||||
var handle := _rig.get_node_or_null(NodePath(IK_HANDLE_PATHS[handle_name])) as Marker2D
|
||||
@@ -435,6 +696,53 @@ func _resolve_rig_nodes() -> void:
|
||||
else:
|
||||
push_warning("TestHarness: missing IK handle '%s'." % IK_HANDLE_PATHS[handle_name])
|
||||
|
||||
_resolve_bend_joints()
|
||||
_resolve_coord_bones()
|
||||
|
||||
|
||||
func _resolve_bend_joints() -> void:
|
||||
_bend_joint_bones.clear()
|
||||
_bend_modifications.clear()
|
||||
if _skeleton == null:
|
||||
return
|
||||
|
||||
for joint: String in BEND_JOINTS:
|
||||
var path: String = BEND_JOINT_BONE_PATHS[joint]
|
||||
var bone := _skeleton.get_node_or_null(NodePath(path)) as Bone2D
|
||||
if bone != null and is_instance_valid(bone):
|
||||
_bend_joint_bones[joint] = bone
|
||||
else:
|
||||
push_warning("TestHarness: missing bend-joint bone '%s'." % path)
|
||||
|
||||
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
||||
if stack == null:
|
||||
return
|
||||
for i: int in stack.modification_count:
|
||||
var mod := stack.get_modification(i)
|
||||
if not (mod is SkeletonModification2DTwoBoneIK):
|
||||
continue
|
||||
var ik := mod as SkeletonModification2DTwoBoneIK
|
||||
for joint: String in BEND_JOINTS:
|
||||
if ik.joint_two_bone2d_node == NodePath(BEND_JOINT_BONE_PATHS[joint]):
|
||||
_bend_modifications[joint] = ik
|
||||
break
|
||||
|
||||
for joint: String in BEND_JOINTS:
|
||||
if not _bend_modifications.has(joint):
|
||||
push_warning("TestHarness: missing TwoBoneIK modification for bend joint '%s'." % joint)
|
||||
|
||||
|
||||
func _resolve_coord_bones() -> void:
|
||||
_coord_bones.clear()
|
||||
if _skeleton == null:
|
||||
return
|
||||
for path: String in COORD_BONE_PATHS:
|
||||
var bone := _skeleton.get_node_or_null(NodePath(path)) as Bone2D
|
||||
if bone != null and is_instance_valid(bone):
|
||||
_coord_bones[path.get_file()] = bone
|
||||
else:
|
||||
push_warning("TestHarness: missing coordinate bone '%s'." % path)
|
||||
|
||||
|
||||
func _ensure_modification_stack_enabled() -> void:
|
||||
if _skeleton == null:
|
||||
@@ -467,3 +775,59 @@ func _on_show_bones_toggled(pressed: bool) -> void:
|
||||
func _on_show_ik_toggled(pressed: bool) -> void:
|
||||
_show_ik = pressed
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
|
||||
func _on_show_coords_toggled(pressed: bool) -> void:
|
||||
_show_coords = pressed
|
||||
if _coords_panel != null:
|
||||
_coords_panel.visible = pressed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coordinates display
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _show_coords or _coords_label == null:
|
||||
return
|
||||
_update_coords_display()
|
||||
|
||||
|
||||
func _update_coords_display() -> void:
|
||||
if _rig == null or not is_instance_valid(_rig):
|
||||
if _coords_label.text != "No rig loaded":
|
||||
_coords_label.text = "No rig loaded"
|
||||
return
|
||||
|
||||
var lines := PackedStringArray()
|
||||
|
||||
# Skeleton2D root.
|
||||
if _skeleton != null and is_instance_valid(_skeleton):
|
||||
lines.append("%s pos %s rot %s" % ["Skeleton2D".lpad(14), _fmt_vec2(_skeleton.global_position), _fmt_deg(_skeleton.global_rotation)])
|
||||
|
||||
# Bones.
|
||||
lines.append("Bones")
|
||||
for path: String in COORD_BONE_PATHS:
|
||||
var bone := _coord_bones.get(path.get_file(), null) as Bone2D
|
||||
if bone == null or not is_instance_valid(bone):
|
||||
continue
|
||||
lines.append("%s pos %s rot %s" % [path.get_file().lpad(14), _fmt_vec2(bone.global_position), _fmt_deg(bone.global_rotation)])
|
||||
|
||||
# IK targets.
|
||||
lines.append("IK Targets")
|
||||
for handle_name: String in IK_HANDLE_PATHS:
|
||||
var handle := _ik_handles.get(handle_name, null) as Marker2D
|
||||
if handle == null or not is_instance_valid(handle):
|
||||
continue
|
||||
lines.append("%s pos %s" % [handle_name.lpad(14), _fmt_vec2(handle.global_position)])
|
||||
|
||||
var text := "\n".join(lines)
|
||||
if _coords_label.text != text:
|
||||
_coords_label.text = text
|
||||
|
||||
|
||||
func _fmt_vec2(v: Vector2) -> String:
|
||||
return "(%8.1f, %8.1f)" % [v.x, v.y]
|
||||
|
||||
|
||||
func _fmt_deg(rad: float) -> String:
|
||||
return "%7.1f°" % rad_to_deg(rad)
|
||||
|
||||
Reference in New Issue
Block a user