simvx.core.physics.builtin.world2d¶
Role¶
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.
This is the basic tier: semi-implicit (symplectic) Euler integration, scalar rotation, real per-shape scalar moment of inertia. Where the basic approach simplifies versus a production solver, the simplification is commented and the eventual pymunk backend is named; nothing silently fakes a result.
Stage T2b scope¶
This sub-stage adds the narrowphase + sequential-impulse contact solver on top of
the T2a integrator: step() is now integrate -> collide -> solve -> position-
correct (mirroring the 3D :meth:~simvx.core.physics.builtin.world.BuiltinPhysics.step).
The 2D narrowphase is genuinely new code: 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).
Stage T2c scope¶
This sub-stage adds forces, joints, sleeping, and CCD on top of the T2b solver:
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 == pin this tier since 2D rotation is 1-DOF; motor / limit is a follow-on. The groove is a 1-DOF slider-on-a-line, the last sub-stage T2g.)
sleeping: a settled DYNAMIC body sleeps after _SLEEP_TIME and wakes at every disturbance (set / force / impulse / a live joint / an awake contact).
CCD:
continuousbodies centre-sweep vs STATIC and clamp to the TOI.
Subsequent sub-stages filled the remaining seam: queries / events / kinematic
sweep (T2d), one-way platforms + the character controller (T2e), the tree / node
integration (T2f), and the groove joint (create_groove_joint, the last sub-stage
T2g, with the GUI demo). No NotImplementedError seam stubs remain: every
Physics2DWorld method is implemented.
The class is fully instantiable; the integrate -> CCD -> collide -> solve (contacts
joints) -> correct -> sleep path (create_body / forces / joints -> step -> read_*) is complete.
Basic-tier honesty (T2c)¶
Joints converge in a few iterations (small _VELOCITY_ITERATIONS / _POSITION_ITERATIONS): a joint chain sags slightly and stiff springs are soft. Motors, angular limits and breakable joints are deferred to the pymunk (Chipmunk2D) backend. (Contact warm-starting landed in Stage P2; see the header.)
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.
Basic-tier honesty (T2b)¶
SAT manifold on a DEEP first-frame interpenetration can pick a sub-optimal minimum-overlap axis (the shallowest-overlap axis is not always the true MTV when two boxes are nearly co-centred); Baumgarte position correction recovers it over the following steps. A full clipping-with-feature-caching manifold and robust deep-overlap handling are deferred to the pymunk (Chipmunk2D) backend.
Warm-starting / accumulated-impulse caching (Stage P2): 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 from T2a (
_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: pure-Python 2D backend implementing the 2D seam.
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.Physics2DWorldfor the full contract. Implements the COMPLETE seam: shapes + bodies + integrator + bulk readers (T2a), narrowphase + sequential-impulse solver (T2b), forces + joints + sleepingCCD (T2c), queries + events + kinematic sweep (T2d), one-way platforms + the character controller (T2e), and the groove joint (T2g). 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]¶
No Tier-3 capabilities: the builtin 2D tier is an honest rigid-body solver.
2D sibling of :meth:
BuiltinPhysics.capabilities: advertises NONE of- Class:
Capability. Explicit (not inherited) so the claim is a deliberate, tested promise.
- clear() None[source]¶
Remove every body, character, and joint, emptying the world.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.clear. 2D sibling of- Meth:
BuiltinPhysics.clear: resets the body / character / 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]¶
- create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: object, *, 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]¶
- destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: object) 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) 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]¶
- 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 (T2g).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 converted to the world-axes offsets the constraint records use (anchors captured at create, rotation ignored, basic tier; see- Class:
_GrooveConstraint2D). The world 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 (Stage T2e).
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:_collide, :meth:move_and_collideand the character sweep) 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).
- 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 T2c_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.Contact2D | 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 (asserted). 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.
- move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.Contact2D | None[source]¶
Move body
handlebymotion, stop at the first contact.2D sibling of the 3D
move_and_collideand the kinematic-sweep primitive the character controller (T2e) builds 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. It advances the body alongmotion; the first substep that penetrates an OTHER body backs the mover off to the previous non-penetrating substep and reports the contact. A fast mover vs a very thin collider can still tunnel between substeps (replaced by an analytic sweep in the pymunk backend). ReturnsNone(and applies the full move) when the path stays clear.
- create_character(shape: simvx.core.physics.world2d.ShapeHandle, transform: object, *, 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 kinematic character controller (Stage T2e).
2D sibling of the 3D
create_character: the character is NOT registered as a body (the node holds the handle), so it never appears in the solver, the bulk readers, orread_transforms.upis normalised;slope_limitis radians (the maximum floor incline the character treats as ground).
- destroy_character(handle: simvx.core.physics.world2d.CharacterHandle) None[source]¶
- set_character_transform(handle: simvx.core.physics.world2d.CharacterHandle, transform: object) None[source]¶
- character_transform(handle: simvx.core.physics.world2d.CharacterHandle) tuple[simvx.core.math.Vec2, float][source]¶
- 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]¶
Arcade collide-and-slide against world bodies (basic tier) (T2e).
2D sibling of the 3D
character_move_and_slide: sweeps the character alongvelocity * dt; on each blocking contact it advances to the contact, classifies floor / wall / ceiling from the contact normal vsupand the storedslope_limit, deflects both the remaining motion and the velocity out of the surface (projects out the normal component), and repeats up tomax_slidestimes. A walkable surface within a small snap distance below the feet setson_floor+floor_normalvia a dedicated downward probe (the horizontal sweep intentionally ignores the non-opposing floor a walking character rests on). Basic-tier honesty: a single up / forward / down_try_step_up_2dprobe handles a ledge up tostep_height(one step only); a multi-step stair and a robust depenetration are pymunk concerns.
- property gravity: simvx.core.math.Vec2¶
- __slots__¶
()
- simvx.core.physics.builtin.world2d.__all__¶
[‘BuiltinPhysics2D’]