Source code for simvx.core.physics.world

"""PhysicsWorld: the backend transport seam (Stage 1 additive scaffolding).

Role
----
This module defines ``PhysicsWorld``, the abstract interface every physics
backend (``BuiltinPhysics`` now, ``JoltPhysics`` later) implements. It is a
**transport** abstraction, not a semantics one: all backends run the same kind
of rigid-body simulation, so the contract is about *moving body state across
the Python<->native boundary efficiently*, not about defining solver behaviour.

The load-bearing part of the contract is the **bulk-array transfer**: per-frame
body state is exchanged as a single numpy buffer (one transfer per world per
frame), in a fixed body->row order, by filling a caller-preallocated array
**in place**. Per-body crossings are reserved for setup/teardown and are never
used on the hot path. See :meth:`PhysicsWorld.register_bodies`,
:meth:`PhysicsWorld.read_transforms`, and :meth:`PhysicsWorld.read_velocities`.

Stage 1 status
--------------
This is **additive, non-breaking scaffolding**. It defines the interface only
(full signatures, docstrings, ``@abstractmethod``); there is no implementation
here, and nothing in this module is wired into ``SceneTree`` or the old
``simvx.core.physics`` system. The seam is testable standalone by constructing
a concrete backend and calling :meth:`step` manually.
"""

from __future__ import annotations

import math
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any

import numpy as np

from ..math import Quat, Vec3
from .capability import Capability
from .material import CombineMode

# Opaque handle aliases. Backends choose the concrete representation; callers
# treat these as opaque tokens and never inspect them.
BodyHandle = int
ShapeHandle = Any
# A character controller token. DISTINCT type from BodyHandle so a backend can
# back it with a Jolt CharacterVirtual (not a rigid Body); callers treat it as
# opaque and never inspect it. Character handles live in their own namespace and
# never appear in register_bodies / read_transforms.
CharacterHandle = Any
# A joint / constraint token (Stage T1c). DISTINCT namespace from BodyHandle /
# CharacterHandle so a backend can back it with a Jolt Constraint (not a Body);
# callers treat it as opaque and never inspect it. Joints are node-agnostic and
# keyed only by the two BodyHandles they constrain.
JointHandle = Any

# Default world up (Y-up). A module-level singleton so it is not constructed in
# argument defaults (Vec3 is a mutable ndarray subclass); callers never mutate it.
_DEFAULT_UP = Vec3(0.0, 1.0, 0.0)


