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.
@@ -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
+ };
@@ -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
+ };
@@ -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
- const [a, b] = mesh.faces[hit.face].v;
14
- const ex = norm(sub(mesh.positions[b], mesh.positions[a]));
15
- const ey = hit.normal;
16
- const ez = cross(ex, ey);
17
- return box({
18
- size: [base, height, base],
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
+ };
@@ -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
@@ -0,0 +1,2 @@
1
+ // Shared types of the engine. Everything is plain data: arrays and objects.
2
+ export {};
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;
@@ -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.1013",
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": "./src/index.js",
7
- "types": "./src/index.d.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.d.ts",
11
- "default": "./src/index.js"
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
- "src",
17
- "!src/**/*.test.js",
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
  }