- 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.
8.9 KiB
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
- Add
const GUIDE_JOINT_FOR_PART: Dictionary(the 10-row mapping above). - Bump
FILE_VERSION"1.4"→"1.5"; add"1.5"toSUPPORTED_VERSIONS; update the unsupported-version error message ('1.0'..'1.5'). - In
_collect_all_shape_data(), for each part compute the guide offset:(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 storevar 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"guide_offset": { "x": ..., "y": ... }in the part dict (uniform for all 10 parts). guide_offsetis write-only metadata likepivot/length— the load path ignores it (_apply_json_dataunchanged); v1.0–v1.4 files load unchanged and gain the key on their next save.
3. Adapter changes (scripts/stk_rig_adapter.gd)
- Read
guide_offset({x, y}) from the part dict (defaultnull/absent). - Only when present (old files keep the current offset-0 behavior), compute the anchor
offset:
delta = guide_offset + (A − C)whereA= the mount anchor already computed (the transformed joint endJ', or the transformed far endF_pt'when flipped — Round 3);C= the raw bbox center (the editor measuredguide_offsetfrom the bbox center).
- Convert to the node frame:
t = delta.rotated(-c_node)wherec_node= the part's driverRemoteTransform2D.global_rotationat apply time (the rig is not yet in the tree — this is the authored/guide pose frame; the driver later maps local+Yonto the bone, sotis a bone-relative placement, preserved as the bone flexes). - Apply
tto the mounted points (final translation, afterE/θ/scale/flip; independent of the flip logic). - Head: when
guide_offsetis present, apply it exactly like the other parts (anchorA= 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.5when the user aligned it there). When absent (old files), keep the Round 4HEAD_CHIN_DROPtranslation as the fallback. - 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 part →
Ais 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 frame →
c_nodeis the authored (guide) pose; at most ~15° pose difference for the legs — sub-pixel error for typical small offsets; documented.
6. Test plan
- Parse check:
..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit. - 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): anchorA= hip end atC + (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 aboutCtoC + (0, +197.5), soA − C = (0, +197.5);delta = (0, −195.75) + (0, 197.5) = (0, 1.75); assert the anchor point (drawn neck end) lands atdelta.rotated(-π) = (0, −1.75)local. - IK regression: with an offset applied, moving an IK target still rotates the Body nodes correctly.
- Part WITHOUT
- 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-partguide_offsetfor the 9 non-head parts; the harness then shows the placement 1:1. - 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
whole_stickman_preview.gd—get_guide_joint_preview().stickman_editor.gd— const map + version bump +guide_offsetin_collect_all_shape_data().stk_rig_adapter.gd— read/applyguide_offset.- Headless smoke test + parse check.
- Docs (BUGS.md Round 5 note, AGENTS.md, README.md).