Add initial draft for Phase 7 UI Polish plan

This commit is contained in:
2026-09-17 22:17:42 -04:00
parent 2071ee03b7
commit baa19a9341
2 changed files with 298 additions and 153 deletions
+58 -47
View File
@@ -1,11 +1,13 @@
# AGENTS.md — stickman (Godot 4.4)
# AGENTS.md — stickman (Godot 4.7.1)
## Project type
- **Godot 4.4** 2D/GUI project (Forward Plus renderer)
- **Godot 4.7.1** 2D/GUI project (Forward Plus renderer)
- Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`.
- **Main scene:** `res://scenes/stickman_editor.tscn` (set as `run/main_scene` in `project.godot`)
## Command-line testing & debug playback
- **Godot binary (4.7.1) — resolve per platform.** Commands below use `<godot>` as shorthand:
- macOS: `/Applications/Godot.app/Contents/MacOS/Godot`
- Windows (PowerShell): `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe"`
@@ -21,17 +23,28 @@
- **Richer debug logs:** append `--verbose` to the scene-playback (or headless) command to surface engine warnings/errors alongside your own `print()` lines.
## Project overview
Stickman Studio is an editor tool for drawing and assembling stick figures. It is a
`Control`-based GUI (not physics/animation) in its current phase. Body-part vector shapes
are authored per-panel (each panel supports **multiple shapes** with Z-ordering) and
assembled in a "Whole Stickman" preview that supports translation, rotation, and scale.
Stickman Studio is a kid-friendly 2D scene-building and directing project—not only a
stick-figure editor. Its broader product direction is an approachable moviemaker where
children can create and customize stickmen, build sets with floors/terrain and props,
arrange characters and objects, direct behavior with queued actions and event-triggered
rules, and play the resulting scenes.
The codebase currently has two primary workflows. The main `Control`-based stickman editor
authors body-part vector shapes and assembles them in the "Whole Stickman" preview. The
Sandbox Stage provides the wider scene workflow through EDIT/DIRECT/PLAY modes, terrain,
props, live stickman rigs, action queues, and events. The editor remains the configured
`run/main_scene`; the Sandbox Stage and its runtime systems are separate scenes and tools
that form the foundation of the broader moviemaking experience.
## Required addon
- **Scalable Vector Shapes 2D** (v2.27.7) at `addons/curved_lines_2d/`
- Declared dependency for the project. The current editor UI does not instantiate it directly,
but keep it present — it is required for the legacy `stick.tscn` rig.
## Architecture
- `scripts/stickman_editor.gd` — `extends Control`; the main controller. Owns the menu bar,
save/load/clear flow, JSON (de)serialization, and populates the 10 body-part panels.
Writes `FILE_VERSION "1.5"`; auto-migrates `"1.0"`–`"1.4"` files on load. Coordinates cross-panel
@@ -44,7 +57,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
otherwise. `pivot`/`length`/`proportions` are write-only metadata — never read back on load,
recomputed on every save.
- **Phase 9 Round 5 guide-offset export:** each part's save dict also gains `"guide_offset":
{x, y}` = (part bbox center in preview space) − (guide joint in preview space), computed in
{x, y}` = (part bbox center in preview space) − (guide joint in preview space), computed in
`_collect_all_shape_data()` as `(pos + pivot) - _whole_preview.get_guide_joint_preview(...)` —
a **pure master-space delta** (both points are preview-world coordinates, so panel-size terms
cancel). The part→joint map is `const GUIDE_JOINT_FOR_PART` (head→"Neck" — the head bone's
@@ -124,7 +137,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
gizmos and drag highlight. The guide is **centered** at the default view and after
`Reset Views`, computed at draw time from the live preview size via
`_guide_to_preview()` = `(master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE +
preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
world-space fixture that moves with pan/zoom. Joint positions are **hardcoded
constants** derived from the `master_rig.tscn` rest pose (`GUIDE_JOINTS`: 13 anchors
Head/Neck/Shoulders/Elbows/Wrists/Hips/Knees/Ankles;
@@ -149,7 +162,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
whole dict as a single shape (`shapes = [pd]`), so v1.0/v1.1 single-shape `.stk` files (e.g.
`stickmen/test.stk`) mount as visible geometry instead of being cleared to nothing.
- **Phase 9 extension:** also fits the head bone (`Skeleton2D/Torso/Head.position.y =
-proportions.torso_length`, x preserved) and mounts the head as **full geometry** like every
-proportions.torso_length`, x preserved) and mounts the head as **full geometry** like every
other part — it clears the `Body/Head` node's inline `@tool` circle script via
`set_script(null)` and mounts `.stk` head shapes as `Line2D`/`Polygon2D`. Dead helpers
`_mount_head_circle`, `_compute_shapes_bbox`, and `_first_shape_color` were removed.
@@ -220,7 +233,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
(`j_prime = E(J_raw)` or `f_pt_prime = E(F_pt_raw)`) is **nearest the part's stored guide
joint** (`center − guide_offset`): if `d_far < d_joint` (strict) the far end attaches
(`anchor = f_pt_prime`, `v = −f_prime`), else the family end (`anchor = j_prime`, `v =
f_prime`). This replaces the per-side family choice **and** the 180° flip heuristic for the
f_prime`). This replaces the per-side family choice **and** the 180° flip heuristic for the
`guide_offset` case, fixing the **lower left leg** (knee now at the joint, was the ankle)
and **lower right arm** (elbow now at the joint, was the wrist), both 180° off their bones
because the user's drawn-side conventions are inconsistent per part. The nearest-end rule
@@ -291,9 +304,9 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
intentionally static.
- **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL`
physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL,
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
int)`, and public API `set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() ->
bool` / `request_recovery()` / `snap_to_standing()` (Phase 2 Sandbox: instantly destroys the
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
int)`, and public API `set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() ->
bool` / `request_recovery()` / `snap_to_standing()` (Phase 2 Sandbox: instantly destroys the
ragdoll or cancels the recovery tween, sets the 6 IK targets directly to `STAND_POSE`, re-shows
`Body/*`, re-enables IK, and returns to `ANIMATED` with no stand-up glide). `_physics_process()`
→ `_track_momentum(delta)` caches the rig
@@ -304,7 +317,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
it first would revert the bones to the authored rest pose, popping the figure), then hides
`Body/*` and disables the IK stack **immediately** — an instant handoff with no crossfade (the
ragdoll spawns at exactly the same pose, so a fade would only read as ghosting), sets `state =
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
container `"RagdollBodyContainer"` under the rig's **parent** (world root; fallback
`get_tree().current_scene`) and populates it from the `RAGDOLL_BODIES` table (**10**
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
@@ -325,20 +338,18 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
**not** modified.
- **Phase 11 instant handoff + recovery:** `_update_rest_detection()` (only when `state ==
RAGDOLL`)
RAGDOLL`)
reads `_ragdoll_bodies["torso"]`: when its linear/angular velocity drops below
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` (or it is sleeping) it accumulates
`_rest_timer`; after `rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover`
on it calls `_start_recovery()`.
- **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
on it calls `_start_recovery()`. - **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
which snapshots, into `_pre_ragdoll_state`, the 6 IK marker values, the 8 limb bone
rotations, the head bone rotation, the facing profile, the 4 live bend flags, the head
LookAt enabled state, per-leg knee side + flag (`leg_sides`), and the chain-space entry
offsets `torso_chain_offset` / `head_chain_offset` — captured at entry so the snap needs
**no authored geometry constants** and the LEFT mirror's ambiguous `global_rotation`
decomposition (±π) cancels out. This snapshot is the recovery **END target** (possibly a
mid-walk pose; the animation is not resumed).
- **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
mid-walk pose; the animation is not resumed). - **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
unless in RAGDOLL) calls `_capture_ragdoll_pose()` — records the 10 bodies' rig-local
`{pos, rot, half}` (`half` = each capsule's half-length from build-time metadata) and
probes the terrain for the ground surface under the fallen figure via `_probe_ground_y()`
@@ -348,10 +359,9 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
clamp at snap time. It then `_destroy_ragdoll()`s, calls `_reanchor_root_to_landing()`
(translates the rig root so the standing figure's **feet** sit on the ground at the
landing X — `feet = (_captured_landing_center.x, _captured_ground_y)`, `new_root = feet +
FOOT_OFFSET` — and re-bases the captured rig-local positions with a **mirror-aware local
FOOT_OFFSET` — and re-bases the captured rig-local positions with a **mirror-aware local
shift** `local_shift = to_local(shift) − to_local(Vector2.ZERO)`, so the re-base survives
the LEFT X-mirror), sets `state = RECOVERING` + emits.
- **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
the LEFT X-mirror), sets `state = RECOVERING` + emits. - **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
`to_local(center_world − spine_dir·half)` with rotation `chain(spine) − torso_chain_offset`;
the limb bones `rot_u = chain(u) − θ_t − bone_angle_u` and
`rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`; the head via
@@ -365,22 +375,19 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's
`RemoteTransform2D` update flags), then **disables** the 4 arm TwoBoneIK mods + the head
LookAt and **enables** the 2 leg mods (bend side matched to the captured fall pose's knee
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`).
- **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`). - **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
of the 6 markers → their **pre-ragdoll** values and of the arm + head bones to the
`_recovery_bones` targets — upper arm bones take the shortest path, lower arm bones use
`_natural_joint_target(current, target, offset)` (offset = `bone_angle_lower −
bone_angle_upper`; the through-straight rule is evaluated in the joint's rendered space
bone_angle_upper`; the through-straight rule is evaluated in the joint's rendered space
and a `"shortest"` entry in `RECOVERY_JOINT_PATH` bypasses it). The **legs are IK-driven**
toward their tweening, ground-clamped ankle markers. The arm mods + head LookAt stay
disabled during the tween. The baked `"stand_up"` animation is **not** played (a fixed
first keyframe can never match an arbitrary ragdoll rest pose).
- **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
first keyframe can never match an arbitrary ragdoll rest pose). - **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
bend flags + head LookAt state (`_restore_head_lookat_state()`), re-enables the arm mods,
re-shows `Body/*`, sets `state = ANIMATED`, and emits. The resumed IK solve reproduces the
pre-ragdoll pose exactly (the end pose matches the pre-ragdoll frame, including mid-walk
poses).
- `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
poses). - `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
`set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll;
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
@@ -431,15 +438,15 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
assertions; the implementation is user-verified (2026-09-17).
- **Phase 3a director functionality:** adds navigation/walking, speech, an action queue, and a
queue-runner state machine. Enums `RunnerState { IDLE, EXECUTING }` and `ActionPhase { NONE,
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
(feet → root; matches `StageSpawner.STICKMAN_FOOT_OFFSET`), `NAV_AGENT_LOCAL_POS := (0, 385)`
(`== -FOOT_OFFSET`), `ARRIVE_DISTANCE` / `NAV_PATH_DESIRED_DISTANCE` / `NAV_TARGET_DESIRED_DISTANCE`
/ `SPEECH_BUBBLE_OFFSET`. New signals `arrived`, `action_started(action, index)`,
`action_finished(action, index)`, `queue_finished` (on completion, not stop), `queue_changed`
(any queue mutation), `speech_finished`. Exports `walk_speed` (300.0). Public API: `walk_to(
target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED`), `is_walking()`,
target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED`), `is_walking()`,
`speak(text, duration)` (lazily creates a `SpeechBubble` child at `SPEECH_BUBBLE_OFFSET`, auto-hides
+ emits `speech_finished`), the queue API `queue_action` / `clear_queue` / `get_queue` /
- emits `speech_finished`), the queue API `queue_action` / `clear_queue` / `get_queue` /
`remove_action` / `insert_action` / `queue_size` (all mutations emit `queue_changed`), and the
runner API `start_queue` / `stop_queue` / `is_queue_running`. `_ready()` builds a
`NavigationAgent2D` child at `NAV_AGENT_LOCAL_POS` (feet, on the ground-level nav mesh;
@@ -584,7 +591,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`offset_top = 40.0`, `offset_right = -8.0`, `offset_left = -COORDS_PANEL_WIDTH` (320.0),
`grow_vertical = GROW_DIRECTION_END` + `grow_horizontal = GROW_DIRECTION_BEGIN`
(auto-height/width, grows left so text never runs off-screen), `mouse_filter =
MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for selection);
MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for selection);
StyleBoxFlat
bg `Color(0,0,0,0.55)`, border `Color(1,1,1,0.12)` w1, corner radius 4, content margin 8.
Consts `COORDS_PANEL_WIDTH := 320.0` and `COORD_BONE_PATHS` (10 Skeleton2D-relative bone
@@ -647,7 +654,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
(`BUILD_SOLIDS`) in `POLYGON` mode, or `CollisionShape2D` + `CircleShape2D` (48-segment
radial loop, `CIRCLE_SEGMENTS = 48`) in `CIRCLE` mode, toggled via `shape_type`. Exported
properties: `shape_type` (`@export_enum("Polygon","Circle")`), `polygon_points:
PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_width`, and
PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_width`, and
`material_preset` (`@export_enum("None","Wood","Rubber","Cardboard","Metal")`). Presets set
`mass` + `physics_material_override` via static `mass_for()`/`physics_material()`/`tint_for()`
(Wood: mass 3.0, friction 0.6, bounce 0.1; Rubber: mass 0.5, friction 0.9, bounce 0.85;
@@ -663,17 +670,17 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
stickman-prop contacts; the old geometric feet-point stickman test is removed.
- `scripts/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor**
(Phase 4, **not used by the editor**). `@export size: Vector2` (default 96×96), `get_area_rect()
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
dashed border). **No physics and no signals** — it is a pure geometric region evaluated by
`sandbox_stage.gd`'s event engine (`_update_area_entry`) for `entered_area` rules.
- `scripts/prop_utils.gd` — `class_name PropUtils`, `extends RefCounted`; static factory
(**not used by the editor**):
- `create_box(size := Vector2(48,48))` / `create_ball(radius := 24.0)` /
`create_plank(length := 160.0, thickness := 16.0)` / `create_triangle(base := 56.0, height
:= 48.0)` — primitive generators returning shape-payload dictionaries with default
:= 48.0)` — primitive generators returning shape-payload dictionaries with default
dimensions + color themes (wood/rubber/metal/cardboard).
- `spawn_prop(container, position, shape_payload, material_preset := WOOD, initial_velocity
:= Vector2.ZERO) -> PropBlock` — instantiates a `PropBlock`, applies the payload
:= Vector2.ZERO) -> PropBlock` — instantiates a `PropBlock`, applies the payload
(`shape_type` + geometry + colors via `_apply_shape_payload()`, sanitizing polygon points
through `TerrainUtils.sanitize_points()`), sets `linear_velocity` after `add_child` (only
when non-zero), and returns the spawned prop.
@@ -701,7 +708,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
returns early if one exists, so rapid toggling leaves no duplicate proxies.
- **Phase 10 external forces:** a **"Knock Up"** button (`_knock_up()`) tests impulses beyond
gravity: when the rig is in RAGDOLL mode it calls `StickmanRig.apply_ragdoll_velocity_boost(
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
preserving internal structure), and applies the same upward velocity delta to every dynamic
prop (`RigidBody2D` child of `_environment`) so the whole pile flies up together.
- **Phase 11 recovery UI:** a `Label("Rest")` + `SpinBox` (`_rest_timeout_spinbox`, min 0.1 /
@@ -781,7 +788,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`node.queue_changed → _director_visuals.mark_dirty` when a stickman is placed; `object_deleted` →
`mark_dirty()`. `_set_build_controls_visible()` also hides the Direct button in PLAY.
- **Phase 4 triggers & event system:** adds a When→Then **rule system** — `_event_rules:
Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at_waypoint`,
Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at_waypoint`,
`action_finished`, `speech_finished`, `entered_area`, `collided`; action types reuse the Phase
3a set: `walk_to`/`speak`/`wait`/`ragdoll`/`recover`). Rules persist across Play/Edit mode
toggles but are **not** saved to disk (Phase 5). A geometric **area-entry engine** runs in PLAY
@@ -832,7 +839,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
an instantiated `scenes/asset_selector.tscn` `PopupPanel`, added to the UI `CanvasLayer` in
`_build_ui()` with theme/font overrides applied via `AssetSelector.apply_font`), backed by a
dim backdrop `_selector_dim` (a black `ColorRect` at `SELECTOR_DIM_ALPHA` = 0.5, `mouse_filter =
MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only while the selector
MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only while the selector
is open). State
`_selector_open`/`_selector_kind`, `_thumbnail_queue`, `_thumbnail_busy`. `_ready()` builds
`_stickman_library`/`_thumbnail_cache` and the two thumbnail renderer `Node`s
@@ -927,7 +934,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
are `static func` and cannot run in a `const`). Public API: `static get_entries()` (builds once,
returns the 4 templates), `static get_ids()`, `static get_entry(id) -> Dictionary` (`{}` if
unknown), `static get_default_id() -> String` (`"crate"`). Template dicts carry `{id, name,
material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOOD`), Ball/Rubber
material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOOD`), Ball/Rubber
(`create_ball()` + `RUBBER`), Plank/Metal (`create_plank()` + `METAL`), Triangle/Cardboard
(`create_triangle()` + `CARDBOARD`) — generator color themes already match the presets.
- `scripts/thumbnails/thumbnail_cache.gd` — `class_name ThumbnailCache`, `extends RefCounted`
@@ -953,7 +960,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
identical to the rig actually placed on stage, not a re-implementation of the editor preview.
- `scripts/thumbnails/prop_thumbnail.gd` — `class_name PropThumbnail`, `extends Node` (Phase 3b,
**not used by the editor**). Renders a prop template into a `Texture2D`. `const SIZE :=
Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camera2D` pattern as
Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camera2D` pattern as
`StickmanThumbnail`. `render(payload, material_preset) -> Texture2D` mirrors `PropBlock`'s
geometry into a plain `Node2D` with a `Polygon2D` (fill) + `Line2D` (closed loop, round
joints/caps) — **not** a `RigidBody2D`, so nothing falls under gravity inside the viewport;
@@ -969,13 +976,13 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
stickmen, then `popup_centered()`), `set_entries(entries)` (used by Refresh — resets page +
thumbnails), `set_thumbnail(entry, tex)` (lazy handoff from the stage's drain), `close()`, and
`apply_font(ui_font, emoji_font)` (walks the authored controls). `_ready()` sets `exclusive =
true` and wires the footer buttons; `_unhandled_input` maps `KEY_ESCAPE` → `cancelled.emit()`
true` and wires the footer buttons; `_unhandled_input` maps `KEY_ESCAPE` → `cancelled.emit()`
(the Window's built-in Esc close is not relied on). Re-centers on window resize: the root's
`size_changed` signal re-runs `popup_centered()` while the popup is visible. Cell build:
`_rebuild()` frees the `%GridContainer` children, toggles the empty-state `%EmptyLabel`,
shows/hides Prev/Next/page label by page count, and slices the current page via the
**static pure** `page_bounds(total, page, page_size) -> Dictionary {start, end, total,
page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect`
page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect`
(cached/placeholder texture) + a name `Label` + (prop only) a material-badge `Label`; cells emit
`item_selected`. No cell is pre-highlighted on open (the previous selection highlight styling
was removed). The `.tscn` is a **minimal shell**
@@ -1014,7 +1021,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
disables in PLAY. Also draws the box-select marquee via `set_box_rect(rect)`.
- `scripts/stage_grid.gd` — `class_name StageGrid`, `extends Node2D`; the optional world-space
grid overlay (Phase 2). Pure `_draw()`: grid lines pan/zoom with the camera (`camera.get_screen_center_position()`
+ viewport extent / zoom), with a heavier **major line every 5 cells**; `grid_size` / `enabled`
- viewport extent / zoom), with a heavier **major line every 5 cells**; `grid_size` / `enabled`
are set by `SandboxStage`. Added as the first child of the stage root so it renders behind
`World`; no hit-testing.
- Scenes:
@@ -1061,11 +1068,12 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
palette-button selector grids.
### Body-part data model
- 10 internal part keys (ordered): `head`, `torso`, `left_upper_arm`, `left_lower_arm`,
`right_upper_arm`, `right_lower_arm`, `left_upper_leg`, `left_lower_leg`,
`right_upper_leg`, `right_lower_leg`.
- Shape dictionary: `{ "shape_type": String, "points": Array[{x,y}], "color": "#hex",
"closed": bool, "vertex_flags": Array[int] }`.
"closed": bool, "vertex_flags": Array[int] }`.
- `shape_type` values: `"line"`, `"rectangle"`, `"circle"`, `""` (empty).
Descriptive tag in Phase 2 — rendering uses `closed`.
- `closed`: `true` = filled + closed outline, `false` = open outline-only.
@@ -1073,7 +1081,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`1` = user-created via "Create Point" (hollow rectangle).
- **Phase 4:** A panel stores a `shapes[]` array of shape dictionaries. Z-order = array
position (first = back, last = front). Per-part data includes `{shapes[], position,
rotation, scale}`.
rotation, scale}`.
- **Phase 8:** per-part data adds `pivot` `{x, y}` (local bounding-box center = rotation
origin) and `length` (float, bbox extent along the segment axis) for format v1.4; the
`.stk` root also gains a top-level `proportions` object (5 rig bone lengths). All three
@@ -1086,6 +1094,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`"1.0"`–`"1.4"` files auto-migrate on load).
### settings.json (Phase 6)
- Persisted editor preferences written to `settings.json` via `_save_settings()` and
loaded in `_load_settings()`.
- Keys: `version`, `grid_size`, `snap_to_grid`, `recent_colors`, `show_pose_guide`.
@@ -1096,12 +1105,14 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
via `_broadcast_settings()`.
### Legacy scene (do not delete)
- `stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets +
`RemoteTransform2D` + `Line2D` limbs, plus an embedded `@tool` script drawing the head
circle. Animations: "walk", "walk_to", "RESET". **Not** the current main scene; kept for
future animation/rigging phases.
## Editor conventions
- `.godot/` is gitignored; never edit it manually.
- Scene files (`*.tscn`) and `.import` files are text-based; use Godot's editor for complex changes.
- UID references (Godot 4 native) exist in scenes; do not change them by hand.
+134
View File
@@ -0,0 +1,134 @@
# High-Level Architectural Plan: Graphical UI & Palette Polish Phase
## Objective
To overhaul the `SandboxStage` user interface into a polished, kid-friendly graphical experience, while establishing a flexible, schema-driven theme system that seamlessly supports present UI elements and future feature additions (such as dynamic speech bubble styling, sound customization, and extended rule editors).
---
## 1. Centralized Theme & Resource Infrastructure
### 1.1 JSON Theme Schema Expansion (`sandbox_theme.json`)
- **Unified Theme Specification**: Expand the `sandbox_theme.json` contract to serve as the single source of truth for typography, spatial grids, mode accent colors, and component-specific visual presets.
- **Subsystem Category Blocks**: Structure the schema into modular sub-dictionaries (e.g., `fonts`, `mode_colors`, `grid`, `popups`, `speech_bubbles`).
- **Graceful Fallback Pipeline**:
1. Specific component override value.
2. Subsystem block default (e.g., `speech_bubbles.style_defaults`).
3. Global theme default (`fonts.ui_font`).
4. System fallback (`ThemeDB.fallback_font`).
- **Resource Preloading & Validation**: Ensure the theme loader validates paths, gracefully handles missing assets with non-blocking warnings, and caches loaded `Font` and `StyleBox` objects.
---
## 2. Graphical Stage Chrome & Mode Navigation
### 2.1 Mode Switcher & Viewport Frame
- **3-Segment Graphical Switcher**: Replace standard text toggles with a stylized 3-segment mode control (`[✏️ Edit | 🎬 Direct | ▶️ Play]`).
- **Viewport Frame Accent**: Render a screen-constant border frame (`_mode_frame`) around the viewport that dynamically tints according to the active mode accent color.
- **Mode Badge Pill**: Maintain a rounded, high-contrast status badge in the viewport corner displaying the active state with mode-specific icons and accent backgrounds.
### 2.2 Status Bar & Dynamic Cursor System
- **Status & Coordinate Bar**: Polish the bottom bar for scannability, hosting contextual operational hints and live spatial coordinates.
- **Contextual Cursors & Floating Tooltips**:
- Swap between arrow/crosshair, placement cursors, and custom action reticles depending on mode and pending operations.
- Attach floating, high-contrast tooltip pills offset from the cursor during target selection workflows (e.g., direct mode commands or trigger mapping).
---
## 3. Icon-Based Palette & Thumbnail Engine
### 3.1 Draggable / Clickable Card Palette
- **Visual Asset Cards**: Transition spawner controls from text buttons to visual card tiles displaying item icons, labels, and category badges.
- **Placement Integration**: Support both click-to-select active placement and direct drag-and-drop actions onto the stage.
### 3.2 Modal Asset Selector & Thumbnail Pipeline
- **Asset Selector Modal**: Refine the modal popup and its dimming background backdrop for browsing extended stickmen and prop libraries.
- **Asynchronous Thumbnail Cache**:
- Streamline off-screen thumbnail generation for custom props and stickmen.
- Cache generated preview textures to prevent UI stuttering during library scans.
---
## 4. Visual Overlay & Placement Feedback Engine
### 4.1 Terrain Drag-Painting & Ghosting Feedback
- **Block-Unit Stride Alignment**: Align placement ghosts and Bresenham line painting to the active terrain template's AABB extent.
- **3-State Ghost Tinting**:
- **Green**: Valid, empty space ready for placement.
- **Faint / Neutral**: Same-type block (skipped to avoid double-stamping).
- **Red**: Conflicting entity or invalid geometry.
### 4.2 Guidance & Target Trajectories
- **Axis-Aligned Guides**: Render high-contrast dashed lines along cardinal axes during Shift-locked terrain drags.
- **Rubber-Band Trajectories**: Draw contextual connection lines and target reticles from character origins to mouse targets during Direct mode actions.
---
## 5. Reusable Control Widgets & Forward-Compatible Systems
### 5.1 Standardized Kid-Friendly UI Components
Develop a suite of reusable, theme-aware control widgets that can be embedded across dialogs, action popups, and future parameter editors:
- **Color Swatch Selector**: Clickable circle palettes populated from theme swatch arrays or editor history.
- **Preset Size Toggles**: Segmented buttons for coarse adjustments (e.g., **Small**, **Medium**, **HUGE**) mapping to backend integer/scale metrics.
- **Preset Style / Font Pickers**: Visual icon or text buttons allowing selection between preset fonts (e.g., Standard, Comic, Pixel, Scary).
- **Emoji Quick Grid**: A dedicated grid widget for inserting expressive graphics directly into text fields.
### 5.2 Context Menus & Action Popups
- **Uniform Popup Styling**: Apply standardized theme rules, font sizes, and emoji scaling across action context menus, trigger submenus, and dialogs.
- **Clean Action Dialogs**: Align text input (Speak), duration (Wait), and rename popups with dark-mode sandbox styling.
### 5.3 Extension Blueprint: Speech Bubble Customization System
Architect the theme and component pipeline so that future speech bubble upgrades plug directly into the visual foundation without refactoring:
- **Data Contract**: Ensure the `speak` action payload accepts visual parameters (`font_preset`, `size_preset`, `color`, `bubble_style`) alongside text.
- **Style Engine Integration**: Speech bubble nodes consume geometry styles (Standard, Yell, Thought, Whisper) and theme font presets directly from `sandbox_theme.json`.
- **UI Reusability**: The standard Color Swatch, Size Toggle, Font Picker, and Emoji Grid widgets drop directly into the Direct mode `Speak` parameter dialog.
---
## Recommended Execution Sequence
1. **Phase 1: Theme Engine & Core Schema**: Extend `sandbox_theme.json` schema, build centralized theme loading/fallback pipeline, and integrate theme application hooks across stage UI.
2. **Phase 2: Chrome & Mode Navigation Polish**: Implement the 3-segment mode control, viewport frame accents, status badges, and cursor/tooltip overlay controllers.
3. **Phase 3: Reusable UI Component Library**: Build the Color Swatch, Size Toggle, Font Picker, and Emoji Grid widgets.
4. **Phase 4: Graphical Palette & Modal Overhaul**: Implement icon-based asset cards, refine the `AssetSelector` modal, and optimize asynchronous thumbnail caching.
5. **Phase 5: Context Menus & Overlay Polish**: Update action popups with uniform theme sizing and polish drag-painting ghost/guide overlays.