27 KiB
Stickman Studio Bugs
Stickman editor (Phase 5 Round 1)
Snap to grid
- 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
- 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
- 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
- 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
- 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
- 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
- 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
RemoteTransform2Ddriver rotations (update_rotation = trueagain) 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, droppedDEFAULT_LINE_WIDTHto 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.
- 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. - IK only moves lower-arm shapes; upper-arm shapes static. Round 1's
_neutralize_driver_rotations()setupdate_rotation = falseon the 10RemoteTransform2Ddrivers, soBody/*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. - 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.
- Head features wrong scale / too thick / nose not triangular.
DEFAULT_LINE_WIDTH = 16.0dominated 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.
- "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— theSkeletonModification2DLookAtaim 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 bybone_anglethenglobal_rotation), so the head bone runs neck→head-top. Limb leaf bones (LeftLowerArm, RightLowerArm, LeftLowerLeg, RightLowerLeg) still draw to their IK targets. - Duplicate Polygon2D + Line2D pairs per closed shape. Closed shapes mounted as a
Polygon2Dfill plus aLine2Doutline, producing two nodes per shape (e.g.@Polygon2D@165+@Line2D@166). Fixed: one node per shape — closed shapes mount as a singlePolygon2D, open shapes as a singleLine2D(width 2). - 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 transformE(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). - 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 intoE(the head mounts upright with θ = 0, s = 1 but still applies the part scale viaE, so the face mounts at ≈160 px and the chin sits on the neck joint); the bone-fit scalesis 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.
- 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 aVector2(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_VERSIONbumped 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.StkRigAdapterapplies the offset as a node-frame translationt = (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 driverRemoteTransform2D.global_rotationat apply time); old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4HEAD_CHIN_DROP. Note:.stkfiles must be re-saved from the editor to populate the offsets — only files carryingguide_offsetpick 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_offsetis present. Verified with a 32-assertion headless smoke test.
- Lower-left-leg mounted 180° off its bone. The leg's drawn knee was at the
min_xend, but the per-side family rule anchored it at themax_xend, 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 carriesguide_offset, the anchor is whichever transformed end (E(J_raw)orE(F_pt_raw)) is nearestcenter − guide_offset(the part's guide joint) — the knee is now at the joint. Old files without the key keep the previous family rules. - Lower-right-arm mounted 180° off its bone. Same root cause mirrored on the right: the drawn elbow was at the
max_xend, but the family rule anchored the arm at themin_xend, 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_PATHSgains"Head"(IK_Targets/Head, theSkeletonModification2DLookAtaim point) and"Torso"(IK_Targets/Torso, whose childRemoteTransform2Dmoves the hip bone). Dragging the Torso handle moves bones only — the marker'sRemoteTransform2Dtranslates 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/Headfollows._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/TorsoandBody/*translate by exactly (60, −40); Head marker moved → Head bone +Body/Headrotate).
Sandbox Stage — Director Tool (Phase 3a)
FIXED — 2026-08-29:
walk_tono longer stops after a few pixels._update_walkingnow defers all nav reads untilNavigationServer2D.map_get_iteration_id(...) != 0(map-sync guard), forces the path query viaget_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()instickman_rig.gd,DEBUG_STAGE+_stage_dbg()insandbox_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). Nopush_warningon off-mesh (a supported case, logged only via_walk_dbg);_walk_path_graceremoved (map-sync guard + forced path query replace it); the debug trace now includesmode=nav|direct.
walk_tostops 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_towardstep then stops; the walk never reaches its target. - Root cause:
_update_walkingcheckedis_navigation_finished()before the map had synchronized. An unsyncedNavigationAgent2D(map iteration id0) 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 — onlyget_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) callget_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 oris_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) nopush_warningfor 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_gracecounter is removed — the map-sync guard + forced path query replace it.
- Symptom: in Play, the stickman advances one
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_popuprefresh; bug 5 (spec/design defect — theme schema) fixed by extendingsandbox_theme.json'sfontsblock + addingapply_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.
- "Edit Queue" shows no actions (implementation defect).
queue_panel.gdrefresh()builds each row via_make_row()into_rowsbut 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 inrefresh()).- FIXED (2026-09-04): rows are now parented into the list container (
_list.add_child(panel)in_make_row()), so the queue rows render.
- FIXED (2026-09-04): rows are now parented into the list container (
- "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 whenrig.get_queue().is_empty(), refreshed onabout_to_popup.- FIXED (2026-09-04): the popup items are now gated via an
about_to_popuprefresh — "📋 Edit Queue…" is hidden/disabled whenrig.get_queue().is_empty().
- FIXED (2026-09-04): the popup items are now gated via an
- "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 hastrigger.source == rig.get_instance_id().- FIXED (2026-09-04): "⚡ Edit Rules…" is now gated via the same
about_to_popuprefresh — hidden/disabled when no rule hastrigger.source == rig.get_instance_id().
- FIXED (2026-09-04): "⚡ Edit Rules…" is now gated via the same
- "Edit Rules" shows no rules (implementation defect).
rule_panel.gdrefresh()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.
- FIXED (2026-09-04): rows are now parented into the list container (
- Font styles/sizes not configurable in
sandbox_theme.json(spec/design). Thefontsblock 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_sizeis a dead key. Fix: extend the schema (per-widget{size, bold, italic}+ Phase 3c sizes/style flags) and addapply_font(...)toQueuePanel/RulePanel/ActionEditor/RuleEditor; consumeaction_popup_emoji_size.- FIXED (2026-09-04): the
fontsschema is extended (per-widget{size, bold, italic}object form + Phase 3c size/style flags); the Phase 3c panels/editors now acceptapply_font(...);set_item_font_sizeis replaced withadd_theme_font_size_override; and stale bold/italic are reset on theme reload.
- FIXED (2026-09-04): the
Stickman rig (head mirror + recovery re-anchor)
FIXED — 2026-09-04: two runtime bugs in
scripts/stickman_rig.gdfixed (developer stage), covered by a new 76-assertion headless regression suitetests/test_phase3c_walk_recovery.gd. See per-bug notes below. No spec changes; no.stk/ scene /.tscnfile touched.
- 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 underLookAt(the driver only forwardsupdate_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), rotation0) and mirror the mounted geometry directly onBody/Head.scale.x(-1for LEFT,(1,1)for RIGHT/FORWARD); the driver'supdate_scale = falsenever 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 asBody/Head.scale.x = -1(RIGHT/FORWARD →(1,1)).
- FIXED (2026-09-04):
- 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 soSTAND_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.
- FIXED (2026-09-04):
- 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_diris 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_worldwith 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 setsnew_root = (landing_center.x, ground_y) + FOOT_OFFSETso the standing figure's feet sit on the ground at the landing X.- Also per user request,
STAND_UP_DURATIONis bumped 0.8 s → 2.0 s so the lying→standing stand-up tween is clearly visible before returning toANIMATED. (Docs updated: README §17, AGENTS.md Phase 11.)
- Also per user request,
Stickman rig (whole-rig Y-axis mirror — design change)
DESIGN CHANGE (proposed by user, 2026-09-05): replace the per-part/head mirroring for
FacingProfileLEFT/RIGHT with mirroring the entire stickman over the Y-axis (Master.scale.x = -1for LEFT,(1,1)otherwise), so the head AND body mirror together and face the correct direction._apply_head_flip()and theBody/Head.scale.xmirror are removed; per-jointflip_bend_directionflags (PROFILE_FLAGS) andZ_ORDER_BY_PROFILEare kept provisionally (unchanged). Walk-clip mapping — Option A (single canonical clip):walk_rightis the canonical walk; forFacingProfile.LEFTthe rig root is X-mirrored and the samewalk_rightclip plays mirrored (walk_leftbecomes unused at runtime; the animation.:facing_profiletracks are neutralized/removed and facing is set explicitly byset_facing_profile()/walk_to()). Seedocs/phase9_task4_refactor_spec.md§9a and theAGENTS.mdstickman_rig section. Follow-up (2026-09-05): under the LEFT root mirror the head was displaced/flipped — (a) the headRemoteTransform2D(Skeleton2D/Torso/Head/Pivot) setupdate_scale = false, a partial-channel push that re-canonicalizedBody/Head.scaleunder the mirrored root (per-frame Y-flips/wrap-jumps), and (b) the headSkeletonModification2DLookAtis 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 otherBodydriver (update_scaleno longerfalse), 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_processframe while ANIMATED/RECOVERING (RIGHT/FORWARD re-enable the LookAt). Interactive head-aiming is intentionally static while facing LEFT.