@seatlayer/core 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{de-73TIXYJH.js → de-UHAOVDS3.js} +3 -2
- package/dist/de-UHAOVDS3.js.map +1 -0
- package/dist/{es-SKAODLTR.js → es-6TJS2L7S.js} +3 -2
- package/dist/es-6TJS2L7S.js.map +1 -0
- package/dist/{fr-J4637T6V.js → fr-MMSY7FPE.js} +3 -2
- package/dist/fr-MMSY7FPE.js.map +1 -0
- package/dist/index.cjs +2497 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +700 -56
- package/dist/index.d.ts +700 -56
- package/dist/index.js +2482 -334
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-73TIXYJH.js.map +0 -1
- package/dist/es-SKAODLTR.js.map +0 -1
- package/dist/fr-J4637T6V.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var ACCESSIBILITY_TYPES = [
|
|
|
4
4
|
{ key: "companion", label: "Companion seat", short: "Companion", icon: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" },
|
|
5
5
|
{ key: "semi-ambulatory", label: "Semi-ambulatory (limited mobility)", short: "Limited mobility", icon: "\u{1F9AF}" },
|
|
6
6
|
{ key: "hearing", label: "Assistive listening", short: "Hearing", icon: "\u{1F9BB}" },
|
|
7
|
+
{ key: "cart", label: "CART live-caption view", short: "CART captions", icon: "CC" },
|
|
7
8
|
{ key: "sign-language", label: "Sign-language view", short: "Sign language", icon: "\u{1F91F}" },
|
|
8
9
|
{ key: "plus-size", label: "Plus-size seat", short: "Plus-size", icon: "\u{1F4BA}" },
|
|
9
10
|
{ key: "lift-armrest", label: "Lift-up armrest", short: "Lift armrest", icon: "\u2195\uFE0F" }
|
|
@@ -17,6 +18,7 @@ var ACCESSIBILITY_RING_COLOR = {
|
|
|
17
18
|
companion: "#8b5cf6",
|
|
18
19
|
"semi-ambulatory": "#0ea5e9",
|
|
19
20
|
hearing: "#14b8a6",
|
|
21
|
+
cart: "#7c3aed",
|
|
20
22
|
"sign-language": "#f59e0b",
|
|
21
23
|
"plus-size": "#ec4899",
|
|
22
24
|
"lift-armrest": "#22c55e"
|
|
@@ -27,6 +29,19 @@ function accessibilityRingColor(types) {
|
|
|
27
29
|
}
|
|
28
30
|
var LABEL_STYLE_MIN_SIZE = 8;
|
|
29
31
|
var LABEL_STYLE_MAX_SIZE = 24;
|
|
32
|
+
var SURROUNDINGS_SHAPE_ROLES = [
|
|
33
|
+
"reference-focal",
|
|
34
|
+
"bar",
|
|
35
|
+
"entrance",
|
|
36
|
+
"exit",
|
|
37
|
+
"restroom",
|
|
38
|
+
"screen",
|
|
39
|
+
"sound",
|
|
40
|
+
"concession",
|
|
41
|
+
"coat",
|
|
42
|
+
"wall"
|
|
43
|
+
];
|
|
44
|
+
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
30
45
|
function layerOf(obj) {
|
|
31
46
|
switch (obj.type) {
|
|
32
47
|
case "row":
|
|
@@ -35,10 +50,17 @@ function layerOf(obj) {
|
|
|
35
50
|
case "booth":
|
|
36
51
|
case "section":
|
|
37
52
|
return "interactive";
|
|
38
|
-
//
|
|
53
|
+
// Raster décor follows its explicit authored z-layer. Absence retains the
|
|
54
|
+
// legacy Background default; bitmap content is never guessed semantically.
|
|
55
|
+
case "decorImage":
|
|
56
|
+
return obj.layer === "foreground" ? "foreground" : "background";
|
|
57
|
+
// Source-backed focal geometry and curated venue landmarks help an author
|
|
58
|
+
// orient around the sellable plan. A stage and an ordinary authored shape
|
|
59
|
+
// remain Background even when they carry an arbitrary/custom role.
|
|
39
60
|
case "shape":
|
|
61
|
+
return obj.role && SURROUNDINGS_SHAPE_ROLE_SET.has(obj.role) ? "surroundings" : "background";
|
|
62
|
+
// Free text — including semantic icon text — is background furniture.
|
|
40
63
|
case "text":
|
|
41
|
-
case "decorImage":
|
|
42
64
|
return "background";
|
|
43
65
|
default:
|
|
44
66
|
return "interactive";
|
|
@@ -158,10 +180,57 @@ function toRoman(value) {
|
|
|
158
180
|
return out;
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
// src/core/units.ts
|
|
184
|
+
var METRES_PER_CHART_UNIT = 0.55 / 24;
|
|
185
|
+
var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
|
|
186
|
+
var LIFT_PER_STEP_WORLD = 58;
|
|
187
|
+
var TIER_HEIGHT_M = LIFT_PER_STEP_WORLD * METRES_PER_CHART_UNIT;
|
|
188
|
+
var SECTION_ELEVATION_TIER_MIN = 0;
|
|
189
|
+
var SECTION_ELEVATION_TIER_MAX = 3;
|
|
190
|
+
var SECTION_HEIGHT_MIN_M = 0;
|
|
191
|
+
var SECTION_HEIGHT_MAX_M = 120;
|
|
192
|
+
var SECTION_RAKE_MIN_DEG = 0;
|
|
193
|
+
var SECTION_RAKE_MAX_DEG = 45;
|
|
194
|
+
var LEGACY_SECTION_ELEVATION_TIER_MAX = 7;
|
|
195
|
+
var SEATED_EYE_HEIGHT_M = 1.2;
|
|
196
|
+
function finiteClamped(value, min, max, fallback) {
|
|
197
|
+
return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
198
|
+
}
|
|
199
|
+
function sectionElevationTier(value) {
|
|
200
|
+
if (!Number.isFinite(value)) return SECTION_ELEVATION_TIER_MIN;
|
|
201
|
+
return Math.max(
|
|
202
|
+
SECTION_ELEVATION_TIER_MIN,
|
|
203
|
+
Math.min(SECTION_ELEVATION_TIER_MAX, Math.round(value))
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
function compatibleAutomaticTier(value) {
|
|
207
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) return 0;
|
|
208
|
+
if (value <= LEGACY_SECTION_ELEVATION_TIER_MAX) return value;
|
|
209
|
+
return SECTION_ELEVATION_TIER_MAX;
|
|
210
|
+
}
|
|
211
|
+
function sectionGeometry(section, context = {}) {
|
|
212
|
+
const floorBaseHeightM = finiteClamped(
|
|
213
|
+
context.floorBaseHeightM,
|
|
214
|
+
SECTION_HEIGHT_MIN_M,
|
|
215
|
+
SECTION_HEIGHT_MAX_M,
|
|
216
|
+
0
|
|
217
|
+
);
|
|
218
|
+
const automaticHeight = Math.min(
|
|
219
|
+
SECTION_HEIGHT_MAX_M,
|
|
220
|
+
floorBaseHeightM + compatibleAutomaticTier(section.elevation) * TIER_HEIGHT_M
|
|
221
|
+
);
|
|
222
|
+
const height = section.height === void 0 ? automaticHeight : finiteClamped(section.height, SECTION_HEIGHT_MIN_M, SECTION_HEIGHT_MAX_M, automaticHeight);
|
|
223
|
+
const rake = finiteClamped(section.rake, SECTION_RAKE_MIN_DEG, SECTION_RAKE_MAX_DEG, 0);
|
|
224
|
+
return { height, rake };
|
|
225
|
+
}
|
|
226
|
+
|
|
161
227
|
// src/core/layout.ts
|
|
162
228
|
function overrideAccessibility(o) {
|
|
163
229
|
if (!o) return [];
|
|
164
|
-
if (o.accessibility && o.accessibility.length)
|
|
230
|
+
if (o.accessibility && o.accessibility.length) {
|
|
231
|
+
return o.wheelchairSpaceType && !o.accessibility.includes("wheelchair") ? ["wheelchair", ...o.accessibility] : o.accessibility;
|
|
232
|
+
}
|
|
233
|
+
if (o.wheelchairSpaceType) return ["wheelchair"];
|
|
165
234
|
return o.accessible ? ["wheelchair"] : [];
|
|
166
235
|
}
|
|
167
236
|
var DEG = Math.PI / 180;
|
|
@@ -203,6 +272,10 @@ function overrideMap(row) {
|
|
|
203
272
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
204
273
|
return m;
|
|
205
274
|
}
|
|
275
|
+
function rowInventoryCount(row) {
|
|
276
|
+
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));
|
|
277
|
+
return Math.max(0, row.seatCount - skipped.size);
|
|
278
|
+
}
|
|
206
279
|
function centerRank(n) {
|
|
207
280
|
const rank = new Array(n);
|
|
208
281
|
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);
|
|
@@ -216,9 +289,9 @@ function seatLabelPart(row, i) {
|
|
|
216
289
|
const prefix = row.seatNumbering?.prefix ?? "";
|
|
217
290
|
const endAt = row.seatNumbering?.endAt;
|
|
218
291
|
const n = row.seatCount;
|
|
219
|
-
if (scheme === "updown") {
|
|
292
|
+
if (scheme === "updown" || scheme === "updown-descending") {
|
|
220
293
|
const half = Math.ceil(n / 2);
|
|
221
|
-
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
294
|
+
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);
|
|
222
295
|
return `${prefix}${core2}`;
|
|
223
296
|
}
|
|
224
297
|
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
@@ -271,6 +344,7 @@ function expandRowSlots(row) {
|
|
|
271
344
|
skipped: !!o?.skip,
|
|
272
345
|
accessible: accessibility.length > 0,
|
|
273
346
|
accessibility,
|
|
347
|
+
wheelchairSpaceType: o?.wheelchairSpaceType,
|
|
274
348
|
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
275
349
|
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl,
|
|
276
350
|
labelStyle: o?.labelStyle
|
|
@@ -291,6 +365,7 @@ function expandRow(row) {
|
|
|
291
365
|
categoryKey: slot.categoryKey,
|
|
292
366
|
accessible: slot.accessible || void 0,
|
|
293
367
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
368
|
+
wheelchairSpaceType: slot.wheelchairSpaceType,
|
|
294
369
|
commercial: slot.commercial,
|
|
295
370
|
viewUrl: slot.viewUrl,
|
|
296
371
|
labelStyle: slot.labelStyle
|
|
@@ -298,18 +373,43 @@ function expandRow(row) {
|
|
|
298
373
|
}
|
|
299
374
|
return seats;
|
|
300
375
|
}
|
|
301
|
-
function
|
|
376
|
+
function tableSeatCountsBySide(t2) {
|
|
377
|
+
if (t2.seatCountsBySide) return { ...t2.seatCountsBySide };
|
|
378
|
+
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
379
|
+
const order = ["top", "bottom", "left", "right"].filter((side) => enabled.includes(side));
|
|
380
|
+
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
381
|
+
if (!order.length) return counts;
|
|
382
|
+
const n = Math.max(0, Math.round(t2.seatCount));
|
|
383
|
+
for (let index = 0; index < n; index++) counts[order[index % order.length]] += 1;
|
|
384
|
+
return counts;
|
|
385
|
+
}
|
|
386
|
+
function expandTableSlots(t2) {
|
|
302
387
|
const seats = [];
|
|
303
388
|
const n = Math.max(0, Math.round(t2.seatCount));
|
|
304
389
|
if (n === 0) return seats;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
390
|
+
const overrides2 = new Map((t2.overrides ?? []).map((override) => [override.index, override]));
|
|
391
|
+
const mk = (index, x, y, side) => {
|
|
392
|
+
const override = overrides2.get(index);
|
|
393
|
+
const accessibility = overrideAccessibility(override);
|
|
394
|
+
const label = override?.label ?? `${t2.label}-${index + 1}`;
|
|
395
|
+
const displayPrefix = t2.displayLabel ?? t2.label;
|
|
396
|
+
return {
|
|
397
|
+
index,
|
|
398
|
+
label,
|
|
399
|
+
displayLabel: override?.displayLabel ?? `${displayPrefix}-${index + 1}`,
|
|
400
|
+
x: x + (override?.dx ?? 0),
|
|
401
|
+
y: y + (override?.dy ?? 0),
|
|
402
|
+
categoryKey: override?.categoryKey ?? t2.categoryKey,
|
|
403
|
+
skipped: !!override?.skip,
|
|
404
|
+
accessible: accessibility.length > 0,
|
|
405
|
+
accessibility,
|
|
406
|
+
wheelchairSpaceType: override?.wheelchairSpaceType,
|
|
407
|
+
commercial: override?.commercial,
|
|
408
|
+
viewUrl: override?.viewFromSeatUrl,
|
|
409
|
+
labelStyle: override?.labelStyle,
|
|
410
|
+
...side ? { side } : {}
|
|
411
|
+
};
|
|
412
|
+
};
|
|
313
413
|
if (t2.shape === "round") {
|
|
314
414
|
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
315
415
|
const base = t2.rotation * DEG;
|
|
@@ -332,17 +432,11 @@ function expandTable(t2) {
|
|
|
332
432
|
}
|
|
333
433
|
const w = t2.width ?? 80;
|
|
334
434
|
const h = t2.height ?? 50;
|
|
335
|
-
const
|
|
336
|
-
const order = ["top", "bottom", "left", "right"]
|
|
337
|
-
if (!order.length) return seats;
|
|
338
|
-
const counts = new Map(order.map((s) => [s, 0]));
|
|
339
|
-
for (let i = 0; i < n; i++) {
|
|
340
|
-
const s = order[i % order.length];
|
|
341
|
-
counts.set(s, counts.get(s) + 1);
|
|
342
|
-
}
|
|
435
|
+
const counts = tableSeatCountsBySide(t2);
|
|
436
|
+
const order = ["top", "bottom", "left", "right"];
|
|
343
437
|
let idx = 0;
|
|
344
438
|
for (const side of order) {
|
|
345
|
-
const count = counts
|
|
439
|
+
const count = counts[side];
|
|
346
440
|
for (let j = 0; j < count; j++) {
|
|
347
441
|
let localX;
|
|
348
442
|
let localY;
|
|
@@ -359,17 +453,39 @@ function expandTable(t2) {
|
|
|
359
453
|
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
360
454
|
localY = -h / 2 + (j + 0.5) * h / count;
|
|
361
455
|
}
|
|
362
|
-
const
|
|
363
|
-
seats.push(mk(idx++,
|
|
456
|
+
const point = place(localX, localY, t2.rotation, t2.center);
|
|
457
|
+
seats.push(mk(idx++, point.x, point.y, side));
|
|
364
458
|
}
|
|
365
459
|
}
|
|
366
460
|
return seats;
|
|
367
461
|
}
|
|
462
|
+
function tableInventoryCount(t2) {
|
|
463
|
+
if (t2.bookAsWhole || t2.variableOccupancy) return Math.max(0, Math.round(t2.seatCount));
|
|
464
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).length;
|
|
465
|
+
}
|
|
466
|
+
function expandTable(t2) {
|
|
467
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).map((slot) => ({
|
|
468
|
+
id: `${t2.id}:${slot.index}`,
|
|
469
|
+
label: slot.label,
|
|
470
|
+
...slot.displayLabel !== slot.label ? { displayLabel: slot.displayLabel } : {},
|
|
471
|
+
x: slot.x,
|
|
472
|
+
y: slot.y,
|
|
473
|
+
rowId: t2.id,
|
|
474
|
+
categoryKey: slot.categoryKey,
|
|
475
|
+
...slot.accessible ? { accessible: true } : {},
|
|
476
|
+
...slot.accessibility.length ? { accessibility: slot.accessibility } : {},
|
|
477
|
+
...slot.wheelchairSpaceType ? { wheelchairSpaceType: slot.wheelchairSpaceType } : {},
|
|
478
|
+
...slot.commercial ? { commercial: slot.commercial } : {},
|
|
479
|
+
...slot.viewUrl ? { viewUrl: slot.viewUrl } : {},
|
|
480
|
+
...slot.labelStyle ? { labelStyle: slot.labelStyle } : {}
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
368
483
|
function expandBooth(b) {
|
|
369
484
|
return [
|
|
370
485
|
{
|
|
371
486
|
id: `${b.id}:0`,
|
|
372
487
|
label: b.label,
|
|
488
|
+
...b.displayLabel && b.displayLabel !== b.label ? { displayLabel: b.displayLabel } : {},
|
|
373
489
|
x: b.center.x,
|
|
374
490
|
y: b.center.y,
|
|
375
491
|
rowId: b.id,
|
|
@@ -407,7 +523,7 @@ function polygonLabelPoint(outer, holes) {
|
|
|
407
523
|
if (!outer.length) return { x: 0, y: 0 };
|
|
408
524
|
const xs = outer.map((point) => point.x);
|
|
409
525
|
const ys = outer.map((point) => point.y);
|
|
410
|
-
const
|
|
526
|
+
const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
411
527
|
const centroid2 = polygonCentroid(outer);
|
|
412
528
|
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
413
529
|
let best = outer[0];
|
|
@@ -416,8 +532,8 @@ function polygonLabelPoint(outer, holes) {
|
|
|
416
532
|
for (let row = 1; row < 24; row += 1) {
|
|
417
533
|
for (let column = 1; column < 24; column += 1) {
|
|
418
534
|
const point = {
|
|
419
|
-
x:
|
|
420
|
-
y:
|
|
535
|
+
x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
|
|
536
|
+
y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
|
|
421
537
|
};
|
|
422
538
|
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
423
539
|
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
@@ -479,9 +595,32 @@ function objectCenter(o) {
|
|
|
479
595
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
480
596
|
}
|
|
481
597
|
}
|
|
598
|
+
function samePoints(left, right) {
|
|
599
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
600
|
+
}
|
|
601
|
+
function sameGASurfaceAsSection(object, section) {
|
|
602
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
603
|
+
const objectHoles = object.holes ?? [];
|
|
604
|
+
const sectionHoles = section.holes ?? [];
|
|
605
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
606
|
+
}
|
|
607
|
+
function owningSectionForObject(objects, object) {
|
|
608
|
+
const sections = objects.filter((candidate) => candidate.type === "section");
|
|
609
|
+
const referencedLogicalId = "referenceInventorySource" in object ? object.referenceInventorySource?.logicalSectionId : void 0;
|
|
610
|
+
const center = objectCenter(object);
|
|
611
|
+
const referencedOwner = referencedLogicalId ? sections.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(object, section) || pointInPolygonWithHoles(center, section.outline, section.holes))) : void 0;
|
|
612
|
+
return referencedOwner ?? sections.find((section) => pointInPolygonWithHoles(center, section.outline, section.holes));
|
|
613
|
+
}
|
|
482
614
|
function floorsOf(doc) {
|
|
483
615
|
if (doc.floors && doc.floors.length) return doc.floors;
|
|
484
|
-
return [{
|
|
616
|
+
return [{
|
|
617
|
+
id: "floor-0",
|
|
618
|
+
name: "Main",
|
|
619
|
+
objects: doc.objects,
|
|
620
|
+
focalPoint: doc.focalPoint,
|
|
621
|
+
referenceImage: doc.referenceImage,
|
|
622
|
+
backgroundImage: doc.backgroundImage
|
|
623
|
+
}];
|
|
485
624
|
}
|
|
486
625
|
function floorObjects(doc, floorId) {
|
|
487
626
|
const floors = floorsOf(doc);
|
|
@@ -530,15 +669,138 @@ function stackFloors(doc, spread = 900) {
|
|
|
530
669
|
});
|
|
531
670
|
return { ...doc, objects, floors: void 0 };
|
|
532
671
|
}
|
|
533
|
-
function
|
|
672
|
+
function expandFloorObjects(objects, zones, fallbackFocal) {
|
|
534
673
|
const out = [];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
674
|
+
const segmented = /* @__PURE__ */ new Map();
|
|
675
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const object of objects) {
|
|
677
|
+
if (object.type !== "row" || !object.segmentedRow) continue;
|
|
678
|
+
const list = grouped.get(object.segmentedRow.groupId) ?? [];
|
|
679
|
+
list.push(object);
|
|
680
|
+
grouped.set(object.segmentedRow.groupId, list);
|
|
681
|
+
}
|
|
682
|
+
for (const [groupId, members] of grouped) {
|
|
683
|
+
const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
|
|
684
|
+
const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
|
|
685
|
+
const first = ordered[0]?.segmentedRow;
|
|
686
|
+
if (!first) continue;
|
|
687
|
+
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);
|
|
688
|
+
if (!valid) continue;
|
|
689
|
+
const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
|
|
690
|
+
let adjacencyOffset = 0;
|
|
691
|
+
let displayOffset = 0;
|
|
692
|
+
for (const row of ordered) {
|
|
693
|
+
if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
|
|
694
|
+
segmented.set(row.id, {
|
|
695
|
+
groupId,
|
|
696
|
+
adjacencyOffset,
|
|
697
|
+
displayOffset,
|
|
698
|
+
displayLabel: first.displayLabel,
|
|
699
|
+
totalSeats,
|
|
700
|
+
canonical: ordered[0],
|
|
701
|
+
viewFromSeatUrl: first.viewFromSeatUrl
|
|
702
|
+
});
|
|
703
|
+
adjacencyOffset += row.seatCount;
|
|
704
|
+
displayOffset += row.seatCount;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const obj of objects) {
|
|
708
|
+
let seats = [];
|
|
709
|
+
if (obj.type === "row") seats = expandRow(obj);
|
|
710
|
+
else if (obj.type === "table") seats = expandTable(obj);
|
|
711
|
+
else if (obj.type === "booth") seats = expandBooth(obj);
|
|
712
|
+
if (!seats.length) continue;
|
|
713
|
+
if (obj.type === "row") {
|
|
714
|
+
const logical = segmented.get(obj.id);
|
|
715
|
+
if (logical) {
|
|
716
|
+
const overrides2 = new Map((obj.overrides ?? []).map((override) => [override.index, override]));
|
|
717
|
+
for (const seat of seats) {
|
|
718
|
+
const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
|
|
719
|
+
if (!Number.isInteger(physicalIndex)) continue;
|
|
720
|
+
const displayOrdinal = logical.displayOffset + physicalIndex;
|
|
721
|
+
seat.logicalRowId = logical.groupId;
|
|
722
|
+
seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
|
|
723
|
+
if (!overrides2.get(physicalIndex)?.displayLabel) {
|
|
724
|
+
const numberingRow = {
|
|
725
|
+
...logical.canonical,
|
|
726
|
+
seatCount: logical.totalSeats,
|
|
727
|
+
label: logical.displayLabel,
|
|
728
|
+
displayLabel: logical.displayLabel
|
|
729
|
+
};
|
|
730
|
+
seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
|
|
731
|
+
}
|
|
732
|
+
seat.viewUrl ??= logical.viewFromSeatUrl;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const owner = owningSectionForObject(objects, obj);
|
|
737
|
+
const inheritedView = owner?.viewFromSeatUrl;
|
|
738
|
+
const zone = owner?.zone ? zones?.find((candidate) => candidate.id === owner.zone) : void 0;
|
|
739
|
+
const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
|
|
740
|
+
for (const seat of seats) {
|
|
741
|
+
if (inheritedView) seat.viewUrl ??= inheritedView;
|
|
742
|
+
if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
|
|
743
|
+
if (owner?.zone) seat.zoneId = owner.zone;
|
|
744
|
+
if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
|
|
745
|
+
}
|
|
746
|
+
out.push(...seats);
|
|
539
747
|
}
|
|
540
748
|
return out;
|
|
541
749
|
}
|
|
750
|
+
function expandChart(doc, options = {}) {
|
|
751
|
+
if (doc.floors?.length) {
|
|
752
|
+
const out2 = [];
|
|
753
|
+
for (const floor of doc.floors) {
|
|
754
|
+
const floorFocal = floor.focalPoint ?? doc.focalPoint;
|
|
755
|
+
const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
|
|
756
|
+
assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
|
|
757
|
+
out2.push(...seats);
|
|
758
|
+
}
|
|
759
|
+
return out2;
|
|
760
|
+
}
|
|
761
|
+
const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
|
|
762
|
+
assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
|
|
766
|
+
const sections = objects.filter((o) => o.type === "section");
|
|
767
|
+
const hasGeometry = floorBaseHeightM > 0 || sections.some((s) => s.height !== void 0 || s.rake !== void 0 || (s.elevation ?? 0) > 0);
|
|
768
|
+
if (!sections.length || !hasGeometry || !focal) {
|
|
769
|
+
for (const seat of seats) seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const owner = new Array(seats.length);
|
|
773
|
+
const geo = /* @__PURE__ */ new Map();
|
|
774
|
+
const frontDistU = /* @__PURE__ */ new Map();
|
|
775
|
+
for (let i = 0; i < seats.length; i++) {
|
|
776
|
+
const seat = seats[i];
|
|
777
|
+
const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
|
|
778
|
+
owner[i] = sec;
|
|
779
|
+
if (!sec) continue;
|
|
780
|
+
if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
|
|
781
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
782
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
783
|
+
const cur = frontDistU.get(sec.id);
|
|
784
|
+
if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
|
|
785
|
+
}
|
|
786
|
+
for (let i = 0; i < seats.length; i++) {
|
|
787
|
+
const seat = seats[i];
|
|
788
|
+
const sec = owner[i];
|
|
789
|
+
if (!sec) {
|
|
790
|
+
seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const g = geo.get(sec.id);
|
|
794
|
+
let riseM = 0;
|
|
795
|
+
if (g.rake > 0) {
|
|
796
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
797
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
798
|
+
const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
|
|
799
|
+
riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
|
|
800
|
+
}
|
|
801
|
+
seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
542
804
|
var PAD = 40;
|
|
543
805
|
function chartBounds(doc) {
|
|
544
806
|
let minX = Infinity;
|
|
@@ -582,8 +844,9 @@ function chartBounds(doc) {
|
|
|
582
844
|
acc(obj.position.x + w, obj.position.y + obj.fontSize);
|
|
583
845
|
}
|
|
584
846
|
}
|
|
585
|
-
|
|
586
|
-
|
|
847
|
+
for (const image of [doc.referenceImage, doc.backgroundImage]) {
|
|
848
|
+
if (!image) continue;
|
|
849
|
+
const { center, width } = image;
|
|
587
850
|
const bh = width * 3 / 4;
|
|
588
851
|
acc(center.x - width / 2, center.y - bh / 2);
|
|
589
852
|
acc(center.x + width / 2, center.y + bh / 2);
|
|
@@ -607,9 +870,55 @@ var MAX_EVENT_INVENTORY = 1e5;
|
|
|
607
870
|
function gaUnitLabel(areaId, index) {
|
|
608
871
|
return `${PREFIX}${encodeURIComponent(areaId)}__${index + 1}`;
|
|
609
872
|
}
|
|
610
|
-
function
|
|
873
|
+
function validGAInventorySegments(area) {
|
|
874
|
+
const segments = area.inventorySegments;
|
|
875
|
+
if (segments === void 0) return true;
|
|
876
|
+
if (!Array.isArray(segments) || segments.length < 1 || segments.length > 256) return false;
|
|
877
|
+
let total = 0;
|
|
878
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
879
|
+
for (const segment of segments) {
|
|
880
|
+
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;
|
|
881
|
+
total += segment.count;
|
|
882
|
+
if (total > MAX_GA_CAPACITY) return false;
|
|
883
|
+
const sourceRanges = ranges.get(segment.sourceAreaId) ?? [];
|
|
884
|
+
const end = segment.startIndex + segment.count;
|
|
885
|
+
if (sourceRanges.some((range) => segment.startIndex < range.end && end > range.start)) return false;
|
|
886
|
+
sourceRanges.push({ start: segment.startIndex, end });
|
|
887
|
+
ranges.set(segment.sourceAreaId, sourceRanges);
|
|
888
|
+
}
|
|
889
|
+
return total === area.capacity;
|
|
890
|
+
}
|
|
891
|
+
function gaInventorySegments(area) {
|
|
611
892
|
const capacity = Math.min(MAX_GA_CAPACITY, Math.max(0, Math.floor(area.capacity)));
|
|
612
|
-
|
|
893
|
+
if (area.inventorySegments === void 0) {
|
|
894
|
+
return capacity ? [{ sourceAreaId: area.id, startIndex: 0, count: capacity }] : [];
|
|
895
|
+
}
|
|
896
|
+
if (!validGAInventorySegments(area)) return [];
|
|
897
|
+
return area.inventorySegments.map((segment) => ({ ...segment }));
|
|
898
|
+
}
|
|
899
|
+
function gaUnitLabels(area) {
|
|
900
|
+
return gaInventorySegments(area).flatMap((segment) => Array.from(
|
|
901
|
+
{ length: segment.count },
|
|
902
|
+
(_, offset) => gaUnitLabel(segment.sourceAreaId, segment.startIndex + offset)
|
|
903
|
+
));
|
|
904
|
+
}
|
|
905
|
+
function growJoinedGAInventory(area, nextCapacity) {
|
|
906
|
+
if (area.inventorySegments === void 0 || nextCapacity === area.capacity) return area.inventorySegments;
|
|
907
|
+
if (nextCapacity < area.capacity) return void 0;
|
|
908
|
+
const segments = gaInventorySegments(area);
|
|
909
|
+
if (!segments.length) return void 0;
|
|
910
|
+
const extra = nextCapacity - area.capacity;
|
|
911
|
+
if (!extra) return segments;
|
|
912
|
+
const ownRanges = segments.filter((segment) => segment.sourceAreaId === area.id);
|
|
913
|
+
const startIndex = ownRanges.reduce((max, segment) => Math.max(max, segment.startIndex + segment.count), 0);
|
|
914
|
+
const last = segments[segments.length - 1];
|
|
915
|
+
if (last?.sourceAreaId === area.id && last.startIndex + last.count === startIndex) {
|
|
916
|
+
last.count += extra;
|
|
917
|
+
} else {
|
|
918
|
+
if (segments.length >= 256) return void 0;
|
|
919
|
+
segments.push({ sourceAreaId: area.id, startIndex, count: extra });
|
|
920
|
+
}
|
|
921
|
+
return segments;
|
|
613
922
|
}
|
|
614
923
|
function gaAreasOf(doc) {
|
|
615
924
|
return allObjects(doc).filter((o) => o.type === "gaArea");
|
|
@@ -630,15 +939,6 @@ function objectSeatLabels(o) {
|
|
|
630
939
|
function isSeatObject(o) {
|
|
631
940
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
632
941
|
}
|
|
633
|
-
function samePoints(left, right) {
|
|
634
|
-
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
635
|
-
}
|
|
636
|
-
function sameGASurfaceAsSection(object, section) {
|
|
637
|
-
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
638
|
-
const objectHoles = object.holes ?? [];
|
|
639
|
-
const sectionHoles = section.holes ?? [];
|
|
640
|
-
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
641
|
-
}
|
|
642
942
|
function computeSections(doc) {
|
|
643
943
|
const objs = allObjects(doc);
|
|
644
944
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
@@ -664,10 +964,7 @@ function computeSections(doc) {
|
|
|
664
964
|
if (!isSeatObject(obj)) continue;
|
|
665
965
|
const labels = objectSeatLabels(obj);
|
|
666
966
|
if (labels.length === 0) continue;
|
|
667
|
-
const
|
|
668
|
-
const c = objectCenter(obj);
|
|
669
|
-
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
670
|
-
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
967
|
+
const owner = owningSectionForObject(objs, obj);
|
|
671
968
|
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
672
969
|
node.seatCount += labels.length;
|
|
673
970
|
node.objectIds.push(obj.id);
|
|
@@ -1238,11 +1535,473 @@ import { Circle } from "konva/lib/shapes/Circle";
|
|
|
1238
1535
|
import { Rect } from "konva/lib/shapes/Rect";
|
|
1239
1536
|
import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
1240
1537
|
import { Line } from "konva/lib/shapes/Line";
|
|
1538
|
+
import { Arrow } from "konva/lib/shapes/Arrow";
|
|
1241
1539
|
import { Text } from "konva/lib/shapes/Text";
|
|
1242
1540
|
import { Path } from "konva/lib/shapes/Path";
|
|
1243
1541
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
1244
1542
|
import { Shape } from "konva/lib/Shape";
|
|
1245
1543
|
|
|
1544
|
+
// src/core/chartBackgrounds.ts
|
|
1545
|
+
function buyerBackgroundImage(owner) {
|
|
1546
|
+
const background = owner.backgroundImage;
|
|
1547
|
+
return background?.url && !background.assetId ? background : void 0;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/core/icons.ts
|
|
1551
|
+
var VENUE_ICON_VIEWBOX = 24;
|
|
1552
|
+
var VENUE_ICON_STROKE = 2;
|
|
1553
|
+
var WHEELCHAIR = "M13 4.6a1.6 1.6 0 1 0-3.2 0 1.6 1.6 0 0 0 3.2 0 M11 6.5V13h5l2.4 6 M15 16.4A5 5 0 1 1 9 11.3";
|
|
1554
|
+
var VENUE_ICONS = [
|
|
1555
|
+
// ---- Facilities --------------------------------------------------------
|
|
1556
|
+
{
|
|
1557
|
+
key: "restroom-men",
|
|
1558
|
+
label: "Men's restroom",
|
|
1559
|
+
group: "facilities",
|
|
1560
|
+
path: "M16 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0 M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
key: "restroom-women",
|
|
1564
|
+
label: "Women's restroom",
|
|
1565
|
+
group: "facilities",
|
|
1566
|
+
path: "M15 5a3 3 0 1 0-6 0 3 3 0 0 0 6 0 M12 8l-4 10h8l-4-10 M10 18v4 M14 18v4"
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
key: "restroom-accessible",
|
|
1570
|
+
label: "Accessible restroom",
|
|
1571
|
+
group: "facilities",
|
|
1572
|
+
path: WHEELCHAIR
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
key: "first-aid",
|
|
1576
|
+
label: "First aid",
|
|
1577
|
+
group: "facilities",
|
|
1578
|
+
path: "M5 4h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M12 8v8 M8 12h8"
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
key: "coat-check",
|
|
1582
|
+
label: "Coat check",
|
|
1583
|
+
group: "facilities",
|
|
1584
|
+
path: "M12 6a2 2 0 0 1 0-4 2 2 0 0 1 1.6 3.2L21 13H3l8.4-7.8 M3 13h18"
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
key: "atm",
|
|
1588
|
+
label: "ATM / cash",
|
|
1589
|
+
group: "facilities",
|
|
1590
|
+
path: "M2 6h20v12H2V6Z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M6 9h.01 M18 15h.01"
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
key: "info",
|
|
1594
|
+
label: "Info point",
|
|
1595
|
+
group: "facilities",
|
|
1596
|
+
path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M12 11v5 M12 8h.01"
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
key: "lost-found",
|
|
1600
|
+
label: "Lost & found",
|
|
1601
|
+
group: "facilities",
|
|
1602
|
+
path: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14 M16 16l5 5 M9 8.5a2 2 0 1 1 3 1.7c-.8.5-1 .8-1 1.8 M11 15h.01"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
key: "charging",
|
|
1606
|
+
label: "Charging point",
|
|
1607
|
+
group: "facilities",
|
|
1608
|
+
path: "M3 8h13v8H3V8Z M16 10h3v4h-3 M9.6 9l-2 3.5h2.5l-1.5 3"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
key: "smoking",
|
|
1612
|
+
label: "Smoking area",
|
|
1613
|
+
group: "facilities",
|
|
1614
|
+
path: "M2 15h13v3H2v-3Z M18 16h2 M19 8c0 1 1 1.5 1 2.5S19 12 19 13"
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
key: "no-smoking",
|
|
1618
|
+
label: "No smoking",
|
|
1619
|
+
group: "facilities",
|
|
1620
|
+
path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M5.6 5.6l12.8 12.8 M7 13h6v2H7v-2Z"
|
|
1621
|
+
},
|
|
1622
|
+
// ---- Food & drink ------------------------------------------------------
|
|
1623
|
+
{
|
|
1624
|
+
key: "food",
|
|
1625
|
+
label: "Food",
|
|
1626
|
+
group: "food-drink",
|
|
1627
|
+
path: "M7 3v6a2 2 0 0 0 4 0V3 M9 11v10 M17 3c-1.6 1-2.2 3-2.2 6 0 2 .9 3.2 2.2 3.6V21"
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
key: "bar",
|
|
1631
|
+
label: "Bar",
|
|
1632
|
+
group: "food-drink",
|
|
1633
|
+
path: "M4 5h16l-8 8-8-8Z M12 13v6 M8 20h8"
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
key: "coffee",
|
|
1637
|
+
label: "Caf\xE9 / coffee",
|
|
1638
|
+
group: "food-drink",
|
|
1639
|
+
path: "M4 8h13v4a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V8Z M17 9h2a2 2 0 0 1 0 4h-2 M8 2v2 M11 2v2 M4 21h14"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
key: "water",
|
|
1643
|
+
label: "Water / drinks",
|
|
1644
|
+
group: "food-drink",
|
|
1645
|
+
path: "M12 22a7 7 0 0 1-7-7c0-5 7-12 7-12s7 7 7 12a7 7 0 0 1-7 7Z"
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
key: "merch",
|
|
1649
|
+
label: "Merch / shop",
|
|
1650
|
+
group: "food-drink",
|
|
1651
|
+
path: "M6 8h12l-1 12H7L6 8Z M9 8V6a3 3 0 0 1 6 0v2"
|
|
1652
|
+
},
|
|
1653
|
+
// ---- Getting around ----------------------------------------------------
|
|
1654
|
+
{
|
|
1655
|
+
key: "entrance",
|
|
1656
|
+
label: "Entrance",
|
|
1657
|
+
group: "navigation",
|
|
1658
|
+
path: "M14 3h5a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-5 M3 12h11 M10 8l4 4-4 4"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
key: "exit",
|
|
1662
|
+
label: "Exit",
|
|
1663
|
+
group: "navigation",
|
|
1664
|
+
path: "M10 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h5 M14 12h7 M17 8l4 4-4 4"
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
key: "emergency-exit",
|
|
1668
|
+
label: "Emergency exit",
|
|
1669
|
+
group: "navigation",
|
|
1670
|
+
path: "M14 4.6a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0 M12.5 8l-3.5 2 1.6 3.6-2 4.4 M12.5 10l3.5 1.5 M9 10.5l-3.5 1 M15 12h6 M18 9l3 3-3 3"
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
key: "stairs",
|
|
1674
|
+
label: "Stairs",
|
|
1675
|
+
group: "navigation",
|
|
1676
|
+
path: "M3 20v-4h4v-4h4v-4h4V4h5"
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
key: "elevator",
|
|
1680
|
+
label: "Elevator",
|
|
1681
|
+
group: "navigation",
|
|
1682
|
+
path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M8 11l1.5-2 1.5 2 M8 14l1.5 2 1.5-2 M15 8v8"
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
key: "parking",
|
|
1686
|
+
label: "Parking",
|
|
1687
|
+
group: "navigation",
|
|
1688
|
+
path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M9 17V7h4a3 3 0 0 1 0 6H9"
|
|
1689
|
+
},
|
|
1690
|
+
// ---- Accessibility -----------------------------------------------------
|
|
1691
|
+
{
|
|
1692
|
+
key: "wheelchair",
|
|
1693
|
+
label: "Wheelchair access",
|
|
1694
|
+
group: "accessibility",
|
|
1695
|
+
path: WHEELCHAIR
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
key: "hearing",
|
|
1699
|
+
label: "Hearing assistance",
|
|
1700
|
+
group: "accessibility",
|
|
1701
|
+
path: "M8 20a4 4 0 0 1-2-3c0-1 .5-2 .5-3a4.5 4.5 0 1 1 9 0c0 1.4-1 2-2 2.4-1.2.5-1.5 1-1.5 2.2 M17 6a5 5 0 0 1 1 6 M19.5 4a8 8 0 0 1 1.2 9"
|
|
1702
|
+
}
|
|
1703
|
+
];
|
|
1704
|
+
var BY_KEY = new Map(VENUE_ICONS.map((icon) => [icon.key, icon]));
|
|
1705
|
+
function venueIcon(key) {
|
|
1706
|
+
return key ? BY_KEY.get(key) : void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function venueIconPath(key) {
|
|
1709
|
+
return venueIcon(key)?.path;
|
|
1710
|
+
}
|
|
1711
|
+
var VENUE_ICON_KEYS = VENUE_ICONS.map((icon) => icon.key);
|
|
1712
|
+
|
|
1713
|
+
// src/core/spatialIndex.ts
|
|
1714
|
+
var DEFAULT_SEAT_RADIUS = 9;
|
|
1715
|
+
function halfExtents(shape) {
|
|
1716
|
+
if (shape.kind === "circle") return { hx: shape.r, hy: shape.r };
|
|
1717
|
+
const hw = shape.width / 2;
|
|
1718
|
+
const hh = shape.height / 2;
|
|
1719
|
+
const rot = shape.rotation ?? 0;
|
|
1720
|
+
if (!rot) return { hx: hw, hy: hh };
|
|
1721
|
+
const rad = rot * Math.PI / 180;
|
|
1722
|
+
const c = Math.abs(Math.cos(rad));
|
|
1723
|
+
const s = Math.abs(Math.sin(rad));
|
|
1724
|
+
return { hx: hw * c + hh * s, hy: hw * s + hh * c };
|
|
1725
|
+
}
|
|
1726
|
+
function shapeIntersectsRect(e, x0, y0, x1, y1) {
|
|
1727
|
+
if (e.shape.kind === "circle") {
|
|
1728
|
+
const cx = Math.min(Math.max(e.seat.x, x0), x1);
|
|
1729
|
+
const cy = Math.min(Math.max(e.seat.y, y0), y1);
|
|
1730
|
+
const dx = e.seat.x - cx;
|
|
1731
|
+
const dy = e.seat.y - cy;
|
|
1732
|
+
return dx * dx + dy * dy <= e.shape.r * e.shape.r;
|
|
1733
|
+
}
|
|
1734
|
+
const rot = e.shape.rotation ?? 0;
|
|
1735
|
+
if (!rot) {
|
|
1736
|
+
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;
|
|
1737
|
+
}
|
|
1738
|
+
const rad = rot * Math.PI / 180;
|
|
1739
|
+
const c = Math.cos(rad);
|
|
1740
|
+
const s = Math.sin(rad);
|
|
1741
|
+
const hw = e.shape.width / 2;
|
|
1742
|
+
const hh = e.shape.height / 2;
|
|
1743
|
+
const corners = [
|
|
1744
|
+
[-hw, -hh],
|
|
1745
|
+
[hw, -hh],
|
|
1746
|
+
[hw, hh],
|
|
1747
|
+
[-hw, hh]
|
|
1748
|
+
].map(([lx, ly]) => [e.seat.x + lx * c - ly * s, e.seat.y + lx * s + ly * c]);
|
|
1749
|
+
const query = [
|
|
1750
|
+
[x0, y0],
|
|
1751
|
+
[x1, y0],
|
|
1752
|
+
[x1, y1],
|
|
1753
|
+
[x0, y1]
|
|
1754
|
+
];
|
|
1755
|
+
const axes = [
|
|
1756
|
+
[1, 0],
|
|
1757
|
+
[0, 1],
|
|
1758
|
+
[c, s],
|
|
1759
|
+
[-s, c]
|
|
1760
|
+
];
|
|
1761
|
+
for (const [ax, ay] of axes) {
|
|
1762
|
+
let minA = Infinity;
|
|
1763
|
+
let maxA = -Infinity;
|
|
1764
|
+
for (const [px, py] of corners) {
|
|
1765
|
+
const d = px * ax + py * ay;
|
|
1766
|
+
if (d < minA) minA = d;
|
|
1767
|
+
if (d > maxA) maxA = d;
|
|
1768
|
+
}
|
|
1769
|
+
let minB = Infinity;
|
|
1770
|
+
let maxB = -Infinity;
|
|
1771
|
+
for (const [px, py] of query) {
|
|
1772
|
+
const d = px * ax + py * ay;
|
|
1773
|
+
if (d < minB) minB = d;
|
|
1774
|
+
if (d > maxB) maxB = d;
|
|
1775
|
+
}
|
|
1776
|
+
if (maxA < minB || maxB < minA) return false;
|
|
1777
|
+
}
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
function buildSeatIndex(seats, opts = {}) {
|
|
1781
|
+
const seatRadius = opts.seatRadius ?? DEFAULT_SEAT_RADIUS;
|
|
1782
|
+
const n = seats.length;
|
|
1783
|
+
const entries = new Array(n);
|
|
1784
|
+
let minX = Infinity;
|
|
1785
|
+
let minY = Infinity;
|
|
1786
|
+
let maxX = -Infinity;
|
|
1787
|
+
let maxY = -Infinity;
|
|
1788
|
+
let maxHalf = 0;
|
|
1789
|
+
for (let i = 0; i < n; i++) {
|
|
1790
|
+
const seat = seats[i];
|
|
1791
|
+
const shape = opts.shapeOf?.(seat) ?? { kind: "circle", r: seatRadius };
|
|
1792
|
+
const { hx, hy } = halfExtents(shape);
|
|
1793
|
+
entries[i] = { seat, shape, hx, hy };
|
|
1794
|
+
if (seat.x - hx < minX) minX = seat.x - hx;
|
|
1795
|
+
if (seat.y - hy < minY) minY = seat.y - hy;
|
|
1796
|
+
if (seat.x + hx > maxX) maxX = seat.x + hx;
|
|
1797
|
+
if (seat.y + hy > maxY) maxY = seat.y + hy;
|
|
1798
|
+
if (hx > maxHalf) maxHalf = hx;
|
|
1799
|
+
if (hy > maxHalf) maxHalf = hy;
|
|
1800
|
+
}
|
|
1801
|
+
if (n === 0) {
|
|
1802
|
+
minX = 0;
|
|
1803
|
+
minY = 0;
|
|
1804
|
+
maxX = 0;
|
|
1805
|
+
maxY = 0;
|
|
1806
|
+
}
|
|
1807
|
+
const nominal = maxHalf > 0 ? maxHalf * 4 : 1;
|
|
1808
|
+
const spanX = Math.max(maxX - minX, 0);
|
|
1809
|
+
const spanY = Math.max(maxY - minY, 0);
|
|
1810
|
+
const maxCells = Math.max(64, opts.maxCells ?? n * 4);
|
|
1811
|
+
let cell = nominal;
|
|
1812
|
+
let cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1813
|
+
let rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1814
|
+
while (cols * rows > maxCells && (cols > 1 || rows > 1)) {
|
|
1815
|
+
cell *= 2;
|
|
1816
|
+
cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1817
|
+
rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1818
|
+
}
|
|
1819
|
+
const cellCount = cols * rows;
|
|
1820
|
+
const cellStart = new Int32Array(cellCount + 1);
|
|
1821
|
+
let total = 0;
|
|
1822
|
+
for (let i = 0; i < n; i++) {
|
|
1823
|
+
const e = entries[i];
|
|
1824
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1825
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1826
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1827
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1828
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1829
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1830
|
+
cellStart[cy * cols + cx + 1]++;
|
|
1831
|
+
total++;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
for (let c = 0; c < cellCount; c++) cellStart[c + 1] += cellStart[c];
|
|
1836
|
+
const items = new Int32Array(total);
|
|
1837
|
+
const cursor = cellStart.slice(0, cellCount);
|
|
1838
|
+
for (let i = 0; i < n; i++) {
|
|
1839
|
+
const e = entries[i];
|
|
1840
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1841
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1842
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1843
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1844
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1845
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1846
|
+
const c = cy * cols + cx;
|
|
1847
|
+
items[cursor[c]++] = i;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
seats,
|
|
1853
|
+
entries,
|
|
1854
|
+
minX,
|
|
1855
|
+
minY,
|
|
1856
|
+
cell,
|
|
1857
|
+
cols,
|
|
1858
|
+
rows,
|
|
1859
|
+
cellStart,
|
|
1860
|
+
items,
|
|
1861
|
+
stamp: new Int32Array(n),
|
|
1862
|
+
epoch: 0
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function clampInt(v, lo, hi) {
|
|
1866
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
1867
|
+
}
|
|
1868
|
+
function queryRect(index, rect, opts = {}) {
|
|
1869
|
+
const x0 = Math.min(rect.x, rect.x + rect.width);
|
|
1870
|
+
const x1 = Math.max(rect.x, rect.x + rect.width);
|
|
1871
|
+
const y0 = Math.min(rect.y, rect.y + rect.height);
|
|
1872
|
+
const y1 = Math.max(rect.y, rect.y + rect.height);
|
|
1873
|
+
const mode = opts.mode ?? "center";
|
|
1874
|
+
const { cols, rows, cell, minX, minY, cellStart, items, entries } = index;
|
|
1875
|
+
const cx0 = Math.max(0, Math.floor((x0 - minX) / cell));
|
|
1876
|
+
const cx1 = Math.min(cols - 1, Math.floor((x1 - minX) / cell));
|
|
1877
|
+
const cy0 = Math.max(0, Math.floor((y0 - minY) / cell));
|
|
1878
|
+
const cy1 = Math.min(rows - 1, Math.floor((y1 - minY) / cell));
|
|
1879
|
+
if (cx0 > cx1 || cy0 > cy1) return [];
|
|
1880
|
+
const found = [];
|
|
1881
|
+
const stamp = index.stamp;
|
|
1882
|
+
const ep = ++index.epoch;
|
|
1883
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1884
|
+
const rowBase = cy * cols;
|
|
1885
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1886
|
+
const c = rowBase + cx;
|
|
1887
|
+
const end = cellStart[c + 1];
|
|
1888
|
+
for (let k = cellStart[c]; k < end; k++) {
|
|
1889
|
+
const i = items[k];
|
|
1890
|
+
if (stamp[i] === ep) continue;
|
|
1891
|
+
stamp[i] = ep;
|
|
1892
|
+
const e = entries[i];
|
|
1893
|
+
if (opts.filter && !opts.filter(e.seat.id, e.seat)) continue;
|
|
1894
|
+
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);
|
|
1895
|
+
if (ok) found.push(i);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
found.sort((a, b) => a - b);
|
|
1900
|
+
return found.map((i) => entries[i].seat.id);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/core/perspectiveProjection.ts
|
|
1904
|
+
function createPerspectiveCamera(bounds2, maxSurfaceHeightWorld = 0) {
|
|
1905
|
+
const span = Math.max(1, bounds2.width, bounds2.height);
|
|
1906
|
+
const target = {
|
|
1907
|
+
x: bounds2.x + bounds2.width / 2,
|
|
1908
|
+
y: bounds2.y + bounds2.height / 2
|
|
1909
|
+
};
|
|
1910
|
+
const distance = span * 1.55;
|
|
1911
|
+
const height = Math.max(span * 0.82, Math.max(0, maxSurfaceHeightWorld) + span * 0.45);
|
|
1912
|
+
return {
|
|
1913
|
+
target,
|
|
1914
|
+
distance,
|
|
1915
|
+
height,
|
|
1916
|
+
focalLength: Math.hypot(distance, height),
|
|
1917
|
+
yawRad: -8 * Math.PI / 180
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function projectPerspectivePoint(camera, point, heightWorld = 0) {
|
|
1921
|
+
const dx = point.x - camera.target.x;
|
|
1922
|
+
const dy = point.y - camera.target.y;
|
|
1923
|
+
const cos = Math.cos(camera.yawRad);
|
|
1924
|
+
const sin = Math.sin(camera.yawRad);
|
|
1925
|
+
const x = dx * cos - dy * sin;
|
|
1926
|
+
const y = dx * sin + dy * cos;
|
|
1927
|
+
const translatedY = y - camera.distance;
|
|
1928
|
+
const translatedZ = heightWorld - camera.height;
|
|
1929
|
+
const cameraLength = Math.hypot(camera.distance, camera.height);
|
|
1930
|
+
const depth = (-camera.distance * translatedY - camera.height * translatedZ) / cameraLength;
|
|
1931
|
+
const up = (-camera.height * translatedY + camera.distance * translatedZ) / cameraLength;
|
|
1932
|
+
const safeDepth = Math.max(cameraLength * 0.08, depth);
|
|
1933
|
+
const scale = camera.focalLength / safeDepth;
|
|
1934
|
+
return {
|
|
1935
|
+
x: camera.target.x + x * scale,
|
|
1936
|
+
y: camera.target.y - up * scale,
|
|
1937
|
+
depth: safeDepth,
|
|
1938
|
+
scale
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function applyAffine(affine, point) {
|
|
1942
|
+
return {
|
|
1943
|
+
x: affine.a * point.x + affine.c * point.y + affine.e,
|
|
1944
|
+
y: affine.b * point.x + affine.d * point.y + affine.f
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function invertAffine(affine) {
|
|
1948
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
1949
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-9) {
|
|
1950
|
+
throw new Error("perspective projection produced a singular affine");
|
|
1951
|
+
}
|
|
1952
|
+
const a = affine.d / determinant;
|
|
1953
|
+
const b = -affine.b / determinant;
|
|
1954
|
+
const c = -affine.c / determinant;
|
|
1955
|
+
const d = affine.a / determinant;
|
|
1956
|
+
return {
|
|
1957
|
+
a,
|
|
1958
|
+
b,
|
|
1959
|
+
c,
|
|
1960
|
+
d,
|
|
1961
|
+
e: -(a * affine.e + c * affine.f),
|
|
1962
|
+
f: -(b * affine.e + d * affine.f)
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function composeAffine(left, right) {
|
|
1966
|
+
return {
|
|
1967
|
+
a: left.a * right.a + left.c * right.b,
|
|
1968
|
+
b: left.b * right.a + left.d * right.b,
|
|
1969
|
+
c: left.a * right.c + left.c * right.d,
|
|
1970
|
+
d: left.b * right.c + left.d * right.d,
|
|
1971
|
+
e: left.a * right.e + left.c * right.f + left.e,
|
|
1972
|
+
f: left.b * right.e + left.d * right.f + left.f
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function perspectiveTangentAffine(camera, anchor, heightAt = () => 0) {
|
|
1976
|
+
const origin = projectPerspectivePoint(camera, anchor, heightAt(anchor));
|
|
1977
|
+
const alongXPoint = { x: anchor.x + 1, y: anchor.y };
|
|
1978
|
+
const alongYPoint = { x: anchor.x, y: anchor.y + 1 };
|
|
1979
|
+
const alongX = projectPerspectivePoint(camera, alongXPoint, heightAt(alongXPoint));
|
|
1980
|
+
const alongY = projectPerspectivePoint(camera, alongYPoint, heightAt(alongYPoint));
|
|
1981
|
+
const a = alongX.x - origin.x;
|
|
1982
|
+
const b = alongX.y - origin.y;
|
|
1983
|
+
const c = alongY.x - origin.x;
|
|
1984
|
+
const d = alongY.y - origin.y;
|
|
1985
|
+
return {
|
|
1986
|
+
a,
|
|
1987
|
+
b,
|
|
1988
|
+
c,
|
|
1989
|
+
d,
|
|
1990
|
+
e: origin.x - a * anchor.x - c * anchor.y,
|
|
1991
|
+
f: origin.y - b * anchor.x - d * anchor.y
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
function decomposeAffineLinear(affine) {
|
|
1995
|
+
const scaleX = Math.hypot(affine.a, affine.b);
|
|
1996
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
1997
|
+
return {
|
|
1998
|
+
rotationDeg: Math.atan2(affine.b, affine.a) * 180 / Math.PI,
|
|
1999
|
+
scaleX,
|
|
2000
|
+
scaleY: determinant / Math.max(scaleX, 1e-9),
|
|
2001
|
+
skewX: (affine.a * affine.c + affine.b * affine.d) / Math.max(determinant, 1e-9)
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1246
2005
|
// src/lib/money.ts
|
|
1247
2006
|
var DEFAULT_CURRENCY = "USD";
|
|
1248
2007
|
var displayLocale;
|
|
@@ -1288,6 +2047,7 @@ var en = {
|
|
|
1288
2047
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
1289
2048
|
"map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
|
|
1290
2049
|
"map.seatsLeft": "{count} LEFT",
|
|
2050
|
+
"map.soldOut": "SOLD OUT",
|
|
1291
2051
|
"map.fromPrice": "FROM {price}",
|
|
1292
2052
|
"map.statusHeld": "On hold",
|
|
1293
2053
|
"map.statusTaken": "Taken",
|
|
@@ -1295,7 +2055,7 @@ var en = {
|
|
|
1295
2055
|
"picker.floor": "Floor",
|
|
1296
2056
|
"picker.zoomLevel": "Zoom level",
|
|
1297
2057
|
"picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
|
|
1298
|
-
"picker.rungTip.sections": "Section blocks \u2014
|
|
2058
|
+
"picker.rungTip.sections": "Section blocks \u2014 sold-out and nearly-gone sections shade to show availability at a glance",
|
|
1299
2059
|
"picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
|
|
1300
2060
|
"picker.rungLabel.zones": "ZONES",
|
|
1301
2061
|
"picker.rungLabel.sections": "SECTIONS",
|
|
@@ -1363,6 +2123,25 @@ function formatDate(value, opts) {
|
|
|
1363
2123
|
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
1364
2124
|
}
|
|
1365
2125
|
|
|
2126
|
+
// src/core/shapeLineStyle.ts
|
|
2127
|
+
var DEFAULT_SHAPE_LINE_CAP = "round";
|
|
2128
|
+
var DEFAULT_SHAPE_LINE_JOIN = "round";
|
|
2129
|
+
var DEFAULT_SHAPE_LINE_ENDING = "none";
|
|
2130
|
+
function resolvedShapeLineStyle(shape) {
|
|
2131
|
+
return {
|
|
2132
|
+
lineCap: shape.lineCap ?? DEFAULT_SHAPE_LINE_CAP,
|
|
2133
|
+
lineJoin: shape.lineJoin ?? DEFAULT_SHAPE_LINE_JOIN,
|
|
2134
|
+
startEnding: shape.startEnding ?? DEFAULT_SHAPE_LINE_ENDING,
|
|
2135
|
+
endEnding: shape.endEnding ?? DEFAULT_SHAPE_LINE_ENDING
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
function shapeArrowMetrics(strokeWidth) {
|
|
2139
|
+
return {
|
|
2140
|
+
pointerLength: Math.min(48, Math.max(8, strokeWidth * 3)),
|
|
2141
|
+
pointerWidth: Math.min(40, Math.max(8, strokeWidth * 2.25))
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1366
2145
|
// src/engine/SeatmapRenderer.ts
|
|
1367
2146
|
var SEAT_RADIUS = 9;
|
|
1368
2147
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
@@ -1370,17 +2149,18 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
1370
2149
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1371
2150
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
1372
2151
|
var SEAT_TAP_SLOP_PX = 14;
|
|
1373
|
-
var
|
|
2152
|
+
var WHEELCHAIR_GLYPH_MIN_PX = 10;
|
|
2153
|
+
var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
|
|
1374
2154
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
1375
2155
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
1376
2156
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
1377
2157
|
var PAN_START_SLOP_PX = 8;
|
|
2158
|
+
var GHOST_CLICK_MS = 700;
|
|
1378
2159
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
1379
2160
|
var MAX_LABELS = 700;
|
|
1380
2161
|
var MARQUEE_RING_CAP = 2500;
|
|
1381
2162
|
var ISO_ANGLE_DEG = -11.5;
|
|
1382
2163
|
var ISO_SQUASH = 0.58;
|
|
1383
|
-
var LIFT_PER_STEP = 58;
|
|
1384
2164
|
var ISO_TWEEN_MS = 320;
|
|
1385
2165
|
var CAMERA_GLIDE_MS = 650;
|
|
1386
2166
|
var BLOCK_FILL_ALPHA = 1;
|
|
@@ -1417,6 +2197,10 @@ var DEF_SELECTION = "#ffffff";
|
|
|
1417
2197
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1418
2198
|
var DEF_DECOR_FILL = "#232c40";
|
|
1419
2199
|
var DEF_TEXT = "#8b93a7";
|
|
2200
|
+
function konvaFontStyle(bold, italic, defaultWeight = "normal") {
|
|
2201
|
+
const weight = bold ? "bold" : defaultWeight;
|
|
2202
|
+
return italic ? `italic ${weight}`.trim() : weight;
|
|
2203
|
+
}
|
|
1420
2204
|
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1421
2205
|
function colorLuminance(color) {
|
|
1422
2206
|
const s = color.trim();
|
|
@@ -1472,6 +2256,22 @@ function overviewPalette(canvasBackground) {
|
|
|
1472
2256
|
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
1473
2257
|
};
|
|
1474
2258
|
}
|
|
2259
|
+
var SECTION_NEARLY_GONE_RATIO = 0.1;
|
|
2260
|
+
function overviewStateFill(palette, state) {
|
|
2261
|
+
const base = palette.sectionFill;
|
|
2262
|
+
const soldOut = desaturate(darken(base, 0.2), 0.85);
|
|
2263
|
+
switch (state) {
|
|
2264
|
+
case "closed":
|
|
2265
|
+
return darken(base, 0.12);
|
|
2266
|
+
case "sold-out":
|
|
2267
|
+
return soldOut;
|
|
2268
|
+
case "nearly-gone":
|
|
2269
|
+
return lerpColor(base, soldOut, 0.4);
|
|
2270
|
+
case "normal":
|
|
2271
|
+
default:
|
|
2272
|
+
return base;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
1475
2275
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
1476
2276
|
function seatIdOf(target) {
|
|
1477
2277
|
const n = target;
|
|
@@ -1494,6 +2294,13 @@ function darken(hex, amt) {
|
|
|
1494
2294
|
const mix = (c) => Math.round(c * (1 - amt));
|
|
1495
2295
|
return `#${(1 << 24 | mix(n >> 16 & 255) << 16 | mix(n >> 8 & 255) << 8 | mix(n & 255)).toString(16).slice(1)}`;
|
|
1496
2296
|
}
|
|
2297
|
+
function desaturate(hex, amt) {
|
|
2298
|
+
const rgb2 = hexToRgb(hex);
|
|
2299
|
+
if (!rgb2) return hex;
|
|
2300
|
+
const [r, g, b] = rgb2;
|
|
2301
|
+
const grey = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
2302
|
+
return toHex(r + (grey - r) * amt, g + (grey - g) * amt, b + (grey - b) * amt);
|
|
2303
|
+
}
|
|
1497
2304
|
function polyBounds(pts) {
|
|
1498
2305
|
let minX = Infinity;
|
|
1499
2306
|
let minY = Infinity;
|
|
@@ -1522,8 +2329,8 @@ function rotatedRectPoints(center, width, height, rotation) {
|
|
|
1522
2329
|
}));
|
|
1523
2330
|
}
|
|
1524
2331
|
function pointsBounds(points) {
|
|
1525
|
-
const
|
|
1526
|
-
return { x:
|
|
2332
|
+
const bounds2 = polyBounds(points);
|
|
2333
|
+
return { x: bounds2.x, y: bounds2.y, width: bounds2.width, height: bounds2.height };
|
|
1527
2334
|
}
|
|
1528
2335
|
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1529
2336
|
const radians = rotation * Math.PI / 180;
|
|
@@ -1543,32 +2350,50 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
|
1543
2350
|
return true;
|
|
1544
2351
|
}
|
|
1545
2352
|
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1546
|
-
const
|
|
1547
|
-
const centre = { x:
|
|
2353
|
+
const bounds2 = polyBounds(outer);
|
|
2354
|
+
const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
|
|
1548
2355
|
const points = [preferred];
|
|
1549
2356
|
for (let row = 1; row < 12; row += 1) {
|
|
1550
2357
|
for (let column = 1; column < 12; column += 1) {
|
|
1551
2358
|
const point = {
|
|
1552
|
-
x:
|
|
1553
|
-
y:
|
|
2359
|
+
x: bounds2.x + bounds2.width * column / 12,
|
|
2360
|
+
y: bounds2.y + bounds2.height * row / 12
|
|
1554
2361
|
};
|
|
1555
2362
|
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1556
2363
|
}
|
|
1557
2364
|
}
|
|
1558
2365
|
return points.sort((left, right) => Math.hypot(left.x - centre.x, left.y - centre.y) - Math.hypot(right.x - centre.x, right.y - centre.y)).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
|
|
1559
2366
|
}
|
|
2367
|
+
function roundedPolygonSubpath(context, points, radius) {
|
|
2368
|
+
const n = points.length;
|
|
2369
|
+
const start = { x: (points[n - 1].x + points[0].x) / 2, y: (points[n - 1].y + points[0].y) / 2 };
|
|
2370
|
+
context.moveTo(start.x, start.y);
|
|
2371
|
+
for (let i = 0; i < n; i += 1) {
|
|
2372
|
+
const curr = points[i];
|
|
2373
|
+
const next = points[(i + 1) % n];
|
|
2374
|
+
const prev = points[(i - 1 + n) % n];
|
|
2375
|
+
const rMax = Math.min(
|
|
2376
|
+
Math.hypot(curr.x - prev.x, curr.y - prev.y) / 2,
|
|
2377
|
+
Math.hypot(next.x - curr.x, next.y - curr.y) / 2
|
|
2378
|
+
);
|
|
2379
|
+
context.arcTo(curr.x, curr.y, next.x, next.y, Math.max(0, Math.min(radius, rMax)));
|
|
2380
|
+
}
|
|
2381
|
+
context.closePath();
|
|
2382
|
+
}
|
|
1560
2383
|
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1561
2384
|
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1562
2385
|
const next = points[(index + 1) % points.length];
|
|
1563
2386
|
return sum + point.x * next.y - next.x * point.y;
|
|
1564
2387
|
}, 0);
|
|
1565
2388
|
const outerClockwise = signedArea(outer) > 0;
|
|
2389
|
+
const cornerRadius = attrs.cornerRadius && attrs.cornerRadius > 0 ? attrs.cornerRadius : 0;
|
|
1566
2390
|
return new Shape({
|
|
1567
2391
|
...attrs,
|
|
1568
2392
|
sceneFunc(context, shape) {
|
|
1569
2393
|
context.beginPath();
|
|
1570
2394
|
const polygonPath = (points) => {
|
|
1571
2395
|
if (!points.length) return;
|
|
2396
|
+
if (cornerRadius > 0 && points.length >= 3) return roundedPolygonSubpath(context, points, cornerRadius);
|
|
1572
2397
|
context.moveTo(points[0].x, points[0].y);
|
|
1573
2398
|
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1574
2399
|
context.closePath();
|
|
@@ -1615,6 +2440,33 @@ function rgba(hex, a) {
|
|
|
1615
2440
|
const n = parseInt(m[1], 16);
|
|
1616
2441
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1617
2442
|
}
|
|
2443
|
+
var ViewportGroup = class extends Group {
|
|
2444
|
+
constructor() {
|
|
2445
|
+
super(...arguments);
|
|
2446
|
+
this.viewportCulled = false;
|
|
2447
|
+
}
|
|
2448
|
+
setViewportCulled(culled) {
|
|
2449
|
+
if (culled === this.viewportCulled) return;
|
|
2450
|
+
this.viewportCulled = culled;
|
|
2451
|
+
if (!culled) super._clearSelfAndDescendantCache();
|
|
2452
|
+
}
|
|
2453
|
+
isViewportCulled() {
|
|
2454
|
+
return this.viewportCulled;
|
|
2455
|
+
}
|
|
2456
|
+
drawScene(...args) {
|
|
2457
|
+
return this.viewportCulled ? this : super.drawScene(...args);
|
|
2458
|
+
}
|
|
2459
|
+
drawHit(...args) {
|
|
2460
|
+
return this.viewportCulled ? this : super.drawHit(...args);
|
|
2461
|
+
}
|
|
2462
|
+
_clearSelfAndDescendantCache(attr) {
|
|
2463
|
+
if (!this.viewportCulled) {
|
|
2464
|
+
super._clearSelfAndDescendantCache(attr);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
this._clearCache(attr);
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
1618
2470
|
function hexToRgb(hex) {
|
|
1619
2471
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1620
2472
|
if (!m) return null;
|
|
@@ -1645,8 +2497,15 @@ function lerpColor(a, b, t2) {
|
|
|
1645
2497
|
}
|
|
1646
2498
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
1647
2499
|
constructor(container, options = {}) {
|
|
2500
|
+
/** Per-elevated-section label containers. Keeping the labels grouped lets the
|
|
2501
|
+
* Phase-B lift move thousands of seat labels with one transform per section
|
|
2502
|
+
* instead of rewriting every label on every tween frame. */
|
|
2503
|
+
this.labelLiftGroups = /* @__PURE__ */ new Map();
|
|
1648
2504
|
this.seats = [];
|
|
1649
2505
|
this.seatById = /* @__PURE__ */ new Map();
|
|
2506
|
+
/** One build per chart/floor; section membership queries only inspect seats in
|
|
2507
|
+
* the section bounds instead of rescanning the full 13k venue per section. */
|
|
2508
|
+
this.seatIndex = null;
|
|
1650
2509
|
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
1651
2510
|
this.chartDoc = null;
|
|
1652
2511
|
this.activeFloorId = "";
|
|
@@ -1662,15 +2521,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1662
2521
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1663
2522
|
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1664
2523
|
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1665
|
-
/** Centred
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
2524
|
+
/** Centred ♿ glyph per physical wheelchair provision. It keeps a small
|
|
2525
|
+
* screen-space floor at every LOD and grows when the Wheelchair filter is
|
|
2526
|
+
* active. Kept in a map so zoom updates touch only the handful of matching
|
|
2527
|
+
* seats, never all 13k nodes. */
|
|
1669
2528
|
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1670
|
-
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1671
|
-
this.accessGlyphVisible = false;
|
|
1672
2529
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1673
2530
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
2531
|
+
/** Vector venue-icon Path nodes + authored size; obey the free-text size floor. */
|
|
2532
|
+
this.iconNodeById = /* @__PURE__ */ new Map();
|
|
1674
2533
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1675
2534
|
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1676
2535
|
/** GA paint and text share price/highlight filter state. */
|
|
@@ -1684,6 +2543,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1684
2543
|
this.effSelection = DEF_SELECTION;
|
|
1685
2544
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
1686
2545
|
this.colorblind = false;
|
|
2546
|
+
/** Stable per-category-KEY colorblind hue (OV-46) — order-independent. */
|
|
2547
|
+
this.cbHueByKey = /* @__PURE__ */ new Map();
|
|
2548
|
+
/** OV-45(a): authored row-letter labels for the buyer/preview map, resolved in
|
|
2549
|
+
* world space (matching seat labels) and honouring each row's labelPresentation
|
|
2550
|
+
* (position/rotation/visibility/style). Painted at the seats LOD rung. */
|
|
2551
|
+
this.rowLabelPlan = [];
|
|
1687
2552
|
/** Category order from the doc — the stable index into the CB palette. */
|
|
1688
2553
|
this.catOrder = [];
|
|
1689
2554
|
this.selection = /* @__PURE__ */ new Set();
|
|
@@ -1693,6 +2558,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1693
2558
|
this.ownedHold = /* @__PURE__ */ new Set();
|
|
1694
2559
|
/** One selected seat being inspected before it is committed to the cart. */
|
|
1695
2560
|
this.selectionFocusId = null;
|
|
2561
|
+
/** Seat currently owning the shared hover ring (null while not hovering). */
|
|
2562
|
+
this.hoveredId = null;
|
|
1696
2563
|
this.focusedId = null;
|
|
1697
2564
|
/**
|
|
1698
2565
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -1711,6 +2578,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1711
2578
|
this.sections = [];
|
|
1712
2579
|
this.zones = [];
|
|
1713
2580
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
2581
|
+
/** Seats outside every authored section retain the legacy path in one group. */
|
|
2582
|
+
this.unsectionedSeatGroup = null;
|
|
1714
2583
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1715
2584
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1716
2585
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
@@ -1723,6 +2592,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1723
2592
|
this.focusedSectionId = null;
|
|
1724
2593
|
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1725
2594
|
this.focusBackdrop = null;
|
|
2595
|
+
/** Non-listening visual dim outside the focused section. One overlay replaces
|
|
2596
|
+
* descendant opacity mutations across every seat in the venue. */
|
|
2597
|
+
this.focusDimOverlay = null;
|
|
1726
2598
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1727
2599
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1728
2600
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1735,10 +2607,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1735
2607
|
this.isoT = 0;
|
|
1736
2608
|
this.isoTarget = 0;
|
|
1737
2609
|
this.isoRaf = 0;
|
|
2610
|
+
/** Public view target; perspective is a separate non-affine Phase-C lane. */
|
|
2611
|
+
this.viewMode = "flat";
|
|
2612
|
+
this.perspectiveCamera = null;
|
|
2613
|
+
/** Ground-plane tangent applied once to every renderer layer. */
|
|
2614
|
+
this.perspectiveBaseAffine = null;
|
|
2615
|
+
this.perspectiveBaseInverse = null;
|
|
2616
|
+
/** Exact pinhole anchor expressed in the overlay tangent-layer coordinates. */
|
|
2617
|
+
this.perspectiveSeatLocal = /* @__PURE__ */ new Map();
|
|
2618
|
+
/** Exact projected point (before Stage pan/zoom), keyed by seat. */
|
|
2619
|
+
this.perspectiveSeatProjected = /* @__PURE__ */ new Map();
|
|
2620
|
+
/** Positive camera depth, used for deterministic far→near paint order. */
|
|
2621
|
+
this.perspectiveSeatDepth = /* @__PURE__ */ new Map();
|
|
2622
|
+
/** Exact pinhole size ratio at each seat anchor. */
|
|
2623
|
+
this.perspectiveSeatScale = /* @__PURE__ */ new Map();
|
|
2624
|
+
/** Seats whose native Konva nodes currently carry the exact projected pose.
|
|
2625
|
+
* Large sectioned venues apply these lazily as a section enters the live-seat
|
|
2626
|
+
* viewport; the overview rung hides the seat layer completely. */
|
|
2627
|
+
this.perspectiveAppliedSeats = /* @__PURE__ */ new Set();
|
|
2628
|
+
this.perspectiveBounds = null;
|
|
1738
2629
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
1739
2630
|
this.isoCentre = { x: 0, y: 0 };
|
|
1740
2631
|
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
1741
2632
|
this.glideRaf = 0;
|
|
2633
|
+
/** While the camera tween is active, defer polygon label fitting/collision to
|
|
2634
|
+
* the settled frame. Geometry remains smoothly stage-scaled, while expensive
|
|
2635
|
+
* point-in-polygon text work no longer repeats at 120 Hz. */
|
|
2636
|
+
this.glideInProgress = false;
|
|
1742
2637
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
1743
2638
|
this.destroyed = false;
|
|
1744
2639
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -1768,6 +2663,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1768
2663
|
this.panStarted = false;
|
|
1769
2664
|
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
1770
2665
|
this.moved = 0;
|
|
2666
|
+
/**
|
|
2667
|
+
* Ghost-click guard. Konva binds `_pointerup` to mouseup, touchend AND
|
|
2668
|
+
* pointerup, so `on('click tap')` is two handlers for one finger tap unless
|
|
2669
|
+
* the browser's synthesized compatibility click is suppressed. Konva only
|
|
2670
|
+
* suppresses it when the touch lands on a LISTENING SHAPE (Stage.js returns
|
|
2671
|
+
* early before its preventDefault() when `!shape || !shape.isListening()`),
|
|
2672
|
+
* so a near-miss rescued by nearestSeatToScreen used to fire `tap` (select)
|
|
2673
|
+
* then `click` (deselect) 1-3ms apart and net to nothing. With
|
|
2674
|
+
* SEAT_TAP_SLOP_PX=14 against a ~12px seat radius, that broken annulus is
|
|
2675
|
+
* ~3.7x the area of the seat itself — i.e. most successful mobile taps.
|
|
2676
|
+
* `touch-action: none` does NOT suppress compatibility mouse events.
|
|
2677
|
+
*/
|
|
2678
|
+
this.lastTapAt = 0;
|
|
1771
2679
|
/**
|
|
1772
2680
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
1773
2681
|
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
@@ -1864,6 +2772,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1864
2772
|
x: mid.x - this.pinch.worldMid.x * scale,
|
|
1865
2773
|
y: mid.y - this.pinch.worldMid.y * scale
|
|
1866
2774
|
});
|
|
2775
|
+
this.updateSeatGroupVisibility();
|
|
1867
2776
|
this.stage.batchDraw();
|
|
1868
2777
|
this.scheduleViewChange();
|
|
1869
2778
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
@@ -1876,6 +2785,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1876
2785
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
1877
2786
|
});
|
|
1878
2787
|
this.panLast = p;
|
|
2788
|
+
this.updateSeatGroupVisibility();
|
|
1879
2789
|
this.stage.batchDraw();
|
|
1880
2790
|
this.scheduleViewChange();
|
|
1881
2791
|
}
|
|
@@ -1928,6 +2838,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1928
2838
|
this.overlayLayer = new Layer({ listening: false });
|
|
1929
2839
|
this.labelGroup = new Group({ listening: false });
|
|
1930
2840
|
this.overlayLayer.add(this.labelGroup);
|
|
2841
|
+
this.fgDecorGroup = new Group({ listening: false });
|
|
2842
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1931
2843
|
this.hoverRing = new Circle({
|
|
1932
2844
|
radius: SEAT_RADIUS + 2,
|
|
1933
2845
|
stroke: "#ffffff",
|
|
@@ -1962,6 +2874,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1962
2874
|
this.resizeObs.observe(container);
|
|
1963
2875
|
}
|
|
1964
2876
|
}
|
|
2877
|
+
/**
|
|
2878
|
+
* True when this is the browser's synthesized compatibility click following a
|
|
2879
|
+
* finger tap we already handled. Discriminates on `e.type` deliberately —
|
|
2880
|
+
* `PointerEvent extends MouseEvent`, so an `instanceof MouseEvent` test
|
|
2881
|
+
* matches genuine pointer taps too. One shared timestamp across the layer and
|
|
2882
|
+
* stage handlers: a tap consumed at the layer must also suppress the ghost at
|
|
2883
|
+
* the stage, and only `click` is ever rejected, so bubbling stays intact.
|
|
2884
|
+
*/
|
|
2885
|
+
isGhostClick(e) {
|
|
2886
|
+
if (e.type === "tap") {
|
|
2887
|
+
this.lastTapAt = performance.now();
|
|
2888
|
+
return false;
|
|
2889
|
+
}
|
|
2890
|
+
return this.lastTapAt > 0 && performance.now() - this.lastTapAt < GHOST_CLICK_MS;
|
|
2891
|
+
}
|
|
1965
2892
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
1966
2893
|
setChart(doc, opts) {
|
|
1967
2894
|
if (doc !== this.chartDoc) this.stacked = false;
|
|
@@ -1975,8 +2902,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1975
2902
|
this.focusedId = null;
|
|
1976
2903
|
this.focusRing.visible(false);
|
|
1977
2904
|
this.bgLayer.destroyChildren();
|
|
2905
|
+
this.focusDimOverlay?.destroy();
|
|
2906
|
+
this.focusDimOverlay = null;
|
|
2907
|
+
this.seatLayer.clearCache();
|
|
2908
|
+
this.seatLayer.listening(true);
|
|
1978
2909
|
this.seatLayer.destroyChildren();
|
|
2910
|
+
this.unsectionedSeatGroup = null;
|
|
1979
2911
|
this.labelGroup.destroyChildren();
|
|
2912
|
+
this.labelLiftGroups.clear();
|
|
2913
|
+
this.fgDecorGroup.destroyChildren();
|
|
1980
2914
|
this.circleById.clear();
|
|
1981
2915
|
this.boothDims.clear();
|
|
1982
2916
|
this.boothLabelById.clear();
|
|
@@ -1984,6 +2918,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1984
2918
|
this.accessRingById.clear();
|
|
1985
2919
|
this.accessGlyphById.clear();
|
|
1986
2920
|
this.freeTextById.clear();
|
|
2921
|
+
this.iconNodeById.clear();
|
|
1987
2922
|
this.primaryFocalLabels.clear();
|
|
1988
2923
|
this.gaById.clear();
|
|
1989
2924
|
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
@@ -1993,6 +2928,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1993
2928
|
this.statusById.clear();
|
|
1994
2929
|
this.selection.clear();
|
|
1995
2930
|
this.seatById.clear();
|
|
2931
|
+
this.seatIndex = null;
|
|
1996
2932
|
this.cached = false;
|
|
1997
2933
|
this.accessFilter = null;
|
|
1998
2934
|
this.sections = [];
|
|
@@ -2010,11 +2946,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2010
2946
|
}
|
|
2011
2947
|
this.isoT = 0;
|
|
2012
2948
|
this.isoTarget = 0;
|
|
2949
|
+
this.viewMode = "flat";
|
|
2950
|
+
this.perspectiveCamera = null;
|
|
2951
|
+
this.perspectiveBaseAffine = null;
|
|
2952
|
+
this.perspectiveBaseInverse = null;
|
|
2953
|
+
this.perspectiveSeatLocal.clear();
|
|
2954
|
+
this.perspectiveSeatProjected.clear();
|
|
2955
|
+
this.perspectiveSeatDepth.clear();
|
|
2956
|
+
this.perspectiveSeatScale.clear();
|
|
2957
|
+
this.perspectiveAppliedSeats.clear();
|
|
2958
|
+
this.perspectiveBounds = null;
|
|
2013
2959
|
this.resetLayerTransforms();
|
|
2014
2960
|
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
2015
2961
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
2016
2962
|
this.seatLayer.opacity(1);
|
|
2017
2963
|
this.hoverRing.visible(false);
|
|
2964
|
+
this.hoveredId = null;
|
|
2018
2965
|
this.theme = doc.theme ?? {};
|
|
2019
2966
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
2020
2967
|
this.container.style.background = "";
|
|
@@ -2026,35 +2973,80 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2026
2973
|
this.catColor.clear();
|
|
2027
2974
|
this.catPrice.clear();
|
|
2028
2975
|
this.catOrder = doc.categories.map((c) => c.key);
|
|
2976
|
+
this.cbHueByKey.clear();
|
|
2977
|
+
[...this.catOrder].sort().forEach((key, i) => {
|
|
2978
|
+
this.cbHueByKey.set(key, CB_PALETTE[i % CB_PALETTE.length]);
|
|
2979
|
+
});
|
|
2029
2980
|
for (const c of doc.categories) {
|
|
2030
2981
|
this.catColor.set(c.key, c.color);
|
|
2031
2982
|
if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
|
|
2032
2983
|
}
|
|
2033
2984
|
for (const obj of view.objects) {
|
|
2034
2985
|
if (obj.type === "booth") {
|
|
2035
|
-
this.boothDims.set(obj.id, {
|
|
2986
|
+
this.boothDims.set(obj.id, {
|
|
2987
|
+
width: obj.width,
|
|
2988
|
+
height: obj.height,
|
|
2989
|
+
rotation: obj.rotation,
|
|
2990
|
+
...obj.points && obj.points.length >= 3 ? { points: obj.points.map((p) => ({ x: p.x, y: p.y })) } : {}
|
|
2991
|
+
});
|
|
2036
2992
|
}
|
|
2037
2993
|
}
|
|
2038
|
-
this.seats = expandChart(view
|
|
2994
|
+
this.seats = expandChart(view, {
|
|
2995
|
+
// A stacked overview is presentation-only and never opens a seat POV. The
|
|
2996
|
+
// per-section renderer below still resolves each object's real floor base.
|
|
2997
|
+
floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
|
|
2998
|
+
});
|
|
2039
2999
|
for (const s of this.seats) {
|
|
2040
3000
|
this.seatById.set(s.id, s);
|
|
2041
3001
|
this.statusById.set(s.id, "free");
|
|
2042
3002
|
}
|
|
3003
|
+
this.seatIndex = buildSeatIndex(this.seats, { seatRadius: this.seatR });
|
|
3004
|
+
this.bounds = chartBounds(view);
|
|
3005
|
+
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3006
|
+
this.buildPerspectiveProjection(view);
|
|
2043
3007
|
this.renderBackground(view);
|
|
3008
|
+
this.unsectionedSeatGroup = new Group({ listening: true });
|
|
3009
|
+
this.seatLayer.add(this.unsectionedSeatGroup);
|
|
2044
3010
|
this.renderSeats();
|
|
3011
|
+
this.buildRowLabelPlan(view);
|
|
2045
3012
|
this.overlayLayer.add(this.labelGroup);
|
|
3013
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
2046
3014
|
this.overlayLayer.add(this.hoverRing);
|
|
2047
|
-
this.
|
|
2048
|
-
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
3015
|
+
this.overlayLayer.add(this.focusRing);
|
|
2049
3016
|
this.zoomToFit();
|
|
2050
3017
|
}
|
|
2051
3018
|
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
2052
3019
|
* the whole chart for single-floor charts. */
|
|
2053
3020
|
floorView(doc) {
|
|
2054
|
-
if (!doc.floors || !doc.floors.length) return
|
|
2055
|
-
|
|
3021
|
+
if (!doc.floors || !doc.floors.length) return {
|
|
3022
|
+
...doc,
|
|
3023
|
+
referenceImage: void 0,
|
|
3024
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3025
|
+
};
|
|
3026
|
+
if (this.stacked && doc.floors.length >= 2) {
|
|
3027
|
+
return {
|
|
3028
|
+
...stackFloors(doc),
|
|
3029
|
+
referenceImage: void 0,
|
|
3030
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
2056
3033
|
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
2057
|
-
return {
|
|
3034
|
+
return {
|
|
3035
|
+
...doc,
|
|
3036
|
+
objects: floor.objects,
|
|
3037
|
+
focalPoint: floor.focalPoint,
|
|
3038
|
+
referenceImage: void 0,
|
|
3039
|
+
backgroundImage: buyerBackgroundImage(floor),
|
|
3040
|
+
floors: void 0
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
/** Physical floor height is authored data; the 900-unit stacked overview
|
|
3044
|
+
* spread is presentation-only and must never leak into real 3D geometry. */
|
|
3045
|
+
floorBaseHeightMFor(objectId) {
|
|
3046
|
+
const floors = this.chartDoc?.floors;
|
|
3047
|
+
if (!floors?.length) return 0;
|
|
3048
|
+
const floorId = this.stacked && objectId ? this.objectFloor.get(objectId) ?? this.activeFloorId : this.activeFloorId;
|
|
3049
|
+
return floors.find((floor) => floor.id === floorId)?.baseHeightM ?? 0;
|
|
2058
3050
|
}
|
|
2059
3051
|
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
2060
3052
|
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
@@ -2367,7 +3359,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2367
3359
|
}
|
|
2368
3360
|
const ids = [];
|
|
2369
3361
|
for (const seat of this.seats) {
|
|
2370
|
-
|
|
3362
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3363
|
+
if (rendered.x < x0 || rendered.x > x1 || rendered.y < y0 || rendered.y > y1) continue;
|
|
2371
3364
|
if (!this.isSelectable(seat.id)) continue;
|
|
2372
3365
|
ids.push(seat.id);
|
|
2373
3366
|
}
|
|
@@ -2377,9 +3370,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2377
3370
|
flashSeat(seatId, color = "#f43f5e") {
|
|
2378
3371
|
const seat = this.seatById.get(seatId);
|
|
2379
3372
|
if (!seat) return;
|
|
3373
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
2380
3374
|
const ring = new Circle({
|
|
2381
|
-
x:
|
|
2382
|
-
y:
|
|
3375
|
+
x: rendered.x,
|
|
3376
|
+
y: rendered.y,
|
|
2383
3377
|
radius: this.seatR,
|
|
2384
3378
|
stroke: color,
|
|
2385
3379
|
strokeWidth: 3,
|
|
@@ -2388,6 +3382,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2388
3382
|
perfectDrawEnabled: false,
|
|
2389
3383
|
shadowForStrokeEnabled: false
|
|
2390
3384
|
});
|
|
3385
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seatId) ?? 1 : 1;
|
|
3386
|
+
ring.scale({ x: projectionScale, y: projectionScale });
|
|
2391
3387
|
this.overlayLayer.add(ring);
|
|
2392
3388
|
const start = performance.now();
|
|
2393
3389
|
const dur = 620;
|
|
@@ -2414,17 +3410,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2414
3410
|
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2415
3411
|
if (!matches.length) return;
|
|
2416
3412
|
for (const section of matches) {
|
|
2417
|
-
const
|
|
3413
|
+
const outline = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
3414
|
+
const lift = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
3415
|
+
return { x: point.x + lift.x, y: point.y + lift.y };
|
|
3416
|
+
});
|
|
3417
|
+
const centre = outline.reduce(
|
|
2418
3418
|
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2419
3419
|
{ x: 0, y: 0 }
|
|
2420
3420
|
);
|
|
2421
|
-
centre.x /=
|
|
2422
|
-
centre.y /=
|
|
2423
|
-
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
3421
|
+
centre.x /= outline.length;
|
|
3422
|
+
centre.y /= outline.length;
|
|
2424
3423
|
const halo = new Line({
|
|
2425
|
-
x: centre.x
|
|
2426
|
-
y: centre.y
|
|
2427
|
-
points:
|
|
3424
|
+
x: centre.x,
|
|
3425
|
+
y: centre.y,
|
|
3426
|
+
points: outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2428
3427
|
closed: true,
|
|
2429
3428
|
stroke: color,
|
|
2430
3429
|
strokeWidth: 3,
|
|
@@ -2468,12 +3467,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2468
3467
|
nearestSeat(fromId, dir) {
|
|
2469
3468
|
const from = this.seatById.get(fromId);
|
|
2470
3469
|
if (!from) return null;
|
|
3470
|
+
const fromPoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(from.id) ?? from : from;
|
|
2471
3471
|
let best = null;
|
|
2472
3472
|
let bestScore = Infinity;
|
|
2473
3473
|
for (const s of this.seats) {
|
|
2474
3474
|
if (s.id === fromId) continue;
|
|
2475
|
-
const
|
|
2476
|
-
const
|
|
3475
|
+
const candidatePoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(s.id) ?? s : s;
|
|
3476
|
+
const dx = candidatePoint.x - fromPoint.x;
|
|
3477
|
+
const dy = candidatePoint.y - fromPoint.y;
|
|
2477
3478
|
const proj = dx * dir.x + dy * dir.y;
|
|
2478
3479
|
if (proj <= 0.5) continue;
|
|
2479
3480
|
const perp = Math.abs(dx * dir.y - dy * dir.x);
|
|
@@ -2490,7 +3491,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2490
3491
|
if (!seat) return;
|
|
2491
3492
|
this.focusedId = id;
|
|
2492
3493
|
this.focusRing.radius(this.seatR + 3);
|
|
2493
|
-
this.focusRing.position(
|
|
3494
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
3495
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
3496
|
+
this.focusRing.scale({ x: projectionScale, y: projectionScale });
|
|
2494
3497
|
this.focusRing.visible(true);
|
|
2495
3498
|
this.ensureVisible(seat);
|
|
2496
3499
|
this.overlayLayer.batchDraw();
|
|
@@ -2505,7 +3508,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2505
3508
|
const margin = 70;
|
|
2506
3509
|
const offscreen = p.x < margin || p.x > w - margin || p.y < margin || p.y > h - margin;
|
|
2507
3510
|
if (this.stage.scaleX() < target || offscreen) {
|
|
2508
|
-
const
|
|
3511
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3512
|
+
const ip = this.viewMode === "perspective" && this.perspectiveBaseAffine ? applyAffine(this.perspectiveBaseAffine, rendered) : this.isoT === 0 ? rendered : this.isoForward(rendered);
|
|
2509
3513
|
this.stage.scale({ x: target, y: target });
|
|
2510
3514
|
this.stage.position({ x: w / 2 - ip.x * target, y: h / 2 - ip.y * target });
|
|
2511
3515
|
this.afterViewChange();
|
|
@@ -2515,7 +3519,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2515
3519
|
zoomToFit() {
|
|
2516
3520
|
const w = this.stage.width();
|
|
2517
3521
|
const h = this.stage.height();
|
|
2518
|
-
const b = this.bounds;
|
|
3522
|
+
const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
|
|
2519
3523
|
this.fitScale = Math.min(w / b.width, h / b.height) || 1;
|
|
2520
3524
|
const s = this.fitScale;
|
|
2521
3525
|
this.stage.scale({ x: s, y: s });
|
|
@@ -2544,7 +3548,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2544
3548
|
}
|
|
2545
3549
|
worldToScreen(point) {
|
|
2546
3550
|
const s = this.stage.scaleX();
|
|
2547
|
-
const
|
|
3551
|
+
const candidateId = point.id;
|
|
3552
|
+
const seat = typeof candidateId === "string" ? this.seatById.get(candidateId) : void 0;
|
|
3553
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3554
|
+
const projected = seat ? this.perspectiveSeatProjected.get(seat.id) ?? applyAffine(this.perspectiveBaseAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3555
|
+
return { x: projected.x * s + this.stage.x(), y: projected.y * s + this.stage.y() };
|
|
3556
|
+
}
|
|
3557
|
+
const localPoint = seat ? this.renderedSeatPoint(seat) : point;
|
|
3558
|
+
const p = this.isoT === 0 ? localPoint : this.isoForward(localPoint);
|
|
2548
3559
|
return { x: p.x * s + this.stage.x(), y: p.y * s + this.stage.y() };
|
|
2549
3560
|
}
|
|
2550
3561
|
setAccessibleFilter(on) {
|
|
@@ -2554,6 +3565,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2554
3565
|
const next = types === null ? null : [...types];
|
|
2555
3566
|
if (this.sameAccessFilter(next)) return;
|
|
2556
3567
|
this.accessFilter = next;
|
|
3568
|
+
this.updateAccessGlyphs(this.effScale());
|
|
2557
3569
|
for (const seat of this.seats) {
|
|
2558
3570
|
const c = this.circleById.get(seat.id);
|
|
2559
3571
|
if (c) this.paintSeat(c, seat.id);
|
|
@@ -2652,10 +3664,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2652
3664
|
let minY = Infinity;
|
|
2653
3665
|
let maxY = -Infinity;
|
|
2654
3666
|
for (const seat of matching) {
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3667
|
+
const point = this.renderedSeatPoint(seat);
|
|
3668
|
+
minX = Math.min(minX, point.x);
|
|
3669
|
+
maxX = Math.max(maxX, point.x);
|
|
3670
|
+
minY = Math.min(minY, point.y);
|
|
3671
|
+
maxY = Math.max(maxY, point.y);
|
|
2659
3672
|
}
|
|
2660
3673
|
const pad = Math.max(24, this.seatR * 3);
|
|
2661
3674
|
minX -= pad;
|
|
@@ -2676,12 +3689,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2676
3689
|
* inverse) keeps landing on the projected seats/sections.
|
|
2677
3690
|
*/
|
|
2678
3691
|
setViewMode(mode) {
|
|
3692
|
+
if (mode === "perspective") {
|
|
3693
|
+
if (this.viewMode === mode) {
|
|
3694
|
+
this.afterViewChange();
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
const resetElevatedSeatGroups = this.viewMode === "isometric" && this.isoT > 0;
|
|
3698
|
+
this.viewMode = mode;
|
|
3699
|
+
this.isoTarget = 1;
|
|
3700
|
+
this.isoT = 1;
|
|
3701
|
+
if (this.isoRaf) {
|
|
3702
|
+
cancelAnimationFrame(this.isoRaf);
|
|
3703
|
+
this.isoRaf = 0;
|
|
3704
|
+
}
|
|
3705
|
+
this.applyPerspective(resetElevatedSeatGroups);
|
|
3706
|
+
this.zoomToFit();
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
2679
3709
|
const target = mode === "isometric" ? 1 : 0;
|
|
3710
|
+
const leavingPerspective = this.viewMode === "perspective";
|
|
3711
|
+
this.viewMode = mode;
|
|
2680
3712
|
this.isoTarget = target;
|
|
2681
3713
|
if (this.isoRaf) {
|
|
2682
3714
|
cancelAnimationFrame(this.isoRaf);
|
|
2683
3715
|
this.isoRaf = 0;
|
|
2684
3716
|
}
|
|
3717
|
+
if (leavingPerspective) {
|
|
3718
|
+
this.isoT = target;
|
|
3719
|
+
this.applyIso();
|
|
3720
|
+
this.afterViewChange();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
2685
3723
|
if (this.reducedMotion) {
|
|
2686
3724
|
this.isoT = target;
|
|
2687
3725
|
this.applyIso();
|
|
@@ -2715,7 +3753,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2715
3753
|
}
|
|
2716
3754
|
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
2717
3755
|
getViewMode() {
|
|
2718
|
-
return this.
|
|
3756
|
+
return this.viewMode;
|
|
2719
3757
|
}
|
|
2720
3758
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
2721
3759
|
isoParams() {
|
|
@@ -2723,6 +3761,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2723
3761
|
}
|
|
2724
3762
|
/** Effective vertical scale = stage scale × iso squash — legibility math uses this. */
|
|
2725
3763
|
effScale() {
|
|
3764
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3765
|
+
const xScale = Math.hypot(this.perspectiveBaseAffine.a, this.perspectiveBaseAffine.b);
|
|
3766
|
+
const yScale = Math.hypot(this.perspectiveBaseAffine.c, this.perspectiveBaseAffine.d);
|
|
3767
|
+
return this.stage.scaleX() * Math.min(xScale, yScale);
|
|
3768
|
+
}
|
|
2726
3769
|
return this.stage.scaleX() * (1 - (1 - ISO_SQUASH) * this.isoT);
|
|
2727
3770
|
}
|
|
2728
3771
|
/** Project a world point through the iso affine about the chart centre (→ iso-world). */
|
|
@@ -2746,19 +3789,113 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2746
3789
|
const wy = -dx * Math.sin(th) + uy * Math.cos(th);
|
|
2747
3790
|
return { x: c.x + wx, y: c.y + wy };
|
|
2748
3791
|
}
|
|
3792
|
+
/** Precompute the Phase-C pinhole camera and every exact seat anchor. */
|
|
3793
|
+
buildPerspectiveProjection(view) {
|
|
3794
|
+
let maxSurfaceHeightWorld = 0;
|
|
3795
|
+
for (const seat of this.seats) {
|
|
3796
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3797
|
+
maxSurfaceHeightWorld = Math.max(maxSurfaceHeightWorld, surfaceM * CHART_UNITS_PER_METRE);
|
|
3798
|
+
}
|
|
3799
|
+
for (const object of view.objects) {
|
|
3800
|
+
if (object.type !== "section") continue;
|
|
3801
|
+
maxSurfaceHeightWorld = Math.max(
|
|
3802
|
+
maxSurfaceHeightWorld,
|
|
3803
|
+
sectionGeometry(object, { floorBaseHeightM: this.floorBaseHeightMFor(object.id) }).height * CHART_UNITS_PER_METRE
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
const camera = createPerspectiveCamera(this.bounds, maxSurfaceHeightWorld);
|
|
3807
|
+
const base = perspectiveTangentAffine(camera, camera.target);
|
|
3808
|
+
const inverse = invertAffine(base);
|
|
3809
|
+
this.perspectiveCamera = camera;
|
|
3810
|
+
this.perspectiveBaseAffine = base;
|
|
3811
|
+
this.perspectiveBaseInverse = inverse;
|
|
3812
|
+
this.perspectiveSeatLocal.clear();
|
|
3813
|
+
this.perspectiveSeatProjected.clear();
|
|
3814
|
+
this.perspectiveSeatDepth.clear();
|
|
3815
|
+
this.perspectiveSeatScale.clear();
|
|
3816
|
+
this.perspectiveBounds = null;
|
|
3817
|
+
let minX = Infinity;
|
|
3818
|
+
let minY = Infinity;
|
|
3819
|
+
let maxX = -Infinity;
|
|
3820
|
+
let maxY = -Infinity;
|
|
3821
|
+
for (const seat of this.seats) {
|
|
3822
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3823
|
+
const projected = projectPerspectivePoint(camera, seat, surfaceM * CHART_UNITS_PER_METRE);
|
|
3824
|
+
const point = { x: projected.x, y: projected.y };
|
|
3825
|
+
this.perspectiveSeatProjected.set(seat.id, point);
|
|
3826
|
+
this.perspectiveSeatLocal.set(seat.id, applyAffine(inverse, point));
|
|
3827
|
+
this.perspectiveSeatDepth.set(seat.id, projected.depth);
|
|
3828
|
+
this.perspectiveSeatScale.set(seat.id, projected.scale);
|
|
3829
|
+
minX = Math.min(minX, projected.x);
|
|
3830
|
+
minY = Math.min(minY, projected.y);
|
|
3831
|
+
maxX = Math.max(maxX, projected.x);
|
|
3832
|
+
maxY = Math.max(maxY, projected.y);
|
|
3833
|
+
}
|
|
3834
|
+
for (const point of [
|
|
3835
|
+
{ x: this.bounds.x, y: this.bounds.y },
|
|
3836
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y },
|
|
3837
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y + this.bounds.height },
|
|
3838
|
+
{ x: this.bounds.x, y: this.bounds.y + this.bounds.height }
|
|
3839
|
+
]) {
|
|
3840
|
+
const projected = projectPerspectivePoint(camera, point, 0);
|
|
3841
|
+
minX = Math.min(minX, projected.x);
|
|
3842
|
+
minY = Math.min(minY, projected.y);
|
|
3843
|
+
maxX = Math.max(maxX, projected.x);
|
|
3844
|
+
maxY = Math.max(maxY, projected.y);
|
|
3845
|
+
}
|
|
3846
|
+
const pad = Math.max(24, this.seatR * 3);
|
|
3847
|
+
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;
|
|
3848
|
+
}
|
|
3849
|
+
/** Apply an affine to a Konva container while keeping `pivot` fixed logically. */
|
|
3850
|
+
setContainerAffine(node, affine, pivot) {
|
|
3851
|
+
const decomposed = decomposeAffineLinear(affine);
|
|
3852
|
+
node.offset(pivot);
|
|
3853
|
+
node.position(applyAffine(affine, pivot));
|
|
3854
|
+
node.rotation(decomposed.rotationDeg);
|
|
3855
|
+
node.scale({ x: decomposed.scaleX, y: decomposed.scaleY });
|
|
3856
|
+
node.skewX(decomposed.skewX);
|
|
3857
|
+
node.skewY(0);
|
|
3858
|
+
}
|
|
3859
|
+
resetContainerTransform(node) {
|
|
3860
|
+
node.position({ x: 0, y: 0 });
|
|
3861
|
+
node.offset({ x: 0, y: 0 });
|
|
3862
|
+
node.scale({ x: 1, y: 1 });
|
|
3863
|
+
node.rotation(0);
|
|
3864
|
+
node.skewX(0);
|
|
3865
|
+
node.skewY(0);
|
|
3866
|
+
}
|
|
3867
|
+
/** Exact raked surface height used for the bounded section tangent plane. */
|
|
3868
|
+
sectionSurfaceHeight(section, point) {
|
|
3869
|
+
const radial = Math.hypot(point.x - section.surfaceFocal.x, point.y - section.surfaceFocal.y);
|
|
3870
|
+
const depth = Math.max(0, radial - section.frontDistanceWorld);
|
|
3871
|
+
return section.liftWorld + depth * Math.tan(section.rakeDeg * Math.PI / 180);
|
|
3872
|
+
}
|
|
3873
|
+
/** Desired perspective-space point for a section surface, before Stage pan/zoom. */
|
|
3874
|
+
projectedSectionPoint(section, point) {
|
|
3875
|
+
return section.perspectiveAffine ? applyAffine(section.perspectiveAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3876
|
+
}
|
|
3877
|
+
/** Shared-layer local point which the ground affine paints at `projected`. */
|
|
3878
|
+
perspectiveLocalPoint(projected) {
|
|
3879
|
+
return this.perspectiveBaseInverse ? applyAffine(this.perspectiveBaseInverse, projected) : projected;
|
|
3880
|
+
}
|
|
2749
3881
|
/**
|
|
2750
3882
|
* Local-space offset that, once the layer applies the iso affine, lifts an
|
|
2751
|
-
* object straight UP in iso-world by `
|
|
2752
|
-
* (= inverse-linear of the pure vertical lift). Zero at isoT=0.
|
|
3883
|
+
* object straight UP in iso-world by `liftWorld × isoT` world units
|
|
3884
|
+
* (= inverse-linear of the pure vertical lift). Zero at isoT=0. `liftWorld` is
|
|
3885
|
+
* the section's resolved real height in world units (Phase B1).
|
|
2753
3886
|
*/
|
|
2754
|
-
isoLiftLocal(
|
|
3887
|
+
isoLiftLocal(liftWorld) {
|
|
2755
3888
|
const { th, sg } = this.isoParams();
|
|
2756
|
-
const delta =
|
|
3889
|
+
const delta = liftWorld * this.isoT;
|
|
2757
3890
|
return { x: -(delta / sg) * Math.sin(th), y: -(delta / sg) * Math.cos(th) };
|
|
2758
3891
|
}
|
|
2759
|
-
/** Restore
|
|
2760
|
-
|
|
2761
|
-
|
|
3892
|
+
/** Restore projection layers to identity (flat) — byte-for-byte the original.
|
|
3893
|
+
* `seatLayer` can be skipped when entering perspective from flat: assigning a
|
|
3894
|
+
* top-level transform invalidates all 14k descendant absolute transforms even
|
|
3895
|
+
* while the semantic overview keeps that layer fully transparent. */
|
|
3896
|
+
resetLayerTransforms(includeSeatLayer = true) {
|
|
3897
|
+
const layers = includeSeatLayer ? [this.bgLayer, this.seatLayer, this.overlayLayer] : [this.bgLayer, this.overlayLayer];
|
|
3898
|
+
for (const layer of layers) {
|
|
2762
3899
|
layer.position({ x: 0, y: 0 });
|
|
2763
3900
|
layer.offset({ x: 0, y: 0 });
|
|
2764
3901
|
layer.scale({ x: 1, y: 1 });
|
|
@@ -2767,6 +3904,117 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2767
3904
|
layer.skewY(0);
|
|
2768
3905
|
}
|
|
2769
3906
|
}
|
|
3907
|
+
/** Restore section-owned containers before applying another projection. */
|
|
3908
|
+
resetSectionProjection(resetSeatGroups = true) {
|
|
3909
|
+
for (const section of this.sections) {
|
|
3910
|
+
this.resetContainerTransform(section.rootGroupBg);
|
|
3911
|
+
this.resetContainerTransform(section.liftGroupBg);
|
|
3912
|
+
if (resetSeatGroups) this.resetContainerTransform(section.liftGroupSeat);
|
|
3913
|
+
section.rootGroupBg.zIndex(section.bgZIndex);
|
|
3914
|
+
if (resetSeatGroups) section.liftGroupSeat.zIndex(section.seatZIndex);
|
|
3915
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
3916
|
+
if (labelGroup && resetSeatGroups) this.resetContainerTransform(labelGroup);
|
|
3917
|
+
section.perspectiveAffine = void 0;
|
|
3918
|
+
section.perspectiveCorrection = void 0;
|
|
3919
|
+
section.perspectiveDepth = void 0;
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
/** Move native seat/booth hit shapes to or from their exact pinhole anchors. */
|
|
3923
|
+
applyExactSeatAnchors(perspective, seatIds) {
|
|
3924
|
+
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);
|
|
3925
|
+
for (const seat of seats) {
|
|
3926
|
+
if (perspective && this.perspectiveAppliedSeats.has(seat.id)) continue;
|
|
3927
|
+
const point = perspective ? this.perspectiveSeatProjected.get(seat.id) ?? seat : seat;
|
|
3928
|
+
const scale = perspective ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
3929
|
+
const shape = this.circleById.get(seat.id);
|
|
3930
|
+
shape?.position(point);
|
|
3931
|
+
shape?.scale({ x: scale, y: scale });
|
|
3932
|
+
const ring = this.accessRingById.get(seat.id);
|
|
3933
|
+
ring?.position(point);
|
|
3934
|
+
ring?.scale({ x: scale, y: scale });
|
|
3935
|
+
const glyph = this.accessGlyphById.get(seat.id);
|
|
3936
|
+
if (glyph) {
|
|
3937
|
+
const baseGlyphScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
|
|
3938
|
+
glyph.position(point);
|
|
3939
|
+
glyph.scale({ x: baseGlyphScale * scale, y: baseGlyphScale * scale });
|
|
3940
|
+
}
|
|
3941
|
+
const boothLabel = this.boothLabelById.get(seat.id);
|
|
3942
|
+
boothLabel?.position(point);
|
|
3943
|
+
boothLabel?.scale({ x: scale, y: scale });
|
|
3944
|
+
if (perspective) this.perspectiveAppliedSeats.add(seat.id);
|
|
3945
|
+
}
|
|
3946
|
+
if (!perspective) this.perspectiveAppliedSeats.clear();
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Phase C projected 2.5D. Seat/booth anchors are exact pinhole projections;
|
|
3950
|
+
* section top surfaces use one explicitly bounded tangent plane per authored
|
|
3951
|
+
* section. Native Konva shapes move with the anchors, so direct hit testing is
|
|
3952
|
+
* the same graph that paints the buyer-visible unit.
|
|
3953
|
+
*/
|
|
3954
|
+
applyPerspective(resetElevatedSeatGroups = true) {
|
|
3955
|
+
const camera = this.perspectiveCamera;
|
|
3956
|
+
const base = this.perspectiveBaseAffine;
|
|
3957
|
+
const inverse = this.perspectiveBaseInverse;
|
|
3958
|
+
if (!camera || !base || !inverse) return;
|
|
3959
|
+
const lazySectionSeats = this.seats.length > 2500 && this.sections.length > 0;
|
|
3960
|
+
if (this.cached && !lazySectionSeats) {
|
|
3961
|
+
this.seatLayer.clearCache();
|
|
3962
|
+
this.seatLayer.listening(true);
|
|
3963
|
+
this.cached = false;
|
|
3964
|
+
}
|
|
3965
|
+
this.resetLayerTransforms(resetElevatedSeatGroups);
|
|
3966
|
+
this.resetSectionProjection(resetElevatedSeatGroups);
|
|
3967
|
+
for (const layer of [this.bgLayer, this.overlayLayer]) {
|
|
3968
|
+
this.setContainerAffine(layer, base, camera.target);
|
|
3969
|
+
}
|
|
3970
|
+
this.perspectiveAppliedSeats.clear();
|
|
3971
|
+
if (!lazySectionSeats) this.applyExactSeatAnchors(true);
|
|
3972
|
+
for (const section of this.sections) {
|
|
3973
|
+
const heightAt = (point) => this.sectionSurfaceHeight(section, point);
|
|
3974
|
+
const desired = perspectiveTangentAffine(camera, section.centroid, heightAt);
|
|
3975
|
+
const correction = composeAffine(inverse, desired);
|
|
3976
|
+
section.perspectiveAffine = desired;
|
|
3977
|
+
section.perspectiveCorrection = correction;
|
|
3978
|
+
section.perspectiveDepth = projectPerspectivePoint(
|
|
3979
|
+
camera,
|
|
3980
|
+
section.centroid,
|
|
3981
|
+
heightAt(section.centroid)
|
|
3982
|
+
).depth;
|
|
3983
|
+
this.setContainerAffine(section.liftGroupBg, correction, section.centroid);
|
|
3984
|
+
for (let index = 0; index < section.sideFaces.length; index++) {
|
|
3985
|
+
const p0 = section.outline[index];
|
|
3986
|
+
const p1 = section.outline[(index + 1) % section.outline.length];
|
|
3987
|
+
const base0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, 0));
|
|
3988
|
+
const base1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, 0));
|
|
3989
|
+
const top1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, heightAt(p1)));
|
|
3990
|
+
const top0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, heightAt(p0)));
|
|
3991
|
+
section.sideFaces[index].points([
|
|
3992
|
+
base0.x,
|
|
3993
|
+
base0.y,
|
|
3994
|
+
base1.x,
|
|
3995
|
+
base1.y,
|
|
3996
|
+
top1.x,
|
|
3997
|
+
top1.y,
|
|
3998
|
+
top0.x,
|
|
3999
|
+
top0.y
|
|
4000
|
+
]);
|
|
4001
|
+
section.sideFaces[index].opacity(0.9);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
const farToNear = [...this.sections].sort(
|
|
4005
|
+
(left, right) => (right.perspectiveDepth ?? 0) - (left.perspectiveDepth ?? 0)
|
|
4006
|
+
);
|
|
4007
|
+
for (const section of farToNear) {
|
|
4008
|
+
section.rootGroupBg.moveToTop();
|
|
4009
|
+
section.liftGroupSeat.moveToTop();
|
|
4010
|
+
}
|
|
4011
|
+
this.unsectionedSeatGroup?.moveToTop();
|
|
4012
|
+
this.syncSeatOverlayPositions();
|
|
4013
|
+
if (!lazySectionSeats) this.updateSeatGroupVisibility();
|
|
4014
|
+
this.bgLayer.batchDraw();
|
|
4015
|
+
this.seatLayer.batchDraw();
|
|
4016
|
+
this.overlayLayer.batchDraw();
|
|
4017
|
+
}
|
|
2770
4018
|
/**
|
|
2771
4019
|
* Apply the current isoT to the scene: the base rotate+squash as a decomposed
|
|
2772
4020
|
* layer transform (so seats/décor/rings project together and Konva's own
|
|
@@ -2774,6 +4022,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2774
4022
|
* lift + extruded side faces on elevated sections.
|
|
2775
4023
|
*/
|
|
2776
4024
|
applyIso() {
|
|
4025
|
+
this.resetSectionProjection();
|
|
4026
|
+
this.applyExactSeatAnchors(false);
|
|
2777
4027
|
const t2 = this.isoT;
|
|
2778
4028
|
if (t2 === 0) {
|
|
2779
4029
|
this.resetLayerTransforms();
|
|
@@ -2802,6 +4052,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2802
4052
|
}
|
|
2803
4053
|
this.applyUprightLabels();
|
|
2804
4054
|
this.applyElevation();
|
|
4055
|
+
this.updateSeatGroupVisibility();
|
|
2805
4056
|
this.bgLayer.batchDraw();
|
|
2806
4057
|
this.seatLayer.batchDraw();
|
|
2807
4058
|
this.overlayLayer.batchDraw();
|
|
@@ -2830,10 +4081,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2830
4081
|
applyElevation() {
|
|
2831
4082
|
const t2 = this.isoT;
|
|
2832
4083
|
for (const sec of this.sections) {
|
|
2833
|
-
if (sec.
|
|
2834
|
-
const off = this.isoLiftLocal(sec.
|
|
4084
|
+
if (sec.liftWorld <= 0) continue;
|
|
4085
|
+
const off = this.isoLiftLocal(sec.liftWorld);
|
|
2835
4086
|
sec.liftGroupBg?.position(off);
|
|
2836
|
-
sec.liftGroupSeat
|
|
4087
|
+
sec.liftGroupSeat.position(off);
|
|
4088
|
+
this.labelLiftGroups.get(sec.id)?.position(off);
|
|
2837
4089
|
const alpha = 0.9 * t2;
|
|
2838
4090
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
2839
4091
|
const face = sec.sideFaces[i];
|
|
@@ -2843,6 +4095,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2843
4095
|
face.opacity(alpha);
|
|
2844
4096
|
}
|
|
2845
4097
|
}
|
|
4098
|
+
this.syncSeatOverlayPositions();
|
|
2846
4099
|
}
|
|
2847
4100
|
destroy() {
|
|
2848
4101
|
this.destroyed = true;
|
|
@@ -2869,16 +4122,166 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2869
4122
|
* tier shifts with one offset in iso view) or the seat layer directly.
|
|
2870
4123
|
*/
|
|
2871
4124
|
seatContainer(id) {
|
|
2872
|
-
return this.seatSection.get(id)?.liftGroupSeat ?? this.seatLayer;
|
|
4125
|
+
return this.seatSection.get(id)?.liftGroupSeat ?? this.unsectionedSeatGroup ?? this.seatLayer;
|
|
2873
4126
|
}
|
|
2874
|
-
|
|
4127
|
+
/** True when a section belongs to the active logical section/zone focus. */
|
|
4128
|
+
sectionMatchesFocus(section) {
|
|
4129
|
+
const focus = this.focusedSectionId;
|
|
4130
|
+
return focus == null || section.id === focus || section.logicalId === focus || section.zone === focus;
|
|
4131
|
+
}
|
|
4132
|
+
/** Visual opacity after the section-focus overlay is composed. */
|
|
4133
|
+
focusedSeatOpacity(id, localOpacity) {
|
|
4134
|
+
if (!this.focusedSectionId) return localOpacity;
|
|
4135
|
+
const section = this.seatSection.get(id);
|
|
4136
|
+
return localOpacity * (section && this.sectionMatchesFocus(section) ? 1 : FOCUS_DIM_OPACITY);
|
|
4137
|
+
}
|
|
4138
|
+
/** Project a section outline through elevation + the active affine view and
|
|
4139
|
+
* then into viewport pixels. The result is conservative (AABB), so a visible
|
|
4140
|
+
* section is never clipped even for rotated or isometric outlines. */
|
|
4141
|
+
sectionScreenBounds(section) {
|
|
4142
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
4143
|
+
const scale2 = this.stage.scaleX();
|
|
4144
|
+
const stageX2 = this.stage.x();
|
|
4145
|
+
const stageY2 = this.stage.y();
|
|
4146
|
+
return polyBounds(section.outline.map((point) => {
|
|
4147
|
+
const projected = this.projectedSectionPoint(section, point);
|
|
4148
|
+
return { x: projected.x * scale2 + stageX2, y: projected.y * scale2 + stageY2 };
|
|
4149
|
+
}));
|
|
4150
|
+
}
|
|
4151
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4152
|
+
const scale = this.stage.scaleX();
|
|
4153
|
+
const stageX = this.stage.x();
|
|
4154
|
+
const stageY = this.stage.y();
|
|
4155
|
+
return polyBounds(section.outline.map((point) => {
|
|
4156
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
4157
|
+
const projected = this.isoT === 0 ? local : this.isoForward(local);
|
|
4158
|
+
return { x: projected.x * scale + stageX, y: projected.y * scale + stageY };
|
|
4159
|
+
}));
|
|
4160
|
+
}
|
|
4161
|
+
/** Hide only section groups wholly outside a padded viewport at the live-seat
|
|
4162
|
+
* rung. Overview caching always restores all groups first, so panning a
|
|
4163
|
+
* cached whole-venue bitmap can never reveal missing inventory. */
|
|
4164
|
+
updateSeatGroupVisibility() {
|
|
4165
|
+
const liveSeats = this.effScale() >= CACHE_THRESHOLD;
|
|
4166
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
4167
|
+
const padding = 96;
|
|
4168
|
+
const width = this.stage.width();
|
|
4169
|
+
const height = this.stage.height();
|
|
4170
|
+
for (const section of this.sections) {
|
|
4171
|
+
let visible = true;
|
|
4172
|
+
if (liveSeats) {
|
|
4173
|
+
const bounds2 = this.sectionScreenBounds(section);
|
|
4174
|
+
visible = bounds2.x + bounds2.width >= -padding && bounds2.y + bounds2.height >= -padding && bounds2.x <= width + padding && bounds2.y <= height + padding;
|
|
4175
|
+
if (visible && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4176
|
+
this.applyExactSeatAnchors(true, section.memberIds);
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
section.liftGroupSeat.setViewportCulled(!visible);
|
|
4180
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
4181
|
+
labelGroup?.setViewportCulled(!visible);
|
|
4182
|
+
}
|
|
4183
|
+
if (this.unsectionedSeatGroup && !this.unsectionedSeatGroup.visible()) {
|
|
4184
|
+
this.unsectionedSeatGroup.visible(true);
|
|
4185
|
+
}
|
|
4186
|
+
if (liveSeats && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
4187
|
+
this.applyExactSeatAnchors(true, this.seats.filter((seat) => !this.seatSection.has(seat.id)).map((seat) => seat.id));
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
/** Seats worth considering for viewport labels. Section culling is a safe
|
|
4191
|
+
* coarse index; the later screen test remains the exact filter. */
|
|
4192
|
+
visibleSeatCandidates() {
|
|
4193
|
+
if (!this.sections.length) return this.seats;
|
|
4194
|
+
const candidates = [];
|
|
4195
|
+
for (const section of this.sections) {
|
|
4196
|
+
if (section.liftGroupSeat.isViewportCulled()) continue;
|
|
4197
|
+
for (const id of section.memberIds) {
|
|
4198
|
+
const seat = this.seatById.get(id);
|
|
4199
|
+
if (seat) candidates.push(seat);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
2875
4202
|
for (const seat of this.seats) {
|
|
4203
|
+
if (!this.seatSection.has(seat.id)) candidates.push(seat);
|
|
4204
|
+
}
|
|
4205
|
+
return candidates;
|
|
4206
|
+
}
|
|
4207
|
+
seatViewportCulled(id) {
|
|
4208
|
+
return this.seatSection.get(id)?.liftGroupSeat.isViewportCulled() ?? false;
|
|
4209
|
+
}
|
|
4210
|
+
/** Local world coordinate at which an elevated seat is actually painted for
|
|
4211
|
+
* the current iso tween. Flat seats and the 2D endpoint are unchanged. */
|
|
4212
|
+
renderedSeatPoint(seat) {
|
|
4213
|
+
if (this.viewMode === "perspective") return this.perspectiveSeatLocal.get(seat.id) ?? seat;
|
|
4214
|
+
const section = this.seatSection.get(seat.id);
|
|
4215
|
+
if (!section || section.liftWorld <= 0 || this.isoT === 0) return seat;
|
|
4216
|
+
const offset = this.isoLiftLocal(section.liftWorld);
|
|
4217
|
+
return { x: seat.x + offset.x, y: seat.y + offset.y };
|
|
4218
|
+
}
|
|
4219
|
+
/** Overlay labels mirror the seat-layer lift without an O(seats) per-frame
|
|
4220
|
+
* rewrite. The groups are rebuilt with the zoom-dependent label set. */
|
|
4221
|
+
seatLabelContainer(id) {
|
|
4222
|
+
const section = this.seatSection.get(id);
|
|
4223
|
+
if (!section) return this.labelGroup;
|
|
4224
|
+
let group = this.labelLiftGroups.get(section.id);
|
|
4225
|
+
if (!group) {
|
|
4226
|
+
group = new ViewportGroup({
|
|
4227
|
+
listening: false
|
|
4228
|
+
});
|
|
4229
|
+
group.setViewportCulled(section.liftGroupSeat.isViewportCulled());
|
|
4230
|
+
group.position(this.viewMode === "perspective" ? { x: 0, y: 0 } : this.isoLiftLocal(section.liftWorld));
|
|
4231
|
+
this.labelGroup.add(group);
|
|
4232
|
+
this.labelLiftGroups.set(section.id, group);
|
|
4233
|
+
}
|
|
4234
|
+
return group;
|
|
4235
|
+
}
|
|
4236
|
+
/** Shared overlay furniture is not parented to section lift groups, so keep
|
|
4237
|
+
* the small transient set aligned explicitly. Selection is capped in buyer
|
|
4238
|
+
* mode, making this constant-sized work during the iso tween. */
|
|
4239
|
+
syncSeatOverlayPositions() {
|
|
4240
|
+
if (this.hoveredId) {
|
|
4241
|
+
const seat = this.seatById.get(this.hoveredId);
|
|
4242
|
+
if (seat) {
|
|
4243
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
4244
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4245
|
+
this.hoverRing.scale({ x: scale, y: scale });
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (this.focusedId) {
|
|
4249
|
+
const seat = this.seatById.get(this.focusedId);
|
|
4250
|
+
if (seat) {
|
|
4251
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
4252
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4253
|
+
this.focusRing.scale({ x: scale, y: scale });
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
for (const [id, marker] of this.selectionMarkers) {
|
|
4257
|
+
const seat = this.seatById.get(id);
|
|
4258
|
+
if (seat) {
|
|
4259
|
+
marker.position(this.renderedSeatPoint(seat));
|
|
4260
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4261
|
+
marker.scale({ x: scale, y: scale });
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
renderSeats() {
|
|
4266
|
+
const paintOrder = [...this.seats].sort((left, right) => (this.perspectiveSeatDepth.get(right.id) ?? 0) - (this.perspectiveSeatDepth.get(left.id) ?? 0));
|
|
4267
|
+
for (const seat of paintOrder) {
|
|
2876
4268
|
if (seat.kind === "booth") {
|
|
2877
4269
|
this.renderBoothUnit(seat);
|
|
2878
4270
|
continue;
|
|
2879
4271
|
}
|
|
2880
4272
|
const target = this.seatContainer(seat.id);
|
|
2881
|
-
const c = new
|
|
4273
|
+
const c = seat.wheelchairSpaceType === "no-seat" ? new Rect({
|
|
4274
|
+
x: seat.x,
|
|
4275
|
+
y: seat.y,
|
|
4276
|
+
width: this.seatR * 2,
|
|
4277
|
+
height: this.seatR * 2,
|
|
4278
|
+
offsetX: this.seatR,
|
|
4279
|
+
offsetY: this.seatR,
|
|
4280
|
+
cornerRadius: 2,
|
|
4281
|
+
perfectDrawEnabled: false,
|
|
4282
|
+
shadowForStrokeEnabled: false,
|
|
4283
|
+
hitStrokeWidth: 0
|
|
4284
|
+
}) : new Circle({
|
|
2882
4285
|
x: seat.x,
|
|
2883
4286
|
y: seat.y,
|
|
2884
4287
|
radius: this.seatR,
|
|
@@ -2891,21 +4294,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2891
4294
|
this.paintSeat(c, seat.id);
|
|
2892
4295
|
target.add(c);
|
|
2893
4296
|
if (seat.accessible) {
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
4297
|
+
if (seat.wheelchairSpaceType !== "no-seat") {
|
|
4298
|
+
const ring = new Circle({
|
|
4299
|
+
x: seat.x,
|
|
4300
|
+
y: seat.y,
|
|
4301
|
+
radius: this.seatR + 1.5,
|
|
4302
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
4303
|
+
strokeWidth: 2.5,
|
|
4304
|
+
listening: false,
|
|
4305
|
+
perfectDrawEnabled: false,
|
|
4306
|
+
shadowForStrokeEnabled: false
|
|
4307
|
+
});
|
|
4308
|
+
this.accessRingById.set(seat.id, ring);
|
|
4309
|
+
target.add(ring);
|
|
4310
|
+
}
|
|
4311
|
+
if (seat.accessibility?.includes("wheelchair")) {
|
|
4312
|
+
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
4313
|
+
this.accessGlyphById.set(seat.id, glyph);
|
|
4314
|
+
target.add(glyph);
|
|
4315
|
+
}
|
|
2909
4316
|
}
|
|
2910
4317
|
}
|
|
2911
4318
|
}
|
|
@@ -2913,25 +4320,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2913
4320
|
renderBoothUnit(seat) {
|
|
2914
4321
|
const target = this.seatContainer(seat.id);
|
|
2915
4322
|
const dims = this.boothDims.get(seat.rowId) ?? { width: 40, height: 30, rotation: 0 };
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
4323
|
+
let labelX = seat.x;
|
|
4324
|
+
let labelY = seat.y;
|
|
4325
|
+
let node;
|
|
4326
|
+
if (dims.points && dims.points.length >= 3) {
|
|
4327
|
+
node = new Line({
|
|
4328
|
+
x: seat.x,
|
|
4329
|
+
y: seat.y,
|
|
4330
|
+
points: dims.points.flatMap((p) => [p.x, p.y]),
|
|
4331
|
+
closed: true,
|
|
4332
|
+
perfectDrawEnabled: false,
|
|
4333
|
+
shadowForStrokeEnabled: false,
|
|
4334
|
+
hitStrokeWidth: 0
|
|
4335
|
+
});
|
|
4336
|
+
labelX = seat.x + dims.points.reduce((a, p) => a + p.x, 0) / dims.points.length;
|
|
4337
|
+
labelY = seat.y + dims.points.reduce((a, p) => a + p.y, 0) / dims.points.length;
|
|
4338
|
+
} else {
|
|
4339
|
+
node = new Rect({
|
|
4340
|
+
x: seat.x,
|
|
4341
|
+
y: seat.y,
|
|
4342
|
+
width: dims.width,
|
|
4343
|
+
height: dims.height,
|
|
4344
|
+
offsetX: dims.width / 2,
|
|
4345
|
+
offsetY: dims.height / 2,
|
|
4346
|
+
rotation: dims.rotation,
|
|
4347
|
+
cornerRadius: 4,
|
|
4348
|
+
perfectDrawEnabled: false,
|
|
4349
|
+
shadowForStrokeEnabled: false,
|
|
4350
|
+
hitStrokeWidth: 0
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
node.setAttr("seatId", seat.id);
|
|
4354
|
+
this.circleById.set(seat.id, node);
|
|
4355
|
+
target.add(node);
|
|
2932
4356
|
const t2 = new Text({
|
|
2933
|
-
x:
|
|
2934
|
-
y:
|
|
4357
|
+
x: labelX,
|
|
4358
|
+
y: labelY,
|
|
2935
4359
|
text: seat.displayLabel ?? seat.label,
|
|
2936
4360
|
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
2937
4361
|
fontStyle: "600",
|
|
@@ -2943,11 +4367,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2943
4367
|
t2.offsetX(t2.width() / 2);
|
|
2944
4368
|
t2.offsetY(t2.height() / 2);
|
|
2945
4369
|
t2.visible(false);
|
|
2946
|
-
this.boothLabelById.set(seat.id, t2);
|
|
2947
4370
|
this.hasBoothText = true;
|
|
2948
4371
|
this.boothLabelById.set(seat.id, t2);
|
|
2949
4372
|
target.add(t2);
|
|
2950
|
-
this.paintSeat(
|
|
4373
|
+
this.paintSeat(node, seat.id);
|
|
2951
4374
|
}
|
|
2952
4375
|
/**
|
|
2953
4376
|
* Build the centred accessibility glyph for a seat. Sized relative to the seat
|
|
@@ -2966,24 +4389,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2966
4389
|
scaleY: k,
|
|
2967
4390
|
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
2968
4391
|
listening: false,
|
|
2969
|
-
visible:
|
|
4392
|
+
visible: false,
|
|
2970
4393
|
perfectDrawEnabled: false,
|
|
2971
4394
|
shadowForStrokeEnabled: false
|
|
2972
4395
|
});
|
|
2973
4396
|
}
|
|
2974
4397
|
/**
|
|
2975
|
-
*
|
|
2976
|
-
*
|
|
2977
|
-
*
|
|
2978
|
-
* never the full node graph, so it is cheap to call on every view change.
|
|
4398
|
+
* Keep physical wheelchair provision readable in screen space at every map
|
|
4399
|
+
* LOD. The active Wheelchair filter promotes it further. Iterates the small
|
|
4400
|
+
* wheelchair-seat set, never the full node graph.
|
|
2979
4401
|
*/
|
|
2980
4402
|
updateAccessGlyphs(scale) {
|
|
2981
4403
|
if (!this.accessGlyphById.size) return;
|
|
2982
|
-
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
2983
4404
|
for (const [id, glyph] of this.accessGlyphById) {
|
|
2984
|
-
|
|
4405
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
4406
|
+
const baseScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX * perspectiveScale;
|
|
4407
|
+
const emphasized = this.accessGlyphFilterEmphasized(id);
|
|
4408
|
+
const minimumScreenPx = emphasized ? FILTERED_WHEELCHAIR_GLYPH_MIN_PX : WHEELCHAIR_GLYPH_MIN_PX;
|
|
4409
|
+
const glyphScale = Math.max(
|
|
4410
|
+
baseScale,
|
|
4411
|
+
minimumScreenPx / (ACCESS_GLYPH_VIEWBOX * Math.max(scale, 1e-3))
|
|
4412
|
+
);
|
|
4413
|
+
glyph.scale({ x: glyphScale, y: glyphScale });
|
|
4414
|
+
glyph.visible(this.accessGlyphShouldShow(id));
|
|
2985
4415
|
}
|
|
2986
4416
|
}
|
|
4417
|
+
/** True only for a physical wheelchair provision matching an active buyer filter. */
|
|
4418
|
+
accessGlyphFilterEmphasized(id) {
|
|
4419
|
+
const seat = this.seatById.get(id);
|
|
4420
|
+
const filter = this.accessFilter;
|
|
4421
|
+
return !!seat && filter !== null && !!seat.accessibility?.includes("wheelchair") && (filter.length === 0 || filter.includes("wheelchair")) && seatMatchesAccess(seat, filter);
|
|
4422
|
+
}
|
|
4423
|
+
accessGlyphShouldShow(id) {
|
|
4424
|
+
return this.accessGlyphById.has(id) && this.accessGlyphEligible(id);
|
|
4425
|
+
}
|
|
2987
4426
|
/**
|
|
2988
4427
|
* Whether an accessible seat's glyph should show for its current status. It is
|
|
2989
4428
|
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
@@ -2999,8 +4438,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2999
4438
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
3000
4439
|
seatBaseColor(categoryKey) {
|
|
3001
4440
|
if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
|
|
3002
|
-
|
|
3003
|
-
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
4441
|
+
return this.cbHueByKey.get(categoryKey) ?? CB_PALETTE[0];
|
|
3004
4442
|
}
|
|
3005
4443
|
/** Per-seat authored label size relative to the neutral default (1 = default),
|
|
3006
4444
|
* mirroring the section convention (labelStyle.size ?? 18) / 18. Only real
|
|
@@ -3063,6 +4501,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3063
4501
|
c.dash([2, 2]);
|
|
3064
4502
|
break;
|
|
3065
4503
|
}
|
|
4504
|
+
if (seat.wheelchairSpaceType === "no-seat" && status === "free") {
|
|
4505
|
+
c.stroke(accessibilityRingColor(seat.accessibility));
|
|
4506
|
+
c.strokeWidth(selected ? 3 : 2);
|
|
4507
|
+
c.dash([3, 2]);
|
|
4508
|
+
}
|
|
3066
4509
|
if (boothLabel) {
|
|
3067
4510
|
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? "HELD" : seat.label);
|
|
3068
4511
|
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
@@ -3096,11 +4539,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3096
4539
|
c.dash([]);
|
|
3097
4540
|
c.opacity(CLOSED_SEAT_OPACITY);
|
|
3098
4541
|
}
|
|
3099
|
-
if (this.focusedSectionId) {
|
|
3100
|
-
const sec = this.seatSection.get(id);
|
|
3101
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
3102
|
-
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
3103
|
-
}
|
|
3104
4542
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
3105
4543
|
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
3106
4544
|
if (bookableLabel) {
|
|
@@ -3114,7 +4552,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3114
4552
|
const fill = c.fill();
|
|
3115
4553
|
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
3116
4554
|
accessGlyph.opacity(c.opacity());
|
|
3117
|
-
accessGlyph.visible(this.
|
|
4555
|
+
accessGlyph.visible(this.accessGlyphShouldShow(id));
|
|
3118
4556
|
}
|
|
3119
4557
|
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
3120
4558
|
}
|
|
@@ -3183,7 +4621,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3183
4621
|
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
3184
4622
|
this.focusedSectionId = id;
|
|
3185
4623
|
this.drawFocusBackdrop(id);
|
|
3186
|
-
this.
|
|
4624
|
+
this.bgLayer.batchDraw();
|
|
4625
|
+
this.overlayLayer.batchDraw();
|
|
3187
4626
|
this.updateLOD();
|
|
3188
4627
|
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
3189
4628
|
}
|
|
@@ -3195,7 +4634,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3195
4634
|
this.focusBackdrop.destroy();
|
|
3196
4635
|
this.focusBackdrop = null;
|
|
3197
4636
|
}
|
|
3198
|
-
this.
|
|
4637
|
+
this.focusDimOverlay?.destroy();
|
|
4638
|
+
this.focusDimOverlay = null;
|
|
4639
|
+
this.bgLayer.batchDraw();
|
|
4640
|
+
this.overlayLayer.batchDraw();
|
|
3199
4641
|
this.updateLOD();
|
|
3200
4642
|
}
|
|
3201
4643
|
/** The currently AXS-focused section id, or null. */
|
|
@@ -3212,16 +4654,69 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3212
4654
|
if (!sections.length) return;
|
|
3213
4655
|
const backdrop = new Group({ listening: false });
|
|
3214
4656
|
for (const section of sections) {
|
|
3215
|
-
|
|
4657
|
+
const perspective = this.viewMode === "perspective" && section.perspectiveAffine;
|
|
4658
|
+
const outline = perspective ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline;
|
|
4659
|
+
const holes = perspective ? section.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point)))) : section.holes;
|
|
4660
|
+
backdrop.add(polygonWithHolesShape(outline, holes, {
|
|
3216
4661
|
fill: FOCUS_BACKDROP_FILL,
|
|
3217
4662
|
stroke: rgba("#ffffff", 0.1),
|
|
3218
4663
|
strokeWidth: 1,
|
|
3219
4664
|
listening: false
|
|
3220
|
-
}, section.outlinePath));
|
|
4665
|
+
}, perspective ? void 0 : section.outlinePath));
|
|
3221
4666
|
}
|
|
3222
4667
|
this.bgLayer.add(backdrop);
|
|
3223
4668
|
backdrop.moveToTop();
|
|
3224
4669
|
this.focusBackdrop = backdrop;
|
|
4670
|
+
this.drawFocusDimOverlay(sections);
|
|
4671
|
+
}
|
|
4672
|
+
/** Dim everything outside the active section with one paint-only shape. Group
|
|
4673
|
+
* opacity would make Konva recursively invalidate absolute-opacity caches on
|
|
4674
|
+
* all 14k descendants. The cut-out follows the live elevation offset and is
|
|
4675
|
+
* non-listening, so direct seat hit-testing is unchanged. */
|
|
4676
|
+
drawFocusDimOverlay(sections) {
|
|
4677
|
+
this.focusDimOverlay?.destroy();
|
|
4678
|
+
const span = Math.max(this.bounds.width, this.bounds.height, 1);
|
|
4679
|
+
const padding = span * 4 + 1e3;
|
|
4680
|
+
const outer = [
|
|
4681
|
+
{ x: this.bounds.x - padding, y: this.bounds.y - padding },
|
|
4682
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y - padding },
|
|
4683
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y + this.bounds.height + padding },
|
|
4684
|
+
{ x: this.bounds.x - padding, y: this.bounds.y + this.bounds.height + padding }
|
|
4685
|
+
];
|
|
4686
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
4687
|
+
const next = points[(index + 1) % points.length];
|
|
4688
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
4689
|
+
}, 0);
|
|
4690
|
+
const overlay = new Shape({
|
|
4691
|
+
fill: this.canvasBackground,
|
|
4692
|
+
opacity: 1 - FOCUS_DIM_OPACITY,
|
|
4693
|
+
listening: false,
|
|
4694
|
+
perfectDrawEnabled: false,
|
|
4695
|
+
sceneFunc: (context, shape) => {
|
|
4696
|
+
const polygonPath = (points) => {
|
|
4697
|
+
if (!points.length) return;
|
|
4698
|
+
context.moveTo(points[0].x, points[0].y);
|
|
4699
|
+
for (let index = 1; index < points.length; index++) context.lineTo(points[index].x, points[index].y);
|
|
4700
|
+
context.closePath();
|
|
4701
|
+
};
|
|
4702
|
+
context.beginPath();
|
|
4703
|
+
polygonPath(outer);
|
|
4704
|
+
for (const section of sections) {
|
|
4705
|
+
const hole = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
4706
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4707
|
+
return { x: point.x + offset.x, y: point.y + offset.y };
|
|
4708
|
+
});
|
|
4709
|
+
polygonPath(signedArea(hole) > 0 ? [...hole].reverse() : hole);
|
|
4710
|
+
}
|
|
4711
|
+
context.fillStrokeShape(shape);
|
|
4712
|
+
}
|
|
4713
|
+
});
|
|
4714
|
+
this.overlayLayer.add(overlay);
|
|
4715
|
+
overlay.moveToTop();
|
|
4716
|
+
for (const marker of this.selectionMarkers.values()) marker.moveToTop();
|
|
4717
|
+
this.hoverRing.moveToTop();
|
|
4718
|
+
this.focusRing.moveToTop();
|
|
4719
|
+
this.focusDimOverlay = overlay;
|
|
3225
4720
|
}
|
|
3226
4721
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
3227
4722
|
repaintSectionsAndSeats() {
|
|
@@ -3285,7 +4780,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3285
4780
|
this.applyGAFilterState();
|
|
3286
4781
|
}
|
|
3287
4782
|
renderBackground(doc) {
|
|
3288
|
-
|
|
4783
|
+
const buyerBackground = buyerBackgroundImage(doc);
|
|
4784
|
+
if (buyerBackground) this.renderBackgroundImage(buyerBackground);
|
|
3289
4785
|
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
3290
4786
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
3291
4787
|
this.renderZones(doc);
|
|
@@ -3365,10 +4861,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3365
4861
|
listening: false,
|
|
3366
4862
|
perfectDrawEnabled: false
|
|
3367
4863
|
});
|
|
3368
|
-
this.
|
|
4864
|
+
if (obj.layer === "foreground") this.fgDecorGroup.add(node);
|
|
4865
|
+
else this.bgLayer.add(node);
|
|
3369
4866
|
img.onload = () => {
|
|
3370
|
-
|
|
3371
|
-
|
|
4867
|
+
const layer = node.getLayer();
|
|
4868
|
+
if (!layer) return;
|
|
4869
|
+
layer.batchDraw();
|
|
3372
4870
|
};
|
|
3373
4871
|
img.src = obj.href;
|
|
3374
4872
|
}
|
|
@@ -3405,23 +4903,46 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3405
4903
|
})
|
|
3406
4904
|
);
|
|
3407
4905
|
}
|
|
3408
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
4906
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3409
4907
|
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
3410
4908
|
}
|
|
3411
4909
|
renderText(obj) {
|
|
3412
4910
|
const background = this.canvasBackground;
|
|
3413
4911
|
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
4912
|
+
const iconPath = obj.semanticKind === "icon" ? venueIconPath(obj.iconKey) : void 0;
|
|
4913
|
+
if (iconPath) {
|
|
4914
|
+
const scale = obj.fontSize / VENUE_ICON_VIEWBOX;
|
|
4915
|
+
const iconNode = new Path({
|
|
4916
|
+
x: obj.position.x,
|
|
4917
|
+
y: obj.position.y,
|
|
4918
|
+
data: iconPath,
|
|
4919
|
+
rotation: obj.rotation,
|
|
4920
|
+
scaleX: scale,
|
|
4921
|
+
scaleY: scale,
|
|
4922
|
+
stroke: stateAwareBookableLabelInk(background, preferredInk),
|
|
4923
|
+
strokeWidth: VENUE_ICON_STROKE,
|
|
4924
|
+
lineCap: "round",
|
|
4925
|
+
lineJoin: "round",
|
|
4926
|
+
fillEnabled: false,
|
|
4927
|
+
listening: false,
|
|
4928
|
+
perfectDrawEnabled: false
|
|
4929
|
+
});
|
|
4930
|
+
this.iconNodeById.set(obj.id, { node: iconNode, fontSize: obj.fontSize });
|
|
4931
|
+
this.bgLayer.add(iconNode);
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
3414
4934
|
const node = new Text({
|
|
3415
4935
|
x: obj.position.x,
|
|
3416
4936
|
y: obj.position.y,
|
|
3417
4937
|
text: obj.text,
|
|
3418
4938
|
fontSize: obj.fontSize,
|
|
3419
4939
|
rotation: obj.rotation,
|
|
4940
|
+
fontStyle: konvaFontStyle(obj.bold, obj.italic, "normal"),
|
|
3420
4941
|
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3421
4942
|
// the actual canvas. Fail over to readable black/white instead of
|
|
3422
4943
|
// painting an otherwise valid caption invisibly on that active surface.
|
|
3423
4944
|
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3424
|
-
fontFamily: this.labelFont(),
|
|
4945
|
+
fontFamily: obj.fontFamily ?? this.labelFont(),
|
|
3425
4946
|
listening: false,
|
|
3426
4947
|
perfectDrawEnabled: false
|
|
3427
4948
|
});
|
|
@@ -3439,11 +4960,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3439
4960
|
const isDecor = !!obj.role && !isStage;
|
|
3440
4961
|
const palette = overviewPalette(this.canvasBackground);
|
|
3441
4962
|
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
4963
|
+
const isOpenPath = obj.kind === "line" || obj.kind === "polyline";
|
|
4964
|
+
const stroke = obj.stroke?.color ?? (isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : isOpenPath ? "#9aa3b5" : void 0);
|
|
4965
|
+
const strokeWidth = obj.stroke?.width ?? (isStage ? 1 : referenceFocal ? 2 : isOpenPath ? 2 : 0);
|
|
4966
|
+
const opacity = obj.opacity != null ? clamp(obj.opacity, 0.1, 1) : 1;
|
|
3444
4967
|
let cx = 0;
|
|
3445
4968
|
let cy = 0;
|
|
3446
|
-
if (
|
|
4969
|
+
if (isOpenPath && obj.points && obj.points.length >= 2) {
|
|
4970
|
+
cx = obj.points.reduce((a, p) => a + p.x, 0) / obj.points.length;
|
|
4971
|
+
cy = obj.points.reduce((a, p) => a + p.y, 0) / obj.points.length;
|
|
4972
|
+
const lineStyle = resolvedShapeLineStyle(obj);
|
|
4973
|
+
const arrow = shapeArrowMetrics(strokeWidth);
|
|
4974
|
+
const path = new Arrow({
|
|
4975
|
+
points: obj.points.flatMap((p) => [p.x, p.y]),
|
|
4976
|
+
closed: false,
|
|
4977
|
+
x: cx,
|
|
4978
|
+
y: cy,
|
|
4979
|
+
offsetX: cx,
|
|
4980
|
+
offsetY: cy,
|
|
4981
|
+
rotation: obj.rotation ?? 0,
|
|
4982
|
+
stroke,
|
|
4983
|
+
fill: stroke,
|
|
4984
|
+
strokeWidth,
|
|
4985
|
+
opacity,
|
|
4986
|
+
lineCap: lineStyle.lineCap,
|
|
4987
|
+
lineJoin: lineStyle.lineJoin,
|
|
4988
|
+
pointerAtBeginning: lineStyle.startEnding === "arrow",
|
|
4989
|
+
pointerAtEnding: lineStyle.endEnding === "arrow",
|
|
4990
|
+
...arrow,
|
|
4991
|
+
listening: false,
|
|
4992
|
+
name: "buyer-shape-open-path"
|
|
4993
|
+
});
|
|
4994
|
+
path.setAttr("shapeObjectId", obj.id);
|
|
4995
|
+
this.bgLayer.add(path);
|
|
4996
|
+
} else if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
3447
4997
|
const grad = isStage ? {
|
|
3448
4998
|
fillLinearGradientStartPoint: { x: 0, y: 0 },
|
|
3449
4999
|
fillLinearGradientEndPoint: { x: 0, y: obj.height },
|
|
@@ -3463,7 +5013,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3463
5013
|
...grad,
|
|
3464
5014
|
stroke,
|
|
3465
5015
|
strokeWidth,
|
|
3466
|
-
cornerRadius: 4,
|
|
5016
|
+
cornerRadius: clamp(obj.cornerRadius ?? 4, 0, Math.min(obj.width, obj.height) / 2),
|
|
5017
|
+
opacity,
|
|
3467
5018
|
listening: false
|
|
3468
5019
|
})
|
|
3469
5020
|
);
|
|
@@ -3476,7 +5027,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3476
5027
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3477
5028
|
} : { fill };
|
|
3478
5029
|
this.bgLayer.add(
|
|
3479
|
-
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
5030
|
+
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3480
5031
|
);
|
|
3481
5032
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
3482
5033
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -3491,7 +5042,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3491
5042
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3492
5043
|
} : { fill };
|
|
3493
5044
|
this.bgLayer.add(
|
|
3494
|
-
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
5045
|
+
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3495
5046
|
);
|
|
3496
5047
|
}
|
|
3497
5048
|
if (obj.label) {
|
|
@@ -3547,10 +5098,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3547
5098
|
fill: color,
|
|
3548
5099
|
opacity: GA_FILL_OPACITY,
|
|
3549
5100
|
stroke: color,
|
|
3550
|
-
strokeWidth: 1.5
|
|
5101
|
+
strokeWidth: 1.5,
|
|
5102
|
+
cornerRadius: obj.cornerRadius
|
|
3551
5103
|
});
|
|
3552
5104
|
poly.setAttr("gaId", obj.id);
|
|
3553
|
-
poly.on("click tap", () =>
|
|
5105
|
+
poly.on("click tap", (e) => {
|
|
5106
|
+
if (this.isGhostClick(e)) return;
|
|
5107
|
+
this.opts.onGAClick?.(obj.id);
|
|
5108
|
+
});
|
|
3554
5109
|
poly.on("mouseenter", () => {
|
|
3555
5110
|
this.container.style.cursor = "pointer";
|
|
3556
5111
|
});
|
|
@@ -3560,7 +5115,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3560
5115
|
this.bgLayer.add(poly);
|
|
3561
5116
|
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3562
5117
|
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3563
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
5118
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3564
5119
|
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3565
5120
|
this.freeTextById.set(`${obj.id}:label`, {
|
|
3566
5121
|
objectId: obj.id,
|
|
@@ -3577,7 +5132,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3577
5132
|
categoryKey: obj.categoryKey
|
|
3578
5133
|
});
|
|
3579
5134
|
this.gaById.set(obj.id, {
|
|
3580
|
-
label: obj.label,
|
|
5135
|
+
label: obj.displayLabel ?? obj.label,
|
|
3581
5136
|
capacity: obj.capacity,
|
|
3582
5137
|
categoryKey: obj.categoryKey,
|
|
3583
5138
|
points: obj.points,
|
|
@@ -3600,7 +5155,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3600
5155
|
const memberIds = [];
|
|
3601
5156
|
const catCounts = /* @__PURE__ */ new Map();
|
|
3602
5157
|
let free = 0;
|
|
3603
|
-
|
|
5158
|
+
const bounds2 = polyBounds(obj.outline);
|
|
5159
|
+
const candidates = this.seatIndex ? queryRect(this.seatIndex, bounds2).map((id) => this.seatById.get(id)).filter((seat) => !!seat) : this.seats;
|
|
5160
|
+
for (const seat of candidates) {
|
|
3604
5161
|
if (this.seatSection.has(seat.id)) continue;
|
|
3605
5162
|
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
3606
5163
|
memberIds.push(seat.id);
|
|
@@ -3611,11 +5168,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3611
5168
|
[...catCounts].map(([key, w]) => ({ hex: this.catColor.get(key) ?? "#6e7bff", w })),
|
|
3612
5169
|
"#3a4358"
|
|
3613
5170
|
);
|
|
3614
|
-
const elevation =
|
|
3615
|
-
|
|
3616
|
-
|
|
5171
|
+
const elevation = sectionElevationTier(obj.elevation);
|
|
5172
|
+
const geometry = sectionGeometry(obj, {
|
|
5173
|
+
floorBaseHeightM: this.floorBaseHeightMFor(obj.id)
|
|
5174
|
+
});
|
|
5175
|
+
const liftWorld = geometry.height * CHART_UNITS_PER_METRE;
|
|
5176
|
+
const rootGroupBg = new Group({ listening: false });
|
|
5177
|
+
this.bgLayer.add(rootGroupBg);
|
|
5178
|
+
const liftGroupBg = new Group({ listening: false });
|
|
5179
|
+
rootGroupBg.add(liftGroupBg);
|
|
5180
|
+
const liftGroupSeat = new ViewportGroup({ listening: true });
|
|
5181
|
+
this.seatLayer.add(liftGroupSeat);
|
|
3617
5182
|
const sideFaces = [];
|
|
3618
|
-
if (
|
|
5183
|
+
if (liftWorld > 0) {
|
|
3619
5184
|
const faceFill = darken(this.zoneColor.get(obj.zone ?? "") ?? baseFill, 0.42);
|
|
3620
5185
|
for (let i = 0; i < obj.outline.length; i++) {
|
|
3621
5186
|
const face = new Line({
|
|
@@ -3629,14 +5194,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3629
5194
|
perfectDrawEnabled: false
|
|
3630
5195
|
});
|
|
3631
5196
|
sideFaces.push(face);
|
|
3632
|
-
|
|
5197
|
+
rootGroupBg.add(face);
|
|
3633
5198
|
}
|
|
3634
|
-
liftGroupBg = new Group({ listening: false });
|
|
3635
|
-
this.bgLayer.add(liftGroupBg);
|
|
3636
|
-
liftGroupSeat = new Group({ listening: false });
|
|
3637
|
-
this.seatLayer.add(liftGroupSeat);
|
|
3638
5199
|
}
|
|
3639
|
-
|
|
5200
|
+
liftGroupBg.moveToTop();
|
|
5201
|
+
const bgTarget = liftGroupBg;
|
|
3640
5202
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
3641
5203
|
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3642
5204
|
stroke: rgba(outlineTint, 0.5),
|
|
@@ -3710,10 +5272,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3710
5272
|
nameLabelFits: true,
|
|
3711
5273
|
subLabelFits: true,
|
|
3712
5274
|
elevation,
|
|
5275
|
+
liftWorld,
|
|
5276
|
+
rakeDeg: geometry.rake,
|
|
5277
|
+
surfaceFocal: memberIds.length ? { ...this.seatById.get(memberIds[0])?.focalPoint ?? this.isoCentre } : { ...this.chartDoc?.focalPoint ?? this.isoCentre },
|
|
5278
|
+
frontDistanceWorld: 0,
|
|
5279
|
+
rootGroupBg,
|
|
5280
|
+
bgZIndex: rootGroupBg.zIndex(),
|
|
5281
|
+
seatZIndex: liftGroupSeat.zIndex(),
|
|
3713
5282
|
liftGroupBg,
|
|
3714
5283
|
liftGroupSeat,
|
|
3715
5284
|
sideFaces
|
|
3716
5285
|
};
|
|
5286
|
+
const surfaceCandidates = memberIds.map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
5287
|
+
const distanceCandidates = surfaceCandidates.length ? surfaceCandidates : obj.outline;
|
|
5288
|
+
sec.frontDistanceWorld = Math.min(...distanceCandidates.map((point) => Math.hypot(
|
|
5289
|
+
point.x - sec.surfaceFocal.x,
|
|
5290
|
+
point.y - sec.surfaceFocal.y
|
|
5291
|
+
)));
|
|
3717
5292
|
for (const id of memberIds) this.seatSection.set(id, sec);
|
|
3718
5293
|
this.refreshSectionFill(sec);
|
|
3719
5294
|
this.refreshSectionHeat(sec);
|
|
@@ -3736,20 +5311,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3736
5311
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
3737
5312
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
3738
5313
|
}
|
|
3739
|
-
/** Recompute a section's
|
|
5314
|
+
/** Recompute a section's availability shading + retained availability text. */
|
|
3740
5315
|
refreshSectionFill(sec) {
|
|
3741
5316
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
3742
|
-
sec.subLabel.text(
|
|
5317
|
+
sec.subLabel.text(
|
|
5318
|
+
this.sectionAvailabilityState(sec) === "sold-out" ? t("map.soldOut") : t("map.seatsLeft", { count: sec.free })
|
|
5319
|
+
);
|
|
3743
5320
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
3744
5321
|
}
|
|
3745
5322
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
3746
5323
|
isSectionClosed(sec) {
|
|
3747
5324
|
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
3748
5325
|
}
|
|
3749
|
-
/**
|
|
5326
|
+
/** Resolve the four-bucket overview availability state for a section (OV-69).
|
|
5327
|
+
* `closed` (operator) outranks live availability; the availability buckets
|
|
5328
|
+
* only apply to seated sections (GA-only shells have no seat capacity here). */
|
|
5329
|
+
sectionAvailabilityState(sec) {
|
|
5330
|
+
if (this.isSectionClosed(sec)) return "closed";
|
|
5331
|
+
if (sec.total <= 0) return "normal";
|
|
5332
|
+
if (sec.free <= 0) return "sold-out";
|
|
5333
|
+
if (sec.free <= sec.total * SECTION_NEARLY_GONE_RATIO) return "nearly-gone";
|
|
5334
|
+
return "normal";
|
|
5335
|
+
}
|
|
5336
|
+
/** Overview shells stay neutral except for the bounded availability shading
|
|
5337
|
+
* (nearly-gone / sold-out) and the operator `closed` slab — never category
|
|
5338
|
+
* or price paint, which is deferred to section focus/seat detail. */
|
|
3750
5339
|
sectionBlockFill(sec) {
|
|
3751
|
-
|
|
3752
|
-
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
5340
|
+
return overviewStateFill(overviewPalette(this.canvasBackground), this.sectionAvailabilityState(sec));
|
|
3753
5341
|
}
|
|
3754
5342
|
/**
|
|
3755
5343
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -3859,15 +5447,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3859
5447
|
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3860
5448
|
if (sectionOverview) blockT = 1;
|
|
3861
5449
|
if (!this.zones.length) zoneT = 0;
|
|
3862
|
-
this.
|
|
5450
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
5451
|
+
this.seatLayer.opacity(sectionOverview || deferPerspectiveSeatReveal ? 0 : 1 - blockT);
|
|
3863
5452
|
const sx = this.stage.scaleX();
|
|
3864
|
-
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);
|
|
3865
5454
|
if (rescale) this.lodScale = scale;
|
|
3866
5455
|
const focus = this.focusedSectionId;
|
|
3867
5456
|
const palette = overviewPalette(this.canvasBackground);
|
|
3868
5457
|
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3869
5458
|
for (const sec of this.sections) {
|
|
3870
|
-
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;
|
|
3871
5460
|
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3872
5461
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3873
5462
|
sec.blockPoly.stroke(palette.sectionStroke);
|
|
@@ -3879,7 +5468,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3879
5468
|
);
|
|
3880
5469
|
sec.nameLabel.fill(sectionInk);
|
|
3881
5470
|
sec.subLabel.fill(sectionInk);
|
|
3882
|
-
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
5471
|
+
if (rescale && sectionLabelT > 0.01) this.fitSectionRungLabels(sec, sx);
|
|
3883
5472
|
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3884
5473
|
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3885
5474
|
sec.subLabel.opacity(0);
|
|
@@ -3891,12 +5480,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3891
5480
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3892
5481
|
if (rescale) this.sizeZonePill(zone, sx);
|
|
3893
5482
|
}
|
|
3894
|
-
this.
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3899
|
-
|
|
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
|
+
}
|
|
3900
5491
|
}
|
|
3901
5492
|
this.bgLayer.batchDraw();
|
|
3902
5493
|
}
|
|
@@ -4023,13 +5614,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4023
5614
|
screenToWorld(clientPoint) {
|
|
4024
5615
|
const s = this.stage.scaleX();
|
|
4025
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
|
+
}
|
|
4026
5620
|
return this.isoT === 0 ? iso : this.isoInverse(iso);
|
|
4027
5621
|
}
|
|
4028
5622
|
/** Section id under a container-relative screen point, or null (Slice 5 tap-to-zoom). */
|
|
4029
5623
|
sectionAt(clientPoint) {
|
|
4030
5624
|
if (!this.sections.length) return null;
|
|
4031
5625
|
const world = this.screenToWorld(clientPoint);
|
|
4032
|
-
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
|
+
});
|
|
4033
5641
|
return hit ? hit.logicalId : null;
|
|
4034
5642
|
}
|
|
4035
5643
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
@@ -4125,15 +5733,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4125
5733
|
if (!seat) return;
|
|
4126
5734
|
const candidate = this.selectionFocusId === id;
|
|
4127
5735
|
const dims = this.boothDims.get(seat.rowId);
|
|
5736
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
4128
5737
|
const marker = new Group({
|
|
4129
5738
|
name: "selection-ring",
|
|
4130
|
-
x:
|
|
4131
|
-
y:
|
|
5739
|
+
x: rendered.x,
|
|
5740
|
+
y: rendered.y,
|
|
4132
5741
|
rotation: dims?.rotation ?? 0,
|
|
4133
5742
|
listening: false,
|
|
4134
5743
|
perfectDrawEnabled: false,
|
|
4135
5744
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
4136
5745
|
});
|
|
5746
|
+
if (this.viewMode === "perspective") {
|
|
5747
|
+
const projectionScale = this.perspectiveSeatScale.get(id) ?? 1;
|
|
5748
|
+
marker.scale({ x: projectionScale, y: projectionScale });
|
|
5749
|
+
}
|
|
4137
5750
|
marker.setAttr("seatId", id);
|
|
4138
5751
|
const common = {
|
|
4139
5752
|
stroke: this.effSelection,
|
|
@@ -4212,12 +5825,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4212
5825
|
* must fall through and pick.
|
|
4213
5826
|
*/
|
|
4214
5827
|
sectionBounds(id) {
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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));
|
|
4221
5844
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
4222
5845
|
}
|
|
4223
5846
|
sectionFrameScale(id) {
|
|
@@ -4270,15 +5893,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4270
5893
|
* hijacking a clean tap on empty space beyond the slop.
|
|
4271
5894
|
*/
|
|
4272
5895
|
nearestSeatToScreen(screen, slopPx) {
|
|
4273
|
-
const s = this.stage.scaleX() || 1;
|
|
4274
|
-
const reachWorld = this.seatR + slopPx / s;
|
|
4275
|
-
const world = this.screenToWorld(screen);
|
|
4276
5896
|
let best = null;
|
|
4277
|
-
let bestD =
|
|
5897
|
+
let bestD = Infinity;
|
|
5898
|
+
const stageScale = this.stage.scaleX() || 1;
|
|
4278
5899
|
for (const seat of this.seats) {
|
|
4279
5900
|
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
4280
|
-
const
|
|
4281
|
-
|
|
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) {
|
|
4282
5906
|
bestD = d;
|
|
4283
5907
|
best = seat.id;
|
|
4284
5908
|
}
|
|
@@ -4288,6 +5912,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4288
5912
|
wireInteraction() {
|
|
4289
5913
|
this.seatLayer.on("click tap", (e) => {
|
|
4290
5914
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5915
|
+
if (this.isGhostClick(e)) return;
|
|
4291
5916
|
const id = seatIdOf(e.target);
|
|
4292
5917
|
if (!id) return;
|
|
4293
5918
|
this.handleSeatTap(id);
|
|
@@ -4297,13 +5922,17 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4297
5922
|
if (!id) return;
|
|
4298
5923
|
const seat = this.seatById.get(id);
|
|
4299
5924
|
if (!seat) return;
|
|
4300
|
-
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 });
|
|
4301
5929
|
this.hoverRing.visible(true);
|
|
4302
5930
|
this.overlayLayer.batchDraw();
|
|
4303
5931
|
this.container.style.cursor = "pointer";
|
|
4304
5932
|
this.opts.onHover?.(seat);
|
|
4305
5933
|
});
|
|
4306
5934
|
this.seatLayer.on("mouseout", () => {
|
|
5935
|
+
this.hoveredId = null;
|
|
4307
5936
|
this.hoverRing.visible(false);
|
|
4308
5937
|
this.overlayLayer.batchDraw();
|
|
4309
5938
|
this.container.style.cursor = "default";
|
|
@@ -4318,6 +5947,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4318
5947
|
});
|
|
4319
5948
|
this.stage.on("click tap", (e) => {
|
|
4320
5949
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5950
|
+
if (this.isGhostClick(e)) return;
|
|
4321
5951
|
const pointer = this.stage.getPointerPosition();
|
|
4322
5952
|
if (!pointer) return;
|
|
4323
5953
|
if (!this.cached) {
|
|
@@ -4334,11 +5964,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4334
5964
|
}
|
|
4335
5965
|
}
|
|
4336
5966
|
if (this.sections.length) {
|
|
4337
|
-
const
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
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);
|
|
4342
5971
|
return;
|
|
4343
5972
|
}
|
|
4344
5973
|
}
|
|
@@ -4355,13 +5984,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4355
5984
|
*/
|
|
4356
5985
|
deckFloorAt() {
|
|
4357
5986
|
if (!this.objectFloor.size) return null;
|
|
4358
|
-
const
|
|
4359
|
-
if (!
|
|
5987
|
+
const pointer = this.stage.getPointerPosition();
|
|
5988
|
+
if (!pointer) return null;
|
|
5989
|
+
const p = this.screenToWorld(pointer);
|
|
4360
5990
|
let best = null;
|
|
4361
5991
|
let bestD = Infinity;
|
|
4362
5992
|
for (const s of this.seats) {
|
|
4363
|
-
const
|
|
4364
|
-
const
|
|
5993
|
+
const rendered = this.renderedSeatPoint(s);
|
|
5994
|
+
const dx = rendered.x - p.x;
|
|
5995
|
+
const dy = rendered.y - p.y;
|
|
4365
5996
|
const d = dx * dx + dy * dy;
|
|
4366
5997
|
if (d < bestD) {
|
|
4367
5998
|
bestD = d;
|
|
@@ -4448,9 +6079,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4448
6079
|
}
|
|
4449
6080
|
const start = performance.now();
|
|
4450
6081
|
const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
|
|
6082
|
+
this.glideInProgress = true;
|
|
4451
6083
|
const step = (now) => {
|
|
4452
6084
|
if (this.destroyed) {
|
|
4453
6085
|
this.glideRaf = 0;
|
|
6086
|
+
this.glideInProgress = false;
|
|
4454
6087
|
return;
|
|
4455
6088
|
}
|
|
4456
6089
|
const raw = Math.min(1, (now - start) / duration);
|
|
@@ -4458,6 +6091,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4458
6091
|
const sc = fromScale + (toScale - fromScale) * e;
|
|
4459
6092
|
this.stage.scale({ x: sc, y: sc });
|
|
4460
6093
|
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
6094
|
+
this.updateSeatGroupVisibility();
|
|
4461
6095
|
this.updateLOD();
|
|
4462
6096
|
this.scheduleViewChange();
|
|
4463
6097
|
this.stage.batchDraw();
|
|
@@ -4465,6 +6099,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4465
6099
|
this.glideRaf = requestAnimationFrame(step);
|
|
4466
6100
|
} else {
|
|
4467
6101
|
this.glideRaf = 0;
|
|
6102
|
+
this.glideInProgress = false;
|
|
4468
6103
|
this.afterViewChange();
|
|
4469
6104
|
}
|
|
4470
6105
|
};
|
|
@@ -4476,6 +6111,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4476
6111
|
cancelAnimationFrame(this.glideRaf);
|
|
4477
6112
|
this.glideRaf = 0;
|
|
4478
6113
|
}
|
|
6114
|
+
this.glideInProgress = false;
|
|
4479
6115
|
}
|
|
4480
6116
|
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
4481
6117
|
getRung() {
|
|
@@ -4492,11 +6128,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4492
6128
|
const labels = this.seats.map((seat) => {
|
|
4493
6129
|
const shape = this.circleById.get(seat.id);
|
|
4494
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;
|
|
4495
6133
|
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4496
|
-
const
|
|
6134
|
+
const labelEffectiveScale = this.viewMode === "perspective" && seat.kind !== "booth" ? effectiveScale * localPerspectiveScale : seatEffectiveScale;
|
|
6135
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * labelEffectiveScale);
|
|
4497
6136
|
const screen = this.worldToScreen(seat);
|
|
4498
6137
|
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4499
|
-
const opacity = shape?.
|
|
6138
|
+
const opacity = this.focusedSeatOpacity(seat.id, shape?.getAbsoluteOpacity() ?? 0);
|
|
4500
6139
|
const section = this.seatSection.get(seat.id);
|
|
4501
6140
|
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4502
6141
|
let hiddenReason;
|
|
@@ -4507,17 +6146,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4507
6146
|
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4508
6147
|
else hiddenReason = "renderer-hidden";
|
|
4509
6148
|
}
|
|
4510
|
-
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4511
|
-
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4512
|
-
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale : this.seatR * 2 *
|
|
4513
|
-
const directHeightPx = shape instanceof Rect ? shape.height() * stageScale : this.seatR * 2 *
|
|
4514
|
-
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 Rect ? shape.width() * stageScale * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6152
|
+
const directHeightPx = shape instanceof Rect ? shape.height() * (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
6153
|
+
const assistedDiameterPx = 2 * (this.seatR * seatEffectiveScale + SEAT_TAP_SLOP_PX);
|
|
4515
6154
|
const fill = shape?.fill();
|
|
4516
6155
|
const ink = label?.fill();
|
|
6156
|
+
const accessGlyph = this.accessGlyphById.get(seat.id);
|
|
6157
|
+
const accessGlyphScale = accessGlyph?.getAbsoluteScale();
|
|
4517
6158
|
return {
|
|
4518
6159
|
seatId: seat.id,
|
|
4519
6160
|
label: seat.label,
|
|
4520
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 } : {},
|
|
4521
6164
|
categoryKey: seat.categoryKey,
|
|
4522
6165
|
...section ? { sectionId: section.id } : {},
|
|
4523
6166
|
...section?.zone ? { zoneId: section.zone } : {},
|
|
@@ -4528,8 +6171,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4528
6171
|
fill: typeof fill === "string" ? fill : "",
|
|
4529
6172
|
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4530
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
|
+
} : {},
|
|
4531
6181
|
pointerTarget: {
|
|
4532
|
-
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),
|
|
4533
6183
|
directWidthPx: rounded(directWidthPx),
|
|
4534
6184
|
directHeightPx: rounded(directHeightPx),
|
|
4535
6185
|
effectiveMinimumPx: rounded(Math.max(
|
|
@@ -4557,7 +6207,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4557
6207
|
node.height(),
|
|
4558
6208
|
node.rotation()
|
|
4559
6209
|
);
|
|
4560
|
-
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
|
+
})));
|
|
4561
6215
|
const opacity = rounded(node.opacity());
|
|
4562
6216
|
const ink = node.fill();
|
|
4563
6217
|
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
@@ -4689,13 +6343,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4689
6343
|
};
|
|
4690
6344
|
});
|
|
4691
6345
|
const palette = overviewPalette(this.canvasBackground);
|
|
4692
|
-
const neutralSectionFills =
|
|
4693
|
-
palette.
|
|
4694
|
-
|
|
4695
|
-
]);
|
|
6346
|
+
const neutralSectionFills = new Set(
|
|
6347
|
+
["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
|
|
6348
|
+
);
|
|
4696
6349
|
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4697
6350
|
return {
|
|
4698
6351
|
viewport,
|
|
6352
|
+
projection: this.viewMode,
|
|
6353
|
+
...this.viewMode === "perspective" ? {
|
|
6354
|
+
perspective: {
|
|
6355
|
+
model: "pinhole-exact-seat-anchors",
|
|
6356
|
+
sectionSurfaceModel: "tangent-plane",
|
|
6357
|
+
exactSeatAnchorCount: this.perspectiveSeatProjected.size,
|
|
6358
|
+
depthSorted: true
|
|
6359
|
+
}
|
|
6360
|
+
} : {},
|
|
4699
6361
|
canvasBackground: this.canvasBackground,
|
|
4700
6362
|
effectiveScale: rounded(effectiveScale),
|
|
4701
6363
|
rung: this.getRung(),
|
|
@@ -4750,10 +6412,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4750
6412
|
let cy = viewCentre.y;
|
|
4751
6413
|
if (rung === "sections" && this.sections.length > 0) {
|
|
4752
6414
|
const sectionCentres = this.sections.map((section) => {
|
|
4753
|
-
const
|
|
6415
|
+
const bounds2 = polyBounds(section.outline);
|
|
4754
6416
|
return {
|
|
4755
|
-
x:
|
|
4756
|
-
y:
|
|
6417
|
+
x: bounds2.x + bounds2.width / 2,
|
|
6418
|
+
y: bounds2.y + bounds2.height / 2
|
|
4757
6419
|
};
|
|
4758
6420
|
});
|
|
4759
6421
|
const halfWidth = w / (target * 2);
|
|
@@ -4820,6 +6482,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4820
6482
|
}
|
|
4821
6483
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
4822
6484
|
afterViewChange() {
|
|
6485
|
+
this.updateSeatGroupVisibility();
|
|
4823
6486
|
this.updateLOD();
|
|
4824
6487
|
this.updateFreeTextVisibility();
|
|
4825
6488
|
this.updateLabels();
|
|
@@ -4844,10 +6507,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4844
6507
|
this.paintGAStateForView();
|
|
4845
6508
|
this.updateAccessGlyphs(scale);
|
|
4846
6509
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
6510
|
+
const suppressPerspectiveSeatCache = this.viewMode === "perspective" && this.hasSections && this.seats.length > 2500 && shouldCache;
|
|
6511
|
+
if (suppressPerspectiveSeatCache) {
|
|
6512
|
+
this.seatLayer.listening(false);
|
|
6513
|
+
return;
|
|
6514
|
+
}
|
|
4847
6515
|
if (shouldCache && !this.cached) {
|
|
4848
6516
|
this.cacheSeatLayer();
|
|
4849
|
-
} else if (!shouldCache && this.cached) {
|
|
4850
|
-
this.seatLayer.clearCache();
|
|
6517
|
+
} else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
|
|
6518
|
+
if (this.cached) this.seatLayer.clearCache();
|
|
4851
6519
|
this.seatLayer.listening(true);
|
|
4852
6520
|
this.cached = false;
|
|
4853
6521
|
this.seatLayer.batchDraw();
|
|
@@ -4856,6 +6524,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4856
6524
|
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
4857
6525
|
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
4858
6526
|
rebuildSeatCache() {
|
|
6527
|
+
this.updateSeatGroupVisibility();
|
|
4859
6528
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
4860
6529
|
this.seatLayer.clearCache();
|
|
4861
6530
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
@@ -4900,6 +6569,63 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4900
6569
|
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
4901
6570
|
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
4902
6571
|
}
|
|
6572
|
+
for (const { node, fontSize } of this.iconNodeById.values()) {
|
|
6573
|
+
node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
/** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
|
|
6577
|
+
* world space, matching the Designer's placement contract (start/end/both/none
|
|
6578
|
+
* preset, a free-drag `position` that wins over the preset, rotation, and
|
|
6579
|
+
* labelStyle colour/size fed through the same auto-contrast guard). Segmented
|
|
6580
|
+
* rows keep their own logical-label handling and are resolved by their owner. */
|
|
6581
|
+
buildRowLabelPlan(view) {
|
|
6582
|
+
this.rowLabelPlan = [];
|
|
6583
|
+
const background = this.theme.background ?? "#0e1117";
|
|
6584
|
+
const seatsByRow = /* @__PURE__ */ new Map();
|
|
6585
|
+
for (const seat of this.seats) {
|
|
6586
|
+
if (seat.kind === "booth") continue;
|
|
6587
|
+
const key = seat.logicalRowId ?? seat.rowId;
|
|
6588
|
+
(seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
|
|
6589
|
+
}
|
|
6590
|
+
const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
|
|
6591
|
+
for (const obj of view.objects) {
|
|
6592
|
+
if (obj.type !== "row") continue;
|
|
6593
|
+
if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
|
|
6594
|
+
const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
|
|
6595
|
+
if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
|
|
6596
|
+
const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
|
|
6597
|
+
const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
|
|
6598
|
+
if (!seats.length) continue;
|
|
6599
|
+
const first = seats[0];
|
|
6600
|
+
const last = seats[seats.length - 1];
|
|
6601
|
+
const labelStyle = presentation?.labelStyle;
|
|
6602
|
+
const ink = stateAwareBookableLabelInk(
|
|
6603
|
+
background,
|
|
6604
|
+
labelStyle?.color ?? this.theme.rowLabelColor ?? this.theme.textColor ?? "#e6e9f0"
|
|
6605
|
+
);
|
|
6606
|
+
const fontSize = labelStyle?.size ?? 12;
|
|
6607
|
+
const rotation = presentation?.rotation ?? 0;
|
|
6608
|
+
const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
|
|
6609
|
+
const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
|
|
6610
|
+
const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
|
|
6611
|
+
if (presentation?.position) {
|
|
6612
|
+
push(presentation.position.x, presentation.position.y);
|
|
6613
|
+
continue;
|
|
6614
|
+
}
|
|
6615
|
+
const radians = obj.rotation * Math.PI / 180;
|
|
6616
|
+
const along = { x: Math.cos(radians), y: Math.sin(radians) };
|
|
6617
|
+
const gap = this.seatR + 12;
|
|
6618
|
+
const preset = presentation?.positionPreset ?? "start";
|
|
6619
|
+
if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
|
|
6620
|
+
if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
/** Row labels never carry status; the buyer just dims them under the same
|
|
6624
|
+
* category/price filters that dim their seats. */
|
|
6625
|
+
objectFilteredOut(obj) {
|
|
6626
|
+
const key = obj.categoryKey;
|
|
6627
|
+
if (!key) return false;
|
|
6628
|
+
return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
|
|
4903
6629
|
}
|
|
4904
6630
|
updateLabels() {
|
|
4905
6631
|
const effectiveScale = this.effScale();
|
|
@@ -4907,31 +6633,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4907
6633
|
for (const [id, label] of this.boothLabelById) {
|
|
4908
6634
|
const shape = this.circleById.get(id);
|
|
4909
6635
|
label.visible(
|
|
4910
|
-
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.
|
|
6636
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && Boolean(shape?.isVisible()) && this.focusedSeatOpacity(id, shape?.getAbsoluteOpacity() ?? 1) >= 0.5
|
|
4911
6637
|
);
|
|
4912
6638
|
}
|
|
4913
6639
|
this.labelGroup.destroyChildren();
|
|
6640
|
+
this.labelLiftGroups.clear();
|
|
4914
6641
|
this.seatLabelById.clear();
|
|
4915
6642
|
if (!show) {
|
|
4916
6643
|
this.overlayLayer.batchDraw();
|
|
4917
6644
|
return;
|
|
4918
6645
|
}
|
|
4919
|
-
const s = this.stage.scaleX();
|
|
4920
|
-
const x0 = -this.stage.x() / s;
|
|
4921
|
-
const y0 = -this.stage.y() / s;
|
|
4922
|
-
const x1 = (this.stage.width() - this.stage.x()) / s;
|
|
4923
|
-
const y1 = (this.stage.height() - this.stage.y()) / s;
|
|
4924
6646
|
let count = 0;
|
|
4925
|
-
for (const seat of this.
|
|
6647
|
+
for (const seat of this.visibleSeatCandidates()) {
|
|
4926
6648
|
if (seat.kind === "booth") continue;
|
|
4927
|
-
|
|
4928
|
-
if (
|
|
6649
|
+
const screen = this.worldToScreen(seat);
|
|
6650
|
+
if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
|
|
6651
|
+
if (this.accessGlyphById.has(seat.id) && this.accessGlyphShouldShow(seat.id)) continue;
|
|
4929
6652
|
const shape = this.circleById.get(seat.id);
|
|
4930
|
-
if (
|
|
6653
|
+
if (!shape?.isVisible() || this.focusedSeatOpacity(seat.id, shape.getAbsoluteOpacity()) < 0.5) continue;
|
|
4931
6654
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
6655
|
+
const labelPoint = this.renderedSeatPoint(seat);
|
|
6656
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4932
6657
|
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
4933
6658
|
if (unavailable) {
|
|
4934
|
-
const cue = new Group({ x:
|
|
6659
|
+
const cue = new Group({ x: labelPoint.x, y: labelPoint.y, listening: false });
|
|
6660
|
+
cue.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
4935
6661
|
if (status === "held") {
|
|
4936
6662
|
cue.add(new Rect({
|
|
4937
6663
|
x: -4.2,
|
|
@@ -4960,14 +6686,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4960
6686
|
listening: false
|
|
4961
6687
|
}));
|
|
4962
6688
|
}
|
|
4963
|
-
this.
|
|
6689
|
+
this.seatLabelContainer(seat.id).add(cue);
|
|
4964
6690
|
if (++count >= MAX_LABELS) break;
|
|
4965
6691
|
continue;
|
|
4966
6692
|
}
|
|
4967
6693
|
const authoredFontSize = SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4968
6694
|
const t2 = new Text({
|
|
4969
|
-
x:
|
|
4970
|
-
y:
|
|
6695
|
+
x: labelPoint.x,
|
|
6696
|
+
y: labelPoint.y,
|
|
4971
6697
|
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4972
6698
|
fontSize: authoredFontSize,
|
|
4973
6699
|
fontStyle: "600",
|
|
@@ -4988,11 +6714,36 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4988
6714
|
}
|
|
4989
6715
|
t2.offsetX(t2.width() / 2);
|
|
4990
6716
|
t2.offsetY(t2.height() / 2);
|
|
4991
|
-
|
|
6717
|
+
t2.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
6718
|
+
this.seatLabelContainer(seat.id).add(t2);
|
|
4992
6719
|
this.seatLabelById.set(seat.id, t2);
|
|
4993
6720
|
if (++count >= MAX_LABELS) break;
|
|
4994
6721
|
}
|
|
4995
|
-
|
|
6722
|
+
for (const rl of this.rowLabelPlan) {
|
|
6723
|
+
const screen = this.worldToScreen({ x: rl.x, y: rl.y });
|
|
6724
|
+
if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
|
|
6725
|
+
const t2 = new Text({
|
|
6726
|
+
x: rl.x,
|
|
6727
|
+
y: rl.y,
|
|
6728
|
+
text: rl.text,
|
|
6729
|
+
fontSize: rl.fontSize,
|
|
6730
|
+
fontStyle: "700",
|
|
6731
|
+
fontFamily: this.labelFont(),
|
|
6732
|
+
fill: rl.ink,
|
|
6733
|
+
rotation: rl.rotation,
|
|
6734
|
+
opacity: rl.opacity,
|
|
6735
|
+
shadowColor: "#05070c",
|
|
6736
|
+
shadowBlur: rl.ink.toLowerCase() === "#ffffff" ? 5 : 0,
|
|
6737
|
+
shadowOpacity: rl.ink.toLowerCase() === "#ffffff" ? 0.8 : 0,
|
|
6738
|
+
shadowForStrokeEnabled: false,
|
|
6739
|
+
listening: false,
|
|
6740
|
+
perfectDrawEnabled: false
|
|
6741
|
+
});
|
|
6742
|
+
t2.offsetX(t2.width() / 2);
|
|
6743
|
+
t2.offsetY(t2.height() / 2);
|
|
6744
|
+
this.labelGroup.add(t2);
|
|
6745
|
+
}
|
|
6746
|
+
if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
|
|
4996
6747
|
this.overlayLayer.batchDraw();
|
|
4997
6748
|
}
|
|
4998
6749
|
handleResize() {
|
|
@@ -5058,6 +6809,7 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
5058
6809
|
|
|
5059
6810
|
// src/core/bestAvailable.ts
|
|
5060
6811
|
function seatIndex2(seat, fallback) {
|
|
6812
|
+
if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
|
|
5061
6813
|
const i = seat.id.lastIndexOf(":");
|
|
5062
6814
|
if (i < 0) return fallback;
|
|
5063
6815
|
const n = Number(seat.id.slice(i + 1));
|
|
@@ -5077,21 +6829,25 @@ function centroid(seats) {
|
|
|
5077
6829
|
}
|
|
5078
6830
|
return { x: x / seats.length, y: y / seats.length };
|
|
5079
6831
|
}
|
|
6832
|
+
function candidateFocal(seats, fallback) {
|
|
6833
|
+
return seats.find((seat) => seat.focalPoint)?.focalPoint ?? fallback;
|
|
6834
|
+
}
|
|
5080
6835
|
function isPremium(seat) {
|
|
5081
6836
|
return seat.commercial?.premium === true;
|
|
5082
6837
|
}
|
|
5083
6838
|
function pickBestAvailable(seats, available, opts) {
|
|
5084
6839
|
const qty = Math.floor(opts.qty);
|
|
5085
6840
|
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
5086
|
-
const { categoryKey, focal, preferPremium } = opts;
|
|
6841
|
+
const { categoryKey, zoneId, focal, preferPremium } = opts;
|
|
5087
6842
|
const rows = /* @__PURE__ */ new Map();
|
|
5088
6843
|
const eligibleAll = [];
|
|
5089
6844
|
seats.forEach((seat, i) => {
|
|
5090
|
-
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
5091
|
-
|
|
6845
|
+
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey) && (!zoneId || seat.zoneId === zoneId);
|
|
6846
|
+
const rowId = seat.logicalRowId ?? seat.rowId;
|
|
6847
|
+
let arr = rows.get(rowId);
|
|
5092
6848
|
if (!arr) {
|
|
5093
6849
|
arr = [];
|
|
5094
|
-
rows.set(
|
|
6850
|
+
rows.set(rowId, arr);
|
|
5095
6851
|
}
|
|
5096
6852
|
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
5097
6853
|
if (elig) eligibleAll.push(seat);
|
|
@@ -5116,7 +6872,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5116
6872
|
continue;
|
|
5117
6873
|
}
|
|
5118
6874
|
let j = i;
|
|
5119
|
-
while (j < slots.length && slots[j].elig) j++;
|
|
6875
|
+
while (j < slots.length && slots[j].elig && (j === i || slots[j].index === slots[j - 1].index + 1)) j++;
|
|
5120
6876
|
const segLen = j - i;
|
|
5121
6877
|
for (let p = 0; p + qty <= segLen; p++) {
|
|
5122
6878
|
const leftRem = p;
|
|
@@ -5130,7 +6886,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5130
6886
|
startIndex: slots[i + p].index,
|
|
5131
6887
|
orphan,
|
|
5132
6888
|
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
5133
|
-
d2: dist2(centroid(runSeats), focal)
|
|
6889
|
+
d2: dist2(centroid(runSeats), candidateFocal(runSeats, focal))
|
|
5134
6890
|
};
|
|
5135
6891
|
if (better(c)) best = c;
|
|
5136
6892
|
}
|
|
@@ -5139,20 +6895,71 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5139
6895
|
}
|
|
5140
6896
|
if (best) return { labels: best.labels };
|
|
5141
6897
|
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
5142
|
-
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
|
|
6898
|
+
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, seat.focalPoint ?? focal) })).sort((a, b) => {
|
|
5143
6899
|
if (preferPremium) {
|
|
5144
6900
|
const pa = isPremium(a.seat) ? 0 : 1;
|
|
5145
6901
|
const pb = isPremium(b.seat) ? 0 : 1;
|
|
5146
6902
|
if (pa !== pb) return pa - pb;
|
|
5147
6903
|
}
|
|
5148
6904
|
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
5149
|
-
const r = a.seat.rowId.localeCompare(b.seat.rowId);
|
|
6905
|
+
const r = (a.seat.logicalRowId ?? a.seat.rowId).localeCompare(b.seat.logicalRowId ?? b.seat.rowId);
|
|
5150
6906
|
if (r !== 0) return r;
|
|
5151
6907
|
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
5152
6908
|
});
|
|
5153
6909
|
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
5154
6910
|
}
|
|
5155
6911
|
|
|
6912
|
+
// src/core/tableInventory.ts
|
|
6913
|
+
var GroupedTableProjectionError = class extends Error {
|
|
6914
|
+
constructor(objectId, message) {
|
|
6915
|
+
super(message);
|
|
6916
|
+
this.objectId = objectId;
|
|
6917
|
+
this.name = "GroupedTableProjectionError";
|
|
6918
|
+
}
|
|
6919
|
+
};
|
|
6920
|
+
function bounds(table) {
|
|
6921
|
+
const min = table.minOccupancy;
|
|
6922
|
+
const max = table.maxOccupancy;
|
|
6923
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > table.seatCount) {
|
|
6924
|
+
throw new GroupedTableProjectionError(
|
|
6925
|
+
table.id,
|
|
6926
|
+
`Table "${table.label}" has invalid variable-occupancy bounds`
|
|
6927
|
+
);
|
|
6928
|
+
}
|
|
6929
|
+
return { min, max };
|
|
6930
|
+
}
|
|
6931
|
+
function groupedTableInventory(doc, modelVersion) {
|
|
6932
|
+
if (modelVersion !== 2) return [];
|
|
6933
|
+
const units = [];
|
|
6934
|
+
for (const object of allObjects(doc)) {
|
|
6935
|
+
if (object.type !== "table" || !object.bookAsWhole && !object.variableOccupancy) continue;
|
|
6936
|
+
if (object.bookAsWhole && object.variableOccupancy) {
|
|
6937
|
+
throw new GroupedTableProjectionError(
|
|
6938
|
+
object.id,
|
|
6939
|
+
`Table "${object.label}" cannot be whole-table and variable-occupancy inventory`
|
|
6940
|
+
);
|
|
6941
|
+
}
|
|
6942
|
+
if (!Number.isInteger(object.seatCount) || object.seatCount < 1 || !object.label.trim()) {
|
|
6943
|
+
throw new GroupedTableProjectionError(object.id, "Grouped tables require a label and at least one chair");
|
|
6944
|
+
}
|
|
6945
|
+
const mode = object.variableOccupancy ? "variable" : "whole";
|
|
6946
|
+
const occupancy = mode === "variable" ? bounds(object) : { min: object.seatCount, max: object.seatCount };
|
|
6947
|
+
units.push({
|
|
6948
|
+
objectId: object.id,
|
|
6949
|
+
label: object.label,
|
|
6950
|
+
...object.displayLabel ? { displayLabel: object.displayLabel } : {},
|
|
6951
|
+
...object.displayType ? { displayType: object.displayType } : {},
|
|
6952
|
+
categoryKey: object.categoryKey,
|
|
6953
|
+
mode,
|
|
6954
|
+
capacity: object.seatCount,
|
|
6955
|
+
minOccupancy: occupancy.min,
|
|
6956
|
+
maxOccupancy: occupancy.max,
|
|
6957
|
+
chairs: expandTable(object)
|
|
6958
|
+
});
|
|
6959
|
+
}
|
|
6960
|
+
return units;
|
|
6961
|
+
}
|
|
6962
|
+
|
|
5156
6963
|
// src/picker/PickerController.ts
|
|
5157
6964
|
var DEFAULT_MAX_SELECTION = 10;
|
|
5158
6965
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -5184,6 +6991,14 @@ var PickerController = class {
|
|
|
5184
6991
|
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
5185
6992
|
this.seatContext = /* @__PURE__ */ new Map();
|
|
5186
6993
|
this.allIds = [];
|
|
6994
|
+
/** Model-2 tables are one booking unit whose chairs remain renderer geometry. */
|
|
6995
|
+
this.groupedTablesByObject = /* @__PURE__ */ new Map();
|
|
6996
|
+
this.groupedTablesByLabel = /* @__PURE__ */ new Map();
|
|
6997
|
+
this.groupedTableBySeatId = /* @__PURE__ */ new Map();
|
|
6998
|
+
this.tableQuantities = /* @__PURE__ */ new Map();
|
|
6999
|
+
/** Variable quantity is a buyer contract, never an inferred default. */
|
|
7000
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
7001
|
+
this.groupedSelectionOverhead = 0;
|
|
5187
7002
|
// realtime socket
|
|
5188
7003
|
this.ws = null;
|
|
5189
7004
|
this.reconnectTimer = null;
|
|
@@ -5245,6 +7060,20 @@ var PickerController = class {
|
|
|
5245
7060
|
idForLabel(label) {
|
|
5246
7061
|
return this.labelToId.get(label);
|
|
5247
7062
|
}
|
|
7063
|
+
idsForLabel(label) {
|
|
7064
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7065
|
+
if (table) return table.chairs.map((chair) => chair.id);
|
|
7066
|
+
const id = this.labelToId.get(label);
|
|
7067
|
+
return id ? [id] : [];
|
|
7068
|
+
}
|
|
7069
|
+
idsForLabels(labels) {
|
|
7070
|
+
return [...new Set([...labels].flatMap((label) => this.idsForLabel(label)))];
|
|
7071
|
+
}
|
|
7072
|
+
/** Resolve a physical chair id (or table inventory label) to its table unit. */
|
|
7073
|
+
tableSelection(seatIdOrLabel) {
|
|
7074
|
+
const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
|
|
7075
|
+
return group ? this.toTableSeat(group) : null;
|
|
7076
|
+
}
|
|
5248
7077
|
/** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
|
|
5249
7078
|
async render(host) {
|
|
5250
7079
|
if (this.renderer) return null;
|
|
@@ -5258,6 +7087,29 @@ var PickerController = class {
|
|
|
5258
7087
|
}
|
|
5259
7088
|
if (this.closed) return null;
|
|
5260
7089
|
this._doc = res.doc;
|
|
7090
|
+
try {
|
|
7091
|
+
const groups = groupedTableInventory(
|
|
7092
|
+
res.doc,
|
|
7093
|
+
res.event.inventoryModelVersion === 2 ? 2 : 1
|
|
7094
|
+
);
|
|
7095
|
+
this.groupedTablesByObject = new Map(groups.map((group) => [group.objectId, group]));
|
|
7096
|
+
this.groupedTablesByLabel = new Map(groups.map((group) => [group.label, group]));
|
|
7097
|
+
this.groupedTableBySeatId = new Map(
|
|
7098
|
+
groups.flatMap((group) => group.chairs.map((chair) => [chair.id, group]))
|
|
7099
|
+
);
|
|
7100
|
+
this.tableQuantities = new Map(groups.map((group) => [
|
|
7101
|
+
group.label,
|
|
7102
|
+
group.mode === "whole" ? group.capacity : group.minOccupancy
|
|
7103
|
+
]));
|
|
7104
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
7105
|
+
this.groupedSelectionOverhead = groups.reduce(
|
|
7106
|
+
(sum, group) => sum + Math.max(0, group.chairs.length - 1),
|
|
7107
|
+
0
|
|
7108
|
+
);
|
|
7109
|
+
} catch (err) {
|
|
7110
|
+
this.emitError(err);
|
|
7111
|
+
return null;
|
|
7112
|
+
}
|
|
5261
7113
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
5262
7114
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
5263
7115
|
this.seatById = /* @__PURE__ */ new Map();
|
|
@@ -5275,32 +7127,43 @@ var PickerController = class {
|
|
|
5275
7127
|
this.allIds.push(s.id);
|
|
5276
7128
|
const source = chartObjects.get(s.rowId);
|
|
5277
7129
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
5278
|
-
const
|
|
5279
|
-
const
|
|
5280
|
-
const
|
|
7130
|
+
const logicalRow = source?.type === "row" ? source.segmentedRow : void 0;
|
|
7131
|
+
const sourceDisplayLabel = logicalRow?.displayLabel ?? (source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel);
|
|
7132
|
+
const groupedTable = this.groupedTablesByObject.get(s.rowId);
|
|
7133
|
+
const objectType = source?.type === "table" ? "table" : source?.type === "booth" ? "booth" : "seat";
|
|
7134
|
+
const rowLabel = sourceDisplayLabel;
|
|
7135
|
+
const rowType = logicalRow?.displayType?.trim() || (source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0);
|
|
5281
7136
|
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
5282
7137
|
const labelParts = visibleSeatLabel.split("-");
|
|
5283
|
-
const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) :
|
|
7138
|
+
const seatNumber = groupedTable ? void 0 : s.kind === "booth" ? void 0 : sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
|
|
5284
7139
|
this.seatContext.set(s.id, {
|
|
7140
|
+
objectId: s.rowId,
|
|
7141
|
+
objectType,
|
|
5285
7142
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
5286
7143
|
rowLabel,
|
|
5287
7144
|
seatNumber,
|
|
5288
|
-
rowType
|
|
7145
|
+
...rowType ? { displayType: rowType, rowType } : {}
|
|
5289
7146
|
});
|
|
5290
7147
|
}
|
|
7148
|
+
for (const group of this.groupedTablesByLabel.values()) {
|
|
7149
|
+
const representative = group.chairs[0];
|
|
7150
|
+
if (!representative) continue;
|
|
7151
|
+
this.labelToId.set(group.label, representative.id);
|
|
7152
|
+
this.labelToSeat.set(group.label, representative);
|
|
7153
|
+
}
|
|
5291
7154
|
const currency = res.event.currency ?? this.opts.currency;
|
|
5292
7155
|
this.currency = currency ?? "USD";
|
|
5293
7156
|
const renderer = createRenderer(host, {
|
|
5294
|
-
|
|
7157
|
+
// Group chairs are selected together for paint, but count as one logical
|
|
7158
|
+
// inventory line. The controller enforces the guest-weighted cap.
|
|
7159
|
+
maxSelection: this.rendererSelectionCap(),
|
|
5295
7160
|
confirmSelection: this.opts.confirmSelection,
|
|
5296
7161
|
currency,
|
|
5297
7162
|
onSelect: (seat) => {
|
|
5298
|
-
this.
|
|
5299
|
-
this.emitSelectionChange();
|
|
7163
|
+
this.handleRendererSelect(seat);
|
|
5300
7164
|
},
|
|
5301
7165
|
onDeselect: (seat) => {
|
|
5302
|
-
this.
|
|
5303
|
-
this.emitSelectionChange();
|
|
7166
|
+
this.handleRendererDeselect(seat);
|
|
5304
7167
|
},
|
|
5305
7168
|
onSelectionLimit: this.opts.onSelectionLimit,
|
|
5306
7169
|
onHover: (seat) => {
|
|
@@ -5355,7 +7218,20 @@ var PickerController = class {
|
|
|
5355
7218
|
// ---- selection ------------------------------------------------------------
|
|
5356
7219
|
getSelection() {
|
|
5357
7220
|
if (!this.renderer) return [];
|
|
5358
|
-
|
|
7221
|
+
const out = [];
|
|
7222
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
7223
|
+
for (const seat of this.renderer.getSelection()) {
|
|
7224
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7225
|
+
if (table) {
|
|
7226
|
+
if (!grouped.has(table.label)) {
|
|
7227
|
+
grouped.add(table.label);
|
|
7228
|
+
out.push(this.toTableSeat(table));
|
|
7229
|
+
}
|
|
7230
|
+
} else {
|
|
7231
|
+
out.push(this.toSeat(seat));
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
return out;
|
|
5359
7235
|
}
|
|
5360
7236
|
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
5361
7237
|
seatDetails(seatId) {
|
|
@@ -5364,20 +7240,168 @@ var PickerController = class {
|
|
|
5364
7240
|
}
|
|
5365
7241
|
clearSelection() {
|
|
5366
7242
|
this.renderer?.clearSelection();
|
|
7243
|
+
this.resetUnheldTableQuantities();
|
|
5367
7244
|
this.emitSelectionChange();
|
|
5368
7245
|
}
|
|
5369
7246
|
deselect(ids) {
|
|
5370
|
-
|
|
7247
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7248
|
+
for (const id of ids) {
|
|
7249
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7250
|
+
if (table) {
|
|
7251
|
+
table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7252
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7253
|
+
this.tableQuantities.set(
|
|
7254
|
+
table.label,
|
|
7255
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7256
|
+
);
|
|
7257
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7258
|
+
}
|
|
7259
|
+
} else expanded.add(id);
|
|
7260
|
+
}
|
|
7261
|
+
this.renderer?.deselect([...expanded]);
|
|
5371
7262
|
this.emitSelectionChange();
|
|
5372
7263
|
}
|
|
5373
7264
|
setMaxSelection(maxSelection) {
|
|
5374
7265
|
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
5375
|
-
this.renderer?.setMaxSelection?.(this.
|
|
7266
|
+
this.renderer?.setMaxSelection?.(this.rendererSelectionCap());
|
|
5376
7267
|
}
|
|
5377
7268
|
select(ids) {
|
|
5378
|
-
const
|
|
7269
|
+
const before = new Set(this.getSelection().map((seat) => seat.label));
|
|
7270
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
7271
|
+
for (const id of ids) {
|
|
7272
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
7273
|
+
if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
7274
|
+
else expanded.add(id);
|
|
7275
|
+
}
|
|
7276
|
+
this.renderer?.select?.([...expanded]);
|
|
7277
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7278
|
+
this.renderer?.deselect([...expanded]);
|
|
7279
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7280
|
+
return [];
|
|
7281
|
+
}
|
|
7282
|
+
const added = this.getSelection().filter((seat) => !before.has(seat.label));
|
|
5379
7283
|
if (added.length) this.emitSelectionChange();
|
|
5380
|
-
return added
|
|
7284
|
+
return added;
|
|
7285
|
+
}
|
|
7286
|
+
/** Confirm/update the guest quantity for a selected variable table. */
|
|
7287
|
+
setTableQuantity(label, quantity) {
|
|
7288
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7289
|
+
if (!table) return false;
|
|
7290
|
+
const q = table.mode === "whole" ? table.capacity : Math.floor(quantity);
|
|
7291
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return false;
|
|
7292
|
+
const previous = this.tableQuantities.get(label) ?? table.minOccupancy;
|
|
7293
|
+
this.tableQuantities.set(label, q);
|
|
7294
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7295
|
+
this.tableQuantities.set(label, previous);
|
|
7296
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7297
|
+
return false;
|
|
7298
|
+
}
|
|
7299
|
+
if (table.mode === "variable") this.confirmedVariableTables.add(label);
|
|
7300
|
+
this.emitSelectionChange();
|
|
7301
|
+
return true;
|
|
7302
|
+
}
|
|
7303
|
+
/** Atomically replace an active variable-table hold with a new guest count. */
|
|
7304
|
+
async replaceTableQuantity(label, quantity) {
|
|
7305
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7306
|
+
const hold = this.hold_;
|
|
7307
|
+
if (!table || table.mode !== "variable" || !hold?.labels.includes(label)) return null;
|
|
7308
|
+
const q = Math.floor(quantity);
|
|
7309
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return null;
|
|
7310
|
+
const otherGuests = (hold.items ?? []).filter((item) => item.label !== label).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
7311
|
+
if (otherGuests + q > this.maxSelection) {
|
|
7312
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7313
|
+
return null;
|
|
7314
|
+
}
|
|
7315
|
+
const selections = (hold.items ?? []).map((item) => ({
|
|
7316
|
+
label: item.label,
|
|
7317
|
+
tierId: item.tierId,
|
|
7318
|
+
quantity: item.label === label ? q : item.quantity
|
|
7319
|
+
}));
|
|
7320
|
+
if (!selections.some((item) => item.label === label)) return null;
|
|
7321
|
+
const result = await this.api.hold(this.key, selections, void 0, hold.holdId);
|
|
7322
|
+
this.tableQuantities.set(label, q);
|
|
7323
|
+
this.setHold({
|
|
7324
|
+
holdId: result.holdId,
|
|
7325
|
+
labels: selections.map((item) => item.label),
|
|
7326
|
+
expiresAt: result.expiresAt,
|
|
7327
|
+
items: result.items
|
|
7328
|
+
});
|
|
7329
|
+
return this.hold_;
|
|
7330
|
+
}
|
|
7331
|
+
rendererSelectionCap() {
|
|
7332
|
+
return this.maxSelection + this.groupedSelectionOverhead;
|
|
7333
|
+
}
|
|
7334
|
+
selectionGuestCount() {
|
|
7335
|
+
return this.getSelection().reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
|
|
7336
|
+
}
|
|
7337
|
+
handleRendererSelect(seat) {
|
|
7338
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7339
|
+
if (table) {
|
|
7340
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7341
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7342
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7343
|
+
this.emitSelectionChange();
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
this.renderer?.select?.(table.chairs.map((chair) => chair.id));
|
|
7347
|
+
this.opts.onSelect?.(table.chairs[0] ?? seat);
|
|
7348
|
+
this.opts.onTableSelectionRequest?.(this.toTableSeat(table));
|
|
7349
|
+
this.emitSelectionChange();
|
|
7350
|
+
return;
|
|
7351
|
+
}
|
|
7352
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
7353
|
+
this.renderer?.deselect([seat.id]);
|
|
7354
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
7355
|
+
this.emitSelectionChange();
|
|
7356
|
+
return;
|
|
7357
|
+
}
|
|
7358
|
+
this.opts.onSelect?.(seat);
|
|
7359
|
+
this.emitSelectionChange();
|
|
7360
|
+
}
|
|
7361
|
+
handleRendererDeselect(seat) {
|
|
7362
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7363
|
+
if (table) {
|
|
7364
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7365
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7366
|
+
this.tableQuantities.set(
|
|
7367
|
+
table.label,
|
|
7368
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7369
|
+
);
|
|
7370
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7371
|
+
}
|
|
7372
|
+
this.opts.onDeselect?.(table.chairs[0] ?? seat);
|
|
7373
|
+
} else {
|
|
7374
|
+
this.opts.onDeselect?.(seat);
|
|
7375
|
+
}
|
|
7376
|
+
this.emitSelectionChange();
|
|
7377
|
+
}
|
|
7378
|
+
resetUnheldTableQuantities() {
|
|
7379
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7380
|
+
if (this.hold_?.labels.includes(table.label)) continue;
|
|
7381
|
+
this.tableQuantities.set(
|
|
7382
|
+
table.label,
|
|
7383
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7384
|
+
);
|
|
7385
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
resetTableQuantitiesForLabels(labels) {
|
|
7389
|
+
for (const label of labels) {
|
|
7390
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7391
|
+
if (!table) continue;
|
|
7392
|
+
this.tableQuantities.set(
|
|
7393
|
+
label,
|
|
7394
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7395
|
+
);
|
|
7396
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(label);
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
tableQuantityRequest(seat) {
|
|
7400
|
+
if (seat?.objectType !== "table") return {};
|
|
7401
|
+
if (seat.bookingMode === "variable" && !this.confirmedVariableTables.has(seat.label)) {
|
|
7402
|
+
throw new Error("picker: confirm a variable table guest quantity before holding");
|
|
7403
|
+
}
|
|
7404
|
+
return { quantity: seat.quantity };
|
|
5381
7405
|
}
|
|
5382
7406
|
// ---- booking machine ------------------------------------------------------
|
|
5383
7407
|
/**
|
|
@@ -5389,7 +7413,7 @@ var PickerController = class {
|
|
|
5389
7413
|
async hold(labelsArg, ttlMs) {
|
|
5390
7414
|
const r = this.renderer;
|
|
5391
7415
|
if (!r) return null;
|
|
5392
|
-
const labels = labelsArg ??
|
|
7416
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5393
7417
|
const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
|
|
5394
7418
|
const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
|
|
5395
7419
|
if (!combinedLabels.length) return null;
|
|
@@ -5397,10 +7421,14 @@ var PickerController = class {
|
|
|
5397
7421
|
try {
|
|
5398
7422
|
const selections = combinedLabels.map((label) => {
|
|
5399
7423
|
const ga = existingGA.find((item) => item.label === label);
|
|
5400
|
-
if (ga) return { label, tierId: ga.tierId };
|
|
7424
|
+
if (ga) return { label, tierId: ga.tierId, quantity: ga.quantity };
|
|
5401
7425
|
const seat = this.labelToSeat.get(label);
|
|
5402
7426
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5403
|
-
return {
|
|
7427
|
+
return {
|
|
7428
|
+
label,
|
|
7429
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7430
|
+
...this.tableQuantityRequest(resolved)
|
|
7431
|
+
};
|
|
5404
7432
|
});
|
|
5405
7433
|
const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
|
|
5406
7434
|
this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
|
|
@@ -5452,11 +7480,19 @@ var PickerController = class {
|
|
|
5452
7480
|
const out = {};
|
|
5453
7481
|
const closedMembers = this.closedMemberIds();
|
|
5454
7482
|
for (const [id, s] of this.seatById) {
|
|
7483
|
+
if (this.groupedTableBySeatId.has(id)) continue;
|
|
5455
7484
|
if (closedMembers.has(id)) continue;
|
|
5456
7485
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
5457
7486
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
5458
7487
|
}
|
|
5459
7488
|
}
|
|
7489
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7490
|
+
if (table.chairs.some((chair) => closedMembers.has(chair.id))) continue;
|
|
7491
|
+
const representative = table.chairs[0];
|
|
7492
|
+
if (representative && (this.getStatus(representative.id) ?? "free") === "free") {
|
|
7493
|
+
out[table.categoryKey] = (out[table.categoryKey] ?? 0) + table.maxOccupancy;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
5460
7496
|
return out;
|
|
5461
7497
|
}
|
|
5462
7498
|
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
@@ -5475,6 +7511,8 @@ var PickerController = class {
|
|
|
5475
7511
|
return {
|
|
5476
7512
|
id: area.id,
|
|
5477
7513
|
label: area.label,
|
|
7514
|
+
...area.displayLabel ? { displayLabel: area.displayLabel } : {},
|
|
7515
|
+
...area.displayType ? { displayType: area.displayType } : {},
|
|
5478
7516
|
capacity: Math.max(0, Math.floor(area.capacity)),
|
|
5479
7517
|
available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
|
|
5480
7518
|
categoryKey: area.categoryKey,
|
|
@@ -5493,14 +7531,20 @@ var PickerController = class {
|
|
|
5493
7531
|
const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
|
|
5494
7532
|
if (labels.length !== Math.floor(qty)) return null;
|
|
5495
7533
|
const selections = /* @__PURE__ */ new Map();
|
|
5496
|
-
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7534
|
+
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7535
|
+
label: item.label,
|
|
7536
|
+
tierId: item.tierId,
|
|
7537
|
+
quantity: item.quantity
|
|
7538
|
+
});
|
|
5497
7539
|
if (this.hold_ && !this.hold_?.items?.length) {
|
|
5498
7540
|
for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
|
|
5499
7541
|
}
|
|
5500
|
-
for (const seat of this.
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
7542
|
+
for (const seat of this.getSelection()) {
|
|
7543
|
+
selections.set(seat.label, {
|
|
7544
|
+
label: seat.label,
|
|
7545
|
+
...seat.tierId ? { tierId: seat.tierId } : {},
|
|
7546
|
+
...this.tableQuantityRequest(seat)
|
|
7547
|
+
});
|
|
5504
7548
|
}
|
|
5505
7549
|
for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
|
|
5506
7550
|
const combined = [...selections.values()];
|
|
@@ -5517,6 +7561,15 @@ var PickerController = class {
|
|
|
5517
7561
|
}
|
|
5518
7562
|
return false;
|
|
5519
7563
|
}
|
|
7564
|
+
/** Zones which still own visible buyer inventory, in authored order. */
|
|
7565
|
+
getBestAvailableZones() {
|
|
7566
|
+
const doc = this.visibleDoc();
|
|
7567
|
+
if (!doc?.zones?.length) return [];
|
|
7568
|
+
const used = new Set(
|
|
7569
|
+
computeSections(doc).sections.filter((section) => section.seatCount > 0 && !!section.zone).map((section) => section.zone)
|
|
7570
|
+
);
|
|
7571
|
+
return doc.zones.filter((zone) => used.has(zone.id)).map((zone) => ({ id: zone.id, label: zone.label }));
|
|
7572
|
+
}
|
|
5520
7573
|
/**
|
|
5521
7574
|
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5522
7575
|
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
@@ -5525,11 +7578,12 @@ var PickerController = class {
|
|
|
5525
7578
|
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5526
7579
|
* exists (the caller then falls back to the normal server pick).
|
|
5527
7580
|
*/
|
|
5528
|
-
pickPremiumBlock(qty, categoryKey) {
|
|
7581
|
+
pickPremiumBlock(qty, categoryKey, zoneId) {
|
|
5529
7582
|
const doc = this.visibleDoc();
|
|
5530
7583
|
if (!doc?.objects?.length) return null;
|
|
5531
7584
|
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5532
|
-
const
|
|
7585
|
+
const groupedObjectIds = new Set(this.groupedTablesByObject.keys());
|
|
7586
|
+
const seats = expandChart(doc).filter((seat) => !groupedObjectIds.has(seat.rowId));
|
|
5533
7587
|
const held = new Set(this.hold_?.labels ?? []);
|
|
5534
7588
|
const available = /* @__PURE__ */ new Set();
|
|
5535
7589
|
for (const seat of seats) {
|
|
@@ -5538,7 +7592,7 @@ var PickerController = class {
|
|
|
5538
7592
|
const status = id ? this.getStatus(id) : void 0;
|
|
5539
7593
|
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5540
7594
|
}
|
|
5541
|
-
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
7595
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, zoneId, focal, preferPremium: true });
|
|
5542
7596
|
if (pick.labels.length !== qty) return null;
|
|
5543
7597
|
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5544
7598
|
return allPremium ? [...pick.labels] : null;
|
|
@@ -5553,7 +7607,7 @@ var PickerController = class {
|
|
|
5553
7607
|
const r = this.renderer;
|
|
5554
7608
|
if (!r) return null;
|
|
5555
7609
|
if (opts.preferPremium) {
|
|
5556
|
-
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
7610
|
+
const block = this.pickPremiumBlock(qty, categoryKey, opts.zoneId);
|
|
5557
7611
|
if (block) {
|
|
5558
7612
|
if (this.hold_ && !await this.release()) return null;
|
|
5559
7613
|
try {
|
|
@@ -5564,7 +7618,7 @@ var PickerController = class {
|
|
|
5564
7618
|
});
|
|
5565
7619
|
const result = await this.api.hold(this.key, selection);
|
|
5566
7620
|
r.clearSelection();
|
|
5567
|
-
const ids =
|
|
7621
|
+
const ids = this.idsForLabels(block);
|
|
5568
7622
|
if (ids.length) r.setStatus(ids, "held");
|
|
5569
7623
|
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5570
7624
|
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5581,9 +7635,9 @@ var PickerController = class {
|
|
|
5581
7635
|
}
|
|
5582
7636
|
if (this.hold_ && !await this.release()) return null;
|
|
5583
7637
|
try {
|
|
5584
|
-
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
7638
|
+
const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
|
|
5585
7639
|
r.clearSelection();
|
|
5586
|
-
const ids =
|
|
7640
|
+
const ids = this.idsForLabels(result.labels);
|
|
5587
7641
|
if (ids.length) r.setStatus(ids, "held");
|
|
5588
7642
|
this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
|
|
5589
7643
|
const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5607,7 +7661,7 @@ var PickerController = class {
|
|
|
5607
7661
|
const r = this.renderer;
|
|
5608
7662
|
if (!r) return null;
|
|
5609
7663
|
if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
|
|
5610
|
-
const labels = labelsArg ??
|
|
7664
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5611
7665
|
if (!labels.length) return null;
|
|
5612
7666
|
let holdId;
|
|
5613
7667
|
try {
|
|
@@ -5620,7 +7674,11 @@ var PickerController = class {
|
|
|
5620
7674
|
labels.map((label) => {
|
|
5621
7675
|
const seat = this.labelToSeat.get(label);
|
|
5622
7676
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5623
|
-
return {
|
|
7677
|
+
return {
|
|
7678
|
+
label,
|
|
7679
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7680
|
+
...this.tableQuantityRequest(resolved)
|
|
7681
|
+
};
|
|
5624
7682
|
}),
|
|
5625
7683
|
void 0,
|
|
5626
7684
|
replaceHoldId
|
|
@@ -5636,10 +7694,11 @@ var PickerController = class {
|
|
|
5636
7694
|
this.opts.onSalesClosed?.();
|
|
5637
7695
|
} else if (status === 409 && conflicts?.length) {
|
|
5638
7696
|
const takenLabels = new Set(conflicts.map((c) => c.label));
|
|
5639
|
-
const takenIds =
|
|
7697
|
+
const takenIds = this.idsForLabels(takenLabels);
|
|
5640
7698
|
if (takenIds.length) {
|
|
5641
7699
|
r.setStatus(takenIds, "booked");
|
|
5642
7700
|
r.deselect(takenIds);
|
|
7701
|
+
this.resetTableQuantitiesForLabels(takenLabels);
|
|
5643
7702
|
}
|
|
5644
7703
|
const stillFree = labels.filter((l) => !takenLabels.has(l));
|
|
5645
7704
|
if (stillFree.length && holdId) void this.api.release(this.key, stillFree, holdId).catch(() => {
|
|
@@ -5651,12 +7710,13 @@ var PickerController = class {
|
|
|
5651
7710
|
}
|
|
5652
7711
|
throw err;
|
|
5653
7712
|
}
|
|
5654
|
-
const ids =
|
|
7713
|
+
const ids = this.idsForLabels(labels);
|
|
5655
7714
|
if (ids.length) {
|
|
5656
7715
|
r.setStatus(ids, "booked");
|
|
5657
7716
|
r.deselect(ids);
|
|
5658
7717
|
}
|
|
5659
7718
|
this.clearHold();
|
|
7719
|
+
this.resetTableQuantitiesForLabels(labels);
|
|
5660
7720
|
this.emitSelectionChange();
|
|
5661
7721
|
this.opts.onBook?.(bookingRef);
|
|
5662
7722
|
return labels;
|
|
@@ -5677,7 +7737,8 @@ var PickerController = class {
|
|
|
5677
7737
|
}
|
|
5678
7738
|
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
5679
7739
|
this.clearHold();
|
|
5680
|
-
|
|
7740
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
7741
|
+
const ids = this.idsForLabels(hold.labels);
|
|
5681
7742
|
if (ids.length) {
|
|
5682
7743
|
this.renderer?.deselect(ids);
|
|
5683
7744
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5708,7 +7769,8 @@ var PickerController = class {
|
|
|
5708
7769
|
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
5709
7770
|
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
5710
7771
|
else this.clearHold();
|
|
5711
|
-
|
|
7772
|
+
this.resetTableQuantitiesForLabels(drop);
|
|
7773
|
+
const ids = this.idsForLabels(drop);
|
|
5712
7774
|
if (ids.length) {
|
|
5713
7775
|
this.renderer?.deselect(ids);
|
|
5714
7776
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5797,7 +7859,7 @@ var PickerController = class {
|
|
|
5797
7859
|
this.opts.onDeckTap?.(floorId);
|
|
5798
7860
|
}
|
|
5799
7861
|
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
5800
|
-
/** Switch the map projection
|
|
7862
|
+
/** Switch the map projection. No-op on a flat-only renderer. */
|
|
5801
7863
|
setViewMode(mode) {
|
|
5802
7864
|
this.renderer?.setViewMode?.(mode);
|
|
5803
7865
|
}
|
|
@@ -5896,12 +7958,21 @@ var PickerController = class {
|
|
|
5896
7958
|
if (!sec) return null;
|
|
5897
7959
|
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
5898
7960
|
const byCat = /* @__PURE__ */ new Map();
|
|
7961
|
+
const seenTables = /* @__PURE__ */ new Set();
|
|
5899
7962
|
let seatsLeft = 0;
|
|
5900
7963
|
for (const sid of memberIds) {
|
|
5901
7964
|
const seat = this.seatById.get(sid);
|
|
5902
7965
|
if (!seat) continue;
|
|
5903
|
-
|
|
5904
|
-
if (
|
|
7966
|
+
const table = this.groupedTableBySeatId.get(sid);
|
|
7967
|
+
if (table) {
|
|
7968
|
+
if (seenTables.has(table.label)) continue;
|
|
7969
|
+
seenTables.add(table.label);
|
|
7970
|
+
byCat.set(table.categoryKey, (byCat.get(table.categoryKey) ?? 0) + table.maxOccupancy);
|
|
7971
|
+
if (r.getStatus(table.chairs[0]?.id ?? sid) === "free") seatsLeft += table.maxOccupancy;
|
|
7972
|
+
} else {
|
|
7973
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
7974
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
7975
|
+
}
|
|
5905
7976
|
}
|
|
5906
7977
|
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
5907
7978
|
const cat = doc.categories.find((c) => c.key === key);
|
|
@@ -5918,7 +7989,7 @@ var PickerController = class {
|
|
|
5918
7989
|
const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
|
|
5919
7990
|
return {
|
|
5920
7991
|
id,
|
|
5921
|
-
label: sec.label,
|
|
7992
|
+
label: sec.displayLabel ?? sec.label,
|
|
5922
7993
|
zoneLabel: zone?.label ?? "",
|
|
5923
7994
|
...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
|
|
5924
7995
|
color,
|
|
@@ -5951,22 +8022,54 @@ var PickerController = class {
|
|
|
5951
8022
|
}
|
|
5952
8023
|
// ---- internals ------------------------------------------------------------
|
|
5953
8024
|
toSeat(s) {
|
|
8025
|
+
const table = this.groupedTableBySeatId.get(s.id);
|
|
8026
|
+
if (table) return this.toTableSeat(table);
|
|
5954
8027
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
5955
8028
|
const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
|
|
8029
|
+
const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
|
|
8030
|
+
const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
|
|
8031
|
+
const context = this.seatContext.get(s.id);
|
|
5956
8032
|
const tiers = this.tiersFor(s.categoryKey);
|
|
5957
8033
|
if (!tiers) {
|
|
5958
|
-
return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
8034
|
+
return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
|
|
5959
8035
|
}
|
|
5960
8036
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
5961
8037
|
return {
|
|
5962
8038
|
id: s.id,
|
|
5963
8039
|
label: s.label,
|
|
5964
8040
|
...display,
|
|
8041
|
+
...context,
|
|
5965
8042
|
categoryKey: s.categoryKey,
|
|
5966
8043
|
price: chosen.price,
|
|
5967
8044
|
tiers,
|
|
5968
8045
|
tierId: chosen.id,
|
|
5969
|
-
...commercial
|
|
8046
|
+
...commercial,
|
|
8047
|
+
...accessibility,
|
|
8048
|
+
...wheelchair
|
|
8049
|
+
};
|
|
8050
|
+
}
|
|
8051
|
+
toTableSeat(table) {
|
|
8052
|
+
const representative = table.chairs[0];
|
|
8053
|
+
const tiers = this.tiersFor(table.categoryKey);
|
|
8054
|
+
const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
|
|
8055
|
+
return {
|
|
8056
|
+
id: representative?.id ?? table.objectId,
|
|
8057
|
+
label: table.label,
|
|
8058
|
+
displayLabel: table.displayLabel ?? table.label,
|
|
8059
|
+
...table.displayType ? { displayType: table.displayType, rowType: table.displayType } : {},
|
|
8060
|
+
objectId: table.objectId,
|
|
8061
|
+
sectionLabel: representative ? this.seatContext.get(representative.id)?.sectionLabel : void 0,
|
|
8062
|
+
rowLabel: table.displayLabel ?? table.label,
|
|
8063
|
+
categoryKey: table.categoryKey,
|
|
8064
|
+
price: chosen?.price ?? this.priceFor(table.categoryKey),
|
|
8065
|
+
...tiers ? { tiers, tierId: chosen?.id } : {},
|
|
8066
|
+
objectType: "table",
|
|
8067
|
+
bookingMode: table.mode,
|
|
8068
|
+
quantity: this.tableQuantities.get(table.label) ?? (table.mode === "whole" ? table.capacity : table.minOccupancy),
|
|
8069
|
+
capacity: table.capacity,
|
|
8070
|
+
minOccupancy: table.minOccupancy,
|
|
8071
|
+
maxOccupancy: table.maxOccupancy,
|
|
8072
|
+
physicalSeatIds: table.chairs.map((chair) => chair.id)
|
|
5970
8073
|
};
|
|
5971
8074
|
}
|
|
5972
8075
|
/** Tooltip payload for a hovered seat — see PickerCallbacks.onSeatHover. */
|
|
@@ -6050,10 +8153,13 @@ var PickerController = class {
|
|
|
6050
8153
|
const h = this.hold_;
|
|
6051
8154
|
if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
|
|
6052
8155
|
const tiers = new Map((h.items ?? []).map((item) => [item.label, item.tierId]));
|
|
8156
|
+
const quantities = new Map((h.items ?? []).map((item) => [item.label, item.quantity ?? 1]));
|
|
6053
8157
|
return labels.every((label) => {
|
|
6054
8158
|
const seat = this.labelToSeat.get(label);
|
|
6055
|
-
const
|
|
6056
|
-
|
|
8159
|
+
const resolved = seat ? this.toSeat(seat) : null;
|
|
8160
|
+
const currentTier = resolved?.tierId ?? null;
|
|
8161
|
+
const quantityMatches = resolved?.objectType !== "table" || (resolved.bookingMode !== "variable" || this.confirmedVariableTables.has(label)) && (!quantities.has(label) || quantities.get(label) === resolved.quantity);
|
|
8162
|
+
return quantityMatches && (!tiers.has(label) || tiers.get(label) === currentTier);
|
|
6057
8163
|
});
|
|
6058
8164
|
}
|
|
6059
8165
|
handle409Conflicts(err) {
|
|
@@ -6061,19 +8167,28 @@ var PickerController = class {
|
|
|
6061
8167
|
if (status !== 409 || !conflicts?.length) return;
|
|
6062
8168
|
const r = this.renderer;
|
|
6063
8169
|
if (!r) return;
|
|
6064
|
-
const takenIds = conflicts.map((
|
|
8170
|
+
const takenIds = this.idsForLabels(conflicts.map((conflict) => conflict.label));
|
|
6065
8171
|
if (takenIds.length) {
|
|
6066
8172
|
r.deselect(takenIds);
|
|
6067
8173
|
r.setStatus(takenIds, "held");
|
|
8174
|
+
this.resetTableQuantitiesForLabels(conflicts.map((conflict) => conflict.label));
|
|
6068
8175
|
}
|
|
6069
8176
|
this.emitSelectionChange();
|
|
6070
8177
|
}
|
|
6071
8178
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
6072
8179
|
setHold(hold, source = "created") {
|
|
8180
|
+
for (const item of hold.items ?? []) {
|
|
8181
|
+
if (this.groupedTablesByLabel.has(item.label) && item.quantity != null) {
|
|
8182
|
+
this.tableQuantities.set(item.label, item.quantity);
|
|
8183
|
+
if (this.groupedTablesByLabel.get(item.label)?.mode === "variable") {
|
|
8184
|
+
this.confirmedVariableTables.add(item.label);
|
|
8185
|
+
}
|
|
8186
|
+
}
|
|
8187
|
+
}
|
|
6073
8188
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
6074
8189
|
this.hold_ = full;
|
|
6075
8190
|
this.renderer?.setOwnedHold?.(
|
|
6076
|
-
|
|
8191
|
+
this.idsForLabels(full.labels)
|
|
6077
8192
|
);
|
|
6078
8193
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
6079
8194
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
@@ -6116,12 +8231,11 @@ var PickerController = class {
|
|
|
6116
8231
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
6117
8232
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
6118
8233
|
r.setOwnedHold?.(
|
|
6119
|
-
(this.hold_?.labels ?? [])
|
|
8234
|
+
this.idsForLabels(this.hold_?.labels ?? [])
|
|
6120
8235
|
);
|
|
6121
8236
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
6122
8237
|
for (const [label, st] of Object.entries(seats)) {
|
|
6123
|
-
|
|
6124
|
-
if (id) byStatus[mapStatus(st)].push(id);
|
|
8238
|
+
byStatus[mapStatus(st)].push(...this.idsForLabel(label));
|
|
6125
8239
|
}
|
|
6126
8240
|
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
6127
8241
|
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
@@ -6130,7 +8244,11 @@ var PickerController = class {
|
|
|
6130
8244
|
this.clearBookedHoldIfSettled();
|
|
6131
8245
|
}
|
|
6132
8246
|
clearBookedHoldIfSettled() {
|
|
6133
|
-
|
|
8247
|
+
const hold = this.hold_;
|
|
8248
|
+
if (hold?.labels.every((label) => this.liveStatuses.get(label) === "booked")) {
|
|
8249
|
+
this.clearHold();
|
|
8250
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
8251
|
+
}
|
|
6134
8252
|
}
|
|
6135
8253
|
async resnapshot() {
|
|
6136
8254
|
try {
|
|
@@ -6219,13 +8337,13 @@ var PickerController = class {
|
|
|
6219
8337
|
} else if (Array.isArray(m.changes)) {
|
|
6220
8338
|
for (const ch of m.changes) {
|
|
6221
8339
|
this.liveStatuses.set(ch.label, ch.status);
|
|
6222
|
-
const
|
|
6223
|
-
if (!
|
|
8340
|
+
const ids = this.idsForLabel(ch.label);
|
|
8341
|
+
if (!ids.length) continue;
|
|
6224
8342
|
const next = mapStatus(ch.status);
|
|
6225
|
-
if (this.opts.flashOnLiveChange && next !== "free" && r.getStatus(id) === "free" && !this.hold_?.labels.includes(ch.label)) {
|
|
6226
|
-
r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e");
|
|
8343
|
+
if (this.opts.flashOnLiveChange && next !== "free" && ids.some((id) => r.getStatus(id) === "free") && !this.hold_?.labels.includes(ch.label)) {
|
|
8344
|
+
ids.forEach((id) => r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e"));
|
|
6227
8345
|
}
|
|
6228
|
-
r.setStatus(
|
|
8346
|
+
r.setStatus(ids, next);
|
|
6229
8347
|
}
|
|
6230
8348
|
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
6231
8349
|
r.forceDraw();
|
|
@@ -6256,10 +8374,25 @@ var PickerController = class {
|
|
|
6256
8374
|
}
|
|
6257
8375
|
};
|
|
6258
8376
|
|
|
8377
|
+
// src/view/sightline.ts
|
|
8378
|
+
var STAGE_TOP_M = 5;
|
|
8379
|
+
var STAGE_BASE_M = -1.1;
|
|
8380
|
+
var MAX_LOOKDOWN_DEG = 35;
|
|
8381
|
+
function stageSightlinePitch(eyeHeightM, distM) {
|
|
8382
|
+
const extraEyeM = eyeHeightM - SEATED_EYE_HEIGHT_M;
|
|
8383
|
+
let topPitch = Math.atan2(STAGE_TOP_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8384
|
+
let basePitch = Math.atan2(STAGE_BASE_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8385
|
+
if (basePitch < -MAX_LOOKDOWN_DEG) {
|
|
8386
|
+
topPitch += -MAX_LOOKDOWN_DEG - basePitch;
|
|
8387
|
+
basePitch = -MAX_LOOKDOWN_DEG;
|
|
8388
|
+
}
|
|
8389
|
+
return { topPitch, basePitch };
|
|
8390
|
+
}
|
|
8391
|
+
|
|
6259
8392
|
// src/view/generatePanorama.ts
|
|
6260
8393
|
var W = 2048;
|
|
6261
8394
|
var H = 1024;
|
|
6262
|
-
var UNIT =
|
|
8395
|
+
var UNIT = METRES_PER_CHART_UNIT;
|
|
6263
8396
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
6264
8397
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
6265
8398
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
@@ -6279,9 +8412,11 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
6279
8412
|
sky.addColorStop(1, "#07090f");
|
|
6280
8413
|
ctx.fillStyle = sky;
|
|
6281
8414
|
ctx.fillRect(0, 0, W, H);
|
|
8415
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
6282
8416
|
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6283
|
-
const
|
|
6284
|
-
const
|
|
8417
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8418
|
+
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8419
|
+
const stageBasePitch = sightline.basePitch;
|
|
6285
8420
|
const sx0 = yawToX(-stageHalfYaw);
|
|
6286
8421
|
const sx1 = yawToX(stageHalfYaw);
|
|
6287
8422
|
const sy0 = pitchToY(stageTopPitch);
|
|
@@ -6388,9 +8523,11 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6388
8523
|
sky.addColorStop(1, "#070910");
|
|
6389
8524
|
ctx.fillStyle = sky;
|
|
6390
8525
|
ctx.fillRect(0, 0, TW, TH);
|
|
8526
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8527
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
6391
8528
|
const stageHalfYaw = Math.min(HALF_HFOV - 2, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6392
|
-
const stageTopPitch = Math.min(HALF_VFOV - 2,
|
|
6393
|
-
const stageBasePitch = Math.max(-HALF_VFOV + 2,
|
|
8529
|
+
const stageTopPitch = Math.min(HALF_VFOV - 2, sightline.topPitch);
|
|
8530
|
+
const stageBasePitch = Math.max(-HALF_VFOV + 2, sightline.basePitch);
|
|
6394
8531
|
const sx0 = yawToTX(-stageHalfYaw);
|
|
6395
8532
|
const sx1 = yawToTX(stageHalfYaw);
|
|
6396
8533
|
const sy0 = pitchToTY(stageTopPitch);
|
|
@@ -6454,9 +8591,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6454
8591
|
|
|
6455
8592
|
// src/i18n/bundles.ts
|
|
6456
8593
|
var LOADERS = {
|
|
6457
|
-
es: () => import("./es-
|
|
6458
|
-
de: () => import("./de-
|
|
6459
|
-
fr: () => import("./fr-
|
|
8594
|
+
es: () => import("./es-6TJS2L7S.js").then((m) => ({ default: m.es })),
|
|
8595
|
+
de: () => import("./de-UHAOVDS3.js").then((m) => ({ default: m.de })),
|
|
8596
|
+
fr: () => import("./fr-MMSY7FPE.js").then((m) => ({ default: m.fr }))
|
|
6460
8597
|
};
|
|
6461
8598
|
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
6462
8599
|
async function loadLocale(code) {
|
|
@@ -6487,7 +8624,9 @@ export {
|
|
|
6487
8624
|
PickerController,
|
|
6488
8625
|
RENDERED_QUALITY_REPORT_VERSION,
|
|
6489
8626
|
SUPPORTED_LOCALES,
|
|
8627
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
6490
8628
|
SeatmapRenderer,
|
|
8629
|
+
TIER_HEIGHT_M,
|
|
6491
8630
|
UNGROUPED_ID,
|
|
6492
8631
|
accessibilityMeta,
|
|
6493
8632
|
accessibilityRingColor,
|
|
@@ -6502,16 +8641,19 @@ export {
|
|
|
6502
8641
|
expandRow,
|
|
6503
8642
|
expandRowSlots,
|
|
6504
8643
|
expandTable,
|
|
8644
|
+
expandTableSlots,
|
|
6505
8645
|
floorObjects,
|
|
6506
8646
|
floorsOf,
|
|
6507
8647
|
formatDate,
|
|
6508
8648
|
formatMoney,
|
|
6509
8649
|
gaAreasOf,
|
|
8650
|
+
gaInventorySegments,
|
|
6510
8651
|
gaUnitLabel,
|
|
6511
8652
|
gaUnitLabels,
|
|
6512
8653
|
generateSeatPanorama,
|
|
6513
8654
|
generateSeatThumb,
|
|
6514
8655
|
getLocale,
|
|
8656
|
+
growJoinedGAInventory,
|
|
6515
8657
|
hiddenObjectIds,
|
|
6516
8658
|
inspectRenderedQualityEvidence,
|
|
6517
8659
|
isGaUnitLabel,
|
|
@@ -6519,17 +8661,23 @@ export {
|
|
|
6519
8661
|
layerOf,
|
|
6520
8662
|
loadLocale,
|
|
6521
8663
|
objectCenter,
|
|
8664
|
+
owningSectionForObject,
|
|
6522
8665
|
pointInPolygon,
|
|
6523
8666
|
pointInPolygonWithHoles,
|
|
6524
8667
|
polygonLabelPoint,
|
|
6525
8668
|
resolveLocale,
|
|
8669
|
+
rowInventoryCount,
|
|
6526
8670
|
rowSeatPositions,
|
|
6527
8671
|
seatLabelPart,
|
|
8672
|
+
sectionGeometry,
|
|
6528
8673
|
setLocale,
|
|
6529
8674
|
setMoneyLocale,
|
|
6530
8675
|
setStringOverrides,
|
|
6531
8676
|
stackFloors,
|
|
6532
8677
|
t,
|
|
6533
|
-
tCount
|
|
8678
|
+
tCount,
|
|
8679
|
+
tableInventoryCount,
|
|
8680
|
+
tableSeatCountsBySide,
|
|
8681
|
+
validGAInventorySegments
|
|
6534
8682
|
};
|
|
6535
8683
|
//# sourceMappingURL=index.js.map
|