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:
2026-08-23 22:21:23 -04:00
parent ab5c79ab6a
commit 273090993e
9 changed files with 1013 additions and 26 deletions
+146
View File
@@ -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.