URDF Studio

#Renderer

src/renderer/main.ts (≈1800 lines) owns the Three.js scene, the URDFLoader call, the joint panel, the link tree, the inspector, the checks panel, and the tools panel. It is bundled separately and identical on both targets — the only thing that differs is what's posting messages to it.

#Message protocol

Every interaction between host and renderer goes through postMessage:

host → renderer
  loadRobot                  full payload (URDF text, metadata, packageMap, …)
  recenter                   reset camera to iso fit
  exportPose                 ask renderer to report current pose
  captureScreenshot          ask renderer to render and report a PNG dataURL
  sampleReachability         trigger reachability sampling
  requestPoseSnapshot        ask renderer to report pose (used for hot reload)
  bookmarksUpdated           push new bookmark list (after add/delete/rename)
  disableCollisionsUpdated   push new disable_collisions list after SRDF write

renderer → host
  ready                      handshake on startup
  geometryLoaded             all meshes finished loading (used to commit blobs)
  poseSnapshot               reply to requestPoseSnapshot
  exportPoseResult           reply to exportPose
  screenshotResult           reply to captureScreenshot
  reloadWithXacroArgs        user changed xacro args; please re-expand
  jointChanged               user moved a joint
  selectionChanged           user selected a link
  requestSavePose            user clicked Save Pose
  requestSaveBookmark        user clicked Save As
  requestDeleteBookmark      user removed a bookmark
  requestRenameBookmark      user renamed a bookmark
  requestWriteDisableCollisions  user clicked Write SRDF in the Tools panel

In VS Code this rides on webview.postMessage between the extension process and the webview iframe. In the browser, host and renderer are on the same page; we use window.postMessage(msg, '*') which dispatches a synchronous MessageEvent to the same window's listeners.

#acquireVsCodeApi shim

The renderer calls acquireVsCodeApi() once at startup to get a { postMessage, setState, getState } triple. In the VS Code webview this is real. In the browser, the host installs a shim before the renderer loads:

globalThis.acquireVsCodeApi = () => ({
  postMessage(msg)  { queueMicrotask(() => host.handle(msg)); },
  setState(_)       { /* no-op */ },
  getState()        { return null; }
});

The queueMicrotask defers handling so the renderer's call site finishes synchronously first, matching VS Code's behaviour.

#URL modifier and the vfs:// scheme

The renderer never knows what the underlying filesystem looks like. It only sees the packageMap and sourceBaseUri fields of the loadRobot message, which the host builds. URDFLoader concatenates these with the mesh filename to produce a URL like:

urdf-studio-vfs:///franka_description/meshes/robots/fr3/visual/link0.dae

Three.js can't fetch that — there's no real urdf-studio-vfs: scheme. The renderer installs a LoadingManager.setURLModifier that maps these URLs to blob URLs at fetch time:

if (vfsUrlMap && vfsScheme) {
  manager.setURLModifier(url => {
    if (!url.startsWith(vfsScheme)) return url;
    return vfsUrlMap[url] || vfsUrlMap[normalizeVfsUrl(url, vfsScheme)] || url;
  });
}

normalizeVfsUrl collapses empty segments (a//ba/b), drops ., and resolves ... This handles the case where DAE/GLTF files reference textures with relative paths that resolve outside the visible mesh directory.

VS Code build never sets vfsUrlMap, so the modifier is inert and URLs pass through to webview-resolved file URIs.

#Blob URL lifecycle

URL.createObjectURL pins the underlying File reference; the browser won't reclaim that memory until you call URL.revokeObjectURL. To avoid leaking on reloads, the browser VFS maintains a two-generation cache.

#Renderer-internal mesh cache

Once a mesh has been parsed by Three.js (geometry + materials), the parsed object is cached in a module-level meshCache: Map<url, Object3D>. Subsequent loads of the same URL clone the cached object rather than re-parsing. This matters for robots with repeated links (gripper fingers, multi-axis carriage rails, etc.).

The cache is cleared when a loadRobot arrives for a different document, or when the renderer's reload-with-collision path runs.