simvx.core.physics.world2d

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 physics world 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.

Every method is declared @abstractmethod here, so a backend has a complete contract to implement and a partial backend fails loudly rather than silently returning nothing.

Physics2DWorld: the 2D backend transport interface.

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 interface (rather than the 3D one constrained to a plane) is deliberate: 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.

Module Contents

Classes

RaycastHit2D

Result of a successful 2D raycast against the world.

SweepHit2D

Result of a shape sweep stopping against a body.

ContactEvent2D

A node-agnostic 2D body-pair collision event emitted by a physics world.

OverlapEvent2D

A node-agnostic 2D sensor-overlap event emitted by a physics world.

Physics2DWorld

Abstract 2D backend interface: one isolated simulation world.

Functions

is_unit_scale_2d

True when scale is exactly (1, 1) and so changes no geometry.

normalise_body_scale_2d

Validate a 2D body scale against a shape kind, as float32 (2,).

Data

API

simvx.core.physics.world2d.BodyHandle

None

simvx.core.physics.world2d.ShapeHandle

None

simvx.core.physics.world2d.JointHandle

None

simvx.core.physics.world2d.is_unit_scale_2d(scale: numpy.ndarray) bool[source]

True when scale is exactly (1, 1) and so changes no geometry.

simvx.core.physics.world2d.normalise_body_scale_2d(scale: object, kind: str) numpy.ndarray[source]

Validate a 2D body scale against a shape kind, as float32 (2,).

The 2D twin of :func:~simvx.core.physics.world.normalise_body_scale, with the same rule: geometry defined by a single radius (a circle, a capsule, a thick segment) cannot express a non-uniform scale and raises rather than silently substituting a component, while a rectangle, a convex polygon and a segment soup scale componentwise. Negative components mirror, and uniformity is judged on magnitudes, so the scale.x = -1 sprite flip is uniform.

Args: scale: A Vec2 or any 2-sequence. kind: The backend’s own name for the geometry ("circle", "box", "capsule", "segment", …). Unknown kinds scale componentwise.

Returns: The scale as a float32 (2,) array, always fresh and never a view of the caller’s, for the reason the 3D twin gives.

Raises: ValueError: If either component is zero or non-finite, or if the geometry cannot represent the requested non-uniform scale.

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

Result of a successful 2D raycast against the world.

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.SweepHit2D[source]

Result of a shape sweep stopping against a body.

2D sibling of :class:~simvx.core.physics.world.SweepHit: 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). A sweep that begins already in contact still reports the surface’s separating normal, never the cast axis. distance: A distance along motion at which the mover’s shape is guaranteed NOT to penetrate the blocker. An exact time-of-impact backend reports max(0.0, toi - skin); a substepped backend reports a bound refined by bisection to |motion| / (substeps * 2**8) and ignores skin. It is therefore an under-estimate bounded by the backend’s sweep granularity, and may be exactly 0.0 for a sweep that begins in contact. Callers advance to exactly distance and subtract nothing further.

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 a physics world.

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

A contact event is emitted only for pairs with at least one DYNAMIC participant. Kinematic-vs-kinematic and kinematic-vs-static pairs cannot be pushed apart, produce no solver work, and produce no ContactEvent2D on any backend. Sensor overlap is the separate one-directional stream (see

Class:

OverlapEvent2D) and is unaffected.

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, and on the rare ENTER whose manifold carries no point at all (:func:~simvx.core.physics.capability.contact_manifold_payload). normal: Unit contact normal oriented a -> b (Vec2). Degenerate (Vec2(0)) on EXIT. impulse: Normal impulse magnitude the solver applied to the pair this step. 0.0 on EXIT and 0.0 on an ENTER the solver resolved with no impulse. None iff the backend does not advertise :attr:Capability.CONTACT_IMPULSE, which is a different fact from a measured 0.0. impulse_estimate: The portable stand-in for impulse, identical in meaning to :attr:~simvx.core.physics.world.ContactEvent.impulse_estimate: the impulse it would take to arrest the approach, computed the same way on every backend from the pair’s masses and the difference of the two bodies’ LINEAR velocities – not from the at-point rel_velocity below (:func:~simvx.core.physics.capability.contact_impulse_estimate). Always a number, 0.0 on EXIT and 0.0 for a pair that is not approaching. rel_velocity: Pre-solve velocity of b w.r.t. a AT THE CONTACT POINT (Vec2), spin included, identical in meaning to :attr:~simvx.core.physics.world.ContactEvent.rel_velocity: each body’s v + omega * perp(r) for the offset from its centre to :attr:point, differenced. A wheel spinning against the ground reports the speed of its tread, which its centre is not moving at. Where :attr:point is degenerate for want of a manifold point, this is the difference of the two bodies’ LINEAR velocities: there is nowhere to measure the spin at. 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

