- Implemented test_phase9a_prop_attach.gd to validate anchor schema, alignment math, and preview isolation. - Implemented test_phase9b_prop_centering.gd to check canonical origin presentation, bounds, centering, and save threshold. - Implemented test_phase9c_prop_editor_ux.gd to assess the user experience in the prop editor, including anchor renaming and UI responsiveness. - Added corresponding .uid files for each test. - Created sync_sources.py script to manage markdown file synchronization with the notebook. - Added .gitignore for virtual environment files.
639 lines
31 KiB
Markdown
639 lines
31 KiB
Markdown
# Phase 9c — Prop Editor UX and Walking Hold Preview
|
||
|
||
Status: **approved specification — ready for implementation** (2026-09-21).
|
||
|
||
Phase 9c is a Prop Editor-only refinement discovered during Phase 9a/9b user verification. It
|
||
does not amend their saved-file contracts and does not add Sandbox Stage or gameplay behavior.
|
||
|
||
## 1. Approved decisions
|
||
|
||
The user approved these interaction choices:
|
||
|
||
1. Replace the preview's separate Facing/Frame controls with one **Preview Motion** selector:
|
||
`Front Pose`, `Walk Left`, and `Walk Right`, plus a dynamic `Pause Walk` / `Resume Walk`
|
||
button.
|
||
2. Rename anchors through a modal, focused text-entry dialog. The current automatic
|
||
`old_name_renamed` behavior is removed.
|
||
3. Rename `Show Inactive Gizmos` to **Show Other Markers**. When off, it hides the passive
|
||
origin marker and all unselected anchor markers while the selected editable anchor remains
|
||
visible.
|
||
|
||
This work is named Phase 9c because Phase 9a and 9b are already implemented and verified. It
|
||
changes preview motion ownership and established UI contracts rather than retroactively changing
|
||
those completed specifications.
|
||
|
||
## 2. Complete requested refinement set
|
||
|
||
| # | Requirement | Phase 9c contract |
|
||
| --- | --- | --- |
|
||
| 1 | Rename accepts a name | Open a modal LineEdit dialog, normalize the entered value, preserve the anchor payload, and select the renamed anchor. |
|
||
| 2 | Remove `(0,0)` copy | Keep only the small passive ring/cross at the canonical origin. |
|
||
| 3 | Inspector background `#262c39` | Add `ui.colors.inspector_background` to `prop_theme.json` and a validated accessor. |
|
||
| 4 | No add-shape hint in Reshape | The empty-canvas right-click hint is Arrange-only. |
|
||
| 5 | Clear hand names | Preview items read `Left Hand` and `Right Hand`. |
|
||
| 6 | Remove ineffective frame choice | Remove Upper Body/Full Figure and always frame the complete rig plus visible prop. |
|
||
| 7 | Replace synthetic arm animation | Preview Front Pose, Walk Left, and Walk Right; the old control becomes Pause/Resume Walk. |
|
||
| 8 | Preserve manual arm IK | The cyan target remains draggable; starting a drag pauses gait playback first. |
|
||
| 9 | Hide Material in Attach | Group the full Material UI and hide it only in Attach. |
|
||
| 10 | Remove duplicate Snap Anchors | Top-row Snap is the sole snap control and source of truth. |
|
||
| 11 | Preview checked on Attach entry | Every transition into Attach checks and shows the preview; the user may hide it for that Attach session. |
|
||
| 12 | Make marker visibility meaningful | Show Other Markers controls passive origin plus unselected anchor gizmos. |
|
||
| 13 | Fix clipped Grid/Snap | Use container-driven top rows, themed minimum heights, and vertical margins. |
|
||
| 14 | Remove preview blue border | Retain the dark preview surface but use no colored border. |
|
||
|
||
## 3. Scope and preserved contracts
|
||
|
||
### 3.1 In scope
|
||
|
||
- `PropEditor`, `PropCanvas`, `PropAttachPreview`, Prop Editor scene layout, and PropTheme UI
|
||
presentation.
|
||
- Preview-only use of the existing `stickman_rig.tscn` animation and IK nodes.
|
||
- Regression tests for all new UI state and preview ownership behavior.
|
||
|
||
### 3.2 Explicitly out of scope
|
||
|
||
- Any Sandbox Stage, `PropBlock`, spawner, runtime equip/release, physics, collision, terrain,
|
||
director, or action-queue change.
|
||
- Navigation or actual root translation in the holding preview. Walking is an in-place visual
|
||
gait check.
|
||
- A new animation asset, animation blending layer, or simultaneous animation/manual ownership
|
||
of the same hand IK target.
|
||
- `.prop` schema/version changes. Anchor IDs remain the persisted names; no separate display-name
|
||
field is added.
|
||
- Persistent UI preferences for preview motion, playback, marker visibility, or preview
|
||
visibility.
|
||
- Document-wide undo/redo, already tracked by debt item #37.
|
||
|
||
### 3.3 Saved-document behavior
|
||
|
||
Only a successful anchor rename mutates document data. It marks dirty once, arms the existing
|
||
autosave debounce once, refreshes the inspector/canvas/preview, and is included automatically in
|
||
ordinary save, Save As, autosave, recovery, and load round trips.
|
||
|
||
Preview motion, paused state, chosen preview hand, Show Stickman Preview, and Show Other Markers
|
||
are session-only tool state. Changing them must not mark dirty, arm autosave, or alter serialized
|
||
shape/anchor data. Hiding the Material section never changes the current material preset.
|
||
|
||
## 4. Existing rig and animation constraints
|
||
|
||
`stickman_rig.tscn` contains looped `walk_left` and `walk_right` clips. The actual
|
||
`StickmanLocomotion.walk_to()` path selects a LEFT/RIGHT facing profile and plays `walk_right`
|
||
for both directions. LEFT is represented through the rig's facing/mirroring policy, not by
|
||
playing `walk_left` during runtime locomotion.
|
||
|
||
Phase 9c must follow that proven runtime gait rule:
|
||
|
||
- `Walk Right`: RIGHT facing, positive preview wrapper, play `walk_right`.
|
||
- `Walk Left`: LEFT facing, keep the solving skeleton positive, mirror `PreviewRigRoot`, play
|
||
`walk_right`.
|
||
- `Front Pose`: FORWARD facing, player stopped, authored standing targets restored.
|
||
|
||
The existing `walk_left` asset is not deleted or modified. The preview must not call
|
||
`StickmanRig.walk_to()`: doing so would introduce NavigationServer/map dependencies and unwanted
|
||
root translation. It directly owns only its private AnimationPlayer.
|
||
|
||
The existing isolation contract remains mandatory:
|
||
|
||
- rig physics processing disabled;
|
||
- action queue stopped and cleared;
|
||
- no terrain following or navigation request;
|
||
- all collision layers/masks disabled;
|
||
- preview prop contains visual CanvasItems only;
|
||
- no reference to Sandbox Stage state.
|
||
|
||
## 5. Target files and typed contracts
|
||
|
||
| File | Required change |
|
||
| --- | --- |
|
||
| `scripts/prop_editor.gd` | Rename dialog; mode visibility; one snap control; Attach-entry preview default; marker toggle routing; theme/layout application. |
|
||
| `scripts/prop_canvas.gd` | Arrange-only empty hint; origin visibility API; remove origin text. |
|
||
| `scripts/prop_attach_preview.gd` | Replace synthetic motion and Frame/Facing UI with the approved motion state machine; preserve manual IK and wrist attachment. |
|
||
| `scripts/prop_theme.gd` | Add validated inspector background accessor/fallback. |
|
||
| `prop_theme.json` | Add `ui.colors.inspector_background: "#262C39"`. |
|
||
| `scenes/prop_editor.tscn` | Container-driven editor chrome and responsive top-row margins/minimum sizing. |
|
||
| `scenes/prop_attach_preview.tscn` | No structural requirement beyond retaining the scripted PanelContainer shell. |
|
||
| `tests/test_phase9c_prop_editor_ux.gd` | New Phase 9c unit/integration suite. |
|
||
| Existing Prop Editor tests | Replace assertions for removed controls and add token/layout regressions where they naturally belong. |
|
||
|
||
No change is required in `prop_file.gd`, `prop_anchor_data.gd`, `stickman_rig.gd`,
|
||
`stickman_locomotion.gd`, or `stickman_rig.tscn` unless implementation uncovers a verified defect
|
||
independent of this feature. Such a discovery must be reported rather than folded into Phase 9c.
|
||
|
||
## 6. Node and layout design
|
||
|
||
Keep dialogs as direct children of the editor root. Put visible editor chrome under one
|
||
full-rect `VBoxContainer` so font and hit-target tokens determine row height instead of fixed
|
||
32 px offsets:
|
||
|
||
```text
|
||
PropEditor (Control)
|
||
├── EditorChrome (VBoxContainer, full rect)
|
||
│ ├── TopRow (HBoxContainer, min height >= button_min_height + margins)
|
||
│ │ ├── MenuBar (expand)
|
||
│ │ └── TopRightMargin (MarginContainer)
|
||
│ │ └── TopRightControls (HBoxContainer)
|
||
│ │ ├── Grid label
|
||
│ │ ├── Grid SpinBox
|
||
│ │ ├── Snap CheckBox
|
||
│ │ └── Center Prop Button
|
||
│ ├── TopBar (HBoxContainer, themed minimum height)
|
||
│ │ ├── Prop Name label
|
||
│ │ └── PropNameEdit
|
||
│ ├── MainLayout (HSplitContainer, expand/fill)
|
||
│ │ ├── CanvasPanel
|
||
│ │ │ └── CanvasArea
|
||
│ │ │ ├── ModeBar
|
||
│ │ │ ├── AttachTools (FlowContainer)
|
||
│ │ │ │ ├── Show Stickman Preview
|
||
│ │ │ │ └── Show Other Markers
|
||
│ │ │ └── CanvasWorkspace (HSplitContainer)
|
||
│ │ │ ├── PropCanvas
|
||
│ │ │ └── PropAttachPreview
|
||
│ │ └── InspectorColumn
|
||
│ │ └── InspectorScroll
|
||
│ │ └── InspectorList
|
||
│ │ ├── MaterialSection
|
||
│ │ ├── ShapeSection
|
||
│ │ ├── AddSection
|
||
│ │ └── AnchorSection
|
||
│ └── StatusBar
|
||
├── SaveDialog / LoadDialog / ErrorDialog
|
||
├── ExitDialog / RecoveryDialog / OffOriginSaveDialog
|
||
└── RenameAnchorDialog
|
||
```
|
||
|
||
Existing unique names remain unique so controller lookup continues through `%NodeName`.
|
||
Path-based tests must migrate to unique lookup or the revised hierarchy instead of forcing the
|
||
old fixed layout to remain.
|
||
|
||
`TopRightMargin` uses small vertical margins (nominally 4 px top and bottom). Grid SpinBox,
|
||
Snap CheckBox, Center Prop, and both top rows use at least `PropTheme.button_min_height()`. The
|
||
layout must remain unclipped at the committed 18–22 px font tokens and when a custom theme raises
|
||
those font sizes. Attach tools use a `FlowContainer` so the two longer kid-facing labels wrap
|
||
instead of extending outside a narrow canvas pane.
|
||
|
||
## 7. Theme and surface styling
|
||
|
||
Extend both `PropTheme.DEFAULT_CONFIG` and the committed JSON:
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"colors": {
|
||
"inspector_background": "#262C39"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Add:
|
||
|
||
```gdscript
|
||
func inspector_background_color() -> Color:
|
||
return color("ui.colors.inspector_background", Color("#262C39"))
|
||
```
|
||
|
||
The InspectorColumn panel style derives its background only from this accessor. Keep the subtle
|
||
theme border and split grabber so the adjustable inspector boundary remains legible.
|
||
|
||
`PropAttachPreview` retains its dark surface but sets all border widths to zero. It must not use
|
||
the accent color as a frame border. Pane separation is supplied by the split grabber and the
|
||
contrast between preview and inspector backgrounds.
|
||
|
||
## 8. Anchor rename workflow
|
||
|
||
### 8.1 Dialog contract
|
||
|
||
Add typed state:
|
||
|
||
```gdscript
|
||
var _rename_anchor_dialog: ConfirmationDialog
|
||
var _rename_anchor_edit: LineEdit
|
||
var _rename_anchor_source_id: String = ""
|
||
```
|
||
|
||
`Rename` is enabled only for a selected editable non-origin anchor. Pressing it:
|
||
|
||
1. stores the selected ID in `_rename_anchor_source_id`;
|
||
2. fills the LineEdit with that ID;
|
||
3. opens the dialog;
|
||
4. defers focus and selects all text so typing replaces the old name.
|
||
|
||
The dialog title is `Rename Anchor`, confirmation text is `Rename`, and cancel closes without
|
||
mutation. The confirm button is disabled when the trimmed entry is empty.
|
||
|
||
### 8.2 Commit algorithm
|
||
|
||
On confirm:
|
||
|
||
```gdscript
|
||
var remaining := _anchors.duplicate(true)
|
||
var value := remaining[_rename_anchor_source_id].duplicate(true)
|
||
remaining.erase(_rename_anchor_source_id)
|
||
var new_id := PropAnchorData.normalize_id(_rename_anchor_edit.text, remaining)
|
||
```
|
||
|
||
- Human input is normalized with the existing lowercase-snake-case rules.
|
||
- A collision receives the existing deterministic `_2`, `_3`, ... suffix.
|
||
- Renaming to the same normalized ID is a no-op: do not dirty or autosave.
|
||
- Otherwise insert the unchanged payload at `new_id`, normalize anchors once, replace selection,
|
||
update canvas selection, mark dirty once, refresh the inspector, and sync the preview once.
|
||
- Position, role, allowed hands, rotation, and Z offset must remain byte/value equivalent.
|
||
- Clear `_rename_anchor_source_id` on confirmed commit, cancel, hide, New, and load so a stale
|
||
dialog cannot rename another document's anchor.
|
||
|
||
No `display_name` field is introduced. The normalized ID displayed in the list is the saved ID.
|
||
|
||
## 9. Marker and canvas behavior
|
||
|
||
### 9.1 Passive origin marker
|
||
|
||
Add to `PropCanvas`:
|
||
|
||
```gdscript
|
||
var _show_origin_marker: bool = true
|
||
|
||
func set_show_origin_marker(enabled: bool) -> void:
|
||
_show_origin_marker = enabled
|
||
queue_redraw()
|
||
```
|
||
|
||
`_draw_origin_marker()` retains its constant-screen-size dark halo, ring, and cross, but removes
|
||
the font lookup and `(0,0)` `draw_string()` call. It remains non-interactive and is drawn at
|
||
world `Vector2.ZERO` in Arrange, Reshape, and Attach whenever `_show_origin_marker` is true.
|
||
|
||
### 9.2 Show Other Markers
|
||
|
||
The Attach toolbar contains one default-on `Show Other Markers` CheckBox. Its callback routes
|
||
the same value to:
|
||
|
||
```gdscript
|
||
_canvas.set_show_inactive_anchors(enabled)
|
||
_canvas.set_show_origin_marker(enabled)
|
||
```
|
||
|
||
The anchor overlay continues to draw and hit-test the selected editable anchor when false while
|
||
hiding every unselected editable anchor. The origin marker follows the setting in all modes,
|
||
including after leaving Attach. The setting is session-only and is not reset by switching modes,
|
||
New, load, or save.
|
||
|
||
### 9.3 Empty-canvas hint
|
||
|
||
Draw `Right-click to add a shape` only when all are true:
|
||
|
||
- `shapes.is_empty()`;
|
||
- mode is `EditMode.TRANSFORM` (Arrange);
|
||
- the canvas font is available.
|
||
|
||
Reshape and Attach show no add-shape hint. The origin marker is evaluated independently afterward.
|
||
|
||
## 10. Inspector and mode synchronization
|
||
|
||
Wrap the existing Material title, preset OptionButton, mass, friction, and bounce labels in
|
||
`_material_section: VBoxContainer`.
|
||
|
||
Mode visibility is authoritative in `_sync_mode_controls(mode)`:
|
||
|
||
| Section/control | Arrange | Reshape | Attach |
|
||
| --- | --- | --- | --- |
|
||
| MaterialSection | visible | visible | hidden |
|
||
| ShapeSection | selected shape only | selected shape only | hidden |
|
||
| AddSection / Erase Shape | visible | hidden | hidden |
|
||
| AnchorSection / AttachTools | hidden | hidden | visible |
|
||
| Holding Preview | hidden | hidden | visible when checked |
|
||
|
||
Delete `_snap_anchors_check`, `_on_snap_anchors_toggled()`, and the reverse synchronization from
|
||
`_on_top_snap_toggled()`. The existing top Snap CheckBox remains the only writer of
|
||
`PropCanvas.set_snap_enabled()`. Anchor drag already consumes the canvas's shared snap state and
|
||
grid size; this behavior must not be duplicated in the controller.
|
||
|
||
Track the last canvas mode. On a real transition from Arrange/Reshape into Attach:
|
||
|
||
```gdscript
|
||
_show_preview_check.set_pressed_no_signal(true)
|
||
```
|
||
|
||
then show/sync the preview. Toggling it off while already in Attach stays off until the user
|
||
leaves and re-enters Attach. Repeated inspector refreshes or document updates while remaining in
|
||
Attach must not force it back on.
|
||
|
||
## 11. Holding-preview UI
|
||
|
||
The dynamic preview hierarchy becomes:
|
||
|
||
```text
|
||
PropAttachPreview
|
||
└── VBoxContainer
|
||
├── Header (FlowContainer)
|
||
│ ├── Holding Preview title
|
||
│ ├── HandOption: Left Hand / Right Hand
|
||
│ └── MotionOption: Front Pose / Walk Left / Walk Right
|
||
├── HintLabel
|
||
├── SubViewportContainer
|
||
└── Footer (FlowContainer)
|
||
├── WalkPlaybackButton: Pause Walk / Resume Walk
|
||
├── Reset Arm
|
||
└── Reset View
|
||
```
|
||
|
||
Remove `_facing_option`, `_frame_option`, `FrameMode`, and `_animate_check`. The initial hand
|
||
remains Right Hand. The initial motion is Front Pose. The control hit rectangles continue to use
|
||
the full themed minimum height and `MOUSE_FILTER_STOP`.
|
||
|
||
The motion choice is a preview tool preference: `set_document()` does not reset it, so authors
|
||
can compare multiple props in the same walk. Opening a fresh editor instance begins at Front
|
||
Pose. New/load do not serialize or dirty this state.
|
||
|
||
## 12. Preview motion and IK ownership state machine
|
||
|
||
### 12.1 Typed state/API
|
||
|
||
```gdscript
|
||
enum PreviewMotion { FRONT_POSE, WALK_LEFT, WALK_RIGHT }
|
||
|
||
var _motion: PreviewMotion = PreviewMotion.FRONT_POSE
|
||
var _walk_paused: bool = true
|
||
var _manual_arm_override: bool = false
|
||
var _hand_target_rests_local: Dictionary = {}
|
||
|
||
func set_preview_motion(motion: int) -> void
|
||
func set_walk_paused(paused: bool) -> void
|
||
func toggle_walk_playback() -> void
|
||
func reset_arm_pose() -> void
|
||
```
|
||
|
||
Standing hand target rest values are captured as each Marker2D's **local `position`**, never as
|
||
global positions. Animation tracks also write these local positions, so reset remains correct
|
||
under the LEFT preview-wrapper mirror.
|
||
|
||
### 12.2 State transitions
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> FrontPose
|
||
FrontPose --> WalkPlaying: choose Walk Left / Walk Right
|
||
WalkPlaying --> WalkPaused: Pause Walk
|
||
WalkPlaying --> WalkPaused: begin cyan-target drag
|
||
WalkPaused --> WalkPlaying: Resume Walk
|
||
WalkPaused --> FrontPose: choose Front Pose
|
||
WalkPlaying --> FrontPose: choose Front Pose
|
||
WalkPlaying --> WalkPlaying: switch walk direction
|
||
WalkPaused --> WalkPlaying: switch walk direction (restart selected gait)
|
||
```
|
||
|
||
Exact transition behavior:
|
||
|
||
- **Front Pose entry:** stop AnimationPlayer with reset, set FORWARD profile/wrapper, restore all
|
||
standing target local positions, enable arm IK, set `_walk_paused = true`, clear manual
|
||
override, update marker/prop, and reframe.
|
||
- **Walk Left/Right entry:** clear manual override, apply the requested profile/wrapper, enable
|
||
arm IK, start `walk_right` from time `0.0`, set `_walk_paused = false`, update button copy,
|
||
and reframe. Changing between walk directions restarts at `0.0` for deterministic pose and
|
||
mirror state.
|
||
- **Pause:** call AnimationPlayer pause without resetting targets or playhead. The currently
|
||
solved pose freezes; button becomes `Resume Walk`.
|
||
- **Resume:** resume the current walk clip from its saved playhead, clear manual override, and
|
||
change the button to `Pause Walk`.
|
||
- **Manual drag begin:** if walking, pause first; then set manual override true and allow the
|
||
selected hand target to be clamped/dragged. Mouse release ends pointer capture but intentionally
|
||
leaves walking paused and preserves the authored inspection pose.
|
||
- **Reset Arm in Front:** restore both hand targets from local standing rests, clear manual
|
||
override, remain stopped.
|
||
- **Reset Arm during a paused walk:** re-seek/reapply the current animation playhead so the
|
||
dragged target returns to the frozen gait pose, pause again, clear manual override, and keep
|
||
the same direction/playhead.
|
||
- **Reset Arm during a playing walk:** first pause, then apply the paused-walk rule.
|
||
- **Hand selection/forced compatibility change:** keep the current motion direction and playback
|
||
state, clear any manual override by reapplying the current front/gait pose, switch the cyan
|
||
marker and prop binding, and do not restart a playing clip unless reapplication requires it.
|
||
|
||
`Pause Walk` is disabled in Front Pose and carries a tooltip such as `Choose Walk Left or Walk
|
||
Right to preview walking.` `Reset Arm` remains enabled when a compatible grip is visible.
|
||
|
||
### 12.3 Frame ownership and prop following
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Motion[Preview Motion selector] --> Player[Private AnimationPlayer]
|
||
Player --> Targets[IK target Marker2D positions]
|
||
Drag[Cyan target drag] -->|pauses player, then owns selected target| Targets
|
||
Targets --> IK[TwoBoneIK solve]
|
||
IK --> Wrist[Solved lower-arm visual endpoint]
|
||
Wrist --> Solve[PropAnchorData.solve_attachment_transform]
|
||
Solve --> Prop[Full multi-shape preview prop]
|
||
```
|
||
|
||
There is exactly one writer for the selected hand target at a time. Phase 9c deliberately does
|
||
not blend manual offsets additively over a moving animation. This avoids frame-order fights
|
||
between AnimationPlayer and pointer input.
|
||
|
||
Every preview frame still updates the cyan marker and derives the held prop transform from
|
||
`_solved_wrist_transform()`, never directly from the IK target. It therefore follows arm swing,
|
||
paused gait, and manual IK poses. Direction changes retain the positive-scale solver/wrapper
|
||
mirror strategy and never mutate stored anchor coordinates.
|
||
|
||
## 13. Camera behavior
|
||
|
||
Delete the upper-body filtering branch. `_reframe_camera()` always unions:
|
||
|
||
- every visible rig polygon/line, including legs;
|
||
- every visible authored prop polygon/line/circle visualization.
|
||
|
||
Grow the union by the existing safe margin, center the Camera2D, and fit it to the viewport.
|
||
`Reset View` calls this one function. Wheel zoom and middle-mouse pan remain unchanged. Resizing
|
||
the preview pane may refit as it does today. The removed frame choice carries no persisted state.
|
||
|
||
## 14. Signals and update flow
|
||
|
||
No public document signal payload changes:
|
||
|
||
```gdscript
|
||
signal shape_changed(shapes: Array[Dictionary])
|
||
signal anchors_changed(anchors: Dictionary)
|
||
signal anchor_selected(anchor_id: String)
|
||
```
|
||
|
||
Rename flow:
|
||
|
||
```text
|
||
Rename button
|
||
-> RenameAnchorDialog + focused LineEdit
|
||
-> normalize entered ID against anchors excluding old ID
|
||
-> replace dictionary key, retain payload
|
||
-> canvas.set_anchor_data + canvas.select_anchor
|
||
-> mark dirty once
|
||
-> refresh anchor inspector
|
||
-> preview.set_document
|
||
```
|
||
|
||
Preview controls communicate internally through direct typed methods because the preview owns
|
||
the complete private visualization. No global signal bus or runtime component is introduced.
|
||
|
||
## 15. Edge cases
|
||
|
||
- No selected editable anchor: Rename remains disabled and cannot open a stale source ID.
|
||
- Rename entry empty/whitespace: confirmation disabled.
|
||
- Rename normalizes to current ID: no-op and no dirty event.
|
||
- Rename collides after normalization: deterministic numeric suffix; no existing anchor replaced.
|
||
- Rename while autosave countdown exists: one ordinary document mutation resets the debounce.
|
||
- Origin: never appears in the editable list and cannot be renamed.
|
||
- No shapes in Reshape: blank grid plus optional origin marker; no add-shape hint.
|
||
- Show Other Markers off with no selection: all editable markers and origin are hidden.
|
||
- Show Other Markers off with selection: selected marker and its rotation dot remain visible and
|
||
hit-testable; all others and origin are hidden.
|
||
- Preview hidden, then Attach exited/re-entered: re-entry checks and shows it again.
|
||
- Attach document has no compatible grip: preview shows its existing selection guidance; gait
|
||
controls may animate the rig, but prop and cyan hold target remain unavailable as appropriate.
|
||
- Hand restriction forces another hand during a paused/manual pose: current preview pose is
|
||
reapplied before binding the new hand, preventing stale global target placement.
|
||
- Walk paused at loop boundary: resuming remains looped and does not restart unless direction
|
||
changes.
|
||
- Prop extends far beyond the figure: full framing includes it; no separate frame selector.
|
||
- Custom large UI fonts: top controls grow through containers instead of clipping.
|
||
- Invalid/missing new theme token: accessor returns exact `#262C39` fallback and warns only under
|
||
the existing PropTheme rules.
|
||
|
||
## 16. Automated validation
|
||
|
||
Add `tests/test_phase9c_prop_editor_ux.gd` as a headless SceneTree suite and update affected
|
||
existing suites. Assertions must verify state and values, not only node counts.
|
||
|
||
### 16.1 Rename
|
||
|
||
1. Rename opens a ConfirmationDialog with the current ID selected in a focused LineEdit.
|
||
2. Empty/whitespace input disables confirmation.
|
||
3. `Main Handle` becomes `main_handle` and preserves every anchor payload field.
|
||
4. A collision becomes `main_handle_2` without replacing the existing anchor.
|
||
5. Same-normalized-name commit emits no dirty mutation.
|
||
6. Successful rename updates list selection, canvas selection, preview selected ID, save output,
|
||
and load round trip.
|
||
7. Cancel/New/load clears pending rename source and does not mutate the document.
|
||
|
||
### 16.2 Canvas, modes, and markers
|
||
|
||
8. Origin drawing has no `(0,0)` string path and remains present by default in all three modes.
|
||
9. Show Other Markers false hides origin and unselected anchors in draw/hit-test while preserving
|
||
selected anchor interaction.
|
||
10. Arrange alone shows the empty-canvas right-click hint.
|
||
11. Material is visible in Arrange/Reshape and hidden in Attach without changing preset.
|
||
12. Attach toolbar contains no Snap Anchors control; top Snap still snaps anchor dragging by the
|
||
configured Grid value.
|
||
13. A real transition into Attach checks/shows preview; ordinary refresh while hidden does not
|
||
re-check it; leaving and re-entering does.
|
||
|
||
### 16.3 Theme/layout
|
||
|
||
14. Default and committed theme return exact inspector background `#262C39`.
|
||
15. Custom valid override works; invalid value falls back under current validation.
|
||
16. Inspector surface consumes the accessor.
|
||
17. Preview panel border widths are all zero.
|
||
18. Top rows and Grid/Snap controls meet themed minimum height after layout; their visible rects
|
||
are not clipped by parents.
|
||
19. Inspector and preview remain adjustable through the existing split panes.
|
||
|
||
### 16.4 Preview motion and IK
|
||
|
||
20. Hand items are exactly `Left Hand` and `Right Hand`.
|
||
21. Motion items are exactly `Front Pose`, `Walk Left`, and `Walk Right`; no FrameOption or
|
||
FacingOption remains.
|
||
22. Initial state is FORWARD Front Pose with stopped player and local standing targets.
|
||
23. Walk Right selects RIGHT and loop-plays the proven gait clip in place.
|
||
24. Walk Left selects LEFT, keeps the solving rig positive, mirrors only PreviewRigRoot, and
|
||
loop-plays the proven gait clip in place.
|
||
25. Pause preserves current animation position and target values; Resume advances from it.
|
||
26. Direction change restarts deterministically at `0.0`.
|
||
27. Starting cyan-target drag pauses gait before writing the selected target and leaves it paused
|
||
after release.
|
||
28. Reset Arm restores standing local target in Front and the frozen animation sample in Walk.
|
||
29. Switching/forcing hand keeps motion/play state but clears stale manual override.
|
||
30. Held prop continues to map the selected grip exactly to the solved wrist over several walk
|
||
frames and after a manual drag.
|
||
31. Preview root does not translate during gait; queues stay empty, physics stays disabled,
|
||
collision layers/masks stay zero, and preview prop children are non-physics CanvasItems.
|
||
32. Reset View frames the complete figure and intersects the visible held-prop bounds.
|
||
|
||
### 16.5 Regression commands
|
||
|
||
Run all Prop Editor suites from `AGENTS.md`, particularly:
|
||
|
||
- `tests/test_prop_editor_file.gd`
|
||
- `tests/test_prop_editor_theme.gd`
|
||
- `tests/test_prop_editor_canvas.gd`
|
||
- `tests/test_prop_editor_thumb.gd`
|
||
- `tests/test_prop_editor_round3.gd`
|
||
- `tests/test_phase9a_prop_attach.gd`
|
||
- `tests/test_phase9b_prop_centering.gd`
|
||
|
||
Update old assertions that explicitly expect Animate Arm, FrameOption, FacingOption, Snap Anchors,
|
||
the blue preview border, or `(0,0)` label. Do not weaken anchor round-trip, origin
|
||
canonicalization, wrist alignment, preview isolation, or centering tests.
|
||
|
||
Run Godot 4.7.1 `--headless --check-only` for every changed script and `git diff --check`.
|
||
|
||
## 17. Manual acceptance checklist
|
||
|
||
Launch `res://scenes/prop_editor.tscn` with F6 and verify:
|
||
|
||
1. Select an editable anchor, click Rename, type a new name, and confirm the entered normalized
|
||
name appears; it no longer appends `_renamed` automatically.
|
||
2. Rename to a name already in use and confirm a safe numeric suffix is applied.
|
||
3. Cancel Rename and confirm nothing changes; rename/save/reload and confirm persistence.
|
||
4. The passive center marker has no `(0,0)` text in Arrange, Reshape, or Attach.
|
||
5. `Show Other Markers` off hides that passive marker and unselected anchors but retains the
|
||
selected anchor and rotation dot; turn it on and confirm they return.
|
||
6. Inspector background is visibly `#262C39` and distinct from canvas/preview.
|
||
7. Empty Arrange shows `Right-click to add a shape`; empty Reshape does not.
|
||
8. Material controls are hidden in Attach and return unchanged in Arrange/Reshape.
|
||
9. Attach contains only Show Stickman Preview and Show Other Markers; no Snap Anchors duplicate.
|
||
10. Top Snap with a non-default Grid value still snaps an anchor drag.
|
||
11. Hide the preview, leave Attach, and re-enter; it is checked and shown again.
|
||
12. Grid and Snap have comfortable vertical padding and are not clipped at top or bottom.
|
||
13. Resize the outer inspector split and inner preview split in both directions.
|
||
14. Preview has a dark surface without a blue border.
|
||
15. Hand choices read Left Hand and Right Hand.
|
||
16. There is no Upper Body/Full Figure or separate Facing control.
|
||
17. Initial preview is a static Front Pose.
|
||
18. Choose Walk Right and confirm an in-place full-body walking gait while the complete prop
|
||
follows the holding wrist.
|
||
19. Choose Walk Left and confirm the stickman faces/walks left without inverted limbs and the
|
||
complete prop still follows the wrist.
|
||
20. Pause Walk freezes the exact pose and changes to Resume Walk; Resume continues rather than
|
||
jumping to the beginning.
|
||
21. While walking, drag the cyan hand target. Walking pauses first, the arm follows the pointer,
|
||
the prop follows the solved wrist, and the pose remains paused on release.
|
||
22. Reset Arm restores the appropriate front/frozen-gait arm pose without changing anchor data.
|
||
23. Switch Left Hand/Right Hand and test a `both` grip plus left-only/right-only restrictions.
|
||
24. Reset View includes the entire figure and visible prop; wheel zoom and middle-pan still work.
|
||
25. Change preview controls and verify the document does not become dirty.
|
||
26. Confirm save/load/autosave/recovery retain shapes, material, and renamed anchors exactly.
|
||
|
||
## 18. Architectural trade-offs
|
||
|
||
| Decision | Benefit | Cost / future work |
|
||
| --- | --- | --- |
|
||
| Separate Phase 9c | Preserves completed 9a/9b history and gives behavior changes focused regressions | User guide spans another verified phase before its next update |
|
||
| One Preview Motion selector | Removes ineffective/ambiguous combinations and is kid-readable | Facing is no longer independently previewable without gait |
|
||
| Runtime gait rule, preview in place | Matches current gameplay direction/mirror semantics without navigation dependencies | Not a terrain, speed, arrival, or root-motion simulation |
|
||
| Pause before manual IK | One deterministic owner writes the hand target | No live additive hand posing while gait continues |
|
||
| Local rest targets | Stable under wrapper mirroring and matches animation track coordinates | Existing global-rest code must be replaced carefully |
|
||
| Full figure only | One reliable frame and fewer controls | Authors cannot request a tighter automatic upper-body crop; wheel zoom remains available |
|
||
| Modal rename | Focused, touch-friendly, and hard to trigger accidentally | One additional dialog lifecycle must be cleared on document changes |
|
||
| One snap source | Eliminates contradictory UI state | Attach toolbar no longer exposes snap next to anchors; top control must remain visible |
|
||
| Show Other Markers controls origin too | Toggle has visible meaning even with one anchor | A setting changed in Attach also affects the passive marker in other modes |
|
||
| Container-driven chrome | Adapts to theme/font changes and prevents clipping | Scene hierarchy and path-based tests require a targeted update |
|
||
|
||
## 19. Implementation order and pipeline handoff
|
||
|
||
1. Add theme token/accessor and convert editor chrome to responsive containers.
|
||
2. Group mode sections; remove duplicate/obsolete controls; implement marker visibility and hint
|
||
rules.
|
||
3. Add the rename dialog and document transaction tests.
|
||
4. Replace preview synthetic motion with the exact PreviewMotion/playback/IK state machine.
|
||
5. Add Phase 9c tests, update affected expectations, run all regressions/static checks.
|
||
6. Hand the implementation to the user for Godot Editor verification.
|
||
|
||
Do not update the user guide or README during implementation handoff. After the user confirms
|
||
Phase 9c is verified and working, the writer must revise `docs/prop_editor_user_guide.md` to
|
||
remove the old automatic Rename, Snap Anchors, Facing, Frame, and Animate Arm instructions and
|
||
document the approved Phase 9c controls.
|