Files
stickman/docs/phase9_round6_bugfix_spec.md
T
ryan ab5c79ab6a 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.
2026-08-21 12:39:31 -04:00

104 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).