@seatlayer/js 0.28.4 → 0.30.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/README.md +32 -5
- package/dist/index.cjs +370 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -4
- package/dist/index.d.ts +104 -4
- package/dist/index.js +365 -23
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/SeatingChart.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
PickerController,
|
|
4
|
+
loadLocale,
|
|
5
|
+
setStringOverrides,
|
|
6
|
+
t
|
|
7
|
+
} from "@seatlayer/core";
|
|
3
8
|
|
|
4
9
|
// src/api.ts
|
|
5
10
|
var ApiError = class extends Error {
|
|
@@ -161,6 +166,7 @@ var SeatingChart = class {
|
|
|
161
166
|
this.rendered = false;
|
|
162
167
|
return this;
|
|
163
168
|
}
|
|
169
|
+
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
164
170
|
this.mode_ = info.mode === "test" ? "test" : "live";
|
|
165
171
|
if (this.opts.seatTooltip !== false) {
|
|
166
172
|
const tip = document.createElement("div");
|
|
@@ -341,7 +347,14 @@ var SeatingChart = class {
|
|
|
341
347
|
/** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
|
|
342
348
|
async bestAvailableOrThrow(qty, categoryKey, options = {}) {
|
|
343
349
|
const h = await this.controller.bestAvailable(qty, categoryKey, options);
|
|
344
|
-
return h ? {
|
|
350
|
+
return h ? {
|
|
351
|
+
holdId: h.holdId,
|
|
352
|
+
expiresAt: h.expiresAt,
|
|
353
|
+
labels: h.labels,
|
|
354
|
+
seats: h.seats,
|
|
355
|
+
items: h.items,
|
|
356
|
+
...options.zoneId ? { zoneId: options.zoneId } : {}
|
|
357
|
+
} : null;
|
|
345
358
|
}
|
|
346
359
|
/**
|
|
347
360
|
* Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
|
|
@@ -373,6 +386,17 @@ var SeatingChart = class {
|
|
|
373
386
|
setColorblindSafe(on) {
|
|
374
387
|
this.controller.setColorblindSafe(on);
|
|
375
388
|
}
|
|
389
|
+
/** Switch the 2D canvas projection.
|
|
390
|
+
* @deprecated `'isometric'` and `'perspective'` are retired in favour of the
|
|
391
|
+
* real 3D venue view (`setBuyerView('venue3d')`); accepted for source
|
|
392
|
+
* compatibility until the next major. */
|
|
393
|
+
setViewMode(mode) {
|
|
394
|
+
this.controller.setViewMode(mode);
|
|
395
|
+
}
|
|
396
|
+
/** Current canvas projection. */
|
|
397
|
+
getViewMode() {
|
|
398
|
+
return this.controller.getViewMode();
|
|
399
|
+
}
|
|
376
400
|
/** Zoom in one step (same increment as the wheel/pinch gesture). */
|
|
377
401
|
zoomIn() {
|
|
378
402
|
this.controller.zoomIn();
|
|
@@ -1082,6 +1106,41 @@ function escapeOption(value) {
|
|
|
1082
1106
|
"'": "'"
|
|
1083
1107
|
})[character]);
|
|
1084
1108
|
}
|
|
1109
|
+
var SEATLAYER_MODULE_URL = (() => {
|
|
1110
|
+
if (typeof document !== "undefined" && document.currentScript instanceof HTMLScriptElement && document.currentScript.src) {
|
|
1111
|
+
return document.currentScript.src;
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
const u = import.meta.url;
|
|
1115
|
+
if (typeof u === "string" && u) return u;
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
return void 0;
|
|
1119
|
+
})();
|
|
1120
|
+
var _webgl2Cache = null;
|
|
1121
|
+
function hasWebGL2() {
|
|
1122
|
+
if (_webgl2Cache !== null) return _webgl2Cache;
|
|
1123
|
+
try {
|
|
1124
|
+
if (typeof document === "undefined") return _webgl2Cache = false;
|
|
1125
|
+
const canvas = document.createElement("canvas");
|
|
1126
|
+
_webgl2Cache = !!canvas.getContext("webgl2");
|
|
1127
|
+
} catch {
|
|
1128
|
+
_webgl2Cache = false;
|
|
1129
|
+
}
|
|
1130
|
+
return _webgl2Cache;
|
|
1131
|
+
}
|
|
1132
|
+
async function loadVenue3d() {
|
|
1133
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1134
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1135
|
+
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1136
|
+
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1137
|
+
return import(
|
|
1138
|
+
/* @vite-ignore */
|
|
1139
|
+
url
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
return import("@seatlayer/core/view3d");
|
|
1143
|
+
}
|
|
1085
1144
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1086
1145
|
var CSS = `
|
|
1087
1146
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
@@ -1613,6 +1672,45 @@ var CSS = `
|
|
|
1613
1672
|
.sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1614
1673
|
.sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
|
|
1615
1674
|
|
|
1675
|
+
/* 3D venue overlay \u2014 mounts over the (paused) Konva stage inside the map host.
|
|
1676
|
+
Carries its own gradient so it paints instantly before the scene builds, and
|
|
1677
|
+
cross-fades on enter/exit via a compositor-only opacity transition. Sits below
|
|
1678
|
+
the anchored chrome (z-index:5) and the confirm card (z-index:10) so the
|
|
1679
|
+
Map|3D toggle and the seat confirm both stay usable over it. */
|
|
1680
|
+
.sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
|
|
1681
|
+
transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
|
|
1682
|
+
.sl-view3d canvas{display:block;width:100%;height:100%}
|
|
1683
|
+
[data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
|
|
1684
|
+
.sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
|
|
1685
|
+
padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
|
|
1686
|
+
color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
|
|
1687
|
+
.sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
|
|
1688
|
+
.sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1689
|
+
/* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
|
|
1690
|
+
.sl-picker[data-view3d="on"] .sl-rungs,
|
|
1691
|
+
.sl-picker[data-view3d="on"] .sl-floors,
|
|
1692
|
+
.sl-picker[data-view3d="on"] .sl-zoom,
|
|
1693
|
+
.sl-picker[data-view3d="on"] .sl-seccard,
|
|
1694
|
+
.sl-picker[data-view3d="on"] .sl-minimap{display:none!important}
|
|
1695
|
+
/* The confirm card bottom-sheets over 3D (no 2D screen anchor to track). */
|
|
1696
|
+
.sl-picker[data-view3d="on"] .sl-confirm{left:50%!important;top:auto!important;bottom:16px;
|
|
1697
|
+
transform:translateX(-50%);width:min(342px,calc(100% - 24px))}
|
|
1698
|
+
.sl-picker[data-view3d="on"] .sl-confirm[data-placement]{transform:translateX(-50%)}
|
|
1699
|
+
|
|
1700
|
+
/* Plain "View from here" action shown when no real photo exists (the synthetic
|
|
1701
|
+
thumb is suppressed at card size \u2014 full-screen is where it earns its keep). */
|
|
1702
|
+
.sl-confirm-viewbtn{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1703
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1704
|
+
color:var(--sl-text);background:transparent;transition:border-color .15s,background .15s}
|
|
1705
|
+
.sl-confirm-viewbtn:hover,.sl-confirm-viewbtn:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
1706
|
+
/* confirm-card "See it in 3D" / "View from this seat" action \u2014 the purchase-
|
|
1707
|
+
moment bridge into the cinematic. Styled like the view-from-seat button. */
|
|
1708
|
+
.sl-confirm-3d{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1709
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1710
|
+
color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 12%,transparent);transition:border-color .15s,background .15s}
|
|
1711
|
+
.sl-confirm-3d:hover,.sl-confirm-3d:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 20%,transparent)}
|
|
1712
|
+
.sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1713
|
+
|
|
1616
1714
|
/* multi-floor switcher (flows within the left-rail region) */
|
|
1617
1715
|
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
1618
1716
|
.sl-floors.on{display:flex}
|
|
@@ -1840,6 +1938,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1840
1938
|
// arena / multi-floor / seat-view chrome
|
|
1841
1939
|
this.rungsEl = null;
|
|
1842
1940
|
this.projectionEl = null;
|
|
1941
|
+
// --- 3D venue view (Map | 3D) ---
|
|
1942
|
+
this.buyerView = "map";
|
|
1943
|
+
this.view3dEl = null;
|
|
1944
|
+
this.view3dHandle = null;
|
|
1945
|
+
/** Monotonic token so a stale async mount (buyer left before OGL finished
|
|
1946
|
+
* loading) never installs its handle over a newer state. */
|
|
1947
|
+
this.view3dGen = 0;
|
|
1948
|
+
/** Seat whose 2D confirm card launched "See it in 3D"; re-shown on return. */
|
|
1949
|
+
this.view3dReturnSeat = null;
|
|
1843
1950
|
this.floorsEl = null;
|
|
1844
1951
|
this.secCardEl = null;
|
|
1845
1952
|
this.viewEl = null;
|
|
@@ -1890,6 +1997,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1890
1997
|
this.lastAvailFloorId = "";
|
|
1891
1998
|
this.availQuietUntil = 0;
|
|
1892
1999
|
this.liveTimer = null;
|
|
2000
|
+
/** `perspective` (2.5D) is retired from the buyer surface — accept it for
|
|
2001
|
+
* source compatibility but coerce to `flat` with a one-time deprecation warn. */
|
|
2002
|
+
this.perspectiveWarned = false;
|
|
1893
2003
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1894
2004
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1895
2005
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
@@ -1908,12 +2018,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1908
2018
|
onSelectionChange: () => {
|
|
1909
2019
|
this.syncTray();
|
|
1910
2020
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
2021
|
+
this.syncSelectionTo3d();
|
|
1911
2022
|
},
|
|
1912
2023
|
onStatusChange: () => {
|
|
1913
2024
|
this.syncPrices();
|
|
1914
2025
|
this.evictTakenSelections();
|
|
1915
2026
|
this.detectBooked();
|
|
1916
2027
|
this.refreshMinimap();
|
|
2028
|
+
this.pushAvailabilityTo3d();
|
|
1917
2029
|
},
|
|
1918
2030
|
onHoldExpired: () => {
|
|
1919
2031
|
this.hold = null;
|
|
@@ -2002,19 +2114,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2002
2114
|
const realPhoto = seat.viewUrl ?? "";
|
|
2003
2115
|
const hasStage = this.chartHasStage();
|
|
2004
2116
|
if (!realPhoto && !hasStage) return "";
|
|
2005
|
-
let url = realPhoto;
|
|
2006
2117
|
let distance = null;
|
|
2007
|
-
if (!
|
|
2118
|
+
if (!realPhoto) {
|
|
2008
2119
|
try {
|
|
2009
2120
|
const thumb = generateSeatThumb(seat, seat.focalPoint ?? doc.focalPoint);
|
|
2010
|
-
url = thumb.url;
|
|
2011
2121
|
distance = thumb.distanceM ?? null;
|
|
2012
2122
|
} catch {
|
|
2013
2123
|
return "";
|
|
2014
2124
|
}
|
|
2015
2125
|
}
|
|
2016
2126
|
const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
|
|
2017
|
-
|
|
2127
|
+
const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${realPhoto}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
|
|
2128
|
+
return viewBtn + sightHtml;
|
|
2129
|
+
}
|
|
2130
|
+
/** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
|
|
2131
|
+
* confirm card. Only when 3D is available — the purchase-moment bridge into
|
|
2132
|
+
* the cinematic that reaches buyers who never press the Map | 3D toggle. */
|
|
2133
|
+
see3dConfirmHtml() {
|
|
2134
|
+
if (!this.canOffer3d()) return "";
|
|
2135
|
+
const label = this.buyerView === "venue3d" ? this.tf("picker.viewFromThisSeat", "View from this seat") : this.tf("picker.seeItIn3d", "See it in 3D");
|
|
2136
|
+
return `<button type="button" class="sl-confirm-3d" aria-label="${label}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l9 5v10l-9 5-9-5V7z"/><path d="M12 12l9-5M12 12v10M12 12L3 7"/></svg><span>${label}</span></button>`;
|
|
2018
2137
|
}
|
|
2019
2138
|
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
2020
2139
|
escCx(value) {
|
|
@@ -2431,7 +2550,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2431
2550
|
}
|
|
2432
2551
|
this.els.boot.remove();
|
|
2433
2552
|
this.salesClosed = !!info.salesClosed;
|
|
2434
|
-
this.controller.setViewMode(this.opts.initialView
|
|
2553
|
+
this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
|
|
2435
2554
|
this.buildRegions();
|
|
2436
2555
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
2437
2556
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -3115,15 +3234,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
3115
3234
|
const doc = this.controller.doc;
|
|
3116
3235
|
if (!doc || !this.els.map) return;
|
|
3117
3236
|
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
3118
|
-
if (
|
|
3237
|
+
if (this.canOffer3d()) {
|
|
3119
3238
|
const projection = document.createElement("div");
|
|
3120
3239
|
projection.className = "sl-projection";
|
|
3121
3240
|
projection.setAttribute("role", "group");
|
|
3122
|
-
projection.setAttribute("aria-label", "
|
|
3123
|
-
projection.innerHTML = '<button type="button" data-
|
|
3241
|
+
projection.setAttribute("aria-label", "Venue view");
|
|
3242
|
+
projection.innerHTML = '<button type="button" data-view="map" aria-pressed="true" title="Flat 2D map">Map</button><button type="button" data-view="venue3d" aria-pressed="false" title="Interactive 3D venue view">3D</button>';
|
|
3124
3243
|
projection.querySelectorAll("button").forEach((button) => {
|
|
3125
3244
|
button.addEventListener("click", () => {
|
|
3126
|
-
this.
|
|
3245
|
+
this.setBuyerView(button.dataset.view);
|
|
3127
3246
|
});
|
|
3128
3247
|
});
|
|
3129
3248
|
this.regions["top-right"].appendChild(projection);
|
|
@@ -3193,13 +3312,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
3193
3312
|
}
|
|
3194
3313
|
syncProjection() {
|
|
3195
3314
|
if (!this.projectionEl) return;
|
|
3196
|
-
const active = this.controller.getViewMode();
|
|
3197
3315
|
this.projectionEl.querySelectorAll("button").forEach((button) => {
|
|
3198
|
-
const on = button.dataset.
|
|
3316
|
+
const on = button.dataset.view === this.buyerView;
|
|
3199
3317
|
button.classList.toggle("on", on);
|
|
3200
3318
|
button.setAttribute("aria-pressed", String(on));
|
|
3201
3319
|
});
|
|
3202
3320
|
}
|
|
3321
|
+
/** Can this picker offer the 3D venue view? Requires the option (default on)
|
|
3322
|
+
* and WebGL2, and a chart to render. */
|
|
3323
|
+
canOffer3d() {
|
|
3324
|
+
return this.opts.enable3D !== false && hasWebGL2() && !!this.controller.doc;
|
|
3325
|
+
}
|
|
3203
3326
|
/** Reflect the active floor onto the switcher rail. */
|
|
3204
3327
|
syncFloors() {
|
|
3205
3328
|
if (!this.floorsEl) return;
|
|
@@ -3504,17 +3627,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
3504
3627
|
el.setAttribute("aria-modal", "true");
|
|
3505
3628
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
3506
3629
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
3507
|
-
el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
|
|
3630
|
+
el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + this.see3dConfirmHtml() + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
|
|
3508
3631
|
this.els.map.appendChild(el);
|
|
3509
3632
|
this.confirmEl = el;
|
|
3510
3633
|
this.reanchorConfirm();
|
|
3511
3634
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
3635
|
+
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
3636
|
+
if (this.buyerView === "venue3d") {
|
|
3637
|
+
void this.view3dHandle?.flyToSeat(seat.id);
|
|
3638
|
+
} else {
|
|
3639
|
+
this.view3dReturnSeat = seat;
|
|
3640
|
+
this.dismissConfirm();
|
|
3641
|
+
void this.enter3d(seat.id);
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3512
3644
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
3513
3645
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
3514
3646
|
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
3515
3647
|
}
|
|
3516
3648
|
reanchorConfirm() {
|
|
3517
3649
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
3650
|
+
if (this.root?.dataset.view3d === "on") return;
|
|
3518
3651
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
3519
3652
|
if (this.root?.dataset.layout === "narrow") return;
|
|
3520
3653
|
const mapWidth = this.els.map.clientWidth;
|
|
@@ -3597,16 +3730,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
3597
3730
|
this.viewEl = el;
|
|
3598
3731
|
const pano = el.querySelector(".sl-view-pano");
|
|
3599
3732
|
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
3600
|
-
|
|
3601
|
-
|
|
3733
|
+
const VFOV_DEG = 70;
|
|
3734
|
+
const MAX_PITCH_DEG = 35;
|
|
3735
|
+
let zoom = 1;
|
|
3736
|
+
const vh0 = pano.clientHeight || 1;
|
|
3737
|
+
const vw0 = pano.clientWidth || 1;
|
|
3738
|
+
let posX = -(vh0 * (180 / VFOV_DEG) * 2 / 2 - vw0 / 2);
|
|
3602
3739
|
let posY = 0;
|
|
3603
3740
|
const apply = () => {
|
|
3604
3741
|
const h = pano.clientHeight || 1;
|
|
3605
|
-
const bgH = h * zoom;
|
|
3742
|
+
const bgH = h * (180 / VFOV_DEG) * zoom;
|
|
3606
3743
|
const overV = Math.max(0, bgH - h);
|
|
3607
|
-
|
|
3744
|
+
const pitchLimit = Math.min(overV / 2, MAX_PITCH_DEG / 180 * bgH);
|
|
3745
|
+
posY = Math.min(pitchLimit, Math.max(-pitchLimit, posY));
|
|
3608
3746
|
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
3609
|
-
pano.style.backgroundPosition = `${posX}px ${posY
|
|
3747
|
+
pano.style.backgroundPosition = `${posX}px ${posY - overV / 2}px`;
|
|
3610
3748
|
};
|
|
3611
3749
|
apply();
|
|
3612
3750
|
let dragging = false;
|
|
@@ -4361,9 +4499,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
4361
4499
|
this.controller.setColorblindSafe(on);
|
|
4362
4500
|
writeStoredColorblind(on);
|
|
4363
4501
|
}
|
|
4364
|
-
/** Switch the
|
|
4502
|
+
/** Switch the underlying 2D renderer projection (flat / isometric). The buyer
|
|
4503
|
+
* UI no longer exposes `perspective`; it is coerced to `flat`. */
|
|
4365
4504
|
setViewMode(mode) {
|
|
4366
|
-
this.controller.setViewMode(mode);
|
|
4505
|
+
this.controller.setViewMode(this.normalizeInitialView(mode));
|
|
4367
4506
|
this.syncProjection();
|
|
4368
4507
|
this.dismissConfirm();
|
|
4369
4508
|
}
|
|
@@ -4371,6 +4510,208 @@ var SeatPicker = class _SeatPicker {
|
|
|
4371
4510
|
getViewMode() {
|
|
4372
4511
|
return this.controller.getViewMode();
|
|
4373
4512
|
}
|
|
4513
|
+
normalizeInitialView(mode) {
|
|
4514
|
+
if (mode === "perspective") {
|
|
4515
|
+
if (!this.perspectiveWarned) {
|
|
4516
|
+
this.perspectiveWarned = true;
|
|
4517
|
+
console.warn(
|
|
4518
|
+
"[seatlayer] initialView:'perspective' (2.5D) is deprecated for the buyer picker and was coerced to 'flat'. Use the Map | 3D control for the immersive view."
|
|
4519
|
+
);
|
|
4520
|
+
}
|
|
4521
|
+
return "flat";
|
|
4522
|
+
}
|
|
4523
|
+
return mode ?? "flat";
|
|
4524
|
+
}
|
|
4525
|
+
// ---- 3D venue view ---------------------------------------------------------
|
|
4526
|
+
/** Current buyer view: the flat map, or the interactive 3D venue. */
|
|
4527
|
+
getBuyerView() {
|
|
4528
|
+
return this.buyerView;
|
|
4529
|
+
}
|
|
4530
|
+
/**
|
|
4531
|
+
* Switch between the flat seat **Map** and the interactive **3D venue** view —
|
|
4532
|
+
* the same control the buyer's on-widget `Map | 3D` toggle drives.
|
|
4533
|
+
*
|
|
4534
|
+
* Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
|
|
4535
|
+
* camera flies to the seat and holds in the live scene (a chip offers the
|
|
4536
|
+
* 360° view-from-seat) —
|
|
4537
|
+
* the widget enters 3D and auto-flies the camera to that seat, dissolving into
|
|
4538
|
+
* its view-from-seat panorama (the same chain as tapping "See the view from
|
|
4539
|
+
* here" on a seat's confirm card). When the widget is **already** in the 3D
|
|
4540
|
+
* view, the camera simply flies to the requested seat — the GL scene is not
|
|
4541
|
+
* torn down or rebuilt, so there is no flash or re-entry.
|
|
4542
|
+
*
|
|
4543
|
+
* No-op when the view is unchanged and no `flyToSeatId` is given, or when 3D is
|
|
4544
|
+
* unavailable for this chart.
|
|
4545
|
+
*
|
|
4546
|
+
* @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
|
|
4547
|
+
* @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
|
|
4548
|
+
* id to fly the camera to — cinematic → panorama.
|
|
4549
|
+
* Ignored when `view` is `'map'`.
|
|
4550
|
+
*
|
|
4551
|
+
* @example
|
|
4552
|
+
* // Public 3D tour entry — enter 3D and fly straight to the buyer's seat:
|
|
4553
|
+
* picker.setBuyerView('venue3d', { flyToSeatId: 'A-12' });
|
|
4554
|
+
*/
|
|
4555
|
+
setBuyerView(view, opts) {
|
|
4556
|
+
if (view === "map") {
|
|
4557
|
+
this.exit3d();
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
const flyToSeatId = opts?.flyToSeatId;
|
|
4561
|
+
if (this.buyerView === "venue3d") {
|
|
4562
|
+
if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4565
|
+
void this.enter3d(flyToSeatId);
|
|
4566
|
+
}
|
|
4567
|
+
/** SeatStatus → the view3d palette state. Selection is layered separately. */
|
|
4568
|
+
seatState3dFor(id) {
|
|
4569
|
+
switch (this.controller.getStatus(id)) {
|
|
4570
|
+
case "held":
|
|
4571
|
+
return "held";
|
|
4572
|
+
case "booked":
|
|
4573
|
+
return "sold";
|
|
4574
|
+
case "not_for_sale":
|
|
4575
|
+
return "dimmed";
|
|
4576
|
+
default:
|
|
4577
|
+
return "available";
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
/** Push the full live availability snapshot into the 3D handle (selection is
|
|
4581
|
+
* preserved inside the module). Cheap enough per status delta. */
|
|
4582
|
+
pushAvailabilityTo3d() {
|
|
4583
|
+
if (!this.view3dHandle) return;
|
|
4584
|
+
const updates = this.allSeats().map((s) => ({ seatId: s.id, state: this.seatState3dFor(s.id) }));
|
|
4585
|
+
this.view3dHandle.setAvailability(updates);
|
|
4586
|
+
}
|
|
4587
|
+
/** Mirror the authoritative widget selection into the 3D handle. */
|
|
4588
|
+
syncSelectionTo3d() {
|
|
4589
|
+
if (!this.view3dHandle) return;
|
|
4590
|
+
this.view3dHandle.setSelection(this.controller.getSelection().map((s) => s.id));
|
|
4591
|
+
}
|
|
4592
|
+
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
4593
|
+
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
4594
|
+
seatViewFor3d(seatId) {
|
|
4595
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4596
|
+
if (!seat) return null;
|
|
4597
|
+
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
4598
|
+
const doc = this.controller.doc;
|
|
4599
|
+
if (!doc) return null;
|
|
4600
|
+
const activeId = this.controller.getActiveFloorId();
|
|
4601
|
+
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
4602
|
+
try {
|
|
4603
|
+
return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
|
|
4604
|
+
} catch {
|
|
4605
|
+
return null;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
/** Route the module's decoupled analytics into the host callback, tagged buyer. */
|
|
4609
|
+
emit3dAnalytics(event, props) {
|
|
4610
|
+
try {
|
|
4611
|
+
this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
|
|
4612
|
+
} catch {
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
/** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
|
|
4616
|
+
* controller, then raise the shared confirm card (bottom-sheeted in 3D). */
|
|
4617
|
+
onView3dSeatPick(seatId) {
|
|
4618
|
+
if (this.salesClosed) {
|
|
4619
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
4620
|
+
this.syncSelectionTo3d();
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4624
|
+
if (!seat) return;
|
|
4625
|
+
const already = this.committedSelection().some((s) => s.id === seatId);
|
|
4626
|
+
if (already) {
|
|
4627
|
+
this.controller.deselect([seatId]);
|
|
4628
|
+
this.dismissConfirm();
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
this.flashPickedSeat(seatId);
|
|
4632
|
+
this.controller.select([seatId]);
|
|
4633
|
+
if (this.opts.confirmSelection !== false) this.showConfirm(seat);
|
|
4634
|
+
else this.syncTray();
|
|
4635
|
+
}
|
|
4636
|
+
async enter3d(flySeatId) {
|
|
4637
|
+
if (this.view3dEl || !this.canOffer3d() || !this.els.map) return;
|
|
4638
|
+
const doc = this.controller.doc;
|
|
4639
|
+
if (!doc) return;
|
|
4640
|
+
this.buyerView = "venue3d";
|
|
4641
|
+
this.root?.setAttribute("data-view3d", "on");
|
|
4642
|
+
this.dismissConfirm();
|
|
4643
|
+
this.syncProjection();
|
|
4644
|
+
const overlay = document.createElement("div");
|
|
4645
|
+
overlay.className = "sl-view3d";
|
|
4646
|
+
overlay.setAttribute("role", "group");
|
|
4647
|
+
overlay.setAttribute("aria-label", "Interactive 3D venue view");
|
|
4648
|
+
const back = document.createElement("button");
|
|
4649
|
+
back.type = "button";
|
|
4650
|
+
back.className = "sl-view3d-back";
|
|
4651
|
+
back.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg><span>${this.tf("picker.backToMap", "Back to map")}</span>`;
|
|
4652
|
+
back.addEventListener("click", () => this.exit3d());
|
|
4653
|
+
overlay.appendChild(back);
|
|
4654
|
+
this.els.map.appendChild(overlay);
|
|
4655
|
+
this.view3dEl = overlay;
|
|
4656
|
+
requestAnimationFrame(() => {
|
|
4657
|
+
overlay.style.opacity = "1";
|
|
4658
|
+
});
|
|
4659
|
+
const gen = ++this.view3dGen;
|
|
4660
|
+
try {
|
|
4661
|
+
const seats = expandChart(doc);
|
|
4662
|
+
const mod = await loadVenue3d();
|
|
4663
|
+
if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
|
|
4664
|
+
const handle = mod.mountVenue3D(overlay, { doc, seats }, {
|
|
4665
|
+
onSeatPick: (id) => this.onView3dSeatPick(id),
|
|
4666
|
+
// Deferred off the tap gesture: generateSeatPanorama walks every seat
|
|
4667
|
+
// (O(n) on a 13k chart), and the module prefetches at pick — by the
|
|
4668
|
+
// time the flight lands (~2.5s) the idle render has long finished. A
|
|
4669
|
+
// null view REJECTS so the module's no-panorama path keeps the buyer
|
|
4670
|
+
// in orbit instead of dissolving into an empty overlay.
|
|
4671
|
+
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
4672
|
+
const run = () => {
|
|
4673
|
+
const view = this.seatViewFor3d(id);
|
|
4674
|
+
if (view) resolve(view);
|
|
4675
|
+
else reject(new Error("seat_view_unavailable"));
|
|
4676
|
+
};
|
|
4677
|
+
const ric = globalThis.requestIdleCallback;
|
|
4678
|
+
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
4679
|
+
else setTimeout(run, 50);
|
|
4680
|
+
}),
|
|
4681
|
+
onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
|
|
4682
|
+
});
|
|
4683
|
+
this.view3dHandle = handle;
|
|
4684
|
+
this.pushAvailabilityTo3d();
|
|
4685
|
+
this.syncSelectionTo3d();
|
|
4686
|
+
if (flySeatId) void handle.flyToSeat(flySeatId);
|
|
4687
|
+
} catch (err) {
|
|
4688
|
+
this.opts.onError?.(err);
|
|
4689
|
+
this.exit3d();
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
exit3d() {
|
|
4693
|
+
if (this.buyerView !== "venue3d" && !this.view3dEl) return;
|
|
4694
|
+
this.view3dGen++;
|
|
4695
|
+
this.buyerView = "map";
|
|
4696
|
+
this.root?.removeAttribute("data-view3d");
|
|
4697
|
+
try {
|
|
4698
|
+
this.view3dHandle?.dispose();
|
|
4699
|
+
} catch {
|
|
4700
|
+
}
|
|
4701
|
+
this.view3dHandle = null;
|
|
4702
|
+
const overlay = this.view3dEl;
|
|
4703
|
+
this.view3dEl = null;
|
|
4704
|
+
if (overlay) {
|
|
4705
|
+
overlay.style.opacity = "0";
|
|
4706
|
+
setTimeout(() => overlay.remove(), 320);
|
|
4707
|
+
}
|
|
4708
|
+
this.syncProjection();
|
|
4709
|
+
const seat = this.view3dReturnSeat;
|
|
4710
|
+
this.view3dReturnSeat = null;
|
|
4711
|
+
if (seat && this.committedSelection().some((s) => s.id === seat.id) && this.opts.confirmSelection !== false) {
|
|
4712
|
+
this.showConfirm(seat);
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4374
4715
|
/** Current active/restored hold reflected in the tray. */
|
|
4375
4716
|
getCurrentHold() {
|
|
4376
4717
|
return this.hold;
|
|
@@ -4462,6 +4803,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4462
4803
|
this.closeConfirm();
|
|
4463
4804
|
this.dismissTableDialog(false);
|
|
4464
4805
|
this.closeSeatView();
|
|
4806
|
+
this.exit3d();
|
|
4465
4807
|
this.stopHoldTimer();
|
|
4466
4808
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
4467
4809
|
if (this.liveTimer) clearTimeout(this.liveTimer);
|
|
@@ -6118,7 +6460,7 @@ var SeatManager = class {
|
|
|
6118
6460
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
6119
6461
|
const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
|
|
6120
6462
|
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
6121
|
-
const
|
|
6463
|
+
const location2 = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
|
|
6122
6464
|
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
6123
6465
|
this.els.rail.innerHTML = `
|
|
6124
6466
|
<p class="slm-eyebrow">${itemKind} details</p>
|
|
@@ -6128,7 +6470,7 @@ var SeatManager = class {
|
|
|
6128
6470
|
<div class="slm-inspect-grid">
|
|
6129
6471
|
<div><span>Status</span><b>${statusLabel[status]}</b></div>
|
|
6130
6472
|
<div><span>Section</span><b>${esc(sectionLabel)}</b></div>
|
|
6131
|
-
${
|
|
6473
|
+
${location2 ? `<div><span>${location2.label}</span><b>${esc(location2.value)}</b></div>` : ""}
|
|
6132
6474
|
<div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
|
|
6133
6475
|
<div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
6134
6476
|
<div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|