Mount & Workspace Transforms
Two transforms position the device in application space. Understanding the pipeline helps you pick the right one.
Coordinate pipeline
| Stage | What it does | How to set it | Persistent? |
|---|---|---|---|
| Basis | Remaps axes (e.g., Z-up → Y-up) | session.configure.basis — see tutorial 08 (HTTP remote config) | Yes |
| Mount transform | Physical mounting offset of the device (arm position, rotation, scale) | configure.mount or configure.preset — see tutorial 08 (HTTP remote config) | Yes |
| Workspace transform | Camera / scene navigation drift | commands.set_transform — see tutorial 09 (WebSocket per-tick streaming) | Yes (sticky) |
Mount transform
The mount describes where the device physically sits — its arm offset, orientation, and scale. It's set once (or when the user changes the physical setup) and rarely updated at runtime.
Presets (factory configurations)
Instead of computing the mount yourself, pick a named preset:
| Preset | Origin placement | Arm facing |
|---|---|---|
defaults | Device base | Forward (arm front) |
arm_front | Device base | Forward |
arm_front_centered | Workspace centre | Forward |
led_front | Device base | LED side forward |
led_front_centered | Workspace centre | LED side forward |
custom | Manual — set mount yourself | — |
A preset sets the mount, basis, and workspace origin in one shot.
Setting the mount directly
- WebSocket
- HTTP
{
"inverse3": [{
"device_id": "049D",
"configure": {
"mount": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}]
}
curl -X POST "http://localhost:10001/inverse3/049D/config/mount?session=:0" \
-H "Content-Type: application/json" \
-d '{"transform":{"position":{"x":0,"y":0,"z":0},"rotation":{"w":1,"x":0,"y":0,"z":0},"scale":{"x":1,"y":1,"z":1}}}'
mount and preset are mutually exclusiveSending both in the same message is ambiguous — the service rejects it.
Choose one: a named preset (which sets the mount for you) or an explicit
mount transform.
When sending a mount, wrap the transform: { "mount": { "transform": { … } } }.
When reading a mount from the snapshot, it's flat: { "mount": { "position": …, "rotation": …, "scale": … } }.
This is by design — commands use the unified command_data wrapper; snapshots
serialize the core::transform directly.
Workspace transform
The workspace transform is a runtime drift applied on top of the mount. Its typical use case is scene navigation — panning, rotating, or zooming the application's view while the device stays physically in place.
Both mount and workspace transforms are persistent — the service remembers the last value you sent. The difference is purpose and performance:
- Mount is designed to be set once and stay — it describes the physical setup and is optimised for infrequent updates.
- Workspace transform is designed to be streamed at high frequency (e.g., every frame during camera navigation) — the internal pipeline is optimised for this update rate.
- WebSocket
- HTTP
{
"inverse3": [{
"device_id": "049D",
"commands": {
"set_transform": {
"transform": {
"position": { "x": 1.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}]
}
curl -X POST "http://localhost:10001/inverse3/049D/state/transform?session=:0" \
-H "Content-Type: application/json" \
-d '{"transform":{"position":{"x":1,"y":0,"z":0},"rotation":{"w":1,"x":0,"y":0,"z":0},"scale":{"x":1,"y":1,"z":1}}}'
GET returns the current transform; DELETE resets to identity.
Scaling the mount or workspace changes the force the device renders, because scaling affects the penetration depth used to compute the contact force. For example, a 2x scale roughly halves the penetration depth (the virtual scene is twice as large relative to the device's travel), so the resulting force is roughly half of what you'd feel at 1x. Conversely, a 0.5x scale roughly doubles the force.
This is usually the behavior you want: zooming out of a haptic scene naturally produces weaker contacts (you're "farther away"), and zooming in makes them stronger. If that's not what you want — e.g., you need stiffness to stay constant as the user zooms — compensate by scaling your force gains inversely.
If you need continuous locomotion (bubble navigation, rate-control drift),
use the Navigation module instead of manually
streaming set_transform every tick — it handles the physics, haptic
feedback, and workspace bounding for you.