import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Box, Button, Checkbox, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Slider, Stack, Typography, } from '@mui/material' import { getQhull } from '../qhull' import { points2D, type Dist2D } from '../points' const SIZE = 520 const PAD = 24 interface Result { pts: number[][]; tris: number[][]; hull: number[][] } export function Delaunay2D() { const [dist, setDist] = useState('uniform') const [n, setN] = useState(60) const [seed, setSeed] = useState(1) const [extra, setExtra] = useState([]) const [showTri, setShowTri] = useState(true) const [showHull, setShowHull] = useState(true) const [showCircles, setShowCircles] = useState(false) const pts = useMemo( () => [...points2D(n, dist, seed), ...extra], [n, dist, seed, extra], ) // The triangulation/hull are stored together with the exact points they were // computed from, so facet indices can never reference a stale (out-of-range) // point set while qhull is recomputing. const [result, setResult] = useState({ pts: [], tris: [], hull: [] }) useEffect(() => { let cancelled = false getQhull().then((q) => { if (cancelled) return if (pts.length < 3) { setResult({ pts, tris: [], hull: [] }); return } try { setResult({ pts, tris: q.delaunay(pts, 2).facets, hull: q.convexHull(pts, 2).facets }) } catch { setResult({ pts, tris: [], hull: [] }) } }) return () => { cancelled = true } }, [pts]) // Map data coords (roughly [0,1]) to screen. const toScreen = useCallback((p: number[]) => [ PAD + p[0] * (SIZE - 2 * PAD), SIZE - (PAD + p[1] * (SIZE - 2 * PAD)), ], []) const svgRef = useRef(null) const addPoint = (e: React.MouseEvent) => { const svg = svgRef.current if (!svg) return const r = svg.getBoundingClientRect() const sx = ((e.clientX - r.left) / r.width) * SIZE const sy = ((e.clientY - r.top) / r.height) * SIZE const x = (sx - PAD) / (SIZE - 2 * PAD) const y = (SIZE - sy - PAD) / (SIZE - 2 * PAD) setExtra((cur) => [...cur, [x, y]]) } const { pts: rpts, tris, hull } = result const circles = useMemo( () => (showCircles ? tris.map((t) => circumcircle(rpts[t[0]], rpts[t[1]], rpts[t[2]])) : []), [showCircles, tris, rpts], ) return ( Delaunay triangulation (blue) and convex hull (orange) computed by qhull-wasm. Click the canvas to add points. Distribution Points: {n} setN(v as number)} /> setShowTri(e.target.checked)} />} label="Triangulation" /> setShowHull(e.target.checked)} />} label="Hull" /> setShowCircles(e.target.checked)} />} label="Circumcircles" /> {showCircles && circles.map((c, i) => c && ( ))} {showTri && tris.map((t, i) => { const a = toScreen(rpts[t[0]]), b = toScreen(rpts[t[1]]), c = toScreen(rpts[t[2]]) return })} {showHull && hull.map((e, i) => { const a = toScreen(rpts[e[0]]), b = toScreen(rpts[e[1]]) return })} {rpts.map((p, i) => { const s = toScreen(p) return })} {rpts.length} points → {tris.length} triangles, {hull.length} hull edges. ) } function circumcircle(a: number[], b: number[], c: number[]) { const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1] const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) if (Math.abs(d) < 1e-12) return null const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d return { x: ux, y: uy, r: Math.hypot(ax - ux, ay - uy) } }