Files
stickman/AGENTS.md
T
ryan 48d98ce0fe Add StkRigAdapter for runtime skeleton fitting and shape mounting
- Implemented StkRigAdapter class to adapt a master rig to a loaded .stk dictionary.
- Added methods for fitting bone lengths, recalibrating IK targets, and mounting vector shapes.
- Defined constants for default proportions and bone paths.
- Included error handling for missing nodes and invalid data structures.
2026-08-18 17:12:54 -04:00

13 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.4"; auto-migrates "1.0""1.3" 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} 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 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(), and is_cursor_over_preview(global_pos).
    • 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). 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), _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 — open → Line2D, closed → Polygon2D fill + Line2D outline, width 16). 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.
  • 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.

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.
  • The JSON .stk format is defined in README.md (versioned "1.4", extensible; "1.0""1.3" 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.