Files
stickman/docs/phase9_task3_coords_spec.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

8.4 KiB

Phase 9 Task 3 — Feature: Coordinates Display

Overview

RIGGING.md Task 3: the test harness needs a show/hide readout on the right side of the screen displaying the current skeleton pose so bone/IK work can be inspected numerically:

  1. Skeleton2D position and rotation.
  2. Bone positions and rotations — Torso, Head, Left/RightUpperArm, Left/RightLowerArm, Left/RightUpperLeg, Left/RightLowerLeg.
  3. IK target positions — Head, Torso, Right_Hand, Left_Hand, Right_Leg, Left_Leg.

The display is easy to read on the right side of the screen and is toggled by a checkbox like the existing "Show Bones" / "Show IK Handles" toggles. It is a non-persistent debug aid: the toggle defaults ON per launch, and the panel is rebuilt from scratch each run.

1. Mechanism

The readout is a PanelContainer + RichTextLabel pair built entirely in code (_build_coords_panel()), not authored in test_harness.tscn, and added as a child of the harness root after the viewport container so it renders in front of the SubViewport. It anchors to the top-right of the window below the top UI bar (offset_top = 40.0) with a fixed width and auto-height, and the panel is MOUSE_FILTER_IGNORE so it never intercepts viewport pan/zoom/drag input — the label itself stays interactive: text is selectable and copyable (mouse drag + Ctrl+C, plus the built-in right-click copy menu).

Values are read live each frame in _process(delta), so the panel always mirrors the current pose. All values are world-space (global_position / global_rotation). Rotation is reported in degrees (rad_to_deg); positions are Vector2 formatted to 1 decimal place.

2. Implementation — scripts/test_harness.gd

2a. Constants

  • const COORDS_PANEL_WIDTH: float = 320.0 — fixed panel width (also the offset_left extent).
  • const COORD_BONE_PATHS: Array[String] — 10 Skeleton2D-relative bone paths, in display order; the display name is the last path segment (path.get_file()): Torso, Torso/Head, Torso/LeftUpperArm, Torso/LeftUpperArm/LeftLowerArm, Torso/RightUpperArm, Torso/RightUpperArm/RightLowerArm, Torso/LeftUpperLeg, Torso/LeftUpperLeg/LeftLowerLeg, Torso/RightUpperLeg, Torso/RightUpperLeg/RightLowerLeg.

2b. State

  • var _coords_panel: PanelContainer = null — the readout container.
  • var _coords_label: RichTextLabel = null — the monospace, selectable text readout.
  • var _coord_bones: Dictionary = {} — bone display name → Bone2D, resolved per spawn.
  • var _show_coords: bool = true — toggle state (default ON, harness-level, persists across spawns but not to disk).

2c. UI construction — _build_coords_panel()

Called from _build_ui() right after the viewport container. Builds:

  • _coords_panel (PanelContainer):
    • PRESET_TOP_RIGHT anchors; offset_top = 40.0, offset_right = -8.0, offset_left = -COORDS_PANEL_WIDTH.
    • grow_vertical = GROW_DIRECTION_END + grow_horizontal = GROW_DIRECTION_BEGIN (auto-height/width from label content; grows leftward so text never runs off the right edge of the screen).
    • mouse_filter = MOUSE_FILTER_IGNORE on the panel (input passes through to the viewport; the label itself keeps mouse handling for text selection).
    • StyleBoxFlat override "panel": bg Color(0,0,0,0.55), border Color(1,1,1,0.12) width 1, corner radius 4, content margin 8.
  • _coords_label (RichTextLabel): selection_enabled = true, context_menu_enabled = true, fit_content = true, autowrap_mode = OFF, scroll_active = false, focus_mode = FOCUS_CLICK; monospace via a SystemFont override on the normal_font theme item (Consolas, Menlo, DejaVu Sans Mono, Courier New fallbacks) at normal_font_size = 18.
  • _coords_panel added to the harness root (add_child(_coords_panel)).

2d. Toggle — _on_show_coords_toggled(pressed)

CheckBox "Show Coords" (top bar HBox, after "Show IK Handles", before the status label), button_pressed = true at build. Handler sets _show_coords = pressed and flips _coords_panel.visible. No persistence to disk.

