- 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.
6.1 KiB
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):
- When
has_guide_offsetis true:joint_pos := center - guide_offset(the guide joint in the part's E-space; the editor storedguide_offset = (pos + C) − joint_preview, soC − 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.
- When
has_guide_offsetis false (old files): keep the current family rules + 180° flip heuristic exactly as today. theta,s, the Round 5 offsett = (guide_offset + (anchor − center)).rotated(−c_node), the head fallback (HEAD_CHIN_DROP), and everything downstream are unchanged — they already consumeanchor/vgenerically.
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_DROPfallback are unchanged.
5. Test plan
- Parse check:
..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit. - 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/LeftLowerArmelbow ≈ (0,0);Body/RightLowerLegknee ≈ (0,0);Body/RightUpperArmshoulder ≈ (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.
- 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
stk_rig_adapter.gd— nearest-end anchor selection (guarded byhas_guide_offset).- Headless smoke test + parse check.
- Docs (BUGS.md Round 6 note, AGENTS.md, README.md).