Files
stickman/BUGS.md
T

27 KiB
Raw Blame History

Stickman Studio Bugs

Stickman editor (Phase 5 Round 1)

Snap to grid

  1. The snap to grid menu item checkbox is still not populating with a check mark no matter how many times it is clicked.

Shape copy and paste

  1. The paste item in the context menu is only displaying if a shape is selected and the context menu is brought up. If I have an empty window OR if I don't have a shape selected in that window, 'paste' do not show up in the menu. It should show up as long as something is in the clipboard. Pasting into an empty window should be possible.

Stickman editor (Phase 5 Round 2)

Snap to grid

  1. The snap to grid menu item checkbox is still not populating with a check mark. Should we come up with another solution for this?

Object scaling after rotation

  1. When I rotate an object to a different angle than the original (0) and the try to scale the object, the object returns back to 0 rotation and then begins to scale. The scale should happen while the object is rotated, and the object keeps that rotation while being scaled.

Stickman editor (Phase 5 Round 3)

Snap to grid

  1. The snap to grid menu item checkbox is still not populating with a check mark. Let's change the implementation. Propose an alternate solution

Pasting a shape

  1. If an object has been scaled or rotated, and then the object's corresponding shape is deleted from a shape editor window - when a new shape is pasted, that shape takes on the scale, rotation, and position of the previous deleted shape or object had. When a shape is deleted, it's corrsponding object need to have it's position, rotation, and scale reset.

Stickman editor (Phase 5 Round 4)

Snap to grid when moving an object

  1. Currently snapping to the grid snaps the mouse cursor to the grid, however, that can lead to object still not lining up properly. The object's bounding box should be what snaps to the grid. If a reference point is needed for snapping, use the object's upper left point of the bounding box.

Stickman editor (Phase 6 Round 1)

Seletion bounding box as well as rotation 'dot' should stay in front of all other objects.

If an object is selected, it's bounding box and rotation dot should be in front of all other objects.

Touchpad panning is too slow

Panning using the touchpad does not moves too slowly. We need to increase the distance the panning moves through touchpad.

Zooming in/out

When zooming in, we should use the mouse cursor as the zoom in point. If the cursor is in the window should determine where the zoom should happen.

Stickman editor (Phase 7 Round 1)

Silhouette starting position (or camera)

When starting the application, the silhouette stickman should be in the center of the view. Right now it starts on the left upper side of the view so the user has to pan over. Either move the silhouette or the camera.

Showing the pose guide

When clicking View->Show Pose Guide the guide toggles on and off, but the text never changes. By default the pose is visible so the menu text should say "Hide Pose Guide" When it is hidden it should say "Show Pose Guide"

Guide visibility

The guide and joints should slightly 'ghost' in front of the objects so that the use can see how well the objects are aligned to the joints and limbs.

Stickman editor (Phase 9 Round 1)

FIXED — 2026 Round 1: implemented per docs/phase9_round1_bugfix_spec.md; anisotropic scaling, joint-based anchors, and node/driver transform neutralization; verified with a 37-assertion headless smoke test.

Stickman editor (Phase 9 Round 2)

FIXED — 2026 Round 2: implemented per docs/phase9_round2_bugfix_spec.md; restored the RemoteTransform2D driver rotations (update_rotation = true again) so mounted shapes follow their bones under IK flexing, switched the mount to the rig's hanging convention (joint anchor at the local origin, far end along +Y, per-family alignment rotation), auto-detected the drawn long axis so only that axis scales to the bone length (cross-axis thickness 1:1), zeroed the Head driver's local position so the chin lands on the neck joint, dropped DEFAULT_LINE_WIDTH to 2.0 (editor's outline width), and rewrote the harness bone overlay (true parent→child segments + leaf bones drawn to their IK targets). Verified with a 46-assertion headless smoke test.

  1. Lower-limb bones/joints invisible in "Show Bones". The harness _draw_bones() drew parent-origin → bone-origin lines, so for lower bones the segment duplicated the upper segment and wrist/ankle joints (bone ends, not origins) were never drawn. Fixed: the overlay now draws a joint dot per bone, parent→child bone segments, and leaf bones out to their IK targets.
  2. IK only moves lower-arm shapes; upper-arm shapes static. Round 1's _neutralize_driver_rotations() set update_rotation = false on the 10 RemoteTransform2D drivers, so Body/* visuals sat unrotated at fixed joints. Fixed: driver rotation neutralization removed — the drivers keep their defaults and the hanging-convention mount rotates every part to follow its bone in any pose.
  3. Torso top connects at the hip joint. With driver rotation suppressed, the torso (top-center anchor) hung with its top at the hip-driven origin. Fixed: the torso now mounts with the hip end (bottom-center anchor, θ = π) at the origin, so the top lands at the neck.
  4. Head features wrong scale / too thick / nose not triangular. DEFAULT_LINE_WIDTH = 16.0 dominated small face features, and the Head driver's local position (0.05, 72) floated the head above the neck. Fixed: line width is 2.0 (editor's 2 px outline) and the Head driver position is zeroed so the mounted head's chin sits on the neck joint.

