3draft 26.11.5001 → 26.11.11010
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/APP.md +118 -28
- package/README.md +6 -1
- package/dist/camera.js +3 -2
- package/dist/csg.d.ts +2 -0
- package/dist/csg.js +13 -1
- package/dist/gl.d.ts +8 -0
- package/dist/gl.js +517 -0
- package/dist/gpu.d.ts +40 -2
- package/dist/gpu.js +374 -18
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8 -3
- package/dist/instances.d.ts +19 -0
- package/dist/instances.js +41 -0
- package/dist/kernel.js +6 -5
- package/dist/pick.js +3 -2
- package/dist/saw.d.ts +29 -0
- package/dist/saw.js +121 -0
- package/dist/svg.js +1 -1
- package/dist/triangulate.d.ts +4 -0
- package/dist/triangulate.js +72 -0
- package/dist/types.d.ts +4 -0
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +111 -9
- package/package.json +1 -1
package/dist/gpu.js
CHANGED
|
@@ -4,14 +4,148 @@
|
|
|
4
4
|
// buildVertices) need no GPU and are unit tested; createGpu() needs a browser.
|
|
5
5
|
import { dot, norm } from './vec.js';
|
|
6
6
|
import { bounds, faceNormal } from './mesh.js';
|
|
7
|
+
import { triangulate } from './triangulate.js';
|
|
7
8
|
import { makeCamera } from './camera.js';
|
|
8
9
|
import { surfaceOf } from './material.js';
|
|
10
|
+
/** Floats of one mark segment: a (xyz), b (xyz), colour (rgba), width in CSS pixels, one spare. */
|
|
11
|
+
export const MARK_FLOATS = 12;
|
|
12
|
+
const colorOf = (css) => {
|
|
13
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((css ?? '#0b7285').trim());
|
|
14
|
+
const h = m ? (m[1].length === 3 ? [...m[1]].map((c) => c + c).join('') : m[1]) : '0b7285';
|
|
15
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
16
|
+
};
|
|
17
|
+
/** One instance per segment of every polyline of every mark (a zero-length segment is a round point). */
|
|
18
|
+
export const buildMarks = (marks) => {
|
|
19
|
+
let n = 0;
|
|
20
|
+
for (const m of marks)
|
|
21
|
+
for (const l of m.lines)
|
|
22
|
+
n += Math.max(1, l.length - 1);
|
|
23
|
+
const out = new Float32Array(n * MARK_FLOATS);
|
|
24
|
+
let o = 0;
|
|
25
|
+
for (const m of marks) {
|
|
26
|
+
const [r, g, b] = colorOf(m.color);
|
|
27
|
+
for (const l of m.lines) {
|
|
28
|
+
const segs = l.length > 1 ? l.length - 1 : 1;
|
|
29
|
+
for (let i = 0; i < segs; i++) {
|
|
30
|
+
const a = l[i];
|
|
31
|
+
const c = l[Math.min(i + 1, l.length - 1)];
|
|
32
|
+
out.set([a[0], a[1], a[2], c[0], c[1], c[2], r, g, b, m.opacity ?? 1, m.width ?? 2], o);
|
|
33
|
+
o += MARK_FLOATS;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
/** Line-list endpoints (x y z, x y z) of every edge of every face, each pair of vertices once. */
|
|
40
|
+
export const buildEdges = (mesh) => {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const n = mesh.positions.length;
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const f of mesh.faces)
|
|
45
|
+
f.v.forEach((a, i) => {
|
|
46
|
+
const b = f.v[(i + 1) % f.v.length];
|
|
47
|
+
const k = a < b ? a * n + b : b * n + a;
|
|
48
|
+
if (seen.has(k))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(k);
|
|
51
|
+
out.push(...mesh.positions[a], ...mesh.positions[b]);
|
|
52
|
+
});
|
|
53
|
+
return new Float32Array(out);
|
|
54
|
+
};
|
|
55
|
+
const EDGE_LIMIT = 300000; // faces: a bigger scene (a galaxy of super-stars) is drawn without edge lines
|
|
56
|
+
const partEdges = new WeakMap();
|
|
57
|
+
/** The edge lines of a scene: of its parts (kept while a part stays the same Mesh) or of the whole mesh. Empty when the scene is huge. */
|
|
58
|
+
export const sceneEdges = (mesh, parts) => {
|
|
59
|
+
if (parts && parts.length) {
|
|
60
|
+
if (parts.reduce((n, m) => n + m.faces.length, 0) > EDGE_LIMIT)
|
|
61
|
+
return new Float32Array(0);
|
|
62
|
+
const built = parts.map((m) => {
|
|
63
|
+
let e = partEdges.get(m);
|
|
64
|
+
if (!e)
|
|
65
|
+
partEdges.set(m, (e = buildEdges(m)));
|
|
66
|
+
return e;
|
|
67
|
+
});
|
|
68
|
+
const all = new Float32Array(built.reduce((n, e) => n + e.length, 0));
|
|
69
|
+
let at = 0;
|
|
70
|
+
for (const e of built)
|
|
71
|
+
(all.set(e, at), (at += e.length));
|
|
72
|
+
return all;
|
|
73
|
+
}
|
|
74
|
+
return mesh.faces.length > EDGE_LIMIT ? new Float32Array(0) : buildEdges(mesh);
|
|
75
|
+
};
|
|
76
|
+
/** Floats of one glow light: position (3), reach (1), colour times intensity (3), spare (1). */
|
|
77
|
+
export const LIGHT_FLOATS = 8;
|
|
78
|
+
export const MAX_LIGHTS = 16;
|
|
79
|
+
const emitters = new WeakMap();
|
|
80
|
+
/** Every emitting face of a mesh: centre (3), area times emit (1), colour (3), area (1). */
|
|
81
|
+
const emittersOf = (mesh, palette) => {
|
|
82
|
+
const hit = emitters.get(mesh);
|
|
83
|
+
if (hit && hit.palette === palette)
|
|
84
|
+
return hit.data;
|
|
85
|
+
const rows = [];
|
|
86
|
+
for (const face of mesh.faces) {
|
|
87
|
+
const s = surfaceOf(face, palette);
|
|
88
|
+
if (s.emit < 0.05)
|
|
89
|
+
continue;
|
|
90
|
+
const pts = face.v.map((k) => mesh.positions[k]);
|
|
91
|
+
const area = Math.hypot(...(faceNormal(pts))) || 0;
|
|
92
|
+
if (!area)
|
|
93
|
+
continue;
|
|
94
|
+
const c = [0, 1, 2].map((k) => pts.reduce((a, p) => a + p[k], 0) / pts.length);
|
|
95
|
+
const [r, g, b] = colorOf(s.color);
|
|
96
|
+
rows.push(...c, area * s.emit, r, g, b, area);
|
|
97
|
+
}
|
|
98
|
+
const data = new Float32Array(rows);
|
|
99
|
+
emitters.set(mesh, { palette, data });
|
|
100
|
+
return data;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Glow: the emitting faces of a scene act as local light sources. Faces close to each other (one cell of a grid) merge into
|
|
104
|
+
* one light at their weighted centre; the brightest MAX_LIGHTS stay. Each light has a reach that grows with the emitting area.
|
|
105
|
+
* Float32Array, LIGHT_FLOATS per light.
|
|
106
|
+
*/
|
|
107
|
+
export const sceneLights = (mesh, parts, palette) => {
|
|
108
|
+
const list = (parts && parts.length ? parts : [mesh]).map((m) => emittersOf(m, palette)).filter((e) => e.length);
|
|
109
|
+
if (!list.length)
|
|
110
|
+
return new Float32Array(0);
|
|
111
|
+
const lo = [Infinity, Infinity, Infinity];
|
|
112
|
+
const hi = [-Infinity, -Infinity, -Infinity];
|
|
113
|
+
for (const e of list)
|
|
114
|
+
for (let i = 0; i < e.length; i += 8)
|
|
115
|
+
for (let k = 0; k < 3; k++)
|
|
116
|
+
(lo[k] = Math.min(lo[k], e[i + k]), (hi[k] = Math.max(hi[k], e[i + k])));
|
|
117
|
+
const cell = Math.max((Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) / 6) || 1, 1e-3);
|
|
118
|
+
const cells = new Map();
|
|
119
|
+
for (const e of list) {
|
|
120
|
+
for (let i = 0; i < e.length; i += 8) {
|
|
121
|
+
const key = [0, 1, 2].map((k) => Math.floor((e[i + k] - lo[k]) / cell)).join(',');
|
|
122
|
+
const c = cells.get(key) ?? { p: 0, x: 0, y: 0, z: 0, r: 0, g: 0, b: 0, a: 0 };
|
|
123
|
+
const w = e[i + 3];
|
|
124
|
+
c.p += w;
|
|
125
|
+
c.x += e[i] * w;
|
|
126
|
+
c.y += e[i + 1] * w;
|
|
127
|
+
c.z += e[i + 2] * w;
|
|
128
|
+
c.r += e[i + 4] * w;
|
|
129
|
+
c.g += e[i + 5] * w;
|
|
130
|
+
c.b += e[i + 6] * w;
|
|
131
|
+
c.a += e[i + 7];
|
|
132
|
+
cells.set(key, c);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const top = [...cells.values()].sort((a, b) => b.p - a.p).slice(0, MAX_LIGHTS);
|
|
136
|
+
const out = new Float32Array(top.length * LIGHT_FLOATS);
|
|
137
|
+
top.forEach((c, i) => {
|
|
138
|
+
const reach = Math.max(2.5 * Math.sqrt(c.p), 0.05); // a 1 m x 1 m lamp at emit 1 reaches 2.5 m
|
|
139
|
+
out.set([c.x / c.p, c.y / c.p, c.z / c.p, reach, c.r / c.p, c.g / c.p, c.b / c.p, 0], i * LIGHT_FLOATS);
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
9
143
|
const FLOATS = 14; // position(3) normal(3) color(3) material(4: metal, rough, alpha, emit) soft(1)
|
|
10
144
|
/**
|
|
11
145
|
* Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
|
|
12
146
|
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
13
147
|
*/
|
|
14
|
-
export const viewProjection = (cam, near = 0.03, far =
|
|
148
|
+
export const viewProjection = (cam, near = 0.03, far = 1e9) => {
|
|
15
149
|
const kk = cam.k / (cam.size / 2);
|
|
16
150
|
const row = (v, s) => [v[0] * s, v[1] * s, v[2] * s, -dot(v, cam.eye) * s];
|
|
17
151
|
const a = cam.ortho ? 1 / (far - near) : far / (far - near);
|
|
@@ -22,6 +156,21 @@ export const viewProjection = (cam, near = 0.03, far = 1000) => {
|
|
|
22
156
|
rows.forEach((r, i) => r.forEach((v, j) => (m[j * 4 + i] = v)));
|
|
23
157
|
return m;
|
|
24
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Near plane for a scene seen from `cam`: the usual 0.03, unless everything is very far (a galaxy seen from outside, the
|
|
161
|
+
* nearest point thousands of units away), where a near plane that close would leave no depth precision at all.
|
|
162
|
+
*/
|
|
163
|
+
export const nearPlane = (cam, mesh) => {
|
|
164
|
+
const [ex, ey, ez] = cam.eye;
|
|
165
|
+
const [fx, fy, fz] = cam.f;
|
|
166
|
+
let nearest = Infinity;
|
|
167
|
+
for (const p of mesh.positions) {
|
|
168
|
+
const d = (p[0] - ex) * fx + (p[1] - ey) * fy + (p[2] - ez) * fz;
|
|
169
|
+
if (d > 1e-6 && d < nearest)
|
|
170
|
+
nearest = d;
|
|
171
|
+
}
|
|
172
|
+
return nearest > 1000 ? nearest * 0.25 : 0.03;
|
|
173
|
+
};
|
|
25
174
|
const rgb = (color) => {
|
|
26
175
|
const h = String(color ?? '').replace('#', '');
|
|
27
176
|
const full = h.length === 3 ? [...h].map((c) => c + c).join('') : h;
|
|
@@ -34,8 +183,13 @@ const rgb = (color) => {
|
|
|
34
183
|
* crack in the indices does not break it) are averaged, weighted by area, but only those within `crease` degrees of
|
|
35
184
|
* this face: a 90 degree box corner stays sharp, a faceted fuselage becomes round.
|
|
36
185
|
*/
|
|
37
|
-
export const cornerNormals = (mesh, crease) => {
|
|
38
|
-
|
|
186
|
+
export const cornerNormals = (mesh, crease, soft) => {
|
|
187
|
+
// the material's soft decides how far its edges may be smoothed: 0 keeps every edge hard, from 0.5 up the whole crease angle is allowed;
|
|
188
|
+
// between two faces the harder one rules
|
|
189
|
+
const cosAt = (i, j) => {
|
|
190
|
+
const k = soft ? Math.min(1, 2 * Math.min(soft[i], soft[j])) : 1;
|
|
191
|
+
return Math.cos((crease * k * Math.PI) / 180);
|
|
192
|
+
};
|
|
39
193
|
const area = mesh.faces.map((f) => faceNormal(f.v.map((k) => mesh.positions[k]))); // length = area
|
|
40
194
|
const unit = area.map((n) => (Math.hypot(...n) > 0 ? norm(n) : [0, 1, 0]));
|
|
41
195
|
const q = (bounds(mesh).radius || 1) * 1e-6;
|
|
@@ -51,7 +205,9 @@ export const cornerNormals = (mesh, crease) => {
|
|
|
51
205
|
return mesh.faces.map((f, i) => f.v.map((k) => {
|
|
52
206
|
const sum = [0, 0, 0];
|
|
53
207
|
for (const j of at.get(key(mesh.positions[k])) ?? []) {
|
|
54
|
-
if (
|
|
208
|
+
if (i !== j && (soft ? Math.min(soft[i], soft[j]) <= 0 : false))
|
|
209
|
+
continue;
|
|
210
|
+
if (dot(unit[i], unit[j]) < cosAt(i, j) - 1e-12)
|
|
55
211
|
continue;
|
|
56
212
|
for (let c = 0; c < 3; c++)
|
|
57
213
|
sum[c] += area[j][c];
|
|
@@ -68,7 +224,7 @@ export const buildVertices = (mesh, palette, { smooth = 0 } = {}) => {
|
|
|
68
224
|
for (const f of mesh.faces)
|
|
69
225
|
tris += Math.max(0, f.v.length - 2);
|
|
70
226
|
const out = new Float32Array(tris * 3 * FLOATS);
|
|
71
|
-
const corners = smooth > 0 ? cornerNormals(mesh, smooth) : null;
|
|
227
|
+
const corners = smooth > 0 ? cornerNormals(mesh, smooth, mesh.faces.map((f) => surfaceOf(f, palette).soft)) : null;
|
|
72
228
|
let o = 0;
|
|
73
229
|
mesh.faces.forEach((face, fi) => {
|
|
74
230
|
const pts = face.v.map((k) => mesh.positions[k]);
|
|
@@ -76,8 +232,8 @@ export const buildVertices = (mesh, palette, { smooth = 0 } = {}) => {
|
|
|
76
232
|
const s = surfaceOf(face, palette);
|
|
77
233
|
const c = rgb(s.color);
|
|
78
234
|
const mat = [s.metal, s.rough, s.alpha, s.emit];
|
|
79
|
-
for (
|
|
80
|
-
for (const k of
|
|
235
|
+
for (const tri of triangulate(pts)) {
|
|
236
|
+
for (const k of tri) {
|
|
81
237
|
out.set([...pts[k], ...(corners ? corners[fi][k] : flat), ...c, ...mat, s.soft], o);
|
|
82
238
|
o += FLOATS;
|
|
83
239
|
}
|
|
@@ -108,6 +264,9 @@ struct U {
|
|
|
108
264
|
fx : vec4<f32>, // filmic (0 or 1), exposure, emission gain, studio (0 or 1)
|
|
109
265
|
fill : vec4<f32>, // studio: xyz direction towards the fill light, w intensity
|
|
110
266
|
rim : vec4<f32>, // studio: xyz direction towards the rim light, w intensity
|
|
267
|
+
res : vec4<f32>, // marks: canvas pixels (x, y), device pixels per CSS pixel (z); w: number of glow lights
|
|
268
|
+
lpos : array<vec4<f32>, 16>, // glow lights: xyz position, w reach
|
|
269
|
+
lcol : array<vec4<f32>, 16>, // rgb colour times intensity
|
|
111
270
|
};
|
|
112
271
|
@group(0) @binding(0) var<uniform> u : U;
|
|
113
272
|
|
|
@@ -152,6 +311,58 @@ struct IIn {
|
|
|
152
311
|
return o;
|
|
153
312
|
}
|
|
154
313
|
|
|
314
|
+
// the edge lines: 1 pixel, nudged a hair towards the eye so they win against the faces they lie on
|
|
315
|
+
@vertex fn vsl(@location(0) pos : vec3<f32>) -> @builtin(position) vec4<f32> {
|
|
316
|
+
var p = u.vp * vec4<f32>(pos, 1.0);
|
|
317
|
+
p.z = p.z - 0.0002 * p.w;
|
|
318
|
+
return p;
|
|
319
|
+
}
|
|
320
|
+
@fragment fn fsl() -> @location(0) vec4<f32> {
|
|
321
|
+
// dark lines on the lit picture; with no light the picture is dark, so the lines turn light
|
|
322
|
+
let g = select(0.0, 0.85, u.light.w > 0.5);
|
|
323
|
+
return vec4<f32>(g, g, g, select(0.4, 0.55, u.light.w > 0.5));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// the marks: thick round-capped segments, expanded to screen-space quads, drawn over everything (no depth test)
|
|
327
|
+
struct MOut { @builtin(position) p : vec4<f32>, @location(0) q : vec2<f32>, @location(1) c : vec4<f32>, @location(2) h : vec3<f32> };
|
|
328
|
+
@vertex fn vsm(@builtin(vertex_index) vi : u32, @location(0) a : vec3<f32>, @location(1) b : vec3<f32>, @location(2) col : vec4<f32>, @location(3) wd : f32) -> MOut {
|
|
329
|
+
var o : MOut;
|
|
330
|
+
var ca = u.vp * vec4<f32>(a, 1.0);
|
|
331
|
+
var cb = u.vp * vec4<f32>(b, 1.0);
|
|
332
|
+
let e = 0.001;
|
|
333
|
+
o.p = vec4<f32>(2.0, 2.0, 2.0, 1.0);
|
|
334
|
+
if (ca.w < e && cb.w < e) { return o; }
|
|
335
|
+
if (ca.w < e) { ca = mix(ca, cb, (e - ca.w) / (cb.w - ca.w)); }
|
|
336
|
+
if (cb.w < e) { cb = mix(cb, ca, (e - cb.w) / (ca.w - cb.w)); }
|
|
337
|
+
let half = vec2<f32>(u.res.x, u.res.y) * 0.5;
|
|
338
|
+
let pa = ca.xy / ca.w * half;
|
|
339
|
+
let pb = cb.xy / cb.w * half;
|
|
340
|
+
let len = length(pb - pa);
|
|
341
|
+
var d = vec2<f32>(1.0, 0.0);
|
|
342
|
+
if (len > 0.0001) { d = (pb - pa) / len; }
|
|
343
|
+
let hw = wd * u.res.z * 0.5;
|
|
344
|
+
let n = vec2<f32>(-d.y, d.x);
|
|
345
|
+
let cx = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
|
|
346
|
+
let cy = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
|
347
|
+
let t = cx[vi];
|
|
348
|
+
let s = cy[vi];
|
|
349
|
+
let ext = hw + 1.0;
|
|
350
|
+
let along = mix(-ext, len + ext, t);
|
|
351
|
+
let across = s * ext;
|
|
352
|
+
let pos = pa + d * along + n * across;
|
|
353
|
+
o.p = vec4<f32>(pos / half, 0.0, 1.0);
|
|
354
|
+
o.q = vec2<f32>(along, across);
|
|
355
|
+
o.c = col;
|
|
356
|
+
o.h = vec3<f32>(len, hw, 0.0);
|
|
357
|
+
return o;
|
|
358
|
+
}
|
|
359
|
+
@fragment fn fsm(i : MOut) -> @location(0) vec4<f32> {
|
|
360
|
+
let dx = max(max(-i.q.x, i.q.x - i.h.x), 0.0);
|
|
361
|
+
let dist = length(vec2<f32>(dx, i.q.y));
|
|
362
|
+
let cov = clamp(i.h.y + 0.5 - dist, 0.0, 1.0);
|
|
363
|
+
return vec4<f32>(i.c.rgb, i.c.a * cov);
|
|
364
|
+
}
|
|
365
|
+
|
|
155
366
|
fn toLinear(c : vec3<f32>) -> vec3<f32> {
|
|
156
367
|
return select(pow((c + 0.055) / 1.055, vec3<f32>(2.4)), c / 12.92, c <= vec3<f32>(0.04045));
|
|
157
368
|
}
|
|
@@ -186,6 +397,24 @@ fn lights(n : vec3<f32>, v : vec3<f32>, shin : f32, soft : f32) -> vec3<f32> {
|
|
|
186
397
|
return vec3<f32>(diff, spec, scat);
|
|
187
398
|
}
|
|
188
399
|
|
|
400
|
+
// Glow: every emitting cluster is a local light with a smooth falloff to zero at its reach, a little wrap so shade edges soften
|
|
401
|
+
fn glowAt(p : vec3<f32>, n : vec3<f32>) -> vec3<f32> {
|
|
402
|
+
var sum = vec3<f32>(0.0);
|
|
403
|
+
let cnt = u32(u.res.w);
|
|
404
|
+
for (var k = 0u; k < cnt; k = k + 1u) {
|
|
405
|
+
let d = u.lpos[k].xyz - p;
|
|
406
|
+
let dist = max(length(d), 0.001);
|
|
407
|
+
let reach = u.lpos[k].w;
|
|
408
|
+
let x = dist / reach;
|
|
409
|
+
var win = clamp(1.0 - x * x * x * x, 0.0, 1.0);
|
|
410
|
+
win = win * win;
|
|
411
|
+
let s = dist / (reach * 0.25);
|
|
412
|
+
let wrap = max((dot(n, d / dist) + 0.3) / 1.3, 0.0);
|
|
413
|
+
sum = sum + u.lcol[k].rgb * (win / (1.0 + s * s)) * wrap * 1.6;
|
|
414
|
+
}
|
|
415
|
+
return sum;
|
|
416
|
+
}
|
|
417
|
+
|
|
189
418
|
// The filmic path: the same light model, but in linear colour with HDR light and emission, then tone mapped.
|
|
190
419
|
fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
191
420
|
let metal = i.m.x;
|
|
@@ -207,6 +436,7 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
207
436
|
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), albedo, metal);
|
|
208
437
|
// emission adds light, so a lamp can go above 1.0 and still keep its colour after tone mapping
|
|
209
438
|
lit = lit + albedo * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.5 * (1.0 - metal); // soft: the warm scattered band
|
|
439
|
+
lit = lit + albedo * toLinear(vec3<f32>(1.0)) * glowAt(i.w, n) * (1.0 - metal);
|
|
210
440
|
lit = lit + albedo * i.m.w * u.fx.z;
|
|
211
441
|
return vec4<f32>(toSrgb(aces(lit * u.fx.y)), i.m.z);
|
|
212
442
|
}
|
|
@@ -216,6 +446,12 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
216
446
|
if (!front) { n = -n; }
|
|
217
447
|
let l = normalize(u.light.xyz);
|
|
218
448
|
let v = normalize(u.eye.xyz - i.w);
|
|
449
|
+
if (u.light.w > 0.5) {
|
|
450
|
+
// unlit: no light in the scene, only what glows
|
|
451
|
+
let g = glowAt(i.w, n) * (1.0 - i.m.x);
|
|
452
|
+
if (u.fx.x > 0.5) { return vec4<f32>(toSrgb(aces((toLinear(i.c) * (i.m.w * u.fx.z + g)) * u.fx.y)), i.m.z); }
|
|
453
|
+
return vec4<f32>(clamp(i.c * (min(i.m.w, 1.0) + g), vec3<f32>(0.0), vec3<f32>(1.0)), i.m.z);
|
|
454
|
+
}
|
|
219
455
|
if (u.fx.x > 0.5) { return filmic(i, n, l, v); }
|
|
220
456
|
let metal = i.m.x;
|
|
221
457
|
let rough = i.m.y;
|
|
@@ -232,6 +468,7 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
232
468
|
+ i.c * env * metal * (1.0 - 0.6 * rough)
|
|
233
469
|
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), i.c, metal)
|
|
234
470
|
+ i.c * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.25 * (1.0 - metal);
|
|
471
|
+
lit = lit + i.c * glowAt(i.w, n) * (1.0 - metal);
|
|
235
472
|
lit = mix(lit, i.c * 1.15, min(i.m.w, 1.0)); // the plain look: emit 0..1 fades to flat colour, more does not add
|
|
236
473
|
return vec4<f32>(clamp(lit, vec3<f32>(0.0), vec3<f32>(1.0)), i.m.z);
|
|
237
474
|
}
|
|
@@ -316,13 +553,55 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
316
553
|
const glassPipeline = make('vs', [meshLayout], true);
|
|
317
554
|
// the instanced pipeline reads position and normal only from the prototype (locations 0, 1)
|
|
318
555
|
const instPipeline = make('vsi', [{ ...meshLayout, attributes: meshLayout.attributes.slice(0, 2) }, instanceLayout]);
|
|
319
|
-
const
|
|
556
|
+
const linePipeline = device.createRenderPipeline({
|
|
557
|
+
layout: pipelineLayout,
|
|
558
|
+
vertex: { module, entryPoint: 'vsl', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
|
|
559
|
+
fragment: {
|
|
560
|
+
module,
|
|
561
|
+
entryPoint: 'fsl',
|
|
562
|
+
targets: [{ format, blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' } } }],
|
|
563
|
+
},
|
|
564
|
+
primitive: { topology: 'line-list' },
|
|
565
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'less-equal' },
|
|
566
|
+
multisample: { count: sampleCount },
|
|
567
|
+
});
|
|
568
|
+
const markPipeline = device.createRenderPipeline({
|
|
569
|
+
layout: pipelineLayout,
|
|
570
|
+
vertex: {
|
|
571
|
+
module,
|
|
572
|
+
entryPoint: 'vsm',
|
|
573
|
+
buffers: [
|
|
574
|
+
{
|
|
575
|
+
arrayStride: MARK_FLOATS * 4,
|
|
576
|
+
stepMode: 'instance',
|
|
577
|
+
attributes: [
|
|
578
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
579
|
+
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
580
|
+
{ shaderLocation: 2, offset: 24, format: 'float32x4' },
|
|
581
|
+
{ shaderLocation: 3, offset: 40, format: 'float32' },
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
fragment: {
|
|
587
|
+
module,
|
|
588
|
+
entryPoint: 'fsm',
|
|
589
|
+
targets: [{ format, blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' } } }],
|
|
590
|
+
},
|
|
591
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
592
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'always' },
|
|
593
|
+
multisample: { count: sampleCount },
|
|
594
|
+
});
|
|
595
|
+
const uniforms = device.createBuffer({ size: 720, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
320
596
|
const bind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
321
597
|
const instBind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
322
598
|
// Geometry that did not change since the last frame is not rebuilt or uploaded again.
|
|
323
599
|
let uploaded = null; // the mesh now in vertexBuffer
|
|
324
600
|
let uploadedPalette; // and the palette it was built with
|
|
325
601
|
let uploadedSmooth = 0; // and the smoothing
|
|
602
|
+
let partCache = new WeakMap(); // vertices per part mesh (see `parts`)
|
|
603
|
+
let partPalette;
|
|
604
|
+
let partSmooth = 0;
|
|
326
605
|
let vertexCount = 0;
|
|
327
606
|
let protoMesh = null;
|
|
328
607
|
let protoBuffer = null;
|
|
@@ -333,6 +612,15 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
333
612
|
let instCount = 0;
|
|
334
613
|
let vertexBuffer = null;
|
|
335
614
|
let capacity = 0;
|
|
615
|
+
let markBuffer = null;
|
|
616
|
+
let markCapacity = 0;
|
|
617
|
+
let markCount = 0;
|
|
618
|
+
let edgeBuffer = null;
|
|
619
|
+
let edgeCapacity = 0;
|
|
620
|
+
let edgeCount = 0; // vertices (two per line)
|
|
621
|
+
let edgeMesh = null;
|
|
622
|
+
let edgeParts;
|
|
623
|
+
let edgeOn = false;
|
|
336
624
|
let glassBuffer = null; // the see-through triangles, sorted far to near for the eye
|
|
337
625
|
let glassCapacity = 0;
|
|
338
626
|
let glassTris = []; // centroid and offset (in floats) in glassSource
|
|
@@ -353,7 +641,7 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
353
641
|
depth = device.createTexture({ size: [n, n], format: 'depth24plus', sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
354
642
|
color = sampleCount > 1 ? device.createTexture({ size: [n, n], format, sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT }) : null;
|
|
355
643
|
};
|
|
356
|
-
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, tonemap = false, smooth = false, studio = false } = {}, readback = false) => {
|
|
644
|
+
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, parts, tonemap = false, smooth = false, studio = false, edges = false, marks, unlit = false, glow = false } = {}, readback = false) => {
|
|
357
645
|
const L = { ...defaultLight, ...light };
|
|
358
646
|
const filmic = tonemap ? { exposure: 1, emit: 2, ...(tonemap === true ? {} : tonemap) } : null;
|
|
359
647
|
const M = { ...defaultMaterial, ...material };
|
|
@@ -362,23 +650,46 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
362
650
|
const crease = smooth === true ? 40 : smooth || 0;
|
|
363
651
|
if (mesh !== uploaded || palette !== uploadedPalette || crease !== uploadedSmooth) {
|
|
364
652
|
uploadedSmooth = crease;
|
|
365
|
-
|
|
653
|
+
let all;
|
|
654
|
+
if (parts && parts.length) {
|
|
655
|
+
if (palette !== partPalette || crease !== partSmooth) {
|
|
656
|
+
partCache = new WeakMap();
|
|
657
|
+
partPalette = palette;
|
|
658
|
+
partSmooth = crease;
|
|
659
|
+
}
|
|
660
|
+
const built = parts.map((m) => {
|
|
661
|
+
let v = partCache.get(m);
|
|
662
|
+
if (!v)
|
|
663
|
+
partCache.set(m, (v = buildVertices(m, palette, { smooth: crease })));
|
|
664
|
+
return v;
|
|
665
|
+
});
|
|
666
|
+
all = new Float32Array(built.reduce((n, v) => n + v.length, 0));
|
|
667
|
+
let at = 0;
|
|
668
|
+
for (const v of built) {
|
|
669
|
+
all.set(v, at);
|
|
670
|
+
at += v.length;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else
|
|
674
|
+
all = buildVertices(mesh, palette, { smooth: crease });
|
|
366
675
|
// split: solid triangles are drawn first; triangles with alpha < 1 are kept apart and blended afterwards
|
|
367
676
|
const tri = 3 * FLOATS;
|
|
368
|
-
const solid =
|
|
677
|
+
const solid = new Float32Array(all.length);
|
|
678
|
+
let solidLen = 0;
|
|
369
679
|
glassTris = [];
|
|
370
680
|
for (let o = 0; o < all.length; o += tri) {
|
|
371
681
|
if (all[o + 11] < 1) {
|
|
372
682
|
const c = [0, 1, 2].map((k) => (all[o + k] + all[o + FLOATS + k] + all[o + 2 * FLOATS + k]) / 3);
|
|
373
683
|
glassTris.push({ c, at: o });
|
|
374
684
|
}
|
|
375
|
-
else
|
|
376
|
-
|
|
377
|
-
|
|
685
|
+
else {
|
|
686
|
+
solid.set(all.subarray(o, o + tri), solidLen);
|
|
687
|
+
solidLen += tri;
|
|
688
|
+
}
|
|
378
689
|
}
|
|
379
690
|
glassSource = all;
|
|
380
691
|
glassEye = '';
|
|
381
|
-
const verts =
|
|
692
|
+
const verts = solid.subarray(0, solidLen);
|
|
382
693
|
if (verts.byteLength > capacity) {
|
|
383
694
|
vertexBuffer?.destroy();
|
|
384
695
|
capacity = Math.max(verts.byteLength, 4096);
|
|
@@ -390,6 +701,22 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
390
701
|
uploaded = mesh;
|
|
391
702
|
uploadedPalette = palette;
|
|
392
703
|
}
|
|
704
|
+
if (!edges)
|
|
705
|
+
edgeCount = 0;
|
|
706
|
+
else if (mesh !== edgeMesh || parts !== edgeParts || !edgeOn) {
|
|
707
|
+
const e = sceneEdges(mesh, parts);
|
|
708
|
+
if (e.byteLength > edgeCapacity) {
|
|
709
|
+
edgeBuffer?.destroy();
|
|
710
|
+
edgeCapacity = Math.max(e.byteLength, 4096);
|
|
711
|
+
edgeBuffer = device.createBuffer({ size: edgeCapacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
712
|
+
}
|
|
713
|
+
if (e.byteLength)
|
|
714
|
+
device.queue.writeBuffer(edgeBuffer, 0, e);
|
|
715
|
+
edgeCount = e.length / 3;
|
|
716
|
+
edgeMesh = mesh;
|
|
717
|
+
edgeParts = parts;
|
|
718
|
+
}
|
|
719
|
+
edgeOn = edges;
|
|
393
720
|
if (instances?.mesh && instances.mesh !== protoMesh) {
|
|
394
721
|
const v = buildVertices(instances.mesh);
|
|
395
722
|
protoBuffer?.destroy();
|
|
@@ -433,9 +760,9 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
433
760
|
// studio rig, relative to the camera: key upper left in front, fill from the right, rim from behind and above
|
|
434
761
|
const rig = (f, r, up) => norm([0, 1, 2].map((k) => -cam.f[k] * f + cam.r[k] * r + cam.u[k] * up));
|
|
435
762
|
const S = studio ? { fill: 0.4, rim: 1, ...(studio === true ? {} : studio) } : null;
|
|
436
|
-
const u = new Float32Array(
|
|
437
|
-
u.set(viewProjection(cam), 0);
|
|
438
|
-
u.set([...(S ? rig(0.7, -0.5, 0.6) : norm(L.direction)), 0], 16);
|
|
763
|
+
const u = new Float32Array(52 + MAX_LIGHTS * 8);
|
|
764
|
+
u.set(viewProjection(cam, nearPlane(cam, mesh)), 0);
|
|
765
|
+
u.set([...(S ? rig(0.7, -0.5, 0.6) : norm(L.direction)), unlit ? 1 : 0], 16);
|
|
439
766
|
u.set([...L.sky, 0], 20);
|
|
440
767
|
u.set([...L.ground, 0], 24);
|
|
441
768
|
u.set([...cam.eye, 0], 28);
|
|
@@ -443,7 +770,23 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
443
770
|
u.set([filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, S ? 1 : 0], 36);
|
|
444
771
|
u.set([...rig(0.6, 0.8, 0.1), S ? S.fill : 0], 40);
|
|
445
772
|
u.set([...rig(-0.8, 0.3, 0.5), S ? S.rim : 0], 44);
|
|
773
|
+
const lights = glow ? sceneLights(mesh, parts, palette) : new Float32Array(0);
|
|
774
|
+
u.set([px, px, px / size, lights.length / LIGHT_FLOATS], 48);
|
|
775
|
+
for (let k = 0; k < lights.length / LIGHT_FLOATS; k++) {
|
|
776
|
+
u.set(lights.subarray(k * LIGHT_FLOATS, k * LIGHT_FLOATS + 4), 52 + k * 4);
|
|
777
|
+
u.set([...lights.subarray(k * LIGHT_FLOATS + 4, k * LIGHT_FLOATS + 7), 0], 52 + MAX_LIGHTS * 4 + k * 4);
|
|
778
|
+
}
|
|
446
779
|
device.queue.writeBuffer(uniforms, 0, u);
|
|
780
|
+
const markData = marks?.length ? buildMarks(marks) : null;
|
|
781
|
+
markCount = markData ? markData.length / MARK_FLOATS : 0;
|
|
782
|
+
if (markData && markData.byteLength) {
|
|
783
|
+
if (markData.byteLength > markCapacity) {
|
|
784
|
+
markBuffer?.destroy();
|
|
785
|
+
markCapacity = Math.ceil(markData.byteLength * 1.5);
|
|
786
|
+
markBuffer = device.createBuffer({ size: markCapacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
787
|
+
}
|
|
788
|
+
device.queue.writeBuffer(markBuffer, 0, markData);
|
|
789
|
+
}
|
|
447
790
|
const encoder = device.createCommandEncoder();
|
|
448
791
|
const offscreen = readback
|
|
449
792
|
? device.createTexture({ size: [px, px], format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC })
|
|
@@ -474,6 +817,12 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
474
817
|
pass.setVertexBuffer(1, instBuffer);
|
|
475
818
|
pass.draw(protoCount, instCount);
|
|
476
819
|
}
|
|
820
|
+
if (edgeCount) {
|
|
821
|
+
pass.setPipeline(linePipeline);
|
|
822
|
+
pass.setBindGroup(0, bind);
|
|
823
|
+
pass.setVertexBuffer(0, edgeBuffer);
|
|
824
|
+
pass.draw(edgeCount);
|
|
825
|
+
}
|
|
477
826
|
if (glassCount) {
|
|
478
827
|
// after everything solid (and the instances): see-through faces, far to near
|
|
479
828
|
pass.setPipeline(glassPipeline);
|
|
@@ -481,6 +830,12 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
481
830
|
pass.setVertexBuffer(0, glassBuffer);
|
|
482
831
|
pass.draw(glassCount);
|
|
483
832
|
}
|
|
833
|
+
if (markCount) {
|
|
834
|
+
pass.setPipeline(markPipeline);
|
|
835
|
+
pass.setBindGroup(0, bind);
|
|
836
|
+
pass.setVertexBuffer(0, markBuffer);
|
|
837
|
+
pass.draw(6, markCount);
|
|
838
|
+
}
|
|
484
839
|
pass.end();
|
|
485
840
|
if (!offscreen) {
|
|
486
841
|
device.queue.submit([encoder.finish()]);
|
|
@@ -519,6 +874,7 @@ export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
|
519
874
|
const destroy = () => {
|
|
520
875
|
vertexBuffer?.destroy();
|
|
521
876
|
glassBuffer?.destroy();
|
|
877
|
+
markBuffer?.destroy();
|
|
522
878
|
protoBuffer?.destroy();
|
|
523
879
|
instBuffer?.destroy();
|
|
524
880
|
depth?.destroy();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
export declare const name: "3draft";
|
|
2
|
-
export declare const version = "26.11.
|
|
2
|
+
export declare const version = "26.11.11010";
|
|
3
3
|
export type { Vec3, Face, Mesh, Axes, ViewName, ViewSpec, Fit, Camera, Hit, Mark } from './types.js';
|
|
4
4
|
export { box, extrude, merge, rotate, bounds, faceNormal, faceCenter } from './mesh.js';
|
|
5
5
|
export type { BoxOptions } from './mesh.js';
|
|
6
6
|
export { validate } from './check.js';
|
|
7
|
-
export { boolean, union, subtract, intersect, health } from './csg.js';
|
|
7
|
+
export { boolean, union, subtract, intersect, health, splitParts } from './csg.js';
|
|
8
|
+
export { saw, planeThroughLine, planeThroughTriangle, sectionSegments } from './saw.js';
|
|
9
|
+
export type { CutPlane, Sawn } from './saw.js';
|
|
8
10
|
export type { BooleanOp, Health, Part } from './csg.js';
|
|
9
11
|
export type { Validation } from './check.js';
|
|
10
12
|
export { extrudeFace, moveVertices } from './edit.js';
|
|
11
13
|
export { VIEW_ORDER, makeCamera, orbit, project, ray } from './camera.js';
|
|
12
14
|
export { pick, crosshair } from './pick.js';
|
|
15
|
+
export { instantiate, lodLevel } from './instances.js';
|
|
16
|
+
export type { Prototype, Placement } from './instances.js';
|
|
13
17
|
export { standOn } from './place.js';
|
|
14
18
|
export type { StandOptions, Frame } from './place.js';
|
|
15
19
|
export { paintOrder } from './order.js';
|
|
16
20
|
export type { PaintItem } from './order.js';
|
|
17
21
|
export { createGpu, viewProjection, buildVertices, cornerNormals, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
22
|
+
export { createGl } from './gl.js';
|
|
18
23
|
export type { Gpu, GpuLight, GpuMaterial, GpuRenderOptions, Instance, Pixels } from './gpu.js';
|
|
19
24
|
export { renderView, renderViews, renderSheet } from './svg.js';
|
|
20
25
|
export type { RenderOptions } from './svg.js';
|
|
@@ -31,5 +36,8 @@ export interface Info {
|
|
|
31
36
|
export declare const info: () => Info;
|
|
32
37
|
export { initKernel } from './kernel.js';
|
|
33
38
|
export type { Kernel } from './kernel.js';
|
|
34
|
-
export { insertVertex, cutFace, moveVertex, fillLoop, addLine, findLoop, nearestSurface, wireFromSegments, moveVertexSet } from './wire.js';
|
|
39
|
+
export { insertVertex, cutFace, moveVertex, fillLoop, addLine, findLoop, nearestSurface, wireFromSegments, moveVertexSet, reshape, outlineOf } from './wire.js';
|
|
40
|
+
export { triangulate } from './triangulate.js';
|
|
41
|
+
export { buildEdges, buildMarks, MARK_FLOATS, sceneEdges } from './gpu.js';
|
|
42
|
+
export type { Tri } from './triangulate.js';
|
|
35
43
|
export type { Wire } from './wire.js';
|
package/dist/index.js
CHANGED
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
// a target cross that lies on the surface under the cursor. No textures, no
|
|
5
5
|
// shading yet.
|
|
6
6
|
export const name = '3draft';
|
|
7
|
-
export const version = '26.11.
|
|
7
|
+
export const version = '26.11.11010';
|
|
8
8
|
export { box, extrude, merge, rotate, bounds, faceNormal, faceCenter } from './mesh.js';
|
|
9
9
|
export { validate } from './check.js';
|
|
10
|
-
export { boolean, union, subtract, intersect, health } from './csg.js';
|
|
10
|
+
export { boolean, union, subtract, intersect, health, splitParts } from './csg.js';
|
|
11
|
+
export { saw, planeThroughLine, planeThroughTriangle, sectionSegments } from './saw.js';
|
|
11
12
|
export { extrudeFace, moveVertices } from './edit.js';
|
|
12
13
|
export { VIEW_ORDER, makeCamera, orbit, project, ray } from './camera.js';
|
|
13
14
|
export { pick, crosshair } from './pick.js';
|
|
15
|
+
export { instantiate, lodLevel } from './instances.js';
|
|
14
16
|
export { standOn } from './place.js';
|
|
15
17
|
export { paintOrder } from './order.js';
|
|
16
18
|
export { createGpu, viewProjection, buildVertices, cornerNormals, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
19
|
+
export { createGl } from './gl.js';
|
|
17
20
|
export { renderView, renderViews, renderSheet } from './svg.js';
|
|
18
21
|
export { surfaceOf, DEFAULTS as MATERIAL_DEFAULTS } from './material.js';
|
|
19
22
|
export { displayVersion } from './version.js';
|
|
@@ -30,4 +33,6 @@ export const info = () => ({
|
|
|
30
33
|
],
|
|
31
34
|
});
|
|
32
35
|
export { initKernel } from './kernel.js';
|
|
33
|
-
export { insertVertex, cutFace, moveVertex, fillLoop, addLine, findLoop, nearestSurface, wireFromSegments, moveVertexSet } from './wire.js';
|
|
36
|
+
export { insertVertex, cutFace, moveVertex, fillLoop, addLine, findLoop, nearestSurface, wireFromSegments, moveVertexSet, reshape, outlineOf } from './wire.js';
|
|
37
|
+
export { triangulate } from './triangulate.js';
|
|
38
|
+
export { buildEdges, buildMarks, MARK_FLOATS, sceneEdges } from './gpu.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Mesh, Vec3 } from './types.js';
|
|
2
|
+
export interface Prototype {
|
|
3
|
+
lods: Mesh[];
|
|
4
|
+
}
|
|
5
|
+
export interface Placement {
|
|
6
|
+
at: Vec3;
|
|
7
|
+
/** Size of the instance; a record scales each slot on its own ('*' is the default), e.g. a star core and its halo. */
|
|
8
|
+
scale: number | Record<string, number>;
|
|
9
|
+
/** Slot -> palette name. */
|
|
10
|
+
mats?: Record<string, string>;
|
|
11
|
+
/** Turn of the instance about its own centre: [tilt about x, turn about y] in radians (a galaxy disc seen from any side). */
|
|
12
|
+
rot?: [number, number];
|
|
13
|
+
/** Colour of every face (a tint on top of the material). */
|
|
14
|
+
color?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A copy of the prototype mesh, scaled, moved and given its materials. Slots must not share vertices when scaled apart. */
|
|
17
|
+
export declare const instantiate: (proto: Mesh, p: Placement) => Mesh;
|
|
18
|
+
/** Which level of detail to draw for something that is `px` pixels across (its radius); `limits` are the pixel sizes where the next finer level starts. */
|
|
19
|
+
export declare const lodLevel: (px: number, levels: number, limits?: readonly number[]) => number;
|