- 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.
7.5 KiB
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:
- 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.
- 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.
- Facing forward → all four limb pairs draw in front of the torso.
- 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_containervia_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 theflip_bend_directionflags (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
Bodycontainer (foreign rig): null-guarded; z-order silently skipped. - Missing part nodes (partial
.stk): skipped byget_node_or_null; no warnings spam. - Re-spawn:
_body_containeris re-resolved per spawn;_free_current_rig()nulls it. z_index: all authoredBody/*nodes use defaultz_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
- Parse check:
..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit(user-requested; no errors). - Temporary headless SceneTree script (pattern from the Round 7 spec; deleted afterwards):
- Instantiate the harness scene,
add_childit, call_load_and_spawn("res://stickmen/basic.stk"). - Assert
Bodychildren order equalsZ_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 forRIGHT(mirrored). - Assert
Body/Headis the last child in all three profiles.
- Instantiate the harness scene,
- Manual F6 check: load a
.stk, switch Facing — far-side limbs visibly tuck behind the torso; head stays on top.
8. Implementation order
scripts/test_harness.gd— constants, state, resolution, reorder + hook.- Parse check + headless verification script (temp, then removed).
- Docs:
AGENTS.md,README.md,RIGGING.md.