Stickman editor (Phase 9 Round 3)

FIXED — 2026 Round 3: implemented per docs/phase9_round3_bugfix_spec.md; one node per shape (closed → single Polygon2D, open → single Line2D), the part's preview rotation/scale composed into the mount before the hanging-convention fit (anchor/θ/bone-fit scale computed on the transformed geometry; ±180° flips swap the attachment to the drawn far end so flips render), and the harness head-leaf overlay drawn along the bone's own direction instead of to the LookAt aim point. Verified with a 26-assertion headless smoke test.

  1. "The head bone has another bone extending out of it." The harness bone overlay drew the Head leaf bone as a segment to IK_Targets/Head — the SkeletonModification2DLookAt aim point 232 px above the neck — a long line sticking out of the head. Fixed: the Head leaf now draws a ~90 px segment along the bone's own direction (Vector2(length, 0) rotated by bone_angle then global_rotation), so the head bone runs neck→head-top. Limb leaf bones (LeftLowerArm, RightLowerArm, LeftLowerLeg, RightLowerLeg) still draw to their IK targets.
  2. Duplicate Polygon2D + Line2D pairs per closed shape. Closed shapes mounted as a Polygon2D fill plus a Line2D outline, producing two nodes per shape (e.g. @Polygon2D@165 + @Line2D@166). Fixed: one node per shape — closed shapes mount as a single Polygon2D, open shapes as a single Line2D (width 2).
  3. Part rotation not applied. The adapter ignored the per-part rotation (degrees) the user applied in the Whole Stickman preview — a torso rotated 180° looked identical to an unrotated one. Fixed: the mount now applies the preview's exact transform E(P) = C + R(rot)·S·(P C) (scale-then-rotate about the bbox center) before the hanging-convention mount, and the anchor/alignment θ/bone-fit scale are computed on the transformed geometry; rotations near ±180° (|wrapf(rot)| > 0.75π) additionally swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint).
  4. Part scale not applied / head too small. The adapter ignored the per-part scale, so a head scaled ~2× in the editor (≈160 px face) rendered ~80×80 in the harness. Fixed: part scale composes into E (the head mounts upright with θ = 0, s = 1 but still applies the part scale via E, so the face mounts at ≈160 px and the chin sits on the neck joint); the bone-fit scale s is measured on the transformed extent, so user-scaled parts are not double-fitted.

Fix Shape Mount Math & Point Scaling in StkRigAdapter.gd

