@seatlayer/core 0.25.0 → 0.27.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/{de-73TIXYJH.js → de-UHAOVDS3.js} +3 -2
- package/dist/de-UHAOVDS3.js.map +1 -0
- package/dist/{es-SKAODLTR.js → es-6TJS2L7S.js} +3 -2
- package/dist/es-6TJS2L7S.js.map +1 -0
- package/dist/{fr-J4637T6V.js → fr-MMSY7FPE.js} +3 -2
- package/dist/fr-MMSY7FPE.js.map +1 -0
- package/dist/index.cjs +2497 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +700 -56
- package/dist/index.d.ts +700 -56
- package/dist/index.js +2482 -334
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-73TIXYJH.js.map +0 -1
- package/dist/es-SKAODLTR.js.map +0 -1
- package/dist/fr-J4637T6V.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -48,6 +48,7 @@ var init_es = __esm({
|
|
|
48
48
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
49
49
|
"map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
|
|
50
50
|
"map.seatsLeft": "{count} LIBRES",
|
|
51
|
+
"map.soldOut": "AGOTADO",
|
|
51
52
|
"map.statusHeld": "En espera",
|
|
52
53
|
"map.statusTaken": "Ocupado",
|
|
53
54
|
"map.fromPrice": "DESDE {price}",
|
|
@@ -55,7 +56,7 @@ var init_es = __esm({
|
|
|
55
56
|
"picker.floor": "Piso",
|
|
56
57
|
"picker.zoomLevel": "Nivel de zoom",
|
|
57
58
|
"picker.rungTip.zones": "Vista general \u2014 grupos de secciones como Grada norte o VIP",
|
|
58
|
-
"picker.rungTip.sections": "Bloques de secci\xF3n \u2014
|
|
59
|
+
"picker.rungTip.sections": "Bloques de secci\xF3n \u2014 las secciones agotadas y casi llenas se aten\xFAan para ver la disponibilidad de un vistazo",
|
|
59
60
|
"picker.rungTip.seats": "Asientos individuales \u2014 los bloques se convierten en puntos",
|
|
60
61
|
"picker.rungLabel.zones": "ZONAS",
|
|
61
62
|
"picker.rungLabel.sections": "SECCIONES",
|
|
@@ -107,6 +108,7 @@ var init_de = __esm({
|
|
|
107
108
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
108
109
|
"map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
|
|
109
110
|
"map.seatsLeft": "{count} FREI",
|
|
111
|
+
"map.soldOut": "AUSVERKAUFT",
|
|
110
112
|
"map.statusHeld": "Reserviert",
|
|
111
113
|
"map.statusTaken": "Vergeben",
|
|
112
114
|
"map.fromPrice": "AB {price}",
|
|
@@ -114,7 +116,7 @@ var init_de = __esm({
|
|
|
114
116
|
"picker.floor": "Etage",
|
|
115
117
|
"picker.zoomLevel": "Zoomstufe",
|
|
116
118
|
"picker.rungTip.zones": "\xDCbersicht \u2014 Bereichsgruppen wie Nordtrib\xFCne oder VIP",
|
|
117
|
-
"picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014
|
|
119
|
+
"picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014 ausverkaufte und fast volle Bereiche werden abgedunkelt, um die Verf\xFCgbarkeit auf einen Blick zu zeigen",
|
|
118
120
|
"picker.rungTip.seats": "Einzelne Pl\xE4tze \u2014 Bl\xF6cke l\xF6sen sich zu Punkten auf",
|
|
119
121
|
"picker.rungLabel.zones": "ZONEN",
|
|
120
122
|
"picker.rungLabel.sections": "BEREICHE",
|
|
@@ -166,6 +168,7 @@ var init_fr = __esm({
|
|
|
166
168
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
167
169
|
"map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
|
|
168
170
|
"map.seatsLeft": "{count} LIBRES",
|
|
171
|
+
"map.soldOut": "COMPLET",
|
|
169
172
|
"map.statusHeld": "En attente",
|
|
170
173
|
"map.statusTaken": "Pris",
|
|
171
174
|
"map.fromPrice": "D\xC8S {price}",
|
|
@@ -173,7 +176,7 @@ var init_fr = __esm({
|
|
|
173
176
|
"picker.floor": "\xC9tage",
|
|
174
177
|
"picker.zoomLevel": "Niveau de zoom",
|
|
175
178
|
"picker.rungTip.zones": "Vue g\xE9n\xE9rale \u2014 groupes de sections comme Tribune nord ou VIP",
|
|
176
|
-
"picker.rungTip.sections": "Blocs de sections \u2014
|
|
179
|
+
"picker.rungTip.sections": "Blocs de sections \u2014 les sections compl\xE8tes ou presque pleines sont estomp\xE9es pour voir la disponibilit\xE9 en un coup d\u2019\u0153il",
|
|
177
180
|
"picker.rungTip.seats": "Si\xE8ges individuels \u2014 les blocs se transforment en points",
|
|
178
181
|
"picker.rungLabel.zones": "ZONES",
|
|
179
182
|
"picker.rungLabel.sections": "SECTIONS",
|
|
@@ -216,7 +219,9 @@ __export(index_exports, {
|
|
|
216
219
|
PickerController: () => PickerController,
|
|
217
220
|
RENDERED_QUALITY_REPORT_VERSION: () => RENDERED_QUALITY_REPORT_VERSION,
|
|
218
221
|
SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
|
|
222
|
+
SURROUNDINGS_SHAPE_ROLES: () => SURROUNDINGS_SHAPE_ROLES,
|
|
219
223
|
SeatmapRenderer: () => SeatmapRenderer,
|
|
224
|
+
TIER_HEIGHT_M: () => TIER_HEIGHT_M,
|
|
220
225
|
UNGROUPED_ID: () => UNGROUPED_ID,
|
|
221
226
|
accessibilityMeta: () => accessibilityMeta,
|
|
222
227
|
accessibilityRingColor: () => accessibilityRingColor,
|
|
@@ -231,16 +236,19 @@ __export(index_exports, {
|
|
|
231
236
|
expandRow: () => expandRow,
|
|
232
237
|
expandRowSlots: () => expandRowSlots,
|
|
233
238
|
expandTable: () => expandTable,
|
|
239
|
+
expandTableSlots: () => expandTableSlots,
|
|
234
240
|
floorObjects: () => floorObjects,
|
|
235
241
|
floorsOf: () => floorsOf,
|
|
236
242
|
formatDate: () => formatDate,
|
|
237
243
|
formatMoney: () => formatMoney,
|
|
238
244
|
gaAreasOf: () => gaAreasOf,
|
|
245
|
+
gaInventorySegments: () => gaInventorySegments,
|
|
239
246
|
gaUnitLabel: () => gaUnitLabel,
|
|
240
247
|
gaUnitLabels: () => gaUnitLabels,
|
|
241
248
|
generateSeatPanorama: () => generateSeatPanorama,
|
|
242
249
|
generateSeatThumb: () => generateSeatThumb,
|
|
243
250
|
getLocale: () => getLocale,
|
|
251
|
+
growJoinedGAInventory: () => growJoinedGAInventory,
|
|
244
252
|
hiddenObjectIds: () => hiddenObjectIds,
|
|
245
253
|
inspectRenderedQualityEvidence: () => inspectRenderedQualityEvidence,
|
|
246
254
|
isGaUnitLabel: () => isGaUnitLabel,
|
|
@@ -248,18 +256,24 @@ __export(index_exports, {
|
|
|
248
256
|
layerOf: () => layerOf,
|
|
249
257
|
loadLocale: () => loadLocale,
|
|
250
258
|
objectCenter: () => objectCenter,
|
|
259
|
+
owningSectionForObject: () => owningSectionForObject,
|
|
251
260
|
pointInPolygon: () => pointInPolygon,
|
|
252
261
|
pointInPolygonWithHoles: () => pointInPolygonWithHoles,
|
|
253
262
|
polygonLabelPoint: () => polygonLabelPoint,
|
|
254
263
|
resolveLocale: () => resolveLocale,
|
|
264
|
+
rowInventoryCount: () => rowInventoryCount,
|
|
255
265
|
rowSeatPositions: () => rowSeatPositions,
|
|
256
266
|
seatLabelPart: () => seatLabelPart,
|
|
267
|
+
sectionGeometry: () => sectionGeometry,
|
|
257
268
|
setLocale: () => setLocale,
|
|
258
269
|
setMoneyLocale: () => setMoneyLocale,
|
|
259
270
|
setStringOverrides: () => setStringOverrides,
|
|
260
271
|
stackFloors: () => stackFloors,
|
|
261
272
|
t: () => t,
|
|
262
|
-
tCount: () => tCount
|
|
273
|
+
tCount: () => tCount,
|
|
274
|
+
tableInventoryCount: () => tableInventoryCount,
|
|
275
|
+
tableSeatCountsBySide: () => tableSeatCountsBySide,
|
|
276
|
+
validGAInventorySegments: () => validGAInventorySegments
|
|
263
277
|
});
|
|
264
278
|
module.exports = __toCommonJS(index_exports);
|
|
265
279
|
|
|
@@ -269,6 +283,7 @@ var ACCESSIBILITY_TYPES = [
|
|
|
269
283
|
{ key: "companion", label: "Companion seat", short: "Companion", icon: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" },
|
|
270
284
|
{ key: "semi-ambulatory", label: "Semi-ambulatory (limited mobility)", short: "Limited mobility", icon: "\u{1F9AF}" },
|
|
271
285
|
{ key: "hearing", label: "Assistive listening", short: "Hearing", icon: "\u{1F9BB}" },
|
|
286
|
+
{ key: "cart", label: "CART live-caption view", short: "CART captions", icon: "CC" },
|
|
272
287
|
{ key: "sign-language", label: "Sign-language view", short: "Sign language", icon: "\u{1F91F}" },
|
|
273
288
|
{ key: "plus-size", label: "Plus-size seat", short: "Plus-size", icon: "\u{1F4BA}" },
|
|
274
289
|
{ key: "lift-armrest", label: "Lift-up armrest", short: "Lift armrest", icon: "\u2195\uFE0F" }
|
|
@@ -282,6 +297,7 @@ var ACCESSIBILITY_RING_COLOR = {
|
|
|
282
297
|
companion: "#8b5cf6",
|
|
283
298
|
"semi-ambulatory": "#0ea5e9",
|
|
284
299
|
hearing: "#14b8a6",
|
|
300
|
+
cart: "#7c3aed",
|
|
285
301
|
"sign-language": "#f59e0b",
|
|
286
302
|
"plus-size": "#ec4899",
|
|
287
303
|
"lift-armrest": "#22c55e"
|
|
@@ -292,6 +308,19 @@ function accessibilityRingColor(types) {
|
|
|
292
308
|
}
|
|
293
309
|
var LABEL_STYLE_MIN_SIZE = 8;
|
|
294
310
|
var LABEL_STYLE_MAX_SIZE = 24;
|
|
311
|
+
var SURROUNDINGS_SHAPE_ROLES = [
|
|
312
|
+
"reference-focal",
|
|
313
|
+
"bar",
|
|
314
|
+
"entrance",
|
|
315
|
+
"exit",
|
|
316
|
+
"restroom",
|
|
317
|
+
"screen",
|
|
318
|
+
"sound",
|
|
319
|
+
"concession",
|
|
320
|
+
"coat",
|
|
321
|
+
"wall"
|
|
322
|
+
];
|
|
323
|
+
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
295
324
|
function layerOf(obj) {
|
|
296
325
|
switch (obj.type) {
|
|
297
326
|
case "row":
|
|
@@ -300,10 +329,17 @@ function layerOf(obj) {
|
|
|
300
329
|
case "booth":
|
|
301
330
|
case "section":
|
|
302
331
|
return "interactive";
|
|
303
|
-
//
|
|
332
|
+
// Raster décor follows its explicit authored z-layer. Absence retains the
|
|
333
|
+
// legacy Background default; bitmap content is never guessed semantically.
|
|
334
|
+
case "decorImage":
|
|
335
|
+
return obj.layer === "foreground" ? "foreground" : "background";
|
|
336
|
+
// Source-backed focal geometry and curated venue landmarks help an author
|
|
337
|
+
// orient around the sellable plan. A stage and an ordinary authored shape
|
|
338
|
+
// remain Background even when they carry an arbitrary/custom role.
|
|
304
339
|
case "shape":
|
|
340
|
+
return obj.role && SURROUNDINGS_SHAPE_ROLE_SET.has(obj.role) ? "surroundings" : "background";
|
|
341
|
+
// Free text — including semantic icon text — is background furniture.
|
|
305
342
|
case "text":
|
|
306
|
-
case "decorImage":
|
|
307
343
|
return "background";
|
|
308
344
|
default:
|
|
309
345
|
return "interactive";
|
|
@@ -423,10 +459,57 @@ function toRoman(value) {
|
|
|
423
459
|
return out;
|
|
424
460
|
}
|
|
425
461
|
|
|
462
|
+
// src/core/units.ts
|
|
463
|
+
var METRES_PER_CHART_UNIT = 0.55 / 24;
|
|
464
|
+
var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
|
|
465
|
+
var LIFT_PER_STEP_WORLD = 58;
|
|
466
|
+
var TIER_HEIGHT_M = LIFT_PER_STEP_WORLD * METRES_PER_CHART_UNIT;
|
|
467
|
+
var SECTION_ELEVATION_TIER_MIN = 0;
|
|
468
|
+
var SECTION_ELEVATION_TIER_MAX = 3;
|
|
469
|
+
var SECTION_HEIGHT_MIN_M = 0;
|
|
470
|
+
var SECTION_HEIGHT_MAX_M = 120;
|
|
471
|
+
var SECTION_RAKE_MIN_DEG = 0;
|
|
472
|
+
var SECTION_RAKE_MAX_DEG = 45;
|
|
473
|
+
var LEGACY_SECTION_ELEVATION_TIER_MAX = 7;
|
|
474
|
+
var SEATED_EYE_HEIGHT_M = 1.2;
|
|
475
|
+
function finiteClamped(value, min, max, fallback) {
|
|
476
|
+
return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
477
|
+
}
|
|
478
|
+
function sectionElevationTier(value) {
|
|
479
|
+
if (!Number.isFinite(value)) return SECTION_ELEVATION_TIER_MIN;
|
|
480
|
+
return Math.max(
|
|
481
|
+
SECTION_ELEVATION_TIER_MIN,
|
|
482
|
+
Math.min(SECTION_ELEVATION_TIER_MAX, Math.round(value))
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function compatibleAutomaticTier(value) {
|
|
486
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) return 0;
|
|
487
|
+
if (value <= LEGACY_SECTION_ELEVATION_TIER_MAX) return value;
|
|
488
|
+
return SECTION_ELEVATION_TIER_MAX;
|
|
489
|
+
}
|
|
490
|
+
function sectionGeometry(section, context = {}) {
|
|
491
|
+
const floorBaseHeightM = finiteClamped(
|
|
492
|
+
context.floorBaseHeightM,
|
|
493
|
+
SECTION_HEIGHT_MIN_M,
|
|
494
|
+
SECTION_HEIGHT_MAX_M,
|
|
495
|
+
0
|
|
496
|
+
);
|
|
497
|
+
const automaticHeight = Math.min(
|
|
498
|
+
SECTION_HEIGHT_MAX_M,
|
|
499
|
+
floorBaseHeightM + compatibleAutomaticTier(section.elevation) * TIER_HEIGHT_M
|
|
500
|
+
);
|
|
501
|
+
const height = section.height === void 0 ? automaticHeight : finiteClamped(section.height, SECTION_HEIGHT_MIN_M, SECTION_HEIGHT_MAX_M, automaticHeight);
|
|
502
|
+
const rake = finiteClamped(section.rake, SECTION_RAKE_MIN_DEG, SECTION_RAKE_MAX_DEG, 0);
|
|
503
|
+
return { height, rake };
|
|
504
|
+
}
|
|
505
|
+
|
|
426
506
|
// src/core/layout.ts
|
|
427
507
|
function overrideAccessibility(o) {
|
|
428
508
|
if (!o) return [];
|
|
429
|
-
if (o.accessibility && o.accessibility.length)
|
|
509
|
+
if (o.accessibility && o.accessibility.length) {
|
|
510
|
+
return o.wheelchairSpaceType && !o.accessibility.includes("wheelchair") ? ["wheelchair", ...o.accessibility] : o.accessibility;
|
|
511
|
+
}
|
|
512
|
+
if (o.wheelchairSpaceType) return ["wheelchair"];
|
|
430
513
|
return o.accessible ? ["wheelchair"] : [];
|
|
431
514
|
}
|
|
432
515
|
var DEG = Math.PI / 180;
|
|
@@ -468,6 +551,10 @@ function overrideMap(row) {
|
|
|
468
551
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
469
552
|
return m;
|
|
470
553
|
}
|
|
554
|
+
function rowInventoryCount(row) {
|
|
555
|
+
const skipped = new Set((row.overrides ?? []).filter((override) => override.skip && Number.isInteger(override.index) && override.index >= 0 && override.index < row.seatCount).map((override) => override.index));
|
|
556
|
+
return Math.max(0, row.seatCount - skipped.size);
|
|
557
|
+
}
|
|
471
558
|
function centerRank(n) {
|
|
472
559
|
const rank = new Array(n);
|
|
473
560
|
Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
|
|
@@ -481,9 +568,9 @@ function seatLabelPart(row, i) {
|
|
|
481
568
|
const prefix = row.seatNumbering?.prefix ?? "";
|
|
482
569
|
const endAt = row.seatNumbering?.endAt;
|
|
483
570
|
const n = row.seatCount;
|
|
484
|
-
if (scheme === "updown") {
|
|
571
|
+
if (scheme === "updown" || scheme === "updown-descending") {
|
|
485
572
|
const half = Math.ceil(n / 2);
|
|
486
|
-
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
573
|
+
const core2 = scheme === "updown" ? i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i) : i < half ? rawStart + 2 * (half - 1 - i) : rawStart + 1 + 2 * (i - half);
|
|
487
574
|
return `${prefix}${core2}`;
|
|
488
575
|
}
|
|
489
576
|
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
@@ -536,6 +623,7 @@ function expandRowSlots(row) {
|
|
|
536
623
|
skipped: !!o?.skip,
|
|
537
624
|
accessible: accessibility.length > 0,
|
|
538
625
|
accessibility,
|
|
626
|
+
wheelchairSpaceType: o?.wheelchairSpaceType,
|
|
539
627
|
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
540
628
|
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl,
|
|
541
629
|
labelStyle: o?.labelStyle
|
|
@@ -556,6 +644,7 @@ function expandRow(row) {
|
|
|
556
644
|
categoryKey: slot.categoryKey,
|
|
557
645
|
accessible: slot.accessible || void 0,
|
|
558
646
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
647
|
+
wheelchairSpaceType: slot.wheelchairSpaceType,
|
|
559
648
|
commercial: slot.commercial,
|
|
560
649
|
viewUrl: slot.viewUrl,
|
|
561
650
|
labelStyle: slot.labelStyle
|
|
@@ -563,18 +652,43 @@ function expandRow(row) {
|
|
|
563
652
|
}
|
|
564
653
|
return seats;
|
|
565
654
|
}
|
|
566
|
-
function
|
|
655
|
+
function tableSeatCountsBySide(t2) {
|
|
656
|
+
if (t2.seatCountsBySide) return { ...t2.seatCountsBySide };
|
|
657
|
+
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
658
|
+
const order = ["top", "bottom", "left", "right"].filter((side) => enabled.includes(side));
|
|
659
|
+
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
660
|
+
if (!order.length) return counts;
|
|
661
|
+
const n = Math.max(0, Math.round(t2.seatCount));
|
|
662
|
+
for (let index = 0; index < n; index++) counts[order[index % order.length]] += 1;
|
|
663
|
+
return counts;
|
|
664
|
+
}
|
|
665
|
+
function expandTableSlots(t2) {
|
|
567
666
|
const seats = [];
|
|
568
667
|
const n = Math.max(0, Math.round(t2.seatCount));
|
|
569
668
|
if (n === 0) return seats;
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
669
|
+
const overrides2 = new Map((t2.overrides ?? []).map((override) => [override.index, override]));
|
|
670
|
+
const mk = (index, x, y, side) => {
|
|
671
|
+
const override = overrides2.get(index);
|
|
672
|
+
const accessibility = overrideAccessibility(override);
|
|
673
|
+
const label = override?.label ?? `${t2.label}-${index + 1}`;
|
|
674
|
+
const displayPrefix = t2.displayLabel ?? t2.label;
|
|
675
|
+
return {
|
|
676
|
+
index,
|
|
677
|
+
label,
|
|
678
|
+
displayLabel: override?.displayLabel ?? `${displayPrefix}-${index + 1}`,
|
|
679
|
+
x: x + (override?.dx ?? 0),
|
|
680
|
+
y: y + (override?.dy ?? 0),
|
|
681
|
+
categoryKey: override?.categoryKey ?? t2.categoryKey,
|
|
682
|
+
skipped: !!override?.skip,
|
|
683
|
+
accessible: accessibility.length > 0,
|
|
684
|
+
accessibility,
|
|
685
|
+
wheelchairSpaceType: override?.wheelchairSpaceType,
|
|
686
|
+
commercial: override?.commercial,
|
|
687
|
+
viewUrl: override?.viewFromSeatUrl,
|
|
688
|
+
labelStyle: override?.labelStyle,
|
|
689
|
+
...side ? { side } : {}
|
|
690
|
+
};
|
|
691
|
+
};
|
|
578
692
|
if (t2.shape === "round") {
|
|
579
693
|
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
580
694
|
const base = t2.rotation * DEG;
|
|
@@ -597,17 +711,11 @@ function expandTable(t2) {
|
|
|
597
711
|
}
|
|
598
712
|
const w = t2.width ?? 80;
|
|
599
713
|
const h = t2.height ?? 50;
|
|
600
|
-
const
|
|
601
|
-
const order = ["top", "bottom", "left", "right"]
|
|
602
|
-
if (!order.length) return seats;
|
|
603
|
-
const counts = new Map(order.map((s) => [s, 0]));
|
|
604
|
-
for (let i = 0; i < n; i++) {
|
|
605
|
-
const s = order[i % order.length];
|
|
606
|
-
counts.set(s, counts.get(s) + 1);
|
|
607
|
-
}
|
|
714
|
+
const counts = tableSeatCountsBySide(t2);
|
|
715
|
+
const order = ["top", "bottom", "left", "right"];
|
|
608
716
|
let idx = 0;
|
|
609
717
|
for (const side of order) {
|
|
610
|
-
const count = counts
|
|
718
|
+
const count = counts[side];
|
|
611
719
|
for (let j = 0; j < count; j++) {
|
|
612
720
|
let localX;
|
|
613
721
|
let localY;
|
|
@@ -624,17 +732,39 @@ function expandTable(t2) {
|
|
|
624
732
|
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
625
733
|
localY = -h / 2 + (j + 0.5) * h / count;
|
|
626
734
|
}
|
|
627
|
-
const
|
|
628
|
-
seats.push(mk(idx++,
|
|
735
|
+
const point = place(localX, localY, t2.rotation, t2.center);
|
|
736
|
+
seats.push(mk(idx++, point.x, point.y, side));
|
|
629
737
|
}
|
|
630
738
|
}
|
|
631
739
|
return seats;
|
|
632
740
|
}
|
|
741
|
+
function tableInventoryCount(t2) {
|
|
742
|
+
if (t2.bookAsWhole || t2.variableOccupancy) return Math.max(0, Math.round(t2.seatCount));
|
|
743
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).length;
|
|
744
|
+
}
|
|
745
|
+
function expandTable(t2) {
|
|
746
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).map((slot) => ({
|
|
747
|
+
id: `${t2.id}:${slot.index}`,
|
|
748
|
+
label: slot.label,
|
|
749
|
+
...slot.displayLabel !== slot.label ? { displayLabel: slot.displayLabel } : {},
|
|
750
|
+
x: slot.x,
|
|
751
|
+
y: slot.y,
|
|
752
|
+
rowId: t2.id,
|
|
753
|
+
categoryKey: slot.categoryKey,
|
|
754
|
+
...slot.accessible ? { accessible: true } : {},
|
|
755
|
+
...slot.accessibility.length ? { accessibility: slot.accessibility } : {},
|
|
756
|
+
...slot.wheelchairSpaceType ? { wheelchairSpaceType: slot.wheelchairSpaceType } : {},
|
|
757
|
+
...slot.commercial ? { commercial: slot.commercial } : {},
|
|
758
|
+
...slot.viewUrl ? { viewUrl: slot.viewUrl } : {},
|
|
759
|
+
...slot.labelStyle ? { labelStyle: slot.labelStyle } : {}
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
633
762
|
function expandBooth(b) {
|
|
634
763
|
return [
|
|
635
764
|
{
|
|
636
765
|
id: `${b.id}:0`,
|
|
637
766
|
label: b.label,
|
|
767
|
+
...b.displayLabel && b.displayLabel !== b.label ? { displayLabel: b.displayLabel } : {},
|
|
638
768
|
x: b.center.x,
|
|
639
769
|
y: b.center.y,
|
|
640
770
|
rowId: b.id,
|
|
@@ -672,7 +802,7 @@ function polygonLabelPoint(outer, holes) {
|
|
|
672
802
|
if (!outer.length) return { x: 0, y: 0 };
|
|
673
803
|
const xs = outer.map((point) => point.x);
|
|
674
804
|
const ys = outer.map((point) => point.y);
|
|
675
|
-
const
|
|
805
|
+
const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
676
806
|
const centroid2 = polygonCentroid(outer);
|
|
677
807
|
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
678
808
|
let best = outer[0];
|
|
@@ -681,8 +811,8 @@ function polygonLabelPoint(outer, holes) {
|
|
|
681
811
|
for (let row = 1; row < 24; row += 1) {
|
|
682
812
|
for (let column = 1; column < 24; column += 1) {
|
|
683
813
|
const point = {
|
|
684
|
-
x:
|
|
685
|
-
y:
|
|
814
|
+
x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
|
|
815
|
+
y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
|
|
686
816
|
};
|
|
687
817
|
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
688
818
|
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
@@ -744,9 +874,32 @@ function objectCenter(o) {
|
|
|
744
874
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
745
875
|
}
|
|
746
876
|
}
|
|
877
|
+
function samePoints(left, right) {
|
|
878
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
879
|
+
}
|
|
880
|
+
function sameGASurfaceAsSection(object, section) {
|
|
881
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
882
|
+
const objectHoles = object.holes ?? [];
|
|
883
|
+
const sectionHoles = section.holes ?? [];
|
|
884
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
885
|
+
}
|
|
886
|
+
function owningSectionForObject(objects, object) {
|
|
887
|
+
const sections = objects.filter((candidate) => candidate.type === "section");
|
|
888
|
+
const referencedLogicalId = "referenceInventorySource" in object ? object.referenceInventorySource?.logicalSectionId : void 0;
|
|
889
|
+
const center = objectCenter(object);
|
|
890
|
+
const referencedOwner = referencedLogicalId ? sections.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(object, section) || pointInPolygonWithHoles(center, section.outline, section.holes))) : void 0;
|
|
891
|
+
return referencedOwner ?? sections.find((section) => pointInPolygonWithHoles(center, section.outline, section.holes));
|
|
892
|
+
}
|
|
747
893
|
function floorsOf(doc) {
|
|
748
894
|
if (doc.floors && doc.floors.length) return doc.floors;
|
|
749
|
-
return [{
|
|
895
|
+
return [{
|
|
896
|
+
id: "floor-0",
|
|
897
|
+
name: "Main",
|
|
898
|
+
objects: doc.objects,
|
|
899
|
+
focalPoint: doc.focalPoint,
|
|
900
|
+
referenceImage: doc.referenceImage,
|
|
901
|
+
backgroundImage: doc.backgroundImage
|
|
902
|
+
}];
|
|
750
903
|
}
|
|
751
904
|
function floorObjects(doc, floorId) {
|
|
752
905
|
const floors = floorsOf(doc);
|
|
@@ -795,15 +948,138 @@ function stackFloors(doc, spread = 900) {
|
|
|
795
948
|
});
|
|
796
949
|
return { ...doc, objects, floors: void 0 };
|
|
797
950
|
}
|
|
798
|
-
function
|
|
951
|
+
function expandFloorObjects(objects, zones, fallbackFocal) {
|
|
799
952
|
const out = [];
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
953
|
+
const segmented = /* @__PURE__ */ new Map();
|
|
954
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const object of objects) {
|
|
956
|
+
if (object.type !== "row" || !object.segmentedRow) continue;
|
|
957
|
+
const list = grouped.get(object.segmentedRow.groupId) ?? [];
|
|
958
|
+
list.push(object);
|
|
959
|
+
grouped.set(object.segmentedRow.groupId, list);
|
|
960
|
+
}
|
|
961
|
+
for (const [groupId, members] of grouped) {
|
|
962
|
+
const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
|
|
963
|
+
const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
|
|
964
|
+
const first = ordered[0]?.segmentedRow;
|
|
965
|
+
if (!first) continue;
|
|
966
|
+
const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
|
|
967
|
+
if (!valid) continue;
|
|
968
|
+
const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
|
|
969
|
+
let adjacencyOffset = 0;
|
|
970
|
+
let displayOffset = 0;
|
|
971
|
+
for (const row of ordered) {
|
|
972
|
+
if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
|
|
973
|
+
segmented.set(row.id, {
|
|
974
|
+
groupId,
|
|
975
|
+
adjacencyOffset,
|
|
976
|
+
displayOffset,
|
|
977
|
+
displayLabel: first.displayLabel,
|
|
978
|
+
totalSeats,
|
|
979
|
+
canonical: ordered[0],
|
|
980
|
+
viewFromSeatUrl: first.viewFromSeatUrl
|
|
981
|
+
});
|
|
982
|
+
adjacencyOffset += row.seatCount;
|
|
983
|
+
displayOffset += row.seatCount;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
for (const obj of objects) {
|
|
987
|
+
let seats = [];
|
|
988
|
+
if (obj.type === "row") seats = expandRow(obj);
|
|
989
|
+
else if (obj.type === "table") seats = expandTable(obj);
|
|
990
|
+
else if (obj.type === "booth") seats = expandBooth(obj);
|
|
991
|
+
if (!seats.length) continue;
|
|
992
|
+
if (obj.type === "row") {
|
|
993
|
+
const logical = segmented.get(obj.id);
|
|
994
|
+
if (logical) {
|
|
995
|
+
const overrides2 = new Map((obj.overrides ?? []).map((override) => [override.index, override]));
|
|
996
|
+
for (const seat of seats) {
|
|
997
|
+
const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
|
|
998
|
+
if (!Number.isInteger(physicalIndex)) continue;
|
|
999
|
+
const displayOrdinal = logical.displayOffset + physicalIndex;
|
|
1000
|
+
seat.logicalRowId = logical.groupId;
|
|
1001
|
+
seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
|
|
1002
|
+
if (!overrides2.get(physicalIndex)?.displayLabel) {
|
|
1003
|
+
const numberingRow = {
|
|
1004
|
+
...logical.canonical,
|
|
1005
|
+
seatCount: logical.totalSeats,
|
|
1006
|
+
label: logical.displayLabel,
|
|
1007
|
+
displayLabel: logical.displayLabel
|
|
1008
|
+
};
|
|
1009
|
+
seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
|
|
1010
|
+
}
|
|
1011
|
+
seat.viewUrl ??= logical.viewFromSeatUrl;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const owner = owningSectionForObject(objects, obj);
|
|
1016
|
+
const inheritedView = owner?.viewFromSeatUrl;
|
|
1017
|
+
const zone = owner?.zone ? zones?.find((candidate) => candidate.id === owner.zone) : void 0;
|
|
1018
|
+
const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
|
|
1019
|
+
for (const seat of seats) {
|
|
1020
|
+
if (inheritedView) seat.viewUrl ??= inheritedView;
|
|
1021
|
+
if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
|
|
1022
|
+
if (owner?.zone) seat.zoneId = owner.zone;
|
|
1023
|
+
if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
|
|
1024
|
+
}
|
|
1025
|
+
out.push(...seats);
|
|
804
1026
|
}
|
|
805
1027
|
return out;
|
|
806
1028
|
}
|
|
1029
|
+
function expandChart(doc, options = {}) {
|
|
1030
|
+
if (doc.floors?.length) {
|
|
1031
|
+
const out2 = [];
|
|
1032
|
+
for (const floor of doc.floors) {
|
|
1033
|
+
const floorFocal = floor.focalPoint ?? doc.focalPoint;
|
|
1034
|
+
const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
|
|
1035
|
+
assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
|
|
1036
|
+
out2.push(...seats);
|
|
1037
|
+
}
|
|
1038
|
+
return out2;
|
|
1039
|
+
}
|
|
1040
|
+
const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
|
|
1041
|
+
assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
|
|
1045
|
+
const sections = objects.filter((o) => o.type === "section");
|
|
1046
|
+
const hasGeometry = floorBaseHeightM > 0 || sections.some((s) => s.height !== void 0 || s.rake !== void 0 || (s.elevation ?? 0) > 0);
|
|
1047
|
+
if (!sections.length || !hasGeometry || !focal) {
|
|
1048
|
+
for (const seat of seats) seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const owner = new Array(seats.length);
|
|
1052
|
+
const geo = /* @__PURE__ */ new Map();
|
|
1053
|
+
const frontDistU = /* @__PURE__ */ new Map();
|
|
1054
|
+
for (let i = 0; i < seats.length; i++) {
|
|
1055
|
+
const seat = seats[i];
|
|
1056
|
+
const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
|
|
1057
|
+
owner[i] = sec;
|
|
1058
|
+
if (!sec) continue;
|
|
1059
|
+
if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
|
|
1060
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
1061
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
1062
|
+
const cur = frontDistU.get(sec.id);
|
|
1063
|
+
if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
|
|
1064
|
+
}
|
|
1065
|
+
for (let i = 0; i < seats.length; i++) {
|
|
1066
|
+
const seat = seats[i];
|
|
1067
|
+
const sec = owner[i];
|
|
1068
|
+
if (!sec) {
|
|
1069
|
+
seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const g = geo.get(sec.id);
|
|
1073
|
+
let riseM = 0;
|
|
1074
|
+
if (g.rake > 0) {
|
|
1075
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
1076
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
1077
|
+
const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
|
|
1078
|
+
riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
|
|
1079
|
+
}
|
|
1080
|
+
seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
807
1083
|
var PAD = 40;
|
|
808
1084
|
function chartBounds(doc) {
|
|
809
1085
|
let minX = Infinity;
|
|
@@ -847,8 +1123,9 @@ function chartBounds(doc) {
|
|
|
847
1123
|
acc(obj.position.x + w, obj.position.y + obj.fontSize);
|
|
848
1124
|
}
|
|
849
1125
|
}
|
|
850
|
-
|
|
851
|
-
|
|
1126
|
+
for (const image of [doc.referenceImage, doc.backgroundImage]) {
|
|
1127
|
+
if (!image) continue;
|
|
1128
|
+
const { center, width } = image;
|
|
852
1129
|
const bh = width * 3 / 4;
|
|
853
1130
|
acc(center.x - width / 2, center.y - bh / 2);
|
|
854
1131
|
acc(center.x + width / 2, center.y + bh / 2);
|
|
@@ -872,9 +1149,55 @@ var MAX_EVENT_INVENTORY = 1e5;
|
|
|
872
1149
|
function gaUnitLabel(areaId, index) {
|
|
873
1150
|
return `${PREFIX}${encodeURIComponent(areaId)}__${index + 1}`;
|
|
874
1151
|
}
|
|
875
|
-
function
|
|
1152
|
+
function validGAInventorySegments(area) {
|
|
1153
|
+
const segments = area.inventorySegments;
|
|
1154
|
+
if (segments === void 0) return true;
|
|
1155
|
+
if (!Array.isArray(segments) || segments.length < 1 || segments.length > 256) return false;
|
|
1156
|
+
let total = 0;
|
|
1157
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1158
|
+
for (const segment of segments) {
|
|
1159
|
+
if (!segment || typeof segment.sourceAreaId !== "string" || !segment.sourceAreaId.trim() || segment.sourceAreaId.length > 160 || !Number.isInteger(segment.startIndex) || segment.startIndex < 0 || !Number.isInteger(segment.count) || segment.count < 1 || segment.startIndex + segment.count > MAX_GA_CAPACITY) return false;
|
|
1160
|
+
total += segment.count;
|
|
1161
|
+
if (total > MAX_GA_CAPACITY) return false;
|
|
1162
|
+
const sourceRanges = ranges.get(segment.sourceAreaId) ?? [];
|
|
1163
|
+
const end = segment.startIndex + segment.count;
|
|
1164
|
+
if (sourceRanges.some((range) => segment.startIndex < range.end && end > range.start)) return false;
|
|
1165
|
+
sourceRanges.push({ start: segment.startIndex, end });
|
|
1166
|
+
ranges.set(segment.sourceAreaId, sourceRanges);
|
|
1167
|
+
}
|
|
1168
|
+
return total === area.capacity;
|
|
1169
|
+
}
|
|
1170
|
+
function gaInventorySegments(area) {
|
|
876
1171
|
const capacity = Math.min(MAX_GA_CAPACITY, Math.max(0, Math.floor(area.capacity)));
|
|
877
|
-
|
|
1172
|
+
if (area.inventorySegments === void 0) {
|
|
1173
|
+
return capacity ? [{ sourceAreaId: area.id, startIndex: 0, count: capacity }] : [];
|
|
1174
|
+
}
|
|
1175
|
+
if (!validGAInventorySegments(area)) return [];
|
|
1176
|
+
return area.inventorySegments.map((segment) => ({ ...segment }));
|
|
1177
|
+
}
|
|
1178
|
+
function gaUnitLabels(area) {
|
|
1179
|
+
return gaInventorySegments(area).flatMap((segment) => Array.from(
|
|
1180
|
+
{ length: segment.count },
|
|
1181
|
+
(_, offset) => gaUnitLabel(segment.sourceAreaId, segment.startIndex + offset)
|
|
1182
|
+
));
|
|
1183
|
+
}
|
|
1184
|
+
function growJoinedGAInventory(area, nextCapacity) {
|
|
1185
|
+
if (area.inventorySegments === void 0 || nextCapacity === area.capacity) return area.inventorySegments;
|
|
1186
|
+
if (nextCapacity < area.capacity) return void 0;
|
|
1187
|
+
const segments = gaInventorySegments(area);
|
|
1188
|
+
if (!segments.length) return void 0;
|
|
1189
|
+
const extra = nextCapacity - area.capacity;
|
|
1190
|
+
if (!extra) return segments;
|
|
1191
|
+
const ownRanges = segments.filter((segment) => segment.sourceAreaId === area.id);
|
|
1192
|
+
const startIndex = ownRanges.reduce((max, segment) => Math.max(max, segment.startIndex + segment.count), 0);
|
|
1193
|
+
const last = segments[segments.length - 1];
|
|
1194
|
+
if (last?.sourceAreaId === area.id && last.startIndex + last.count === startIndex) {
|
|
1195
|
+
last.count += extra;
|
|
1196
|
+
} else {
|
|
1197
|
+
if (segments.length >= 256) return void 0;
|
|
1198
|
+
segments.push({ sourceAreaId: area.id, startIndex, count: extra });
|
|
1199
|
+
}
|
|
1200
|
+
return segments;
|
|
878
1201
|
}
|
|
879
1202
|
function gaAreasOf(doc) {
|
|
880
1203
|
return allObjects(doc).filter((o) => o.type === "gaArea");
|
|
@@ -895,15 +1218,6 @@ function objectSeatLabels(o) {
|
|
|
895
1218
|
function isSeatObject(o) {
|
|
896
1219
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
897
1220
|
}
|
|
898
|
-
function samePoints(left, right) {
|
|
899
|
-
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
900
|
-
}
|
|
901
|
-
function sameGASurfaceAsSection(object, section) {
|
|
902
|
-
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
903
|
-
const objectHoles = object.holes ?? [];
|
|
904
|
-
const sectionHoles = section.holes ?? [];
|
|
905
|
-
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
906
|
-
}
|
|
907
1221
|
function computeSections(doc) {
|
|
908
1222
|
const objs = allObjects(doc);
|
|
909
1223
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
@@ -929,10 +1243,7 @@ function computeSections(doc) {
|
|
|
929
1243
|
if (!isSeatObject(obj)) continue;
|
|
930
1244
|
const labels = objectSeatLabels(obj);
|
|
931
1245
|
if (labels.length === 0) continue;
|
|
932
|
-
const
|
|
933
|
-
const c = objectCenter(obj);
|
|
934
|
-
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
935
|
-
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
1246
|
+
const owner = owningSectionForObject(objs, obj);
|
|
936
1247
|
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
937
1248
|
node.seatCount += labels.length;
|
|
938
1249
|
node.objectIds.push(obj.id);
|
|
@@ -1503,11 +1814,473 @@ var import_Circle = require("konva/lib/shapes/Circle");
|
|
|
1503
1814
|
var import_Rect = require("konva/lib/shapes/Rect");
|
|
1504
1815
|
var import_Ellipse = require("konva/lib/shapes/Ellipse");
|
|
1505
1816
|
var import_Line = require("konva/lib/shapes/Line");
|
|
1817
|
+
var import_Arrow = require("konva/lib/shapes/Arrow");
|
|
1506
1818
|
var import_Text = require("konva/lib/shapes/Text");
|
|
1507
1819
|
var import_Path = require("konva/lib/shapes/Path");
|
|
1508
1820
|
var import_Image = require("konva/lib/shapes/Image");
|
|
1509
1821
|
var import_Shape = require("konva/lib/Shape");
|
|
1510
1822
|
|
|
1823
|
+
// src/core/chartBackgrounds.ts
|
|
1824
|
+
function buyerBackgroundImage(owner) {
|
|
1825
|
+
const background = owner.backgroundImage;
|
|
1826
|
+
return background?.url && !background.assetId ? background : void 0;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/core/icons.ts
|
|
1830
|
+
var VENUE_ICON_VIEWBOX = 24;
|
|
1831
|
+
var VENUE_ICON_STROKE = 2;
|
|
1832
|
+
var WHEELCHAIR = "M13 4.6a1.6 1.6 0 1 0-3.2 0 1.6 1.6 0 0 0 3.2 0 M11 6.5V13h5l2.4 6 M15 16.4A5 5 0 1 1 9 11.3";
|
|
1833
|
+
var VENUE_ICONS = [
|
|
1834
|
+
// ---- Facilities --------------------------------------------------------
|
|
1835
|
+
{
|
|
1836
|
+
key: "restroom-men",
|
|
1837
|
+
label: "Men's restroom",
|
|
1838
|
+
group: "facilities",
|
|
1839
|
+
path: "M16 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0 M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
key: "restroom-women",
|
|
1843
|
+
label: "Women's restroom",
|
|
1844
|
+
group: "facilities",
|
|
1845
|
+
path: "M15 5a3 3 0 1 0-6 0 3 3 0 0 0 6 0 M12 8l-4 10h8l-4-10 M10 18v4 M14 18v4"
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
key: "restroom-accessible",
|
|
1849
|
+
label: "Accessible restroom",
|
|
1850
|
+
group: "facilities",
|
|
1851
|
+
path: WHEELCHAIR
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
key: "first-aid",
|
|
1855
|
+
label: "First aid",
|
|
1856
|
+
group: "facilities",
|
|
1857
|
+
path: "M5 4h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M12 8v8 M8 12h8"
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
key: "coat-check",
|
|
1861
|
+
label: "Coat check",
|
|
1862
|
+
group: "facilities",
|
|
1863
|
+
path: "M12 6a2 2 0 0 1 0-4 2 2 0 0 1 1.6 3.2L21 13H3l8.4-7.8 M3 13h18"
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
key: "atm",
|
|
1867
|
+
label: "ATM / cash",
|
|
1868
|
+
group: "facilities",
|
|
1869
|
+
path: "M2 6h20v12H2V6Z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M6 9h.01 M18 15h.01"
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
key: "info",
|
|
1873
|
+
label: "Info point",
|
|
1874
|
+
group: "facilities",
|
|
1875
|
+
path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M12 11v5 M12 8h.01"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
key: "lost-found",
|
|
1879
|
+
label: "Lost & found",
|
|
1880
|
+
group: "facilities",
|
|
1881
|
+
path: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14 M16 16l5 5 M9 8.5a2 2 0 1 1 3 1.7c-.8.5-1 .8-1 1.8 M11 15h.01"
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
key: "charging",
|
|
1885
|
+
label: "Charging point",
|
|
1886
|
+
group: "facilities",
|
|
1887
|
+
path: "M3 8h13v8H3V8Z M16 10h3v4h-3 M9.6 9l-2 3.5h2.5l-1.5 3"
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
key: "smoking",
|
|
1891
|
+
label: "Smoking area",
|
|
1892
|
+
group: "facilities",
|
|
1893
|
+
path: "M2 15h13v3H2v-3Z M18 16h2 M19 8c0 1 1 1.5 1 2.5S19 12 19 13"
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
key: "no-smoking",
|
|
1897
|
+
label: "No smoking",
|
|
1898
|
+
group: "facilities",
|
|
1899
|
+
path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M5.6 5.6l12.8 12.8 M7 13h6v2H7v-2Z"
|
|
1900
|
+
},
|
|
1901
|
+
// ---- Food & drink ------------------------------------------------------
|
|
1902
|
+
{
|
|
1903
|
+
key: "food",
|
|
1904
|
+
label: "Food",
|
|
1905
|
+
group: "food-drink",
|
|
1906
|
+
path: "M7 3v6a2 2 0 0 0 4 0V3 M9 11v10 M17 3c-1.6 1-2.2 3-2.2 6 0 2 .9 3.2 2.2 3.6V21"
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
key: "bar",
|
|
1910
|
+
label: "Bar",
|
|
1911
|
+
group: "food-drink",
|
|
1912
|
+
path: "M4 5h16l-8 8-8-8Z M12 13v6 M8 20h8"
|
|
1913
|
+
},
|
|
1914
|
+
{
|
|
1915
|
+
key: "coffee",
|
|
1916
|
+
label: "Caf\xE9 / coffee",
|
|
1917
|
+
group: "food-drink",
|
|
1918
|
+
path: "M4 8h13v4a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V8Z M17 9h2a2 2 0 0 1 0 4h-2 M8 2v2 M11 2v2 M4 21h14"
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
key: "water",
|
|
1922
|
+
label: "Water / drinks",
|
|
1923
|
+
group: "food-drink",
|
|
1924
|
+
path: "M12 22a7 7 0 0 1-7-7c0-5 7-12 7-12s7 7 7 12a7 7 0 0 1-7 7Z"
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
key: "merch",
|
|
1928
|
+
label: "Merch / shop",
|
|
1929
|
+
group: "food-drink",
|
|
1930
|
+
path: "M6 8h12l-1 12H7L6 8Z M9 8V6a3 3 0 0 1 6 0v2"
|
|
1931
|
+
},
|
|
1932
|
+
// ---- Getting around ----------------------------------------------------
|
|
1933
|
+
{
|
|
1934
|
+
key: "entrance",
|
|
1935
|
+
label: "Entrance",
|
|
1936
|
+
group: "navigation",
|
|
1937
|
+
path: "M14 3h5a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-5 M3 12h11 M10 8l4 4-4 4"
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
key: "exit",
|
|
1941
|
+
label: "Exit",
|
|
1942
|
+
group: "navigation",
|
|
1943
|
+
path: "M10 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h5 M14 12h7 M17 8l4 4-4 4"
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
key: "emergency-exit",
|
|
1947
|
+
label: "Emergency exit",
|
|
1948
|
+
group: "navigation",
|
|
1949
|
+
path: "M14 4.6a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0 M12.5 8l-3.5 2 1.6 3.6-2 4.4 M12.5 10l3.5 1.5 M9 10.5l-3.5 1 M15 12h6 M18 9l3 3-3 3"
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
key: "stairs",
|
|
1953
|
+
label: "Stairs",
|
|
1954
|
+
group: "navigation",
|
|
1955
|
+
path: "M3 20v-4h4v-4h4v-4h4V4h5"
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
key: "elevator",
|
|
1959
|
+
label: "Elevator",
|
|
1960
|
+
group: "navigation",
|
|
1961
|
+
path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M8 11l1.5-2 1.5 2 M8 14l1.5 2 1.5-2 M15 8v8"
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
key: "parking",
|
|
1965
|
+
label: "Parking",
|
|
1966
|
+
group: "navigation",
|
|
1967
|
+
path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M9 17V7h4a3 3 0 0 1 0 6H9"
|
|
1968
|
+
},
|
|
1969
|
+
// ---- Accessibility -----------------------------------------------------
|
|
1970
|
+
{
|
|
1971
|
+
key: "wheelchair",
|
|
1972
|
+
label: "Wheelchair access",
|
|
1973
|
+
group: "accessibility",
|
|
1974
|
+
path: WHEELCHAIR
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
key: "hearing",
|
|
1978
|
+
label: "Hearing assistance",
|
|
1979
|
+
group: "accessibility",
|
|
1980
|
+
path: "M8 20a4 4 0 0 1-2-3c0-1 .5-2 .5-3a4.5 4.5 0 1 1 9 0c0 1.4-1 2-2 2.4-1.2.5-1.5 1-1.5 2.2 M17 6a5 5 0 0 1 1 6 M19.5 4a8 8 0 0 1 1.2 9"
|
|
1981
|
+
}
|
|
1982
|
+
];
|
|
1983
|
+
var BY_KEY = new Map(VENUE_ICONS.map((icon) => [icon.key, icon]));
|
|
1984
|
+
function venueIcon(key) {
|
|
1985
|
+
return key ? BY_KEY.get(key) : void 0;
|
|
1986
|
+
}
|
|
1987
|
+
function venueIconPath(key) {
|
|
1988
|
+
return venueIcon(key)?.path;
|
|
1989
|
+
}
|
|
1990
|
+
var VENUE_ICON_KEYS = VENUE_ICONS.map((icon) => icon.key);
|
|
1991
|
+
|
|
1992
|
+
// src/core/spatialIndex.ts
|
|
1993
|
+
var DEFAULT_SEAT_RADIUS = 9;
|
|
1994
|
+
function halfExtents(shape) {
|
|
1995
|
+
if (shape.kind === "circle") return { hx: shape.r, hy: shape.r };
|
|
1996
|
+
const hw = shape.width / 2;
|
|
1997
|
+
const hh = shape.height / 2;
|
|
1998
|
+
const rot = shape.rotation ?? 0;
|
|
1999
|
+
if (!rot) return { hx: hw, hy: hh };
|
|
2000
|
+
const rad = rot * Math.PI / 180;
|
|
2001
|
+
const c = Math.abs(Math.cos(rad));
|
|
2002
|
+
const s = Math.abs(Math.sin(rad));
|
|
2003
|
+
return { hx: hw * c + hh * s, hy: hw * s + hh * c };
|
|
2004
|
+
}
|
|
2005
|
+
function shapeIntersectsRect(e, x0, y0, x1, y1) {
|
|
2006
|
+
if (e.shape.kind === "circle") {
|
|
2007
|
+
const cx = Math.min(Math.max(e.seat.x, x0), x1);
|
|
2008
|
+
const cy = Math.min(Math.max(e.seat.y, y0), y1);
|
|
2009
|
+
const dx = e.seat.x - cx;
|
|
2010
|
+
const dy = e.seat.y - cy;
|
|
2011
|
+
return dx * dx + dy * dy <= e.shape.r * e.shape.r;
|
|
2012
|
+
}
|
|
2013
|
+
const rot = e.shape.rotation ?? 0;
|
|
2014
|
+
if (!rot) {
|
|
2015
|
+
return e.seat.x - e.hx <= x1 && e.seat.x + e.hx >= x0 && e.seat.y - e.hy <= y1 && e.seat.y + e.hy >= y0;
|
|
2016
|
+
}
|
|
2017
|
+
const rad = rot * Math.PI / 180;
|
|
2018
|
+
const c = Math.cos(rad);
|
|
2019
|
+
const s = Math.sin(rad);
|
|
2020
|
+
const hw = e.shape.width / 2;
|
|
2021
|
+
const hh = e.shape.height / 2;
|
|
2022
|
+
const corners = [
|
|
2023
|
+
[-hw, -hh],
|
|
2024
|
+
[hw, -hh],
|
|
2025
|
+
[hw, hh],
|
|
2026
|
+
[-hw, hh]
|
|
2027
|
+
].map(([lx, ly]) => [e.seat.x + lx * c - ly * s, e.seat.y + lx * s + ly * c]);
|
|
2028
|
+
const query = [
|
|
2029
|
+
[x0, y0],
|
|
2030
|
+
[x1, y0],
|
|
2031
|
+
[x1, y1],
|
|
2032
|
+
[x0, y1]
|
|
2033
|
+
];
|
|
2034
|
+
const axes = [
|
|
2035
|
+
[1, 0],
|
|
2036
|
+
[0, 1],
|
|
2037
|
+
[c, s],
|
|
2038
|
+
[-s, c]
|
|
2039
|
+
];
|
|
2040
|
+
for (const [ax, ay] of axes) {
|
|
2041
|
+
let minA = Infinity;
|
|
2042
|
+
let maxA = -Infinity;
|
|
2043
|
+
for (const [px, py] of corners) {
|
|
2044
|
+
const d = px * ax + py * ay;
|
|
2045
|
+
if (d < minA) minA = d;
|
|
2046
|
+
if (d > maxA) maxA = d;
|
|
2047
|
+
}
|
|
2048
|
+
let minB = Infinity;
|
|
2049
|
+
let maxB = -Infinity;
|
|
2050
|
+
for (const [px, py] of query) {
|
|
2051
|
+
const d = px * ax + py * ay;
|
|
2052
|
+
if (d < minB) minB = d;
|
|
2053
|
+
if (d > maxB) maxB = d;
|
|
2054
|
+
}
|
|
2055
|
+
if (maxA < minB || maxB < minA) return false;
|
|
2056
|
+
}
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
function buildSeatIndex(seats, opts = {}) {
|
|
2060
|
+
const seatRadius = opts.seatRadius ?? DEFAULT_SEAT_RADIUS;
|
|
2061
|
+
const n = seats.length;
|
|
2062
|
+
const entries = new Array(n);
|
|
2063
|
+
let minX = Infinity;
|
|
2064
|
+
let minY = Infinity;
|
|
2065
|
+
let maxX = -Infinity;
|
|
2066
|
+
let maxY = -Infinity;
|
|
2067
|
+
let maxHalf = 0;
|
|
2068
|
+
for (let i = 0; i < n; i++) {
|
|
2069
|
+
const seat = seats[i];
|
|
2070
|
+
const shape = opts.shapeOf?.(seat) ?? { kind: "circle", r: seatRadius };
|
|
2071
|
+
const { hx, hy } = halfExtents(shape);
|
|
2072
|
+
entries[i] = { seat, shape, hx, hy };
|
|
2073
|
+
if (seat.x - hx < minX) minX = seat.x - hx;
|
|
2074
|
+
if (seat.y - hy < minY) minY = seat.y - hy;
|
|
2075
|
+
if (seat.x + hx > maxX) maxX = seat.x + hx;
|
|
2076
|
+
if (seat.y + hy > maxY) maxY = seat.y + hy;
|
|
2077
|
+
if (hx > maxHalf) maxHalf = hx;
|
|
2078
|
+
if (hy > maxHalf) maxHalf = hy;
|
|
2079
|
+
}
|
|
2080
|
+
if (n === 0) {
|
|
2081
|
+
minX = 0;
|
|
2082
|
+
minY = 0;
|
|
2083
|
+
maxX = 0;
|
|
2084
|
+
maxY = 0;
|
|
2085
|
+
}
|
|
2086
|
+
const nominal = maxHalf > 0 ? maxHalf * 4 : 1;
|
|
2087
|
+
const spanX = Math.max(maxX - minX, 0);
|
|
2088
|
+
const spanY = Math.max(maxY - minY, 0);
|
|
2089
|
+
const maxCells = Math.max(64, opts.maxCells ?? n * 4);
|
|
2090
|
+
let cell = nominal;
|
|
2091
|
+
let cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
2092
|
+
let rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
2093
|
+
while (cols * rows > maxCells && (cols > 1 || rows > 1)) {
|
|
2094
|
+
cell *= 2;
|
|
2095
|
+
cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
2096
|
+
rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
2097
|
+
}
|
|
2098
|
+
const cellCount = cols * rows;
|
|
2099
|
+
const cellStart = new Int32Array(cellCount + 1);
|
|
2100
|
+
let total = 0;
|
|
2101
|
+
for (let i = 0; i < n; i++) {
|
|
2102
|
+
const e = entries[i];
|
|
2103
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
2104
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
2105
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
2106
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
2107
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
2108
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
2109
|
+
cellStart[cy * cols + cx + 1]++;
|
|
2110
|
+
total++;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
for (let c = 0; c < cellCount; c++) cellStart[c + 1] += cellStart[c];
|
|
2115
|
+
const items = new Int32Array(total);
|
|
2116
|
+
const cursor = cellStart.slice(0, cellCount);
|
|
2117
|
+
for (let i = 0; i < n; i++) {
|
|
2118
|
+
const e = entries[i];
|
|
2119
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
2120
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
2121
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
2122
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
2123
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
2124
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
2125
|
+
const c = cy * cols + cx;
|
|
2126
|
+
items[cursor[c]++] = i;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
seats,
|
|
2132
|
+
entries,
|
|
2133
|
+
minX,
|
|
2134
|
+
minY,
|
|
2135
|
+
cell,
|
|
2136
|
+
cols,
|
|
2137
|
+
rows,
|
|
2138
|
+
cellStart,
|
|
2139
|
+
items,
|
|
2140
|
+
stamp: new Int32Array(n),
|
|
2141
|
+
epoch: 0
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function clampInt(v, lo, hi) {
|
|
2145
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
2146
|
+
}
|
|
2147
|
+
function queryRect(index, rect, opts = {}) {
|
|
2148
|
+
const x0 = Math.min(rect.x, rect.x + rect.width);
|
|
2149
|
+
const x1 = Math.max(rect.x, rect.x + rect.width);
|
|
2150
|
+
const y0 = Math.min(rect.y, rect.y + rect.height);
|
|
2151
|
+
const y1 = Math.max(rect.y, rect.y + rect.height);
|
|
2152
|
+
const mode = opts.mode ?? "center";
|
|
2153
|
+
const { cols, rows, cell, minX, minY, cellStart, items, entries } = index;
|
|
2154
|
+
const cx0 = Math.max(0, Math.floor((x0 - minX) / cell));
|
|
2155
|
+
const cx1 = Math.min(cols - 1, Math.floor((x1 - minX) / cell));
|
|
2156
|
+
const cy0 = Math.max(0, Math.floor((y0 - minY) / cell));
|
|
2157
|
+
const cy1 = Math.min(rows - 1, Math.floor((y1 - minY) / cell));
|
|
2158
|
+
if (cx0 > cx1 || cy0 > cy1) return [];
|
|
2159
|
+
const found = [];
|
|
2160
|
+
const stamp = index.stamp;
|
|
2161
|
+
const ep = ++index.epoch;
|
|
2162
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
2163
|
+
const rowBase = cy * cols;
|
|
2164
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
2165
|
+
const c = rowBase + cx;
|
|
2166
|
+
const end = cellStart[c + 1];
|
|
2167
|
+
for (let k = cellStart[c]; k < end; k++) {
|
|
2168
|
+
const i = items[k];
|
|
2169
|
+
if (stamp[i] === ep) continue;
|
|
2170
|
+
stamp[i] = ep;
|
|
2171
|
+
const e = entries[i];
|
|
2172
|
+
if (opts.filter && !opts.filter(e.seat.id, e.seat)) continue;
|
|
2173
|
+
const ok = mode === "center" ? e.seat.x >= x0 && e.seat.x <= x1 && e.seat.y >= y0 && e.seat.y <= y1 : shapeIntersectsRect(e, x0, y0, x1, y1);
|
|
2174
|
+
if (ok) found.push(i);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
found.sort((a, b) => a - b);
|
|
2179
|
+
return found.map((i) => entries[i].seat.id);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/core/perspectiveProjection.ts
|
|
2183
|
+
function createPerspectiveCamera(bounds2, maxSurfaceHeightWorld = 0) {
|
|
2184
|
+
const span = Math.max(1, bounds2.width, bounds2.height);
|
|
2185
|
+
const target = {
|
|
2186
|
+
x: bounds2.x + bounds2.width / 2,
|
|
2187
|
+
y: bounds2.y + bounds2.height / 2
|
|
2188
|
+
};
|
|
2189
|
+
const distance = span * 1.55;
|
|
2190
|
+
const height = Math.max(span * 0.82, Math.max(0, maxSurfaceHeightWorld) + span * 0.45);
|
|
2191
|
+
return {
|
|
2192
|
+
target,
|
|
2193
|
+
distance,
|
|
2194
|
+
height,
|
|
2195
|
+
focalLength: Math.hypot(distance, height),
|
|
2196
|
+
yawRad: -8 * Math.PI / 180
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
function projectPerspectivePoint(camera, point, heightWorld = 0) {
|
|
2200
|
+
const dx = point.x - camera.target.x;
|
|
2201
|
+
const dy = point.y - camera.target.y;
|
|
2202
|
+
const cos = Math.cos(camera.yawRad);
|
|
2203
|
+
const sin = Math.sin(camera.yawRad);
|
|
2204
|
+
const x = dx * cos - dy * sin;
|
|
2205
|
+
const y = dx * sin + dy * cos;
|
|
2206
|
+
const translatedY = y - camera.distance;
|
|
2207
|
+
const translatedZ = heightWorld - camera.height;
|
|
2208
|
+
const cameraLength = Math.hypot(camera.distance, camera.height);
|
|
2209
|
+
const depth = (-camera.distance * translatedY - camera.height * translatedZ) / cameraLength;
|
|
2210
|
+
const up = (-camera.height * translatedY + camera.distance * translatedZ) / cameraLength;
|
|
2211
|
+
const safeDepth = Math.max(cameraLength * 0.08, depth);
|
|
2212
|
+
const scale = camera.focalLength / safeDepth;
|
|
2213
|
+
return {
|
|
2214
|
+
x: camera.target.x + x * scale,
|
|
2215
|
+
y: camera.target.y - up * scale,
|
|
2216
|
+
depth: safeDepth,
|
|
2217
|
+
scale
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function applyAffine(affine, point) {
|
|
2221
|
+
return {
|
|
2222
|
+
x: affine.a * point.x + affine.c * point.y + affine.e,
|
|
2223
|
+
y: affine.b * point.x + affine.d * point.y + affine.f
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function invertAffine(affine) {
|
|
2227
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
2228
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-9) {
|
|
2229
|
+
throw new Error("perspective projection produced a singular affine");
|
|
2230
|
+
}
|
|
2231
|
+
const a = affine.d / determinant;
|
|
2232
|
+
const b = -affine.b / determinant;
|
|
2233
|
+
const c = -affine.c / determinant;
|
|
2234
|
+
const d = affine.a / determinant;
|
|
2235
|
+
return {
|
|
2236
|
+
a,
|
|
2237
|
+
b,
|
|
2238
|
+
c,
|
|
2239
|
+
d,
|
|
2240
|
+
e: -(a * affine.e + c * affine.f),
|
|
2241
|
+
f: -(b * affine.e + d * affine.f)
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function composeAffine(left, right) {
|
|
2245
|
+
return {
|
|
2246
|
+
a: left.a * right.a + left.c * right.b,
|
|
2247
|
+
b: left.b * right.a + left.d * right.b,
|
|
2248
|
+
c: left.a * right.c + left.c * right.d,
|
|
2249
|
+
d: left.b * right.c + left.d * right.d,
|
|
2250
|
+
e: left.a * right.e + left.c * right.f + left.e,
|
|
2251
|
+
f: left.b * right.e + left.d * right.f + left.f
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
function perspectiveTangentAffine(camera, anchor, heightAt = () => 0) {
|
|
2255
|
+
const origin = projectPerspectivePoint(camera, anchor, heightAt(anchor));
|
|
2256
|
+
const alongXPoint = { x: anchor.x + 1, y: anchor.y };
|
|
2257
|
+
const alongYPoint = { x: anchor.x, y: anchor.y + 1 };
|
|
2258
|
+
const alongX = projectPerspectivePoint(camera, alongXPoint, heightAt(alongXPoint));
|
|
2259
|
+
const alongY = projectPerspectivePoint(camera, alongYPoint, heightAt(alongYPoint));
|
|
2260
|
+
const a = alongX.x - origin.x;
|
|
2261
|
+
const b = alongX.y - origin.y;
|
|
2262
|
+
const c = alongY.x - origin.x;
|
|
2263
|
+
const d = alongY.y - origin.y;
|
|
2264
|
+
return {
|
|
2265
|
+
a,
|
|
2266
|
+
b,
|
|
2267
|
+
c,
|
|
2268
|
+
d,
|
|
2269
|
+
e: origin.x - a * anchor.x - c * anchor.y,
|
|
2270
|
+
f: origin.y - b * anchor.x - d * anchor.y
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
function decomposeAffineLinear(affine) {
|
|
2274
|
+
const scaleX = Math.hypot(affine.a, affine.b);
|
|
2275
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
2276
|
+
return {
|
|
2277
|
+
rotationDeg: Math.atan2(affine.b, affine.a) * 180 / Math.PI,
|
|
2278
|
+
scaleX,
|
|
2279
|
+
scaleY: determinant / Math.max(scaleX, 1e-9),
|
|
2280
|
+
skewX: (affine.a * affine.c + affine.b * affine.d) / Math.max(determinant, 1e-9)
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
|
|
1511
2284
|
// src/lib/money.ts
|
|
1512
2285
|
var DEFAULT_CURRENCY = "USD";
|
|
1513
2286
|
var displayLocale;
|
|
@@ -1553,6 +2326,7 @@ var en = {
|
|
|
1553
2326
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
1554
2327
|
"map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
|
|
1555
2328
|
"map.seatsLeft": "{count} LEFT",
|
|
2329
|
+
"map.soldOut": "SOLD OUT",
|
|
1556
2330
|
"map.fromPrice": "FROM {price}",
|
|
1557
2331
|
"map.statusHeld": "On hold",
|
|
1558
2332
|
"map.statusTaken": "Taken",
|
|
@@ -1560,7 +2334,7 @@ var en = {
|
|
|
1560
2334
|
"picker.floor": "Floor",
|
|
1561
2335
|
"picker.zoomLevel": "Zoom level",
|
|
1562
2336
|
"picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
|
|
1563
|
-
"picker.rungTip.sections": "Section blocks \u2014
|
|
2337
|
+
"picker.rungTip.sections": "Section blocks \u2014 sold-out and nearly-gone sections shade to show availability at a glance",
|
|
1564
2338
|
"picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
|
|
1565
2339
|
"picker.rungLabel.zones": "ZONES",
|
|
1566
2340
|
"picker.rungLabel.sections": "SECTIONS",
|
|
@@ -1628,6 +2402,25 @@ function formatDate(value, opts) {
|
|
|
1628
2402
|
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
1629
2403
|
}
|
|
1630
2404
|
|
|
2405
|
+
// src/core/shapeLineStyle.ts
|
|
2406
|
+
var DEFAULT_SHAPE_LINE_CAP = "round";
|
|
2407
|
+
var DEFAULT_SHAPE_LINE_JOIN = "round";
|
|
2408
|
+
var DEFAULT_SHAPE_LINE_ENDING = "none";
|
|
2409
|
+
function resolvedShapeLineStyle(shape) {
|
|
2410
|
+
return {
|
|
2411
|
+
lineCap: shape.lineCap ?? DEFAULT_SHAPE_LINE_CAP,
|
|
2412
|
+
lineJoin: shape.lineJoin ?? DEFAULT_SHAPE_LINE_JOIN,
|
|
2413
|
+
startEnding: shape.startEnding ?? DEFAULT_SHAPE_LINE_ENDING,
|
|
2414
|
+
endEnding: shape.endEnding ?? DEFAULT_SHAPE_LINE_ENDING
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
function shapeArrowMetrics(strokeWidth) {
|
|
2418
|
+
return {
|
|
2419
|
+
pointerLength: Math.min(48, Math.max(8, strokeWidth * 3)),
|
|
2420
|
+
pointerWidth: Math.min(40, Math.max(8, strokeWidth * 2.25))
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
1631
2424
|
// src/engine/SeatmapRenderer.ts
|
|
1632
2425
|
var SEAT_RADIUS = 9;
|
|
1633
2426
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
@@ -1635,17 +2428,18 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
1635
2428
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1636
2429
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
1637
2430
|
var SEAT_TAP_SLOP_PX = 14;
|
|
1638
|
-
var
|
|
2431
|
+
var WHEELCHAIR_GLYPH_MIN_PX = 10;
|
|
2432
|
+
var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
|
|
1639
2433
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
1640
2434
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
1641
2435
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
1642
2436
|
var PAN_START_SLOP_PX = 8;
|
|
2437
|
+
var GHOST_CLICK_MS = 700;
|
|
1643
2438
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
1644
2439
|
var MAX_LABELS = 700;
|
|
1645
2440
|
var MARQUEE_RING_CAP = 2500;
|
|
1646
2441
|
var ISO_ANGLE_DEG = -11.5;
|
|
1647
2442
|
var ISO_SQUASH = 0.58;
|
|
1648
|
-
var LIFT_PER_STEP = 58;
|
|
1649
2443
|
var ISO_TWEEN_MS = 320;
|
|
1650
2444
|
var CAMERA_GLIDE_MS = 650;
|
|
1651
2445
|
var BLOCK_FILL_ALPHA = 1;
|
|
@@ -1682,6 +2476,10 @@ var DEF_SELECTION = "#ffffff";
|
|
|
1682
2476
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1683
2477
|
var DEF_DECOR_FILL = "#232c40";
|
|
1684
2478
|
var DEF_TEXT = "#8b93a7";
|
|
2479
|
+
function konvaFontStyle(bold, italic, defaultWeight = "normal") {
|
|
2480
|
+
const weight = bold ? "bold" : defaultWeight;
|
|
2481
|
+
return italic ? `italic ${weight}`.trim() : weight;
|
|
2482
|
+
}
|
|
1685
2483
|
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1686
2484
|
function colorLuminance(color) {
|
|
1687
2485
|
const s = color.trim();
|
|
@@ -1737,6 +2535,22 @@ function overviewPalette(canvasBackground) {
|
|
|
1737
2535
|
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
1738
2536
|
};
|
|
1739
2537
|
}
|
|
2538
|
+
var SECTION_NEARLY_GONE_RATIO = 0.1;
|
|
2539
|
+
function overviewStateFill(palette, state) {
|
|
2540
|
+
const base = palette.sectionFill;
|
|
2541
|
+
const soldOut = desaturate(darken(base, 0.2), 0.85);
|
|
2542
|
+
switch (state) {
|
|
2543
|
+
case "closed":
|
|
2544
|
+
return darken(base, 0.12);
|
|
2545
|
+
case "sold-out":
|
|
2546
|
+
return soldOut;
|
|
2547
|
+
case "nearly-gone":
|
|
2548
|
+
return lerpColor(base, soldOut, 0.4);
|
|
2549
|
+
case "normal":
|
|
2550
|
+
default:
|
|
2551
|
+
return base;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
1740
2554
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
1741
2555
|
function seatIdOf(target) {
|
|
1742
2556
|
const n = target;
|
|
@@ -1759,6 +2573,13 @@ function darken(hex, amt) {
|
|
|
1759
2573
|
const mix = (c) => Math.round(c * (1 - amt));
|
|
1760
2574
|
return `#${(1 << 24 | mix(n >> 16 & 255) << 16 | mix(n >> 8 & 255) << 8 | mix(n & 255)).toString(16).slice(1)}`;
|
|
1761
2575
|
}
|
|
2576
|
+
function desaturate(hex, amt) {
|
|
2577
|
+
const rgb2 = hexToRgb(hex);
|
|
2578
|
+
if (!rgb2) return hex;
|
|
2579
|
+
const [r, g, b] = rgb2;
|
|
2580
|
+
const grey = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
2581
|
+
return toHex(r + (grey - r) * amt, g + (grey - g) * amt, b + (grey - b) * amt);
|
|
2582
|
+
}
|
|
1762
2583
|
function polyBounds(pts) {
|
|
1763
2584
|
let minX = Infinity;
|
|
1764
2585
|
let minY = Infinity;
|
|
@@ -1787,8 +2608,8 @@ function rotatedRectPoints(center, width, height, rotation) {
|
|
|
1787
2608
|
}));
|
|
1788
2609
|
}
|
|
1789
2610
|
function pointsBounds(points) {
|
|
1790
|
-
const
|
|
1791
|
-
return { x:
|
|
2611
|
+
const bounds2 = polyBounds(points);
|
|
2612
|
+
return { x: bounds2.x, y: bounds2.y, width: bounds2.width, height: bounds2.height };
|
|
1792
2613
|
}
|
|
1793
2614
|
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1794
2615
|
const radians = rotation * Math.PI / 180;
|
|
@@ -1808,32 +2629,50 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
|
1808
2629
|
return true;
|
|
1809
2630
|
}
|
|
1810
2631
|
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1811
|
-
const
|
|
1812
|
-
const centre = { x:
|
|
2632
|
+
const bounds2 = polyBounds(outer);
|
|
2633
|
+
const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
|
|
1813
2634
|
const points = [preferred];
|
|
1814
2635
|
for (let row = 1; row < 12; row += 1) {
|
|
1815
2636
|
for (let column = 1; column < 12; column += 1) {
|
|
1816
2637
|
const point = {
|
|
1817
|
-
x:
|
|
1818
|
-
y:
|
|
2638
|
+
x: bounds2.x + bounds2.width * column / 12,
|
|
2639
|
+
y: bounds2.y + bounds2.height * row / 12
|
|
1819
2640
|
};
|
|
1820
2641
|
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1821
2642
|
}
|
|
1822
2643
|
}
|
|
1823
2644
|
return points.sort((left, right) => Math.hypot(left.x - centre.x, left.y - centre.y) - Math.hypot(right.x - centre.x, right.y - centre.y)).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
|
|
1824
2645
|
}
|
|
2646
|
+
function roundedPolygonSubpath(context, points, radius) {
|
|
2647
|
+
const n = points.length;
|
|
2648
|
+
const start = { x: (points[n - 1].x + points[0].x) / 2, y: (points[n - 1].y + points[0].y) / 2 };
|
|
2649
|
+
context.moveTo(start.x, start.y);
|
|
2650
|
+
for (let i = 0; i < n; i += 1) {
|
|
2651
|
+
const curr = points[i];
|
|
2652
|
+
const next = points[(i + 1) % n];
|
|
2653
|
+
const prev = points[(i - 1 + n) % n];
|
|
2654
|
+
const rMax = Math.min(
|
|
2655
|
+
Math.hypot(curr.x - prev.x, curr.y - prev.y) / 2,
|
|
2656
|
+
Math.hypot(next.x - curr.x, next.y - curr.y) / 2
|
|
2657
|
+
);
|
|
2658
|
+
context.arcTo(curr.x, curr.y, next.x, next.y, Math.max(0, Math.min(radius, rMax)));
|
|
2659
|
+
}
|
|
2660
|
+
context.closePath();
|
|
2661
|
+
}
|
|
1825
2662
|
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1826
2663
|
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1827
2664
|
const next = points[(index + 1) % points.length];
|
|
1828
2665
|
return sum + point.x * next.y - next.x * point.y;
|
|
1829
2666
|
}, 0);
|
|
1830
2667
|
const outerClockwise = signedArea(outer) > 0;
|
|
2668
|
+
const cornerRadius = attrs.cornerRadius && attrs.cornerRadius > 0 ? attrs.cornerRadius : 0;
|
|
1831
2669
|
return new import_Shape.Shape({
|
|
1832
2670
|
...attrs,
|
|
1833
2671
|
sceneFunc(context, shape) {
|
|
1834
2672
|
context.beginPath();
|
|
1835
2673
|
const polygonPath = (points) => {
|
|
1836
2674
|
if (!points.length) return;
|
|
2675
|
+
if (cornerRadius > 0 && points.length >= 3) return roundedPolygonSubpath(context, points, cornerRadius);
|
|
1837
2676
|
context.moveTo(points[0].x, points[0].y);
|
|
1838
2677
|
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1839
2678
|
context.closePath();
|
|
@@ -1880,6 +2719,33 @@ function rgba(hex, a) {
|
|
|
1880
2719
|
const n = parseInt(m[1], 16);
|
|
1881
2720
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1882
2721
|
}
|
|
2722
|
+
var ViewportGroup = class extends import_Group.Group {
|
|
2723
|
+
constructor() {
|
|
2724
|
+
super(...arguments);
|
|
2725
|
+
this.viewportCulled = false;
|
|
2726
|
+
}
|
|
2727
|
+
setViewportCulled(culled) {
|
|
2728
|
+
if (culled === this.viewportCulled) return;
|
|
2729
|
+
this.viewportCulled = culled;
|
|
2730
|
+
if (!culled) super._clearSelfAndDescendantCache();
|
|
2731
|
+
}
|
|
2732
|
+
isViewportCulled() {
|
|
2733
|
+
return this.viewportCulled;
|
|
2734
|
+
}
|
|
2735
|
+
drawScene(...args) {
|
|
2736
|
+
return this.viewportCulled ? this : super.drawScene(...args);
|
|
2737
|
+
}
|
|
2738
|
+
drawHit(...args) {
|
|
2739
|
+
return this.viewportCulled ? this : super.drawHit(...args);
|
|
2740
|
+
}
|
|
2741
|
+
_clearSelfAndDescendantCache(attr) {
|
|
2742
|
+
if (!this.viewportCulled) {
|
|
2743
|
+
super._clearSelfAndDescendantCache(attr);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
this._clearCache(attr);
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
1883
2749
|
function hexToRgb(hex) {
|
|
1884
2750
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1885
2751
|
if (!m) return null;
|
|
@@ -1910,8 +2776,15 @@ function lerpColor(a, b, t2) {
|
|
|
1910
2776
|
}
|
|
1911
2777
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
1912
2778
|
constructor(container, options = {}) {
|
|
2779
|
+
/** Per-elevated-section label containers. Keeping the labels grouped lets the
|
|
2780
|
+
* Phase-B lift move thousands of seat labels with one transform per section
|
|
2781
|
+
* instead of rewriting every label on every tween frame. */
|
|
2782
|
+
this.labelLiftGroups = /* @__PURE__ */ new Map();
|
|
1913
2783
|
this.seats = [];
|
|
1914
2784
|
this.seatById = /* @__PURE__ */ new Map();
|
|
2785
|
+
/** One build per chart/floor; section membership queries only inspect seats in
|
|
2786
|
+
* the section bounds instead of rescanning the full 13k venue per section. */
|
|
2787
|
+
this.seatIndex = null;
|
|
1915
2788
|
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
1916
2789
|
this.chartDoc = null;
|
|
1917
2790
|
this.activeFloorId = "";
|
|
@@ -1927,15 +2800,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1927
2800
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1928
2801
|
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1929
2802
|
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1930
|
-
/** Centred
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
2803
|
+
/** Centred ♿ glyph per physical wheelchair provision. It keeps a small
|
|
2804
|
+
* screen-space floor at every LOD and grows when the Wheelchair filter is
|
|
2805
|
+
* active. Kept in a map so zoom updates touch only the handful of matching
|
|
2806
|
+
* seats, never all 13k nodes. */
|
|
1934
2807
|
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1935
|
-
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1936
|
-
this.accessGlyphVisible = false;
|
|
1937
2808
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1938
2809
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
2810
|
+
/** Vector venue-icon Path nodes + authored size; obey the free-text size floor. */
|
|
2811
|
+
this.iconNodeById = /* @__PURE__ */ new Map();
|
|
1939
2812
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1940
2813
|
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1941
2814
|
/** GA paint and text share price/highlight filter state. */
|
|
@@ -1949,6 +2822,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1949
2822
|
this.effSelection = DEF_SELECTION;
|
|
1950
2823
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
1951
2824
|
this.colorblind = false;
|
|
2825
|
+
/** Stable per-category-KEY colorblind hue (OV-46) — order-independent. */
|
|
2826
|
+
this.cbHueByKey = /* @__PURE__ */ new Map();
|
|
2827
|
+
/** OV-45(a): authored row-letter labels for the buyer/preview map, resolved in
|
|
2828
|
+
* world space (matching seat labels) and honouring each row's labelPresentation
|
|
2829
|
+
* (position/rotation/visibility/style). Painted at the seats LOD rung. */
|
|
2830
|
+
this.rowLabelPlan = [];
|
|
1952
2831
|
/** Category order from the doc — the stable index into the CB palette. */
|
|
1953
2832
|
this.catOrder = [];
|
|
1954
2833
|
this.selection = /* @__PURE__ */ new Set();
|
|
@@ -1958,6 +2837,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1958
2837
|
this.ownedHold = /* @__PURE__ */ new Set();
|
|
1959
2838
|
/** One selected seat being inspected before it is committed to the cart. */
|
|
1960
2839
|
this.selectionFocusId = null;
|
|
2840
|
+
/** Seat currently owning the shared hover ring (null while not hovering). */
|
|
2841
|
+
this.hoveredId = null;
|
|
1961
2842
|
this.focusedId = null;
|
|
1962
2843
|
/**
|
|
1963
2844
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -1976,6 +2857,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1976
2857
|
this.sections = [];
|
|
1977
2858
|
this.zones = [];
|
|
1978
2859
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
2860
|
+
/** Seats outside every authored section retain the legacy path in one group. */
|
|
2861
|
+
this.unsectionedSeatGroup = null;
|
|
1979
2862
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1980
2863
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1981
2864
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
@@ -1988,6 +2871,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1988
2871
|
this.focusedSectionId = null;
|
|
1989
2872
|
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1990
2873
|
this.focusBackdrop = null;
|
|
2874
|
+
/** Non-listening visual dim outside the focused section. One overlay replaces
|
|
2875
|
+
* descendant opacity mutations across every seat in the venue. */
|
|
2876
|
+
this.focusDimOverlay = null;
|
|
1991
2877
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1992
2878
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1993
2879
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -2000,10 +2886,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2000
2886
|
this.isoT = 0;
|
|
2001
2887
|
this.isoTarget = 0;
|
|
2002
2888
|
this.isoRaf = 0;
|
|
2889
|
+
/** Public view target; perspective is a separate non-affine Phase-C lane. */
|
|
2890
|
+
this.viewMode = "flat";
|
|
2891
|
+
this.perspectiveCamera = null;
|
|
2892
|
+
/** Ground-plane tangent applied once to every renderer layer. */
|
|
2893
|
+
this.perspectiveBaseAffine = null;
|
|
2894
|
+
this.perspectiveBaseInverse = null;
|
|
2895
|
+
/** Exact pinhole anchor expressed in the overlay tangent-layer coordinates. */
|
|
2896
|
+
this.perspectiveSeatLocal = /* @__PURE__ */ new Map();
|
|
2897
|
+
/** Exact projected point (before Stage pan/zoom), keyed by seat. */
|
|
2898
|
+
this.perspectiveSeatProjected = /* @__PURE__ */ new Map();
|
|
2899
|
+
/** Positive camera depth, used for deterministic far→near paint order. */
|
|
2900
|
+
this.perspectiveSeatDepth = /* @__PURE__ */ new Map();
|
|
2901
|
+
/** Exact pinhole size ratio at each seat anchor. */
|
|
2902
|
+
this.perspectiveSeatScale = /* @__PURE__ */ new Map();
|
|
2903
|
+
/** Seats whose native Konva nodes currently carry the exact projected pose.
|
|
2904
|
+
* Large sectioned venues apply these lazily as a section enters the live-seat
|
|
2905
|
+
* viewport; the overview rung hides the seat layer completely. */
|
|
2906
|
+
this.perspectiveAppliedSeats = /* @__PURE__ */ new Set();
|
|
2907
|
+
this.perspectiveBounds = null;
|
|
2003
2908
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
2004
2909
|
this.isoCentre = { x: 0, y: 0 };
|
|
2005
2910
|
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
2006
2911
|
this.glideRaf = 0;
|
|
2912
|
+
/** While the camera tween is active, defer polygon label fitting/collision to
|
|
2913
|
+
* the settled frame. Geometry remains smoothly stage-scaled, while expensive
|
|
2914
|
+
* point-in-polygon text work no longer repeats at 120 Hz. */
|
|
2915
|
+
this.glideInProgress = false;
|
|
2007
2916
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
2008
2917
|
this.destroyed = false;
|
|
2009
2918
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -2033,6 +2942,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2033
2942
|
this.panStarted = false;
|
|
2034
2943
|
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
2035
2944
|
this.moved = 0;
|
|
2945
|
+
/**
|
|
2946
|
+
* Ghost-click guard. Konva binds `_pointerup` to mouseup, touchend AND
|
|
2947
|
+
* pointerup, so `on('click tap')` is two handlers for one finger tap unless
|
|
2948
|
+
* the browser's synthesized compatibility click is suppressed. Konva only
|
|
2949
|
+
* suppresses it when the touch lands on a LISTENING SHAPE (Stage.js returns
|
|
2950
|
+
* early before its preventDefault() when `!shape || !shape.isListening()`),
|
|
2951
|
+
* so a near-miss rescued by nearestSeatToScreen used to fire `tap` (select)
|
|
2952
|
+
* then `click` (deselect) 1-3ms apart and net to nothing. With
|
|
2953
|
+
* SEAT_TAP_SLOP_PX=14 against a ~12px seat radius, that broken annulus is
|
|
2954
|
+
* ~3.7x the area of the seat itself — i.e. most successful mobile taps.
|
|
2955
|
+
* `touch-action: none` does NOT suppress compatibility mouse events.
|
|
2956
|
+
*/
|
|
2957
|
+
this.lastTapAt = 0;
|
|
2036
2958
|
/**
|
|
2037
2959
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
2038
2960
|
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
@@ -2129,6 +3051,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2129
3051
|
x: mid.x - this.pinch.worldMid.x * scale,
|
|
2130
3052
|
y: mid.y - this.pinch.worldMid.y * scale
|
|
2131
3053
|
});
|
|
3054
|
+
this.updateSeatGroupVisibility();
|
|
2132
3055
|
this.stage.batchDraw();
|
|
2133
3056
|
this.scheduleViewChange();
|
|
2134
3057
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
@@ -2141,6 +3064,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2141
3064
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
2142
3065
|
});
|
|
2143
3066
|
this.panLast = p;
|
|
3067
|
+
this.updateSeatGroupVisibility();
|
|
2144
3068
|
this.stage.batchDraw();
|
|
2145
3069
|
this.scheduleViewChange();
|
|
2146
3070
|
}
|
|
@@ -2193,6 +3117,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2193
3117
|
this.overlayLayer = new import_Layer.Layer({ listening: false });
|
|
2194
3118
|
this.labelGroup = new import_Group.Group({ listening: false });
|
|
2195
3119
|
this.overlayLayer.add(this.labelGroup);
|
|
3120
|
+
this.fgDecorGroup = new import_Group.Group({ listening: false });
|
|
3121
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
2196
3122
|
this.hoverRing = new import_Circle.Circle({
|
|
2197
3123
|
radius: SEAT_RADIUS + 2,
|
|
2198
3124
|
stroke: "#ffffff",
|
|
@@ -2227,6 +3153,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2227
3153
|
this.resizeObs.observe(container);
|
|
2228
3154
|
}
|
|
2229
3155
|
}
|
|
3156
|
+
/**
|
|
3157
|
+
* True when this is the browser's synthesized compatibility click following a
|
|
3158
|
+
* finger tap we already handled. Discriminates on `e.type` deliberately —
|
|
3159
|
+
* `PointerEvent extends MouseEvent`, so an `instanceof MouseEvent` test
|
|
3160
|
+
* matches genuine pointer taps too. One shared timestamp across the layer and
|
|
3161
|
+
* stage handlers: a tap consumed at the layer must also suppress the ghost at
|
|
3162
|
+
* the stage, and only `click` is ever rejected, so bubbling stays intact.
|
|
3163
|
+
*/
|
|
3164
|
+
isGhostClick(e) {
|
|
3165
|
+
if (e.type === "tap") {
|
|
3166
|
+
this.lastTapAt = performance.now();
|
|
3167
|
+
return false;
|
|
3168
|
+
}
|
|
3169
|
+
return this.lastTapAt > 0 && performance.now() - this.lastTapAt < GHOST_CLICK_MS;
|
|
3170
|
+
}
|
|
2230
3171
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
2231
3172
|
setChart(doc, opts) {
|
|
2232
3173
|
if (doc !== this.chartDoc) this.stacked = false;
|
|
@@ -2240,8 +3181,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2240
3181
|
this.focusedId = null;
|
|
2241
3182
|
this.focusRing.visible(false);
|
|
2242
3183
|
this.bgLayer.destroyChildren();
|
|
3184
|
+
this.focusDimOverlay?.destroy();
|
|
3185
|
+
this.focusDimOverlay = null;
|
|
3186
|
+
this.seatLayer.clearCache();
|
|
3187
|
+
this.seatLayer.listening(true);
|
|
2243
3188
|
this.seatLayer.destroyChildren();
|
|
3189
|
+
this.unsectionedSeatGroup = null;
|
|
2244
3190
|
this.labelGroup.destroyChildren();
|
|
3191
|
+
this.labelLiftGroups.clear();
|
|
3192
|
+
this.fgDecorGroup.destroyChildren();
|
|
2245
3193
|
this.circleById.clear();
|
|
2246
3194
|
this.boothDims.clear();
|
|
2247
3195
|
this.boothLabelById.clear();
|
|
@@ -2249,6 +3197,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2249
3197
|
this.accessRingById.clear();
|
|
2250
3198
|
this.accessGlyphById.clear();
|
|
2251
3199
|
this.freeTextById.clear();
|
|
3200
|
+
this.iconNodeById.clear();
|
|
2252
3201
|
this.primaryFocalLabels.clear();
|
|
2253
3202
|
this.gaById.clear();
|
|
2254
3203
|
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
@@ -2258,6 +3207,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2258
3207
|
this.statusById.clear();
|
|
2259
3208
|
this.selection.clear();
|
|
2260
3209
|
this.seatById.clear();
|
|
3210
|
+
this.seatIndex = null;
|
|
2261
3211
|
this.cached = false;
|
|
2262
3212
|
this.accessFilter = null;
|
|
2263
3213
|
this.sections = [];
|
|
@@ -2275,11 +3225,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2275
3225
|
}
|
|
2276
3226
|
this.isoT = 0;
|
|
2277
3227
|
this.isoTarget = 0;
|
|
3228
|
+
this.viewMode = "flat";
|
|
3229
|
+
this.perspectiveCamera = null;
|
|
3230
|
+
this.perspectiveBaseAffine = null;
|
|
3231
|
+
this.perspectiveBaseInverse = null;
|
|
3232
|
+
this.perspectiveSeatLocal.clear();
|
|
3233
|
+
this.perspectiveSeatProjected.clear();
|
|
3234
|
+
this.perspectiveSeatDepth.clear();
|
|
3235
|
+
this.perspectiveSeatScale.clear();
|
|
3236
|
+
this.perspectiveAppliedSeats.clear();
|
|
3237
|
+
this.perspectiveBounds = null;
|
|
2278
3238
|
this.resetLayerTransforms();
|
|
2279
3239
|
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
2280
3240
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
2281
3241
|
this.seatLayer.opacity(1);
|
|
2282
3242
|
this.hoverRing.visible(false);
|
|
3243
|
+
this.hoveredId = null;
|
|
2283
3244
|
this.theme = doc.theme ?? {};
|
|
2284
3245
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
2285
3246
|
this.container.style.background = "";
|
|
@@ -2291,35 +3252,80 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2291
3252
|
this.catColor.clear();
|
|
2292
3253
|
this.catPrice.clear();
|
|
2293
3254
|
this.catOrder = doc.categories.map((c) => c.key);
|
|
3255
|
+
this.cbHueByKey.clear();
|
|
3256
|
+
[...this.catOrder].sort().forEach((key, i) => {
|
|
3257
|
+
this.cbHueByKey.set(key, CB_PALETTE[i % CB_PALETTE.length]);
|
|
3258
|
+
});
|
|
2294
3259
|
for (const c of doc.categories) {
|
|
2295
3260
|
this.catColor.set(c.key, c.color);
|
|
2296
3261
|
if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
|
|
2297
3262
|
}
|
|
2298
3263
|
for (const obj of view.objects) {
|
|
2299
3264
|
if (obj.type === "booth") {
|
|
2300
|
-
this.boothDims.set(obj.id, {
|
|
3265
|
+
this.boothDims.set(obj.id, {
|
|
3266
|
+
width: obj.width,
|
|
3267
|
+
height: obj.height,
|
|
3268
|
+
rotation: obj.rotation,
|
|
3269
|
+
...obj.points && obj.points.length >= 3 ? { points: obj.points.map((p) => ({ x: p.x, y: p.y })) } : {}
|
|
3270
|
+
});
|
|
2301
3271
|
}
|
|
2302
3272
|
}
|
|
2303
|
-
this.seats = expandChart(view
|
|
3273
|
+
this.seats = expandChart(view, {
|
|
3274
|
+
// A stacked overview is presentation-only and never opens a seat POV. The
|
|
3275
|
+
// per-section renderer below still resolves each object's real floor base.
|
|
3276
|
+
floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
|
|
3277
|
+
});
|
|
2304
3278
|
for (const s of this.seats) {
|
|
2305
3279
|
this.seatById.set(s.id, s);
|
|
2306
3280
|
this.statusById.set(s.id, "free");
|
|
2307
3281
|
}
|
|
3282
|
+
this.seatIndex = buildSeatIndex(this.seats, { seatRadius: this.seatR });
|
|
3283
|
+
this.bounds = chartBounds(view);
|
|
3284
|
+
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3285
|
+
this.buildPerspectiveProjection(view);
|
|
2308
3286
|
this.renderBackground(view);
|
|
3287
|
+
this.unsectionedSeatGroup = new import_Group.Group({ listening: true });
|
|
3288
|
+
this.seatLayer.add(this.unsectionedSeatGroup);
|
|
2309
3289
|
this.renderSeats();
|
|
3290
|
+
this.buildRowLabelPlan(view);
|
|
2310
3291
|
this.overlayLayer.add(this.labelGroup);
|
|
3292
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
2311
3293
|
this.overlayLayer.add(this.hoverRing);
|
|
2312
|
-
this.
|
|
2313
|
-
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3294
|
+
this.overlayLayer.add(this.focusRing);
|
|
2314
3295
|
this.zoomToFit();
|
|
2315
3296
|
}
|
|
2316
3297
|
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
2317
3298
|
* the whole chart for single-floor charts. */
|
|
2318
3299
|
floorView(doc) {
|
|
2319
|
-
if (!doc.floors || !doc.floors.length) return
|
|
2320
|
-
|
|
3300
|
+
if (!doc.floors || !doc.floors.length) return {
|
|
3301
|
+
...doc,
|
|
3302
|
+
referenceImage: void 0,
|
|
3303
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3304
|
+
};
|
|
3305
|
+
if (this.stacked && doc.floors.length >= 2) {
|
|
3306
|
+
return {
|
|
3307
|
+
...stackFloors(doc),
|
|
3308
|
+
referenceImage: void 0,
|
|
3309
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
2321
3312
|
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
2322
|
-
return {
|
|
3313
|
+
return {
|
|
3314
|
+
...doc,
|
|
3315
|
+
objects: floor.objects,
|
|
3316
|
+
focalPoint: floor.focalPoint,
|
|
3317
|
+
referenceImage: void 0,
|
|
3318
|
+
backgroundImage: buyerBackgroundImage(floor),
|
|
3319
|
+
floors: void 0
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
/** Physical floor height is authored data; the 900-unit stacked overview
|
|
3323
|
+
* spread is presentation-only and must never leak into real 3D geometry. */
|
|
3324
|
+
floorBaseHeightMFor(objectId) {
|
|
3325
|
+
const floors = this.chartDoc?.floors;
|
|
3326
|
+
if (!floors?.length) return 0;
|
|
3327
|
+
const floorId = this.stacked && objectId ? this.objectFloor.get(objectId) ?? this.activeFloorId : this.activeFloorId;
|
|
3328
|
+
return floors.find((floor) => floor.id === floorId)?.baseHeightM ?? 0;
|
|
2323
3329
|
}
|
|
2324
3330
|
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
2325
3331
|
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
@@ -2632,7 +3638,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2632
3638
|
}
|
|
2633
3639
|
const ids = [];
|
|
2634
3640
|
for (const seat of this.seats) {
|
|
2635
|
-
|
|
3641
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3642
|
+
if (rendered.x < x0 || rendered.x > x1 || rendered.y < y0 || rendered.y > y1) continue;
|
|
2636
3643
|
if (!this.isSelectable(seat.id)) continue;
|
|
2637
3644
|
ids.push(seat.id);
|
|
2638
3645
|
}
|
|
@@ -2642,9 +3649,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2642
3649
|
flashSeat(seatId, color = "#f43f5e") {
|
|
2643
3650
|
const seat = this.seatById.get(seatId);
|
|
2644
3651
|
if (!seat) return;
|
|
3652
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
2645
3653
|
const ring = new import_Circle.Circle({
|
|
2646
|
-
x:
|
|
2647
|
-
y:
|
|
3654
|
+
x: rendered.x,
|
|
3655
|
+
y: rendered.y,
|
|
2648
3656
|
radius: this.seatR,
|
|
2649
3657
|
stroke: color,
|
|
2650
3658
|
strokeWidth: 3,
|
|
@@ -2653,6 +3661,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2653
3661
|
perfectDrawEnabled: false,
|
|
2654
3662
|
shadowForStrokeEnabled: false
|
|
2655
3663
|
});
|
|
3664
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seatId) ?? 1 : 1;
|
|
3665
|
+
ring.scale({ x: projectionScale, y: projectionScale });
|
|
2656
3666
|
this.overlayLayer.add(ring);
|
|
2657
3667
|
const start = performance.now();
|
|
2658
3668
|
const dur = 620;
|
|
@@ -2679,17 +3689,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2679
3689
|
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2680
3690
|
if (!matches.length) return;
|
|
2681
3691
|
for (const section of matches) {
|
|
2682
|
-
const
|
|
3692
|
+
const outline = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
3693
|
+
const lift = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
3694
|
+
return { x: point.x + lift.x, y: point.y + lift.y };
|
|
3695
|
+
});
|
|
3696
|
+
const centre = outline.reduce(
|
|
2683
3697
|
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2684
3698
|
{ x: 0, y: 0 }
|
|
2685
3699
|
);
|
|
2686
|
-
centre.x /=
|
|
2687
|
-
centre.y /=
|
|
2688
|
-
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
3700
|
+
centre.x /= outline.length;
|
|
3701
|
+
centre.y /= outline.length;
|
|
2689
3702
|
const halo = new import_Line.Line({
|
|
2690
|
-
x: centre.x
|
|
2691
|
-
y: centre.y
|
|
2692
|
-
points:
|
|
3703
|
+
x: centre.x,
|
|
3704
|
+
y: centre.y,
|
|
3705
|
+
points: outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2693
3706
|
closed: true,
|
|
2694
3707
|
stroke: color,
|
|
2695
3708
|
strokeWidth: 3,
|
|
@@ -2733,12 +3746,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2733
3746
|
nearestSeat(fromId, dir) {
|
|
2734
3747
|
const from = this.seatById.get(fromId);
|
|
2735
3748
|
if (!from) return null;
|
|
3749
|
+
const fromPoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(from.id) ?? from : from;
|
|
2736
3750
|
let best = null;
|
|
2737
3751
|
let bestScore = Infinity;
|
|
2738
3752
|
for (const s of this.seats) {
|
|
2739
3753
|
if (s.id === fromId) continue;
|
|
2740
|
-
const
|
|
2741
|
-
const
|
|
3754
|
+
const candidatePoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(s.id) ?? s : s;
|
|
3755
|
+
const dx = candidatePoint.x - fromPoint.x;
|
|
3756
|
+
const dy = candidatePoint.y - fromPoint.y;
|
|
2742
3757
|
const proj = dx * dir.x + dy * dir.y;
|
|
2743
3758
|
if (proj <= 0.5) continue;
|
|
2744
3759
|
const perp = Math.abs(dx * dir.y - dy * dir.x);
|
|
@@ -2755,7 +3770,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2755
3770
|
if (!seat) return;
|
|
2756
3771
|
this.focusedId = id;
|
|
2757
3772
|
this.focusRing.radius(this.seatR + 3);
|
|
2758
|
-
this.focusRing.position(
|
|
3773
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
3774
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
3775
|
+
this.focusRing.scale({ x: projectionScale, y: projectionScale });
|
|
2759
3776
|
this.focusRing.visible(true);
|
|
2760
3777
|
this.ensureVisible(seat);
|
|
2761
3778
|
this.overlayLayer.batchDraw();
|
|
@@ -2770,7 +3787,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2770
3787
|
const margin = 70;
|
|
2771
3788
|
const offscreen = p.x < margin || p.x > w - margin || p.y < margin || p.y > h - margin;
|
|
2772
3789
|
if (this.stage.scaleX() < target || offscreen) {
|
|
2773
|
-
const
|
|
3790
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3791
|
+
const ip = this.viewMode === "perspective" && this.perspectiveBaseAffine ? applyAffine(this.perspectiveBaseAffine, rendered) : this.isoT === 0 ? rendered : this.isoForward(rendered);
|
|
2774
3792
|
this.stage.scale({ x: target, y: target });
|
|
2775
3793
|
this.stage.position({ x: w / 2 - ip.x * target, y: h / 2 - ip.y * target });
|
|
2776
3794
|
this.afterViewChange();
|
|
@@ -2780,7 +3798,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2780
3798
|
zoomToFit() {
|
|
2781
3799
|
const w = this.stage.width();
|
|
2782
3800
|
const h = this.stage.height();
|
|
2783
|
-
const b = this.bounds;
|
|
3801
|
+
const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
|
|
2784
3802
|
this.fitScale = Math.min(w / b.width, h / b.height) || 1;
|
|
2785
3803
|
const s = this.fitScale;
|
|
2786
3804
|
this.stage.scale({ x: s, y: s });
|
|
@@ -2809,7 +3827,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2809
3827
|
}
|
|
2810
3828
|
worldToScreen(point) {
|
|
2811
3829
|
const s = this.stage.scaleX();
|
|
2812
|
-
const
|
|
3830
|
+
const candidateId = point.id;
|
|
3831
|
+
const seat = typeof candidateId === "string" ? this.seatById.get(candidateId) : void 0;
|
|
3832
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3833
|
+
const projected = seat ? this.perspectiveSeatProjected.get(seat.id) ?? applyAffine(this.perspectiveBaseAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3834
|
+
return { x: projected.x * s + this.stage.x(), y: projected.y * s + this.stage.y() };
|
|
3835
|
+
}
|
|
3836
|
+
const localPoint = seat ? this.renderedSeatPoint(seat) : point;
|
|
3837
|
+
const p = this.isoT === 0 ? localPoint : this.isoForward(localPoint);
|
|
2813
3838
|
return { x: p.x * s + this.stage.x(), y: p.y * s + this.stage.y() };
|
|
2814
3839
|
}
|
|
2815
3840
|
setAccessibleFilter(on) {
|
|
@@ -2819,6 +3844,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2819
3844
|
const next = types === null ? null : [...types];
|
|
2820
3845
|
if (this.sameAccessFilter(next)) return;
|
|
2821
3846
|
this.accessFilter = next;
|
|
3847
|
+
this.updateAccessGlyphs(this.effScale());
|
|
2822
3848
|
for (const seat of this.seats) {
|
|
2823
3849
|
const c = this.circleById.get(seat.id);
|
|
2824
3850
|
if (c) this.paintSeat(c, seat.id);
|
|
@@ -2917,10 +3943,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2917
3943
|
let minY = Infinity;
|
|
2918
3944
|
let maxY = -Infinity;
|
|
2919
3945
|
for (const seat of matching) {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3946
|
+
const point = this.renderedSeatPoint(seat);
|
|
3947
|
+
minX = Math.min(minX, point.x);
|
|
3948
|
+
maxX = Math.max(maxX, point.x);
|
|
3949
|
+
minY = Math.min(minY, point.y);
|
|
3950
|
+
maxY = Math.max(maxY, point.y);
|
|
2924
3951
|
}
|
|
2925
3952
|
const pad = Math.max(24, this.seatR * 3);
|
|
2926
3953
|
minX -= pad;
|
|
@@ -2941,12 +3968,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2941
3968
|
* inverse) keeps landing on the projected seats/sections.
|
|
2942
3969
|
*/
|
|
2943
3970
|
setViewMode(mode) {
|
|
3971
|
+
if (mode === "perspective") {
|
|
3972
|
+
if (this.viewMode === mode) {
|
|
3973
|
+
this.afterViewChange();
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
const resetElevatedSeatGroups = this.viewMode === "isometric" && this.isoT > 0;
|
|
3977
|
+
this.viewMode = mode;
|
|
3978
|
+
this.isoTarget = 1;
|
|
3979
|
+
this.isoT = 1;
|
|
3980
|
+
if (this.isoRaf) {
|
|
3981
|
+
cancelAnimationFrame(this.isoRaf);
|
|
3982
|
+
this.isoRaf = 0;
|
|
3983
|
+
}
|
|
3984
|
+
this.applyPerspective(resetElevatedSeatGroups);
|
|
3985
|
+
this.zoomToFit();
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
2944
3988
|
const target = mode === "isometric" ? 1 : 0;
|
|
3989
|
+
const leavingPerspective = this.viewMode === "perspective";
|
|
3990
|
+
this.viewMode = mode;
|
|
2945
3991
|
this.isoTarget = target;
|
|
2946
3992
|
if (this.isoRaf) {
|
|
2947
3993
|
cancelAnimationFrame(this.isoRaf);
|
|
2948
3994
|
this.isoRaf = 0;
|
|
2949
3995
|
}
|
|
3996
|
+
if (leavingPerspective) {
|
|
3997
|
+
this.isoT = target;
|
|
3998
|
+
this.applyIso();
|
|
3999
|
+
this.afterViewChange();
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
2950
4002
|
if (this.reducedMotion) {
|
|
2951
4003
|
this.isoT = target;
|
|
2952
4004
|
this.applyIso();
|
|
@@ -2980,7 +4032,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2980
4032
|
}
|
|
2981
4033
|
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
2982
4034
|
getViewMode() {
|
|
2983
|
-
return this.
|
|
4035
|
+
return this.viewMode;
|
|
2984
4036
|
}
|
|
2985
4037
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
2986
4038
|
isoParams() {
|
|
@@ -2988,6 +4040,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2988
4040
|
}
|
|
2989
4041
|
/** Effective vertical scale = stage scale × iso squash — legibility math uses this. */
|
|
2990
4042
|
effScale() {
|
|
4043
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
4044
|
+
const xScale = Math.hypot(this.perspectiveBaseAffine.a, this.perspectiveBaseAffine.b);
|
|
4045
|
+
const yScale = Math.hypot(this.perspectiveBaseAffine.c, this.perspectiveBaseAffine.d);
|
|
4046
|
+
return this.stage.scaleX() * Math.min(xScale, yScale);
|
|
4047
|
+
}
|
|
2991
4048
|
return this.stage.scaleX() * (1 - (1 - ISO_SQUASH) * this.isoT);
|
|
2992
4049
|
}
|
|
2993
4050
|
/** Project a world point through the iso affine about the chart centre (→ iso-world). */
|
|
@@ -3011,19 +4068,113 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3011
4068
|
const wy = -dx * Math.sin(th) + uy * Math.cos(th);
|
|
3012
4069
|
return { x: c.x + wx, y: c.y + wy };
|
|
3013
4070
|
}
|
|
4071
|
+
/** Precompute the Phase-C pinhole camera and every exact seat anchor. */
|
|
4072
|
+
buildPerspectiveProjection(view) {
|
|
4073
|
+
let maxSurfaceHeightWorld = 0;
|
|
4074
|
+
for (const seat of this.seats) {
|
|
4075
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
4076
|
+
maxSurfaceHeightWorld = Math.max(maxSurfaceHeightWorld, surfaceM * CHART_UNITS_PER_METRE);
|
|
4077
|
+
}
|
|
4078
|
+
for (const object of view.objects) {
|
|
4079
|
+
if (object.type !== "section") continue;
|
|
4080
|
+
maxSurfaceHeightWorld = Math.max(
|
|
4081
|
+
maxSurfaceHeightWorld,
|
|
4082
|
+
sectionGeometry(object, { floorBaseHeightM: this.floorBaseHeightMFor(object.id) }).height * CHART_UNITS_PER_METRE
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
const camera = createPerspectiveCamera(this.bounds, maxSurfaceHeightWorld);
|
|
4086
|
+
const base = perspectiveTangentAffine(camera, camera.target);
|
|
4087
|
+
const inverse = invertAffine(base);
|
|
4088
|
+
this.perspectiveCamera = camera;
|
|
4089
|
+
this.perspectiveBaseAffine = base;
|
|
4090
|
+
this.perspectiveBaseInverse = inverse;
|
|
4091
|
+
this.perspectiveSeatLocal.clear();
|
|
4092
|
+
this.perspectiveSeatProjected.clear();
|
|
4093
|
+
this.perspectiveSeatDepth.clear();
|
|
4094
|
+
this.perspectiveSeatScale.clear();
|
|
4095
|
+
this.perspectiveBounds = null;
|
|
4096
|
+
let minX = Infinity;
|
|
4097
|
+
let minY = Infinity;
|
|
4098
|
+
let maxX = -Infinity;
|
|
4099
|
+
let maxY = -Infinity;
|
|
4100
|
+
for (const seat of this.seats) {
|
|
4101
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
4102
|
+
const projected = projectPerspectivePoint(camera, seat, surfaceM * CHART_UNITS_PER_METRE);
|
|
4103
|
+
const point = { x: projected.x, y: projected.y };
|
|
4104
|
+
this.perspectiveSeatProjected.set(seat.id, point);
|
|
4105
|
+
this.perspectiveSeatLocal.set(seat.id, applyAffine(inverse, point));
|
|
4106
|
+
this.perspectiveSeatDepth.set(seat.id, projected.depth);
|
|
4107
|
+
this.perspectiveSeatScale.set(seat.id, projected.scale);
|
|
4108
|
+
minX = Math.min(minX, projected.x);
|
|
4109
|
+
minY = Math.min(minY, projected.y);
|
|
4110
|
+
maxX = Math.max(maxX, projected.x);
|
|
4111
|
+
maxY = Math.max(maxY, projected.y);
|
|
4112
|
+
}
|
|
4113
|
+
for (const point of [
|
|
4114
|
+
{ x: this.bounds.x, y: this.bounds.y },
|
|
4115
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y },
|
|
4116
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y + this.bounds.height },
|
|
4117
|
+
{ x: this.bounds.x, y: this.bounds.y + this.bounds.height }
|
|
4118
|
+
]) {
|
|
4119
|
+
const projected = projectPerspectivePoint(camera, point, 0);
|
|
4120
|
+
minX = Math.min(minX, projected.x);
|
|
4121
|
+
minY = Math.min(minY, projected.y);
|
|
4122
|
+
maxX = Math.max(maxX, projected.x);
|
|
4123
|
+
maxY = Math.max(maxY, projected.y);
|
|
4124
|
+
}
|
|
4125
|
+
const pad = Math.max(24, this.seatR * 3);
|
|
4126
|
+
this.perspectiveBounds = Number.isFinite(minX) ? { x: minX - pad, y: minY - pad, width: Math.max(1, maxX - minX + pad * 2), height: Math.max(1, maxY - minY + pad * 2) } : null;
|
|
4127
|
+
}
|
|
4128
|
+
/** Apply an affine to a Konva container while keeping `pivot` fixed logically. */
|
|
4129
|
+
setContainerAffine(node, affine, pivot) {
|
|
4130
|
+
const decomposed = decomposeAffineLinear(affine);
|
|
4131
|
+
node.offset(pivot);
|
|
4132
|
+
node.position(applyAffine(affine, pivot));
|
|
4133
|
+
node.rotation(decomposed.rotationDeg);
|
|
4134
|
+
node.scale({ x: decomposed.scaleX, y: decomposed.scaleY });
|
|
4135
|
+
node.skewX(decomposed.skewX);
|
|
4136
|
+
node.skewY(0);
|
|
4137
|
+
}
|
|
4138
|
+
resetContainerTransform(node) {
|
|
4139
|
+
node.position({ x: 0, y: 0 });
|
|
4140
|
+
node.offset({ x: 0, y: 0 });
|
|
4141
|
+
node.scale({ x: 1, y: 1 });
|
|
4142
|
+
node.rotation(0);
|
|
4143
|
+
node.skewX(0);
|
|
4144
|
+
node.skewY(0);
|
|
4145
|
+
}
|
|
4146
|
+
/** Exact raked surface height used for the bounded section tangent plane. */
|
|
4147
|
+
sectionSurfaceHeight(section, point) {
|
|
4148
|
+
const radial = Math.hypot(point.x - section.surfaceFocal.x, point.y - section.surfaceFocal.y);
|
|
4149
|
+
const depth = Math.max(0, radial - section.frontDistanceWorld);
|
|
4150
|
+
return section.liftWorld + depth * Math.tan(section.rakeDeg * Math.PI / 180);
|
|
4151
|
+
}
|
|
4152
|
+
/** Desired perspective-space point for a section surface, before Stage pan/zoom. */
|
|
4153
|
+
projectedSectionPoint(section, point) {
|
|
4154
|
+
return section.perspectiveAffine ? applyAffine(section.perspectiveAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
4155
|
+
}
|
|
4156
|
+
/** Shared-layer local point which the ground affine paints at `projected`. */
|
|
4157
|
+
perspectiveLocalPoint(projected) {
|
|
4158
|
+
return this.perspectiveBaseInverse ? applyAffine(this.perspectiveBaseInverse, projected) : projected;
|
|
4159
|
+
}
|
|
3014
4160
|
/**
|
|
3015
4161
|
* Local-space offset that, once the layer applies the iso affine, lifts an
|
|
3016
|
-
* object straight UP in iso-world by `
|
|
3017
|
-
* (= inverse-linear of the pure vertical lift). Zero at isoT=0.
|
|
4162
|
+
* object straight UP in iso-world by `liftWorld × isoT` world units
|
|
4163
|
+
* (= inverse-linear of the pure vertical lift). Zero at isoT=0. `liftWorld` is
|
|
4164
|
+
* the section's resolved real height in world units (Phase B1).
|
|
3018
4165
|
*/
|
|
3019
|
-
isoLiftLocal(
|
|
4166
|
+
isoLiftLocal(liftWorld) {
|
|
3020
4167
|
const { th, sg } = this.isoParams();
|
|
3021
|
-
const delta =
|
|
4168
|
+
const delta = liftWorld * this.isoT;
|
|
3022
4169
|
return { x: -(delta / sg) * Math.sin(th), y: -(delta / sg) * Math.cos(th) };
|
|
3023
4170
|
}
|
|
3024
|
-
/** Restore
|
|
3025
|
-
|
|
3026
|
-
|
|
4171
|
+
/** Restore projection layers to identity (flat) — byte-for-byte the original.
|
|
4172
|
+
* `seatLayer` can be skipped when entering perspective from flat: assigning a
|
|
4173
|
+
* top-level transform invalidates all 14k descendant absolute transforms even
|
|
4174
|
+
* while the semantic overview keeps that layer fully transparent. */
|
|
4175
|
+
resetLayerTransforms(includeSeatLayer = true) {
|
|
4176
|
+
const layers = includeSeatLayer ? [this.bgLayer, this.seatLayer, this.overlayLayer] : [this.bgLayer, this.overlayLayer];
|
|
4177
|
+
for (const layer of layers) {
|
|
3027
4178
|
layer.position({ x: 0, y: 0 });
|
|
3028
4179
|
layer.offset({ x: 0, y: 0 });
|
|
3029
4180
|
layer.scale({ x: 1, y: 1 });
|
|
@@ -3032,6 +4183,117 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3032
4183
|
layer.skewY(0);
|
|
3033
4184
|
}
|
|
3034
4185
|
}
|
|
4186
|
+
/** Restore section-owned containers before applying another projection. */
|
|
4187
|
+
resetSectionProjection(resetSeatGroups = true) {
|
|
4188
|
+
for (const section of this.sections) {
|
|
4189
|
+
this.resetContainerTransform(section.rootGroupBg);
|
|
4190
|
+
this.resetContainerTransform(section.liftGroupBg);
|
|
4191
|
+
if (resetSeatGroups) this.resetContainerTransform(section.liftGroupSeat);
|
|
4192
|
+
section.rootGroupBg.zIndex(section.bgZIndex);
|
|
4193
|
+
if (resetSeatGroups) section.liftGroupSeat.zIndex(section.seatZIndex);
|
|
4194
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
4195
|
+
if (labelGroup && resetSeatGroups) this.resetContainerTransform(labelGroup);
|
|
4196
|
+
section.perspectiveAffine = void 0;
|
|
4197
|
+
section.perspectiveCorrection = void 0;
|
|
4198
|
+
section.perspectiveDepth = void 0;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
/** Move native seat/booth hit shapes to or from their exact pinhole anchors. */
|
|
4202
|
+
applyExactSeatAnchors(perspective, seatIds) {
|
|
4203
|
+
const seats = seatIds ? [...seatIds].map((id) => this.seatById.get(id)).filter((seat) => !!seat) : perspective ? this.seats : [...this.perspectiveAppliedSeats].map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
4204
|
+
for (const seat of seats) {
|
|
4205
|
+
if (perspective && this.perspectiveAppliedSeats.has(seat.id)) continue;
|
|
4206
|
+
const point = perspective ? this.perspectiveSeatProjected.get(seat.id) ?? seat : seat;
|
|
4207
|
+
const scale = perspective ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4208
|
+
const shape = this.circleById.get(seat.id);
|
|
4209
|
+
shape?.position(point);
|
|
4210
|
+
shape?.scale({ x: scale, y: scale });
|
|
4211
|
+
const ring = this.accessRingById.get(seat.id);
|
|
4212
|
+
ring?.position(point);
|
|
4213
|
+
ring?.scale({ x: scale, y: scale });
|
|
4214
|
+
const glyph = this.accessGlyphById.get(seat.id);
|
|
4215
|
+
if (glyph) {
|
|
4216
|
+
const baseGlyphScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
|
|
4217
|
+
glyph.position(point);
|
|
4218
|
+
glyph.scale({ x: baseGlyphScale * scale, y: baseGlyphScale * scale });
|
|
4219
|
+
}
|
|
4220
|
+
const boothLabel = this.boothLabelById.get(seat.id);
|
|
4221
|
+
boothLabel?.position(point);
|
|
4222
|
+
boothLabel?.scale({ x: scale, y: scale });
|
|
4223
|
+
if (perspective) this.perspectiveAppliedSeats.add(seat.id);
|
|
4224
|
+
}
|
|
4225
|
+
if (!perspective) this.perspectiveAppliedSeats.clear();
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* Phase C projected 2.5D. Seat/booth anchors are exact pinhole projections;
|
|
4229
|
+
* section top surfaces use one explicitly bounded tangent plane per authored
|
|
4230
|
+
* section. Native Konva shapes move with the anchors, so direct hit testing is
|
|
4231
|
+
* the same graph that paints the buyer-visible unit.
|
|
4232
|
+
*/
|
|
4233
|
+
applyPerspective(resetElevatedSeatGroups = true) {
|
|
4234
|
+
const camera = this.perspectiveCamera;
|
|
4235
|
+
const base = this.perspectiveBaseAffine;
|
|
4236
|
+
const inverse = this.perspectiveBaseInverse;
|
|
4237
|
+
if (!camera || !base || !inverse) return;
|
|
4238
|
+
const lazySectionSeats = this.seats.length > 2500 && this.sections.length > 0;
|
|
4239
|
+
if (this.cached && !lazySectionSeats) {
|
|
4240
|
+
this.seatLayer.clearCache();
|
|
4241
|
+
this.seatLayer.listening(true);
|
|
4242
|
+
this.cached = false;
|
|
4243
|
+
}
|
|
4244
|
+
this.resetLayerTransforms(resetElevatedSeatGroups);
|
|
4245
|
+
this.resetSectionProjection(resetElevatedSeatGroups);
|
|
4246
|
+
for (const layer of [this.bgLayer, this.overlayLayer]) {
|
|
4247
|
+
this.setContainerAffine(layer, base, camera.target);
|
|
4248
|
+
}
|
|
4249
|
+
this.perspectiveAppliedSeats.clear();
|
|
4250
|
+
if (!lazySectionSeats) this.applyExactSeatAnchors(true);
|
|
4251
|
+
for (const section of this.sections) {
|
|
4252
|
+
const heightAt = (point) => this.sectionSurfaceHeight(section, point);
|
|
4253
|
+
const desired = perspectiveTangentAffine(camera, section.centroid, heightAt);
|
|
4254
|
+
const correction = composeAffine(inverse, desired);
|
|
4255
|
+
section.perspectiveAffine = desired;
|
|
4256
|
+
section.perspectiveCorrection = correction;
|
|
4257
|
+
section.perspectiveDepth = projectPerspectivePoint(
|
|
4258
|
+
camera,
|
|
4259
|
+
section.centroid,
|
|
4260
|
+
heightAt(section.centroid)
|
|
4261
|
+
).depth;
|
|
4262
|
+
this.setContainerAffine(section.liftGroupBg, correction, section.centroid);
|
|
4263
|
+
for (let index = 0; index < section.sideFaces.length; index++) {
|
|
4264
|
+
const p0 = section.outline[index];
|
|
4265
|
+
const p1 = section.outline[(index + 1) % section.outline.length];
|
|
4266
|
+
const base0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, 0));
|
|
4267
|
+
const base1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, 0));
|
|
4268
|
+
const top1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, heightAt(p1)));
|
|
4269
|
+
const top0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, heightAt(p0)));
|
|
4270
|
+
section.sideFaces[index].points([
|
|
4271
|
+
base0.x,
|
|
4272
|
+
base0.y,
|
|
4273
|
+
base1.x,
|
|
4274
|
+
base1.y,
|
|
4275
|
+
top1.x,
|
|
4276
|
+
top1.y,
|
|
4277
|
+
top0.x,
|
|
4278
|
+
top0.y
|
|
4279
|
+
]);
|
|
4280
|
+
section.sideFaces[index].opacity(0.9);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
const farToNear = [...this.sections].sort(
|
|
4284
|
+
(left, right) => (right.perspectiveDepth ?? 0) - (left.perspectiveDepth ?? 0)
|
|
4285
|
+
);
|
|
4286
|
+
for (const section of farToNear) {
|
|
4287
|
+
section.rootGroupBg.moveToTop();
|
|
4288
|
+
section.liftGroupSeat.moveToTop();
|
|
4289
|
+
}
|
|
4290
|
+
this.unsectionedSeatGroup?.moveToTop();
|
|
4291
|
+
this.syncSeatOverlayPositions();
|
|
4292
|
+
if (!lazySectionSeats) this.updateSeatGroupVisibility();
|
|
4293
|
+
this.bgLayer.batchDraw();
|
|
4294
|
+
this.seatLayer.batchDraw();
|
|
4295
|
+
this.overlayLayer.batchDraw();
|
|
4296
|
+
}
|
|
3035
4297
|
/**
|
|
3036
4298
|
* Apply the current isoT to the scene: the base rotate+squash as a decomposed
|
|
3037
4299
|
* layer transform (so seats/décor/rings project together and Konva's own
|
|
@@ -3039,6 +4301,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3039
4301
|
* lift + extruded side faces on elevated sections.
|
|
3040
4302
|
*/
|
|
3041
4303
|
applyIso() {
|
|
4304
|
+
this.resetSectionProjection();
|
|
4305
|
+
this.applyExactSeatAnchors(false);
|
|
3042
4306
|
const t2 = this.isoT;
|
|
3043
4307
|
if (t2 === 0) {
|
|
3044
4308
|
this.resetLayerTransforms();
|
|
@@ -3067,6 +4331,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3067
4331
|
}
|
|
3068
4332
|
this.applyUprightLabels();
|
|
3069
4333
|
this.applyElevation();
|
|
4334
|
+
this.updateSeatGroupVisibility();
|
|
3070
4335
|
this.bgLayer.batchDraw();
|
|
3071
4336
|
this.seatLayer.batchDraw();
|
|
3072
4337
|
this.overlayLayer.batchDraw();
|
|
@@ -3095,10 +4360,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3095
4360
|
applyElevation() {
|
|
3096
4361
|
const t2 = this.isoT;
|
|
3097
4362
|
for (const sec of this.sections) {
|
|
3098
|
-
if (sec.
|
|
3099
|
-
const off = this.isoLiftLocal(sec.
|
|
4363
|
+
if (sec.liftWorld <= 0) continue;
|
|
4364
|
+
const off = this.isoLiftLocal(sec.liftWorld);
|
|
3100
4365
|
sec.liftGroupBg?.position(off);
|
|
3101
|
-
sec.liftGroupSeat
|
|
4366
|
+
sec.liftGroupSeat.position(off);
|
|
4367
|
+
this.labelLiftGroups.get(sec.id)?.position(off);
|
|
3102
4368
|
const alpha = 0.9 * t2;
|
|
3103
4369
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
3104
4370
|
const face = sec.sideFaces[i];
|
|
@@ -3108,6 +4374,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3108
4374
|
face.opacity(alpha);
|
|
3109
4375
|
}
|
|
3110
4376
|
}
|
|
4377
|
+
this.syncSeatOverlayPositions();
|
|
3111
4378
|
}
|
|
3112
4379
|
destroy() {
|
|
3113
4380
|
this.destroyed = true;
|
|
@@ -3134,16 +4401,166 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3134
4401
|
* tier shifts with one offset in iso view) or the seat layer directly.
|
|
3135
4402
|
*/
|
|
3136
4403
|
seatContainer(id) {
|
|
3137
|
-
return this.seatSection.get(id)?.liftGroupSeat ?? this.seatLayer;
|
|
4404
|
+
return this.seatSection.get(id)?.liftGroupSeat ?? this.unsectionedSeatGroup ?? this.seatLayer;
|
|
3138
4405
|
}
|
|
3139
|
-
|
|
4406
|
+
/** True when a section belongs to the active logical section/zone focus. */
|
|
4407
|
+
sectionMatchesFocus(section) {
|
|
4408
|
+
const focus = this.focusedSectionId;
|
|
4409
|
+
return focus == null || section.id === focus || section.logicalId === focus || section.zone === focus;
|
|
4410
|
+
}
|
|
4411
|
+
/** Visual opacity after the section-focus overlay is composed. */
|
|
4412
|
+
focusedSeatOpacity(id, localOpacity) {
|
|
4413
|
+
if (!this.focusedSectionId) return localOpacity;
|
|
4414
|
+
const section = this.seatSection.get(id);
|
|
4415
|
+
return localOpacity * (section && this.sectionMatchesFocus(section) ? 1 : FOCUS_DIM_OPACITY);
|
|
4416
|
+
}
|
|
4417
|
+
/** Project a section outline through elevation + the active affine view and
|
|
4418
|
+
* then into viewport pixels. The result is conservative (AABB), so a visible
|
|
4419
|
+
* section is never clipped even for rotated or isometric outlines. */
|
|
4420
|
+
sectionScreenBounds(section) {
|
|
4421
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
4422
|
+
const scale2 = this.stage.scaleX();
|
|
4423
|
+
const stageX2 = this.stage.x();
|
|
4424
|
+
const stageY2 = this.stage.y();
|
|
4425
|
+
return polyBounds(section.outline.map((point) => {
|
|
4426
|
+
const projected = this.projectedSectionPoint(section, point);
|
|
4427
|
+
return { x: projected.x * scale2 + stageX2, y: projected.y * scale2 + stageY2 };
|
|
4428
|
+
}));
|
|
4429
|
+
}
|
|
4430
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4431
|
+
const scale = this.stage.scaleX();
|
|
4432
|
+
const stageX = this.stage.x();
|
|
4433
|
+
const stageY = this.stage.y();
|
|
4434
|
+
return polyBounds(section.outline.map((point) => {
|
|
4435
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
4436
|
+
const projected = this.isoT === 0 ? local : this.isoForward(local);
|
|
4437
|
+
return { x: projected.x * scale + stageX, y: projected.y * scale + stageY };
|
|
4438
|
+
}));
|
|
4439
|
+
}
|
|
4440
|
+
/** Hide only section groups wholly outside a padded viewport at the live-seat
|
|
4441
|
+
* rung. Overview caching always restores all groups first, so panning a
|
|
4442
|
+
* cached whole-venue bitmap can never reveal missing inventory. */
|
|
4443
|
+
updateSeatGroupVisibility() {
|
|
4444
|
+
const liveSeats = this.effScale() >= CACHE_THRESHOLD;
|
|
4445
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
4446
|
+
const padding = 96;
|
|
4447
|
+
const width = this.stage.width();
|
|
4448
|
+
const height = this.stage.height();
|
|
4449
|
+
for (const section of this.sections) {
|
|
4450
|
+
let visible = true;
|
|
4451
|
+
if (liveSeats) {
|
|
4452
|
+
const bounds2 = this.sectionScreenBounds(section);
|
|
4453
|
+
visible = bounds2.x + bounds2.width >= -padding && bounds2.y + bounds2.height >= -padding && bounds2.x <= width + padding && bounds2.y <= height + padding;
|
|
4454
|
+
if (visible && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4455
|
+
this.applyExactSeatAnchors(true, section.memberIds);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
section.liftGroupSeat.setViewportCulled(!visible);
|
|
4459
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
4460
|
+
labelGroup?.setViewportCulled(!visible);
|
|
4461
|
+
}
|
|
4462
|
+
if (this.unsectionedSeatGroup && !this.unsectionedSeatGroup.visible()) {
|
|
4463
|
+
this.unsectionedSeatGroup.visible(true);
|
|
4464
|
+
}
|
|
4465
|
+
if (liveSeats && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4466
|
+
this.applyExactSeatAnchors(true, this.seats.filter((seat) => !this.seatSection.has(seat.id)).map((seat) => seat.id));
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
/** Seats worth considering for viewport labels. Section culling is a safe
|
|
4470
|
+
* coarse index; the later screen test remains the exact filter. */
|
|
4471
|
+
visibleSeatCandidates() {
|
|
4472
|
+
if (!this.sections.length) return this.seats;
|
|
4473
|
+
const candidates = [];
|
|
4474
|
+
for (const section of this.sections) {
|
|
4475
|
+
if (section.liftGroupSeat.isViewportCulled()) continue;
|
|
4476
|
+
for (const id of section.memberIds) {
|
|
4477
|
+
const seat = this.seatById.get(id);
|
|
4478
|
+
if (seat) candidates.push(seat);
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
3140
4481
|
for (const seat of this.seats) {
|
|
4482
|
+
if (!this.seatSection.has(seat.id)) candidates.push(seat);
|
|
4483
|
+
}
|
|
4484
|
+
return candidates;
|
|
4485
|
+
}
|
|
4486
|
+
seatViewportCulled(id) {
|
|
4487
|
+
return this.seatSection.get(id)?.liftGroupSeat.isViewportCulled() ?? false;
|
|
4488
|
+
}
|
|
4489
|
+
/** Local world coordinate at which an elevated seat is actually painted for
|
|
4490
|
+
* the current iso tween. Flat seats and the 2D endpoint are unchanged. */
|
|
4491
|
+
renderedSeatPoint(seat) {
|
|
4492
|
+
if (this.viewMode === "perspective") return this.perspectiveSeatLocal.get(seat.id) ?? seat;
|
|
4493
|
+
const section = this.seatSection.get(seat.id);
|
|
4494
|
+
if (!section || section.liftWorld <= 0 || this.isoT === 0) return seat;
|
|
4495
|
+
const offset = this.isoLiftLocal(section.liftWorld);
|
|
4496
|
+
return { x: seat.x + offset.x, y: seat.y + offset.y };
|
|
4497
|
+
}
|
|
4498
|
+
/** Overlay labels mirror the seat-layer lift without an O(seats) per-frame
|
|
4499
|
+
* rewrite. The groups are rebuilt with the zoom-dependent label set. */
|
|
4500
|
+
seatLabelContainer(id) {
|
|
4501
|
+
const section = this.seatSection.get(id);
|
|
4502
|
+
if (!section) return this.labelGroup;
|
|
4503
|
+
let group = this.labelLiftGroups.get(section.id);
|
|
4504
|
+
if (!group) {
|
|
4505
|
+
group = new ViewportGroup({
|
|
4506
|
+
listening: false
|
|
4507
|
+
});
|
|
4508
|
+
group.setViewportCulled(section.liftGroupSeat.isViewportCulled());
|
|
4509
|
+
group.position(this.viewMode === "perspective" ? { x: 0, y: 0 } : this.isoLiftLocal(section.liftWorld));
|
|
4510
|
+
this.labelGroup.add(group);
|
|
4511
|
+
this.labelLiftGroups.set(section.id, group);
|
|
4512
|
+
}
|
|
4513
|
+
return group;
|
|
4514
|
+
}
|
|
4515
|
+
/** Shared overlay furniture is not parented to section lift groups, so keep
|
|
4516
|
+
* the small transient set aligned explicitly. Selection is capped in buyer
|
|
4517
|
+
* mode, making this constant-sized work during the iso tween. */
|
|
4518
|
+
syncSeatOverlayPositions() {
|
|
4519
|
+
if (this.hoveredId) {
|
|
4520
|
+
const seat = this.seatById.get(this.hoveredId);
|
|
4521
|
+
if (seat) {
|
|
4522
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
4523
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4524
|
+
this.hoverRing.scale({ x: scale, y: scale });
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
if (this.focusedId) {
|
|
4528
|
+
const seat = this.seatById.get(this.focusedId);
|
|
4529
|
+
if (seat) {
|
|
4530
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
4531
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4532
|
+
this.focusRing.scale({ x: scale, y: scale });
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
for (const [id, marker] of this.selectionMarkers) {
|
|
4536
|
+
const seat = this.seatById.get(id);
|
|
4537
|
+
if (seat) {
|
|
4538
|
+
marker.position(this.renderedSeatPoint(seat));
|
|
4539
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4540
|
+
marker.scale({ x: scale, y: scale });
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
renderSeats() {
|
|
4545
|
+
const paintOrder = [...this.seats].sort((left, right) => (this.perspectiveSeatDepth.get(right.id) ?? 0) - (this.perspectiveSeatDepth.get(left.id) ?? 0));
|
|
4546
|
+
for (const seat of paintOrder) {
|
|
3141
4547
|
if (seat.kind === "booth") {
|
|
3142
4548
|
this.renderBoothUnit(seat);
|
|
3143
4549
|
continue;
|
|
3144
4550
|
}
|
|
3145
4551
|
const target = this.seatContainer(seat.id);
|
|
3146
|
-
const c = new
|
|
4552
|
+
const c = seat.wheelchairSpaceType === "no-seat" ? new import_Rect.Rect({
|
|
4553
|
+
x: seat.x,
|
|
4554
|
+
y: seat.y,
|
|
4555
|
+
width: this.seatR * 2,
|
|
4556
|
+
height: this.seatR * 2,
|
|
4557
|
+
offsetX: this.seatR,
|
|
4558
|
+
offsetY: this.seatR,
|
|
4559
|
+
cornerRadius: 2,
|
|
4560
|
+
perfectDrawEnabled: false,
|
|
4561
|
+
shadowForStrokeEnabled: false,
|
|
4562
|
+
hitStrokeWidth: 0
|
|
4563
|
+
}) : new import_Circle.Circle({
|
|
3147
4564
|
x: seat.x,
|
|
3148
4565
|
y: seat.y,
|
|
3149
4566
|
radius: this.seatR,
|
|
@@ -3156,21 +4573,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3156
4573
|
this.paintSeat(c, seat.id);
|
|
3157
4574
|
target.add(c);
|
|
3158
4575
|
if (seat.accessible) {
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
4576
|
+
if (seat.wheelchairSpaceType !== "no-seat") {
|
|
4577
|
+
const ring = new import_Circle.Circle({
|
|
4578
|
+
x: seat.x,
|
|
4579
|
+
y: seat.y,
|
|
4580
|
+
radius: this.seatR + 1.5,
|
|
4581
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
4582
|
+
strokeWidth: 2.5,
|
|
4583
|
+
listening: false,
|
|
4584
|
+
perfectDrawEnabled: false,
|
|
4585
|
+
shadowForStrokeEnabled: false
|
|
4586
|
+
});
|
|
4587
|
+
this.accessRingById.set(seat.id, ring);
|
|
4588
|
+
target.add(ring);
|
|
4589
|
+
}
|
|
4590
|
+
if (seat.accessibility?.includes("wheelchair")) {
|
|
4591
|
+
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
4592
|
+
this.accessGlyphById.set(seat.id, glyph);
|
|
4593
|
+
target.add(glyph);
|
|
4594
|
+
}
|
|
3174
4595
|
}
|
|
3175
4596
|
}
|
|
3176
4597
|
}
|
|
@@ -3178,25 +4599,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3178
4599
|
renderBoothUnit(seat) {
|
|
3179
4600
|
const target = this.seatContainer(seat.id);
|
|
3180
4601
|
const dims = this.boothDims.get(seat.rowId) ?? { width: 40, height: 30, rotation: 0 };
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
4602
|
+
let labelX = seat.x;
|
|
4603
|
+
let labelY = seat.y;
|
|
4604
|
+
let node;
|
|
4605
|
+
if (dims.points && dims.points.length >= 3) {
|
|
4606
|
+
node = new import_Line.Line({
|
|
4607
|
+
x: seat.x,
|
|
4608
|
+
y: seat.y,
|
|
4609
|
+
points: dims.points.flatMap((p) => [p.x, p.y]),
|
|
4610
|
+
closed: true,
|
|
4611
|
+
perfectDrawEnabled: false,
|
|
4612
|
+
shadowForStrokeEnabled: false,
|
|
4613
|
+
hitStrokeWidth: 0
|
|
4614
|
+
});
|
|
4615
|
+
labelX = seat.x + dims.points.reduce((a, p) => a + p.x, 0) / dims.points.length;
|
|
4616
|
+
labelY = seat.y + dims.points.reduce((a, p) => a + p.y, 0) / dims.points.length;
|
|
4617
|
+
} else {
|
|
4618
|
+
node = new import_Rect.Rect({
|
|
4619
|
+
x: seat.x,
|
|
4620
|
+
y: seat.y,
|
|
4621
|
+
width: dims.width,
|
|
4622
|
+
height: dims.height,
|
|
4623
|
+
offsetX: dims.width / 2,
|
|
4624
|
+
offsetY: dims.height / 2,
|
|
4625
|
+
rotation: dims.rotation,
|
|
4626
|
+
cornerRadius: 4,
|
|
4627
|
+
perfectDrawEnabled: false,
|
|
4628
|
+
shadowForStrokeEnabled: false,
|
|
4629
|
+
hitStrokeWidth: 0
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
node.setAttr("seatId", seat.id);
|
|
4633
|
+
this.circleById.set(seat.id, node);
|
|
4634
|
+
target.add(node);
|
|
3197
4635
|
const t2 = new import_Text.Text({
|
|
3198
|
-
x:
|
|
3199
|
-
y:
|
|
4636
|
+
x: labelX,
|
|
4637
|
+
y: labelY,
|
|
3200
4638
|
text: seat.displayLabel ?? seat.label,
|
|
3201
4639
|
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
3202
4640
|
fontStyle: "600",
|
|
@@ -3208,11 +4646,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3208
4646
|
t2.offsetX(t2.width() / 2);
|
|
3209
4647
|
t2.offsetY(t2.height() / 2);
|
|
3210
4648
|
t2.visible(false);
|
|
3211
|
-
this.boothLabelById.set(seat.id, t2);
|
|
3212
4649
|
this.hasBoothText = true;
|
|
3213
4650
|
this.boothLabelById.set(seat.id, t2);
|
|
3214
4651
|
target.add(t2);
|
|
3215
|
-
this.paintSeat(
|
|
4652
|
+
this.paintSeat(node, seat.id);
|
|
3216
4653
|
}
|
|
3217
4654
|
/**
|
|
3218
4655
|
* Build the centred accessibility glyph for a seat. Sized relative to the seat
|
|
@@ -3231,24 +4668,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3231
4668
|
scaleY: k,
|
|
3232
4669
|
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
3233
4670
|
listening: false,
|
|
3234
|
-
visible:
|
|
4671
|
+
visible: false,
|
|
3235
4672
|
perfectDrawEnabled: false,
|
|
3236
4673
|
shadowForStrokeEnabled: false
|
|
3237
4674
|
});
|
|
3238
4675
|
}
|
|
3239
4676
|
/**
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
* never the full node graph, so it is cheap to call on every view change.
|
|
4677
|
+
* Keep physical wheelchair provision readable in screen space at every map
|
|
4678
|
+
* LOD. The active Wheelchair filter promotes it further. Iterates the small
|
|
4679
|
+
* wheelchair-seat set, never the full node graph.
|
|
3244
4680
|
*/
|
|
3245
4681
|
updateAccessGlyphs(scale) {
|
|
3246
4682
|
if (!this.accessGlyphById.size) return;
|
|
3247
|
-
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
3248
4683
|
for (const [id, glyph] of this.accessGlyphById) {
|
|
3249
|
-
|
|
4684
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
4685
|
+
const baseScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX * perspectiveScale;
|
|
4686
|
+
const emphasized = this.accessGlyphFilterEmphasized(id);
|
|
4687
|
+
const minimumScreenPx = emphasized ? FILTERED_WHEELCHAIR_GLYPH_MIN_PX : WHEELCHAIR_GLYPH_MIN_PX;
|
|
4688
|
+
const glyphScale = Math.max(
|
|
4689
|
+
baseScale,
|
|
4690
|
+
minimumScreenPx / (ACCESS_GLYPH_VIEWBOX * Math.max(scale, 1e-3))
|
|
4691
|
+
);
|
|
4692
|
+
glyph.scale({ x: glyphScale, y: glyphScale });
|
|
4693
|
+
glyph.visible(this.accessGlyphShouldShow(id));
|
|
3250
4694
|
}
|
|
3251
4695
|
}
|
|
4696
|
+
/** True only for a physical wheelchair provision matching an active buyer filter. */
|
|
4697
|
+
accessGlyphFilterEmphasized(id) {
|
|
4698
|
+
const seat = this.seatById.get(id);
|
|
4699
|
+
const filter = this.accessFilter;
|
|
4700
|
+
return !!seat && filter !== null && !!seat.accessibility?.includes("wheelchair") && (filter.length === 0 || filter.includes("wheelchair")) && seatMatchesAccess(seat, filter);
|
|
4701
|
+
}
|
|
4702
|
+
accessGlyphShouldShow(id) {
|
|
4703
|
+
return this.accessGlyphById.has(id) && this.accessGlyphEligible(id);
|
|
4704
|
+
}
|
|
3252
4705
|
/**
|
|
3253
4706
|
* Whether an accessible seat's glyph should show for its current status. It is
|
|
3254
4707
|
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
@@ -3264,8 +4717,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3264
4717
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
3265
4718
|
seatBaseColor(categoryKey) {
|
|
3266
4719
|
if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
|
|
3267
|
-
|
|
3268
|
-
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
4720
|
+
return this.cbHueByKey.get(categoryKey) ?? CB_PALETTE[0];
|
|
3269
4721
|
}
|
|
3270
4722
|
/** Per-seat authored label size relative to the neutral default (1 = default),
|
|
3271
4723
|
* mirroring the section convention (labelStyle.size ?? 18) / 18. Only real
|
|
@@ -3328,6 +4780,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3328
4780
|
c.dash([2, 2]);
|
|
3329
4781
|
break;
|
|
3330
4782
|
}
|
|
4783
|
+
if (seat.wheelchairSpaceType === "no-seat" && status === "free") {
|
|
4784
|
+
c.stroke(accessibilityRingColor(seat.accessibility));
|
|
4785
|
+
c.strokeWidth(selected ? 3 : 2);
|
|
4786
|
+
c.dash([3, 2]);
|
|
4787
|
+
}
|
|
3331
4788
|
if (boothLabel) {
|
|
3332
4789
|
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? "HELD" : seat.label);
|
|
3333
4790
|
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
@@ -3361,11 +4818,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3361
4818
|
c.dash([]);
|
|
3362
4819
|
c.opacity(CLOSED_SEAT_OPACITY);
|
|
3363
4820
|
}
|
|
3364
|
-
if (this.focusedSectionId) {
|
|
3365
|
-
const sec = this.seatSection.get(id);
|
|
3366
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
3367
|
-
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
3368
|
-
}
|
|
3369
4821
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
3370
4822
|
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
3371
4823
|
if (bookableLabel) {
|
|
@@ -3379,7 +4831,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3379
4831
|
const fill = c.fill();
|
|
3380
4832
|
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
3381
4833
|
accessGlyph.opacity(c.opacity());
|
|
3382
|
-
accessGlyph.visible(this.
|
|
4834
|
+
accessGlyph.visible(this.accessGlyphShouldShow(id));
|
|
3383
4835
|
}
|
|
3384
4836
|
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
3385
4837
|
}
|
|
@@ -3448,7 +4900,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3448
4900
|
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
3449
4901
|
this.focusedSectionId = id;
|
|
3450
4902
|
this.drawFocusBackdrop(id);
|
|
3451
|
-
this.
|
|
4903
|
+
this.bgLayer.batchDraw();
|
|
4904
|
+
this.overlayLayer.batchDraw();
|
|
3452
4905
|
this.updateLOD();
|
|
3453
4906
|
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
3454
4907
|
}
|
|
@@ -3460,7 +4913,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3460
4913
|
this.focusBackdrop.destroy();
|
|
3461
4914
|
this.focusBackdrop = null;
|
|
3462
4915
|
}
|
|
3463
|
-
this.
|
|
4916
|
+
this.focusDimOverlay?.destroy();
|
|
4917
|
+
this.focusDimOverlay = null;
|
|
4918
|
+
this.bgLayer.batchDraw();
|
|
4919
|
+
this.overlayLayer.batchDraw();
|
|
3464
4920
|
this.updateLOD();
|
|
3465
4921
|
}
|
|
3466
4922
|
/** The currently AXS-focused section id, or null. */
|
|
@@ -3477,16 +4933,69 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3477
4933
|
if (!sections.length) return;
|
|
3478
4934
|
const backdrop = new import_Group.Group({ listening: false });
|
|
3479
4935
|
for (const section of sections) {
|
|
3480
|
-
|
|
4936
|
+
const perspective = this.viewMode === "perspective" && section.perspectiveAffine;
|
|
4937
|
+
const outline = perspective ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline;
|
|
4938
|
+
const holes = perspective ? section.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point)))) : section.holes;
|
|
4939
|
+
backdrop.add(polygonWithHolesShape(outline, holes, {
|
|
3481
4940
|
fill: FOCUS_BACKDROP_FILL,
|
|
3482
4941
|
stroke: rgba("#ffffff", 0.1),
|
|
3483
4942
|
strokeWidth: 1,
|
|
3484
4943
|
listening: false
|
|
3485
|
-
}, section.outlinePath));
|
|
4944
|
+
}, perspective ? void 0 : section.outlinePath));
|
|
3486
4945
|
}
|
|
3487
4946
|
this.bgLayer.add(backdrop);
|
|
3488
4947
|
backdrop.moveToTop();
|
|
3489
4948
|
this.focusBackdrop = backdrop;
|
|
4949
|
+
this.drawFocusDimOverlay(sections);
|
|
4950
|
+
}
|
|
4951
|
+
/** Dim everything outside the active section with one paint-only shape. Group
|
|
4952
|
+
* opacity would make Konva recursively invalidate absolute-opacity caches on
|
|
4953
|
+
* all 14k descendants. The cut-out follows the live elevation offset and is
|
|
4954
|
+
* non-listening, so direct seat hit-testing is unchanged. */
|
|
4955
|
+
drawFocusDimOverlay(sections) {
|
|
4956
|
+
this.focusDimOverlay?.destroy();
|
|
4957
|
+
const span = Math.max(this.bounds.width, this.bounds.height, 1);
|
|
4958
|
+
const padding = span * 4 + 1e3;
|
|
4959
|
+
const outer = [
|
|
4960
|
+
{ x: this.bounds.x - padding, y: this.bounds.y - padding },
|
|
4961
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y - padding },
|
|
4962
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y + this.bounds.height + padding },
|
|
4963
|
+
{ x: this.bounds.x - padding, y: this.bounds.y + this.bounds.height + padding }
|
|
4964
|
+
];
|
|
4965
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
4966
|
+
const next = points[(index + 1) % points.length];
|
|
4967
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
4968
|
+
}, 0);
|
|
4969
|
+
const overlay = new import_Shape.Shape({
|
|
4970
|
+
fill: this.canvasBackground,
|
|
4971
|
+
opacity: 1 - FOCUS_DIM_OPACITY,
|
|
4972
|
+
listening: false,
|
|
4973
|
+
perfectDrawEnabled: false,
|
|
4974
|
+
sceneFunc: (context, shape) => {
|
|
4975
|
+
const polygonPath = (points) => {
|
|
4976
|
+
if (!points.length) return;
|
|
4977
|
+
context.moveTo(points[0].x, points[0].y);
|
|
4978
|
+
for (let index = 1; index < points.length; index++) context.lineTo(points[index].x, points[index].y);
|
|
4979
|
+
context.closePath();
|
|
4980
|
+
};
|
|
4981
|
+
context.beginPath();
|
|
4982
|
+
polygonPath(outer);
|
|
4983
|
+
for (const section of sections) {
|
|
4984
|
+
const hole = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
4985
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4986
|
+
return { x: point.x + offset.x, y: point.y + offset.y };
|
|
4987
|
+
});
|
|
4988
|
+
polygonPath(signedArea(hole) > 0 ? [...hole].reverse() : hole);
|
|
4989
|
+
}
|
|
4990
|
+
context.fillStrokeShape(shape);
|
|
4991
|
+
}
|
|
4992
|
+
});
|
|
4993
|
+
this.overlayLayer.add(overlay);
|
|
4994
|
+
overlay.moveToTop();
|
|
4995
|
+
for (const marker of this.selectionMarkers.values()) marker.moveToTop();
|
|
4996
|
+
this.hoverRing.moveToTop();
|
|
4997
|
+
this.focusRing.moveToTop();
|
|
4998
|
+
this.focusDimOverlay = overlay;
|
|
3490
4999
|
}
|
|
3491
5000
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
3492
5001
|
repaintSectionsAndSeats() {
|
|
@@ -3550,7 +5059,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3550
5059
|
this.applyGAFilterState();
|
|
3551
5060
|
}
|
|
3552
5061
|
renderBackground(doc) {
|
|
3553
|
-
|
|
5062
|
+
const buyerBackground = buyerBackgroundImage(doc);
|
|
5063
|
+
if (buyerBackground) this.renderBackgroundImage(buyerBackground);
|
|
3554
5064
|
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
3555
5065
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
3556
5066
|
this.renderZones(doc);
|
|
@@ -3630,10 +5140,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3630
5140
|
listening: false,
|
|
3631
5141
|
perfectDrawEnabled: false
|
|
3632
5142
|
});
|
|
3633
|
-
this.
|
|
5143
|
+
if (obj.layer === "foreground") this.fgDecorGroup.add(node);
|
|
5144
|
+
else this.bgLayer.add(node);
|
|
3634
5145
|
img.onload = () => {
|
|
3635
|
-
|
|
3636
|
-
|
|
5146
|
+
const layer = node.getLayer();
|
|
5147
|
+
if (!layer) return;
|
|
5148
|
+
layer.batchDraw();
|
|
3637
5149
|
};
|
|
3638
5150
|
img.src = obj.href;
|
|
3639
5151
|
}
|
|
@@ -3670,23 +5182,46 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3670
5182
|
})
|
|
3671
5183
|
);
|
|
3672
5184
|
}
|
|
3673
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
5185
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3674
5186
|
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
3675
5187
|
}
|
|
3676
5188
|
renderText(obj) {
|
|
3677
5189
|
const background = this.canvasBackground;
|
|
3678
5190
|
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
5191
|
+
const iconPath = obj.semanticKind === "icon" ? venueIconPath(obj.iconKey) : void 0;
|
|
5192
|
+
if (iconPath) {
|
|
5193
|
+
const scale = obj.fontSize / VENUE_ICON_VIEWBOX;
|
|
5194
|
+
const iconNode = new import_Path.Path({
|
|
5195
|
+
x: obj.position.x,
|
|
5196
|
+
y: obj.position.y,
|
|
5197
|
+
data: iconPath,
|
|
5198
|
+
rotation: obj.rotation,
|
|
5199
|
+
scaleX: scale,
|
|
5200
|
+
scaleY: scale,
|
|
5201
|
+
stroke: stateAwareBookableLabelInk(background, preferredInk),
|
|
5202
|
+
strokeWidth: VENUE_ICON_STROKE,
|
|
5203
|
+
lineCap: "round",
|
|
5204
|
+
lineJoin: "round",
|
|
5205
|
+
fillEnabled: false,
|
|
5206
|
+
listening: false,
|
|
5207
|
+
perfectDrawEnabled: false
|
|
5208
|
+
});
|
|
5209
|
+
this.iconNodeById.set(obj.id, { node: iconNode, fontSize: obj.fontSize });
|
|
5210
|
+
this.bgLayer.add(iconNode);
|
|
5211
|
+
return;
|
|
5212
|
+
}
|
|
3679
5213
|
const node = new import_Text.Text({
|
|
3680
5214
|
x: obj.position.x,
|
|
3681
5215
|
y: obj.position.y,
|
|
3682
5216
|
text: obj.text,
|
|
3683
5217
|
fontSize: obj.fontSize,
|
|
3684
5218
|
rotation: obj.rotation,
|
|
5219
|
+
fontStyle: konvaFontStyle(obj.bold, obj.italic, "normal"),
|
|
3685
5220
|
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3686
5221
|
// the actual canvas. Fail over to readable black/white instead of
|
|
3687
5222
|
// painting an otherwise valid caption invisibly on that active surface.
|
|
3688
5223
|
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3689
|
-
fontFamily: this.labelFont(),
|
|
5224
|
+
fontFamily: obj.fontFamily ?? this.labelFont(),
|
|
3690
5225
|
listening: false,
|
|
3691
5226
|
perfectDrawEnabled: false
|
|
3692
5227
|
});
|
|
@@ -3704,11 +5239,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3704
5239
|
const isDecor = !!obj.role && !isStage;
|
|
3705
5240
|
const palette = overviewPalette(this.canvasBackground);
|
|
3706
5241
|
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3707
|
-
const
|
|
3708
|
-
const
|
|
5242
|
+
const isOpenPath = obj.kind === "line" || obj.kind === "polyline";
|
|
5243
|
+
const stroke = obj.stroke?.color ?? (isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : isOpenPath ? "#9aa3b5" : void 0);
|
|
5244
|
+
const strokeWidth = obj.stroke?.width ?? (isStage ? 1 : referenceFocal ? 2 : isOpenPath ? 2 : 0);
|
|
5245
|
+
const opacity = obj.opacity != null ? clamp(obj.opacity, 0.1, 1) : 1;
|
|
3709
5246
|
let cx = 0;
|
|
3710
5247
|
let cy = 0;
|
|
3711
|
-
if (
|
|
5248
|
+
if (isOpenPath && obj.points && obj.points.length >= 2) {
|
|
5249
|
+
cx = obj.points.reduce((a, p) => a + p.x, 0) / obj.points.length;
|
|
5250
|
+
cy = obj.points.reduce((a, p) => a + p.y, 0) / obj.points.length;
|
|
5251
|
+
const lineStyle = resolvedShapeLineStyle(obj);
|
|
5252
|
+
const arrow = shapeArrowMetrics(strokeWidth);
|
|
5253
|
+
const path = new import_Arrow.Arrow({
|
|
5254
|
+
points: obj.points.flatMap((p) => [p.x, p.y]),
|
|
5255
|
+
closed: false,
|
|
5256
|
+
x: cx,
|
|
5257
|
+
y: cy,
|
|
5258
|
+
offsetX: cx,
|
|
5259
|
+
offsetY: cy,
|
|
5260
|
+
rotation: obj.rotation ?? 0,
|
|
5261
|
+
stroke,
|
|
5262
|
+
fill: stroke,
|
|
5263
|
+
strokeWidth,
|
|
5264
|
+
opacity,
|
|
5265
|
+
lineCap: lineStyle.lineCap,
|
|
5266
|
+
lineJoin: lineStyle.lineJoin,
|
|
5267
|
+
pointerAtBeginning: lineStyle.startEnding === "arrow",
|
|
5268
|
+
pointerAtEnding: lineStyle.endEnding === "arrow",
|
|
5269
|
+
...arrow,
|
|
5270
|
+
listening: false,
|
|
5271
|
+
name: "buyer-shape-open-path"
|
|
5272
|
+
});
|
|
5273
|
+
path.setAttr("shapeObjectId", obj.id);
|
|
5274
|
+
this.bgLayer.add(path);
|
|
5275
|
+
} else if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
3712
5276
|
const grad = isStage ? {
|
|
3713
5277
|
fillLinearGradientStartPoint: { x: 0, y: 0 },
|
|
3714
5278
|
fillLinearGradientEndPoint: { x: 0, y: obj.height },
|
|
@@ -3728,7 +5292,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3728
5292
|
...grad,
|
|
3729
5293
|
stroke,
|
|
3730
5294
|
strokeWidth,
|
|
3731
|
-
cornerRadius: 4,
|
|
5295
|
+
cornerRadius: clamp(obj.cornerRadius ?? 4, 0, Math.min(obj.width, obj.height) / 2),
|
|
5296
|
+
opacity,
|
|
3732
5297
|
listening: false
|
|
3733
5298
|
})
|
|
3734
5299
|
);
|
|
@@ -3741,7 +5306,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3741
5306
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3742
5307
|
} : { fill };
|
|
3743
5308
|
this.bgLayer.add(
|
|
3744
|
-
new import_Ellipse.Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
5309
|
+
new import_Ellipse.Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3745
5310
|
);
|
|
3746
5311
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
3747
5312
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -3756,7 +5321,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3756
5321
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3757
5322
|
} : { fill };
|
|
3758
5323
|
this.bgLayer.add(
|
|
3759
|
-
new import_Line.Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
5324
|
+
new import_Line.Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3760
5325
|
);
|
|
3761
5326
|
}
|
|
3762
5327
|
if (obj.label) {
|
|
@@ -3812,10 +5377,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3812
5377
|
fill: color,
|
|
3813
5378
|
opacity: GA_FILL_OPACITY,
|
|
3814
5379
|
stroke: color,
|
|
3815
|
-
strokeWidth: 1.5
|
|
5380
|
+
strokeWidth: 1.5,
|
|
5381
|
+
cornerRadius: obj.cornerRadius
|
|
3816
5382
|
});
|
|
3817
5383
|
poly.setAttr("gaId", obj.id);
|
|
3818
|
-
poly.on("click tap", () =>
|
|
5384
|
+
poly.on("click tap", (e) => {
|
|
5385
|
+
if (this.isGhostClick(e)) return;
|
|
5386
|
+
this.opts.onGAClick?.(obj.id);
|
|
5387
|
+
});
|
|
3819
5388
|
poly.on("mouseenter", () => {
|
|
3820
5389
|
this.container.style.cursor = "pointer";
|
|
3821
5390
|
});
|
|
@@ -3825,7 +5394,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3825
5394
|
this.bgLayer.add(poly);
|
|
3826
5395
|
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3827
5396
|
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3828
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
5397
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3829
5398
|
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3830
5399
|
this.freeTextById.set(`${obj.id}:label`, {
|
|
3831
5400
|
objectId: obj.id,
|
|
@@ -3842,7 +5411,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3842
5411
|
categoryKey: obj.categoryKey
|
|
3843
5412
|
});
|
|
3844
5413
|
this.gaById.set(obj.id, {
|
|
3845
|
-
label: obj.label,
|
|
5414
|
+
label: obj.displayLabel ?? obj.label,
|
|
3846
5415
|
capacity: obj.capacity,
|
|
3847
5416
|
categoryKey: obj.categoryKey,
|
|
3848
5417
|
points: obj.points,
|
|
@@ -3865,7 +5434,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3865
5434
|
const memberIds = [];
|
|
3866
5435
|
const catCounts = /* @__PURE__ */ new Map();
|
|
3867
5436
|
let free = 0;
|
|
3868
|
-
|
|
5437
|
+
const bounds2 = polyBounds(obj.outline);
|
|
5438
|
+
const candidates = this.seatIndex ? queryRect(this.seatIndex, bounds2).map((id) => this.seatById.get(id)).filter((seat) => !!seat) : this.seats;
|
|
5439
|
+
for (const seat of candidates) {
|
|
3869
5440
|
if (this.seatSection.has(seat.id)) continue;
|
|
3870
5441
|
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
3871
5442
|
memberIds.push(seat.id);
|
|
@@ -3876,11 +5447,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3876
5447
|
[...catCounts].map(([key, w]) => ({ hex: this.catColor.get(key) ?? "#6e7bff", w })),
|
|
3877
5448
|
"#3a4358"
|
|
3878
5449
|
);
|
|
3879
|
-
const elevation =
|
|
3880
|
-
|
|
3881
|
-
|
|
5450
|
+
const elevation = sectionElevationTier(obj.elevation);
|
|
5451
|
+
const geometry = sectionGeometry(obj, {
|
|
5452
|
+
floorBaseHeightM: this.floorBaseHeightMFor(obj.id)
|
|
5453
|
+
});
|
|
5454
|
+
const liftWorld = geometry.height * CHART_UNITS_PER_METRE;
|
|
5455
|
+
const rootGroupBg = new import_Group.Group({ listening: false });
|
|
5456
|
+
this.bgLayer.add(rootGroupBg);
|
|
5457
|
+
const liftGroupBg = new import_Group.Group({ listening: false });
|
|
5458
|
+
rootGroupBg.add(liftGroupBg);
|
|
5459
|
+
const liftGroupSeat = new ViewportGroup({ listening: true });
|
|
5460
|
+
this.seatLayer.add(liftGroupSeat);
|
|
3882
5461
|
const sideFaces = [];
|
|
3883
|
-
if (
|
|
5462
|
+
if (liftWorld > 0) {
|
|
3884
5463
|
const faceFill = darken(this.zoneColor.get(obj.zone ?? "") ?? baseFill, 0.42);
|
|
3885
5464
|
for (let i = 0; i < obj.outline.length; i++) {
|
|
3886
5465
|
const face = new import_Line.Line({
|
|
@@ -3894,14 +5473,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3894
5473
|
perfectDrawEnabled: false
|
|
3895
5474
|
});
|
|
3896
5475
|
sideFaces.push(face);
|
|
3897
|
-
|
|
5476
|
+
rootGroupBg.add(face);
|
|
3898
5477
|
}
|
|
3899
|
-
liftGroupBg = new import_Group.Group({ listening: false });
|
|
3900
|
-
this.bgLayer.add(liftGroupBg);
|
|
3901
|
-
liftGroupSeat = new import_Group.Group({ listening: false });
|
|
3902
|
-
this.seatLayer.add(liftGroupSeat);
|
|
3903
5478
|
}
|
|
3904
|
-
|
|
5479
|
+
liftGroupBg.moveToTop();
|
|
5480
|
+
const bgTarget = liftGroupBg;
|
|
3905
5481
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
3906
5482
|
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3907
5483
|
stroke: rgba(outlineTint, 0.5),
|
|
@@ -3975,10 +5551,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3975
5551
|
nameLabelFits: true,
|
|
3976
5552
|
subLabelFits: true,
|
|
3977
5553
|
elevation,
|
|
5554
|
+
liftWorld,
|
|
5555
|
+
rakeDeg: geometry.rake,
|
|
5556
|
+
surfaceFocal: memberIds.length ? { ...this.seatById.get(memberIds[0])?.focalPoint ?? this.isoCentre } : { ...this.chartDoc?.focalPoint ?? this.isoCentre },
|
|
5557
|
+
frontDistanceWorld: 0,
|
|
5558
|
+
rootGroupBg,
|
|
5559
|
+
bgZIndex: rootGroupBg.zIndex(),
|
|
5560
|
+
seatZIndex: liftGroupSeat.zIndex(),
|
|
3978
5561
|
liftGroupBg,
|
|
3979
5562
|
liftGroupSeat,
|
|
3980
5563
|
sideFaces
|
|
3981
5564
|
};
|
|
5565
|
+
const surfaceCandidates = memberIds.map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
5566
|
+
const distanceCandidates = surfaceCandidates.length ? surfaceCandidates : obj.outline;
|
|
5567
|
+
sec.frontDistanceWorld = Math.min(...distanceCandidates.map((point) => Math.hypot(
|
|
5568
|
+
point.x - sec.surfaceFocal.x,
|
|
5569
|
+
point.y - sec.surfaceFocal.y
|
|
5570
|
+
)));
|
|
3982
5571
|
for (const id of memberIds) this.seatSection.set(id, sec);
|
|
3983
5572
|
this.refreshSectionFill(sec);
|
|
3984
5573
|
this.refreshSectionHeat(sec);
|
|
@@ -4001,20 +5590,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4001
5590
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
4002
5591
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
4003
5592
|
}
|
|
4004
|
-
/** Recompute a section's
|
|
5593
|
+
/** Recompute a section's availability shading + retained availability text. */
|
|
4005
5594
|
refreshSectionFill(sec) {
|
|
4006
5595
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
4007
|
-
sec.subLabel.text(
|
|
5596
|
+
sec.subLabel.text(
|
|
5597
|
+
this.sectionAvailabilityState(sec) === "sold-out" ? t("map.soldOut") : t("map.seatsLeft", { count: sec.free })
|
|
5598
|
+
);
|
|
4008
5599
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
4009
5600
|
}
|
|
4010
5601
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
4011
5602
|
isSectionClosed(sec) {
|
|
4012
5603
|
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
4013
5604
|
}
|
|
4014
|
-
/**
|
|
5605
|
+
/** Resolve the four-bucket overview availability state for a section (OV-69).
|
|
5606
|
+
* `closed` (operator) outranks live availability; the availability buckets
|
|
5607
|
+
* only apply to seated sections (GA-only shells have no seat capacity here). */
|
|
5608
|
+
sectionAvailabilityState(sec) {
|
|
5609
|
+
if (this.isSectionClosed(sec)) return "closed";
|
|
5610
|
+
if (sec.total <= 0) return "normal";
|
|
5611
|
+
if (sec.free <= 0) return "sold-out";
|
|
5612
|
+
if (sec.free <= sec.total * SECTION_NEARLY_GONE_RATIO) return "nearly-gone";
|
|
5613
|
+
return "normal";
|
|
5614
|
+
}
|
|
5615
|
+
/** Overview shells stay neutral except for the bounded availability shading
|
|
5616
|
+
* (nearly-gone / sold-out) and the operator `closed` slab — never category
|
|
5617
|
+
* or price paint, which is deferred to section focus/seat detail. */
|
|
4015
5618
|
sectionBlockFill(sec) {
|
|
4016
|
-
|
|
4017
|
-
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
5619
|
+
return overviewStateFill(overviewPalette(this.canvasBackground), this.sectionAvailabilityState(sec));
|
|
4018
5620
|
}
|
|
4019
5621
|
/**
|
|
4020
5622
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -4124,15 +5726,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4124
5726
|
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
4125
5727
|
if (sectionOverview) blockT = 1;
|
|
4126
5728
|
if (!this.zones.length) zoneT = 0;
|
|
4127
|
-
this.
|
|
5729
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
5730
|
+
this.seatLayer.opacity(sectionOverview || deferPerspectiveSeatReveal ? 0 : 1 - blockT);
|
|
4128
5731
|
const sx = this.stage.scaleX();
|
|
4129
|
-
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.
|
|
5732
|
+
const rescale = !this.glideInProgress && (this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.08);
|
|
4130
5733
|
if (rescale) this.lodScale = scale;
|
|
4131
5734
|
const focus = this.focusedSectionId;
|
|
4132
5735
|
const palette = overviewPalette(this.canvasBackground);
|
|
4133
5736
|
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
4134
5737
|
for (const sec of this.sections) {
|
|
4135
|
-
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
5738
|
+
const dim = this.focusDimOverlay ? 1 : focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
4136
5739
|
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
4137
5740
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
4138
5741
|
sec.blockPoly.stroke(palette.sectionStroke);
|
|
@@ -4144,7 +5747,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4144
5747
|
);
|
|
4145
5748
|
sec.nameLabel.fill(sectionInk);
|
|
4146
5749
|
sec.subLabel.fill(sectionInk);
|
|
4147
|
-
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
5750
|
+
if (rescale && sectionLabelT > 0.01) this.fitSectionRungLabels(sec, sx);
|
|
4148
5751
|
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
4149
5752
|
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
4150
5753
|
sec.subLabel.opacity(0);
|
|
@@ -4156,12 +5759,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4156
5759
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
4157
5760
|
if (rescale) this.sizeZonePill(zone, sx);
|
|
4158
5761
|
}
|
|
4159
|
-
this.
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
const
|
|
4163
|
-
|
|
4164
|
-
|
|
5762
|
+
if (!this.glideInProgress && (sectionLabelT > 0.01 || zoneOpacity > 0.01)) {
|
|
5763
|
+
this.decollideRungLabels(sx);
|
|
5764
|
+
this.dedupeLogicalSectionLabels();
|
|
5765
|
+
for (const zone of this.zones) {
|
|
5766
|
+
const opacity = zone.label.opacity();
|
|
5767
|
+
zone.back.opacity(opacity);
|
|
5768
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
5769
|
+
}
|
|
4165
5770
|
}
|
|
4166
5771
|
this.bgLayer.batchDraw();
|
|
4167
5772
|
}
|
|
@@ -4288,13 +5893,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4288
5893
|
screenToWorld(clientPoint) {
|
|
4289
5894
|
const s = this.stage.scaleX();
|
|
4290
5895
|
const iso = { x: (clientPoint.x - this.stage.x()) / s, y: (clientPoint.y - this.stage.y()) / s };
|
|
5896
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseInverse) {
|
|
5897
|
+
return applyAffine(this.perspectiveBaseInverse, iso);
|
|
5898
|
+
}
|
|
4291
5899
|
return this.isoT === 0 ? iso : this.isoInverse(iso);
|
|
4292
5900
|
}
|
|
4293
5901
|
/** Section id under a container-relative screen point, or null (Slice 5 tap-to-zoom). */
|
|
4294
5902
|
sectionAt(clientPoint) {
|
|
4295
5903
|
if (!this.sections.length) return null;
|
|
4296
5904
|
const world = this.screenToWorld(clientPoint);
|
|
4297
|
-
const hit = this.sections.find((sec) =>
|
|
5905
|
+
const hit = this.sections.find((sec) => {
|
|
5906
|
+
if (this.viewMode === "perspective" && sec.perspectiveAffine) {
|
|
5907
|
+
return pointInPolygonWithHoles(
|
|
5908
|
+
world,
|
|
5909
|
+
sec.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))),
|
|
5910
|
+
sec.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))))
|
|
5911
|
+
);
|
|
5912
|
+
}
|
|
5913
|
+
const offset = sec.liftWorld > 0 ? this.isoLiftLocal(sec.liftWorld) : { x: 0, y: 0 };
|
|
5914
|
+
return pointInPolygonWithHoles(
|
|
5915
|
+
{ x: world.x - offset.x, y: world.y - offset.y },
|
|
5916
|
+
sec.outline,
|
|
5917
|
+
sec.holes
|
|
5918
|
+
);
|
|
5919
|
+
});
|
|
4298
5920
|
return hit ? hit.logicalId : null;
|
|
4299
5921
|
}
|
|
4300
5922
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
@@ -4390,15 +6012,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4390
6012
|
if (!seat) return;
|
|
4391
6013
|
const candidate = this.selectionFocusId === id;
|
|
4392
6014
|
const dims = this.boothDims.get(seat.rowId);
|
|
6015
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
4393
6016
|
const marker = new import_Group.Group({
|
|
4394
6017
|
name: "selection-ring",
|
|
4395
|
-
x:
|
|
4396
|
-
y:
|
|
6018
|
+
x: rendered.x,
|
|
6019
|
+
y: rendered.y,
|
|
4397
6020
|
rotation: dims?.rotation ?? 0,
|
|
4398
6021
|
listening: false,
|
|
4399
6022
|
perfectDrawEnabled: false,
|
|
4400
6023
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
4401
6024
|
});
|
|
6025
|
+
if (this.viewMode === "perspective") {
|
|
6026
|
+
const projectionScale = this.perspectiveSeatScale.get(id) ?? 1;
|
|
6027
|
+
marker.scale({ x: projectionScale, y: projectionScale });
|
|
6028
|
+
}
|
|
4402
6029
|
marker.setAttr("seatId", id);
|
|
4403
6030
|
const common = {
|
|
4404
6031
|
stroke: this.effSelection,
|
|
@@ -4477,12 +6104,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4477
6104
|
* must fall through and pick.
|
|
4478
6105
|
*/
|
|
4479
6106
|
sectionBounds(id) {
|
|
4480
|
-
const
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
6107
|
+
const bounds2 = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => {
|
|
6108
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
6109
|
+
return polyBounds(section.outline.map((point) => this.projectedSectionPoint(section, point)));
|
|
6110
|
+
}
|
|
6111
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
6112
|
+
const points = section.outline.map((point) => {
|
|
6113
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
6114
|
+
return this.isoT === 0 ? local : this.isoForward(local);
|
|
6115
|
+
});
|
|
6116
|
+
return polyBounds(points);
|
|
6117
|
+
});
|
|
6118
|
+
if (!bounds2.length) return null;
|
|
6119
|
+
const left = Math.min(...bounds2.map((box) => box.x));
|
|
6120
|
+
const top = Math.min(...bounds2.map((box) => box.y));
|
|
6121
|
+
const right = Math.max(...bounds2.map((box) => box.x + box.width));
|
|
6122
|
+
const bottom = Math.max(...bounds2.map((box) => box.y + box.height));
|
|
4486
6123
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
4487
6124
|
}
|
|
4488
6125
|
sectionFrameScale(id) {
|
|
@@ -4535,15 +6172,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4535
6172
|
* hijacking a clean tap on empty space beyond the slop.
|
|
4536
6173
|
*/
|
|
4537
6174
|
nearestSeatToScreen(screen, slopPx) {
|
|
4538
|
-
const s = this.stage.scaleX() || 1;
|
|
4539
|
-
const reachWorld = this.seatR + slopPx / s;
|
|
4540
|
-
const world = this.screenToWorld(screen);
|
|
4541
6175
|
let best = null;
|
|
4542
|
-
let bestD =
|
|
6176
|
+
let bestD = Infinity;
|
|
6177
|
+
const stageScale = this.stage.scaleX() || 1;
|
|
4543
6178
|
for (const seat of this.seats) {
|
|
4544
6179
|
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
4545
|
-
const
|
|
4546
|
-
|
|
6180
|
+
const centre = this.worldToScreen(seat);
|
|
6181
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
6182
|
+
const reachPx = this.seatR * stageScale * perspectiveScale + slopPx;
|
|
6183
|
+
const d = Math.hypot(centre.x - screen.x, centre.y - screen.y);
|
|
6184
|
+
if (d <= reachPx && d < bestD) {
|
|
4547
6185
|
bestD = d;
|
|
4548
6186
|
best = seat.id;
|
|
4549
6187
|
}
|
|
@@ -4553,6 +6191,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4553
6191
|
wireInteraction() {
|
|
4554
6192
|
this.seatLayer.on("click tap", (e) => {
|
|
4555
6193
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
6194
|
+
if (this.isGhostClick(e)) return;
|
|
4556
6195
|
const id = seatIdOf(e.target);
|
|
4557
6196
|
if (!id) return;
|
|
4558
6197
|
this.handleSeatTap(id);
|
|
@@ -4562,13 +6201,17 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4562
6201
|
if (!id) return;
|
|
4563
6202
|
const seat = this.seatById.get(id);
|
|
4564
6203
|
if (!seat) return;
|
|
4565
|
-
this.
|
|
6204
|
+
this.hoveredId = id;
|
|
6205
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
6206
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
6207
|
+
this.hoverRing.scale({ x: projectionScale, y: projectionScale });
|
|
4566
6208
|
this.hoverRing.visible(true);
|
|
4567
6209
|
this.overlayLayer.batchDraw();
|
|
4568
6210
|
this.container.style.cursor = "pointer";
|
|
4569
6211
|
this.opts.onHover?.(seat);
|
|
4570
6212
|
});
|
|
4571
6213
|
this.seatLayer.on("mouseout", () => {
|
|
6214
|
+
this.hoveredId = null;
|
|
4572
6215
|
this.hoverRing.visible(false);
|
|
4573
6216
|
this.overlayLayer.batchDraw();
|
|
4574
6217
|
this.container.style.cursor = "default";
|
|
@@ -4583,6 +6226,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4583
6226
|
});
|
|
4584
6227
|
this.stage.on("click tap", (e) => {
|
|
4585
6228
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
6229
|
+
if (this.isGhostClick(e)) return;
|
|
4586
6230
|
const pointer = this.stage.getPointerPosition();
|
|
4587
6231
|
if (!pointer) return;
|
|
4588
6232
|
if (!this.cached) {
|
|
@@ -4599,11 +6243,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4599
6243
|
}
|
|
4600
6244
|
}
|
|
4601
6245
|
if (this.sections.length) {
|
|
4602
|
-
const
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
else this.focusRegion(hit.logicalId);
|
|
6246
|
+
const sectionId = this.sectionAt(pointer);
|
|
6247
|
+
if (sectionId) {
|
|
6248
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sectionId);
|
|
6249
|
+
else this.focusRegion(sectionId);
|
|
4607
6250
|
return;
|
|
4608
6251
|
}
|
|
4609
6252
|
}
|
|
@@ -4620,13 +6263,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4620
6263
|
*/
|
|
4621
6264
|
deckFloorAt() {
|
|
4622
6265
|
if (!this.objectFloor.size) return null;
|
|
4623
|
-
const
|
|
4624
|
-
if (!
|
|
6266
|
+
const pointer = this.stage.getPointerPosition();
|
|
6267
|
+
if (!pointer) return null;
|
|
6268
|
+
const p = this.screenToWorld(pointer);
|
|
4625
6269
|
let best = null;
|
|
4626
6270
|
let bestD = Infinity;
|
|
4627
6271
|
for (const s of this.seats) {
|
|
4628
|
-
const
|
|
4629
|
-
const
|
|
6272
|
+
const rendered = this.renderedSeatPoint(s);
|
|
6273
|
+
const dx = rendered.x - p.x;
|
|
6274
|
+
const dy = rendered.y - p.y;
|
|
4630
6275
|
const d = dx * dx + dy * dy;
|
|
4631
6276
|
if (d < bestD) {
|
|
4632
6277
|
bestD = d;
|
|
@@ -4713,9 +6358,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4713
6358
|
}
|
|
4714
6359
|
const start = performance.now();
|
|
4715
6360
|
const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
|
|
6361
|
+
this.glideInProgress = true;
|
|
4716
6362
|
const step = (now) => {
|
|
4717
6363
|
if (this.destroyed) {
|
|
4718
6364
|
this.glideRaf = 0;
|
|
6365
|
+
this.glideInProgress = false;
|
|
4719
6366
|
return;
|
|
4720
6367
|
}
|
|
4721
6368
|
const raw = Math.min(1, (now - start) / duration);
|
|
@@ -4723,6 +6370,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4723
6370
|
const sc = fromScale + (toScale - fromScale) * e;
|
|
4724
6371
|
this.stage.scale({ x: sc, y: sc });
|
|
4725
6372
|
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
6373
|
+
this.updateSeatGroupVisibility();
|
|
4726
6374
|
this.updateLOD();
|
|
4727
6375
|
this.scheduleViewChange();
|
|
4728
6376
|
this.stage.batchDraw();
|
|
@@ -4730,6 +6378,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4730
6378
|
this.glideRaf = requestAnimationFrame(step);
|
|
4731
6379
|
} else {
|
|
4732
6380
|
this.glideRaf = 0;
|
|
6381
|
+
this.glideInProgress = false;
|
|
4733
6382
|
this.afterViewChange();
|
|
4734
6383
|
}
|
|
4735
6384
|
};
|
|
@@ -4741,6 +6390,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4741
6390
|
cancelAnimationFrame(this.glideRaf);
|
|
4742
6391
|
this.glideRaf = 0;
|
|
4743
6392
|
}
|
|
6393
|
+
this.glideInProgress = false;
|
|
4744
6394
|
}
|
|
4745
6395
|
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
4746
6396
|
getRung() {
|
|
@@ -4757,11 +6407,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4757
6407
|
const labels = this.seats.map((seat) => {
|
|
4758
6408
|
const shape = this.circleById.get(seat.id);
|
|
4759
6409
|
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
6410
|
+
const localPerspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
6411
|
+
const seatEffectiveScale = (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale;
|
|
4760
6412
|
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4761
|
-
const
|
|
6413
|
+
const labelEffectiveScale = this.viewMode === "perspective" && seat.kind !== "booth" ? effectiveScale * localPerspectiveScale : seatEffectiveScale;
|
|
6414
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * labelEffectiveScale);
|
|
4762
6415
|
const screen = this.worldToScreen(seat);
|
|
4763
6416
|
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4764
|
-
const opacity = shape?.
|
|
6417
|
+
const opacity = this.focusedSeatOpacity(seat.id, shape?.getAbsoluteOpacity() ?? 0);
|
|
4765
6418
|
const section = this.seatSection.get(seat.id);
|
|
4766
6419
|
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4767
6420
|
let hiddenReason;
|
|
@@ -4772,17 +6425,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4772
6425
|
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4773
6426
|
else hiddenReason = "renderer-hidden";
|
|
4774
6427
|
}
|
|
4775
|
-
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4776
|
-
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4777
|
-
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale : this.seatR * 2 *
|
|
4778
|
-
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * stageScale : this.seatR * 2 *
|
|
4779
|
-
const assistedDiameterPx = 2 * (this.seatR *
|
|
6428
|
+
const labelWidth = label ? label.width() * stageScale * localPerspectiveScale : 0;
|
|
6429
|
+
const labelHeight = label ? label.height() * (this.viewMode === "perspective" && seat.kind === "booth" ? stageScale : effectiveScale) * localPerspectiveScale : 0;
|
|
6430
|
+
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6431
|
+
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6432
|
+
const assistedDiameterPx = 2 * (this.seatR * seatEffectiveScale + SEAT_TAP_SLOP_PX);
|
|
4780
6433
|
const fill = shape?.fill();
|
|
4781
6434
|
const ink = label?.fill();
|
|
6435
|
+
const accessGlyph = this.accessGlyphById.get(seat.id);
|
|
6436
|
+
const accessGlyphScale = accessGlyph?.getAbsoluteScale();
|
|
4782
6437
|
return {
|
|
4783
6438
|
seatId: seat.id,
|
|
4784
6439
|
label: seat.label,
|
|
4785
6440
|
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
6441
|
+
markerShape: seat.kind === "booth" ? "booth" : seat.wheelchairSpaceType === "no-seat" ? "square" : "circle",
|
|
6442
|
+
...seat.wheelchairSpaceType ? { wheelchairSpaceType: seat.wheelchairSpaceType } : {},
|
|
4786
6443
|
categoryKey: seat.categoryKey,
|
|
4787
6444
|
...section ? { sectionId: section.id } : {},
|
|
4788
6445
|
...section?.zone ? { zoneId: section.zone } : {},
|
|
@@ -4793,8 +6450,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4793
6450
|
fill: typeof fill === "string" ? fill : "",
|
|
4794
6451
|
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4795
6452
|
opacity: rounded(opacity),
|
|
6453
|
+
...accessGlyph ? {
|
|
6454
|
+
accessibilityMarker: {
|
|
6455
|
+
glyphVisible: accessGlyph.isVisible(),
|
|
6456
|
+
glyphWidthPx: rounded(ACCESS_GLYPH_VIEWBOX * Math.abs(accessGlyphScale?.x ?? 0)),
|
|
6457
|
+
emphasizedByFilter: this.accessGlyphFilterEmphasized(seat.id)
|
|
6458
|
+
}
|
|
6459
|
+
} : {},
|
|
4796
6460
|
pointerTarget: {
|
|
4797
|
-
active: !this.cached && this.isSelectable(seat.id),
|
|
6461
|
+
active: !this.cached && !this.seatViewportCulled(seat.id) && Boolean(shape?.isVisible()) && Boolean(shape?.isListening()) && this.isSelectable(seat.id),
|
|
4798
6462
|
directWidthPx: rounded(directWidthPx),
|
|
4799
6463
|
directHeightPx: rounded(directHeightPx),
|
|
4800
6464
|
effectiveMinimumPx: rounded(Math.max(
|
|
@@ -4822,7 +6486,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4822
6486
|
node.height(),
|
|
4823
6487
|
node.rotation()
|
|
4824
6488
|
);
|
|
4825
|
-
const
|
|
6489
|
+
const lift = section && section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
6490
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen({
|
|
6491
|
+
x: corner.x + lift.x,
|
|
6492
|
+
y: corner.y + lift.y
|
|
6493
|
+
})));
|
|
4826
6494
|
const opacity = rounded(node.opacity());
|
|
4827
6495
|
const ink = node.fill();
|
|
4828
6496
|
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
@@ -4954,13 +6622,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4954
6622
|
};
|
|
4955
6623
|
});
|
|
4956
6624
|
const palette = overviewPalette(this.canvasBackground);
|
|
4957
|
-
const neutralSectionFills =
|
|
4958
|
-
palette.
|
|
4959
|
-
|
|
4960
|
-
]);
|
|
6625
|
+
const neutralSectionFills = new Set(
|
|
6626
|
+
["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
|
|
6627
|
+
);
|
|
4961
6628
|
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4962
6629
|
return {
|
|
4963
6630
|
viewport,
|
|
6631
|
+
projection: this.viewMode,
|
|
6632
|
+
...this.viewMode === "perspective" ? {
|
|
6633
|
+
perspective: {
|
|
6634
|
+
model: "pinhole-exact-seat-anchors",
|
|
6635
|
+
sectionSurfaceModel: "tangent-plane",
|
|
6636
|
+
exactSeatAnchorCount: this.perspectiveSeatProjected.size,
|
|
6637
|
+
depthSorted: true
|
|
6638
|
+
}
|
|
6639
|
+
} : {},
|
|
4964
6640
|
canvasBackground: this.canvasBackground,
|
|
4965
6641
|
effectiveScale: rounded(effectiveScale),
|
|
4966
6642
|
rung: this.getRung(),
|
|
@@ -5015,10 +6691,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5015
6691
|
let cy = viewCentre.y;
|
|
5016
6692
|
if (rung === "sections" && this.sections.length > 0) {
|
|
5017
6693
|
const sectionCentres = this.sections.map((section) => {
|
|
5018
|
-
const
|
|
6694
|
+
const bounds2 = polyBounds(section.outline);
|
|
5019
6695
|
return {
|
|
5020
|
-
x:
|
|
5021
|
-
y:
|
|
6696
|
+
x: bounds2.x + bounds2.width / 2,
|
|
6697
|
+
y: bounds2.y + bounds2.height / 2
|
|
5022
6698
|
};
|
|
5023
6699
|
});
|
|
5024
6700
|
const halfWidth = w / (target * 2);
|
|
@@ -5085,6 +6761,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5085
6761
|
}
|
|
5086
6762
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
5087
6763
|
afterViewChange() {
|
|
6764
|
+
this.updateSeatGroupVisibility();
|
|
5088
6765
|
this.updateLOD();
|
|
5089
6766
|
this.updateFreeTextVisibility();
|
|
5090
6767
|
this.updateLabels();
|
|
@@ -5109,10 +6786,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5109
6786
|
this.paintGAStateForView();
|
|
5110
6787
|
this.updateAccessGlyphs(scale);
|
|
5111
6788
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
6789
|
+
const suppressPerspectiveSeatCache = this.viewMode === "perspective" && this.hasSections && this.seats.length > 2500 && shouldCache;
|
|
6790
|
+
if (suppressPerspectiveSeatCache) {
|
|
6791
|
+
this.seatLayer.listening(false);
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
5112
6794
|
if (shouldCache && !this.cached) {
|
|
5113
6795
|
this.cacheSeatLayer();
|
|
5114
|
-
} else if (!shouldCache && this.cached) {
|
|
5115
|
-
this.seatLayer.clearCache();
|
|
6796
|
+
} else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
|
|
6797
|
+
if (this.cached) this.seatLayer.clearCache();
|
|
5116
6798
|
this.seatLayer.listening(true);
|
|
5117
6799
|
this.cached = false;
|
|
5118
6800
|
this.seatLayer.batchDraw();
|
|
@@ -5121,6 +6803,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5121
6803
|
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
5122
6804
|
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
5123
6805
|
rebuildSeatCache() {
|
|
6806
|
+
this.updateSeatGroupVisibility();
|
|
5124
6807
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
5125
6808
|
this.seatLayer.clearCache();
|
|
5126
6809
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
@@ -5165,6 +6848,63 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5165
6848
|
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
5166
6849
|
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
5167
6850
|
}
|
|
6851
|
+
for (const { node, fontSize } of this.iconNodeById.values()) {
|
|
6852
|
+
node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
/** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
|
|
6856
|
+
* world space, matching the Designer's placement contract (start/end/both/none
|
|
6857
|
+
* preset, a free-drag `position` that wins over the preset, rotation, and
|
|
6858
|
+
* labelStyle colour/size fed through the same auto-contrast guard). Segmented
|
|
6859
|
+
* rows keep their own logical-label handling and are resolved by their owner. */
|
|
6860
|
+
buildRowLabelPlan(view) {
|
|
6861
|
+
this.rowLabelPlan = [];
|
|
6862
|
+
const background = this.theme.background ?? "#0e1117";
|
|
6863
|
+
const seatsByRow = /* @__PURE__ */ new Map();
|
|
6864
|
+
for (const seat of this.seats) {
|
|
6865
|
+
if (seat.kind === "booth") continue;
|
|
6866
|
+
const key = seat.logicalRowId ?? seat.rowId;
|
|
6867
|
+
(seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
|
|
6868
|
+
}
|
|
6869
|
+
const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
|
|
6870
|
+
for (const obj of view.objects) {
|
|
6871
|
+
if (obj.type !== "row") continue;
|
|
6872
|
+
if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
|
|
6873
|
+
const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
|
|
6874
|
+
if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
|
|
6875
|
+
const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
|
|
6876
|
+
const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
|
|
6877
|
+
if (!seats.length) continue;
|
|
6878
|
+
const first = seats[0];
|
|
6879
|
+
const last = seats[seats.length - 1];
|
|
6880
|
+
const labelStyle = presentation?.labelStyle;
|
|
6881
|
+
const ink = stateAwareBookableLabelInk(
|
|
6882
|
+
background,
|
|
6883
|
+
labelStyle?.color ?? this.theme.rowLabelColor ?? this.theme.textColor ?? "#e6e9f0"
|
|
6884
|
+
);
|
|
6885
|
+
const fontSize = labelStyle?.size ?? 12;
|
|
6886
|
+
const rotation = presentation?.rotation ?? 0;
|
|
6887
|
+
const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
|
|
6888
|
+
const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
|
|
6889
|
+
const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
|
|
6890
|
+
if (presentation?.position) {
|
|
6891
|
+
push(presentation.position.x, presentation.position.y);
|
|
6892
|
+
continue;
|
|
6893
|
+
}
|
|
6894
|
+
const radians = obj.rotation * Math.PI / 180;
|
|
6895
|
+
const along = { x: Math.cos(radians), y: Math.sin(radians) };
|
|
6896
|
+
const gap = this.seatR + 12;
|
|
6897
|
+
const preset = presentation?.positionPreset ?? "start";
|
|
6898
|
+
if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
|
|
6899
|
+
if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
/** Row labels never carry status; the buyer just dims them under the same
|
|
6903
|
+
* category/price filters that dim their seats. */
|
|
6904
|
+
objectFilteredOut(obj) {
|
|
6905
|
+
const key = obj.categoryKey;
|
|
6906
|
+
if (!key) return false;
|
|
6907
|
+
return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
|
|
5168
6908
|
}
|
|
5169
6909
|
updateLabels() {
|
|
5170
6910
|
const effectiveScale = this.effScale();
|
|
@@ -5172,31 +6912,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5172
6912
|
for (const [id, label] of this.boothLabelById) {
|
|
5173
6913
|
const shape = this.circleById.get(id);
|
|
5174
6914
|
label.visible(
|
|
5175
|
-
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.
|
|
6915
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && Boolean(shape?.isVisible()) && this.focusedSeatOpacity(id, shape?.getAbsoluteOpacity() ?? 1) >= 0.5
|
|
5176
6916
|
);
|
|
5177
6917
|
}
|
|
5178
6918
|
this.labelGroup.destroyChildren();
|
|
6919
|
+
this.labelLiftGroups.clear();
|
|
5179
6920
|
this.seatLabelById.clear();
|
|
5180
6921
|
if (!show) {
|
|
5181
6922
|
this.overlayLayer.batchDraw();
|
|
5182
6923
|
return;
|
|
5183
6924
|
}
|
|
5184
|
-
const s = this.stage.scaleX();
|
|
5185
|
-
const x0 = -this.stage.x() / s;
|
|
5186
|
-
const y0 = -this.stage.y() / s;
|
|
5187
|
-
const x1 = (this.stage.width() - this.stage.x()) / s;
|
|
5188
|
-
const y1 = (this.stage.height() - this.stage.y()) / s;
|
|
5189
6925
|
let count = 0;
|
|
5190
|
-
for (const seat of this.
|
|
6926
|
+
for (const seat of this.visibleSeatCandidates()) {
|
|
5191
6927
|
if (seat.kind === "booth") continue;
|
|
5192
|
-
|
|
5193
|
-
if (
|
|
6928
|
+
const screen = this.worldToScreen(seat);
|
|
6929
|
+
if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
|
|
6930
|
+
if (this.accessGlyphById.has(seat.id) && this.accessGlyphShouldShow(seat.id)) continue;
|
|
5194
6931
|
const shape = this.circleById.get(seat.id);
|
|
5195
|
-
if (
|
|
6932
|
+
if (!shape?.isVisible() || this.focusedSeatOpacity(seat.id, shape.getAbsoluteOpacity()) < 0.5) continue;
|
|
5196
6933
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
6934
|
+
const labelPoint = this.renderedSeatPoint(seat);
|
|
6935
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
5197
6936
|
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
5198
6937
|
if (unavailable) {
|
|
5199
|
-
const cue = new import_Group.Group({ x:
|
|
6938
|
+
const cue = new import_Group.Group({ x: labelPoint.x, y: labelPoint.y, listening: false });
|
|
6939
|
+
cue.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
5200
6940
|
if (status === "held") {
|
|
5201
6941
|
cue.add(new import_Rect.Rect({
|
|
5202
6942
|
x: -4.2,
|
|
@@ -5225,14 +6965,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5225
6965
|
listening: false
|
|
5226
6966
|
}));
|
|
5227
6967
|
}
|
|
5228
|
-
this.
|
|
6968
|
+
this.seatLabelContainer(seat.id).add(cue);
|
|
5229
6969
|
if (++count >= MAX_LABELS) break;
|
|
5230
6970
|
continue;
|
|
5231
6971
|
}
|
|
5232
6972
|
const authoredFontSize = SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
5233
6973
|
const t2 = new import_Text.Text({
|
|
5234
|
-
x:
|
|
5235
|
-
y:
|
|
6974
|
+
x: labelPoint.x,
|
|
6975
|
+
y: labelPoint.y,
|
|
5236
6976
|
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
5237
6977
|
fontSize: authoredFontSize,
|
|
5238
6978
|
fontStyle: "600",
|
|
@@ -5253,11 +6993,36 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
5253
6993
|
}
|
|
5254
6994
|
t2.offsetX(t2.width() / 2);
|
|
5255
6995
|
t2.offsetY(t2.height() / 2);
|
|
5256
|
-
|
|
6996
|
+
t2.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
6997
|
+
this.seatLabelContainer(seat.id).add(t2);
|
|
5257
6998
|
this.seatLabelById.set(seat.id, t2);
|
|
5258
6999
|
if (++count >= MAX_LABELS) break;
|
|
5259
7000
|
}
|
|
5260
|
-
|
|
7001
|
+
for (const rl of this.rowLabelPlan) {
|
|
7002
|
+
const screen = this.worldToScreen({ x: rl.x, y: rl.y });
|
|
7003
|
+
if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
|
|
7004
|
+
const t2 = new import_Text.Text({
|
|
7005
|
+
x: rl.x,
|
|
7006
|
+
y: rl.y,
|
|
7007
|
+
text: rl.text,
|
|
7008
|
+
fontSize: rl.fontSize,
|
|
7009
|
+
fontStyle: "700",
|
|
7010
|
+
fontFamily: this.labelFont(),
|
|
7011
|
+
fill: rl.ink,
|
|
7012
|
+
rotation: rl.rotation,
|
|
7013
|
+
opacity: rl.opacity,
|
|
7014
|
+
shadowColor: "#05070c",
|
|
7015
|
+
shadowBlur: rl.ink.toLowerCase() === "#ffffff" ? 5 : 0,
|
|
7016
|
+
shadowOpacity: rl.ink.toLowerCase() === "#ffffff" ? 0.8 : 0,
|
|
7017
|
+
shadowForStrokeEnabled: false,
|
|
7018
|
+
listening: false,
|
|
7019
|
+
perfectDrawEnabled: false
|
|
7020
|
+
});
|
|
7021
|
+
t2.offsetX(t2.width() / 2);
|
|
7022
|
+
t2.offsetY(t2.height() / 2);
|
|
7023
|
+
this.labelGroup.add(t2);
|
|
7024
|
+
}
|
|
7025
|
+
if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
|
|
5261
7026
|
this.overlayLayer.batchDraw();
|
|
5262
7027
|
}
|
|
5263
7028
|
handleResize() {
|
|
@@ -5323,6 +7088,7 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
5323
7088
|
|
|
5324
7089
|
// src/core/bestAvailable.ts
|
|
5325
7090
|
function seatIndex2(seat, fallback) {
|
|
7091
|
+
if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
|
|
5326
7092
|
const i = seat.id.lastIndexOf(":");
|
|
5327
7093
|
if (i < 0) return fallback;
|
|
5328
7094
|
const n = Number(seat.id.slice(i + 1));
|
|
@@ -5342,21 +7108,25 @@ function centroid(seats) {
|
|
|
5342
7108
|
}
|
|
5343
7109
|
return { x: x / seats.length, y: y / seats.length };
|
|
5344
7110
|
}
|
|
7111
|
+
function candidateFocal(seats, fallback) {
|
|
7112
|
+
return seats.find((seat) => seat.focalPoint)?.focalPoint ?? fallback;
|
|
7113
|
+
}
|
|
5345
7114
|
function isPremium(seat) {
|
|
5346
7115
|
return seat.commercial?.premium === true;
|
|
5347
7116
|
}
|
|
5348
7117
|
function pickBestAvailable(seats, available, opts) {
|
|
5349
7118
|
const qty = Math.floor(opts.qty);
|
|
5350
7119
|
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
5351
|
-
const { categoryKey, focal, preferPremium } = opts;
|
|
7120
|
+
const { categoryKey, zoneId, focal, preferPremium } = opts;
|
|
5352
7121
|
const rows = /* @__PURE__ */ new Map();
|
|
5353
7122
|
const eligibleAll = [];
|
|
5354
7123
|
seats.forEach((seat, i) => {
|
|
5355
|
-
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
5356
|
-
|
|
7124
|
+
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey) && (!zoneId || seat.zoneId === zoneId);
|
|
7125
|
+
const rowId = seat.logicalRowId ?? seat.rowId;
|
|
7126
|
+
let arr = rows.get(rowId);
|
|
5357
7127
|
if (!arr) {
|
|
5358
7128
|
arr = [];
|
|
5359
|
-
rows.set(
|
|
7129
|
+
rows.set(rowId, arr);
|
|
5360
7130
|
}
|
|
5361
7131
|
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
5362
7132
|
if (elig) eligibleAll.push(seat);
|
|
@@ -5381,7 +7151,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5381
7151
|
continue;
|
|
5382
7152
|
}
|
|
5383
7153
|
let j = i;
|
|
5384
|
-
while (j < slots.length && slots[j].elig) j++;
|
|
7154
|
+
while (j < slots.length && slots[j].elig && (j === i || slots[j].index === slots[j - 1].index + 1)) j++;
|
|
5385
7155
|
const segLen = j - i;
|
|
5386
7156
|
for (let p = 0; p + qty <= segLen; p++) {
|
|
5387
7157
|
const leftRem = p;
|
|
@@ -5395,7 +7165,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5395
7165
|
startIndex: slots[i + p].index,
|
|
5396
7166
|
orphan,
|
|
5397
7167
|
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
5398
|
-
d2: dist2(centroid(runSeats), focal)
|
|
7168
|
+
d2: dist2(centroid(runSeats), candidateFocal(runSeats, focal))
|
|
5399
7169
|
};
|
|
5400
7170
|
if (better(c)) best = c;
|
|
5401
7171
|
}
|
|
@@ -5404,20 +7174,71 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5404
7174
|
}
|
|
5405
7175
|
if (best) return { labels: best.labels };
|
|
5406
7176
|
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
5407
|
-
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
|
|
7177
|
+
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, seat.focalPoint ?? focal) })).sort((a, b) => {
|
|
5408
7178
|
if (preferPremium) {
|
|
5409
7179
|
const pa = isPremium(a.seat) ? 0 : 1;
|
|
5410
7180
|
const pb = isPremium(b.seat) ? 0 : 1;
|
|
5411
7181
|
if (pa !== pb) return pa - pb;
|
|
5412
7182
|
}
|
|
5413
7183
|
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
5414
|
-
const r = a.seat.rowId.localeCompare(b.seat.rowId);
|
|
7184
|
+
const r = (a.seat.logicalRowId ?? a.seat.rowId).localeCompare(b.seat.logicalRowId ?? b.seat.rowId);
|
|
5415
7185
|
if (r !== 0) return r;
|
|
5416
7186
|
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
5417
7187
|
});
|
|
5418
7188
|
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
5419
7189
|
}
|
|
5420
7190
|
|
|
7191
|
+
// src/core/tableInventory.ts
|
|
7192
|
+
var GroupedTableProjectionError = class extends Error {
|
|
7193
|
+
constructor(objectId, message) {
|
|
7194
|
+
super(message);
|
|
7195
|
+
this.objectId = objectId;
|
|
7196
|
+
this.name = "GroupedTableProjectionError";
|
|
7197
|
+
}
|
|
7198
|
+
};
|
|
7199
|
+
function bounds(table) {
|
|
7200
|
+
const min = table.minOccupancy;
|
|
7201
|
+
const max = table.maxOccupancy;
|
|
7202
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > table.seatCount) {
|
|
7203
|
+
throw new GroupedTableProjectionError(
|
|
7204
|
+
table.id,
|
|
7205
|
+
`Table "${table.label}" has invalid variable-occupancy bounds`
|
|
7206
|
+
);
|
|
7207
|
+
}
|
|
7208
|
+
return { min, max };
|
|
7209
|
+
}
|
|
7210
|
+
function groupedTableInventory(doc, modelVersion) {
|
|
7211
|
+
if (modelVersion !== 2) return [];
|
|
7212
|
+
const units = [];
|
|
7213
|
+
for (const object of allObjects(doc)) {
|
|
7214
|
+
if (object.type !== "table" || !object.bookAsWhole && !object.variableOccupancy) continue;
|
|
7215
|
+
if (object.bookAsWhole && object.variableOccupancy) {
|
|
7216
|
+
throw new GroupedTableProjectionError(
|
|
7217
|
+
object.id,
|
|
7218
|
+
`Table "${object.label}" cannot be whole-table and variable-occupancy inventory`
|
|
7219
|
+
);
|
|
7220
|
+
}
|
|
7221
|
+
if (!Number.isInteger(object.seatCount) || object.seatCount < 1 || !object.label.trim()) {
|
|
7222
|
+
throw new GroupedTableProjectionError(object.id, "Grouped tables require a label and at least one chair");
|
|
7223
|
+
}
|
|
7224
|
+
const mode = object.variableOccupancy ? "variable" : "whole";
|
|
7225
|
+
const occupancy = mode === "variable" ? bounds(object) : { min: object.seatCount, max: object.seatCount };
|
|
7226
|
+
units.push({
|
|
7227
|
+
objectId: object.id,
|
|
7228
|
+
label: object.label,
|
|
7229
|
+
...object.displayLabel ? { displayLabel: object.displayLabel } : {},
|
|
7230
|
+
...object.displayType ? { displayType: object.displayType } : {},
|
|
7231
|
+
categoryKey: object.categoryKey,
|
|
7232
|
+
mode,
|
|
7233
|
+
capacity: object.seatCount,
|
|
7234
|
+
minOccupancy: occupancy.min,
|
|
7235
|
+
maxOccupancy: occupancy.max,
|
|
7236
|
+
chairs: expandTable(object)
|
|
7237
|
+
});
|
|
7238
|
+
}
|
|
7239
|
+
return units;
|
|
7240
|
+
}
|
|
7241
|
+
|
|
5421
7242
|
// src/picker/PickerController.ts
|
|
5422
7243
|
var DEFAULT_MAX_SELECTION = 10;
|
|
5423
7244
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -5449,6 +7270,14 @@ var PickerController = class {
|
|
|
5449
7270
|
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
5450
7271
|
this.seatContext = /* @__PURE__ */ new Map();
|
|
5451
7272
|
this.allIds = [];
|
|
7273
|
+
/** Model-2 tables are one booking unit whose chairs remain renderer geometry. */
|
|
7274
|
+
this.groupedTablesByObject = /* @__PURE__ */ new Map();
|
|
7275
|
+
this.groupedTablesByLabel = /* @__PURE__ */ new Map();
|
|
7276
|
+
this.groupedTableBySeatId = /* @__PURE__ */ new Map();
|
|
7277
|
+
this.tableQuantities = /* @__PURE__ */ new Map();
|
|
7278
|
+
/** Variable quantity is a buyer contract, never an inferred default. */
|
|
7279
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
7280
|
+
this.groupedSelectionOverhead = 0;
|
|
5452
7281
|
// realtime socket
|
|
5453
7282
|
this.ws = null;
|
|
5454
7283
|
this.reconnectTimer = null;
|
|
@@ -5510,6 +7339,20 @@ var PickerController = class {
|
|
|
5510
7339
|
idForLabel(label) {
|
|
5511
7340
|
return this.labelToId.get(label);
|
|
5512
7341
|
}
|
|
7342
|
+
idsForLabel(label) {
|
|
7343
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7344
|
+
if (table) return table.chairs.map((chair) => chair.id);
|
|
7345
|
+
const id = this.labelToId.get(label);
|
|
7346
|
+
return id ? [id] : [];
|
|
7347
|
+
}
|
|
7348
|
+
idsForLabels(labels) {
|
|
7349
|
+
return [...new Set([...labels].flatMap((label) => this.idsForLabel(label)))];
|
|
7350
|
+
}
|
|
7351
|
+
/** Resolve a physical chair id (or table inventory label) to its table unit. */
|
|
7352
|
+
tableSelection(seatIdOrLabel) {
|
|
7353
|
+
const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
|
|
7354
|
+
return group ? this.toTableSeat(group) : null;
|
|
7355
|
+
}
|
|
5513
7356
|
/** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
|
|
5514
7357
|
async render(host) {
|
|
5515
7358
|
if (this.renderer) return null;
|
|
@@ -5523,6 +7366,29 @@ var PickerController = class {
|
|
|
5523
7366
|
}
|
|
5524
7367
|
if (this.closed) return null;
|
|
5525
7368
|
this._doc = res.doc;
|
|
7369
|
+
try {
|
|
7370
|
+
const groups = groupedTableInventory(
|
|
7371
|
+
res.doc,
|
|
7372
|
+
res.event.inventoryModelVersion === 2 ? 2 : 1
|
|
7373
|
+
);
|
|
7374
|
+
this.groupedTablesByObject = new Map(groups.map((group) => [group.objectId, group]));
|
|
7375
|
+
this.groupedTablesByLabel = new Map(groups.map((group) => [group.label, group]));
|
|
7376
|
+
this.groupedTableBySeatId = new Map(
|
|
7377
|
+
groups.flatMap((group) => group.chairs.map((chair) => [chair.id, group]))
|
|
7378
|
+
);
|
|
7379
|
+
this.tableQuantities = new Map(groups.map((group) => [
|
|
7380
|
+
group.label,
|
|
7381
|
+
group.mode === "whole" ? group.capacity : group.minOccupancy
|
|
7382
|
+
]));
|
|
7383
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
7384
|
+
this.groupedSelectionOverhead = groups.reduce(
|
|
7385
|
+
(sum, group) => sum + Math.max(0, group.chairs.length - 1),
|
|
7386
|
+
0
|
|
7387
|
+
);
|
|
7388
|
+
} catch (err) {
|
|
7389
|
+
this.emitError(err);
|
|
7390
|
+
return null;
|
|
7391
|
+
}
|
|
5526
7392
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
5527
7393
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
5528
7394
|
this.seatById = /* @__PURE__ */ new Map();
|
|
@@ -5540,32 +7406,43 @@ var PickerController = class {
|
|
|
5540
7406
|
this.allIds.push(s.id);
|
|
5541
7407
|
const source = chartObjects.get(s.rowId);
|
|
5542
7408
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
5543
|
-
const
|
|
5544
|
-
const
|
|
5545
|
-
const
|
|
7409
|
+
const logicalRow = source?.type === "row" ? source.segmentedRow : void 0;
|
|
7410
|
+
const sourceDisplayLabel = logicalRow?.displayLabel ?? (source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel);
|
|
7411
|
+
const groupedTable = this.groupedTablesByObject.get(s.rowId);
|
|
7412
|
+
const objectType = source?.type === "table" ? "table" : source?.type === "booth" ? "booth" : "seat";
|
|
7413
|
+
const rowLabel = sourceDisplayLabel;
|
|
7414
|
+
const rowType = logicalRow?.displayType?.trim() || (source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0);
|
|
5546
7415
|
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
5547
7416
|
const labelParts = visibleSeatLabel.split("-");
|
|
5548
|
-
const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) :
|
|
7417
|
+
const seatNumber = groupedTable ? void 0 : s.kind === "booth" ? void 0 : sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
|
|
5549
7418
|
this.seatContext.set(s.id, {
|
|
7419
|
+
objectId: s.rowId,
|
|
7420
|
+
objectType,
|
|
5550
7421
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
5551
7422
|
rowLabel,
|
|
5552
7423
|
seatNumber,
|
|
5553
|
-
rowType
|
|
7424
|
+
...rowType ? { displayType: rowType, rowType } : {}
|
|
5554
7425
|
});
|
|
5555
7426
|
}
|
|
7427
|
+
for (const group of this.groupedTablesByLabel.values()) {
|
|
7428
|
+
const representative = group.chairs[0];
|
|
7429
|
+
if (!representative) continue;
|
|
7430
|
+
this.labelToId.set(group.label, representative.id);
|
|
7431
|
+
this.labelToSeat.set(group.label, representative);
|
|
7432
|
+
}
|
|
5556
7433
|
const currency = res.event.currency ?? this.opts.currency;
|
|
5557
7434
|
this.currency = currency ?? "USD";
|
|
5558
7435
|
const renderer = createRenderer(host, {
|
|
5559
|
-
|
|
7436
|
+
// Group chairs are selected together for paint, but count as one logical
|
|
7437
|
+
// inventory line. The controller enforces the guest-weighted cap.
|
|
7438
|
+
maxSelection: this.rendererSelectionCap(),
|
|
5560
7439
|
confirmSelection: this.opts.confirmSelection,
|
|
5561
7440
|
currency,
|
|
5562
7441
|
onSelect: (seat) => {
|
|
5563
|
-
this.
|
|
5564
|
-
this.emitSelectionChange();
|
|
7442
|
+
this.handleRendererSelect(seat);
|
|
5565
7443
|
},
|
|
5566
7444
|
onDeselect: (seat) => {
|
|
5567
|
-
this.
|
|
5568
|
-
this.emitSelectionChange();
|
|
7445
|
+
this.handleRendererDeselect(seat);
|
|
5569
7446
|
},
|
|
5570
7447
|
onSelectionLimit: this.opts.onSelectionLimit,
|
|
5571
7448
|
onHover: (seat) => {
|
|
@@ -5620,7 +7497,20 @@ var PickerController = class {
|
|
|
5620
7497
|
// ---- selection ------------------------------------------------------------
|
|
5621
7498
|
getSelection() {
|
|
5622
7499
|
if (!this.renderer) return [];
|
|
5623
|
-
|
|
7500
|
+
const out = [];
|
|
7501
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
7502
|
+
for (const seat of this.renderer.getSelection()) {
|
|
7503
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7504
|
+
if (table) {
|
|
7505
|
+
if (!grouped.has(table.label)) {
|
|
7506
|
+
grouped.add(table.label);
|
|
7507
|
+
out.push(this.toTableSeat(table));
|
|
7508
|
+
}
|
|
7509
|
+
} else {
|
|
7510
|
+
out.push(this.toSeat(seat));
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
return out;
|
|
5624
7514
|
}
|
|
5625
7515
|
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
5626
7516
|
seatDetails(seatId) {
|
|
@@ -5629,20 +7519,168 @@ var PickerController = class {
|
|
|
5629
7519
|
}
|
|
5630
7520
|
clearSelection() {
|
|
5631
7521
|
this.renderer?.clearSelection();
|
|
7522
|
+
this.resetUnheldTableQuantities();
|
|
5632
7523
|
this.emitSelectionChange();
|
|
5633
7524
|
}
|
|
5634
7525
|
deselect(ids) {
|
|
5635
|
-
|
|
7526
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7527
|
+
for (const id of ids) {
|
|
7528
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7529
|
+
if (table) {
|
|
7530
|
+
table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7531
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7532
|
+
this.tableQuantities.set(
|
|
7533
|
+
table.label,
|
|
7534
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7535
|
+
);
|
|
7536
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7537
|
+
}
|
|
7538
|
+
} else expanded.add(id);
|
|
7539
|
+
}
|
|
7540
|
+
this.renderer?.deselect([...expanded]);
|
|
5636
7541
|
this.emitSelectionChange();
|
|
5637
7542
|
}
|
|
5638
7543
|
setMaxSelection(maxSelection) {
|
|
5639
7544
|
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
5640
|
-
this.renderer?.setMaxSelection?.(this.
|
|
7545
|
+
this.renderer?.setMaxSelection?.(this.rendererSelectionCap());
|
|
5641
7546
|
}
|
|
5642
7547
|
select(ids) {
|
|
5643
|
-
const
|
|
7548
|
+
const before = new Set(this.getSelection().map((seat) => seat.label));
|
|
7549
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7550
|
+
for (const id of ids) {
|
|
7551
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7552
|
+
if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7553
|
+
else expanded.add(id);
|
|
7554
|
+
}
|
|
7555
|
+
this.renderer?.select?.([...expanded]);
|
|
7556
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7557
|
+
this.renderer?.deselect([...expanded]);
|
|
7558
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7559
|
+
return [];
|
|
7560
|
+
}
|
|
7561
|
+
const added = this.getSelection().filter((seat) => !before.has(seat.label));
|
|
5644
7562
|
if (added.length) this.emitSelectionChange();
|
|
5645
|
-
return added
|
|
7563
|
+
return added;
|
|
7564
|
+
}
|
|
7565
|
+
/** Confirm/update the guest quantity for a selected variable table. */
|
|
7566
|
+
setTableQuantity(label, quantity) {
|
|
7567
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7568
|
+
if (!table) return false;
|
|
7569
|
+
const q = table.mode === "whole" ? table.capacity : Math.floor(quantity);
|
|
7570
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return false;
|
|
7571
|
+
const previous = this.tableQuantities.get(label) ?? table.minOccupancy;
|
|
7572
|
+
this.tableQuantities.set(label, q);
|
|
7573
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7574
|
+
this.tableQuantities.set(label, previous);
|
|
7575
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7576
|
+
return false;
|
|
7577
|
+
}
|
|
7578
|
+
if (table.mode === "variable") this.confirmedVariableTables.add(label);
|
|
7579
|
+
this.emitSelectionChange();
|
|
7580
|
+
return true;
|
|
7581
|
+
}
|
|
7582
|
+
/** Atomically replace an active variable-table hold with a new guest count. */
|
|
7583
|
+
async replaceTableQuantity(label, quantity) {
|
|
7584
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7585
|
+
const hold = this.hold_;
|
|
7586
|
+
if (!table || table.mode !== "variable" || !hold?.labels.includes(label)) return null;
|
|
7587
|
+
const q = Math.floor(quantity);
|
|
7588
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return null;
|
|
7589
|
+
const otherGuests = (hold.items ?? []).filter((item) => item.label !== label).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
7590
|
+
if (otherGuests + q > this.maxSelection) {
|
|
7591
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
const selections = (hold.items ?? []).map((item) => ({
|
|
7595
|
+
label: item.label,
|
|
7596
|
+
tierId: item.tierId,
|
|
7597
|
+
quantity: item.label === label ? q : item.quantity
|
|
7598
|
+
}));
|
|
7599
|
+
if (!selections.some((item) => item.label === label)) return null;
|
|
7600
|
+
const result = await this.api.hold(this.key, selections, void 0, hold.holdId);
|
|
7601
|
+
this.tableQuantities.set(label, q);
|
|
7602
|
+
this.setHold({
|
|
7603
|
+
holdId: result.holdId,
|
|
7604
|
+
labels: selections.map((item) => item.label),
|
|
7605
|
+
expiresAt: result.expiresAt,
|
|
7606
|
+
items: result.items
|
|
7607
|
+
});
|
|
7608
|
+
return this.hold_;
|
|
7609
|
+
}
|
|
7610
|
+
rendererSelectionCap() {
|
|
7611
|
+
return this.maxSelection + this.groupedSelectionOverhead;
|
|
7612
|
+
}
|
|
7613
|
+
selectionGuestCount() {
|
|
7614
|
+
return this.getSelection().reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
|
|
7615
|
+
}
|
|
7616
|
+
handleRendererSelect(seat) {
|
|
7617
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7618
|
+
if (table) {
|
|
7619
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7620
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7621
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7622
|
+
this.emitSelectionChange();
|
|
7623
|
+
return;
|
|
7624
|
+
}
|
|
7625
|
+
this.renderer?.select?.(table.chairs.map((chair) => chair.id));
|
|
7626
|
+
this.opts.onSelect?.(table.chairs[0] ?? seat);
|
|
7627
|
+
this.opts.onTableSelectionRequest?.(this.toTableSeat(table));
|
|
7628
|
+
this.emitSelectionChange();
|
|
7629
|
+
return;
|
|
7630
|
+
}
|
|
7631
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7632
|
+
this.renderer?.deselect([seat.id]);
|
|
7633
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7634
|
+
this.emitSelectionChange();
|
|
7635
|
+
return;
|
|
7636
|
+
}
|
|
7637
|
+
this.opts.onSelect?.(seat);
|
|
7638
|
+
this.emitSelectionChange();
|
|
7639
|
+
}
|
|
7640
|
+
handleRendererDeselect(seat) {
|
|
7641
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7642
|
+
if (table) {
|
|
7643
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7644
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7645
|
+
this.tableQuantities.set(
|
|
7646
|
+
table.label,
|
|
7647
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7648
|
+
);
|
|
7649
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7650
|
+
}
|
|
7651
|
+
this.opts.onDeselect?.(table.chairs[0] ?? seat);
|
|
7652
|
+
} else {
|
|
7653
|
+
this.opts.onDeselect?.(seat);
|
|
7654
|
+
}
|
|
7655
|
+
this.emitSelectionChange();
|
|
7656
|
+
}
|
|
7657
|
+
resetUnheldTableQuantities() {
|
|
7658
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7659
|
+
if (this.hold_?.labels.includes(table.label)) continue;
|
|
7660
|
+
this.tableQuantities.set(
|
|
7661
|
+
table.label,
|
|
7662
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7663
|
+
);
|
|
7664
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7665
|
+
}
|
|
7666
|
+
}
|
|
7667
|
+
resetTableQuantitiesForLabels(labels) {
|
|
7668
|
+
for (const label of labels) {
|
|
7669
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7670
|
+
if (!table) continue;
|
|
7671
|
+
this.tableQuantities.set(
|
|
7672
|
+
label,
|
|
7673
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7674
|
+
);
|
|
7675
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(label);
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
tableQuantityRequest(seat) {
|
|
7679
|
+
if (seat?.objectType !== "table") return {};
|
|
7680
|
+
if (seat.bookingMode === "variable" && !this.confirmedVariableTables.has(seat.label)) {
|
|
7681
|
+
throw new Error("picker: confirm a variable table guest quantity before holding");
|
|
7682
|
+
}
|
|
7683
|
+
return { quantity: seat.quantity };
|
|
5646
7684
|
}
|
|
5647
7685
|
// ---- booking machine ------------------------------------------------------
|
|
5648
7686
|
/**
|
|
@@ -5654,7 +7692,7 @@ var PickerController = class {
|
|
|
5654
7692
|
async hold(labelsArg, ttlMs) {
|
|
5655
7693
|
const r = this.renderer;
|
|
5656
7694
|
if (!r) return null;
|
|
5657
|
-
const labels = labelsArg ??
|
|
7695
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5658
7696
|
const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
|
|
5659
7697
|
const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
|
|
5660
7698
|
if (!combinedLabels.length) return null;
|
|
@@ -5662,10 +7700,14 @@ var PickerController = class {
|
|
|
5662
7700
|
try {
|
|
5663
7701
|
const selections = combinedLabels.map((label) => {
|
|
5664
7702
|
const ga = existingGA.find((item) => item.label === label);
|
|
5665
|
-
if (ga) return { label, tierId: ga.tierId };
|
|
7703
|
+
if (ga) return { label, tierId: ga.tierId, quantity: ga.quantity };
|
|
5666
7704
|
const seat = this.labelToSeat.get(label);
|
|
5667
7705
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5668
|
-
return {
|
|
7706
|
+
return {
|
|
7707
|
+
label,
|
|
7708
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7709
|
+
...this.tableQuantityRequest(resolved)
|
|
7710
|
+
};
|
|
5669
7711
|
});
|
|
5670
7712
|
const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
|
|
5671
7713
|
this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
|
|
@@ -5717,11 +7759,19 @@ var PickerController = class {
|
|
|
5717
7759
|
const out = {};
|
|
5718
7760
|
const closedMembers = this.closedMemberIds();
|
|
5719
7761
|
for (const [id, s] of this.seatById) {
|
|
7762
|
+
if (this.groupedTableBySeatId.has(id)) continue;
|
|
5720
7763
|
if (closedMembers.has(id)) continue;
|
|
5721
7764
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
5722
7765
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
5723
7766
|
}
|
|
5724
7767
|
}
|
|
7768
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7769
|
+
if (table.chairs.some((chair) => closedMembers.has(chair.id))) continue;
|
|
7770
|
+
const representative = table.chairs[0];
|
|
7771
|
+
if (representative && (this.getStatus(representative.id) ?? "free") === "free") {
|
|
7772
|
+
out[table.categoryKey] = (out[table.categoryKey] ?? 0) + table.maxOccupancy;
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
5725
7775
|
return out;
|
|
5726
7776
|
}
|
|
5727
7777
|
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
@@ -5740,6 +7790,8 @@ var PickerController = class {
|
|
|
5740
7790
|
return {
|
|
5741
7791
|
id: area.id,
|
|
5742
7792
|
label: area.label,
|
|
7793
|
+
...area.displayLabel ? { displayLabel: area.displayLabel } : {},
|
|
7794
|
+
...area.displayType ? { displayType: area.displayType } : {},
|
|
5743
7795
|
capacity: Math.max(0, Math.floor(area.capacity)),
|
|
5744
7796
|
available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
|
|
5745
7797
|
categoryKey: area.categoryKey,
|
|
@@ -5758,14 +7810,20 @@ var PickerController = class {
|
|
|
5758
7810
|
const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
|
|
5759
7811
|
if (labels.length !== Math.floor(qty)) return null;
|
|
5760
7812
|
const selections = /* @__PURE__ */ new Map();
|
|
5761
|
-
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7813
|
+
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7814
|
+
label: item.label,
|
|
7815
|
+
tierId: item.tierId,
|
|
7816
|
+
quantity: item.quantity
|
|
7817
|
+
});
|
|
5762
7818
|
if (this.hold_ && !this.hold_?.items?.length) {
|
|
5763
7819
|
for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
|
|
5764
7820
|
}
|
|
5765
|
-
for (const seat of this.
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
7821
|
+
for (const seat of this.getSelection()) {
|
|
7822
|
+
selections.set(seat.label, {
|
|
7823
|
+
label: seat.label,
|
|
7824
|
+
...seat.tierId ? { tierId: seat.tierId } : {},
|
|
7825
|
+
...this.tableQuantityRequest(seat)
|
|
7826
|
+
});
|
|
5769
7827
|
}
|
|
5770
7828
|
for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
|
|
5771
7829
|
const combined = [...selections.values()];
|
|
@@ -5782,6 +7840,15 @@ var PickerController = class {
|
|
|
5782
7840
|
}
|
|
5783
7841
|
return false;
|
|
5784
7842
|
}
|
|
7843
|
+
/** Zones which still own visible buyer inventory, in authored order. */
|
|
7844
|
+
getBestAvailableZones() {
|
|
7845
|
+
const doc = this.visibleDoc();
|
|
7846
|
+
if (!doc?.zones?.length) return [];
|
|
7847
|
+
const used = new Set(
|
|
7848
|
+
computeSections(doc).sections.filter((section) => section.seatCount > 0 && !!section.zone).map((section) => section.zone)
|
|
7849
|
+
);
|
|
7850
|
+
return doc.zones.filter((zone) => used.has(zone.id)).map((zone) => ({ id: zone.id, label: zone.label }));
|
|
7851
|
+
}
|
|
5785
7852
|
/**
|
|
5786
7853
|
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5787
7854
|
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
@@ -5790,11 +7857,12 @@ var PickerController = class {
|
|
|
5790
7857
|
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5791
7858
|
* exists (the caller then falls back to the normal server pick).
|
|
5792
7859
|
*/
|
|
5793
|
-
pickPremiumBlock(qty, categoryKey) {
|
|
7860
|
+
pickPremiumBlock(qty, categoryKey, zoneId) {
|
|
5794
7861
|
const doc = this.visibleDoc();
|
|
5795
7862
|
if (!doc?.objects?.length) return null;
|
|
5796
7863
|
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5797
|
-
const
|
|
7864
|
+
const groupedObjectIds = new Set(this.groupedTablesByObject.keys());
|
|
7865
|
+
const seats = expandChart(doc).filter((seat) => !groupedObjectIds.has(seat.rowId));
|
|
5798
7866
|
const held = new Set(this.hold_?.labels ?? []);
|
|
5799
7867
|
const available = /* @__PURE__ */ new Set();
|
|
5800
7868
|
for (const seat of seats) {
|
|
@@ -5803,7 +7871,7 @@ var PickerController = class {
|
|
|
5803
7871
|
const status = id ? this.getStatus(id) : void 0;
|
|
5804
7872
|
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5805
7873
|
}
|
|
5806
|
-
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
7874
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, zoneId, focal, preferPremium: true });
|
|
5807
7875
|
if (pick.labels.length !== qty) return null;
|
|
5808
7876
|
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5809
7877
|
return allPremium ? [...pick.labels] : null;
|
|
@@ -5818,7 +7886,7 @@ var PickerController = class {
|
|
|
5818
7886
|
const r = this.renderer;
|
|
5819
7887
|
if (!r) return null;
|
|
5820
7888
|
if (opts.preferPremium) {
|
|
5821
|
-
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
7889
|
+
const block = this.pickPremiumBlock(qty, categoryKey, opts.zoneId);
|
|
5822
7890
|
if (block) {
|
|
5823
7891
|
if (this.hold_ && !await this.release()) return null;
|
|
5824
7892
|
try {
|
|
@@ -5829,7 +7897,7 @@ var PickerController = class {
|
|
|
5829
7897
|
});
|
|
5830
7898
|
const result = await this.api.hold(this.key, selection);
|
|
5831
7899
|
r.clearSelection();
|
|
5832
|
-
const ids =
|
|
7900
|
+
const ids = this.idsForLabels(block);
|
|
5833
7901
|
if (ids.length) r.setStatus(ids, "held");
|
|
5834
7902
|
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5835
7903
|
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5846,9 +7914,9 @@ var PickerController = class {
|
|
|
5846
7914
|
}
|
|
5847
7915
|
if (this.hold_ && !await this.release()) return null;
|
|
5848
7916
|
try {
|
|
5849
|
-
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
7917
|
+
const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
|
|
5850
7918
|
r.clearSelection();
|
|
5851
|
-
const ids =
|
|
7919
|
+
const ids = this.idsForLabels(result.labels);
|
|
5852
7920
|
if (ids.length) r.setStatus(ids, "held");
|
|
5853
7921
|
this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
|
|
5854
7922
|
const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5872,7 +7940,7 @@ var PickerController = class {
|
|
|
5872
7940
|
const r = this.renderer;
|
|
5873
7941
|
if (!r) return null;
|
|
5874
7942
|
if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
|
|
5875
|
-
const labels = labelsArg ??
|
|
7943
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5876
7944
|
if (!labels.length) return null;
|
|
5877
7945
|
let holdId;
|
|
5878
7946
|
try {
|
|
@@ -5885,7 +7953,11 @@ var PickerController = class {
|
|
|
5885
7953
|
labels.map((label) => {
|
|
5886
7954
|
const seat = this.labelToSeat.get(label);
|
|
5887
7955
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5888
|
-
return {
|
|
7956
|
+
return {
|
|
7957
|
+
label,
|
|
7958
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7959
|
+
...this.tableQuantityRequest(resolved)
|
|
7960
|
+
};
|
|
5889
7961
|
}),
|
|
5890
7962
|
void 0,
|
|
5891
7963
|
replaceHoldId
|
|
@@ -5901,10 +7973,11 @@ var PickerController = class {
|
|
|
5901
7973
|
this.opts.onSalesClosed?.();
|
|
5902
7974
|
} else if (status === 409 && conflicts?.length) {
|
|
5903
7975
|
const takenLabels = new Set(conflicts.map((c) => c.label));
|
|
5904
|
-
const takenIds =
|
|
7976
|
+
const takenIds = this.idsForLabels(takenLabels);
|
|
5905
7977
|
if (takenIds.length) {
|
|
5906
7978
|
r.setStatus(takenIds, "booked");
|
|
5907
7979
|
r.deselect(takenIds);
|
|
7980
|
+
this.resetTableQuantitiesForLabels(takenLabels);
|
|
5908
7981
|
}
|
|
5909
7982
|
const stillFree = labels.filter((l) => !takenLabels.has(l));
|
|
5910
7983
|
if (stillFree.length && holdId) void this.api.release(this.key, stillFree, holdId).catch(() => {
|
|
@@ -5916,12 +7989,13 @@ var PickerController = class {
|
|
|
5916
7989
|
}
|
|
5917
7990
|
throw err;
|
|
5918
7991
|
}
|
|
5919
|
-
const ids =
|
|
7992
|
+
const ids = this.idsForLabels(labels);
|
|
5920
7993
|
if (ids.length) {
|
|
5921
7994
|
r.setStatus(ids, "booked");
|
|
5922
7995
|
r.deselect(ids);
|
|
5923
7996
|
}
|
|
5924
7997
|
this.clearHold();
|
|
7998
|
+
this.resetTableQuantitiesForLabels(labels);
|
|
5925
7999
|
this.emitSelectionChange();
|
|
5926
8000
|
this.opts.onBook?.(bookingRef);
|
|
5927
8001
|
return labels;
|
|
@@ -5942,7 +8016,8 @@ var PickerController = class {
|
|
|
5942
8016
|
}
|
|
5943
8017
|
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
5944
8018
|
this.clearHold();
|
|
5945
|
-
|
|
8019
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
8020
|
+
const ids = this.idsForLabels(hold.labels);
|
|
5946
8021
|
if (ids.length) {
|
|
5947
8022
|
this.renderer?.deselect(ids);
|
|
5948
8023
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5973,7 +8048,8 @@ var PickerController = class {
|
|
|
5973
8048
|
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
5974
8049
|
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
5975
8050
|
else this.clearHold();
|
|
5976
|
-
|
|
8051
|
+
this.resetTableQuantitiesForLabels(drop);
|
|
8052
|
+
const ids = this.idsForLabels(drop);
|
|
5977
8053
|
if (ids.length) {
|
|
5978
8054
|
this.renderer?.deselect(ids);
|
|
5979
8055
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -6062,7 +8138,7 @@ var PickerController = class {
|
|
|
6062
8138
|
this.opts.onDeckTap?.(floorId);
|
|
6063
8139
|
}
|
|
6064
8140
|
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
6065
|
-
/** Switch the map projection
|
|
8141
|
+
/** Switch the map projection. No-op on a flat-only renderer. */
|
|
6066
8142
|
setViewMode(mode) {
|
|
6067
8143
|
this.renderer?.setViewMode?.(mode);
|
|
6068
8144
|
}
|
|
@@ -6161,12 +8237,21 @@ var PickerController = class {
|
|
|
6161
8237
|
if (!sec) return null;
|
|
6162
8238
|
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
6163
8239
|
const byCat = /* @__PURE__ */ new Map();
|
|
8240
|
+
const seenTables = /* @__PURE__ */ new Set();
|
|
6164
8241
|
let seatsLeft = 0;
|
|
6165
8242
|
for (const sid of memberIds) {
|
|
6166
8243
|
const seat = this.seatById.get(sid);
|
|
6167
8244
|
if (!seat) continue;
|
|
6168
|
-
|
|
6169
|
-
if (
|
|
8245
|
+
const table = this.groupedTableBySeatId.get(sid);
|
|
8246
|
+
if (table) {
|
|
8247
|
+
if (seenTables.has(table.label)) continue;
|
|
8248
|
+
seenTables.add(table.label);
|
|
8249
|
+
byCat.set(table.categoryKey, (byCat.get(table.categoryKey) ?? 0) + table.maxOccupancy);
|
|
8250
|
+
if (r.getStatus(table.chairs[0]?.id ?? sid) === "free") seatsLeft += table.maxOccupancy;
|
|
8251
|
+
} else {
|
|
8252
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
8253
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
8254
|
+
}
|
|
6170
8255
|
}
|
|
6171
8256
|
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
6172
8257
|
const cat = doc.categories.find((c) => c.key === key);
|
|
@@ -6183,7 +8268,7 @@ var PickerController = class {
|
|
|
6183
8268
|
const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
|
|
6184
8269
|
return {
|
|
6185
8270
|
id,
|
|
6186
|
-
label: sec.label,
|
|
8271
|
+
label: sec.displayLabel ?? sec.label,
|
|
6187
8272
|
zoneLabel: zone?.label ?? "",
|
|
6188
8273
|
...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
|
|
6189
8274
|
color,
|
|
@@ -6216,22 +8301,54 @@ var PickerController = class {
|
|
|
6216
8301
|
}
|
|
6217
8302
|
// ---- internals ------------------------------------------------------------
|
|
6218
8303
|
toSeat(s) {
|
|
8304
|
+
const table = this.groupedTableBySeatId.get(s.id);
|
|
8305
|
+
if (table) return this.toTableSeat(table);
|
|
6219
8306
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
6220
8307
|
const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
|
|
8308
|
+
const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
|
|
8309
|
+
const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
|
|
8310
|
+
const context = this.seatContext.get(s.id);
|
|
6221
8311
|
const tiers = this.tiersFor(s.categoryKey);
|
|
6222
8312
|
if (!tiers) {
|
|
6223
|
-
return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
8313
|
+
return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
|
|
6224
8314
|
}
|
|
6225
8315
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
6226
8316
|
return {
|
|
6227
8317
|
id: s.id,
|
|
6228
8318
|
label: s.label,
|
|
6229
8319
|
...display,
|
|
8320
|
+
...context,
|
|
6230
8321
|
categoryKey: s.categoryKey,
|
|
6231
8322
|
price: chosen.price,
|
|
6232
8323
|
tiers,
|
|
6233
8324
|
tierId: chosen.id,
|
|
6234
|
-
...commercial
|
|
8325
|
+
...commercial,
|
|
8326
|
+
...accessibility,
|
|
8327
|
+
...wheelchair
|
|
8328
|
+
};
|
|
8329
|
+
}
|
|
8330
|
+
toTableSeat(table) {
|
|
8331
|
+
const representative = table.chairs[0];
|
|
8332
|
+
const tiers = this.tiersFor(table.categoryKey);
|
|
8333
|
+
const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
|
|
8334
|
+
return {
|
|
8335
|
+
id: representative?.id ?? table.objectId,
|
|
8336
|
+
label: table.label,
|
|
8337
|
+
displayLabel: table.displayLabel ?? table.label,
|
|
8338
|
+
...table.displayType ? { displayType: table.displayType, rowType: table.displayType } : {},
|
|
8339
|
+
objectId: table.objectId,
|
|
8340
|
+
sectionLabel: representative ? this.seatContext.get(representative.id)?.sectionLabel : void 0,
|
|
8341
|
+
rowLabel: table.displayLabel ?? table.label,
|
|
8342
|
+
categoryKey: table.categoryKey,
|
|
8343
|
+
price: chosen?.price ?? this.priceFor(table.categoryKey),
|
|
8344
|
+
...tiers ? { tiers, tierId: chosen?.id } : {},
|
|
8345
|
+
objectType: "table",
|
|
8346
|
+
bookingMode: table.mode,
|
|
8347
|
+
quantity: this.tableQuantities.get(table.label) ?? (table.mode === "whole" ? table.capacity : table.minOccupancy),
|
|
8348
|
+
capacity: table.capacity,
|
|
8349
|
+
minOccupancy: table.minOccupancy,
|
|
8350
|
+
maxOccupancy: table.maxOccupancy,
|
|
8351
|
+
physicalSeatIds: table.chairs.map((chair) => chair.id)
|
|
6235
8352
|
};
|
|
6236
8353
|
}
|
|
6237
8354
|
/** Tooltip payload for a hovered seat — see PickerCallbacks.onSeatHover. */
|
|
@@ -6315,10 +8432,13 @@ var PickerController = class {
|
|
|
6315
8432
|
const h = this.hold_;
|
|
6316
8433
|
if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
|
|
6317
8434
|
const tiers = new Map((h.items ?? []).map((item) => [item.label, item.tierId]));
|
|
8435
|
+
const quantities = new Map((h.items ?? []).map((item) => [item.label, item.quantity ?? 1]));
|
|
6318
8436
|
return labels.every((label) => {
|
|
6319
8437
|
const seat = this.labelToSeat.get(label);
|
|
6320
|
-
const
|
|
6321
|
-
|
|
8438
|
+
const resolved = seat ? this.toSeat(seat) : null;
|
|
8439
|
+
const currentTier = resolved?.tierId ?? null;
|
|
8440
|
+
const quantityMatches = resolved?.objectType !== "table" || (resolved.bookingMode !== "variable" || this.confirmedVariableTables.has(label)) && (!quantities.has(label) || quantities.get(label) === resolved.quantity);
|
|
8441
|
+
return quantityMatches && (!tiers.has(label) || tiers.get(label) === currentTier);
|
|
6322
8442
|
});
|
|
6323
8443
|
}
|
|
6324
8444
|
handle409Conflicts(err) {
|
|
@@ -6326,19 +8446,28 @@ var PickerController = class {
|
|
|
6326
8446
|
if (status !== 409 || !conflicts?.length) return;
|
|
6327
8447
|
const r = this.renderer;
|
|
6328
8448
|
if (!r) return;
|
|
6329
|
-
const takenIds = conflicts.map((
|
|
8449
|
+
const takenIds = this.idsForLabels(conflicts.map((conflict) => conflict.label));
|
|
6330
8450
|
if (takenIds.length) {
|
|
6331
8451
|
r.deselect(takenIds);
|
|
6332
8452
|
r.setStatus(takenIds, "held");
|
|
8453
|
+
this.resetTableQuantitiesForLabels(conflicts.map((conflict) => conflict.label));
|
|
6333
8454
|
}
|
|
6334
8455
|
this.emitSelectionChange();
|
|
6335
8456
|
}
|
|
6336
8457
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
6337
8458
|
setHold(hold, source = "created") {
|
|
8459
|
+
for (const item of hold.items ?? []) {
|
|
8460
|
+
if (this.groupedTablesByLabel.has(item.label) && item.quantity != null) {
|
|
8461
|
+
this.tableQuantities.set(item.label, item.quantity);
|
|
8462
|
+
if (this.groupedTablesByLabel.get(item.label)?.mode === "variable") {
|
|
8463
|
+
this.confirmedVariableTables.add(item.label);
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
6338
8467
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
6339
8468
|
this.hold_ = full;
|
|
6340
8469
|
this.renderer?.setOwnedHold?.(
|
|
6341
|
-
|
|
8470
|
+
this.idsForLabels(full.labels)
|
|
6342
8471
|
);
|
|
6343
8472
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
6344
8473
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
@@ -6381,12 +8510,11 @@ var PickerController = class {
|
|
|
6381
8510
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
6382
8511
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
6383
8512
|
r.setOwnedHold?.(
|
|
6384
|
-
(this.hold_?.labels ?? [])
|
|
8513
|
+
this.idsForLabels(this.hold_?.labels ?? [])
|
|
6385
8514
|
);
|
|
6386
8515
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
6387
8516
|
for (const [label, st] of Object.entries(seats)) {
|
|
6388
|
-
|
|
6389
|
-
if (id) byStatus[mapStatus(st)].push(id);
|
|
8517
|
+
byStatus[mapStatus(st)].push(...this.idsForLabel(label));
|
|
6390
8518
|
}
|
|
6391
8519
|
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
6392
8520
|
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
@@ -6395,7 +8523,11 @@ var PickerController = class {
|
|
|
6395
8523
|
this.clearBookedHoldIfSettled();
|
|
6396
8524
|
}
|
|
6397
8525
|
clearBookedHoldIfSettled() {
|
|
6398
|
-
|
|
8526
|
+
const hold = this.hold_;
|
|
8527
|
+
if (hold?.labels.every((label) => this.liveStatuses.get(label) === "booked")) {
|
|
8528
|
+
this.clearHold();
|
|
8529
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
8530
|
+
}
|
|
6399
8531
|
}
|
|
6400
8532
|
async resnapshot() {
|
|
6401
8533
|
try {
|
|
@@ -6484,13 +8616,13 @@ var PickerController = class {
|
|
|
6484
8616
|
} else if (Array.isArray(m.changes)) {
|
|
6485
8617
|
for (const ch of m.changes) {
|
|
6486
8618
|
this.liveStatuses.set(ch.label, ch.status);
|
|
6487
|
-
const
|
|
6488
|
-
if (!
|
|
8619
|
+
const ids = this.idsForLabel(ch.label);
|
|
8620
|
+
if (!ids.length) continue;
|
|
6489
8621
|
const next = mapStatus(ch.status);
|
|
6490
|
-
if (this.opts.flashOnLiveChange && next !== "free" && r.getStatus(id) === "free" && !this.hold_?.labels.includes(ch.label)) {
|
|
6491
|
-
r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e");
|
|
8622
|
+
if (this.opts.flashOnLiveChange && next !== "free" && ids.some((id) => r.getStatus(id) === "free") && !this.hold_?.labels.includes(ch.label)) {
|
|
8623
|
+
ids.forEach((id) => r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e"));
|
|
6492
8624
|
}
|
|
6493
|
-
r.setStatus(
|
|
8625
|
+
r.setStatus(ids, next);
|
|
6494
8626
|
}
|
|
6495
8627
|
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
6496
8628
|
r.forceDraw();
|
|
@@ -6521,10 +8653,25 @@ var PickerController = class {
|
|
|
6521
8653
|
}
|
|
6522
8654
|
};
|
|
6523
8655
|
|
|
8656
|
+
// src/view/sightline.ts
|
|
8657
|
+
var STAGE_TOP_M = 5;
|
|
8658
|
+
var STAGE_BASE_M = -1.1;
|
|
8659
|
+
var MAX_LOOKDOWN_DEG = 35;
|
|
8660
|
+
function stageSightlinePitch(eyeHeightM, distM) {
|
|
8661
|
+
const extraEyeM = eyeHeightM - SEATED_EYE_HEIGHT_M;
|
|
8662
|
+
let topPitch = Math.atan2(STAGE_TOP_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8663
|
+
let basePitch = Math.atan2(STAGE_BASE_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8664
|
+
if (basePitch < -MAX_LOOKDOWN_DEG) {
|
|
8665
|
+
topPitch += -MAX_LOOKDOWN_DEG - basePitch;
|
|
8666
|
+
basePitch = -MAX_LOOKDOWN_DEG;
|
|
8667
|
+
}
|
|
8668
|
+
return { topPitch, basePitch };
|
|
8669
|
+
}
|
|
8670
|
+
|
|
6524
8671
|
// src/view/generatePanorama.ts
|
|
6525
8672
|
var W = 2048;
|
|
6526
8673
|
var H = 1024;
|
|
6527
|
-
var UNIT =
|
|
8674
|
+
var UNIT = METRES_PER_CHART_UNIT;
|
|
6528
8675
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
6529
8676
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
6530
8677
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
@@ -6544,9 +8691,11 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
6544
8691
|
sky.addColorStop(1, "#07090f");
|
|
6545
8692
|
ctx.fillStyle = sky;
|
|
6546
8693
|
ctx.fillRect(0, 0, W, H);
|
|
8694
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
6547
8695
|
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6548
|
-
const
|
|
6549
|
-
const
|
|
8696
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8697
|
+
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8698
|
+
const stageBasePitch = sightline.basePitch;
|
|
6550
8699
|
const sx0 = yawToX(-stageHalfYaw);
|
|
6551
8700
|
const sx1 = yawToX(stageHalfYaw);
|
|
6552
8701
|
const sy0 = pitchToY(stageTopPitch);
|
|
@@ -6653,9 +8802,11 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6653
8802
|
sky.addColorStop(1, "#070910");
|
|
6654
8803
|
ctx.fillStyle = sky;
|
|
6655
8804
|
ctx.fillRect(0, 0, TW, TH);
|
|
8805
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8806
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
6656
8807
|
const stageHalfYaw = Math.min(HALF_HFOV - 2, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6657
|
-
const stageTopPitch = Math.min(HALF_VFOV - 2,
|
|
6658
|
-
const stageBasePitch = Math.max(-HALF_VFOV + 2,
|
|
8808
|
+
const stageTopPitch = Math.min(HALF_VFOV - 2, sightline.topPitch);
|
|
8809
|
+
const stageBasePitch = Math.max(-HALF_VFOV + 2, sightline.basePitch);
|
|
6659
8810
|
const sx0 = yawToTX(-stageHalfYaw);
|
|
6660
8811
|
const sx1 = yawToTX(stageHalfYaw);
|
|
6661
8812
|
const sy0 = pitchToTY(stageTopPitch);
|
|
@@ -6753,7 +8904,9 @@ async function loadLocale(code) {
|
|
|
6753
8904
|
PickerController,
|
|
6754
8905
|
RENDERED_QUALITY_REPORT_VERSION,
|
|
6755
8906
|
SUPPORTED_LOCALES,
|
|
8907
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
6756
8908
|
SeatmapRenderer,
|
|
8909
|
+
TIER_HEIGHT_M,
|
|
6757
8910
|
UNGROUPED_ID,
|
|
6758
8911
|
accessibilityMeta,
|
|
6759
8912
|
accessibilityRingColor,
|
|
@@ -6768,16 +8921,19 @@ async function loadLocale(code) {
|
|
|
6768
8921
|
expandRow,
|
|
6769
8922
|
expandRowSlots,
|
|
6770
8923
|
expandTable,
|
|
8924
|
+
expandTableSlots,
|
|
6771
8925
|
floorObjects,
|
|
6772
8926
|
floorsOf,
|
|
6773
8927
|
formatDate,
|
|
6774
8928
|
formatMoney,
|
|
6775
8929
|
gaAreasOf,
|
|
8930
|
+
gaInventorySegments,
|
|
6776
8931
|
gaUnitLabel,
|
|
6777
8932
|
gaUnitLabels,
|
|
6778
8933
|
generateSeatPanorama,
|
|
6779
8934
|
generateSeatThumb,
|
|
6780
8935
|
getLocale,
|
|
8936
|
+
growJoinedGAInventory,
|
|
6781
8937
|
hiddenObjectIds,
|
|
6782
8938
|
inspectRenderedQualityEvidence,
|
|
6783
8939
|
isGaUnitLabel,
|
|
@@ -6785,17 +8941,23 @@ async function loadLocale(code) {
|
|
|
6785
8941
|
layerOf,
|
|
6786
8942
|
loadLocale,
|
|
6787
8943
|
objectCenter,
|
|
8944
|
+
owningSectionForObject,
|
|
6788
8945
|
pointInPolygon,
|
|
6789
8946
|
pointInPolygonWithHoles,
|
|
6790
8947
|
polygonLabelPoint,
|
|
6791
8948
|
resolveLocale,
|
|
8949
|
+
rowInventoryCount,
|
|
6792
8950
|
rowSeatPositions,
|
|
6793
8951
|
seatLabelPart,
|
|
8952
|
+
sectionGeometry,
|
|
6794
8953
|
setLocale,
|
|
6795
8954
|
setMoneyLocale,
|
|
6796
8955
|
setStringOverrides,
|
|
6797
8956
|
stackFloors,
|
|
6798
8957
|
t,
|
|
6799
|
-
tCount
|
|
8958
|
+
tCount,
|
|
8959
|
+
tableInventoryCount,
|
|
8960
|
+
tableSeatCountsBySide,
|
|
8961
|
+
validGAInventorySegments
|
|
6800
8962
|
});
|
|
6801
8963
|
//# sourceMappingURL=index.cjs.map
|