@seatlayer/core 0.46.0 → 0.47.1
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-AZODENEL.js +97 -0
- package/dist/chunk-AZODENEL.js.map +1 -0
- package/dist/chunk-FVCOOLJO.js +335 -0
- package/dist/chunk-FVCOOLJO.js.map +1 -0
- package/dist/{chunk-5MRJCIZP.js → chunk-TCTAS4Z2.js} +88 -14
- package/dist/chunk-TCTAS4Z2.js.map +1 -0
- package/dist/{de-UHAOVDS3.js → de-ST4H423D.js} +6 -3
- package/dist/{de-UHAOVDS3.js.map → de-ST4H423D.js.map} +1 -1
- package/dist/{es-6TJS2L7S.js → es-WEC5NHRT.js} +6 -3
- package/dist/{es-6TJS2L7S.js.map → es-WEC5NHRT.js.map} +1 -1
- package/dist/{fr-MMSY7FPE.js → fr-SGBFNAQV.js} +6 -3
- package/dist/{fr-MMSY7FPE.js.map → fr-SGBFNAQV.js.map} +1 -1
- package/dist/index.cjs +251 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -4
- package/dist/index.d.ts +119 -4
- package/dist/index.js +152 -24
- package/dist/index.js.map +1 -1
- package/dist/{types-DBnRO2hX.d.cts → types-CE63BK0j.d.cts} +88 -10
- package/dist/{types-DBnRO2hX.d.ts → types-CE63BK0j.d.ts} +88 -10
- package/dist/view/panoramaDelivery.cjs +126 -0
- package/dist/view/panoramaDelivery.cjs.map +1 -0
- package/dist/view/panoramaDelivery.d.cts +34 -0
- package/dist/view/panoramaDelivery.d.ts +34 -0
- package/dist/view/panoramaDelivery.js +17 -0
- package/dist/view/panoramaDelivery.js.map +1 -0
- package/dist/view3d/crossfade/panorama.cjs +453 -0
- package/dist/view3d/crossfade/panorama.cjs.map +1 -0
- package/dist/view3d/crossfade/panorama.d.cts +110 -0
- package/dist/view3d/crossfade/panorama.d.ts +110 -0
- package/dist/view3d/crossfade/panorama.js +30 -0
- package/dist/view3d/crossfade/panorama.js.map +1 -0
- package/dist/view3d/index.cjs +1176 -128
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +73 -35
- package/dist/view3d/index.d.ts +73 -35
- package/dist/view3d/index.js +960 -300
- package/dist/view3d/index.js.map +1 -1
- package/package.json +28 -1
- package/dist/chunk-5MRJCIZP.js.map +0 -1
package/dist/view3d/index.js
CHANGED
|
@@ -7,7 +7,20 @@ import {
|
|
|
7
7
|
pointInPolygonWithHoles,
|
|
8
8
|
resolveSection,
|
|
9
9
|
sectionGeometry
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-TCTAS4Z2.js";
|
|
11
|
+
import {
|
|
12
|
+
MAX_PITCH_DEG,
|
|
13
|
+
VFOV_DEG,
|
|
14
|
+
clampPanoramaFov,
|
|
15
|
+
mountPanorama,
|
|
16
|
+
seatViewDisclosure
|
|
17
|
+
} from "../chunk-FVCOOLJO.js";
|
|
18
|
+
import {
|
|
19
|
+
browserPanoramaConstraints,
|
|
20
|
+
loadPanoramaImage,
|
|
21
|
+
planPanoramaDelivery,
|
|
22
|
+
schedulePanoramaUpgrade
|
|
23
|
+
} from "../chunk-AZODENEL.js";
|
|
11
24
|
|
|
12
25
|
// src/view3d/index.ts
|
|
13
26
|
import { Quat as Quat2, Vec3 as Vec33 } from "ogl";
|
|
@@ -110,6 +123,12 @@ var GLContext = class {
|
|
|
110
123
|
this.canvas.style.width = "100%";
|
|
111
124
|
this.canvas.style.height = "100%";
|
|
112
125
|
this.canvas.style.touchAction = "none";
|
|
126
|
+
this.canvas.tabIndex = 0;
|
|
127
|
+
this.canvas.setAttribute("role", "application");
|
|
128
|
+
this.canvas.setAttribute(
|
|
129
|
+
"aria-label",
|
|
130
|
+
"Interactive 3D venue. Drag or use arrow keys to look around; pinch, scroll, plus or minus to zoom."
|
|
131
|
+
);
|
|
113
132
|
this.renderer = new Renderer({
|
|
114
133
|
canvas: this.canvas,
|
|
115
134
|
dpr: computeDpr(),
|
|
@@ -218,6 +237,10 @@ var OrbitCamera = class {
|
|
|
218
237
|
/** Venue centre + radius, so pan can be clamped to somewhere still useful. */
|
|
219
238
|
this.panAnchor = new Vec3();
|
|
220
239
|
this.panLimit = 0;
|
|
240
|
+
/** What an unmodified primary-button / one-finger drag does. Section drill-in
|
|
241
|
+
* switches this to pan so a buyer can slide hidden seats into view without
|
|
242
|
+
* discovering a modifier gesture. Shift temporarily inverts the mode. */
|
|
243
|
+
this.primaryDragMode = "orbit";
|
|
221
244
|
this.camera = new Camera(gl, { fov: FOV, near: 0.1, far: 5e3, aspect: 1 });
|
|
222
245
|
this.canvas = canvas;
|
|
223
246
|
this.requestRender = requestRender;
|
|
@@ -229,7 +252,9 @@ var OrbitCamera = class {
|
|
|
229
252
|
}
|
|
230
253
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
231
254
|
if (this.activePointers.size === 1) {
|
|
232
|
-
|
|
255
|
+
const secondaryPan = e.button === 2 || e.button === 1;
|
|
256
|
+
const primaryPan = e.button === 0 && (e.shiftKey ? this.primaryDragMode === "orbit" : this.primaryDragMode === "pan");
|
|
257
|
+
this.panning = secondaryPan || primaryPan;
|
|
233
258
|
this.dragging = !this.panning;
|
|
234
259
|
this.lastX = e.clientX;
|
|
235
260
|
this.lastY = e.clientY;
|
|
@@ -295,15 +320,38 @@ var OrbitCamera = class {
|
|
|
295
320
|
this.onWheel = (e) => {
|
|
296
321
|
e.preventDefault();
|
|
297
322
|
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
298
|
-
const norm2 = e.deltaY * unit / 100;
|
|
299
|
-
this.dollyBy(Math.exp(norm2 * 0.
|
|
323
|
+
const norm2 = Math.max(-2, Math.min(2, e.deltaY * unit / 100));
|
|
324
|
+
this.dollyBy(Math.exp(norm2 * 0.22));
|
|
325
|
+
this.fireGesture();
|
|
326
|
+
};
|
|
327
|
+
this.onKeyDown = (e) => {
|
|
328
|
+
const step = 7 * DEG;
|
|
329
|
+
if (e.shiftKey && (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
330
|
+
this.panBy(
|
|
331
|
+
e.key === "ArrowLeft" ? -32 : e.key === "ArrowRight" ? 32 : 0,
|
|
332
|
+
e.key === "ArrowUp" ? -32 : e.key === "ArrowDown" ? 32 : 0
|
|
333
|
+
);
|
|
334
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
335
|
+
this.azT += e.key === "ArrowLeft" ? -step : step;
|
|
336
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
337
|
+
this.polT = Math.max(POLAR_MIN, Math.min(POLAR_MAX, this.polT + (e.key === "ArrowUp" ? -step : step)));
|
|
338
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
339
|
+
this.dollyBy(0.82);
|
|
340
|
+
} else if (e.key === "-" || e.key === "_") {
|
|
341
|
+
this.dollyBy(1.22);
|
|
342
|
+
} else {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
e.preventDefault();
|
|
300
346
|
this.fireGesture();
|
|
347
|
+
this.requestRender();
|
|
301
348
|
};
|
|
302
349
|
canvas.addEventListener("pointerdown", this.onPointerDown);
|
|
303
350
|
canvas.addEventListener("pointermove", this.onPointerMove);
|
|
304
351
|
canvas.addEventListener("pointerup", this.onPointerUp);
|
|
305
352
|
canvas.addEventListener("pointercancel", this.onPointerUp);
|
|
306
353
|
canvas.addEventListener("wheel", this.onWheel, { passive: false });
|
|
354
|
+
canvas.addEventListener("keydown", this.onKeyDown);
|
|
307
355
|
canvas.addEventListener("contextmenu", this.onContextMenu);
|
|
308
356
|
}
|
|
309
357
|
pinchCentroid() {
|
|
@@ -371,20 +419,32 @@ var OrbitCamera = class {
|
|
|
371
419
|
this.distT = Math.max(this.minDist, Math.min(this.maxDist, this.distT * factor));
|
|
372
420
|
this.requestRender();
|
|
373
421
|
}
|
|
422
|
+
/** Programmatic zoom used by the visible camera controls. Keeping it on the
|
|
423
|
+
* same dolly path as wheel, pinch and keyboard preserves all distance limits. */
|
|
424
|
+
zoomBy(factor) {
|
|
425
|
+
this.dollyBy(factor);
|
|
426
|
+
this.fireGesture();
|
|
427
|
+
}
|
|
428
|
+
/** Choose what a normal left/one-finger drag does. Shift temporarily performs
|
|
429
|
+
* the other action. This is intentionally public so the view can make the
|
|
430
|
+
* currently active gesture visible rather than hiding it in documentation. */
|
|
431
|
+
setPrimaryDragMode(mode) {
|
|
432
|
+
this.primaryDragMode = mode;
|
|
433
|
+
}
|
|
374
434
|
/**
|
|
375
435
|
* Fit a flattering 3/4 view to the bounds sphere. With `intro`, the camera
|
|
376
436
|
* STARTS nearly top-down (matching the 2D map's orientation) and further out,
|
|
377
437
|
* then the damped `update()` eases it up into the 3/4 architectural angle and
|
|
378
438
|
* dollies in — the venue "stands up" instead of teleporting (~600ms).
|
|
379
439
|
*/
|
|
380
|
-
frame(bounds, intro = false, stageAzimuth) {
|
|
440
|
+
frame(bounds, intro = false, stageAzimuth, portraitCrop = false) {
|
|
381
441
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
382
442
|
this.targetT.copy(this.target);
|
|
383
443
|
this.panAnchor.copy(this.target);
|
|
384
444
|
this.panLimit = Math.max(1, bounds.radius) * 1.5;
|
|
385
445
|
const r = Math.max(1, bounds.radius);
|
|
386
446
|
const halfV = this.fovY * DEG / 2;
|
|
387
|
-
const aspect = this.camera.aspect || 1;
|
|
447
|
+
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
388
448
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
389
449
|
const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
390
450
|
this.azT = stageAzimuth ?? -30 * DEG;
|
|
@@ -411,7 +471,7 @@ var OrbitCamera = class {
|
|
|
411
471
|
* seat, behind the shell, anywhere): re-pivot on the venue centre without
|
|
412
472
|
* moving the camera, then glide targets back to the 3/4 architectural pose.
|
|
413
473
|
*/
|
|
414
|
-
frameSoft(bounds, stageAzimuth) {
|
|
474
|
+
frameSoft(bounds, stageAzimuth, portraitCrop = false) {
|
|
415
475
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
416
476
|
this.targetT.copy(this.target);
|
|
417
477
|
this.panAnchor.copy(this.target);
|
|
@@ -420,7 +480,7 @@ var OrbitCamera = class {
|
|
|
420
480
|
this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
|
|
421
481
|
const r = Math.max(1, bounds.radius);
|
|
422
482
|
const halfV = this.fovY * DEG / 2;
|
|
423
|
-
const aspect = this.camera.aspect || 1;
|
|
483
|
+
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
424
484
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
425
485
|
const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
426
486
|
this.azT = stageAzimuth ?? this.azimuth;
|
|
@@ -493,6 +553,7 @@ var OrbitCamera = class {
|
|
|
493
553
|
this.canvas.removeEventListener("pointerup", this.onPointerUp);
|
|
494
554
|
this.canvas.removeEventListener("pointercancel", this.onPointerUp);
|
|
495
555
|
this.canvas.removeEventListener("wheel", this.onWheel);
|
|
556
|
+
this.canvas.removeEventListener("keydown", this.onKeyDown);
|
|
496
557
|
this.canvas.removeEventListener("contextmenu", this.onContextMenu);
|
|
497
558
|
this.activePointers.clear();
|
|
498
559
|
}
|
|
@@ -1349,6 +1410,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1349
1410
|
const iCategory = new Float32Array(count * 3);
|
|
1350
1411
|
const iMaxRadius = new Float32Array(count);
|
|
1351
1412
|
const iChairWidth = new Float32Array(count);
|
|
1413
|
+
const iPhysicalSeat = new Float32Array(count);
|
|
1352
1414
|
const iRing = new Float32Array(count * 3);
|
|
1353
1415
|
const idToIndex = /* @__PURE__ */ new Map();
|
|
1354
1416
|
const spacing = nearestNeighbourSpacing(seats);
|
|
@@ -1358,6 +1420,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1358
1420
|
const pitchM = (resolved ?? spacing[i]) * M;
|
|
1359
1421
|
iMaxRadius[i] = Number.isFinite(pitchM) ? Math.max(0.06, Math.min(SEAT_DOT_RADIUS_M, pitchM * SEAT_PITCH_FRACTION)) : SEAT_DOT_RADIUS_M;
|
|
1360
1422
|
iChairWidth[i] = chairHalfWidth(Number.isFinite(pitchM) ? pitchM : void 0);
|
|
1423
|
+
iPhysicalSeat[i] = seat.wheelchairSpaceType === "no-seat" ? 0 : 1;
|
|
1361
1424
|
iPosition[i * 3] = seat.x * M;
|
|
1362
1425
|
iPosition[i * 3 + 1] = surfaces ? surfaces.seatDeckY(i) : seatSurfaceY(seat);
|
|
1363
1426
|
iPosition[i * 3 + 2] = seat.y * M;
|
|
@@ -1385,6 +1448,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1385
1448
|
iRing,
|
|
1386
1449
|
idToIndex,
|
|
1387
1450
|
iChairWidth,
|
|
1451
|
+
iPhysicalSeat,
|
|
1388
1452
|
iFloor: seatFloor ?? new Float32Array(count),
|
|
1389
1453
|
iYaw: new Float32Array(count)
|
|
1390
1454
|
};
|
|
@@ -2420,15 +2484,22 @@ function buildVenueSurfaces(units, seats) {
|
|
|
2420
2484
|
}
|
|
2421
2485
|
|
|
2422
2486
|
// src/view3d/scene/sceneModel.ts
|
|
2423
|
-
function
|
|
2487
|
+
function shapeVisibleInConfiguration(shape, activeConfigurationId) {
|
|
2488
|
+
const ids = shape.eventConfigurationIds;
|
|
2489
|
+
return !ids?.length || !!activeConfigurationId && ids.includes(activeConfigurationId);
|
|
2490
|
+
}
|
|
2491
|
+
function visibleObjects(objects, activeConfigurationId) {
|
|
2492
|
+
return objects.filter((object) => object.type !== "shape" || shapeVisibleInConfiguration(object, activeConfigurationId));
|
|
2493
|
+
}
|
|
2494
|
+
function floorUnits(doc, activeConfigurationId) {
|
|
2424
2495
|
if (doc.floors?.length) {
|
|
2425
2496
|
return doc.floors.map((f) => ({
|
|
2426
|
-
objects: f.objects,
|
|
2497
|
+
objects: visibleObjects(f.objects, activeConfigurationId),
|
|
2427
2498
|
focal: f.focalPoint ?? doc.focalPoint,
|
|
2428
2499
|
baseHeightM: f.baseHeightM ?? 0
|
|
2429
2500
|
}));
|
|
2430
2501
|
}
|
|
2431
|
-
return [{ objects: doc.objects, focal: doc.focalPoint, baseHeightM: 0 }];
|
|
2502
|
+
return [{ objects: visibleObjects(doc.objects, activeConfigurationId), focal: doc.focalPoint, baseHeightM: 0 }];
|
|
2432
2503
|
}
|
|
2433
2504
|
var AO = { top: 1, wallBottom: 0.5, bottomCap: 0.4 };
|
|
2434
2505
|
function tintTop(fill, neutral) {
|
|
@@ -2709,23 +2780,114 @@ function buildTable(builder, table, base, fill, S) {
|
|
|
2709
2780
|
AO
|
|
2710
2781
|
);
|
|
2711
2782
|
}
|
|
2783
|
+
function rotateShapePolygon(poly, degrees) {
|
|
2784
|
+
if (!degrees) return poly;
|
|
2785
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
2786
|
+
for (const point of poly) {
|
|
2787
|
+
minX = Math.min(minX, point.x);
|
|
2788
|
+
maxX = Math.max(maxX, point.x);
|
|
2789
|
+
minY = Math.min(minY, point.y);
|
|
2790
|
+
maxY = Math.max(maxY, point.y);
|
|
2791
|
+
}
|
|
2792
|
+
const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
|
|
2793
|
+
const radians = degrees * Math.PI / 180;
|
|
2794
|
+
const cos = Math.cos(radians), sin = Math.sin(radians);
|
|
2795
|
+
return poly.map((point) => {
|
|
2796
|
+
const dx = point.x - cx, dy = point.y - cy;
|
|
2797
|
+
return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2712
2800
|
function shapePolygon(shape) {
|
|
2713
|
-
|
|
2801
|
+
let poly = null;
|
|
2802
|
+
if (shape.kind === "polygon" && shape.points && shape.points.length >= 3) poly = shape.points;
|
|
2714
2803
|
if (shape.kind === "rect" && shape.width && shape.height) {
|
|
2715
|
-
|
|
2804
|
+
poly = rectPolygon(shape.x ?? 0, shape.y ?? 0, shape.width, shape.height);
|
|
2716
2805
|
}
|
|
2717
2806
|
if (shape.kind === "ellipse" && shape.width && shape.height) {
|
|
2718
2807
|
const cx = (shape.x ?? 0) + shape.width / 2;
|
|
2719
2808
|
const cy = (shape.y ?? 0) + shape.height / 2;
|
|
2720
|
-
|
|
2809
|
+
poly = ellipsePolygon(cx, cy, shape.width / 2, shape.height / 2);
|
|
2721
2810
|
}
|
|
2722
|
-
return null;
|
|
2811
|
+
return poly ? rotateShapePolygon(poly, shape.rotation) : null;
|
|
2812
|
+
}
|
|
2813
|
+
function architectureForShape(shape) {
|
|
2814
|
+
const role = shape.role ?? "";
|
|
2815
|
+
const defaults = {
|
|
2816
|
+
stage: { kind: "stage", heightM: 1 },
|
|
2817
|
+
entrance: { kind: "portal", heightM: 2.7 },
|
|
2818
|
+
exit: { kind: "portal", heightM: 2.7 },
|
|
2819
|
+
screen: { kind: "solid", heightM: 6 },
|
|
2820
|
+
wall: { kind: "solid", heightM: 2.8 },
|
|
2821
|
+
rail: { kind: "solid", heightM: 1.1 },
|
|
2822
|
+
suite: { kind: "suite", heightM: 3 },
|
|
2823
|
+
obstruction: { kind: "solid", heightM: 4.5 },
|
|
2824
|
+
sound: { kind: "solid", heightM: 1.45 },
|
|
2825
|
+
restroom: { kind: "solid", heightM: 2.4 },
|
|
2826
|
+
bar: { kind: "solid", heightM: 1.1 },
|
|
2827
|
+
concession: { kind: "solid", heightM: 1.1 },
|
|
2828
|
+
coat: { kind: "solid", heightM: 1.1 }
|
|
2829
|
+
};
|
|
2830
|
+
const resolved = defaults[role] ?? { kind: "plate", heightM: 0.25 };
|
|
2831
|
+
return { ...resolved, heightM: shape.heightM ?? resolved.heightM };
|
|
2723
2832
|
}
|
|
2724
|
-
|
|
2833
|
+
var SHAPE_LAYER_LIFT_M = 0.08;
|
|
2834
|
+
function claimShapeTopOffset(shape, claimed) {
|
|
2835
|
+
const poly = shapePolygon(shape);
|
|
2836
|
+
const architecture = architectureForShape(shape);
|
|
2837
|
+
const defaultTop = architecture.heightM;
|
|
2838
|
+
if (!poly) return defaultTop;
|
|
2839
|
+
if (architecture.kind !== "plate" && architecture.kind !== "stage") return defaultTop;
|
|
2840
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2841
|
+
for (const p of poly) {
|
|
2842
|
+
minX = Math.min(minX, p.x);
|
|
2843
|
+
minY = Math.min(minY, p.y);
|
|
2844
|
+
maxX = Math.max(maxX, p.x);
|
|
2845
|
+
maxY = Math.max(maxY, p.y);
|
|
2846
|
+
}
|
|
2847
|
+
let topOffsetM = defaultTop;
|
|
2848
|
+
for (const prior of claimed) {
|
|
2849
|
+
const overlaps = maxX > prior.minX && minX < prior.maxX && maxY > prior.minY && minY < prior.maxY;
|
|
2850
|
+
if (overlaps && topOffsetM <= prior.topOffsetM + 1e-6) {
|
|
2851
|
+
topOffsetM = prior.topOffsetM + SHAPE_LAYER_LIFT_M;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
claimed.push({ minX, minY, maxX, maxY, topOffsetM });
|
|
2855
|
+
return topOffsetM;
|
|
2856
|
+
}
|
|
2857
|
+
function lerpPoint(a, b, t) {
|
|
2858
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
2859
|
+
}
|
|
2860
|
+
function orientQuadLong(poly) {
|
|
2861
|
+
if (poly.length !== 4) return poly;
|
|
2862
|
+
const edge0 = Math.hypot(poly[1].x - poly[0].x, poly[1].y - poly[0].y);
|
|
2863
|
+
const edge1 = Math.hypot(poly[2].x - poly[1].x, poly[2].y - poly[1].y);
|
|
2864
|
+
return edge0 >= edge1 ? poly : [poly[1], poly[2], poly[3], poly[0]];
|
|
2865
|
+
}
|
|
2866
|
+
function quadWidthSlice(poly, t0, t1) {
|
|
2867
|
+
return [
|
|
2868
|
+
lerpPoint(poly[0], poly[1], t0),
|
|
2869
|
+
lerpPoint(poly[0], poly[1], t1),
|
|
2870
|
+
lerpPoint(poly[3], poly[2], t1),
|
|
2871
|
+
lerpPoint(poly[3], poly[2], t0)
|
|
2872
|
+
];
|
|
2873
|
+
}
|
|
2874
|
+
function quadDepthSlice(poly, t0, t1) {
|
|
2875
|
+
return [
|
|
2876
|
+
lerpPoint(poly[0], poly[3], t0),
|
|
2877
|
+
lerpPoint(poly[1], poly[2], t0),
|
|
2878
|
+
lerpPoint(poly[1], poly[2], t1),
|
|
2879
|
+
lerpPoint(poly[0], poly[3], t1)
|
|
2880
|
+
];
|
|
2881
|
+
}
|
|
2882
|
+
function emitArchitecturalPrism(builder, poly, top, bottom, colTop, colWall) {
|
|
2883
|
+
extrudePrism(builder, poly, void 0, () => top, bottom, colTop, colWall, AO);
|
|
2884
|
+
}
|
|
2885
|
+
function buildShape(builder, shape, base, topOffsetM, S, focal, stages) {
|
|
2725
2886
|
const poly = shapePolygon(shape);
|
|
2726
2887
|
if (!poly) return;
|
|
2727
2888
|
const isStage = shape.role === "stage";
|
|
2728
|
-
const
|
|
2889
|
+
const architecture = architectureForShape(shape);
|
|
2890
|
+
const height = base + topOffsetM;
|
|
2729
2891
|
if (isStage && stages) {
|
|
2730
2892
|
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
2731
2893
|
for (const pt of poly) {
|
|
@@ -2743,8 +2905,29 @@ function buildShape(builder, shape, base, S, stages) {
|
|
|
2743
2905
|
});
|
|
2744
2906
|
}
|
|
2745
2907
|
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
2746
|
-
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
2747
|
-
|
|
2908
|
+
const colWall = isStage ? S.stageWall : shape.role === "screen" ? [0.24, 0.46, 0.82] : tintTop(hexToRgb(shape.fill), S.decorWall);
|
|
2909
|
+
if (architecture.kind === "portal" && poly.length === 4) {
|
|
2910
|
+
const quad = orientQuadLong(poly);
|
|
2911
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.16), height, base, colTop, colWall);
|
|
2912
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0.84, 1), height, base, colTop, colWall);
|
|
2913
|
+
emitArchitecturalPrism(builder, quad, height, Math.max(base, height - 0.35), colTop, colWall);
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
if (architecture.kind === "suite" && poly.length === 4) {
|
|
2917
|
+
const edgeDistances = poly.map((point, index) => {
|
|
2918
|
+
const next = poly[(index + 1) % 4];
|
|
2919
|
+
const mx = (point.x + next.x) / 2, my = (point.y + next.y) / 2;
|
|
2920
|
+
return Math.hypot(mx - focal.x, my - focal.y);
|
|
2921
|
+
});
|
|
2922
|
+
const front = edgeDistances.indexOf(Math.min(...edgeDistances));
|
|
2923
|
+
const quad = [0, 1, 2, 3].map((offset) => poly[(front + offset) % 4]);
|
|
2924
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.08), height, base, colTop, colWall);
|
|
2925
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0.92, 1), height, base, colTop, colWall);
|
|
2926
|
+
emitArchitecturalPrism(builder, quadDepthSlice(quad, 0.88, 1), height, base, colTop, colWall);
|
|
2927
|
+
emitArchitecturalPrism(builder, quad, height, Math.max(base, height - 0.18), colTop, colWall);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
emitArchitecturalPrism(builder, poly, height, base, colTop, colWall);
|
|
2748
2931
|
}
|
|
2749
2932
|
function decorPolygon(decor) {
|
|
2750
2933
|
const { x, y, width, height } = decor;
|
|
@@ -2842,7 +3025,7 @@ function buildSceneModel(input) {
|
|
|
2842
3025
|
const { doc, seats } = input;
|
|
2843
3026
|
const theme = resolveTheme3D(doc.theme);
|
|
2844
3027
|
const S = theme.structure;
|
|
2845
|
-
const units = floorUnits(doc);
|
|
3028
|
+
const units = floorUnits(doc, input.eventConfigurationId ?? doc.eventConfigurationId);
|
|
2846
3029
|
const builder = new MeshBuilder();
|
|
2847
3030
|
const fp = chartFootprint(units, seats);
|
|
2848
3031
|
const padU = Math.max(60, (fp.maxX - fp.minX + fp.maxY - fp.minY) * 0.06);
|
|
@@ -2858,10 +3041,19 @@ function buildSceneModel(input) {
|
|
|
2858
3041
|
const unit = units[unitIndex];
|
|
2859
3042
|
builder.setFloor(unitIndex);
|
|
2860
3043
|
const claimed = new ClaimedArea();
|
|
3044
|
+
const claimedShapeLayers = [];
|
|
2861
3045
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
2862
3046
|
for (const o of unit.objects) {
|
|
2863
3047
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
2864
|
-
else if (o.type === "shape") buildShape(
|
|
3048
|
+
else if (o.type === "shape") buildShape(
|
|
3049
|
+
builder,
|
|
3050
|
+
o,
|
|
3051
|
+
unit.baseHeightM,
|
|
3052
|
+
claimShapeTopOffset(o, claimedShapeLayers),
|
|
3053
|
+
S,
|
|
3054
|
+
unit.focal,
|
|
3055
|
+
stageBounds
|
|
3056
|
+
);
|
|
2865
3057
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2866
3058
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2867
3059
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
@@ -2931,6 +3123,7 @@ function buildSceneModel(input) {
|
|
|
2931
3123
|
}
|
|
2932
3124
|
const labels = [];
|
|
2933
3125
|
const sections = [];
|
|
3126
|
+
const rows = [];
|
|
2934
3127
|
for (const z of zones) {
|
|
2935
3128
|
if (z.seatCount === 0) continue;
|
|
2936
3129
|
labels.push({
|
|
@@ -3027,6 +3220,49 @@ function buildSceneModel(input) {
|
|
|
3027
3220
|
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
|
|
3028
3221
|
});
|
|
3029
3222
|
}
|
|
3223
|
+
const rowAcc = /* @__PURE__ */ new Map();
|
|
3224
|
+
for (let i = 0; i < seats.length; i++) {
|
|
3225
|
+
const seat = seats[i];
|
|
3226
|
+
const cut = seat.label ? seat.label.lastIndexOf("-") : -1;
|
|
3227
|
+
let acc = rowAcc.get(seat.rowId);
|
|
3228
|
+
if (!acc) {
|
|
3229
|
+
acc = {
|
|
3230
|
+
n: 0,
|
|
3231
|
+
x: 0,
|
|
3232
|
+
y: 0,
|
|
3233
|
+
z: 0,
|
|
3234
|
+
minX: Infinity,
|
|
3235
|
+
maxX: -Infinity,
|
|
3236
|
+
minZ: Infinity,
|
|
3237
|
+
maxZ: -Infinity,
|
|
3238
|
+
label: cut > 0 ? seat.label.slice(0, cut) : seat.rowId,
|
|
3239
|
+
sectionId: seat.sectionId
|
|
3240
|
+
};
|
|
3241
|
+
rowAcc.set(seat.rowId, acc);
|
|
3242
|
+
}
|
|
3243
|
+
const offset = i * 3;
|
|
3244
|
+
const x = seatData.iPosition[offset];
|
|
3245
|
+
const y = seatData.iPosition[offset + 1];
|
|
3246
|
+
const z = seatData.iPosition[offset + 2];
|
|
3247
|
+
acc.n++;
|
|
3248
|
+
acc.x += x;
|
|
3249
|
+
acc.y += y;
|
|
3250
|
+
acc.z += z;
|
|
3251
|
+
acc.minX = Math.min(acc.minX, x);
|
|
3252
|
+
acc.maxX = Math.max(acc.maxX, x);
|
|
3253
|
+
acc.minZ = Math.min(acc.minZ, z);
|
|
3254
|
+
acc.maxZ = Math.max(acc.maxZ, z);
|
|
3255
|
+
}
|
|
3256
|
+
for (const [id, acc] of rowAcc) {
|
|
3257
|
+
rows.push({
|
|
3258
|
+
id,
|
|
3259
|
+
label: acc.label,
|
|
3260
|
+
sectionId: acc.sectionId,
|
|
3261
|
+
seatCount: acc.n,
|
|
3262
|
+
center: [acc.x / acc.n, acc.y / acc.n, acc.z / acc.n],
|
|
3263
|
+
radius: Math.max(0.8, Math.hypot(acc.maxX - acc.minX, acc.maxZ - acc.minZ) * 0.5)
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3030
3266
|
if (seats.length <= SEAT_LABEL_MAX) {
|
|
3031
3267
|
for (let i = 0; i < seats.length; i++) {
|
|
3032
3268
|
const s = seats[i];
|
|
@@ -3043,7 +3279,18 @@ function buildSceneModel(input) {
|
|
|
3043
3279
|
}
|
|
3044
3280
|
for (const unit of units) {
|
|
3045
3281
|
for (const o of unit.objects) {
|
|
3046
|
-
if (o.type === "
|
|
3282
|
+
if (o.type === "shape" && o.role && ["entrance", "exit", "screen", "suite", "obstruction"].includes(o.role)) {
|
|
3283
|
+
const poly = shapePolygon(o);
|
|
3284
|
+
const centre = poly ? centroidOf(poly) : null;
|
|
3285
|
+
if (!centre || !o.label) continue;
|
|
3286
|
+
labels.push({
|
|
3287
|
+
id: `architecture:${o.id}`,
|
|
3288
|
+
kind: "annotation",
|
|
3289
|
+
text: o.label,
|
|
3290
|
+
anchor: [centre.x * M, unit.baseHeightM + architectureForShape(o).heightM + ANNOTATION_LIFT_M, centre.y * M],
|
|
3291
|
+
color: o.fill
|
|
3292
|
+
});
|
|
3293
|
+
} else if (o.type === "text") {
|
|
3047
3294
|
if (!o.text) continue;
|
|
3048
3295
|
labels.push({
|
|
3049
3296
|
id: `text:${o.id}`,
|
|
@@ -3125,6 +3372,7 @@ function buildSceneModel(input) {
|
|
|
3125
3372
|
zones,
|
|
3126
3373
|
stages: stageBounds,
|
|
3127
3374
|
sections,
|
|
3375
|
+
rows,
|
|
3128
3376
|
labels,
|
|
3129
3377
|
floors
|
|
3130
3378
|
};
|
|
@@ -3158,6 +3406,7 @@ var LabelOverlay = class {
|
|
|
3158
3406
|
constructor(container, opts = {}) {
|
|
3159
3407
|
this.nodes = /* @__PURE__ */ new Map();
|
|
3160
3408
|
this.labels = [];
|
|
3409
|
+
this.forcedDense = false;
|
|
3161
3410
|
this.opts = opts;
|
|
3162
3411
|
this.root = document.createElement("div");
|
|
3163
3412
|
this.root.setAttribute("data-view3d-labels", "");
|
|
@@ -3192,6 +3441,11 @@ var LabelOverlay = class {
|
|
|
3192
3441
|
}
|
|
3193
3442
|
}
|
|
3194
3443
|
}
|
|
3444
|
+
/** Section/row drill-in makes their dense labels explicit, regardless of the
|
|
3445
|
+
* whole-venue distance heuristic used during free orbit. */
|
|
3446
|
+
setForcedDense(enabled) {
|
|
3447
|
+
this.forcedDense = enabled;
|
|
3448
|
+
}
|
|
3195
3449
|
/**
|
|
3196
3450
|
* Reposition every label for the current camera.
|
|
3197
3451
|
*
|
|
@@ -3200,6 +3454,10 @@ var LabelOverlay = class {
|
|
|
3200
3454
|
update(viewProjection, width, height, cameraDistance, venueRadius, cameraWorld) {
|
|
3201
3455
|
if (!this.labels.length) return;
|
|
3202
3456
|
const kinds = visibleLabelKinds(cameraDistance, venueRadius);
|
|
3457
|
+
if (this.forcedDense) {
|
|
3458
|
+
kinds.add("row");
|
|
3459
|
+
kinds.add("seat");
|
|
3460
|
+
}
|
|
3203
3461
|
const candidates = [];
|
|
3204
3462
|
for (const label of this.labels) {
|
|
3205
3463
|
if (!kinds.has(label.kind)) continue;
|
|
@@ -3348,6 +3606,7 @@ in vec2 position; // quad corner in [-1,1]
|
|
|
3348
3606
|
in vec3 iOffset; // per-instance world position
|
|
3349
3607
|
in vec3 iColor; // per-instance state colour (resolved CPU-side)
|
|
3350
3608
|
in float iMaxRadius; // per-instance world-radius ceiling (seat pitch derived)
|
|
3609
|
+
in float iPhysicalSeat;// 1 = chair; 0 = empty wheelchair bay
|
|
3351
3610
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3352
3611
|
in float iFloor; // owning floor index
|
|
3353
3612
|
uniform mat4 modelViewMatrix;
|
|
@@ -3365,6 +3624,7 @@ out float vBudget; // 1 = dot holds its minimum pixel size, <1 = it cannot
|
|
|
3365
3624
|
out vec3 vRing;
|
|
3366
3625
|
out float vDim;
|
|
3367
3626
|
out float vDotWeight; // 1 = the dot IS this seat, 0 = the chair has taken over
|
|
3627
|
+
out float vPhysicalSeat;
|
|
3368
3628
|
${CHAIR_WEIGHT_GLSL}
|
|
3369
3629
|
void main() {
|
|
3370
3630
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -3394,6 +3654,7 @@ void main() {
|
|
|
3394
3654
|
vUv = position;
|
|
3395
3655
|
vColor = iColor;
|
|
3396
3656
|
vRing = iRing;
|
|
3657
|
+
vPhysicalSeat = iPhysicalSeat;
|
|
3397
3658
|
vDim = (uFocusFloor < -0.5 || abs(iFloor - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
3398
3659
|
gl_Position = projectionMatrix * mv;
|
|
3399
3660
|
}`
|
|
@@ -3408,11 +3669,13 @@ in float vBudget;
|
|
|
3408
3669
|
in vec3 vRing;
|
|
3409
3670
|
in float vDim;
|
|
3410
3671
|
in float vDotWeight;
|
|
3672
|
+
in float vPhysicalSeat;
|
|
3411
3673
|
uniform float uSeatFade; // fade toward tier colour with distance (LOD)
|
|
3412
3674
|
uniform vec3 uFadeColor;
|
|
3413
3675
|
out vec4 fragColor;
|
|
3414
3676
|
void main() {
|
|
3415
|
-
|
|
3677
|
+
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
3678
|
+
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3416
3679
|
if (d > 1.0) discard;
|
|
3417
3680
|
float alpha = smoothstep(1.0, 0.72, d);
|
|
3418
3681
|
float shade = 0.80 + 0.28 * (0.5 - vUv.y * 0.5); // subtle top-lit
|
|
@@ -3454,6 +3717,7 @@ in float iYaw; // per-instance facing, radians (local +Z -> facing dir)
|
|
|
3454
3717
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3455
3718
|
in float iFloor;
|
|
3456
3719
|
in float iSeed; // <0 = seat is empty; else per-person hash in [0,1)
|
|
3720
|
+
in float iPhysicalSeat;// 1 = chair; 0 = empty wheelchair bay
|
|
3457
3721
|
uniform mat4 modelViewMatrix;
|
|
3458
3722
|
uniform mat4 projectionMatrix;
|
|
3459
3723
|
uniform float uChairFull;
|
|
@@ -3489,7 +3753,9 @@ void main() {
|
|
|
3489
3753
|
float occupant = step(2.5, part);
|
|
3490
3754
|
float taken = step(0.0, iSeed);
|
|
3491
3755
|
vOccupant = occupant;
|
|
3492
|
-
if (
|
|
3756
|
+
if (iPhysicalSeat < 0.5) {
|
|
3757
|
+
p = vec3(0.0); // sellable space, but no physical chair/person
|
|
3758
|
+
} else if (occupant > 0.5) {
|
|
3493
3759
|
if (taken < 0.5) {
|
|
3494
3760
|
p = vec3(0.0); // empty seat: no person
|
|
3495
3761
|
} else {
|
|
@@ -3632,6 +3898,7 @@ precision highp float;
|
|
|
3632
3898
|
in vec2 position;
|
|
3633
3899
|
in vec3 iOffset;
|
|
3634
3900
|
in float iMaxRadius;
|
|
3901
|
+
in float iPhysicalSeat;
|
|
3635
3902
|
uniform mat4 modelViewMatrix;
|
|
3636
3903
|
uniform mat4 projectionMatrix;
|
|
3637
3904
|
uniform float uSeatRadius;
|
|
@@ -3639,6 +3906,7 @@ uniform float uSeatScale;
|
|
|
3639
3906
|
uniform float uMinPixels;
|
|
3640
3907
|
uniform float uPixelToWorld;
|
|
3641
3908
|
out vec2 vUv;
|
|
3909
|
+
out float vPhysicalSeat;
|
|
3642
3910
|
flat out vec3 vPick;
|
|
3643
3911
|
void main() {
|
|
3644
3912
|
int id = gl_InstanceID + 1; // 0 reserved for no-hit
|
|
@@ -3651,6 +3919,7 @@ void main() {
|
|
|
3651
3919
|
// Must match SEAT_VERT exactly, or the hit mask drifts off the drawn dot.
|
|
3652
3920
|
mv.xy += normalize(vec3(modelViewMatrix * vec4(0.0, 1.0, 0.0, 0.0))).xy * r;
|
|
3653
3921
|
vUv = position;
|
|
3922
|
+
vPhysicalSeat = iPhysicalSeat;
|
|
3654
3923
|
gl_Position = projectionMatrix * mv;
|
|
3655
3924
|
}`
|
|
3656
3925
|
);
|
|
@@ -3659,10 +3928,12 @@ var SEAT_PICK_FRAG = (
|
|
|
3659
3928
|
`#version 300 es
|
|
3660
3929
|
precision highp float;
|
|
3661
3930
|
in vec2 vUv;
|
|
3931
|
+
in float vPhysicalSeat;
|
|
3662
3932
|
flat in vec3 vPick;
|
|
3663
3933
|
out vec4 fragColor;
|
|
3664
3934
|
void main() {
|
|
3665
|
-
|
|
3935
|
+
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3936
|
+
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
3666
3937
|
fragColor = vec4(vPick, 1.0);
|
|
3667
3938
|
}`
|
|
3668
3939
|
);
|
|
@@ -3824,6 +4095,7 @@ function buildGpuScene(gl, model) {
|
|
|
3824
4095
|
// Per-seat world-radius ceiling: what stops distant rows merging into one
|
|
3825
4096
|
// mass when the shader grows a dot to hold its minimum pixel size.
|
|
3826
4097
|
iMaxRadius: { size: 1, data: model.seats.iMaxRadius, instanced: 1 },
|
|
4098
|
+
iPhysicalSeat: { size: 1, data: model.seats.iPhysicalSeat, instanced: 1 },
|
|
3827
4099
|
// Accommodation ring colour; (0,0,0) means the seat carries no access type.
|
|
3828
4100
|
iRing: { size: 3, data: model.seats.iRing, instanced: 1 },
|
|
3829
4101
|
iFloor: { size: 1, data: model.seats.iFloor, instanced: 1 }
|
|
@@ -3841,6 +4113,7 @@ function buildGpuScene(gl, model) {
|
|
|
3841
4113
|
const cYaw = new Float32Array(CAP);
|
|
3842
4114
|
const cRing = new Float32Array(CAP * 3);
|
|
3843
4115
|
const cFloor = new Float32Array(CAP);
|
|
4116
|
+
const cPhysicalSeat = new Float32Array(CAP);
|
|
3844
4117
|
const cSeed = new Float32Array(CAP);
|
|
3845
4118
|
const chairGeo = new Geometry(gl, {
|
|
3846
4119
|
position: { size: 3, data: chairBase.position },
|
|
@@ -3853,6 +4126,7 @@ function buildGpuScene(gl, model) {
|
|
|
3853
4126
|
iYaw: { size: 1, data: cYaw, instanced: 1 },
|
|
3854
4127
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
3855
4128
|
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4129
|
+
iPhysicalSeat: { size: 1, data: cPhysicalSeat, instanced: 1 },
|
|
3856
4130
|
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
3857
4131
|
});
|
|
3858
4132
|
const chairMesh = new Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
@@ -3909,6 +4183,7 @@ function buildGpuScene(gl, model) {
|
|
|
3909
4183
|
cRing[k * 3 + 1] = src.iRing[i * 3 + 1];
|
|
3910
4184
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
3911
4185
|
cFloor[k] = src.iFloor[i];
|
|
4186
|
+
cPhysicalSeat[k] = src.iPhysicalSeat[i];
|
|
3912
4187
|
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
3913
4188
|
}
|
|
3914
4189
|
writeChairColors();
|
|
@@ -3917,6 +4192,7 @@ function buildGpuScene(gl, model) {
|
|
|
3917
4192
|
chairGeo.attributes.iYaw.needsUpdate = true;
|
|
3918
4193
|
chairGeo.attributes.iRing.needsUpdate = true;
|
|
3919
4194
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4195
|
+
chairGeo.attributes.iPhysicalSeat.needsUpdate = true;
|
|
3920
4196
|
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
3921
4197
|
chairGeo.instancedCount = n;
|
|
3922
4198
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
@@ -4247,192 +4523,17 @@ var Cinematic = class {
|
|
|
4247
4523
|
}
|
|
4248
4524
|
};
|
|
4249
4525
|
|
|
4250
|
-
// src/view3d/
|
|
4251
|
-
var
|
|
4252
|
-
var
|
|
4253
|
-
function
|
|
4254
|
-
const col = (0.5 + bearingDeg / 360) * bgW;
|
|
4255
|
-
return viewportW / 2 - col;
|
|
4256
|
-
}
|
|
4257
|
-
function windowedBgHeight(viewportH, vfovDeg = VFOV_DEG) {
|
|
4258
|
-
return viewportH * (180 / vfovDeg);
|
|
4259
|
-
}
|
|
4260
|
-
function horizonOffsetPy(viewportH, bgH, pitchPx) {
|
|
4261
|
-
return (viewportH - bgH) / 2 + clampPitchPx(pitchPx, bgH);
|
|
4262
|
-
}
|
|
4263
|
-
function clampPitchPx(pitchPx, bgH) {
|
|
4264
|
-
const limit = MAX_PITCH_DEG / 180 * bgH;
|
|
4265
|
-
return Math.max(-limit, Math.min(limit, pitchPx));
|
|
4266
|
-
}
|
|
4267
|
-
function mountPanorama(container, view, opts = {}) {
|
|
4268
|
-
const fadeMs = opts.fadeMs ?? 400;
|
|
4269
|
-
const bearing = view.initialBearingDeg ?? 0;
|
|
4270
|
-
const root = document.createElement("div");
|
|
4271
|
-
root.setAttribute("role", "dialog");
|
|
4272
|
-
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
4273
|
-
Object.assign(root.style, {
|
|
4274
|
-
position: "absolute",
|
|
4275
|
-
inset: "0",
|
|
4276
|
-
zIndex: "10",
|
|
4277
|
-
opacity: "0",
|
|
4278
|
-
transition: `opacity ${fadeMs}ms ease`,
|
|
4279
|
-
background: "#05070c",
|
|
4280
|
-
overflow: "hidden",
|
|
4281
|
-
touchAction: "none"
|
|
4282
|
-
});
|
|
4283
|
-
const pano = document.createElement("div");
|
|
4284
|
-
Object.assign(pano.style, {
|
|
4285
|
-
position: "absolute",
|
|
4286
|
-
inset: "0",
|
|
4287
|
-
backgroundImage: `url("${view.url}")`,
|
|
4288
|
-
backgroundRepeat: "repeat-x",
|
|
4289
|
-
cursor: "grab"
|
|
4290
|
-
});
|
|
4291
|
-
root.appendChild(pano);
|
|
4292
|
-
const closeBtn = document.createElement("button");
|
|
4293
|
-
closeBtn.type = "button";
|
|
4294
|
-
closeBtn.setAttribute("aria-label", "Close");
|
|
4295
|
-
closeBtn.textContent = "\u2715";
|
|
4296
|
-
Object.assign(closeBtn.style, {
|
|
4297
|
-
position: "absolute",
|
|
4298
|
-
top: "12px",
|
|
4299
|
-
right: "12px",
|
|
4300
|
-
zIndex: "2",
|
|
4301
|
-
width: "34px",
|
|
4302
|
-
height: "34px",
|
|
4303
|
-
borderRadius: "999px",
|
|
4304
|
-
cursor: "pointer",
|
|
4305
|
-
border: "1px solid rgba(255,255,255,0.25)",
|
|
4306
|
-
background: "rgba(8,12,18,0.6)",
|
|
4307
|
-
color: "#e6edf3",
|
|
4308
|
-
fontSize: "15px",
|
|
4309
|
-
lineHeight: "1"
|
|
4310
|
-
});
|
|
4311
|
-
root.appendChild(closeBtn);
|
|
4312
|
-
const hint = document.createElement("div");
|
|
4313
|
-
hint.textContent = "Drag to look around \xB7 Esc to close";
|
|
4314
|
-
Object.assign(hint.style, {
|
|
4315
|
-
position: "absolute",
|
|
4316
|
-
bottom: "12px",
|
|
4317
|
-
left: "0",
|
|
4318
|
-
right: "0",
|
|
4319
|
-
textAlign: "center",
|
|
4320
|
-
color: "rgba(230,237,243,0.7)",
|
|
4321
|
-
font: "12px ui-sans-serif, system-ui, sans-serif",
|
|
4322
|
-
pointerEvents: "none"
|
|
4323
|
-
});
|
|
4324
|
-
root.appendChild(hint);
|
|
4325
|
-
container.appendChild(root);
|
|
4326
|
-
let bgW = 0;
|
|
4327
|
-
let bgH = 0;
|
|
4328
|
-
let posX = 0;
|
|
4329
|
-
let pitchPx = 0;
|
|
4330
|
-
const layout = () => {
|
|
4331
|
-
const vh = root.clientHeight || 1;
|
|
4332
|
-
const vw = root.clientWidth || 1;
|
|
4333
|
-
const natW = img.naturalWidth || vw * 2;
|
|
4334
|
-
const natH = img.naturalHeight || vh;
|
|
4335
|
-
bgH = windowedBgHeight(vh);
|
|
4336
|
-
bgW = bgH * (natW / natH);
|
|
4337
|
-
pano.style.backgroundSize = `${bgW}px ${bgH}px`;
|
|
4338
|
-
if (!posInitialised) {
|
|
4339
|
-
posX = bearingToOffsetPx(bearing, vw, bgW);
|
|
4340
|
-
posInitialised = true;
|
|
4341
|
-
}
|
|
4342
|
-
pitchPx = clampPitchPx(pitchPx, bgH);
|
|
4343
|
-
pano.style.backgroundPosition = `${posX}px ${horizonOffsetPy(vh, bgH, pitchPx)}px`;
|
|
4344
|
-
};
|
|
4345
|
-
let posInitialised = false;
|
|
4346
|
-
const applyPos = () => {
|
|
4347
|
-
const vh = root.clientHeight || 1;
|
|
4348
|
-
pitchPx = clampPitchPx(pitchPx, bgH);
|
|
4349
|
-
pano.style.backgroundPosition = `${posX}px ${horizonOffsetPy(vh, bgH, pitchPx)}px`;
|
|
4350
|
-
};
|
|
4351
|
-
const img = new Image();
|
|
4352
|
-
img.onload = layout;
|
|
4353
|
-
img.src = view.url;
|
|
4354
|
-
requestAnimationFrame(layout);
|
|
4355
|
-
let dragging = false;
|
|
4356
|
-
let lastX = 0;
|
|
4357
|
-
let lastY = 0;
|
|
4358
|
-
const onDown = (e) => {
|
|
4359
|
-
dragging = true;
|
|
4360
|
-
lastX = e.clientX;
|
|
4361
|
-
lastY = e.clientY;
|
|
4362
|
-
pano.style.cursor = "grabbing";
|
|
4363
|
-
try {
|
|
4364
|
-
pano.setPointerCapture?.(e.pointerId);
|
|
4365
|
-
} catch {
|
|
4366
|
-
}
|
|
4367
|
-
};
|
|
4368
|
-
const onMove = (e) => {
|
|
4369
|
-
if (!dragging) return;
|
|
4370
|
-
posX += e.clientX - lastX;
|
|
4371
|
-
pitchPx += e.clientY - lastY;
|
|
4372
|
-
lastX = e.clientX;
|
|
4373
|
-
lastY = e.clientY;
|
|
4374
|
-
applyPos();
|
|
4375
|
-
};
|
|
4376
|
-
const onUp = (e) => {
|
|
4377
|
-
dragging = false;
|
|
4378
|
-
pano.style.cursor = "grab";
|
|
4379
|
-
try {
|
|
4380
|
-
pano.releasePointerCapture?.(e.pointerId);
|
|
4381
|
-
} catch {
|
|
4382
|
-
}
|
|
4383
|
-
};
|
|
4384
|
-
pano.addEventListener("pointerdown", onDown);
|
|
4385
|
-
pano.addEventListener("pointermove", onMove);
|
|
4386
|
-
pano.addEventListener("pointerup", onUp);
|
|
4387
|
-
pano.addEventListener("pointercancel", onUp);
|
|
4388
|
-
let closed = false;
|
|
4389
|
-
let disposed = false;
|
|
4390
|
-
let fadeTimer = 0;
|
|
4391
|
-
const removeListeners = () => {
|
|
4392
|
-
pano.removeEventListener("pointerdown", onDown);
|
|
4393
|
-
pano.removeEventListener("pointermove", onMove);
|
|
4394
|
-
pano.removeEventListener("pointerup", onUp);
|
|
4395
|
-
pano.removeEventListener("pointercancel", onUp);
|
|
4396
|
-
window.removeEventListener("keydown", onKey);
|
|
4397
|
-
};
|
|
4398
|
-
const teardown = () => {
|
|
4399
|
-
if (fadeTimer) {
|
|
4400
|
-
window.clearTimeout(fadeTimer);
|
|
4401
|
-
fadeTimer = 0;
|
|
4402
|
-
}
|
|
4403
|
-
removeListeners();
|
|
4404
|
-
if (root.parentNode) root.parentNode.removeChild(root);
|
|
4405
|
-
};
|
|
4406
|
-
const close = () => {
|
|
4407
|
-
if (closed) return;
|
|
4408
|
-
closed = true;
|
|
4409
|
-
root.style.opacity = "0";
|
|
4410
|
-
const done = () => {
|
|
4411
|
-
fadeTimer = 0;
|
|
4412
|
-
if (disposed) return;
|
|
4413
|
-
teardown();
|
|
4414
|
-
opts.onClose?.();
|
|
4415
|
-
};
|
|
4416
|
-
fadeTimer = window.setTimeout(done, fadeMs);
|
|
4417
|
-
};
|
|
4418
|
-
const onKey = (e) => {
|
|
4419
|
-
if (e.key === "Escape") {
|
|
4420
|
-
e.stopPropagation();
|
|
4421
|
-
close();
|
|
4422
|
-
}
|
|
4423
|
-
};
|
|
4424
|
-
window.addEventListener("keydown", onKey);
|
|
4425
|
-
closeBtn.addEventListener("click", close);
|
|
4426
|
-
requestAnimationFrame(() => {
|
|
4427
|
-
root.style.opacity = "1";
|
|
4428
|
-
});
|
|
4526
|
+
// src/view3d/camera/seatViewPose.ts
|
|
4527
|
+
var SEAT_EYE_ABOVE_DECK_M = 1.02;
|
|
4528
|
+
var FOCAL_LOOK_HEIGHT_M = 1.5;
|
|
4529
|
+
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0) {
|
|
4429
4530
|
return {
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4531
|
+
eye: [seatDeckWorld[0], seatDeckWorld[1] + SEAT_EYE_ABOVE_DECK_M, seatDeckWorld[2]],
|
|
4532
|
+
focal: [
|
|
4533
|
+
focalPoint.x * M,
|
|
4534
|
+
floorBaseHeightM + FOCAL_LOOK_HEIGHT_M,
|
|
4535
|
+
focalPoint.y * M
|
|
4536
|
+
]
|
|
4436
4537
|
};
|
|
4437
4538
|
}
|
|
4438
4539
|
|
|
@@ -4557,6 +4658,10 @@ function createPanoSphere(gl, image) {
|
|
|
4557
4658
|
setOpacity(value) {
|
|
4558
4659
|
program.uniforms.uOpacity.value = Math.max(0, Math.min(1, value));
|
|
4559
4660
|
},
|
|
4661
|
+
setImage(nextImage) {
|
|
4662
|
+
texture.image = nextImage;
|
|
4663
|
+
texture.needsUpdate = true;
|
|
4664
|
+
},
|
|
4560
4665
|
dispose() {
|
|
4561
4666
|
mesh.setParent(null);
|
|
4562
4667
|
geometry.remove();
|
|
@@ -4568,20 +4673,29 @@ function createPanoSphere(gl, image) {
|
|
|
4568
4673
|
|
|
4569
4674
|
// src/view3d/crossfade/panoramaSphere.ts
|
|
4570
4675
|
var DEG_PER_PX = 0.15;
|
|
4571
|
-
function loadImage(url) {
|
|
4572
|
-
return new Promise((resolve, reject) => {
|
|
4573
|
-
const img = new Image();
|
|
4574
|
-
img.crossOrigin = "anonymous";
|
|
4575
|
-
img.onload = () => resolve(img);
|
|
4576
|
-
img.onerror = () => reject(new Error("panorama_image_failed"));
|
|
4577
|
-
img.src = url;
|
|
4578
|
-
});
|
|
4579
|
-
}
|
|
4580
4676
|
async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
4581
4677
|
const fadeMs = opts.fadeMs ?? 400;
|
|
4582
4678
|
let bearing = view?.initialBearingDeg ?? 0;
|
|
4583
|
-
let pitch = 0;
|
|
4679
|
+
let pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, view?.initialPitchDeg ?? 0));
|
|
4680
|
+
let viewFov = VFOV_DEG;
|
|
4584
4681
|
let disposed = false;
|
|
4682
|
+
const loadAbort = new AbortController();
|
|
4683
|
+
const abortFromCaller = () => loadAbort.abort();
|
|
4684
|
+
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
4685
|
+
let cancelUpgrade = () => {
|
|
4686
|
+
};
|
|
4687
|
+
const priorPosition = [
|
|
4688
|
+
deps.camera.position.x,
|
|
4689
|
+
deps.camera.position.y,
|
|
4690
|
+
deps.camera.position.z
|
|
4691
|
+
];
|
|
4692
|
+
if (!view && deps.cameraOriginWorld) {
|
|
4693
|
+
deps.camera.position.set(
|
|
4694
|
+
deps.cameraOriginWorld[0],
|
|
4695
|
+
deps.cameraOriginWorld[1],
|
|
4696
|
+
deps.cameraOriginWorld[2]
|
|
4697
|
+
);
|
|
4698
|
+
}
|
|
4585
4699
|
if (!view && deps.focalWorld) {
|
|
4586
4700
|
const p = deps.camera.position;
|
|
4587
4701
|
const dx = deps.focalWorld[0] - p.x;
|
|
@@ -4595,22 +4709,37 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4595
4709
|
}
|
|
4596
4710
|
let sphere = null;
|
|
4597
4711
|
if (view) {
|
|
4712
|
+
const maxTextureSize = deps.gl.getParameter(deps.gl.MAX_TEXTURE_SIZE);
|
|
4713
|
+
const delivery = planPanoramaDelivery(view, browserPanoramaConstraints(maxTextureSize));
|
|
4598
4714
|
let image;
|
|
4599
4715
|
try {
|
|
4600
|
-
image = await
|
|
4716
|
+
image = await loadPanoramaImage(delivery.initialUrl, loadAbort.signal);
|
|
4601
4717
|
} catch {
|
|
4718
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
4602
4719
|
return null;
|
|
4603
4720
|
}
|
|
4604
4721
|
try {
|
|
4605
4722
|
sphere = createPanoSphere(deps.gl, image);
|
|
4606
4723
|
} catch {
|
|
4724
|
+
loadAbort.abort();
|
|
4725
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
4607
4726
|
return null;
|
|
4608
4727
|
}
|
|
4609
4728
|
sphere.mesh.renderOrder = 999;
|
|
4610
4729
|
sphere.mesh.setParent(deps.scene);
|
|
4730
|
+
if (delivery.upgradeUrl) {
|
|
4731
|
+
cancelUpgrade = schedulePanoramaUpgrade(() => {
|
|
4732
|
+
void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then((full) => {
|
|
4733
|
+
if (disposed || loadAbort.signal.aborted) return;
|
|
4734
|
+
sphere?.setImage(full);
|
|
4735
|
+
deps.requestRender();
|
|
4736
|
+
}).catch(() => {
|
|
4737
|
+
});
|
|
4738
|
+
});
|
|
4739
|
+
}
|
|
4611
4740
|
}
|
|
4612
4741
|
const priorFov = deps.camera.fov;
|
|
4613
|
-
deps.camera.perspective({ fov:
|
|
4742
|
+
deps.camera.perspective({ fov: viewFov, aspect: deps.camera.aspect });
|
|
4614
4743
|
const priorQuat = deps.camera.quaternion.slice();
|
|
4615
4744
|
const aim = () => {
|
|
4616
4745
|
const p = deps.camera.position;
|
|
@@ -4621,8 +4750,11 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4621
4750
|
};
|
|
4622
4751
|
aim();
|
|
4623
4752
|
const root = document.createElement("div");
|
|
4753
|
+
const priorFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
4624
4754
|
root.setAttribute("role", "dialog");
|
|
4755
|
+
root.setAttribute("aria-modal", "true");
|
|
4625
4756
|
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
4757
|
+
root.tabIndex = -1;
|
|
4626
4758
|
Object.assign(root.style, {
|
|
4627
4759
|
position: "absolute",
|
|
4628
4760
|
inset: "0",
|
|
@@ -4639,8 +4771,8 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4639
4771
|
top: "12px",
|
|
4640
4772
|
right: "12px",
|
|
4641
4773
|
zIndex: "2",
|
|
4642
|
-
width: "
|
|
4643
|
-
height: "
|
|
4774
|
+
width: "44px",
|
|
4775
|
+
height: "44px",
|
|
4644
4776
|
borderRadius: "999px",
|
|
4645
4777
|
cursor: "pointer",
|
|
4646
4778
|
border: "1px solid rgba(255,255,255,0.25)",
|
|
@@ -4650,8 +4782,30 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4650
4782
|
lineHeight: "1"
|
|
4651
4783
|
});
|
|
4652
4784
|
root.appendChild(closeBtn);
|
|
4785
|
+
if (opts.disclosure) {
|
|
4786
|
+
const disclosure = document.createElement("div");
|
|
4787
|
+
disclosure.textContent = `360\xB0 panorama \xB7 ${opts.disclosure}`;
|
|
4788
|
+
Object.assign(disclosure.style, {
|
|
4789
|
+
position: "absolute",
|
|
4790
|
+
top: "12px",
|
|
4791
|
+
left: "12px",
|
|
4792
|
+
zIndex: "2",
|
|
4793
|
+
maxWidth: "calc(100% - 88px)",
|
|
4794
|
+
padding: "8px 11px",
|
|
4795
|
+
borderRadius: "999px",
|
|
4796
|
+
overflow: "hidden",
|
|
4797
|
+
color: "#dce6f8",
|
|
4798
|
+
background: "rgba(8,12,18,0.68)",
|
|
4799
|
+
font: "600 11px/1.2 ui-sans-serif, system-ui, sans-serif",
|
|
4800
|
+
textOverflow: "ellipsis",
|
|
4801
|
+
whiteSpace: "nowrap",
|
|
4802
|
+
pointerEvents: "none",
|
|
4803
|
+
backdropFilter: "blur(6px)"
|
|
4804
|
+
});
|
|
4805
|
+
root.appendChild(disclosure);
|
|
4806
|
+
}
|
|
4653
4807
|
const hint = document.createElement("div");
|
|
4654
|
-
hint.textContent = "Drag to look
|
|
4808
|
+
hint.textContent = "Drag to look \xB7 pinch or scroll to zoom \xB7 Esc to close";
|
|
4655
4809
|
Object.assign(hint.style, {
|
|
4656
4810
|
position: "absolute",
|
|
4657
4811
|
bottom: "12px",
|
|
@@ -4664,18 +4818,43 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4664
4818
|
});
|
|
4665
4819
|
root.appendChild(hint);
|
|
4666
4820
|
container.appendChild(root);
|
|
4821
|
+
closeBtn.focus({ preventScroll: true });
|
|
4667
4822
|
let dragging = false;
|
|
4668
4823
|
let lastX = 0;
|
|
4669
4824
|
let lastY = 0;
|
|
4825
|
+
let pinchDistance = 0;
|
|
4826
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
4827
|
+
const distanceBetweenPointers = () => {
|
|
4828
|
+
const [a, b] = [...pointers.values()];
|
|
4829
|
+
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
4830
|
+
};
|
|
4831
|
+
const setViewFov = (nextFov) => {
|
|
4832
|
+
const clamped = clampPanoramaFov(nextFov);
|
|
4833
|
+
if (clamped === viewFov) return;
|
|
4834
|
+
viewFov = clamped;
|
|
4835
|
+
deps.camera.perspective({ fov: viewFov, aspect: deps.camera.aspect });
|
|
4836
|
+
aim();
|
|
4837
|
+
};
|
|
4670
4838
|
const onDown = (e) => {
|
|
4671
4839
|
if (e.target === closeBtn) return;
|
|
4672
|
-
|
|
4840
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
4841
|
+
dragging = pointers.size === 1;
|
|
4673
4842
|
lastX = e.clientX;
|
|
4674
4843
|
lastY = e.clientY;
|
|
4844
|
+
if (pointers.size === 2) pinchDistance = distanceBetweenPointers();
|
|
4675
4845
|
root.setPointerCapture?.(e.pointerId);
|
|
4676
4846
|
root.style.cursor = "grabbing";
|
|
4677
4847
|
};
|
|
4678
4848
|
const onMove = (e) => {
|
|
4849
|
+
if (!pointers.has(e.pointerId)) return;
|
|
4850
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
4851
|
+
if (pointers.size >= 2) {
|
|
4852
|
+
const nextDistance = distanceBetweenPointers();
|
|
4853
|
+
if (pinchDistance > 0 && nextDistance > 0) setViewFov(viewFov * (pinchDistance / nextDistance));
|
|
4854
|
+
pinchDistance = nextDistance;
|
|
4855
|
+
dragging = false;
|
|
4856
|
+
return;
|
|
4857
|
+
}
|
|
4679
4858
|
if (!dragging) return;
|
|
4680
4859
|
bearing += (e.clientX - lastX) * DEG_PER_PX;
|
|
4681
4860
|
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.clientY - lastY) * DEG_PER_PX));
|
|
@@ -4683,18 +4862,58 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4683
4862
|
lastY = e.clientY;
|
|
4684
4863
|
aim();
|
|
4685
4864
|
};
|
|
4686
|
-
const onUp = () => {
|
|
4687
|
-
|
|
4688
|
-
|
|
4865
|
+
const onUp = (e) => {
|
|
4866
|
+
pointers.delete(e.pointerId);
|
|
4867
|
+
pinchDistance = pointers.size === 2 ? distanceBetweenPointers() : 0;
|
|
4868
|
+
const remaining = pointers.values().next().value;
|
|
4869
|
+
dragging = pointers.size === 1;
|
|
4870
|
+
if (remaining) {
|
|
4871
|
+
lastX = remaining.x;
|
|
4872
|
+
lastY = remaining.y;
|
|
4873
|
+
} else root.style.cursor = "grab";
|
|
4874
|
+
try {
|
|
4875
|
+
root.releasePointerCapture?.(e.pointerId);
|
|
4876
|
+
} catch {
|
|
4877
|
+
}
|
|
4878
|
+
};
|
|
4879
|
+
const onWheel = (e) => {
|
|
4880
|
+
e.preventDefault();
|
|
4881
|
+
setViewFov(viewFov + e.deltaY * 0.04);
|
|
4689
4882
|
};
|
|
4690
4883
|
root.addEventListener("pointerdown", onDown);
|
|
4691
4884
|
root.addEventListener("pointermove", onMove);
|
|
4692
4885
|
root.addEventListener("pointerup", onUp);
|
|
4693
4886
|
root.addEventListener("pointercancel", onUp);
|
|
4887
|
+
root.addEventListener("wheel", onWheel, { passive: false });
|
|
4694
4888
|
const onKey = (e) => {
|
|
4695
|
-
if (
|
|
4696
|
-
e.
|
|
4697
|
-
|
|
4889
|
+
if (disposed) return;
|
|
4890
|
+
if (e.key === "Tab") {
|
|
4891
|
+
e.preventDefault();
|
|
4892
|
+
closeBtn.focus({ preventScroll: true });
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
if (e.key === "Escape") {
|
|
4896
|
+
e.stopPropagation();
|
|
4897
|
+
handle.close();
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
4901
|
+
bearing += e.key === "ArrowLeft" ? -4 : 4;
|
|
4902
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
4903
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.key === "ArrowUp" ? 4 : -4)));
|
|
4904
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
4905
|
+
setViewFov(viewFov - 5);
|
|
4906
|
+
e.preventDefault();
|
|
4907
|
+
return;
|
|
4908
|
+
} else if (e.key === "-") {
|
|
4909
|
+
setViewFov(viewFov + 5);
|
|
4910
|
+
e.preventDefault();
|
|
4911
|
+
return;
|
|
4912
|
+
} else {
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
e.preventDefault();
|
|
4916
|
+
aim();
|
|
4698
4917
|
};
|
|
4699
4918
|
window.addEventListener("keydown", onKey);
|
|
4700
4919
|
let raf = 0;
|
|
@@ -4719,14 +4938,20 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4719
4938
|
const teardown = () => {
|
|
4720
4939
|
if (disposed) return;
|
|
4721
4940
|
disposed = true;
|
|
4941
|
+
cancelUpgrade();
|
|
4942
|
+
loadAbort.abort();
|
|
4943
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
4722
4944
|
cancelAnimationFrame(raf);
|
|
4723
4945
|
root.removeEventListener("pointerdown", onDown);
|
|
4724
4946
|
root.removeEventListener("pointermove", onMove);
|
|
4725
4947
|
root.removeEventListener("pointerup", onUp);
|
|
4726
4948
|
root.removeEventListener("pointercancel", onUp);
|
|
4949
|
+
root.removeEventListener("wheel", onWheel);
|
|
4727
4950
|
window.removeEventListener("keydown", onKey);
|
|
4728
4951
|
root.remove();
|
|
4952
|
+
if (priorFocus?.isConnected) priorFocus.focus({ preventScroll: true });
|
|
4729
4953
|
sphere?.dispose();
|
|
4954
|
+
deps.camera.position.set(priorPosition[0], priorPosition[1], priorPosition[2]);
|
|
4730
4955
|
deps.camera.perspective({ fov: priorFov, aspect: deps.camera.aspect });
|
|
4731
4956
|
deps.camera.quaternion.set(priorQuat[0], priorQuat[1], priorQuat[2], priorQuat[3]);
|
|
4732
4957
|
deps.requestRender();
|
|
@@ -4763,8 +4988,13 @@ var Analytics3D = class {
|
|
|
4763
4988
|
} catch {
|
|
4764
4989
|
}
|
|
4765
4990
|
}
|
|
4766
|
-
opened(seats, hasHeights) {
|
|
4767
|
-
this.emit("3d_opened", {
|
|
4991
|
+
opened(seats, hasHeights, buildMs, prepSource) {
|
|
4992
|
+
this.emit("3d_opened", {
|
|
4993
|
+
seats,
|
|
4994
|
+
hasHeights,
|
|
4995
|
+
...buildMs === void 0 ? {} : { buildMs: Math.round(buildMs) },
|
|
4996
|
+
...prepSource === void 0 ? {} : { prepSource }
|
|
4997
|
+
});
|
|
4768
4998
|
}
|
|
4769
4999
|
/** First user-driven orbit/dolly per mount only (the intro ease is not user
|
|
4770
5000
|
* input, so callers must gate this on real pointer/wheel gestures). */
|
|
@@ -4794,18 +5024,77 @@ var Analytics3D = class {
|
|
|
4794
5024
|
this.panoramaOpenedAt = 0;
|
|
4795
5025
|
this.emit("3d_panorama_closed", { viewMs });
|
|
4796
5026
|
}
|
|
5027
|
+
panoramaFailed(seatId, phase) {
|
|
5028
|
+
this.emit("3d_panorama_failed", { seatId, phase });
|
|
5029
|
+
}
|
|
5030
|
+
panoramaFallback(seatId) {
|
|
5031
|
+
this.emit("3d_panorama_fallback", { seatId });
|
|
5032
|
+
}
|
|
5033
|
+
contextLost() {
|
|
5034
|
+
this.emit("3d_context_lost");
|
|
5035
|
+
}
|
|
5036
|
+
contextRestored() {
|
|
5037
|
+
this.emit("3d_context_restored");
|
|
5038
|
+
}
|
|
4797
5039
|
};
|
|
4798
5040
|
|
|
5041
|
+
// src/view3d/positioningContext.ts
|
|
5042
|
+
function establishPositioningContext(container) {
|
|
5043
|
+
const originalInline = container.style.position;
|
|
5044
|
+
if (getComputedStyle(container).position !== "static") return () => {
|
|
5045
|
+
};
|
|
5046
|
+
container.style.position = "relative";
|
|
5047
|
+
return () => {
|
|
5048
|
+
if (container.style.position === "relative") container.style.position = originalInline;
|
|
5049
|
+
};
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
// src/view3d/prepareScene.ts
|
|
5053
|
+
function prepareOnMain(input) {
|
|
5054
|
+
const started = performance.now();
|
|
5055
|
+
const model = buildSceneModel(input);
|
|
5056
|
+
return { model, buildMs: performance.now() - started, source: "main" };
|
|
5057
|
+
}
|
|
5058
|
+
function prepareVenue3D(input) {
|
|
5059
|
+
if (typeof Worker === "undefined") return Promise.resolve(prepareOnMain(input));
|
|
5060
|
+
return new Promise((resolve) => {
|
|
5061
|
+
const worker = new Worker(new URL("./scene/scene.worker.ts", import.meta.url), { type: "module" });
|
|
5062
|
+
let settled = false;
|
|
5063
|
+
const finish = (result) => {
|
|
5064
|
+
if (settled) return;
|
|
5065
|
+
settled = true;
|
|
5066
|
+
clearTimeout(timeout);
|
|
5067
|
+
worker.terminate();
|
|
5068
|
+
resolve(result);
|
|
5069
|
+
};
|
|
5070
|
+
const fallback = () => finish(prepareOnMain(input));
|
|
5071
|
+
const timeout = window.setTimeout(fallback, 15e3);
|
|
5072
|
+
worker.onerror = fallback;
|
|
5073
|
+
worker.onmessage = (event) => {
|
|
5074
|
+
finish({ model: event.data.model, buildMs: event.data.buildMs, source: "worker" });
|
|
5075
|
+
};
|
|
5076
|
+
worker.postMessage(input);
|
|
5077
|
+
});
|
|
5078
|
+
}
|
|
5079
|
+
|
|
4799
5080
|
// src/view3d/index.ts
|
|
4800
|
-
var SEAT_EYE_ABOVE_DECK = 1.02;
|
|
4801
5081
|
var DEG2 = Math.PI / 180;
|
|
4802
5082
|
var TAP_SLOP = 6;
|
|
4803
5083
|
var TAP_MS = 500;
|
|
4804
5084
|
function mountVenue3D(container, input, opts = {}) {
|
|
4805
|
-
const
|
|
5085
|
+
const buildStartedAt = performance.now();
|
|
5086
|
+
const model = input.prepared?.model ?? buildSceneModel(input);
|
|
4806
5087
|
const analytics = new Analytics3D(opts.onAnalytics);
|
|
4807
5088
|
const seatIdByIndex = new Array(model.seats.count);
|
|
4808
5089
|
for (const [id, idx] of model.seats.idToIndex) seatIdByIndex[idx] = id;
|
|
5090
|
+
const expandedSeatById = new Map(input.seats.map((seat) => [seat.id, seat]));
|
|
5091
|
+
const seatIdsByRow = /* @__PURE__ */ new Map();
|
|
5092
|
+
for (const seat of input.seats) {
|
|
5093
|
+
if (!seat.rowId) continue;
|
|
5094
|
+
const row = seatIdsByRow.get(seat.rowId);
|
|
5095
|
+
if (row) row.push(seat.id);
|
|
5096
|
+
else seatIdsByRow.set(seat.rowId, [seat.id]);
|
|
5097
|
+
}
|
|
4809
5098
|
const sectionIdBySeatId = /* @__PURE__ */ new Map();
|
|
4810
5099
|
for (const s of input.seats) sectionIdBySeatId.set(s.id, s.sectionId);
|
|
4811
5100
|
const hasHeights = (() => {
|
|
@@ -4820,10 +5109,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4820
5109
|
let disposed = false;
|
|
4821
5110
|
let selection = /* @__PURE__ */ new Map();
|
|
4822
5111
|
let panorama = null;
|
|
5112
|
+
let panoramaLoadAbort = null;
|
|
5113
|
+
let panoramaPriorZIndex = null;
|
|
4823
5114
|
const prefetch = /* @__PURE__ */ new Map();
|
|
4824
5115
|
let flightGen = 0;
|
|
4825
5116
|
let reducedForced = null;
|
|
4826
5117
|
let focusedFloor = -1;
|
|
5118
|
+
const raisePanoramaLayer = () => {
|
|
5119
|
+
if (panoramaPriorZIndex !== null) return;
|
|
5120
|
+
panoramaPriorZIndex = container.style.zIndex;
|
|
5121
|
+
container.style.zIndex = "20";
|
|
5122
|
+
};
|
|
5123
|
+
const restorePanoramaLayer = () => {
|
|
5124
|
+
if (panoramaPriorZIndex === null) return;
|
|
5125
|
+
if (container.style.zIndex === "20") container.style.zIndex = panoramaPriorZIndex;
|
|
5126
|
+
panoramaPriorZIndex = null;
|
|
5127
|
+
};
|
|
4827
5128
|
const rebuildGpu = () => {
|
|
4828
5129
|
gpu = buildGpuScene(glctx.gl, model);
|
|
4829
5130
|
pick = new PickPipeline(glctx.renderer, gpu.seatGeometry, gpu.solidGeometry, model.seats.count);
|
|
@@ -4857,6 +5158,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4857
5158
|
};
|
|
4858
5159
|
const glctx = new GLContext(container, {
|
|
4859
5160
|
onContextLost: () => {
|
|
5161
|
+
if (!disposed) analytics.contextLost();
|
|
4860
5162
|
contextLost = true;
|
|
4861
5163
|
loop.stop();
|
|
4862
5164
|
gpu = null;
|
|
@@ -4865,6 +5167,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4865
5167
|
onContextRestored: () => {
|
|
4866
5168
|
rebuildGpu();
|
|
4867
5169
|
contextLost = false;
|
|
5170
|
+
if (!disposed) analytics.contextRestored();
|
|
4868
5171
|
loop.requestRender();
|
|
4869
5172
|
}
|
|
4870
5173
|
});
|
|
@@ -4881,16 +5184,48 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4881
5184
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
4882
5185
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
4883
5186
|
})();
|
|
4884
|
-
orbit.frame(model.bounds, true, stageAzimuth);
|
|
5187
|
+
orbit.frame(model.bounds, true, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
4885
5188
|
const cinematic = new Cinematic(orbit.camera);
|
|
4886
|
-
|
|
4887
|
-
container.style.position = "relative";
|
|
4888
|
-
}
|
|
5189
|
+
const restoreContainerPosition = establishPositioningContext(container);
|
|
4889
5190
|
const labelOverlay = new LabelOverlay(container, {
|
|
4890
5191
|
fontFamily: input.doc.theme?.fontFamily,
|
|
4891
5192
|
ink: input.doc.theme?.textColor
|
|
4892
5193
|
});
|
|
4893
5194
|
labelOverlay.setLabels(model.labels);
|
|
5195
|
+
const baseLabels = model.labels;
|
|
5196
|
+
let focusedSectionId = null;
|
|
5197
|
+
const showSectionSeatLabels = (sectionId) => {
|
|
5198
|
+
focusedSectionId = sectionId;
|
|
5199
|
+
labelOverlay.setForcedDense(!!sectionId);
|
|
5200
|
+
if (!sectionId) {
|
|
5201
|
+
labelOverlay.setLabels(baseLabels);
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
5205
|
+
const seatIds = new Set(sectionSeats.map((seat) => seat.id));
|
|
5206
|
+
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
5207
|
+
const focusedBase = baseLabels.filter((label) => label.kind !== "row" && label.kind !== "seat" || (label.kind === "row" ? rowIds.has(label.id.slice("row:".length)) : seatIds.has(label.id.slice("seat:".length))));
|
|
5208
|
+
if (model.seatCount <= 6e3) {
|
|
5209
|
+
labelOverlay.setLabels(focusedBase);
|
|
5210
|
+
return;
|
|
5211
|
+
}
|
|
5212
|
+
const labels = sectionSeats.flatMap((seat) => {
|
|
5213
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
5214
|
+
if (index === void 0) return [];
|
|
5215
|
+
const offset = index * 3;
|
|
5216
|
+
return [{
|
|
5217
|
+
id: `seat:${seat.id}`,
|
|
5218
|
+
kind: "seat",
|
|
5219
|
+
text: seat.displayLabel || seat.label,
|
|
5220
|
+
anchor: [
|
|
5221
|
+
model.seats.iPosition[offset],
|
|
5222
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
5223
|
+
model.seats.iPosition[offset + 2]
|
|
5224
|
+
]
|
|
5225
|
+
}];
|
|
5226
|
+
});
|
|
5227
|
+
labelOverlay.setLabels([...focusedBase, ...labels]);
|
|
5228
|
+
};
|
|
4894
5229
|
rebuildGpu();
|
|
4895
5230
|
const loop = new RenderLoop(() => {
|
|
4896
5231
|
if (contextLost || !gpu || frozen) return false;
|
|
@@ -4943,6 +5278,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4943
5278
|
if (!p) {
|
|
4944
5279
|
p = Promise.resolve(opts.getSeatView(seatId));
|
|
4945
5280
|
prefetch.set(seatId, p);
|
|
5281
|
+
void p.catch(() => {
|
|
5282
|
+
if (prefetch.get(seatId) === p) prefetch.delete(seatId);
|
|
5283
|
+
});
|
|
4946
5284
|
while (prefetch.size > PREFETCH_CAP) {
|
|
4947
5285
|
const oldest = prefetch.keys().next().value;
|
|
4948
5286
|
if (oldest === void 0) break;
|
|
@@ -4951,56 +5289,212 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4951
5289
|
}
|
|
4952
5290
|
return p;
|
|
4953
5291
|
};
|
|
4954
|
-
const
|
|
4955
|
-
|
|
4956
|
-
model.seats.
|
|
4957
|
-
|
|
4958
|
-
|
|
5292
|
+
const resolvedSeatViewPose = (seatId, idx) => {
|
|
5293
|
+
const seat = expandedSeatById.get(seatId);
|
|
5294
|
+
const floorIndex = Math.max(0, Math.round(model.seats.iFloor[idx] ?? 0));
|
|
5295
|
+
const floor = input.doc.floors?.[floorIndex];
|
|
5296
|
+
const focalPoint = seat?.focalPoint ?? floor?.focalPoint ?? input.doc.focalPoint;
|
|
5297
|
+
const deck = [
|
|
5298
|
+
model.seats.iPosition[idx * 3],
|
|
5299
|
+
model.seats.iPosition[idx * 3 + 1],
|
|
5300
|
+
model.seats.iPosition[idx * 3 + 2]
|
|
5301
|
+
];
|
|
5302
|
+
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0);
|
|
5303
|
+
};
|
|
4959
5304
|
const placeCameraFinal = (finalPos, focal) => {
|
|
4960
5305
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
4961
5306
|
orbit.camera.lookAt(new Vec33(focal[0], focal[1], focal[2]));
|
|
4962
5307
|
orbit.camera.fov = FOV_END;
|
|
4963
5308
|
orbit.camera.updateProjectionMatrix();
|
|
4964
5309
|
};
|
|
5310
|
+
const isNarrow = () => (container.clientWidth || glctx.canvas.clientWidth) <= 520;
|
|
4965
5311
|
let arriveChip = null;
|
|
5312
|
+
const layoutArriveChip = () => {
|
|
5313
|
+
if (!arriveChip) return;
|
|
5314
|
+
const narrow = isNarrow();
|
|
5315
|
+
Object.assign(arriveChip.style, narrow ? {
|
|
5316
|
+
left: "12px",
|
|
5317
|
+
right: "12px",
|
|
5318
|
+
bottom: "70px",
|
|
5319
|
+
transform: "none",
|
|
5320
|
+
justifyContent: "center"
|
|
5321
|
+
} : {
|
|
5322
|
+
left: "50%",
|
|
5323
|
+
right: "auto",
|
|
5324
|
+
bottom: "18px",
|
|
5325
|
+
transform: "translateX(-50%)",
|
|
5326
|
+
justifyContent: "initial"
|
|
5327
|
+
});
|
|
5328
|
+
};
|
|
4966
5329
|
const removeArriveChip = () => {
|
|
4967
5330
|
arriveChip?.remove();
|
|
4968
5331
|
arriveChip = null;
|
|
4969
5332
|
};
|
|
4970
|
-
const showArriveChip = (seatId, gen) => {
|
|
5333
|
+
const showArriveChip = (seatId, gen, retry = false) => {
|
|
4971
5334
|
removeArriveChip();
|
|
4972
|
-
if (disposed
|
|
4973
|
-
const
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
chip.setAttribute("aria-label", `Open the 360\xB0 view from seat ${seatId}`);
|
|
4977
|
-
Object.assign(chip.style, {
|
|
5335
|
+
if (disposed) return null;
|
|
5336
|
+
const controls = document.createElement("div");
|
|
5337
|
+
controls.setAttribute("role", "group");
|
|
5338
|
+
Object.assign(controls.style, {
|
|
4978
5339
|
position: "absolute",
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
padding: "10px 18px",
|
|
4984
|
-
borderRadius: "999px",
|
|
4985
|
-
background: "rgba(12,18,32,0.78)",
|
|
4986
|
-
color: "#eef1f8",
|
|
4987
|
-
border: "1px solid rgba(150,165,205,0.4)",
|
|
4988
|
-
backdropFilter: "blur(6px)",
|
|
4989
|
-
font: "600 13px/1 inherit",
|
|
4990
|
-
cursor: "pointer",
|
|
5340
|
+
display: "flex",
|
|
5341
|
+
flexDirection: "column",
|
|
5342
|
+
alignItems: "center",
|
|
5343
|
+
gap: "6px",
|
|
4991
5344
|
zIndex: "4"
|
|
4992
5345
|
});
|
|
4993
|
-
|
|
4994
|
-
|
|
5346
|
+
const seat = expandedSeatById.get(seatId);
|
|
5347
|
+
const section = seat?.sectionId ? model.sections.find((candidate) => candidate.id === seat.sectionId) : void 0;
|
|
5348
|
+
const seatLabel = seat?.label ?? seatId;
|
|
5349
|
+
controls.setAttribute("aria-label", `Explore views near ${section?.label ? `${section.label}, ` : ""}seat ${seatLabel}`);
|
|
5350
|
+
const location = document.createElement("span");
|
|
5351
|
+
location.textContent = ["Live 3D seat view", section?.label, seatLabel].filter(Boolean).join(" \xB7 ");
|
|
5352
|
+
Object.assign(location.style, {
|
|
5353
|
+
maxWidth: "min(320px, calc(100vw - 32px))",
|
|
5354
|
+
padding: "5px 10px",
|
|
5355
|
+
borderRadius: "999px",
|
|
5356
|
+
overflow: "hidden",
|
|
5357
|
+
color: "#d9e3f5",
|
|
5358
|
+
background: "rgba(12,18,32,0.72)",
|
|
5359
|
+
font: "600 11px/1.2 inherit",
|
|
5360
|
+
textOverflow: "ellipsis",
|
|
5361
|
+
whiteSpace: "nowrap",
|
|
5362
|
+
backdropFilter: "blur(6px)"
|
|
5363
|
+
});
|
|
5364
|
+
controls.appendChild(location);
|
|
5365
|
+
const actions = document.createElement("div");
|
|
5366
|
+
Object.assign(actions.style, {
|
|
5367
|
+
display: "flex",
|
|
5368
|
+
alignItems: "center",
|
|
5369
|
+
justifyContent: "center",
|
|
5370
|
+
gap: "6px"
|
|
5371
|
+
});
|
|
5372
|
+
controls.appendChild(actions);
|
|
5373
|
+
const row = seat?.rowId ? seatIdsByRow.get(seat.rowId) : void 0;
|
|
5374
|
+
const at = row?.indexOf(seatId) ?? -1;
|
|
5375
|
+
const neighbours = at >= 0 ? [row?.[at - 1], row?.[at + 1]] : [void 0, void 0];
|
|
5376
|
+
const addButton = (label, ariaLabel, action, primary = false) => {
|
|
5377
|
+
const button = document.createElement("button");
|
|
5378
|
+
button.type = "button";
|
|
5379
|
+
button.textContent = label;
|
|
5380
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
5381
|
+
Object.assign(button.style, {
|
|
5382
|
+
minHeight: "44px",
|
|
5383
|
+
padding: primary ? "10px 16px" : "10px",
|
|
5384
|
+
borderRadius: "999px",
|
|
5385
|
+
background: "rgba(12,18,32,0.78)",
|
|
5386
|
+
color: "#eef1f8",
|
|
5387
|
+
border: "1px solid rgba(150,165,205,0.4)",
|
|
5388
|
+
backdropFilter: "blur(6px)",
|
|
5389
|
+
font: "600 13px/1 inherit",
|
|
5390
|
+
cursor: "pointer",
|
|
5391
|
+
whiteSpace: "nowrap"
|
|
5392
|
+
});
|
|
5393
|
+
if (!primary) button.style.minWidth = "44px";
|
|
5394
|
+
if (primary) button.dataset.panoramaTrigger = "";
|
|
5395
|
+
button.addEventListener("click", action);
|
|
5396
|
+
actions.appendChild(button);
|
|
5397
|
+
};
|
|
5398
|
+
if (neighbours[0]) {
|
|
5399
|
+
const label = expandedSeatById.get(neighbours[0])?.label ?? neighbours[0];
|
|
5400
|
+
addButton("\u2039", `View previous seat ${label}`, () => {
|
|
5401
|
+
void flyToSeat(neighbours[0]);
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
if (opts.getSeatView) {
|
|
5405
|
+
addButton(retry ? "\u21BB Retry 360\xB0 panorama" : "\u25C9 Open 360\xB0 panorama", `${retry ? "Retry" : "Open"} the 360\xB0 panorama from seat ${seatLabel}`, () => {
|
|
5406
|
+
if (disposed || gen !== flightGen) {
|
|
5407
|
+
removeArriveChip();
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
4995
5410
|
removeArriveChip();
|
|
4996
|
-
|
|
4997
|
-
}
|
|
4998
|
-
|
|
4999
|
-
|
|
5411
|
+
void openPanorama(seatId, 400, gen);
|
|
5412
|
+
}, true);
|
|
5413
|
+
}
|
|
5414
|
+
if (neighbours[1]) {
|
|
5415
|
+
const label = expandedSeatById.get(neighbours[1])?.label ?? neighbours[1];
|
|
5416
|
+
addButton("\u203A", `View next seat ${label}`, () => {
|
|
5417
|
+
void flyToSeat(neighbours[1]);
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
if (!actions.childElementCount) return null;
|
|
5421
|
+
container.appendChild(controls);
|
|
5422
|
+
arriveChip = controls;
|
|
5423
|
+
layoutArriveChip();
|
|
5424
|
+
return controls;
|
|
5425
|
+
};
|
|
5426
|
+
const navigationModeChip = document.createElement("div");
|
|
5427
|
+
navigationModeChip.setAttribute("role", "group");
|
|
5428
|
+
navigationModeChip.setAttribute("aria-label", "3D navigation mode");
|
|
5429
|
+
Object.assign(navigationModeChip.style, {
|
|
5430
|
+
position: "absolute",
|
|
5431
|
+
left: "14px",
|
|
5432
|
+
top: "62px",
|
|
5433
|
+
display: "flex",
|
|
5434
|
+
gap: "3px",
|
|
5435
|
+
padding: "3px",
|
|
5436
|
+
borderRadius: "999px",
|
|
5437
|
+
zIndex: "4",
|
|
5438
|
+
background: "rgba(12,18,32,0.72)",
|
|
5439
|
+
border: "1px solid rgba(150,165,205,0.35)",
|
|
5440
|
+
backdropFilter: "blur(6px)"
|
|
5441
|
+
});
|
|
5442
|
+
const rotateModeButton = document.createElement("button");
|
|
5443
|
+
rotateModeButton.type = "button";
|
|
5444
|
+
rotateModeButton.textContent = "\u21BB Rotate";
|
|
5445
|
+
rotateModeButton.setAttribute("aria-label", "Drag to rotate the 3D venue");
|
|
5446
|
+
const moveModeButton = document.createElement("button");
|
|
5447
|
+
moveModeButton.type = "button";
|
|
5448
|
+
moveModeButton.textContent = "\u2725 Move";
|
|
5449
|
+
moveModeButton.setAttribute("aria-label", "Drag to move the 3D venue left, right, up or down");
|
|
5450
|
+
const zoomOutButton = document.createElement("button");
|
|
5451
|
+
zoomOutButton.type = "button";
|
|
5452
|
+
zoomOutButton.textContent = "\u2212";
|
|
5453
|
+
zoomOutButton.setAttribute("aria-label", "Zoom out of the 3D venue");
|
|
5454
|
+
zoomOutButton.title = "Zoom out";
|
|
5455
|
+
const zoomInButton = document.createElement("button");
|
|
5456
|
+
zoomInButton.type = "button";
|
|
5457
|
+
zoomInButton.textContent = "+";
|
|
5458
|
+
zoomInButton.setAttribute("aria-label", "Zoom into the 3D venue");
|
|
5459
|
+
zoomInButton.title = "Zoom in";
|
|
5460
|
+
for (const button of [rotateModeButton, moveModeButton, zoomOutButton, zoomInButton]) {
|
|
5461
|
+
Object.assign(button.style, {
|
|
5462
|
+
minHeight: "34px",
|
|
5463
|
+
padding: "6px 10px",
|
|
5464
|
+
border: "0",
|
|
5465
|
+
borderRadius: "999px",
|
|
5466
|
+
color: "#c9d4ea",
|
|
5467
|
+
background: "transparent",
|
|
5468
|
+
font: "600 11px/1 inherit",
|
|
5469
|
+
cursor: "pointer",
|
|
5470
|
+
whiteSpace: "nowrap"
|
|
5000
5471
|
});
|
|
5001
|
-
|
|
5002
|
-
|
|
5472
|
+
if (button === zoomOutButton || button === zoomInButton) {
|
|
5473
|
+
button.style.minWidth = "34px";
|
|
5474
|
+
button.style.padding = "6px";
|
|
5475
|
+
button.style.fontSize = "17px";
|
|
5476
|
+
}
|
|
5477
|
+
navigationModeChip.appendChild(button);
|
|
5478
|
+
}
|
|
5479
|
+
const setNavigationMode = (mode) => {
|
|
5480
|
+
orbit.setPrimaryDragMode(mode);
|
|
5481
|
+
const rotateActive = mode === "orbit";
|
|
5482
|
+
rotateModeButton.setAttribute("aria-pressed", String(rotateActive));
|
|
5483
|
+
moveModeButton.setAttribute("aria-pressed", String(!rotateActive));
|
|
5484
|
+
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.48)" : "transparent";
|
|
5485
|
+
moveModeButton.style.background = rotateActive ? "transparent" : "rgba(96,110,150,0.48)";
|
|
5486
|
+
navigationModeChip.title = rotateActive ? "Drag to rotate \xB7 Shift-drag to move" : "Drag to move \xB7 Shift-drag to rotate";
|
|
5487
|
+
};
|
|
5488
|
+
rotateModeButton.addEventListener("click", () => setNavigationMode("orbit"));
|
|
5489
|
+
moveModeButton.addEventListener("click", () => setNavigationMode("pan"));
|
|
5490
|
+
zoomOutButton.addEventListener("click", () => orbit.zoomBy(1.22));
|
|
5491
|
+
zoomInButton.addEventListener("click", () => orbit.zoomBy(0.82));
|
|
5492
|
+
setNavigationMode("orbit");
|
|
5493
|
+
container.appendChild(navigationModeChip);
|
|
5494
|
+
const layoutNavigationChip = () => {
|
|
5495
|
+
navigationModeChip.style.display = isNarrow() ? "none" : "flex";
|
|
5003
5496
|
};
|
|
5497
|
+
layoutNavigationChip();
|
|
5004
5498
|
const overviewChip = document.createElement("button");
|
|
5005
5499
|
overviewChip.type = "button";
|
|
5006
5500
|
overviewChip.textContent = "\u2302 Overview";
|
|
@@ -5020,17 +5514,46 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5020
5514
|
cursor: "pointer",
|
|
5021
5515
|
zIndex: "4"
|
|
5022
5516
|
});
|
|
5023
|
-
|
|
5024
|
-
if (disposed
|
|
5517
|
+
const focusOverview = () => {
|
|
5518
|
+
if (disposed) return;
|
|
5519
|
+
if (panorama) {
|
|
5520
|
+
panorama.dispose();
|
|
5521
|
+
panorama = null;
|
|
5522
|
+
analytics.panoramaClosed();
|
|
5523
|
+
}
|
|
5524
|
+
panoramaLoadAbort?.abort();
|
|
5525
|
+
panoramaLoadAbort = null;
|
|
5526
|
+
restorePanoramaLayer();
|
|
5527
|
+
overviewChip.style.display = "";
|
|
5528
|
+
labelOverlay.setVisible(true);
|
|
5529
|
+
frozen = false;
|
|
5025
5530
|
cancelFlight();
|
|
5026
5531
|
removeArriveChip();
|
|
5027
|
-
orbit
|
|
5532
|
+
setNavigationMode("orbit");
|
|
5533
|
+
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5534
|
+
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5535
|
+
opts.onViewTargetChange?.(null);
|
|
5536
|
+
showSectionSeatLabels(null);
|
|
5537
|
+
opts.onSectionFocusChange?.(null);
|
|
5028
5538
|
loop.requestRender();
|
|
5029
|
-
}
|
|
5539
|
+
};
|
|
5540
|
+
overviewChip.addEventListener("click", focusOverview);
|
|
5030
5541
|
container.appendChild(overviewChip);
|
|
5031
5542
|
const openPanorama = async (seatId, fadeMs, gen) => {
|
|
5543
|
+
panoramaLoadAbort?.abort();
|
|
5544
|
+
const loadAbort = new AbortController();
|
|
5545
|
+
panoramaLoadAbort = loadAbort;
|
|
5546
|
+
const seatIndex = model.seats.idToIndex.get(seatId);
|
|
5547
|
+
if (seatIndex === void 0) {
|
|
5548
|
+
orbit.syncFromCamera();
|
|
5549
|
+
return;
|
|
5550
|
+
}
|
|
5551
|
+
const seatLabel = expandedSeatById.get(seatId)?.label ?? seatId;
|
|
5552
|
+
const seatPose = resolvedSeatViewPose(seatId, seatIndex);
|
|
5032
5553
|
const viewPromise = ensureSeatView(seatId);
|
|
5033
5554
|
if (!viewPromise) {
|
|
5555
|
+
loadAbort.abort();
|
|
5556
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5034
5557
|
orbit.syncFromCamera();
|
|
5035
5558
|
return;
|
|
5036
5559
|
}
|
|
@@ -5040,34 +5563,56 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5040
5563
|
try {
|
|
5041
5564
|
view = await viewPromise;
|
|
5042
5565
|
} catch {
|
|
5566
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5567
|
+
analytics.panoramaFailed(seatId, "source");
|
|
5043
5568
|
if (!disposed && gen === flightGen) {
|
|
5044
5569
|
frozen = false;
|
|
5045
|
-
orbit.resumeAfterFlight(
|
|
5570
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5046
5571
|
loop.requestRender();
|
|
5572
|
+
const controls = showArriveChip(seatId, gen, true);
|
|
5573
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5047
5574
|
}
|
|
5048
5575
|
return;
|
|
5049
5576
|
}
|
|
5050
|
-
if (disposed || gen !== flightGen)
|
|
5577
|
+
if (disposed || gen !== flightGen) {
|
|
5578
|
+
loadAbort.abort();
|
|
5579
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5580
|
+
return;
|
|
5581
|
+
}
|
|
5051
5582
|
removeArriveChip();
|
|
5052
5583
|
const onClose = () => {
|
|
5584
|
+
loadAbort.abort();
|
|
5585
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5053
5586
|
panorama = null;
|
|
5587
|
+
restorePanoramaLayer();
|
|
5588
|
+
overviewChip.style.display = "";
|
|
5054
5589
|
labelOverlay.setVisible(true);
|
|
5055
5590
|
frozen = false;
|
|
5056
5591
|
analytics.panoramaClosed();
|
|
5057
|
-
orbit.resumeAfterFlight(
|
|
5592
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5058
5593
|
loop.requestRender();
|
|
5059
|
-
showArriveChip(seatId, flightGen);
|
|
5594
|
+
const controls = showArriveChip(seatId, flightGen);
|
|
5595
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5060
5596
|
};
|
|
5061
5597
|
const sceneMode = view.generated === true;
|
|
5598
|
+
const disclosure = seatViewDisclosure(view);
|
|
5599
|
+
raisePanoramaLayer();
|
|
5600
|
+
overviewChip.style.display = "none";
|
|
5601
|
+
const effectiveFadeMs = reducedMotion() ? 0 : fadeMs;
|
|
5062
5602
|
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5063
5603
|
gl: glctx.gl,
|
|
5064
5604
|
scene: gpu.main,
|
|
5065
5605
|
camera: orbit.camera,
|
|
5066
5606
|
requestRender: () => loop.requestRender(),
|
|
5067
|
-
|
|
5068
|
-
|
|
5607
|
+
cameraOriginWorld: sceneMode ? seatPose.eye : void 0,
|
|
5608
|
+
focalWorld: seatPose.focal
|
|
5609
|
+
}, { fadeMs: effectiveFadeMs, seatLabel, disclosure, onClose, signal: loadAbort.signal }) : null;
|
|
5069
5610
|
if (disposed || gen !== flightGen) {
|
|
5611
|
+
loadAbort.abort();
|
|
5612
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5070
5613
|
spherical?.dispose();
|
|
5614
|
+
overviewChip.style.display = "";
|
|
5615
|
+
restorePanoramaLayer();
|
|
5071
5616
|
return;
|
|
5072
5617
|
}
|
|
5073
5618
|
if (spherical) {
|
|
@@ -5077,12 +5622,21 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5077
5622
|
loop.requestRender();
|
|
5078
5623
|
panorama = spherical;
|
|
5079
5624
|
} else {
|
|
5080
|
-
|
|
5625
|
+
analytics.panoramaFallback(seatId);
|
|
5626
|
+
panorama = mountPanorama(container, view, {
|
|
5627
|
+
fadeMs: effectiveFadeMs,
|
|
5628
|
+
seatLabel,
|
|
5629
|
+
disclosure,
|
|
5630
|
+
onClose,
|
|
5631
|
+
signal: loadAbort.signal
|
|
5632
|
+
});
|
|
5081
5633
|
}
|
|
5082
5634
|
analytics.panoramaOpened();
|
|
5083
5635
|
};
|
|
5084
5636
|
const cancelFlight = () => {
|
|
5085
5637
|
flightGen++;
|
|
5638
|
+
panoramaLoadAbort?.abort();
|
|
5639
|
+
panoramaLoadAbort = null;
|
|
5086
5640
|
if (cinematic.active) {
|
|
5087
5641
|
cinematic.cancel();
|
|
5088
5642
|
orbit.resumeAfterFlight(model.focalWorld);
|
|
@@ -5092,15 +5646,19 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5092
5646
|
if (disposed || !gpu) return Promise.resolve();
|
|
5093
5647
|
const idx = model.seats.idToIndex.get(seatId);
|
|
5094
5648
|
if (idx === void 0) return Promise.resolve();
|
|
5649
|
+
opts.onViewTargetChange?.(seatId);
|
|
5095
5650
|
if (panorama) {
|
|
5096
5651
|
panorama.dispose();
|
|
5097
5652
|
panorama = null;
|
|
5653
|
+
overviewChip.style.display = "";
|
|
5654
|
+
restorePanoramaLayer();
|
|
5098
5655
|
}
|
|
5656
|
+
panoramaLoadAbort?.abort();
|
|
5657
|
+
panoramaLoadAbort = null;
|
|
5099
5658
|
frozen = false;
|
|
5100
5659
|
const gen = ++flightGen;
|
|
5101
5660
|
removeArriveChip();
|
|
5102
|
-
const seatEye =
|
|
5103
|
-
const focal = model.focalWorld;
|
|
5661
|
+
const { eye: seatEye, focal } = resolvedSeatViewPose(seatId, idx);
|
|
5104
5662
|
const start = [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z];
|
|
5105
5663
|
const { waypoints, finalPos } = buildWaypoints(start, seatEye, focal, model.bounds.center, model.bounds.radius);
|
|
5106
5664
|
if (reducedMotion()) {
|
|
@@ -5117,11 +5675,26 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5117
5675
|
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
5118
5676
|
if (disposed || gen !== flightGen) return;
|
|
5119
5677
|
analytics.cinematicPlayed(FLIGHT_DURATION_MS);
|
|
5120
|
-
orbit.resumeAfterFlight(
|
|
5678
|
+
orbit.resumeAfterFlight(focal);
|
|
5121
5679
|
loop.requestRender();
|
|
5122
5680
|
showArriveChip(seatId, gen);
|
|
5123
5681
|
});
|
|
5124
5682
|
};
|
|
5683
|
+
const focusSectionCamera = (sectionId) => {
|
|
5684
|
+
const sec = model.sections.find((candidate) => candidate.id === sectionId);
|
|
5685
|
+
if (!sec || sec.seatCount === 0) return false;
|
|
5686
|
+
cinematic.cancel();
|
|
5687
|
+
removeArriveChip();
|
|
5688
|
+
setNavigationMode("pan");
|
|
5689
|
+
showSectionSeatLabels(sectionId);
|
|
5690
|
+
const dx = sec.focalWorld[0] - sec.center[0];
|
|
5691
|
+
const dz = sec.focalWorld[2] - sec.center[2];
|
|
5692
|
+
const azimuth = Math.hypot(dx, dz) > sec.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
5693
|
+
orbit.frame({ center: sec.center, radius: Math.max(5, sec.radius * 0.5) }, false, azimuth);
|
|
5694
|
+
opts.onSectionFocusChange?.(sectionId);
|
|
5695
|
+
loop.requestRender();
|
|
5696
|
+
return true;
|
|
5697
|
+
};
|
|
5125
5698
|
let downX = 0, downY = 0, downT = 0, downId = -1, moved = false, suppressTap = false;
|
|
5126
5699
|
const onDown = (e) => {
|
|
5127
5700
|
if (downId !== -1) return;
|
|
@@ -5140,6 +5713,50 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5140
5713
|
if (e.pointerId !== downId) return;
|
|
5141
5714
|
if (Math.hypot(e.clientX - downX, e.clientY - downY) > TAP_SLOP) moved = true;
|
|
5142
5715
|
};
|
|
5716
|
+
const pickNearestProjectedSeat = (clientX, clientY, sectionId, maxDistance, bookableOnly) => {
|
|
5717
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
5718
|
+
const width = glctx.canvas.clientWidth || rect.width || 1;
|
|
5719
|
+
const height = glctx.canvas.clientHeight || rect.height || 1;
|
|
5720
|
+
const tapX = clientX - rect.left;
|
|
5721
|
+
const tapY = clientY - rect.top;
|
|
5722
|
+
const maxDistanceSq = maxDistance * maxDistance;
|
|
5723
|
+
let bestIndex = -1;
|
|
5724
|
+
let bestDistanceSq = maxDistanceSq;
|
|
5725
|
+
let bestDepth = Infinity;
|
|
5726
|
+
for (const seat of input.seats) {
|
|
5727
|
+
if (sectionId && seat.sectionId !== sectionId) continue;
|
|
5728
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
5729
|
+
if (index === void 0) continue;
|
|
5730
|
+
if (bookableOnly) {
|
|
5731
|
+
const state = Math.round(model.seats.iState[index] ?? 0);
|
|
5732
|
+
if (state !== 0 && state !== 3) continue;
|
|
5733
|
+
}
|
|
5734
|
+
const offset = index * 3;
|
|
5735
|
+
const screen = projectToScreen(
|
|
5736
|
+
orbit.camera.projectionViewMatrix,
|
|
5737
|
+
[
|
|
5738
|
+
model.seats.iPosition[offset],
|
|
5739
|
+
// Match the visible chair/number rather than the deck-level instance
|
|
5740
|
+
// origin. At strong zoom the vertical difference is dozens of pixels
|
|
5741
|
+
// and a perfectly reasonable click on the chair otherwise misses.
|
|
5742
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
5743
|
+
model.seats.iPosition[offset + 2]
|
|
5744
|
+
],
|
|
5745
|
+
width,
|
|
5746
|
+
height
|
|
5747
|
+
);
|
|
5748
|
+
if (!screen.visible) continue;
|
|
5749
|
+
const dx = screen.x - tapX;
|
|
5750
|
+
const dy = screen.y - tapY;
|
|
5751
|
+
const distanceSq = dx * dx + dy * dy;
|
|
5752
|
+
if (distanceSq < bestDistanceSq || Math.abs(distanceSq - bestDistanceSq) < 1 && screen.depth < bestDepth) {
|
|
5753
|
+
bestIndex = index;
|
|
5754
|
+
bestDistanceSq = distanceSq;
|
|
5755
|
+
bestDepth = screen.depth;
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5758
|
+
return bestIndex;
|
|
5759
|
+
};
|
|
5143
5760
|
const onUp = (e) => {
|
|
5144
5761
|
if (e.pointerId !== downId) return;
|
|
5145
5762
|
const isTap = !moved && performance.now() - downT < TAP_MS;
|
|
@@ -5149,12 +5766,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5149
5766
|
return;
|
|
5150
5767
|
}
|
|
5151
5768
|
if (!isTap || !gpu || !pick) return;
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5769
|
+
let idx = focusedSectionId ? pickNearestProjectedSeat(e.clientX, e.clientY, focusedSectionId, 44, true) : -1;
|
|
5770
|
+
if (focusedSectionId && idx < 0) {
|
|
5771
|
+
const sectionIndex = pickNearestProjectedSeat(e.clientX, e.clientY, null, 72, false);
|
|
5772
|
+
const sectionSeatId = sectionIndex >= 0 ? seatIdByIndex[sectionIndex] : void 0;
|
|
5773
|
+
const nextSectionId = sectionSeatId ? sectionIdBySeatId.get(sectionSeatId) : void 0;
|
|
5774
|
+
if (nextSectionId && nextSectionId !== focusedSectionId && focusSectionCamera(nextSectionId)) return;
|
|
5775
|
+
}
|
|
5776
|
+
if (!focusedSectionId) {
|
|
5777
|
+
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
5778
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
5779
|
+
const dpr = glctx.renderer.dpr;
|
|
5780
|
+
const { x, y } = pickPixelCoords(e.clientX, e.clientY, rect, dpr, glctx.gl.drawingBufferWidth, glctx.gl.drawingBufferHeight);
|
|
5781
|
+
const radius = Math.max(2, Math.round(8 * dpr));
|
|
5782
|
+
idx = pick.pick(orbit.camera, x, y, radius);
|
|
5783
|
+
if (idx < 0) idx = pickNearestProjectedSeat(e.clientX, e.clientY, null, 42, false);
|
|
5784
|
+
const overviewSeatId = idx >= 0 ? seatIdByIndex[idx] : void 0;
|
|
5785
|
+
const sectionId = overviewSeatId ? sectionIdBySeatId.get(overviewSeatId) : void 0;
|
|
5786
|
+
if (sectionId && focusSectionCamera(sectionId)) return;
|
|
5787
|
+
}
|
|
5158
5788
|
if (idx < 0 || idx >= seatIdByIndex.length) {
|
|
5159
5789
|
if (selection.size) setSelection([]);
|
|
5160
5790
|
return;
|
|
@@ -5180,9 +5810,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5180
5810
|
},
|
|
5181
5811
|
setSelection,
|
|
5182
5812
|
flyToSeat,
|
|
5813
|
+
focusOverview,
|
|
5183
5814
|
resize() {
|
|
5184
5815
|
const { width, height } = glctx.resize();
|
|
5185
5816
|
orbit.setAspect(width / Math.max(1, height));
|
|
5817
|
+
layoutArriveChip();
|
|
5818
|
+
layoutNavigationChip();
|
|
5186
5819
|
loop.requestRender();
|
|
5187
5820
|
},
|
|
5188
5821
|
stats() {
|
|
@@ -5207,6 +5840,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5207
5840
|
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
5208
5841
|
}
|
|
5209
5842
|
focusedFloor = value;
|
|
5843
|
+
setNavigationMode("orbit");
|
|
5210
5844
|
if (index !== null) {
|
|
5211
5845
|
const f = model.floors[index];
|
|
5212
5846
|
if (f.seatCount > 0) {
|
|
@@ -5224,6 +5858,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5224
5858
|
const zone = model.zones.find((z) => z.id === zoneId);
|
|
5225
5859
|
if (!zone || zone.seatCount === 0) return false;
|
|
5226
5860
|
cinematic.cancel();
|
|
5861
|
+
setNavigationMode("orbit");
|
|
5862
|
+
showSectionSeatLabels(null);
|
|
5863
|
+
opts.onSectionFocusChange?.(null);
|
|
5227
5864
|
const dx = zone.focalWorld[0] - zone.center[0];
|
|
5228
5865
|
const dz = zone.focalWorld[2] - zone.center[2];
|
|
5229
5866
|
const azimuth = Math.hypot(dx, dz) > zone.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
@@ -5235,13 +5872,24 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5235
5872
|
return model.sections;
|
|
5236
5873
|
},
|
|
5237
5874
|
focusSection(sectionId) {
|
|
5238
|
-
|
|
5239
|
-
|
|
5875
|
+
return focusSectionCamera(sectionId);
|
|
5876
|
+
},
|
|
5877
|
+
rows(sectionId) {
|
|
5878
|
+
return sectionId ? model.rows.filter((row) => row.sectionId === sectionId) : model.rows;
|
|
5879
|
+
},
|
|
5880
|
+
seatsInRow(rowId) {
|
|
5881
|
+
return input.seats.filter((seat) => seat.rowId === rowId).map((seat) => ({ id: seat.id, label: seat.displayLabel || seat.label }));
|
|
5882
|
+
},
|
|
5883
|
+
focusRow(rowId) {
|
|
5884
|
+
const row = model.rows.find((candidate) => candidate.id === rowId);
|
|
5885
|
+
if (!row || row.seatCount === 0) return false;
|
|
5240
5886
|
cinematic.cancel();
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5887
|
+
setNavigationMode("pan");
|
|
5888
|
+
if (row.sectionId) {
|
|
5889
|
+
showSectionSeatLabels(row.sectionId);
|
|
5890
|
+
opts.onSectionFocusChange?.(row.sectionId);
|
|
5891
|
+
}
|
|
5892
|
+
orbit.frame({ center: row.center, radius: Math.max(2.5, row.radius * 0.65) });
|
|
5245
5893
|
loop.requestRender();
|
|
5246
5894
|
return true;
|
|
5247
5895
|
},
|
|
@@ -5252,6 +5900,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5252
5900
|
disposed = true;
|
|
5253
5901
|
removeArriveChip();
|
|
5254
5902
|
overviewChip.remove();
|
|
5903
|
+
navigationModeChip.remove();
|
|
5255
5904
|
cancelFlight();
|
|
5256
5905
|
loop.stop();
|
|
5257
5906
|
labelOverlay.dispose();
|
|
@@ -5260,6 +5909,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5260
5909
|
panorama.dispose();
|
|
5261
5910
|
panorama = null;
|
|
5262
5911
|
}
|
|
5912
|
+
restorePanoramaLayer();
|
|
5913
|
+
restoreContainerPosition();
|
|
5263
5914
|
glctx.canvas.removeEventListener("pointerdown", onDown);
|
|
5264
5915
|
glctx.canvas.removeEventListener("pointermove", onMove);
|
|
5265
5916
|
glctx.canvas.removeEventListener("pointerup", onUp);
|
|
@@ -5272,11 +5923,20 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5272
5923
|
glctx.dispose();
|
|
5273
5924
|
}
|
|
5274
5925
|
};
|
|
5275
|
-
analytics.opened(
|
|
5926
|
+
analytics.opened(
|
|
5927
|
+
model.seatCount,
|
|
5928
|
+
hasHeights,
|
|
5929
|
+
input.prepared?.buildMs ?? performance.now() - buildStartedAt,
|
|
5930
|
+
// Known synchronously at mount: either the caller handed us a scene
|
|
5931
|
+
// prepared by `prepareVenue3D` (which reports worker vs. main-thread
|
|
5932
|
+
// fallback) or we compiled it inline here. Never delays the event.
|
|
5933
|
+
input.prepared?.source ?? "inline"
|
|
5934
|
+
);
|
|
5276
5935
|
return handle;
|
|
5277
5936
|
}
|
|
5278
5937
|
export {
|
|
5279
5938
|
buildSceneModel,
|
|
5280
|
-
mountVenue3D
|
|
5939
|
+
mountVenue3D,
|
|
5940
|
+
prepareVenue3D
|
|
5281
5941
|
};
|
|
5282
5942
|
//# sourceMappingURL=index.js.map
|