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

6.1 KiB
Raw Blame History

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