@genex-ai/multiplayer 0.17.6 → 0.17.8
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 +101 -30
- 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-ZSWF4VCN.js → chunk-CSQEEANB.js} +78 -2
- package/dist/{chunk-UX3F6YTV.js → chunk-FPWP22Y7.js} +1 -1
- package/dist/{chunk-5JGRFT2N.js → chunk-NFMGIKFZ.js} +879 -1898
- package/dist/{chunk-K3SF5VDS.js → chunk-OD6NRSX2.js} +3 -1
- package/dist/{chunk-7QVOIHJF.js → chunk-QY3QQNDJ.js} +2 -2
- package/dist/{chunk-Y6XHXDIK.js → chunk-QZOCUQJT.js} +820 -537
- package/dist/chunk-TSYEWPVO.js +44 -0
- package/dist/chunk-TVTVIMIH.js +675 -0
- package/dist/chunk-YWQ3ALYY.js +69 -0
- 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 +87 -2
- package/dist/protocol.js +15 -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,4763 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IDENTITY_MATRIX,
|
|
3
|
+
LEVEL_LIMITS,
|
|
4
|
+
LevelCollide,
|
|
5
|
+
LevelRefusal,
|
|
6
|
+
LevelRefusalCode,
|
|
7
|
+
LevelShape,
|
|
8
|
+
buildLevelScene,
|
|
9
|
+
composeMatrix,
|
|
10
|
+
inheritMark,
|
|
11
|
+
levelMarkOf,
|
|
12
|
+
levelSettingsOf,
|
|
13
|
+
multiplyMatrix,
|
|
14
|
+
namesInRefusal,
|
|
15
|
+
spawnNameOf
|
|
16
|
+
} from "../chunk-TVTVIMIH.js";
|
|
17
|
+
import {
|
|
18
|
+
ClaimVerdict,
|
|
19
|
+
ShotWorldCost
|
|
20
|
+
} from "../chunk-TSYEWPVO.js";
|
|
21
|
+
import {
|
|
22
|
+
WorldLimits
|
|
23
|
+
} from "../chunk-2NGXXQDS.js";
|
|
24
|
+
|
|
25
|
+
// src/world-fit/lattice.ts
|
|
26
|
+
function makeLattice(h, min, max) {
|
|
27
|
+
const x0 = Math.floor(min[0] / h) * h;
|
|
28
|
+
const y0 = Math.floor(min[1] / h) * h;
|
|
29
|
+
const z0 = Math.floor(min[2] / h) * h;
|
|
30
|
+
const nx = Math.max(1, Math.ceil((max[0] - x0) / h));
|
|
31
|
+
const ny = Math.max(1, Math.ceil((max[1] - y0) / h));
|
|
32
|
+
const nz = Math.max(1, Math.ceil((max[2] - z0) / h));
|
|
33
|
+
return { h, x0, y0, z0, nx, ny, nz, w: Math.ceil(nz / 32) };
|
|
34
|
+
}
|
|
35
|
+
var CELL_EPSILON = 1e-4;
|
|
36
|
+
var cellFloor = (v) => Math.floor(v + CELL_EPSILON);
|
|
37
|
+
var cellCeil = (v) => Math.ceil(v - CELL_EPSILON);
|
|
38
|
+
var newLayer = (l) => new Uint32Array(l.nx * l.ny * l.w);
|
|
39
|
+
var cellCount = (l) => l.nx * l.ny * l.nz;
|
|
40
|
+
var centreX = (l, i) => l.x0 + (i + 0.5) * l.h;
|
|
41
|
+
var centreY = (l, j) => l.y0 + (j + 0.5) * l.h;
|
|
42
|
+
var plane = (l, k) => l.z0 + k * l.h;
|
|
43
|
+
function testBit(layer, offset, k) {
|
|
44
|
+
return (layer[offset + (k >>> 5)] >>> (k & 31) & 1) === 1;
|
|
45
|
+
}
|
|
46
|
+
function setBit(layer, offset, k) {
|
|
47
|
+
const at = offset + (k >>> 5);
|
|
48
|
+
layer[at] = layer[at] | 1 << (k & 31);
|
|
49
|
+
}
|
|
50
|
+
function wordMask(first, a, b) {
|
|
51
|
+
const lo = Math.max(a - first, 0);
|
|
52
|
+
const hi = Math.min(b - first, 31);
|
|
53
|
+
if (lo > hi) return 0;
|
|
54
|
+
const top = hi === 31 ? 4294967295 : (1 << hi + 1) - 1 >>> 0;
|
|
55
|
+
return (top & ~((1 << lo) - 1)) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
function setRun(l, layer, offset, a, b) {
|
|
58
|
+
const lo = Math.max(a, 0);
|
|
59
|
+
const hi = Math.min(b, l.nz - 1);
|
|
60
|
+
if (lo > hi) return;
|
|
61
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) {
|
|
62
|
+
const at = offset + word;
|
|
63
|
+
layer[at] = (layer[at] | wordMask(word * 32, lo, hi)) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function clearRun(l, layer, offset, a, b) {
|
|
67
|
+
const lo = Math.max(a, 0);
|
|
68
|
+
const hi = Math.min(b, l.nz - 1);
|
|
69
|
+
if (lo > hi) return;
|
|
70
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) {
|
|
71
|
+
const at = offset + word;
|
|
72
|
+
layer[at] = (layer[at] & ~wordMask(word * 32, lo, hi)) >>> 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function anyInRun(l, layer, offset, a, b) {
|
|
76
|
+
const lo = Math.max(a, 0);
|
|
77
|
+
const hi = Math.min(b, l.nz - 1);
|
|
78
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) {
|
|
79
|
+
if ((layer[offset + word] & wordMask(word * 32, lo, hi)) !== 0) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function popcount(v) {
|
|
84
|
+
let n = v - (v >>> 1 & 1431655765);
|
|
85
|
+
n = (n & 858993459) + (n >>> 2 & 858993459);
|
|
86
|
+
return (n + (n >>> 4) & 252645135) * 16843009 >>> 24;
|
|
87
|
+
}
|
|
88
|
+
function countInRun(l, layer, offset, a, b) {
|
|
89
|
+
const lo = Math.max(a, 0);
|
|
90
|
+
const hi = Math.min(b, l.nz - 1);
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) count += popcount((layer[offset + word] & wordMask(word * 32, lo, hi)) >>> 0);
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
95
|
+
function firstSet(l, layer, offset, a, b) {
|
|
96
|
+
const lo = Math.max(a, 0);
|
|
97
|
+
const hi = Math.min(b, l.nz - 1);
|
|
98
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) {
|
|
99
|
+
const v = (layer[offset + word] & wordMask(word * 32, lo, hi)) >>> 0;
|
|
100
|
+
if (v !== 0) return word * 32 + (31 - Math.clz32(v & -v));
|
|
101
|
+
}
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
104
|
+
function firstClear(l, layer, offset, a, b) {
|
|
105
|
+
const lo = Math.max(a, 0);
|
|
106
|
+
const hi = Math.min(b, l.nz - 1);
|
|
107
|
+
for (let word = lo >>> 5; word <= hi >>> 5; word++) {
|
|
108
|
+
const v = (~layer[offset + word] & wordMask(word * 32, lo, hi)) >>> 0;
|
|
109
|
+
if (v !== 0) return word * 32 + (31 - Math.clz32(v & -v));
|
|
110
|
+
}
|
|
111
|
+
return -1;
|
|
112
|
+
}
|
|
113
|
+
function lastSet(l, layer, offset, a, b) {
|
|
114
|
+
const lo = Math.max(a, 0);
|
|
115
|
+
const hi = Math.min(b, l.nz - 1);
|
|
116
|
+
for (let word = hi >>> 5; word >= lo >>> 5; word--) {
|
|
117
|
+
const v = (layer[offset + word] & wordMask(word * 32, lo, hi)) >>> 0;
|
|
118
|
+
if (v !== 0) return word * 32 + (31 - Math.clz32(v));
|
|
119
|
+
}
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
function dilateXY(l, from, radius) {
|
|
123
|
+
const out = from.slice();
|
|
124
|
+
if (radius <= 0) return out;
|
|
125
|
+
const { nx, ny, w } = l;
|
|
126
|
+
const size = 2 * radius + 1;
|
|
127
|
+
const passes = [
|
|
128
|
+
[nx, 1, ny],
|
|
129
|
+
[ny, nx, nx]
|
|
130
|
+
];
|
|
131
|
+
const prefix = new Uint32Array(Math.max(nx, ny));
|
|
132
|
+
const suffix = new Uint32Array(Math.max(nx, ny));
|
|
133
|
+
for (const [length, stride, lines] of passes) {
|
|
134
|
+
const lineStride = stride === 1 ? nx : 1;
|
|
135
|
+
for (let word = 0; word < w; word++) {
|
|
136
|
+
for (let line = 0; line < lines; line++) {
|
|
137
|
+
const base = line * lineStride * w + word;
|
|
138
|
+
let any = 0;
|
|
139
|
+
for (let i = 0; i < length; i++) any |= out[base + i * stride * w];
|
|
140
|
+
if (any === 0) continue;
|
|
141
|
+
for (let i = 0; i < length; i++) {
|
|
142
|
+
const v = out[base + i * stride * w];
|
|
143
|
+
prefix[i] = i % size === 0 ? v : prefix[i - 1] | v;
|
|
144
|
+
}
|
|
145
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
146
|
+
const v = out[base + i * stride * w];
|
|
147
|
+
suffix[i] = i === length - 1 || (i + 1) % size === 0 ? v : suffix[i + 1] | v;
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < length; i++) {
|
|
150
|
+
const a = Math.max(0, i - radius);
|
|
151
|
+
const b = Math.min(length - 1, i + radius);
|
|
152
|
+
out[base + i * stride * w] = (suffix[a] | prefix[b]) >>> 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function reachDown(l, from, span) {
|
|
160
|
+
const out = from.slice();
|
|
161
|
+
const { w } = l;
|
|
162
|
+
let filled = 1;
|
|
163
|
+
while (filled < span) {
|
|
164
|
+
const shift = Math.min(filled, span - filled);
|
|
165
|
+
const words = shift >>> 5;
|
|
166
|
+
const bits = shift & 31;
|
|
167
|
+
for (let o = 0; o < out.length; o += w) {
|
|
168
|
+
for (let i = 0; i < w; i++) {
|
|
169
|
+
const a = i + words < w ? out[o + i + words] : 0;
|
|
170
|
+
const b = i + words + 1 < w ? out[o + i + words + 1] : 0;
|
|
171
|
+
const moved = bits === 0 ? a : a >>> bits | b << 32 - bits;
|
|
172
|
+
out[o + i] = (out[o + i] | moved) >>> 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
filled += shift;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function reachUp(l, from, span) {
|
|
180
|
+
const out = from.slice();
|
|
181
|
+
const { w } = l;
|
|
182
|
+
let filled = 1;
|
|
183
|
+
while (filled < span) {
|
|
184
|
+
const shift = Math.min(filled, span - filled);
|
|
185
|
+
const words = shift >>> 5;
|
|
186
|
+
const bits = shift & 31;
|
|
187
|
+
for (let o = 0; o < out.length; o += w) {
|
|
188
|
+
for (let i = w - 1; i >= 0; i--) {
|
|
189
|
+
const a = i - words >= 0 ? out[o + i - words] : 0;
|
|
190
|
+
const b = i - words - 1 >= 0 ? out[o + i - words - 1] : 0;
|
|
191
|
+
const moved = bits === 0 ? a : a << bits | b >>> 32 - bits;
|
|
192
|
+
out[o + i] = (out[o + i] | moved) >>> 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
filled += shift;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/world-fit/scene.ts
|
|
201
|
+
var WORLD_SCENE_FORMAT = "genex-world-scene";
|
|
202
|
+
var WORLD_SCENE_VERSION = 1;
|
|
203
|
+
var SceneChannel = { Move: "move", Shot: "shot" };
|
|
204
|
+
var SceneTag = {
|
|
205
|
+
/** Moves or disappears at run time: never part of the static world. Named in the report. */
|
|
206
|
+
Movable: "movable",
|
|
207
|
+
/** May be simplified or dropped when a budget runs out, before anything else that costs reach. */
|
|
208
|
+
Prop: "prop"
|
|
209
|
+
};
|
|
210
|
+
var SceneSurfaceKind = {
|
|
211
|
+
Box: "box",
|
|
212
|
+
Hull: "hull",
|
|
213
|
+
Sphere: "sphere",
|
|
214
|
+
Capsule: "capsule",
|
|
215
|
+
Mesh: "mesh",
|
|
216
|
+
Heightfield: "heightfield"
|
|
217
|
+
};
|
|
218
|
+
var WorldSceneError = class extends RangeError {
|
|
219
|
+
path;
|
|
220
|
+
constructor(path, message) {
|
|
221
|
+
super(`world scene: ${path} ${message}`);
|
|
222
|
+
this.name = "WorldSceneError";
|
|
223
|
+
this.path = path;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var MAX_COORD = 1e9;
|
|
227
|
+
var MAX_NAME_CHARS = 256;
|
|
228
|
+
var MAX_TAGS = 64;
|
|
229
|
+
var MAX_HULL_POINTS = 4096;
|
|
230
|
+
var MAX_HEIGHTFIELD_SIDE = 16384;
|
|
231
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
232
|
+
function finite(v, path) {
|
|
233
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new WorldSceneError(path, "is not a finite number");
|
|
234
|
+
if (Math.abs(v) > MAX_COORD) throw new WorldSceneError(path, `is past \xB1${MAX_COORD}`);
|
|
235
|
+
return v;
|
|
236
|
+
}
|
|
237
|
+
function positive(v, path) {
|
|
238
|
+
const n = finite(v, path);
|
|
239
|
+
if (!(n > 0)) throw new WorldSceneError(path, "is not above zero");
|
|
240
|
+
return n;
|
|
241
|
+
}
|
|
242
|
+
function vec3(v, path) {
|
|
243
|
+
if (!Array.isArray(v) || v.length !== 3) throw new WorldSceneError(path, "is not [x, y, z]");
|
|
244
|
+
return [finite(v[0], `${path}[0]`), finite(v[1], `${path}[1]`), finite(v[2], `${path}[2]`)];
|
|
245
|
+
}
|
|
246
|
+
function text(v, path) {
|
|
247
|
+
if (typeof v !== "string" || v.length === 0) throw new WorldSceneError(path, "is not a name");
|
|
248
|
+
if (v.length > MAX_NAME_CHARS) throw new WorldSceneError(path, `is longer than ${MAX_NAME_CHARS} characters`);
|
|
249
|
+
return v;
|
|
250
|
+
}
|
|
251
|
+
function textList(v, path) {
|
|
252
|
+
if (v === void 0) return [];
|
|
253
|
+
if (!Array.isArray(v)) throw new WorldSceneError(path, "is not a list");
|
|
254
|
+
if (v.length > MAX_TAGS) throw new WorldSceneError(path, `holds more than ${MAX_TAGS}`);
|
|
255
|
+
return v.map((t, i) => text(t, `${path}[${i}]`));
|
|
256
|
+
}
|
|
257
|
+
function floats(v, path) {
|
|
258
|
+
if (Array.isArray(v)) {
|
|
259
|
+
const out = new Float32Array(v.length);
|
|
260
|
+
for (let i = 0; i < v.length; i++) out[i] = v[i] === null ? Number.NaN : finite(v[i], `${path}[${i}]`);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
if (isObject(v) && typeof v.f32le === "string") {
|
|
264
|
+
const bytes = Buffer.from(v.f32le, "base64");
|
|
265
|
+
if (bytes.length % 4 !== 0) throw new WorldSceneError(path, "is not a whole number of float32s");
|
|
266
|
+
const out = new Float32Array(bytes.length / 4);
|
|
267
|
+
for (let i = 0; i < out.length; i++) out[i] = bytes.readFloatLE(i * 4);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
throw new WorldSceneError(path, "is not an array of numbers or { f32le }");
|
|
271
|
+
}
|
|
272
|
+
function indices(v, path) {
|
|
273
|
+
if (Array.isArray(v)) {
|
|
274
|
+
const out = new Uint32Array(v.length);
|
|
275
|
+
for (let i = 0; i < v.length; i++) {
|
|
276
|
+
const n = v[i];
|
|
277
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0 || n > 4294967295) throw new WorldSceneError(`${path}[${i}]`, "is not an index");
|
|
278
|
+
out[i] = n;
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
if (isObject(v) && typeof v.u32le === "string") {
|
|
283
|
+
const bytes = Buffer.from(v.u32le, "base64");
|
|
284
|
+
if (bytes.length % 4 !== 0) throw new WorldSceneError(path, "is not a whole number of u32s");
|
|
285
|
+
const out = new Uint32Array(bytes.length / 4);
|
|
286
|
+
for (let i = 0; i < out.length; i++) out[i] = bytes.readUInt32LE(i * 4);
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
throw new WorldSceneError(path, "is not an array of indices or { u32le }");
|
|
290
|
+
}
|
|
291
|
+
function common(raw, path) {
|
|
292
|
+
const out = {};
|
|
293
|
+
if (raw.channels !== void 0) {
|
|
294
|
+
if (!Array.isArray(raw.channels)) throw new WorldSceneError(`${path}.channels`, "is not a list");
|
|
295
|
+
out.channels = raw.channels.map((c, i) => {
|
|
296
|
+
if (c !== SceneChannel.Move && c !== SceneChannel.Shot) throw new WorldSceneError(`${path}.channels[${i}]`, "is not move or shot");
|
|
297
|
+
return c;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (raw.tags !== void 0) out.tags = textList(raw.tags, `${path}.tags`);
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
function parseBox(raw, path) {
|
|
304
|
+
const half = vec3(raw.half, `${path}.half`);
|
|
305
|
+
half.forEach((h, i) => {
|
|
306
|
+
if (h < 0) throw new WorldSceneError(`${path}.half[${i}]`, "is below zero");
|
|
307
|
+
});
|
|
308
|
+
const box = { kind: "box", center: vec3(raw.center, `${path}.center`), half, ...common(raw, path) };
|
|
309
|
+
if (raw.axes === void 0) return box;
|
|
310
|
+
if (!Array.isArray(raw.axes) || raw.axes.length !== 3) throw new WorldSceneError(`${path}.axes`, "is not three axes");
|
|
311
|
+
const axes = [vec3(raw.axes[0], `${path}.axes[0]`), vec3(raw.axes[1], `${path}.axes[1]`), vec3(raw.axes[2], `${path}.axes[2]`)];
|
|
312
|
+
for (const [i, a] of axes.entries()) {
|
|
313
|
+
if (Math.abs(Math.hypot(a[0], a[1], a[2]) - 1) > 1e-3) throw new WorldSceneError(`${path}.axes[${i}]`, "is not a unit vector");
|
|
314
|
+
}
|
|
315
|
+
return { ...box, axes };
|
|
316
|
+
}
|
|
317
|
+
function parseHeightfield(raw, path) {
|
|
318
|
+
const cols = raw.cols;
|
|
319
|
+
const rows = raw.rows;
|
|
320
|
+
const isSide = (n) => typeof n === "number" && Number.isInteger(n) && n >= 2 && n <= MAX_HEIGHTFIELD_SIDE;
|
|
321
|
+
if (!isSide(cols)) throw new WorldSceneError(`${path}.cols`, `is not an integer in 2..${MAX_HEIGHTFIELD_SIDE}`);
|
|
322
|
+
if (!isSide(rows)) throw new WorldSceneError(`${path}.rows`, `is not an integer in 2..${MAX_HEIGHTFIELD_SIDE}`);
|
|
323
|
+
if (!Array.isArray(raw.origin) || raw.origin.length !== 2) throw new WorldSceneError(`${path}.origin`, "is not [x, y]");
|
|
324
|
+
if (!Array.isArray(raw.cell) || raw.cell.length !== 2) throw new WorldSceneError(`${path}.cell`, "is not [dx, dy]");
|
|
325
|
+
const heights = floats(raw.heights, `${path}.heights`);
|
|
326
|
+
if (heights.length !== cols * rows) throw new WorldSceneError(`${path}.heights`, `holds ${heights.length} values, not cols \xD7 rows = ${cols * rows}`);
|
|
327
|
+
for (let i = 0; i < heights.length; i++) {
|
|
328
|
+
const h = heights[i];
|
|
329
|
+
if (Number.isFinite(h) ? Math.abs(h) > MAX_COORD : !Number.isNaN(h)) throw new WorldSceneError(`${path}.heights[${i}]`, "is not a height or a hole");
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: "heightfield",
|
|
333
|
+
origin: [finite(raw.origin[0], `${path}.origin[0]`), finite(raw.origin[1], `${path}.origin[1]`)],
|
|
334
|
+
cell: [positive(raw.cell[0], `${path}.cell[0]`), positive(raw.cell[1], `${path}.cell[1]`)],
|
|
335
|
+
cols,
|
|
336
|
+
rows,
|
|
337
|
+
heights,
|
|
338
|
+
...common(raw, path)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function parseMesh(raw, path) {
|
|
342
|
+
const vertices = floats(raw.vertices, `${path}.vertices`);
|
|
343
|
+
const tris = indices(raw.indices, `${path}.indices`);
|
|
344
|
+
if (vertices.length % 3 !== 0) throw new WorldSceneError(`${path}.vertices`, "is not x y z per vertex");
|
|
345
|
+
if (tris.length % 3 !== 0) throw new WorldSceneError(`${path}.indices`, "is not three per triangle");
|
|
346
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
347
|
+
const v = vertices[i];
|
|
348
|
+
if (!Number.isFinite(v) || Math.abs(v) > MAX_COORD) throw new WorldSceneError(`${path}.vertices[${i}]`, "is not a usable coordinate");
|
|
349
|
+
}
|
|
350
|
+
const count = vertices.length / 3;
|
|
351
|
+
for (let i = 0; i < tris.length; i++) {
|
|
352
|
+
if (tris[i] >= count) throw new WorldSceneError(`${path}.indices[${i}]`, `is past the ${count} vertices`);
|
|
353
|
+
}
|
|
354
|
+
return { kind: "mesh", vertices, indices: tris, ...common(raw, path) };
|
|
355
|
+
}
|
|
356
|
+
function parseSurface(raw, path) {
|
|
357
|
+
if (!isObject(raw)) throw new WorldSceneError(path, "is not an object");
|
|
358
|
+
switch (raw.kind) {
|
|
359
|
+
case SceneSurfaceKind.Box:
|
|
360
|
+
return parseBox(raw, path);
|
|
361
|
+
case SceneSurfaceKind.Hull: {
|
|
362
|
+
if (!Array.isArray(raw.points) || raw.points.length === 0) throw new WorldSceneError(`${path}.points`, "is not a list of points");
|
|
363
|
+
if (raw.points.length > MAX_HULL_POINTS) throw new WorldSceneError(`${path}.points`, `holds more than ${MAX_HULL_POINTS}`);
|
|
364
|
+
return { kind: "hull", points: raw.points.map((p, i) => vec3(p, `${path}.points[${i}]`)), ...common(raw, path) };
|
|
365
|
+
}
|
|
366
|
+
case SceneSurfaceKind.Sphere:
|
|
367
|
+
return { kind: "sphere", center: vec3(raw.center, `${path}.center`), radius: positive(raw.radius, `${path}.radius`), ...common(raw, path) };
|
|
368
|
+
case SceneSurfaceKind.Capsule:
|
|
369
|
+
return { kind: "capsule", a: vec3(raw.a, `${path}.a`), b: vec3(raw.b, `${path}.b`), radius: positive(raw.radius, `${path}.radius`), ...common(raw, path) };
|
|
370
|
+
case SceneSurfaceKind.Mesh:
|
|
371
|
+
return parseMesh(raw, path);
|
|
372
|
+
case SceneSurfaceKind.Heightfield:
|
|
373
|
+
return parseHeightfield(raw, path);
|
|
374
|
+
default:
|
|
375
|
+
throw new WorldSceneError(`${path}.kind`, "is not box, hull, sphere, capsule, mesh or heightfield");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function parseHullSize(raw, path) {
|
|
379
|
+
if (!isObject(raw)) throw new WorldSceneError(path, "is not an object");
|
|
380
|
+
const radius = positive(raw.radius, `${path}.radius`);
|
|
381
|
+
const height = positive(raw.height, `${path}.height`);
|
|
382
|
+
const crouchHeight = positive(raw.crouchHeight, `${path}.crouchHeight`);
|
|
383
|
+
if (crouchHeight > height) throw new WorldSceneError(`${path}.crouchHeight`, "is above height");
|
|
384
|
+
return {
|
|
385
|
+
radius,
|
|
386
|
+
height,
|
|
387
|
+
crouchHeight,
|
|
388
|
+
step: positive(raw.step, `${path}.step`),
|
|
389
|
+
jump: finite(raw.jump, `${path}.jump`),
|
|
390
|
+
leap: finite(raw.leap, `${path}.leap`)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function boundCorner(raw, path) {
|
|
394
|
+
const ok = Array.isArray(raw) && raw.length === 2 && raw.every((n) => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= MAX_COORD);
|
|
395
|
+
if (!ok) throw new WorldSceneError(path, `is not two finite numbers within \xB1${MAX_COORD}`);
|
|
396
|
+
return [raw[0], raw[1]];
|
|
397
|
+
}
|
|
398
|
+
function parseBound(raw) {
|
|
399
|
+
if (!isObject(raw)) throw new WorldSceneError("$.bound", "is not an object");
|
|
400
|
+
const min = boundCorner(raw.min, "$.bound.min");
|
|
401
|
+
const max = boundCorner(raw.max, "$.bound.max");
|
|
402
|
+
if (max[0] <= min[0] || max[1] <= min[1]) throw new WorldSceneError("$.bound", "has a max not past its min");
|
|
403
|
+
if (raw.ceiling === void 0) return { min, max };
|
|
404
|
+
return { min, max, ceiling: finite(raw.ceiling, "$.bound.ceiling") };
|
|
405
|
+
}
|
|
406
|
+
function insideBound(bound, at) {
|
|
407
|
+
const inPlan = at[0] > bound.min[0] && at[0] < bound.max[0] && at[1] > bound.min[1] && at[1] < bound.max[1];
|
|
408
|
+
return inPlan && (bound.ceiling === void 0 || at[2] < bound.ceiling);
|
|
409
|
+
}
|
|
410
|
+
function parseWorldScene(raw) {
|
|
411
|
+
if (!isObject(raw)) throw new WorldSceneError("$", "is not an object");
|
|
412
|
+
if (raw.format !== WORLD_SCENE_FORMAT) throw new WorldSceneError("$.format", `is not ${WORLD_SCENE_FORMAT}`);
|
|
413
|
+
if (raw.version !== WORLD_SCENE_VERSION) throw new WorldSceneError("$.version", `is not ${WORLD_SCENE_VERSION}`);
|
|
414
|
+
if (!Array.isArray(raw.actors)) throw new WorldSceneError("$.actors", "is not a list");
|
|
415
|
+
if (!Array.isArray(raw.spawns)) throw new WorldSceneError("$.spawns", "is not a list");
|
|
416
|
+
const actors = raw.actors.map((a, i) => {
|
|
417
|
+
const path = `$.actors[${i}]`;
|
|
418
|
+
if (!isObject(a)) throw new WorldSceneError(path, "is not an object");
|
|
419
|
+
if (!Array.isArray(a.surfaces)) throw new WorldSceneError(`${path}.surfaces`, "is not a list");
|
|
420
|
+
return {
|
|
421
|
+
name: text(a.name, `${path}.name`),
|
|
422
|
+
tags: textList(a.tags, `${path}.tags`),
|
|
423
|
+
surfaces: a.surfaces.map((s, k) => parseSurface(s, `${path}.surfaces[${k}]`))
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
const spawns = raw.spawns.map((s, i) => {
|
|
427
|
+
const path = `$.spawns[${i}]`;
|
|
428
|
+
if (!isObject(s)) throw new WorldSceneError(path, "is not an object");
|
|
429
|
+
const spawn = { at: vec3(s.at, `${path}.at`) };
|
|
430
|
+
if (s.name !== void 0) spawn.name = text(s.name, `${path}.name`);
|
|
431
|
+
if (s.tags !== void 0) spawn.tags = textList(s.tags, `${path}.tags`);
|
|
432
|
+
return spawn;
|
|
433
|
+
});
|
|
434
|
+
const source = {};
|
|
435
|
+
if (raw.source !== void 0) {
|
|
436
|
+
if (!isObject(raw.source)) throw new WorldSceneError("$.source", "is not an object");
|
|
437
|
+
for (const [k, v] of Object.entries(raw.source)) source[k] = text(v, `$.source.${k}`);
|
|
438
|
+
}
|
|
439
|
+
const bound = raw.bound === void 0 ? void 0 : parseBound(raw.bound);
|
|
440
|
+
if (bound !== void 0) {
|
|
441
|
+
spawns.forEach((spawn, i) => {
|
|
442
|
+
if (!insideBound(bound, spawn.at)) throw new WorldSceneError(`$.spawns[${i}].at`, "is outside the bound");
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const scene = {
|
|
446
|
+
format: WORLD_SCENE_FORMAT,
|
|
447
|
+
version: WORLD_SCENE_VERSION,
|
|
448
|
+
unitsPerMeter: positive(raw.unitsPerMeter, "$.unitsPerMeter"),
|
|
449
|
+
actors,
|
|
450
|
+
spawns,
|
|
451
|
+
...raw.source === void 0 ? {} : { source },
|
|
452
|
+
...raw.hull === void 0 ? {} : { hull: parseHullSize(raw.hull, "$.hull") },
|
|
453
|
+
...bound === void 0 ? {} : { bound }
|
|
454
|
+
};
|
|
455
|
+
return scene;
|
|
456
|
+
}
|
|
457
|
+
function surfaceAnswers(surface, channel) {
|
|
458
|
+
return surface.channels === void 0 || surface.channels.includes(channel);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/world-fit/raster.ts
|
|
462
|
+
var SPHERE_SEGMENTS = 12;
|
|
463
|
+
var SPHERE_STACKS = 8;
|
|
464
|
+
var HULL_EPSILON = 1e-7;
|
|
465
|
+
var PLANAR_EPSILON = 1e-6;
|
|
466
|
+
var BOX_CORNER_SHARE = 1e-5;
|
|
467
|
+
var CLIP_CAPACITY = 16;
|
|
468
|
+
var polyA = new Float64Array(CLIP_CAPACITY * 3);
|
|
469
|
+
var polyB = new Float64Array(CLIP_CAPACITY * 3);
|
|
470
|
+
var polyC = new Float64Array(CLIP_CAPACITY * 3);
|
|
471
|
+
function clipAxis(input, count, axis, lo, hi, output, scratch) {
|
|
472
|
+
let n = 0;
|
|
473
|
+
for (let i = 0; i < count; i++) {
|
|
474
|
+
const j = (i + 1) % count;
|
|
475
|
+
const pi = input[i * 3 + axis] - lo;
|
|
476
|
+
const pj = input[j * 3 + axis] - lo;
|
|
477
|
+
if (pi >= 0) {
|
|
478
|
+
scratch[n * 3] = input[i * 3];
|
|
479
|
+
scratch[n * 3 + 1] = input[i * 3 + 1];
|
|
480
|
+
scratch[n * 3 + 2] = input[i * 3 + 2];
|
|
481
|
+
n++;
|
|
482
|
+
}
|
|
483
|
+
if (pi > 0 && pj < 0 || pi < 0 && pj > 0) {
|
|
484
|
+
const t = pi / (pi - pj);
|
|
485
|
+
scratch[n * 3] = input[i * 3] + (input[j * 3] - input[i * 3]) * t;
|
|
486
|
+
scratch[n * 3 + 1] = input[i * 3 + 1] + (input[j * 3 + 1] - input[i * 3 + 1]) * t;
|
|
487
|
+
scratch[n * 3 + 2] = input[i * 3 + 2] + (input[j * 3 + 2] - input[i * 3 + 2]) * t;
|
|
488
|
+
n++;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const mid = n;
|
|
492
|
+
n = 0;
|
|
493
|
+
for (let i = 0; i < mid; i++) {
|
|
494
|
+
const j = (i + 1) % mid;
|
|
495
|
+
const pi = hi - scratch[i * 3 + axis];
|
|
496
|
+
const pj = hi - scratch[j * 3 + axis];
|
|
497
|
+
if (pi >= 0) {
|
|
498
|
+
output[n * 3] = scratch[i * 3];
|
|
499
|
+
output[n * 3 + 1] = scratch[i * 3 + 1];
|
|
500
|
+
output[n * 3 + 2] = scratch[i * 3 + 2];
|
|
501
|
+
n++;
|
|
502
|
+
}
|
|
503
|
+
if (pi > 0 && pj < 0 || pi < 0 && pj > 0) {
|
|
504
|
+
const t = pi / (pi - pj);
|
|
505
|
+
output[n * 3] = scratch[i * 3] + (scratch[j * 3] - scratch[i * 3]) * t;
|
|
506
|
+
output[n * 3 + 1] = scratch[i * 3 + 1] + (scratch[j * 3 + 1] - scratch[i * 3 + 1]) * t;
|
|
507
|
+
output[n * 3 + 2] = scratch[i * 3 + 2] + (scratch[j * 3 + 2] - scratch[i * 3 + 2]) * t;
|
|
508
|
+
n++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return n;
|
|
512
|
+
}
|
|
513
|
+
var scratchClip = new Float64Array(CLIP_CAPACITY * 3);
|
|
514
|
+
function triangleColumns(l, half, ax, ay, az, bx, by, bz, cx, cy, cz, visit) {
|
|
515
|
+
const { h, nx, ny } = l;
|
|
516
|
+
const minX = Math.min(ax, bx, cx);
|
|
517
|
+
const maxX = Math.max(ax, bx, cx);
|
|
518
|
+
const minY = Math.min(ay, by, cy);
|
|
519
|
+
const maxY = Math.max(ay, by, cy);
|
|
520
|
+
const i0 = Math.max(0, cellFloor((minX - half - l.x0) / h - 0.5) + 1);
|
|
521
|
+
const i1 = Math.min(nx - 1, cellCeil((maxX + half - l.x0) / h - 0.5) - 1);
|
|
522
|
+
const j0 = Math.max(0, cellFloor((minY - half - l.y0) / h - 0.5) + 1);
|
|
523
|
+
const j1 = Math.min(ny - 1, cellCeil((maxY + half - l.y0) / h - 0.5) - 1);
|
|
524
|
+
if (i0 > i1 || j0 > j1) return;
|
|
525
|
+
polyA[0] = ax;
|
|
526
|
+
polyA[1] = ay;
|
|
527
|
+
polyA[2] = az;
|
|
528
|
+
polyA[3] = bx;
|
|
529
|
+
polyA[4] = by;
|
|
530
|
+
polyA[5] = bz;
|
|
531
|
+
polyA[6] = cx;
|
|
532
|
+
polyA[7] = cy;
|
|
533
|
+
polyA[8] = cz;
|
|
534
|
+
for (let i = i0; i <= i1; i++) {
|
|
535
|
+
const px = centreX(l, i);
|
|
536
|
+
const nA = clipAxis(polyA, 3, 0, px - half, px + half, polyB, scratchClip);
|
|
537
|
+
if (nA < 1) continue;
|
|
538
|
+
for (let j = j0; j <= j1; j++) {
|
|
539
|
+
const py = centreY(l, j);
|
|
540
|
+
const nB = clipAxis(polyB, nA, 1, py - half, py + half, polyC, scratchClip);
|
|
541
|
+
if (nB < 1) continue;
|
|
542
|
+
let zMin = Number.POSITIVE_INFINITY;
|
|
543
|
+
let zMax = Number.NEGATIVE_INFINITY;
|
|
544
|
+
for (let v = 0; v < nB; v++) {
|
|
545
|
+
const z = polyC[v * 3 + 2];
|
|
546
|
+
if (z < zMin) zMin = z;
|
|
547
|
+
if (z > zMax) zMax = z;
|
|
548
|
+
}
|
|
549
|
+
visit(i, j, zMin, zMax);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function fill(l, layer, i, j, zMin, zMax) {
|
|
554
|
+
const a = cellFloor((zMin - l.z0) / l.h);
|
|
555
|
+
const b = Math.max(a, cellCeil((zMax - l.z0) / l.h) - 1);
|
|
556
|
+
setRun(l, layer, (j * l.nx + i) * l.w, a, b);
|
|
557
|
+
}
|
|
558
|
+
var BOX_FACES = [0, 1, 3, 0, 3, 2, 4, 6, 7, 4, 7, 5, 0, 4, 5, 0, 5, 1, 2, 3, 7, 2, 7, 6, 0, 2, 6, 0, 6, 4, 1, 5, 7, 1, 7, 3];
|
|
559
|
+
function boxCorners(surface) {
|
|
560
|
+
const { center: c, half: e } = surface;
|
|
561
|
+
const a = surface.axes ?? [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
|
562
|
+
const out = [];
|
|
563
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
564
|
+
const sx = corner & 1 ? 1 : -1;
|
|
565
|
+
const sy = corner & 2 ? 1 : -1;
|
|
566
|
+
const sz = corner & 4 ? 1 : -1;
|
|
567
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
568
|
+
out.push(c[axis] + sx * e[0] * a[0][axis] + sy * e[1] * a[1][axis] + sz * e[2] * a[2][axis]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function spherePoints(c, radius) {
|
|
574
|
+
const r = radius / Math.cos(Math.PI / SPHERE_SEGMENTS);
|
|
575
|
+
const out = [c[0], c[1], c[2] + r, c[0], c[1], c[2] - r];
|
|
576
|
+
for (let s = 1; s < SPHERE_STACKS; s++) {
|
|
577
|
+
const polar = s / SPHERE_STACKS * Math.PI;
|
|
578
|
+
for (let a = 0; a < SPHERE_SEGMENTS; a++) {
|
|
579
|
+
const azimuth = a / SPHERE_SEGMENTS * Math.PI * 2;
|
|
580
|
+
out.push(c[0] + r * Math.sin(polar) * Math.cos(azimuth), c[1] + r * Math.sin(polar) * Math.sin(azimuth), c[2] + r * Math.cos(polar));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
function convexHullFaces(points) {
|
|
586
|
+
const n = points.length / 3;
|
|
587
|
+
const at = (i) => [points[i * 3], points[i * 3 + 1], points[i * 3 + 2]];
|
|
588
|
+
let size = 0;
|
|
589
|
+
for (let i = 1; i < n; i++) {
|
|
590
|
+
const [x, y, z] = at(i);
|
|
591
|
+
size = Math.max(size, Math.abs(x - points[0]), Math.abs(y - points[1]), Math.abs(z - points[2]));
|
|
592
|
+
}
|
|
593
|
+
if (n < 3 || size === 0) return [];
|
|
594
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
595
|
+
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]];
|
|
596
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
597
|
+
const p0 = at(0);
|
|
598
|
+
let i1 = 0;
|
|
599
|
+
let best = 0;
|
|
600
|
+
for (let i = 1; i < n; i++) {
|
|
601
|
+
const d = sub(at(i), p0);
|
|
602
|
+
const v = dot(d, d);
|
|
603
|
+
if (v > best) {
|
|
604
|
+
best = v;
|
|
605
|
+
i1 = i;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const lineDir = sub(at(i1), p0);
|
|
609
|
+
let i2 = -1;
|
|
610
|
+
best = (PLANAR_EPSILON * size) ** 2 * dot(lineDir, lineDir);
|
|
611
|
+
for (let i = 0; i < n; i++) {
|
|
612
|
+
const c = cross(lineDir, sub(at(i), p0));
|
|
613
|
+
const v = dot(c, c);
|
|
614
|
+
if (v > best) {
|
|
615
|
+
best = v;
|
|
616
|
+
i2 = i;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (i2 < 0) return [];
|
|
620
|
+
const normal = cross(lineDir, sub(at(i2), p0));
|
|
621
|
+
const normalLength = Math.sqrt(dot(normal, normal));
|
|
622
|
+
let i3 = -1;
|
|
623
|
+
best = PLANAR_EPSILON * size;
|
|
624
|
+
for (let i = 0; i < n; i++) {
|
|
625
|
+
const v = Math.abs(dot(normal, sub(at(i), p0))) / normalLength;
|
|
626
|
+
if (v > best) {
|
|
627
|
+
best = v;
|
|
628
|
+
i3 = i;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (i3 < 0) {
|
|
632
|
+
const centre = [0, 0, 0];
|
|
633
|
+
for (let i = 0; i < n; i++) {
|
|
634
|
+
const p = at(i);
|
|
635
|
+
centre[0] += p[0] / n;
|
|
636
|
+
centre[1] += p[1] / n;
|
|
637
|
+
centre[2] += p[2] / n;
|
|
638
|
+
}
|
|
639
|
+
const u = lineDir;
|
|
640
|
+
const v = cross(normal, u);
|
|
641
|
+
const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => {
|
|
642
|
+
const da = sub(at(a), centre);
|
|
643
|
+
const db = sub(at(b), centre);
|
|
644
|
+
return Math.atan2(dot(da, v), dot(da, u) * Math.hypot(...v) / Math.hypot(...u)) - Math.atan2(dot(db, v), dot(db, u) * Math.hypot(...v) / Math.hypot(...u));
|
|
645
|
+
});
|
|
646
|
+
const fan = [];
|
|
647
|
+
for (let k = 1; k + 1 < n; k++) fan.push(order[0], order[k], order[k + 1]);
|
|
648
|
+
return fan;
|
|
649
|
+
}
|
|
650
|
+
const inner = [0, 0, 0];
|
|
651
|
+
for (const i of [0, i1, i2, i3]) {
|
|
652
|
+
const p = at(i);
|
|
653
|
+
inner[0] += p[0] / 4;
|
|
654
|
+
inner[1] += p[1] / 4;
|
|
655
|
+
inner[2] += p[2] / 4;
|
|
656
|
+
}
|
|
657
|
+
const makeFace = (a, b, c) => {
|
|
658
|
+
let m = cross(sub(at(b), at(a)), sub(at(c), at(a)));
|
|
659
|
+
const length = Math.sqrt(dot(m, m)) || 1;
|
|
660
|
+
m = [m[0] / length, m[1] / length, m[2] / length];
|
|
661
|
+
let d = dot(m, at(a));
|
|
662
|
+
if (dot(m, inner) - d > 0) {
|
|
663
|
+
m = [-m[0], -m[1], -m[2]];
|
|
664
|
+
d = -d;
|
|
665
|
+
return { a, b: c, c: b, nx: m[0], ny: m[1], nz: m[2], d };
|
|
666
|
+
}
|
|
667
|
+
return { a, b, c, nx: m[0], ny: m[1], nz: m[2], d };
|
|
668
|
+
};
|
|
669
|
+
let faces = [makeFace(0, i1, i2), makeFace(0, i1, i3), makeFace(0, i2, i3), makeFace(i1, i2, i3)];
|
|
670
|
+
const tolerance = HULL_EPSILON * size;
|
|
671
|
+
for (let p = 0; p < n; p++) {
|
|
672
|
+
const [x, y, z] = at(p);
|
|
673
|
+
const visible = faces.filter((f2) => f2.nx * x + f2.ny * y + f2.nz * z - f2.d > tolerance);
|
|
674
|
+
if (visible.length === 0) continue;
|
|
675
|
+
const edges = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const f2 of visible) {
|
|
677
|
+
for (const [a, b] of [[f2.a, f2.b], [f2.b, f2.c], [f2.c, f2.a]]) {
|
|
678
|
+
if (edges.has(b * n + a)) edges.delete(b * n + a);
|
|
679
|
+
else edges.set(a * n + b, [a, b]);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const seen = new Set(visible);
|
|
683
|
+
faces = faces.filter((f2) => !seen.has(f2));
|
|
684
|
+
for (const [a, b] of edges.values()) faces.push(makeFace(a, b, p));
|
|
685
|
+
}
|
|
686
|
+
return faces.flatMap((f2) => [f2.a, f2.b, f2.c]);
|
|
687
|
+
}
|
|
688
|
+
function convexTriangles(points, indices2, sink) {
|
|
689
|
+
for (let t = 0; t + 2 < indices2.length; t += 3) {
|
|
690
|
+
const a = indices2[t] * 3;
|
|
691
|
+
const b = indices2[t + 1] * 3;
|
|
692
|
+
const c = indices2[t + 2] * 3;
|
|
693
|
+
sink(points[a], points[a + 1], points[a + 2], points[b], points[b + 1], points[b + 2], points[c], points[c + 1], points[c + 2]);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function hullAsBox(points) {
|
|
697
|
+
const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
698
|
+
const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
699
|
+
for (const p of points) {
|
|
700
|
+
for (let a = 0; a < 3; a++) {
|
|
701
|
+
lo[a] = Math.min(lo[a], p[a]);
|
|
702
|
+
hi[a] = Math.max(hi[a], p[a]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const size = Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]);
|
|
706
|
+
const near = (v, target) => Math.abs(v - target) <= size * BOX_CORNER_SHARE;
|
|
707
|
+
const seen = /* @__PURE__ */ new Set();
|
|
708
|
+
for (const p of points) {
|
|
709
|
+
let corner = 0;
|
|
710
|
+
for (let a = 0; a < 3; a++) {
|
|
711
|
+
if (near(p[a], hi[a])) corner |= 1 << a;
|
|
712
|
+
else if (!near(p[a], lo[a])) return null;
|
|
713
|
+
}
|
|
714
|
+
seen.add(corner);
|
|
715
|
+
}
|
|
716
|
+
if (seen.size !== 8) return null;
|
|
717
|
+
return { min: [lo[0], lo[1], lo[2]], max: [hi[0], hi[1], hi[2]] };
|
|
718
|
+
}
|
|
719
|
+
function exactBoxBounds(surface) {
|
|
720
|
+
if (surface.kind === "hull") return hullAsBox(surface.points);
|
|
721
|
+
if (surface.kind !== "box") return null;
|
|
722
|
+
const a = surface.axes;
|
|
723
|
+
if (a !== void 0) {
|
|
724
|
+
const on = (v) => {
|
|
725
|
+
const big = Math.max(Math.abs(v[0]), Math.abs(v[1]), Math.abs(v[2]));
|
|
726
|
+
return big > 1 - 1e-6;
|
|
727
|
+
};
|
|
728
|
+
if (!on(a[0]) || !on(a[1]) || !on(a[2])) return null;
|
|
729
|
+
}
|
|
730
|
+
const row = (axis) => {
|
|
731
|
+
if (a === void 0) return surface.half[axis];
|
|
732
|
+
return Math.abs(a[0][axis]) * surface.half[0] + Math.abs(a[1][axis]) * surface.half[1] + Math.abs(a[2][axis]) * surface.half[2];
|
|
733
|
+
};
|
|
734
|
+
const e = [row(0), row(1), row(2)];
|
|
735
|
+
const c = surface.center;
|
|
736
|
+
return { min: [c[0] - e[0], c[1] - e[1], c[2] - e[2]], max: [c[0] + e[0], c[1] + e[1], c[2] + e[2]] };
|
|
737
|
+
}
|
|
738
|
+
function rasterConvex(l, layer, half, points, indices2) {
|
|
739
|
+
const spans = /* @__PURE__ */ new Map();
|
|
740
|
+
convexTriangles(points, indices2, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
741
|
+
triangleColumns(l, half, ax, ay, az, bx, by, bz, cx, cy, cz, (i, j, zMin, zMax) => {
|
|
742
|
+
const key = j * l.nx + i;
|
|
743
|
+
const span = spans.get(key);
|
|
744
|
+
if (span === void 0) spans.set(key, [zMin, zMax]);
|
|
745
|
+
else {
|
|
746
|
+
if (zMin < span[0]) span[0] = zMin;
|
|
747
|
+
if (zMax > span[1]) span[1] = zMax;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
for (const [key, span] of spans) fill(l, layer, key % l.nx, Math.floor(key / l.nx), span[0], span[1]);
|
|
752
|
+
}
|
|
753
|
+
function rasterTriangle(l, layer, half, ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
754
|
+
triangleColumns(l, half, ax, ay, az, bx, by, bz, cx, cy, cz, (i, j, zMin, zMax) => fill(l, layer, i, j, zMin, zMax));
|
|
755
|
+
}
|
|
756
|
+
function surfaceTriangles(surface, sink) {
|
|
757
|
+
switch (surface.kind) {
|
|
758
|
+
case "box":
|
|
759
|
+
convexTriangles(boxCorners(surface), BOX_FACES, sink);
|
|
760
|
+
return;
|
|
761
|
+
case "hull": {
|
|
762
|
+
const points = surface.points.flat();
|
|
763
|
+
convexTriangles(points, convexHullFaces(points), sink);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
case "sphere": {
|
|
767
|
+
const points = spherePoints(surface.center, surface.radius);
|
|
768
|
+
convexTriangles(points, convexHullFaces(points), sink);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
case "capsule": {
|
|
772
|
+
const points = [...spherePoints(surface.a, surface.radius), ...spherePoints(surface.b, surface.radius)];
|
|
773
|
+
convexTriangles(points, convexHullFaces(points), sink);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
case "mesh": {
|
|
777
|
+
const { vertices: v, indices: t } = surface;
|
|
778
|
+
for (let k = 0; k + 2 < t.length; k += 3) {
|
|
779
|
+
const a = t[k] * 3;
|
|
780
|
+
const b = t[k + 1] * 3;
|
|
781
|
+
const c = t[k + 2] * 3;
|
|
782
|
+
sink(v[a], v[a + 1], v[a + 2], v[b], v[b + 1], v[b + 2], v[c], v[c + 1], v[c + 2]);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
case "heightfield": {
|
|
787
|
+
const { origin, cell, cols, rows, heights } = surface;
|
|
788
|
+
for (let r = 0; r + 1 < rows; r++) {
|
|
789
|
+
for (let c = 0; c + 1 < cols; c++) {
|
|
790
|
+
const za = heights[r * cols + c];
|
|
791
|
+
const zb = heights[r * cols + c + 1];
|
|
792
|
+
const zc = heights[(r + 1) * cols + c];
|
|
793
|
+
const zd = heights[(r + 1) * cols + c + 1];
|
|
794
|
+
const x0 = origin[0] + c * cell[0];
|
|
795
|
+
const x1 = x0 + cell[0];
|
|
796
|
+
const y0 = origin[1] + r * cell[1];
|
|
797
|
+
const y1 = y0 + cell[1];
|
|
798
|
+
if (!Number.isNaN(za) && !Number.isNaN(zb) && !Number.isNaN(zc)) sink(x0, y0, za, x1, y0, zb, x0, y1, zc);
|
|
799
|
+
if (!Number.isNaN(zb) && !Number.isNaN(zc) && !Number.isNaN(zd)) sink(x1, y0, zb, x0, y1, zc, x1, y1, zd);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function rasterAabb(l, layer, half, box) {
|
|
807
|
+
const { h } = l;
|
|
808
|
+
const i0 = Math.max(0, cellFloor((box.min[0] - half - l.x0) / h - 0.5) + 1);
|
|
809
|
+
const i1 = Math.min(l.nx - 1, cellCeil((box.max[0] + half - l.x0) / h - 0.5) - 1);
|
|
810
|
+
const j0 = Math.max(0, cellFloor((box.min[1] - half - l.y0) / h - 0.5) + 1);
|
|
811
|
+
const j1 = Math.min(l.ny - 1, cellCeil((box.max[1] + half - l.y0) / h - 0.5) - 1);
|
|
812
|
+
const a = cellFloor((box.min[2] - l.z0) / h);
|
|
813
|
+
const b = Math.max(a, cellCeil((box.max[2] - l.z0) / h) - 1);
|
|
814
|
+
for (let j = j0; j <= j1; j++) {
|
|
815
|
+
for (let i = i0; i <= i1; i++) setRun(l, layer, (j * l.nx + i) * l.w, a, b);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function rasterSurface(l, layer, half, surface) {
|
|
819
|
+
switch (surface.kind) {
|
|
820
|
+
case "box": {
|
|
821
|
+
const exact = exactBoxBounds(surface);
|
|
822
|
+
if (exact !== null) rasterAabb(l, layer, half, exact);
|
|
823
|
+
else rasterConvex(l, layer, half, boxCorners(surface), BOX_FACES);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
case "hull": {
|
|
827
|
+
const exact = exactBoxBounds(surface);
|
|
828
|
+
if (exact !== null) {
|
|
829
|
+
rasterAabb(l, layer, half, exact);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const points = surface.points.flat();
|
|
833
|
+
rasterConvex(l, layer, half, points, convexHullFaces(points));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
case "sphere": {
|
|
837
|
+
const points = spherePoints(surface.center, surface.radius);
|
|
838
|
+
rasterConvex(l, layer, half, points, convexHullFaces(points));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
case "capsule": {
|
|
842
|
+
const points = [...spherePoints(surface.a, surface.radius), ...spherePoints(surface.b, surface.radius)];
|
|
843
|
+
rasterConvex(l, layer, half, points, convexHullFaces(points));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
case "mesh": {
|
|
847
|
+
const { vertices: v, indices: t } = surface;
|
|
848
|
+
for (let k = 0; k + 2 < t.length; k += 3) {
|
|
849
|
+
const a = t[k] * 3;
|
|
850
|
+
const b = t[k + 1] * 3;
|
|
851
|
+
const c = t[k + 2] * 3;
|
|
852
|
+
triangleColumns(l, half, v[a], v[a + 1], v[a + 2], v[b], v[b + 1], v[b + 2], v[c], v[c + 1], v[c + 2], (i, j, zMin, zMax) => fill(l, layer, i, j, zMin, zMax));
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
case "heightfield":
|
|
857
|
+
rasterHeightfield(l, layer, half, surface);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function rasterHeightfield(l, layer, half, surface) {
|
|
862
|
+
const { origin, cell, cols, rows, heights } = surface;
|
|
863
|
+
const below = l.z0 - l.h;
|
|
864
|
+
const solid = (i, j, _zMin, zMax) => fill(l, layer, i, j, below, zMax);
|
|
865
|
+
for (let r = 0; r + 1 < rows; r++) {
|
|
866
|
+
for (let c = 0; c + 1 < cols; c++) {
|
|
867
|
+
const za = heights[r * cols + c];
|
|
868
|
+
const zb = heights[r * cols + c + 1];
|
|
869
|
+
const zc = heights[(r + 1) * cols + c];
|
|
870
|
+
const zd = heights[(r + 1) * cols + c + 1];
|
|
871
|
+
const x0 = origin[0] + c * cell[0];
|
|
872
|
+
const x1 = x0 + cell[0];
|
|
873
|
+
const y0 = origin[1] + r * cell[1];
|
|
874
|
+
const y1 = y0 + cell[1];
|
|
875
|
+
if (!Number.isNaN(za) && !Number.isNaN(zb) && !Number.isNaN(zc)) triangleColumns(l, half, x0, y0, za, x1, y0, zb, x0, y1, zc, solid);
|
|
876
|
+
if (!Number.isNaN(zb) && !Number.isNaN(zc) && !Number.isNaN(zd)) triangleColumns(l, half, x1, y0, zb, x0, y1, zc, x1, y1, zd, solid);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function stopsShot(actor, surface) {
|
|
881
|
+
if (actor.tags.includes(SceneTag.Movable) || surface.tags?.includes(SceneTag.Movable)) return false;
|
|
882
|
+
return surfaceAnswers(surface, SceneChannel.Shot);
|
|
883
|
+
}
|
|
884
|
+
function blocksHull(actor, surface) {
|
|
885
|
+
if (actor.tags.includes(SceneTag.Movable) || surface.tags?.includes(SceneTag.Movable)) return false;
|
|
886
|
+
return surfaceAnswers(surface, SceneChannel.Move);
|
|
887
|
+
}
|
|
888
|
+
function rasterScene(l, layer, half, actors) {
|
|
889
|
+
for (const actor of actors) {
|
|
890
|
+
for (const surface of actor.surfaces) {
|
|
891
|
+
if (blocksHull(actor, surface)) rasterSurface(l, layer, half, surface);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function surfaceBounds(surface) {
|
|
896
|
+
const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
897
|
+
const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
898
|
+
const add = (x, y, z) => {
|
|
899
|
+
if (x < lo[0]) lo[0] = x;
|
|
900
|
+
if (y < lo[1]) lo[1] = y;
|
|
901
|
+
if (z < lo[2]) lo[2] = z;
|
|
902
|
+
if (x > hi[0]) hi[0] = x;
|
|
903
|
+
if (y > hi[1]) hi[1] = y;
|
|
904
|
+
if (z > hi[2]) hi[2] = z;
|
|
905
|
+
};
|
|
906
|
+
switch (surface.kind) {
|
|
907
|
+
case "box": {
|
|
908
|
+
const corners = boxCorners(surface);
|
|
909
|
+
for (let i = 0; i < 24; i += 3) add(corners[i], corners[i + 1], corners[i + 2]);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
case "hull":
|
|
913
|
+
for (const p of surface.points) add(p[0], p[1], p[2]);
|
|
914
|
+
break;
|
|
915
|
+
case "sphere": {
|
|
916
|
+
const r = surface.radius;
|
|
917
|
+
add(surface.center[0] - r, surface.center[1] - r, surface.center[2] - r);
|
|
918
|
+
add(surface.center[0] + r, surface.center[1] + r, surface.center[2] + r);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case "capsule": {
|
|
922
|
+
const r = surface.radius;
|
|
923
|
+
for (const p of [surface.a, surface.b]) {
|
|
924
|
+
add(p[0] - r, p[1] - r, p[2] - r);
|
|
925
|
+
add(p[0] + r, p[1] + r, p[2] + r);
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
case "mesh": {
|
|
930
|
+
const v = surface.vertices;
|
|
931
|
+
for (const i of surface.indices) add(v[i * 3], v[i * 3 + 1], v[i * 3 + 2]);
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
case "heightfield": {
|
|
935
|
+
const { origin, cell, cols, rows, heights } = surface;
|
|
936
|
+
for (const z of heights) {
|
|
937
|
+
if (Number.isNaN(z)) continue;
|
|
938
|
+
add(origin[0], origin[1], z);
|
|
939
|
+
add(origin[0] + (cols - 1) * cell[0], origin[1] + (rows - 1) * cell[1], z);
|
|
940
|
+
}
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return { min: [lo[0], lo[1], lo[2]], max: [hi[0], hi[1], hi[2]] };
|
|
945
|
+
}
|
|
946
|
+
function visitTops(l, half, actors, visit) {
|
|
947
|
+
const onColumn = (i, j, _zMin, zMax) => visit(j * l.nx + i, zMax);
|
|
948
|
+
const triangles = (points, faces) => convexTriangles(points, faces, (ax, ay, az, bx, by, bz, cx, cy, cz) => triangleColumns(l, half, ax, ay, az, bx, by, bz, cx, cy, cz, onColumn));
|
|
949
|
+
for (const actor of actors) {
|
|
950
|
+
for (const surface of actor.surfaces) {
|
|
951
|
+
if (!blocksHull(actor, surface)) continue;
|
|
952
|
+
const exact = exactBoxBounds(surface);
|
|
953
|
+
if (exact !== null) {
|
|
954
|
+
const i0 = Math.max(0, cellFloor((exact.min[0] - half - l.x0) / l.h - 0.5) + 1);
|
|
955
|
+
const i1 = Math.min(l.nx - 1, cellCeil((exact.max[0] + half - l.x0) / l.h - 0.5) - 1);
|
|
956
|
+
const j0 = Math.max(0, cellFloor((exact.min[1] - half - l.y0) / l.h - 0.5) + 1);
|
|
957
|
+
const j1 = Math.min(l.ny - 1, cellCeil((exact.max[1] + half - l.y0) / l.h - 0.5) - 1);
|
|
958
|
+
for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) visit(j * l.nx + i, exact.max[2]);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (surface.kind === "box") triangles(boxCorners(surface), BOX_FACES);
|
|
962
|
+
else if (surface.kind === "hull") {
|
|
963
|
+
const points = surface.points.flat();
|
|
964
|
+
triangles(points, convexHullFaces(points));
|
|
965
|
+
} else if (surface.kind === "sphere") {
|
|
966
|
+
const points = spherePoints(surface.center, surface.radius);
|
|
967
|
+
triangles(points, convexHullFaces(points));
|
|
968
|
+
} else if (surface.kind === "capsule") {
|
|
969
|
+
const points = [...spherePoints(surface.a, surface.radius), ...spherePoints(surface.b, surface.radius)];
|
|
970
|
+
triangles(points, convexHullFaces(points));
|
|
971
|
+
} else if (surface.kind === "mesh") {
|
|
972
|
+
const { vertices: v, indices: t } = surface;
|
|
973
|
+
for (let k = 0; k + 2 < t.length; k += 3) {
|
|
974
|
+
const a = t[k] * 3;
|
|
975
|
+
const b = t[k + 1] * 3;
|
|
976
|
+
const c = t[k + 2] * 3;
|
|
977
|
+
triangleColumns(l, half, v[a], v[a + 1], v[a + 2], v[b], v[b + 1], v[b + 2], v[c], v[c + 1], v[c + 2], onColumn);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
const { origin, cell, cols, rows, heights } = surface;
|
|
981
|
+
for (let r = 0; r + 1 < rows; r++) {
|
|
982
|
+
for (let c = 0; c + 1 < cols; c++) {
|
|
983
|
+
const za = heights[r * cols + c];
|
|
984
|
+
const zb = heights[r * cols + c + 1];
|
|
985
|
+
const zc = heights[(r + 1) * cols + c];
|
|
986
|
+
const zd = heights[(r + 1) * cols + c + 1];
|
|
987
|
+
const x0 = origin[0] + c * cell[0];
|
|
988
|
+
const y0 = origin[1] + r * cell[1];
|
|
989
|
+
if (!Number.isNaN(za) && !Number.isNaN(zb) && !Number.isNaN(zc)) triangleColumns(l, half, x0, y0, za, x0 + cell[0], y0, zb, x0, y0 + cell[1], zc, onColumn);
|
|
990
|
+
if (!Number.isNaN(zb) && !Number.isNaN(zc) && !Number.isNaN(zd)) triangleColumns(l, half, x0 + cell[0], y0, zb, x0, y0 + cell[1], zc, x0 + cell[0], y0 + cell[1], zd, onColumn);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/world-fit/cover.ts
|
|
999
|
+
var SolidStage = {
|
|
1000
|
+
Exact: "exact",
|
|
1001
|
+
Ramp: "ramp",
|
|
1002
|
+
Tile: "tile",
|
|
1003
|
+
Cover: "cover",
|
|
1004
|
+
Prop: "prop"
|
|
1005
|
+
};
|
|
1006
|
+
var MIN_THICKNESS_CELLS = 1 / 8;
|
|
1007
|
+
var GAP_MARGIN_CELLS = 1 / 8;
|
|
1008
|
+
var SEAM_OVERLAP_CELLS = GAP_MARGIN_CELLS * 1.25;
|
|
1009
|
+
var MAX_GROW_CELLS = 512;
|
|
1010
|
+
var MIN_TILE_FOOTPRINTS = 1;
|
|
1011
|
+
var MIN_PIECE_SHARE = 1 / 4;
|
|
1012
|
+
var MIN_RAMP_FOOTPRINTS = 3;
|
|
1013
|
+
var RAMP_PASSES = 4;
|
|
1014
|
+
var RAMP_SEED_STRIDE_FOOTPRINTS = 1;
|
|
1015
|
+
var WINDOW_TRIES = [0, 0.5, 1];
|
|
1016
|
+
var TILE_DEPTH_RANGES = 1;
|
|
1017
|
+
var TILE_DEPTH_CELLS = 2;
|
|
1018
|
+
var f = Math.fround;
|
|
1019
|
+
var ActorIndex = class {
|
|
1020
|
+
bins = /* @__PURE__ */ new Map();
|
|
1021
|
+
binSize;
|
|
1022
|
+
constructor(actors, binSize) {
|
|
1023
|
+
this.binSize = binSize;
|
|
1024
|
+
for (const [actor, a] of actors.entries()) {
|
|
1025
|
+
for (const surface of a.surfaces) {
|
|
1026
|
+
if (!blocksHull(a, surface)) continue;
|
|
1027
|
+
const box = surfaceBounds(surface);
|
|
1028
|
+
const entry = { actor, box };
|
|
1029
|
+
this.eachBin(box.min[0], box.min[1], box.max[0], box.max[1], (key) => {
|
|
1030
|
+
const list = this.bins.get(key);
|
|
1031
|
+
if (list === void 0) this.bins.set(key, [entry]);
|
|
1032
|
+
else list.push(entry);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
eachBin(x0, y0, x1, y1, visit) {
|
|
1038
|
+
const a0 = Math.floor(x0 / this.binSize);
|
|
1039
|
+
const a1 = Math.floor(x1 / this.binSize);
|
|
1040
|
+
const b0 = Math.floor(y0 / this.binSize);
|
|
1041
|
+
const b1 = Math.floor(y1 / this.binSize);
|
|
1042
|
+
if ((a1 - a0 + 1) * (b1 - b0 + 1) > 4096) {
|
|
1043
|
+
visit(Number.MAX_SAFE_INTEGER);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
for (let a = a0; a <= a1; a++) for (let b = b0; b <= b1; b++) visit(a * 1000003 + b);
|
|
1047
|
+
}
|
|
1048
|
+
owner(min, max) {
|
|
1049
|
+
let best = -1;
|
|
1050
|
+
let bestScore = -1;
|
|
1051
|
+
const score = (entry) => {
|
|
1052
|
+
let volume = 1;
|
|
1053
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1054
|
+
const overlap = Math.min(max[axis], entry.box.max[axis]) - Math.max(min[axis], entry.box.min[axis]);
|
|
1055
|
+
if (overlap < 0) return;
|
|
1056
|
+
volume *= overlap + 1;
|
|
1057
|
+
}
|
|
1058
|
+
if (volume > bestScore) {
|
|
1059
|
+
bestScore = volume;
|
|
1060
|
+
best = entry.actor;
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
this.eachBin(min[0], min[1], max[0], max[1], (key) => {
|
|
1064
|
+
for (const entry of this.bins.get(key) ?? []) score(entry);
|
|
1065
|
+
});
|
|
1066
|
+
for (const entry of this.bins.get(Number.MAX_SAFE_INTEGER) ?? []) score(entry);
|
|
1067
|
+
return best;
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
function footRange(l, radius, min, max, origin, count) {
|
|
1071
|
+
return [Math.max(0, cellFloor((min - radius - origin) / l.h - 0.5) + 1), Math.min(count - 1, cellCeil((max + radius - origin) / l.h - 0.5) - 1)];
|
|
1072
|
+
}
|
|
1073
|
+
function markBox(input, covered, box, zLow, zHigh) {
|
|
1074
|
+
const { l, radius } = input;
|
|
1075
|
+
const [i0, i1] = footRange(l, radius, box[0], box[3], l.x0, l.nx);
|
|
1076
|
+
const [j0, j1] = footRange(l, radius, box[1], box[4], l.y0, l.ny);
|
|
1077
|
+
for (let j = j0; j <= j1; j++) {
|
|
1078
|
+
for (let i = i0; i <= i1; i++) setRun(l, covered, (j * l.nx + i) * l.w, zLow, zHigh);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function hullTouches(input, box) {
|
|
1082
|
+
const { l, radius, body, touch } = input;
|
|
1083
|
+
const [i0, i1] = footRange(l, radius + l.h, box.min[0], box.max[0], l.x0, l.nx);
|
|
1084
|
+
const [j0, j1] = footRange(l, radius + l.h, box.min[1], box.max[1], l.y0, l.ny);
|
|
1085
|
+
const a = cellFloor((box.min[2] - l.z0) / l.h) - 1;
|
|
1086
|
+
const b = cellCeil((box.max[2] - l.z0) / l.h);
|
|
1087
|
+
for (let j = j0; j <= j1; j++) {
|
|
1088
|
+
for (let i = i0; i <= i1; i++) {
|
|
1089
|
+
if (anyInRun(l, body, (j * l.nx + i) * l.w, a, b) || anyInRun(l, touch, (j * l.nx + i) * l.w, a, b)) return true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
function solidOfExact(box, h) {
|
|
1095
|
+
const axis = (lo, hi) => {
|
|
1096
|
+
const a = f(lo);
|
|
1097
|
+
const b = f(hi);
|
|
1098
|
+
if (a < b) return [a, b];
|
|
1099
|
+
const pad = h * MIN_THICKNESS_CELLS;
|
|
1100
|
+
return [f(lo - pad / 2), f(hi + pad / 2)];
|
|
1101
|
+
};
|
|
1102
|
+
const [x0, x1] = axis(box.min[0], box.max[0]);
|
|
1103
|
+
const [y0, y1] = axis(box.min[1], box.max[1]);
|
|
1104
|
+
const [z0, z1] = axis(box.min[2], box.max[2]);
|
|
1105
|
+
return [x0, y0, z0, x1, y1, z1];
|
|
1106
|
+
}
|
|
1107
|
+
function placeExactBoxes(input, knobs, placed, covered, exactSolid) {
|
|
1108
|
+
const { actors, l } = input;
|
|
1109
|
+
for (const [index, actor] of actors.entries()) {
|
|
1110
|
+
const bounded = knobs.boundedActors.has(index);
|
|
1111
|
+
for (const surface of actor.surfaces) {
|
|
1112
|
+
if (!blocksHull(actor, surface)) continue;
|
|
1113
|
+
const exact = bounded ? surfaceBounds(surface) : exactBoxBounds(surface);
|
|
1114
|
+
if (exact === null || !hullTouches(input, exact)) continue;
|
|
1115
|
+
const box = solidOfExact(exact, l.h);
|
|
1116
|
+
placed.boxes.push(box);
|
|
1117
|
+
placed.boxInfo.push({ stage: bounded ? SolidStage.Prop : SolidStage.Exact, actor: index });
|
|
1118
|
+
markBox(input, covered, box, cellFloor((box[2] - l.z0) / l.h), cellCeil((box[5] - l.z0) / l.h) - 1);
|
|
1119
|
+
rasterAabb(l, exactSolid, l.h / 2, exact);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function readFloors(input) {
|
|
1124
|
+
const { l, solid, envelope, actors } = input;
|
|
1125
|
+
const { nx, ny, w } = l;
|
|
1126
|
+
const columns = nx * ny;
|
|
1127
|
+
const offsets = new Int32Array(columns + 1);
|
|
1128
|
+
let total = 0;
|
|
1129
|
+
const visit = (write) => {
|
|
1130
|
+
for (let col = 0; col < columns; col++) {
|
|
1131
|
+
const o = col * w;
|
|
1132
|
+
if (write === null) offsets[col] = total;
|
|
1133
|
+
for (let word = 0; word < w; word++) {
|
|
1134
|
+
const s = solid[o + word];
|
|
1135
|
+
if (s === 0) continue;
|
|
1136
|
+
const above = s >>> 1 | (word + 1 < w ? solid[o + word + 1] << 31 : 0);
|
|
1137
|
+
const open = envelope[o + word] >>> 1 | (word + 1 < w ? envelope[o + word + 1] << 31 : 0);
|
|
1138
|
+
let tops2 = (s & ~above & open) >>> 0;
|
|
1139
|
+
while (tops2 !== 0) {
|
|
1140
|
+
const bit = 31 - Math.clz32(tops2 & -tops2);
|
|
1141
|
+
tops2 = (tops2 & tops2 - 1) >>> 0;
|
|
1142
|
+
if (write !== null) write[total] = word * 32 + bit;
|
|
1143
|
+
total += 1;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (write === null) offsets[columns] = total;
|
|
1148
|
+
};
|
|
1149
|
+
visit(null);
|
|
1150
|
+
const cells = new Int16Array(total);
|
|
1151
|
+
total = 0;
|
|
1152
|
+
visit(cells);
|
|
1153
|
+
const tops = new Float32Array(total);
|
|
1154
|
+
for (let at = 0; at < total; at++) tops[at] = plane(l, cells[at] + 1);
|
|
1155
|
+
const seen = new Uint8Array(total);
|
|
1156
|
+
visitTops(l, l.h / 2, actors, (col, top) => {
|
|
1157
|
+
const cell = cellCeil((top - l.z0) / l.h) - 1;
|
|
1158
|
+
for (let at = offsets[col]; at < offsets[col + 1]; at++) {
|
|
1159
|
+
if (cells[at] !== cell) continue;
|
|
1160
|
+
if (seen[at] === 0 || top > tops[at]) tops[at] = top;
|
|
1161
|
+
seen[at] = 1;
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
return { offsets, cells, tops };
|
|
1165
|
+
}
|
|
1166
|
+
function floorsLeft(l, base, exactSolid) {
|
|
1167
|
+
const cells = base.cells.slice();
|
|
1168
|
+
for (let col = 0; col < l.nx * l.ny; col++) {
|
|
1169
|
+
for (let at = base.offsets[col]; at < base.offsets[col + 1]; at++) {
|
|
1170
|
+
if (testBit(exactSolid, col * l.w, cells[at])) cells[at] = -1;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return { offsets: base.offsets, tops: base.tops, cells };
|
|
1174
|
+
}
|
|
1175
|
+
function floorBottom(input, i0, i1, j0, j1, lowest, depth) {
|
|
1176
|
+
const { l, envelope } = input;
|
|
1177
|
+
let bottom = Math.max(0, lowest - depth);
|
|
1178
|
+
for (let j = j0; j <= j1; j++) {
|
|
1179
|
+
for (let i = i0; i <= i1; i++) {
|
|
1180
|
+
const hit = lastSet(l, envelope, (j * l.nx + i) * l.w, bottom, lowest - 1);
|
|
1181
|
+
if (hit >= bottom) bottom = hit + 1;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return bottom;
|
|
1185
|
+
}
|
|
1186
|
+
function floorIn(image, col, lo, hi) {
|
|
1187
|
+
for (let at = image.offsets[col]; at < image.offsets[col + 1]; at++) {
|
|
1188
|
+
const z = image.cells[at];
|
|
1189
|
+
if (z >= lo && z <= hi) return z;
|
|
1190
|
+
}
|
|
1191
|
+
return -1;
|
|
1192
|
+
}
|
|
1193
|
+
function consume(image, col, lo, hi) {
|
|
1194
|
+
for (let at = image.offsets[col]; at < image.offsets[col + 1]; at++) {
|
|
1195
|
+
const z = image.cells[at];
|
|
1196
|
+
if (z >= lo && z <= hi) image.cells[at] = -1;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function zoneColumnsOf(l, zone) {
|
|
1200
|
+
if (zone === null) return null;
|
|
1201
|
+
const out = new Uint8Array(l.nx * l.ny);
|
|
1202
|
+
for (let col = 0; col < out.length; col++) {
|
|
1203
|
+
for (let word = 0; word < l.w; word++) {
|
|
1204
|
+
if (zone[col * l.w + word] !== 0) {
|
|
1205
|
+
out[col] = 1;
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
var MAX_BLOCK_CELLS = 256;
|
|
1213
|
+
function cutBlock(input, heights, zoneColumns, bx, by, side, floorRange, out) {
|
|
1214
|
+
const { l, envelope } = input;
|
|
1215
|
+
const { nx, ny, w } = l;
|
|
1216
|
+
const levels = Math.round(Math.log2(side));
|
|
1217
|
+
let blockLo = Number.MAX_SAFE_INTEGER;
|
|
1218
|
+
let blockHi = -1;
|
|
1219
|
+
for (let y = by; y < Math.min(ny, by + side); y++) {
|
|
1220
|
+
for (let x = bx; x < Math.min(nx, bx + side); x++) {
|
|
1221
|
+
const z = heights[y * nx + x];
|
|
1222
|
+
if (z < 0) continue;
|
|
1223
|
+
if (z < blockLo) blockLo = z;
|
|
1224
|
+
if (z > blockHi) blockHi = z;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (blockHi < 0) return;
|
|
1228
|
+
const offsets = [];
|
|
1229
|
+
let states = 0;
|
|
1230
|
+
const key = (a, b) => a * (levels + 1) + b;
|
|
1231
|
+
for (let a = 0; a <= levels; a++) {
|
|
1232
|
+
for (let b = 0; b <= levels; b++) {
|
|
1233
|
+
offsets[key(a, b)] = states;
|
|
1234
|
+
states += (side >> a) * (side >> b);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const lo = new Int16Array(states);
|
|
1238
|
+
const hi = new Int16Array(states);
|
|
1239
|
+
const flags = new Uint8Array(states);
|
|
1240
|
+
const cost = new Int32Array(states);
|
|
1241
|
+
const BLOCKS = 1;
|
|
1242
|
+
const ZONED = 2;
|
|
1243
|
+
const SPLIT_X = 4;
|
|
1244
|
+
const SPLIT_Y = 8;
|
|
1245
|
+
const at = (a, b, x, y) => offsets[key(a, b)] + (y >> b) * (side >> a) + (x >> a);
|
|
1246
|
+
for (let dy = 0; dy < side; dy++) {
|
|
1247
|
+
for (let dx = 0; dx < side; dx++) {
|
|
1248
|
+
const i = at(0, 0, dx, dy);
|
|
1249
|
+
const x = bx + dx;
|
|
1250
|
+
const y = by + dy;
|
|
1251
|
+
const inside = x < nx && y < ny;
|
|
1252
|
+
const col = y * nx + x;
|
|
1253
|
+
const z = inside ? heights[col] : -1;
|
|
1254
|
+
lo[i] = z >= 0 ? z : 32767;
|
|
1255
|
+
hi[i] = z;
|
|
1256
|
+
if (z >= 0) {
|
|
1257
|
+
cost[i] = 1;
|
|
1258
|
+
if (zoneColumns !== null && zoneColumns[col] === 1) flags[i] = ZONED;
|
|
1259
|
+
} else if (inside && anyInRun(l, envelope, col * w, blockLo, blockHi + 1)) {
|
|
1260
|
+
flags[i] = BLOCKS;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
for (let sum = 1; sum <= 2 * levels; sum++) {
|
|
1265
|
+
for (let a = Math.max(0, sum - levels); a <= Math.min(levels, sum); a++) {
|
|
1266
|
+
const b = sum - a;
|
|
1267
|
+
const stepX = 1 << a;
|
|
1268
|
+
const stepY = 1 << b;
|
|
1269
|
+
for (let y = 0; y < side; y += stepY) {
|
|
1270
|
+
for (let x = 0; x < side; x += stepX) {
|
|
1271
|
+
const i = at(a, b, x, y);
|
|
1272
|
+
const p = a > 0 ? at(a - 1, b, x, y) : at(a, b - 1, x, y);
|
|
1273
|
+
const q = a > 0 ? at(a - 1, b, x + (stepX >> 1), y) : at(a, b - 1, x, y + (stepY >> 1));
|
|
1274
|
+
const low = Math.min(lo[p], lo[q]);
|
|
1275
|
+
const high = Math.max(hi[p], hi[q]);
|
|
1276
|
+
const f2 = (flags[p] | flags[q]) & (BLOCKS | ZONED);
|
|
1277
|
+
lo[i] = low;
|
|
1278
|
+
hi[i] = high;
|
|
1279
|
+
const range = f2 & ZONED ? 0 : floorRange;
|
|
1280
|
+
if (!(f2 & BLOCKS) && (high < 0 || high - low <= range)) {
|
|
1281
|
+
flags[i] = f2;
|
|
1282
|
+
cost[i] = high < 0 ? 0 : 1;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
let best = Number.POSITIVE_INFINITY;
|
|
1286
|
+
let split = 0;
|
|
1287
|
+
if (a > 0) {
|
|
1288
|
+
best = cost[at(a - 1, b, x, y)] + cost[at(a - 1, b, x + (stepX >> 1), y)];
|
|
1289
|
+
split = SPLIT_X;
|
|
1290
|
+
}
|
|
1291
|
+
if (b > 0) {
|
|
1292
|
+
const viaY = cost[at(a, b - 1, x, y)] + cost[at(a, b - 1, x, y + (stepY >> 1))];
|
|
1293
|
+
if (viaY < best) {
|
|
1294
|
+
best = viaY;
|
|
1295
|
+
split = SPLIT_Y;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
flags[i] = f2 | split;
|
|
1299
|
+
cost[i] = best;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const stack = [[levels, levels, 0, 0]];
|
|
1305
|
+
while (stack.length > 0) {
|
|
1306
|
+
const [a, b, x, y] = stack.pop();
|
|
1307
|
+
const i = at(a, b, x, y);
|
|
1308
|
+
if (cost[i] === 0) continue;
|
|
1309
|
+
const f2 = flags[i];
|
|
1310
|
+
if (f2 & SPLIT_X) {
|
|
1311
|
+
stack.push([a - 1, b, x + (1 << a >> 1), y], [a - 1, b, x, y]);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (f2 & SPLIT_Y) {
|
|
1315
|
+
stack.push([a, b - 1, x, y + (1 << b >> 1)], [a, b - 1, x, y]);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
let x0 = Number.MAX_SAFE_INTEGER;
|
|
1319
|
+
let x1 = -1;
|
|
1320
|
+
let y0 = Number.MAX_SAFE_INTEGER;
|
|
1321
|
+
let y1 = -1;
|
|
1322
|
+
for (let v = by + y; v < Math.min(ny, by + y + (1 << b)); v++) {
|
|
1323
|
+
for (let u = bx + x; u < Math.min(nx, bx + x + (1 << a)); u++) {
|
|
1324
|
+
if (heights[v * nx + u] < 0) continue;
|
|
1325
|
+
if (u < x0) x0 = u;
|
|
1326
|
+
if (u > x1) x1 = u;
|
|
1327
|
+
if (v < y0) y0 = v;
|
|
1328
|
+
if (v > y1) y1 = v;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
out.push({ x0, x1, y0, y1, lo: lo[i], hi: hi[i], range: f2 & ZONED ? 0 : floorRange });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function cutFloor(input, heights, zoneColumns, bounds, floorRange) {
|
|
1335
|
+
const size = Math.max(bounds.x1 - bounds.x0, bounds.y1 - bounds.y0) + 1;
|
|
1336
|
+
const side = Math.min(MAX_BLOCK_CELLS, 2 ** Math.ceil(Math.log2(size)));
|
|
1337
|
+
const out = [];
|
|
1338
|
+
for (let by = bounds.y0; by <= bounds.y1; by += side) {
|
|
1339
|
+
for (let bx = bounds.x0; bx <= bounds.x1; bx += side) cutBlock(input, heights, zoneColumns, bx, by, side, floorRange, out);
|
|
1340
|
+
}
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
function trimOffHulls(input, t) {
|
|
1344
|
+
const { l, radius, body } = input;
|
|
1345
|
+
const { nx, w, h } = l;
|
|
1346
|
+
const below = t.lo - t.range - 1;
|
|
1347
|
+
if (below < 0) return true;
|
|
1348
|
+
const edge = radius / h;
|
|
1349
|
+
for (; ; ) {
|
|
1350
|
+
const [i0, i1] = footRange(l, radius, l.x0 + t.x0 * h, l.x0 + (t.x1 + 1) * h, l.x0, l.nx);
|
|
1351
|
+
const [j0, j1] = footRange(l, radius, l.y0 + t.y0 * h, l.y0 + (t.y1 + 1) * h, l.y0, l.ny);
|
|
1352
|
+
let hitI = -1;
|
|
1353
|
+
let hitJ = -1;
|
|
1354
|
+
for (let j = j0; j <= j1 && hitI < 0; j++) {
|
|
1355
|
+
for (let i = i0; i <= i1; i++) {
|
|
1356
|
+
const o = (j * nx + i) * w;
|
|
1357
|
+
if (testBit(body, o, below) && testBit(body, o, t.lo)) {
|
|
1358
|
+
hitI = i;
|
|
1359
|
+
hitJ = j;
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (hitI < 0) return true;
|
|
1365
|
+
const left = Math.floor(hitI + 0.5 - edge - 1);
|
|
1366
|
+
const right = Math.ceil(hitI + 0.5 + edge);
|
|
1367
|
+
const near = Math.floor(hitJ + 0.5 - edge - 1);
|
|
1368
|
+
const far = Math.ceil(hitJ + 0.5 + edge);
|
|
1369
|
+
const width = t.x1 - t.x0 + 1;
|
|
1370
|
+
const depth = t.y1 - t.y0 + 1;
|
|
1371
|
+
const options = [
|
|
1372
|
+
[(left - t.x0 + 1) * depth, () => {
|
|
1373
|
+
t.x1 = left;
|
|
1374
|
+
}],
|
|
1375
|
+
[(t.x1 - right + 1) * depth, () => {
|
|
1376
|
+
t.x0 = right;
|
|
1377
|
+
}],
|
|
1378
|
+
[(near - t.y0 + 1) * width, () => {
|
|
1379
|
+
t.y1 = near;
|
|
1380
|
+
}],
|
|
1381
|
+
[(t.y1 - far + 1) * width, () => {
|
|
1382
|
+
t.y0 = far;
|
|
1383
|
+
}]
|
|
1384
|
+
];
|
|
1385
|
+
let best = options[0];
|
|
1386
|
+
for (const option of options) if (option[0] > best[0]) best = option;
|
|
1387
|
+
if (best[0] <= 0) return false;
|
|
1388
|
+
best[1]();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function mergeTiles(tiles) {
|
|
1392
|
+
let current = tiles;
|
|
1393
|
+
for (; ; ) {
|
|
1394
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
1395
|
+
const gone = /* @__PURE__ */ new Set();
|
|
1396
|
+
let merged = false;
|
|
1397
|
+
const tryJoin = (key, edgeKey, t, join3) => {
|
|
1398
|
+
const other = byEdge.get(edgeKey);
|
|
1399
|
+
if (other !== void 0 && !gone.has(other)) {
|
|
1400
|
+
const lo = Math.min(other.lo, t.lo);
|
|
1401
|
+
const hi = Math.max(other.hi, t.hi);
|
|
1402
|
+
if (hi - lo <= Math.min(other.range, t.range)) {
|
|
1403
|
+
join3(other);
|
|
1404
|
+
other.lo = lo;
|
|
1405
|
+
other.hi = hi;
|
|
1406
|
+
gone.add(t);
|
|
1407
|
+
merged = true;
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
byEdge.set(key, t);
|
|
1412
|
+
return false;
|
|
1413
|
+
};
|
|
1414
|
+
const sorted = [...current].sort((a, b) => a.y0 - b.y0 || a.x0 - b.x0);
|
|
1415
|
+
for (const t of sorted) {
|
|
1416
|
+
if (tryJoin(`x${t.y0},${t.y1},${t.x1 + 1}`, `x${t.y0},${t.y1},${t.x0}`, t, (o) => {
|
|
1417
|
+
o.x1 = t.x1;
|
|
1418
|
+
byEdge.set(`x${o.y0},${o.y1},${o.x1 + 1}`, o);
|
|
1419
|
+
})) continue;
|
|
1420
|
+
}
|
|
1421
|
+
byEdge.clear();
|
|
1422
|
+
const remaining = sorted.filter((t) => !gone.has(t));
|
|
1423
|
+
gone.clear();
|
|
1424
|
+
for (const t of remaining) {
|
|
1425
|
+
tryJoin(`y${t.x0},${t.x1},${t.y1 + 1}`, `y${t.x0},${t.x1},${t.y0}`, t, (o) => {
|
|
1426
|
+
o.y1 = t.y1;
|
|
1427
|
+
byEdge.set(`y${o.x0},${o.x1},${o.y1 + 1}`, o);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
current = remaining.filter((t) => !gone.has(t));
|
|
1431
|
+
if (!merged) return current;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function placeTiles(input, knobs, image, footprintCells, stepCells, index, placed, covered) {
|
|
1435
|
+
const { l } = input;
|
|
1436
|
+
const { nx, ny, h } = l;
|
|
1437
|
+
const zoneColumns = zoneColumnsOf(l, knobs.zone);
|
|
1438
|
+
const minArea = MIN_TILE_FOOTPRINTS * footprintCells * footprintCells;
|
|
1439
|
+
const minTile = MIN_PIECE_SHARE * footprintCells * footprintCells;
|
|
1440
|
+
const heights = new Int16Array(nx * ny).fill(-1);
|
|
1441
|
+
const surface = new Float32Array(nx * ny);
|
|
1442
|
+
const stack = [];
|
|
1443
|
+
for (let start = 0; start < nx * ny; start++) {
|
|
1444
|
+
for (let at = image.offsets[start]; at < image.offsets[start + 1]; at++) {
|
|
1445
|
+
const z0 = image.cells[at];
|
|
1446
|
+
if (z0 < 0) continue;
|
|
1447
|
+
const members = [];
|
|
1448
|
+
const bounds = { x0: nx, x1: -1, y0: ny, y1: -1 };
|
|
1449
|
+
image.cells[at] = -1;
|
|
1450
|
+
heights[start] = z0;
|
|
1451
|
+
surface[start] = image.tops[at];
|
|
1452
|
+
stack.push(start);
|
|
1453
|
+
while (stack.length > 0) {
|
|
1454
|
+
const col = stack.pop();
|
|
1455
|
+
members.push(col);
|
|
1456
|
+
const y = Math.floor(col / nx);
|
|
1457
|
+
const x = col - y * nx;
|
|
1458
|
+
if (x < bounds.x0) bounds.x0 = x;
|
|
1459
|
+
if (x > bounds.x1) bounds.x1 = x;
|
|
1460
|
+
if (y < bounds.y0) bounds.y0 = y;
|
|
1461
|
+
if (y > bounds.y1) bounds.y1 = y;
|
|
1462
|
+
const z = heights[col];
|
|
1463
|
+
for (const next of [x > 0 ? col - 1 : -1, x + 1 < nx ? col + 1 : -1, y > 0 ? col - nx : -1, y + 1 < ny ? col + nx : -1]) {
|
|
1464
|
+
if (next < 0 || heights[next] >= 0) continue;
|
|
1465
|
+
for (let c = image.offsets[next]; c < image.offsets[next + 1]; c++) {
|
|
1466
|
+
const got = image.cells[c];
|
|
1467
|
+
if (got >= 0 && Math.abs(got - z) <= stepCells) {
|
|
1468
|
+
image.cells[c] = -1;
|
|
1469
|
+
heights[next] = got;
|
|
1470
|
+
surface[next] = image.tops[c];
|
|
1471
|
+
stack.push(next);
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (members.length >= minArea) {
|
|
1478
|
+
for (const t of mergeTiles(cutFloor(input, heights, zoneColumns, bounds, knobs.floorRange))) {
|
|
1479
|
+
if ((t.x1 - t.x0 + 1) * (t.y1 - t.y0 + 1) < minTile) continue;
|
|
1480
|
+
if (!trimOffHulls(input, t)) continue;
|
|
1481
|
+
let lowest = Number.POSITIVE_INFINITY;
|
|
1482
|
+
let highest = Number.NEGATIVE_INFINITY;
|
|
1483
|
+
for (let y = t.y0; y <= t.y1; y++) {
|
|
1484
|
+
for (let x = t.x0; x <= t.x1; x++) {
|
|
1485
|
+
const col = y * nx + x;
|
|
1486
|
+
if (heights[col] < 0) continue;
|
|
1487
|
+
lowest = Math.min(lowest, surface[col]);
|
|
1488
|
+
highest = Math.max(highest, surface[col]);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (highest === Number.NEGATIVE_INFINITY) continue;
|
|
1492
|
+
const topZ = (lowest + highest) / 2;
|
|
1493
|
+
const top = Math.max(t.lo + 1, cellCeil((topZ - l.z0) / l.h));
|
|
1494
|
+
const bottom = floorBottom(input, t.x0, t.x1, t.y0, t.y1, t.lo, TILE_DEPTH_CELLS + TILE_DEPTH_RANGES * t.range);
|
|
1495
|
+
const zLow = Math.min(bottom, top - 1);
|
|
1496
|
+
const box = [f(l.x0 + t.x0 * h), f(l.y0 + t.y0 * h), f(plane(l, zLow)), f(l.x0 + (t.x1 + 1) * h), f(l.y0 + (t.y1 + 1) * h), f(topZ)];
|
|
1497
|
+
placed.boxes.push(box);
|
|
1498
|
+
placed.boxInfo.push({ stage: SolidStage.Tile, actor: index.owner([box[0], box[1], box[2]], [box[3], box[4], box[5]]) });
|
|
1499
|
+
markBox(input, covered, box, zLow, t.hi);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
for (const col of members) heights[col] = -1;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function growRamp(image, l, i, j, z, axis, range, maxSlope) {
|
|
1507
|
+
const { nx, ny } = l;
|
|
1508
|
+
const half = range / 2 + 0.5;
|
|
1509
|
+
const along = axis === 0 ? nx : ny;
|
|
1510
|
+
const across = axis === 0 ? ny : nx;
|
|
1511
|
+
const colAt = (a, b) => axis === 0 ? b * nx + a : a * nx + b;
|
|
1512
|
+
const m = axis === 0 ? i : j;
|
|
1513
|
+
const n = axis === 0 ? j : i;
|
|
1514
|
+
const heights = [];
|
|
1515
|
+
let hi = m;
|
|
1516
|
+
let cur = z;
|
|
1517
|
+
const follow = (a, near) => floorIn(image, colAt(a, n), near - 2, near + 2);
|
|
1518
|
+
while (hi + 1 < along) {
|
|
1519
|
+
const got = follow(hi + 1, cur);
|
|
1520
|
+
if (got < 0) break;
|
|
1521
|
+
heights.push(got);
|
|
1522
|
+
cur = got;
|
|
1523
|
+
hi += 1;
|
|
1524
|
+
}
|
|
1525
|
+
let lo = m;
|
|
1526
|
+
cur = z;
|
|
1527
|
+
const before = [];
|
|
1528
|
+
while (lo - 1 >= 0) {
|
|
1529
|
+
const got = follow(lo - 1, cur);
|
|
1530
|
+
if (got < 0) break;
|
|
1531
|
+
before.push(got);
|
|
1532
|
+
cur = got;
|
|
1533
|
+
lo -= 1;
|
|
1534
|
+
}
|
|
1535
|
+
const line = [...before.reverse(), z, ...heights];
|
|
1536
|
+
let a0 = 0;
|
|
1537
|
+
let a1 = line.length - 1;
|
|
1538
|
+
const fitsLine = (p, q) => {
|
|
1539
|
+
const s = (line[q] - line[p]) / Math.max(1, q - p);
|
|
1540
|
+
if (Math.abs(s) > maxSlope) return false;
|
|
1541
|
+
for (let k = p; k <= q; k++) if (Math.abs(line[k] - (line[p] + s * (k - p))) > half) return false;
|
|
1542
|
+
return true;
|
|
1543
|
+
};
|
|
1544
|
+
const centre = m - lo;
|
|
1545
|
+
while (!fitsLine(a0, a1)) {
|
|
1546
|
+
if (centre - a0 > a1 - centre) a0 += 1;
|
|
1547
|
+
else a1 -= 1;
|
|
1548
|
+
if (a1 - a0 < 4) return null;
|
|
1549
|
+
}
|
|
1550
|
+
const slope = (line[a1] - line[a0]) / Math.max(1, a1 - a0);
|
|
1551
|
+
const first = lo + a0;
|
|
1552
|
+
const last = lo + a1;
|
|
1553
|
+
const startZ = line[a0];
|
|
1554
|
+
const holds = (b) => {
|
|
1555
|
+
for (let a = first; a <= last; a++) {
|
|
1556
|
+
const want = startZ + slope * (a - first);
|
|
1557
|
+
if (floorIn(image, colAt(a, b), Math.floor(want - half), Math.ceil(want + half)) < 0) return false;
|
|
1558
|
+
}
|
|
1559
|
+
return true;
|
|
1560
|
+
};
|
|
1561
|
+
let b0 = n;
|
|
1562
|
+
let b1 = n;
|
|
1563
|
+
while (b1 + 1 < across && holds(b1 + 1)) b1 += 1;
|
|
1564
|
+
while (b0 - 1 >= 0 && holds(b0 - 1)) b0 -= 1;
|
|
1565
|
+
const area = (last - first + 1) * (b1 - b0 + 1);
|
|
1566
|
+
return axis === 0 ? { axis, i0: first, i1: last, j0: b0, j1: b1, start: startZ, slope, area } : { axis, i0: b0, i1: b1, j0: first, j1: last, start: startZ, slope, area };
|
|
1567
|
+
}
|
|
1568
|
+
function placeRamps(input, knobs, image, footprintCells, index, placed, covered) {
|
|
1569
|
+
if (knobs.maxRamps <= 0) return;
|
|
1570
|
+
for (let pass = 0; pass < RAMP_PASSES && placed.ramps.length < knobs.maxRamps; pass++) {
|
|
1571
|
+
const before = placed.ramps.length;
|
|
1572
|
+
rampPass(input, knobs, image, footprintCells, index, placed, covered);
|
|
1573
|
+
if (placed.ramps.length === before) return;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
function rampPass(input, knobs, image, footprintCells, index, placed, covered) {
|
|
1577
|
+
const { l } = input;
|
|
1578
|
+
const { nx, ny, h } = l;
|
|
1579
|
+
const stride = Math.max(1, Math.round(RAMP_SEED_STRIDE_FOOTPRINTS * footprintCells));
|
|
1580
|
+
const minArea = MIN_RAMP_FOOTPRINTS * footprintCells * footprintCells;
|
|
1581
|
+
const zoneColumns = zoneColumnsOf(l, knobs.zone);
|
|
1582
|
+
const candidates = [];
|
|
1583
|
+
const range = Math.max(knobs.floorRange, 1);
|
|
1584
|
+
const maxSlope = 0.95;
|
|
1585
|
+
for (let j = 0; j < ny; j += stride) {
|
|
1586
|
+
for (let i = 0; i < nx; i += stride) {
|
|
1587
|
+
const col = j * nx + i;
|
|
1588
|
+
if (image.offsets[col] === image.offsets[col + 1]) continue;
|
|
1589
|
+
if (zoneColumns !== null && zoneColumns[col] === 1) continue;
|
|
1590
|
+
let z = -1;
|
|
1591
|
+
for (let at = image.offsets[col]; at < image.offsets[col + 1] && z < 0; at++) z = image.cells[at];
|
|
1592
|
+
if (z < 0) continue;
|
|
1593
|
+
for (const axis of [0, 1]) {
|
|
1594
|
+
const c = growRamp(image, l, i, j, z, axis, range, maxSlope);
|
|
1595
|
+
if (c !== null && c.area >= minArea && Math.abs(c.slope) * (axis === 0 ? c.i1 - c.i0 : c.j1 - c.j0) >= 2) candidates.push(c);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
candidates.sort((p, q) => q.area - p.area || p.j0 - q.j0 || p.i0 - q.i0 || p.axis - q.axis);
|
|
1600
|
+
const half = range / 2 + 0.5;
|
|
1601
|
+
for (const c of candidates) {
|
|
1602
|
+
if (placed.ramps.length >= knobs.maxRamps) return;
|
|
1603
|
+
const wantAt = (x, y) => c.start + c.slope * (c.axis === 0 ? x - c.i0 : y - c.j0);
|
|
1604
|
+
let whole = true;
|
|
1605
|
+
let lowest = Number.POSITIVE_INFINITY;
|
|
1606
|
+
let highest = Number.NEGATIVE_INFINITY;
|
|
1607
|
+
for (let y = c.j0; y <= c.j1 && whole; y++) {
|
|
1608
|
+
for (let x = c.i0; x <= c.i1; x++) {
|
|
1609
|
+
const want = wantAt(x, y);
|
|
1610
|
+
const got = floorIn(image, y * nx + x, Math.floor(want - half), Math.ceil(want + half));
|
|
1611
|
+
if (got < 0 || zoneColumns !== null && zoneColumns[y * nx + x] === 1) {
|
|
1612
|
+
whole = false;
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
if (got < lowest) lowest = got;
|
|
1616
|
+
if (got > highest) highest = got;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (!whole) continue;
|
|
1620
|
+
const span = c.axis === 0 ? c.i1 - c.i0 : c.j1 - c.j0;
|
|
1621
|
+
const lo = c.start + 0.5 - 0.5 * c.slope;
|
|
1622
|
+
const hi = c.start + 0.5 + c.slope * (span + 0.5);
|
|
1623
|
+
const low = floorBottom(input, c.i0, c.i1, c.j0, c.j1, Math.min(lowest, Math.floor(Math.min(lo, hi)) - 1), TILE_DEPTH_CELLS + TILE_DEPTH_RANGES * range);
|
|
1624
|
+
const zLo = f(plane(l, 0) + lo * h);
|
|
1625
|
+
const zHi = f(plane(l, 0) + hi * h);
|
|
1626
|
+
const minZ = f(plane(l, low));
|
|
1627
|
+
if (!(minZ <= zLo && minZ <= zHi)) continue;
|
|
1628
|
+
const m0Try = c.axis === 0 ? l.x0 + c.i0 * h : l.y0 + c.j0 * h;
|
|
1629
|
+
const m1Try = c.axis === 0 ? l.x0 + (c.i1 + 1) * h : l.y0 + (c.j1 + 1) * h;
|
|
1630
|
+
const [hullI0, hullI1] = footRange(l, input.radius, l.x0 + c.i0 * h, l.x0 + (c.i1 + 1) * h, l.x0, nx);
|
|
1631
|
+
const [hullJ0, hullJ1] = footRange(l, input.radius, l.y0 + c.j0 * h, l.y0 + (c.j1 + 1) * h, l.y0, ny);
|
|
1632
|
+
let clear = true;
|
|
1633
|
+
for (let y = hullJ0; y <= hullJ1 && clear; y++) {
|
|
1634
|
+
for (let x = hullI0; x <= hullI1; x++) {
|
|
1635
|
+
const centre = c.axis === 0 ? centreX(l, x) : centreY(l, y);
|
|
1636
|
+
const uphill = zHi >= zLo ? Math.min(m1Try, centre + input.radius) : Math.max(m0Try, centre - input.radius);
|
|
1637
|
+
const planeZ = zLo + (zHi - zLo) * (uphill - m0Try) / (m1Try - m0Try);
|
|
1638
|
+
const top = cellCeil((planeZ - l.z0) / h) - 1;
|
|
1639
|
+
const under = top - range - 2;
|
|
1640
|
+
const o = (y * nx + x) * l.w;
|
|
1641
|
+
if (under >= 0 && testBit(input.body, o, under) && testBit(input.body, o, top)) {
|
|
1642
|
+
clear = false;
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (!clear) continue;
|
|
1648
|
+
const ramp = [f(l.x0 + c.i0 * h), f(l.y0 + c.j0 * h), minZ, f(l.x0 + (c.i1 + 1) * h), f(l.y0 + (c.j1 + 1) * h), Math.max(zLo, zHi), c.axis, zLo, zHi];
|
|
1649
|
+
placed.ramps.push(ramp);
|
|
1650
|
+
placed.rampInfo.push({ stage: SolidStage.Ramp, actor: index.owner([ramp[0], ramp[1], ramp[2]], [ramp[3], ramp[4], ramp[5]]) });
|
|
1651
|
+
for (let y = c.j0; y <= c.j1; y++) {
|
|
1652
|
+
for (let x = c.i0; x <= c.i1; x++) {
|
|
1653
|
+
const want = wantAt(x, y);
|
|
1654
|
+
consume(image, y * nx + x, Math.floor(want - half), Math.ceil(want + half));
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const [i0, i1] = footRange(l, input.radius, ramp[0], ramp[3], l.x0, nx);
|
|
1658
|
+
const [j0, j1] = footRange(l, input.radius, ramp[1], ramp[4], l.y0, ny);
|
|
1659
|
+
const m0 = c.axis === 0 ? ramp[0] : ramp[1];
|
|
1660
|
+
const m1 = c.axis === 0 ? ramp[3] : ramp[4];
|
|
1661
|
+
for (let y = j0; y <= j1; y++) {
|
|
1662
|
+
for (let x = i0; x <= i1; x++) {
|
|
1663
|
+
const centre = c.axis === 0 ? centreX(l, x) : centreY(l, y);
|
|
1664
|
+
const uphill = zHi >= zLo ? Math.min(m1, centre + input.radius) : Math.max(m0, centre - input.radius);
|
|
1665
|
+
const planeZ = zLo + (zHi - zLo) * (uphill - m0) / (m1 - m0);
|
|
1666
|
+
setRun(l, covered, (y * nx + x) * l.w, low, cellCeil((planeZ - l.z0) / h) - 1 + Math.ceil(half));
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
function placeFloors(input, knobs, footprintCells, stepCells, index, placed, covered, exactSolid, floors) {
|
|
1672
|
+
const image = floorsLeft(input.l, floors, exactSolid);
|
|
1673
|
+
placeRamps(input, knobs, image, footprintCells, index, placed, covered);
|
|
1674
|
+
placeTiles(input, knobs, image, footprintCells, stepCells, index, placed, covered);
|
|
1675
|
+
}
|
|
1676
|
+
function mustCover(input, knobs, covered) {
|
|
1677
|
+
const { l, body, foot } = input;
|
|
1678
|
+
const { nx, ny, w } = l;
|
|
1679
|
+
const near = knobs.slack > 0 ? dilateXY(l, body, knobs.slack) : body.slice();
|
|
1680
|
+
if (knobs.zone !== null) {
|
|
1681
|
+
for (let at = 0; at < near.length; at++) near[at] = (body[at] | near[at] & ~knobs.zone[at]) >>> 0;
|
|
1682
|
+
}
|
|
1683
|
+
const must = newLayer(l);
|
|
1684
|
+
const seal = (open, skip) => {
|
|
1685
|
+
for (let j = 0; j < ny; j++) {
|
|
1686
|
+
for (let i = 0; i < nx; i++) {
|
|
1687
|
+
const o = (j * nx + i) * w;
|
|
1688
|
+
for (let word = 0; word < w; word++) {
|
|
1689
|
+
let candidate = (foot[o + word] & ~open[o + word] & ~input.leanCells[o + word]) >>> 0;
|
|
1690
|
+
if (skip !== null) candidate = (candidate & ~skip[o + word]) >>> 0;
|
|
1691
|
+
if (candidate === 0) continue;
|
|
1692
|
+
const up = word + 1 < w ? open[o + word + 1] : 0;
|
|
1693
|
+
const down = word > 0 ? open[o + word - 1] : 0;
|
|
1694
|
+
let touching = open[o + word] << 1 | down >>> 31 | open[o + word] >>> 1 | up << 31;
|
|
1695
|
+
if (i > 0) touching |= open[o - w + word];
|
|
1696
|
+
if (i + 1 < nx) touching |= open[o + w + word];
|
|
1697
|
+
if (j > 0) touching |= open[o - nx * w + word];
|
|
1698
|
+
if (j + 1 < ny) touching |= open[o + nx * w + word];
|
|
1699
|
+
must[o + word] = (must[o + word] | candidate & touching) >>> 0;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
seal(near, covered);
|
|
1705
|
+
const sink = Math.ceil((knobs.floorRange + 1) / 2);
|
|
1706
|
+
if (knobs.floorRange > 0) {
|
|
1707
|
+
for (let o = 0; o < must.length; o += w) {
|
|
1708
|
+
for (let word = 0; word < w; word++) {
|
|
1709
|
+
let bits = must[o + word];
|
|
1710
|
+
while (bits !== 0) {
|
|
1711
|
+
const bit = 31 - Math.clz32(bits & -bits);
|
|
1712
|
+
bits = (bits & bits - 1) >>> 0;
|
|
1713
|
+
const z = word * 32 + bit;
|
|
1714
|
+
if (z > 0 && anyInRun(l, covered, o, z - sink, z - 1)) must[o + word] = (must[o + word] & ~(1 << bit)) >>> 0;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
seal(reachDown(l, near, sink + 1), covered);
|
|
1720
|
+
seal(reachUp(l, near, sink + 1), covered);
|
|
1721
|
+
if (knobs.slack === 0) seal(input.touch, covered);
|
|
1722
|
+
const { nz } = l;
|
|
1723
|
+
for (let o = 0; o < must.length; o += w) {
|
|
1724
|
+
let z = firstSet(l, must, o, 0, nz - 1);
|
|
1725
|
+
while (z >= 0) {
|
|
1726
|
+
const clear = firstClear(l, foot, o, z, nz - 1);
|
|
1727
|
+
const top = clear < 0 ? nz - 1 : clear - 1;
|
|
1728
|
+
if (top >= z) setRun(l, must, o, z, top);
|
|
1729
|
+
z = top + 1 < nz ? firstSet(l, must, o, top + 1, nz - 1) : -1;
|
|
1730
|
+
}
|
|
1731
|
+
for (let word = 0; word < w; word++) must[o + word] = (must[o + word] & ~covered[o + word] & ~near[o + word]) >>> 0;
|
|
1732
|
+
}
|
|
1733
|
+
return must;
|
|
1734
|
+
}
|
|
1735
|
+
function rectSolid(input, r) {
|
|
1736
|
+
const { l, radius } = input;
|
|
1737
|
+
const { h } = l;
|
|
1738
|
+
const span = (lo, hi) => {
|
|
1739
|
+
const a = lo + radius - h / 2;
|
|
1740
|
+
const b = hi - radius + h / 2;
|
|
1741
|
+
const hair = h * SEAM_OVERLAP_CELLS;
|
|
1742
|
+
if (b - a >= h * MIN_THICKNESS_CELLS) return [a - hair, b + hair];
|
|
1743
|
+
const mid = (a + b) / 2;
|
|
1744
|
+
return [mid - h * MIN_THICKNESS_CELLS / 2 - hair, mid + h * MIN_THICKNESS_CELLS / 2 + hair];
|
|
1745
|
+
};
|
|
1746
|
+
const [x0, x1] = span(centreX(l, r.x0), centreX(l, r.x1));
|
|
1747
|
+
const [y0, y1] = span(centreY(l, r.y0), centreY(l, r.y1));
|
|
1748
|
+
return [f(x0), f(y0), f(plane(l, r.z0)), f(x1), f(y1), f(plane(l, r.z1 + 1))];
|
|
1749
|
+
}
|
|
1750
|
+
function placeCover(input, must, footprintCells, index, placed, limit, keepTops) {
|
|
1751
|
+
const { l, body, foot } = input;
|
|
1752
|
+
const { nx, ny, nz, w } = l;
|
|
1753
|
+
let count = 0;
|
|
1754
|
+
let stuck = 0;
|
|
1755
|
+
const W = footprintCells;
|
|
1756
|
+
const faceClear = (x0, x1, y0, y1, z0, z1) => {
|
|
1757
|
+
if (x0 < 0 || y0 < 0 || z0 < 0 || x1 >= nx || y1 >= ny || z1 >= nz) return false;
|
|
1758
|
+
for (let y = y0; y <= y1; y++) {
|
|
1759
|
+
for (let x = x0; x <= x1; x++) if (anyInRun(l, body, (y * nx + x) * w, z0, z1)) return false;
|
|
1760
|
+
}
|
|
1761
|
+
return true;
|
|
1762
|
+
};
|
|
1763
|
+
const faceHasMust = (x0, x1, y0, y1, z0, z1) => {
|
|
1764
|
+
for (let y = y0; y <= y1; y++) {
|
|
1765
|
+
for (let x = x0; x <= x1; x++) if (anyInRun(l, must, (y * nx + x) * w, z0, z1)) return true;
|
|
1766
|
+
}
|
|
1767
|
+
return false;
|
|
1768
|
+
};
|
|
1769
|
+
const gain = (r) => {
|
|
1770
|
+
let total = 0;
|
|
1771
|
+
for (let y = r.y0; y <= r.y1; y++) {
|
|
1772
|
+
for (let x = r.x0; x <= r.x1; x++) total += countInRun(l, must, (y * nx + x) * w, r.z0, r.z1);
|
|
1773
|
+
}
|
|
1774
|
+
return total;
|
|
1775
|
+
};
|
|
1776
|
+
const grow = (r, axis) => {
|
|
1777
|
+
let moved = true;
|
|
1778
|
+
while (moved) {
|
|
1779
|
+
moved = false;
|
|
1780
|
+
if (axis === 0) {
|
|
1781
|
+
if (r.x1 - r.x0 < MAX_GROW_CELLS && faceClear(r.x1 + 1, r.x1 + 1, r.y0, r.y1, r.z0, r.z1) && faceHasMust(r.x1 + 1, r.x1 + 1, r.y0, r.y1, r.z0, r.z1) && topOk(r.x1 + 1, r.x1 + 1, r.y0, r.y1, r.z1)) {
|
|
1782
|
+
r.x1 += 1;
|
|
1783
|
+
moved = true;
|
|
1784
|
+
}
|
|
1785
|
+
if (r.x1 - r.x0 < MAX_GROW_CELLS && faceClear(r.x0 - 1, r.x0 - 1, r.y0, r.y1, r.z0, r.z1) && faceHasMust(r.x0 - 1, r.x0 - 1, r.y0, r.y1, r.z0, r.z1) && topOk(r.x0 - 1, r.x0 - 1, r.y0, r.y1, r.z1)) {
|
|
1786
|
+
r.x0 -= 1;
|
|
1787
|
+
moved = true;
|
|
1788
|
+
}
|
|
1789
|
+
} else if (axis === 1) {
|
|
1790
|
+
if (r.y1 - r.y0 < MAX_GROW_CELLS && faceClear(r.x0, r.x1, r.y1 + 1, r.y1 + 1, r.z0, r.z1) && faceHasMust(r.x0, r.x1, r.y1 + 1, r.y1 + 1, r.z0, r.z1) && topOk(r.x0, r.x1, r.y1 + 1, r.y1 + 1, r.z1)) {
|
|
1791
|
+
r.y1 += 1;
|
|
1792
|
+
moved = true;
|
|
1793
|
+
}
|
|
1794
|
+
if (r.y1 - r.y0 < MAX_GROW_CELLS && faceClear(r.x0, r.x1, r.y0 - 1, r.y0 - 1, r.z0, r.z1) && faceHasMust(r.x0, r.x1, r.y0 - 1, r.y0 - 1, r.z0, r.z1) && topOk(r.x0, r.x1, r.y0 - 1, r.y0 - 1, r.z1)) {
|
|
1795
|
+
r.y0 -= 1;
|
|
1796
|
+
moved = true;
|
|
1797
|
+
}
|
|
1798
|
+
} else {
|
|
1799
|
+
if (r.z1 - r.z0 < MAX_GROW_CELLS && faceClear(r.x0, r.x1, r.y0, r.y1, r.z1 + 1, r.z1 + 1) && faceHasMust(r.x0, r.x1, r.y0, r.y1, r.z1 + 1, r.z1 + 1) && topOk(r.x0, r.x1, r.y0, r.y1, r.z1 + 1)) {
|
|
1800
|
+
r.z1 += 1;
|
|
1801
|
+
moved = true;
|
|
1802
|
+
}
|
|
1803
|
+
if (r.z1 - r.z0 < MAX_GROW_CELLS && faceClear(r.x0, r.x1, r.y0, r.y1, r.z0 - 1, r.z0 - 1) && faceHasMust(r.x0, r.x1, r.y0, r.y1, r.z0 - 1, r.z0 - 1)) {
|
|
1804
|
+
r.z0 -= 1;
|
|
1805
|
+
moved = true;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
const topOk = (x0, x1, y0, y1, z) => {
|
|
1811
|
+
if (!keepTops) return true;
|
|
1812
|
+
if (z < 0 || z >= nz) return false;
|
|
1813
|
+
for (let y = y0; y <= y1; y++) {
|
|
1814
|
+
for (let x = x0; x <= x1; x++) if (!testBit(foot, (y * nx + x) * w, z)) return false;
|
|
1815
|
+
}
|
|
1816
|
+
return true;
|
|
1817
|
+
};
|
|
1818
|
+
const allSolid = (r, z) => {
|
|
1819
|
+
if (z < 0 || z >= nz) return false;
|
|
1820
|
+
for (let y = r.y0; y <= r.y1; y++) {
|
|
1821
|
+
for (let x = r.x0; x <= r.x1; x++) if (!testBit(foot, (y * nx + x) * w, z)) return false;
|
|
1822
|
+
}
|
|
1823
|
+
return true;
|
|
1824
|
+
};
|
|
1825
|
+
for (let j = 0; j < ny; j++) {
|
|
1826
|
+
for (let i = 0; i < nx; i++) {
|
|
1827
|
+
const o = (j * nx + i) * w;
|
|
1828
|
+
for (; ; ) {
|
|
1829
|
+
const z = firstSet(l, must, o, 0, nz - 1);
|
|
1830
|
+
if (z < 0) break;
|
|
1831
|
+
let bestX = -1;
|
|
1832
|
+
let bestY = -1;
|
|
1833
|
+
let bestScore = -1;
|
|
1834
|
+
const tryWindow = (wx, wy) => {
|
|
1835
|
+
if (wx < 0 || wy < 0 || wx + W > nx || wy + W > ny) return;
|
|
1836
|
+
if (!faceClear(wx, wx + W - 1, wy, wy + W - 1, z, z) || !topOk(wx, wx + W - 1, wy, wy + W - 1, z)) return;
|
|
1837
|
+
const score = gain({ x0: wx, x1: wx + W - 1, y0: wy, y1: wy + W - 1, z0: z, z1: z });
|
|
1838
|
+
if (score > bestScore) {
|
|
1839
|
+
bestScore = score;
|
|
1840
|
+
bestX = wx;
|
|
1841
|
+
bestY = wy;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
for (const oy of WINDOW_TRIES) for (const ox of WINDOW_TRIES) tryWindow(i - Math.floor(ox * (W - 1)), j - Math.floor(oy * (W - 1)));
|
|
1845
|
+
if (bestX < 0) {
|
|
1846
|
+
for (let wy = j - W + 1; wy <= j; wy++) for (let wx = i - W + 1; wx <= i; wx++) tryWindow(wx, wy);
|
|
1847
|
+
}
|
|
1848
|
+
if (bestX < 0) {
|
|
1849
|
+
stuck += 1;
|
|
1850
|
+
clearRun(l, must, o, z, z);
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
const seed = { x0: bestX, x1: bestX + W - 1, y0: bestY, y1: bestY + W - 1, z0: z, z1: z };
|
|
1854
|
+
let best = seed;
|
|
1855
|
+
let bestGain = -1;
|
|
1856
|
+
for (const order of [[0, 1, 2], [1, 0, 2], [2, 0, 1], [2, 1, 0]]) {
|
|
1857
|
+
const r = { ...seed };
|
|
1858
|
+
for (const axis of order) grow(r, axis);
|
|
1859
|
+
const got = gain(r);
|
|
1860
|
+
if (got > bestGain) {
|
|
1861
|
+
bestGain = got;
|
|
1862
|
+
best = r;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
while (best.z1 + 1 < nz && allSolid(best, best.z1 + 1) && faceClear(best.x0, best.x1, best.y0, best.y1, best.z1 + 1, best.z1 + 1)) best.z1 += 1;
|
|
1866
|
+
while (best.z0 - 1 >= 0 && allSolid(best, best.z0 - 1) && faceClear(best.x0, best.x1, best.y0, best.y1, best.z0 - 1, best.z0 - 1)) best.z0 -= 1;
|
|
1867
|
+
for (let y = best.y0; y <= best.y1; y++) {
|
|
1868
|
+
for (let x = best.x0; x <= best.x1; x++) clearRun(l, must, (y * nx + x) * w, best.z0, best.z1);
|
|
1869
|
+
}
|
|
1870
|
+
const box = rectSolid(input, best);
|
|
1871
|
+
placed.boxes.push(box);
|
|
1872
|
+
placed.boxInfo.push({ stage: SolidStage.Cover, actor: index.owner([box[0], box[1], box[2]], [box[3], box[4], box[5]]) });
|
|
1873
|
+
count += 1;
|
|
1874
|
+
if (count > limit) return { placed: count, stuck, overLimit: true };
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return { placed: count, stuck, overLimit: false };
|
|
1879
|
+
}
|
|
1880
|
+
var SLIT_BIN_CELLS = 32;
|
|
1881
|
+
function closeSlits(input, placed, crouchHeight) {
|
|
1882
|
+
const { l, radius, body } = input;
|
|
1883
|
+
const { h, nx, ny, w } = l;
|
|
1884
|
+
const bins = /* @__PURE__ */ new Map();
|
|
1885
|
+
const pad = radius + h;
|
|
1886
|
+
const size = SLIT_BIN_CELLS * h;
|
|
1887
|
+
const binsOf = (b, visit) => {
|
|
1888
|
+
for (let a = Math.floor((b[0] - pad) / size); a <= Math.floor((b[3] + pad) / size); a++) {
|
|
1889
|
+
for (let c = Math.floor((b[1] - pad) / size); c <= Math.floor((b[4] + pad) / size); c++) visit(a * 1000003 + c);
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
placed.boxes.forEach((b, i) => binsOf(b, (key) => {
|
|
1893
|
+
const list = bins.get(key);
|
|
1894
|
+
if (list === void 0) bins.set(key, [i]);
|
|
1895
|
+
else list.push(i);
|
|
1896
|
+
}));
|
|
1897
|
+
const holdsPoint = (from, to, origin) => {
|
|
1898
|
+
const first = cellCeil((from - origin) / h - 0.5);
|
|
1899
|
+
return origin + (first + 0.5) * h <= to;
|
|
1900
|
+
};
|
|
1901
|
+
const takesBody = (b, axis, sign, from, to) => {
|
|
1902
|
+
const other = axis === 0 ? 1 : 0;
|
|
1903
|
+
const origin = axis === 0 ? l.x0 : l.y0;
|
|
1904
|
+
const count = axis === 0 ? nx : ny;
|
|
1905
|
+
const [lo, hi] = sign > 0 ? [from, to] : [to, from];
|
|
1906
|
+
const first = Math.max(0, cellCeil((lo - origin) / h - 0.5));
|
|
1907
|
+
const last = Math.min(count - 1, cellFloor((hi - origin) / h - 0.5));
|
|
1908
|
+
const otherOrigin = axis === 0 ? l.y0 : l.x0;
|
|
1909
|
+
const otherCount = axis === 0 ? ny : nx;
|
|
1910
|
+
const from2 = Math.max(0, cellCeil((b[other] - radius - otherOrigin) / h - 0.5));
|
|
1911
|
+
const to2 = Math.min(otherCount - 1, cellFloor((b[other + 3] + radius - otherOrigin) / h - 0.5));
|
|
1912
|
+
const zLow = cellFloor((b[2] - l.z0) / h);
|
|
1913
|
+
const zHigh = cellCeil((b[5] - l.z0) / h) - 1;
|
|
1914
|
+
for (let m = first; m <= last; m++) {
|
|
1915
|
+
for (let n = from2; n <= to2; n++) {
|
|
1916
|
+
const col = axis === 0 ? n * nx + m : m * nx + n;
|
|
1917
|
+
if (anyInRun(l, body, col * w, zLow, zHigh)) return true;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return false;
|
|
1921
|
+
};
|
|
1922
|
+
let closed = 0;
|
|
1923
|
+
for (let i = 0; i < placed.boxes.length; i++) {
|
|
1924
|
+
if (placed.boxInfo[i]?.stage !== SolidStage.Cover) continue;
|
|
1925
|
+
const b = placed.boxes[i];
|
|
1926
|
+
const near = /* @__PURE__ */ new Set();
|
|
1927
|
+
binsOf(b, (key) => {
|
|
1928
|
+
for (const j of bins.get(key) ?? []) near.add(j);
|
|
1929
|
+
});
|
|
1930
|
+
for (const j of near) {
|
|
1931
|
+
if (j === i) continue;
|
|
1932
|
+
const o = placed.boxes[j];
|
|
1933
|
+
if (Math.min(b[5], o[5]) - Math.max(b[2], o[2]) < crouchHeight) continue;
|
|
1934
|
+
for (const axis of [0, 1]) {
|
|
1935
|
+
const other = axis === 0 ? 1 : 0;
|
|
1936
|
+
const along = b[other] - radius < o[other + 3] + radius && o[other] - radius < b[other + 3] + radius;
|
|
1937
|
+
if (!along) continue;
|
|
1938
|
+
const origin = axis === 0 ? l.x0 : l.y0;
|
|
1939
|
+
const overlap = 2 * h * SEAM_OVERLAP_CELLS;
|
|
1940
|
+
const up = o[axis] - radius - (b[axis + 3] + radius);
|
|
1941
|
+
if (up > -overlap && up < h && !(up > 0 && holdsPoint(b[axis + 3] + radius, o[axis] - radius, origin))) {
|
|
1942
|
+
const edge = b[axis + 3] + radius;
|
|
1943
|
+
if (!takesBody(b, axis, 1, edge, o[axis] - radius + overlap)) {
|
|
1944
|
+
b[axis + 3] = f(o[axis] - 2 * radius + overlap);
|
|
1945
|
+
closed += 1;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const down = b[axis] - radius - (o[axis + 3] + radius);
|
|
1949
|
+
if (down > -overlap && down < h && !(down > 0 && holdsPoint(o[axis + 3] + radius, b[axis] - radius, origin))) {
|
|
1950
|
+
const edge = b[axis] - radius;
|
|
1951
|
+
if (!takesBody(b, axis, -1, edge, o[axis + 3] + radius - overlap)) {
|
|
1952
|
+
b[axis] = f(o[axis + 3] + 2 * radius - overlap);
|
|
1953
|
+
closed += 1;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return closed;
|
|
1960
|
+
}
|
|
1961
|
+
function countOpenSlits(input, placed, crouchHeight) {
|
|
1962
|
+
const { l, radius } = input;
|
|
1963
|
+
const { h } = l;
|
|
1964
|
+
const size = SLIT_BIN_CELLS * h;
|
|
1965
|
+
const pad = radius + h;
|
|
1966
|
+
const bins = /* @__PURE__ */ new Map();
|
|
1967
|
+
const keysOf = (b) => {
|
|
1968
|
+
const keys = [];
|
|
1969
|
+
for (let a = Math.floor((b[0] - pad) / size); a <= Math.floor((b[3] + pad) / size); a++) {
|
|
1970
|
+
for (let c = Math.floor((b[1] - pad) / size); c <= Math.floor((b[4] + pad) / size); c++) keys.push(a * 1000003 + c);
|
|
1971
|
+
}
|
|
1972
|
+
return keys;
|
|
1973
|
+
};
|
|
1974
|
+
placed.boxes.forEach((b, i) => {
|
|
1975
|
+
for (const key of keysOf(b)) {
|
|
1976
|
+
const list = bins.get(key);
|
|
1977
|
+
if (list === void 0) bins.set(key, [i]);
|
|
1978
|
+
else list.push(i);
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
const shutByAnother = (i, j, axis, at) => {
|
|
1982
|
+
const b = placed.boxes[i];
|
|
1983
|
+
const o = placed.boxes[j];
|
|
1984
|
+
const other = axis === 0 ? 1 : 0;
|
|
1985
|
+
const along = (Math.max(b[other], o[other]) + Math.min(b[other + 3], o[other + 3])) / 2;
|
|
1986
|
+
const zLow = Math.max(b[2], o[2]);
|
|
1987
|
+
const zHigh = Math.min(b[5], o[5]);
|
|
1988
|
+
const x = axis === 0 ? at : along;
|
|
1989
|
+
const y = axis === 0 ? along : at;
|
|
1990
|
+
const reach = radius + 2 * h * GAP_MARGIN_CELLS;
|
|
1991
|
+
for (const k of bins.get(Math.floor(x / size) * 1000003 + Math.floor(y / size)) ?? []) {
|
|
1992
|
+
if (k === i || k === j) continue;
|
|
1993
|
+
const c = placed.boxes[k];
|
|
1994
|
+
const covers = c[0] - reach < x && c[3] + reach > x && c[1] - reach < y && c[4] + reach > y && c[2] <= zLow && c[5] >= zHigh;
|
|
1995
|
+
if (covers) return true;
|
|
1996
|
+
}
|
|
1997
|
+
return false;
|
|
1998
|
+
};
|
|
1999
|
+
let open = 0;
|
|
2000
|
+
placed.boxes.forEach((b, i) => {
|
|
2001
|
+
const near = /* @__PURE__ */ new Set();
|
|
2002
|
+
for (const key of keysOf(b)) for (const j of bins.get(key) ?? []) if (j > i) near.add(j);
|
|
2003
|
+
for (const j of near) {
|
|
2004
|
+
const o = placed.boxes[j];
|
|
2005
|
+
const bothExact = placed.boxInfo[i]?.stage === SolidStage.Exact && placed.boxInfo[j]?.stage === SolidStage.Exact;
|
|
2006
|
+
if (bothExact || Math.min(b[5], o[5]) - Math.max(b[2], o[2]) < crouchHeight) continue;
|
|
2007
|
+
for (const axis of [0, 1]) {
|
|
2008
|
+
const other = axis === 0 ? 1 : 0;
|
|
2009
|
+
if (!(b[other] - radius < o[other + 3] + radius && o[other] - radius < b[other + 3] + radius)) continue;
|
|
2010
|
+
const origin = axis === 0 ? l.x0 : l.y0;
|
|
2011
|
+
const [from, to] = o[axis] >= b[axis + 3] ? [b[axis + 3] + radius, o[axis] - radius] : [o[axis + 3] + radius, b[axis] - radius];
|
|
2012
|
+
const overlap = 2 * h * GAP_MARGIN_CELLS;
|
|
2013
|
+
if (to - from >= h || to - from <= -overlap) continue;
|
|
2014
|
+
const first = cellCeil((from - origin) / h - 0.5);
|
|
2015
|
+
const isSlit = to <= from || origin + (first + 0.5) * h > to;
|
|
2016
|
+
if (isSlit && !shutByAnother(i, j, axis, (from + to) / 2)) open += 1;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
return open;
|
|
2021
|
+
}
|
|
2022
|
+
function propActors(actors, radius) {
|
|
2023
|
+
const out = [];
|
|
2024
|
+
const small = 8 * radius;
|
|
2025
|
+
for (const [index, actor] of actors.entries()) {
|
|
2026
|
+
const surfaces = actor.surfaces.filter((s) => blocksHull(actor, s));
|
|
2027
|
+
if (surfaces.length === 0) continue;
|
|
2028
|
+
if (actor.tags.includes(SceneTag.Prop)) {
|
|
2029
|
+
out.push(index);
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2033
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2034
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2035
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2036
|
+
for (const s of surfaces) {
|
|
2037
|
+
const box = surfaceBounds(s);
|
|
2038
|
+
minX = Math.min(minX, box.min[0]);
|
|
2039
|
+
minY = Math.min(minY, box.min[1]);
|
|
2040
|
+
maxX = Math.max(maxX, box.max[0]);
|
|
2041
|
+
maxY = Math.max(maxY, box.max[1]);
|
|
2042
|
+
}
|
|
2043
|
+
if (maxX - minX <= small && maxY - minY <= small) out.push(index);
|
|
2044
|
+
}
|
|
2045
|
+
return out;
|
|
2046
|
+
}
|
|
2047
|
+
function rasterWorld(l, radius, world) {
|
|
2048
|
+
const foot = newLayer(l);
|
|
2049
|
+
const { h, nx, w } = l;
|
|
2050
|
+
for (const b of world.boxes) {
|
|
2051
|
+
const [i0, i1] = footRange(l, radius, b[0], b[3], l.x0, l.nx);
|
|
2052
|
+
const [j0, j1] = footRange(l, radius, b[1], b[4], l.y0, l.ny);
|
|
2053
|
+
const a = cellFloor((b[2] - l.z0) / h);
|
|
2054
|
+
const c = Math.max(a, cellCeil((b[5] - l.z0) / h) - 1);
|
|
2055
|
+
for (let j = j0; j <= j1; j++) {
|
|
2056
|
+
for (let i = i0; i <= i1; i++) setRun(l, foot, (j * nx + i) * w, a, c);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
for (const r of world.ramps) {
|
|
2060
|
+
const [i0, i1] = footRange(l, radius, r[0], r[3], l.x0, l.nx);
|
|
2061
|
+
const [j0, j1] = footRange(l, radius, r[1], r[4], l.y0, l.ny);
|
|
2062
|
+
const a = cellFloor((r[2] - l.z0) / h);
|
|
2063
|
+
const axis = r[6];
|
|
2064
|
+
const m0 = axis === 0 ? r[0] : r[1];
|
|
2065
|
+
const m1 = axis === 0 ? r[3] : r[4];
|
|
2066
|
+
const lo = r[7];
|
|
2067
|
+
const hi = r[8];
|
|
2068
|
+
for (let j = j0; j <= j1; j++) {
|
|
2069
|
+
for (let i = i0; i <= i1; i++) {
|
|
2070
|
+
const centre = axis === 0 ? centreX(l, i) : centreY(l, j);
|
|
2071
|
+
const uphill = hi >= lo ? Math.min(m1, centre + radius) : Math.max(m0, centre - radius);
|
|
2072
|
+
const top = lo + (hi - lo) * (uphill - m0) / (m1 - m0);
|
|
2073
|
+
setRun(l, foot, (j * nx + i) * w, a, Math.max(a, cellCeil((top - l.z0) / h) - 1));
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return foot;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/world-fit/narrow.ts
|
|
2081
|
+
var FINE_PER_CELL = [16, 32];
|
|
2082
|
+
var SIDE_REACH_CELLS = 3;
|
|
2083
|
+
var STRIP_HEIGHTS = 8;
|
|
2084
|
+
var STRIP_END_CELLS = 2;
|
|
2085
|
+
function readAbove(l, from, up) {
|
|
2086
|
+
const out = newLayer(l);
|
|
2087
|
+
const words = up >>> 5;
|
|
2088
|
+
const bits = up & 31;
|
|
2089
|
+
for (let o = 0; o < from.length; o += l.w) {
|
|
2090
|
+
for (let i = 0; i + words < l.w; i++) {
|
|
2091
|
+
const a = from[o + i + words];
|
|
2092
|
+
const b = i + words + 1 < l.w ? from[o + i + words + 1] : 0;
|
|
2093
|
+
out[o + i] = (bits === 0 ? a : a >>> bits | b << 32 - bits) >>> 0;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return out;
|
|
2097
|
+
}
|
|
2098
|
+
function stripCells(l, trueBlocked, narrowBlocked, narrowFoot, crouchCells) {
|
|
2099
|
+
const { nx, ny, w } = l;
|
|
2100
|
+
const walls = readAbove(l, narrowFoot, crouchCells - 1);
|
|
2101
|
+
const out = newLayer(l);
|
|
2102
|
+
const narrowAt = (i, j, word) => i < 0 || j < 0 || i >= nx || j >= ny ? 4294967295 : walls[(j * nx + i) * w + word];
|
|
2103
|
+
for (let j = 0; j < ny; j++) {
|
|
2104
|
+
for (let i = 0; i < nx; i++) {
|
|
2105
|
+
const o = (j * nx + i) * w;
|
|
2106
|
+
for (let word = 0; word < w; word++) {
|
|
2107
|
+
const open = (trueBlocked[o + word] & ~narrowBlocked[o + word]) >>> 0;
|
|
2108
|
+
if (open === 0) continue;
|
|
2109
|
+
let across = 0;
|
|
2110
|
+
for (const [dx, dy] of [[1, 0], [0, 1]]) {
|
|
2111
|
+
const a1 = narrowAt(i - dx, j - dy, word);
|
|
2112
|
+
const b1 = narrowAt(i + dx, j + dy, word);
|
|
2113
|
+
const a2 = narrowAt(i - 2 * dx, j - 2 * dy, word);
|
|
2114
|
+
const b2 = narrowAt(i + 2 * dx, j + 2 * dy, word);
|
|
2115
|
+
across = (across | a1 & b1 | a1 & b2 | a2 & b1) >>> 0;
|
|
2116
|
+
}
|
|
2117
|
+
out[o + word] = (open & across) >>> 0;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
return out;
|
|
2122
|
+
}
|
|
2123
|
+
function gatherStrips(l, strip) {
|
|
2124
|
+
const { nx, ny, nz, w } = l;
|
|
2125
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2126
|
+
const out = [];
|
|
2127
|
+
for (let col = 0; col < nx * ny; col++) {
|
|
2128
|
+
for (let word = 0; word < w; word++) {
|
|
2129
|
+
let bits = strip[col * w + word];
|
|
2130
|
+
while (bits !== 0) {
|
|
2131
|
+
const bit = 31 - Math.clz32(bits & -bits);
|
|
2132
|
+
bits = (bits & bits - 1) >>> 0;
|
|
2133
|
+
const start = col * nz + word * 32 + bit;
|
|
2134
|
+
if (seen.has(start)) continue;
|
|
2135
|
+
const j0 = Math.floor(col / nx);
|
|
2136
|
+
const found = { cells: [], x0: col - j0 * nx, x1: col - j0 * nx, y0: j0, y1: j0, z0: word * 32 + bit, z1: word * 32 + bit };
|
|
2137
|
+
const stack = [start];
|
|
2138
|
+
seen.add(start);
|
|
2139
|
+
while (stack.length > 0) {
|
|
2140
|
+
const node = stack.pop();
|
|
2141
|
+
found.cells.push(node);
|
|
2142
|
+
const k = node % nz;
|
|
2143
|
+
const c = (node - k) / nz;
|
|
2144
|
+
const y = Math.floor(c / nx);
|
|
2145
|
+
const x = c - y * nx;
|
|
2146
|
+
found.x0 = Math.min(found.x0, x);
|
|
2147
|
+
found.x1 = Math.max(found.x1, x);
|
|
2148
|
+
found.y0 = Math.min(found.y0, y);
|
|
2149
|
+
found.y1 = Math.max(found.y1, y);
|
|
2150
|
+
found.z0 = Math.min(found.z0, k);
|
|
2151
|
+
found.z1 = Math.max(found.z1, k);
|
|
2152
|
+
for (const [dx, dy, dz] of [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]]) {
|
|
2153
|
+
const nextX = x + dx;
|
|
2154
|
+
const nextY = y + dy;
|
|
2155
|
+
const nextK = k + dz;
|
|
2156
|
+
if (nextX < 0 || nextY < 0 || nextX >= nx || nextY >= ny || nextK < 0 || nextK >= nz) continue;
|
|
2157
|
+
const nextCol = nextY * nx + nextX;
|
|
2158
|
+
if ((strip[nextCol * w + (nextK >>> 5)] >>> (nextK & 31) & 1) === 0) continue;
|
|
2159
|
+
const next = nextCol * nz + nextK;
|
|
2160
|
+
if (seen.has(next)) continue;
|
|
2161
|
+
seen.add(next);
|
|
2162
|
+
stack.push(next);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
out.push(found);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return out;
|
|
2170
|
+
}
|
|
2171
|
+
function stripIsOpen(l, actors, radius, crouchCells, s, trueBlocked) {
|
|
2172
|
+
const { h, nx, w } = l;
|
|
2173
|
+
const reach = SIDE_REACH_CELLS;
|
|
2174
|
+
const stride = Math.max(1, Math.ceil((s.z1 - s.z0 + 1) / STRIP_HEIGHTS));
|
|
2175
|
+
const heights = [];
|
|
2176
|
+
for (let k = s.z0; k <= s.z1; k += stride) {
|
|
2177
|
+
const sides = [];
|
|
2178
|
+
for (let y = Math.max(0, s.y0 - reach); y <= Math.min(l.ny - 1, s.y1 + reach); y++) {
|
|
2179
|
+
for (let x = Math.max(0, s.x0 - reach); x <= Math.min(nx - 1, s.x1 + reach); x++) {
|
|
2180
|
+
const inBox = x >= s.x0 && x <= s.x1 && y >= s.y0 && y <= s.y1;
|
|
2181
|
+
if (!inBox && (trueBlocked[(y * nx + x) * w + (k >>> 5)] >>> (k & 31) & 1) === 0) sides.push([x, y]);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (sides.length >= 2 && coarseSeparated(l, sides, s, trueBlocked, k, reach)) heights.push({ k, sides });
|
|
2185
|
+
}
|
|
2186
|
+
if (heights.length === 0) return false;
|
|
2187
|
+
const pad = reach * h;
|
|
2188
|
+
const zLow = heights[0].k;
|
|
2189
|
+
const zHigh = heights[heights.length - 1].k + crouchCells;
|
|
2190
|
+
const min = [l.x0 + s.x0 * h - pad, l.y0 + s.y0 * h - pad, l.z0 + zLow * h];
|
|
2191
|
+
const max = [l.x0 + (s.x1 + 1) * h + pad, l.y0 + (s.y1 + 1) * h + pad, l.z0 + zHigh * h];
|
|
2192
|
+
const local = actors.map((a) => ({ ...a, surfaces: a.surfaces.filter((f2) => {
|
|
2193
|
+
if (!blocksHull(a, f2)) return false;
|
|
2194
|
+
const b = surfaceBounds(f2);
|
|
2195
|
+
return b.min[0] - radius < max[0] && b.max[0] + radius > min[0] && b.min[1] - radius < max[1] && b.max[1] + radius > min[1] && b.min[2] < max[2] && b.max[2] > min[2];
|
|
2196
|
+
}) })).filter((a) => a.surfaces.length > 0);
|
|
2197
|
+
const doorShaped = Math.min(s.x1 - s.x0, s.y1 - s.y0) <= 1;
|
|
2198
|
+
for (const finePerCell of doorShaped ? FINE_PER_CELL : FINE_PER_CELL.slice(0, 1)) {
|
|
2199
|
+
const fine = makeLattice(h / finePerCell, min, max);
|
|
2200
|
+
const layer = newLayer(fine);
|
|
2201
|
+
rasterScene(fine, layer, radius, local);
|
|
2202
|
+
for (const { k, sides } of heights) {
|
|
2203
|
+
const a = Math.floor((k - zLow) * finePerCell);
|
|
2204
|
+
const b = Math.min(fine.nz - 1, a + crouchCells * finePerCell - 1);
|
|
2205
|
+
if (joined(l, fine, layer, a, b, sides)) return true;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
function joined(l, fine, layer, a, b, sides) {
|
|
2211
|
+
const { h } = l;
|
|
2212
|
+
const free = new Uint8Array(fine.nx * fine.ny);
|
|
2213
|
+
for (let c = 0; c < free.length; c++) free[c] = anyInRun(fine, layer, c * fine.w, a, b) ? 0 : 1;
|
|
2214
|
+
const fineIndex = (x, y) => {
|
|
2215
|
+
const i = Math.floor((l.x0 + (x + 0.5) * h - fine.x0) / fine.h);
|
|
2216
|
+
const j = Math.floor((l.y0 + (y + 0.5) * h - fine.y0) / fine.h);
|
|
2217
|
+
return i < 0 || j < 0 || i >= fine.nx || j >= fine.ny ? -1 : j * fine.nx + i;
|
|
2218
|
+
};
|
|
2219
|
+
const label = new Int32Array(free.length).fill(-1);
|
|
2220
|
+
let next = 0;
|
|
2221
|
+
const labels = /* @__PURE__ */ new Set();
|
|
2222
|
+
for (const [x, y] of sides) {
|
|
2223
|
+
const start = fineIndex(x, y);
|
|
2224
|
+
if (start < 0 || free[start] === 0) continue;
|
|
2225
|
+
if (label[start] >= 0) {
|
|
2226
|
+
labels.add(label[start]);
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
const id = next++;
|
|
2230
|
+
labels.add(id);
|
|
2231
|
+
const stack = [start];
|
|
2232
|
+
label[start] = id;
|
|
2233
|
+
while (stack.length > 0) {
|
|
2234
|
+
const c = stack.pop();
|
|
2235
|
+
const cx = c % fine.nx;
|
|
2236
|
+
for (const o of [cx > 0 ? c - 1 : -1, cx + 1 < fine.nx ? c + 1 : -1, c >= fine.nx ? c - fine.nx : -1, c + fine.nx < free.length ? c + fine.nx : -1]) {
|
|
2237
|
+
if (o >= 0 && free[o] === 1 && label[o] === -1) {
|
|
2238
|
+
label[o] = id;
|
|
2239
|
+
stack.push(o);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return labels.size === 1;
|
|
2245
|
+
}
|
|
2246
|
+
function coarseSeparated(l, sides, s, trueBlocked, k, reach) {
|
|
2247
|
+
const { nx, w } = l;
|
|
2248
|
+
const x0 = Math.max(0, s.x0 - reach);
|
|
2249
|
+
const x1 = Math.min(nx - 1, s.x1 + reach);
|
|
2250
|
+
const y0 = Math.max(0, s.y0 - reach);
|
|
2251
|
+
const y1 = Math.min(l.ny - 1, s.y1 + reach);
|
|
2252
|
+
const clear = (x, y) => (trueBlocked[(y * nx + x) * w + (k >>> 5)] >>> (k & 31) & 1) === 0;
|
|
2253
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2254
|
+
const first = sides[0];
|
|
2255
|
+
const stack = [first[1] * nx + first[0]];
|
|
2256
|
+
seen.add(stack[0]);
|
|
2257
|
+
while (stack.length > 0) {
|
|
2258
|
+
const c = stack.pop();
|
|
2259
|
+
const y = Math.floor(c / nx);
|
|
2260
|
+
const x = c - y * nx;
|
|
2261
|
+
for (const [a, b] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
|
|
2262
|
+
if (a < x0 || a > x1 || b < y0 || b > y1 || !clear(a, b)) continue;
|
|
2263
|
+
const n = b * nx + a;
|
|
2264
|
+
if (!seen.has(n)) {
|
|
2265
|
+
seen.add(n);
|
|
2266
|
+
stack.push(n);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return sides.some(([x, y]) => !seen.has(y * nx + x));
|
|
2271
|
+
}
|
|
2272
|
+
function turnedActorNear(l, actors, radius, crouchCells, s) {
|
|
2273
|
+
const { h } = l;
|
|
2274
|
+
const min = [l.x0 + s.x0 * h - radius, l.y0 + s.y0 * h - radius, l.z0 + s.z0 * h];
|
|
2275
|
+
const max = [l.x0 + (s.x1 + 1) * h + radius, l.y0 + (s.y1 + 1) * h + radius, l.z0 + (s.z1 + 1) * h];
|
|
2276
|
+
for (const a of actors) {
|
|
2277
|
+
for (const f2 of a.surfaces) {
|
|
2278
|
+
if (!blocksHull(a, f2) || exactBoxBounds(f2) !== null) continue;
|
|
2279
|
+
const b = surfaceBounds(f2);
|
|
2280
|
+
const overlaps = b.min[0] < max[0] && b.max[0] > min[0] && b.min[1] < max[1] && b.max[1] > min[1] && b.min[2] < max[2] && b.max[2] > min[2];
|
|
2281
|
+
if (overlaps && b.min[2] <= min[2] + h && b.max[2] >= min[2] + crouchCells * h) return a.name;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
function narrowPassages(l, actors, radius, crouchCells, foot) {
|
|
2287
|
+
const { nx, nz, w, h } = l;
|
|
2288
|
+
const narrowFoot = newLayer(l);
|
|
2289
|
+
rasterScene(l, narrowFoot, Math.max(h / 2, radius - h / 2), actors);
|
|
2290
|
+
const trueBlocked = reachDown(l, foot, crouchCells);
|
|
2291
|
+
const narrowBlocked = reachDown(l, narrowFoot, crouchCells);
|
|
2292
|
+
const opened = newLayer(l);
|
|
2293
|
+
const shut = [];
|
|
2294
|
+
const strips = stripCells(l, trueBlocked, narrowBlocked, narrowFoot, crouchCells);
|
|
2295
|
+
for (const s of gatherStrips(l, strips)) {
|
|
2296
|
+
if (!stripIsOpen(l, actors, radius, crouchCells, s, trueBlocked)) continue;
|
|
2297
|
+
const turned = turnedActorNear(l, actors, radius, crouchCells, s);
|
|
2298
|
+
if (turned !== null) {
|
|
2299
|
+
shut.push({ actor: turned, at: [l.x0 + (s.x0 + s.x1 + 1) / 2 * h, l.y0 + (s.y0 + s.y1 + 1) / 2 * h, l.z0 + s.z0 * h] });
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
const cells = new Set(s.cells);
|
|
2303
|
+
let edge = s.cells;
|
|
2304
|
+
for (let round = 0; round < STRIP_END_CELLS; round++) {
|
|
2305
|
+
const nextEdge = [];
|
|
2306
|
+
for (const node of edge) {
|
|
2307
|
+
const k = node % nz;
|
|
2308
|
+
const col = (node - k) / nz;
|
|
2309
|
+
for (const o of [col - 1, col + 1, col - nx, col + nx]) {
|
|
2310
|
+
if (o < 0 || o * w >= foot.length) continue;
|
|
2311
|
+
const closedTrue = (trueBlocked[o * w + (k >>> 5)] >>> (k & 31) & 1) === 1;
|
|
2312
|
+
const openNarrow = (narrowBlocked[o * w + (k >>> 5)] >>> (k & 31) & 1) === 0;
|
|
2313
|
+
const id = o * nz + k;
|
|
2314
|
+
if (closedTrue && openNarrow && !cells.has(id)) {
|
|
2315
|
+
cells.add(id);
|
|
2316
|
+
nextEdge.push(id);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
edge = nextEdge;
|
|
2321
|
+
}
|
|
2322
|
+
for (const node of cells) {
|
|
2323
|
+
const k = node % nz;
|
|
2324
|
+
const col = (node - k) / nz;
|
|
2325
|
+
if (lastSet(l, narrowBlocked, col * w, 0, k - 1) < 0) continue;
|
|
2326
|
+
for (let z = k; z < k + crouchCells; z++) {
|
|
2327
|
+
if (z >= nz) break;
|
|
2328
|
+
const inFoot = (foot[col * w + (z >>> 5)] >>> (z & 31) & 1) === 1;
|
|
2329
|
+
const inNarrow = (narrowFoot[col * w + (z >>> 5)] >>> (z & 31) & 1) === 1;
|
|
2330
|
+
if (inFoot && !inNarrow) setRun(l, opened, col * w, z, z);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
return { opened, strips, shut };
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/world-fit/reach.ts
|
|
2338
|
+
function hullCells(hull, h) {
|
|
2339
|
+
const stand = cellCeil(hull.height / h);
|
|
2340
|
+
const crouch = cellCeil(hull.crouchHeight / h);
|
|
2341
|
+
const jump = cellFloor(hull.jump / h);
|
|
2342
|
+
return { stand, crouch, jump, climb: jump + (stand - crouch), step: cellFloor(hull.step / h), leap: cellCeil(hull.leap / h) };
|
|
2343
|
+
}
|
|
2344
|
+
var LEAP_DIRECTIONS = [
|
|
2345
|
+
[1, 0],
|
|
2346
|
+
[-1, 0],
|
|
2347
|
+
[0, 1],
|
|
2348
|
+
[0, -1],
|
|
2349
|
+
[Math.SQRT1_2, Math.SQRT1_2],
|
|
2350
|
+
[Math.SQRT1_2, -Math.SQRT1_2],
|
|
2351
|
+
[-Math.SQRT1_2, Math.SQRT1_2],
|
|
2352
|
+
[-Math.SQRT1_2, -Math.SQRT1_2]
|
|
2353
|
+
];
|
|
2354
|
+
var NEIGHBOURS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
2355
|
+
var INITIAL_NODES = 1 << 18;
|
|
2356
|
+
var LEAP_OVERRUN = 1.5;
|
|
2357
|
+
var LEAN_MIN_DROP_CELLS = 2;
|
|
2358
|
+
var AXES = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
2359
|
+
function closeLeanLedges(l, foot, closedCells, reach, hull) {
|
|
2360
|
+
const { nx, ny, nz, w } = l;
|
|
2361
|
+
const closed = [];
|
|
2362
|
+
for (let n = 0; n < reach.count; n++) {
|
|
2363
|
+
const node = reach.nodes[n];
|
|
2364
|
+
const k = node % nz;
|
|
2365
|
+
if (k === 0 || k + hull.crouch > nz) continue;
|
|
2366
|
+
const col = (node - k) / nz;
|
|
2367
|
+
if (testBit(foot, col * w, k)) continue;
|
|
2368
|
+
const j = Math.floor(col / nx);
|
|
2369
|
+
const i = col - j * nx;
|
|
2370
|
+
for (const [dx, dy] of AXES) {
|
|
2371
|
+
const wi = i + dx;
|
|
2372
|
+
const wj = j + dy;
|
|
2373
|
+
const oi = i - dx;
|
|
2374
|
+
const oj = j - dy;
|
|
2375
|
+
if (wi < 0 || wj < 0 || wi >= nx || wj >= ny || oi < 0 || oj < 0 || oi >= nx || oj >= ny) continue;
|
|
2376
|
+
const face = (wj * nx + wi) * w;
|
|
2377
|
+
const clear = firstClear(l, foot, face, k, nz - 1);
|
|
2378
|
+
const top = clear < 0 ? nz - 1 : clear - 1;
|
|
2379
|
+
if (top < k) continue;
|
|
2380
|
+
let faceTop = top;
|
|
2381
|
+
for (let fi = wi + dx, fj = wj + dy; fi >= 0 && fj >= 0 && fi < nx && fj < ny && faceTop - k + 1 < hull.crouch; fi += dx, fj += dy) {
|
|
2382
|
+
const further = (fj * nx + fi) * w;
|
|
2383
|
+
if (faceTop + 1 >= nz || !testBit(foot, further, faceTop + 1)) break;
|
|
2384
|
+
const end = firstClear(l, foot, further, faceTop + 1, nz - 1);
|
|
2385
|
+
faceTop = end < 0 ? nz - 1 : end - 1;
|
|
2386
|
+
}
|
|
2387
|
+
if (faceTop - k + 1 < hull.crouch) continue;
|
|
2388
|
+
if (anyInRun(l, foot, (oj * nx + oi) * w, Math.max(0, k - LEAN_MIN_DROP_CELLS), k - 1)) continue;
|
|
2389
|
+
closed.push([col * w, k, top]);
|
|
2390
|
+
break;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
for (const [o, k, top] of closed) {
|
|
2394
|
+
setRun(l, foot, o, k, top);
|
|
2395
|
+
setRun(l, closedCells, o, k, top);
|
|
2396
|
+
}
|
|
2397
|
+
return closed.length;
|
|
2398
|
+
}
|
|
2399
|
+
function floodReach(l, foot, hull, spawns) {
|
|
2400
|
+
const { nx, ny, nz, w, h } = l;
|
|
2401
|
+
const blocked = reachDown(l, foot, hull.crouch);
|
|
2402
|
+
const standing = newLayer(l);
|
|
2403
|
+
const body = newLayer(l);
|
|
2404
|
+
let nodes = new Int32Array(INITIAL_NODES);
|
|
2405
|
+
let tail = 0;
|
|
2406
|
+
const fell = new Uint8Array(nx * ny);
|
|
2407
|
+
let fellOut = 0;
|
|
2408
|
+
const land = (offset, k) => {
|
|
2409
|
+
const under = lastSet(l, blocked, offset, 0, k - 1);
|
|
2410
|
+
return under < 0 ? -1 : under + 1;
|
|
2411
|
+
};
|
|
2412
|
+
const push = (col, k) => {
|
|
2413
|
+
const offset = col * w;
|
|
2414
|
+
if (testBit(standing, offset, k)) return;
|
|
2415
|
+
setBit(standing, offset, k);
|
|
2416
|
+
if (tail === nodes.length) {
|
|
2417
|
+
const grown = new Int32Array(nodes.length * 2);
|
|
2418
|
+
grown.set(nodes);
|
|
2419
|
+
nodes = grown;
|
|
2420
|
+
}
|
|
2421
|
+
nodes[tail++] = col * nz + k;
|
|
2422
|
+
const ceiling = firstSet(l, foot, offset, k, k + hull.jump + hull.stand);
|
|
2423
|
+
setRun(l, body, offset, k, ceiling < 0 ? k + hull.jump + hull.stand : ceiling - 1);
|
|
2424
|
+
};
|
|
2425
|
+
const descend = (col, k) => {
|
|
2426
|
+
const offset = col * w;
|
|
2427
|
+
const to = land(offset, k);
|
|
2428
|
+
if (to < 0) {
|
|
2429
|
+
if (fell[col] === 0) {
|
|
2430
|
+
fell[col] = 1;
|
|
2431
|
+
fellOut += 1;
|
|
2432
|
+
}
|
|
2433
|
+
return -1;
|
|
2434
|
+
}
|
|
2435
|
+
const ceiling = firstSet(l, foot, offset, k, k + hull.stand);
|
|
2436
|
+
setRun(l, body, offset, to, ceiling < 0 ? k + hull.stand - 1 : ceiling - 1);
|
|
2437
|
+
push(col, to);
|
|
2438
|
+
return to;
|
|
2439
|
+
};
|
|
2440
|
+
const unplaced = [];
|
|
2441
|
+
for (const [index, s] of spawns.entries()) {
|
|
2442
|
+
const i = Math.floor((s[0] - l.x0) / h);
|
|
2443
|
+
const j = Math.floor((s[1] - l.y0) / h);
|
|
2444
|
+
const k = Math.floor((s[2] - l.z0) / h);
|
|
2445
|
+
if (i < 0 || j < 0 || i >= nx || j >= ny) {
|
|
2446
|
+
unplaced.push(index);
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
const offset = (j * nx + i) * w;
|
|
2450
|
+
const free = firstClear(l, blocked, offset, Math.max(0, k), Math.min(nz - 1, k + hull.climb));
|
|
2451
|
+
if (free < 0 || descend(j * nx + i, free) < 0) unplaced.push(index);
|
|
2452
|
+
}
|
|
2453
|
+
const leap = (i, j, k) => {
|
|
2454
|
+
const reach = hull.leap * LEAP_OVERRUN;
|
|
2455
|
+
for (const [dx, dy] of LEAP_DIRECTIONS) {
|
|
2456
|
+
let lastI = i;
|
|
2457
|
+
let lastJ = j;
|
|
2458
|
+
for (let n = 1; n <= reach; n++) {
|
|
2459
|
+
const ci = Math.round(i + dx * n);
|
|
2460
|
+
const cj = Math.round(j + dy * n);
|
|
2461
|
+
if (ci < 0 || cj < 0 || ci >= nx || cj >= ny) break;
|
|
2462
|
+
if (ci === lastI && cj === lastJ) continue;
|
|
2463
|
+
const s = n / hull.leap;
|
|
2464
|
+
const z = k + Math.round(4 * hull.jump * s * (1 - s));
|
|
2465
|
+
if (z < 1) break;
|
|
2466
|
+
const col = cj * nx + ci;
|
|
2467
|
+
if (z >= nz || testBit(blocked, col * w, z)) break;
|
|
2468
|
+
const cornered = ci !== lastI && cj !== lastJ && testBit(blocked, (lastJ * nx + ci) * w, z) && testBit(blocked, (cj * nx + lastI) * w, z);
|
|
2469
|
+
if (cornered) break;
|
|
2470
|
+
lastI = ci;
|
|
2471
|
+
lastJ = cj;
|
|
2472
|
+
if (descend(col, z) < 0) break;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
let head = 0;
|
|
2477
|
+
while (head < tail) {
|
|
2478
|
+
const node = nodes[head++];
|
|
2479
|
+
const k = node % nz;
|
|
2480
|
+
const col = (node - k) / nz;
|
|
2481
|
+
const j = Math.floor(col / nx);
|
|
2482
|
+
const i = col - j * nx;
|
|
2483
|
+
const offset = col * w;
|
|
2484
|
+
const stop = firstSet(l, blocked, offset, k + 1, k + hull.climb);
|
|
2485
|
+
const top = stop < 0 ? Math.min(nz - 1, k + hull.climb) : stop - 1;
|
|
2486
|
+
let edge = false;
|
|
2487
|
+
for (const [dx, dy] of NEIGHBOURS) {
|
|
2488
|
+
const ni = i + dx;
|
|
2489
|
+
const nj = j + dy;
|
|
2490
|
+
if (ni < 0 || nj < 0 || ni >= nx || nj >= ny) continue;
|
|
2491
|
+
const nextCol = nj * nx + ni;
|
|
2492
|
+
const nextOffset = nextCol * w;
|
|
2493
|
+
let at = k;
|
|
2494
|
+
for (; ; ) {
|
|
2495
|
+
const free = firstClear(l, blocked, nextOffset, at, top);
|
|
2496
|
+
if (free < 0) break;
|
|
2497
|
+
const to = descend(nextCol, free);
|
|
2498
|
+
if (to < k - hull.step) edge = true;
|
|
2499
|
+
const next = firstSet(l, blocked, nextOffset, free, top);
|
|
2500
|
+
if (next < 0) break;
|
|
2501
|
+
at = next;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (edge) leap(i, j, k);
|
|
2505
|
+
}
|
|
2506
|
+
return { nodes: nodes.subarray(0, tail), count: tail, standing, body, fell, fellOut, unplaced };
|
|
2507
|
+
}
|
|
2508
|
+
function hasNear(l, layer, offset, k, tolerance) {
|
|
2509
|
+
return firstSet(l, layer, offset, k - tolerance, k + tolerance) >= 0;
|
|
2510
|
+
}
|
|
2511
|
+
function worstRegionCells(l, mask) {
|
|
2512
|
+
const { nx } = l;
|
|
2513
|
+
let worst = 0;
|
|
2514
|
+
const stack = [];
|
|
2515
|
+
for (let s = 0; s < mask.length; s++) {
|
|
2516
|
+
if (mask[s] === 0) continue;
|
|
2517
|
+
let size = 0;
|
|
2518
|
+
mask[s] = 0;
|
|
2519
|
+
stack.push(s);
|
|
2520
|
+
while (stack.length > 0) {
|
|
2521
|
+
const c = stack.pop();
|
|
2522
|
+
size += 1;
|
|
2523
|
+
const x = c % nx;
|
|
2524
|
+
if (x > 0 && mask[c - 1] !== 0) {
|
|
2525
|
+
mask[c - 1] = 0;
|
|
2526
|
+
stack.push(c - 1);
|
|
2527
|
+
}
|
|
2528
|
+
if (x < nx - 1 && mask[c + 1] !== 0) {
|
|
2529
|
+
mask[c + 1] = 0;
|
|
2530
|
+
stack.push(c + 1);
|
|
2531
|
+
}
|
|
2532
|
+
if (c >= nx && mask[c - nx] !== 0) {
|
|
2533
|
+
mask[c - nx] = 0;
|
|
2534
|
+
stack.push(c - nx);
|
|
2535
|
+
}
|
|
2536
|
+
if (c + nx < mask.length && mask[c + nx] !== 0) {
|
|
2537
|
+
mask[c + nx] = 0;
|
|
2538
|
+
stack.push(c + nx);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
worst = Math.max(worst, size);
|
|
2542
|
+
}
|
|
2543
|
+
return worst;
|
|
2544
|
+
}
|
|
2545
|
+
function standsNear(l, standing, col, k, tolerance, radius) {
|
|
2546
|
+
const { nx, ny, w } = l;
|
|
2547
|
+
const j = Math.floor(col / nx);
|
|
2548
|
+
const i = col - j * nx;
|
|
2549
|
+
for (let y = Math.max(0, j - radius); y <= Math.min(ny - 1, j + radius); y++) {
|
|
2550
|
+
for (let x = Math.max(0, i - radius); x <= Math.min(nx - 1, i + radius); x++) {
|
|
2551
|
+
if (hasNear(l, standing, (y * nx + x) * w, k, tolerance)) return true;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return false;
|
|
2555
|
+
}
|
|
2556
|
+
function fellNear(l, fell, col, radius) {
|
|
2557
|
+
const { nx, ny } = l;
|
|
2558
|
+
const j = Math.floor(col / nx);
|
|
2559
|
+
const i = col - j * nx;
|
|
2560
|
+
for (let y = Math.max(0, j - radius); y <= Math.min(ny - 1, j + radius); y++) {
|
|
2561
|
+
for (let x = Math.max(0, i - radius); x <= Math.min(nx - 1, i + radius); x++) {
|
|
2562
|
+
if (fell[y * nx + x] === 1) return true;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
function compareReach(l, truth, fit, tolerance, unitsPerMeter, sourceNear, nearCells) {
|
|
2568
|
+
const { nx, ny, nz, w, h } = l;
|
|
2569
|
+
const cellM2 = (h / unitsPerMeter) ** 2;
|
|
2570
|
+
const lostMask = new Uint8Array(nx * ny);
|
|
2571
|
+
const extraMask = new Uint8Array(nx * ny);
|
|
2572
|
+
const lost = [];
|
|
2573
|
+
let lostCount = 0;
|
|
2574
|
+
const extra = [];
|
|
2575
|
+
for (let n = 0; n < truth.count; n++) {
|
|
2576
|
+
const node = truth.nodes[n];
|
|
2577
|
+
const k = node % nz;
|
|
2578
|
+
const col = (node - k) / nz;
|
|
2579
|
+
if (hasNear(l, fit.standing, col * w, k, tolerance)) continue;
|
|
2580
|
+
lostCount += 1;
|
|
2581
|
+
lostMask[col] = 1;
|
|
2582
|
+
if (!standsNear(l, fit.standing, col, k, tolerance, nearCells)) lost.push(node);
|
|
2583
|
+
}
|
|
2584
|
+
let underFloorNodes = 0;
|
|
2585
|
+
for (let n = 0; n < fit.count; n++) {
|
|
2586
|
+
const node = fit.nodes[n];
|
|
2587
|
+
const k = node % nz;
|
|
2588
|
+
const col = (node - k) / nz;
|
|
2589
|
+
if (hasNear(l, truth.standing, col * w, k, tolerance)) continue;
|
|
2590
|
+
extra.push(node);
|
|
2591
|
+
extraMask[col] = 1;
|
|
2592
|
+
if (firstSet(l, sourceNear, col * w, k - tolerance, k + tolerance) < 0 && firstSet(l, sourceNear, col * w, k + tolerance, nz - 1) >= 0) underFloorNodes += 1;
|
|
2593
|
+
}
|
|
2594
|
+
let newFalls = 0;
|
|
2595
|
+
for (let col = 0; col < fit.fell.length; col++) {
|
|
2596
|
+
if (fit.fell[col] === 1 && !fellNear(l, truth.fell, col, nearCells)) newFalls += 1;
|
|
2597
|
+
}
|
|
2598
|
+
const denominator = Math.max(1, truth.count);
|
|
2599
|
+
const pct = (count) => Math.round(1e4 * count / denominator) / 100;
|
|
2600
|
+
const area = (mask) => Math.round(worstRegionCells(l, mask) * cellM2 * 100) / 100;
|
|
2601
|
+
return {
|
|
2602
|
+
truthNodes: truth.count,
|
|
2603
|
+
fitNodes: fit.count,
|
|
2604
|
+
lostNodes: lostCount,
|
|
2605
|
+
extraNodes: extra.length,
|
|
2606
|
+
lostPct: pct(lostCount),
|
|
2607
|
+
lostBeyondSlackPct: pct(lost.length),
|
|
2608
|
+
extraPct: pct(extra.length),
|
|
2609
|
+
worstLostM2: area(lostMask),
|
|
2610
|
+
worstExtraM2: area(extraMask),
|
|
2611
|
+
underFloorNodes,
|
|
2612
|
+
newFalls,
|
|
2613
|
+
lost,
|
|
2614
|
+
extra
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// src/world-fit/fit.ts
|
|
2619
|
+
var FitStatus = {
|
|
2620
|
+
Ok: "ok",
|
|
2621
|
+
/** Even the coarsest fit needs more solids than the budget. */
|
|
2622
|
+
OverBudget: "over_budget",
|
|
2623
|
+
/** A hull gets under the source's floor or out of the level in every fit tried. */
|
|
2624
|
+
FallsThrough: "falls_through",
|
|
2625
|
+
/** A fit within budget loses more reach than a fit may. */
|
|
2626
|
+
ReachLost: "reach_lost",
|
|
2627
|
+
/** A gap the source has shut is open, or shut by less than the stated margin, in every fit tried: never a world. */
|
|
2628
|
+
OpenSlits: "open_slits",
|
|
2629
|
+
/** In every fit tried, feet stay further from the source's floor than a player can miss, and the budget cannot buy them back. */
|
|
2630
|
+
FloorOver: "floor_over",
|
|
2631
|
+
/** No spawn stands anywhere, so there is no reach to fit. */
|
|
2632
|
+
NoReach: "no_reach"
|
|
2633
|
+
};
|
|
2634
|
+
var CELLS_PER_RADIUS = 4;
|
|
2635
|
+
var MAX_LATTICE_CELLS = 16e8;
|
|
2636
|
+
var SAME_FLOOR_HEIGHTS = 1 / 3;
|
|
2637
|
+
var MAX_REACH_LOST_PCT = 2;
|
|
2638
|
+
var MAX_REPAIR_ROUNDS = 8;
|
|
2639
|
+
var FLOOR_P99_RADII = 0.5;
|
|
2640
|
+
var FLOOR_MAX_RADII = 1;
|
|
2641
|
+
var MAX_LEAN_FLOODS = 4;
|
|
2642
|
+
var BOUND_ACTOR = "Bound";
|
|
2643
|
+
var REACH_FLAG_PCT = 25;
|
|
2644
|
+
var GROUND_PLAN_SHARE = 0.25;
|
|
2645
|
+
var REFUSAL_NAMES = 5;
|
|
2646
|
+
var REPAIR_RADII = 2;
|
|
2647
|
+
var COARSE_CELLS = 1 << 28;
|
|
2648
|
+
var CROP_PAD_RADII = 6;
|
|
2649
|
+
var FLOOR_SAMPLES = 2e6;
|
|
2650
|
+
var REPORT_FLOOR_WORST = 8;
|
|
2651
|
+
var NARROW_MATCH_CELLS = 2;
|
|
2652
|
+
var REPORT_ACTORS = 12;
|
|
2653
|
+
var REPORT_CUT_PROPS = 64;
|
|
2654
|
+
var SOURCE_UNITS_PER_METER = 39.37;
|
|
2655
|
+
var SOURCE_HULL = { radius: 16, height: 72, crouchHeight: 54, step: 18, jump: 57, leap: 189 };
|
|
2656
|
+
function sceneHull(scene) {
|
|
2657
|
+
if (scene.hull !== void 0) return scene.hull;
|
|
2658
|
+
const k = scene.unitsPerMeter / SOURCE_UNITS_PER_METER;
|
|
2659
|
+
return { radius: SOURCE_HULL.radius * k, height: SOURCE_HULL.height * k, crouchHeight: SOURCE_HULL.crouchHeight * k, step: SOURCE_HULL.step * k, jump: SOURCE_HULL.jump * k, leap: SOURCE_HULL.leap * k };
|
|
2660
|
+
}
|
|
2661
|
+
function fitWorldHash(boxes, ramps) {
|
|
2662
|
+
const scratch = new DataView(new ArrayBuffer(4));
|
|
2663
|
+
let hash = 2166136261;
|
|
2664
|
+
const u32 = (v) => {
|
|
2665
|
+
for (let shift = 0; shift < 32; shift += 8) hash = Math.imul(hash ^ v >>> shift & 255, 16777619) >>> 0;
|
|
2666
|
+
};
|
|
2667
|
+
const f32 = (v) => {
|
|
2668
|
+
scratch.setFloat32(0, v === 0 ? 0 : v, true);
|
|
2669
|
+
for (let b = 0; b < 4; b++) hash = Math.imul(hash ^ scratch.getUint8(b), 16777619) >>> 0;
|
|
2670
|
+
};
|
|
2671
|
+
u32(boxes.length);
|
|
2672
|
+
u32(ramps.length);
|
|
2673
|
+
for (const solid of [...boxes, ...ramps]) solid.forEach(f32);
|
|
2674
|
+
return hash;
|
|
2675
|
+
}
|
|
2676
|
+
function sceneBounds(scene) {
|
|
2677
|
+
const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
2678
|
+
const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
2679
|
+
const add = (p) => {
|
|
2680
|
+
for (let a = 0; a < 3; a++) {
|
|
2681
|
+
lo[a] = Math.min(lo[a], p[a]);
|
|
2682
|
+
hi[a] = Math.max(hi[a], p[a]);
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
for (const actor of scene.actors) {
|
|
2686
|
+
for (const surface of actor.surfaces) {
|
|
2687
|
+
if (!blocksHull(actor, surface)) continue;
|
|
2688
|
+
const box = surfaceBounds(surface);
|
|
2689
|
+
add(box.min);
|
|
2690
|
+
add(box.max);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
for (const spawn of scene.spawns) add(spawn.at);
|
|
2694
|
+
return { min: [lo[0], lo[1], lo[2]], max: [hi[0], hi[1], hi[2]] };
|
|
2695
|
+
}
|
|
2696
|
+
var spawnsOf = (scene) => scene.spawns.map((s) => s.at);
|
|
2697
|
+
function assertWorld(boxes, ramps) {
|
|
2698
|
+
const good = (s) => s.every((v) => Number.isFinite(v) && Math.fround(v) === v) && s[0] < s[3] && s[1] < s[4] && s[2] < s[5];
|
|
2699
|
+
boxes.forEach((b, i) => {
|
|
2700
|
+
if (!good(b)) throw new RangeError(`world fit: box ${i} is not one a room accepts: ${b.join(" ")}`);
|
|
2701
|
+
});
|
|
2702
|
+
ramps.forEach((r, i) => {
|
|
2703
|
+
if (!good(r)) throw new RangeError(`world fit: ramp ${i} is not one a room accepts: ${r.join(" ")}`);
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
function cropToReach(scene, hull) {
|
|
2707
|
+
const whole = sceneBounds(scene);
|
|
2708
|
+
const size = [whole.max[0] - whole.min[0], whole.max[1] - whole.min[1], whole.max[2] - whole.min[2]];
|
|
2709
|
+
const pad = hull.radius * CROP_PAD_RADII + hull.leap;
|
|
2710
|
+
let h = hull.radius;
|
|
2711
|
+
while (size[0] / h * (size[1] / h) * (size[2] / h) > COARSE_CELLS) h *= 2;
|
|
2712
|
+
const l = makeLattice(h, [whole.min[0] - h, whole.min[1] - h, whole.min[2] - hull.height], [whole.max[0] + h, whole.max[1] + h, whole.max[2] + hull.height * 2]);
|
|
2713
|
+
const foot = newLayer(l);
|
|
2714
|
+
rasterScene(l, foot, hull.radius / 2, scene.actors);
|
|
2715
|
+
const cells = { stand: Math.max(1, Math.floor(hull.height / h)), crouch: Math.max(1, Math.floor(hull.crouchHeight / h)), climb: Math.ceil((hull.jump + hull.height - hull.crouchHeight) / h), jump: Math.ceil(hull.jump / h), step: Math.ceil(hull.step / h), leap: Math.ceil(hull.leap / h) };
|
|
2716
|
+
const reach = floodReach(l, foot, cells, spawnsOf(scene));
|
|
2717
|
+
if (reach.count === 0) return whole;
|
|
2718
|
+
let minI = l.nx;
|
|
2719
|
+
let maxI = 0;
|
|
2720
|
+
let minJ = l.ny;
|
|
2721
|
+
let maxJ = 0;
|
|
2722
|
+
let minK = l.nz;
|
|
2723
|
+
let maxK = 0;
|
|
2724
|
+
for (let n = 0; n < reach.count; n++) {
|
|
2725
|
+
const node = reach.nodes[n];
|
|
2726
|
+
const k = node % l.nz;
|
|
2727
|
+
const col = (node - k) / l.nz;
|
|
2728
|
+
const j = Math.floor(col / l.nx);
|
|
2729
|
+
const i = col - j * l.nx;
|
|
2730
|
+
if (i < minI) minI = i;
|
|
2731
|
+
if (i > maxI) maxI = i;
|
|
2732
|
+
if (j < minJ) minJ = j;
|
|
2733
|
+
if (j > maxJ) maxJ = j;
|
|
2734
|
+
if (k < minK) minK = k;
|
|
2735
|
+
if (k > maxK) maxK = k;
|
|
2736
|
+
}
|
|
2737
|
+
return {
|
|
2738
|
+
min: [l.x0 + minI * h - pad, l.y0 + minJ * h - pad, l.z0 + minK * h - pad],
|
|
2739
|
+
max: [l.x0 + (maxI + 1) * h + pad, l.y0 + (maxJ + 1) * h + pad, l.z0 + (maxK + 1) * h + hull.height + hull.jump + hull.height]
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function chooseCell(bounds, hull, asked) {
|
|
2743
|
+
if (asked !== void 0) return asked;
|
|
2744
|
+
let h = hull.radius / CELLS_PER_RADIUS;
|
|
2745
|
+
const cells = (c) => (bounds.max[0] - bounds.min[0]) / c * ((bounds.max[1] - bounds.min[1]) / c) * ((bounds.max[2] - bounds.min[2]) / c);
|
|
2746
|
+
while (cells(h) > MAX_LATTICE_CELLS) h *= 2;
|
|
2747
|
+
return h;
|
|
2748
|
+
}
|
|
2749
|
+
function fitRungs(hull, h) {
|
|
2750
|
+
const maxRange = Math.max(1, Math.floor(hull.step / h));
|
|
2751
|
+
const maxSlack = Math.max(0, Math.floor(hull.radius / h) - 1);
|
|
2752
|
+
const steps = [[1, 0], [1, 1], [2, 1], [2, 2], [3, 2], [3, 3], [4, 3]];
|
|
2753
|
+
const rungs = [];
|
|
2754
|
+
for (const [range, slack] of steps) {
|
|
2755
|
+
const rung = { floorRange: Math.min(range, maxRange), slack: Math.min(slack, maxSlack), props: false };
|
|
2756
|
+
const last = rungs[rungs.length - 1];
|
|
2757
|
+
if (last === void 0 || last.floorRange !== rung.floorRange || last.slack !== rung.slack) rungs.push(rung);
|
|
2758
|
+
}
|
|
2759
|
+
const coarsest = rungs[rungs.length - 1];
|
|
2760
|
+
rungs.push({ ...coarsest, props: true });
|
|
2761
|
+
return rungs;
|
|
2762
|
+
}
|
|
2763
|
+
function attempt(ctx, rung, zone, bounded) {
|
|
2764
|
+
const { input, l, options } = ctx;
|
|
2765
|
+
const placed = { boxes: [], boxInfo: [], ramps: [], rampInfo: [] };
|
|
2766
|
+
const knobs = { floorRange: rung.floorRange, slack: rung.slack, maxRamps: options.maxRamps, boundedActors: bounded, zone };
|
|
2767
|
+
const covered = newLayer(l);
|
|
2768
|
+
const exactSolid = newLayer(l);
|
|
2769
|
+
placeExactBoxes(input, knobs, placed, covered, exactSolid);
|
|
2770
|
+
placeFloors(input, knobs, ctx.footprintCells, ctx.cells.step, ctx.index, placed, covered, exactSolid, ctx.floors);
|
|
2771
|
+
const must = mustCover(input, knobs, covered);
|
|
2772
|
+
const left = Math.max(0, options.maxBoxes - placed.boxes.length);
|
|
2773
|
+
const cover = placeCover(input, must, ctx.footprintCells, ctx.index, placed, left, rung.slack === 0);
|
|
2774
|
+
closeSlits(input, placed, ctx.cells.crouch * l.h);
|
|
2775
|
+
return { placed, overLimit: cover.overLimit || placed.boxes.length > options.maxBoxes, cover };
|
|
2776
|
+
}
|
|
2777
|
+
function checkView(ctx, placed) {
|
|
2778
|
+
const { l, hull, cells } = ctx;
|
|
2779
|
+
const margin = l.h * GAP_MARGIN_CELLS;
|
|
2780
|
+
const foot = rasterWorld(l, hull.radius, placed);
|
|
2781
|
+
const narrower = rasterWorld(l, hull.radius - margin, placed);
|
|
2782
|
+
const solids = [...placed.boxes, ...placed.ramps].map((b) => ({ kind: "box", center: [(b[0] + b[3]) / 2, (b[1] + b[4]) / 2, (b[2] + b[5]) / 2], half: [(b[3] - b[0]) / 2, (b[4] - b[1]) / 2, (b[5] - b[2]) / 2] }));
|
|
2783
|
+
const { opened: fitOpen } = narrowPassages(l, [{ name: "fit", tags: [], surfaces: solids }], hull.radius - margin, cells.crouch, narrower);
|
|
2784
|
+
const opened = newLayer(l);
|
|
2785
|
+
for (let at = 0; at < foot.length; at++) {
|
|
2786
|
+
const left = (fitOpen[at] & ~ctx.sourceStrips[at]) >>> 0;
|
|
2787
|
+
opened[at] = (ctx.opened[at] | left) >>> 0;
|
|
2788
|
+
foot[at] = (foot[at] & ~opened[at]) >>> 0;
|
|
2789
|
+
}
|
|
2790
|
+
return { foot, opened };
|
|
2791
|
+
}
|
|
2792
|
+
function check(ctx, placed, rung) {
|
|
2793
|
+
const { l, cells, truth, input } = ctx;
|
|
2794
|
+
const { foot, opened } = checkView(ctx, placed);
|
|
2795
|
+
const reach = floodReach(l, foot, cells, ctx.spawns);
|
|
2796
|
+
const sourceNear = dilateXY(l, input.foot, rung.slack + 1);
|
|
2797
|
+
const compare = compareReach(l, truth, reach, ctx.tolerance, ctx.scene.unitsPerMeter, sourceNear, rung.slack + 1);
|
|
2798
|
+
const { floor, over } = measureFloors(ctx, placed, reach, opened);
|
|
2799
|
+
return { reach, compare, opened, floor, floorOver: over };
|
|
2800
|
+
}
|
|
2801
|
+
function growZone(ctx, zone, compare, floorOver, rung) {
|
|
2802
|
+
const { l, input, hull, cells } = ctx;
|
|
2803
|
+
const { nx, nz, w, h } = l;
|
|
2804
|
+
const out = zone === null ? newLayer(l) : zone;
|
|
2805
|
+
const radius = Math.ceil(hull.radius * REPAIR_RADII / h);
|
|
2806
|
+
const near = rung.slack > 0 ? dilateXY(l, input.body, rung.slack + 1) : input.body;
|
|
2807
|
+
let added = 0;
|
|
2808
|
+
const add = (node, slackAllowed, reachCells = radius) => {
|
|
2809
|
+
const k = node % nz;
|
|
2810
|
+
const col = (node - k) / nz;
|
|
2811
|
+
if (slackAllowed && (near[col * w + (k >>> 5)] >>> (k & 31) & 1) === 1) return;
|
|
2812
|
+
added += 1;
|
|
2813
|
+
const j = Math.floor(col / nx);
|
|
2814
|
+
const i = col - j * nx;
|
|
2815
|
+
for (let y = Math.max(0, j - reachCells); y <= Math.min(l.ny - 1, j + reachCells); y++) {
|
|
2816
|
+
for (let x = Math.max(0, i - reachCells); x <= Math.min(nx - 1, i + reachCells); x++) setRun(l, out, (y * nx + x) * w, k - cells.stand, k + cells.stand);
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
for (const node of compare.lost) add(node, false);
|
|
2820
|
+
for (const node of compare.extra) add(node, true);
|
|
2821
|
+
const footprint = Math.ceil(hull.radius / h);
|
|
2822
|
+
for (const node of floorOver) add(node, false, footprint);
|
|
2823
|
+
return { zone: out, added };
|
|
2824
|
+
}
|
|
2825
|
+
function actorCosts(ctx, placed) {
|
|
2826
|
+
const { scene, l, truth, hull } = ctx;
|
|
2827
|
+
const boxes = new Int32Array(scene.actors.length);
|
|
2828
|
+
const ramps = new Int32Array(scene.actors.length);
|
|
2829
|
+
for (const info of placed.boxInfo) if (info.actor >= 0) boxes[info.actor] += 1;
|
|
2830
|
+
for (const info of placed.rampInfo) if (info.actor >= 0) ramps[info.actor] += 1;
|
|
2831
|
+
const order = Array.from({ length: scene.actors.length }, (_, i) => i).sort((a, b) => boxes[b] + ramps[b] - (boxes[a] + ramps[a]) || a - b).slice(0, REPORT_ACTORS);
|
|
2832
|
+
const out = [];
|
|
2833
|
+
for (const index of order) {
|
|
2834
|
+
if (boxes[index] === 0 && ramps[index] === 0) continue;
|
|
2835
|
+
const actor = scene.actors[index];
|
|
2836
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2837
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2838
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2839
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2840
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
2841
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
2842
|
+
for (const surface of actor.surfaces) {
|
|
2843
|
+
if (!blocksHull(actor, surface)) continue;
|
|
2844
|
+
const b = surfaceBounds(surface);
|
|
2845
|
+
minX = Math.min(minX, b.min[0]);
|
|
2846
|
+
minY = Math.min(minY, b.min[1]);
|
|
2847
|
+
minZ = Math.min(minZ, b.min[2]);
|
|
2848
|
+
maxX = Math.max(maxX, b.max[0]);
|
|
2849
|
+
maxY = Math.max(maxY, b.max[1]);
|
|
2850
|
+
maxZ = Math.max(maxZ, b.max[2]);
|
|
2851
|
+
}
|
|
2852
|
+
let inside = 0;
|
|
2853
|
+
for (let n = 0; n < truth.count; n++) {
|
|
2854
|
+
const node = truth.nodes[n];
|
|
2855
|
+
const k = node % l.nz;
|
|
2856
|
+
const col = (node - k) / l.nz;
|
|
2857
|
+
const j = Math.floor(col / l.nx);
|
|
2858
|
+
const i = col - j * l.nx;
|
|
2859
|
+
const x = l.x0 + (i + 0.5) * l.h;
|
|
2860
|
+
const y = l.y0 + (j + 0.5) * l.h;
|
|
2861
|
+
const z = l.z0 + k * l.h;
|
|
2862
|
+
const overlaps = x > minX - hull.radius && x < maxX + hull.radius && y > minY - hull.radius && y < maxY + hull.radius && z < maxZ && z + hull.height > minZ;
|
|
2863
|
+
if (overlaps) inside += 1;
|
|
2864
|
+
}
|
|
2865
|
+
out.push({ name: actor.name, boxes: boxes[index], ramps: ramps[index], reachIfBoxedPct: Math.round(1e4 * inside / Math.max(1, truth.count)) / 100 });
|
|
2866
|
+
}
|
|
2867
|
+
return out;
|
|
2868
|
+
}
|
|
2869
|
+
function measureFloors(ctx, placed, fitted, viewOpened) {
|
|
2870
|
+
const { l, scene, hull, tolerance } = ctx;
|
|
2871
|
+
const { nx, ny, nz, h } = l;
|
|
2872
|
+
const stride = Math.max(1, Math.ceil(fitted.count / FLOOR_SAMPLES));
|
|
2873
|
+
const count = Math.floor(fitted.count / stride);
|
|
2874
|
+
const head = new Int32Array(nx * ny).fill(-1);
|
|
2875
|
+
const next = new Int32Array(count);
|
|
2876
|
+
const feet = new Float32Array(count);
|
|
2877
|
+
for (let s = 0; s < count; s++) {
|
|
2878
|
+
const node = fitted.nodes[s * stride];
|
|
2879
|
+
const k = node % nz;
|
|
2880
|
+
const col = (node - k) / nz;
|
|
2881
|
+
feet[s] = l.z0 + k * h;
|
|
2882
|
+
if (firstSet(l, viewOpened, col * l.w, k, k + ctx.cells.crouch) >= 0) continue;
|
|
2883
|
+
if (k > 0 && testBit(ctx.leanCells, col * l.w, k - 1)) continue;
|
|
2884
|
+
next[s] = head[col];
|
|
2885
|
+
head[col] = s;
|
|
2886
|
+
}
|
|
2887
|
+
const margin = tolerance * h;
|
|
2888
|
+
const offer = (tops) => (col, top) => {
|
|
2889
|
+
for (let s = head[col]; s >= 0; s = next[s]) {
|
|
2890
|
+
if (top <= feet[s] + margin && top > tops[s]) tops[s] = top;
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
const fresh = () => new Float32Array(count).fill(Number.NEGATIVE_INFINITY);
|
|
2894
|
+
const sourceTops = (half) => {
|
|
2895
|
+
const tops = fresh();
|
|
2896
|
+
visitTops(l, half, scene.actors, offer(tops));
|
|
2897
|
+
return tops;
|
|
2898
|
+
};
|
|
2899
|
+
const worldTops = (half) => {
|
|
2900
|
+
const tops = fresh();
|
|
2901
|
+
const onWorld = offer(tops);
|
|
2902
|
+
const columnsOf = (min, max, origin, size) => [Math.max(0, cellFloor((min - half - origin) / h - 0.5) + 1), Math.min(size - 1, cellCeil((max + half - origin) / h - 0.5) - 1)];
|
|
2903
|
+
for (const b of placed.boxes) {
|
|
2904
|
+
const [i0, i1] = columnsOf(b[0], b[3], l.x0, nx);
|
|
2905
|
+
const [j0, j1] = columnsOf(b[1], b[4], l.y0, ny);
|
|
2906
|
+
for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) onWorld(j * nx + i, b[5]);
|
|
2907
|
+
}
|
|
2908
|
+
for (const r of placed.ramps) {
|
|
2909
|
+
const [i0, i1] = columnsOf(r[0], r[3], l.x0, nx);
|
|
2910
|
+
const [j0, j1] = columnsOf(r[1], r[4], l.y0, ny);
|
|
2911
|
+
const axis = r[6];
|
|
2912
|
+
const m0 = axis === 0 ? r[0] : r[1];
|
|
2913
|
+
const m1 = axis === 0 ? r[3] : r[4];
|
|
2914
|
+
for (let j = j0; j <= j1; j++) {
|
|
2915
|
+
for (let i = i0; i <= i1; i++) {
|
|
2916
|
+
const centre = axis === 0 ? l.x0 + (i + 0.5) * h : l.y0 + (j + 0.5) * h;
|
|
2917
|
+
const uphill = r[8] >= r[7] ? Math.min(m1, centre + half) : Math.max(m0, centre - half);
|
|
2918
|
+
onWorld(j * nx + i, r[7] + (r[8] - r[7]) * (uphill - m0) / (m1 - m0));
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
return tops;
|
|
2923
|
+
};
|
|
2924
|
+
const { radius } = hull;
|
|
2925
|
+
const source = sourceTops(radius);
|
|
2926
|
+
const world = worldTops(radius);
|
|
2927
|
+
const sourceWide = sourceTops(radius + h / 2);
|
|
2928
|
+
const sourceNarrow = sourceTops(Math.max(h / 2, radius - h / 2));
|
|
2929
|
+
const worldWide = worldTops(radius + h / 2);
|
|
2930
|
+
const worldNarrow = worldTops(Math.max(h / 2, radius - h / 2));
|
|
2931
|
+
const errors = [];
|
|
2932
|
+
const order = [];
|
|
2933
|
+
const over = [];
|
|
2934
|
+
const overUnits = FLOOR_MAX_RADII * radius;
|
|
2935
|
+
let above = 0;
|
|
2936
|
+
let upperBound = 0;
|
|
2937
|
+
let edgeFree = 0;
|
|
2938
|
+
for (let s = 0; s < count; s++) {
|
|
2939
|
+
const a = source[s];
|
|
2940
|
+
const b = world[s];
|
|
2941
|
+
const fitHigh = worldWide[s];
|
|
2942
|
+
const sourceHigh = sourceWide[s];
|
|
2943
|
+
const fitLow = Math.max(worldNarrow[s], Number.NEGATIVE_INFINITY);
|
|
2944
|
+
const sourceLow = Math.max(sourceNarrow[s], Number.NEGATIVE_INFINITY);
|
|
2945
|
+
if (fitHigh !== Number.NEGATIVE_INFINITY && sourceHigh !== Number.NEGATIVE_INFINITY) {
|
|
2946
|
+
const lowOfFit = fitLow === Number.NEGATIVE_INFINITY ? fitHigh : fitLow;
|
|
2947
|
+
const lowOfSource = sourceLow === Number.NEGATIVE_INFINITY ? sourceHigh : sourceLow;
|
|
2948
|
+
upperBound = Math.max(upperBound, fitHigh - lowOfSource, sourceHigh - lowOfFit);
|
|
2949
|
+
edgeFree = Math.max(edgeFree, lowOfFit - sourceHigh, lowOfSource - fitHigh);
|
|
2950
|
+
}
|
|
2951
|
+
if (a === Number.NEGATIVE_INFINITY || b === Number.NEGATIVE_INFINITY) continue;
|
|
2952
|
+
errors.push(Math.abs(b - a));
|
|
2953
|
+
order.push(s);
|
|
2954
|
+
if (Math.abs(b - a) > overUnits) over.push(fitted.nodes[s * stride]);
|
|
2955
|
+
if (b > a) above += 1;
|
|
2956
|
+
}
|
|
2957
|
+
const worstOrder = order.map((s, index) => [errors[index], s]).sort((p, q) => q[0] - p[0]).slice(0, REPORT_FLOOR_WORST);
|
|
2958
|
+
const worst = worstOrder.map(([, s]) => {
|
|
2959
|
+
const node = fitted.nodes[s * stride];
|
|
2960
|
+
const k = node % nz;
|
|
2961
|
+
const col = (node - k) / nz;
|
|
2962
|
+
const j = Math.floor(col / nx);
|
|
2963
|
+
const i = col - j * nx;
|
|
2964
|
+
return { at: [l.x0 + (i + 0.5) * h, l.y0 + (j + 0.5) * h, l.z0 + k * h], source: source[s], fit: world[s] };
|
|
2965
|
+
});
|
|
2966
|
+
errors.sort((p, q) => p - q);
|
|
2967
|
+
const at = (share) => Math.round((errors[Math.min(errors.length - 1, Math.floor(share * errors.length))] ?? 0) * 100) / 100;
|
|
2968
|
+
const floor = { samples: errors.length, p50Units: at(0.5), p95Units: at(0.95), p99Units: at(0.99), maxUnits: at(1), upperBoundUnits: Math.ceil(upperBound * 100) / 100, edgeFreeUnits: Math.ceil(edgeFree * 100) / 100, abovePct: Math.round(1e4 * above / Math.max(1, errors.length)) / 100, worst };
|
|
2969
|
+
return { floor, over };
|
|
2970
|
+
}
|
|
2971
|
+
function stagesOf(placed) {
|
|
2972
|
+
const out = { exact: 0, ramp: 0, tile: 0, cover: 0, prop: 0 };
|
|
2973
|
+
for (const info of placed.boxInfo) out[info.stage] += 1;
|
|
2974
|
+
for (const info of placed.rampInfo) out[info.stage] += 1;
|
|
2975
|
+
return out;
|
|
2976
|
+
}
|
|
2977
|
+
var EMPTY_COMPARE = { truthNodes: 0, fitNodes: 0, lostNodes: 0, extraNodes: 0, lostPct: 0, extraPct: 0, worstLostM2: 0, worstExtraM2: 0, lostBeyondSlackPct: 0, underFloorNodes: 0, newFalls: 0 };
|
|
2978
|
+
function buildReport(ctx, parts, started) {
|
|
2979
|
+
const { placed, rung, checked } = parts;
|
|
2980
|
+
const { scene, options, l, input, truth } = ctx;
|
|
2981
|
+
const unreachable = /* @__PURE__ */ new Set();
|
|
2982
|
+
for (let a = 0; a < scene.actors.length; a++) unreachable.add(a);
|
|
2983
|
+
for (const info of placed.boxInfo) unreachable.delete(info.actor);
|
|
2984
|
+
for (const info of placed.rampInfo) unreachable.delete(info.actor);
|
|
2985
|
+
const { lost: _lost, extra: _extra, ...compare } = checked?.compare ?? { ...EMPTY_COMPARE, lost: [], extra: [] };
|
|
2986
|
+
return {
|
|
2987
|
+
status: parts.status,
|
|
2988
|
+
detail: parts.detail,
|
|
2989
|
+
budget: { maxBoxes: options.maxBoxes, maxRamps: options.maxRamps },
|
|
2990
|
+
boxes: placed.boxes.length,
|
|
2991
|
+
ramps: placed.ramps.length,
|
|
2992
|
+
stages: stagesOf(placed),
|
|
2993
|
+
cell: l.h,
|
|
2994
|
+
rung,
|
|
2995
|
+
floor: checked === null ? null : checked.floor,
|
|
2996
|
+
floorTileSpanUnits: (rung.floorRange + 1) * l.h,
|
|
2997
|
+
wallSlackUnits: rung.slack * l.h,
|
|
2998
|
+
reach: { ...compare, truthFellOut: truth.fellOut, fitFellOut: checked?.reach.fellOut ?? 0 },
|
|
2999
|
+
gapMarginUnits: l.h * GAP_MARGIN_CELLS,
|
|
3000
|
+
openSlits: countOpenSlits(input, placed, ctx.cells.crouch * l.h),
|
|
3001
|
+
narrowDoorsShut: ctx.shut,
|
|
3002
|
+
leanLedges: ctx.leanLedges,
|
|
3003
|
+
reachExtent: ctx.extent,
|
|
3004
|
+
repairs: parts.repairs,
|
|
3005
|
+
unreachableActors: unreachable.size,
|
|
3006
|
+
movableActors: scene.actors.filter((a) => a.tags.includes(SceneTag.Movable)).map((a) => a.name),
|
|
3007
|
+
cutProps: [...parts.bounded].slice(0, REPORT_CUT_PROPS).map((i) => scene.actors[i].name),
|
|
3008
|
+
actors: actorCosts(ctx, placed),
|
|
3009
|
+
spawnsNotPlaced: truth.unplaced.map((i) => scene.spawns[i]?.name ?? `spawn ${i}`),
|
|
3010
|
+
seconds: Math.round((performance.now() - started) / 100) / 10
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
function fallsThrough(checked) {
|
|
3014
|
+
return checked.compare.underFloorNodes > 0 || checked.compare.newFalls > 0;
|
|
3015
|
+
}
|
|
3016
|
+
function sameDifferences(a, b) {
|
|
3017
|
+
return a.compare.lostNodes === b.compare.lostNodes && a.compare.extraNodes === b.compare.extraNodes && a.floorOver.length === b.floorOver.length;
|
|
3018
|
+
}
|
|
3019
|
+
function runRung(ctx, rung, bounded) {
|
|
3020
|
+
const { options } = ctx;
|
|
3021
|
+
let zone = null;
|
|
3022
|
+
let repairs = 0;
|
|
3023
|
+
let last = null;
|
|
3024
|
+
for (let round = 0; round <= (options.noRepair ? 0 : MAX_REPAIR_ROUNDS); round++) {
|
|
3025
|
+
const run = attempt(ctx, rung, zone, bounded);
|
|
3026
|
+
options.log?.(`rung range ${rung.floorRange} slack ${rung.slack} props ${rung.props} round ${round}: ${run.placed.boxes.length} boxes, ${run.placed.ramps.length} ramps${run.overLimit ? " (over)" : ""}, ${run.cover.stuck} stuck`);
|
|
3027
|
+
if (run.overLimit) return last;
|
|
3028
|
+
const checked = check(ctx, run.placed, rung);
|
|
3029
|
+
options.log?.(` lost ${checked.compare.lostPct}% extra ${checked.compare.extraPct}% under ${checked.compare.underFloorNodes} falls ${checked.compare.newFalls} floor ${checked.floor.p99Units}/${checked.floor.maxUnits} over ${checked.floorOver.length}`);
|
|
3030
|
+
const standing = last !== null && sameDifferences(last.checked, checked);
|
|
3031
|
+
last = { placed: run.placed, checked, repairs };
|
|
3032
|
+
if (options.noRepair || standing) return last;
|
|
3033
|
+
const grown = growZone(ctx, zone, checked.compare, checked.floorOver, rung);
|
|
3034
|
+
if (grown.added === 0) return last;
|
|
3035
|
+
zone = grown.zone;
|
|
3036
|
+
repairs += 1;
|
|
3037
|
+
}
|
|
3038
|
+
return last;
|
|
3039
|
+
}
|
|
3040
|
+
function prepare(given, options) {
|
|
3041
|
+
const hull = sceneHull(given);
|
|
3042
|
+
const scene = withBound(given, hull);
|
|
3043
|
+
const crop = cropToReach(scene, hull);
|
|
3044
|
+
const whole = sceneBounds(scene);
|
|
3045
|
+
const bounds = {
|
|
3046
|
+
min: [Math.max(crop.min[0], whole.min[0] - hull.radius * CROP_PAD_RADII), Math.max(crop.min[1], whole.min[1] - hull.radius * CROP_PAD_RADII), Math.max(crop.min[2], whole.min[2] - hull.height)],
|
|
3047
|
+
max: [Math.min(crop.max[0], whole.max[0] + hull.radius * CROP_PAD_RADII), Math.min(crop.max[1], whole.max[1] + hull.radius * CROP_PAD_RADII), crop.max[2]]
|
|
3048
|
+
};
|
|
3049
|
+
const h = chooseCell(bounds, hull, options.cell);
|
|
3050
|
+
const l = makeLattice(h, bounds.min, bounds.max);
|
|
3051
|
+
options.log?.(`lattice ${l.nx} x ${l.ny} x ${l.nz} at ${h}: ${Math.round(cellCount(l) / 1e6)}M cells`);
|
|
3052
|
+
const foot = newLayer(l);
|
|
3053
|
+
rasterScene(l, foot, hull.radius, scene.actors);
|
|
3054
|
+
const solid = newLayer(l);
|
|
3055
|
+
rasterScene(l, solid, h / 2, scene.actors);
|
|
3056
|
+
const cells = hullCells(hull, h);
|
|
3057
|
+
const { opened, strips, shut } = narrowPassages(l, scene.actors, hull.radius, cells.crouch, foot);
|
|
3058
|
+
let openedDoors = 0;
|
|
3059
|
+
for (let at = 0; at < foot.length; at++) {
|
|
3060
|
+
if (opened[at] !== 0) openedDoors += 1;
|
|
3061
|
+
foot[at] = (foot[at] & ~opened[at]) >>> 0;
|
|
3062
|
+
}
|
|
3063
|
+
options.log?.(`narrow passages: ${openedDoors} cell words opened`);
|
|
3064
|
+
const spawns = spawnsOf(scene);
|
|
3065
|
+
let truth = floodReach(l, foot, cells, spawns);
|
|
3066
|
+
let leanLedges = 0;
|
|
3067
|
+
const leanCells = newLayer(l);
|
|
3068
|
+
for (let flood = 0; flood < MAX_LEAN_FLOODS; flood++) {
|
|
3069
|
+
let closedNow = 0;
|
|
3070
|
+
for (let closed = closeLeanLedges(l, foot, leanCells, truth, cells); closed > 0; closed = closeLeanLedges(l, foot, leanCells, truth, cells)) closedNow += closed;
|
|
3071
|
+
if (closedNow === 0) break;
|
|
3072
|
+
leanLedges += closedNow;
|
|
3073
|
+
truth = floodReach(l, foot, cells, spawns);
|
|
3074
|
+
}
|
|
3075
|
+
options.log?.(`truth ${truth.count} positions, ${truth.fellOut} fell out, ${leanLedges} on leaning faces closed`);
|
|
3076
|
+
const footprintCells = Math.max(1, cellCeil(2 * hull.radius / h));
|
|
3077
|
+
const envelope = dilateXY(l, truth.body, cellCeil(hull.radius / h));
|
|
3078
|
+
const input = { l, actors: scene.actors, radius: hull.radius, foot, solid, body: truth.body, touch: strips, envelope, leanCells };
|
|
3079
|
+
return {
|
|
3080
|
+
scene,
|
|
3081
|
+
options,
|
|
3082
|
+
hull,
|
|
3083
|
+
l,
|
|
3084
|
+
cells,
|
|
3085
|
+
input,
|
|
3086
|
+
opened,
|
|
3087
|
+
sourceStrips: dilateXY(l, strips, NARROW_MATCH_CELLS),
|
|
3088
|
+
shut,
|
|
3089
|
+
floors: readFloors(input),
|
|
3090
|
+
truth,
|
|
3091
|
+
leanLedges,
|
|
3092
|
+
leanCells,
|
|
3093
|
+
extent: measureExtent(scene, hull, l, cells, foot, truth, spawns),
|
|
3094
|
+
footprintCells,
|
|
3095
|
+
index: new ActorIndex(scene.actors, hull.radius * 16),
|
|
3096
|
+
tolerance: Math.ceil(hull.height * SAME_FLOOR_HEIGHTS / h),
|
|
3097
|
+
propIndices: propActors(scene.actors, hull.radius),
|
|
3098
|
+
// A spawn with no room to stand in the source is named in the report. The check starts where the source has hulls, or
|
|
3099
|
+
// that spawn's pocket, uncovered because nothing reaches it, would read as reach the fit added.
|
|
3100
|
+
spawns: spawns.filter((_, index) => !truth.unplaced.includes(index))
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function withBound(scene, hull) {
|
|
3104
|
+
const { bound } = scene;
|
|
3105
|
+
if (bound === void 0) return scene;
|
|
3106
|
+
const whole = sceneBounds(scene);
|
|
3107
|
+
const t = hull.radius * 2;
|
|
3108
|
+
const bottom = whole.min[2] - hull.height;
|
|
3109
|
+
const top = bound.ceiling ?? whole.max[2] + hull.height * 2 + hull.jump;
|
|
3110
|
+
const [x0, y0] = bound.min;
|
|
3111
|
+
const [x1, y1] = bound.max;
|
|
3112
|
+
const slab = (ax, ay, az, bx, by, bz) => ({
|
|
3113
|
+
kind: "box",
|
|
3114
|
+
center: [(ax + bx) / 2, (ay + by) / 2, (az + bz) / 2],
|
|
3115
|
+
half: [(bx - ax) / 2, (by - ay) / 2, (bz - az) / 2],
|
|
3116
|
+
channels: [SceneChannel.Move]
|
|
3117
|
+
});
|
|
3118
|
+
const surfaces = [
|
|
3119
|
+
slab(x0 - t, y0 - t, bottom, x0, y1 + t, top + t),
|
|
3120
|
+
slab(x1, y0 - t, bottom, x1 + t, y1 + t, top + t),
|
|
3121
|
+
slab(x0, y0 - t, bottom, x1, y0, top + t),
|
|
3122
|
+
slab(x0, y1, bottom, x1, y1 + t, top + t)
|
|
3123
|
+
];
|
|
3124
|
+
if (bound.ceiling !== void 0) surfaces.push(slab(x0, y0, bound.ceiling, x1, y1, bound.ceiling + t));
|
|
3125
|
+
return { ...scene, actors: [...scene.actors, { name: BOUND_ACTOR, tags: [], surfaces }] };
|
|
3126
|
+
}
|
|
3127
|
+
function levelCore(scene, hull) {
|
|
3128
|
+
const whole = sceneBounds(scene);
|
|
3129
|
+
const plan = Math.max(1, (whole.max[0] - whole.min[0]) * (whole.max[1] - whole.min[1]));
|
|
3130
|
+
const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
3131
|
+
const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
3132
|
+
const take = (x0, y0, x1, y1) => {
|
|
3133
|
+
min[0] = Math.min(min[0], x0);
|
|
3134
|
+
min[1] = Math.min(min[1], y0);
|
|
3135
|
+
max[0] = Math.max(max[0], x1);
|
|
3136
|
+
max[1] = Math.max(max[1], y1);
|
|
3137
|
+
};
|
|
3138
|
+
for (const spawn of scene.spawns) take(spawn.at[0], spawn.at[1], spawn.at[0], spawn.at[1]);
|
|
3139
|
+
for (const actor of scene.actors) {
|
|
3140
|
+
const a = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
3141
|
+
const b = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
3142
|
+
for (const surface of actor.surfaces) {
|
|
3143
|
+
if (!blocksHull(actor, surface)) continue;
|
|
3144
|
+
const box = surfaceBounds(surface);
|
|
3145
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
3146
|
+
a[axis] = Math.min(a[axis], box.min[axis]);
|
|
3147
|
+
b[axis] = Math.max(b[axis], box.max[axis]);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
if (a[0] === Number.POSITIVE_INFINITY) continue;
|
|
3151
|
+
const isGround = (b[0] - a[0]) * (b[1] - a[1]) >= plan * GROUND_PLAN_SHARE && b[2] - a[2] < hull.height;
|
|
3152
|
+
if (!isGround) take(a[0], a[1], b[0], b[1]);
|
|
3153
|
+
}
|
|
3154
|
+
return { min, max };
|
|
3155
|
+
}
|
|
3156
|
+
function measureExtent(scene, hull, l, cells, foot, truth, spawns) {
|
|
3157
|
+
const core = levelCore(scene, hull);
|
|
3158
|
+
const cellM2 = (l.h / scene.unitsPerMeter) ** 2;
|
|
3159
|
+
const walked = floodReach(l, foot, { ...cells, jump: 0, climb: 0, leap: 0 }, spawns);
|
|
3160
|
+
const planOf = (node) => {
|
|
3161
|
+
const col = (node - node % l.nz) / l.nz;
|
|
3162
|
+
const j = Math.floor(col / l.nx);
|
|
3163
|
+
return [l.x0 + (col - j * l.nx + 0.5) * l.h, l.y0 + (j + 0.5) * l.h];
|
|
3164
|
+
};
|
|
3165
|
+
const walkedMin = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
3166
|
+
const walkedMax = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
3167
|
+
for (let n = 0; n < walked.count; n++) {
|
|
3168
|
+
const [x, y] = planOf(walked.nodes[n]);
|
|
3169
|
+
walkedMin[0] = Math.min(walkedMin[0], x);
|
|
3170
|
+
walkedMin[1] = Math.min(walkedMin[1], y);
|
|
3171
|
+
walkedMax[0] = Math.max(walkedMax[0], x);
|
|
3172
|
+
walkedMax[1] = Math.max(walkedMax[1], y);
|
|
3173
|
+
}
|
|
3174
|
+
let outside = 0;
|
|
3175
|
+
let outsideWalked = 0;
|
|
3176
|
+
let farthest = 0;
|
|
3177
|
+
for (let n = 0; n < truth.count; n++) {
|
|
3178
|
+
const [x, y] = planOf(truth.nodes[n]);
|
|
3179
|
+
if (x < walkedMin[0] - hull.leap || x > walkedMax[0] + hull.leap || y < walkedMin[1] - hull.leap || y > walkedMax[1] + hull.leap) outsideWalked += 1;
|
|
3180
|
+
const dx = Math.max(core.min[0] - hull.leap - x, x - core.max[0] - hull.leap, 0);
|
|
3181
|
+
const dy = Math.max(core.min[1] - hull.leap - y, y - core.max[1] - hull.leap, 0);
|
|
3182
|
+
if (dx === 0 && dy === 0) continue;
|
|
3183
|
+
outside += 1;
|
|
3184
|
+
farthest = Math.max(farthest, Math.hypot(dx, dy));
|
|
3185
|
+
}
|
|
3186
|
+
const share = (part) => truth.count === 0 ? 0 : Math.round(1e4 * part / truth.count) / 100;
|
|
3187
|
+
const outsideCorePct = share(outside);
|
|
3188
|
+
const beyondWalkPct = share(Math.max(0, truth.count - walked.count));
|
|
3189
|
+
const outsideWalkedPct = share(outsideWalked);
|
|
3190
|
+
const flagged = scene.bound === void 0 && Math.max(outsideCorePct, outsideWalkedPct) >= REACH_FLAG_PCT;
|
|
3191
|
+
return {
|
|
3192
|
+
coreMin: core.min,
|
|
3193
|
+
coreMax: core.max,
|
|
3194
|
+
reachM2: Math.round(truth.count * cellM2),
|
|
3195
|
+
walkedM2: Math.round(walked.count * cellM2),
|
|
3196
|
+
beyondWalkPct,
|
|
3197
|
+
outsideWalkedPct,
|
|
3198
|
+
outsideCoreM2: Math.round(outside * cellM2),
|
|
3199
|
+
outsideCorePct,
|
|
3200
|
+
farthestFromCoreM: Math.round(farthest / scene.unitsPerMeter * 10) / 10,
|
|
3201
|
+
bounded: scene.bound !== void 0,
|
|
3202
|
+
flagged
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function floorPastGate(floor, radius) {
|
|
3206
|
+
return floor.maxUnits > FLOOR_MAX_RADII * radius || floor.p99Units > FLOOR_P99_RADII * radius;
|
|
3207
|
+
}
|
|
3208
|
+
function sceneReach(scene, cell) {
|
|
3209
|
+
const { hull, l, truth } = prepare(scene, { maxBoxes: 0, maxRamps: 0, ...cell === void 0 ? {} : { cell } });
|
|
3210
|
+
return { hull, l, truth };
|
|
3211
|
+
}
|
|
3212
|
+
var EMPTY_PLACED = { boxes: [], boxInfo: [], ramps: [], rampInfo: [] };
|
|
3213
|
+
function fitScene(scene, options) {
|
|
3214
|
+
const started = performance.now();
|
|
3215
|
+
const ctx = prepare(scene, options);
|
|
3216
|
+
const rungs = options.rung !== void 0 ? [options.rung] : fitRungs(ctx.hull, ctx.l.h);
|
|
3217
|
+
const finish = (parts) => {
|
|
3218
|
+
const report = buildReport(ctx, parts, started);
|
|
3219
|
+
const { boxes, ramps } = parts.placed;
|
|
3220
|
+
if (parts.status === FitStatus.Ok) assertWorld(boxes, ramps);
|
|
3221
|
+
const probe = options.keepProbe ? { probe: { l: ctx.l, truth: ctx.truth, attempted: { boxes, ramps }, fitted: parts.checked?.reach ?? null } } : {};
|
|
3222
|
+
if (parts.status !== FitStatus.Ok) return { report, world: null, ...probe };
|
|
3223
|
+
return { report, world: { boxes, ramps, hash: fitWorldHash(boxes, ramps) }, ...probe };
|
|
3224
|
+
};
|
|
3225
|
+
const noBounded = /* @__PURE__ */ new Set();
|
|
3226
|
+
if (ctx.truth.count === 0) {
|
|
3227
|
+
return finish({ status: FitStatus.NoReach, detail: "no spawn stands anywhere in the scene: nothing to fit from", placed: EMPTY_PLACED, rung: rungs[0], checked: null, repairs: 0, bounded: noBounded });
|
|
3228
|
+
}
|
|
3229
|
+
let best = null;
|
|
3230
|
+
let fellThrough = null;
|
|
3231
|
+
let lostTooMuch = null;
|
|
3232
|
+
let slitsOpen = null;
|
|
3233
|
+
let floorOver = null;
|
|
3234
|
+
const boundedFor = (rung) => rung.props ? new Set(ctx.propIndices) : noBounded;
|
|
3235
|
+
let finest = 0;
|
|
3236
|
+
let coarsest = rungs.length - 1;
|
|
3237
|
+
while (finest < coarsest) {
|
|
3238
|
+
const mid = finest + coarsest >> 1;
|
|
3239
|
+
const rung = rungs[mid];
|
|
3240
|
+
if (attempt(ctx, rung, null, boundedFor(rung)).overLimit) finest = mid + 1;
|
|
3241
|
+
else coarsest = mid;
|
|
3242
|
+
}
|
|
3243
|
+
for (const rung of rungs.slice(finest)) {
|
|
3244
|
+
const bounded2 = boundedFor(rung);
|
|
3245
|
+
const run2 = runRung(ctx, rung, bounded2);
|
|
3246
|
+
if (run2 === null) continue;
|
|
3247
|
+
const entry = { ...run2, rung, bounded: bounded2 };
|
|
3248
|
+
if (fallsThrough(run2.checked)) {
|
|
3249
|
+
fellThrough = entry;
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
if (countOpenSlits(ctx.input, run2.placed, ctx.cells.crouch * ctx.l.h) > 0) {
|
|
3253
|
+
slitsOpen = entry;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
if (floorPastGate(run2.checked.floor, ctx.hull.radius)) {
|
|
3257
|
+
floorOver ??= entry;
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
if (run2.checked.compare.lostBeyondSlackPct > MAX_REACH_LOST_PCT) {
|
|
3261
|
+
lostTooMuch = entry;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
best = entry;
|
|
3265
|
+
break;
|
|
3266
|
+
}
|
|
3267
|
+
if (best !== null) {
|
|
3268
|
+
return finish({ status: FitStatus.Ok, detail: "fits", placed: best.placed, rung: best.rung, checked: best.checked, repairs: best.repairs, bounded: best.bounded });
|
|
3269
|
+
}
|
|
3270
|
+
if (floorOver !== null && lostTooMuch === null && slitsOpen === null) {
|
|
3271
|
+
const { floor } = floorOver.checked;
|
|
3272
|
+
const named2 = actorCosts(ctx, floorOver.placed).slice(0, REFUSAL_NAMES).map((a) => `${a.name} (${a.boxes} boxes, ${a.ramps} ramps)`);
|
|
3273
|
+
const detail2 = `feet stay ${floor.maxUnits} units from the source's floor at worst and ${floor.p99Units} at the 99th percentile, at the finest fit the budget of ${options.maxBoxes} boxes and ${options.maxRamps} ramps holds; a fit may be ${FLOOR_MAX_RADII * ctx.hull.radius} and ${FLOOR_P99_RADII * ctx.hull.radius}. Costliest: ${named2.join(", ")}. Simplify those or raise the budget`;
|
|
3274
|
+
return finish({ status: FitStatus.FloorOver, detail: detail2, placed: floorOver.placed, rung: floorOver.rung, checked: floorOver.checked, repairs: floorOver.repairs, bounded: floorOver.bounded });
|
|
3275
|
+
}
|
|
3276
|
+
const failed = lostTooMuch ?? slitsOpen ?? fellThrough;
|
|
3277
|
+
if (failed !== null) {
|
|
3278
|
+
const status = lostTooMuch !== null ? FitStatus.ReachLost : slitsOpen !== null ? FitStatus.OpenSlits : FitStatus.FallsThrough;
|
|
3279
|
+
const detail2 = status === FitStatus.ReachLost ? `the only fit within budget loses ${failed.checked.compare.lostBeyondSlackPct}% of the reach; a fit may lose ${MAX_REACH_LOST_PCT}%` : status === FitStatus.OpenSlits ? "a gap too narrow for the hull stays open or barely shut between solids in every fit tried: see openSlits" : `a hull gets under the floor in every fit tried: ${failed.checked.compare.underFloorNodes} positions, ${failed.checked.compare.newFalls} places it falls out of the level`;
|
|
3280
|
+
return finish({ status, detail: detail2, placed: failed.placed, rung: failed.rung, checked: failed.checked, repairs: failed.repairs, bounded: failed.bounded });
|
|
3281
|
+
}
|
|
3282
|
+
const last = rungs[rungs.length - 1];
|
|
3283
|
+
const bounded = boundedFor(last);
|
|
3284
|
+
const unlimited = { ...ctx, options: { ...options, maxBoxes: Number.MAX_SAFE_INTEGER } };
|
|
3285
|
+
const run = attempt(unlimited, last, null, bounded);
|
|
3286
|
+
const need = run.placed.boxes.length;
|
|
3287
|
+
const named = actorCosts(ctx, run.placed).slice(0, REFUSAL_NAMES).map((a) => `${a.name} (${a.boxes} boxes, ${a.reachIfBoxedPct}% of the reach if it were one box)`);
|
|
3288
|
+
const detail = `needs ${need} boxes at the coarsest fit; the budget is ${options.maxBoxes}. Costliest: ${named.join(", ")}. Simplify those or raise the budget`;
|
|
3289
|
+
return finish({ status: FitStatus.OverBudget, detail, placed: run.placed, rung: last, checked: null, repairs: 0, bounded });
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// src/world-fit/shot.ts
|
|
3293
|
+
var ShotFitStatus = {
|
|
3294
|
+
Ok: "ok",
|
|
3295
|
+
/** Even the coarsest shot world needs more boxes or bytes than the budget. */
|
|
3296
|
+
OverBudget: "over_budget",
|
|
3297
|
+
/** No spawn stands anywhere, so no muzzle sees anything. */
|
|
3298
|
+
NoReach: "no_reach",
|
|
3299
|
+
/** Within budget, but rays disagree with the source past what a shot world may. */
|
|
3300
|
+
Inaccurate: "inaccurate"
|
|
3301
|
+
};
|
|
3302
|
+
var FINEST_CELL_RADII = 1 / 4;
|
|
3303
|
+
var CELL_RUNGS = 6;
|
|
3304
|
+
var MAX_LATTICE_CELLS2 = 1e9;
|
|
3305
|
+
var DEFAULT_RANGE_RADII = 1024;
|
|
3306
|
+
var DEFAULT_RAYS = 1e5;
|
|
3307
|
+
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
3308
|
+
var SOLID_GROW_CELLS = 1 / 64;
|
|
3309
|
+
var MAX_GROW_CELLS2 = 256;
|
|
3310
|
+
var GRID_CELLS = 1 / 32;
|
|
3311
|
+
var SEAM_GRIDS = 1;
|
|
3312
|
+
var SURFACE_BAND_CELLS = 1;
|
|
3313
|
+
var CLAIM_MULTIPLES = [1, 0.5, 2, 4];
|
|
3314
|
+
var HOSTILE_PAST_RADII = 3;
|
|
3315
|
+
var SURFACE_STEPS_CELLS = [1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 4, 8];
|
|
3316
|
+
var MAX_HIT_MISS_PCT = 5;
|
|
3317
|
+
var LEVEL_RAY_SHARE = 0.5;
|
|
3318
|
+
var LEVEL_RAY_SLOPE = 0.2;
|
|
3319
|
+
var EYE_SHARE = 8 / 9;
|
|
3320
|
+
var CROUCH_EYE_SHARE = 0.85;
|
|
3321
|
+
var CROUCH_RAY_SHARE = 0.25;
|
|
3322
|
+
var RAY_SEED = 20261107;
|
|
3323
|
+
var REPORT_WORST = 8;
|
|
3324
|
+
var REPORT_ACTORS2 = 12;
|
|
3325
|
+
var REPORT_UNSEEN = 64;
|
|
3326
|
+
var LEAF_ITEMS = 4;
|
|
3327
|
+
var STACK_DEPTH = 128;
|
|
3328
|
+
var REFUSAL_COUNT_FACTOR = 4;
|
|
3329
|
+
var REFUSAL_COUNT_LEAST = 4096;
|
|
3330
|
+
var BoundsTree = class {
|
|
3331
|
+
bounds;
|
|
3332
|
+
order;
|
|
3333
|
+
first;
|
|
3334
|
+
second;
|
|
3335
|
+
nodes = 0;
|
|
3336
|
+
constructor(itemBounds, count) {
|
|
3337
|
+
const nodeCap = Math.max(1, 2 * count);
|
|
3338
|
+
this.bounds = new Float64Array(nodeCap * 6);
|
|
3339
|
+
this.order = new Int32Array(count);
|
|
3340
|
+
this.first = new Int32Array(nodeCap);
|
|
3341
|
+
this.second = new Int32Array(nodeCap);
|
|
3342
|
+
for (let i = 0; i < count; i++) this.order[i] = i;
|
|
3343
|
+
if (count > 0) this.build(itemBounds, 0, count);
|
|
3344
|
+
}
|
|
3345
|
+
/** Node n: a leaf holds order[first..second) with first < 0 written as ~first; an inner node its two children. */
|
|
3346
|
+
build(items, from, to) {
|
|
3347
|
+
const node = this.nodes++;
|
|
3348
|
+
const b = this.bounds;
|
|
3349
|
+
const at = node * 6;
|
|
3350
|
+
let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
3351
|
+
let cx0 = Infinity, cy0 = Infinity, cz0 = Infinity, cx1 = -Infinity, cy1 = -Infinity, cz1 = -Infinity;
|
|
3352
|
+
for (let n = from; n < to; n++) {
|
|
3353
|
+
const o = this.order[n] * 6;
|
|
3354
|
+
x0 = Math.min(x0, items[o]);
|
|
3355
|
+
y0 = Math.min(y0, items[o + 1]);
|
|
3356
|
+
z0 = Math.min(z0, items[o + 2]);
|
|
3357
|
+
x1 = Math.max(x1, items[o + 3]);
|
|
3358
|
+
y1 = Math.max(y1, items[o + 4]);
|
|
3359
|
+
z1 = Math.max(z1, items[o + 5]);
|
|
3360
|
+
const cx = items[o] + items[o + 3];
|
|
3361
|
+
const cy = items[o + 1] + items[o + 4];
|
|
3362
|
+
const cz = items[o + 2] + items[o + 5];
|
|
3363
|
+
cx0 = Math.min(cx0, cx);
|
|
3364
|
+
cy0 = Math.min(cy0, cy);
|
|
3365
|
+
cz0 = Math.min(cz0, cz);
|
|
3366
|
+
cx1 = Math.max(cx1, cx);
|
|
3367
|
+
cy1 = Math.max(cy1, cy);
|
|
3368
|
+
cz1 = Math.max(cz1, cz);
|
|
3369
|
+
}
|
|
3370
|
+
b[at] = x0;
|
|
3371
|
+
b[at + 1] = y0;
|
|
3372
|
+
b[at + 2] = z0;
|
|
3373
|
+
b[at + 3] = x1;
|
|
3374
|
+
b[at + 4] = y1;
|
|
3375
|
+
b[at + 5] = z1;
|
|
3376
|
+
if (to - from <= LEAF_ITEMS) {
|
|
3377
|
+
this.first[node] = ~from;
|
|
3378
|
+
this.second[node] = to;
|
|
3379
|
+
return node;
|
|
3380
|
+
}
|
|
3381
|
+
const sizes = [cx1 - cx0, cy1 - cy0, cz1 - cz0];
|
|
3382
|
+
const axis = sizes[0] >= sizes[1] && sizes[0] >= sizes[2] ? 0 : sizes[1] >= sizes[2] ? 1 : 2;
|
|
3383
|
+
const mid = from + to >> 1;
|
|
3384
|
+
this.select(items, axis, from, to - 1, mid);
|
|
3385
|
+
this.first[node] = this.build(items, from, mid);
|
|
3386
|
+
this.second[node] = this.build(items, mid, to);
|
|
3387
|
+
return node;
|
|
3388
|
+
}
|
|
3389
|
+
/** Quickselect on the items' centres: order[k] ends where a full sort would put it. */
|
|
3390
|
+
select(items, axis, lo, hi, k) {
|
|
3391
|
+
const { order } = this;
|
|
3392
|
+
const key = (index) => items[order[index] * 6 + axis] + items[order[index] * 6 + axis + 3];
|
|
3393
|
+
while (lo < hi) {
|
|
3394
|
+
const pivot = key(lo + hi >> 1);
|
|
3395
|
+
let i = lo;
|
|
3396
|
+
let j = hi;
|
|
3397
|
+
while (i <= j) {
|
|
3398
|
+
while (key(i) < pivot) i++;
|
|
3399
|
+
while (key(j) > pivot) j--;
|
|
3400
|
+
if (i <= j) {
|
|
3401
|
+
const swap = order[i];
|
|
3402
|
+
order[i] = order[j];
|
|
3403
|
+
order[j] = swap;
|
|
3404
|
+
i++;
|
|
3405
|
+
j--;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
if (k <= j) hi = j;
|
|
3409
|
+
else if (k >= i) lo = i;
|
|
3410
|
+
else return;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
stack = new Int32Array(STACK_DEPTH);
|
|
3414
|
+
/** Calls `test` with each item whose node the ray may enter before `best()`. */
|
|
3415
|
+
walk(ox, oy, oz, ix, iy, iz, best, test) {
|
|
3416
|
+
const { bounds: b, first, second, order, stack } = this;
|
|
3417
|
+
if (this.nodes === 0) return;
|
|
3418
|
+
let top = 0;
|
|
3419
|
+
stack[top++] = 0;
|
|
3420
|
+
while (top > 0) {
|
|
3421
|
+
const node = stack[--top];
|
|
3422
|
+
const at = node * 6;
|
|
3423
|
+
const ax = (b[at] - ox) * ix, bx = (b[at + 3] - ox) * ix;
|
|
3424
|
+
const ay = (b[at + 1] - oy) * iy, by = (b[at + 4] - oy) * iy;
|
|
3425
|
+
const az = (b[at + 2] - oz) * iz, bz = (b[at + 5] - oz) * iz;
|
|
3426
|
+
const near = Math.max(ax < bx ? ax : bx, ay < by ? ay : by, az < bz ? az : bz);
|
|
3427
|
+
const far = Math.min(ax < bx ? bx : ax, ay < by ? by : ay, az < bz ? bz : az);
|
|
3428
|
+
if (near > far || far < 0 || near > best()) continue;
|
|
3429
|
+
const leaf = first[node];
|
|
3430
|
+
if (leaf < 0) {
|
|
3431
|
+
for (let n = ~leaf; n < second[node]; n++) test(order[n]);
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
stack[top++] = leaf;
|
|
3435
|
+
stack[top++] = second[node];
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
/** Calls `visit` with each item whose node meets the box. */
|
|
3439
|
+
overlap(min, max, visit) {
|
|
3440
|
+
const { bounds: b, first, second, order, stack } = this;
|
|
3441
|
+
if (this.nodes === 0) return;
|
|
3442
|
+
let top = 0;
|
|
3443
|
+
stack[top++] = 0;
|
|
3444
|
+
while (top > 0) {
|
|
3445
|
+
const node = stack[--top];
|
|
3446
|
+
const at = node * 6;
|
|
3447
|
+
if (b[at + 3] < min[0] || b[at] > max[0] || b[at + 4] < min[1] || b[at + 1] > max[1] || b[at + 5] < min[2] || b[at + 2] > max[2]) continue;
|
|
3448
|
+
const leaf = first[node];
|
|
3449
|
+
if (leaf < 0) {
|
|
3450
|
+
for (let n = ~leaf; n < second[node]; n++) visit(order[n]);
|
|
3451
|
+
continue;
|
|
3452
|
+
}
|
|
3453
|
+
stack[top++] = leaf;
|
|
3454
|
+
stack[top++] = second[node];
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
var TriangleSet = class {
|
|
3459
|
+
values = new Float64Array(9 * 4096);
|
|
3460
|
+
owners = new Int32Array(4096);
|
|
3461
|
+
count = 0;
|
|
3462
|
+
add(actor, ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
3463
|
+
if (this.count === this.owners.length) {
|
|
3464
|
+
const values = new Float64Array(this.values.length * 2);
|
|
3465
|
+
values.set(this.values);
|
|
3466
|
+
this.values = values;
|
|
3467
|
+
const owners = new Int32Array(this.owners.length * 2);
|
|
3468
|
+
owners.set(this.owners);
|
|
3469
|
+
this.owners = owners;
|
|
3470
|
+
}
|
|
3471
|
+
const o = this.count * 9;
|
|
3472
|
+
const v = this.values;
|
|
3473
|
+
v[o] = ax;
|
|
3474
|
+
v[o + 1] = ay;
|
|
3475
|
+
v[o + 2] = az;
|
|
3476
|
+
v[o + 3] = bx;
|
|
3477
|
+
v[o + 4] = by;
|
|
3478
|
+
v[o + 5] = bz;
|
|
3479
|
+
v[o + 6] = cx;
|
|
3480
|
+
v[o + 7] = cy;
|
|
3481
|
+
v[o + 8] = cz;
|
|
3482
|
+
this.owners[this.count] = actor;
|
|
3483
|
+
this.count += 1;
|
|
3484
|
+
}
|
|
3485
|
+
get vertices() {
|
|
3486
|
+
return this.values;
|
|
3487
|
+
}
|
|
3488
|
+
owner(t) {
|
|
3489
|
+
return this.owners[t];
|
|
3490
|
+
}
|
|
3491
|
+
tree() {
|
|
3492
|
+
const items = new Float64Array(this.count * 6);
|
|
3493
|
+
const v = this.values;
|
|
3494
|
+
for (let t = 0; t < this.count; t++) {
|
|
3495
|
+
const o = t * 9;
|
|
3496
|
+
const at = t * 6;
|
|
3497
|
+
items[at] = Math.min(v[o], v[o + 3], v[o + 6]);
|
|
3498
|
+
items[at + 1] = Math.min(v[o + 1], v[o + 4], v[o + 7]);
|
|
3499
|
+
items[at + 2] = Math.min(v[o + 2], v[o + 5], v[o + 8]);
|
|
3500
|
+
items[at + 3] = Math.max(v[o], v[o + 3], v[o + 6]);
|
|
3501
|
+
items[at + 4] = Math.max(v[o + 1], v[o + 4], v[o + 7]);
|
|
3502
|
+
items[at + 5] = Math.max(v[o + 2], v[o + 5], v[o + 8]);
|
|
3503
|
+
}
|
|
3504
|
+
return new BoundsTree(items, this.count);
|
|
3505
|
+
}
|
|
3506
|
+
};
|
|
3507
|
+
var inverse = (d) => d !== 0 ? 1 / d : Number.POSITIVE_INFINITY;
|
|
3508
|
+
var SourceRays = class {
|
|
3509
|
+
tree;
|
|
3510
|
+
set;
|
|
3511
|
+
constructor(set) {
|
|
3512
|
+
this.set = set;
|
|
3513
|
+
this.tree = set.tree();
|
|
3514
|
+
}
|
|
3515
|
+
/** How far a point is from a surface, on any axis, read by steps of a cell: the first box round it that holds any. */
|
|
3516
|
+
offSurface(x, y, z, h) {
|
|
3517
|
+
const v = this.set.vertices;
|
|
3518
|
+
const bounds = [0, 0, 0, 0, 0, 0];
|
|
3519
|
+
for (const cells of SURFACE_STEPS_CELLS) {
|
|
3520
|
+
const r = cells * h;
|
|
3521
|
+
const min = [x - r, y - r, z - r];
|
|
3522
|
+
const max = [x + r, y + r, z + r];
|
|
3523
|
+
let found = false;
|
|
3524
|
+
this.tree.overlap(min, max, (t) => {
|
|
3525
|
+
if (!found) found = clippedBounds(v, t * 9, min, max, bounds);
|
|
3526
|
+
});
|
|
3527
|
+
if (found) return r;
|
|
3528
|
+
}
|
|
3529
|
+
return SURFACE_STEPS_CELLS[SURFACE_STEPS_CELLS.length - 1] * 2 * h;
|
|
3530
|
+
}
|
|
3531
|
+
/** The nearest triangle along a unit ray within range, or null. Both faces count: open or closed, wound either way. */
|
|
3532
|
+
ray(ox, oy, oz, dx, dy, dz, range) {
|
|
3533
|
+
const v = this.set.vertices;
|
|
3534
|
+
let best = range;
|
|
3535
|
+
let found = false;
|
|
3536
|
+
this.tree.walk(ox, oy, oz, inverse(dx), inverse(dy), inverse(dz), () => best, (t) => {
|
|
3537
|
+
const o = t * 9;
|
|
3538
|
+
const ax = v[o], ay = v[o + 1], az = v[o + 2];
|
|
3539
|
+
const e1x = v[o + 3] - ax, e1y = v[o + 4] - ay, e1z = v[o + 5] - az;
|
|
3540
|
+
const e2x = v[o + 6] - ax, e2y = v[o + 7] - ay, e2z = v[o + 8] - az;
|
|
3541
|
+
const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x;
|
|
3542
|
+
const det = e1x * px + e1y * py + e1z * pz;
|
|
3543
|
+
if (det === 0) return;
|
|
3544
|
+
const inv = 1 / det;
|
|
3545
|
+
const sx = ox - ax, sy = oy - ay, sz = oz - az;
|
|
3546
|
+
const u = (sx * px + sy * py + sz * pz) * inv;
|
|
3547
|
+
if (u < 0 || u > 1) return;
|
|
3548
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
3549
|
+
const w = (dx * qx + dy * qy + dz * qz) * inv;
|
|
3550
|
+
if (w < 0 || u + w > 1) return;
|
|
3551
|
+
const dist = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
|
3552
|
+
if (dist >= 0 && dist < best) {
|
|
3553
|
+
best = dist;
|
|
3554
|
+
found = true;
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3557
|
+
return found ? best : null;
|
|
3558
|
+
}
|
|
3559
|
+
};
|
|
3560
|
+
var worldTrees = /* @__PURE__ */ new WeakMap();
|
|
3561
|
+
function treeOfWorld(world) {
|
|
3562
|
+
const known = worldTrees.get(world);
|
|
3563
|
+
if (known !== void 0) return known;
|
|
3564
|
+
const items = new Float64Array(world.boxes.length * 6);
|
|
3565
|
+
world.boxes.forEach((b, i) => items.set(b, i * 6));
|
|
3566
|
+
const tree = new BoundsTree(items, world.boxes.length);
|
|
3567
|
+
worldTrees.set(world, tree);
|
|
3568
|
+
return tree;
|
|
3569
|
+
}
|
|
3570
|
+
function shotRay(world, origin, dir, range = Number.POSITIVE_INFINITY) {
|
|
3571
|
+
const [ox, oy, oz] = origin;
|
|
3572
|
+
const [dx, dy, dz] = dir;
|
|
3573
|
+
const { boxes } = world;
|
|
3574
|
+
const lo = [0, 0, 0];
|
|
3575
|
+
let best = range;
|
|
3576
|
+
let solid = -1;
|
|
3577
|
+
let axis = -1;
|
|
3578
|
+
treeOfWorld(world).walk(ox, oy, oz, inverse(dx), inverse(dy), inverse(dz), () => best, (s) => {
|
|
3579
|
+
const b = boxes[s];
|
|
3580
|
+
let near = Number.NEGATIVE_INFINITY;
|
|
3581
|
+
let far = Number.POSITIVE_INFINITY;
|
|
3582
|
+
let entered = -1;
|
|
3583
|
+
for (let a = 0; a < 3; a++) {
|
|
3584
|
+
const d = a === 0 ? dx : a === 1 ? dy : dz;
|
|
3585
|
+
lo[a] = a === 0 ? ox : a === 1 ? oy : oz;
|
|
3586
|
+
if (d === 0) {
|
|
3587
|
+
if (!(lo[a] > b[a] && lo[a] < b[a + 3])) return;
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3590
|
+
const inv = 1 / d;
|
|
3591
|
+
const t0 = (b[a] - lo[a]) * inv;
|
|
3592
|
+
const t1 = (b[a + 3] - lo[a]) * inv;
|
|
3593
|
+
const enter = Math.min(t0, t1);
|
|
3594
|
+
if (enter > near) {
|
|
3595
|
+
near = enter;
|
|
3596
|
+
entered = a;
|
|
3597
|
+
}
|
|
3598
|
+
far = Math.min(far, Math.max(t0, t1));
|
|
3599
|
+
}
|
|
3600
|
+
if (!(near < far && far > 0)) return;
|
|
3601
|
+
const t = Math.max(near, 0);
|
|
3602
|
+
if (t < best || t === best && (solid < 0 || s < solid)) {
|
|
3603
|
+
best = t;
|
|
3604
|
+
solid = s;
|
|
3605
|
+
axis = near < 0 ? -1 : entered;
|
|
3606
|
+
}
|
|
3607
|
+
});
|
|
3608
|
+
return solid < 0 ? null : { dist: best, solid, axis };
|
|
3609
|
+
}
|
|
3610
|
+
function sweepOctants(l, solid, sources) {
|
|
3611
|
+
const { nx, ny, w } = l;
|
|
3612
|
+
const seenAir = newLayer(l);
|
|
3613
|
+
const seenSolid = newLayer(l);
|
|
3614
|
+
const visited = newLayer(l);
|
|
3615
|
+
for (let octant = 0; octant < 8; octant++) {
|
|
3616
|
+
const sx = octant & 1 ? 1 : -1;
|
|
3617
|
+
const sy = octant & 2 ? 1 : -1;
|
|
3618
|
+
const up = (octant & 4) !== 0;
|
|
3619
|
+
visited.fill(0);
|
|
3620
|
+
for (let jStep = 0; jStep < ny; jStep++) {
|
|
3621
|
+
const j = sy > 0 ? jStep : ny - 1 - jStep;
|
|
3622
|
+
const fromY = jStep > 0 ? (j - sy) * nx : -1;
|
|
3623
|
+
for (let iStep = 0; iStep < nx; iStep++) {
|
|
3624
|
+
const i = sx > 0 ? iStep : nx - 1 - iStep;
|
|
3625
|
+
const col = (j * nx + i) * w;
|
|
3626
|
+
const prevX = iStep > 0 ? (j * nx + i - sx) * w : -1;
|
|
3627
|
+
const prevY = fromY >= 0 ? (fromY + i) * w : -1;
|
|
3628
|
+
let carry = 0;
|
|
3629
|
+
for (let step = 0; step < w; step++) {
|
|
3630
|
+
const a = up ? step : w - 1 - step;
|
|
3631
|
+
let seed = sources[col + a] | carry;
|
|
3632
|
+
if (prevX >= 0) seed |= visited[prevX + a];
|
|
3633
|
+
if (prevY >= 0) seed |= visited[prevY + a];
|
|
3634
|
+
const rock = solid[col + a];
|
|
3635
|
+
const air = ~rock;
|
|
3636
|
+
let g = seed & air;
|
|
3637
|
+
let p = air;
|
|
3638
|
+
if (up) {
|
|
3639
|
+
g |= p & g << 1;
|
|
3640
|
+
p &= p << 1;
|
|
3641
|
+
g |= p & g << 2;
|
|
3642
|
+
p &= p << 2;
|
|
3643
|
+
g |= p & g << 4;
|
|
3644
|
+
p &= p << 4;
|
|
3645
|
+
g |= p & g << 8;
|
|
3646
|
+
p &= p << 8;
|
|
3647
|
+
g |= p & g << 16;
|
|
3648
|
+
carry = g >>> 31;
|
|
3649
|
+
seenSolid[col + a] |= (seed | g << 1) & rock;
|
|
3650
|
+
} else {
|
|
3651
|
+
g |= p & g >>> 1;
|
|
3652
|
+
p &= p >>> 1;
|
|
3653
|
+
g |= p & g >>> 2;
|
|
3654
|
+
p &= p >>> 2;
|
|
3655
|
+
g |= p & g >>> 4;
|
|
3656
|
+
p &= p >>> 4;
|
|
3657
|
+
g |= p & g >>> 8;
|
|
3658
|
+
p &= p >>> 8;
|
|
3659
|
+
g |= p & g >>> 16;
|
|
3660
|
+
carry = (g & 1) << 31;
|
|
3661
|
+
seenSolid[col + a] |= (seed | g >>> 1) & rock;
|
|
3662
|
+
}
|
|
3663
|
+
visited[col + a] = g;
|
|
3664
|
+
seenAir[col + a] |= g;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
return { seenAir, seenSolid };
|
|
3670
|
+
}
|
|
3671
|
+
function sourcesFromReach(from, body, shots) {
|
|
3672
|
+
const out = newLayer(shots);
|
|
3673
|
+
for (let j = 0; j < from.ny; j++) {
|
|
3674
|
+
const y = from.y0 + (j + 0.5) * from.h;
|
|
3675
|
+
const sj = Math.floor((y - shots.y0) / shots.h);
|
|
3676
|
+
if (sj < 0 || sj >= shots.ny) continue;
|
|
3677
|
+
for (let i = 0; i < from.nx; i++) {
|
|
3678
|
+
const col = (j * from.nx + i) * from.w;
|
|
3679
|
+
let any = false;
|
|
3680
|
+
for (let a = 0; a < from.w && !any; a++) any = body[col + a] !== 0;
|
|
3681
|
+
if (!any) continue;
|
|
3682
|
+
const x = from.x0 + (i + 0.5) * from.h;
|
|
3683
|
+
const si = Math.floor((x - shots.x0) / shots.h);
|
|
3684
|
+
if (si < 0 || si >= shots.nx) continue;
|
|
3685
|
+
const target = (sj * shots.nx + si) * shots.w;
|
|
3686
|
+
for (let k = 0; k < from.nz; k++) {
|
|
3687
|
+
if (!testBit(body, col, k)) continue;
|
|
3688
|
+
const sk = Math.floor((from.z0 + (k + 0.5) * from.h - shots.z0) / shots.h);
|
|
3689
|
+
if (sk >= 0 && sk < shots.nz) setBit(out, target, sk);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
return out;
|
|
3694
|
+
}
|
|
3695
|
+
function faceGain(l, must, avoid, i0, i1, j0, j1, k0, k1) {
|
|
3696
|
+
let gain = 0;
|
|
3697
|
+
for (let j = j0; j <= j1; j++) {
|
|
3698
|
+
for (let i = i0; i <= i1; i++) {
|
|
3699
|
+
const col = (j * l.nx + i) * l.w;
|
|
3700
|
+
if (anyInRun(l, avoid, col, k0, k1)) return -1;
|
|
3701
|
+
gain += countInRun(l, must, col, k0, k1);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
return gain;
|
|
3705
|
+
}
|
|
3706
|
+
function growRect(l, must, avoid, i, j, k, order) {
|
|
3707
|
+
const r = { i0: i, i1: i, j0: j, j1: j, k0: k, k1: k };
|
|
3708
|
+
let gained = 1;
|
|
3709
|
+
for (const axis of order) {
|
|
3710
|
+
for (const way of [1, -1]) {
|
|
3711
|
+
for (; ; ) {
|
|
3712
|
+
const size = axis === 0 ? r.i1 - r.i0 : axis === 1 ? r.j1 - r.j0 : r.k1 - r.k0;
|
|
3713
|
+
if (size + 1 >= MAX_GROW_CELLS2) break;
|
|
3714
|
+
let gain;
|
|
3715
|
+
if (axis === 0) {
|
|
3716
|
+
const x = way > 0 ? r.i1 + 1 : r.i0 - 1;
|
|
3717
|
+
if (x < 0 || x >= l.nx) break;
|
|
3718
|
+
gain = faceGain(l, must, avoid, x, x, r.j0, r.j1, r.k0, r.k1);
|
|
3719
|
+
if (gain <= 0) break;
|
|
3720
|
+
if (way > 0) r.i1 = x;
|
|
3721
|
+
else r.i0 = x;
|
|
3722
|
+
} else if (axis === 1) {
|
|
3723
|
+
const y = way > 0 ? r.j1 + 1 : r.j0 - 1;
|
|
3724
|
+
if (y < 0 || y >= l.ny) break;
|
|
3725
|
+
gain = faceGain(l, must, avoid, r.i0, r.i1, y, y, r.k0, r.k1);
|
|
3726
|
+
if (gain <= 0) break;
|
|
3727
|
+
if (way > 0) r.j1 = y;
|
|
3728
|
+
else r.j0 = y;
|
|
3729
|
+
} else {
|
|
3730
|
+
const z = way > 0 ? r.k1 + 1 : r.k0 - 1;
|
|
3731
|
+
if (z < 0 || z >= l.nz) break;
|
|
3732
|
+
gain = faceGain(l, must, avoid, r.i0, r.i1, r.j0, r.j1, z, z);
|
|
3733
|
+
if (gain <= 0) break;
|
|
3734
|
+
if (way > 0) r.k1 = z;
|
|
3735
|
+
else r.k0 = z;
|
|
3736
|
+
}
|
|
3737
|
+
gained += gain;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return { rect: r, gained };
|
|
3742
|
+
}
|
|
3743
|
+
var GROW_ORDERS = [[0, 1, 2], [1, 0, 2]];
|
|
3744
|
+
function coverCells(l, must, avoid, stopAt) {
|
|
3745
|
+
const rects = [];
|
|
3746
|
+
for (let col = 0; col < l.nx * l.ny; col++) {
|
|
3747
|
+
const offset = col * l.w;
|
|
3748
|
+
let any = false;
|
|
3749
|
+
for (let a = 0; a < l.w && !any; a++) any = must[offset + a] !== 0;
|
|
3750
|
+
if (!any) continue;
|
|
3751
|
+
const j = Math.floor(col / l.nx);
|
|
3752
|
+
const i = col - j * l.nx;
|
|
3753
|
+
for (let k = 0; k < l.nz; k++) {
|
|
3754
|
+
if (!testBit(must, offset, k)) continue;
|
|
3755
|
+
let best = null;
|
|
3756
|
+
for (const order of GROW_ORDERS) {
|
|
3757
|
+
const grown = growRect(l, must, avoid, i, j, k, order);
|
|
3758
|
+
if (best === null || grown.gained > best.gained) best = grown;
|
|
3759
|
+
}
|
|
3760
|
+
const { rect } = best;
|
|
3761
|
+
rects.push(rect);
|
|
3762
|
+
if (rects.length >= stopAt) return rects;
|
|
3763
|
+
for (let y = rect.j0; y <= rect.j1; y++) {
|
|
3764
|
+
for (let x = rect.i0; x <= rect.i1; x++) clearRun(l, must, (y * l.nx + x) * l.w, rect.k0, rect.k1);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return rects;
|
|
3769
|
+
}
|
|
3770
|
+
var clipIn = new Float64Array(3 * 16);
|
|
3771
|
+
var clipOut = new Float64Array(3 * 16);
|
|
3772
|
+
function clippedBounds(v, o, min, max, bounds) {
|
|
3773
|
+
let src = clipIn;
|
|
3774
|
+
let dst = clipOut;
|
|
3775
|
+
for (let n = 0; n < 9; n++) src[n] = v[o + n];
|
|
3776
|
+
let count = 3;
|
|
3777
|
+
for (let axis = 0; axis < 3 && count > 0; axis++) {
|
|
3778
|
+
for (const side of [0, 1]) {
|
|
3779
|
+
const edge = side === 0 ? min[axis] : max[axis];
|
|
3780
|
+
const sign = side === 0 ? 1 : -1;
|
|
3781
|
+
let out = 0;
|
|
3782
|
+
for (let a = 0; a < count; a++) {
|
|
3783
|
+
const b = (a + 1) % count;
|
|
3784
|
+
const pa = (src[a * 3 + axis] - edge) * sign;
|
|
3785
|
+
const pb = (src[b * 3 + axis] - edge) * sign;
|
|
3786
|
+
if (pa >= 0) {
|
|
3787
|
+
dst[out * 3] = src[a * 3];
|
|
3788
|
+
dst[out * 3 + 1] = src[a * 3 + 1];
|
|
3789
|
+
dst[out * 3 + 2] = src[a * 3 + 2];
|
|
3790
|
+
out += 1;
|
|
3791
|
+
}
|
|
3792
|
+
if (pa > 0 && pb < 0 || pa < 0 && pb > 0) {
|
|
3793
|
+
const t = pa / (pa - pb);
|
|
3794
|
+
for (let c = 0; c < 3; c++) dst[out * 3 + c] = src[a * 3 + c] + (src[b * 3 + c] - src[a * 3 + c]) * t;
|
|
3795
|
+
out += 1;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
count = out;
|
|
3799
|
+
const swap = src;
|
|
3800
|
+
src = dst;
|
|
3801
|
+
dst = swap;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
if (count === 0) return false;
|
|
3805
|
+
for (let p = 0; p < count; p++) {
|
|
3806
|
+
for (let a = 0; a < 3; a++) {
|
|
3807
|
+
const value = src[p * 3 + a];
|
|
3808
|
+
if (value < bounds[a]) bounds[a] = value;
|
|
3809
|
+
if (value > bounds[a + 3]) bounds[a + 3] = value;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
return true;
|
|
3813
|
+
}
|
|
3814
|
+
var gridOf = (h) => 2 ** Math.floor(Math.log2(h * GRID_CELLS));
|
|
3815
|
+
function faceDown(value, grid) {
|
|
3816
|
+
const on = Math.floor(value / grid) * grid;
|
|
3817
|
+
const f2 = Math.fround(on);
|
|
3818
|
+
return f2 > on ? Math.fround(on - grid) : f2;
|
|
3819
|
+
}
|
|
3820
|
+
function faceUp(value, grid) {
|
|
3821
|
+
const on = Math.ceil(value / grid) * grid;
|
|
3822
|
+
const f2 = Math.fround(on);
|
|
3823
|
+
return f2 < on ? Math.fround(on + grid) : f2;
|
|
3824
|
+
}
|
|
3825
|
+
function boxRound(lo, hi, grid, seam) {
|
|
3826
|
+
return [faceDown(lo[0] - seam, grid), faceDown(lo[1] - seam, grid), faceDown(lo[2] - seam, grid), faceUp(hi[0] + seam, grid), faceUp(hi[1] + seam, grid), faceUp(hi[2] + seam, grid)];
|
|
3827
|
+
}
|
|
3828
|
+
function fitOnLattice(input, h, exact, maxBoxes, log) {
|
|
3829
|
+
const { scene } = input;
|
|
3830
|
+
const bounds = shotBounds(input);
|
|
3831
|
+
const l = makeLattice(h, bounds.min, bounds.max);
|
|
3832
|
+
const half = h / 2 + h * SOLID_GROW_CELLS;
|
|
3833
|
+
const solid = newLayer(l);
|
|
3834
|
+
const exactLists = [];
|
|
3835
|
+
const others = new TriangleSet();
|
|
3836
|
+
const shell = newLayer(l);
|
|
3837
|
+
const inReach = (b) => b.max[0] >= l.x0 && b.max[1] >= l.y0 && b.max[2] >= l.z0 && b.min[0] <= l.x0 + l.nx * h && b.min[1] <= l.y0 + l.ny * h && b.min[2] <= l.z0 + l.nz * h;
|
|
3838
|
+
scene.actors.forEach((actor, index) => {
|
|
3839
|
+
for (const surface of actor.surfaces) {
|
|
3840
|
+
if (!stopsShot(actor, surface) || !inReach(surfaceBounds(surface))) continue;
|
|
3841
|
+
const asBox = exact ? exactBoxBounds(surface) : null;
|
|
3842
|
+
if (asBox !== null) {
|
|
3843
|
+
exactLists.push({ actor: index, box: asBox });
|
|
3844
|
+
rasterSurface(l, solid, half, surface);
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
rasterSurface(l, solid, half, surface);
|
|
3848
|
+
surfaceTriangles(surface, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
3849
|
+
others.add(index, ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
3850
|
+
rasterTriangle(l, shell, half, ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
const dilate = Math.max(0, Math.floor(input.hull.radius / h) - 1);
|
|
3855
|
+
const raw = sourcesFromReach(input.reachL, input.reachBody, l);
|
|
3856
|
+
const sources = dilate > 0 ? dilateXY(l, raw, dilate) : raw;
|
|
3857
|
+
const { seenAir, seenSolid } = sweepOctants(l, solid, sources);
|
|
3858
|
+
let airCells = 0;
|
|
3859
|
+
const topBits = l.nz % 32;
|
|
3860
|
+
const topMask = topBits === 0 ? 4294967295 : 2 ** topBits - 1;
|
|
3861
|
+
for (let at = 0; at < seenAir.length; at++) {
|
|
3862
|
+
let word = (at + 1) % l.w === 0 ? (seenAir[at] & topMask) >>> 0 : seenAir[at];
|
|
3863
|
+
while (word !== 0) {
|
|
3864
|
+
word &= word - 1;
|
|
3865
|
+
airCells += 1;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
const grid = gridOf(h);
|
|
3869
|
+
const seam = grid * SEAM_GRIDS;
|
|
3870
|
+
const boxes = [];
|
|
3871
|
+
const actorBoxes = new Int32Array(scene.actors.length);
|
|
3872
|
+
const actorsKept = new Uint8Array(scene.actors.length);
|
|
3873
|
+
for (const { actor, box } of exactLists) {
|
|
3874
|
+
const i0 = Math.max(0, cellFloor((box.min[0] - l.x0) / h - SOLID_GROW_CELLS));
|
|
3875
|
+
const i1 = Math.min(l.nx - 1, cellCeil((box.max[0] - l.x0) / h + SOLID_GROW_CELLS) - 1);
|
|
3876
|
+
const j0 = Math.max(0, cellFloor((box.min[1] - l.y0) / h - SOLID_GROW_CELLS));
|
|
3877
|
+
const j1 = Math.min(l.ny - 1, cellCeil((box.max[1] - l.y0) / h + SOLID_GROW_CELLS) - 1);
|
|
3878
|
+
const k0 = Math.max(0, cellFloor((box.min[2] - l.z0) / h));
|
|
3879
|
+
const k1 = Math.min(l.nz - 1, Math.max(k0, cellCeil((box.max[2] - l.z0) / h) - 1));
|
|
3880
|
+
let seen = false;
|
|
3881
|
+
for (let j = j0; j <= j1 && !seen; j++) {
|
|
3882
|
+
for (let i = i0; i <= i1 && !seen; i++) seen = anyInRun(l, seenSolid, (j * l.nx + i) * l.w, k0, k1);
|
|
3883
|
+
}
|
|
3884
|
+
if (!seen) continue;
|
|
3885
|
+
boxes.push(boxRound(box.min, box.max, grid, 0));
|
|
3886
|
+
actorBoxes[actor] += 1;
|
|
3887
|
+
actorsKept[actor] = 1;
|
|
3888
|
+
}
|
|
3889
|
+
const exactBoxes = boxes.length;
|
|
3890
|
+
const band = reachUp(l, reachDown(l, dilateXY(l, seenSolid, SURFACE_BAND_CELLS), SURFACE_BAND_CELLS + 1), SURFACE_BAND_CELLS + 1);
|
|
3891
|
+
const must = newLayer(l);
|
|
3892
|
+
for (let at = 0; at < must.length; at++) must[at] = band[at] & shell[at];
|
|
3893
|
+
const left = Math.max(0, maxBoxes - exactBoxes);
|
|
3894
|
+
const stopAt = Math.max(REFUSAL_COUNT_LEAST, Math.ceil(maxBoxes * REFUSAL_COUNT_FACTOR)) + 1;
|
|
3895
|
+
const rects = coverCells(l, must, seenAir, stopAt);
|
|
3896
|
+
const tree = others.tree();
|
|
3897
|
+
const v = others.vertices;
|
|
3898
|
+
let coverBoxes = 0;
|
|
3899
|
+
const lo = [0, 0, 0];
|
|
3900
|
+
const hi = [0, 0, 0];
|
|
3901
|
+
for (const r of rects) {
|
|
3902
|
+
lo[0] = l.x0 + r.i0 * h;
|
|
3903
|
+
lo[1] = l.y0 + r.j0 * h;
|
|
3904
|
+
lo[2] = l.z0 + r.k0 * h;
|
|
3905
|
+
hi[0] = l.x0 + (r.i1 + 1) * h;
|
|
3906
|
+
hi[1] = l.y0 + (r.j1 + 1) * h;
|
|
3907
|
+
hi[2] = l.z0 + (r.k1 + 1) * h;
|
|
3908
|
+
const tight = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
|
|
3909
|
+
let owner = -1;
|
|
3910
|
+
tree.overlap(lo, hi, (t) => {
|
|
3911
|
+
if (clippedBounds(v, t * 9, lo, hi, tight)) {
|
|
3912
|
+
const actor = others.owner(t);
|
|
3913
|
+
actorsKept[actor] = 1;
|
|
3914
|
+
if (owner < 0) owner = actor;
|
|
3915
|
+
}
|
|
3916
|
+
});
|
|
3917
|
+
if (owner < 0) continue;
|
|
3918
|
+
boxes.push(boxRound([tight[0], tight[1], tight[2]], [tight[3], tight[4], tight[5]], grid, seam));
|
|
3919
|
+
actorBoxes[owner] += 1;
|
|
3920
|
+
coverBoxes += 1;
|
|
3921
|
+
}
|
|
3922
|
+
const over = rects.length >= stopAt || coverBoxes > left;
|
|
3923
|
+
log?.(`shot lattice ${l.nx} x ${l.ny} x ${l.nz} at ${h}${exact ? "" : " (no exact boxes)"}: ${exactBoxes} exact + ${coverBoxes} cover${over ? " (over)" : ""}, air seen ${Math.round(100 * airCells / (l.nx * l.ny * l.nz))}%`);
|
|
3924
|
+
return { boxes, exactBoxes, coverBoxes, actorBoxes, actorsKept, seenAirPct: Math.round(1e4 * airCells / (l.nx * l.ny * l.nz)) / 100, over };
|
|
3925
|
+
}
|
|
3926
|
+
function shotBounds(input) {
|
|
3927
|
+
const { eyeBounds: eye, range, scene } = input;
|
|
3928
|
+
const lo = [eye.min[0], eye.min[1], eye.min[2]];
|
|
3929
|
+
const hi = [eye.max[0], eye.max[1], eye.max[2]];
|
|
3930
|
+
for (const actor of scene.actors) {
|
|
3931
|
+
for (const surface of actor.surfaces) {
|
|
3932
|
+
if (!stopsShot(actor, surface)) continue;
|
|
3933
|
+
const b = surfaceBounds(surface);
|
|
3934
|
+
for (let a = 0; a < 3; a++) {
|
|
3935
|
+
lo[a] = Math.min(lo[a], Math.max(b.min[a], eye.min[a] - range));
|
|
3936
|
+
hi[a] = Math.max(hi[a], Math.min(b.max[a], eye.max[a] + range));
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return { min: [lo[0], lo[1], lo[2]], max: [hi[0], hi[1], hi[2]] };
|
|
3941
|
+
}
|
|
3942
|
+
function ladder(bounds, hull, asked) {
|
|
3943
|
+
if (asked !== void 0) return [asked];
|
|
3944
|
+
let h = hull.radius * FINEST_CELL_RADII;
|
|
3945
|
+
const cells = (c) => (bounds.max[0] - bounds.min[0]) / c * ((bounds.max[1] - bounds.min[1]) / c) * ((bounds.max[2] - bounds.min[2]) / c);
|
|
3946
|
+
while (cells(h) > MAX_LATTICE_CELLS2) h *= 2;
|
|
3947
|
+
return Array.from({ length: CELL_RUNGS }, (_, n) => h * 2 ** n);
|
|
3948
|
+
}
|
|
3949
|
+
function eyeBoundsOf(l, nodes, count, hull) {
|
|
3950
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
3951
|
+
for (let n = 0; n < count; n++) {
|
|
3952
|
+
const node = nodes[n];
|
|
3953
|
+
const k = node % l.nz;
|
|
3954
|
+
const col = (node - k) / l.nz;
|
|
3955
|
+
const j = Math.floor(col / l.nx);
|
|
3956
|
+
const i = col - j * l.nx;
|
|
3957
|
+
minX = Math.min(minX, l.x0 + i * l.h);
|
|
3958
|
+
maxX = Math.max(maxX, l.x0 + (i + 1) * l.h);
|
|
3959
|
+
minY = Math.min(minY, l.y0 + j * l.h);
|
|
3960
|
+
maxY = Math.max(maxY, l.y0 + (j + 1) * l.h);
|
|
3961
|
+
minZ = Math.min(minZ, l.z0 + k * l.h);
|
|
3962
|
+
maxZ = Math.max(maxZ, l.z0 + k * l.h + hull.height + hull.jump);
|
|
3963
|
+
}
|
|
3964
|
+
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
|
3965
|
+
}
|
|
3966
|
+
function claimArithmetic(server, facing, claimed, tolerance, range, minFacing = ShotWorldCost.MIN_CLAIM_FACING) {
|
|
3967
|
+
const said = claimed === null || Number.isNaN(claimed) ? Number.POSITIVE_INFINITY : Math.max(claimed, 0);
|
|
3968
|
+
const stop = Math.min(said, range);
|
|
3969
|
+
const slack = tolerance / Math.max(facing, minFacing);
|
|
3970
|
+
if (stop > server + slack) return { dist: server, verdict: ClaimVerdict.Refused, slack };
|
|
3971
|
+
if (stop < server - slack) return { dist: server - slack, verdict: ClaimVerdict.Capped, slack };
|
|
3972
|
+
return { dist: stop, verdict: ClaimVerdict.Accepted, slack };
|
|
3973
|
+
}
|
|
3974
|
+
function claimedStop(world, origin, dir, claimed, tolerance, range, minFacing = ShotWorldCost.MIN_CLAIM_FACING) {
|
|
3975
|
+
const big = Math.max(Math.abs(dir[0]), Math.abs(dir[1]), Math.abs(dir[2]));
|
|
3976
|
+
const a = dir[0] / big;
|
|
3977
|
+
const b = dir[1] / big;
|
|
3978
|
+
const c = dir[2] / big;
|
|
3979
|
+
const length = Math.sqrt(a * a + b * b + c * c);
|
|
3980
|
+
const unit = [a / length, b / length, c / length];
|
|
3981
|
+
const hit = shotRay(world, origin, unit, range);
|
|
3982
|
+
const facing = hit === null || hit.axis < 0 ? 1 : Math.abs(unit[hit.axis]);
|
|
3983
|
+
return { ...claimArithmetic(hit === null ? range : hit.dist, facing, claimed, tolerance, range, minFacing), server: hit === null ? null : hit.dist };
|
|
3984
|
+
}
|
|
3985
|
+
function claimMargin(record, toleranceUnits, past, range, minFacing) {
|
|
3986
|
+
const { truths, stops, facings, answered } = record;
|
|
3987
|
+
let off = 0;
|
|
3988
|
+
let stopped = 0;
|
|
3989
|
+
let passed = 0;
|
|
3990
|
+
const gains = new Float64Array(answered);
|
|
3991
|
+
for (let n = 0; n < answered; n++) {
|
|
3992
|
+
const truth = truths[n];
|
|
3993
|
+
const stop = stops[n];
|
|
3994
|
+
const server = stop < 0 ? range : stop;
|
|
3995
|
+
const facing = facings[n];
|
|
3996
|
+
const honest = claimArithmetic(server, facing, truth < 0 ? null : truth, toleranceUnits, range, minFacing);
|
|
3997
|
+
if (honest.verdict !== ClaimVerdict.Accepted) off += 1;
|
|
3998
|
+
if (truth < 0) continue;
|
|
3999
|
+
if (stop >= 0) gains[stopped] = Math.max(0, stop + honest.slack - truth);
|
|
4000
|
+
stopped += 1;
|
|
4001
|
+
if (claimArithmetic(server, facing, truth + past, toleranceUnits, range, minFacing).verdict === ClaimVerdict.Accepted) passed += 1;
|
|
4002
|
+
}
|
|
4003
|
+
const sorted = gains.subarray(0, stopped).sort();
|
|
4004
|
+
const at = (share) => Math.round((sorted[Math.min(stopped - 1, Math.floor(share * stopped))] ?? 0) * 100) / 100;
|
|
4005
|
+
return { toleranceUnits, minFacing, honestOffPct: Math.round(1e4 * off / Math.max(1, answered)) / 100, hostilePassPct: Math.round(1e4 * passed / Math.max(1, stopped)) / 100, hostileGainP50Units: at(0.5), hostileGainP99Units: at(0.99), hostileGainMaxUnits: at(1) };
|
|
4006
|
+
}
|
|
4007
|
+
function claimMargins(record, stated, hull, range) {
|
|
4008
|
+
return CLAIM_MULTIPLES.map((multiple) => claimMargin(record, stated * multiple, hull.radius * HOSTILE_PAST_RADII, range, ShotWorldCost.MIN_CLAIM_FACING));
|
|
4009
|
+
}
|
|
4010
|
+
function claimGrid(record, stated, hull, range, facings) {
|
|
4011
|
+
return facings.flatMap((minFacing) => CLAIM_MULTIPLES.map((multiple) => claimMargin(record, stated * multiple, hull.radius * HOSTILE_PAST_RADII, range, minFacing)));
|
|
4012
|
+
}
|
|
4013
|
+
function measureRays(input, world, rays) {
|
|
4014
|
+
const { source, l, nodes, count, hull, range } = input;
|
|
4015
|
+
let state = RAY_SEED >>> 0;
|
|
4016
|
+
const next = () => {
|
|
4017
|
+
state = Math.imul(state, 1664525) + 1013904223 >>> 0;
|
|
4018
|
+
return state / 2 ** 32;
|
|
4019
|
+
};
|
|
4020
|
+
const errors = new Float64Array(rays);
|
|
4021
|
+
const offSurface = new Float64Array(rays);
|
|
4022
|
+
const truths = new Float64Array(rays);
|
|
4023
|
+
const stopsOf = new Float64Array(rays);
|
|
4024
|
+
const facingsOf = new Float64Array(rays);
|
|
4025
|
+
let answered = 0;
|
|
4026
|
+
let surfaced = 0;
|
|
4027
|
+
const order = [];
|
|
4028
|
+
let stopped = 0;
|
|
4029
|
+
let passes = 0;
|
|
4030
|
+
let stops = 0;
|
|
4031
|
+
const keepWorst = (entry) => {
|
|
4032
|
+
if (order.length === REPORT_WORST && entry.error <= order[order.length - 1].error) return;
|
|
4033
|
+
order.push(entry);
|
|
4034
|
+
order.sort((a, b) => b.error - a.error);
|
|
4035
|
+
if (order.length > REPORT_WORST) order.pop();
|
|
4036
|
+
};
|
|
4037
|
+
for (let n = 0; n < rays; n++) {
|
|
4038
|
+
const node = nodes[Math.floor(next() * count)];
|
|
4039
|
+
const k = node % l.nz;
|
|
4040
|
+
const col = (node - k) / l.nz;
|
|
4041
|
+
const j = Math.floor(col / l.nx);
|
|
4042
|
+
const i = col - j * l.nx;
|
|
4043
|
+
const eyeHeight = next() < CROUCH_RAY_SHARE ? hull.crouchHeight * CROUCH_EYE_SHARE : hull.height * EYE_SHARE;
|
|
4044
|
+
const ox = l.x0 + (i + 0.5) * l.h;
|
|
4045
|
+
const oy = l.y0 + (j + 0.5) * l.h;
|
|
4046
|
+
const oz = l.z0 + k * l.h + eyeHeight;
|
|
4047
|
+
const level = next() < LEVEL_RAY_SHARE;
|
|
4048
|
+
const yaw = next() * 2 * Math.PI;
|
|
4049
|
+
const z = level ? (next() * 2 - 1) * LEVEL_RAY_SLOPE : next() * 2 - 1;
|
|
4050
|
+
const across = Math.sqrt(Math.max(0, 1 - z * z));
|
|
4051
|
+
const dx = Math.cos(yaw) * across;
|
|
4052
|
+
const dy = Math.sin(yaw) * across;
|
|
4053
|
+
const truth = source.ray(ox, oy, oz, dx, dy, z, range);
|
|
4054
|
+
const hit = shotRay(world, [ox, oy, oz], [dx, dy, z], range);
|
|
4055
|
+
if (truth === null && hit === null) continue;
|
|
4056
|
+
truths[answered] = truth ?? -1;
|
|
4057
|
+
stopsOf[answered] = hit === null ? -1 : hit.dist;
|
|
4058
|
+
facingsOf[answered] = hit === null || hit.axis < 0 ? 1 : Math.abs(hit.axis === 0 ? dx : hit.axis === 1 ? dy : z);
|
|
4059
|
+
answered += 1;
|
|
4060
|
+
if (hit !== null) offSurface[surfaced++] = source.offSurface(ox + dx * hit.dist, oy + dy * hit.dist, oz + z * hit.dist, input.h);
|
|
4061
|
+
if (truth === null) {
|
|
4062
|
+
stops += 1;
|
|
4063
|
+
keepWorst({ error: range, from: [ox, oy, oz], dir: [dx, dy, z], source: null, fit: hit?.dist ?? null });
|
|
4064
|
+
continue;
|
|
4065
|
+
}
|
|
4066
|
+
if (hit === null) {
|
|
4067
|
+
passes += 1;
|
|
4068
|
+
keepWorst({ error: range, from: [ox, oy, oz], dir: [dx, dy, z], source: truth, fit: null });
|
|
4069
|
+
continue;
|
|
4070
|
+
}
|
|
4071
|
+
const error = Math.abs(truth - hit.dist);
|
|
4072
|
+
errors[stopped++] = error;
|
|
4073
|
+
keepWorst({ error, from: [ox, oy, oz], dir: [dx, dy, z], source: truth, fit: hit.dist });
|
|
4074
|
+
}
|
|
4075
|
+
const sorted = errors.subarray(0, stopped).sort();
|
|
4076
|
+
const at = (share) => Math.round((sorted[Math.min(stopped - 1, Math.floor(share * stopped))] ?? 0) * 100) / 100;
|
|
4077
|
+
const pct = (v) => Math.round(1e4 * v / rays) / 100;
|
|
4078
|
+
const surfaces = offSurface.subarray(0, surfaced).sort();
|
|
4079
|
+
const surfaceAt = (share) => surfaces[Math.min(surfaced - 1, Math.floor(share * surfaced))] ?? 0;
|
|
4080
|
+
return { accuracy: { rays, hitMissPct: pct(passes + stops), passesPct: pct(passes), stopsPct: pct(stops), stopped, p50Units: at(0.5), p95Units: at(0.95), p99Units: at(0.99), maxUnits: at(1), surfaceP50Units: surfaceAt(0.5), surfaceP99Units: surfaceAt(0.99), surfaceMaxUnits: surfaceAt(1), worst: order.map(({ from: f2, dir, source: s, fit }) => ({ from: f2, dir, source: s, fit })) }, truths, stops: stopsOf, facings: facingsOf, answered };
|
|
4081
|
+
}
|
|
4082
|
+
function sceneShotRays(scene) {
|
|
4083
|
+
const triangles = new TriangleSet();
|
|
4084
|
+
scene.actors.forEach((actor, index) => {
|
|
4085
|
+
for (const surface of actor.surfaces) {
|
|
4086
|
+
if (stopsShot(actor, surface)) surfaceTriangles(surface, (ax, ay, az, bx, by, bz, cx, cy, cz) => triangles.add(index, ax, ay, az, bx, by, bz, cx, cy, cz));
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
const source = new SourceRays(triangles);
|
|
4090
|
+
return (origin, dir, range) => source.ray(origin[0], origin[1], origin[2], dir[0], dir[1], dir[2], range);
|
|
4091
|
+
}
|
|
4092
|
+
function costliest(scene, actorBoxes) {
|
|
4093
|
+
return Array.from(actorBoxes, (boxes, index) => ({ name: scene.actors[index].name, boxes, index })).filter((a) => a.boxes > 0).sort((a, b) => b.boxes - a.boxes || a.index - b.index).slice(0, REPORT_ACTORS2).map(({ name, boxes }) => ({ name, boxes }));
|
|
4094
|
+
}
|
|
4095
|
+
var answersShot = (actor) => actor.surfaces.some((surface) => stopsShot(actor, surface));
|
|
4096
|
+
function fitShotWorld(scene, options) {
|
|
4097
|
+
const started = performance.now();
|
|
4098
|
+
const hull = sceneHull(scene);
|
|
4099
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
4100
|
+
const range = options.range ?? hull.radius * DEFAULT_RANGE_RADII;
|
|
4101
|
+
const rays = options.rays ?? DEFAULT_RAYS;
|
|
4102
|
+
const { l: reachL, truth } = sceneReach(scene);
|
|
4103
|
+
const movableActors = scene.actors.filter((a) => a.tags.includes(SceneTag.Movable)).map((a) => a.name);
|
|
4104
|
+
const finish = (status, detail, rung, h, accuracy, claims, grid, tolerance, bytes) => {
|
|
4105
|
+
const unseen = rung === null ? [] : scene.actors.filter((a, index) => answersShot(a) && rung.actorsKept[index] === 0).map((a) => a.name);
|
|
4106
|
+
const report = {
|
|
4107
|
+
status,
|
|
4108
|
+
detail,
|
|
4109
|
+
budget: { maxBoxes: options.maxBoxes, maxBytes },
|
|
4110
|
+
boxes: rung?.boxes.length ?? 0,
|
|
4111
|
+
exactBoxes: rung?.exactBoxes ?? 0,
|
|
4112
|
+
coverBoxes: rung?.coverBoxes ?? 0,
|
|
4113
|
+
bytes,
|
|
4114
|
+
cell: h,
|
|
4115
|
+
toleranceUnits: tolerance,
|
|
4116
|
+
accuracy,
|
|
4117
|
+
claims,
|
|
4118
|
+
claimGrid: grid,
|
|
4119
|
+
seenAirPct: rung?.seenAirPct ?? 0,
|
|
4120
|
+
movableActors,
|
|
4121
|
+
unseenActors: unseen.slice(0, REPORT_UNSEEN),
|
|
4122
|
+
actors: rung === null ? [] : costliest(scene, rung.actorBoxes),
|
|
4123
|
+
seconds: Math.round((performance.now() - started) / 100) / 10
|
|
4124
|
+
};
|
|
4125
|
+
if (status !== ShotFitStatus.Ok || rung === null) return { report, world: null };
|
|
4126
|
+
return { report, world: { boxes: rung.boxes, ramps: [], tolerance } };
|
|
4127
|
+
};
|
|
4128
|
+
if (truth.count === 0) return finish(ShotFitStatus.NoReach, "no spawn stands anywhere in the scene: nothing to fit from", null, 0, null, [], [], 0, 0);
|
|
4129
|
+
const triangles = new TriangleSet();
|
|
4130
|
+
scene.actors.forEach((actor, index) => {
|
|
4131
|
+
for (const surface of actor.surfaces) {
|
|
4132
|
+
if (stopsShot(actor, surface)) surfaceTriangles(surface, (ax, ay, az, bx, by, bz, cx, cy, cz) => triangles.add(index, ax, ay, az, bx, by, bz, cx, cy, cz));
|
|
4133
|
+
}
|
|
4134
|
+
});
|
|
4135
|
+
const eyeBounds = eyeBoundsOf(reachL, truth.nodes, truth.count, hull);
|
|
4136
|
+
const input = { scene, hull, reachL, reachBody: truth.body, eyeBounds, triangles, exactOf: [], range };
|
|
4137
|
+
const cells = ladder(shotBounds(input), hull, options.cell);
|
|
4138
|
+
const source = new SourceRays(triangles);
|
|
4139
|
+
const ownBudget = (rung) => JSON.stringify(rung.boxes).length;
|
|
4140
|
+
let last = null;
|
|
4141
|
+
for (const h of cells) {
|
|
4142
|
+
let rung = fitOnLattice(input, h, true, options.maxBoxes, options.log);
|
|
4143
|
+
if (rung.exactBoxes > options.maxBoxes / 2 && (rung.over || ownBudget(rung) > maxBytes)) rung = fitOnLattice(input, h, false, options.maxBoxes, options.log);
|
|
4144
|
+
last = { rung, h };
|
|
4145
|
+
const bytes = ownBudget(rung);
|
|
4146
|
+
if (rung.over || rung.boxes.length > options.maxBoxes || bytes > maxBytes) continue;
|
|
4147
|
+
const provisional = { boxes: rung.boxes, ramps: [], tolerance: 0 };
|
|
4148
|
+
const record = measureRays({ source, h, l: reachL, nodes: truth.nodes, count: truth.count, hull, range }, provisional, rays);
|
|
4149
|
+
const { accuracy } = record;
|
|
4150
|
+
const tolerance = Math.max(gridOf(h), accuracy.surfaceP99Units);
|
|
4151
|
+
const claims = claimMargins(record, tolerance, hull, range);
|
|
4152
|
+
const grid = options.facings === void 0 ? [] : claimGrid(record, tolerance, hull, range, options.facings);
|
|
4153
|
+
if (accuracy.hitMissPct > MAX_HIT_MISS_PCT) {
|
|
4154
|
+
return finish(ShotFitStatus.Inaccurate, `at the cell the budget holds (${h} units) ${accuracy.hitMissPct}% of rays disagree with the source; a shot world may disagree on ${MAX_HIT_MISS_PCT}%: raise the budget or simplify what is named`, rung, h, accuracy, claims, grid, tolerance, bytes);
|
|
4155
|
+
}
|
|
4156
|
+
options.log?.(`rays ${accuracy.rays}: hit or miss ${accuracy.hitMissPct}%, off by ${accuracy.p50Units} / ${accuracy.p95Units} / ${accuracy.p99Units} / ${accuracy.maxUnits} along the ray, ${accuracy.surfaceP50Units} / ${accuracy.surfaceP99Units} / ${accuracy.surfaceMaxUnits} from a surface`);
|
|
4157
|
+
return finish(ShotFitStatus.Ok, "fits", rung, h, accuracy, claims, grid, tolerance, bytes);
|
|
4158
|
+
}
|
|
4159
|
+
const named = last === null ? [] : costliest(scene, last.rung.actorBoxes).slice(0, 5).map((a) => `${a.name} (${a.boxes} boxes)`);
|
|
4160
|
+
const need = last === null ? 0 : last.rung.boxes.length;
|
|
4161
|
+
return finish(ShotFitStatus.OverBudget, `needs ${need} boxes${last?.rung.over ? " or more" : ""} or ${last === null ? 0 : ownBudget(last.rung)} bytes at the coarsest cell; the budget is ${options.maxBoxes} and ${maxBytes}. Costliest: ${named.join(", ")}. Simplify those or raise the budget`, last?.rung ?? null, last?.h ?? 0, null, [], [], 0, last === null ? 0 : ownBudget(last.rung));
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// src/world-fit/gltf.ts
|
|
4165
|
+
import { createHash } from "crypto";
|
|
4166
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync } from "fs";
|
|
4167
|
+
import { basename, dirname, join, sep } from "path";
|
|
4168
|
+
var DEFAULT_UNITS_PER_METER = 39.37;
|
|
4169
|
+
var GLB_MAGIC = 1179937895;
|
|
4170
|
+
var CHUNK_JSON = 1313821514;
|
|
4171
|
+
var CHUNK_BIN = 5130562;
|
|
4172
|
+
var GLB_HEADER_BYTES = 12;
|
|
4173
|
+
var CHUNK_HEADER_BYTES = 8;
|
|
4174
|
+
var GLTF_MAJOR = 2;
|
|
4175
|
+
var QUATERNION_SLACK = 0.01;
|
|
4176
|
+
var ComponentType = { I8: 5120, U8: 5121, I16: 5122, U16: 5123, U32: 5125, F32: 5126 };
|
|
4177
|
+
var COMPONENT_BYTES = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
4178
|
+
var PrimitiveMode = { Points: 0, Lines: 1, LineLoop: 2, LineStrip: 3, Triangles: 4, TriangleStrip: 5, TriangleFan: 6 };
|
|
4179
|
+
var Extension = {
|
|
4180
|
+
Draco: "KHR_draco_mesh_compression",
|
|
4181
|
+
MeshoptExt: "EXT_meshopt_compression",
|
|
4182
|
+
MeshoptKhr: "KHR_meshopt_compression",
|
|
4183
|
+
Quantization: "KHR_mesh_quantization",
|
|
4184
|
+
Instancing: "EXT_mesh_gpu_instancing"
|
|
4185
|
+
};
|
|
4186
|
+
var READS_FINE = [Extension.Quantization, Extension.Instancing, Extension.Draco, Extension.MeshoptExt, Extension.MeshoptKhr, "KHR_lights_punctual", "KHR_materials_", "KHR_texture_", "EXT_texture_", "KHR_animation_pointer", "KHR_xmp", "KHR_node_"];
|
|
4187
|
+
var COMPRESSION_FIX = "re-export uncompressed (gltf-transform copy, or your exporter's compression off), or load it in three.js and use sceneFromThree, whose loader decodes it";
|
|
4188
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4189
|
+
var isIndex = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
4190
|
+
function readPlainFile(path, refuse, tooLarge) {
|
|
4191
|
+
const before = lstatSync(path);
|
|
4192
|
+
if (!before.isFile()) throw refuse("is not a plain file");
|
|
4193
|
+
if (before.size > LEVEL_LIMITS.maxFileBytes) throw tooLarge();
|
|
4194
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
4195
|
+
try {
|
|
4196
|
+
const now = fstatSync(fd);
|
|
4197
|
+
if (!now.isFile() || now.ino !== before.ino || now.dev !== before.dev) throw refuse("changed while it was read");
|
|
4198
|
+
if (now.size > LEVEL_LIMITS.maxFileBytes) throw tooLarge();
|
|
4199
|
+
return readFileSync(fd);
|
|
4200
|
+
} finally {
|
|
4201
|
+
closeSync(fd);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
function relativeUri(uri, where) {
|
|
4205
|
+
const refuse = (why) => new LevelRefusal(LevelRefusalCode.UriRefused, `${where}: the uri ${JSON.stringify(uri.slice(0, 200))} ${why}; keep the buffer beside the file, or embed it`);
|
|
4206
|
+
if (uri === "") throw refuse("is empty");
|
|
4207
|
+
let decoded;
|
|
4208
|
+
try {
|
|
4209
|
+
decoded = decodeURIComponent(uri);
|
|
4210
|
+
} catch {
|
|
4211
|
+
throw refuse("has a bad percent escape");
|
|
4212
|
+
}
|
|
4213
|
+
if (/[\0\\:?#%]/.test(decoded)) throw refuse("holds a character no plain relative path needs");
|
|
4214
|
+
if (decoded.startsWith("/")) throw refuse("is absolute");
|
|
4215
|
+
for (const segment of decoded.split("/")) {
|
|
4216
|
+
if (segment === "" || segment.endsWith(".") || segment.endsWith(" ")) throw refuse("leaves the input folder or is one a path would collapse");
|
|
4217
|
+
}
|
|
4218
|
+
return decoded;
|
|
4219
|
+
}
|
|
4220
|
+
var DATA_URI = /^data:application\/(octet-stream|gltf-buffer);base64,/;
|
|
4221
|
+
function dataUri(uri, where) {
|
|
4222
|
+
if (!uri.startsWith("data:")) return null;
|
|
4223
|
+
const head = DATA_URI.exec(uri);
|
|
4224
|
+
if (head === null) throw new LevelRefusal(LevelRefusalCode.UriRefused, `${where}: only a base64 data uri of application/octet-stream or application/gltf-buffer is read`);
|
|
4225
|
+
const encodedChars = uri.length - head[0].length;
|
|
4226
|
+
if (encodedChars * 3 / 4 > LEVEL_LIMITS.maxFileBytes) throw new LevelRefusal(LevelRefusalCode.TooLarge, `${where}: a data uri past ${LEVEL_LIMITS.maxFileBytes} bytes`);
|
|
4227
|
+
return Buffer.from(uri.slice(head[0].length), "base64");
|
|
4228
|
+
}
|
|
4229
|
+
var Document = class {
|
|
4230
|
+
json;
|
|
4231
|
+
buffers = /* @__PURE__ */ new Map();
|
|
4232
|
+
bin;
|
|
4233
|
+
root;
|
|
4234
|
+
readBytes = 0;
|
|
4235
|
+
uriOf;
|
|
4236
|
+
quantized;
|
|
4237
|
+
/** POSITION and index reads, by accessor. */
|
|
4238
|
+
positionsCache = /* @__PURE__ */ new Map();
|
|
4239
|
+
constructor(json, bin, root) {
|
|
4240
|
+
this.json = json;
|
|
4241
|
+
this.bin = bin;
|
|
4242
|
+
this.root = root;
|
|
4243
|
+
const declared = Array.isArray(json.buffers) ? json.buffers : [];
|
|
4244
|
+
this.uriOf = declared.map((b, i) => {
|
|
4245
|
+
const where = `$.buffers[${i}]`;
|
|
4246
|
+
if (!isObject2(b)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${where} is not an object`);
|
|
4247
|
+
if (b.uri === void 0) return null;
|
|
4248
|
+
if (typeof b.uri !== "string") throw new LevelRefusal(LevelRefusalCode.UriRefused, `${where}: the uri is not text`);
|
|
4249
|
+
if (!b.uri.startsWith("data:")) relativeUri(b.uri, where);
|
|
4250
|
+
else dataUri(b.uri.slice(0, 256), where);
|
|
4251
|
+
return b.uri;
|
|
4252
|
+
});
|
|
4253
|
+
this.quantized = Array.isArray(json.extensionsUsed) && json.extensionsUsed.includes(Extension.Quantization);
|
|
4254
|
+
}
|
|
4255
|
+
list(name) {
|
|
4256
|
+
const v = this.json[name];
|
|
4257
|
+
return Array.isArray(v) ? v : [];
|
|
4258
|
+
}
|
|
4259
|
+
entry(name, index, where) {
|
|
4260
|
+
if (!isIndex(index)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${where} is not an index`);
|
|
4261
|
+
const item = this.list(name)[index];
|
|
4262
|
+
if (!isObject2(item)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${where} names ${name}[${index}], which is not there`);
|
|
4263
|
+
return item;
|
|
4264
|
+
}
|
|
4265
|
+
buffer(index, where) {
|
|
4266
|
+
const cached = this.buffers.get(index);
|
|
4267
|
+
if (cached !== void 0) return cached;
|
|
4268
|
+
const spec = this.entry("buffers", index, where);
|
|
4269
|
+
const bufferWhere = `$.buffers[${index}]`;
|
|
4270
|
+
const uri = this.uriOf[index] ?? null;
|
|
4271
|
+
let bytes;
|
|
4272
|
+
if (uri === null) {
|
|
4273
|
+
if (index !== 0 || this.bin === null) throw new LevelRefusal(LevelRefusalCode.BadReference, `${bufferWhere} has no uri and the file has no binary chunk for it`);
|
|
4274
|
+
bytes = this.bin;
|
|
4275
|
+
} else {
|
|
4276
|
+
bytes = dataUri(uri, bufferWhere) ?? this.externalBuffer(relativeUri(uri, bufferWhere), uri, bufferWhere);
|
|
4277
|
+
}
|
|
4278
|
+
if (!isIndex(spec.byteLength) || bytes.length < spec.byteLength) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${bufferWhere} says ${String(spec.byteLength)} bytes and holds ${bytes.length}`);
|
|
4279
|
+
this.readBytes += bytes.length;
|
|
4280
|
+
if (this.readBytes > LEVEL_LIMITS.maxFileBytes) throw new LevelRefusal(LevelRefusalCode.TooLarge, `the level's buffers pass ${LEVEL_LIMITS.maxFileBytes} bytes together`);
|
|
4281
|
+
this.buffers.set(index, bytes);
|
|
4282
|
+
return bytes;
|
|
4283
|
+
}
|
|
4284
|
+
externalBuffer(relative, uri, where) {
|
|
4285
|
+
const refuse = (why) => new LevelRefusal(LevelRefusalCode.UriRefused, `${where}: ${uri.slice(0, 200)} ${why}`);
|
|
4286
|
+
let real;
|
|
4287
|
+
try {
|
|
4288
|
+
real = realpathSync(join(this.root, relative));
|
|
4289
|
+
} catch {
|
|
4290
|
+
throw refuse("is not there, beside the file");
|
|
4291
|
+
}
|
|
4292
|
+
if (!real.startsWith(this.root + sep)) throw refuse("leads outside the input folder");
|
|
4293
|
+
return readPlainFile(real, refuse, () => new LevelRefusal(LevelRefusalCode.TooLarge, `${where}: ${uri.slice(0, 200)} is past ${LEVEL_LIMITS.maxFileBytes} bytes`));
|
|
4294
|
+
}
|
|
4295
|
+
/** An accessor's own shape: what it reads, from which view, at what stride. Nothing decoded. */
|
|
4296
|
+
layout(accessorIndex, where, elementComponents) {
|
|
4297
|
+
const accessor = this.entry("accessors", accessorIndex, where);
|
|
4298
|
+
const at = `$.accessors[${String(accessorIndex)}]`;
|
|
4299
|
+
const bad = (why) => new LevelRefusal(LevelRefusalCode.BadAccessor, `${at} ${why}`);
|
|
4300
|
+
if (accessor.sparse !== void 0) throw bad("is sparse: re-export it plain");
|
|
4301
|
+
if (accessor.bufferView === void 0) throw bad("has no bufferView");
|
|
4302
|
+
const count = accessor.count;
|
|
4303
|
+
if (!isIndex(count) || count === 0) throw bad("has no usable count");
|
|
4304
|
+
const componentType = accessor.componentType;
|
|
4305
|
+
const componentBytes = COMPONENT_BYTES[componentType];
|
|
4306
|
+
if (componentBytes === void 0) throw bad("has an unknown componentType");
|
|
4307
|
+
const viewAt = `$.bufferViews[${String(accessor.bufferView)}]`;
|
|
4308
|
+
const view = this.entry("bufferViews", accessor.bufferView, `${at}.bufferView`);
|
|
4309
|
+
if (isObject2(view.extensions)) {
|
|
4310
|
+
for (const name of [Extension.MeshoptExt, Extension.MeshoptKhr]) {
|
|
4311
|
+
if (view.extensions[name] !== void 0) throw new LevelRefusal(LevelRefusalCode.Compressed, `${viewAt} is compressed with ${name}: ${COMPRESSION_FIX}`);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
const badView = (why) => new LevelRefusal(LevelRefusalCode.BadAccessor, `${viewAt} ${why}`);
|
|
4315
|
+
if (!isIndex(view.buffer) || !isObject2(this.list("buffers")[view.buffer])) throw new LevelRefusal(LevelRefusalCode.BadReference, `${viewAt}.buffer names no buffer`);
|
|
4316
|
+
const viewOffset = view.byteOffset ?? 0;
|
|
4317
|
+
const viewLength = view.byteLength;
|
|
4318
|
+
if (!isIndex(viewOffset) || !isIndex(viewLength)) throw badView("has no usable offset or length");
|
|
4319
|
+
const spec = this.list("buffers")[view.buffer];
|
|
4320
|
+
if (!isIndex(spec.byteLength) || viewOffset + viewLength > spec.byteLength) throw badView(`runs past its buffer's ${String(spec.byteLength)} bytes`);
|
|
4321
|
+
const element = componentBytes * elementComponents;
|
|
4322
|
+
const stride = view.byteStride ?? element;
|
|
4323
|
+
if (!isIndex(stride) || stride < element) throw badView(`has a stride under its ${element}-byte element`);
|
|
4324
|
+
const accessorOffset = accessor.byteOffset ?? 0;
|
|
4325
|
+
if (!isIndex(accessorOffset)) throw bad("has no usable byteOffset");
|
|
4326
|
+
if (accessorOffset + (count - 1) * stride + element > viewLength) throw bad(`reads ${count} elements past its view's ${viewLength} bytes`);
|
|
4327
|
+
const bytes = this.buffer(view.buffer, viewAt);
|
|
4328
|
+
return { bytes, offset: viewOffset + accessorOffset, stride, count, componentType, normalized: accessor.normalized === true };
|
|
4329
|
+
}
|
|
4330
|
+
/** An accessor's element count, checked for type, before anything is decoded. */
|
|
4331
|
+
checkType(accessorIndex, where, type) {
|
|
4332
|
+
const accessor = this.entry("accessors", accessorIndex, where);
|
|
4333
|
+
if (accessor.type !== type) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `$.accessors[${String(accessorIndex)}] is not ${type}`);
|
|
4334
|
+
}
|
|
4335
|
+
/** How many elements an accessor holds. */
|
|
4336
|
+
countOf(accessorIndex, where) {
|
|
4337
|
+
const count = this.entry("accessors", accessorIndex, where).count;
|
|
4338
|
+
if (!isIndex(count)) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `$.accessors[${String(accessorIndex)}] has no usable count`);
|
|
4339
|
+
return count;
|
|
4340
|
+
}
|
|
4341
|
+
/** VEC3 positions, as the numbers they mean. */
|
|
4342
|
+
positions(accessorIndex, where) {
|
|
4343
|
+
this.checkType(accessorIndex, where, "VEC3");
|
|
4344
|
+
const cached = this.positionsCache.get(accessorIndex);
|
|
4345
|
+
if (cached !== void 0) return cached;
|
|
4346
|
+
const out = this.numbers(accessorIndex, where, 3, true);
|
|
4347
|
+
this.positionsCache.set(accessorIndex, out);
|
|
4348
|
+
return out;
|
|
4349
|
+
}
|
|
4350
|
+
/** A float or normalized-integer accessor as numbers. `quantizable`: integers only with KHR_mesh_quantization. */
|
|
4351
|
+
numbers(accessorIndex, where, components, quantizable) {
|
|
4352
|
+
const layout = this.layout(accessorIndex, where, components);
|
|
4353
|
+
if (layout.count > LEVEL_LIMITS.maxVertices) throw new LevelRefusal(LevelRefusalCode.TooLarge, `$.accessors[${String(accessorIndex)}] holds ${layout.count} vertices, past ${LEVEL_LIMITS.maxVertices}`);
|
|
4354
|
+
const { bytes, offset, stride, count, componentType, normalized } = layout;
|
|
4355
|
+
if (componentType !== ComponentType.F32 && quantizable && !this.quantized) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `$.accessors[${String(accessorIndex)}] is not float, and the file does not use ${Extension.Quantization}`);
|
|
4356
|
+
const read = this.reader(bytes, componentType, normalized, accessorIndex);
|
|
4357
|
+
const size = COMPONENT_BYTES[componentType];
|
|
4358
|
+
const out = new Float64Array(count * components);
|
|
4359
|
+
for (let i = 0; i < count; i++) {
|
|
4360
|
+
for (let c = 0; c < components; c++) out[i * components + c] = read(offset + i * stride + c * size);
|
|
4361
|
+
}
|
|
4362
|
+
return out;
|
|
4363
|
+
}
|
|
4364
|
+
reader(bytes, componentType, normalized, accessorIndex) {
|
|
4365
|
+
switch (componentType) {
|
|
4366
|
+
case ComponentType.F32:
|
|
4367
|
+
return (at) => bytes.readFloatLE(at);
|
|
4368
|
+
case ComponentType.I8:
|
|
4369
|
+
return normalized ? (at) => Math.max(bytes.readInt8(at) / 127, -1) : (at) => bytes.readInt8(at);
|
|
4370
|
+
case ComponentType.U8:
|
|
4371
|
+
return normalized ? (at) => bytes.readUInt8(at) / 255 : (at) => bytes.readUInt8(at);
|
|
4372
|
+
case ComponentType.I16:
|
|
4373
|
+
return normalized ? (at) => Math.max(bytes.readInt16LE(at) / 32767, -1) : (at) => bytes.readInt16LE(at);
|
|
4374
|
+
case ComponentType.U16:
|
|
4375
|
+
return normalized ? (at) => bytes.readUInt16LE(at) / 65535 : (at) => bytes.readUInt16LE(at);
|
|
4376
|
+
default:
|
|
4377
|
+
throw new LevelRefusal(LevelRefusalCode.BadAccessor, `$.accessors[${String(accessorIndex)}] has a componentType these numbers cannot be`);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
/** A SCALAR index accessor as indices. */
|
|
4381
|
+
indices(accessorIndex, where) {
|
|
4382
|
+
this.checkType(accessorIndex, where, "SCALAR");
|
|
4383
|
+
const { bytes, offset, stride, count, componentType, normalized } = this.layout(accessorIndex, where, 1);
|
|
4384
|
+
const at = `$.accessors[${String(accessorIndex)}]`;
|
|
4385
|
+
if (normalized) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${at} is normalized and an index cannot be`);
|
|
4386
|
+
const out = new Uint32Array(count);
|
|
4387
|
+
if (componentType === ComponentType.U16) for (let i = 0; i < count; i++) out[i] = bytes.readUInt16LE(offset + i * stride);
|
|
4388
|
+
else if (componentType === ComponentType.U32) for (let i = 0; i < count; i++) out[i] = bytes.readUInt32LE(offset + i * stride);
|
|
4389
|
+
else if (componentType === ComponentType.U8) for (let i = 0; i < count; i++) out[i] = bytes.readUInt8(offset + i * stride);
|
|
4390
|
+
else throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${at} is not an index type`);
|
|
4391
|
+
return out;
|
|
4392
|
+
}
|
|
4393
|
+
};
|
|
4394
|
+
function numberList(raw, length, where) {
|
|
4395
|
+
if (!Array.isArray(raw) || raw.length !== length) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where} is not ${length} numbers`);
|
|
4396
|
+
return raw.map((v, i) => {
|
|
4397
|
+
if (typeof v !== "number") throw new LevelRefusal(LevelRefusalCode.BadValue, `${where}[${i}] is not a number`);
|
|
4398
|
+
if (!Number.isFinite(v)) throw new LevelRefusal(LevelRefusalCode.NotFinite, `${where}[${i}] is ${v}`);
|
|
4399
|
+
return v;
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
function localMatrix(node, where) {
|
|
4403
|
+
const hasTrs = node.translation !== void 0 || node.rotation !== void 0 || node.scale !== void 0;
|
|
4404
|
+
if (node.matrix !== void 0) {
|
|
4405
|
+
if (hasTrs) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where}.matrix sits beside a translation, rotation or scale: glTF allows one or the other`);
|
|
4406
|
+
return numberList(node.matrix, 16, `${where}.matrix`);
|
|
4407
|
+
}
|
|
4408
|
+
if (!hasTrs) return IDENTITY_MATRIX;
|
|
4409
|
+
const t = node.translation === void 0 ? [0, 0, 0] : numberList(node.translation, 3, `${where}.translation`);
|
|
4410
|
+
const q = node.rotation === void 0 ? [0, 0, 0, 1] : numberList(node.rotation, 4, `${where}.rotation`);
|
|
4411
|
+
const s = node.scale === void 0 ? [1, 1, 1] : numberList(node.scale, 3, `${where}.scale`);
|
|
4412
|
+
const length = Math.hypot(q[0], q[1], q[2], q[3]);
|
|
4413
|
+
if (Math.abs(length - 1) > QUATERNION_SLACK) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where}.rotation is not a unit quaternion`);
|
|
4414
|
+
return composeMatrix(t, q, s);
|
|
4415
|
+
}
|
|
4416
|
+
function animatedNodes(json) {
|
|
4417
|
+
const out = /* @__PURE__ */ new Set();
|
|
4418
|
+
if (!Array.isArray(json.animations)) return out;
|
|
4419
|
+
for (const animation of json.animations) {
|
|
4420
|
+
if (!isObject2(animation) || !Array.isArray(animation.channels)) continue;
|
|
4421
|
+
for (const channel of animation.channels) {
|
|
4422
|
+
if (!isObject2(channel) || !isObject2(channel.target)) continue;
|
|
4423
|
+
const path = channel.target.path;
|
|
4424
|
+
if (isIndex(channel.target.node) && (path === "translation" || path === "rotation" || path === "scale")) out.add(channel.target.node);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
return out;
|
|
4428
|
+
}
|
|
4429
|
+
function asTriangles(mode, indices2) {
|
|
4430
|
+
if (mode === PrimitiveMode.Triangles) return indices2;
|
|
4431
|
+
const triangles = Math.max(0, indices2.length - 2);
|
|
4432
|
+
const out = new Uint32Array(triangles * 3);
|
|
4433
|
+
for (let i = 0; i < triangles; i++) {
|
|
4434
|
+
if (mode === PrimitiveMode.TriangleFan) {
|
|
4435
|
+
out[i * 3] = indices2[0];
|
|
4436
|
+
out[i * 3 + 1] = indices2[i + 1];
|
|
4437
|
+
out[i * 3 + 2] = indices2[i + 2];
|
|
4438
|
+
} else if (i % 2 === 0) {
|
|
4439
|
+
out[i * 3] = indices2[i];
|
|
4440
|
+
out[i * 3 + 1] = indices2[i + 1];
|
|
4441
|
+
out[i * 3 + 2] = indices2[i + 2];
|
|
4442
|
+
} else {
|
|
4443
|
+
out[i * 3] = indices2[i + 2];
|
|
4444
|
+
out[i * 3 + 1] = indices2[i + 1];
|
|
4445
|
+
out[i * 3 + 2] = indices2[i];
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
return out;
|
|
4449
|
+
}
|
|
4450
|
+
var Walker = class {
|
|
4451
|
+
doc;
|
|
4452
|
+
meshes = [];
|
|
4453
|
+
spawns = [];
|
|
4454
|
+
notes = [];
|
|
4455
|
+
nodes;
|
|
4456
|
+
visited;
|
|
4457
|
+
animated;
|
|
4458
|
+
primitives = /* @__PURE__ */ new Map();
|
|
4459
|
+
skinned = [];
|
|
4460
|
+
morphed = [];
|
|
4461
|
+
triangles = 0;
|
|
4462
|
+
constructor(doc) {
|
|
4463
|
+
this.doc = doc;
|
|
4464
|
+
this.nodes = Array.isArray(doc.json.nodes) ? doc.json.nodes : [];
|
|
4465
|
+
if (this.nodes.length > LEVEL_LIMITS.maxNodes) throw new LevelRefusal(LevelRefusalCode.TooLarge, `the level holds ${this.nodes.length} nodes, past ${LEVEL_LIMITS.maxNodes}`);
|
|
4466
|
+
this.visited = new Uint8Array(this.nodes.length);
|
|
4467
|
+
this.animated = animatedNodes(doc.json);
|
|
4468
|
+
}
|
|
4469
|
+
walk(roots, rootWhere, rootMark) {
|
|
4470
|
+
const stack = [];
|
|
4471
|
+
for (let i = roots.length - 1; i >= 0; i--) stack.push({ index: roots[i], where: `${rootWhere}.nodes[${i}]`, parent: IDENTITY_MATRIX, mark: rootMark, path: "", depth: 0 });
|
|
4472
|
+
for (let frame = stack.pop(); frame !== void 0; frame = stack.pop()) {
|
|
4473
|
+
const { index, where } = frame;
|
|
4474
|
+
const node = this.doc.entry("nodes", index, where);
|
|
4475
|
+
const i = index;
|
|
4476
|
+
const name = typeof node.name === "string" && node.name !== "" ? node.name : `node_${i}`;
|
|
4477
|
+
if (this.visited[i] === 1) throw new LevelRefusal(LevelRefusalCode.NodeCycle, `${name} is reached twice: a cycle, or a node with two parents`);
|
|
4478
|
+
this.visited[i] = 1;
|
|
4479
|
+
if (frame.depth > LEVEL_LIMITS.maxDepth) throw new LevelRefusal(LevelRefusalCode.TooLarge, `${name} sits past a depth of ${LEVEL_LIMITS.maxDepth} nodes`);
|
|
4480
|
+
const nodeWhere = `$.nodes[${i}]`;
|
|
4481
|
+
const genex = isObject2(node.extras) ? node.extras.genex : void 0;
|
|
4482
|
+
const own = levelMarkOf(name, genex, name);
|
|
4483
|
+
const mark = inheritMark(frame.mark, this.animated.has(i) ? { ...own, movable: true } : own);
|
|
4484
|
+
const world = multiplyMatrix(frame.parent, localMatrix(node, nodeWhere));
|
|
4485
|
+
const path = frame.path === "" ? name : `${frame.path}/${name}`;
|
|
4486
|
+
if (own.spawn === true) this.spawns.push({ name: spawnNameOf(name), at: [world[12], world[13], world[14]], tags: own.tags ?? [] });
|
|
4487
|
+
else if (node.mesh !== void 0) this.addMesh(node, nodeWhere, name, path, mark, world);
|
|
4488
|
+
const children = node.children;
|
|
4489
|
+
if (children === void 0) continue;
|
|
4490
|
+
if (!Array.isArray(children)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${nodeWhere}.children is not a list`);
|
|
4491
|
+
for (let c = children.length - 1; c >= 0; c--) stack.push({ index: children[c], where: `${nodeWhere}.children[${c}]`, parent: world, mark, path, depth: frame.depth + 1 });
|
|
4492
|
+
}
|
|
4493
|
+
this.refuseThoseThatMove();
|
|
4494
|
+
}
|
|
4495
|
+
refuseThoseThatMove() {
|
|
4496
|
+
if (this.skinned.length > 0) throw new LevelRefusal(LevelRefusalCode.Skinned, `a room's world is static and these are skinned: ${namesInRefusal(this.skinned)}. Mark them nocol_ and collide with a static proxy, or movable_`);
|
|
4497
|
+
if (this.morphed.length > 0) throw new LevelRefusal(LevelRefusalCode.Morphed, `a room's world is static and these are posed by morph targets: ${namesInRefusal(this.morphed)}. Mark them nocol_ and collide with a static proxy, or movable_`);
|
|
4498
|
+
}
|
|
4499
|
+
addMesh(node, nodeWhere, name, path, mark, world) {
|
|
4500
|
+
const meshIndex = node.mesh;
|
|
4501
|
+
const mesh = this.doc.entry("meshes", meshIndex, `${nodeWhere}.mesh`);
|
|
4502
|
+
if (mark.collide === LevelCollide.None) return;
|
|
4503
|
+
if (!Array.isArray(mesh.primitives)) throw new LevelRefusal(LevelRefusalCode.BadReference, `$.meshes[${String(meshIndex)}].primitives is not a list`);
|
|
4504
|
+
if (mark.movable !== true) {
|
|
4505
|
+
if (node.skin !== void 0) {
|
|
4506
|
+
this.skinned.push(name);
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4509
|
+
const weights = node.weights ?? mesh.weights;
|
|
4510
|
+
const posed = Array.isArray(weights) && weights.some((w) => w !== 0);
|
|
4511
|
+
const morphs = mesh.primitives.some((p) => isObject2(p) && Array.isArray(p.targets) && p.targets.length > 0);
|
|
4512
|
+
if (posed && morphs) {
|
|
4513
|
+
this.morphed.push(name);
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
const instances = this.instancesOf(node, nodeWhere, world);
|
|
4518
|
+
const primitives = [];
|
|
4519
|
+
mesh.primitives.forEach((_raw, k) => {
|
|
4520
|
+
const p = this.primitive(meshIndex, k, mesh.primitives, instances.count, name);
|
|
4521
|
+
if (p !== null) primitives.push(p);
|
|
4522
|
+
});
|
|
4523
|
+
if (primitives.length === 0) {
|
|
4524
|
+
this.notes.push(`${name}: holds no triangles (points or lines), so it is not in the scene`);
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
this.meshes.push({ name, path, mark, instances: instances.matrices(), primitives });
|
|
4528
|
+
}
|
|
4529
|
+
instancesOf(node, nodeWhere, world) {
|
|
4530
|
+
const spec = isObject2(node.extensions) ? node.extensions[Extension.Instancing] : void 0;
|
|
4531
|
+
if (spec === void 0) return { count: 1, matrices: () => [world] };
|
|
4532
|
+
const where = `${nodeWhere}.extensions.${Extension.Instancing}`;
|
|
4533
|
+
if (!isObject2(spec) || !isObject2(spec.attributes)) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where} holds no attributes`);
|
|
4534
|
+
const { TRANSLATION: t, ROTATION: r, SCALE: s } = spec.attributes;
|
|
4535
|
+
const named = [t, r, s].find((a) => a !== void 0);
|
|
4536
|
+
if (named === void 0) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where} holds no attributes`);
|
|
4537
|
+
const count = this.doc.countOf(named, `${where}.attributes`);
|
|
4538
|
+
return {
|
|
4539
|
+
count,
|
|
4540
|
+
matrices: () => {
|
|
4541
|
+
const ts = t === void 0 ? null : this.doc.numbers(t, `${where}.attributes.TRANSLATION`, 3, false);
|
|
4542
|
+
const rs = r === void 0 ? null : this.doc.numbers(r, `${where}.attributes.ROTATION`, 4, false);
|
|
4543
|
+
const ss = s === void 0 ? null : this.doc.numbers(s, `${where}.attributes.SCALE`, 3, false);
|
|
4544
|
+
for (const list of [[ts, 3], [rs, 4], [ss, 3]]) {
|
|
4545
|
+
if (list[0] !== null && list[0].length !== count * list[1]) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${where}: its attributes hold different counts`);
|
|
4546
|
+
}
|
|
4547
|
+
const out = [];
|
|
4548
|
+
for (let i = 0; i < count; i++) {
|
|
4549
|
+
const translation = ts === null ? [0, 0, 0] : [ts[i * 3], ts[i * 3 + 1], ts[i * 3 + 2]];
|
|
4550
|
+
const rotation = rs === null ? [0, 0, 0, 1] : [rs[i * 4], rs[i * 4 + 1], rs[i * 4 + 2], rs[i * 4 + 3]];
|
|
4551
|
+
const scale = ss === null ? [1, 1, 1] : [ss[i * 3], ss[i * 3 + 1], ss[i * 3 + 2]];
|
|
4552
|
+
out.push(multiplyMatrix(world, composeMatrix(translation, rotation, scale)));
|
|
4553
|
+
}
|
|
4554
|
+
return out;
|
|
4555
|
+
}
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
primitive(meshIndex, k, list, instanceCount, name) {
|
|
4559
|
+
const raw = list[k];
|
|
4560
|
+
const where = `$.meshes[${meshIndex}].primitives[${k}]`;
|
|
4561
|
+
if (!isObject2(raw)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${where} is not an object`);
|
|
4562
|
+
if (isObject2(raw.extensions) && raw.extensions[Extension.Draco] !== void 0) throw new LevelRefusal(LevelRefusalCode.Compressed, `${name}: ${where} is compressed with ${Extension.Draco}: ${COMPRESSION_FIX}`);
|
|
4563
|
+
const mode = raw.mode ?? PrimitiveMode.Triangles;
|
|
4564
|
+
if (mode === PrimitiveMode.Points || mode === PrimitiveMode.Lines || mode === PrimitiveMode.LineLoop || mode === PrimitiveMode.LineStrip) return null;
|
|
4565
|
+
if (mode !== PrimitiveMode.Triangles && mode !== PrimitiveMode.TriangleStrip && mode !== PrimitiveMode.TriangleFan) throw new LevelRefusal(LevelRefusalCode.BadValue, `${where}.mode is not a primitive mode`);
|
|
4566
|
+
const attributes = raw.attributes;
|
|
4567
|
+
if (!isObject2(attributes) || attributes.POSITION === void 0) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${name}: ${where} has no POSITION`);
|
|
4568
|
+
const positionsAt = `${where}.attributes.POSITION`;
|
|
4569
|
+
const positionCount = this.doc.countOf(attributes.POSITION, positionsAt);
|
|
4570
|
+
const indexCount = raw.indices === void 0 ? positionCount : this.doc.countOf(raw.indices, `${where}.indices`);
|
|
4571
|
+
const triangleCount = mode === PrimitiveMode.Triangles ? indexCount / 3 : Math.max(0, indexCount - 2);
|
|
4572
|
+
this.triangles += Math.ceil(triangleCount) * instanceCount;
|
|
4573
|
+
if (this.triangles > LEVEL_LIMITS.maxTriangles) throw new LevelRefusal(LevelRefusalCode.TooLarge, `the level holds over ${LEVEL_LIMITS.maxTriangles} triangles (${name} is where it passes); give what no one walks on a nocol_ mark or a collision proxy`);
|
|
4574
|
+
const key = `${meshIndex}/${k}`;
|
|
4575
|
+
if (this.primitives.has(key)) return this.primitives.get(key) ?? null;
|
|
4576
|
+
const positions = this.doc.positions(attributes.POSITION, positionsAt);
|
|
4577
|
+
const stated = raw.indices === void 0 ? Uint32Array.from({ length: positionCount }, (_, i) => i) : this.doc.indices(raw.indices, `${where}.indices`);
|
|
4578
|
+
if (mode === PrimitiveMode.Triangles && stated.length % 3 !== 0) throw new LevelRefusal(LevelRefusalCode.BadAccessor, `${name}: ${where} is not three indices per triangle`);
|
|
4579
|
+
const primitive = { positions, indices: asTriangles(mode, stated) };
|
|
4580
|
+
this.primitives.set(key, primitive);
|
|
4581
|
+
return primitive;
|
|
4582
|
+
}
|
|
4583
|
+
};
|
|
4584
|
+
function requireExtensions(json) {
|
|
4585
|
+
const required = json.extensionsRequired;
|
|
4586
|
+
if (required === void 0) return;
|
|
4587
|
+
if (!Array.isArray(required)) throw new LevelRefusal(LevelRefusalCode.BadValue, "$.extensionsRequired is not a list");
|
|
4588
|
+
for (const name of required) {
|
|
4589
|
+
if (typeof name !== "string" || !READS_FINE.some((known) => name.startsWith(known))) throw new LevelRefusal(LevelRefusalCode.UnknownExtension, `the file requires ${String(name)}, which this reader cannot honour; re-export without it`);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
function checkAsset(json) {
|
|
4593
|
+
if (!isObject2(json.asset)) throw new LevelRefusal(LevelRefusalCode.BadVersion, "the file has no asset block, so it is no glTF 2");
|
|
4594
|
+
const version = json.asset.version;
|
|
4595
|
+
const major = typeof version === "string" ? Number.parseInt(version, 10) : Number.NaN;
|
|
4596
|
+
if (major !== GLTF_MAJOR) throw new LevelRefusal(LevelRefusalCode.BadVersion, `asset.version is ${String(version)}; this reads glTF 2`);
|
|
4597
|
+
const min = json.asset.minVersion;
|
|
4598
|
+
if (typeof min === "string" && Number.parseInt(min, 10) > GLTF_MAJOR) throw new LevelRefusal(LevelRefusalCode.BadVersion, `asset.minVersion is ${min}; this reads glTF 2`);
|
|
4599
|
+
}
|
|
4600
|
+
function unpackGlb(bytes, label) {
|
|
4601
|
+
if (bytes.length < GLB_HEADER_BYTES) throw new LevelRefusal(LevelRefusalCode.BadContainer, `${label}: a glb header is ${GLB_HEADER_BYTES} bytes`);
|
|
4602
|
+
const version = bytes.readUInt32LE(4);
|
|
4603
|
+
if (version !== GLTF_MAJOR) throw new LevelRefusal(LevelRefusalCode.BadVersion, `${label} is glb version ${version}; this reads version ${GLTF_MAJOR}`);
|
|
4604
|
+
const length = bytes.readUInt32LE(8);
|
|
4605
|
+
if (length > bytes.length || length < GLB_HEADER_BYTES + CHUNK_HEADER_BYTES) throw new LevelRefusal(LevelRefusalCode.BadContainer, `${label}: the header says a length of ${length} and the file has ${bytes.length}`);
|
|
4606
|
+
const chunk = (at) => {
|
|
4607
|
+
const size = bytes.readUInt32LE(at);
|
|
4608
|
+
const type = bytes.readUInt32LE(at + 4);
|
|
4609
|
+
const start = at + CHUNK_HEADER_BYTES;
|
|
4610
|
+
if (start + size > length) throw new LevelRefusal(LevelRefusalCode.BadContainer, `${label}: a chunk of ${size} bytes runs past the file`);
|
|
4611
|
+
return { type, body: bytes.subarray(start, start + size), next: start + size };
|
|
4612
|
+
};
|
|
4613
|
+
const first = chunk(GLB_HEADER_BYTES);
|
|
4614
|
+
if (first.type !== CHUNK_JSON) throw new LevelRefusal(LevelRefusalCode.BadContainer, `${label}: the first chunk is not JSON`);
|
|
4615
|
+
let bin = null;
|
|
4616
|
+
if (first.next + CHUNK_HEADER_BYTES <= length) {
|
|
4617
|
+
const second = chunk(first.next);
|
|
4618
|
+
if (second.type === CHUNK_BIN) bin = second.body;
|
|
4619
|
+
}
|
|
4620
|
+
return { json: parseJson(first.body, label), bin };
|
|
4621
|
+
}
|
|
4622
|
+
function parseJson(bytes, label) {
|
|
4623
|
+
try {
|
|
4624
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes));
|
|
4625
|
+
} catch {
|
|
4626
|
+
throw new LevelRefusal(LevelRefusalCode.NotGltf, `${label}: its JSON does not parse`);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
function readInput(path) {
|
|
4630
|
+
const label = basename(path);
|
|
4631
|
+
const refuse = (why) => new LevelRefusal(LevelRefusalCode.NotGltf, `${label} ${why}`);
|
|
4632
|
+
let real;
|
|
4633
|
+
try {
|
|
4634
|
+
real = realpathSync(path);
|
|
4635
|
+
} catch {
|
|
4636
|
+
throw refuse("is not there");
|
|
4637
|
+
}
|
|
4638
|
+
return { real, bytes: readPlainFile(real, refuse, () => new LevelRefusal(LevelRefusalCode.TooLarge, `${label} is past ${LEVEL_LIMITS.maxFileBytes} bytes`)) };
|
|
4639
|
+
}
|
|
4640
|
+
function readGltfLevel(path, options = {}) {
|
|
4641
|
+
const label = basename(path);
|
|
4642
|
+
const { bytes, real } = readInput(path);
|
|
4643
|
+
let raw;
|
|
4644
|
+
let bin = null;
|
|
4645
|
+
if (bytes.length >= 4 && bytes.readUInt32LE(0) === GLB_MAGIC) ({ json: raw, bin } = unpackGlb(bytes, label));
|
|
4646
|
+
else if (bytes.length > 0 && bytes.subarray(0, 16).toString("latin1").trimStart().startsWith("{")) raw = parseJson(bytes, label);
|
|
4647
|
+
else throw new LevelRefusal(LevelRefusalCode.NotGltf, `${label} is neither a glb nor a glTF`);
|
|
4648
|
+
if (!isObject2(raw)) throw new LevelRefusal(LevelRefusalCode.NotGltf, `${label}: its JSON is not a glTF object`);
|
|
4649
|
+
checkAsset(raw);
|
|
4650
|
+
requireExtensions(raw);
|
|
4651
|
+
const doc = new Document(raw, bin, dirname(real));
|
|
4652
|
+
if (!Array.isArray(raw.scenes) || raw.scenes.length === 0) throw new LevelRefusal(LevelRefusalCode.BadReference, `$.scene: ${label} holds no scene`);
|
|
4653
|
+
const sceneIndex = raw.scene ?? 0;
|
|
4654
|
+
const scene = doc.entry("scenes", sceneIndex, "$.scene");
|
|
4655
|
+
const sceneWhere = `$.scenes[${String(sceneIndex)}]`;
|
|
4656
|
+
if (scene.nodes !== void 0 && !Array.isArray(scene.nodes)) throw new LevelRefusal(LevelRefusalCode.BadReference, `${sceneWhere}.nodes is not a list`);
|
|
4657
|
+
const sceneGenex = isObject2(scene.extras) ? scene.extras.genex : void 0;
|
|
4658
|
+
const settings = levelSettingsOf(sceneGenex, `${sceneWhere}`);
|
|
4659
|
+
const unitsPerMeter = options.unitsPerMeter ?? settings.unitsPerMeter ?? DEFAULT_UNITS_PER_METER;
|
|
4660
|
+
const hull = options.hull ?? settings.hull;
|
|
4661
|
+
const walker = new Walker(doc);
|
|
4662
|
+
walker.walk(scene.nodes ?? [], sceneWhere, {});
|
|
4663
|
+
const level = buildLevelScene(
|
|
4664
|
+
{ meshes: walker.meshes, spawns: walker.spawns, notes: walker.notes },
|
|
4665
|
+
{ scale: unitsPerMeter, unitsPerMeter, ...hull === void 0 ? {} : { hull }, ...options.spawns === void 0 ? {} : { spawns: options.spawns }, source: { engine: "gltf", level: label.slice(0, 200), tool: "genex world gltf" } }
|
|
4666
|
+
);
|
|
4667
|
+
parseWorldScene(level.scene);
|
|
4668
|
+
return { ...level, sha256: createHash("sha256").update(level.json).digest("hex") };
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
// src/world-fit/gltf-command.ts
|
|
4672
|
+
import { lstatSync as lstatSync2, mkdtempSync, realpathSync as realpathSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
4673
|
+
import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
|
|
4674
|
+
import { fileURLToPath } from "url";
|
|
4675
|
+
var WorldGltfExit = { Ok: 0, Usage: 1, Refused: 3 };
|
|
4676
|
+
var USAGE = "usage: genex world gltf <level.glb|level.gltf> <scene.json>";
|
|
4677
|
+
function writeWhole(path, text2) {
|
|
4678
|
+
const dir = dirname2(path);
|
|
4679
|
+
const scratch = mkdtempSync(join2(dir, ".world-gltf-"));
|
|
4680
|
+
try {
|
|
4681
|
+
const temp = join2(scratch, basename2(path));
|
|
4682
|
+
writeFileSync(temp, text2);
|
|
4683
|
+
renameSync(temp, path);
|
|
4684
|
+
} finally {
|
|
4685
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
function sameFile(a, b) {
|
|
4689
|
+
try {
|
|
4690
|
+
return realpathSync2(a) === realpathSync2(b);
|
|
4691
|
+
} catch {
|
|
4692
|
+
return false;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
function runWorldGltf(args, io = { print: console.log, error: console.error }) {
|
|
4696
|
+
const [input, output] = args;
|
|
4697
|
+
if (args.length !== 2 || input === void 0 || output === void 0 || args.some((a) => a.startsWith("-"))) {
|
|
4698
|
+
io.error(USAGE);
|
|
4699
|
+
return WorldGltfExit.Usage;
|
|
4700
|
+
}
|
|
4701
|
+
const target = resolve(output);
|
|
4702
|
+
try {
|
|
4703
|
+
if (sameFile(input, target)) throw new LevelRefusal(LevelRefusalCode.NotGltf, "the input and the output are one file; name another output");
|
|
4704
|
+
const isLink = lstatIsLink(target);
|
|
4705
|
+
if (isLink) throw new LevelRefusal(LevelRefusalCode.UriRefused, `${output} is a link; name the file itself`);
|
|
4706
|
+
const level = readGltfLevel(input);
|
|
4707
|
+
writeWhole(target, level.json);
|
|
4708
|
+
io.print(`SCENE ${JSON.stringify({ path: output, sha256: level.sha256, ...level.stats, notes: level.notes })}`);
|
|
4709
|
+
return WorldGltfExit.Ok;
|
|
4710
|
+
} catch (error) {
|
|
4711
|
+
if (!(error instanceof LevelRefusal)) throw error;
|
|
4712
|
+
io.print(`REFUSED ${JSON.stringify({ code: error.code, message: error.message })}`);
|
|
4713
|
+
io.error(error.message);
|
|
4714
|
+
return WorldGltfExit.Refused;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
function lstatIsLink(path) {
|
|
4718
|
+
try {
|
|
4719
|
+
return lstatSync2(path).isSymbolicLink();
|
|
4720
|
+
} catch {
|
|
4721
|
+
return false;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
4725
|
+
process.exitCode = runWorldGltf(process.argv.slice(2));
|
|
4726
|
+
}
|
|
4727
|
+
export {
|
|
4728
|
+
BOUND_ACTOR,
|
|
4729
|
+
DEFAULT_UNITS_PER_METER,
|
|
4730
|
+
FLOOR_MAX_RADII,
|
|
4731
|
+
FLOOR_P99_RADII,
|
|
4732
|
+
FitStatus,
|
|
4733
|
+
HOSTILE_PAST_RADII,
|
|
4734
|
+
LEVEL_LIMITS,
|
|
4735
|
+
LevelCollide,
|
|
4736
|
+
LevelRefusal,
|
|
4737
|
+
LevelRefusalCode,
|
|
4738
|
+
LevelShape,
|
|
4739
|
+
SceneChannel,
|
|
4740
|
+
SceneSurfaceKind,
|
|
4741
|
+
SceneTag,
|
|
4742
|
+
ShotFitStatus,
|
|
4743
|
+
WORLD_SCENE_FORMAT,
|
|
4744
|
+
WORLD_SCENE_VERSION,
|
|
4745
|
+
WorldGltfExit,
|
|
4746
|
+
WorldLimits,
|
|
4747
|
+
WorldSceneError,
|
|
4748
|
+
claimArithmetic,
|
|
4749
|
+
claimedStop,
|
|
4750
|
+
fitRungs,
|
|
4751
|
+
fitScene,
|
|
4752
|
+
fitShotWorld,
|
|
4753
|
+
fitWorldHash,
|
|
4754
|
+
levelMarkOf,
|
|
4755
|
+
parseWorldScene,
|
|
4756
|
+
readGltfLevel,
|
|
4757
|
+
runWorldGltf,
|
|
4758
|
+
sceneHull,
|
|
4759
|
+
sceneReach,
|
|
4760
|
+
sceneShotRays,
|
|
4761
|
+
shotRay,
|
|
4762
|
+
surfaceAnswers
|
|
4763
|
+
};
|