None

impulse_estimate: 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 a physics world.

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.Physics2DWorld(*, gravity: simvx.core.math.Vec2)[source]

Bases: abc.ABC

Abstract 2D backend interface: 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.

What this promises across backends

The same contract the 3D seam states, and it is stated there once for both dimensions: see :class:~simvx.core.physics.world.PhysicsWorld for the three promises (identical contract behaviour, numbers only within a documented tolerance, backend-dependent features and payload fields queryable as a

Class:

~simvx.core.physics.capability.Capability) and the two rules that follow from them. The seam is two disjoint ABCs because the payload types differ, not because the promise does.

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 checked by subclasses (see _check_transforms_out / _check_velocities_out, which raise ValueError: the buffer is caller input, so the check is not an assert). 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).

Shape contract (immutable values, owned by the resource that asked)

Identical to the 3D one stated on

Class:

~simvx.core.physics.world.PhysicsWorld, restated here so a 2D backend author never has to read the 3D interface: a shape handle names an IMMUTABLE VALUE and every create_* factory mints a FRESH one. The factories do not memoise on the geometry, because the world cannot know when a caller has finished with a handle and a geometry-keyed cache would only ever grow.

Sharing and lifetime belong to the Shape2D resource (:class:~simvx.core.physics.shapes2d.Shape2D), which builds its handle once per world and releases it when the resource is collected. A caller working against this interface directly owns what it creates and releases it with

Meth:

destroy_shape.

  1. There is no mutation API, and there must never be one. A hypothetical set_shape_radius would silently resize every body sharing the handle. Any future edit-the-geometry surface must BUILD A NEW HANDLE and hand it to

    meth:

    set_body_shape, which is what changing a collider does today.

  2. Destruction is about the handle, never about a body.

    meth:

    destroy_shape releases the world’s reference and leaves every body’s geometry alone.

Per-body SCALE belongs to the body, precisely because a shape is shared:

Meth:

create_body and :meth:set_body_transform take a scale and the backend applies it to its own instance of the geometry. Geometries that cannot express a non-uniform scale raise rather than approximate (see

Func:

~simvx.core.physics.world2d.normalise_body_scale_2d).

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

Scaled per body by the gravity_scale :meth:create_body takes.

property solver_iterations: int[source]

Impulse-solver iterations per :meth:step (>= 1).

See :attr:~simvx.core.physics.world.PhysicsWorld.solver_iterations; the 2D tiers honour it through the builtin velocity loop and Chipmunk’s space.iterations.

property position_iterations: int[source]

Rigid-joint position passes per :meth:step (>= 1).

See :attr:~simvx.core.physics.world.PhysicsWorld.position_iterations. The builtin 2D solver runs this many Baumgarte passes over its pins, hinges, welds and grooves; Chipmunk has no position solver, so the pymunk lane keeps the value and reads it back without running it.

property sleep_time_threshold: float[source]

Seconds of continuous sub-threshold motion before a body sleeps (> 0).

See :attr:~simvx.core.physics.world.PhysicsWorld.sleep_time_threshold.

property sleep_velocity_threshold: float[source]

Speed below which a body counts as at rest, m/s (>= 0).

See :attr:~simvx.core.physics.world.PhysicsWorld.sleep_velocity_threshold.

property contact_slop: float[source]

Contact overlap tolerated without correction, world units (>= 0).

See :attr:~simvx.core.physics.world.PhysicsWorld.contact_slop. A pixel-scale 2D game is the case this knob most exists for: at gravity 980 and 50-unit sprites, raise it to roughly 0.1-0.5.

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

