- 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.
104 lines
6.1 KiB
Markdown
104 lines
6.1 KiB
Markdown
# 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 397–407):
|
||
|
||
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).
|