simvx.core.physics.world2d

Role

This module defines Physics2DWorld, the abstract interface every 2D physics backend (BuiltinPhysics2D now, an optional PymunkPhysics2D later) implements. It is the 2D sibling of :class:~simvx.core.physics.world.PhysicsWorld and serves the same purpose: a transport abstraction that moves rigid-body state across the Python boundary efficiently, not a definition of solver behaviour.

A SEPARATE seam (not the 3D one constrained to a plane) is deliberate (design physics_2d_design.md): the bulk contract width differs ((N,4) transforms / (N,3) velocities here vs (N,7) / (N,6) in 3D), the optional native backend differs (pymunk / Chipmunk2D vs Jolt), and rotation is a scalar radian angle rather than a quaternion. Dimension-agnostic pieces (:class:BodyMode,

class:

CombineMode, :class:ContactPhase, :class:PhysicsMaterial) are REUSED by import from the 3D modules, never duplicated.

World convention (state it loudly)

Y-up, identical to the 3D world. Gravity defaults to Vec2(0, -9.81): down is -Y. A Y-down game (renders +Y downward on screen) simply sets gravity=Vec2(0, 9.81); the seam itself stays neutral and never assumes a screen orientation. Rotation is a scalar angle in radians, positive counter-clockwise (standard math convention).

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. The transform row is (N, 4) = [px, py, cos(theta), sin(theta)] (cos/sin rather than the bare angle so interpolation lerps the unit vector with no +-pi wraparound, matching Transform2D and the pymunk angle convention); the velocity row is (N, 3) = [lx, ly, omega] (linear x/y plus scalar angular velocity in radians/s). See :meth:register_bodies,

meth:

read_transforms, and :meth:read_velocities.

Stage T2a status

This module defines the FULL ABC surface so subclasses have a complete contract. BuiltinPhysics2D implements the T2a subset (gravity, shapes, bodies, integrator, bulk readers); the collision-dependent methods (queries, events, joints, forces, character, one-way) are declared @abstractmethod here and filled by later sub-stages (T2b-T2e), with honest NotImplementedError stubs in the backend until then.

Physics2DWorld: the 2D backend transport seam (Stage T2 additive scaffolding).

Module Contents

Classes

RaycastHit2D

Result of a successful 2D raycast against the world (T2d).

Contact2D

Result of a kinematic / character shape-sweep stopping against a body (T2d).

ContactEvent2D

A node-agnostic 2D body-pair collision event emitted by the seam (T2d).

OverlapEvent2D

A node-agnostic 2D sensor-overlap event emitted by the seam (T2d).

CharacterMoveResult2D

Returned struct of a 2D character collide-and-slide move (T2e).

Physics2DWorld

Abstract 2D backend seam: one isolated simulation world.

Data

API

simvx.core.physics.world2d.BodyHandle

None

simvx.core.physics.world2d.ShapeHandle

None

simvx.core.physics.world2d.CharacterHandle

None

simvx.core.physics.world2d.JointHandle

None

class simvx.core.physics.world2d.RaycastHit2D[source]

Result of a successful 2D raycast against the world (T2d).

Attributes: body: Handle of the body the ray hit. point: World-space contact point (Vec2). normal: World-space surface normal at the hit (Vec2, unit length). distance: Distance from the ray origin to point along the ray.

body: simvx.core.physics.world2d.BodyHandle

None

point: simvx.core.math.Vec2

None

normal: simvx.core.math.Vec2

None

distance: float

None

class simvx.core.physics.world2d.Contact2D[source]

Result of a kinematic / character shape-sweep stopping against a body (T2d).

2D sibling of :class:~simvx.core.physics.world.Contact: a single “other” body (like a query result); the swept body is implicit (the caller).

Attributes: body: Handle of the OTHER body that was hit. point: World-space contact point (Vec2). normal: World-space surface normal (Vec2, unit), pointing AWAY from the other body toward the moving body (the direction that separates the mover). distance: Distance the mover actually travelled before contact (TOI distance, 0..|motion|).

body: simvx.core.physics.world2d.BodyHandle

None

point: simvx.core.math.Vec2

None

