# Phase 9 Round 3 — Bugfix: Overlay Head Bone, One Node Per Shape, Part Rotation/Scale in the Harness ## Overview Three defects remain after Round 2 (user report, testing `break.stk` in the harness): 1. **"The head bone has another bone extending out of it"** — the harness bone overlay draws the Head leaf bone as a segment to `IK_Targets/Head` (0, −624), the `SkeletonModification2DLookAt` target — a long line sticking out of the head. It should draw the head bone along its own direction instead. 2. **Duplicate nodes per closed shape** — `_mount_shape()` mounts closed shapes as a `Polygon2D` fill **plus** a `Line2D` outline (e.g. `Body/Head/@Polygon2D@165` + `@Line2D@166`). Per the user, closed shapes should mount as **just the Polygon2D** and open shapes as **just one Line2D**. 3. **Part rotation & scale not applied** — the adapter ignores the per-part `rotation` (degrees) and `scale` the user applied in the Whole Stickman preview. A torso rotated 180° looks identical to an unrotated one; a head scaled ~2× in the editor (≈160×160 px) renders ~80×80 in the harness. --- ## 1. Root-cause analysis ### 1a. Overlay head segment (`scripts/test_harness.gd`) `_draw_bones()` treats every leaf bone identically: draw to its IK target. For the four limbs the targets ARE the wrist/ankle joints (correct). For the Head the target is a LookAt aim point 232 px above the neck — drawn as a "bone" it looks broken. The Head bone is authored with `length = 90`, `bone_angle = −90` (points up), so its true tip is `origin + Vector2(0, -length).rotated(global_rotation)` — a 90 px segment inside the head. **Fix:** special-case the Head leaf — draw along the bone's own direction; keep the limb leaf bones drawing to their IK targets. ### 1b. Duplicate nodes (`scripts/stk_rig_adapter.gd` `_mount_shape`) Closed shapes mount `Polygon2D` + closed `Line2D` outline (Round 1 mirrored the editor's fill+outline rendering). The user wants one node per shape: closed → `Polygon2D` only; open → `Line2D` only. `DEFAULT_LINE_WIDTH = 2.0` still applies to open lines. ### 1c. Part rotation/scale (`scripts/stk_rig_adapter.gd` mount pipeline) The `.stk` part dict carries `rotation` (degrees, about the part bbox center) and `scale` (about the bbox center) from the Whole Stickman preview (`whole_stickman_preview.gd:500-506`: `world = C + R(rot)·S·(pt − C)`, `C =` bbox center). The adapter currently mounts the raw drawn geometry, ignoring both. The user's requirement: the shapes must appear in the harness exactly as rotated/scaled in the editor (the guide in the editor IS the rig's rest pose). **Full application is possible without detaching limbs** — the earlier "flip-only" concern was based on computing the alignment on the *raw* geometry and then rotating on top, which double-rotates. The correct composition (per user direction) is: 1. Apply the part transform **first**: `Q = E(P) = C + R(rot)·S·(P − C)` for every point (scale then rotate about the bbox center — the editor's exact transform). 2. Compute the anchor and alignment **on the transformed geometry**: - `J' = E(J_raw)` (the drawn joint end, transformed). - `F' = E(J_raw + F_raw) − J' = R(rot)·S·F_raw` (the transformed far vector). 3. Alignment `θ = F'.normalized().angle_to(Vector2.DOWN)` — rotates the fitted long axis onto the bone. Because the user aligned the shape to the guide (the bone direction), θ is the residual between their drawn orientation and the bone; the shape appears in the harness as in the editor, attached at the joint. 90° rotations (break.stk's lower limbs — sideways drawn bars turned vertical) work: `F'` points along the fitted limb, `θ` keeps it hanging, and the limb stays connected to the elbow/knee. 4. **180° flips** (rotations whose wrapped value is within ±45° of 180°): a flip about the center swaps the ends of the shape. In the editor the flipped shape shows its drawn far end at the joint region, so the harness attaches the **drawn far end** at the joint: - `flipped = |wrapf(rot_rad, -PI, PI)| > PI * 0.75` - if flipped: anchor `A = E(far_raw_end)`, far vector `V = −F'`; else `A = J'`, `V = F'`. The flipped part then hangs the other way along the bone with its content turned around — the rotation is visibly applied (e.g. the 180° torso shows its drawn neck end at the hip joint and the drawn hip end up at the neck — matching the editor's flipped torso). 5. Bone fit scale `s = bone_length / V.length()` measured on the transformed extent (so a user-scaled part is not double-fitted; `s ≈ 1` when the user already scaled to the bone length — verified numerically for every `break.stk` part). Guards: `|V| <= 0.0001 → s = 1, θ = 0`. 6. Mounted points: `v = R(θ)·(Q − A)`; `v.y *= s`. 7. **Head special case (unchanged semantics):** `θ = 0`, `s = 1` (the head mounts upright, chin at the origin, extending `−Y`; the head driver has no rotation). The flip anchor rule still applies (a 180° head attaches at its cap top — upside-down, chin up — matching the editor). Part scale applies through `E` (head face ≈160 px: drawn 80 × 1.986). --- ## 2. Fix specification ### 2a. `scripts/test_harness.gd` — head leaf segment In `_draw_bones()`, for the leaf bone `Head` (no Bone2D child): draw `origin → origin + Vector2(0, -length).rotated(global_rotation)` instead of the IK-target segment. Limb leaf bones (LeftLowerArm, RightLowerArm, LeftLowerLeg, RightLowerLeg) keep the IK-target segments. Implement via a per-bone check (`bone.name == "Head"`) or by removing `Head` from `LEAF_BONE_IK_PATHS` and handling it in the no-target fallback (the fallback already draws `origin + R(global_rotation)·(length, 0)` — change it to use the bone's `bone_angle` direction: `Vector2(length, 0).rotated(deg_to_rad(bone.bone_angle)).rotated(global_rotation)`; for the Head (`bone_angle = −90`) that yields `(0, −90)` rotated by the pose — the desired segment; for other bones `bone_angle` is 0 → `(length, 0)` — same as today). ### 2b. `scripts/stk_rig_adapter.gd` — one node per shape `_mount_shape()`: closed → `Polygon2D` only (polygon + color; **no** outline Line2D); open → `Line2D` (width `DEFAULT_LINE_WIDTH`). Delete the outline branch. ### 2c. `scripts/stk_rig_adapter.gd` — part transform composition Read per-part `rotation` (float, **degrees**, default 0.0) and `scale` (`{x, y}`, default `(1, 1)`) from the part dict. Apply in the mount pipeline: 1. Raw bbox `C` (center), raw anchor `J_raw`, raw far point `F_pt_raw = J_raw + F_raw` (direction from the joint end to the far end, per the Round 2 family rules), as today. 2. `E(P) = C + R(rot_rad)·S_part·(P − C)` — the preview's part transform, applied to every point and to the anchor/far point: `J' = E(J_raw)`, `F_pt' = E(F_pt_raw)`, `F' = F_pt' − J'`. 3. `flipped = |wrapf(rot_rad, -PI, PI)| > PI * 0.75`. 4. Anchor `A` and far vector `V`: `A = F_pt'`, `V = −F'` if flipped, else `A = J'`, `V = F'`. 5. `θ = V.normalized().angle_to(Vector2.DOWN)` (0 if `|V| <= 0.0001`). 6. `s = bone_length / |V|` (head → 1.0; guards `|V| <= 0.0001 → 1.0`). 7. Mounted points: `v = R(θ)·(Q − A)`; `v.y *= s`. Head: `θ = 0`, `s = 1` (upright mount, flip anchor rule still applies). **Example (torso, rot 180°, scale (0.857, 3.99)):** `E` flips the drawn torso; `J'` = the transformed hip end (now the fitted top), `F_pt'` = the transformed neck end (fitted bottom); flipped → `A` = the neck end; `V` points up → `θ = π`; `s ≈ 391.5/395`; the torso extends up from the hip joint with the drawn **neck end attached at the hip** and the drawn hip end up at the neck — the 180° rotation visibly applied (unrotated: hip end at the hip, neck at the neck). **Example (lower arm, rot 90°):** not flipped; `J'` = the elbow (fitted bottom); `F'` points up (the editor's bent forearm) → `θ = π`; the forearm hangs along the bone from the elbow with its content turned 90° — exactly as assembled in the editor. **Scale check:** part scale composes into `E` and the fit `s` is measured on the transformed extent → no double-fitting. Head: `s = 1` but `E` applies `1.986` → the face mounts at ≈160 px (drawn 80 × 1.986). ### 2d. Keep (unchanged) `_reset_node_transform`, head `set_script(null)`, head-driver zeroing, `_fit_bones`, `_recalibrate_ik`, `_bone_length_for`, `_compute_part_bbox`, width 2.0, all node-path constants, null guards. --- ## 3. Files modified | File | Changes | |---|---| | `scripts/stk_rig_adapter.gd` | `_mount_shapes()` reads part `rotation`/`scale`; mount pipeline composes `E_full` + fitted anchor/θ/fit-scale + flip heuristic; `_mount_shape()` mounts one node per shape (closed → Polygon2D only). | | `scripts/test_harness.gd` | `_draw_bones()` leaf fallback uses the bone's `bone_angle` direction (Head draws a 90 px segment inside the head; limbs unchanged). | | `docs/phase9_round3_bugfix_spec.md` | This file. | ## 4. Open question — RESOLVED (user directive) 1. **Rotation semantics.** User directive: *the shapes must appear in the harness exactly as rotated/scaled in the editor — that is the point of the guide.* ✅ **Full application**: the part transform `E` is applied to the geometry first, and the mount (anchor + alignment + fit) is computed on the transformed geometry, so every rotation is rendered. Rotations align the drawn long axis onto the bone (a 90° rotation turns a sideways-drawn bar into the limb as assembled — the limb stays attached to its joint; this does **not** detach limbs because the anchor is the transformed joint end, not a raw-geometry point). 180° flips additionally swap the attachment to the drawn far end (the end the user rotated into the joint position), making flips visible. ## 5. Edge cases - Rotation absent (old files) → 0.0; scale absent → (1, 1) — Round 2 behavior unchanged. - Negative part scale (mirroring) → `E_full` mirrors the points; `θ` adapts; fine. - `F'` ≈ 0 (degenerate) → `s = 1`, `θ = 0`, no flip effect. - Flip + IK: the flip is baked into the mounted points, so IK flexing still follows the bones. - Multi-shape parts: bbox/transform over all shapes (unchanged). - `-360`-style rotations normalize via `wrapf` ✓. ## 6. Test plan 1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit`. 2. Headless smoke test (spawn `break.stk`, stack enabled, 2 frames): - Every `Body/*` node's children: closed shapes → exactly one Polygon2D and zero Line2D for that shape (count Polygon2D == number of closed shapes; Line2D count == number of open shapes; no node has both for the same shape). - Head: mounted face width ≈ 159–160 px (bbox width 108 × 1.986 ≈ 215 incl. cap; the face circle x-span ≈ 160), chin still at local origin. - Legs/arms still hang along bones: `Body/LeftUpperLeg` far end ≈ (0, +~200) local after `θ`+fit; thickness ≈ 24 (drawn 28 × part-scale 0.853). - Flip: temporarily modify the stk dict — set `torso.rotation = 180.0` — re-apply to a fresh rig, assert the drawn torso **neck end** now lands ≈ (0, 0) (hip joint) and the drawn **hip end** ≈ (0, −391.5) (neck) — the ends swapped vs. rotation 0 (hip end at the hip). Restore rotation 0 → hip end back at (0, 0). - Overlay: `test_harness.gd` head-leaf drawing path exists (code review; the overlay draws cannot be asserted headlessly — verified by review + manual F6). - Regression: driver rotations enabled; IK-following still works (move Left_Hand target, assert Body/LeftUpperArm rotation tracks bone + π/2). 3. Manual harness check (F6): no bone sticking out of the head; single nodes in the Remote Inspector; torso flip visible after rotating 180° in the editor and saving. ## 7. Design decisions | # | Decision | Justification | |---|---|---| | D1 | Head leaf overlay segment along the bone's own direction (`bone_angle`-aware fallback) | The LookAt target is an aim point, not a joint; the bone itself is neck→head-top. | | D2 | One node per shape (closed → Polygon2D only) | User request; removes node duplication in the Body tree. | | D3 | Apply part scale fully (via `E`) + measure bone-fit scale on the transformed extent | Reproduces the editor scale (head 160 px) without double-fitting bone-scaled parts. | | D4 | Apply part rotation fully: `E` first, then anchor/alignment/fit on the transformed geometry; 180° flips swap the attachment to the drawn far end | User directive (editor fidelity); the transformed joint anchor keeps 90°-rotated limbs attached to their bones. | | D5 | Flip shown via the anchor swap (no separate content rotation) | Matches the editor: the flipped shape's far end sits at the joint region; content turns around on the bone. | ## 8. Implementation order 1. `stk_rig_adapter.gd` — part `rotation`/`scale` read + `E_full`/`J'`/`F'`/`θ`/fit/flip in `_mount_shapes()` + `_compute_mount_transform()` restructure. 2. `stk_rig_adapter.gd` — `_mount_shape()` single-node-per-shape. 3. `test_harness.gd` — head-leaf overlay segment. 4. Headless smoke test + parse check. 5. Docs (`BUGS.md` Round 3 note, `AGENTS.md`, `README.md`).