@seatlayer/core 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-FGS67GT3.js → chunk-MGC5BVAD.js} +156 -33
- package/dist/chunk-MGC5BVAD.js.map +1 -0
- package/dist/index.cjs +209 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -5
- package/dist/index.d.ts +28 -5
- package/dist/index.js +61 -13
- package/dist/index.js.map +1 -1
- package/dist/{types-CG2pEDoI.d.cts → types-DO_J5DtX.d.cts} +27 -1
- package/dist/{types-CG2pEDoI.d.ts → types-DO_J5DtX.d.ts} +27 -1
- package/dist/view3d/index.cjs +1556 -251
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +80 -1
- package/dist/view3d/index.d.ts +80 -1
- package/dist/view3d/index.js +1548 -249
- package/dist/view3d/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FGS67GT3.js.map +0 -1
package/dist/view3d/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(view3d_exports, {
|
|
|
34
34
|
mountVenue3D: () => mountVenue3D
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(view3d_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_ogl8 = require("ogl");
|
|
38
38
|
|
|
39
39
|
// src/view3d/gl/context.ts
|
|
40
40
|
var import_ogl = require("ogl");
|
|
@@ -52,13 +52,36 @@ var SEAT_STATE_COLORS = {
|
|
|
52
52
|
selected: [0.24, 0.74, 1],
|
|
53
53
|
dimmed: [0.28, 0.32, 0.37]
|
|
54
54
|
};
|
|
55
|
+
function upholsteryTone(rgb) {
|
|
56
|
+
const luma = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
|
57
|
+
const SAT = 0.42;
|
|
58
|
+
const VALUE = 0.62;
|
|
59
|
+
return [
|
|
60
|
+
(luma + (rgb[0] - luma) * SAT) * VALUE,
|
|
61
|
+
(luma + (rgb[1] - luma) * SAT) * VALUE,
|
|
62
|
+
(luma + (rgb[2] - luma) * SAT) * VALUE
|
|
63
|
+
];
|
|
64
|
+
}
|
|
55
65
|
var STRUCTURE = {
|
|
56
66
|
ground: [0.07, 0.085, 0.11],
|
|
57
67
|
tierTop: [0.24, 0.28, 0.34],
|
|
58
68
|
tierWall: [0.17, 0.2, 0.25],
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The lit performance surface.
|
|
71
|
+
*
|
|
72
|
+
* This was [0.42, 0.36, 0.26] and commented "slightly emissive read", which it
|
|
73
|
+
* was not — under the scene's rig it resolved to a mid-brown slab, and from a
|
|
74
|
+
* seat the stage was the DIMMEST large surface in a dark hall. That is exactly
|
|
75
|
+
* backwards: the stage is the one place in a venue with light pointed at it,
|
|
76
|
+
* and it is what a buyer's eye should land on when they arrive at their seat.
|
|
77
|
+
*
|
|
78
|
+
* Bright and warm enough to hold that role against the surrounding structure,
|
|
79
|
+
* which sits around 0.24. An authored `fill` still tints it (see `buildShape`),
|
|
80
|
+
* so an organizer's own stage colour survives — it is just no longer lit like
|
|
81
|
+
* a basement.
|
|
82
|
+
*/
|
|
83
|
+
stageTop: [0.86, 0.64, 0.36],
|
|
84
|
+
stageWall: [0.34, 0.26, 0.18],
|
|
62
85
|
decorTop: [0.22, 0.25, 0.29],
|
|
63
86
|
decorWall: [0.15, 0.17, 0.2],
|
|
64
87
|
gaTop: [0.24, 0.28, 0.33],
|
|
@@ -207,10 +230,18 @@ var OrbitCamera = class {
|
|
|
207
230
|
this.maxDist = 100;
|
|
208
231
|
this.gestureFired = false;
|
|
209
232
|
this.dragging = false;
|
|
233
|
+
this.panning = false;
|
|
210
234
|
this.lastX = 0;
|
|
211
235
|
this.lastY = 0;
|
|
212
236
|
this.activePointers = /* @__PURE__ */ new Map();
|
|
213
237
|
this.pinchDist = 0;
|
|
238
|
+
this.pinchCx = 0;
|
|
239
|
+
this.pinchCy = 0;
|
|
240
|
+
/** Damped pan pivot. `target` chases this the way azimuth chases `azT`. */
|
|
241
|
+
this.targetT = new import_ogl2.Vec3();
|
|
242
|
+
/** Venue centre + radius, so pan can be clamped to somewhere still useful. */
|
|
243
|
+
this.panAnchor = new import_ogl2.Vec3();
|
|
244
|
+
this.panLimit = 0;
|
|
214
245
|
this.camera = new import_ogl2.Camera(gl, { fov: FOV, near: 0.1, far: 5e3, aspect: 1 });
|
|
215
246
|
this.canvas = canvas;
|
|
216
247
|
this.requestRender = requestRender;
|
|
@@ -222,12 +253,17 @@ var OrbitCamera = class {
|
|
|
222
253
|
}
|
|
223
254
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
224
255
|
if (this.activePointers.size === 1) {
|
|
225
|
-
this.
|
|
256
|
+
this.panning = e.button === 2 || e.button === 1 || e.shiftKey;
|
|
257
|
+
this.dragging = !this.panning;
|
|
226
258
|
this.lastX = e.clientX;
|
|
227
259
|
this.lastY = e.clientY;
|
|
228
260
|
} else if (this.activePointers.size === 2) {
|
|
229
261
|
this.dragging = false;
|
|
262
|
+
this.panning = false;
|
|
230
263
|
this.pinchDist = this.currentPinchDistance();
|
|
264
|
+
const c = this.pinchCentroid();
|
|
265
|
+
this.pinchCx = c.x;
|
|
266
|
+
this.pinchCy = c.y;
|
|
231
267
|
}
|
|
232
268
|
};
|
|
233
269
|
this.onPointerMove = (e) => {
|
|
@@ -240,11 +276,24 @@ var OrbitCamera = class {
|
|
|
240
276
|
this.fireGesture();
|
|
241
277
|
}
|
|
242
278
|
this.pinchDist = d;
|
|
279
|
+
const c = this.pinchCentroid();
|
|
280
|
+
this.panBy(c.x - this.pinchCx, c.y - this.pinchCy);
|
|
281
|
+
this.pinchCx = c.x;
|
|
282
|
+
this.pinchCy = c.y;
|
|
243
283
|
return;
|
|
244
284
|
}
|
|
245
|
-
if (!this.dragging) return;
|
|
246
285
|
const dx = e.clientX - this.lastX;
|
|
247
286
|
const dy = e.clientY - this.lastY;
|
|
287
|
+
if (this.panning) {
|
|
288
|
+
this.lastX = e.clientX;
|
|
289
|
+
this.lastY = e.clientY;
|
|
290
|
+
if (dx !== 0 || dy !== 0) {
|
|
291
|
+
this.panBy(dx, dy);
|
|
292
|
+
this.fireGesture();
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!this.dragging) return;
|
|
248
297
|
this.lastX = e.clientX;
|
|
249
298
|
this.lastY = e.clientY;
|
|
250
299
|
if (dx !== 0 || dy !== 0) this.fireGesture();
|
|
@@ -259,7 +308,13 @@ var OrbitCamera = class {
|
|
|
259
308
|
} catch {
|
|
260
309
|
}
|
|
261
310
|
if (this.activePointers.size < 2) this.pinchDist = 0;
|
|
262
|
-
if (this.activePointers.size === 0)
|
|
311
|
+
if (this.activePointers.size === 0) {
|
|
312
|
+
this.dragging = false;
|
|
313
|
+
this.panning = false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
this.onContextMenu = (e) => {
|
|
317
|
+
e.preventDefault();
|
|
263
318
|
};
|
|
264
319
|
this.onWheel = (e) => {
|
|
265
320
|
e.preventDefault();
|
|
@@ -273,6 +328,55 @@ var OrbitCamera = class {
|
|
|
273
328
|
canvas.addEventListener("pointerup", this.onPointerUp);
|
|
274
329
|
canvas.addEventListener("pointercancel", this.onPointerUp);
|
|
275
330
|
canvas.addEventListener("wheel", this.onWheel, { passive: false });
|
|
331
|
+
canvas.addEventListener("contextmenu", this.onContextMenu);
|
|
332
|
+
}
|
|
333
|
+
pinchCentroid() {
|
|
334
|
+
const pts = [...this.activePointers.values()];
|
|
335
|
+
if (!pts.length) return { x: 0, y: 0 };
|
|
336
|
+
let x = 0;
|
|
337
|
+
let y = 0;
|
|
338
|
+
for (const p of pts) {
|
|
339
|
+
x += p.x;
|
|
340
|
+
y += p.y;
|
|
341
|
+
}
|
|
342
|
+
return { x: x / pts.length, y: y / pts.length };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Slide the orbit pivot across the camera's own screen plane.
|
|
346
|
+
*
|
|
347
|
+
* Scaled by distance and FOV so a pixel of drag moves the same amount of VENUE
|
|
348
|
+
* under the cursor whatever the zoom: at the overview a drag sweeps the whole
|
|
349
|
+
* bowl, and pushed in among the seats it nudges. A fixed world-units-per-pixel
|
|
350
|
+
* would be unusable at one end or the other.
|
|
351
|
+
*/
|
|
352
|
+
panBy(dxPx, dyPx) {
|
|
353
|
+
const h = this.canvas.clientHeight || 1;
|
|
354
|
+
const perPx = 2 * this.distance * Math.tan(this.fovY * DEG / 2) / h;
|
|
355
|
+
const sinA = Math.sin(this.azimuth);
|
|
356
|
+
const cosA = Math.cos(this.azimuth);
|
|
357
|
+
const rightX = cosA;
|
|
358
|
+
const rightZ = -sinA;
|
|
359
|
+
const cp = Math.cos(this.polar);
|
|
360
|
+
const sp = Math.sin(this.polar);
|
|
361
|
+
const fwdX = -sinA * cp;
|
|
362
|
+
const fwdZ = -cosA * cp;
|
|
363
|
+
this.targetT.x += (-dxPx * rightX + dyPx * fwdX) * perPx;
|
|
364
|
+
this.targetT.z += (-dxPx * rightZ + dyPx * fwdZ) * perPx;
|
|
365
|
+
this.targetT.y += dyPx * sp * perPx;
|
|
366
|
+
this.clampPan();
|
|
367
|
+
this.requestRender();
|
|
368
|
+
}
|
|
369
|
+
/** Keep the pivot within a bounds-derived box so a stray drag cannot lose the
|
|
370
|
+
* venue entirely — the Overview chip should never be the only way back. */
|
|
371
|
+
clampPan() {
|
|
372
|
+
if (this.panLimit <= 0) return;
|
|
373
|
+
const lim = this.panLimit;
|
|
374
|
+
const cx = this.panAnchor.x;
|
|
375
|
+
const cy = this.panAnchor.y;
|
|
376
|
+
const cz = this.panAnchor.z;
|
|
377
|
+
this.targetT.x = Math.max(cx - lim, Math.min(cx + lim, this.targetT.x));
|
|
378
|
+
this.targetT.z = Math.max(cz - lim, Math.min(cz + lim, this.targetT.z));
|
|
379
|
+
this.targetT.y = Math.max(cy - lim * 0.5, Math.min(cy + lim, this.targetT.y));
|
|
276
380
|
}
|
|
277
381
|
/** One-shot: notify the first real user gesture (drives 3d_orbit_engaged). */
|
|
278
382
|
fireGesture() {
|
|
@@ -299,6 +403,9 @@ var OrbitCamera = class {
|
|
|
299
403
|
*/
|
|
300
404
|
frame(bounds, intro = false, stageAzimuth) {
|
|
301
405
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
406
|
+
this.targetT.copy(this.target);
|
|
407
|
+
this.panAnchor.copy(this.target);
|
|
408
|
+
this.panLimit = Math.max(1, bounds.radius) * 1.5;
|
|
302
409
|
const r = Math.max(1, bounds.radius);
|
|
303
410
|
const halfV = this.fovY * DEG / 2;
|
|
304
411
|
const aspect = this.camera.aspect || 1;
|
|
@@ -330,6 +437,9 @@ var OrbitCamera = class {
|
|
|
330
437
|
*/
|
|
331
438
|
frameSoft(bounds, stageAzimuth) {
|
|
332
439
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
440
|
+
this.targetT.copy(this.target);
|
|
441
|
+
this.panAnchor.copy(this.target);
|
|
442
|
+
this.panLimit = Math.max(1, bounds.radius) * 1.5;
|
|
333
443
|
this.syncFromCamera();
|
|
334
444
|
this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
|
|
335
445
|
const r = Math.max(1, bounds.radius);
|
|
@@ -346,10 +456,17 @@ var OrbitCamera = class {
|
|
|
346
456
|
const da = this.azT - this.azimuth;
|
|
347
457
|
const dp = this.polT - this.polar;
|
|
348
458
|
const dd = this.distT - this.distance;
|
|
349
|
-
const
|
|
459
|
+
const tx = this.targetT.x - this.target.x;
|
|
460
|
+
const ty = this.targetT.y - this.target.y;
|
|
461
|
+
const tz = this.targetT.z - this.target.z;
|
|
462
|
+
const panEps = Math.max(1e-4, this.distance * 1e-4);
|
|
463
|
+
const moving = Math.abs(da) > 1e-4 || Math.abs(dp) > 1e-4 || Math.abs(dd) > 1e-4 || Math.abs(tx) > panEps || Math.abs(ty) > panEps || Math.abs(tz) > panEps;
|
|
350
464
|
this.azimuth += da * DAMP;
|
|
351
465
|
this.polar += dp * DAMP;
|
|
352
466
|
this.distance += dd * DAMP;
|
|
467
|
+
this.target.x += tx * DAMP;
|
|
468
|
+
this.target.y += ty * DAMP;
|
|
469
|
+
this.target.z += tz * DAMP;
|
|
353
470
|
if (moving) this.applyPosition();
|
|
354
471
|
return moving;
|
|
355
472
|
}
|
|
@@ -375,6 +492,7 @@ var OrbitCamera = class {
|
|
|
375
492
|
/** Point the orbit pivot at a new world target without moving the camera. */
|
|
376
493
|
setTarget(target) {
|
|
377
494
|
this.target.set(target[0], target[1], target[2]);
|
|
495
|
+
this.targetT.copy(this.target);
|
|
378
496
|
}
|
|
379
497
|
/** Restore the base FOV (a flight ends pushed-in) and re-sync orbit state. A
|
|
380
498
|
* flight ends looking at `target` (the venue focal), so re-pivot there first —
|
|
@@ -382,6 +500,7 @@ var OrbitCamera = class {
|
|
|
382
500
|
resumeAfterFlight(target) {
|
|
383
501
|
this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
|
|
384
502
|
if (target) this.target.set(target[0], target[1], target[2]);
|
|
503
|
+
this.targetT.copy(this.target);
|
|
385
504
|
this.syncFromCamera();
|
|
386
505
|
}
|
|
387
506
|
applyPosition() {
|
|
@@ -398,6 +517,7 @@ var OrbitCamera = class {
|
|
|
398
517
|
this.canvas.removeEventListener("pointerup", this.onPointerUp);
|
|
399
518
|
this.canvas.removeEventListener("pointercancel", this.onPointerUp);
|
|
400
519
|
this.canvas.removeEventListener("wheel", this.onWheel);
|
|
520
|
+
this.canvas.removeEventListener("contextmenu", this.onContextMenu);
|
|
401
521
|
this.activePointers.clear();
|
|
402
522
|
}
|
|
403
523
|
};
|
|
@@ -446,17 +566,129 @@ var RenderLoop = class {
|
|
|
446
566
|
};
|
|
447
567
|
|
|
448
568
|
// src/view3d/lod.ts
|
|
569
|
+
var SEAT_MIN_PIXELS_NEAR = 2.5;
|
|
570
|
+
var SEAT_MIN_PIXELS_FAR = 1.15;
|
|
571
|
+
var CHAIR_FULL_M = 12;
|
|
572
|
+
var CHAIR_NONE_M = 22;
|
|
573
|
+
var CHAIR_GATHER_M = 30;
|
|
574
|
+
var CHAIR_REBUILD_M = 4;
|
|
575
|
+
var CHAIR_MAX_INSTANCES = 8192;
|
|
576
|
+
var CHAIR_REQUIRED_COVER_M = CHAIR_NONE_M + CHAIR_REBUILD_M;
|
|
449
577
|
function computeSeatLod(distance, radius) {
|
|
450
578
|
const near = radius * 1.4;
|
|
451
579
|
const far = radius * 3.2;
|
|
452
|
-
if (distance <= near) return { scale: 1, fade: 0 };
|
|
580
|
+
if (distance <= near) return { scale: 1, fade: 0, minPixels: SEAT_MIN_PIXELS_NEAR };
|
|
453
581
|
const t = Math.min(1, (distance - near) / Math.max(1e-3, far - near));
|
|
454
582
|
return {
|
|
455
583
|
scale: 1 - t * 0.4,
|
|
456
|
-
fade: t * 0.55
|
|
584
|
+
fade: t * 0.55,
|
|
585
|
+
// Tapered on the same ramp as the fade: as the block starts reading by its
|
|
586
|
+
// tier tint, the dots stop fighting each other for pixels.
|
|
587
|
+
minPixels: SEAT_MIN_PIXELS_NEAR + t * (SEAT_MIN_PIXELS_FAR - SEAT_MIN_PIXELS_NEAR)
|
|
457
588
|
};
|
|
458
589
|
}
|
|
459
590
|
|
|
591
|
+
// src/view3d/scene/nearField.ts
|
|
592
|
+
var SEATS_PER_CELL = 4;
|
|
593
|
+
var NearFieldIndex = class {
|
|
594
|
+
constructor(iPosition, count) {
|
|
595
|
+
this.minX = 0;
|
|
596
|
+
this.minZ = 0;
|
|
597
|
+
this.cell = 1;
|
|
598
|
+
this.cols = 1;
|
|
599
|
+
this.rows = 1;
|
|
600
|
+
/** CSR-style buckets: `cellStart[c]…cellStart[c+1]` indexes into `cellItems`. */
|
|
601
|
+
this.cellStart = null;
|
|
602
|
+
this.cellItems = null;
|
|
603
|
+
this.iPosition = iPosition;
|
|
604
|
+
this.count = count;
|
|
605
|
+
}
|
|
606
|
+
/** Built on demand; safe to call repeatedly. */
|
|
607
|
+
ensureGrid() {
|
|
608
|
+
if (this.cellStart || this.count === 0) return;
|
|
609
|
+
const p = this.iPosition;
|
|
610
|
+
let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity;
|
|
611
|
+
for (let i = 0; i < this.count; i++) {
|
|
612
|
+
const x = p[i * 3], z = p[i * 3 + 2];
|
|
613
|
+
if (x < minX) minX = x;
|
|
614
|
+
if (x > maxX) maxX = x;
|
|
615
|
+
if (z < minZ) minZ = z;
|
|
616
|
+
if (z > maxZ) maxZ = z;
|
|
617
|
+
}
|
|
618
|
+
const w = Math.max(maxX - minX, 1e-3);
|
|
619
|
+
const h = Math.max(maxZ - minZ, 1e-3);
|
|
620
|
+
this.cell = Math.max(Math.sqrt(w * h * SEATS_PER_CELL / this.count), 0.25);
|
|
621
|
+
this.cols = Math.max(1, Math.ceil(w / this.cell) + 1);
|
|
622
|
+
this.rows = Math.max(1, Math.ceil(h / this.cell) + 1);
|
|
623
|
+
this.minX = minX;
|
|
624
|
+
this.minZ = minZ;
|
|
625
|
+
const nCells = this.cols * this.rows;
|
|
626
|
+
const start = new Int32Array(nCells + 1);
|
|
627
|
+
const cellOf = (i) => {
|
|
628
|
+
const cx = Math.min(this.cols - 1, Math.max(0, Math.floor((p[i * 3] - minX) / this.cell)));
|
|
629
|
+
const cz = Math.min(this.rows - 1, Math.max(0, Math.floor((p[i * 3 + 2] - minZ) / this.cell)));
|
|
630
|
+
return cz * this.cols + cx;
|
|
631
|
+
};
|
|
632
|
+
for (let i = 0; i < this.count; i++) start[cellOf(i) + 1]++;
|
|
633
|
+
for (let c = 0; c < nCells; c++) start[c + 1] += start[c];
|
|
634
|
+
const items = new Int32Array(this.count);
|
|
635
|
+
const cursor = start.slice(0, nCells);
|
|
636
|
+
for (let i = 0; i < this.count; i++) items[cursor[cellOf(i)]++] = i;
|
|
637
|
+
this.cellStart = start;
|
|
638
|
+
this.cellItems = items;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Fill `out` with the indices of seats within `radius` metres of (camX, camZ),
|
|
642
|
+
* nearest cell-ring first, and return how many were written.
|
|
643
|
+
*
|
|
644
|
+
* Ring order is what makes the CHAIR_MAX_INSTANCES cap harmless: when the cap
|
|
645
|
+
* bites it drops the OUTERMOST seats, which are the ones already past the fade
|
|
646
|
+
* band and drawing nothing. A cap that truncated in index order would instead
|
|
647
|
+
* punch holes in the row you are sitting in.
|
|
648
|
+
*
|
|
649
|
+
* Note this is a horizontal (XZ) query and ignores height. A stacked venue's
|
|
650
|
+
* upper tier is therefore gathered along with the stalls beneath it — which is
|
|
651
|
+
* correct, because the fade weight is re-derived from true view depth in the
|
|
652
|
+
* shader anyway. The grid's only job is to bound the candidate set.
|
|
653
|
+
*/
|
|
654
|
+
gather(camX, camZ, radius, out) {
|
|
655
|
+
this.ensureGrid();
|
|
656
|
+
const start = this.cellStart;
|
|
657
|
+
const items = this.cellItems;
|
|
658
|
+
if (!start || !items) return 0;
|
|
659
|
+
const cap = out.length;
|
|
660
|
+
const r2 = radius * radius;
|
|
661
|
+
const cx = Math.floor((camX - this.minX) / this.cell);
|
|
662
|
+
const cz = Math.floor((camZ - this.minZ) / this.cell);
|
|
663
|
+
const maxRing = Math.ceil(radius / this.cell) + 1;
|
|
664
|
+
const p = this.iPosition;
|
|
665
|
+
let n = 0;
|
|
666
|
+
for (let ring = 0; ring <= maxRing && n < cap; ring++) {
|
|
667
|
+
const z0 = cz - ring, z1 = cz + ring;
|
|
668
|
+
const x0 = cx - ring, x1 = cx + ring;
|
|
669
|
+
for (let gz = z0; gz <= z1 && n < cap; gz++) {
|
|
670
|
+
if (gz < 0 || gz >= this.rows) continue;
|
|
671
|
+
const edge = gz === z0 || gz === z1;
|
|
672
|
+
for (let gx = x0; gx <= x1 && n < cap; gx++) {
|
|
673
|
+
if (!edge && gx !== x0 && gx !== x1) {
|
|
674
|
+
gx = x1 - 1;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (gx < 0 || gx >= this.cols) continue;
|
|
678
|
+
const c = gz * this.cols + gx;
|
|
679
|
+
for (let k = start[c], e = start[c + 1]; k < e && n < cap; k++) {
|
|
680
|
+
const i = items[k];
|
|
681
|
+
const dx = p[i * 3] - camX;
|
|
682
|
+
const dz = p[i * 3 + 2] - camZ;
|
|
683
|
+
if (dx * dx + dz * dz <= r2) out[n++] = i;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return n;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
460
692
|
// src/core/types.ts
|
|
461
693
|
var ACCESSIBILITY_TYPES = [
|
|
462
694
|
{ key: "wheelchair", label: "Wheelchair space", short: "Wheelchair", icon: "\u267F" },
|
|
@@ -483,6 +715,12 @@ function accessibilityRingColor(types) {
|
|
|
483
715
|
const primary = types?.[0];
|
|
484
716
|
return primary && ACCESSIBILITY_RING_COLOR[primary] || "#3b82f6";
|
|
485
717
|
}
|
|
718
|
+
var SEAT_COMMERCIAL_MARKS = [
|
|
719
|
+
{ key: "obstructedView", label: "Obstructed view", short: "Obstructed", icon: "\u26D4" },
|
|
720
|
+
{ key: "restrictedView", label: "Restricted view", short: "Restricted", icon: "\u{1F441}" },
|
|
721
|
+
{ key: "premium", label: "Premium seat", short: "Premium", icon: "\u2605" }
|
|
722
|
+
];
|
|
723
|
+
var SEAT_COMMERCIAL_LABEL = new Map(SEAT_COMMERCIAL_MARKS.map((mark) => [mark.key, mark]));
|
|
486
724
|
var SURROUNDINGS_SHAPE_ROLES = [
|
|
487
725
|
"reference-focal",
|
|
488
726
|
"bar",
|
|
@@ -959,8 +1197,195 @@ function rectPolygon(x, y, w, h) {
|
|
|
959
1197
|
];
|
|
960
1198
|
}
|
|
961
1199
|
|
|
1200
|
+
// src/view3d/scene/seatChair.ts
|
|
1201
|
+
var CHAIR_PART = { pedestal: 0, pad: 1, back: 2, body: 3, head: 4 };
|
|
1202
|
+
var CHAIR_PART_OCCUPANT_MIN = CHAIR_PART.body;
|
|
1203
|
+
var CHAIR_PITCH_FRACTION = 0.44;
|
|
1204
|
+
var CHAIR_HALF_WIDTH_MIN_M = 0.15;
|
|
1205
|
+
var CHAIR_HALF_WIDTH_MAX_M = 0.3;
|
|
1206
|
+
var CHAIR_HALF_WIDTH_DEFAULT_M = 0.24;
|
|
1207
|
+
function chairHalfWidth(pitchM) {
|
|
1208
|
+
if (pitchM === void 0 || !Number.isFinite(pitchM) || pitchM <= 0) {
|
|
1209
|
+
return CHAIR_HALF_WIDTH_DEFAULT_M;
|
|
1210
|
+
}
|
|
1211
|
+
return Math.min(CHAIR_HALF_WIDTH_MAX_M, Math.max(CHAIR_HALF_WIDTH_MIN_M, pitchM * CHAIR_PITCH_FRACTION));
|
|
1212
|
+
}
|
|
1213
|
+
var BACK_RAKE_SLOPE = 0.21;
|
|
1214
|
+
var PAD_BACK_GAP_M = 0.05;
|
|
1215
|
+
var PAD_TOP_M = 0.45;
|
|
1216
|
+
var BACK_BASE_M = PAD_TOP_M + PAD_BACK_GAP_M;
|
|
1217
|
+
var BOXES = [
|
|
1218
|
+
// Pedestal — a plain column under the pad. Without it the pad floats 0.36 m
|
|
1219
|
+
// over the deck and the row reads as hovering trays.
|
|
1220
|
+
{ part: CHAIR_PART.pedestal, min: [-0.3, 0, -0.3], max: [0.3, 0.36, 0.3] },
|
|
1221
|
+
// Seat pad — a full seat width across and about as deep, which is what a real
|
|
1222
|
+
// one is. Its depth is bounded by the same pitch as its width, because the
|
|
1223
|
+
// pitch measure is the tighter of the in-row and row-to-row spacings, so a
|
|
1224
|
+
// tightly-raked tier cannot drive a pad into the back of the row in front.
|
|
1225
|
+
{ part: CHAIR_PART.pad, min: [-1, 0.36, -0.95], max: [1, PAD_TOP_M, 1] },
|
|
1226
|
+
// Back panel — thin, raked, and the tallest thing in the row, so it is what
|
|
1227
|
+
// carries the state colour when you look along a row from behind.
|
|
1228
|
+
{
|
|
1229
|
+
part: CHAIR_PART.back,
|
|
1230
|
+
min: [-1, BACK_BASE_M, -1],
|
|
1231
|
+
max: [1, 0.92, -0.72]
|
|
1232
|
+
},
|
|
1233
|
+
// --- the occupant ---------------------------------------------------------
|
|
1234
|
+
//
|
|
1235
|
+
// A hall with every seat empty reads as an architectural model, not a venue.
|
|
1236
|
+
// The 2048-px panorama this replaced drew a crowd; losing it was the price of
|
|
1237
|
+
// sharpness, and this is how it is bought back — in geometry, where there is
|
|
1238
|
+
// no resolution ceiling.
|
|
1239
|
+
//
|
|
1240
|
+
// Deliberately two blocks and no limbs. At the range these are visible a
|
|
1241
|
+
// person is a torso and a head, and every extra part multiplies by the number
|
|
1242
|
+
// of occupied seats in view. The silhouette is what carries it, exactly as it
|
|
1243
|
+
// does in the generated panorama's head-and-shoulder figures.
|
|
1244
|
+
//
|
|
1245
|
+
// Sized as a seated adult against the 0.92 m chair back: hips at the pad top,
|
|
1246
|
+
// shoulders just above the back panel, head clear of it. Torso is narrower
|
|
1247
|
+
// than the chair so neighbours never interpenetrate at any pitch, and it sits
|
|
1248
|
+
// forward of the back panel rather than inside it.
|
|
1249
|
+
{
|
|
1250
|
+
part: CHAIR_PART.body,
|
|
1251
|
+
min: [-0.66, PAD_TOP_M, -0.58],
|
|
1252
|
+
max: [0.66, 1, 0.26]
|
|
1253
|
+
},
|
|
1254
|
+
// The head is deliberately SMALL. Sized by eye against the chair it came out
|
|
1255
|
+
// near-cubic and read as Lego; a real head is about 0.16 m across and 0.22 m
|
|
1256
|
+
// tall, which against a 0.24 m chair half-width is roughly a third of the
|
|
1257
|
+
// chair's width and clearly taller than it is wide. Getting this ratio wrong
|
|
1258
|
+
// is what makes a crowd look like toys rather than people.
|
|
1259
|
+
{
|
|
1260
|
+
part: CHAIR_PART.head,
|
|
1261
|
+
min: [-0.34, 1.03, -0.4],
|
|
1262
|
+
max: [0.34, 1.27, 0.02]
|
|
1263
|
+
}
|
|
1264
|
+
];
|
|
1265
|
+
var FACES = [
|
|
1266
|
+
// +X
|
|
1267
|
+
{ n: [1, 0, 0], c: [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]] },
|
|
1268
|
+
// -X
|
|
1269
|
+
{ n: [-1, 0, 0], c: [[0, 0, 1], [0, 1, 1], [0, 1, 0], [0, 0, 0]] },
|
|
1270
|
+
// +Y
|
|
1271
|
+
{ n: [0, 1, 0], c: [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]] },
|
|
1272
|
+
// -Y
|
|
1273
|
+
{ n: [0, -1, 0], c: [[0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1]] },
|
|
1274
|
+
// +Z
|
|
1275
|
+
{ n: [0, 0, 1], c: [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]] },
|
|
1276
|
+
// -Z
|
|
1277
|
+
{ n: [0, 0, -1], c: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]] }
|
|
1278
|
+
];
|
|
1279
|
+
function buildChairMesh() {
|
|
1280
|
+
const vertexCount = BOXES.length * FACES.length * 4;
|
|
1281
|
+
const indexCount = BOXES.length * FACES.length * 6;
|
|
1282
|
+
const position = new Float32Array(vertexCount * 3);
|
|
1283
|
+
const normal = new Float32Array(vertexCount * 3);
|
|
1284
|
+
const part = new Float32Array(vertexCount);
|
|
1285
|
+
const index = new Uint16Array(indexCount);
|
|
1286
|
+
let v = 0;
|
|
1287
|
+
let t = 0;
|
|
1288
|
+
for (const box of BOXES) {
|
|
1289
|
+
for (const face of FACES) {
|
|
1290
|
+
const base = v;
|
|
1291
|
+
const corner3 = new Float32Array(12);
|
|
1292
|
+
for (let ci = 0; ci < 4; ci++) {
|
|
1293
|
+
const corner = face.c[ci];
|
|
1294
|
+
for (let a = 0; a < 3; a++) {
|
|
1295
|
+
corner3[ci * 3 + a] = corner[a] ? box.max[a] : box.min[a];
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const ax = corner3[3] - corner3[0], ay = corner3[4] - corner3[1], az = corner3[5] - corner3[2];
|
|
1299
|
+
const bx = corner3[6] - corner3[0], by = corner3[7] - corner3[1], bz = corner3[8] - corner3[2];
|
|
1300
|
+
let nx = ay * bz - az * by;
|
|
1301
|
+
let ny = az * bx - ax * bz;
|
|
1302
|
+
let nz = ax * by - ay * bx;
|
|
1303
|
+
const nl = Math.hypot(nx, ny, nz);
|
|
1304
|
+
if (nl > 1e-9) {
|
|
1305
|
+
nx /= nl;
|
|
1306
|
+
ny /= nl;
|
|
1307
|
+
nz /= nl;
|
|
1308
|
+
} else {
|
|
1309
|
+
nx = face.n[0];
|
|
1310
|
+
ny = face.n[1];
|
|
1311
|
+
nz = face.n[2];
|
|
1312
|
+
}
|
|
1313
|
+
if (nx * face.n[0] + ny * face.n[1] + nz * face.n[2] < 0) {
|
|
1314
|
+
nx = -nx;
|
|
1315
|
+
ny = -ny;
|
|
1316
|
+
nz = -nz;
|
|
1317
|
+
}
|
|
1318
|
+
for (let ci = 0; ci < 4; ci++) {
|
|
1319
|
+
position[v * 3] = corner3[ci * 3];
|
|
1320
|
+
position[v * 3 + 1] = corner3[ci * 3 + 1];
|
|
1321
|
+
position[v * 3 + 2] = corner3[ci * 3 + 2];
|
|
1322
|
+
normal[v * 3] = nx;
|
|
1323
|
+
normal[v * 3 + 1] = ny;
|
|
1324
|
+
normal[v * 3 + 2] = nz;
|
|
1325
|
+
part[v] = box.part;
|
|
1326
|
+
v++;
|
|
1327
|
+
}
|
|
1328
|
+
index[t++] = base;
|
|
1329
|
+
index[t++] = base + 1;
|
|
1330
|
+
index[t++] = base + 2;
|
|
1331
|
+
index[t++] = base;
|
|
1332
|
+
index[t++] = base + 2;
|
|
1333
|
+
index[t++] = base + 3;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return { position, normal, part, index, vertexCount, indexCount };
|
|
1337
|
+
}
|
|
1338
|
+
function computeSeatYaw(iPosition, count, rowIdAt, focal) {
|
|
1339
|
+
const yaw = new Float32Array(count);
|
|
1340
|
+
const px = (i) => iPosition[i * 3];
|
|
1341
|
+
const pz = (i) => iPosition[i * 3 + 2];
|
|
1342
|
+
let runStart = 0;
|
|
1343
|
+
const flushRun = (start, end) => {
|
|
1344
|
+
const n = end - start;
|
|
1345
|
+
for (let i = start; i < end; i++) {
|
|
1346
|
+
const [fx, fz] = focal(i);
|
|
1347
|
+
let dx = fx - px(i);
|
|
1348
|
+
let dz = fz - pz(i);
|
|
1349
|
+
if (n >= 2) {
|
|
1350
|
+
const a = Math.max(start, i - 1);
|
|
1351
|
+
const b = Math.min(end - 1, i + 1);
|
|
1352
|
+
const tx = px(b) - px(a);
|
|
1353
|
+
const tz = pz(b) - pz(a);
|
|
1354
|
+
const tl = Math.hypot(tx, tz);
|
|
1355
|
+
if (tl > 1e-6) {
|
|
1356
|
+
let nx = -tz / tl;
|
|
1357
|
+
let nz = tx / tl;
|
|
1358
|
+
if (nx * dx + nz * dz < 0) {
|
|
1359
|
+
nx = -nx;
|
|
1360
|
+
nz = -nz;
|
|
1361
|
+
}
|
|
1362
|
+
dx = nx;
|
|
1363
|
+
dz = nz;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
yaw[i] = dx === 0 && dz === 0 ? 0 : Math.atan2(dx, dz);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
for (let i = 1; i <= count; i++) {
|
|
1370
|
+
if (i === count || rowIdAt(i) !== rowIdAt(runStart)) {
|
|
1371
|
+
flushRun(runStart, i);
|
|
1372
|
+
runStart = i;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return yaw;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
962
1378
|
// src/view3d/scene/seatInstances.ts
|
|
963
1379
|
var SEAT_DOT_RADIUS_M = 0.22;
|
|
1380
|
+
function seatOccupantSeed(stateIndex, seatIndex) {
|
|
1381
|
+
const state = SEAT_STATES[stateIndex];
|
|
1382
|
+
if (state !== "sold" && state !== "held") return -1;
|
|
1383
|
+
let h = (seatIndex + 1) * 2654435761;
|
|
1384
|
+
h ^= h >>> 15;
|
|
1385
|
+
h = Math.imul(h, 2246822519);
|
|
1386
|
+
h ^= h >>> 13;
|
|
1387
|
+
return (h >>> 0) % 1e5 / 1e5;
|
|
1388
|
+
}
|
|
964
1389
|
var SEAT_PITCH_FRACTION = 0.42;
|
|
965
1390
|
function nearestNeighbourSpacing(seats) {
|
|
966
1391
|
const n = seats.length;
|
|
@@ -1023,11 +1448,13 @@ function seatSurfaceY(seat) {
|
|
|
1023
1448
|
if (Number.isFinite(eye)) return Math.max(0, eye - SEATED_EYE_HEIGHT_M);
|
|
1024
1449
|
return 0;
|
|
1025
1450
|
}
|
|
1026
|
-
function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
1451
|
+
function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor) {
|
|
1027
1452
|
const count = seats.length;
|
|
1028
1453
|
const iPosition = new Float32Array(count * 3);
|
|
1029
1454
|
const iState = new Float32Array(count);
|
|
1455
|
+
const iCategory = new Float32Array(count * 3);
|
|
1030
1456
|
const iMaxRadius = new Float32Array(count);
|
|
1457
|
+
const iChairWidth = new Float32Array(count);
|
|
1031
1458
|
const iRing = new Float32Array(count * 3);
|
|
1032
1459
|
const idToIndex = /* @__PURE__ */ new Map();
|
|
1033
1460
|
const spacing = nearestNeighbourSpacing(seats);
|
|
@@ -1036,10 +1463,15 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1036
1463
|
const resolved = surfaces?.seatPitchU(i);
|
|
1037
1464
|
const pitchM = (resolved ?? spacing[i]) * M;
|
|
1038
1465
|
iMaxRadius[i] = Number.isFinite(pitchM) ? Math.max(0.06, Math.min(SEAT_DOT_RADIUS_M, pitchM * SEAT_PITCH_FRACTION)) : SEAT_DOT_RADIUS_M;
|
|
1466
|
+
iChairWidth[i] = chairHalfWidth(Number.isFinite(pitchM) ? pitchM : void 0);
|
|
1039
1467
|
iPosition[i * 3] = seat.x * M;
|
|
1040
1468
|
iPosition[i * 3 + 1] = surfaces ? surfaces.seatDeckY(i) : seatSurfaceY(seat);
|
|
1041
1469
|
iPosition[i * 3 + 2] = seat.y * M;
|
|
1042
1470
|
iState[i] = seatStateIndex(initial ? initial(seat) : "available");
|
|
1471
|
+
const cat = hexToRgb(categoryColor?.get(seat.categoryKey) ?? "#6e7bff") ?? [0.43, 0.48, 1];
|
|
1472
|
+
iCategory[i * 3] = cat[0];
|
|
1473
|
+
iCategory[i * 3 + 1] = cat[1];
|
|
1474
|
+
iCategory[i * 3 + 2] = cat[2];
|
|
1043
1475
|
if (seat.accessibility?.length) {
|
|
1044
1476
|
const rgb = hexToRgb(accessibilityRingColor(seat.accessibility));
|
|
1045
1477
|
if (rgb) {
|
|
@@ -1054,10 +1486,13 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1054
1486
|
count,
|
|
1055
1487
|
iPosition,
|
|
1056
1488
|
iState,
|
|
1489
|
+
iCategory,
|
|
1057
1490
|
iMaxRadius,
|
|
1058
1491
|
iRing,
|
|
1059
1492
|
idToIndex,
|
|
1060
|
-
|
|
1493
|
+
iChairWidth,
|
|
1494
|
+
iFloor: seatFloor ?? new Float32Array(count),
|
|
1495
|
+
iYaw: new Float32Array(count)
|
|
1061
1496
|
};
|
|
1062
1497
|
}
|
|
1063
1498
|
var RUN_MERGE_GAP = 64;
|
|
@@ -1192,6 +1627,28 @@ function cullOverlapping(items, separationX, separationY = separationX) {
|
|
|
1192
1627
|
}
|
|
1193
1628
|
return kept;
|
|
1194
1629
|
}
|
|
1630
|
+
var DENSE_LABEL_BUDGET = { row: 8, seat: 14 };
|
|
1631
|
+
function focusScore(screen, worldDistance, width, height) {
|
|
1632
|
+
const half = Math.max(1, Math.min(width, height) * 0.5);
|
|
1633
|
+
const off = Math.min(2, Math.hypot(screen.x - width / 2, screen.y - height / 2) / half);
|
|
1634
|
+
return worldDistance * (1 + 1.5 * off);
|
|
1635
|
+
}
|
|
1636
|
+
function pickDenseLabels(items, separationX, separationY, budget) {
|
|
1637
|
+
const ordered = [...items].sort((a, b) => a.focus - b.focus);
|
|
1638
|
+
const kept = [];
|
|
1639
|
+
for (const item of ordered) {
|
|
1640
|
+
if (kept.length >= budget) break;
|
|
1641
|
+
let clash = false;
|
|
1642
|
+
for (const k of kept) {
|
|
1643
|
+
if (Math.abs(item.screen.x - k.screen.x) < separationX && Math.abs(item.screen.y - k.screen.y) < separationY) {
|
|
1644
|
+
clash = true;
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (!clash) kept.push(item);
|
|
1649
|
+
}
|
|
1650
|
+
return kept;
|
|
1651
|
+
}
|
|
1195
1652
|
function centroidOf(points) {
|
|
1196
1653
|
if (!points.length) return null;
|
|
1197
1654
|
let x = 0, y = 0;
|
|
@@ -1732,195 +2189,6 @@ function buildSectionRake(rows, focal) {
|
|
|
1732
2189
|
return rowsRake(fits);
|
|
1733
2190
|
}
|
|
1734
2191
|
|
|
1735
|
-
// src/view3d/scene/surface.ts
|
|
1736
|
-
var FLAT_SLAB_TOP_M = 0.05;
|
|
1737
|
-
var CAP_MAX_ERROR_M = 0.05;
|
|
1738
|
-
var SEAT_CLEARANCE_M = 0.15;
|
|
1739
|
-
var SEAT_OWNERSHIP_PAD_U = SEAT_DOT_RADIUS_M * 1.5 * CHART_UNITS_PER_METRE;
|
|
1740
|
-
function bboxOf(pts) {
|
|
1741
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1742
|
-
for (const p of pts) {
|
|
1743
|
-
if (p.x < minX) minX = p.x;
|
|
1744
|
-
if (p.y < minY) minY = p.y;
|
|
1745
|
-
if (p.x > maxX) maxX = p.x;
|
|
1746
|
-
if (p.y > maxY) maxY = p.y;
|
|
1747
|
-
}
|
|
1748
|
-
return { minX, minY, maxX, maxY };
|
|
1749
|
-
}
|
|
1750
|
-
var MAX_TIER_RISE_M = 25;
|
|
1751
|
-
function buildVenueSurfaces(units, seats) {
|
|
1752
|
-
const bySection = /* @__PURE__ */ new Map();
|
|
1753
|
-
const seatOwner = new Array(seats.length).fill(null);
|
|
1754
|
-
const seatDeck = new Float64Array(seats.length);
|
|
1755
|
-
const seatRowLevel = new Array(seats.length).fill(void 0);
|
|
1756
|
-
const seatPitch = new Array(seats.length).fill(void 0);
|
|
1757
|
-
const acc = /* @__PURE__ */ new Map();
|
|
1758
|
-
const boxes = [];
|
|
1759
|
-
for (const unit of units) {
|
|
1760
|
-
for (const o of unit.objects) {
|
|
1761
|
-
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1762
|
-
const owned = outsetRing(o.outline, SEAT_OWNERSHIP_PAD_U);
|
|
1763
|
-
acc.set(o.id, { section: o, unit, frontU: Infinity, hasSeats: false, rows: /* @__PURE__ */ new Map(), seatIndices: [] });
|
|
1764
|
-
boxes.push({ id: o.id, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
for (let i = 0; i < seats.length; i++) {
|
|
1768
|
-
const s = seats[i];
|
|
1769
|
-
for (const b of boxes) {
|
|
1770
|
-
if (s.x < b.box.minX || s.x > b.box.maxX || s.y < b.box.minY || s.y > b.box.maxY) continue;
|
|
1771
|
-
if (!pointInPolygonWithHoles({ x: s.x, y: s.y }, b.outline, b.section.holes)) continue;
|
|
1772
|
-
seatOwner[i] = b.id;
|
|
1773
|
-
const a = acc.get(b.id);
|
|
1774
|
-
a.hasSeats = true;
|
|
1775
|
-
const f = s.focalPoint ?? b.unit.focal;
|
|
1776
|
-
const d = Math.hypot(s.x - f.x, s.y - f.y);
|
|
1777
|
-
if (d < a.frontU) a.frontU = d;
|
|
1778
|
-
a.seatIndices.push(i);
|
|
1779
|
-
const rowKey = s.rowId || `__seat-${i}`;
|
|
1780
|
-
const arr = a.rows.get(rowKey);
|
|
1781
|
-
if (arr) arr.push({ x: s.x, y: s.y });
|
|
1782
|
-
else a.rows.set(rowKey, [{ x: s.x, y: s.y }]);
|
|
1783
|
-
break;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
for (const [id, a] of acc) {
|
|
1787
|
-
const geo = sectionGeometry(a.section, { floorBaseHeightM: a.unit.baseHeightM });
|
|
1788
|
-
const bottomY = a.unit.baseHeightM;
|
|
1789
|
-
const rakeTan = geo.rake > 0 ? Math.tan(geo.rake * Math.PI / 180) : 0;
|
|
1790
|
-
const inferredFlat = geo.rake <= 0.01 && geo.height <= bottomY + 1e-3;
|
|
1791
|
-
const kind = a.section.surfaceKind;
|
|
1792
|
-
const flat = kind === "flat" ? true : kind === "rakedRows" ? geo.rake > 0.01 ? false : inferredFlat : inferredFlat;
|
|
1793
|
-
const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
|
|
1794
|
-
const needsRake = structure.rows.length < 2;
|
|
1795
|
-
const rake = needsRake ? buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal) : null;
|
|
1796
|
-
let frontU = Infinity;
|
|
1797
|
-
if (rake) {
|
|
1798
|
-
if (a.hasSeats) {
|
|
1799
|
-
for (const pts of a.rows.values()) {
|
|
1800
|
-
for (const p of pts) {
|
|
1801
|
-
const d = rake.depthAt(p.x, p.y);
|
|
1802
|
-
if (d < frontU) frontU = d;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
if (!a.hasSeats || !Number.isFinite(frontU)) {
|
|
1807
|
-
frontU = Infinity;
|
|
1808
|
-
for (const p of a.section.outline) {
|
|
1809
|
-
const d = rake.depthAt(p.x, p.y);
|
|
1810
|
-
if (d < frontU) frontU = d;
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
const flatTop = bottomY + FLAT_SLAB_TOP_M;
|
|
1815
|
-
const baseFloor = bottomY + FLAT_SLAB_TOP_M;
|
|
1816
|
-
const levelFor = (depthU) => {
|
|
1817
|
-
const depthM = Math.max(0, depthU - frontU) * METRES_PER_CHART_UNIT;
|
|
1818
|
-
const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
|
|
1819
|
-
return Math.max(baseFloor, geo.height + rise);
|
|
1820
|
-
};
|
|
1821
|
-
const levelForBlockDepth = (blockDepthU) => {
|
|
1822
|
-
const depthM = Math.max(0, blockDepthU) * METRES_PER_CHART_UNIT;
|
|
1823
|
-
const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
|
|
1824
|
-
return Math.max(baseFloor, geo.height + rise);
|
|
1825
|
-
};
|
|
1826
|
-
const rowLevels = flat ? [] : structure.rows.map((r) => ({
|
|
1827
|
-
pts: r.pts,
|
|
1828
|
-
y: levelForBlockDepth(r.blockDepth),
|
|
1829
|
-
depth: r.blockDepth,
|
|
1830
|
-
blockId: r.blockId
|
|
1831
|
-
}));
|
|
1832
|
-
const landingY = rowLevels.length ? rowLevels[0].y : flatTop;
|
|
1833
|
-
const rowBounds = rowLevels.map((r) => {
|
|
1834
|
-
let cx = 0, cy = 0;
|
|
1835
|
-
for (const p of r.pts) {
|
|
1836
|
-
cx += p.x;
|
|
1837
|
-
cy += p.y;
|
|
1838
|
-
}
|
|
1839
|
-
const n = r.pts.length || 1;
|
|
1840
|
-
cx /= n;
|
|
1841
|
-
cy /= n;
|
|
1842
|
-
let rad = 0;
|
|
1843
|
-
for (const p of r.pts) {
|
|
1844
|
-
const d = Math.hypot(p.x - cx, p.y - cy);
|
|
1845
|
-
if (d > rad) rad = d;
|
|
1846
|
-
}
|
|
1847
|
-
return { cx, cy, rad };
|
|
1848
|
-
});
|
|
1849
|
-
const deckAt = flat ? () => flatTop : rowLevels.length >= 2 ? (x, y) => {
|
|
1850
|
-
let best = Infinity, bestY = landingY;
|
|
1851
|
-
for (let i = 0; i < rowLevels.length; i++) {
|
|
1852
|
-
const b = rowBounds[i];
|
|
1853
|
-
if (Math.hypot(x - b.cx, y - b.cy) - b.rad >= best) continue;
|
|
1854
|
-
const d = distanceToPolyline(rowLevels[i].pts, x, y);
|
|
1855
|
-
if (d < best) {
|
|
1856
|
-
best = d;
|
|
1857
|
-
bestY = rowLevels[i].y;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return bestY;
|
|
1861
|
-
} : (x, y) => rake ? levelFor(rake.depthAt(x, y)) : flatTop;
|
|
1862
|
-
const UP = [0, 1, 0];
|
|
1863
|
-
const normalAt = rowLevels.length >= 2 || flat ? () => UP : (x, y) => {
|
|
1864
|
-
if (!rake) return UP;
|
|
1865
|
-
const d = rake.depthAt(x, y);
|
|
1866
|
-
if (d <= frontU) return UP;
|
|
1867
|
-
const depthM = (d - frontU) * METRES_PER_CHART_UNIT;
|
|
1868
|
-
if (depthM * rakeTan >= MAX_TIER_RISE_M) return UP;
|
|
1869
|
-
if (geo.height + depthM * rakeTan <= baseFloor) return UP;
|
|
1870
|
-
const [gx, gy] = rake.gradientAt(x, y);
|
|
1871
|
-
if (gx === 0 && gy === 0) return UP;
|
|
1872
|
-
const inv = 1 / Math.hypot(rakeTan, 1);
|
|
1873
|
-
return [-gx * rakeTan * inv, inv, -gy * rakeTan * inv];
|
|
1874
|
-
};
|
|
1875
|
-
if (!flat) {
|
|
1876
|
-
for (const r of structure.rows) {
|
|
1877
|
-
const y = levelForBlockDepth(r.blockDepth);
|
|
1878
|
-
for (const si of r.seatIndices) seatRowLevel[si] = y;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
for (const r of structure.rows) {
|
|
1882
|
-
const gaps = [];
|
|
1883
|
-
for (let k = 1; k < r.pts.length; k++) {
|
|
1884
|
-
const d = Math.hypot(r.pts[k].x - r.pts[k - 1].x, r.pts[k].y - r.pts[k - 1].y);
|
|
1885
|
-
if (d > 1e-6) gaps.push(d);
|
|
1886
|
-
}
|
|
1887
|
-
gaps.sort((x, y) => x - y);
|
|
1888
|
-
const along = gaps.length ? gaps[Math.floor(gaps.length / 2)] : Infinity;
|
|
1889
|
-
let across = Infinity;
|
|
1890
|
-
const probe = r.pts[Math.floor(r.pts.length / 2)];
|
|
1891
|
-
if (probe) {
|
|
1892
|
-
for (const other of structure.rows) {
|
|
1893
|
-
if (other === r || other.blockId !== r.blockId) continue;
|
|
1894
|
-
const d = distanceToPolyline(other.pts, probe.x, probe.y);
|
|
1895
|
-
if (d > 1e-6 && d < across) across = d;
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
const pitch = Math.min(along, Math.max(across, along * 0.5));
|
|
1899
|
-
if (Number.isFinite(pitch) && pitch > 0) {
|
|
1900
|
-
for (const si of r.seatIndices) seatPitch[si] = pitch;
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
bySection.set(id, { sectionId: id, deckAt, normalAt, flat, bottomY, rowLevels, landingY });
|
|
1904
|
-
}
|
|
1905
|
-
for (let i = 0; i < seats.length; i++) {
|
|
1906
|
-
const ownerId = seatOwner[i];
|
|
1907
|
-
const s = seats[i];
|
|
1908
|
-
if (ownerId) {
|
|
1909
|
-
const own = seatRowLevel[i];
|
|
1910
|
-
seatDeck[i] = (own ?? bySection.get(ownerId).deckAt(s.x, s.y)) + SEAT_CLEARANCE_M;
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
1913
|
-
const eye = s.eyeHeightM;
|
|
1914
|
-
seatDeck[i] = Number.isFinite(eye) ? Math.max(0, eye - SEATED_EYE_HEIGHT_M) : 0;
|
|
1915
|
-
}
|
|
1916
|
-
return {
|
|
1917
|
-
bySection,
|
|
1918
|
-
seatOwner,
|
|
1919
|
-
seatDeckY: (i) => seatDeck[i],
|
|
1920
|
-
seatPitchU: (i) => seatPitch[i]
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
2192
|
// src/view3d/scene/deckBands.ts
|
|
1925
2193
|
var import_polygon_clipping2 = __toESM(require("polygon-clipping"), 1);
|
|
1926
2194
|
var import_earcut2 = __toESM(require("earcut"), 1);
|
|
@@ -1969,6 +2237,68 @@ function extendEnds(pts, by) {
|
|
|
1969
2237
|
});
|
|
1970
2238
|
return out;
|
|
1971
2239
|
}
|
|
2240
|
+
var CORNER_STEP_RAD = Math.PI / 12;
|
|
2241
|
+
function convexHull(pts) {
|
|
2242
|
+
if (pts.length < 3) return [...pts];
|
|
2243
|
+
const s = [...pts].sort((a, b) => a.x - b.x || a.y - b.y);
|
|
2244
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
2245
|
+
const half = (src) => {
|
|
2246
|
+
const out = [];
|
|
2247
|
+
for (const p of src) {
|
|
2248
|
+
while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], p) <= 0) out.pop();
|
|
2249
|
+
out.push(p);
|
|
2250
|
+
}
|
|
2251
|
+
out.pop();
|
|
2252
|
+
return out;
|
|
2253
|
+
};
|
|
2254
|
+
const hull = [...half(s), ...half([...s].reverse())];
|
|
2255
|
+
return hull.length >= 3 ? hull : [...pts];
|
|
2256
|
+
}
|
|
2257
|
+
function seatClusterPatch(pts, pad) {
|
|
2258
|
+
if (!pts.length || !(pad > 0)) return [];
|
|
2259
|
+
const hull = convexHull(pts);
|
|
2260
|
+
const arc = (v, from, to, out2) => {
|
|
2261
|
+
let sweep = to - from;
|
|
2262
|
+
while (sweep < 0) sweep += Math.PI * 2;
|
|
2263
|
+
while (sweep > Math.PI * 2) sweep -= Math.PI * 2;
|
|
2264
|
+
const steps = Math.max(1, Math.ceil(sweep / CORNER_STEP_RAD));
|
|
2265
|
+
const r = pad / Math.cos(sweep / steps / 2);
|
|
2266
|
+
for (let k = 0; k <= steps; k++) {
|
|
2267
|
+
const a = from + sweep * k / steps;
|
|
2268
|
+
out2.push({ x: v.x + Math.cos(a) * r, y: v.y + Math.sin(a) * r });
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
if (hull.length < 3) {
|
|
2272
|
+
let cx = 0, cy = 0;
|
|
2273
|
+
for (const p of hull) {
|
|
2274
|
+
cx += p.x;
|
|
2275
|
+
cy += p.y;
|
|
2276
|
+
}
|
|
2277
|
+
cx /= hull.length || 1;
|
|
2278
|
+
cy /= hull.length || 1;
|
|
2279
|
+
let far = 0;
|
|
2280
|
+
for (const p of hull) far = Math.max(far, Math.hypot(p.x - cx, p.y - cy));
|
|
2281
|
+
const out2 = [];
|
|
2282
|
+
const steps = Math.max(3, Math.ceil(Math.PI * 2 / CORNER_STEP_RAD));
|
|
2283
|
+
const r = (far + pad) / Math.cos(Math.PI / steps);
|
|
2284
|
+
for (let k = 0; k < steps; k++) {
|
|
2285
|
+
const a = k / steps * Math.PI * 2;
|
|
2286
|
+
out2.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r });
|
|
2287
|
+
}
|
|
2288
|
+
return out2;
|
|
2289
|
+
}
|
|
2290
|
+
const n = hull.length;
|
|
2291
|
+
const edgeAngle = [];
|
|
2292
|
+
for (let i = 0; i < n; i++) {
|
|
2293
|
+
const a = hull[i], b = hull[(i + 1) % n];
|
|
2294
|
+
edgeAngle.push(Math.atan2(-(b.x - a.x), b.y - a.y));
|
|
2295
|
+
}
|
|
2296
|
+
const out = [];
|
|
2297
|
+
for (let i = 0; i < n; i++) {
|
|
2298
|
+
arc(hull[i], edgeAngle[(i + n - 1) % n], edgeAngle[i], out);
|
|
2299
|
+
}
|
|
2300
|
+
return dedupeAdjacent(out);
|
|
2301
|
+
}
|
|
1972
2302
|
function distToPolyline(pts, x, y) {
|
|
1973
2303
|
if (pts.length === 1) return Math.hypot(x - pts[0].x, y - pts[0].y);
|
|
1974
2304
|
let best = Infinity;
|
|
@@ -2099,7 +2429,14 @@ function deckFootprints(rows, focal, shared) {
|
|
|
2099
2429
|
else byBlock.set(rows[i].blockId, [i]);
|
|
2100
2430
|
}
|
|
2101
2431
|
const out = [];
|
|
2102
|
-
for (const [,
|
|
2432
|
+
for (const [, allIndices] of byBlock) {
|
|
2433
|
+
const indices = [];
|
|
2434
|
+
for (const i of allIndices) {
|
|
2435
|
+
const patch = rows[i].patch;
|
|
2436
|
+
if (patch && patch.length >= 3) out.push({ outline: [...patch], holes: [], topY: rows[i].y });
|
|
2437
|
+
else indices.push(i);
|
|
2438
|
+
}
|
|
2439
|
+
if (!indices.length) continue;
|
|
2103
2440
|
indices.sort((a, b) => rows[a].depth - rows[b].depth);
|
|
2104
2441
|
const ribbons = indices.map((i) => ribbonOf(rows, i, nbrs, focal));
|
|
2105
2442
|
const usable = ribbons.filter((r) => r !== null);
|
|
@@ -2227,15 +2564,18 @@ var ClipTest = class {
|
|
|
2227
2564
|
return false;
|
|
2228
2565
|
}
|
|
2229
2566
|
};
|
|
2230
|
-
function
|
|
2231
|
-
if (
|
|
2567
|
+
function emitClippedPoly(builder, clipRing, clipTest, poly, y, color) {
|
|
2568
|
+
if (poly.length < 3) return;
|
|
2569
|
+
if (clipTest.containsAll(poly)) {
|
|
2232
2570
|
const UPF = [0, 1, 0];
|
|
2233
|
-
const a =
|
|
2234
|
-
|
|
2235
|
-
|
|
2571
|
+
const a = poly[0];
|
|
2572
|
+
for (let i = 1; i + 1 < poly.length; i++) {
|
|
2573
|
+
const b = poly[i], c = poly[i + 1];
|
|
2574
|
+
builder.tri([a.x * M, y, a.y * M], [b.x * M, y, b.y * M], [c.x * M, y, c.y * M], UPF, color);
|
|
2575
|
+
}
|
|
2236
2576
|
return;
|
|
2237
2577
|
}
|
|
2238
|
-
const ring =
|
|
2578
|
+
const ring = poly.map((p) => [p.x, p.y]);
|
|
2239
2579
|
ring.push(ring[0]);
|
|
2240
2580
|
let pieces;
|
|
2241
2581
|
try {
|
|
@@ -2244,9 +2584,9 @@ function emitClippedQuad(builder, clipRing, clipTest, quad, y, color) {
|
|
|
2244
2584
|
return;
|
|
2245
2585
|
}
|
|
2246
2586
|
const UP = [0, 1, 0];
|
|
2247
|
-
for (const
|
|
2248
|
-
if (!
|
|
2249
|
-
const outer =
|
|
2587
|
+
for (const poly2 of pieces) {
|
|
2588
|
+
if (!poly2.length || poly2[0].length < 4) continue;
|
|
2589
|
+
const outer = poly2[0];
|
|
2250
2590
|
const flat = [];
|
|
2251
2591
|
const pts = [];
|
|
2252
2592
|
for (let i = 0; i < outer.length - 1; i++) {
|
|
@@ -2275,6 +2615,46 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2275
2615
|
const clipTest = clip && clip.length >= 3 ? new ClipTest([[...clip]]) : null;
|
|
2276
2616
|
for (let i = 0; i < rows.length; i++) {
|
|
2277
2617
|
const row = rows[i];
|
|
2618
|
+
if (row.patch && row.patch.length >= 3) {
|
|
2619
|
+
const patch = [...row.patch];
|
|
2620
|
+
const belowY2 = nbrs[i].belowY ?? landingY;
|
|
2621
|
+
if (clipRing && clipTest) emitClippedPoly(builder, clipRing, clipTest, patch, row.y, colors.tread);
|
|
2622
|
+
else {
|
|
2623
|
+
const a = patch[0];
|
|
2624
|
+
for (let k = 1; k + 1 < patch.length; k++) {
|
|
2625
|
+
const b = patch[k], c = patch[k + 1];
|
|
2626
|
+
builder.tri([a.x * M, row.y, a.y * M], [b.x * M, row.y, b.y * M], [c.x * M, row.y, c.y * M], UP, colors.tread);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (row.y > belowY2 + MIN_RISER_M) {
|
|
2630
|
+
let cx = 0, cy = 0;
|
|
2631
|
+
for (const p of patch) {
|
|
2632
|
+
cx += p.x;
|
|
2633
|
+
cy += p.y;
|
|
2634
|
+
}
|
|
2635
|
+
cx /= patch.length;
|
|
2636
|
+
cy /= patch.length;
|
|
2637
|
+
for (let k = 0; k < patch.length; k++) {
|
|
2638
|
+
const p = patch[k], q = patch[(k + 1) % patch.length];
|
|
2639
|
+
const dx = q.x - p.x, dy = q.y - p.y;
|
|
2640
|
+
const len = Math.hypot(dx, dy);
|
|
2641
|
+
if (len < 1e-6) continue;
|
|
2642
|
+
let nx = dy / len, ny = -dx / len;
|
|
2643
|
+
if ((p.x - cx) * nx + (p.y - cy) * ny < 0) {
|
|
2644
|
+
nx = -nx;
|
|
2645
|
+
ny = -ny;
|
|
2646
|
+
}
|
|
2647
|
+
const rn = [nx, 0, ny];
|
|
2648
|
+
const pt = [p.x * M, row.y, p.y * M];
|
|
2649
|
+
const qt = [q.x * M, row.y, q.y * M];
|
|
2650
|
+
const pb = [p.x * M, belowY2, p.y * M];
|
|
2651
|
+
const qb = [q.x * M, belowY2, q.y * M];
|
|
2652
|
+
builder.tri(pt, qt, qb, rn, colors.riser);
|
|
2653
|
+
builder.tri(pt, qb, pb, rn, colors.riser);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2278
2658
|
const rib = ribbonOf(rows, i, nbrs, focal);
|
|
2279
2659
|
if (!rib) continue;
|
|
2280
2660
|
const { pts, nrm, front, back } = rib;
|
|
@@ -2289,7 +2669,7 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2289
2669
|
const pB = [(p.x + np[0] * back) * M, row.y, (p.y + np[1] * back) * M];
|
|
2290
2670
|
const qB = [(q.x + nq[0] * back) * M, row.y, (q.y + nq[1] * back) * M];
|
|
2291
2671
|
if (clipRing && clipTest) {
|
|
2292
|
-
|
|
2672
|
+
emitClippedPoly(builder, clipRing, clipTest, [
|
|
2293
2673
|
{ x: p.x - np[0] * front, y: p.y - np[1] * front },
|
|
2294
2674
|
{ x: q.x - nq[0] * front, y: q.y - nq[1] * front },
|
|
2295
2675
|
{ x: q.x + nq[0] * back, y: q.y + nq[1] * back },
|
|
@@ -2310,6 +2690,208 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2310
2690
|
}
|
|
2311
2691
|
}
|
|
2312
2692
|
|
|
2693
|
+
// src/view3d/scene/surface.ts
|
|
2694
|
+
var FLAT_SLAB_TOP_M = 0.05;
|
|
2695
|
+
var CAP_MAX_ERROR_M = 0.05;
|
|
2696
|
+
var SEAT_CLEARANCE_M = 0.15;
|
|
2697
|
+
var SEAT_OWNERSHIP_PAD_U = SEAT_DOT_RADIUS_M * 1.5 * CHART_UNITS_PER_METRE;
|
|
2698
|
+
function bboxOf(pts) {
|
|
2699
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2700
|
+
for (const p of pts) {
|
|
2701
|
+
if (p.x < minX) minX = p.x;
|
|
2702
|
+
if (p.y < minY) minY = p.y;
|
|
2703
|
+
if (p.x > maxX) maxX = p.x;
|
|
2704
|
+
if (p.y > maxY) maxY = p.y;
|
|
2705
|
+
}
|
|
2706
|
+
return { minX, minY, maxX, maxY };
|
|
2707
|
+
}
|
|
2708
|
+
function pointInRing2(ring, x, y) {
|
|
2709
|
+
let inside = false;
|
|
2710
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
2711
|
+
const a = ring[i], b = ring[j];
|
|
2712
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;
|
|
2713
|
+
}
|
|
2714
|
+
return inside;
|
|
2715
|
+
}
|
|
2716
|
+
var MAX_TIER_RISE_M = 25;
|
|
2717
|
+
function buildVenueSurfaces(units, seats) {
|
|
2718
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
2719
|
+
const seatOwner = new Array(seats.length).fill(null);
|
|
2720
|
+
const seatDeck = new Float64Array(seats.length);
|
|
2721
|
+
const seatRowLevel = new Array(seats.length).fill(void 0);
|
|
2722
|
+
const seatPitch = new Array(seats.length).fill(void 0);
|
|
2723
|
+
const acc = /* @__PURE__ */ new Map();
|
|
2724
|
+
const boxes = [];
|
|
2725
|
+
const tableRowIds = /* @__PURE__ */ new Set();
|
|
2726
|
+
for (const unit of units) {
|
|
2727
|
+
for (const o of unit.objects) {
|
|
2728
|
+
if (o.type === "table") tableRowIds.add(o.id);
|
|
2729
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
2730
|
+
const owned = outsetRing(o.outline, SEAT_OWNERSHIP_PAD_U);
|
|
2731
|
+
acc.set(o.id, { section: o, unit, frontU: Infinity, hasSeats: false, rows: /* @__PURE__ */ new Map(), seatIndices: [] });
|
|
2732
|
+
boxes.push({ id: o.id, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
for (let i = 0; i < seats.length; i++) {
|
|
2736
|
+
const s = seats[i];
|
|
2737
|
+
for (const b of boxes) {
|
|
2738
|
+
if (s.x < b.box.minX || s.x > b.box.maxX || s.y < b.box.minY || s.y > b.box.maxY) continue;
|
|
2739
|
+
if (!pointInPolygonWithHoles({ x: s.x, y: s.y }, b.outline, b.section.holes)) continue;
|
|
2740
|
+
seatOwner[i] = b.id;
|
|
2741
|
+
const a = acc.get(b.id);
|
|
2742
|
+
a.hasSeats = true;
|
|
2743
|
+
const f = s.focalPoint ?? b.unit.focal;
|
|
2744
|
+
const d = Math.hypot(s.x - f.x, s.y - f.y);
|
|
2745
|
+
if (d < a.frontU) a.frontU = d;
|
|
2746
|
+
a.seatIndices.push(i);
|
|
2747
|
+
const rowKey = s.rowId || `__seat-${i}`;
|
|
2748
|
+
const arr = a.rows.get(rowKey);
|
|
2749
|
+
if (arr) arr.push({ x: s.x, y: s.y });
|
|
2750
|
+
else a.rows.set(rowKey, [{ x: s.x, y: s.y }]);
|
|
2751
|
+
break;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
for (const [id, a] of acc) {
|
|
2755
|
+
const geo = sectionGeometry(a.section, { floorBaseHeightM: a.unit.baseHeightM });
|
|
2756
|
+
const bottomY = a.unit.baseHeightM;
|
|
2757
|
+
const rakeTan = geo.rake > 0 ? Math.tan(geo.rake * Math.PI / 180) : 0;
|
|
2758
|
+
const inferredFlat = geo.rake <= 0.01 && geo.height <= bottomY + 1e-3;
|
|
2759
|
+
const kind = a.section.surfaceKind;
|
|
2760
|
+
const flat = kind === "flat" ? true : kind === "rakedRows" ? geo.rake > 0.01 ? false : inferredFlat : inferredFlat;
|
|
2761
|
+
const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
|
|
2762
|
+
const needsRake = structure.rows.length < 2;
|
|
2763
|
+
const rake = needsRake ? buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal) : null;
|
|
2764
|
+
let frontU = Infinity;
|
|
2765
|
+
if (rake) {
|
|
2766
|
+
if (a.hasSeats) {
|
|
2767
|
+
for (const pts of a.rows.values()) {
|
|
2768
|
+
for (const p of pts) {
|
|
2769
|
+
const d = rake.depthAt(p.x, p.y);
|
|
2770
|
+
if (d < frontU) frontU = d;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (!a.hasSeats || !Number.isFinite(frontU)) {
|
|
2775
|
+
frontU = Infinity;
|
|
2776
|
+
for (const p of a.section.outline) {
|
|
2777
|
+
const d = rake.depthAt(p.x, p.y);
|
|
2778
|
+
if (d < frontU) frontU = d;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const baseFloor = bottomY + FLAT_SLAB_TOP_M;
|
|
2783
|
+
const flatTop = Math.max(baseFloor, geo.height);
|
|
2784
|
+
const levelFor = (depthU) => {
|
|
2785
|
+
const depthM = Math.max(0, depthU - frontU) * METRES_PER_CHART_UNIT;
|
|
2786
|
+
const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
|
|
2787
|
+
return Math.max(baseFloor, geo.height + rise);
|
|
2788
|
+
};
|
|
2789
|
+
const levelForBlockDepth = (blockDepthU) => {
|
|
2790
|
+
const depthM = Math.max(0, blockDepthU) * METRES_PER_CHART_UNIT;
|
|
2791
|
+
const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
|
|
2792
|
+
return Math.max(baseFloor, geo.height + rise);
|
|
2793
|
+
};
|
|
2794
|
+
const rowLevels = flat ? [] : structure.rows.map((r) => ({
|
|
2795
|
+
pts: r.pts,
|
|
2796
|
+
y: levelForBlockDepth(r.blockDepth),
|
|
2797
|
+
depth: r.blockDepth,
|
|
2798
|
+
blockId: r.blockId,
|
|
2799
|
+
...tableRowIds.has(r.id) ? { patch: seatClusterPatch(r.pts, MIN_REACH_U) } : {}
|
|
2800
|
+
}));
|
|
2801
|
+
const landingY = rowLevels.length ? rowLevels[0].y : flatTop;
|
|
2802
|
+
const rowBounds = rowLevels.map((r) => {
|
|
2803
|
+
const ext = r.patch && r.patch.length >= 3 ? r.patch : r.pts;
|
|
2804
|
+
let cx = 0, cy = 0;
|
|
2805
|
+
for (const p of ext) {
|
|
2806
|
+
cx += p.x;
|
|
2807
|
+
cy += p.y;
|
|
2808
|
+
}
|
|
2809
|
+
const n = ext.length || 1;
|
|
2810
|
+
cx /= n;
|
|
2811
|
+
cy /= n;
|
|
2812
|
+
let rad = 0;
|
|
2813
|
+
for (const p of ext) {
|
|
2814
|
+
const d = Math.hypot(p.x - cx, p.y - cy);
|
|
2815
|
+
if (d > rad) rad = d;
|
|
2816
|
+
}
|
|
2817
|
+
return { cx, cy, rad };
|
|
2818
|
+
});
|
|
2819
|
+
const deckAt = flat ? () => flatTop : rowLevels.length >= 2 ? (x, y) => {
|
|
2820
|
+
let best = Infinity, bestY = landingY;
|
|
2821
|
+
for (let i = 0; i < rowLevels.length; i++) {
|
|
2822
|
+
const b = rowBounds[i];
|
|
2823
|
+
if (Math.hypot(x - b.cx, y - b.cy) - b.rad >= best) continue;
|
|
2824
|
+
const patch = rowLevels[i].patch;
|
|
2825
|
+
const d = patch && patch.length >= 3 ? pointInRing2(patch, x, y) ? 0 : distanceToPolyline(patch, x, y) : distanceToPolyline(rowLevels[i].pts, x, y);
|
|
2826
|
+
if (d < best) {
|
|
2827
|
+
best = d;
|
|
2828
|
+
bestY = rowLevels[i].y;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return bestY;
|
|
2832
|
+
} : (x, y) => rake ? levelFor(rake.depthAt(x, y)) : flatTop;
|
|
2833
|
+
const UP = [0, 1, 0];
|
|
2834
|
+
const normalAt = rowLevels.length >= 2 || flat ? () => UP : (x, y) => {
|
|
2835
|
+
if (!rake) return UP;
|
|
2836
|
+
const d = rake.depthAt(x, y);
|
|
2837
|
+
if (d <= frontU) return UP;
|
|
2838
|
+
const depthM = (d - frontU) * METRES_PER_CHART_UNIT;
|
|
2839
|
+
if (depthM * rakeTan >= MAX_TIER_RISE_M) return UP;
|
|
2840
|
+
if (geo.height + depthM * rakeTan <= baseFloor) return UP;
|
|
2841
|
+
const [gx, gy] = rake.gradientAt(x, y);
|
|
2842
|
+
if (gx === 0 && gy === 0) return UP;
|
|
2843
|
+
const inv = 1 / Math.hypot(rakeTan, 1);
|
|
2844
|
+
return [-gx * rakeTan * inv, inv, -gy * rakeTan * inv];
|
|
2845
|
+
};
|
|
2846
|
+
if (!flat) {
|
|
2847
|
+
for (const r of structure.rows) {
|
|
2848
|
+
const y = levelForBlockDepth(r.blockDepth);
|
|
2849
|
+
for (const si of r.seatIndices) seatRowLevel[si] = y;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
for (const r of structure.rows) {
|
|
2853
|
+
const gaps = [];
|
|
2854
|
+
for (let k = 1; k < r.pts.length; k++) {
|
|
2855
|
+
const d = Math.hypot(r.pts[k].x - r.pts[k - 1].x, r.pts[k].y - r.pts[k - 1].y);
|
|
2856
|
+
if (d > 1e-6) gaps.push(d);
|
|
2857
|
+
}
|
|
2858
|
+
gaps.sort((x, y) => x - y);
|
|
2859
|
+
const along = gaps.length ? gaps[Math.floor(gaps.length / 2)] : Infinity;
|
|
2860
|
+
let across = Infinity;
|
|
2861
|
+
const probe = r.pts[Math.floor(r.pts.length / 2)];
|
|
2862
|
+
if (probe) {
|
|
2863
|
+
for (const other of structure.rows) {
|
|
2864
|
+
if (other === r || other.blockId !== r.blockId) continue;
|
|
2865
|
+
const d = distanceToPolyline(other.pts, probe.x, probe.y);
|
|
2866
|
+
if (d > 1e-6 && d < across) across = d;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
const pitch = Math.min(along, Math.max(across, along * 0.5));
|
|
2870
|
+
if (Number.isFinite(pitch) && pitch > 0) {
|
|
2871
|
+
for (const si of r.seatIndices) seatPitch[si] = pitch;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
bySection.set(id, { sectionId: id, deckAt, normalAt, flat, bottomY, rowLevels, landingY });
|
|
2875
|
+
}
|
|
2876
|
+
for (let i = 0; i < seats.length; i++) {
|
|
2877
|
+
const ownerId = seatOwner[i];
|
|
2878
|
+
const s = seats[i];
|
|
2879
|
+
if (ownerId) {
|
|
2880
|
+
const own = seatRowLevel[i];
|
|
2881
|
+
seatDeck[i] = (own ?? bySection.get(ownerId).deckAt(s.x, s.y)) + SEAT_CLEARANCE_M;
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const eye = s.eyeHeightM;
|
|
2885
|
+
seatDeck[i] = Number.isFinite(eye) ? Math.max(0, eye - SEATED_EYE_HEIGHT_M) : 0;
|
|
2886
|
+
}
|
|
2887
|
+
return {
|
|
2888
|
+
bySection,
|
|
2889
|
+
seatOwner,
|
|
2890
|
+
seatDeckY: (i) => seatDeck[i],
|
|
2891
|
+
seatPitchU: (i) => seatPitch[i]
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2313
2895
|
// src/view3d/scene/sceneModel.ts
|
|
2314
2896
|
function floorUnits(doc) {
|
|
2315
2897
|
if (doc.floors?.length) {
|
|
@@ -2475,22 +3057,39 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2475
3057
|
}
|
|
2476
3058
|
const maxErr = surface.flat ? Infinity : CAP_MAX_ERROR_M;
|
|
2477
3059
|
const topN = (p) => surface.normalAt(p.x, p.y);
|
|
2478
|
-
|
|
3060
|
+
const level = surface.flat ? surface.landingY : void 0;
|
|
3061
|
+
for (const ring of claimed.subtract(outline, level)) {
|
|
2479
3062
|
extrudePrism(builder, ring, section.holes, topY, bottomY, colTop, S.tierWall, AO, maxErr, topN);
|
|
2480
3063
|
}
|
|
2481
3064
|
}
|
|
3065
|
+
function coplanarLevels(a, b) {
|
|
3066
|
+
if (a === void 0 || b === void 0) return true;
|
|
3067
|
+
return Math.abs(a - b) < 1e-3;
|
|
3068
|
+
}
|
|
2482
3069
|
var ClaimedArea = class {
|
|
2483
3070
|
constructor() {
|
|
2484
3071
|
this.rings = [];
|
|
2485
3072
|
this.boxes = [];
|
|
3073
|
+
/** Constant deck height of each claim, or undefined when it is not level. */
|
|
3074
|
+
this.levels = [];
|
|
2486
3075
|
}
|
|
2487
|
-
/**
|
|
2488
|
-
|
|
3076
|
+
/**
|
|
3077
|
+
* `ring` minus everything claimed so far AT THE SAME HEIGHT; then claim it.
|
|
3078
|
+
*
|
|
3079
|
+
* `level` is the claim's constant deck height when it has one. Two decks only
|
|
3080
|
+
* z-fight when they are coplanar, so only a coplanar claim may take ground
|
|
3081
|
+
* away — an elevated box hanging over a ground-level section overlaps it in
|
|
3082
|
+
* plan and must still draw its whole floor, or the box loses the part of its
|
|
3083
|
+
* deck that shares a footprint with whatever is underneath it. Passing
|
|
3084
|
+
* undefined (a surface with no single height) keeps the original
|
|
3085
|
+
* clip-against-everything behaviour.
|
|
3086
|
+
*/
|
|
3087
|
+
subtract(ring, level) {
|
|
2489
3088
|
const closed = ring.map((p) => [p.x, p.y]);
|
|
2490
3089
|
if (closed.length < 3) return [];
|
|
2491
3090
|
closed.push(closed[0]);
|
|
2492
3091
|
const box = bboxOfRing(ring);
|
|
2493
|
-
const overlapping = this.rings.filter((_, i) => boxesOverlap(box, this.boxes[i]));
|
|
3092
|
+
const overlapping = this.rings.filter((_, i) => boxesOverlap(box, this.boxes[i]) && coplanarLevels(level, this.levels[i]));
|
|
2494
3093
|
let pieces = [[closed]];
|
|
2495
3094
|
if (overlapping.length) {
|
|
2496
3095
|
try {
|
|
@@ -2502,6 +3101,7 @@ var ClaimedArea = class {
|
|
|
2502
3101
|
}
|
|
2503
3102
|
this.rings.push([closed]);
|
|
2504
3103
|
this.boxes.push(box);
|
|
3104
|
+
this.levels.push(level);
|
|
2505
3105
|
const out = [];
|
|
2506
3106
|
for (const poly of pieces) {
|
|
2507
3107
|
if (!poly.length) continue;
|
|
@@ -2594,11 +3194,27 @@ function shapePolygon(shape) {
|
|
|
2594
3194
|
}
|
|
2595
3195
|
return null;
|
|
2596
3196
|
}
|
|
2597
|
-
function buildShape(builder, shape, base, S) {
|
|
3197
|
+
function buildShape(builder, shape, base, S, stages) {
|
|
2598
3198
|
const poly = shapePolygon(shape);
|
|
2599
3199
|
if (!poly) return;
|
|
2600
3200
|
const isStage = shape.role === "stage";
|
|
2601
3201
|
const height = isStage ? base + 1 : base + 0.25;
|
|
3202
|
+
if (isStage && stages) {
|
|
3203
|
+
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
3204
|
+
for (const pt of poly) {
|
|
3205
|
+
if (pt.x < minX) minX = pt.x;
|
|
3206
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
3207
|
+
if (pt.y < minZ) minZ = pt.y;
|
|
3208
|
+
if (pt.y > maxZ) maxZ = pt.y;
|
|
3209
|
+
}
|
|
3210
|
+
stages.push({
|
|
3211
|
+
cx: (minX + maxX) / 2 * M,
|
|
3212
|
+
cz: (minZ + maxZ) / 2 * M,
|
|
3213
|
+
y: height,
|
|
3214
|
+
halfX: Math.max((maxX - minX) / 2 * M, 0.5),
|
|
3215
|
+
halfZ: Math.max((maxZ - minZ) / 2 * M, 0.5)
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
2602
3218
|
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
2603
3219
|
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
2604
3220
|
extrudePrism(builder, poly, void 0, () => height, base, colTop, colWall, AO);
|
|
@@ -2708,6 +3324,7 @@ function buildSceneModel(input) {
|
|
|
2708
3324
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
2709
3325
|
const catColor = /* @__PURE__ */ new Map();
|
|
2710
3326
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
3327
|
+
const stageBounds = [];
|
|
2711
3328
|
const surfaces = buildVenueSurfaces(units, seats);
|
|
2712
3329
|
const seatFloor = new Float32Array(seats.length);
|
|
2713
3330
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
@@ -2717,7 +3334,7 @@ function buildSceneModel(input) {
|
|
|
2717
3334
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
2718
3335
|
for (const o of unit.objects) {
|
|
2719
3336
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
2720
|
-
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
|
|
3337
|
+
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S, stageBounds);
|
|
2721
3338
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2722
3339
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2723
3340
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
@@ -2726,7 +3343,7 @@ function buildSceneModel(input) {
|
|
|
2726
3343
|
}
|
|
2727
3344
|
const focal = doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
2728
3345
|
const solids = mergeMeshData([builder.build()]);
|
|
2729
|
-
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor);
|
|
3346
|
+
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor, catColor);
|
|
2730
3347
|
const zoneDefs = doc.zones ?? [];
|
|
2731
3348
|
const zones = [];
|
|
2732
3349
|
if (zoneDefs.length) {
|
|
@@ -2957,6 +3574,15 @@ function buildSceneModel(input) {
|
|
|
2957
3574
|
});
|
|
2958
3575
|
}
|
|
2959
3576
|
}
|
|
3577
|
+
seatData.iYaw = computeSeatYaw(
|
|
3578
|
+
seatData.iPosition,
|
|
3579
|
+
seats.length,
|
|
3580
|
+
(i) => seats[i].rowId,
|
|
3581
|
+
(i) => {
|
|
3582
|
+
const f = units[seatFloor[i]]?.focal ?? focal;
|
|
3583
|
+
return [f.x * M, f.y * M];
|
|
3584
|
+
}
|
|
3585
|
+
);
|
|
2960
3586
|
const cx = (fp.minX + fp.maxX) / 2 * M;
|
|
2961
3587
|
const cz = (fp.minY + fp.maxY) / 2 * M;
|
|
2962
3588
|
const radius = 0.5 * Math.hypot((fp.maxX - fp.minX) * M, (fp.maxY - fp.minY) * M) || 10;
|
|
@@ -2970,6 +3596,7 @@ function buildSceneModel(input) {
|
|
|
2970
3596
|
// Look-at target ~1.5 m up so a seated camera aims slightly down at the stage.
|
|
2971
3597
|
focalWorld: [focal.x * M, 1.5, focal.y * M],
|
|
2972
3598
|
zones,
|
|
3599
|
+
stages: stageBounds,
|
|
2973
3600
|
sections,
|
|
2974
3601
|
labels,
|
|
2975
3602
|
floors
|
|
@@ -2994,7 +3621,11 @@ var KIND_STYLE = {
|
|
|
2994
3621
|
var DENSE_KINDS = /* @__PURE__ */ new Set(["row", "seat"]);
|
|
2995
3622
|
var DENSE_SEPARATION = {
|
|
2996
3623
|
row: { x: 62, y: 16 },
|
|
2997
|
-
|
|
3624
|
+
// Widened from 24: at close range seat labels stopped overlapping at all, so
|
|
3625
|
+
// the separation had no work left to do and the budget was carrying the whole
|
|
3626
|
+
// load. A wider box means the few labels that ARE kept are spread across the
|
|
3627
|
+
// seating instead of clustering into one stack.
|
|
3628
|
+
seat: { x: 46, y: 18 }
|
|
2998
3629
|
};
|
|
2999
3630
|
var LabelOverlay = class {
|
|
3000
3631
|
constructor(container, opts = {}) {
|
|
@@ -3011,6 +3642,20 @@ var LabelOverlay = class {
|
|
|
3011
3642
|
if (opts.fontFamily) s.fontFamily = opts.fontFamily;
|
|
3012
3643
|
container.appendChild(this.root);
|
|
3013
3644
|
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Hide or show the whole overlay without disturbing which labels exist.
|
|
3647
|
+
*
|
|
3648
|
+
* The in-scene panorama sphere is GL, so it draws BEHIND every DOM label —
|
|
3649
|
+
* row and seat labels floated on top of a 360 photo until this existed. The
|
|
3650
|
+
* old DOM panorama never needed it because its opaque div covered them.
|
|
3651
|
+
*
|
|
3652
|
+
* Deliberately not `setLabels([])`: that destroys the nodes and the declutter
|
|
3653
|
+
* state, so closing the panorama would rebuild and re-rank the whole overlay
|
|
3654
|
+
* and flash. Visibility is a view concern, not a data one.
|
|
3655
|
+
*/
|
|
3656
|
+
setVisible(visible) {
|
|
3657
|
+
this.root.style.visibility = visible ? "" : "hidden";
|
|
3658
|
+
}
|
|
3014
3659
|
setLabels(labels) {
|
|
3015
3660
|
this.labels = labels;
|
|
3016
3661
|
for (const [id, node] of this.nodes) {
|
|
@@ -3025,7 +3670,7 @@ var LabelOverlay = class {
|
|
|
3025
3670
|
*
|
|
3026
3671
|
* `viewProjection` is column-major, as OGL supplies it.
|
|
3027
3672
|
*/
|
|
3028
|
-
update(viewProjection, width, height, cameraDistance, venueRadius) {
|
|
3673
|
+
update(viewProjection, width, height, cameraDistance, venueRadius, cameraWorld) {
|
|
3029
3674
|
if (!this.labels.length) return;
|
|
3030
3675
|
const kinds = visibleLabelKinds(cameraDistance, venueRadius);
|
|
3031
3676
|
const candidates = [];
|
|
@@ -3033,15 +3678,24 @@ var LabelOverlay = class {
|
|
|
3033
3678
|
if (!kinds.has(label.kind)) continue;
|
|
3034
3679
|
const screen = projectToScreen(viewProjection, label.anchor, width, height);
|
|
3035
3680
|
if (!screen.visible) continue;
|
|
3036
|
-
|
|
3681
|
+
const world = cameraWorld ? Math.hypot(
|
|
3682
|
+
label.anchor[0] - cameraWorld[0],
|
|
3683
|
+
label.anchor[1] - cameraWorld[1],
|
|
3684
|
+
label.anchor[2] - cameraWorld[2]
|
|
3685
|
+
) : 1;
|
|
3686
|
+
candidates.push({ label, screen, focus: focusScore(screen, world, width, height) });
|
|
3037
3687
|
}
|
|
3038
3688
|
const structure = candidates.filter((c) => !DENSE_KINDS.has(c.label.kind));
|
|
3039
3689
|
const kept = [
|
|
3040
3690
|
...cullOverlapping(structure, SEPARATION_X_PX, SEPARATION_Y_PX),
|
|
3041
|
-
|
|
3691
|
+
// The dense rungs are BUDGETED, not merely deduplicated — see
|
|
3692
|
+
// DENSE_LABEL_BUDGET for why the overlap test alone gets worse the closer
|
|
3693
|
+
// the camera gets.
|
|
3694
|
+
...["row", "seat"].flatMap((kind) => pickDenseLabels(
|
|
3042
3695
|
candidates.filter((c) => c.label.kind === kind),
|
|
3043
3696
|
DENSE_SEPARATION[kind].x,
|
|
3044
|
-
DENSE_SEPARATION[kind].y
|
|
3697
|
+
DENSE_SEPARATION[kind].y,
|
|
3698
|
+
DENSE_LABEL_BUDGET[kind]
|
|
3045
3699
|
))
|
|
3046
3700
|
];
|
|
3047
3701
|
const keptIds = new Set(kept.map((k) => k.label.id));
|
|
@@ -3090,6 +3744,13 @@ var import_ogl4 = require("ogl");
|
|
|
3090
3744
|
|
|
3091
3745
|
// src/view3d/scene/materials.ts
|
|
3092
3746
|
var import_ogl3 = require("ogl");
|
|
3747
|
+
var CHAIR_WEIGHT_GLSL = (
|
|
3748
|
+
/* glsl */
|
|
3749
|
+
`
|
|
3750
|
+
float chairWeight(float depth) {
|
|
3751
|
+
return 1.0 - smoothstep(uChairFull, uChairNone, depth);
|
|
3752
|
+
}`
|
|
3753
|
+
);
|
|
3093
3754
|
var SOLID_VERT = (
|
|
3094
3755
|
/* glsl */
|
|
3095
3756
|
`#version 300 es
|
|
@@ -3169,14 +3830,23 @@ uniform float uSeatScale;
|
|
|
3169
3830
|
uniform float uMinPixels;
|
|
3170
3831
|
uniform float uPixelToWorld; // (2*tan(fovY/2)) / viewportHeightPx
|
|
3171
3832
|
uniform float uFocusFloor; // -1 = show every floor
|
|
3833
|
+
uniform float uChairFull; // view depth at which the chair mesh is full size
|
|
3834
|
+
uniform float uChairNone; // ...and at which it has scaled away entirely
|
|
3172
3835
|
out vec2 vUv;
|
|
3173
3836
|
out vec3 vColor;
|
|
3174
3837
|
out float vBudget; // 1 = dot holds its minimum pixel size, <1 = it cannot
|
|
3175
3838
|
out vec3 vRing;
|
|
3176
3839
|
out float vDim;
|
|
3840
|
+
out float vDotWeight; // 1 = the dot IS this seat, 0 = the chair has taken over
|
|
3841
|
+
${CHAIR_WEIGHT_GLSL}
|
|
3177
3842
|
void main() {
|
|
3178
3843
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
3179
3844
|
float depth = max(-mv.z, 0.001);
|
|
3845
|
+
// Hand the seat over to the chair mesh as it comes into range. Derived from
|
|
3846
|
+
// this instance's OWN depth rather than from a global uniform, so a row two
|
|
3847
|
+
// metres away and the far side of the bowl resolve differently in the same
|
|
3848
|
+
// frame \u2014 which is the entire point of a ladder over a switch.
|
|
3849
|
+
vDotWeight = 1.0 - chairWeight(depth);
|
|
3180
3850
|
float minR = uMinPixels * depth * uPixelToWorld; // screen-space floor
|
|
3181
3851
|
// Grow to hold the pixel floor, but never past this seat's own pitch ceiling:
|
|
3182
3852
|
// unbounded growth is what merges neighbouring rows into one mass at range.
|
|
@@ -3210,6 +3880,7 @@ in vec3 vColor;
|
|
|
3210
3880
|
in float vBudget;
|
|
3211
3881
|
in vec3 vRing;
|
|
3212
3882
|
in float vDim;
|
|
3883
|
+
in float vDotWeight;
|
|
3213
3884
|
uniform float uSeatFade; // fade toward tier colour with distance (LOD)
|
|
3214
3885
|
uniform vec3 uFadeColor;
|
|
3215
3886
|
out vec4 fragColor;
|
|
@@ -3234,9 +3905,173 @@ void main() {
|
|
|
3234
3905
|
// Seats on an unfocused floor recede with their structure.
|
|
3235
3906
|
c = mix(c, uFadeColor, vDim * 0.75);
|
|
3236
3907
|
alpha *= mix(1.0, 0.30, vDim);
|
|
3908
|
+
// Yield to the chair. The chair grows out of this exact point, so through the
|
|
3909
|
+
// band the dot is always at least as big as the chair inside it and the seat
|
|
3910
|
+
// never thins out to nothing in between.
|
|
3911
|
+
alpha *= vDotWeight;
|
|
3912
|
+
if (alpha <= 0.0) discard;
|
|
3237
3913
|
fragColor = vec4(c, alpha);
|
|
3238
3914
|
}`
|
|
3239
3915
|
);
|
|
3916
|
+
var CHAIR_VERT = (
|
|
3917
|
+
/* glsl */
|
|
3918
|
+
`#version 300 es
|
|
3919
|
+
precision highp float;
|
|
3920
|
+
in vec3 position; // local: x/z in units of the seat radius, y in METRES
|
|
3921
|
+
in vec3 normal;
|
|
3922
|
+
in float part; // 0 = pedestal, 1 = pad, 2 = back, 3 = body, 4 = head
|
|
3923
|
+
in vec3 iOffset; // per-instance world deck point (identical to the dot's)
|
|
3924
|
+
in vec3 iColor; // per-instance state colour
|
|
3925
|
+
in float iRadius; // per-instance horizontal half-width, world metres
|
|
3926
|
+
in float iYaw; // per-instance facing, radians (local +Z -> facing dir)
|
|
3927
|
+
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3928
|
+
in float iFloor;
|
|
3929
|
+
in float iSeed; // <0 = seat is empty; else per-person hash in [0,1)
|
|
3930
|
+
uniform mat4 modelViewMatrix;
|
|
3931
|
+
uniform mat4 projectionMatrix;
|
|
3932
|
+
uniform float uChairFull;
|
|
3933
|
+
uniform float uChairNone;
|
|
3934
|
+
uniform float uFocusFloor;
|
|
3935
|
+
uniform float uBackRake; // metres of z per metre of rise, above uBackBase
|
|
3936
|
+
uniform float uBackBase; // local height at which the back starts
|
|
3937
|
+
out vec3 vColor;
|
|
3938
|
+
out vec3 vNormalWorld;
|
|
3939
|
+
out vec3 vNormalView;
|
|
3940
|
+
out vec3 vPosView;
|
|
3941
|
+
out float vPart;
|
|
3942
|
+
out float vHeight; // local height in metres, for the vertical occlusion ramp
|
|
3943
|
+
out vec3 vRing;
|
|
3944
|
+
out float vDim;
|
|
3945
|
+
out float vOccupant; // 1 = this vertex belongs to a person, not to the chair
|
|
3946
|
+
out vec3 vOccupantTint;
|
|
3947
|
+
${CHAIR_WEIGHT_GLSL}
|
|
3948
|
+
void main() {
|
|
3949
|
+
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
3950
|
+
float w = chairWeight(max(-anchor.z, 0.001));
|
|
3951
|
+
// 1. Local units -> world metres. Only x/z scale: narrow rows get narrow
|
|
3952
|
+
// chairs, but nobody gets a short one (people are the same height at every
|
|
3953
|
+
// seat pitch).
|
|
3954
|
+
vec3 p = position;
|
|
3955
|
+
// The occupant. Parts 3 and 4 are a person; everything below is furniture.
|
|
3956
|
+
//
|
|
3957
|
+
// One instanced mesh draws both an empty seat and a taken one, because the
|
|
3958
|
+
// alternative \u2014 a second geometry and a second draw call gathered per frame \u2014
|
|
3959
|
+
// would double the near-field cost to show what is already known per instance.
|
|
3960
|
+
// An unoccupied seat collapses its person to a point at the seat, which the
|
|
3961
|
+
// rasteriser discards, exactly as the chair itself collapses at w=0.
|
|
3962
|
+
float occupant = step(2.5, part);
|
|
3963
|
+
float taken = step(0.0, iSeed);
|
|
3964
|
+
vOccupant = occupant;
|
|
3965
|
+
if (occupant > 0.5) {
|
|
3966
|
+
if (taken < 0.5) {
|
|
3967
|
+
p = vec3(0.0); // empty seat: no person
|
|
3968
|
+
} else {
|
|
3969
|
+
// Vary the build so a sold-out row is people rather than a rank of
|
|
3970
|
+
// identical mannequins: +/-6% height and +/-8% width off the hash.
|
|
3971
|
+
p.y *= 0.94 + 0.12 * iSeed;
|
|
3972
|
+
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
p.xz *= iRadius;
|
|
3976
|
+
// 2. Lean the back. Done here rather than in the base mesh so the lean is a
|
|
3977
|
+
// real angle in METRES \u2014 baked into the mesh it would scale with the seat's
|
|
3978
|
+
// width and the same chair would lean 20 degrees on a wide stadium row and
|
|
3979
|
+
// 6 on a tight theatre one.
|
|
3980
|
+
// Only the BACK panel rakes. The old test (part > 1.5) meant "the back", and
|
|
3981
|
+
// now also catches the occupant \u2014 leaning a person by the panel's rule would
|
|
3982
|
+
// translate their head backwards by a rake measured from the panel's base.
|
|
3983
|
+
float rake = (part > 1.5 && part < 2.5) ? max(p.y - uBackBase, 0.0) * uBackRake : 0.0;
|
|
3984
|
+
p.z -= rake;
|
|
3985
|
+
// 3. Scale-in. At w=0 the chair is a point at the seat, under a dot at full
|
|
3986
|
+
// opacity \u2014 which is what makes the handover invisible. sqrt front-loads
|
|
3987
|
+
// the growth so the chair is already near full size while the dot is still
|
|
3988
|
+
// half there; see chairScale() in lod.ts.
|
|
3989
|
+
p *= sqrt(w);
|
|
3990
|
+
float c = cos(iYaw), s = sin(iYaw);
|
|
3991
|
+
vec3 rp = vec3(p.x * c + p.z * s, p.y, -p.x * s + p.z * c);
|
|
3992
|
+
// Normals under the same two transforms, in reverse and inverted-transposed.
|
|
3993
|
+
// The xz scale is non-uniform, so an axis-aligned normal does NOT survive it
|
|
3994
|
+
// unchanged; and the rake is a shear, whose normal transform adds a y term.
|
|
3995
|
+
// Skipping either lights the raked back as though it were still vertical.
|
|
3996
|
+
vec3 n = vec3(normal.x / iRadius, normal.y, normal.z / iRadius);
|
|
3997
|
+
if (part > 1.5 && part < 2.5) n.y += uBackRake * n.z;
|
|
3998
|
+
n = normalize(n);
|
|
3999
|
+
vec3 rn = vec3(n.x * c + n.z * s, n.y, -n.x * s + n.z * c);
|
|
4000
|
+
vec4 mv = modelViewMatrix * vec4(iOffset + rp, 1.0);
|
|
4001
|
+
vPosView = mv.xyz;
|
|
4002
|
+
vNormalWorld = rn;
|
|
4003
|
+
vNormalView = normalize(mat3(modelViewMatrix) * rn);
|
|
4004
|
+
vColor = iColor;
|
|
4005
|
+
// Occupant colour, resolved here so the fragment stage needs no extra
|
|
4006
|
+
// varyings beyond this one. A person is NOT painted in the seat's state
|
|
4007
|
+
// colour: a sold seat is red, and a hall of red people reads as a warning,
|
|
4008
|
+
// not an audience. Hair/clothing for the body, a warm tone for the head,
|
|
4009
|
+
// both varied by the same per-person hash.
|
|
4010
|
+
float t = fract(iSeed * 3.71);
|
|
4011
|
+
vec3 clothes = mix(vec3(0.13, 0.15, 0.20), vec3(0.34, 0.30, 0.36), t);
|
|
4012
|
+
vec3 skin = mix(vec3(0.52, 0.38, 0.29), vec3(0.86, 0.70, 0.58), fract(iSeed * 11.3));
|
|
4013
|
+
vOccupantTint = (part > 3.5) ? skin : clothes;
|
|
4014
|
+
vPart = part;
|
|
4015
|
+
vHeight = position.y;
|
|
4016
|
+
vRing = iRing;
|
|
4017
|
+
vDim = (uFocusFloor < -0.5 || abs(iFloor - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
4018
|
+
gl_Position = projectionMatrix * mv;
|
|
4019
|
+
}`
|
|
4020
|
+
);
|
|
4021
|
+
var CHAIR_FRAG = (
|
|
4022
|
+
/* glsl */
|
|
4023
|
+
`#version 300 es
|
|
4024
|
+
precision highp float;
|
|
4025
|
+
in vec3 vColor;
|
|
4026
|
+
in vec3 vNormalWorld;
|
|
4027
|
+
in vec3 vNormalView;
|
|
4028
|
+
in vec3 vPosView;
|
|
4029
|
+
in float vPart;
|
|
4030
|
+
in float vHeight;
|
|
4031
|
+
in vec3 vRing;
|
|
4032
|
+
in float vDim;
|
|
4033
|
+
in float vOccupant;
|
|
4034
|
+
in vec3 vOccupantTint;
|
|
4035
|
+
uniform vec3 uKeyDir;
|
|
4036
|
+
uniform vec3 uFadeColor;
|
|
4037
|
+
out vec4 fragColor;
|
|
4038
|
+
void main() {
|
|
4039
|
+
vec3 N = normalize(vNormalWorld);
|
|
4040
|
+
vec3 V = normalize(-vPosView);
|
|
4041
|
+
// The solids' rig, unchanged, so the chairs sit in the venue's light.
|
|
4042
|
+
float hemi = 0.5 + 0.5 * N.y;
|
|
4043
|
+
float key = max(dot(N, uKeyDir), 0.0);
|
|
4044
|
+
vec3 fillDir = normalize(vec3(-uKeyDir.x, 0.25, -uKeyDir.z));
|
|
4045
|
+
float fill = max(dot(N, fillDir), 0.0);
|
|
4046
|
+
vec3 tint = vColor;
|
|
4047
|
+
if (vOccupant > 0.5) tint = vOccupantTint;
|
|
4048
|
+
// The accommodation ring, kept legible once the dot (which drew it) is gone:
|
|
4049
|
+
// an accessible seat's PEDESTAL is painted in the ring colour, so the marker
|
|
4050
|
+
// survives to close range instead of vanishing exactly when the buyer arrives.
|
|
4051
|
+
float ringMask = step(0.001, dot(vRing, vRing));
|
|
4052
|
+
if (vPart < 0.5 && ringMask > 0.5) tint = vRing;
|
|
4053
|
+
// Pad brightest, back a step below it, pedestal darkest. Three untextured
|
|
4054
|
+
// boxes only read as one object if they are separated tonally \u2014 with a single
|
|
4055
|
+
// flat colour the chair silhouettes as a crate.
|
|
4056
|
+
// A person is lit as a person, not as upholstery: no part shading, and less
|
|
4057
|
+
// of the pad's sheen, so a head does not read as a polished box.
|
|
4058
|
+
float partShade = vOccupant > 0.5 ? 0.95 : (vPart < 0.5 ? 0.50 : (vPart < 1.5 ? 1.10 : 0.80));
|
|
4059
|
+
// Cheap vertical occlusion: a chair is in a dense row, so the closer a surface
|
|
4060
|
+
// sits to the deck the less sky it can actually see. This is the depth cue \u2014
|
|
4061
|
+
// without it the pad top, the back and the deck all resolve to the same flat
|
|
4062
|
+
// value and the row loses its form entirely.
|
|
4063
|
+
// Occupants rise above the 0.92 m chair back, so their ramp uses their own
|
|
4064
|
+
// height or every head would clamp to full brightness and float.
|
|
4065
|
+
float ao = mix(0.58, 1.0, clamp(vHeight / (vOccupant > 0.5 ? 1.30 : 0.92), 0.0, 1.0));
|
|
4066
|
+
vec3 base = tint * partShade * ao * (0.52 + 0.40 * hemi) + tint * key * 0.38 + tint * fill * 0.10;
|
|
4067
|
+
float fres = pow(1.0 - max(dot(normalize(vNormalView), V), 0.0), 3.0);
|
|
4068
|
+
// A brighter rim than the solids get: it picks out every chair's own edge,
|
|
4069
|
+
// which is what stops a block of them merging into one mass up close.
|
|
4070
|
+
base += vec3(0.26, 0.31, 0.38) * fres * 0.55;
|
|
4071
|
+
base = mix(base, uFadeColor, vDim * 0.75);
|
|
4072
|
+
fragColor = vec4(base, 1.0);
|
|
4073
|
+
}`
|
|
4074
|
+
);
|
|
3240
4075
|
var BG_VERT = (
|
|
3241
4076
|
/* glsl */
|
|
3242
4077
|
`#version 300 es
|
|
@@ -3333,7 +4168,7 @@ function createSeatPickProgram(gl) {
|
|
|
3333
4168
|
uniforms: {
|
|
3334
4169
|
uSeatRadius: { value: SEAT_DOT_RADIUS_M },
|
|
3335
4170
|
uSeatScale: { value: 1 },
|
|
3336
|
-
uMinPixels: { value:
|
|
4171
|
+
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
3337
4172
|
uPixelToWorld: { value: 2e-3 }
|
|
3338
4173
|
}
|
|
3339
4174
|
});
|
|
@@ -3378,11 +4213,32 @@ function createSeatProgram(gl) {
|
|
|
3378
4213
|
uniforms: {
|
|
3379
4214
|
uSeatRadius: { value: SEAT_DOT_RADIUS_M },
|
|
3380
4215
|
uSeatScale: { value: 1 },
|
|
3381
|
-
uMinPixels: { value:
|
|
4216
|
+
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
3382
4217
|
uPixelToWorld: { value: 2e-3 },
|
|
3383
4218
|
uSeatFade: { value: 0 },
|
|
3384
4219
|
uFocusFloor: { value: -1 },
|
|
3385
|
-
uFadeColor: { value: new Float32Array([0.32, 0.37, 0.43]) }
|
|
4220
|
+
uFadeColor: { value: new Float32Array([0.32, 0.37, 0.43]) },
|
|
4221
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4222
|
+
uChairNone: { value: CHAIR_NONE_M }
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
function createChairProgram(gl) {
|
|
4227
|
+
return new import_ogl3.Program(gl, {
|
|
4228
|
+
vertex: CHAIR_VERT,
|
|
4229
|
+
fragment: CHAIR_FRAG,
|
|
4230
|
+
transparent: false,
|
|
4231
|
+
depthTest: true,
|
|
4232
|
+
depthWrite: true,
|
|
4233
|
+
cullFace: false,
|
|
4234
|
+
uniforms: {
|
|
4235
|
+
uKeyDir: { value: new Float32Array([0.38, 0.86, 0.34]) },
|
|
4236
|
+
uFadeColor: { value: new Float32Array([0.32, 0.37, 0.43]) },
|
|
4237
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4238
|
+
uChairNone: { value: CHAIR_NONE_M },
|
|
4239
|
+
uBackRake: { value: BACK_RAKE_SLOPE },
|
|
4240
|
+
uBackBase: { value: BACK_BASE_M },
|
|
4241
|
+
uFocusFloor: { value: -1 }
|
|
3386
4242
|
}
|
|
3387
4243
|
});
|
|
3388
4244
|
}
|
|
@@ -3401,12 +4257,13 @@ function createBackgroundProgram(gl, top, bottom) {
|
|
|
3401
4257
|
}
|
|
3402
4258
|
|
|
3403
4259
|
// src/view3d/scene/build.ts
|
|
3404
|
-
function writeSeatColors(iColor, iState, start, count, states) {
|
|
4260
|
+
function writeSeatColors(iColor, iState, start, count, states, iCategory) {
|
|
3405
4261
|
for (let i = start; i < start + count; i++) {
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
iColor[i * 3
|
|
3409
|
-
iColor[i * 3 +
|
|
4262
|
+
const useCategory = iCategory && iState[i] === 0;
|
|
4263
|
+
const c = useCategory ? null : states[iState[i]] ?? states[0];
|
|
4264
|
+
iColor[i * 3] = c ? c[0] : iCategory[i * 3];
|
|
4265
|
+
iColor[i * 3 + 1] = c ? c[1] : iCategory[i * 3 + 1];
|
|
4266
|
+
iColor[i * 3 + 2] = c ? c[2] : iCategory[i * 3 + 2];
|
|
3410
4267
|
}
|
|
3411
4268
|
}
|
|
3412
4269
|
var SEAT_QUAD = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
|
|
@@ -3432,7 +4289,7 @@ function buildGpuScene(gl, model) {
|
|
|
3432
4289
|
const seatProg = createSeatProgram(gl);
|
|
3433
4290
|
const iColor = new Float32Array(model.seats.count * 3);
|
|
3434
4291
|
const stateColors = SEAT_STATES.map((st) => model.theme.seatStates[st]);
|
|
3435
|
-
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors);
|
|
4292
|
+
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors, model.seats.iCategory);
|
|
3436
4293
|
const seatGeo = new import_ogl4.Geometry(gl, {
|
|
3437
4294
|
position: { size: 2, data: SEAT_QUAD },
|
|
3438
4295
|
iOffset: { size: 3, data: model.seats.iPosition, instanced: 1 },
|
|
@@ -3448,17 +4305,105 @@ function buildGpuScene(gl, model) {
|
|
|
3448
4305
|
seatMesh.frustumCulled = false;
|
|
3449
4306
|
if (model.seats.count > 0) seatMesh.setParent(main);
|
|
3450
4307
|
const colorAttr = seatGeo.attributes.iColor;
|
|
3451
|
-
|
|
4308
|
+
const chairBase = buildChairMesh();
|
|
4309
|
+
const chairProg = createChairProgram(gl);
|
|
4310
|
+
const CAP = CHAIR_MAX_INSTANCES;
|
|
4311
|
+
const cOffset = new Float32Array(CAP * 3);
|
|
4312
|
+
const cColor = new Float32Array(CAP * 3);
|
|
4313
|
+
const cRadius = new Float32Array(CAP);
|
|
4314
|
+
const cYaw = new Float32Array(CAP);
|
|
4315
|
+
const cRing = new Float32Array(CAP * 3);
|
|
4316
|
+
const cFloor = new Float32Array(CAP);
|
|
4317
|
+
const cSeed = new Float32Array(CAP);
|
|
4318
|
+
const chairGeo = new import_ogl4.Geometry(gl, {
|
|
4319
|
+
position: { size: 3, data: chairBase.position },
|
|
4320
|
+
normal: { size: 3, data: chairBase.normal },
|
|
4321
|
+
part: { size: 1, data: chairBase.part },
|
|
4322
|
+
index: { data: chairBase.index },
|
|
4323
|
+
iOffset: { size: 3, data: cOffset, instanced: 1 },
|
|
4324
|
+
iColor: { size: 3, data: cColor, instanced: 1 },
|
|
4325
|
+
iRadius: { size: 1, data: cRadius, instanced: 1 },
|
|
4326
|
+
iYaw: { size: 1, data: cYaw, instanced: 1 },
|
|
4327
|
+
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
4328
|
+
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4329
|
+
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
4330
|
+
});
|
|
4331
|
+
const chairMesh = new import_ogl4.Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
4332
|
+
chairMesh.frustumCulled = false;
|
|
4333
|
+
let nearCount = 0;
|
|
4334
|
+
let nearIndices = new Int32Array(0);
|
|
4335
|
+
const writeChairColors = () => {
|
|
4336
|
+
const src = model.seats;
|
|
4337
|
+
for (let k = 0; k < nearCount; k++) {
|
|
4338
|
+
const i = nearIndices[k];
|
|
4339
|
+
const useCategory = src.iCategory && src.iState[i] === 0;
|
|
4340
|
+
const c = useCategory ? null : stateColors[src.iState[i]] ?? stateColors[0];
|
|
4341
|
+
if (c) {
|
|
4342
|
+
cColor[k * 3] = c[0];
|
|
4343
|
+
cColor[k * 3 + 1] = c[1];
|
|
4344
|
+
cColor[k * 3 + 2] = c[2];
|
|
4345
|
+
} else {
|
|
4346
|
+
const u = upholsteryTone([src.iCategory[i * 3], src.iCategory[i * 3 + 1], src.iCategory[i * 3 + 2]]);
|
|
4347
|
+
cColor[k * 3] = u[0];
|
|
4348
|
+
cColor[k * 3 + 1] = u[1];
|
|
4349
|
+
cColor[k * 3 + 2] = u[2];
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
chairGeo.attributes.iColor.needsUpdate = true;
|
|
4353
|
+
};
|
|
4354
|
+
const scene = {
|
|
3452
4355
|
main,
|
|
3453
4356
|
background,
|
|
3454
4357
|
seatProgram: seatProg,
|
|
3455
4358
|
solidProgram: solidProg,
|
|
4359
|
+
chairProgram: chairProg,
|
|
3456
4360
|
seatGeometry: seatGeo,
|
|
3457
4361
|
solidGeometry: solidGeo,
|
|
3458
4362
|
drawCalls: 3,
|
|
4363
|
+
setNearSeats(indices, count) {
|
|
4364
|
+
const n = Math.min(count, CAP);
|
|
4365
|
+
nearIndices = indices;
|
|
4366
|
+
nearCount = n;
|
|
4367
|
+
if (n === 0) {
|
|
4368
|
+
if (chairMesh.parent) chairMesh.setParent(null);
|
|
4369
|
+
chairGeo.instancedCount = 0;
|
|
4370
|
+
scene.drawCalls = 3;
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const src = model.seats;
|
|
4374
|
+
for (let k = 0; k < n; k++) {
|
|
4375
|
+
const i = indices[k];
|
|
4376
|
+
cOffset[k * 3] = src.iPosition[i * 3];
|
|
4377
|
+
cOffset[k * 3 + 1] = src.iPosition[i * 3 + 1];
|
|
4378
|
+
cOffset[k * 3 + 2] = src.iPosition[i * 3 + 2];
|
|
4379
|
+
cRadius[k] = src.iChairWidth[i];
|
|
4380
|
+
cYaw[k] = src.iYaw[i];
|
|
4381
|
+
cRing[k * 3] = src.iRing[i * 3];
|
|
4382
|
+
cRing[k * 3 + 1] = src.iRing[i * 3 + 1];
|
|
4383
|
+
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
4384
|
+
cFloor[k] = src.iFloor[i];
|
|
4385
|
+
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
4386
|
+
}
|
|
4387
|
+
writeChairColors();
|
|
4388
|
+
chairGeo.attributes.iOffset.needsUpdate = true;
|
|
4389
|
+
chairGeo.attributes.iRadius.needsUpdate = true;
|
|
4390
|
+
chairGeo.attributes.iYaw.needsUpdate = true;
|
|
4391
|
+
chairGeo.attributes.iRing.needsUpdate = true;
|
|
4392
|
+
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4393
|
+
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4394
|
+
chairGeo.instancedCount = n;
|
|
4395
|
+
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
4396
|
+
scene.drawCalls = 4;
|
|
4397
|
+
},
|
|
4398
|
+
nearSeatCount() {
|
|
4399
|
+
return nearCount;
|
|
4400
|
+
},
|
|
3459
4401
|
uploadSeatStateRuns(runs) {
|
|
3460
4402
|
if (!runs.length) return;
|
|
3461
|
-
|
|
4403
|
+
if (nearCount) writeChairColors();
|
|
4404
|
+
for (const run of runs) {
|
|
4405
|
+
writeSeatColors(iColor, model.seats.iState, run.start, run.length, stateColors, model.seats.iCategory);
|
|
4406
|
+
}
|
|
3462
4407
|
const buffer = colorAttr.buffer;
|
|
3463
4408
|
if (!buffer) {
|
|
3464
4409
|
colorAttr.needsUpdate = true;
|
|
@@ -3477,8 +4422,11 @@ function buildGpuScene(gl, model) {
|
|
|
3477
4422
|
solidProg.remove();
|
|
3478
4423
|
seatGeo.remove();
|
|
3479
4424
|
seatProg.remove();
|
|
4425
|
+
chairGeo.remove();
|
|
4426
|
+
chairProg.remove();
|
|
3480
4427
|
}
|
|
3481
4428
|
};
|
|
4429
|
+
return scene;
|
|
3482
4430
|
}
|
|
3483
4431
|
|
|
3484
4432
|
// src/view3d/pick/pickPipeline.ts
|
|
@@ -3961,6 +4909,317 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
3961
4909
|
};
|
|
3962
4910
|
}
|
|
3963
4911
|
|
|
4912
|
+
// src/view3d/scene/panoSphere.ts
|
|
4913
|
+
var import_ogl7 = require("ogl");
|
|
4914
|
+
var PANO_SEGMENTS_LON = 64;
|
|
4915
|
+
var PANO_SEGMENTS_LAT = 32;
|
|
4916
|
+
var PANO_RADIUS_M = 500;
|
|
4917
|
+
function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEGMENTS_LAT, radius = PANO_RADIUS_M) {
|
|
4918
|
+
const lonCount = segmentsLon + 1;
|
|
4919
|
+
const latCount = segmentsLat + 1;
|
|
4920
|
+
const vertexCount = lonCount * latCount;
|
|
4921
|
+
const position = new Float32Array(vertexCount * 3);
|
|
4922
|
+
const uv = new Float32Array(vertexCount * 2);
|
|
4923
|
+
for (let iLat = 0; iLat < latCount; iLat++) {
|
|
4924
|
+
const v = iLat / segmentsLat;
|
|
4925
|
+
const polar = v * Math.PI;
|
|
4926
|
+
const sinPolar = Math.sin(polar);
|
|
4927
|
+
const cosPolar = Math.cos(polar);
|
|
4928
|
+
for (let iLon = 0; iLon < lonCount; iLon++) {
|
|
4929
|
+
const u = iLon / segmentsLon;
|
|
4930
|
+
const azimuth = (u - 0.5) * Math.PI * 2;
|
|
4931
|
+
const i = iLat * lonCount + iLon;
|
|
4932
|
+
position[i * 3] = radius * sinPolar * Math.sin(azimuth);
|
|
4933
|
+
position[i * 3 + 1] = radius * cosPolar;
|
|
4934
|
+
position[i * 3 + 2] = -radius * sinPolar * Math.cos(azimuth);
|
|
4935
|
+
uv[i * 2] = u;
|
|
4936
|
+
uv[i * 2 + 1] = v;
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
const index = new Uint16Array(segmentsLon * segmentsLat * 6);
|
|
4940
|
+
let w = 0;
|
|
4941
|
+
for (let iLat = 0; iLat < segmentsLat; iLat++) {
|
|
4942
|
+
for (let iLon = 0; iLon < segmentsLon; iLon++) {
|
|
4943
|
+
const a = iLat * lonCount + iLon;
|
|
4944
|
+
const b = a + lonCount;
|
|
4945
|
+
index[w++] = a;
|
|
4946
|
+
index[w++] = b;
|
|
4947
|
+
index[w++] = a + 1;
|
|
4948
|
+
index[w++] = a + 1;
|
|
4949
|
+
index[w++] = b;
|
|
4950
|
+
index[w++] = b + 1;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
return { position, uv, index };
|
|
4954
|
+
}
|
|
4955
|
+
function bearingPitchToDirection(bearingDeg, pitchDeg) {
|
|
4956
|
+
const yaw = bearingDeg * Math.PI / 180;
|
|
4957
|
+
const pitch = pitchDeg * Math.PI / 180;
|
|
4958
|
+
const cosPitch = Math.cos(pitch);
|
|
4959
|
+
return [cosPitch * Math.sin(yaw), Math.sin(pitch), -cosPitch * Math.cos(yaw)];
|
|
4960
|
+
}
|
|
4961
|
+
var PANO_VERT = (
|
|
4962
|
+
/* glsl */
|
|
4963
|
+
`#version 300 es
|
|
4964
|
+
precision highp float;
|
|
4965
|
+
in vec3 position;
|
|
4966
|
+
in vec2 uv;
|
|
4967
|
+
uniform mat4 modelViewMatrix;
|
|
4968
|
+
uniform mat4 projectionMatrix;
|
|
4969
|
+
out vec2 vUv;
|
|
4970
|
+
void main() {
|
|
4971
|
+
vUv = uv;
|
|
4972
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
4973
|
+
}`
|
|
4974
|
+
);
|
|
4975
|
+
var PANO_FRAG = (
|
|
4976
|
+
/* glsl */
|
|
4977
|
+
`#version 300 es
|
|
4978
|
+
precision highp float;
|
|
4979
|
+
in vec2 vUv;
|
|
4980
|
+
uniform sampler2D uPano;
|
|
4981
|
+
uniform float uOpacity;
|
|
4982
|
+
out vec4 fragColor;
|
|
4983
|
+
void main() {
|
|
4984
|
+
vec3 rgb = texture(uPano, vUv).rgb;
|
|
4985
|
+
fragColor = vec4(rgb, uOpacity);
|
|
4986
|
+
}`
|
|
4987
|
+
);
|
|
4988
|
+
function createPanoSphere(gl, image) {
|
|
4989
|
+
const { position, uv, index } = buildPanoSphere();
|
|
4990
|
+
const geometry = new import_ogl7.Geometry(gl, {
|
|
4991
|
+
position: { size: 3, data: position },
|
|
4992
|
+
uv: { size: 2, data: uv },
|
|
4993
|
+
index: { data: index }
|
|
4994
|
+
});
|
|
4995
|
+
const texture = new import_ogl7.Texture(gl, {
|
|
4996
|
+
image,
|
|
4997
|
+
// The seam column is duplicated in geometry, so CLAMP is correct and avoids
|
|
4998
|
+
// a wrapped bilinear tap bleeding the far edge of the image into it.
|
|
4999
|
+
wrapS: gl.CLAMP_TO_EDGE,
|
|
5000
|
+
wrapT: gl.CLAMP_TO_EDGE,
|
|
5001
|
+
generateMipmaps: true,
|
|
5002
|
+
// MUST be false, and this is the one line that decides whether the whole
|
|
5003
|
+
// panorama is upside down.
|
|
5004
|
+
//
|
|
5005
|
+
// OGL defaults `flipY` to true for 2D textures, which is right for the usual
|
|
5006
|
+
// case: a UV of 0 means the BOTTOM of a quad, an image's first row is its
|
|
5007
|
+
// TOP, and flipping on upload reconciles the two. This sphere is the other
|
|
5008
|
+
// case. Its `v` is authored in IMAGE space — v=0 is the top of the equirect
|
|
5009
|
+
// and maps to the top of the sphere (`buildPanoSphere`, and the mapping
|
|
5010
|
+
// `generatePanorama` and the 2D viewer both use). Flipping on upload as well
|
|
5011
|
+
// applies the correction twice: the stadium pitch renders on the ceiling and
|
|
5012
|
+
// the audience sits overhead.
|
|
5013
|
+
//
|
|
5014
|
+
// The geometry tests state the v mapping and pass either way — they never
|
|
5015
|
+
// touch GL — so this is not something a unit test can defend. It was caught
|
|
5016
|
+
// by looking at a stadium.
|
|
5017
|
+
flipY: false
|
|
5018
|
+
});
|
|
5019
|
+
const program = new import_ogl7.Program(gl, {
|
|
5020
|
+
vertex: PANO_VERT,
|
|
5021
|
+
fragment: PANO_FRAG,
|
|
5022
|
+
uniforms: { uPano: { value: texture }, uOpacity: { value: 0 } },
|
|
5023
|
+
transparent: true,
|
|
5024
|
+
depthTest: false,
|
|
5025
|
+
depthWrite: false
|
|
5026
|
+
});
|
|
5027
|
+
const mesh = new import_ogl7.Mesh(gl, { geometry, program });
|
|
5028
|
+
return {
|
|
5029
|
+
mesh,
|
|
5030
|
+
setOpacity(value) {
|
|
5031
|
+
program.uniforms.uOpacity.value = Math.max(0, Math.min(1, value));
|
|
5032
|
+
},
|
|
5033
|
+
dispose() {
|
|
5034
|
+
mesh.setParent(null);
|
|
5035
|
+
geometry.remove();
|
|
5036
|
+
if (texture.texture) gl.deleteTexture(texture.texture);
|
|
5037
|
+
program.remove();
|
|
5038
|
+
}
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
// src/view3d/crossfade/panoramaSphere.ts
|
|
5043
|
+
var DEG_PER_PX = 0.15;
|
|
5044
|
+
function loadImage(url) {
|
|
5045
|
+
return new Promise((resolve, reject) => {
|
|
5046
|
+
const img = new Image();
|
|
5047
|
+
img.crossOrigin = "anonymous";
|
|
5048
|
+
img.onload = () => resolve(img);
|
|
5049
|
+
img.onerror = () => reject(new Error("panorama_image_failed"));
|
|
5050
|
+
img.src = url;
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
5054
|
+
const fadeMs = opts.fadeMs ?? 400;
|
|
5055
|
+
let bearing = view?.initialBearingDeg ?? 0;
|
|
5056
|
+
let pitch = 0;
|
|
5057
|
+
let disposed = false;
|
|
5058
|
+
if (!view && deps.focalWorld) {
|
|
5059
|
+
const p = deps.camera.position;
|
|
5060
|
+
const dx = deps.focalWorld[0] - p.x;
|
|
5061
|
+
const dy = deps.focalWorld[1] - p.y;
|
|
5062
|
+
const dz = deps.focalWorld[2] - p.z;
|
|
5063
|
+
const flat = Math.hypot(dx, dz);
|
|
5064
|
+
if (flat > 1e-3) {
|
|
5065
|
+
bearing = Math.atan2(dx, -dz) * 180 / Math.PI;
|
|
5066
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, Math.atan2(dy, flat) * 180 / Math.PI));
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
let sphere = null;
|
|
5070
|
+
if (view) {
|
|
5071
|
+
let image;
|
|
5072
|
+
try {
|
|
5073
|
+
image = await loadImage(view.url);
|
|
5074
|
+
} catch {
|
|
5075
|
+
return null;
|
|
5076
|
+
}
|
|
5077
|
+
try {
|
|
5078
|
+
sphere = createPanoSphere(deps.gl, image);
|
|
5079
|
+
} catch {
|
|
5080
|
+
return null;
|
|
5081
|
+
}
|
|
5082
|
+
sphere.mesh.renderOrder = 999;
|
|
5083
|
+
sphere.mesh.setParent(deps.scene);
|
|
5084
|
+
}
|
|
5085
|
+
const priorFov = deps.camera.fov;
|
|
5086
|
+
deps.camera.perspective({ fov: VFOV_DEG, aspect: deps.camera.aspect });
|
|
5087
|
+
const priorQuat = deps.camera.quaternion.slice();
|
|
5088
|
+
const aim = () => {
|
|
5089
|
+
const p = deps.camera.position;
|
|
5090
|
+
sphere?.mesh.position.set(p.x, p.y, p.z);
|
|
5091
|
+
const [dx, dy, dz] = bearingPitchToDirection(bearing, pitch);
|
|
5092
|
+
deps.camera.lookAt([p.x + dx, p.y + dy, p.z + dz]);
|
|
5093
|
+
deps.requestRender();
|
|
5094
|
+
};
|
|
5095
|
+
aim();
|
|
5096
|
+
const root = document.createElement("div");
|
|
5097
|
+
root.setAttribute("role", "dialog");
|
|
5098
|
+
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
5099
|
+
Object.assign(root.style, {
|
|
5100
|
+
position: "absolute",
|
|
5101
|
+
inset: "0",
|
|
5102
|
+
zIndex: "10",
|
|
5103
|
+
cursor: "grab",
|
|
5104
|
+
touchAction: "none"
|
|
5105
|
+
});
|
|
5106
|
+
const closeBtn = document.createElement("button");
|
|
5107
|
+
closeBtn.type = "button";
|
|
5108
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
5109
|
+
closeBtn.textContent = "\u2715";
|
|
5110
|
+
Object.assign(closeBtn.style, {
|
|
5111
|
+
position: "absolute",
|
|
5112
|
+
top: "12px",
|
|
5113
|
+
right: "12px",
|
|
5114
|
+
zIndex: "2",
|
|
5115
|
+
width: "34px",
|
|
5116
|
+
height: "34px",
|
|
5117
|
+
borderRadius: "999px",
|
|
5118
|
+
cursor: "pointer",
|
|
5119
|
+
border: "1px solid rgba(255,255,255,0.25)",
|
|
5120
|
+
background: "rgba(8,12,18,0.6)",
|
|
5121
|
+
color: "#e6edf3",
|
|
5122
|
+
fontSize: "15px",
|
|
5123
|
+
lineHeight: "1"
|
|
5124
|
+
});
|
|
5125
|
+
root.appendChild(closeBtn);
|
|
5126
|
+
const hint = document.createElement("div");
|
|
5127
|
+
hint.textContent = "Drag to look around \xB7 Esc to close";
|
|
5128
|
+
Object.assign(hint.style, {
|
|
5129
|
+
position: "absolute",
|
|
5130
|
+
bottom: "12px",
|
|
5131
|
+
left: "0",
|
|
5132
|
+
right: "0",
|
|
5133
|
+
textAlign: "center",
|
|
5134
|
+
color: "rgba(230,237,243,0.7)",
|
|
5135
|
+
font: "12px ui-sans-serif, system-ui, sans-serif",
|
|
5136
|
+
pointerEvents: "none"
|
|
5137
|
+
});
|
|
5138
|
+
root.appendChild(hint);
|
|
5139
|
+
container.appendChild(root);
|
|
5140
|
+
let dragging = false;
|
|
5141
|
+
let lastX = 0;
|
|
5142
|
+
let lastY = 0;
|
|
5143
|
+
const onDown = (e) => {
|
|
5144
|
+
if (e.target === closeBtn) return;
|
|
5145
|
+
dragging = true;
|
|
5146
|
+
lastX = e.clientX;
|
|
5147
|
+
lastY = e.clientY;
|
|
5148
|
+
root.setPointerCapture?.(e.pointerId);
|
|
5149
|
+
root.style.cursor = "grabbing";
|
|
5150
|
+
};
|
|
5151
|
+
const onMove = (e) => {
|
|
5152
|
+
if (!dragging) return;
|
|
5153
|
+
bearing += (e.clientX - lastX) * DEG_PER_PX;
|
|
5154
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.clientY - lastY) * DEG_PER_PX));
|
|
5155
|
+
lastX = e.clientX;
|
|
5156
|
+
lastY = e.clientY;
|
|
5157
|
+
aim();
|
|
5158
|
+
};
|
|
5159
|
+
const onUp = () => {
|
|
5160
|
+
dragging = false;
|
|
5161
|
+
root.style.cursor = "grab";
|
|
5162
|
+
};
|
|
5163
|
+
root.addEventListener("pointerdown", onDown);
|
|
5164
|
+
root.addEventListener("pointermove", onMove);
|
|
5165
|
+
root.addEventListener("pointerup", onUp);
|
|
5166
|
+
root.addEventListener("pointercancel", onUp);
|
|
5167
|
+
const onKey = (e) => {
|
|
5168
|
+
if (e.key !== "Escape" || disposed) return;
|
|
5169
|
+
e.stopPropagation();
|
|
5170
|
+
handle.close();
|
|
5171
|
+
};
|
|
5172
|
+
window.addEventListener("keydown", onKey);
|
|
5173
|
+
let raf = 0;
|
|
5174
|
+
const fadeTo = (target, done) => {
|
|
5175
|
+
if (!sphere) {
|
|
5176
|
+
done?.();
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
const from = target === 1 ? 0 : 1;
|
|
5180
|
+
const start = performance.now();
|
|
5181
|
+
const step = () => {
|
|
5182
|
+
if (disposed) return;
|
|
5183
|
+
const t = fadeMs <= 0 ? 1 : Math.min(1, (performance.now() - start) / fadeMs);
|
|
5184
|
+
sphere?.setOpacity(from + (target - from) * t);
|
|
5185
|
+
deps.requestRender();
|
|
5186
|
+
if (t < 1) raf = requestAnimationFrame(step);
|
|
5187
|
+
else done?.();
|
|
5188
|
+
};
|
|
5189
|
+
raf = requestAnimationFrame(step);
|
|
5190
|
+
};
|
|
5191
|
+
fadeTo(1);
|
|
5192
|
+
const teardown = () => {
|
|
5193
|
+
if (disposed) return;
|
|
5194
|
+
disposed = true;
|
|
5195
|
+
cancelAnimationFrame(raf);
|
|
5196
|
+
root.removeEventListener("pointerdown", onDown);
|
|
5197
|
+
root.removeEventListener("pointermove", onMove);
|
|
5198
|
+
root.removeEventListener("pointerup", onUp);
|
|
5199
|
+
root.removeEventListener("pointercancel", onUp);
|
|
5200
|
+
window.removeEventListener("keydown", onKey);
|
|
5201
|
+
root.remove();
|
|
5202
|
+
sphere?.dispose();
|
|
5203
|
+
deps.camera.perspective({ fov: priorFov, aspect: deps.camera.aspect });
|
|
5204
|
+
deps.camera.quaternion.set(priorQuat[0], priorQuat[1], priorQuat[2], priorQuat[3]);
|
|
5205
|
+
deps.requestRender();
|
|
5206
|
+
};
|
|
5207
|
+
const handle = {
|
|
5208
|
+
close() {
|
|
5209
|
+
if (disposed) return;
|
|
5210
|
+
const finish = () => {
|
|
5211
|
+
teardown();
|
|
5212
|
+
opts.onClose?.();
|
|
5213
|
+
};
|
|
5214
|
+
cancelAnimationFrame(raf);
|
|
5215
|
+
fadeTo(0, finish);
|
|
5216
|
+
},
|
|
5217
|
+
dispose: teardown
|
|
5218
|
+
};
|
|
5219
|
+
closeBtn.addEventListener("click", () => handle.close());
|
|
5220
|
+
return handle;
|
|
5221
|
+
}
|
|
5222
|
+
|
|
3964
5223
|
// src/view3d/analytics.ts
|
|
3965
5224
|
var now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
3966
5225
|
var Analytics3D = class {
|
|
@@ -4046,6 +5305,28 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4046
5305
|
gpu.seatProgram.uniforms.uSeatRadius.value = SEAT_DOT_RADIUS_M * model.theme.seatScale;
|
|
4047
5306
|
gpu.seatProgram.uniforms.uFocusFloor.value = focusedFloor;
|
|
4048
5307
|
gpu.solidProgram.uniforms.uFocusFloor.value = focusedFloor;
|
|
5308
|
+
gpu.chairProgram.uniforms.uFocusFloor.value = focusedFloor;
|
|
5309
|
+
lastGatherX = Infinity;
|
|
5310
|
+
};
|
|
5311
|
+
const nearIndex = new NearFieldIndex(model.seats.iPosition, model.seats.count);
|
|
5312
|
+
const nearBuf = new Int32Array(CHAIR_MAX_INSTANCES);
|
|
5313
|
+
let lastGatherX = Infinity;
|
|
5314
|
+
let lastGatherZ = Infinity;
|
|
5315
|
+
const updateNearField = () => {
|
|
5316
|
+
if (!gpu) return;
|
|
5317
|
+
const cam = orbit.camera.position;
|
|
5318
|
+
const moved2 = Math.hypot(cam.x - lastGatherX, cam.z - lastGatherZ);
|
|
5319
|
+
if (moved2 < CHAIR_REBUILD_M) return;
|
|
5320
|
+
lastGatherX = cam.x;
|
|
5321
|
+
lastGatherZ = cam.z;
|
|
5322
|
+
const outside = Math.hypot(cam.x - model.bounds.center[0], cam.z - model.bounds.center[2]) - model.bounds.radius;
|
|
5323
|
+
if (outside > CHAIR_GATHER_M) {
|
|
5324
|
+
if (gpu.nearSeatCount()) gpu.setNearSeats(nearBuf, 0);
|
|
5325
|
+
return;
|
|
5326
|
+
}
|
|
5327
|
+
const n = nearIndex.gather(cam.x, cam.z, CHAIR_GATHER_M, nearBuf);
|
|
5328
|
+
if (n === 0 && gpu.nearSeatCount() === 0) return;
|
|
5329
|
+
gpu.setNearSeats(nearBuf, n);
|
|
4049
5330
|
};
|
|
4050
5331
|
const glctx = new GLContext(container, {
|
|
4051
5332
|
onContextLost: () => {
|
|
@@ -4092,7 +5373,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4092
5373
|
const u = gpu.seatProgram.uniforms;
|
|
4093
5374
|
u.uSeatScale.value = lod.scale;
|
|
4094
5375
|
u.uSeatFade.value = lod.fade;
|
|
5376
|
+
u.uMinPixels.value = lod.minPixels;
|
|
4095
5377
|
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov * DEG3 / 2) / Math.max(1, glctx.pixelHeight);
|
|
5378
|
+
updateNearField();
|
|
4096
5379
|
glctx.renderer.render({ scene: gpu.background, clear: true });
|
|
4097
5380
|
glctx.renderer.render({ scene: gpu.main, camera: orbit.camera, clear: false });
|
|
4098
5381
|
labelOverlay.update(
|
|
@@ -4100,7 +5383,10 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4100
5383
|
glctx.canvas.clientWidth || 1,
|
|
4101
5384
|
glctx.canvas.clientHeight || 1,
|
|
4102
5385
|
orbit.currentDistance,
|
|
4103
|
-
model.bounds.radius
|
|
5386
|
+
model.bounds.radius,
|
|
5387
|
+
// The dense label rungs rank by real distance from the eye, not by the
|
|
5388
|
+
// orbit radius — at the arrival pose those are wildly different numbers.
|
|
5389
|
+
[orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z]
|
|
4104
5390
|
);
|
|
4105
5391
|
return moving;
|
|
4106
5392
|
});
|
|
@@ -4145,7 +5431,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4145
5431
|
];
|
|
4146
5432
|
const placeCameraFinal = (finalPos, focal) => {
|
|
4147
5433
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
4148
|
-
orbit.camera.lookAt(new
|
|
5434
|
+
orbit.camera.lookAt(new import_ogl8.Vec3(focal[0], focal[1], focal[2]));
|
|
4149
5435
|
orbit.camera.fov = FOV_END;
|
|
4150
5436
|
orbit.camera.updateProjectionMatrix();
|
|
4151
5437
|
};
|
|
@@ -4208,7 +5494,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4208
5494
|
zIndex: "4"
|
|
4209
5495
|
});
|
|
4210
5496
|
overviewChip.addEventListener("click", () => {
|
|
4211
|
-
if (disposed || frozen) return;
|
|
5497
|
+
if (disposed || frozen || panorama) return;
|
|
4212
5498
|
cancelFlight();
|
|
4213
5499
|
removeArriveChip();
|
|
4214
5500
|
orbit.frameSoft(model.bounds, stageAzimuth);
|
|
@@ -4236,18 +5522,36 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4236
5522
|
}
|
|
4237
5523
|
if (disposed || gen !== flightGen) return;
|
|
4238
5524
|
removeArriveChip();
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
5525
|
+
const onClose = () => {
|
|
5526
|
+
panorama = null;
|
|
5527
|
+
labelOverlay.setVisible(true);
|
|
5528
|
+
frozen = false;
|
|
5529
|
+
analytics.panoramaClosed();
|
|
5530
|
+
orbit.resumeAfterFlight(model.focalWorld);
|
|
5531
|
+
loop.requestRender();
|
|
5532
|
+
showArriveChip(seatId, flightGen);
|
|
5533
|
+
};
|
|
5534
|
+
const sceneMode = view.generated === true;
|
|
5535
|
+
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5536
|
+
gl: glctx.gl,
|
|
5537
|
+
scene: gpu.main,
|
|
5538
|
+
camera: orbit.camera,
|
|
5539
|
+
requestRender: () => loop.requestRender(),
|
|
5540
|
+
focalWorld: model.focalWorld
|
|
5541
|
+
}, { fadeMs, seatLabel: seatId, onClose }) : null;
|
|
5542
|
+
if (disposed || gen !== flightGen) {
|
|
5543
|
+
spherical?.dispose();
|
|
5544
|
+
return;
|
|
5545
|
+
}
|
|
5546
|
+
if (spherical) {
|
|
5547
|
+
orbit.syncFromCamera();
|
|
5548
|
+
labelOverlay.setVisible(false);
|
|
5549
|
+
frozen = false;
|
|
5550
|
+
loop.requestRender();
|
|
5551
|
+
panorama = spherical;
|
|
5552
|
+
} else {
|
|
5553
|
+
panorama = mountPanorama(container, view, { fadeMs, seatLabel: seatId, onClose });
|
|
5554
|
+
}
|
|
4251
5555
|
analytics.panoramaOpened();
|
|
4252
5556
|
};
|
|
4253
5557
|
const cancelFlight = () => {
|
|
@@ -4280,7 +5584,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4280
5584
|
showArriveChip(seatId, gen);
|
|
4281
5585
|
return Promise.resolve();
|
|
4282
5586
|
}
|
|
4283
|
-
const startQuat = new
|
|
5587
|
+
const startQuat = new import_ogl8.Quat().copy(orbit.camera.quaternion);
|
|
4284
5588
|
const endQuat = lookAtQuat(orbit.camera, finalPos, focal);
|
|
4285
5589
|
loop.requestRender();
|
|
4286
5590
|
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
@@ -4373,6 +5677,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4373
5677
|
if (gpu) {
|
|
4374
5678
|
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
4375
5679
|
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
5680
|
+
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
4376
5681
|
}
|
|
4377
5682
|
focusedFloor = value;
|
|
4378
5683
|
if (index !== null) {
|