Skip to content

Embedding <Visualizer />

Common use cases: embedding a snapshot viewer next to a robot’s data, rendering test fixtures in a custom dashboard, or shipping the visualizer in a larger debugging surface.

A Svelte 5 / SvelteKit (or Astro+Svelte, Vite+Svelte, etc.) project. The package is svelte-typed and built for runes; you cannot consume it from Svelte 4.

pnpm add @viamrobotics/visualization

The package declares a list of peerDependencies (Svelte, Threlte, Three.js, the Viam SDK, prime-core, several Zag.js components, etc.). pnpm will print warnings for any that aren’t already in your project — install whatever it asks for.

Peers that only a plugin needs are marked optional, so you install them only if you mount that plugin: @dimforge/rapier3d-compat and @threlte/rapier for MoveFrame, @zag-js/tree-view and svelte-virtuallists for <WorldTree />, @zag-js/tabs for <Settings />, @threlte/xr for <XR />, earcut for <SelectionTool />, and the Connect RPC client for <DrawService />.

The visualizer’s scene imports two things a stock Vite config won’t handle: an HDR environment map and GLSL shader sources. Both need a line in your config, or the build fails parsing them as JavaScript (Unexpected identifier 'highp' is the shader one).

// vite.config.ts
import glsl from 'vite-plugin-glsl'
import { defineConfig } from 'vite'

export default defineConfig({
	assetsInclude: ['**/*.hdr'],
	plugins: [glsl()],
	resolve: {
		// three is a peer of both your app and the visualizer; resolve it from one place
		// so a second copy can't slip in. Two three instances break `instanceof`.
		dedupe: ['three'],
	},
})

vite-plugin-glsl is a build-time dependency of your app — install it alongside the package.

Threlte renders WebGL, which needs the browser. Disable SSR on any page that mounts <Visualizer />. In SvelteKit:

// src/routes/visualizer/+page.ts
export const ssr = false

<Visualizer /> is the entire visualizer rendered as a single Svelte component. It expects an explicitly-sized parent because it stretches to fill its container:

<script lang="ts">
	import { Visualizer } from '@viamrobotics/visualization'
</script>

<div class="h-screen w-screen">
	<Visualizer />
</div>

That’s enough to get an empty scene: the camera and its controls, the grid, the entity renderer, and the portal targets plugins render into. From there, you can:

  • Pass partID to bind it to a specific Viam machine part.
  • Pass cameraPose to set an initial camera position and look-at target.
  • Provide a children snippet to render your own Threlte primitives or the <Snapshot /> component inside the scene.

<Visualizer /> is the scene, not the app. It renders:

  • the <Canvas>, in renderMode="on-demand"
  • the camera, camera controls, and the reset / orthographic buttons
  • the grid, lighting, and environment
  • the entity renderer — everything spawned into the ECS world, including <PCD />
  • selection and hover affordances
  • the portal targets that plugins render their UI into

Everything else is a plugin you opt into — including the workspace modes: with no mode plugin mounted the mode is none and the visualizer is a bare renderer, with no toggle, no details cards, and no live-query pausing. The visualizer’s own chrome ships as six plugins:

PluginAdds
<Monitor />Monitor mode: the mode button and details cards
<WorldTree />The scene hierarchy panel
<Settings />The settings cog and its tabbed popover
<BuildFrames />Build mode: gizmos, snapping, frame editing
<FileDrop />Drag-and-drop loading of snapshots, PCD and PLY
<FramePov />Per-frame point-of-view panels

To reproduce the standalone app, mount all six plus the feature plugins you want, keeping Monitor before BuildFrames before MoveFrame — mode plugins’ mount order sets both the toggle’s button order and the fallback priority. To embed a focused viewer, mount none of them — or just the one or two that earn their space.

The smallest useful embed: a scene, a point cloud you already have the bytes for, and a ruler. <PCD /> parses in a worker and spawns an entity the visualizer’s renderer picks up, so nothing else is needed — no partID, and no network calls of its own.

<script lang="ts">
	import { PCD, Visualizer } from '@viamrobotics/visualization'
	import { MeasureTool } from '@viamrobotics/visualization/plugins'

	interface Props {
		data: Uint8Array
	}

	let { data }: Props = $props()
