Files
stickman/docs/phase7_round1_spec.md
ryan 48d98ce0fe Add StkRigAdapter for runtime skeleton fitting and shape mounting
- 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.
2026-08-18 17:12:54 -04:00

272 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)"):
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.
2. **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.
3. **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_input` hit-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 Views` restores 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):
```gdscript
const GUIDE_OFFSET: Vector2 = Vector2(170.0, 580.0) # master_rig (0,0) [hips] -> preview world
```
with:
```gdscript
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 468469):
```gdscript
func _guide_to_preview(master_pos: Vector2) -> Vector2:
return master_pos * GUIDE_SCALE + GUIDE_OFFSET
```
with:
```gdscript
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_SCALE` stays `1.0` and `GUIDE_HEAD_RADIUS` stays `100.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:271272`) always returns `"Show Pose Guide"`, and `_update_guide_menu_item()` (`:266268`) 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 271272):
```gdscript
func _guide_menu_label() -> String:
return "Show Pose Guide"
```
with:
```gdscript
func _guide_menu_label() -> String:
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
```
Replace `_update_guide_menu_item()` (lines 266268):
```gdscript
func _update_guide_menu_item() -> void:
if _view_menu:
_view_menu.set_item_checked(1, _show_guide)
```
with:
```gdscript
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()` (`:540571`) already re-syncs the Edit menu text after load (`_edit_menu.set_item_text(1, _snap_menu_label())` at `:570571`), 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):
```gdscript
_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()` (`:103` vs `:109`), so the initial label uses the `_show_guide` default (`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:318408`), `_draw_silhouette_guide()` is called at line 322, **before** the `for part_name in _part_order` loop (`:325384`). 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 325384)
5. _draw_silhouette_guide() ← MOVED here (ghost over parts)
6. dragged-part highlight (lines 386390)
7. selection gizmos (lines 392408)
```
**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):
```gdscript
# 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-reads `preview_area.size` each 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.size` is zero before first layout** — `_on_preview_draw` is connected to `preview_area.draw`, which only fires after the control is laid out, so `size` is 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 persisted `show_pose_guide: false` shows "Show Pose Guide" immediately at startup; the `about_to_popup` handler remains as a defensive re-sync.
- **Clear / Load / Reset Views** — none of them touch `_show_guide` or the label; `Reset Views` re-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.
1. **Parse check** — run from `C:\Godot4\stickman`:
```
..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit
```
(The plain `--check-only` form hangs on renderer init in 4.7.1; use the `--headless --check-only --quit` variant.)
2. **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.
3. **Bug 1 — resize** — drag-resize the window; the guide stays centered in the preview area.
4. **Bug 1 — Reset Views** — after panning/zooming away, View → Reset Views returns zoom 1 / pan 0 with the guide centered again.
5. **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).
6. **Bug 2 — persistence** — set `show_pose_guide: false` in `settings.json`, relaunch; the menu reads "Show Pose Guide" and the guide is hidden.
7. **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.
8. **Bug 3 — drag highlight** — during a translation drag, the yellow highlight stays visible above the guide.
9. **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)
1. **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.
2. **Bug 1 — world anchoring.** *Resolved:* the guide stays a **world-space fixture** (moves with pan/zoom; centered at default view and after Reset Views).
3. **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.
4. **Bug 2 — checkmark.** *Resolved:* **drop the checkbox** — text-only dynamic action label ("Hide Pose Guide" / "Show Pose Guide"); all `set_item_checked` usage for this item is removed.
---
## 8. Recommended Implementation Order
1. `scripts/whole_stickman_preview.gd` — Bug 1 centering (`GUIDE_FIGURE_CENTER` + `_guide_to_preview`).
2. `scripts/whole_stickman_preview.gd` — Bug 3 reorder (`_draw_silhouette_guide()` move).
3. `scripts/stickman_editor.gd` — Bug 2 dynamic label + `_update_guide_menu_item()` + `_load_settings()` sync.
4. Manual verification (§6) + `--headless --check-only --quit`.
5. Doc updates (`README.md`, `AGENTS.md`).