Navigation
The Navigation module lets an Inverse3 cursor move the application's workspace — sometimes called rate-control locomotion or "gamepad-like drift". Instead of the cursor hitting a hard wall at the edge of the physical workspace, it enters a soft shell where its distance from a virtual centre is mapped to a velocity that slides the whole workspace. The further the cursor pushes, the faster the scene scrolls.
The primary (and currently only) behaviour is Bubble Navigation. The bubble shape is defined using an SDF primitive — see What is an SDF? for the concept.
Bubble Navigation — concept
A virtual bubble is anchored around a centre point in the device's mount space. The cursor experiences three concentric zones:
┌──────────────────────────────────────────┐
│ WALL ZONE │ cursor beyond outer shell
│ ┌────────────────────────────────────┐ │
│ │ VELOCITY ZONE │ │ soft shell → scene moves
│ │ ┌──────────────────────────────┐ │ │
│ │ │ DEAD ZONE (inside) │ │ │ no scene movement
│ │ │ │ │ │
│ │ │ ● centre │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ bubble surface │ │
│ └────────────────────────────────────┘ │
│ outer boundary │
└──────────────────────────────────────────┘
| Zone | Cursor position | Behaviour |
|---|---|---|
| Dead zone | Inside the bubble | No navigation. Light damping. You can manipulate the scene freely. |
| Velocity zone | Between the surface and the outer boundary | Distance → velocity curve moves the workspace in the cursor's direction. |
| Wall zone | Past the outer boundary | A hard spring pushes the cursor back in and navigation velocity saturates. |
A spring-damper haptic force is applied across all three zones — you feel the surface, the drift shell, and the outer wall.
Quick start — enable bubble navigation
Navigation is a persistent, one-shot configuration — send it once and it stays active until you explicitly stop it, restart the service, or close the session.
Start (minimal — default rounded-box bubble)
- WebSocket
- HTTP
{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": { "mode": "bubble" }
}
}
]
}
curl -X POST "http://localhost:10001/inverse3/04C3/config/navigation?session=:0" \
-H "Content-Type: application/json" \
-d '{"mode": "bubble"}'
Stop
- WebSocket
- HTTP
{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": { "mode": "disabled" }
}
}
]
}
curl -X DELETE "http://localhost:10001/inverse3/04C3/config/navigation?session=:0"
Other HTTP routes
| Method | Path | Purpose |
|---|---|---|
GET | /{type}/{id}/config/navigation?session=<expr> | Current navigation config + state |
POST | /{type}/{id}/config/navigation?session=<expr> | Start or update navigation |
DELETE | /{type}/{id}/config/navigation?session=<expr> | Stop navigation |
Bubble shape catalogue
The bubble's dead zone is described by a signed-distance-function (SDF) shape. Different shapes give different navigation feels — a rounded box (the default) for a comfortable rectangular rest zone, a sphere for isotropic drift, an ellipsoid to favour certain axes, a capsule for corridors.
Rounded box (default)
The default dead zone is a flat, slightly taller-than-deep volume — tuned to feel comfortable on both Inverse3 and MInverse:
{
"shape": {
"primitive": "rounded_box",
"parameters": { "b": { "x": 0.05, "y": 0.02, "z": 0.03 }, "r": 0.01 }
}
}
Sphere — isotropic drift
{
"shape": {
"primitive": "sphere",
"parameters": { "r": 0.05 }
}
}
Ellipsoid — wider in X/Z than Y
Use when horizontal drift should feel looser than vertical:
{
"shape": {
"primitive": "ellipsoid",
"parameters": { "a": { "x": 0.06, "y": 0.03, "z": 0.06 } }
}
}
Capsule — elongated corridor
Two end points a/b plus a radius r:
{
"shape": {
"primitive": "capsule",
"parameters": {
"a": { "x": 0.0, "y": -0.03, "z": 0.0 },
"b": { "x": 0.0, "y": 0.03, "z": 0.0 },
"r": 0.04
}
}
}
Full example with custom sizing and motion feel:
{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": {
"mode": "bubble",
"bubble": {
"shape": {
"primitive": "ellipsoid",
"parameters": { "a": { "x": 0.06, "y": 0.03, "z": 0.06 } }
},
"velocity_zone_width": 0.025,
"max_velocity": 1.0,
"velocity_ease": "quadratic_in"
}
}
}
}
]
}
Velocity curve
The velocity-zone distance is mapped through an easing curve before being turned into workspace velocity. Pick the easing that matches how you want the scene to accelerate as the user pushes further into the shell.
velocity_ease | Feel | When to use |
|---|---|---|
linear | Constant ramp | Predictable, good default |
quadratic_in | Slow start, faster push | Precise near the surface, fast long travel |
cubic_in | Very slow start | Very precise, long travel ramps up |
sine_out | Smooth fast start, plateaus near outer | Responsive, caps gently |
quadratic_out | Fast at entry, comfortable long-distance | Fast nudge, comfortable cruising |
{ "velocity_ease": "quadratic_in", "max_velocity": 1.5 }
Centre behaviour
The bubble's centre is configured through a nested center object with four
fields — position, relative, follow, speed — replacing the older flat
layout made of center, center_enabled, center_mode, and center_drift_speed.
{
"center": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"relative": true,
"follow": false,
"speed": 0.02
}
}
Where the centre is anchored
position and relative control the initial centre location at activation:
relative | Semantics of position |
|---|---|
true (default) | position is an offset from the cursor — the initial centre is cursor + position. With the default (0, 0, 0), the bubble spawns exactly on the cursor. |
false | position is an absolute point in application space — it is converted to mount space on ingress and converted back to application space on egress, so round-trip values match. |
How the centre moves afterwards
follow and speed control the per-tick behaviour once the bubble is active:
follow | speed | Behaviour | Previous equivalent |
|---|---|---|---|
false (default) | ignored | Centre stays where it was initialised — fixed centre. | center_mode: "fixed" |
true | > 0 | Centre drifts toward the cursor at speed m/s while the cursor is inside. | center_mode: "auto_follow" (with center_drift_speed) |
true | 0.0 | Centre snaps to the cursor each tick — teleport feel. | center_mode: "track_cursor" |
The default centre behaviour is now fixed (follow = false). Earlier versions
drifted toward the cursor by default (auto_follow). Set center.follow = true
to restore the old drift behaviour, and tune center.speed to match the old value
of center_drift_speed.
When follow = true and the cursor pushes past the outer boundary, the centre
tracks the cursor regardless of speed, to keep the bubble around it and
prevent the cursor from escaping.
Collision response
The bubble reacts to external cursor forces exceeding collision_detection.force_threshold:
- The velocity zone temporarily inflates (up to
collision_detection.inflate_ratiotimesvelocity_zone_width) to give the user more room to manoeuvre around the obstacle. - When
collision_detection.enabledistrue(the default), the bubble centre refuses to drift in the direction of the collision force, preventing the user from dragging the workspace into a hard wall.
Collision detection uses hysteresis to avoid flicker around the threshold:
once a collision is active it stays active until |ext_force| drops below the
product collision_detection.force_threshold × collision_detection.exit_ratio.
Lower the exit_ratio to widen the hysteresis band (stickier collisions); set
it to 1.0 to disable hysteresis and use a single threshold.
{
"collision_detection": {
"enabled": true,
"force_threshold": 0.5,
"inflate_ratio": 2.0,
"exit_ratio": 0.7
}
}
Parameters reference
Most commonly tuned
| Parameter | Default | Description |
|---|---|---|
shape | {rounded_box, r=0.01, b=(0.05,0.02,0.03)} | SDF shape defining the dead zone |
velocity_zone_width | 0.03 m | Thickness of the rate-control shell |
max_velocity | 0.5 m/s | Maximum navigation velocity |
velocity_ease | "quadratic_in" | Distance → velocity easing curve |
center.follow | false | Whether the centre tracks the cursor |
center.speed | 0.02 m/s | Drift rate when follow = true |
Full parameter list
| Field | Type | Default | Description |
|---|---|---|---|
center.position | vec3 | (0, 0, 0) | Initial centre — cursor offset when relative = true, absolute app-space point when false |
center.relative | bool | true | true: position is an offset from the cursor. false: absolute app-space point, converted to mount space at the boundary |
center.follow | bool | false | false: centre stays fixed at its initialised position. true: centre tracks the cursor |
center.speed | float | 0.02 | Drift speed (m/s) when follow = true; 0.0 snaps to the cursor every tick |
shape | shape | {rounded_box, r=0.01, b=(0.05,0.02,0.03)} | Dead-zone SDF shape |
velocity_zone_width | float | 0.03 | Width of the rate-control shell (m) |
max_velocity | float | 0.5 | Max navigation velocity (m/s) |
velocity_ease | string | "quadratic_in" | Distance → velocity easing |
reset_velocity_on_entry | bool | true | Zero the accumulated speed when the cursor re-enters the velocity zone |
bump_width | float | 0.005 | Tactile bump at the surface (m) |
bump_stiffness | float | 200.0 | Surface bump spring constant |
spring_inner | float | 0.0 | Spring at the bubble centre |
spring_surface | float | 10.0 | Spring at the surface |
spring_outer | float | 15.0 | Spring at the outer boundary |
wall_stiffness | float | 700.0 | Hard-wall spring past the outer boundary |
damping_inner | float | 0.1 | Damping at the centre |
damping_surface | float | 4.0 | Damping at the surface |
damping_outer | float | 7.0 | Damping at the outer boundary |
rotation_enabled | bool | false | Apply the workspace rotation to the navigation direction |
scale_enabled | bool | false | Apply the workspace scale to the navigation velocity |
collision_detection.enabled | bool | true | Block navigation in the collision direction |
collision_detection.force_threshold | float | 1.0 | External force magnitude to enter a collision (N) |
collision_detection.inflate_ratio | float | 2.0 | Multiplier for the velocity-zone width during a collision (must be ≥ 1.0) |
collision_detection.exit_ratio | float | 0.7 | Hysteresis ratio — stay in collision until |ext_force| < force_threshold × exit_ratio. Range (0, 1]; 1.0 disables hysteresis |
Two feature groups are omitted from the 3.5 JSON schema while their implementations are reshaped for 3.6:
- Avatar-boundary clamping: the fields
avatar_boundary_enabled,avatar_boundary, andavatar_boundary_hysteresis. - Workspace bounding: the fields
workspace_bounded,workspace_transition_speed, andworkspace_transition_ease.
The tick code still compiles and runs with defaults, but clients cannot read or
write those fields via HTTP or WebSocket. In 3.6 the avatar boundary will
return as a top-level bounds peer of the bubble object with a full placement
transform; workspace bounding will return once the centre-jitter investigation
lands.
Validation rules
velocity_zone_width > 00 ≤ bump_width < velocity_zone_widthmax_velocity > 0bump_stiffness ≥ 0spring_inner ≥ 0andspring_inner ≤ spring_surface ≤ spring_outerwall_stiffness ≥ 0damping_inner ≥ 0anddamping_inner ≤ damping_surface ≤ damping_outercollision_detection.force_threshold > 0collision_detection.inflate_ratio ≥ 1.0collision_detection.exit_ratioin(0, 1]center.speed ≥ 0
A POST or configure.navigation with invalid parameters is rejected and an
invalid-value event is emitted; the previous configuration remains active.
Events
| Event name | Fired when |
|---|---|
navigation-started | Navigation is activated on a device |
navigation-updated | Navigation config is updated while already active |
navigation-stopped | Navigation is stopped (explicit disable, DELETE, or session close) |
invalid-value | A navigation config is rejected by validation |
Known limitations
- Workspace bounding and avatar boundaries hidden in 3.5: both feature groups are absent from the JSON schema while they are reshaped for 3.6. See the note above under Parameters reference.
- Non-uniform scale + rotation: when both
rotation_enabledandscale_enabledare on, the velocity direction is slightly inaccurate — rotation is not applied to the scale axes. - Per-device size scaling: bubble sizes (radius, zone width) are not scaled by the device's physical scale factor — MInverse and Inverse3 use the same absolute sizes, which may feel different on each.