Files
stickman/AGENTS.md
T
ryan 273090993e feat: Implement facing profiles and bend direction toggles in test harness
- Added functionality for skeleton IK bone switches, allowing users to dynamically change the bend direction of joints via a context menu.
- Introduced facing profiles (LEFT, RIGHT, FORWARD) that modify the bend direction of limbs based on the selected profile.
- Implemented body part z-ordering based on the facing profile, ensuring correct rendering order of limbs relative to the torso.
- Added a coordinates display panel that shows the position and rotation of the Skeleton2D and its bones, as well as IK target positions.
- Created a new script to generate a walk animation for the stickman rig, including keyframe tracks for various IK targets.
- Updated documentation to reflect the new features and their specifications.
2026-08-23 22:21:23 -04:00

398 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — stickman (Godot 4.4)
## Project type
- **Godot 4.4** 2D/GUI project (Forward Plus renderer)
- No CLI build/test/lint commands; open in the Godot editor to run
- **Main scene:** `res://scenes/stickman_editor.tscn` (set as `run/main_scene` in `project.godot`)
## 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.
## 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
selection so only one shape is selected at a time (`shape_selected` → deselect others).
Collects per-part `{shapes[], position, rotation, scale, pivot, length, guide_offset}` for save/load.
- **Phase 8 save export:** writes top-level `proportions` (hardcoded master-rig rest-pose
constants 168/200/200/200/391.5 via the `PROPORTIONS` const) and per-part `pivot`/`length`
computed from the panel's local shape bounding box (`_compute_part_pivot_length()`): `pivot` =
bbox center, `length` = bbox width for the 4 arm parts (`X_AXIS_PARTS`) and bbox height
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
`_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
rig attachment origin, NOT the circle center — torso→"Hips", upper arms→Shoulders, lower
arms→Elbows, upper legs→"Hips", lower legs→Knees). Write-only metadata like `pivot`/`length`;
the load path (`_apply_json_data`) ignores it, so v1.0v1.4 files load unchanged and gain the
key on their next save.
- **Phase 6 recent colors:** stores `_recent_colors: Array[String]` (max 8,
most-recent-first), loads from `settings.json` (`recent_colors` key) in
`_load_settings()`, saves on each color selection via `_save_settings()`, and
broadcasts to all 10 panels via `_broadcast_recent_colors()`.
- `_on_color_selected(color, part_name)`: deduplicates, inserts the hex string at the
front, trims to 8 entries, saves settings, then broadcasts to all panels.
- `_broadcast_recent_colors()`: pushes `_recent_colors` to every panel via
`BodyPartPanel.set_recent_colors(_recent_colors)`.
- **Phase 6 status bar (cursor coords):** `_process()` polls
`get_global_mouse_position()` each frame, checks each panel via
`is_cursor_over_drawing(global_pos)` and the preview via
`is_cursor_over_preview(global_pos)`, converts to world space via
`global_to_world(global_pos)`, and writes `"X: ### Y: ###"` to `_status_cursor_coords`.
- **Phase 6 snap status:** `_status_snap_status` displays `"SNAP: ON"` / `"SNAP: OFF"`,
set in `_ready()` and re-synced when snap is toggled (`_on_edit_menu_id_pressed`).
- **Phase 7 pose guide toggle:** stores `_show_guide: bool` (default `true`), persisted to
`settings.json` under the `show_pose_guide` key (default `true`) via `_load_settings()` /
`_save_settings()`. The View menu item (id 1) is a **dynamic, text-only** label (no
checkmark): "Hide Pose Guide" while the guide is visible, "Show Pose Guide" while hidden,
set via `_guide_menu_label()`. `_update_guide_menu_item()` refreshes only the item text;
it is synced on the `about_to_popup` signal (`_on_view_menu_about_to_popup`) and again at
the end of `_load_settings()` (so a persisted `show_pose_guide: false` shows "Show Pose
Guide" immediately at startup). `_on_view_menu_id_pressed` (id 1) toggles the state, saves,
and broadcasts. `_broadcast_settings()` pushes the value to the preview via
`WholeStickmanPreview.set_show_guide(_show_guide)` (called in `_ready()` after
`_load_settings()`).
- `scripts/body_part_panel.gd` — `class_name BodyPartPanel`, `extends PanelContainer`.
Reusable per-part editor. Public API:
- `set_shape_data(data: Variant)` — import shape data (Array or single Dictionary; used on Load/Clear)
- `get_shape_data() -> Array[Dictionary]` — export array of `{shape_type, points, color, closed, vertex_flags}`
- `clear_shape()` — reset panel, clears all shapes (does **not** emit `shape_changed`)
- `select()` — mark the panel's topmost shape as selected (white outline highlight)
- `deselect()` — clear selection and cancel any in-progress vertex drag
- `signal shape_changed(shapes: Array)` — emitted when any shape is created, modified, deleted, or reordered
- `signal shape_selected()` — emitted on left-click; the editor deselects all other panels
- `signal color_selected(color: Color)` — emitted when the color is confirmed (OK button)
- `set_recent_colors(colors_hex: Array)` — clears existing ColorPicker presets and populates with the given hex colors
- `is_cursor_over_drawing(global_pos: Vector2) -> bool` — true if `global_pos` is over the drawing surface
- `global_to_world(global_pos: Vector2) -> Vector2` — maps a global position to drawing ("world") space
- **Phase 2 vertex editing:** left-click on a shape selects it; drag a vertex handle to
reshape in real time; with a shape selected, right-click near an outline edge offers
"Create Point" (inserts a vertex flagged `1` at the edge midpoint). Original vertices
are filled circles; user-created vertices are hollow rectangles.
- **Phase 4 multi-shape:** panels store a `shapes[]` array. Right-click context menu
includes "Send Back" (id 7) and "Bring Forward" (id 8) for Z-ordering. "Delete" (id 5)
removes the specific shape under the mouse. `_selected_shape_idx` tracks which shape
is active for vertex editing.
- **Per-panel zoom:** mouse wheel multiplies `_zoom` by 1.10, clamped to `[0.3, 3.0]`;
drawing and input hit-testing both run in world space via `draw_set_transform`.
- **Phase 6 touchpad:** `_gui_input` handles `InputEventMagnifyGesture` (pinch zoom)
and `InputEventPanGesture` (2-finger drag panning), both checked before
`InputEventMouseButton`. Pan gesture delta is multiplied by 3.0 for speed
parity with mouse panning.
- **Phase 6 cursor-centered zoom:** both mouse wheel and pinch zoom adjust
`_pan_offset` so the world point under the cursor stays fixed during zoom.
- **Selection gizmos always on top:** bounding box, rotation circle, and scale
crosses for the selected part are drawn in a second pass after all parts,
via `_selected_gizmo_bounds`, so they always render in front.
- `scripts/whole_stickman_preview.gd` — `class_name WholeStickmanPreview`, `extends Control`;
the assembly preview. Owns per-part position/rotation/scale, Z-order (`_part_order`),
selection + gizmos, grid drawing, and pan/zoom. Public API includes `set_body_parts()`,
`set_show_guide(enabled: bool)`, `reset_view()`, `is_cursor_over_preview(global_pos)`, and
(Phase 9 Round 5) `get_guide_joint_preview(joint_name) -> Vector2` — returns the preview-space
position of a `GUIDE_JOINTS` entry via `_guide_to_preview()`, guarded against unknown names
(`push_warning` + `Vector2.ZERO`).
- **Phase 7 pose silhouette guide:** `set_show_guide()` stores `_show_guide: bool`
(default `true`) and redraws; `_draw_silhouette_guide()` is called in `_on_preview_draw()`
after the part loop and before the drag highlight/selection gizmos, so it renders above
the grid **and in front of user parts** (ghosting over them), below the selection
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
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;
`GUIDE_SCALE = 1.0`, `GUIDE_FIGURE_CENTER = (0, -93.75)`, `GUIDE_HEAD_RADIUS = 100.0`).
Color-coded: left limbs cyan-blue `Color(0.35, 0.70, 1.00)`, right limbs orange-red
`Color(1.00, 0.50, 0.20)`, central spine/head white, with lines at alpha `0.45` and joint
dots at alpha `0.65`. Joint dots use `GUIDE_JOINT_RADIUS / _zoom` (6 px constant screen
size). The guide is pure drawing — **no hit-testing** is added, so it never intercepts
part dragging/selection.
- `scripts/stk_rig_adapter.gd` — `class_name StkRigAdapter`, `extends RefCounted`; a **standalone
runtime adapter** (Phase 8, **not referenced by the editor**, extended by Phase 9).
`static func apply(stk_data, rig)` fits an instantiated `master_rig.tscn` to a loaded `.stk`
dictionary, calling three private helpers in order: `_fit_bones` (re-fits the 8 limb `Bone2D`
lengths + lower-bone origins, and zeroes the Head driver's local position so the chin sits on
the neck joint), `_recalibrate_ik` (repositions the `IK_Targets/Left|Right_Hand` and
`Left|Right_Leg` targets), and `_mount_shapes` (mounts `.stk` shapes onto the `Body/*` visual
nodes — one node per shape: closed → single `Polygon2D`, open → single `Line2D` width 2). The
`RemoteTransform2D` drivers keep their defaults (`update_rotation = true`), so mounted shapes
follow their bones in every pose.
- **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
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.
- **Phase 9 Round 2 bugfix (hanging-convention mount):** driver rotation neutralization was
**removed** — the `RemoteTransform2D` drivers keep `update_rotation = true`, so each mounted
part rotates to follow its bone in every pose (IK flexing included). `_mount_shapes()` no
longer reads the file's `pivot`/`length` fields — it recomputes a per-part bounding box at
mount time via `_compute_part_bbox()` (empty bbox → the part is skipped) and derives the
mount transform via `_compute_mount_transform()`, which emits `{anchor, scale, theta}`:
geometry is mounted in the rig's **hanging convention** (joint anchor at the local origin,
far end along local `+Y`). Anchors: head and torso → bottom-center `(cx, max_y)` (chin / hip
end at the origin); left limbs drawn horizontally → `(max_x, cy)`; right limbs drawn
horizontally → `(min_x, cy)`; vertically drawn limbs → top-center `(cx, min_y)`. Alignment
rotation θ maps the far end onto local `+Y`: head `0`, torso `π` (driver π cancels it at
rest), left horizontal limbs `−π/2`, right horizontal limbs `+π/2`, vertical limbs `0`.
Scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`)
scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`); the
cross axis stays 1:1 (so horizontally-drawn legs become ~200×28, not 101-px bars).
`_bone_length_for()` maps each part to its bone length (upper/lower arm/leg, torso).
`_map_point()` applies `(P J) ⋅ S` then `q.rotated(θ)`. The **head driver's** local
position (`Skeleton2D/Torso/Head/RemoteTransform2D`) is zeroed in `_fit_bones()` so the
mounted head's chin lands on the neck joint. `DEFAULT_LINE_WIDTH := 2.0` (was 16.0) matches
the editor's 2 px outline. `_reset_node_transform()` still resets each `Body/*` container's
scale to `(1, 1)` and rotation to `0` before mounting (position untouched, owned by the
driver).
- **Phase 9 Round 3 bugfix (part preview transform + one node per shape):** the mount
pipeline now **composes the part's preview transform** `E(P) = C + R(rot)·S·(P C)`
(scale-then-rotate about the raw bbox center — the editor's exact Whole-Stickman-preview
transform) **before** the hanging-convention mount. `_mount_shapes()` reads the per-part
`rotation` (degrees, default `0.0`) and `scale` (`{x,y}`, default `(1,1)`) from the part
dict and applies `E` to the raw joint end `J_raw` and far point `F_pt_raw`
(`J' = E(J_raw)`, `F' = E(F_pt_raw) J'`). The anchor, alignment θ, and bone-fit scale `s`
are then computed on the **transformed geometry**: rotations near ±180°
(`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end (`A = F_pt'`, `V = F'`)
so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint and its
hip end at the neck); other rotations keep limbs attached along their bones (a 90° forearm
hangs from the elbow with its content turned, exactly as assembled). The bone-fit scale
`s = bone_length / |V|` is measured on the transformed extent so user-scaled parts are not
double-fitted. The **head** mounts upright with `θ = 0`, `s = 1` (a bone-fit scale would
double-scale the face), but still applies the part scale through `E` (face ≈160 px) with
the chin at the neck joint and the flip anchor rule still applying. `_mount_shape()` mounts
**one node per shape**: closed → single `Polygon2D` (fill only, no paired `Line2D` outline);
open → single `Line2D` (width 2).
- **Phase 9 Round 4 bugfix (head chin drop):** adds `const HEAD_CHIN_DROP := 28.0`, derived
from the editor's pose guide — the head circle (radius 100) is centered at the Head joint
`(0, 463.5)`, so its bottom is `363.5`; the neck (Head bone origin) is at `391.5`, so the
chin drops 28 px below the neck. The mounted head points get a `Vector2(0.0, 28.0)`
rig-space translation (`offset` in `_compute_mount_transform()` / `_map_point()`, applied
**after** the part transform and the `(θ = 0, s = 1)` transform; flip-agnostic — only the
head branch sets a non-zero `offset`). Result: the head's chin lands at world ≈ `(0, 363.5)`,
overlapping the torso's top (which ends at `391.5`) by 28 px — matching the silhouette
guide in the editor.
- **Phase 9 Round 5 guide-offset application:** `_mount_shapes()` reads each part's
`guide_offset` (`{x, y}`, default absent) and, **only when the key is present** (old files
keep the previous offset-0 behavior and the head falls back to the Round 4
`HEAD_CHIN_DROP`), applies a node-frame translation
`t = (guide_offset + (A C)).rotated(c_node)` where A = the mount anchor already computed
(the transformed joint end `J'`, or the transformed far end `F_pt'` when flipped — Round 3),
C = the raw bbox center, and `c_node` = the part's driver `RemoteTransform2D.global_rotation`
at apply time (read via the reintroduced `DRIVER_PATHS` const; null-guarded, fallback 0.0).
This converts the editor's master-space guide offset into a bone-relative placement so the
harness reproduces the guide placement 1:1 (and, for the head — mapped to the guide **Neck**
joint — subsumes the `HEAD_CHIN_DROP` fallback). `t` is applied in `_map_point()` as the
final rig-space translation, after `E`/θ/scale/flip and independent of the flip logic.
Re-saving a `.stk` from the editor populates the offsets.
- **Phase 9 Round 6 bugfix (guide-driven anchor selection):** when `guide_offset` is present,
the joint anchor in `_compute_mount_transform()` is now whichever transformed end
(`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
`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
preserves every previously-correct case and naturally reproduces the 180° flip (a flipped
part's far end lands nearest the joint — e.g. the flipped right upper arm shoulder and the
flipped torso neck end), plus the head chin (nearest the guide Neck). Old files **without**
the key keep the previous family rules + flip heuristic exactly as before. `theta`, `s`, the
Round 5 offset `t`, and the `HEAD_CHIN_DROP` fallback are unchanged — they consume
`anchor`/`v` generically.
Targets `master_rig.tscn` node paths; every node lookup is null-guarded (missing node →
`push_warning` + skip, never crash). Consumed by a future runtime pipeline.
- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static
factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a
`.stk` file into a live, rigged `master_rig.tscn` instance:
- `static func load_stk(path: String) -> Dictionary` — reads a `.stk` file (`FileAccess` +
`JSON.parse_string`); returns `{}` + `push_warning` on failure.
- `static func spawn_from_data(stk_data: Dictionary) -> Node2D` — instantiates
`res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root.
- `static func spawn(path: String) -> Node2D` — `load_stk()` then `spawn_from_data()`;
returns `null` on empty data.
- `scripts/test_harness.gd` — **standalone staging scene** (Phase 9, **not wired into the editor**;
run via **F6** on `res://scenes/test_harness.tscn`) for debugging bone scales, vector drawing
offsets, and IK limits in isolation. Top UI bar: "Open .stk…" button → `FileDialog` (`*.stk`);
quick-select buttons for `stickmen/break.stk`, `stickmen/basic.stk`, `stickmen/test.stk`; "Show
Bones" / "Show IK Handles" checkboxes; a status label showing the loaded filename. Viewport:
`SubViewportContainer` → `SubViewport` → world `Node2D` + enabled `Camera2D`; middle-mouse pan,
mouse-wheel zoom, camera recenters on each spawn. Each load frees the previous rig and spawns a
fresh one via `StickmanFactory.spawn()`. Debug overlay (a world-space `Node2D` `_draw()`): true
bone **segments** (a joint dot at each `Bone2D` origin + a parent→child line to each `Bone2D`
child, color-coded left cyan / right orange / central white) with leaf bones drawn out to their
IK targets (`LeftLowerArm→Left_Hand`, `RightLowerArm→Right_Hand`, `LeftLowerLeg→Left_Leg`,
`RightLowerLeg→Right_Leg`) so wrist/ankle joints are visible (Phase 9 Round 2; previously only
origin→parent-origin lines were drawn). The **Head** leaf is the exception (Phase 9 Round 3):
its IK target is a `SkeletonModification2DLookAt` aim point, not a joint, so it is **not** in
`LEAF_BONE_IK_PATHS` and the no-target fallback draws a ~90 px segment along the bone's own
direction (`Vector2(length, 0)` rotated by `bone_angle` then `global_rotation`) instead of a
line to the aim point; colored markers at
`IK_Targets/{Left_Hand,Right_Hand,Left_Leg,Right_Leg}` when "Show IK Handles" is on. Interactive
IK: click-drag the `Marker2D` IK targets; the scene's `SkeletonModificationStack2D` TwoBoneIK
flexes limbs live. The harness enables the modification stack (`enabled = true`) after each
spawn.
- **Phase 9 Round 7 draggable Torso & Head handles:** `IK_HANDLE_PATHS` now has **6 entries**
— the 4 limb targets plus `"Head"` (`IK_Targets/Head`, the `SkeletonModification2DLookAt` aim
point) and `"Torso"` (`IK_Targets/Torso`, whose child `RemoteTransform2D` moves the hip bone).
Dragging the **Torso** handle translates **bones only** (no target following) — the marker's
`RemoteTransform2D` moves the hip bone and the whole skeleton + `Body/*` visuals follow
rigidly, while the limb/head targets stay put (dragging the figure away from them stretches
the limbs toward the stationary targets, per user decision). Dragging the **Head** handle
drives the Head bone's LookAt rotation (clamped at the authored ~55° constraint);
`Body/Head` follows. `_handle_color()` colors the head marker yellow (`HANDLE_COLOR_HEAD`)
and the torso marker magenta (`HANDLE_COLOR_TORSO`); hands stay green, feet blue. The IK
overlay also draws a **null-guarded semi-transparent yellow aim line** from the Head bone
origin to the head marker (`_draw_ik_handles`, width `1.5/zoom`, alpha `0.5`) — a visual aid
for the LookAt test.
- **Phase 9 Task 1 skeleton IK bone switches:** adds a "Facing" `MenuButton` (leftmost control
in the top-bar `HBox`) and per-joint bend-direction toggles for the rig's TwoBoneIK "Flip
Bend Direction" flags. `enum FacingProfile { LEFT, RIGHT, FORWARD }` (values used directly as
menu item ids); `BEND_JOINTS: Array[String] = ["LeftArm","RightArm","LeftLeg","RightLeg"]`
with `BEND_JOINT_BONE_PATHS` (each joint → its lower `Bone2D` NodePath relative to
`Skeleton2D`, e.g. `Torso/LeftUpperArm/LeftLowerArm`); `PROFILE_FLAGS` maps each profile to a
per-joint `flip_bend_direction` set (LEFT: arms off / legs on; RIGHT: arms on / legs off;
FORWARD: LeftArm off, RightArm on, LeftLeg on, RightLeg off — matching `master_rig.tscn`'s
authored defaults). State `_facing_profile` (default `FORWARD`, harness-level, persists across
spawns), `_context_joint`, `_bend_joint_bones`, `_bend_modifications`, `_facing_button`/
`_facing_menu`, `_context_menu`. The Facing popup (Left/Right/Forward) uses dynamic text-only
labels with the current profile prefixed `[√] `, refreshed on `about_to_popup` and after
selection (mirroring the editor's snap-menu pattern). Runtime resolution:
`_resolve_bend_joints()` (called from `_resolve_rig_nodes()`) resolves the 4 lower-limb
`Bone2D`s via `get_node_or_null` and matches each `SkeletonModification2DTwoBoneIK` in the
modification stack by its `joint_two_bone2d_node` NodePath (no hardcoded stack index);
`push_warning` on missing nodes/mods. Per-joint toggle: right-click inside the viewport on an
elbow/knee (the upper↔lower limb connector, within `JOINT_HIT_RADIUS_PX := 14.0` screen px
converted to world by `_camera.zoom.x`, nearest joint wins) pops a one-item context menu
labeled **"Normal Bend"** (when `flip_bend_direction` is currently true) or **"Invert Bend"**
(when false); selecting toggles that joint's `flip_bend_direction` on the live TwoBoneIK
modification. Only the 4 elbows/knees are right-click targets — shoulders/hips/wrists/ankles/
head/torso are not. Lifecycle: `_free_current_rig()` clears `_bend_joint_bones`/
`_bend_modifications`/`_context_joint`; `_load_and_spawn()` re-applies
`_apply_facing_profile(_facing_profile)` right after `_ensure_modification_stack_enabled()` so
every fresh spawn matches the current profile. No persistence to disk.
- **Phase 9 Task 2 body-part z-order:** `_apply_facing_profile()` now also sets the
`_facing_profile` state itself (previously only the menu handler did) and calls
`_apply_body_z_order()`, so bend flags + draw order stay in sync from one entry point.
`const BODY_CONTAINER_PATH := "Body"` and `const Z_ORDER_BY_PROFILE: Dictionary` map each
`FacingProfile` to the rig `Body/*` visual part node names in **back-to-front draw order**
(Godot 4 `Node2D` draws siblings in tree order; all parts keep `z_index = 0`). FORWARD:
torso → left/right upper legs → left/right lower legs → left/right upper arms → left/right
lower arms → head (all limbs in front of the torso); LEFT: left arm pair then left leg
pair **behind** the torso, right leg pair then right arm pair in front; RIGHT: mirrored.
In every profile upper limbs stay behind lower limbs; far-side (behind-torso) arms draw
behind the legs while near-side arms draw in front of the legs; the **head is always
frontmost**. State `_body_container: Node2D` (resolved in
`_resolve_rig_nodes()` via `get_node_or_null`, null-guarded with `push_warning`, cleared
in `_free_current_rig()`). `_apply_body_z_order()` walks the profile array back-to-front
and `move_child(part, count - 1)`s each existing child (missing parts skipped), which
yields the profile order; unknown extra children stay at the back. Safe with the adapter:
shapes are children of the part nodes, so moving a part moves its whole shape group. No
persistence to disk.
- **Phase 9 Task 3 coordinates display:** adds a "Show Coords" `CheckBox` in the top-bar
`HBox` (after "Show IK Handles", before the status label), default ON, toggled via
`_on_show_coords_toggled(pressed)` (sets `_show_coords: bool = true`, flips
`_coords_panel.visible`). A code-built right-side readout (`_build_coords_panel()`, called
from `_build_ui()` after the viewport container so it renders in front): `_coords_panel`
(`PanelContainer`) + monospace selectable `_coords_label` (`RichTextLabel`,
`selection_enabled` + `context_menu_enabled` + `fit_content`, autowrap off, scroll off,
`FOCUS_CLICK`; `SystemFont`: Consolas/Menlo/DejaVu Sans Mono/Courier New via the
`normal_font`/`normal_font_size` (18) theme overrides), anchored `PRESET_TOP_RIGHT`,
`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);
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
paths: Torso, Torso/Head, both upper/lower arms, both upper/lower legs). State
`_coord_bones: Dictionary` (bone display name → `Bone2D`, keyed by `path.get_file()`),
`_show_coords`. `_resolve_rig_nodes()` calls `_resolve_coord_bones()` (via
`_skeleton.get_node_or_null`, `push_warning` on missing); `_free_current_rig()` clears
`_coord_bones` (toggle persists across respawns). `_process(delta)` early-outs when hidden
or label null, else `_update_coords_display()` — sections "Skeleton2D" pos + rot deg,
"Bones" pos + rot deg, "IK Targets" pos only (reusing `IK_HANDLE_PATHS`/`_ik_handles`);
"No rig loaded" fallback; every read guarded with `is_instance_valid`; the label text is
only reassigned when the built string changes, so an active text selection survives idle
frames. Values are
world-space (`global_position`/`global_rotation`), rotation in degrees via
`_fmt_deg(rad)` (1 decimal, `°`), `_fmt_vec2(v)` for positions. No persistence to disk.
- Scenes:
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
`%CenterColumn`, `%WholeStickmanPreview`, `%SaveDialog`, `%LoadDialog`,
`%ClearConfirmDialog`, `%ErrorDialog`, `%StatusBar`, `%CursorCoords`, `%SnapStatus`.
- **Phase 6 status bar:** `StatusBar` is an `HBoxContainer` bottom-anchored at 28 px
height containing `CursorCoords` (left) and `SnapStatus` (right); `MainLayout`
`offset_bottom` is `-28.0` to leave room for it.
- `scenes/body_part_panel.tscn` — instantiated 10× at runtime (5 per column). Each panel
sets `size_flags_vertical = SIZE_EXPAND_FILL` so the panels expand to fill the column
height in their parent VBoxContainer.
- `scenes/test_harness.tscn` — **standalone staging scene** (Phase 9, not wired into the
editor; run via **F6**). Backed by `scripts/test_harness.gd`. Top UI bar with file open /
quick-select, "Show Bones" / "Show IK Handles" / "Show Coords" toggles, and a
loaded-filename status label;
`SubViewport` world with an enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on
spawn); interactive limb IK via `SkeletonModificationStack2D` TwoBoneIK.
### 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] }`.
- `shape_type` values: `"line"`, `"rectangle"`, `"circle"`, `""` (empty).
Descriptive tag in Phase 2 — rendering uses `closed`.
- `closed`: `true` = filled + closed outline, `false` = open outline-only.
- `vertex_flags`: same length as `points`; `0` = original vertex (filled circle),
`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}`.
- **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
are write-only metadata recomputed on every save — never read back on load.
- **Phase 9 Round 5:** per-part data adds `guide_offset` `{x, y}` (bbox center guide joint,
preview space; pure master-space delta) for format v1.5. Write-only metadata like
`pivot`/`length`; the load path ignores it, so v1.0v1.4 files load unchanged and gain the
key on their next save.
- The JSON `.stk` format is defined in `README.md` (versioned `"1.5"`, extensible;
`"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`.
- `recent_colors: Array[String]` — the last up-to-8 selected hex colors, most-recent-first.
Populated on load and flushed on every color selection.
- `show_pose_guide: bool` — whether the pose silhouette guide is visible in the Whole
Stickman preview. Default `true`. Loaded in `_load_settings()` and flushed on every toggle
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.
- Use standard Godot `Control` nodes where applicable.
- Use `class_name` for globally referenced scripts (`BodyPartPanel`, `WholeStickmanPreview`).
- Prefer `%UniqueName` access over exported node paths in `stickman_editor.gd` / `body_part_panel.gd`.
- Keep the `.stk` JSON format backward compatible — never change the meaning of an existing key.