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

32 KiB
Raw Blame History

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.gdextends 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.gdclass_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.gdclass_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.gdclass_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.00011.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.gdclass_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) -> Node2Dload_stk() then spawn_from_data(); returns null on empty data.
  • scripts/test_harness.gdstandalone 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: SubViewportContainerSubViewport → 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 Bone2Ds 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.tscnstandalone 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.