[docs] class BodyMode(Enum): """Motion mode of a body, mirroring every serious solver's seam. - ``STATIC``: immovable collider. Never integrated; infinite mass. - ``DYNAMIC``: force-simulated; responds to gravity, impulses, contacts. - ``KINEMATIC``: code-moved; pushes dynamic bodies, immune to forces. """ STATIC = "static" DYNAMIC = "dynamic" KINEMATIC = "kinematic"
[docs] @dataclass(slots=True, frozen=True) class RaycastHit: """Result of a successful raycast against the world. Attributes: body: Handle of the body the ray hit. point: World-space contact point (``Vec3``). normal: World-space surface normal at the hit (``Vec3``, unit length). distance: Distance from the ray origin to ``point`` along the ray. """ body: BodyHandle point: Vec3 normal: Vec3 distance: float
[docs] @dataclass(slots=True, frozen=True) class Contact: """Result of a kinematic / character shape-sweep stopping against a body. Mirrors :class:`RaycastHit` exactly (a single "other" body, like a query result): the swept body is implicit (the caller). Maps cleanly onto Jolt (``body`` <- hit ``BodyID``, ``normal`` <- contact normal, ``distance`` <- ``fraction * |motion|``). Attributes: body: Handle of the OTHER body that was hit. point: World-space contact point (``Vec3``). normal: World-space surface normal (``Vec3``, unit), pointing AWAY from the other body toward the moving body, i.e. the direction that separates the mover. distance: Distance the mover actually travelled along ``motion`` before contact (TOI distance, ``0..|motion|``). """ body: BodyHandle point: Vec3 normal: Vec3 distance: float
[docs] class ContactPhase(Enum): """Edge phase of a body-pair contact, diffed by the broadphase each step. Only the two transitions are reported (no per-frame "stay"): a pair fires ``ENTER`` the step it begins overlapping and ``EXIT`` the step it stops. """ ENTER = "enter" EXIT = "exit"
[docs] @dataclass(slots=True, frozen=True) class ContactEvent: """A node-agnostic body-pair collision event emitted by the seam. Keyed by body HANDLES only: the seam never names a node. The tree maps ``a`` / ``b`` back to nodes and fires the node-level ``collided`` / ``separated`` Signals. Both static-dynamic and dynamic-dynamic pairs are reported, and both bodies are notified (the tree reorients per side). Orientation convention is fixed here as ``a -> b`` (mirroring the internal narrow-phase ``_Contact.normal``); the tree negates ``normal`` / ``rel_velocity`` for the ``b`` side so each body sees the separating direction pointing toward itself. Attributes: a: Handle of the first body of the pair (canonical order). b: Handle of the second body of the pair. phase: :class:`ContactPhase` (``ENTER`` / ``EXIT``). point: World contact point (``Vec3``). Meaningful on ``ENTER``; degenerate (``Vec3(0)``) on ``EXIT`` (no live manifold). normal: Unit contact normal oriented ``a -> b`` (``Vec3``). Degenerate (``Vec3(0)``) on ``EXIT``. impulse: Normal impulse magnitude applied to the pair this step. ``0`` on ``EXIT`` and ``0`` on an ``ENTER`` where the solver applied no impulse (e.g. separating velocity). rel_velocity: Pre-solve velocity of ``b`` w.r.t. ``a`` at the contact (``Vec3``). Degenerate (``Vec3(0)``) on ``EXIT``. """ a: BodyHandle b: BodyHandle phase: ContactPhase point: Vec3 normal: Vec3 impulse: float rel_velocity: Vec3
[docs] @dataclass(slots=True, frozen=True) class OverlapEvent: """A node-agnostic sensor-overlap event emitted by the seam. A SECOND, independent edge-diffed stream, parallel to :class:`ContactEvent` but never mixed with it: a sensor pair produces NO collision response and NO manifold, so there is no point / normal / impulse / rel_velocity to carry. Keyed by body HANDLES only (node-agnostic, like :class:`ContactEvent`), but DIRECTED ``sensor -> other`` rather than a canonical unordered pair: the detection is one-directional (the observing sensor decides via its mask), so a sensor-vs-sensor overlap can fire on one side without the other. The tree maps both handles to nodes and routes ``body_entered`` vs ``area_entered`` by the OTHER node's type. Attributes: sensor: Handle of the detecting sensor body (the observer). other: Handle of the detected body (a normal body OR another sensor). phase: :class:`ContactPhase` (``ENTER`` / ``EXIT``); reused, no second phase enum. """ sensor: BodyHandle other: BodyHandle phase: ContactPhase
[docs] @dataclass(slots=True, frozen=True) class CharacterMoveResult: """Returned struct of a character collide-and-slide move. Returning a struct (rather than mutating caller state) keeps the seam node-agnostic and gives Jolt a clean place to surface ``CharacterVirtual::GetGroundState()`` / ``GetGroundNormal()``. Floor/wall/ceiling are computed seam-side from contact normals vs ``up`` (and the character's ``slope_limit``) so every backend exposes identical semantics. Attributes: velocity: Post-slide velocity (deflected along contact normals); the caller writes this back as its new velocity. on_floor: True if a contact this move was classified as floor. on_wall: True if a contact this move was classified as wall. on_ceiling: True if a contact this move was classified as ceiling. floor_normal: Normal of the floor contact this move (unit), or ``+up`` if there was no floor contact. """ velocity: Vec3 on_floor: bool on_wall: bool on_ceiling: bool floor_normal: Vec3
[docs] class PhysicsWorld(ABC): """Abstract backend seam: one isolated simulation world. A ``PhysicsWorld`` owns a set of bodies, advances them as a unit at a fixed timestep via :meth:`step`, and exchanges per-frame state in bulk. Concrete backends (``BuiltinPhysics``, later ``JoltPhysics``) implement every method. Bulk-array contract (the keystone) ---------------------------------- 1. Call :meth:`register_bodies` once (or whenever membership changes) to fix the body->row order used by the bulk readers. 2. Each frame, after :meth:`step`, call :meth:`read_transforms` and/or :meth:`read_velocities`, passing a caller-preallocated, C-contiguous ``float32`` numpy array of the documented shape. The backend fills it **in place**; it must not allocate or return a new array on the hot path. The array shapes/dtypes/contiguity are part of the contract and MUST be asserted by subclasses (see ``_check_transforms_out`` / ``_check_velocities_out``). """ def __init__(self, *, gravity: Vec3) -> None: """Initialise the world. Args: gravity: World gravity acceleration vector (``Vec3``), metres/s^2. """ self._gravity: Vec3 = Vec3(*gravity) # -- configuration ------------------------------------------------------ @property def gravity(self) -> Vec3: """World gravity acceleration vector (``Vec3``), metres/s^2.""" return self._gravity
[docs] @gravity.setter def gravity(self, value: Vec3) -> None: self._gravity = Vec3(*value)
# -- capability gate ----------------------------------------------------
[docs] def capabilities(self) -> frozenset[Capability]: """Return the set of Tier-3 :class:`Capability` features this backend honours. The strict Tier-1 rigid-body surface is the parity contract every backend implements identically; this method is the ONE place backends advertise the few features that genuinely cannot be faked (cross-platform determinism, vehicles, soft bodies). A Tier-3 node checks ``Capability.X in world.capabilities()`` and degrades / refuses if absent. The default (this base implementation) is the empty set: a backend advertises nothing it cannot honour. Backends override to list only the capabilities they actually support. Not abstract: the empty default is the honest answer for any plain rigid-body backend. """ return frozenset()
# -- shapes (opaque handles) -------------------------------------------
[docs] @abstractmethod def create_sphere(self, radius: float) -> ShapeHandle: """Create a sphere collision shape and return an opaque handle. Args: radius: Sphere radius, world units (> 0). Returns: An opaque shape handle for use with :meth:`create_body`. """
[docs] @abstractmethod def create_box(self, half_extents: Vec3) -> ShapeHandle: """Create an axis-aligned box collision shape (centred at the origin). Args: half_extents: Half-sizes along x/y/z (``Vec3``, all > 0). Returns: An opaque shape handle for use with :meth:`create_body`. """
[docs] @abstractmethod def create_capsule(self, radius: float, height: float) -> ShapeHandle: """Create a Y-axis capsule collision shape and return an opaque handle. Args: radius: Capsule radius, world units (> 0). height: Total extent along Y including the two hemispherical caps (> 0). The central segment half-length is ``max(0, height / 2 - radius)``; when ``height <= 2 * radius`` the segment collapses to a point and the capsule behaves as a sphere of ``radius``. Returns: An opaque shape handle for use with :meth:`create_body`. """
[docs] @abstractmethod def create_cylinder(self, radius: float, height: float) -> ShapeHandle: """Create a Y-axis cylinder collision shape and return an opaque handle. Args: radius: Cylinder radius, world units (> 0). height: Total extent along Y with flat caps at ``+-height / 2`` (> 0). Returns: An opaque shape handle for use with :meth:`create_body`. """
[docs] @abstractmethod def create_convex_hull(self, points: np.ndarray) -> ShapeHandle: """Create a convex-hull collision shape from a point cloud. Args: points: ``(N, 3)`` float32 array of >= 4 finite points. The backend computes its own internal hull representation from the cloud. Orientation is supported via the body transform like other shapes, EXCEPT the basic ``BuiltinPhysics`` backend, which IGNORES hull rotation (the cloud is treated in world axes offset by the body position); the Jolt backend rotates properly. Returns: An opaque shape handle for use with :meth:`create_body`. The basic backend's penetration depth/normal for a hull is an EPA-lite approximation (GJK overlap is exact); see ``builtin/world.py``. """
[docs] @abstractmethod def create_mesh(self, vertices: np.ndarray, indices: np.ndarray) -> ShapeHandle: """Create a STATIC triangle-mesh collision shape (level geometry). Args: vertices: ``(N, 3)`` float32 vertex positions. indices: ``(3 * T,)`` int64 flat triangle-list indices (three per triangle), each in ``[0, N)``. Returns: An opaque shape handle for use with :meth:`create_body`. A mesh shape is a **STATIC-ONLY** collider: placing it on a non-STATIC body is an error (rejected at :meth:`create_body` and :meth:`set_body_mode`). It carries no inertia / mass and cannot be used as a moving query shape (:meth:`shapecast` / :meth:`overlap` reject a mesh probe). """
# -- bodies -------------------------------------------------------------
[docs] @abstractmethod def create_body( self, shape: ShapeHandle, body_type: BodyMode, transform: Any, *, mass: float = 1.0, collision_layer: int = 1, collision_mask: int = 0xFFFFFFFF, is_sensor: bool = False, friction: float = 0.5, restitution: float = 0.0, friction_combine: CombineMode = CombineMode.AVERAGE, restitution_combine: CombineMode = CombineMode.AVERAGE, continuous: bool = False, ) -> BodyHandle: """Create a body in the world and return its handle. Args: shape: An opaque shape handle from :meth:`create_sphere`, :meth:`create_box`, :meth:`create_capsule`, :meth:`create_cylinder`, :meth:`create_convex_hull`, or :meth:`create_mesh`. A mesh shape on a non-STATIC body is an error (mesh colliders are STATIC-only), rejected here and in :meth:`set_body_mode`. body_type: One of :class:`BodyMode`. transform: Initial world transform (a ``Transform3D`` or anything a backend accepts as an initial pose; position + orientation). mass: Body mass in kg, used only for ``DYNAMIC`` bodies. Ignored for ``STATIC`` / ``KINEMATIC`` (treated as infinite). collision_layer: 32-bit layer membership of this body (which layers it lives on). Stored verbatim; defaults to layer 1. collision_mask: 32-bit mask of layers this body scans for collisions. Defaults to all (``0xFFFFFFFF``) so the bare API collides every pair (non-breaking). A pair (a, b) collides iff ``(a.mask & b.layer) or (b.mask & a.layer)`` (bidirectional OR). is_sensor: When True, this body is a SENSOR (trigger). It is created / destroyed / teleported exactly like a normal body and participates in the broadphase, but is EXCLUDED from collision resolution (it skips the solver, applies no impulse, and never appears in the contact-event stream) and instead generates a SEPARATE overlap-event stream (see :meth:`drain_overlap_events`) using the ONE-DIRECTIONAL filter ``sensor.mask & other.layer`` (the observer decides; the other body's mask is irrelevant), never the AND body-body rule. A sensor is an ordinary body with a flag, not a separate kind of handle. friction: Coulomb friction coefficient ``mu`` (``>= 0``). The contact solver clamps the tangential impulse by ``mu`` times the normal impulse. Defaults to ``0.5`` (matching :class:`PhysicsMaterial`). Node-agnostic primitive: the node unpacks its ``PhysicsMaterial`` resource into this and the next three params (the seam never sees the resource or the node). restitution: Bounciness in ``[0, 1]`` (``0`` = inelastic, the default, so the bare seam API does not bounce). Combined per-contact and gated by a small velocity rest-threshold (see the builtin solver). friction_combine: How this body's ``friction`` combines with the other body's at a contact (:class:`CombineMode`); defaults to AVERAGE. restitution_combine: How this body's ``restitution`` combines with the other body's, INDEPENDENT of ``friction_combine`` (defaults to AVERAGE). continuous: When True, this body uses continuous collision detection: each step its centre displacement is swept against STATIC geometry and clamped to the time-of-impact so a fast small body cannot tunnel through thin static colliders. Defaults False (discrete). Basic-tier honesty: a CENTRE ray / shapecast sweep vs STATIC bodies only, no rotational sweep and no dynamic-vs-dynamic CCD; the Jolt backend honours the flag faithfully via ``EMotionQuality::LinearCast``. Returns: An opaque body handle, stable until :meth:`destroy_body`. """
[docs] @abstractmethod def destroy_body(self, handle: BodyHandle) -> None: """Remove a body from the world. After destruction the handle is invalid. Callers that use the bulk readers must re-call :meth:`register_bodies` to re-establish row order. Args: handle: A handle previously returned by :meth:`create_body`. """
[docs] @abstractmethod def set_body_transform(self, handle: BodyHandle, transform: Any) -> None: """Teleport a body to a new world transform. Args: handle: Body handle. transform: New world transform (position + orientation). """
[docs] @abstractmethod def set_body_velocity( self, handle: BodyHandle, linear: Vec3, angular: Vec3 | None = None, ) -> None: """Set a body's linear and angular velocity directly. Args: handle: Body handle. linear: Linear velocity (``Vec3``), world units/s. angular: Angular velocity (``Vec3``), radians/s about each axis. ``None`` (default) means zero angular velocity. """
[docs] @abstractmethod def set_body_mode(self, handle: BodyHandle, mode: BodyMode) -> None: """Change a live body's motion mode in place (no destroy/recreate). Flips the body between STATIC / KINEMATIC / DYNAMIC, updating its effective (inverse) mass: STATIC and KINEMATIC are infinite-mass (inv_mass 0), DYNAMIC uses the body's stored mass. Maps onto Jolt's Body::SetMotionType; the builtin backend flips body_type + inverse_mass. """
[docs] @abstractmethod def body_velocity(self, handle: BodyHandle) -> tuple[Vec3, Vec3]: """Read a body's current ``(linear, angular)`` velocity, per-body. Cold per-body read parallel to :meth:`body_transform`. The bulk :meth:`read_velocities` stays the hot scatter path; this is the accessor used by ``PhysicsBody3D.velocity`` / ``.spin`` for a single synchronous read-back (e.g. ``self.velocity += dv``). Args: handle: Body handle. Returns: ``(linear, angular)`` velocity (``Vec3``, ``Vec3``); angular in radians/s. Returns zero velocities for an infinite-mass body that was never moved. """
[docs] @abstractmethod def sleeping(self, handle: BodyHandle) -> bool: """True if the body is asleep (skipped by integrate + solve until woken). STATIC / KINEMATIC bodies are never 'asleep' (they were never awake): returns False for them. A sleeping body stays a full collider and still reads back its (frozen) transform / velocity through the bulk readers. """
# -- forces -------------------------------------------------------------
[docs] @abstractmethod def apply_impulse( self, handle: BodyHandle, impulse: Vec3, *, at: Vec3 | None = None, angular: Vec3 | None = None, ) -> None: """Apply an instantaneous velocity change to a body NOW. Unlike :meth:`apply_force` this takes effect immediately (it mutates velocity, not an accumulator) and is NOT cleared by :meth:`step`. Inert on non-DYNAMIC bodies (inverse mass 0). Args: handle: Body handle. impulse: Linear impulse (``Vec3``), N*s. Adds ``impulse * inv_mass`` to the linear velocity. at: Optional world-space application point. When given, the offset ``r = at - position`` contributes an angular impulse ``cross(r, impulse)`` (basic tier scales it by ``inv_mass`` as a stand-in for the inverse inertia tensor, which the basic backend does not model). ``None`` applies the impulse purely through the centre of mass (no torque). angular: Optional explicit angular impulse (``Vec3``), for ``spin_up``. Adds ``angular * inv_mass`` (basic-tier inverse inertia stand-in) to the angular velocity, independent of ``at``. """
[docs] @abstractmethod def apply_force(self, handle: BodyHandle, force: Vec3, *, at: Vec3 | None = None) -> None: """Accumulate a continuous force, applied during the NEXT :meth:`step`. The force is integrated as acceleration (``force * inv_mass``) before position integration, then **auto-cleared** at the end of the step. To sustain a force the caller must re-add it every fixed step (per design S7); a single call affects exactly one step. Inert on non-DYNAMIC. Args: handle: Body handle. force: Linear force (``Vec3``), N. at: Optional world-space application point. When given, the offset ``r = at - position`` adds a torque ``cross(r, force)`` to the torque accumulator. ``None`` applies the force through the COM. """
[docs] @abstractmethod def apply_torque(self, handle: BodyHandle, torque: Vec3) -> None: """Accumulate a continuous torque, applied during the NEXT :meth:`step`. Auto-cleared after the step like :meth:`apply_force`: re-add each fixed step to sustain it. Inert on non-DYNAMIC. Basic tier applies it as ``torque * inv_mass`` (inverse inertia stand-in). Args: handle: Body handle. torque: Torque (``Vec3``), N*m. """
# -- joints / constraints (Stage T1c, sequential-impulse basic tier) ----
[docs] @abstractmethod def create_fixed_joint(self, a: BodyHandle, b: BodyHandle) -> JointHandle: """Weld two bodies: lock their full relative transform. Captures the CURRENT relative pose of ``b`` in ``a``'s frame at create time (relative position AND relative orientation) and holds it: the two bodies thereafter move as one rigid assembly. Basic-tier honesty: the builtin backend has NO inertia tensor, so the angular lock uses ``inverse_mass`` as the inverse-inertia scalar (per T1a); a long thin body or an off-centre weld will rotate too easily versus a real solver. Convergence is a few sequential-impulse iterations, so a long weld chain sags slightly. Precise articulated mechanisms are a Jolt concern. Args: a: First body handle (the reference frame). b: Second body handle (welded into ``a``'s frame). Returns: An opaque :data:`JointHandle`, valid until :meth:`remove_joint` (or until either body is destroyed, which silently drops the joint). """
[docs] @abstractmethod def create_pin_joint(self, a: BodyHandle, b: BodyHandle, anchor: Vec3) -> JointHandle: """Pin two bodies at a single world-space point (ball / point-to-point). Constrains the two bodies so the world point ``anchor`` stays coincident on both (they cannot separate there) while leaving all three rotational DOF free. The anchor is stored as a per-body offset (``r_a = anchor - pos_a``, ``r_b = anchor - pos_b``) captured at create. Basic-tier honesty: the stored anchor offset does NOT rotate with the body (re-derived each step as ``pos + r`` in WORLD axes, same limitation as the hull-rotation-ignored narrowphase), so a pin on a spinning body drifts. Angular cross-coupling uses ``inverse_mass`` as the inverse-inertia scalar. A few iterations of convergence. Args: a: First body handle. b: Second body handle. anchor: World-space pivot point shared by both bodies (``Vec3``). Returns: An opaque :data:`JointHandle`. """
[docs] @abstractmethod def create_hinge_joint(self, a: BodyHandle, b: BodyHandle, anchor: Vec3, axis: Vec3) -> JointHandle: """Hinge two bodies: pin at ``anchor`` + one free rotational DOF about ``axis``. A point constraint at ``anchor`` (like :meth:`create_pin_joint`) PLUS an angular constraint that locks the two off-axis rotational DOF, leaving free rotation only about the world-space ``axis`` (normalised and captured at create). Motors and angular limits are explicitly OUT of scope this stage (a deliberate follow-on). Basic-tier honesty: same anchor-does-not-rotate and ``inverse_mass`` inverse-inertia-scalar caveats as :meth:`create_pin_joint`; a few iterations of convergence. Args: a: First body handle. b: Second body handle. anchor: World-space hinge pivot point (``Vec3``). axis: World-space hinge axis (``Vec3``, normalised at create). Returns: An opaque :data:`JointHandle`. """
[docs] @abstractmethod def create_spring_joint( self, a: BodyHandle, b: BodyHandle, rest_length: float, stiffness: float, damping: float ) -> JointHandle: """Soft distance-spring between the two body centres (compliant, not rigid). A soft constraint that pulls the two body centres of mass toward ``rest_length`` apart with spring constant ``stiffness`` (N/m) and damping ``damping`` (N*s/m). Unlike the rigid joints it is intentionally compliant: it applies a soft velocity impulse with a ``k*x`` bias and a ``c*v`` damping term, and is NEVER position-corrected. Basic-tier honesty: the builtin backend uses the two COMs, NOT per-body anchors (Pin / Hinge use anchors, Spring uses centres for simplicity). The explicit soft-impulse form can oscillate or overshoot when ``stiffness`` is large relative to the fixed ``dt``; a stiff spring needs a smaller ``dt`` or the Jolt backend. Nothing is silently clamped. Args: a: First body handle. b: Second body handle. rest_length: Target centre-to-centre separation (world units, >= 0). stiffness: Spring constant k (N/m). damping: Damping coefficient c (N*s/m). Returns: An opaque :data:`JointHandle`. """
[docs] @abstractmethod def remove_joint(self, handle: JointHandle) -> None: """Remove a constraint; the handle is invalid afterwards. A no-op if ``handle`` is unknown (already removed, or silently dropped because one of its bodies was destroyed): this is the SAME silent-drop contract :meth:`destroy_body` already uses for touching / overlap pairs, not an error-swallowing shim. A joint whose body was freed is the expected case, so removing it twice (once by the body-purge, once by the joint node's own teardown) must be safe in either teardown order. Args: handle: A handle previously returned by a ``create_*_joint`` call. """
# -- introspection ------------------------------------------------------
[docs] @property @abstractmethod def body_count(self) -> int: """Number of bodies currently in the world. Read-only. Used by ``SceneTree.physics_tick`` to skip stepping empty worlds for zero overhead, mirroring ``PhysicsServer.body_count``. """
[docs] @abstractmethod def clear(self) -> None: """Remove every body, character, and joint, emptying the world. The seam equivalent of the old global ``PhysicsServer.reset()``: a level teardown / restart-the-scene primitive that returns the world to an empty state (``body_count == 0``) WITHOUT discarding the world object, its configured :attr:`gravity`, or its backend. Per-step edge-diff buffers (contacts / overlaps) and any warm-start cache are reset so the next step starts from a clean broadphase. Cached shape handles stay valid (shapes are reusable resources), and handle counters keep advancing so a freed handle is never re-issued to a new body. After :meth:`clear`, callers using the bulk readers must re-:meth:`register_bodies`. """
# -- stepping -----------------------------------------------------------
[docs] @abstractmethod def step(self, dt: float) -> None: """Advance the whole world once by a fixed timestep. This integrates every body, resolves collisions, and updates internal state for the entire world as a unit. It is designed to be driven by a fixed-step accumulator (Stage 2 wires it to ``SceneTree.physics_tick``); in Stage 1 it is called manually by tests. Args: dt: Fixed timestep in seconds. Callers must pass a constant value. """
# -- collision events (broadphase-diffed edges) ------------------------
[docs] @abstractmethod def drain_contact_events(self) -> list[ContactEvent]: """Return and CLEAR this step's buffered enter/exit contact events. Edge-only and broadphase-driven: the backend diffs the touching-pair set each :meth:`step` and buffers a :class:`ContactEvent` for every pair that began (``ENTER``) or stopped (``EXIT``) overlapping. Returns ``[]`` when nothing changed. Node-agnostic: events are keyed by body handles only; the tree maps handles to nodes and fires the Signals. Both static-dynamic AND dynamic-dynamic pairs are reported, filtered by the same layer/mask rule the simulation uses. """
[docs] @abstractmethod def drain_overlap_events(self) -> list[OverlapEvent]: """Return and CLEAR this step's buffered sensor-overlap enter/exit events. Edge-only, broadphase-driven, DIRECTED (keyed ``sensor -> other``), filtered by the one-directional sensor rule (``sensor.mask & other.layer``), node-agnostic. Distinct from :meth:`drain_contact_events`: a sensor pair produces NO collision response and NO manifold, so the event carries only the two handles plus the :class:`ContactPhase`. Returns ``[]`` when nothing changed. """
# -- bulk transfer (the keystone) --------------------------------------
[docs] @abstractmethod def register_bodies(self, handles: list[BodyHandle]) -> None: """Fix the body->row order used by the bulk readers. Establishes the mapping from each body handle to a row index. After this call, :meth:`read_transforms` / :meth:`read_velocities` fill row ``i`` with the state of ``handles[i]``. Call again whenever membership or desired ordering changes. Args: handles: Ordered list of body handles. ``len(handles)`` is the row count ``N`` expected by the bulk readers. """
[docs] @abstractmethod def read_transforms(self, out: np.ndarray) -> None: """Fill ``out`` with current body transforms, in place. Bulk hot-path read. The backend writes into the caller-owned buffer and allocates nothing. Args: out: Pre-allocated array of shape ``(N, 7)``, dtype ``float32``, C-contiguous, where ``N`` matches the most recent :meth:`register_bodies`. Each row is ``[px, py, pz, qx, qy, qz, qw]``: position xyz followed by an orientation quaternion in **xyzw** order (scalar-last). Row ``i`` corresponds to ``handles[i]``. """
[docs] @abstractmethod def read_velocities(self, out: np.ndarray) -> None: """Fill ``out`` with current body velocities, in place. Bulk hot-path read. The backend writes into the caller-owned buffer and allocates nothing. Args: out: Pre-allocated array of shape ``(N, 6)``, dtype ``float32``, C-contiguous, where ``N`` matches the most recent :meth:`register_bodies`. Each row is ``[lx, ly, lz, ax, ay, az]``: linear velocity xyz followed by angular velocity xyz (radians/s). Row ``i`` corresponds to ``handles[i]``. """
# -- queries (minimal) --------------------------------------------------
[docs] @abstractmethod def raycast( self, origin: Vec3, direction: Vec3, max_dist: float, *, mask: int = 0xFFFFFFFF, ) -> RaycastHit | None: """Cast a ray and return the nearest hit, or ``None``. Args: origin: Ray origin in world space (``Vec3``). direction: Ray direction (``Vec3``); need not be normalised. max_dist: Maximum distance along ``direction`` to test. mask: Query layer mask. Only bodies whose ``collision_layer & mask`` is non-zero are considered. Defaults to all layers. This is the single query-mask convention (one query mask vs each body's layer), distinct from the bidirectional body-pair rule used by the simulation. Returns: A :class:`RaycastHit` for the closest body intersected within ``max_dist`` whose layer matches ``mask``, or ``None`` if the ray hits nothing. """
[docs] @abstractmethod def raycast_all( self, origin: Vec3, direction: Vec3, max_dist: float, *, mask: int = 0xFFFFFFFF, ) -> list[RaycastHit]: """Cast a ray and return EVERY hit within ``max_dist``, sorted by distance. Like :meth:`raycast` but collects all intersected bodies (whose ``collision_layer & mask`` is set) instead of only the nearest, returned ascending by :attr:`RaycastHit.distance`. Empty list on no hit. Backs ``PhysicsQuery.raycast_all`` and the ``exclude=`` filter path of ``raycast`` (which must skip excluded nearer hits). Args: origin: Ray origin in world space (``Vec3``). direction: Ray direction (``Vec3``); need not be normalised. max_dist: Maximum distance along ``direction`` to test. mask: Query layer mask (single query-mask vs body-layer convention). Returns: All :class:`RaycastHit`\\ s within ``max_dist``, sorted ascending by distance (empty if the ray hits nothing). """
[docs] @abstractmethod def shapecast( self, shape: ShapeHandle, origin: Vec3, direction: Vec3, max_dist: float, *, mask: int = 0xFFFFFFFF, ) -> Contact | None: """Sweep a shape along a ray and return the earliest-TOI contact, or ``None``. Sweeps ``shape`` from ``origin`` along ``direction`` (need not be normalised) up to ``max_dist`` against world bodies, returning the earliest time-of-impact contact among bodies whose ``collision_layer & mask`` is set, else ``None``. Reuses :class:`Contact`: the swept shape is the implicit caller, ``body`` is the hit body, ``normal`` is the separating normal pointing back toward the cast origin, and ``distance`` is the TOI distance along ``direction``. Basic-tier honesty: this is a substepped sweep, not a true continuous cast, so fast casts vs very thin colliders can tunnel and box orientation is ignored (AABB), matching :meth:`move_and_collide`. Args: shape: An opaque shape handle from :meth:`create_sphere`, :meth:`create_box`, :meth:`create_capsule`, or :meth:`create_cylinder`. origin: Cast origin in world space (``Vec3``). direction: Cast direction (``Vec3``); need not be normalised. max_dist: Maximum sweep distance along ``direction``. mask: Query layer mask (single query-mask vs body-layer convention). Returns: The earliest-TOI :class:`Contact`, or ``None`` if nothing was hit. """
[docs] @abstractmethod def overlap(self, shape: ShapeHandle, transform: Any, *, mask: int = 0xFFFFFFFF) -> list[BodyHandle]: """Return all bodies a static shape overlaps at ``transform``. Places ``shape`` at ``transform`` (same flexible forms as :meth:`create_body`) and returns the handles of every body it overlaps whose ``collision_layer & mask`` is set, sorted by handle for determinism. Basic-tier honesty: AABB-ish narrowphase, box orientation ignored, like :meth:`move_and_collide`. Args: shape: An opaque shape handle from :meth:`create_sphere`, :meth:`create_box`, :meth:`create_capsule`, or :meth:`create_cylinder`. transform: World pose to place the shape at (same flexible forms as :meth:`create_body`). mask: Query layer mask (single query-mask vs body-layer convention). Returns: Sorted list of overlapping body handles (empty if none). """
# -- kinematic sweep ----------------------------------------------------
[docs] @abstractmethod def move_and_collide(self, handle: BodyHandle, motion: Vec3) -> Contact | None: """Move a kinematic body by ``motion``, stopping at the first contact. Shape-casts the body's shape from its current pose along world-space ``motion`` (already ``velocity * dt``: the seam takes a displacement, not a velocity, so ``dt`` lives in the node) against every OTHER body and finds the earliest time-of-impact. Sets the body's stored transform to the position it actually reached (``origin + motion * toi`` minus a tiny safe margin). Does NOT slide and does NOT integrate gravity: one sweep, stop at first contact. Args: handle: A body created with :attr:`BodyMode.KINEMATIC`. motion: World-space displacement (``Vec3``). Returns: A :class:`Contact` (other body, world point, separating normal, distance travelled) if the sweep stopped early, else ``None`` after moving the full ``motion``. """
# -- body read-back -----------------------------------------------------
[docs] @abstractmethod def body_transform(self, handle: BodyHandle) -> tuple[Vec3, Quat]: """Read a body's current pose as ``(position, orientation)``. Parallel to :meth:`character_transform`; returns plain ``Vec3`` / ``Quat`` so callers (e.g. ``KinematicBody3D``) get a clean synchronous read-back after a user-driven :meth:`move_and_collide` without the bulk path. Args: handle: Body handle. Returns: ``(position, orientation)``. """
# -- character controller (CharacterVirtual-style) ---------------------
[docs] @abstractmethod def create_character( self, shape: ShapeHandle, transform: Any, *, up: Vec3 = _DEFAULT_UP, slope_limit: float = math.radians(45.0), step_height: float = 0.0, skin_width: float = 0.001, collision_layer: int = 1, collision_mask: int = 0xFFFFFFFF, ) -> CharacterHandle: """Create a character controller bound to this world. The character collides against world bodies but is NOT added to the dynamic-body set: it is never force-integrated and never appears in :meth:`register_bodies` / :meth:`read_transforms`. This mirrors a Jolt ``CharacterVirtual`` (distinct from a rigid ``Body``). Args: shape: An opaque shape handle from :meth:`create_sphere`, :meth:`create_box`, :meth:`create_capsule`, or :meth:`create_cylinder`. transform: Initial world pose (position + orientation), same flexible forms accepted by :meth:`create_body`. up: World up vector (``Vec3``), engine Y-up convention. slope_limit: Maximum walkable slope, in RADIANS (engine convention). step_height: Maximum step-up height in world units (0 disables). skin_width: Collision safe margin in world units. collision_layer: 32-bit layer membership of this character. Defaults to layer 1. collision_mask: 32-bit mask of layers the character collides with. Defaults to all (``0xFFFFFFFF``). Character sweeps use the same bidirectional OR rule as bodies (``char.mask & body.layer`` or ``body.mask & char.layer``). Returns: An opaque :data:`CharacterHandle`, distinct from any body handle. """
[docs] @abstractmethod def destroy_character(self, handle: CharacterHandle) -> None: """Remove a character controller from the world. Args: handle: A handle previously returned by :meth:`create_character`. """
[docs] @abstractmethod def set_character_transform(self, handle: CharacterHandle, transform: Any) -> None: """Teleport a character to a new pose (no collision). For sync-down on re-parent / explicit position writes. Args: handle: Character handle. transform: New world pose (position + orientation). """
[docs] @abstractmethod def character_transform(self, handle: CharacterHandle) -> tuple[Vec3, Quat]: """Read a character's current pose as ``(position, orientation)``. For synchronous node sync after a move. Args: handle: Character handle. Returns: ``(position, orientation)``. """
[docs] @abstractmethod def character_move_and_slide( self, handle: CharacterHandle, velocity: Vec3, dt: float, *, up: Vec3, max_slides: int = 4, ) -> CharacterMoveResult: """Collide-and-slide a character by ``velocity * dt`` against the world. Deflects velocity along contact normals over up to ``max_slides`` iterations, classifies floor/wall/ceiling from contact normals vs ``up`` (and the character's ``slope_limit``), and updates the character's stored pose. ``up`` is passed per-call (the node owns the up vector); the fixed collider config (``slope_limit`` / ``step_height`` / ``skin_width``) is set at create time, like Jolt's ``CharacterVirtualSettings``. Floor/wall/ceiling state is returned per-move in :class:`CharacterMoveResult` (not a separate query method), so it is valid only right after this call, matching Jolt's ``GetGroundState()``. Args: handle: Character handle. velocity: Desired world-space velocity (``Vec3``), units/s. dt: Timestep in seconds. up: World up vector (``Vec3``). max_slides: Maximum collide-and-slide iterations. Returns: A :class:`CharacterMoveResult`. """
# -- subclass helpers (contract enforcement) --------------------------- @staticmethod def _check_transforms_out(out: np.ndarray, n: int) -> None: """Assert ``out`` meets the :meth:`read_transforms` contract. Subclasses call this at the top of ``read_transforms`` so the bulk contract (shape ``(N, 7)``, ``float32``, C-contiguous) is enforced uniformly across backends. """ assert out.shape == (n, 7), f"read_transforms out must be ({n}, 7), got {out.shape}" assert out.dtype == np.float32, f"read_transforms out must be float32, got {out.dtype}" assert out.flags["C_CONTIGUOUS"], "read_transforms out must be C-contiguous" @staticmethod def _check_velocities_out(out: np.ndarray, n: int) -> None: """Assert ``out`` meets the :meth:`read_velocities` contract. Subclasses call this at the top of ``read_velocities`` so the bulk contract (shape ``(N, 6)``, ``float32``, C-contiguous) is enforced uniformly across backends. """ assert out.shape == (n, 6), f"read_velocities out must be ({n}, 6), got {out.shape}" assert out.dtype == np.float32, f"read_velocities out must be float32, got {out.dtype}" assert out.flags["C_CONTIGUOUS"], "read_velocities out must be C-contiguous"
__all__ = [ "BodyMode", "Capability", "CombineMode", "RaycastHit", "Contact", "ContactPhase", "ContactEvent", "OverlapEvent", "CharacterMoveResult", "PhysicsWorld", "BodyHandle", "ShapeHandle", "CharacterHandle", "JointHandle", ] # Quat is imported for the orientation convention documented above (xyzw, # scalar-last) and to keep the math-type dependency explicit for Stage 2. _ = Quat