import { useEffect, useMemo, useState, type CSSProperties } from "react"; import { SurfView } from "./render/SurfView.js"; import type { SurfTrace, Quiver3Trace } from "./render/types.js"; import { onData, onHostEvent, sendToMATLAB } from "./bridge.js"; /** Mesh payload from the numbl script: one flat (column-major) x/y/z array per * patch, each an `n x n` grid. Mirrors what refine_demo.m sends. */ interface MeshData { n: number; x: number[][]; y: number[][]; z: number[][]; npatches: number; level?: number; maxLevel?: number; } /** Vector field payload: surface patches + sampled tangent vectors at patch centers. */ interface VectorFieldData extends MeshData { type: "vectorfield"; vectors: { x: number[]; y: number[]; z: number[]; u: number[]; v: number[]; w: number[]; }; } function isMeshData(d: unknown): d is MeshData { return ( !!d && typeof d === "object" && Array.isArray((d as MeshData).x) && typeof (d as MeshData).n === "number" ); } function isVectorFieldData(d: unknown): d is VectorFieldData { return ( isMeshData(d) && (d as VectorFieldData).type === "vectorfield" && !!(d as VectorFieldData).vectors ); } export function App() { const [mesh, setMesh] = useState(null); const [level, setLevel] = useState(0); const [busy, setBusy] = useState(false); const [arrowScale, setArrowScale] = useState(0.15); useEffect(() => { const apply = (d: unknown) => { if (!isMeshData(d)) return; setMesh(d); if (typeof d.level === "number") setLevel(d.level); setBusy(false); }; const offData = onData(apply); const offMesh = onHostEvent("mesh", apply); return () => { offData(); offMesh(); }; }, []); const surfTraces = useMemo(() => { if (!mesh) return []; return mesh.x.map((_, k) => ({ x: mesh.x[k], y: mesh.y[k], z: mesh.z[k], rows: mesh.n, cols: mesh.n, })); }, [mesh]); const quiverTraces = useMemo(() => { if (!isVectorFieldData(mesh)) return []; const { vectors } = mesh; return [ { x: vectors.x, y: vectors.y, z: vectors.z, u: vectors.u.map(v => v * arrowScale), v: vectors.v.map(v => v * arrowScale), w: vectors.w.map(v => v * arrowScale), showArrowHead: true, color: [0.85, 0.325, 0.098] as [number, number, number], lineWidth: 1.5, }, ]; }, [mesh, arrowScale]); const isVectorField = isVectorFieldData(mesh); const maxLevel = mesh?.maxLevel ?? 3; const onSlider = (v: number) => { setLevel(v); setBusy(true); sendToMATLAB("refine", v); }; return (
{mesh ? ( ) : (
Waiting for mesh from the script…
)}
{isVectorField ? ( <>
Vector field on surface
patches: {mesh?.npatches ?? "—"}
) : ( <>
surfacefun mesh
patches: {mesh?.npatches ?? "—"} {busy ? " · refining…" : ""}
)}
drag to rotate · scroll to zoom
); } const rootStyle: CSSProperties = { position: "absolute", inset: 0, background: "#ffffff", fontFamily: "system-ui, -apple-system, Arial, sans-serif", }; const waitingStyle: CSSProperties = { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", color: "#94a3b8", }; const panelStyle: CSSProperties = { position: "absolute", top: 12, left: 12, width: 220, padding: "12px 14px", background: "rgba(255,255,255,0.92)", border: "1px solid #e2e8f0", borderRadius: 8, boxShadow: "0 1px 4px rgba(0,0,0,0.1)", color: "#0f172a", };