# 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).