The engine is the central coordinator. It creates all Three.js infrastructure and delegates domain-specific work to SceneManager and GizmoController.
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
preserveDrawingBuffer: true,
});| Option | Value | Reason |
|---|---|---|
| antialias | true | Smooth edges on geometry |
| alpha | true | Transparent background so the canvas artboard shows through |
| preserveDrawingBuffer | true | Required for toDataURL() and screenshot capture |
| Setting | Value | Purpose |
|---|---|---|
| setPixelRatio | Math.min(window.devicePixelRatio, 2) | Caps at 2x. Higher ratios quadruple pixel count with diminishing visual return. |
| shadowMap.enabled | true | Enables shadow casting |
| shadowMap.type | THREE.PCFSoftShadowMap | Percentage-Closer Filtering with bilinear filtering, producing smooth penumbras |
| toneMapping | THREE.ACESFilmicToneMapping | ACES filmic curve maps HDR to LDR with natural highlight rolloff |
| toneMappingExposure | 1.0 | Neutral exposure |
| setClearColor | 0x000000, 0 | Fully transparent clear so the background shows through |
| Parameter | Value | Purpose |
|---|---|---|
| FOV | 50 | Moderate field of view. Avoids fisheye distortion while keeping natural perspective. |
| Near plane | 0.1 | Close enough that small objects aren't clipped |
| Far plane | 1000 | Far enough for large imported models |
| Position | (4, 3, 4) | A diagonal giving an isometric-like initial view |
OrbitControls are created with damping enabled and a damping factor of 0.1, targeting the origin. Damping creates smooth deceleration when the user releases the mouse; the change event marks the scene dirty.
Three lights are added to every scene:
| Light | Type | Intensity | Position | Shadow |
|---|---|---|---|---|
| Ambient | AmbientLight | 0.4 | N/A | No |
| Directional | DirectionalLight | 1.0 | (5, 10, 5) | Yes (2048x2048 shadow map) |
| Point | PointLight | 0.3 | (-3, 5, -3) | No |
All three are white. The directional light is the primary source; its shadow camera uses orthographic bounds of -10 to 10 on all axes, with a near plane of 0.1 and a far plane of 50.
A 20-unit grid with 40 divisions is created at 0.4 opacity but is hidden by default and never added to the scene. It exists as a reference that can be retrieved via getGridHelper() and toggled externally if needed.
| Method | Description |
|---|---|
| markDirty() | Forces a re-render on the next frame |
| onSelect(cb) | Registers a callback for object selection events |
| onTransformEnd(cb) | Registers a callback for when a gizmo drag ends |
| selectObject(id) | Selects an object by ID and attaches the gizmo, or deselects if null |
| addPrimitive(type, material?, geometry?) | Creates a new primitive mesh and adds it to the scene |
| importModelFromFile(file) | Loads a 3D model file, normalizes it and adds it to the scene |
| removeObject(id) | Removes an object and disposes its resources |
| setGizmoMode(mode) | Switches the gizmo between translate, rotate and scale |
| updateTransform(id, pos?, rot?, scale?) | Updates an object's position, rotation or scale |
| updateMaterial(id, config) | Replaces an object's material |
| updateGeometry(id, config) | Replaces an object's geometry (primitives only) |
| updateEnvironment(config) | Updates lighting and background color |
| getSelectedObject() | Returns the config of the selected object |
| getAllObjects() | Returns configs for all objects in the scene |
| getCameraPosition() / getCameraTarget() | Returns the current camera position or orbit target |
| setCameraPosition(pos, target) | Sets camera position and orbit target |
| resize(width, height) | Resizes the renderer and updates camera aspect |
| setOrbitEnabled(enabled) | Enables or disables orbit controls |
| pauseLoop() / resumeLoop() | Stops or restarts the render loop |
| dispose() | Full teardown |
dispose(): void {
this.disposed = true;
cancelAnimationFrame(this.animFrameId);
this.renderer.domElement.removeEventListener('pointerdown', this.handlePointerDown);
this.gizmo.dispose();
this.sceneManager.dispose();
this.orbit.dispose();
this.renderer.dispose();
if (this.renderer.domElement.parentElement) {
this.renderer.domElement.parentElement.removeChild(this.renderer.domElement);
}
}