Return the set of :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: a whole feature this interface has no methods for, a strengthened guarantee about a method every backend has, or a payload field that carries a value only where the backend can measure it. The default is the empty set, so a partially implemented backend claims nothing by accident; concrete backends override to list exactly what they honour, in either direction.

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 destroy_shape(shape: simvx.core.physics.world2d.ShapeHandle) None[source]

Release the world’s reference to a shape handle.

2D sibling of

Meth:

~simvx.core.physics.world.PhysicsWorld.destroy_shape, with the identical contract. Drops shape from the backend’s shape table: a backend that owns a native shape object frees it here, a pure-Python one simply forgets the record. This is what a

Class:

~simvx.core.physics.shapes2d.Shape2D resource calls for each handle it owns when it is collected, and the escape hatch for a caller working against this interface directly.

Bodies are not touched. One handle is routinely shared by many bodies, so destroying it cannot mean “take the collider away from whoever is using it”: every body created with this shape keeps its geometry and keeps simulating. It neither raises nor detaches. As in 3D, “keeps simulating” covers the full surface: the body stays editable through every set_body_* setter and sweepable through

Meth:

sweep_body, so a backend must reach a body’s geometry through the record that body owns rather than by re-indexing its shape table.

An unknown handle is a silent no-op, so destroying twice or destroying after :meth:clear is safe in any teardown order. The HANDLE is invalid afterwards: passing it to :meth:create_body, :meth:set_body_shape,

Meth:

shapecast or :meth:overlap raises KeyError, and is a caller error. KeyError on every backend and every one of those four calls, matching what a bogus BODY handle already raises.

Args: shape: A handle previously returned by one of the create_* shape factories.

abstractmethod create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: Any, *, mass: float = 1.0, scale: simvx.core.math.Vec2 | None = None, can_sleep: bool = True, linear_damping: float = DEFAULT_LINEAR_DAMPING, angular_damping: float = DEFAULT_ANGULAR_DAMPING, gravity_scale: float = DEFAULT_GRAVITY_SCALE, collision_layer: int = 1, collision_mask: int = 4294967295, is_sensor: bool = False, material: simvx.core.physics.material.PhysicsMaterial | None = None, 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, in the form every transform argument on this interface accepts: a Transform2D (.position + .rotation), a bare Vec2 / sequence (position only, zero rotation), or a (position, rotation) pair where rotation is any real scalar in radians. numpy floats are real scalars here, since the engine’s own maths is float32; the pair must be a tuple, because a 2-element list or Vec2 is a bare position. mass: Body mass in kg, > 0 (a non-positive or NaN mass raises ValueError, whatever the mode). Consulted while the body is DYNAMIC and RETAINED across :meth:set_body_mode, so a body created STATIC or KINEMATIC carries this mass into a later DYNAMIC flip. STATIC and KINEMATIC integrate as infinite-mass regardless of the value. scale: Per-body scale of shape (Vec2); None (the default) means unscaled. Scale belongs to the body rather than the shape because a shape handle is shared, so one collider resource serves bodies at any number of sizes: a body at scale=4 collides as the four-times-larger collider, which is what makes a scaled node’s collider match what is drawn. Geometry that cannot express a non-uniform scale RAISES rather than approximating (see :func:normalise_body_scale_2d); negative components mirror. Editable afterwards through :meth:set_body_transform. can_sleep: Whether this body is ALLOWED to fall asleep once it settles (see :meth:sleeping). Defaults True; False keeps it permanently simulated. Editable through :meth:set_body_can_sleep. linear_damping: Per-second rate at which the body sheds linear speed with nothing touching it, applied as v = v * max(0, 1 - linear_damping * dt) + a * dt (see :func:~simvx.core.physics.world.normalise_damping, which also records where Jolt’s integrator differs under sustained acceleration). Defaults to :data:~simvx.core.physics.world.DEFAULT_LINEAR_DAMPING. Editable through :meth:set_body_damping. angular_damping: The same rate for spin. Defaults to :data:~simvx.core.physics.world.DEFAULT_ANGULAR_DAMPING, and is independent of linear_damping. gravity_scale: Multiplier on the world’s gravity for this body alone. 1 (the default) falls normally, 0 ignores gravity, and a negative value falls upward. Editable through :meth:set_body_gravity_scale. 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, never blocks a shape sweep, feeds the separate overlap stream (one-directional sensor.mask & other.layer). material: The body’s SURFACE (friction, restitution, combine modes) as one :class:PhysicsMaterial resource, shareable across bodies. None (the default) uses :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL. The resource is frozen; a live body’s surface changes by passing a different one to :meth:set_body_material. 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.

