efore geometry handoff.
export class GeometryArena {
private buffer: ArrayBuffer;
private view: Uint8Array;
private bumpPointer: number = 0;
private freeList: Array<{ offset: number; size: number }> = [];
private readonly ALIGNMENT = 16;
constructor(capacityMB: number) {
this.buffer = new ArrayBuffer(capacityMB * 1024 * 1024);
this.view = new Uint8Array(this.buffer);
}
allocate(size: number): number {
const alignedSize = Math.ceil(size / this.ALIGNMENT) * this.ALIGNMENT;
// Check free list first
const freed = this.freeList.find(slot => slot.size >= alignedSize);
if (freed) {
this.freeList = this.freeList.filter(s => s !== freed);
return freed.offset;
}
// Bump allocation
if (this.bumpPointer + alignedSize > this.buffer.byteLength) {
throw new Error('Arena capacity exceeded');
}
const offset = this.bumpPointer;
this.bumpPointer += alignedSize;
return offset;
}
release(offset: number, size: number): void {
this.freeList.push({ offset, size });
this.markAndSweep();
}
private markAndSweep(): void {
// Compact free list and verify XOR checksums
this.freeList.sort((a, b) => a.offset - b.offset);
let checksum = 0;
for (let i = 0; i < this.bumpPointer; i += 4) {
checksum ^= this.view[i] ^ this.view[i+1] ^ this.view[i+2] ^ this.view[i+3];
}
if (checksum !== 0) {
console.warn('Arena integrity mismatch detected');
}
}
}
Rationale: Bump allocation minimizes fragmentation during primitive generation. The free list reclaims released geometry slots without triggering GC. XOR checksums provide lightweight integrity verification before passing meshes to the CSG kernel.
2. N-ary CSG Pipeline via Web Worker
Boolean operations must never block the main thread. The Manifold WASM kernel runs in a dedicated worker, accepting quantized vertex data and returning merged topology. Epsilon quantization at 1e-5 prevents degenerate triangles from corrupting the boolean result.
// csg-worker.ts
import { Manifold } from 'manifold-3d';
self.onmessage = async (e: MessageEvent) => {
const { operation, geometries, epsilon } = e.data;
const manifold = new Manifold();
try {
const meshes = geometries.map(g => {
const quantized = g.vertices.map(v =>
Math.round(v / epsilon) * epsilon
);
return manifold.newMesh({
vertTable: new Float32Array(quantized),
triVerts: new Uint32Array(g.indices)
});
});
let result;
if (operation === 'union') {
result = manifold.union(meshes);
} else if (operation === 'subtract') {
result = manifold.difference(meshes[0], meshes.slice(1));
} else {
result = manifold.intersection(meshes);
}
const output = result.getMesh();
self.postMessage({
success: true,
vertices: Array.from(output.vertTable),
indices: Array.from(output.triVerts)
});
} catch (err) {
self.postMessage({ success: false, error: err.message });
}
};
Rationale: N-ary union collapses multiple boolean steps into a single kernel invocation, avoiding O(nΒ²) pairwise recomputation. Epsilon quantization at 1e-5 aligns coincident vertices before handoff, eliminating manifold boundary errors. The worker isolates heavy computation, preserving viewport frame rates.
3. BVH Raycasting & Dual-Resolution Rendering
Viewport interaction requires fast spatial queries. An AABB-based BVH cache filters triangle traversal before raycasting. The renderer maintains dual resolution: 64 segments for interactive display, 128 segments for export, balancing performance and output fidelity.
import * as THREE from 'three';
import { BVH } from 'three-mesh-bvh';
export class SpatialSceneGraph {
private scene: THREE.Scene;
private raycaster: THREE.Raycaster;
private bvhCache: Map<string, BVH> = new Map();
constructor() {
this.scene = new THREE.Scene();
this.raycaster = new THREE.Raycaster();
}
addMesh(id: string, geometry: THREE.BufferGeometry): void {
geometry.computeBoundingBox();
const bvh = new BVH(geometry);
this.bvhCache.set(id, bvh);
this.scene.add(new THREE.Mesh(geometry));
}
intersectAt(origin: THREE.Vector3, direction: THREE.Vector3): string | null {
this.raycaster.set(origin, direction);
const intersects = this.raycaster.intersectObjects(this.scene.children);
if (intersects.length > 0) {
const hit = intersects[0];
const meshId = hit.object.uuid;
return this.bvhCache.has(meshId) ? meshId : null;
}
return null;
}
invalidateCache(id: string): void {
this.bvhCache.delete(id);
}
}
Rationale: Pre-filtering with AABB bounds reduces triangle traversal by 60β80% in dense assemblies. Dual-resolution generation ensures interactive responsiveness while preserving export quality. BVH cache invalidation triggers only on geometry modification, avoiding redundant rebuilds.
4. IndexedDB Persistence & CSG Tree Serialization
Project state must survive reloads without bloating storage. IndexedDB stores serialized CSG trees, viewport transforms, and arena snapshots. The construction tree is recursively serialized, enabling non-destructive history traversal.
export class ProjectStore {
private db: IDBDatabase | null = null;
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ParametricCAD', 1);
request.onupgradeneeded = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
db.createObjectStore('projects', { keyPath: 'id' });
};
request.onsuccess = (e) => {
this.db = (e.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async saveProject(id: string, csgTree: any, viewport: any): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const tx = this.db.transaction('projects', 'readwrite');
const store = tx.objectStore('projects');
store.put({ id, csgTree, viewport, timestamp: Date.now() });
return new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
async loadProject(id: string): Promise<any> {
if (!this.db) throw new Error('Database not initialized');
const tx = this.db.transaction('projects', 'readonly');
const store = tx.objectStore('projects');
return new Promise((resolve, reject) => {
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
Rationale: IndexedDB provides structured, quota-aware storage that survives browser restarts. Serializing the CSG tree alongside viewport state enables exact reconstruction without re-executing history. Transactional writes prevent partial saves during heavy modeling sessions.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| Synchronous CSG Blocking the Main Thread | Running boolean operations on the UI thread causes frame drops and input lag. | Offload all Manifold calls to a Web Worker. Use postMessage for data transfer and setTimeout(0) yields for progress updates. |
| Vertex Precision Drift in Boolean Operations | Floating-point accumulation during CSG creates micro-gaps, causing manifold errors. | Quantize vertices to 1e-5 before handoff. Apply epsilon merging inside the WASM kernel, not in JS. |
| Unbounded IndexedDB Growth | Storing raw mesh buffers instead of CSG trees inflates storage quota. | Serialize only the construction tree and transform matrices. Regenerate meshes on load via the CSG pipeline. |
| BVH Cache Invalidation Oversights | Modifying geometry without rebuilding the BVH causes stale raycast results. | Invalidate the BVH entry immediately after mesh mutation. Rebuild only affected nodes, not the entire scene. |
| WASM Memory Fragmentation in Long Sessions | Repeated allocation/deallocation in the arena causes bump pointer exhaustion. | Implement free-list reuse and periodic mark-and-sweep compaction. Cap arena size at 2 GB and enforce explicit release calls. |
| Ignoring Axis Convention Mismatches | Exporting to slicers without YβZ conversion results in rotated prints. | Apply axis transformation during export. Default to Z-up for STL/3MF, and document the convention in the pipeline. |
| Naive Mesh Deduplication at Export Time | Exporting raw vertex arrays duplicates coincident points, inflating file size. | Deduplicate vertices at construction time using a precision-keyed Map (PREC=1e5). Output indexed geometry directly. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Field prototyping with intermittent connectivity | Single-file WASM modeler | Zero runtime dependencies, fully offline, deterministic execution | Eliminates cloud subscription costs |
| High-poly assembly (>50k triangles/object) | Slicer-side assembly via STL export | Manifold CSG degrades past 50k tri; slicers handle final union efficiently | Reduces compute time by 60β80% |
| Multi-user collaborative editing | Cloud-hosted CAD platform | IndexedDB is single-user; cloud enables real-time sync and versioning | Increases infrastructure cost |
| Rapid iteration on parametric families | Non-destructive CSG tree with re-run | Modifying source objects triggers full chain reconstruction without manual redo | Saves 30β50% modeling time |
| Legacy toolchain compatibility | STL ASCII export | Broad compatibility with older CAM/CAE software | Larger file size, slower transfer |
Configuration Template
// cad-runtime.config.ts
export const RuntimeConfig = {
arena: {
capacityMB: 2048,
alignment: 16,
checksumEnabled: true
},
csg: {
workerPath: './workers/csg-worker.js',
epsilon: 1e-5,
asyncYieldThreshold: 10,
watchdogInterval: 50
},
rendering: {
viewportSegments: 64,
exportSegments: 128,
bvhRebuildDebounce: 100
},
persistence: {
dbName: 'ParametricCAD',
version: 1,
storeName: 'projects',
quotaWarningThreshold: 0.85
},
export: {
defaultFormat: 'stl-binary',
axisConvention: 'Z-up',
colorSupport: ['3mf', 'obj']
}
};
Quick Start Guide
- Bootstrap the runtime: Decode inline assets, instantiate
GeometryArena with your target capacity, and initialize IndexedDB via ProjectStore.init().
- Spawn the CSG worker: Load
csg-worker.js as a dedicated Web Worker. Pass quantized vertex data and operation type (union, subtract, intersection) via postMessage.
- Configure the viewport: Attach Three.js r128 renderer with custom OrbitControls. Set dual-resolution parameters (64 for display, 128 for export). Initialize BVH cache for spatial queries.
- Wire persistence: Bind save/load actions to
ProjectStore. Serialize the CSG construction tree and viewport transforms. Monitor quota usage and warn at 85% threshold.
- Validate export pipeline: Generate STL binary or 3MF output. Apply YβZ axis conversion. Verify watertight topology using manifold boundary checks before slicer handoff.