Implement rig animation controls in the test harness
- Added a new animation specification document for Phase 9 Task 5 detailing the requirements for rig animation controls. - Introduced a new `StickmanRig` script to manage the facing direction and joint bending for the rig. - Implemented UI elements in the test harness for selecting animations, controlling playback (play/pause/resume/stop), and toggling loop mode. - Enhanced the `test_harness.gd` script to handle animation playback state and UI interactions. - Updated documentation in `AGENTS.md`, `README.md`, and `RIGGING.md` to reflect the new animation features.
This commit is contained in:
@@ -213,14 +213,38 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`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_rig.gd` — `class_name StickmanRig`, `extends Node2D`; the **runtime owner of
|
||||
facing direction, per-joint bone bend, and `Body/*` z-order** for `master_rig.tscn` (Phase 9
|
||||
Task 4). Attached to the `Master` root node of `master_rig.tscn`. **Non-`@tool`** — node
|
||||
resolution, flag writes, and z-order reordering run only at runtime (`_ready` + setters on a
|
||||
live instance). Enums `FacingProfile { LEFT, RIGHT, FORWARD }` (values are the harness facing-menu
|
||||
ids) and `BendDirection { NORMAL, INVERTED }`. Constants (moved from the harness): `SKELETON_PATH`,
|
||||
`BODY_CONTAINER_PATH`, `BEND_JOINTS` (`["LeftArm","RightArm","LeftLeg","RightLeg"]`),
|
||||
`BEND_JOINT_BONE_PATHS` (each joint → its lower `Bone2D` NodePath relative to `Skeleton2D`),
|
||||
`PROFILE_FLAGS` (per-profile `flip_bend_direction` sets), `Z_ORDER_BY_PROFILE` (per-profile
|
||||
`Body/*` draw-order tables, back-to-front). Exports: `facing_profile: FacingProfile` (default
|
||||
`FORWARD`, a preset whose setter writes the four per-joint vars + reorders `Body/*`) and an
|
||||
`@export_group("Bend Direction")` of four `@export_enum("Normal","Inverted")` vars
|
||||
`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend` (defaults
|
||||
NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Signals `facing_profile_changed(profile)` /
|
||||
`bend_flag_changed(joint, flipped)`. Public API: `set_facing_profile`/`get_facing_profile`,
|
||||
`set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, and
|
||||
`get_bend_joint_global_position(joint)` (unknown joint → `push_warning` + no-op/`false`/
|
||||
`Vector2.ZERO`). `_ready()` resolves `Skeleton2D`/`Body`/4 lower `Bone2D`s/4 TwoBoneIK
|
||||
modifications (matched by `joint_two_bone2d_node` NodePath, **never stack index**), enables the
|
||||
modification stack (`stack.enabled = true`), applies the current profile once, then sets
|
||||
`_nodes_ready`; a `_nodes_ready` guard makes pre-`_ready` setters store-only (robust against
|
||||
setter timing during `PackedScene.instantiate()`). Null-guards + `push_warning` prefixed
|
||||
`"StickmanRig: "` throughout; never crashes.
|
||||
- `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()`;
|
||||
- `static func spawn_from_data(stk_data: Dictionary) -> StickmanRig` — instantiates
|
||||
`res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root
|
||||
(typed as `StickmanRig` since the rig now carries the `StickmanRig` root script).
|
||||
- `static func spawn(path: String) -> StickmanRig` — `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
|
||||
@@ -241,8 +265,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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.
|
||||
flexes limbs live (the rig self-enables its modification stack in `_ready()`).
|
||||
- **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).
|
||||
@@ -258,46 +281,41 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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:
|
||||
Bend Direction" flags. The facing profile, the per-joint bend flags, and the `Body/*` z-order
|
||||
tables now **live in the `StickmanRig` script** (Phase 9 Task 4) — the harness drives the rig
|
||||
via `_rig_script: StickmanRig` (typed root from `StickmanFactory.spawn()`; signals connected
|
||||
**before** `add_child`) and keeps `_facing_profile` (default `FORWARD`) only as a **UI mirror**
|
||||
for the `[√] ` menu prefix + respawn re-application; the rig's exported `facing_profile` is the
|
||||
authority. Menu item ids are `StickmanRig.FacingProfile.LEFT/RIGHT/FORWARD` (values used
|
||||
directly as menu item ids). State `_context_joint`, `_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); selection calls `_rig_script.set_facing_profile(id)`. Hit-test
|
||||
for the right-click toggle iterates `_rig_script.get_bend_joints()` with positions from
|
||||
`_rig_script.get_bend_joint_global_position(joint)`. 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 the rig's `get_joint_bend_flipped(joint)` is true) or
|
||||
**"Invert Bend"** (when false); selecting calls
|
||||
`_rig_script.set_joint_bend_flipped(_context_joint, not _rig_script.get_joint_bend_flipped(_context_joint))`.
|
||||
Only the 4 elbows/knees are right-click targets — shoulders/hips/wrists/ankles/head/torso are
|
||||
not. Lifecycle: `_free_current_rig()` clears `_rig_script` (and `_context_joint`);
|
||||
`_load_and_spawn()` captures the remembered profile before `add_child` (so the rig's `_ready()`
|
||||
`facing_profile_changed(FORWARD)` doesn't clobber the mirror) then re-applies
|
||||
`_rig_script.set_facing_profile(remembered_profile)` after `_resolve_rig_nodes()`. The rig
|
||||
enables its own modification stack in `_ready()`. No persistence to disk.
|
||||
- **Phase 9 Task 2 body-part z-order:** the rig's `_apply_profile()` reorders the rig's `Body/*`
|
||||
visual part nodes (tree order = draw order) from the Facing profile; `Z_ORDER_BY_PROFILE` (now
|
||||
owned by `StickmanRig`) maps each `FacingProfile` to the `Body/*` 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**. The rig's `_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
|
||||
@@ -328,6 +346,25 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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.
|
||||
- **Phase 9 Task 5 rig animation:** adds top-bar controls immediately after the "Facing" menu
|
||||
— an `_anim_dropdown` `OptionButton` populated per spawn from
|
||||
`AnimationPlayer.get_animation_list()` (preferring `walk_right` via `DEFAULT_ANIMATION`), a
|
||||
`_play_button` whose label swaps "Play"/"Pause"/"Resume" by `_playback_state`, a `_stop_button`
|
||||
(Stop), and a `_loop_check` `CheckBox` default ON (harness-level, persists across respawns like
|
||||
`_show_coords`). The harness resolves the rig's `AnimationPlayer` directly by node path via the
|
||||
`ANIMATION_PLAYER_PATH` const (`_resolve_anim_player()`, called at the end of
|
||||
`_resolve_rig_nodes()`) and drives it directly; the `AnimationTree` node remains an untouched
|
||||
unconfigured placeholder (out of scope, D1). Loop is implemented by writing
|
||||
`Animation.loop_mode` (`LOOP_LINEAR`/`LOOP_NONE`) on the selected animation before each play
|
||||
(`_apply_loop_mode()`); playback state is tracked by the enum
|
||||
`PlaybackState {STOPPED, PLAYING, PAUSED}` via the button handlers + the `animation_finished`
|
||||
signal (guarded by `_loop`) — no polling in `_process`. Changing the dropdown selection stops
|
||||
playback; `_free_current_rig()` clears `_anim_player`, the dropdown, `_selected_animation`,
|
||||
and state. Playing `walk_right` also sets the rig's `facing_profile` via its animation track →
|
||||
export setter → the existing `_on_facing_profile_changed` handling (menu `[√] ` + redraw). No
|
||||
persistence to disk. The "Facing" menu and all animation controls are **hidden until an .stk is
|
||||
loaded** (`_set_rig_controls_visible(false)` at the end of `_build_ui()` and in
|
||||
`_free_current_rig()`; shown on successful spawn in `_load_and_spawn()`).
|
||||
- Scenes:
|
||||
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
||||
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
||||
|
||||
Reference in New Issue
Block a user