simvx.core.physics.nodes

Body + shape-carrier nodes for 3D physics.

The user-facing 3D body/shape node taxonomy:

  • class:

    CollisionShape3D – a Node3D that carries a :class:Shape resource. Bodies discover their geometry by scanning their direct children for the first CollisionShape3D and building its shape.

  • class:

    PhysicsObject3D – the abstract base of every node that owns a body in the physics world: it holds the backend body handle, the layer/mask pair, the shape resolution, the collision Signals and the teardown. Annotate with this wherever the meaning is “a node with a body in the world”.

  • class:

    PhysicsBody3D – the one concrete body node. Its motion mode is a user-facing, inspectable, serialized :class:BodyMode Property (STATIC | KINEMATIC | DYNAMIC), not fixed per class: one body node with a runtime-mutable mode knob, mirroring every backend’s native model (Jolt EMotionType, pymunk body_type, Box2D b2BodyType). On enter-tree it resolves its world (:func:resolve_world), builds its shape, creates a backend body, and (for non-static bodies) registers with the tree’s handle->node sync registry. On exit-tree it unregisters, destroys the backend body, and clears its state.

  • class:

    CharacterBody3D – a KINEMATIC body with a swept movement helper (:meth:CharacterBody3D.move_and_slide), which is the only thing that distinguishes it from any other kinematic body.

The 2D siblings live in nodes2d.py. All of these are exported from simvx.core.

Module Contents

Classes

Contact

Node-level collision-event payload for collided / separated.

CollisionShape3D

A Node3D that carries a :class:Shape collision-geometry resource.

PhysicsObject3D

Abstract base of every 3D node that owns a body in the physics world.

PhysicsBody3D

A 3D physics body whose motion mode is a Property.

CharacterBody3D

A KINEMATIC physics body with a swept movement helper.

Area3D

A pure sensor zone (trigger): broadphase-driven overlap detection.

GravityArea3D

A force-field zone: an ADDITIVE gravity effector over the bodies it overlaps.

Joint3D

Base class for the constraint nodes (node-agnostic carriers).

FixedJoint3D

Weld two bodies: lock their full relative transform (position + orientation).

PinJoint3D

Pin two bodies at a single point (ball / point-to-point), rotation free.

HingeJoint3D

Hinge two bodies: pin at :attr:anchor + one free rotational DOF about :attr:axis.

SpringJoint3D

Soft distance-spring between the two body centres (compliant, not rigid).

Data

API

simvx.core.physics.nodes.log

‘getLogger(…)’

simvx.core.physics.nodes.__all__

