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:
@@ -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`.
|
||||
Reference in New Issue
Block a user