Add initial draft for Phase 7 UI Polish plan
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user