simvx.core.physics._pose_reconcile

Node-transform -> physics-handle pose reconciliation.

A physics node (PhysicsBody2D/PhysicsBody3D, CharacterBody2D/ CharacterBody3D) owns BOTH a Node transform and a seam handle in a physics world. The scene tree syncs handle -> node every step, and render interpolation does it every frame. Without the reverse direction, a direct node.position = x would move only the visual node, and the next step / move_and_slide would snap it back to the stale handle pose (the body teleports to the world corner). That made attribute assignment – the obvious way to place a node – silently wrong.

This mixin closes the loop: assigning the node’s OWN transform eagerly teleports the seam handle to match. So node.position = spawn is the single, canonical way to place a physics node (there is deliberately no separate teleport() method: it would be a pure behavioural alias). Because the push is eager, every later read – the step, move_and_slide, and raycast/overlap queries – sees the assigned pose with no further bookkeeping.

Mechanics:

  • _invalidate_transform is the single transform choke point (local position, the world_position setter, in-place Vec mutation, and rotation’s on_change). The override pushes the node world pose into the handle, but ONLY for a direct write to THIS node (_from_parent False) and NOT during the engine’s own handle->node write-backs (_applying_physics_pose). Skipping _from_parent means a character parented under a moving node is not dragged through its handle (which would override move_and_slide).

  • The engine’s handle->node write-backs (bulk scatter, render interpolation, move_and_slide / move_and_collide) MUST route through

    meth:

    _write_pose_from_seam, which sets the guard so they never bounce the simulated / interpolated pose back into the handle (that would, depending on backend, zero velocity, wake sleepers, or corrupt interpolation).

Subclass contract: implement :meth:_push_node_pose_to_seam to write (world_position, world_rotation) into the handle, a no-op when inert (no handle yet, e.g. before on_enter_tree or with no collider). Subclasses that create their handle in __init__ order must set their seam fields BEFORE super().__init__ so the hook, which can fire on a position= kwarg during base init, reads valid state.