Rendering with three.js
Shapemetry is renderer-agnostic: you tessellate, then feed flat channels to your engine. This guide uses three.js (^0.183, what the examples app runs). The minimal version is in Quickstart: solid; this page is the full picture.
Channels → BufferGeometry
Brep.tessellate returns plain number[] channels; three copies them into typed arrays. No manual conversion, no interleaving:
import * as THREE from 'three'
import { Brep } from '@huukhanhnguyen/geometry'
function toThreeMesh(document: string, deflection = 0.5): THREE.Mesh {
const tess = Brep.tessellate(document, deflection)
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute(tess.positions, 3))
if (tess.normals.length > 0) {
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(tess.normals, 3))
} else {
geometry.computeVertexNormals()
}
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(tess.uvs, 2))
geometry.setIndex(tess.indices) // plain number[] — three picks Uint16/Uint32
const material = new THREE.MeshStandardMaterial({
color: 0x8ab4f8,
roughness: 0.7,
metalness: 0.0,
side: THREE.DoubleSide,
})
return new THREE.Mesh(geometry, material)
}Edge overlays
The edges channel carries display polylines (crease/boundary) — draw them as line segments over the mesh for the CAD look:
for (const edge of tess.edges ?? []) {
if (edge.type === 'unset') continue
const geo = new THREE.BufferGeometry()
geo.setAttribute('position', new THREE.Float32BufferAttribute(edge.positions, 3))
scene.add(new THREE.Line(geo, new THREE.LineBasicMaterial({ color: 0x1f2937 })))
}groups ({ start, count, material? }) are authored material spans in indices — feed them to geometry.addGroup(start, count, i) when a producer wrote per-material ranges. Tessellation does not map triangles back to B-Rep faces.
Z-up camera
Shapemetry is Z-up; three defaults to Y-up. One line fixes the camera:
camera.up.set(0, 0, 1)
camera.position.set(180, -220, 160)
camera.lookAt(50, 50, 50)Updates: rebuild, don't mutate
Kernel results are immutable documents, so the viewer's update strategy is a full rebuild per change: re-tessellate, replace the mesh, dispose the old GPU resources. Never patch attributes in place.
function replaceMesh(scene: THREE.Scene, oldMesh: THREE.Mesh | null, document: string) {
if (oldMesh) {
scene.remove(oldMesh)
oldMesh.geometry.dispose()
;(oldMesh.material as THREE.Material).dispose()
}
const mesh = toThreeMesh(document)
scene.add(mesh)
return mesh
}For slider-driven parametric edits this is fast enough at interactive rates — tessellation at display deflection is sub-millisecond for typical parts. Debounce the rebuild; don't rebuild per mousemove.
From parametric models
Evaluated entities (evaluateModel(...).geometry[].items) are already baked render meshes — meshEntity items carry { positions, faces }. Triangulate the n-gon faces (fan per face is fine for planar n-gons; for general n-gons use Mesh.tessellate(entity.geometry)), then the same BufferGeometry path. Face entities that stayed B-Rep go through Brep.tessellate on their Shell document first. Kind-sniff with entityTypeOf from @huukhanhnguyen/model.
Bundler setup
The wasm kernel needs Vite plugins — see Installation. The working reference config is apps/examples/vite.config.ts in the repo.
Where next
- Tessellate for display — deflection and channel semantics.
- glTF, USD, drawing export — when the target is a file, not a canvas.