#Browser host
src/web/host.ts is the browser equivalent of src/extension.ts. It
discovers packages, expands xacro, runs URDF analysis, and emits the
loadRobot message to the renderer.
#Module layout
src/web/
main.ts Boot: install CoreIo, host, UI, then import renderer.
host.ts The host. Owns the active document, posts loadRobot.
ioBrowser.ts CoreIo impl backed by BrowserVfs + native DOM.
storage.ts localStorage for pose / bookmarks / settings.
ui/app.ts Topbar shell: folder picker, file dropdown, settings.
ui/onboarding.ts First-run tour.
ui/web.css Visual refresh tokens.
vfs/
types.ts BrowserVfs interface.
directoryHandle.ts File System Access API impl.
fileList.ts webkitdirectory fallback.
posixPath.ts Tiny path utility.
#The BrowserVfs interface
Two implementations:
DirectoryHandleVfs— wraps aFileSystemDirectoryHandle. Lazily reads files on demand; only the path index is built up front. Best on Chromium-based browsers.FileListVfs— wraps awebkitdirectoryFileList. The browser reads file metadata into memory before handing us the list; reads happen viaFile.text()/File.arrayBuffer(). Used on Safari and mobile Chrome where File System Access isn't exposed.
Both implement the same surface: readText, readBytes, readTextSync
(from a pre-warmed cache), existsSync, readdir, getBlobUrl, plus
generation methods.
#Loading a document — what happens
- User picks a folder. AppShell instantiates a VFS, populates the file dropdown with every URDF/xacro file.
- User selects a file. AppShell calls
host.openDocument(absPath). - Host runs:
vfs.warmTextCache(allYamlsInVfs)— every.yaml/.ymlis loaded into the sync cache so xacro'sload_yaml()works synchronously. The aggressive variant; targeted regex scans miss indirect refs.discoverPackages([vfs.root, ...extraRoots])— recursive scan viaio.readdir/io.readText.renderRobotDocument(absPath, packages, xacroArgs)— xacro expansion usingglobalThis.DOMParser(no jsdom!).analyzeUrdf(urdf, absPath, packages)— link/joint graph, mesh resolution, inertia checks.loadSemanticMetadata(semanticFiles, packages)— SRDF + YAML.vfs.beginGeneration()+preallocateMeshUrls— see below.postToRenderer({ type: 'loadRobot', … }).
- Renderer receives
loadRobot, builds the Three.js scene, fetches meshes via the URL modifier, eventually postsgeometryLoaded. - Host commits the blob generation (
vfs.commitGeneration()).
#Blob URL lifecycle
URL.createObjectURL pins the underlying File reference. To prevent
leaks across reloads, BrowserVfs uses a two-generation cache:
beginGeneration()at load start movescurrentBlobstopreviousBlobs. Any leftover previous-gen from a load that never completed gets revoked here (so abandoned loads don't accumulate).getBlobUrl(path)first checkscurrentBlobs. If a previous-gen URL exists for the same path, it's promoted to current rather than re-allocated — saves churn when consecutive loads share meshes.commitGeneration()runs when the renderer postsgeometryLoaded. Previous-gen URLs not carried over are revoked.
A Playwright spec verifies that alternating between two URDFs keeps the live blob count bounded.
#Pre-allocation: meshes, DAE textures, GLTF assets
URL modifier is synchronous (it returns a string). We can't await getBlobUrl inside it. So before sending loadRobot, the host
pre-allocates a URL for every asset that might be fetched:
- Every mesh in
metadata.mesheswithexists === true. - Every external
<image>/<init_from>reference inside the DAE files (parsed textually). - Every external buffer / image referenced in GLTF JSONs (parsed JSON).
- GLB binaries embed their assets — no pre-parsing needed.
The renderer sees the resulting vfsUrlMap in loadRobot and uses it
to install the URL modifier.
#URI conventions
| Use | Shape | Example |
|---|---|---|
sourceBaseUri (workingPath) |
trailing slash REQUIRED | urdf-studio-vfs:///fr/robots/fr3/ |
packageMap[pkg] |
trailing slash FORBIDDEN | urdf-studio-vfs:///fr |
vfsUrlMap keys |
canonical | urdf-studio-vfs:///fr/meshes/foo.dae |
The asymmetry exists because URDFLoader does packages[pkg] + '/' + relPath
internally; if our packageMap entry had a trailing slash, every URL
would gain a double slash and miss the lookup.
#Settings, pose, bookmarks
All persisted in localStorage:
| Key | Shape |
|---|---|
urdf-studio:settings:v1 |
UserSettings object |
urdf-studio:pose:v1 |
{ <vfsLabel><absPath>: PreviewState } |
urdf-studio:bookmarks:v1 |
{ <vfsLabel><absPath>: PoseBookmark[] } |
urdf-studio:onboarded:v1 |
'1' when the tour has been dismissed |
localStorage failures (private mode, blocked) degrade to in-memory only.