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