@seatlayer/core 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-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 +1161 -128
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +60 -35
- package/dist/view3d/index.d.ts +60 -35
- package/dist/view3d/index.js +945 -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,12 @@ var Analytics3D = class {
|
|
|
4763
4988
|
} catch {
|
|
4764
4989
|
}
|
|
4765
4990
|
}
|
|
4766
|
-
opened(seats, hasHeights) {
|
|
4767
|
-
this.emit("3d_opened", {
|
|
4991
|
+
opened(seats, hasHeights, buildMs) {
|
|
4992
|
+
this.emit("3d_opened", {
|
|
4993
|
+
seats,
|
|
4994
|
+
hasHeights,
|
|
4995
|
+
...buildMs === void 0 ? {} : { buildMs: Math.round(buildMs) }
|
|
4996
|
+
});
|
|
4768
4997
|
}
|
|
4769
4998
|
/** First user-driven orbit/dolly per mount only (the intro ease is not user
|
|
4770
4999
|
* input, so callers must gate this on real pointer/wheel gestures). */
|
|
@@ -4794,18 +5023,77 @@ var Analytics3D = class {
|
|
|
4794
5023
|
this.panoramaOpenedAt = 0;
|
|
4795
5024
|
this.emit("3d_panorama_closed", { viewMs });
|
|
4796
5025
|
}
|
|
5026
|
+
panoramaFailed(seatId, phase) {
|
|
5027
|
+
this.emit("3d_panorama_failed", { seatId, phase });
|
|
5028
|
+
}
|
|
5029
|
+
panoramaFallback(seatId) {
|
|
5030
|
+
this.emit("3d_panorama_fallback", { seatId });
|
|
5031
|
+
}
|
|
5032
|
+
contextLost() {
|
|
5033
|
+
this.emit("3d_context_lost");
|
|
5034
|
+
}
|
|
5035
|
+
contextRestored() {
|
|
5036
|
+
this.emit("3d_context_restored");
|
|
5037
|
+
}
|
|
4797
5038
|
};
|
|
4798
5039
|
|
|
5040
|
+
// src/view3d/positioningContext.ts
|
|
5041
|
+
function establishPositioningContext(container) {
|
|
5042
|
+
const originalInline = container.style.position;
|
|
5043
|
+
if (getComputedStyle(container).position !== "static") return () => {
|
|
5044
|
+
};
|
|
5045
|
+
container.style.position = "relative";
|
|
5046
|
+
return () => {
|
|
5047
|
+
if (container.style.position === "relative") container.style.position = originalInline;
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
// src/view3d/prepareScene.ts
|
|
5052
|
+
function prepareOnMain(input) {
|
|
5053
|
+
const started = performance.now();
|
|
5054
|
+
const model = buildSceneModel(input);
|
|
5055
|
+
return { model, buildMs: performance.now() - started, source: "main" };
|
|
5056
|
+
}
|
|
5057
|
+
function prepareVenue3D(input) {
|
|
5058
|
+
if (typeof Worker === "undefined") return Promise.resolve(prepareOnMain(input));
|
|
5059
|
+
return new Promise((resolve) => {
|
|
5060
|
+
const worker = new Worker(new URL("./scene/scene.worker.ts", import.meta.url), { type: "module" });
|
|
5061
|
+
let settled = false;
|
|
5062
|
+
const finish = (result) => {
|
|
5063
|
+
if (settled) return;
|
|
5064
|
+
settled = true;
|
|
5065
|
+
clearTimeout(timeout);
|
|
5066
|
+
worker.terminate();
|
|
5067
|
+
resolve(result);
|
|
5068
|
+
};
|
|
5069
|
+
const fallback = () => finish(prepareOnMain(input));
|
|
5070
|
+
const timeout = window.setTimeout(fallback, 15e3);
|
|
5071
|
+
worker.onerror = fallback;
|
|
5072
|
+
worker.onmessage = (event) => {
|
|
5073
|
+
finish({ model: event.data.model, buildMs: event.data.buildMs, source: "worker" });
|
|
5074
|
+
};
|
|
5075
|
+
worker.postMessage(input);
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
|
|
4799
5079
|
// src/view3d/index.ts
|
|
4800
|
-
var SEAT_EYE_ABOVE_DECK = 1.02;
|
|
4801
5080
|
var DEG2 = Math.PI / 180;
|
|
4802
5081
|
var TAP_SLOP = 6;
|
|
4803
5082
|
var TAP_MS = 500;
|
|
4804
5083
|
function mountVenue3D(container, input, opts = {}) {
|
|
4805
|
-
const
|
|
5084
|
+
const buildStartedAt = performance.now();
|
|
5085
|
+
const model = input.prepared?.model ?? buildSceneModel(input);
|
|
4806
5086
|
const analytics = new Analytics3D(opts.onAnalytics);
|
|
4807
5087
|
const seatIdByIndex = new Array(model.seats.count);
|
|
4808
5088
|
for (const [id, idx] of model.seats.idToIndex) seatIdByIndex[idx] = id;
|
|
5089
|
+
const expandedSeatById = new Map(input.seats.map((seat) => [seat.id, seat]));
|
|
5090
|
+
const seatIdsByRow = /* @__PURE__ */ new Map();
|
|
5091
|
+
for (const seat of input.seats) {
|
|
5092
|
+
if (!seat.rowId) continue;
|
|
5093
|
+
const row = seatIdsByRow.get(seat.rowId);
|
|
5094
|
+
if (row) row.push(seat.id);
|
|
5095
|
+
else seatIdsByRow.set(seat.rowId, [seat.id]);
|
|
5096
|
+
}
|
|
4809
5097
|
const sectionIdBySeatId = /* @__PURE__ */ new Map();
|
|
4810
5098
|
for (const s of input.seats) sectionIdBySeatId.set(s.id, s.sectionId);
|
|
4811
5099
|
const hasHeights = (() => {
|
|
@@ -4820,10 +5108,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4820
5108
|
let disposed = false;
|
|
4821
5109
|
let selection = /* @__PURE__ */ new Map();
|
|
4822
5110
|
let panorama = null;
|
|
5111
|
+
let panoramaLoadAbort = null;
|
|
5112
|
+
let panoramaPriorZIndex = null;
|
|
4823
5113
|
const prefetch = /* @__PURE__ */ new Map();
|
|
4824
5114
|
let flightGen = 0;
|
|
4825
5115
|
let reducedForced = null;
|
|
4826
5116
|
let focusedFloor = -1;
|
|
5117
|
+
const raisePanoramaLayer = () => {
|
|
5118
|
+
if (panoramaPriorZIndex !== null) return;
|
|
5119
|
+
panoramaPriorZIndex = container.style.zIndex;
|
|
5120
|
+
container.style.zIndex = "20";
|
|
5121
|
+
};
|
|
5122
|
+
const restorePanoramaLayer = () => {
|
|
5123
|
+
if (panoramaPriorZIndex === null) return;
|
|
5124
|
+
if (container.style.zIndex === "20") container.style.zIndex = panoramaPriorZIndex;
|
|
5125
|
+
panoramaPriorZIndex = null;
|
|
5126
|
+
};
|
|
4827
5127
|
const rebuildGpu = () => {
|
|
4828
5128
|
gpu = buildGpuScene(glctx.gl, model);
|
|
4829
5129
|
pick = new PickPipeline(glctx.renderer, gpu.seatGeometry, gpu.solidGeometry, model.seats.count);
|
|
@@ -4857,6 +5157,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4857
5157
|
};
|
|
4858
5158
|
const glctx = new GLContext(container, {
|
|
4859
5159
|
onContextLost: () => {
|
|
5160
|
+
if (!disposed) analytics.contextLost();
|
|
4860
5161
|
contextLost = true;
|
|
4861
5162
|
loop.stop();
|
|
4862
5163
|
gpu = null;
|
|
@@ -4865,6 +5166,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4865
5166
|
onContextRestored: () => {
|
|
4866
5167
|
rebuildGpu();
|
|
4867
5168
|
contextLost = false;
|
|
5169
|
+
if (!disposed) analytics.contextRestored();
|
|
4868
5170
|
loop.requestRender();
|
|
4869
5171
|
}
|
|
4870
5172
|
});
|
|
@@ -4881,16 +5183,48 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4881
5183
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
4882
5184
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
4883
5185
|
})();
|
|
4884
|
-
orbit.frame(model.bounds, true, stageAzimuth);
|
|
5186
|
+
orbit.frame(model.bounds, true, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
4885
5187
|
const cinematic = new Cinematic(orbit.camera);
|
|
4886
|
-
|
|
4887
|
-
container.style.position = "relative";
|
|
4888
|
-
}
|
|
5188
|
+
const restoreContainerPosition = establishPositioningContext(container);
|
|
4889
5189
|
const labelOverlay = new LabelOverlay(container, {
|
|
4890
5190
|
fontFamily: input.doc.theme?.fontFamily,
|
|
4891
5191
|
ink: input.doc.theme?.textColor
|
|
4892
5192
|
});
|
|
4893
5193
|
labelOverlay.setLabels(model.labels);
|
|
5194
|
+
const baseLabels = model.labels;
|
|
5195
|
+
let focusedSectionId = null;
|
|
5196
|
+
const showSectionSeatLabels = (sectionId) => {
|
|
5197
|
+
focusedSectionId = sectionId;
|
|
5198
|
+
labelOverlay.setForcedDense(!!sectionId);
|
|
5199
|
+
if (!sectionId) {
|
|
5200
|
+
labelOverlay.setLabels(baseLabels);
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
5203
|
+
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
5204
|
+
const seatIds = new Set(sectionSeats.map((seat) => seat.id));
|
|
5205
|
+
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
5206
|
+
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))));
|
|
5207
|
+
if (model.seatCount <= 6e3) {
|
|
5208
|
+
labelOverlay.setLabels(focusedBase);
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
const labels = sectionSeats.flatMap((seat) => {
|
|
5212
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
5213
|
+
if (index === void 0) return [];
|
|
5214
|
+
const offset = index * 3;
|
|
5215
|
+
return [{
|
|
5216
|
+
id: `seat:${seat.id}`,
|
|
5217
|
+
kind: "seat",
|
|
5218
|
+
text: seat.displayLabel || seat.label,
|
|
5219
|
+
anchor: [
|
|
5220
|
+
model.seats.iPosition[offset],
|
|
5221
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
5222
|
+
model.seats.iPosition[offset + 2]
|
|
5223
|
+
]
|
|
5224
|
+
}];
|
|
5225
|
+
});
|
|
5226
|
+
labelOverlay.setLabels([...focusedBase, ...labels]);
|
|
5227
|
+
};
|
|
4894
5228
|
rebuildGpu();
|
|
4895
5229
|
const loop = new RenderLoop(() => {
|
|
4896
5230
|
if (contextLost || !gpu || frozen) return false;
|
|
@@ -4943,6 +5277,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4943
5277
|
if (!p) {
|
|
4944
5278
|
p = Promise.resolve(opts.getSeatView(seatId));
|
|
4945
5279
|
prefetch.set(seatId, p);
|
|
5280
|
+
void p.catch(() => {
|
|
5281
|
+
if (prefetch.get(seatId) === p) prefetch.delete(seatId);
|
|
5282
|
+
});
|
|
4946
5283
|
while (prefetch.size > PREFETCH_CAP) {
|
|
4947
5284
|
const oldest = prefetch.keys().next().value;
|
|
4948
5285
|
if (oldest === void 0) break;
|
|
@@ -4951,11 +5288,18 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4951
5288
|
}
|
|
4952
5289
|
return p;
|
|
4953
5290
|
};
|
|
4954
|
-
const
|
|
4955
|
-
|
|
4956
|
-
model.seats.
|
|
4957
|
-
|
|
4958
|
-
|
|
5291
|
+
const resolvedSeatViewPose = (seatId, idx) => {
|
|
5292
|
+
const seat = expandedSeatById.get(seatId);
|
|
5293
|
+
const floorIndex = Math.max(0, Math.round(model.seats.iFloor[idx] ?? 0));
|
|
5294
|
+
const floor = input.doc.floors?.[floorIndex];
|
|
5295
|
+
const focalPoint = seat?.focalPoint ?? floor?.focalPoint ?? input.doc.focalPoint;
|
|
5296
|
+
const deck = [
|
|
5297
|
+
model.seats.iPosition[idx * 3],
|
|
5298
|
+
model.seats.iPosition[idx * 3 + 1],
|
|
5299
|
+
model.seats.iPosition[idx * 3 + 2]
|
|
5300
|
+
];
|
|
5301
|
+
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0);
|
|
5302
|
+
};
|
|
4959
5303
|
const placeCameraFinal = (finalPos, focal) => {
|
|
4960
5304
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
4961
5305
|
orbit.camera.lookAt(new Vec33(focal[0], focal[1], focal[2]));
|
|
@@ -4963,44 +5307,188 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4963
5307
|
orbit.camera.updateProjectionMatrix();
|
|
4964
5308
|
};
|
|
4965
5309
|
let arriveChip = null;
|
|
5310
|
+
const layoutArriveChip = () => {
|
|
5311
|
+
if (!arriveChip) return;
|
|
5312
|
+
const narrow = (container.clientWidth || glctx.canvas.clientWidth) <= 520;
|
|
5313
|
+
Object.assign(arriveChip.style, narrow ? {
|
|
5314
|
+
left: "12px",
|
|
5315
|
+
right: "12px",
|
|
5316
|
+
bottom: "70px",
|
|
5317
|
+
transform: "none",
|
|
5318
|
+
justifyContent: "center"
|
|
5319
|
+
} : {
|
|
5320
|
+
left: "50%",
|
|
5321
|
+
right: "auto",
|
|
5322
|
+
bottom: "18px",
|
|
5323
|
+
transform: "translateX(-50%)",
|
|
5324
|
+
justifyContent: "initial"
|
|
5325
|
+
});
|
|
5326
|
+
};
|
|
4966
5327
|
const removeArriveChip = () => {
|
|
4967
5328
|
arriveChip?.remove();
|
|
4968
5329
|
arriveChip = null;
|
|
4969
5330
|
};
|
|
4970
|
-
const showArriveChip = (seatId, gen) => {
|
|
5331
|
+
const showArriveChip = (seatId, gen, retry = false) => {
|
|
4971
5332
|
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, {
|
|
5333
|
+
if (disposed) return null;
|
|
5334
|
+
const controls = document.createElement("div");
|
|
5335
|
+
controls.setAttribute("role", "group");
|
|
5336
|
+
Object.assign(controls.style, {
|
|
4978
5337
|
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",
|
|
5338
|
+
display: "flex",
|
|
5339
|
+
flexDirection: "column",
|
|
5340
|
+
alignItems: "center",
|
|
5341
|
+
gap: "6px",
|
|
4991
5342
|
zIndex: "4"
|
|
4992
5343
|
});
|
|
4993
|
-
|
|
4994
|
-
|
|
5344
|
+
const seat = expandedSeatById.get(seatId);
|
|
5345
|
+
const section = seat?.sectionId ? model.sections.find((candidate) => candidate.id === seat.sectionId) : void 0;
|
|
5346
|
+
const seatLabel = seat?.label ?? seatId;
|
|
5347
|
+
controls.setAttribute("aria-label", `Explore views near ${section?.label ? `${section.label}, ` : ""}seat ${seatLabel}`);
|
|
5348
|
+
const location = document.createElement("span");
|
|
5349
|
+
location.textContent = ["Live 3D seat view", section?.label, seatLabel].filter(Boolean).join(" \xB7 ");
|
|
5350
|
+
Object.assign(location.style, {
|
|
5351
|
+
maxWidth: "min(320px, calc(100vw - 32px))",
|
|
5352
|
+
padding: "5px 10px",
|
|
5353
|
+
borderRadius: "999px",
|
|
5354
|
+
overflow: "hidden",
|
|
5355
|
+
color: "#d9e3f5",
|
|
5356
|
+
background: "rgba(12,18,32,0.72)",
|
|
5357
|
+
font: "600 11px/1.2 inherit",
|
|
5358
|
+
textOverflow: "ellipsis",
|
|
5359
|
+
whiteSpace: "nowrap",
|
|
5360
|
+
backdropFilter: "blur(6px)"
|
|
5361
|
+
});
|
|
5362
|
+
controls.appendChild(location);
|
|
5363
|
+
const actions = document.createElement("div");
|
|
5364
|
+
Object.assign(actions.style, {
|
|
5365
|
+
display: "flex",
|
|
5366
|
+
alignItems: "center",
|
|
5367
|
+
justifyContent: "center",
|
|
5368
|
+
gap: "6px"
|
|
5369
|
+
});
|
|
5370
|
+
controls.appendChild(actions);
|
|
5371
|
+
const row = seat?.rowId ? seatIdsByRow.get(seat.rowId) : void 0;
|
|
5372
|
+
const at = row?.indexOf(seatId) ?? -1;
|
|
5373
|
+
const neighbours = at >= 0 ? [row?.[at - 1], row?.[at + 1]] : [void 0, void 0];
|
|
5374
|
+
const addButton = (label, ariaLabel, action, primary = false) => {
|
|
5375
|
+
const button = document.createElement("button");
|
|
5376
|
+
button.type = "button";
|
|
5377
|
+
button.textContent = label;
|
|
5378
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
5379
|
+
Object.assign(button.style, {
|
|
5380
|
+
minHeight: "44px",
|
|
5381
|
+
padding: primary ? "10px 16px" : "10px",
|
|
5382
|
+
borderRadius: "999px",
|
|
5383
|
+
background: "rgba(12,18,32,0.78)",
|
|
5384
|
+
color: "#eef1f8",
|
|
5385
|
+
border: "1px solid rgba(150,165,205,0.4)",
|
|
5386
|
+
backdropFilter: "blur(6px)",
|
|
5387
|
+
font: "600 13px/1 inherit",
|
|
5388
|
+
cursor: "pointer",
|
|
5389
|
+
whiteSpace: "nowrap"
|
|
5390
|
+
});
|
|
5391
|
+
if (!primary) button.style.minWidth = "44px";
|
|
5392
|
+
if (primary) button.dataset.panoramaTrigger = "";
|
|
5393
|
+
button.addEventListener("click", action);
|
|
5394
|
+
actions.appendChild(button);
|
|
5395
|
+
};
|
|
5396
|
+
if (neighbours[0]) {
|
|
5397
|
+
const label = expandedSeatById.get(neighbours[0])?.label ?? neighbours[0];
|
|
5398
|
+
addButton("\u2039", `View previous seat ${label}`, () => {
|
|
5399
|
+
void flyToSeat(neighbours[0]);
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
if (opts.getSeatView) {
|
|
5403
|
+
addButton(retry ? "\u21BB Retry 360\xB0 panorama" : "\u25C9 Open 360\xB0 panorama", `${retry ? "Retry" : "Open"} the 360\xB0 panorama from seat ${seatLabel}`, () => {
|
|
5404
|
+
if (disposed || gen !== flightGen) {
|
|
5405
|
+
removeArriveChip();
|
|
5406
|
+
return;
|
|
5407
|
+
}
|
|
4995
5408
|
removeArriveChip();
|
|
4996
|
-
|
|
4997
|
-
}
|
|
4998
|
-
|
|
4999
|
-
|
|
5409
|
+
void openPanorama(seatId, 400, gen);
|
|
5410
|
+
}, true);
|
|
5411
|
+
}
|
|
5412
|
+
if (neighbours[1]) {
|
|
5413
|
+
const label = expandedSeatById.get(neighbours[1])?.label ?? neighbours[1];
|
|
5414
|
+
addButton("\u203A", `View next seat ${label}`, () => {
|
|
5415
|
+
void flyToSeat(neighbours[1]);
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
if (!actions.childElementCount) return null;
|
|
5419
|
+
container.appendChild(controls);
|
|
5420
|
+
arriveChip = controls;
|
|
5421
|
+
layoutArriveChip();
|
|
5422
|
+
return controls;
|
|
5423
|
+
};
|
|
5424
|
+
const navigationModeChip = document.createElement("div");
|
|
5425
|
+
navigationModeChip.setAttribute("role", "group");
|
|
5426
|
+
navigationModeChip.setAttribute("aria-label", "3D navigation mode");
|
|
5427
|
+
Object.assign(navigationModeChip.style, {
|
|
5428
|
+
position: "absolute",
|
|
5429
|
+
left: "14px",
|
|
5430
|
+
top: "62px",
|
|
5431
|
+
display: "flex",
|
|
5432
|
+
gap: "3px",
|
|
5433
|
+
padding: "3px",
|
|
5434
|
+
borderRadius: "999px",
|
|
5435
|
+
zIndex: "4",
|
|
5436
|
+
background: "rgba(12,18,32,0.72)",
|
|
5437
|
+
border: "1px solid rgba(150,165,205,0.35)",
|
|
5438
|
+
backdropFilter: "blur(6px)"
|
|
5439
|
+
});
|
|
5440
|
+
const rotateModeButton = document.createElement("button");
|
|
5441
|
+
rotateModeButton.type = "button";
|
|
5442
|
+
rotateModeButton.textContent = "\u21BB Rotate";
|
|
5443
|
+
rotateModeButton.setAttribute("aria-label", "Drag to rotate the 3D venue");
|
|
5444
|
+
const moveModeButton = document.createElement("button");
|
|
5445
|
+
moveModeButton.type = "button";
|
|
5446
|
+
moveModeButton.textContent = "\u2725 Move";
|
|
5447
|
+
moveModeButton.setAttribute("aria-label", "Drag to move the 3D venue left, right, up or down");
|
|
5448
|
+
const zoomOutButton = document.createElement("button");
|
|
5449
|
+
zoomOutButton.type = "button";
|
|
5450
|
+
zoomOutButton.textContent = "\u2212";
|
|
5451
|
+
zoomOutButton.setAttribute("aria-label", "Zoom out of the 3D venue");
|
|
5452
|
+
zoomOutButton.title = "Zoom out";
|
|
5453
|
+
const zoomInButton = document.createElement("button");
|
|
5454
|
+
zoomInButton.type = "button";
|
|
5455
|
+
zoomInButton.textContent = "+";
|
|
5456
|
+
zoomInButton.setAttribute("aria-label", "Zoom into the 3D venue");
|
|
5457
|
+
zoomInButton.title = "Zoom in";
|
|
5458
|
+
for (const button of [rotateModeButton, moveModeButton, zoomOutButton, zoomInButton]) {
|
|
5459
|
+
Object.assign(button.style, {
|
|
5460
|
+
minHeight: "34px",
|
|
5461
|
+
padding: "6px 10px",
|
|
5462
|
+
border: "0",
|
|
5463
|
+
borderRadius: "999px",
|
|
5464
|
+
color: "#c9d4ea",
|
|
5465
|
+
background: "transparent",
|
|
5466
|
+
font: "600 11px/1 inherit",
|
|
5467
|
+
cursor: "pointer",
|
|
5468
|
+
whiteSpace: "nowrap"
|
|
5000
5469
|
});
|
|
5001
|
-
|
|
5002
|
-
|
|
5470
|
+
if (button === zoomOutButton || button === zoomInButton) {
|
|
5471
|
+
button.style.minWidth = "34px";
|
|
5472
|
+
button.style.padding = "6px";
|
|
5473
|
+
button.style.fontSize = "17px";
|
|
5474
|
+
}
|
|
5475
|
+
navigationModeChip.appendChild(button);
|
|
5476
|
+
}
|
|
5477
|
+
const setNavigationMode = (mode) => {
|
|
5478
|
+
orbit.setPrimaryDragMode(mode);
|
|
5479
|
+
const rotateActive = mode === "orbit";
|
|
5480
|
+
rotateModeButton.setAttribute("aria-pressed", String(rotateActive));
|
|
5481
|
+
moveModeButton.setAttribute("aria-pressed", String(!rotateActive));
|
|
5482
|
+
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.48)" : "transparent";
|
|
5483
|
+
moveModeButton.style.background = rotateActive ? "transparent" : "rgba(96,110,150,0.48)";
|
|
5484
|
+
navigationModeChip.title = rotateActive ? "Drag to rotate \xB7 Shift-drag to move" : "Drag to move \xB7 Shift-drag to rotate";
|
|
5003
5485
|
};
|
|
5486
|
+
rotateModeButton.addEventListener("click", () => setNavigationMode("orbit"));
|
|
5487
|
+
moveModeButton.addEventListener("click", () => setNavigationMode("pan"));
|
|
5488
|
+
zoomOutButton.addEventListener("click", () => orbit.zoomBy(1.22));
|
|
5489
|
+
zoomInButton.addEventListener("click", () => orbit.zoomBy(0.82));
|
|
5490
|
+
setNavigationMode("orbit");
|
|
5491
|
+
container.appendChild(navigationModeChip);
|
|
5004
5492
|
const overviewChip = document.createElement("button");
|
|
5005
5493
|
overviewChip.type = "button";
|
|
5006
5494
|
overviewChip.textContent = "\u2302 Overview";
|
|
@@ -5020,17 +5508,46 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5020
5508
|
cursor: "pointer",
|
|
5021
5509
|
zIndex: "4"
|
|
5022
5510
|
});
|
|
5023
|
-
|
|
5024
|
-
if (disposed
|
|
5511
|
+
const focusOverview = () => {
|
|
5512
|
+
if (disposed) return;
|
|
5513
|
+
if (panorama) {
|
|
5514
|
+
panorama.dispose();
|
|
5515
|
+
panorama = null;
|
|
5516
|
+
analytics.panoramaClosed();
|
|
5517
|
+
}
|
|
5518
|
+
panoramaLoadAbort?.abort();
|
|
5519
|
+
panoramaLoadAbort = null;
|
|
5520
|
+
restorePanoramaLayer();
|
|
5521
|
+
overviewChip.style.display = "";
|
|
5522
|
+
labelOverlay.setVisible(true);
|
|
5523
|
+
frozen = false;
|
|
5025
5524
|
cancelFlight();
|
|
5026
5525
|
removeArriveChip();
|
|
5027
|
-
orbit
|
|
5526
|
+
setNavigationMode("orbit");
|
|
5527
|
+
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5528
|
+
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5529
|
+
opts.onViewTargetChange?.(null);
|
|
5530
|
+
showSectionSeatLabels(null);
|
|
5531
|
+
opts.onSectionFocusChange?.(null);
|
|
5028
5532
|
loop.requestRender();
|
|
5029
|
-
}
|
|
5533
|
+
};
|
|
5534
|
+
overviewChip.addEventListener("click", focusOverview);
|
|
5030
5535
|
container.appendChild(overviewChip);
|
|
5031
5536
|
const openPanorama = async (seatId, fadeMs, gen) => {
|
|
5537
|
+
panoramaLoadAbort?.abort();
|
|
5538
|
+
const loadAbort = new AbortController();
|
|
5539
|
+
panoramaLoadAbort = loadAbort;
|
|
5540
|
+
const seatIndex = model.seats.idToIndex.get(seatId);
|
|
5541
|
+
if (seatIndex === void 0) {
|
|
5542
|
+
orbit.syncFromCamera();
|
|
5543
|
+
return;
|
|
5544
|
+
}
|
|
5545
|
+
const seatLabel = expandedSeatById.get(seatId)?.label ?? seatId;
|
|
5546
|
+
const seatPose = resolvedSeatViewPose(seatId, seatIndex);
|
|
5032
5547
|
const viewPromise = ensureSeatView(seatId);
|
|
5033
5548
|
if (!viewPromise) {
|
|
5549
|
+
loadAbort.abort();
|
|
5550
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5034
5551
|
orbit.syncFromCamera();
|
|
5035
5552
|
return;
|
|
5036
5553
|
}
|
|
@@ -5040,34 +5557,56 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5040
5557
|
try {
|
|
5041
5558
|
view = await viewPromise;
|
|
5042
5559
|
} catch {
|
|
5560
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5561
|
+
analytics.panoramaFailed(seatId, "source");
|
|
5043
5562
|
if (!disposed && gen === flightGen) {
|
|
5044
5563
|
frozen = false;
|
|
5045
|
-
orbit.resumeAfterFlight(
|
|
5564
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5046
5565
|
loop.requestRender();
|
|
5566
|
+
const controls = showArriveChip(seatId, gen, true);
|
|
5567
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5047
5568
|
}
|
|
5048
5569
|
return;
|
|
5049
5570
|
}
|
|
5050
|
-
if (disposed || gen !== flightGen)
|
|
5571
|
+
if (disposed || gen !== flightGen) {
|
|
5572
|
+
loadAbort.abort();
|
|
5573
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5574
|
+
return;
|
|
5575
|
+
}
|
|
5051
5576
|
removeArriveChip();
|
|
5052
5577
|
const onClose = () => {
|
|
5578
|
+
loadAbort.abort();
|
|
5579
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5053
5580
|
panorama = null;
|
|
5581
|
+
restorePanoramaLayer();
|
|
5582
|
+
overviewChip.style.display = "";
|
|
5054
5583
|
labelOverlay.setVisible(true);
|
|
5055
5584
|
frozen = false;
|
|
5056
5585
|
analytics.panoramaClosed();
|
|
5057
|
-
orbit.resumeAfterFlight(
|
|
5586
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5058
5587
|
loop.requestRender();
|
|
5059
|
-
showArriveChip(seatId, flightGen);
|
|
5588
|
+
const controls = showArriveChip(seatId, flightGen);
|
|
5589
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5060
5590
|
};
|
|
5061
5591
|
const sceneMode = view.generated === true;
|
|
5592
|
+
const disclosure = seatViewDisclosure(view);
|
|
5593
|
+
raisePanoramaLayer();
|
|
5594
|
+
overviewChip.style.display = "none";
|
|
5595
|
+
const effectiveFadeMs = reducedMotion() ? 0 : fadeMs;
|
|
5062
5596
|
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5063
5597
|
gl: glctx.gl,
|
|
5064
5598
|
scene: gpu.main,
|
|
5065
5599
|
camera: orbit.camera,
|
|
5066
5600
|
requestRender: () => loop.requestRender(),
|
|
5067
|
-
|
|
5068
|
-
|
|
5601
|
+
cameraOriginWorld: sceneMode ? seatPose.eye : void 0,
|
|
5602
|
+
focalWorld: seatPose.focal
|
|
5603
|
+
}, { fadeMs: effectiveFadeMs, seatLabel, disclosure, onClose, signal: loadAbort.signal }) : null;
|
|
5069
5604
|
if (disposed || gen !== flightGen) {
|
|
5605
|
+
loadAbort.abort();
|
|
5606
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5070
5607
|
spherical?.dispose();
|
|
5608
|
+
overviewChip.style.display = "";
|
|
5609
|
+
restorePanoramaLayer();
|
|
5071
5610
|
return;
|
|
5072
5611
|
}
|
|
5073
5612
|
if (spherical) {
|
|
@@ -5077,12 +5616,21 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5077
5616
|
loop.requestRender();
|
|
5078
5617
|
panorama = spherical;
|
|
5079
5618
|
} else {
|
|
5080
|
-
|
|
5619
|
+
analytics.panoramaFallback(seatId);
|
|
5620
|
+
panorama = mountPanorama(container, view, {
|
|
5621
|
+
fadeMs: effectiveFadeMs,
|
|
5622
|
+
seatLabel,
|
|
5623
|
+
disclosure,
|
|
5624
|
+
onClose,
|
|
5625
|
+
signal: loadAbort.signal
|
|
5626
|
+
});
|
|
5081
5627
|
}
|
|
5082
5628
|
analytics.panoramaOpened();
|
|
5083
5629
|
};
|
|
5084
5630
|
const cancelFlight = () => {
|
|
5085
5631
|
flightGen++;
|
|
5632
|
+
panoramaLoadAbort?.abort();
|
|
5633
|
+
panoramaLoadAbort = null;
|
|
5086
5634
|
if (cinematic.active) {
|
|
5087
5635
|
cinematic.cancel();
|
|
5088
5636
|
orbit.resumeAfterFlight(model.focalWorld);
|
|
@@ -5092,15 +5640,19 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5092
5640
|
if (disposed || !gpu) return Promise.resolve();
|
|
5093
5641
|
const idx = model.seats.idToIndex.get(seatId);
|
|
5094
5642
|
if (idx === void 0) return Promise.resolve();
|
|
5643
|
+
opts.onViewTargetChange?.(seatId);
|
|
5095
5644
|
if (panorama) {
|
|
5096
5645
|
panorama.dispose();
|
|
5097
5646
|
panorama = null;
|
|
5647
|
+
overviewChip.style.display = "";
|
|
5648
|
+
restorePanoramaLayer();
|
|
5098
5649
|
}
|
|
5650
|
+
panoramaLoadAbort?.abort();
|
|
5651
|
+
panoramaLoadAbort = null;
|
|
5099
5652
|
frozen = false;
|
|
5100
5653
|
const gen = ++flightGen;
|
|
5101
5654
|
removeArriveChip();
|
|
5102
|
-
const seatEye =
|
|
5103
|
-
const focal = model.focalWorld;
|
|
5655
|
+
const { eye: seatEye, focal } = resolvedSeatViewPose(seatId, idx);
|
|
5104
5656
|
const start = [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z];
|
|
5105
5657
|
const { waypoints, finalPos } = buildWaypoints(start, seatEye, focal, model.bounds.center, model.bounds.radius);
|
|
5106
5658
|
if (reducedMotion()) {
|
|
@@ -5117,11 +5669,26 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5117
5669
|
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
5118
5670
|
if (disposed || gen !== flightGen) return;
|
|
5119
5671
|
analytics.cinematicPlayed(FLIGHT_DURATION_MS);
|
|
5120
|
-
orbit.resumeAfterFlight(
|
|
5672
|
+
orbit.resumeAfterFlight(focal);
|
|
5121
5673
|
loop.requestRender();
|
|
5122
5674
|
showArriveChip(seatId, gen);
|
|
5123
5675
|
});
|
|
5124
5676
|
};
|
|
5677
|
+
const focusSectionCamera = (sectionId) => {
|
|
5678
|
+
const sec = model.sections.find((candidate) => candidate.id === sectionId);
|
|
5679
|
+
if (!sec || sec.seatCount === 0) return false;
|
|
5680
|
+
cinematic.cancel();
|
|
5681
|
+
removeArriveChip();
|
|
5682
|
+
setNavigationMode("pan");
|
|
5683
|
+
showSectionSeatLabels(sectionId);
|
|
5684
|
+
const dx = sec.focalWorld[0] - sec.center[0];
|
|
5685
|
+
const dz = sec.focalWorld[2] - sec.center[2];
|
|
5686
|
+
const azimuth = Math.hypot(dx, dz) > sec.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
5687
|
+
orbit.frame({ center: sec.center, radius: Math.max(5, sec.radius * 0.5) }, false, azimuth);
|
|
5688
|
+
opts.onSectionFocusChange?.(sectionId);
|
|
5689
|
+
loop.requestRender();
|
|
5690
|
+
return true;
|
|
5691
|
+
};
|
|
5125
5692
|
let downX = 0, downY = 0, downT = 0, downId = -1, moved = false, suppressTap = false;
|
|
5126
5693
|
const onDown = (e) => {
|
|
5127
5694
|
if (downId !== -1) return;
|
|
@@ -5140,6 +5707,50 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5140
5707
|
if (e.pointerId !== downId) return;
|
|
5141
5708
|
if (Math.hypot(e.clientX - downX, e.clientY - downY) > TAP_SLOP) moved = true;
|
|
5142
5709
|
};
|
|
5710
|
+
const pickNearestProjectedSeat = (clientX, clientY, sectionId, maxDistance, bookableOnly) => {
|
|
5711
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
5712
|
+
const width = glctx.canvas.clientWidth || rect.width || 1;
|
|
5713
|
+
const height = glctx.canvas.clientHeight || rect.height || 1;
|
|
5714
|
+
const tapX = clientX - rect.left;
|
|
5715
|
+
const tapY = clientY - rect.top;
|
|
5716
|
+
const maxDistanceSq = maxDistance * maxDistance;
|
|
5717
|
+
let bestIndex = -1;
|
|
5718
|
+
let bestDistanceSq = maxDistanceSq;
|
|
5719
|
+
let bestDepth = Infinity;
|
|
5720
|
+
for (const seat of input.seats) {
|
|
5721
|
+
if (sectionId && seat.sectionId !== sectionId) continue;
|
|
5722
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
5723
|
+
if (index === void 0) continue;
|
|
5724
|
+
if (bookableOnly) {
|
|
5725
|
+
const state = Math.round(model.seats.iState[index] ?? 0);
|
|
5726
|
+
if (state !== 0 && state !== 3) continue;
|
|
5727
|
+
}
|
|
5728
|
+
const offset = index * 3;
|
|
5729
|
+
const screen = projectToScreen(
|
|
5730
|
+
orbit.camera.projectionViewMatrix,
|
|
5731
|
+
[
|
|
5732
|
+
model.seats.iPosition[offset],
|
|
5733
|
+
// Match the visible chair/number rather than the deck-level instance
|
|
5734
|
+
// origin. At strong zoom the vertical difference is dozens of pixels
|
|
5735
|
+
// and a perfectly reasonable click on the chair otherwise misses.
|
|
5736
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
5737
|
+
model.seats.iPosition[offset + 2]
|
|
5738
|
+
],
|
|
5739
|
+
width,
|
|
5740
|
+
height
|
|
5741
|
+
);
|
|
5742
|
+
if (!screen.visible) continue;
|
|
5743
|
+
const dx = screen.x - tapX;
|
|
5744
|
+
const dy = screen.y - tapY;
|
|
5745
|
+
const distanceSq = dx * dx + dy * dy;
|
|
5746
|
+
if (distanceSq < bestDistanceSq || Math.abs(distanceSq - bestDistanceSq) < 1 && screen.depth < bestDepth) {
|
|
5747
|
+
bestIndex = index;
|
|
5748
|
+
bestDistanceSq = distanceSq;
|
|
5749
|
+
bestDepth = screen.depth;
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
return bestIndex;
|
|
5753
|
+
};
|
|
5143
5754
|
const onUp = (e) => {
|
|
5144
5755
|
if (e.pointerId !== downId) return;
|
|
5145
5756
|
const isTap = !moved && performance.now() - downT < TAP_MS;
|
|
@@ -5149,12 +5760,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5149
5760
|
return;
|
|
5150
5761
|
}
|
|
5151
5762
|
if (!isTap || !gpu || !pick) return;
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5763
|
+
let idx = focusedSectionId ? pickNearestProjectedSeat(e.clientX, e.clientY, focusedSectionId, 44, true) : -1;
|
|
5764
|
+
if (focusedSectionId && idx < 0) {
|
|
5765
|
+
const sectionIndex = pickNearestProjectedSeat(e.clientX, e.clientY, null, 72, false);
|
|
5766
|
+
const sectionSeatId = sectionIndex >= 0 ? seatIdByIndex[sectionIndex] : void 0;
|
|
5767
|
+
const nextSectionId = sectionSeatId ? sectionIdBySeatId.get(sectionSeatId) : void 0;
|
|
5768
|
+
if (nextSectionId && nextSectionId !== focusedSectionId && focusSectionCamera(nextSectionId)) return;
|
|
5769
|
+
}
|
|
5770
|
+
if (!focusedSectionId) {
|
|
5771
|
+
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
5772
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
5773
|
+
const dpr = glctx.renderer.dpr;
|
|
5774
|
+
const { x, y } = pickPixelCoords(e.clientX, e.clientY, rect, dpr, glctx.gl.drawingBufferWidth, glctx.gl.drawingBufferHeight);
|
|
5775
|
+
const radius = Math.max(2, Math.round(8 * dpr));
|
|
5776
|
+
idx = pick.pick(orbit.camera, x, y, radius);
|
|
5777
|
+
if (idx < 0) idx = pickNearestProjectedSeat(e.clientX, e.clientY, null, 42, false);
|
|
5778
|
+
const overviewSeatId = idx >= 0 ? seatIdByIndex[idx] : void 0;
|
|
5779
|
+
const sectionId = overviewSeatId ? sectionIdBySeatId.get(overviewSeatId) : void 0;
|
|
5780
|
+
if (sectionId && focusSectionCamera(sectionId)) return;
|
|
5781
|
+
}
|
|
5158
5782
|
if (idx < 0 || idx >= seatIdByIndex.length) {
|
|
5159
5783
|
if (selection.size) setSelection([]);
|
|
5160
5784
|
return;
|
|
@@ -5180,9 +5804,11 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5180
5804
|
},
|
|
5181
5805
|
setSelection,
|
|
5182
5806
|
flyToSeat,
|
|
5807
|
+
focusOverview,
|
|
5183
5808
|
resize() {
|
|
5184
5809
|
const { width, height } = glctx.resize();
|
|
5185
5810
|
orbit.setAspect(width / Math.max(1, height));
|
|
5811
|
+
layoutArriveChip();
|
|
5186
5812
|
loop.requestRender();
|
|
5187
5813
|
},
|
|
5188
5814
|
stats() {
|
|
@@ -5207,6 +5833,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5207
5833
|
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
5208
5834
|
}
|
|
5209
5835
|
focusedFloor = value;
|
|
5836
|
+
setNavigationMode("orbit");
|
|
5210
5837
|
if (index !== null) {
|
|
5211
5838
|
const f = model.floors[index];
|
|
5212
5839
|
if (f.seatCount > 0) {
|
|
@@ -5224,6 +5851,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5224
5851
|
const zone = model.zones.find((z) => z.id === zoneId);
|
|
5225
5852
|
if (!zone || zone.seatCount === 0) return false;
|
|
5226
5853
|
cinematic.cancel();
|
|
5854
|
+
setNavigationMode("orbit");
|
|
5855
|
+
showSectionSeatLabels(null);
|
|
5856
|
+
opts.onSectionFocusChange?.(null);
|
|
5227
5857
|
const dx = zone.focalWorld[0] - zone.center[0];
|
|
5228
5858
|
const dz = zone.focalWorld[2] - zone.center[2];
|
|
5229
5859
|
const azimuth = Math.hypot(dx, dz) > zone.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
@@ -5235,13 +5865,24 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5235
5865
|
return model.sections;
|
|
5236
5866
|
},
|
|
5237
5867
|
focusSection(sectionId) {
|
|
5238
|
-
|
|
5239
|
-
|
|
5868
|
+
return focusSectionCamera(sectionId);
|
|
5869
|
+
},
|
|
5870
|
+
rows(sectionId) {
|
|
5871
|
+
return sectionId ? model.rows.filter((row) => row.sectionId === sectionId) : model.rows;
|
|
5872
|
+
},
|
|
5873
|
+
seatsInRow(rowId) {
|
|
5874
|
+
return input.seats.filter((seat) => seat.rowId === rowId).map((seat) => ({ id: seat.id, label: seat.displayLabel || seat.label }));
|
|
5875
|
+
},
|
|
5876
|
+
focusRow(rowId) {
|
|
5877
|
+
const row = model.rows.find((candidate) => candidate.id === rowId);
|
|
5878
|
+
if (!row || row.seatCount === 0) return false;
|
|
5240
5879
|
cinematic.cancel();
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5880
|
+
setNavigationMode("pan");
|
|
5881
|
+
if (row.sectionId) {
|
|
5882
|
+
showSectionSeatLabels(row.sectionId);
|
|
5883
|
+
opts.onSectionFocusChange?.(row.sectionId);
|
|
5884
|
+
}
|
|
5885
|
+
orbit.frame({ center: row.center, radius: Math.max(2.5, row.radius * 0.65) });
|
|
5245
5886
|
loop.requestRender();
|
|
5246
5887
|
return true;
|
|
5247
5888
|
},
|
|
@@ -5252,6 +5893,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5252
5893
|
disposed = true;
|
|
5253
5894
|
removeArriveChip();
|
|
5254
5895
|
overviewChip.remove();
|
|
5896
|
+
navigationModeChip.remove();
|
|
5255
5897
|
cancelFlight();
|
|
5256
5898
|
loop.stop();
|
|
5257
5899
|
labelOverlay.dispose();
|
|
@@ -5260,6 +5902,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5260
5902
|
panorama.dispose();
|
|
5261
5903
|
panorama = null;
|
|
5262
5904
|
}
|
|
5905
|
+
restorePanoramaLayer();
|
|
5906
|
+
restoreContainerPosition();
|
|
5263
5907
|
glctx.canvas.removeEventListener("pointerdown", onDown);
|
|
5264
5908
|
glctx.canvas.removeEventListener("pointermove", onMove);
|
|
5265
5909
|
glctx.canvas.removeEventListener("pointerup", onUp);
|
|
@@ -5272,11 +5916,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5272
5916
|
glctx.dispose();
|
|
5273
5917
|
}
|
|
5274
5918
|
};
|
|
5275
|
-
analytics.opened(model.seatCount, hasHeights);
|
|
5919
|
+
analytics.opened(model.seatCount, hasHeights, input.prepared?.buildMs ?? performance.now() - buildStartedAt);
|
|
5276
5920
|
return handle;
|
|
5277
5921
|
}
|
|
5278
5922
|
export {
|
|
5279
5923
|
buildSceneModel,
|
|
5280
|
-
mountVenue3D
|
|
5924
|
+
mountVenue3D,
|
|
5925
|
+
prepareVenue3D
|
|
5281
5926
|
};
|
|
5282
5927
|
//# sourceMappingURL=index.js.map
|