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 body 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 simulated body 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 for a direct write to THIS node (_from_parent False), and for an ANCESTOR’s move only where the body follows it (:meth:_body_follows_parent_transform, true for a STATIC body). Neither fires during the engine’s own handle->node write-backs (_applying_physics_pose).

  • A simulated body deliberately does NOT follow its ancestors: dragging a character through its handle every time a parent moves would override move_and_slide, and a parent move erased by the next simulation write-back is worse than one that never happened. A STATIC body has no such authority to protect – nothing simulates it, and its pose is authored data – so it follows. Note this is a deliberate difference from engines that push the composed transform for every body type.

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

    meth:

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

Subclass contract: implement :meth:_push_node_pose_to_body 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). In practice that is implemented ONCE per dimension, on PhysicsObject2D / PhysicsObject3D: every physics node owns a body, so every one of them writes through set_body_transform. Subclasses that create their handle in __init__ order must set their world / handle fields BEFORE super().__init__ so the hook, which can fire on a position= kwarg during base init, reads valid state.