/ concept-collection / qhull-wasm-demo
Sign in
concept-collection / qhull-wasm-demo
qhull-wasm-demo / src / components / Hull3D.tsx
182 lines · 6.8 KBCodeBlameHistory
283cae2qhull-wasm demo: 2D/3D triangulation, convex hull, and Delaunay benchmarksJeremy Magland 1import { useEffect, useMemo, useRef, useState } from 'react'
2import {
3 Box, Button, Checkbox, FormControl, FormControlLabel, InputLabel,
4 MenuItem, Select, Slider, Stack, Typography,
5} from '@mui/material'
6import * as THREE from 'three'
7import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
8import { getQhull } from '../qhull'
9import { points3D, type Dist3D } from '../points'
11export function Hull3D() {
12 const [dist, setDist] = useState<Dist3D>('gaussian')
13 const [n, setN] = useState(120)
14 const [seed, setSeed] = useState(1)
15 const [showPoints, setShowPoints] = useState(true)
16 const [wireframe, setWireframe] = useState(true)
17 const [spin, setSpin] = useState(true)
19 const pts = useMemo(() => points3D(n, dist, seed), [n, dist, seed])
435c5b9Fix stale-index crash on shrinking point count; add desktop reference timingsJeremy Magland 20 // Hull facets are kept together with the points they were computed from, so
21 // facet indices never reference a stale point set while qhull recomputes.
22 const [data, setData] = useState<{ pts: number[][]; hull: number[][] }>({ pts: [], hull: [] })
24 useEffect(() => {
25 let cancelled = false
26 getQhull().then((q) => {
27 if (cancelled) return
435c5b9Fix stale-index crash on shrinking point count; add desktop reference timingsJeremy Magland 28 try { setData({ pts, hull: q.convexHull(pts, 3).facets }) } catch { setData({ pts, hull: [] }) }
30 return () => { cancelled = true }
31 }, [pts])
33 const mountRef = useRef<HTMLDivElement>(null)
34 const sceneRef = useRef<{
35 renderer: THREE.WebGLRenderer
36 scene: THREE.Scene
37 camera: THREE.PerspectiveCamera
38 controls: OrbitControls
39 group: THREE.Group
40 } | null>(null)
41 const spinRef = useRef(spin)
42 spinRef.current = spin
44 // One-time scene setup.
45 useEffect(() => {
46 const mount = mountRef.current!
47 const w = mount.clientWidth, h = 480
48 const renderer = new THREE.WebGLRenderer({ antialias: true })
49 renderer.setPixelRatio(window.devicePixelRatio)
50 renderer.setSize(w, h)
51 mount.appendChild(renderer.domElement)
53 const scene = new THREE.Scene()
54 scene.background = new THREE.Color('#0f1722')
55 const camera = new THREE.PerspectiveCamera(45, w / h, 0.01, 100)
56 camera.position.set(1.6, 1.2, 1.8)
58 const controls = new OrbitControls(camera, renderer.domElement)
59 controls.enableDamping = true
61 scene.add(new THREE.AmbientLight(0xffffff, 0.6))
62 const dir = new THREE.DirectionalLight(0xffffff, 0.8)
63 dir.position.set(2, 3, 4)
64 scene.add(dir)
66 const group = new THREE.Group()
67 scene.add(group)
69 sceneRef.current = { renderer, scene, camera, controls, group }
71 let raf = 0
72 const animate = () => {
73 raf = requestAnimationFrame(animate)
74 if (spinRef.current) group.rotation.y += 0.004
75 controls.update()
76 renderer.render(scene, camera)
77 }
78 animate()
80 const onResize = () => {
81 const nw = mount.clientWidth
82 camera.aspect = nw / h
83 camera.updateProjectionMatrix()
84 renderer.setSize(nw, h)
85 }
86 window.addEventListener('resize', onResize)
88 return () => {
89 cancelAnimationFrame(raf)
90 window.removeEventListener('resize', onResize)
91 controls.dispose()
92 renderer.dispose()
93 mount.removeChild(renderer.domElement)
94 sceneRef.current = null
95 }
96 }, [])
98 // Rebuild geometry when points / hull / toggles change.
99 useEffect(() => {
100 const s = sceneRef.current
101 if (!s) return
102 const { group } = s
103 group.traverse((o) => {
104 const any = o as Partial<THREE.Mesh>
105 any.geometry?.dispose()
106 const mat = any.material
107 if (Array.isArray(mat)) mat.forEach((m) => m.dispose())
108 else mat?.dispose()
109 })
110 group.clear()
283cae2qhull-wasm demo: 2D/3D triangulation, convex hull, and Delaunay benchmarksJeremy Magland 113 // center & scale points to fit a unit-ish box
114 const flat = pts.flat()
115 const c = [0, 1, 2].map((k) => mean(pts.map((p) => p[k])))
116 const span = Math.max(1e-6, ...flat.map((v, i) => Math.abs(v - c[i % 3]))) * 2
117 const scale = 1.4 / span
118 const xf = (p: number[]) => new THREE.Vector3(
119 (p[0] - c[0]) * scale, (p[1] - c[1]) * scale, (p[2] - c[2]) * scale,
120 )
122 if (hull.length) {
123 const geo = new THREE.BufferGeometry()
124 const verts: number[] = []
125 for (const f of hull) for (const idx of f) {
126 const v = xf(pts[idx]); verts.push(v.x, v.y, v.z)
127 }
128 geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3))
129 geo.computeVertexNormals()
130 const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
131 color: 0x42a5f5, transparent: true, opacity: 0.55,
132 side: THREE.DoubleSide, flatShading: true,
133 }))
134 group.add(mesh)
135 if (wireframe) {
136 group.add(new THREE.LineSegments(
137 new THREE.WireframeGeometry(geo),
138 new THREE.LineBasicMaterial({ color: 0x90caf9, transparent: true, opacity: 0.5 }),
139 ))
140 }
141 }
143 if (showPoints) {
144 const pg = new THREE.BufferGeometry()
145 pg.setAttribute('position', new THREE.Float32BufferAttribute(
146 pts.flatMap((p) => { const v = xf(p); return [v.x, v.y, v.z] }), 3))
147 group.add(new THREE.Points(pg, new THREE.PointsMaterial({ color: 0xffd54f, size: 0.04 })))
148 }
435c5b9Fix stale-index crash on shrinking point count; add desktop reference timingsJeremy Magland 149 }, [data, showPoints, wireframe])
151 return (
152 <Stack spacing={2}>
153 <Typography variant="body2" color="text.secondary">
154 Convex hull of a 3D point cloud, triangulated by qhull-wasm and rendered with three.js. Drag to rotate.
155 </Typography>
156 <Stack direction="row" spacing={2} flexWrap="wrap" alignItems="center" useFlexGap>
157 <FormControl size="small" sx={{ minWidth: 130 }}>
158 <InputLabel>Distribution</InputLabel>
159 <Select label="Distribution" value={dist} onChange={(e) => setDist(e.target.value as Dist3D)}>
160 <MenuItem value="gaussian">Gaussian blob</MenuItem>
161 <MenuItem value="uniform">Uniform cube</MenuItem>
162 <MenuItem value="sphere">Sphere surface</MenuItem>
163 </Select>
164 </FormControl>
165 <Box sx={{ width: 180 }}>
166 <Typography variant="caption">Points: {n}</Typography>
167 <Slider size="small" min={8} max={2000} value={n} onChange={(_, v) => setN(v as number)} />
168 </Box>
169 <Button size="small" variant="outlined" onClick={() => setSeed((x) => x + 1)}>Regenerate</Button>
170 <FormControlLabel control={<Checkbox size="small" checked={showPoints} onChange={(e) => setShowPoints(e.target.checked)} />} label="Points" />
171 <FormControlLabel control={<Checkbox size="small" checked={wireframe} onChange={(e) => setWireframe(e.target.checked)} />} label="Wireframe" />
172 <FormControlLabel control={<Checkbox size="small" checked={spin} onChange={(e) => setSpin(e.target.checked)} />} label="Spin" />
173 </Stack>
174 <Box ref={mountRef} sx={{ width: '100%', borderRadius: 1, overflow: 'hidden', lineHeight: 0 }} />
175 <Typography variant="body2" color="text.secondary">
435c5b9Fix stale-index crash on shrinking point count; add desktop reference timingsJeremy Magland 176 {pts.length} points → hull with {data.hull.length} triangular facets.
178 </Stack>
179 )
182function mean(a: number[]) { return a.reduce((s, x) => s + x, 0) / (a.length || 1) }
moveopenescclose