Files
stickman/AGENTS.md
T
ryan 6b273c049c fix: address shape mounting issues in StkRigAdapter
- Implement anisotropic scaling for shape mounting to prevent distortion.
- Replace bounding-box midpoint anchors with joint-based anchors for correct rotation.
- Reset node transforms to ensure clean scaling and rotation before mounting shapes.
- Introduce new helper functions for computing bounding boxes, anchors, part lengths, and scales.
- Neutralize driver rotations to maintain a consistent frame of reference during shape mounting.
- Update documentation to reflect changes and provide detailed bugfix specifications.
2026-08-19 10:01:25 -04:00

223 lines
17 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.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**, extended by Phase 9).
`static func apply(stk_data, rig)` fits an instantiated `master_rig.tscn` to a loaded `.stk`
dictionary, calling four 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), `_neutralize_driver_rotations` (sets `update_rotation = false`
on the 10 `Body/*` `RemoteTransform2D` drivers so the `Body/*` nodes stay in the clean
unrotated frame the mount math assumes; position/scale pushes are retained), and
`_mount_shapes` (mounts `.stk` shapes onto the `Body/*` visual nodes — open → `Line2D`,
closed → `Polygon2D` fill + `Line2D` outline, width 16).
- **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 1 bugfix (mount math):** `_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). `_compute_anchor()` derives a
**joint-based anchor** per part family: head → bottom-center `(cx, max_y)`; torso + legs →
top-center `(cx, min_y)`; left arms → `(max_x, cy)`; right arms → `(min_x, cy)`. Scaling is
**anisotropic** via `_compute_scale()`, applied as a `Vector2`: arms scale X only
`(bone_length/part_length, 1.0)`, legs/torso scale Y only `(1.0, bone_length/part_length)`,
head unscaled `(1.0, 1.0)`, with a `part_length <= 0` guard → `1.0`. `_bone_length_for()`
maps each part to its bone length (upper/lower arm/leg, torso); `X_AXIS_PARTS` const
identifies the four arm keys. `_reset_node_transform()` resets each `Body/*` container's
scale to `(1, 1)` and rotation to `0` (position untouched, owned by the driver).
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()`): bone
lines between each `Bone2D` global origin and its parent's (color-coded left cyan / right orange /
central white + joint dots) when "Show Bones" is on; colored markers at
`IK_Targets/{Left_Hand,Right_Hand,Left_Leg,Right_Leg}` when "Show IK Handles" is on. Interactive
IK: click-drag the 4 limb `Marker2D` IK targets; the scene's `SkeletonModificationStack2D`
TwoBoneIK flexes limbs live. The harness enables the modification stack (`enabled = true`) after
each spawn.
- 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" 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.
- 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.