An unknown handle is a silent no-op, the same contract

Meth:

destroy_shape follows, so destroying twice or destroying after

Meth:

clear is safe in any teardown order.

Destruction ENDS every contact and sensor overlap the body was in, so the event streams report one EXIT per open edge rather than dropping it: a listener is never left believing a pair is still in contact (the same rule

Meth:

set_body_filter follows). The events surface in the first

Meth:

drain_contact_events / :meth:drain_overlap_events after the next

Meth:

step, and never twice. Their payload is the ordinary degenerate EXIT payload, and the destroyed handle they carry is an identity to match against a caller’s own bookkeeping, never an argument for a further world call.

Destruction also WAKES every body that was in contact with this one, so whatever it was holding up falls (see :meth:sleeping).

abstractmethod set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: Any, *, scale: simvx.core.math.Vec2 | None = None, wake: bool = True) None[source]

Teleport a body to a new world transform, optionally rescaling it.

Wakes the body, and a pose that really moves it also wakes the bodies it was in contact with, so a platform driven out from under a sleeping crate drops the crate (see :meth:sleeping). Re-writing the pose a body already holds moves nothing and wakes nothing.

Args: handle: Body handle. transform: New world transform, in the forms this interface accepts. scale: New per-body scale of the body’s shape (Vec2), or None (the default) to leave the scale it already has alone. None rather than (1, 1) is load-bearing: a character controller re-writes its body’s pose every step and knows nothing about scale. Rescaling is a geometry change, so it wakes the body’s neighbours exactly as :meth:set_body_shape does. wake: Whether this write may disturb sleepers (see :meth:sleeping). False re-poses the body without waking it or anything it was holding up: streaming a level in, or repositioning a pooled object, must not re-activate a settled pile.

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.

A body the caller made STATIC is never set in motion by this call, on either backend: an immovable body stays where it was put, and the way to move one is :meth:set_body_transform. The write is still remembered and read back, on both backends. The pymunk adapter holds a STATIC sensor KINEMATIC (see

Attr:

~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATIC), which Chipmunk WOULD integrate, so for that one case the value is kept by the adapter instead of being given to the library, and becomes the live velocity if the body later leaves STATIC. A static body’s velocity is also read as a surface velocity by the friction solve on both backends, which is how a conveyor belt is expressed.

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, *, wake: bool = True) 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 restores the mass the body was created with and recomputes the per-shape moment from it. Flips are lossless, so freezing a body to STATIC and waking it later gives back exactly its original mass and moment.

A flip to DYNAMIC wakes the bodies this one was in contact with: it can no longer hold anything up, so what rested on it must fall (see

Meth:

sleeping). Freezing a body to STATIC takes nothing away and leaves a sleeping neighbour asleep, and re-asserting the mode a body already has does nothing at all.

wake governs both halves of that, and the two directions of the flip are not symmetric:

  • LEAVING DYNAMIC ends the body’s own sleep whatever wake says, because an immovable body is never asleep (see :meth:sleeping). That restores the invariant rather than waking anything, so it is not suppressible.

  • ENTERING DYNAMIC with wake=False hands the body to the simulation PARKED: DYNAMIC and asleep, its velocity zeroed, costing nothing until something disturbs it (:meth:wake, a pose write, an impulse, or an awake body arriving at it). That is what streaming a section in wants, its geometry landing settled rather than paying to fall into place. The default hands the body over awake, moving from the next step.

  • A body whose can_sleep is False has no parked state to be handed to, so it is freed AWAKE whichever way wake points: forbidding sleep forbids it by this route too.

  • Entering KINEMATIC parks nothing. A KINEMATIC body never reports as asleep, and it moves only when it is written to, which is itself a disturbance. So it stays in the pair and overlap search whichever way wake points: a platform streamed in and flipped to KINEMATIC where it stands still fires the triggers it is standing in.

Parking needs a backend that advertises :attr:Capability.SLEEP. Without it nothing can be held out of the simulation, so a body freed with wake=False is simulated from the very next step.

