simvx.core.physics.nodes

Role

This is the user-facing 3D body/shape node taxonomy on the new transport seam:

  • 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:

    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 seam (Jolt EMotionType, pymunk body_type, Box2D b2BodyType). On enter-tree it resolves its world (:func:resolve_world), builds its shape, creates a seam body, and (for non-static bodies) registers with the tree’s handle->node sync registry. On exit-tree it unregisters, destroys the seam body, and clears its state.

  • class:

    CharacterBody3D – a swept-collision controller (NOT a body), distinct from the :class:PhysicsBody3D family.

These nodes deliberately reuse the canonical names CollisionShape3D / PhysicsBody3D / CharacterBody3D but live ONLY here. They are NOT wired into any facade (simvx.core / simvx.core.physics still export the OLD body system); tests import them via this module path. The facade flip + removal of the old nodes is a later stage; 2D is later still.

Body + shape-carrier nodes for the new physics seam (3D only).

Module Contents

Classes

Contact

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

CollisionShape3D

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

PhysicsBody3D

A 3D physics body on the new seam; its motion mode is a Property.

CharacterBody3D

A character controller (CharacterVirtual-style), distinct from a rigid body.

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 four T1c 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’, ‘PhysicsBody3D’, ‘CharacterBody3D’, ‘Area3D’, ‘GravityAr…

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

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

Distinct from the seam-level world.Contact (a query/sweep TOI result): 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 body 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 applied this step. 0 on separated and on an enter the solver did not push apart. velocity: Relative velocity of other w.r.t. the receiver at the contact, pre-solve. Degenerate (Vec3(0)) on separated.

other: PhysicsBody3D

None

point: simvx.core.math.Vec3

None

normal: simvx.core.math.Vec3

None

impulse: float

None

velocity: simvx.core.math.Vec3

None

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

Bases: simvx.core.nodes_3d.node3d.Node3D

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

Unlike the old physics_nodes.CollisionShape3D (which stored kind/radius/extents fields), geometry here lives in a single

Class:

Shape resource (SphereShape3D / BoxShape3D), which keeps the node open for new shape kinds. This stage it is a pure data carrier: it has no overlap/penetration tests (the seam does narrowphase). 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 seam shape handle.

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 node’s largest world-scale axis, so a uniformly or non-uniformly scaled collider still has a conservative pick sphere.

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.nodes_3d.node3d.Node3D

A 3D physics body on the new seam; its motion mode is a Property.

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

Per mode:

  • STATIC: immovable collider (floors, walls). Infinite mass; never integrated, so it is intentionally NOT registered for scatter read-back.

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

  • KINEMATIC: code-moved via :meth:move_and_collide; immune to gravity/forces, pushes dynamic bodies. mass is ignored (infinite).

The base owns the seam lifecycle: resolve world, build shape, create body on enter; 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 seam body, re-resolved against whatever PhysicsRoot it now sits under.

Initialization

mode: simvx.core.physics.world.BodyMode

‘Property(…)’

mass: float

‘Property(…)’

collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

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

‘Property(…)’

material: simvx.core.physics.material.PhysicsMaterial

‘Property(…)’

continuous: bool

‘Property(…)’

collided

‘Signal(…)’

separated

‘Signal(…)’

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

This body’s opaque seam handle, or None if no body was created.

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

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

property is_sleeping: bool[source]

True if the seam body is asleep (basic-tier sleeping). 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 seam body).

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

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

