- Implemented StkRigAdapter class to adapt a master rig to a loaded .stk dictionary. - Added methods for fitting bone lengths, recalibrating IK targets, and mounting vector shapes. - Defined constants for default proportions and bone paths. - Included error handling for missing nodes and invalid data structures.
18 KiB
Phase 7 Round 1 — Architectural Specification
Overview
This round fixes three user-reported bugs in the pose silhouette guide introduced in Phase 7 (see BUGS.md, "Stickman editor (Phase 7 Round 1)"):
- Silhouette starting position — on startup the guide is anchored at a fixed world position (
GUIDE_OFFSET = (170, 580)) that puts the figure up-left of the preview, forcing the user to pan over to it. The guide should start centered in the Whole Stickman view. - Menu label never changes — View → "Show Pose Guide" toggles visibility but its text stays "Show Pose Guide" regardless of state. It must read "Hide Pose Guide" when visible and "Show Pose Guide" when hidden.
- Guide renders behind parts — the guide is drawn below user parts, so it cannot be used to judge how well parts align to the joints/limbs. It should "ghost" in front of the parts.
All three are confined to two scripts (scripts/whole_stickman_preview.gd, scripts/stickman_editor.gd) and touch no .stk format and no settings.json schema (the show_pose_guide key already exists). No .tscn changes are required.
Key behavior that is preserved (must not regress):
- The guide is a world-space fixture: it pans/zooms with the grid and parts (drawn under
draw_set_transform(_pan_offset, 0, Vector2(_zoom, _zoom))). - The guide is pure drawing — no
_gui_inputhit-testing — so it never intercepts part dragging/selection or the right-click context menu. - 1:1 master-rig scale (
GUIDE_SCALE = 1.0), color-coded limbs (left cyan-blue, right orange-red, central white), 6 px constant joint dots. Reset Viewsrestores zoom 1.0 / pan (0,0); Clear/Load do not touch guide visibility.
1. Bug 1 — Centering the guide at startup
1a. Problem
GUIDE_OFFSET = Vector2(170.0, 580.0) (a const at whole_stickman_preview.gd:48) anchors the figure's hips at preview-world (170, 580). The 1:1 figure spans roughly world x ∈ [2, 338], y ∈ [16.5, 956] (center ≈ (170, 486)). On a typical panel (~640 × 600 px) at the default view (zoom 1, pan 0), that places the head near the top-left and the feet far below the fold — the user must pan to reach a centered figure.
1b. Recommended approach — dynamic, draw-time centering
Replace the static GUIDE_OFFSET with a figure-center constant and compute the offset at draw time from the live preview_area.size. This centers the figure's bounding-box center at the preview-area center in world space, so at the default view (zoom 1, pan 0) the figure is centered on screen, and it re-centers automatically on window resize (because preview_area.size is re-read on every draw).
Why this approach:
- Deterministic & resize-proof — centering is a pure function of the current viewport, recomputed every frame; no reliance on a guessed window size or on
_ready()-time layout (which is not yet settled when the script initializes). - Minimal — a one-line change to
_guide_to_preview()plus a renamed constant; no new state, no camera/timing machinery. - Preserves world-fixture semantics — the guide is not screen-locked; it still moves with pan/zoom. "Centered" is true at the default view and after
Reset Views, and the user can still pan/zoom the guide anywhere afterward.
1c. Code changes (scripts/whole_stickman_preview.gd)
Replace the constant (line 48):
const GUIDE_OFFSET: Vector2 = Vector2(170.0, 580.0) # master_rig (0,0) [hips] -> preview world
with:
const GUIDE_FIGURE_CENTER: Vector2 = Vector2(0.0, -93.75) # master-space bbox center of the 1:1 guide figure
Replace _guide_to_preview() (lines 468–469):
func _guide_to_preview(master_pos: Vector2) -> Vector2:
return master_pos * GUIDE_SCALE + GUIDE_OFFSET
with:
func _guide_to_preview(master_pos: Vector2) -> Vector2:
# Center the guide figure's bounding box at the preview-area center.
# At zoom 1 / pan (0,0) — startup and Reset Views — this is the on-screen center.
return (master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE + preview_area.size * 0.5
GUIDE_FIGURE_CENTER derivation (mirrors phase7_spec.md §3 style):
| Bound | Master-space | Source |
|---|---|---|
| Top | -563.5 |
Head circle center (0, -463.5) minus GUIDE_HEAD_RADIUS (100) |
| Bottom | +376.0 |
Ankle joints (LeftAnkle/RightAnkle) |
| Left / Right | ±168.0 |
Elbow/Wrist joints |
- Center X =
0(figure is left/right symmetric). - Center Y =
(-563.5 + 376.0) / 2 = -93.75.
GUIDE_SCALEstays1.0andGUIDE_HEAD_RADIUSstays100.0— the bug is about position, not size.
1d. Alternatives considered (not chosen)
| Option | Description | Rejected because |
|---|---|---|
| (b) Static offset tuned for a typical window | Pick a new fixed GUIDE_OFFSET that looks centered at a representative size. |
Fragile: breaks on different window sizes / DPI; still not "centered" in general. |
| (c) Auto-fit camera at startup | Set _zoom/_pan_offset once so the whole figure (head→feet) fits centered. |
Needs the panel's final size, which isn't ready in _ready() (requires deferred/NOTIFICATION_RESIZED handling); interacts badly with Reset Views (resets to zoom 1/pan 0, un-centering again) and shrinks the default starter parts. Adds timing complexity for a cosmetic fix. |
Open question (see §7 Q1): centering alone makes head and feet clip equally above/below the fold at 1:1 zoom (the figure is ~940 px tall vs a ~600 px panel). If full visibility is required, option (c) must be layered on top.
2. Bug 2 — Dynamic menu label
2a. Problem
_guide_menu_label() (stickman_editor.gd:271–272) always returns "Show Pose Guide", and _update_guide_menu_item() (:266–268) only refreshes the checkmark, never the text. So the label is static even though the toggle works.
2b. Recommended approach
Make the label reflect the action the click will perform, per the user's exact wording, and refresh the text whenever the menu is about to open or the toggle fires:
- Visible (
_show_guide == true) → "Hide Pose Guide" - Hidden (
_show_guide == false) → "Show Pose Guide"
User decision: drop the checkmark. The item is text-only; the action wording itself communicates the current state. Remove all set_item_checked usage for this item (setup + refresh).
2c. Code changes (scripts/stickman_editor.gd)
Replace _guide_menu_label() (lines 271–272):
func _guide_menu_label() -> String:
return "Show Pose Guide"
with:
func _guide_menu_label() -> String:
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
Replace _update_guide_menu_item() (lines 266–268):
func _update_guide_menu_item() -> void:
if _view_menu:
_view_menu.set_item_checked(1, _show_guide)
with:
func _update_guide_menu_item() -> void:
if _view_menu:
_view_menu.set_item_text(1, _guide_menu_label())
Also remove the now-obsolete view_menu.set_item_checked(1, _show_guide) call from _setup_menu_bar() (the item is no longer checkable).
2d. Startup-sync fix (related robustness gap)
_load_settings() (:540–571) already re-syncs the Edit menu text after load (_edit_menu.set_item_text(1, _snap_menu_label()) at :570–571), but it does not re-sync the View menu. If settings.json has show_pose_guide: false, the label is stale ("Hide Pose Guide") until the menu first opens. Add a sync call at the end of _load_settings(), right after _show_guide is assigned (line 558):
_show_guide = bool(d.get("show_pose_guide", true))
# ... existing grid/clamp/recent_colors logic ...
if _edit_menu:
_edit_menu.set_item_text(1, _snap_menu_label())
if _view_menu:
_update_guide_menu_item()
This mirrors the existing Edit-menu pattern and guarantees the label is correct immediately after load (the about_to_popup handler remains as a defensive re-sync).
Note:
_setup_menu_bar()runs before_load_settings()in_ready()(:103vs:109), so the initial label uses the_show_guidedefault (true→ "Hide Pose Guide"), which is correct for the default state; the load-sync covers the non-default case.
3. Bug 3 — Draw the guide in front of parts
3a. Problem
In _on_preview_draw() (whole_stickman_preview.gd:318–408), _draw_silhouette_guide() is called at line 322, before the for part_name in _part_order loop (:325–384). User parts (opaque fills) therefore cover the guide, hiding the joints/limbs exactly where the user needs to check alignment.
3b. Recommended draw order
Move _draw_silhouette_guide() to draw after the part loop but before the selection gizmos (and before/after the drag highlight as specified below):
1. draw_set_transform(_pan_offset, 0, Vector2(_zoom, _zoom)) (line 319)
2. _draw_grid() (line 321)
3. _selected_gizmo_bounds = Rect2() (line 323)
4. part loop + labels (lines 325–384)
5. _draw_silhouette_guide() ← MOVED here (ghost over parts)
6. dragged-part highlight (lines 386–390)
7. selection gizmos (lines 392–408)
Z-order rationale:
- Guide over parts — the semi-transparent guide (line alpha 0.45, joint alpha 0.65) drawn after parts "ghosts" over the opaque fills, so limbs/joints remain visible through/over a part. This is the whole point of Bug 3.
- Selection gizmos above the guide — the bounding box, rotation circle, and scale crosses are the user's manipulation affordances; they must never be obscured by the (now-front) guide. They stay last.
- Drag highlight above the guide — the yellow drag rect (drawn during
Interaction.TRANSLATE) is also an affordance; keep it above the guide for the same reason. (Draw guide at step 5, before the highlight at step 6, so highlight + gizmos remain on top.) - Labels — part labels (
H,T,LUA, …) are drawn inside the part loop (step 4), so the guide may overlap them slightly. This is acceptable: labels are small, and the guide is semi-transparent. (Splitting labels into a later pass is not worth the complexity.)
3c. Code changes (scripts/whole_stickman_preview.gd)
Delete line 322 (_draw_silhouette_guide() from its current position) and insert it after the part loop, immediately before the "Highlight dragged part" block (i.e., between line 384 and line 386):
# Phase 7: ghost the silhouette guide IN FRONT of parts (below selection affordances)
_draw_silhouette_guide()
# Highlight dragged part
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
...
3d. Alpha values — no change recommended
The existing alphas (lines 0.45, joints 0.65) already produce the intended "ghost" effect; now that they render over parts instead of under them, the semi-transparency is exactly what keeps them reading as a ghost rather than a solid overlay. Keep the constants unchanged for consistency. (If, during manual review, the guide feels too heavy over large filled parts, a single-line tweak to GUIDE_COLOR_* / GUIDE_JOINT_COLOR_* is the fallback — noted here, not applied.)
3e. Hit-testing — unaffected
The guide is pure draw_* calls and participates in no input logic. Hit-testing runs entirely on _part_bounds (_try_start_interaction at :659, _handle_right_click at :944), which is unchanged. Drawing the guide over parts cannot intercept clicks or the context menu. preview_area.clip_contents = true still clips the guide to the panel bounds (as before).
4. Files Modified
| File | Changes |
|---|---|
scripts/whole_stickman_preview.gd |
Replace GUIDE_OFFSET const with GUIDE_FIGURE_CENTER; re-center in _guide_to_preview(); move _draw_silhouette_guide() call after the part loop (before the drag highlight). |
scripts/stickman_editor.gd |
Make _guide_menu_label() dynamic; refresh text in _update_guide_menu_item() and drop the checkmark (set_item_checked calls removed); add _update_guide_menu_item() call in _load_settings(). |
docs/phase7_round1_spec.md |
This file. |
README.md |
Update §5 "Pose Silhouette Guide" (centering behavior, "ghosts in front of parts", dynamic menu label) and the menu-bar structure diagram (§ "Menu bar structure"). |
AGENTS.md |
Update the Phase 7 notes: GUIDE_OFFSET (170, 580) → dynamic centering via GUIDE_FIGURE_CENTER; "renders above the grid but below user parts" → "above the grid and above user parts, below selection gizmos"; "Show Pose Guide" → dynamic "Hide/Show Pose Guide" label. |
No .stk / settings.json / .tscn changes.
5. Edge Cases & Constraints
- Guide vs. pan/zoom — the guide remains a world-space fixture. It is centered only at the default view (zoom 1, pan 0) and after
Reset Views; panning/zooming moves it with the grid/parts exactly as before. - Window resize — because
_guide_to_preview()re-readspreview_area.sizeeach draw, the guide re-centers on resize. This means the guide's world position shifts relative to already-placed parts when the window is resized; accepted as a cosmetic consequence for a view aid (and is the desired "handles resize for free" behavior). Parts/grid stay fixed; only the guide re-centers. preview_area.sizeis zero before first layout —_on_preview_drawis connected topreview_area.draw, which only fires after the control is laid out, sosizeis valid at draw time. No guard required, but note the figure-center maps to(0,0)-relative origin in the (theoretical) zero-size case.- Negative master coords — centering produces negative world coords for the head/arms (e.g., head top at world y ≈ −170 on a 600 px panel); the grid already handles negative world space (it draws from
floor(world_origin / gs) * gs), so no change is needed. - Vertical clipping at 1:1 zoom — the figure (~940 px) is taller than a typical panel (~600 px), so head and feet clip equally above/below the fold when centered. This is the 1:1-scale decision from Phase 7, unchanged here; if full visibility is wanted, auto-fit (option (c)) must be added (see Q1).
- Menu state after load — with the
_load_settings()sync added, a persistedshow_pose_guide: falseshows "Show Pose Guide" immediately at startup; theabout_to_popuphandler remains as a defensive re-sync. - Clear / Load / Reset Views — none of them touch
_show_guideor the label;Reset Viewsre-centers the guide (zoom 1/pan 0 + draw-time centering) but does not change its visibility.
6. Testing / Verification
There is no automated test suite; verification is manual in the editor plus the project parse check.
- Parse check — run from
C:\Godot4\stickman:(The plain..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit--check-onlyform hangs on renderer init in 4.7.1; use the--headless --check-only --quitvariant.) - Bug 1 — centered — launch the app; the guide's head/neck/torso/hips/knees should appear centered horizontally in the preview (head/feet may clip above/below at 1:1 zoom — expected). No panning needed to find it.
- Bug 1 — resize — drag-resize the window; the guide stays centered in the preview area.
- Bug 1 — Reset Views — after panning/zooming away, View → Reset Views returns zoom 1 / pan 0 with the guide centered again.
- Bug 2 — label — View menu shows "Hide Pose Guide" by default; clicking toggles the guide off and the item becomes "Show Pose Guide"; clicking again restores both. Re-open the menu to confirm the text persists correctly across opens (no checkmark either way).
- Bug 2 — persistence — set
show_pose_guide: falseinsettings.json, relaunch; the menu reads "Show Pose Guide" and the guide is hidden. - Bug 3 — in front — draw a filled part over a joint; the guide line/joint dot should be visible through/over the part (ghosted), while the selected part's bounding box / rotation circle / scale crosses still render on top of the guide.
- Bug 3 — drag highlight — during a translation drag, the yellow highlight stays visible above the guide.
- Non-regression — with the guide on, left-click-drag a part and right-click for the context menu; behavior unchanged (guide is not hit-testable).
7. Clarifying Questions (resolved)
- Bug 1 — center vs. fit. Resolved: centering only (no auto-zoom-to-fit). At 1:1 zoom the head/feet clip equally above/below the fold — accepted.
- Bug 1 — world anchoring. Resolved: the guide stays a world-space fixture (moves with pan/zoom; centered at default view and after Reset Views).
- Bug 3 — z-order vs. affordances. Resolved: grid → parts → guide → drag highlight → selection gizmos — gizmos and drag highlight stay on top of the ghosted guide.
- Bug 2 — checkmark. Resolved: drop the checkbox — text-only dynamic action label ("Hide Pose Guide" / "Show Pose Guide"); all
set_item_checkedusage for this item is removed.
8. Recommended Implementation Order
scripts/whole_stickman_preview.gd— Bug 1 centering (GUIDE_FIGURE_CENTER+_guide_to_preview).scripts/whole_stickman_preview.gd— Bug 3 reorder (_draw_silhouette_guide()move).scripts/stickman_editor.gd— Bug 2 dynamic label +_update_guide_menu_item()+_load_settings()sync.- Manual verification (§6) +
--headless --check-only --quit. - Doc updates (
README.md,AGENTS.md).