Publishing Arm State and Assets
This page covers how a mode publishes arm geometry and extra scene objects back to the client.
Publishing arm state
By default, DualArmSimulationBase.arm_states() publishes one ArmState for left_rod and one for right_rod.
The base implementation extracts:
centerlinefromrod.position_collectionradiifromrod.radiuselement_lengthsfromrod.lengthswhen availabledirectorsfromrod.director_collectiontip.rotation_xyzwfrom the final director framecontact_pointsfromcontact_points_for_arm()
For many modes that is enough. If your rod state already lives in a standard PyElastica-like structure, you often do not need to override arm_states() at all.
Adding contact-point trails
If you only need extra visualization points, override contact_points_for_arm():
def contact_points_for_arm(self, arm_id: str) -> list[list[float]]:
queue = self._recording_queues.get(arm_id)
if queue is None:
return []
return [point for _, point in queue]
This keeps the default ArmState conversion while adding mode-specific point data.
Customizing ArmState
If you need a different arm representation entirely, override arm_states() or _rod_to_arm_state() and return your own ArmState objects.
from virtual_field.core.state import ArmState, Transform
def arm_states(self) -> dict[str, ArmState]:
return {
self.arm_ids[0]: ArmState(
arm_id=self.arm_ids[0],
owner_user_id=self.user_id,
base=Transform(
translation=self.base_left,
rotation_xyzw=[0.0, 0.0, 0.0, 1.0],
),
tip=Transform(
translation=[0.0, 1.0, -0.5],
rotation_xyzw=[0.0, 0.0, 0.0, 1.0],
),
centerline=[[0.0, 1.0, 0.0], [0.0, 1.0, -0.5]],
radii=[0.03],
),
self.arm_ids[1]: ...,
}
Important note: the current backend treats registered simulation modes as dual-arm modes and forces arm_count = 2 during registration. If you want a mode with more than two simulated arms, you will need backend, mapper, and client changes in addition to the mode class itself.
Publishing spheres
If your mode owns dynamic spheres, keep the physics object inside the simulator and expose it through sphere_entities().
CathyThrowSimulation is the current example. It creates Elastica spheres in __post_init__ and converts them into SphereEntity values each tick:
from virtual_field.core.state import SphereEntity
def sphere_entities(self) -> list[SphereEntity]:
spheres: list[SphereEntity] = []
for idx, sphere in enumerate(self.spheres):
position = np.asarray(sphere.position_collection[..., 0], dtype=np.float64)
spheres.append(
SphereEntity(
sphere_id=f"{self.user_id}_my_mode_sphere_{idx}",
owner_id=self.user_id,
translation=position.tolist(),
radius=float(sphere.radius),
color_rgb=[0.95, 0.62, 0.32],
)
)
return spheres
The backend automatically picks these up:
once during
register_user()again after every
simulation.step()
Use stable ids such as f"{self.user_id}_my_mode_sphere_{idx}" so updates replace the existing sphere instead of looking like new objects each frame.
Publishing meshes
For static or generated scenery, override mesh_entities() and return MeshEntity values. NoelC4Simulation uses this for obstacle cylinders.
This is the right place for:
generated GLTF data URIs
environment props tied to one user session
procedurally placed obstacles that the client should render