Contacts rj45 plug
Mesh and SDF contacts
An RJ45 plug moved into its socket with the translation gizmo.
steel_vbd preset, recordedon an NVIDIA RTX PRO 6000 Blackwell.Run it
cd examples# the default preset (vbd), then the othersuv run shelf play contacts_rj45_pluguv run shelf play contacts_rj45_plug --solver steel_vbd# change solver settings liveuv run shelf tune contacts_rj45_plugSource: examples/shelf/03_mesh_sdf_contacts/contacts_rj45_plug/ · ported from newton/examples/contacts/example_contacts_rj45_plug.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
vbddefault | vbd | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
steel | steel | unsupported | the cable is a rod of ROD joints, which SolverSteel rejects at construction (NotImplementedError); steel_vbd runs the plug and latch on Steel and the cable on VBD |
steel_vbd | coupled | works | 2026-11-11, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers# SPDX-License-Identifier: Apache-2.0# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example SDF RJ45 Plug-Socket Insertion## Use the translation gizmo to move the plug toward the socket.# Click an axis arrow to slide along one axis, or a plane square# for two-axis motion. The latch deflects on entry and locks# the plug once fully inserted. ``--insert`` scripts the gizmo instead:# push the plug in, then pull it back against the latch.## Command: uv run -m shelf run contacts_rj45_plug############################################################################
import dataclasses
import numpy as npimport warp as wpfrom pxr import Usd, UsdGeom
import newtonimport newton.examplesimport newton.usdfrom newton.math import quat_between_vectors_robustfrom newton.solvers.experimental.coupled import SolverCoupled, SolverCoupledProxy
from common import presets
CONTACT_KE = 1.0e5CONTACT_KD = 0.0
SHAPE_CFG = newton.ModelBuilder.ShapeConfig( mu=0.0, ke=CONTACT_KE, kd=CONTACT_KD, gap=0.002, density=1.0e6, mu_torsional=0.0, mu_rolling=0.0,)
MESH_SDF_MAX_RESOLUTION = 128MESH_SDF_NARROW_BAND_RANGE = (-2.0 * SHAPE_CFG.gap, 2.0 * SHAPE_CFG.gap)
PLUG_Y_OFFSET = -0.025
CABLE_RADIUS = 0.00325CABLE_KINEMATIC_COUNT = 4 # first N rod bodies are inside the plug and follow itSOCKET_OPACITY = 0.35
# Contact parameters for cable and ground plane (tuned for VBD).CABLE_MU = 2.0
# Latch revolute-joint tuning.LATCH_LIMIT_LOWER = -0.2 # max inward deflection [rad]LATCH_LIMIT_UPPER = 0.3 # max outward deflection [rad]LATCH_SPRING_KE = 0.15 # angular return-spring stiffness [N*m/rad]LATCH_SPRING_KD = 0.03 # angular return-spring damping [N*m*s/rad]LATCH_LIMIT_KD = 1.0e-4 # angular limit damping [N*m*s/rad]
# --insert: (time [s], gizmo y offset from the start [m]) keyframes; -PLUG_Y_OFFSET is fully inserted, and the# push aims past it so the gizmo spring seats the plug and snaps the latch.INSERT_PUSH = -PLUG_Y_OFFSET + 0.015INSERT_KEYS = ((0.3, 0.0), (1.8, INSERT_PUSH), (2.6, INSERT_PUSH), (3.4, 0.0))INSERT_LATCHED_TOL = 0.002 # [m] the plug may back out this far against the latch
@wp.kerneldef _apply_gizmo_force( body_q: wp.array[wp.transform], body_qd: wp.array[wp.spatial_vector], body_f: wp.array[wp.spatial_vector], body_mass: wp.array[float], pick_target: wp.array[wp.vec3], stiffness: float, damping: float, pick_body: wp.array[int], plug_idx: int, latch_idx: int, gravity: wp.vec3,): """Apply forces based on interaction mode.
``pick_body[0]`` encodes the mode: * ``>= 0`` -- viewer is picking that body index (damping only on others) * ``< 0`` -- spring toward ``pick_target[0]``
Also cancels gravity for the plug and latch so only the cable sags. """ # Cancel gravity for plug and latch unconditionally. anti_g0 = -gravity * body_mass[plug_idx] anti_g1 = -gravity * body_mass[latch_idx] wp.atomic_add(body_f, plug_idx, wp.spatial_vector(anti_g0, wp.vec3(0.0))) wp.atomic_add(body_f, latch_idx, wp.spatial_vector(anti_g1, wp.vec3(0.0)))
target = pick_target[0] picked_body = pick_body[0]
if picked_body >= 0: if picked_body != plug_idx: vel0 = wp.spatial_top(body_qd[plug_idx]) mass0 = body_mass[plug_idx] f0 = -(10.0 + mass0) * damping * vel0 wp.atomic_add(body_f, plug_idx, wp.spatial_vector(f0, wp.vec3(0.0))) if picked_body != latch_idx: vel1 = wp.spatial_top(body_qd[latch_idx]) mass1 = body_mass[latch_idx] f1 = -(10.0 + mass1) * damping * vel1 wp.atomic_add(body_f, latch_idx, wp.spatial_vector(f1, wp.vec3(0.0))) return
pos0 = wp.transform_get_translation(body_q[plug_idx]) vel0 = wp.spatial_top(body_qd[plug_idx]) mass0 = body_mass[plug_idx] mult0 = 10.0 + mass0
f0 = mult0 * (stiffness * (target - pos0) - damping * vel0) wp.atomic_add(body_f, plug_idx, wp.spatial_vector(f0, wp.vec3(0.0)))
vel1 = wp.spatial_top(body_qd[latch_idx]) mass1 = body_mass[latch_idx] spring_accel = (target - pos0) * (mult0 * stiffness / mass0) f1 = spring_accel * mass1 - vel1 * ((10.0 + mass1) * damping) wp.atomic_add(body_f, latch_idx, wp.spatial_vector(f1, wp.vec3(0.0)))
@wp.kerneldef _sync_cable_anchors( body_q: wp.array[wp.transform], body_qd: wp.array[wp.spatial_vector], plug_idx: int, anchor_indices: wp.array[int], anchor_offsets: wp.array[wp.vec3], anchor_rotations: wp.array[wp.quat],): """Copy the plug transform into kinematic cable bodies.""" tid = wp.tid() plug_tf = body_q[plug_idx] plug_pos = wp.transform_get_translation(plug_tf) plug_rot = wp.transform_get_rotation(plug_tf) idx = anchor_indices[tid] anchor_world = plug_pos + wp.quat_rotate(plug_rot, anchor_offsets[tid]) cable_rot = wp.normalize(wp.mul(plug_rot, anchor_rotations[tid])) body_q[idx] = wp.transform(anchor_world, cable_rot) body_qd[idx] = wp.spatial_vector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
@wp.kerneldef _align_cable_orientations( body_q: wp.array[wp.transform], cable_body_idx: wp.array[int], cable_next_idx: wp.array[int], cable_next_start_offsets: wp.array[wp.vec3],): """Swing-correct each dynamic cable capsule to its deformed segment direction.
Keeps the body origin (capsule midpoint/COM) fixed and only updates the rotation so +Z points toward the next capsule's start endpoint. """ tid = wp.tid() bi = cable_body_idx[tid] bi_next = cable_next_idx[tid]
tf = body_q[bi] pos = wp.transform_get_translation(tf) rot = wp.transform_get_rotation(tf)
next_tf = body_q[bi_next] next_pos = wp.transform_get_translation(next_tf) next_rot = wp.transform_get_rotation(next_tf) seg = next_pos + wp.quat_rotate(next_rot, cable_next_start_offsets[tid]) - pos seg_len = wp.length(seg) if seg_len < 1.0e-10: return d = seg / seg_len
z_current = wp.quat_rotate(rot, wp.vec3(0.0, 0.0, 1.0)) q_swing = quat_between_vectors_robust(z_current, d) rot_new = wp.normalize(wp.mul(q_swing, rot))
body_q[bi] = wp.transform(pos, rot_new)
def _load_mesh(stage, prim_path: str) -> tuple[newton.Mesh, wp.vec3]: """Load a mesh from USD, center at prim origin, and build SDF.
Returns: Tuple of (mesh, prim_pos) where prim_pos is the prim world-space translation [m], usable as the body/shape position. """ prim = stage.GetPrimAtPath(prim_path) usd_mesh = newton.usd.get_mesh(prim, load_normals=True)
tf = newton.usd.get_transform(prim, local=False) prim_pos = wp.transform_get_translation(tf)
vertices = np.array(usd_mesh.vertices, dtype=np.float32) indices = np.array(usd_mesh.indices, dtype=np.int32) normals = np.array(usd_mesh.normals, dtype=np.float32) if usd_mesh.normals is not None else None
mesh = newton.Mesh(vertices, indices, normals=normals) mesh.build_sdf( max_resolution=MESH_SDF_MAX_RESOLUTION, narrow_band_range=MESH_SDF_NARROW_BAND_RANGE, margin=SHAPE_CFG.gap, ) return mesh, prim_pos
def _load_cable_centerline(stage) -> tuple[wp.vec3, ...]: """Load cable centerline from the ``/World/CableCurve`` BasisCurves prim.
Returns world-space positions with :data:`PLUG_Y_OFFSET` applied. """ prim = stage.GetPrimAtPath("/World/CableCurve") all_points = UsdGeom.BasisCurves(prim).GetPointsAttr().Get()
tf = newton.usd.get_transform(prim, local=False) prim_pos = wp.transform_get_translation(tf)
return tuple( wp.vec3( float(p[0]) + float(prim_pos[0]), float(p[1]) + float(prim_pos[1]) + PLUG_Y_OFFSET, float(p[2]) + float(prim_pos[2]), ) for p in all_points )
class Example: def __init__(self, viewer, args=None): self.preset = presets.load(__file__) self.fps = self.preset.fps self.frame_dt = 1.0 / self.fps self.sim_time = 0.0 self.sim_substeps = self.preset.substeps self.sim_dt = self.frame_dt / self.sim_substeps
self.viewer = viewer self.insert = bool(getattr(args, "insert", False)) self.pick_stiffness = 50.0 self.pick_damping = 10.0
usd_path = newton.examples.get_asset("rj45_plug.usd") stage = Usd.Stage.Open(usd_path)
socket_mesh, sc = _load_mesh(stage, "/World/Socket") plug_mesh, pc = _load_mesh(stage, "/World/Plug") latch_mesh, lc = _load_mesh(stage, "/World/Latch")
builder = newton.ModelBuilder(gravity=(0.0, 0.0, -9.81)) builder.rigid_gap = 0.005
builder.add_ground_plane()
# Socket (static body) socket_shape = builder.add_shape_mesh( -1, mesh=socket_mesh, xform=wp.transform(sc, wp.quat_identity()), cfg=SHAPE_CFG, opacity=SOCKET_OPACITY, label="socket", )
# Plug (dynamic body, offset along -Y insertion axis) plug_pos = wp.vec3(pc[0], pc[1] + PLUG_Y_OFFSET, pc[2]) self._plug_body = builder.add_link( xform=wp.transform(plug_pos, wp.quat_identity()), label="plug", ) plug_shape = builder.add_shape_mesh( self._plug_body, mesh=plug_mesh, cfg=SHAPE_CFG, )
# Latch (dynamic body, same Y offset as plug) latch_pos = wp.vec3(lc[0], lc[1] + PLUG_Y_OFFSET, lc[2]) self._latch_body = builder.add_link( xform=wp.transform(latch_pos, wp.quat_identity()), label="latch", ) latch_shape = builder.add_shape_mesh( self._latch_body, mesh=latch_mesh, cfg=SHAPE_CFG, ) connector_shapes = (socket_shape, plug_shape, latch_shape)
# D6 joint: world -> plug (free translation, locked rotation) JointDof = newton.ModelBuilder.JointDofConfig d6_joint = builder.add_joint_d6( parent=-1, child=self._plug_body, linear_axes=( JointDof(axis=(1.0, 0.0, 0.0)), JointDof(axis=(0.0, 1.0, 0.0)), JointDof(axis=(0.0, 0.0, 1.0)), ), angular_axes=None, parent_xform=wp.transform(plug_pos, wp.quat_identity()), child_xform=wp.transform_identity(), )
# Revolute joint: plug -> latch (hinge along -X axis) rev_joint = builder.add_joint_revolute( parent=self._plug_body, child=self._latch_body, axis=(-1.0, 0.0, 0.0), parent_xform=wp.transform(lc - pc, wp.quat_identity()), child_xform=wp.transform_identity(), target_ke=LATCH_SPRING_KE, target_kd=LATCH_SPRING_KD, limit_lower=LATCH_LIMIT_LOWER, limit_upper=LATCH_LIMIT_UPPER, limit_kd=LATCH_LIMIT_KD, collision_filter_parent=True, )
builder.add_articulation([d6_joint, rev_joint]) self._connector_joints = [d6_joint, rev_joint]
cable_points = _load_cable_centerline(stage) rod = newton.Rod(cable_points, radius=CABLE_RADIUS) cable_quats = [wp.quat(*(float(value) for value in frame)) for frame in rod.quaternions] bend_stiffness = 1.0e1
rod_bodies, rod_joints = builder.add_rod( rod=rod, cfg=dataclasses.replace( builder.default_shape_cfg, ke=CONTACT_KE, kd=CONTACT_KD, mu=CABLE_MU, ), bend_stiffness=bend_stiffness, bend_damping=1.0e0, label="cable", )
# Collision-filter cable segments that overlap the plug at rest. for body_idx in rod_bodies[:CABLE_KINEMATIC_COUNT]: for cable_shape in builder.body_shapes[body_idx]: for conn_shape in connector_shapes: builder.add_shape_collision_filter_pair(cable_shape, conn_shape)
# Lock the kinematic prefix and the far cable end. for idx in (*rod_bodies[:CABLE_KINEMATIC_COUNT], rod_bodies[-1]): builder.body_mass[idx] = 0.0 builder.body_inv_mass[idx] = 0.0 builder.body_inertia[idx] = wp.mat33(0.0) builder.body_inv_inertia[idx] = wp.mat33(0.0)
anchor_body_ids = tuple(rod_bodies[:CABLE_KINEMATIC_COUNT]) anchor_offsets = tuple( 0.5 * (cable_points[i] + cable_points[i + 1]) - plug_pos for i in range(CABLE_KINEMATIC_COUNT) ) anchor_rots = tuple(cable_quats[i] for i in range(CABLE_KINEMATIC_COUNT))
self._cable_bodies, self._cable_joints = list(rod_bodies), list(rod_joints)
builder.color() self.model = builder.finalize()
self._cable_anchor_indices = wp.array(anchor_body_ids, dtype=int, device=self.model.device) self._cable_anchor_offsets = wp.array(anchor_offsets, dtype=wp.vec3, device=self.model.device) self._cable_anchor_rotations = wp.array(anchor_rots, dtype=wp.quat, device=self.model.device)
# Endpoint alignment: include the last kinematic body so it aims toward # the first dynamic body's start endpoint after deformation. The # kinematic prefix is reset by _sync_cable_anchors before each solve; # dynamic body rotations carry into the next collision pass. align_start = max(CABLE_KINEMATIC_COUNT - 1, 0) align_bodies = tuple(rod_bodies[align_start:-1]) align_next = tuple(rod_bodies[align_start + 1 :]) align_next_start_offsets = tuple( wp.vec3(0.0, 0.0, -0.5 * float(wp.length(cable_points[i + 2] - cable_points[i + 1]))) for i in range(align_start, len(rod_bodies) - 1) ) self._cable_align_indices = wp.array(align_bodies, dtype=int, device=self.model.device) self._cable_align_next = wp.array(align_next, dtype=int, device=self.model.device) self._cable_align_next_start_offsets = wp.array( align_next_start_offsets, dtype=wp.vec3, device=self.model.device ) self._cable_align_count = len(align_bodies)
self.viewer.set_model(self.model) self.viewer.picking_enabled = True
self.viewer.set_camera( pos=wp.vec3(0.125, plug_pos[1] - 0.025, 0.03), pitch=-10.0, yaw=180.0, ) self.viewer.camera_speed = 0.2
self.state_0 = self.model.state() self.state_1 = self.model.state() self.control = self.model.control() self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts()
self._initial_body_q = self.state_0.body_q.numpy().copy()
self.build_solver()
self._rest_pos = plug_pos self._latch_deflection_min = 0.0 self.gizmo_tf = wp.transform(plug_pos, wp.quat_identity())
self._pick_body = wp.array([-1], dtype=int, device=self.model.device) self._pick_target = wp.zeros(1, dtype=wp.vec3, device=self.model.device) self._gravity = wp.vec3(*self.model.gravity.numpy()[-1])
self.capture()
def build_solver(self): """Build the solver and everything bound to its instance; the tuner calls this again to swap it.""" if not self.preset.entries: self.solver = self.preset.make_solver(self.model) return # steel_vbd: Steel owns the plug and latch, VBD the cable rod; the connector proxies into VBD for # cable contact. self.model.rigid_contact_max = self.contacts.rigid_contact_max # Steel sizes its contact scratch from it coupling = self.preset.solver connector_bodies = [self._plug_body, self._latch_body] self.solver = SolverCoupledProxy( model=self.model, entries=[ SolverCoupled.Entry( name="steel", solver=lambda v: self.preset.entry("steel").make_solver(v), bodies=connector_bodies, joints=self._connector_joints, ), SolverCoupled.Entry( name="vbd", solver=lambda v: self.preset.entry("vbd").make_solver(v), bodies=self._cable_bodies, joints=self._cable_joints, ), ], coupling=SolverCoupledProxy.Config( proxies=[ SolverCoupledProxy.Proxy( source="steel", destination="vbd", bodies=connector_bodies, mass_scale=float(coupling.need("mass_scale")), mode=coupling.need("coupling_mode"), ) ], iterations=int(coupling.need("proxy_iterations")), ), )
def capture(self): with wp.ScopedCapture() as capture: self.simulate() self.graph = capture.graph
def simulate(self): for substep in range(self.sim_substeps): self.state_0.clear_forces() wp.launch( kernel=_apply_gizmo_force, dim=1, inputs=( self.state_0.body_q, self.state_0.body_qd, self.state_0.body_f, self.model.body_mass, self._pick_target, self.pick_stiffness, self.pick_damping, self._pick_body, self._plug_body, self._latch_body, self._gravity, ), device=self.model.device, ) self.viewer.apply_forces(self.state_0)
# Teleport kinematic cable bodies to follow the plug before the # solver runs, so joint constraints at the boundary see the # correct anchor positions and rest-relative rotations. wp.launch( kernel=_sync_cable_anchors, dim=CABLE_KINEMATIC_COUNT, inputs=( self.state_0.body_q, self.state_0.body_qd, self._plug_body, self._cable_anchor_indices, self._cable_anchor_offsets, self._cable_anchor_rotations, ), device=self.model.device, ) self.collision_pipeline.collide(self.state_0, self.contacts) self.solver.step(self.state_0, self.state_1, self.control, self.contacts, self.sim_dt) if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1: # an odd count under graph capture: copy the last result into state_0 instead of swapping, # so every replay of the captured graph starts from the newest state self.state_0.assign(self.state_1) else: self.state_0, self.state_1 = self.state_1, self.state_0
# Snap each capsule's +Z to the next capsule's start endpoint so # collision/render geometry follows the deformed centerline. wp.launch( kernel=_align_cable_orientations, dim=self._cable_align_count, inputs=( self.state_0.body_q, self._cable_align_indices, self._cable_align_next, self._cable_align_next_start_offsets, ), device=self.model.device, )
def step(self): if self.insert: dy = float(np.interp(self.sim_time, *zip(*INSERT_KEYS))) self.gizmo_tf = wp.transform( wp.vec3(self._rest_pos[0], self._rest_pos[1] + dy, self._rest_pos[2]), wp.quat_identity() ) gp = wp.transform_get_translation(self.gizmo_tf)
picking = getattr(self.viewer, "picking", None) picked_body = picking.pick_body if picking is not None else -1
self._pick_body.assign([picked_body]) self._pick_target.assign([gp])
if self.graph: wp.capture_launch(self.graph) else: self.simulate()
self.sim_time += self.frame_dt
if isinstance(self.solver, newton.solvers.SolverVBD): counts = self.solver.body_body_contact_counts.numpy() buf = self.solver.body_body_contact_buffer_pre_alloc overflow = np.where(counts > buf)[0] if len(overflow): for i in overflow: label = self.model.body_label[i] if hasattr(self.model, "body_label") else i print(f"[contact overflow] body {label} (idx={i}): {counts[i]} contacts (buffer={buf})") if self.insert: # Hinge angle about -X from the body poses (VBD leaves joint_q as built); the plug cannot rotate. body_q = self.state_0.body_q.numpy() plug_qx, plug_qw = body_q[self._plug_body][3], body_q[self._plug_body][6] latch_qx, latch_qw = body_q[self._latch_body][3], body_q[self._latch_body][6] latch_angle = -2.0 * float( np.arctan2(latch_qx * plug_qw - latch_qw * plug_qx, latch_qw * plug_qw + latch_qx * plug_qx) ) self._latch_deflection_min = min(self._latch_deflection_min, latch_angle) return
# Snap gizmo to the plug when the user isn't dragging it. gizmo_active = bool(getattr(self.viewer, "gizmo_is_using", False)) if not gizmo_active: plug_tf = self.state_0.body_q.numpy()[self._plug_body] if picked_body >= 0: snap = wp.vec3(*plug_tf[:3]) else: snap = wp.vec3(self._rest_pos[0], plug_tf[1], self._rest_pos[2]) self.gizmo_tf[:] = wp.transform(snap, wp.quat_identity())
def render(self): self.viewer.begin_frame(self.sim_time) self.viewer.log_gizmo("plug", self.gizmo_tf, rotate=()) self.viewer.log_state(self.state_0) self.viewer.log_contacts(self.contacts, self.state_0) self.viewer.end_frame()
def test_final(self): body_q = self.state_0.body_q.numpy() initial_q = self._initial_body_q for i in range(len(body_q)): assert np.all(np.isfinite(body_q[i])), f"Body {i} has non-finite transform" drift = np.linalg.norm(body_q[i] - initial_q[i]) assert drift < 1.0, f"Body {i} drifted {drift:.4f} from initial transform" if self.insert and self.sim_time >= INSERT_KEYS[-1][0]: backed_out = -PLUG_Y_OFFSET - (body_q[self._plug_body][1] - self._rest_pos[1]) assert backed_out < INSERT_LATCHED_TOL, f"plug backed out {backed_out * 1e3:.1f} mm: the latch did not hold" assert self._latch_deflection_min < -0.05, ( f"latch never deflected (min {self._latch_deflection_min:.3f} rad)" )
@staticmethod def create_parser(): parser = newton.examples.create_parser() parser.add_argument( "--insert", action="store_true", help="script the gizmo: insert the plug, then pull it back" ) return parser
if __name__ == "__main__": viewer, args = newton.examples.init(Example.create_parser()) newton.examples.run(Example(viewer, args), args)example = "contacts_rj45_plug"source = "newton/examples/contacts/example_contacts_rj45_plug.py @ skild-ai/newton 7565a26b1"default = "vbd"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py), plus --insert:# the scripted gizmo pushes the plug in and pulls it back; test_final asserts the latch deflected and held.args = ["--insert", "--num-frames", "240"]
[gallery]short = "An RJ45 plug moved into its socket with the translation gizmo."
[xpbd]status = "untested"
[vbd]status = "works"sim = { fps = 60, substeps = 6 }solver = { iterations = 12, rigid_body_contact_buffer_size = 256 }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[mujoco]status = "untested"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "unsupported"reason = "the cable is a rod of ROD joints, which SolverSteel rejects at construction (NotImplementedError); steel_vbd runs the plug and latch on Steel and the cable on VBD"
[kamino]status = "untested"
[implicit_mpm]status = "untested"
[style3d]status = "untested"
[steel_vbd]status = "works"# Steel owns the plug (D6) and the latch (revolute with its spring and limits), VBD the cable rod; plug and latch proxy# into VBD for cable contact. The mesh contacts need far more than the default 32 articulation rows; 512 is about# twice the measured peak. At 6 iterations the cable drifts off within a 10 s run.sim = { fps = 60, substeps = 6 }solver = { coupling_mode = "lagged", proxy_iterations = 1, mass_scale = 1.0 }entries.steel.solver = { solve_mode = "serial", iterations = 12, enable_joint_limits = true, articulation_constraint_max = 512 }entries.vbd.solver = { iterations = 12, rigid_body_contact_buffer_size = 256 }checked = "2026-11-11, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition"Steel docs and tutorials are licensed under Apache-2.0. Third-party notices. "Skild", "Skild AI" and the Skild logo are trademarks of Skild AI and are not licensed under Apache-2.0.