@seatlayer/core 0.46.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.cjs
CHANGED
|
@@ -31,7 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var view3d_exports = {};
|
|
32
32
|
__export(view3d_exports, {
|
|
33
33
|
buildSceneModel: () => buildSceneModel,
|
|
34
|
-
mountVenue3D: () => mountVenue3D
|
|
34
|
+
mountVenue3D: () => mountVenue3D,
|
|
35
|
+
prepareVenue3D: () => prepareVenue3D
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(view3d_exports);
|
|
37
38
|
var import_ogl8 = require("ogl");
|
|
@@ -134,6 +135,12 @@ var GLContext = class {
|
|
|
134
135
|
this.canvas.style.width = "100%";
|
|
135
136
|
this.canvas.style.height = "100%";
|
|
136
137
|
this.canvas.style.touchAction = "none";
|
|
138
|
+
this.canvas.tabIndex = 0;
|
|
139
|
+
this.canvas.setAttribute("role", "application");
|
|
140
|
+
this.canvas.setAttribute(
|
|
141
|
+
"aria-label",
|
|
142
|
+
"Interactive 3D venue. Drag or use arrow keys to look around; pinch, scroll, plus or minus to zoom."
|
|
143
|
+
);
|
|
137
144
|
this.renderer = new import_ogl.Renderer({
|
|
138
145
|
canvas: this.canvas,
|
|
139
146
|
dpr: computeDpr(),
|
|
@@ -242,6 +249,10 @@ var OrbitCamera = class {
|
|
|
242
249
|
/** Venue centre + radius, so pan can be clamped to somewhere still useful. */
|
|
243
250
|
this.panAnchor = new import_ogl2.Vec3();
|
|
244
251
|
this.panLimit = 0;
|
|
252
|
+
/** What an unmodified primary-button / one-finger drag does. Section drill-in
|
|
253
|
+
* switches this to pan so a buyer can slide hidden seats into view without
|
|
254
|
+
* discovering a modifier gesture. Shift temporarily inverts the mode. */
|
|
255
|
+
this.primaryDragMode = "orbit";
|
|
245
256
|
this.camera = new import_ogl2.Camera(gl, { fov: FOV, near: 0.1, far: 5e3, aspect: 1 });
|
|
246
257
|
this.canvas = canvas;
|
|
247
258
|
this.requestRender = requestRender;
|
|
@@ -253,7 +264,9 @@ var OrbitCamera = class {
|
|
|
253
264
|
}
|
|
254
265
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
255
266
|
if (this.activePointers.size === 1) {
|
|
256
|
-
|
|
267
|
+
const secondaryPan = e.button === 2 || e.button === 1;
|
|
268
|
+
const primaryPan = e.button === 0 && (e.shiftKey ? this.primaryDragMode === "orbit" : this.primaryDragMode === "pan");
|
|
269
|
+
this.panning = secondaryPan || primaryPan;
|
|
257
270
|
this.dragging = !this.panning;
|
|
258
271
|
this.lastX = e.clientX;
|
|
259
272
|
this.lastY = e.clientY;
|
|
@@ -319,15 +332,38 @@ var OrbitCamera = class {
|
|
|
319
332
|
this.onWheel = (e) => {
|
|
320
333
|
e.preventDefault();
|
|
321
334
|
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
322
|
-
const norm2 = e.deltaY * unit / 100;
|
|
323
|
-
this.dollyBy(Math.exp(norm2 * 0.
|
|
335
|
+
const norm2 = Math.max(-2, Math.min(2, e.deltaY * unit / 100));
|
|
336
|
+
this.dollyBy(Math.exp(norm2 * 0.22));
|
|
337
|
+
this.fireGesture();
|
|
338
|
+
};
|
|
339
|
+
this.onKeyDown = (e) => {
|
|
340
|
+
const step = 7 * DEG;
|
|
341
|
+
if (e.shiftKey && (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
342
|
+
this.panBy(
|
|
343
|
+
e.key === "ArrowLeft" ? -32 : e.key === "ArrowRight" ? 32 : 0,
|
|
344
|
+
e.key === "ArrowUp" ? -32 : e.key === "ArrowDown" ? 32 : 0
|
|
345
|
+
);
|
|
346
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
347
|
+
this.azT += e.key === "ArrowLeft" ? -step : step;
|
|
348
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
349
|
+
this.polT = Math.max(POLAR_MIN, Math.min(POLAR_MAX, this.polT + (e.key === "ArrowUp" ? -step : step)));
|
|
350
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
351
|
+
this.dollyBy(0.82);
|
|
352
|
+
} else if (e.key === "-" || e.key === "_") {
|
|
353
|
+
this.dollyBy(1.22);
|
|
354
|
+
} else {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
e.preventDefault();
|
|
324
358
|
this.fireGesture();
|
|
359
|
+
this.requestRender();
|
|
325
360
|
};
|
|
326
361
|
canvas.addEventListener("pointerdown", this.onPointerDown);
|
|
327
362
|
canvas.addEventListener("pointermove", this.onPointerMove);
|
|
328
363
|
canvas.addEventListener("pointerup", this.onPointerUp);
|
|
329
364
|
canvas.addEventListener("pointercancel", this.onPointerUp);
|
|
330
365
|
canvas.addEventListener("wheel", this.onWheel, { passive: false });
|
|
366
|
+
canvas.addEventListener("keydown", this.onKeyDown);
|
|
331
367
|
canvas.addEventListener("contextmenu", this.onContextMenu);
|
|
332
368
|
}
|
|
333
369
|
pinchCentroid() {
|
|
@@ -395,20 +431,32 @@ var OrbitCamera = class {
|
|
|
395
431
|
this.distT = Math.max(this.minDist, Math.min(this.maxDist, this.distT * factor));
|
|
396
432
|
this.requestRender();
|
|
397
433
|
}
|
|
434
|
+
/** Programmatic zoom used by the visible camera controls. Keeping it on the
|
|
435
|
+
* same dolly path as wheel, pinch and keyboard preserves all distance limits. */
|
|
436
|
+
zoomBy(factor) {
|
|
437
|
+
this.dollyBy(factor);
|
|
438
|
+
this.fireGesture();
|
|
439
|
+
}
|
|
440
|
+
/** Choose what a normal left/one-finger drag does. Shift temporarily performs
|
|
441
|
+
* the other action. This is intentionally public so the view can make the
|
|
442
|
+
* currently active gesture visible rather than hiding it in documentation. */
|
|
443
|
+
setPrimaryDragMode(mode) {
|
|
444
|
+
this.primaryDragMode = mode;
|
|
445
|
+
}
|
|
398
446
|
/**
|
|
399
447
|
* Fit a flattering 3/4 view to the bounds sphere. With `intro`, the camera
|
|
400
448
|
* STARTS nearly top-down (matching the 2D map's orientation) and further out,
|
|
401
449
|
* then the damped `update()` eases it up into the 3/4 architectural angle and
|
|
402
450
|
* dollies in — the venue "stands up" instead of teleporting (~600ms).
|
|
403
451
|
*/
|
|
404
|
-
frame(bounds, intro = false, stageAzimuth) {
|
|
452
|
+
frame(bounds, intro = false, stageAzimuth, portraitCrop = false) {
|
|
405
453
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
406
454
|
this.targetT.copy(this.target);
|
|
407
455
|
this.panAnchor.copy(this.target);
|
|
408
456
|
this.panLimit = Math.max(1, bounds.radius) * 1.5;
|
|
409
457
|
const r = Math.max(1, bounds.radius);
|
|
410
458
|
const halfV = this.fovY * DEG / 2;
|
|
411
|
-
const aspect = this.camera.aspect || 1;
|
|
459
|
+
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
412
460
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
413
461
|
const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
414
462
|
this.azT = stageAzimuth ?? -30 * DEG;
|
|
@@ -435,7 +483,7 @@ var OrbitCamera = class {
|
|
|
435
483
|
* seat, behind the shell, anywhere): re-pivot on the venue centre without
|
|
436
484
|
* moving the camera, then glide targets back to the 3/4 architectural pose.
|
|
437
485
|
*/
|
|
438
|
-
frameSoft(bounds, stageAzimuth) {
|
|
486
|
+
frameSoft(bounds, stageAzimuth, portraitCrop = false) {
|
|
439
487
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
440
488
|
this.targetT.copy(this.target);
|
|
441
489
|
this.panAnchor.copy(this.target);
|
|
@@ -444,7 +492,7 @@ var OrbitCamera = class {
|
|
|
444
492
|
this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
|
|
445
493
|
const r = Math.max(1, bounds.radius);
|
|
446
494
|
const halfV = this.fovY * DEG / 2;
|
|
447
|
-
const aspect = this.camera.aspect || 1;
|
|
495
|
+
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
448
496
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
449
497
|
const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
450
498
|
this.azT = stageAzimuth ?? this.azimuth;
|
|
@@ -517,6 +565,7 @@ var OrbitCamera = class {
|
|
|
517
565
|
this.canvas.removeEventListener("pointerup", this.onPointerUp);
|
|
518
566
|
this.canvas.removeEventListener("pointercancel", this.onPointerUp);
|
|
519
567
|
this.canvas.removeEventListener("wheel", this.onWheel);
|
|
568
|
+
this.canvas.removeEventListener("keydown", this.onKeyDown);
|
|
520
569
|
this.canvas.removeEventListener("contextmenu", this.onContextMenu);
|
|
521
570
|
this.activePointers.clear();
|
|
522
571
|
}
|
|
@@ -731,7 +780,10 @@ var SURROUNDINGS_SHAPE_ROLES = [
|
|
|
731
780
|
"sound",
|
|
732
781
|
"concession",
|
|
733
782
|
"coat",
|
|
734
|
-
"wall"
|
|
783
|
+
"wall",
|
|
784
|
+
"rail",
|
|
785
|
+
"suite",
|
|
786
|
+
"obstruction"
|
|
735
787
|
];
|
|
736
788
|
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
737
789
|
|
|
@@ -1455,6 +1507,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1455
1507
|
const iCategory = new Float32Array(count * 3);
|
|
1456
1508
|
const iMaxRadius = new Float32Array(count);
|
|
1457
1509
|
const iChairWidth = new Float32Array(count);
|
|
1510
|
+
const iPhysicalSeat = new Float32Array(count);
|
|
1458
1511
|
const iRing = new Float32Array(count * 3);
|
|
1459
1512
|
const idToIndex = /* @__PURE__ */ new Map();
|
|
1460
1513
|
const spacing = nearestNeighbourSpacing(seats);
|
|
@@ -1464,6 +1517,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1464
1517
|
const pitchM = (resolved ?? spacing[i]) * M;
|
|
1465
1518
|
iMaxRadius[i] = Number.isFinite(pitchM) ? Math.max(0.06, Math.min(SEAT_DOT_RADIUS_M, pitchM * SEAT_PITCH_FRACTION)) : SEAT_DOT_RADIUS_M;
|
|
1466
1519
|
iChairWidth[i] = chairHalfWidth(Number.isFinite(pitchM) ? pitchM : void 0);
|
|
1520
|
+
iPhysicalSeat[i] = seat.wheelchairSpaceType === "no-seat" ? 0 : 1;
|
|
1467
1521
|
iPosition[i * 3] = seat.x * M;
|
|
1468
1522
|
iPosition[i * 3 + 1] = surfaces ? surfaces.seatDeckY(i) : seatSurfaceY(seat);
|
|
1469
1523
|
iPosition[i * 3 + 2] = seat.y * M;
|
|
@@ -1491,6 +1545,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor)
|
|
|
1491
1545
|
iRing,
|
|
1492
1546
|
idToIndex,
|
|
1493
1547
|
iChairWidth,
|
|
1548
|
+
iPhysicalSeat,
|
|
1494
1549
|
iFloor: seatFloor ?? new Float32Array(count),
|
|
1495
1550
|
iYaw: new Float32Array(count)
|
|
1496
1551
|
};
|
|
@@ -2893,15 +2948,22 @@ function buildVenueSurfaces(units, seats) {
|
|
|
2893
2948
|
}
|
|
2894
2949
|
|
|
2895
2950
|
// src/view3d/scene/sceneModel.ts
|
|
2896
|
-
function
|
|
2951
|
+
function shapeVisibleInConfiguration(shape, activeConfigurationId) {
|
|
2952
|
+
const ids = shape.eventConfigurationIds;
|
|
2953
|
+
return !ids?.length || !!activeConfigurationId && ids.includes(activeConfigurationId);
|
|
2954
|
+
}
|
|
2955
|
+
function visibleObjects(objects, activeConfigurationId) {
|
|
2956
|
+
return objects.filter((object) => object.type !== "shape" || shapeVisibleInConfiguration(object, activeConfigurationId));
|
|
2957
|
+
}
|
|
2958
|
+
function floorUnits(doc, activeConfigurationId) {
|
|
2897
2959
|
if (doc.floors?.length) {
|
|
2898
2960
|
return doc.floors.map((f) => ({
|
|
2899
|
-
objects: f.objects,
|
|
2961
|
+
objects: visibleObjects(f.objects, activeConfigurationId),
|
|
2900
2962
|
focal: f.focalPoint ?? doc.focalPoint,
|
|
2901
2963
|
baseHeightM: f.baseHeightM ?? 0
|
|
2902
2964
|
}));
|
|
2903
2965
|
}
|
|
2904
|
-
return [{ objects: doc.objects, focal: doc.focalPoint, baseHeightM: 0 }];
|
|
2966
|
+
return [{ objects: visibleObjects(doc.objects, activeConfigurationId), focal: doc.focalPoint, baseHeightM: 0 }];
|
|
2905
2967
|
}
|
|
2906
2968
|
var AO = { top: 1, wallBottom: 0.5, bottomCap: 0.4 };
|
|
2907
2969
|
function tintTop(fill, neutral) {
|
|
@@ -3182,23 +3244,114 @@ function buildTable(builder, table, base, fill, S) {
|
|
|
3182
3244
|
AO
|
|
3183
3245
|
);
|
|
3184
3246
|
}
|
|
3247
|
+
function rotateShapePolygon(poly, degrees) {
|
|
3248
|
+
if (!degrees) return poly;
|
|
3249
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
3250
|
+
for (const point of poly) {
|
|
3251
|
+
minX = Math.min(minX, point.x);
|
|
3252
|
+
maxX = Math.max(maxX, point.x);
|
|
3253
|
+
minY = Math.min(minY, point.y);
|
|
3254
|
+
maxY = Math.max(maxY, point.y);
|
|
3255
|
+
}
|
|
3256
|
+
const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
|
|
3257
|
+
const radians = degrees * Math.PI / 180;
|
|
3258
|
+
const cos = Math.cos(radians), sin = Math.sin(radians);
|
|
3259
|
+
return poly.map((point) => {
|
|
3260
|
+
const dx = point.x - cx, dy = point.y - cy;
|
|
3261
|
+
return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3185
3264
|
function shapePolygon(shape) {
|
|
3186
|
-
|
|
3265
|
+
let poly = null;
|
|
3266
|
+
if (shape.kind === "polygon" && shape.points && shape.points.length >= 3) poly = shape.points;
|
|
3187
3267
|
if (shape.kind === "rect" && shape.width && shape.height) {
|
|
3188
|
-
|
|
3268
|
+
poly = rectPolygon(shape.x ?? 0, shape.y ?? 0, shape.width, shape.height);
|
|
3189
3269
|
}
|
|
3190
3270
|
if (shape.kind === "ellipse" && shape.width && shape.height) {
|
|
3191
3271
|
const cx = (shape.x ?? 0) + shape.width / 2;
|
|
3192
3272
|
const cy = (shape.y ?? 0) + shape.height / 2;
|
|
3193
|
-
|
|
3273
|
+
poly = ellipsePolygon(cx, cy, shape.width / 2, shape.height / 2);
|
|
3274
|
+
}
|
|
3275
|
+
return poly ? rotateShapePolygon(poly, shape.rotation) : null;
|
|
3276
|
+
}
|
|
3277
|
+
function architectureForShape(shape) {
|
|
3278
|
+
const role = shape.role ?? "";
|
|
3279
|
+
const defaults = {
|
|
3280
|
+
stage: { kind: "stage", heightM: 1 },
|
|
3281
|
+
entrance: { kind: "portal", heightM: 2.7 },
|
|
3282
|
+
exit: { kind: "portal", heightM: 2.7 },
|
|
3283
|
+
screen: { kind: "solid", heightM: 6 },
|
|
3284
|
+
wall: { kind: "solid", heightM: 2.8 },
|
|
3285
|
+
rail: { kind: "solid", heightM: 1.1 },
|
|
3286
|
+
suite: { kind: "suite", heightM: 3 },
|
|
3287
|
+
obstruction: { kind: "solid", heightM: 4.5 },
|
|
3288
|
+
sound: { kind: "solid", heightM: 1.45 },
|
|
3289
|
+
restroom: { kind: "solid", heightM: 2.4 },
|
|
3290
|
+
bar: { kind: "solid", heightM: 1.1 },
|
|
3291
|
+
concession: { kind: "solid", heightM: 1.1 },
|
|
3292
|
+
coat: { kind: "solid", heightM: 1.1 }
|
|
3293
|
+
};
|
|
3294
|
+
const resolved = defaults[role] ?? { kind: "plate", heightM: 0.25 };
|
|
3295
|
+
return { ...resolved, heightM: shape.heightM ?? resolved.heightM };
|
|
3296
|
+
}
|
|
3297
|
+
var SHAPE_LAYER_LIFT_M = 0.08;
|
|
3298
|
+
function claimShapeTopOffset(shape, claimed) {
|
|
3299
|
+
const poly = shapePolygon(shape);
|
|
3300
|
+
const architecture = architectureForShape(shape);
|
|
3301
|
+
const defaultTop = architecture.heightM;
|
|
3302
|
+
if (!poly) return defaultTop;
|
|
3303
|
+
if (architecture.kind !== "plate" && architecture.kind !== "stage") return defaultTop;
|
|
3304
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
3305
|
+
for (const p of poly) {
|
|
3306
|
+
minX = Math.min(minX, p.x);
|
|
3307
|
+
minY = Math.min(minY, p.y);
|
|
3308
|
+
maxX = Math.max(maxX, p.x);
|
|
3309
|
+
maxY = Math.max(maxY, p.y);
|
|
3310
|
+
}
|
|
3311
|
+
let topOffsetM = defaultTop;
|
|
3312
|
+
for (const prior of claimed) {
|
|
3313
|
+
const overlaps = maxX > prior.minX && minX < prior.maxX && maxY > prior.minY && minY < prior.maxY;
|
|
3314
|
+
if (overlaps && topOffsetM <= prior.topOffsetM + 1e-6) {
|
|
3315
|
+
topOffsetM = prior.topOffsetM + SHAPE_LAYER_LIFT_M;
|
|
3316
|
+
}
|
|
3194
3317
|
}
|
|
3195
|
-
|
|
3318
|
+
claimed.push({ minX, minY, maxX, maxY, topOffsetM });
|
|
3319
|
+
return topOffsetM;
|
|
3196
3320
|
}
|
|
3197
|
-
function
|
|
3321
|
+
function lerpPoint(a, b, t) {
|
|
3322
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
3323
|
+
}
|
|
3324
|
+
function orientQuadLong(poly) {
|
|
3325
|
+
if (poly.length !== 4) return poly;
|
|
3326
|
+
const edge0 = Math.hypot(poly[1].x - poly[0].x, poly[1].y - poly[0].y);
|
|
3327
|
+
const edge1 = Math.hypot(poly[2].x - poly[1].x, poly[2].y - poly[1].y);
|
|
3328
|
+
return edge0 >= edge1 ? poly : [poly[1], poly[2], poly[3], poly[0]];
|
|
3329
|
+
}
|
|
3330
|
+
function quadWidthSlice(poly, t0, t1) {
|
|
3331
|
+
return [
|
|
3332
|
+
lerpPoint(poly[0], poly[1], t0),
|
|
3333
|
+
lerpPoint(poly[0], poly[1], t1),
|
|
3334
|
+
lerpPoint(poly[3], poly[2], t1),
|
|
3335
|
+
lerpPoint(poly[3], poly[2], t0)
|
|
3336
|
+
];
|
|
3337
|
+
}
|
|
3338
|
+
function quadDepthSlice(poly, t0, t1) {
|
|
3339
|
+
return [
|
|
3340
|
+
lerpPoint(poly[0], poly[3], t0),
|
|
3341
|
+
lerpPoint(poly[1], poly[2], t0),
|
|
3342
|
+
lerpPoint(poly[1], poly[2], t1),
|
|
3343
|
+
lerpPoint(poly[0], poly[3], t1)
|
|
3344
|
+
];
|
|
3345
|
+
}
|
|
3346
|
+
function emitArchitecturalPrism(builder, poly, top, bottom, colTop, colWall) {
|
|
3347
|
+
extrudePrism(builder, poly, void 0, () => top, bottom, colTop, colWall, AO);
|
|
3348
|
+
}
|
|
3349
|
+
function buildShape(builder, shape, base, topOffsetM, S, focal, stages) {
|
|
3198
3350
|
const poly = shapePolygon(shape);
|
|
3199
3351
|
if (!poly) return;
|
|
3200
3352
|
const isStage = shape.role === "stage";
|
|
3201
|
-
const
|
|
3353
|
+
const architecture = architectureForShape(shape);
|
|
3354
|
+
const height = base + topOffsetM;
|
|
3202
3355
|
if (isStage && stages) {
|
|
3203
3356
|
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
3204
3357
|
for (const pt of poly) {
|
|
@@ -3216,8 +3369,29 @@ function buildShape(builder, shape, base, S, stages) {
|
|
|
3216
3369
|
});
|
|
3217
3370
|
}
|
|
3218
3371
|
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
3219
|
-
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
3220
|
-
|
|
3372
|
+
const colWall = isStage ? S.stageWall : shape.role === "screen" ? [0.24, 0.46, 0.82] : tintTop(hexToRgb(shape.fill), S.decorWall);
|
|
3373
|
+
if (architecture.kind === "portal" && poly.length === 4) {
|
|
3374
|
+
const quad = orientQuadLong(poly);
|
|
3375
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.16), height, base, colTop, colWall);
|
|
3376
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0.84, 1), height, base, colTop, colWall);
|
|
3377
|
+
emitArchitecturalPrism(builder, quad, height, Math.max(base, height - 0.35), colTop, colWall);
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
if (architecture.kind === "suite" && poly.length === 4) {
|
|
3381
|
+
const edgeDistances = poly.map((point, index) => {
|
|
3382
|
+
const next = poly[(index + 1) % 4];
|
|
3383
|
+
const mx = (point.x + next.x) / 2, my = (point.y + next.y) / 2;
|
|
3384
|
+
return Math.hypot(mx - focal.x, my - focal.y);
|
|
3385
|
+
});
|
|
3386
|
+
const front = edgeDistances.indexOf(Math.min(...edgeDistances));
|
|
3387
|
+
const quad = [0, 1, 2, 3].map((offset) => poly[(front + offset) % 4]);
|
|
3388
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.08), height, base, colTop, colWall);
|
|
3389
|
+
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0.92, 1), height, base, colTop, colWall);
|
|
3390
|
+
emitArchitecturalPrism(builder, quadDepthSlice(quad, 0.88, 1), height, base, colTop, colWall);
|
|
3391
|
+
emitArchitecturalPrism(builder, quad, height, Math.max(base, height - 0.18), colTop, colWall);
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
emitArchitecturalPrism(builder, poly, height, base, colTop, colWall);
|
|
3221
3395
|
}
|
|
3222
3396
|
function decorPolygon(decor) {
|
|
3223
3397
|
const { x, y, width, height } = decor;
|
|
@@ -3315,7 +3489,7 @@ function buildSceneModel(input) {
|
|
|
3315
3489
|
const { doc, seats } = input;
|
|
3316
3490
|
const theme = resolveTheme3D(doc.theme);
|
|
3317
3491
|
const S = theme.structure;
|
|
3318
|
-
const units = floorUnits(doc);
|
|
3492
|
+
const units = floorUnits(doc, input.eventConfigurationId ?? doc.eventConfigurationId);
|
|
3319
3493
|
const builder = new MeshBuilder();
|
|
3320
3494
|
const fp = chartFootprint(units, seats);
|
|
3321
3495
|
const padU = Math.max(60, (fp.maxX - fp.minX + fp.maxY - fp.minY) * 0.06);
|
|
@@ -3331,10 +3505,19 @@ function buildSceneModel(input) {
|
|
|
3331
3505
|
const unit = units[unitIndex];
|
|
3332
3506
|
builder.setFloor(unitIndex);
|
|
3333
3507
|
const claimed = new ClaimedArea();
|
|
3508
|
+
const claimedShapeLayers = [];
|
|
3334
3509
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
3335
3510
|
for (const o of unit.objects) {
|
|
3336
3511
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
3337
|
-
else if (o.type === "shape") buildShape(
|
|
3512
|
+
else if (o.type === "shape") buildShape(
|
|
3513
|
+
builder,
|
|
3514
|
+
o,
|
|
3515
|
+
unit.baseHeightM,
|
|
3516
|
+
claimShapeTopOffset(o, claimedShapeLayers),
|
|
3517
|
+
S,
|
|
3518
|
+
unit.focal,
|
|
3519
|
+
stageBounds
|
|
3520
|
+
);
|
|
3338
3521
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3339
3522
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3340
3523
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
@@ -3404,6 +3587,7 @@ function buildSceneModel(input) {
|
|
|
3404
3587
|
}
|
|
3405
3588
|
const labels = [];
|
|
3406
3589
|
const sections = [];
|
|
3590
|
+
const rows = [];
|
|
3407
3591
|
for (const z of zones) {
|
|
3408
3592
|
if (z.seatCount === 0) continue;
|
|
3409
3593
|
labels.push({
|
|
@@ -3500,6 +3684,49 @@ function buildSceneModel(input) {
|
|
|
3500
3684
|
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
|
|
3501
3685
|
});
|
|
3502
3686
|
}
|
|
3687
|
+
const rowAcc = /* @__PURE__ */ new Map();
|
|
3688
|
+
for (let i = 0; i < seats.length; i++) {
|
|
3689
|
+
const seat = seats[i];
|
|
3690
|
+
const cut = seat.label ? seat.label.lastIndexOf("-") : -1;
|
|
3691
|
+
let acc = rowAcc.get(seat.rowId);
|
|
3692
|
+
if (!acc) {
|
|
3693
|
+
acc = {
|
|
3694
|
+
n: 0,
|
|
3695
|
+
x: 0,
|
|
3696
|
+
y: 0,
|
|
3697
|
+
z: 0,
|
|
3698
|
+
minX: Infinity,
|
|
3699
|
+
maxX: -Infinity,
|
|
3700
|
+
minZ: Infinity,
|
|
3701
|
+
maxZ: -Infinity,
|
|
3702
|
+
label: cut > 0 ? seat.label.slice(0, cut) : seat.rowId,
|
|
3703
|
+
sectionId: seat.sectionId
|
|
3704
|
+
};
|
|
3705
|
+
rowAcc.set(seat.rowId, acc);
|
|
3706
|
+
}
|
|
3707
|
+
const offset = i * 3;
|
|
3708
|
+
const x = seatData.iPosition[offset];
|
|
3709
|
+
const y = seatData.iPosition[offset + 1];
|
|
3710
|
+
const z = seatData.iPosition[offset + 2];
|
|
3711
|
+
acc.n++;
|
|
3712
|
+
acc.x += x;
|
|
3713
|
+
acc.y += y;
|
|
3714
|
+
acc.z += z;
|
|
3715
|
+
acc.minX = Math.min(acc.minX, x);
|
|
3716
|
+
acc.maxX = Math.max(acc.maxX, x);
|
|
3717
|
+
acc.minZ = Math.min(acc.minZ, z);
|
|
3718
|
+
acc.maxZ = Math.max(acc.maxZ, z);
|
|
3719
|
+
}
|
|
3720
|
+
for (const [id, acc] of rowAcc) {
|
|
3721
|
+
rows.push({
|
|
3722
|
+
id,
|
|
3723
|
+
label: acc.label,
|
|
3724
|
+
sectionId: acc.sectionId,
|
|
3725
|
+
seatCount: acc.n,
|
|
3726
|
+
center: [acc.x / acc.n, acc.y / acc.n, acc.z / acc.n],
|
|
3727
|
+
radius: Math.max(0.8, Math.hypot(acc.maxX - acc.minX, acc.maxZ - acc.minZ) * 0.5)
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3503
3730
|
if (seats.length <= SEAT_LABEL_MAX) {
|
|
3504
3731
|
for (let i = 0; i < seats.length; i++) {
|
|
3505
3732
|
const s = seats[i];
|
|
@@ -3516,7 +3743,18 @@ function buildSceneModel(input) {
|
|
|
3516
3743
|
}
|
|
3517
3744
|
for (const unit of units) {
|
|
3518
3745
|
for (const o of unit.objects) {
|
|
3519
|
-
if (o.type === "
|
|
3746
|
+
if (o.type === "shape" && o.role && ["entrance", "exit", "screen", "suite", "obstruction"].includes(o.role)) {
|
|
3747
|
+
const poly = shapePolygon(o);
|
|
3748
|
+
const centre = poly ? centroidOf(poly) : null;
|
|
3749
|
+
if (!centre || !o.label) continue;
|
|
3750
|
+
labels.push({
|
|
3751
|
+
id: `architecture:${o.id}`,
|
|
3752
|
+
kind: "annotation",
|
|
3753
|
+
text: o.label,
|
|
3754
|
+
anchor: [centre.x * M, unit.baseHeightM + architectureForShape(o).heightM + ANNOTATION_LIFT_M, centre.y * M],
|
|
3755
|
+
color: o.fill
|
|
3756
|
+
});
|
|
3757
|
+
} else if (o.type === "text") {
|
|
3520
3758
|
if (!o.text) continue;
|
|
3521
3759
|
labels.push({
|
|
3522
3760
|
id: `text:${o.id}`,
|
|
@@ -3598,6 +3836,7 @@ function buildSceneModel(input) {
|
|
|
3598
3836
|
zones,
|
|
3599
3837
|
stages: stageBounds,
|
|
3600
3838
|
sections,
|
|
3839
|
+
rows,
|
|
3601
3840
|
labels,
|
|
3602
3841
|
floors
|
|
3603
3842
|
};
|
|
@@ -3631,6 +3870,7 @@ var LabelOverlay = class {
|
|
|
3631
3870
|
constructor(container, opts = {}) {
|
|
3632
3871
|
this.nodes = /* @__PURE__ */ new Map();
|
|
3633
3872
|
this.labels = [];
|
|
3873
|
+
this.forcedDense = false;
|
|
3634
3874
|
this.opts = opts;
|
|
3635
3875
|
this.root = document.createElement("div");
|
|
3636
3876
|
this.root.setAttribute("data-view3d-labels", "");
|
|
@@ -3665,6 +3905,11 @@ var LabelOverlay = class {
|
|
|
3665
3905
|
}
|
|
3666
3906
|
}
|
|
3667
3907
|
}
|
|
3908
|
+
/** Section/row drill-in makes their dense labels explicit, regardless of the
|
|
3909
|
+
* whole-venue distance heuristic used during free orbit. */
|
|
3910
|
+
setForcedDense(enabled) {
|
|
3911
|
+
this.forcedDense = enabled;
|
|
3912
|
+
}
|
|
3668
3913
|
/**
|
|
3669
3914
|
* Reposition every label for the current camera.
|
|
3670
3915
|
*
|
|
@@ -3673,6 +3918,10 @@ var LabelOverlay = class {
|
|
|
3673
3918
|
update(viewProjection, width, height, cameraDistance, venueRadius, cameraWorld) {
|
|
3674
3919
|
if (!this.labels.length) return;
|
|
3675
3920
|
const kinds = visibleLabelKinds(cameraDistance, venueRadius);
|
|
3921
|
+
if (this.forcedDense) {
|
|
3922
|
+
kinds.add("row");
|
|
3923
|
+
kinds.add("seat");
|
|
3924
|
+
}
|
|
3676
3925
|
const candidates = [];
|
|
3677
3926
|
for (const label of this.labels) {
|
|
3678
3927
|
if (!kinds.has(label.kind)) continue;
|
|
@@ -3821,6 +4070,7 @@ in vec2 position; // quad corner in [-1,1]
|
|
|
3821
4070
|
in vec3 iOffset; // per-instance world position
|
|
3822
4071
|
in vec3 iColor; // per-instance state colour (resolved CPU-side)
|
|
3823
4072
|
in float iMaxRadius; // per-instance world-radius ceiling (seat pitch derived)
|
|
4073
|
+
in float iPhysicalSeat;// 1 = chair; 0 = empty wheelchair bay
|
|
3824
4074
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3825
4075
|
in float iFloor; // owning floor index
|
|
3826
4076
|
uniform mat4 modelViewMatrix;
|
|
@@ -3838,6 +4088,7 @@ out float vBudget; // 1 = dot holds its minimum pixel size, <1 = it cannot
|
|
|
3838
4088
|
out vec3 vRing;
|
|
3839
4089
|
out float vDim;
|
|
3840
4090
|
out float vDotWeight; // 1 = the dot IS this seat, 0 = the chair has taken over
|
|
4091
|
+
out float vPhysicalSeat;
|
|
3841
4092
|
${CHAIR_WEIGHT_GLSL}
|
|
3842
4093
|
void main() {
|
|
3843
4094
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -3867,6 +4118,7 @@ void main() {
|
|
|
3867
4118
|
vUv = position;
|
|
3868
4119
|
vColor = iColor;
|
|
3869
4120
|
vRing = iRing;
|
|
4121
|
+
vPhysicalSeat = iPhysicalSeat;
|
|
3870
4122
|
vDim = (uFocusFloor < -0.5 || abs(iFloor - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
3871
4123
|
gl_Position = projectionMatrix * mv;
|
|
3872
4124
|
}`
|
|
@@ -3881,11 +4133,13 @@ in float vBudget;
|
|
|
3881
4133
|
in vec3 vRing;
|
|
3882
4134
|
in float vDim;
|
|
3883
4135
|
in float vDotWeight;
|
|
4136
|
+
in float vPhysicalSeat;
|
|
3884
4137
|
uniform float uSeatFade; // fade toward tier colour with distance (LOD)
|
|
3885
4138
|
uniform vec3 uFadeColor;
|
|
3886
4139
|
out vec4 fragColor;
|
|
3887
4140
|
void main() {
|
|
3888
|
-
|
|
4141
|
+
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
4142
|
+
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3889
4143
|
if (d > 1.0) discard;
|
|
3890
4144
|
float alpha = smoothstep(1.0, 0.72, d);
|
|
3891
4145
|
float shade = 0.80 + 0.28 * (0.5 - vUv.y * 0.5); // subtle top-lit
|
|
@@ -3927,6 +4181,7 @@ in float iYaw; // per-instance facing, radians (local +Z -> facing dir)
|
|
|
3927
4181
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3928
4182
|
in float iFloor;
|
|
3929
4183
|
in float iSeed; // <0 = seat is empty; else per-person hash in [0,1)
|
|
4184
|
+
in float iPhysicalSeat;// 1 = chair; 0 = empty wheelchair bay
|
|
3930
4185
|
uniform mat4 modelViewMatrix;
|
|
3931
4186
|
uniform mat4 projectionMatrix;
|
|
3932
4187
|
uniform float uChairFull;
|
|
@@ -3962,7 +4217,9 @@ void main() {
|
|
|
3962
4217
|
float occupant = step(2.5, part);
|
|
3963
4218
|
float taken = step(0.0, iSeed);
|
|
3964
4219
|
vOccupant = occupant;
|
|
3965
|
-
if (
|
|
4220
|
+
if (iPhysicalSeat < 0.5) {
|
|
4221
|
+
p = vec3(0.0); // sellable space, but no physical chair/person
|
|
4222
|
+
} else if (occupant > 0.5) {
|
|
3966
4223
|
if (taken < 0.5) {
|
|
3967
4224
|
p = vec3(0.0); // empty seat: no person
|
|
3968
4225
|
} else {
|
|
@@ -4105,6 +4362,7 @@ precision highp float;
|
|
|
4105
4362
|
in vec2 position;
|
|
4106
4363
|
in vec3 iOffset;
|
|
4107
4364
|
in float iMaxRadius;
|
|
4365
|
+
in float iPhysicalSeat;
|
|
4108
4366
|
uniform mat4 modelViewMatrix;
|
|
4109
4367
|
uniform mat4 projectionMatrix;
|
|
4110
4368
|
uniform float uSeatRadius;
|
|
@@ -4112,6 +4370,7 @@ uniform float uSeatScale;
|
|
|
4112
4370
|
uniform float uMinPixels;
|
|
4113
4371
|
uniform float uPixelToWorld;
|
|
4114
4372
|
out vec2 vUv;
|
|
4373
|
+
out float vPhysicalSeat;
|
|
4115
4374
|
flat out vec3 vPick;
|
|
4116
4375
|
void main() {
|
|
4117
4376
|
int id = gl_InstanceID + 1; // 0 reserved for no-hit
|
|
@@ -4124,6 +4383,7 @@ void main() {
|
|
|
4124
4383
|
// Must match SEAT_VERT exactly, or the hit mask drifts off the drawn dot.
|
|
4125
4384
|
mv.xy += normalize(vec3(modelViewMatrix * vec4(0.0, 1.0, 0.0, 0.0))).xy * r;
|
|
4126
4385
|
vUv = position;
|
|
4386
|
+
vPhysicalSeat = iPhysicalSeat;
|
|
4127
4387
|
gl_Position = projectionMatrix * mv;
|
|
4128
4388
|
}`
|
|
4129
4389
|
);
|
|
@@ -4132,10 +4392,12 @@ var SEAT_PICK_FRAG = (
|
|
|
4132
4392
|
`#version 300 es
|
|
4133
4393
|
precision highp float;
|
|
4134
4394
|
in vec2 vUv;
|
|
4395
|
+
in float vPhysicalSeat;
|
|
4135
4396
|
flat in vec3 vPick;
|
|
4136
4397
|
out vec4 fragColor;
|
|
4137
4398
|
void main() {
|
|
4138
|
-
|
|
4399
|
+
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4400
|
+
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
4139
4401
|
fragColor = vec4(vPick, 1.0);
|
|
4140
4402
|
}`
|
|
4141
4403
|
);
|
|
@@ -4297,6 +4559,7 @@ function buildGpuScene(gl, model) {
|
|
|
4297
4559
|
// Per-seat world-radius ceiling: what stops distant rows merging into one
|
|
4298
4560
|
// mass when the shader grows a dot to hold its minimum pixel size.
|
|
4299
4561
|
iMaxRadius: { size: 1, data: model.seats.iMaxRadius, instanced: 1 },
|
|
4562
|
+
iPhysicalSeat: { size: 1, data: model.seats.iPhysicalSeat, instanced: 1 },
|
|
4300
4563
|
// Accommodation ring colour; (0,0,0) means the seat carries no access type.
|
|
4301
4564
|
iRing: { size: 3, data: model.seats.iRing, instanced: 1 },
|
|
4302
4565
|
iFloor: { size: 1, data: model.seats.iFloor, instanced: 1 }
|
|
@@ -4314,6 +4577,7 @@ function buildGpuScene(gl, model) {
|
|
|
4314
4577
|
const cYaw = new Float32Array(CAP);
|
|
4315
4578
|
const cRing = new Float32Array(CAP * 3);
|
|
4316
4579
|
const cFloor = new Float32Array(CAP);
|
|
4580
|
+
const cPhysicalSeat = new Float32Array(CAP);
|
|
4317
4581
|
const cSeed = new Float32Array(CAP);
|
|
4318
4582
|
const chairGeo = new import_ogl4.Geometry(gl, {
|
|
4319
4583
|
position: { size: 3, data: chairBase.position },
|
|
@@ -4326,6 +4590,7 @@ function buildGpuScene(gl, model) {
|
|
|
4326
4590
|
iYaw: { size: 1, data: cYaw, instanced: 1 },
|
|
4327
4591
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
4328
4592
|
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4593
|
+
iPhysicalSeat: { size: 1, data: cPhysicalSeat, instanced: 1 },
|
|
4329
4594
|
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
4330
4595
|
});
|
|
4331
4596
|
const chairMesh = new import_ogl4.Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
@@ -4382,6 +4647,7 @@ function buildGpuScene(gl, model) {
|
|
|
4382
4647
|
cRing[k * 3 + 1] = src.iRing[i * 3 + 1];
|
|
4383
4648
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
4384
4649
|
cFloor[k] = src.iFloor[i];
|
|
4650
|
+
cPhysicalSeat[k] = src.iPhysicalSeat[i];
|
|
4385
4651
|
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
4386
4652
|
}
|
|
4387
4653
|
writeChairColors();
|
|
@@ -4390,6 +4656,7 @@ function buildGpuScene(gl, model) {
|
|
|
4390
4656
|
chairGeo.attributes.iYaw.needsUpdate = true;
|
|
4391
4657
|
chairGeo.attributes.iRing.needsUpdate = true;
|
|
4392
4658
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4659
|
+
chairGeo.attributes.iPhysicalSeat.needsUpdate = true;
|
|
4393
4660
|
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4394
4661
|
chairGeo.instancedCount = n;
|
|
4395
4662
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
@@ -4720,8 +4987,120 @@ var Cinematic = class {
|
|
|
4720
4987
|
}
|
|
4721
4988
|
};
|
|
4722
4989
|
|
|
4990
|
+
// src/view3d/camera/seatViewPose.ts
|
|
4991
|
+
var SEAT_EYE_ABOVE_DECK_M = 1.02;
|
|
4992
|
+
var FOCAL_LOOK_HEIGHT_M = 1.5;
|
|
4993
|
+
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0) {
|
|
4994
|
+
return {
|
|
4995
|
+
eye: [seatDeckWorld[0], seatDeckWorld[1] + SEAT_EYE_ABOVE_DECK_M, seatDeckWorld[2]],
|
|
4996
|
+
focal: [
|
|
4997
|
+
focalPoint.x * M,
|
|
4998
|
+
floorBaseHeightM + FOCAL_LOOK_HEIGHT_M,
|
|
4999
|
+
focalPoint.y * M
|
|
5000
|
+
]
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
// src/view/panoramaDelivery.ts
|
|
5005
|
+
function estimatePanoramaTextureBytes(width, height) {
|
|
5006
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return 0;
|
|
5007
|
+
return Math.ceil(width * height * 4 * (4 / 3));
|
|
5008
|
+
}
|
|
5009
|
+
function panoramaTextureBudgetBytes(deviceMemoryGb) {
|
|
5010
|
+
const mib = deviceMemoryGb !== void 0 && deviceMemoryGb <= 2 ? 32 : deviceMemoryGb !== void 0 && deviceMemoryGb >= 8 ? 192 : 96;
|
|
5011
|
+
return mib * 1024 * 1024;
|
|
5012
|
+
}
|
|
5013
|
+
function browserPanoramaConstraints(maxTextureSize) {
|
|
5014
|
+
const nav = typeof navigator === "undefined" ? void 0 : navigator;
|
|
5015
|
+
return {
|
|
5016
|
+
saveData: nav?.connection?.saveData === true,
|
|
5017
|
+
maxTextureSize,
|
|
5018
|
+
maxDecodedBytes: panoramaTextureBudgetBytes(nav?.deviceMemory)
|
|
5019
|
+
};
|
|
5020
|
+
}
|
|
5021
|
+
function planPanoramaDelivery(source, constraints = {}) {
|
|
5022
|
+
const previewUrl = source.previewUrl?.trim();
|
|
5023
|
+
if (!previewUrl || previewUrl === source.url) {
|
|
5024
|
+
return {
|
|
5025
|
+
initialUrl: source.url,
|
|
5026
|
+
initialWidth: source.sourceWidth,
|
|
5027
|
+
initialHeight: source.sourceHeight,
|
|
5028
|
+
reason: "full-only"
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
const initial = {
|
|
5032
|
+
initialUrl: previewUrl,
|
|
5033
|
+
initialWidth: source.previewWidth,
|
|
5034
|
+
initialHeight: source.previewHeight
|
|
5035
|
+
};
|
|
5036
|
+
if (constraints.saveData) return { ...initial, reason: "save-data" };
|
|
5037
|
+
if (constraints.maxTextureSize !== void 0 && source.sourceWidth !== void 0 && source.sourceHeight !== void 0 && Math.max(source.sourceWidth, source.sourceHeight) > constraints.maxTextureSize) {
|
|
5038
|
+
return { ...initial, reason: "texture-limit" };
|
|
5039
|
+
}
|
|
5040
|
+
if (constraints.maxDecodedBytes !== void 0 && source.sourceWidth !== void 0 && source.sourceHeight !== void 0 && estimatePanoramaTextureBytes(source.sourceWidth, source.sourceHeight) > constraints.maxDecodedBytes) {
|
|
5041
|
+
return { ...initial, reason: "memory-limit" };
|
|
5042
|
+
}
|
|
5043
|
+
return { ...initial, upgradeUrl: source.url, reason: "progressive" };
|
|
5044
|
+
}
|
|
5045
|
+
function abortError() {
|
|
5046
|
+
return new DOMException("Panorama image load aborted", "AbortError");
|
|
5047
|
+
}
|
|
5048
|
+
function loadPanoramaImage(url, signal) {
|
|
5049
|
+
return new Promise((resolve, reject) => {
|
|
5050
|
+
if (signal?.aborted) {
|
|
5051
|
+
reject(abortError());
|
|
5052
|
+
return;
|
|
5053
|
+
}
|
|
5054
|
+
const image = new Image();
|
|
5055
|
+
image.crossOrigin = "anonymous";
|
|
5056
|
+
let settled = false;
|
|
5057
|
+
const cleanup = () => {
|
|
5058
|
+
image.onload = null;
|
|
5059
|
+
image.onerror = null;
|
|
5060
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5061
|
+
};
|
|
5062
|
+
const finish = (error) => {
|
|
5063
|
+
if (settled) return;
|
|
5064
|
+
settled = true;
|
|
5065
|
+
cleanup();
|
|
5066
|
+
if (error) reject(error);
|
|
5067
|
+
else resolve(image);
|
|
5068
|
+
};
|
|
5069
|
+
const onAbort = () => {
|
|
5070
|
+
image.src = "";
|
|
5071
|
+
finish(abortError());
|
|
5072
|
+
};
|
|
5073
|
+
image.onload = () => {
|
|
5074
|
+
const decoded = typeof image.decode === "function" ? image.decode() : Promise.resolve();
|
|
5075
|
+
void decoded.then(() => finish(), () => finish());
|
|
5076
|
+
};
|
|
5077
|
+
image.onerror = () => finish(new Error("panorama_image_failed"));
|
|
5078
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5079
|
+
image.src = url;
|
|
5080
|
+
});
|
|
5081
|
+
}
|
|
5082
|
+
function schedulePanoramaUpgrade(work) {
|
|
5083
|
+
const host = globalThis;
|
|
5084
|
+
if (host.requestIdleCallback) {
|
|
5085
|
+
const id2 = host.requestIdleCallback(work, { timeout: 1200 });
|
|
5086
|
+
return () => host.cancelIdleCallback?.(id2);
|
|
5087
|
+
}
|
|
5088
|
+
const id = globalThis.setTimeout(work, 32);
|
|
5089
|
+
return () => globalThis.clearTimeout(id);
|
|
5090
|
+
}
|
|
5091
|
+
|
|
4723
5092
|
// src/view3d/crossfade/panorama.ts
|
|
5093
|
+
function seatViewDisclosure(view) {
|
|
5094
|
+
const coverage = view.generated ? "Live 3D \xB7 exact seat-eye" : view.coverage === "exact-seat" ? "Exact seat photo" : view.coverage === "row-representative" ? "Representative row view" : view.coverage === "section-representative" ? "Representative section view" : view.coverage === "venue-representative" ? "Representative venue view" : "Venue photo";
|
|
5095
|
+
const year = view.capturedAt && /^\d{4}/.test(view.capturedAt) ? view.capturedAt.slice(0, 4) : "";
|
|
5096
|
+
return [coverage, year ? `captured ${year}` : "", view.sourceLabel ?? ""].filter(Boolean).join(" \xB7 ");
|
|
5097
|
+
}
|
|
4724
5098
|
var VFOV_DEG = 70;
|
|
5099
|
+
var MIN_VFOV_DEG = 35;
|
|
5100
|
+
var MAX_VFOV_DEG = 90;
|
|
5101
|
+
function clampPanoramaFov(fovDeg) {
|
|
5102
|
+
return Math.max(MIN_VFOV_DEG, Math.min(MAX_VFOV_DEG, fovDeg));
|
|
5103
|
+
}
|
|
4725
5104
|
var MAX_PITCH_DEG = 35;
|
|
4726
5105
|
function bearingToOffsetPx(bearingDeg, viewportW, bgW) {
|
|
4727
5106
|
const col = (0.5 + bearingDeg / 360) * bgW;
|
|
@@ -4740,9 +5119,16 @@ function clampPitchPx(pitchPx, bgH) {
|
|
|
4740
5119
|
function mountPanorama(container, view, opts = {}) {
|
|
4741
5120
|
const fadeMs = opts.fadeMs ?? 400;
|
|
4742
5121
|
const bearing = view.initialBearingDeg ?? 0;
|
|
5122
|
+
const delivery = planPanoramaDelivery(view, browserPanoramaConstraints());
|
|
5123
|
+
const loadAbort = new AbortController();
|
|
5124
|
+
const abortFromCaller = () => loadAbort.abort();
|
|
5125
|
+
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
4743
5126
|
const root = document.createElement("div");
|
|
5127
|
+
const priorFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
4744
5128
|
root.setAttribute("role", "dialog");
|
|
5129
|
+
root.setAttribute("aria-modal", "true");
|
|
4745
5130
|
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
5131
|
+
root.tabIndex = -1;
|
|
4746
5132
|
Object.assign(root.style, {
|
|
4747
5133
|
position: "absolute",
|
|
4748
5134
|
inset: "0",
|
|
@@ -4757,7 +5143,7 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4757
5143
|
Object.assign(pano.style, {
|
|
4758
5144
|
position: "absolute",
|
|
4759
5145
|
inset: "0",
|
|
4760
|
-
backgroundImage: `url("${
|
|
5146
|
+
backgroundImage: `url("${delivery.initialUrl}")`,
|
|
4761
5147
|
backgroundRepeat: "repeat-x",
|
|
4762
5148
|
cursor: "grab"
|
|
4763
5149
|
});
|
|
@@ -4771,8 +5157,8 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4771
5157
|
top: "12px",
|
|
4772
5158
|
right: "12px",
|
|
4773
5159
|
zIndex: "2",
|
|
4774
|
-
width: "
|
|
4775
|
-
height: "
|
|
5160
|
+
width: "44px",
|
|
5161
|
+
height: "44px",
|
|
4776
5162
|
borderRadius: "999px",
|
|
4777
5163
|
cursor: "pointer",
|
|
4778
5164
|
border: "1px solid rgba(255,255,255,0.25)",
|
|
@@ -4782,8 +5168,30 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4782
5168
|
lineHeight: "1"
|
|
4783
5169
|
});
|
|
4784
5170
|
root.appendChild(closeBtn);
|
|
5171
|
+
if (opts.disclosure) {
|
|
5172
|
+
const disclosure = document.createElement("div");
|
|
5173
|
+
disclosure.textContent = `360\xB0 panorama \xB7 ${opts.disclosure}`;
|
|
5174
|
+
Object.assign(disclosure.style, {
|
|
5175
|
+
position: "absolute",
|
|
5176
|
+
top: "12px",
|
|
5177
|
+
left: "12px",
|
|
5178
|
+
zIndex: "2",
|
|
5179
|
+
maxWidth: "calc(100% - 88px)",
|
|
5180
|
+
padding: "8px 11px",
|
|
5181
|
+
borderRadius: "999px",
|
|
5182
|
+
overflow: "hidden",
|
|
5183
|
+
color: "#dce6f8",
|
|
5184
|
+
background: "rgba(8,12,18,0.68)",
|
|
5185
|
+
font: "600 11px/1.2 ui-sans-serif, system-ui, sans-serif",
|
|
5186
|
+
textOverflow: "ellipsis",
|
|
5187
|
+
whiteSpace: "nowrap",
|
|
5188
|
+
pointerEvents: "none",
|
|
5189
|
+
backdropFilter: "blur(6px)"
|
|
5190
|
+
});
|
|
5191
|
+
root.appendChild(disclosure);
|
|
5192
|
+
}
|
|
4785
5193
|
const hint = document.createElement("div");
|
|
4786
|
-
hint.textContent = "Drag to look
|
|
5194
|
+
hint.textContent = "Drag to look \xB7 pinch or scroll to zoom \xB7 Esc to close";
|
|
4787
5195
|
Object.assign(hint.style, {
|
|
4788
5196
|
position: "absolute",
|
|
4789
5197
|
bottom: "12px",
|
|
@@ -4796,23 +5204,28 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4796
5204
|
});
|
|
4797
5205
|
root.appendChild(hint);
|
|
4798
5206
|
container.appendChild(root);
|
|
5207
|
+
closeBtn.focus({ preventScroll: true });
|
|
4799
5208
|
let bgW = 0;
|
|
4800
5209
|
let bgH = 0;
|
|
4801
5210
|
let posX = 0;
|
|
4802
5211
|
let pitchPx = 0;
|
|
5212
|
+
let viewFov = VFOV_DEG;
|
|
4803
5213
|
const layout = () => {
|
|
4804
5214
|
const vh = root.clientHeight || 1;
|
|
4805
5215
|
const vw = root.clientWidth || 1;
|
|
4806
5216
|
const natW = img.naturalWidth || vw * 2;
|
|
4807
5217
|
const natH = img.naturalHeight || vh;
|
|
4808
|
-
|
|
5218
|
+
const centredImageRatio = bgW > 0 ? (vw / 2 - posX) / bgW : 0;
|
|
5219
|
+
const pitchRatio = bgH > 0 ? pitchPx / bgH : 0;
|
|
5220
|
+
bgH = windowedBgHeight(vh, viewFov);
|
|
4809
5221
|
bgW = bgH * (natW / natH);
|
|
4810
5222
|
pano.style.backgroundSize = `${bgW}px ${bgH}px`;
|
|
4811
5223
|
if (!posInitialised) {
|
|
4812
5224
|
posX = bearingToOffsetPx(bearing, vw, bgW);
|
|
5225
|
+
pitchPx = clampPitchPx((view.initialPitchDeg ?? 0) / 180 * bgH, bgH);
|
|
4813
5226
|
posInitialised = true;
|
|
4814
|
-
}
|
|
4815
|
-
pitchPx = clampPitchPx(
|
|
5227
|
+
} else posX = vw / 2 - centredImageRatio * bgW;
|
|
5228
|
+
pitchPx = clampPitchPx(pitchRatio * bgH, bgH);
|
|
4816
5229
|
pano.style.backgroundPosition = `${posX}px ${horizonOffsetPy(vh, bgH, pitchPx)}px`;
|
|
4817
5230
|
};
|
|
4818
5231
|
let posInitialised = false;
|
|
@@ -4821,17 +5234,44 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4821
5234
|
pitchPx = clampPitchPx(pitchPx, bgH);
|
|
4822
5235
|
pano.style.backgroundPosition = `${posX}px ${horizonOffsetPy(vh, bgH, pitchPx)}px`;
|
|
4823
5236
|
};
|
|
4824
|
-
|
|
5237
|
+
let img = new Image();
|
|
4825
5238
|
img.onload = layout;
|
|
4826
|
-
img.src =
|
|
5239
|
+
img.src = delivery.initialUrl;
|
|
5240
|
+
let cancelUpgrade = () => {
|
|
5241
|
+
};
|
|
5242
|
+
if (delivery.upgradeUrl) {
|
|
5243
|
+
cancelUpgrade = schedulePanoramaUpgrade(() => {
|
|
5244
|
+
void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then((full) => {
|
|
5245
|
+
if (disposed || loadAbort.signal.aborted) return;
|
|
5246
|
+
img = full;
|
|
5247
|
+
pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
|
|
5248
|
+
layout();
|
|
5249
|
+
}).catch(() => {
|
|
5250
|
+
});
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
4827
5253
|
requestAnimationFrame(layout);
|
|
4828
5254
|
let dragging = false;
|
|
4829
5255
|
let lastX = 0;
|
|
4830
5256
|
let lastY = 0;
|
|
5257
|
+
let pinchDistance = 0;
|
|
5258
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
5259
|
+
const distanceBetweenPointers = () => {
|
|
5260
|
+
const [a, b] = [...pointers.values()];
|
|
5261
|
+
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
5262
|
+
};
|
|
5263
|
+
const setViewFov = (nextFov) => {
|
|
5264
|
+
const clamped = clampPanoramaFov(nextFov);
|
|
5265
|
+
if (clamped === viewFov) return;
|
|
5266
|
+
viewFov = clamped;
|
|
5267
|
+
layout();
|
|
5268
|
+
};
|
|
4831
5269
|
const onDown = (e) => {
|
|
4832
|
-
|
|
5270
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5271
|
+
dragging = pointers.size === 1;
|
|
4833
5272
|
lastX = e.clientX;
|
|
4834
5273
|
lastY = e.clientY;
|
|
5274
|
+
if (pointers.size === 2) pinchDistance = distanceBetweenPointers();
|
|
4835
5275
|
pano.style.cursor = "grabbing";
|
|
4836
5276
|
try {
|
|
4837
5277
|
pano.setPointerCapture?.(e.pointerId);
|
|
@@ -4839,6 +5279,15 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4839
5279
|
}
|
|
4840
5280
|
};
|
|
4841
5281
|
const onMove = (e) => {
|
|
5282
|
+
if (!pointers.has(e.pointerId)) return;
|
|
5283
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5284
|
+
if (pointers.size >= 2) {
|
|
5285
|
+
const nextDistance = distanceBetweenPointers();
|
|
5286
|
+
if (pinchDistance > 0 && nextDistance > 0) setViewFov(viewFov * (pinchDistance / nextDistance));
|
|
5287
|
+
pinchDistance = nextDistance;
|
|
5288
|
+
dragging = false;
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
4842
5291
|
if (!dragging) return;
|
|
4843
5292
|
posX += e.clientX - lastX;
|
|
4844
5293
|
pitchPx += e.clientY - lastY;
|
|
@@ -4847,17 +5296,28 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4847
5296
|
applyPos();
|
|
4848
5297
|
};
|
|
4849
5298
|
const onUp = (e) => {
|
|
4850
|
-
|
|
4851
|
-
|
|
5299
|
+
pointers.delete(e.pointerId);
|
|
5300
|
+
pinchDistance = pointers.size === 2 ? distanceBetweenPointers() : 0;
|
|
5301
|
+
const remaining = pointers.values().next().value;
|
|
5302
|
+
dragging = pointers.size === 1;
|
|
5303
|
+
if (remaining) {
|
|
5304
|
+
lastX = remaining.x;
|
|
5305
|
+
lastY = remaining.y;
|
|
5306
|
+
} else pano.style.cursor = "grab";
|
|
4852
5307
|
try {
|
|
4853
5308
|
pano.releasePointerCapture?.(e.pointerId);
|
|
4854
5309
|
} catch {
|
|
4855
5310
|
}
|
|
4856
5311
|
};
|
|
5312
|
+
const onWheel = (e) => {
|
|
5313
|
+
e.preventDefault();
|
|
5314
|
+
setViewFov(viewFov + e.deltaY * 0.04);
|
|
5315
|
+
};
|
|
4857
5316
|
pano.addEventListener("pointerdown", onDown);
|
|
4858
5317
|
pano.addEventListener("pointermove", onMove);
|
|
4859
5318
|
pano.addEventListener("pointerup", onUp);
|
|
4860
5319
|
pano.addEventListener("pointercancel", onUp);
|
|
5320
|
+
pano.addEventListener("wheel", onWheel, { passive: false });
|
|
4861
5321
|
let closed = false;
|
|
4862
5322
|
let disposed = false;
|
|
4863
5323
|
let fadeTimer = 0;
|
|
@@ -4866,15 +5326,20 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4866
5326
|
pano.removeEventListener("pointermove", onMove);
|
|
4867
5327
|
pano.removeEventListener("pointerup", onUp);
|
|
4868
5328
|
pano.removeEventListener("pointercancel", onUp);
|
|
5329
|
+
pano.removeEventListener("wheel", onWheel);
|
|
4869
5330
|
window.removeEventListener("keydown", onKey);
|
|
4870
5331
|
};
|
|
4871
5332
|
const teardown = () => {
|
|
5333
|
+
cancelUpgrade();
|
|
5334
|
+
loadAbort.abort();
|
|
5335
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
4872
5336
|
if (fadeTimer) {
|
|
4873
5337
|
window.clearTimeout(fadeTimer);
|
|
4874
5338
|
fadeTimer = 0;
|
|
4875
5339
|
}
|
|
4876
5340
|
removeListeners();
|
|
4877
5341
|
if (root.parentNode) root.parentNode.removeChild(root);
|
|
5342
|
+
if (priorFocus?.isConnected) priorFocus.focus({ preventScroll: true });
|
|
4878
5343
|
};
|
|
4879
5344
|
const close = () => {
|
|
4880
5345
|
if (closed) return;
|
|
@@ -4889,10 +5354,33 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4889
5354
|
fadeTimer = window.setTimeout(done, fadeMs);
|
|
4890
5355
|
};
|
|
4891
5356
|
const onKey = (e) => {
|
|
5357
|
+
if (e.key === "Tab") {
|
|
5358
|
+
e.preventDefault();
|
|
5359
|
+
closeBtn.focus({ preventScroll: true });
|
|
5360
|
+
return;
|
|
5361
|
+
}
|
|
4892
5362
|
if (e.key === "Escape") {
|
|
4893
5363
|
e.stopPropagation();
|
|
4894
5364
|
close();
|
|
5365
|
+
return;
|
|
4895
5366
|
}
|
|
5367
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
5368
|
+
posX += e.key === "ArrowLeft" ? -32 : 32;
|
|
5369
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
5370
|
+
pitchPx += (e.key === "ArrowUp" ? 4 : -4) * (bgH / 180);
|
|
5371
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
5372
|
+
setViewFov(viewFov - 5);
|
|
5373
|
+
e.preventDefault();
|
|
5374
|
+
return;
|
|
5375
|
+
} else if (e.key === "-") {
|
|
5376
|
+
setViewFov(viewFov + 5);
|
|
5377
|
+
e.preventDefault();
|
|
5378
|
+
return;
|
|
5379
|
+
} else {
|
|
5380
|
+
return;
|
|
5381
|
+
}
|
|
5382
|
+
e.preventDefault();
|
|
5383
|
+
applyPos();
|
|
4896
5384
|
};
|
|
4897
5385
|
window.addEventListener("keydown", onKey);
|
|
4898
5386
|
closeBtn.addEventListener("click", close);
|
|
@@ -5030,6 +5518,10 @@ function createPanoSphere(gl, image) {
|
|
|
5030
5518
|
setOpacity(value) {
|
|
5031
5519
|
program.uniforms.uOpacity.value = Math.max(0, Math.min(1, value));
|
|
5032
5520
|
},
|
|
5521
|
+
setImage(nextImage) {
|
|
5522
|
+
texture.image = nextImage;
|
|
5523
|
+
texture.needsUpdate = true;
|
|
5524
|
+
},
|
|
5033
5525
|
dispose() {
|
|
5034
5526
|
mesh.setParent(null);
|
|
5035
5527
|
geometry.remove();
|
|
@@ -5041,20 +5533,29 @@ function createPanoSphere(gl, image) {
|
|
|
5041
5533
|
|
|
5042
5534
|
// src/view3d/crossfade/panoramaSphere.ts
|
|
5043
5535
|
var DEG_PER_PX = 0.15;
|
|
5044
|
-
function loadImage(url) {
|
|
5045
|
-
return new Promise((resolve, reject) => {
|
|
5046
|
-
const img = new Image();
|
|
5047
|
-
img.crossOrigin = "anonymous";
|
|
5048
|
-
img.onload = () => resolve(img);
|
|
5049
|
-
img.onerror = () => reject(new Error("panorama_image_failed"));
|
|
5050
|
-
img.src = url;
|
|
5051
|
-
});
|
|
5052
|
-
}
|
|
5053
5536
|
async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
5054
5537
|
const fadeMs = opts.fadeMs ?? 400;
|
|
5055
5538
|
let bearing = view?.initialBearingDeg ?? 0;
|
|
5056
|
-
let pitch = 0;
|
|
5539
|
+
let pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, view?.initialPitchDeg ?? 0));
|
|
5540
|
+
let viewFov = VFOV_DEG;
|
|
5057
5541
|
let disposed = false;
|
|
5542
|
+
const loadAbort = new AbortController();
|
|
5543
|
+
const abortFromCaller = () => loadAbort.abort();
|
|
5544
|
+
opts.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
5545
|
+
let cancelUpgrade = () => {
|
|
5546
|
+
};
|
|
5547
|
+
const priorPosition = [
|
|
5548
|
+
deps.camera.position.x,
|
|
5549
|
+
deps.camera.position.y,
|
|
5550
|
+
deps.camera.position.z
|
|
5551
|
+
];
|
|
5552
|
+
if (!view && deps.cameraOriginWorld) {
|
|
5553
|
+
deps.camera.position.set(
|
|
5554
|
+
deps.cameraOriginWorld[0],
|
|
5555
|
+
deps.cameraOriginWorld[1],
|
|
5556
|
+
deps.cameraOriginWorld[2]
|
|
5557
|
+
);
|
|
5558
|
+
}
|
|
5058
5559
|
if (!view && deps.focalWorld) {
|
|
5059
5560
|
const p = deps.camera.position;
|
|
5060
5561
|
const dx = deps.focalWorld[0] - p.x;
|
|
@@ -5068,22 +5569,37 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5068
5569
|
}
|
|
5069
5570
|
let sphere = null;
|
|
5070
5571
|
if (view) {
|
|
5572
|
+
const maxTextureSize = deps.gl.getParameter(deps.gl.MAX_TEXTURE_SIZE);
|
|
5573
|
+
const delivery = planPanoramaDelivery(view, browserPanoramaConstraints(maxTextureSize));
|
|
5071
5574
|
let image;
|
|
5072
5575
|
try {
|
|
5073
|
-
image = await
|
|
5576
|
+
image = await loadPanoramaImage(delivery.initialUrl, loadAbort.signal);
|
|
5074
5577
|
} catch {
|
|
5578
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
5075
5579
|
return null;
|
|
5076
5580
|
}
|
|
5077
5581
|
try {
|
|
5078
5582
|
sphere = createPanoSphere(deps.gl, image);
|
|
5079
5583
|
} catch {
|
|
5584
|
+
loadAbort.abort();
|
|
5585
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
5080
5586
|
return null;
|
|
5081
5587
|
}
|
|
5082
5588
|
sphere.mesh.renderOrder = 999;
|
|
5083
5589
|
sphere.mesh.setParent(deps.scene);
|
|
5590
|
+
if (delivery.upgradeUrl) {
|
|
5591
|
+
cancelUpgrade = schedulePanoramaUpgrade(() => {
|
|
5592
|
+
void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then((full) => {
|
|
5593
|
+
if (disposed || loadAbort.signal.aborted) return;
|
|
5594
|
+
sphere?.setImage(full);
|
|
5595
|
+
deps.requestRender();
|
|
5596
|
+
}).catch(() => {
|
|
5597
|
+
});
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5084
5600
|
}
|
|
5085
5601
|
const priorFov = deps.camera.fov;
|
|
5086
|
-
deps.camera.perspective({ fov:
|
|
5602
|
+
deps.camera.perspective({ fov: viewFov, aspect: deps.camera.aspect });
|
|
5087
5603
|
const priorQuat = deps.camera.quaternion.slice();
|
|
5088
5604
|
const aim = () => {
|
|
5089
5605
|
const p = deps.camera.position;
|
|
@@ -5094,8 +5610,11 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5094
5610
|
};
|
|
5095
5611
|
aim();
|
|
5096
5612
|
const root = document.createElement("div");
|
|
5613
|
+
const priorFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
5097
5614
|
root.setAttribute("role", "dialog");
|
|
5615
|
+
root.setAttribute("aria-modal", "true");
|
|
5098
5616
|
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
5617
|
+
root.tabIndex = -1;
|
|
5099
5618
|
Object.assign(root.style, {
|
|
5100
5619
|
position: "absolute",
|
|
5101
5620
|
inset: "0",
|
|
@@ -5112,8 +5631,8 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5112
5631
|
top: "12px",
|
|
5113
5632
|
right: "12px",
|
|
5114
5633
|
zIndex: "2",
|
|
5115
|
-
width: "
|
|
5116
|
-
height: "
|
|
5634
|
+
width: "44px",
|
|
5635
|
+
height: "44px",
|
|
5117
5636
|
borderRadius: "999px",
|
|
5118
5637
|
cursor: "pointer",
|
|
5119
5638
|
border: "1px solid rgba(255,255,255,0.25)",
|
|
@@ -5123,8 +5642,30 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5123
5642
|
lineHeight: "1"
|
|
5124
5643
|
});
|
|
5125
5644
|
root.appendChild(closeBtn);
|
|
5645
|
+
if (opts.disclosure) {
|
|
5646
|
+
const disclosure = document.createElement("div");
|
|
5647
|
+
disclosure.textContent = `360\xB0 panorama \xB7 ${opts.disclosure}`;
|
|
5648
|
+
Object.assign(disclosure.style, {
|
|
5649
|
+
position: "absolute",
|
|
5650
|
+
top: "12px",
|
|
5651
|
+
left: "12px",
|
|
5652
|
+
zIndex: "2",
|
|
5653
|
+
maxWidth: "calc(100% - 88px)",
|
|
5654
|
+
padding: "8px 11px",
|
|
5655
|
+
borderRadius: "999px",
|
|
5656
|
+
overflow: "hidden",
|
|
5657
|
+
color: "#dce6f8",
|
|
5658
|
+
background: "rgba(8,12,18,0.68)",
|
|
5659
|
+
font: "600 11px/1.2 ui-sans-serif, system-ui, sans-serif",
|
|
5660
|
+
textOverflow: "ellipsis",
|
|
5661
|
+
whiteSpace: "nowrap",
|
|
5662
|
+
pointerEvents: "none",
|
|
5663
|
+
backdropFilter: "blur(6px)"
|
|
5664
|
+
});
|
|
5665
|
+
root.appendChild(disclosure);
|
|
5666
|
+
}
|
|
5126
5667
|
const hint = document.createElement("div");
|
|
5127
|
-
hint.textContent = "Drag to look
|
|
5668
|
+
hint.textContent = "Drag to look \xB7 pinch or scroll to zoom \xB7 Esc to close";
|
|
5128
5669
|
Object.assign(hint.style, {
|
|
5129
5670
|
position: "absolute",
|
|
5130
5671
|
bottom: "12px",
|
|
@@ -5137,18 +5678,43 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5137
5678
|
});
|
|
5138
5679
|
root.appendChild(hint);
|
|
5139
5680
|
container.appendChild(root);
|
|
5681
|
+
closeBtn.focus({ preventScroll: true });
|
|
5140
5682
|
let dragging = false;
|
|
5141
5683
|
let lastX = 0;
|
|
5142
5684
|
let lastY = 0;
|
|
5685
|
+
let pinchDistance = 0;
|
|
5686
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
5687
|
+
const distanceBetweenPointers = () => {
|
|
5688
|
+
const [a, b] = [...pointers.values()];
|
|
5689
|
+
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
5690
|
+
};
|
|
5691
|
+
const setViewFov = (nextFov) => {
|
|
5692
|
+
const clamped = clampPanoramaFov(nextFov);
|
|
5693
|
+
if (clamped === viewFov) return;
|
|
5694
|
+
viewFov = clamped;
|
|
5695
|
+
deps.camera.perspective({ fov: viewFov, aspect: deps.camera.aspect });
|
|
5696
|
+
aim();
|
|
5697
|
+
};
|
|
5143
5698
|
const onDown = (e) => {
|
|
5144
5699
|
if (e.target === closeBtn) return;
|
|
5145
|
-
|
|
5700
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5701
|
+
dragging = pointers.size === 1;
|
|
5146
5702
|
lastX = e.clientX;
|
|
5147
5703
|
lastY = e.clientY;
|
|
5704
|
+
if (pointers.size === 2) pinchDistance = distanceBetweenPointers();
|
|
5148
5705
|
root.setPointerCapture?.(e.pointerId);
|
|
5149
5706
|
root.style.cursor = "grabbing";
|
|
5150
5707
|
};
|
|
5151
5708
|
const onMove = (e) => {
|
|
5709
|
+
if (!pointers.has(e.pointerId)) return;
|
|
5710
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
5711
|
+
if (pointers.size >= 2) {
|
|
5712
|
+
const nextDistance = distanceBetweenPointers();
|
|
5713
|
+
if (pinchDistance > 0 && nextDistance > 0) setViewFov(viewFov * (pinchDistance / nextDistance));
|
|
5714
|
+
pinchDistance = nextDistance;
|
|
5715
|
+
dragging = false;
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5152
5718
|
if (!dragging) return;
|
|
5153
5719
|
bearing += (e.clientX - lastX) * DEG_PER_PX;
|
|
5154
5720
|
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.clientY - lastY) * DEG_PER_PX));
|
|
@@ -5156,18 +5722,58 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5156
5722
|
lastY = e.clientY;
|
|
5157
5723
|
aim();
|
|
5158
5724
|
};
|
|
5159
|
-
const onUp = () => {
|
|
5160
|
-
|
|
5161
|
-
|
|
5725
|
+
const onUp = (e) => {
|
|
5726
|
+
pointers.delete(e.pointerId);
|
|
5727
|
+
pinchDistance = pointers.size === 2 ? distanceBetweenPointers() : 0;
|
|
5728
|
+
const remaining = pointers.values().next().value;
|
|
5729
|
+
dragging = pointers.size === 1;
|
|
5730
|
+
if (remaining) {
|
|
5731
|
+
lastX = remaining.x;
|
|
5732
|
+
lastY = remaining.y;
|
|
5733
|
+
} else root.style.cursor = "grab";
|
|
5734
|
+
try {
|
|
5735
|
+
root.releasePointerCapture?.(e.pointerId);
|
|
5736
|
+
} catch {
|
|
5737
|
+
}
|
|
5738
|
+
};
|
|
5739
|
+
const onWheel = (e) => {
|
|
5740
|
+
e.preventDefault();
|
|
5741
|
+
setViewFov(viewFov + e.deltaY * 0.04);
|
|
5162
5742
|
};
|
|
5163
5743
|
root.addEventListener("pointerdown", onDown);
|
|
5164
5744
|
root.addEventListener("pointermove", onMove);
|
|
5165
5745
|
root.addEventListener("pointerup", onUp);
|
|
5166
5746
|
root.addEventListener("pointercancel", onUp);
|
|
5747
|
+
root.addEventListener("wheel", onWheel, { passive: false });
|
|
5167
5748
|
const onKey = (e) => {
|
|
5168
|
-
if (
|
|
5169
|
-
e.
|
|
5170
|
-
|
|
5749
|
+
if (disposed) return;
|
|
5750
|
+
if (e.key === "Tab") {
|
|
5751
|
+
e.preventDefault();
|
|
5752
|
+
closeBtn.focus({ preventScroll: true });
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
if (e.key === "Escape") {
|
|
5756
|
+
e.stopPropagation();
|
|
5757
|
+
handle.close();
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5760
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
5761
|
+
bearing += e.key === "ArrowLeft" ? -4 : 4;
|
|
5762
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
5763
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.key === "ArrowUp" ? 4 : -4)));
|
|
5764
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
5765
|
+
setViewFov(viewFov - 5);
|
|
5766
|
+
e.preventDefault();
|
|
5767
|
+
return;
|
|
5768
|
+
} else if (e.key === "-") {
|
|
5769
|
+
setViewFov(viewFov + 5);
|
|
5770
|
+
e.preventDefault();
|
|
5771
|
+
return;
|
|
5772
|
+
} else {
|
|
5773
|
+
return;
|
|
5774
|
+
}
|
|
5775
|
+
e.preventDefault();
|
|
5776
|
+
aim();
|
|
5171
5777
|
};
|
|
5172
5778
|
window.addEventListener("keydown", onKey);
|
|
5173
5779
|
let raf = 0;
|
|
@@ -5192,14 +5798,20 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5192
5798
|
const teardown = () => {
|
|
5193
5799
|
if (disposed) return;
|
|
5194
5800
|
disposed = true;
|
|
5801
|
+
cancelUpgrade();
|
|
5802
|
+
loadAbort.abort();
|
|
5803
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
5195
5804
|
cancelAnimationFrame(raf);
|
|
5196
5805
|
root.removeEventListener("pointerdown", onDown);
|
|
5197
5806
|
root.removeEventListener("pointermove", onMove);
|
|
5198
5807
|
root.removeEventListener("pointerup", onUp);
|
|
5199
5808
|
root.removeEventListener("pointercancel", onUp);
|
|
5809
|
+
root.removeEventListener("wheel", onWheel);
|
|
5200
5810
|
window.removeEventListener("keydown", onKey);
|
|
5201
5811
|
root.remove();
|
|
5812
|
+
if (priorFocus?.isConnected) priorFocus.focus({ preventScroll: true });
|
|
5202
5813
|
sphere?.dispose();
|
|
5814
|
+
deps.camera.position.set(priorPosition[0], priorPosition[1], priorPosition[2]);
|
|
5203
5815
|
deps.camera.perspective({ fov: priorFov, aspect: deps.camera.aspect });
|
|
5204
5816
|
deps.camera.quaternion.set(priorQuat[0], priorQuat[1], priorQuat[2], priorQuat[3]);
|
|
5205
5817
|
deps.requestRender();
|
|
@@ -5236,8 +5848,12 @@ var Analytics3D = class {
|
|
|
5236
5848
|
} catch {
|
|
5237
5849
|
}
|
|
5238
5850
|
}
|
|
5239
|
-
opened(seats, hasHeights) {
|
|
5240
|
-
this.emit("3d_opened", {
|
|
5851
|
+
opened(seats, hasHeights, buildMs) {
|
|
5852
|
+
this.emit("3d_opened", {
|
|
5853
|
+
seats,
|
|
5854
|
+
hasHeights,
|
|
5855
|
+
...buildMs === void 0 ? {} : { buildMs: Math.round(buildMs) }
|
|
5856
|
+
});
|
|
5241
5857
|
}
|
|
5242
5858
|
/** First user-driven orbit/dolly per mount only (the intro ease is not user
|
|
5243
5859
|
* input, so callers must gate this on real pointer/wheel gestures). */
|
|
@@ -5267,18 +5883,78 @@ var Analytics3D = class {
|
|
|
5267
5883
|
this.panoramaOpenedAt = 0;
|
|
5268
5884
|
this.emit("3d_panorama_closed", { viewMs });
|
|
5269
5885
|
}
|
|
5886
|
+
panoramaFailed(seatId, phase) {
|
|
5887
|
+
this.emit("3d_panorama_failed", { seatId, phase });
|
|
5888
|
+
}
|
|
5889
|
+
panoramaFallback(seatId) {
|
|
5890
|
+
this.emit("3d_panorama_fallback", { seatId });
|
|
5891
|
+
}
|
|
5892
|
+
contextLost() {
|
|
5893
|
+
this.emit("3d_context_lost");
|
|
5894
|
+
}
|
|
5895
|
+
contextRestored() {
|
|
5896
|
+
this.emit("3d_context_restored");
|
|
5897
|
+
}
|
|
5270
5898
|
};
|
|
5271
5899
|
|
|
5900
|
+
// src/view3d/positioningContext.ts
|
|
5901
|
+
function establishPositioningContext(container) {
|
|
5902
|
+
const originalInline = container.style.position;
|
|
5903
|
+
if (getComputedStyle(container).position !== "static") return () => {
|
|
5904
|
+
};
|
|
5905
|
+
container.style.position = "relative";
|
|
5906
|
+
return () => {
|
|
5907
|
+
if (container.style.position === "relative") container.style.position = originalInline;
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
// src/view3d/prepareScene.ts
|
|
5912
|
+
var import_meta = {};
|
|
5913
|
+
function prepareOnMain(input) {
|
|
5914
|
+
const started = performance.now();
|
|
5915
|
+
const model = buildSceneModel(input);
|
|
5916
|
+
return { model, buildMs: performance.now() - started, source: "main" };
|
|
5917
|
+
}
|
|
5918
|
+
function prepareVenue3D(input) {
|
|
5919
|
+
if (typeof Worker === "undefined") return Promise.resolve(prepareOnMain(input));
|
|
5920
|
+
return new Promise((resolve) => {
|
|
5921
|
+
const worker = new Worker(new URL("./scene/scene.worker.ts", import_meta.url), { type: "module" });
|
|
5922
|
+
let settled = false;
|
|
5923
|
+
const finish = (result) => {
|
|
5924
|
+
if (settled) return;
|
|
5925
|
+
settled = true;
|
|
5926
|
+
clearTimeout(timeout);
|
|
5927
|
+
worker.terminate();
|
|
5928
|
+
resolve(result);
|
|
5929
|
+
};
|
|
5930
|
+
const fallback = () => finish(prepareOnMain(input));
|
|
5931
|
+
const timeout = window.setTimeout(fallback, 15e3);
|
|
5932
|
+
worker.onerror = fallback;
|
|
5933
|
+
worker.onmessage = (event) => {
|
|
5934
|
+
finish({ model: event.data.model, buildMs: event.data.buildMs, source: "worker" });
|
|
5935
|
+
};
|
|
5936
|
+
worker.postMessage(input);
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5272
5940
|
// src/view3d/index.ts
|
|
5273
|
-
var SEAT_EYE_ABOVE_DECK = 1.02;
|
|
5274
5941
|
var DEG3 = Math.PI / 180;
|
|
5275
5942
|
var TAP_SLOP = 6;
|
|
5276
5943
|
var TAP_MS = 500;
|
|
5277
5944
|
function mountVenue3D(container, input, opts = {}) {
|
|
5278
|
-
const
|
|
5945
|
+
const buildStartedAt = performance.now();
|
|
5946
|
+
const model = input.prepared?.model ?? buildSceneModel(input);
|
|
5279
5947
|
const analytics = new Analytics3D(opts.onAnalytics);
|
|
5280
5948
|
const seatIdByIndex = new Array(model.seats.count);
|
|
5281
5949
|
for (const [id, idx] of model.seats.idToIndex) seatIdByIndex[idx] = id;
|
|
5950
|
+
const expandedSeatById = new Map(input.seats.map((seat) => [seat.id, seat]));
|
|
5951
|
+
const seatIdsByRow = /* @__PURE__ */ new Map();
|
|
5952
|
+
for (const seat of input.seats) {
|
|
5953
|
+
if (!seat.rowId) continue;
|
|
5954
|
+
const row = seatIdsByRow.get(seat.rowId);
|
|
5955
|
+
if (row) row.push(seat.id);
|
|
5956
|
+
else seatIdsByRow.set(seat.rowId, [seat.id]);
|
|
5957
|
+
}
|
|
5282
5958
|
const sectionIdBySeatId = /* @__PURE__ */ new Map();
|
|
5283
5959
|
for (const s of input.seats) sectionIdBySeatId.set(s.id, s.sectionId);
|
|
5284
5960
|
const hasHeights = (() => {
|
|
@@ -5293,10 +5969,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5293
5969
|
let disposed = false;
|
|
5294
5970
|
let selection = /* @__PURE__ */ new Map();
|
|
5295
5971
|
let panorama = null;
|
|
5972
|
+
let panoramaLoadAbort = null;
|
|
5973
|
+
let panoramaPriorZIndex = null;
|
|
5296
5974
|
const prefetch = /* @__PURE__ */ new Map();
|
|
5297
5975
|
let flightGen = 0;
|
|
5298
5976
|
let reducedForced = null;
|
|
5299
5977
|
let focusedFloor = -1;
|
|
5978
|
+
const raisePanoramaLayer = () => {
|
|
5979
|
+
if (panoramaPriorZIndex !== null) return;
|
|
5980
|
+
panoramaPriorZIndex = container.style.zIndex;
|
|
5981
|
+
container.style.zIndex = "20";
|
|
5982
|
+
};
|
|
5983
|
+
const restorePanoramaLayer = () => {
|
|
5984
|
+
if (panoramaPriorZIndex === null) return;
|
|
5985
|
+
if (container.style.zIndex === "20") container.style.zIndex = panoramaPriorZIndex;
|
|
5986
|
+
panoramaPriorZIndex = null;
|
|
5987
|
+
};
|
|
5300
5988
|
const rebuildGpu = () => {
|
|
5301
5989
|
gpu = buildGpuScene(glctx.gl, model);
|
|
5302
5990
|
pick = new PickPipeline(glctx.renderer, gpu.seatGeometry, gpu.solidGeometry, model.seats.count);
|
|
@@ -5330,6 +6018,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5330
6018
|
};
|
|
5331
6019
|
const glctx = new GLContext(container, {
|
|
5332
6020
|
onContextLost: () => {
|
|
6021
|
+
if (!disposed) analytics.contextLost();
|
|
5333
6022
|
contextLost = true;
|
|
5334
6023
|
loop.stop();
|
|
5335
6024
|
gpu = null;
|
|
@@ -5338,6 +6027,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5338
6027
|
onContextRestored: () => {
|
|
5339
6028
|
rebuildGpu();
|
|
5340
6029
|
contextLost = false;
|
|
6030
|
+
if (!disposed) analytics.contextRestored();
|
|
5341
6031
|
loop.requestRender();
|
|
5342
6032
|
}
|
|
5343
6033
|
});
|
|
@@ -5354,16 +6044,48 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5354
6044
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
5355
6045
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
5356
6046
|
})();
|
|
5357
|
-
orbit.frame(model.bounds, true, stageAzimuth);
|
|
6047
|
+
orbit.frame(model.bounds, true, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5358
6048
|
const cinematic = new Cinematic(orbit.camera);
|
|
5359
|
-
|
|
5360
|
-
container.style.position = "relative";
|
|
5361
|
-
}
|
|
6049
|
+
const restoreContainerPosition = establishPositioningContext(container);
|
|
5362
6050
|
const labelOverlay = new LabelOverlay(container, {
|
|
5363
6051
|
fontFamily: input.doc.theme?.fontFamily,
|
|
5364
6052
|
ink: input.doc.theme?.textColor
|
|
5365
6053
|
});
|
|
5366
6054
|
labelOverlay.setLabels(model.labels);
|
|
6055
|
+
const baseLabels = model.labels;
|
|
6056
|
+
let focusedSectionId = null;
|
|
6057
|
+
const showSectionSeatLabels = (sectionId) => {
|
|
6058
|
+
focusedSectionId = sectionId;
|
|
6059
|
+
labelOverlay.setForcedDense(!!sectionId);
|
|
6060
|
+
if (!sectionId) {
|
|
6061
|
+
labelOverlay.setLabels(baseLabels);
|
|
6062
|
+
return;
|
|
6063
|
+
}
|
|
6064
|
+
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
6065
|
+
const seatIds = new Set(sectionSeats.map((seat) => seat.id));
|
|
6066
|
+
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
6067
|
+
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))));
|
|
6068
|
+
if (model.seatCount <= 6e3) {
|
|
6069
|
+
labelOverlay.setLabels(focusedBase);
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
const labels = sectionSeats.flatMap((seat) => {
|
|
6073
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
6074
|
+
if (index === void 0) return [];
|
|
6075
|
+
const offset = index * 3;
|
|
6076
|
+
return [{
|
|
6077
|
+
id: `seat:${seat.id}`,
|
|
6078
|
+
kind: "seat",
|
|
6079
|
+
text: seat.displayLabel || seat.label,
|
|
6080
|
+
anchor: [
|
|
6081
|
+
model.seats.iPosition[offset],
|
|
6082
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
6083
|
+
model.seats.iPosition[offset + 2]
|
|
6084
|
+
]
|
|
6085
|
+
}];
|
|
6086
|
+
});
|
|
6087
|
+
labelOverlay.setLabels([...focusedBase, ...labels]);
|
|
6088
|
+
};
|
|
5367
6089
|
rebuildGpu();
|
|
5368
6090
|
const loop = new RenderLoop(() => {
|
|
5369
6091
|
if (contextLost || !gpu || frozen) return false;
|
|
@@ -5416,6 +6138,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5416
6138
|
if (!p) {
|
|
5417
6139
|
p = Promise.resolve(opts.getSeatView(seatId));
|
|
5418
6140
|
prefetch.set(seatId, p);
|
|
6141
|
+
void p.catch(() => {
|
|
6142
|
+
if (prefetch.get(seatId) === p) prefetch.delete(seatId);
|
|
6143
|
+
});
|
|
5419
6144
|
while (prefetch.size > PREFETCH_CAP) {
|
|
5420
6145
|
const oldest = prefetch.keys().next().value;
|
|
5421
6146
|
if (oldest === void 0) break;
|
|
@@ -5424,11 +6149,18 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5424
6149
|
}
|
|
5425
6150
|
return p;
|
|
5426
6151
|
};
|
|
5427
|
-
const
|
|
5428
|
-
|
|
5429
|
-
model.seats.
|
|
5430
|
-
|
|
5431
|
-
|
|
6152
|
+
const resolvedSeatViewPose = (seatId, idx) => {
|
|
6153
|
+
const seat = expandedSeatById.get(seatId);
|
|
6154
|
+
const floorIndex = Math.max(0, Math.round(model.seats.iFloor[idx] ?? 0));
|
|
6155
|
+
const floor = input.doc.floors?.[floorIndex];
|
|
6156
|
+
const focalPoint = seat?.focalPoint ?? floor?.focalPoint ?? input.doc.focalPoint;
|
|
6157
|
+
const deck = [
|
|
6158
|
+
model.seats.iPosition[idx * 3],
|
|
6159
|
+
model.seats.iPosition[idx * 3 + 1],
|
|
6160
|
+
model.seats.iPosition[idx * 3 + 2]
|
|
6161
|
+
];
|
|
6162
|
+
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0);
|
|
6163
|
+
};
|
|
5432
6164
|
const placeCameraFinal = (finalPos, focal) => {
|
|
5433
6165
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
5434
6166
|
orbit.camera.lookAt(new import_ogl8.Vec3(focal[0], focal[1], focal[2]));
|
|
@@ -5436,44 +6168,188 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5436
6168
|
orbit.camera.updateProjectionMatrix();
|
|
5437
6169
|
};
|
|
5438
6170
|
let arriveChip = null;
|
|
6171
|
+
const layoutArriveChip = () => {
|
|
6172
|
+
if (!arriveChip) return;
|
|
6173
|
+
const narrow = (container.clientWidth || glctx.canvas.clientWidth) <= 520;
|
|
6174
|
+
Object.assign(arriveChip.style, narrow ? {
|
|
6175
|
+
left: "12px",
|
|
6176
|
+
right: "12px",
|
|
6177
|
+
bottom: "70px",
|
|
6178
|
+
transform: "none",
|
|
6179
|
+
justifyContent: "center"
|
|
6180
|
+
} : {
|
|
6181
|
+
left: "50%",
|
|
6182
|
+
right: "auto",
|
|
6183
|
+
bottom: "18px",
|
|
6184
|
+
transform: "translateX(-50%)",
|
|
6185
|
+
justifyContent: "initial"
|
|
6186
|
+
});
|
|
6187
|
+
};
|
|
5439
6188
|
const removeArriveChip = () => {
|
|
5440
6189
|
arriveChip?.remove();
|
|
5441
6190
|
arriveChip = null;
|
|
5442
6191
|
};
|
|
5443
|
-
const showArriveChip = (seatId, gen) => {
|
|
6192
|
+
const showArriveChip = (seatId, gen, retry = false) => {
|
|
5444
6193
|
removeArriveChip();
|
|
5445
|
-
if (disposed
|
|
5446
|
-
const
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
chip.setAttribute("aria-label", `Open the 360\xB0 view from seat ${seatId}`);
|
|
5450
|
-
Object.assign(chip.style, {
|
|
6194
|
+
if (disposed) return null;
|
|
6195
|
+
const controls = document.createElement("div");
|
|
6196
|
+
controls.setAttribute("role", "group");
|
|
6197
|
+
Object.assign(controls.style, {
|
|
5451
6198
|
position: "absolute",
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
padding: "10px 18px",
|
|
5457
|
-
borderRadius: "999px",
|
|
5458
|
-
background: "rgba(12,18,32,0.78)",
|
|
5459
|
-
color: "#eef1f8",
|
|
5460
|
-
border: "1px solid rgba(150,165,205,0.4)",
|
|
5461
|
-
backdropFilter: "blur(6px)",
|
|
5462
|
-
font: "600 13px/1 inherit",
|
|
5463
|
-
cursor: "pointer",
|
|
6199
|
+
display: "flex",
|
|
6200
|
+
flexDirection: "column",
|
|
6201
|
+
alignItems: "center",
|
|
6202
|
+
gap: "6px",
|
|
5464
6203
|
zIndex: "4"
|
|
5465
6204
|
});
|
|
5466
|
-
|
|
5467
|
-
|
|
6205
|
+
const seat = expandedSeatById.get(seatId);
|
|
6206
|
+
const section = seat?.sectionId ? model.sections.find((candidate) => candidate.id === seat.sectionId) : void 0;
|
|
6207
|
+
const seatLabel = seat?.label ?? seatId;
|
|
6208
|
+
controls.setAttribute("aria-label", `Explore views near ${section?.label ? `${section.label}, ` : ""}seat ${seatLabel}`);
|
|
6209
|
+
const location = document.createElement("span");
|
|
6210
|
+
location.textContent = ["Live 3D seat view", section?.label, seatLabel].filter(Boolean).join(" \xB7 ");
|
|
6211
|
+
Object.assign(location.style, {
|
|
6212
|
+
maxWidth: "min(320px, calc(100vw - 32px))",
|
|
6213
|
+
padding: "5px 10px",
|
|
6214
|
+
borderRadius: "999px",
|
|
6215
|
+
overflow: "hidden",
|
|
6216
|
+
color: "#d9e3f5",
|
|
6217
|
+
background: "rgba(12,18,32,0.72)",
|
|
6218
|
+
font: "600 11px/1.2 inherit",
|
|
6219
|
+
textOverflow: "ellipsis",
|
|
6220
|
+
whiteSpace: "nowrap",
|
|
6221
|
+
backdropFilter: "blur(6px)"
|
|
6222
|
+
});
|
|
6223
|
+
controls.appendChild(location);
|
|
6224
|
+
const actions = document.createElement("div");
|
|
6225
|
+
Object.assign(actions.style, {
|
|
6226
|
+
display: "flex",
|
|
6227
|
+
alignItems: "center",
|
|
6228
|
+
justifyContent: "center",
|
|
6229
|
+
gap: "6px"
|
|
6230
|
+
});
|
|
6231
|
+
controls.appendChild(actions);
|
|
6232
|
+
const row = seat?.rowId ? seatIdsByRow.get(seat.rowId) : void 0;
|
|
6233
|
+
const at = row?.indexOf(seatId) ?? -1;
|
|
6234
|
+
const neighbours = at >= 0 ? [row?.[at - 1], row?.[at + 1]] : [void 0, void 0];
|
|
6235
|
+
const addButton = (label, ariaLabel, action, primary = false) => {
|
|
6236
|
+
const button = document.createElement("button");
|
|
6237
|
+
button.type = "button";
|
|
6238
|
+
button.textContent = label;
|
|
6239
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
6240
|
+
Object.assign(button.style, {
|
|
6241
|
+
minHeight: "44px",
|
|
6242
|
+
padding: primary ? "10px 16px" : "10px",
|
|
6243
|
+
borderRadius: "999px",
|
|
6244
|
+
background: "rgba(12,18,32,0.78)",
|
|
6245
|
+
color: "#eef1f8",
|
|
6246
|
+
border: "1px solid rgba(150,165,205,0.4)",
|
|
6247
|
+
backdropFilter: "blur(6px)",
|
|
6248
|
+
font: "600 13px/1 inherit",
|
|
6249
|
+
cursor: "pointer",
|
|
6250
|
+
whiteSpace: "nowrap"
|
|
6251
|
+
});
|
|
6252
|
+
if (!primary) button.style.minWidth = "44px";
|
|
6253
|
+
if (primary) button.dataset.panoramaTrigger = "";
|
|
6254
|
+
button.addEventListener("click", action);
|
|
6255
|
+
actions.appendChild(button);
|
|
6256
|
+
};
|
|
6257
|
+
if (neighbours[0]) {
|
|
6258
|
+
const label = expandedSeatById.get(neighbours[0])?.label ?? neighbours[0];
|
|
6259
|
+
addButton("\u2039", `View previous seat ${label}`, () => {
|
|
6260
|
+
void flyToSeat(neighbours[0]);
|
|
6261
|
+
});
|
|
6262
|
+
}
|
|
6263
|
+
if (opts.getSeatView) {
|
|
6264
|
+
addButton(retry ? "\u21BB Retry 360\xB0 panorama" : "\u25C9 Open 360\xB0 panorama", `${retry ? "Retry" : "Open"} the 360\xB0 panorama from seat ${seatLabel}`, () => {
|
|
6265
|
+
if (disposed || gen !== flightGen) {
|
|
6266
|
+
removeArriveChip();
|
|
6267
|
+
return;
|
|
6268
|
+
}
|
|
5468
6269
|
removeArriveChip();
|
|
5469
|
-
|
|
5470
|
-
}
|
|
5471
|
-
|
|
5472
|
-
|
|
6270
|
+
void openPanorama(seatId, 400, gen);
|
|
6271
|
+
}, true);
|
|
6272
|
+
}
|
|
6273
|
+
if (neighbours[1]) {
|
|
6274
|
+
const label = expandedSeatById.get(neighbours[1])?.label ?? neighbours[1];
|
|
6275
|
+
addButton("\u203A", `View next seat ${label}`, () => {
|
|
6276
|
+
void flyToSeat(neighbours[1]);
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6279
|
+
if (!actions.childElementCount) return null;
|
|
6280
|
+
container.appendChild(controls);
|
|
6281
|
+
arriveChip = controls;
|
|
6282
|
+
layoutArriveChip();
|
|
6283
|
+
return controls;
|
|
6284
|
+
};
|
|
6285
|
+
const navigationModeChip = document.createElement("div");
|
|
6286
|
+
navigationModeChip.setAttribute("role", "group");
|
|
6287
|
+
navigationModeChip.setAttribute("aria-label", "3D navigation mode");
|
|
6288
|
+
Object.assign(navigationModeChip.style, {
|
|
6289
|
+
position: "absolute",
|
|
6290
|
+
left: "14px",
|
|
6291
|
+
top: "62px",
|
|
6292
|
+
display: "flex",
|
|
6293
|
+
gap: "3px",
|
|
6294
|
+
padding: "3px",
|
|
6295
|
+
borderRadius: "999px",
|
|
6296
|
+
zIndex: "4",
|
|
6297
|
+
background: "rgba(12,18,32,0.72)",
|
|
6298
|
+
border: "1px solid rgba(150,165,205,0.35)",
|
|
6299
|
+
backdropFilter: "blur(6px)"
|
|
6300
|
+
});
|
|
6301
|
+
const rotateModeButton = document.createElement("button");
|
|
6302
|
+
rotateModeButton.type = "button";
|
|
6303
|
+
rotateModeButton.textContent = "\u21BB Rotate";
|
|
6304
|
+
rotateModeButton.setAttribute("aria-label", "Drag to rotate the 3D venue");
|
|
6305
|
+
const moveModeButton = document.createElement("button");
|
|
6306
|
+
moveModeButton.type = "button";
|
|
6307
|
+
moveModeButton.textContent = "\u2725 Move";
|
|
6308
|
+
moveModeButton.setAttribute("aria-label", "Drag to move the 3D venue left, right, up or down");
|
|
6309
|
+
const zoomOutButton = document.createElement("button");
|
|
6310
|
+
zoomOutButton.type = "button";
|
|
6311
|
+
zoomOutButton.textContent = "\u2212";
|
|
6312
|
+
zoomOutButton.setAttribute("aria-label", "Zoom out of the 3D venue");
|
|
6313
|
+
zoomOutButton.title = "Zoom out";
|
|
6314
|
+
const zoomInButton = document.createElement("button");
|
|
6315
|
+
zoomInButton.type = "button";
|
|
6316
|
+
zoomInButton.textContent = "+";
|
|
6317
|
+
zoomInButton.setAttribute("aria-label", "Zoom into the 3D venue");
|
|
6318
|
+
zoomInButton.title = "Zoom in";
|
|
6319
|
+
for (const button of [rotateModeButton, moveModeButton, zoomOutButton, zoomInButton]) {
|
|
6320
|
+
Object.assign(button.style, {
|
|
6321
|
+
minHeight: "34px",
|
|
6322
|
+
padding: "6px 10px",
|
|
6323
|
+
border: "0",
|
|
6324
|
+
borderRadius: "999px",
|
|
6325
|
+
color: "#c9d4ea",
|
|
6326
|
+
background: "transparent",
|
|
6327
|
+
font: "600 11px/1 inherit",
|
|
6328
|
+
cursor: "pointer",
|
|
6329
|
+
whiteSpace: "nowrap"
|
|
5473
6330
|
});
|
|
5474
|
-
|
|
5475
|
-
|
|
6331
|
+
if (button === zoomOutButton || button === zoomInButton) {
|
|
6332
|
+
button.style.minWidth = "34px";
|
|
6333
|
+
button.style.padding = "6px";
|
|
6334
|
+
button.style.fontSize = "17px";
|
|
6335
|
+
}
|
|
6336
|
+
navigationModeChip.appendChild(button);
|
|
6337
|
+
}
|
|
6338
|
+
const setNavigationMode = (mode) => {
|
|
6339
|
+
orbit.setPrimaryDragMode(mode);
|
|
6340
|
+
const rotateActive = mode === "orbit";
|
|
6341
|
+
rotateModeButton.setAttribute("aria-pressed", String(rotateActive));
|
|
6342
|
+
moveModeButton.setAttribute("aria-pressed", String(!rotateActive));
|
|
6343
|
+
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.48)" : "transparent";
|
|
6344
|
+
moveModeButton.style.background = rotateActive ? "transparent" : "rgba(96,110,150,0.48)";
|
|
6345
|
+
navigationModeChip.title = rotateActive ? "Drag to rotate \xB7 Shift-drag to move" : "Drag to move \xB7 Shift-drag to rotate";
|
|
5476
6346
|
};
|
|
6347
|
+
rotateModeButton.addEventListener("click", () => setNavigationMode("orbit"));
|
|
6348
|
+
moveModeButton.addEventListener("click", () => setNavigationMode("pan"));
|
|
6349
|
+
zoomOutButton.addEventListener("click", () => orbit.zoomBy(1.22));
|
|
6350
|
+
zoomInButton.addEventListener("click", () => orbit.zoomBy(0.82));
|
|
6351
|
+
setNavigationMode("orbit");
|
|
6352
|
+
container.appendChild(navigationModeChip);
|
|
5477
6353
|
const overviewChip = document.createElement("button");
|
|
5478
6354
|
overviewChip.type = "button";
|
|
5479
6355
|
overviewChip.textContent = "\u2302 Overview";
|
|
@@ -5493,17 +6369,46 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5493
6369
|
cursor: "pointer",
|
|
5494
6370
|
zIndex: "4"
|
|
5495
6371
|
});
|
|
5496
|
-
|
|
5497
|
-
if (disposed
|
|
6372
|
+
const focusOverview = () => {
|
|
6373
|
+
if (disposed) return;
|
|
6374
|
+
if (panorama) {
|
|
6375
|
+
panorama.dispose();
|
|
6376
|
+
panorama = null;
|
|
6377
|
+
analytics.panoramaClosed();
|
|
6378
|
+
}
|
|
6379
|
+
panoramaLoadAbort?.abort();
|
|
6380
|
+
panoramaLoadAbort = null;
|
|
6381
|
+
restorePanoramaLayer();
|
|
6382
|
+
overviewChip.style.display = "";
|
|
6383
|
+
labelOverlay.setVisible(true);
|
|
6384
|
+
frozen = false;
|
|
5498
6385
|
cancelFlight();
|
|
5499
6386
|
removeArriveChip();
|
|
5500
|
-
orbit
|
|
6387
|
+
setNavigationMode("orbit");
|
|
6388
|
+
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6389
|
+
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6390
|
+
opts.onViewTargetChange?.(null);
|
|
6391
|
+
showSectionSeatLabels(null);
|
|
6392
|
+
opts.onSectionFocusChange?.(null);
|
|
5501
6393
|
loop.requestRender();
|
|
5502
|
-
}
|
|
6394
|
+
};
|
|
6395
|
+
overviewChip.addEventListener("click", focusOverview);
|
|
5503
6396
|
container.appendChild(overviewChip);
|
|
5504
6397
|
const openPanorama = async (seatId, fadeMs, gen) => {
|
|
6398
|
+
panoramaLoadAbort?.abort();
|
|
6399
|
+
const loadAbort = new AbortController();
|
|
6400
|
+
panoramaLoadAbort = loadAbort;
|
|
6401
|
+
const seatIndex = model.seats.idToIndex.get(seatId);
|
|
6402
|
+
if (seatIndex === void 0) {
|
|
6403
|
+
orbit.syncFromCamera();
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
const seatLabel = expandedSeatById.get(seatId)?.label ?? seatId;
|
|
6407
|
+
const seatPose = resolvedSeatViewPose(seatId, seatIndex);
|
|
5505
6408
|
const viewPromise = ensureSeatView(seatId);
|
|
5506
6409
|
if (!viewPromise) {
|
|
6410
|
+
loadAbort.abort();
|
|
6411
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5507
6412
|
orbit.syncFromCamera();
|
|
5508
6413
|
return;
|
|
5509
6414
|
}
|
|
@@ -5513,34 +6418,56 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5513
6418
|
try {
|
|
5514
6419
|
view = await viewPromise;
|
|
5515
6420
|
} catch {
|
|
6421
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6422
|
+
analytics.panoramaFailed(seatId, "source");
|
|
5516
6423
|
if (!disposed && gen === flightGen) {
|
|
5517
6424
|
frozen = false;
|
|
5518
|
-
orbit.resumeAfterFlight(
|
|
6425
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5519
6426
|
loop.requestRender();
|
|
6427
|
+
const controls = showArriveChip(seatId, gen, true);
|
|
6428
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5520
6429
|
}
|
|
5521
6430
|
return;
|
|
5522
6431
|
}
|
|
5523
|
-
if (disposed || gen !== flightGen)
|
|
6432
|
+
if (disposed || gen !== flightGen) {
|
|
6433
|
+
loadAbort.abort();
|
|
6434
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6435
|
+
return;
|
|
6436
|
+
}
|
|
5524
6437
|
removeArriveChip();
|
|
5525
6438
|
const onClose = () => {
|
|
6439
|
+
loadAbort.abort();
|
|
6440
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5526
6441
|
panorama = null;
|
|
6442
|
+
restorePanoramaLayer();
|
|
6443
|
+
overviewChip.style.display = "";
|
|
5527
6444
|
labelOverlay.setVisible(true);
|
|
5528
6445
|
frozen = false;
|
|
5529
6446
|
analytics.panoramaClosed();
|
|
5530
|
-
orbit.resumeAfterFlight(
|
|
6447
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
5531
6448
|
loop.requestRender();
|
|
5532
|
-
showArriveChip(seatId, flightGen);
|
|
6449
|
+
const controls = showArriveChip(seatId, flightGen);
|
|
6450
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
5533
6451
|
};
|
|
5534
6452
|
const sceneMode = view.generated === true;
|
|
6453
|
+
const disclosure = seatViewDisclosure(view);
|
|
6454
|
+
raisePanoramaLayer();
|
|
6455
|
+
overviewChip.style.display = "none";
|
|
6456
|
+
const effectiveFadeMs = reducedMotion() ? 0 : fadeMs;
|
|
5535
6457
|
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5536
6458
|
gl: glctx.gl,
|
|
5537
6459
|
scene: gpu.main,
|
|
5538
6460
|
camera: orbit.camera,
|
|
5539
6461
|
requestRender: () => loop.requestRender(),
|
|
5540
|
-
|
|
5541
|
-
|
|
6462
|
+
cameraOriginWorld: sceneMode ? seatPose.eye : void 0,
|
|
6463
|
+
focalWorld: seatPose.focal
|
|
6464
|
+
}, { fadeMs: effectiveFadeMs, seatLabel, disclosure, onClose, signal: loadAbort.signal }) : null;
|
|
5542
6465
|
if (disposed || gen !== flightGen) {
|
|
6466
|
+
loadAbort.abort();
|
|
6467
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5543
6468
|
spherical?.dispose();
|
|
6469
|
+
overviewChip.style.display = "";
|
|
6470
|
+
restorePanoramaLayer();
|
|
5544
6471
|
return;
|
|
5545
6472
|
}
|
|
5546
6473
|
if (spherical) {
|
|
@@ -5550,12 +6477,21 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5550
6477
|
loop.requestRender();
|
|
5551
6478
|
panorama = spherical;
|
|
5552
6479
|
} else {
|
|
5553
|
-
|
|
6480
|
+
analytics.panoramaFallback(seatId);
|
|
6481
|
+
panorama = mountPanorama(container, view, {
|
|
6482
|
+
fadeMs: effectiveFadeMs,
|
|
6483
|
+
seatLabel,
|
|
6484
|
+
disclosure,
|
|
6485
|
+
onClose,
|
|
6486
|
+
signal: loadAbort.signal
|
|
6487
|
+
});
|
|
5554
6488
|
}
|
|
5555
6489
|
analytics.panoramaOpened();
|
|
5556
6490
|
};
|
|
5557
6491
|
const cancelFlight = () => {
|
|
5558
6492
|
flightGen++;
|
|
6493
|
+
panoramaLoadAbort?.abort();
|
|
6494
|
+
panoramaLoadAbort = null;
|
|
5559
6495
|
if (cinematic.active) {
|
|
5560
6496
|
cinematic.cancel();
|
|
5561
6497
|
orbit.resumeAfterFlight(model.focalWorld);
|
|
@@ -5565,15 +6501,19 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5565
6501
|
if (disposed || !gpu) return Promise.resolve();
|
|
5566
6502
|
const idx = model.seats.idToIndex.get(seatId);
|
|
5567
6503
|
if (idx === void 0) return Promise.resolve();
|
|
6504
|
+
opts.onViewTargetChange?.(seatId);
|
|
5568
6505
|
if (panorama) {
|
|
5569
6506
|
panorama.dispose();
|
|
5570
6507
|
panorama = null;
|
|
6508
|
+
overviewChip.style.display = "";
|
|
6509
|
+
restorePanoramaLayer();
|
|
5571
6510
|
}
|
|
6511
|
+
panoramaLoadAbort?.abort();
|
|
6512
|
+
panoramaLoadAbort = null;
|
|
5572
6513
|
frozen = false;
|
|
5573
6514
|
const gen = ++flightGen;
|
|
5574
6515
|
removeArriveChip();
|
|
5575
|
-
const seatEye =
|
|
5576
|
-
const focal = model.focalWorld;
|
|
6516
|
+
const { eye: seatEye, focal } = resolvedSeatViewPose(seatId, idx);
|
|
5577
6517
|
const start = [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z];
|
|
5578
6518
|
const { waypoints, finalPos } = buildWaypoints(start, seatEye, focal, model.bounds.center, model.bounds.radius);
|
|
5579
6519
|
if (reducedMotion()) {
|
|
@@ -5590,11 +6530,26 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5590
6530
|
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
5591
6531
|
if (disposed || gen !== flightGen) return;
|
|
5592
6532
|
analytics.cinematicPlayed(FLIGHT_DURATION_MS);
|
|
5593
|
-
orbit.resumeAfterFlight(
|
|
6533
|
+
orbit.resumeAfterFlight(focal);
|
|
5594
6534
|
loop.requestRender();
|
|
5595
6535
|
showArriveChip(seatId, gen);
|
|
5596
6536
|
});
|
|
5597
6537
|
};
|
|
6538
|
+
const focusSectionCamera = (sectionId) => {
|
|
6539
|
+
const sec = model.sections.find((candidate) => candidate.id === sectionId);
|
|
6540
|
+
if (!sec || sec.seatCount === 0) return false;
|
|
6541
|
+
cinematic.cancel();
|
|
6542
|
+
removeArriveChip();
|
|
6543
|
+
setNavigationMode("pan");
|
|
6544
|
+
showSectionSeatLabels(sectionId);
|
|
6545
|
+
const dx = sec.focalWorld[0] - sec.center[0];
|
|
6546
|
+
const dz = sec.focalWorld[2] - sec.center[2];
|
|
6547
|
+
const azimuth = Math.hypot(dx, dz) > sec.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
6548
|
+
orbit.frame({ center: sec.center, radius: Math.max(5, sec.radius * 0.5) }, false, azimuth);
|
|
6549
|
+
opts.onSectionFocusChange?.(sectionId);
|
|
6550
|
+
loop.requestRender();
|
|
6551
|
+
return true;
|
|
6552
|
+
};
|
|
5598
6553
|
let downX = 0, downY = 0, downT = 0, downId = -1, moved = false, suppressTap = false;
|
|
5599
6554
|
const onDown = (e) => {
|
|
5600
6555
|
if (downId !== -1) return;
|
|
@@ -5613,6 +6568,50 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5613
6568
|
if (e.pointerId !== downId) return;
|
|
5614
6569
|
if (Math.hypot(e.clientX - downX, e.clientY - downY) > TAP_SLOP) moved = true;
|
|
5615
6570
|
};
|
|
6571
|
+
const pickNearestProjectedSeat = (clientX, clientY, sectionId, maxDistance, bookableOnly) => {
|
|
6572
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
6573
|
+
const width = glctx.canvas.clientWidth || rect.width || 1;
|
|
6574
|
+
const height = glctx.canvas.clientHeight || rect.height || 1;
|
|
6575
|
+
const tapX = clientX - rect.left;
|
|
6576
|
+
const tapY = clientY - rect.top;
|
|
6577
|
+
const maxDistanceSq = maxDistance * maxDistance;
|
|
6578
|
+
let bestIndex = -1;
|
|
6579
|
+
let bestDistanceSq = maxDistanceSq;
|
|
6580
|
+
let bestDepth = Infinity;
|
|
6581
|
+
for (const seat of input.seats) {
|
|
6582
|
+
if (sectionId && seat.sectionId !== sectionId) continue;
|
|
6583
|
+
const index = model.seats.idToIndex.get(seat.id);
|
|
6584
|
+
if (index === void 0) continue;
|
|
6585
|
+
if (bookableOnly) {
|
|
6586
|
+
const state = Math.round(model.seats.iState[index] ?? 0);
|
|
6587
|
+
if (state !== 0 && state !== 3) continue;
|
|
6588
|
+
}
|
|
6589
|
+
const offset = index * 3;
|
|
6590
|
+
const screen = projectToScreen(
|
|
6591
|
+
orbit.camera.projectionViewMatrix,
|
|
6592
|
+
[
|
|
6593
|
+
model.seats.iPosition[offset],
|
|
6594
|
+
// Match the visible chair/number rather than the deck-level instance
|
|
6595
|
+
// origin. At strong zoom the vertical difference is dozens of pixels
|
|
6596
|
+
// and a perfectly reasonable click on the chair otherwise misses.
|
|
6597
|
+
model.seats.iPosition[offset + 1] + 0.55,
|
|
6598
|
+
model.seats.iPosition[offset + 2]
|
|
6599
|
+
],
|
|
6600
|
+
width,
|
|
6601
|
+
height
|
|
6602
|
+
);
|
|
6603
|
+
if (!screen.visible) continue;
|
|
6604
|
+
const dx = screen.x - tapX;
|
|
6605
|
+
const dy = screen.y - tapY;
|
|
6606
|
+
const distanceSq = dx * dx + dy * dy;
|
|
6607
|
+
if (distanceSq < bestDistanceSq || Math.abs(distanceSq - bestDistanceSq) < 1 && screen.depth < bestDepth) {
|
|
6608
|
+
bestIndex = index;
|
|
6609
|
+
bestDistanceSq = distanceSq;
|
|
6610
|
+
bestDepth = screen.depth;
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
return bestIndex;
|
|
6614
|
+
};
|
|
5616
6615
|
const onUp = (e) => {
|
|
5617
6616
|
if (e.pointerId !== downId) return;
|
|
5618
6617
|
const isTap = !moved && performance.now() - downT < TAP_MS;
|
|
@@ -5622,12 +6621,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5622
6621
|
return;
|
|
5623
6622
|
}
|
|
5624
6623
|
if (!isTap || !gpu || !pick) return;
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
6624
|
+
let idx = focusedSectionId ? pickNearestProjectedSeat(e.clientX, e.clientY, focusedSectionId, 44, true) : -1;
|
|
6625
|
+
if (focusedSectionId && idx < 0) {
|
|
6626
|
+
const sectionIndex = pickNearestProjectedSeat(e.clientX, e.clientY, null, 72, false);
|
|
6627
|
+
const sectionSeatId = sectionIndex >= 0 ? seatIdByIndex[sectionIndex] : void 0;
|
|
6628
|
+
const nextSectionId = sectionSeatId ? sectionIdBySeatId.get(sectionSeatId) : void 0;
|
|
6629
|
+
if (nextSectionId && nextSectionId !== focusedSectionId && focusSectionCamera(nextSectionId)) return;
|
|
6630
|
+
}
|
|
6631
|
+
if (!focusedSectionId) {
|
|
6632
|
+
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
6633
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
6634
|
+
const dpr = glctx.renderer.dpr;
|
|
6635
|
+
const { x, y } = pickPixelCoords(e.clientX, e.clientY, rect, dpr, glctx.gl.drawingBufferWidth, glctx.gl.drawingBufferHeight);
|
|
6636
|
+
const radius = Math.max(2, Math.round(8 * dpr));
|
|
6637
|
+
idx = pick.pick(orbit.camera, x, y, radius);
|
|
6638
|
+
if (idx < 0) idx = pickNearestProjectedSeat(e.clientX, e.clientY, null, 42, false);
|
|
6639
|
+
const overviewSeatId = idx >= 0 ? seatIdByIndex[idx] : void 0;
|
|
6640
|
+
const sectionId = overviewSeatId ? sectionIdBySeatId.get(overviewSeatId) : void 0;
|
|
6641
|
+
if (sectionId && focusSectionCamera(sectionId)) return;
|
|
6642
|
+
}
|
|
5631
6643
|
if (idx < 0 || idx >= seatIdByIndex.length) {
|
|
5632
6644
|
if (selection.size) setSelection([]);
|
|
5633
6645
|
return;
|
|
@@ -5653,9 +6665,11 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5653
6665
|
},
|
|
5654
6666
|
setSelection,
|
|
5655
6667
|
flyToSeat,
|
|
6668
|
+
focusOverview,
|
|
5656
6669
|
resize() {
|
|
5657
6670
|
const { width, height } = glctx.resize();
|
|
5658
6671
|
orbit.setAspect(width / Math.max(1, height));
|
|
6672
|
+
layoutArriveChip();
|
|
5659
6673
|
loop.requestRender();
|
|
5660
6674
|
},
|
|
5661
6675
|
stats() {
|
|
@@ -5680,6 +6694,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5680
6694
|
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
5681
6695
|
}
|
|
5682
6696
|
focusedFloor = value;
|
|
6697
|
+
setNavigationMode("orbit");
|
|
5683
6698
|
if (index !== null) {
|
|
5684
6699
|
const f = model.floors[index];
|
|
5685
6700
|
if (f.seatCount > 0) {
|
|
@@ -5697,6 +6712,9 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5697
6712
|
const zone = model.zones.find((z) => z.id === zoneId);
|
|
5698
6713
|
if (!zone || zone.seatCount === 0) return false;
|
|
5699
6714
|
cinematic.cancel();
|
|
6715
|
+
setNavigationMode("orbit");
|
|
6716
|
+
showSectionSeatLabels(null);
|
|
6717
|
+
opts.onSectionFocusChange?.(null);
|
|
5700
6718
|
const dx = zone.focalWorld[0] - zone.center[0];
|
|
5701
6719
|
const dz = zone.focalWorld[2] - zone.center[2];
|
|
5702
6720
|
const azimuth = Math.hypot(dx, dz) > zone.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
@@ -5708,13 +6726,24 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5708
6726
|
return model.sections;
|
|
5709
6727
|
},
|
|
5710
6728
|
focusSection(sectionId) {
|
|
5711
|
-
|
|
5712
|
-
|
|
6729
|
+
return focusSectionCamera(sectionId);
|
|
6730
|
+
},
|
|
6731
|
+
rows(sectionId) {
|
|
6732
|
+
return sectionId ? model.rows.filter((row) => row.sectionId === sectionId) : model.rows;
|
|
6733
|
+
},
|
|
6734
|
+
seatsInRow(rowId) {
|
|
6735
|
+
return input.seats.filter((seat) => seat.rowId === rowId).map((seat) => ({ id: seat.id, label: seat.displayLabel || seat.label }));
|
|
6736
|
+
},
|
|
6737
|
+
focusRow(rowId) {
|
|
6738
|
+
const row = model.rows.find((candidate) => candidate.id === rowId);
|
|
6739
|
+
if (!row || row.seatCount === 0) return false;
|
|
5713
6740
|
cinematic.cancel();
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
6741
|
+
setNavigationMode("pan");
|
|
6742
|
+
if (row.sectionId) {
|
|
6743
|
+
showSectionSeatLabels(row.sectionId);
|
|
6744
|
+
opts.onSectionFocusChange?.(row.sectionId);
|
|
6745
|
+
}
|
|
6746
|
+
orbit.frame({ center: row.center, radius: Math.max(2.5, row.radius * 0.65) });
|
|
5718
6747
|
loop.requestRender();
|
|
5719
6748
|
return true;
|
|
5720
6749
|
},
|
|
@@ -5725,6 +6754,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5725
6754
|
disposed = true;
|
|
5726
6755
|
removeArriveChip();
|
|
5727
6756
|
overviewChip.remove();
|
|
6757
|
+
navigationModeChip.remove();
|
|
5728
6758
|
cancelFlight();
|
|
5729
6759
|
loop.stop();
|
|
5730
6760
|
labelOverlay.dispose();
|
|
@@ -5733,6 +6763,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5733
6763
|
panorama.dispose();
|
|
5734
6764
|
panorama = null;
|
|
5735
6765
|
}
|
|
6766
|
+
restorePanoramaLayer();
|
|
6767
|
+
restoreContainerPosition();
|
|
5736
6768
|
glctx.canvas.removeEventListener("pointerdown", onDown);
|
|
5737
6769
|
glctx.canvas.removeEventListener("pointermove", onMove);
|
|
5738
6770
|
glctx.canvas.removeEventListener("pointerup", onUp);
|
|
@@ -5745,12 +6777,13 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5745
6777
|
glctx.dispose();
|
|
5746
6778
|
}
|
|
5747
6779
|
};
|
|
5748
|
-
analytics.opened(model.seatCount, hasHeights);
|
|
6780
|
+
analytics.opened(model.seatCount, hasHeights, input.prepared?.buildMs ?? performance.now() - buildStartedAt);
|
|
5749
6781
|
return handle;
|
|
5750
6782
|
}
|
|
5751
6783
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5752
6784
|
0 && (module.exports = {
|
|
5753
6785
|
buildSceneModel,
|
|
5754
|
-
mountVenue3D
|
|
6786
|
+
mountVenue3D,
|
|
6787
|
+
prepareVenue3D
|
|
5755
6788
|
});
|
|
5756
6789
|
//# sourceMappingURL=index.cjs.map
|