Problem Summary: When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:Giant Polygon Explosions (Legs): Uniformly scaling points using scale_factor = bone_length / part_length on both X and Y multiplies the shape's thickness, turning thin leg segments into screen-filling blocks.Misaligned Joint Rotations (Head/Torso): Using (min + max) / 2 sets the pivot to the geometric center of the shape instead of its joint connection (e.g., neck base or hip joint).Required Fixes in StkRigAdapter.gd (or ActorFactory.gd):Please refactor _mount_shapes() / point calculation logic using the following rules:1. Anisotropic Scaling (Primary Axis Only)Do not apply scale_factor to both axes. Only scale points along the bone's primary directional axis; leave cross-axis thickness at a 1.0 scale multiplier:Arms (Primary Axis: X):$\text{point}*{\text{local}}.x = (P*x - \text{anchor}\_x) \times \left(\frac{\text{upper_arm_length}}{\text{part_length}}\right)$$\text{point}*{\text{local}}.y = P*y - \text{anchor}\_y$Legs / Torso (Primary Axis: Y):$\text{point}*{\text{local}}.x = P*x - \text{anchor}\_x$$\text{point}*{\text{local}}.y = (P*y - \text{anchor}\_y) \times \left(\frac{\text{upper_leg_length}}{\text{part_length}}\right)$Head: Keep unscaled (1.0 factor) on both axes:$\text{point}*{\text{local}} = P - \text{anchor}$(Guard against division by zero if part_length <= 0 by defaulting the scale factor to 1.0.)2. Joint-Based Anchor Alignment (Not BBox Center)Replace bounding-box midpoint anchors with joint origins so shapes rotate correctly around the bone joints:Head Anchor: Bottom-center of bounding box (x = (min_x + max_x) / 2, y = max_y).Torso & Legs Anchor: Top-center of bounding box (x = (min_x + max_x) / 2, y = min_y).Arms Anchor: Joint-end connection (x = min_x for right arms, x = max_x for left arms, y = (min_y + max_y) / 2).3. Node Transform ResetEnsure target Body/* container nodes have their local scale reset to Vector2(1, 1) and rotation = 0 so Godot's

Stickman editor (Phase 9 Round 4)

FIXED — 2026 Round 4: implemented per docs/phase9_round4_bugfix_spec.md; the mounted head geometry is dropped +28 px (HEAD_CHIN_DROP) in rig space so the chin lands 28 px below the neck, aligned with the editor silhouette guide's head circle bottom and overlapping the torso. Verified with a 15-assertion headless smoke test.

  1. Head too high in the harness. The head bone origin (neck) is at y = 391.5, and the mounted head's chin (its local origin) previously landed on that neck joint, so the head floated above the torso with no overlap. The editor's silhouette guide draws the head as a radius-100 circle centered at the Head joint (0, 463.5), so the circle's bottom is 363.5 — 28 px below the neck, overlapping the torso's top region. Fixed: the mounted head points now receive a Vector2(0.0, 28.0) rig-space translation after the part transform and the (θ = 0, s = 1) transform, so the chin lands at world ≈ (0, 363.5) — the guide circle's bottom — overlapping the torso by 28 px, matching the editor guide. The drop is a rig-space fixture position (applied after part scale/rotation), so a flipped/rotated head drops identically. node hierarchy does not multiply the geometry scaling a second time.Deliverable:Please update StkRigAdapter.gd to implement these corrected coordinate transform calculations and return the full updated GDScript.

Stickman editor (Phase 9 Round 5)

ADDED — 2026 Round 5: implemented per docs/phase9_round5_bugfix_spec.md; the harness now reproduces the editor's guide-relative part placement 1:1. The editor exports per-part guide_offset (bbox center guide joint, preview space; a pure master-space delta) as write-only metadata on save (FILE_VERSION bumped to "1.5"; the load path ignores the key). The head maps to the guide Neck joint — the head bone's rig attachment origin (0, 391.5)not the circle center, which sits 72 px above the neck. StkRigAdapter applies the offset as a node-frame translation t = (guide_offset + (A C)).rotated(c_node) only when the key is present (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, c_node = the part's driver RemoteTransform2D.global_rotation at apply time); old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 HEAD_CHIN_DROP. Note: .stk files must be re-saved from the editor to populate the offsets — only files carrying guide_offset pick up the guide-relative placement in the harness.

Stickman editor (Phase 9 Round 6)

FIXED — 2026 Round 6: implemented per docs/phase9_round6_bugfix_spec.md; the mount anchor is now the transformed end nearest the part's stored guide joint when guide_offset is present. Verified with a 32-assertion headless smoke test.

  1. Lower-left-leg mounted 180° off its bone. The leg's drawn knee was at the min_x end, but the per-side family rule anchored it at the max_x end, so the ankle attached at the joint and the shin hung backward. Root cause: the adapter picked the joint end with fixed per-side family rules (left limbs → max_x, right limbs → min_x) plus a 180° flip heuristic, but the user's drawn-side conventions are inconsistent across parts — no fixed rule can know which drawn end is the knee. Fix: when a part carries guide_offset, the anchor is whichever transformed end (E(J_raw) or E(F_pt_raw)) is nearest center guide_offset (the part's guide joint) — the knee is now at the joint. Old files without the key keep the previous family rules.
  2. Lower-right-arm mounted 180° off its bone. Same root cause mirrored on the right: the drawn elbow was at the max_x end, but the family rule anchored the arm at the min_x end, so the wrist attached at the joint and the forearm hung backward. Fix: the same nearest-end-to-guide-joint anchor selection puts the elbow at the joint. Old files without the key keep the previous rules.

Stickman editor (Phase 9 Round 7)

ADDED — 2026 Round 7: implemented per docs/phase9_round7_feature_spec.md; the test harness now exposes six draggable IK handles. IK_HANDLE_PATHS gains "Head" (IK_Targets/Head, the SkeletonModification2DLookAt aim point) and "Torso" (IK_Targets/Torso, whose child RemoteTransform2D moves the hip bone). Dragging the Torso handle moves bones only — the marker's RemoteTransform2D translates the hip bone and the whole skeleton + Body/* visuals follow rigidly, while the limb/head targets stay put (dragging the figure away from them stretches the limbs toward the stationary targets, per user decision). Dragging the Head handle drives the Head bone's LookAt rotation (clamped at the authored ~55° constraint); Body/Head follows. _handle_color() colors the head marker yellow (HANDLE_COLOR_HEAD) and the torso marker magenta (HANDLE_COLOR_TORSO); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). Verified with a 17-assertion headless test (Torso moved by (60, 40) → Skeleton2D/Torso and Body/* translate by exactly (60, 40); Head marker moved → Head bone + Body/Head rotate).

Sandbox Stage — Director Tool (Phase 3a)

FIXED — 2026-08-29: walk_to no longer stops after a few pixels. _update_walking now defers all nav reads until NavigationServer2D.map_get_iteration_id(...) != 0 (map-sync guard), forces the path query via get_next_path_position() before any empty-path/finished check, and consumes the empty-path grace (_walk_path_grace = 2) only after map sync. Off-by-default diagnostics added: DEBUG_WALK + _walk_dbg() in stickman_rig.gd, DEBUG_STAGE + _stage_dbg() in sandbox_stage.gd. Verified with a 44-assertion headless regression suite. Follow-up (2026-08-29): the initial "warn + finish in place" unreachable-target policy was itself reported as "stickman stands still with a waypoint" and was replaced by hybrid nav/direct steering — on-mesh targets follow the nav path (_walk_mode = "nav"), off-mesh/unreachable targets switch to straight-line direct steering toward the clicked waypoint (_walk_mode = "direct", root target = waypoint + FOOT_OFFSET). No push_warning on off-mesh (a supported case, logged only via _walk_dbg); _walk_path_grace removed (map-sync guard + forced path query replace it); the debug trace now includes mode=nav|direct.

  1. walk_to stops after a few pixels. A stickman directed to walk stops ~5 px into the move and then either sits still or is treated as finished, instead of walking to the target.
    • Symptom: in Play, the stickman advances one move_toward step then stops; the walk never reaches its target.
    • Root cause: _update_walking checked is_navigation_finished() before the map had synchronized. An unsynced NavigationAgent2D (map iteration id 0) reports an empty, already-finished path, so the walk was ended after a single step. Additionally, get_current_navigation_path() alone never triggers a path computation — only get_next_path_position() forces the agent's internal _update_navigation() to re-query the map for a fresh target, so the empty-path guard would misjudge an off-mesh target as "unreachable" immediately.
    • Fix: (1) defer all nav reads until map_get_iteration_id(...) != 0; (2) call get_next_path_position() before the empty-path / finished checks so the path is actually computed; (3) make walking hybrid — an on-mesh target follows the nav path (_walk_mode = "nav"), while an off-mesh/unreachable target (path empty or is_target_reachable() false) switches to direct straight-line steering toward the clicked waypoint (_walk_mode = "direct", root target = waypoint + FOOT_OFFSET), so the stickman always reaches the waypoint the user clicked; (4) no push_warning for an off-mesh waypoint (a supported case — logged only via _walk_dbg); the original "warn + finish in place" policy was itself reported as "stickman stands still with a waypoint" and was superseded by this DIRECT branch; (5) _finish_walk(reason: String) internal param for the debug trace. The _walk_path_grace counter is removed — the map-sync guard + forced path query replace it.

Sandbox Stage (Phase 3c)

FIXED — 2026-09-04: all five bugs fixed (developer stage). Bugs 1 and 4 (implementation defects — panel rows never parented into the list container) fixed by adding the row to the list container; bugs 2 and 3 (spec/design defects — unconditional entry-point items) fixed by gating popup items behind an about_to_popup refresh; bug 5 (spec/design defect — theme schema) fixed by extending sandbox_theme.json's fonts block + adding apply_font(...) to the Phase 3c panels/editors. See per-bug notes below. Spec amendments: docs/phase_3c_editor_spec.md §2 decision 8, §5.3, §11, and §14.

  1. "Edit Queue" shows no actions (implementation defect). queue_panel.gd refresh() builds each row via _make_row() into _rows but never _list.add_child(...)s it, so rows are tracked for reorder but never rendered. Fix: add _list.add_child(panel) in _make_row() (or in refresh()).
    • FIXED (2026-09-04): rows are now parented into the list container (_list.add_child(panel) in _make_row()), so the queue rows render.
  2. "Edit Queue…" should not appear when the stickman has no actions (spec/design). The Direct action popup (sandbox_stage.gd:1389) and the stickman right-click menu (:1475) add "📋 Edit Queue…" unconditionally; no gating is specified in the plan/spec. Fix: hide/disable the item when rig.get_queue().is_empty(), refreshed on about_to_popup.
    • FIXED (2026-09-04): the popup items are now gated via an about_to_popup refresh — "📋 Edit Queue…" is hidden/disabled when rig.get_queue().is_empty().
  3. "Edit Rules…" should not appear when an object has no rules (spec/design). Same unconditional add (sandbox_stage.gd:1390, :1476); no gating specified. Fix: hide/disable the item when no rule has trigger.source == rig.get_instance_id().
    • FIXED (2026-09-04): " Edit Rules…" is now gated via the same about_to_popup refresh — hidden/disabled when no rule has trigger.source == rig.get_instance_id().
  4. "Edit Rules" shows no rules (implementation defect). rule_panel.gd refresh() has the identical missing _list.add_child(panel) as bug 1. Fix: add _list.add_child(panel) in _make_row().
    • FIXED (2026-09-04): rows are now parented into the list container (_list.add_child(panel) in _make_row()), so the rule rows render.
  5. Font styles/sizes not configurable in sandbox_theme.json (spec/design). The fonts block only carries sizes + font paths; no style (bold/italic) and no keys for the Phase 3c panels/editors (which currently get no font override at all). action_popup_emoji_size is a dead key. Fix: extend the schema (per-widget {size, bold, italic} + Phase 3c sizes/style flags) and add apply_font(...) to QueuePanel/RulePanel/ActionEditor/RuleEditor; consume action_popup_emoji_size.
    • FIXED (2026-09-04): the fonts schema is extended (per-widget {size, bold, italic} object form + Phase 3c size/style flags); the Phase 3c panels/editors now accept apply_font(...); set_item_font_size is replaced with add_theme_font_size_override; and stale bold/italic are reset on theme reload.

Stickman rig (head mirror + recovery re-anchor)

FIXED — 2026-09-04: two runtime bugs in scripts/stickman_rig.gd fixed (developer stage), covered by a new 76-assertion headless regression suite tests/test_phase3c_walk_recovery.gd. See per-bug notes below. No spec changes; no .stk / scene / .tscn file touched.

  1. Head does not mirror when the rig faces LEFT (implementation defect). _apply_head_flip() reflected the mirror through the Head Pivot node's negative-determinant scale, which Godot re-decomposed into a per-frame Y-scale flip as the Head bone rotated under LookAt (the driver only forwards update_rotation = true, so the negative scale leaked into the rotation channel). Root cause: folding the reflection into a driver whose transform is applied through the rotation channel. Fix: keep the Pivot's transform identity for all profiles (scale (1,1), rotation 0) and mirror the mounted geometry directly on Body/Head.scale.x (-1 for LEFT, (1,1) for RIGHT/FORWARD); the driver's update_scale = false never clobbers this manual scale. Note: the driver may canonicalize the mirror to (1,-1) — the X-mirror semantics are unchanged.
    • FIXED (2026-09-04): _apply_head_flip() now resets the Pivot transform to identity for every profile and applies the LEFT mirror as Body/Head.scale.x = -1 (RIGHT/FORWARD → (1,1)).
  2. Figure slides back to its pre-ragdoll position when it stands up after a fall (implementation defect). The ragdoll bodies spawn under a world sibling (the rig's parent), so the rig root never moves while the figure falls; recovery captured only rig-local body poses and re-solved them against the root's original world position, so the stand-up tween dragged the standing figure back to where it was before the ragdoll instead of where it landed. Root cause: recovery never translated the rig root to the ragdoll's landing spot. Fix: _capture_ragdoll_pose() additionally records _captured_hip_world (the ragdoll torso's world-space hip — torso.global_position spine_dir·half); _start_recovery() calls a new _reanchor_root_to_landing() which translates the rig root so STAND_POSE's hip lands on that captured world hip, then re-bases the captured rig-local positions by the root shift — the figure stands up in place where the ragdoll landed.
    • FIXED (2026-09-04): _capture_ragdoll_pose() records _captured_hip_world; _start_recovery() calls _reanchor_root_to_landing() before the snap/tween.
  3. Recovery buries the standing figure when the ragdoll lands lying flat (implementation defect). The 2026-09-04 re-anchor fix anchored STAND_POSE's hip onto a spine-direction hip (torso.global_position spine_dir·half), which is only correct while the torso is upright. When the ragdoll lies flat, spine_dir is horizontal, so the derived hip sits at ground level (torso center.y + capsule radius ≈ ground), and _reanchor_root_to_landing() places the standing hip at ground level — burying the standing feet ~363 px into the ground and making the stand-up tween read as a ground-level pivot instead of a lying→standing rise. Root cause: the landing anchor assumed an upright torso. Fix: replace _captured_hip_world with a landing-center + ground-contact anchor_captured_landing_center = the ragdoll torso's world center, _captured_ground_y = torso.center.y + RAGDOLL_TORSO_RADIUS; _reanchor_root_to_landing() then sets new_root = (landing_center.x, ground_y) + FOOT_OFFSET so the standing figure's feet sit on the ground at the landing X.
    • Also per user request, STAND_UP_DURATION is bumped 0.8 s → 2.0 s so the lying→standing stand-up tween is clearly visible before returning to ANIMATED. (Docs updated: README §17, AGENTS.md Phase 11.)

Stickman rig (whole-rig Y-axis mirror — design change)

DESIGN CHANGE (proposed by user, 2026-09-05): replace the per-part/head mirroring for FacingProfile LEFT/RIGHT with mirroring the entire stickman over the Y-axis (Master.scale.x = -1 for LEFT, (1,1) otherwise), so the head AND body mirror together and face the correct direction. _apply_head_flip() and the Body/Head.scale.x mirror are removed; per-joint flip_bend_direction flags (PROFILE_FLAGS) and Z_ORDER_BY_PROFILE are kept provisionally (unchanged). Walk-clip mapping — Option A (single canonical clip): walk_right is the canonical walk; for FacingProfile.LEFT the rig root is X-mirrored and the same walk_right clip plays mirrored (walk_left becomes unused at runtime; the animation .:facing_profile tracks are neutralized/removed and facing is set explicitly by set_facing_profile()/walk_to()). See docs/phase9_task4_refactor_spec.md §9a and the AGENTS.md stickman_rig section. Follow-up (2026-09-05): under the LEFT root mirror the head was displaced/flipped — (a) the head RemoteTransform2D (Skeleton2D/Torso/Head/Pivot) set update_scale = false, a partial-channel push that re-canonicalized Body/Head.scale under the mirrored root (per-frame Y-flips/wrap-jumps), and (b) the head SkeletonModification2DLookAt is not mirror-invariant, writing a bone rotation 180° off the FORWARD aim that flips the head to hang below the neck. Fixed: the head driver now pushes the full transform like every other Body driver (update_scale no longer false), and _apply_head_lookat_mirror_mode() disables the LookAt when facing LEFT, pinning the head bone to the FORWARD canonical aim (π) with _pin_mirrored_head_rotation() re-asserting the pin each _physics_process frame while ANIMATED/RECOVERING (RIGHT/FORWARD re-enable the LookAt). Interactive head-aiming is intentionally static while facing LEFT.