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/dist/order.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Vec3 } from './types.js';
|
|
2
|
+
export interface ScreenPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** One projected face: see paintOrder(). */
|
|
7
|
+
export interface PaintItem {
|
|
8
|
+
i: number;
|
|
9
|
+
world: Vec3[];
|
|
10
|
+
screen: ScreenPoint[];
|
|
11
|
+
n: Vec3;
|
|
12
|
+
p: Vec3;
|
|
13
|
+
eye: 1 | -1;
|
|
14
|
+
depth: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Items need: { i, world, screen, n, p, eye, depth }
|
|
18
|
+
* world face points in world space screen the same, projected { x, y }
|
|
19
|
+
* n, p unit normal and a point on it eye +1 if the face faces the camera, else -1
|
|
20
|
+
* depth mean depth, used for ties i face index, used for ties
|
|
21
|
+
*/
|
|
22
|
+
export declare const paintOrder: <T extends PaintItem>(items: T[]) => T[];
|
package/dist/order.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Back-to-front painting order, Newell-Sancha style.
|
|
2
|
+
//
|
|
3
|
+
// Faces that overlap on screen are ordered by plane tests: face A can be painted
|
|
4
|
+
// after face B when A lies wholly on the eye side of B's plane, or when B lies
|
|
5
|
+
// wholly behind A's plane. The resulting "paint this first" relations are then
|
|
6
|
+
// sorted topologically. Centroid depth only breaks ties, and orders faces that
|
|
7
|
+
// cannot be compared (intersecting solids, cycles) as well as before.
|
|
8
|
+
import { dot, minus } from './vec.js';
|
|
9
|
+
const PLANE_EPS = 1e-6; // plane test tolerance, world units
|
|
10
|
+
const PX = 0.01; // overlaps thinner than this are treated as touching
|
|
11
|
+
// Do two convex screen polygons overlap by more than a sliver? (separating axis test)
|
|
12
|
+
const overlaps = (a, b) => {
|
|
13
|
+
for (const poly of [a, b]) {
|
|
14
|
+
for (let i = 0; i < poly.length; i++) {
|
|
15
|
+
const p = poly[i];
|
|
16
|
+
const q = poly[(i + 1) % poly.length];
|
|
17
|
+
const nx = q.y - p.y;
|
|
18
|
+
const ny = p.x - q.x;
|
|
19
|
+
const length = Math.hypot(nx, ny);
|
|
20
|
+
if (!length)
|
|
21
|
+
continue;
|
|
22
|
+
let minA = Infinity;
|
|
23
|
+
let maxA = -Infinity;
|
|
24
|
+
let minB = Infinity;
|
|
25
|
+
let maxB = -Infinity;
|
|
26
|
+
for (const v of a) {
|
|
27
|
+
const d = (v.x * nx + v.y * ny) / length;
|
|
28
|
+
if (d < minA)
|
|
29
|
+
minA = d;
|
|
30
|
+
if (d > maxA)
|
|
31
|
+
maxA = d;
|
|
32
|
+
}
|
|
33
|
+
for (const v of b) {
|
|
34
|
+
const d = (v.x * nx + v.y * ny) / length;
|
|
35
|
+
if (d < minB)
|
|
36
|
+
minB = d;
|
|
37
|
+
if (d > maxB)
|
|
38
|
+
maxB = d;
|
|
39
|
+
}
|
|
40
|
+
if (maxA <= minB + PX || maxB <= minA + PX)
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
// Signed distances of points to a face's plane, positive on the eye side.
|
|
47
|
+
const distances = (points, face) => points.map((v) => dot(face.n, minus(face.p)(v)) * face.eye);
|
|
48
|
+
/**
|
|
49
|
+
* Items need: { i, world, screen, n, p, eye, depth }
|
|
50
|
+
* world face points in world space screen the same, projected { x, y }
|
|
51
|
+
* n, p unit normal and a point on it eye +1 if the face faces the camera, else -1
|
|
52
|
+
* depth mean depth, used for ties i face index, used for ties
|
|
53
|
+
*/
|
|
54
|
+
export const paintOrder = (items) => {
|
|
55
|
+
const count = items.length;
|
|
56
|
+
const after = items.map(() => []);
|
|
57
|
+
const waiting = new Array(count).fill(0);
|
|
58
|
+
const link = (first, second) => {
|
|
59
|
+
after[first].push(second);
|
|
60
|
+
waiting[second] += 1;
|
|
61
|
+
};
|
|
62
|
+
// Screen bounding boxes, swept along x: only faces whose boxes overlap get the exact tests.
|
|
63
|
+
const box = items.map(({ screen }) => {
|
|
64
|
+
let x0 = Infinity;
|
|
65
|
+
let x1 = -Infinity;
|
|
66
|
+
let y0 = Infinity;
|
|
67
|
+
let y1 = -Infinity;
|
|
68
|
+
for (const { x, y } of screen) {
|
|
69
|
+
if (x < x0)
|
|
70
|
+
x0 = x;
|
|
71
|
+
if (x > x1)
|
|
72
|
+
x1 = x;
|
|
73
|
+
if (y < y0)
|
|
74
|
+
y0 = y;
|
|
75
|
+
if (y > y1)
|
|
76
|
+
y1 = y;
|
|
77
|
+
}
|
|
78
|
+
return { x0, x1, y0, y1 };
|
|
79
|
+
});
|
|
80
|
+
const byX = items.map((_, k) => k).sort((a, b) => box[a].x0 - box[b].x0 || a - b);
|
|
81
|
+
for (let s = 0; s < count; s++) {
|
|
82
|
+
for (let t = s + 1; t < count; t++) {
|
|
83
|
+
const [i, j] = byX[s] < byX[t] ? [byX[s], byX[t]] : [byX[t], byX[s]];
|
|
84
|
+
const bi = box[byX[s]];
|
|
85
|
+
const bj = box[byX[t]];
|
|
86
|
+
if (bj.x0 >= bi.x1 - PX)
|
|
87
|
+
break; // every later box starts further right
|
|
88
|
+
if (bj.y0 >= bi.y1 - PX || bi.y0 >= bj.y1 - PX)
|
|
89
|
+
continue;
|
|
90
|
+
const A = items[i];
|
|
91
|
+
const B = items[j];
|
|
92
|
+
if (!overlaps(A.screen, B.screen))
|
|
93
|
+
continue;
|
|
94
|
+
const a = distances(A.world, B); // A against B's plane
|
|
95
|
+
const b = distances(B.world, A); // B against A's plane
|
|
96
|
+
const aAfterB = a.every((d) => d >= -PLANE_EPS) || b.every((d) => d <= PLANE_EPS);
|
|
97
|
+
const bAfterA = b.every((d) => d >= -PLANE_EPS) || a.every((d) => d <= PLANE_EPS);
|
|
98
|
+
if (aAfterB && !bAfterA)
|
|
99
|
+
link(j, i);
|
|
100
|
+
else if (bAfterA && !aAfterB)
|
|
101
|
+
link(i, j);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const farther = (x, y) => items[x].depth > items[y].depth || (items[x].depth === items[y].depth && items[x].i < items[y].i);
|
|
105
|
+
const done = new Array(count).fill(false);
|
|
106
|
+
const out = [];
|
|
107
|
+
while (out.length < count) {
|
|
108
|
+
let best = -1;
|
|
109
|
+
for (let k = 0; k < count; k++) {
|
|
110
|
+
if (!done[k] && waiting[k] === 0 && (best < 0 || farther(k, best)))
|
|
111
|
+
best = k;
|
|
112
|
+
}
|
|
113
|
+
if (best < 0) {
|
|
114
|
+
// a cycle: take the farthest face that is left
|
|
115
|
+
for (let k = 0; k < count; k++)
|
|
116
|
+
if (!done[k] && (best < 0 || farther(k, best)))
|
|
117
|
+
best = k;
|
|
118
|
+
}
|
|
119
|
+
done[best] = true;
|
|
120
|
+
out.push(items[best]);
|
|
121
|
+
for (const k of after[best])
|
|
122
|
+
waiting[k] -= 1;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
package/dist/pick.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Camera, Hit, Mesh, Vec3 } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The nearest surface under panel pixel (x, y), or null.
|
|
4
|
+
* { face, distance, point, normal, front, color }
|
|
5
|
+
* `normal` is a unit vector facing the camera; `front` tells whether the face
|
|
6
|
+
* itself faces the camera (always true while `cull` is on). Faces are assumed
|
|
7
|
+
* convex and planar.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pick: (mesh: Mesh, cam: Camera, x: number, y: number, { cull }?: {
|
|
10
|
+
cull?: boolean;
|
|
11
|
+
}) => Hit | null;
|
|
12
|
+
/**
|
|
13
|
+
* A target cross lying flat on the picked surface (perpendicular to its
|
|
14
|
+
* normal): four arms with a gap in the middle and a ring, as 3D polylines.
|
|
15
|
+
* It is aligned to the first edge of the face, so it turns with the object.
|
|
16
|
+
* `radius` is the ring radius in world units.
|
|
17
|
+
*/
|
|
18
|
+
export declare const crosshair: (mesh: Mesh, hit: Hit, radius?: number) => Vec3[][];
|
package/dist/pick.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Surface sensing: what is under a screen pixel, and a marker that lies on it.
|
|
2
|
+
// Picking casts a real ray against the face geometry, so it is exact even where
|
|
3
|
+
// the painter-sorted drawing is not.
|
|
4
|
+
import { cross, dot, norm, plus, minus, times, scale } from './vec.js';
|
|
5
|
+
import { faceNormal } from './mesh.js';
|
|
6
|
+
import { ray } from './camera.js';
|
|
7
|
+
const EPS = 1e-9;
|
|
8
|
+
// Moller-Trumbore ray/triangle test. Distance along the ray, or null.
|
|
9
|
+
const hitTriangle = (o, d, a, b, c) => {
|
|
10
|
+
const e1 = minus(a)(b);
|
|
11
|
+
const e2 = minus(a)(c);
|
|
12
|
+
const h = cross(d, e2);
|
|
13
|
+
const det = dot(e1, h);
|
|
14
|
+
if (Math.abs(det) < EPS)
|
|
15
|
+
return null;
|
|
16
|
+
const inv = 1 / det;
|
|
17
|
+
const s = minus(a)(o);
|
|
18
|
+
const u = inv * dot(s, h);
|
|
19
|
+
if (u < 0 || u > 1)
|
|
20
|
+
return null;
|
|
21
|
+
const q = cross(s, e1);
|
|
22
|
+
const v = inv * dot(d, q);
|
|
23
|
+
if (v < 0 || u + v > 1)
|
|
24
|
+
return null;
|
|
25
|
+
const t = inv * dot(e2, q);
|
|
26
|
+
return t > EPS ? t : null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The nearest surface under panel pixel (x, y), or null.
|
|
30
|
+
* { face, distance, point, normal, front, color }
|
|
31
|
+
* `normal` is a unit vector facing the camera; `front` tells whether the face
|
|
32
|
+
* itself faces the camera (always true while `cull` is on). Faces are assumed
|
|
33
|
+
* convex and planar.
|
|
34
|
+
*/
|
|
35
|
+
export const pick = (mesh, cam, x, y, { cull = true } = {}) => {
|
|
36
|
+
const { origin, dir } = ray(cam, x, y);
|
|
37
|
+
let best = null;
|
|
38
|
+
mesh.faces.forEach((face, i) => {
|
|
39
|
+
const pts = face.v.map((k) => mesh.positions[k]);
|
|
40
|
+
const outward = norm(faceNormal(pts));
|
|
41
|
+
const front = dot(outward, dir) < 0;
|
|
42
|
+
if (cull && !front)
|
|
43
|
+
return;
|
|
44
|
+
for (let j = 1; j < pts.length - 1; j++) {
|
|
45
|
+
const t = hitTriangle(origin, dir, pts[0], pts[j], pts[j + 1]);
|
|
46
|
+
if (t !== null && (!best || t < best.distance)) {
|
|
47
|
+
best = {
|
|
48
|
+
face: i,
|
|
49
|
+
distance: t,
|
|
50
|
+
point: plus(origin)(times(t)(dir)),
|
|
51
|
+
normal: front ? outward : times(-1)(outward),
|
|
52
|
+
front,
|
|
53
|
+
color: face.color,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return best;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A target cross lying flat on the picked surface (perpendicular to its
|
|
62
|
+
* normal): four arms with a gap in the middle and a ring, as 3D polylines.
|
|
63
|
+
* It is aligned to the first edge of the face, so it turns with the object.
|
|
64
|
+
* `radius` is the ring radius in world units.
|
|
65
|
+
*/
|
|
66
|
+
export const crosshair = (mesh, hit, radius = 0.1) => {
|
|
67
|
+
const [a, b] = mesh.faces[hit.face].v;
|
|
68
|
+
const t1 = norm(minus(mesh.positions[a])(mesh.positions[b]));
|
|
69
|
+
const t2 = norm(cross(hit.normal, t1));
|
|
70
|
+
const at = (p, q) => plus(hit.point)(plus(scale(t2, q * radius))(scale(t1, p * radius)));
|
|
71
|
+
const ring = Array.from({ length: 33 }, (_, i) => {
|
|
72
|
+
const angle = (i / 32) * 2 * Math.PI;
|
|
73
|
+
return at(Math.cos(angle), Math.sin(angle));
|
|
74
|
+
});
|
|
75
|
+
return [
|
|
76
|
+
[at(-1.7, 0), at(-0.4, 0)],
|
|
77
|
+
[at(0.4, 0), at(1.7, 0)],
|
|
78
|
+
[at(0, -1.7), at(0, -0.4)],
|
|
79
|
+
[at(0, 0.4), at(0, 1.7)],
|
|
80
|
+
ring,
|
|
81
|
+
];
|
|
82
|
+
};
|
package/dist/place.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Axes, Hit, Mesh, Vec3 } from './types.js';
|
|
2
|
+
export interface StandOptions {
|
|
3
|
+
/** Square base edge (default 0.15). */
|
|
4
|
+
base?: number;
|
|
5
|
+
/** Height (default 3 x base). */
|
|
6
|
+
height?: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Where and how an instanced renderer draws the prism. */
|
|
10
|
+
export interface Frame {
|
|
11
|
+
size: Vec3;
|
|
12
|
+
at: Vec3;
|
|
13
|
+
axes: Axes;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A square prism standing on the picked surface: its base lies on the surface,
|
|
17
|
+
* centered at the hit point, and it rises along the surface normal. Its sides
|
|
18
|
+
* line up with the first edge of the face. The height is `3 * base` unless given.
|
|
19
|
+
* Returns a new mesh; merge() it into the scene.
|
|
20
|
+
*/
|
|
21
|
+
export declare const standOn: (mesh: Mesh, hit: Hit, { base, height, color }?: StandOptions) => Mesh & {
|
|
22
|
+
frame: Frame;
|
|
23
|
+
};
|
package/{src → dist}/place.js
RENAMED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Putting things down on surfaces.
|
|
2
|
-
|
|
3
|
-
import { add, cross, norm, scale, sub } from './vec.js';
|
|
2
|
+
import { cross, norm, plus, minus, times } from './vec.js';
|
|
4
3
|
import { box } from './mesh.js';
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* A square prism standing on the picked surface: its base lies on the surface,
|
|
8
6
|
* centered at the hit point, and it rises along the surface normal. Its sides
|
|
@@ -10,14 +8,10 @@ import { box } from './mesh.js';
|
|
|
10
8
|
* Returns a new mesh; merge() it into the scene.
|
|
11
9
|
*/
|
|
12
10
|
export const standOn = (mesh, hit, { base = 0.15, height = 3 * base, color = '#c8c8c8' } = {}) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
at: add(hit.point, scale(ey, height / 2)),
|
|
20
|
-
axes: [ex, ey, ez],
|
|
21
|
-
color,
|
|
22
|
-
});
|
|
11
|
+
const [a, b] = mesh.faces[hit.face].v;
|
|
12
|
+
const ex = norm(minus(mesh.positions[a])(mesh.positions[b]));
|
|
13
|
+
const ey = hit.normal;
|
|
14
|
+
const ez = cross(ex, ey);
|
|
15
|
+
const frame = { size: [base, height, base], at: plus(hit.point)(times(height / 2)(ey)), axes: [ex, ey, ez] };
|
|
16
|
+
return { ...box({ ...frame, color }), frame }; // frame: what an instanced renderer needs
|
|
23
17
|
};
|
package/dist/svg.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Fit, Mark, Mesh, ViewName, ViewSpec } from './types.js';
|
|
2
|
+
export interface RenderOptions {
|
|
3
|
+
/** Panel size in px (default 400). */
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Drop back-facing faces (default true). */
|
|
6
|
+
cull?: boolean;
|
|
7
|
+
/** Draw the vertices as circles (default true). */
|
|
8
|
+
points?: boolean;
|
|
9
|
+
stroke?: string;
|
|
10
|
+
pointColor?: string;
|
|
11
|
+
background?: string;
|
|
12
|
+
/** Keep the camera target/framing fixed while the mesh moves. */
|
|
13
|
+
fit?: Fit;
|
|
14
|
+
/** 3D polylines drawn on top, e.g. the surface cursor. */
|
|
15
|
+
marks?: Mark[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One standalone SVG for a single view: 'front' | 'side' | 'top' | 'persp',
|
|
19
|
+
* or a custom { dir, up, ortho, name? } spec.
|
|
20
|
+
*/
|
|
21
|
+
export declare const renderView: (mesh: Mesh, view: ViewName | ViewSpec, opts?: RenderOptions) => string;
|
|
22
|
+
/** The four views one after another: [{ name, svg }, ...]. */
|
|
23
|
+
export declare const renderViews: (mesh: Mesh, opts?: RenderOptions) => {
|
|
24
|
+
name: ViewName;
|
|
25
|
+
svg: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** All four views on one 2x2 sheet: front, side / top, persp. */
|
|
28
|
+
export declare const renderSheet: (mesh: Mesh, opts?: RenderOptions) => string;
|
package/dist/svg.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Flat SVG preview: points, edges, faces. No shading, no textures.
|
|
2
|
+
// Faces are painted far to near (see order.js), back faces culled. The polygon stroke
|
|
3
|
+
// is the edge layer, so hidden edges stay hidden. Output is deterministic.
|
|
4
|
+
import { dot, norm, minus } from './vec.js';
|
|
5
|
+
import { faceNormal } from './mesh.js';
|
|
6
|
+
import { paintOrder } from './order.js';
|
|
7
|
+
import { VIEW_ORDER, makeCamera, project } from './camera.js';
|
|
8
|
+
const defaults = {
|
|
9
|
+
size: 400,
|
|
10
|
+
cull: true,
|
|
11
|
+
points: true,
|
|
12
|
+
stroke: '#1d1d1f',
|
|
13
|
+
pointColor: '#d6336c',
|
|
14
|
+
background: '#fafafa',
|
|
15
|
+
};
|
|
16
|
+
const num = (n) => String(Math.round(n * 100) / 100 + 0); // +0 turns -0 into 0
|
|
17
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
18
|
+
const panel = (mesh, view, o, x = 0, y = 0) => {
|
|
19
|
+
const cam = makeCamera(view, mesh, o.size, o.fit);
|
|
20
|
+
const name = esc(cam.name);
|
|
21
|
+
const proj = mesh.positions.map((p) => project(cam, p));
|
|
22
|
+
const items = [];
|
|
23
|
+
const NEAR = 0.02; // perspective: nearer than this is cut off
|
|
24
|
+
const keep = -8; // faces entirely off the panel (by more than a stroke) are not drawn
|
|
25
|
+
const seen = (pts) => !pts.every((p) => p.x < keep) && !pts.every((p) => p.y < keep) && !pts.every((p) => p.x > o.size - keep) && !pts.every((p) => p.y > o.size - keep);
|
|
26
|
+
mesh.faces.forEach((face, i) => {
|
|
27
|
+
let world = face.v.map((k) => mesh.positions[k]);
|
|
28
|
+
let screen = face.v.map((k) => proj[k]);
|
|
29
|
+
const dir = cam.ortho ? cam.f : minus(cam.eye)(world[0]);
|
|
30
|
+
const normal = faceNormal(world);
|
|
31
|
+
const facing = dot(normal, dir) < 0;
|
|
32
|
+
if (o.cull && !facing)
|
|
33
|
+
return; // back face
|
|
34
|
+
if (cam.ortho) {
|
|
35
|
+
if (screen.some((s) => s.depth <= 0.01))
|
|
36
|
+
return; // behind the camera
|
|
37
|
+
}
|
|
38
|
+
else if (screen.some((s) => s.depth < NEAR)) {
|
|
39
|
+
// cut the face at the near plane, like a camera does
|
|
40
|
+
if (screen.every((s) => s.depth < NEAR))
|
|
41
|
+
return;
|
|
42
|
+
const cut = [];
|
|
43
|
+
for (let a = 0; a < world.length; a++) {
|
|
44
|
+
const b = (a + 1) % world.length;
|
|
45
|
+
const da = screen[a].depth - NEAR;
|
|
46
|
+
const db = screen[b].depth - NEAR;
|
|
47
|
+
if (da >= 0)
|
|
48
|
+
cut.push(world[a]);
|
|
49
|
+
if (da * db < 0) {
|
|
50
|
+
const t = da / (da - db);
|
|
51
|
+
cut.push(world[a].map((c, k) => c + (world[b][k] - c) * t));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (cut.length < 3)
|
|
55
|
+
return;
|
|
56
|
+
world = cut;
|
|
57
|
+
screen = world.map((p) => project(cam, p));
|
|
58
|
+
}
|
|
59
|
+
if (!seen(screen))
|
|
60
|
+
return; // outside the panel
|
|
61
|
+
items.push({
|
|
62
|
+
i,
|
|
63
|
+
vs: face.v,
|
|
64
|
+
world,
|
|
65
|
+
screen,
|
|
66
|
+
n: norm(normal),
|
|
67
|
+
p: world[0],
|
|
68
|
+
eye: facing ? 1 : -1,
|
|
69
|
+
color: face.color ?? '#c8c8c8',
|
|
70
|
+
depth: screen.reduce((s, p) => s + p.depth, 0) / screen.length,
|
|
71
|
+
pts: screen.map((p) => `${num(p.x)},${num(p.y)}`).join(' '),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const ordered = paintOrder(items); // far to near
|
|
75
|
+
// A point is drawn right after the nearest painted face that holds it, so
|
|
76
|
+
// faces painted later (closer to the camera) still cover it.
|
|
77
|
+
const last = new Map();
|
|
78
|
+
ordered.forEach((it, idx) => it.vs.forEach((k) => last.set(k, idx)));
|
|
79
|
+
const body = ordered.flatMap((it, idx) => {
|
|
80
|
+
const polygon = `<polygon data-face="${it.i}" data-depth="${num(it.depth)}" points="${it.pts}" ` +
|
|
81
|
+
`fill="${it.color}" stroke="${o.stroke}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
82
|
+
const dots = o.points
|
|
83
|
+
? it.vs
|
|
84
|
+
.filter((k) => last.get(k) === idx && (cam.ortho || proj[k].depth >= NEAR))
|
|
85
|
+
.sort((a, b) => a - b)
|
|
86
|
+
.map((k) => `<circle data-v="${k}" cx="${num(proj[k].x)}" cy="${num(proj[k].y)}" r="3" fill="${o.pointColor}"/>`)
|
|
87
|
+
: [];
|
|
88
|
+
return [polygon, ...dots];
|
|
89
|
+
});
|
|
90
|
+
// 3D polylines (e.g. a surface cursor) drawn on top of everything else.
|
|
91
|
+
const marks = (o.marks ?? []).flatMap((m) => m.lines.flatMap((line) => {
|
|
92
|
+
const pts = line.map((p) => project(cam, p));
|
|
93
|
+
if (pts.some((p) => p.depth <= 0.01))
|
|
94
|
+
return [];
|
|
95
|
+
return [
|
|
96
|
+
`<polyline data-mark="${esc(m.name ?? 'mark')}" points="${pts.map((p) => `${num(p.x)},${num(p.y)}`).join(' ')}" ` +
|
|
97
|
+
`fill="none" stroke="${m.color ?? '#0b7285'}" stroke-width="${m.width ?? 2}" ` +
|
|
98
|
+
`stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
99
|
+
];
|
|
100
|
+
}));
|
|
101
|
+
return [
|
|
102
|
+
`<g data-view="${name}" transform="translate(${x} ${y})">`,
|
|
103
|
+
`<rect width="${o.size}" height="${o.size}" fill="${o.background}" stroke="#cfcfd4"/>`,
|
|
104
|
+
`<text x="10" y="20" font-family="monospace" font-size="13" fill="#555">${name}</text>`,
|
|
105
|
+
...body,
|
|
106
|
+
...marks,
|
|
107
|
+
'</g>',
|
|
108
|
+
].join('\n');
|
|
109
|
+
};
|
|
110
|
+
const root = (w, h, body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">\n${body}\n</svg>\n`;
|
|
111
|
+
/**
|
|
112
|
+
* One standalone SVG for a single view: 'front' | 'side' | 'top' | 'persp',
|
|
113
|
+
* or a custom { dir, up, ortho, name? } spec.
|
|
114
|
+
*/
|
|
115
|
+
export const renderView = (mesh, view, opts = {}) => {
|
|
116
|
+
const o = { ...defaults, ...opts };
|
|
117
|
+
return root(o.size, o.size, panel(mesh, view, o));
|
|
118
|
+
};
|
|
119
|
+
/** The four views one after another: [{ name, svg }, ...]. */
|
|
120
|
+
export const renderViews = (mesh, opts = {}) => VIEW_ORDER.map((name) => ({ name, svg: renderView(mesh, name, opts) }));
|
|
121
|
+
/** All four views on one 2x2 sheet: front, side / top, persp. */
|
|
122
|
+
export const renderSheet = (mesh, opts = {}) => {
|
|
123
|
+
const o = { ...defaults, ...opts };
|
|
124
|
+
const body = VIEW_ORDER.map((name, i) => panel(mesh, name, o, (i % 2) * o.size, Math.floor(i / 2) * o.size)).join('\n');
|
|
125
|
+
return root(o.size * 2, o.size * 2, body);
|
|
126
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Vec3 = [number, number, number];
|
|
2
|
+
export interface Face {
|
|
3
|
+
/** Point indices, CCW seen from outside. */
|
|
4
|
+
v: number[];
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Mesh {
|
|
8
|
+
positions: Vec3[];
|
|
9
|
+
faces: Face[];
|
|
10
|
+
}
|
|
11
|
+
/** An orthonormal right-handed frame: local x, y and z axes. */
|
|
12
|
+
export type Axes = [Vec3, Vec3, Vec3];
|
|
13
|
+
export type ViewName = 'front' | 'side' | 'top' | 'persp';
|
|
14
|
+
/** Custom camera: `dir` points from the target towards the eye. */
|
|
15
|
+
export interface ViewSpec {
|
|
16
|
+
dir: Vec3;
|
|
17
|
+
up: Vec3;
|
|
18
|
+
ortho?: boolean;
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Fit {
|
|
22
|
+
center: Vec3;
|
|
23
|
+
radius: number;
|
|
24
|
+
/** Perspective only: distance from the eye to the center. A first-person eye sits almost on the center. */
|
|
25
|
+
dist?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Camera {
|
|
28
|
+
name: string;
|
|
29
|
+
eye: Vec3;
|
|
30
|
+
f: Vec3;
|
|
31
|
+
r: Vec3;
|
|
32
|
+
u: Vec3;
|
|
33
|
+
ortho: boolean;
|
|
34
|
+
k: number;
|
|
35
|
+
size: number;
|
|
36
|
+
}
|
|
37
|
+
export interface Hit {
|
|
38
|
+
/** Index into `mesh.faces`. */
|
|
39
|
+
face: number;
|
|
40
|
+
/** Distance along the ray. */
|
|
41
|
+
distance: number;
|
|
42
|
+
point: Vec3;
|
|
43
|
+
/** Unit normal, facing the camera. */
|
|
44
|
+
normal: Vec3;
|
|
45
|
+
/** Whether the face itself faces the camera. */
|
|
46
|
+
front: boolean;
|
|
47
|
+
color?: string;
|
|
48
|
+
}
|
|
49
|
+
/** 3D polylines drawn on top of the view. */
|
|
50
|
+
export interface Mark {
|
|
51
|
+
name?: string;
|
|
52
|
+
lines: Vec3[][];
|
|
53
|
+
color?: string;
|
|
54
|
+
width?: number;
|
|
55
|
+
}
|
package/dist/types.js
ADDED
package/dist/vec.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Vec3 } from './types.js';
|
|
2
|
+
export declare const add: (a: Vec3, b: Vec3) => Vec3;
|
|
3
|
+
export declare const sub: (a: Vec3, b: Vec3) => Vec3;
|
|
4
|
+
export declare const scale: (a: Vec3, s: number) => Vec3;
|
|
5
|
+
export declare const dot: (a: Vec3, b: Vec3) => number;
|
|
6
|
+
export declare const cross: (a: Vec3, b: Vec3) => Vec3;
|
|
7
|
+
export declare const len: (a: Vec3) => number;
|
|
8
|
+
export declare const norm: (a: Vec3) => Vec3;
|
|
9
|
+
/** `a |> plus(b)` is a + b. */
|
|
10
|
+
export declare const plus: (b: Vec3) => (a: Vec3) => Vec3;
|
|
11
|
+
/** `a |> minus(b)` is a - b. */
|
|
12
|
+
export declare const minus: (b: Vec3) => (a: Vec3) => Vec3;
|
|
13
|
+
/** `a |> times(s)` is a * s. */
|
|
14
|
+
export declare const times: (s: number) => (a: Vec3) => Vec3;
|
package/dist/vec.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tiny vec3 helpers on plain [x, y, z] arrays.
|
|
2
|
+
// The curried ones (plus, minus, times) are made for pipelines: `b |> minus(a) |> norm`.
|
|
3
|
+
export const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
4
|
+
export const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
5
|
+
export const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
6
|
+
export const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
7
|
+
export const cross = (a, b) => [
|
|
8
|
+
a[1] * b[2] - a[2] * b[1],
|
|
9
|
+
a[2] * b[0] - a[0] * b[2],
|
|
10
|
+
a[0] * b[1] - a[1] * b[0],
|
|
11
|
+
];
|
|
12
|
+
export const len = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
13
|
+
export const norm = (a) => {
|
|
14
|
+
const l = len(a);
|
|
15
|
+
return l ? scale(a, 1 / l) : [0, 0, 0];
|
|
16
|
+
};
|
|
17
|
+
/** `a |> plus(b)` is a + b. */
|
|
18
|
+
export const plus = (b) => (a) => add(a, b);
|
|
19
|
+
/** `a |> minus(b)` is a - b. */
|
|
20
|
+
export const minus = (b) => (a) => sub(a, b);
|
|
21
|
+
/** `a |> times(s)` is a * s. */
|
|
22
|
+
export const times = (s) => (a) => scale(a, s);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const displayVersion: (version: string) => string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Versions are date based and stored as semver YY.M.DNNN (see scripts/bump.js).
|
|
2
|
+
// This gives the readable form: '26.11.1001' -> '26.11.01.001'.
|
|
3
|
+
export const displayVersion = (version) => {
|
|
4
|
+
const [yy, mm, p] = version.split('.').map(Number);
|
|
5
|
+
const pad = (n, w) => String(n).padStart(w, '0');
|
|
6
|
+
return `${pad(yy, 2)}.${pad(mm, 2)}.${pad(Math.floor(p / 1000), 2)}.${pad(p % 1000, 3)}`;
|
|
7
|
+
};
|
package/package.json
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "3draft",
|
|
3
|
-
"version": "26.11.
|
|
3
|
+
"version": "26.11.2005",
|
|
4
4
|
"description": "Minimalist AI first 3D engine: palette-texture, WASM core, WebGPU/WebGL2 renderer.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"!
|
|
16
|
+
"dist",
|
|
17
|
+
"!dist/**/*.test.*",
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --test",
|
|
22
|
+
"test": "node scripts/build.mjs && node --test \"dist/**/*.test.js\" \"examples/**/*.test.js\" \"mcp/**/*.test.js\" \"scripts/**/*.test.js\"",
|
|
23
23
|
"mcp": "node mcp/server.js",
|
|
24
24
|
"example": "node examples/table.js",
|
|
25
|
+
"bridge": "node examples/bridge.js",
|
|
26
|
+
"kernel": "node scripts/build-kernel.mjs",
|
|
25
27
|
"app": "node app/serve.js",
|
|
26
|
-
"bump": "node scripts/bump.js"
|
|
28
|
+
"bump": "node scripts/bump.js",
|
|
29
|
+
"build": "node scripts/build.mjs",
|
|
30
|
+
"build:check": "node scripts/build.mjs --check"
|
|
27
31
|
},
|
|
28
32
|
"keywords": [
|
|
29
33
|
"3d",
|
|
@@ -55,5 +59,9 @@
|
|
|
55
59
|
},
|
|
56
60
|
"publishConfig": {
|
|
57
61
|
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"@types/node": "^22.0.0"
|
|
58
66
|
}
|
|
59
67
|
}
|