2e. Update loop — _process(_delta)_update_coords_display()

_process(delta) early-outs when _show_coords is false or _coords_label is null; otherwise calls _update_coords_display() each frame. _update_coords_display():

  • If _rig is null/invalid → label text "No rig loaded", return.
  • "Skeleton2D" section: position + rotation (degrees) via _skeleton.global_position / _skeleton.global_rotation (guarded).
  • "Bones" section: iterates COORD_BONE_PATHS, looking each bone up in _coord_bones by display name; position + rotation (degrees). Missing/invalid bones skipped.
  • "IK Targets" section: iterates IK_HANDLE_PATHS and reads each handle from _ik_handles (the same 6 handle set used by the drag handles); position only. Missing/invalid handles skipped.
  • Lines joined with \n into _coords_label.text; the text is only reassigned when the built string differs from the current one, so an active text selection survives idle frames (it refreshes while bones are actively moving). Formatting helpers: _fmt_vec2(v) (%8.1f, %8.1f), _fmt_deg(rad) %7.1f° (1 decimal place).

2f. Resolution & lifecycle

  • _resolve_rig_nodes() additionally calls _resolve_coord_bones().
  • _resolve_coord_bones() clears _coord_bones, then for each COORD_BONE_PATHS entry resolves via _skeleton.get_node_or_null(NodePath(path)) as Bone2D; valid bones are keyed by path.get_file(), missing ones emit push_warning.
  • _free_current_rig() clears _coord_bones. The _show_coords toggle itself persists across respawns (it lives at harness level, not per-rig).

3. Files modified

File Changes
scripts/test_harness.gd COORDS_PANEL_WIDTH, COORD_BONE_PATHS, _coords_panel, _coords_label, _coord_bones, _show_coords, _build_coords_panel(), _on_show_coords_toggled(), _process(), _update_coords_display(), _resolve_coord_bones(), _fmt_vec2(), _fmt_deg(); "Show Coords" checkbox.
docs/phase9_task3_coords_spec.md This file.
AGENTS.md Test-harness section: "Phase 9 Task 3 coordinates display" bullet; test_harness.tscn scene description now lists the "Show Coords" toggle.
RIGGING.md Mark Task 3 implemented.

4. Edge cases

  • No rig loaded: _update_coords_display() shows "No rig loaded".
  • Missing bone nodes (foreign/partial rig): _resolve_coord_bones() warns once, and the per-frame loop skips missing/invalid bones via is_instance_valid.
  • Toggle off: _process() early-outs; panel hidden.
  • Re-spawn: _coord_bones re-resolved per spawn; _free_current_rig() clears it. The _show_coords toggle stays at its user-set value across spawns (harness-level state).
  • Input blocking: MOUSE_FILTER_IGNORE keeps the panel from intercepting viewport pan/zoom/ drag.
  • World-space values: global_position/global_rotation reflect the live pose regardless of Camera2D pan/zoom, so the readout tracks the actual rig transforms.

5. Design decisions

# Decision Justification
D1 Panel is code-built (_build_coords_panel()), not a scene node Mirrors the code-built harness top bar; keeps test_harness.tscn unchanged and the panel trivial to rebuild.
D2 MOUSE_FILTER_IGNORE on the panel The readout must never block viewport interaction (pan/zoom/IK dragging).
D3 World-space values + degrees Matches what the debug overlay and drag handles use; degrees are the convention in the harness and the editor.
D4 IK targets report position only IK targets carry no meaningful rotation for this readout (they are aim/IK markers).
D5 Toggle non-persistent, default ON Debug aid only; no settings.json involvement, consistent with the other harness toggles.

6. Test plan

  1. Parse check: ..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit (user-requested; no errors).
  2. Manual F6 check:
    • Launch harness; readout visible on the top-right with "No rig loaded".
    • Load stickmen/basic.stk; Skeleton2D, 10 Bones, and 6 IK Target rows populate with numeric values.
    • Drag an IK handle → position/rotation values update live each frame.
    • Uncheck "Show Coords" → panel hides; recheck → reappears with current values.
    • Confirm the panel does not block middle-mouse pan / wheel zoom / IK drags beneath it.