simvx.core.physics.builtin.world2d¶
Narrowphase¶
Exact circle-circle, ROTATION-honouring circle-box (a 2D improvement over the 3D AABB approximation), circle/capsule segment reductions, and the keystone poly-poly SAT + Sutherland-Hodgman edge clipping (box and segment route through the poly path as degenerate polys). Concave (static edge soup) is broadphase-AABB-culled then tested per candidate segment. The solver mirrors the 3D structure with 2D scalar-cross-product angular terms: the 2D system carries a real scalar moment of inertia, so contacts apply linear AND angular impulse, unlike the linear-only 3D basic tier.
Dynamics¶
forces: apply_force / apply_torque / apply_impulse (force/torque accumulate and clear each step; impulse is instantaneous; the lever-arm variants add a scalar angular term via the 2D cross product).
joints: create_fixed / pin / hinge / spring / groove joint + remove_joint, solved in the SAME velocity loop as contacts plus a Baumgarte position pass. A 2D hinge equals a pin here since 2D rotation is 1-DOF, and there is no motor or limit. The groove is a 1-DOF slider-on-a-line.
sleeping: a settled DYNAMIC body sleeps after sleep_time_threshold and wakes at every disturbance (set / force / impulse / an awake contact / a joint or spring whose OTHER end moved since the previous step).
CCD:
continuousbodies centre-sweep vs STATIC and clamp to the TOI.
Basic-tier honesty¶
Joints converge in a few iterations (small solver_iterations / position_iterations) and stiff springs are soft. A loaded chain really sags: six 1 kg beads at 1200 units/s^2 settle with the worst link 22.0% long here and the tip 341 units down on 300 units of chain, where pymunk settles the same chain on its rest length. Anchoring is exact on both – what differs is how hard the constraint pulls. Motors, angular limits and breakable joints are in the 2D seam on NEITHER backend.
CCD is a CENTRE sweep vs STATIC only (no rotational / dynamic-vs-dynamic sweep); a fast body grazing a corner can still tunnel it. pymunk honours it properly.
SAT resolves a DEEP interpenetration along the minimum-overlap axis, which is the shortest way OUT rather than the way the body came in. Once the penetration passes half the two shapes’ summed extent along that axis, the shortest way out is the FAR side, so a pair that was solidly touching at the previous step is recovered out the face its remembered contact normal names (a resting body teleported into its support, a collider grown through its floor). A pair with NO solid history keeps the blind minimum deliberately: the entry face of a fresh deep overlap is not knowable (a tunnelled arrival and an unstick shove present the same state and need opposite answers), so past the midplane a history-less pair still exits the far face. The manifold points are the clipped incident edge, so on a deep overlap they can also sit outside the reference collider, which makes the contact lever arms approximate there. A full clipping-with-feature-caching manifold and true deep-overlap handling are deferred to the pymunk (Chipmunk2D) backend.
Warm-starting / accumulated-impulse caching: each contact’s per- manifold-point normal + tangent impulse is cached by the persistent body-pair id and applied before the iteration loop (standard Box2D technique), so a tall stack converges in a couple of iterations instead of rebuilding its support from zero. Full feature-id manifold persistence (vs the point-index keying used here) is the pymunk concern.
Capsule / poly / segment / concave scalar moments are the AABB-box estimate (
_moment_from_aabb); the exact per-shape composite is a pymunk concern.Concave broadphase is a linear AABB scan over candidate segments (a BVH is deferred to pymunk), and concave is STATIC-ONLY: it is never the moving shape.
BuiltinPhysics2D world: the pure-Python 2D physics backend.
Concrete :class:~simvx.core.physics.world2d.Physics2DWorld implementation, pure
Python (numpy only). The 2D sibling of
- class:
~simvx.core.physics.builtin.world.BuiltinPhysics: the engine’s default, always-available 2D backend and the behavioural parity target for the optional native pymunk (Chipmunk2D) backend.
Mechanism: semi-implicit (symplectic) Euler integration, scalar rotation, real per-shape scalar moment of inertia. Where it simplifies versus a production solver the simplification is commented and the pymunk backend is named as the exact alternative; nothing silently fakes a result. This solver is a tier above its 3D sibling, refuses no shape pair, and is not covered by that module’s honesty list.
The GEOMETRY is written for clarity over raw throughput. What is optimised is how OFTEN it runs: a sweep-and-prune broad phase proposes only the pairs whose world AABBs meet, and a pair whose bodies have not moved replays its last answer instead of re-deriving it. 2D needs both more than 3D does, because a 2D contact costs more: this solver carries a real moment of inertia, so every contact point has a lever arm and a Coulomb friction cone where the 3D tier applies one linear impulse at the centre of mass.
step() runs integrate -> CCD -> collide -> solve (contacts + joints) ->
position-correct -> sleep, mirroring
- meth:
~simvx.core.physics.builtin.world.BuiltinPhysics.step. EveryPhysics2DWorldmethod is implemented: there are no stubs.
Module Contents¶
Classes¶
Pure-Python default 2D backend (basic tier). |
Data¶
API¶
- class simvx.core.physics.builtin.world2d.BuiltinPhysics2D(*, gravity: simvx.core.math.Vec2 | None = None)[source]¶
Bases:
simvx.core.physics.world2d.Physics2DWorldPure-Python default 2D backend (basic tier).
See :class:`~simvx.core.physics.world2d.Physics2DWorld` for the full contract. Implements the COMPLETE interface: shapes + bodies + integrator + bulk readers
, narrowphase + sequential-impulse solver, forces + joints + sleeping + CCD, queries + events + the swept body primitive, one-way platforms, and the groove joint. No method is a stub.
Initialization
Initialise the world.
Args: gravity: World gravity acceleration vector (
Vec2), metres/s^2. Y-up:Vec2(0, -9.81)is “down”.- capabilities() frozenset[simvx.core.physics.capability.Capability][source]¶
Advertise the measured contact impulse and continuous collision.
2D sibling of :meth:
BuiltinPhysics.capabilities: this backend owns its sequential-impulse solver, so the contact-event impulse is the converged normal impulse it actually applied, and its integrator honours theCONTINUOUSflag by sweeping a flagged body against STATIC geometry, andSLEEPby parking a settled DYNAMIC body.SENSOR_DETECTS_STATICholds for the same reason it does in 3D: the sensor sweep considers every body the mask admits, not only the ones that can move. No determinism, vehicles or soft bodies. Explicit (not inherited) so the claim is a deliberate, tested promise.
- clear() None[source]¶
Remove every body and joint, emptying the world.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.clear. 2D sibling of- Meth:
BuiltinPhysics.clear: resets the body / joint tables and the per-step edge-diff + warm-start caches. Gravity, shapes, and handle counters are intentionally NOT reset.
- create_circle(radius: float) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_box(half_extents: simvx.core.math.Vec2) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_capsule(radius: float, height: float) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_segment(a: simvx.core.math.Vec2, b: simvx.core.math.Vec2, radius: float = 0.0) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_convex_polygon(points: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_concave_polygon(segments: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
- destroy_shape(shape: simvx.core.physics.world2d.ShapeHandle) None[source]¶
Release this world’s record of a shape handle.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.destroy_shape. 2D sibling of :meth:BuiltinPhysics.destroy_shape: nothing native to free, so the record is simply dropped from the shape table. Bodies built from it hold their own_Shape2Drecord directly and are unaffected. Unknown handles are a silent no-op.
- create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: object, *, 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]¶
- destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: object, *, scale: simvx.core.math.Vec2 | None = None, wake: bool = True) None[source]¶
- set_body_velocity(handle: simvx.core.physics.world2d.BodyHandle, linear: simvx.core.math.Vec2, angular: float = 0.0) None[source]¶
- set_body_mode(handle: simvx.core.physics.world2d.BodyHandle, mode: simvx.core.physics.world.BodyMode, *, wake: bool = True) None[source]¶
- set_body_mass(handle: simvx.core.physics.world2d.BodyHandle, mass: float, *, wake: bool = True) None[source]¶
- set_body_filter(handle: simvx.core.physics.world2d.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]¶
- set_body_material(handle: simvx.core.physics.world2d.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]¶
- set_body_damping(handle: simvx.core.physics.world2d.BodyHandle, linear: float, angular: float) None[source]¶
- set_body_gravity_scale(handle: simvx.core.physics.world2d.BodyHandle, scale: float) None[source]¶
- set_body_continuous(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
- set_body_shape(handle: simvx.core.physics.world2d.BodyHandle, shape: simvx.core.physics.world2d.ShapeHandle, *, wake: bool = True) None[source]¶
- body_velocity(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
- body_transform(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
- body_mass(handle: simvx.core.physics.world2d.BodyHandle) float[source]¶
- wake(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- sleep(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- set_body_can_sleep(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
- sleeping(handle: simvx.core.physics.world2d.BodyHandle) bool[source]¶
- drain_contact_events() list[simvx.core.physics.world2d.ContactEvent2D][source]¶
- drain_overlap_events() list[simvx.core.physics.world2d.OverlapEvent2D][source]¶
- register_bodies(handles: list[simvx.core.physics.world2d.BodyHandle]) None[source]¶
- 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_force(handle: simvx.core.physics.world2d.BodyHandle, force: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None) None[source]¶
- apply_torque(handle: simvx.core.physics.world2d.BodyHandle, torque: float) None[source]¶
- create_fixed_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle) simvx.core.physics.world2d.JointHandle[source]¶
- 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]¶
- 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]¶
- 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]¶
- 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’sanchor_bto slide alonga’s groove segment.groove_a/groove_bare body-LOCAL points ina’s frame defining the groove segment;anchor_bis a body-local point inb’s frame. They are stored exactly as given – the constraint records keep every arm in its own body’s frame (see :class:_GrooveConstraint2D) – so the rail turns witha. The groove direction must be non-zero.
- remove_joint(handle: simvx.core.physics.world2d.JointHandle) None[source]¶
- 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.
Stores the enabled flag and the world-space pass-through (“solid side”) normal on the body. When enabled, the contact filter (:meth:
_one_way_rejects, applied in :meth:_collideand :meth:sweep_body) keeps a contact only when the other body lands from the+normalside and discards it when the other body passes up through. The normal is normalised (a degenerate zero normal falls back to+Y, a floor).A platform that stops resolving contacts from one side has taken support away from whatever was resting on that side, so a real change here is a support-taking mutation like a filter edit or a shape swap: the wake reaches the body’s sleeping neighbours and not only the platform itself. Without it a settled stack would hang in the air above a platform that no longer holds it, with no later step able to notice. Any real change takes the same path, including the off direction and a normal edit, which ADD support rather than take it: the affected bodies still need a step awake to settle against the new rule. A write that changes neither the flag nor the normal takes nothing away and wakes nothing, so a game may drive this every frame.
- 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, return the nearest body whose layer matches
mask.2D sibling of the 3D
raycast: a single query-mask convention (mask & body.collision_layer: the OBSERVER decides, unlike the body-body AND rule). Reuses the_raycast_body_2dper-shape helpers (the exact analytic casts CCD already needs) over every body; the nearest positivetwithinmax_distwins. An infinitemax_distis fine (the casts are analytic). ReturnsNoneon a clean miss.
- 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, return EVERY hit within
max_distsorted by distance.2D sibling of the 3D
raycast_all: the same per-body_raycast_body_2dcast as :meth:raycast, but collects all hits and sorts by distance (so the first element is the nearest). Single query-mask convention.
- 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
shapefromoriginalongdirection, earliest-TOI contact.Basic-tier honesty: a SUBSTEPPED sweep (mirrors the 3D
shapecast), not a true continuous TOI: a transient probe body is advanced along the ray and the first substep that the narrowphase reports penetrating an OTHER body is the earliest hit; the probe backs off to the previous non-penetrating substep and reports the contact. Substep count is sized from the probe’s smallest feature, capped at 64. A fast sweep past a very thin collider can tunnel between substeps; an analytic shape-cast (conservative advancement) is deferred to the pymunk backend. Single query-mask convention (mask & body.layer). Concave shapes are STATIC-only and cannot be the moving probe (rejected). ReturnsNonewhen the sweep stays clear.
- overlap(shape: simvx.core.physics.world2d.ShapeHandle, transform: object, *, mask: int = 4294967295) list[simvx.core.physics.world2d.BodyHandle][source]¶
All bodies overlapping
shapeattransform, sorted by handle.2D sibling of the 3D
overlap: places a transient probe body attransform(aTransform2D/ bare position /(position, rotation)pair, via the same_unpack_transform) and returns every body it overlaps whoselayer & maskis set. Broadphase AABB cull then the exact narrowphase, mirroring_collide’s prefilter. Sorted by handle for determinism. Concave shapes are STATIC-only and cannot be the probe.
- 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]¶
Substepped, non-mutating sweep of a body’s shape (basic tier).
2D sibling of :meth:
~simvx.core.physics.builtin.world.BuiltinPhysics.sweep_bodyand the primitive the collide-and-slide policy is built on. Basic-tier honesty: a SUBSTEPPED narrowphase sweep (the narrowphase is analytic overlap, not a continuous TOI), sized from the mover’s smallest feature and capped at 64 substeps; the substep that penetrates a blocking body brackets the contact, and the bracket is BISECTED against that blocker alone, sodistanceis a bound good to|motion| / (substeps * 2**8)rather than to one whole substep. It is still a lower bound and may be exactly0.0for a sweep that begins in contact. A fast mover vs a very thin collider can still tunnel between substeps (an analytic sweep is a pymunk concern).Structurally non-mutating: the mover’s shape and pose are wrapped in a TRANSIENT
_Body2Dprobe, so nothing in the body table is touched andfrom_transformcosts nothing.skinis accepted and IGNORED (the substep quantum already exceeds any sane skin).A body blocks only when it is not the mover, is not a sensor, passes the canonical AND layer/mask rule, is not rejected by the one-way filter, and presents a contact that OPPOSES the sweep. The one-way test runs BEFORE the opposition test and honours a one-way MOVER as well as a one-way blocker, so a body sweeping UP through a one-way platform passes and a top landing is blocked. Sensors take no part in collision resolution, so one never blocks a sweep and (because the ground and step-up probes route through here) never counts as ground or as a step surface. A non-opposing touch, e.g. the floor a character already rests on while it walks, is a touch and not a blocker: reporting it would halt every slide at distance
0. Fraction0is never sampled (the substep loop starts ats = 1), so this backend cannot report the cast axis as a normal.
- property gravity: simvx.core.math.Vec2¶
- property solver_iterations: int¶
- property position_iterations: int¶
- property sleep_time_threshold: float¶
- property sleep_velocity_threshold: float¶
- property contact_slop: float¶
- move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.SweepHit2D | None¶
- __slots__¶
()
- simvx.core.physics.builtin.world2d.__all__¶
[‘BuiltinPhysics2D’]