Files
stickman/docs/phase9_task2_zorder_spec.md
T
ryan 273090993e 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.
2026-08-23 22:21:23 -04:00

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:

  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.