3draft 26.11.1013 → 26.11.2005

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,7 @@ keeps the framing fixed while it moves.
71
71
  space and picks stay valid. `standOn(mesh, hit, { base, height, color })`
72
72
  returns a prism standing on the picked surface along its normal (height
73
73
  defaults to 3 x base). In the viewer, Space puts one down under the cross and
74
- Backspace takes the last one back. Over empty space Space shoots the prism out along the cursor ray to a random distance, animated. The viewer starts empty (no table) and lays out only what you place. D deletes the prism under the cross. H toggles edges and points. T flies the camera, animated, to the object under the cross and in front of the surface you point at (the whole scene is framed again when T is pressed over nothing, or with Reset view). Prisms get a random wood brown.
74
+ Backspace takes the last one back. Over empty space Space shoots the prism out under the cursor to a random distance, animated, flying exactly along the camera axis (it lies along that axis like a bullet). The viewer starts empty (no table) and lays out only what you place. X deletes the prism under the cross. V is the Spider bar: the first V remembers the surface point under the cross, the second joins the two points with a thin prism (half the base of a shot prism, a darker brown); V over empty space forgets a pending first point. T flies the camera straight to the front of the object under the cross without turning it; T over nothing sets the speed to zero. W A S D R F fly: every press is a speed step along that axis (the speed grows faster than the level: 1, 2.7, 5.6, 9.9 ...) (w w w = speed 3 forward, s s s brings it back to a stop), R and F go up and down. While Shift is held the view turns towards the pointer, faster the farther the pointer is from the middle. H toggles edges and points. T flies the camera, animated, to the object under the cross and in front of the surface you point at (the whole scene is framed again when T is pressed over nothing, or with Reset view). Prisms get a random wood brown. On a phone or tablet the stage fills the screen with no text: drag orbits, and four round translucent buttons do T, Space, D and the SVG/WebGPU switch (the cross starts in the middle and stays where you last touched).
75
75
 
76
76
  ### WebGPU view
77
77
 
@@ -144,16 +144,58 @@ Everything is deterministic, so the same calls give the same SVG.
144
144
  - Edges are the polygon outlines, so hidden edges are not drawn.
145
145
  - Back-face culling assumes closed meshes with CCW faces seen from outside.
146
146
 
