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,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