</script>

<div class="h-120 w-full">
	<Visualizer>
		<PCD {data} />
		<MeasureTool />
	</Visualizer>
</div>

Rebinding data reparses and respawns the entity, so this works for a polling feed as well as a one-shot capture.

<script lang="ts">
	import { Visualizer } from '@viamrobotics/visualization'
</script>

<div class="h-screen w-screen">
	<Visualizer
		partID="my-part-id"
		cameraPose={{ position: [2, 2, 2], lookAt: [0, 0, 0] }}
	>
		{#snippet children()}
			<!-- Anything Threlte-compatible. Runs inside <Canvas>. -->
		{/snippet}
	</Visualizer>
</div>

A snapshot is a serialized scene — every transform, drawing, and camera setting captured into one protobuf payload. Snapshots render purely client-side; no draw server, no live machine, no network calls.

Use the <Snapshot /> component as a child of <Visualizer />:

Put the snapshot in your app’s public/ (or static/) directory and fetch it at runtime. This is the pattern the playground uses.

<script lang="ts">
  import { onMount } from 'svelte'
  import { Visualizer } from '@viamrobotics/visualization'
  import { Snapshot, SnapshotProto } from '@viamrobotics/visualization/lib'

  let snapshot = $state<SnapshotProto | undefined>()

  onMount(async () => {
    const response = await fetch('/scene.snapshot.pb')
    const buffer = await response.arrayBuffer()
    snapshot = SnapshotProto.fromBinary(new Uint8Array(buffer))
  })
</script>

<div class="h-screen w-screen">
  <Visualizer>
    {#if snapshot}
      <Snapshot {snapshot} />
    {/if}
  </Visualizer>
</div>

You can re-bind a different snapshot at any time — <Snapshot /> watches the prop and respawns the scene entities when the value changes.

The playground renders this snapshot by default. To experiment with it, download visualization_snapshot.json, edit a few values, and drag the modified file onto the playground — the scene re-renders with your changes. The drop zone accepts .json, .pb, and .pb.gz snapshot files, but they must be named with a visualization_snapshot prefix (see src/lib/plugins/FileDrop/file-names.ts) for the loader to recognize them.

Snapshots are produced from Go using the draw package. The most common pattern: build up a *draw.Snapshot, then call MarshalBinary() (or MarshalJSON() for human-readable output) and ship the bytes wherever your frontend can fetch them.

A minimal producer:

package main

import (
    "os"

    "github.com/golang/geo/r3"
    "github.com/viamrobotics/visualization/draw"
    "go.viam.com/rdk/spatialmath"
)

func main() {
    snapshot := draw.NewSnapshot(
        draw.WithGrid(true),
        draw.WithSceneCamera(draw.NewSceneCamera(
            r3.Vector{X: 2000, Y: 2000, Z: 2000},
            r3.Vector{X: 0, Y: 0, Z: 0},
        )),
    )

    box, _ := spatialmath.NewBox(
        spatialmath.NewZeroPose(),
        r3.Vector{X: 100, Y: 100, Z: 100},
        "box",
    )
    _, _ = snapshot.DrawGeometry(draw.DrawGeometryOptions{
        Geometry: box,
        Pose:     spatialmath.NewZeroPose(),
        Parent:   "world",
        Color:    draw.ColorFromName("red"),
    })

    bytes, _ := snapshot.MarshalBinary()
    _ = os.WriteFile("scene.snapshot.pb", bytes, 0644)
}

See the draw API reference for the full set of Snapshot.Draw* methods.

A few components are exported via the /lib subpath and work without a <Visualizer /> parent — useful if you just need a piece of the visualizer (e.g. an axes helper) inside your own Threlte canvas:

<script lang="ts">
	import { Canvas } from '@threlte/core'
	import { AxesHelper } from '@viamrobotics/visualization/lib'
</script>

<Canvas>
	<AxesHelper />
</Canvas>

<Snapshot /> is also exported from /lib, but unlike <AxesHelper /> it requires the context that <Visualizer /> provides — use it as a child of <Visualizer />, not standalone.