a65b3e2Interactive surfacefun mesh refinement (numbl project)Jeremy Magland 1import { useEffect, useMemo, useState, type CSSProperties } from "react";
2import { SurfView } from "./render/SurfView.js";
3import type { SurfTrace } from "./render/types.js";
4import { onData, onHostEvent, sendToMATLAB } from "./bridge.js";
6/** Mesh payload from the numbl script: one flat (column-major) x/y/z array per
7 * patch, each an `n x n` grid. Mirrors what refine_demo.m sends. */
8interface MeshData {
9 n: number;
10 x: number[][];
11 y: number[][];
12 z: number[][];
13 npatches: number;
14 level?: number;
15 maxLevel?: number;
16}
18function isMeshData(d: unknown): d is MeshData {
19 return (
20 !!d &&
21 typeof d === "object" &&
22 Array.isArray((d as MeshData).x) &&
23 typeof (d as MeshData).n === "number"
24 );
25}
27export function App() {
28 const [mesh, setMesh] = useState<MeshData | null>(null);
29 const [level, setLevel] = useState(0);
30 const [busy, setBusy] = useState(false);
32 // Initial mesh arrives via Data; refinements arrive via "mesh" events
33 // (which update React state without remounting the iframe, so the camera /
34 // orientation is preserved across refinements).
35 useEffect(() => {
36 const apply = (d: unknown) => {
37 if (!isMeshData(d)) return;
38 setMesh(d);
39 if (typeof d.level === "number") setLevel(d.level);
40 setBusy(false);
41 };
42 const offData = onData(apply);
43 const offMesh = onHostEvent("mesh", apply);
44 return () => {
45 offData();
46 offMesh();
47 };
48 }, []);
50 const traces = useMemo<SurfTrace[]>(() => {
51 if (!mesh) return [];
52 const out: SurfTrace[] = [];
53 for (let k = 0; k < mesh.x.length; k++) {
54 out.push({
55 x: mesh.x[k],
56 y: mesh.y[k],
57 z: mesh.z[k],
58 rows: mesh.n,
59 cols: mesh.n,
60 });
61 }
62 return out;
63 }, [mesh]);
65 const maxLevel = mesh?.maxLevel ?? 3;
67 const onSlider = (v: number) => {
68 setLevel(v);
69 setBusy(true);
70 sendToMATLAB("refine", v);
71 };
73 return (
74 <div style={rootStyle}>
75 {mesh ? (
76 <SurfView surfTraces={traces} shading="faceted" />
77 ) : (
78 <div style={waitingStyle}>Waiting for mesh from the script…</div>
79 )}
81 <div style={panelStyle}>
82 <div style={{ fontWeight: 600, marginBottom: 8 }}>surfacefun mesh</div>
83 <label style={{ display: "block", fontSize: 13 }}>
84 Refinement level: <b>{level}</b>
85 <input
86 type="range"
87 min={0}
88 max={maxLevel}
89 step={1}
90 value={level}
91 onChange={e => onSlider(Number(e.target.value))}
92 style={{ width: "100%", marginTop: 4 }}
93 />
94 </label>
95 <div style={{ fontSize: 12, color: "#475569", marginTop: 4 }}>
96 patches: {mesh?.npatches ?? "—"}
97 {busy ? " · refining…" : ""}
98 </div>
99 <div style={{ fontSize: 11, color: "#94a3b8", marginTop: 6 }}>
100 drag to rotate · scroll to zoom
101 </div>
102 </div>
103 </div>
104 );
105}
107const rootStyle: CSSProperties = {
108 position: "absolute",
109 inset: 0,
110 background: "#ffffff",
111 fontFamily: "system-ui, -apple-system, Arial, sans-serif",
112};
114const waitingStyle: CSSProperties = {
115 position: "absolute",
116 inset: 0,
117 display: "flex",
118 alignItems: "center",
119 justifyContent: "center",
120 color: "#94a3b8",
121};
123const panelStyle: CSSProperties = {
124 position: "absolute",
125 top: 12,
126 left: 12,
127 width: 220,
128 padding: "12px 14px",
129 background: "rgba(255,255,255,0.92)",
130 border: "1px solid #e2e8f0",
131 borderRadius: 8,
132 boxShadow: "0 1px 4px rgba(0,0,0,0.1)",
133 color: "#0f172a",
134};