@yali-engine/cli 0.0.0-stage → 0.4.0
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/bin/yali.mjs +12 -0
- package/dist/budgets.d.ts +25 -0
- package/dist/budgets.js +27 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cdp.d.ts +25 -0
- package/dist/cdp.js +106 -0
- package/dist/cdp.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/cmd-bench.d.ts +17 -0
- package/dist/cmd-bench.js +74 -0
- package/dist/cmd-bench.js.map +1 -0
- package/dist/cmd-build.d.ts +13 -0
- package/dist/cmd-build.js +40 -0
- package/dist/cmd-build.js.map +1 -0
- package/dist/cmd-convert.d.ts +27 -0
- package/dist/cmd-convert.js +456 -0
- package/dist/cmd-convert.js.map +1 -0
- package/dist/cmd-create.d.ts +8 -0
- package/dist/cmd-create.js +248 -0
- package/dist/cmd-create.js.map +1 -0
- package/dist/cmd-dev.d.ts +9 -0
- package/dist/cmd-dev.js +24 -0
- package/dist/cmd-dev.js.map +1 -0
- package/dist/cmd-game.d.ts +24 -0
- package/dist/cmd-game.js +127 -0
- package/dist/cmd-game.js.map +1 -0
- package/dist/cmd-headless.d.ts +15 -0
- package/dist/cmd-headless.js +59 -0
- package/dist/cmd-headless.js.map +1 -0
- package/dist/cmd-inspect.d.ts +13 -0
- package/dist/cmd-inspect.js +64 -0
- package/dist/cmd-inspect.js.map +1 -0
- package/dist/cmd-test.d.ts +9 -0
- package/dist/cmd-test.js +68 -0
- package/dist/cmd-test.js.map +1 -0
- package/dist/collision.d.ts +31 -0
- package/dist/collision.js +471 -0
- package/dist/collision.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +105 -0
- package/dist/commands.js.map +1 -0
- package/dist/encoder.d.ts +63 -0
- package/dist/encoder.js +119 -0
- package/dist/encoder.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -0
- package/dist/main.js.map +1 -0
- package/dist/native-encoder.d.ts +39 -0
- package/dist/native-encoder.js +142 -0
- package/dist/native-encoder.js.map +1 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.js +27 -0
- package/dist/output.js.map +1 -0
- package/dist/png.d.ts +26 -0
- package/dist/png.js +134 -0
- package/dist/png.js.map +1 -0
- package/dist/project.d.ts +15 -0
- package/dist/project.js +35 -0
- package/dist/project.js.map +1 -0
- package/dist/size.d.ts +47 -0
- package/dist/size.js +100 -0
- package/dist/size.js.map +1 -0
- package/package.json +43 -3
- package/vendor/ENCODER-BUILD.json +33 -0
- package/vendor/LICENSE-basis_universal +201 -0
- package/vendor/LICENSE-zstd +30 -0
- package/vendor/NOTICE +33 -0
- package/vendor/basis_encoder.mjs +2 -0
- package/vendor/basis_encoder.wasm +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd-inspect.js","sourceRoot":"","sources":["../src/cmd-inspect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Outcome } from './output.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `yali test`: runs the project's tests with `node:test` (files under `test/`, or the ones
|
|
4
|
+
* given), TypeScript stripped, and summarises every test. Exit 1 if any failed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function testCommand(opts: {
|
|
7
|
+
root?: string;
|
|
8
|
+
files: readonly string[];
|
|
9
|
+
}): Promise<Outcome>;
|
package/dist/cmd-test.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { run } from 'node:test';
|
|
4
|
+
import { YaliError } from '@yali-engine/core';
|
|
5
|
+
import { ok } from "./output.js";
|
|
6
|
+
import { loadProject } from "./project.js";
|
|
7
|
+
/** `*.test.ts` / `*.test.js` files under `dir`, skipping dependencies and build output. */
|
|
8
|
+
function findTests(dir) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const walk = (d) => {
|
|
11
|
+
for (const e of readdirSync(d)) {
|
|
12
|
+
if (e === 'node_modules' || e === 'dist' || e.startsWith('.'))
|
|
13
|
+
continue;
|
|
14
|
+
const p = join(d, e);
|
|
15
|
+
if (statSync(p).isDirectory())
|
|
16
|
+
walk(p);
|
|
17
|
+
else if (/\.test\.(ts|mts|js|mjs)$/.test(e))
|
|
18
|
+
out.push(p);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
if (existsSync(dir))
|
|
22
|
+
walk(dir);
|
|
23
|
+
return out.sort();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `yali test`: runs the project's tests with `node:test` (files under `test/`, or the ones
|
|
27
|
+
* given), TypeScript stripped, and summarises every test. Exit 1 if any failed.
|
|
28
|
+
*/
|
|
29
|
+
export async function testCommand(opts) {
|
|
30
|
+
const project = await loadProject(opts.root);
|
|
31
|
+
const files = opts.files.length ? opts.files.map((f) => resolve(project.root, f)) : findTests(join(project.root, 'test'));
|
|
32
|
+
if (files.length === 0) {
|
|
33
|
+
throw new YaliError('NO_TESTS', `No *.test.ts files under ${join(project.root, 'test')}.`, 'Add tests under test/, or pass the files to run.');
|
|
34
|
+
}
|
|
35
|
+
const rows = [];
|
|
36
|
+
const t0 = performance.now();
|
|
37
|
+
// Under an outer `node --test` (CI scripts, this package's own tests) the variable makes
|
|
38
|
+
// the test files report to that outer runner, and this run would see no tests at all.
|
|
39
|
+
delete process.env.NODE_TEST_CONTEXT;
|
|
40
|
+
const stream = run({ files, cwd: project.root, execArgv: ['--experimental-strip-types', '--no-warnings'] });
|
|
41
|
+
const fileNames = new Set(files);
|
|
42
|
+
const record = (d, passed) => {
|
|
43
|
+
// Each file is wrapped in a test named by its path; report the tests inside it.
|
|
44
|
+
if (d.details.type === 'suite' || (d.file && fileNames.has(d.name)) || d.name === d.file)
|
|
45
|
+
return;
|
|
46
|
+
const err = d.details.error;
|
|
47
|
+
rows.push({
|
|
48
|
+
name: d.name,
|
|
49
|
+
file: relative(project.root, d.file ?? '').split(sep).join('/'),
|
|
50
|
+
ok: passed,
|
|
51
|
+
ms: Math.round(d.details.duration_ms * 10) / 10,
|
|
52
|
+
...(passed ? {} : { error: String(err?.cause?.message ?? err?.message ?? 'failed') }),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
stream.on('test:pass', (d) => record(d, true));
|
|
56
|
+
stream.on('test:fail', (d) => record(d, false));
|
|
57
|
+
// Drain the stream; it ends when every file has finished.
|
|
58
|
+
for await (const _ of stream)
|
|
59
|
+
void _;
|
|
60
|
+
const failed = rows.filter((r) => !r.ok);
|
|
61
|
+
const result = { files: files.map((f) => relative(project.root, f).split(sep).join('/')), passed: rows.length - failed.length, failed: failed.length, ms: Math.round(performance.now() - t0), tests: rows };
|
|
62
|
+
const text = [
|
|
63
|
+
...rows.map((r) => `${r.ok ? 'ok ' : 'FAIL'} ${r.name}${r.ok ? '' : `\n ${r.error}`}`),
|
|
64
|
+
`${result.passed} passed, ${result.failed} failed (${result.ms} ms)`,
|
|
65
|
+
];
|
|
66
|
+
return ok('test', result, text, failed.length ? 1 : 0);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=cmd-test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd-test.js","sourceRoot":"","sources":["../src/cmd-test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CollisionPiece } from '@yali-engine/physics';
|
|
2
|
+
/** What decomposition takes. */
|
|
3
|
+
export interface DecomposeOptions {
|
|
4
|
+
/** Voxels along the longest side (default 32). */
|
|
5
|
+
readonly resolution?: number;
|
|
6
|
+
/** Most hulls per mesh (default 16). */
|
|
7
|
+
readonly maxHulls?: number;
|
|
8
|
+
/** Wasted volume a piece may keep, a fraction of its hull (default 0.08). */
|
|
9
|
+
readonly concavity?: number;
|
|
10
|
+
}
|
|
11
|
+
/** A node's collision role by its name. */
|
|
12
|
+
export declare function collisionRole(name: string): 'convex' | 'heightfield' | 'none' | 'mesh';
|
|
13
|
+
/** The convex hull of `pts` (xyz triples): its vertices' indices and its faces (index triples, outward). */
|
|
14
|
+
export declare function convexHull(pts: Float64Array | Float32Array): {
|
|
15
|
+
vertices: number[];
|
|
16
|
+
faces: [number, number, number][];
|
|
17
|
+
volume: number;
|
|
18
|
+
};
|
|
19
|
+
/** Splits a closed-or-not triangle mesh into convex pieces (each a point cloud for `Physics.addHull`). */
|
|
20
|
+
export declare function decompose(positions: Float32Array, indices: Uint32Array, o?: DecomposeOptions): Float32Array[];
|
|
21
|
+
/** A mesh's highest surface sampled on a grid about as fine as its vertices (2–512 per side). */
|
|
22
|
+
export declare function heightfieldOf(positions: Float32Array, indices: Uint32Array): Extract<CollisionPiece, {
|
|
23
|
+
kind: 'heightfield';
|
|
24
|
+
}> & {
|
|
25
|
+
node: number;
|
|
26
|
+
};
|
|
27
|
+
/** Welds identical positions; returns positions and remapped indices. */
|
|
28
|
+
export declare function weld(positions: Float32Array, indices: Uint32Array): {
|
|
29
|
+
vertices: Float32Array;
|
|
30
|
+
indices: Uint32Array;
|
|
31
|
+
};
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// `yali convert --collision` (ADR-0090): a glTF level's collision, baked into the level's frame.
|
|
2
|
+
// Each mesh node becomes, by its name (the conventions Unreal and Godot use):
|
|
3
|
+
//
|
|
4
|
+
// - `UCX_*`, `UBX_*`, `USP_*`, `UCP_*`, `*-convcol`, `*_convex`: convex pieces, from an
|
|
5
|
+
// approximate convex decomposition (below);
|
|
6
|
+
// - `*_heightfield`, `*-hf`: a heightfield sampled from the mesh (its highest surface);
|
|
7
|
+
// - `UNC_*`, `*-nocol`: nothing;
|
|
8
|
+
// - anything else: a static triangle mesh (welded).
|
|
9
|
+
//
|
|
10
|
+
// The decomposition: the mesh is voxelized solid (a parity fill along X, plus every voxel a
|
|
11
|
+
// triangle crosses, so open meshes and thin walls survive), then split best-first. The piece
|
|
12
|
+
// whose hull wastes the most volume (hull volume − its voxels' volume) is cut by the axis plane
|
|
13
|
+
// whose two hulls cover the least volume, until every piece is within `concavity` (a fraction of
|
|
14
|
+
// its hull) or there are `maxHulls`. Each piece's hull is the convex hull of the mesh's surface
|
|
15
|
+
// samples inside its voxels (so it does not grow past the surface), trimmed to at most 64 points
|
|
16
|
+
// by support sampling. V-HACD's idea (Mamou), without its
|
|
17
|
+
// clipping or merging: coarser, smaller, deterministic.
|
|
18
|
+
/** A node's collision role by its name. */
|
|
19
|
+
export function collisionRole(name) {
|
|
20
|
+
if (/^(UCX|UBX|USP|UCP)_/i.test(name) || /[-_](convcol|convex)$/i.test(name))
|
|
21
|
+
return 'convex';
|
|
22
|
+
if (/[-_](heightfield|hf)$/i.test(name))
|
|
23
|
+
return 'heightfield';
|
|
24
|
+
if (/^UNC_/i.test(name) || /[-_]nocol$/i.test(name))
|
|
25
|
+
return 'none';
|
|
26
|
+
return 'mesh';
|
|
27
|
+
}
|
|
28
|
+
// ---- Convex hull (incremental, in f64) -------------------------------------------------------
|
|
29
|
+
/** The convex hull of `pts` (xyz triples): its vertices' indices and its faces (index triples, outward). */
|
|
30
|
+
export function convexHull(pts) {
|
|
31
|
+
const n = pts.length / 3;
|
|
32
|
+
const P = (i) => [pts[i * 3], pts[i * 3 + 1], pts[i * 3 + 2]];
|
|
33
|
+
if (n < 4)
|
|
34
|
+
return { vertices: [...Array(n).keys()], faces: [], volume: 0 };
|
|
35
|
+
let lo = [Infinity, Infinity, Infinity];
|
|
36
|
+
let hi = [-Infinity, -Infinity, -Infinity];
|
|
37
|
+
for (let i = 0; i < n; i++)
|
|
38
|
+
for (let c = 0; c < 3; c++) {
|
|
39
|
+
lo[c] = Math.min(lo[c], pts[i * 3 + c]);
|
|
40
|
+
hi[c] = Math.max(hi[c], pts[i * 3 + c]);
|
|
41
|
+
}
|
|
42
|
+
const eps = 1e-9 * Math.max(1, hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]);
|
|
43
|
+
// The initial tetrahedron: two extreme points, the farthest from their line, the farthest from their plane.
|
|
44
|
+
let a = 0;
|
|
45
|
+
let b = 0;
|
|
46
|
+
for (let i = 1; i < n; i++) {
|
|
47
|
+
if (pts[i * 3] < pts[a * 3])
|
|
48
|
+
a = i;
|
|
49
|
+
if (pts[i * 3] > pts[b * 3])
|
|
50
|
+
b = i;
|
|
51
|
+
}
|
|
52
|
+
if (a === b) {
|
|
53
|
+
for (let i = 0; i < n; i++)
|
|
54
|
+
if (dist2(P(i), P(a)) > dist2(P(b), P(a)))
|
|
55
|
+
b = i;
|
|
56
|
+
}
|
|
57
|
+
let c = -1;
|
|
58
|
+
let best = eps;
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
const d = len(cross(sub(P(b), P(a)), sub(P(i), P(a))));
|
|
61
|
+
if (d > best)
|
|
62
|
+
[best, c] = [d, i];
|
|
63
|
+
}
|
|
64
|
+
if (c < 0)
|
|
65
|
+
return { vertices: [a, b], faces: [], volume: 0 };
|
|
66
|
+
let d = -1;
|
|
67
|
+
best = eps;
|
|
68
|
+
const nrm = cross(sub(P(b), P(a)), sub(P(c), P(a)));
|
|
69
|
+
for (let i = 0; i < n; i++) {
|
|
70
|
+
const h = Math.abs(dot(nrm, sub(P(i), P(a))));
|
|
71
|
+
if (h > best)
|
|
72
|
+
[best, d] = [h, i];
|
|
73
|
+
}
|
|
74
|
+
if (d < 0)
|
|
75
|
+
return { vertices: [a, b, c], faces: [], volume: 0 };
|
|
76
|
+
const inside = scale(add(add(P(a), P(b)), add(P(c), P(d))), 0.25);
|
|
77
|
+
const faces = [];
|
|
78
|
+
const make = (i, j, k) => {
|
|
79
|
+
let nn = cross(sub(P(j), P(i)), sub(P(k), P(i)));
|
|
80
|
+
if (dot(nn, sub(inside, P(i))) > 0) {
|
|
81
|
+
[j, k] = [k, j];
|
|
82
|
+
nn = scale(nn, -1);
|
|
83
|
+
}
|
|
84
|
+
const l = len(nn) || 1;
|
|
85
|
+
nn = scale(nn, 1 / l);
|
|
86
|
+
faces.push({ v: [i, j, k], n: nn, off: dot(nn, P(i)), alive: true });
|
|
87
|
+
};
|
|
88
|
+
make(a, b, c);
|
|
89
|
+
make(a, b, d);
|
|
90
|
+
make(a, c, d);
|
|
91
|
+
make(b, c, d);
|
|
92
|
+
const used = new Set([a, b, c, d]);
|
|
93
|
+
for (let p = 0; p < n; p++) {
|
|
94
|
+
if (used.has(p))
|
|
95
|
+
continue;
|
|
96
|
+
const q = P(p);
|
|
97
|
+
const visible = [];
|
|
98
|
+
for (let f = 0; f < faces.length; f++)
|
|
99
|
+
if (faces[f].alive && dot(faces[f].n, q) - faces[f].off > eps)
|
|
100
|
+
visible.push(f);
|
|
101
|
+
if (visible.length === 0)
|
|
102
|
+
continue;
|
|
103
|
+
// The horizon: edges of visible faces whose twin is not visible.
|
|
104
|
+
const edges = new Map();
|
|
105
|
+
for (const f of visible) {
|
|
106
|
+
faces[f].alive = false;
|
|
107
|
+
const [i, j, k] = faces[f].v;
|
|
108
|
+
for (const [u, w] of [[i, j], [j, k], [k, i]]) {
|
|
109
|
+
const twin = `${w},${u}`;
|
|
110
|
+
if (edges.has(twin))
|
|
111
|
+
edges.delete(twin);
|
|
112
|
+
else
|
|
113
|
+
edges.set(`${u},${w}`, [u, w]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [u, w] of edges.values())
|
|
117
|
+
make(u, w, p);
|
|
118
|
+
used.add(p);
|
|
119
|
+
}
|
|
120
|
+
const live = faces.filter((f) => f.alive);
|
|
121
|
+
const verts = new Set();
|
|
122
|
+
let volume = 0;
|
|
123
|
+
for (const f of live) {
|
|
124
|
+
f.v.forEach((v) => verts.add(v));
|
|
125
|
+
volume += dot(P(f.v[0]), cross(P(f.v[1]), P(f.v[2]))) / 6;
|
|
126
|
+
}
|
|
127
|
+
return { vertices: [...verts].sort((x, y) => x - y), faces: live.map((f) => f.v), volume: Math.abs(volume) };
|
|
128
|
+
}
|
|
129
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
130
|
+
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
131
|
+
const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
132
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
133
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
134
|
+
const len = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
135
|
+
const dist2 = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
|
|
136
|
+
/** At most `max` of the hull's points: the support points in `max` evenly spread directions. */
|
|
137
|
+
function trimHull(points, max) {
|
|
138
|
+
if (points.length <= max)
|
|
139
|
+
return points;
|
|
140
|
+
const out = new Set();
|
|
141
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
142
|
+
for (let i = 0; i < max; i++) {
|
|
143
|
+
const y = 1 - (2 * (i + 0.5)) / max;
|
|
144
|
+
const r = Math.sqrt(1 - y * y);
|
|
145
|
+
const d = [Math.cos(golden * i) * r, y, Math.sin(golden * i) * r];
|
|
146
|
+
let bi = 0;
|
|
147
|
+
let bd = -Infinity;
|
|
148
|
+
points.forEach((p, k) => {
|
|
149
|
+
const s = p[0] * d[0] + p[1] * d[1] + p[2] * d[2];
|
|
150
|
+
if (s > bd)
|
|
151
|
+
[bd, bi] = [s, k];
|
|
152
|
+
});
|
|
153
|
+
out.add(bi);
|
|
154
|
+
}
|
|
155
|
+
return [...out].sort((x, y) => x - y).map((k) => points[k]);
|
|
156
|
+
}
|
|
157
|
+
// ---- Voxelization and decomposition --------------------------------------------------------
|
|
158
|
+
/** Splits a closed-or-not triangle mesh into convex pieces (each a point cloud for `Physics.addHull`). */
|
|
159
|
+
export function decompose(positions, indices, o = {}) {
|
|
160
|
+
const res = Math.max(4, Math.min(128, o.resolution ?? 32));
|
|
161
|
+
const maxHulls = Math.max(1, o.maxHulls ?? 16);
|
|
162
|
+
const concavity = o.concavity ?? 0.08;
|
|
163
|
+
const nv = positions.length / 3;
|
|
164
|
+
const lo = [Infinity, Infinity, Infinity];
|
|
165
|
+
const hi = [-Infinity, -Infinity, -Infinity];
|
|
166
|
+
for (let i = 0; i < nv; i++)
|
|
167
|
+
for (let c = 0; c < 3; c++) {
|
|
168
|
+
lo[c] = Math.min(lo[c], positions[i * 3 + c]);
|
|
169
|
+
hi[c] = Math.max(hi[c], positions[i * 3 + c]);
|
|
170
|
+
}
|
|
171
|
+
const ext = sub(hi, lo);
|
|
172
|
+
const size = Math.max(ext[0], ext[1], ext[2]) / res || 1;
|
|
173
|
+
const dims = ext.map((e) => Math.max(1, Math.ceil(e / size)));
|
|
174
|
+
const [nx, ny, nz] = dims;
|
|
175
|
+
const idx = (x, y, z) => (z * ny + y) * nx + x;
|
|
176
|
+
const solid = new Uint8Array(nx * ny * nz);
|
|
177
|
+
const P = (i) => [positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]];
|
|
178
|
+
// Parity fill along X: each column's crossings, sorted, fill between pairs.
|
|
179
|
+
const crossings = Array.from({ length: ny * nz }, () => []);
|
|
180
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
181
|
+
const [a, b, c] = [P(indices[t]), P(indices[t + 1]), P(indices[t + 2])];
|
|
182
|
+
const y0 = Math.max(0, Math.floor((Math.min(a[1], b[1], c[1]) - lo[1]) / size));
|
|
183
|
+
const y1 = Math.min(ny - 1, Math.floor((Math.max(a[1], b[1], c[1]) - lo[1]) / size));
|
|
184
|
+
const z0 = Math.max(0, Math.floor((Math.min(a[2], b[2], c[2]) - lo[2]) / size));
|
|
185
|
+
const z1 = Math.min(nz - 1, Math.floor((Math.max(a[2], b[2], c[2]) - lo[2]) / size));
|
|
186
|
+
for (let z = z0; z <= z1; z++) {
|
|
187
|
+
for (let y = y0; y <= y1; y++) {
|
|
188
|
+
const py = lo[1] + (y + 0.5) * size;
|
|
189
|
+
const pz = lo[2] + (z + 0.5) * size;
|
|
190
|
+
// The ray (x, py, pz) against the triangle, in the YZ plane.
|
|
191
|
+
const d = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2]);
|
|
192
|
+
if (Math.abs(d) < 1e-12)
|
|
193
|
+
continue;
|
|
194
|
+
const u = ((py - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (pz - a[2])) / d;
|
|
195
|
+
const v = ((b[1] - a[1]) * (pz - a[2]) - (py - a[1]) * (b[2] - a[2])) / d;
|
|
196
|
+
if (u < 0 || v < 0 || u + v > 1)
|
|
197
|
+
continue;
|
|
198
|
+
crossings[z * ny + y].push(a[0] + u * (b[0] - a[0]) + v * (c[0] - a[0]));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
crossings.forEach((xs, k) => {
|
|
203
|
+
if (xs.length < 2)
|
|
204
|
+
return;
|
|
205
|
+
xs.sort((p, q) => p - q);
|
|
206
|
+
const y = k % ny;
|
|
207
|
+
const z = Math.floor(k / ny);
|
|
208
|
+
for (let i = 0; i + 1 < xs.length; i += 2) {
|
|
209
|
+
const x0 = Math.max(0, Math.ceil((xs[i] - lo[0]) / size - 0.5));
|
|
210
|
+
const x1 = Math.min(nx - 1, Math.floor((xs[i + 1] - lo[0]) / size - 0.5));
|
|
211
|
+
for (let x = x0; x <= x1; x++)
|
|
212
|
+
solid[idx(x, y, z)] = 1;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Every voxel a triangle passes through (sampled at a third of a voxel); the samples are kept,
|
|
216
|
+
// each with its voxel: a piece's hull is the hull of its surface samples (on the true surface,
|
|
217
|
+
// not a voxel's corner out).
|
|
218
|
+
const samples = [];
|
|
219
|
+
const sampleVoxel = [];
|
|
220
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
221
|
+
const [a, b, c] = [P(indices[t]), P(indices[t + 1]), P(indices[t + 2])];
|
|
222
|
+
const span = Math.max(len(sub(b, a)), len(sub(c, a)), len(sub(c, b)));
|
|
223
|
+
const steps = Math.max(1, Math.ceil((3 * span) / size));
|
|
224
|
+
// A sample on a voxel boundary goes to the voxel on the solid side of its own face: nudged a
|
|
225
|
+
// hair inward (against the outward normal) and towards the triangle's centre (an edge sample
|
|
226
|
+
// belongs to its face, not the next column) before it is binned. The sample stays exact.
|
|
227
|
+
const nrm = cross(sub(b, a), sub(c, a));
|
|
228
|
+
const nl = len(nrm);
|
|
229
|
+
const inward = nl > 0 ? scale(nrm, -1e-3 * size / nl) : [0, 0, 0];
|
|
230
|
+
const centre = scale(add(add(a, b), c), 1 / 3);
|
|
231
|
+
for (let i = 0; i <= steps; i++) {
|
|
232
|
+
for (let j = 0; j <= steps - i; j++) {
|
|
233
|
+
const u = i / steps;
|
|
234
|
+
const v = j / steps;
|
|
235
|
+
const p = add(a, add(scale(sub(b, a), u), scale(sub(c, a), v)));
|
|
236
|
+
const toCentre = sub(centre, p);
|
|
237
|
+
const tl = len(toCentre);
|
|
238
|
+
const q = add(add(p, inward), tl > 0 ? scale(toCentre, (1e-3 * size) / tl) : [0, 0, 0]);
|
|
239
|
+
const x = Math.min(nx - 1, Math.max(0, Math.floor((q[0] - lo[0]) / size)));
|
|
240
|
+
const y = Math.min(ny - 1, Math.max(0, Math.floor((q[1] - lo[1]) / size)));
|
|
241
|
+
const z = Math.min(nz - 1, Math.max(0, Math.floor((q[2] - lo[2]) / size)));
|
|
242
|
+
solid[idx(x, y, z)] = 1;
|
|
243
|
+
samples.push(p[0], p[1], p[2]);
|
|
244
|
+
sampleVoxel.push(idx(x, y, z));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const all = [];
|
|
249
|
+
for (let i = 0; i < solid.length; i++)
|
|
250
|
+
if (solid[i])
|
|
251
|
+
all.push(i);
|
|
252
|
+
if (all.length === 0)
|
|
253
|
+
return [];
|
|
254
|
+
const coord = (i) => [i % nx, Math.floor(i / nx) % ny, Math.floor(i / (nx * ny))];
|
|
255
|
+
// Per voxel, only its samples extreme in 14 directions (the axes and the cube's diagonals):
|
|
256
|
+
// the only ones a hull can use.
|
|
257
|
+
const DIRS = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
258
|
+
for (let k = 0; k < 8; k++)
|
|
259
|
+
DIRS.push([k & 1 ? 1 : -1, k & 2 ? 1 : -1, k & 4 ? 1 : -1]);
|
|
260
|
+
const extremes = new Map();
|
|
261
|
+
{
|
|
262
|
+
const best = new Map();
|
|
263
|
+
sampleVoxel.forEach((v, k) => {
|
|
264
|
+
let ex = extremes.get(v);
|
|
265
|
+
let bs = best.get(v);
|
|
266
|
+
if (!ex || !bs) {
|
|
267
|
+
ex = new Array(DIRS.length).fill(k);
|
|
268
|
+
bs = new Float64Array(DIRS.length).fill(-Infinity);
|
|
269
|
+
extremes.set(v, ex);
|
|
270
|
+
best.set(v, bs);
|
|
271
|
+
}
|
|
272
|
+
for (let d = 0; d < DIRS.length; d++) {
|
|
273
|
+
const sd = samples[k * 3] * DIRS[d][0] + samples[k * 3 + 1] * DIRS[d][1] + samples[k * 3 + 2] * DIRS[d][2];
|
|
274
|
+
if (sd > bs[d]) {
|
|
275
|
+
bs[d] = sd;
|
|
276
|
+
ex[d] = k;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
for (const [v, ex] of extremes)
|
|
281
|
+
extremes.set(v, [...new Set(ex)]);
|
|
282
|
+
}
|
|
283
|
+
/** The support points of `pts` in 64 spread directions (cheap), then their exact hull. */
|
|
284
|
+
const SUPPORT = [];
|
|
285
|
+
{
|
|
286
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
287
|
+
for (let i = 0; i < 64; i++) {
|
|
288
|
+
const y = 1 - (2 * (i + 0.5)) / 64;
|
|
289
|
+
const r = Math.sqrt(1 - y * y);
|
|
290
|
+
SUPPORT.push([Math.cos(golden * i) * r, y, Math.sin(golden * i) * r]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const supportHull = (pts) => {
|
|
294
|
+
const picked = new Set();
|
|
295
|
+
for (const d of SUPPORT) {
|
|
296
|
+
let bi = 0;
|
|
297
|
+
let bd = -Infinity;
|
|
298
|
+
for (let k = 0; k < pts.length; k += 3) {
|
|
299
|
+
const sd = pts[k] * d[0] + pts[k + 1] * d[1] + pts[k + 2] * d[2];
|
|
300
|
+
if (sd > bd)
|
|
301
|
+
[bd, bi] = [sd, k];
|
|
302
|
+
}
|
|
303
|
+
picked.add(bi);
|
|
304
|
+
}
|
|
305
|
+
const out = [];
|
|
306
|
+
for (const k of [...picked].sort((x, y) => x - y))
|
|
307
|
+
out.push(pts[k], pts[k + 1], pts[k + 2]);
|
|
308
|
+
return { h: convexHull(new Float64Array(out)), pts: out };
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* A piece's hull: of the surface samples in its voxels (clamped to the voxels' box: a sample on
|
|
312
|
+
* a voxel boundary may lie on a neighbouring piece's face), or, for a piece with too few, of its
|
|
313
|
+
* voxels' corners.
|
|
314
|
+
*/
|
|
315
|
+
const hullOf = (voxels) => {
|
|
316
|
+
const bmin = [Infinity, Infinity, Infinity];
|
|
317
|
+
const bmax = [-Infinity, -Infinity, -Infinity];
|
|
318
|
+
for (const v of voxels) {
|
|
319
|
+
const c = coord(v);
|
|
320
|
+
for (let a = 0; a < 3; a++) {
|
|
321
|
+
bmin[a] = Math.min(bmin[a], lo[a] + c[a] * size);
|
|
322
|
+
bmax[a] = Math.max(bmax[a], lo[a] + (c[a] + 1) * size);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const own = [];
|
|
326
|
+
for (const v of voxels) {
|
|
327
|
+
for (const k of extremes.get(v) ?? []) {
|
|
328
|
+
for (let a = 0; a < 3; a++)
|
|
329
|
+
own.push(Math.min(bmax[a], Math.max(bmin[a], samples[k * 3 + a])));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (own.length >= 12) {
|
|
333
|
+
const r = supportHull(own);
|
|
334
|
+
if (r.h.volume > 0)
|
|
335
|
+
return r;
|
|
336
|
+
}
|
|
337
|
+
const corners = [];
|
|
338
|
+
for (const v of voxels) {
|
|
339
|
+
const [x, y, z] = coord(v);
|
|
340
|
+
for (let k = 0; k < 8; k++)
|
|
341
|
+
corners.push(lo[0] + (x + (k & 1)) * size, lo[1] + (y + ((k >> 1) & 1)) * size, lo[2] + (z + ((k >> 2) & 1)) * size);
|
|
342
|
+
}
|
|
343
|
+
return supportHull(corners);
|
|
344
|
+
};
|
|
345
|
+
const voxelVolume = size ** 3;
|
|
346
|
+
const piece = (voxels) => {
|
|
347
|
+
const hull = hullOf(voxels);
|
|
348
|
+
return { voxels, hull, waste: Math.max(0, hull.h.volume - voxels.length * voxelVolume) };
|
|
349
|
+
};
|
|
350
|
+
const pieces = [piece(all)];
|
|
351
|
+
while (pieces.length < maxHulls) {
|
|
352
|
+
// The piece wasting the most volume, if it is over the threshold.
|
|
353
|
+
let wi = -1;
|
|
354
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
355
|
+
const p = pieces[i];
|
|
356
|
+
if (p.waste > concavity * p.hull.h.volume && p.voxels.length > 1 && (wi < 0 || p.waste > pieces[wi].waste))
|
|
357
|
+
wi = i;
|
|
358
|
+
}
|
|
359
|
+
if (wi < 0)
|
|
360
|
+
break;
|
|
361
|
+
const p = pieces[wi];
|
|
362
|
+
// The cut whose two hulls cover the least space (waste, clamped at 0 and measured against
|
|
363
|
+
// voxels that overhang the surface, ties between good cuts; hull volume does not).
|
|
364
|
+
let bestSplit = null;
|
|
365
|
+
let bestVolume = p.hull.h.volume;
|
|
366
|
+
const pc = p.voxels.map(coord);
|
|
367
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
368
|
+
let mn = Infinity;
|
|
369
|
+
let mx = -Infinity;
|
|
370
|
+
for (const c of pc) {
|
|
371
|
+
mn = Math.min(mn, c[axis]);
|
|
372
|
+
mx = Math.max(mx, c[axis]);
|
|
373
|
+
}
|
|
374
|
+
if (mx <= mn)
|
|
375
|
+
continue;
|
|
376
|
+
const step = Math.max(1, Math.floor((mx - mn) / 8));
|
|
377
|
+
for (let cut = mn + 1; cut <= mx; cut += step) {
|
|
378
|
+
const left = [];
|
|
379
|
+
const right = [];
|
|
380
|
+
p.voxels.forEach((v, k) => (pc[k][axis] < cut ? left : right).push(v));
|
|
381
|
+
if (left.length === 0 || right.length === 0)
|
|
382
|
+
continue;
|
|
383
|
+
const l = piece(left);
|
|
384
|
+
const r = piece(right);
|
|
385
|
+
const v = l.hull.h.volume + r.hull.h.volume;
|
|
386
|
+
if (v < bestVolume - 1e-9 * bestVolume) {
|
|
387
|
+
bestVolume = v;
|
|
388
|
+
bestSplit = [l, r];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!bestSplit) {
|
|
393
|
+
// No cut helps: accept it as is.
|
|
394
|
+
p.waste = 0;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
pieces.splice(wi, 1, ...bestSplit);
|
|
398
|
+
}
|
|
399
|
+
return pieces.map((p) => {
|
|
400
|
+
const pts = p.hull.h.vertices.map((v) => [p.hull.pts[v * 3], p.hull.pts[v * 3 + 1], p.hull.pts[v * 3 + 2]]);
|
|
401
|
+
return Float32Array.from(trimHull(pts, 64).flat());
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// ---- Heightfields ----------------------------------------------------------------------------
|
|
405
|
+
/** A mesh's highest surface sampled on a grid about as fine as its vertices (2–512 per side). */
|
|
406
|
+
export function heightfieldOf(positions, indices) {
|
|
407
|
+
const nv = positions.length / 3;
|
|
408
|
+
let [x0, x1, z0, z1, y0] = [Infinity, -Infinity, Infinity, -Infinity, Infinity];
|
|
409
|
+
for (let i = 0; i < nv; i++) {
|
|
410
|
+
x0 = Math.min(x0, positions[i * 3]);
|
|
411
|
+
x1 = Math.max(x1, positions[i * 3]);
|
|
412
|
+
y0 = Math.min(y0, positions[i * 3 + 1]);
|
|
413
|
+
z0 = Math.min(z0, positions[i * 3 + 2]);
|
|
414
|
+
z1 = Math.max(z1, positions[i * 3 + 2]);
|
|
415
|
+
}
|
|
416
|
+
const per = Math.max(2, Math.min(512, Math.round(Math.sqrt(nv))));
|
|
417
|
+
const sx = x1 - x0 || 1;
|
|
418
|
+
const sz = z1 - z0 || 1;
|
|
419
|
+
const cols = Math.max(2, Math.min(512, Math.round((per * sx) / Math.max(sx, sz)) + 1));
|
|
420
|
+
const rows = Math.max(2, Math.min(512, Math.round((per * sz) / Math.max(sx, sz)) + 1));
|
|
421
|
+
const cx = sx / (cols - 1);
|
|
422
|
+
const cz = sz / (rows - 1);
|
|
423
|
+
const h = new Float32Array(rows * cols).fill(-Infinity);
|
|
424
|
+
const P = (i) => [positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]];
|
|
425
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
426
|
+
const [a, b, c] = [P(indices[t]), P(indices[t + 1]), P(indices[t + 2])];
|
|
427
|
+
const c0 = Math.max(0, Math.floor((Math.min(a[0], b[0], c[0]) - x0) / cx));
|
|
428
|
+
const c1 = Math.min(cols - 1, Math.ceil((Math.max(a[0], b[0], c[0]) - x0) / cx));
|
|
429
|
+
const r0 = Math.max(0, Math.floor((Math.min(a[2], b[2], c[2]) - z0) / cz));
|
|
430
|
+
const r1 = Math.min(rows - 1, Math.ceil((Math.max(a[2], b[2], c[2]) - z0) / cz));
|
|
431
|
+
const d = (b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2]);
|
|
432
|
+
if (Math.abs(d) < 1e-12)
|
|
433
|
+
continue;
|
|
434
|
+
for (let r = r0; r <= r1; r++) {
|
|
435
|
+
for (let col = c0; col <= c1; col++) {
|
|
436
|
+
const px = x0 + col * cx;
|
|
437
|
+
const pz = z0 + r * cz;
|
|
438
|
+
const u = ((px - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (pz - a[2])) / d;
|
|
439
|
+
const v = ((b[0] - a[0]) * (pz - a[2]) - (px - a[0]) * (b[2] - a[2])) / d;
|
|
440
|
+
if (u < -1e-6 || v < -1e-6 || u + v > 1 + 1e-6)
|
|
441
|
+
continue;
|
|
442
|
+
const y = a[1] + u * (b[1] - a[1]) + v * (c[1] - a[1]);
|
|
443
|
+
if (y > h[r * cols + col])
|
|
444
|
+
h[r * cols + col] = y;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Holes (no triangle above a sample) take the lowest height.
|
|
449
|
+
for (let i = 0; i < h.length; i++)
|
|
450
|
+
if (!Number.isFinite(h[i]))
|
|
451
|
+
h[i] = y0;
|
|
452
|
+
return { kind: 'heightfield', node: -1, rows, cols, cell: [cx, cz], center: [(x0 + x1) / 2, 0, (z0 + z1) / 2], heights: h };
|
|
453
|
+
}
|
|
454
|
+
/** Welds identical positions; returns positions and remapped indices. */
|
|
455
|
+
export function weld(positions, indices) {
|
|
456
|
+
const map = new Map();
|
|
457
|
+
const out = [];
|
|
458
|
+
const remap = new Uint32Array(positions.length / 3);
|
|
459
|
+
for (let i = 0; i < positions.length / 3; i++) {
|
|
460
|
+
const key = `${positions[i * 3]},${positions[i * 3 + 1]},${positions[i * 3 + 2]}`;
|
|
461
|
+
let k = map.get(key);
|
|
462
|
+
if (k === undefined) {
|
|
463
|
+
k = out.length / 3;
|
|
464
|
+
map.set(key, k);
|
|
465
|
+
out.push(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
|
|
466
|
+
}
|
|
467
|
+
remap[i] = k;
|
|
468
|
+
}
|
|
469
|
+
return { vertices: Float32Array.from(out), indices: indices.map((i) => remap[i]) };
|
|
470
|
+
}
|
|
471
|
+
//# sourceMappingURL=collision.js.map
|