Args: handle: Body handle. mode: The new :class:BodyMode. wake: Whether this write may disturb sleepers (see :meth:sleeping), and whether a body entering DYNAMIC is handed over awake.

Raises: ValueError: If the stored mass is not > 0 and mode is DYNAMIC, or if the body’s shape kind forbids mode (a concave polygon is STATIC-only).

abstractmethod set_body_mass(handle: simvx.core.physics.world2d.BodyHandle, mass: float, *, wake: bool = True) None[source]

Set a live body’s mass, recomputing its moment from its current shape.

Velocity is preserved, not momentum. The new mass is RETAINED like the create-time one, so it survives a later :meth:set_body_mode flip and setting it on a STATIC or KINEMATIC body takes effect when that body becomes DYNAMIC.

A re-massed body is also a support whose behaviour has changed under whatever is resting on it, so the wake reaches its neighbours too.

Args: handle: Body handle. mass: New mass in kg, > 0. wake: Whether this write may disturb sleepers (see :meth:sleeping).

Raises: ValueError: If mass is not > 0 (NaN included).

abstractmethod set_body_filter(handle: simvx.core.physics.world2d.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]

Set a live body’s 32-bit layer membership and collision mask together.

One method for the pair because the acceptance rule reads both sides of both bodies. Honoured from the next :meth:step, and the event streams follow it: a pair that was touching and no longer matches reports an EXIT rather than vanishing silently, one that newly matches reports an ENTER, and an edit that changes no verdict reports nothing at all.

Args: handle: Body handle. collision_layer: New 32-bit layer membership. collision_mask: New 32-bit mask of layers this body scans. wake: Whether this write may disturb sleepers (see :meth:sleeping).

abstractmethod set_body_material(handle: simvx.core.physics.world2d.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]

Replace a live body’s surface material.

The whole surface at once, because that is what a material IS. See

Meth:

~simvx.core.physics.world.PhysicsWorld.set_body_material: the world reads the resource’s values and does not retain it, which is safe because the frozen resource has no later edit to miss.

Args: handle: Body handle. material: The new surface, or None for :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL.

abstractmethod set_body_damping(handle: simvx.core.physics.world2d.BodyHandle, linear: float, angular: float) None[source]

Set a live body’s linear and angular damping together.

See :meth:~simvx.core.physics.world.PhysicsWorld.set_body_damping.

Args: handle: Body handle. linear: Per-second linear damping rate (>= 0, finite). angular: Per-second angular damping rate (>= 0, finite).

Raises: ValueError: If either rate is negative, NaN or infinite.

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

Set a live body’s gravity multiplier.

Wakes the body, for the reason

Meth:

~simvx.core.physics.world.PhysicsWorld.set_body_gravity_scale gives: gravity is applied by integration, which skips a sleeper.

Args: handle: Body handle. scale: New multiplier on the world’s gravity for this body.

Raises: ValueError: If scale is NaN or infinite.

abstractmethod set_body_continuous(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]

Turn continuous collision detection on or off for a live body.

Honoured only where the backend advertises

Attr:

~simvx.core.physics.capability.Capability.CONTINUOUS; a backend that does not accepts the call and does nothing, exactly as it already ignores the continuous argument to :meth:create_body.

Args: handle: Body handle. enabled: True for continuous (swept) integration, False for discrete.

abstractmethod set_body_shape(handle: simvx.core.physics.world2d.BodyHandle, shape: simvx.core.physics.world2d.ShapeHandle, *, wake: bool = True) None[source]

Swap a live body’s collision shape, keeping everything else.

The body keeps its handle, pose, velocity, mode, mass, filter, sensor flag, material, CCD flag and one-way configuration; only the geometry changes, with the moment of inertia recomputed from the new shape at the retained mass. An overlap the new shape introduces is not resolved at swap time: the ordinary contact solve pushes it apart over the following steps, which is why the body wakes.

The event streams describe the geometry, not the swap: a pair the new shape still touches is NOT announced again, a pair it no longer reaches reports an EXIT, and one it newly reaches reports an ENTER, so replacing a collider with an identical one is silent.

The swap also vacates whatever volume the old geometry held, so the wake reaches the body’s neighbours as well: shrinking a platform under a sleeping crate drops the crate rather than leaving it on a ledge that is no longer there.