147
+ ## TypeScript and the `|>` pipeline operator
148
+
149
+ The engine is written in TypeScript (`src/*.ts`) with the F# style pipeline operator, `value |> fn`
150
+ (see [Pipeline operator great again](https://dev.to/pengeszikra/pipeline-operator-great-again-1cbn)).
151
+ Neither `tsc` nor Node understands `|>` yet, so `npm run build` (`scripts/build.mjs`) first rewrites
152
+ every `|>` into plain calls (`scripts/pipe.mjs`, `a |> f |> g(1)` becomes `g(1)(f(a))`) and then lets the
153
+ TypeScript compiler type check and emit `dist/*.js` and `dist/*.d.ts`.
154
+
155
+ ```ts
156
+ const t1 = mesh.positions[b] |> minus(mesh.positions[a]) |> norm; // the right side is a function
157
+ const dir = spec.dir |> norm |> times(dist) |> plus(center); // curried helpers: plus, minus, times
158
+ ```
159
+
160
+ - The right side must evaluate to a function; put an arrow function in parentheses: `x |> ((v) => v + 1)`.
161
+ - `|>` binds like `>>>` while it is parsed, so wrap a pipeline in parentheses next to `+`, `-` and friends.
162
+ - `dist/` is committed: the viewer, the examples, the MCP server and the npm package use the built JavaScript, so a
163
+ clone runs without a build. `npm run build:check` fails when `dist/` is out of date, `npm test` builds first.
164
+ - The compiler is a dev dependency (`typescript`); `TYPESCRIPT=/path/to/typescript.js` points the build at another copy.
165
+
166
+ ## Develop
167
+
168
+ ```bash
169
+ npm run build # src/*.ts -> dist/ (|> desugared, type checked)
170
+ npm test # builds, then runs every test (dist, examples, mcp, scripts)
171
+ npm run build:check # fails when dist/ is out of date
172
+ npm run kernel # re-assemble kernel/pick.hexaw (needs the hexaw repo next to this one)
173
+ npm run app # the viewer, http://localhost:5173/app/
174
+ ```
175
+
176
+ Edit `src/*.ts`, never `dist/`: it is generated and committed so that a clone runs without a build.
177
+
147
178
  ## Viewer app (repo only, not part of the npm package)
148
179
 
149
180
  ```sh
150
181
  npm run app # http://localhost:5173/app/
151
182
  ```
152
183
 
153
- Drag to rotate the view, double click to reset. G switches between the SVG and the WebGPU view, L turns the light (WebGPU). A
154
- target cross follows the pointer and lies on the surface under it. It is a
184
+ Drag to rotate the view, double click to reset. A target cross follows the pointer and lies on the surface under it.
185
+
186
+ | key | does |
187
+ | --- | --- |
188
+ | `w a s d r f` | fly: forward, left, back, right, up, down; every press is a speed step (an accelerating curve), the opposite key slows down |
189
+ | `Shift` | steer towards the pointer while flying |
190
+ | `Space` | place a prism on the surface (or shoot one into empty space) |
191
+ | `V` | Spider bar: two surface points joined by a thin brown prism; `V` over empty space forgets the first point |
192
+ | `T` | travel to the target (a straight flight); `T` over nothing stops and zeroes the speed |
193
+ | `X`, `Backspace` | delete the prism under the cross, undo |
194
+ | `H`, `G`, `L` | edges, SVG / WebGPU view, light |
195
+
196
+ On a phone the four round buttons do the same. The viewer picks with the HEXAW kernel (the footer shows `pick hexaw`). It is a
155
197
  single `app/index.html` plus a dependency-free static server
156
- (`app/serve.js`, localhost only, serves `app/`, `src/` and `examples/`).
198
+ (`app/serve.js`, localhost only, serves `app/`, `dist/` and `examples/`).
157
199
 
158
200
  ## Versioning
159
201
 
@@ -168,7 +210,7 @@ day * 1000 + build:
168
210
  | `26.11.15.012` | `26.11.15012` |
169
211
 
170
212
  `npm run bump` sets the next version for today's date in `package.json` and
171
- `src/index.js` (`node scripts/bump.js --dry` only prints it).
213
+ `src/index.ts` (`node scripts/bump.js --dry` only prints it).
172
214
 
173
215
  ## Idea
174
216
 
@@ -192,3 +234,22 @@ day * 1000 + build:
192
234
  ## License
193
235
 
194
236
  MIT
237
+
238
+ ## Examples
239
+
240
+ - `examples/table.js`: the table demo (`npm run example`).
241
+ - `examples/bridge.js`: a stone arch bridge of 50 named objects (`npm run bridge`). It writes `examples/bridge.scene.json`, a scene file you can load with the MCP `import` tool (`path: "examples/bridge.scene.json"`).
242
+ - `examples/cessna.js`: a small Cessna as ONE mesh with 9 face colours (single flat material, colour per face): lofted closed parts (fuselage, high wing, tail, struts, gear, spinner, propeller) merged into one mesh. `node examples/cessna.js` checks it and writes `examples/cessna.scene.json` (one object, loadable with the MCP `import` tool) and `examples/out/cessna.svg`.
243
+
244
+ ## HEXAW kernel
245
+
246
+ The picking hot loop (ray against every triangle, Moller-Trumbore, the same arithmetic as `src/pick.ts`) is also written in
247
+ [HEXAW](https://github.com/Pengeszikra/hexaw), the byte-visible WebAssembly assembly dialect:
248
+
249
+ - `kernel/pick.hexaw` is the source (569 bytes of WebAssembly once assembled).
250
+ - `npm run kernel` assembles it with `hexaw-core.js` (found through `HEXAW_CORE`, a checkout next to this repo, or the `hexaw` package) and writes `src/kernel-bytes.ts`, the module as base64 (then `npm run build` carries it into `dist/`). The engine needs neither HEXAW nor a file fetch at runtime. `node scripts/build-kernel.mjs --check` fails when the generated file is out of date.
251
+ - `initKernel()` starts it; `kernel.pick(mesh, camera, x, y)` returns exactly what `pick()` returns. The mesh is copied into the kernel's linear memory once for as long as it is the same object.
252
+ - The viewer uses the kernel as soon as it has started (the footer shows `pick hexaw`), and plain `pick()` before that.
253
+ - Measured on 1200 prisms (7200 faces): 1.7 ms per pick in JavaScript, 0.14 ms with the kernel.
254
+
255
+ The kernel exports its linear memory (`EXPM "memory"`, needs hexaw with JS-compatible memory), so the mesh is written straight into it with `Float64Array` / `Int32Array` views and the distance is read back from address 0; the former `setf`/`seti`/`getf`/`grow`/`size` helper functions are gone.
@@ -0,0 +1,29 @@
1
+ import type { Camera, Fit, Mesh, Vec3, ViewName, ViewSpec } from './types.js';
2
+ export declare const VIEW_ORDER: readonly ViewName[];
3
+ /**
4
+ * `fit` = { center, radius, dist? } overrides the target and framing (perspective views only use `dist`). Use it when the
5
+ * mesh moves (e.g. rotates) so the camera keeps looking at the same spot.
6
+ */
7
+ export declare const makeCamera: (view: ViewName | ViewSpec, mesh: Mesh, size: number, fit?: Fit) => Camera;
8
+ /**
9
+ * Turntable rotation as a camera: the view you get from rotate(mesh, { yaw, pitch })
10
+ * with a fixed camera, but the mesh stays where it is. Picks and marks are then
11
+ * in object space.
12
+ */
13
+ export declare const orbit: ({ yaw, pitch, ortho, name }?: {
14
+ yaw?: number;
15
+ pitch?: number;
16
+ ortho?: boolean;
17
+ name?: string;
18
+ }) => ViewSpec;
19
+ /** Ray through panel pixel (x, y), the inverse of project(): { origin, dir } in world space, dir is a unit vector. */
20
+ export declare const ray: (cam: Camera, x: number, y: number) => {
21
+ origin: Vec3;
22
+ dir: Vec3;
23
+ };
24
+ /** World point -> { x, y } in panel pixels (y down) and depth along the view direction. */
25
+ export declare const project: (cam: Camera, p: Vec3) => {
26
+ x: number;
27
+ y: number;
28
+ depth: number;
29
+ };
package/dist/camera.js ADDED
@@ -0,0 +1,68 @@
1
+ // Four named views, all looking at the mesh bounding-box center.
2
+ // front / side / top are orthographic (same scale), persp is a perspective view.
3
+ // A view can also be a custom spec: { dir, up, ortho, name? } where `dir` is the
4
+ // direction from the target towards the eye.
5
+ import { add, cross, dot, norm, scale, plus, minus, times } from './vec.js';
6
+ import { bounds } from './mesh.js';
7
+ export const VIEW_ORDER = ['front', 'side', 'top', 'persp'];
8
+ const FOV = (35 * Math.PI) / 180;
9
+ const VIEWS = {
10
+ front: { dir: [0, 0, 1], up: [0, 1, 0], ortho: true }, // eye on +z
11
+ side: { dir: [1, 0, 0], up: [0, 1, 0], ortho: true }, // eye on +x
12
+ top: { dir: [0, 1, 0], up: [0, 0, -1], ortho: true }, // eye on +y, -z is screen-up
13
+ persp: { dir: norm([1, 0.8, 1.2]), up: [0, 1, 0], ortho: false },
14
+ };
15
+ /**
16
+ * `fit` = { center, radius, dist? } overrides the target and framing (perspective views only use `dist`). Use it when the
17
+ * mesh moves (e.g. rotates) so the camera keeps looking at the same spot.
18
+ */
19
+ export const makeCamera = (view, mesh, size, fit) => {
20
+ const spec = typeof view === 'string' ? VIEWS[view] : view;
21
+ if (!spec)
22
+ throw new Error(`unknown view: ${view}`);
23
+ const name = typeof view === 'string' ? view : (view.name ?? 'custom');
24
+ const ortho = Boolean(spec.ortho);
25
+ const { center, radius } = fit ?? bounds(mesh);
26
+ const r0 = radius || 1;
27
+ const dist = ortho ? r0 * 4 : (fit?.dist ?? (r0 * 1.25) / Math.sin(FOV / 2)); // fit.dist: eye distance override (a first-person eye sits almost on the center)
28
+ const eye = plus(center)(times(dist)(norm(spec.dir)));
29
+ const f = norm(minus(eye)(center)); // forward
30
+ const r = norm(cross(f, spec.up)); // right
31
+ const u = cross(r, f); // up
32
+ const k = ortho ? ((size / 2) * 0.8) / r0 : size / 2 / Math.tan(FOV / 2);
33
+ return { name, eye, f, r, u, ortho, k, size };
34
+ };
35
+ /**
36
+ * Turntable rotation as a camera: the view you get from rotate(mesh, { yaw, pitch })
37
+ * with a fixed camera, but the mesh stays where it is. Picks and marks are then
38
+ * in object space.
39
+ */
40
+ export const orbit = ({ yaw = 0, pitch = 0, ortho = false, name = 'orbit' } = {}) => {
41
+ const cy = Math.cos(yaw);
42
+ const sy = Math.sin(yaw);
43
+ const cp = Math.cos(pitch);
44
+ const sp = Math.sin(pitch);
45
+ return { name, ortho, dir: [-sy * cp, sp, cy * cp], up: [sy * sp, cp, -cy * sp] };
46
+ };
47
+ /** Ray through panel pixel (x, y), the inverse of project(): { origin, dir } in world space, dir is a unit vector. */
48
+ export const ray = (cam, x, y) => {
49
+ const nx = (x - cam.size / 2) / cam.k;
50
+ const ny = -(y - cam.size / 2) / cam.k;
51
+ const lateral = plus(scale(cam.u, ny))(scale(cam.r, nx));
52
+ return cam.ortho
53
+ ? { origin: add(cam.eye, lateral), dir: cam.f }
54
+ : { origin: cam.eye, dir: norm(plus(lateral)(cam.f)) };
55
+ };
56
+ /** World point -> { x, y } in panel pixels (y down) and depth along the view direction. */
57
+ export const project = (cam, p) => {
58
+ const d = minus(cam.eye)(p);
59
+ const cx = dot(d, cam.r);
60
+ const cy = dot(d, cam.u);
61
+ const depth = dot(d, cam.f);
62
+ const w = cam.ortho ? 1 : depth;
63
+ return {
64
+ x: cam.size / 2 + (cx / w) * cam.k,
65
+ y: cam.size / 2 - (cy / w) * cam.k,
66
+ depth,
67
+ };
68
+ };
@@ -0,0 +1,24 @@
1
+ import type { Mesh } from './types.js';
2
+ export interface Validation {
3
+ vertices: number;
4
+ faces: number;
5
+ /** Every edge is shared by two faces, wound consistently. */
6
+ closed: boolean;
7
+ openEdges: number;
8
+ /** Signed volume; null unless closed. */
9
+ volume: number | null;
10
+ /** Closed, but the faces point inward. */
11
+ inside_out: boolean;
12
+ degenerateFaces: number[];
13
+ nonPlanarFaces: number[];
14
+ nonConvexFaces: number[];
15
+ ok: boolean;
16
+ /** Plain-text findings. */
17
+ notes: string[];
18
+ }
19
+ /**
20
+ * { vertices, faces, closed, openEdges, volume, inside_out, degenerateFaces,
21
+ * nonPlanarFaces, nonConvexFaces, ok, notes }
22
+ * `volume` and `inside_out` only mean something for a closed mesh.
23
+ */
24
+ export declare const validate: (mesh: Mesh) => Validation;
package/dist/check.js ADDED
@@ -0,0 +1,83 @@
1
+ // Model checks an AI can read as text: is the mesh closed, are the faces CCW from
2
+ // outside, planar and convex (picking and the painter assume that).
3
+ import { sub, dot, cross, len, norm } from './vec.js';
4
+ import { faceNormal, bounds } from './mesh.js';
5
+ /**
6
+ * { vertices, faces, closed, openEdges, volume, inside_out, degenerateFaces,
7
+ * nonPlanarFaces, nonConvexFaces, ok, notes }
8
+ * `volume` and `inside_out` only mean something for a closed mesh.
9
+ */
10
+ export const validate = (mesh) => {
11
+ const { positions, faces } = mesh;
12
+ const size = bounds(mesh).radius || 1;
13
+ const degenerate = [];
14
+ const nonPlanar = [];
15
+ const nonConvex = [];
16
+ const directed = new Map();
17
+ let volume = 0;
18
+ faces.forEach((face, i) => {
19
+ const pts = face.v.map((k) => positions[k]);
20
+ const m = pts.length;
21
+ const n = faceNormal(pts);
22
+ const area2 = len(n);
23
+ if (area2 < 1e-12 * size * size) {
24
+ degenerate.push(i);
25
+ return;
26
+ }
27
+ const unit = norm(n);
28
+ if (Math.max(...pts.map((p) => Math.abs(dot(sub(p, pts[0]), unit)))) > 1e-6 * size)
29
+ nonPlanar.push(i);
30
+ for (let j = 0; j < m; j++) {
31
+ const a = pts[j];
32
+ const b = pts[(j + 1) % m];
33
+ const c = pts[(j + 2) % m];
34
+ if (dot(cross(sub(b, a), sub(c, b)), unit) < -1e-9 * size * size) {
35
+ nonConvex.push(i);
36
+ break;
37
+ }
38
+ }
39
+ for (let j = 0; j < m; j++) {
40
+ const key = `${face.v[j]},${face.v[(j + 1) % m]}`;
41
+ directed.set(key, (directed.get(key) ?? 0) + 1);
42
+ }
43
+ for (let j = 1; j < m - 1; j++)
44
+ volume += dot(pts[0], cross(pts[j], pts[j + 1])) / 6;
45
+ });
46
+ let open = 0;
47
+ let repeated = 0;
48
+ for (const [key, count] of directed) {
49
+ const [a, b] = key.split(',');
50
+ if (count > 1)
51
+ repeated += 1;
52
+ if (!directed.has(`${b},${a}`))
53
+ open += 1;
54
+ }
55
+ const closed = faces.length > 0 && open === 0 && repeated === 0;
56
+ const insideOut = closed && volume < 0;
57
+ const notes = [];
58
+ if (degenerate.length)
59
+ notes.push(`degenerate faces (no area): ${degenerate.join(', ')}`);
60
+ if (nonPlanar.length)
61
+ notes.push(`non-planar faces: ${nonPlanar.join(', ')}`);
62
+ if (nonConvex.length)
63
+ notes.push(`non-convex faces (picking and painting assume convex): ${nonConvex.join(', ')}`);
64
+ if (repeated)
65
+ notes.push(`${repeated} edge(s) are used twice in the same direction: faces are wound inconsistently`);
66
+ if (open)
67
+ notes.push(`${open} open edge(s): the mesh is not closed (fine for a surface, e.g. a missing bottom)`);
68
+ if (insideOut)
69
+ notes.push('the faces point inward: reverse the vertex order of every face');
70
+ return {
71
+ vertices: positions.length,
72
+ faces: faces.length,
73
+ closed,
74
+ openEdges: open,
75
+ volume: closed ? Math.round(volume * 1e9) / 1e9 : null,
76
+ inside_out: insideOut,
77
+ degenerateFaces: degenerate,
78
+ nonPlanarFaces: nonPlanar,
79
+ nonConvexFaces: nonConvex,
80
+ ok: !degenerate.length && !nonPlanar.length && !nonConvex.length && !repeated && !insideOut,
81
+ notes,
82
+ };
83
+ };
package/dist/edit.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Mesh, Vec3 } from './types.js';
2
+ /**
3
+ * Extrude a face: it is replaced (same index) by a copy moved `distance` along its
4
+ * normal and scaled by `scale` about its center, and one quad per edge joins the two.
5
+ * distance > 0, scale 1 a prism out of the face
6
+ * distance > 0, scale < 1 a taper (roof, spire, frustum)
7
+ * distance 0, scale < 1 an inset (a panel, a window frame)
8
+ * distance < 0 a dent into the solid (a niche, a door recess)
9
+ * The new quads are appended after the existing faces; `color` recolors the cap and
10
+ * the quads, otherwise they keep the face color.
11
+ * Returns { mesh, cap, sides } with the index of the cap face and the new quad indices.
12
+ */
13
+ export declare const extrudeFace: (mesh: Mesh, index: number, { distance, scale, color }?: {
14
+ distance?: number;
15
+ scale?: number;
16
+ color?: string;
17
+ }) => {
18
+ mesh: Mesh;
19
+ cap: number;
20
+ sides: number[];
21
+ };
22
+ /** Move the vertices at `indices` by `delta` [dx, dy, dz]. Faces keep their vertices, so faces may stop being planar: check with validate(). */
23
+ export declare const moveVertices: (mesh: Mesh, indices: number[], delta: Vec3) => Mesh;
package/dist/edit.js ADDED
@@ -0,0 +1,48 @@
1
+ // Shaping an existing mesh: the topology edits a modeller does by hand.
2
+ // Both functions return a NEW mesh; faces stay CCW from outside and closed meshes stay closed.
3
+ import { add, sub, scale as mul, norm } from './vec.js';
4
+ import { faceNormal, faceCenter } from './mesh.js';
5
+ /**
6
+ * Extrude a face: it is replaced (same index) by a copy moved `distance` along its
7
+ * normal and scaled by `scale` about its center, and one quad per edge joins the two.
8
+ * distance > 0, scale 1 a prism out of the face
9
+ * distance > 0, scale < 1 a taper (roof, spire, frustum)
10
+ * distance 0, scale < 1 an inset (a panel, a window frame)
11
+ * distance < 0 a dent into the solid (a niche, a door recess)
12
+ * The new quads are appended after the existing faces; `color` recolors the cap and
13
+ * the quads, otherwise they keep the face color.
14
+ * Returns { mesh, cap, sides } with the index of the cap face and the new quad indices.
15
+ */
16
+ export const extrudeFace = (mesh, index, { distance = 0, scale = 1, color } = {}) => {
17
+ const face = mesh.faces[index];
18
+ if (!face)
19
+ throw new Error(`no face ${index}: this mesh has ${mesh.faces.length} faces`);
20
+ if (!(scale > 0))
21
+ throw new Error('scale must be positive');
22
+ if (distance === 0 && scale === 1)
23
+ throw new Error('nothing to do: give a distance or a scale other than 1');
24
+ const pts = face.v.map((k) => mesh.positions[k]);
25
+ const n = norm(faceNormal(pts));
26
+ const c = faceCenter(pts);
27
+ const moved = pts.map((p) => add(add(c, mul(sub(p, c), scale)), mul(n, distance)));
28
+ const base = mesh.positions.length;
29
+ const m = pts.length;
30
+ const tint = color ?? face.color;
31
+ const faces = mesh.faces.map((f, i) => (i === index ? { ...f, v: moved.map((_, j) => base + j), color: tint } : f));
32
+ const sides = [];
33
+ for (let i = 0; i < m; i++) {
34
+ sides.push(faces.length);
35
+ faces.push({ v: [face.v[i], face.v[(i + 1) % m], base + ((i + 1) % m), base + i], color: tint });
36
+ }
37
+ return { mesh: { positions: [...mesh.positions, ...moved], faces }, cap: index, sides };
38
+ };
39
+ /** Move the vertices at `indices` by `delta` [dx, dy, dz]. Faces keep their vertices, so faces may stop being planar: check with validate(). */
40
+ export const moveVertices = (mesh, indices, delta) => {
41
+ const set = new Set();
42
+ for (const i of indices) {
43
+ if (!Number.isInteger(i) || i < 0 || i >= mesh.positions.length)
44
+ throw new Error(`vertex ${i} does not exist: this mesh has ${mesh.positions.length} vertices`);
45
+ set.add(i);
46
+ }
47
+ return { ...mesh, positions: mesh.positions.map((p, i) => (set.has(i) ? add(p, delta) : p)) };
48
+ };
package/dist/gpu.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { Axes, Camera, Fit, Mesh, Vec3, ViewName, ViewSpec } from './types.js';
2
+ type Handle = any;
3
+ export interface GpuLight {
4
+ /** Direction towards the light, world space. */
5
+ direction?: Vec3;
6
+ /** Hemisphere ambient colors (rgb 0..1): up and down. */
7
+ sky?: Vec3;
8
+ ground?: Vec3;
9
+ }
10
+ export interface GpuMaterial {
11
+ ambient?: number;
12
+ diffuse?: number;
13
+ specular?: number;
14
+ shininess?: number;
15
+ }
16
+ export interface GpuRenderOptions {
17
+ /** CSS pixels, the canvas becomes size x size (times dpr in device pixels). */
18
+ size?: number;
19
+ fit?: Fit;
20
+ /** Clear color, rgb 0..1. */
21
+ background?: Vec3;
22
+ light?: GpuLight;
23
+ material?: GpuMaterial;
24
+ dpr?: number;
25
+ /** Many copies of one prototype mesh (modelled around the origin), drawn after `mesh`. */
26
+ instances?: {
27
+ mesh: Mesh;
28
+ data: Float32Array;
29
+ };
30
+ }
31
+ /** One copy of a prototype: its position, orthonormal frame and color. */
32
+ export interface Instance {
33
+ at: Vec3;
34
+ axes: Axes;
35
+ color?: string;
36
+ }
37
+ export interface Pixels {
38
+ width: number;
39
+ height: number;
40
+ data: Uint8ClampedArray;
41
+ }
42
+ export interface Gpu {
43
+ render(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): void;
44
+ /** Same frame, drawn offscreen and read back as RGBA (tests, thumbnails). */
45
+ pixels(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): Promise<Pixels>;
46
+ destroy(): void;
47
+ device: Handle;
48
+ adapter: Handle;
49
+ }
50
+ /**
51
+ * Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
52
+ * like camera.project(): the same pixels, plus a depth between near and far.
53
+ */
54
+ export declare const viewProjection: (cam: Camera, near?: number, far?: number) => Float32Array;
55
+ /** Triangle list for the GPU: flat face normals and face colors. Float32Array, 9 floats per vertex. */
56
+ export declare const buildVertices: (mesh: Mesh) => Float32Array;
57
+ /**
58
+ * Per-instance data for drawInstances: one entry { at, axes: [ex, ey, ez], color } per copy of a
59
+ * prototype mesh (modelled around the origin). Float32Array, 15 floats per instance.
60
+ */
61
+ export declare const buildInstances: (items: Instance[]) => Float32Array;
62
+ export declare const defaultLight: Required<GpuLight>;
63
+ export declare const defaultMaterial: Required<GpuMaterial>;
64
+ /**
65
+ * Sets up WebGPU on a canvas. Rejects when WebGPU is not available.
66
+ * Returns { render(mesh, view, options), destroy() }.
67
+ * render options: size (CSS px, the canvas is made size x size), fit, instances { mesh, data } (many copies of one
68
+ * prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
69
+ * light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
70
+ */
71
+ export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount }?: {
72
+ sampleCount?: number;
73
+ }) => Promise<Gpu>;
74
+ export {};