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 +66 -5
- package/dist/camera.d.ts +29 -0
- package/dist/camera.js +68 -0
- package/dist/check.d.ts +24 -0
- package/dist/check.js +83 -0
- package/dist/edit.d.ts +23 -0
- package/dist/edit.js +48 -0
- package/dist/gpu.d.ts +74 -0
- package/dist/gpu.js +319 -0
- package/dist/index.d.ts +29 -0
- package/{src → dist}/index.js +12 -14
- package/dist/kernel-bytes.d.ts +1 -0
- package/dist/kernel-bytes.js +2 -0
- package/dist/kernel.d.ts +14 -0
- package/dist/kernel.js +81 -0
- package/dist/mesh.d.ts +40 -0
- package/dist/mesh.js +130 -0
- package/dist/order.d.ts +22 -0
- package/dist/order.js +125 -0
- package/dist/pick.d.ts +18 -0
- package/dist/pick.js +82 -0
- package/dist/place.d.ts +23 -0
- package/{src → dist}/place.js +7 -13
- package/dist/svg.d.ts +28 -0
- package/dist/svg.js +126 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +2 -0
- package/dist/vec.d.ts +14 -0
- package/dist/vec.js +22 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/package.json +17 -9
- package/src/camera.js +0 -75
- package/src/check.js +0 -79
- package/src/edit.js +0 -47
- package/src/gpu.js +0 -252
- package/src/index.d.ts +0 -209
- package/src/mesh.js +0 -127
- package/src/order.js +0 -94
- package/src/pick.js +0 -83
- package/src/svg.js +0 -119
- package/src/vec.js +0 -16
- package/src/version.js +0 -8
package/src/camera.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
import { add, cross, dot, norm, scale, sub } from './vec.js';
|
|
7
|
-
import { bounds } from './mesh.js';
|
|
8
|
-
|
|
9
|
-
export const VIEW_ORDER = ['front', 'side', 'top', 'persp'];
|
|
10
|
-
|
|
11
|
-
const FOV = (35 * Math.PI) / 180;
|
|
12
|
-
|
|
13
|
-
const VIEWS = {
|
|
14
|
-
front: { dir: [0, 0, 1], up: [0, 1, 0], ortho: true }, // eye on +z
|
|
15
|
-
side: { dir: [1, 0, 0], up: [0, 1, 0], ortho: true }, // eye on +x
|
|
16
|
-
top: { dir: [0, 1, 0], up: [0, 0, -1], ortho: true }, // eye on +y, -z is screen-up
|
|
17
|
-
persp: { dir: norm([1, 0.8, 1.2]), up: [0, 1, 0], ortho: false },
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* `fit` = { center, radius } overrides the target and framing. Use it when the
|
|
22
|
-
* mesh moves (e.g. rotates) so the camera keeps looking at the same spot.
|
|
23
|
-
*/
|
|
24
|
-
export const makeCamera = (view, mesh, size, fit) => {
|
|
25
|
-
const spec = typeof view === 'string' ? VIEWS[view] : view;
|
|
26
|
-
if (!spec) throw new Error(`unknown view: ${view}`);
|
|
27
|
-
const name = typeof view === 'string' ? view : (view.name ?? 'custom');
|
|
28
|
-
const ortho = Boolean(spec.ortho);
|
|
29
|
-
const { center, radius } = fit ?? bounds(mesh);
|
|
30
|
-
const r0 = radius || 1;
|
|
31
|
-
const dist = ortho ? r0 * 4 : (r0 * 1.25) / Math.sin(FOV / 2);
|
|
32
|
-
const eye = add(center, scale(norm(spec.dir), dist));
|
|
33
|
-
const f = norm(sub(center, eye)); // forward
|
|
34
|
-
const r = norm(cross(f, spec.up)); // right
|
|
35
|
-
const u = cross(r, f); // up
|
|
36
|
-
const k = ortho ? ((size / 2) * 0.8) / r0 : size / 2 / Math.tan(FOV / 2);
|
|
37
|
-
return { name, eye, f, r, u, ortho, k, size };
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Turntable rotation as a camera: the view you get from rotate(mesh, { yaw, pitch })
|
|
42
|
-
* with a fixed camera, but the mesh stays where it is. Picks and marks are then
|
|
43
|
-
* in object space.
|
|
44
|
-
*/
|
|
45
|
-
export const orbit = ({ yaw = 0, pitch = 0, ortho = false, name = 'orbit' } = {}) => {
|
|
46
|
-
const cy = Math.cos(yaw);
|
|
47
|
-
const sy = Math.sin(yaw);
|
|
48
|
-
const cp = Math.cos(pitch);
|
|
49
|
-
const sp = Math.sin(pitch);
|
|
50
|
-
return { name, ortho, dir: [-sy * cp, sp, cy * cp], up: [sy * sp, cp, -cy * sp] };
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** Ray through panel pixel (x, y), the inverse of project(): { origin, dir } in world space, dir is a unit vector. */
|
|
54
|
-
export const ray = (cam, x, y) => {
|
|
55
|
-
const nx = (x - cam.size / 2) / cam.k;
|
|
56
|
-
const ny = -(y - cam.size / 2) / cam.k;
|
|
57
|
-
const lateral = add(scale(cam.r, nx), scale(cam.u, ny));
|
|
58
|
-
return cam.ortho
|
|
59
|
-
? { origin: add(cam.eye, lateral), dir: cam.f }
|
|
60
|
-
: { origin: cam.eye, dir: norm(add(cam.f, lateral)) };
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/** World point -> { x, y } in panel pixels (y down) and depth along the view direction. */
|
|
64
|
-
export const project = (cam, p) => {
|
|
65
|
-
const d = sub(p, cam.eye);
|
|
66
|
-
const cx = dot(d, cam.r);
|
|
67
|
-
const cy = dot(d, cam.u);
|
|
68
|
-
const depth = dot(d, cam.f);
|
|
69
|
-
const w = cam.ortho ? 1 : depth;
|
|
70
|
-
return {
|
|
71
|
-
x: cam.size / 2 + (cx / w) * cam.k,
|
|
72
|
-
y: cam.size / 2 - (cy / w) * cam.k,
|
|
73
|
-
depth,
|
|
74
|
-
};
|
|
75
|
-
};
|
package/src/check.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
|
|
4
|
-
import { sub, dot, cross, len, norm } from './vec.js';
|
|
5
|
-
import { faceNormal, bounds } from './mesh.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* { vertices, faces, closed, openEdges, volume, inside_out, degenerateFaces,
|
|
9
|
-
* nonPlanarFaces, nonConvexFaces, ok, notes }
|
|
10
|
-
* `volume` and `inside_out` only mean something for a closed mesh.
|
|
11
|
-
*/
|
|
12
|
-
export const validate = (mesh) => {
|
|
13
|
-
const { positions, faces } = mesh;
|
|
14
|
-
const size = bounds(mesh).radius || 1;
|
|
15
|
-
const degenerate = [];
|
|
16
|
-
const nonPlanar = [];
|
|
17
|
-
const nonConvex = [];
|
|
18
|
-
const directed = new Map();
|
|
19
|
-
let volume = 0;
|
|
20
|
-
|
|
21
|
-
faces.forEach((face, i) => {
|
|
22
|
-
const pts = face.v.map((k) => positions[k]);
|
|
23
|
-
const m = pts.length;
|
|
24
|
-
const n = faceNormal(pts);
|
|
25
|
-
const area2 = len(n);
|
|
26
|
-
if (area2 < 1e-12 * size * size) {
|
|
27
|
-
degenerate.push(i);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const unit = norm(n);
|
|
31
|
-
if (Math.max(...pts.map((p) => Math.abs(dot(sub(p, pts[0]), unit)))) > 1e-6 * size) nonPlanar.push(i);
|
|
32
|
-
for (let j = 0; j < m; j++) {
|
|
33
|
-
const a = pts[j];
|
|
34
|
-
const b = pts[(j + 1) % m];
|
|
35
|
-
const c = pts[(j + 2) % m];
|
|
36
|
-
if (dot(cross(sub(b, a), sub(c, b)), unit) < -1e-9 * size * size) {
|
|
37
|
-
nonConvex.push(i);
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
for (let j = 0; j < m; j++) {
|
|
42
|
-
const key = `${face.v[j]},${face.v[(j + 1) % m]}`;
|
|
43
|
-
directed.set(key, (directed.get(key) ?? 0) + 1);
|
|
44
|
-
}
|
|
45
|
-
for (let j = 1; j < m - 1; j++) volume += dot(pts[0], cross(pts[j], pts[j + 1])) / 6;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
let open = 0;
|
|
49
|
-
let repeated = 0;
|
|
50
|
-
for (const [key, count] of directed) {
|
|
51
|
-
const [a, b] = key.split(',');
|
|
52
|
-
if (count > 1) repeated += 1;
|
|
53
|
-
if (!directed.has(`${b},${a}`)) open += 1;
|
|
54
|
-
}
|
|
55
|
-
const closed = faces.length > 0 && open === 0 && repeated === 0;
|
|
56
|
-
const insideOut = closed && volume < 0;
|
|
57
|
-
|
|
58
|
-
const notes = [];
|
|
59
|
-
if (degenerate.length) notes.push(`degenerate faces (no area): ${degenerate.join(', ')}`);
|
|
60
|
-
if (nonPlanar.length) notes.push(`non-planar faces: ${nonPlanar.join(', ')}`);
|
|
61
|
-
if (nonConvex.length) notes.push(`non-convex faces (picking and painting assume convex): ${nonConvex.join(', ')}`);
|
|
62
|
-
if (repeated) notes.push(`${repeated} edge(s) are used twice in the same direction: faces are wound inconsistently`);
|
|
63
|
-
if (open) notes.push(`${open} open edge(s): the mesh is not closed (fine for a surface, e.g. a missing bottom)`);
|
|
64
|
-
if (insideOut) notes.push('the faces point inward: reverse the vertex order of every face');
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
vertices: positions.length,
|
|
68
|
-
faces: faces.length,
|
|
69
|
-
closed,
|
|
70
|
-
openEdges: open,
|
|
71
|
-
volume: closed ? Math.round(volume * 1e9) / 1e9 : null,
|
|
72
|
-
inside_out: insideOut,
|
|
73
|
-
degenerateFaces: degenerate,
|
|
74
|
-
nonPlanarFaces: nonPlanar,
|
|
75
|
-
nonConvexFaces: nonConvex,
|
|
76
|
-
ok: !degenerate.length && !nonPlanar.length && !nonConvex.length && !repeated && !insideOut,
|
|
77
|
-
notes,
|
|
78
|
-
};
|
|
79
|
-
};
|
package/src/edit.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
|
|
4
|
-
import { add, sub, scale as mul, norm } from './vec.js';
|
|
5
|
-
import { faceNormal, faceCenter } from './mesh.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Extrude a face: it is replaced (same index) by a copy moved `distance` along its
|
|
9
|
-
* normal and scaled by `scale` about its center, and one quad per edge joins the two.
|
|
10
|
-
* distance > 0, scale 1 a prism out of the face
|
|
11
|
-
* distance > 0, scale < 1 a taper (roof, spire, frustum)
|
|
12
|
-
* distance 0, scale < 1 an inset (a panel, a window frame)
|
|
13
|
-
* distance < 0 a dent into the solid (a niche, a door recess)
|
|
14
|
-
* The new quads are appended after the existing faces; `color` recolors the cap and
|
|
15
|
-
* the quads, otherwise they keep the face color.
|
|
16
|
-
* Returns { mesh, cap, sides } with the index of the cap face and the new quad indices.
|
|
17
|
-
*/
|
|
18
|
-
export const extrudeFace = (mesh, index, { distance = 0, scale = 1, color } = {}) => {
|
|
19
|
-
const face = mesh.faces[index];
|
|
20
|
-
if (!face) throw new Error(`no face ${index}: this mesh has ${mesh.faces.length} faces`);
|
|
21
|
-
if (!(scale > 0)) throw new Error('scale must be positive');
|
|
22
|
-
if (distance === 0 && scale === 1) throw new Error('nothing to do: give a distance or a scale other than 1');
|
|
23
|
-
const pts = face.v.map((k) => mesh.positions[k]);
|
|
24
|
-
const n = norm(faceNormal(pts));
|
|
25
|
-
const c = faceCenter(pts);
|
|
26
|
-
const moved = pts.map((p) => add(add(c, mul(sub(p, c), scale)), mul(n, distance)));
|
|
27
|
-
const base = mesh.positions.length;
|
|
28
|
-
const m = pts.length;
|
|
29
|
-
const tint = color ?? face.color;
|
|
30
|
-
const faces = mesh.faces.map((f, i) => (i === index ? { ...f, v: moved.map((_, j) => base + j), color: tint } : f));
|
|
31
|
-
const sides = [];
|
|
32
|
-
for (let i = 0; i < m; i++) {
|
|
33
|
-
sides.push(faces.length);
|
|
34
|
-
faces.push({ v: [face.v[i], face.v[(i + 1) % m], base + ((i + 1) % m), base + i], color: tint });
|
|
35
|
-
}
|
|
36
|
-
return { mesh: { positions: [...mesh.positions, ...moved], faces }, cap: index, sides };
|
|
37
|
-
};
|
|
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) throw new Error(`vertex ${i} does not exist: this mesh has ${mesh.positions.length} vertices`);
|
|
44
|
-
set.add(i);
|
|
45
|
-
}
|
|
46
|
-
return { ...mesh, positions: mesh.positions.map((p, i) => (set.has(i) ? add(p, delta) : p)) };
|
|
47
|
-
};
|
package/src/gpu.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
// WebGPU renderer: flat-colored faces, one simple lit material, one global light.
|
|
2
|
-
// It draws the same scene through the same camera as the SVG renderer, so picking,
|
|
3
|
-
// the cross marks and the framing all stay valid. The pure helpers (viewProjection,
|
|
4
|
-
// buildVertices) need no GPU and are unit tested; createGpu() needs a browser.
|
|
5
|
-
|
|
6
|
-
import { sub, dot, norm } from './vec.js';
|
|
7
|
-
import { faceNormal } from './mesh.js';
|
|
8
|
-
import { makeCamera } from './camera.js';
|
|
9
|
-
|
|
10
|
-
const FLOATS = 9; // position(3) normal(3) color(3)
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
|
|
14
|
-
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
15
|
-
*/
|
|
16
|
-
export const viewProjection = (cam, near = 0.1, far = 1000) => {
|
|
17
|
-
const kk = cam.k / (cam.size / 2);
|
|
18
|
-
const row = (v, s) => [v[0] * s, v[1] * s, v[2] * s, -dot(v, cam.eye) * s];
|
|
19
|
-
const a = cam.ortho ? 1 / (far - near) : far / (far - near);
|
|
20
|
-
const zr = row(cam.f, a);
|
|
21
|
-
zr[3] -= a * near;
|
|
22
|
-
const rows = [row(cam.r, kk), row(cam.u, kk), zr, cam.ortho ? [0, 0, 0, 1] : row(cam.f, 1)];
|
|
23
|
-
const m = new Float32Array(16);
|
|
24
|
-
rows.forEach((r, i) => r.forEach((v, j) => (m[j * 4 + i] = v)));
|
|
25
|
-
return m;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const rgb = (color) => {
|
|
29
|
-
const h = String(color ?? '').replace('#', '');
|
|
30
|
-
const full = h.length === 3 ? [...h].map((c) => c + c).join('') : h;
|
|
31
|
-
if (!/^[0-9a-f]{6}$/i.test(full)) return [0.78, 0.78, 0.78];
|
|
32
|
-
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** Triangle list for the GPU: flat face normals and face colors. Float32Array, 9 floats per vertex. */
|
|
36
|
-
export const buildVertices = (mesh) => {
|
|
37
|
-
let tris = 0;
|
|
38
|
-
for (const f of mesh.faces) tris += Math.max(0, f.v.length - 2);
|
|
39
|
-
const out = new Float32Array(tris * 3 * FLOATS);
|
|
40
|
-
let o = 0;
|
|
41
|
-
for (const face of mesh.faces) {
|
|
42
|
-
const pts = face.v.map((k) => mesh.positions[k]);
|
|
43
|
-
const n = norm(faceNormal(pts));
|
|
44
|
-
const c = rgb(face.color);
|
|
45
|
-
for (let j = 1; j < pts.length - 1; j++) {
|
|
46
|
-
for (const p of [pts[0], pts[j], pts[j + 1]]) {
|
|
47
|
-
out.set([...p, ...n, ...c], o);
|
|
48
|
-
o += FLOATS;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const defaultLight = { direction: norm([0.5, 0.8, 0.6]), sky: [1, 0.97, 0.92], ground: [0.42, 0.38, 0.36] };
|
|
56
|
-
export const defaultMaterial = { ambient: 0.62, diffuse: 0.78, specular: 0.2, shininess: 30 };
|
|
57
|
-
|
|
58
|
-
const SHADER = /* wgsl */ `
|
|
59
|
-
struct U {
|
|
60
|
-
vp : mat4x4<f32>,
|
|
61
|
-
light : vec4<f32>, // xyz: direction towards the light
|
|
62
|
-
sky : vec4<f32>,
|
|
63
|
-
ground : vec4<f32>,
|
|
64
|
-
eye : vec4<f32>,
|
|
65
|
-
mat : vec4<f32>, // ambient, diffuse, specular, shininess
|
|
66
|
-
};
|
|
67
|
-
@group(0) @binding(0) var<uniform> u : U;
|
|
68
|
-
|
|
69
|
-
struct VIn { @location(0) pos : vec3<f32>, @location(1) nrm : vec3<f32>, @location(2) col : vec3<f32> };
|
|
70
|
-
struct VOut {
|
|
71
|
-
@builtin(position) p : vec4<f32>,
|
|
72
|
-
@location(0) n : vec3<f32>,
|
|
73
|
-
@location(1) c : vec3<f32>,
|
|
74
|
-
@location(2) w : vec3<f32>,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
@vertex fn vs(i : VIn) -> VOut {
|
|
78
|
-
var o : VOut;
|
|
79
|
-
o.p = u.vp * vec4<f32>(i.pos, 1.0);
|
|
80
|
-
o.n = i.nrm;
|
|
81
|
-
o.c = i.col;
|
|
82
|
-
o.w = i.pos;
|
|
83
|
-
return o;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@fragment fn fs(i : VOut, @builtin(front_facing) front : bool) -> @location(0) vec4<f32> {
|
|
87
|
-
var n = normalize(i.n);
|
|
88
|
-
if (!front) { n = -n; }
|
|
89
|
-
let l = normalize(u.light.xyz);
|
|
90
|
-
let v = normalize(u.eye.xyz - i.w);
|
|
91
|
-
let hemi = mix(u.ground.rgb, u.sky.rgb, n.y * 0.5 + 0.5) * u.mat.x;
|
|
92
|
-
let diff = max(dot(n, l), 0.0) * u.mat.y;
|
|
93
|
-
let h = normalize(l + v);
|
|
94
|
-
let spec = pow(max(dot(n, h), 0.0), u.mat.w) * u.mat.z * step(0.0, dot(n, l));
|
|
95
|
-
let lit = i.c * (hemi + vec3<f32>(diff)) + vec3<f32>(spec);
|
|
96
|
-
return vec4<f32>(clamp(lit, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
|
|
97
|
-
}
|
|
98
|
-
`;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Sets up WebGPU on a canvas. Rejects when WebGPU is not available.
|
|
102
|
-
* Returns { render(mesh, view, options), destroy() }.
|
|
103
|
-
* render options: size (CSS px, the canvas is made size x size), fit, background [r,g,b] 0..1,
|
|
104
|
-
* light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
|
|
105
|
-
*/
|
|
106
|
-
export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
107
|
-
if (!globalThis.navigator?.gpu) throw new Error('WebGPU is not available in this browser');
|
|
108
|
-
const adapter = await navigator.gpu.requestAdapter();
|
|
109
|
-
if (!adapter) throw new Error('No WebGPU adapter found');
|
|
110
|
-
const device = await adapter.requestDevice();
|
|
111
|
-
device.addEventListener('uncapturederror', (e) => console.error('WebGPU:', e.error?.message ?? e));
|
|
112
|
-
device.lost.then((info) => console.error('WebGPU device lost:', info.reason, info.message));
|
|
113
|
-
const context = canvas.getContext('webgpu');
|
|
114
|
-
if (!context) throw new Error('Could not get a WebGPU canvas context');
|
|
115
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
116
|
-
context.configure({ device, format, alphaMode: 'opaque', });
|
|
117
|
-
|
|
118
|
-
const module = device.createShaderModule({ code: SHADER });
|
|
119
|
-
const pipeline = device.createRenderPipeline({
|
|
120
|
-
layout: 'auto',
|
|
121
|
-
vertex: {
|
|
122
|
-
module,
|
|
123
|
-
entryPoint: 'vs',
|
|
124
|
-
buffers: [
|
|
125
|
-
{
|
|
126
|
-
arrayStride: FLOATS * 4,
|
|
127
|
-
attributes: [
|
|
128
|
-
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
129
|
-
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
130
|
-
{ shaderLocation: 2, offset: 24, format: 'float32x3' },
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
|
|
136
|
-
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
137
|
-
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
|
|
138
|
-
multisample: { count: sampleCount },
|
|
139
|
-
});
|
|
140
|
-
const uniforms = device.createBuffer({ size: 144, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
141
|
-
const bind = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
142
|
-
|
|
143
|
-
let vertexBuffer = null;
|
|
144
|
-
let capacity = 0;
|
|
145
|
-
let px = 0;
|
|
146
|
-
let depth = null;
|
|
147
|
-
let color = null;
|
|
148
|
-
|
|
149
|
-
const targets = (n) => {
|
|
150
|
-
if (n === px) return;
|
|
151
|
-
px = n;
|
|
152
|
-
canvas.width = n;
|
|
153
|
-
canvas.height = n;
|
|
154
|
-
depth?.destroy();
|
|
155
|
-
color?.destroy();
|
|
156
|
-
depth = device.createTexture({ size: [n, n], format: 'depth24plus', sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
157
|
-
color = sampleCount > 1 ? device.createTexture({ size: [n, n], format, sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT }) : null;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, dpr = globalThis.devicePixelRatio || 1 } = {}, readback = false) => {
|
|
161
|
-
const L = { ...defaultLight, ...light };
|
|
162
|
-
const M = { ...defaultMaterial, ...material };
|
|
163
|
-
const cam = makeCamera(view, mesh, size, fit);
|
|
164
|
-
targets(Math.max(1, Math.round(size * dpr)));
|
|
165
|
-
|
|
166
|
-
const verts = buildVertices(mesh);
|
|
167
|
-
if (verts.byteLength > capacity) {
|
|
168
|
-
vertexBuffer?.destroy();
|
|
169
|
-
capacity = Math.max(verts.byteLength, 4096);
|
|
170
|
-
vertexBuffer = device.createBuffer({ size: capacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
171
|
-
}
|
|
172
|
-
if (verts.byteLength) device.queue.writeBuffer(vertexBuffer, 0, verts);
|
|
173
|
-
|
|
174
|
-
const u = new Float32Array(36);
|
|
175
|
-
u.set(viewProjection(cam), 0);
|
|
176
|
-
u.set([...norm(L.direction), 0], 16);
|
|
177
|
-
u.set([...L.sky, 0], 20);
|
|
178
|
-
u.set([...L.ground, 0], 24);
|
|
179
|
-
u.set([...cam.eye, 0], 28);
|
|
180
|
-
u.set([M.ambient, M.diffuse, M.specular, M.shininess], 32);
|
|
181
|
-
device.queue.writeBuffer(uniforms, 0, u);
|
|
182
|
-
|
|
183
|
-
const encoder = device.createCommandEncoder();
|
|
184
|
-
const offscreen = readback
|
|
185
|
-
? device.createTexture({ size: [px, px], format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC })
|
|
186
|
-
: null;
|
|
187
|
-
const target = (offscreen ?? context.getCurrentTexture()).createView();
|
|
188
|
-
const pass = encoder.beginRenderPass({
|
|
189
|
-
colorAttachments: [
|
|
190
|
-
{
|
|
191
|
-
view: color ? color.createView() : target,
|
|
192
|
-
resolveTarget: color ? target : undefined,
|
|
193
|
-
clearValue: { r: background[0], g: background[1], b: background[2], a: 1 },
|
|
194
|
-
loadOp: 'clear',
|
|
195
|
-
storeOp: color ? 'discard' : 'store',
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
depthStencilAttachment: { view: depth.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' },
|
|
199
|
-
});
|
|
200
|
-
if (verts.byteLength) {
|
|
201
|
-
pass.setPipeline(pipeline);
|
|
202
|
-
pass.setBindGroup(0, bind);
|
|
203
|
-
pass.setVertexBuffer(0, vertexBuffer);
|
|
204
|
-
pass.draw(verts.length / FLOATS);
|
|
205
|
-
}
|
|
206
|
-
pass.end();
|
|
207
|
-
if (!offscreen) {
|
|
208
|
-
device.queue.submit([encoder.finish()]);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
const row = Math.ceil((px * 4) / 256) * 256;
|
|
212
|
-
const out = device.createBuffer({ size: row * px, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
213
|
-
encoder.copyTextureToBuffer({ texture: offscreen }, { buffer: out, bytesPerRow: row }, [px, px]);
|
|
214
|
-
device.queue.submit([encoder.finish()]);
|
|
215
|
-
return out.mapAsync(GPUMapMode.READ).then(() => {
|
|
216
|
-
const raw = new Uint8Array(out.getMappedRange());
|
|
217
|
-
const data = new Uint8ClampedArray(px * px * 4);
|
|
218
|
-
const swap = format.startsWith('bgra');
|
|
219
|
-
for (let y = 0; y < px; y++) {
|
|
220
|
-
for (let x = 0; x < px; x++) {
|
|
221
|
-
const i = y * row + x * 4;
|
|
222
|
-
const o = (y * px + x) * 4;
|
|
223
|
-
data[o] = raw[i + (swap ? 2 : 0)];
|
|
224
|
-
data[o + 1] = raw[i + 1];
|
|
225
|
-
data[o + 2] = raw[i + (swap ? 0 : 2)];
|
|
226
|
-
data[o + 3] = 255;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
out.unmap();
|
|
230
|
-
out.destroy();
|
|
231
|
-
offscreen.destroy();
|
|
232
|
-
return { width: px, height: px, data };
|
|
233
|
-
});
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
/** Draws a frame onto the canvas. */
|
|
237
|
-
const render = (mesh, view, options) => {
|
|
238
|
-
draw(mesh, view, options);
|
|
239
|
-
};
|
|
240
|
-
/** Draws a frame into an offscreen texture and reads it back: Promise<{ width, height, data: RGBA }>. */
|
|
241
|
-
const pixels = (mesh, view, options) => draw(mesh, view, options, true);
|
|
242
|
-
|
|
243
|
-
const destroy = () => {
|
|
244
|
-
vertexBuffer?.destroy();
|
|
245
|
-
depth?.destroy();
|
|
246
|
-
color?.destroy();
|
|
247
|
-
uniforms.destroy();
|
|
248
|
-
device.destroy();
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
return { render, pixels, destroy, device, adapter };
|
|
252
|
-
};
|
package/src/index.d.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
export const name: '3draft';
|
|
2
|
-
export const version: string;
|
|
3
|
-
|
|
4
|
-
export type Vec3 = [number, number, number];
|
|
5
|
-
|
|
6
|
-
export interface Face {
|
|
7
|
-
/** Point indices, CCW seen from outside. */
|
|
8
|
-
v: number[];
|
|
9
|
-
color?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Mesh {
|
|
13
|
-
positions: Vec3[];
|
|
14
|
-
faces: Face[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface BoxOptions {
|
|
18
|
-
size?: Vec3;
|
|
19
|
-
at?: Vec3;
|
|
20
|
-
color?: string;
|
|
21
|
-
/** Orthonormal local x/y/z axes (default: the world axes). */
|
|
22
|
-
axes?: [Vec3, Vec3, Vec3];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function box(options?: BoxOptions): Mesh;
|
|
26
|
-
/** Prism from a convex footprint [[x, z], ...] (any winding), extruded up from `y` by `height`. */
|
|
27
|
-
export function extrude(options: { polygon: [number, number][]; height?: number; y?: number; color?: string }): Mesh;
|
|
28
|
-
export function merge(...meshes: Mesh[]): Mesh;
|
|
29
|
-
/** Turntable rotation (radians): yaw around Y, then pitch around X, around `center`. */
|
|
30
|
-
export function rotate(mesh: Mesh, angles?: { yaw?: number; pitch?: number }, center?: Vec3): Mesh;
|
|
31
|
-
export function bounds(mesh: Mesh): { center: Vec3; radius: number };
|
|
32
|
-
export function faceNormal(points: Vec3[]): Vec3;
|
|
33
|
-
export function faceCenter(points: Vec3[]): Vec3;
|
|
34
|
-
|
|
35
|
-
export type ViewName = 'front' | 'side' | 'top' | 'persp';
|
|
36
|
-
export const VIEW_ORDER: readonly ViewName[];
|
|
37
|
-
|
|
38
|
-
/** Custom camera: `dir` points from the target towards the eye. */
|
|
39
|
-
export interface ViewSpec {
|
|
40
|
-
dir: Vec3;
|
|
41
|
-
up: Vec3;
|
|
42
|
-
ortho?: boolean;
|
|
43
|
-
name?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface Fit {
|
|
47
|
-
center: Vec3;
|
|
48
|
-
radius: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface Camera {
|
|
52
|
-
name: string;
|
|
53
|
-
eye: Vec3;
|
|
54
|
-
f: Vec3;
|
|
55
|
-
r: Vec3;
|
|
56
|
-
u: Vec3;
|
|
57
|
-
ortho: boolean;
|
|
58
|
-
k: number;
|
|
59
|
-
size: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function makeCamera(view: ViewName | ViewSpec, mesh: Mesh, size: number, fit?: Fit): Camera;
|
|
63
|
-
export function project(camera: Camera, p: Vec3): { x: number; y: number; depth: number };
|
|
64
|
-
|
|
65
|
-
/** Ray through a panel pixel, the inverse of `project`. `dir` is a unit vector. */
|
|
66
|
-
export function ray(camera: Camera, x: number, y: number): { origin: Vec3; dir: Vec3 };
|
|
67
|
-
|
|
68
|
-
export interface Hit {
|
|
69
|
-
/** Index into `mesh.faces`. */
|
|
70
|
-
face: number;
|
|
71
|
-
/** Distance along the ray. */
|
|
72
|
-
distance: number;
|
|
73
|
-
point: Vec3;
|
|
74
|
-
/** Unit normal, facing the camera. */
|
|
75
|
-
normal: Vec3;
|
|
76
|
-
/** Whether the face itself faces the camera. */
|
|
77
|
-
front: boolean;
|
|
78
|
-
color?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The nearest surface under panel pixel (x, y), or null. */
|
|
82
|
-
export function pick(
|
|
83
|
-
mesh: Mesh,
|
|
84
|
-
camera: Camera,
|
|
85
|
-
x: number,
|
|
86
|
-
y: number,
|
|
87
|
-
options?: { cull?: boolean },
|
|
88
|
-
): Hit | null;
|
|
89
|
-
|
|
90
|
-
/** A target cross lying on the picked surface: 4 arms and a ring, as 3D polylines. */
|
|
91
|
-
export function crosshair(mesh: Mesh, hit: Hit, radius?: number): Vec3[][];
|
|
92
|
-
|
|
93
|
-
/** 3D polylines drawn on top of the view. */
|
|
94
|
-
export interface Mark {
|
|
95
|
-
name?: string;
|
|
96
|
-
lines: Vec3[][];
|
|
97
|
-
color?: string;
|
|
98
|
-
width?: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** '26.11.1001' -> '26.11.01.001' */
|
|
102
|
-
/** Turntable as a camera: the object stays put, the eye orbits (radians). */
|
|
103
|
-
export function orbit(angles?: { yaw?: number; pitch?: number; ortho?: boolean; name?: string }): ViewSpec;
|
|
104
|
-
|
|
105
|
-
export interface StandOptions {
|
|
106
|
-
/** Square base edge (default 0.15). */
|
|
107
|
-
base?: number;
|
|
108
|
-
/** Height (default 3 x base). */
|
|
109
|
-
height?: number;
|
|
110
|
-
color?: string;
|
|
111
|
-
}
|
|
112
|
-
/** A prism standing on the picked surface, along its normal. */
|
|
113
|
-
export function standOn(mesh: Mesh, hit: Hit, options?: StandOptions): Mesh;
|
|
114
|
-
|
|
115
|
-
/** Back-to-front paint order of projected faces (plane tests; depth fallback on cycles). */
|
|
116
|
-
export function paintOrder(items: unknown[]): number[];
|
|
117
|
-
|
|
118
|
-
export interface GpuLight {
|
|
119
|
-
/** Direction towards the light, world space. */
|
|
120
|
-
direction?: Vec3;
|
|
121
|
-
/** Hemisphere ambient colors (rgb 0..1): up and down. */
|
|
122
|
-
sky?: Vec3;
|
|
123
|
-
ground?: Vec3;
|
|
124
|
-
}
|
|
125
|
-
export interface GpuMaterial {
|
|
126
|
-
ambient?: number;
|
|
127
|
-
diffuse?: number;
|
|
128
|
-
specular?: number;
|
|
129
|
-
shininess?: number;
|
|
130
|
-
}
|
|
131
|
-
export interface GpuRenderOptions {
|
|
132
|
-
/** CSS pixels, the canvas becomes size x size (times dpr in device pixels). */
|
|
133
|
-
size?: number;
|
|
134
|
-
fit?: Fit;
|
|
135
|
-
/** Clear color, rgb 0..1. */
|
|
136
|
-
background?: Vec3;
|
|
137
|
-
light?: GpuLight;
|
|
138
|
-
material?: GpuMaterial;
|
|
139
|
-
dpr?: number;
|
|
140
|
-
}
|
|
141
|
-
export interface Gpu {
|
|
142
|
-
render(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): void;
|
|
143
|
-
/** Same frame, drawn offscreen and read back as RGBA (tests, thumbnails). */
|
|
144
|
-
pixels(mesh: Mesh, view: ViewName | ViewSpec, options?: GpuRenderOptions): Promise<{ width: number; height: number; data: Uint8ClampedArray }>;
|
|
145
|
-
destroy(): void;
|
|
146
|
-
}
|
|
147
|
-
/** WebGPU renderer on a canvas; rejects when WebGPU is unavailable. Same camera as the SVG renderer. */
|
|
148
|
-
export function createGpu(canvas: HTMLCanvasElement, options?: { sampleCount?: number }): Promise<Gpu>;
|
|
149
|
-
/** Column-major 4x4 matrix mapping world points to the same pixels as project(). */
|
|
150
|
-
export function viewProjection(camera: Camera, near?: number, far?: number): Float32Array;
|
|
151
|
-
/** Triangle list: position(3) normal(3) color(3) per vertex. */
|
|
152
|
-
export function buildVertices(mesh: Mesh): Float32Array;
|
|
153
|
-
export const defaultLight: Required<GpuLight>;
|
|
154
|
-
export const defaultMaterial: Required<GpuMaterial>;
|
|
155
|
-
|
|
156
|
-
export interface Validation {
|
|
157
|
-
vertices: number;
|
|
158
|
-
faces: number;
|
|
159
|
-
/** Every edge is shared by two faces, wound consistently. */
|
|
160
|
-
closed: boolean;
|
|
161
|
-
openEdges: number;
|
|
162
|
-
/** Signed volume; null unless closed. */
|
|
163
|
-
volume: number | null;
|
|
164
|
-
/** Closed, but the faces point inward. */
|
|
165
|
-
inside_out: boolean;
|
|
166
|
-
degenerateFaces: number[];
|
|
167
|
-
nonPlanarFaces: number[];
|
|
168
|
-
nonConvexFaces: number[];
|
|
169
|
-
ok: boolean;
|
|
170
|
-
/** Plain-text findings. */
|
|
171
|
-
notes: string[];
|
|
172
|
-
}
|
|
173
|
-
/** Geometry checks: closed shell, CCW from outside, planar, convex faces. */
|
|
174
|
-
export function validate(mesh: Mesh): Validation;
|
|
175
|
-
|
|
176
|
-
/** Extrude / taper / inset / dent a face; returns the new mesh, the cap face index and the new side quads. */
|
|
177
|
-
export function extrudeFace(mesh: Mesh, face: number, options?: { distance?: number; scale?: number; color?: string }): { mesh: Mesh; cap: number; sides: number[] };
|
|
178
|
-
export function moveVertices(mesh: Mesh, indices: number[], delta: Vec3): Mesh;
|
|
179
|
-
|
|
180
|
-
export function displayVersion(version: string): string;
|
|
181
|
-
|
|
182
|
-
export interface RenderOptions {
|
|
183
|
-
/** Panel size in px (default 400). */
|
|
184
|
-
size?: number;
|
|
185
|
-
/** Drop back-facing faces (default true). */
|
|
186
|
-
cull?: boolean;
|
|
187
|
-
/** Draw the vertices as circles (default true). */
|
|
188
|
-
points?: boolean;
|
|
189
|
-
stroke?: string;
|
|
190
|
-
pointColor?: string;
|
|
191
|
-
background?: string;
|
|
192
|
-
/** Keep the camera target/framing fixed while the mesh moves. */
|
|
193
|
-
fit?: Fit;
|
|
194
|
-
/** 3D polylines drawn on top, e.g. the surface cursor. */
|
|
195
|
-
marks?: Mark[];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function renderView(mesh: Mesh, view: ViewName | ViewSpec, options?: RenderOptions): string;
|
|
199
|
-
export function renderViews(mesh: Mesh, options?: RenderOptions): { name: ViewName; svg: string }[];
|
|
200
|
-
export function renderSheet(mesh: Mesh, options?: RenderOptions): string;
|
|
201
|
-
|
|
202
|
-
export interface Info {
|
|
203
|
-
name: string;
|
|
204
|
-
version: string;
|
|
205
|
-
stage: 'pre-alpha';
|
|
206
|
-
goals: string[];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function info(): Info;
|