normal: simvx.core.math.Vec2

None

distance: float

None

class simvx.core.physics.world2d.ContactEvent2D[source]

A node-agnostic 2D body-pair collision event emitted by the seam (T2d).

2D sibling of :class:~simvx.core.physics.world.ContactEvent. Keyed by body HANDLES only (the seam never names a node); orientation is fixed a -> b. Reuses :class:ContactPhase (no second phase enum).

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 (Vec2). Meaningful on ENTER; degenerate (Vec2(0)) on EXIT. normal: Unit contact normal oriented a -> b (Vec2). Degenerate (Vec2(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 none. rel_velocity: Pre-solve velocity of b w.r.t. a at the contact (Vec2). Degenerate (Vec2(0)) on EXIT.

a: simvx.core.physics.world2d.BodyHandle

None

b: simvx.core.physics.world2d.BodyHandle

None

phase: simvx.core.physics.world.ContactPhase

None

point: simvx.core.math.Vec2

None

normal: simvx.core.math.Vec2

None

impulse: float

None

rel_velocity: simvx.core.math.Vec2

None

class simvx.core.physics.world2d.OverlapEvent2D[source]

A node-agnostic 2D sensor-overlap event emitted by the seam (T2d).

2D sibling of :class:~simvx.core.physics.world.OverlapEvent: a SECOND, independent edge-diffed stream. DIRECTED sensor -> other (the observing sensor decides via its mask). Reuses :class:ContactPhase.

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).

sensor: simvx.core.physics.world2d.BodyHandle

None

other: simvx.core.physics.world2d.BodyHandle

None

phase: simvx.core.physics.world.ContactPhase

None

class simvx.core.physics.world2d.CharacterMoveResult2D[source]

Returned struct of a 2D character collide-and-slide move (T2e).

2D sibling of :class:~simvx.core.physics.world.CharacterMoveResult. 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 (Vec2). 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 Vec2), or +up if there was no floor contact.

velocity: simvx.core.math.Vec2

None

on_floor: bool

None

on_wall: bool

None

on_ceiling: bool

None

floor_normal: simvx.core.math.Vec2

None

class simvx.core.physics.world2d.Physics2DWorld(*, gravity: simvx.core.math.Vec2)[source]

Bases: abc.ABC

Abstract 2D backend seam: one isolated simulation world.

A Physics2DWorld 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 (BuiltinPhysics2D, later PymunkPhysics2D) 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). The transform row is (N, 4) = [px, py, cos(theta), sin(theta)] and the velocity row is (N, 3) = [lx, ly, omega] (see the module docstring for the Y-up / radians convention).

Initialization

Initialise the world.

Args: gravity: World gravity acceleration vector (Vec2), metres/s^2. Y-up: Vec2(0, -9.81) is “down”.

property gravity: simvx.core.math.Vec2[source]

World gravity acceleration vector (Vec2), metres/s^2 (Y-up).

capabilities() frozenset[simvx.core.physics.capability.Capability][source]

Return the set of Tier-3 :class:Capability features this 2D backend honours.

2D sibling of :meth:~simvx.core.physics.world.PhysicsWorld.capabilities, sharing the same dimension-agnostic :class:Capability enum. The default is the empty set (a backend advertises nothing it cannot honour); native backends override to list only what they actually support.

abstractmethod create_circle(radius: float) simvx.core.physics.world2d.ShapeHandle[source]

Create a circle collision shape and return an opaque handle.

Args: radius: Circle radius, world units (> 0).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_box(half_extents: simvx.core.math.Vec2) simvx.core.physics.world2d.ShapeHandle[source]

Create an axis-aligned (body-local) box shape, centred at the origin.

Args: half_extents: Half-sizes along x/y (Vec2, both > 0).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_capsule(radius: float, height: float) simvx.core.physics.world2d.ShapeHandle[source]

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 semicircular 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 circle.

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_segment(a: simvx.core.math.Vec2, b: simvx.core.math.Vec2, radius: float = 0.0) simvx.core.physics.world2d.ShapeHandle[source]

Create a line-segment collision shape (2D-only).