Args: handle: Body handle. shape: An opaque shape handle from one of the create_* factories. wake: Whether this write may disturb sleepers (see :meth:sleeping).

Raises: ValueError: If the new shape’s kind forbids the body’s current mode (a concave polygon is a STATIC-only collider).

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 body_mass(handle: simvx.core.physics.world2d.BodyHandle) float[source]

Read a body’s EFFECTIVE mass in kg, the one an impulse divides by.

The counterpart of :meth:set_body_mass, and deliberately not a read-back of what was set: a STATIC or KINEMATIC body is infinite-mass whatever mass it was created with, so it answers math.inf and an impulse applied to it moves nothing. Flipping the same body to DYNAMIC restores its retained mass and this returns that.

Returns: Mass in kg for a DYNAMIC body, math.inf for an immovable one.

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, and a body flipped off DYNAMIC stops reporting as asleep at once, whatever wake= the flip was given – an immovable body has no motion to resume, so clearing the flag restores this invariant rather than waking anything.

A sleeper is woken by anything the solver would otherwise let it miss: a contact, an impulse or force, a write to its own pose, velocity, mode or any of the live-edit setters, AND a change to a body it was in contact with that takes support away. Destroying a body, teleporting it, changing its geometry or its mass, or flipping it to DYNAMIC therefore wakes whatever was resting on it; without that a crate whose support has gone hangs in mid-air for the life of the world, because the ordinary wake-on-contact needs a contact that still exists. Writes that change nothing (re-posing a parked platform where it already is, re-asserting the mode a body already has) wake nothing, so a game may drive both every frame.

Sleep and wake are ISLAND-ATOMIC. A pile parks as a unit: no body commits until every DYNAMIC body it touches, and everything those touch in turn, has come to rest as well. A disturbance that wakes any member of a sleeping island wakes all of it, so nothing a change took support from is left asleep however deep the pile: pull the bottom crate out of a settled stack and the whole stack falls, not just the crate that was on it. What a pile rests ON is in nobody’s island (STATIC and KINEMATIC bodies end the walk), so two piles sharing a floor sleep and wake independently.

Asleep is FROZEN. A sleeping body’s pose is the pose it fell asleep at, every step it stays asleep, so anything that reads a settled scene – a save, a placement check, a golden image – reads the same numbers until something wakes it.

That is an invariant of this interface rather than a courtesy each backend remembers: every mutator that can take support away (:meth:set_body_transform, :meth:set_body_mode, :meth:set_body_mass,

Meth:

set_body_filter, :meth:set_body_shape, :meth:set_one_way) wakes the body and its neighbours. All but :meth:set_one_way take wake=False to suppress it for the cases where a write is bookkeeping rather than a disturbance: streaming a level in, respawning a pooled object, or an editor writing a value the player cannot feel. Suppression is opt-IN because getting it wrong the other way strands a body in mid-air, which no later step can recover.

Meth:

apply_impulse, :meth:apply_force, :meth:apply_torque and

Meth:

set_one_way have no such flag, because changing what the solver does with the body is the whole point of each of them; a

Meth:

set_one_way that changes nothing is not a disturbance in the first place, and wakes nothing.

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

Wake a sleeping body, whatever its sleep timer had reached.

Reaches only the body it names: the island rule belongs to a change that takes support away, and this call takes nothing away. A body that is already awake, and one that is STATIC or KINEMATIC and so was never asleep, are both a no-op.

Args: handle: Body handle.

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

Put a body to sleep now, without waiting for it to settle.

Freezes the body where it is: skipped by integration and the contact velocity solve until something wakes it, while staying a full collider that still reads back its (frozen) pose. Use it to park a pile a game knows is finished rather than paying for it to come to rest first.

A no-op on a body that is STATIC or KINEMATIC (never awake), and on one whose can_sleep is False: forbidding sleep means forbidding it.

Args: handle: Body handle.

abstractmethod set_body_can_sleep(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]

Allow or forbid this body ever falling asleep.

The create-time can_sleep argument, live. Forbidding it wakes the body if it was asleep; allowing it again lets the body settle from the current step, with no credit for time already spent at rest.

A body that may not sleep is simulated every step for the life of the world. That is the point of it, and it is also the cost: it will show up in a profile as a body that never leaves the integrate and solve sets, which is correct rather than a defect.

