# 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.0–v1.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.0–v1.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.