A thick line from a to b (a “beam”), the 2D analogue with no 3D equivalent. Useful for thin static walls / floors and one-way platforms.

Args: a: Segment start, body-local (Vec2). b: Segment end, body-local (Vec2). radius: Segment thickness radius (>= 0); 0 is an infinitely thin line.

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_convex_polygon(points: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]

Create a convex polygon collision shape from CCW points.

Args: points: (N, 2) float32 array of >= 3 points in counter-clockwise winding, defining a convex polygon (body-local).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_concave_polygon(segments: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]

Create a STATIC edge-soup collision shape (2D analogue of a mesh).

Args: segments: (N, 2, 2) float32 array of N line segments (each an [start, end] pair of Vec2 points), body-local. The 2D analogue of a static triangle mesh: STATIC-ONLY level geometry.

Returns: An opaque shape handle for use with :meth:create_body. A concave polygon is a STATIC-ONLY collider: placing it on a non-STATIC body is an error.

abstractmethod create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: Any, *, mass: float = 1.0, collision_layer: int = 1, collision_mask: int = 4294967295, is_sensor: bool = False, friction: float = 0.5, restitution: float = 0.0, friction_combine: simvx.core.physics.material.CombineMode = CombineMode.AVERAGE, restitution_combine: simvx.core.physics.material.CombineMode = CombineMode.AVERAGE, continuous: bool = False) simvx.core.physics.world2d.BodyHandle[source]

Create a body in the world and return its handle.

Mirrors :meth:~simvx.core.physics.world.PhysicsWorld.create_body (same layer/mask, sensor, material, and continuous semantics), with 2D transforms (position Vec2 + scalar rotation).

Args: shape: An opaque shape handle from one of the create_* shape factories. A concave polygon on a non-STATIC body is an error. body_type: One of :class:BodyMode. transform: Initial world transform. Accepted as a Transform2D (.position + .rotation), a bare Vec2 / sequence (position only, zero rotation), or a (position, rotation) pair where rotation is a scalar in radians. mass: Body mass in kg, used only for DYNAMIC bodies. Ignored for STATIC / KINEMATIC (treated as infinite). collision_layer: 32-bit layer membership; defaults to layer 1. collision_mask: 32-bit mask of layers this body scans; defaults all. is_sensor: When True, this body is a SENSOR (trigger): broadphase only, excluded from collision resolution, feeds the separate overlap stream (one-directional sensor.mask & other.layer). friction: Coulomb friction coefficient mu (>= 0), default 0.5. restitution: Bounciness in [0, 1] (default 0.0, no bounce). friction_combine: Per-contact friction combine mode (default AVERAGE). restitution_combine: Per-contact restitution combine mode (default AVERAGE), independent of friction_combine. continuous: When True, use continuous collision detection (centre sweep vs STATIC, TOI clamp; basic-tier honesty, full CCD deferred to pymunk). Defaults False (discrete).

Returns: An opaque body handle, stable until :meth:destroy_body.

abstractmethod destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None[source]

Remove a body from the world; the handle is invalid afterwards.

Callers that use the bulk readers must re-call :meth:register_bodies to re-establish row order.

abstractmethod set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: Any) None[source]

Teleport a body to a new world transform (position + scalar rotation).

abstractmethod set_body_velocity(handle: simvx.core.physics.world2d.BodyHandle, linear: simvx.core.math.Vec2, angular: float = 0.0) None[source]

Set a body’s linear and (scalar) angular velocity directly.

Args: handle: Body handle. linear: Linear velocity (Vec2), world units/s. angular: Angular velocity (scalar float), radians/s (CCW positive). Defaults to 0.0.

abstractmethod set_body_mode(handle: simvx.core.physics.world2d.BodyHandle, mode: simvx.core.physics.world.BodyMode) None[source]

Change a live body’s motion mode in place (no destroy/recreate).

Flips STATIC / KINEMATIC / DYNAMIC, updating effective (inverse) mass and moment of inertia: STATIC / KINEMATIC are infinite (inverse 0), DYNAMIC uses the body’s stored mass and per-shape moment.

abstractmethod body_velocity(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]

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.

