@seatlayer/core 0.1.2 → 0.1.4
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-A4KJQ374.js +72 -0
- package/dist/de-A4KJQ374.js.map +1 -0
- package/dist/es-YNYMMP37.js +72 -0
- package/dist/es-YNYMMP37.js.map +1 -0
- package/dist/fr-DQ4KWKKF.js +72 -0
- package/dist/fr-DQ4KWKKF.js.map +1 -0
- package/dist/index.cjs +1248 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +468 -8
- package/dist/index.d.ts +468 -8
- package/dist/index.js +985 -93
- 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,26 +456,170 @@ 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 SeatLayer",
|
|
504
|
+
"picker.colorblind": "Colorblind-friendly colors",
|
|
505
|
+
"picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
|
|
506
|
+
"picker.seats.one": "{count} seat",
|
|
507
|
+
"picker.seats.other": "{count} seats",
|
|
508
|
+
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
509
|
+
"map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
|
|
510
|
+
"map.seatsLeft": "{count} LEFT",
|
|
511
|
+
"map.fromPrice": "FROM {price}",
|
|
512
|
+
// buyer picker page (src/pages/PickerPage.tsx)
|
|
513
|
+
"picker.language": "Language",
|
|
514
|
+
"picker.zoomToFit": "Zoom to fit",
|
|
515
|
+
"picker.viewMode": "View mode",
|
|
516
|
+
"picker.floor": "Floor",
|
|
517
|
+
"picker.zoomLevel": "Zoom level",
|
|
518
|
+
"picker.rungTip.zones": "Farthest rung \u2014 zone blocks (Floor / Lower Bowl / Upper Bowl)",
|
|
519
|
+
"picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
|
|
520
|
+
"picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
|
|
521
|
+
"picker.rungLabel.zones": "ZONES",
|
|
522
|
+
"picker.rungLabel.sections": "SECTIONS",
|
|
523
|
+
"picker.rungLabel.seats": "SEATS",
|
|
524
|
+
"picker.sectionSummaryAria": "{label} section summary",
|
|
525
|
+
"picker.closeSectionSummary": "Close section summary",
|
|
526
|
+
"picker.seatsLeftInSection.one": "{count} seat left",
|
|
527
|
+
"picker.seatsLeftInSection.other": "{count} seats left",
|
|
528
|
+
"picker.overview": "Overview",
|
|
529
|
+
"picker.tapSeatHint": "Tap any seat to check its view",
|
|
530
|
+
"picker.chartSize": "Chart size",
|
|
531
|
+
"picker.custom": "Custom",
|
|
532
|
+
"picker.categories": "Categories",
|
|
533
|
+
"picker.accessibility": "Accessibility",
|
|
534
|
+
"picker.showAnyAccessible": "Show any accessible seat",
|
|
535
|
+
"picker.any": "Any",
|
|
536
|
+
"picker.yourSeats": "Your seats",
|
|
537
|
+
"picker.emptySeats": "Tap seats on the map",
|
|
538
|
+
"picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
|
|
539
|
+
"picker.oneFewer": "One fewer",
|
|
540
|
+
"picker.oneMore": "One more",
|
|
541
|
+
"picker.remove": "Remove {label}",
|
|
542
|
+
"picker.ticketTierFor": "Ticket tier for {label}",
|
|
543
|
+
"picker.total": "Total: {amount}",
|
|
544
|
+
"picker.viewFromSeat": "View from seat {label}",
|
|
545
|
+
"picker.real360": "REAL 360\xB0",
|
|
546
|
+
"picker.preview": "PREVIEW",
|
|
547
|
+
"picker.open360": "Open 360\xB0 view",
|
|
548
|
+
"picker.sightline": "\u2248 {rows} rows from stage \xB7 {angle}\xB0 off-center",
|
|
549
|
+
"picker.bookedDemo": "Booked! (demo)",
|
|
550
|
+
"picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
|
|
551
|
+
"picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
|
|
552
|
+
"picker.simulateCrowd": "Simulate crowd: {state}",
|
|
553
|
+
"picker.on": "ON",
|
|
554
|
+
"picker.off": "OFF",
|
|
555
|
+
"picker.panorama360": "360\xB0 venue photo",
|
|
556
|
+
"picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage"
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/i18n/index.ts
|
|
560
|
+
var SUPPORTED_LOCALES = ["en", "es", "de", "fr"];
|
|
561
|
+
var bundles = { en };
|
|
562
|
+
var overrides = {};
|
|
563
|
+
var active = "en";
|
|
564
|
+
function resolveLocale(explicit, stored) {
|
|
565
|
+
for (const candidate of [explicit, stored, typeof navigator !== "undefined" ? navigator.language : null]) {
|
|
566
|
+
if (!candidate) continue;
|
|
567
|
+
const base = candidate.toLowerCase().split("-")[0];
|
|
568
|
+
if (SUPPORTED_LOCALES.includes(base)) return base;
|
|
569
|
+
}
|
|
570
|
+
return "en";
|
|
571
|
+
}
|
|
572
|
+
function getLocale() {
|
|
573
|
+
return active;
|
|
574
|
+
}
|
|
575
|
+
function setLocale(locale, bundle) {
|
|
576
|
+
if (bundle) bundles[locale] = { ...bundles[locale], ...bundle };
|
|
577
|
+
active = bundles[locale] ? locale : "en";
|
|
578
|
+
setMoneyLocale(active);
|
|
579
|
+
if (typeof document !== "undefined") document.documentElement.lang = active;
|
|
580
|
+
}
|
|
581
|
+
function setStringOverrides(next) {
|
|
582
|
+
overrides = next;
|
|
583
|
+
}
|
|
584
|
+
var PLACEHOLDER = /\{(\w+)\}/g;
|
|
585
|
+
function t(key, vars) {
|
|
586
|
+
const raw = overrides[key] ?? bundles[active]?.[key] ?? en[key] ?? key;
|
|
587
|
+
if (!vars) return raw;
|
|
588
|
+
return raw.replace(PLACEHOLDER, (_, name) => String(vars[name] ?? `{${name}}`));
|
|
589
|
+
}
|
|
590
|
+
function tCount(key, count, vars) {
|
|
591
|
+
const rule = new Intl.PluralRules(active).select(count);
|
|
592
|
+
const exact = overrides[`${key}.${rule}`] ?? bundles[active]?.[`${key}.${rule}`] ?? en[`${key}.${rule}`];
|
|
593
|
+
const raw = exact ?? overrides[`${key}.other`] ?? bundles[active]?.[`${key}.other`] ?? en[`${key}.other`] ?? key;
|
|
594
|
+
return raw.replace(PLACEHOLDER, (_, name) => String({ count, ...vars }[name] ?? `{${name}}`));
|
|
595
|
+
}
|
|
596
|
+
function formatDate(value, opts) {
|
|
597
|
+
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/engine/SeatmapRenderer.ts
|
|
315
601
|
var SEAT_RADIUS = 9;
|
|
316
602
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
317
603
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
318
604
|
var LABEL_SCALE = 1;
|
|
319
|
-
var SECTION_PROMINENT_SCALE = 0.
|
|
605
|
+
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
606
|
+
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
320
607
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
321
608
|
var MAX_LABELS = 700;
|
|
322
609
|
var ISO_ANGLE_DEG = -11.5;
|
|
323
610
|
var ISO_SQUASH = 0.58;
|
|
324
|
-
var LIFT_PER_STEP =
|
|
611
|
+
var LIFT_PER_STEP = 58;
|
|
325
612
|
var ISO_TWEEN_MS = 320;
|
|
326
613
|
var BLOCK_FILL_ALPHA = 0.85;
|
|
327
614
|
var SOLD_DARKEN = 0.5;
|
|
328
|
-
var SECTION_LABEL_PX =
|
|
329
|
-
var SECTION_SUB_PX =
|
|
615
|
+
var SECTION_LABEL_PX = 20;
|
|
616
|
+
var SECTION_SUB_PX = 12.5;
|
|
330
617
|
var ZONE_LABEL_PX = 30;
|
|
331
618
|
var ZONE_SUB_PX = 12;
|
|
332
619
|
var HELD_FILL = "#6b7280";
|
|
333
620
|
var TAKEN_FILL = "#374151";
|
|
334
621
|
var NFS_STROKE = "#4b5563";
|
|
622
|
+
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
335
623
|
var ACCESS_RING = {
|
|
336
624
|
wheelchair: "#3b82f6",
|
|
337
625
|
companion: "#8b5cf6",
|
|
@@ -343,7 +631,7 @@ var ACCESS_RING = {
|
|
|
343
631
|
};
|
|
344
632
|
function seatMatchesAccess(seat, filter) {
|
|
345
633
|
if (filter.length === 0) return !!seat.accessible;
|
|
346
|
-
return !!seat.accessibility?.some((
|
|
634
|
+
return !!seat.accessibility?.some((t2) => filter.includes(t2));
|
|
347
635
|
}
|
|
348
636
|
var DEF_SEAT_LABEL = "#0b1220";
|
|
349
637
|
var DEF_SELECTION = "#ffffff";
|
|
@@ -412,16 +700,21 @@ function mixColors(parts, fallback) {
|
|
|
412
700
|
}
|
|
413
701
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
414
702
|
}
|
|
415
|
-
function lerpColor(a, b,
|
|
703
|
+
function lerpColor(a, b, t2) {
|
|
416
704
|
const ca = hexToRgb(a);
|
|
417
705
|
const cb = hexToRgb(b);
|
|
418
706
|
if (!ca || !cb) return a;
|
|
419
|
-
return toHex(ca[0] + (cb[0] - ca[0]) *
|
|
707
|
+
return toHex(ca[0] + (cb[0] - ca[0]) * t2, ca[1] + (cb[1] - ca[1]) * t2, ca[2] + (cb[2] - ca[2]) * t2);
|
|
420
708
|
}
|
|
421
709
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
422
710
|
constructor(container, options = {}) {
|
|
423
711
|
this.seats = [];
|
|
424
712
|
this.seatById = /* @__PURE__ */ new Map();
|
|
713
|
+
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
714
|
+
this.chartDoc = null;
|
|
715
|
+
this.activeFloorId = "";
|
|
716
|
+
/** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
|
|
717
|
+
this.stacked = false;
|
|
425
718
|
/** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
|
|
426
719
|
this.circleById = /* @__PURE__ */ new Map();
|
|
427
720
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
@@ -429,6 +722,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
429
722
|
this.statusById = /* @__PURE__ */ new Map();
|
|
430
723
|
this.catColor = /* @__PURE__ */ new Map();
|
|
431
724
|
this.theme = {};
|
|
725
|
+
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
726
|
+
this.colorblind = false;
|
|
727
|
+
/** Category order from the doc — the stable index into the CB palette. */
|
|
728
|
+
this.catOrder = [];
|
|
432
729
|
this.selection = /* @__PURE__ */ new Set();
|
|
433
730
|
this.selectionRings = /* @__PURE__ */ new Map();
|
|
434
731
|
this.focusedId = null;
|
|
@@ -445,6 +742,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
445
742
|
this.zones = [];
|
|
446
743
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
447
744
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
745
|
+
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
746
|
+
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
747
|
+
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
748
|
+
this.objectFloor = /* @__PURE__ */ new Map();
|
|
448
749
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
449
750
|
this.zoneColor = /* @__PURE__ */ new Map();
|
|
450
751
|
this.hasSections = false;
|
|
@@ -457,6 +758,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
457
758
|
this.isoRaf = 0;
|
|
458
759
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
459
760
|
this.isoCentre = { x: 0, y: 0 };
|
|
761
|
+
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
762
|
+
this.glideRaf = 0;
|
|
460
763
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
461
764
|
this.destroyed = false;
|
|
462
765
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -504,6 +807,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
504
807
|
// pointer events away from Konva's canvas, killing its click/tap/hover
|
|
505
808
|
// pipeline. We rely on bubbling instead.
|
|
506
809
|
this.onPointerDown = (e) => {
|
|
810
|
+
this.cancelGlide();
|
|
507
811
|
this.pointers.set(e.pointerId, this.toLocal(e));
|
|
508
812
|
if (this.pointers.size === 1) {
|
|
509
813
|
this.moved = 0;
|
|
@@ -565,6 +869,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
565
869
|
};
|
|
566
870
|
this.container = container;
|
|
567
871
|
this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
|
|
872
|
+
this.currency = options.currency;
|
|
568
873
|
Konva.pixelRatio = this.dpr;
|
|
569
874
|
this.stage = new Stage({
|
|
570
875
|
container,
|
|
@@ -581,7 +886,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
581
886
|
if (container.tabIndex < 0) container.tabIndex = 0;
|
|
582
887
|
container.setAttribute("role", "application");
|
|
583
888
|
if (!container.getAttribute("aria-label")) {
|
|
584
|
-
container.setAttribute("aria-label", "
|
|
889
|
+
container.setAttribute("aria-label", t("map.aria"));
|
|
585
890
|
}
|
|
586
891
|
container.addEventListener("keydown", this.onKeyDown);
|
|
587
892
|
this.bgLayer = new Layer({ listening: true });
|
|
@@ -624,7 +929,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
624
929
|
}
|
|
625
930
|
}
|
|
626
931
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
627
|
-
setChart(doc) {
|
|
932
|
+
setChart(doc, opts) {
|
|
933
|
+
if (doc !== this.chartDoc) this.stacked = false;
|
|
934
|
+
this.chartDoc = doc;
|
|
935
|
+
this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
|
|
936
|
+
this.objectFloor.clear();
|
|
937
|
+
if (doc.floors && doc.floors.length > 1) {
|
|
938
|
+
for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
|
|
939
|
+
}
|
|
940
|
+
const view = this.floorView(doc);
|
|
628
941
|
this.focusedId = null;
|
|
629
942
|
this.focusRing.visible(false);
|
|
630
943
|
this.bgLayer.destroyChildren();
|
|
@@ -652,7 +965,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
652
965
|
this.isoT = 0;
|
|
653
966
|
this.isoTarget = 0;
|
|
654
967
|
this.resetLayerTransforms();
|
|
655
|
-
this.hasSections =
|
|
968
|
+
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
656
969
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
657
970
|
this.seatLayer.opacity(1);
|
|
658
971
|
this.hoverRing.visible(false);
|
|
@@ -663,28 +976,61 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
663
976
|
this.hoverRing.radius(this.seatR + 2);
|
|
664
977
|
this.catColor.clear();
|
|
665
978
|
this.catPrice.clear();
|
|
979
|
+
this.catOrder = doc.categories.map((c) => c.key);
|
|
666
980
|
for (const c of doc.categories) {
|
|
667
981
|
this.catColor.set(c.key, c.color);
|
|
668
982
|
if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
|
|
669
983
|
}
|
|
670
|
-
for (const obj of
|
|
984
|
+
for (const obj of view.objects) {
|
|
671
985
|
if (obj.type === "booth") {
|
|
672
986
|
this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
|
|
673
987
|
}
|
|
674
988
|
}
|
|
675
|
-
this.seats = expandChart(
|
|
989
|
+
this.seats = expandChart(view);
|
|
676
990
|
for (const s of this.seats) {
|
|
677
991
|
this.seatById.set(s.id, s);
|
|
678
992
|
this.statusById.set(s.id, "free");
|
|
679
993
|
}
|
|
680
|
-
this.renderBackground(
|
|
994
|
+
this.renderBackground(view);
|
|
681
995
|
this.renderSeats();
|
|
682
996
|
this.overlayLayer.add(this.labelGroup);
|
|
683
997
|
this.overlayLayer.add(this.hoverRing);
|
|
684
|
-
this.bounds = chartBounds(
|
|
998
|
+
this.bounds = chartBounds(view);
|
|
685
999
|
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
686
1000
|
this.zoomToFit();
|
|
687
1001
|
}
|
|
1002
|
+
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
1003
|
+
* the whole chart for single-floor charts. */
|
|
1004
|
+
floorView(doc) {
|
|
1005
|
+
if (!doc.floors || !doc.floors.length) return doc;
|
|
1006
|
+
if (this.stacked && doc.floors.length >= 2) return stackFloors(doc);
|
|
1007
|
+
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
1008
|
+
return { ...doc, objects: floor.objects, focalPoint: floor.focalPoint, backgroundImage: floor.backgroundImage, floors: void 0 };
|
|
1009
|
+
}
|
|
1010
|
+
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
1011
|
+
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
1012
|
+
setStacked(on) {
|
|
1013
|
+
if (!this.chartDoc || on === this.stacked) return;
|
|
1014
|
+
const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
|
|
1015
|
+
if (!multi) return;
|
|
1016
|
+
this.stacked = on;
|
|
1017
|
+
this.setChart(this.chartDoc, { floorId: this.activeFloorId });
|
|
1018
|
+
}
|
|
1019
|
+
isStacked() {
|
|
1020
|
+
return this.stacked;
|
|
1021
|
+
}
|
|
1022
|
+
/** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
|
|
1023
|
+
setActiveFloor(floorId) {
|
|
1024
|
+
if (!this.chartDoc || floorId === this.activeFloorId) return;
|
|
1025
|
+
this.setChart(this.chartDoc, { floorId });
|
|
1026
|
+
}
|
|
1027
|
+
/** Floors for the host's switcher (single-floor charts return one synthetic floor). */
|
|
1028
|
+
getFloors() {
|
|
1029
|
+
return this.chartDoc ? floorsOf(this.chartDoc).map((f) => ({ id: f.id, name: f.name })) : [];
|
|
1030
|
+
}
|
|
1031
|
+
getActiveFloorId() {
|
|
1032
|
+
return this.activeFloorId;
|
|
1033
|
+
}
|
|
688
1034
|
setStatus(seatIds, status) {
|
|
689
1035
|
let touched = false;
|
|
690
1036
|
const affected = this.hasSections ? /* @__PURE__ */ new Set() : null;
|
|
@@ -764,11 +1110,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
764
1110
|
const dur = 620;
|
|
765
1111
|
const step = (now) => {
|
|
766
1112
|
if (!ring.getLayer()) return;
|
|
767
|
-
const
|
|
768
|
-
ring.radius(this.seatR * (1 +
|
|
769
|
-
ring.opacity(0.9 * (1 -
|
|
1113
|
+
const t2 = Math.min(1, (now - start) / dur);
|
|
1114
|
+
ring.radius(this.seatR * (1 + t2 * 1.8));
|
|
1115
|
+
ring.opacity(0.9 * (1 - t2));
|
|
770
1116
|
this.overlayLayer.batchDraw();
|
|
771
|
-
if (
|
|
1117
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
772
1118
|
else {
|
|
773
1119
|
ring.destroy();
|
|
774
1120
|
this.overlayLayer.batchDraw();
|
|
@@ -875,7 +1221,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
875
1221
|
sameAccessFilter(next) {
|
|
876
1222
|
const cur = this.accessFilter;
|
|
877
1223
|
if (cur === null || next === null) return cur === next;
|
|
878
|
-
return cur.length === next.length && cur.every((
|
|
1224
|
+
return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
|
|
879
1225
|
}
|
|
880
1226
|
// ---- isometric ("3D") view mode -------------------------------------------
|
|
881
1227
|
/**
|
|
@@ -923,6 +1269,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
923
1269
|
};
|
|
924
1270
|
this.isoRaf = requestAnimationFrame(step);
|
|
925
1271
|
}
|
|
1272
|
+
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
1273
|
+
getViewMode() {
|
|
1274
|
+
return this.isoTarget === 1 ? "isometric" : "flat";
|
|
1275
|
+
}
|
|
926
1276
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
927
1277
|
isoParams() {
|
|
928
1278
|
return { th: ISO_ANGLE_DEG * Math.PI / 180 * this.isoT, sg: 1 - (1 - ISO_SQUASH) * this.isoT };
|
|
@@ -980,8 +1330,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
980
1330
|
* lift + extruded side faces on elevated sections.
|
|
981
1331
|
*/
|
|
982
1332
|
applyIso() {
|
|
983
|
-
const
|
|
984
|
-
if (
|
|
1333
|
+
const t2 = this.isoT;
|
|
1334
|
+
if (t2 === 0) {
|
|
985
1335
|
this.resetLayerTransforms();
|
|
986
1336
|
} else {
|
|
987
1337
|
const { th, sg } = this.isoParams();
|
|
@@ -1034,13 +1384,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1034
1384
|
}
|
|
1035
1385
|
/** Lift elevated sections (+ their members) and update their extruded side faces. */
|
|
1036
1386
|
applyElevation() {
|
|
1037
|
-
const
|
|
1387
|
+
const t2 = this.isoT;
|
|
1038
1388
|
for (const sec of this.sections) {
|
|
1039
1389
|
if (sec.elevation <= 0) continue;
|
|
1040
1390
|
const off = this.isoLiftLocal(sec.elevation);
|
|
1041
1391
|
sec.liftGroupBg?.position(off);
|
|
1042
1392
|
sec.liftGroupSeat?.position(off);
|
|
1043
|
-
const alpha = 0.9 *
|
|
1393
|
+
const alpha = 0.9 * t2;
|
|
1044
1394
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
1045
1395
|
const face = sec.sideFaces[i];
|
|
1046
1396
|
const p0 = sec.outline[i];
|
|
@@ -1054,6 +1404,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1054
1404
|
this.destroyed = true;
|
|
1055
1405
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
1056
1406
|
if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
|
|
1407
|
+
if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
|
|
1057
1408
|
if (this.viewChangeRaf) cancelAnimationFrame(this.viewChangeRaf);
|
|
1058
1409
|
if (this.recacheTimer) clearTimeout(this.recacheTimer);
|
|
1059
1410
|
this.resizeObs?.disconnect();
|
|
@@ -1133,7 +1484,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1133
1484
|
this.circleById.set(seat.id, rect);
|
|
1134
1485
|
this.paintSeat(rect, seat.id);
|
|
1135
1486
|
target.add(rect);
|
|
1136
|
-
const
|
|
1487
|
+
const t2 = new Text({
|
|
1137
1488
|
x: seat.x,
|
|
1138
1489
|
y: seat.y,
|
|
1139
1490
|
text: seat.label,
|
|
@@ -1144,17 +1495,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1144
1495
|
listening: false,
|
|
1145
1496
|
perfectDrawEnabled: false
|
|
1146
1497
|
});
|
|
1147
|
-
|
|
1148
|
-
|
|
1498
|
+
t2.offsetX(t2.width() / 2);
|
|
1499
|
+
t2.offsetY(t2.height() / 2);
|
|
1149
1500
|
this.hasBoothText = true;
|
|
1150
|
-
target.add(
|
|
1501
|
+
target.add(t2);
|
|
1502
|
+
}
|
|
1503
|
+
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
1504
|
+
seatBaseColor(categoryKey) {
|
|
1505
|
+
if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
|
|
1506
|
+
const idx = this.catOrder.indexOf(categoryKey);
|
|
1507
|
+
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
1151
1508
|
}
|
|
1152
1509
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
1153
1510
|
paintSeat(c, id) {
|
|
1154
1511
|
const seat = this.seatById.get(id);
|
|
1155
1512
|
const status = this.statusById.get(id) ?? "free";
|
|
1156
1513
|
const selected = this.selection.has(id);
|
|
1157
|
-
const base = this.
|
|
1514
|
+
const base = this.seatBaseColor(seat.categoryKey);
|
|
1158
1515
|
c.dash([]);
|
|
1159
1516
|
c.strokeWidth(0);
|
|
1160
1517
|
c.stroke("");
|
|
@@ -1167,8 +1524,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1167
1524
|
c.fill(HELD_FILL);
|
|
1168
1525
|
break;
|
|
1169
1526
|
case "booked":
|
|
1170
|
-
|
|
1171
|
-
|
|
1527
|
+
if (this.colorblind) {
|
|
1528
|
+
c.fill("rgba(0,0,0,0)");
|
|
1529
|
+
c.stroke(TAKEN_FILL);
|
|
1530
|
+
c.strokeWidth(1.5);
|
|
1531
|
+
c.opacity(0.9);
|
|
1532
|
+
} else {
|
|
1533
|
+
c.fill(TAKEN_FILL);
|
|
1534
|
+
c.opacity(0.45);
|
|
1535
|
+
}
|
|
1172
1536
|
break;
|
|
1173
1537
|
case "not_for_sale":
|
|
1174
1538
|
c.fill(TAKEN_FILL);
|
|
@@ -1183,6 +1547,50 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1183
1547
|
if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
|
|
1184
1548
|
c.opacity(0.25);
|
|
1185
1549
|
}
|
|
1550
|
+
if (this.dimmedSections.size) {
|
|
1551
|
+
const sec = this.seatSection.get(id);
|
|
1552
|
+
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1553
|
+
c.opacity(0.18);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
1559
|
+
* render booked seats hollow. Off restores the exact default rendering.
|
|
1560
|
+
*/
|
|
1561
|
+
setColorblindSafe(on) {
|
|
1562
|
+
if (on === this.colorblind) return;
|
|
1563
|
+
this.colorblind = on;
|
|
1564
|
+
for (const seat of this.seats) {
|
|
1565
|
+
const c = this.circleById.get(seat.id);
|
|
1566
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1567
|
+
}
|
|
1568
|
+
if (this.cached) {
|
|
1569
|
+
this.seatLayer.clearCache();
|
|
1570
|
+
this.cacheSeatLayer();
|
|
1571
|
+
} else {
|
|
1572
|
+
this.seatLayer.batchDraw();
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Dim the seats of these section/zone ids (organizer manager use) so held-back
|
|
1577
|
+
* inventory is visually distinct on the canvas without hiding it. `null`/empty
|
|
1578
|
+
* clears. Unlike the buyer's applyHidden, the sections stay rendered.
|
|
1579
|
+
*/
|
|
1580
|
+
setDimmedSections(ids) {
|
|
1581
|
+
const next = new Set(ids ?? []);
|
|
1582
|
+
if (next.size === this.dimmedSections.size && [...next].every((i) => this.dimmedSections.has(i))) return;
|
|
1583
|
+
this.dimmedSections = next;
|
|
1584
|
+
for (const seat of this.seats) {
|
|
1585
|
+
const c = this.circleById.get(seat.id);
|
|
1586
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1587
|
+
}
|
|
1588
|
+
if (this.cached) {
|
|
1589
|
+
this.seatLayer.clearCache();
|
|
1590
|
+
this.cacheSeatLayer();
|
|
1591
|
+
} else {
|
|
1592
|
+
this.seatLayer.batchDraw();
|
|
1593
|
+
}
|
|
1186
1594
|
}
|
|
1187
1595
|
/** Legend hover: highlight one category (dim the rest), or null to clear. */
|
|
1188
1596
|
setCategoryHighlight(key) {
|
|
@@ -1365,7 +1773,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1365
1773
|
}
|
|
1366
1774
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
1367
1775
|
addStageLabel(x, y, text) {
|
|
1368
|
-
const
|
|
1776
|
+
const t2 = new Text({
|
|
1369
1777
|
x,
|
|
1370
1778
|
y,
|
|
1371
1779
|
text: text.toUpperCase(),
|
|
@@ -1377,9 +1785,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1377
1785
|
listening: false,
|
|
1378
1786
|
perfectDrawEnabled: false
|
|
1379
1787
|
});
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
this.bgLayer.add(
|
|
1788
|
+
t2.offsetX(t2.width() / 2);
|
|
1789
|
+
t2.offsetY(t2.height() / 2);
|
|
1790
|
+
this.bgLayer.add(t2);
|
|
1383
1791
|
}
|
|
1384
1792
|
renderGA(obj) {
|
|
1385
1793
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
@@ -1460,12 +1868,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1460
1868
|
this.seatLayer.add(liftGroupSeat);
|
|
1461
1869
|
}
|
|
1462
1870
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
1871
|
+
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
1463
1872
|
const outlinePoly = new Line({
|
|
1464
1873
|
points: pts,
|
|
1465
1874
|
closed: true,
|
|
1466
|
-
stroke:
|
|
1467
|
-
strokeWidth: 1.
|
|
1468
|
-
fill: rgba(
|
|
1875
|
+
stroke: rgba(outlineTint, 0.5),
|
|
1876
|
+
strokeWidth: 1.75,
|
|
1877
|
+
fill: rgba(outlineTint, 0.08),
|
|
1878
|
+
lineJoin: "round",
|
|
1469
1879
|
listening: false,
|
|
1470
1880
|
perfectDrawEnabled: false
|
|
1471
1881
|
});
|
|
@@ -1481,6 +1891,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1481
1891
|
perfectDrawEnabled: false
|
|
1482
1892
|
});
|
|
1483
1893
|
bgTarget.add(blockPoly);
|
|
1894
|
+
const rowSeats = /* @__PURE__ */ new Map();
|
|
1895
|
+
for (const id of memberIds) {
|
|
1896
|
+
const s = this.seatById.get(id);
|
|
1897
|
+
if (!s) continue;
|
|
1898
|
+
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
1899
|
+
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
1900
|
+
}
|
|
1901
|
+
const rowLines = [];
|
|
1902
|
+
for (const arr of rowSeats.values()) {
|
|
1903
|
+
if (arr.length < 2) continue;
|
|
1904
|
+
arr.sort((a, b) => a.i - b.i);
|
|
1905
|
+
const line = new Line({
|
|
1906
|
+
points: arr.flatMap((p) => [p.x, p.y]),
|
|
1907
|
+
stroke: rgba("#ffffff", 0.34),
|
|
1908
|
+
strokeWidth: SEAT_RADIUS * 0.55,
|
|
1909
|
+
lineCap: "round",
|
|
1910
|
+
lineJoin: "round",
|
|
1911
|
+
opacity: 0,
|
|
1912
|
+
listening: false,
|
|
1913
|
+
perfectDrawEnabled: false
|
|
1914
|
+
});
|
|
1915
|
+
rowLines.push(line);
|
|
1916
|
+
bgTarget.add(line);
|
|
1917
|
+
}
|
|
1484
1918
|
const nameLabel = new Text({
|
|
1485
1919
|
x: centroid.x,
|
|
1486
1920
|
y: centroid.y,
|
|
@@ -1489,6 +1923,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1489
1923
|
fontStyle: "700",
|
|
1490
1924
|
fontFamily: this.labelFont(),
|
|
1491
1925
|
fill: "#8b93a7",
|
|
1926
|
+
// Dark halo so the label reads over the seat dots at any zoom.
|
|
1927
|
+
shadowColor: "#05070c",
|
|
1928
|
+
shadowBlur: 6,
|
|
1929
|
+
shadowOpacity: 0.9,
|
|
1930
|
+
shadowForStrokeEnabled: false,
|
|
1492
1931
|
listening: false,
|
|
1493
1932
|
perfectDrawEnabled: false
|
|
1494
1933
|
});
|
|
@@ -1498,11 +1937,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1498
1937
|
const subLabel = new Text({
|
|
1499
1938
|
x: centroid.x,
|
|
1500
1939
|
y: centroid.y,
|
|
1501
|
-
text:
|
|
1940
|
+
text: t("map.seatsLeft", { count: free }),
|
|
1502
1941
|
fontSize: 12,
|
|
1503
|
-
fontStyle: "
|
|
1942
|
+
fontStyle: "700",
|
|
1504
1943
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
1505
|
-
fill:
|
|
1944
|
+
fill: "#f4f6fb",
|
|
1945
|
+
shadowColor: "#05070c",
|
|
1946
|
+
shadowBlur: 5,
|
|
1947
|
+
shadowOpacity: 0.9,
|
|
1948
|
+
shadowForStrokeEnabled: false,
|
|
1506
1949
|
opacity: 0,
|
|
1507
1950
|
listening: false,
|
|
1508
1951
|
perfectDrawEnabled: false
|
|
@@ -1521,6 +1964,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1521
1964
|
baseFill,
|
|
1522
1965
|
outlinePoly,
|
|
1523
1966
|
blockPoly,
|
|
1967
|
+
rowLines,
|
|
1524
1968
|
nameLabel,
|
|
1525
1969
|
subLabel,
|
|
1526
1970
|
elevation,
|
|
@@ -1536,7 +1980,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1536
1980
|
refreshSectionFill(sec) {
|
|
1537
1981
|
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
1538
1982
|
sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
|
|
1539
|
-
sec.subLabel.text(
|
|
1983
|
+
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
1540
1984
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
1541
1985
|
}
|
|
1542
1986
|
/**
|
|
@@ -1554,8 +1998,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1554
1998
|
for (const z of doc.zones) {
|
|
1555
1999
|
const members = byZone.get(z.id);
|
|
1556
2000
|
if (!members || !members.length) continue;
|
|
1557
|
-
|
|
1558
|
-
const
|
|
2001
|
+
let anchor = members[0];
|
|
2002
|
+
for (const s of members) if (s.centroid.y < anchor.centroid.y) anchor = s;
|
|
2003
|
+
const cx = anchor.centroid.x;
|
|
2004
|
+
const cy = anchor.centroid.y;
|
|
1559
2005
|
let minPrice = Infinity;
|
|
1560
2006
|
for (const sec of members) {
|
|
1561
2007
|
for (const id of sec.memberIds) {
|
|
@@ -1573,6 +2019,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1573
2019
|
letterSpacing: 2,
|
|
1574
2020
|
fontFamily: this.labelFont(),
|
|
1575
2021
|
fill: z.color ?? "#f2f4f8",
|
|
2022
|
+
shadowColor: "#05070c",
|
|
2023
|
+
shadowBlur: 10,
|
|
2024
|
+
shadowOpacity: 0.95,
|
|
2025
|
+
shadowForStrokeEnabled: false,
|
|
1576
2026
|
opacity: 0,
|
|
1577
2027
|
listening: false,
|
|
1578
2028
|
perfectDrawEnabled: false
|
|
@@ -1585,7 +2035,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1585
2035
|
sub = new Text({
|
|
1586
2036
|
x: cx,
|
|
1587
2037
|
y: cy,
|
|
1588
|
-
text:
|
|
2038
|
+
text: t("map.fromPrice", { price: formatMoney(minPrice, this.currency) }),
|
|
1589
2039
|
fontSize: 14,
|
|
1590
2040
|
fontStyle: "600",
|
|
1591
2041
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
@@ -1616,7 +2066,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1616
2066
|
blockT = scale <= SECTION_PROMINENT_SCALE ? 1 : 0;
|
|
1617
2067
|
zoneT = scale <= ZONE_PROMINENT_SCALE ? 1 : 0;
|
|
1618
2068
|
} else {
|
|
1619
|
-
blockT = clamp((
|
|
2069
|
+
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
1620
2070
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
1621
2071
|
}
|
|
1622
2072
|
if (!this.zones.length) zoneT = 0;
|
|
@@ -1626,7 +2076,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1626
2076
|
if (rescale) this.lodScale = scale;
|
|
1627
2077
|
for (const sec of this.sections) {
|
|
1628
2078
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT);
|
|
1629
|
-
sec.
|
|
2079
|
+
for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT));
|
|
2080
|
+
sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
|
|
1630
2081
|
sec.nameLabel.opacity(1 - zoneT);
|
|
1631
2082
|
sec.subLabel.opacity(blockT * (1 - zoneT));
|
|
1632
2083
|
if (rescale) {
|
|
@@ -1634,9 +2085,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1634
2085
|
this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
|
|
1635
2086
|
}
|
|
1636
2087
|
}
|
|
2088
|
+
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
1637
2089
|
for (const zone of this.zones) {
|
|
1638
|
-
zone.label.opacity(
|
|
1639
|
-
if (zone.sub) zone.sub.opacity(
|
|
2090
|
+
zone.label.opacity(zoneOpacity);
|
|
2091
|
+
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
1640
2092
|
if (rescale) {
|
|
1641
2093
|
const cy = zone.label.y();
|
|
1642
2094
|
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
@@ -1646,11 +2098,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1646
2098
|
this.bgLayer.batchDraw();
|
|
1647
2099
|
}
|
|
1648
2100
|
/** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
|
|
1649
|
-
sizeLabel(
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2101
|
+
sizeLabel(t2, fontSize, y) {
|
|
2102
|
+
t2.fontSize(Math.max(1, fontSize));
|
|
2103
|
+
t2.offsetX(t2.width() / 2);
|
|
2104
|
+
t2.offsetY(t2.height() / 2);
|
|
2105
|
+
t2.y(y);
|
|
1654
2106
|
}
|
|
1655
2107
|
/**
|
|
1656
2108
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
@@ -1674,7 +2126,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1674
2126
|
return this.sections.find((s) => s.id === id)?.memberIds.slice() ?? [];
|
|
1675
2127
|
}
|
|
1676
2128
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
1677
|
-
const
|
|
2129
|
+
const t2 = new Text({
|
|
1678
2130
|
x,
|
|
1679
2131
|
y,
|
|
1680
2132
|
text,
|
|
@@ -1685,9 +2137,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1685
2137
|
listening: false,
|
|
1686
2138
|
perfectDrawEnabled: false
|
|
1687
2139
|
});
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
layer.add(
|
|
2140
|
+
t2.offsetX(t2.width() / 2);
|
|
2141
|
+
t2.offsetY(t2.height() / 2);
|
|
2142
|
+
layer.add(t2);
|
|
1691
2143
|
}
|
|
1692
2144
|
// ---- selection ------------------------------------------------------------
|
|
1693
2145
|
isSelectable(id) {
|
|
@@ -1741,7 +2193,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1741
2193
|
this.seatLayer.on("click tap", (e) => {
|
|
1742
2194
|
if (this.moved > 8) return;
|
|
1743
2195
|
const id = seatIdOf(e.target);
|
|
1744
|
-
if (id)
|
|
2196
|
+
if (!id) return;
|
|
2197
|
+
if (this.stacked && this.opts.onDeckTap) {
|
|
2198
|
+
const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
|
|
2199
|
+
if (floorId) {
|
|
2200
|
+
this.opts.onDeckTap(floorId);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
this.toggleSeat(id);
|
|
1745
2205
|
});
|
|
1746
2206
|
this.seatLayer.on("mouseover", (e) => {
|
|
1747
2207
|
const id = seatIdOf(e.target);
|
|
@@ -1771,11 +2231,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1771
2231
|
if (!this.cached || this.moved > 8) return;
|
|
1772
2232
|
const pointer = this.stage.getPointerPosition();
|
|
1773
2233
|
if (!pointer) return;
|
|
2234
|
+
if (this.stacked && this.opts.onDeckTap) {
|
|
2235
|
+
const floorId = this.deckFloorAt();
|
|
2236
|
+
if (floorId) {
|
|
2237
|
+
this.opts.onDeckTap(floorId);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
1774
2241
|
if (this.sections.length) {
|
|
1775
2242
|
const world = this.screenToWorld(pointer);
|
|
1776
2243
|
const hit = this.sections.find((sn) => pointInPolygon(world, sn.outline));
|
|
1777
2244
|
if (hit) {
|
|
1778
|
-
this.
|
|
2245
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.id);
|
|
2246
|
+
else this.focusRegion(hit.id);
|
|
1779
2247
|
return;
|
|
1780
2248
|
}
|
|
1781
2249
|
}
|
|
@@ -1783,6 +2251,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1783
2251
|
this.zoomAbout(target, pointer);
|
|
1784
2252
|
});
|
|
1785
2253
|
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Which deck (floor) a screen tap landed on in the 3D stacked overview. The
|
|
2256
|
+
* seat layer is melt-cached at that zoom so individual seat nodes aren't
|
|
2257
|
+
* hit-testable; instead we invert the iso+stage transform via the layer's
|
|
2258
|
+
* relative pointer and take the nearest seat's floor. Only the floor matters,
|
|
2259
|
+
* so within-floor elevation offsets don't affect the result. Null if none.
|
|
2260
|
+
*/
|
|
2261
|
+
deckFloorAt() {
|
|
2262
|
+
if (!this.objectFloor.size) return null;
|
|
2263
|
+
const p = this.seatLayer.getRelativePointerPosition();
|
|
2264
|
+
if (!p) return null;
|
|
2265
|
+
let best = null;
|
|
2266
|
+
let bestD = Infinity;
|
|
2267
|
+
for (const s of this.seats) {
|
|
2268
|
+
const dx = s.x - p.x;
|
|
2269
|
+
const dy = s.y - p.y;
|
|
2270
|
+
const d = dx * dx + dy * dy;
|
|
2271
|
+
if (d < bestD) {
|
|
2272
|
+
bestD = d;
|
|
2273
|
+
best = s.rowId;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return best ? this.objectFloor.get(best) ?? null : null;
|
|
2277
|
+
}
|
|
1786
2278
|
// ---- Pan & pinch (raw pointer events — mouse, touch and pen alike) --------
|
|
1787
2279
|
toLocal(e) {
|
|
1788
2280
|
const r = this.container.getBoundingClientRect();
|
|
@@ -1793,7 +2285,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1793
2285
|
* reach a readable size on any chart, however large the venue.
|
|
1794
2286
|
*/
|
|
1795
2287
|
zoomBounds() {
|
|
1796
|
-
|
|
2288
|
+
let min = 0.5 * this.fitScale;
|
|
2289
|
+
if (this.zones.length) min = Math.min(min, ZONE_PROMINENT_SCALE * 0.9);
|
|
2290
|
+
return { min, max: Math.max(10 * this.fitScale, 4) };
|
|
1797
2291
|
}
|
|
1798
2292
|
/** Zoom so `clientPoint` (stage-relative px) stays fixed under the cursor. */
|
|
1799
2293
|
zoomAbout(nextScale, clientPoint) {
|
|
@@ -1826,6 +2320,92 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1826
2320
|
this.afterViewChange();
|
|
1827
2321
|
this.stage.batchDraw();
|
|
1828
2322
|
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Smoothly glide the camera to frame a section (by id) or a world-space bounds
|
|
2325
|
+
* rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
|
|
2326
|
+
* melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
|
|
2327
|
+
* `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
|
|
2328
|
+
* newer glide cancels an in-flight one.
|
|
2329
|
+
*/
|
|
2330
|
+
focusRegion(target, opts) {
|
|
2331
|
+
const b = typeof target === "string" ? (() => {
|
|
2332
|
+
const sec = this.sections.find((s) => s.id === target);
|
|
2333
|
+
return sec ? polyBounds(sec.outline) : null;
|
|
2334
|
+
})() : target;
|
|
2335
|
+
if (!b) return;
|
|
2336
|
+
this.cancelGlide();
|
|
2337
|
+
if (opts?.animate === false || this.reducedMotion) {
|
|
2338
|
+
this.zoomToBounds(b);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const w = this.stage.width();
|
|
2342
|
+
const h = this.stage.height();
|
|
2343
|
+
const { min, max } = this.zoomBounds();
|
|
2344
|
+
const margin = 1.12;
|
|
2345
|
+
const toScale = clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
|
|
2346
|
+
const toX = w / 2 - (b.x + b.width / 2) * toScale;
|
|
2347
|
+
const toY = h / 2 - (b.y + b.height / 2) * toScale;
|
|
2348
|
+
const fromScale = this.stage.scaleX();
|
|
2349
|
+
const fromX = this.stage.x();
|
|
2350
|
+
const fromY = this.stage.y();
|
|
2351
|
+
if (Math.abs(toScale - fromScale) < 1e-4 && Math.abs(toX - fromX) < 0.5 && Math.abs(toY - fromY) < 0.5) {
|
|
2352
|
+
this.afterViewChange();
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const start = performance.now();
|
|
2356
|
+
const DUR = 450;
|
|
2357
|
+
const step = (now) => {
|
|
2358
|
+
if (this.destroyed) {
|
|
2359
|
+
this.glideRaf = 0;
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const raw = Math.min(1, (now - start) / DUR);
|
|
2363
|
+
const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
|
|
2364
|
+
const sc = fromScale + (toScale - fromScale) * e;
|
|
2365
|
+
this.stage.scale({ x: sc, y: sc });
|
|
2366
|
+
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
2367
|
+
this.updateLOD();
|
|
2368
|
+
this.scheduleViewChange();
|
|
2369
|
+
this.stage.batchDraw();
|
|
2370
|
+
if (raw < 1) {
|
|
2371
|
+
this.glideRaf = requestAnimationFrame(step);
|
|
2372
|
+
} else {
|
|
2373
|
+
this.glideRaf = 0;
|
|
2374
|
+
this.afterViewChange();
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
this.glideRaf = requestAnimationFrame(step);
|
|
2378
|
+
}
|
|
2379
|
+
/** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
|
|
2380
|
+
cancelGlide() {
|
|
2381
|
+
if (this.glideRaf) {
|
|
2382
|
+
cancelAnimationFrame(this.glideRaf);
|
|
2383
|
+
this.glideRaf = 0;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
2387
|
+
getRung() {
|
|
2388
|
+
const scale = this.effScale();
|
|
2389
|
+
if (scale >= CACHE_THRESHOLD) return "seats";
|
|
2390
|
+
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
2391
|
+
return "sections";
|
|
2392
|
+
}
|
|
2393
|
+
/** Jump the camera to a rung's zoom band, centred on the chart (glided). */
|
|
2394
|
+
setRung(rung) {
|
|
2395
|
+
if (rung === "zones") {
|
|
2396
|
+
this.cancelGlide();
|
|
2397
|
+
this.zoomToFit();
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_LEGIBLE_SCALE * 1.1, CACHE_THRESHOLD * 1.3);
|
|
2401
|
+
const w = this.stage.width();
|
|
2402
|
+
const h = this.stage.height();
|
|
2403
|
+
const cx = this.bounds.x + this.bounds.width / 2;
|
|
2404
|
+
const cy = this.bounds.y + this.bounds.height / 2;
|
|
2405
|
+
const bw = w / (target * 1.12);
|
|
2406
|
+
const bh = h / (target * 1.12);
|
|
2407
|
+
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
2408
|
+
}
|
|
1829
2409
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
1830
2410
|
afterViewChange() {
|
|
1831
2411
|
this.updateLOD();
|
|
@@ -1876,7 +2456,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1876
2456
|
let count = 0;
|
|
1877
2457
|
for (const seat of this.seats) {
|
|
1878
2458
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
1879
|
-
const
|
|
2459
|
+
const t2 = new Text({
|
|
1880
2460
|
x: seat.x,
|
|
1881
2461
|
y: seat.y,
|
|
1882
2462
|
text: seat.label,
|
|
@@ -1888,14 +2468,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1888
2468
|
perfectDrawEnabled: false
|
|
1889
2469
|
});
|
|
1890
2470
|
const maxW = this.seatR * 2 - 3;
|
|
1891
|
-
if (
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
2471
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(4, 7 * maxW / t2.width()));
|
|
2472
|
+
if (t2.fontSize() < 4.2) {
|
|
2473
|
+
t2.destroy();
|
|
1894
2474
|
continue;
|
|
1895
2475
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
this.labelGroup.add(
|
|
2476
|
+
t2.offsetX(t2.width() / 2);
|
|
2477
|
+
t2.offsetY(t2.height() / 2);
|
|
2478
|
+
this.labelGroup.add(t2);
|
|
1899
2479
|
if (++count >= MAX_LABELS) break;
|
|
1900
2480
|
}
|
|
1901
2481
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -1930,6 +2510,38 @@ function createRenderer(container, opts) {
|
|
|
1930
2510
|
return new SeatmapRenderer(container, opts);
|
|
1931
2511
|
}
|
|
1932
2512
|
|
|
2513
|
+
// src/core/orphans.ts
|
|
2514
|
+
function seatIndex(id) {
|
|
2515
|
+
const n = Number(id.slice(id.lastIndexOf(":") + 1));
|
|
2516
|
+
return Number.isFinite(n) ? n : -1;
|
|
2517
|
+
}
|
|
2518
|
+
function strandedSingles(seats, statusOf, selectedIds) {
|
|
2519
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2520
|
+
for (const s of seats) {
|
|
2521
|
+
if (s.kind === "booth") continue;
|
|
2522
|
+
const list = rows.get(s.rowId);
|
|
2523
|
+
if (list) list.push(s);
|
|
2524
|
+
else rows.set(s.rowId, [s]);
|
|
2525
|
+
}
|
|
2526
|
+
const unavailable = (s) => selectedIds.has(s.id) || statusOf(s.id) !== "free";
|
|
2527
|
+
const out = [];
|
|
2528
|
+
for (const list of rows.values()) {
|
|
2529
|
+
if (list.length < 3) continue;
|
|
2530
|
+
list.sort((a, b) => seatIndex(a.id) - seatIndex(b.id));
|
|
2531
|
+
for (let i = 1; i < list.length - 1; i++) {
|
|
2532
|
+
const seat = list[i];
|
|
2533
|
+
if (unavailable(seat)) continue;
|
|
2534
|
+
const left = list[i - 1];
|
|
2535
|
+
const right = list[i + 1];
|
|
2536
|
+
if (seatIndex(seat.id) - seatIndex(left.id) !== 1 || seatIndex(right.id) - seatIndex(seat.id) !== 1) continue;
|
|
2537
|
+
if (!unavailable(left) || !unavailable(right)) continue;
|
|
2538
|
+
if (!selectedIds.has(left.id) && !selectedIds.has(right.id)) continue;
|
|
2539
|
+
out.push(seat);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return out;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
1933
2545
|
// src/picker/PickerController.ts
|
|
1934
2546
|
var DEFAULT_MAX_SELECTION = 10;
|
|
1935
2547
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -1946,9 +2558,15 @@ var PickerController = class {
|
|
|
1946
2558
|
constructor(options) {
|
|
1947
2559
|
this.renderer = null;
|
|
1948
2560
|
this._doc = null;
|
|
2561
|
+
/** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
|
|
2562
|
+
this.hidden = /* @__PURE__ */ new Set();
|
|
1949
2563
|
/** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
|
|
1950
2564
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
1951
2565
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
2566
|
+
/** seatId → chosen ticket-tier id (absent ⇒ the category's first/default tier). */
|
|
2567
|
+
this.seatTiers = /* @__PURE__ */ new Map();
|
|
2568
|
+
/** id → seat, for the section-summary breakdown (renderer members are ids). */
|
|
2569
|
+
this.seatById = /* @__PURE__ */ new Map();
|
|
1952
2570
|
this.allIds = [];
|
|
1953
2571
|
// realtime socket
|
|
1954
2572
|
this.ws = null;
|
|
@@ -1958,6 +2576,8 @@ var PickerController = class {
|
|
|
1958
2576
|
// hold state
|
|
1959
2577
|
this.hold_ = null;
|
|
1960
2578
|
this.expiryTimer = null;
|
|
2579
|
+
/** Whether the last emitted hint was non-null (avoids clearing repeatedly). */
|
|
2580
|
+
this.hintShown = false;
|
|
1961
2581
|
this.opts = options;
|
|
1962
2582
|
this.api = options.transport;
|
|
1963
2583
|
this.key = options.eventKey;
|
|
@@ -1966,6 +2586,18 @@ var PickerController = class {
|
|
|
1966
2586
|
get doc() {
|
|
1967
2587
|
return this._doc;
|
|
1968
2588
|
}
|
|
2589
|
+
/** The chart with hidden sections' seats removed — what buyers actually see. */
|
|
2590
|
+
visibleDoc() {
|
|
2591
|
+
return this._doc ? applyHidden(this._doc, this.hidden) : { objects: [] };
|
|
2592
|
+
}
|
|
2593
|
+
/** Adopt a new hidden set; rebuild the visible chart only if it differs. */
|
|
2594
|
+
syncHidden(ids) {
|
|
2595
|
+
const next = ids.filter((x) => typeof x === "string");
|
|
2596
|
+
if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
|
|
2597
|
+
this.hidden = new Set(next);
|
|
2598
|
+
this.renderer?.setChart(this.visibleDoc());
|
|
2599
|
+
return true;
|
|
2600
|
+
}
|
|
1969
2601
|
currentHold() {
|
|
1970
2602
|
return this.hold_;
|
|
1971
2603
|
}
|
|
@@ -1993,15 +2625,19 @@ var PickerController = class {
|
|
|
1993
2625
|
this._doc = res.doc;
|
|
1994
2626
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
1995
2627
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
2628
|
+
this.seatById = /* @__PURE__ */ new Map();
|
|
1996
2629
|
this.allIds = [];
|
|
1997
2630
|
for (const s of expandChart(res.doc)) {
|
|
1998
2631
|
this.labelToId.set(s.label, s.id);
|
|
1999
2632
|
this.labelToSeat.set(s.label, s);
|
|
2633
|
+
this.seatById.set(s.id, s);
|
|
2000
2634
|
this.allIds.push(s.id);
|
|
2001
2635
|
}
|
|
2636
|
+
const currency = res.event.currency ?? this.opts.currency;
|
|
2002
2637
|
const renderer = createRenderer(host, {
|
|
2003
2638
|
maxSelection: this.maxSelection,
|
|
2004
2639
|
confirmSelection: this.opts.confirmSelection,
|
|
2640
|
+
currency,
|
|
2005
2641
|
onSelect: (seat) => {
|
|
2006
2642
|
this.opts.onSelect?.(seat);
|
|
2007
2643
|
this.emitSelectionChange();
|
|
@@ -2014,6 +2650,9 @@ var PickerController = class {
|
|
|
2014
2650
|
onFocusSeat: this.opts.onFocusSeat,
|
|
2015
2651
|
onViewChange: this.opts.onViewChange,
|
|
2016
2652
|
onGAClick: this.opts.onGAClick,
|
|
2653
|
+
// Section tap at the far/zone rung → glide in + surface the summary card.
|
|
2654
|
+
onSectionTap: (id) => this.handleSectionTap(id),
|
|
2655
|
+
onDeckTap: (floorId) => this.handleDeckTap(floorId),
|
|
2017
2656
|
onFps: this.opts.onFps
|
|
2018
2657
|
});
|
|
2019
2658
|
if (this.closed) {
|
|
@@ -2021,15 +2660,29 @@ var PickerController = class {
|
|
|
2021
2660
|
return null;
|
|
2022
2661
|
}
|
|
2023
2662
|
this.renderer = renderer;
|
|
2024
|
-
|
|
2025
|
-
|
|
2663
|
+
let seats = null;
|
|
2664
|
+
try {
|
|
2665
|
+
const objs = await this.api.objects(this.key);
|
|
2666
|
+
this.hidden = new Set(objs.hidden ?? []);
|
|
2667
|
+
seats = objs.seats;
|
|
2668
|
+
} catch {
|
|
2669
|
+
}
|
|
2670
|
+
if (this.closed) {
|
|
2671
|
+
renderer.destroy();
|
|
2672
|
+
this.renderer = null;
|
|
2673
|
+
return null;
|
|
2674
|
+
}
|
|
2675
|
+
renderer.setChart(this.visibleDoc());
|
|
2676
|
+
if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
|
|
2677
|
+
if (seats) this.applySeatsMap(seats);
|
|
2026
2678
|
this.connect();
|
|
2027
2679
|
return {
|
|
2028
2680
|
doc: res.doc,
|
|
2029
2681
|
salesClosed: !!res.event.salesClosed,
|
|
2030
2682
|
eventName: res.event.name,
|
|
2031
2683
|
venue: res.event.venue,
|
|
2032
|
-
startsAt: res.event.startsAt
|
|
2684
|
+
startsAt: res.event.startsAt,
|
|
2685
|
+
currency
|
|
2033
2686
|
};
|
|
2034
2687
|
}
|
|
2035
2688
|
// ---- selection ------------------------------------------------------------
|
|
@@ -2201,6 +2854,126 @@ var PickerController = class {
|
|
|
2201
2854
|
setAccessibilityFilter(types) {
|
|
2202
2855
|
this.renderer?.setAccessibilityFilter?.(types);
|
|
2203
2856
|
}
|
|
2857
|
+
// ---- multi-floor (Batch 5) ------------------------------------------------
|
|
2858
|
+
/** Floors for the buyer's switcher (>1 ⇒ show it). Single-floor ⇒ one entry. */
|
|
2859
|
+
getFloors() {
|
|
2860
|
+
return this.renderer?.getFloors?.() ?? [];
|
|
2861
|
+
}
|
|
2862
|
+
getActiveFloorId() {
|
|
2863
|
+
return this.renderer?.getActiveFloorId?.() ?? "";
|
|
2864
|
+
}
|
|
2865
|
+
/** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
|
|
2866
|
+
setFloor(id) {
|
|
2867
|
+
const r = this.renderer;
|
|
2868
|
+
if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
|
|
2869
|
+
r.setStacked?.(false);
|
|
2870
|
+
r.setActiveFloor(id);
|
|
2871
|
+
void this.resnapshot();
|
|
2872
|
+
this.emitSelectionChange();
|
|
2873
|
+
}
|
|
2874
|
+
/** 3D all-floors stacked overview ⇄ active floor. Re-applies statuses after. */
|
|
2875
|
+
setStacked(on) {
|
|
2876
|
+
const r = this.renderer;
|
|
2877
|
+
if (!r?.setStacked || on === r.isStacked?.()) return;
|
|
2878
|
+
r.setStacked(on);
|
|
2879
|
+
void this.resnapshot();
|
|
2880
|
+
}
|
|
2881
|
+
isMultiFloor() {
|
|
2882
|
+
return (this._doc?.floors?.length ?? 0) > 1;
|
|
2883
|
+
}
|
|
2884
|
+
/** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
|
|
2885
|
+
* tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
|
|
2886
|
+
handleDeckTap(floorId) {
|
|
2887
|
+
const r = this.renderer;
|
|
2888
|
+
if (!r) return;
|
|
2889
|
+
r.setViewMode?.("flat");
|
|
2890
|
+
r.setStacked?.(false);
|
|
2891
|
+
r.setActiveFloor?.(floorId);
|
|
2892
|
+
void this.resnapshot();
|
|
2893
|
+
this.emitSelectionChange();
|
|
2894
|
+
this.opts.onDeckTap?.(floorId);
|
|
2895
|
+
}
|
|
2896
|
+
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
2897
|
+
/** Switch the map projection (2D flat ⇄ 3D isometric). No-op on a flat renderer. */
|
|
2898
|
+
setViewMode(mode) {
|
|
2899
|
+
this.renderer?.setViewMode?.(mode);
|
|
2900
|
+
}
|
|
2901
|
+
getViewMode() {
|
|
2902
|
+
return this.renderer?.getViewMode?.() ?? "flat";
|
|
2903
|
+
}
|
|
2904
|
+
/** Current LOD rung (for the ZONES/SECTIONS/SEATS pill). */
|
|
2905
|
+
getRung() {
|
|
2906
|
+
return this.renderer?.getRung?.() ?? "seats";
|
|
2907
|
+
}
|
|
2908
|
+
/** Jump to a rung; ZONES clears any focused summary (back to overview). */
|
|
2909
|
+
setRung(rung) {
|
|
2910
|
+
if (rung === "zones") {
|
|
2911
|
+
this.overview();
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
this.renderer?.setRung?.(rung);
|
|
2915
|
+
}
|
|
2916
|
+
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
2917
|
+
focusSection(id) {
|
|
2918
|
+
this.handleSectionTap(id);
|
|
2919
|
+
}
|
|
2920
|
+
/** Zoom back out to the whole chart and clear the section-summary card. */
|
|
2921
|
+
overview() {
|
|
2922
|
+
this.renderer?.zoomToFit();
|
|
2923
|
+
this.opts.onSectionFocus?.(null);
|
|
2924
|
+
}
|
|
2925
|
+
/** Glide the camera into a tapped section and emit its computed summary. */
|
|
2926
|
+
handleSectionTap(id) {
|
|
2927
|
+
const r = this.renderer;
|
|
2928
|
+
if (!r) return;
|
|
2929
|
+
r.focusRegion?.(id);
|
|
2930
|
+
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Build a section summary from the renderer's spatial membership: section +
|
|
2934
|
+
* zone labels, live seats-left, price range, and the per-category breakdown.
|
|
2935
|
+
*/
|
|
2936
|
+
sectionSummary(id) {
|
|
2937
|
+
const r = this.renderer;
|
|
2938
|
+
const doc = this._doc;
|
|
2939
|
+
if (!r || !doc) return null;
|
|
2940
|
+
const sec = doc.objects.find(
|
|
2941
|
+
(o) => o.type === "section" && o.id === id
|
|
2942
|
+
);
|
|
2943
|
+
if (!sec) return null;
|
|
2944
|
+
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
2945
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
2946
|
+
let seatsLeft = 0;
|
|
2947
|
+
for (const sid of memberIds) {
|
|
2948
|
+
const seat = this.seatById.get(sid);
|
|
2949
|
+
if (!seat) continue;
|
|
2950
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
2951
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
2952
|
+
}
|
|
2953
|
+
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
2954
|
+
const cat = doc.categories.find((c) => c.key === key);
|
|
2955
|
+
return {
|
|
2956
|
+
key,
|
|
2957
|
+
count,
|
|
2958
|
+
label: cat?.label ?? key,
|
|
2959
|
+
color: cat?.color ?? "#6e7bff",
|
|
2960
|
+
price: cat?.price ?? 0
|
|
2961
|
+
};
|
|
2962
|
+
}).sort((a, b) => a.price - b.price);
|
|
2963
|
+
const prices = categories.map((c) => c.price);
|
|
2964
|
+
const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
|
|
2965
|
+
const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
|
|
2966
|
+
return {
|
|
2967
|
+
id,
|
|
2968
|
+
label: sec.label,
|
|
2969
|
+
zoneLabel: zone?.label ?? "",
|
|
2970
|
+
color,
|
|
2971
|
+
seatsLeft,
|
|
2972
|
+
priceMin: prices.length ? prices[0] : 0,
|
|
2973
|
+
priceMax: prices.length ? prices[prices.length - 1] : 0,
|
|
2974
|
+
categories
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2204
2977
|
destroy() {
|
|
2205
2978
|
this.closed = true;
|
|
2206
2979
|
if (this.reconnectTimer) {
|
|
@@ -2223,13 +2996,80 @@ var PickerController = class {
|
|
|
2223
2996
|
}
|
|
2224
2997
|
// ---- internals ------------------------------------------------------------
|
|
2225
2998
|
toSeat(s) {
|
|
2226
|
-
|
|
2999
|
+
const tiers = this.tiersFor(s.categoryKey);
|
|
3000
|
+
if (!tiers) {
|
|
3001
|
+
return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
|
|
3002
|
+
}
|
|
3003
|
+
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
3004
|
+
return {
|
|
3005
|
+
id: s.id,
|
|
3006
|
+
label: s.label,
|
|
3007
|
+
categoryKey: s.categoryKey,
|
|
3008
|
+
price: chosen.price,
|
|
3009
|
+
tiers,
|
|
3010
|
+
tierId: chosen.id
|
|
3011
|
+
};
|
|
2227
3012
|
}
|
|
2228
3013
|
priceFor(categoryKey) {
|
|
2229
3014
|
return this._doc?.categories.find((c) => c.key === categoryKey)?.price ?? 0;
|
|
2230
3015
|
}
|
|
3016
|
+
tiersFor(categoryKey) {
|
|
3017
|
+
const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
|
|
3018
|
+
return t2 && t2.length ? t2 : void 0;
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
|
|
3022
|
+
* selection so the host's cart + the eventual hold carry the new tier + price.
|
|
3023
|
+
* `tierId = null` reverts to the category's first (default) tier. No-op if the
|
|
3024
|
+
* seat's category has no tiers or the id isn't one of them.
|
|
3025
|
+
*/
|
|
3026
|
+
setSeatTier(seatId, tierId) {
|
|
3027
|
+
const seat = this.seatById.get(seatId);
|
|
3028
|
+
if (!seat) return;
|
|
3029
|
+
const tiers = this.tiersFor(seat.categoryKey);
|
|
3030
|
+
if (!tiers) return;
|
|
3031
|
+
if (tierId == null) this.seatTiers.delete(seatId);
|
|
3032
|
+
else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
|
|
3033
|
+
else return;
|
|
3034
|
+
this.emitSelectionChange();
|
|
3035
|
+
if (this.hold_) this.hold_ = { ...this.hold_, seats: this.seatsForLabels(this.hold_.labels) };
|
|
3036
|
+
}
|
|
3037
|
+
/** PickerSeats (with chosen tier) for a set of held labels. */
|
|
3038
|
+
seatsForLabels(labels) {
|
|
3039
|
+
const out = [];
|
|
3040
|
+
for (const l of labels) {
|
|
3041
|
+
const s = this.labelToSeat.get(l);
|
|
3042
|
+
if (s) out.push(this.toSeat(s));
|
|
3043
|
+
}
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
2231
3046
|
emitSelectionChange() {
|
|
2232
3047
|
this.opts.onSelectionChange?.(this.getSelection());
|
|
3048
|
+
this.emitOrphanHint();
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* Orphan-seat advice on manual selection: when the buyer's current picks
|
|
3052
|
+
* strand one (or more) single free seats between unavailable same-row
|
|
3053
|
+
* neighbors, surface a localized, non-blocking hint. Cleared (null) as soon
|
|
3054
|
+
* as the selection stops stranding anyone.
|
|
3055
|
+
*/
|
|
3056
|
+
emitOrphanHint() {
|
|
3057
|
+
if (!this.opts.onHint) return;
|
|
3058
|
+
const r = this.renderer;
|
|
3059
|
+
if (!r) return;
|
|
3060
|
+
const selected = new Set(r.getSelection().map((s) => s.id));
|
|
3061
|
+
const stranded = selected.size ? strandedSingles(this.seatById.values(), (id) => r.getStatus(id), selected) : [];
|
|
3062
|
+
if (stranded.length > 0) {
|
|
3063
|
+
this.hintShown = true;
|
|
3064
|
+
this.opts.onHint(t("picker.orphanHint"));
|
|
3065
|
+
} else if (this.hintShown) {
|
|
3066
|
+
this.hintShown = false;
|
|
3067
|
+
this.opts.onHint(null);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
/** Toggle colorblind-safe rendering at runtime (see PickerOptions.colorblindSafe). */
|
|
3071
|
+
setColorblindSafe(on) {
|
|
3072
|
+
this.renderer?.setColorblindSafe?.(on);
|
|
2233
3073
|
}
|
|
2234
3074
|
emitError(err) {
|
|
2235
3075
|
if (this.opts.onError) this.opts.onError(err);
|
|
@@ -2253,11 +3093,12 @@ var PickerController = class {
|
|
|
2253
3093
|
}
|
|
2254
3094
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
2255
3095
|
setHold(hold) {
|
|
2256
|
-
|
|
3096
|
+
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
3097
|
+
this.hold_ = full;
|
|
2257
3098
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
2258
|
-
const ms = Math.max(0,
|
|
3099
|
+
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
2259
3100
|
this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
|
|
2260
|
-
this.opts.onHold?.(
|
|
3101
|
+
this.opts.onHold?.(full);
|
|
2261
3102
|
}
|
|
2262
3103
|
clearHold() {
|
|
2263
3104
|
this.hold_ = null;
|
|
@@ -2321,6 +3162,11 @@ var PickerController = class {
|
|
|
2321
3162
|
const r = this.renderer;
|
|
2322
3163
|
if (!r || !msg || typeof msg !== "object") return;
|
|
2323
3164
|
const m = msg;
|
|
3165
|
+
if (Array.isArray(m.hidden) && this.syncHidden(m.hidden)) {
|
|
3166
|
+
void this.resnapshot();
|
|
3167
|
+
this.opts.onStatusChange?.();
|
|
3168
|
+
}
|
|
3169
|
+
if (m.type === "hidden") return;
|
|
2324
3170
|
if (m.seats && typeof m.seats === "object") {
|
|
2325
3171
|
this.applySeatsMap(m.seats);
|
|
2326
3172
|
} else if (Array.isArray(m.changes)) {
|
|
@@ -2357,21 +3203,67 @@ var PickerController = class {
|
|
|
2357
3203
|
}, delay);
|
|
2358
3204
|
}
|
|
2359
3205
|
};
|
|
3206
|
+
|
|
3207
|
+
// src/i18n/bundles.ts
|
|
3208
|
+
var LOADERS = {
|
|
3209
|
+
es: () => import("./es-YNYMMP37.js").then((m) => ({ default: m.es })),
|
|
3210
|
+
de: () => import("./de-A4KJQ374.js").then((m) => ({ default: m.de })),
|
|
3211
|
+
fr: () => import("./fr-DQ4KWKKF.js").then((m) => ({ default: m.fr }))
|
|
3212
|
+
};
|
|
3213
|
+
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
3214
|
+
async function loadLocale(code) {
|
|
3215
|
+
const locale = resolveLocale(code);
|
|
3216
|
+
if (locale === "en" || loaded.has(locale)) {
|
|
3217
|
+
setLocale(locale);
|
|
3218
|
+
return locale;
|
|
3219
|
+
}
|
|
3220
|
+
try {
|
|
3221
|
+
const mod = await LOADERS[locale]();
|
|
3222
|
+
setLocale(locale, mod.default);
|
|
3223
|
+
loaded.add(locale);
|
|
3224
|
+
return locale;
|
|
3225
|
+
} catch {
|
|
3226
|
+
setLocale("en");
|
|
3227
|
+
return "en";
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
2360
3230
|
export {
|
|
2361
3231
|
ACCESSIBILITY_TYPES,
|
|
2362
3232
|
CHART_STORAGE_KEY,
|
|
3233
|
+
DEFAULT_CURRENCY,
|
|
2363
3234
|
PickerController,
|
|
3235
|
+
SUPPORTED_LOCALES,
|
|
2364
3236
|
SeatmapRenderer,
|
|
3237
|
+
UNGROUPED_ID,
|
|
2365
3238
|
accessibilityMeta,
|
|
3239
|
+
allObjects,
|
|
3240
|
+
applyHidden,
|
|
2366
3241
|
chartBounds,
|
|
3242
|
+
computeSections,
|
|
2367
3243
|
createRenderer,
|
|
3244
|
+
currencySymbol,
|
|
2368
3245
|
expandBooth,
|
|
2369
3246
|
expandChart,
|
|
2370
3247
|
expandRow,
|
|
2371
3248
|
expandRowSlots,
|
|
2372
3249
|
expandTable,
|
|
3250
|
+
floorObjects,
|
|
3251
|
+
floorsOf,
|
|
3252
|
+
formatDate,
|
|
3253
|
+
formatMoney,
|
|
3254
|
+
getLocale,
|
|
3255
|
+
hiddenObjectIds,
|
|
3256
|
+
isSectionHidden,
|
|
2373
3257
|
layerOf,
|
|
3258
|
+
loadLocale,
|
|
2374
3259
|
objectCenter,
|
|
2375
|
-
pointInPolygon
|
|
3260
|
+
pointInPolygon,
|
|
3261
|
+
resolveLocale,
|
|
3262
|
+
setLocale,
|
|
3263
|
+
setMoneyLocale,
|
|
3264
|
+
setStringOverrides,
|
|
3265
|
+
stackFloors,
|
|
3266
|
+
t,
|
|
3267
|
+
tCount
|
|
2376
3268
|
};
|
|
2377
3269
|
//# sourceMappingURL=index.js.map
|