@genex-ai/multiplayer 0.17.6 → 0.17.9
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/PROTOCOL.md +102 -31
- package/README.md +35 -14
- package/WORLD-GLTF.md +109 -0
- package/WORLD-SCENE.md +135 -0
- package/dist/authority/fast-udp.d.ts +4 -2
- package/dist/authority/fast-udp.js +8 -46
- package/dist/authority/fast-webtransport.d.ts +26 -4
- package/dist/authority/fast-webtransport.js +12 -115
- package/dist/authority/index.d.ts +379 -41
- package/dist/authority/index.js +247 -47
- package/dist/authority/module.d.ts +2 -1
- package/dist/authority/module.js +1 -1
- package/dist/bridge-authority.d.ts +3 -2
- package/dist/bridge-authority.global.js +10 -9
- package/dist/bridge-authority.js +11 -6
- package/dist/bridge.global.js +5 -5
- package/dist/bridge.js +3 -3
- package/dist/chunk-2NGXXQDS.js +1916 -0
- package/dist/{chunk-7QVOIHJF.js → chunk-DJ2XYETS.js} +2 -2
- package/dist/{chunk-Y6XHXDIK.js → chunk-MQYARUGU.js} +864 -539
- package/dist/{chunk-K3SF5VDS.js → chunk-OD6NRSX2.js} +3 -1
- package/dist/{chunk-UX3F6YTV.js → chunk-TEHGMFAG.js} +1 -1
- package/dist/chunk-TSYEWPVO.js +44 -0
- package/dist/chunk-TVTVIMIH.js +675 -0
- package/dist/{chunk-ZSWF4VCN.js → chunk-WEMRRIHH.js} +80 -2
- package/dist/chunk-YWQ3ALYY.js +69 -0
- package/dist/{chunk-5JGRFT2N.js → chunk-ZDGSUR6E.js} +879 -1898
- package/dist/{fast-client-CZ41v-oB.d.ts → fast-client--A88q0kk.d.ts} +85 -4
- package/dist/index.js +3 -3
- package/dist/level-build-CbRqrYJq.d.ts +140 -0
- package/dist/{module-DqtFb5wA.d.ts → module-CSpXrzFc.d.ts} +151 -400
- package/dist/{physics-predict-49udN3a1.d.ts → physics-predict-C7ZB1gDG.d.ts} +18 -3
- package/dist/protocol.d.ts +90 -2
- package/dist/protocol.js +17 -1
- package/dist/shot-world-D22tqOUT.d.ts +485 -0
- package/dist/world-fit/index.d.ts +549 -0
- package/dist/world-fit/index.js +4763 -0
- package/dist/world-fit/three.d.ts +77 -0
- package/dist/world-fit/three.js +140 -0
- package/package.json +15 -3
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/world-fit/level-build.ts
|
|
2
|
+
var LevelRefusalCode = {
|
|
3
|
+
/** Not a glb or a glTF, or not a file that can be read. */
|
|
4
|
+
NotGltf: "not_gltf",
|
|
5
|
+
/** A glb's chunks do not add up. */
|
|
6
|
+
BadContainer: "bad_container",
|
|
7
|
+
/** glTF 1 or a version this reader does not take. */
|
|
8
|
+
BadVersion: "bad_version",
|
|
9
|
+
/** An index that names nothing: a scene, a node, a mesh, an accessor, a buffer. */
|
|
10
|
+
BadReference: "bad_reference",
|
|
11
|
+
/** An accessor or a view that does not fit what it reads or is not what it must be. */
|
|
12
|
+
BadAccessor: "bad_accessor",
|
|
13
|
+
/** A triangle's index past its vertices. */
|
|
14
|
+
BadIndex: "bad_index",
|
|
15
|
+
/** A transform or a field of the wrong shape. */
|
|
16
|
+
BadValue: "bad_value",
|
|
17
|
+
/** NaN, an infinity, or a coordinate past the format's range. */
|
|
18
|
+
NotFinite: "not_finite",
|
|
19
|
+
/** A cycle, or a node with two parents. */
|
|
20
|
+
NodeCycle: "node_cycle",
|
|
21
|
+
/** Past a stated limit (LEVEL_LIMITS). */
|
|
22
|
+
TooLarge: "too_large",
|
|
23
|
+
/** Draco or meshopt on something that collides. */
|
|
24
|
+
Compressed: "compressed",
|
|
25
|
+
/** A required extension this reader cannot honour. */
|
|
26
|
+
UnknownExtension: "unknown_extension",
|
|
27
|
+
/** A buffer URI that leaves the input's folder, names the network, or is not a plain file. */
|
|
28
|
+
UriRefused: "uri_refused",
|
|
29
|
+
/** A skinned mesh that collides. */
|
|
30
|
+
Skinned: "skinned",
|
|
31
|
+
/** A mesh posed by morph targets that collides. */
|
|
32
|
+
Morphed: "morphed",
|
|
33
|
+
/** A kind of three.js object the helper cannot read. */
|
|
34
|
+
Unsupported: "unsupported",
|
|
35
|
+
/** A mesh with no triangle left of any area. */
|
|
36
|
+
Degenerate: "degenerate",
|
|
37
|
+
/** More triangles on one edge than any surface has. */
|
|
38
|
+
NonManifold: "non_manifold",
|
|
39
|
+
/** No spawn: a fit starts from where a hull can stand. */
|
|
40
|
+
NoSpawn: "no_spawn",
|
|
41
|
+
/** Nothing collides. */
|
|
42
|
+
NoSurface: "no_surface",
|
|
43
|
+
/** A mark that is not one. */
|
|
44
|
+
BadMark: "bad_mark"
|
|
45
|
+
};
|
|
46
|
+
var LevelRefusal = class extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(`level: ${message} (${code})`);
|
|
50
|
+
this.name = "LevelRefusal";
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var LEVEL_LIMITS = {
|
|
55
|
+
/** One glb, or one external buffer, and all buffers together. */
|
|
56
|
+
maxFileBytes: 2 ** 30,
|
|
57
|
+
/** Triangles in the level, instances counted. A fit reads a scene of this size; art past it needs a proxy. */
|
|
58
|
+
maxTriangles: 8e6,
|
|
59
|
+
/** Vertices one accessor may read. */
|
|
60
|
+
maxVertices: 24e6,
|
|
61
|
+
maxNodes: 1e6,
|
|
62
|
+
maxDepth: 1024,
|
|
63
|
+
/** Points a `hull` holds, as the scene format says. */
|
|
64
|
+
maxHullPoints: 4096,
|
|
65
|
+
/** No surface puts more triangles on one edge: past this it is junk, or an attack on what reads it. */
|
|
66
|
+
maxTrianglesOnEdge: 64
|
|
67
|
+
};
|
|
68
|
+
var LevelCollide = { Both: "both", Move: "move", Shot: "shot", None: "none" };
|
|
69
|
+
var LevelShape = { Mesh: "mesh", Box: "box", Hull: "hull" };
|
|
70
|
+
var NAME_MARKS = [
|
|
71
|
+
["nocol_", { collide: LevelCollide.None }],
|
|
72
|
+
["clip_", { collide: LevelCollide.Move }],
|
|
73
|
+
["shot_", { collide: LevelCollide.Shot }],
|
|
74
|
+
["box_", { shape: LevelShape.Box }],
|
|
75
|
+
["hull_", { shape: LevelShape.Hull }],
|
|
76
|
+
["movable_", { movable: true }],
|
|
77
|
+
["prop_", { prop: true }],
|
|
78
|
+
["spawn_", { spawn: true }]
|
|
79
|
+
];
|
|
80
|
+
var MARK_FIELDS = /* @__PURE__ */ new Set(["collide", "shape", "movable", "prop", "spawn", "tags"]);
|
|
81
|
+
var SETTING_FIELDS = /* @__PURE__ */ new Set(["unitsPerMeter", "hull"]);
|
|
82
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
83
|
+
function splitNameMarks(name) {
|
|
84
|
+
let rest = name;
|
|
85
|
+
let mark = {};
|
|
86
|
+
for (; ; ) {
|
|
87
|
+
const lower = rest.toLowerCase();
|
|
88
|
+
const found = NAME_MARKS.find(([prefix]) => lower.startsWith(prefix));
|
|
89
|
+
if (found === void 0) return { rest, mark };
|
|
90
|
+
rest = rest.slice(found[0].length);
|
|
91
|
+
mark = { ...mark, ...found[1] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function extrasMark(raw, where, allowSettings) {
|
|
95
|
+
if (raw === void 0) return {};
|
|
96
|
+
if (!isObject(raw)) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex is not an object`);
|
|
97
|
+
for (const key of Object.keys(raw)) {
|
|
98
|
+
if (MARK_FIELDS.has(key) || allowSettings && SETTING_FIELDS.has(key)) continue;
|
|
99
|
+
throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.${key} is not a mark (${[...MARK_FIELDS].join(", ")})`);
|
|
100
|
+
}
|
|
101
|
+
const out = {};
|
|
102
|
+
if (raw.collide !== void 0) {
|
|
103
|
+
if (!Object.values(LevelCollide).includes(raw.collide)) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.collide is not ${Object.values(LevelCollide).join(", ")}`);
|
|
104
|
+
out.collide = raw.collide;
|
|
105
|
+
}
|
|
106
|
+
if (raw.shape !== void 0) {
|
|
107
|
+
if (!Object.values(LevelShape).includes(raw.shape)) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.shape is not ${Object.values(LevelShape).join(", ")}`);
|
|
108
|
+
out.shape = raw.shape;
|
|
109
|
+
}
|
|
110
|
+
for (const key of ["movable", "prop", "spawn"]) {
|
|
111
|
+
if (raw[key] === void 0) continue;
|
|
112
|
+
if (typeof raw[key] !== "boolean") throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.${key} is not true or false`);
|
|
113
|
+
out[key] = raw[key];
|
|
114
|
+
}
|
|
115
|
+
if (raw.tags !== void 0) {
|
|
116
|
+
const tags = raw.tags;
|
|
117
|
+
const fine = Array.isArray(tags) && tags.length <= MAX_TAGS && tags.every((t) => typeof t === "string" && t.length > 0 && t.length <= MAX_NAME_CHARS);
|
|
118
|
+
if (!fine) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.tags is not a list of up to ${MAX_TAGS} names`);
|
|
119
|
+
out.tags = tags;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function levelMarkOf(name, genex, where = name, allowSettings = false) {
|
|
124
|
+
return { ...splitNameMarks(name).mark, ...extrasMark(genex, where, allowSettings) };
|
|
125
|
+
}
|
|
126
|
+
function inheritMark(parent, own) {
|
|
127
|
+
const { spawn: _notInherited, ...passed } = parent;
|
|
128
|
+
const tags = [.../* @__PURE__ */ new Set([...parent.tags ?? [], ...own.tags ?? []])];
|
|
129
|
+
return { ...passed, ...own, ...tags.length > 0 ? { tags } : {} };
|
|
130
|
+
}
|
|
131
|
+
function levelSettingsOf(genex, where) {
|
|
132
|
+
if (genex === void 0) return {};
|
|
133
|
+
if (!isObject(genex)) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex is not an object`);
|
|
134
|
+
const out = {};
|
|
135
|
+
const units = genex.unitsPerMeter;
|
|
136
|
+
if (units !== void 0) {
|
|
137
|
+
if (typeof units !== "number" || !Number.isFinite(units) || units <= 0) throw new LevelRefusal(LevelRefusalCode.BadMark, `${where}: extras.genex.unitsPerMeter is not above zero`);
|
|
138
|
+
out.unitsPerMeter = units;
|
|
139
|
+
}
|
|
140
|
+
if (genex.hull !== void 0) out.hull = hullOf(genex.hull, where);
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function hullOf(raw, where) {
|
|
144
|
+
const bad = (field) => new LevelRefusal(LevelRefusalCode.BadMark, `${where}: hull.${field} is not a usable size`);
|
|
145
|
+
if (!isObject(raw)) throw bad("");
|
|
146
|
+
const size = (field, positive) => {
|
|
147
|
+
const v = raw[field];
|
|
148
|
+
const fine = typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_COORD && (positive ? v > 0 : v >= 0);
|
|
149
|
+
if (!fine) throw bad(field);
|
|
150
|
+
return v;
|
|
151
|
+
};
|
|
152
|
+
const hull = { radius: size("radius", true), height: size("height", true), crouchHeight: size("crouchHeight", true), step: size("step", true), jump: size("jump", false), leap: size("leap", false) };
|
|
153
|
+
if (hull.crouchHeight > hull.height) throw bad("crouchHeight");
|
|
154
|
+
return hull;
|
|
155
|
+
}
|
|
156
|
+
function multiplyMatrix(a, b) {
|
|
157
|
+
const out = new Float64Array(16);
|
|
158
|
+
for (let row = 0; row < 4; row++) {
|
|
159
|
+
for (let col = 0; col < 4; col++) {
|
|
160
|
+
const k = col * 4;
|
|
161
|
+
out[k + row] = a[row] * b[k] + a[row + 4] * b[k + 1] + a[row + 8] * b[k + 2] + a[row + 12] * b[k + 3];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function composeMatrix(t, q, s) {
|
|
167
|
+
const [x, y, z, w] = q;
|
|
168
|
+
const x2 = x + x;
|
|
169
|
+
const y2 = y + y;
|
|
170
|
+
const z2 = z + z;
|
|
171
|
+
const xx = x * x2;
|
|
172
|
+
const xy = x * y2;
|
|
173
|
+
const xz = x * z2;
|
|
174
|
+
const yy = y * y2;
|
|
175
|
+
const yz = y * z2;
|
|
176
|
+
const zz = z * z2;
|
|
177
|
+
const wx = w * x2;
|
|
178
|
+
const wy = w * y2;
|
|
179
|
+
const wz = w * z2;
|
|
180
|
+
const [sx, sy, sz] = s;
|
|
181
|
+
return Float64Array.of(
|
|
182
|
+
(1 - (yy + zz)) * sx,
|
|
183
|
+
(xy + wz) * sx,
|
|
184
|
+
(xz - wy) * sx,
|
|
185
|
+
0,
|
|
186
|
+
(xy - wz) * sy,
|
|
187
|
+
(1 - (xx + zz)) * sy,
|
|
188
|
+
(yz + wx) * sy,
|
|
189
|
+
0,
|
|
190
|
+
(xz + wy) * sz,
|
|
191
|
+
(yz - wx) * sz,
|
|
192
|
+
(1 - (xx + yy)) * sz,
|
|
193
|
+
0,
|
|
194
|
+
t[0],
|
|
195
|
+
t[1],
|
|
196
|
+
t[2],
|
|
197
|
+
1
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
var IDENTITY_MATRIX = Float64Array.of(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
201
|
+
var SNAP = 256;
|
|
202
|
+
var MAX_COORD = 1e9;
|
|
203
|
+
var MAX_NAME_CHARS = 256;
|
|
204
|
+
var MAX_TAGS = 64;
|
|
205
|
+
var MIN_CROSS_SQUARED = 1e-12;
|
|
206
|
+
var SQUARE_TOLERANCE = 1e-6;
|
|
207
|
+
var ON_AXIS = 1 - 1e-9;
|
|
208
|
+
var SHOWN_NAMES = 8;
|
|
209
|
+
var CUBE_CORNERS = 8;
|
|
210
|
+
var CUBE_TRIANGLES = 12;
|
|
211
|
+
var CUBE_FACES = 6;
|
|
212
|
+
var FACE_COVER_TOLERANCE = 1e-6;
|
|
213
|
+
var snap = (v) => Math.fround(Math.round(v * SNAP) / SNAP) + 0;
|
|
214
|
+
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
215
|
+
function base64(bytes) {
|
|
216
|
+
const out = new Uint8Array(Math.ceil(bytes.length / 3) * 4);
|
|
217
|
+
const alphabet = new TextEncoder().encode(BASE64);
|
|
218
|
+
let o = 0;
|
|
219
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
220
|
+
const a = bytes[i];
|
|
221
|
+
const b = bytes[i + 1] ?? 0;
|
|
222
|
+
const c = bytes[i + 2] ?? 0;
|
|
223
|
+
out[o++] = alphabet[a >> 2];
|
|
224
|
+
out[o++] = alphabet[(a & 3) << 4 | b >> 4];
|
|
225
|
+
out[o++] = i + 1 < bytes.length ? alphabet[(b & 15) << 2 | c >> 6] : 61;
|
|
226
|
+
out[o++] = i + 2 < bytes.length ? alphabet[c & 63] : 61;
|
|
227
|
+
}
|
|
228
|
+
return new TextDecoder().decode(out);
|
|
229
|
+
}
|
|
230
|
+
function f32le(values) {
|
|
231
|
+
const bytes = new Uint8Array(values.length * 4);
|
|
232
|
+
const view = new DataView(bytes.buffer);
|
|
233
|
+
for (let i = 0; i < values.length; i++) view.setFloat32(i * 4, values[i], true);
|
|
234
|
+
return { f32le: base64(bytes) };
|
|
235
|
+
}
|
|
236
|
+
function u32le(values) {
|
|
237
|
+
const bytes = new Uint8Array(values.length * 4);
|
|
238
|
+
const view = new DataView(bytes.buffer);
|
|
239
|
+
for (let i = 0; i < values.length; i++) view.setUint32(i * 4, values[i], true);
|
|
240
|
+
return { u32le: base64(bytes) };
|
|
241
|
+
}
|
|
242
|
+
var Welder = class {
|
|
243
|
+
positions = [];
|
|
244
|
+
slots;
|
|
245
|
+
mask;
|
|
246
|
+
constructor(expected) {
|
|
247
|
+
let size = 16;
|
|
248
|
+
while (size < expected * 2) size *= 2;
|
|
249
|
+
this.slots = new Int32Array(size).fill(-1);
|
|
250
|
+
this.mask = size - 1;
|
|
251
|
+
}
|
|
252
|
+
add(x, y, z) {
|
|
253
|
+
const h = (Math.imul(Math.round(x * SNAP) | 0, 73856093) ^ Math.imul(Math.round(y * SNAP) | 0, 19349663) ^ Math.imul(Math.round(z * SNAP) | 0, 83492791)) >>> 0;
|
|
254
|
+
let slot = h & this.mask;
|
|
255
|
+
for (; ; ) {
|
|
256
|
+
const at = this.slots[slot];
|
|
257
|
+
if (at < 0) break;
|
|
258
|
+
const p = this.positions;
|
|
259
|
+
if (p[at * 3] === x && p[at * 3 + 1] === y && p[at * 3 + 2] === z) return at;
|
|
260
|
+
slot = slot + 1 & this.mask;
|
|
261
|
+
}
|
|
262
|
+
const index = this.positions.length / 3;
|
|
263
|
+
this.slots[slot] = index;
|
|
264
|
+
this.positions.push(x, y, z);
|
|
265
|
+
return index;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function checkPrimitive(p, name) {
|
|
269
|
+
const { positions, indices } = p;
|
|
270
|
+
for (let i = 0; i < positions.length; i++) {
|
|
271
|
+
const v = positions[i];
|
|
272
|
+
if (!Number.isFinite(v) || Math.abs(v) > MAX_COORD) throw new LevelRefusal(LevelRefusalCode.NotFinite, `${name}: vertex ${Math.floor(i / 3)} holds ${v}, which is no coordinate`);
|
|
273
|
+
}
|
|
274
|
+
const count = positions.length / 3;
|
|
275
|
+
for (let i = 0; i < indices.length; i++) {
|
|
276
|
+
if (indices[i] >= count) throw new LevelRefusal(LevelRefusalCode.BadIndex, `${name}: index ${indices[i]} is past the ${count} vertices`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function boxOf(positions, triangles, vertices) {
|
|
280
|
+
if (vertices.length !== CUBE_CORNERS || triangles.length !== CUBE_TRIANGLES * 3) return null;
|
|
281
|
+
const min = [Infinity, Infinity, Infinity];
|
|
282
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
283
|
+
for (const v of vertices) {
|
|
284
|
+
for (let a = 0; a < 3; a++) {
|
|
285
|
+
const value = positions[v * 3 + a];
|
|
286
|
+
if (value < min[a]) min[a] = value;
|
|
287
|
+
if (value > max[a]) max[a] = value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const size = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
291
|
+
if (size.some((s) => s <= 0)) return null;
|
|
292
|
+
const corners = /* @__PURE__ */ new Set();
|
|
293
|
+
for (const v of vertices) {
|
|
294
|
+
let corner = 0;
|
|
295
|
+
for (let a = 0; a < 3; a++) {
|
|
296
|
+
const value = positions[v * 3 + a];
|
|
297
|
+
if (value === max[a]) corner |= 1 << a;
|
|
298
|
+
else if (value !== min[a]) return null;
|
|
299
|
+
}
|
|
300
|
+
corners.add(corner);
|
|
301
|
+
}
|
|
302
|
+
if (corners.size !== CUBE_CORNERS) return null;
|
|
303
|
+
const covered = new Float64Array(CUBE_FACES);
|
|
304
|
+
for (let t = 0; t < triangles.length; t += 3) {
|
|
305
|
+
const face = faceOf(positions, triangles, t, min, max);
|
|
306
|
+
if (face < 0) return null;
|
|
307
|
+
covered[face] = covered[face] + triangleArea(positions, triangles, t);
|
|
308
|
+
}
|
|
309
|
+
for (let a = 0; a < 3; a++) {
|
|
310
|
+
const want = size[(a + 1) % 3] * size[(a + 2) % 3];
|
|
311
|
+
for (const face of [a * 2, a * 2 + 1]) {
|
|
312
|
+
if (Math.abs(covered[face] - want) > want * FACE_COVER_TOLERANCE) return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { min, max };
|
|
316
|
+
}
|
|
317
|
+
function faceOf(positions, tri, t, min, max) {
|
|
318
|
+
for (let a = 0; a < 3; a++) {
|
|
319
|
+
const p0 = positions[tri[t] * 3 + a];
|
|
320
|
+
const p1 = positions[tri[t + 1] * 3 + a];
|
|
321
|
+
const p2 = positions[tri[t + 2] * 3 + a];
|
|
322
|
+
if (p0 === p1 && p1 === p2) {
|
|
323
|
+
if (p0 === min[a]) return a * 2;
|
|
324
|
+
if (p0 === max[a]) return a * 2 + 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return -1;
|
|
328
|
+
}
|
|
329
|
+
function triangleArea(p, tri, t) {
|
|
330
|
+
const a = tri[t] * 3;
|
|
331
|
+
const b = tri[t + 1] * 3;
|
|
332
|
+
const c = tri[t + 2] * 3;
|
|
333
|
+
const ux = p[b] - p[a];
|
|
334
|
+
const uy = p[b + 1] - p[a + 1];
|
|
335
|
+
const uz = p[b + 2] - p[a + 2];
|
|
336
|
+
const vx = p[c] - p[a];
|
|
337
|
+
const vy = p[c + 1] - p[a + 1];
|
|
338
|
+
const vz = p[c + 2] - p[a + 2];
|
|
339
|
+
const cx = uy * vz - uz * vy;
|
|
340
|
+
const cy = uz * vx - ux * vz;
|
|
341
|
+
const cz = ux * vy - uy * vx;
|
|
342
|
+
return Math.sqrt(cx * cx + cy * cy + cz * cz) / 2;
|
|
343
|
+
}
|
|
344
|
+
function analyse(p, findBoxes) {
|
|
345
|
+
if (!findBoxes) return { boxes: [], rest: p.indices };
|
|
346
|
+
const welder = new Welder(p.positions.length / 3);
|
|
347
|
+
const welded = new Int32Array(p.positions.length / 3);
|
|
348
|
+
for (let i = 0; i < welded.length; i++) welded[i] = welder.add(p.positions[i * 3], p.positions[i * 3 + 1], p.positions[i * 3 + 2]);
|
|
349
|
+
const parent = Int32Array.from({ length: welder.positions.length / 3 }, (_, i) => i);
|
|
350
|
+
const find = (x) => {
|
|
351
|
+
let r = x;
|
|
352
|
+
while (parent[r] !== r) {
|
|
353
|
+
parent[r] = parent[parent[r]];
|
|
354
|
+
r = parent[r];
|
|
355
|
+
}
|
|
356
|
+
return r;
|
|
357
|
+
};
|
|
358
|
+
const tri = p.indices;
|
|
359
|
+
for (let t = 0; t < tri.length; t += 3) {
|
|
360
|
+
const a = find(welded[tri[t]]);
|
|
361
|
+
parent[find(welded[tri[t + 1]])] = a;
|
|
362
|
+
parent[find(welded[tri[t + 2]])] = a;
|
|
363
|
+
}
|
|
364
|
+
const pieces = /* @__PURE__ */ new Map();
|
|
365
|
+
for (let t = 0; t < tri.length; t += 3) {
|
|
366
|
+
const root = find(welded[tri[t]]);
|
|
367
|
+
let list = pieces.get(root);
|
|
368
|
+
if (list === void 0) {
|
|
369
|
+
list = [];
|
|
370
|
+
pieces.set(root, list);
|
|
371
|
+
}
|
|
372
|
+
list.push(tri[t], tri[t + 1], tri[t + 2]);
|
|
373
|
+
}
|
|
374
|
+
const weldedPositions = Float64Array.from(welder.positions);
|
|
375
|
+
const boxes = [];
|
|
376
|
+
const rest = [];
|
|
377
|
+
for (const list of pieces.values()) {
|
|
378
|
+
const found = list.length === CUBE_TRIANGLES * 3 ? pieceBox(list, welded, weldedPositions) : null;
|
|
379
|
+
if (found === null) {
|
|
380
|
+
for (const v of list) rest.push(v);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
boxes.push(found);
|
|
384
|
+
}
|
|
385
|
+
return { boxes, rest: Uint32Array.from(rest) };
|
|
386
|
+
}
|
|
387
|
+
function pieceBox(list, welded, positions) {
|
|
388
|
+
const triangles = new Uint32Array(list.length);
|
|
389
|
+
const seen = /* @__PURE__ */ new Set();
|
|
390
|
+
for (let i = 0; i < list.length; i++) {
|
|
391
|
+
const v = welded[list[i]];
|
|
392
|
+
triangles[i] = v;
|
|
393
|
+
seen.add(v);
|
|
394
|
+
}
|
|
395
|
+
const bounds = boxOf(positions, triangles, [...seen]);
|
|
396
|
+
return bounds === null ? null : { ...bounds, triangles: Uint32Array.from(list) };
|
|
397
|
+
}
|
|
398
|
+
function transformPoint(m, x, y, z, scale, out) {
|
|
399
|
+
const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
400
|
+
const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
401
|
+
const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
402
|
+
out[0] = wx * scale;
|
|
403
|
+
out[1] = -wz * scale;
|
|
404
|
+
out[2] = wy * scale;
|
|
405
|
+
}
|
|
406
|
+
var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
407
|
+
function placeBox(box, m, scale) {
|
|
408
|
+
const point = new Float64Array(3);
|
|
409
|
+
const halfLocal = [0, 1, 2].map((a) => (box.max[a] - box.min[a]) / 2);
|
|
410
|
+
const mid = [0, 1, 2].map((a) => (box.max[a] + box.min[a]) / 2);
|
|
411
|
+
transformPoint(m, mid[0], mid[1], mid[2], scale, point);
|
|
412
|
+
const center = [snap(point[0]), snap(point[1]), snap(point[2])];
|
|
413
|
+
const sides = [0, 1, 2].map((a) => {
|
|
414
|
+
const lx = m[a * 4] * halfLocal[a] * scale;
|
|
415
|
+
const ly = m[a * 4 + 1] * halfLocal[a] * scale;
|
|
416
|
+
const lz = m[a * 4 + 2] * halfLocal[a] * scale;
|
|
417
|
+
return [lx, -lz, ly];
|
|
418
|
+
});
|
|
419
|
+
const lengths = sides.map((s) => Math.sqrt(dot(s, s)));
|
|
420
|
+
if (lengths.some((l) => l === 0)) return null;
|
|
421
|
+
for (const [a, b] of [[0, 1], [1, 2], [0, 2]]) {
|
|
422
|
+
if (Math.abs(dot(sides[a], sides[b])) > SQUARE_TOLERANCE * lengths[a] * lengths[b]) return cornersHull(box, m, scale);
|
|
423
|
+
}
|
|
424
|
+
const axes = sides.map((s, i) => s.map((v) => v / lengths[i]));
|
|
425
|
+
const onWorld = axes.map((u) => u.findIndex((v) => Math.abs(v) >= ON_AXIS));
|
|
426
|
+
if (new Set(onWorld).size === 3 && !onWorld.includes(-1)) {
|
|
427
|
+
const half = [0, 0, 0];
|
|
428
|
+
onWorld.forEach((w, i) => {
|
|
429
|
+
half[w] = snap(lengths[i]);
|
|
430
|
+
});
|
|
431
|
+
return { kind: "box", center, half };
|
|
432
|
+
}
|
|
433
|
+
const cross = [axes[0][1] * axes[1][2] - axes[0][2] * axes[1][1], axes[0][2] * axes[1][0] - axes[0][0] * axes[1][2], axes[0][0] * axes[1][1] - axes[0][1] * axes[1][0]];
|
|
434
|
+
const third = dot(cross, axes[2]) < 0 ? axes[2].map((v) => -v) : axes[2];
|
|
435
|
+
return { kind: "box", center, axes: [axes[0], axes[1], third].map((u) => u.map((v) => Math.fround(v) + 0)), half: lengths.map(snap) };
|
|
436
|
+
}
|
|
437
|
+
function cornersHull(box, m, scale) {
|
|
438
|
+
const point = new Float64Array(3);
|
|
439
|
+
const points = [];
|
|
440
|
+
for (let corner = 0; corner < CUBE_CORNERS; corner++) {
|
|
441
|
+
const x = corner & 1 ? box.max[0] : box.min[0];
|
|
442
|
+
const y = corner & 2 ? box.max[1] : box.min[1];
|
|
443
|
+
const z = corner & 4 ? box.max[2] : box.min[2];
|
|
444
|
+
transformPoint(m, x, y, z, scale, point);
|
|
445
|
+
points.push([snap(point[0]), snap(point[1]), snap(point[2])]);
|
|
446
|
+
}
|
|
447
|
+
return { kind: "hull", points };
|
|
448
|
+
}
|
|
449
|
+
function boundsOf(p) {
|
|
450
|
+
if (p.indices.length === 0) return null;
|
|
451
|
+
const min = [Infinity, Infinity, Infinity];
|
|
452
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
453
|
+
for (let i = 0; i < p.indices.length; i++) {
|
|
454
|
+
const v = p.indices[i] * 3;
|
|
455
|
+
for (let a = 0; a < 3; a++) {
|
|
456
|
+
const value = p.positions[v + a];
|
|
457
|
+
if (value < min[a]) min[a] = value;
|
|
458
|
+
if (value > max[a]) max[a] = value;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { min, max };
|
|
462
|
+
}
|
|
463
|
+
function edgeLoad(indices) {
|
|
464
|
+
const edges = new Float64Array(indices.length);
|
|
465
|
+
const vertices = 2 ** 26;
|
|
466
|
+
for (let t = 0, e = 0; t < indices.length; t += 3) {
|
|
467
|
+
for (let k = 0; k < 3; k++) {
|
|
468
|
+
const a = indices[t + k];
|
|
469
|
+
const b = indices[t + (k + 1) % 3];
|
|
470
|
+
edges[e++] = a < b ? a * vertices + b : b * vertices + a;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
edges.sort();
|
|
474
|
+
let overTwo = 0;
|
|
475
|
+
let most = 0;
|
|
476
|
+
let run = 0;
|
|
477
|
+
for (let i = 0; i < edges.length; i++) {
|
|
478
|
+
run = i > 0 && edges[i] === edges[i - 1] ? run + 1 : 1;
|
|
479
|
+
if (run > most) most = run;
|
|
480
|
+
if (run === 3) overTwo++;
|
|
481
|
+
}
|
|
482
|
+
return { overTwo, most };
|
|
483
|
+
}
|
|
484
|
+
function placeTriangles(p, triangles, m, scale, name) {
|
|
485
|
+
const world = new Float64Array(p.positions.length);
|
|
486
|
+
const point = new Float64Array(3);
|
|
487
|
+
const done = new Uint8Array(p.positions.length / 3);
|
|
488
|
+
const welder = new Welder(Math.min(triangles.length, p.positions.length / 3));
|
|
489
|
+
const out = [];
|
|
490
|
+
let dropped = 0;
|
|
491
|
+
const place = (v) => {
|
|
492
|
+
if (done[v] === 1) return;
|
|
493
|
+
done[v] = 1;
|
|
494
|
+
transformPoint(m, p.positions[v * 3], p.positions[v * 3 + 1], p.positions[v * 3 + 2], scale, point);
|
|
495
|
+
for (let a = 0; a < 3; a++) {
|
|
496
|
+
const value = point[a];
|
|
497
|
+
if (!Number.isFinite(value) || Math.abs(value) > MAX_COORD) throw new LevelRefusal(LevelRefusalCode.NotFinite, `${name}: a vertex lands at ${value}, past the scene's range`);
|
|
498
|
+
world[v * 3 + a] = snap(value);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
for (let t = 0; t < triangles.length; t += 3) {
|
|
502
|
+
const a = triangles[t];
|
|
503
|
+
const b = triangles[t + 1];
|
|
504
|
+
const c = triangles[t + 2];
|
|
505
|
+
place(a);
|
|
506
|
+
place(b);
|
|
507
|
+
place(c);
|
|
508
|
+
const ux = world[b * 3] - world[a * 3];
|
|
509
|
+
const uy = world[b * 3 + 1] - world[a * 3 + 1];
|
|
510
|
+
const uz = world[b * 3 + 2] - world[a * 3 + 2];
|
|
511
|
+
const vx = world[c * 3] - world[a * 3];
|
|
512
|
+
const vy = world[c * 3 + 1] - world[a * 3 + 1];
|
|
513
|
+
const vz = world[c * 3 + 2] - world[a * 3 + 2];
|
|
514
|
+
const cx = uy * vz - uz * vy;
|
|
515
|
+
const cy = uz * vx - ux * vz;
|
|
516
|
+
const cz = ux * vy - uy * vx;
|
|
517
|
+
if (cx * cx + cy * cy + cz * cz <= MIN_CROSS_SQUARED) {
|
|
518
|
+
dropped++;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
out.push(welder.add(world[a * 3], world[a * 3 + 1], world[a * 3 + 2]), welder.add(world[b * 3], world[b * 3 + 1], world[b * 3 + 2]), welder.add(world[c * 3], world[c * 3 + 1], world[c * 3 + 2]));
|
|
522
|
+
}
|
|
523
|
+
return { vertices: welder.positions, indices: out, dropped };
|
|
524
|
+
}
|
|
525
|
+
function channelsOf(collide) {
|
|
526
|
+
if (collide === LevelCollide.Move) return { channels: ["move"] };
|
|
527
|
+
if (collide === LevelCollide.Shot) return { channels: ["shot"] };
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
function actorTags(mark) {
|
|
531
|
+
const tags = new Set(mark.tags ?? []);
|
|
532
|
+
if (mark.movable === true) tags.add("movable");
|
|
533
|
+
if (mark.prop === true) tags.add("prop");
|
|
534
|
+
return [...tags];
|
|
535
|
+
}
|
|
536
|
+
function actorNames(meshes) {
|
|
537
|
+
const counts = /* @__PURE__ */ new Map();
|
|
538
|
+
for (const m of meshes) counts.set(m.name, (counts.get(m.name) ?? 0) + 1);
|
|
539
|
+
const used = /* @__PURE__ */ new Map();
|
|
540
|
+
return meshes.map((m) => {
|
|
541
|
+
const wanted = counts.get(m.name) > 1 ? m.path : m.name;
|
|
542
|
+
const seen = used.get(wanted) ?? 0;
|
|
543
|
+
used.set(wanted, seen + 1);
|
|
544
|
+
const name = seen === 0 ? wanted : `${wanted}#${seen + 1}`;
|
|
545
|
+
return name.length <= MAX_NAME_CHARS ? name : `\u2026${name.slice(name.length - MAX_NAME_CHARS + 1)}`;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function surfacesOf(node, name, scale, analysed, notes, tally) {
|
|
549
|
+
const surfaces = [];
|
|
550
|
+
const channels = channelsOf(node.mark.collide);
|
|
551
|
+
const shape = node.mark.shape;
|
|
552
|
+
let dropped = 0;
|
|
553
|
+
let overTwo = 0;
|
|
554
|
+
for (const p of node.primitives) checkPrimitive(p, name);
|
|
555
|
+
for (const matrix of node.instances) {
|
|
556
|
+
for (const p of node.primitives) {
|
|
557
|
+
if (shape === LevelShape.Hull || shape === LevelShape.Box) {
|
|
558
|
+
const placed = shape === LevelShape.Box ? markedBox(p, matrix, scale) : markedHull(p, matrix, scale, name);
|
|
559
|
+
if (placed === null) continue;
|
|
560
|
+
if (placed.kind === "box") tally.boxes++;
|
|
561
|
+
else tally.hulls++;
|
|
562
|
+
surfaces.push({ ...placed, ...channels });
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
let found = analysed.get(p);
|
|
566
|
+
if (found === void 0) {
|
|
567
|
+
found = analyse(p, shape !== LevelShape.Mesh);
|
|
568
|
+
analysed.set(p, found);
|
|
569
|
+
}
|
|
570
|
+
const flattened = [];
|
|
571
|
+
for (const box of found.boxes) {
|
|
572
|
+
const placed = placeBox(box, matrix, scale);
|
|
573
|
+
if (placed === null) {
|
|
574
|
+
flattened.push(box.triangles);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (placed.kind === "box") tally.boxes++;
|
|
578
|
+
else tally.hulls++;
|
|
579
|
+
surfaces.push({ ...placed, ...channels });
|
|
580
|
+
}
|
|
581
|
+
for (const triangles of [...flattened, found.rest]) {
|
|
582
|
+
if (triangles.length === 0) continue;
|
|
583
|
+
const placed = placeTriangles(p, triangles, matrix, scale, name);
|
|
584
|
+
dropped += placed.dropped;
|
|
585
|
+
if (placed.indices.length === 0) continue;
|
|
586
|
+
const load = edgeLoad(placed.indices);
|
|
587
|
+
if (load.most > LEVEL_LIMITS.maxTrianglesOnEdge) throw new LevelRefusal(LevelRefusalCode.NonManifold, `${name}: one edge holds ${load.most} triangles, past the ${LEVEL_LIMITS.maxTrianglesOnEdge} a surface can (repeated or fanned faces); clean the mesh or give it a collision proxy`);
|
|
588
|
+
overTwo += load.overTwo;
|
|
589
|
+
tally.meshes++;
|
|
590
|
+
tally.triangles += placed.indices.length / 3;
|
|
591
|
+
surfaces.push({ kind: "mesh", vertices: f32le(placed.vertices), indices: u32le(placed.indices), ...channels });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (dropped > 0) notes.push(`${name}: dropped ${dropped} zero-area triangles`);
|
|
596
|
+
if (overTwo > 0) notes.push(`${name}: kept ${overTwo} non-manifold edges (three or more triangles on one edge)`);
|
|
597
|
+
return surfaces;
|
|
598
|
+
}
|
|
599
|
+
function markedBox(p, matrix, scale) {
|
|
600
|
+
const bounds = boundsOf(p);
|
|
601
|
+
return bounds === null ? null : placeBox(bounds, matrix, scale);
|
|
602
|
+
}
|
|
603
|
+
function markedHull(p, matrix, scale, name) {
|
|
604
|
+
if (p.indices.length === 0) return null;
|
|
605
|
+
const point = new Float64Array(3);
|
|
606
|
+
const welder = new Welder(Math.min(p.positions.length / 3, LEVEL_LIMITS.maxHullPoints * 2));
|
|
607
|
+
const used = new Set(p.indices);
|
|
608
|
+
for (const v of used) {
|
|
609
|
+
transformPoint(matrix, p.positions[v * 3], p.positions[v * 3 + 1], p.positions[v * 3 + 2], scale, point);
|
|
610
|
+
welder.add(snap(point[0]), snap(point[1]), snap(point[2]));
|
|
611
|
+
if (welder.positions.length / 3 > LEVEL_LIMITS.maxHullPoints) throw new LevelRefusal(LevelRefusalCode.TooLarge, `${name}: a hull holds up to ${LEVEL_LIMITS.maxHullPoints} points and this mesh has more; simplify it or leave the hull mark off`);
|
|
612
|
+
}
|
|
613
|
+
const points = [];
|
|
614
|
+
for (let i = 0; i < welder.positions.length; i += 3) points.push([welder.positions[i], welder.positions[i + 1], welder.positions[i + 2]]);
|
|
615
|
+
return { kind: "hull", points };
|
|
616
|
+
}
|
|
617
|
+
function buildLevelScene(input, options) {
|
|
618
|
+
const notes = [...input.notes];
|
|
619
|
+
const tally = { boxes: 0, hulls: 0, meshes: 0, triangles: 0 };
|
|
620
|
+
const names = actorNames(input.meshes);
|
|
621
|
+
const analysed = /* @__PURE__ */ new Map();
|
|
622
|
+
const actors = [];
|
|
623
|
+
input.meshes.forEach((node, i) => {
|
|
624
|
+
const name = names[i];
|
|
625
|
+
const surfaces = surfacesOf(node, name, options.scale, analysed, notes, tally);
|
|
626
|
+
if (surfaces.length === 0) throw new LevelRefusal(LevelRefusalCode.Degenerate, `${name}: no triangle has any area (a scale of zero, or every triangle repeats a point); fix the mesh or mark it nocol_`);
|
|
627
|
+
const tags = actorTags(node.mark);
|
|
628
|
+
actors.push({ name, ...tags.length > 0 ? { tags } : {}, surfaces });
|
|
629
|
+
});
|
|
630
|
+
if (actors.length === 0) throw new LevelRefusal(LevelRefusalCode.NoSurface, "nothing collides: the level holds no mesh, or every mesh is marked nocol_");
|
|
631
|
+
const point = new Float64Array(3);
|
|
632
|
+
const spawns = input.spawns.map((s) => {
|
|
633
|
+
transformPoint(IDENTITY_MATRIX, s.at[0], s.at[1], s.at[2], options.scale, point);
|
|
634
|
+
const at = [snap(point[0]), snap(point[1]), snap(point[2])];
|
|
635
|
+
if (at.some((v) => Math.abs(v) > MAX_COORD)) throw new LevelRefusal(LevelRefusalCode.NotFinite, `spawn ${s.name}: past the scene's range`);
|
|
636
|
+
return { at, ...s.name === "" ? {} : { name: s.name.slice(0, MAX_NAME_CHARS) }, ...s.tags.length > 0 ? { tags: s.tags } : {} };
|
|
637
|
+
});
|
|
638
|
+
for (const s of options.spawns ?? []) spawns.push({ at: [s.at[0], s.at[1], s.at[2]], ...s.name === void 0 ? {} : { name: s.name }, ...s.tags === void 0 ? {} : { tags: s.tags } });
|
|
639
|
+
if (spawns.length === 0) throw new LevelRefusal(LevelRefusalCode.NoSpawn, "no spawn: name an empty node spawn_<name> (or set extras.genex.spawn) where a player starts; the fit works out what a hull can reach from there");
|
|
640
|
+
const scene = {
|
|
641
|
+
format: "genex-world-scene",
|
|
642
|
+
version: 1,
|
|
643
|
+
unitsPerMeter: options.unitsPerMeter,
|
|
644
|
+
source: options.source,
|
|
645
|
+
...options.hull === void 0 ? {} : { hull: options.hull },
|
|
646
|
+
actors,
|
|
647
|
+
spawns
|
|
648
|
+
};
|
|
649
|
+
return { scene, notes, stats: { actors: actors.length, ...tally, spawns: spawns.length }, json: `${JSON.stringify(scene)}
|
|
650
|
+
` };
|
|
651
|
+
}
|
|
652
|
+
function namesInRefusal(names) {
|
|
653
|
+
const shown = names.slice(0, SHOWN_NAMES).join(", ");
|
|
654
|
+
return names.length > SHOWN_NAMES ? `${shown} and ${names.length - SHOWN_NAMES} more` : shown;
|
|
655
|
+
}
|
|
656
|
+
function spawnNameOf(name) {
|
|
657
|
+
return splitNameMarks(name).rest;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export {
|
|
661
|
+
LevelRefusalCode,
|
|
662
|
+
LevelRefusal,
|
|
663
|
+
LEVEL_LIMITS,
|
|
664
|
+
LevelCollide,
|
|
665
|
+
LevelShape,
|
|
666
|
+
levelMarkOf,
|
|
667
|
+
inheritMark,
|
|
668
|
+
levelSettingsOf,
|
|
669
|
+
multiplyMatrix,
|
|
670
|
+
composeMatrix,
|
|
671
|
+
IDENTITY_MATRIX,
|
|
672
|
+
buildLevelScene,
|
|
673
|
+
namesInRefusal,
|
|
674
|
+
spawnNameOf
|
|
675
|
+
};
|