Returns: (linear, angular) velocity (Vec2, scalar float in radians/s). Returns zero velocities for an infinite-mass body that was never moved.

abstractmethod body_transform(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]

Read a body’s current pose as (position, rotation).

Cold per-body read parallel to :meth:body_velocity.

Returns: (position, rotation) (Vec2, scalar float radians).

abstractmethod sleeping(handle: simvx.core.physics.world2d.BodyHandle) bool[source]

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.

abstractmethod apply_impulse(handle: simvx.core.physics.world2d.BodyHandle, impulse: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None, angular: float = 0.0) None[source]

Apply an instantaneous velocity change to a body NOW (T2c).

Args: handle: Body handle. impulse: Linear impulse (Vec2), N*s. at: Optional world-space application point; the offset r = at - position contributes a scalar angular impulse via the 2D cross product cross(r, impulse) scaled by the inverse moment of inertia. angular: Optional explicit scalar angular impulse (radians-equivalent), applied via the inverse moment of inertia.

abstractmethod apply_force(handle: simvx.core.physics.world2d.BodyHandle, force: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None) None[source]

Accumulate a continuous force, applied during the NEXT :meth:step (T2c).

Auto-cleared at the end of the step (re-add each fixed step to sustain). Inert on non-DYNAMIC. at adds a scalar torque cross(r, force).

abstractmethod apply_torque(handle: simvx.core.physics.world2d.BodyHandle, torque: float) None[source]

Accumulate a continuous scalar torque for the NEXT :meth:step (T2c).

Auto-cleared after the step (re-add each fixed step to sustain). Inert on non-DYNAMIC. Applied via the inverse moment of inertia.

abstractmethod create_fixed_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle) simvx.core.physics.world2d.JointHandle[source]

Weld two bodies: lock their full relative transform (T2c).

abstractmethod create_pin_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, anchor: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]

Pin two bodies at a single world-space point, rotation free (T2c).

abstractmethod create_hinge_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, anchor: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]

Hinge two bodies at anchor (T2c).

2D rotation is 1-DOF, so a 2D hinge has no axis argument: it is a pin at anchor (this tier). Motors and angular limits are a follow-on.

abstractmethod create_spring_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, rest_length: float, stiffness: float, damping: float) simvx.core.physics.world2d.JointHandle[source]

Soft distance-spring between the two body centres (compliant) (T2c).

rest_length < 0 auto-captures the current centre distance as the rest length (the common “spring at its natural length on creation” case).

abstractmethod create_groove_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, groove_a: simvx.core.math.Vec2, groove_b: simvx.core.math.Vec2, anchor_b: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]

Constrain b’s anchor to slide along a groove on a (2D-only) (T2g).

The pymunk-native slider-on-a-line constraint with no 3D equivalent. The groove is the segment [groove_a, groove_b] in a’s frame; b’s anchor_b (in b’s frame) is constrained to lie on that line.

abstractmethod remove_joint(handle: simvx.core.physics.world2d.JointHandle) None[source]

Remove a constraint; the handle is invalid afterwards (T2c).

A no-op if handle is unknown (already removed, or silently dropped because one of its bodies was destroyed).

abstract property body_count: int[source]

Number of bodies currently in the world (skip-empty-world fast path).

abstractmethod clear() None[source]

Remove every body, character, and joint, emptying the world.

2D sibling of :meth:~simvx.core.physics.world.PhysicsWorld.clear: the seam equivalent of the old global PhysicsServer.reset(). Returns the world to an empty state (body_count == 0) WITHOUT discarding the world, its

Attr:

gravity, or its backend; per-step edge-diff buffers and warm-start cache are reset; cached shape handles stay valid; handle counters keep advancing. After :meth:clear, re-:meth:register_bodies before the bulk readers.

abstractmethod step(dt: float) None[source]

Advance the whole world once by a fixed timestep dt (seconds).

abstractmethod drain_contact_events() list[simvx.core.physics.world2d.ContactEvent2D][source]

Return and CLEAR this step’s buffered enter/exit contact events (T2d).

abstractmethod drain_overlap_events() list[simvx.core.physics.world2d.OverlapEvent2D][source]

Return and CLEAR this step’s buffered sensor-overlap events (T2d).

