VectoJS Three
Use this skill to place a live VectoJS 2D interface onto a Three.js texture and route 3D pointer input back into the VectoJS scene.
Integration workflow
- Install
@vectojs/core,@vectojs/three,three, and@vectojs/uiif using UI components. - Create one
ThreeAdapterper VectoJS panel texture. - Add VectoJS entities to
adapter.vectoScene, then start the inner scene. - Add
adapter.meshor the adapter texture/material to the host Three.js scene. - In the host pointer/wheel loop, raycast against the adapter mesh and call
adapter.updateIntersection(...). - Call
adapter.dispose()when removing the panel.
Read references/three-recipes.md for snippets.
Constraints
- The default output is a flat textured plane, not DOM rendered in 3D.
- VectoJS logical hit-testing remains 2D even when the mesh is transformed in world space.
- The host application owns the camera, renderer, controls, XR session, raycaster, and occlusion rules.
- Raycast UVs should map to the adapter’s logical
width/height; do not use backing-store size for layout math. - Texture resolution affects sharpness and upload cost. Choose dimensions for the viewing distance.
Common mistakes
| Mistake | Correction |
|---|---|
| Adding an offscreen adapter canvas to the DOM | Add adapter.mesh to Three.js; the canvas backs the texture. |
Forgetting to call updateIntersection |
Route host raycaster events into the adapter each pointer/wheel event. |
| Dispatching by screen coordinates | Use raycast UVs through ThreeAdapter. |
| Keeping disposed textures alive | Call adapter.dispose() and remove host references. |
| Expecting full DOM a11y inside XR | Treat the 3D panel as canvas texture; provide host-level semantics where needed. |
Programmatic input and panel focus
The adapter can drive input without a raycaster or DOM — the entry point for tests and automation:
dispatchPointer(type, x, y, init?)synthesizes pointer events at logical scene coordinates through the same downstream path asupdateIntersection(hover transitions, entity dispatch, pointerdown-driven focus, texture-dirty scheduling). Returns whether the point hit an entity. Wheel is deliberately not covered — route realWheelEvents throughupdateIntersection.dispatchKey(key, mods?, phase?)synthesizes keyboard events (phase'press'is the default full keydown+keyup pair;'keydown'/'keyup'model held keys). With panel focus, the event hits the focused entity's projected mirror so core's own listeners run (Enter/Space activation included); otherwise — or after an unprevented pass-through — it reacheswindow, where the scene-level channel applies its ownership gates (defaultPrevented, auto-repeat,ownsKeyboard). A focused textbox-like role owns its keys exclusively; nothing leaks to the page.- Panel focus mirrors DOM tabbability:
focus(entity)/blur()/focusedEntity/isFocusable(entity)(projectedtabindex, natively-focusable tags, interactive roles). A pointerdown focuses the nearest focusable ancestor of the hit; clicking empty background blurs. Focus flips are delivered as syntheticfocus/blurevents so caret and highlight visuals repaint immediately inonDemandmode.
Version and backend gotchas (source-verified)
- three ≤ 0.1.3:
ThreeRenderer.flush()performed a full GL render, and the Scene flushes around every non-batched node — frame cost grew O(N²) in entity count. three 0.1.4 renders once per frame via thepresent()hook; upgrade before profiling anything else. - Native input inside a texture is limited. The adapter's canvas is
offscreen, so the Scene's projected a11y elements are never connected to the
document;
updateIntersectionfalls back to VectoJS's own event dispatch. Buttons/hover/wheel work; full native IME/text editing does not — keep text entry outside the 3D panel or accept simplified input. stroke()line width is effectively 1px on most platforms (LineBasicMaterial.linewidthis a known WebGL limitation). Draw thick lines as filled shapes instead.- Texture caches (three 0.1.7+): both
fillTextanddrawImagetextures are cached with a 256-entry LRU (text keyed by font|color|text). Before 0.1.7drawImageallocated a texture per call per frame, so reusing a source canvas mattered more; it is still good practice for very large images. - GPU context loss + runtime DPR (three 0.1.7+):
ThreeRendererrecovers on its own.webglcontextlostispreventDefault()-ed (required, or the browser never fires the restore event) and flipsisContextLost(), which makespresent()a no-op while lost;webglcontextrestoredre-applies pixel ratio and size (a restore can land on a different display) and forces a repaint. A(resolution: Ndppx)media query re-appliessetPixelRatiowhen DPR changes at runtime (monitor move, browser zoom) and re-arms itself. All of it is guarded for SSR/OffscreenCanvas and torn down indispose(). Do not add your own context-loss listener — you would fight the built-in recovery.