Files
stickman/docs/phase9_round5_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

8.9 KiB
Raw Blame History

Phase 9 Round 5 — Feature: Guide-Relative Part Placement in the Harness

Overview

User request: for the torso, legs, and arms, attach the shapes to the rig relative to the silhouette guide in the editor — the harness stickman should represent how each part was placed on the editor's guide 1:1 (the guide's joint positions are the reference).

Current behavior

The adapter assumes perfect alignment: each part's joint end mounts exactly at the rig joint (offset 0), so any placement offset the user applied in the Whole Stickman preview is lost.

Design

The editor computes, at save time, each part's placement offset from its guide joint (a size-independent delta in preview pixels = master pixels, since GUIDE_SCALE = 1.0) and writes it as per-part write-only metadata. The adapter converts it to the joint-end offset and translates the mounted geometry accordingly, in the bone's frame.

1. Guide reference geometry (scripts/whole_stickman_preview.gd)

GUIDE_JOINTS (:59-73) gives every joint in master space. The part→joint mapping:

Part Guide joint
head Neck (the head bone origin / rig attachment, (0, 391.5) — NOT the circle center, which sits 72 px above the neck)
torso Hips
left_upper_arm LeftShoulder
left_lower_arm LeftElbow
right_upper_arm RightShoulder
right_lower_arm RightElbow
left_upper_leg Hips
left_lower_leg LeftKnee
right_upper_leg Hips
right_lower_leg RightKnee

2. Editor changes

2a. scripts/whole_stickman_preview.gd

Add a public method:

func get_guide_joint_preview(joint_name: String) -> Vector2:
    if not GUIDE_JOINTS.has(joint_name):
        push_warning("WholeStickmanPreview: unknown guide joint '%s'." % joint_name)
        return Vector2.ZERO
    return _guide_to_preview(GUIDE_JOINTS[joint_name])

