@aethervtt/shard-structure 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,926 @@
1
+ import { polygon } from "@aethervtt/shard-core";
2
+ import { pointAt } from "./curve.js";
3
+ function wallLength(w) {
4
+ return w.line.length;
5
+ }
6
+ function piece(out, n, s0, s1, y0, y1, half, bottom, frame, source = -1) {
7
+ if (s1 - s0 <= 1e-9 || y1 - y0 <= 1e-9) return n;
8
+ let p = out[n];
9
+ if (!p) {
10
+ p = { s0, s1, y0, y1, half, capStart: true, capEnd: true, bottom, frame, source };
11
+ out[n] = p;
12
+ } else {
13
+ p.source = source;
14
+ p.s0 = s0;
15
+ p.s1 = s1;
16
+ p.y0 = y0;
17
+ p.y1 = y1;
18
+ p.half = half;
19
+ p.capStart = true;
20
+ p.capEnd = true;
21
+ p.bottom = bottom;
22
+ p.frame = frame;
23
+ }
24
+ return n + 1;
25
+ }
26
+ function wallPieces(w, openings, out) {
27
+ const length = wallLength(w);
28
+ if (length < 1e-6 || w.height <= 0) return 0;
29
+ const half = w.thickness / 2;
30
+ const base = w.elevation;
31
+ const top = base + w.height;
32
+ let n = 0;
33
+ let cursor = 0;
34
+ for (let i = 0; i < openings.length; i++) {
35
+ const o = openings[i];
36
+ const o0 = Math.max(cursor, Math.min(length, o.offset));
37
+ const o1 = Math.max(o0, Math.min(length, o.offset + o.width));
38
+ if (o1 - o0 <= 1e-9) continue;
39
+ n = piece(out, n, cursor, o0, base, top, half, false, false);
40
+ const sill = base + Math.max(0, o.sill);
41
+ const head = Math.min(top, sill + o.height);
42
+ const below = n;
43
+ n = piece(out, n, o0, o1, base, sill, half, false, false);
44
+ if (n > below) {
45
+ out[below].capStart = o0 === 0;
46
+ out[below].capEnd = o1 === length;
47
+ }
48
+ const above = n;
49
+ n = piece(out, n, o0, o1, head, top, half, true, false);
50
+ if (n > above) {
51
+ out[above].capStart = o0 === 0;
52
+ out[above].capEnd = o1 === length;
53
+ }
54
+ const fw = Math.min(o.frameWidth, (o1 - o0) / 2, head - sill);
55
+ if (fw > 1e-9) {
56
+ const fh = half + Math.max(0, o.frameDepth);
57
+ n = piece(out, n, o0, o0 + fw, sill, head, fh, false, true, i);
58
+ n = piece(out, n, o1 - fw, o1, sill, head, fh, false, true, i);
59
+ n = piece(out, n, o0 + fw, o1 - fw, head - fw, head, fh, true, true, i);
60
+ if (o.kind === "window")
61
+ n = piece(out, n, o0 + fw, o1 - fw, sill, sill + fw, fh, true, true, i);
62
+ }
63
+ cursor = o1;
64
+ }
65
+ n = piece(out, n, cursor, length, base, top, half, false, false);
66
+ return n;
67
+ }
68
+ function chunkKey(cx, cz) {
69
+ return (cx + 32768) * 65536 + (cz + 32768);
70
+ }
71
+ function chunkX(key) {
72
+ return Math.floor(key / 65536) - 32768;
73
+ }
74
+ function chunkZ(key) {
75
+ return key % 65536 - 32768;
76
+ }
77
+ const SIDE_POS = 0;
78
+ const CAP_END = 1;
79
+ const SIDE_NEG = 2;
80
+ const CAP_START = 3;
81
+ const CUT = 4;
82
+ class ClipScratch {
83
+ a = new Float64Array(24);
84
+ b = new Float64Array(24);
85
+ la = new Int8Array(12);
86
+ lb = new Int8Array(12);
87
+ /** Arc length at each point. */
88
+ sa = new Float64Array(12);
89
+ sb = new Float64Array(12);
90
+ tri = new Float64Array(6);
91
+ /** A centreline point and normal (pointAt). */
92
+ at = new Float64Array(4);
93
+ /** The quad boundaries of the piece being walked: arc lengths. */
94
+ bounds = new Float64Array(64);
95
+ }
96
+ function strip(line, p, s) {
97
+ let n = 0;
98
+ const push = (v) => {
99
+ if (n === s.bounds.length) {
100
+ const next = new Float64Array(n * 2);
101
+ next.set(s.bounds);
102
+ s.bounds = next;
103
+ }
104
+ s.bounds[n++] = v;
105
+ };
106
+ push(p.s0);
107
+ for (let i = 1; i < line.count - 1; i++) {
108
+ const v = line.s[i];
109
+ if (v > p.s0 + 1e-9 && v < p.s1 - 1e-9) push(v);
110
+ }
111
+ push(p.s1);
112
+ return n;
113
+ }
114
+ function quad(line, p, q, last, s) {
115
+ const sa = s.bounds[q];
116
+ const sb = s.bounds[q + 1];
117
+ const at = s.at;
118
+ pointAt(line, sa, at);
119
+ const ax = at[0];
120
+ const az = at[1];
121
+ const anx = at[2] * p.half;
122
+ const anz = at[3] * p.half;
123
+ pointAt(line, sb, at);
124
+ const bx = at[0];
125
+ const bz = at[1];
126
+ const bnx = at[2] * p.half;
127
+ const bnz = at[3] * p.half;
128
+ const o = s.a;
129
+ o[0] = ax + anx;
130
+ o[1] = az + anz;
131
+ o[2] = bx + bnx;
132
+ o[3] = bz + bnz;
133
+ o[4] = bx - bnx;
134
+ o[5] = bz - bnz;
135
+ o[6] = ax - anx;
136
+ o[7] = az - anz;
137
+ s.la[0] = SIDE_POS;
138
+ s.la[1] = q + 1 === last ? CAP_END : CUT;
139
+ s.la[2] = SIDE_NEG;
140
+ s.la[3] = q === 0 ? CAP_START : CUT;
141
+ s.sa[0] = sa;
142
+ s.sa[1] = sb;
143
+ s.sa[2] = sb;
144
+ s.sa[3] = sa;
145
+ }
146
+ function clipLabeled(src, srcLabels, srcS, n, dst, dstLabels, dstS, axis, at, sign) {
147
+ if (n === 0) return 0;
148
+ let count = 0;
149
+ let pi = n - 1;
150
+ let px = src[pi * 2];
151
+ let pz = src[pi * 2 + 1];
152
+ let ps = srcS[pi];
153
+ let pd = sign * ((axis === 0 ? px : pz) - at);
154
+ for (let i = 0; i < n; i++) {
155
+ const x = src[i * 2];
156
+ const z = src[i * 2 + 1];
157
+ const sv = srcS[i];
158
+ const d = sign * ((axis === 0 ? x : z) - at);
159
+ if (d >= 0) {
160
+ if (pd < 0) {
161
+ const t = pd / (pd - d);
162
+ dst[count * 2] = px + (x - px) * t;
163
+ dst[count * 2 + 1] = pz + (z - pz) * t;
164
+ dstS[count] = ps + (sv - ps) * t;
165
+ dstLabels[count] = srcLabels[pi];
166
+ count++;
167
+ }
168
+ dst[count * 2] = x;
169
+ dst[count * 2 + 1] = z;
170
+ dstS[count] = sv;
171
+ dstLabels[count] = srcLabels[i];
172
+ count++;
173
+ } else if (pd >= 0) {
174
+ const t = pd / (pd - d);
175
+ dst[count * 2] = px + (x - px) * t;
176
+ dst[count * 2 + 1] = pz + (z - pz) * t;
177
+ dstS[count] = ps + (sv - ps) * t;
178
+ dstLabels[count] = CUT;
179
+ count++;
180
+ }
181
+ pi = i;
182
+ px = x;
183
+ pz = z;
184
+ ps = sv;
185
+ pd = d;
186
+ }
187
+ return count;
188
+ }
189
+ function clipQuad(minX, minZ, maxX, maxZ, s) {
190
+ let n = clipLabeled(s.a, s.la, s.sa, 4, s.b, s.lb, s.sb, 0, minX, 1);
191
+ n = clipLabeled(s.b, s.lb, s.sb, n, s.a, s.la, s.sa, 0, maxX, -1);
192
+ n = clipLabeled(s.a, s.la, s.sa, n, s.b, s.lb, s.sb, 1, minZ, 1);
193
+ n = clipLabeled(s.b, s.lb, s.sb, n, s.a, s.la, s.sa, 1, maxZ, -1);
194
+ if (n < 3 || Math.abs(polygon.signedArea(s.a, 0, n)) <= 1e-9) return 0;
195
+ return n;
196
+ }
197
+ function wallChunks(w, pieces, count, size, out, s) {
198
+ const line = w.line;
199
+ for (let i = 0; i < count; i++) {
200
+ const p = pieces[i];
201
+ const bounds = strip(line, p, s);
202
+ for (let q = 0; q + 1 < bounds; q++) {
203
+ quad(line, p, q, bounds - 1, s);
204
+ let x0 = Infinity;
205
+ let z0 = Infinity;
206
+ let x1 = -Infinity;
207
+ let z1 = -Infinity;
208
+ for (let k = 0; k < 4; k++) {
209
+ const x = s.a[k * 2];
210
+ const z = s.a[k * 2 + 1];
211
+ if (x < x0) x0 = x;
212
+ if (x > x1) x1 = x;
213
+ if (z < z0) z0 = z;
214
+ if (z > z1) z1 = z;
215
+ }
216
+ for (let cx = Math.floor(x0 / size); cx <= Math.floor(x1 / size); cx++) {
217
+ for (let cz = Math.floor(z0 / size); cz <= Math.floor(z1 / size); cz++) {
218
+ const key = chunkKey(cx, cz);
219
+ if (out.has(key)) continue;
220
+ quad(line, p, q, bounds - 1, s);
221
+ if (clipQuad(cx * size, cz * size, (cx + 1) * size, (cz + 1) * size, s) > 0) out.add(key);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ class MeshBuilder {
228
+ positions = new Float32Array(768);
229
+ normals = new Float32Array(768);
230
+ uvs = new Float32Array(512);
231
+ tangents = new Float32Array(1024);
232
+ indices = new Uint32Array(1024);
233
+ vertexCount = 0;
234
+ indexCount = 0;
235
+ /** Local-space bounds of what was added. */
236
+ minX = Infinity;
237
+ minY = Infinity;
238
+ minZ = Infinity;
239
+ maxX = -Infinity;
240
+ maxY = -Infinity;
241
+ maxZ = -Infinity;
242
+ reset() {
243
+ this.vertexCount = 0;
244
+ this.indexCount = 0;
245
+ this.minX = this.minY = this.minZ = Infinity;
246
+ this.maxX = this.maxY = this.maxZ = -Infinity;
247
+ }
248
+ /** A vertex: position, normal, uv, and the tangent along u with its bitangent sign. */
249
+ vertex(x, y, z, nx, ny, nz, u, v, tx, ty, tz, tw) {
250
+ const i = this.vertexCount;
251
+ if ((i + 1) * 3 > this.positions.length) this.growVertices();
252
+ const p = i * 3;
253
+ this.positions[p] = x;
254
+ this.positions[p + 1] = y;
255
+ this.positions[p + 2] = z;
256
+ this.normals[p] = nx;
257
+ this.normals[p + 1] = ny;
258
+ this.normals[p + 2] = nz;
259
+ this.uvs[i * 2] = u;
260
+ this.uvs[i * 2 + 1] = v;
261
+ const t = i * 4;
262
+ this.tangents[t] = tx;
263
+ this.tangents[t + 1] = ty;
264
+ this.tangents[t + 2] = tz;
265
+ this.tangents[t + 3] = tw;
266
+ if (x < this.minX) this.minX = x;
267
+ if (y < this.minY) this.minY = y;
268
+ if (z < this.minZ) this.minZ = z;
269
+ if (x > this.maxX) this.maxX = x;
270
+ if (y > this.maxY) this.maxY = y;
271
+ if (z > this.maxZ) this.maxZ = z;
272
+ this.vertexCount = i + 1;
273
+ return i;
274
+ }
275
+ triangle(a, b, c) {
276
+ if (this.indexCount + 3 > this.indices.length) {
277
+ const next = new Uint32Array(this.indices.length * 2);
278
+ next.set(this.indices);
279
+ this.indices = next;
280
+ }
281
+ this.indices[this.indexCount++] = a;
282
+ this.indices[this.indexCount++] = b;
283
+ this.indices[this.indexCount++] = c;
284
+ }
285
+ growVertices() {
286
+ const grow = (a) => {
287
+ const next = new Float32Array(a.length * 2);
288
+ next.set(a);
289
+ return next;
290
+ };
291
+ this.positions = grow(this.positions);
292
+ this.normals = grow(this.normals);
293
+ this.uvs = grow(this.uvs);
294
+ this.tangents = grow(this.tangents);
295
+ }
296
+ /**
297
+ * A convex planar polygon as a fan, wound so its front faces along (nx, ny, nz). `first` is the
298
+ * index of its first vertex; the rest follow.
299
+ */
300
+ fan(first, count, nx, ny, nz) {
301
+ if (count < 3) return;
302
+ const p = this.positions;
303
+ const a = first * 3;
304
+ let cx = 0;
305
+ let cy = 0;
306
+ let cz = 0;
307
+ for (let k = 1; k + 1 < count; k++) {
308
+ const b = (first + k) * 3;
309
+ const c = (first + k + 1) * 3;
310
+ const ux = p[b] - p[a];
311
+ const uy = p[b + 1] - p[a + 1];
312
+ const uz = p[b + 2] - p[a + 2];
313
+ const vx = p[c] - p[a];
314
+ const vy = p[c + 1] - p[a + 1];
315
+ const vz = p[c + 2] - p[a + 2];
316
+ cx += uy * vz - uz * vy;
317
+ cy += uz * vx - ux * vz;
318
+ cz += ux * vy - uy * vx;
319
+ }
320
+ const flip = cx * nx + cy * ny + cz * nz < 0;
321
+ for (let k = 1; k + 1 < count; k++) {
322
+ if (flip) this.triangle(first, first + k + 1, first + k);
323
+ else this.triangle(first, first + k, first + k + 1);
324
+ }
325
+ }
326
+ }
327
+ function emitWall(w, pieces, count, minX, minZ, maxX, maxZ, builder, s) {
328
+ const line = w.line;
329
+ if (line.length < 1e-6) return false;
330
+ const d0x = line.nz[0];
331
+ const d0z = -line.nx[0];
332
+ const u0 = w.ax * d0x + w.az * d0z;
333
+ const at = s.at;
334
+ let any = false;
335
+ for (let i = 0; i < count; i++) {
336
+ const p = pieces[i];
337
+ const m = builder(i);
338
+ const bounds = strip(line, p, s);
339
+ for (let q = 0; q + 1 < bounds; q++) {
340
+ quad(line, p, q, bounds - 1, s);
341
+ const n = clipQuad(minX, minZ, maxX, maxZ, s);
342
+ if (n === 0) continue;
343
+ any = true;
344
+ const pts = s.a;
345
+ const labels = s.la;
346
+ const arc = s.sa;
347
+ let first = m.vertexCount;
348
+ for (let k = 0; k < n; k++)
349
+ m.vertex(
350
+ pts[k * 2],
351
+ p.y1,
352
+ pts[k * 2 + 1],
353
+ 0,
354
+ 1,
355
+ 0,
356
+ pts[k * 2],
357
+ pts[k * 2 + 1],
358
+ 1,
359
+ 0,
360
+ 0,
361
+ 1
362
+ );
363
+ m.fan(first, n, 0, 1, 0);
364
+ if (p.bottom) {
365
+ first = m.vertexCount;
366
+ for (let k = 0; k < n; k++)
367
+ m.vertex(
368
+ pts[k * 2],
369
+ p.y0,
370
+ pts[k * 2 + 1],
371
+ 0,
372
+ -1,
373
+ 0,
374
+ pts[k * 2],
375
+ pts[k * 2 + 1],
376
+ 1,
377
+ 0,
378
+ 0,
379
+ -1
380
+ );
381
+ m.fan(first, n, 0, -1, 0);
382
+ }
383
+ for (let k = 0; k < n; k++) {
384
+ const label = labels[k];
385
+ if (label === CUT) continue;
386
+ if (label === CAP_START && !p.capStart) continue;
387
+ if (label === CAP_END && !p.capEnd) continue;
388
+ const k2 = (k + 1) % n;
389
+ const x0 = pts[k * 2];
390
+ const z0 = pts[k * 2 + 1];
391
+ const x1 = pts[k2 * 2];
392
+ const z1 = pts[k2 * 2 + 1];
393
+ first = m.vertexCount;
394
+ if (label === SIDE_POS || label === SIDE_NEG) {
395
+ const sgn = label === SIDE_POS ? 1 : -1;
396
+ for (let e = 0; e < 4; e++) {
397
+ const end = e === 0 || e === 3 ? k : k2;
398
+ const x = end === k ? x0 : x1;
399
+ const z = end === k ? z0 : z1;
400
+ pointAt(line, arc[end], at);
401
+ const fx = at[2] * sgn;
402
+ const fz = at[3] * sgn;
403
+ const u = sgn * (arc[end] + u0);
404
+ const y = e < 2 ? p.y0 : p.y1;
405
+ m.vertex(x, y, z, fx, 0, fz, u, -y, fz, 0, -fx, 1);
406
+ }
407
+ pointAt(line, (arc[k] + arc[k2]) / 2, at);
408
+ m.fan(first, 4, at[2] * sgn, 0, at[3] * sgn);
409
+ } else {
410
+ pointAt(line, label === CAP_END ? p.s1 : p.s0, at);
411
+ const sgn = label === CAP_END ? 1 : -1;
412
+ const fx = at[3] * sgn;
413
+ const fz = -at[2] * sgn;
414
+ const ux0 = x0 * fz - z0 * fx;
415
+ const ux1 = x1 * fz - z1 * fx;
416
+ m.vertex(x0, p.y0, z0, fx, 0, fz, ux0, -p.y0, fz, 0, -fx, 1);
417
+ m.vertex(x1, p.y0, z1, fx, 0, fz, ux1, -p.y0, fz, 0, -fx, 1);
418
+ m.vertex(x1, p.y1, z1, fx, 0, fz, ux1, -p.y1, fz, 0, -fx, 1);
419
+ m.vertex(x0, p.y1, z0, fx, 0, fz, ux0, -p.y1, fz, 0, -fx, 1);
420
+ m.fan(first, 4, fx, 0, fz);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ return any;
426
+ }
427
+ function slabY(f, x, z) {
428
+ return f.elevation + f.slope * (x * f.rx + z * f.rz - f.d0);
429
+ }
430
+ function floorShape(points, elevation, options = {}) {
431
+ const holes = options.holes ?? [];
432
+ let count = points.length;
433
+ for (const h of holes) count += h.length;
434
+ const flat = new Float64Array(count * 2);
435
+ const rings = new Uint32Array(holes.length + 2);
436
+ let n = 0;
437
+ const add = (ring) => {
438
+ for (let i = 0; i < ring.length; i++) {
439
+ flat[n * 2] = ring[i][0];
440
+ flat[n * 2 + 1] = ring[i][1];
441
+ n++;
442
+ }
443
+ };
444
+ add(points);
445
+ for (let h = 0; h < holes.length; h++) {
446
+ rings[h + 1] = n;
447
+ add(holes[h]);
448
+ }
449
+ rings[holes.length + 1] = n;
450
+ const sides = new Int8Array(holes.length + 1);
451
+ for (let r = 0; r <= holes.length; r++) {
452
+ const area = polygon.signedArea(flat, rings[r], rings[r + 1]);
453
+ sides[r] = area > 0 === (r === 0) ? -1 : 1;
454
+ }
455
+ const triangles = Uint32Array.from(polygon.triangulate(flat, rings.subarray(1, holes.length + 1)));
456
+ const pitch = (options.pitch ?? 0) * Math.PI / 180;
457
+ let rx = options.ridge?.[0] ?? 0;
458
+ let rz = options.ridge?.[1] ?? 1;
459
+ const rl = Math.sqrt(rx * rx + rz * rz);
460
+ if (rl < 1e-9) {
461
+ rx = 0;
462
+ rz = 1;
463
+ } else {
464
+ rx /= rl;
465
+ rz /= rl;
466
+ }
467
+ let d0 = Infinity;
468
+ let minX = Infinity;
469
+ let minZ = Infinity;
470
+ let maxX = -Infinity;
471
+ let maxZ = -Infinity;
472
+ for (let i = 0; i < points.length; i++) {
473
+ const x = flat[i * 2];
474
+ const z = flat[i * 2 + 1];
475
+ d0 = Math.min(d0, x * rx + z * rz);
476
+ if (x < minX) minX = x;
477
+ if (x > maxX) maxX = x;
478
+ if (z < minZ) minZ = z;
479
+ if (z > maxZ) maxZ = z;
480
+ }
481
+ const f = {
482
+ points: flat,
483
+ rings,
484
+ sides,
485
+ triangles,
486
+ elevation,
487
+ slope: pitch > 1e-9 ? Math.tan(pitch) : 0,
488
+ rx,
489
+ rz,
490
+ d0: Number.isFinite(d0) ? d0 : 0,
491
+ thickness: Math.max(0, options.thickness ?? 0),
492
+ frames: (options.frames ?? []).filter(
493
+ (fr) => fr.width > 1e-9 && fr.ring > 0 && fr.ring <= holes.length
494
+ ),
495
+ frameOuter: new Float64Array(flat.length),
496
+ minX,
497
+ minZ,
498
+ maxX,
499
+ maxZ
500
+ };
501
+ for (const frame of f.frames) {
502
+ offsetRing(f, frame.ring, frame.width, f.frameOuter);
503
+ for (let i = f.rings[frame.ring]; i < f.rings[frame.ring + 1]; i++) {
504
+ const x = f.frameOuter[i * 2];
505
+ const z = f.frameOuter[i * 2 + 1];
506
+ if (x < f.minX) f.minX = x;
507
+ if (x > f.maxX) f.maxX = x;
508
+ if (z < f.minZ) f.minZ = z;
509
+ if (z > f.maxZ) f.maxZ = z;
510
+ }
511
+ }
512
+ return f;
513
+ }
514
+ function ringShape(f, ring, thickness, drop = 0) {
515
+ const start = f.rings[ring];
516
+ const end = f.rings[ring + 1];
517
+ const points = f.points.slice(start * 2, end * 2);
518
+ const count = end - start;
519
+ let minX = Infinity;
520
+ let minZ = Infinity;
521
+ let maxX = -Infinity;
522
+ let maxZ = -Infinity;
523
+ for (let i = 0; i < count; i++) {
524
+ const x = points[i * 2];
525
+ const z = points[i * 2 + 1];
526
+ if (x < minX) minX = x;
527
+ if (x > maxX) maxX = x;
528
+ if (z < minZ) minZ = z;
529
+ if (z > maxZ) maxZ = z;
530
+ }
531
+ return {
532
+ points,
533
+ rings: Uint32Array.of(0, count),
534
+ sides: Int8Array.of(polygon.signedArea(points, 0, count) > 0 ? -1 : 1),
535
+ triangles: Uint32Array.from(polygon.triangulate(points)),
536
+ elevation: f.elevation - drop,
537
+ slope: f.slope,
538
+ rx: f.rx,
539
+ rz: f.rz,
540
+ d0: f.d0,
541
+ thickness,
542
+ frames: [],
543
+ frameOuter: new Float64Array(0),
544
+ minX,
545
+ minZ,
546
+ maxX,
547
+ maxZ
548
+ };
549
+ }
550
+ function edgeNormal(f, ring, i, j, out) {
551
+ const p = f.points;
552
+ const dx = p[j * 2] - p[i * 2];
553
+ const dz = p[j * 2 + 1] - p[i * 2 + 1];
554
+ const l = Math.sqrt(dx * dx + dz * dz) || 1;
555
+ const sgn = f.sides[ring];
556
+ out[0] = -dz / l * sgn;
557
+ out[1] = dx / l * sgn;
558
+ }
559
+ function offsetRing(f, ring, width, out) {
560
+ const start = f.rings[ring];
561
+ const end = f.rings[ring + 1];
562
+ const count = end - start;
563
+ const a = new Float64Array(2);
564
+ const b = new Float64Array(2);
565
+ for (let k = 0; k < count; k++) {
566
+ const i = start + k;
567
+ const prev = start + (k + count - 1) % count;
568
+ const next = start + (k + 1) % count;
569
+ edgeNormal(f, ring, prev, i, a);
570
+ edgeNormal(f, ring, i, next, b);
571
+ const d = Math.max(0.25, 1 + a[0] * b[0] + a[1] * b[1]);
572
+ out[i * 2] = f.points[i * 2] - (a[0] + b[0]) / d * width;
573
+ out[i * 2 + 1] = f.points[i * 2 + 1] - (a[1] + b[1]) / d * width;
574
+ }
575
+ }
576
+ function clipTri(ax, az, bx, bz, cx, cz, minX, minZ, maxX, maxZ, s) {
577
+ const tri = s.tri;
578
+ tri[0] = ax;
579
+ tri[1] = az;
580
+ tri[2] = bx;
581
+ tri[3] = bz;
582
+ tri[4] = cx;
583
+ tri[5] = cz;
584
+ const n = polygon.clipToRect(tri, 3, minX, minZ, maxX, maxZ, s.a, s.b);
585
+ if (n < 3 || Math.abs(polygon.signedArea(s.a, 0, n)) <= 1e-9) return 0;
586
+ return n;
587
+ }
588
+ function clipSegment(ax, az, bx, bz, minX, minZ, maxX, maxZ, out) {
589
+ const dx = bx - ax;
590
+ const dz = bz - az;
591
+ let t0 = 0;
592
+ let t1 = 1;
593
+ for (let k = 0; k < 4; k++) {
594
+ const p = k === 0 ? -dx : k === 1 ? dx : k === 2 ? -dz : dz;
595
+ const q = k === 0 ? ax - minX : k === 1 ? maxX - ax : k === 2 ? az - minZ : maxZ - az;
596
+ if (Math.abs(p) < 1e-12) {
597
+ if (q < 0) return false;
598
+ continue;
599
+ }
600
+ const t = q / p;
601
+ if (p < 0) {
602
+ if (t > t1) return false;
603
+ if (t > t0) t0 = t;
604
+ } else {
605
+ if (t < t0) return false;
606
+ if (t < t1) t1 = t;
607
+ }
608
+ }
609
+ if ((t1 - t0) * Math.sqrt(dx * dx + dz * dz) <= 1e-9) return false;
610
+ out[0] = ax + dx * t0;
611
+ out[1] = az + dz * t0;
612
+ out[2] = ax + dx * t1;
613
+ out[3] = az + dz * t1;
614
+ return true;
615
+ }
616
+ function addTriangleChunks(ax, az, bx, bz, cx, cz, size, out, s) {
617
+ const x0 = Math.floor(Math.min(ax, bx, cx) / size);
618
+ const x1 = Math.floor(Math.max(ax, bx, cx) / size);
619
+ const z0 = Math.floor(Math.min(az, bz, cz) / size);
620
+ const z1 = Math.floor(Math.max(az, bz, cz) / size);
621
+ for (let x = x0; x <= x1; x++)
622
+ for (let z = z0; z <= z1; z++) {
623
+ const key = chunkKey(x, z);
624
+ if (out.has(key)) continue;
625
+ const n = clipTri(
626
+ ax,
627
+ az,
628
+ bx,
629
+ bz,
630
+ cx,
631
+ cz,
632
+ x * size,
633
+ z * size,
634
+ (x + 1) * size,
635
+ (z + 1) * size,
636
+ s
637
+ );
638
+ if (n > 0) out.add(key);
639
+ }
640
+ }
641
+ function floorChunks(f, size, out, s) {
642
+ const p = f.points;
643
+ const t = f.triangles;
644
+ for (let k = 0; k < t.length; k += 3) {
645
+ const a = t[k] * 2;
646
+ const b = t[k + 1] * 2;
647
+ const c = t[k + 2] * 2;
648
+ addTriangleChunks(p[a], p[a + 1], p[b], p[b + 1], p[c], p[c + 1], size, out, s);
649
+ }
650
+ const o = f.frameOuter;
651
+ for (const frame of f.frames) {
652
+ const start = f.rings[frame.ring];
653
+ const end = f.rings[frame.ring + 1];
654
+ for (let i = start; i < end; i++) {
655
+ const pi = i * 2;
656
+ const pj = (i + 1 < end ? i + 1 : start) * 2;
657
+ addTriangleChunks(p[pi], p[pi + 1], p[pj], p[pj + 1], o[pj], o[pj + 1], size, out, s);
658
+ addTriangleChunks(p[pi], p[pi + 1], o[pj], o[pj + 1], o[pi], o[pi + 1], size, out, s);
659
+ }
660
+ }
661
+ }
662
+ function addSegmentChunks(ax, az, bx, bz, size, out) {
663
+ for (let x = Math.floor(Math.min(ax, bx) / size); x <= Math.floor(Math.max(ax, bx) / size); x++)
664
+ for (let z = Math.floor(Math.min(az, bz) / size); z <= Math.floor(Math.max(az, bz) / size); z++)
665
+ if (clipSegment(ax, az, bx, bz, x * size, z * size, (x + 1) * size, (z + 1) * size, seg))
666
+ out.add(chunkKey(x, z));
667
+ }
668
+ function ringChunks(f, ring, size, out, s) {
669
+ const start = f.rings[ring];
670
+ const end = f.rings[ring + 1];
671
+ const p = f.points;
672
+ const tris = polygon.triangulate(p.subarray(start * 2, end * 2));
673
+ for (let k = 0; k < tris.length; k += 3) {
674
+ const a = (start + tris[k]) * 2;
675
+ const b = (start + tris[k + 1]) * 2;
676
+ const c = (start + tris[k + 2]) * 2;
677
+ addTriangleChunks(p[a], p[a + 1], p[b], p[b + 1], p[c], p[c + 1], size, out, s);
678
+ }
679
+ const frame = f.frames.some((fr) => fr.ring === ring);
680
+ const o = f.frameOuter;
681
+ for (let i = start; i < end; i++) {
682
+ const pi = i * 2;
683
+ const pj = (i + 1 < end ? i + 1 : start) * 2;
684
+ addSegmentChunks(p[pi], p[pi + 1], p[pj], p[pj + 1], size, out);
685
+ if (!frame) continue;
686
+ addTriangleChunks(p[pi], p[pi + 1], p[pj], p[pj + 1], o[pj], o[pj + 1], size, out, s);
687
+ addTriangleChunks(p[pi], p[pi + 1], o[pj], o[pj + 1], o[pi], o[pi + 1], size, out, s);
688
+ addSegmentChunks(o[pi], o[pi + 1], o[pj], o[pj + 1], size, out);
689
+ }
690
+ }
691
+ function surfaceBasis(f, out) {
692
+ const len = Math.sqrt(1 + f.slope * f.slope);
693
+ const nx = -f.slope * f.rx / len;
694
+ const ny = 1 / len;
695
+ const nz = -f.slope * f.rz / len;
696
+ const tx = f.rz;
697
+ const tz = -f.rx;
698
+ const cx = ny * tz;
699
+ const cy = nz * tx - nx * tz;
700
+ const cz = -ny * tx;
701
+ out[0] = nx;
702
+ out[1] = ny;
703
+ out[2] = nz;
704
+ out[3] = tx;
705
+ out[4] = tz;
706
+ out[5] = -(cx * f.rx + cy * f.slope + cz * f.rz) >= 0 ? 1 : -1;
707
+ out[6] = len;
708
+ }
709
+ const basis = new Float64Array(7);
710
+ const seg = new Float64Array(4);
711
+ const edge = new Float64Array(2);
712
+ function sideFace(f, m, ax, az, bx, bz, lo, hi, fx, fz) {
713
+ const ya = slabY(f, ax, az);
714
+ const yb = slabY(f, bx, bz);
715
+ const ua = ax * fz - az * fx;
716
+ const ub = bx * fz - bz * fx;
717
+ const first = m.vertexCount;
718
+ m.vertex(ax, ya + lo, az, fx, 0, fz, ua, -(ya + lo), fz, 0, -fx, 1);
719
+ m.vertex(bx, yb + lo, bz, fx, 0, fz, ub, -(yb + lo), fz, 0, -fx, 1);
720
+ m.vertex(bx, yb + hi, bz, fx, 0, fz, ub, -(yb + hi), fz, 0, -fx, 1);
721
+ m.vertex(ax, ya + hi, az, fx, 0, fz, ua, -(ya + hi), fz, 0, -fx, 1);
722
+ m.fan(first, 4, fx, 0, fz);
723
+ }
724
+ function surfaceFace(f, m, n, lift, up, s) {
725
+ const b = basis;
726
+ const sgn = up ? 1 : -1;
727
+ const first = m.vertexCount;
728
+ for (let k = 0; k < n; k++) {
729
+ const x = s.a[k * 2];
730
+ const z = s.a[k * 2 + 1];
731
+ m.vertex(
732
+ x,
733
+ slabY(f, x, z) + lift,
734
+ z,
735
+ b[0] * sgn,
736
+ b[1] * sgn,
737
+ b[2] * sgn,
738
+ x * b[3] + z * b[4],
739
+ (x * f.rx + z * f.rz) * b[6],
740
+ b[3],
741
+ 0,
742
+ b[4],
743
+ b[5] * sgn
744
+ );
745
+ }
746
+ m.fan(first, n, b[0] * sgn, b[1] * sgn, b[2] * sgn);
747
+ }
748
+ function emitFloor(f, minX, minZ, maxX, maxZ, m, s, frame) {
749
+ if (f.maxX <= minX || f.minX >= maxX || f.maxZ <= minZ || f.minZ >= maxZ) return false;
750
+ surfaceBasis(f, basis);
751
+ let any = false;
752
+ const p = f.points;
753
+ const t = f.triangles;
754
+ const thick = f.thickness;
755
+ for (let k = 0; k < t.length; k += 3) {
756
+ const a = t[k] * 2;
757
+ const b = t[k + 1] * 2;
758
+ const c = t[k + 2] * 2;
759
+ const n = clipTri(
760
+ p[a],
761
+ p[a + 1],
762
+ p[b],
763
+ p[b + 1],
764
+ p[c],
765
+ p[c + 1],
766
+ minX,
767
+ minZ,
768
+ maxX,
769
+ maxZ,
770
+ s
771
+ );
772
+ if (n === 0) continue;
773
+ any = true;
774
+ surfaceFace(f, m, n, 0, true, s);
775
+ if (thick > 0) surfaceFace(f, m, n, -thick, false, s);
776
+ }
777
+ if (thick > 0) {
778
+ for (let r = 0; r + 1 < f.rings.length; r++) {
779
+ const start = f.rings[r];
780
+ const end = f.rings[r + 1];
781
+ for (let i = start; i < end; i++) {
782
+ const j = i + 1 < end ? i + 1 : start;
783
+ const ax = p[i * 2];
784
+ const az = p[i * 2 + 1];
785
+ if (!clipSegment(ax, az, p[j * 2], p[j * 2 + 1], minX, minZ, maxX, maxZ, seg)) continue;
786
+ any = true;
787
+ edgeNormal(f, r, i, j, edge);
788
+ sideFace(f, m, seg[0], seg[1], seg[2], seg[3], -thick, 0, edge[0], edge[1]);
789
+ }
790
+ }
791
+ }
792
+ if (!frame) return any;
793
+ const o = f.frameOuter;
794
+ for (let fi = 0; fi < f.frames.length; fi++) {
795
+ const fr = f.frames[fi];
796
+ const fm = frame(fi);
797
+ const start = f.rings[fr.ring];
798
+ const end = f.rings[fr.ring + 1];
799
+ for (let i = start; i < end; i++) {
800
+ const j = i + 1 < end ? i + 1 : start;
801
+ const pi = i * 2;
802
+ const pj = j * 2;
803
+ for (let half = 0; half < 2; half++) {
804
+ const n = half === 0 ? clipTri(
805
+ p[pi],
806
+ p[pi + 1],
807
+ p[pj],
808
+ p[pj + 1],
809
+ o[pj],
810
+ o[pj + 1],
811
+ minX,
812
+ minZ,
813
+ maxX,
814
+ maxZ,
815
+ s
816
+ ) : clipTri(
817
+ p[pi],
818
+ p[pi + 1],
819
+ o[pj],
820
+ o[pj + 1],
821
+ o[pi],
822
+ o[pi + 1],
823
+ minX,
824
+ minZ,
825
+ maxX,
826
+ maxZ,
827
+ s
828
+ );
829
+ if (n === 0) continue;
830
+ any = true;
831
+ surfaceFace(f, fm, n, fr.depth, true, s);
832
+ }
833
+ if (fr.depth <= 1e-9) continue;
834
+ edgeNormal(f, fr.ring, i, j, edge);
835
+ const ix = edge[0];
836
+ const iz = edge[1];
837
+ if (clipSegment(p[pi], p[pi + 1], p[pj], p[pj + 1], minX, minZ, maxX, maxZ, seg))
838
+ sideFace(f, fm, seg[0], seg[1], seg[2], seg[3], 0, fr.depth, ix, iz);
839
+ if (clipSegment(o[pj], o[pj + 1], o[pi], o[pi + 1], minX, minZ, maxX, maxZ, seg))
840
+ sideFace(f, fm, seg[0], seg[1], seg[2], seg[3], 0, fr.depth, -ix, -iz);
841
+ }
842
+ }
843
+ return any;
844
+ }
845
+ function insideRing(points, start, end, x, z, edge2) {
846
+ let inside = false;
847
+ for (let i = start, j = end - 1; i < end; j = i++) {
848
+ const xi = points[i * 2];
849
+ const zi = points[i * 2 + 1];
850
+ const xj = points[j * 2];
851
+ const zj = points[j * 2 + 1];
852
+ const dx = xj - xi;
853
+ const dz = zj - zi;
854
+ const l2 = dx * dx + dz * dz;
855
+ const cross = (x - xi) * dz - (z - zi) * dx;
856
+ const dot = (x - xi) * dx + (z - zi) * dz;
857
+ if (Math.abs(cross) <= 1e-9 * Math.sqrt(l2) && dot >= -1e-12 && dot <= l2 + 1e-12) return edge2;
858
+ if (zi > z !== zj > z && x < dx * (z - zi) / dz + xi) inside = !inside;
859
+ }
860
+ return inside;
861
+ }
862
+ function orient(ax, az, bx, bz, cx, cz) {
863
+ const v = (bx - ax) * (cz - az) - (bz - az) * (cx - ax);
864
+ return Math.abs(v) <= 1e-12 ? 0 : v > 0 ? 1 : -1;
865
+ }
866
+ function within(ax, az, bx, bz, x, z) {
867
+ return Math.min(ax, bx) - 1e-12 <= x && x <= Math.max(ax, bx) + 1e-12 && Math.min(az, bz) - 1e-12 <= z && z <= Math.max(az, bz) + 1e-12;
868
+ }
869
+ function segmentsMeet(ax, az, bx, bz, cx, cz, dx, dz) {
870
+ const o1 = orient(ax, az, bx, bz, cx, cz);
871
+ const o2 = orient(ax, az, bx, bz, dx, dz);
872
+ const o3 = orient(cx, cz, dx, dz, ax, az);
873
+ const o4 = orient(cx, cz, dx, dz, bx, bz);
874
+ if (o1 !== o2 && o3 !== o4) return true;
875
+ return o1 === 0 && within(ax, az, bx, bz, cx, cz) || o2 === 0 && within(ax, az, bx, bz, dx, dz) || o3 === 0 && within(cx, cz, dx, dz, ax, az) || o4 === 0 && within(cx, cz, dx, dz, bx, bz);
876
+ }
877
+ function ringsTouch(a, b) {
878
+ const na = a.length / 2;
879
+ const nb = b.length / 2;
880
+ for (let i = 0; i < na; i++) {
881
+ const i2 = (i + 1) % na * 2;
882
+ for (let j = 0; j < nb; j++) {
883
+ const j2 = (j + 1) % nb * 2;
884
+ const meet = segmentsMeet(
885
+ a[i * 2],
886
+ a[i * 2 + 1],
887
+ a[i2],
888
+ a[i2 + 1],
889
+ b[j * 2],
890
+ b[j * 2 + 1],
891
+ b[j2],
892
+ b[j2 + 1]
893
+ );
894
+ if (meet) return true;
895
+ }
896
+ }
897
+ return false;
898
+ }
899
+ function ringInside(inner, outer) {
900
+ if (ringsTouch(inner, outer)) return false;
901
+ return insideRing(outer, 0, outer.length / 2, inner[0], inner[1], false);
902
+ }
903
+ function ringsOverlap(a, b) {
904
+ if (ringsTouch(a, b)) return true;
905
+ return insideRing(b, 0, b.length / 2, a[0], a[1], true) || insideRing(a, 0, a.length / 2, b[0], b[1], true);
906
+ }
907
+ export {
908
+ ClipScratch,
909
+ MeshBuilder,
910
+ chunkKey,
911
+ chunkX,
912
+ chunkZ,
913
+ emitFloor,
914
+ emitWall,
915
+ floorChunks,
916
+ floorShape,
917
+ insideRing,
918
+ ringChunks,
919
+ ringInside,
920
+ ringShape,
921
+ ringsOverlap,
922
+ slabY,
923
+ wallChunks,
924
+ wallLength,
925
+ wallPieces
926
+ };