import { useEffect, useRef, useState } from 'react' import { SurfaceView } from './render/SurfaceView' import type { ViewMode } from './render/SurfaceView' import type { NurbsPatch, SurfaceModel } from './model/types' import { nurbsCoverage, triangleCount, vertexCount } from './model/types' import { mergeTriMeshes } from './model/tessellate' import { loadOpenCascade } from './occ/loader' import type { OpenCascade, Shape } from './occ/types' import { buildModel, retessellate } from './occ/extract' import { importCadFile } from './occ/importCad' import { shapeToStep } from './occ/exportCad' import { primitives } from './sources' import { ABC_DATASET_URL, fetchRandomAbcStep } from './abcDataset' import { toOBJ, toPLY, toSTL } from './export/meshWriters' import { toNurbsJson } from './export/nurbsJson' import './index.css' type EngineState = 'idle' | 'loading' | 'ready' | 'error' const VIEW_MODES: { id: ViewMode; label: string }[] = [ { id: 'shaded', label: 'Shaded' }, { id: 'wire', label: 'Wireframe' }, { id: 'net', label: 'Control net' }, { id: 'iso', label: 'Isocurves' }, ] const EXPORT_FORMATS = [ { id: 'obj', label: 'OBJ (triangles)', ext: '.obj' }, { id: 'ply', label: 'PLY (triangles)', ext: '.ply' }, { id: 'stl', label: 'STL (triangles)', ext: '.stl' }, { id: 'nurbs', label: 'NURBS patches (JSON)', ext: '.nurbs.json' }, { id: 'step', label: 'STEP (B-rep)', ext: '.step' }, ] as const type ExportId = (typeof EXPORT_FORMATS)[number]['id'] function download(bytes: Uint8Array, filename: string) { // copy into a fresh ArrayBuffer-backed view so it is a valid BlobPart const blob = new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } function App() { const [engine, setEngine] = useState<{ state: EngineState; message: string }>({ state: 'idle', message: 'CAD engine loads on first use (~30 MB).', }) const [model, setModel] = useState(null) const [baseName, setBaseName] = useState('model') const [quality, setQuality] = useState(0.5) const [mode, setMode] = useState('shaded') const [anaglyph, setAnaglyph] = useState(false) const [selectedFaceId, setSelectedFaceId] = useState(null) const [exportId, setExportId] = useState('obj') const [busy, setBusy] = useState(null) const [error, setError] = useState(null) const ocRef = useRef(null) const meshShapeRef = useRef(null) const fileInputRef = useRef(null) async function ensureOc(): Promise { if (ocRef.current) return ocRef.current const oc = await loadOpenCascade((message) => setEngine({ state: 'loading', message })) ocRef.current = oc setEngine({ state: 'ready', message: 'CAD engine ready' }) return oc } async function load( build: (oc: OpenCascade) => Shape, source: SurfaceModel['source'], name: string, raw?: SurfaceModel['raw'], ) { setError(null) setBusy('building') setSelectedFaceId(null) try { const oc = await ensureOc() const shape = build(oc) const { model: built, meshShape } = buildModel(oc, shape, quality, source, raw) meshShapeRef.current = meshShape setModel(built) setBaseName(name) } catch (e) { setError(e instanceof Error ? e.message : String(e)) setEngine((s) => (s.state === 'loading' ? { state: 'error', message: 'CAD engine failed to load.' } : s)) } finally { setBusy(null) } } const loadPrimitive = (id: string) => { const src = primitives.find((p) => p.id === id) if (!src) return void load(src.build, { kind: 'primitive', label: src.label }, src.id) } const loadRandomAbc = async () => { setError(null) setBusy('downloading') try { const { name, bytes } = await fetchRandomAbcStep() await load( (oc) => importCadFile(oc, name, bytes).shape, { kind: 'step', label: `${name} (ABC dataset)` }, name.replace(/\.[^.]+$/, ''), { format: 'step', bytes }, ) } catch (e) { setError(e instanceof Error ? e.message : String(e)) setBusy(null) } } const openFile = async (file: File) => { const bytes = new Uint8Array(await file.arrayBuffer()) const lower = file.name.toLowerCase() const format: 'step' | 'iges' = lower.endsWith('.iges') || lower.endsWith('.igs') ? 'iges' : 'step' void load( (oc) => importCadFile(oc, file.name, bytes).shape, { kind: format, label: file.name }, file.name.replace(/\.[^.]+$/, ''), { format, bytes }, ) } // Re-tessellate (debounced) when the resolution slider settles. useEffect(() => { const oc = ocRef.current const meshShape = meshShapeRef.current if (!oc || !meshShape || !model) return const t = setTimeout(() => { setBusy('meshing') try { const patches = retessellate(oc, meshShape, quality, model.patches) setModel((m) => (m ? { ...m, patches } : m)) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setBusy(null) } }, 150) return () => clearTimeout(t) // eslint-disable-next-line react-hooks/exhaustive-deps }, [quality]) const doExport = () => { if (!model) return setError(null) try { const fmt = EXPORT_FORMATS.find((f) => f.id === exportId)! let bytes: Uint8Array if (exportId === 'nurbs') { bytes = toNurbsJson(model) } else if (exportId === 'step') { if (model.raw) { bytes = model.raw.bytes } else if (ocRef.current && meshShapeRef.current) { bytes = shapeToStep(ocRef.current, meshShapeRef.current) } else { throw new Error('STEP export unavailable for this model.') } } else { const merged = mergeTriMeshes(model) bytes = exportId === 'obj' ? toOBJ(merged) : exportId === 'ply' ? toPLY(merged) : toSTL(merged) } const base = baseName.replace(/[^\w-]+/g, '_').toLowerCase() || 'model' download(bytes, base + fmt.ext) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } } const selectedPatch = selectedFaceId != null ? (model?.patches.find((p) => p.id === selectedFaceId) as NurbsPatch | undefined) : undefined const coverage = model ? nurbsCoverage(model) : null return (

Mesh Studio

Generate surface meshes with different tools and inspect them in 3D. First tool:{' '} OpenCASCADE.js — CAD B-rep faces are true NURBS surfaces (polynomials on faces), extracted here alongside the triangulation.

{engine.message}

Sources

{primitives.map((p) => ( ))}

Random models are drawn from{' '} abc-step-1000, a rehosted slice of the ABC dataset of CAD models (Koch et al., CVPR 2019).

{ const file = e.target.files?.[0] if (file) void openFile(file) e.target.value = '' }} /> {busy && (
{busy === 'building' ? 'Building model…' : busy === 'downloading' ? 'Downloading model…' : 'Re-meshing…'}
)} {error &&
{error}
}
{model && (

Model

{model.source.label}
{model.patches.length} faces · {triangleCount(model).toLocaleString()} triangles ·{' '} {vertexCount(model).toLocaleString()} vertices
{coverage && (
NURBS extracted on {coverage.withNurbs}/{coverage.total} faces
)}
)} {model && (

View

Coarse ↔ fine re-tessellates the same NURBS faces — drag to see the polynomial surface go from faceted to smooth. Click a face to inspect it.

)} {selectedPatch && (

Face #{selectedPatch.id}

{selectedPatch.nurbs ? (
Degree (u, v): {selectedPatch.nurbs.uDegree}, {selectedPatch.nurbs.vDegree}
Control net: {selectedPatch.nurbs.nu} × {selectedPatch.nurbs.nv} poles
{selectedPatch.nurbs.weights ? 'Rational (NURBS)' : 'Polynomial (non-rational)'}
Knots: {selectedPatch.nurbs.uKnots.length} u, {selectedPatch.nurbs.vKnots.length} v
{selectedPatch.tri.indices.length / 3} triangles at this resolution
) : (
No NURBS data extracted for this face.
)}
)} {model && (

Export

Triangle formats export the current tessellation. NURBS JSON stores the exact polynomial patches. STEP hands back the B-rep (original bytes for imported files).

)}
{model && (
{VIEW_MODES.map((m) => ( ))}
)}
) } export default App