Skip to content

<DrawService />

<DrawService /> connects an embedded <Visualizer /> to a running draw server over Connect-RPC so any client/api calls made elsewhere render in this instance live. Skip it if you only render snapshots — snapshots are entirely client-side and don’t need the server.

The plugin is headless: it provides the connection context and starts the stream; it doesn’t render anything itself.

The Connect RPC client is required for the <DrawService />.

pnpm add @connectrpc/connect @connectrpc/connect-web
<script lang="ts">
	import { Visualizer } from '@viamrobotics/visualization'
	import { DrawService } from '@viamrobotics/visualization/plugins'
</script>

<div class="h-screen w-screen">
	<Visualizer>
		<DrawService config={{ backendIP: 'localhost' }} />
	</Visualizer>
</div>
PropTypeDescription
config{ backendIP: string; port?: string }Host and port of the draw server. port defaults to 3030.

The config prop is reactive — change it and the plugin reconnects to the new endpoint.

<DrawService /> opens one connection: a Connect-RPC stream for entity changes.

The stream reconnects on its own with exponential backoff, and resubscribing replays the full scene, so a dropped connection recovers without anything to act on.

The plugin reports connection lifecycle through the useLogs hook. Mount <Logs /> alongside it to see the messages in a panel; without it they’re dropped.

MessageLevelWhen
Connected to draw server at <url>infoThe stream delivers its first message.
Disconnected from draw server, reconnecting...warnAn established connection drops.
Could not reach draw server at <url>, retrying...warnThe first attempt fails, once per outage rather than per retry.
Draw server error: <message>errorAn attempt fails with a stream error.

Because the stream retries on a backoff, a server that stays down logs the “could not reach” warning once, not once per attempt. Errors also go to the browser console with their stack.

<DrawService /> is a client — it needs a draw server to talk to. You have three options:

The fastest path: run the visualizer locally (make up or pnpm dev). The local app already hosts a draw server on port 3030, and client/api calls to the same host land in it.

Viam Visualization exports a ready-made DrawService from github.com/viamrobotics/visualization/draw — the same Connect-RPC handler the local app uses. Wire it up to your own HTTP server:

package main

import (
	"log"
	"net/http"

	"connectrpc.com/connect"
	"github.com/viamrobotics/visualization/draw"
	"github.com/viamrobotics/visualization/draw/v1/drawv1connect"
)

func main() {
	svc := draw.NewDrawService("") // "" = use os.TempDir for chunked buffers

	mux := http.NewServeMux()
	mux.Handle(drawv1connect.NewDrawServiceHandler(svc))

	log.Fatal(http.ListenAndServe(":3030", mux))
}

Your client/api producer code then points at this server, and <DrawService config={{ backendIP }} /> in the browser subscribes to its stream.

If you need a custom backend (different language, different storage, extra side effects), the proto definitions are published under github.com/viamrobotics/visualization/draw/v1 (Go) and ship as .proto files in the source tree under protos/draw/v1/. Implement the DrawService RPCs in whichever language you like; <DrawService /> just needs a Connect-RPC server at the configured host:port.

Start with these, which between them cover everything client/api calls:

RPCWhy it’s needed
AddEntitySingle-entity draws (DrawGeometry, DrawPoints, DrawLine, and friends).
AddEntitiesBatch draws. DrawFrameSystem, DrawWorldState, DrawGeometriesInFrame, and DrawFrames send one batch rather than one request per entity, so a backend without this fails on the most common calls.
UpdateEntityPartial updates, and the delivery channel for chunked point-cloud payloads.
RemoveEntity, RemoveAll, RemoveAllTransforms, RemoveAllDrawingsRemoval.
StreamEntityChangesThe subscription <DrawService /> consumes. Replay the current scene as ADDED events on connect so a reconnecting client resyncs.
GetEntityChunkOnly if you support chunked entities.
SetScene, StreamSceneChangesOnly if you drive the camera from the server.
CreateRelationship, DeleteRelationshipOnly if you use entity relationships.

The cleared_scope field on StreamEntityChangesResponse is optional. It lets a bulk removal travel as one event instead of one per entity, but a backend that emits an individual REMOVED per entity works against the client unchanged.

See the client/api reference for the producer-side calls regardless of which server you use.