@seatlayer/core 0.24.0 → 0.26.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-EIG65UFU.js → de-73TIXYJH.js} +2 -1
- package/dist/{de-EIG65UFU.js.map → de-73TIXYJH.js.map} +1 -1
- package/dist/{es-47HFAWS6.js → es-SKAODLTR.js} +2 -1
- package/dist/{es-47HFAWS6.js.map → es-SKAODLTR.js.map} +1 -1
- package/dist/{fr-7IXNNFBS.js → fr-J4637T6V.js} +2 -1
- package/dist/{fr-7IXNNFBS.js.map → fr-J4637T6V.js.map} +1 -1
- package/dist/index.cjs +2575 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +784 -51
- package/dist/index.d.ts +784 -51
- package/dist/index.js +2561 -313
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65,6 +65,7 @@ var init_es = __esm({
|
|
|
65
65
|
"picker.seatsLeftInSection.one": "{count} asiento disponible",
|
|
66
66
|
"picker.seatsLeftInSection.other": "{count} asientos disponibles",
|
|
67
67
|
"picker.overview": "Descripci\xF3n general",
|
|
68
|
+
"picker.entrance": "Entrada",
|
|
68
69
|
"picker.tapSeatHint": "Toca cualquier asiento para ver su vista",
|
|
69
70
|
"picker.ticketTierFor": "Categor\xEDa de entrada para {label}",
|
|
70
71
|
"picker.viewFromSeat": "Vista desde el asiento {label}",
|
|
@@ -123,6 +124,7 @@ var init_de = __esm({
|
|
|
123
124
|
"picker.seatsLeftInSection.one": "{count} Platz verf\xFCgbar",
|
|
124
125
|
"picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
|
|
125
126
|
"picker.overview": "\xDCbersicht",
|
|
127
|
+
"picker.entrance": "Eingang",
|
|
126
128
|
"picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
|
|
127
129
|
"picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
|
|
128
130
|
"picker.viewFromSeat": "Ansicht von Platz {label}",
|
|
@@ -181,6 +183,7 @@ var init_fr = __esm({
|
|
|
181
183
|
"picker.seatsLeftInSection.one": "{count} si\xE8ge restant",
|
|
182
184
|
"picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
|
|
183
185
|
"picker.overview": "Vue d'ensemble",
|
|
186
|
+
"picker.entrance": "Entr\xE9e",
|
|
184
187
|
"picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
|
|
185
188
|
"picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
|
|
186
189
|
"picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
|
|
@@ -211,8 +214,11 @@ __export(index_exports, {
|
|
|
211
214
|
MAX_EVENT_INVENTORY: () => MAX_EVENT_INVENTORY,
|
|
212
215
|
MAX_GA_CAPACITY: () => MAX_GA_CAPACITY,
|
|
213
216
|
PickerController: () => PickerController,
|
|
217
|
+
RENDERED_QUALITY_REPORT_VERSION: () => RENDERED_QUALITY_REPORT_VERSION,
|
|
214
218
|
SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
|
|
219
|
+
SURROUNDINGS_SHAPE_ROLES: () => SURROUNDINGS_SHAPE_ROLES,
|
|
215
220
|
SeatmapRenderer: () => SeatmapRenderer,
|
|
221
|
+
TIER_HEIGHT_M: () => TIER_HEIGHT_M,
|
|
216
222
|
UNGROUPED_ID: () => UNGROUPED_ID,
|
|
217
223
|
accessibilityMeta: () => accessibilityMeta,
|
|
218
224
|
accessibilityRingColor: () => accessibilityRingColor,
|
|
@@ -227,34 +233,44 @@ __export(index_exports, {
|
|
|
227
233
|
expandRow: () => expandRow,
|
|
228
234
|
expandRowSlots: () => expandRowSlots,
|
|
229
235
|
expandTable: () => expandTable,
|
|
236
|
+
expandTableSlots: () => expandTableSlots,
|
|
230
237
|
floorObjects: () => floorObjects,
|
|
231
238
|
floorsOf: () => floorsOf,
|
|
232
239
|
formatDate: () => formatDate,
|
|
233
240
|
formatMoney: () => formatMoney,
|
|
234
241
|
gaAreasOf: () => gaAreasOf,
|
|
242
|
+
gaInventorySegments: () => gaInventorySegments,
|
|
235
243
|
gaUnitLabel: () => gaUnitLabel,
|
|
236
244
|
gaUnitLabels: () => gaUnitLabels,
|
|
237
245
|
generateSeatPanorama: () => generateSeatPanorama,
|
|
238
246
|
generateSeatThumb: () => generateSeatThumb,
|
|
239
247
|
getLocale: () => getLocale,
|
|
248
|
+
growJoinedGAInventory: () => growJoinedGAInventory,
|
|
240
249
|
hiddenObjectIds: () => hiddenObjectIds,
|
|
250
|
+
inspectRenderedQualityEvidence: () => inspectRenderedQualityEvidence,
|
|
241
251
|
isGaUnitLabel: () => isGaUnitLabel,
|
|
242
252
|
isSectionHidden: () => isSectionHidden,
|
|
243
253
|
layerOf: () => layerOf,
|
|
244
254
|
loadLocale: () => loadLocale,
|
|
245
255
|
objectCenter: () => objectCenter,
|
|
256
|
+
owningSectionForObject: () => owningSectionForObject,
|
|
246
257
|
pointInPolygon: () => pointInPolygon,
|
|
247
258
|
pointInPolygonWithHoles: () => pointInPolygonWithHoles,
|
|
248
259
|
polygonLabelPoint: () => polygonLabelPoint,
|
|
249
260
|
resolveLocale: () => resolveLocale,
|
|
261
|
+
rowInventoryCount: () => rowInventoryCount,
|
|
250
262
|
rowSeatPositions: () => rowSeatPositions,
|
|
251
263
|
seatLabelPart: () => seatLabelPart,
|
|
264
|
+
sectionGeometry: () => sectionGeometry,
|
|
252
265
|
setLocale: () => setLocale,
|
|
253
266
|
setMoneyLocale: () => setMoneyLocale,
|
|
254
267
|
setStringOverrides: () => setStringOverrides,
|
|
255
268
|
stackFloors: () => stackFloors,
|
|
256
269
|
t: () => t,
|
|
257
|
-
tCount: () => tCount
|
|
270
|
+
tCount: () => tCount,
|
|
271
|
+
tableInventoryCount: () => tableInventoryCount,
|
|
272
|
+
tableSeatCountsBySide: () => tableSeatCountsBySide,
|
|
273
|
+
validGAInventorySegments: () => validGAInventorySegments
|
|
258
274
|
});
|
|
259
275
|
module.exports = __toCommonJS(index_exports);
|
|
260
276
|
|
|
@@ -264,6 +280,7 @@ var ACCESSIBILITY_TYPES = [
|
|
|
264
280
|
{ key: "companion", label: "Companion seat", short: "Companion", icon: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" },
|
|
265
281
|
{ key: "semi-ambulatory", label: "Semi-ambulatory (limited mobility)", short: "Limited mobility", icon: "\u{1F9AF}" },
|
|
266
282
|
{ key: "hearing", label: "Assistive listening", short: "Hearing", icon: "\u{1F9BB}" },
|
|
283
|
+
{ key: "cart", label: "CART live-caption view", short: "CART captions", icon: "CC" },
|
|
267
284
|
{ key: "sign-language", label: "Sign-language view", short: "Sign language", icon: "\u{1F91F}" },
|
|
268
285
|
{ key: "plus-size", label: "Plus-size seat", short: "Plus-size", icon: "\u{1F4BA}" },
|
|
269
286
|
{ key: "lift-armrest", label: "Lift-up armrest", short: "Lift armrest", icon: "\u2195\uFE0F" }
|
|
@@ -277,6 +294,7 @@ var ACCESSIBILITY_RING_COLOR = {
|
|
|
277
294
|
companion: "#8b5cf6",
|
|
278
295
|
"semi-ambulatory": "#0ea5e9",
|
|
279
296
|
hearing: "#14b8a6",
|
|
297
|
+
cart: "#7c3aed",
|
|
280
298
|
"sign-language": "#f59e0b",
|
|
281
299
|
"plus-size": "#ec4899",
|
|
282
300
|
"lift-armrest": "#22c55e"
|
|
@@ -287,6 +305,19 @@ function accessibilityRingColor(types) {
|
|
|
287
305
|
}
|
|
288
306
|
var LABEL_STYLE_MIN_SIZE = 8;
|
|
289
307
|
var LABEL_STYLE_MAX_SIZE = 24;
|
|
308
|
+
var SURROUNDINGS_SHAPE_ROLES = [
|
|
309
|
+
"reference-focal",
|
|
310
|
+
"bar",
|
|
311
|
+
"entrance",
|
|
312
|
+
"exit",
|
|
313
|
+
"restroom",
|
|
314
|
+
"screen",
|
|
315
|
+
"sound",
|
|
316
|
+
"concession",
|
|
317
|
+
"coat",
|
|
318
|
+
"wall"
|
|
319
|
+
];
|
|
320
|
+
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
290
321
|
function layerOf(obj) {
|
|
291
322
|
switch (obj.type) {
|
|
292
323
|
case "row":
|
|
@@ -295,10 +326,17 @@ function layerOf(obj) {
|
|
|
295
326
|
case "booth":
|
|
296
327
|
case "section":
|
|
297
328
|
return "interactive";
|
|
298
|
-
//
|
|
329
|
+
// Raster décor follows its explicit authored z-layer. Absence retains the
|
|
330
|
+
// legacy Background default; bitmap content is never guessed semantically.
|
|
331
|
+
case "decorImage":
|
|
332
|
+
return obj.layer === "foreground" ? "foreground" : "background";
|
|
333
|
+
// Source-backed focal geometry and curated venue landmarks help an author
|
|
334
|
+
// orient around the sellable plan. A stage and an ordinary authored shape
|
|
335
|
+
// remain Background even when they carry an arbitrary/custom role.
|
|
299
336
|
case "shape":
|
|
337
|
+
return obj.role && SURROUNDINGS_SHAPE_ROLE_SET.has(obj.role) ? "surroundings" : "background";
|
|
338
|
+
// Free text — including semantic icon text — is background furniture.
|
|
300
339
|
case "text":
|
|
301
|
-
case "decorImage":
|
|
302
340
|
return "background";
|
|
303
341
|
default:
|
|
304
342
|
return "interactive";
|
|
@@ -418,10 +456,57 @@ function toRoman(value) {
|
|
|
418
456
|
return out;
|
|
419
457
|
}
|
|
420
458
|
|
|
459
|
+
// src/core/units.ts
|
|
460
|
+
var METRES_PER_CHART_UNIT = 0.55 / 24;
|
|
461
|
+
var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
|
|
462
|
+
var LIFT_PER_STEP_WORLD = 58;
|
|
463
|
+
var TIER_HEIGHT_M = LIFT_PER_STEP_WORLD * METRES_PER_CHART_UNIT;
|
|
464
|
+
var SECTION_ELEVATION_TIER_MIN = 0;
|
|
465
|
+
var SECTION_ELEVATION_TIER_MAX = 3;
|
|
466
|
+
var SECTION_HEIGHT_MIN_M = 0;
|
|
467
|
+
var SECTION_HEIGHT_MAX_M = 120;
|
|
468
|
+
var SECTION_RAKE_MIN_DEG = 0;
|
|
469
|
+
var SECTION_RAKE_MAX_DEG = 45;
|
|
470
|
+
var LEGACY_SECTION_ELEVATION_TIER_MAX = 7;
|
|
471
|
+
var SEATED_EYE_HEIGHT_M = 1.2;
|
|
472
|
+
function finiteClamped(value, min, max, fallback) {
|
|
473
|
+
return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
474
|
+
}
|
|
475
|
+
function sectionElevationTier(value) {
|
|
476
|
+
if (!Number.isFinite(value)) return SECTION_ELEVATION_TIER_MIN;
|
|
477
|
+
return Math.max(
|
|
478
|
+
SECTION_ELEVATION_TIER_MIN,
|
|
479
|
+
Math.min(SECTION_ELEVATION_TIER_MAX, Math.round(value))
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function compatibleAutomaticTier(value) {
|
|
483
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) return 0;
|
|
484
|
+
if (value <= LEGACY_SECTION_ELEVATION_TIER_MAX) return value;
|
|
485
|
+
return SECTION_ELEVATION_TIER_MAX;
|
|
486
|
+
}
|
|
487
|
+
function sectionGeometry(section, context = {}) {
|
|
488
|
+
const floorBaseHeightM = finiteClamped(
|
|
489
|
+
context.floorBaseHeightM,
|
|
490
|
+
SECTION_HEIGHT_MIN_M,
|
|
491
|
+
SECTION_HEIGHT_MAX_M,
|
|
492
|
+
0
|
|
493
|
+
);
|
|
494
|
+
const automaticHeight = Math.min(
|
|
495
|
+
SECTION_HEIGHT_MAX_M,
|
|
496
|
+
floorBaseHeightM + compatibleAutomaticTier(section.elevation) * TIER_HEIGHT_M
|
|
497
|
+
);
|
|
498
|
+
const height = section.height === void 0 ? automaticHeight : finiteClamped(section.height, SECTION_HEIGHT_MIN_M, SECTION_HEIGHT_MAX_M, automaticHeight);
|
|
499
|
+
const rake = finiteClamped(section.rake, SECTION_RAKE_MIN_DEG, SECTION_RAKE_MAX_DEG, 0);
|
|
500
|
+
return { height, rake };
|
|
501
|
+
}
|
|
502
|
+
|
|
421
503
|
// src/core/layout.ts
|
|
422
504
|
function overrideAccessibility(o) {
|
|
423
505
|
if (!o) return [];
|
|
424
|
-
if (o.accessibility && o.accessibility.length)
|
|
506
|
+
if (o.accessibility && o.accessibility.length) {
|
|
507
|
+
return o.wheelchairSpaceType && !o.accessibility.includes("wheelchair") ? ["wheelchair", ...o.accessibility] : o.accessibility;
|
|
508
|
+
}
|
|
509
|
+
if (o.wheelchairSpaceType) return ["wheelchair"];
|
|
425
510
|
return o.accessible ? ["wheelchair"] : [];
|
|
426
511
|
}
|
|
427
512
|
var DEG = Math.PI / 180;
|
|
@@ -463,6 +548,10 @@ function overrideMap(row) {
|
|
|
463
548
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
464
549
|
return m;
|
|
465
550
|
}
|
|
551
|
+
function rowInventoryCount(row) {
|
|
552
|
+
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));
|
|
553
|
+
return Math.max(0, row.seatCount - skipped.size);
|
|
554
|
+
}
|
|
466
555
|
function centerRank(n) {
|
|
467
556
|
const rank = new Array(n);
|
|
468
557
|
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);
|
|
@@ -476,9 +565,9 @@ function seatLabelPart(row, i) {
|
|
|
476
565
|
const prefix = row.seatNumbering?.prefix ?? "";
|
|
477
566
|
const endAt = row.seatNumbering?.endAt;
|
|
478
567
|
const n = row.seatCount;
|
|
479
|
-
if (scheme === "updown") {
|
|
568
|
+
if (scheme === "updown" || scheme === "updown-descending") {
|
|
480
569
|
const half = Math.ceil(n / 2);
|
|
481
|
-
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
570
|
+
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);
|
|
482
571
|
return `${prefix}${core2}`;
|
|
483
572
|
}
|
|
484
573
|
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
@@ -531,6 +620,7 @@ function expandRowSlots(row) {
|
|
|
531
620
|
skipped: !!o?.skip,
|
|
532
621
|
accessible: accessibility.length > 0,
|
|
533
622
|
accessibility,
|
|
623
|
+
wheelchairSpaceType: o?.wheelchairSpaceType,
|
|
534
624
|
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
535
625
|
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl,
|
|
536
626
|
labelStyle: o?.labelStyle
|
|
@@ -551,6 +641,7 @@ function expandRow(row) {
|
|
|
551
641
|
categoryKey: slot.categoryKey,
|
|
552
642
|
accessible: slot.accessible || void 0,
|
|
553
643
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
644
|
+
wheelchairSpaceType: slot.wheelchairSpaceType,
|
|
554
645
|
commercial: slot.commercial,
|
|
555
646
|
viewUrl: slot.viewUrl,
|
|
556
647
|
labelStyle: slot.labelStyle
|
|
@@ -558,18 +649,43 @@ function expandRow(row) {
|
|
|
558
649
|
}
|
|
559
650
|
return seats;
|
|
560
651
|
}
|
|
561
|
-
function
|
|
652
|
+
function tableSeatCountsBySide(t2) {
|
|
653
|
+
if (t2.seatCountsBySide) return { ...t2.seatCountsBySide };
|
|
654
|
+
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
655
|
+
const order = ["top", "bottom", "left", "right"].filter((side) => enabled.includes(side));
|
|
656
|
+
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
657
|
+
if (!order.length) return counts;
|
|
658
|
+
const n = Math.max(0, Math.round(t2.seatCount));
|
|
659
|
+
for (let index = 0; index < n; index++) counts[order[index % order.length]] += 1;
|
|
660
|
+
return counts;
|
|
661
|
+
}
|
|
662
|
+
function expandTableSlots(t2) {
|
|
562
663
|
const seats = [];
|
|
563
664
|
const n = Math.max(0, Math.round(t2.seatCount));
|
|
564
665
|
if (n === 0) return seats;
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
666
|
+
const overrides2 = new Map((t2.overrides ?? []).map((override) => [override.index, override]));
|
|
667
|
+
const mk = (index, x, y, side) => {
|
|
668
|
+
const override = overrides2.get(index);
|
|
669
|
+
const accessibility = overrideAccessibility(override);
|
|
670
|
+
const label = override?.label ?? `${t2.label}-${index + 1}`;
|
|
671
|
+
const displayPrefix = t2.displayLabel ?? t2.label;
|
|
672
|
+
return {
|
|
673
|
+
index,
|
|
674
|
+
label,
|
|
675
|
+
displayLabel: override?.displayLabel ?? `${displayPrefix}-${index + 1}`,
|
|
676
|
+
x: x + (override?.dx ?? 0),
|
|
677
|
+
y: y + (override?.dy ?? 0),
|
|
678
|
+
categoryKey: override?.categoryKey ?? t2.categoryKey,
|
|
679
|
+
skipped: !!override?.skip,
|
|
680
|
+
accessible: accessibility.length > 0,
|
|
681
|
+
accessibility,
|
|
682
|
+
wheelchairSpaceType: override?.wheelchairSpaceType,
|
|
683
|
+
commercial: override?.commercial,
|
|
684
|
+
viewUrl: override?.viewFromSeatUrl,
|
|
685
|
+
labelStyle: override?.labelStyle,
|
|
686
|
+
...side ? { side } : {}
|
|
687
|
+
};
|
|
688
|
+
};
|
|
573
689
|
if (t2.shape === "round") {
|
|
574
690
|
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
575
691
|
const base = t2.rotation * DEG;
|
|
@@ -592,17 +708,11 @@ function expandTable(t2) {
|
|
|
592
708
|
}
|
|
593
709
|
const w = t2.width ?? 80;
|
|
594
710
|
const h = t2.height ?? 50;
|
|
595
|
-
const
|
|
596
|
-
const order = ["top", "bottom", "left", "right"]
|
|
597
|
-
if (!order.length) return seats;
|
|
598
|
-
const counts = new Map(order.map((s) => [s, 0]));
|
|
599
|
-
for (let i = 0; i < n; i++) {
|
|
600
|
-
const s = order[i % order.length];
|
|
601
|
-
counts.set(s, counts.get(s) + 1);
|
|
602
|
-
}
|
|
711
|
+
const counts = tableSeatCountsBySide(t2);
|
|
712
|
+
const order = ["top", "bottom", "left", "right"];
|
|
603
713
|
let idx = 0;
|
|
604
714
|
for (const side of order) {
|
|
605
|
-
const count = counts
|
|
715
|
+
const count = counts[side];
|
|
606
716
|
for (let j = 0; j < count; j++) {
|
|
607
717
|
let localX;
|
|
608
718
|
let localY;
|
|
@@ -619,17 +729,39 @@ function expandTable(t2) {
|
|
|
619
729
|
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
620
730
|
localY = -h / 2 + (j + 0.5) * h / count;
|
|
621
731
|
}
|
|
622
|
-
const
|
|
623
|
-
seats.push(mk(idx++,
|
|
732
|
+
const point = place(localX, localY, t2.rotation, t2.center);
|
|
733
|
+
seats.push(mk(idx++, point.x, point.y, side));
|
|
624
734
|
}
|
|
625
735
|
}
|
|
626
736
|
return seats;
|
|
627
737
|
}
|
|
738
|
+
function tableInventoryCount(t2) {
|
|
739
|
+
if (t2.bookAsWhole || t2.variableOccupancy) return Math.max(0, Math.round(t2.seatCount));
|
|
740
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).length;
|
|
741
|
+
}
|
|
742
|
+
function expandTable(t2) {
|
|
743
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).map((slot) => ({
|
|
744
|
+
id: `${t2.id}:${slot.index}`,
|
|
745
|
+
label: slot.label,
|
|
746
|
+
...slot.displayLabel !== slot.label ? { displayLabel: slot.displayLabel } : {},
|
|
747
|
+
x: slot.x,
|
|
748
|
+
y: slot.y,
|
|
749
|
+
rowId: t2.id,
|
|
750
|
+
categoryKey: slot.categoryKey,
|
|
751
|
+
...slot.accessible ? { accessible: true } : {},
|
|
752
|
+
...slot.accessibility.length ? { accessibility: slot.accessibility } : {},
|
|
753
|
+
...slot.wheelchairSpaceType ? { wheelchairSpaceType: slot.wheelchairSpaceType } : {},
|
|
754
|
+
...slot.commercial ? { commercial: slot.commercial } : {},
|
|
755
|
+
...slot.viewUrl ? { viewUrl: slot.viewUrl } : {},
|
|
756
|
+
...slot.labelStyle ? { labelStyle: slot.labelStyle } : {}
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
628
759
|
function expandBooth(b) {
|
|
629
760
|
return [
|
|
630
761
|
{
|
|
631
762
|
id: `${b.id}:0`,
|
|
632
763
|
label: b.label,
|
|
764
|
+
...b.displayLabel && b.displayLabel !== b.label ? { displayLabel: b.displayLabel } : {},
|
|
633
765
|
x: b.center.x,
|
|
634
766
|
y: b.center.y,
|
|
635
767
|
rowId: b.id,
|
|
@@ -667,7 +799,7 @@ function polygonLabelPoint(outer, holes) {
|
|
|
667
799
|
if (!outer.length) return { x: 0, y: 0 };
|
|
668
800
|
const xs = outer.map((point) => point.x);
|
|
669
801
|
const ys = outer.map((point) => point.y);
|
|
670
|
-
const
|
|
802
|
+
const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
671
803
|
const centroid2 = polygonCentroid(outer);
|
|
672
804
|
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
673
805
|
let best = outer[0];
|
|
@@ -676,8 +808,8 @@ function polygonLabelPoint(outer, holes) {
|
|
|
676
808
|
for (let row = 1; row < 24; row += 1) {
|
|
677
809
|
for (let column = 1; column < 24; column += 1) {
|
|
678
810
|
const point = {
|
|
679
|
-
x:
|
|
680
|
-
y:
|
|
811
|
+
x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
|
|
812
|
+
y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
|
|
681
813
|
};
|
|
682
814
|
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
683
815
|
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
@@ -739,9 +871,32 @@ function objectCenter(o) {
|
|
|
739
871
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
740
872
|
}
|
|
741
873
|
}
|
|
874
|
+
function samePoints(left, right) {
|
|
875
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
876
|
+
}
|
|
877
|
+
function sameGASurfaceAsSection(object, section) {
|
|
878
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
879
|
+
const objectHoles = object.holes ?? [];
|
|
880
|
+
const sectionHoles = section.holes ?? [];
|
|
881
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
882
|
+
}
|
|
883
|
+
function owningSectionForObject(objects, object) {
|
|
884
|
+
const sections = objects.filter((candidate) => candidate.type === "section");
|
|
885
|
+
const referencedLogicalId = "referenceInventorySource" in object ? object.referenceInventorySource?.logicalSectionId : void 0;
|
|
886
|
+
const center = objectCenter(object);
|
|
887
|
+
const referencedOwner = referencedLogicalId ? sections.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(object, section) || pointInPolygonWithHoles(center, section.outline, section.holes))) : void 0;
|
|
888
|
+
return referencedOwner ?? sections.find((section) => pointInPolygonWithHoles(center, section.outline, section.holes));
|
|
889
|
+
}
|
|
742
890
|
function floorsOf(doc) {
|
|
743
891
|
if (doc.floors && doc.floors.length) return doc.floors;
|
|
744
|
-
return [{
|
|
892
|
+
return [{
|
|
893
|
+
id: "floor-0",
|
|
894
|
+
name: "Main",
|
|
895
|
+
objects: doc.objects,
|
|
896
|
+
focalPoint: doc.focalPoint,
|
|
897
|
+
referenceImage: doc.referenceImage,
|
|
898
|
+
backgroundImage: doc.backgroundImage
|
|
899
|
+
}];
|
|
745
900
|
}
|
|
746
901
|
function floorObjects(doc, floorId) {
|
|
747
902
|
const floors = floorsOf(doc);
|
|
@@ -790,15 +945,138 @@ function stackFloors(doc, spread = 900) {
|
|
|
790
945
|
});
|
|
791
946
|
return { ...doc, objects, floors: void 0 };
|
|
792
947
|
}
|
|
793
|
-
function
|
|
948
|
+
function expandFloorObjects(objects, zones, fallbackFocal) {
|
|
794
949
|
const out = [];
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
950
|
+
const segmented = /* @__PURE__ */ new Map();
|
|
951
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const object of objects) {
|
|
953
|
+
if (object.type !== "row" || !object.segmentedRow) continue;
|
|
954
|
+
const list = grouped.get(object.segmentedRow.groupId) ?? [];
|
|
955
|
+
list.push(object);
|
|
956
|
+
grouped.set(object.segmentedRow.groupId, list);
|
|
957
|
+
}
|
|
958
|
+
for (const [groupId, members] of grouped) {
|
|
959
|
+
const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
|
|
960
|
+
const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
|
|
961
|
+
const first = ordered[0]?.segmentedRow;
|
|
962
|
+
if (!first) continue;
|
|
963
|
+
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);
|
|
964
|
+
if (!valid) continue;
|
|
965
|
+
const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
|
|
966
|
+
let adjacencyOffset = 0;
|
|
967
|
+
let displayOffset = 0;
|
|
968
|
+
for (const row of ordered) {
|
|
969
|
+
if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
|
|
970
|
+
segmented.set(row.id, {
|
|
971
|
+
groupId,
|
|
972
|
+
adjacencyOffset,
|
|
973
|
+
displayOffset,
|
|
974
|
+
displayLabel: first.displayLabel,
|
|
975
|
+
totalSeats,
|
|
976
|
+
canonical: ordered[0],
|
|
977
|
+
viewFromSeatUrl: first.viewFromSeatUrl
|
|
978
|
+
});
|
|
979
|
+
adjacencyOffset += row.seatCount;
|
|
980
|
+
displayOffset += row.seatCount;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
for (const obj of objects) {
|
|
984
|
+
let seats = [];
|
|
985
|
+
if (obj.type === "row") seats = expandRow(obj);
|
|
986
|
+
else if (obj.type === "table") seats = expandTable(obj);
|
|
987
|
+
else if (obj.type === "booth") seats = expandBooth(obj);
|
|
988
|
+
if (!seats.length) continue;
|
|
989
|
+
if (obj.type === "row") {
|
|
990
|
+
const logical = segmented.get(obj.id);
|
|
991
|
+
if (logical) {
|
|
992
|
+
const overrides2 = new Map((obj.overrides ?? []).map((override) => [override.index, override]));
|
|
993
|
+
for (const seat of seats) {
|
|
994
|
+
const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
|
|
995
|
+
if (!Number.isInteger(physicalIndex)) continue;
|
|
996
|
+
const displayOrdinal = logical.displayOffset + physicalIndex;
|
|
997
|
+
seat.logicalRowId = logical.groupId;
|
|
998
|
+
seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
|
|
999
|
+
if (!overrides2.get(physicalIndex)?.displayLabel) {
|
|
1000
|
+
const numberingRow = {
|
|
1001
|
+
...logical.canonical,
|
|
1002
|
+
seatCount: logical.totalSeats,
|
|
1003
|
+
label: logical.displayLabel,
|
|
1004
|
+
displayLabel: logical.displayLabel
|
|
1005
|
+
};
|
|
1006
|
+
seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
|
|
1007
|
+
}
|
|
1008
|
+
seat.viewUrl ??= logical.viewFromSeatUrl;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const owner = owningSectionForObject(objects, obj);
|
|
1013
|
+
const inheritedView = owner?.viewFromSeatUrl;
|
|
1014
|
+
const zone = owner?.zone ? zones?.find((candidate) => candidate.id === owner.zone) : void 0;
|
|
1015
|
+
const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
|
|
1016
|
+
for (const seat of seats) {
|
|
1017
|
+
if (inheritedView) seat.viewUrl ??= inheritedView;
|
|
1018
|
+
if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
|
|
1019
|
+
if (owner?.zone) seat.zoneId = owner.zone;
|
|
1020
|
+
if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
|
|
1021
|
+
}
|
|
1022
|
+
out.push(...seats);
|
|
799
1023
|
}
|
|
800
1024
|
return out;
|
|
801
1025
|
}
|
|
1026
|
+
function expandChart(doc, options = {}) {
|
|
1027
|
+
if (doc.floors?.length) {
|
|
1028
|
+
const out2 = [];
|
|
1029
|
+
for (const floor of doc.floors) {
|
|
1030
|
+
const floorFocal = floor.focalPoint ?? doc.focalPoint;
|
|
1031
|
+
const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
|
|
1032
|
+
assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
|
|
1033
|
+
out2.push(...seats);
|
|
1034
|
+
}
|
|
1035
|
+
return out2;
|
|
1036
|
+
}
|
|
1037
|
+
const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
|
|
1038
|
+
assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
|
|
1042
|
+
const sections = objects.filter((o) => o.type === "section");
|
|
1043
|
+
const hasGeometry = floorBaseHeightM > 0 || sections.some((s) => s.height !== void 0 || s.rake !== void 0 || (s.elevation ?? 0) > 0);
|
|
1044
|
+
if (!sections.length || !hasGeometry || !focal) {
|
|
1045
|
+
for (const seat of seats) seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const owner = new Array(seats.length);
|
|
1049
|
+
const geo = /* @__PURE__ */ new Map();
|
|
1050
|
+
const frontDistU = /* @__PURE__ */ new Map();
|
|
1051
|
+
for (let i = 0; i < seats.length; i++) {
|
|
1052
|
+
const seat = seats[i];
|
|
1053
|
+
const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
|
|
1054
|
+
owner[i] = sec;
|
|
1055
|
+
if (!sec) continue;
|
|
1056
|
+
if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
|
|
1057
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
1058
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
1059
|
+
const cur = frontDistU.get(sec.id);
|
|
1060
|
+
if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
|
|
1061
|
+
}
|
|
1062
|
+
for (let i = 0; i < seats.length; i++) {
|
|
1063
|
+
const seat = seats[i];
|
|
1064
|
+
const sec = owner[i];
|
|
1065
|
+
if (!sec) {
|
|
1066
|
+
seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const g = geo.get(sec.id);
|
|
1070
|
+
let riseM = 0;
|
|
1071
|
+
if (g.rake > 0) {
|
|
1072
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
1073
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
1074
|
+
const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
|
|
1075
|
+
riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
|
|
1076
|
+
}
|
|
1077
|
+
seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
802
1080
|
var PAD = 40;
|
|
803
1081
|
function chartBounds(doc) {
|
|
804
1082
|
let minX = Infinity;
|
|
@@ -842,8 +1120,9 @@ function chartBounds(doc) {
|
|
|
842
1120
|
acc(obj.position.x + w, obj.position.y + obj.fontSize);
|
|
843
1121
|
}
|
|
844
1122
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1123
|
+
for (const image of [doc.referenceImage, doc.backgroundImage]) {
|
|
1124
|
+
if (!image) continue;
|
|
1125
|
+
const { center, width } = image;
|
|
847
1126
|
const bh = width * 3 / 4;
|
|
848
1127
|
acc(center.x - width / 2, center.y - bh / 2);
|
|
849
1128
|
acc(center.x + width / 2, center.y + bh / 2);
|
|
@@ -867,9 +1146,55 @@ var MAX_EVENT_INVENTORY = 1e5;
|
|
|
867
1146
|
function gaUnitLabel(areaId, index) {
|
|
868
1147
|
return `${PREFIX}${encodeURIComponent(areaId)}__${index + 1}`;
|
|
869
1148
|
}
|
|
870
|
-
function
|
|
1149
|
+
function validGAInventorySegments(area) {
|
|
1150
|
+
const segments = area.inventorySegments;
|
|
1151
|
+
if (segments === void 0) return true;
|
|
1152
|
+
if (!Array.isArray(segments) || segments.length < 1 || segments.length > 256) return false;
|
|
1153
|
+
let total = 0;
|
|
1154
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const segment of segments) {
|
|
1156
|
+
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;
|
|
1157
|
+
total += segment.count;
|
|
1158
|
+
if (total > MAX_GA_CAPACITY) return false;
|
|
1159
|
+
const sourceRanges = ranges.get(segment.sourceAreaId) ?? [];
|
|
1160
|
+
const end = segment.startIndex + segment.count;
|
|
1161
|
+
if (sourceRanges.some((range) => segment.startIndex < range.end && end > range.start)) return false;
|
|
1162
|
+
sourceRanges.push({ start: segment.startIndex, end });
|
|
1163
|
+
ranges.set(segment.sourceAreaId, sourceRanges);
|
|
1164
|
+
}
|
|
1165
|
+
return total === area.capacity;
|
|
1166
|
+
}
|
|
1167
|
+
function gaInventorySegments(area) {
|
|
871
1168
|
const capacity = Math.min(MAX_GA_CAPACITY, Math.max(0, Math.floor(area.capacity)));
|
|
872
|
-
|
|
1169
|
+
if (area.inventorySegments === void 0) {
|
|
1170
|
+
return capacity ? [{ sourceAreaId: area.id, startIndex: 0, count: capacity }] : [];
|
|
1171
|
+
}
|
|
1172
|
+
if (!validGAInventorySegments(area)) return [];
|
|
1173
|
+
return area.inventorySegments.map((segment) => ({ ...segment }));
|
|
1174
|
+
}
|
|
1175
|
+
function gaUnitLabels(area) {
|
|
1176
|
+
return gaInventorySegments(area).flatMap((segment) => Array.from(
|
|
1177
|
+
{ length: segment.count },
|
|
1178
|
+
(_, offset) => gaUnitLabel(segment.sourceAreaId, segment.startIndex + offset)
|
|
1179
|
+
));
|
|
1180
|
+
}
|
|
1181
|
+
function growJoinedGAInventory(area, nextCapacity) {
|
|
1182
|
+
if (area.inventorySegments === void 0 || nextCapacity === area.capacity) return area.inventorySegments;
|
|
1183
|
+
if (nextCapacity < area.capacity) return void 0;
|
|
1184
|
+
const segments = gaInventorySegments(area);
|
|
1185
|
+
if (!segments.length) return void 0;
|
|
1186
|
+
const extra = nextCapacity - area.capacity;
|
|
1187
|
+
if (!extra) return segments;
|
|
1188
|
+
const ownRanges = segments.filter((segment) => segment.sourceAreaId === area.id);
|
|
1189
|
+
const startIndex = ownRanges.reduce((max, segment) => Math.max(max, segment.startIndex + segment.count), 0);
|
|
1190
|
+
const last = segments[segments.length - 1];
|
|
1191
|
+
if (last?.sourceAreaId === area.id && last.startIndex + last.count === startIndex) {
|
|
1192
|
+
last.count += extra;
|
|
1193
|
+
} else {
|
|
1194
|
+
if (segments.length >= 256) return void 0;
|
|
1195
|
+
segments.push({ sourceAreaId: area.id, startIndex, count: extra });
|
|
1196
|
+
}
|
|
1197
|
+
return segments;
|
|
873
1198
|
}
|
|
874
1199
|
function gaAreasOf(doc) {
|
|
875
1200
|
return allObjects(doc).filter((o) => o.type === "gaArea");
|
|
@@ -890,15 +1215,6 @@ function objectSeatLabels(o) {
|
|
|
890
1215
|
function isSeatObject(o) {
|
|
891
1216
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
892
1217
|
}
|
|
893
|
-
function samePoints(left, right) {
|
|
894
|
-
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
895
|
-
}
|
|
896
|
-
function sameGASurfaceAsSection(object, section) {
|
|
897
|
-
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
898
|
-
const objectHoles = object.holes ?? [];
|
|
899
|
-
const sectionHoles = section.holes ?? [];
|
|
900
|
-
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
901
|
-
}
|
|
902
1218
|
function computeSections(doc) {
|
|
903
1219
|
const objs = allObjects(doc);
|
|
904
1220
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
@@ -924,10 +1240,7 @@ function computeSections(doc) {
|
|
|
924
1240
|
if (!isSeatObject(obj)) continue;
|
|
925
1241
|
const labels = objectSeatLabels(obj);
|
|
926
1242
|
if (labels.length === 0) continue;
|
|
927
|
-
const
|
|
928
|
-
const c = objectCenter(obj);
|
|
929
|
-
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
930
|
-
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
1243
|
+
const owner = owningSectionForObject(objs, obj);
|
|
931
1244
|
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
932
1245
|
node.seatCount += labels.length;
|
|
933
1246
|
node.objectIds.push(obj.id);
|
|
@@ -983,20 +1296,6 @@ function applyHidden(doc, hidden) {
|
|
|
983
1296
|
return { ...doc, objects };
|
|
984
1297
|
}
|
|
985
1298
|
|
|
986
|
-
// src/engine/SeatmapRenderer.ts
|
|
987
|
-
var import_Core = require("konva/lib/Core");
|
|
988
|
-
var import_Stage = require("konva/lib/Stage");
|
|
989
|
-
var import_Layer = require("konva/lib/Layer");
|
|
990
|
-
var import_Group = require("konva/lib/Group");
|
|
991
|
-
var import_Circle = require("konva/lib/shapes/Circle");
|
|
992
|
-
var import_Rect = require("konva/lib/shapes/Rect");
|
|
993
|
-
var import_Ellipse = require("konva/lib/shapes/Ellipse");
|
|
994
|
-
var import_Line = require("konva/lib/shapes/Line");
|
|
995
|
-
var import_Text = require("konva/lib/shapes/Text");
|
|
996
|
-
var import_Path = require("konva/lib/shapes/Path");
|
|
997
|
-
var import_Image = require("konva/lib/shapes/Image");
|
|
998
|
-
var import_Shape = require("konva/lib/Shape");
|
|
999
|
-
|
|
1000
1299
|
// src/core/chartRenderRules.ts
|
|
1001
1300
|
var SEAT_LABEL_FONT_SIZE = 7;
|
|
1002
1301
|
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
@@ -1053,6 +1352,769 @@ function stateAwareBookableLabelInk(fill, preferred) {
|
|
|
1053
1352
|
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
1054
1353
|
}
|
|
1055
1354
|
|
|
1355
|
+
// src/core/renderedQuality.ts
|
|
1356
|
+
var RENDERED_QUALITY_REPORT_VERSION = 3;
|
|
1357
|
+
var MIN_TEXT_CONTRAST = 4.5;
|
|
1358
|
+
var MIN_GRAPHICAL_CONTRAST = 3;
|
|
1359
|
+
var MIN_POINTER_TARGET_PX = 24;
|
|
1360
|
+
var MAX_SAMPLES_PER_FINDING = 20;
|
|
1361
|
+
function visibleWithBox(items) {
|
|
1362
|
+
return items.filter((item) => item.visible && Boolean(item.screenBox));
|
|
1363
|
+
}
|
|
1364
|
+
function intersects(left, right) {
|
|
1365
|
+
return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
|
|
1366
|
+
}
|
|
1367
|
+
function minimum(values) {
|
|
1368
|
+
return values.length ? Math.round(Math.min(...values) * 100) / 100 : null;
|
|
1369
|
+
}
|
|
1370
|
+
function finding(code, message, samples) {
|
|
1371
|
+
if (!samples.length) return null;
|
|
1372
|
+
return {
|
|
1373
|
+
code,
|
|
1374
|
+
message,
|
|
1375
|
+
count: samples.length,
|
|
1376
|
+
samples: samples.slice(0, MAX_SAMPLES_PER_FINDING)
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function labelSample(label, measured, minimumValue) {
|
|
1380
|
+
return {
|
|
1381
|
+
primaryId: label.seatId,
|
|
1382
|
+
primaryLabel: label.label,
|
|
1383
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1384
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function hierarchySample(label, measured, minimumValue) {
|
|
1388
|
+
return {
|
|
1389
|
+
primaryId: label.id,
|
|
1390
|
+
primaryLabel: label.label,
|
|
1391
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1392
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function freeTextSample(label, measured, minimumValue) {
|
|
1396
|
+
return {
|
|
1397
|
+
primaryId: label.objectId,
|
|
1398
|
+
primaryLabel: label.text,
|
|
1399
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1400
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function collisionSample(primaryId, primaryLabel, secondaryId, secondaryLabel) {
|
|
1404
|
+
return { primaryId, primaryLabel, secondaryId, secondaryLabel };
|
|
1405
|
+
}
|
|
1406
|
+
function inspectRenderedQualityEvidence(evidence, state = "overview", targetCategoryKey = null) {
|
|
1407
|
+
const bookable = visibleWithBox(evidence.labels);
|
|
1408
|
+
const hierarchy = visibleWithBox(evidence.hierarchyLabels);
|
|
1409
|
+
const freeText = visibleWithBox(evidence.freeTextLabels);
|
|
1410
|
+
const visibleGA = evidence.gaAreas.filter((area) => area.visible);
|
|
1411
|
+
const bookableContrast = bookable.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
|
|
1412
|
+
const hierarchyContrast = hierarchy.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
|
|
1413
|
+
const freeTextContrast = freeText.map((label) => renderedTextContrast(label.ink, label.background) ?? 0);
|
|
1414
|
+
const gaContrast = visibleGA.map((area) => renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0);
|
|
1415
|
+
const targetLabels = targetCategoryKey ? evidence.labels.filter((label) => label.categoryKey === targetCategoryKey) : evidence.labels;
|
|
1416
|
+
const targetGAAreas = targetCategoryKey ? evidence.gaAreas.filter((area) => area.categoryKey === targetCategoryKey) : evidence.gaAreas;
|
|
1417
|
+
const selected = targetLabels.filter((label) => label.selected);
|
|
1418
|
+
const held = targetLabels.filter((label) => label.status === "held");
|
|
1419
|
+
const booked = targetLabels.filter((label) => label.status === "booked");
|
|
1420
|
+
const activePointerTargets = targetLabels.filter((label) => label.pointerTarget.active);
|
|
1421
|
+
const activeCategoryKeys = /* @__PURE__ */ new Set([
|
|
1422
|
+
...evidence.labels.filter((label) => label.visible && (label.opacity > 0.25 || label.selected || label.status !== "free")).map((label) => label.categoryKey),
|
|
1423
|
+
...evidence.gaAreas.filter((area) => area.visible && area.opacity > 0.1).map((area) => area.categoryKey)
|
|
1424
|
+
]);
|
|
1425
|
+
const selectedRingContrast = selected.length ? minimum(selected.flatMap((label) => [
|
|
1426
|
+
renderedTextContrast(evidence.selectionRingColor, evidence.canvasBackground) ?? 0,
|
|
1427
|
+
renderedTextContrast(evidence.selectionRingColor, label.fill) ?? 0
|
|
1428
|
+
])) : null;
|
|
1429
|
+
const findings = [];
|
|
1430
|
+
const overviewCountSamples = (count, label) => Array.from({ length: count }, (_, index) => ({
|
|
1431
|
+
primaryId: `overview:${index + 1}`,
|
|
1432
|
+
primaryLabel: label
|
|
1433
|
+
}));
|
|
1434
|
+
if (state === "overview" && evidence.rung === "sections") {
|
|
1435
|
+
findings.push(finding(
|
|
1436
|
+
"overview-section-category-paint",
|
|
1437
|
+
"Section overview shells must use the neutral hierarchy palette, not category paint",
|
|
1438
|
+
overviewCountSamples(evidence.overviewStyle.categoryPaintedSectionShells, "category-painted section shell")
|
|
1439
|
+
));
|
|
1440
|
+
findings.push(finding(
|
|
1441
|
+
"overview-category-detail-visible",
|
|
1442
|
+
"Category-tinted section detail must wait until section focus or seat zoom",
|
|
1443
|
+
overviewCountSamples(evidence.overviewStyle.visibleCategoryDetailOutlines, "category detail outline")
|
|
1444
|
+
));
|
|
1445
|
+
findings.push(finding(
|
|
1446
|
+
"overview-row-hints-visible",
|
|
1447
|
+
"Row and seat patterns must not clutter the section overview",
|
|
1448
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionRowHints, "section row hint")
|
|
1449
|
+
));
|
|
1450
|
+
findings.push(finding(
|
|
1451
|
+
"overview-availability-clutter",
|
|
1452
|
+
"Live availability counts belong in focused detail, not on section overview shells",
|
|
1453
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionAvailabilityLabels, "section availability label")
|
|
1454
|
+
));
|
|
1455
|
+
findings.push(finding(
|
|
1456
|
+
"overview-ga-detail-visible",
|
|
1457
|
+
"Section-contained standing paint belongs in focused detail, not on section overview shells",
|
|
1458
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionGADetails, "section-contained GA detail")
|
|
1459
|
+
));
|
|
1460
|
+
}
|
|
1461
|
+
findings.push(finding(
|
|
1462
|
+
"bookable-label-undersized",
|
|
1463
|
+
"Visible bookable labels must meet the rendered small-text size floor",
|
|
1464
|
+
bookable.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => labelSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
|
|
1465
|
+
));
|
|
1466
|
+
if (state === "interaction") {
|
|
1467
|
+
const labelledInventory = targetLabels.length > 0;
|
|
1468
|
+
const gaInventory = targetGAAreas.length > 0;
|
|
1469
|
+
const detailInventory = labelledInventory || gaInventory;
|
|
1470
|
+
const visibleTargetGA = targetGAAreas.filter((area) => area.visible);
|
|
1471
|
+
const sections = /* @__PURE__ */ new Set([
|
|
1472
|
+
...targetLabels.flatMap((label) => label.sectionId ? [label.sectionId] : []),
|
|
1473
|
+
...targetGAAreas.flatMap((area) => area.sectionId ? [area.sectionId] : [])
|
|
1474
|
+
]);
|
|
1475
|
+
const categories = /* @__PURE__ */ new Set([
|
|
1476
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1477
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1478
|
+
]);
|
|
1479
|
+
const inViewport = (label) => label.screenCenter.x >= 0 && label.screenCenter.x <= evidence.viewport.width && label.screenCenter.y >= 0 && label.screenCenter.y <= evidence.viewport.height;
|
|
1480
|
+
findings.push(finding(
|
|
1481
|
+
"detail-rung-missing",
|
|
1482
|
+
"Interaction evidence with bookable inventory must render the seat-detail rung",
|
|
1483
|
+
detailInventory && evidence.rung !== "seats" ? [{ primaryId: "renderer", primaryLabel: evidence.rung }] : []
|
|
1484
|
+
));
|
|
1485
|
+
findings.push(finding(
|
|
1486
|
+
"detail-inventory-not-visible",
|
|
1487
|
+
"Interaction evidence must frame at least one real bookable unit",
|
|
1488
|
+
detailInventory && !targetLabels.some(inViewport) && !visibleTargetGA.length ? [{ primaryId: "renderer", primaryLabel: "no target inventory in viewport" }] : []
|
|
1489
|
+
));
|
|
1490
|
+
findings.push(finding(
|
|
1491
|
+
"pointer-target-inactive",
|
|
1492
|
+
"Interaction evidence must expose a live production pointer target",
|
|
1493
|
+
labelledInventory && !activePointerTargets.some(inViewport) || !labelledInventory && gaInventory && !visibleTargetGA.some((area) => area.interactive) ? [{ primaryId: "renderer", primaryLabel: "no active pointer target in viewport" }] : []
|
|
1494
|
+
));
|
|
1495
|
+
findings.push(finding(
|
|
1496
|
+
"pointer-target-undersized",
|
|
1497
|
+
"Every active production pointer target must reach at least 24 CSS pixels",
|
|
1498
|
+
activePointerTargets.filter((label) => inViewport(label) && label.pointerTarget.effectiveMinimumPx < MIN_POINTER_TARGET_PX).map((label) => labelSample(label, label.pointerTarget.effectiveMinimumPx, MIN_POINTER_TARGET_PX))
|
|
1499
|
+
));
|
|
1500
|
+
findings.push(finding(
|
|
1501
|
+
"selected-state-missing",
|
|
1502
|
+
"Interaction evidence must paint a selected unit and its renderer-owned ring",
|
|
1503
|
+
labelledInventory && (!selected.length || !selected.some((label) => evidence.selectionRingSeatIds.includes(label.seatId))) ? [{ primaryId: "renderer", primaryLabel: "selected state" }] : []
|
|
1504
|
+
));
|
|
1505
|
+
findings.push(finding(
|
|
1506
|
+
"selected-state-contrast-low",
|
|
1507
|
+
"The selected-state ring must maintain 3:1 graphical contrast with the canvas",
|
|
1508
|
+
selected.length && (selectedRingContrast ?? 0) < MIN_GRAPHICAL_CONTRAST ? [{
|
|
1509
|
+
primaryId: selected[0].seatId,
|
|
1510
|
+
primaryLabel: selected[0].label,
|
|
1511
|
+
measured: Math.round((selectedRingContrast ?? 0) * 100) / 100,
|
|
1512
|
+
minimum: MIN_GRAPHICAL_CONTRAST
|
|
1513
|
+
}] : []
|
|
1514
|
+
));
|
|
1515
|
+
findings.push(finding(
|
|
1516
|
+
"held-state-missing",
|
|
1517
|
+
"Interaction evidence must paint a held state when the floor has at least two status-managed units",
|
|
1518
|
+
targetLabels.length >= 2 && !held.length ? [{ primaryId: "renderer", primaryLabel: "held state" }] : []
|
|
1519
|
+
));
|
|
1520
|
+
findings.push(finding(
|
|
1521
|
+
"booked-state-missing",
|
|
1522
|
+
"Interaction evidence must paint a taken state when the floor has at least three status-managed units",
|
|
1523
|
+
targetLabels.length >= 3 && !booked.length ? [{ primaryId: "renderer", primaryLabel: "booked state" }] : []
|
|
1524
|
+
));
|
|
1525
|
+
const heldSignatures = new Set(held.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
|
|
1526
|
+
const bookedSignatures = new Set(booked.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
|
|
1527
|
+
findings.push(finding(
|
|
1528
|
+
"status-state-indistinct",
|
|
1529
|
+
"Held and taken evidence must resolve to distinct renderer paint",
|
|
1530
|
+
held.length && booked.length && [...heldSignatures].some((signature) => bookedSignatures.has(signature)) ? [{ primaryId: held[0].seatId, primaryLabel: held[0].label, secondaryId: booked[0].seatId, secondaryLabel: booked[0].label }] : []
|
|
1531
|
+
));
|
|
1532
|
+
findings.push(finding(
|
|
1533
|
+
"section-focus-missing",
|
|
1534
|
+
"Interaction evidence must exercise section focus and its backdrop when section membership exists",
|
|
1535
|
+
sections.size && (!evidence.focusedSectionId || !evidence.focusBackdropVisible) ? [{ primaryId: "renderer", primaryLabel: "section focus" }] : []
|
|
1536
|
+
));
|
|
1537
|
+
findings.push(finding(
|
|
1538
|
+
"category-filter-missing",
|
|
1539
|
+
"Interaction evidence must exercise a category filter when multiple categories exist",
|
|
1540
|
+
categories.size >= 2 && (!evidence.categoryFilterKeys || !evidence.categoryFilterKeys.length) ? [{ primaryId: "renderer", primaryLabel: "category filter" }] : []
|
|
1541
|
+
));
|
|
1542
|
+
const excludedFree = evidence.labels.filter((label) => label.status === "free" && !label.selected && evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(label.categoryKey));
|
|
1543
|
+
const excludedGA = evidence.gaAreas.filter((area) => evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(area.categoryKey));
|
|
1544
|
+
findings.push(finding(
|
|
1545
|
+
"category-filter-ineffective",
|
|
1546
|
+
"The active category filter must visibly dim excluded free inventory",
|
|
1547
|
+
(excludedFree.length || excludedGA.length) && !excludedFree.some((label) => label.opacity <= 0.25) && !excludedGA.some((area) => area.opacity <= 0.1) ? [
|
|
1548
|
+
...excludedFree.map((label) => labelSample(label, label.opacity)),
|
|
1549
|
+
...excludedGA.map((area) => ({ primaryId: area.areaId, primaryLabel: area.label, measured: area.opacity }))
|
|
1550
|
+
] : []
|
|
1551
|
+
));
|
|
1552
|
+
findings.push(finding(
|
|
1553
|
+
"target-category-not-visible",
|
|
1554
|
+
"A category-specific interaction scene must visibly paint its exact target category",
|
|
1555
|
+
targetCategoryKey && !activeCategoryKeys.has(targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
|
|
1556
|
+
));
|
|
1557
|
+
findings.push(finding(
|
|
1558
|
+
"target-category-filter-mismatch",
|
|
1559
|
+
"A category-specific interaction scene must bind its filter to only the exact target category",
|
|
1560
|
+
targetCategoryKey && (evidence.categoryFilterKeys?.length !== 1 || evidence.categoryFilterKeys[0] !== targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
|
|
1561
|
+
));
|
|
1562
|
+
}
|
|
1563
|
+
findings.push(finding(
|
|
1564
|
+
"bookable-label-contrast-low",
|
|
1565
|
+
"Visible bookable labels must meet 4.5:1 contrast against their actual paint",
|
|
1566
|
+
bookable.flatMap((label) => {
|
|
1567
|
+
const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
|
|
1568
|
+
return ratio < MIN_TEXT_CONTRAST ? [labelSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1569
|
+
})
|
|
1570
|
+
));
|
|
1571
|
+
const bookableCollisions = [];
|
|
1572
|
+
for (let index = 0; index < bookable.length; index += 1) {
|
|
1573
|
+
for (let other = 0; other < index; other += 1) {
|
|
1574
|
+
if (intersects(bookable[index].screenBox, bookable[other].screenBox)) {
|
|
1575
|
+
bookableCollisions.push(collisionSample(
|
|
1576
|
+
bookable[other].seatId,
|
|
1577
|
+
bookable[other].label,
|
|
1578
|
+
bookable[index].seatId,
|
|
1579
|
+
bookable[index].label
|
|
1580
|
+
));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
findings.push(finding(
|
|
1585
|
+
"bookable-label-collision",
|
|
1586
|
+
"Visible bookable labels must not overlap",
|
|
1587
|
+
bookableCollisions
|
|
1588
|
+
));
|
|
1589
|
+
findings.push(finding(
|
|
1590
|
+
"hierarchy-label-undersized",
|
|
1591
|
+
"Visible section and zone labels must meet the rendered small-text size floor",
|
|
1592
|
+
hierarchy.filter((label) => label.renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX).map((label) => hierarchySample(label, label.renderedFontPx, MIN_VISIBLE_BOOKABLE_LABEL_PX))
|
|
1593
|
+
));
|
|
1594
|
+
findings.push(finding(
|
|
1595
|
+
"hierarchy-label-contrast-low",
|
|
1596
|
+
"Visible section and zone labels must meet 4.5:1 contrast against their backing paint",
|
|
1597
|
+
hierarchy.flatMap((label) => {
|
|
1598
|
+
const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
|
|
1599
|
+
return ratio < MIN_TEXT_CONTRAST ? [hierarchySample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1600
|
+
})
|
|
1601
|
+
));
|
|
1602
|
+
findings.push(finding(
|
|
1603
|
+
"hierarchy-label-outside-section",
|
|
1604
|
+
"Visible section labels must remain inside the filled section surface and outside holes",
|
|
1605
|
+
hierarchy.filter((label) => label.kind === "section" && label.fitsContainer === false).map((label) => hierarchySample(label))
|
|
1606
|
+
));
|
|
1607
|
+
const hierarchyCollisions = [];
|
|
1608
|
+
for (let index = 0; index < hierarchy.length; index += 1) {
|
|
1609
|
+
for (let other = 0; other < index; other += 1) {
|
|
1610
|
+
if (intersects(hierarchy[index].screenBox, hierarchy[other].screenBox)) {
|
|
1611
|
+
hierarchyCollisions.push(collisionSample(
|
|
1612
|
+
hierarchy[other].id,
|
|
1613
|
+
hierarchy[other].label,
|
|
1614
|
+
hierarchy[index].id,
|
|
1615
|
+
hierarchy[index].label
|
|
1616
|
+
));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
findings.push(finding(
|
|
1621
|
+
"hierarchy-label-collision",
|
|
1622
|
+
"Visible hierarchy labels must not overlap each other",
|
|
1623
|
+
hierarchyCollisions
|
|
1624
|
+
));
|
|
1625
|
+
const hierarchyBookableCollisions = [];
|
|
1626
|
+
for (const upper of hierarchy) {
|
|
1627
|
+
for (const unit of bookable) {
|
|
1628
|
+
if (intersects(upper.screenBox, unit.screenBox)) {
|
|
1629
|
+
hierarchyBookableCollisions.push(collisionSample(upper.id, upper.label, unit.seatId, unit.label));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
findings.push(finding(
|
|
1634
|
+
"hierarchy-bookable-collision",
|
|
1635
|
+
"Visible hierarchy labels must not overlap bookable labels",
|
|
1636
|
+
hierarchyBookableCollisions
|
|
1637
|
+
));
|
|
1638
|
+
findings.push(finding(
|
|
1639
|
+
"free-text-undersized",
|
|
1640
|
+
"Visible chart text must meet the rendered small-text size floor",
|
|
1641
|
+
freeText.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => freeTextSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
|
|
1642
|
+
));
|
|
1643
|
+
findings.push(finding(
|
|
1644
|
+
"free-text-contrast-low",
|
|
1645
|
+
"Visible chart text must meet 4.5:1 contrast against its measured background",
|
|
1646
|
+
freeText.flatMap((label) => {
|
|
1647
|
+
const ratio = renderedTextContrast(label.ink, label.background) ?? 0;
|
|
1648
|
+
return ratio < MIN_TEXT_CONTRAST ? [freeTextSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1649
|
+
})
|
|
1650
|
+
));
|
|
1651
|
+
const freeTextCollisions = [];
|
|
1652
|
+
for (let index = 0; index < freeText.length; index += 1) {
|
|
1653
|
+
for (let other = 0; other < index; other += 1) {
|
|
1654
|
+
if (intersects(freeText[index].screenBox, freeText[other].screenBox)) {
|
|
1655
|
+
freeTextCollisions.push(collisionSample(
|
|
1656
|
+
freeText[other].objectId,
|
|
1657
|
+
freeText[other].text,
|
|
1658
|
+
freeText[index].objectId,
|
|
1659
|
+
freeText[index].text
|
|
1660
|
+
));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
findings.push(finding(
|
|
1665
|
+
"free-text-collision",
|
|
1666
|
+
"Visible chart text labels must not overlap each other",
|
|
1667
|
+
freeTextCollisions
|
|
1668
|
+
));
|
|
1669
|
+
const freeTextBookableCollisions = [];
|
|
1670
|
+
for (const text of freeText) {
|
|
1671
|
+
for (const unit of bookable) {
|
|
1672
|
+
if (intersects(text.screenBox, unit.screenBox)) {
|
|
1673
|
+
freeTextBookableCollisions.push(collisionSample(text.objectId, text.text, unit.seatId, unit.label));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
findings.push(finding(
|
|
1678
|
+
"free-text-bookable-collision",
|
|
1679
|
+
"Visible chart text must not overlap bookable labels",
|
|
1680
|
+
freeTextBookableCollisions
|
|
1681
|
+
));
|
|
1682
|
+
const freeTextHierarchyCollisions = [];
|
|
1683
|
+
for (const text of freeText) {
|
|
1684
|
+
for (const upper of hierarchy) {
|
|
1685
|
+
if (intersects(text.screenBox, upper.screenBox)) {
|
|
1686
|
+
freeTextHierarchyCollisions.push(collisionSample(text.objectId, text.text, upper.id, upper.label));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
findings.push(finding(
|
|
1691
|
+
"free-text-hierarchy-collision",
|
|
1692
|
+
"Visible chart text must not overlap hierarchy labels",
|
|
1693
|
+
freeTextHierarchyCollisions
|
|
1694
|
+
));
|
|
1695
|
+
findings.push(finding(
|
|
1696
|
+
"ga-contrast-low",
|
|
1697
|
+
"Visible GA surfaces must maintain 3:1 graphical contrast with the canvas",
|
|
1698
|
+
visibleGA.flatMap((area) => {
|
|
1699
|
+
const ratio = renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0;
|
|
1700
|
+
return ratio < MIN_GRAPHICAL_CONTRAST ? [{
|
|
1701
|
+
primaryId: area.areaId,
|
|
1702
|
+
primaryLabel: area.label,
|
|
1703
|
+
measured: Math.round(ratio * 100) / 100,
|
|
1704
|
+
minimum: MIN_GRAPHICAL_CONTRAST
|
|
1705
|
+
}] : [];
|
|
1706
|
+
})
|
|
1707
|
+
));
|
|
1708
|
+
const materialFindings = findings.filter((item) => Boolean(item));
|
|
1709
|
+
return {
|
|
1710
|
+
version: RENDERED_QUALITY_REPORT_VERSION,
|
|
1711
|
+
passed: materialFindings.length === 0,
|
|
1712
|
+
state,
|
|
1713
|
+
targetCategoryKey,
|
|
1714
|
+
resolvedRules: [
|
|
1715
|
+
"rendered-overview-label-size",
|
|
1716
|
+
"rendered-overview-label-contrast",
|
|
1717
|
+
"rendered-overview-label-collision",
|
|
1718
|
+
"rendered-overview-hierarchy-containment",
|
|
1719
|
+
"rendered-overview-section-first-style",
|
|
1720
|
+
"rendered-overview-ga-contrast",
|
|
1721
|
+
...state === "interaction" ? [
|
|
1722
|
+
"rendered-detail-inventory",
|
|
1723
|
+
"rendered-pointer-target",
|
|
1724
|
+
"rendered-selected-held-taken-states",
|
|
1725
|
+
"rendered-section-focus",
|
|
1726
|
+
"rendered-category-filter"
|
|
1727
|
+
] : []
|
|
1728
|
+
],
|
|
1729
|
+
viewport: evidence.viewport,
|
|
1730
|
+
canvasBackground: evidence.canvasBackground,
|
|
1731
|
+
effectiveScale: evidence.effectiveScale,
|
|
1732
|
+
rung: evidence.rung,
|
|
1733
|
+
inventory: {
|
|
1734
|
+
totalBookableUnits: evidence.totalBookableUnits,
|
|
1735
|
+
totalLabelledBookableUnits: evidence.totalLabelledBookableUnits,
|
|
1736
|
+
visibleBookableLabels: bookable.length,
|
|
1737
|
+
hiddenBookableLabels: evidence.hiddenLabels,
|
|
1738
|
+
visibleHierarchyLabels: hierarchy.length,
|
|
1739
|
+
visibleFreeTextLabels: freeText.length,
|
|
1740
|
+
visibleGAAreas: visibleGA.length
|
|
1741
|
+
},
|
|
1742
|
+
overviewStyle: evidence.overviewStyle,
|
|
1743
|
+
composition: {
|
|
1744
|
+
hierarchy: evidence.hierarchyLabels.filter((label) => label.role === "name").map((label) => ({
|
|
1745
|
+
id: label.id,
|
|
1746
|
+
kind: label.kind,
|
|
1747
|
+
label: label.label,
|
|
1748
|
+
visible: label.visible
|
|
1749
|
+
})).sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)),
|
|
1750
|
+
labelledObjects: evidence.freeTextLabels.map((label) => ({
|
|
1751
|
+
objectId: label.objectId,
|
|
1752
|
+
kind: label.kind,
|
|
1753
|
+
text: label.text,
|
|
1754
|
+
visible: label.visible
|
|
1755
|
+
})).sort((left, right) => left.objectId.localeCompare(right.objectId) || left.kind.localeCompare(right.kind)),
|
|
1756
|
+
gaAreas: evidence.gaAreas.map((area) => ({
|
|
1757
|
+
areaId: area.areaId,
|
|
1758
|
+
label: area.label,
|
|
1759
|
+
categoryKey: area.categoryKey,
|
|
1760
|
+
...area.sectionId ? { sectionId: area.sectionId } : {},
|
|
1761
|
+
visible: area.visible
|
|
1762
|
+
})).sort((left, right) => left.areaId.localeCompare(right.areaId)),
|
|
1763
|
+
bookableSectionIds: [...new Set(evidence.labels.flatMap((label) => label.sectionId ? [label.sectionId] : []))].sort(),
|
|
1764
|
+
categoryKeys: [.../* @__PURE__ */ new Set([
|
|
1765
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1766
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1767
|
+
])].sort(),
|
|
1768
|
+
activeCategoryKeys: [...activeCategoryKeys].sort()
|
|
1769
|
+
},
|
|
1770
|
+
metrics: {
|
|
1771
|
+
minimumRenderedBookableLabelPx: minimum(bookable.map((label) => label.renderedFontPx)),
|
|
1772
|
+
minimumBookableLabelContrast: minimum(bookableContrast),
|
|
1773
|
+
minimumRenderedHierarchyLabelPx: minimum(hierarchy.map((label) => label.renderedFontPx)),
|
|
1774
|
+
minimumHierarchyLabelContrast: minimum(hierarchyContrast),
|
|
1775
|
+
minimumRenderedFreeTextPx: minimum(freeText.map((label) => label.renderedFontPx)),
|
|
1776
|
+
minimumFreeTextContrast: minimum(freeTextContrast),
|
|
1777
|
+
minimumGAContrast: minimum(gaContrast),
|
|
1778
|
+
minimumEffectivePointerTargetPx: minimum(activePointerTargets.map((label) => label.pointerTarget.effectiveMinimumPx)),
|
|
1779
|
+
selectedRingContrast: selectedRingContrast == null ? null : Math.round(selectedRingContrast * 100) / 100
|
|
1780
|
+
},
|
|
1781
|
+
interaction: {
|
|
1782
|
+
applicable: {
|
|
1783
|
+
detail: targetLabels.length > 0 || targetGAAreas.length > 0,
|
|
1784
|
+
pointer: targetLabels.length > 0 || targetGAAreas.length > 0,
|
|
1785
|
+
held: targetLabels.length >= 2,
|
|
1786
|
+
booked: targetLabels.length >= 3,
|
|
1787
|
+
sectionFocus: targetLabels.some((label) => Boolean(label.sectionId)) || targetGAAreas.some((area) => Boolean(area.sectionId)),
|
|
1788
|
+
categoryFilter: (/* @__PURE__ */ new Set([
|
|
1789
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1790
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1791
|
+
])).size >= 2
|
|
1792
|
+
},
|
|
1793
|
+
selectedUnits: selected.length,
|
|
1794
|
+
heldUnits: held.length,
|
|
1795
|
+
bookedUnits: booked.length,
|
|
1796
|
+
activePointerTargets: activePointerTargets.length,
|
|
1797
|
+
focusedSectionId: evidence.focusedSectionId,
|
|
1798
|
+
focusBackdropVisible: evidence.focusBackdropVisible,
|
|
1799
|
+
categoryFilterKeys: evidence.categoryFilterKeys
|
|
1800
|
+
},
|
|
1801
|
+
findings: materialFindings
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// src/engine/SeatmapRenderer.ts
|
|
1806
|
+
var import_Core = require("konva/lib/Core");
|
|
1807
|
+
var import_Stage = require("konva/lib/Stage");
|
|
1808
|
+
var import_Layer = require("konva/lib/Layer");
|
|
1809
|
+
var import_Group = require("konva/lib/Group");
|
|
1810
|
+
var import_Circle = require("konva/lib/shapes/Circle");
|
|
1811
|
+
var import_Rect = require("konva/lib/shapes/Rect");
|
|
1812
|
+
var import_Ellipse = require("konva/lib/shapes/Ellipse");
|
|
1813
|
+
var import_Line = require("konva/lib/shapes/Line");
|
|
1814
|
+
var import_Arrow = require("konva/lib/shapes/Arrow");
|
|
1815
|
+
var import_Text = require("konva/lib/shapes/Text");
|
|
1816
|
+
var import_Path = require("konva/lib/shapes/Path");
|
|
1817
|
+
var import_Image = require("konva/lib/shapes/Image");
|
|
1818
|
+
var import_Shape = require("konva/lib/Shape");
|
|
1819
|
+
|
|
1820
|
+
// src/core/chartBackgrounds.ts
|
|
1821
|
+
function buyerBackgroundImage(owner) {
|
|
1822
|
+
const background = owner.backgroundImage;
|
|
1823
|
+
return background?.url && !background.assetId ? background : void 0;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/core/spatialIndex.ts
|
|
1827
|
+
var DEFAULT_SEAT_RADIUS = 9;
|
|
1828
|
+
function halfExtents(shape) {
|
|
1829
|
+
if (shape.kind === "circle") return { hx: shape.r, hy: shape.r };
|
|
1830
|
+
const hw = shape.width / 2;
|
|
1831
|
+
const hh = shape.height / 2;
|
|
1832
|
+
const rot = shape.rotation ?? 0;
|
|
1833
|
+
if (!rot) return { hx: hw, hy: hh };
|
|
1834
|
+
const rad = rot * Math.PI / 180;
|
|
1835
|
+
const c = Math.abs(Math.cos(rad));
|
|
1836
|
+
const s = Math.abs(Math.sin(rad));
|
|
1837
|
+
return { hx: hw * c + hh * s, hy: hw * s + hh * c };
|
|
1838
|
+
}
|
|
1839
|
+
function shapeIntersectsRect(e, x0, y0, x1, y1) {
|
|
1840
|
+
if (e.shape.kind === "circle") {
|
|
1841
|
+
const cx = Math.min(Math.max(e.seat.x, x0), x1);
|
|
1842
|
+
const cy = Math.min(Math.max(e.seat.y, y0), y1);
|
|
1843
|
+
const dx = e.seat.x - cx;
|
|
1844
|
+
const dy = e.seat.y - cy;
|
|
1845
|
+
return dx * dx + dy * dy <= e.shape.r * e.shape.r;
|
|
1846
|
+
}
|
|
1847
|
+
const rot = e.shape.rotation ?? 0;
|
|
1848
|
+
if (!rot) {
|
|
1849
|
+
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;
|
|
1850
|
+
}
|
|
1851
|
+
const rad = rot * Math.PI / 180;
|
|
1852
|
+
const c = Math.cos(rad);
|
|
1853
|
+
const s = Math.sin(rad);
|
|
1854
|
+
const hw = e.shape.width / 2;
|
|
1855
|
+
const hh = e.shape.height / 2;
|
|
1856
|
+
const corners = [
|
|
1857
|
+
[-hw, -hh],
|
|
1858
|
+
[hw, -hh],
|
|
1859
|
+
[hw, hh],
|
|
1860
|
+
[-hw, hh]
|
|
1861
|
+
].map(([lx, ly]) => [e.seat.x + lx * c - ly * s, e.seat.y + lx * s + ly * c]);
|
|
1862
|
+
const query = [
|
|
1863
|
+
[x0, y0],
|
|
1864
|
+
[x1, y0],
|
|
1865
|
+
[x1, y1],
|
|
1866
|
+
[x0, y1]
|
|
1867
|
+
];
|
|
1868
|
+
const axes = [
|
|
1869
|
+
[1, 0],
|
|
1870
|
+
[0, 1],
|
|
1871
|
+
[c, s],
|
|
1872
|
+
[-s, c]
|
|
1873
|
+
];
|
|
1874
|
+
for (const [ax, ay] of axes) {
|
|
1875
|
+
let minA = Infinity;
|
|
1876
|
+
let maxA = -Infinity;
|
|
1877
|
+
for (const [px, py] of corners) {
|
|
1878
|
+
const d = px * ax + py * ay;
|
|
1879
|
+
if (d < minA) minA = d;
|
|
1880
|
+
if (d > maxA) maxA = d;
|
|
1881
|
+
}
|
|
1882
|
+
let minB = Infinity;
|
|
1883
|
+
let maxB = -Infinity;
|
|
1884
|
+
for (const [px, py] of query) {
|
|
1885
|
+
const d = px * ax + py * ay;
|
|
1886
|
+
if (d < minB) minB = d;
|
|
1887
|
+
if (d > maxB) maxB = d;
|
|
1888
|
+
}
|
|
1889
|
+
if (maxA < minB || maxB < minA) return false;
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
function buildSeatIndex(seats, opts = {}) {
|
|
1894
|
+
const seatRadius = opts.seatRadius ?? DEFAULT_SEAT_RADIUS;
|
|
1895
|
+
const n = seats.length;
|
|
1896
|
+
const entries = new Array(n);
|
|
1897
|
+
let minX = Infinity;
|
|
1898
|
+
let minY = Infinity;
|
|
1899
|
+
let maxX = -Infinity;
|
|
1900
|
+
let maxY = -Infinity;
|
|
1901
|
+
let maxHalf = 0;
|
|
1902
|
+
for (let i = 0; i < n; i++) {
|
|
1903
|
+
const seat = seats[i];
|
|
1904
|
+
const shape = opts.shapeOf?.(seat) ?? { kind: "circle", r: seatRadius };
|
|
1905
|
+
const { hx, hy } = halfExtents(shape);
|
|
1906
|
+
entries[i] = { seat, shape, hx, hy };
|
|
1907
|
+
if (seat.x - hx < minX) minX = seat.x - hx;
|
|
1908
|
+
if (seat.y - hy < minY) minY = seat.y - hy;
|
|
1909
|
+
if (seat.x + hx > maxX) maxX = seat.x + hx;
|
|
1910
|
+
if (seat.y + hy > maxY) maxY = seat.y + hy;
|
|
1911
|
+
if (hx > maxHalf) maxHalf = hx;
|
|
1912
|
+
if (hy > maxHalf) maxHalf = hy;
|
|
1913
|
+
}
|
|
1914
|
+
if (n === 0) {
|
|
1915
|
+
minX = 0;
|
|
1916
|
+
minY = 0;
|
|
1917
|
+
maxX = 0;
|
|
1918
|
+
maxY = 0;
|
|
1919
|
+
}
|
|
1920
|
+
const nominal = maxHalf > 0 ? maxHalf * 4 : 1;
|
|
1921
|
+
const spanX = Math.max(maxX - minX, 0);
|
|
1922
|
+
const spanY = Math.max(maxY - minY, 0);
|
|
1923
|
+
const maxCells = Math.max(64, opts.maxCells ?? n * 4);
|
|
1924
|
+
let cell = nominal;
|
|
1925
|
+
let cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1926
|
+
let rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1927
|
+
while (cols * rows > maxCells && (cols > 1 || rows > 1)) {
|
|
1928
|
+
cell *= 2;
|
|
1929
|
+
cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1930
|
+
rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1931
|
+
}
|
|
1932
|
+
const cellCount = cols * rows;
|
|
1933
|
+
const cellStart = new Int32Array(cellCount + 1);
|
|
1934
|
+
let total = 0;
|
|
1935
|
+
for (let i = 0; i < n; i++) {
|
|
1936
|
+
const e = entries[i];
|
|
1937
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1938
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1939
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1940
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1941
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1942
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1943
|
+
cellStart[cy * cols + cx + 1]++;
|
|
1944
|
+
total++;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
for (let c = 0; c < cellCount; c++) cellStart[c + 1] += cellStart[c];
|
|
1949
|
+
const items = new Int32Array(total);
|
|
1950
|
+
const cursor = cellStart.slice(0, cellCount);
|
|
1951
|
+
for (let i = 0; i < n; i++) {
|
|
1952
|
+
const e = entries[i];
|
|
1953
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1954
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1955
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1956
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1957
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1958
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1959
|
+
const c = cy * cols + cx;
|
|
1960
|
+
items[cursor[c]++] = i;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return {
|
|
1965
|
+
seats,
|
|
1966
|
+
entries,
|
|
1967
|
+
minX,
|
|
1968
|
+
minY,
|
|
1969
|
+
cell,
|
|
1970
|
+
cols,
|
|
1971
|
+
rows,
|
|
1972
|
+
cellStart,
|
|
1973
|
+
items,
|
|
1974
|
+
stamp: new Int32Array(n),
|
|
1975
|
+
epoch: 0
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
function clampInt(v, lo, hi) {
|
|
1979
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
1980
|
+
}
|
|
1981
|
+
function queryRect(index, rect, opts = {}) {
|
|
1982
|
+
const x0 = Math.min(rect.x, rect.x + rect.width);
|
|
1983
|
+
const x1 = Math.max(rect.x, rect.x + rect.width);
|
|
1984
|
+
const y0 = Math.min(rect.y, rect.y + rect.height);
|
|
1985
|
+
const y1 = Math.max(rect.y, rect.y + rect.height);
|
|
1986
|
+
const mode = opts.mode ?? "center";
|
|
1987
|
+
const { cols, rows, cell, minX, minY, cellStart, items, entries } = index;
|
|
1988
|
+
const cx0 = Math.max(0, Math.floor((x0 - minX) / cell));
|
|
1989
|
+
const cx1 = Math.min(cols - 1, Math.floor((x1 - minX) / cell));
|
|
1990
|
+
const cy0 = Math.max(0, Math.floor((y0 - minY) / cell));
|
|
1991
|
+
const cy1 = Math.min(rows - 1, Math.floor((y1 - minY) / cell));
|
|
1992
|
+
if (cx0 > cx1 || cy0 > cy1) return [];
|
|
1993
|
+
const found = [];
|
|
1994
|
+
const stamp = index.stamp;
|
|
1995
|
+
const ep = ++index.epoch;
|
|
1996
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1997
|
+
const rowBase = cy * cols;
|
|
1998
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1999
|
+
const c = rowBase + cx;
|
|
2000
|
+
const end = cellStart[c + 1];
|
|
2001
|
+
for (let k = cellStart[c]; k < end; k++) {
|
|
2002
|
+
const i = items[k];
|
|
2003
|
+
if (stamp[i] === ep) continue;
|
|
2004
|
+
stamp[i] = ep;
|
|
2005
|
+
const e = entries[i];
|
|
2006
|
+
if (opts.filter && !opts.filter(e.seat.id, e.seat)) continue;
|
|
2007
|
+
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);
|
|
2008
|
+
if (ok) found.push(i);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
found.sort((a, b) => a - b);
|
|
2013
|
+
return found.map((i) => entries[i].seat.id);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/core/perspectiveProjection.ts
|
|
2017
|
+
function createPerspectiveCamera(bounds2, maxSurfaceHeightWorld = 0) {
|
|
2018
|
+
const span = Math.max(1, bounds2.width, bounds2.height);
|
|
2019
|
+
const target = {
|
|
2020
|
+
x: bounds2.x + bounds2.width / 2,
|
|
2021
|
+
y: bounds2.y + bounds2.height / 2
|
|
2022
|
+
};
|
|
2023
|
+
const distance = span * 1.55;
|
|
2024
|
+
const height = Math.max(span * 0.82, Math.max(0, maxSurfaceHeightWorld) + span * 0.45);
|
|
2025
|
+
return {
|
|
2026
|
+
target,
|
|
2027
|
+
distance,
|
|
2028
|
+
height,
|
|
2029
|
+
focalLength: Math.hypot(distance, height),
|
|
2030
|
+
yawRad: -8 * Math.PI / 180
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function projectPerspectivePoint(camera, point, heightWorld = 0) {
|
|
2034
|
+
const dx = point.x - camera.target.x;
|
|
2035
|
+
const dy = point.y - camera.target.y;
|
|
2036
|
+
const cos = Math.cos(camera.yawRad);
|
|
2037
|
+
const sin = Math.sin(camera.yawRad);
|
|
2038
|
+
const x = dx * cos - dy * sin;
|
|
2039
|
+
const y = dx * sin + dy * cos;
|
|
2040
|
+
const translatedY = y - camera.distance;
|
|
2041
|
+
const translatedZ = heightWorld - camera.height;
|
|
2042
|
+
const cameraLength = Math.hypot(camera.distance, camera.height);
|
|
2043
|
+
const depth = (-camera.distance * translatedY - camera.height * translatedZ) / cameraLength;
|
|
2044
|
+
const up = (-camera.height * translatedY + camera.distance * translatedZ) / cameraLength;
|
|
2045
|
+
const safeDepth = Math.max(cameraLength * 0.08, depth);
|
|
2046
|
+
const scale = camera.focalLength / safeDepth;
|
|
2047
|
+
return {
|
|
2048
|
+
x: camera.target.x + x * scale,
|
|
2049
|
+
y: camera.target.y - up * scale,
|
|
2050
|
+
depth: safeDepth,
|
|
2051
|
+
scale
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function applyAffine(affine, point) {
|
|
2055
|
+
return {
|
|
2056
|
+
x: affine.a * point.x + affine.c * point.y + affine.e,
|
|
2057
|
+
y: affine.b * point.x + affine.d * point.y + affine.f
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function invertAffine(affine) {
|
|
2061
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
2062
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-9) {
|
|
2063
|
+
throw new Error("perspective projection produced a singular affine");
|
|
2064
|
+
}
|
|
2065
|
+
const a = affine.d / determinant;
|
|
2066
|
+
const b = -affine.b / determinant;
|
|
2067
|
+
const c = -affine.c / determinant;
|
|
2068
|
+
const d = affine.a / determinant;
|
|
2069
|
+
return {
|
|
2070
|
+
a,
|
|
2071
|
+
b,
|
|
2072
|
+
c,
|
|
2073
|
+
d,
|
|
2074
|
+
e: -(a * affine.e + c * affine.f),
|
|
2075
|
+
f: -(b * affine.e + d * affine.f)
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function composeAffine(left, right) {
|
|
2079
|
+
return {
|
|
2080
|
+
a: left.a * right.a + left.c * right.b,
|
|
2081
|
+
b: left.b * right.a + left.d * right.b,
|
|
2082
|
+
c: left.a * right.c + left.c * right.d,
|
|
2083
|
+
d: left.b * right.c + left.d * right.d,
|
|
2084
|
+
e: left.a * right.e + left.c * right.f + left.e,
|
|
2085
|
+
f: left.b * right.e + left.d * right.f + left.f
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
function perspectiveTangentAffine(camera, anchor, heightAt = () => 0) {
|
|
2089
|
+
const origin = projectPerspectivePoint(camera, anchor, heightAt(anchor));
|
|
2090
|
+
const alongXPoint = { x: anchor.x + 1, y: anchor.y };
|
|
2091
|
+
const alongYPoint = { x: anchor.x, y: anchor.y + 1 };
|
|
2092
|
+
const alongX = projectPerspectivePoint(camera, alongXPoint, heightAt(alongXPoint));
|
|
2093
|
+
const alongY = projectPerspectivePoint(camera, alongYPoint, heightAt(alongYPoint));
|
|
2094
|
+
const a = alongX.x - origin.x;
|
|
2095
|
+
const b = alongX.y - origin.y;
|
|
2096
|
+
const c = alongY.x - origin.x;
|
|
2097
|
+
const d = alongY.y - origin.y;
|
|
2098
|
+
return {
|
|
2099
|
+
a,
|
|
2100
|
+
b,
|
|
2101
|
+
c,
|
|
2102
|
+
d,
|
|
2103
|
+
e: origin.x - a * anchor.x - c * anchor.y,
|
|
2104
|
+
f: origin.y - b * anchor.x - d * anchor.y
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function decomposeAffineLinear(affine) {
|
|
2108
|
+
const scaleX = Math.hypot(affine.a, affine.b);
|
|
2109
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
2110
|
+
return {
|
|
2111
|
+
rotationDeg: Math.atan2(affine.b, affine.a) * 180 / Math.PI,
|
|
2112
|
+
scaleX,
|
|
2113
|
+
scaleY: determinant / Math.max(scaleX, 1e-9),
|
|
2114
|
+
skewX: (affine.a * affine.c + affine.b * affine.d) / Math.max(determinant, 1e-9)
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1056
2118
|
// src/lib/money.ts
|
|
1057
2119
|
var DEFAULT_CURRENCY = "USD";
|
|
1058
2120
|
var displayLocale;
|
|
@@ -1115,6 +2177,7 @@ var en = {
|
|
|
1115
2177
|
"picker.seatsLeftInSection.one": "{count} seat left",
|
|
1116
2178
|
"picker.seatsLeftInSection.other": "{count} seats left",
|
|
1117
2179
|
"picker.overview": "Overview",
|
|
2180
|
+
"picker.entrance": "Entrance",
|
|
1118
2181
|
"picker.tapSeatHint": "Tap any seat to check its view",
|
|
1119
2182
|
"picker.ticketTierFor": "Ticket tier for {label}",
|
|
1120
2183
|
"picker.viewFromSeat": "View from seat {label}",
|
|
@@ -1172,6 +2235,25 @@ function formatDate(value, opts) {
|
|
|
1172
2235
|
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
1173
2236
|
}
|
|
1174
2237
|
|
|
2238
|
+
// src/core/shapeLineStyle.ts
|
|
2239
|
+
var DEFAULT_SHAPE_LINE_CAP = "round";
|
|
2240
|
+
var DEFAULT_SHAPE_LINE_JOIN = "round";
|
|
2241
|
+
var DEFAULT_SHAPE_LINE_ENDING = "none";
|
|
2242
|
+
function resolvedShapeLineStyle(shape) {
|
|
2243
|
+
return {
|
|
2244
|
+
lineCap: shape.lineCap ?? DEFAULT_SHAPE_LINE_CAP,
|
|
2245
|
+
lineJoin: shape.lineJoin ?? DEFAULT_SHAPE_LINE_JOIN,
|
|
2246
|
+
startEnding: shape.startEnding ?? DEFAULT_SHAPE_LINE_ENDING,
|
|
2247
|
+
endEnding: shape.endEnding ?? DEFAULT_SHAPE_LINE_ENDING
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function shapeArrowMetrics(strokeWidth) {
|
|
2251
|
+
return {
|
|
2252
|
+
pointerLength: Math.min(48, Math.max(8, strokeWidth * 3)),
|
|
2253
|
+
pointerWidth: Math.min(40, Math.max(8, strokeWidth * 2.25))
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
1175
2257
|
// src/engine/SeatmapRenderer.ts
|
|
1176
2258
|
var SEAT_RADIUS = 9;
|
|
1177
2259
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
@@ -1179,17 +2261,18 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
1179
2261
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1180
2262
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
1181
2263
|
var SEAT_TAP_SLOP_PX = 14;
|
|
1182
|
-
var
|
|
2264
|
+
var WHEELCHAIR_GLYPH_MIN_PX = 10;
|
|
2265
|
+
var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
|
|
1183
2266
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
1184
2267
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
1185
2268
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
1186
2269
|
var PAN_START_SLOP_PX = 8;
|
|
2270
|
+
var GHOST_CLICK_MS = 700;
|
|
1187
2271
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
1188
2272
|
var MAX_LABELS = 700;
|
|
1189
2273
|
var MARQUEE_RING_CAP = 2500;
|
|
1190
2274
|
var ISO_ANGLE_DEG = -11.5;
|
|
1191
2275
|
var ISO_SQUASH = 0.58;
|
|
1192
|
-
var LIFT_PER_STEP = 58;
|
|
1193
2276
|
var ISO_TWEEN_MS = 320;
|
|
1194
2277
|
var CAMERA_GLIDE_MS = 650;
|
|
1195
2278
|
var BLOCK_FILL_ALPHA = 1;
|
|
@@ -1226,6 +2309,10 @@ var DEF_SELECTION = "#ffffff";
|
|
|
1226
2309
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1227
2310
|
var DEF_DECOR_FILL = "#232c40";
|
|
1228
2311
|
var DEF_TEXT = "#8b93a7";
|
|
2312
|
+
function konvaFontStyle(bold, italic, defaultWeight = "normal") {
|
|
2313
|
+
const weight = bold ? "bold" : defaultWeight;
|
|
2314
|
+
return italic ? `italic ${weight}`.trim() : weight;
|
|
2315
|
+
}
|
|
1229
2316
|
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1230
2317
|
function colorLuminance(color) {
|
|
1231
2318
|
const s = color.trim();
|
|
@@ -1331,8 +2418,8 @@ function rotatedRectPoints(center, width, height, rotation) {
|
|
|
1331
2418
|
}));
|
|
1332
2419
|
}
|
|
1333
2420
|
function pointsBounds(points) {
|
|
1334
|
-
const
|
|
1335
|
-
return { x:
|
|
2421
|
+
const bounds2 = polyBounds(points);
|
|
2422
|
+
return { x: bounds2.x, y: bounds2.y, width: bounds2.width, height: bounds2.height };
|
|
1336
2423
|
}
|
|
1337
2424
|
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1338
2425
|
const radians = rotation * Math.PI / 180;
|
|
@@ -1352,14 +2439,14 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
|
1352
2439
|
return true;
|
|
1353
2440
|
}
|
|
1354
2441
|
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1355
|
-
const
|
|
1356
|
-
const centre = { x:
|
|
2442
|
+
const bounds2 = polyBounds(outer);
|
|
2443
|
+
const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
|
|
1357
2444
|
const points = [preferred];
|
|
1358
2445
|
for (let row = 1; row < 12; row += 1) {
|
|
1359
2446
|
for (let column = 1; column < 12; column += 1) {
|
|
1360
2447
|
const point = {
|
|
1361
|
-
x:
|
|
1362
|
-
y:
|
|
2448
|
+
x: bounds2.x + bounds2.width * column / 12,
|
|
2449
|
+
y: bounds2.y + bounds2.height * row / 12
|
|
1363
2450
|
};
|
|
1364
2451
|
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1365
2452
|
}
|
|
@@ -1424,6 +2511,33 @@ function rgba(hex, a) {
|
|
|
1424
2511
|
const n = parseInt(m[1], 16);
|
|
1425
2512
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1426
2513
|
}
|
|
2514
|
+
var ViewportGroup = class extends import_Group.Group {
|
|
2515
|
+
constructor() {
|
|
2516
|
+
super(...arguments);
|
|
2517
|
+
this.viewportCulled = false;
|
|
2518
|
+
}
|
|
2519
|
+
setViewportCulled(culled) {
|
|
2520
|
+
if (culled === this.viewportCulled) return;
|
|
2521
|
+
this.viewportCulled = culled;
|
|
2522
|
+
if (!culled) super._clearSelfAndDescendantCache();
|
|
2523
|
+
}
|
|
2524
|
+
isViewportCulled() {
|
|
2525
|
+
return this.viewportCulled;
|
|
2526
|
+
}
|
|
2527
|
+
drawScene(...args) {
|
|
2528
|
+
return this.viewportCulled ? this : super.drawScene(...args);
|
|
2529
|
+
}
|
|
2530
|
+
drawHit(...args) {
|
|
2531
|
+
return this.viewportCulled ? this : super.drawHit(...args);
|
|
2532
|
+
}
|
|
2533
|
+
_clearSelfAndDescendantCache(attr) {
|
|
2534
|
+
if (!this.viewportCulled) {
|
|
2535
|
+
super._clearSelfAndDescendantCache(attr);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
this._clearCache(attr);
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
1427
2541
|
function hexToRgb(hex) {
|
|
1428
2542
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1429
2543
|
if (!m) return null;
|
|
@@ -1454,8 +2568,15 @@ function lerpColor(a, b, t2) {
|
|
|
1454
2568
|
}
|
|
1455
2569
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
1456
2570
|
constructor(container, options = {}) {
|
|
2571
|
+
/** Per-elevated-section label containers. Keeping the labels grouped lets the
|
|
2572
|
+
* Phase-B lift move thousands of seat labels with one transform per section
|
|
2573
|
+
* instead of rewriting every label on every tween frame. */
|
|
2574
|
+
this.labelLiftGroups = /* @__PURE__ */ new Map();
|
|
1457
2575
|
this.seats = [];
|
|
1458
2576
|
this.seatById = /* @__PURE__ */ new Map();
|
|
2577
|
+
/** One build per chart/floor; section membership queries only inspect seats in
|
|
2578
|
+
* the section bounds instead of rescanning the full 13k venue per section. */
|
|
2579
|
+
this.seatIndex = null;
|
|
1459
2580
|
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
1460
2581
|
this.chartDoc = null;
|
|
1461
2582
|
this.activeFloorId = "";
|
|
@@ -1471,13 +2592,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1471
2592
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1472
2593
|
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1473
2594
|
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1474
|
-
/** Centred
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
2595
|
+
/** Centred ♿ glyph per physical wheelchair provision. It keeps a small
|
|
2596
|
+
* screen-space floor at every LOD and grows when the Wheelchair filter is
|
|
2597
|
+
* active. Kept in a map so zoom updates touch only the handful of matching
|
|
2598
|
+
* seats, never all 13k nodes. */
|
|
1478
2599
|
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1479
|
-
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1480
|
-
this.accessGlyphVisible = false;
|
|
1481
2600
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1482
2601
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1483
2602
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
@@ -1502,6 +2621,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1502
2621
|
this.ownedHold = /* @__PURE__ */ new Set();
|
|
1503
2622
|
/** One selected seat being inspected before it is committed to the cart. */
|
|
1504
2623
|
this.selectionFocusId = null;
|
|
2624
|
+
/** Seat currently owning the shared hover ring (null while not hovering). */
|
|
2625
|
+
this.hoveredId = null;
|
|
1505
2626
|
this.focusedId = null;
|
|
1506
2627
|
/**
|
|
1507
2628
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -1520,6 +2641,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1520
2641
|
this.sections = [];
|
|
1521
2642
|
this.zones = [];
|
|
1522
2643
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
2644
|
+
/** Seats outside every authored section retain the legacy path in one group. */
|
|
2645
|
+
this.unsectionedSeatGroup = null;
|
|
1523
2646
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1524
2647
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1525
2648
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
@@ -1532,6 +2655,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1532
2655
|
this.focusedSectionId = null;
|
|
1533
2656
|
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1534
2657
|
this.focusBackdrop = null;
|
|
2658
|
+
/** Non-listening visual dim outside the focused section. One overlay replaces
|
|
2659
|
+
* descendant opacity mutations across every seat in the venue. */
|
|
2660
|
+
this.focusDimOverlay = null;
|
|
1535
2661
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1536
2662
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1537
2663
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1544,10 +2670,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1544
2670
|
this.isoT = 0;
|
|
1545
2671
|
this.isoTarget = 0;
|
|
1546
2672
|
this.isoRaf = 0;
|
|
2673
|
+
/** Public view target; perspective is a separate non-affine Phase-C lane. */
|
|
2674
|
+
this.viewMode = "flat";
|
|
2675
|
+
this.perspectiveCamera = null;
|
|
2676
|
+
/** Ground-plane tangent applied once to every renderer layer. */
|
|
2677
|
+
this.perspectiveBaseAffine = null;
|
|
2678
|
+
this.perspectiveBaseInverse = null;
|
|
2679
|
+
/** Exact pinhole anchor expressed in the overlay tangent-layer coordinates. */
|
|
2680
|
+
this.perspectiveSeatLocal = /* @__PURE__ */ new Map();
|
|
2681
|
+
/** Exact projected point (before Stage pan/zoom), keyed by seat. */
|
|
2682
|
+
this.perspectiveSeatProjected = /* @__PURE__ */ new Map();
|
|
2683
|
+
/** Positive camera depth, used for deterministic far→near paint order. */
|
|
2684
|
+
this.perspectiveSeatDepth = /* @__PURE__ */ new Map();
|
|
2685
|
+
/** Exact pinhole size ratio at each seat anchor. */
|
|
2686
|
+
this.perspectiveSeatScale = /* @__PURE__ */ new Map();
|
|
2687
|
+
/** Seats whose native Konva nodes currently carry the exact projected pose.
|
|
2688
|
+
* Large sectioned venues apply these lazily as a section enters the live-seat
|
|
2689
|
+
* viewport; the overview rung hides the seat layer completely. */
|
|
2690
|
+
this.perspectiveAppliedSeats = /* @__PURE__ */ new Set();
|
|
2691
|
+
this.perspectiveBounds = null;
|
|
1547
2692
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
1548
2693
|
this.isoCentre = { x: 0, y: 0 };
|
|
1549
2694
|
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
1550
2695
|
this.glideRaf = 0;
|
|
2696
|
+
/** While the camera tween is active, defer polygon label fitting/collision to
|
|
2697
|
+
* the settled frame. Geometry remains smoothly stage-scaled, while expensive
|
|
2698
|
+
* point-in-polygon text work no longer repeats at 120 Hz. */
|
|
2699
|
+
this.glideInProgress = false;
|
|
1551
2700
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
1552
2701
|
this.destroyed = false;
|
|
1553
2702
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -1577,6 +2726,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1577
2726
|
this.panStarted = false;
|
|
1578
2727
|
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
1579
2728
|
this.moved = 0;
|
|
2729
|
+
/**
|
|
2730
|
+
* Ghost-click guard. Konva binds `_pointerup` to mouseup, touchend AND
|
|
2731
|
+
* pointerup, so `on('click tap')` is two handlers for one finger tap unless
|
|
2732
|
+
* the browser's synthesized compatibility click is suppressed. Konva only
|
|
2733
|
+
* suppresses it when the touch lands on a LISTENING SHAPE (Stage.js returns
|
|
2734
|
+
* early before its preventDefault() when `!shape || !shape.isListening()`),
|
|
2735
|
+
* so a near-miss rescued by nearestSeatToScreen used to fire `tap` (select)
|
|
2736
|
+
* then `click` (deselect) 1-3ms apart and net to nothing. With
|
|
2737
|
+
* SEAT_TAP_SLOP_PX=14 against a ~12px seat radius, that broken annulus is
|
|
2738
|
+
* ~3.7x the area of the seat itself — i.e. most successful mobile taps.
|
|
2739
|
+
* `touch-action: none` does NOT suppress compatibility mouse events.
|
|
2740
|
+
*/
|
|
2741
|
+
this.lastTapAt = 0;
|
|
1580
2742
|
/**
|
|
1581
2743
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
1582
2744
|
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
@@ -1673,6 +2835,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1673
2835
|
x: mid.x - this.pinch.worldMid.x * scale,
|
|
1674
2836
|
y: mid.y - this.pinch.worldMid.y * scale
|
|
1675
2837
|
});
|
|
2838
|
+
this.updateSeatGroupVisibility();
|
|
1676
2839
|
this.stage.batchDraw();
|
|
1677
2840
|
this.scheduleViewChange();
|
|
1678
2841
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
@@ -1685,6 +2848,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1685
2848
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
1686
2849
|
});
|
|
1687
2850
|
this.panLast = p;
|
|
2851
|
+
this.updateSeatGroupVisibility();
|
|
1688
2852
|
this.stage.batchDraw();
|
|
1689
2853
|
this.scheduleViewChange();
|
|
1690
2854
|
}
|
|
@@ -1737,6 +2901,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1737
2901
|
this.overlayLayer = new import_Layer.Layer({ listening: false });
|
|
1738
2902
|
this.labelGroup = new import_Group.Group({ listening: false });
|
|
1739
2903
|
this.overlayLayer.add(this.labelGroup);
|
|
2904
|
+
this.fgDecorGroup = new import_Group.Group({ listening: false });
|
|
2905
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1740
2906
|
this.hoverRing = new import_Circle.Circle({
|
|
1741
2907
|
radius: SEAT_RADIUS + 2,
|
|
1742
2908
|
stroke: "#ffffff",
|
|
@@ -1771,6 +2937,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1771
2937
|
this.resizeObs.observe(container);
|
|
1772
2938
|
}
|
|
1773
2939
|
}
|
|
2940
|
+
/**
|
|
2941
|
+
* True when this is the browser's synthesized compatibility click following a
|
|
2942
|
+
* finger tap we already handled. Discriminates on `e.type` deliberately —
|
|
2943
|
+
* `PointerEvent extends MouseEvent`, so an `instanceof MouseEvent` test
|
|
2944
|
+
* matches genuine pointer taps too. One shared timestamp across the layer and
|
|
2945
|
+
* stage handlers: a tap consumed at the layer must also suppress the ghost at
|
|
2946
|
+
* the stage, and only `click` is ever rejected, so bubbling stays intact.
|
|
2947
|
+
*/
|
|
2948
|
+
isGhostClick(e) {
|
|
2949
|
+
if (e.type === "tap") {
|
|
2950
|
+
this.lastTapAt = performance.now();
|
|
2951
|
+
return false;
|
|
2952
|
+
}
|
|
2953
|
+
return this.lastTapAt > 0 && performance.now() - this.lastTapAt < GHOST_CLICK_MS;
|
|
2954
|
+
}
|
|
1774
2955
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
1775
2956
|
setChart(doc, opts) {
|
|
1776
2957
|
if (doc !== this.chartDoc) this.stacked = false;
|
|
@@ -1784,8 +2965,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1784
2965
|
this.focusedId = null;
|
|
1785
2966
|
this.focusRing.visible(false);
|
|
1786
2967
|
this.bgLayer.destroyChildren();
|
|
2968
|
+
this.focusDimOverlay?.destroy();
|
|
2969
|
+
this.focusDimOverlay = null;
|
|
2970
|
+
this.seatLayer.clearCache();
|
|
2971
|
+
this.seatLayer.listening(true);
|
|
1787
2972
|
this.seatLayer.destroyChildren();
|
|
2973
|
+
this.unsectionedSeatGroup = null;
|
|
1788
2974
|
this.labelGroup.destroyChildren();
|
|
2975
|
+
this.labelLiftGroups.clear();
|
|
2976
|
+
this.fgDecorGroup.destroyChildren();
|
|
1789
2977
|
this.circleById.clear();
|
|
1790
2978
|
this.boothDims.clear();
|
|
1791
2979
|
this.boothLabelById.clear();
|
|
@@ -1802,6 +2990,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1802
2990
|
this.statusById.clear();
|
|
1803
2991
|
this.selection.clear();
|
|
1804
2992
|
this.seatById.clear();
|
|
2993
|
+
this.seatIndex = null;
|
|
1805
2994
|
this.cached = false;
|
|
1806
2995
|
this.accessFilter = null;
|
|
1807
2996
|
this.sections = [];
|
|
@@ -1819,11 +3008,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1819
3008
|
}
|
|
1820
3009
|
this.isoT = 0;
|
|
1821
3010
|
this.isoTarget = 0;
|
|
3011
|
+
this.viewMode = "flat";
|
|
3012
|
+
this.perspectiveCamera = null;
|
|
3013
|
+
this.perspectiveBaseAffine = null;
|
|
3014
|
+
this.perspectiveBaseInverse = null;
|
|
3015
|
+
this.perspectiveSeatLocal.clear();
|
|
3016
|
+
this.perspectiveSeatProjected.clear();
|
|
3017
|
+
this.perspectiveSeatDepth.clear();
|
|
3018
|
+
this.perspectiveSeatScale.clear();
|
|
3019
|
+
this.perspectiveAppliedSeats.clear();
|
|
3020
|
+
this.perspectiveBounds = null;
|
|
1822
3021
|
this.resetLayerTransforms();
|
|
1823
3022
|
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
1824
3023
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
1825
3024
|
this.seatLayer.opacity(1);
|
|
1826
3025
|
this.hoverRing.visible(false);
|
|
3026
|
+
this.hoveredId = null;
|
|
1827
3027
|
this.theme = doc.theme ?? {};
|
|
1828
3028
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1829
3029
|
this.container.style.background = "";
|
|
@@ -1844,26 +3044,61 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1844
3044
|
this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
|
|
1845
3045
|
}
|
|
1846
3046
|
}
|
|
1847
|
-
this.seats = expandChart(view
|
|
3047
|
+
this.seats = expandChart(view, {
|
|
3048
|
+
// A stacked overview is presentation-only and never opens a seat POV. The
|
|
3049
|
+
// per-section renderer below still resolves each object's real floor base.
|
|
3050
|
+
floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
|
|
3051
|
+
});
|
|
1848
3052
|
for (const s of this.seats) {
|
|
1849
3053
|
this.seatById.set(s.id, s);
|
|
1850
3054
|
this.statusById.set(s.id, "free");
|
|
1851
3055
|
}
|
|
3056
|
+
this.seatIndex = buildSeatIndex(this.seats, { seatRadius: this.seatR });
|
|
3057
|
+
this.bounds = chartBounds(view);
|
|
3058
|
+
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3059
|
+
this.buildPerspectiveProjection(view);
|
|
1852
3060
|
this.renderBackground(view);
|
|
3061
|
+
this.unsectionedSeatGroup = new import_Group.Group({ listening: true });
|
|
3062
|
+
this.seatLayer.add(this.unsectionedSeatGroup);
|
|
1853
3063
|
this.renderSeats();
|
|
1854
3064
|
this.overlayLayer.add(this.labelGroup);
|
|
3065
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1855
3066
|
this.overlayLayer.add(this.hoverRing);
|
|
1856
|
-
this.
|
|
1857
|
-
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3067
|
+
this.overlayLayer.add(this.focusRing);
|
|
1858
3068
|
this.zoomToFit();
|
|
1859
3069
|
}
|
|
1860
3070
|
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
1861
3071
|
* the whole chart for single-floor charts. */
|
|
1862
3072
|
floorView(doc) {
|
|
1863
|
-
if (!doc.floors || !doc.floors.length) return
|
|
1864
|
-
|
|
3073
|
+
if (!doc.floors || !doc.floors.length) return {
|
|
3074
|
+
...doc,
|
|
3075
|
+
referenceImage: void 0,
|
|
3076
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3077
|
+
};
|
|
3078
|
+
if (this.stacked && doc.floors.length >= 2) {
|
|
3079
|
+
return {
|
|
3080
|
+
...stackFloors(doc),
|
|
3081
|
+
referenceImage: void 0,
|
|
3082
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
1865
3085
|
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
1866
|
-
return {
|
|
3086
|
+
return {
|
|
3087
|
+
...doc,
|
|
3088
|
+
objects: floor.objects,
|
|
3089
|
+
focalPoint: floor.focalPoint,
|
|
3090
|
+
referenceImage: void 0,
|
|
3091
|
+
backgroundImage: buyerBackgroundImage(floor),
|
|
3092
|
+
floors: void 0
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
/** Physical floor height is authored data; the 900-unit stacked overview
|
|
3096
|
+
* spread is presentation-only and must never leak into real 3D geometry. */
|
|
3097
|
+
floorBaseHeightMFor(objectId) {
|
|
3098
|
+
const floors = this.chartDoc?.floors;
|
|
3099
|
+
if (!floors?.length) return 0;
|
|
3100
|
+
const floorId = this.stacked && objectId ? this.objectFloor.get(objectId) ?? this.activeFloorId : this.activeFloorId;
|
|
3101
|
+
return floors.find((floor) => floor.id === floorId)?.baseHeightM ?? 0;
|
|
1867
3102
|
}
|
|
1868
3103
|
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
1869
3104
|
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
@@ -2176,7 +3411,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2176
3411
|
}
|
|
2177
3412
|
const ids = [];
|
|
2178
3413
|
for (const seat of this.seats) {
|
|
2179
|
-
|
|
3414
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3415
|
+
if (rendered.x < x0 || rendered.x > x1 || rendered.y < y0 || rendered.y > y1) continue;
|
|
2180
3416
|
if (!this.isSelectable(seat.id)) continue;
|
|
2181
3417
|
ids.push(seat.id);
|
|
2182
3418
|
}
|
|
@@ -2186,9 +3422,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2186
3422
|
flashSeat(seatId, color = "#f43f5e") {
|
|
2187
3423
|
const seat = this.seatById.get(seatId);
|
|
2188
3424
|
if (!seat) return;
|
|
3425
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
2189
3426
|
const ring = new import_Circle.Circle({
|
|
2190
|
-
x:
|
|
2191
|
-
y:
|
|
3427
|
+
x: rendered.x,
|
|
3428
|
+
y: rendered.y,
|
|
2192
3429
|
radius: this.seatR,
|
|
2193
3430
|
stroke: color,
|
|
2194
3431
|
strokeWidth: 3,
|
|
@@ -2197,6 +3434,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2197
3434
|
perfectDrawEnabled: false,
|
|
2198
3435
|
shadowForStrokeEnabled: false
|
|
2199
3436
|
});
|
|
3437
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seatId) ?? 1 : 1;
|
|
3438
|
+
ring.scale({ x: projectionScale, y: projectionScale });
|
|
2200
3439
|
this.overlayLayer.add(ring);
|
|
2201
3440
|
const start = performance.now();
|
|
2202
3441
|
const dur = 620;
|
|
@@ -2223,17 +3462,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2223
3462
|
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2224
3463
|
if (!matches.length) return;
|
|
2225
3464
|
for (const section of matches) {
|
|
2226
|
-
const
|
|
3465
|
+
const outline = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
3466
|
+
const lift = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
3467
|
+
return { x: point.x + lift.x, y: point.y + lift.y };
|
|
3468
|
+
});
|
|
3469
|
+
const centre = outline.reduce(
|
|
2227
3470
|
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2228
3471
|
{ x: 0, y: 0 }
|
|
2229
3472
|
);
|
|
2230
|
-
centre.x /=
|
|
2231
|
-
centre.y /=
|
|
2232
|
-
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
3473
|
+
centre.x /= outline.length;
|
|
3474
|
+
centre.y /= outline.length;
|
|
2233
3475
|
const halo = new import_Line.Line({
|
|
2234
|
-
x: centre.x
|
|
2235
|
-
y: centre.y
|
|
2236
|
-
points:
|
|
3476
|
+
x: centre.x,
|
|
3477
|
+
y: centre.y,
|
|
3478
|
+
points: outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2237
3479
|
closed: true,
|
|
2238
3480
|
stroke: color,
|
|
2239
3481
|
strokeWidth: 3,
|
|
@@ -2277,12 +3519,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2277
3519
|
nearestSeat(fromId, dir) {
|
|
2278
3520
|
const from = this.seatById.get(fromId);
|
|
2279
3521
|
if (!from) return null;
|
|
3522
|
+
const fromPoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(from.id) ?? from : from;
|
|
2280
3523
|
let best = null;
|
|
2281
3524
|
let bestScore = Infinity;
|
|
2282
3525
|
for (const s of this.seats) {
|
|
2283
3526
|
if (s.id === fromId) continue;
|
|
2284
|
-
const
|
|
2285
|
-
const
|
|
3527
|
+
const candidatePoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(s.id) ?? s : s;
|
|
3528
|
+
const dx = candidatePoint.x - fromPoint.x;
|
|
3529
|
+
const dy = candidatePoint.y - fromPoint.y;
|
|
2286
3530
|
const proj = dx * dir.x + dy * dir.y;
|
|
2287
3531
|
if (proj <= 0.5) continue;
|
|
2288
3532
|
const perp = Math.abs(dx * dir.y - dy * dir.x);
|
|
@@ -2299,7 +3543,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2299
3543
|
if (!seat) return;
|
|
2300
3544
|
this.focusedId = id;
|
|
2301
3545
|
this.focusRing.radius(this.seatR + 3);
|
|
2302
|
-
this.focusRing.position(
|
|
3546
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
3547
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
3548
|
+
this.focusRing.scale({ x: projectionScale, y: projectionScale });
|
|
2303
3549
|
this.focusRing.visible(true);
|
|
2304
3550
|
this.ensureVisible(seat);
|
|
2305
3551
|
this.overlayLayer.batchDraw();
|
|
@@ -2314,7 +3560,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2314
3560
|
const margin = 70;
|
|
2315
3561
|
const offscreen = p.x < margin || p.x > w - margin || p.y < margin || p.y > h - margin;
|
|
2316
3562
|
if (this.stage.scaleX() < target || offscreen) {
|
|
2317
|
-
const
|
|
3563
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3564
|
+
const ip = this.viewMode === "perspective" && this.perspectiveBaseAffine ? applyAffine(this.perspectiveBaseAffine, rendered) : this.isoT === 0 ? rendered : this.isoForward(rendered);
|
|
2318
3565
|
this.stage.scale({ x: target, y: target });
|
|
2319
3566
|
this.stage.position({ x: w / 2 - ip.x * target, y: h / 2 - ip.y * target });
|
|
2320
3567
|
this.afterViewChange();
|
|
@@ -2324,7 +3571,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2324
3571
|
zoomToFit() {
|
|
2325
3572
|
const w = this.stage.width();
|
|
2326
3573
|
const h = this.stage.height();
|
|
2327
|
-
const b = this.bounds;
|
|
3574
|
+
const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
|
|
2328
3575
|
this.fitScale = Math.min(w / b.width, h / b.height) || 1;
|
|
2329
3576
|
const s = this.fitScale;
|
|
2330
3577
|
this.stage.scale({ x: s, y: s });
|
|
@@ -2353,7 +3600,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2353
3600
|
}
|
|
2354
3601
|
worldToScreen(point) {
|
|
2355
3602
|
const s = this.stage.scaleX();
|
|
2356
|
-
const
|
|
3603
|
+
const candidateId = point.id;
|
|
3604
|
+
const seat = typeof candidateId === "string" ? this.seatById.get(candidateId) : void 0;
|
|
3605
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3606
|
+
const projected = seat ? this.perspectiveSeatProjected.get(seat.id) ?? applyAffine(this.perspectiveBaseAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3607
|
+
return { x: projected.x * s + this.stage.x(), y: projected.y * s + this.stage.y() };
|
|
3608
|
+
}
|
|
3609
|
+
const localPoint = seat ? this.renderedSeatPoint(seat) : point;
|
|
3610
|
+
const p = this.isoT === 0 ? localPoint : this.isoForward(localPoint);
|
|
2357
3611
|
return { x: p.x * s + this.stage.x(), y: p.y * s + this.stage.y() };
|
|
2358
3612
|
}
|
|
2359
3613
|
setAccessibleFilter(on) {
|
|
@@ -2363,6 +3617,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2363
3617
|
const next = types === null ? null : [...types];
|
|
2364
3618
|
if (this.sameAccessFilter(next)) return;
|
|
2365
3619
|
this.accessFilter = next;
|
|
3620
|
+
this.updateAccessGlyphs(this.effScale());
|
|
2366
3621
|
for (const seat of this.seats) {
|
|
2367
3622
|
const c = this.circleById.get(seat.id);
|
|
2368
3623
|
if (c) this.paintSeat(c, seat.id);
|
|
@@ -2461,10 +3716,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2461
3716
|
let minY = Infinity;
|
|
2462
3717
|
let maxY = -Infinity;
|
|
2463
3718
|
for (const seat of matching) {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3719
|
+
const point = this.renderedSeatPoint(seat);
|
|
3720
|
+
minX = Math.min(minX, point.x);
|
|
3721
|
+
maxX = Math.max(maxX, point.x);
|
|
3722
|
+
minY = Math.min(minY, point.y);
|
|
3723
|
+
maxY = Math.max(maxY, point.y);
|
|
2468
3724
|
}
|
|
2469
3725
|
const pad = Math.max(24, this.seatR * 3);
|
|
2470
3726
|
minX -= pad;
|
|
@@ -2485,12 +3741,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2485
3741
|
* inverse) keeps landing on the projected seats/sections.
|
|
2486
3742
|
*/
|
|
2487
3743
|
setViewMode(mode) {
|
|
3744
|
+
if (mode === "perspective") {
|
|
3745
|
+
if (this.viewMode === mode) {
|
|
3746
|
+
this.afterViewChange();
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
const resetElevatedSeatGroups = this.viewMode === "isometric" && this.isoT > 0;
|
|
3750
|
+
this.viewMode = mode;
|
|
3751
|
+
this.isoTarget = 1;
|
|
3752
|
+
this.isoT = 1;
|
|
3753
|
+
if (this.isoRaf) {
|
|
3754
|
+
cancelAnimationFrame(this.isoRaf);
|
|
3755
|
+
this.isoRaf = 0;
|
|
3756
|
+
}
|
|
3757
|
+
this.applyPerspective(resetElevatedSeatGroups);
|
|
3758
|
+
this.zoomToFit();
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
2488
3761
|
const target = mode === "isometric" ? 1 : 0;
|
|
3762
|
+
const leavingPerspective = this.viewMode === "perspective";
|
|
3763
|
+
this.viewMode = mode;
|
|
2489
3764
|
this.isoTarget = target;
|
|
2490
3765
|
if (this.isoRaf) {
|
|
2491
3766
|
cancelAnimationFrame(this.isoRaf);
|
|
2492
3767
|
this.isoRaf = 0;
|
|
2493
3768
|
}
|
|
3769
|
+
if (leavingPerspective) {
|
|
3770
|
+
this.isoT = target;
|
|
3771
|
+
this.applyIso();
|
|
3772
|
+
this.afterViewChange();
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
2494
3775
|
if (this.reducedMotion) {
|
|
2495
3776
|
this.isoT = target;
|
|
2496
3777
|
this.applyIso();
|
|
@@ -2524,7 +3805,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2524
3805
|
}
|
|
2525
3806
|
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
2526
3807
|
getViewMode() {
|
|
2527
|
-
return this.
|
|
3808
|
+
return this.viewMode;
|
|
2528
3809
|
}
|
|
2529
3810
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
2530
3811
|
isoParams() {
|
|
@@ -2532,6 +3813,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2532
3813
|
}
|
|
2533
3814
|
/** Effective vertical scale = stage scale × iso squash — legibility math uses this. */
|
|
2534
3815
|
effScale() {
|
|
3816
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3817
|
+
const xScale = Math.hypot(this.perspectiveBaseAffine.a, this.perspectiveBaseAffine.b);
|
|
3818
|
+
const yScale = Math.hypot(this.perspectiveBaseAffine.c, this.perspectiveBaseAffine.d);
|
|
3819
|
+
return this.stage.scaleX() * Math.min(xScale, yScale);
|
|
3820
|
+
}
|
|
2535
3821
|
return this.stage.scaleX() * (1 - (1 - ISO_SQUASH) * this.isoT);
|
|
2536
3822
|
}
|
|
2537
3823
|
/** Project a world point through the iso affine about the chart centre (→ iso-world). */
|
|
@@ -2555,19 +3841,113 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2555
3841
|
const wy = -dx * Math.sin(th) + uy * Math.cos(th);
|
|
2556
3842
|
return { x: c.x + wx, y: c.y + wy };
|
|
2557
3843
|
}
|
|
3844
|
+
/** Precompute the Phase-C pinhole camera and every exact seat anchor. */
|
|
3845
|
+
buildPerspectiveProjection(view) {
|
|
3846
|
+
let maxSurfaceHeightWorld = 0;
|
|
3847
|
+
for (const seat of this.seats) {
|
|
3848
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3849
|
+
maxSurfaceHeightWorld = Math.max(maxSurfaceHeightWorld, surfaceM * CHART_UNITS_PER_METRE);
|
|
3850
|
+
}
|
|
3851
|
+
for (const object of view.objects) {
|
|
3852
|
+
if (object.type !== "section") continue;
|
|
3853
|
+
maxSurfaceHeightWorld = Math.max(
|
|
3854
|
+
maxSurfaceHeightWorld,
|
|
3855
|
+
sectionGeometry(object, { floorBaseHeightM: this.floorBaseHeightMFor(object.id) }).height * CHART_UNITS_PER_METRE
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
const camera = createPerspectiveCamera(this.bounds, maxSurfaceHeightWorld);
|
|
3859
|
+
const base = perspectiveTangentAffine(camera, camera.target);
|
|
3860
|
+
const inverse = invertAffine(base);
|
|
3861
|
+
this.perspectiveCamera = camera;
|
|
3862
|
+
this.perspectiveBaseAffine = base;
|
|
3863
|
+
this.perspectiveBaseInverse = inverse;
|
|
3864
|
+
this.perspectiveSeatLocal.clear();
|
|
3865
|
+
this.perspectiveSeatProjected.clear();
|
|
3866
|
+
this.perspectiveSeatDepth.clear();
|
|
3867
|
+
this.perspectiveSeatScale.clear();
|
|
3868
|
+
this.perspectiveBounds = null;
|
|
3869
|
+
let minX = Infinity;
|
|
3870
|
+
let minY = Infinity;
|
|
3871
|
+
let maxX = -Infinity;
|
|
3872
|
+
let maxY = -Infinity;
|
|
3873
|
+
for (const seat of this.seats) {
|
|
3874
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3875
|
+
const projected = projectPerspectivePoint(camera, seat, surfaceM * CHART_UNITS_PER_METRE);
|
|
3876
|
+
const point = { x: projected.x, y: projected.y };
|
|
3877
|
+
this.perspectiveSeatProjected.set(seat.id, point);
|
|
3878
|
+
this.perspectiveSeatLocal.set(seat.id, applyAffine(inverse, point));
|
|
3879
|
+
this.perspectiveSeatDepth.set(seat.id, projected.depth);
|
|
3880
|
+
this.perspectiveSeatScale.set(seat.id, projected.scale);
|
|
3881
|
+
minX = Math.min(minX, projected.x);
|
|
3882
|
+
minY = Math.min(minY, projected.y);
|
|
3883
|
+
maxX = Math.max(maxX, projected.x);
|
|
3884
|
+
maxY = Math.max(maxY, projected.y);
|
|
3885
|
+
}
|
|
3886
|
+
for (const point of [
|
|
3887
|
+
{ x: this.bounds.x, y: this.bounds.y },
|
|
3888
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y },
|
|
3889
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y + this.bounds.height },
|
|
3890
|
+
{ x: this.bounds.x, y: this.bounds.y + this.bounds.height }
|
|
3891
|
+
]) {
|
|
3892
|
+
const projected = projectPerspectivePoint(camera, point, 0);
|
|
3893
|
+
minX = Math.min(minX, projected.x);
|
|
3894
|
+
minY = Math.min(minY, projected.y);
|
|
3895
|
+
maxX = Math.max(maxX, projected.x);
|
|
3896
|
+
maxY = Math.max(maxY, projected.y);
|
|
3897
|
+
}
|
|
3898
|
+
const pad = Math.max(24, this.seatR * 3);
|
|
3899
|
+
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;
|
|
3900
|
+
}
|
|
3901
|
+
/** Apply an affine to a Konva container while keeping `pivot` fixed logically. */
|
|
3902
|
+
setContainerAffine(node, affine, pivot) {
|
|
3903
|
+
const decomposed = decomposeAffineLinear(affine);
|
|
3904
|
+
node.offset(pivot);
|
|
3905
|
+
node.position(applyAffine(affine, pivot));
|
|
3906
|
+
node.rotation(decomposed.rotationDeg);
|
|
3907
|
+
node.scale({ x: decomposed.scaleX, y: decomposed.scaleY });
|
|
3908
|
+
node.skewX(decomposed.skewX);
|
|
3909
|
+
node.skewY(0);
|
|
3910
|
+
}
|
|
3911
|
+
resetContainerTransform(node) {
|
|
3912
|
+
node.position({ x: 0, y: 0 });
|
|
3913
|
+
node.offset({ x: 0, y: 0 });
|
|
3914
|
+
node.scale({ x: 1, y: 1 });
|
|
3915
|
+
node.rotation(0);
|
|
3916
|
+
node.skewX(0);
|
|
3917
|
+
node.skewY(0);
|
|
3918
|
+
}
|
|
3919
|
+
/** Exact raked surface height used for the bounded section tangent plane. */
|
|
3920
|
+
sectionSurfaceHeight(section, point) {
|
|
3921
|
+
const radial = Math.hypot(point.x - section.surfaceFocal.x, point.y - section.surfaceFocal.y);
|
|
3922
|
+
const depth = Math.max(0, radial - section.frontDistanceWorld);
|
|
3923
|
+
return section.liftWorld + depth * Math.tan(section.rakeDeg * Math.PI / 180);
|
|
3924
|
+
}
|
|
3925
|
+
/** Desired perspective-space point for a section surface, before Stage pan/zoom. */
|
|
3926
|
+
projectedSectionPoint(section, point) {
|
|
3927
|
+
return section.perspectiveAffine ? applyAffine(section.perspectiveAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3928
|
+
}
|
|
3929
|
+
/** Shared-layer local point which the ground affine paints at `projected`. */
|
|
3930
|
+
perspectiveLocalPoint(projected) {
|
|
3931
|
+
return this.perspectiveBaseInverse ? applyAffine(this.perspectiveBaseInverse, projected) : projected;
|
|
3932
|
+
}
|
|
2558
3933
|
/**
|
|
2559
3934
|
* Local-space offset that, once the layer applies the iso affine, lifts an
|
|
2560
|
-
* object straight UP in iso-world by `
|
|
2561
|
-
* (= inverse-linear of the pure vertical lift). Zero at isoT=0.
|
|
3935
|
+
* object straight UP in iso-world by `liftWorld × isoT` world units
|
|
3936
|
+
* (= inverse-linear of the pure vertical lift). Zero at isoT=0. `liftWorld` is
|
|
3937
|
+
* the section's resolved real height in world units (Phase B1).
|
|
2562
3938
|
*/
|
|
2563
|
-
isoLiftLocal(
|
|
3939
|
+
isoLiftLocal(liftWorld) {
|
|
2564
3940
|
const { th, sg } = this.isoParams();
|
|
2565
|
-
const delta =
|
|
3941
|
+
const delta = liftWorld * this.isoT;
|
|
2566
3942
|
return { x: -(delta / sg) * Math.sin(th), y: -(delta / sg) * Math.cos(th) };
|
|
2567
3943
|
}
|
|
2568
|
-
/** Restore
|
|
2569
|
-
|
|
2570
|
-
|
|
3944
|
+
/** Restore projection layers to identity (flat) — byte-for-byte the original.
|
|
3945
|
+
* `seatLayer` can be skipped when entering perspective from flat: assigning a
|
|
3946
|
+
* top-level transform invalidates all 14k descendant absolute transforms even
|
|
3947
|
+
* while the semantic overview keeps that layer fully transparent. */
|
|
3948
|
+
resetLayerTransforms(includeSeatLayer = true) {
|
|
3949
|
+
const layers = includeSeatLayer ? [this.bgLayer, this.seatLayer, this.overlayLayer] : [this.bgLayer, this.overlayLayer];
|
|
3950
|
+
for (const layer of layers) {
|
|
2571
3951
|
layer.position({ x: 0, y: 0 });
|
|
2572
3952
|
layer.offset({ x: 0, y: 0 });
|
|
2573
3953
|
layer.scale({ x: 1, y: 1 });
|
|
@@ -2576,6 +3956,117 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2576
3956
|
layer.skewY(0);
|
|
2577
3957
|
}
|
|
2578
3958
|
}
|
|
3959
|
+
/** Restore section-owned containers before applying another projection. */
|
|
3960
|
+
resetSectionProjection(resetSeatGroups = true) {
|
|
3961
|
+
for (const section of this.sections) {
|
|
3962
|
+
this.resetContainerTransform(section.rootGroupBg);
|
|
3963
|
+
this.resetContainerTransform(section.liftGroupBg);
|
|
3964
|
+
if (resetSeatGroups) this.resetContainerTransform(section.liftGroupSeat);
|
|
3965
|
+
section.rootGroupBg.zIndex(section.bgZIndex);
|
|
3966
|
+
if (resetSeatGroups) section.liftGroupSeat.zIndex(section.seatZIndex);
|
|
3967
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
3968
|
+
if (labelGroup && resetSeatGroups) this.resetContainerTransform(labelGroup);
|
|
3969
|
+
section.perspectiveAffine = void 0;
|
|
3970
|
+
section.perspectiveCorrection = void 0;
|
|
3971
|
+
section.perspectiveDepth = void 0;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
/** Move native seat/booth hit shapes to or from their exact pinhole anchors. */
|
|
3975
|
+
applyExactSeatAnchors(perspective, seatIds) {
|
|
3976
|
+
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);
|
|
3977
|
+
for (const seat of seats) {
|
|
3978
|
+
if (perspective && this.perspectiveAppliedSeats.has(seat.id)) continue;
|
|
3979
|
+
const point = perspective ? this.perspectiveSeatProjected.get(seat.id) ?? seat : seat;
|
|
3980
|
+
const scale = perspective ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
3981
|
+
const shape = this.circleById.get(seat.id);
|
|
3982
|
+
shape?.position(point);
|
|
3983
|
+
shape?.scale({ x: scale, y: scale });
|
|
3984
|
+
const ring = this.accessRingById.get(seat.id);
|
|
3985
|
+
ring?.position(point);
|
|
3986
|
+
ring?.scale({ x: scale, y: scale });
|
|
3987
|
+
const glyph = this.accessGlyphById.get(seat.id);
|
|
3988
|
+
if (glyph) {
|
|
3989
|
+
const baseGlyphScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
|
|
3990
|
+
glyph.position(point);
|
|
3991
|
+
glyph.scale({ x: baseGlyphScale * scale, y: baseGlyphScale * scale });
|
|
3992
|
+
}
|
|
3993
|
+
const boothLabel = this.boothLabelById.get(seat.id);
|
|
3994
|
+
boothLabel?.position(point);
|
|
3995
|
+
boothLabel?.scale({ x: scale, y: scale });
|
|
3996
|
+
if (perspective) this.perspectiveAppliedSeats.add(seat.id);
|
|
3997
|
+
}
|
|
3998
|
+
if (!perspective) this.perspectiveAppliedSeats.clear();
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Phase C projected 2.5D. Seat/booth anchors are exact pinhole projections;
|
|
4002
|
+
* section top surfaces use one explicitly bounded tangent plane per authored
|
|
4003
|
+
* section. Native Konva shapes move with the anchors, so direct hit testing is
|
|
4004
|
+
* the same graph that paints the buyer-visible unit.
|
|
4005
|
+
*/
|
|
4006
|
+
applyPerspective(resetElevatedSeatGroups = true) {
|
|
4007
|
+
const camera = this.perspectiveCamera;
|
|
4008
|
+
const base = this.perspectiveBaseAffine;
|
|
4009
|
+
const inverse = this.perspectiveBaseInverse;
|
|
4010
|
+
if (!camera || !base || !inverse) return;
|
|
4011
|
+
const lazySectionSeats = this.seats.length > 2500 && this.sections.length > 0;
|
|
4012
|
+
if (this.cached && !lazySectionSeats) {
|
|
4013
|
+
this.seatLayer.clearCache();
|
|
4014
|
+
this.seatLayer.listening(true);
|
|
4015
|
+
this.cached = false;
|
|
4016
|
+
}
|
|
4017
|
+
this.resetLayerTransforms(resetElevatedSeatGroups);
|
|
4018
|
+
this.resetSectionProjection(resetElevatedSeatGroups);
|
|
4019
|
+
for (const layer of [this.bgLayer, this.overlayLayer]) {
|
|
4020
|
+
this.setContainerAffine(layer, base, camera.target);
|
|
4021
|
+
}
|
|
4022
|
+
this.perspectiveAppliedSeats.clear();
|
|
4023
|
+
if (!lazySectionSeats) this.applyExactSeatAnchors(true);
|
|
4024
|
+
for (const section of this.sections) {
|
|
4025
|
+
const heightAt = (point) => this.sectionSurfaceHeight(section, point);
|
|
4026
|
+
const desired = perspectiveTangentAffine(camera, section.centroid, heightAt);
|
|
4027
|
+
const correction = composeAffine(inverse, desired);
|
|
4028
|
+
section.perspectiveAffine = desired;
|
|
4029
|
+
section.perspectiveCorrection = correction;
|
|
4030
|
+
section.perspectiveDepth = projectPerspectivePoint(
|
|
4031
|
+
camera,
|
|
4032
|
+
section.centroid,
|
|
4033
|
+
heightAt(section.centroid)
|
|
4034
|
+
).depth;
|
|
4035
|
+
this.setContainerAffine(section.liftGroupBg, correction, section.centroid);
|
|
4036
|
+
for (let index = 0; index < section.sideFaces.length; index++) {
|
|
4037
|
+
const p0 = section.outline[index];
|
|
4038
|
+
const p1 = section.outline[(index + 1) % section.outline.length];
|
|
4039
|
+
const base0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, 0));
|
|
4040
|
+
const base1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, 0));
|
|
4041
|
+
const top1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, heightAt(p1)));
|
|
4042
|
+
const top0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, heightAt(p0)));
|
|
4043
|
+
section.sideFaces[index].points([
|
|
4044
|
+
base0.x,
|
|
4045
|
+
base0.y,
|
|
4046
|
+
base1.x,
|
|
4047
|
+
base1.y,
|
|
4048
|
+
top1.x,
|
|
4049
|
+
top1.y,
|
|
4050
|
+
top0.x,
|
|
4051
|
+
top0.y
|
|
4052
|
+
]);
|
|
4053
|
+
section.sideFaces[index].opacity(0.9);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
const farToNear = [...this.sections].sort(
|
|
4057
|
+
(left, right) => (right.perspectiveDepth ?? 0) - (left.perspectiveDepth ?? 0)
|
|
4058
|
+
);
|
|
4059
|
+
for (const section of farToNear) {
|
|
4060
|
+
section.rootGroupBg.moveToTop();
|
|
4061
|
+
section.liftGroupSeat.moveToTop();
|
|
4062
|
+
}
|
|
4063
|
+
this.unsectionedSeatGroup?.moveToTop();
|
|
4064
|
+
this.syncSeatOverlayPositions();
|
|
4065
|
+
if (!lazySectionSeats) this.updateSeatGroupVisibility();
|
|
4066
|
+
this.bgLayer.batchDraw();
|
|
4067
|
+
this.seatLayer.batchDraw();
|
|
4068
|
+
this.overlayLayer.batchDraw();
|
|
4069
|
+
}
|
|
2579
4070
|
/**
|
|
2580
4071
|
* Apply the current isoT to the scene: the base rotate+squash as a decomposed
|
|
2581
4072
|
* layer transform (so seats/décor/rings project together and Konva's own
|
|
@@ -2583,6 +4074,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2583
4074
|
* lift + extruded side faces on elevated sections.
|
|
2584
4075
|
*/
|
|
2585
4076
|
applyIso() {
|
|
4077
|
+
this.resetSectionProjection();
|
|
4078
|
+
this.applyExactSeatAnchors(false);
|
|
2586
4079
|
const t2 = this.isoT;
|
|
2587
4080
|
if (t2 === 0) {
|
|
2588
4081
|
this.resetLayerTransforms();
|
|
@@ -2611,6 +4104,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2611
4104
|
}
|
|
2612
4105
|
this.applyUprightLabels();
|
|
2613
4106
|
this.applyElevation();
|
|
4107
|
+
this.updateSeatGroupVisibility();
|
|
2614
4108
|
this.bgLayer.batchDraw();
|
|
2615
4109
|
this.seatLayer.batchDraw();
|
|
2616
4110
|
this.overlayLayer.batchDraw();
|
|
@@ -2639,10 +4133,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2639
4133
|
applyElevation() {
|
|
2640
4134
|
const t2 = this.isoT;
|
|
2641
4135
|
for (const sec of this.sections) {
|
|
2642
|
-
if (sec.
|
|
2643
|
-
const off = this.isoLiftLocal(sec.
|
|
4136
|
+
if (sec.liftWorld <= 0) continue;
|
|
4137
|
+
const off = this.isoLiftLocal(sec.liftWorld);
|
|
2644
4138
|
sec.liftGroupBg?.position(off);
|
|
2645
|
-
sec.liftGroupSeat
|
|
4139
|
+
sec.liftGroupSeat.position(off);
|
|
4140
|
+
this.labelLiftGroups.get(sec.id)?.position(off);
|
|
2646
4141
|
const alpha = 0.9 * t2;
|
|
2647
4142
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
2648
4143
|
const face = sec.sideFaces[i];
|
|
@@ -2652,6 +4147,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2652
4147
|
face.opacity(alpha);
|
|
2653
4148
|
}
|
|
2654
4149
|
}
|
|
4150
|
+
this.syncSeatOverlayPositions();
|
|
2655
4151
|
}
|
|
2656
4152
|
destroy() {
|
|
2657
4153
|
this.destroyed = true;
|
|
@@ -2678,16 +4174,166 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2678
4174
|
* tier shifts with one offset in iso view) or the seat layer directly.
|
|
2679
4175
|
*/
|
|
2680
4176
|
seatContainer(id) {
|
|
2681
|
-
return this.seatSection.get(id)?.liftGroupSeat ?? this.seatLayer;
|
|
4177
|
+
return this.seatSection.get(id)?.liftGroupSeat ?? this.unsectionedSeatGroup ?? this.seatLayer;
|
|
2682
4178
|
}
|
|
2683
|
-
|
|
4179
|
+
/** True when a section belongs to the active logical section/zone focus. */
|
|
4180
|
+
sectionMatchesFocus(section) {
|
|
4181
|
+
const focus = this.focusedSectionId;
|
|
4182
|
+
return focus == null || section.id === focus || section.logicalId === focus || section.zone === focus;
|
|
4183
|
+
}
|
|
4184
|
+
/** Visual opacity after the section-focus overlay is composed. */
|
|
4185
|
+
focusedSeatOpacity(id, localOpacity) {
|
|
4186
|
+
if (!this.focusedSectionId) return localOpacity;
|
|
4187
|
+
const section = this.seatSection.get(id);
|
|
4188
|
+
return localOpacity * (section && this.sectionMatchesFocus(section) ? 1 : FOCUS_DIM_OPACITY);
|
|
4189
|
+
}
|
|
4190
|
+
/** Project a section outline through elevation + the active affine view and
|
|
4191
|
+
* then into viewport pixels. The result is conservative (AABB), so a visible
|
|
4192
|
+
* section is never clipped even for rotated or isometric outlines. */
|
|
4193
|
+
sectionScreenBounds(section) {
|
|
4194
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
4195
|
+
const scale2 = this.stage.scaleX();
|
|
4196
|
+
const stageX2 = this.stage.x();
|
|
4197
|
+
const stageY2 = this.stage.y();
|
|
4198
|
+
return polyBounds(section.outline.map((point) => {
|
|
4199
|
+
const projected = this.projectedSectionPoint(section, point);
|
|
4200
|
+
return { x: projected.x * scale2 + stageX2, y: projected.y * scale2 + stageY2 };
|
|
4201
|
+
}));
|
|
4202
|
+
}
|
|
4203
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4204
|
+
const scale = this.stage.scaleX();
|
|
4205
|
+
const stageX = this.stage.x();
|
|
4206
|
+
const stageY = this.stage.y();
|
|
4207
|
+
return polyBounds(section.outline.map((point) => {
|
|
4208
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
4209
|
+
const projected = this.isoT === 0 ? local : this.isoForward(local);
|
|
4210
|
+
return { x: projected.x * scale + stageX, y: projected.y * scale + stageY };
|
|
4211
|
+
}));
|
|
4212
|
+
}
|
|
4213
|
+
/** Hide only section groups wholly outside a padded viewport at the live-seat
|
|
4214
|
+
* rung. Overview caching always restores all groups first, so panning a
|
|
4215
|
+
* cached whole-venue bitmap can never reveal missing inventory. */
|
|
4216
|
+
updateSeatGroupVisibility() {
|
|
4217
|
+
const liveSeats = this.effScale() >= CACHE_THRESHOLD;
|
|
4218
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
4219
|
+
const padding = 96;
|
|
4220
|
+
const width = this.stage.width();
|
|
4221
|
+
const height = this.stage.height();
|
|
4222
|
+
for (const section of this.sections) {
|
|
4223
|
+
let visible = true;
|
|
4224
|
+
if (liveSeats) {
|
|
4225
|
+
const bounds2 = this.sectionScreenBounds(section);
|
|
4226
|
+
visible = bounds2.x + bounds2.width >= -padding && bounds2.y + bounds2.height >= -padding && bounds2.x <= width + padding && bounds2.y <= height + padding;
|
|
4227
|
+
if (visible && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4228
|
+
this.applyExactSeatAnchors(true, section.memberIds);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
section.liftGroupSeat.setViewportCulled(!visible);
|
|
4232
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
4233
|
+
labelGroup?.setViewportCulled(!visible);
|
|
4234
|
+
}
|
|
4235
|
+
if (this.unsectionedSeatGroup && !this.unsectionedSeatGroup.visible()) {
|
|
4236
|
+
this.unsectionedSeatGroup.visible(true);
|
|
4237
|
+
}
|
|
4238
|
+
if (liveSeats && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4239
|
+
this.applyExactSeatAnchors(true, this.seats.filter((seat) => !this.seatSection.has(seat.id)).map((seat) => seat.id));
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
/** Seats worth considering for viewport labels. Section culling is a safe
|
|
4243
|
+
* coarse index; the later screen test remains the exact filter. */
|
|
4244
|
+
visibleSeatCandidates() {
|
|
4245
|
+
if (!this.sections.length) return this.seats;
|
|
4246
|
+
const candidates = [];
|
|
4247
|
+
for (const section of this.sections) {
|
|
4248
|
+
if (section.liftGroupSeat.isViewportCulled()) continue;
|
|
4249
|
+
for (const id of section.memberIds) {
|
|
4250
|
+
const seat = this.seatById.get(id);
|
|
4251
|
+
if (seat) candidates.push(seat);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
2684
4254
|
for (const seat of this.seats) {
|
|
4255
|
+
if (!this.seatSection.has(seat.id)) candidates.push(seat);
|
|
4256
|
+
}
|
|
4257
|
+
return candidates;
|
|
4258
|
+
}
|
|
4259
|
+
seatViewportCulled(id) {
|
|
4260
|
+
return this.seatSection.get(id)?.liftGroupSeat.isViewportCulled() ?? false;
|
|
4261
|
+
}
|
|
4262
|
+
/** Local world coordinate at which an elevated seat is actually painted for
|
|
4263
|
+
* the current iso tween. Flat seats and the 2D endpoint are unchanged. */
|
|
4264
|
+
renderedSeatPoint(seat) {
|
|
4265
|
+
if (this.viewMode === "perspective") return this.perspectiveSeatLocal.get(seat.id) ?? seat;
|
|
4266
|
+
const section = this.seatSection.get(seat.id);
|
|
4267
|
+
if (!section || section.liftWorld <= 0 || this.isoT === 0) return seat;
|
|
4268
|
+
const offset = this.isoLiftLocal(section.liftWorld);
|
|
4269
|
+
return { x: seat.x + offset.x, y: seat.y + offset.y };
|
|
4270
|
+
}
|
|
4271
|
+
/** Overlay labels mirror the seat-layer lift without an O(seats) per-frame
|
|
4272
|
+
* rewrite. The groups are rebuilt with the zoom-dependent label set. */
|
|
4273
|
+
seatLabelContainer(id) {
|
|
4274
|
+
const section = this.seatSection.get(id);
|
|
4275
|
+
if (!section) return this.labelGroup;
|
|
4276
|
+
let group = this.labelLiftGroups.get(section.id);
|
|
4277
|
+
if (!group) {
|
|
4278
|
+
group = new ViewportGroup({
|
|
4279
|
+
listening: false
|
|
4280
|
+
});
|
|
4281
|
+
group.setViewportCulled(section.liftGroupSeat.isViewportCulled());
|
|
4282
|
+
group.position(this.viewMode === "perspective" ? { x: 0, y: 0 } : this.isoLiftLocal(section.liftWorld));
|
|
4283
|
+
this.labelGroup.add(group);
|
|
4284
|
+
this.labelLiftGroups.set(section.id, group);
|
|
4285
|
+
}
|
|
4286
|
+
return group;
|
|
4287
|
+
}
|
|
4288
|
+
/** Shared overlay furniture is not parented to section lift groups, so keep
|
|
4289
|
+
* the small transient set aligned explicitly. Selection is capped in buyer
|
|
4290
|
+
* mode, making this constant-sized work during the iso tween. */
|
|
4291
|
+
syncSeatOverlayPositions() {
|
|
4292
|
+
if (this.hoveredId) {
|
|
4293
|
+
const seat = this.seatById.get(this.hoveredId);
|
|
4294
|
+
if (seat) {
|
|
4295
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
4296
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4297
|
+
this.hoverRing.scale({ x: scale, y: scale });
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
if (this.focusedId) {
|
|
4301
|
+
const seat = this.seatById.get(this.focusedId);
|
|
4302
|
+
if (seat) {
|
|
4303
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
4304
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4305
|
+
this.focusRing.scale({ x: scale, y: scale });
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
for (const [id, marker] of this.selectionMarkers) {
|
|
4309
|
+
const seat = this.seatById.get(id);
|
|
4310
|
+
if (seat) {
|
|
4311
|
+
marker.position(this.renderedSeatPoint(seat));
|
|
4312
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4313
|
+
marker.scale({ x: scale, y: scale });
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
renderSeats() {
|
|
4318
|
+
const paintOrder = [...this.seats].sort((left, right) => (this.perspectiveSeatDepth.get(right.id) ?? 0) - (this.perspectiveSeatDepth.get(left.id) ?? 0));
|
|
4319
|
+
for (const seat of paintOrder) {
|
|
2685
4320
|
if (seat.kind === "booth") {
|
|
2686
4321
|
this.renderBoothUnit(seat);
|
|
2687
4322
|
continue;
|
|
2688
4323
|
}
|
|
2689
4324
|
const target = this.seatContainer(seat.id);
|
|
2690
|
-
const c = new
|
|
4325
|
+
const c = seat.wheelchairSpaceType === "no-seat" ? new import_Rect.Rect({
|
|
4326
|
+
x: seat.x,
|
|
4327
|
+
y: seat.y,
|
|
4328
|
+
width: this.seatR * 2,
|
|
4329
|
+
height: this.seatR * 2,
|
|
4330
|
+
offsetX: this.seatR,
|
|
4331
|
+
offsetY: this.seatR,
|
|
4332
|
+
cornerRadius: 2,
|
|
4333
|
+
perfectDrawEnabled: false,
|
|
4334
|
+
shadowForStrokeEnabled: false,
|
|
4335
|
+
hitStrokeWidth: 0
|
|
4336
|
+
}) : new import_Circle.Circle({
|
|
2691
4337
|
x: seat.x,
|
|
2692
4338
|
y: seat.y,
|
|
2693
4339
|
radius: this.seatR,
|
|
@@ -2700,21 +4346,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2700
4346
|
this.paintSeat(c, seat.id);
|
|
2701
4347
|
target.add(c);
|
|
2702
4348
|
if (seat.accessible) {
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
4349
|
+
if (seat.wheelchairSpaceType !== "no-seat") {
|
|
4350
|
+
const ring = new import_Circle.Circle({
|
|
4351
|
+
x: seat.x,
|
|
4352
|
+
y: seat.y,
|
|
4353
|
+
radius: this.seatR + 1.5,
|
|
4354
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
4355
|
+
strokeWidth: 2.5,
|
|
4356
|
+
listening: false,
|
|
4357
|
+
perfectDrawEnabled: false,
|
|
4358
|
+
shadowForStrokeEnabled: false
|
|
4359
|
+
});
|
|
4360
|
+
this.accessRingById.set(seat.id, ring);
|
|
4361
|
+
target.add(ring);
|
|
4362
|
+
}
|
|
4363
|
+
if (seat.accessibility?.includes("wheelchair")) {
|
|
4364
|
+
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
4365
|
+
this.accessGlyphById.set(seat.id, glyph);
|
|
4366
|
+
target.add(glyph);
|
|
4367
|
+
}
|
|
2718
4368
|
}
|
|
2719
4369
|
}
|
|
2720
4370
|
}
|
|
@@ -2752,7 +4402,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2752
4402
|
t2.offsetX(t2.width() / 2);
|
|
2753
4403
|
t2.offsetY(t2.height() / 2);
|
|
2754
4404
|
t2.visible(false);
|
|
2755
|
-
this.boothLabelById.set(seat.id, t2);
|
|
2756
4405
|
this.hasBoothText = true;
|
|
2757
4406
|
this.boothLabelById.set(seat.id, t2);
|
|
2758
4407
|
target.add(t2);
|
|
@@ -2775,24 +4424,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2775
4424
|
scaleY: k,
|
|
2776
4425
|
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
2777
4426
|
listening: false,
|
|
2778
|
-
visible:
|
|
4427
|
+
visible: false,
|
|
2779
4428
|
perfectDrawEnabled: false,
|
|
2780
4429
|
shadowForStrokeEnabled: false
|
|
2781
4430
|
});
|
|
2782
4431
|
}
|
|
2783
4432
|
/**
|
|
2784
|
-
*
|
|
2785
|
-
*
|
|
2786
|
-
*
|
|
2787
|
-
* never the full node graph, so it is cheap to call on every view change.
|
|
4433
|
+
* Keep physical wheelchair provision readable in screen space at every map
|
|
4434
|
+
* LOD. The active Wheelchair filter promotes it further. Iterates the small
|
|
4435
|
+
* wheelchair-seat set, never the full node graph.
|
|
2788
4436
|
*/
|
|
2789
4437
|
updateAccessGlyphs(scale) {
|
|
2790
4438
|
if (!this.accessGlyphById.size) return;
|
|
2791
|
-
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
2792
4439
|
for (const [id, glyph] of this.accessGlyphById) {
|
|
2793
|
-
|
|
4440
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
4441
|
+
const baseScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX * perspectiveScale;
|
|
4442
|
+
const emphasized = this.accessGlyphFilterEmphasized(id);
|
|
4443
|
+
const minimumScreenPx = emphasized ? FILTERED_WHEELCHAIR_GLYPH_MIN_PX : WHEELCHAIR_GLYPH_MIN_PX;
|
|
4444
|
+
const glyphScale = Math.max(
|
|
4445
|
+
baseScale,
|
|
4446
|
+
minimumScreenPx / (ACCESS_GLYPH_VIEWBOX * Math.max(scale, 1e-3))
|
|
4447
|
+
);
|
|
4448
|
+
glyph.scale({ x: glyphScale, y: glyphScale });
|
|
4449
|
+
glyph.visible(this.accessGlyphShouldShow(id));
|
|
2794
4450
|
}
|
|
2795
4451
|
}
|
|
4452
|
+
/** True only for a physical wheelchair provision matching an active buyer filter. */
|
|
4453
|
+
accessGlyphFilterEmphasized(id) {
|
|
4454
|
+
const seat = this.seatById.get(id);
|
|
4455
|
+
const filter = this.accessFilter;
|
|
4456
|
+
return !!seat && filter !== null && !!seat.accessibility?.includes("wheelchair") && (filter.length === 0 || filter.includes("wheelchair")) && seatMatchesAccess(seat, filter);
|
|
4457
|
+
}
|
|
4458
|
+
accessGlyphShouldShow(id) {
|
|
4459
|
+
return this.accessGlyphById.has(id) && this.accessGlyphEligible(id);
|
|
4460
|
+
}
|
|
2796
4461
|
/**
|
|
2797
4462
|
* Whether an accessible seat's glyph should show for its current status. It is
|
|
2798
4463
|
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
@@ -2872,6 +4537,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2872
4537
|
c.dash([2, 2]);
|
|
2873
4538
|
break;
|
|
2874
4539
|
}
|
|
4540
|
+
if (seat.wheelchairSpaceType === "no-seat" && status === "free") {
|
|
4541
|
+
c.stroke(accessibilityRingColor(seat.accessibility));
|
|
4542
|
+
c.strokeWidth(selected ? 3 : 2);
|
|
4543
|
+
c.dash([3, 2]);
|
|
4544
|
+
}
|
|
2875
4545
|
if (boothLabel) {
|
|
2876
4546
|
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? "HELD" : seat.label);
|
|
2877
4547
|
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
@@ -2905,11 +4575,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2905
4575
|
c.dash([]);
|
|
2906
4576
|
c.opacity(CLOSED_SEAT_OPACITY);
|
|
2907
4577
|
}
|
|
2908
|
-
if (this.focusedSectionId) {
|
|
2909
|
-
const sec = this.seatSection.get(id);
|
|
2910
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2911
|
-
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
2912
|
-
}
|
|
2913
4578
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2914
4579
|
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2915
4580
|
if (bookableLabel) {
|
|
@@ -2923,7 +4588,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2923
4588
|
const fill = c.fill();
|
|
2924
4589
|
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
2925
4590
|
accessGlyph.opacity(c.opacity());
|
|
2926
|
-
accessGlyph.visible(this.
|
|
4591
|
+
accessGlyph.visible(this.accessGlyphShouldShow(id));
|
|
2927
4592
|
}
|
|
2928
4593
|
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
2929
4594
|
}
|
|
@@ -2992,7 +4657,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2992
4657
|
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2993
4658
|
this.focusedSectionId = id;
|
|
2994
4659
|
this.drawFocusBackdrop(id);
|
|
2995
|
-
this.
|
|
4660
|
+
this.bgLayer.batchDraw();
|
|
4661
|
+
this.overlayLayer.batchDraw();
|
|
2996
4662
|
this.updateLOD();
|
|
2997
4663
|
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
2998
4664
|
}
|
|
@@ -3004,7 +4670,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3004
4670
|
this.focusBackdrop.destroy();
|
|
3005
4671
|
this.focusBackdrop = null;
|
|
3006
4672
|
}
|
|
3007
|
-
this.
|
|
4673
|
+
this.focusDimOverlay?.destroy();
|
|
4674
|
+
this.focusDimOverlay = null;
|
|
4675
|
+
this.bgLayer.batchDraw();
|
|
4676
|
+
this.overlayLayer.batchDraw();
|
|
3008
4677
|
this.updateLOD();
|
|
3009
4678
|
}
|
|
3010
4679
|
/** The currently AXS-focused section id, or null. */
|
|
@@ -3021,16 +4690,69 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3021
4690
|
if (!sections.length) return;
|
|
3022
4691
|
const backdrop = new import_Group.Group({ listening: false });
|
|
3023
4692
|
for (const section of sections) {
|
|
3024
|
-
|
|
4693
|
+
const perspective = this.viewMode === "perspective" && section.perspectiveAffine;
|
|
4694
|
+
const outline = perspective ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline;
|
|
4695
|
+
const holes = perspective ? section.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point)))) : section.holes;
|
|
4696
|
+
backdrop.add(polygonWithHolesShape(outline, holes, {
|
|
3025
4697
|
fill: FOCUS_BACKDROP_FILL,
|
|
3026
4698
|
stroke: rgba("#ffffff", 0.1),
|
|
3027
4699
|
strokeWidth: 1,
|
|
3028
4700
|
listening: false
|
|
3029
|
-
}, section.outlinePath));
|
|
4701
|
+
}, perspective ? void 0 : section.outlinePath));
|
|
3030
4702
|
}
|
|
3031
4703
|
this.bgLayer.add(backdrop);
|
|
3032
4704
|
backdrop.moveToTop();
|
|
3033
4705
|
this.focusBackdrop = backdrop;
|
|
4706
|
+
this.drawFocusDimOverlay(sections);
|
|
4707
|
+
}
|
|
4708
|
+
/** Dim everything outside the active section with one paint-only shape. Group
|
|
4709
|
+
* opacity would make Konva recursively invalidate absolute-opacity caches on
|
|
4710
|
+
* all 14k descendants. The cut-out follows the live elevation offset and is
|
|
4711
|
+
* non-listening, so direct seat hit-testing is unchanged. */
|
|
4712
|
+
drawFocusDimOverlay(sections) {
|
|
4713
|
+
this.focusDimOverlay?.destroy();
|
|
4714
|
+
const span = Math.max(this.bounds.width, this.bounds.height, 1);
|
|
4715
|
+
const padding = span * 4 + 1e3;
|
|
4716
|
+
const outer = [
|
|
4717
|
+
{ x: this.bounds.x - padding, y: this.bounds.y - padding },
|
|
4718
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y - padding },
|
|
4719
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y + this.bounds.height + padding },
|
|
4720
|
+
{ x: this.bounds.x - padding, y: this.bounds.y + this.bounds.height + padding }
|
|
4721
|
+
];
|
|
4722
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
4723
|
+
const next = points[(index + 1) % points.length];
|
|
4724
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
4725
|
+
}, 0);
|
|
4726
|
+
const overlay = new import_Shape.Shape({
|
|
4727
|
+
fill: this.canvasBackground,
|
|
4728
|
+
opacity: 1 - FOCUS_DIM_OPACITY,
|
|
4729
|
+
listening: false,
|
|
4730
|
+
perfectDrawEnabled: false,
|
|
4731
|
+
sceneFunc: (context, shape) => {
|
|
4732
|
+
const polygonPath = (points) => {
|
|
4733
|
+
if (!points.length) return;
|
|
4734
|
+
context.moveTo(points[0].x, points[0].y);
|
|
4735
|
+
for (let index = 1; index < points.length; index++) context.lineTo(points[index].x, points[index].y);
|
|
4736
|
+
context.closePath();
|
|
4737
|
+
};
|
|
4738
|
+
context.beginPath();
|
|
4739
|
+
polygonPath(outer);
|
|
4740
|
+
for (const section of sections) {
|
|
4741
|
+
const hole = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
4742
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4743
|
+
return { x: point.x + offset.x, y: point.y + offset.y };
|
|
4744
|
+
});
|
|
4745
|
+
polygonPath(signedArea(hole) > 0 ? [...hole].reverse() : hole);
|
|
4746
|
+
}
|
|
4747
|
+
context.fillStrokeShape(shape);
|
|
4748
|
+
}
|
|
4749
|
+
});
|
|
4750
|
+
this.overlayLayer.add(overlay);
|
|
4751
|
+
overlay.moveToTop();
|
|
4752
|
+
for (const marker of this.selectionMarkers.values()) marker.moveToTop();
|
|
4753
|
+
this.hoverRing.moveToTop();
|
|
4754
|
+
this.focusRing.moveToTop();
|
|
4755
|
+
this.focusDimOverlay = overlay;
|
|
3034
4756
|
}
|
|
3035
4757
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
3036
4758
|
repaintSectionsAndSeats() {
|
|
@@ -3094,7 +4816,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3094
4816
|
this.applyGAFilterState();
|
|
3095
4817
|
}
|
|
3096
4818
|
renderBackground(doc) {
|
|
3097
|
-
|
|
4819
|
+
const buyerBackground = buyerBackgroundImage(doc);
|
|
4820
|
+
if (buyerBackground) this.renderBackgroundImage(buyerBackground);
|
|
3098
4821
|
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
3099
4822
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
3100
4823
|
this.renderZones(doc);
|
|
@@ -3174,10 +4897,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3174
4897
|
listening: false,
|
|
3175
4898
|
perfectDrawEnabled: false
|
|
3176
4899
|
});
|
|
3177
|
-
this.
|
|
4900
|
+
if (obj.layer === "foreground") this.fgDecorGroup.add(node);
|
|
4901
|
+
else this.bgLayer.add(node);
|
|
3178
4902
|
img.onload = () => {
|
|
3179
|
-
|
|
3180
|
-
|
|
4903
|
+
const layer = node.getLayer();
|
|
4904
|
+
if (!layer) return;
|
|
4905
|
+
layer.batchDraw();
|
|
3181
4906
|
};
|
|
3182
4907
|
img.src = obj.href;
|
|
3183
4908
|
}
|
|
@@ -3214,7 +4939,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3214
4939
|
})
|
|
3215
4940
|
);
|
|
3216
4941
|
}
|
|
3217
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
4942
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3218
4943
|
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
3219
4944
|
}
|
|
3220
4945
|
renderText(obj) {
|
|
@@ -3226,11 +4951,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3226
4951
|
text: obj.text,
|
|
3227
4952
|
fontSize: obj.fontSize,
|
|
3228
4953
|
rotation: obj.rotation,
|
|
4954
|
+
fontStyle: konvaFontStyle(obj.bold, obj.italic, "normal"),
|
|
3229
4955
|
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3230
4956
|
// the actual canvas. Fail over to readable black/white instead of
|
|
3231
4957
|
// painting an otherwise valid caption invisibly on that active surface.
|
|
3232
4958
|
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3233
|
-
fontFamily: this.labelFont(),
|
|
4959
|
+
fontFamily: obj.fontFamily ?? this.labelFont(),
|
|
3234
4960
|
listening: false,
|
|
3235
4961
|
perfectDrawEnabled: false
|
|
3236
4962
|
});
|
|
@@ -3248,11 +4974,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3248
4974
|
const isDecor = !!obj.role && !isStage;
|
|
3249
4975
|
const palette = overviewPalette(this.canvasBackground);
|
|
3250
4976
|
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3251
|
-
const
|
|
3252
|
-
const
|
|
4977
|
+
const isOpenPath = obj.kind === "line" || obj.kind === "polyline";
|
|
4978
|
+
const stroke = obj.stroke?.color ?? (isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : isOpenPath ? "#9aa3b5" : void 0);
|
|
4979
|
+
const strokeWidth = obj.stroke?.width ?? (isStage ? 1 : referenceFocal ? 2 : isOpenPath ? 2 : 0);
|
|
4980
|
+
const opacity = obj.opacity != null ? clamp(obj.opacity, 0.1, 1) : 1;
|
|
3253
4981
|
let cx = 0;
|
|
3254
4982
|
let cy = 0;
|
|
3255
|
-
if (
|
|
4983
|
+
if (isOpenPath && obj.points && obj.points.length >= 2) {
|
|
4984
|
+
cx = obj.points.reduce((a, p) => a + p.x, 0) / obj.points.length;
|
|
4985
|
+
cy = obj.points.reduce((a, p) => a + p.y, 0) / obj.points.length;
|
|
4986
|
+
const lineStyle = resolvedShapeLineStyle(obj);
|
|
4987
|
+
const arrow = shapeArrowMetrics(strokeWidth);
|
|
4988
|
+
const path = new import_Arrow.Arrow({
|
|
4989
|
+
points: obj.points.flatMap((p) => [p.x, p.y]),
|
|
4990
|
+
closed: false,
|
|
4991
|
+
x: cx,
|
|
4992
|
+
y: cy,
|
|
4993
|
+
offsetX: cx,
|
|
4994
|
+
offsetY: cy,
|
|
4995
|
+
rotation: obj.rotation ?? 0,
|
|
4996
|
+
stroke,
|
|
4997
|
+
fill: stroke,
|
|
4998
|
+
strokeWidth,
|
|
4999
|
+
opacity,
|
|
5000
|
+
lineCap: lineStyle.lineCap,
|
|
5001
|
+
lineJoin: lineStyle.lineJoin,
|
|
5002
|
+
pointerAtBeginning: lineStyle.startEnding === "arrow",
|
|
5003
|
+
pointerAtEnding: lineStyle.endEnding === "arrow",
|
|
5004
|
+
...arrow,
|
|
5005
|
+
listening: false,
|
|
5006
|
+
name: "buyer-shape-open-path"
|
|
5007
|
+
});
|
|
5008
|
+
path.setAttr("shapeObjectId", obj.id);
|
|
5009
|
+
this.bgLayer.add(path);
|
|
5010
|
+
} else if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
3256
5011
|
const grad = isStage ? {
|
|
3257
5012
|
fillLinearGradientStartPoint: { x: 0, y: 0 },
|
|
3258
5013
|
fillLinearGradientEndPoint: { x: 0, y: obj.height },
|
|
@@ -3272,7 +5027,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3272
5027
|
...grad,
|
|
3273
5028
|
stroke,
|
|
3274
5029
|
strokeWidth,
|
|
3275
|
-
cornerRadius: 4,
|
|
5030
|
+
cornerRadius: clamp(obj.cornerRadius ?? 4, 0, Math.min(obj.width, obj.height) / 2),
|
|
5031
|
+
opacity,
|
|
3276
5032
|
listening: false
|
|
3277
5033
|
})
|
|
3278
5034
|
);
|
|
@@ -3285,7 +5041,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3285
5041
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3286
5042
|
} : { fill };
|
|
3287
5043
|
this.bgLayer.add(
|
|
3288
|
-
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 })
|
|
5044
|
+
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 })
|
|
3289
5045
|
);
|
|
3290
5046
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
3291
5047
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -3300,7 +5056,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3300
5056
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3301
5057
|
} : { fill };
|
|
3302
5058
|
this.bgLayer.add(
|
|
3303
|
-
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 })
|
|
5059
|
+
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 })
|
|
3304
5060
|
);
|
|
3305
5061
|
}
|
|
3306
5062
|
if (obj.label) {
|
|
@@ -3359,7 +5115,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3359
5115
|
strokeWidth: 1.5
|
|
3360
5116
|
});
|
|
3361
5117
|
poly.setAttr("gaId", obj.id);
|
|
3362
|
-
poly.on("click tap", () =>
|
|
5118
|
+
poly.on("click tap", (e) => {
|
|
5119
|
+
if (this.isGhostClick(e)) return;
|
|
5120
|
+
this.opts.onGAClick?.(obj.id);
|
|
5121
|
+
});
|
|
3363
5122
|
poly.on("mouseenter", () => {
|
|
3364
5123
|
this.container.style.cursor = "pointer";
|
|
3365
5124
|
});
|
|
@@ -3369,7 +5128,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3369
5128
|
this.bgLayer.add(poly);
|
|
3370
5129
|
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3371
5130
|
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3372
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
5131
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3373
5132
|
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3374
5133
|
this.freeTextById.set(`${obj.id}:label`, {
|
|
3375
5134
|
objectId: obj.id,
|
|
@@ -3386,7 +5145,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3386
5145
|
categoryKey: obj.categoryKey
|
|
3387
5146
|
});
|
|
3388
5147
|
this.gaById.set(obj.id, {
|
|
3389
|
-
label: obj.label,
|
|
5148
|
+
label: obj.displayLabel ?? obj.label,
|
|
3390
5149
|
capacity: obj.capacity,
|
|
3391
5150
|
categoryKey: obj.categoryKey,
|
|
3392
5151
|
points: obj.points,
|
|
@@ -3409,7 +5168,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3409
5168
|
const memberIds = [];
|
|
3410
5169
|
const catCounts = /* @__PURE__ */ new Map();
|
|
3411
5170
|
let free = 0;
|
|
3412
|
-
|
|
5171
|
+
const bounds2 = polyBounds(obj.outline);
|
|
5172
|
+
const candidates = this.seatIndex ? queryRect(this.seatIndex, bounds2).map((id) => this.seatById.get(id)).filter((seat) => !!seat) : this.seats;
|
|
5173
|
+
for (const seat of candidates) {
|
|
3413
5174
|
if (this.seatSection.has(seat.id)) continue;
|
|
3414
5175
|
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
3415
5176
|
memberIds.push(seat.id);
|
|
@@ -3420,11 +5181,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3420
5181
|
[...catCounts].map(([key, w]) => ({ hex: this.catColor.get(key) ?? "#6e7bff", w })),
|
|
3421
5182
|
"#3a4358"
|
|
3422
5183
|
);
|
|
3423
|
-
const elevation =
|
|
3424
|
-
|
|
3425
|
-
|
|
5184
|
+
const elevation = sectionElevationTier(obj.elevation);
|
|
5185
|
+
const geometry = sectionGeometry(obj, {
|
|
5186
|
+
floorBaseHeightM: this.floorBaseHeightMFor(obj.id)
|
|
5187
|
+
});
|
|
5188
|
+
const liftWorld = geometry.height * CHART_UNITS_PER_METRE;
|
|
5189
|
+
const rootGroupBg = new import_Group.Group({ listening: false });
|
|
5190
|
+
this.bgLayer.add(rootGroupBg);
|
|
5191
|
+
const liftGroupBg = new import_Group.Group({ listening: false });
|
|
5192
|
+
rootGroupBg.add(liftGroupBg);
|
|
5193
|
+
const liftGroupSeat = new ViewportGroup({ listening: true });
|
|
5194
|
+
this.seatLayer.add(liftGroupSeat);
|
|
3426
5195
|
const sideFaces = [];
|
|
3427
|
-
if (
|
|
5196
|
+
if (liftWorld > 0) {
|
|
3428
5197
|
const faceFill = darken(this.zoneColor.get(obj.zone ?? "") ?? baseFill, 0.42);
|
|
3429
5198
|
for (let i = 0; i < obj.outline.length; i++) {
|
|
3430
5199
|
const face = new import_Line.Line({
|
|
@@ -3438,14 +5207,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3438
5207
|
perfectDrawEnabled: false
|
|
3439
5208
|
});
|
|
3440
5209
|
sideFaces.push(face);
|
|
3441
|
-
|
|
5210
|
+
rootGroupBg.add(face);
|
|
3442
5211
|
}
|
|
3443
|
-
liftGroupBg = new import_Group.Group({ listening: false });
|
|
3444
|
-
this.bgLayer.add(liftGroupBg);
|
|
3445
|
-
liftGroupSeat = new import_Group.Group({ listening: false });
|
|
3446
|
-
this.seatLayer.add(liftGroupSeat);
|
|
3447
5212
|
}
|
|
3448
|
-
|
|
5213
|
+
liftGroupBg.moveToTop();
|
|
5214
|
+
const bgTarget = liftGroupBg;
|
|
3449
5215
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
3450
5216
|
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3451
5217
|
stroke: rgba(outlineTint, 0.5),
|
|
@@ -3519,10 +5285,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3519
5285
|
nameLabelFits: true,
|
|
3520
5286
|
subLabelFits: true,
|
|
3521
5287
|
elevation,
|
|
5288
|
+
liftWorld,
|
|
5289
|
+
rakeDeg: geometry.rake,
|
|
5290
|
+
surfaceFocal: memberIds.length ? { ...this.seatById.get(memberIds[0])?.focalPoint ?? this.isoCentre } : { ...this.chartDoc?.focalPoint ?? this.isoCentre },
|
|
5291
|
+
frontDistanceWorld: 0,
|
|
5292
|
+
rootGroupBg,
|
|
5293
|
+
bgZIndex: rootGroupBg.zIndex(),
|
|
5294
|
+
seatZIndex: liftGroupSeat.zIndex(),
|
|
3522
5295
|
liftGroupBg,
|
|
3523
5296
|
liftGroupSeat,
|
|
3524
5297
|
sideFaces
|
|
3525
5298
|
};
|
|
5299
|
+
const surfaceCandidates = memberIds.map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
5300
|
+
const distanceCandidates = surfaceCandidates.length ? surfaceCandidates : obj.outline;
|
|
5301
|
+
sec.frontDistanceWorld = Math.min(...distanceCandidates.map((point) => Math.hypot(
|
|
5302
|
+
point.x - sec.surfaceFocal.x,
|
|
5303
|
+
point.y - sec.surfaceFocal.y
|
|
5304
|
+
)));
|
|
3526
5305
|
for (const id of memberIds) this.seatSection.set(id, sec);
|
|
3527
5306
|
this.refreshSectionFill(sec);
|
|
3528
5307
|
this.refreshSectionHeat(sec);
|
|
@@ -3668,15 +5447,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3668
5447
|
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3669
5448
|
if (sectionOverview) blockT = 1;
|
|
3670
5449
|
if (!this.zones.length) zoneT = 0;
|
|
3671
|
-
this.
|
|
5450
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
5451
|
+
this.seatLayer.opacity(sectionOverview || deferPerspectiveSeatReveal ? 0 : 1 - blockT);
|
|
3672
5452
|
const sx = this.stage.scaleX();
|
|
3673
|
-
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.
|
|
5453
|
+
const rescale = !this.glideInProgress && (this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.08);
|
|
3674
5454
|
if (rescale) this.lodScale = scale;
|
|
3675
5455
|
const focus = this.focusedSectionId;
|
|
3676
5456
|
const palette = overviewPalette(this.canvasBackground);
|
|
3677
5457
|
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3678
5458
|
for (const sec of this.sections) {
|
|
3679
|
-
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
5459
|
+
const dim = this.focusDimOverlay ? 1 : focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3680
5460
|
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3681
5461
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3682
5462
|
sec.blockPoly.stroke(palette.sectionStroke);
|
|
@@ -3688,7 +5468,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3688
5468
|
);
|
|
3689
5469
|
sec.nameLabel.fill(sectionInk);
|
|
3690
5470
|
sec.subLabel.fill(sectionInk);
|
|
3691
|
-
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
5471
|
+
if (rescale && sectionLabelT > 0.01) this.fitSectionRungLabels(sec, sx);
|
|
3692
5472
|
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3693
5473
|
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3694
5474
|
sec.subLabel.opacity(0);
|
|
@@ -3700,12 +5480,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3700
5480
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3701
5481
|
if (rescale) this.sizeZonePill(zone, sx);
|
|
3702
5482
|
}
|
|
3703
|
-
this.
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
const
|
|
3707
|
-
|
|
3708
|
-
|
|
5483
|
+
if (!this.glideInProgress && (sectionLabelT > 0.01 || zoneOpacity > 0.01)) {
|
|
5484
|
+
this.decollideRungLabels(sx);
|
|
5485
|
+
this.dedupeLogicalSectionLabels();
|
|
5486
|
+
for (const zone of this.zones) {
|
|
5487
|
+
const opacity = zone.label.opacity();
|
|
5488
|
+
zone.back.opacity(opacity);
|
|
5489
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
5490
|
+
}
|
|
3709
5491
|
}
|
|
3710
5492
|
this.bgLayer.batchDraw();
|
|
3711
5493
|
}
|
|
@@ -3832,13 +5614,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3832
5614
|
screenToWorld(clientPoint) {
|
|
3833
5615
|
const s = this.stage.scaleX();
|
|
3834
5616
|
const iso = { x: (clientPoint.x - this.stage.x()) / s, y: (clientPoint.y - this.stage.y()) / s };
|
|
5617
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseInverse) {
|
|
5618
|
+
return applyAffine(this.perspectiveBaseInverse, iso);
|
|
5619
|
+
}
|
|
3835
5620
|
return this.isoT === 0 ? iso : this.isoInverse(iso);
|
|
3836
5621
|
}
|
|
3837
5622
|
/** Section id under a container-relative screen point, or null (Slice 5 tap-to-zoom). */
|
|
3838
5623
|
sectionAt(clientPoint) {
|
|
3839
5624
|
if (!this.sections.length) return null;
|
|
3840
5625
|
const world = this.screenToWorld(clientPoint);
|
|
3841
|
-
const hit = this.sections.find((sec) =>
|
|
5626
|
+
const hit = this.sections.find((sec) => {
|
|
5627
|
+
if (this.viewMode === "perspective" && sec.perspectiveAffine) {
|
|
5628
|
+
return pointInPolygonWithHoles(
|
|
5629
|
+
world,
|
|
5630
|
+
sec.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))),
|
|
5631
|
+
sec.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))))
|
|
5632
|
+
);
|
|
5633
|
+
}
|
|
5634
|
+
const offset = sec.liftWorld > 0 ? this.isoLiftLocal(sec.liftWorld) : { x: 0, y: 0 };
|
|
5635
|
+
return pointInPolygonWithHoles(
|
|
5636
|
+
{ x: world.x - offset.x, y: world.y - offset.y },
|
|
5637
|
+
sec.outline,
|
|
5638
|
+
sec.holes
|
|
5639
|
+
);
|
|
5640
|
+
});
|
|
3842
5641
|
return hit ? hit.logicalId : null;
|
|
3843
5642
|
}
|
|
3844
5643
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
@@ -3934,15 +5733,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3934
5733
|
if (!seat) return;
|
|
3935
5734
|
const candidate = this.selectionFocusId === id;
|
|
3936
5735
|
const dims = this.boothDims.get(seat.rowId);
|
|
5736
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3937
5737
|
const marker = new import_Group.Group({
|
|
3938
5738
|
name: "selection-ring",
|
|
3939
|
-
x:
|
|
3940
|
-
y:
|
|
5739
|
+
x: rendered.x,
|
|
5740
|
+
y: rendered.y,
|
|
3941
5741
|
rotation: dims?.rotation ?? 0,
|
|
3942
5742
|
listening: false,
|
|
3943
5743
|
perfectDrawEnabled: false,
|
|
3944
5744
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
3945
5745
|
});
|
|
5746
|
+
if (this.viewMode === "perspective") {
|
|
5747
|
+
const projectionScale = this.perspectiveSeatScale.get(id) ?? 1;
|
|
5748
|
+
marker.scale({ x: projectionScale, y: projectionScale });
|
|
5749
|
+
}
|
|
3946
5750
|
marker.setAttr("seatId", id);
|
|
3947
5751
|
const common = {
|
|
3948
5752
|
stroke: this.effSelection,
|
|
@@ -4021,12 +5825,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4021
5825
|
* must fall through and pick.
|
|
4022
5826
|
*/
|
|
4023
5827
|
sectionBounds(id) {
|
|
4024
|
-
const
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
5828
|
+
const bounds2 = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => {
|
|
5829
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
5830
|
+
return polyBounds(section.outline.map((point) => this.projectedSectionPoint(section, point)));
|
|
5831
|
+
}
|
|
5832
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
5833
|
+
const points = section.outline.map((point) => {
|
|
5834
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
5835
|
+
return this.isoT === 0 ? local : this.isoForward(local);
|
|
5836
|
+
});
|
|
5837
|
+
return polyBounds(points);
|
|
5838
|
+
});
|
|
5839
|
+
if (!bounds2.length) return null;
|
|
5840
|
+
const left = Math.min(...bounds2.map((box) => box.x));
|
|
5841
|
+
const top = Math.min(...bounds2.map((box) => box.y));
|
|
5842
|
+
const right = Math.max(...bounds2.map((box) => box.x + box.width));
|
|
5843
|
+
const bottom = Math.max(...bounds2.map((box) => box.y + box.height));
|
|
4030
5844
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
4031
5845
|
}
|
|
4032
5846
|
sectionFrameScale(id) {
|
|
@@ -4079,15 +5893,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4079
5893
|
* hijacking a clean tap on empty space beyond the slop.
|
|
4080
5894
|
*/
|
|
4081
5895
|
nearestSeatToScreen(screen, slopPx) {
|
|
4082
|
-
const s = this.stage.scaleX() || 1;
|
|
4083
|
-
const reachWorld = this.seatR + slopPx / s;
|
|
4084
|
-
const world = this.screenToWorld(screen);
|
|
4085
5896
|
let best = null;
|
|
4086
|
-
let bestD =
|
|
5897
|
+
let bestD = Infinity;
|
|
5898
|
+
const stageScale = this.stage.scaleX() || 1;
|
|
4087
5899
|
for (const seat of this.seats) {
|
|
4088
5900
|
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
4089
|
-
const
|
|
4090
|
-
|
|
5901
|
+
const centre = this.worldToScreen(seat);
|
|
5902
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
5903
|
+
const reachPx = this.seatR * stageScale * perspectiveScale + slopPx;
|
|
5904
|
+
const d = Math.hypot(centre.x - screen.x, centre.y - screen.y);
|
|
5905
|
+
if (d <= reachPx && d < bestD) {
|
|
4091
5906
|
bestD = d;
|
|
4092
5907
|
best = seat.id;
|
|
4093
5908
|
}
|
|
@@ -4097,6 +5912,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4097
5912
|
wireInteraction() {
|
|
4098
5913
|
this.seatLayer.on("click tap", (e) => {
|
|
4099
5914
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5915
|
+
if (this.isGhostClick(e)) return;
|
|
4100
5916
|
const id = seatIdOf(e.target);
|
|
4101
5917
|
if (!id) return;
|
|
4102
5918
|
this.handleSeatTap(id);
|
|
@@ -4106,13 +5922,17 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4106
5922
|
if (!id) return;
|
|
4107
5923
|
const seat = this.seatById.get(id);
|
|
4108
5924
|
if (!seat) return;
|
|
4109
|
-
this.
|
|
5925
|
+
this.hoveredId = id;
|
|
5926
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
5927
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
5928
|
+
this.hoverRing.scale({ x: projectionScale, y: projectionScale });
|
|
4110
5929
|
this.hoverRing.visible(true);
|
|
4111
5930
|
this.overlayLayer.batchDraw();
|
|
4112
5931
|
this.container.style.cursor = "pointer";
|
|
4113
5932
|
this.opts.onHover?.(seat);
|
|
4114
5933
|
});
|
|
4115
5934
|
this.seatLayer.on("mouseout", () => {
|
|
5935
|
+
this.hoveredId = null;
|
|
4116
5936
|
this.hoverRing.visible(false);
|
|
4117
5937
|
this.overlayLayer.batchDraw();
|
|
4118
5938
|
this.container.style.cursor = "default";
|
|
@@ -4127,6 +5947,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4127
5947
|
});
|
|
4128
5948
|
this.stage.on("click tap", (e) => {
|
|
4129
5949
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5950
|
+
if (this.isGhostClick(e)) return;
|
|
4130
5951
|
const pointer = this.stage.getPointerPosition();
|
|
4131
5952
|
if (!pointer) return;
|
|
4132
5953
|
if (!this.cached) {
|
|
@@ -4143,11 +5964,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4143
5964
|
}
|
|
4144
5965
|
}
|
|
4145
5966
|
if (this.sections.length) {
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
else this.focusRegion(hit.logicalId);
|
|
5967
|
+
const sectionId = this.sectionAt(pointer);
|
|
5968
|
+
if (sectionId) {
|
|
5969
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sectionId);
|
|
5970
|
+
else this.focusRegion(sectionId);
|
|
4151
5971
|
return;
|
|
4152
5972
|
}
|
|
4153
5973
|
}
|
|
@@ -4164,13 +5984,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4164
5984
|
*/
|
|
4165
5985
|
deckFloorAt() {
|
|
4166
5986
|
if (!this.objectFloor.size) return null;
|
|
4167
|
-
const
|
|
4168
|
-
if (!
|
|
5987
|
+
const pointer = this.stage.getPointerPosition();
|
|
5988
|
+
if (!pointer) return null;
|
|
5989
|
+
const p = this.screenToWorld(pointer);
|
|
4169
5990
|
let best = null;
|
|
4170
5991
|
let bestD = Infinity;
|
|
4171
5992
|
for (const s of this.seats) {
|
|
4172
|
-
const
|
|
4173
|
-
const
|
|
5993
|
+
const rendered = this.renderedSeatPoint(s);
|
|
5994
|
+
const dx = rendered.x - p.x;
|
|
5995
|
+
const dy = rendered.y - p.y;
|
|
4174
5996
|
const d = dx * dx + dy * dy;
|
|
4175
5997
|
if (d < bestD) {
|
|
4176
5998
|
bestD = d;
|
|
@@ -4257,9 +6079,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4257
6079
|
}
|
|
4258
6080
|
const start = performance.now();
|
|
4259
6081
|
const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
|
|
6082
|
+
this.glideInProgress = true;
|
|
4260
6083
|
const step = (now) => {
|
|
4261
6084
|
if (this.destroyed) {
|
|
4262
6085
|
this.glideRaf = 0;
|
|
6086
|
+
this.glideInProgress = false;
|
|
4263
6087
|
return;
|
|
4264
6088
|
}
|
|
4265
6089
|
const raw = Math.min(1, (now - start) / duration);
|
|
@@ -4267,6 +6091,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4267
6091
|
const sc = fromScale + (toScale - fromScale) * e;
|
|
4268
6092
|
this.stage.scale({ x: sc, y: sc });
|
|
4269
6093
|
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
6094
|
+
this.updateSeatGroupVisibility();
|
|
4270
6095
|
this.updateLOD();
|
|
4271
6096
|
this.scheduleViewChange();
|
|
4272
6097
|
this.stage.batchDraw();
|
|
@@ -4274,6 +6099,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4274
6099
|
this.glideRaf = requestAnimationFrame(step);
|
|
4275
6100
|
} else {
|
|
4276
6101
|
this.glideRaf = 0;
|
|
6102
|
+
this.glideInProgress = false;
|
|
4277
6103
|
this.afterViewChange();
|
|
4278
6104
|
}
|
|
4279
6105
|
};
|
|
@@ -4285,6 +6111,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4285
6111
|
cancelAnimationFrame(this.glideRaf);
|
|
4286
6112
|
this.glideRaf = 0;
|
|
4287
6113
|
}
|
|
6114
|
+
this.glideInProgress = false;
|
|
4288
6115
|
}
|
|
4289
6116
|
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
4290
6117
|
getRung() {
|
|
@@ -4301,11 +6128,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4301
6128
|
const labels = this.seats.map((seat) => {
|
|
4302
6129
|
const shape = this.circleById.get(seat.id);
|
|
4303
6130
|
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
6131
|
+
const localPerspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
6132
|
+
const seatEffectiveScale = (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale;
|
|
4304
6133
|
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4305
|
-
const
|
|
6134
|
+
const labelEffectiveScale = this.viewMode === "perspective" && seat.kind !== "booth" ? effectiveScale * localPerspectiveScale : seatEffectiveScale;
|
|
6135
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * labelEffectiveScale);
|
|
4306
6136
|
const screen = this.worldToScreen(seat);
|
|
4307
6137
|
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4308
|
-
const opacity = shape?.
|
|
6138
|
+
const opacity = this.focusedSeatOpacity(seat.id, shape?.getAbsoluteOpacity() ?? 0);
|
|
4309
6139
|
const section = this.seatSection.get(seat.id);
|
|
4310
6140
|
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4311
6141
|
let hiddenReason;
|
|
@@ -4316,17 +6146,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4316
6146
|
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4317
6147
|
else hiddenReason = "renderer-hidden";
|
|
4318
6148
|
}
|
|
4319
|
-
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4320
|
-
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4321
|
-
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale : this.seatR * 2 *
|
|
4322
|
-
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * stageScale : this.seatR * 2 *
|
|
4323
|
-
const assistedDiameterPx = 2 * (this.seatR *
|
|
6149
|
+
const labelWidth = label ? label.width() * stageScale * localPerspectiveScale : 0;
|
|
6150
|
+
const labelHeight = label ? label.height() * (this.viewMode === "perspective" && seat.kind === "booth" ? stageScale : effectiveScale) * localPerspectiveScale : 0;
|
|
6151
|
+
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6152
|
+
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6153
|
+
const assistedDiameterPx = 2 * (this.seatR * seatEffectiveScale + SEAT_TAP_SLOP_PX);
|
|
4324
6154
|
const fill = shape?.fill();
|
|
4325
6155
|
const ink = label?.fill();
|
|
6156
|
+
const accessGlyph = this.accessGlyphById.get(seat.id);
|
|
6157
|
+
const accessGlyphScale = accessGlyph?.getAbsoluteScale();
|
|
4326
6158
|
return {
|
|
4327
6159
|
seatId: seat.id,
|
|
4328
6160
|
label: seat.label,
|
|
4329
6161
|
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
6162
|
+
markerShape: seat.kind === "booth" ? "booth" : seat.wheelchairSpaceType === "no-seat" ? "square" : "circle",
|
|
6163
|
+
...seat.wheelchairSpaceType ? { wheelchairSpaceType: seat.wheelchairSpaceType } : {},
|
|
4330
6164
|
categoryKey: seat.categoryKey,
|
|
4331
6165
|
...section ? { sectionId: section.id } : {},
|
|
4332
6166
|
...section?.zone ? { zoneId: section.zone } : {},
|
|
@@ -4337,8 +6171,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4337
6171
|
fill: typeof fill === "string" ? fill : "",
|
|
4338
6172
|
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4339
6173
|
opacity: rounded(opacity),
|
|
6174
|
+
...accessGlyph ? {
|
|
6175
|
+
accessibilityMarker: {
|
|
6176
|
+
glyphVisible: accessGlyph.isVisible(),
|
|
6177
|
+
glyphWidthPx: rounded(ACCESS_GLYPH_VIEWBOX * Math.abs(accessGlyphScale?.x ?? 0)),
|
|
6178
|
+
emphasizedByFilter: this.accessGlyphFilterEmphasized(seat.id)
|
|
6179
|
+
}
|
|
6180
|
+
} : {},
|
|
4340
6181
|
pointerTarget: {
|
|
4341
|
-
active: !this.cached && this.isSelectable(seat.id),
|
|
6182
|
+
active: !this.cached && !this.seatViewportCulled(seat.id) && Boolean(shape?.isVisible()) && Boolean(shape?.isListening()) && this.isSelectable(seat.id),
|
|
4342
6183
|
directWidthPx: rounded(directWidthPx),
|
|
4343
6184
|
directHeightPx: rounded(directHeightPx),
|
|
4344
6185
|
effectiveMinimumPx: rounded(Math.max(
|
|
@@ -4366,7 +6207,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4366
6207
|
node.height(),
|
|
4367
6208
|
node.rotation()
|
|
4368
6209
|
);
|
|
4369
|
-
const
|
|
6210
|
+
const lift = section && section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
6211
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen({
|
|
6212
|
+
x: corner.x + lift.x,
|
|
6213
|
+
y: corner.y + lift.y
|
|
6214
|
+
})));
|
|
4370
6215
|
const opacity = rounded(node.opacity());
|
|
4371
6216
|
const ink = node.fill();
|
|
4372
6217
|
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
@@ -4505,6 +6350,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4505
6350
|
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4506
6351
|
return {
|
|
4507
6352
|
viewport,
|
|
6353
|
+
projection: this.viewMode,
|
|
6354
|
+
...this.viewMode === "perspective" ? {
|
|
6355
|
+
perspective: {
|
|
6356
|
+
model: "pinhole-exact-seat-anchors",
|
|
6357
|
+
sectionSurfaceModel: "tangent-plane",
|
|
6358
|
+
exactSeatAnchorCount: this.perspectiveSeatProjected.size,
|
|
6359
|
+
depthSorted: true
|
|
6360
|
+
}
|
|
6361
|
+
} : {},
|
|
4508
6362
|
canvasBackground: this.canvasBackground,
|
|
4509
6363
|
effectiveScale: rounded(effectiveScale),
|
|
4510
6364
|
rung: this.getRung(),
|
|
@@ -4559,10 +6413,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4559
6413
|
let cy = viewCentre.y;
|
|
4560
6414
|
if (rung === "sections" && this.sections.length > 0) {
|
|
4561
6415
|
const sectionCentres = this.sections.map((section) => {
|
|
4562
|
-
const
|
|
6416
|
+
const bounds2 = polyBounds(section.outline);
|
|
4563
6417
|
return {
|
|
4564
|
-
x:
|
|
4565
|
-
y:
|
|
6418
|
+
x: bounds2.x + bounds2.width / 2,
|
|
6419
|
+
y: bounds2.y + bounds2.height / 2
|
|
4566
6420
|
};
|
|
4567
6421
|
});
|
|
4568
6422
|
const halfWidth = w / (target * 2);
|
|
@@ -4629,6 +6483,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4629
6483
|
}
|
|
4630
6484
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
4631
6485
|
afterViewChange() {
|
|
6486
|
+
this.updateSeatGroupVisibility();
|
|
4632
6487
|
this.updateLOD();
|
|
4633
6488
|
this.updateFreeTextVisibility();
|
|
4634
6489
|
this.updateLabels();
|
|
@@ -4653,10 +6508,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4653
6508
|
this.paintGAStateForView();
|
|
4654
6509
|
this.updateAccessGlyphs(scale);
|
|
4655
6510
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
6511
|
+
const suppressPerspectiveSeatCache = this.viewMode === "perspective" && this.hasSections && this.seats.length > 2500 && shouldCache;
|
|
6512
|
+
if (suppressPerspectiveSeatCache) {
|
|
6513
|
+
this.seatLayer.listening(false);
|
|
6514
|
+
return;
|
|
6515
|
+
}
|
|
4656
6516
|
if (shouldCache && !this.cached) {
|
|
4657
6517
|
this.cacheSeatLayer();
|
|
4658
|
-
} else if (!shouldCache && this.cached) {
|
|
4659
|
-
this.seatLayer.clearCache();
|
|
6518
|
+
} else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
|
|
6519
|
+
if (this.cached) this.seatLayer.clearCache();
|
|
4660
6520
|
this.seatLayer.listening(true);
|
|
4661
6521
|
this.cached = false;
|
|
4662
6522
|
this.seatLayer.batchDraw();
|
|
@@ -4665,6 +6525,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4665
6525
|
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
4666
6526
|
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
4667
6527
|
rebuildSeatCache() {
|
|
6528
|
+
this.updateSeatGroupVisibility();
|
|
4668
6529
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
4669
6530
|
this.seatLayer.clearCache();
|
|
4670
6531
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
@@ -4716,31 +6577,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4716
6577
|
for (const [id, label] of this.boothLabelById) {
|
|
4717
6578
|
const shape = this.circleById.get(id);
|
|
4718
6579
|
label.visible(
|
|
4719
|
-
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.
|
|
6580
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && Boolean(shape?.isVisible()) && this.focusedSeatOpacity(id, shape?.getAbsoluteOpacity() ?? 1) >= 0.5
|
|
4720
6581
|
);
|
|
4721
6582
|
}
|
|
4722
6583
|
this.labelGroup.destroyChildren();
|
|
6584
|
+
this.labelLiftGroups.clear();
|
|
4723
6585
|
this.seatLabelById.clear();
|
|
4724
6586
|
if (!show) {
|
|
4725
6587
|
this.overlayLayer.batchDraw();
|
|
4726
6588
|
return;
|
|
4727
6589
|
}
|
|
4728
|
-
const s = this.stage.scaleX();
|
|
4729
|
-
const x0 = -this.stage.x() / s;
|
|
4730
|
-
const y0 = -this.stage.y() / s;
|
|
4731
|
-
const x1 = (this.stage.width() - this.stage.x()) / s;
|
|
4732
|
-
const y1 = (this.stage.height() - this.stage.y()) / s;
|
|
4733
6590
|
let count = 0;
|
|
4734
|
-
for (const seat of this.
|
|
6591
|
+
for (const seat of this.visibleSeatCandidates()) {
|
|
4735
6592
|
if (seat.kind === "booth") continue;
|
|
4736
|
-
|
|
4737
|
-
if (
|
|
6593
|
+
const screen = this.worldToScreen(seat);
|
|
6594
|
+
if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
|
|
6595
|
+
if (this.accessGlyphById.has(seat.id) && this.accessGlyphShouldShow(seat.id)) continue;
|
|
4738
6596
|
const shape = this.circleById.get(seat.id);
|
|
4739
|
-
if (
|
|
6597
|
+
if (!shape?.isVisible() || this.focusedSeatOpacity(seat.id, shape.getAbsoluteOpacity()) < 0.5) continue;
|
|
4740
6598
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
6599
|
+
const labelPoint = this.renderedSeatPoint(seat);
|
|
6600
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4741
6601
|
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
4742
6602
|
if (unavailable) {
|
|
4743
|
-
const cue = new import_Group.Group({ x:
|
|
6603
|
+
const cue = new import_Group.Group({ x: labelPoint.x, y: labelPoint.y, listening: false });
|
|
6604
|
+
cue.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
4744
6605
|
if (status === "held") {
|
|
4745
6606
|
cue.add(new import_Rect.Rect({
|
|
4746
6607
|
x: -4.2,
|
|
@@ -4769,14 +6630,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4769
6630
|
listening: false
|
|
4770
6631
|
}));
|
|
4771
6632
|
}
|
|
4772
|
-
this.
|
|
6633
|
+
this.seatLabelContainer(seat.id).add(cue);
|
|
4773
6634
|
if (++count >= MAX_LABELS) break;
|
|
4774
6635
|
continue;
|
|
4775
6636
|
}
|
|
4776
6637
|
const authoredFontSize = SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4777
6638
|
const t2 = new import_Text.Text({
|
|
4778
|
-
x:
|
|
4779
|
-
y:
|
|
6639
|
+
x: labelPoint.x,
|
|
6640
|
+
y: labelPoint.y,
|
|
4780
6641
|
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4781
6642
|
fontSize: authoredFontSize,
|
|
4782
6643
|
fontStyle: "600",
|
|
@@ -4797,11 +6658,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4797
6658
|
}
|
|
4798
6659
|
t2.offsetX(t2.width() / 2);
|
|
4799
6660
|
t2.offsetY(t2.height() / 2);
|
|
4800
|
-
|
|
6661
|
+
t2.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
6662
|
+
this.seatLabelContainer(seat.id).add(t2);
|
|
4801
6663
|
this.seatLabelById.set(seat.id, t2);
|
|
4802
6664
|
if (++count >= MAX_LABELS) break;
|
|
4803
6665
|
}
|
|
4804
|
-
if (this.isoT > 0) this.applyUprightLabels();
|
|
6666
|
+
if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
|
|
4805
6667
|
this.overlayLayer.batchDraw();
|
|
4806
6668
|
}
|
|
4807
6669
|
handleResize() {
|
|
@@ -4867,6 +6729,7 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
4867
6729
|
|
|
4868
6730
|
// src/core/bestAvailable.ts
|
|
4869
6731
|
function seatIndex2(seat, fallback) {
|
|
6732
|
+
if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
|
|
4870
6733
|
const i = seat.id.lastIndexOf(":");
|
|
4871
6734
|
if (i < 0) return fallback;
|
|
4872
6735
|
const n = Number(seat.id.slice(i + 1));
|
|
@@ -4886,21 +6749,25 @@ function centroid(seats) {
|
|
|
4886
6749
|
}
|
|
4887
6750
|
return { x: x / seats.length, y: y / seats.length };
|
|
4888
6751
|
}
|
|
6752
|
+
function candidateFocal(seats, fallback) {
|
|
6753
|
+
return seats.find((seat) => seat.focalPoint)?.focalPoint ?? fallback;
|
|
6754
|
+
}
|
|
4889
6755
|
function isPremium(seat) {
|
|
4890
6756
|
return seat.commercial?.premium === true;
|
|
4891
6757
|
}
|
|
4892
6758
|
function pickBestAvailable(seats, available, opts) {
|
|
4893
6759
|
const qty = Math.floor(opts.qty);
|
|
4894
6760
|
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
4895
|
-
const { categoryKey, focal, preferPremium } = opts;
|
|
6761
|
+
const { categoryKey, zoneId, focal, preferPremium } = opts;
|
|
4896
6762
|
const rows = /* @__PURE__ */ new Map();
|
|
4897
6763
|
const eligibleAll = [];
|
|
4898
6764
|
seats.forEach((seat, i) => {
|
|
4899
|
-
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
4900
|
-
|
|
6765
|
+
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey) && (!zoneId || seat.zoneId === zoneId);
|
|
6766
|
+
const rowId = seat.logicalRowId ?? seat.rowId;
|
|
6767
|
+
let arr = rows.get(rowId);
|
|
4901
6768
|
if (!arr) {
|
|
4902
6769
|
arr = [];
|
|
4903
|
-
rows.set(
|
|
6770
|
+
rows.set(rowId, arr);
|
|
4904
6771
|
}
|
|
4905
6772
|
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
4906
6773
|
if (elig) eligibleAll.push(seat);
|
|
@@ -4925,7 +6792,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4925
6792
|
continue;
|
|
4926
6793
|
}
|
|
4927
6794
|
let j = i;
|
|
4928
|
-
while (j < slots.length && slots[j].elig) j++;
|
|
6795
|
+
while (j < slots.length && slots[j].elig && (j === i || slots[j].index === slots[j - 1].index + 1)) j++;
|
|
4929
6796
|
const segLen = j - i;
|
|
4930
6797
|
for (let p = 0; p + qty <= segLen; p++) {
|
|
4931
6798
|
const leftRem = p;
|
|
@@ -4939,7 +6806,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4939
6806
|
startIndex: slots[i + p].index,
|
|
4940
6807
|
orphan,
|
|
4941
6808
|
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
4942
|
-
d2: dist2(centroid(runSeats), focal)
|
|
6809
|
+
d2: dist2(centroid(runSeats), candidateFocal(runSeats, focal))
|
|
4943
6810
|
};
|
|
4944
6811
|
if (better(c)) best = c;
|
|
4945
6812
|
}
|
|
@@ -4948,20 +6815,71 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4948
6815
|
}
|
|
4949
6816
|
if (best) return { labels: best.labels };
|
|
4950
6817
|
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
4951
|
-
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
|
|
6818
|
+
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, seat.focalPoint ?? focal) })).sort((a, b) => {
|
|
4952
6819
|
if (preferPremium) {
|
|
4953
6820
|
const pa = isPremium(a.seat) ? 0 : 1;
|
|
4954
6821
|
const pb = isPremium(b.seat) ? 0 : 1;
|
|
4955
6822
|
if (pa !== pb) return pa - pb;
|
|
4956
6823
|
}
|
|
4957
6824
|
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
4958
|
-
const r = a.seat.rowId.localeCompare(b.seat.rowId);
|
|
6825
|
+
const r = (a.seat.logicalRowId ?? a.seat.rowId).localeCompare(b.seat.logicalRowId ?? b.seat.rowId);
|
|
4959
6826
|
if (r !== 0) return r;
|
|
4960
6827
|
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
4961
6828
|
});
|
|
4962
6829
|
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
4963
6830
|
}
|
|
4964
6831
|
|
|
6832
|
+
// src/core/tableInventory.ts
|
|
6833
|
+
var GroupedTableProjectionError = class extends Error {
|
|
6834
|
+
constructor(objectId, message) {
|
|
6835
|
+
super(message);
|
|
6836
|
+
this.objectId = objectId;
|
|
6837
|
+
this.name = "GroupedTableProjectionError";
|
|
6838
|
+
}
|
|
6839
|
+
};
|
|
6840
|
+
function bounds(table) {
|
|
6841
|
+
const min = table.minOccupancy;
|
|
6842
|
+
const max = table.maxOccupancy;
|
|
6843
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > table.seatCount) {
|
|
6844
|
+
throw new GroupedTableProjectionError(
|
|
6845
|
+
table.id,
|
|
6846
|
+
`Table "${table.label}" has invalid variable-occupancy bounds`
|
|
6847
|
+
);
|
|
6848
|
+
}
|
|
6849
|
+
return { min, max };
|
|
6850
|
+
}
|
|
6851
|
+
function groupedTableInventory(doc, modelVersion) {
|
|
6852
|
+
if (modelVersion !== 2) return [];
|
|
6853
|
+
const units = [];
|
|
6854
|
+
for (const object of allObjects(doc)) {
|
|
6855
|
+
if (object.type !== "table" || !object.bookAsWhole && !object.variableOccupancy) continue;
|
|
6856
|
+
if (object.bookAsWhole && object.variableOccupancy) {
|
|
6857
|
+
throw new GroupedTableProjectionError(
|
|
6858
|
+
object.id,
|
|
6859
|
+
`Table "${object.label}" cannot be whole-table and variable-occupancy inventory`
|
|
6860
|
+
);
|
|
6861
|
+
}
|
|
6862
|
+
if (!Number.isInteger(object.seatCount) || object.seatCount < 1 || !object.label.trim()) {
|
|
6863
|
+
throw new GroupedTableProjectionError(object.id, "Grouped tables require a label and at least one chair");
|
|
6864
|
+
}
|
|
6865
|
+
const mode = object.variableOccupancy ? "variable" : "whole";
|
|
6866
|
+
const occupancy = mode === "variable" ? bounds(object) : { min: object.seatCount, max: object.seatCount };
|
|
6867
|
+
units.push({
|
|
6868
|
+
objectId: object.id,
|
|
6869
|
+
label: object.label,
|
|
6870
|
+
...object.displayLabel ? { displayLabel: object.displayLabel } : {},
|
|
6871
|
+
...object.displayType ? { displayType: object.displayType } : {},
|
|
6872
|
+
categoryKey: object.categoryKey,
|
|
6873
|
+
mode,
|
|
6874
|
+
capacity: object.seatCount,
|
|
6875
|
+
minOccupancy: occupancy.min,
|
|
6876
|
+
maxOccupancy: occupancy.max,
|
|
6877
|
+
chairs: expandTable(object)
|
|
6878
|
+
});
|
|
6879
|
+
}
|
|
6880
|
+
return units;
|
|
6881
|
+
}
|
|
6882
|
+
|
|
4965
6883
|
// src/picker/PickerController.ts
|
|
4966
6884
|
var DEFAULT_MAX_SELECTION = 10;
|
|
4967
6885
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -4993,6 +6911,14 @@ var PickerController = class {
|
|
|
4993
6911
|
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
4994
6912
|
this.seatContext = /* @__PURE__ */ new Map();
|
|
4995
6913
|
this.allIds = [];
|
|
6914
|
+
/** Model-2 tables are one booking unit whose chairs remain renderer geometry. */
|
|
6915
|
+
this.groupedTablesByObject = /* @__PURE__ */ new Map();
|
|
6916
|
+
this.groupedTablesByLabel = /* @__PURE__ */ new Map();
|
|
6917
|
+
this.groupedTableBySeatId = /* @__PURE__ */ new Map();
|
|
6918
|
+
this.tableQuantities = /* @__PURE__ */ new Map();
|
|
6919
|
+
/** Variable quantity is a buyer contract, never an inferred default. */
|
|
6920
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
6921
|
+
this.groupedSelectionOverhead = 0;
|
|
4996
6922
|
// realtime socket
|
|
4997
6923
|
this.ws = null;
|
|
4998
6924
|
this.reconnectTimer = null;
|
|
@@ -5054,6 +6980,20 @@ var PickerController = class {
|
|
|
5054
6980
|
idForLabel(label) {
|
|
5055
6981
|
return this.labelToId.get(label);
|
|
5056
6982
|
}
|
|
6983
|
+
idsForLabel(label) {
|
|
6984
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
6985
|
+
if (table) return table.chairs.map((chair) => chair.id);
|
|
6986
|
+
const id = this.labelToId.get(label);
|
|
6987
|
+
return id ? [id] : [];
|
|
6988
|
+
}
|
|
6989
|
+
idsForLabels(labels) {
|
|
6990
|
+
return [...new Set([...labels].flatMap((label) => this.idsForLabel(label)))];
|
|
6991
|
+
}
|
|
6992
|
+
/** Resolve a physical chair id (or table inventory label) to its table unit. */
|
|
6993
|
+
tableSelection(seatIdOrLabel) {
|
|
6994
|
+
const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
|
|
6995
|
+
return group ? this.toTableSeat(group) : null;
|
|
6996
|
+
}
|
|
5057
6997
|
/** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
|
|
5058
6998
|
async render(host) {
|
|
5059
6999
|
if (this.renderer) return null;
|
|
@@ -5067,6 +7007,29 @@ var PickerController = class {
|
|
|
5067
7007
|
}
|
|
5068
7008
|
if (this.closed) return null;
|
|
5069
7009
|
this._doc = res.doc;
|
|
7010
|
+
try {
|
|
7011
|
+
const groups = groupedTableInventory(
|
|
7012
|
+
res.doc,
|
|
7013
|
+
res.event.inventoryModelVersion === 2 ? 2 : 1
|
|
7014
|
+
);
|
|
7015
|
+
this.groupedTablesByObject = new Map(groups.map((group) => [group.objectId, group]));
|
|
7016
|
+
this.groupedTablesByLabel = new Map(groups.map((group) => [group.label, group]));
|
|
7017
|
+
this.groupedTableBySeatId = new Map(
|
|
7018
|
+
groups.flatMap((group) => group.chairs.map((chair) => [chair.id, group]))
|
|
7019
|
+
);
|
|
7020
|
+
this.tableQuantities = new Map(groups.map((group) => [
|
|
7021
|
+
group.label,
|
|
7022
|
+
group.mode === "whole" ? group.capacity : group.minOccupancy
|
|
7023
|
+
]));
|
|
7024
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
7025
|
+
this.groupedSelectionOverhead = groups.reduce(
|
|
7026
|
+
(sum, group) => sum + Math.max(0, group.chairs.length - 1),
|
|
7027
|
+
0
|
|
7028
|
+
);
|
|
7029
|
+
} catch (err) {
|
|
7030
|
+
this.emitError(err);
|
|
7031
|
+
return null;
|
|
7032
|
+
}
|
|
5070
7033
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
5071
7034
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
5072
7035
|
this.seatById = /* @__PURE__ */ new Map();
|
|
@@ -5084,30 +7047,43 @@ var PickerController = class {
|
|
|
5084
7047
|
this.allIds.push(s.id);
|
|
5085
7048
|
const source = chartObjects.get(s.rowId);
|
|
5086
7049
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
5087
|
-
const
|
|
5088
|
-
const
|
|
7050
|
+
const logicalRow = source?.type === "row" ? source.segmentedRow : void 0;
|
|
7051
|
+
const sourceDisplayLabel = logicalRow?.displayLabel ?? (source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel);
|
|
7052
|
+
const groupedTable = this.groupedTablesByObject.get(s.rowId);
|
|
7053
|
+
const objectType = source?.type === "table" ? "table" : source?.type === "booth" ? "booth" : "seat";
|
|
7054
|
+
const rowLabel = sourceDisplayLabel;
|
|
7055
|
+
const rowType = logicalRow?.displayType?.trim() || (source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0);
|
|
5089
7056
|
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
5090
7057
|
const labelParts = visibleSeatLabel.split("-");
|
|
5091
|
-
const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) :
|
|
7058
|
+
const seatNumber = groupedTable ? void 0 : s.kind === "booth" ? void 0 : sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
|
|
5092
7059
|
this.seatContext.set(s.id, {
|
|
7060
|
+
objectId: s.rowId,
|
|
7061
|
+
objectType,
|
|
5093
7062
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
5094
7063
|
rowLabel,
|
|
5095
|
-
seatNumber
|
|
7064
|
+
seatNumber,
|
|
7065
|
+
...rowType ? { displayType: rowType, rowType } : {}
|
|
5096
7066
|
});
|
|
5097
7067
|
}
|
|
7068
|
+
for (const group of this.groupedTablesByLabel.values()) {
|
|
7069
|
+
const representative = group.chairs[0];
|
|
7070
|
+
if (!representative) continue;
|
|
7071
|
+
this.labelToId.set(group.label, representative.id);
|
|
7072
|
+
this.labelToSeat.set(group.label, representative);
|
|
7073
|
+
}
|
|
5098
7074
|
const currency = res.event.currency ?? this.opts.currency;
|
|
5099
7075
|
this.currency = currency ?? "USD";
|
|
5100
7076
|
const renderer = createRenderer(host, {
|
|
5101
|
-
|
|
7077
|
+
// Group chairs are selected together for paint, but count as one logical
|
|
7078
|
+
// inventory line. The controller enforces the guest-weighted cap.
|
|
7079
|
+
maxSelection: this.rendererSelectionCap(),
|
|
5102
7080
|
confirmSelection: this.opts.confirmSelection,
|
|
5103
7081
|
currency,
|
|
5104
7082
|
onSelect: (seat) => {
|
|
5105
|
-
this.
|
|
5106
|
-
this.emitSelectionChange();
|
|
7083
|
+
this.handleRendererSelect(seat);
|
|
5107
7084
|
},
|
|
5108
7085
|
onDeselect: (seat) => {
|
|
5109
|
-
this.
|
|
5110
|
-
this.emitSelectionChange();
|
|
7086
|
+
this.handleRendererDeselect(seat);
|
|
5111
7087
|
},
|
|
5112
7088
|
onSelectionLimit: this.opts.onSelectionLimit,
|
|
5113
7089
|
onHover: (seat) => {
|
|
@@ -5162,7 +7138,20 @@ var PickerController = class {
|
|
|
5162
7138
|
// ---- selection ------------------------------------------------------------
|
|
5163
7139
|
getSelection() {
|
|
5164
7140
|
if (!this.renderer) return [];
|
|
5165
|
-
|
|
7141
|
+
const out = [];
|
|
7142
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
7143
|
+
for (const seat of this.renderer.getSelection()) {
|
|
7144
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7145
|
+
if (table) {
|
|
7146
|
+
if (!grouped.has(table.label)) {
|
|
7147
|
+
grouped.add(table.label);
|
|
7148
|
+
out.push(this.toTableSeat(table));
|
|
7149
|
+
}
|
|
7150
|
+
} else {
|
|
7151
|
+
out.push(this.toSeat(seat));
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
return out;
|
|
5166
7155
|
}
|
|
5167
7156
|
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
5168
7157
|
seatDetails(seatId) {
|
|
@@ -5171,20 +7160,168 @@ var PickerController = class {
|
|
|
5171
7160
|
}
|
|
5172
7161
|
clearSelection() {
|
|
5173
7162
|
this.renderer?.clearSelection();
|
|
7163
|
+
this.resetUnheldTableQuantities();
|
|
5174
7164
|
this.emitSelectionChange();
|
|
5175
7165
|
}
|
|
5176
7166
|
deselect(ids) {
|
|
5177
|
-
|
|
7167
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7168
|
+
for (const id of ids) {
|
|
7169
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7170
|
+
if (table) {
|
|
7171
|
+
table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7172
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7173
|
+
this.tableQuantities.set(
|
|
7174
|
+
table.label,
|
|
7175
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7176
|
+
);
|
|
7177
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7178
|
+
}
|
|
7179
|
+
} else expanded.add(id);
|
|
7180
|
+
}
|
|
7181
|
+
this.renderer?.deselect([...expanded]);
|
|
5178
7182
|
this.emitSelectionChange();
|
|
5179
7183
|
}
|
|
5180
7184
|
setMaxSelection(maxSelection) {
|
|
5181
7185
|
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
5182
|
-
this.renderer?.setMaxSelection?.(this.
|
|
7186
|
+
this.renderer?.setMaxSelection?.(this.rendererSelectionCap());
|
|
5183
7187
|
}
|
|
5184
7188
|
select(ids) {
|
|
5185
|
-
const
|
|
7189
|
+
const before = new Set(this.getSelection().map((seat) => seat.label));
|
|
7190
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7191
|
+
for (const id of ids) {
|
|
7192
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7193
|
+
if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7194
|
+
else expanded.add(id);
|
|
7195
|
+
}
|
|
7196
|
+
this.renderer?.select?.([...expanded]);
|
|
7197
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7198
|
+
this.renderer?.deselect([...expanded]);
|
|
7199
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7200
|
+
return [];
|
|
7201
|
+
}
|
|
7202
|
+
const added = this.getSelection().filter((seat) => !before.has(seat.label));
|
|
5186
7203
|
if (added.length) this.emitSelectionChange();
|
|
5187
|
-
return added
|
|
7204
|
+
return added;
|
|
7205
|
+
}
|
|
7206
|
+
/** Confirm/update the guest quantity for a selected variable table. */
|
|
7207
|
+
setTableQuantity(label, quantity) {
|
|
7208
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7209
|
+
if (!table) return false;
|
|
7210
|
+
const q = table.mode === "whole" ? table.capacity : Math.floor(quantity);
|
|
7211
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return false;
|
|
7212
|
+
const previous = this.tableQuantities.get(label) ?? table.minOccupancy;
|
|
7213
|
+
this.tableQuantities.set(label, q);
|
|
7214
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7215
|
+
this.tableQuantities.set(label, previous);
|
|
7216
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7217
|
+
return false;
|
|
7218
|
+
}
|
|
7219
|
+
if (table.mode === "variable") this.confirmedVariableTables.add(label);
|
|
7220
|
+
this.emitSelectionChange();
|
|
7221
|
+
return true;
|
|
7222
|
+
}
|
|
7223
|
+
/** Atomically replace an active variable-table hold with a new guest count. */
|
|
7224
|
+
async replaceTableQuantity(label, quantity) {
|
|
7225
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7226
|
+
const hold = this.hold_;
|
|
7227
|
+
if (!table || table.mode !== "variable" || !hold?.labels.includes(label)) return null;
|
|
7228
|
+
const q = Math.floor(quantity);
|
|
7229
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return null;
|
|
7230
|
+
const otherGuests = (hold.items ?? []).filter((item) => item.label !== label).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
7231
|
+
if (otherGuests + q > this.maxSelection) {
|
|
7232
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7233
|
+
return null;
|
|
7234
|
+
}
|
|
7235
|
+
const selections = (hold.items ?? []).map((item) => ({
|
|
7236
|
+
label: item.label,
|
|
7237
|
+
tierId: item.tierId,
|
|
7238
|
+
quantity: item.label === label ? q : item.quantity
|
|
7239
|
+
}));
|
|
7240
|
+
if (!selections.some((item) => item.label === label)) return null;
|
|
7241
|
+
const result = await this.api.hold(this.key, selections, void 0, hold.holdId);
|
|
7242
|
+
this.tableQuantities.set(label, q);
|
|
7243
|
+
this.setHold({
|
|
7244
|
+
holdId: result.holdId,
|
|
7245
|
+
labels: selections.map((item) => item.label),
|
|
7246
|
+
expiresAt: result.expiresAt,
|
|
7247
|
+
items: result.items
|
|
7248
|
+
});
|
|
7249
|
+
return this.hold_;
|
|
7250
|
+
}
|
|
7251
|
+
rendererSelectionCap() {
|
|
7252
|
+
return this.maxSelection + this.groupedSelectionOverhead;
|
|
7253
|
+
}
|
|
7254
|
+
selectionGuestCount() {
|
|
7255
|
+
return this.getSelection().reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
|
|
7256
|
+
}
|
|
7257
|
+
handleRendererSelect(seat) {
|
|
7258
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7259
|
+
if (table) {
|
|
7260
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7261
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7262
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7263
|
+
this.emitSelectionChange();
|
|
7264
|
+
return;
|
|
7265
|
+
}
|
|
7266
|
+
this.renderer?.select?.(table.chairs.map((chair) => chair.id));
|
|
7267
|
+
this.opts.onSelect?.(table.chairs[0] ?? seat);
|
|
7268
|
+
this.opts.onTableSelectionRequest?.(this.toTableSeat(table));
|
|
7269
|
+
this.emitSelectionChange();
|
|
7270
|
+
return;
|
|
7271
|
+
}
|
|
7272
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7273
|
+
this.renderer?.deselect([seat.id]);
|
|
7274
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7275
|
+
this.emitSelectionChange();
|
|
7276
|
+
return;
|
|
7277
|
+
}
|
|
7278
|
+
this.opts.onSelect?.(seat);
|
|
7279
|
+
this.emitSelectionChange();
|
|
7280
|
+
}
|
|
7281
|
+
handleRendererDeselect(seat) {
|
|
7282
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7283
|
+
if (table) {
|
|
7284
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7285
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7286
|
+
this.tableQuantities.set(
|
|
7287
|
+
table.label,
|
|
7288
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7289
|
+
);
|
|
7290
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7291
|
+
}
|
|
7292
|
+
this.opts.onDeselect?.(table.chairs[0] ?? seat);
|
|
7293
|
+
} else {
|
|
7294
|
+
this.opts.onDeselect?.(seat);
|
|
7295
|
+
}
|
|
7296
|
+
this.emitSelectionChange();
|
|
7297
|
+
}
|
|
7298
|
+
resetUnheldTableQuantities() {
|
|
7299
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7300
|
+
if (this.hold_?.labels.includes(table.label)) continue;
|
|
7301
|
+
this.tableQuantities.set(
|
|
7302
|
+
table.label,
|
|
7303
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7304
|
+
);
|
|
7305
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
resetTableQuantitiesForLabels(labels) {
|
|
7309
|
+
for (const label of labels) {
|
|
7310
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7311
|
+
if (!table) continue;
|
|
7312
|
+
this.tableQuantities.set(
|
|
7313
|
+
label,
|
|
7314
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7315
|
+
);
|
|
7316
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(label);
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
tableQuantityRequest(seat) {
|
|
7320
|
+
if (seat?.objectType !== "table") return {};
|
|
7321
|
+
if (seat.bookingMode === "variable" && !this.confirmedVariableTables.has(seat.label)) {
|
|
7322
|
+
throw new Error("picker: confirm a variable table guest quantity before holding");
|
|
7323
|
+
}
|
|
7324
|
+
return { quantity: seat.quantity };
|
|
5188
7325
|
}
|
|
5189
7326
|
// ---- booking machine ------------------------------------------------------
|
|
5190
7327
|
/**
|
|
@@ -5196,7 +7333,7 @@ var PickerController = class {
|
|
|
5196
7333
|
async hold(labelsArg, ttlMs) {
|
|
5197
7334
|
const r = this.renderer;
|
|
5198
7335
|
if (!r) return null;
|
|
5199
|
-
const labels = labelsArg ??
|
|
7336
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5200
7337
|
const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
|
|
5201
7338
|
const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
|
|
5202
7339
|
if (!combinedLabels.length) return null;
|
|
@@ -5204,10 +7341,14 @@ var PickerController = class {
|
|
|
5204
7341
|
try {
|
|
5205
7342
|
const selections = combinedLabels.map((label) => {
|
|
5206
7343
|
const ga = existingGA.find((item) => item.label === label);
|
|
5207
|
-
if (ga) return { label, tierId: ga.tierId };
|
|
7344
|
+
if (ga) return { label, tierId: ga.tierId, quantity: ga.quantity };
|
|
5208
7345
|
const seat = this.labelToSeat.get(label);
|
|
5209
7346
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5210
|
-
return {
|
|
7347
|
+
return {
|
|
7348
|
+
label,
|
|
7349
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7350
|
+
...this.tableQuantityRequest(resolved)
|
|
7351
|
+
};
|
|
5211
7352
|
});
|
|
5212
7353
|
const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
|
|
5213
7354
|
this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
|
|
@@ -5259,11 +7400,19 @@ var PickerController = class {
|
|
|
5259
7400
|
const out = {};
|
|
5260
7401
|
const closedMembers = this.closedMemberIds();
|
|
5261
7402
|
for (const [id, s] of this.seatById) {
|
|
7403
|
+
if (this.groupedTableBySeatId.has(id)) continue;
|
|
5262
7404
|
if (closedMembers.has(id)) continue;
|
|
5263
7405
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
5264
7406
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
5265
7407
|
}
|
|
5266
7408
|
}
|
|
7409
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7410
|
+
if (table.chairs.some((chair) => closedMembers.has(chair.id))) continue;
|
|
7411
|
+
const representative = table.chairs[0];
|
|
7412
|
+
if (representative && (this.getStatus(representative.id) ?? "free") === "free") {
|
|
7413
|
+
out[table.categoryKey] = (out[table.categoryKey] ?? 0) + table.maxOccupancy;
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
5267
7416
|
return out;
|
|
5268
7417
|
}
|
|
5269
7418
|
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
@@ -5282,6 +7431,8 @@ var PickerController = class {
|
|
|
5282
7431
|
return {
|
|
5283
7432
|
id: area.id,
|
|
5284
7433
|
label: area.label,
|
|
7434
|
+
...area.displayLabel ? { displayLabel: area.displayLabel } : {},
|
|
7435
|
+
...area.displayType ? { displayType: area.displayType } : {},
|
|
5285
7436
|
capacity: Math.max(0, Math.floor(area.capacity)),
|
|
5286
7437
|
available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
|
|
5287
7438
|
categoryKey: area.categoryKey,
|
|
@@ -5300,14 +7451,20 @@ var PickerController = class {
|
|
|
5300
7451
|
const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
|
|
5301
7452
|
if (labels.length !== Math.floor(qty)) return null;
|
|
5302
7453
|
const selections = /* @__PURE__ */ new Map();
|
|
5303
|
-
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7454
|
+
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7455
|
+
label: item.label,
|
|
7456
|
+
tierId: item.tierId,
|
|
7457
|
+
quantity: item.quantity
|
|
7458
|
+
});
|
|
5304
7459
|
if (this.hold_ && !this.hold_?.items?.length) {
|
|
5305
7460
|
for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
|
|
5306
7461
|
}
|
|
5307
|
-
for (const seat of this.
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
7462
|
+
for (const seat of this.getSelection()) {
|
|
7463
|
+
selections.set(seat.label, {
|
|
7464
|
+
label: seat.label,
|
|
7465
|
+
...seat.tierId ? { tierId: seat.tierId } : {},
|
|
7466
|
+
...this.tableQuantityRequest(seat)
|
|
7467
|
+
});
|
|
5311
7468
|
}
|
|
5312
7469
|
for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
|
|
5313
7470
|
const combined = [...selections.values()];
|
|
@@ -5324,6 +7481,15 @@ var PickerController = class {
|
|
|
5324
7481
|
}
|
|
5325
7482
|
return false;
|
|
5326
7483
|
}
|
|
7484
|
+
/** Zones which still own visible buyer inventory, in authored order. */
|
|
7485
|
+
getBestAvailableZones() {
|
|
7486
|
+
const doc = this.visibleDoc();
|
|
7487
|
+
if (!doc?.zones?.length) return [];
|
|
7488
|
+
const used = new Set(
|
|
7489
|
+
computeSections(doc).sections.filter((section) => section.seatCount > 0 && !!section.zone).map((section) => section.zone)
|
|
7490
|
+
);
|
|
7491
|
+
return doc.zones.filter((zone) => used.has(zone.id)).map((zone) => ({ id: zone.id, label: zone.label }));
|
|
7492
|
+
}
|
|
5327
7493
|
/**
|
|
5328
7494
|
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5329
7495
|
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
@@ -5332,11 +7498,12 @@ var PickerController = class {
|
|
|
5332
7498
|
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5333
7499
|
* exists (the caller then falls back to the normal server pick).
|
|
5334
7500
|
*/
|
|
5335
|
-
pickPremiumBlock(qty, categoryKey) {
|
|
7501
|
+
pickPremiumBlock(qty, categoryKey, zoneId) {
|
|
5336
7502
|
const doc = this.visibleDoc();
|
|
5337
7503
|
if (!doc?.objects?.length) return null;
|
|
5338
7504
|
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5339
|
-
const
|
|
7505
|
+
const groupedObjectIds = new Set(this.groupedTablesByObject.keys());
|
|
7506
|
+
const seats = expandChart(doc).filter((seat) => !groupedObjectIds.has(seat.rowId));
|
|
5340
7507
|
const held = new Set(this.hold_?.labels ?? []);
|
|
5341
7508
|
const available = /* @__PURE__ */ new Set();
|
|
5342
7509
|
for (const seat of seats) {
|
|
@@ -5345,7 +7512,7 @@ var PickerController = class {
|
|
|
5345
7512
|
const status = id ? this.getStatus(id) : void 0;
|
|
5346
7513
|
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5347
7514
|
}
|
|
5348
|
-
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
7515
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, zoneId, focal, preferPremium: true });
|
|
5349
7516
|
if (pick.labels.length !== qty) return null;
|
|
5350
7517
|
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5351
7518
|
return allPremium ? [...pick.labels] : null;
|
|
@@ -5360,7 +7527,7 @@ var PickerController = class {
|
|
|
5360
7527
|
const r = this.renderer;
|
|
5361
7528
|
if (!r) return null;
|
|
5362
7529
|
if (opts.preferPremium) {
|
|
5363
|
-
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
7530
|
+
const block = this.pickPremiumBlock(qty, categoryKey, opts.zoneId);
|
|
5364
7531
|
if (block) {
|
|
5365
7532
|
if (this.hold_ && !await this.release()) return null;
|
|
5366
7533
|
try {
|
|
@@ -5371,7 +7538,7 @@ var PickerController = class {
|
|
|
5371
7538
|
});
|
|
5372
7539
|
const result = await this.api.hold(this.key, selection);
|
|
5373
7540
|
r.clearSelection();
|
|
5374
|
-
const ids =
|
|
7541
|
+
const ids = this.idsForLabels(block);
|
|
5375
7542
|
if (ids.length) r.setStatus(ids, "held");
|
|
5376
7543
|
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5377
7544
|
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5388,9 +7555,9 @@ var PickerController = class {
|
|
|
5388
7555
|
}
|
|
5389
7556
|
if (this.hold_ && !await this.release()) return null;
|
|
5390
7557
|
try {
|
|
5391
|
-
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
7558
|
+
const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
|
|
5392
7559
|
r.clearSelection();
|
|
5393
|
-
const ids =
|
|
7560
|
+
const ids = this.idsForLabels(result.labels);
|
|
5394
7561
|
if (ids.length) r.setStatus(ids, "held");
|
|
5395
7562
|
this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
|
|
5396
7563
|
const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5414,7 +7581,7 @@ var PickerController = class {
|
|
|
5414
7581
|
const r = this.renderer;
|
|
5415
7582
|
if (!r) return null;
|
|
5416
7583
|
if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
|
|
5417
|
-
const labels = labelsArg ??
|
|
7584
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5418
7585
|
if (!labels.length) return null;
|
|
5419
7586
|
let holdId;
|
|
5420
7587
|
try {
|
|
@@ -5427,7 +7594,11 @@ var PickerController = class {
|
|
|
5427
7594
|
labels.map((label) => {
|
|
5428
7595
|
const seat = this.labelToSeat.get(label);
|
|
5429
7596
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5430
|
-
return {
|
|
7597
|
+
return {
|
|
7598
|
+
label,
|
|
7599
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7600
|
+
...this.tableQuantityRequest(resolved)
|
|
7601
|
+
};
|
|
5431
7602
|
}),
|
|
5432
7603
|
void 0,
|
|
5433
7604
|
replaceHoldId
|
|
@@ -5443,10 +7614,11 @@ var PickerController = class {
|
|
|
5443
7614
|
this.opts.onSalesClosed?.();
|
|
5444
7615
|
} else if (status === 409 && conflicts?.length) {
|
|
5445
7616
|
const takenLabels = new Set(conflicts.map((c) => c.label));
|
|
5446
|
-
const takenIds =
|
|
7617
|
+
const takenIds = this.idsForLabels(takenLabels);
|
|
5447
7618
|
if (takenIds.length) {
|
|
5448
7619
|
r.setStatus(takenIds, "booked");
|
|
5449
7620
|
r.deselect(takenIds);
|
|
7621
|
+
this.resetTableQuantitiesForLabels(takenLabels);
|
|
5450
7622
|
}
|
|
5451
7623
|
const stillFree = labels.filter((l) => !takenLabels.has(l));
|
|
5452
7624
|
if (stillFree.length && holdId) void this.api.release(this.key, stillFree, holdId).catch(() => {
|
|
@@ -5458,12 +7630,13 @@ var PickerController = class {
|
|
|
5458
7630
|
}
|
|
5459
7631
|
throw err;
|
|
5460
7632
|
}
|
|
5461
|
-
const ids =
|
|
7633
|
+
const ids = this.idsForLabels(labels);
|
|
5462
7634
|
if (ids.length) {
|
|
5463
7635
|
r.setStatus(ids, "booked");
|
|
5464
7636
|
r.deselect(ids);
|
|
5465
7637
|
}
|
|
5466
7638
|
this.clearHold();
|
|
7639
|
+
this.resetTableQuantitiesForLabels(labels);
|
|
5467
7640
|
this.emitSelectionChange();
|
|
5468
7641
|
this.opts.onBook?.(bookingRef);
|
|
5469
7642
|
return labels;
|
|
@@ -5484,7 +7657,8 @@ var PickerController = class {
|
|
|
5484
7657
|
}
|
|
5485
7658
|
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
5486
7659
|
this.clearHold();
|
|
5487
|
-
|
|
7660
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
7661
|
+
const ids = this.idsForLabels(hold.labels);
|
|
5488
7662
|
if (ids.length) {
|
|
5489
7663
|
this.renderer?.deselect(ids);
|
|
5490
7664
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5515,7 +7689,8 @@ var PickerController = class {
|
|
|
5515
7689
|
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
5516
7690
|
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
5517
7691
|
else this.clearHold();
|
|
5518
|
-
|
|
7692
|
+
this.resetTableQuantitiesForLabels(drop);
|
|
7693
|
+
const ids = this.idsForLabels(drop);
|
|
5519
7694
|
if (ids.length) {
|
|
5520
7695
|
this.renderer?.deselect(ids);
|
|
5521
7696
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5604,7 +7779,7 @@ var PickerController = class {
|
|
|
5604
7779
|
this.opts.onDeckTap?.(floorId);
|
|
5605
7780
|
}
|
|
5606
7781
|
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
5607
|
-
/** Switch the map projection
|
|
7782
|
+
/** Switch the map projection. No-op on a flat-only renderer. */
|
|
5608
7783
|
setViewMode(mode) {
|
|
5609
7784
|
this.renderer?.setViewMode?.(mode);
|
|
5610
7785
|
}
|
|
@@ -5703,12 +7878,21 @@ var PickerController = class {
|
|
|
5703
7878
|
if (!sec) return null;
|
|
5704
7879
|
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
5705
7880
|
const byCat = /* @__PURE__ */ new Map();
|
|
7881
|
+
const seenTables = /* @__PURE__ */ new Set();
|
|
5706
7882
|
let seatsLeft = 0;
|
|
5707
7883
|
for (const sid of memberIds) {
|
|
5708
7884
|
const seat = this.seatById.get(sid);
|
|
5709
7885
|
if (!seat) continue;
|
|
5710
|
-
|
|
5711
|
-
if (
|
|
7886
|
+
const table = this.groupedTableBySeatId.get(sid);
|
|
7887
|
+
if (table) {
|
|
7888
|
+
if (seenTables.has(table.label)) continue;
|
|
7889
|
+
seenTables.add(table.label);
|
|
7890
|
+
byCat.set(table.categoryKey, (byCat.get(table.categoryKey) ?? 0) + table.maxOccupancy);
|
|
7891
|
+
if (r.getStatus(table.chairs[0]?.id ?? sid) === "free") seatsLeft += table.maxOccupancy;
|
|
7892
|
+
} else {
|
|
7893
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
7894
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
7895
|
+
}
|
|
5712
7896
|
}
|
|
5713
7897
|
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
5714
7898
|
const cat = doc.categories.find((c) => c.key === key);
|
|
@@ -5727,6 +7911,7 @@ var PickerController = class {
|
|
|
5727
7911
|
id,
|
|
5728
7912
|
label: sec.label,
|
|
5729
7913
|
zoneLabel: zone?.label ?? "",
|
|
7914
|
+
...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
|
|
5730
7915
|
color,
|
|
5731
7916
|
seatsLeft,
|
|
5732
7917
|
priceMin: prices.length ? prices[0] : 0,
|
|
@@ -5757,22 +7942,54 @@ var PickerController = class {
|
|
|
5757
7942
|
}
|
|
5758
7943
|
// ---- internals ------------------------------------------------------------
|
|
5759
7944
|
toSeat(s) {
|
|
7945
|
+
const table = this.groupedTableBySeatId.get(s.id);
|
|
7946
|
+
if (table) return this.toTableSeat(table);
|
|
5760
7947
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
5761
7948
|
const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
|
|
7949
|
+
const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
|
|
7950
|
+
const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
|
|
7951
|
+
const context = this.seatContext.get(s.id);
|
|
5762
7952
|
const tiers = this.tiersFor(s.categoryKey);
|
|
5763
7953
|
if (!tiers) {
|
|
5764
|
-
return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
7954
|
+
return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
|
|
5765
7955
|
}
|
|
5766
7956
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
5767
7957
|
return {
|
|
5768
7958
|
id: s.id,
|
|
5769
7959
|
label: s.label,
|
|
5770
7960
|
...display,
|
|
7961
|
+
...context,
|
|
5771
7962
|
categoryKey: s.categoryKey,
|
|
5772
7963
|
price: chosen.price,
|
|
5773
7964
|
tiers,
|
|
5774
7965
|
tierId: chosen.id,
|
|
5775
|
-
...commercial
|
|
7966
|
+
...commercial,
|
|
7967
|
+
...accessibility,
|
|
7968
|
+
...wheelchair
|
|
7969
|
+
};
|
|
7970
|
+
}
|
|
7971
|
+
toTableSeat(table) {
|
|
7972
|
+
const representative = table.chairs[0];
|
|
7973
|
+
const tiers = this.tiersFor(table.categoryKey);
|
|
7974
|
+
const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
|
|
7975
|
+
return {
|
|
7976
|
+
id: representative?.id ?? table.objectId,
|
|
7977
|
+
label: table.label,
|
|
7978
|
+
displayLabel: table.displayLabel ?? table.label,
|
|
7979
|
+
...table.displayType ? { displayType: table.displayType, rowType: table.displayType } : {},
|
|
7980
|
+
objectId: table.objectId,
|
|
7981
|
+
sectionLabel: representative ? this.seatContext.get(representative.id)?.sectionLabel : void 0,
|
|
7982
|
+
rowLabel: table.displayLabel ?? table.label,
|
|
7983
|
+
categoryKey: table.categoryKey,
|
|
7984
|
+
price: chosen?.price ?? this.priceFor(table.categoryKey),
|
|
7985
|
+
...tiers ? { tiers, tierId: chosen?.id } : {},
|
|
7986
|
+
objectType: "table",
|
|
7987
|
+
bookingMode: table.mode,
|
|
7988
|
+
quantity: this.tableQuantities.get(table.label) ?? (table.mode === "whole" ? table.capacity : table.minOccupancy),
|
|
7989
|
+
capacity: table.capacity,
|
|
7990
|
+
minOccupancy: table.minOccupancy,
|
|
7991
|
+
maxOccupancy: table.maxOccupancy,
|
|
7992
|
+
physicalSeatIds: table.chairs.map((chair) => chair.id)
|
|
5776
7993
|
};
|
|
5777
7994
|
}
|
|
5778
7995
|
/** Tooltip payload for a hovered seat — see PickerCallbacks.onSeatHover. */
|
|
@@ -5856,10 +8073,13 @@ var PickerController = class {
|
|
|
5856
8073
|
const h = this.hold_;
|
|
5857
8074
|
if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
|
|
5858
8075
|
const tiers = new Map((h.items ?? []).map((item) => [item.label, item.tierId]));
|
|
8076
|
+
const quantities = new Map((h.items ?? []).map((item) => [item.label, item.quantity ?? 1]));
|
|
5859
8077
|
return labels.every((label) => {
|
|
5860
8078
|
const seat = this.labelToSeat.get(label);
|
|
5861
|
-
const
|
|
5862
|
-
|
|
8079
|
+
const resolved = seat ? this.toSeat(seat) : null;
|
|
8080
|
+
const currentTier = resolved?.tierId ?? null;
|
|
8081
|
+
const quantityMatches = resolved?.objectType !== "table" || (resolved.bookingMode !== "variable" || this.confirmedVariableTables.has(label)) && (!quantities.has(label) || quantities.get(label) === resolved.quantity);
|
|
8082
|
+
return quantityMatches && (!tiers.has(label) || tiers.get(label) === currentTier);
|
|
5863
8083
|
});
|
|
5864
8084
|
}
|
|
5865
8085
|
handle409Conflicts(err) {
|
|
@@ -5867,19 +8087,28 @@ var PickerController = class {
|
|
|
5867
8087
|
if (status !== 409 || !conflicts?.length) return;
|
|
5868
8088
|
const r = this.renderer;
|
|
5869
8089
|
if (!r) return;
|
|
5870
|
-
const takenIds = conflicts.map((
|
|
8090
|
+
const takenIds = this.idsForLabels(conflicts.map((conflict) => conflict.label));
|
|
5871
8091
|
if (takenIds.length) {
|
|
5872
8092
|
r.deselect(takenIds);
|
|
5873
8093
|
r.setStatus(takenIds, "held");
|
|
8094
|
+
this.resetTableQuantitiesForLabels(conflicts.map((conflict) => conflict.label));
|
|
5874
8095
|
}
|
|
5875
8096
|
this.emitSelectionChange();
|
|
5876
8097
|
}
|
|
5877
8098
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
5878
8099
|
setHold(hold, source = "created") {
|
|
8100
|
+
for (const item of hold.items ?? []) {
|
|
8101
|
+
if (this.groupedTablesByLabel.has(item.label) && item.quantity != null) {
|
|
8102
|
+
this.tableQuantities.set(item.label, item.quantity);
|
|
8103
|
+
if (this.groupedTablesByLabel.get(item.label)?.mode === "variable") {
|
|
8104
|
+
this.confirmedVariableTables.add(item.label);
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
5879
8108
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
5880
8109
|
this.hold_ = full;
|
|
5881
8110
|
this.renderer?.setOwnedHold?.(
|
|
5882
|
-
|
|
8111
|
+
this.idsForLabels(full.labels)
|
|
5883
8112
|
);
|
|
5884
8113
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
5885
8114
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
@@ -5922,12 +8151,11 @@ var PickerController = class {
|
|
|
5922
8151
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
5923
8152
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
5924
8153
|
r.setOwnedHold?.(
|
|
5925
|
-
(this.hold_?.labels ?? [])
|
|
8154
|
+
this.idsForLabels(this.hold_?.labels ?? [])
|
|
5926
8155
|
);
|
|
5927
8156
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
5928
8157
|
for (const [label, st] of Object.entries(seats)) {
|
|
5929
|
-
|
|
5930
|
-
if (id) byStatus[mapStatus(st)].push(id);
|
|
8158
|
+
byStatus[mapStatus(st)].push(...this.idsForLabel(label));
|
|
5931
8159
|
}
|
|
5932
8160
|
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
5933
8161
|
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
@@ -5936,7 +8164,11 @@ var PickerController = class {
|
|
|
5936
8164
|
this.clearBookedHoldIfSettled();
|
|
5937
8165
|
}
|
|
5938
8166
|
clearBookedHoldIfSettled() {
|
|
5939
|
-
|
|
8167
|
+
const hold = this.hold_;
|
|
8168
|
+
if (hold?.labels.every((label) => this.liveStatuses.get(label) === "booked")) {
|
|
8169
|
+
this.clearHold();
|
|
8170
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
8171
|
+
}
|
|
5940
8172
|
}
|
|
5941
8173
|
async resnapshot() {
|
|
5942
8174
|
try {
|
|
@@ -6025,13 +8257,13 @@ var PickerController = class {
|
|
|
6025
8257
|
} else if (Array.isArray(m.changes)) {
|
|
6026
8258
|
for (const ch of m.changes) {
|
|
6027
8259
|
this.liveStatuses.set(ch.label, ch.status);
|
|
6028
|
-
const
|
|
6029
|
-
if (!
|
|
8260
|
+
const ids = this.idsForLabel(ch.label);
|
|
8261
|
+
if (!ids.length) continue;
|
|
6030
8262
|
const next = mapStatus(ch.status);
|
|
6031
|
-
if (this.opts.flashOnLiveChange && next !== "free" && r.getStatus(id) === "free" && !this.hold_?.labels.includes(ch.label)) {
|
|
6032
|
-
r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e");
|
|
8263
|
+
if (this.opts.flashOnLiveChange && next !== "free" && ids.some((id) => r.getStatus(id) === "free") && !this.hold_?.labels.includes(ch.label)) {
|
|
8264
|
+
ids.forEach((id) => r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e"));
|
|
6033
8265
|
}
|
|
6034
|
-
r.setStatus(
|
|
8266
|
+
r.setStatus(ids, next);
|
|
6035
8267
|
}
|
|
6036
8268
|
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
6037
8269
|
r.forceDraw();
|
|
@@ -6062,10 +8294,25 @@ var PickerController = class {
|
|
|
6062
8294
|
}
|
|
6063
8295
|
};
|
|
6064
8296
|
|
|
8297
|
+
// src/view/sightline.ts
|
|
8298
|
+
var STAGE_TOP_M = 5;
|
|
8299
|
+
var STAGE_BASE_M = -1.1;
|
|
8300
|
+
var MAX_LOOKDOWN_DEG = 35;
|
|
8301
|
+
function stageSightlinePitch(eyeHeightM, distM) {
|
|
8302
|
+
const extraEyeM = eyeHeightM - SEATED_EYE_HEIGHT_M;
|
|
8303
|
+
let topPitch = Math.atan2(STAGE_TOP_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8304
|
+
let basePitch = Math.atan2(STAGE_BASE_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8305
|
+
if (basePitch < -MAX_LOOKDOWN_DEG) {
|
|
8306
|
+
topPitch += -MAX_LOOKDOWN_DEG - basePitch;
|
|
8307
|
+
basePitch = -MAX_LOOKDOWN_DEG;
|
|
8308
|
+
}
|
|
8309
|
+
return { topPitch, basePitch };
|
|
8310
|
+
}
|
|
8311
|
+
|
|
6065
8312
|
// src/view/generatePanorama.ts
|
|
6066
8313
|
var W = 2048;
|
|
6067
8314
|
var H = 1024;
|
|
6068
|
-
var UNIT =
|
|
8315
|
+
var UNIT = METRES_PER_CHART_UNIT;
|
|
6069
8316
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
6070
8317
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
6071
8318
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
@@ -6085,9 +8332,11 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
6085
8332
|
sky.addColorStop(1, "#07090f");
|
|
6086
8333
|
ctx.fillStyle = sky;
|
|
6087
8334
|
ctx.fillRect(0, 0, W, H);
|
|
8335
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
6088
8336
|
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6089
|
-
const
|
|
6090
|
-
const
|
|
8337
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8338
|
+
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8339
|
+
const stageBasePitch = sightline.basePitch;
|
|
6091
8340
|
const sx0 = yawToX(-stageHalfYaw);
|
|
6092
8341
|
const sx1 = yawToX(stageHalfYaw);
|
|
6093
8342
|
const sy0 = pitchToY(stageTopPitch);
|
|
@@ -6194,9 +8443,11 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6194
8443
|
sky.addColorStop(1, "#070910");
|
|
6195
8444
|
ctx.fillStyle = sky;
|
|
6196
8445
|
ctx.fillRect(0, 0, TW, TH);
|
|
8446
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8447
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
6197
8448
|
const stageHalfYaw = Math.min(HALF_HFOV - 2, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6198
|
-
const stageTopPitch = Math.min(HALF_VFOV - 2,
|
|
6199
|
-
const stageBasePitch = Math.max(-HALF_VFOV + 2,
|
|
8449
|
+
const stageTopPitch = Math.min(HALF_VFOV - 2, sightline.topPitch);
|
|
8450
|
+
const stageBasePitch = Math.max(-HALF_VFOV + 2, sightline.basePitch);
|
|
6200
8451
|
const sx0 = yawToTX(-stageHalfYaw);
|
|
6201
8452
|
const sx1 = yawToTX(stageHalfYaw);
|
|
6202
8453
|
const sy0 = pitchToTY(stageTopPitch);
|
|
@@ -6292,8 +8543,11 @@ async function loadLocale(code) {
|
|
|
6292
8543
|
MAX_EVENT_INVENTORY,
|
|
6293
8544
|
MAX_GA_CAPACITY,
|
|
6294
8545
|
PickerController,
|
|
8546
|
+
RENDERED_QUALITY_REPORT_VERSION,
|
|
6295
8547
|
SUPPORTED_LOCALES,
|
|
8548
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
6296
8549
|
SeatmapRenderer,
|
|
8550
|
+
TIER_HEIGHT_M,
|
|
6297
8551
|
UNGROUPED_ID,
|
|
6298
8552
|
accessibilityMeta,
|
|
6299
8553
|
accessibilityRingColor,
|
|
@@ -6308,33 +8562,43 @@ async function loadLocale(code) {
|
|
|
6308
8562
|
expandRow,
|
|
6309
8563
|
expandRowSlots,
|
|
6310
8564
|
expandTable,
|
|
8565
|
+
expandTableSlots,
|
|
6311
8566
|
floorObjects,
|
|
6312
8567
|
floorsOf,
|
|
6313
8568
|
formatDate,
|
|
6314
8569
|
formatMoney,
|
|
6315
8570
|
gaAreasOf,
|
|
8571
|
+
gaInventorySegments,
|
|
6316
8572
|
gaUnitLabel,
|
|
6317
8573
|
gaUnitLabels,
|
|
6318
8574
|
generateSeatPanorama,
|
|
6319
8575
|
generateSeatThumb,
|
|
6320
8576
|
getLocale,
|
|
8577
|
+
growJoinedGAInventory,
|
|
6321
8578
|
hiddenObjectIds,
|
|
8579
|
+
inspectRenderedQualityEvidence,
|
|
6322
8580
|
isGaUnitLabel,
|
|
6323
8581
|
isSectionHidden,
|
|
6324
8582
|
layerOf,
|
|
6325
8583
|
loadLocale,
|
|
6326
8584
|
objectCenter,
|
|
8585
|
+
owningSectionForObject,
|
|
6327
8586
|
pointInPolygon,
|
|
6328
8587
|
pointInPolygonWithHoles,
|
|
6329
8588
|
polygonLabelPoint,
|
|
6330
8589
|
resolveLocale,
|
|
8590
|
+
rowInventoryCount,
|
|
6331
8591
|
rowSeatPositions,
|
|
6332
8592
|
seatLabelPart,
|
|
8593
|
+
sectionGeometry,
|
|
6333
8594
|
setLocale,
|
|
6334
8595
|
setMoneyLocale,
|
|
6335
8596
|
setStringOverrides,
|
|
6336
8597
|
stackFloors,
|
|
6337
8598
|
t,
|
|
6338
|
-
tCount
|
|
8599
|
+
tCount,
|
|
8600
|
+
tableInventoryCount,
|
|
8601
|
+
tableSeatCountsBySide,
|
|
8602
|
+
validGAInventorySegments
|
|
6339
8603
|
});
|
|
6340
8604
|
//# sourceMappingURL=index.cjs.map
|