abstractmethod set_one_way(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool, normal: simvx.core.math.Vec2 = _DEFAULT_UP_2D) None[source]

Mark a body as a one-way platform (2D-only) (T2e).

When enabled, the body only collides with bodies approaching from the +normal side (landing on it); bodies passing up through it (moving along +normal) are not blocked.

Args: handle: Body handle. enabled: Whether one-way filtering is active. normal: World-space “solid side” normal (Vec2, unit); the side a lander must approach from. Defaults to +Y (a floor).

abstractmethod register_bodies(handles: list[simvx.core.physics.world2d.BodyHandle]) None[source]

Fix the body->row order used by the bulk readers.

After this call, :meth:read_transforms / :meth:read_velocities fill row i with the state of handles[i]. len(handles) is N.

abstractmethod read_transforms(out: numpy.ndarray) None[source]

Fill out with current body transforms, in place.

Args: out: Pre-allocated array of shape (N, 4), dtype float32, C-contiguous, where N matches the most recent :meth:register_bodies. Each row is [px, py, cos(theta), sin(theta)]: position xy followed by the rotation as a unit vector (cos/sin of the scalar angle, NOT the bare angle: interpolation lerps the unit vector with no +-pi wraparound). Row i corresponds to handles[i].

abstractmethod read_velocities(out: numpy.ndarray) None[source]

Fill out with current body velocities, in place.

Args: out: Pre-allocated array of shape (N, 3), dtype float32, C-contiguous, where N matches the most recent :meth:register_bodies. Each row is [lx, ly, omega]: linear velocity xy followed by scalar angular velocity (radians/s, CCW positive). Row i corresponds to handles[i].

abstractmethod raycast(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.RaycastHit2D | None[source]

Cast a ray and return the nearest hit, or None (T2d).

abstractmethod raycast_all(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) list[simvx.core.physics.world2d.RaycastHit2D][source]

Cast a ray and return EVERY hit within max_dist, sorted (T2d).

abstractmethod shapecast(shape: simvx.core.physics.world2d.ShapeHandle, origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.Contact2D | None[source]

Sweep a shape along a ray, return the earliest-TOI contact (T2d).

abstractmethod overlap(shape: simvx.core.physics.world2d.ShapeHandle, transform: Any, *, mask: int = 4294967295) list[simvx.core.physics.world2d.BodyHandle][source]

Return all bodies a static shape overlaps at transform (T2d).

abstractmethod move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.Contact2D | None[source]

Move a kinematic body by motion, stop at first contact (T2d).

abstractmethod create_character(shape: simvx.core.physics.world2d.ShapeHandle, transform: Any, *, up: simvx.core.math.Vec2 = _DEFAULT_UP_2D, slope_limit: float = math.radians(45.0), step_height: float = 0.0, skin_width: float = 0.001, collision_layer: int = 1, collision_mask: int = 4294967295) simvx.core.physics.world2d.CharacterHandle[source]

Create a 2D character controller bound to this world (T2e).

abstractmethod destroy_character(handle: simvx.core.physics.world2d.CharacterHandle) None[source]

Remove a character controller from the world (T2e).

abstractmethod set_character_transform(handle: simvx.core.physics.world2d.CharacterHandle, transform: Any) None[source]

Teleport a character to a new pose (no collision) (T2e).

abstractmethod character_transform(handle: simvx.core.physics.world2d.CharacterHandle) tuple[simvx.core.math.Vec2, float][source]

Read a character’s current pose as (position, rotation) (T2e).

abstractmethod character_move_and_slide(handle: simvx.core.physics.world2d.CharacterHandle, velocity: simvx.core.math.Vec2, dt: float, *, up: simvx.core.math.Vec2, max_slides: int = 4) simvx.core.physics.world2d.CharacterMoveResult2D[source]

Collide-and-slide a 2D character by velocity * dt (T2e).

__slots__

()

simvx.core.physics.world2d.__all__

[‘BodyMode’, ‘Capability’, ‘CombineMode’, ‘PhysicsMaterial’, ‘ContactPhase’, ‘RaycastHit2D’, ‘Contac…