# Phase 9 Round 2 — Bugfix: Bone Visibility, IK Shape Following, Torso/Head Mount in the Harness ## Overview Testing `break.stk` in the test harness revealed four defects. This round fixes all four with changes confined to `scripts/stk_rig_adapter.gd` (mount pipeline) and `scripts/test_harness.gd` (debug bone overlay). Root causes were established with a headless probe of the live rig (spawned via `StickmanFactory` with the modification stack enabled, 2 frames). ### Root causes (verified) | # | Symptom | Root cause (probe-verified) | |---|---|---| | 1 | Lower-limb bones/joints invisible in "Show Bones" | `test_harness.gd` `_draw_bones()` draws parent-**origin** → bone-origin lines. For lower bones the parent origin is the *hip/shoulder* (upper bone origin), so the lower segment duplicates the upper segment; wrist/ankle joints are never drawn (they are bone *ends*, not origins). | | 2 | IK only moves lower-arm shapes; upper-arm shapes static | Round 1's `_neutralize_driver_rotations()` sets `update_rotation = false` on the 10 `RemoteTransform2D` drivers. Probe: every `Body/*` node shows `rot_deg = 0.00` while its bone rotates. Upper-limb visuals sit at fixed joints (shoulder/hip origins don't move) → static; lower-limb visuals *translate* with the elbow/knee but never rotate → appear "attached to the upper bones". | | 3 | Torso top connects at the hip joint | With driver rotation neutralized, the torso (anchor top-center, driver rotation π suppressed) hangs with its top at the hip-driven `Body/Body` origin. The user's torso is drawn hip-at-bottom (`break.stk` torso rect y∈[209,308], hip end at 308) and must mount with the **hip end at the origin**, extending up. | | 4 | Head features wrong scale / too thick / nose not triangular | `DEFAULT_LINE_WIDTH = 16.0` vs the editor's 2 px outline (`body_part_panel.gd:435`). Small eye/nose/mouth shapes get 16 px outlines that dominate their geometry. Additionally the `Head` `RemoteTransform2D` has a local position offset `(0.05, −72)` designed for the old 100-px-radius circle, so the mounted head floats 72 px above the neck joint. | --- ## 1. The rig's visual convention (verified numerically) The `RemoteTransform2D` drivers push **global** position + rotation onto the `Body/*` nodes. With rotation pushes enabled, each `Body/*` node's world rotation = `bone_pose_rotation + RT_local_rotation`. The authored RT local rotations are: | Part | RT local rotation | Bone pose → visual direction at rest/IK (probe-verified) | |---|---|---| | `Body/Body` (torso) | `π` (3.1415927) | hanging geometry `(0,+Y)` → world `−Y` (up) ✓ | | `Body/LeftUpperArm` | `+π/2` | hanging → world `−X` (left arm outward) ✓ | | `Body/RightUpperArm` | `−π/2` | hanging → world `+X` (right arm outward) ✓ | | `Body/LeftLowerArm` | `π` | hanging → world `−Y` (forearm direction) ✓ | | `Body/RightLowerArm` | `−π/2` | hanging → world `−Y` (forearm direction) ✓ | | `Body/LeftUpperLeg` / `RightUpperLeg` | `0` | hanging → world toward the knee ✓ | | `Body/LeftLowerLeg` / `RightLowerLeg` | `−π/2` | hanging → world toward the ankle ✓ | | `Body/Head` | `0` | upright (no rotation needed) ✓ | **Consequence:** mounted geometry must be authored in the rig's "hanging" convention — the joint end at the local origin, the far end along local `+Y` — and the drivers rotate it correctly **in any pose**, so IK flexing follows the bones exactly. (This replaces the Round 1 "clean unrotated frame + rotation neutralization" approach, which is what broke IK following.) ## 2. Fix 1 — `StkRigAdapter` mount pipeline (rotation compensation by hanging convention) ### 2a. Remove driver neutralization Delete `_neutralize_driver_rotations()` and its call in `apply()`. The drivers keep their defaults (`update_position/rotation/scale = true`). ### 2b. Mount transform (per part) For each part compute the bbox over all shape points. Let `cx = (min_x + max_x) / 2`, `cy = (min_y + max_y) / 2`, `width = max_x − min_x`, `height = max_y − min_y`, `long_axis_is_x = width >= height`, `extent = max(width, height)`. **Anchor `J`, scale `S` (drawn space), and alignment rotation `θ`:** | Part family | Anchor J | Scale S | θ (maps far end to local +Y) | |---|---|---|---| | Head | `(cx, max_y)` (bottom-center = chin) | `(1, 1)` | `0` (upright, extends `−Y`) | | Torso | `(cx, max_y)` (bottom-center = hip end) | `(1, s)` | `π` (drawn extends up → rotated to hang down; driver π flips it back up) | | Left limbs (arms + legs), long axis X | `(max_x, cy)` (joint at right end) | `(s, 1)` | `−π/2` (far end `−X` → `+Y`) | | Right limbs, long axis X | `(min_x, cy)` (joint at left end) | `(s, 1)` | `+π/2` (far end `+X` → `+Y`) | | Legs drawn vertically (long axis Y) | `(cx, min_y)` (hip at top) | `(1, s)` | `0` (already hanging) | | Arms drawn vertically (long axis Y) | `(cx, min_y)` (shoulder at top) | `(1, s)` | `0` (already hanging) | where `s = bone_length / extent` with `bone_length` from `_bone_length_for()` (unchanged), guarded: `extent <= 0.0001 → s = 1.0`. Head: no scale, no rotation. **Point transform** (replaces `_transform_points` math): ``` q = (P − J) ⋅ S # scale along the drawn long axis only (cross axis 1:1) pt_local = q.rotated(θ) # rotate into the hanging frame ``` Notes: - This preserves the Round 1 anisotropic-scaling requirement in effect: **only the long axis is scaled** (arms/legs = the drawn segment axis), cross-axis thickness stays 1:1. For horizontally-drawn arms it is mathematically identical to Round 1's `x·(bone/part_length)`. - `break.stk`'s legs are drawn **horizontally** (long axis X, extent ≈101), so under Round 1's literal "legs scale Y" they mounted as 101-px-thick bars. The long-axis rule fixes this: legs become ~200 long × 28 thick — matching the drawn/assembled proportions 1:1. - The torso pre-rotation π + the driver's π cancel at rest, so the mounted torso appears with the hip end at the hip joint and the top at `−torso_length` (the neck) — exactly where the user assembled it (their preview scale 3.99 ≈ 391.5/99 = 3.955). ### 2c. Head joint placement The `Head` driver (`Skeleton2D/Torso/Head/RemoteTransform2D`) has local position `(0.0503, −72.0)` — authored to center the old 100-px circle 72 px above the neck. In `_fit_bones()` (head section), **zero the driver's local position** so `Body/Head` sits on the neck joint. The mounted head (chin at local origin) then lands with the chin on the neck and rotates about the neck with the head bone's LookAt. ### 2d. Line width `DEFAULT_LINE_WIDTH := 2.0` (was 16.0) — matches the editor's outline width (`body_part_panel.gd:435` `width: float = 2.0`), so eyes/nose/mouth/cap render 1:1. ### 2e. Keep `_reset_node_transform()` (scale `(1,1)`, rotation `0` before mounting — drivers overwrite each frame anyway), head `set_script(null)`, `_fit_bones` bone fitting, `_recalibrate_ik`, `_bone_length_for`, node-path constants, bbox recompute (file `pivot`/`length` still ignored). ## 3. Fix 2 — `test_harness.gd` bone overlay Rewrite `_draw_bones()`: 1. For each bone (all 10): draw a joint dot at `bone.global_position`. 2. For each **Bone2D child** of the bone: draw a line `origin → child.global_position` (true bone segment; works for nested lower bones). 3. For **leaf bones** (no Bone2D child): draw a line from the origin to the bone's corresponding IK target (map: `LeftLowerArm → IK_Targets/Left_Hand`, `RightLowerArm → IK_Targets/Right_Hand`, `LeftLowerLeg → IK_Targets/Left_Leg`, `RightLowerLeg → IK_Targets/Right_Leg`, `Head → IK_Targets/Head`) — shows the forearm/shin segments and the wrist/ankle joints; falls back to `origin + R(global_rotation) · (length, 0)` when the target node is missing. 4. Keep the existing colors/widths (`_bone_color`). Result: 9 distinct segments (spine, neck-head, both arms ×2, both legs ×2) + 10 bone-origin dots + target joints — lower limbs clearly visible. ## 4. Files modified | File | Changes | |---|---| | `scripts/stk_rig_adapter.gd` | Remove `_neutralize_driver_rotations()` + call; new anchor/scale/rotation table in `_mount_shapes()` + `_compute_anchor`/`_compute_scale`/rotation helpers; `DEFAULT_LINE_WIDTH = 2.0`; zero the Head driver's local position in `_fit_bones()`; update doc comments. | | `scripts/test_harness.gd` | Rewrite `_draw_bones()` (child-direction segments + leaf→IK-target segments + dots). | | `docs/phase9_round2_bugfix_spec.md` | This file. | ## 5. Edge cases - Empty part → skip (unchanged). `extent <= 0` → `s = 1`. - Vertically-drawn legs/arms → θ = 0 (hanging already); horizontally-drawn → ±π/2. - Torso drawn with hip at top (not bottom) would mount flipped — accepted assumption (mirrors Round 1's shoulder-inward assumption); `break.stk` has hip at bottom. - Multi-shape parts: bbox over all shapes (unchanged). - Old files without `pivot`/`length`: unaffected (bbox recomputed). - Missing driver/target nodes: null-guarded warnings (unchanged pattern). ## 6. Test plan 1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit`. 2. Headless smoke test (SceneTree, spawn `break.stk` via `StickmanFactory`, enable the modification stack, wait 2 frames), asserting: - No `update_rotation == false` on any of the 10 drivers. - `Body/*` world rotations ≈ their bone's world rotation + RT local rotation (e.g. `Body/LeftUpperArm.global_rotation ≈ LeftUpperArm.global_rotation + π/2`). - Mounted geometry hangs: `Body/LeftUpperLeg` shape points start at the node origin and extend toward `+Y` local, with Y extent ≈ 200 (bone length) and X extent ≈ 28 (thickness). - `Body/Body` (torso) shape Y extent ≈ 391.5, X extent ≈ 35. - `Body/Head` shape chin point ≈ local origin; the Head driver position == (0,0); mounted Line2D width == 2.0. - Bones overlay data: 10 bones; each lower bone's parent is the upper bone (sanity). - Then SET a pose override or drag an IK target (move `IK_Targets/Left_Hand`), await a frame, and assert `Body/LeftUpperArm.global_rotation` **changed** with the bone (upper arm follows) and `Body/LeftLowerArm.global_position` moved with the elbow. 3. Harness manual check (F6): lower limb bones + wrist/ankle joints visible; drag hand IK → upper arm shape rotates at the shoulder and lower arm shape flexes at the elbow. 4. Cleanup: delete the temporary test script. ## 7. Design decisions | # | Decision | Justification | |---|---|---| | D1 | Restore driver rotation pushes; drop `_neutralize_driver_rotations()` | Probe-verified root cause of the IK-following bug; the hanging convention + driver rotations track bones in every pose. | | D2 | Mount geometry in the rig's hanging frame (joint at origin, far end `+Y`) with a per-part alignment rotation | Makes the mount pose-agnostic; no per-part hardcoded compensation table needed. | | D3 | Scale only the drawn long axis (auto-detected); cross axis 1:1 | Fixes `break.stk`'s horizontally-drawn legs (101-px bars → 200×28) and keeps Round 1's anisotropic-scaling intent. | | D4 | Torso: anchor bottom-center + θ = π | Hip end at the hip joint; top lands at the neck, matching the user's assembled torso (preview scale ≈ 391.5/99). | | D5 | Head: zero the Head driver's local position | Chin lands on the neck joint; head rotates about the neck with LookAt. | | D6 | `DEFAULT_LINE_WIDTH = 2.0` | Matches the editor's 2 px outline — features render 1:1. | | D7 | Harness overlay: parent→child segments + leaf→IK-target segments | Shows true lower-limb bones and wrist/ankle joints. | ## 8. Open questions — RESOLVED (user-approved) 1. **Legs drawn horizontally (break.stk).** ✅ **Auto-detect the long axis** (rotate to hang, scale the long axis) — replaces Round 1's fixed "legs = Y axis". 2. **Figure pose = rig rest/IK pose.** ✅ **Rig pose, shapes 1:1** — part transforms remain v1-ignored; the drawn shapes are fitted 1:1 onto the rig's bones. ## 9. Implementation order 1. `stk_rig_adapter.gd` — remove driver neutralization; new anchor/scale/θ helpers. 2. `stk_rig_adapter.gd` — rewrite `_mount_shapes()`/`_mount_shape()`/`_transform_points()`; `DEFAULT_LINE_WIDTH = 2.0`; zero Head driver position. 3. `test_harness.gd` — rewrite `_draw_bones()`. 4. Headless smoke test + parse check. 5. Docs (`BUGS.md` Round 2 note, `AGENTS.md`, `README.md`).