concept-collection / mesh-pde-solver
mesh-pde-solver: upload a quad mesh, solve PDEs on the surface in-browser
Upload a quadrilateral surface mesh (any meshio-readable format), convert it to Gmsh MSH 2.2 with meshio on Pyodide, and solve Laplace-Beltrami Poisson or variable-coefficient Helmholtz problems on the surface with surfacefun running on numbl (numbl/browser managed session; mip installs surfacefun and packages persist in IndexedDB). The right-hand side and coefficient are MATLAB expressions in x, y, z with presets; the solution renders in a rotatable three.js view with a parula colorbar. npm run engine-test exercises the full MATLAB pipeline headlessly in Node, including an exact spherical-harmonic check.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 28f8ec12ce2b Browse files
35 changed files+4935−0
.github/workflows/deploy.ymladded+40−0View file
@@ -0,0 +1,40 @@
1+name: Deploy to GitHub Pages
2+
3+on:
4+ push:
5+ branches: [main]
6+ workflow_dispatch:
7+
8+permissions:
9+ contents: read
10+ pages: write
11+ id-token: write
12+
13+concurrency:
14+ group: pages
15+ cancel-in-progress: false
16+
17+jobs:
18+ build:
19+ runs-on: ubuntu-latest
20+ steps:
21+ - uses: actions/checkout@v4
22+ - uses: actions/setup-node@v4
23+ with:
24+ node-version: 22
25+ cache: npm
26+ - run: npm ci
27+ - run: npm run build
28+ - uses: actions/upload-pages-artifact@v3
29+ with:
30+ path: dist
31+
32+ deploy:
33+ needs: build
34+ runs-on: ubuntu-latest
35+ environment:
36+ name: github-pages
37+ url: ${{ steps.deployment.outputs.page_url }}
38+ steps:
39+ - id: deployment
40+ uses: actions/deploy-pages@v4
.gitignoreadded+3−0View file
@@ -0,0 +1,3 @@
1+node_modules/
2+dist/
3+.cache/
CLAUDE.mdadded+62−0View file
@@ -0,0 +1,62 @@
1+# CLAUDE.md
2+
3+Tips for future agents working in this repo.
4+
5+## Architecture
6+
7+```
8+matlab/ the MATLAB project the numbl session runs
9+ main.m `mip load --install surfacefun` -> solver_session()
10+ solver_session.m opens the placeholder uihtml figure (the event bridge)
11+ solve_pde.m mesh -> surfacemesh -> resample -> surfaceop -> per-patch data
12+ load_gmsh_quads.m minimal MSH 2.2 ASCII reader (canonical form only)
13+ surfacemesh_from_quads.m replaces surfacemesh.fromGmsh (see below)
14+src/mesh/ Pyodide + meshio upload pipeline (bridge.py runs in Pyodide)
15+src/engine/ thin wrapper over numbl/browser's createNumblSession:
16+ the app-level solve protocol (mesh.msh + 'solve' events,
17+ one in flight, timeout). numbl owns the worker, VFS, mip
18+ bootstrap, and IndexedDB package persistence.
19+src/pde/presets.ts PDE definitions, presets, size limits
20+src/render/ three.js SurfaceView (mesh preview / solution) + parula
21+scripts/engine-test.mjs headless Node check of the whole MATLAB pipeline
22+```
23+
24+## Key gotchas
25+
26+- **numbl >= 0.4.9 from npm** (`numbl/browser` entry, executeCode
27+ searchPaths scanning, and the flip/fieldnames/containers.Map compat fixes
28+ all landed in 0.4.9). To develop against a local numbl checkout, point
29+ package.json at `file:../../numbl` and run
30+ `npm run build:lib && npm run build:browser` there after source changes.
31+- **surfacemesh.fromGmsh is not used.** It locates the QUADS field via
32+ `startsWith(fieldnames(...), 'QUADS')` (cellstr startsWith — unsupported in
33+ numbl) and handles high-order gmsh quads. Our converter only emits 4-node
34+ quads, so `surfacemesh_from_quads.m` builds the 2x2 patches directly.
35+- **Canonical .msh only.** `load_gmsh_quads.m` assumes what
36+ `src/mesh/bridge.py` writes: MSH 2.2 ASCII, sequential 1-based node ids,
37+ type-3 elements, two tags. Uploaded .msh files in other layouts are fine —
38+ they pass through meshio and get rewritten canonically.
39+- **Solve-time errors are recoverable**: solver_session catches them and
40+ sends a `solveError` event; the uihtml session stays live for the next
41+ solve. Boot errors and dispatch-level interpreter errors are surfaced by
42+ the engine wrapper.
43+- **jsonencode collapses 1-element vectors to scalars.** Patch arrays are
44+ (p+1)^2 >= 9 long so it never bites here, but remember it when adding
45+ payload fields.
46+- Package caching: numbl/browser persists /system (mip + installed
47+ packages) in IndexedDB, wiped after 24 h of inactivity — first solve of a
48+ fresh day re-downloads ~28 MB. Delete the `numbl-embed-system` IndexedDB
49+ database to test cold boots.
50+
51+## Testing
52+
53+- `npm run engine-test` — full headless solve in Node against the local
54+ numbl build (dist-lib), including a quantitative eigenfunction check. It
55+ passes the mip search path explicitly, exercising the same
56+ searchPaths-scan behavior the numbl/browser session relies on. Downloads
57+ are cached in `.cache/` keyed by URL; delete the cache to test fresh
58+ installs.
59+- `python3 <venv>/bin/python` with meshio 5.3.5 can exercise
60+ `src/mesh/bridge.py` outside Pyodide (redirect its `/work` constant).
61+- Browser verification (Pyodide upload path, session boot, IndexedDB
62+ persistence across reloads, 3D view) is manual: `npm run dev`.
README.mdadded+69−0View file
@@ -0,0 +1,69 @@
1+# mesh-pde-solver
2+
3+Upload a quadrilateral surface mesh, pick a PDE, tweak its right-hand side
4+and coefficients, and solve it **on the surface** — entirely in your browser.
5+The mesh is converted to Gmsh format with [meshio](https://github.com/nschloe/meshio)
6+(via [Pyodide](https://pyodide.org)), and the PDE is solved by
7+[surfacefun](https://github.com/danfortunato/surfacefun) running on
8+[numbl](https://numbl.org), a MATLAB-compatible runtime, in a web worker.
9+The solution renders in a rotatable 3D view (drag to rotate, scroll to zoom).
10+
11+## PDEs
12+
13+- **Poisson (Laplace–Beltrami)** — Δu = f. On a closed surface the problem is
14+ rank-deficient: f is projected to mean zero and the mean-zero solution is
15+ returned. On an open surface, zero Dirichlet data is imposed.
16+- **Helmholtz (variable coefficient)** — (Δ + c)u = f with c(x, y, z) an
17+ arbitrary expression.
18+
19+The right-hand side f and coefficient c are MATLAB expressions in the surface
20+coordinates x, y, z (elementwise operators: `.*`, `.^`, …), with presets to
21+start from. The polynomial order per patch is adjustable (accuracy vs. time).
22+
23+## Meshes
24+
25+Uploads go through meshio, so any of `.msh .vtk .vtu .obj .off .ply .inp
26+.mesh .bdf .avs` works — but the mesh **must contain quadrilateral cells**
27+(surfacefun computes on quad patches; triangle-only meshes are rejected).
28+Two sample meshes are bundled. The converted Gmsh file can be downloaded.
29+Whether the surface is closed or open is detected from the edge connectivity.
30+
31+## How it works
32+
33+1. `src/mesh/` — meshio in Pyodide parses the upload, keeps the quad cells,
34+ and writes a canonical Gmsh MSH 2.2 ASCII file plus preview arrays.
35+2. `src/engine/` — a managed numbl session (`createNumblSession` from
36+ `numbl/browser`): numbl owns the worker and VFS, bootstraps the
37+ [mip](https://github.com/mip-org) package manager, and runs
38+ [`matlab/main.m`](matlab/main.m), which begins with
39+ `mip load --install surfacefun`. The script opens a placeholder `uihtml`
40+ figure that is never rendered — it is the event bridge: the host writes
41+ `mesh.msh` into the VFS and dispatches `solve` events; the script solves
42+ and sends per-patch data back.
43+3. `matlab/solve_pde.m` — parses the mesh (`load_gmsh_quads.m`), builds a
44+ `surfacemesh` from the quads, `resample`s it to the requested order, and
45+ solves with `surfaceop`.
46+4. `src/render/SurfaceView.tsx` — three.js view of the quad mesh or the
47+ per-patch solution grids with a parula colormap.
48+
49+The first visit downloads the Python runtime (~15 MB, browser-cached) and
50+the surfacefun/chebfun packages (~28 MB). Installed MATLAB packages persist
51+in IndexedDB across page loads (numbl wipes them after 24 h of inactivity),
52+so later visits skip the package downloads.
53+
54+## Development
55+
56+```bash
57+npm install
58+npm run dev # local dev server
59+npm run build # static build in dist/
60+npm run engine-test # headless solver check in Node (no browser)
61+python3 scripts/make_samples.py # regenerate public/samples/
62+```
63+
64+The engine test runs the exact MATLAB project the worker runs, shimming
65+numbl's synchronous-XHR `websave`/`webread` with curl (responses cached in
66+`.cache/`), and checks a Poisson solve against an exact spherical-harmonic
67+solution.
68+
69+Requires numbl >= 0.4.9 (the `numbl/browser` managed-session entry).
index.htmladded+14−0View file
@@ -0,0 +1,14 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <title>Mesh PDE Solver</title>
8+ <script src="https://cdn.jsdelivr.net/pyodide/v314.0.2/full/pyodide.js"></script>
9+ </head>
10+ <body>
11+ <div id="root"></div>
12+ <script type="module" src="/src/main.tsx"></script>
13+ </body>
14+</html>
matlab/load_gmsh_quads.madded+56−0View file
@@ -0,0 +1,56 @@
1+function msh = load_gmsh_quads(filename)
2+%LOAD_GMSH_QUADS Minimal Gmsh MSH 2.2 ASCII reader for all-quad meshes.
3+% Returns a struct with POS (n-by-3 node coordinates) and QUADS
4+% (m-by-5: four 1-based node indices plus a tag), the layout that
5+% surfacemesh.fromGmsh expects.
6+%
7+% Only the canonical form written by this app's converter is supported:
8+% sequential node ids starting at 1, and 4-node quadrangle elements
9+% (type 3) with exactly two tags.
10+
11+txt = fileread(filename);
12+
13+block = extract_block(txt, '$Nodes', '$EndNodes');
14+vals = sscanf(block, '%f');
15+nn = vals(1);
16+if numel(vals) ~= 1 + 4*nn
17+ error('load_gmsh_quads:badNodes', 'Unexpected $Nodes layout.');
18+end
19+data = reshape(vals(2:end), 4, nn);
20+if any(data(1, :) ~= 1:nn)
21+ error('load_gmsh_quads:badNodes', 'Node ids must be sequential from 1.');
22+end
23+pos = data(2:4, :)';
24+
25+block = extract_block(txt, '$Elements', '$EndElements');
26+vals = sscanf(block, '%f');
27+ne = vals(1);
28+% Each element line: id type(=3) ntags(=2) tag tag n1 n2 n3 n4
29+if numel(vals) ~= 1 + 9*ne
30+ error('load_gmsh_quads:badElements', ...
31+ 'Expected only 4-node quadrangles with two tags.');
32+end
33+data = reshape(vals(2:end), 9, ne);
34+if any(data(2, :) ~= 3)
35+ error('load_gmsh_quads:badElements', ...
36+ 'Expected only 4-node quadrangle elements (type 3).');
37+end
38+idx = data(6:9, :)';
39+if any(idx(:) < 1) || any(idx(:) > nn)
40+ error('load_gmsh_quads:badElements', 'Element node index out of range.');
41+end
42+quads = [idx, ones(ne, 1)];
43+
44+msh = struct();
45+msh.POS = pos;
46+msh.QUADS = quads;
47+end
48+
49+function block = extract_block(txt, startMarker, endMarker)
50+i1 = strfind(txt, startMarker);
51+i2 = strfind(txt, endMarker);
52+if isempty(i1) || isempty(i2)
53+ error('load_gmsh_quads:badFile', 'Missing %s section.', startMarker);
54+end
55+block = txt(i1(1) + length(startMarker) : i2(1) - 1);
56+end
matlab/main.madded+13−0View file
@@ -0,0 +1,13 @@
1+% main.m — persistent solver session for mesh-pde-solver.
2+%
3+% Runs once inside a numbl/browser managed session, which bootstraps the mip
4+% package manager (and puts it on the path) before this script starts. mip
5+% fetches surfacefun and its chebfun dependency on first use; the session
6+% persists installed packages across page loads. The script then opens a
7+% placeholder uihtml figure purely as an event bridge: the host writes
8+% mesh.msh into the VFS and dispatches 'solve' events with the PDE
9+% parameters; solver_session solves and sends back per-patch data.
10+
11+mip load --install surfacefun;
12+
13+solver_session();
matlab/placeholder.htmladded+1−0View file
@@ -0,0 +1 @@
1+<!-- headless: the uihtml bridge is intercepted host-side and never rendered -->
matlab/solve_pde.madded+76−0View file
@@ -0,0 +1,76 @@
1+function result = solve_pde(mshfile, params)
2+%SOLVE_PDE Load the quad mesh and solve the selected PDE on it.
3+% params fields (from the host UI):
4+% pde - 'poisson' (lap u = f) or 'helmholtz' ((lap + c) u = f)
5+% f - right-hand side, a MATLAB expression in x, y, z
6+% c - zeroth-order coefficient expression (helmholtz only)
7+% p - polynomial order per patch
8+% closed - true if every mesh edge is shared by exactly two quads
9+% (determined host-side from the connectivity)
10+
11+msh = load_gmsh_quads(mshfile);
12+dom = surfacemesh_from_quads(msh);
13+dom = resample(dom, params.p + 1);
14+
15+fh = eval(['@(x, y, z) ', params.f]);
16+f = surfacefun(@(x, y, z) fh(x, y, z) + 0*x, dom);
17+
18+pdo = [];
19+pdo.lap = 1;
20+isPoisson = strcmp(params.pde, 'poisson');
21+if ~isPoisson
22+ ch = eval(['@(x, y, z) ', params.c]);
23+ pdo.c = @(x, y, z) ch(x, y, z) + 0*x;
24+end
25+
26+closed = params.closed;
27+if isPoisson && closed
28+ % The closed-surface Laplace-Beltrami problem is rank-deficient by one
29+ % and only solvable for mean-zero data; project the RHS accordingly.
30+ f = f - mean(f);
31+end
32+
33+L = surfaceop(dom, pdo, f);
34+if closed
35+ if isPoisson
36+ L.rankdef = true;
37+ end
38+ u = L.solve();
39+else
40+ u = L.solve(0); % zero Dirichlet boundary data on open surfaces
41+end
42+
43+result = pack_solution(dom, u);
44+result.pde = params.pde;
45+end
46+
47+function data = pack_solution(dom, u)
48+% One flat (column-major) x/y/z/u array per patch, each an n-by-n grid —
49+% the same layout surfacefun-interactive's figure app uses.
50+np = length(dom);
51+px = cell(1, np);
52+py = cell(1, np);
53+pz = cell(1, np);
54+pu = cell(1, np);
55+umin = inf;
56+umax = -inf;
57+for k = 1:np
58+ px{k} = real(dom.x{k}(:).');
59+ py{k} = real(dom.y{k}(:).');
60+ pz{k} = real(dom.z{k}(:).');
61+ vals = real(u.vals{k}(:).');
62+ pu{k} = vals;
63+ umin = min(umin, min(vals));
64+ umax = max(umax, max(vals));
65+end
66+data = struct();
67+data.type = 'solution';
68+data.n = size(dom.x{1}, 1);
69+data.npatches = np;
70+data.x = px;
71+data.y = py;
72+data.z = pz;
73+data.u = pu;
74+data.umin = umin;
75+data.umax = umax;
76+end
matlab/solver_session.madded+22−0View file
@@ -0,0 +1,22 @@
1+function solver_session()
2+%SOLVER_SESSION Open the uihtml event bridge and serve solve requests.
3+% The figure is never rendered — the host intercepts the uihtml component
4+% and speaks its event protocol directly (see src/engine/).
5+
6+html = fileread('placeholder.html');
7+fig = figure;
8+uihtml(fig, 'HTMLSource', html, 'Data', struct('type', 'ready'), ...
9+ 'HTMLEventReceivedFcn', @on_event);
10+end
11+
12+function on_event(src, ev)
13+if ~strcmp(ev.HTMLEventName, 'solve')
14+ return
15+end
16+try
17+ result = solve_pde('mesh.msh', ev.HTMLEventData);
18+ sendEventToHTMLSource(src, 'solution', result);
19+catch err
20+ sendEventToHTMLSource(src, 'solveError', struct('message', err.message));
21+end
22+end
matlab/surfacemesh_from_quads.madded+29−0View file
@@ -0,0 +1,29 @@
1+function dom = surfacemesh_from_quads(msh)
2+%SURFACEMESH_FROM_QUADS Build a surfacemesh from a 4-node quad msh struct.
3+% Adapted from surfacemesh.fromGmsh (surfacefun): that version locates a
4+% QUADS* field via startsWith over fieldnames — which numbl's startsWith
5+% does not support for cell arrays — and handles high-order gmsh quads.
6+% Our converter only ever emits plain 4-node quads (Gmsh corner order
7+% 1-2-3-4 counterclockwise), where the equispaced and Chebyshev 2-point
8+% grids coincide, so each patch is just its corner values arranged as a
9+% 2-by-2 tensor grid.
10+
11+quads = msh.QUADS;
12+nelem = size(quads, 1);
13+x = cell(nelem, 1);
14+y = cell(nelem, 1);
15+z = cell(nelem, 1);
16+
17+for k = 1:nelem
18+ idx = quads(k, 1:4);
19+ px = msh.POS(idx, 1);
20+ py = msh.POS(idx, 2);
21+ pz = msh.POS(idx, 3);
22+ % Gmsh corners 1,2,3,4 -> tensor grid (1,1),(2,1),(2,2),(1,2)
23+ x{k} = [px(1) px(4); px(2) px(3)];
24+ y{k} = [py(1) py(4); py(2) py(3)];
25+ z{k} = [pz(1) pz(4); pz(2) pz(3)];
26+end
27+
28+dom = surfacemesh(x, y, z);
29+end
package-lock.jsonadded+1565−0View file
@@ -0,0 +1,1565 @@
1+{
2+ "name": "mesh-pde-solver",
3+ "version": "0.0.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "mesh-pde-solver",
9+ "version": "0.0.0",
10+ "dependencies": {
11+ "fflate": "^0.8.2",
12+ "numbl": "^0.4.9",
13+ "react": "^19.2.7",
14+ "react-dom": "^19.2.7",
15+ "three": "^0.185.1"
16+ },
17+ "devDependencies": {
18+ "@types/node": "^24.13.2",
19+ "@types/react": "^19.2.17",
20+ "@types/react-dom": "^19.2.3",
21+ "@types/three": "^0.185.0",
22+ "@vitejs/plugin-react": "^6.0.3",
23+ "oxlint": "^1.71.0",
24+ "typescript": "~6.0.2",
25+ "vite": "^8.1.1"
26+ }
27+ },
28+ "../../numbl": {
29+ "version": "0.4.9",
30+ "hasInstallScript": true,
31+ "license": "Apache-2.0",
32+ "dependencies": {
33+ "fflate": "^0.8.2",
34+ "h5wasm": "^0.10.3",
35+ "node-addon-api": "^8.3.0",
36+ "pako": "^2.1.0",
37+ "qhull-wasm": "^0.0.1",
38+ "react-markdown": "^10.1.0",
39+ "react-syntax-highlighter": "^16.1.1",
40+ "remark-gfm": "^4.0.1",
41+ "three": "^0.183.2"
42+ },
43+ "bin": {
44+ "numbl": "dist-cli/cli.js"
45+ },
46+ "devDependencies": {
47+ "@emotion/react": "^11.14.0",
48+ "@emotion/styled": "^11.14.1",
49+ "@eslint/js": "^9.39.1",
50+ "@monaco-editor/react": "^4.7.0",
51+ "@mui/icons-material": "^7.3.7",
52+ "@mui/material": "^7.3.7",
53+ "@playwright/test": "^1.59.1",
54+ "@types/node": "^24.10.1",
55+ "@types/pako": "^2.0.4",
56+ "@types/react": "^19.2.5",
57+ "@types/react-dom": "^19.2.3",
58+ "@types/react-syntax-highlighter": "^15.5.13",
59+ "@types/three": "^0.183.1",
60+ "@vitejs/plugin-react": "^5.1.1",
61+ "@vitest/coverage-v8": "^4.0.18",
62+ "@xterm/addon-fit": "^0.11.0",
63+ "@xterm/xterm": "^6.0.0",
64+ "dexie": "^4.3.0",
65+ "esbuild": "^0.27.3",
66+ "eslint": "^9.39.1",
67+ "eslint-config-prettier": "^10.1.8",
68+ "eslint-plugin-react-hooks": "^7.0.1",
69+ "eslint-plugin-react-refresh": "^0.4.24",
70+ "globals": "^16.5.0",
71+ "husky": "^9.1.7",
72+ "lint-staged": "^16.2.7",
73+ "node-gyp": "^11.2.0",
74+ "prettier": "^3.8.1",
75+ "react": "^19.2.0",
76+ "react-dom": "^19.2.0",
77+ "react-router-dom": "^7.13.0",
78+ "tsx": "^4.21.0",
79+ "typescript": "~5.9.3",
80+ "typescript-eslint": "^8.46.4",
81+ "vite": "^7.2.4",
82+ "vite-plugin-wasm": "^3.5.0",
83+ "vitest": "^4.0.18"
84+ },
85+ "engines": {
86+ "node": ">=18"
87+ },
88+ "optionalDependencies": {
89+ "koffi": "^2.15.2"
90+ },
91+ "peerDependencies": {
92+ "react": ">=18",
93+ "react-dom": ">=18"
94+ },
95+ "peerDependenciesMeta": {
96+ "react": {
97+ "optional": true
98+ },
99+ "react-dom": {
100+ "optional": true
101+ }
102+ }
103+ },
104+ "node_modules/@dimforge/rapier3d-compat": {
105+ "version": "0.12.0",
106+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
107+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
108+ "dev": true,
109+ "license": "Apache-2.0"
110+ },
111+ "node_modules/@emnapi/core": {
112+ "version": "1.11.1",
113+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
114+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
115+ "dev": true,
116+ "license": "MIT",
117+ "optional": true,
118+ "dependencies": {
119+ "@emnapi/wasi-threads": "1.2.2",
120+ "tslib": "^2.4.0"
121+ }
122+ },
123+ "node_modules/@emnapi/runtime": {
124+ "version": "1.11.1",
125+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
126+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
127+ "dev": true,
128+ "license": "MIT",
129+ "optional": true,
130+ "dependencies": {
131+ "tslib": "^2.4.0"
132+ }
133+ },
134+ "node_modules/@emnapi/wasi-threads": {
135+ "version": "1.2.2",
136+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
137+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
138+ "dev": true,
139+ "license": "MIT",
140+ "optional": true,
141+ "dependencies": {
142+ "tslib": "^2.4.0"
143+ }
144+ },
145+ "node_modules/@napi-rs/wasm-runtime": {
146+ "version": "1.1.6",
147+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
148+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
149+ "dev": true,
150+ "license": "MIT",
151+ "optional": true,
152+ "dependencies": {
153+ "@tybys/wasm-util": "^0.10.3"
154+ },
155+ "funding": {
156+ "type": "github",
157+ "url": "https://github.com/sponsors/Brooooooklyn"
158+ },
159+ "peerDependencies": {
160+ "@emnapi/core": "^1.7.1",
161+ "@emnapi/runtime": "^1.7.1"
162+ }
163+ },
164+ "node_modules/@oxc-project/types": {
165+ "version": "0.139.0",
166+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
167+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
168+ "dev": true,
169+ "license": "MIT",
170+ "funding": {
171+ "url": "https://github.com/sponsors/Boshen"
172+ }
173+ },
174+ "node_modules/@oxlint/binding-android-arm-eabi": {
175+ "version": "1.73.0",
176+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
177+ "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==",
178+ "cpu": [
179+ "arm"
180+ ],
181+ "dev": true,
182+ "license": "MIT",
183+ "optional": true,
184+ "os": [
185+ "android"
186+ ],
187+ "engines": {
188+ "node": "^20.19.0 || >=22.12.0"
189+ }
190+ },
191+ "node_modules/@oxlint/binding-android-arm64": {
192+ "version": "1.73.0",
193+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz",
194+ "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==",
195+ "cpu": [
196+ "arm64"
197+ ],
198+ "dev": true,
199+ "license": "MIT",
200+ "optional": true,
201+ "os": [
202+ "android"
203+ ],
204+ "engines": {
205+ "node": "^20.19.0 || >=22.12.0"
206+ }
207+ },
208+ "node_modules/@oxlint/binding-darwin-arm64": {
209+ "version": "1.73.0",
210+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz",
211+ "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==",
212+ "cpu": [
213+ "arm64"
214+ ],
215+ "dev": true,
216+ "license": "MIT",
217+ "optional": true,
218+ "os": [
219+ "darwin"
220+ ],
221+ "engines": {
222+ "node": "^20.19.0 || >=22.12.0"
223+ }
224+ },
225+ "node_modules/@oxlint/binding-darwin-x64": {
226+ "version": "1.73.0",
227+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz",
228+ "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==",
229+ "cpu": [
230+ "x64"
231+ ],
232+ "dev": true,
233+ "license": "MIT",
234+ "optional": true,
235+ "os": [
236+ "darwin"
237+ ],
238+ "engines": {
239+ "node": "^20.19.0 || >=22.12.0"
240+ }
241+ },
242+ "node_modules/@oxlint/binding-freebsd-x64": {
243+ "version": "1.73.0",
244+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz",
245+ "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==",
246+ "cpu": [
247+ "x64"
248+ ],
249+ "dev": true,
250+ "license": "MIT",
251+ "optional": true,
252+ "os": [
253+ "freebsd"
254+ ],
255+ "engines": {
256+ "node": "^20.19.0 || >=22.12.0"
257+ }
258+ },
259+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
260+ "version": "1.73.0",
261+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz",
262+ "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==",
263+ "cpu": [
264+ "arm"
265+ ],
266+ "dev": true,
267+ "license": "MIT",
268+ "optional": true,
269+ "os": [
270+ "linux"
271+ ],
272+ "engines": {
273+ "node": "^20.19.0 || >=22.12.0"
274+ }
275+ },
276+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
277+ "version": "1.73.0",
278+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz",
279+ "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==",
280+ "cpu": [
281+ "arm"
282+ ],
283+ "dev": true,
284+ "license": "MIT",
285+ "optional": true,
286+ "os": [
287+ "linux"
288+ ],
289+ "engines": {
290+ "node": "^20.19.0 || >=22.12.0"
291+ }
292+ },
293+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
294+ "version": "1.73.0",
295+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz",
296+ "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==",
297+ "cpu": [
298+ "arm64"
299+ ],
300+ "dev": true,
301+ "libc": [
302+ "glibc"
303+ ],
304+ "license": "MIT",
305+ "optional": true,
306+ "os": [
307+ "linux"
308+ ],
309+ "engines": {
310+ "node": "^20.19.0 || >=22.12.0"
311+ }
312+ },
313+ "node_modules/@oxlint/binding-linux-arm64-musl": {
314+ "version": "1.73.0",
315+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz",
316+ "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==",
317+ "cpu": [
318+ "arm64"
319+ ],
320+ "dev": true,
321+ "libc": [
322+ "musl"
323+ ],
324+ "license": "MIT",
325+ "optional": true,
326+ "os": [
327+ "linux"
328+ ],
329+ "engines": {
330+ "node": "^20.19.0 || >=22.12.0"
331+ }
332+ },
333+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
334+ "version": "1.73.0",
335+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz",
336+ "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==",
337+ "cpu": [
338+ "ppc64"
339+ ],
340+ "dev": true,
341+ "libc": [
342+ "glibc"
343+ ],
344+ "license": "MIT",
345+ "optional": true,
346+ "os": [
347+ "linux"
348+ ],
349+ "engines": {
350+ "node": "^20.19.0 || >=22.12.0"
351+ }
352+ },
353+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
354+ "version": "1.73.0",
355+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz",
356+ "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==",
357+ "cpu": [
358+ "riscv64"
359+ ],
360+ "dev": true,
361+ "libc": [
362+ "glibc"
363+ ],
364+ "license": "MIT",
365+ "optional": true,
366+ "os": [
367+ "linux"
368+ ],
369+ "engines": {
370+ "node": "^20.19.0 || >=22.12.0"
371+ }
372+ },
373+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
374+ "version": "1.73.0",
375+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz",
376+ "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==",
377+ "cpu": [
378+ "riscv64"
379+ ],
380+ "dev": true,
381+ "libc": [
382+ "musl"
383+ ],
384+ "license": "MIT",
385+ "optional": true,
386+ "os": [
387+ "linux"
388+ ],
389+ "engines": {
390+ "node": "^20.19.0 || >=22.12.0"
391+ }
392+ },
393+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
394+ "version": "1.73.0",
395+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz",
396+ "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==",
397+ "cpu": [
398+ "s390x"
399+ ],
400+ "dev": true,
401+ "libc": [
402+ "glibc"
403+ ],
404+ "license": "MIT",
405+ "optional": true,
406+ "os": [
407+ "linux"
408+ ],
409+ "engines": {
410+ "node": "^20.19.0 || >=22.12.0"
411+ }
412+ },
413+ "node_modules/@oxlint/binding-linux-x64-gnu": {
414+ "version": "1.73.0",
415+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz",
416+ "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==",
417+ "cpu": [
418+ "x64"
419+ ],
420+ "dev": true,
421+ "libc": [
422+ "glibc"
423+ ],
424+ "license": "MIT",
425+ "optional": true,
426+ "os": [
427+ "linux"
428+ ],
429+ "engines": {
430+ "node": "^20.19.0 || >=22.12.0"
431+ }
432+ },
433+ "node_modules/@oxlint/binding-linux-x64-musl": {
434+ "version": "1.73.0",
435+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz",
436+ "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==",
437+ "cpu": [
438+ "x64"
439+ ],
440+ "dev": true,
441+ "libc": [
442+ "musl"
443+ ],
444+ "license": "MIT",
445+ "optional": true,
446+ "os": [
447+ "linux"
448+ ],
449+ "engines": {
450+ "node": "^20.19.0 || >=22.12.0"
451+ }
452+ },
453+ "node_modules/@oxlint/binding-openharmony-arm64": {
454+ "version": "1.73.0",
455+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz",
456+ "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==",
457+ "cpu": [
458+ "arm64"
459+ ],
460+ "dev": true,
461+ "license": "MIT",
462+ "optional": true,
463+ "os": [
464+ "openharmony"
465+ ],
466+ "engines": {
467+ "node": "^20.19.0 || >=22.12.0"
468+ }
469+ },
470+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
471+ "version": "1.73.0",
472+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz",
473+ "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==",
474+ "cpu": [
475+ "arm64"
476+ ],
477+ "dev": true,
478+ "license": "MIT",
479+ "optional": true,
480+ "os": [
481+ "win32"
482+ ],
483+ "engines": {
484+ "node": "^20.19.0 || >=22.12.0"
485+ }
486+ },
487+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
488+ "version": "1.73.0",
489+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz",
490+ "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==",
491+ "cpu": [
492+ "ia32"
493+ ],
494+ "dev": true,
495+ "license": "MIT",
496+ "optional": true,
497+ "os": [
498+ "win32"
499+ ],
500+ "engines": {
501+ "node": "^20.19.0 || >=22.12.0"
502+ }
503+ },
504+ "node_modules/@oxlint/binding-win32-x64-msvc": {
505+ "version": "1.73.0",
506+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz",
507+ "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==",
508+ "cpu": [
509+ "x64"
510+ ],
511+ "dev": true,
512+ "license": "MIT",
513+ "optional": true,
514+ "os": [
515+ "win32"
516+ ],
517+ "engines": {
518+ "node": "^20.19.0 || >=22.12.0"
519+ }
520+ },
521+ "node_modules/@rolldown/binding-android-arm64": {
522+ "version": "1.1.5",
523+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
524+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
525+ "cpu": [
526+ "arm64"
527+ ],
528+ "dev": true,
529+ "license": "MIT",
530+ "optional": true,
531+ "os": [
532+ "android"
533+ ],
534+ "engines": {
535+ "node": "^20.19.0 || >=22.12.0"
536+ }
537+ },
538+ "node_modules/@rolldown/binding-darwin-arm64": {
539+ "version": "1.1.5",
540+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
541+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
542+ "cpu": [
543+ "arm64"
544+ ],
545+ "dev": true,
546+ "license": "MIT",
547+ "optional": true,
548+ "os": [
549+ "darwin"
550+ ],
551+ "engines": {
552+ "node": "^20.19.0 || >=22.12.0"
553+ }
554+ },
555+ "node_modules/@rolldown/binding-darwin-x64": {
556+ "version": "1.1.5",
557+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
558+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
559+ "cpu": [
560+ "x64"
561+ ],
562+ "dev": true,
563+ "license": "MIT",
564+ "optional": true,
565+ "os": [
566+ "darwin"
567+ ],
568+ "engines": {
569+ "node": "^20.19.0 || >=22.12.0"
570+ }
571+ },
572+ "node_modules/@rolldown/binding-freebsd-x64": {
573+ "version": "1.1.5",
574+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
575+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
576+ "cpu": [
577+ "x64"
578+ ],
579+ "dev": true,
580+ "license": "MIT",
581+ "optional": true,
582+ "os": [
583+ "freebsd"
584+ ],
585+ "engines": {
586+ "node": "^20.19.0 || >=22.12.0"
587+ }
588+ },
589+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
590+ "version": "1.1.5",
591+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
592+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
593+ "cpu": [
594+ "arm"
595+ ],
596+ "dev": true,
597+ "license": "MIT",
598+ "optional": true,
599+ "os": [
600+ "linux"
601+ ],
602+ "engines": {
603+ "node": "^20.19.0 || >=22.12.0"
604+ }
605+ },
606+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
607+ "version": "1.1.5",
608+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
609+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
610+ "cpu": [
611+ "arm64"
612+ ],
613+ "dev": true,
614+ "libc": [
615+ "glibc"
616+ ],
617+ "license": "MIT",
618+ "optional": true,
619+ "os": [
620+ "linux"
621+ ],
622+ "engines": {
623+ "node": "^20.19.0 || >=22.12.0"
624+ }
625+ },
626+ "node_modules/@rolldown/binding-linux-arm64-musl": {
627+ "version": "1.1.5",
628+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
629+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
630+ "cpu": [
631+ "arm64"
632+ ],
633+ "dev": true,
634+ "libc": [
635+ "musl"
636+ ],
637+ "license": "MIT",
638+ "optional": true,
639+ "os": [
640+ "linux"
641+ ],
642+ "engines": {
643+ "node": "^20.19.0 || >=22.12.0"
644+ }
645+ },
646+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
647+ "version": "1.1.5",
648+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
649+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
650+ "cpu": [
651+ "ppc64"
652+ ],
653+ "dev": true,
654+ "libc": [
655+ "glibc"
656+ ],
657+ "license": "MIT",
658+ "optional": true,
659+ "os": [
660+ "linux"
661+ ],
662+ "engines": {
663+ "node": "^20.19.0 || >=22.12.0"
664+ }
665+ },
666+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
667+ "version": "1.1.5",
668+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
669+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
670+ "cpu": [
671+ "s390x"
672+ ],
673+ "dev": true,
674+ "libc": [
675+ "glibc"
676+ ],
677+ "license": "MIT",
678+ "optional": true,
679+ "os": [
680+ "linux"
681+ ],
682+ "engines": {
683+ "node": "^20.19.0 || >=22.12.0"
684+ }
685+ },
686+ "node_modules/@rolldown/binding-linux-x64-gnu": {
687+ "version": "1.1.5",
688+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
689+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
690+ "cpu": [
691+ "x64"
692+ ],
693+ "dev": true,
694+ "libc": [
695+ "glibc"
696+ ],
697+ "license": "MIT",
698+ "optional": true,
699+ "os": [
700+ "linux"
701+ ],
702+ "engines": {
703+ "node": "^20.19.0 || >=22.12.0"
704+ }
705+ },
706+ "node_modules/@rolldown/binding-linux-x64-musl": {
707+ "version": "1.1.5",
708+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
709+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
710+ "cpu": [
711+ "x64"
712+ ],
713+ "dev": true,
714+ "libc": [
715+ "musl"
716+ ],
717+ "license": "MIT",
718+ "optional": true,
719+ "os": [
720+ "linux"
721+ ],
722+ "engines": {
723+ "node": "^20.19.0 || >=22.12.0"
724+ }
725+ },
726+ "node_modules/@rolldown/binding-openharmony-arm64": {
727+ "version": "1.1.5",
728+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
729+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
730+ "cpu": [
731+ "arm64"
732+ ],
733+ "dev": true,
734+ "license": "MIT",
735+ "optional": true,
736+ "os": [
737+ "openharmony"
738+ ],
739+ "engines": {
740+ "node": "^20.19.0 || >=22.12.0"
741+ }
742+ },
743+ "node_modules/@rolldown/binding-wasm32-wasi": {
744+ "version": "1.1.5",
745+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
746+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
747+ "cpu": [
748+ "wasm32"
749+ ],
750+ "dev": true,
751+ "license": "MIT",
752+ "optional": true,
753+ "dependencies": {
754+ "@emnapi/core": "1.11.1",
755+ "@emnapi/runtime": "1.11.1",
756+ "@napi-rs/wasm-runtime": "^1.1.6"
757+ },
758+ "engines": {
759+ "node": "^20.19.0 || >=22.12.0"
760+ }
761+ },
762+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
763+ "version": "1.1.5",
764+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
765+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
766+ "cpu": [
767+ "arm64"
768+ ],
769+ "dev": true,
770+ "license": "MIT",
771+ "optional": true,
772+ "os": [
773+ "win32"
774+ ],
775+ "engines": {
776+ "node": "^20.19.0 || >=22.12.0"
777+ }
778+ },
779+ "node_modules/@rolldown/binding-win32-x64-msvc": {
780+ "version": "1.1.5",
781+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
782+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
783+ "cpu": [
784+ "x64"
785+ ],
786+ "dev": true,
787+ "license": "MIT",
788+ "optional": true,
789+ "os": [
790+ "win32"
791+ ],
792+ "engines": {
793+ "node": "^20.19.0 || >=22.12.0"
794+ }
795+ },
796+ "node_modules/@rolldown/pluginutils": {
797+ "version": "1.0.1",
798+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
799+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
800+ "dev": true,
801+ "license": "MIT"
802+ },
803+ "node_modules/@tweenjs/tween.js": {
804+ "version": "23.1.3",
805+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
806+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
807+ "dev": true,
808+ "license": "MIT"
809+ },
810+ "node_modules/@tybys/wasm-util": {
811+ "version": "0.10.3",
812+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
813+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
814+ "dev": true,
815+ "license": "MIT",
816+ "optional": true,
817+ "dependencies": {
818+ "tslib": "^2.4.0"
819+ }
820+ },
821+ "node_modules/@types/node": {
822+ "version": "24.13.3",
823+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
824+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
825+ "dev": true,
826+ "license": "MIT",
827+ "dependencies": {
828+ "undici-types": "~7.18.0"
829+ }
830+ },
831+ "node_modules/@types/react": {
832+ "version": "19.2.17",
833+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
834+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
835+ "dev": true,
836+ "license": "MIT",
837+ "dependencies": {
838+ "csstype": "^3.2.2"
839+ }
840+ },
841+ "node_modules/@types/react-dom": {
842+ "version": "19.2.3",
843+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
844+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
845+ "dev": true,
846+ "license": "MIT",
847+ "peerDependencies": {
848+ "@types/react": "^19.2.0"
849+ }
850+ },
851+ "node_modules/@types/stats.js": {
852+ "version": "0.17.4",
853+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
854+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
855+ "dev": true,
856+ "license": "MIT"
857+ },
858+ "node_modules/@types/three": {
859+ "version": "0.185.0",
860+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz",
861+ "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==",
862+ "dev": true,
863+ "license": "MIT",
864+ "dependencies": {
865+ "@dimforge/rapier3d-compat": "~0.12.0",
866+ "@tweenjs/tween.js": "~23.1.3",
867+ "@types/stats.js": "*",
868+ "@types/webxr": ">=0.5.17",
869+ "fflate": "~0.8.2",
870+ "meshoptimizer": "~1.1.1"
871+ }
872+ },
873+ "node_modules/@types/webxr": {
874+ "version": "0.5.24",
875+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
876+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
877+ "dev": true,
878+ "license": "MIT"
879+ },
880+ "node_modules/@vitejs/plugin-react": {
881+ "version": "6.0.3",
882+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
883+ "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
884+ "dev": true,
885+ "license": "MIT",
886+ "dependencies": {
887+ "@rolldown/pluginutils": "^1.0.1"
888+ },
889+ "engines": {
890+ "node": "^20.19.0 || >=22.12.0"
891+ },
892+ "peerDependencies": {
893+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
894+ "babel-plugin-react-compiler": "^1.0.0",
895+ "vite": "^8.0.0"
896+ },
897+ "peerDependenciesMeta": {
898+ "@rolldown/plugin-babel": {
899+ "optional": true
900+ },
901+ "babel-plugin-react-compiler": {
902+ "optional": true
903+ }
904+ }
905+ },
906+ "node_modules/csstype": {
907+ "version": "3.2.3",
908+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
909+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
910+ "dev": true,
911+ "license": "MIT"
912+ },
913+ "node_modules/detect-libc": {
914+ "version": "2.1.2",
915+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
916+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
917+ "dev": true,
918+ "license": "Apache-2.0",
919+ "engines": {
920+ "node": ">=8"
921+ }
922+ },
923+ "node_modules/fdir": {
924+ "version": "6.5.0",
925+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
926+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
927+ "dev": true,
928+ "license": "MIT",
929+ "engines": {
930+ "node": ">=12.0.0"
931+ },
932+ "peerDependencies": {
933+ "picomatch": "^3 || ^4"
934+ },
935+ "peerDependenciesMeta": {
936+ "picomatch": {
937+ "optional": true
938+ }
939+ }
940+ },
941+ "node_modules/fflate": {
942+ "version": "0.8.3",
943+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
944+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
945+ "license": "MIT"
946+ },
947+ "node_modules/fsevents": {
948+ "version": "2.3.3",
949+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
950+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
951+ "dev": true,
952+ "hasInstallScript": true,
953+ "license": "MIT",
954+ "optional": true,
955+ "os": [
956+ "darwin"
957+ ],
958+ "engines": {
959+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
960+ }
961+ },
962+ "node_modules/lightningcss": {
963+ "version": "1.32.0",
964+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
965+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
966+ "dev": true,
967+ "license": "MPL-2.0",
968+ "dependencies": {
969+ "detect-libc": "^2.0.3"
970+ },
971+ "engines": {
972+ "node": ">= 12.0.0"
973+ },
974+ "funding": {
975+ "type": "opencollective",
976+ "url": "https://opencollective.com/parcel"
977+ },
978+ "optionalDependencies": {
979+ "lightningcss-android-arm64": "1.32.0",
980+ "lightningcss-darwin-arm64": "1.32.0",
981+ "lightningcss-darwin-x64": "1.32.0",
982+ "lightningcss-freebsd-x64": "1.32.0",
983+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
984+ "lightningcss-linux-arm64-gnu": "1.32.0",
985+ "lightningcss-linux-arm64-musl": "1.32.0",
986+ "lightningcss-linux-x64-gnu": "1.32.0",
987+ "lightningcss-linux-x64-musl": "1.32.0",
988+ "lightningcss-win32-arm64-msvc": "1.32.0",
989+ "lightningcss-win32-x64-msvc": "1.32.0"
990+ }
991+ },
992+ "node_modules/lightningcss-android-arm64": {
993+ "version": "1.32.0",
994+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
995+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
996+ "cpu": [
997+ "arm64"
998+ ],
999+ "dev": true,
1000+ "license": "MPL-2.0",
1001+ "optional": true,
1002+ "os": [
1003+ "android"
1004+ ],
1005+ "engines": {
1006+ "node": ">= 12.0.0"
1007+ },
1008+ "funding": {
1009+ "type": "opencollective",
1010+ "url": "https://opencollective.com/parcel"
1011+ }
1012+ },
1013+ "node_modules/lightningcss-darwin-arm64": {
1014+ "version": "1.32.0",
1015+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1016+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1017+ "cpu": [
1018+ "arm64"
1019+ ],
1020+ "dev": true,
1021+ "license": "MPL-2.0",
1022+ "optional": true,
1023+ "os": [
1024+ "darwin"
1025+ ],
1026+ "engines": {
1027+ "node": ">= 12.0.0"
1028+ },
1029+ "funding": {
1030+ "type": "opencollective",
1031+ "url": "https://opencollective.com/parcel"
1032+ }
1033+ },
1034+ "node_modules/lightningcss-darwin-x64": {
1035+ "version": "1.32.0",
1036+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1037+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1038+ "cpu": [
1039+ "x64"
1040+ ],
1041+ "dev": true,
1042+ "license": "MPL-2.0",
1043+ "optional": true,
1044+ "os": [
1045+ "darwin"
1046+ ],
1047+ "engines": {
1048+ "node": ">= 12.0.0"
1049+ },
1050+ "funding": {
1051+ "type": "opencollective",
1052+ "url": "https://opencollective.com/parcel"
1053+ }
1054+ },
1055+ "node_modules/lightningcss-freebsd-x64": {
1056+ "version": "1.32.0",
1057+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1058+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1059+ "cpu": [
1060+ "x64"
1061+ ],
1062+ "dev": true,
1063+ "license": "MPL-2.0",
1064+ "optional": true,
1065+ "os": [
1066+ "freebsd"
1067+ ],
1068+ "engines": {
1069+ "node": ">= 12.0.0"
1070+ },
1071+ "funding": {
1072+ "type": "opencollective",
1073+ "url": "https://opencollective.com/parcel"
1074+ }
1075+ },
1076+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1077+ "version": "1.32.0",
1078+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1079+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1080+ "cpu": [
1081+ "arm"
1082+ ],
1083+ "dev": true,
1084+ "license": "MPL-2.0",
1085+ "optional": true,
1086+ "os": [
1087+ "linux"
1088+ ],
1089+ "engines": {
1090+ "node": ">= 12.0.0"
1091+ },
1092+ "funding": {
1093+ "type": "opencollective",
1094+ "url": "https://opencollective.com/parcel"
1095+ }
1096+ },
1097+ "node_modules/lightningcss-linux-arm64-gnu": {
1098+ "version": "1.32.0",
1099+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
1100+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
1101+ "cpu": [
1102+ "arm64"
1103+ ],
1104+ "dev": true,
1105+ "libc": [
1106+ "glibc"
1107+ ],
1108+ "license": "MPL-2.0",
1109+ "optional": true,
1110+ "os": [
1111+ "linux"
1112+ ],
1113+ "engines": {
1114+ "node": ">= 12.0.0"
1115+ },
1116+ "funding": {
1117+ "type": "opencollective",
1118+ "url": "https://opencollective.com/parcel"
1119+ }
1120+ },
1121+ "node_modules/lightningcss-linux-arm64-musl": {
1122+ "version": "1.32.0",
1123+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
1124+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
1125+ "cpu": [
1126+ "arm64"
1127+ ],
1128+ "dev": true,
1129+ "libc": [
1130+ "musl"
1131+ ],
1132+ "license": "MPL-2.0",
1133+ "optional": true,
1134+ "os": [
1135+ "linux"
1136+ ],
1137+ "engines": {
1138+ "node": ">= 12.0.0"
1139+ },
1140+ "funding": {
1141+ "type": "opencollective",
1142+ "url": "https://opencollective.com/parcel"
1143+ }
1144+ },
1145+ "node_modules/lightningcss-linux-x64-gnu": {
1146+ "version": "1.32.0",
1147+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
1148+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
1149+ "cpu": [
1150+ "x64"
1151+ ],
1152+ "dev": true,
1153+ "libc": [
1154+ "glibc"
1155+ ],
1156+ "license": "MPL-2.0",
1157+ "optional": true,
1158+ "os": [
1159+ "linux"
1160+ ],
1161+ "engines": {
1162+ "node": ">= 12.0.0"
1163+ },
1164+ "funding": {
1165+ "type": "opencollective",
1166+ "url": "https://opencollective.com/parcel"
1167+ }
1168+ },
1169+ "node_modules/lightningcss-linux-x64-musl": {
1170+ "version": "1.32.0",
1171+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
1172+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
1173+ "cpu": [
1174+ "x64"
1175+ ],
1176+ "dev": true,
1177+ "libc": [
1178+ "musl"
1179+ ],
1180+ "license": "MPL-2.0",
1181+ "optional": true,
1182+ "os": [
1183+ "linux"
1184+ ],
1185+ "engines": {
1186+ "node": ">= 12.0.0"
1187+ },
1188+ "funding": {
1189+ "type": "opencollective",
1190+ "url": "https://opencollective.com/parcel"
1191+ }
1192+ },
1193+ "node_modules/lightningcss-win32-arm64-msvc": {
1194+ "version": "1.32.0",
1195+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
1196+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
1197+ "cpu": [
1198+ "arm64"
1199+ ],
1200+ "dev": true,
1201+ "license": "MPL-2.0",
1202+ "optional": true,
1203+ "os": [
1204+ "win32"
1205+ ],
1206+ "engines": {
1207+ "node": ">= 12.0.0"
1208+ },
1209+ "funding": {
1210+ "type": "opencollective",
1211+ "url": "https://opencollective.com/parcel"
1212+ }
1213+ },
1214+ "node_modules/lightningcss-win32-x64-msvc": {
1215+ "version": "1.32.0",
1216+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
1217+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
1218+ "cpu": [
1219+ "x64"
1220+ ],
1221+ "dev": true,
1222+ "license": "MPL-2.0",
1223+ "optional": true,
1224+ "os": [
1225+ "win32"
1226+ ],
1227+ "engines": {
1228+ "node": ">= 12.0.0"
1229+ },
1230+ "funding": {
1231+ "type": "opencollective",
1232+ "url": "https://opencollective.com/parcel"
1233+ }
1234+ },
1235+ "node_modules/meshoptimizer": {
1236+ "version": "1.1.1",
1237+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
1238+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
1239+ "dev": true,
1240+ "license": "MIT"
1241+ },
1242+ "node_modules/nanoid": {
1243+ "version": "3.3.15",
1244+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
1245+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
1246+ "dev": true,
1247+ "funding": [
1248+ {
1249+ "type": "github",
1250+ "url": "https://github.com/sponsors/ai"
1251+ }
1252+ ],
1253+ "license": "MIT",
1254+ "bin": {
1255+ "nanoid": "bin/nanoid.cjs"
1256+ },
1257+ "engines": {
1258+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1259+ }
1260+ },
1261+ "node_modules/numbl": {
1262+ "resolved": "../../numbl",
1263+ "link": true
1264+ },
1265+ "node_modules/oxlint": {
1266+ "version": "1.73.0",
1267+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
1268+ "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==",
1269+ "dev": true,
1270+ "license": "MIT",
1271+ "bin": {
1272+ "oxlint": "bin/oxlint"
1273+ },
1274+ "engines": {
1275+ "node": "^20.19.0 || >=22.12.0"
1276+ },
1277+ "funding": {
1278+ "url": "https://github.com/sponsors/Boshen"
1279+ },
1280+ "optionalDependencies": {
1281+ "@oxlint/binding-android-arm-eabi": "1.73.0",
1282+ "@oxlint/binding-android-arm64": "1.73.0",
1283+ "@oxlint/binding-darwin-arm64": "1.73.0",
1284+ "@oxlint/binding-darwin-x64": "1.73.0",
1285+ "@oxlint/binding-freebsd-x64": "1.73.0",
1286+ "@oxlint/binding-linux-arm-gnueabihf": "1.73.0",
1287+ "@oxlint/binding-linux-arm-musleabihf": "1.73.0",
1288+ "@oxlint/binding-linux-arm64-gnu": "1.73.0",
1289+ "@oxlint/binding-linux-arm64-musl": "1.73.0",
1290+ "@oxlint/binding-linux-ppc64-gnu": "1.73.0",
1291+ "@oxlint/binding-linux-riscv64-gnu": "1.73.0",
1292+ "@oxlint/binding-linux-riscv64-musl": "1.73.0",
1293+ "@oxlint/binding-linux-s390x-gnu": "1.73.0",
1294+ "@oxlint/binding-linux-x64-gnu": "1.73.0",
1295+ "@oxlint/binding-linux-x64-musl": "1.73.0",
1296+ "@oxlint/binding-openharmony-arm64": "1.73.0",
1297+ "@oxlint/binding-win32-arm64-msvc": "1.73.0",
1298+ "@oxlint/binding-win32-ia32-msvc": "1.73.0",
1299+ "@oxlint/binding-win32-x64-msvc": "1.73.0"
1300+ },
1301+ "peerDependencies": {
1302+ "oxlint-tsgolint": ">=0.24.0",
1303+ "vite-plus": "*"
1304+ },
1305+ "peerDependenciesMeta": {
1306+ "oxlint-tsgolint": {
1307+ "optional": true
1308+ },
1309+ "vite-plus": {
1310+ "optional": true
1311+ }
1312+ }
1313+ },
1314+ "node_modules/picocolors": {
1315+ "version": "1.1.1",
1316+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1317+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1318+ "dev": true,
1319+ "license": "ISC"
1320+ },
1321+ "node_modules/picomatch": {
1322+ "version": "4.0.5",
1323+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
1324+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
1325+ "dev": true,
1326+ "license": "MIT",
1327+ "engines": {
1328+ "node": ">=12"
1329+ },
1330+ "funding": {
1331+ "url": "https://github.com/sponsors/jonschlinkert"
1332+ }
1333+ },
1334+ "node_modules/postcss": {
1335+ "version": "8.5.16",
1336+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
1337+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
1338+ "dev": true,
1339+ "funding": [
1340+ {
1341+ "type": "opencollective",
1342+ "url": "https://opencollective.com/postcss/"
1343+ },
1344+ {
1345+ "type": "tidelift",
1346+ "url": "https://tidelift.com/funding/github/npm/postcss"
1347+ },
1348+ {
1349+ "type": "github",
1350+ "url": "https://github.com/sponsors/ai"
1351+ }
1352+ ],
1353+ "license": "MIT",
1354+ "dependencies": {
1355+ "nanoid": "^3.3.12",
1356+ "picocolors": "^1.1.1",
1357+ "source-map-js": "^1.2.1"
1358+ },
1359+ "engines": {
1360+ "node": "^10 || ^12 || >=14"
1361+ }
1362+ },
1363+ "node_modules/react": {
1364+ "version": "19.2.7",
1365+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
1366+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
1367+ "license": "MIT",
1368+ "engines": {
1369+ "node": ">=0.10.0"
1370+ }
1371+ },
1372+ "node_modules/react-dom": {
1373+ "version": "19.2.7",
1374+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
1375+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
1376+ "license": "MIT",
1377+ "dependencies": {
1378+ "scheduler": "^0.27.0"
1379+ },
1380+ "peerDependencies": {
1381+ "react": "^19.2.7"
1382+ }
1383+ },
1384+ "node_modules/rolldown": {
1385+ "version": "1.1.5",
1386+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
1387+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
1388+ "dev": true,
1389+ "license": "MIT",
1390+ "dependencies": {
1391+ "@oxc-project/types": "=0.139.0",
1392+ "@rolldown/pluginutils": "^1.0.0"
1393+ },
1394+ "bin": {
1395+ "rolldown": "bin/cli.mjs"
1396+ },
1397+ "engines": {
1398+ "node": "^20.19.0 || >=22.12.0"
1399+ },
1400+ "optionalDependencies": {
1401+ "@rolldown/binding-android-arm64": "1.1.5",
1402+ "@rolldown/binding-darwin-arm64": "1.1.5",
1403+ "@rolldown/binding-darwin-x64": "1.1.5",
1404+ "@rolldown/binding-freebsd-x64": "1.1.5",
1405+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
1406+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
1407+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
1408+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
1409+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
1410+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
1411+ "@rolldown/binding-linux-x64-musl": "1.1.5",
1412+ "@rolldown/binding-openharmony-arm64": "1.1.5",
1413+ "@rolldown/binding-wasm32-wasi": "1.1.5",
1414+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
1415+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
1416+ }
1417+ },
1418+ "node_modules/scheduler": {
1419+ "version": "0.27.0",
1420+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1421+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1422+ "license": "MIT"
1423+ },
1424+ "node_modules/source-map-js": {
1425+ "version": "1.2.1",
1426+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1427+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1428+ "dev": true,
1429+ "license": "BSD-3-Clause",
1430+ "engines": {
1431+ "node": ">=0.10.0"
1432+ }
1433+ },
1434+ "node_modules/three": {
1435+ "version": "0.185.1",
1436+ "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
1437+ "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==",
1438+ "license": "MIT"
1439+ },
1440+ "node_modules/tinyglobby": {
1441+ "version": "0.2.17",
1442+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1443+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1444+ "dev": true,
1445+ "license": "MIT",
1446+ "dependencies": {
1447+ "fdir": "^6.5.0",
1448+ "picomatch": "^4.0.4"
1449+ },
1450+ "engines": {
1451+ "node": ">=12.0.0"
1452+ },
1453+ "funding": {
1454+ "url": "https://github.com/sponsors/SuperchupuDev"
1455+ }
1456+ },
1457+ "node_modules/tslib": {
1458+ "version": "2.8.1",
1459+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1460+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1461+ "dev": true,
1462+ "license": "0BSD",
1463+ "optional": true
1464+ },
1465+ "node_modules/typescript": {
1466+ "version": "6.0.3",
1467+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
1468+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
1469+ "dev": true,
1470+ "license": "Apache-2.0",
1471+ "bin": {
1472+ "tsc": "bin/tsc",
1473+ "tsserver": "bin/tsserver"
1474+ },
1475+ "engines": {
1476+ "node": ">=14.17"
1477+ }
1478+ },
1479+ "node_modules/undici-types": {
1480+ "version": "7.18.2",
1481+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
1482+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
1483+ "dev": true,
1484+ "license": "MIT"
1485+ },
1486+ "node_modules/vite": {
1487+ "version": "8.1.3",
1488+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
1489+ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
1490+ "dev": true,
1491+ "license": "MIT",
1492+ "dependencies": {
1493+ "lightningcss": "^1.32.0",
1494+ "picomatch": "^4.0.4",
1495+ "postcss": "^8.5.16",
1496+ "rolldown": "~1.1.3",
1497+ "tinyglobby": "^0.2.17"
1498+ },
1499+ "bin": {
1500+ "vite": "bin/vite.js"
1501+ },
1502+ "engines": {
1503+ "node": "^20.19.0 || >=22.12.0"
1504+ },
1505+ "funding": {
1506+ "url": "https://github.com/vitejs/vite?sponsor=1"
1507+ },
1508+ "optionalDependencies": {
1509+ "fsevents": "~2.3.3"
1510+ },
1511+ "peerDependencies": {
1512+ "@types/node": "^20.19.0 || >=22.12.0",
1513+ "@vitejs/devtools": "^0.3.0",
1514+ "esbuild": "^0.27.0 || ^0.28.0",
1515+ "jiti": ">=1.21.0",
1516+ "less": "^4.0.0",
1517+ "sass": "^1.70.0",
1518+ "sass-embedded": "^1.70.0",
1519+ "stylus": ">=0.54.8",
1520+ "sugarss": "^5.0.0",
1521+ "terser": "^5.16.0",
1522+ "tsx": "^4.8.1",
1523+ "yaml": "^2.4.2"
1524+ },
1525+ "peerDependenciesMeta": {
1526+ "@types/node": {
1527+ "optional": true
1528+ },
1529+ "@vitejs/devtools": {
1530+ "optional": true
1531+ },
1532+ "esbuild": {
1533+ "optional": true
1534+ },
1535+ "jiti": {
1536+ "optional": true
1537+ },
1538+ "less": {
1539+ "optional": true
1540+ },
1541+ "sass": {
1542+ "optional": true
1543+ },
1544+ "sass-embedded": {
1545+ "optional": true
1546+ },
1547+ "stylus": {
1548+ "optional": true
1549+ },
1550+ "sugarss": {
1551+ "optional": true
1552+ },
1553+ "terser": {
1554+ "optional": true
1555+ },
1556+ "tsx": {
1557+ "optional": true
1558+ },
1559+ "yaml": {
1560+ "optional": true
1561+ }
1562+ }
1563+ }
1564+ }
1565+}
package.jsonadded+30−0View file
@@ -0,0 +1,30 @@
1+{
2+ "name": "mesh-pde-solver",
3+ "private": true,
4+ "version": "0.0.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "lint": "oxlint",
10+ "preview": "vite preview",
11+ "engine-test": "node scripts/engine-test.mjs"
12+ },
13+ "dependencies": {
14+ "fflate": "^0.8.2",
15+ "numbl": "^0.4.9",
16+ "react": "^19.2.7",
17+ "react-dom": "^19.2.7",
18+ "three": "^0.185.1"
19+ },
20+ "devDependencies": {
21+ "@types/node": "^24.13.2",
22+ "@types/react": "^19.2.17",
23+ "@types/react-dom": "^19.2.3",
24+ "@types/three": "^0.185.0",
25+ "@vitejs/plugin-react": "^6.0.3",
26+ "oxlint": "^1.71.0",
27+ "typescript": "~6.0.2",
28+ "vite": "^8.1.1"
29+ }
30+}
public/favicon.svgadded+11−0View file
@@ -0,0 +1,11 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2+ <rect x="2" y="2" width="28" height="28" rx="6" fill="#1667c2"/>
3+ <g stroke="#ffffff" stroke-width="1.6" fill="none" opacity="0.9">
4+ <path d="M7 11 Q16 5 25 11"/>
5+ <path d="M7 16 Q16 10 25 16"/>
6+ <path d="M7 21 Q16 15 25 21"/>
7+ <path d="M10 8.6 V 23.4"/>
8+ <path d="M16 7.2 V 22"/>
9+ <path d="M22 8.6 V 23.4"/>
10+ </g>
11+</svg>
public/samples/sphere.mshadded+443−0View file
@@ -0,0 +1,443 @@
1+$MeshFormat
2+2.2 0 8
3+$EndMeshFormat
4+$Nodes
5+218
6+1 -0.5773502691896258 -0.5773502691896258 0.5773502691896258
7+2 -0.4264014327112209 -0.6396021490668312 0.6396021490668312
8+3 -0.485071250072666 -0.485071250072666 0.7276068751089989
9+4 -0.6396021490668312 -0.4264014327112209 0.6396021490668312
10+5 -0.5345224838248488 -0.2672612419124244 0.8017837257372731
11+6 -0.6882472016116853 -0.2294157338705618 0.6882472016116853
12+7 -0.5547001962252291 0 0.8320502943378437
13+8 -0.7071067811865475 0 0.7071067811865475
14+9 -0.5345224838248489 0.2672612419124243 0.8017837257372732
15+10 -0.6882472016116853 0.2294157338705617 0.6882472016116853
16+11 -0.485071250072666 0.485071250072666 0.7276068751089989
17+12 -0.6396021490668312 0.4264014327112209 0.6396021490668312
18+13 -0.4264014327112209 0.6396021490668312 0.6396021490668312
19+14 -0.5773502691896258 0.5773502691896258 0.5773502691896258
20+15 -0.2294157338705618 -0.6882472016116853 0.6882472016116853
21+16 -0.2672612419124244 -0.5345224838248488 0.8017837257372731
22+17 -0.3015113445777636 -0.3015113445777636 0.9045340337332908
23+18 -0.3162277660168379 0 0.9486832980505138
24+19 -0.3015113445777636 0.3015113445777635 0.9045340337332908
25+20 -0.2672612419124244 0.5345224838248488 0.8017837257372731
26+21 -0.2294157338705618 0.6882472016116853 0.6882472016116853
27+22 0 -0.7071067811865475 0.7071067811865475
28+23 0 -0.5547001962252291 0.8320502943378437
29+24 0 -0.3162277660168379 0.9486832980505138
30+25 0 0 1
31+26 0 0.3162277660168378 0.9486832980505138
32+27 0 0.5547001962252291 0.8320502943378437
33+28 0 0.7071067811865475 0.7071067811865475
34+29 0.2294157338705617 -0.6882472016116853 0.6882472016116853
35+30 0.2672612419124243 -0.5345224838248489 0.8017837257372732
36+31 0.3015113445777635 -0.3015113445777636 0.9045340337332908
37+32 0.3162277660168378 0 0.9486832980505138
38+33 0.3015113445777636 0.3015113445777636 0.9045340337332909
39+34 0.2672612419124243 0.5345224838248489 0.8017837257372732
40+35 0.2294157338705617 0.6882472016116853 0.6882472016116853
41+36 0.4264014327112209 -0.6396021490668312 0.6396021490668312
42+37 0.485071250072666 -0.485071250072666 0.7276068751089989
43+38 0.5345224838248488 -0.2672612419124244 0.8017837257372731
44+39 0.5547001962252291 0 0.8320502943378437
45+40 0.5345224838248489 0.2672612419124243 0.8017837257372732
46+41 0.485071250072666 0.485071250072666 0.7276068751089989
47+42 0.4264014327112209 0.6396021490668312 0.6396021490668312
48+43 0.5773502691896258 -0.5773502691896258 0.5773502691896258
49+44 0.6396021490668312 -0.4264014327112209 0.6396021490668312
50+45 0.6882472016116853 -0.2294157338705618 0.6882472016116853
51+46 0.7071067811865475 0 0.7071067811865475
52+47 0.6882472016116853 0.2294157338705617 0.6882472016116853
53+48 0.6396021490668312 0.4264014327112209 0.6396021490668312
54+49 0.5773502691896258 0.5773502691896258 0.5773502691896258
55+50 -0.5773502691896258 0.5773502691896258 -0.5773502691896258
56+51 -0.4264014327112209 0.6396021490668312 -0.6396021490668312
57+52 -0.485071250072666 0.485071250072666 -0.7276068751089989
58+53 -0.6396021490668312 0.4264014327112209 -0.6396021490668312
59+54 -0.5345224838248488 0.2672612419124244 -0.8017837257372731
60+55 -0.6882472016116853 0.2294157338705618 -0.6882472016116853
61+56 -0.5547001962252291 0 -0.8320502943378437
62+57 -0.7071067811865475 0 -0.7071067811865475
63+58 -0.5345224838248489 -0.2672612419124243 -0.8017837257372732
64+59 -0.6882472016116853 -0.2294157338705617 -0.6882472016116853
65+60 -0.485071250072666 -0.485071250072666 -0.7276068751089989
66+61 -0.6396021490668312 -0.4264014327112209 -0.6396021490668312
67+62 -0.4264014327112209 -0.6396021490668312 -0.6396021490668312
68+63 -0.5773502691896258 -0.5773502691896258 -0.5773502691896258
69+64 -0.2294157338705618 0.6882472016116853 -0.6882472016116853
70+65 -0.2672612419124244 0.5345224838248488 -0.8017837257372731
71+66 -0.3015113445777636 0.3015113445777636 -0.9045340337332908
72+67 -0.3162277660168379 0 -0.9486832980505138
73+68 -0.3015113445777636 -0.3015113445777635 -0.9045340337332908
74+69 -0.2672612419124244 -0.5345224838248488 -0.8017837257372731
75+70 -0.2294157338705618 -0.6882472016116853 -0.6882472016116853
76+71 0 0.7071067811865475 -0.7071067811865475
77+72 0 0.5547001962252291 -0.8320502943378437
78+73 0 0.3162277660168379 -0.9486832980505138
79+74 0 0 -1
80+75 0 -0.3162277660168378 -0.9486832980505138
81+76 0 -0.5547001962252291 -0.8320502943378437
82+77 0 -0.7071067811865475 -0.7071067811865475
83+78 0.2294157338705617 0.6882472016116853 -0.6882472016116853
84+79 0.2672612419124243 0.5345224838248489 -0.8017837257372732
85+80 0.3015113445777635 0.3015113445777636 -0.9045340337332908
86+81 0.3162277660168378 0 -0.9486832980505138
87+82 0.3015113445777636 -0.3015113445777636 -0.9045340337332909
88+83 0.2672612419124243 -0.5345224838248489 -0.8017837257372732
89+84 0.2294157338705617 -0.6882472016116853 -0.6882472016116853
90+85 0.4264014327112209 0.6396021490668312 -0.6396021490668312
91+86 0.485071250072666 0.485071250072666 -0.7276068751089989
92+87 0.5345224838248488 0.2672612419124244 -0.8017837257372731
93+88 0.5547001962252291 0 -0.8320502943378437
94+89 0.5345224838248489 -0.2672612419124243 -0.8017837257372732
95+90 0.485071250072666 -0.485071250072666 -0.7276068751089989
96+91 0.4264014327112209 -0.6396021490668312 -0.6396021490668312
97+92 0.5773502691896258 0.5773502691896258 -0.5773502691896258
98+93 0.6396021490668312 0.4264014327112209 -0.6396021490668312
99+94 0.6882472016116853 0.2294157338705618 -0.6882472016116853
100+95 0.7071067811865475 0 -0.7071067811865475
101+96 0.6882472016116853 -0.2294157338705617 -0.6882472016116853
102+97 0.6396021490668312 -0.4264014327112209 -0.6396021490668312
103+98 0.5773502691896258 -0.5773502691896258 -0.5773502691896258
104+99 -0.7276068751089988 -0.4850712500726659 -0.4850712500726659
105+100 -0.6396021490668312 -0.6396021490668312 -0.4264014327112209
106+101 -0.8017837257372731 -0.5345224838248488 -0.2672612419124244
107+102 -0.6882472016116853 -0.6882472016116853 -0.2294157338705618
108+103 -0.8320502943378437 -0.5547001962252291 0
109+104 -0.7071067811865475 -0.7071067811865475 0
110+105 -0.8017837257372731 -0.5345224838248488 0.2672612419124243
111+106 -0.6882472016116853 -0.6882472016116853 0.2294157338705617
112+107 -0.7276068751089988 -0.4850712500726659 0.4850712500726659
113+108 -0.6396021490668312 -0.6396021490668312 0.4264014327112209
114+109 -0.8017837257372731 -0.2672612419124244 -0.5345224838248488
115+110 -0.9045340337332908 -0.3015113445777636 -0.3015113445777636
116+111 -0.9486832980505138 -0.3162277660168379 0
117+112 -0.9045340337332908 -0.3015113445777636 0.3015113445777635
118+113 -0.8017837257372731 -0.2672612419124244 0.5345224838248488
119+114 -0.8320502943378437 0 -0.5547001962252291
120+115 -0.9486832980505138 0 -0.3162277660168379
121+116 -1 0 0
122+117 -0.9486832980505138 0 0.3162277660168378
123+118 -0.8320502943378437 0 0.5547001962252291
124+119 -0.8017837257372731 0.2672612419124243 -0.5345224838248488
125+120 -0.9045340337332908 0.3015113445777635 -0.3015113445777636
126+121 -0.9486832980505138 0.3162277660168378 0
127+122 -0.9045340337332908 0.3015113445777635 0.3015113445777635
128+123 -0.8017837257372731 0.2672612419124243 0.5345224838248488
129+124 -0.7276068751089988 0.4850712500726659 -0.4850712500726659
130+125 -0.8017837257372731 0.5345224838248488 -0.2672612419124244
131+126 -0.8320502943378437 0.5547001962252291 0
132+127 -0.8017837257372731 0.5345224838248488 0.2672612419124243
133+128 -0.7276068751089988 0.4850712500726659 0.4850712500726659
134+129 -0.6396021490668312 0.6396021490668312 -0.4264014327112209
135+130 -0.6882472016116853 0.6882472016116853 -0.2294157338705618
136+131 -0.7071067811865475 0.7071067811865475 0
137+132 -0.6882472016116853 0.6882472016116853 0.2294157338705617
138+133 -0.6396021490668312 0.6396021490668312 0.4264014327112209
139+134 0.6396021490668312 -0.6396021490668312 -0.4264014327112209
140+135 0.7276068751089988 -0.4850712500726659 -0.4850712500726659
141+136 0.8017837257372731 -0.2672612419124244 -0.5345224838248488
142+137 0.8320502943378437 0 -0.5547001962252291
143+138 0.8017837257372731 0.2672612419124243 -0.5345224838248488
144+139 0.7276068751089988 0.4850712500726659 -0.4850712500726659
145+140 0.6396021490668312 0.6396021490668312 -0.4264014327112209
146+141 0.6882472016116853 -0.6882472016116853 -0.2294157338705618
147+142 0.8017837257372731 -0.5345224838248488 -0.2672612419124244
148+143 0.9045340337332908 -0.3015113445777636 -0.3015113445777636
149+144 0.9486832980505138 0 -0.3162277660168379
150+145 0.9045340337332908 0.3015113445777635 -0.3015113445777636
151+146 0.8017837257372731 0.5345224838248488 -0.2672612419124244
152+147 0.6882472016116853 0.6882472016116853 -0.2294157338705618
153+148 0.7071067811865475 -0.7071067811865475 0
154+149 0.8320502943378437 -0.5547001962252291 0
155+150 0.9486832980505138 -0.3162277660168379 0
156+151 1 0 0
157+152 0.9486832980505138 0.3162277660168378 0
158+153 0.8320502943378437 0.5547001962252291 0
159+154 0.7071067811865475 0.7071067811865475 0
160+155 0.6882472016116853 -0.6882472016116853 0.2294157338705617
161+156 0.8017837257372731 -0.5345224838248488 0.2672612419124243
162+157 0.9045340337332908 -0.3015113445777636 0.3015113445777635
163+158 0.9486832980505138 0 0.3162277660168378
164+159 0.9045340337332908 0.3015113445777635 0.3015113445777635
165+160 0.8017837257372731 0.5345224838248488 0.2672612419124243
166+161 0.6882472016116853 0.6882472016116853 0.2294157338705617
167+162 0.6396021490668312 -0.6396021490668312 0.4264014327112209
168+163 0.7276068751089988 -0.4850712500726659 0.4850712500726659
169+164 0.8017837257372731 -0.2672612419124244 0.5345224838248488
170+165 0.8320502943378437 0 0.5547001962252291
171+166 0.8017837257372731 0.2672612419124243 0.5345224838248488
172+167 0.7276068751089988 0.4850712500726659 0.4850712500726659
173+168 0.6396021490668312 0.6396021490668312 0.4264014327112209
174+169 -0.4850712500726659 -0.7276068751089988 -0.4850712500726659
175+170 -0.2672612419124244 -0.8017837257372731 -0.5345224838248488
176+171 0 -0.8320502943378437 -0.5547001962252291
177+172 0.2672612419124243 -0.8017837257372731 -0.5345224838248488
178+173 0.4850712500726659 -0.7276068751089988 -0.4850712500726659
179+174 -0.5345224838248488 -0.8017837257372731 -0.2672612419124244
180+175 -0.3015113445777636 -0.9045340337332908 -0.3015113445777636
181+176 0 -0.9486832980505138 -0.3162277660168379
182+177 0.3015113445777635 -0.9045340337332908 -0.3015113445777636
183+178 0.5345224838248488 -0.8017837257372731 -0.2672612419124244
184+179 -0.5547001962252291 -0.8320502943378437 0
185+180 -0.3162277660168379 -0.9486832980505138 0
186+181 0 -1 0
187+182 0.3162277660168378 -0.9486832980505138 0
188+183 0.5547001962252291 -0.8320502943378437 0
189+184 -0.5345224838248488 -0.8017837257372731 0.2672612419124243
190+185 -0.3015113445777636 -0.9045340337332908 0.3015113445777635
191+186 0 -0.9486832980505138 0.3162277660168378
192+187 0.3015113445777635 -0.9045340337332908 0.3015113445777635
193+188 0.5345224838248488 -0.8017837257372731 0.2672612419124243
194+189 -0.4850712500726659 -0.7276068751089988 0.4850712500726659
195+190 -0.2672612419124244 -0.8017837257372731 0.5345224838248488
196+191 0 -0.8320502943378437 0.5547001962252291
197+192 0.2672612419124243 -0.8017837257372731 0.5345224838248488
198+193 0.4850712500726659 -0.7276068751089988 0.4850712500726659
199+194 -0.4850712500726659 0.7276068751089988 -0.4850712500726659
200+195 -0.5345224838248488 0.8017837257372731 -0.2672612419124244
201+196 -0.5547001962252291 0.8320502943378437 0
202+197 -0.5345224838248488 0.8017837257372731 0.2672612419124243
203+198 -0.4850712500726659 0.7276068751089988 0.4850712500726659
204+199 -0.2672612419124244 0.8017837257372731 -0.5345224838248488
205+200 -0.3015113445777636 0.9045340337332908 -0.3015113445777636
206+201 -0.3162277660168379 0.9486832980505138 0
207+202 -0.3015113445777636 0.9045340337332908 0.3015113445777635
208+203 -0.2672612419124244 0.8017837257372731 0.5345224838248488
209+204 0 0.8320502943378437 -0.5547001962252291
210+205 0 0.9486832980505138 -0.3162277660168379
211+206 0 1 0
212+207 0 0.9486832980505138 0.3162277660168378
213+208 0 0.8320502943378437 0.5547001962252291
214+209 0.2672612419124243 0.8017837257372731 -0.5345224838248488
215+210 0.3015113445777635 0.9045340337332908 -0.3015113445777636
216+211 0.3162277660168378 0.9486832980505138 0
217+212 0.3015113445777635 0.9045340337332908 0.3015113445777635
218+213 0.2672612419124243 0.8017837257372731 0.5345224838248488
219+214 0.4850712500726659 0.7276068751089988 -0.4850712500726659
220+215 0.5345224838248488 0.8017837257372731 -0.2672612419124244
221+216 0.5547001962252291 0.8320502943378437 0
222+217 0.5345224838248488 0.8017837257372731 0.2672612419124243
223+218 0.4850712500726659 0.7276068751089988 0.4850712500726659
224+$EndNodes
225+$Elements
226+216
227+1 3 2 1 1 1 2 3 4
228+2 3 2 1 1 4 3 5 6
229+3 3 2 1 1 6 5 7 8
230+4 3 2 1 1 8 7 9 10
231+5 3 2 1 1 10 9 11 12
232+6 3 2 1 1 12 11 13 14
233+7 3 2 1 1 2 15 16 3
234+8 3 2 1 1 3 16 17 5
235+9 3 2 1 1 5 17 18 7
236+10 3 2 1 1 7 18 19 9
237+11 3 2 1 1 9 19 20 11
238+12 3 2 1 1 11 20 21 13
239+13 3 2 1 1 15 22 23 16
240+14 3 2 1 1 16 23 24 17
241+15 3 2 1 1 17 24 25 18
242+16 3 2 1 1 18 25 26 19
243+17 3 2 1 1 19 26 27 20
244+18 3 2 1 1 20 27 28 21
245+19 3 2 1 1 22 29 30 23
246+20 3 2 1 1 23 30 31 24
247+21 3 2 1 1 24 31 32 25
248+22 3 2 1 1 25 32 33 26
249+23 3 2 1 1 26 33 34 27
250+24 3 2 1 1 27 34 35 28
251+25 3 2 1 1 29 36 37 30
252+26 3 2 1 1 30 37 38 31
253+27 3 2 1 1 31 38 39 32
254+28 3 2 1 1 32 39 40 33
255+29 3 2 1 1 33 40 41 34
256+30 3 2 1 1 34 41 42 35
257+31 3 2 1 1 36 43 44 37
258+32 3 2 1 1 37 44 45 38
259+33 3 2 1 1 38 45 46 39
260+34 3 2 1 1 39 46 47 40
261+35 3 2 1 1 40 47 48 41
262+36 3 2 1 1 41 48 49 42
263+37 3 2 1 1 50 51 52 53
264+38 3 2 1 1 53 52 54 55
265+39 3 2 1 1 55 54 56 57
266+40 3 2 1 1 57 56 58 59
267+41 3 2 1 1 59 58 60 61
268+42 3 2 1 1 61 60 62 63
269+43 3 2 1 1 51 64 65 52
270+44 3 2 1 1 52 65 66 54
271+45 3 2 1 1 54 66 67 56
272+46 3 2 1 1 56 67 68 58
273+47 3 2 1 1 58 68 69 60
274+48 3 2 1 1 60 69 70 62
275+49 3 2 1 1 64 71 72 65
276+50 3 2 1 1 65 72 73 66
277+51 3 2 1 1 66 73 74 67
278+52 3 2 1 1 67 74 75 68
279+53 3 2 1 1 68 75 76 69
280+54 3 2 1 1 69 76 77 70
281+55 3 2 1 1 71 78 79 72
282+56 3 2 1 1 72 79 80 73
283+57 3 2 1 1 73 80 81 74
284+58 3 2 1 1 74 81 82 75
285+59 3 2 1 1 75 82 83 76
286+60 3 2 1 1 76 83 84 77
287+61 3 2 1 1 78 85 86 79
288+62 3 2 1 1 79 86 87 80
289+63 3 2 1 1 80 87 88 81
290+64 3 2 1 1 81 88 89 82
291+65 3 2 1 1 82 89 90 83
292+66 3 2 1 1 83 90 91 84
293+67 3 2 1 1 85 92 93 86
294+68 3 2 1 1 86 93 94 87
295+69 3 2 1 1 87 94 95 88
296+70 3 2 1 1 88 95 96 89
297+71 3 2 1 1 89 96 97 90
298+72 3 2 1 1 90 97 98 91
299+73 3 2 1 1 63 61 99 100
300+74 3 2 1 1 100 99 101 102
301+75 3 2 1 1 102 101 103 104
302+76 3 2 1 1 104 103 105 106
303+77 3 2 1 1 106 105 107 108
304+78 3 2 1 1 108 107 4 1
305+79 3 2 1 1 61 59 109 99
306+80 3 2 1 1 99 109 110 101
307+81 3 2 1 1 101 110 111 103
308+82 3 2 1 1 103 111 112 105
309+83 3 2 1 1 105 112 113 107
310+84 3 2 1 1 107 113 6 4
311+85 3 2 1 1 59 57 114 109
312+86 3 2 1 1 109 114 115 110
313+87 3 2 1 1 110 115 116 111
314+88 3 2 1 1 111 116 117 112
315+89 3 2 1 1 112 117 118 113
316+90 3 2 1 1 113 118 8 6
317+91 3 2 1 1 57 55 119 114
318+92 3 2 1 1 114 119 120 115
319+93 3 2 1 1 115 120 121 116
320+94 3 2 1 1 116 121 122 117
321+95 3 2 1 1 117 122 123 118
322+96 3 2 1 1 118 123 10 8
323+97 3 2 1 1 55 53 124 119
324+98 3 2 1 1 119 124 125 120
325+99 3 2 1 1 120 125 126 121
326+100 3 2 1 1 121 126 127 122
327+101 3 2 1 1 122 127 128 123
328+102 3 2 1 1 123 128 12 10
329+103 3 2 1 1 53 50 129 124
330+104 3 2 1 1 124 129 130 125
331+105 3 2 1 1 125 130 131 126
332+106 3 2 1 1 126 131 132 127
333+107 3 2 1 1 127 132 133 128
334+108 3 2 1 1 128 133 14 12
335+109 3 2 1 1 98 134 135 97
336+110 3 2 1 1 97 135 136 96
337+111 3 2 1 1 96 136 137 95
338+112 3 2 1 1 95 137 138 94
339+113 3 2 1 1 94 138 139 93
340+114 3 2 1 1 93 139 140 92
341+115 3 2 1 1 134 141 142 135
342+116 3 2 1 1 135 142 143 136
343+117 3 2 1 1 136 143 144 137
344+118 3 2 1 1 137 144 145 138
345+119 3 2 1 1 138 145 146 139
346+120 3 2 1 1 139 146 147 140
347+121 3 2 1 1 141 148 149 142
348+122 3 2 1 1 142 149 150 143
349+123 3 2 1 1 143 150 151 144
350+124 3 2 1 1 144 151 152 145
351+125 3 2 1 1 145 152 153 146
352+126 3 2 1 1 146 153 154 147
353+127 3 2 1 1 148 155 156 149
354+128 3 2 1 1 149 156 157 150
355+129 3 2 1 1 150 157 158 151
356+130 3 2 1 1 151 158 159 152
357+131 3 2 1 1 152 159 160 153
358+132 3 2 1 1 153 160 161 154
359+133 3 2 1 1 155 162 163 156
360+134 3 2 1 1 156 163 164 157
361+135 3 2 1 1 157 164 165 158
362+136 3 2 1 1 158 165 166 159
363+137 3 2 1 1 159 166 167 160
364+138 3 2 1 1 160 167 168 161
365+139 3 2 1 1 162 43 44 163
366+140 3 2 1 1 163 44 45 164
367+141 3 2 1 1 164 45 46 165
368+142 3 2 1 1 165 46 47 166
369+143 3 2 1 1 166 47 48 167
370+144 3 2 1 1 167 48 49 168
371+145 3 2 1 1 63 100 169 62
372+146 3 2 1 1 62 169 170 70
373+147 3 2 1 1 70 170 171 77
374+148 3 2 1 1 77 171 172 84
375+149 3 2 1 1 84 172 173 91
376+150 3 2 1 1 91 173 134 98
377+151 3 2 1 1 100 102 174 169
378+152 3 2 1 1 169 174 175 170
379+153 3 2 1 1 170 175 176 171
380+154 3 2 1 1 171 176 177 172
381+155 3 2 1 1 172 177 178 173
382+156 3 2 1 1 173 178 141 134
383+157 3 2 1 1 102 104 179 174
384+158 3 2 1 1 174 179 180 175
385+159 3 2 1 1 175 180 181 176
386+160 3 2 1 1 176 181 182 177
387+161 3 2 1 1 177 182 183 178
388+162 3 2 1 1 178 183 148 141
389+163 3 2 1 1 104 106 184 179
390+164 3 2 1 1 179 184 185 180
391+165 3 2 1 1 180 185 186 181
392+166 3 2 1 1 181 186 187 182
393+167 3 2 1 1 182 187 188 183
394+168 3 2 1 1 183 188 155 148
395+169 3 2 1 1 106 108 189 184
396+170 3 2 1 1 184 189 190 185
397+171 3 2 1 1 185 190 191 186
398+172 3 2 1 1 186 191 192 187
399+173 3 2 1 1 187 192 193 188
400+174 3 2 1 1 188 193 162 155
401+175 3 2 1 1 108 1 2 189
402+176 3 2 1 1 189 2 15 190
403+177 3 2 1 1 190 15 22 191
404+178 3 2 1 1 191 22 29 192
405+179 3 2 1 1 192 29 36 193
406+180 3 2 1 1 193 36 43 162
407+181 3 2 1 1 50 51 194 129
408+182 3 2 1 1 129 194 195 130
409+183 3 2 1 1 130 195 196 131
410+184 3 2 1 1 131 196 197 132
411+185 3 2 1 1 132 197 198 133
412+186 3 2 1 1 133 198 13 14
413+187 3 2 1 1 51 64 199 194
414+188 3 2 1 1 194 199 200 195
415+189 3 2 1 1 195 200 201 196
416+190 3 2 1 1 196 201 202 197
417+191 3 2 1 1 197 202 203 198
418+192 3 2 1 1 198 203 21 13
419+193 3 2 1 1 64 71 204 199
420+194 3 2 1 1 199 204 205 200
421+195 3 2 1 1 200 205 206 201
422+196 3 2 1 1 201 206 207 202
423+197 3 2 1 1 202 207 208 203
424+198 3 2 1 1 203 208 28 21
425+199 3 2 1 1 71 78 209 204
426+200 3 2 1 1 204 209 210 205
427+201 3 2 1 1 205 210 211 206
428+202 3 2 1 1 206 211 212 207
429+203 3 2 1 1 207 212 213 208
430+204 3 2 1 1 208 213 35 28
431+205 3 2 1 1 78 85 214 209
432+206 3 2 1 1 209 214 215 210
433+207 3 2 1 1 210 215 216 211
434+208 3 2 1 1 211 216 217 212
435+209 3 2 1 1 212 217 218 213
436+210 3 2 1 1 213 218 42 35
437+211 3 2 1 1 85 92 140 214
438+212 3 2 1 1 214 140 147 215
439+213 3 2 1 1 215 147 154 216
440+214 3 2 1 1 216 154 161 217
441+215 3 2 1 1 217 161 168 218
442+216 3 2 1 1 218 168 49 42
443+$EndElements
public/samples/torus.mshadded+585−0View file
@@ -0,0 +1,585 @@
1+$MeshFormat
2+2.2 0 8
3+$EndMeshFormat
4+$Nodes
5+288
6+1 1.4 0 0
7+2 1.346410161513776 0 0.2
8+3 1.2 0 0.3464101615137755
9+4 1 0 0.4
10+5 0.8 0 0.3464101615137755
11+6 0.6535898384862244 0 0.2
12+7 0.6 0 4.898587196589413e-17
13+8 0.6535898384862244 0 -0.1999999999999999
14+9 0.7999999999999998 0 -0.3464101615137753
15+10 0.9999999999999999 0 -0.4
16+11 1.2 0 -0.3464101615137755
17+12 1.346410161513775 0 -0.2000000000000002
18+13 1.352296156804696 0.362346663143529 0
19+14 1.300532347784192 0.3484765923193261 0.2
20+15 1.159110991546882 0.3105828541230249 0.3464101615137755
21+16 0.9659258262890683 0.2588190451025207 0.4
22+17 0.7727406610312547 0.2070552360820166 0.3464101615137755
23+18 0.631319304793945 0.1691614978857154 0.2
24+19 0.579555495773441 0.1552914270615124 4.898587196589413e-17
25+20 0.631319304793945 0.1691614978857154 -0.1999999999999999
26+21 0.7727406610312545 0.2070552360820165 -0.3464101615137753
27+22 0.9659258262890682 0.2588190451025207 -0.4
28+23 1.159110991546882 0.3105828541230249 -0.3464101615137755
29+24 1.300532347784191 0.348476592319326 -0.2000000000000002
30+25 1.212435565298214 0.6999999999999998 0
31+26 1.166025403784439 0.6732050807568877 0.2
32+27 1.039230484541326 0.6 0.3464101615137755
33+28 0.8660254037844387 0.4999999999999999 0.4
34+29 0.692820323027551 0.4 0.3464101615137755
35+30 0.5660254037844386 0.3267949192431122 0.2
36+31 0.5196152422706632 0.2999999999999999 4.898587196589413e-17
37+32 0.5660254037844386 0.3267949192431122 -0.1999999999999999
38+33 0.6928203230275508 0.3999999999999999 -0.3464101615137753
39+34 0.8660254037844386 0.4999999999999999 -0.4
40+35 1.039230484541326 0.6 -0.3464101615137755
41+36 1.166025403784439 0.6732050807568876 -0.2000000000000002
42+37 0.9899494936611666 0.9899494936611664 0
43+38 0.9520557554648654 0.9520557554648653 0.2
44+39 0.8485281374238572 0.8485281374238571 0.3464101615137755
45+40 0.7071067811865476 0.7071067811865475 0.4
46+41 0.5656854249492381 0.565685424949238 0.3464101615137755
47+42 0.4621578069082297 0.4621578069082296 0.2
48+43 0.4242640687119285 0.4242640687119285 4.898587196589413e-17
49+44 0.4621578069082297 0.4621578069082296 -0.1999999999999999
50+45 0.5656854249492379 0.5656854249492378 -0.3464101615137753
51+46 0.7071067811865475 0.7071067811865474 -0.4
52+47 0.8485281374238572 0.8485281374238571 -0.3464101615137755
53+48 0.9520557554648653 0.9520557554648652 -0.2000000000000002
54+49 0.7000000000000001 1.212435565298214 0
55+50 0.6732050807568879 1.166025403784439 0.2
56+51 0.6000000000000002 1.039230484541326 0.3464101615137755
57+52 0.5000000000000001 0.8660254037844386 0.4
58+53 0.4000000000000001 0.6928203230275509 0.3464101615137755
59+54 0.3267949192431123 0.5660254037844386 0.2
60+55 0.3 0.5196152422706631 4.898587196589413e-17
61+56 0.3267949192431123 0.5660254037844386 -0.1999999999999999
62+57 0.4 0.6928203230275507 -0.3464101615137753
63+58 0.5 0.8660254037844385 -0.4
64+59 0.6000000000000002 1.039230484541326 -0.3464101615137755
65+60 0.6732050807568878 1.166025403784438 -0.2000000000000002
66+61 0.362346663143529 1.352296156804696 0
67+62 0.3484765923193261 1.300532347784192 0.2
68+63 0.3105828541230249 1.159110991546882 0.3464101615137755
69+64 0.2588190451025207 0.9659258262890683 0.4
70+65 0.2070552360820166 0.7727406610312547 0.3464101615137755
71+66 0.1691614978857154 0.631319304793945 0.2
72+67 0.1552914270615124 0.579555495773441 4.898587196589413e-17
73+68 0.1691614978857154 0.631319304793945 -0.1999999999999999
74+69 0.2070552360820165 0.7727406610312545 -0.3464101615137753
75+70 0.2588190451025207 0.9659258262890682 -0.4
76+71 0.3105828541230249 1.159110991546882 -0.3464101615137755
77+72 0.348476592319326 1.300532347784191 -0.2000000000000002
78+73 8.572527594031472e-17 1.4 0
79+74 8.244384473186581e-17 1.346410161513776 0.2
80+75 7.34788079488412e-17 1.2 0.3464101615137755
81+76 6.123233995736766e-17 1 0.4
82+77 4.898587196589413e-17 0.8 0.3464101615137755
83+78 4.002083518286952e-17 0.6535898384862244 0.2
84+79 3.673940397442059e-17 0.6 4.898587196589413e-17
85+80 4.002083518286952e-17 0.6535898384862244 -0.1999999999999999
86+81 4.898587196589412e-17 0.7999999999999998 -0.3464101615137753
87+82 6.123233995736765e-17 0.9999999999999999 -0.4
88+83 7.34788079488412e-17 1.2 -0.3464101615137755
89+84 8.24438447318658e-17 1.346410161513775 -0.2000000000000002
90+85 -0.3623466631435289 1.352296156804696 0
91+86 -0.348476592319326 1.300532347784192 0.2
92+87 -0.3105828541230248 1.159110991546882 0.3464101615137755
93+88 -0.2588190451025206 0.9659258262890683 0.4
94+89 -0.2070552360820165 0.7727406610312547 0.3464101615137755
95+90 -0.1691614978857153 0.631319304793945 0.2
96+91 -0.1552914270615124 0.579555495773441 4.898587196589413e-17
97+92 -0.1691614978857153 0.631319304793945 -0.1999999999999999
98+93 -0.2070552360820165 0.7727406610312545 -0.3464101615137753
99+94 -0.2588190451025206 0.9659258262890682 -0.4
100+95 -0.3105828541230248 1.159110991546882 -0.3464101615137755
101+96 -0.3484765923193259 1.300532347784191 -0.2000000000000002
102+97 -0.6999999999999996 1.212435565298214 0
103+98 -0.6732050807568875 1.166025403784439 0.2
104+99 -0.5999999999999999 1.039230484541326 0.3464101615137755
105+100 -0.4999999999999998 0.8660254037844387 0.4
106+101 -0.3999999999999999 0.692820323027551 0.3464101615137755
107+102 -0.326794919243112 0.5660254037844386 0.2
108+103 -0.2999999999999999 0.5196152422706632 4.898587196589413e-17
109+104 -0.326794919243112 0.5660254037844386 -0.1999999999999999
110+105 -0.3999999999999997 0.6928203230275508 -0.3464101615137753
111+106 -0.4999999999999997 0.8660254037844386 -0.4
112+107 -0.5999999999999999 1.039230484541326 -0.3464101615137755
113+108 -0.6732050807568873 1.166025403784439 -0.2000000000000002
114+109 -0.9899494936611664 0.9899494936611666 0
115+110 -0.9520557554648653 0.9520557554648654 0.2
116+111 -0.8485281374238571 0.8485281374238572 0.3464101615137755
117+112 -0.7071067811865475 0.7071067811865476 0.4
118+113 -0.565685424949238 0.5656854249492381 0.3464101615137755
119+114 -0.4621578069082296 0.4621578069082297 0.2
120+115 -0.4242640687119285 0.4242640687119285 4.898587196589413e-17
121+116 -0.4621578069082296 0.4621578069082297 -0.1999999999999999
122+117 -0.5656854249492378 0.5656854249492379 -0.3464101615137753
123+118 -0.7071067811865474 0.7071067811865475 -0.4
124+119 -0.8485281374238571 0.8485281374238572 -0.3464101615137755
125+120 -0.9520557554648652 0.9520557554648653 -0.2000000000000002
126+121 -1.212435565298214 0.6999999999999998 0
127+122 -1.166025403784439 0.6732050807568877 0.2
128+123 -1.039230484541326 0.6 0.3464101615137755
129+124 -0.8660254037844387 0.4999999999999999 0.4
130+125 -0.692820323027551 0.4 0.3464101615137755
131+126 -0.5660254037844386 0.3267949192431122 0.2
132+127 -0.5196152422706632 0.2999999999999999 4.898587196589413e-17
133+128 -0.5660254037844386 0.3267949192431122 -0.1999999999999999
134+129 -0.6928203230275508 0.3999999999999999 -0.3464101615137753
135+130 -0.8660254037844386 0.4999999999999999 -0.4
136+131 -1.039230484541326 0.6 -0.3464101615137755
137+132 -1.166025403784439 0.6732050807568876 -0.2000000000000002
138+133 -1.352296156804695 0.3623466631435294 0
139+134 -1.300532347784191 0.3484765923193265 0.2
140+135 -1.159110991546882 0.3105828541230253 0.3464101615137755
141+136 -0.9659258262890682 0.258819045102521 0.4
142+137 -0.7727406610312546 0.2070552360820168 0.3464101615137755
143+138 -0.6313193047939449 0.1691614978857156 0.2
144+139 -0.5795554957734409 0.1552914270615126 4.898587196589413e-17
145+140 -0.6313193047939449 0.1691614978857156 -0.1999999999999999
146+141 -0.7727406610312544 0.2070552360820168 -0.3464101615137753
147+142 -0.9659258262890681 0.258819045102521 -0.4
148+143 -1.159110991546882 0.3105828541230253 -0.3464101615137755
149+144 -1.300532347784191 0.3484765923193264 -0.2000000000000002
150+145 -1.4 1.714505518806294e-16 0
151+146 -1.346410161513776 1.648876894637316e-16 0.2
152+147 -1.2 1.469576158976824e-16 0.3464101615137755
153+148 -1 1.224646799147353e-16 0.4
154+149 -0.8 9.797174393178826e-17 0.3464101615137755
155+150 -0.6535898384862244 8.004167036573904e-17 0.2
156+151 -0.6 7.347880794884119e-17 4.898587196589413e-17
157+152 -0.6535898384862244 8.004167036573904e-17 -0.1999999999999999
158+153 -0.7999999999999998 9.797174393178824e-17 -0.3464101615137753
159+154 -0.9999999999999999 1.224646799147353e-16 -0.4
160+155 -1.2 1.469576158976824e-16 -0.3464101615137755
161+156 -1.346410161513775 1.648876894637316e-16 -0.2000000000000002
162+157 -1.352296156804696 -0.3623466631435291 0
163+158 -1.300532347784192 -0.3484765923193262 0.2
164+159 -1.159110991546882 -0.310582854123025 0.3464101615137755
165+160 -0.9659258262890683 -0.2588190451025208 0.4
166+161 -0.7727406610312547 -0.2070552360820166 0.3464101615137755
167+162 -0.631319304793945 -0.1691614978857154 0.2
168+163 -0.579555495773441 -0.1552914270615125 4.898587196589413e-17
169+164 -0.631319304793945 -0.1691614978857154 -0.1999999999999999
170+165 -0.7727406610312545 -0.2070552360820166 -0.3464101615137753
171+166 -0.9659258262890682 -0.2588190451025207 -0.4
172+167 -1.159110991546882 -0.310582854123025 -0.3464101615137755
173+168 -1.300532347784191 -0.3484765923193262 -0.2000000000000002
174+169 -1.212435565298214 -0.6999999999999996 0
175+170 -1.166025403784439 -0.6732050807568875 0.2
176+171 -1.039230484541327 -0.5999999999999998 0.3464101615137755
177+172 -0.8660254037844388 -0.4999999999999997 0.4
178+173 -0.6928203230275511 -0.3999999999999998 0.3464101615137755
179+174 -0.5660254037844387 -0.326794919243112 0.2
180+175 -0.5196152422706632 -0.2999999999999998 4.898587196589413e-17
181+176 -0.5660254037844387 -0.326794919243112 -0.1999999999999999
182+177 -0.6928203230275509 -0.3999999999999997 -0.3464101615137753
183+178 -0.8660254037844387 -0.4999999999999997 -0.4
184+179 -1.039230484541327 -0.5999999999999998 -0.3464101615137755
185+180 -1.166025403784439 -0.6732050807568873 -0.2000000000000002
186+181 -0.989949493661167 -0.9899494936611659 0
187+182 -0.952055755464866 -0.9520557554648649 0.2
188+183 -0.8485281374238576 -0.8485281374238567 0.3464101615137755
189+184 -0.7071067811865479 -0.7071067811865471 0.4
190+185 -0.5656854249492383 -0.5656854249492377 0.3464101615137755
191+186 -0.4621578069082299 -0.4621578069082294 0.2
192+187 -0.4242640687119287 -0.4242640687119283 4.898587196589413e-17
193+188 -0.4621578069082299 -0.4621578069082294 -0.1999999999999999
194+189 -0.5656854249492382 -0.5656854249492376 -0.3464101615137753
195+190 -0.7071067811865478 -0.707106781186547 -0.4
196+191 -0.8485281374238576 -0.8485281374238567 -0.3464101615137755
197+192 -0.9520557554648658 -0.9520557554648648 -0.2000000000000002
198+193 -0.7000000000000006 -1.212435565298214 0
199+194 -0.6732050807568883 -1.166025403784438 0.2
200+195 -0.6000000000000006 -1.039230484541326 0.3464101615137755
201+196 -0.5000000000000004 -0.8660254037844384 0.4
202+197 -0.4000000000000004 -0.6928203230275507 0.3464101615137755
203+198 -0.3267949192431125 -0.5660254037844383 0.2
204+199 -0.3000000000000003 -0.519615242270663 4.898587196589413e-17
205+200 -0.3267949192431125 -0.5660254037844383 -0.1999999999999999
206+201 -0.4000000000000002 -0.6928203230275506 -0.3464101615137753
207+202 -0.5000000000000003 -0.8660254037844383 -0.4
208+203 -0.6000000000000006 -1.039230484541326 -0.3464101615137755
209+204 -0.6732050807568882 -1.166025403784438 -0.2000000000000002
210+205 -0.3623466631435289 -1.352296156804696 0
211+206 -0.348476592319326 -1.300532347784192 0.2
212+207 -0.3105828541230248 -1.159110991546882 0.3464101615137755
213+208 -0.2588190451025206 -0.9659258262890683 0.4
214+209 -0.2070552360820165 -0.7727406610312547 0.3464101615137755
215+210 -0.1691614978857153 -0.631319304793945 0.2
216+211 -0.1552914270615124 -0.579555495773441 4.898587196589413e-17
217+212 -0.1691614978857153 -0.631319304793945 -0.1999999999999999
218+213 -0.2070552360820165 -0.7727406610312545 -0.3464101615137753
219+214 -0.2588190451025206 -0.9659258262890682 -0.4
220+215 -0.3105828541230248 -1.159110991546882 -0.3464101615137755
221+216 -0.3484765923193259 -1.300532347784191 -0.2000000000000002
222+217 -2.571758278209441e-16 -1.4 0
223+218 -2.473315341955974e-16 -1.346410161513776 0.2
224+219 -2.204364238465236e-16 -1.2 0.3464101615137755
225+220 -1.83697019872103e-16 -1 0.4
226+221 -1.469576158976824e-16 -0.8 0.3464101615137755
227+222 -1.200625055486085e-16 -0.6535898384862244 0.2
228+223 -1.102182119232618e-16 -0.6 4.898587196589413e-17
229+224 -1.200625055486085e-16 -0.6535898384862244 -0.1999999999999999
230+225 -1.469576158976823e-16 -0.7999999999999998 -0.3464101615137753
231+226 -1.836970198721029e-16 -0.9999999999999999 -0.4
232+227 -2.204364238465236e-16 -1.2 -0.3464101615137755
233+228 -2.473315341955973e-16 -1.346410161513775 -0.2000000000000002
234+229 0.3623466631435284 -1.352296156804696 0
235+230 0.3484765923193255 -1.300532347784192 0.2
236+231 0.3105828541230244 -1.159110991546882 0.3464101615137755
237+232 0.2588190451025203 -0.9659258262890684 0.4
238+233 0.2070552360820163 -0.7727406610312548 0.3464101615137755
239+234 0.1691614978857151 -0.6313193047939452 0.2
240+235 0.1552914270615122 -0.579555495773441 4.898587196589413e-17
241+236 0.1691614978857151 -0.6313193047939452 -0.1999999999999999
242+237 0.2070552360820162 -0.7727406610312546 -0.3464101615137753
243+238 0.2588190451025202 -0.9659258262890683 -0.4
244+239 0.3105828541230244 -1.159110991546882 -0.3464101615137755
245+240 0.3484765923193255 -1.300532347784191 -0.2000000000000002
246+241 0.7000000000000001 -1.212435565298214 0
247+242 0.6732050807568879 -1.166025403784439 0.2
248+243 0.6000000000000002 -1.039230484541326 0.3464101615137755
249+244 0.5000000000000001 -0.8660254037844386 0.4
250+245 0.4000000000000001 -0.6928203230275509 0.3464101615137755
251+246 0.3267949192431123 -0.5660254037844386 0.2
252+247 0.3 -0.5196152422706631 4.898587196589413e-17
253+248 0.3267949192431123 -0.5660254037844386 -0.1999999999999999
254+249 0.4 -0.6928203230275507 -0.3464101615137753
255+250 0.5 -0.8660254037844385 -0.4
256+251 0.6000000000000002 -1.039230484541326 -0.3464101615137755
257+252 0.6732050807568878 -1.166025403784438 -0.2000000000000002
258+253 0.9899494936611662 -0.9899494936611667 0
259+254 0.9520557554648652 -0.9520557554648656 0.2
260+255 0.8485281374238569 -0.8485281374238574 0.3464101615137755
261+256 0.7071067811865474 -0.7071067811865477 0.4
262+257 0.5656854249492379 -0.5656854249492381 0.3464101615137755
263+258 0.4621578069082295 -0.4621578069082297 0.2
264+259 0.4242640687119284 -0.4242640687119286 4.898587196589413e-17
265+260 0.4621578069082295 -0.4621578069082297 -0.1999999999999999
266+261 0.5656854249492378 -0.565685424949238 -0.3464101615137753
267+262 0.7071067811865472 -0.7071067811865476 -0.4
268+263 0.8485281374238569 -0.8485281374238574 -0.3464101615137755
269+264 0.952055755464865 -0.9520557554648654 -0.2000000000000002
270+265 1.212435565298214 -0.7000000000000006 0
271+266 1.166025403784438 -0.6732050807568883 0.2
272+267 1.039230484541326 -0.6000000000000006 0.3464101615137755
273+268 0.8660254037844384 -0.5000000000000004 0.4
274+269 0.6928203230275507 -0.4000000000000004 0.3464101615137755
275+270 0.5660254037844383 -0.3267949192431125 0.2
276+271 0.519615242270663 -0.3000000000000003 4.898587196589413e-17
277+272 0.5660254037844383 -0.3267949192431125 -0.1999999999999999
278+273 0.6928203230275506 -0.4000000000000002 -0.3464101615137753
279+274 0.8660254037844383 -0.5000000000000003 -0.4
280+275 1.039230484541326 -0.6000000000000006 -0.3464101615137755
281+276 1.166025403784438 -0.6732050807568882 -0.2000000000000002
282+277 1.352296156804695 -0.3623466631435302 0
283+278 1.300532347784191 -0.3484765923193273 0.2
284+279 1.159110991546882 -0.3105828541230259 0.3464101615137755
285+280 0.9659258262890681 -0.2588190451025216 0.4
286+281 0.7727406610312545 -0.2070552360820173 0.3464101615137755
287+282 0.6313193047939449 -0.1691614978857159 0.2
288+283 0.5795554957734408 -0.1552914270615129 4.898587196589413e-17
289+284 0.6313193047939449 -0.1691614978857159 -0.1999999999999999
290+285 0.7727406610312543 -0.2070552360820172 -0.3464101615137753
291+286 0.965925826289068 -0.2588190451025215 -0.4
292+287 1.159110991546882 -0.3105828541230259 -0.3464101615137755
293+288 1.300532347784191 -0.3484765923193271 -0.2000000000000002
294+$EndNodes
295+$Elements
296+288
297+1 3 2 1 1 1 13 14 2
298+2 3 2 1 1 2 14 15 3
299+3 3 2 1 1 3 15 16 4
300+4 3 2 1 1 4 16 17 5
301+5 3 2 1 1 5 17 18 6
302+6 3 2 1 1 6 18 19 7
303+7 3 2 1 1 7 19 20 8
304+8 3 2 1 1 8 20 21 9
305+9 3 2 1 1 9 21 22 10
306+10 3 2 1 1 10 22 23 11
307+11 3 2 1 1 11 23 24 12
308+12 3 2 1 1 12 24 13 1
309+13 3 2 1 1 13 25 26 14
310+14 3 2 1 1 14 26 27 15
311+15 3 2 1 1 15 27 28 16
312+16 3 2 1 1 16 28 29 17
313+17 3 2 1 1 17 29 30 18
314+18 3 2 1 1 18 30 31 19
315+19 3 2 1 1 19 31 32 20
316+20 3 2 1 1 20 32 33 21
317+21 3 2 1 1 21 33 34 22
318+22 3 2 1 1 22 34 35 23
319+23 3 2 1 1 23 35 36 24
320+24 3 2 1 1 24 36 25 13
321+25 3 2 1 1 25 37 38 26
322+26 3 2 1 1 26 38 39 27
323+27 3 2 1 1 27 39 40 28
324+28 3 2 1 1 28 40 41 29
325+29 3 2 1 1 29 41 42 30
326+30 3 2 1 1 30 42 43 31
327+31 3 2 1 1 31 43 44 32
328+32 3 2 1 1 32 44 45 33
329+33 3 2 1 1 33 45 46 34
330+34 3 2 1 1 34 46 47 35
331+35 3 2 1 1 35 47 48 36
332+36 3 2 1 1 36 48 37 25
333+37 3 2 1 1 37 49 50 38
334+38 3 2 1 1 38 50 51 39
335+39 3 2 1 1 39 51 52 40
336+40 3 2 1 1 40 52 53 41
337+41 3 2 1 1 41 53 54 42
338+42 3 2 1 1 42 54 55 43
339+43 3 2 1 1 43 55 56 44
340+44 3 2 1 1 44 56 57 45
341+45 3 2 1 1 45 57 58 46
342+46 3 2 1 1 46 58 59 47
343+47 3 2 1 1 47 59 60 48
344+48 3 2 1 1 48 60 49 37
345+49 3 2 1 1 49 61 62 50
346+50 3 2 1 1 50 62 63 51
347+51 3 2 1 1 51 63 64 52
348+52 3 2 1 1 52 64 65 53
349+53 3 2 1 1 53 65 66 54
350+54 3 2 1 1 54 66 67 55
351+55 3 2 1 1 55 67 68 56
352+56 3 2 1 1 56 68 69 57
353+57 3 2 1 1 57 69 70 58
354+58 3 2 1 1 58 70 71 59
355+59 3 2 1 1 59 71 72 60
356+60 3 2 1 1 60 72 61 49
357+61 3 2 1 1 61 73 74 62
358+62 3 2 1 1 62 74 75 63
359+63 3 2 1 1 63 75 76 64
360+64 3 2 1 1 64 76 77 65
361+65 3 2 1 1 65 77 78 66
362+66 3 2 1 1 66 78 79 67
363+67 3 2 1 1 67 79 80 68
364+68 3 2 1 1 68 80 81 69
365+69 3 2 1 1 69 81 82 70
366+70 3 2 1 1 70 82 83 71
367+71 3 2 1 1 71 83 84 72
368+72 3 2 1 1 72 84 73 61
369+73 3 2 1 1 73 85 86 74
370+74 3 2 1 1 74 86 87 75
371+75 3 2 1 1 75 87 88 76
372+76 3 2 1 1 76 88 89 77
373+77 3 2 1 1 77 89 90 78
374+78 3 2 1 1 78 90 91 79
375+79 3 2 1 1 79 91 92 80
376+80 3 2 1 1 80 92 93 81
377+81 3 2 1 1 81 93 94 82
378+82 3 2 1 1 82 94 95 83
379+83 3 2 1 1 83 95 96 84
380+84 3 2 1 1 84 96 85 73
381+85 3 2 1 1 85 97 98 86
382+86 3 2 1 1 86 98 99 87
383+87 3 2 1 1 87 99 100 88
384+88 3 2 1 1 88 100 101 89
385+89 3 2 1 1 89 101 102 90
386+90 3 2 1 1 90 102 103 91
387+91 3 2 1 1 91 103 104 92
388+92 3 2 1 1 92 104 105 93
389+93 3 2 1 1 93 105 106 94
390+94 3 2 1 1 94 106 107 95
391+95 3 2 1 1 95 107 108 96
392+96 3 2 1 1 96 108 97 85
393+97 3 2 1 1 97 109 110 98
394+98 3 2 1 1 98 110 111 99
395+99 3 2 1 1 99 111 112 100
396+100 3 2 1 1 100 112 113 101
397+101 3 2 1 1 101 113 114 102
398+102 3 2 1 1 102 114 115 103
399+103 3 2 1 1 103 115 116 104
400+104 3 2 1 1 104 116 117 105
401+105 3 2 1 1 105 117 118 106
402+106 3 2 1 1 106 118 119 107
403+107 3 2 1 1 107 119 120 108
404+108 3 2 1 1 108 120 109 97
405+109 3 2 1 1 109 121 122 110
406+110 3 2 1 1 110 122 123 111
407+111 3 2 1 1 111 123 124 112
408+112 3 2 1 1 112 124 125 113
409+113 3 2 1 1 113 125 126 114
410+114 3 2 1 1 114 126 127 115
411+115 3 2 1 1 115 127 128 116
412+116 3 2 1 1 116 128 129 117
413+117 3 2 1 1 117 129 130 118
414+118 3 2 1 1 118 130 131 119
415+119 3 2 1 1 119 131 132 120
416+120 3 2 1 1 120 132 121 109
417+121 3 2 1 1 121 133 134 122
418+122 3 2 1 1 122 134 135 123
419+123 3 2 1 1 123 135 136 124
420+124 3 2 1 1 124 136 137 125
421+125 3 2 1 1 125 137 138 126
422+126 3 2 1 1 126 138 139 127
423+127 3 2 1 1 127 139 140 128
424+128 3 2 1 1 128 140 141 129
425+129 3 2 1 1 129 141 142 130
426+130 3 2 1 1 130 142 143 131
427+131 3 2 1 1 131 143 144 132
428+132 3 2 1 1 132 144 133 121
429+133 3 2 1 1 133 145 146 134
430+134 3 2 1 1 134 146 147 135
431+135 3 2 1 1 135 147 148 136
432+136 3 2 1 1 136 148 149 137
433+137 3 2 1 1 137 149 150 138
434+138 3 2 1 1 138 150 151 139
435+139 3 2 1 1 139 151 152 140
436+140 3 2 1 1 140 152 153 141
437+141 3 2 1 1 141 153 154 142
438+142 3 2 1 1 142 154 155 143
439+143 3 2 1 1 143 155 156 144
440+144 3 2 1 1 144 156 145 133
441+145 3 2 1 1 145 157 158 146
442+146 3 2 1 1 146 158 159 147
443+147 3 2 1 1 147 159 160 148
444+148 3 2 1 1 148 160 161 149
445+149 3 2 1 1 149 161 162 150
446+150 3 2 1 1 150 162 163 151
447+151 3 2 1 1 151 163 164 152
448+152 3 2 1 1 152 164 165 153
449+153 3 2 1 1 153 165 166 154
450+154 3 2 1 1 154 166 167 155
451+155 3 2 1 1 155 167 168 156
452+156 3 2 1 1 156 168 157 145
453+157 3 2 1 1 157 169 170 158
454+158 3 2 1 1 158 170 171 159
455+159 3 2 1 1 159 171 172 160
456+160 3 2 1 1 160 172 173 161
457+161 3 2 1 1 161 173 174 162
458+162 3 2 1 1 162 174 175 163
459+163 3 2 1 1 163 175 176 164
460+164 3 2 1 1 164 176 177 165
461+165 3 2 1 1 165 177 178 166
462+166 3 2 1 1 166 178 179 167
463+167 3 2 1 1 167 179 180 168
464+168 3 2 1 1 168 180 169 157
465+169 3 2 1 1 169 181 182 170
466+170 3 2 1 1 170 182 183 171
467+171 3 2 1 1 171 183 184 172
468+172 3 2 1 1 172 184 185 173
469+173 3 2 1 1 173 185 186 174
470+174 3 2 1 1 174 186 187 175
471+175 3 2 1 1 175 187 188 176
472+176 3 2 1 1 176 188 189 177
473+177 3 2 1 1 177 189 190 178
474+178 3 2 1 1 178 190 191 179
475+179 3 2 1 1 179 191 192 180
476+180 3 2 1 1 180 192 181 169
477+181 3 2 1 1 181 193 194 182
478+182 3 2 1 1 182 194 195 183
479+183 3 2 1 1 183 195 196 184
480+184 3 2 1 1 184 196 197 185
481+185 3 2 1 1 185 197 198 186
482+186 3 2 1 1 186 198 199 187
483+187 3 2 1 1 187 199 200 188
484+188 3 2 1 1 188 200 201 189
485+189 3 2 1 1 189 201 202 190
486+190 3 2 1 1 190 202 203 191
487+191 3 2 1 1 191 203 204 192
488+192 3 2 1 1 192 204 193 181
489+193 3 2 1 1 193 205 206 194
490+194 3 2 1 1 194 206 207 195
491+195 3 2 1 1 195 207 208 196
492+196 3 2 1 1 196 208 209 197
493+197 3 2 1 1 197 209 210 198
494+198 3 2 1 1 198 210 211 199
495+199 3 2 1 1 199 211 212 200
496+200 3 2 1 1 200 212 213 201
497+201 3 2 1 1 201 213 214 202
498+202 3 2 1 1 202 214 215 203
499+203 3 2 1 1 203 215 216 204
500+204 3 2 1 1 204 216 205 193
501+205 3 2 1 1 205 217 218 206
502+206 3 2 1 1 206 218 219 207
503+207 3 2 1 1 207 219 220 208
504+208 3 2 1 1 208 220 221 209
505+209 3 2 1 1 209 221 222 210
506+210 3 2 1 1 210 222 223 211
507+211 3 2 1 1 211 223 224 212
508+212 3 2 1 1 212 224 225 213
509+213 3 2 1 1 213 225 226 214
510+214 3 2 1 1 214 226 227 215
511+215 3 2 1 1 215 227 228 216
512+216 3 2 1 1 216 228 217 205
513+217 3 2 1 1 217 229 230 218
514+218 3 2 1 1 218 230 231 219
515+219 3 2 1 1 219 231 232 220
516+220 3 2 1 1 220 232 233 221
517+221 3 2 1 1 221 233 234 222
518+222 3 2 1 1 222 234 235 223
519+223 3 2 1 1 223 235 236 224
520+224 3 2 1 1 224 236 237 225
521+225 3 2 1 1 225 237 238 226
522+226 3 2 1 1 226 238 239 227
523+227 3 2 1 1 227 239 240 228
524+228 3 2 1 1 228 240 229 217
525+229 3 2 1 1 229 241 242 230
526+230 3 2 1 1 230 242 243 231
527+231 3 2 1 1 231 243 244 232
528+232 3 2 1 1 232 244 245 233
529+233 3 2 1 1 233 245 246 234
530+234 3 2 1 1 234 246 247 235
531+235 3 2 1 1 235 247 248 236
532+236 3 2 1 1 236 248 249 237
533+237 3 2 1 1 237 249 250 238
534+238 3 2 1 1 238 250 251 239
535+239 3 2 1 1 239 251 252 240
536+240 3 2 1 1 240 252 241 229
537+241 3 2 1 1 241 253 254 242
538+242 3 2 1 1 242 254 255 243
539+243 3 2 1 1 243 255 256 244
540+244 3 2 1 1 244 256 257 245
541+245 3 2 1 1 245 257 258 246
542+246 3 2 1 1 246 258 259 247
543+247 3 2 1 1 247 259 260 248
544+248 3 2 1 1 248 260 261 249
545+249 3 2 1 1 249 261 262 250
546+250 3 2 1 1 250 262 263 251
547+251 3 2 1 1 251 263 264 252
548+252 3 2 1 1 252 264 253 241
549+253 3 2 1 1 253 265 266 254
550+254 3 2 1 1 254 266 267 255
551+255 3 2 1 1 255 267 268 256
552+256 3 2 1 1 256 268 269 257
553+257 3 2 1 1 257 269 270 258
554+258 3 2 1 1 258 270 271 259
555+259 3 2 1 1 259 271 272 260
556+260 3 2 1 1 260 272 273 261
557+261 3 2 1 1 261 273 274 262
558+262 3 2 1 1 262 274 275 263
559+263 3 2 1 1 263 275 276 264
560+264 3 2 1 1 264 276 265 253
561+265 3 2 1 1 265 277 278 266
562+266 3 2 1 1 266 278 279 267
563+267 3 2 1 1 267 279 280 268
564+268 3 2 1 1 268 280 281 269
565+269 3 2 1 1 269 281 282 270
566+270 3 2 1 1 270 282 283 271
567+271 3 2 1 1 271 283 284 272
568+272 3 2 1 1 272 284 285 273
569+273 3 2 1 1 273 285 286 274
570+274 3 2 1 1 274 286 287 275
571+275 3 2 1 1 275 287 288 276
572+276 3 2 1 1 276 288 277 265
573+277 3 2 1 1 277 1 2 278
574+278 3 2 1 1 278 2 3 279
575+279 3 2 1 1 279 3 4 280
576+280 3 2 1 1 280 4 5 281
577+281 3 2 1 1 281 5 6 282
578+282 3 2 1 1 282 6 7 283
579+283 3 2 1 1 283 7 8 284
580+284 3 2 1 1 284 8 9 285
581+285 3 2 1 1 285 9 10 286
582+286 3 2 1 1 286 10 11 287
583+287 3 2 1 1 287 11 12 288
584+288 3 2 1 1 288 12 1 277
585+$EndElements
scripts/engine-test.mjsadded+176−0View file
@@ -0,0 +1,176 @@
1+// Headless validation of the solver engine — runs the same MATLAB project the
2+// browser worker runs, in Node, against the installed numbl. Bootstraps mip
3+// exactly like the worker does and lets `mip load --install surfacefun` in
4+// main.m fetch surfacefun/chebfun itself. Node has no synchronous
5+// XMLHttpRequest, so websave/webread are shimmed with curl (responses cached
6+// in .cache/ keyed by URL, so repeat runs are offline).
7+//
8+// npm run engine-test
9+
10+import {
11+ executeCode,
12+ VirtualFileSystem,
13+ BrowserFileIOAdapter,
14+ BrowserSystemAdapter,
15+} from 'numbl'
16+import {unzipSync} from 'fflate'
17+import {execFileSync} from 'node:child_process'
18+import crypto from 'node:crypto'
19+import fs from 'node:fs'
20+import path from 'node:path'
21+import {fileURLToPath} from 'node:url'
22+
23+const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
24+const cacheDir = path.join(root, '.cache')
25+
26+const MIP_MHL_URL =
27+ 'https://github.com/mip-org/mip-core/releases/download/mip-numbl/mip-numbl-any.mhl'
28+const MIP_SYSTEM_PREFIX = '/system/mip/packages/gh/mip-org/core/mip/'
29+// In the browser this is passed by numbl/browser's session worker; executeCode
30+// scans searchPaths directories since the same numbl change.
31+const MIP_SEARCH_PATH = MIP_SYSTEM_PREFIX + 'mip'
32+
33+function curlCached(url) {
34+ fs.mkdirSync(cacheDir, {recursive: true})
35+ const key = crypto.createHash('sha1').update(url).digest('hex').slice(0, 16)
36+ const cached = path.join(cacheDir, key)
37+ if (!fs.existsSync(cached)) {
38+ console.log(`fetching ${url}`)
39+ execFileSync('curl', ['-sfL', '-o', cached, url], {stdio: 'inherit'})
40+ }
41+ return fs.readFileSync(cached)
42+}
43+
44+// numbl's BrowserFileIOAdapter implements websave/webread with synchronous
45+// XHR (fine in a web worker, absent in Node); override with curl.
46+class NodeFileIOAdapter extends BrowserFileIOAdapter {
47+ constructor(vfs) {
48+ super(vfs)
49+ this.nodeVfs = vfs
50+ }
51+ websave(url, filename) {
52+ this.nodeVfs.writeFile(this.nodeVfs.normalizePath(filename), new Uint8Array(curlCached(url)))
53+ }
54+ webread(url) {
55+ return curlCached(url).toString('utf8')
56+ }
57+}
58+
59+const readProjectFile = name =>
60+ fs.readFileSync(path.join(root, 'matlab', name), 'utf8')
61+
62+async function main() {
63+ const vfs = new VirtualFileSystem()
64+ const enc = new TextEncoder()
65+
66+ // Bootstrap mip into the system VFS, as the browser worker does.
67+ const mipEntries = unzipSync(new Uint8Array(curlCached(MIP_MHL_URL)))
68+ let nMip = 0
69+ for (const [name, content] of Object.entries(mipEntries)) {
70+ if (name.endsWith('/')) continue
71+ vfs.writeFile(MIP_SYSTEM_PREFIX + name, content)
72+ nMip++
73+ }
74+ console.log(`mip core: ${nMip} files into VFS`)
75+
76+ const projectFiles = [
77+ 'main.m',
78+ 'solver_session.m',
79+ 'solve_pde.m',
80+ 'surfacemesh_from_quads.m',
81+ 'load_gmsh_quads.m',
82+ 'placeholder.html',
83+ ]
84+ for (const name of projectFiles) {
85+ vfs.writeFile(`/project/${name}`, enc.encode(readProjectFile(name)))
86+ }
87+ vfs.writeFile(
88+ '/project/mesh.msh',
89+ fs.readFileSync(path.join(root, 'public', 'samples', 'sphere.msh'))
90+ )
91+ vfs.clearChangeTracking()
92+ vfs.setCwd('/project')
93+
94+ const events = []
95+ let compId = null
96+
97+ console.log('running main.m (mip load --install surfacefun) ...')
98+ const t0 = Date.now()
99+ const result = executeCode(
100+ readProjectFile('main.m'),
101+ {
102+ onOutput: text => process.stdout.write(`[numbl] ${text}`),
103+ onDrawnow: () => {},
104+ displayResults: false,
105+ maxIterations: 1e9,
106+ optimization: '1',
107+ fileIO: new NodeFileIOAdapter(vfs),
108+ system: new BrowserSystemAdapter(vfs),
109+ onHtmlSourceEvent: (id, name, dataJson) =>
110+ events.push({name, data: JSON.parse(dataJson)}),
111+ },
112+ projectFiles
113+ .filter(n => n.endsWith('.m'))
114+ .map(n => ({name: n, source: readProjectFile(n)})),
115+ vfs.normalizePath('/project/main.m'),
116+ [MIP_SEARCH_PATH]
117+ )
118+ for (const pi of result.plotInstructions) {
119+ if (pi.type === 'uihtml') compId = pi.id
120+ }
121+ console.log(`boot: ${(Date.now() - t0) / 1000}s, uihtml comp = ${compId}`)
122+ const session = result.uihtmlSession
123+ if (!session || !compId) throw new Error('no live uihtml session after run')
124+
125+ const solve = params => {
126+ events.length = 0
127+ const t = Date.now()
128+ session.dispatchEvent(compId, 'HTMLEventReceived', {
129+ name: 'solve',
130+ data: params,
131+ })
132+ const ev = events[events.length - 1]
133+ if (!ev) throw new Error('no event came back from solve')
134+ console.log(`solve [${params.pde}] -> '${ev.name}' in ${(Date.now() - t) / 1000}s`)
135+ return ev
136+ }
137+
138+ // 1. Poisson on the closed sphere
139+ let ev = solve({pde: 'poisson', f: 'x.*y.*z', c: '', p: 6, closed: true})
140+ if (ev.name !== 'solution') throw new Error(`poisson failed: ${JSON.stringify(ev.data)}`)
141+ let d = ev.data
142+ console.log(` npatches=${d.npatches} n=${d.n} u in [${d.umin.toFixed(6)}, ${d.umax.toFixed(6)}]`)
143+ if (d.npatches !== 216 || d.n !== 7) throw new Error('unexpected solution shape')
144+ if (!isFinite(d.umin) || !isFinite(d.umax) || d.umin === d.umax)
145+ throw new Error('degenerate solution values')
146+
147+ // Eigenfunction check: x*y*z is a degree-3 solid harmonic, so on the unit
148+ // sphere lap_S (x*y*z) = -12 * (x*y*z). Solving with f = -12*x*y*z must
149+ // reproduce u = x*y*z, whose max on the sphere is 1/(3*sqrt(3)).
150+ ev = solve({pde: 'poisson', f: '-12*(x.*y.*z)', c: '', p: 8, closed: true})
151+ d = ev.data
152+ const expected = 1 / (3 * Math.sqrt(3))
153+ console.log(` eigencheck: umax=${d.umax.toFixed(6)} expected~${expected.toFixed(6)}`)
154+ if (Math.abs(d.umax - expected) > 0.01) throw new Error('eigenfunction check failed')
155+
156+ // 2. Helmholtz with a variable coefficient
157+ ev = solve({pde: 'helmholtz', f: '1 + 0*x', c: '100*(1 - z)', p: 6, closed: true})
158+ if (ev.name !== 'solution') throw new Error(`helmholtz failed: ${JSON.stringify(ev.data)}`)
159+ console.log(` u in [${ev.data.umin.toFixed(6)}, ${ev.data.umax.toFixed(6)}]`)
160+
161+ // 3. Bad expression surfaces as a solveError, session stays alive
162+ ev = solve({pde: 'poisson', f: 'this is not matlab', c: '', p: 4, closed: true})
163+ if (ev.name !== 'solveError') throw new Error('expected solveError for bad expression')
164+ console.log(` error path OK: ${JSON.stringify(ev.data).slice(0, 100)}`)
165+
166+ // 4. Session still works after an error
167+ ev = solve({pde: 'poisson', f: 'x', c: '', p: 4, closed: true})
168+ if (ev.name !== 'solution') throw new Error('session did not survive the error')
169+
170+ console.log('engine-test: all checks passed')
171+}
172+
173+main().catch(err => {
174+ console.error(err)
175+ process.exit(1)
176+})
scripts/make_samples.pyadded+109−0View file
@@ -0,0 +1,109 @@
1+"""Generate the bundled sample quad meshes (public/samples/*.msh).
2+
3+Writes Gmsh MSH 2.2 ASCII files in the same canonical form the in-app
4+converter produces: sequential 1-based node ids and 4-node quadrangle
5+elements (type 3) with two tags.
6+
7+Run from the repo root: python3 scripts/make_samples.py
8+"""
9+
10+import math
11+import os
12+
13+OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "public", "samples")
14+
15+
16+def write_msh(path, points, quads):
17+ lines = ["$MeshFormat", "2.2 0 8", "$EndMeshFormat"]
18+ lines.append("$Nodes")
19+ lines.append(str(len(points)))
20+ for i, (x, y, z) in enumerate(points, start=1):
21+ lines.append(f"{i} {x:.16g} {y:.16g} {z:.16g}")
22+ lines.append("$EndNodes")
23+ lines.append("$Elements")
24+ lines.append(str(len(quads)))
25+ for i, q in enumerate(quads, start=1):
26+ a, b, c, d = (v + 1 for v in q) # 0-based -> 1-based
27+ lines.append(f"{i} 3 2 1 1 {a} {b} {c} {d}")
28+ lines.append("$EndElements")
29+ with open(path, "w") as f:
30+ f.write("\n".join(lines) + "\n")
31+
32+
33+def cubed_sphere(m):
34+ """Cube [-1,1]^3 with m-by-m quads per face, projected onto the sphere."""
35+ points = []
36+ index = {}
37+
38+ def vertex(x, y, z):
39+ # Normalize onto the unit sphere; dedup shared face-boundary vertices.
40+ r = math.sqrt(x * x + y * y + z * z)
41+ p = (x / r, y / r, z / r)
42+ key = tuple(round(v, 12) for v in p)
43+ if key not in index:
44+ index[key] = len(points)
45+ points.append(p)
46+ return index[key]
47+
48+ # Each face: origin corner + two axis vectors spanning the face.
49+ faces = [
50+ ((-1, -1, 1), (1, 0, 0), (0, 1, 0)), # +z
51+ ((-1, 1, -1), (1, 0, 0), (0, -1, 0)), # -z
52+ ((-1, -1, -1), (0, 1, 0), (0, 0, 1)), # -x
53+ ((1, -1, -1), (0, 0, 1), (0, 1, 0)), # +x
54+ ((-1, -1, -1), (0, 0, 1), (1, 0, 0)), # -y
55+ ((-1, 1, -1), (1, 0, 0), (0, 0, 1)), # +y
56+ ]
57+ quads = []
58+ for origin, du, dv in faces:
59+ for i in range(m):
60+ for j in range(m):
61+ corners = []
62+ for di, dj in ((0, 0), (1, 0), (1, 1), (0, 1)):
63+ s = 2 * (i + di) / m
64+ t = 2 * (j + dj) / m
65+ corners.append(
66+ vertex(
67+ origin[0] + s * du[0] + t * dv[0],
68+ origin[1] + s * du[1] + t * dv[1],
69+ origin[2] + s * du[2] + t * dv[2],
70+ )
71+ )
72+ quads.append(tuple(corners))
73+ return points, quads
74+
75+
76+def torus(nu, nv, R=1.0, r=0.4):
77+ points = []
78+ for i in range(nu):
79+ a = 2 * math.pi * i / nu
80+ for j in range(nv):
81+ b = 2 * math.pi * j / nv
82+ points.append(
83+ (
84+ (R + r * math.cos(b)) * math.cos(a),
85+ (R + r * math.cos(b)) * math.sin(a),
86+ r * math.sin(b),
87+ )
88+ )
89+ quads = []
90+ for i in range(nu):
91+ for j in range(nv):
92+ i2 = (i + 1) % nu
93+ j2 = (j + 1) % nv
94+ quads.append((i * nv + j, i2 * nv + j, i2 * nv + j2, i * nv + j2))
95+ return points, quads
96+
97+
98+def main():
99+ os.makedirs(OUT_DIR, exist_ok=True)
100+ pts, quads = cubed_sphere(6)
101+ write_msh(os.path.join(OUT_DIR, "sphere.msh"), pts, quads)
102+ print(f"sphere.msh: {len(pts)} nodes, {len(quads)} quads")
103+ pts, quads = torus(24, 12)
104+ write_msh(os.path.join(OUT_DIR, "torus.msh"), pts, quads)
105+ print(f"torus.msh: {len(pts)} nodes, {len(quads)} quads")
106+
107+
108+if __name__ == "__main__":
109+ main()
src/App.tsxadded+377−0View file
@@ -0,0 +1,377 @@
1+import { useCallback, useEffect, useRef, useState } from 'react'
2+import { ACCEPT, formatForFilename } from './mesh/formats'
3+import { initMeshio, parseMeshFile } from './mesh/meshio'
4+import { edgeClassification, type QuadMeshData } from './mesh/quadmesh'
5+import { SolverEngine } from './engine/engine'
6+import type { SolutionData } from './engine/protocol'
7+import {
8+ PDES,
9+ MAX_QUADS,
10+ SLOW_QUADS,
11+ MIN_ORDER,
12+ MAX_ORDER,
13+ DEFAULT_ORDER,
14+ type PdeDef,
15+} from './pde/presets'
16+import { SurfaceView } from './render/SurfaceView'
17+
18+// Module-level singletons so React StrictMode double-mounting doesn't boot
19+// two engines (each boot downloads the MATLAB packages).
20+let engineSingleton: SolverEngine | null = null
21+function getEngine(): SolverEngine {
22+ if (!engineSingleton) {
23+ engineSingleton = new SolverEngine()
24+ engineSingleton.start().catch(() => {
25+ /* surfaced through onProgress/status below */
26+ })
27+ }
28+ return engineSingleton
29+}
30+
31+interface LoadedMesh {
32+ name: string
33+ data: QuadMeshData
34+ numVertices: number
35+ numQuads: number
36+ closed: boolean
37+ nonManifold: boolean
38+ warnings: string[]
39+}
40+
41+const SAMPLES = [
42+ { label: 'Sphere (cubed)', file: 'sphere.msh' },
43+ { label: 'Torus', file: 'torus.msh' },
44+]
45+
46+export default function App() {
47+ const engineRef = useRef<SolverEngine>(null)
48+ if (!engineRef.current) engineRef.current = getEngine()
49+ const engine = engineRef.current
50+
51+ const [meshioStatus, setMeshioStatus] = useState('Loading Python runtime…')
52+ const [meshioReady, setMeshioReady] = useState(false)
53+ const [engineStatus, setEngineStatus] = useState('Starting MATLAB engine…')
54+ const [engineReady, setEngineReady] = useState(false)
55+ const [consoleLines, setConsoleLines] = useState<string[]>([])
56+
57+ const [mesh, setMesh] = useState<LoadedMesh | null>(null)
58+ const [meshError, setMeshError] = useState<string | null>(null)
59+ const [parsing, setParsing] = useState(false)
60+
61+ const [pde, setPde] = useState<PdeDef>(PDES[0])
62+ const [fExpr, setFExpr] = useState(PDES[0].fPresets[0].expr)
63+ const [cExpr, setCExpr] = useState('100*(1 - z)')
64+ const [order, setOrder] = useState(DEFAULT_ORDER)
65+
66+ const [solving, setSolving] = useState(false)
67+ const [solveError, setSolveError] = useState<string | null>(null)
68+ const [solution, setSolution] = useState<SolutionData | null>(null)
69+ const [solveSeconds, setSolveSeconds] = useState<number | null>(null)
70+
71+ useEffect(() => {
72+ initMeshio(setMeshioStatus)
73+ .then(() => {
74+ setMeshioReady(true)
75+ setMeshioStatus('')
76+ })
77+ .catch((err) => setMeshioStatus(`Mesh reader failed: ${String(err.message ?? err)}`))
78+
79+ engine.onProgress = setEngineStatus
80+ engine.onError = (message) => setEngineStatus(`Engine failed: ${message}`)
81+ engine.onOutput = (text) =>
82+ setConsoleLines((lines) => [...lines.slice(-199), text.replace(/\n$/, '')])
83+ if (engine.lastError) setEngineStatus(`Engine failed: ${engine.lastError}`)
84+ const poll = setInterval(() => {
85+ if (engine.isReady) {
86+ setEngineReady(true)
87+ setEngineStatus('')
88+ clearInterval(poll)
89+ }
90+ }, 250)
91+ return () => clearInterval(poll)
92+ }, [engine])
93+
94+ const loadMesh = useCallback(
95+ async (name: string, bytes: Uint8Array) => {
96+ setMeshError(null)
97+ setSolveError(null)
98+ setSolution(null)
99+ setParsing(true)
100+ try {
101+ const format = formatForFilename(name)
102+ if (!format) throw new Error(`Unsupported file extension on "${name}"`)
103+ const result = await parseMeshFile(bytes, format)
104+ if (result.numQuads > MAX_QUADS) {
105+ throw new Error(
106+ `${result.numQuads} quads is too many for an in-browser solve ` +
107+ `(limit ${MAX_QUADS}); please upload a coarser mesh.`,
108+ )
109+ }
110+ const cls = edgeClassification(result.mesh.quads)
111+ setMesh({
112+ name,
113+ data: result.mesh,
114+ numVertices: result.numVertices,
115+ numQuads: result.numQuads,
116+ closed: cls.closed,
117+ nonManifold: cls.nonManifold,
118+ warnings: result.warnings,
119+ })
120+ engine.setMesh(result.mesh.mshBytes)
121+ } catch (err) {
122+ setMesh(null)
123+ setMeshError(err instanceof Error ? err.message : String(err))
124+ } finally {
125+ setParsing(false)
126+ }
127+ },
128+ [engine],
129+ )
130+
131+ const onUpload = useCallback(
132+ async (e: React.ChangeEvent<HTMLInputElement>) => {
133+ const file = e.target.files?.[0]
134+ e.target.value = ''
135+ if (!file) return
136+ await loadMesh(file.name, new Uint8Array(await file.arrayBuffer()))
137+ },
138+ [loadMesh],
139+ )
140+
141+ const onSample = useCallback(
142+ async (file: string) => {
143+ const resp = await fetch(`${import.meta.env.BASE_URL}samples/${file}`)
144+ if (!resp.ok) {
145+ setMeshError(`Failed to fetch sample: HTTP ${resp.status}`)
146+ return
147+ }
148+ await loadMesh(file, new Uint8Array(await resp.arrayBuffer()))
149+ },
150+ [loadMesh],
151+ )
152+
153+ const onSolve = useCallback(async () => {
154+ if (!mesh) return
155+ setSolveError(null)
156+ setSolving(true)
157+ setSolveSeconds(null)
158+ const t0 = performance.now()
159+ try {
160+ const result = await engine.solve({
161+ pde: pde.id,
162+ f: fExpr.trim(),
163+ c: cExpr.trim(),
164+ p: order,
165+ closed: mesh.closed,
166+ })
167+ setSolution(result)
168+ setSolveSeconds((performance.now() - t0) / 1000)
169+ } catch (err) {
170+ setSolveError(err instanceof Error ? err.message : String(err))
171+ } finally {
172+ setSolving(false)
173+ }
174+ }, [engine, mesh, pde, fExpr, cExpr, order])
175+
176+ const onDownloadMsh = useCallback(() => {
177+ if (!mesh) return
178+ const blob = new Blob([mesh.data.mshBytes as BlobPart], { type: 'application/octet-stream' })
179+ const a = document.createElement('a')
180+ a.href = URL.createObjectURL(blob)
181+ a.download = mesh.name.replace(/\.[^.]*$/, '') + '.msh'
182+ a.click()
183+ URL.revokeObjectURL(a.href)
184+ }, [mesh])
185+
186+ const onPdeChange = (id: string) => {
187+ const def = PDES.find((p) => p.id === id) ?? PDES[0]
188+ setPde(def)
189+ setFExpr(def.fPresets[0].expr)
190+ }
191+
192+ const booting = !meshioReady || !engineReady
193+ const dof = mesh ? mesh.numQuads * (order + 1) * (order + 1) : 0
194+ const canSolve = !!mesh && engineReady && !solving && fExpr.trim() !== ''
195+
196+ return (
197+ <div className="app">
198+ <header>
199+ <h1>Mesh PDE Solver</h1>
200+ <p>
201+ Upload a quadrilateral surface mesh, pick a PDE, and solve it on the surface with{' '}
202+ <a href="https://github.com/danfortunato/surfacefun" target="_blank" rel="noreferrer">
203+ surfacefun
204+ </a>{' '}
205+ running in your browser via <a href="https://numbl.org" target="_blank" rel="noreferrer">numbl</a>.
206+ </p>
207+ </header>
208+
209+ <div className="columns">
210+ <aside>
211+ <section>
212+ <h2>1 · Mesh</h2>
213+ <div className="row">
214+ <label className="button">
215+ Upload mesh…
216+ <input type="file" accept={ACCEPT} onChange={onUpload} hidden />
217+ </label>
218+ {SAMPLES.map((s) => (
219+ <button key={s.file} onClick={() => onSample(s.file)} disabled={!meshioReady}>
220+ {s.label}
221+ </button>
222+ ))}
223+ </div>
224+ <p className="hint">
225+ Quad meshes in any format meshio reads ({ACCEPT.replaceAll(',', ' ')}); converted to
226+ Gmsh format for surfacefun.
227+ </p>
228+ <div className="meshinfo">
229+ {parsing ? (
230+ <div className="status">Reading mesh…</div>
231+ ) : meshError ? (
232+ <div className="error">{meshError}</div>
233+ ) : mesh ? (
234+ <>
235+ <div>
236+ <strong>{mesh.name}</strong> — {mesh.numVertices} vertices, {mesh.numQuads}{' '}
237+ quads, {mesh.closed ? 'closed surface' : 'open surface (boundary present)'}
238+ </div>
239+ {mesh.nonManifold && (
240+ <div className="warn">Non-manifold edges detected; the solve may fail.</div>
241+ )}
242+ {mesh.numQuads > SLOW_QUADS && (
243+ <div className="warn">Large mesh — the solve may take a while.</div>
244+ )}
245+ {mesh.warnings.map((w) => (
246+ <div className="warn" key={w}>
247+ {w}
248+ </div>
249+ ))}
250+ <button className="linkish" onClick={onDownloadMsh}>
251+ Download converted .msh
252+ </button>
253+ </>
254+ ) : (
255+ <div className="placeholder">No mesh loaded — upload a file or pick a sample.</div>
256+ )}
257+ </div>
258+ </section>
259+
260+ <section>
261+ <h2>2 · PDE</h2>
262+ <label className="field">
263+ <span>Equation</span>
264+ <select value={pde.id} onChange={(e) => onPdeChange(e.target.value)}>
265+ {PDES.map((p) => (
266+ <option key={p.id} value={p.id}>
267+ {p.label} — {p.equation}
268+ </option>
269+ ))}
270+ </select>
271+ </label>
272+ <p className="hint pde-note">{pde.note}</p>
273+
274+ <label className="field">
275+ <span>Right-hand side f(x, y, z)</span>
276+ <div className="preset-row">
277+ <select
278+ value=""
279+ onChange={(e) => {
280+ if (e.target.value) setFExpr(e.target.value)
281+ }}
282+ >
283+ <option value="">presets…</option>
284+ {pde.fPresets.map((p) => (
285+ <option key={p.label} value={p.expr}>
286+ {p.label}
287+ </option>
288+ ))}
289+ </select>
290+ <input
291+ type="text"
292+ value={fExpr}
293+ onChange={(e) => setFExpr(e.target.value)}
294+ spellCheck={false}
295+ />
296+ </div>
297+ </label>
298+
299+ <label className={pde.cPresets ? 'field' : 'field inactive'}>
300+ <span>Coefficient c(x, y, z)</span>
301+ <div className="preset-row">
302+ <select
303+ value=""
304+ disabled={!pde.cPresets}
305+ onChange={(e) => {
306+ if (e.target.value) setCExpr(e.target.value)
307+ }}
308+ >
309+ <option value="">presets…</option>
310+ {(pde.cPresets ?? []).map((p) => (
311+ <option key={p.label} value={p.expr}>
312+ {p.label}
313+ </option>
314+ ))}
315+ </select>
316+ <input
317+ type="text"
318+ value={pde.cPresets ? cExpr : ''}
319+ placeholder={pde.cPresets ? undefined : 'not used by this equation'}
320+ disabled={!pde.cPresets}
321+ onChange={(e) => setCExpr(e.target.value)}
322+ spellCheck={false}
323+ />
324+ </div>
325+ </label>
326+
327+ <label className="field">
328+ <span>
329+ Polynomial order p = {order}
330+ {mesh ? ` (~${dof.toLocaleString()} unknowns)` : ''}
331+ </span>
332+ <input
333+ type="range"
334+ min={MIN_ORDER}
335+ max={MAX_ORDER}
336+ value={order}
337+ onChange={(e) => setOrder(Number(e.target.value))}
338+ />
339+ </label>
340+ </section>
341+
342+ <section>
343+ <h2>3 · Solve</h2>
344+ <button className="solve" onClick={onSolve} disabled={!canSolve}>
345+ {solving ? 'Solving…' : 'Solve'}
346+ </button>
347+ <div className="solve-status">
348+ {booting ? (
349+ <p className="status">
350+ {[meshioStatus, engineStatus].filter(Boolean).join(' · ') || 'Preparing…'}
351+ </p>
352+ ) : solveError ? (
353+ <p className="error">{solveError}</p>
354+ ) : solution && solveSeconds !== null ? (
355+ <p className="status">
356+ Solved in {solveSeconds.toFixed(1)} s · u ∈ [{solution.umin.toPrecision(4)},{' '}
357+ {solution.umax.toPrecision(4)}]
358+ </p>
359+ ) : null}
360+ </div>
361+ </section>
362+
363+ <section>
364+ <details>
365+ <summary>Engine console</summary>
366+ <pre className="console">{consoleLines.join('\n') || '(no output yet)'}</pre>
367+ </details>
368+ </section>
369+ </aside>
370+
371+ <main>
372+ <SurfaceView mesh={mesh?.data ?? null} solution={solution} />
373+ </main>
374+ </div>
375+ </div>
376+ )
377+}
src/engine/engine.tsadded+139−0View file
@@ -0,0 +1,139 @@
1+// The solver engine, built on numbl/browser's managed session: numbl owns
2+// the worker, the VFS, the mip bootstrap (main.m does
3+// `mip load --install surfacefun`), and IndexedDB persistence of installed
4+// packages across page loads. This wrapper adds the app's solve protocol:
5+// mesh.msh in the VFS + one 'solve' event in flight at a time, with a
6+// timeout.
7+
8+import { createNumblSession, type NumblSession } from 'numbl/browser'
9+import { PROJECT_FILES, MAIN_FILE } from './project'
10+import type { SolveParams, SolutionData } from './protocol'
11+
12+const SOLVE_TIMEOUT_MS = 300_000
13+
14+export class EngineError extends Error {}
15+
16+export class SolverEngine {
17+ private session: NumblSession | null = null
18+ private compId: string | null = null
19+ private disposed = false
20+ private pendingMesh: Uint8Array | null = null
21+
22+ private solveWaiter: {
23+ resolve: (data: SolutionData) => void
24+ reject: (err: Error) => void
25+ timeoutId: ReturnType<typeof setTimeout>
26+ } | null = null
27+
28+ /** Boot progress messages (downloads, engine start) for the UI. */
29+ onProgress: (message: string) => void = () => {}
30+ /** MATLAB console output (mip install logs etc.), for a console panel. */
31+ onOutput: (text: string) => void = () => {}
32+ /** Hard failures (boot errors) — the engine is unusable afterwards. */
33+ onError: (message: string) => void = () => {}
34+ /** Last hard failure, for subscribers that attach after it happened. */
35+ lastError: string | null = null
36+
37+ async start(): Promise<void> {
38+ try {
39+ const session = await createNumblSession({
40+ files: PROJECT_FILES.map((f) => ({ path: f.path, content: f.text })),
41+ mainFile: MAIN_FILE,
42+ onProgress: (message) => this.onProgress(message),
43+ onOutput: (text) => this.onOutput(text),
44+ onHtmlSourceEvent: (_compId, name, dataJson) =>
45+ this.handleScriptEvent(name, dataJson),
46+ })
47+ if (this.disposed) {
48+ session.dispose()
49+ return
50+ }
51+ this.compId = session.uihtmlComponents[0]?.compId ?? null
52+ if (!session.hasUihtmlSession || !this.compId) {
53+ session.dispose()
54+ throw new EngineError('script finished without a live uihtml session')
55+ }
56+ this.session = session
57+ if (this.pendingMesh) {
58+ session.writeFile('mesh.msh', this.pendingMesh)
59+ this.pendingMesh = null
60+ }
61+ } catch (err) {
62+ const message = err instanceof Error ? err.message : String(err)
63+ this.lastError = message
64+ if (!this.disposed) this.onError(message)
65+ throw err instanceof Error ? err : new EngineError(message)
66+ }
67+ }
68+
69+ /** Make `bytes` the mesh.msh the next solve reads. */
70+ setMesh(bytes: Uint8Array): void {
71+ if (this.session) this.session.writeFile('mesh.msh', bytes)
72+ else this.pendingMesh = bytes
73+ }
74+
75+ solve(params: SolveParams): Promise<SolutionData> {
76+ if (!this.session || !this.compId) {
77+ return Promise.reject(new EngineError('engine not ready'))
78+ }
79+ if (this.solveWaiter) {
80+ return Promise.reject(new EngineError('a solve is already running'))
81+ }
82+ const dispatched = this.session.dispatchHtmlEvent(this.compId, 'solve', params)
83+ return new Promise<SolutionData>((resolve, reject) => {
84+ const timeoutId = setTimeout(() => {
85+ this.settleSolve((w) => w.reject(new EngineError('solve timed out')))
86+ }, SOLVE_TIMEOUT_MS)
87+ this.solveWaiter = { resolve, reject, timeoutId }
88+ // An interpreter-level dispatch failure (vs. the solveError event the
89+ // script sends for caught errors) also settles the solve.
90+ dispatched.catch((err) => {
91+ this.settleSolve((w) =>
92+ w.reject(err instanceof Error ? err : new EngineError(String(err))),
93+ )
94+ })
95+ })
96+ }
97+
98+ get isReady(): boolean {
99+ return this.session !== null
100+ }
101+
102+ get isBusy(): boolean {
103+ return this.solveWaiter !== null
104+ }
105+
106+ dispose(): void {
107+ if (this.disposed) return
108+ this.disposed = true
109+ this.settleSolve((w) => w.reject(new EngineError('engine disposed')))
110+ this.session?.dispose()
111+ this.session = null
112+ }
113+
114+ // ---- internals ---------------------------------------------------------
115+
116+ private settleSolve(settle: (w: NonNullable<typeof this.solveWaiter>) => void) {
117+ const w = this.solveWaiter
118+ if (!w) return
119+ this.solveWaiter = null
120+ clearTimeout(w.timeoutId)
121+ settle(w)
122+ }
123+
124+ private handleScriptEvent(name: string, dataJson: string) {
125+ if (name === 'solution') {
126+ this.settleSolve((w) => {
127+ try {
128+ w.resolve(JSON.parse(dataJson) as SolutionData)
129+ } catch (err) {
130+ w.reject(new EngineError(`bad solution payload: ${String(err)}`))
131+ }
132+ })
133+ } else if (name === 'solveError') {
134+ const message =
135+ (JSON.parse(dataJson) as { message?: string }).message ?? 'unknown solver error'
136+ this.settleSolve((w) => w.reject(new EngineError(message)))
137+ }
138+ }
139+}
src/engine/project.tsadded+24−0View file
@@ -0,0 +1,24 @@
1+/** The MATLAB project the solver worker runs, bundled as raw text. */
2+
3+import main from '../../matlab/main.m?raw'
4+import solverSession from '../../matlab/solver_session.m?raw'
5+import solvePde from '../../matlab/solve_pde.m?raw'
6+import surfacemeshFromQuads from '../../matlab/surfacemesh_from_quads.m?raw'
7+import loadGmshQuads from '../../matlab/load_gmsh_quads.m?raw'
8+import placeholder from '../../matlab/placeholder.html?raw'
9+
10+export interface ProjectFile {
11+ path: string
12+ text: string
13+}
14+
15+export const PROJECT_FILES: ProjectFile[] = [
16+ { path: 'main.m', text: main },
17+ { path: 'solver_session.m', text: solverSession },
18+ { path: 'solve_pde.m', text: solvePde },
19+ { path: 'surfacemesh_from_quads.m', text: surfacemeshFromQuads },
20+ { path: 'load_gmsh_quads.m', text: loadGmshQuads },
21+ { path: 'placeholder.html', text: placeholder },
22+]
23+
24+export const MAIN_FILE = 'main.m'
src/engine/protocol.tsadded+28−0View file
@@ -0,0 +1,28 @@
1+/** The app-level solve protocol spoken over the uihtml event bridge. */
2+
3+export interface SolveParams {
4+ pde: 'poisson' | 'helmholtz'
5+ /** RHS f(x,y,z), a MATLAB expression */
6+ f: string
7+ /** zeroth-order coefficient c(x,y,z) (helmholtz only) */
8+ c: string
9+ /** polynomial order per patch */
10+ p: number
11+ /** every mesh edge shared by exactly two quads (from edgeClassification) */
12+ closed: boolean
13+}
14+
15+/** Per-patch solution grids, as packed by matlab/solve_pde.m. */
16+export interface SolutionData {
17+ type: 'solution'
18+ /** points per patch edge (p + 1) */
19+ n: number
20+ npatches: number
21+ x: number[][]
22+ y: number[][]
23+ z: number[][]
24+ u: number[][]
25+ umin: number
26+ umax: number
27+ pde: string
28+}
src/index.cssadded+264−0View file
@@ -0,0 +1,264 @@
1+* {
2+ box-sizing: border-box;
3+}
4+
5+html,
6+body,
7+#root {
8+ height: 100%;
9+ margin: 0;
10+}
11+
12+body {
13+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
14+ font-size: 14px;
15+ color: #1c2330;
16+ background: #f5f6f8;
17+}
18+
19+.app {
20+ display: flex;
21+ flex-direction: column;
22+ height: 100%;
23+}
24+
25+header {
26+ padding: 10px 18px 8px;
27+ border-bottom: 1px solid #dde1e7;
28+ background: #fff;
29+}
30+
31+header h1 {
32+ margin: 0 0 2px;
33+ font-size: 19px;
34+}
35+
36+header p {
37+ margin: 0;
38+ color: #5a6472;
39+}
40+
41+a {
42+ color: #1667c2;
43+}
44+
45+.columns {
46+ display: flex;
47+ flex: 1;
48+ min-height: 0;
49+}
50+
51+aside {
52+ width: 340px;
53+ flex: none;
54+ overflow-y: auto;
55+ padding: 8px 16px 24px;
56+ background: #fff;
57+ border-right: 1px solid #dde1e7;
58+}
59+
60+main {
61+ flex: 1;
62+ min-width: 0;
63+ position: relative;
64+}
65+
66+section {
67+ margin-top: 14px;
68+}
69+
70+section + section {
71+ padding-top: 12px;
72+ border-top: 1px solid #e7eaee;
73+}
74+
75+section h2 {
76+ font-size: 13px;
77+ text-transform: uppercase;
78+ letter-spacing: 0.04em;
79+ color: #5a6472;
80+ margin: 0 0 8px;
81+}
82+
83+.row {
84+ display: flex;
85+ flex-wrap: wrap;
86+ gap: 6px;
87+}
88+
89+button,
90+.button {
91+ font: inherit;
92+ padding: 5px 12px;
93+ border: 1px solid #b9c0ca;
94+ border-radius: 6px;
95+ background: #f2f4f7;
96+ cursor: pointer;
97+ display: inline-block;
98+}
99+
100+button:hover:not(:disabled),
101+.button:hover {
102+ background: #e6eaef;
103+}
104+
105+button:disabled {
106+ opacity: 0.5;
107+ cursor: default;
108+}
109+
110+button.solve {
111+ width: 100%;
112+ padding: 8px;
113+ font-weight: 600;
114+ background: #1667c2;
115+ border-color: #1257a6;
116+ color: #fff;
117+}
118+
119+button.solve:hover:not(:disabled) {
120+ background: #1257a6;
121+}
122+
123+button.linkish {
124+ border: none;
125+ background: none;
126+ padding: 0;
127+ margin-top: 6px;
128+ color: #1667c2;
129+ text-decoration: underline;
130+}
131+
132+.hint {
133+ color: #78818d;
134+ font-size: 12.5px;
135+ margin: 6px 0;
136+}
137+
138+.status {
139+ color: #4a5462;
140+ font-size: 13px;
141+ margin: 8px 0 0;
142+}
143+
144+.error {
145+ color: #b3261e;
146+ font-size: 13px;
147+ margin: 8px 0 0;
148+ white-space: pre-wrap;
149+}
150+
151+.warn {
152+ color: #8a6d00;
153+ font-size: 12.5px;
154+ margin-top: 3px;
155+}
156+
157+.meshinfo {
158+ margin-top: 10px;
159+ padding: 8px 10px;
160+ border: 1px solid #dde1e7;
161+ border-radius: 6px;
162+ background: #f8f9fb;
163+ font-size: 13px;
164+ min-height: 74px;
165+}
166+
167+.meshinfo .status,
168+.meshinfo .error {
169+ margin: 0;
170+}
171+
172+.placeholder {
173+ color: #9aa3af;
174+}
175+
176+.field {
177+ display: block;
178+ margin: 10px 0;
179+}
180+
181+.field > span {
182+ display: block;
183+ font-size: 12.5px;
184+ color: #4a5462;
185+ margin-bottom: 4px;
186+}
187+
188+.field select,
189+.field input[type='text'] {
190+ font: inherit;
191+ width: 100%;
192+ padding: 5px 8px;
193+ border: 1px solid #b9c0ca;
194+ border-radius: 6px;
195+ background: #fff;
196+}
197+
198+.field select:disabled,
199+.field input:disabled {
200+ background: #f2f4f7;
201+ border-color: #d3d8df;
202+ color: #9aa3af;
203+}
204+
205+.field.inactive > span {
206+ color: #9aa3af;
207+}
208+
209+.pde-note {
210+ min-height: 4.4em;
211+}
212+
213+.solve-status {
214+ min-height: 42px;
215+}
216+
217+.field input[type='range'] {
218+ width: 100%;
219+}
220+
221+.preset-row {
222+ display: flex;
223+ gap: 6px;
224+}
225+
226+.preset-row select {
227+ width: 110px;
228+ flex: none;
229+ color: #5a6472;
230+}
231+
232+.preset-row input {
233+ flex: 1;
234+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
235+ font-size: 13px;
236+}
237+
238+details summary {
239+ cursor: pointer;
240+ font-size: 13px;
241+ color: #5a6472;
242+}
243+
244+.console {
245+ max-height: 200px;
246+ overflow-y: auto;
247+ background: #14181f;
248+ color: #c7d0dc;
249+ font-size: 11.5px;
250+ padding: 8px;
251+ border-radius: 6px;
252+ white-space: pre-wrap;
253+ word-break: break-word;
254+}
255+
256+.view-placeholder {
257+ position: absolute;
258+ inset: 0;
259+ display: flex;
260+ align-items: center;
261+ justify-content: center;
262+ color: #9aa3af;
263+ font-size: 15px;
264+}
src/main.tsxadded+10−0View file
@@ -0,0 +1,10 @@
1+import { StrictMode } from 'react'
2+import { createRoot } from 'react-dom/client'
3+import './index.css'
4+import App from './App.tsx'
5+
6+createRoot(document.getElementById('root')!).render(
7+ <StrictMode>
8+ <App />
9+ </StrictMode>,
10+)
src/mesh/bridge.pyadded+111−0View file
@@ -0,0 +1,111 @@
1+# Runs inside Pyodide. Bridges meshio to the JS app.
2+#
3+# parse_quad_mesh reads an uploaded mesh file with meshio, keeps only its
4+# quadrilateral cells, and produces three outputs in Pyodide's in-memory
5+# filesystem: the canonical Gmsh MSH 2.2 ASCII file the numbl solver reads
6+# (full float64 precision), plus float32 positions and uint32 quad indices
7+# for the JS-side preview and connectivity checks. See meshio.ts.
8+
9+import json
10+import os
11+
12+import numpy as np
13+
14+import meshio
15+
16+WORK = "/work"
17+OUT_MSH = WORK + "/out.msh"
18+POSITIONS_F32 = WORK + "/positions.f32"
19+QUADS_U32 = WORK + "/quads.u32"
20+
21+os.makedirs(WORK, exist_ok=True)
22+
23+
24+def _collect_quads(mesh, warnings):
25+ """All quad cells, corner-nodes only; reject meshes without any."""
26+ blocks = []
27+ found = set()
28+ for block in mesh.cells:
29+ data = block.data
30+ if not isinstance(data, np.ndarray) or data.ndim != 2:
31+ continue
32+ found.add(block.type)
33+ if block.type == "quad":
34+ blocks.append(data)
35+ elif block.type in ("quad8", "quad9"):
36+ blocks.append(data[:, :4])
37+ warnings.append(
38+ f"{len(data)} higher-order {block.type} cells reduced to corner nodes"
39+ )
40+ elif block.type == "polygon" and data.shape[1] == 4:
41+ blocks.append(data)
42+ if not blocks:
43+ kinds = ", ".join(sorted(found)) or "none"
44+ raise ValueError(
45+ "No quadrilateral cells found (cell types in file: "
46+ + kinds
47+ + "). surfacefun solves on quad meshes; "
48+ "convert your mesh to quads before uploading."
49+ )
50+ return np.ascontiguousarray(np.vstack(blocks).astype(np.int64))
51+
52+
53+def _write_msh(path, points, quads):
54+ """Canonical Gmsh MSH 2.2 ASCII: sequential ids, type-3 quads, two tags."""
55+ lines = ["$MeshFormat", "2.2 0 8", "$EndMeshFormat", "$Nodes", str(len(points))]
56+ for i, p in enumerate(points, start=1):
57+ lines.append("%d %.16g %.16g %.16g" % (i, p[0], p[1], p[2]))
58+ lines.append("$EndNodes")
59+ lines.append("$Elements")
60+ lines.append(str(len(quads)))
61+ for i, q in enumerate(quads, start=1):
62+ lines.append("%d 3 2 1 1 %d %d %d %d" % (i, q[0] + 1, q[1] + 1, q[2] + 1, q[3] + 1))
63+ lines.append("$EndElements")
64+ with open(path, "w") as f:
65+ f.write("\n".join(lines) + "\n")
66+
67+
68+def parse_quad_mesh(path, file_format=None):
69+ warnings = []
70+ try:
71+ # meshio's read helper exits the interpreter when every candidate
72+ # reader fails; turn that into a normal exception
73+ mesh = meshio.read(path, file_format)
74+ except SystemExit:
75+ raise ValueError(f"Could not read file as {file_format or 'any known format'}")
76+
77+ points = np.asarray(mesh.points, dtype=np.float64)
78+ if points.ndim != 2:
79+ raise ValueError(f"Unexpected points array shape {points.shape}")
80+ if points.shape[1] == 2:
81+ points = np.column_stack([points, np.zeros(len(points))])
82+ warnings.append("2D points: added z=0")
83+ points = np.ascontiguousarray(points[:, :3])
84+
85+ quads = _collect_quads(mesh, warnings)
86+ if quads.size and (int(quads.min()) < 0 or int(quads.max()) >= len(points)):
87+ raise ValueError("Quad node index out of range")
88+
89+ # Drop vertices not referenced by any quad (e.g. triangle-only regions of
90+ # a mixed mesh) so the .msh stays minimal and ids stay dense.
91+ used = np.unique(quads)
92+ if len(used) < len(points):
93+ remap = np.full(len(points), -1, dtype=np.int64)
94+ remap[used] = np.arange(len(used))
95+ points = points[used]
96+ quads = remap[quads]
97+ warnings.append(f"dropped {len(remap) - len(used)} unused vertices")
98+
99+ _write_msh(OUT_MSH, points, quads)
100+ with open(POSITIONS_F32, "wb") as f:
101+ f.write(points.astype(np.float32).tobytes())
102+ with open(QUADS_U32, "wb") as f:
103+ f.write(np.ascontiguousarray(quads.astype(np.uint32)).tobytes())
104+
105+ return json.dumps(
106+ {
107+ "numVertices": len(points),
108+ "numQuads": len(quads),
109+ "warnings": warnings,
110+ }
111+ )
src/mesh/formats.tsadded+32−0View file
@@ -0,0 +1,32 @@
1+/**
2+ * Upload formats the app accepts. All go through meshio (in Pyodide); the
3+ * mesh must contain quadrilateral cells — surfacefun's patches are quads,
4+ * and pure-triangle meshes are rejected with a pointer to why.
5+ */
6+export interface MeshFormat {
7+ id: string // meshio file_format id
8+ label: string
9+ extension: string
10+}
11+
12+export const FORMATS: MeshFormat[] = [
13+ { id: 'gmsh', label: 'Gmsh MSH', extension: '.msh' },
14+ { id: 'vtk', label: 'VTK legacy', extension: '.vtk' },
15+ { id: 'vtu', label: 'VTU (VTK XML)', extension: '.vtu' },
16+ { id: 'obj', label: 'Wavefront OBJ', extension: '.obj' },
17+ { id: 'off', label: 'OFF', extension: '.off' },
18+ { id: 'ply', label: 'PLY', extension: '.ply' },
19+ { id: 'abaqus', label: 'Abaqus', extension: '.inp' },
20+ { id: 'medit', label: 'Medit', extension: '.mesh' },
21+ { id: 'nastran', label: 'Nastran', extension: '.bdf' },
22+ { id: 'avsucd', label: 'AVS-UCD', extension: '.avs' },
23+]
24+
25+export const ACCEPT = FORMATS.map((f) => f.extension).join(',')
26+
27+export function formatForFilename(name: string): MeshFormat | null {
28+ const dot = name.lastIndexOf('.')
29+ if (dot < 0) return null
30+ const ext = name.slice(dot).toLowerCase()
31+ return FORMATS.find((f) => f.extension === ext) ?? null
32+}
src/mesh/meshio.tsadded+107−0View file
@@ -0,0 +1,107 @@
1+/**
2+ * JS side of the meshio bridge (adapted from mesh-converter). Loads Pyodide
3+ * from the script tag in index.html, installs meshio via micropip, and runs
4+ * bridge.py to turn an uploaded mesh file into the app's quad-mesh
5+ * representation — all client-side.
6+ */
7+import bridgeCode from './bridge.py?raw'
8+import type { MeshFormat } from './formats'
9+import type { QuadMeshData } from './quadmesh'
10+
11+const MESHIO_SPEC = 'meshio==5.3.5'
12+
13+const OUT_MSH = '/work/out.msh'
14+const POSITIONS_F32 = '/work/positions.f32'
15+const QUADS_U32 = '/work/quads.u32'
16+
17+interface Pyodide {
18+ runPython(code: string): unknown
19+ loadPackage(names: string[]): Promise<unknown>
20+ pyimport(name: string): { install(spec: string): Promise<void> }
21+ FS: {
22+ writeFile(path: string, data: Uint8Array): void
23+ readFile(path: string): Uint8Array<ArrayBuffer>
24+ unlink(path: string): void
25+ mkdirTree(path: string): void
26+ }
27+}
28+
29+declare global {
30+ // provided by the pyodide.js script tag in index.html
31+ function loadPyodide(options?: { indexURL?: string }): Promise<Pyodide>
32+}
33+
34+export interface ParseResult {
35+ mesh: QuadMeshData
36+ numVertices: number
37+ numQuads: number
38+ warnings: string[]
39+}
40+
41+let initPromise: Promise<Pyodide> | null = null
42+
43+async function doInit(onProgress: (message: string) => void): Promise<Pyodide> {
44+ if (typeof loadPyodide !== 'function') {
45+ throw new Error('Pyodide script failed to load (offline? blocked CDN?)')
46+ }
47+ onProgress('Loading Python runtime (Pyodide)…')
48+ const pyodide = await loadPyodide()
49+ onProgress('Installing meshio…')
50+ await pyodide.loadPackage(['micropip'])
51+ await pyodide.pyimport('micropip').install(MESHIO_SPEC)
52+ pyodide.runPython(bridgeCode)
53+ return pyodide
54+}
55+
56+/** Kick off (or join) the one-time Pyodide + meshio setup. */
57+export function initMeshio(onProgress: (message: string) => void = () => {}): Promise<Pyodide> {
58+ if (!initPromise) initPromise = doInit(onProgress)
59+ return initPromise
60+}
61+
62+/** Last line of a Python traceback, without the exception class name. */
63+function pythonErrorMessage(err: unknown): string {
64+ const raw = err instanceof Error ? err.message : String(err)
65+ const lines = raw
66+ .trim()
67+ .split('\n')
68+ .filter((l) => l.trim())
69+ const last = lines[lines.length - 1] ?? raw
70+ return last.replace(/^[\w.]+(?:Error|Exception|Exit)\s*:\s*/, '')
71+}
72+
73+export async function parseMeshFile(bytes: Uint8Array, format: MeshFormat): Promise<ParseResult> {
74+ const pyodide = await initMeshio()
75+ const inputPath = '/work/input' + format.extension
76+ pyodide.FS.mkdirTree('/work')
77+ pyodide.FS.writeFile(inputPath, bytes)
78+ let infoJson: string
79+ try {
80+ infoJson = String(
81+ pyodide.runPython(
82+ `parse_quad_mesh(${JSON.stringify(inputPath)}, ${JSON.stringify(format.id)})`,
83+ ),
84+ )
85+ } catch (err) {
86+ throw new Error(pythonErrorMessage(err))
87+ } finally {
88+ try {
89+ pyodide.FS.unlink(inputPath)
90+ } catch {
91+ /* not created */
92+ }
93+ }
94+ const info = JSON.parse(infoJson) as {
95+ numVertices: number
96+ numQuads: number
97+ warnings: string[]
98+ }
99+ const posBytes = pyodide.FS.readFile(POSITIONS_F32)
100+ const quadBytes = pyodide.FS.readFile(QUADS_U32)
101+ const mesh: QuadMeshData = {
102+ positions: new Float32Array(posBytes.buffer, posBytes.byteOffset, posBytes.byteLength / 4),
103+ quads: new Uint32Array(quadBytes.buffer, quadBytes.byteOffset, quadBytes.byteLength / 4),
104+ mshBytes: pyodide.FS.readFile(OUT_MSH),
105+ }
106+ return { mesh, ...info }
107+}
src/mesh/quadmesh.tsadded+68−0View file
@@ -0,0 +1,68 @@
1+/** The app's internal quad-mesh representation and connectivity helpers. */
2+
3+export interface QuadMeshData {
4+ /** xyz triples, one per vertex */
5+ positions: Float32Array
6+ /** 4 vertex indices per quad, Gmsh corner order (counterclockwise) */
7+ quads: Uint32Array
8+ /** the canonical Gmsh MSH 2.2 file the solver reads */
9+ mshBytes: Uint8Array
10+}
11+
12+export interface QuadMeshInfo {
13+ numVertices: number
14+ numQuads: number
15+ /** every edge shared by exactly two quads */
16+ closed: boolean
17+ /** some edge shared by more than two quads */
18+ nonManifold: boolean
19+ warnings: string[]
20+}
21+
22+/**
23+ * Classify the mesh from its edge incidence: closed (all edges shared by 2
24+ * quads), open (some boundary edges), or non-manifold (an edge on >2 quads).
25+ */
26+export function edgeClassification(quads: Uint32Array): {
27+ closed: boolean
28+ nonManifold: boolean
29+} {
30+ const counts = new Map<number, number>()
31+ const nq = quads.length / 4
32+ for (let k = 0; k < nq; k++) {
33+ for (let e = 0; e < 4; e++) {
34+ const a = quads[k * 4 + e]
35+ const b = quads[k * 4 + ((e + 1) % 4)]
36+ // 2^26 > any vertex count we accept; safe integer key for the pair
37+ const key = a < b ? a * 67108864 + b : b * 67108864 + a
38+ counts.set(key, (counts.get(key) ?? 0) + 1)
39+ }
40+ }
41+ let closed = true
42+ let nonManifold = false
43+ for (const c of counts.values()) {
44+ if (c !== 2) closed = false
45+ if (c > 2) nonManifold = true
46+ }
47+ return { closed, nonManifold }
48+}
49+
50+/** Axis-aligned bounding box diagonal, for camera framing. */
51+export function meshBounds(positions: Float32Array): {
52+ center: [number, number, number]
53+ size: number
54+} {
55+ let minX = Infinity, minY = Infinity, minZ = Infinity
56+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
57+ for (let i = 0; i < positions.length; i += 3) {
58+ const x = positions[i], y = positions[i + 1], z = positions[i + 2]
59+ if (x < minX) minX = x
60+ if (y < minY) minY = y
61+ if (z < minZ) minZ = z
62+ if (x > maxX) maxX = x
63+ if (y > maxY) maxY = y
64+ if (z > maxZ) maxZ = z
65+ }
66+ const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1
67+ return { center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2], size }
68+}
src/pde/presets.tsadded+65−0View file
@@ -0,0 +1,65 @@
1+/**
2+ * The PDEs the app can solve and preset right-hand sides / coefficients.
3+ * Expressions are MATLAB, elementwise in the surface coordinates x, y, z;
4+ * they are pasted verbatim into the generated solver call (everything runs
5+ * client-side, so this is the user talking to their own interpreter).
6+ */
7+
8+export interface ExprPreset {
9+ label: string
10+ expr: string
11+}
12+
13+export interface PdeDef {
14+ id: 'poisson' | 'helmholtz'
15+ label: string
16+ equation: string
17+ note: string
18+ fPresets: ExprPreset[]
19+ cPresets: ExprPreset[] | null
20+}
21+
22+export const PDES: PdeDef[] = [
23+ {
24+ id: 'poisson',
25+ label: 'Poisson (Laplace–Beltrami)',
26+ equation: 'Δu = f',
27+ note:
28+ 'On a closed surface f is projected to mean zero and the mean-zero ' +
29+ 'solution is returned; on an open surface, u = 0 on the boundary.',
30+ fPresets: [
31+ { label: 'x·y·z', expr: 'x.*y.*z' },
32+ { label: 'sin(3x)·cos(3y)', expr: 'sin(3*x).*cos(3*y)' },
33+ { label: 'tanh(5z)', expr: 'tanh(5*z)' },
34+ { label: 'x', expr: 'x' },
35+ ],
36+ cPresets: null,
37+ },
38+ {
39+ id: 'helmholtz',
40+ label: 'Helmholtz (variable coefficient)',
41+ equation: '(Δ + c)u = f',
42+ note:
43+ 'c may vary over the surface. With c near an eigenvalue of −Δ the ' +
44+ 'problem approaches singular and the solution blows up.',
45+ fPresets: [
46+ { label: 'Constant 1', expr: '1' },
47+ { label: 'x·y·z', expr: 'x.*y.*z' },
48+ { label: 'sin(3x)·cos(3y)', expr: 'sin(3*x).*cos(3*y)' },
49+ ],
50+ cPresets: [
51+ { label: '100·(1 − z)', expr: '100*(1 - z)' },
52+ { label: 'Constant 100', expr: '100' },
53+ { label: '50·(1 + x)', expr: '50*(1 + x)' },
54+ ],
55+ },
56+]
57+
58+/** Above this many quads the in-browser solve becomes unreasonably slow. */
59+export const MAX_QUADS = 4000
60+/** Above this, warn that the solve may take a while. */
61+export const SLOW_QUADS = 1500
62+
63+export const MIN_ORDER = 2
64+export const MAX_ORDER = 10
65+export const DEFAULT_ORDER = 6
src/render/SurfaceView.tsxadded+300−0View file
@@ -0,0 +1,300 @@
1+/**
2+ * The rotatable 3D view (plain three.js, adapted from
3+ * surfacefun-interactive's SurfView): shows the uploaded quad mesh until a
4+ * solution arrives, then the solution colored by u with a colorbar.
5+ * Drag to rotate, scroll to zoom.
6+ */
7+import { useRef, useEffect, type CSSProperties } from 'react'
8+import * as THREE from 'three'
9+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
10+import type { QuadMeshData } from '../mesh/quadmesh'
11+import type { SolutionData } from '../engine/protocol'
12+import { colormapLookup, colormapGradient } from './colormap'
13+
14+export interface ViewContent {
15+ mesh: QuadMeshData | null
16+ solution: SolutionData | null
17+}
18+
19+interface SceneState {
20+ renderer: THREE.WebGLRenderer
21+ scene: THREE.Scene
22+ camera: THREE.OrthographicCamera
23+ controls: OrbitControls
24+ animId: number
25+}
26+
27+// data (x,y,z) -> three (X=x, Y=z, Z=y), so data-z is "up" on screen
28+
29+function clearScene(scene: THREE.Scene) {
30+ const toRemove: THREE.Object3D[] = []
31+ scene.traverse((obj) => {
32+ if (obj instanceof THREE.Mesh || obj instanceof THREE.LineSegments) toRemove.push(obj)
33+ })
34+ for (const obj of toRemove) {
35+ scene.remove(obj)
36+ ;(obj as THREE.Mesh).geometry?.dispose()
37+ }
38+}
39+
40+/** Bounding box across a set of xyz-triple arrays. */
41+function bounds(arrays: ArrayLike<number>[]): { center: [number, number, number]; range: number } {
42+ const min = [Infinity, Infinity, Infinity]
43+ const max = [-Infinity, -Infinity, -Infinity]
44+ for (const a of arrays) {
45+ for (let i = 0; i + 2 < a.length; i += 3) {
46+ for (let d = 0; d < 3; d++) {
47+ const v = a[i + d]
48+ if (v < min[d]) min[d] = v
49+ if (v > max[d]) max[d] = v
50+ }
51+ }
52+ }
53+ const range = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]) || 1
54+ return {
55+ center: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
56+ range,
57+ }
58+}
59+
60+function normalizedPosition(
61+ out: Float32Array,
62+ outIdx: number,
63+ xyz: [number, number, number],
64+ center: [number, number, number],
65+ range: number,
66+) {
67+ out[outIdx] = (xyz[0] - center[0]) / range
68+ out[outIdx + 1] = (xyz[2] - center[2]) / range
69+ out[outIdx + 2] = (xyz[1] - center[1]) / range
70+}
71+
72+function buildMeshPreview(scene: THREE.Scene, mesh: QuadMeshData) {
73+ const { positions, quads } = mesh
74+ const { center, range } = bounds([positions])
75+ const nVerts = positions.length / 3
76+
77+ const pos = new Float32Array(nVerts * 3)
78+ for (let i = 0; i < nVerts; i++) {
79+ normalizedPosition(
80+ pos,
81+ i * 3,
82+ [positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]],
83+ center,
84+ range,
85+ )
86+ }
87+
88+ const indices: number[] = []
89+ const nq = quads.length / 4
90+ for (let k = 0; k < nq; k++) {
91+ const [a, b, c, d] = [quads[k * 4], quads[k * 4 + 1], quads[k * 4 + 2], quads[k * 4 + 3]]
92+ indices.push(a, b, c, a, c, d)
93+ }
94+
95+ const geometry = new THREE.BufferGeometry()
96+ geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3))
97+ geometry.setIndex(indices)
98+ geometry.computeVertexNormals()
99+ scene.add(
100+ new THREE.Mesh(
101+ geometry,
102+ new THREE.MeshPhongMaterial({
103+ color: 0xb8bec9,
104+ flatShading: true,
105+ side: THREE.DoubleSide,
106+ }),
107+ ),
108+ )
109+
110+ // quad edges
111+ const edgePositions: number[] = []
112+ for (let k = 0; k < nq; k++) {
113+ for (let e = 0; e < 4; e++) {
114+ const a = quads[k * 4 + e]
115+ const b = quads[k * 4 + ((e + 1) % 4)]
116+ edgePositions.push(
117+ pos[a * 3], pos[a * 3 + 1], pos[a * 3 + 2],
118+ pos[b * 3], pos[b * 3 + 1], pos[b * 3 + 2],
119+ )
120+ }
121+ }
122+ const edgeGeometry = new THREE.BufferGeometry()
123+ edgeGeometry.setAttribute('position', new THREE.Float32BufferAttribute(edgePositions, 3))
124+ scene.add(
125+ new THREE.LineSegments(
126+ edgeGeometry,
127+ new THREE.LineBasicMaterial({ color: 0x000000, opacity: 0.35, transparent: true }),
128+ ),
129+ )
130+}
131+
132+function buildSolution(scene: THREE.Scene, sol: SolutionData) {
133+ const { n, x, y, z, u, umin, umax } = sol
134+ const flat: number[] = []
135+ for (let k = 0; k < sol.npatches; k++) {
136+ for (let i = 0; i < x[k].length; i++) flat.push(x[k][i], y[k][i], z[k][i])
137+ }
138+ const { center, range } = bounds([flat])
139+ const cRange = umax - umin || 1
140+
141+ for (let k = 0; k < sol.npatches; k++) {
142+ const px = x[k]
143+ const py = y[k]
144+ const pz = z[k]
145+ const pu = u[k]
146+ const nv = px.length // n*n grid, column-major
147+ const pos = new Float32Array(nv * 3)
148+ const col = new Float32Array(nv * 3)
149+ for (let i = 0; i < nv; i++) {
150+ normalizedPosition(pos, i * 3, [px[i], py[i], pz[i]], center, range)
151+ const [r, g, b] = colormapLookup((pu[i] - umin) / cRange)
152+ col[i * 3] = r
153+ col[i * 3 + 1] = g
154+ col[i * 3 + 2] = b
155+ }
156+ const indices: number[] = []
157+ for (let j = 0; j < n - 1; j++) {
158+ for (let i = 0; i < n - 1; i++) {
159+ const a = j * n + i
160+ const b = j * n + i + 1
161+ const c = (j + 1) * n + i
162+ const d = (j + 1) * n + i + 1
163+ indices.push(a, b, c, b, d, c)
164+ }
165+ }
166+ const geometry = new THREE.BufferGeometry()
167+ geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3))
168+ geometry.setAttribute('color', new THREE.BufferAttribute(col, 3))
169+ geometry.setIndex(indices)
170+ geometry.computeVertexNormals()
171+ scene.add(
172+ new THREE.Mesh(
173+ geometry,
174+ new THREE.MeshPhongMaterial({
175+ vertexColors: true,
176+ side: THREE.DoubleSide,
177+ shininess: 10,
178+ }),
179+ ),
180+ )
181+ }
182+}
183+
184+export function SurfaceView({ mesh, solution }: ViewContent) {
185+ const containerRef = useRef<HTMLDivElement>(null)
186+ const stateRef = useRef<SceneState | null>(null)
187+
188+ // Set up the scene once
189+ useEffect(() => {
190+ const container = containerRef.current
191+ if (!container) return
192+
193+ const renderer = new THREE.WebGLRenderer({ antialias: true })
194+ renderer.setPixelRatio(window.devicePixelRatio)
195+ renderer.setClearColor(0xffffff)
196+ container.appendChild(renderer.domElement)
197+
198+ const scene = new THREE.Scene()
199+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 100)
200+ camera.position.set(1.2, 0.8, 1.2)
201+ camera.lookAt(0, 0, 0)
202+
203+ const controls = new OrbitControls(camera, renderer.domElement)
204+ controls.enablePan = false
205+
206+ scene.add(new THREE.AmbientLight(0xffffff, 0.65))
207+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.6)
208+ dirLight.position.set(2, 3, 2)
209+ scene.add(dirLight)
210+
211+ const animId = requestAnimationFrame(function loop() {
212+ controls.update()
213+ renderer.render(scene, camera)
214+ if (stateRef.current) stateRef.current.animId = requestAnimationFrame(loop)
215+ })
216+ stateRef.current = { renderer, scene, camera, controls, animId }
217+
218+ const observer = new ResizeObserver(() => {
219+ const rect = container.getBoundingClientRect()
220+ if (rect.width === 0 || rect.height === 0) return
221+ renderer.setSize(rect.width, rect.height)
222+ const aspect = rect.width / rect.height
223+ const frustumSize = 0.85
224+ camera.left = -frustumSize * aspect
225+ camera.right = frustumSize * aspect
226+ camera.top = frustumSize
227+ camera.bottom = -frustumSize
228+ camera.updateProjectionMatrix()
229+ })
230+ observer.observe(container)
231+
232+ return () => {
233+ observer.disconnect()
234+ cancelAnimationFrame(stateRef.current?.animId ?? animId)
235+ controls.dispose()
236+ renderer.dispose()
237+ container.removeChild(renderer.domElement)
238+ stateRef.current = null
239+ }
240+ }, [])
241+
242+ // Rebuild content when data changes
243+ useEffect(() => {
244+ const st = stateRef.current
245+ if (!st) return
246+ clearScene(st.scene)
247+ if (solution) buildSolution(st.scene, solution)
248+ else if (mesh) buildMeshPreview(st.scene, mesh)
249+ }, [mesh, solution])
250+
251+ return (
252+ <div style={{ position: 'relative', width: '100%', height: '100%' }}>
253+ <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />
254+ {solution && <Colorbar min={solution.umin} max={solution.umax} />}
255+ {!mesh && !solution && (
256+ <div className="view-placeholder">Upload a quad mesh or load a sample to begin</div>
257+ )}
258+ </div>
259+ )
260+}
261+
262+function Colorbar({ min, max }: { min: number; max: number }) {
263+ const fmt = (v: number) => (Number.isInteger(v) ? String(v) : v.toPrecision(3))
264+ const style: CSSProperties = {
265+ position: 'absolute',
266+ top: 12,
267+ bottom: 12,
268+ right: 8,
269+ width: 60,
270+ display: 'flex',
271+ alignItems: 'stretch',
272+ pointerEvents: 'none',
273+ fontSize: 11,
274+ color: '#333',
275+ }
276+ return (
277+ <div style={style}>
278+ <div
279+ style={{
280+ width: 16,
281+ height: '100%',
282+ background: colormapGradient('to top'),
283+ border: '1px solid #999',
284+ boxSizing: 'border-box',
285+ }}
286+ />
287+ <div
288+ style={{
289+ marginLeft: 4,
290+ display: 'flex',
291+ flexDirection: 'column',
292+ justifyContent: 'space-between',
293+ }}
294+ >
295+ <span>{fmt(max)}</span>
296+ <span>{fmt(min)}</span>
297+ </div>
298+ </div>
299+ )
300+}
src/render/colormap.tsadded+42−0View file
@@ -0,0 +1,42 @@
1+/** Parula-inspired colormap (same table surfacefun-interactive uses). */
2+
3+const COLORMAP: [number, number, number][] = [
4+ [0.2422, 0.1504, 0.6603],
5+ [0.281, 0.3228, 0.9579],
6+ [0.1786, 0.5289, 0.9682],
7+ [0.0689, 0.6948, 0.8394],
8+ [0.128, 0.789, 0.5927],
9+ [0.3391, 0.849, 0.3798],
10+ [0.633, 0.8518, 0.2091],
11+ [0.8902, 0.8044, 0.1137],
12+ [0.9905, 0.6816, 0.0235],
13+ [0.9763, 0.517, 0.034],
14+]
15+
16+export function colormapLookup(t: number): [number, number, number] {
17+ const safe = Number.isFinite(t) ? t : 0
18+ const clamped = Math.max(0, Math.min(1, safe))
19+ const idx = clamped * (COLORMAP.length - 1)
20+ const lo = Math.floor(idx)
21+ const hi = Math.min(lo + 1, COLORMAP.length - 1)
22+ const frac = idx - lo
23+ return [
24+ COLORMAP[lo][0] + frac * (COLORMAP[hi][0] - COLORMAP[lo][0]),
25+ COLORMAP[lo][1] + frac * (COLORMAP[hi][1] - COLORMAP[lo][1]),
26+ COLORMAP[lo][2] + frac * (COLORMAP[hi][2] - COLORMAP[lo][2]),
27+ ]
28+}
29+
30+/** CSS gradient stops sampling the colormap, for the colorbar overlay. */
31+export function colormapGradient(direction: string): string {
32+ const stops: string[] = []
33+ const n = 32
34+ for (let i = 0; i < n; i++) {
35+ const t = i / (n - 1)
36+ const [r, g, b] = colormapLookup(t)
37+ stops.push(
38+ `rgb(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)}) ${((t * 100).toFixed(2))}%`,
39+ )
40+ }
41+ return `linear-gradient(${direction}, ${stops.join(',')})`
42+}
tsconfig.app.jsonadded+26−0View file
@@ -0,0 +1,26 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023", "DOM", "WebWorker"],
6+ "module": "esnext",
7+ "types": ["vite/client"],
8+ "allowArbitraryExtensions": true,
9+ "skipLibCheck": true,
10+
11+ /* Bundler mode */
12+ "moduleResolution": "bundler",
13+ "allowImportingTsExtensions": true,
14+ "verbatimModuleSyntax": true,
15+ "moduleDetection": "force",
16+ "noEmit": true,
17+ "jsx": "react-jsx",
18+
19+ /* Linting */
20+ "noUnusedLocals": true,
21+ "noUnusedParameters": true,
22+ "erasableSyntaxOnly": true,
23+ "noFallthroughCasesInSwitch": true
24+ },
25+ "include": ["src"]
26+}
tsconfig.jsonadded+7−0View file
@@ -0,0 +1,7 @@
1+{
2+ "files": [],
3+ "references": [
4+ { "path": "./tsconfig.app.json" },
5+ { "path": "./tsconfig.node.json" }
6+ ]
7+}
tsconfig.node.jsonadded+23−0View file
@@ -0,0 +1,23 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023"],
6+ "types": ["node"],
7+ "skipLibCheck": true,
8+
9+ /* Bundler mode */
10+ "module": "nodenext",
11+ "allowImportingTsExtensions": true,
12+ "verbatimModuleSyntax": true,
13+ "moduleDetection": "force",
14+ "noEmit": true,
15+
16+ /* Linting */
17+ "noUnusedLocals": true,
18+ "noUnusedParameters": true,
19+ "erasableSyntaxOnly": true,
20+ "noFallthroughCasesInSwitch": true
21+ },
22+ "include": ["vite.config.ts"]
23+}
vite.config.tsadded+8−0View file
@@ -0,0 +1,8 @@
1+import { defineConfig } from 'vite'
2+import react from '@vitejs/plugin-react'
3+
4+// https://vite.dev/config/
5+export default defineConfig({
6+ plugins: [react()],
7+ base: './',
8+})