This is runtime sim state, NOT a serialized :class:Property: each access delegates to the seam, so reads are current and writes take effect immediately. Returns Vec3() (zero) when inert (not in tree / no seam 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 seam.

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 seam 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. Basic tier scales by inverse_mass as a stand-in for the inverse inertia tensor (the basic backend has no inertia tensor), so results are physically approximate: real inertia is a Tier-2 / Jolt concern.

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 (design S7). 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. Basic tier 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.nodes.Contact | None[source]

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

Meaningful for mode == KINEMATIC (code-driven movement); it sweeps the seam 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 seam. After the sweep the node’s transform is synced SYNCHRONOUSLY from the seam, so the pose is correct the instant this call returns.

Returns the :class:Contact if the sweep stopped early, else None. Returns None if the body is inert (no CollisionShape3D child).

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.nodes_3d.node3d.Node3D

A character controller (CharacterVirtual-style), distinct from a rigid body.

Unlike a :class:PhysicsBody3D it holds a CharacterHandle (not a BodyHandle), mirroring that a character collides against the world but is never force-simulated. It manages its own seam lifecycle and is deliberately NOT registered for the dynamic auto bulk-sync; :meth:move_and_slide syncs the node transform synchronously.

Geometry is supplied by a direct-child :class:CollisionShape3D, matching the Stage 3a body pattern. velocity is set by game logic each frame and is written back (deflected) after each :meth:move_and_slide.

Initialization

slope_limit: float

‘Property(…)’

step_height: float

‘Property(…)’

max_slides: int

‘Property(…)’

skin_width: float

‘Property(…)’

collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

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

‘Property(…)’

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

This node’s opaque character handle, or None if inert.

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

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

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.

Writes the deflected post-slide velocity back to :attr:velocity, caches floor/wall/ceiling state + :attr:floor_normal, and syncs the node’s transform SYNCHRONOUSLY from the seam (characters are not part of the auto bulk-sync). No-op if the body is inert (no CollisionShape3D child).

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.nodes_3d.node3d.Node3D

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

Owns a SENSOR body on the new seam (a flag on the body, not a separate class: Design S3/S4). 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 (Design S6): 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.

Unlike the old Area3D this 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 Design S3 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.

Lifecycle limitations (this stage, parity with :class:PhysicsBody3D): monitoring / shape / collision_layer / collision_mask are read at enter-tree, so changing them after enter requires a re-enter.

Initialization

collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

monitoring: bool

‘Property(…)’

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

‘Property(…)’

body_entered

‘Signal(…)’

body_exited

‘Signal(…)’

area_entered

‘Signal(…)’

area_exited

‘Signal(…)’

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

This area’s opaque sensor-body handle, or None if inert.

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

The :class:PhysicsWorld this area’s sensor was created in, or None.

get_overlapping_bodies(*, group: str | None = None, type: simvx.core.physics.nodes.Area3D.get_overlapping_bodies.type[simvx.core.physics.nodes.PhysicsBody3D] | None = None) list[simvx.core.physics.nodes.PhysicsBody3D][source]

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

A peer destroyed / removed from the tree mid-overlap is purged silently by the backend (no dangling EXIT, parity with the contact stream), so the tree never discards it from the maintained set. Filter such off-seam peers (handle is None) here so polling never reports a body that has left the simulation, matching the backend’s own purge condition.

The optional filters narrow the result without a tree scan (they test the already-maintained overlap set): this is the durable, broadphase-backed replacement for the old arcade CharacterBody.get_overlapping(group=, body_type=) poll. 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:PhysicsBody3D subclass (isinstance(body, type)).

Returns: The overlapping bodies (live, off-seam 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).

Off-seam 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]
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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.

Per Design S4 (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 an override this stage): 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). Inverse-distance / inverse-square falloff, independently-overridable damping, and a full gravity-OVERRIDE (replace) mode are explicit later refinements.

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 (parity with :class:Area3D, not a regression): the sensor body pose is read at enter-tree, so a GravityArea3D moved at runtime is out of scope this stage.

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.

collision_layer: int

‘Bitmask(…)’

collision_mask: int

‘Bitmask(…)’

monitoring: bool

‘Property(…)’

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

‘Property(…)’

body_entered

‘Signal(…)’

body_exited

‘Signal(…)’

area_entered

‘Signal(…)’

area_exited

‘Signal(…)’

property handle: simvx.core.physics.world.BodyHandle | None
property world: simvx.core.physics.world.PhysicsWorld | None
get_overlapping_bodies(*, group: str | None = None, type: simvx.core.physics.nodes.Area3D.get_overlapping_bodies.type[simvx.core.physics.nodes.PhysicsBody3D] | None = None) list[simvx.core.physics.nodes.PhysicsBody3D]
get_overlapping_areas() list[simvx.core.physics.nodes.Area3D]
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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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 four T1c constraint nodes (node-agnostic carriers).

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

Body references (:attr:body_a / :attr:body_b) are PLAIN instance attributes, NOT :class:Property descriptors: a serialised scene-graph node reference is out of scope for this seam stage (the seam is handle-keyed, not node-keyed; scene-to-scene refs are plain Python imports per the codebase rule). 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 seam joint), 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 seam 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; the seam 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 (parity with body shape / mass / layers): a joint reads its Properties and resolves its bodies ONCE at enter-tree. Changing a param or a body reference after enter requires a re-enter (an on_change re-create hook is a follow-on, not T1c). 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). A robust deferred-resolve is a follow-on.

Initialization

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

This joint’s opaque seam 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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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, so the two bodies move as one rigid assembly. No anchor / axis Properties: the constraint captures the current relative pose at create.

Basic-tier honesty: the backend 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 versus a real solver), and convergence is a few iterations (a long weld chain sags slightly). Use near-uniform masses; precise articulated mechanisms are a Jolt concern.

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.

Basic-tier honesty: the anchor offset does NOT rotate with the bodies (re- derived each step in world axes), so a pin on a spinning body drifts; angular cross-coupling uses the inverse_mass inverse-inertia scalar. A few iterations of convergence.

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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. Motors and angular limits are explicitly OUT of scope this stage (a deliberate follow-on).

Basic-tier honesty: same anchor-does-not-rotate and inverse_mass inverse-inertia-scalar caveats as :class:PinJoint3D; a few iterations of convergence.

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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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.

Basic-tier honesty: uses the two centres of mass (Pin / Hinge use anchors, Spring uses COMs for simplicity). 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

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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)
clear_children()
destroy()
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__()