[‘BodyMode’, ‘Contact’, ‘CollisionShape3D’, ‘PhysicsObject3D’, ‘PhysicsBody3D’, ‘CharacterBody3D’, ‘…

class simvx.core.physics.nodes.Contact[source]

Node-level collision-event payload for collided / separated.

Distinct from the world-level :class:~simvx.core.physics.world.SweepHit (a query/sweep result keyed by handle): this is node-typed and built by the tree’s dispatch from a node-agnostic ContactEvent. The tree fills other with the peer node and reorients normal / velocity so they always point toward the RECEIVING body.

Attributes: other: The OTHER physics object involved in the collision. point: World-space contact point. normal: Unit normal oriented TOWARD the receiving body (the separating direction). Degenerate (Vec3(0)) on separated (no live manifold on exit). impulse: Normal impulse magnitude the solver applied this step. 0.0 on separated and on a collided the solver did not push apart. None iff the resolved backend does not advertise :attr:~simvx.core.physics.capability.Capability.CONTACT_IMPULSE (both Jolt backends): it cannot read its solver’s applied lambda, so it reports no number rather than a guess. Check the capability once at setup rather than testing for None mid-collision. impulse_estimate: The portable stand-in for impulse, one formula on every backend: the impulse it would take to arrest the approach, from the pair’s masses and the difference of the two bodies’ LINEAR velocities. Always a number, and 0.0 on separated and for a pair that is not approaching. This is what a game reads where the backend cannot measure. It is deliberately not computed from velocity below, whose at-point meaning would put each backend’s choice of contact point into the number. See :attr:~simvx.core.physics.world.ContactEvent.impulse_estimate. velocity: Relative velocity of other w.r.t. the receiver AT THE CONTACT POINT, pre-solve. Spin counts: a wheel skidding on the ground reports the speed of its tread, which its centre is not moving at. Degenerate (Vec3(0)) on separated. For the receiver’s own linear velocity, read it off the body.

other: PhysicsObject3D

None

point: simvx.core.math.Vec3

None

normal: simvx.core.math.Vec3

None

impulse: float | None

None

impulse_estimate: float

None

velocity: simvx.core.math.Vec3

None

class simvx.core.physics.nodes.CollisionShape3D(**kwargs: object)[source]

Bases: simvx.core.nodes_3d.node3d.Node3D

A Node3D that carries a :class:Shape collision-geometry resource.

Geometry lives in a single :class:Shape resource (SphereShape3D / BoxShape3D) rather than in loose kind / radius / extents fields, which keeps the node open for new shape kinds. The node is a pure data carrier: it runs no overlap or penetration tests of its own (narrowphase belongs to the physics world). It exists so a body can discover its geometry as a child node.

Initialization

shape: simvx.core.physics.shapes.Shape

‘Property(…)’

pickable: bool

‘Property(…)’

build_shape(world: simvx.core.physics.world.PhysicsWorld) simvx.core.physics.world.ShapeHandle[source]

Build this node’s shape into an opaque backend shape handle.

property collider_scale: simvx.core.math.Vec3[source]

The world scale the SIMULATION applies to this collider’s geometry.

Scale lives on the BODY: a body has one scale and the seam gives a body one, so a collider child’s own local scale does not resize the collider it carries. This resolves the scale that actually reaches the physics world – the owning body’s when this node has one, and this node’s own when it does not, because a free collider (a pick target, an editor gizmo) has no body to take a scale from.

Read this, not :attr:world_scale, anywhere a collider’s SIMULATED size matters. The two differ exactly when a collider child carries a local scale, which is the case a user provokes by trying to resize a collider by scaling the collider node.

property pick_radius: float[source]

Bounding-sphere radius used for CPU picking, in WORLD units.

The shape’s local :attr:~simvx.core.physics.shapes.Shape.bounding_radius scaled by the largest axis of :attr:collider_scale, so a uniformly or non-uniformly scaled collider still has a conservative pick sphere and one that matches the collider the simulation actually holds.

position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_enter_tree() None
on_exit_tree() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.PhysicsObject3D(**kwargs: object)[source]

Bases: simvx.core.physics._pose_reconcile._PhysicsPoseReconcile, simvx.core.nodes_3d.node3d.Node3D

Abstract base of every 3D node that owns a body in the physics world.

Not instantiable in practice (it creates nothing on enter-tree): it exists so that “a node with a body in the physics world” has ONE name. Every physics node family is one of these – :class:PhysicsBody3D (which adds mode / mass / material and the live velocity), :class:CharacterBody3D (which adds the collide-and-slide movement helper) and :class:Area3D (which adds the sensor flag and the overlap sets) – so a payload or an annotation that means “the peer body node” names this class rather than a union or an isinstance tuple.

What it owns: the backend handle and its world, the

Attr:

collision_layer / :attr:collision_mask pair, the :attr:shape convenience collider and its resolution, the initial-pose build, the node-pose-to-body teleport of :class:_PhysicsPoseReconcile, the

Attr:

collided / :attr:separated Signals, and the exit-tree teardown. Subclasses own body CREATION only, because what they create differs (a typed-mode body, a kinematic body, a static sensor).

Initialization

collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

shape: simvx.core.physics.shapes.Shape | None

‘Property(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None[source]

This object’s opaque backend body handle, or None if none was created.

property world: simvx.core.physics.world.PhysicsWorld | None[source]

The :class:PhysicsWorld this object’s body was created in, or None.

on_exit_tree() None[source]

Retire from the tree’s handle->node map and destroy the backend body.

Retired, not dropped: destroying the body ends every contact and sensor overlap it was in, and the seam reports those closing EXITs on the first drain after the next step. The tree holds this node’s handle->node entry until that dispatch has run, so a peer’s separated / body_exited can still name the body that went away. By then this node is detached (tree, handle and world are all None), which is exactly what a handler needs in order to tell “it left” from “it moved away”.

position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_enter_tree() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.PhysicsBody3D(**kwargs: object)[source]

Bases: simvx.core.physics.nodes.PhysicsObject3D

A 3D physics body whose motion mode is a Property.

One concrete body node with a runtime-mutable :class:BodyMode mode knob (STATIC | KINEMATIC | DYNAMIC) rather than a Static/Rigid/Kinematic class split. This mirrors every backend’s native model (Jolt EMotionType, pymunk body_type, Box2D b2BodyType): one body + a mode, runtime-flippable (sleep->static, ragdoll toggle).

Per mode:

  • STATIC: immovable collider (floors, walls). Simulated as infinite-mass and never integrated, so it is intentionally NOT registered for scatter read-back; :attr:mass is retained for a later flip to DYNAMIC.

  • DYNAMIC: force-simulated; responds to gravity, impulses, contacts. Uses :attr:mass.

  • KINEMATIC: code-moved via :meth:move_and_collide; immune to gravity/forces. Simulated as infinite-mass, with :attr:mass retained for a later flip to DYNAMIC.

Class:

PhysicsObject3D owns the rest of the body lifecycle: resolve world, build shape, unregister + destroy on exit. _handle / _world are cleared on exit and rebuilt on enter, so a body that re-enters the tree (re-parenting / change_scene) gets a fresh backend body, re-resolved against whatever PhysicsRoot it now sits under.

Parenting, and which mode follows a moving ancestor. A STATIC body follows: move any ancestor and the collider goes with it, because nothing simulates a STATIC body and its pose is authored data. That is the mode to parent under a node whose motion is authoring rather than gameplay – an editor drag, a group positioned once at setup, a level chunk assembled under a common origin.

A DYNAMIC or KINEMATIC body does NOT follow: the simulation owns its pose, and an ancestor’s move would be erased by the next write-back anyway. So for a platform that must carry or push bodies at RUNTIME, do not parent them to it. Make the platform a KINEMATIC body and move that body itself, because a STATIC pose write carries no derived velocity and would slide riders off rather than transport them. This is a deliberate difference from engines that push the composed transform into every body type regardless of mode.

Initialization

mode: simvx.core.physics.world.BodyMode

‘Property(…)’

mass: float

‘Property(…)’

material: simvx.core.physics.material.PhysicsMaterial

‘Property(…)’

linear_damping: float

‘Property(…)’

angular_damping: float

‘Property(…)’

gravity_scale: float

‘Property(…)’

continuous: bool

‘Property(…)’

can_sleep: bool

‘Property(…)’

property is_sleeping: bool[source]

True if the simulated body is asleep. False when inert.

A DYNAMIC body whose speed stays sub-threshold settles to sleep (skipped by integrate + the contact velocity solve) until a disturbance wakes it. STATIC / KINEMATIC bodies are never ‘asleep’ (they were never awake) and return False, as does an inert node (not in tree / no body).

wake() None[source]

Wake this body now, whatever its sleep timer had reached.

Rarely needed, because every write that changes what the solver reads already wakes the body. Reach for it when a game knows something the solver cannot see: a scripted force is coming next frame, or a script is about to read a velocity that must be current. A no-op when the body is awake, when it is STATIC or KINEMATIC (never asleep), and when inert (not in tree / no body).

sleep() None[source]

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

Freezes it where it is, still a full collider, until something wakes it. Use it to park a pile a game knows is finished rather than paying for it to come to rest first. A no-op when the body is STATIC or KINEMATIC, when

Attr:

can_sleep is False, when inert, and on a backend that does not advertise :attr:~simvx.core.physics.capability.Capability.SLEEP.

property velocity: simvx.core.math.Vec3[source]

Live linear velocity (Vec3), read/written straight to the physics world.

This is runtime sim state, NOT a serialized :class:Property: each access delegates to the physics world, so reads are current and writes take effect immediately. Returns Vec3() (zero) when inert (not in tree / no body). A mass-free instant change is just self.velocity += dv (the getter returns a fresh Vec3, += writes it back through the setter). The setter preserves the current angular velocity (spin).

property spin: simvx.core.math.Vec3[source]

Live angular velocity (Vec3, radians/s), read/written to the physics world.

Companion to :attr:velocity; same live-state, never-serialized rules. Returns Vec3() when inert. The setter preserves linear velocity.

push(impulse: simvx.core.math.Vec3 | collections.abc.Sequence[float], *, at: simvx.core.math.Vec3 | collections.abc.Sequence[float] | None = None) None[source]

Apply an instantaneous linear impulse (v += impulse * inv_mass) NOW.

Meaningful only for mode == DYNAMIC (forces on STATIC / KINEMATIC are physically inert: inverse_mass == 0); a no-op otherwise, and a no-op when inert (not in tree / no body). at is a world-space point; the offset r = at - centre adds an angular impulse contribution (cross(r, impulse)), replacing a separate central-vs-offset split.

Args: impulse: Linear impulse (Vec3 / sequence), N*s. at: Optional world-space application point. None applies the impulse through the centre of mass (no spin).

spin_up(angular_impulse: simvx.core.math.Vec3 | collections.abc.Sequence[float]) None[source]

Apply an instantaneous angular impulse (omega += angular * inv_mass).

DYNAMIC-only, inert otherwise. The built-in backend has no inertia tensor and scales by inverse_mass as a stand-in for the inverse inertia, so spin results there are physically approximate.

Args: angular_impulse: Angular impulse (Vec3 / sequence).

add_force(force: simvx.core.math.Vec3 | collections.abc.Sequence[float], *, at: simvx.core.math.Vec3 | collections.abc.Sequence[float] | None = None) None[source]

Accumulate a continuous force, applied during the NEXT fixed step.

Auto-cleared each step, so to sustain a force re-call this every on_fixed_update. DYNAMIC-only, inert otherwise. at is a world-space point; its offset adds a torque (cross(r, force)).

Args: force: Linear force (Vec3 / sequence), N. at: Optional world-space application point. None applies the force through the centre of mass (no torque).

add_torque(torque: simvx.core.math.Vec3 | collections.abc.Sequence[float]) None[source]

Accumulate a continuous torque, applied during the NEXT fixed step.

Auto-cleared each step like :meth:add_force; re-call per on_fixed_update to sustain. DYNAMIC-only, inert otherwise. On the built-in backend this uses the inverse_mass inverse-inertia stand-in.

Args: torque: Torque (Vec3 / sequence), N*m.

on_enter_tree() None[source]
move_and_collide(velocity: simvx.core.math.Vec3 | collections.abc.Sequence[float], dt: float = 1.0) simvx.core.physics.world.SweepHit | None[source]

Move by velocity * dt, stop at the first contact, sync, return it.

Meaningful for mode == KINEMATIC (code-driven movement); it sweeps the body’s shape and reports the first blocker. velocity is a world-space velocity (units/s); the node multiplies by dt to form the displacement handed to the physics world. After the sweep the node’s transform is synced SYNCHRONOUSLY from the simulated body, so the pose is correct the instant this call returns.

Returns a :class:~simvx.core.physics.world.SweepHit if the sweep stopped early, else None. Also None if the body is inert (no CollisionShape3D child). A sweep is a geometric query, not a solver pass, so the result carries the blocker’s handle, contact point, separating normal and guaranteed-clear distance, and no impulse.

on_exit_tree() None[source]
collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

shape: simvx.core.physics.shapes.Shape | None

‘Property(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.CharacterBody3D(**kwargs: object)[source]

Bases: simvx.core.physics.nodes.PhysicsObject3D

A KINEMATIC physics body with a swept movement helper.

Raycasts, shape queries and areas see it, dynamic bodies collide with it and rest on it, it produces contact events against dynamic bodies, and two characters block each other when their layers and masks mutually opt in.

Meth:

move_and_slide is the only thing that distinguishes it from any other kinematic body. It never carries a body resting on it, but it does shove one: at the default :attr:push_factor of 1.0 a blocking dynamic body takes the arrest impulse of the pair, so it leaves at no more than the speed the character was walking, scaled by the mass ratio. Set the factor to 0.0 and a dynamic body stops the sweep exactly as a static one does. The contacts it resolved are handed back in :attr:collisions either way, so game code can apply its own impulses.

Geometry follows the standard order (:attr:shape Property if set, else the first direct-child :class:CollisionShape3D, else inert). It is deliberately NOT registered for the dynamic auto bulk-sync; :meth:move_and_slide syncs the node transform synchronously. :attr:velocity is set by game logic each frame and written back (deflected) after each move.

Attr:

velocity is a plain instance attribute, deliberately NOT the world-backed :attr:PhysicsBody3D.velocity property: the simulated body’s velocity must stay zero, or the world’s integrator would re-apply the motion

Meth:

move_and_slide has already performed.

Initialization

slope_limit: float

‘Property(…)’

step_height: float

‘Property(…)’

max_slides: int

‘Property(…)’

skin_width: float

‘Property(…)’

mass: float

‘Property(…)’

push_factor: float

‘Property(…)’

is_on_floor() bool[source]

True if the last :meth:move_and_slide ended on a walkable floor.

is_on_wall() bool[source]

True if the last :meth:move_and_slide hit a wall.

is_on_ceiling() bool[source]

True if the last :meth:move_and_slide hit a ceiling.

move_and_slide(dt: float) None[source]

Collide-and-slide by self.velocity * dt, then sync the transform.

Runs the shared policy in :mod:simvx.core.physics.slide against the simulated body. Writes the deflected post-slide velocity back to :attr:velocity, caches floor/wall/ceiling state + :attr:floor_normal +

Attr:

collisions, shoves each non-floor blocking contact if

Attr:

push_factor is non-zero, and syncs the node’s transform SYNCHRONOUSLY from the move (characters are not part of the auto bulk-sync). No-op if the body is inert (no collider).

on_enter_tree() None[source]
collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

shape: simvx.core.physics.shapes.Shape | None

‘Property(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
on_exit_tree() None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.Area3D(**kwargs: object)[source]

Bases: simvx.core.physics.nodes.PhysicsObject3D

A pure sensor zone (trigger): broadphase-driven overlap detection.

Owns a SENSOR body in the physics world (a flag on the body, not a separate class). A sensor participates in the broadphase but is excluded from collision resolution, so an Area3D detects bodies/areas passing through it without pushing them. Detection is ONE-DIRECTIONAL: the area sees another body iff area.collision_mask & other.collision_layer (the observer decides; the other body’s mask is irrelevant), checked per detector independently for sensor-vs-sensor.

Detection is never a per-frame find_all tree scan: overlap edges arrive as a buffered, deferred event stream drained by the tree after each step, and the live overlap sets are maintained from those edges, so :meth:get_overlapping_bodies / :meth:get_overlapping_areas and the Signals are always consistent with the last dispatched step.

Geometry follows the resolution order: the :attr:shape convenience Property if set (it WINS over a child), else the first direct child :class:CollisionShape3D, else inert. The sensor body is STATIC (a zone is typically fixed); a sensor is excluded from response regardless of mode.

What it detects is the same everywhere apart from one exception. An awake DYNAMIC or KINEMATIC body, a SLEEPING one and a STATIC one are all reported on every backend, the last of them where

Attr:

~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATIC is advertised, which today is every backend. The exception is an area whose geometry is a :class:~simvx.core.physics.shapes.ConcaveMeshShape3D on Jolt: Jolt requires a mesh collider to sit on a static body, and a static body never drives the pair search, so such an area sees only what is awake and movable. Trigger volumes are boxes and spheres in practice; give the area convex geometry – at creation or by swapping it in later – and the exception is gone.

shape / collision_layer / collision_mask are live on an area exactly as they are on a body. :attr:monitoring is the one create-time knob: it decides whether a sensor body exists at all, so switching it after enter needs a re-enter (remove and re-add the node).

Initialization

monitoring: bool

‘Property(…)’

shape: simvx.core.physics.shapes.Shape | None

‘Property(…)’

body_entered

‘Signal(…)’

body_exited

‘Signal(…)’

area_entered

‘Signal(…)’

area_exited

‘Signal(…)’

get_overlapping_bodies(*, group=None, type=None)[source]

Bodies currently overlapping this area (live, as of the last step).

A peer destroyed / removed from the tree mid-overlap is dropped from the maintained set by the overlap EXIT the seam reports for it, on the step after it goes (:attr:body_exited fires with it at the same moment). Detached peers (handle is None) are also filtered here, which covers the one step before that event lands and a body carried out with a world that this tree will not step again.

The optional filters narrow the result without a tree scan: they test the already-maintained overlap set. Both are ANDed when given:

Args: group: When set, keep only bodies that belong to this SceneTree group (body.is_in_group(group)). The canonical “what of kind X am I touching?” query (e.g. area.get_overlapping_bodies(group="mobs")). type: When set, keep only bodies that are instances of this :class:PhysicsObject3D subclass (isinstance(body, type)), and the result is typed as a list of THAT class. A character is an ordinary kinematic body, so it appears here like any other; pass type=PhysicsBody3D when the caller needs the force/mass API a character does not have.

Returns: The overlapping bodies (live, detached peers filtered) matching every supplied filter, in arbitrary order.

get_overlapping_areas() list[simvx.core.physics.nodes.Area3D][source]

Other areas currently overlapping this area (live, as of the last step).

Detached peers (a destroyed / removed area, handle is None) are filtered here for the same reason as :meth:get_overlapping_bodies.

on_enter_tree() None[source]
on_exit_tree() None[source]
collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.GravityArea3D(**kwargs: object)[source]

Bases: simvx.core.physics.nodes.Area3D

A force-field zone: an ADDITIVE gravity effector over the bodies it overlaps.

Following the Unreal PhysicsVolume split, gravity is NOT baked into

Class:

Area3D: a plain Area3D stays a zero-cost pure sensor, and this separate effector node consumes its overlap set. GravityArea3D inherits the entire sensor mechanism unchanged (the STATIC sensor body, the broadphase-driven buffered overlap stream, the maintained overlap sets,

Meth:

get_overlapping_bodies / :meth:get_overlapping_areas, the body_entered / body_exited / area_entered / area_exited Signals, and the collision_layer / collision_mask / monitoring / shape Properties). It does NOT override the lifecycle: it is still a sensor that detects without colliding.

What it adds is an :meth:on_fixed_update handler that, each fixed step, reads :meth:get_overlapping_bodies and applies a field to every DYNAMIC body inside it. The field is additive on top of world gravity, never a replacement for it: the world still applies its own gravity inside step, and this re-applied force composes with it. Two independent components that SUM when both are configured:

  • Directional (:attr:gravity): a uniform acceleration vector applied regardless of body mass, exactly like world gravity (e.g. Vec3(0, 9.81, 0) for an anti-gravity lift, or a sideways wind-as-accel).

  • Point (:attr:point_gravity / :attr:point_strength): a CONSTANT (distance-independent) acceleration of magnitude :attr:point_strength toward the area centre (:attr:world_position). There is no inverse-distance / inverse-square falloff, no per-area damping override and no gravity-replacement mode.

The acceleration is converted to a force via add_force(mass * accel): the integrator divides by mass, so the net effect is mass-INDEPENDENT, exactly like gravity. add_force is auto-cleared each step, so the field is freshly re-applied every fixed step and consumed exactly once by the immediately following world.step (no carry-over, no double-apply). When the area is empty the loop is a no-op (zero cost); monitoring=False makes the area inert (empty overlap set) so the field naturally turns off.

Moving-emitter caveat (shared with :class:Area3D): the sensor body pose is read at enter-tree, so moving a GravityArea3D at runtime does not move the zone it detects and pulls in.

Initialization

gravity: simvx.core.math.Vec3

‘Property(…)’

point_gravity: bool

‘Property(…)’

point_strength: float

‘Property(…)’

on_fixed_update(dt: float) None[source]

Apply the additive gravity field to every DYNAMIC overlapping body.

Runs BEFORE world.step (the tree drives node on_fixed_update first, then steps the worlds), accumulating a fresh force consumed by that step. dt is accepted to match the hook signature but is NOT used to scale: add_force is a continuous force the integrator consumes over the step, not an impulse.

A :class:CharacterBody3D overlapping the area IS in the overlap set (it is an ordinary kinematic body), but it is never force-driven: it is not a

Class:

PhysicsBody3D and so has no :meth:~PhysicsBody3D.add_force, and the :attr:~CharacterBody3D.mass it does carry is read only by its own push arithmetic, never by an integrator. It is position-driven by

Meth:

CharacterBody3D.move_and_slide instead, and a game that wants a gravity zone to move one applies the field to its velocity itself. The type= filter is what excludes it, so a character in a gravity zone is simply ignored rather than an error.

monitoring: bool

‘Property(…)’

shape: simvx.core.physics.shapes.Shape | None

‘Property(…)’

body_entered

‘Signal(…)’

body_exited

‘Signal(…)’

area_entered

‘Signal(…)’

area_exited

‘Signal(…)’

get_overlapping_bodies(*, group=None, type=None)
get_overlapping_areas() list[simvx.core.physics.nodes.Area3D]
on_enter_tree() None
on_exit_tree() None
collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.Joint3D(*, body_a: simvx.core.physics.nodes.PhysicsBody3D | None = None, body_b: simvx.core.physics.nodes.PhysicsBody3D | None = None, **kwargs: object)[source]

Bases: simvx.core.nodes_3d.node3d.Node3D

Base class for the constraint nodes (node-agnostic carriers).

A Joint3D constrains two :class:PhysicsBody3D instances in the physics world. It is a thin carrier: it holds the two body references plus its per-joint Properties and owns the constraint lifecycle (create on enter-tree, remove on exit-tree), exactly mirroring how :class:PhysicsBody3D owns its body.

Body references (:attr:body_a / :attr:body_b) are PLAIN instance attributes, NOT :class:Property descriptors: constraints are keyed by body handle rather than by node, and cross-scene references are plain Python imports. Set them programmatically, e.g. joint.body_a = self.crate; joint.body_b = self.anchor, or via the constructor: PinJoint3D(body_a=..., body_b=..., anchor=...).

Resolution at :meth:on_enter_tree:

  1. If either body reference is None the joint stays INERT (no constraint is created), logs at debug, and returns: a one-body joint is a no-op, not a crash (mirrors the body’s no-shape inert path).

  2. Each body’s handle (body.handle) is resolved. If EITHER is None (the body is not yet in the tree, or is inert with no collider) the joint stays inert: it never half-creates.

  3. SAME-WORLD requirement: body_a.world and body_b.world MUST be the identical :class:PhysicsWorld (a joint is a within-world constraint; body handles are world-local). A mismatch (or either None after handles resolved) raises :class:ValueError: this is a genuine programming error, unlike a missing body (a transient tree-order condition), which is the soft-inert case above.

LIFECYCLE LIMITATION (same as body shape / mass / layers): a joint reads its Properties and resolves its bodies ONCE at enter-tree. Changing a parameter or a body reference after enter requires a re-enter. A joint declared BEFORE its bodies resolves to inert and silently never constrains, so ALWAYS add a joint AFTER both of its bodies (or re-enter it).

Initialization

property joint: simvx.core.physics.world.JointHandle | None[source]

This joint’s opaque backend handle, or None if inert / not created.

property world: simvx.core.physics.world.PhysicsWorld | None[source]

The :class:PhysicsWorld this joint was created in, or None.

on_enter_tree() None[source]
on_exit_tree() None[source]
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.FixedJoint3D(*, body_a: simvx.core.physics.nodes.PhysicsBody3D | None = None, body_b: simvx.core.physics.nodes.PhysicsBody3D | None = None, **kwargs: object)[source]

Bases: simvx.core.physics.nodes.Joint3D

Weld two bodies: lock their full relative transform (position + orientation).

Captures the current relative pose of body_b in body_a’s frame at enter-tree and holds it in that frame, so the two bodies move as one rigid assembly and the assembly swings round when body_a turns. No anchor / axis Properties: the constraint captures the current relative pose at create.

Built-in backend caveat: it has NO inertia tensor, so the angular lock uses inverse_mass as the inverse-inertia scalar (a long thin or off-centre weld rotates too easily), and convergence is a few iterations (a long weld chain sags slightly). Use near-uniform masses, or the Jolt backend for precise articulated mechanisms.

Initialization

property joint: simvx.core.physics.world.JointHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
on_enter_tree() None
on_exit_tree() None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.PinJoint3D(*, body_a: simvx.core.physics.nodes.PhysicsBody3D | None = None, body_b: simvx.core.physics.nodes.PhysicsBody3D | None = None, **kwargs: object)[source]

Bases: simvx.core.physics.nodes.Joint3D

Pin two bodies at a single point (ball / point-to-point), rotation free.

The two bodies cannot separate at :attr:anchor but rotate freely about it. The anchor is captured into each body’s own frame, so a pin on a spinning body orbits with it.

Built-in backend caveat: angular cross-coupling uses the inverse_mass inverse-inertia scalar (there is no inertia tensor), and the solver runs only a few iterations, so a long chain sags slightly.

Initialization

anchor: simvx.core.math.Vec3

‘Property(…)’

property joint: simvx.core.physics.world.JointHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
on_enter_tree() None
on_exit_tree() None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.HingeJoint3D(*, body_a: simvx.core.physics.nodes.PhysicsBody3D | None = None, body_b: simvx.core.physics.nodes.PhysicsBody3D | None = None, **kwargs: object)[source]

Bases: simvx.core.physics.nodes.Joint3D

Hinge two bodies: pin at :attr:anchor + one free rotational DOF about :attr:axis.

A point constraint at the anchor PLUS an angular lock removing the two off-axis rotational DOF, leaving free rotation only about axis. The axis is captured into each body’s own frame, so a door hung on a post that is itself turning keeps swinging about the post. There are no motors and no angular limits.

Built-in backend caveat: the same inverse_mass inverse-inertia-scalar stand-in as :class:PinJoint3D, and only a few solver iterations.

Initialization

anchor: simvx.core.math.Vec3

‘Property(…)’

axis: simvx.core.math.Vec3

‘Property(…)’

property joint: simvx.core.physics.world.JointHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
on_enter_tree() None
on_exit_tree() None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.core.physics.nodes.SpringJoint3D(*, body_a: simvx.core.physics.nodes.PhysicsBody3D | None = None, body_b: simvx.core.physics.nodes.PhysicsBody3D | None = None, **kwargs: object)[source]

Bases: simvx.core.physics.nodes.Joint3D

Soft distance-spring between the two body centres (compliant, not rigid).

Pulls the two body centres toward :attr:rest_length apart with

Attr:

stiffness (N/m) and :attr:damping (N*s/m). Intentionally compliant: no position correction.

Acts between the two centres of mass rather than between anchors as Pin and Hinge do. On the built-in backend the explicit soft-impulse integration can oscillate / overshoot when stiffness is large relative to the fixed dt; nothing is silently clamped. A stiff spring needs a smaller dt or the Jolt backend.

Initialization

rest_length: float

‘Property(…)’

stiffness: float

‘Property(…)’

damping: float

‘Property(…)’

property joint: simvx.core.physics.world.JointHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
on_enter_tree() None
on_exit_tree() None
position

‘_SpatialVecProperty(…)’

rotation

‘Property(…)’

scale

‘_SpatialVecProperty(…)’

render_layer

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3
property world_position: simvx.core.math.types.Vec3
property world_rotation: simvx.core.math.types.Quat
property world_scale: simvx.core.math.types.Vec3
property forward: simvx.core.math.types.Vec3
property right: simvx.core.math.types.Vec3
property up: simvx.core.math.types.Vec3
translate(offset: tuple[float, float, float] | numpy.ndarray)
translate_global(offset: tuple[float, float, float] | numpy.ndarray)
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)
rotate_x(angle: float)
rotate_y(angle: float)
rotate_z(angle: float)
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None
set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)
strict_errors: ClassVar[bool]

True

dev_checks: ClassVar[bool]

None

script_error_raised

‘Signal(…)’

dynamic: bool

False

visible

‘Property(…)’

update_mode

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]

None

classmethod __init_subclass__(**kwargs)
property name: str
property visible_in_tree: bool
reset_error() None
add_child(node: simvx.core.node.T) simvx.core.node.T
remove_child(node: simvx.core.node.Node) None
reparent(new_parent: simvx.core.node.Node)
node_at(path, default=_NO_DEFAULT)
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
expect(target, *, direct: bool = False)
ancestor(target)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
property is_scene_root: bool
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
on_ready() None
on_update(dt: float) None
on_fixed_update(dt: float) None
on_draw(renderer) None
on_picked(event: simvx.core.events.InputEvent) None
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
queue_redraw() None
property render_dirty: bool
clear_children()
destroy()
property destroying: bool
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None
property app
property tree: simvx.core.scene_tree.SceneTree
property physics
property physics_2d
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()