2b. scripts/stickman_editor.gd

  1. Add const GUIDE_JOINT_FOR_PART: Dictionary (the 10-row mapping above).
  2. Bump FILE_VERSION "1.4""1.5"; add "1.5" to SUPPORTED_VERSIONS; update the unsupported-version error message ('1.0'..'1.5').
  3. In _collect_all_shape_data(), for each part compute the guide offset:
    var joint_preview := _whole_preview.get_guide_joint_preview(GUIDE_JOINT_FOR_PART[part_name])
    var center_preview := pos + Vector2(float(pl["pivot"].x), float(pl["pivot"].y))
    var guide_offset := center_preview - joint_preview
    
    (the part's bbox center in preview space minus the guide joint in preview space — both are preview-world coordinates, so the panel-size terms cancel and the delta is pure master-space pixels) and store "guide_offset": { "x": ..., "y": ... } in the part dict (uniform for all 10 parts).
  4. guide_offset is write-only metadata like pivot/length — the load path ignores it (_apply_json_data unchanged); v1.0v1.4 files load unchanged and gain the key on their next save.

3. Adapter changes (scripts/stk_rig_adapter.gd)

  1. Read guide_offset ({x, y}) from the part dict (default null/absent).
  2. Only when present (old files keep the current offset-0 behavior), compute the anchor offset: delta = guide_offset + (A C) where
    • A = the mount anchor already computed (the transformed joint end J', or the transformed far end F_pt' when flipped — Round 3);
    • C = the raw bbox center (the editor measured guide_offset from the bbox center).
  3. Convert to the node frame: t = delta.rotated(-c_node) where c_node = the part's driver RemoteTransform2D.global_rotation at apply time (the rig is not yet in the tree — this is the authored/guide pose frame; the driver later maps local +Y onto the bone, so t is a bone-relative placement, preserved as the bone flexes).
  4. Apply t to the mounted points (final translation, after E/θ/scale/flip; independent of the flip logic).
  5. Head: when guide_offset is present, apply it exactly like the other parts (anchor A = the chin, or the cap top when flipped; c_node ≈ 0) — the stored offset then reproduces the head's placement vs. the guide circle (chin at the circle bottom 363.5 when the user aligned it there). When absent (old files), keep the Round 4 HEAD_CHIN_DROP translation as the fallback.
  6. All driver lookups null-guarded (existing pattern).

Worked example (break.stk torso, assembled with its bbox center ≈195.75 px above the guide's Hips): guide_offset ≈ (0, 195.75); A C(0, ±197.5) (hip end below center, or the neck end above when flipped) → delta ≈ (0, ±2) — the torso's joint end lands within ~2 px of the Hips joint, reproducing the editor placement.

4. Files modified

File Changes
scripts/whole_stickman_preview.gd get_guide_joint_preview() public method.
scripts/stickman_editor.gd GUIDE_JOINT_FOR_PART const; FILE_VERSION "1.5" (+ SUPPORTED_VERSIONS + error text); guide_offset computed per non-head part in _collect_all_shape_data().
scripts/stk_rig_adapter.gd Read + apply guide_offset (node-frame translation, only when present).
docs/phase9_round5_bugfix_spec.md This file.

5. Edge cases

  • Old files without guide_offset → no translation (exact current behavior; the head keeps the Round 4 chin drop).
  • Empty part → the adapter skips before reading the offset (unchanged).
  • Flipped partA is the far end; the offset still reproduces the placement (the end the user placed at the joint).
  • Head → the guide joint is the Neck (the bone origin the head mounts at); a user aligned chin-at-circle-bottom yields guide_offset = (0, 72 half-height…)delta ≈ (0, +28) → chin at 363.5 (the Round 4 chin drop is the fallback for files without the key). Using the circle center instead would land the chin 72 px too high.
  • Guide moves on window resize → offsets are computed at save time from the live preview; re-save after resizing to refresh (note to user).
  • Rest vs IK pose framec_node is the authored (guide) pose; at most ~15° pose difference for the legs — sub-pixel error for typical small offsets; documented.

6. Test plan

  1. Parse check: ..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit.
  2. Headless adapter smoke test (synthetic stk dicts):
    • Part WITHOUT guide_offset: mounted anchor at local (0,0) (regression).
    • Torso WITH guide_offset = (0, 195.75) (rotation 0, scale (0.857, 3.99) like break.stk): anchor A = hip end at C + (0, +197.5); delta = (0, 195.75) + (0, 197.5) = (0, 1.75); t = delta.rotated(-π) = (0, 1.75) → assert the mounted hip end lands at local ≈ (0, 1.75) instead of (0, 0).
    • Torso WITHOUT the key → hip end at local (0, 0) exactly.
    • Flipped torso (rotation 180) WITH guide_offset = (0, 195.75): the 180° rotation moves the drawn neck end about C to C + (0, +197.5), so A C = (0, +197.5); delta = (0, 195.75) + (0, 197.5) = (0, 1.75); assert the anchor point (drawn neck end) lands at delta.rotated(-π) = (0, 1.75) local.
    • IK regression: with an offset applied, moving an IK target still rotates the Body nodes correctly.
  3. Editor-side verification: parse check + code review (the save computation); manual editor run (F5): load break.stk, Save, inspect the .stk → version "1.5" + per-part guide_offset for the 9 non-head parts; the harness then shows the placement 1:1.
  4. Cleanup temp test files.

7. Design decisions

# Decision Justification
D1 Editor computes the offset as bbox-center guide-joint (preview space) at save time Both points live in the same preview world; the delta cancels the panel-size term, so the stored value is a pure master-space vector.
D2 Store as per-part guide_offset (write-only), version bump to "1.5" Matches the pivot/length metadata precedent; load path untouched; old files default to offset 0.
D3 Adapter converts center-offset → anchor-offset (+ (A C)) and applies it in the driver's pose frame The anchor is the joint end the user placed; the driver frame keeps the placement bone-relative as the rig flexes.
D4 Head included, mapped to the guide Neck joint (not the circle center); Round 4 chin drop becomes the old-file fallback The neck is the head's rig attachment point; the circle center would misplace the chin by 72 px.
D5 Offset applied only when the key is present Old files keep exact current behavior (perfect-alignment assumption / chin drop).

8. Implementation order

  1. whole_stickman_preview.gdget_guide_joint_preview().
  2. stickman_editor.gd — const map + version bump + guide_offset in _collect_all_shape_data().
  3. stk_rig_adapter.gd — read/apply guide_offset.
  4. Headless smoke test + parse check.
  5. Docs (BUGS.md Round 5 note, AGENTS.md, README.md).