3draft 26.11.1013 → 26.11.3001

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/dist/csg.js ADDED
@@ -0,0 +1,239 @@
1
+ // Boolean operations on closed solids: union, subtract (a - b) and intersect.
2
+ // A BSP tree per solid (after Evan Wallace's csg.js): polygons are cut by the planes of the other solid
3
+ // and the pieces inside or outside are kept, so every result face stays planar and convex, which is what
4
+ // the rest of the engine assumes. The result is welded and its T-junctions are closed, so it is a closed
5
+ // solid again and can go into the next boolean. Faces keep the color and material of the face they came from;
6
+ // a face cut into a solid by the other one takes the color of the cutting face.
7
+ import { sub, dot, cross, len } from './vec.js';
8
+ import { faceNormal, bounds } from './mesh.js';
9
+ import { validate } from './check.js';
10
+ let CSG_EPS = 1e-6; // set per operation, relative to the size of the two solids
11
+ const unit = (v) => {
12
+ const l = len(v) || 1;
13
+ return [v[0] / l, v[1] / l, v[2] / l];
14
+ };
15
+ const polysOf = (mesh) => mesh.faces.flatMap((f) => {
16
+ const pts = f.v.map((k) => mesh.positions[k]);
17
+ const n = unit(faceNormal(pts));
18
+ return len(faceNormal(pts)) < 1e-12 ? [] : [{ pts, n, w: dot(n, pts[0]), color: f.color, mat: f.mat }];
19
+ });
20
+ const flip = (p) => ({ ...p, pts: [...p.pts].reverse(), n: [-p.n[0], -p.n[1], -p.n[2]], w: -p.w });
21
+ /** Cuts `p` by the plane into the four buckets. */
22
+ const splitPoly = (plane, p, cf, cb, front, back) => {
23
+ const COPLANAR = 0;
24
+ const FRONT = 1;
25
+ const BACK = 2;
26
+ const SPANNING = 3;
27
+ let type = 0;
28
+ const types = p.pts.map((q) => {
29
+ const t = dot(plane.n, q) - plane.w;
30
+ const k = t < -CSG_EPS ? BACK : t > CSG_EPS ? FRONT : COPLANAR;
31
+ type |= k;
32
+ return k;
33
+ });
34
+ if (type === COPLANAR)
35
+ (dot(plane.n, p.n) > 0 ? cf : cb).push(p);
36
+ else if (type === FRONT)
37
+ front.push(p);
38
+ else if (type === BACK)
39
+ back.push(p);
40
+ else if (type === SPANNING) {
41
+ const f = [];
42
+ const b = [];
43
+ for (let i = 0; i < p.pts.length; i++) {
44
+ const j = (i + 1) % p.pts.length;
45
+ const ti = types[i];
46
+ const tj = types[j];
47
+ const vi = p.pts[i];
48
+ const vj = p.pts[j];
49
+ if (ti !== BACK)
50
+ f.push(vi);
51
+ if (ti !== FRONT)
52
+ b.push(vi);
53
+ if ((ti | tj) === SPANNING) {
54
+ const t = (plane.w - dot(plane.n, vi)) / dot(plane.n, sub(vj, vi));
55
+ const v = [vi[0] + (vj[0] - vi[0]) * t, vi[1] + (vj[1] - vi[1]) * t, vi[2] + (vj[2] - vi[2]) * t];
56
+ f.push(v);
57
+ b.push(v);
58
+ }
59
+ }
60
+ if (f.length >= 3)
61
+ front.push({ ...p, pts: f });
62
+ if (b.length >= 3)
63
+ back.push({ ...p, pts: b });
64
+ }
65
+ };
66
+ const build = (node, polys) => {
67
+ if (!polys.length)
68
+ return;
69
+ if (!node.plane)
70
+ node.plane = { n: polys[0].n, w: polys[0].w };
71
+ const front = [];
72
+ const back = [];
73
+ for (const p of polys)
74
+ splitPoly(node.plane, p, node.polys, node.polys, front, back);
75
+ if (front.length)
76
+ build((node.front ??= { plane: null, front: null, back: null, polys: [] }), front);
77
+ if (back.length)
78
+ build((node.back ??= { plane: null, front: null, back: null, polys: [] }), back);
79
+ };
80
+ const tree = (polys) => {
81
+ const node = { plane: null, front: null, back: null, polys: [] };
82
+ build(node, polys);
83
+ return node;
84
+ };
85
+ const invert = (node) => {
86
+ node.polys = node.polys.map(flip);
87
+ if (node.plane)
88
+ node.plane = { n: [-node.plane.n[0], -node.plane.n[1], -node.plane.n[2]], w: -node.plane.w };
89
+ invert0(node);
90
+ };
91
+ const invert0 = (node) => {
92
+ if (node.front)
93
+ invert(node.front);
94
+ if (node.back)
95
+ invert(node.back);
96
+ [node.front, node.back] = [node.back, node.front];
97
+ };
98
+ /** The parts of `polys` that are outside the solid of `node`. */
99
+ const clipPolys = (node, polys) => {
100
+ if (!node.plane)
101
+ return [...polys];
102
+ let front = [];
103
+ let back = [];
104
+ for (const p of polys)
105
+ splitPoly(node.plane, p, front, back, front, back);
106
+ front = node.front ? clipPolys(node.front, front) : front;
107
+ back = node.back ? clipPolys(node.back, back) : [];
108
+ return [...front, ...back];
109
+ };
110
+ const clipTo = (node, other) => {
111
+ node.polys = clipPolys(other, node.polys);
112
+ if (node.front)
113
+ clipTo(node.front, other);
114
+ if (node.back)
115
+ clipTo(node.back, other);
116
+ };
117
+ const all = (node) => [...node.polys, ...(node.front ? all(node.front) : []), ...(node.back ? all(node.back) : [])];
118
+ /** Polygons to a welded mesh: shared vertices, no sliver faces, T-junctions closed. */
119
+ const toMesh = (polys, scale) => {
120
+ const cell = scale * 1e-5;
121
+ const ids = new Map();
122
+ const positions = [];
123
+ const weld = (p) => {
124
+ // look in the neighbouring cells too, so a point on a cell border still finds its twin
125
+ const base = p.map((c) => Math.round(c / cell));
126
+ for (let dx = -1; dx <= 1; dx++)
127
+ for (let dy = -1; dy <= 1; dy++)
128
+ for (let dz = -1; dz <= 1; dz++) {
129
+ const hit = ids.get(`${base[0] + dx},${base[1] + dy},${base[2] + dz}`);
130
+ if (hit !== undefined && len(sub(positions[hit], p)) <= cell * 1.5)
131
+ return hit;
132
+ }
133
+ ids.set(base.join(','), positions.length);
134
+ positions.push(p);
135
+ return positions.length - 1;
136
+ };
137
+ let faces = polys.flatMap((p) => {
138
+ const v = [];
139
+ for (const q of p.pts) {
140
+ const k = weld(q);
141
+ if (v[v.length - 1] !== k)
142
+ v.push(k);
143
+ }
144
+ if (v.length > 1 && v[0] === v[v.length - 1])
145
+ v.pop();
146
+ if (v.length < 3)
147
+ return [];
148
+ const n = faceNormal(v.map((k) => positions[k]));
149
+ return len(n) < 1e-9 * scale * scale ? [] : [{ v, color: p.color, mat: p.mat }];
150
+ });
151
+ // T-junctions: a vertex lying on the edge of a neighbouring face gets inserted into that edge
152
+ const used = new Set(faces.flatMap((f) => f.v));
153
+ const verts = [...used];
154
+ if (verts.length * faces.length < 6e7) {
155
+ const tol = scale * 1e-5 * 3;
156
+ faces = faces.map((f) => {
157
+ const out = [];
158
+ for (let i = 0; i < f.v.length; i++) {
159
+ const a = f.v[i];
160
+ const b = f.v[(i + 1) % f.v.length];
161
+ out.push(a);
162
+ const pa = positions[a];
163
+ const pb = positions[b];
164
+ const d = sub(pb, pa);
165
+ const dl = len(d);
166
+ if (dl < tol * 4)
167
+ continue;
168
+ const on = [];
169
+ for (const k of verts) {
170
+ if (k === a || k === b)
171
+ continue;
172
+ const pk = positions[k];
173
+ const t = dot(sub(pk, pa), d) / (dl * dl);
174
+ if (t <= 0 || t >= 1)
175
+ continue;
176
+ if (len(cross(sub(pk, pa), d)) / dl <= tol)
177
+ on.push({ t, k });
178
+ }
179
+ on.sort((x, y) => x.t - y.t);
180
+ for (const { k } of on)
181
+ out.push(k);
182
+ }
183
+ return { ...f, v: out };
184
+ });
185
+ }
186
+ return { positions, faces };
187
+ };
188
+ const closedSolid = (name, m) => {
189
+ const v = validate(m);
190
+ if (!v.closed)
191
+ throw new Error(`boolean needs closed solids: ${name} has ${v.openEdges} open edge(s)`);
192
+ if (v.inside_out)
193
+ throw new Error(`boolean needs outward faces: ${name} is inside out`);
194
+ };
195
+ /**
196
+ * Boolean of two closed solids (outward CCW faces): 'union', 'subtract' (a minus b) or 'intersect'.
197
+ * Throws with a reason when an input is open or inside out.
198
+ */
199
+ export const boolean = (a, b, op) => {
200
+ closedSolid('a', a);
201
+ closedSolid('b', b);
202
+ const sa = bounds(a);
203
+ const sb = bounds(b);
204
+ const scale = Math.max(sa.radius, sb.radius, 1e-9) * 2 + len(sub(sa.center, sb.center));
205
+ CSG_EPS = scale * 1e-9 * 1000;
206
+ const A = tree(polysOf(a));
207
+ const B = tree(polysOf(b));
208
+ if (op === 'union') {
209
+ clipTo(A, B);
210
+ clipTo(B, A);
211
+ invert(B);
212
+ clipTo(B, A);
213
+ invert(B);
214
+ build(A, all(B));
215
+ }
216
+ else if (op === 'subtract') {
217
+ invert(A);
218
+ clipTo(A, B);
219
+ clipTo(B, A);
220
+ invert(B);
221
+ clipTo(B, A);
222
+ invert(B);
223
+ build(A, all(B));
224
+ invert(A);
225
+ }
226
+ else {
227
+ invert(A);
228
+ clipTo(B, A);
229
+ invert(B);
230
+ clipTo(A, B);
231
+ clipTo(B, A);
232
+ build(A, all(B));
233
+ invert(A);
234
+ }
235
+ return toMesh(all(A), scale);
236
+ };
237
+ export const union = (a, b) => boolean(a, b, 'union');
238
+ export const subtract = (a, b) => boolean(a, b, 'subtract');
239
+ export const intersect = (a, b) => boolean(a, b, 'intersect');
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,77 @@
1
+ import type { Palette } from './material.js';
2
+ import type { Axes, Camera, Fit, Mesh, Vec3, ViewName, ViewSpec } from './types.js';
3
+ type Handle = any;
4
+ export interface GpuLight {
5
+ /** Direction towards the light, world space. */
6
+ direction?: Vec3;
7
+ /** Hemisphere ambient colors (rgb 0..1): up and down. */
8
+ sky?: Vec3;
9
+ ground?: Vec3;
10
+ }
11
+ export interface GpuMaterial {
12
+ ambient?: number;
13
+ diffuse?: number;
14
+ specular?: number;
15
+ shininess?: number;
16
+ }
17
+ export interface GpuRenderOptions {
18
+ /** CSS pixels, the canvas becomes size x size (times dpr in device pixels). */
19
+ size?: number;
20
+ fit?: Fit;
21
+ /** Clear color, rgb 0..1. */
22
+ background?: Vec3;
23
+ light?: GpuLight;
24
+ material?: GpuMaterial;
25
+ /** Named materials the faces point at (`face.mat`); pass a new object when an entry changes. */
26
+ palette?: Palette;
27
+ dpr?: number;
28
+ /** Many copies of one prototype mesh (modelled around the origin), drawn after `mesh`. */
29
+ instances?: {
30
+ mesh: Mesh;
31
+ data: Float32Array;
32
+ };
33
+ }
34
+ /** One copy of a prototype: its position, orthonormal frame and color. */
35
+ export interface Instance {
36
+ at: Vec3;
37
+ axes: Axes;
38
+ color?: string;
39
+ }
40
+ export interface Pixels {
41
+ width: number;
42
+ height: number;
43
+ data: Uint8ClampedArray;
44
+ }
45
+ export interface Gpu {
46
+ render(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): void;
47
+ /** Same frame, drawn offscreen and read back as RGBA (tests, thumbnails). */
48
+ pixels(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): Promise<Pixels>;
49
+ destroy(): void;
50
+ device: Handle;
51
+ adapter: Handle;
52
+ }
53
+ /**
54
+ * Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
55
+ * like camera.project(): the same pixels, plus a depth between near and far.
56
+ */
57
+ export declare const viewProjection: (cam: Camera, near?: number, far?: number) => Float32Array;
58
+ /** Triangle list for the GPU: flat face normals, face colors and materials. Float32Array, 13 floats per vertex. */
59
+ export declare const buildVertices: (mesh: Mesh, palette?: Palette) => Float32Array;
60
+ /**
61
+ * Per-instance data for drawInstances: one entry { at, axes: [ex, ey, ez], color } per copy of a
62
+ * prototype mesh (modelled around the origin). Float32Array, 15 floats per instance.
63
+ */
64
+ export declare const buildInstances: (items: Instance[]) => Float32Array;
65
+ export declare const defaultLight: Required<GpuLight>;
66
+ export declare const defaultMaterial: Required<GpuMaterial>;
67
+ /**
68
+ * Sets up WebGPU on a canvas. Rejects when WebGPU is not available.
69
+ * Returns { render(mesh, view, options), destroy() }.
70
+ * render options: size (CSS px, the canvas is made size x size), fit, instances { mesh, data } (many copies of one
71
+ * prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
72
+ * light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
73
+ */
74
+ export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount }?: {
75
+ sampleCount?: number;
76
+ }) => Promise<Gpu>;
77
+ export {};