Skip to content
Shapemetry

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

Last updated: 📖 2 min readEdit on GitHub