- 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.
176 lines
13 KiB
Markdown
176 lines
13 KiB
Markdown
# 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.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.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()`, 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.gd` — `class_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.
|