# 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.