Args: handle: Body handle. enabled: True to allow sleeping (the default), False to forbid it.

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.

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.

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.

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.

The current offset and relative angle are captured in a’s frame at create and held there, so the two move as one rigid assembly and the whole assembly swings round when a turns.

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.

The two bodies cannot separate at anchor but turn freely about it, so chaining pins builds a rope and pinning to a STATIC body builds a pendulum. The anchor is captured at create as a point in EACH body’s own frame and turned back into world axes by that body’s current angle every solver pass, so a pin on a spinning body orbits with it.

Built-in backend caveat: convergence is a few sequential-impulse iterations, so a loaded chain sags and a hard yank stretches a link before it is pulled straight; the pymunk backend hangs the same chain on its rest length. See docs/core/physics_backends.md for the measured sag.

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.

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

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

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. Both stay in the frames they were given in, so the rail turns with a.

What every backend promises is the anchor on the rail: anchor_b, placed by b’s own current rotation, stays on the segment a’s current rotation puts there, however either body is turning. That is the whole of the cross-backend contract, and the 2D contract suite measures it on both implementations.

What is NOT promised is the slider’s own rotation. A groove leaves it free, so what the slider ends up pointing at is whatever each solver’s residual torque about the anchor made of it, and the two backends part company: on the suite’s own scenario – a carrier turning at 1 rad/s under a slider spun at 3 rad/s about an anchor a quarter of a unit off its centre – they are 0.33 rad apart after two seconds and 3.14 rad apart after ten, with both anchors still on the rail. A scene that needs the slider held at an angle welds or pins it rather than reading a heading off the groove.

Motion along the groove is free and no backend drives it: a slider is pushed by forces on b. The two backends stop it at the endpoints with different firmness, measured in docs/core/physics_backends.md.

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

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

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 and joint, emptying the world.

2D sibling of :meth:~simvx.core.physics.world.PhysicsWorld.clear. 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.

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

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

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

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.

Turning one-way ON takes support away from whatever was resting on the side the platform stops resolving, so it wakes the body’s neighbours as well as the body, exactly as :meth:set_body_filter does (see

Meth:

sleeping). A sleeping stack on a platform that becomes pass-through therefore falls, instead of hanging above geometry that no longer holds it. Any REAL change wakes the same set – turning one-way off or editing the normal adds support rather than takes it, but the bodies affected still need a step awake to settle against the new rule. A write that changes neither the flag nor the normal wakes nothing, so a game may drive this every frame.

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.

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.

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.SweepHit2D | None[source]

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

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.

Every body in the table is visible here, including a KINEMATIC character body.

abstractmethod sweep_body(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2, *, from_transform: tuple[simvx.core.math.Vec2, float] | None = None, skin: float = 0.0) simvx.core.physics.world2d.SweepHit2D | None[source]

Cast a body’s shape along motion and report the first blocker.

2D sibling of :meth:~simvx.core.physics.world.PhysicsWorld.sweep_body, with the same non-mutating contract and the same blocking predicate: not the mover, not a sensor, the canonical AND layer/mask rule, the 2D one-way filter, and an opposing contact normal (dot(motion_dir, separating_normal) < -1e-4). A contact reported at distance zero must still carry a true surface normal, never the cast axis.

Args: handle: Body handle of the mover. motion: World-space displacement to sweep along (Vec2). from_transform: Optional (position, rotation) pose to cast from instead of the body’s own stored pose; rotation is a scalar in radians. skin: Contact clearance to leave at the blocker, in world units; ignored by a substepped backend (see :attr:SweepHit2D.distance).

Returns: The nearest blocking :class:SweepHit2D, or None when clear.

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

Move a kinematic body by motion, stop at the first contact.

Concrete composition of :meth:sweep_body and :meth:set_body_transform, identical on every backend: sweep, then advance to exactly SweepHit2D.distance along motion (the full motion when clear). The collide-and-slide policy for a character body lives in simvx.core.physics.slide, not here.

__slots__

()

simvx.core.physics.world2d.__all__

[‘BodyMode’, ‘DEFAULT_ANGULAR_DAMPING’, ‘DEFAULT_GRAVITY_SCALE’, ‘DEFAULT_LINEAR_DAMPING’, ‘Capabili…