feat: Implement draggable torso and head IK targets in the test harness

- Added draggable handles for the torso and head to the test harness.
- Updated `IK_HANDLE_PATHS` to include new entries for "Head" and "Torso".
- Implemented distinct colors for the torso (magenta) and head (yellow) markers.
- Added a visual aid (aim line) to indicate the head's LookAt target direction.
- Ensured that dragging the torso moves only the torso marker, allowing for limb stretching towards stationary targets.
This commit is contained in:
2026-08-21 12:39:31 -04:00
parent 6b273c049c
commit ab5c79ab6a
14 changed files with 1459 additions and 174 deletions
+103
View File
@@ -0,0 +1,103 @@
# Phase 9 Round 6 — Bugfix: Joint-End Anchor Selection via the Guide Placement
## Overview
User report: the **lower left leg** and **lower right arm** are mounted 180° off their bones
(the far end attaches at the joint). Root cause (verified against `break.stk` data): the
adapter picks the joint end with fixed per-side family rules (left limbs → drawn `max_x` end,
right limbs → drawn `min_x` end, plus a 180° flip heuristic), but the user's drawn-side
conventions are **inconsistent across parts** — e.g. the left lower leg's knee is at the drawn
`min_x` end while the rule assumes `max_x`. The only reliable signal for which drawn end is
the joint is the **user's placement**, which Round 5 now stores as `guide_offset`.
## 1. Verified diagnosis (break.stk, real data)
`joint_in_part_space = C guide_offset` (C = raw bbox center; both ends E-transformed):
| Part | rule anchor | dist to joint | other end | dist to joint | correct end |
|---|---|---|---|---|---|
| `left_lower_leg` (rot 90) | E(max_x) = ankle | ≈199 | E(min_x) = **knee** | ≈11 | min_x ✗ (bug) |
| `right_lower_arm` (rot 90) | E(min_x) = wrist | ≈204 | E(max_x) = **elbow** | ≈2 | max_x ✗ (bug) |
| `left_lower_arm` (rot 90) | E(max_x) = elbow | ≈4 | E(min_x) = wrist | ≈204 | max_x ✓ |
| `right_lower_leg` (rot 90) | E(min_x) = knee | ≈6 | E(max_x) = ankle | ≈199 | min_x ✓ |
| `right_upper_arm` (rot 180) | flip → E(max_x) = shoulder | ≈7 | E(min_x) = hand | ≈178 | max_x ✓ (flip heuristic) |
| `torso` (rot 180) | flip → E(neck end) | ≈5 | E(hip end) | ≈400 | neck ✓ (flip heuristic) |
| `head` | E(chin) | ≈43 | E(cap top) | ≈191 | chin ✓ |
**Rule:** when `guide_offset` is present, choose as the joint anchor whichever transformed end
(`E(J_raw)` or `E(F_pt_raw)`) is **nearest to `C guide_offset`** (the part's guide joint in
part space). This fixes both buggy parts, preserves every correct case, and naturally
reproduces the 180° flip behavior (a flipped part's far end lands near the joint).
## 2. Fix specification — `scripts/stk_rig_adapter.gd`
In `_compute_mount_transform()` (the anchor/flip section, ~lines 397407):
1. When `has_guide_offset` is true:
- `joint_pos := center - guide_offset` (the guide joint in the part's E-space; the editor
stored `guide_offset = (pos + C) joint_preview`, so `C guide_offset = joint_preview
pos` — the joint position in the adapter's local frame, up to the preview translation
which is distance-preserving).
- `d_joint := j_prime.distance_to(joint_pos)`; `d_far := f_pt_prime.distance_to(joint_pos)`.
- If `d_far < d_joint` (strict): `anchor = f_pt_prime`, `v = -f_prime` (the far end
attaches — replaces the flip heuristic for this case).
Else: `anchor = j_prime`, `v = f_prime`.
2. When `has_guide_offset` is **false** (old files): keep the current family rules + 180°
flip heuristic exactly as today.
3. `theta`, `s`, the Round 5 offset `t = (guide_offset + (anchor center)).rotated(c_node)`,
the head fallback (`HEAD_CHIN_DROP`), and everything downstream are **unchanged** — they
already consume `anchor`/`v` generically.
Note: this answers the user's hypothesis directly — the pivot point was indeed on the wrong
drawn side; instead of guessing per-side conventions (or baking rotation/scale, which does not
record which end is the joint), the stored guide placement decides the pivot side.
## 3. Files modified
| File | Changes |
|---|---|
| `scripts/stk_rig_adapter.gd` | Nearest-end anchor selection when `guide_offset` is present (replaces the family-side + flip choice for that case). |
| `docs/phase9_round6_bugfix_spec.md` | This file. |
## 4. Edge cases
- **Old files without `guide_offset`** → family rules + flip heuristic (current behavior).
- **Tie** (strict `<`) → keeps the family-rule end.
- **Flipped parts with `guide_offset`** → the far end is naturally nearest (the flip behavior
is preserved without the heuristic).
- **Misplaced parts** (placement error > half the part length) → the wrong end may win;
degenerate input, acceptable.
- **Head** → the chin is nearest the Neck (≈43 vs ≈191); the Round 5 offset and the
`HEAD_CHIN_DROP` fallback are unchanged.
## 5. Test plan
1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit`.
2. Headless smoke test (spawn the current `break.stk`):
- `Body/LeftLowerLeg`: the mounted knee end ≈ local (0, 0) (was the ankle end) and the far
end ≈ (0, +200) — shin hangs from the knee.
- `Body/RightLowerArm`: the mounted elbow end ≈ local (0, 0), far end ≈ (0, +200).
- Regressions: `Body/LeftLowerArm` elbow ≈ (0,0); `Body/RightLowerLeg` knee ≈ (0,0);
`Body/RightUpperArm` shoulder ≈ (0,0) (flip case via nearest rule); torso flipped neck
end ≈ (0,0); head chin ≈ (0, +28) (guide-offset present → no chin-drop, chin ≈ 28 from
the neck per the stored offset); all parts' far ends along +Y local (bone-aligned).
- No-guide-offset fallback (synthetic dict without the key): lower limbs use the family
rules + flip heuristic (regression: flipped torso anchor = far end; 90° limbs anchored at
the family side).
- IK regression: moving a hand target still rotates the body nodes with the bones.
3. Cleanup temp test files.
## 6. Design decisions
| # | Decision | Justification |
|---|---|---|
| D1 | Joint end = the fitted end nearest the stored guide joint (`C guide_offset`) | The drawn-side conventions are inconsistent per part; the placement is the ground truth. |
| D2 | Nearest rule only when `guide_offset` is present | Old files keep exact current behavior. |
| D3 | Keep θ/s/t and the head fallback unchanged | They consume `anchor`/`v` generically; the placement math is already correct. |
| D4 | No baking of rotation/scale into the .stk | Baking alone cannot record which drawn end is the joint; the stored placement already encodes it. |
## 7. Implementation order
1. `stk_rig_adapter.gd` — nearest-end anchor selection (guarded by `has_guide_offset`).
2. Headless smoke test + parse check.
3. Docs (BUGS.md Round 6 note, AGENTS.md, README.md).