Visual Editor
Purpose and Scope
The Visual Editor is a browser-based 3D scene creation and manipulation tool that provides a complete graphical interface for building Three.js scenes. It allows users to create, import, edit, and export 3D content without writing code, featuring real-time rendering, visual property editing, and comprehensive undo/redo functionality.
For information about the examples system that demonstrates Three.js features, see Examples Browser. For rendering architecture details, see WebGL Rendering Pipeline.
Architecture Overview
The editor follows a hub-and-spoke architecture where the Editor class acts as the central state manager, coordinating UI components through a signal-based event system. UI components (Viewport, Sidebar, Menubar, Toolbar) interact with the Editor instance but do not directly communicate with each other.
Editor Core Class
The Editor class manages scene state, object hierarchy, and coordinates all editor subsystems through a signal-based publish-subscribe system.
Signal System
The Editor class initializes 40+ signals in its constructor editor/js/Editor.js19-96 These signals enable decoupled communication between subsystems. For example, when an object is modified, signals.objectChanged.dispatch(object) notifies all registered listeners without tight coupling.
State Management
| Property | Type | Purpose |
|---|---|---|
| scene | THREE.Scene | Main scene graph |
| sceneHelpers | THREE.Scene | Helper objects (grids, light helpers) |
| camera | THREE.PerspectiveCamera | Default editor camera |
| viewportCamera | THREE.Camera | Active viewport camera |
| selected | Object3D | Currently selected object |
| geometries | Object | Geometry UUID → geometry mapping |
| materials | Object | Material UUID → material mapping |
| textures | Object | Texture UUID → texture mapping |
| scripts | Object | Object UUID → script array mapping |
| helpers | Object | Object ID → helper mapping |
| cameras | Object | Camera UUID → camera mapping |
| materialsRefCounter | Map | Tracks material usage count |
The Editor maintains dictionaries of all scene resources indexed by UUID, enabling efficient lookups and reference tracking. The materialsRefCounter tracks how many objects use each material, preventing premature disposal editor/js/Editor.js123
JSON Serialization
The toJSON() method editor/js/Editor.js703-741 serializes the entire editor state including scene hierarchy, scripts, history, and project configuration. The fromJSON() method editor/js/Editor.js668-701 uses THREE.ObjectLoader to reconstruct the scene, preserving UUIDs and relationships.
UI Component Hierarchy
The editor interface consists of five main UI components arranged around a central viewport.
Viewport Component
The Viewport class manages the 3D rendering surface and object manipulation controls.
The Viewport component manages three rendering passes:
- Main scene rendering editor/js/Viewport.js891
- Grid helper rendering (if visible) editor/js/Viewport.js896
- Scene helpers rendering (lights, cameras, skeletons) editor/js/Viewport.js897
The TransformControls emit events when objects are manipulated editor/js/Viewport.js78-144 and the viewport creates corresponding command objects to enable undo/redo functionality.
Viewport Shading Modes
| Mode | Implementation | Purpose |
|---|---|---|
| solid | Default rendering | Standard material display |
| normals | scene.overrideMaterial = MeshNormalMaterial | Visualize surface normals |
| wireframe | scene.overrideMaterial = MeshBasicMaterial({wireframe: true}) | Show mesh topology |
| realistic | ViewportPathtracer + three-gpu-pathtracer | Physically accurate rendering |
Sources: editor/js/Viewport.js678-704
Sidebar Component
The Sidebar uses a tabbed panel structure with four main tabs.
The UIOutliner component editor/js/Sidebar.Scene.js132 displays the scene hierarchy as a collapsible tree, supporting drag-and-drop reordering. Each node shows type icons and associated resources (geometry, material, scripts) editor/js/Sidebar.Scene.js100-128
Menubar Component
The MenubarFile component provides export functionality using dynamic imports of exporter modules editor/js/Menubar.File.js250-445 keeping the initial bundle size small while supporting multiple export formats.
Command Pattern for Undo/Redo
The editor implements a command pattern through the History class and command objects, enabling full undo/redo functionality.
Command Classes
| Command Class | Purpose | Key Methods |
|---|---|---|
| AddObjectCommand | Add object to scene | execute(): adds object, undo(): removes object |
| RemoveObjectCommand | Remove object from scene | Inverse of AddObjectCommand |
| SetPositionCommand | Change object position | Stores old/new positions |
| SetRotationCommand | Change object rotation | Stores old/new rotations |
| SetScaleCommand | Change object scale | Stores old/new scales |
| SetGeometryCommand | Replace object geometry | Swaps geometry references |
| SetMaterialCommand | Replace object material | Swaps material references |
| SetValueCommand | Change arbitrary property | Generic property setter |
| MultiCmdsCommand | Execute multiple commands | Composite command pattern |
Commands can merge with previous commands via the update() method editor/js/Command.js27-31 preventing history spam from continuous value changes (e.g., dragging a slider).
History Persistence
When settings/history is enabled in the config editor/js/Config.js24 the history is serialized alongside the project and persisted across sessions editor/js/History.js80-150 This allows users to undo changes even after closing and reopening the editor.
File Loading System
The Loader class supports importing 30+ file formats through dynamic module loading.
Dynamic Import Strategy
The loader uses dynamic import() to load format-specific parsers on-demand editor/js/Loader.js89-959 reducing the initial bundle size:
// Example: GLTF loading
case 'glb':
reader.addEventListener('load', async function(event) {
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
const loader = await createGLTFLoader();
loader.parse(contents, '', function(result) {
const scene = result.scene;
scene.animations.push(...result.animations);
editor.execute(new AddObjectCommand(editor, scene));
});
});GLTF Loader Configuration
The createGLTFLoader() function editor/js/Loader.js954-976 configures a GLTFLoader with:
DRACOLoaderfor geometry decompression editor/js/Loader.js961-962KTX2Loaderfor texture transcoding editor/js/Loader.js964-965MeshoptDecoderfor meshopt compression editor/js/Loader.js972
The KTX2 loader is synchronized with the renderer's capabilities via signals.rendererDetectKTX2Support editor/js/Loader.js967
ZIP Archive Support
The loader can extract and process ZIP archives editor/js/Loader.js747-760 automatically detecting Poly assets (model.obj + materials.mtl) or individual files within the archive editor/js/Loader.js842-951
Storage and Persistence
The editor implements a two-tier storage system for configuration and project data.
Auto-save Implementation
The auto-save mechanism listens to 10+ signals editor/index.html163-173 and triggers a debounced save after 1000ms of inactivity editor/index.html145-157 The debounce prevents excessive writes during rapid changes:
let timeout;
function saveState() {
if (editor.config.getKey('autosave') === false) return;
clearTimeout(timeout);
timeout = setTimeout(function() {
editor.signals.savingStarted.dispatch();
timeout = setTimeout(function() {
editor.storage.set(editor.toJSON());
editor.signals.savingFinished.dispatch();
}, 100);
}, 1000);
}IndexedDB Schema
The Storage class editor/js/Storage.js1-92 uses IndexedDB with a single object store:
| Database | Object Store | Key | Value |
|---|---|---|---|
| threejs-editor | states | 0 (fixed) | Complete project JSON |
Service Worker and Offline Support
The editor registers a service worker for offline functionality and cross-origin isolation.
The service worker implements a network-first caching strategy editor/sw.js284-323 that:
- Attempts to fetch from network
- Injects COEP/COOP headers for the editor page editor/sw.js290-297
- Updates cache on successful fetch
- Falls back to cached version if network fails
This enables the Draco, Rhino3dm, and Basis Universal WASM decoders to use SharedArrayBuffer for better performance.
Internationalization
The Strings class provides multi-language support for UI text.
The strings dictionary uses hierarchical keys (e.g., menubar/file/open, sidebar/object/position) editor/js/Strings.js417-571 The language is auto-detected from navigator.language but can be changed in settings editor/js/Config.js5-7