@seatlayer/core 0.1.2 → 0.1.3
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-Y3E6YTGP.js +70 -0
- package/dist/de-Y3E6YTGP.js.map +1 -0
- package/dist/es-UM4ZS357.js +70 -0
- package/dist/es-UM4ZS357.js.map +1 -0
- package/dist/fr-IGRD2M4W.js +70 -0
- package/dist/fr-IGRD2M4W.js.map +1 -0
- package/dist/index.cjs +1140 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +432 -8
- package/dist/index.d.ts +432 -8
- package/dist/index.js +883 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -78,7 +78,15 @@ function expandRowSlots(row) {
|
|
|
78
78
|
const start = row.seatLabelStart ?? 1;
|
|
79
79
|
const dir = row.seatNumbering?.direction ?? "ltr";
|
|
80
80
|
const step = row.seatNumbering?.step ?? 1;
|
|
81
|
-
const
|
|
81
|
+
const n = row.seatCount;
|
|
82
|
+
let seatNumber;
|
|
83
|
+
if (dir === "center") {
|
|
84
|
+
const rank = new Array(n);
|
|
85
|
+
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);
|
|
86
|
+
seatNumber = (i) => start + rank[i] * step;
|
|
87
|
+
} else {
|
|
88
|
+
seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
|
|
89
|
+
}
|
|
82
90
|
const ov = overrideMap(row);
|
|
83
91
|
return rowSeatPositions(row).map((p, i) => {
|
|
84
92
|
const o = ov.get(i);
|
|
@@ -113,30 +121,41 @@ function expandRow(row) {
|
|
|
113
121
|
}
|
|
114
122
|
return seats;
|
|
115
123
|
}
|
|
116
|
-
function expandTable(
|
|
124
|
+
function expandTable(t2) {
|
|
117
125
|
const seats = [];
|
|
118
|
-
const n = Math.max(0, Math.round(
|
|
126
|
+
const n = Math.max(0, Math.round(t2.seatCount));
|
|
119
127
|
if (n === 0) return seats;
|
|
120
128
|
const mk = (i, x, y) => ({
|
|
121
|
-
id: `${
|
|
122
|
-
label: `${
|
|
129
|
+
id: `${t2.id}:${i}`,
|
|
130
|
+
label: `${t2.label}-${i + 1}`,
|
|
123
131
|
x,
|
|
124
132
|
y,
|
|
125
|
-
rowId:
|
|
126
|
-
categoryKey:
|
|
133
|
+
rowId: t2.id,
|
|
134
|
+
categoryKey: t2.categoryKey
|
|
127
135
|
});
|
|
128
|
-
if (
|
|
129
|
-
const R = (
|
|
130
|
-
const base =
|
|
136
|
+
if (t2.shape === "round") {
|
|
137
|
+
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
138
|
+
const base = t2.rotation * DEG;
|
|
139
|
+
const arc = Math.max(0, Math.min(360, t2.seatArc ?? 360));
|
|
140
|
+
if (arc >= 360 || n === 1) {
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const a = base + i / n * 2 * Math.PI;
|
|
143
|
+
seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
|
|
144
|
+
}
|
|
145
|
+
return seats;
|
|
146
|
+
}
|
|
147
|
+
const arcRad = arc * DEG;
|
|
148
|
+
const halfGap = (2 * Math.PI - arcRad) / 2;
|
|
149
|
+
const start = base + halfGap;
|
|
131
150
|
for (let i = 0; i < n; i++) {
|
|
132
|
-
const a =
|
|
133
|
-
seats.push(mk(i,
|
|
151
|
+
const a = start + i / (n - 1) * arcRad;
|
|
152
|
+
seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
|
|
134
153
|
}
|
|
135
154
|
return seats;
|
|
136
155
|
}
|
|
137
|
-
const w =
|
|
138
|
-
const h =
|
|
139
|
-
const enabled =
|
|
156
|
+
const w = t2.width ?? 80;
|
|
157
|
+
const h = t2.height ?? 50;
|
|
158
|
+
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
140
159
|
const order = ["top", "bottom", "left", "right"].filter((s) => enabled.includes(s));
|
|
141
160
|
if (!order.length) return seats;
|
|
142
161
|
const counts = new Map(order.map((s) => [s, 0]));
|
|
@@ -163,7 +182,7 @@ function expandTable(t) {
|
|
|
163
182
|
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
164
183
|
localY = -h / 2 + (j + 0.5) * h / count;
|
|
165
184
|
}
|
|
166
|
-
const p = place(localX, localY,
|
|
185
|
+
const p = place(localX, localY, t2.rotation, t2.center);
|
|
167
186
|
seats.push(mk(idx++, p.x, p.y));
|
|
168
187
|
}
|
|
169
188
|
}
|
|
@@ -234,9 +253,53 @@ function objectCenter(o) {
|
|
|
234
253
|
return { x: o.x ?? 0, y: o.y ?? 0 };
|
|
235
254
|
}
|
|
236
255
|
}
|
|
256
|
+
function floorsOf(doc) {
|
|
257
|
+
if (doc.floors && doc.floors.length) return doc.floors;
|
|
258
|
+
return [{ id: "floor-0", name: "Main", objects: doc.objects, focalPoint: doc.focalPoint, backgroundImage: doc.backgroundImage }];
|
|
259
|
+
}
|
|
260
|
+
function floorObjects(doc, floorId) {
|
|
261
|
+
const floors = floorsOf(doc);
|
|
262
|
+
return (floorId ? floors.find((f) => f.id === floorId) : floors[0])?.objects ?? [];
|
|
263
|
+
}
|
|
264
|
+
function allObjects(doc) {
|
|
265
|
+
return doc.floors && doc.floors.length ? doc.floors.flatMap((f) => f.objects) : doc.objects;
|
|
266
|
+
}
|
|
267
|
+
function translateObject(o, dx, dy) {
|
|
268
|
+
const p = (pt) => ({ x: pt.x + dx, y: pt.y + dy });
|
|
269
|
+
const pts = (a) => a.map(p);
|
|
270
|
+
switch (o.type) {
|
|
271
|
+
case "row":
|
|
272
|
+
return { ...o, origin: p(o.origin) };
|
|
273
|
+
case "table":
|
|
274
|
+
case "booth":
|
|
275
|
+
return { ...o, center: p(o.center) };
|
|
276
|
+
case "gaArea":
|
|
277
|
+
return { ...o, points: pts(o.points) };
|
|
278
|
+
case "section":
|
|
279
|
+
return { ...o, outline: pts(o.outline) };
|
|
280
|
+
case "text":
|
|
281
|
+
return { ...o, position: p(o.position) };
|
|
282
|
+
case "shape":
|
|
283
|
+
return {
|
|
284
|
+
...o,
|
|
285
|
+
...o.points ? { points: pts(o.points) } : {},
|
|
286
|
+
...o.x != null ? { x: o.x + dx } : {},
|
|
287
|
+
...o.y != null ? { y: o.y + dy } : {}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function stackFloors(doc, spread = 900) {
|
|
292
|
+
if (!doc.floors || doc.floors.length < 2) return doc;
|
|
293
|
+
const objects = [];
|
|
294
|
+
doc.floors.forEach((f, i) => {
|
|
295
|
+
const dy = -i * spread;
|
|
296
|
+
for (const o of f.objects) objects.push(dy === 0 ? o : translateObject(o, 0, dy));
|
|
297
|
+
});
|
|
298
|
+
return { ...doc, objects, floors: void 0 };
|
|
299
|
+
}
|
|
237
300
|
function expandChart(doc) {
|
|
238
301
|
const out = [];
|
|
239
|
-
for (const obj of doc
|
|
302
|
+
for (const obj of allObjects(doc)) {
|
|
240
303
|
if (obj.type === "row") out.push(...expandRow(obj));
|
|
241
304
|
else if (obj.type === "table") out.push(...expandTable(obj));
|
|
242
305
|
else if (obj.type === "booth") out.push(...expandBooth(obj));
|
|
@@ -256,7 +319,7 @@ function chartBounds(doc) {
|
|
|
256
319
|
if (y > maxY) maxY = y;
|
|
257
320
|
};
|
|
258
321
|
for (const s of expandChart(doc)) acc(s.x, s.y);
|
|
259
|
-
for (const obj of doc
|
|
322
|
+
for (const obj of allObjects(doc)) {
|
|
260
323
|
if (obj.type === "gaArea") {
|
|
261
324
|
for (const p of obj.points) acc(p.x, p.y);
|
|
262
325
|
} else if (obj.type === "section") {
|
|
@@ -301,6 +364,87 @@ function chartBounds(doc) {
|
|
|
301
364
|
};
|
|
302
365
|
}
|
|
303
366
|
|
|
367
|
+
// src/core/sections.ts
|
|
368
|
+
var UNGROUPED_ID = "__ungrouped__";
|
|
369
|
+
function objectSeatLabels(o) {
|
|
370
|
+
if (o.type === "row") return expandRow(o).map((s) => s.label);
|
|
371
|
+
if (o.type === "table") return expandTable(o).map((s) => s.label);
|
|
372
|
+
if (o.type === "booth") return expandBooth(o).map((s) => s.label);
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
function isSeatObject(o) {
|
|
376
|
+
return o.type === "row" || o.type === "table" || o.type === "booth";
|
|
377
|
+
}
|
|
378
|
+
function computeSections(doc) {
|
|
379
|
+
const objs = allObjects(doc);
|
|
380
|
+
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
381
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
382
|
+
for (const s of sectionObjs) {
|
|
383
|
+
nodes.set(s.id, { id: s.id, label: s.label || "Section", zone: s.zone, seatCount: 0, objectIds: [], seatLabels: [] });
|
|
384
|
+
}
|
|
385
|
+
const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
|
|
386
|
+
const objectToSection = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const obj of objs) {
|
|
388
|
+
if (!isSeatObject(obj)) continue;
|
|
389
|
+
const labels = objectSeatLabels(obj);
|
|
390
|
+
if (labels.length === 0) continue;
|
|
391
|
+
const c = objectCenter(obj);
|
|
392
|
+
const owner = sectionObjs.find((s) => pointInPolygon(c, s.outline));
|
|
393
|
+
const node = owner ? nodes.get(owner.id) : ungrouped;
|
|
394
|
+
node.seatCount += labels.length;
|
|
395
|
+
node.objectIds.push(obj.id);
|
|
396
|
+
node.seatLabels.push(...labels);
|
|
397
|
+
objectToSection.set(obj.id, node.id);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
sections: sectionObjs.map((s) => nodes.get(s.id)),
|
|
401
|
+
ungrouped: ungrouped.objectIds.length ? ungrouped : null,
|
|
402
|
+
objectToSection
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function isSectionHidden(s, hidden) {
|
|
406
|
+
return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
|
|
407
|
+
}
|
|
408
|
+
function hiddenObjectIds(doc, hidden) {
|
|
409
|
+
const out = /* @__PURE__ */ new Set();
|
|
410
|
+
if (!hidden.size) return out;
|
|
411
|
+
const { sections, ungrouped, objectToSection } = computeSections(doc);
|
|
412
|
+
const hiddenSectionIds = /* @__PURE__ */ new Set();
|
|
413
|
+
const zoneById = new Map(sections.map((s) => [s.id, s.zone]));
|
|
414
|
+
for (const s of sections) {
|
|
415
|
+
const zone = zoneById.get(s.id);
|
|
416
|
+
if (hidden.has(s.id) || zone && hidden.has(zone)) hiddenSectionIds.add(s.id);
|
|
417
|
+
}
|
|
418
|
+
const ungroupedHidden = !!ungrouped && hidden.has(UNGROUPED_ID);
|
|
419
|
+
for (const [objId, secId] of objectToSection) {
|
|
420
|
+
if (hiddenSectionIds.has(secId) || ungroupedHidden && secId === UNGROUPED_ID) out.add(objId);
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
function applyHidden(doc, hidden) {
|
|
425
|
+
if (!hidden.size) return doc;
|
|
426
|
+
const hide = hiddenObjectIds(doc, hidden);
|
|
427
|
+
const hiddenSet = hidden instanceof Set ? hidden : new Set(hidden);
|
|
428
|
+
const keep = (o) => {
|
|
429
|
+
if (hide.has(o.id)) return false;
|
|
430
|
+
if (o.type === "section" && isSectionHidden(o, hiddenSet)) return false;
|
|
431
|
+
return true;
|
|
432
|
+
};
|
|
433
|
+
if (doc.floors && doc.floors.length) {
|
|
434
|
+
let changed = false;
|
|
435
|
+
const floors = doc.floors.map((f) => {
|
|
436
|
+
const objects2 = f.objects.filter(keep);
|
|
437
|
+
if (objects2.length !== f.objects.length) changed = true;
|
|
438
|
+
return objects2.length === f.objects.length ? f : { ...f, objects: objects2 };
|
|
439
|
+
});
|
|
440
|
+
if (!changed) return doc;
|
|
441
|
+
return { ...doc, floors, objects: floors[0].objects };
|
|
442
|
+
}
|
|
443
|
+
const objects = doc.objects.filter(keep);
|
|
444
|
+
if (objects.length === doc.objects.length) return doc;
|
|
445
|
+
return { ...doc, objects };
|
|
446
|
+
}
|
|
447
|
+
|
|
304
448
|
// src/engine/SeatmapRenderer.ts
|
|
305
449
|
import { Konva } from "konva/lib/Core";
|
|
306
450
|
import { Stage } from "konva/lib/Stage";
|
|
@@ -312,21 +456,162 @@ import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
|
312
456
|
import { Line } from "konva/lib/shapes/Line";
|
|
313
457
|
import { Text } from "konva/lib/shapes/Text";
|
|
314
458
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
459
|
+
|
|
460
|
+
// src/lib/money.ts
|
|
461
|
+
var DEFAULT_CURRENCY = "USD";
|
|
462
|
+
var displayLocale;
|
|
463
|
+
function setMoneyLocale(locale) {
|
|
464
|
+
displayLocale = locale;
|
|
465
|
+
}
|
|
466
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
467
|
+
function formatter(currency, digits) {
|
|
468
|
+
const key = `${displayLocale ?? ""}|${currency}|${digits ?? "auto"}`;
|
|
469
|
+
let fmt = formatterCache.get(key);
|
|
470
|
+
if (!fmt) {
|
|
471
|
+
fmt = new Intl.NumberFormat(displayLocale, {
|
|
472
|
+
style: "currency",
|
|
473
|
+
currency,
|
|
474
|
+
minimumFractionDigits: digits,
|
|
475
|
+
maximumFractionDigits: digits
|
|
476
|
+
});
|
|
477
|
+
formatterCache.set(key, fmt);
|
|
478
|
+
}
|
|
479
|
+
return fmt;
|
|
480
|
+
}
|
|
481
|
+
function formatMoney(amount, currency = DEFAULT_CURRENCY, fractionDigits) {
|
|
482
|
+
const digits = fractionDigits ?? (Number.isInteger(amount) ? 0 : void 0);
|
|
483
|
+
return formatter(currency, digits).format(amount);
|
|
484
|
+
}
|
|
485
|
+
function currencySymbol(currency = DEFAULT_CURRENCY) {
|
|
486
|
+
const part = formatter(currency, 0).formatToParts(0).find((p) => p.type === "currency");
|
|
487
|
+
return part?.value ?? currency;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/i18n/locales/en.ts
|
|
491
|
+
var en = {
|
|
492
|
+
// shared
|
|
493
|
+
"common.cancel": "Cancel",
|
|
494
|
+
"common.close": "Close",
|
|
495
|
+
"common.done": "Done",
|
|
496
|
+
"common.copied": "\u2713 Copied",
|
|
497
|
+
// buyer picker (extraction in progress — P4 of the port plan)
|
|
498
|
+
"picker.holdSeats": "Hold seats & checkout",
|
|
499
|
+
"picker.completeBooking": "Complete booking",
|
|
500
|
+
"picker.seatsHeld": "Seats held \u2014 {time}",
|
|
501
|
+
"picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
|
|
502
|
+
"picker.seatTaken": "Seat {label} was just taken by another buyer.",
|
|
503
|
+
"picker.poweredBy": "Powered by SeatMap",
|
|
504
|
+
"picker.seats.one": "{count} seat",
|
|
505
|
+
"picker.seats.other": "{count} seats",
|
|
506
|
+
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
507
|
+
"map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
|
|
508
|
+
"map.seatsLeft": "{count} LEFT",
|
|
509
|
+
"map.fromPrice": "FROM {price}",
|
|
510
|
+
// buyer picker page (src/pages/PickerPage.tsx)
|
|
511
|
+
"picker.language": "Language",
|
|
512
|
+
"picker.zoomToFit": "Zoom to fit",
|
|
513
|
+
"picker.viewMode": "View mode",
|
|
514
|
+
"picker.floor": "Floor",
|
|
515
|
+
"picker.zoomLevel": "Zoom level",
|
|
516
|
+
"picker.rungTip.zones": "Farthest rung \u2014 zone blocks (Floor / Lower Bowl / Upper Bowl)",
|
|
517
|
+
"picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
|
|
518
|
+
"picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
|
|
519
|
+
"picker.rungLabel.zones": "ZONES",
|
|
520
|
+
"picker.rungLabel.sections": "SECTIONS",
|
|
521
|
+
"picker.rungLabel.seats": "SEATS",
|
|
522
|
+
"picker.sectionSummaryAria": "{label} section summary",
|
|
523
|
+
"picker.closeSectionSummary": "Close section summary",
|
|
524
|
+
"picker.seatsLeftInSection.one": "{count} seat left",
|
|
525
|
+
"picker.seatsLeftInSection.other": "{count} seats left",
|
|
526
|
+
"picker.overview": "Overview",
|
|
527
|
+
"picker.tapSeatHint": "Tap any seat to check its view",
|
|
528
|
+
"picker.chartSize": "Chart size",
|
|
529
|
+
"picker.custom": "Custom",
|
|
530
|
+
"picker.categories": "Categories",
|
|
531
|
+
"picker.accessibility": "Accessibility",
|
|
532
|
+
"picker.showAnyAccessible": "Show any accessible seat",
|
|
533
|
+
"picker.any": "Any",
|
|
534
|
+
"picker.yourSeats": "Your seats",
|
|
535
|
+
"picker.emptySeats": "Tap seats on the map",
|
|
536
|
+
"picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
|
|
537
|
+
"picker.oneFewer": "One fewer",
|
|
538
|
+
"picker.oneMore": "One more",
|
|
539
|
+
"picker.remove": "Remove {label}",
|
|
540
|
+
"picker.ticketTierFor": "Ticket tier for {label}",
|
|
541
|
+
"picker.total": "Total: {amount}",
|
|
542
|
+
"picker.viewFromSeat": "View from seat {label}",
|
|
543
|
+
"picker.real360": "REAL 360\xB0",
|
|
544
|
+
"picker.preview": "PREVIEW",
|
|
545
|
+
"picker.open360": "Open 360\xB0 view",
|
|
546
|
+
"picker.sightline": "\u2248 {rows} rows from stage \xB7 {angle}\xB0 off-center",
|
|
547
|
+
"picker.bookedDemo": "Booked! (demo)",
|
|
548
|
+
"picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
|
|
549
|
+
"picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
|
|
550
|
+
"picker.simulateCrowd": "Simulate crowd: {state}",
|
|
551
|
+
"picker.on": "ON",
|
|
552
|
+
"picker.off": "OFF",
|
|
553
|
+
"picker.panorama360": "360\xB0 venue photo",
|
|
554
|
+
"picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage"
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/i18n/index.ts
|
|
558
|
+
var SUPPORTED_LOCALES = ["en", "es", "de", "fr"];
|
|
559
|
+
var bundles = { en };
|
|
560
|
+
var overrides = {};
|
|
561
|
+
var active = "en";
|
|
562
|
+
function resolveLocale(explicit, stored) {
|
|
563
|
+
for (const candidate of [explicit, stored, typeof navigator !== "undefined" ? navigator.language : null]) {
|
|
564
|
+
if (!candidate) continue;
|
|
565
|
+
const base = candidate.toLowerCase().split("-")[0];
|
|
566
|
+
if (SUPPORTED_LOCALES.includes(base)) return base;
|
|
567
|
+
}
|
|
568
|
+
return "en";
|
|
569
|
+
}
|
|
570
|
+
function getLocale() {
|
|
571
|
+
return active;
|
|
572
|
+
}
|
|
573
|
+
function setLocale(locale, bundle) {
|
|
574
|
+
if (bundle) bundles[locale] = { ...bundles[locale], ...bundle };
|
|
575
|
+
active = bundles[locale] ? locale : "en";
|
|
576
|
+
setMoneyLocale(active);
|
|
577
|
+
if (typeof document !== "undefined") document.documentElement.lang = active;
|
|
578
|
+
}
|
|
579
|
+
function setStringOverrides(next) {
|
|
580
|
+
overrides = next;
|
|
581
|
+
}
|
|
582
|
+
var PLACEHOLDER = /\{(\w+)\}/g;
|
|
583
|
+
function t(key, vars) {
|
|
584
|
+
const raw = overrides[key] ?? bundles[active]?.[key] ?? en[key] ?? key;
|
|
585
|
+
if (!vars) return raw;
|
|
586
|
+
return raw.replace(PLACEHOLDER, (_, name) => String(vars[name] ?? `{${name}}`));
|
|
587
|
+
}
|
|
588
|
+
function tCount(key, count, vars) {
|
|
589
|
+
const rule = new Intl.PluralRules(active).select(count);
|
|
590
|
+
const exact = overrides[`${key}.${rule}`] ?? bundles[active]?.[`${key}.${rule}`] ?? en[`${key}.${rule}`];
|
|
591
|
+
const raw = exact ?? overrides[`${key}.other`] ?? bundles[active]?.[`${key}.other`] ?? en[`${key}.other`] ?? key;
|
|
592
|
+
return raw.replace(PLACEHOLDER, (_, name) => String({ count, ...vars }[name] ?? `{${name}}`));
|
|
593
|
+
}
|
|
594
|
+
function formatDate(value, opts) {
|
|
595
|
+
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/engine/SeatmapRenderer.ts
|
|
315
599
|
var SEAT_RADIUS = 9;
|
|
316
600
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
317
601
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
318
602
|
var LABEL_SCALE = 1;
|
|
319
|
-
var SECTION_PROMINENT_SCALE = 0.
|
|
603
|
+
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
604
|
+
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
320
605
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
321
606
|
var MAX_LABELS = 700;
|
|
322
607
|
var ISO_ANGLE_DEG = -11.5;
|
|
323
608
|
var ISO_SQUASH = 0.58;
|
|
324
|
-
var LIFT_PER_STEP =
|
|
609
|
+
var LIFT_PER_STEP = 58;
|
|
325
610
|
var ISO_TWEEN_MS = 320;
|
|
326
611
|
var BLOCK_FILL_ALPHA = 0.85;
|
|
327
612
|
var SOLD_DARKEN = 0.5;
|
|
328
|
-
var SECTION_LABEL_PX =
|
|
329
|
-
var SECTION_SUB_PX =
|
|
613
|
+
var SECTION_LABEL_PX = 20;
|
|
614
|
+
var SECTION_SUB_PX = 12.5;
|
|
330
615
|
var ZONE_LABEL_PX = 30;
|
|
331
616
|
var ZONE_SUB_PX = 12;
|
|
332
617
|
var HELD_FILL = "#6b7280";
|
|
@@ -343,7 +628,7 @@ var ACCESS_RING = {
|
|
|
343
628
|
};
|
|
344
629
|
function seatMatchesAccess(seat, filter) {
|
|
345
630
|
if (filter.length === 0) return !!seat.accessible;
|
|
346
|
-
return !!seat.accessibility?.some((
|
|
631
|
+
return !!seat.accessibility?.some((t2) => filter.includes(t2));
|
|
347
632
|
}
|
|
348
633
|
var DEF_SEAT_LABEL = "#0b1220";
|
|
349
634
|
var DEF_SELECTION = "#ffffff";
|
|
@@ -412,16 +697,21 @@ function mixColors(parts, fallback) {
|
|
|
412
697
|
}
|
|
413
698
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
414
699
|
}
|
|
415
|
-
function lerpColor(a, b,
|
|
700
|
+
function lerpColor(a, b, t2) {
|
|
416
701
|
const ca = hexToRgb(a);
|
|
417
702
|
const cb = hexToRgb(b);
|
|
418
703
|
if (!ca || !cb) return a;
|
|
419
|
-
return toHex(ca[0] + (cb[0] - ca[0]) *
|
|
704
|
+
return toHex(ca[0] + (cb[0] - ca[0]) * t2, ca[1] + (cb[1] - ca[1]) * t2, ca[2] + (cb[2] - ca[2]) * t2);
|
|
420
705
|
}
|
|
421
706
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
422
707
|
constructor(container, options = {}) {
|
|
423
708
|
this.seats = [];
|
|
424
709
|
this.seatById = /* @__PURE__ */ new Map();
|
|
710
|
+
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
711
|
+
this.chartDoc = null;
|
|
712
|
+
this.activeFloorId = "";
|
|
713
|
+
/** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
|
|
714
|
+
this.stacked = false;
|
|
425
715
|
/** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
|
|
426
716
|
this.circleById = /* @__PURE__ */ new Map();
|
|
427
717
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
@@ -445,6 +735,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
445
735
|
this.zones = [];
|
|
446
736
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
447
737
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
738
|
+
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
739
|
+
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
740
|
+
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
741
|
+
this.objectFloor = /* @__PURE__ */ new Map();
|
|
448
742
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
449
743
|
this.zoneColor = /* @__PURE__ */ new Map();
|
|
450
744
|
this.hasSections = false;
|
|
@@ -457,6 +751,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
457
751
|
this.isoRaf = 0;
|
|
458
752
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
459
753
|
this.isoCentre = { x: 0, y: 0 };
|
|
754
|
+
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
755
|
+
this.glideRaf = 0;
|
|
460
756
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
461
757
|
this.destroyed = false;
|
|
462
758
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -504,6 +800,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
504
800
|
// pointer events away from Konva's canvas, killing its click/tap/hover
|
|
505
801
|
// pipeline. We rely on bubbling instead.
|
|
506
802
|
this.onPointerDown = (e) => {
|
|
803
|
+
this.cancelGlide();
|
|
507
804
|
this.pointers.set(e.pointerId, this.toLocal(e));
|
|
508
805
|
if (this.pointers.size === 1) {
|
|
509
806
|
this.moved = 0;
|
|
@@ -565,6 +862,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
565
862
|
};
|
|
566
863
|
this.container = container;
|
|
567
864
|
this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
|
|
865
|
+
this.currency = options.currency;
|
|
568
866
|
Konva.pixelRatio = this.dpr;
|
|
569
867
|
this.stage = new Stage({
|
|
570
868
|
container,
|
|
@@ -581,7 +879,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
581
879
|
if (container.tabIndex < 0) container.tabIndex = 0;
|
|
582
880
|
container.setAttribute("role", "application");
|
|
583
881
|
if (!container.getAttribute("aria-label")) {
|
|
584
|
-
container.setAttribute("aria-label", "
|
|
882
|
+
container.setAttribute("aria-label", t("map.aria"));
|
|
585
883
|
}
|
|
586
884
|
container.addEventListener("keydown", this.onKeyDown);
|
|
587
885
|
this.bgLayer = new Layer({ listening: true });
|
|
@@ -624,7 +922,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
624
922
|
}
|
|
625
923
|
}
|
|
626
924
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
627
|
-
setChart(doc) {
|
|
925
|
+
setChart(doc, opts) {
|
|
926
|
+
if (doc !== this.chartDoc) this.stacked = false;
|
|
927
|
+
this.chartDoc = doc;
|
|
928
|
+
this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
|
|
929
|
+
this.objectFloor.clear();
|
|
930
|
+
if (doc.floors && doc.floors.length > 1) {
|
|
931
|
+
for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
|
|
932
|
+
}
|
|
933
|
+
const view = this.floorView(doc);
|
|
628
934
|
this.focusedId = null;
|
|
629
935
|
this.focusRing.visible(false);
|
|
630
936
|
this.bgLayer.destroyChildren();
|
|
@@ -652,7 +958,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
652
958
|
this.isoT = 0;
|
|
653
959
|
this.isoTarget = 0;
|
|
654
960
|
this.resetLayerTransforms();
|
|
655
|
-
this.hasSections =
|
|
961
|
+
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
656
962
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
657
963
|
this.seatLayer.opacity(1);
|
|
658
964
|
this.hoverRing.visible(false);
|
|
@@ -667,24 +973,56 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
667
973
|
this.catColor.set(c.key, c.color);
|
|
668
974
|
if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
|
|
669
975
|
}
|
|
670
|
-
for (const obj of
|
|
976
|
+
for (const obj of view.objects) {
|
|
671
977
|
if (obj.type === "booth") {
|
|
672
978
|
this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
|
|
673
979
|
}
|
|
674
980
|
}
|
|
675
|
-
this.seats = expandChart(
|
|
981
|
+
this.seats = expandChart(view);
|
|
676
982
|
for (const s of this.seats) {
|
|
677
983
|
this.seatById.set(s.id, s);
|
|
678
984
|
this.statusById.set(s.id, "free");
|
|
679
985
|
}
|
|
680
|
-
this.renderBackground(
|
|
986
|
+
this.renderBackground(view);
|
|
681
987
|
this.renderSeats();
|
|
682
988
|
this.overlayLayer.add(this.labelGroup);
|
|
683
989
|
this.overlayLayer.add(this.hoverRing);
|
|
684
|
-
this.bounds = chartBounds(
|
|
990
|
+
this.bounds = chartBounds(view);
|
|
685
991
|
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
686
992
|
this.zoomToFit();
|
|
687
993
|
}
|
|
994
|
+
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
995
|
+
* the whole chart for single-floor charts. */
|
|
996
|
+
floorView(doc) {
|
|
997
|
+
if (!doc.floors || !doc.floors.length) return doc;
|
|
998
|
+
if (this.stacked && doc.floors.length >= 2) return stackFloors(doc);
|
|
999
|
+
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
1000
|
+
return { ...doc, objects: floor.objects, focalPoint: floor.focalPoint, backgroundImage: floor.backgroundImage, floors: void 0 };
|
|
1001
|
+
}
|
|
1002
|
+
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
1003
|
+
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
1004
|
+
setStacked(on) {
|
|
1005
|
+
if (!this.chartDoc || on === this.stacked) return;
|
|
1006
|
+
const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
|
|
1007
|
+
if (!multi) return;
|
|
1008
|
+
this.stacked = on;
|
|
1009
|
+
this.setChart(this.chartDoc, { floorId: this.activeFloorId });
|
|
1010
|
+
}
|
|
1011
|
+
isStacked() {
|
|
1012
|
+
return this.stacked;
|
|
1013
|
+
}
|
|
1014
|
+
/** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
|
|
1015
|
+
setActiveFloor(floorId) {
|
|
1016
|
+
if (!this.chartDoc || floorId === this.activeFloorId) return;
|
|
1017
|
+
this.setChart(this.chartDoc, { floorId });
|
|
1018
|
+
}
|
|
1019
|
+
/** Floors for the host's switcher (single-floor charts return one synthetic floor). */
|
|
1020
|
+
getFloors() {
|
|
1021
|
+
return this.chartDoc ? floorsOf(this.chartDoc).map((f) => ({ id: f.id, name: f.name })) : [];
|
|
1022
|
+
}
|
|
1023
|
+
getActiveFloorId() {
|
|
1024
|
+
return this.activeFloorId;
|
|
1025
|
+
}
|
|
688
1026
|
setStatus(seatIds, status) {
|
|
689
1027
|
let touched = false;
|
|
690
1028
|
const affected = this.hasSections ? /* @__PURE__ */ new Set() : null;
|
|
@@ -764,11 +1102,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
764
1102
|
const dur = 620;
|
|
765
1103
|
const step = (now) => {
|
|
766
1104
|
if (!ring.getLayer()) return;
|
|
767
|
-
const
|
|
768
|
-
ring.radius(this.seatR * (1 +
|
|
769
|
-
ring.opacity(0.9 * (1 -
|
|
1105
|
+
const t2 = Math.min(1, (now - start) / dur);
|
|
1106
|
+
ring.radius(this.seatR * (1 + t2 * 1.8));
|
|
1107
|
+
ring.opacity(0.9 * (1 - t2));
|
|
770
1108
|
this.overlayLayer.batchDraw();
|
|
771
|
-
if (
|
|
1109
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
772
1110
|
else {
|
|
773
1111
|
ring.destroy();
|
|
774
1112
|
this.overlayLayer.batchDraw();
|
|
@@ -875,7 +1213,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
875
1213
|
sameAccessFilter(next) {
|
|
876
1214
|
const cur = this.accessFilter;
|
|
877
1215
|
if (cur === null || next === null) return cur === next;
|
|
878
|
-
return cur.length === next.length && cur.every((
|
|
1216
|
+
return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
|
|
879
1217
|
}
|
|
880
1218
|
// ---- isometric ("3D") view mode -------------------------------------------
|
|
881
1219
|
/**
|
|
@@ -923,6 +1261,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
923
1261
|
};
|
|
924
1262
|
this.isoRaf = requestAnimationFrame(step);
|
|
925
1263
|
}
|
|
1264
|
+
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
1265
|
+
getViewMode() {
|
|
1266
|
+
return this.isoTarget === 1 ? "isometric" : "flat";
|
|
1267
|
+
}
|
|
926
1268
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
927
1269
|
isoParams() {
|
|
928
1270
|
return { th: ISO_ANGLE_DEG * Math.PI / 180 * this.isoT, sg: 1 - (1 - ISO_SQUASH) * this.isoT };
|
|
@@ -980,8 +1322,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
980
1322
|
* lift + extruded side faces on elevated sections.
|
|
981
1323
|
*/
|
|
982
1324
|
applyIso() {
|
|
983
|
-
const
|
|
984
|
-
if (
|
|
1325
|
+
const t2 = this.isoT;
|
|
1326
|
+
if (t2 === 0) {
|
|
985
1327
|
this.resetLayerTransforms();
|
|
986
1328
|
} else {
|
|
987
1329
|
const { th, sg } = this.isoParams();
|
|
@@ -1034,13 +1376,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1034
1376
|
}
|
|
1035
1377
|
/** Lift elevated sections (+ their members) and update their extruded side faces. */
|
|
1036
1378
|
applyElevation() {
|
|
1037
|
-
const
|
|
1379
|
+
const t2 = this.isoT;
|
|
1038
1380
|
for (const sec of this.sections) {
|
|
1039
1381
|
if (sec.elevation <= 0) continue;
|
|
1040
1382
|
const off = this.isoLiftLocal(sec.elevation);
|
|
1041
1383
|
sec.liftGroupBg?.position(off);
|
|
1042
1384
|
sec.liftGroupSeat?.position(off);
|
|
1043
|
-
const alpha = 0.9 *
|
|
1385
|
+
const alpha = 0.9 * t2;
|
|
1044
1386
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
1045
1387
|
const face = sec.sideFaces[i];
|
|
1046
1388
|
const p0 = sec.outline[i];
|
|
@@ -1054,6 +1396,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1054
1396
|
this.destroyed = true;
|
|
1055
1397
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
1056
1398
|
if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
|
|
1399
|
+
if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
|
|
1057
1400
|
if (this.viewChangeRaf) cancelAnimationFrame(this.viewChangeRaf);
|
|
1058
1401
|
if (this.recacheTimer) clearTimeout(this.recacheTimer);
|
|
1059
1402
|
this.resizeObs?.disconnect();
|
|
@@ -1133,7 +1476,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1133
1476
|
this.circleById.set(seat.id, rect);
|
|
1134
1477
|
this.paintSeat(rect, seat.id);
|
|
1135
1478
|
target.add(rect);
|
|
1136
|
-
const
|
|
1479
|
+
const t2 = new Text({
|
|
1137
1480
|
x: seat.x,
|
|
1138
1481
|
y: seat.y,
|
|
1139
1482
|
text: seat.label,
|
|
@@ -1144,10 +1487,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1144
1487
|
listening: false,
|
|
1145
1488
|
perfectDrawEnabled: false
|
|
1146
1489
|
});
|
|
1147
|
-
|
|
1148
|
-
|
|
1490
|
+
t2.offsetX(t2.width() / 2);
|
|
1491
|
+
t2.offsetY(t2.height() / 2);
|
|
1149
1492
|
this.hasBoothText = true;
|
|
1150
|
-
target.add(
|
|
1493
|
+
target.add(t2);
|
|
1151
1494
|
}
|
|
1152
1495
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
1153
1496
|
paintSeat(c, id) {
|
|
@@ -1183,6 +1526,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1183
1526
|
if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
|
|
1184
1527
|
c.opacity(0.25);
|
|
1185
1528
|
}
|
|
1529
|
+
if (this.dimmedSections.size) {
|
|
1530
|
+
const sec = this.seatSection.get(id);
|
|
1531
|
+
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1532
|
+
c.opacity(0.18);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Dim the seats of these section/zone ids (organizer manager use) so held-back
|
|
1538
|
+
* inventory is visually distinct on the canvas without hiding it. `null`/empty
|
|
1539
|
+
* clears. Unlike the buyer's applyHidden, the sections stay rendered.
|
|
1540
|
+
*/
|
|
1541
|
+
setDimmedSections(ids) {
|
|
1542
|
+
const next = new Set(ids ?? []);
|
|
1543
|
+
if (next.size === this.dimmedSections.size && [...next].every((i) => this.dimmedSections.has(i))) return;
|
|
1544
|
+
this.dimmedSections = next;
|
|
1545
|
+
for (const seat of this.seats) {
|
|
1546
|
+
const c = this.circleById.get(seat.id);
|
|
1547
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1548
|
+
}
|
|
1549
|
+
if (this.cached) {
|
|
1550
|
+
this.seatLayer.clearCache();
|
|
1551
|
+
this.cacheSeatLayer();
|
|
1552
|
+
} else {
|
|
1553
|
+
this.seatLayer.batchDraw();
|
|
1554
|
+
}
|
|
1186
1555
|
}
|
|
1187
1556
|
/** Legend hover: highlight one category (dim the rest), or null to clear. */
|
|
1188
1557
|
setCategoryHighlight(key) {
|
|
@@ -1365,7 +1734,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1365
1734
|
}
|
|
1366
1735
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
1367
1736
|
addStageLabel(x, y, text) {
|
|
1368
|
-
const
|
|
1737
|
+
const t2 = new Text({
|
|
1369
1738
|
x,
|
|
1370
1739
|
y,
|
|
1371
1740
|
text: text.toUpperCase(),
|
|
@@ -1377,9 +1746,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1377
1746
|
listening: false,
|
|
1378
1747
|
perfectDrawEnabled: false
|
|
1379
1748
|
});
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
this.bgLayer.add(
|
|
1749
|
+
t2.offsetX(t2.width() / 2);
|
|
1750
|
+
t2.offsetY(t2.height() / 2);
|
|
1751
|
+
this.bgLayer.add(t2);
|
|
1383
1752
|
}
|
|
1384
1753
|
renderGA(obj) {
|
|
1385
1754
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
@@ -1460,12 +1829,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1460
1829
|
this.seatLayer.add(liftGroupSeat);
|
|
1461
1830
|
}
|
|
1462
1831
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
1832
|
+
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
1463
1833
|
const outlinePoly = new Line({
|
|
1464
1834
|
points: pts,
|
|
1465
1835
|
closed: true,
|
|
1466
|
-
stroke:
|
|
1467
|
-
strokeWidth: 1.
|
|
1468
|
-
fill: rgba(
|
|
1836
|
+
stroke: rgba(outlineTint, 0.5),
|
|
1837
|
+
strokeWidth: 1.75,
|
|
1838
|
+
fill: rgba(outlineTint, 0.08),
|
|
1839
|
+
lineJoin: "round",
|
|
1469
1840
|
listening: false,
|
|
1470
1841
|
perfectDrawEnabled: false
|
|
1471
1842
|
});
|
|
@@ -1481,6 +1852,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1481
1852
|
perfectDrawEnabled: false
|
|
1482
1853
|
});
|
|
1483
1854
|
bgTarget.add(blockPoly);
|
|
1855
|
+
const rowSeats = /* @__PURE__ */ new Map();
|
|
1856
|
+
for (const id of memberIds) {
|
|
1857
|
+
const s = this.seatById.get(id);
|
|
1858
|
+
if (!s) continue;
|
|
1859
|
+
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
1860
|
+
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
1861
|
+
}
|
|
1862
|
+
const rowLines = [];
|
|
1863
|
+
for (const arr of rowSeats.values()) {
|
|
1864
|
+
if (arr.length < 2) continue;
|
|
1865
|
+
arr.sort((a, b) => a.i - b.i);
|
|
1866
|
+
const line = new Line({
|
|
1867
|
+
points: arr.flatMap((p) => [p.x, p.y]),
|
|
1868
|
+
stroke: rgba("#ffffff", 0.34),
|
|
1869
|
+
strokeWidth: SEAT_RADIUS * 0.55,
|
|
1870
|
+
lineCap: "round",
|
|
1871
|
+
lineJoin: "round",
|
|
1872
|
+
opacity: 0,
|
|
1873
|
+
listening: false,
|
|
1874
|
+
perfectDrawEnabled: false
|
|
1875
|
+
});
|
|
1876
|
+
rowLines.push(line);
|
|
1877
|
+
bgTarget.add(line);
|
|
1878
|
+
}
|
|
1484
1879
|
const nameLabel = new Text({
|
|
1485
1880
|
x: centroid.x,
|
|
1486
1881
|
y: centroid.y,
|
|
@@ -1489,6 +1884,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1489
1884
|
fontStyle: "700",
|
|
1490
1885
|
fontFamily: this.labelFont(),
|
|
1491
1886
|
fill: "#8b93a7",
|
|
1887
|
+
// Dark halo so the label reads over the seat dots at any zoom.
|
|
1888
|
+
shadowColor: "#05070c",
|
|
1889
|
+
shadowBlur: 6,
|
|
1890
|
+
shadowOpacity: 0.9,
|
|
1891
|
+
shadowForStrokeEnabled: false,
|
|
1492
1892
|
listening: false,
|
|
1493
1893
|
perfectDrawEnabled: false
|
|
1494
1894
|
});
|
|
@@ -1498,11 +1898,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1498
1898
|
const subLabel = new Text({
|
|
1499
1899
|
x: centroid.x,
|
|
1500
1900
|
y: centroid.y,
|
|
1501
|
-
text:
|
|
1901
|
+
text: t("map.seatsLeft", { count: free }),
|
|
1502
1902
|
fontSize: 12,
|
|
1503
|
-
fontStyle: "
|
|
1903
|
+
fontStyle: "700",
|
|
1504
1904
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
1505
|
-
fill:
|
|
1905
|
+
fill: "#f4f6fb",
|
|
1906
|
+
shadowColor: "#05070c",
|
|
1907
|
+
shadowBlur: 5,
|
|
1908
|
+
shadowOpacity: 0.9,
|
|
1909
|
+
shadowForStrokeEnabled: false,
|
|
1506
1910
|
opacity: 0,
|
|
1507
1911
|
listening: false,
|
|
1508
1912
|
perfectDrawEnabled: false
|
|
@@ -1521,6 +1925,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1521
1925
|
baseFill,
|
|
1522
1926
|
outlinePoly,
|
|
1523
1927
|
blockPoly,
|
|
1928
|
+
rowLines,
|
|
1524
1929
|
nameLabel,
|
|
1525
1930
|
subLabel,
|
|
1526
1931
|
elevation,
|
|
@@ -1536,7 +1941,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1536
1941
|
refreshSectionFill(sec) {
|
|
1537
1942
|
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
1538
1943
|
sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
|
|
1539
|
-
sec.subLabel.text(
|
|
1944
|
+
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
1540
1945
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
1541
1946
|
}
|
|
1542
1947
|
/**
|
|
@@ -1554,8 +1959,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1554
1959
|
for (const z of doc.zones) {
|
|
1555
1960
|
const members = byZone.get(z.id);
|
|
1556
1961
|
if (!members || !members.length) continue;
|
|
1557
|
-
|
|
1558
|
-
const
|
|
1962
|
+
let anchor = members[0];
|
|
1963
|
+
for (const s of members) if (s.centroid.y < anchor.centroid.y) anchor = s;
|
|
1964
|
+
const cx = anchor.centroid.x;
|
|
1965
|
+
const cy = anchor.centroid.y;
|
|
1559
1966
|
let minPrice = Infinity;
|
|
1560
1967
|
for (const sec of members) {
|
|
1561
1968
|
for (const id of sec.memberIds) {
|
|
@@ -1573,6 +1980,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1573
1980
|
letterSpacing: 2,
|
|
1574
1981
|
fontFamily: this.labelFont(),
|
|
1575
1982
|
fill: z.color ?? "#f2f4f8",
|
|
1983
|
+
shadowColor: "#05070c",
|
|
1984
|
+
shadowBlur: 10,
|
|
1985
|
+
shadowOpacity: 0.95,
|
|
1986
|
+
shadowForStrokeEnabled: false,
|
|
1576
1987
|
opacity: 0,
|
|
1577
1988
|
listening: false,
|
|
1578
1989
|
perfectDrawEnabled: false
|
|
@@ -1585,7 +1996,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1585
1996
|
sub = new Text({
|
|
1586
1997
|
x: cx,
|
|
1587
1998
|
y: cy,
|
|
1588
|
-
text:
|
|
1999
|
+
text: t("map.fromPrice", { price: formatMoney(minPrice, this.currency) }),
|
|
1589
2000
|
fontSize: 14,
|
|
1590
2001
|
fontStyle: "600",
|
|
1591
2002
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
@@ -1616,7 +2027,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1616
2027
|
blockT = scale <= SECTION_PROMINENT_SCALE ? 1 : 0;
|
|
1617
2028
|
zoneT = scale <= ZONE_PROMINENT_SCALE ? 1 : 0;
|
|
1618
2029
|
} else {
|
|
1619
|
-
blockT = clamp((
|
|
2030
|
+
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
1620
2031
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
1621
2032
|
}
|
|
1622
2033
|
if (!this.zones.length) zoneT = 0;
|
|
@@ -1626,7 +2037,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1626
2037
|
if (rescale) this.lodScale = scale;
|
|
1627
2038
|
for (const sec of this.sections) {
|
|
1628
2039
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT);
|
|
1629
|
-
sec.
|
|
2040
|
+
for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT));
|
|
2041
|
+
sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
|
|
1630
2042
|
sec.nameLabel.opacity(1 - zoneT);
|
|
1631
2043
|
sec.subLabel.opacity(blockT * (1 - zoneT));
|
|
1632
2044
|
if (rescale) {
|
|
@@ -1634,9 +2046,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1634
2046
|
this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
|
|
1635
2047
|
}
|
|
1636
2048
|
}
|
|
2049
|
+
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
1637
2050
|
for (const zone of this.zones) {
|
|
1638
|
-
zone.label.opacity(
|
|
1639
|
-
if (zone.sub) zone.sub.opacity(
|
|
2051
|
+
zone.label.opacity(zoneOpacity);
|
|
2052
|
+
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
1640
2053
|
if (rescale) {
|
|
1641
2054
|
const cy = zone.label.y();
|
|
1642
2055
|
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
@@ -1646,11 +2059,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1646
2059
|
this.bgLayer.batchDraw();
|
|
1647
2060
|
}
|
|
1648
2061
|
/** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
|
|
1649
|
-
sizeLabel(
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2062
|
+
sizeLabel(t2, fontSize, y) {
|
|
2063
|
+
t2.fontSize(Math.max(1, fontSize));
|
|
2064
|
+
t2.offsetX(t2.width() / 2);
|
|
2065
|
+
t2.offsetY(t2.height() / 2);
|
|
2066
|
+
t2.y(y);
|
|
1654
2067
|
}
|
|
1655
2068
|
/**
|
|
1656
2069
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
@@ -1674,7 +2087,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1674
2087
|
return this.sections.find((s) => s.id === id)?.memberIds.slice() ?? [];
|
|
1675
2088
|
}
|
|
1676
2089
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
1677
|
-
const
|
|
2090
|
+
const t2 = new Text({
|
|
1678
2091
|
x,
|
|
1679
2092
|
y,
|
|
1680
2093
|
text,
|
|
@@ -1685,9 +2098,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1685
2098
|
listening: false,
|
|
1686
2099
|
perfectDrawEnabled: false
|
|
1687
2100
|
});
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
layer.add(
|
|
2101
|
+
t2.offsetX(t2.width() / 2);
|
|
2102
|
+
t2.offsetY(t2.height() / 2);
|
|
2103
|
+
layer.add(t2);
|
|
1691
2104
|
}
|
|
1692
2105
|
// ---- selection ------------------------------------------------------------
|
|
1693
2106
|
isSelectable(id) {
|
|
@@ -1741,7 +2154,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1741
2154
|
this.seatLayer.on("click tap", (e) => {
|
|
1742
2155
|
if (this.moved > 8) return;
|
|
1743
2156
|
const id = seatIdOf(e.target);
|
|
1744
|
-
if (id)
|
|
2157
|
+
if (!id) return;
|
|
2158
|
+
if (this.stacked && this.opts.onDeckTap) {
|
|
2159
|
+
const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
|
|
2160
|
+
if (floorId) {
|
|
2161
|
+
this.opts.onDeckTap(floorId);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
this.toggleSeat(id);
|
|
1745
2166
|
});
|
|
1746
2167
|
this.seatLayer.on("mouseover", (e) => {
|
|
1747
2168
|
const id = seatIdOf(e.target);
|
|
@@ -1771,11 +2192,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1771
2192
|
if (!this.cached || this.moved > 8) return;
|
|
1772
2193
|
const pointer = this.stage.getPointerPosition();
|
|
1773
2194
|
if (!pointer) return;
|
|
2195
|
+
if (this.stacked && this.opts.onDeckTap) {
|
|
2196
|
+
const floorId = this.deckFloorAt();
|
|
2197
|
+
if (floorId) {
|
|
2198
|
+
this.opts.onDeckTap(floorId);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
1774
2202
|
if (this.sections.length) {
|
|
1775
2203
|
const world = this.screenToWorld(pointer);
|
|
1776
2204
|
const hit = this.sections.find((sn) => pointInPolygon(world, sn.outline));
|
|
1777
2205
|
if (hit) {
|
|
1778
|
-
this.
|
|
2206
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.id);
|
|
2207
|
+
else this.focusRegion(hit.id);
|
|
1779
2208
|
return;
|
|
1780
2209
|
}
|
|
1781
2210
|
}
|
|
@@ -1783,6 +2212,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1783
2212
|
this.zoomAbout(target, pointer);
|
|
1784
2213
|
});
|
|
1785
2214
|
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Which deck (floor) a screen tap landed on in the 3D stacked overview. The
|
|
2217
|
+
* seat layer is melt-cached at that zoom so individual seat nodes aren't
|
|
2218
|
+
* hit-testable; instead we invert the iso+stage transform via the layer's
|
|
2219
|
+
* relative pointer and take the nearest seat's floor. Only the floor matters,
|
|
2220
|
+
* so within-floor elevation offsets don't affect the result. Null if none.
|
|
2221
|
+
*/
|
|
2222
|
+
deckFloorAt() {
|
|
2223
|
+
if (!this.objectFloor.size) return null;
|
|
2224
|
+
const p = this.seatLayer.getRelativePointerPosition();
|
|
2225
|
+
if (!p) return null;
|
|
2226
|
+
let best = null;
|
|
2227
|
+
let bestD = Infinity;
|
|
2228
|
+
for (const s of this.seats) {
|
|
2229
|
+
const dx = s.x - p.x;
|
|
2230
|
+
const dy = s.y - p.y;
|
|
2231
|
+
const d = dx * dx + dy * dy;
|
|
2232
|
+
if (d < bestD) {
|
|
2233
|
+
bestD = d;
|
|
2234
|
+
best = s.rowId;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return best ? this.objectFloor.get(best) ?? null : null;
|
|
2238
|
+
}
|
|
1786
2239
|
// ---- Pan & pinch (raw pointer events — mouse, touch and pen alike) --------
|
|
1787
2240
|
toLocal(e) {
|
|
1788
2241
|
const r = this.container.getBoundingClientRect();
|
|
@@ -1793,7 +2246,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1793
2246
|
* reach a readable size on any chart, however large the venue.
|
|
1794
2247
|
*/
|
|
1795
2248
|
zoomBounds() {
|
|
1796
|
-
|
|
2249
|
+
let min = 0.5 * this.fitScale;
|
|
2250
|
+
if (this.zones.length) min = Math.min(min, ZONE_PROMINENT_SCALE * 0.9);
|
|
2251
|
+
return { min, max: Math.max(10 * this.fitScale, 4) };
|
|
1797
2252
|
}
|
|
1798
2253
|
/** Zoom so `clientPoint` (stage-relative px) stays fixed under the cursor. */
|
|
1799
2254
|
zoomAbout(nextScale, clientPoint) {
|
|
@@ -1826,6 +2281,92 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1826
2281
|
this.afterViewChange();
|
|
1827
2282
|
this.stage.batchDraw();
|
|
1828
2283
|
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Smoothly glide the camera to frame a section (by id) or a world-space bounds
|
|
2286
|
+
* rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
|
|
2287
|
+
* melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
|
|
2288
|
+
* `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
|
|
2289
|
+
* newer glide cancels an in-flight one.
|
|
2290
|
+
*/
|
|
2291
|
+
focusRegion(target, opts) {
|
|
2292
|
+
const b = typeof target === "string" ? (() => {
|
|
2293
|
+
const sec = this.sections.find((s) => s.id === target);
|
|
2294
|
+
return sec ? polyBounds(sec.outline) : null;
|
|
2295
|
+
})() : target;
|
|
2296
|
+
if (!b) return;
|
|
2297
|
+
this.cancelGlide();
|
|
2298
|
+
if (opts?.animate === false || this.reducedMotion) {
|
|
2299
|
+
this.zoomToBounds(b);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const w = this.stage.width();
|
|
2303
|
+
const h = this.stage.height();
|
|
2304
|
+
const { min, max } = this.zoomBounds();
|
|
2305
|
+
const margin = 1.12;
|
|
2306
|
+
const toScale = clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
|
|
2307
|
+
const toX = w / 2 - (b.x + b.width / 2) * toScale;
|
|
2308
|
+
const toY = h / 2 - (b.y + b.height / 2) * toScale;
|
|
2309
|
+
const fromScale = this.stage.scaleX();
|
|
2310
|
+
const fromX = this.stage.x();
|
|
2311
|
+
const fromY = this.stage.y();
|
|
2312
|
+
if (Math.abs(toScale - fromScale) < 1e-4 && Math.abs(toX - fromX) < 0.5 && Math.abs(toY - fromY) < 0.5) {
|
|
2313
|
+
this.afterViewChange();
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
const start = performance.now();
|
|
2317
|
+
const DUR = 450;
|
|
2318
|
+
const step = (now) => {
|
|
2319
|
+
if (this.destroyed) {
|
|
2320
|
+
this.glideRaf = 0;
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const raw = Math.min(1, (now - start) / DUR);
|
|
2324
|
+
const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
|
|
2325
|
+
const sc = fromScale + (toScale - fromScale) * e;
|
|
2326
|
+
this.stage.scale({ x: sc, y: sc });
|
|
2327
|
+
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
2328
|
+
this.updateLOD();
|
|
2329
|
+
this.scheduleViewChange();
|
|
2330
|
+
this.stage.batchDraw();
|
|
2331
|
+
if (raw < 1) {
|
|
2332
|
+
this.glideRaf = requestAnimationFrame(step);
|
|
2333
|
+
} else {
|
|
2334
|
+
this.glideRaf = 0;
|
|
2335
|
+
this.afterViewChange();
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
this.glideRaf = requestAnimationFrame(step);
|
|
2339
|
+
}
|
|
2340
|
+
/** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
|
|
2341
|
+
cancelGlide() {
|
|
2342
|
+
if (this.glideRaf) {
|
|
2343
|
+
cancelAnimationFrame(this.glideRaf);
|
|
2344
|
+
this.glideRaf = 0;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
2348
|
+
getRung() {
|
|
2349
|
+
const scale = this.effScale();
|
|
2350
|
+
if (scale >= CACHE_THRESHOLD) return "seats";
|
|
2351
|
+
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
2352
|
+
return "sections";
|
|
2353
|
+
}
|
|
2354
|
+
/** Jump the camera to a rung's zoom band, centred on the chart (glided). */
|
|
2355
|
+
setRung(rung) {
|
|
2356
|
+
if (rung === "zones") {
|
|
2357
|
+
this.cancelGlide();
|
|
2358
|
+
this.zoomToFit();
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_LEGIBLE_SCALE * 1.1, CACHE_THRESHOLD * 1.3);
|
|
2362
|
+
const w = this.stage.width();
|
|
2363
|
+
const h = this.stage.height();
|
|
2364
|
+
const cx = this.bounds.x + this.bounds.width / 2;
|
|
2365
|
+
const cy = this.bounds.y + this.bounds.height / 2;
|
|
2366
|
+
const bw = w / (target * 1.12);
|
|
2367
|
+
const bh = h / (target * 1.12);
|
|
2368
|
+
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
2369
|
+
}
|
|
1829
2370
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
1830
2371
|
afterViewChange() {
|
|
1831
2372
|
this.updateLOD();
|
|
@@ -1876,7 +2417,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1876
2417
|
let count = 0;
|
|
1877
2418
|
for (const seat of this.seats) {
|
|
1878
2419
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
1879
|
-
const
|
|
2420
|
+
const t2 = new Text({
|
|
1880
2421
|
x: seat.x,
|
|
1881
2422
|
y: seat.y,
|
|
1882
2423
|
text: seat.label,
|
|
@@ -1888,14 +2429,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1888
2429
|
perfectDrawEnabled: false
|
|
1889
2430
|
});
|
|
1890
2431
|
const maxW = this.seatR * 2 - 3;
|
|
1891
|
-
if (
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
2432
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(4, 7 * maxW / t2.width()));
|
|
2433
|
+
if (t2.fontSize() < 4.2) {
|
|
2434
|
+
t2.destroy();
|
|
1894
2435
|
continue;
|
|
1895
2436
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
this.labelGroup.add(
|
|
2437
|
+
t2.offsetX(t2.width() / 2);
|
|
2438
|
+
t2.offsetY(t2.height() / 2);
|
|
2439
|
+
this.labelGroup.add(t2);
|
|
1899
2440
|
if (++count >= MAX_LABELS) break;
|
|
1900
2441
|
}
|
|
1901
2442
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -1946,9 +2487,15 @@ var PickerController = class {
|
|
|
1946
2487
|
constructor(options) {
|
|
1947
2488
|
this.renderer = null;
|
|
1948
2489
|
this._doc = null;
|
|
2490
|
+
/** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
|
|
2491
|
+
this.hidden = /* @__PURE__ */ new Set();
|
|
1949
2492
|
/** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
|
|
1950
2493
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
1951
2494
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
2495
|
+
/** seatId → chosen ticket-tier id (absent ⇒ the category's first/default tier). */
|
|
2496
|
+
this.seatTiers = /* @__PURE__ */ new Map();
|
|
2497
|
+
/** id → seat, for the section-summary breakdown (renderer members are ids). */
|
|
2498
|
+
this.seatById = /* @__PURE__ */ new Map();
|
|
1952
2499
|
this.allIds = [];
|
|
1953
2500
|
// realtime socket
|
|
1954
2501
|
this.ws = null;
|
|
@@ -1966,6 +2513,18 @@ var PickerController = class {
|
|
|
1966
2513
|
get doc() {
|
|
1967
2514
|
return this._doc;
|
|
1968
2515
|
}
|
|
2516
|
+
/** The chart with hidden sections' seats removed — what buyers actually see. */
|
|
2517
|
+
visibleDoc() {
|
|
2518
|
+
return this._doc ? applyHidden(this._doc, this.hidden) : { objects: [] };
|
|
2519
|
+
}
|
|
2520
|
+
/** Adopt a new hidden set; rebuild the visible chart only if it differs. */
|
|
2521
|
+
syncHidden(ids) {
|
|
2522
|
+
const next = ids.filter((x) => typeof x === "string");
|
|
2523
|
+
if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
|
|
2524
|
+
this.hidden = new Set(next);
|
|
2525
|
+
this.renderer?.setChart(this.visibleDoc());
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
1969
2528
|
currentHold() {
|
|
1970
2529
|
return this.hold_;
|
|
1971
2530
|
}
|
|
@@ -1993,15 +2552,19 @@ var PickerController = class {
|
|
|
1993
2552
|
this._doc = res.doc;
|
|
1994
2553
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
1995
2554
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
2555
|
+
this.seatById = /* @__PURE__ */ new Map();
|
|
1996
2556
|
this.allIds = [];
|
|
1997
2557
|
for (const s of expandChart(res.doc)) {
|
|
1998
2558
|
this.labelToId.set(s.label, s.id);
|
|
1999
2559
|
this.labelToSeat.set(s.label, s);
|
|
2560
|
+
this.seatById.set(s.id, s);
|
|
2000
2561
|
this.allIds.push(s.id);
|
|
2001
2562
|
}
|
|
2563
|
+
const currency = res.event.currency ?? this.opts.currency;
|
|
2002
2564
|
const renderer = createRenderer(host, {
|
|
2003
2565
|
maxSelection: this.maxSelection,
|
|
2004
2566
|
confirmSelection: this.opts.confirmSelection,
|
|
2567
|
+
currency,
|
|
2005
2568
|
onSelect: (seat) => {
|
|
2006
2569
|
this.opts.onSelect?.(seat);
|
|
2007
2570
|
this.emitSelectionChange();
|
|
@@ -2014,6 +2577,9 @@ var PickerController = class {
|
|
|
2014
2577
|
onFocusSeat: this.opts.onFocusSeat,
|
|
2015
2578
|
onViewChange: this.opts.onViewChange,
|
|
2016
2579
|
onGAClick: this.opts.onGAClick,
|
|
2580
|
+
// Section tap at the far/zone rung → glide in + surface the summary card.
|
|
2581
|
+
onSectionTap: (id) => this.handleSectionTap(id),
|
|
2582
|
+
onDeckTap: (floorId) => this.handleDeckTap(floorId),
|
|
2017
2583
|
onFps: this.opts.onFps
|
|
2018
2584
|
});
|
|
2019
2585
|
if (this.closed) {
|
|
@@ -2021,15 +2587,28 @@ var PickerController = class {
|
|
|
2021
2587
|
return null;
|
|
2022
2588
|
}
|
|
2023
2589
|
this.renderer = renderer;
|
|
2024
|
-
|
|
2025
|
-
|
|
2590
|
+
let seats = null;
|
|
2591
|
+
try {
|
|
2592
|
+
const objs = await this.api.objects(this.key);
|
|
2593
|
+
this.hidden = new Set(objs.hidden ?? []);
|
|
2594
|
+
seats = objs.seats;
|
|
2595
|
+
} catch {
|
|
2596
|
+
}
|
|
2597
|
+
if (this.closed) {
|
|
2598
|
+
renderer.destroy();
|
|
2599
|
+
this.renderer = null;
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
renderer.setChart(this.visibleDoc());
|
|
2603
|
+
if (seats) this.applySeatsMap(seats);
|
|
2026
2604
|
this.connect();
|
|
2027
2605
|
return {
|
|
2028
2606
|
doc: res.doc,
|
|
2029
2607
|
salesClosed: !!res.event.salesClosed,
|
|
2030
2608
|
eventName: res.event.name,
|
|
2031
2609
|
venue: res.event.venue,
|
|
2032
|
-
startsAt: res.event.startsAt
|
|
2610
|
+
startsAt: res.event.startsAt,
|
|
2611
|
+
currency
|
|
2033
2612
|
};
|
|
2034
2613
|
}
|
|
2035
2614
|
// ---- selection ------------------------------------------------------------
|
|
@@ -2201,6 +2780,126 @@ var PickerController = class {
|
|
|
2201
2780
|
setAccessibilityFilter(types) {
|
|
2202
2781
|
this.renderer?.setAccessibilityFilter?.(types);
|
|
2203
2782
|
}
|
|
2783
|
+
// ---- multi-floor (Batch 5) ------------------------------------------------
|
|
2784
|
+
/** Floors for the buyer's switcher (>1 ⇒ show it). Single-floor ⇒ one entry. */
|
|
2785
|
+
getFloors() {
|
|
2786
|
+
return this.renderer?.getFloors?.() ?? [];
|
|
2787
|
+
}
|
|
2788
|
+
getActiveFloorId() {
|
|
2789
|
+
return this.renderer?.getActiveFloorId?.() ?? "";
|
|
2790
|
+
}
|
|
2791
|
+
/** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
|
|
2792
|
+
setFloor(id) {
|
|
2793
|
+
const r = this.renderer;
|
|
2794
|
+
if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
|
|
2795
|
+
r.setStacked?.(false);
|
|
2796
|
+
r.setActiveFloor(id);
|
|
2797
|
+
void this.resnapshot();
|
|
2798
|
+
this.emitSelectionChange();
|
|
2799
|
+
}
|
|
2800
|
+
/** 3D all-floors stacked overview ⇄ active floor. Re-applies statuses after. */
|
|
2801
|
+
setStacked(on) {
|
|
2802
|
+
const r = this.renderer;
|
|
2803
|
+
if (!r?.setStacked || on === r.isStacked?.()) return;
|
|
2804
|
+
r.setStacked(on);
|
|
2805
|
+
void this.resnapshot();
|
|
2806
|
+
}
|
|
2807
|
+
isMultiFloor() {
|
|
2808
|
+
return (this._doc?.floors?.length ?? 0) > 1;
|
|
2809
|
+
}
|
|
2810
|
+
/** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
|
|
2811
|
+
* tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
|
|
2812
|
+
handleDeckTap(floorId) {
|
|
2813
|
+
const r = this.renderer;
|
|
2814
|
+
if (!r) return;
|
|
2815
|
+
r.setViewMode?.("flat");
|
|
2816
|
+
r.setStacked?.(false);
|
|
2817
|
+
r.setActiveFloor?.(floorId);
|
|
2818
|
+
void this.resnapshot();
|
|
2819
|
+
this.emitSelectionChange();
|
|
2820
|
+
this.opts.onDeckTap?.(floorId);
|
|
2821
|
+
}
|
|
2822
|
+
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
2823
|
+
/** Switch the map projection (2D flat ⇄ 3D isometric). No-op on a flat renderer. */
|
|
2824
|
+
setViewMode(mode) {
|
|
2825
|
+
this.renderer?.setViewMode?.(mode);
|
|
2826
|
+
}
|
|
2827
|
+
getViewMode() {
|
|
2828
|
+
return this.renderer?.getViewMode?.() ?? "flat";
|
|
2829
|
+
}
|
|
2830
|
+
/** Current LOD rung (for the ZONES/SECTIONS/SEATS pill). */
|
|
2831
|
+
getRung() {
|
|
2832
|
+
return this.renderer?.getRung?.() ?? "seats";
|
|
2833
|
+
}
|
|
2834
|
+
/** Jump to a rung; ZONES clears any focused summary (back to overview). */
|
|
2835
|
+
setRung(rung) {
|
|
2836
|
+
if (rung === "zones") {
|
|
2837
|
+
this.overview();
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
this.renderer?.setRung?.(rung);
|
|
2841
|
+
}
|
|
2842
|
+
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
2843
|
+
focusSection(id) {
|
|
2844
|
+
this.handleSectionTap(id);
|
|
2845
|
+
}
|
|
2846
|
+
/** Zoom back out to the whole chart and clear the section-summary card. */
|
|
2847
|
+
overview() {
|
|
2848
|
+
this.renderer?.zoomToFit();
|
|
2849
|
+
this.opts.onSectionFocus?.(null);
|
|
2850
|
+
}
|
|
2851
|
+
/** Glide the camera into a tapped section and emit its computed summary. */
|
|
2852
|
+
handleSectionTap(id) {
|
|
2853
|
+
const r = this.renderer;
|
|
2854
|
+
if (!r) return;
|
|
2855
|
+
r.focusRegion?.(id);
|
|
2856
|
+
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Build a section summary from the renderer's spatial membership: section +
|
|
2860
|
+
* zone labels, live seats-left, price range, and the per-category breakdown.
|
|
2861
|
+
*/
|
|
2862
|
+
sectionSummary(id) {
|
|
2863
|
+
const r = this.renderer;
|
|
2864
|
+
const doc = this._doc;
|
|
2865
|
+
if (!r || !doc) return null;
|
|
2866
|
+
const sec = doc.objects.find(
|
|
2867
|
+
(o) => o.type === "section" && o.id === id
|
|
2868
|
+
);
|
|
2869
|
+
if (!sec) return null;
|
|
2870
|
+
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
2871
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
2872
|
+
let seatsLeft = 0;
|
|
2873
|
+
for (const sid of memberIds) {
|
|
2874
|
+
const seat = this.seatById.get(sid);
|
|
2875
|
+
if (!seat) continue;
|
|
2876
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
2877
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
2878
|
+
}
|
|
2879
|
+
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
2880
|
+
const cat = doc.categories.find((c) => c.key === key);
|
|
2881
|
+
return {
|
|
2882
|
+
key,
|
|
2883
|
+
count,
|
|
2884
|
+
label: cat?.label ?? key,
|
|
2885
|
+
color: cat?.color ?? "#6e7bff",
|
|
2886
|
+
price: cat?.price ?? 0
|
|
2887
|
+
};
|
|
2888
|
+
}).sort((a, b) => a.price - b.price);
|
|
2889
|
+
const prices = categories.map((c) => c.price);
|
|
2890
|
+
const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
|
|
2891
|
+
const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
|
|
2892
|
+
return {
|
|
2893
|
+
id,
|
|
2894
|
+
label: sec.label,
|
|
2895
|
+
zoneLabel: zone?.label ?? "",
|
|
2896
|
+
color,
|
|
2897
|
+
seatsLeft,
|
|
2898
|
+
priceMin: prices.length ? prices[0] : 0,
|
|
2899
|
+
priceMax: prices.length ? prices[prices.length - 1] : 0,
|
|
2900
|
+
categories
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2204
2903
|
destroy() {
|
|
2205
2904
|
this.closed = true;
|
|
2206
2905
|
if (this.reconnectTimer) {
|
|
@@ -2223,11 +2922,53 @@ var PickerController = class {
|
|
|
2223
2922
|
}
|
|
2224
2923
|
// ---- internals ------------------------------------------------------------
|
|
2225
2924
|
toSeat(s) {
|
|
2226
|
-
|
|
2925
|
+
const tiers = this.tiersFor(s.categoryKey);
|
|
2926
|
+
if (!tiers) {
|
|
2927
|
+
return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
|
|
2928
|
+
}
|
|
2929
|
+
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
2930
|
+
return {
|
|
2931
|
+
id: s.id,
|
|
2932
|
+
label: s.label,
|
|
2933
|
+
categoryKey: s.categoryKey,
|
|
2934
|
+
price: chosen.price,
|
|
2935
|
+
tiers,
|
|
2936
|
+
tierId: chosen.id
|
|
2937
|
+
};
|
|
2227
2938
|
}
|
|
2228
2939
|
priceFor(categoryKey) {
|
|
2229
2940
|
return this._doc?.categories.find((c) => c.key === categoryKey)?.price ?? 0;
|
|
2230
2941
|
}
|
|
2942
|
+
tiersFor(categoryKey) {
|
|
2943
|
+
const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
|
|
2944
|
+
return t2 && t2.length ? t2 : void 0;
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
|
|
2948
|
+
* selection so the host's cart + the eventual hold carry the new tier + price.
|
|
2949
|
+
* `tierId = null` reverts to the category's first (default) tier. No-op if the
|
|
2950
|
+
* seat's category has no tiers or the id isn't one of them.
|
|
2951
|
+
*/
|
|
2952
|
+
setSeatTier(seatId, tierId) {
|
|
2953
|
+
const seat = this.seatById.get(seatId);
|
|
2954
|
+
if (!seat) return;
|
|
2955
|
+
const tiers = this.tiersFor(seat.categoryKey);
|
|
2956
|
+
if (!tiers) return;
|
|
2957
|
+
if (tierId == null) this.seatTiers.delete(seatId);
|
|
2958
|
+
else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
|
|
2959
|
+
else return;
|
|
2960
|
+
this.emitSelectionChange();
|
|
2961
|
+
if (this.hold_) this.hold_ = { ...this.hold_, seats: this.seatsForLabels(this.hold_.labels) };
|
|
2962
|
+
}
|
|
2963
|
+
/** PickerSeats (with chosen tier) for a set of held labels. */
|
|
2964
|
+
seatsForLabels(labels) {
|
|
2965
|
+
const out = [];
|
|
2966
|
+
for (const l of labels) {
|
|
2967
|
+
const s = this.labelToSeat.get(l);
|
|
2968
|
+
if (s) out.push(this.toSeat(s));
|
|
2969
|
+
}
|
|
2970
|
+
return out;
|
|
2971
|
+
}
|
|
2231
2972
|
emitSelectionChange() {
|
|
2232
2973
|
this.opts.onSelectionChange?.(this.getSelection());
|
|
2233
2974
|
}
|
|
@@ -2253,11 +2994,12 @@ var PickerController = class {
|
|
|
2253
2994
|
}
|
|
2254
2995
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
2255
2996
|
setHold(hold) {
|
|
2256
|
-
|
|
2997
|
+
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
2998
|
+
this.hold_ = full;
|
|
2257
2999
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
2258
|
-
const ms = Math.max(0,
|
|
3000
|
+
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
2259
3001
|
this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
|
|
2260
|
-
this.opts.onHold?.(
|
|
3002
|
+
this.opts.onHold?.(full);
|
|
2261
3003
|
}
|
|
2262
3004
|
clearHold() {
|
|
2263
3005
|
this.hold_ = null;
|
|
@@ -2321,6 +3063,11 @@ var PickerController = class {
|
|
|
2321
3063
|
const r = this.renderer;
|
|
2322
3064
|
if (!r || !msg || typeof msg !== "object") return;
|
|
2323
3065
|
const m = msg;
|
|
3066
|
+
if (Array.isArray(m.hidden) && this.syncHidden(m.hidden)) {
|
|
3067
|
+
void this.resnapshot();
|
|
3068
|
+
this.opts.onStatusChange?.();
|
|
3069
|
+
}
|
|
3070
|
+
if (m.type === "hidden") return;
|
|
2324
3071
|
if (m.seats && typeof m.seats === "object") {
|
|
2325
3072
|
this.applySeatsMap(m.seats);
|
|
2326
3073
|
} else if (Array.isArray(m.changes)) {
|
|
@@ -2357,21 +3104,67 @@ var PickerController = class {
|
|
|
2357
3104
|
}, delay);
|
|
2358
3105
|
}
|
|
2359
3106
|
};
|
|
3107
|
+
|
|
3108
|
+
// src/i18n/bundles.ts
|
|
3109
|
+
var LOADERS = {
|
|
3110
|
+
es: () => import("./es-UM4ZS357.js").then((m) => ({ default: m.es })),
|
|
3111
|
+
de: () => import("./de-Y3E6YTGP.js").then((m) => ({ default: m.de })),
|
|
3112
|
+
fr: () => import("./fr-IGRD2M4W.js").then((m) => ({ default: m.fr }))
|
|
3113
|
+
};
|
|
3114
|
+
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
3115
|
+
async function loadLocale(code) {
|
|
3116
|
+
const locale = resolveLocale(code);
|
|
3117
|
+
if (locale === "en" || loaded.has(locale)) {
|
|
3118
|
+
setLocale(locale);
|
|
3119
|
+
return locale;
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
const mod = await LOADERS[locale]();
|
|
3123
|
+
setLocale(locale, mod.default);
|
|
3124
|
+
loaded.add(locale);
|
|
3125
|
+
return locale;
|
|
3126
|
+
} catch {
|
|
3127
|
+
setLocale("en");
|
|
3128
|
+
return "en";
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
2360
3131
|
export {
|
|
2361
3132
|
ACCESSIBILITY_TYPES,
|
|
2362
3133
|
CHART_STORAGE_KEY,
|
|
3134
|
+
DEFAULT_CURRENCY,
|
|
2363
3135
|
PickerController,
|
|
3136
|
+
SUPPORTED_LOCALES,
|
|
2364
3137
|
SeatmapRenderer,
|
|
3138
|
+
UNGROUPED_ID,
|
|
2365
3139
|
accessibilityMeta,
|
|
3140
|
+
allObjects,
|
|
3141
|
+
applyHidden,
|
|
2366
3142
|
chartBounds,
|
|
3143
|
+
computeSections,
|
|
2367
3144
|
createRenderer,
|
|
3145
|
+
currencySymbol,
|
|
2368
3146
|
expandBooth,
|
|
2369
3147
|
expandChart,
|
|
2370
3148
|
expandRow,
|
|
2371
3149
|
expandRowSlots,
|
|
2372
3150
|
expandTable,
|
|
3151
|
+
floorObjects,
|
|
3152
|
+
floorsOf,
|
|
3153
|
+
formatDate,
|
|
3154
|
+
formatMoney,
|
|
3155
|
+
getLocale,
|
|
3156
|
+
hiddenObjectIds,
|
|
3157
|
+
isSectionHidden,
|
|
2373
3158
|
layerOf,
|
|
3159
|
+
loadLocale,
|
|
2374
3160
|
objectCenter,
|
|
2375
|
-
pointInPolygon
|
|
3161
|
+
pointInPolygon,
|
|
3162
|
+
resolveLocale,
|
|
3163
|
+
setLocale,
|
|
3164
|
+
setMoneyLocale,
|
|
3165
|
+
setStringOverrides,
|
|
3166
|
+
stackFloors,
|
|
3167
|
+
t,
|
|
3168
|
+
tCount
|
|
2376
3169
|
};
|
|
2377
3170
|
//# sourceMappingURL=index.js.map
|