@seatlayer/core 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2101 -298
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +652 -54
- package/dist/index.d.ts +652 -54
- package/dist/index.js +2089 -297
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var ACCESSIBILITY_TYPES = [
|
|
|
4
4
|
{ key: "companion", label: "Companion seat", short: "Companion", icon: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" },
|
|
5
5
|
{ key: "semi-ambulatory", label: "Semi-ambulatory (limited mobility)", short: "Limited mobility", icon: "\u{1F9AF}" },
|
|
6
6
|
{ key: "hearing", label: "Assistive listening", short: "Hearing", icon: "\u{1F9BB}" },
|
|
7
|
+
{ key: "cart", label: "CART live-caption view", short: "CART captions", icon: "CC" },
|
|
7
8
|
{ key: "sign-language", label: "Sign-language view", short: "Sign language", icon: "\u{1F91F}" },
|
|
8
9
|
{ key: "plus-size", label: "Plus-size seat", short: "Plus-size", icon: "\u{1F4BA}" },
|
|
9
10
|
{ key: "lift-armrest", label: "Lift-up armrest", short: "Lift armrest", icon: "\u2195\uFE0F" }
|
|
@@ -17,6 +18,7 @@ var ACCESSIBILITY_RING_COLOR = {
|
|
|
17
18
|
companion: "#8b5cf6",
|
|
18
19
|
"semi-ambulatory": "#0ea5e9",
|
|
19
20
|
hearing: "#14b8a6",
|
|
21
|
+
cart: "#7c3aed",
|
|
20
22
|
"sign-language": "#f59e0b",
|
|
21
23
|
"plus-size": "#ec4899",
|
|
22
24
|
"lift-armrest": "#22c55e"
|
|
@@ -27,6 +29,19 @@ function accessibilityRingColor(types) {
|
|
|
27
29
|
}
|
|
28
30
|
var LABEL_STYLE_MIN_SIZE = 8;
|
|
29
31
|
var LABEL_STYLE_MAX_SIZE = 24;
|
|
32
|
+
var SURROUNDINGS_SHAPE_ROLES = [
|
|
33
|
+
"reference-focal",
|
|
34
|
+
"bar",
|
|
35
|
+
"entrance",
|
|
36
|
+
"exit",
|
|
37
|
+
"restroom",
|
|
38
|
+
"screen",
|
|
39
|
+
"sound",
|
|
40
|
+
"concession",
|
|
41
|
+
"coat",
|
|
42
|
+
"wall"
|
|
43
|
+
];
|
|
44
|
+
var SURROUNDINGS_SHAPE_ROLE_SET = new Set(SURROUNDINGS_SHAPE_ROLES);
|
|
30
45
|
function layerOf(obj) {
|
|
31
46
|
switch (obj.type) {
|
|
32
47
|
case "row":
|
|
@@ -35,10 +50,17 @@ function layerOf(obj) {
|
|
|
35
50
|
case "booth":
|
|
36
51
|
case "section":
|
|
37
52
|
return "interactive";
|
|
38
|
-
//
|
|
53
|
+
// Raster décor follows its explicit authored z-layer. Absence retains the
|
|
54
|
+
// legacy Background default; bitmap content is never guessed semantically.
|
|
55
|
+
case "decorImage":
|
|
56
|
+
return obj.layer === "foreground" ? "foreground" : "background";
|
|
57
|
+
// Source-backed focal geometry and curated venue landmarks help an author
|
|
58
|
+
// orient around the sellable plan. A stage and an ordinary authored shape
|
|
59
|
+
// remain Background even when they carry an arbitrary/custom role.
|
|
39
60
|
case "shape":
|
|
61
|
+
return obj.role && SURROUNDINGS_SHAPE_ROLE_SET.has(obj.role) ? "surroundings" : "background";
|
|
62
|
+
// Free text — including semantic icon text — is background furniture.
|
|
40
63
|
case "text":
|
|
41
|
-
case "decorImage":
|
|
42
64
|
return "background";
|
|
43
65
|
default:
|
|
44
66
|
return "interactive";
|
|
@@ -158,10 +180,57 @@ function toRoman(value) {
|
|
|
158
180
|
return out;
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
// src/core/units.ts
|
|
184
|
+
var METRES_PER_CHART_UNIT = 0.55 / 24;
|
|
185
|
+
var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
|
|
186
|
+
var LIFT_PER_STEP_WORLD = 58;
|
|
187
|
+
var TIER_HEIGHT_M = LIFT_PER_STEP_WORLD * METRES_PER_CHART_UNIT;
|
|
188
|
+
var SECTION_ELEVATION_TIER_MIN = 0;
|
|
189
|
+
var SECTION_ELEVATION_TIER_MAX = 3;
|
|
190
|
+
var SECTION_HEIGHT_MIN_M = 0;
|
|
191
|
+
var SECTION_HEIGHT_MAX_M = 120;
|
|
192
|
+
var SECTION_RAKE_MIN_DEG = 0;
|
|
193
|
+
var SECTION_RAKE_MAX_DEG = 45;
|
|
194
|
+
var LEGACY_SECTION_ELEVATION_TIER_MAX = 7;
|
|
195
|
+
var SEATED_EYE_HEIGHT_M = 1.2;
|
|
196
|
+
function finiteClamped(value, min, max, fallback) {
|
|
197
|
+
return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
198
|
+
}
|
|
199
|
+
function sectionElevationTier(value) {
|
|
200
|
+
if (!Number.isFinite(value)) return SECTION_ELEVATION_TIER_MIN;
|
|
201
|
+
return Math.max(
|
|
202
|
+
SECTION_ELEVATION_TIER_MIN,
|
|
203
|
+
Math.min(SECTION_ELEVATION_TIER_MAX, Math.round(value))
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
function compatibleAutomaticTier(value) {
|
|
207
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) return 0;
|
|
208
|
+
if (value <= LEGACY_SECTION_ELEVATION_TIER_MAX) return value;
|
|
209
|
+
return SECTION_ELEVATION_TIER_MAX;
|
|
210
|
+
}
|
|
211
|
+
function sectionGeometry(section, context = {}) {
|
|
212
|
+
const floorBaseHeightM = finiteClamped(
|
|
213
|
+
context.floorBaseHeightM,
|
|
214
|
+
SECTION_HEIGHT_MIN_M,
|
|
215
|
+
SECTION_HEIGHT_MAX_M,
|
|
216
|
+
0
|
|
217
|
+
);
|
|
218
|
+
const automaticHeight = Math.min(
|
|
219
|
+
SECTION_HEIGHT_MAX_M,
|
|
220
|
+
floorBaseHeightM + compatibleAutomaticTier(section.elevation) * TIER_HEIGHT_M
|
|
221
|
+
);
|
|
222
|
+
const height = section.height === void 0 ? automaticHeight : finiteClamped(section.height, SECTION_HEIGHT_MIN_M, SECTION_HEIGHT_MAX_M, automaticHeight);
|
|
223
|
+
const rake = finiteClamped(section.rake, SECTION_RAKE_MIN_DEG, SECTION_RAKE_MAX_DEG, 0);
|
|
224
|
+
return { height, rake };
|
|
225
|
+
}
|
|
226
|
+
|
|
161
227
|
// src/core/layout.ts
|
|
162
228
|
function overrideAccessibility(o) {
|
|
163
229
|
if (!o) return [];
|
|
164
|
-
if (o.accessibility && o.accessibility.length)
|
|
230
|
+
if (o.accessibility && o.accessibility.length) {
|
|
231
|
+
return o.wheelchairSpaceType && !o.accessibility.includes("wheelchair") ? ["wheelchair", ...o.accessibility] : o.accessibility;
|
|
232
|
+
}
|
|
233
|
+
if (o.wheelchairSpaceType) return ["wheelchair"];
|
|
165
234
|
return o.accessible ? ["wheelchair"] : [];
|
|
166
235
|
}
|
|
167
236
|
var DEG = Math.PI / 180;
|
|
@@ -203,6 +272,10 @@ function overrideMap(row) {
|
|
|
203
272
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
204
273
|
return m;
|
|
205
274
|
}
|
|
275
|
+
function rowInventoryCount(row) {
|
|
276
|
+
const skipped = new Set((row.overrides ?? []).filter((override) => override.skip && Number.isInteger(override.index) && override.index >= 0 && override.index < row.seatCount).map((override) => override.index));
|
|
277
|
+
return Math.max(0, row.seatCount - skipped.size);
|
|
278
|
+
}
|
|
206
279
|
function centerRank(n) {
|
|
207
280
|
const rank = new Array(n);
|
|
208
281
|
Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
|
|
@@ -216,9 +289,9 @@ function seatLabelPart(row, i) {
|
|
|
216
289
|
const prefix = row.seatNumbering?.prefix ?? "";
|
|
217
290
|
const endAt = row.seatNumbering?.endAt;
|
|
218
291
|
const n = row.seatCount;
|
|
219
|
-
if (scheme === "updown") {
|
|
292
|
+
if (scheme === "updown" || scheme === "updown-descending") {
|
|
220
293
|
const half = Math.ceil(n / 2);
|
|
221
|
-
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
294
|
+
const core2 = scheme === "updown" ? i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i) : i < half ? rawStart + 2 * (half - 1 - i) : rawStart + 1 + 2 * (i - half);
|
|
222
295
|
return `${prefix}${core2}`;
|
|
223
296
|
}
|
|
224
297
|
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
@@ -271,6 +344,7 @@ function expandRowSlots(row) {
|
|
|
271
344
|
skipped: !!o?.skip,
|
|
272
345
|
accessible: accessibility.length > 0,
|
|
273
346
|
accessibility,
|
|
347
|
+
wheelchairSpaceType: o?.wheelchairSpaceType,
|
|
274
348
|
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
275
349
|
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl,
|
|
276
350
|
labelStyle: o?.labelStyle
|
|
@@ -291,6 +365,7 @@ function expandRow(row) {
|
|
|
291
365
|
categoryKey: slot.categoryKey,
|
|
292
366
|
accessible: slot.accessible || void 0,
|
|
293
367
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
368
|
+
wheelchairSpaceType: slot.wheelchairSpaceType,
|
|
294
369
|
commercial: slot.commercial,
|
|
295
370
|
viewUrl: slot.viewUrl,
|
|
296
371
|
labelStyle: slot.labelStyle
|
|
@@ -298,18 +373,43 @@ function expandRow(row) {
|
|
|
298
373
|
}
|
|
299
374
|
return seats;
|
|
300
375
|
}
|
|
301
|
-
function
|
|
376
|
+
function tableSeatCountsBySide(t2) {
|
|
377
|
+
if (t2.seatCountsBySide) return { ...t2.seatCountsBySide };
|
|
378
|
+
const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
|
|
379
|
+
const order = ["top", "bottom", "left", "right"].filter((side) => enabled.includes(side));
|
|
380
|
+
const counts = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
381
|
+
if (!order.length) return counts;
|
|
382
|
+
const n = Math.max(0, Math.round(t2.seatCount));
|
|
383
|
+
for (let index = 0; index < n; index++) counts[order[index % order.length]] += 1;
|
|
384
|
+
return counts;
|
|
385
|
+
}
|
|
386
|
+
function expandTableSlots(t2) {
|
|
302
387
|
const seats = [];
|
|
303
388
|
const n = Math.max(0, Math.round(t2.seatCount));
|
|
304
389
|
if (n === 0) return seats;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
390
|
+
const overrides2 = new Map((t2.overrides ?? []).map((override) => [override.index, override]));
|
|
391
|
+
const mk = (index, x, y, side) => {
|
|
392
|
+
const override = overrides2.get(index);
|
|
393
|
+
const accessibility = overrideAccessibility(override);
|
|
394
|
+
const label = override?.label ?? `${t2.label}-${index + 1}`;
|
|
395
|
+
const displayPrefix = t2.displayLabel ?? t2.label;
|
|
396
|
+
return {
|
|
397
|
+
index,
|
|
398
|
+
label,
|
|
399
|
+
displayLabel: override?.displayLabel ?? `${displayPrefix}-${index + 1}`,
|
|
400
|
+
x: x + (override?.dx ?? 0),
|
|
401
|
+
y: y + (override?.dy ?? 0),
|
|
402
|
+
categoryKey: override?.categoryKey ?? t2.categoryKey,
|
|
403
|
+
skipped: !!override?.skip,
|
|
404
|
+
accessible: accessibility.length > 0,
|
|
405
|
+
accessibility,
|
|
406
|
+
wheelchairSpaceType: override?.wheelchairSpaceType,
|
|
407
|
+
commercial: override?.commercial,
|
|
408
|
+
viewUrl: override?.viewFromSeatUrl,
|
|
409
|
+
labelStyle: override?.labelStyle,
|
|
410
|
+
...side ? { side } : {}
|
|
411
|
+
};
|
|
412
|
+
};
|
|
313
413
|
if (t2.shape === "round") {
|
|
314
414
|
const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
|
|
315
415
|
const base = t2.rotation * DEG;
|
|
@@ -332,17 +432,11 @@ function expandTable(t2) {
|
|
|
332
432
|
}
|
|
333
433
|
const w = t2.width ?? 80;
|
|
334
434
|
const h = t2.height ?? 50;
|
|
335
|
-
const
|
|
336
|
-
const order = ["top", "bottom", "left", "right"]
|
|
337
|
-
if (!order.length) return seats;
|
|
338
|
-
const counts = new Map(order.map((s) => [s, 0]));
|
|
339
|
-
for (let i = 0; i < n; i++) {
|
|
340
|
-
const s = order[i % order.length];
|
|
341
|
-
counts.set(s, counts.get(s) + 1);
|
|
342
|
-
}
|
|
435
|
+
const counts = tableSeatCountsBySide(t2);
|
|
436
|
+
const order = ["top", "bottom", "left", "right"];
|
|
343
437
|
let idx = 0;
|
|
344
438
|
for (const side of order) {
|
|
345
|
-
const count = counts
|
|
439
|
+
const count = counts[side];
|
|
346
440
|
for (let j = 0; j < count; j++) {
|
|
347
441
|
let localX;
|
|
348
442
|
let localY;
|
|
@@ -359,17 +453,39 @@ function expandTable(t2) {
|
|
|
359
453
|
localX = w / 2 + TABLE_SEAT_OFFSET;
|
|
360
454
|
localY = -h / 2 + (j + 0.5) * h / count;
|
|
361
455
|
}
|
|
362
|
-
const
|
|
363
|
-
seats.push(mk(idx++,
|
|
456
|
+
const point = place(localX, localY, t2.rotation, t2.center);
|
|
457
|
+
seats.push(mk(idx++, point.x, point.y, side));
|
|
364
458
|
}
|
|
365
459
|
}
|
|
366
460
|
return seats;
|
|
367
461
|
}
|
|
462
|
+
function tableInventoryCount(t2) {
|
|
463
|
+
if (t2.bookAsWhole || t2.variableOccupancy) return Math.max(0, Math.round(t2.seatCount));
|
|
464
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).length;
|
|
465
|
+
}
|
|
466
|
+
function expandTable(t2) {
|
|
467
|
+
return expandTableSlots(t2).filter((slot) => !slot.skipped).map((slot) => ({
|
|
468
|
+
id: `${t2.id}:${slot.index}`,
|
|
469
|
+
label: slot.label,
|
|
470
|
+
...slot.displayLabel !== slot.label ? { displayLabel: slot.displayLabel } : {},
|
|
471
|
+
x: slot.x,
|
|
472
|
+
y: slot.y,
|
|
473
|
+
rowId: t2.id,
|
|
474
|
+
categoryKey: slot.categoryKey,
|
|
475
|
+
...slot.accessible ? { accessible: true } : {},
|
|
476
|
+
...slot.accessibility.length ? { accessibility: slot.accessibility } : {},
|
|
477
|
+
...slot.wheelchairSpaceType ? { wheelchairSpaceType: slot.wheelchairSpaceType } : {},
|
|
478
|
+
...slot.commercial ? { commercial: slot.commercial } : {},
|
|
479
|
+
...slot.viewUrl ? { viewUrl: slot.viewUrl } : {},
|
|
480
|
+
...slot.labelStyle ? { labelStyle: slot.labelStyle } : {}
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
368
483
|
function expandBooth(b) {
|
|
369
484
|
return [
|
|
370
485
|
{
|
|
371
486
|
id: `${b.id}:0`,
|
|
372
487
|
label: b.label,
|
|
488
|
+
...b.displayLabel && b.displayLabel !== b.label ? { displayLabel: b.displayLabel } : {},
|
|
373
489
|
x: b.center.x,
|
|
374
490
|
y: b.center.y,
|
|
375
491
|
rowId: b.id,
|
|
@@ -407,7 +523,7 @@ function polygonLabelPoint(outer, holes) {
|
|
|
407
523
|
if (!outer.length) return { x: 0, y: 0 };
|
|
408
524
|
const xs = outer.map((point) => point.x);
|
|
409
525
|
const ys = outer.map((point) => point.y);
|
|
410
|
-
const
|
|
526
|
+
const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
411
527
|
const centroid2 = polygonCentroid(outer);
|
|
412
528
|
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
413
529
|
let best = outer[0];
|
|
@@ -416,8 +532,8 @@ function polygonLabelPoint(outer, holes) {
|
|
|
416
532
|
for (let row = 1; row < 24; row += 1) {
|
|
417
533
|
for (let column = 1; column < 24; column += 1) {
|
|
418
534
|
const point = {
|
|
419
|
-
x:
|
|
420
|
-
y:
|
|
535
|
+
x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
|
|
536
|
+
y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
|
|
421
537
|
};
|
|
422
538
|
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
423
539
|
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
@@ -479,9 +595,32 @@ function objectCenter(o) {
|
|
|
479
595
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
480
596
|
}
|
|
481
597
|
}
|
|
598
|
+
function samePoints(left, right) {
|
|
599
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
600
|
+
}
|
|
601
|
+
function sameGASurfaceAsSection(object, section) {
|
|
602
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
603
|
+
const objectHoles = object.holes ?? [];
|
|
604
|
+
const sectionHoles = section.holes ?? [];
|
|
605
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
606
|
+
}
|
|
607
|
+
function owningSectionForObject(objects, object) {
|
|
608
|
+
const sections = objects.filter((candidate) => candidate.type === "section");
|
|
609
|
+
const referencedLogicalId = "referenceInventorySource" in object ? object.referenceInventorySource?.logicalSectionId : void 0;
|
|
610
|
+
const center = objectCenter(object);
|
|
611
|
+
const referencedOwner = referencedLogicalId ? sections.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(object, section) || pointInPolygonWithHoles(center, section.outline, section.holes))) : void 0;
|
|
612
|
+
return referencedOwner ?? sections.find((section) => pointInPolygonWithHoles(center, section.outline, section.holes));
|
|
613
|
+
}
|
|
482
614
|
function floorsOf(doc) {
|
|
483
615
|
if (doc.floors && doc.floors.length) return doc.floors;
|
|
484
|
-
return [{
|
|
616
|
+
return [{
|
|
617
|
+
id: "floor-0",
|
|
618
|
+
name: "Main",
|
|
619
|
+
objects: doc.objects,
|
|
620
|
+
focalPoint: doc.focalPoint,
|
|
621
|
+
referenceImage: doc.referenceImage,
|
|
622
|
+
backgroundImage: doc.backgroundImage
|
|
623
|
+
}];
|
|
485
624
|
}
|
|
486
625
|
function floorObjects(doc, floorId) {
|
|
487
626
|
const floors = floorsOf(doc);
|
|
@@ -530,15 +669,138 @@ function stackFloors(doc, spread = 900) {
|
|
|
530
669
|
});
|
|
531
670
|
return { ...doc, objects, floors: void 0 };
|
|
532
671
|
}
|
|
533
|
-
function
|
|
672
|
+
function expandFloorObjects(objects, zones, fallbackFocal) {
|
|
534
673
|
const out = [];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
674
|
+
const segmented = /* @__PURE__ */ new Map();
|
|
675
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const object of objects) {
|
|
677
|
+
if (object.type !== "row" || !object.segmentedRow) continue;
|
|
678
|
+
const list = grouped.get(object.segmentedRow.groupId) ?? [];
|
|
679
|
+
list.push(object);
|
|
680
|
+
grouped.set(object.segmentedRow.groupId, list);
|
|
681
|
+
}
|
|
682
|
+
for (const [groupId, members] of grouped) {
|
|
683
|
+
const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
|
|
684
|
+
const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
|
|
685
|
+
const first = ordered[0]?.segmentedRow;
|
|
686
|
+
if (!first) continue;
|
|
687
|
+
const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
|
|
688
|
+
if (!valid) continue;
|
|
689
|
+
const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
|
|
690
|
+
let adjacencyOffset = 0;
|
|
691
|
+
let displayOffset = 0;
|
|
692
|
+
for (const row of ordered) {
|
|
693
|
+
if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
|
|
694
|
+
segmented.set(row.id, {
|
|
695
|
+
groupId,
|
|
696
|
+
adjacencyOffset,
|
|
697
|
+
displayOffset,
|
|
698
|
+
displayLabel: first.displayLabel,
|
|
699
|
+
totalSeats,
|
|
700
|
+
canonical: ordered[0],
|
|
701
|
+
viewFromSeatUrl: first.viewFromSeatUrl
|
|
702
|
+
});
|
|
703
|
+
adjacencyOffset += row.seatCount;
|
|
704
|
+
displayOffset += row.seatCount;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const obj of objects) {
|
|
708
|
+
let seats = [];
|
|
709
|
+
if (obj.type === "row") seats = expandRow(obj);
|
|
710
|
+
else if (obj.type === "table") seats = expandTable(obj);
|
|
711
|
+
else if (obj.type === "booth") seats = expandBooth(obj);
|
|
712
|
+
if (!seats.length) continue;
|
|
713
|
+
if (obj.type === "row") {
|
|
714
|
+
const logical = segmented.get(obj.id);
|
|
715
|
+
if (logical) {
|
|
716
|
+
const overrides2 = new Map((obj.overrides ?? []).map((override) => [override.index, override]));
|
|
717
|
+
for (const seat of seats) {
|
|
718
|
+
const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
|
|
719
|
+
if (!Number.isInteger(physicalIndex)) continue;
|
|
720
|
+
const displayOrdinal = logical.displayOffset + physicalIndex;
|
|
721
|
+
seat.logicalRowId = logical.groupId;
|
|
722
|
+
seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
|
|
723
|
+
if (!overrides2.get(physicalIndex)?.displayLabel) {
|
|
724
|
+
const numberingRow = {
|
|
725
|
+
...logical.canonical,
|
|
726
|
+
seatCount: logical.totalSeats,
|
|
727
|
+
label: logical.displayLabel,
|
|
728
|
+
displayLabel: logical.displayLabel
|
|
729
|
+
};
|
|
730
|
+
seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
|
|
731
|
+
}
|
|
732
|
+
seat.viewUrl ??= logical.viewFromSeatUrl;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const owner = owningSectionForObject(objects, obj);
|
|
737
|
+
const inheritedView = owner?.viewFromSeatUrl;
|
|
738
|
+
const zone = owner?.zone ? zones?.find((candidate) => candidate.id === owner.zone) : void 0;
|
|
739
|
+
const resolvedFocal = zone?.focalPoint ?? fallbackFocal;
|
|
740
|
+
for (const seat of seats) {
|
|
741
|
+
if (inheritedView) seat.viewUrl ??= inheritedView;
|
|
742
|
+
if (owner) seat.sectionId = owner.logicalSectionId ?? owner.id;
|
|
743
|
+
if (owner?.zone) seat.zoneId = owner.zone;
|
|
744
|
+
if (resolvedFocal) seat.focalPoint = { ...resolvedFocal };
|
|
745
|
+
}
|
|
746
|
+
out.push(...seats);
|
|
539
747
|
}
|
|
540
748
|
return out;
|
|
541
749
|
}
|
|
750
|
+
function expandChart(doc, options = {}) {
|
|
751
|
+
if (doc.floors?.length) {
|
|
752
|
+
const out2 = [];
|
|
753
|
+
for (const floor of doc.floors) {
|
|
754
|
+
const floorFocal = floor.focalPoint ?? doc.focalPoint;
|
|
755
|
+
const seats = expandFloorObjects(floor.objects, doc.zones, floorFocal);
|
|
756
|
+
assignEyeHeights(floor.objects, floor.focalPoint ?? doc.focalPoint, floor.baseHeightM ?? 0, seats);
|
|
757
|
+
out2.push(...seats);
|
|
758
|
+
}
|
|
759
|
+
return out2;
|
|
760
|
+
}
|
|
761
|
+
const out = expandFloorObjects(doc.objects, doc.zones, doc.focalPoint);
|
|
762
|
+
assignEyeHeights(doc.objects, doc.focalPoint, options.floorBaseHeightM ?? 0, out);
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
function assignEyeHeights(objects, focal, floorBaseHeightM, seats) {
|
|
766
|
+
const sections = objects.filter((o) => o.type === "section");
|
|
767
|
+
const hasGeometry = floorBaseHeightM > 0 || sections.some((s) => s.height !== void 0 || s.rake !== void 0 || (s.elevation ?? 0) > 0);
|
|
768
|
+
if (!sections.length || !hasGeometry || !focal) {
|
|
769
|
+
for (const seat of seats) seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const owner = new Array(seats.length);
|
|
773
|
+
const geo = /* @__PURE__ */ new Map();
|
|
774
|
+
const frontDistU = /* @__PURE__ */ new Map();
|
|
775
|
+
for (let i = 0; i < seats.length; i++) {
|
|
776
|
+
const seat = seats[i];
|
|
777
|
+
const sec = sections.find((s) => pointInPolygonWithHoles({ x: seat.x, y: seat.y }, s.outline, s.holes)) ?? null;
|
|
778
|
+
owner[i] = sec;
|
|
779
|
+
if (!sec) continue;
|
|
780
|
+
if (!geo.has(sec.id)) geo.set(sec.id, sectionGeometry(sec, { floorBaseHeightM }));
|
|
781
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
782
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
783
|
+
const cur = frontDistU.get(sec.id);
|
|
784
|
+
if (cur === void 0 || d < cur) frontDistU.set(sec.id, d);
|
|
785
|
+
}
|
|
786
|
+
for (let i = 0; i < seats.length; i++) {
|
|
787
|
+
const seat = seats[i];
|
|
788
|
+
const sec = owner[i];
|
|
789
|
+
if (!sec) {
|
|
790
|
+
seat.eyeHeightM = floorBaseHeightM + SEATED_EYE_HEIGHT_M;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const g = geo.get(sec.id);
|
|
794
|
+
let riseM = 0;
|
|
795
|
+
if (g.rake > 0) {
|
|
796
|
+
const seatFocal = seat.focalPoint ?? focal;
|
|
797
|
+
const d = Math.hypot(seat.x - seatFocal.x, seat.y - seatFocal.y);
|
|
798
|
+
const depthU = Math.max(0, d - (frontDistU.get(sec.id) ?? d));
|
|
799
|
+
riseM = depthU * METRES_PER_CHART_UNIT * Math.tan(g.rake * Math.PI / 180);
|
|
800
|
+
}
|
|
801
|
+
seat.eyeHeightM = g.height + riseM + SEATED_EYE_HEIGHT_M;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
542
804
|
var PAD = 40;
|
|
543
805
|
function chartBounds(doc) {
|
|
544
806
|
let minX = Infinity;
|
|
@@ -582,8 +844,9 @@ function chartBounds(doc) {
|
|
|
582
844
|
acc(obj.position.x + w, obj.position.y + obj.fontSize);
|
|
583
845
|
}
|
|
584
846
|
}
|
|
585
|
-
|
|
586
|
-
|
|
847
|
+
for (const image of [doc.referenceImage, doc.backgroundImage]) {
|
|
848
|
+
if (!image) continue;
|
|
849
|
+
const { center, width } = image;
|
|
587
850
|
const bh = width * 3 / 4;
|
|
588
851
|
acc(center.x - width / 2, center.y - bh / 2);
|
|
589
852
|
acc(center.x + width / 2, center.y + bh / 2);
|
|
@@ -607,9 +870,55 @@ var MAX_EVENT_INVENTORY = 1e5;
|
|
|
607
870
|
function gaUnitLabel(areaId, index) {
|
|
608
871
|
return `${PREFIX}${encodeURIComponent(areaId)}__${index + 1}`;
|
|
609
872
|
}
|
|
610
|
-
function
|
|
873
|
+
function validGAInventorySegments(area) {
|
|
874
|
+
const segments = area.inventorySegments;
|
|
875
|
+
if (segments === void 0) return true;
|
|
876
|
+
if (!Array.isArray(segments) || segments.length < 1 || segments.length > 256) return false;
|
|
877
|
+
let total = 0;
|
|
878
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
879
|
+
for (const segment of segments) {
|
|
880
|
+
if (!segment || typeof segment.sourceAreaId !== "string" || !segment.sourceAreaId.trim() || segment.sourceAreaId.length > 160 || !Number.isInteger(segment.startIndex) || segment.startIndex < 0 || !Number.isInteger(segment.count) || segment.count < 1 || segment.startIndex + segment.count > MAX_GA_CAPACITY) return false;
|
|
881
|
+
total += segment.count;
|
|
882
|
+
if (total > MAX_GA_CAPACITY) return false;
|
|
883
|
+
const sourceRanges = ranges.get(segment.sourceAreaId) ?? [];
|
|
884
|
+
const end = segment.startIndex + segment.count;
|
|
885
|
+
if (sourceRanges.some((range) => segment.startIndex < range.end && end > range.start)) return false;
|
|
886
|
+
sourceRanges.push({ start: segment.startIndex, end });
|
|
887
|
+
ranges.set(segment.sourceAreaId, sourceRanges);
|
|
888
|
+
}
|
|
889
|
+
return total === area.capacity;
|
|
890
|
+
}
|
|
891
|
+
function gaInventorySegments(area) {
|
|
611
892
|
const capacity = Math.min(MAX_GA_CAPACITY, Math.max(0, Math.floor(area.capacity)));
|
|
612
|
-
|
|
893
|
+
if (area.inventorySegments === void 0) {
|
|
894
|
+
return capacity ? [{ sourceAreaId: area.id, startIndex: 0, count: capacity }] : [];
|
|
895
|
+
}
|
|
896
|
+
if (!validGAInventorySegments(area)) return [];
|
|
897
|
+
return area.inventorySegments.map((segment) => ({ ...segment }));
|
|
898
|
+
}
|
|
899
|
+
function gaUnitLabels(area) {
|
|
900
|
+
return gaInventorySegments(area).flatMap((segment) => Array.from(
|
|
901
|
+
{ length: segment.count },
|
|
902
|
+
(_, offset) => gaUnitLabel(segment.sourceAreaId, segment.startIndex + offset)
|
|
903
|
+
));
|
|
904
|
+
}
|
|
905
|
+
function growJoinedGAInventory(area, nextCapacity) {
|
|
906
|
+
if (area.inventorySegments === void 0 || nextCapacity === area.capacity) return area.inventorySegments;
|
|
907
|
+
if (nextCapacity < area.capacity) return void 0;
|
|
908
|
+
const segments = gaInventorySegments(area);
|
|
909
|
+
if (!segments.length) return void 0;
|
|
910
|
+
const extra = nextCapacity - area.capacity;
|
|
911
|
+
if (!extra) return segments;
|
|
912
|
+
const ownRanges = segments.filter((segment) => segment.sourceAreaId === area.id);
|
|
913
|
+
const startIndex = ownRanges.reduce((max, segment) => Math.max(max, segment.startIndex + segment.count), 0);
|
|
914
|
+
const last = segments[segments.length - 1];
|
|
915
|
+
if (last?.sourceAreaId === area.id && last.startIndex + last.count === startIndex) {
|
|
916
|
+
last.count += extra;
|
|
917
|
+
} else {
|
|
918
|
+
if (segments.length >= 256) return void 0;
|
|
919
|
+
segments.push({ sourceAreaId: area.id, startIndex, count: extra });
|
|
920
|
+
}
|
|
921
|
+
return segments;
|
|
613
922
|
}
|
|
614
923
|
function gaAreasOf(doc) {
|
|
615
924
|
return allObjects(doc).filter((o) => o.type === "gaArea");
|
|
@@ -630,15 +939,6 @@ function objectSeatLabels(o) {
|
|
|
630
939
|
function isSeatObject(o) {
|
|
631
940
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
632
941
|
}
|
|
633
|
-
function samePoints(left, right) {
|
|
634
|
-
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
635
|
-
}
|
|
636
|
-
function sameGASurfaceAsSection(object, section) {
|
|
637
|
-
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
638
|
-
const objectHoles = object.holes ?? [];
|
|
639
|
-
const sectionHoles = section.holes ?? [];
|
|
640
|
-
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
641
|
-
}
|
|
642
942
|
function computeSections(doc) {
|
|
643
943
|
const objs = allObjects(doc);
|
|
644
944
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
@@ -664,10 +964,7 @@ function computeSections(doc) {
|
|
|
664
964
|
if (!isSeatObject(obj)) continue;
|
|
665
965
|
const labels = objectSeatLabels(obj);
|
|
666
966
|
if (labels.length === 0) continue;
|
|
667
|
-
const
|
|
668
|
-
const c = objectCenter(obj);
|
|
669
|
-
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
670
|
-
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
967
|
+
const owner = owningSectionForObject(objs, obj);
|
|
671
968
|
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
672
969
|
node.seatCount += labels.length;
|
|
673
970
|
node.objectIds.push(obj.id);
|
|
@@ -1238,11 +1535,310 @@ import { Circle } from "konva/lib/shapes/Circle";
|
|
|
1238
1535
|
import { Rect } from "konva/lib/shapes/Rect";
|
|
1239
1536
|
import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
1240
1537
|
import { Line } from "konva/lib/shapes/Line";
|
|
1538
|
+
import { Arrow } from "konva/lib/shapes/Arrow";
|
|
1241
1539
|
import { Text } from "konva/lib/shapes/Text";
|
|
1242
1540
|
import { Path } from "konva/lib/shapes/Path";
|
|
1243
1541
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
1244
1542
|
import { Shape } from "konva/lib/Shape";
|
|
1245
1543
|
|
|
1544
|
+
// src/core/chartBackgrounds.ts
|
|
1545
|
+
function buyerBackgroundImage(owner) {
|
|
1546
|
+
const background = owner.backgroundImage;
|
|
1547
|
+
return background?.url && !background.assetId ? background : void 0;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/core/spatialIndex.ts
|
|
1551
|
+
var DEFAULT_SEAT_RADIUS = 9;
|
|
1552
|
+
function halfExtents(shape) {
|
|
1553
|
+
if (shape.kind === "circle") return { hx: shape.r, hy: shape.r };
|
|
1554
|
+
const hw = shape.width / 2;
|
|
1555
|
+
const hh = shape.height / 2;
|
|
1556
|
+
const rot = shape.rotation ?? 0;
|
|
1557
|
+
if (!rot) return { hx: hw, hy: hh };
|
|
1558
|
+
const rad = rot * Math.PI / 180;
|
|
1559
|
+
const c = Math.abs(Math.cos(rad));
|
|
1560
|
+
const s = Math.abs(Math.sin(rad));
|
|
1561
|
+
return { hx: hw * c + hh * s, hy: hw * s + hh * c };
|
|
1562
|
+
}
|
|
1563
|
+
function shapeIntersectsRect(e, x0, y0, x1, y1) {
|
|
1564
|
+
if (e.shape.kind === "circle") {
|
|
1565
|
+
const cx = Math.min(Math.max(e.seat.x, x0), x1);
|
|
1566
|
+
const cy = Math.min(Math.max(e.seat.y, y0), y1);
|
|
1567
|
+
const dx = e.seat.x - cx;
|
|
1568
|
+
const dy = e.seat.y - cy;
|
|
1569
|
+
return dx * dx + dy * dy <= e.shape.r * e.shape.r;
|
|
1570
|
+
}
|
|
1571
|
+
const rot = e.shape.rotation ?? 0;
|
|
1572
|
+
if (!rot) {
|
|
1573
|
+
return e.seat.x - e.hx <= x1 && e.seat.x + e.hx >= x0 && e.seat.y - e.hy <= y1 && e.seat.y + e.hy >= y0;
|
|
1574
|
+
}
|
|
1575
|
+
const rad = rot * Math.PI / 180;
|
|
1576
|
+
const c = Math.cos(rad);
|
|
1577
|
+
const s = Math.sin(rad);
|
|
1578
|
+
const hw = e.shape.width / 2;
|
|
1579
|
+
const hh = e.shape.height / 2;
|
|
1580
|
+
const corners = [
|
|
1581
|
+
[-hw, -hh],
|
|
1582
|
+
[hw, -hh],
|
|
1583
|
+
[hw, hh],
|
|
1584
|
+
[-hw, hh]
|
|
1585
|
+
].map(([lx, ly]) => [e.seat.x + lx * c - ly * s, e.seat.y + lx * s + ly * c]);
|
|
1586
|
+
const query = [
|
|
1587
|
+
[x0, y0],
|
|
1588
|
+
[x1, y0],
|
|
1589
|
+
[x1, y1],
|
|
1590
|
+
[x0, y1]
|
|
1591
|
+
];
|
|
1592
|
+
const axes = [
|
|
1593
|
+
[1, 0],
|
|
1594
|
+
[0, 1],
|
|
1595
|
+
[c, s],
|
|
1596
|
+
[-s, c]
|
|
1597
|
+
];
|
|
1598
|
+
for (const [ax, ay] of axes) {
|
|
1599
|
+
let minA = Infinity;
|
|
1600
|
+
let maxA = -Infinity;
|
|
1601
|
+
for (const [px, py] of corners) {
|
|
1602
|
+
const d = px * ax + py * ay;
|
|
1603
|
+
if (d < minA) minA = d;
|
|
1604
|
+
if (d > maxA) maxA = d;
|
|
1605
|
+
}
|
|
1606
|
+
let minB = Infinity;
|
|
1607
|
+
let maxB = -Infinity;
|
|
1608
|
+
for (const [px, py] of query) {
|
|
1609
|
+
const d = px * ax + py * ay;
|
|
1610
|
+
if (d < minB) minB = d;
|
|
1611
|
+
if (d > maxB) maxB = d;
|
|
1612
|
+
}
|
|
1613
|
+
if (maxA < minB || maxB < minA) return false;
|
|
1614
|
+
}
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
function buildSeatIndex(seats, opts = {}) {
|
|
1618
|
+
const seatRadius = opts.seatRadius ?? DEFAULT_SEAT_RADIUS;
|
|
1619
|
+
const n = seats.length;
|
|
1620
|
+
const entries = new Array(n);
|
|
1621
|
+
let minX = Infinity;
|
|
1622
|
+
let minY = Infinity;
|
|
1623
|
+
let maxX = -Infinity;
|
|
1624
|
+
let maxY = -Infinity;
|
|
1625
|
+
let maxHalf = 0;
|
|
1626
|
+
for (let i = 0; i < n; i++) {
|
|
1627
|
+
const seat = seats[i];
|
|
1628
|
+
const shape = opts.shapeOf?.(seat) ?? { kind: "circle", r: seatRadius };
|
|
1629
|
+
const { hx, hy } = halfExtents(shape);
|
|
1630
|
+
entries[i] = { seat, shape, hx, hy };
|
|
1631
|
+
if (seat.x - hx < minX) minX = seat.x - hx;
|
|
1632
|
+
if (seat.y - hy < minY) minY = seat.y - hy;
|
|
1633
|
+
if (seat.x + hx > maxX) maxX = seat.x + hx;
|
|
1634
|
+
if (seat.y + hy > maxY) maxY = seat.y + hy;
|
|
1635
|
+
if (hx > maxHalf) maxHalf = hx;
|
|
1636
|
+
if (hy > maxHalf) maxHalf = hy;
|
|
1637
|
+
}
|
|
1638
|
+
if (n === 0) {
|
|
1639
|
+
minX = 0;
|
|
1640
|
+
minY = 0;
|
|
1641
|
+
maxX = 0;
|
|
1642
|
+
maxY = 0;
|
|
1643
|
+
}
|
|
1644
|
+
const nominal = maxHalf > 0 ? maxHalf * 4 : 1;
|
|
1645
|
+
const spanX = Math.max(maxX - minX, 0);
|
|
1646
|
+
const spanY = Math.max(maxY - minY, 0);
|
|
1647
|
+
const maxCells = Math.max(64, opts.maxCells ?? n * 4);
|
|
1648
|
+
let cell = nominal;
|
|
1649
|
+
let cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1650
|
+
let rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1651
|
+
while (cols * rows > maxCells && (cols > 1 || rows > 1)) {
|
|
1652
|
+
cell *= 2;
|
|
1653
|
+
cols = Math.max(1, Math.ceil(spanX / cell) || 1);
|
|
1654
|
+
rows = Math.max(1, Math.ceil(spanY / cell) || 1);
|
|
1655
|
+
}
|
|
1656
|
+
const cellCount = cols * rows;
|
|
1657
|
+
const cellStart = new Int32Array(cellCount + 1);
|
|
1658
|
+
let total = 0;
|
|
1659
|
+
for (let i = 0; i < n; i++) {
|
|
1660
|
+
const e = entries[i];
|
|
1661
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1662
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1663
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1664
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1665
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1666
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1667
|
+
cellStart[cy * cols + cx + 1]++;
|
|
1668
|
+
total++;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
for (let c = 0; c < cellCount; c++) cellStart[c + 1] += cellStart[c];
|
|
1673
|
+
const items = new Int32Array(total);
|
|
1674
|
+
const cursor = cellStart.slice(0, cellCount);
|
|
1675
|
+
for (let i = 0; i < n; i++) {
|
|
1676
|
+
const e = entries[i];
|
|
1677
|
+
const cx0 = clampInt(Math.floor((e.seat.x - e.hx - minX) / cell), 0, cols - 1);
|
|
1678
|
+
const cx1 = clampInt(Math.floor((e.seat.x + e.hx - minX) / cell), 0, cols - 1);
|
|
1679
|
+
const cy0 = clampInt(Math.floor((e.seat.y - e.hy - minY) / cell), 0, rows - 1);
|
|
1680
|
+
const cy1 = clampInt(Math.floor((e.seat.y + e.hy - minY) / cell), 0, rows - 1);
|
|
1681
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1682
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1683
|
+
const c = cy * cols + cx;
|
|
1684
|
+
items[cursor[c]++] = i;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
seats,
|
|
1690
|
+
entries,
|
|
1691
|
+
minX,
|
|
1692
|
+
minY,
|
|
1693
|
+
cell,
|
|
1694
|
+
cols,
|
|
1695
|
+
rows,
|
|
1696
|
+
cellStart,
|
|
1697
|
+
items,
|
|
1698
|
+
stamp: new Int32Array(n),
|
|
1699
|
+
epoch: 0
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function clampInt(v, lo, hi) {
|
|
1703
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
1704
|
+
}
|
|
1705
|
+
function queryRect(index, rect, opts = {}) {
|
|
1706
|
+
const x0 = Math.min(rect.x, rect.x + rect.width);
|
|
1707
|
+
const x1 = Math.max(rect.x, rect.x + rect.width);
|
|
1708
|
+
const y0 = Math.min(rect.y, rect.y + rect.height);
|
|
1709
|
+
const y1 = Math.max(rect.y, rect.y + rect.height);
|
|
1710
|
+
const mode = opts.mode ?? "center";
|
|
1711
|
+
const { cols, rows, cell, minX, minY, cellStart, items, entries } = index;
|
|
1712
|
+
const cx0 = Math.max(0, Math.floor((x0 - minX) / cell));
|
|
1713
|
+
const cx1 = Math.min(cols - 1, Math.floor((x1 - minX) / cell));
|
|
1714
|
+
const cy0 = Math.max(0, Math.floor((y0 - minY) / cell));
|
|
1715
|
+
const cy1 = Math.min(rows - 1, Math.floor((y1 - minY) / cell));
|
|
1716
|
+
if (cx0 > cx1 || cy0 > cy1) return [];
|
|
1717
|
+
const found = [];
|
|
1718
|
+
const stamp = index.stamp;
|
|
1719
|
+
const ep = ++index.epoch;
|
|
1720
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
1721
|
+
const rowBase = cy * cols;
|
|
1722
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
1723
|
+
const c = rowBase + cx;
|
|
1724
|
+
const end = cellStart[c + 1];
|
|
1725
|
+
for (let k = cellStart[c]; k < end; k++) {
|
|
1726
|
+
const i = items[k];
|
|
1727
|
+
if (stamp[i] === ep) continue;
|
|
1728
|
+
stamp[i] = ep;
|
|
1729
|
+
const e = entries[i];
|
|
1730
|
+
if (opts.filter && !opts.filter(e.seat.id, e.seat)) continue;
|
|
1731
|
+
const ok = mode === "center" ? e.seat.x >= x0 && e.seat.x <= x1 && e.seat.y >= y0 && e.seat.y <= y1 : shapeIntersectsRect(e, x0, y0, x1, y1);
|
|
1732
|
+
if (ok) found.push(i);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
found.sort((a, b) => a - b);
|
|
1737
|
+
return found.map((i) => entries[i].seat.id);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/core/perspectiveProjection.ts
|
|
1741
|
+
function createPerspectiveCamera(bounds2, maxSurfaceHeightWorld = 0) {
|
|
1742
|
+
const span = Math.max(1, bounds2.width, bounds2.height);
|
|
1743
|
+
const target = {
|
|
1744
|
+
x: bounds2.x + bounds2.width / 2,
|
|
1745
|
+
y: bounds2.y + bounds2.height / 2
|
|
1746
|
+
};
|
|
1747
|
+
const distance = span * 1.55;
|
|
1748
|
+
const height = Math.max(span * 0.82, Math.max(0, maxSurfaceHeightWorld) + span * 0.45);
|
|
1749
|
+
return {
|
|
1750
|
+
target,
|
|
1751
|
+
distance,
|
|
1752
|
+
height,
|
|
1753
|
+
focalLength: Math.hypot(distance, height),
|
|
1754
|
+
yawRad: -8 * Math.PI / 180
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function projectPerspectivePoint(camera, point, heightWorld = 0) {
|
|
1758
|
+
const dx = point.x - camera.target.x;
|
|
1759
|
+
const dy = point.y - camera.target.y;
|
|
1760
|
+
const cos = Math.cos(camera.yawRad);
|
|
1761
|
+
const sin = Math.sin(camera.yawRad);
|
|
1762
|
+
const x = dx * cos - dy * sin;
|
|
1763
|
+
const y = dx * sin + dy * cos;
|
|
1764
|
+
const translatedY = y - camera.distance;
|
|
1765
|
+
const translatedZ = heightWorld - camera.height;
|
|
1766
|
+
const cameraLength = Math.hypot(camera.distance, camera.height);
|
|
1767
|
+
const depth = (-camera.distance * translatedY - camera.height * translatedZ) / cameraLength;
|
|
1768
|
+
const up = (-camera.height * translatedY + camera.distance * translatedZ) / cameraLength;
|
|
1769
|
+
const safeDepth = Math.max(cameraLength * 0.08, depth);
|
|
1770
|
+
const scale = camera.focalLength / safeDepth;
|
|
1771
|
+
return {
|
|
1772
|
+
x: camera.target.x + x * scale,
|
|
1773
|
+
y: camera.target.y - up * scale,
|
|
1774
|
+
depth: safeDepth,
|
|
1775
|
+
scale
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
function applyAffine(affine, point) {
|
|
1779
|
+
return {
|
|
1780
|
+
x: affine.a * point.x + affine.c * point.y + affine.e,
|
|
1781
|
+
y: affine.b * point.x + affine.d * point.y + affine.f
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function invertAffine(affine) {
|
|
1785
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
1786
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-9) {
|
|
1787
|
+
throw new Error("perspective projection produced a singular affine");
|
|
1788
|
+
}
|
|
1789
|
+
const a = affine.d / determinant;
|
|
1790
|
+
const b = -affine.b / determinant;
|
|
1791
|
+
const c = -affine.c / determinant;
|
|
1792
|
+
const d = affine.a / determinant;
|
|
1793
|
+
return {
|
|
1794
|
+
a,
|
|
1795
|
+
b,
|
|
1796
|
+
c,
|
|
1797
|
+
d,
|
|
1798
|
+
e: -(a * affine.e + c * affine.f),
|
|
1799
|
+
f: -(b * affine.e + d * affine.f)
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function composeAffine(left, right) {
|
|
1803
|
+
return {
|
|
1804
|
+
a: left.a * right.a + left.c * right.b,
|
|
1805
|
+
b: left.b * right.a + left.d * right.b,
|
|
1806
|
+
c: left.a * right.c + left.c * right.d,
|
|
1807
|
+
d: left.b * right.c + left.d * right.d,
|
|
1808
|
+
e: left.a * right.e + left.c * right.f + left.e,
|
|
1809
|
+
f: left.b * right.e + left.d * right.f + left.f
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
function perspectiveTangentAffine(camera, anchor, heightAt = () => 0) {
|
|
1813
|
+
const origin = projectPerspectivePoint(camera, anchor, heightAt(anchor));
|
|
1814
|
+
const alongXPoint = { x: anchor.x + 1, y: anchor.y };
|
|
1815
|
+
const alongYPoint = { x: anchor.x, y: anchor.y + 1 };
|
|
1816
|
+
const alongX = projectPerspectivePoint(camera, alongXPoint, heightAt(alongXPoint));
|
|
1817
|
+
const alongY = projectPerspectivePoint(camera, alongYPoint, heightAt(alongYPoint));
|
|
1818
|
+
const a = alongX.x - origin.x;
|
|
1819
|
+
const b = alongX.y - origin.y;
|
|
1820
|
+
const c = alongY.x - origin.x;
|
|
1821
|
+
const d = alongY.y - origin.y;
|
|
1822
|
+
return {
|
|
1823
|
+
a,
|
|
1824
|
+
b,
|
|
1825
|
+
c,
|
|
1826
|
+
d,
|
|
1827
|
+
e: origin.x - a * anchor.x - c * anchor.y,
|
|
1828
|
+
f: origin.y - b * anchor.x - d * anchor.y
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function decomposeAffineLinear(affine) {
|
|
1832
|
+
const scaleX = Math.hypot(affine.a, affine.b);
|
|
1833
|
+
const determinant = affine.a * affine.d - affine.b * affine.c;
|
|
1834
|
+
return {
|
|
1835
|
+
rotationDeg: Math.atan2(affine.b, affine.a) * 180 / Math.PI,
|
|
1836
|
+
scaleX,
|
|
1837
|
+
scaleY: determinant / Math.max(scaleX, 1e-9),
|
|
1838
|
+
skewX: (affine.a * affine.c + affine.b * affine.d) / Math.max(determinant, 1e-9)
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1246
1842
|
// src/lib/money.ts
|
|
1247
1843
|
var DEFAULT_CURRENCY = "USD";
|
|
1248
1844
|
var displayLocale;
|
|
@@ -1363,6 +1959,25 @@ function formatDate(value, opts) {
|
|
|
1363
1959
|
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
1364
1960
|
}
|
|
1365
1961
|
|
|
1962
|
+
// src/core/shapeLineStyle.ts
|
|
1963
|
+
var DEFAULT_SHAPE_LINE_CAP = "round";
|
|
1964
|
+
var DEFAULT_SHAPE_LINE_JOIN = "round";
|
|
1965
|
+
var DEFAULT_SHAPE_LINE_ENDING = "none";
|
|
1966
|
+
function resolvedShapeLineStyle(shape) {
|
|
1967
|
+
return {
|
|
1968
|
+
lineCap: shape.lineCap ?? DEFAULT_SHAPE_LINE_CAP,
|
|
1969
|
+
lineJoin: shape.lineJoin ?? DEFAULT_SHAPE_LINE_JOIN,
|
|
1970
|
+
startEnding: shape.startEnding ?? DEFAULT_SHAPE_LINE_ENDING,
|
|
1971
|
+
endEnding: shape.endEnding ?? DEFAULT_SHAPE_LINE_ENDING
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function shapeArrowMetrics(strokeWidth) {
|
|
1975
|
+
return {
|
|
1976
|
+
pointerLength: Math.min(48, Math.max(8, strokeWidth * 3)),
|
|
1977
|
+
pointerWidth: Math.min(40, Math.max(8, strokeWidth * 2.25))
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1366
1981
|
// src/engine/SeatmapRenderer.ts
|
|
1367
1982
|
var SEAT_RADIUS = 9;
|
|
1368
1983
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
@@ -1370,17 +1985,18 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
1370
1985
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1371
1986
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
1372
1987
|
var SEAT_TAP_SLOP_PX = 14;
|
|
1373
|
-
var
|
|
1988
|
+
var WHEELCHAIR_GLYPH_MIN_PX = 10;
|
|
1989
|
+
var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
|
|
1374
1990
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
1375
1991
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
1376
1992
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
1377
1993
|
var PAN_START_SLOP_PX = 8;
|
|
1994
|
+
var GHOST_CLICK_MS = 700;
|
|
1378
1995
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
1379
1996
|
var MAX_LABELS = 700;
|
|
1380
1997
|
var MARQUEE_RING_CAP = 2500;
|
|
1381
1998
|
var ISO_ANGLE_DEG = -11.5;
|
|
1382
1999
|
var ISO_SQUASH = 0.58;
|
|
1383
|
-
var LIFT_PER_STEP = 58;
|
|
1384
2000
|
var ISO_TWEEN_MS = 320;
|
|
1385
2001
|
var CAMERA_GLIDE_MS = 650;
|
|
1386
2002
|
var BLOCK_FILL_ALPHA = 1;
|
|
@@ -1417,6 +2033,10 @@ var DEF_SELECTION = "#ffffff";
|
|
|
1417
2033
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1418
2034
|
var DEF_DECOR_FILL = "#232c40";
|
|
1419
2035
|
var DEF_TEXT = "#8b93a7";
|
|
2036
|
+
function konvaFontStyle(bold, italic, defaultWeight = "normal") {
|
|
2037
|
+
const weight = bold ? "bold" : defaultWeight;
|
|
2038
|
+
return italic ? `italic ${weight}`.trim() : weight;
|
|
2039
|
+
}
|
|
1420
2040
|
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1421
2041
|
function colorLuminance(color) {
|
|
1422
2042
|
const s = color.trim();
|
|
@@ -1522,8 +2142,8 @@ function rotatedRectPoints(center, width, height, rotation) {
|
|
|
1522
2142
|
}));
|
|
1523
2143
|
}
|
|
1524
2144
|
function pointsBounds(points) {
|
|
1525
|
-
const
|
|
1526
|
-
return { x:
|
|
2145
|
+
const bounds2 = polyBounds(points);
|
|
2146
|
+
return { x: bounds2.x, y: bounds2.y, width: bounds2.width, height: bounds2.height };
|
|
1527
2147
|
}
|
|
1528
2148
|
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1529
2149
|
const radians = rotation * Math.PI / 180;
|
|
@@ -1543,14 +2163,14 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
|
1543
2163
|
return true;
|
|
1544
2164
|
}
|
|
1545
2165
|
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1546
|
-
const
|
|
1547
|
-
const centre = { x:
|
|
2166
|
+
const bounds2 = polyBounds(outer);
|
|
2167
|
+
const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
|
|
1548
2168
|
const points = [preferred];
|
|
1549
2169
|
for (let row = 1; row < 12; row += 1) {
|
|
1550
2170
|
for (let column = 1; column < 12; column += 1) {
|
|
1551
2171
|
const point = {
|
|
1552
|
-
x:
|
|
1553
|
-
y:
|
|
2172
|
+
x: bounds2.x + bounds2.width * column / 12,
|
|
2173
|
+
y: bounds2.y + bounds2.height * row / 12
|
|
1554
2174
|
};
|
|
1555
2175
|
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1556
2176
|
}
|
|
@@ -1615,6 +2235,33 @@ function rgba(hex, a) {
|
|
|
1615
2235
|
const n = parseInt(m[1], 16);
|
|
1616
2236
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1617
2237
|
}
|
|
2238
|
+
var ViewportGroup = class extends Group {
|
|
2239
|
+
constructor() {
|
|
2240
|
+
super(...arguments);
|
|
2241
|
+
this.viewportCulled = false;
|
|
2242
|
+
}
|
|
2243
|
+
setViewportCulled(culled) {
|
|
2244
|
+
if (culled === this.viewportCulled) return;
|
|
2245
|
+
this.viewportCulled = culled;
|
|
2246
|
+
if (!culled) super._clearSelfAndDescendantCache();
|
|
2247
|
+
}
|
|
2248
|
+
isViewportCulled() {
|
|
2249
|
+
return this.viewportCulled;
|
|
2250
|
+
}
|
|
2251
|
+
drawScene(...args) {
|
|
2252
|
+
return this.viewportCulled ? this : super.drawScene(...args);
|
|
2253
|
+
}
|
|
2254
|
+
drawHit(...args) {
|
|
2255
|
+
return this.viewportCulled ? this : super.drawHit(...args);
|
|
2256
|
+
}
|
|
2257
|
+
_clearSelfAndDescendantCache(attr) {
|
|
2258
|
+
if (!this.viewportCulled) {
|
|
2259
|
+
super._clearSelfAndDescendantCache(attr);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
this._clearCache(attr);
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
1618
2265
|
function hexToRgb(hex) {
|
|
1619
2266
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1620
2267
|
if (!m) return null;
|
|
@@ -1645,8 +2292,15 @@ function lerpColor(a, b, t2) {
|
|
|
1645
2292
|
}
|
|
1646
2293
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
1647
2294
|
constructor(container, options = {}) {
|
|
2295
|
+
/** Per-elevated-section label containers. Keeping the labels grouped lets the
|
|
2296
|
+
* Phase-B lift move thousands of seat labels with one transform per section
|
|
2297
|
+
* instead of rewriting every label on every tween frame. */
|
|
2298
|
+
this.labelLiftGroups = /* @__PURE__ */ new Map();
|
|
1648
2299
|
this.seats = [];
|
|
1649
2300
|
this.seatById = /* @__PURE__ */ new Map();
|
|
2301
|
+
/** One build per chart/floor; section membership queries only inspect seats in
|
|
2302
|
+
* the section bounds instead of rescanning the full 13k venue per section. */
|
|
2303
|
+
this.seatIndex = null;
|
|
1650
2304
|
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
1651
2305
|
this.chartDoc = null;
|
|
1652
2306
|
this.activeFloorId = "";
|
|
@@ -1662,13 +2316,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1662
2316
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1663
2317
|
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1664
2318
|
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1665
|
-
/** Centred
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
2319
|
+
/** Centred ♿ glyph per physical wheelchair provision. It keeps a small
|
|
2320
|
+
* screen-space floor at every LOD and grows when the Wheelchair filter is
|
|
2321
|
+
* active. Kept in a map so zoom updates touch only the handful of matching
|
|
2322
|
+
* seats, never all 13k nodes. */
|
|
1669
2323
|
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1670
|
-
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1671
|
-
this.accessGlyphVisible = false;
|
|
1672
2324
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1673
2325
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1674
2326
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
@@ -1693,6 +2345,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1693
2345
|
this.ownedHold = /* @__PURE__ */ new Set();
|
|
1694
2346
|
/** One selected seat being inspected before it is committed to the cart. */
|
|
1695
2347
|
this.selectionFocusId = null;
|
|
2348
|
+
/** Seat currently owning the shared hover ring (null while not hovering). */
|
|
2349
|
+
this.hoveredId = null;
|
|
1696
2350
|
this.focusedId = null;
|
|
1697
2351
|
/**
|
|
1698
2352
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -1711,6 +2365,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1711
2365
|
this.sections = [];
|
|
1712
2366
|
this.zones = [];
|
|
1713
2367
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
2368
|
+
/** Seats outside every authored section retain the legacy path in one group. */
|
|
2369
|
+
this.unsectionedSeatGroup = null;
|
|
1714
2370
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1715
2371
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1716
2372
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
@@ -1723,6 +2379,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1723
2379
|
this.focusedSectionId = null;
|
|
1724
2380
|
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1725
2381
|
this.focusBackdrop = null;
|
|
2382
|
+
/** Non-listening visual dim outside the focused section. One overlay replaces
|
|
2383
|
+
* descendant opacity mutations across every seat in the venue. */
|
|
2384
|
+
this.focusDimOverlay = null;
|
|
1726
2385
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1727
2386
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1728
2387
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1735,10 +2394,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1735
2394
|
this.isoT = 0;
|
|
1736
2395
|
this.isoTarget = 0;
|
|
1737
2396
|
this.isoRaf = 0;
|
|
2397
|
+
/** Public view target; perspective is a separate non-affine Phase-C lane. */
|
|
2398
|
+
this.viewMode = "flat";
|
|
2399
|
+
this.perspectiveCamera = null;
|
|
2400
|
+
/** Ground-plane tangent applied once to every renderer layer. */
|
|
2401
|
+
this.perspectiveBaseAffine = null;
|
|
2402
|
+
this.perspectiveBaseInverse = null;
|
|
2403
|
+
/** Exact pinhole anchor expressed in the overlay tangent-layer coordinates. */
|
|
2404
|
+
this.perspectiveSeatLocal = /* @__PURE__ */ new Map();
|
|
2405
|
+
/** Exact projected point (before Stage pan/zoom), keyed by seat. */
|
|
2406
|
+
this.perspectiveSeatProjected = /* @__PURE__ */ new Map();
|
|
2407
|
+
/** Positive camera depth, used for deterministic far→near paint order. */
|
|
2408
|
+
this.perspectiveSeatDepth = /* @__PURE__ */ new Map();
|
|
2409
|
+
/** Exact pinhole size ratio at each seat anchor. */
|
|
2410
|
+
this.perspectiveSeatScale = /* @__PURE__ */ new Map();
|
|
2411
|
+
/** Seats whose native Konva nodes currently carry the exact projected pose.
|
|
2412
|
+
* Large sectioned venues apply these lazily as a section enters the live-seat
|
|
2413
|
+
* viewport; the overview rung hides the seat layer completely. */
|
|
2414
|
+
this.perspectiveAppliedSeats = /* @__PURE__ */ new Set();
|
|
2415
|
+
this.perspectiveBounds = null;
|
|
1738
2416
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
1739
2417
|
this.isoCentre = { x: 0, y: 0 };
|
|
1740
2418
|
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
1741
2419
|
this.glideRaf = 0;
|
|
2420
|
+
/** While the camera tween is active, defer polygon label fitting/collision to
|
|
2421
|
+
* the settled frame. Geometry remains smoothly stage-scaled, while expensive
|
|
2422
|
+
* point-in-polygon text work no longer repeats at 120 Hz. */
|
|
2423
|
+
this.glideInProgress = false;
|
|
1742
2424
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
1743
2425
|
this.destroyed = false;
|
|
1744
2426
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -1768,6 +2450,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1768
2450
|
this.panStarted = false;
|
|
1769
2451
|
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
1770
2452
|
this.moved = 0;
|
|
2453
|
+
/**
|
|
2454
|
+
* Ghost-click guard. Konva binds `_pointerup` to mouseup, touchend AND
|
|
2455
|
+
* pointerup, so `on('click tap')` is two handlers for one finger tap unless
|
|
2456
|
+
* the browser's synthesized compatibility click is suppressed. Konva only
|
|
2457
|
+
* suppresses it when the touch lands on a LISTENING SHAPE (Stage.js returns
|
|
2458
|
+
* early before its preventDefault() when `!shape || !shape.isListening()`),
|
|
2459
|
+
* so a near-miss rescued by nearestSeatToScreen used to fire `tap` (select)
|
|
2460
|
+
* then `click` (deselect) 1-3ms apart and net to nothing. With
|
|
2461
|
+
* SEAT_TAP_SLOP_PX=14 against a ~12px seat radius, that broken annulus is
|
|
2462
|
+
* ~3.7x the area of the seat itself — i.e. most successful mobile taps.
|
|
2463
|
+
* `touch-action: none` does NOT suppress compatibility mouse events.
|
|
2464
|
+
*/
|
|
2465
|
+
this.lastTapAt = 0;
|
|
1771
2466
|
/**
|
|
1772
2467
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
1773
2468
|
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
@@ -1864,6 +2559,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1864
2559
|
x: mid.x - this.pinch.worldMid.x * scale,
|
|
1865
2560
|
y: mid.y - this.pinch.worldMid.y * scale
|
|
1866
2561
|
});
|
|
2562
|
+
this.updateSeatGroupVisibility();
|
|
1867
2563
|
this.stage.batchDraw();
|
|
1868
2564
|
this.scheduleViewChange();
|
|
1869
2565
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
@@ -1876,6 +2572,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1876
2572
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
1877
2573
|
});
|
|
1878
2574
|
this.panLast = p;
|
|
2575
|
+
this.updateSeatGroupVisibility();
|
|
1879
2576
|
this.stage.batchDraw();
|
|
1880
2577
|
this.scheduleViewChange();
|
|
1881
2578
|
}
|
|
@@ -1928,6 +2625,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1928
2625
|
this.overlayLayer = new Layer({ listening: false });
|
|
1929
2626
|
this.labelGroup = new Group({ listening: false });
|
|
1930
2627
|
this.overlayLayer.add(this.labelGroup);
|
|
2628
|
+
this.fgDecorGroup = new Group({ listening: false });
|
|
2629
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1931
2630
|
this.hoverRing = new Circle({
|
|
1932
2631
|
radius: SEAT_RADIUS + 2,
|
|
1933
2632
|
stroke: "#ffffff",
|
|
@@ -1962,6 +2661,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1962
2661
|
this.resizeObs.observe(container);
|
|
1963
2662
|
}
|
|
1964
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* True when this is the browser's synthesized compatibility click following a
|
|
2666
|
+
* finger tap we already handled. Discriminates on `e.type` deliberately —
|
|
2667
|
+
* `PointerEvent extends MouseEvent`, so an `instanceof MouseEvent` test
|
|
2668
|
+
* matches genuine pointer taps too. One shared timestamp across the layer and
|
|
2669
|
+
* stage handlers: a tap consumed at the layer must also suppress the ghost at
|
|
2670
|
+
* the stage, and only `click` is ever rejected, so bubbling stays intact.
|
|
2671
|
+
*/
|
|
2672
|
+
isGhostClick(e) {
|
|
2673
|
+
if (e.type === "tap") {
|
|
2674
|
+
this.lastTapAt = performance.now();
|
|
2675
|
+
return false;
|
|
2676
|
+
}
|
|
2677
|
+
return this.lastTapAt > 0 && performance.now() - this.lastTapAt < GHOST_CLICK_MS;
|
|
2678
|
+
}
|
|
1965
2679
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
1966
2680
|
setChart(doc, opts) {
|
|
1967
2681
|
if (doc !== this.chartDoc) this.stacked = false;
|
|
@@ -1975,8 +2689,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1975
2689
|
this.focusedId = null;
|
|
1976
2690
|
this.focusRing.visible(false);
|
|
1977
2691
|
this.bgLayer.destroyChildren();
|
|
2692
|
+
this.focusDimOverlay?.destroy();
|
|
2693
|
+
this.focusDimOverlay = null;
|
|
2694
|
+
this.seatLayer.clearCache();
|
|
2695
|
+
this.seatLayer.listening(true);
|
|
1978
2696
|
this.seatLayer.destroyChildren();
|
|
2697
|
+
this.unsectionedSeatGroup = null;
|
|
1979
2698
|
this.labelGroup.destroyChildren();
|
|
2699
|
+
this.labelLiftGroups.clear();
|
|
2700
|
+
this.fgDecorGroup.destroyChildren();
|
|
1980
2701
|
this.circleById.clear();
|
|
1981
2702
|
this.boothDims.clear();
|
|
1982
2703
|
this.boothLabelById.clear();
|
|
@@ -1993,6 +2714,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1993
2714
|
this.statusById.clear();
|
|
1994
2715
|
this.selection.clear();
|
|
1995
2716
|
this.seatById.clear();
|
|
2717
|
+
this.seatIndex = null;
|
|
1996
2718
|
this.cached = false;
|
|
1997
2719
|
this.accessFilter = null;
|
|
1998
2720
|
this.sections = [];
|
|
@@ -2010,11 +2732,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2010
2732
|
}
|
|
2011
2733
|
this.isoT = 0;
|
|
2012
2734
|
this.isoTarget = 0;
|
|
2735
|
+
this.viewMode = "flat";
|
|
2736
|
+
this.perspectiveCamera = null;
|
|
2737
|
+
this.perspectiveBaseAffine = null;
|
|
2738
|
+
this.perspectiveBaseInverse = null;
|
|
2739
|
+
this.perspectiveSeatLocal.clear();
|
|
2740
|
+
this.perspectiveSeatProjected.clear();
|
|
2741
|
+
this.perspectiveSeatDepth.clear();
|
|
2742
|
+
this.perspectiveSeatScale.clear();
|
|
2743
|
+
this.perspectiveAppliedSeats.clear();
|
|
2744
|
+
this.perspectiveBounds = null;
|
|
2013
2745
|
this.resetLayerTransforms();
|
|
2014
2746
|
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
2015
2747
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
2016
2748
|
this.seatLayer.opacity(1);
|
|
2017
2749
|
this.hoverRing.visible(false);
|
|
2750
|
+
this.hoveredId = null;
|
|
2018
2751
|
this.theme = doc.theme ?? {};
|
|
2019
2752
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
2020
2753
|
this.container.style.background = "";
|
|
@@ -2035,26 +2768,61 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2035
2768
|
this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
|
|
2036
2769
|
}
|
|
2037
2770
|
}
|
|
2038
|
-
this.seats = expandChart(view
|
|
2771
|
+
this.seats = expandChart(view, {
|
|
2772
|
+
// A stacked overview is presentation-only and never opens a seat POV. The
|
|
2773
|
+
// per-section renderer below still resolves each object's real floor base.
|
|
2774
|
+
floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
|
|
2775
|
+
});
|
|
2039
2776
|
for (const s of this.seats) {
|
|
2040
2777
|
this.seatById.set(s.id, s);
|
|
2041
2778
|
this.statusById.set(s.id, "free");
|
|
2042
2779
|
}
|
|
2780
|
+
this.seatIndex = buildSeatIndex(this.seats, { seatRadius: this.seatR });
|
|
2781
|
+
this.bounds = chartBounds(view);
|
|
2782
|
+
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
2783
|
+
this.buildPerspectiveProjection(view);
|
|
2043
2784
|
this.renderBackground(view);
|
|
2785
|
+
this.unsectionedSeatGroup = new Group({ listening: true });
|
|
2786
|
+
this.seatLayer.add(this.unsectionedSeatGroup);
|
|
2044
2787
|
this.renderSeats();
|
|
2045
2788
|
this.overlayLayer.add(this.labelGroup);
|
|
2789
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
2046
2790
|
this.overlayLayer.add(this.hoverRing);
|
|
2047
|
-
this.
|
|
2048
|
-
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
2791
|
+
this.overlayLayer.add(this.focusRing);
|
|
2049
2792
|
this.zoomToFit();
|
|
2050
2793
|
}
|
|
2051
2794
|
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
2052
2795
|
* the whole chart for single-floor charts. */
|
|
2053
2796
|
floorView(doc) {
|
|
2054
|
-
if (!doc.floors || !doc.floors.length) return
|
|
2055
|
-
|
|
2797
|
+
if (!doc.floors || !doc.floors.length) return {
|
|
2798
|
+
...doc,
|
|
2799
|
+
referenceImage: void 0,
|
|
2800
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
2801
|
+
};
|
|
2802
|
+
if (this.stacked && doc.floors.length >= 2) {
|
|
2803
|
+
return {
|
|
2804
|
+
...stackFloors(doc),
|
|
2805
|
+
referenceImage: void 0,
|
|
2806
|
+
backgroundImage: buyerBackgroundImage(doc)
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2056
2809
|
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
2057
|
-
return {
|
|
2810
|
+
return {
|
|
2811
|
+
...doc,
|
|
2812
|
+
objects: floor.objects,
|
|
2813
|
+
focalPoint: floor.focalPoint,
|
|
2814
|
+
referenceImage: void 0,
|
|
2815
|
+
backgroundImage: buyerBackgroundImage(floor),
|
|
2816
|
+
floors: void 0
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
/** Physical floor height is authored data; the 900-unit stacked overview
|
|
2820
|
+
* spread is presentation-only and must never leak into real 3D geometry. */
|
|
2821
|
+
floorBaseHeightMFor(objectId) {
|
|
2822
|
+
const floors = this.chartDoc?.floors;
|
|
2823
|
+
if (!floors?.length) return 0;
|
|
2824
|
+
const floorId = this.stacked && objectId ? this.objectFloor.get(objectId) ?? this.activeFloorId : this.activeFloorId;
|
|
2825
|
+
return floors.find((floor) => floor.id === floorId)?.baseHeightM ?? 0;
|
|
2058
2826
|
}
|
|
2059
2827
|
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
2060
2828
|
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
@@ -2367,7 +3135,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2367
3135
|
}
|
|
2368
3136
|
const ids = [];
|
|
2369
3137
|
for (const seat of this.seats) {
|
|
2370
|
-
|
|
3138
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3139
|
+
if (rendered.x < x0 || rendered.x > x1 || rendered.y < y0 || rendered.y > y1) continue;
|
|
2371
3140
|
if (!this.isSelectable(seat.id)) continue;
|
|
2372
3141
|
ids.push(seat.id);
|
|
2373
3142
|
}
|
|
@@ -2377,9 +3146,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2377
3146
|
flashSeat(seatId, color = "#f43f5e") {
|
|
2378
3147
|
const seat = this.seatById.get(seatId);
|
|
2379
3148
|
if (!seat) return;
|
|
3149
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
2380
3150
|
const ring = new Circle({
|
|
2381
|
-
x:
|
|
2382
|
-
y:
|
|
3151
|
+
x: rendered.x,
|
|
3152
|
+
y: rendered.y,
|
|
2383
3153
|
radius: this.seatR,
|
|
2384
3154
|
stroke: color,
|
|
2385
3155
|
strokeWidth: 3,
|
|
@@ -2388,6 +3158,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2388
3158
|
perfectDrawEnabled: false,
|
|
2389
3159
|
shadowForStrokeEnabled: false
|
|
2390
3160
|
});
|
|
3161
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seatId) ?? 1 : 1;
|
|
3162
|
+
ring.scale({ x: projectionScale, y: projectionScale });
|
|
2391
3163
|
this.overlayLayer.add(ring);
|
|
2392
3164
|
const start = performance.now();
|
|
2393
3165
|
const dur = 620;
|
|
@@ -2414,17 +3186,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2414
3186
|
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2415
3187
|
if (!matches.length) return;
|
|
2416
3188
|
for (const section of matches) {
|
|
2417
|
-
const
|
|
3189
|
+
const outline = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
3190
|
+
const lift = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
3191
|
+
return { x: point.x + lift.x, y: point.y + lift.y };
|
|
3192
|
+
});
|
|
3193
|
+
const centre = outline.reduce(
|
|
2418
3194
|
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2419
3195
|
{ x: 0, y: 0 }
|
|
2420
3196
|
);
|
|
2421
|
-
centre.x /=
|
|
2422
|
-
centre.y /=
|
|
2423
|
-
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
3197
|
+
centre.x /= outline.length;
|
|
3198
|
+
centre.y /= outline.length;
|
|
2424
3199
|
const halo = new Line({
|
|
2425
|
-
x: centre.x
|
|
2426
|
-
y: centre.y
|
|
2427
|
-
points:
|
|
3200
|
+
x: centre.x,
|
|
3201
|
+
y: centre.y,
|
|
3202
|
+
points: outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2428
3203
|
closed: true,
|
|
2429
3204
|
stroke: color,
|
|
2430
3205
|
strokeWidth: 3,
|
|
@@ -2468,12 +3243,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2468
3243
|
nearestSeat(fromId, dir) {
|
|
2469
3244
|
const from = this.seatById.get(fromId);
|
|
2470
3245
|
if (!from) return null;
|
|
3246
|
+
const fromPoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(from.id) ?? from : from;
|
|
2471
3247
|
let best = null;
|
|
2472
3248
|
let bestScore = Infinity;
|
|
2473
3249
|
for (const s of this.seats) {
|
|
2474
3250
|
if (s.id === fromId) continue;
|
|
2475
|
-
const
|
|
2476
|
-
const
|
|
3251
|
+
const candidatePoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(s.id) ?? s : s;
|
|
3252
|
+
const dx = candidatePoint.x - fromPoint.x;
|
|
3253
|
+
const dy = candidatePoint.y - fromPoint.y;
|
|
2477
3254
|
const proj = dx * dir.x + dy * dir.y;
|
|
2478
3255
|
if (proj <= 0.5) continue;
|
|
2479
3256
|
const perp = Math.abs(dx * dir.y - dy * dir.x);
|
|
@@ -2490,7 +3267,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2490
3267
|
if (!seat) return;
|
|
2491
3268
|
this.focusedId = id;
|
|
2492
3269
|
this.focusRing.radius(this.seatR + 3);
|
|
2493
|
-
this.focusRing.position(
|
|
3270
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
3271
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
3272
|
+
this.focusRing.scale({ x: projectionScale, y: projectionScale });
|
|
2494
3273
|
this.focusRing.visible(true);
|
|
2495
3274
|
this.ensureVisible(seat);
|
|
2496
3275
|
this.overlayLayer.batchDraw();
|
|
@@ -2505,7 +3284,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2505
3284
|
const margin = 70;
|
|
2506
3285
|
const offscreen = p.x < margin || p.x > w - margin || p.y < margin || p.y > h - margin;
|
|
2507
3286
|
if (this.stage.scaleX() < target || offscreen) {
|
|
2508
|
-
const
|
|
3287
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3288
|
+
const ip = this.viewMode === "perspective" && this.perspectiveBaseAffine ? applyAffine(this.perspectiveBaseAffine, rendered) : this.isoT === 0 ? rendered : this.isoForward(rendered);
|
|
2509
3289
|
this.stage.scale({ x: target, y: target });
|
|
2510
3290
|
this.stage.position({ x: w / 2 - ip.x * target, y: h / 2 - ip.y * target });
|
|
2511
3291
|
this.afterViewChange();
|
|
@@ -2515,7 +3295,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2515
3295
|
zoomToFit() {
|
|
2516
3296
|
const w = this.stage.width();
|
|
2517
3297
|
const h = this.stage.height();
|
|
2518
|
-
const b = this.bounds;
|
|
3298
|
+
const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
|
|
2519
3299
|
this.fitScale = Math.min(w / b.width, h / b.height) || 1;
|
|
2520
3300
|
const s = this.fitScale;
|
|
2521
3301
|
this.stage.scale({ x: s, y: s });
|
|
@@ -2544,7 +3324,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2544
3324
|
}
|
|
2545
3325
|
worldToScreen(point) {
|
|
2546
3326
|
const s = this.stage.scaleX();
|
|
2547
|
-
const
|
|
3327
|
+
const candidateId = point.id;
|
|
3328
|
+
const seat = typeof candidateId === "string" ? this.seatById.get(candidateId) : void 0;
|
|
3329
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3330
|
+
const projected = seat ? this.perspectiveSeatProjected.get(seat.id) ?? applyAffine(this.perspectiveBaseAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3331
|
+
return { x: projected.x * s + this.stage.x(), y: projected.y * s + this.stage.y() };
|
|
3332
|
+
}
|
|
3333
|
+
const localPoint = seat ? this.renderedSeatPoint(seat) : point;
|
|
3334
|
+
const p = this.isoT === 0 ? localPoint : this.isoForward(localPoint);
|
|
2548
3335
|
return { x: p.x * s + this.stage.x(), y: p.y * s + this.stage.y() };
|
|
2549
3336
|
}
|
|
2550
3337
|
setAccessibleFilter(on) {
|
|
@@ -2554,6 +3341,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2554
3341
|
const next = types === null ? null : [...types];
|
|
2555
3342
|
if (this.sameAccessFilter(next)) return;
|
|
2556
3343
|
this.accessFilter = next;
|
|
3344
|
+
this.updateAccessGlyphs(this.effScale());
|
|
2557
3345
|
for (const seat of this.seats) {
|
|
2558
3346
|
const c = this.circleById.get(seat.id);
|
|
2559
3347
|
if (c) this.paintSeat(c, seat.id);
|
|
@@ -2652,10 +3440,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2652
3440
|
let minY = Infinity;
|
|
2653
3441
|
let maxY = -Infinity;
|
|
2654
3442
|
for (const seat of matching) {
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3443
|
+
const point = this.renderedSeatPoint(seat);
|
|
3444
|
+
minX = Math.min(minX, point.x);
|
|
3445
|
+
maxX = Math.max(maxX, point.x);
|
|
3446
|
+
minY = Math.min(minY, point.y);
|
|
3447
|
+
maxY = Math.max(maxY, point.y);
|
|
2659
3448
|
}
|
|
2660
3449
|
const pad = Math.max(24, this.seatR * 3);
|
|
2661
3450
|
minX -= pad;
|
|
@@ -2676,12 +3465,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2676
3465
|
* inverse) keeps landing on the projected seats/sections.
|
|
2677
3466
|
*/
|
|
2678
3467
|
setViewMode(mode) {
|
|
3468
|
+
if (mode === "perspective") {
|
|
3469
|
+
if (this.viewMode === mode) {
|
|
3470
|
+
this.afterViewChange();
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const resetElevatedSeatGroups = this.viewMode === "isometric" && this.isoT > 0;
|
|
3474
|
+
this.viewMode = mode;
|
|
3475
|
+
this.isoTarget = 1;
|
|
3476
|
+
this.isoT = 1;
|
|
3477
|
+
if (this.isoRaf) {
|
|
3478
|
+
cancelAnimationFrame(this.isoRaf);
|
|
3479
|
+
this.isoRaf = 0;
|
|
3480
|
+
}
|
|
3481
|
+
this.applyPerspective(resetElevatedSeatGroups);
|
|
3482
|
+
this.zoomToFit();
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
2679
3485
|
const target = mode === "isometric" ? 1 : 0;
|
|
3486
|
+
const leavingPerspective = this.viewMode === "perspective";
|
|
3487
|
+
this.viewMode = mode;
|
|
2680
3488
|
this.isoTarget = target;
|
|
2681
3489
|
if (this.isoRaf) {
|
|
2682
3490
|
cancelAnimationFrame(this.isoRaf);
|
|
2683
3491
|
this.isoRaf = 0;
|
|
2684
3492
|
}
|
|
3493
|
+
if (leavingPerspective) {
|
|
3494
|
+
this.isoT = target;
|
|
3495
|
+
this.applyIso();
|
|
3496
|
+
this.afterViewChange();
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
2685
3499
|
if (this.reducedMotion) {
|
|
2686
3500
|
this.isoT = target;
|
|
2687
3501
|
this.applyIso();
|
|
@@ -2715,7 +3529,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2715
3529
|
}
|
|
2716
3530
|
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
2717
3531
|
getViewMode() {
|
|
2718
|
-
return this.
|
|
3532
|
+
return this.viewMode;
|
|
2719
3533
|
}
|
|
2720
3534
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
2721
3535
|
isoParams() {
|
|
@@ -2723,6 +3537,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2723
3537
|
}
|
|
2724
3538
|
/** Effective vertical scale = stage scale × iso squash — legibility math uses this. */
|
|
2725
3539
|
effScale() {
|
|
3540
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseAffine) {
|
|
3541
|
+
const xScale = Math.hypot(this.perspectiveBaseAffine.a, this.perspectiveBaseAffine.b);
|
|
3542
|
+
const yScale = Math.hypot(this.perspectiveBaseAffine.c, this.perspectiveBaseAffine.d);
|
|
3543
|
+
return this.stage.scaleX() * Math.min(xScale, yScale);
|
|
3544
|
+
}
|
|
2726
3545
|
return this.stage.scaleX() * (1 - (1 - ISO_SQUASH) * this.isoT);
|
|
2727
3546
|
}
|
|
2728
3547
|
/** Project a world point through the iso affine about the chart centre (→ iso-world). */
|
|
@@ -2746,19 +3565,113 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2746
3565
|
const wy = -dx * Math.sin(th) + uy * Math.cos(th);
|
|
2747
3566
|
return { x: c.x + wx, y: c.y + wy };
|
|
2748
3567
|
}
|
|
3568
|
+
/** Precompute the Phase-C pinhole camera and every exact seat anchor. */
|
|
3569
|
+
buildPerspectiveProjection(view) {
|
|
3570
|
+
let maxSurfaceHeightWorld = 0;
|
|
3571
|
+
for (const seat of this.seats) {
|
|
3572
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3573
|
+
maxSurfaceHeightWorld = Math.max(maxSurfaceHeightWorld, surfaceM * CHART_UNITS_PER_METRE);
|
|
3574
|
+
}
|
|
3575
|
+
for (const object of view.objects) {
|
|
3576
|
+
if (object.type !== "section") continue;
|
|
3577
|
+
maxSurfaceHeightWorld = Math.max(
|
|
3578
|
+
maxSurfaceHeightWorld,
|
|
3579
|
+
sectionGeometry(object, { floorBaseHeightM: this.floorBaseHeightMFor(object.id) }).height * CHART_UNITS_PER_METRE
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
const camera = createPerspectiveCamera(this.bounds, maxSurfaceHeightWorld);
|
|
3583
|
+
const base = perspectiveTangentAffine(camera, camera.target);
|
|
3584
|
+
const inverse = invertAffine(base);
|
|
3585
|
+
this.perspectiveCamera = camera;
|
|
3586
|
+
this.perspectiveBaseAffine = base;
|
|
3587
|
+
this.perspectiveBaseInverse = inverse;
|
|
3588
|
+
this.perspectiveSeatLocal.clear();
|
|
3589
|
+
this.perspectiveSeatProjected.clear();
|
|
3590
|
+
this.perspectiveSeatDepth.clear();
|
|
3591
|
+
this.perspectiveSeatScale.clear();
|
|
3592
|
+
this.perspectiveBounds = null;
|
|
3593
|
+
let minX = Infinity;
|
|
3594
|
+
let minY = Infinity;
|
|
3595
|
+
let maxX = -Infinity;
|
|
3596
|
+
let maxY = -Infinity;
|
|
3597
|
+
for (const seat of this.seats) {
|
|
3598
|
+
const surfaceM = Math.max(0, (seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M) - SEATED_EYE_HEIGHT_M);
|
|
3599
|
+
const projected = projectPerspectivePoint(camera, seat, surfaceM * CHART_UNITS_PER_METRE);
|
|
3600
|
+
const point = { x: projected.x, y: projected.y };
|
|
3601
|
+
this.perspectiveSeatProjected.set(seat.id, point);
|
|
3602
|
+
this.perspectiveSeatLocal.set(seat.id, applyAffine(inverse, point));
|
|
3603
|
+
this.perspectiveSeatDepth.set(seat.id, projected.depth);
|
|
3604
|
+
this.perspectiveSeatScale.set(seat.id, projected.scale);
|
|
3605
|
+
minX = Math.min(minX, projected.x);
|
|
3606
|
+
minY = Math.min(minY, projected.y);
|
|
3607
|
+
maxX = Math.max(maxX, projected.x);
|
|
3608
|
+
maxY = Math.max(maxY, projected.y);
|
|
3609
|
+
}
|
|
3610
|
+
for (const point of [
|
|
3611
|
+
{ x: this.bounds.x, y: this.bounds.y },
|
|
3612
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y },
|
|
3613
|
+
{ x: this.bounds.x + this.bounds.width, y: this.bounds.y + this.bounds.height },
|
|
3614
|
+
{ x: this.bounds.x, y: this.bounds.y + this.bounds.height }
|
|
3615
|
+
]) {
|
|
3616
|
+
const projected = projectPerspectivePoint(camera, point, 0);
|
|
3617
|
+
minX = Math.min(minX, projected.x);
|
|
3618
|
+
minY = Math.min(minY, projected.y);
|
|
3619
|
+
maxX = Math.max(maxX, projected.x);
|
|
3620
|
+
maxY = Math.max(maxY, projected.y);
|
|
3621
|
+
}
|
|
3622
|
+
const pad = Math.max(24, this.seatR * 3);
|
|
3623
|
+
this.perspectiveBounds = Number.isFinite(minX) ? { x: minX - pad, y: minY - pad, width: Math.max(1, maxX - minX + pad * 2), height: Math.max(1, maxY - minY + pad * 2) } : null;
|
|
3624
|
+
}
|
|
3625
|
+
/** Apply an affine to a Konva container while keeping `pivot` fixed logically. */
|
|
3626
|
+
setContainerAffine(node, affine, pivot) {
|
|
3627
|
+
const decomposed = decomposeAffineLinear(affine);
|
|
3628
|
+
node.offset(pivot);
|
|
3629
|
+
node.position(applyAffine(affine, pivot));
|
|
3630
|
+
node.rotation(decomposed.rotationDeg);
|
|
3631
|
+
node.scale({ x: decomposed.scaleX, y: decomposed.scaleY });
|
|
3632
|
+
node.skewX(decomposed.skewX);
|
|
3633
|
+
node.skewY(0);
|
|
3634
|
+
}
|
|
3635
|
+
resetContainerTransform(node) {
|
|
3636
|
+
node.position({ x: 0, y: 0 });
|
|
3637
|
+
node.offset({ x: 0, y: 0 });
|
|
3638
|
+
node.scale({ x: 1, y: 1 });
|
|
3639
|
+
node.rotation(0);
|
|
3640
|
+
node.skewX(0);
|
|
3641
|
+
node.skewY(0);
|
|
3642
|
+
}
|
|
3643
|
+
/** Exact raked surface height used for the bounded section tangent plane. */
|
|
3644
|
+
sectionSurfaceHeight(section, point) {
|
|
3645
|
+
const radial = Math.hypot(point.x - section.surfaceFocal.x, point.y - section.surfaceFocal.y);
|
|
3646
|
+
const depth = Math.max(0, radial - section.frontDistanceWorld);
|
|
3647
|
+
return section.liftWorld + depth * Math.tan(section.rakeDeg * Math.PI / 180);
|
|
3648
|
+
}
|
|
3649
|
+
/** Desired perspective-space point for a section surface, before Stage pan/zoom. */
|
|
3650
|
+
projectedSectionPoint(section, point) {
|
|
3651
|
+
return section.perspectiveAffine ? applyAffine(section.perspectiveAffine, point) : applyAffine(this.perspectiveBaseAffine, point);
|
|
3652
|
+
}
|
|
3653
|
+
/** Shared-layer local point which the ground affine paints at `projected`. */
|
|
3654
|
+
perspectiveLocalPoint(projected) {
|
|
3655
|
+
return this.perspectiveBaseInverse ? applyAffine(this.perspectiveBaseInverse, projected) : projected;
|
|
3656
|
+
}
|
|
2749
3657
|
/**
|
|
2750
3658
|
* Local-space offset that, once the layer applies the iso affine, lifts an
|
|
2751
|
-
* object straight UP in iso-world by `
|
|
2752
|
-
* (= inverse-linear of the pure vertical lift). Zero at isoT=0.
|
|
3659
|
+
* object straight UP in iso-world by `liftWorld × isoT` world units
|
|
3660
|
+
* (= inverse-linear of the pure vertical lift). Zero at isoT=0. `liftWorld` is
|
|
3661
|
+
* the section's resolved real height in world units (Phase B1).
|
|
2753
3662
|
*/
|
|
2754
|
-
isoLiftLocal(
|
|
3663
|
+
isoLiftLocal(liftWorld) {
|
|
2755
3664
|
const { th, sg } = this.isoParams();
|
|
2756
|
-
const delta =
|
|
3665
|
+
const delta = liftWorld * this.isoT;
|
|
2757
3666
|
return { x: -(delta / sg) * Math.sin(th), y: -(delta / sg) * Math.cos(th) };
|
|
2758
3667
|
}
|
|
2759
|
-
/** Restore
|
|
2760
|
-
|
|
2761
|
-
|
|
3668
|
+
/** Restore projection layers to identity (flat) — byte-for-byte the original.
|
|
3669
|
+
* `seatLayer` can be skipped when entering perspective from flat: assigning a
|
|
3670
|
+
* top-level transform invalidates all 14k descendant absolute transforms even
|
|
3671
|
+
* while the semantic overview keeps that layer fully transparent. */
|
|
3672
|
+
resetLayerTransforms(includeSeatLayer = true) {
|
|
3673
|
+
const layers = includeSeatLayer ? [this.bgLayer, this.seatLayer, this.overlayLayer] : [this.bgLayer, this.overlayLayer];
|
|
3674
|
+
for (const layer of layers) {
|
|
2762
3675
|
layer.position({ x: 0, y: 0 });
|
|
2763
3676
|
layer.offset({ x: 0, y: 0 });
|
|
2764
3677
|
layer.scale({ x: 1, y: 1 });
|
|
@@ -2767,6 +3680,117 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2767
3680
|
layer.skewY(0);
|
|
2768
3681
|
}
|
|
2769
3682
|
}
|
|
3683
|
+
/** Restore section-owned containers before applying another projection. */
|
|
3684
|
+
resetSectionProjection(resetSeatGroups = true) {
|
|
3685
|
+
for (const section of this.sections) {
|
|
3686
|
+
this.resetContainerTransform(section.rootGroupBg);
|
|
3687
|
+
this.resetContainerTransform(section.liftGroupBg);
|
|
3688
|
+
if (resetSeatGroups) this.resetContainerTransform(section.liftGroupSeat);
|
|
3689
|
+
section.rootGroupBg.zIndex(section.bgZIndex);
|
|
3690
|
+
if (resetSeatGroups) section.liftGroupSeat.zIndex(section.seatZIndex);
|
|
3691
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
3692
|
+
if (labelGroup && resetSeatGroups) this.resetContainerTransform(labelGroup);
|
|
3693
|
+
section.perspectiveAffine = void 0;
|
|
3694
|
+
section.perspectiveCorrection = void 0;
|
|
3695
|
+
section.perspectiveDepth = void 0;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
/** Move native seat/booth hit shapes to or from their exact pinhole anchors. */
|
|
3699
|
+
applyExactSeatAnchors(perspective, seatIds) {
|
|
3700
|
+
const seats = seatIds ? [...seatIds].map((id) => this.seatById.get(id)).filter((seat) => !!seat) : perspective ? this.seats : [...this.perspectiveAppliedSeats].map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
3701
|
+
for (const seat of seats) {
|
|
3702
|
+
if (perspective && this.perspectiveAppliedSeats.has(seat.id)) continue;
|
|
3703
|
+
const point = perspective ? this.perspectiveSeatProjected.get(seat.id) ?? seat : seat;
|
|
3704
|
+
const scale = perspective ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
3705
|
+
const shape = this.circleById.get(seat.id);
|
|
3706
|
+
shape?.position(point);
|
|
3707
|
+
shape?.scale({ x: scale, y: scale });
|
|
3708
|
+
const ring = this.accessRingById.get(seat.id);
|
|
3709
|
+
ring?.position(point);
|
|
3710
|
+
ring?.scale({ x: scale, y: scale });
|
|
3711
|
+
const glyph = this.accessGlyphById.get(seat.id);
|
|
3712
|
+
if (glyph) {
|
|
3713
|
+
const baseGlyphScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
|
|
3714
|
+
glyph.position(point);
|
|
3715
|
+
glyph.scale({ x: baseGlyphScale * scale, y: baseGlyphScale * scale });
|
|
3716
|
+
}
|
|
3717
|
+
const boothLabel = this.boothLabelById.get(seat.id);
|
|
3718
|
+
boothLabel?.position(point);
|
|
3719
|
+
boothLabel?.scale({ x: scale, y: scale });
|
|
3720
|
+
if (perspective) this.perspectiveAppliedSeats.add(seat.id);
|
|
3721
|
+
}
|
|
3722
|
+
if (!perspective) this.perspectiveAppliedSeats.clear();
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Phase C projected 2.5D. Seat/booth anchors are exact pinhole projections;
|
|
3726
|
+
* section top surfaces use one explicitly bounded tangent plane per authored
|
|
3727
|
+
* section. Native Konva shapes move with the anchors, so direct hit testing is
|
|
3728
|
+
* the same graph that paints the buyer-visible unit.
|
|
3729
|
+
*/
|
|
3730
|
+
applyPerspective(resetElevatedSeatGroups = true) {
|
|
3731
|
+
const camera = this.perspectiveCamera;
|
|
3732
|
+
const base = this.perspectiveBaseAffine;
|
|
3733
|
+
const inverse = this.perspectiveBaseInverse;
|
|
3734
|
+
if (!camera || !base || !inverse) return;
|
|
3735
|
+
const lazySectionSeats = this.seats.length > 2500 && this.sections.length > 0;
|
|
3736
|
+
if (this.cached && !lazySectionSeats) {
|
|
3737
|
+
this.seatLayer.clearCache();
|
|
3738
|
+
this.seatLayer.listening(true);
|
|
3739
|
+
this.cached = false;
|
|
3740
|
+
}
|
|
3741
|
+
this.resetLayerTransforms(resetElevatedSeatGroups);
|
|
3742
|
+
this.resetSectionProjection(resetElevatedSeatGroups);
|
|
3743
|
+
for (const layer of [this.bgLayer, this.overlayLayer]) {
|
|
3744
|
+
this.setContainerAffine(layer, base, camera.target);
|
|
3745
|
+
}
|
|
3746
|
+
this.perspectiveAppliedSeats.clear();
|
|
3747
|
+
if (!lazySectionSeats) this.applyExactSeatAnchors(true);
|
|
3748
|
+
for (const section of this.sections) {
|
|
3749
|
+
const heightAt = (point) => this.sectionSurfaceHeight(section, point);
|
|
3750
|
+
const desired = perspectiveTangentAffine(camera, section.centroid, heightAt);
|
|
3751
|
+
const correction = composeAffine(inverse, desired);
|
|
3752
|
+
section.perspectiveAffine = desired;
|
|
3753
|
+
section.perspectiveCorrection = correction;
|
|
3754
|
+
section.perspectiveDepth = projectPerspectivePoint(
|
|
3755
|
+
camera,
|
|
3756
|
+
section.centroid,
|
|
3757
|
+
heightAt(section.centroid)
|
|
3758
|
+
).depth;
|
|
3759
|
+
this.setContainerAffine(section.liftGroupBg, correction, section.centroid);
|
|
3760
|
+
for (let index = 0; index < section.sideFaces.length; index++) {
|
|
3761
|
+
const p0 = section.outline[index];
|
|
3762
|
+
const p1 = section.outline[(index + 1) % section.outline.length];
|
|
3763
|
+
const base0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, 0));
|
|
3764
|
+
const base1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, 0));
|
|
3765
|
+
const top1 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p1, heightAt(p1)));
|
|
3766
|
+
const top0 = this.perspectiveLocalPoint(projectPerspectivePoint(camera, p0, heightAt(p0)));
|
|
3767
|
+
section.sideFaces[index].points([
|
|
3768
|
+
base0.x,
|
|
3769
|
+
base0.y,
|
|
3770
|
+
base1.x,
|
|
3771
|
+
base1.y,
|
|
3772
|
+
top1.x,
|
|
3773
|
+
top1.y,
|
|
3774
|
+
top0.x,
|
|
3775
|
+
top0.y
|
|
3776
|
+
]);
|
|
3777
|
+
section.sideFaces[index].opacity(0.9);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
const farToNear = [...this.sections].sort(
|
|
3781
|
+
(left, right) => (right.perspectiveDepth ?? 0) - (left.perspectiveDepth ?? 0)
|
|
3782
|
+
);
|
|
3783
|
+
for (const section of farToNear) {
|
|
3784
|
+
section.rootGroupBg.moveToTop();
|
|
3785
|
+
section.liftGroupSeat.moveToTop();
|
|
3786
|
+
}
|
|
3787
|
+
this.unsectionedSeatGroup?.moveToTop();
|
|
3788
|
+
this.syncSeatOverlayPositions();
|
|
3789
|
+
if (!lazySectionSeats) this.updateSeatGroupVisibility();
|
|
3790
|
+
this.bgLayer.batchDraw();
|
|
3791
|
+
this.seatLayer.batchDraw();
|
|
3792
|
+
this.overlayLayer.batchDraw();
|
|
3793
|
+
}
|
|
2770
3794
|
/**
|
|
2771
3795
|
* Apply the current isoT to the scene: the base rotate+squash as a decomposed
|
|
2772
3796
|
* layer transform (so seats/décor/rings project together and Konva's own
|
|
@@ -2774,6 +3798,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2774
3798
|
* lift + extruded side faces on elevated sections.
|
|
2775
3799
|
*/
|
|
2776
3800
|
applyIso() {
|
|
3801
|
+
this.resetSectionProjection();
|
|
3802
|
+
this.applyExactSeatAnchors(false);
|
|
2777
3803
|
const t2 = this.isoT;
|
|
2778
3804
|
if (t2 === 0) {
|
|
2779
3805
|
this.resetLayerTransforms();
|
|
@@ -2802,6 +3828,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2802
3828
|
}
|
|
2803
3829
|
this.applyUprightLabels();
|
|
2804
3830
|
this.applyElevation();
|
|
3831
|
+
this.updateSeatGroupVisibility();
|
|
2805
3832
|
this.bgLayer.batchDraw();
|
|
2806
3833
|
this.seatLayer.batchDraw();
|
|
2807
3834
|
this.overlayLayer.batchDraw();
|
|
@@ -2830,10 +3857,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2830
3857
|
applyElevation() {
|
|
2831
3858
|
const t2 = this.isoT;
|
|
2832
3859
|
for (const sec of this.sections) {
|
|
2833
|
-
if (sec.
|
|
2834
|
-
const off = this.isoLiftLocal(sec.
|
|
3860
|
+
if (sec.liftWorld <= 0) continue;
|
|
3861
|
+
const off = this.isoLiftLocal(sec.liftWorld);
|
|
2835
3862
|
sec.liftGroupBg?.position(off);
|
|
2836
|
-
sec.liftGroupSeat
|
|
3863
|
+
sec.liftGroupSeat.position(off);
|
|
3864
|
+
this.labelLiftGroups.get(sec.id)?.position(off);
|
|
2837
3865
|
const alpha = 0.9 * t2;
|
|
2838
3866
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
2839
3867
|
const face = sec.sideFaces[i];
|
|
@@ -2843,6 +3871,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2843
3871
|
face.opacity(alpha);
|
|
2844
3872
|
}
|
|
2845
3873
|
}
|
|
3874
|
+
this.syncSeatOverlayPositions();
|
|
2846
3875
|
}
|
|
2847
3876
|
destroy() {
|
|
2848
3877
|
this.destroyed = true;
|
|
@@ -2869,16 +3898,166 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2869
3898
|
* tier shifts with one offset in iso view) or the seat layer directly.
|
|
2870
3899
|
*/
|
|
2871
3900
|
seatContainer(id) {
|
|
2872
|
-
return this.seatSection.get(id)?.liftGroupSeat ?? this.seatLayer;
|
|
3901
|
+
return this.seatSection.get(id)?.liftGroupSeat ?? this.unsectionedSeatGroup ?? this.seatLayer;
|
|
2873
3902
|
}
|
|
2874
|
-
|
|
3903
|
+
/** True when a section belongs to the active logical section/zone focus. */
|
|
3904
|
+
sectionMatchesFocus(section) {
|
|
3905
|
+
const focus = this.focusedSectionId;
|
|
3906
|
+
return focus == null || section.id === focus || section.logicalId === focus || section.zone === focus;
|
|
3907
|
+
}
|
|
3908
|
+
/** Visual opacity after the section-focus overlay is composed. */
|
|
3909
|
+
focusedSeatOpacity(id, localOpacity) {
|
|
3910
|
+
if (!this.focusedSectionId) return localOpacity;
|
|
3911
|
+
const section = this.seatSection.get(id);
|
|
3912
|
+
return localOpacity * (section && this.sectionMatchesFocus(section) ? 1 : FOCUS_DIM_OPACITY);
|
|
3913
|
+
}
|
|
3914
|
+
/** Project a section outline through elevation + the active affine view and
|
|
3915
|
+
* then into viewport pixels. The result is conservative (AABB), so a visible
|
|
3916
|
+
* section is never clipped even for rotated or isometric outlines. */
|
|
3917
|
+
sectionScreenBounds(section) {
|
|
3918
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
3919
|
+
const scale2 = this.stage.scaleX();
|
|
3920
|
+
const stageX2 = this.stage.x();
|
|
3921
|
+
const stageY2 = this.stage.y();
|
|
3922
|
+
return polyBounds(section.outline.map((point) => {
|
|
3923
|
+
const projected = this.projectedSectionPoint(section, point);
|
|
3924
|
+
return { x: projected.x * scale2 + stageX2, y: projected.y * scale2 + stageY2 };
|
|
3925
|
+
}));
|
|
3926
|
+
}
|
|
3927
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
3928
|
+
const scale = this.stage.scaleX();
|
|
3929
|
+
const stageX = this.stage.x();
|
|
3930
|
+
const stageY = this.stage.y();
|
|
3931
|
+
return polyBounds(section.outline.map((point) => {
|
|
3932
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
3933
|
+
const projected = this.isoT === 0 ? local : this.isoForward(local);
|
|
3934
|
+
return { x: projected.x * scale + stageX, y: projected.y * scale + stageY };
|
|
3935
|
+
}));
|
|
3936
|
+
}
|
|
3937
|
+
/** Hide only section groups wholly outside a padded viewport at the live-seat
|
|
3938
|
+
* rung. Overview caching always restores all groups first, so panning a
|
|
3939
|
+
* cached whole-venue bitmap can never reveal missing inventory. */
|
|
3940
|
+
updateSeatGroupVisibility() {
|
|
3941
|
+
const liveSeats = this.effScale() >= CACHE_THRESHOLD;
|
|
3942
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
3943
|
+
const padding = 96;
|
|
3944
|
+
const width = this.stage.width();
|
|
3945
|
+
const height = this.stage.height();
|
|
3946
|
+
for (const section of this.sections) {
|
|
3947
|
+
let visible = true;
|
|
3948
|
+
if (liveSeats) {
|
|
3949
|
+
const bounds2 = this.sectionScreenBounds(section);
|
|
3950
|
+
visible = bounds2.x + bounds2.width >= -padding && bounds2.y + bounds2.height >= -padding && bounds2.x <= width + padding && bounds2.y <= height + padding;
|
|
3951
|
+
if (visible && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
3952
|
+
this.applyExactSeatAnchors(true, section.memberIds);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
section.liftGroupSeat.setViewportCulled(!visible);
|
|
3956
|
+
const labelGroup = this.labelLiftGroups.get(section.id);
|
|
3957
|
+
labelGroup?.setViewportCulled(!visible);
|
|
3958
|
+
}
|
|
3959
|
+
if (this.unsectionedSeatGroup && !this.unsectionedSeatGroup.visible()) {
|
|
3960
|
+
this.unsectionedSeatGroup.visible(true);
|
|
3961
|
+
}
|
|
3962
|
+
if (liveSeats && this.viewMode === "perspective" && !deferPerspectiveSeatReveal) {
|
|
3963
|
+
this.applyExactSeatAnchors(true, this.seats.filter((seat) => !this.seatSection.has(seat.id)).map((seat) => seat.id));
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
/** Seats worth considering for viewport labels. Section culling is a safe
|
|
3967
|
+
* coarse index; the later screen test remains the exact filter. */
|
|
3968
|
+
visibleSeatCandidates() {
|
|
3969
|
+
if (!this.sections.length) return this.seats;
|
|
3970
|
+
const candidates = [];
|
|
3971
|
+
for (const section of this.sections) {
|
|
3972
|
+
if (section.liftGroupSeat.isViewportCulled()) continue;
|
|
3973
|
+
for (const id of section.memberIds) {
|
|
3974
|
+
const seat = this.seatById.get(id);
|
|
3975
|
+
if (seat) candidates.push(seat);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
2875
3978
|
for (const seat of this.seats) {
|
|
3979
|
+
if (!this.seatSection.has(seat.id)) candidates.push(seat);
|
|
3980
|
+
}
|
|
3981
|
+
return candidates;
|
|
3982
|
+
}
|
|
3983
|
+
seatViewportCulled(id) {
|
|
3984
|
+
return this.seatSection.get(id)?.liftGroupSeat.isViewportCulled() ?? false;
|
|
3985
|
+
}
|
|
3986
|
+
/** Local world coordinate at which an elevated seat is actually painted for
|
|
3987
|
+
* the current iso tween. Flat seats and the 2D endpoint are unchanged. */
|
|
3988
|
+
renderedSeatPoint(seat) {
|
|
3989
|
+
if (this.viewMode === "perspective") return this.perspectiveSeatLocal.get(seat.id) ?? seat;
|
|
3990
|
+
const section = this.seatSection.get(seat.id);
|
|
3991
|
+
if (!section || section.liftWorld <= 0 || this.isoT === 0) return seat;
|
|
3992
|
+
const offset = this.isoLiftLocal(section.liftWorld);
|
|
3993
|
+
return { x: seat.x + offset.x, y: seat.y + offset.y };
|
|
3994
|
+
}
|
|
3995
|
+
/** Overlay labels mirror the seat-layer lift without an O(seats) per-frame
|
|
3996
|
+
* rewrite. The groups are rebuilt with the zoom-dependent label set. */
|
|
3997
|
+
seatLabelContainer(id) {
|
|
3998
|
+
const section = this.seatSection.get(id);
|
|
3999
|
+
if (!section) return this.labelGroup;
|
|
4000
|
+
let group = this.labelLiftGroups.get(section.id);
|
|
4001
|
+
if (!group) {
|
|
4002
|
+
group = new ViewportGroup({
|
|
4003
|
+
listening: false
|
|
4004
|
+
});
|
|
4005
|
+
group.setViewportCulled(section.liftGroupSeat.isViewportCulled());
|
|
4006
|
+
group.position(this.viewMode === "perspective" ? { x: 0, y: 0 } : this.isoLiftLocal(section.liftWorld));
|
|
4007
|
+
this.labelGroup.add(group);
|
|
4008
|
+
this.labelLiftGroups.set(section.id, group);
|
|
4009
|
+
}
|
|
4010
|
+
return group;
|
|
4011
|
+
}
|
|
4012
|
+
/** Shared overlay furniture is not parented to section lift groups, so keep
|
|
4013
|
+
* the small transient set aligned explicitly. Selection is capped in buyer
|
|
4014
|
+
* mode, making this constant-sized work during the iso tween. */
|
|
4015
|
+
syncSeatOverlayPositions() {
|
|
4016
|
+
if (this.hoveredId) {
|
|
4017
|
+
const seat = this.seatById.get(this.hoveredId);
|
|
4018
|
+
if (seat) {
|
|
4019
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
4020
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4021
|
+
this.hoverRing.scale({ x: scale, y: scale });
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (this.focusedId) {
|
|
4025
|
+
const seat = this.seatById.get(this.focusedId);
|
|
4026
|
+
if (seat) {
|
|
4027
|
+
this.focusRing.position(this.renderedSeatPoint(seat));
|
|
4028
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4029
|
+
this.focusRing.scale({ x: scale, y: scale });
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
for (const [id, marker] of this.selectionMarkers) {
|
|
4033
|
+
const seat = this.seatById.get(id);
|
|
4034
|
+
if (seat) {
|
|
4035
|
+
marker.position(this.renderedSeatPoint(seat));
|
|
4036
|
+
const scale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4037
|
+
marker.scale({ x: scale, y: scale });
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
renderSeats() {
|
|
4042
|
+
const paintOrder = [...this.seats].sort((left, right) => (this.perspectiveSeatDepth.get(right.id) ?? 0) - (this.perspectiveSeatDepth.get(left.id) ?? 0));
|
|
4043
|
+
for (const seat of paintOrder) {
|
|
2876
4044
|
if (seat.kind === "booth") {
|
|
2877
4045
|
this.renderBoothUnit(seat);
|
|
2878
4046
|
continue;
|
|
2879
4047
|
}
|
|
2880
4048
|
const target = this.seatContainer(seat.id);
|
|
2881
|
-
const c = new
|
|
4049
|
+
const c = seat.wheelchairSpaceType === "no-seat" ? new Rect({
|
|
4050
|
+
x: seat.x,
|
|
4051
|
+
y: seat.y,
|
|
4052
|
+
width: this.seatR * 2,
|
|
4053
|
+
height: this.seatR * 2,
|
|
4054
|
+
offsetX: this.seatR,
|
|
4055
|
+
offsetY: this.seatR,
|
|
4056
|
+
cornerRadius: 2,
|
|
4057
|
+
perfectDrawEnabled: false,
|
|
4058
|
+
shadowForStrokeEnabled: false,
|
|
4059
|
+
hitStrokeWidth: 0
|
|
4060
|
+
}) : new Circle({
|
|
2882
4061
|
x: seat.x,
|
|
2883
4062
|
y: seat.y,
|
|
2884
4063
|
radius: this.seatR,
|
|
@@ -2891,21 +4070,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2891
4070
|
this.paintSeat(c, seat.id);
|
|
2892
4071
|
target.add(c);
|
|
2893
4072
|
if (seat.accessible) {
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
4073
|
+
if (seat.wheelchairSpaceType !== "no-seat") {
|
|
4074
|
+
const ring = new Circle({
|
|
4075
|
+
x: seat.x,
|
|
4076
|
+
y: seat.y,
|
|
4077
|
+
radius: this.seatR + 1.5,
|
|
4078
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
4079
|
+
strokeWidth: 2.5,
|
|
4080
|
+
listening: false,
|
|
4081
|
+
perfectDrawEnabled: false,
|
|
4082
|
+
shadowForStrokeEnabled: false
|
|
4083
|
+
});
|
|
4084
|
+
this.accessRingById.set(seat.id, ring);
|
|
4085
|
+
target.add(ring);
|
|
4086
|
+
}
|
|
4087
|
+
if (seat.accessibility?.includes("wheelchair")) {
|
|
4088
|
+
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
4089
|
+
this.accessGlyphById.set(seat.id, glyph);
|
|
4090
|
+
target.add(glyph);
|
|
4091
|
+
}
|
|
2909
4092
|
}
|
|
2910
4093
|
}
|
|
2911
4094
|
}
|
|
@@ -2943,7 +4126,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2943
4126
|
t2.offsetX(t2.width() / 2);
|
|
2944
4127
|
t2.offsetY(t2.height() / 2);
|
|
2945
4128
|
t2.visible(false);
|
|
2946
|
-
this.boothLabelById.set(seat.id, t2);
|
|
2947
4129
|
this.hasBoothText = true;
|
|
2948
4130
|
this.boothLabelById.set(seat.id, t2);
|
|
2949
4131
|
target.add(t2);
|
|
@@ -2966,24 +4148,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2966
4148
|
scaleY: k,
|
|
2967
4149
|
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
2968
4150
|
listening: false,
|
|
2969
|
-
visible:
|
|
4151
|
+
visible: false,
|
|
2970
4152
|
perfectDrawEnabled: false,
|
|
2971
4153
|
shadowForStrokeEnabled: false
|
|
2972
4154
|
});
|
|
2973
4155
|
}
|
|
2974
4156
|
/**
|
|
2975
|
-
*
|
|
2976
|
-
*
|
|
2977
|
-
*
|
|
2978
|
-
* never the full node graph, so it is cheap to call on every view change.
|
|
4157
|
+
* Keep physical wheelchair provision readable in screen space at every map
|
|
4158
|
+
* LOD. The active Wheelchair filter promotes it further. Iterates the small
|
|
4159
|
+
* wheelchair-seat set, never the full node graph.
|
|
2979
4160
|
*/
|
|
2980
4161
|
updateAccessGlyphs(scale) {
|
|
2981
4162
|
if (!this.accessGlyphById.size) return;
|
|
2982
|
-
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
2983
4163
|
for (const [id, glyph] of this.accessGlyphById) {
|
|
2984
|
-
|
|
4164
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
4165
|
+
const baseScale = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX * perspectiveScale;
|
|
4166
|
+
const emphasized = this.accessGlyphFilterEmphasized(id);
|
|
4167
|
+
const minimumScreenPx = emphasized ? FILTERED_WHEELCHAIR_GLYPH_MIN_PX : WHEELCHAIR_GLYPH_MIN_PX;
|
|
4168
|
+
const glyphScale = Math.max(
|
|
4169
|
+
baseScale,
|
|
4170
|
+
minimumScreenPx / (ACCESS_GLYPH_VIEWBOX * Math.max(scale, 1e-3))
|
|
4171
|
+
);
|
|
4172
|
+
glyph.scale({ x: glyphScale, y: glyphScale });
|
|
4173
|
+
glyph.visible(this.accessGlyphShouldShow(id));
|
|
2985
4174
|
}
|
|
2986
4175
|
}
|
|
4176
|
+
/** True only for a physical wheelchair provision matching an active buyer filter. */
|
|
4177
|
+
accessGlyphFilterEmphasized(id) {
|
|
4178
|
+
const seat = this.seatById.get(id);
|
|
4179
|
+
const filter = this.accessFilter;
|
|
4180
|
+
return !!seat && filter !== null && !!seat.accessibility?.includes("wheelchair") && (filter.length === 0 || filter.includes("wheelchair")) && seatMatchesAccess(seat, filter);
|
|
4181
|
+
}
|
|
4182
|
+
accessGlyphShouldShow(id) {
|
|
4183
|
+
return this.accessGlyphById.has(id) && this.accessGlyphEligible(id);
|
|
4184
|
+
}
|
|
2987
4185
|
/**
|
|
2988
4186
|
* Whether an accessible seat's glyph should show for its current status. It is
|
|
2989
4187
|
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
@@ -3063,6 +4261,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3063
4261
|
c.dash([2, 2]);
|
|
3064
4262
|
break;
|
|
3065
4263
|
}
|
|
4264
|
+
if (seat.wheelchairSpaceType === "no-seat" && status === "free") {
|
|
4265
|
+
c.stroke(accessibilityRingColor(seat.accessibility));
|
|
4266
|
+
c.strokeWidth(selected ? 3 : 2);
|
|
4267
|
+
c.dash([3, 2]);
|
|
4268
|
+
}
|
|
3066
4269
|
if (boothLabel) {
|
|
3067
4270
|
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? "HELD" : seat.label);
|
|
3068
4271
|
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
@@ -3096,11 +4299,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3096
4299
|
c.dash([]);
|
|
3097
4300
|
c.opacity(CLOSED_SEAT_OPACITY);
|
|
3098
4301
|
}
|
|
3099
|
-
if (this.focusedSectionId) {
|
|
3100
|
-
const sec = this.seatSection.get(id);
|
|
3101
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
3102
|
-
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
3103
|
-
}
|
|
3104
4302
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
3105
4303
|
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
3106
4304
|
if (bookableLabel) {
|
|
@@ -3114,7 +4312,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3114
4312
|
const fill = c.fill();
|
|
3115
4313
|
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
3116
4314
|
accessGlyph.opacity(c.opacity());
|
|
3117
|
-
accessGlyph.visible(this.
|
|
4315
|
+
accessGlyph.visible(this.accessGlyphShouldShow(id));
|
|
3118
4316
|
}
|
|
3119
4317
|
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
3120
4318
|
}
|
|
@@ -3183,7 +4381,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3183
4381
|
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
3184
4382
|
this.focusedSectionId = id;
|
|
3185
4383
|
this.drawFocusBackdrop(id);
|
|
3186
|
-
this.
|
|
4384
|
+
this.bgLayer.batchDraw();
|
|
4385
|
+
this.overlayLayer.batchDraw();
|
|
3187
4386
|
this.updateLOD();
|
|
3188
4387
|
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
3189
4388
|
}
|
|
@@ -3195,7 +4394,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3195
4394
|
this.focusBackdrop.destroy();
|
|
3196
4395
|
this.focusBackdrop = null;
|
|
3197
4396
|
}
|
|
3198
|
-
this.
|
|
4397
|
+
this.focusDimOverlay?.destroy();
|
|
4398
|
+
this.focusDimOverlay = null;
|
|
4399
|
+
this.bgLayer.batchDraw();
|
|
4400
|
+
this.overlayLayer.batchDraw();
|
|
3199
4401
|
this.updateLOD();
|
|
3200
4402
|
}
|
|
3201
4403
|
/** The currently AXS-focused section id, or null. */
|
|
@@ -3212,16 +4414,69 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3212
4414
|
if (!sections.length) return;
|
|
3213
4415
|
const backdrop = new Group({ listening: false });
|
|
3214
4416
|
for (const section of sections) {
|
|
3215
|
-
|
|
4417
|
+
const perspective = this.viewMode === "perspective" && section.perspectiveAffine;
|
|
4418
|
+
const outline = perspective ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline;
|
|
4419
|
+
const holes = perspective ? section.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point)))) : section.holes;
|
|
4420
|
+
backdrop.add(polygonWithHolesShape(outline, holes, {
|
|
3216
4421
|
fill: FOCUS_BACKDROP_FILL,
|
|
3217
4422
|
stroke: rgba("#ffffff", 0.1),
|
|
3218
4423
|
strokeWidth: 1,
|
|
3219
4424
|
listening: false
|
|
3220
|
-
}, section.outlinePath));
|
|
4425
|
+
}, perspective ? void 0 : section.outlinePath));
|
|
3221
4426
|
}
|
|
3222
4427
|
this.bgLayer.add(backdrop);
|
|
3223
4428
|
backdrop.moveToTop();
|
|
3224
4429
|
this.focusBackdrop = backdrop;
|
|
4430
|
+
this.drawFocusDimOverlay(sections);
|
|
4431
|
+
}
|
|
4432
|
+
/** Dim everything outside the active section with one paint-only shape. Group
|
|
4433
|
+
* opacity would make Konva recursively invalidate absolute-opacity caches on
|
|
4434
|
+
* all 14k descendants. The cut-out follows the live elevation offset and is
|
|
4435
|
+
* non-listening, so direct seat hit-testing is unchanged. */
|
|
4436
|
+
drawFocusDimOverlay(sections) {
|
|
4437
|
+
this.focusDimOverlay?.destroy();
|
|
4438
|
+
const span = Math.max(this.bounds.width, this.bounds.height, 1);
|
|
4439
|
+
const padding = span * 4 + 1e3;
|
|
4440
|
+
const outer = [
|
|
4441
|
+
{ x: this.bounds.x - padding, y: this.bounds.y - padding },
|
|
4442
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y - padding },
|
|
4443
|
+
{ x: this.bounds.x + this.bounds.width + padding, y: this.bounds.y + this.bounds.height + padding },
|
|
4444
|
+
{ x: this.bounds.x - padding, y: this.bounds.y + this.bounds.height + padding }
|
|
4445
|
+
];
|
|
4446
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
4447
|
+
const next = points[(index + 1) % points.length];
|
|
4448
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
4449
|
+
}, 0);
|
|
4450
|
+
const overlay = new Shape({
|
|
4451
|
+
fill: this.canvasBackground,
|
|
4452
|
+
opacity: 1 - FOCUS_DIM_OPACITY,
|
|
4453
|
+
listening: false,
|
|
4454
|
+
perfectDrawEnabled: false,
|
|
4455
|
+
sceneFunc: (context, shape) => {
|
|
4456
|
+
const polygonPath = (points) => {
|
|
4457
|
+
if (!points.length) return;
|
|
4458
|
+
context.moveTo(points[0].x, points[0].y);
|
|
4459
|
+
for (let index = 1; index < points.length; index++) context.lineTo(points[index].x, points[index].y);
|
|
4460
|
+
context.closePath();
|
|
4461
|
+
};
|
|
4462
|
+
context.beginPath();
|
|
4463
|
+
polygonPath(outer);
|
|
4464
|
+
for (const section of sections) {
|
|
4465
|
+
const hole = this.viewMode === "perspective" && section.perspectiveAffine ? section.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(section, point))) : section.outline.map((point) => {
|
|
4466
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
4467
|
+
return { x: point.x + offset.x, y: point.y + offset.y };
|
|
4468
|
+
});
|
|
4469
|
+
polygonPath(signedArea(hole) > 0 ? [...hole].reverse() : hole);
|
|
4470
|
+
}
|
|
4471
|
+
context.fillStrokeShape(shape);
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
this.overlayLayer.add(overlay);
|
|
4475
|
+
overlay.moveToTop();
|
|
4476
|
+
for (const marker of this.selectionMarkers.values()) marker.moveToTop();
|
|
4477
|
+
this.hoverRing.moveToTop();
|
|
4478
|
+
this.focusRing.moveToTop();
|
|
4479
|
+
this.focusDimOverlay = overlay;
|
|
3225
4480
|
}
|
|
3226
4481
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
3227
4482
|
repaintSectionsAndSeats() {
|
|
@@ -3285,7 +4540,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3285
4540
|
this.applyGAFilterState();
|
|
3286
4541
|
}
|
|
3287
4542
|
renderBackground(doc) {
|
|
3288
|
-
|
|
4543
|
+
const buyerBackground = buyerBackgroundImage(doc);
|
|
4544
|
+
if (buyerBackground) this.renderBackgroundImage(buyerBackground);
|
|
3289
4545
|
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
3290
4546
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
3291
4547
|
this.renderZones(doc);
|
|
@@ -3365,10 +4621,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3365
4621
|
listening: false,
|
|
3366
4622
|
perfectDrawEnabled: false
|
|
3367
4623
|
});
|
|
3368
|
-
this.
|
|
4624
|
+
if (obj.layer === "foreground") this.fgDecorGroup.add(node);
|
|
4625
|
+
else this.bgLayer.add(node);
|
|
3369
4626
|
img.onload = () => {
|
|
3370
|
-
|
|
3371
|
-
|
|
4627
|
+
const layer = node.getLayer();
|
|
4628
|
+
if (!layer) return;
|
|
4629
|
+
layer.batchDraw();
|
|
3372
4630
|
};
|
|
3373
4631
|
img.src = obj.href;
|
|
3374
4632
|
}
|
|
@@ -3405,7 +4663,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3405
4663
|
})
|
|
3406
4664
|
);
|
|
3407
4665
|
}
|
|
3408
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
4666
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3409
4667
|
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
3410
4668
|
}
|
|
3411
4669
|
renderText(obj) {
|
|
@@ -3417,11 +4675,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3417
4675
|
text: obj.text,
|
|
3418
4676
|
fontSize: obj.fontSize,
|
|
3419
4677
|
rotation: obj.rotation,
|
|
4678
|
+
fontStyle: konvaFontStyle(obj.bold, obj.italic, "normal"),
|
|
3420
4679
|
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3421
4680
|
// the actual canvas. Fail over to readable black/white instead of
|
|
3422
4681
|
// painting an otherwise valid caption invisibly on that active surface.
|
|
3423
4682
|
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3424
|
-
fontFamily: this.labelFont(),
|
|
4683
|
+
fontFamily: obj.fontFamily ?? this.labelFont(),
|
|
3425
4684
|
listening: false,
|
|
3426
4685
|
perfectDrawEnabled: false
|
|
3427
4686
|
});
|
|
@@ -3439,11 +4698,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3439
4698
|
const isDecor = !!obj.role && !isStage;
|
|
3440
4699
|
const palette = overviewPalette(this.canvasBackground);
|
|
3441
4700
|
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
4701
|
+
const isOpenPath = obj.kind === "line" || obj.kind === "polyline";
|
|
4702
|
+
const stroke = obj.stroke?.color ?? (isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : isOpenPath ? "#9aa3b5" : void 0);
|
|
4703
|
+
const strokeWidth = obj.stroke?.width ?? (isStage ? 1 : referenceFocal ? 2 : isOpenPath ? 2 : 0);
|
|
4704
|
+
const opacity = obj.opacity != null ? clamp(obj.opacity, 0.1, 1) : 1;
|
|
3444
4705
|
let cx = 0;
|
|
3445
4706
|
let cy = 0;
|
|
3446
|
-
if (
|
|
4707
|
+
if (isOpenPath && obj.points && obj.points.length >= 2) {
|
|
4708
|
+
cx = obj.points.reduce((a, p) => a + p.x, 0) / obj.points.length;
|
|
4709
|
+
cy = obj.points.reduce((a, p) => a + p.y, 0) / obj.points.length;
|
|
4710
|
+
const lineStyle = resolvedShapeLineStyle(obj);
|
|
4711
|
+
const arrow = shapeArrowMetrics(strokeWidth);
|
|
4712
|
+
const path = new Arrow({
|
|
4713
|
+
points: obj.points.flatMap((p) => [p.x, p.y]),
|
|
4714
|
+
closed: false,
|
|
4715
|
+
x: cx,
|
|
4716
|
+
y: cy,
|
|
4717
|
+
offsetX: cx,
|
|
4718
|
+
offsetY: cy,
|
|
4719
|
+
rotation: obj.rotation ?? 0,
|
|
4720
|
+
stroke,
|
|
4721
|
+
fill: stroke,
|
|
4722
|
+
strokeWidth,
|
|
4723
|
+
opacity,
|
|
4724
|
+
lineCap: lineStyle.lineCap,
|
|
4725
|
+
lineJoin: lineStyle.lineJoin,
|
|
4726
|
+
pointerAtBeginning: lineStyle.startEnding === "arrow",
|
|
4727
|
+
pointerAtEnding: lineStyle.endEnding === "arrow",
|
|
4728
|
+
...arrow,
|
|
4729
|
+
listening: false,
|
|
4730
|
+
name: "buyer-shape-open-path"
|
|
4731
|
+
});
|
|
4732
|
+
path.setAttr("shapeObjectId", obj.id);
|
|
4733
|
+
this.bgLayer.add(path);
|
|
4734
|
+
} else if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
3447
4735
|
const grad = isStage ? {
|
|
3448
4736
|
fillLinearGradientStartPoint: { x: 0, y: 0 },
|
|
3449
4737
|
fillLinearGradientEndPoint: { x: 0, y: obj.height },
|
|
@@ -3463,7 +4751,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3463
4751
|
...grad,
|
|
3464
4752
|
stroke,
|
|
3465
4753
|
strokeWidth,
|
|
3466
|
-
cornerRadius: 4,
|
|
4754
|
+
cornerRadius: clamp(obj.cornerRadius ?? 4, 0, Math.min(obj.width, obj.height) / 2),
|
|
4755
|
+
opacity,
|
|
3467
4756
|
listening: false
|
|
3468
4757
|
})
|
|
3469
4758
|
);
|
|
@@ -3476,7 +4765,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3476
4765
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3477
4766
|
} : { fill };
|
|
3478
4767
|
this.bgLayer.add(
|
|
3479
|
-
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
4768
|
+
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3480
4769
|
);
|
|
3481
4770
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
3482
4771
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -3491,7 +4780,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3491
4780
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3492
4781
|
} : { fill };
|
|
3493
4782
|
this.bgLayer.add(
|
|
3494
|
-
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
4783
|
+
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, opacity, listening: false })
|
|
3495
4784
|
);
|
|
3496
4785
|
}
|
|
3497
4786
|
if (obj.label) {
|
|
@@ -3550,7 +4839,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3550
4839
|
strokeWidth: 1.5
|
|
3551
4840
|
});
|
|
3552
4841
|
poly.setAttr("gaId", obj.id);
|
|
3553
|
-
poly.on("click tap", () =>
|
|
4842
|
+
poly.on("click tap", (e) => {
|
|
4843
|
+
if (this.isGhostClick(e)) return;
|
|
4844
|
+
this.opts.onGAClick?.(obj.id);
|
|
4845
|
+
});
|
|
3554
4846
|
poly.on("mouseenter", () => {
|
|
3555
4847
|
this.container.style.cursor = "pointer";
|
|
3556
4848
|
});
|
|
@@ -3560,7 +4852,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3560
4852
|
this.bgLayer.add(poly);
|
|
3561
4853
|
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3562
4854
|
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3563
|
-
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
4855
|
+
const label = this.addCentredLabel(this.bgLayer, obj.displayLabel ?? obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3564
4856
|
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3565
4857
|
this.freeTextById.set(`${obj.id}:label`, {
|
|
3566
4858
|
objectId: obj.id,
|
|
@@ -3577,7 +4869,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3577
4869
|
categoryKey: obj.categoryKey
|
|
3578
4870
|
});
|
|
3579
4871
|
this.gaById.set(obj.id, {
|
|
3580
|
-
label: obj.label,
|
|
4872
|
+
label: obj.displayLabel ?? obj.label,
|
|
3581
4873
|
capacity: obj.capacity,
|
|
3582
4874
|
categoryKey: obj.categoryKey,
|
|
3583
4875
|
points: obj.points,
|
|
@@ -3600,7 +4892,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3600
4892
|
const memberIds = [];
|
|
3601
4893
|
const catCounts = /* @__PURE__ */ new Map();
|
|
3602
4894
|
let free = 0;
|
|
3603
|
-
|
|
4895
|
+
const bounds2 = polyBounds(obj.outline);
|
|
4896
|
+
const candidates = this.seatIndex ? queryRect(this.seatIndex, bounds2).map((id) => this.seatById.get(id)).filter((seat) => !!seat) : this.seats;
|
|
4897
|
+
for (const seat of candidates) {
|
|
3604
4898
|
if (this.seatSection.has(seat.id)) continue;
|
|
3605
4899
|
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
3606
4900
|
memberIds.push(seat.id);
|
|
@@ -3611,11 +4905,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3611
4905
|
[...catCounts].map(([key, w]) => ({ hex: this.catColor.get(key) ?? "#6e7bff", w })),
|
|
3612
4906
|
"#3a4358"
|
|
3613
4907
|
);
|
|
3614
|
-
const elevation =
|
|
3615
|
-
|
|
3616
|
-
|
|
4908
|
+
const elevation = sectionElevationTier(obj.elevation);
|
|
4909
|
+
const geometry = sectionGeometry(obj, {
|
|
4910
|
+
floorBaseHeightM: this.floorBaseHeightMFor(obj.id)
|
|
4911
|
+
});
|
|
4912
|
+
const liftWorld = geometry.height * CHART_UNITS_PER_METRE;
|
|
4913
|
+
const rootGroupBg = new Group({ listening: false });
|
|
4914
|
+
this.bgLayer.add(rootGroupBg);
|
|
4915
|
+
const liftGroupBg = new Group({ listening: false });
|
|
4916
|
+
rootGroupBg.add(liftGroupBg);
|
|
4917
|
+
const liftGroupSeat = new ViewportGroup({ listening: true });
|
|
4918
|
+
this.seatLayer.add(liftGroupSeat);
|
|
3617
4919
|
const sideFaces = [];
|
|
3618
|
-
if (
|
|
4920
|
+
if (liftWorld > 0) {
|
|
3619
4921
|
const faceFill = darken(this.zoneColor.get(obj.zone ?? "") ?? baseFill, 0.42);
|
|
3620
4922
|
for (let i = 0; i < obj.outline.length; i++) {
|
|
3621
4923
|
const face = new Line({
|
|
@@ -3629,14 +4931,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3629
4931
|
perfectDrawEnabled: false
|
|
3630
4932
|
});
|
|
3631
4933
|
sideFaces.push(face);
|
|
3632
|
-
|
|
4934
|
+
rootGroupBg.add(face);
|
|
3633
4935
|
}
|
|
3634
|
-
liftGroupBg = new Group({ listening: false });
|
|
3635
|
-
this.bgLayer.add(liftGroupBg);
|
|
3636
|
-
liftGroupSeat = new Group({ listening: false });
|
|
3637
|
-
this.seatLayer.add(liftGroupSeat);
|
|
3638
4936
|
}
|
|
3639
|
-
|
|
4937
|
+
liftGroupBg.moveToTop();
|
|
4938
|
+
const bgTarget = liftGroupBg;
|
|
3640
4939
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
3641
4940
|
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3642
4941
|
stroke: rgba(outlineTint, 0.5),
|
|
@@ -3710,10 +5009,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3710
5009
|
nameLabelFits: true,
|
|
3711
5010
|
subLabelFits: true,
|
|
3712
5011
|
elevation,
|
|
5012
|
+
liftWorld,
|
|
5013
|
+
rakeDeg: geometry.rake,
|
|
5014
|
+
surfaceFocal: memberIds.length ? { ...this.seatById.get(memberIds[0])?.focalPoint ?? this.isoCentre } : { ...this.chartDoc?.focalPoint ?? this.isoCentre },
|
|
5015
|
+
frontDistanceWorld: 0,
|
|
5016
|
+
rootGroupBg,
|
|
5017
|
+
bgZIndex: rootGroupBg.zIndex(),
|
|
5018
|
+
seatZIndex: liftGroupSeat.zIndex(),
|
|
3713
5019
|
liftGroupBg,
|
|
3714
5020
|
liftGroupSeat,
|
|
3715
5021
|
sideFaces
|
|
3716
5022
|
};
|
|
5023
|
+
const surfaceCandidates = memberIds.map((id) => this.seatById.get(id)).filter((seat) => !!seat);
|
|
5024
|
+
const distanceCandidates = surfaceCandidates.length ? surfaceCandidates : obj.outline;
|
|
5025
|
+
sec.frontDistanceWorld = Math.min(...distanceCandidates.map((point) => Math.hypot(
|
|
5026
|
+
point.x - sec.surfaceFocal.x,
|
|
5027
|
+
point.y - sec.surfaceFocal.y
|
|
5028
|
+
)));
|
|
3717
5029
|
for (const id of memberIds) this.seatSection.set(id, sec);
|
|
3718
5030
|
this.refreshSectionFill(sec);
|
|
3719
5031
|
this.refreshSectionHeat(sec);
|
|
@@ -3859,15 +5171,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3859
5171
|
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3860
5172
|
if (sectionOverview) blockT = 1;
|
|
3861
5173
|
if (!this.zones.length) zoneT = 0;
|
|
3862
|
-
this.
|
|
5174
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
5175
|
+
this.seatLayer.opacity(sectionOverview || deferPerspectiveSeatReveal ? 0 : 1 - blockT);
|
|
3863
5176
|
const sx = this.stage.scaleX();
|
|
3864
|
-
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.
|
|
5177
|
+
const rescale = !this.glideInProgress && (this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.08);
|
|
3865
5178
|
if (rescale) this.lodScale = scale;
|
|
3866
5179
|
const focus = this.focusedSectionId;
|
|
3867
5180
|
const palette = overviewPalette(this.canvasBackground);
|
|
3868
5181
|
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3869
5182
|
for (const sec of this.sections) {
|
|
3870
|
-
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
5183
|
+
const dim = this.focusDimOverlay ? 1 : focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3871
5184
|
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3872
5185
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3873
5186
|
sec.blockPoly.stroke(palette.sectionStroke);
|
|
@@ -3879,7 +5192,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3879
5192
|
);
|
|
3880
5193
|
sec.nameLabel.fill(sectionInk);
|
|
3881
5194
|
sec.subLabel.fill(sectionInk);
|
|
3882
|
-
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
5195
|
+
if (rescale && sectionLabelT > 0.01) this.fitSectionRungLabels(sec, sx);
|
|
3883
5196
|
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3884
5197
|
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3885
5198
|
sec.subLabel.opacity(0);
|
|
@@ -3891,12 +5204,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3891
5204
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3892
5205
|
if (rescale) this.sizeZonePill(zone, sx);
|
|
3893
5206
|
}
|
|
3894
|
-
this.
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3899
|
-
|
|
5207
|
+
if (!this.glideInProgress && (sectionLabelT > 0.01 || zoneOpacity > 0.01)) {
|
|
5208
|
+
this.decollideRungLabels(sx);
|
|
5209
|
+
this.dedupeLogicalSectionLabels();
|
|
5210
|
+
for (const zone of this.zones) {
|
|
5211
|
+
const opacity = zone.label.opacity();
|
|
5212
|
+
zone.back.opacity(opacity);
|
|
5213
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
5214
|
+
}
|
|
3900
5215
|
}
|
|
3901
5216
|
this.bgLayer.batchDraw();
|
|
3902
5217
|
}
|
|
@@ -4023,13 +5338,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4023
5338
|
screenToWorld(clientPoint) {
|
|
4024
5339
|
const s = this.stage.scaleX();
|
|
4025
5340
|
const iso = { x: (clientPoint.x - this.stage.x()) / s, y: (clientPoint.y - this.stage.y()) / s };
|
|
5341
|
+
if (this.viewMode === "perspective" && this.perspectiveBaseInverse) {
|
|
5342
|
+
return applyAffine(this.perspectiveBaseInverse, iso);
|
|
5343
|
+
}
|
|
4026
5344
|
return this.isoT === 0 ? iso : this.isoInverse(iso);
|
|
4027
5345
|
}
|
|
4028
5346
|
/** Section id under a container-relative screen point, or null (Slice 5 tap-to-zoom). */
|
|
4029
5347
|
sectionAt(clientPoint) {
|
|
4030
5348
|
if (!this.sections.length) return null;
|
|
4031
5349
|
const world = this.screenToWorld(clientPoint);
|
|
4032
|
-
const hit = this.sections.find((sec) =>
|
|
5350
|
+
const hit = this.sections.find((sec) => {
|
|
5351
|
+
if (this.viewMode === "perspective" && sec.perspectiveAffine) {
|
|
5352
|
+
return pointInPolygonWithHoles(
|
|
5353
|
+
world,
|
|
5354
|
+
sec.outline.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))),
|
|
5355
|
+
sec.holes.map((hole) => hole.map((point) => this.perspectiveLocalPoint(this.projectedSectionPoint(sec, point))))
|
|
5356
|
+
);
|
|
5357
|
+
}
|
|
5358
|
+
const offset = sec.liftWorld > 0 ? this.isoLiftLocal(sec.liftWorld) : { x: 0, y: 0 };
|
|
5359
|
+
return pointInPolygonWithHoles(
|
|
5360
|
+
{ x: world.x - offset.x, y: world.y - offset.y },
|
|
5361
|
+
sec.outline,
|
|
5362
|
+
sec.holes
|
|
5363
|
+
);
|
|
5364
|
+
});
|
|
4033
5365
|
return hit ? hit.logicalId : null;
|
|
4034
5366
|
}
|
|
4035
5367
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
@@ -4125,15 +5457,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4125
5457
|
if (!seat) return;
|
|
4126
5458
|
const candidate = this.selectionFocusId === id;
|
|
4127
5459
|
const dims = this.boothDims.get(seat.rowId);
|
|
5460
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
4128
5461
|
const marker = new Group({
|
|
4129
5462
|
name: "selection-ring",
|
|
4130
|
-
x:
|
|
4131
|
-
y:
|
|
5463
|
+
x: rendered.x,
|
|
5464
|
+
y: rendered.y,
|
|
4132
5465
|
rotation: dims?.rotation ?? 0,
|
|
4133
5466
|
listening: false,
|
|
4134
5467
|
perfectDrawEnabled: false,
|
|
4135
5468
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
4136
5469
|
});
|
|
5470
|
+
if (this.viewMode === "perspective") {
|
|
5471
|
+
const projectionScale = this.perspectiveSeatScale.get(id) ?? 1;
|
|
5472
|
+
marker.scale({ x: projectionScale, y: projectionScale });
|
|
5473
|
+
}
|
|
4137
5474
|
marker.setAttr("seatId", id);
|
|
4138
5475
|
const common = {
|
|
4139
5476
|
stroke: this.effSelection,
|
|
@@ -4212,12 +5549,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4212
5549
|
* must fall through and pick.
|
|
4213
5550
|
*/
|
|
4214
5551
|
sectionBounds(id) {
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
5552
|
+
const bounds2 = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => {
|
|
5553
|
+
if (this.viewMode === "perspective" && section.perspectiveAffine) {
|
|
5554
|
+
return polyBounds(section.outline.map((point) => this.projectedSectionPoint(section, point)));
|
|
5555
|
+
}
|
|
5556
|
+
const offset = section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
5557
|
+
const points = section.outline.map((point) => {
|
|
5558
|
+
const local = { x: point.x + offset.x, y: point.y + offset.y };
|
|
5559
|
+
return this.isoT === 0 ? local : this.isoForward(local);
|
|
5560
|
+
});
|
|
5561
|
+
return polyBounds(points);
|
|
5562
|
+
});
|
|
5563
|
+
if (!bounds2.length) return null;
|
|
5564
|
+
const left = Math.min(...bounds2.map((box) => box.x));
|
|
5565
|
+
const top = Math.min(...bounds2.map((box) => box.y));
|
|
5566
|
+
const right = Math.max(...bounds2.map((box) => box.x + box.width));
|
|
5567
|
+
const bottom = Math.max(...bounds2.map((box) => box.y + box.height));
|
|
4221
5568
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
4222
5569
|
}
|
|
4223
5570
|
sectionFrameScale(id) {
|
|
@@ -4270,15 +5617,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4270
5617
|
* hijacking a clean tap on empty space beyond the slop.
|
|
4271
5618
|
*/
|
|
4272
5619
|
nearestSeatToScreen(screen, slopPx) {
|
|
4273
|
-
const s = this.stage.scaleX() || 1;
|
|
4274
|
-
const reachWorld = this.seatR + slopPx / s;
|
|
4275
|
-
const world = this.screenToWorld(screen);
|
|
4276
5620
|
let best = null;
|
|
4277
|
-
let bestD =
|
|
5621
|
+
let bestD = Infinity;
|
|
5622
|
+
const stageScale = this.stage.scaleX() || 1;
|
|
4278
5623
|
for (const seat of this.seats) {
|
|
4279
5624
|
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
4280
|
-
const
|
|
4281
|
-
|
|
5625
|
+
const centre = this.worldToScreen(seat);
|
|
5626
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
5627
|
+
const reachPx = this.seatR * stageScale * perspectiveScale + slopPx;
|
|
5628
|
+
const d = Math.hypot(centre.x - screen.x, centre.y - screen.y);
|
|
5629
|
+
if (d <= reachPx && d < bestD) {
|
|
4282
5630
|
bestD = d;
|
|
4283
5631
|
best = seat.id;
|
|
4284
5632
|
}
|
|
@@ -4288,6 +5636,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4288
5636
|
wireInteraction() {
|
|
4289
5637
|
this.seatLayer.on("click tap", (e) => {
|
|
4290
5638
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5639
|
+
if (this.isGhostClick(e)) return;
|
|
4291
5640
|
const id = seatIdOf(e.target);
|
|
4292
5641
|
if (!id) return;
|
|
4293
5642
|
this.handleSeatTap(id);
|
|
@@ -4297,13 +5646,17 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4297
5646
|
if (!id) return;
|
|
4298
5647
|
const seat = this.seatById.get(id);
|
|
4299
5648
|
if (!seat) return;
|
|
4300
|
-
this.
|
|
5649
|
+
this.hoveredId = id;
|
|
5650
|
+
this.hoverRing.position(this.renderedSeatPoint(seat));
|
|
5651
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(id) ?? 1 : 1;
|
|
5652
|
+
this.hoverRing.scale({ x: projectionScale, y: projectionScale });
|
|
4301
5653
|
this.hoverRing.visible(true);
|
|
4302
5654
|
this.overlayLayer.batchDraw();
|
|
4303
5655
|
this.container.style.cursor = "pointer";
|
|
4304
5656
|
this.opts.onHover?.(seat);
|
|
4305
5657
|
});
|
|
4306
5658
|
this.seatLayer.on("mouseout", () => {
|
|
5659
|
+
this.hoveredId = null;
|
|
4307
5660
|
this.hoverRing.visible(false);
|
|
4308
5661
|
this.overlayLayer.batchDraw();
|
|
4309
5662
|
this.container.style.cursor = "default";
|
|
@@ -4318,6 +5671,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4318
5671
|
});
|
|
4319
5672
|
this.stage.on("click tap", (e) => {
|
|
4320
5673
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5674
|
+
if (this.isGhostClick(e)) return;
|
|
4321
5675
|
const pointer = this.stage.getPointerPosition();
|
|
4322
5676
|
if (!pointer) return;
|
|
4323
5677
|
if (!this.cached) {
|
|
@@ -4334,11 +5688,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4334
5688
|
}
|
|
4335
5689
|
}
|
|
4336
5690
|
if (this.sections.length) {
|
|
4337
|
-
const
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
else this.focusRegion(hit.logicalId);
|
|
5691
|
+
const sectionId = this.sectionAt(pointer);
|
|
5692
|
+
if (sectionId) {
|
|
5693
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sectionId);
|
|
5694
|
+
else this.focusRegion(sectionId);
|
|
4342
5695
|
return;
|
|
4343
5696
|
}
|
|
4344
5697
|
}
|
|
@@ -4355,13 +5708,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4355
5708
|
*/
|
|
4356
5709
|
deckFloorAt() {
|
|
4357
5710
|
if (!this.objectFloor.size) return null;
|
|
4358
|
-
const
|
|
4359
|
-
if (!
|
|
5711
|
+
const pointer = this.stage.getPointerPosition();
|
|
5712
|
+
if (!pointer) return null;
|
|
5713
|
+
const p = this.screenToWorld(pointer);
|
|
4360
5714
|
let best = null;
|
|
4361
5715
|
let bestD = Infinity;
|
|
4362
5716
|
for (const s of this.seats) {
|
|
4363
|
-
const
|
|
4364
|
-
const
|
|
5717
|
+
const rendered = this.renderedSeatPoint(s);
|
|
5718
|
+
const dx = rendered.x - p.x;
|
|
5719
|
+
const dy = rendered.y - p.y;
|
|
4365
5720
|
const d = dx * dx + dy * dy;
|
|
4366
5721
|
if (d < bestD) {
|
|
4367
5722
|
bestD = d;
|
|
@@ -4448,9 +5803,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4448
5803
|
}
|
|
4449
5804
|
const start = performance.now();
|
|
4450
5805
|
const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
|
|
5806
|
+
this.glideInProgress = true;
|
|
4451
5807
|
const step = (now) => {
|
|
4452
5808
|
if (this.destroyed) {
|
|
4453
5809
|
this.glideRaf = 0;
|
|
5810
|
+
this.glideInProgress = false;
|
|
4454
5811
|
return;
|
|
4455
5812
|
}
|
|
4456
5813
|
const raw = Math.min(1, (now - start) / duration);
|
|
@@ -4458,6 +5815,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4458
5815
|
const sc = fromScale + (toScale - fromScale) * e;
|
|
4459
5816
|
this.stage.scale({ x: sc, y: sc });
|
|
4460
5817
|
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
5818
|
+
this.updateSeatGroupVisibility();
|
|
4461
5819
|
this.updateLOD();
|
|
4462
5820
|
this.scheduleViewChange();
|
|
4463
5821
|
this.stage.batchDraw();
|
|
@@ -4465,6 +5823,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4465
5823
|
this.glideRaf = requestAnimationFrame(step);
|
|
4466
5824
|
} else {
|
|
4467
5825
|
this.glideRaf = 0;
|
|
5826
|
+
this.glideInProgress = false;
|
|
4468
5827
|
this.afterViewChange();
|
|
4469
5828
|
}
|
|
4470
5829
|
};
|
|
@@ -4476,6 +5835,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4476
5835
|
cancelAnimationFrame(this.glideRaf);
|
|
4477
5836
|
this.glideRaf = 0;
|
|
4478
5837
|
}
|
|
5838
|
+
this.glideInProgress = false;
|
|
4479
5839
|
}
|
|
4480
5840
|
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
4481
5841
|
getRung() {
|
|
@@ -4492,11 +5852,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4492
5852
|
const labels = this.seats.map((seat) => {
|
|
4493
5853
|
const shape = this.circleById.get(seat.id);
|
|
4494
5854
|
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
5855
|
+
const localPerspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
5856
|
+
const seatEffectiveScale = (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale;
|
|
4495
5857
|
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4496
|
-
const
|
|
5858
|
+
const labelEffectiveScale = this.viewMode === "perspective" && seat.kind !== "booth" ? effectiveScale * localPerspectiveScale : seatEffectiveScale;
|
|
5859
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * labelEffectiveScale);
|
|
4497
5860
|
const screen = this.worldToScreen(seat);
|
|
4498
5861
|
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4499
|
-
const opacity = shape?.
|
|
5862
|
+
const opacity = this.focusedSeatOpacity(seat.id, shape?.getAbsoluteOpacity() ?? 0);
|
|
4500
5863
|
const section = this.seatSection.get(seat.id);
|
|
4501
5864
|
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4502
5865
|
let hiddenReason;
|
|
@@ -4507,17 +5870,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4507
5870
|
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4508
5871
|
else hiddenReason = "renderer-hidden";
|
|
4509
5872
|
}
|
|
4510
|
-
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4511
|
-
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4512
|
-
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale : this.seatR * 2 *
|
|
4513
|
-
const directHeightPx = shape instanceof Rect ? shape.height() * stageScale : this.seatR * 2 *
|
|
4514
|
-
const assistedDiameterPx = 2 * (this.seatR *
|
|
5873
|
+
const labelWidth = label ? label.width() * stageScale * localPerspectiveScale : 0;
|
|
5874
|
+
const labelHeight = label ? label.height() * (this.viewMode === "perspective" && seat.kind === "booth" ? stageScale : effectiveScale) * localPerspectiveScale : 0;
|
|
5875
|
+
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
5876
|
+
const directHeightPx = shape instanceof Rect ? shape.height() * (this.viewMode === "perspective" ? stageScale : effectiveScale) * localPerspectiveScale : this.seatR * 2 * seatEffectiveScale;
|
|
5877
|
+
const assistedDiameterPx = 2 * (this.seatR * seatEffectiveScale + SEAT_TAP_SLOP_PX);
|
|
4515
5878
|
const fill = shape?.fill();
|
|
4516
5879
|
const ink = label?.fill();
|
|
5880
|
+
const accessGlyph = this.accessGlyphById.get(seat.id);
|
|
5881
|
+
const accessGlyphScale = accessGlyph?.getAbsoluteScale();
|
|
4517
5882
|
return {
|
|
4518
5883
|
seatId: seat.id,
|
|
4519
5884
|
label: seat.label,
|
|
4520
5885
|
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
5886
|
+
markerShape: seat.kind === "booth" ? "booth" : seat.wheelchairSpaceType === "no-seat" ? "square" : "circle",
|
|
5887
|
+
...seat.wheelchairSpaceType ? { wheelchairSpaceType: seat.wheelchairSpaceType } : {},
|
|
4521
5888
|
categoryKey: seat.categoryKey,
|
|
4522
5889
|
...section ? { sectionId: section.id } : {},
|
|
4523
5890
|
...section?.zone ? { zoneId: section.zone } : {},
|
|
@@ -4528,8 +5895,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4528
5895
|
fill: typeof fill === "string" ? fill : "",
|
|
4529
5896
|
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4530
5897
|
opacity: rounded(opacity),
|
|
5898
|
+
...accessGlyph ? {
|
|
5899
|
+
accessibilityMarker: {
|
|
5900
|
+
glyphVisible: accessGlyph.isVisible(),
|
|
5901
|
+
glyphWidthPx: rounded(ACCESS_GLYPH_VIEWBOX * Math.abs(accessGlyphScale?.x ?? 0)),
|
|
5902
|
+
emphasizedByFilter: this.accessGlyphFilterEmphasized(seat.id)
|
|
5903
|
+
}
|
|
5904
|
+
} : {},
|
|
4531
5905
|
pointerTarget: {
|
|
4532
|
-
active: !this.cached && this.isSelectable(seat.id),
|
|
5906
|
+
active: !this.cached && !this.seatViewportCulled(seat.id) && Boolean(shape?.isVisible()) && Boolean(shape?.isListening()) && this.isSelectable(seat.id),
|
|
4533
5907
|
directWidthPx: rounded(directWidthPx),
|
|
4534
5908
|
directHeightPx: rounded(directHeightPx),
|
|
4535
5909
|
effectiveMinimumPx: rounded(Math.max(
|
|
@@ -4557,7 +5931,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4557
5931
|
node.height(),
|
|
4558
5932
|
node.rotation()
|
|
4559
5933
|
);
|
|
4560
|
-
const
|
|
5934
|
+
const lift = section && section.liftWorld > 0 ? this.isoLiftLocal(section.liftWorld) : { x: 0, y: 0 };
|
|
5935
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen({
|
|
5936
|
+
x: corner.x + lift.x,
|
|
5937
|
+
y: corner.y + lift.y
|
|
5938
|
+
})));
|
|
4561
5939
|
const opacity = rounded(node.opacity());
|
|
4562
5940
|
const ink = node.fill();
|
|
4563
5941
|
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
@@ -4696,6 +6074,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4696
6074
|
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4697
6075
|
return {
|
|
4698
6076
|
viewport,
|
|
6077
|
+
projection: this.viewMode,
|
|
6078
|
+
...this.viewMode === "perspective" ? {
|
|
6079
|
+
perspective: {
|
|
6080
|
+
model: "pinhole-exact-seat-anchors",
|
|
6081
|
+
sectionSurfaceModel: "tangent-plane",
|
|
6082
|
+
exactSeatAnchorCount: this.perspectiveSeatProjected.size,
|
|
6083
|
+
depthSorted: true
|
|
6084
|
+
}
|
|
6085
|
+
} : {},
|
|
4699
6086
|
canvasBackground: this.canvasBackground,
|
|
4700
6087
|
effectiveScale: rounded(effectiveScale),
|
|
4701
6088
|
rung: this.getRung(),
|
|
@@ -4750,10 +6137,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4750
6137
|
let cy = viewCentre.y;
|
|
4751
6138
|
if (rung === "sections" && this.sections.length > 0) {
|
|
4752
6139
|
const sectionCentres = this.sections.map((section) => {
|
|
4753
|
-
const
|
|
6140
|
+
const bounds2 = polyBounds(section.outline);
|
|
4754
6141
|
return {
|
|
4755
|
-
x:
|
|
4756
|
-
y:
|
|
6142
|
+
x: bounds2.x + bounds2.width / 2,
|
|
6143
|
+
y: bounds2.y + bounds2.height / 2
|
|
4757
6144
|
};
|
|
4758
6145
|
});
|
|
4759
6146
|
const halfWidth = w / (target * 2);
|
|
@@ -4820,6 +6207,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4820
6207
|
}
|
|
4821
6208
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
4822
6209
|
afterViewChange() {
|
|
6210
|
+
this.updateSeatGroupVisibility();
|
|
4823
6211
|
this.updateLOD();
|
|
4824
6212
|
this.updateFreeTextVisibility();
|
|
4825
6213
|
this.updateLabels();
|
|
@@ -4844,10 +6232,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4844
6232
|
this.paintGAStateForView();
|
|
4845
6233
|
this.updateAccessGlyphs(scale);
|
|
4846
6234
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
6235
|
+
const suppressPerspectiveSeatCache = this.viewMode === "perspective" && this.hasSections && this.seats.length > 2500 && shouldCache;
|
|
6236
|
+
if (suppressPerspectiveSeatCache) {
|
|
6237
|
+
this.seatLayer.listening(false);
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
4847
6240
|
if (shouldCache && !this.cached) {
|
|
4848
6241
|
this.cacheSeatLayer();
|
|
4849
|
-
} else if (!shouldCache && this.cached) {
|
|
4850
|
-
this.seatLayer.clearCache();
|
|
6242
|
+
} else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
|
|
6243
|
+
if (this.cached) this.seatLayer.clearCache();
|
|
4851
6244
|
this.seatLayer.listening(true);
|
|
4852
6245
|
this.cached = false;
|
|
4853
6246
|
this.seatLayer.batchDraw();
|
|
@@ -4856,6 +6249,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4856
6249
|
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
4857
6250
|
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
4858
6251
|
rebuildSeatCache() {
|
|
6252
|
+
this.updateSeatGroupVisibility();
|
|
4859
6253
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
4860
6254
|
this.seatLayer.clearCache();
|
|
4861
6255
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
@@ -4907,31 +6301,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4907
6301
|
for (const [id, label] of this.boothLabelById) {
|
|
4908
6302
|
const shape = this.circleById.get(id);
|
|
4909
6303
|
label.visible(
|
|
4910
|
-
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.
|
|
6304
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && Boolean(shape?.isVisible()) && this.focusedSeatOpacity(id, shape?.getAbsoluteOpacity() ?? 1) >= 0.5
|
|
4911
6305
|
);
|
|
4912
6306
|
}
|
|
4913
6307
|
this.labelGroup.destroyChildren();
|
|
6308
|
+
this.labelLiftGroups.clear();
|
|
4914
6309
|
this.seatLabelById.clear();
|
|
4915
6310
|
if (!show) {
|
|
4916
6311
|
this.overlayLayer.batchDraw();
|
|
4917
6312
|
return;
|
|
4918
6313
|
}
|
|
4919
|
-
const s = this.stage.scaleX();
|
|
4920
|
-
const x0 = -this.stage.x() / s;
|
|
4921
|
-
const y0 = -this.stage.y() / s;
|
|
4922
|
-
const x1 = (this.stage.width() - this.stage.x()) / s;
|
|
4923
|
-
const y1 = (this.stage.height() - this.stage.y()) / s;
|
|
4924
6314
|
let count = 0;
|
|
4925
|
-
for (const seat of this.
|
|
6315
|
+
for (const seat of this.visibleSeatCandidates()) {
|
|
4926
6316
|
if (seat.kind === "booth") continue;
|
|
4927
|
-
|
|
4928
|
-
if (
|
|
6317
|
+
const screen = this.worldToScreen(seat);
|
|
6318
|
+
if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
|
|
6319
|
+
if (this.accessGlyphById.has(seat.id) && this.accessGlyphShouldShow(seat.id)) continue;
|
|
4929
6320
|
const shape = this.circleById.get(seat.id);
|
|
4930
|
-
if (
|
|
6321
|
+
if (!shape?.isVisible() || this.focusedSeatOpacity(seat.id, shape.getAbsoluteOpacity()) < 0.5) continue;
|
|
4931
6322
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
6323
|
+
const labelPoint = this.renderedSeatPoint(seat);
|
|
6324
|
+
const perspectiveScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seat.id) ?? 1 : 1;
|
|
4932
6325
|
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
4933
6326
|
if (unavailable) {
|
|
4934
|
-
const cue = new Group({ x:
|
|
6327
|
+
const cue = new Group({ x: labelPoint.x, y: labelPoint.y, listening: false });
|
|
6328
|
+
cue.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
4935
6329
|
if (status === "held") {
|
|
4936
6330
|
cue.add(new Rect({
|
|
4937
6331
|
x: -4.2,
|
|
@@ -4960,14 +6354,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4960
6354
|
listening: false
|
|
4961
6355
|
}));
|
|
4962
6356
|
}
|
|
4963
|
-
this.
|
|
6357
|
+
this.seatLabelContainer(seat.id).add(cue);
|
|
4964
6358
|
if (++count >= MAX_LABELS) break;
|
|
4965
6359
|
continue;
|
|
4966
6360
|
}
|
|
4967
6361
|
const authoredFontSize = SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4968
6362
|
const t2 = new Text({
|
|
4969
|
-
x:
|
|
4970
|
-
y:
|
|
6363
|
+
x: labelPoint.x,
|
|
6364
|
+
y: labelPoint.y,
|
|
4971
6365
|
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4972
6366
|
fontSize: authoredFontSize,
|
|
4973
6367
|
fontStyle: "600",
|
|
@@ -4988,11 +6382,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4988
6382
|
}
|
|
4989
6383
|
t2.offsetX(t2.width() / 2);
|
|
4990
6384
|
t2.offsetY(t2.height() / 2);
|
|
4991
|
-
|
|
6385
|
+
t2.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
6386
|
+
this.seatLabelContainer(seat.id).add(t2);
|
|
4992
6387
|
this.seatLabelById.set(seat.id, t2);
|
|
4993
6388
|
if (++count >= MAX_LABELS) break;
|
|
4994
6389
|
}
|
|
4995
|
-
if (this.isoT > 0) this.applyUprightLabels();
|
|
6390
|
+
if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
|
|
4996
6391
|
this.overlayLayer.batchDraw();
|
|
4997
6392
|
}
|
|
4998
6393
|
handleResize() {
|
|
@@ -5058,6 +6453,7 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
5058
6453
|
|
|
5059
6454
|
// src/core/bestAvailable.ts
|
|
5060
6455
|
function seatIndex2(seat, fallback) {
|
|
6456
|
+
if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
|
|
5061
6457
|
const i = seat.id.lastIndexOf(":");
|
|
5062
6458
|
if (i < 0) return fallback;
|
|
5063
6459
|
const n = Number(seat.id.slice(i + 1));
|
|
@@ -5077,21 +6473,25 @@ function centroid(seats) {
|
|
|
5077
6473
|
}
|
|
5078
6474
|
return { x: x / seats.length, y: y / seats.length };
|
|
5079
6475
|
}
|
|
6476
|
+
function candidateFocal(seats, fallback) {
|
|
6477
|
+
return seats.find((seat) => seat.focalPoint)?.focalPoint ?? fallback;
|
|
6478
|
+
}
|
|
5080
6479
|
function isPremium(seat) {
|
|
5081
6480
|
return seat.commercial?.premium === true;
|
|
5082
6481
|
}
|
|
5083
6482
|
function pickBestAvailable(seats, available, opts) {
|
|
5084
6483
|
const qty = Math.floor(opts.qty);
|
|
5085
6484
|
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
5086
|
-
const { categoryKey, focal, preferPremium } = opts;
|
|
6485
|
+
const { categoryKey, zoneId, focal, preferPremium } = opts;
|
|
5087
6486
|
const rows = /* @__PURE__ */ new Map();
|
|
5088
6487
|
const eligibleAll = [];
|
|
5089
6488
|
seats.forEach((seat, i) => {
|
|
5090
|
-
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
5091
|
-
|
|
6489
|
+
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey) && (!zoneId || seat.zoneId === zoneId);
|
|
6490
|
+
const rowId = seat.logicalRowId ?? seat.rowId;
|
|
6491
|
+
let arr = rows.get(rowId);
|
|
5092
6492
|
if (!arr) {
|
|
5093
6493
|
arr = [];
|
|
5094
|
-
rows.set(
|
|
6494
|
+
rows.set(rowId, arr);
|
|
5095
6495
|
}
|
|
5096
6496
|
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
5097
6497
|
if (elig) eligibleAll.push(seat);
|
|
@@ -5116,7 +6516,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5116
6516
|
continue;
|
|
5117
6517
|
}
|
|
5118
6518
|
let j = i;
|
|
5119
|
-
while (j < slots.length && slots[j].elig) j++;
|
|
6519
|
+
while (j < slots.length && slots[j].elig && (j === i || slots[j].index === slots[j - 1].index + 1)) j++;
|
|
5120
6520
|
const segLen = j - i;
|
|
5121
6521
|
for (let p = 0; p + qty <= segLen; p++) {
|
|
5122
6522
|
const leftRem = p;
|
|
@@ -5130,7 +6530,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5130
6530
|
startIndex: slots[i + p].index,
|
|
5131
6531
|
orphan,
|
|
5132
6532
|
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
5133
|
-
d2: dist2(centroid(runSeats), focal)
|
|
6533
|
+
d2: dist2(centroid(runSeats), candidateFocal(runSeats, focal))
|
|
5134
6534
|
};
|
|
5135
6535
|
if (better(c)) best = c;
|
|
5136
6536
|
}
|
|
@@ -5139,20 +6539,71 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
5139
6539
|
}
|
|
5140
6540
|
if (best) return { labels: best.labels };
|
|
5141
6541
|
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
5142
|
-
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
|
|
6542
|
+
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, seat.focalPoint ?? focal) })).sort((a, b) => {
|
|
5143
6543
|
if (preferPremium) {
|
|
5144
6544
|
const pa = isPremium(a.seat) ? 0 : 1;
|
|
5145
6545
|
const pb = isPremium(b.seat) ? 0 : 1;
|
|
5146
6546
|
if (pa !== pb) return pa - pb;
|
|
5147
6547
|
}
|
|
5148
6548
|
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
5149
|
-
const r = a.seat.rowId.localeCompare(b.seat.rowId);
|
|
6549
|
+
const r = (a.seat.logicalRowId ?? a.seat.rowId).localeCompare(b.seat.logicalRowId ?? b.seat.rowId);
|
|
5150
6550
|
if (r !== 0) return r;
|
|
5151
6551
|
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
5152
6552
|
});
|
|
5153
6553
|
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
5154
6554
|
}
|
|
5155
6555
|
|
|
6556
|
+
// src/core/tableInventory.ts
|
|
6557
|
+
var GroupedTableProjectionError = class extends Error {
|
|
6558
|
+
constructor(objectId, message) {
|
|
6559
|
+
super(message);
|
|
6560
|
+
this.objectId = objectId;
|
|
6561
|
+
this.name = "GroupedTableProjectionError";
|
|
6562
|
+
}
|
|
6563
|
+
};
|
|
6564
|
+
function bounds(table) {
|
|
6565
|
+
const min = table.minOccupancy;
|
|
6566
|
+
const max = table.maxOccupancy;
|
|
6567
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > table.seatCount) {
|
|
6568
|
+
throw new GroupedTableProjectionError(
|
|
6569
|
+
table.id,
|
|
6570
|
+
`Table "${table.label}" has invalid variable-occupancy bounds`
|
|
6571
|
+
);
|
|
6572
|
+
}
|
|
6573
|
+
return { min, max };
|
|
6574
|
+
}
|
|
6575
|
+
function groupedTableInventory(doc, modelVersion) {
|
|
6576
|
+
if (modelVersion !== 2) return [];
|
|
6577
|
+
const units = [];
|
|
6578
|
+
for (const object of allObjects(doc)) {
|
|
6579
|
+
if (object.type !== "table" || !object.bookAsWhole && !object.variableOccupancy) continue;
|
|
6580
|
+
if (object.bookAsWhole && object.variableOccupancy) {
|
|
6581
|
+
throw new GroupedTableProjectionError(
|
|
6582
|
+
object.id,
|
|
6583
|
+
`Table "${object.label}" cannot be whole-table and variable-occupancy inventory`
|
|
6584
|
+
);
|
|
6585
|
+
}
|
|
6586
|
+
if (!Number.isInteger(object.seatCount) || object.seatCount < 1 || !object.label.trim()) {
|
|
6587
|
+
throw new GroupedTableProjectionError(object.id, "Grouped tables require a label and at least one chair");
|
|
6588
|
+
}
|
|
6589
|
+
const mode = object.variableOccupancy ? "variable" : "whole";
|
|
6590
|
+
const occupancy = mode === "variable" ? bounds(object) : { min: object.seatCount, max: object.seatCount };
|
|
6591
|
+
units.push({
|
|
6592
|
+
objectId: object.id,
|
|
6593
|
+
label: object.label,
|
|
6594
|
+
...object.displayLabel ? { displayLabel: object.displayLabel } : {},
|
|
6595
|
+
...object.displayType ? { displayType: object.displayType } : {},
|
|
6596
|
+
categoryKey: object.categoryKey,
|
|
6597
|
+
mode,
|
|
6598
|
+
capacity: object.seatCount,
|
|
6599
|
+
minOccupancy: occupancy.min,
|
|
6600
|
+
maxOccupancy: occupancy.max,
|
|
6601
|
+
chairs: expandTable(object)
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
return units;
|
|
6605
|
+
}
|
|
6606
|
+
|
|
5156
6607
|
// src/picker/PickerController.ts
|
|
5157
6608
|
var DEFAULT_MAX_SELECTION = 10;
|
|
5158
6609
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -5184,6 +6635,14 @@ var PickerController = class {
|
|
|
5184
6635
|
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
5185
6636
|
this.seatContext = /* @__PURE__ */ new Map();
|
|
5186
6637
|
this.allIds = [];
|
|
6638
|
+
/** Model-2 tables are one booking unit whose chairs remain renderer geometry. */
|
|
6639
|
+
this.groupedTablesByObject = /* @__PURE__ */ new Map();
|
|
6640
|
+
this.groupedTablesByLabel = /* @__PURE__ */ new Map();
|
|
6641
|
+
this.groupedTableBySeatId = /* @__PURE__ */ new Map();
|
|
6642
|
+
this.tableQuantities = /* @__PURE__ */ new Map();
|
|
6643
|
+
/** Variable quantity is a buyer contract, never an inferred default. */
|
|
6644
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
6645
|
+
this.groupedSelectionOverhead = 0;
|
|
5187
6646
|
// realtime socket
|
|
5188
6647
|
this.ws = null;
|
|
5189
6648
|
this.reconnectTimer = null;
|
|
@@ -5245,6 +6704,20 @@ var PickerController = class {
|
|
|
5245
6704
|
idForLabel(label) {
|
|
5246
6705
|
return this.labelToId.get(label);
|
|
5247
6706
|
}
|
|
6707
|
+
idsForLabel(label) {
|
|
6708
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
6709
|
+
if (table) return table.chairs.map((chair) => chair.id);
|
|
6710
|
+
const id = this.labelToId.get(label);
|
|
6711
|
+
return id ? [id] : [];
|
|
6712
|
+
}
|
|
6713
|
+
idsForLabels(labels) {
|
|
6714
|
+
return [...new Set([...labels].flatMap((label) => this.idsForLabel(label)))];
|
|
6715
|
+
}
|
|
6716
|
+
/** Resolve a physical chair id (or table inventory label) to its table unit. */
|
|
6717
|
+
tableSelection(seatIdOrLabel) {
|
|
6718
|
+
const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
|
|
6719
|
+
return group ? this.toTableSeat(group) : null;
|
|
6720
|
+
}
|
|
5248
6721
|
/** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
|
|
5249
6722
|
async render(host) {
|
|
5250
6723
|
if (this.renderer) return null;
|
|
@@ -5258,6 +6731,29 @@ var PickerController = class {
|
|
|
5258
6731
|
}
|
|
5259
6732
|
if (this.closed) return null;
|
|
5260
6733
|
this._doc = res.doc;
|
|
6734
|
+
try {
|
|
6735
|
+
const groups = groupedTableInventory(
|
|
6736
|
+
res.doc,
|
|
6737
|
+
res.event.inventoryModelVersion === 2 ? 2 : 1
|
|
6738
|
+
);
|
|
6739
|
+
this.groupedTablesByObject = new Map(groups.map((group) => [group.objectId, group]));
|
|
6740
|
+
this.groupedTablesByLabel = new Map(groups.map((group) => [group.label, group]));
|
|
6741
|
+
this.groupedTableBySeatId = new Map(
|
|
6742
|
+
groups.flatMap((group) => group.chairs.map((chair) => [chair.id, group]))
|
|
6743
|
+
);
|
|
6744
|
+
this.tableQuantities = new Map(groups.map((group) => [
|
|
6745
|
+
group.label,
|
|
6746
|
+
group.mode === "whole" ? group.capacity : group.minOccupancy
|
|
6747
|
+
]));
|
|
6748
|
+
this.confirmedVariableTables = /* @__PURE__ */ new Set();
|
|
6749
|
+
this.groupedSelectionOverhead = groups.reduce(
|
|
6750
|
+
(sum, group) => sum + Math.max(0, group.chairs.length - 1),
|
|
6751
|
+
0
|
|
6752
|
+
);
|
|
6753
|
+
} catch (err) {
|
|
6754
|
+
this.emitError(err);
|
|
6755
|
+
return null;
|
|
6756
|
+
}
|
|
5261
6757
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
5262
6758
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
5263
6759
|
this.seatById = /* @__PURE__ */ new Map();
|
|
@@ -5275,32 +6771,43 @@ var PickerController = class {
|
|
|
5275
6771
|
this.allIds.push(s.id);
|
|
5276
6772
|
const source = chartObjects.get(s.rowId);
|
|
5277
6773
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
5278
|
-
const
|
|
5279
|
-
const
|
|
5280
|
-
const
|
|
6774
|
+
const logicalRow = source?.type === "row" ? source.segmentedRow : void 0;
|
|
6775
|
+
const sourceDisplayLabel = logicalRow?.displayLabel ?? (source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel);
|
|
6776
|
+
const groupedTable = this.groupedTablesByObject.get(s.rowId);
|
|
6777
|
+
const objectType = source?.type === "table" ? "table" : source?.type === "booth" ? "booth" : "seat";
|
|
6778
|
+
const rowLabel = sourceDisplayLabel;
|
|
6779
|
+
const rowType = logicalRow?.displayType?.trim() || (source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0);
|
|
5281
6780
|
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
5282
6781
|
const labelParts = visibleSeatLabel.split("-");
|
|
5283
|
-
const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) :
|
|
6782
|
+
const seatNumber = groupedTable ? void 0 : s.kind === "booth" ? void 0 : sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
|
|
5284
6783
|
this.seatContext.set(s.id, {
|
|
6784
|
+
objectId: s.rowId,
|
|
6785
|
+
objectType,
|
|
5285
6786
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
5286
6787
|
rowLabel,
|
|
5287
6788
|
seatNumber,
|
|
5288
|
-
rowType
|
|
6789
|
+
...rowType ? { displayType: rowType, rowType } : {}
|
|
5289
6790
|
});
|
|
5290
6791
|
}
|
|
6792
|
+
for (const group of this.groupedTablesByLabel.values()) {
|
|
6793
|
+
const representative = group.chairs[0];
|
|
6794
|
+
if (!representative) continue;
|
|
6795
|
+
this.labelToId.set(group.label, representative.id);
|
|
6796
|
+
this.labelToSeat.set(group.label, representative);
|
|
6797
|
+
}
|
|
5291
6798
|
const currency = res.event.currency ?? this.opts.currency;
|
|
5292
6799
|
this.currency = currency ?? "USD";
|
|
5293
6800
|
const renderer = createRenderer(host, {
|
|
5294
|
-
|
|
6801
|
+
// Group chairs are selected together for paint, but count as one logical
|
|
6802
|
+
// inventory line. The controller enforces the guest-weighted cap.
|
|
6803
|
+
maxSelection: this.rendererSelectionCap(),
|
|
5295
6804
|
confirmSelection: this.opts.confirmSelection,
|
|
5296
6805
|
currency,
|
|
5297
6806
|
onSelect: (seat) => {
|
|
5298
|
-
this.
|
|
5299
|
-
this.emitSelectionChange();
|
|
6807
|
+
this.handleRendererSelect(seat);
|
|
5300
6808
|
},
|
|
5301
6809
|
onDeselect: (seat) => {
|
|
5302
|
-
this.
|
|
5303
|
-
this.emitSelectionChange();
|
|
6810
|
+
this.handleRendererDeselect(seat);
|
|
5304
6811
|
},
|
|
5305
6812
|
onSelectionLimit: this.opts.onSelectionLimit,
|
|
5306
6813
|
onHover: (seat) => {
|
|
@@ -5355,7 +6862,20 @@ var PickerController = class {
|
|
|
5355
6862
|
// ---- selection ------------------------------------------------------------
|
|
5356
6863
|
getSelection() {
|
|
5357
6864
|
if (!this.renderer) return [];
|
|
5358
|
-
|
|
6865
|
+
const out = [];
|
|
6866
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
6867
|
+
for (const seat of this.renderer.getSelection()) {
|
|
6868
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
6869
|
+
if (table) {
|
|
6870
|
+
if (!grouped.has(table.label)) {
|
|
6871
|
+
grouped.add(table.label);
|
|
6872
|
+
out.push(this.toTableSeat(table));
|
|
6873
|
+
}
|
|
6874
|
+
} else {
|
|
6875
|
+
out.push(this.toSeat(seat));
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return out;
|
|
5359
6879
|
}
|
|
5360
6880
|
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
5361
6881
|
seatDetails(seatId) {
|
|
@@ -5364,20 +6884,168 @@ var PickerController = class {
|
|
|
5364
6884
|
}
|
|
5365
6885
|
clearSelection() {
|
|
5366
6886
|
this.renderer?.clearSelection();
|
|
6887
|
+
this.resetUnheldTableQuantities();
|
|
5367
6888
|
this.emitSelectionChange();
|
|
5368
6889
|
}
|
|
5369
6890
|
deselect(ids) {
|
|
5370
|
-
|
|
6891
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
6892
|
+
for (const id of ids) {
|
|
6893
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
6894
|
+
if (table) {
|
|
6895
|
+
table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
6896
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
6897
|
+
this.tableQuantities.set(
|
|
6898
|
+
table.label,
|
|
6899
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
6900
|
+
);
|
|
6901
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
6902
|
+
}
|
|
6903
|
+
} else expanded.add(id);
|
|
6904
|
+
}
|
|
6905
|
+
this.renderer?.deselect([...expanded]);
|
|
5371
6906
|
this.emitSelectionChange();
|
|
5372
6907
|
}
|
|
5373
6908
|
setMaxSelection(maxSelection) {
|
|
5374
6909
|
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
5375
|
-
this.renderer?.setMaxSelection?.(this.
|
|
6910
|
+
this.renderer?.setMaxSelection?.(this.rendererSelectionCap());
|
|
5376
6911
|
}
|
|
5377
6912
|
select(ids) {
|
|
5378
|
-
const
|
|
6913
|
+
const before = new Set(this.getSelection().map((seat) => seat.label));
|
|
6914
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
6915
|
+
for (const id of ids) {
|
|
6916
|
+
const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
|
|
6917
|
+
if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
|
|
6918
|
+
else expanded.add(id);
|
|
6919
|
+
}
|
|
6920
|
+
this.renderer?.select?.([...expanded]);
|
|
6921
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
6922
|
+
this.renderer?.deselect([...expanded]);
|
|
6923
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
6924
|
+
return [];
|
|
6925
|
+
}
|
|
6926
|
+
const added = this.getSelection().filter((seat) => !before.has(seat.label));
|
|
5379
6927
|
if (added.length) this.emitSelectionChange();
|
|
5380
|
-
return added
|
|
6928
|
+
return added;
|
|
6929
|
+
}
|
|
6930
|
+
/** Confirm/update the guest quantity for a selected variable table. */
|
|
6931
|
+
setTableQuantity(label, quantity) {
|
|
6932
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
6933
|
+
if (!table) return false;
|
|
6934
|
+
const q = table.mode === "whole" ? table.capacity : Math.floor(quantity);
|
|
6935
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return false;
|
|
6936
|
+
const previous = this.tableQuantities.get(label) ?? table.minOccupancy;
|
|
6937
|
+
this.tableQuantities.set(label, q);
|
|
6938
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
6939
|
+
this.tableQuantities.set(label, previous);
|
|
6940
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
6941
|
+
return false;
|
|
6942
|
+
}
|
|
6943
|
+
if (table.mode === "variable") this.confirmedVariableTables.add(label);
|
|
6944
|
+
this.emitSelectionChange();
|
|
6945
|
+
return true;
|
|
6946
|
+
}
|
|
6947
|
+
/** Atomically replace an active variable-table hold with a new guest count. */
|
|
6948
|
+
async replaceTableQuantity(label, quantity) {
|
|
6949
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
6950
|
+
const hold = this.hold_;
|
|
6951
|
+
if (!table || table.mode !== "variable" || !hold?.labels.includes(label)) return null;
|
|
6952
|
+
const q = Math.floor(quantity);
|
|
6953
|
+
if (!Number.isInteger(q) || q < table.minOccupancy || q > table.maxOccupancy) return null;
|
|
6954
|
+
const otherGuests = (hold.items ?? []).filter((item) => item.label !== label).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
6955
|
+
if (otherGuests + q > this.maxSelection) {
|
|
6956
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
6957
|
+
return null;
|
|
6958
|
+
}
|
|
6959
|
+
const selections = (hold.items ?? []).map((item) => ({
|
|
6960
|
+
label: item.label,
|
|
6961
|
+
tierId: item.tierId,
|
|
6962
|
+
quantity: item.label === label ? q : item.quantity
|
|
6963
|
+
}));
|
|
6964
|
+
if (!selections.some((item) => item.label === label)) return null;
|
|
6965
|
+
const result = await this.api.hold(this.key, selections, void 0, hold.holdId);
|
|
6966
|
+
this.tableQuantities.set(label, q);
|
|
6967
|
+
this.setHold({
|
|
6968
|
+
holdId: result.holdId,
|
|
6969
|
+
labels: selections.map((item) => item.label),
|
|
6970
|
+
expiresAt: result.expiresAt,
|
|
6971
|
+
items: result.items
|
|
6972
|
+
});
|
|
6973
|
+
return this.hold_;
|
|
6974
|
+
}
|
|
6975
|
+
rendererSelectionCap() {
|
|
6976
|
+
return this.maxSelection + this.groupedSelectionOverhead;
|
|
6977
|
+
}
|
|
6978
|
+
selectionGuestCount() {
|
|
6979
|
+
return this.getSelection().reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
|
|
6980
|
+
}
|
|
6981
|
+
handleRendererSelect(seat) {
|
|
6982
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
6983
|
+
if (table) {
|
|
6984
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
6985
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
6986
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
6987
|
+
this.emitSelectionChange();
|
|
6988
|
+
return;
|
|
6989
|
+
}
|
|
6990
|
+
this.renderer?.select?.(table.chairs.map((chair) => chair.id));
|
|
6991
|
+
this.opts.onSelect?.(table.chairs[0] ?? seat);
|
|
6992
|
+
this.opts.onTableSelectionRequest?.(this.toTableSeat(table));
|
|
6993
|
+
this.emitSelectionChange();
|
|
6994
|
+
return;
|
|
6995
|
+
}
|
|
6996
|
+
if (this.selectionGuestCount() > this.maxSelection) {
|
|
6997
|
+
this.renderer?.deselect([seat.id]);
|
|
6998
|
+
this.opts.onSelectionLimit?.(this.maxSelection);
|
|
6999
|
+
this.emitSelectionChange();
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
this.opts.onSelect?.(seat);
|
|
7003
|
+
this.emitSelectionChange();
|
|
7004
|
+
}
|
|
7005
|
+
handleRendererDeselect(seat) {
|
|
7006
|
+
const table = this.groupedTableBySeatId.get(seat.id);
|
|
7007
|
+
if (table) {
|
|
7008
|
+
this.renderer?.deselect(table.chairs.map((chair) => chair.id));
|
|
7009
|
+
if (!this.hold_?.labels.includes(table.label)) {
|
|
7010
|
+
this.tableQuantities.set(
|
|
7011
|
+
table.label,
|
|
7012
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7013
|
+
);
|
|
7014
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7015
|
+
}
|
|
7016
|
+
this.opts.onDeselect?.(table.chairs[0] ?? seat);
|
|
7017
|
+
} else {
|
|
7018
|
+
this.opts.onDeselect?.(seat);
|
|
7019
|
+
}
|
|
7020
|
+
this.emitSelectionChange();
|
|
7021
|
+
}
|
|
7022
|
+
resetUnheldTableQuantities() {
|
|
7023
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7024
|
+
if (this.hold_?.labels.includes(table.label)) continue;
|
|
7025
|
+
this.tableQuantities.set(
|
|
7026
|
+
table.label,
|
|
7027
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7028
|
+
);
|
|
7029
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(table.label);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
resetTableQuantitiesForLabels(labels) {
|
|
7033
|
+
for (const label of labels) {
|
|
7034
|
+
const table = this.groupedTablesByLabel.get(label);
|
|
7035
|
+
if (!table) continue;
|
|
7036
|
+
this.tableQuantities.set(
|
|
7037
|
+
label,
|
|
7038
|
+
table.mode === "whole" ? table.capacity : table.minOccupancy
|
|
7039
|
+
);
|
|
7040
|
+
if (table.mode === "variable") this.confirmedVariableTables.delete(label);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
tableQuantityRequest(seat) {
|
|
7044
|
+
if (seat?.objectType !== "table") return {};
|
|
7045
|
+
if (seat.bookingMode === "variable" && !this.confirmedVariableTables.has(seat.label)) {
|
|
7046
|
+
throw new Error("picker: confirm a variable table guest quantity before holding");
|
|
7047
|
+
}
|
|
7048
|
+
return { quantity: seat.quantity };
|
|
5381
7049
|
}
|
|
5382
7050
|
// ---- booking machine ------------------------------------------------------
|
|
5383
7051
|
/**
|
|
@@ -5389,7 +7057,7 @@ var PickerController = class {
|
|
|
5389
7057
|
async hold(labelsArg, ttlMs) {
|
|
5390
7058
|
const r = this.renderer;
|
|
5391
7059
|
if (!r) return null;
|
|
5392
|
-
const labels = labelsArg ??
|
|
7060
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5393
7061
|
const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
|
|
5394
7062
|
const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
|
|
5395
7063
|
if (!combinedLabels.length) return null;
|
|
@@ -5397,10 +7065,14 @@ var PickerController = class {
|
|
|
5397
7065
|
try {
|
|
5398
7066
|
const selections = combinedLabels.map((label) => {
|
|
5399
7067
|
const ga = existingGA.find((item) => item.label === label);
|
|
5400
|
-
if (ga) return { label, tierId: ga.tierId };
|
|
7068
|
+
if (ga) return { label, tierId: ga.tierId, quantity: ga.quantity };
|
|
5401
7069
|
const seat = this.labelToSeat.get(label);
|
|
5402
7070
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5403
|
-
return {
|
|
7071
|
+
return {
|
|
7072
|
+
label,
|
|
7073
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7074
|
+
...this.tableQuantityRequest(resolved)
|
|
7075
|
+
};
|
|
5404
7076
|
});
|
|
5405
7077
|
const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
|
|
5406
7078
|
this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
|
|
@@ -5452,11 +7124,19 @@ var PickerController = class {
|
|
|
5452
7124
|
const out = {};
|
|
5453
7125
|
const closedMembers = this.closedMemberIds();
|
|
5454
7126
|
for (const [id, s] of this.seatById) {
|
|
7127
|
+
if (this.groupedTableBySeatId.has(id)) continue;
|
|
5455
7128
|
if (closedMembers.has(id)) continue;
|
|
5456
7129
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
5457
7130
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
5458
7131
|
}
|
|
5459
7132
|
}
|
|
7133
|
+
for (const table of this.groupedTablesByLabel.values()) {
|
|
7134
|
+
if (table.chairs.some((chair) => closedMembers.has(chair.id))) continue;
|
|
7135
|
+
const representative = table.chairs[0];
|
|
7136
|
+
if (representative && (this.getStatus(representative.id) ?? "free") === "free") {
|
|
7137
|
+
out[table.categoryKey] = (out[table.categoryKey] ?? 0) + table.maxOccupancy;
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
5460
7140
|
return out;
|
|
5461
7141
|
}
|
|
5462
7142
|
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
@@ -5475,6 +7155,8 @@ var PickerController = class {
|
|
|
5475
7155
|
return {
|
|
5476
7156
|
id: area.id,
|
|
5477
7157
|
label: area.label,
|
|
7158
|
+
...area.displayLabel ? { displayLabel: area.displayLabel } : {},
|
|
7159
|
+
...area.displayType ? { displayType: area.displayType } : {},
|
|
5478
7160
|
capacity: Math.max(0, Math.floor(area.capacity)),
|
|
5479
7161
|
available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
|
|
5480
7162
|
categoryKey: area.categoryKey,
|
|
@@ -5493,14 +7175,20 @@ var PickerController = class {
|
|
|
5493
7175
|
const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
|
|
5494
7176
|
if (labels.length !== Math.floor(qty)) return null;
|
|
5495
7177
|
const selections = /* @__PURE__ */ new Map();
|
|
5496
|
-
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7178
|
+
for (const item of this.hold_?.items ?? []) selections.set(item.label, {
|
|
7179
|
+
label: item.label,
|
|
7180
|
+
tierId: item.tierId,
|
|
7181
|
+
quantity: item.quantity
|
|
7182
|
+
});
|
|
5497
7183
|
if (this.hold_ && !this.hold_?.items?.length) {
|
|
5498
7184
|
for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
|
|
5499
7185
|
}
|
|
5500
|
-
for (const seat of this.
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
7186
|
+
for (const seat of this.getSelection()) {
|
|
7187
|
+
selections.set(seat.label, {
|
|
7188
|
+
label: seat.label,
|
|
7189
|
+
...seat.tierId ? { tierId: seat.tierId } : {},
|
|
7190
|
+
...this.tableQuantityRequest(seat)
|
|
7191
|
+
});
|
|
5504
7192
|
}
|
|
5505
7193
|
for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
|
|
5506
7194
|
const combined = [...selections.values()];
|
|
@@ -5517,6 +7205,15 @@ var PickerController = class {
|
|
|
5517
7205
|
}
|
|
5518
7206
|
return false;
|
|
5519
7207
|
}
|
|
7208
|
+
/** Zones which still own visible buyer inventory, in authored order. */
|
|
7209
|
+
getBestAvailableZones() {
|
|
7210
|
+
const doc = this.visibleDoc();
|
|
7211
|
+
if (!doc?.zones?.length) return [];
|
|
7212
|
+
const used = new Set(
|
|
7213
|
+
computeSections(doc).sections.filter((section) => section.seatCount > 0 && !!section.zone).map((section) => section.zone)
|
|
7214
|
+
);
|
|
7215
|
+
return doc.zones.filter((zone) => used.has(zone.id)).map((zone) => ({ id: zone.id, label: zone.label }));
|
|
7216
|
+
}
|
|
5520
7217
|
/**
|
|
5521
7218
|
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5522
7219
|
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
@@ -5525,11 +7222,12 @@ var PickerController = class {
|
|
|
5525
7222
|
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5526
7223
|
* exists (the caller then falls back to the normal server pick).
|
|
5527
7224
|
*/
|
|
5528
|
-
pickPremiumBlock(qty, categoryKey) {
|
|
7225
|
+
pickPremiumBlock(qty, categoryKey, zoneId) {
|
|
5529
7226
|
const doc = this.visibleDoc();
|
|
5530
7227
|
if (!doc?.objects?.length) return null;
|
|
5531
7228
|
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5532
|
-
const
|
|
7229
|
+
const groupedObjectIds = new Set(this.groupedTablesByObject.keys());
|
|
7230
|
+
const seats = expandChart(doc).filter((seat) => !groupedObjectIds.has(seat.rowId));
|
|
5533
7231
|
const held = new Set(this.hold_?.labels ?? []);
|
|
5534
7232
|
const available = /* @__PURE__ */ new Set();
|
|
5535
7233
|
for (const seat of seats) {
|
|
@@ -5538,7 +7236,7 @@ var PickerController = class {
|
|
|
5538
7236
|
const status = id ? this.getStatus(id) : void 0;
|
|
5539
7237
|
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5540
7238
|
}
|
|
5541
|
-
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
7239
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, zoneId, focal, preferPremium: true });
|
|
5542
7240
|
if (pick.labels.length !== qty) return null;
|
|
5543
7241
|
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5544
7242
|
return allPremium ? [...pick.labels] : null;
|
|
@@ -5553,7 +7251,7 @@ var PickerController = class {
|
|
|
5553
7251
|
const r = this.renderer;
|
|
5554
7252
|
if (!r) return null;
|
|
5555
7253
|
if (opts.preferPremium) {
|
|
5556
|
-
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
7254
|
+
const block = this.pickPremiumBlock(qty, categoryKey, opts.zoneId);
|
|
5557
7255
|
if (block) {
|
|
5558
7256
|
if (this.hold_ && !await this.release()) return null;
|
|
5559
7257
|
try {
|
|
@@ -5564,7 +7262,7 @@ var PickerController = class {
|
|
|
5564
7262
|
});
|
|
5565
7263
|
const result = await this.api.hold(this.key, selection);
|
|
5566
7264
|
r.clearSelection();
|
|
5567
|
-
const ids =
|
|
7265
|
+
const ids = this.idsForLabels(block);
|
|
5568
7266
|
if (ids.length) r.setStatus(ids, "held");
|
|
5569
7267
|
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5570
7268
|
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5581,9 +7279,9 @@ var PickerController = class {
|
|
|
5581
7279
|
}
|
|
5582
7280
|
if (this.hold_ && !await this.release()) return null;
|
|
5583
7281
|
try {
|
|
5584
|
-
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
7282
|
+
const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
|
|
5585
7283
|
r.clearSelection();
|
|
5586
|
-
const ids =
|
|
7284
|
+
const ids = this.idsForLabels(result.labels);
|
|
5587
7285
|
if (ids.length) r.setStatus(ids, "held");
|
|
5588
7286
|
this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
|
|
5589
7287
|
const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5607,7 +7305,7 @@ var PickerController = class {
|
|
|
5607
7305
|
const r = this.renderer;
|
|
5608
7306
|
if (!r) return null;
|
|
5609
7307
|
if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
|
|
5610
|
-
const labels = labelsArg ??
|
|
7308
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5611
7309
|
if (!labels.length) return null;
|
|
5612
7310
|
let holdId;
|
|
5613
7311
|
try {
|
|
@@ -5620,7 +7318,11 @@ var PickerController = class {
|
|
|
5620
7318
|
labels.map((label) => {
|
|
5621
7319
|
const seat = this.labelToSeat.get(label);
|
|
5622
7320
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5623
|
-
return {
|
|
7321
|
+
return {
|
|
7322
|
+
label,
|
|
7323
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7324
|
+
...this.tableQuantityRequest(resolved)
|
|
7325
|
+
};
|
|
5624
7326
|
}),
|
|
5625
7327
|
void 0,
|
|
5626
7328
|
replaceHoldId
|
|
@@ -5636,10 +7338,11 @@ var PickerController = class {
|
|
|
5636
7338
|
this.opts.onSalesClosed?.();
|
|
5637
7339
|
} else if (status === 409 && conflicts?.length) {
|
|
5638
7340
|
const takenLabels = new Set(conflicts.map((c) => c.label));
|
|
5639
|
-
const takenIds =
|
|
7341
|
+
const takenIds = this.idsForLabels(takenLabels);
|
|
5640
7342
|
if (takenIds.length) {
|
|
5641
7343
|
r.setStatus(takenIds, "booked");
|
|
5642
7344
|
r.deselect(takenIds);
|
|
7345
|
+
this.resetTableQuantitiesForLabels(takenLabels);
|
|
5643
7346
|
}
|
|
5644
7347
|
const stillFree = labels.filter((l) => !takenLabels.has(l));
|
|
5645
7348
|
if (stillFree.length && holdId) void this.api.release(this.key, stillFree, holdId).catch(() => {
|
|
@@ -5651,12 +7354,13 @@ var PickerController = class {
|
|
|
5651
7354
|
}
|
|
5652
7355
|
throw err;
|
|
5653
7356
|
}
|
|
5654
|
-
const ids =
|
|
7357
|
+
const ids = this.idsForLabels(labels);
|
|
5655
7358
|
if (ids.length) {
|
|
5656
7359
|
r.setStatus(ids, "booked");
|
|
5657
7360
|
r.deselect(ids);
|
|
5658
7361
|
}
|
|
5659
7362
|
this.clearHold();
|
|
7363
|
+
this.resetTableQuantitiesForLabels(labels);
|
|
5660
7364
|
this.emitSelectionChange();
|
|
5661
7365
|
this.opts.onBook?.(bookingRef);
|
|
5662
7366
|
return labels;
|
|
@@ -5677,7 +7381,8 @@ var PickerController = class {
|
|
|
5677
7381
|
}
|
|
5678
7382
|
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
5679
7383
|
this.clearHold();
|
|
5680
|
-
|
|
7384
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
7385
|
+
const ids = this.idsForLabels(hold.labels);
|
|
5681
7386
|
if (ids.length) {
|
|
5682
7387
|
this.renderer?.deselect(ids);
|
|
5683
7388
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5708,7 +7413,8 @@ var PickerController = class {
|
|
|
5708
7413
|
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
5709
7414
|
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
5710
7415
|
else this.clearHold();
|
|
5711
|
-
|
|
7416
|
+
this.resetTableQuantitiesForLabels(drop);
|
|
7417
|
+
const ids = this.idsForLabels(drop);
|
|
5712
7418
|
if (ids.length) {
|
|
5713
7419
|
this.renderer?.deselect(ids);
|
|
5714
7420
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5797,7 +7503,7 @@ var PickerController = class {
|
|
|
5797
7503
|
this.opts.onDeckTap?.(floorId);
|
|
5798
7504
|
}
|
|
5799
7505
|
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
5800
|
-
/** Switch the map projection
|
|
7506
|
+
/** Switch the map projection. No-op on a flat-only renderer. */
|
|
5801
7507
|
setViewMode(mode) {
|
|
5802
7508
|
this.renderer?.setViewMode?.(mode);
|
|
5803
7509
|
}
|
|
@@ -5896,12 +7602,21 @@ var PickerController = class {
|
|
|
5896
7602
|
if (!sec) return null;
|
|
5897
7603
|
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
5898
7604
|
const byCat = /* @__PURE__ */ new Map();
|
|
7605
|
+
const seenTables = /* @__PURE__ */ new Set();
|
|
5899
7606
|
let seatsLeft = 0;
|
|
5900
7607
|
for (const sid of memberIds) {
|
|
5901
7608
|
const seat = this.seatById.get(sid);
|
|
5902
7609
|
if (!seat) continue;
|
|
5903
|
-
|
|
5904
|
-
if (
|
|
7610
|
+
const table = this.groupedTableBySeatId.get(sid);
|
|
7611
|
+
if (table) {
|
|
7612
|
+
if (seenTables.has(table.label)) continue;
|
|
7613
|
+
seenTables.add(table.label);
|
|
7614
|
+
byCat.set(table.categoryKey, (byCat.get(table.categoryKey) ?? 0) + table.maxOccupancy);
|
|
7615
|
+
if (r.getStatus(table.chairs[0]?.id ?? sid) === "free") seatsLeft += table.maxOccupancy;
|
|
7616
|
+
} else {
|
|
7617
|
+
byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
|
|
7618
|
+
if (r.getStatus(sid) === "free") seatsLeft++;
|
|
7619
|
+
}
|
|
5905
7620
|
}
|
|
5906
7621
|
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
5907
7622
|
const cat = doc.categories.find((c) => c.key === key);
|
|
@@ -5951,22 +7666,54 @@ var PickerController = class {
|
|
|
5951
7666
|
}
|
|
5952
7667
|
// ---- internals ------------------------------------------------------------
|
|
5953
7668
|
toSeat(s) {
|
|
7669
|
+
const table = this.groupedTableBySeatId.get(s.id);
|
|
7670
|
+
if (table) return this.toTableSeat(table);
|
|
5954
7671
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
5955
7672
|
const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
|
|
7673
|
+
const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
|
|
7674
|
+
const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
|
|
7675
|
+
const context = this.seatContext.get(s.id);
|
|
5956
7676
|
const tiers = this.tiersFor(s.categoryKey);
|
|
5957
7677
|
if (!tiers) {
|
|
5958
|
-
return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
7678
|
+
return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
|
|
5959
7679
|
}
|
|
5960
7680
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
5961
7681
|
return {
|
|
5962
7682
|
id: s.id,
|
|
5963
7683
|
label: s.label,
|
|
5964
7684
|
...display,
|
|
7685
|
+
...context,
|
|
5965
7686
|
categoryKey: s.categoryKey,
|
|
5966
7687
|
price: chosen.price,
|
|
5967
7688
|
tiers,
|
|
5968
7689
|
tierId: chosen.id,
|
|
5969
|
-
...commercial
|
|
7690
|
+
...commercial,
|
|
7691
|
+
...accessibility,
|
|
7692
|
+
...wheelchair
|
|
7693
|
+
};
|
|
7694
|
+
}
|
|
7695
|
+
toTableSeat(table) {
|
|
7696
|
+
const representative = table.chairs[0];
|
|
7697
|
+
const tiers = this.tiersFor(table.categoryKey);
|
|
7698
|
+
const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
|
|
7699
|
+
return {
|
|
7700
|
+
id: representative?.id ?? table.objectId,
|
|
7701
|
+
label: table.label,
|
|
7702
|
+
displayLabel: table.displayLabel ?? table.label,
|
|
7703
|
+
...table.displayType ? { displayType: table.displayType, rowType: table.displayType } : {},
|
|
7704
|
+
objectId: table.objectId,
|
|
7705
|
+
sectionLabel: representative ? this.seatContext.get(representative.id)?.sectionLabel : void 0,
|
|
7706
|
+
rowLabel: table.displayLabel ?? table.label,
|
|
7707
|
+
categoryKey: table.categoryKey,
|
|
7708
|
+
price: chosen?.price ?? this.priceFor(table.categoryKey),
|
|
7709
|
+
...tiers ? { tiers, tierId: chosen?.id } : {},
|
|
7710
|
+
objectType: "table",
|
|
7711
|
+
bookingMode: table.mode,
|
|
7712
|
+
quantity: this.tableQuantities.get(table.label) ?? (table.mode === "whole" ? table.capacity : table.minOccupancy),
|
|
7713
|
+
capacity: table.capacity,
|
|
7714
|
+
minOccupancy: table.minOccupancy,
|
|
7715
|
+
maxOccupancy: table.maxOccupancy,
|
|
7716
|
+
physicalSeatIds: table.chairs.map((chair) => chair.id)
|
|
5970
7717
|
};
|
|
5971
7718
|
}
|
|
5972
7719
|
/** Tooltip payload for a hovered seat — see PickerCallbacks.onSeatHover. */
|
|
@@ -6050,10 +7797,13 @@ var PickerController = class {
|
|
|
6050
7797
|
const h = this.hold_;
|
|
6051
7798
|
if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
|
|
6052
7799
|
const tiers = new Map((h.items ?? []).map((item) => [item.label, item.tierId]));
|
|
7800
|
+
const quantities = new Map((h.items ?? []).map((item) => [item.label, item.quantity ?? 1]));
|
|
6053
7801
|
return labels.every((label) => {
|
|
6054
7802
|
const seat = this.labelToSeat.get(label);
|
|
6055
|
-
const
|
|
6056
|
-
|
|
7803
|
+
const resolved = seat ? this.toSeat(seat) : null;
|
|
7804
|
+
const currentTier = resolved?.tierId ?? null;
|
|
7805
|
+
const quantityMatches = resolved?.objectType !== "table" || (resolved.bookingMode !== "variable" || this.confirmedVariableTables.has(label)) && (!quantities.has(label) || quantities.get(label) === resolved.quantity);
|
|
7806
|
+
return quantityMatches && (!tiers.has(label) || tiers.get(label) === currentTier);
|
|
6057
7807
|
});
|
|
6058
7808
|
}
|
|
6059
7809
|
handle409Conflicts(err) {
|
|
@@ -6061,19 +7811,28 @@ var PickerController = class {
|
|
|
6061
7811
|
if (status !== 409 || !conflicts?.length) return;
|
|
6062
7812
|
const r = this.renderer;
|
|
6063
7813
|
if (!r) return;
|
|
6064
|
-
const takenIds = conflicts.map((
|
|
7814
|
+
const takenIds = this.idsForLabels(conflicts.map((conflict) => conflict.label));
|
|
6065
7815
|
if (takenIds.length) {
|
|
6066
7816
|
r.deselect(takenIds);
|
|
6067
7817
|
r.setStatus(takenIds, "held");
|
|
7818
|
+
this.resetTableQuantitiesForLabels(conflicts.map((conflict) => conflict.label));
|
|
6068
7819
|
}
|
|
6069
7820
|
this.emitSelectionChange();
|
|
6070
7821
|
}
|
|
6071
7822
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
6072
7823
|
setHold(hold, source = "created") {
|
|
7824
|
+
for (const item of hold.items ?? []) {
|
|
7825
|
+
if (this.groupedTablesByLabel.has(item.label) && item.quantity != null) {
|
|
7826
|
+
this.tableQuantities.set(item.label, item.quantity);
|
|
7827
|
+
if (this.groupedTablesByLabel.get(item.label)?.mode === "variable") {
|
|
7828
|
+
this.confirmedVariableTables.add(item.label);
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
6073
7832
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
6074
7833
|
this.hold_ = full;
|
|
6075
7834
|
this.renderer?.setOwnedHold?.(
|
|
6076
|
-
|
|
7835
|
+
this.idsForLabels(full.labels)
|
|
6077
7836
|
);
|
|
6078
7837
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
6079
7838
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
@@ -6116,12 +7875,11 @@ var PickerController = class {
|
|
|
6116
7875
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
6117
7876
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
6118
7877
|
r.setOwnedHold?.(
|
|
6119
|
-
(this.hold_?.labels ?? [])
|
|
7878
|
+
this.idsForLabels(this.hold_?.labels ?? [])
|
|
6120
7879
|
);
|
|
6121
7880
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
6122
7881
|
for (const [label, st] of Object.entries(seats)) {
|
|
6123
|
-
|
|
6124
|
-
if (id) byStatus[mapStatus(st)].push(id);
|
|
7882
|
+
byStatus[mapStatus(st)].push(...this.idsForLabel(label));
|
|
6125
7883
|
}
|
|
6126
7884
|
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
6127
7885
|
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
@@ -6130,7 +7888,11 @@ var PickerController = class {
|
|
|
6130
7888
|
this.clearBookedHoldIfSettled();
|
|
6131
7889
|
}
|
|
6132
7890
|
clearBookedHoldIfSettled() {
|
|
6133
|
-
|
|
7891
|
+
const hold = this.hold_;
|
|
7892
|
+
if (hold?.labels.every((label) => this.liveStatuses.get(label) === "booked")) {
|
|
7893
|
+
this.clearHold();
|
|
7894
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
7895
|
+
}
|
|
6134
7896
|
}
|
|
6135
7897
|
async resnapshot() {
|
|
6136
7898
|
try {
|
|
@@ -6219,13 +7981,13 @@ var PickerController = class {
|
|
|
6219
7981
|
} else if (Array.isArray(m.changes)) {
|
|
6220
7982
|
for (const ch of m.changes) {
|
|
6221
7983
|
this.liveStatuses.set(ch.label, ch.status);
|
|
6222
|
-
const
|
|
6223
|
-
if (!
|
|
7984
|
+
const ids = this.idsForLabel(ch.label);
|
|
7985
|
+
if (!ids.length) continue;
|
|
6224
7986
|
const next = mapStatus(ch.status);
|
|
6225
|
-
if (this.opts.flashOnLiveChange && next !== "free" && r.getStatus(id) === "free" && !this.hold_?.labels.includes(ch.label)) {
|
|
6226
|
-
r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e");
|
|
7987
|
+
if (this.opts.flashOnLiveChange && next !== "free" && ids.some((id) => r.getStatus(id) === "free") && !this.hold_?.labels.includes(ch.label)) {
|
|
7988
|
+
ids.forEach((id) => r.flashSeat(id, next === "held" ? "#f4b740" : "#f43f5e"));
|
|
6227
7989
|
}
|
|
6228
|
-
r.setStatus(
|
|
7990
|
+
r.setStatus(ids, next);
|
|
6229
7991
|
}
|
|
6230
7992
|
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
6231
7993
|
r.forceDraw();
|
|
@@ -6256,10 +8018,25 @@ var PickerController = class {
|
|
|
6256
8018
|
}
|
|
6257
8019
|
};
|
|
6258
8020
|
|
|
8021
|
+
// src/view/sightline.ts
|
|
8022
|
+
var STAGE_TOP_M = 5;
|
|
8023
|
+
var STAGE_BASE_M = -1.1;
|
|
8024
|
+
var MAX_LOOKDOWN_DEG = 35;
|
|
8025
|
+
function stageSightlinePitch(eyeHeightM, distM) {
|
|
8026
|
+
const extraEyeM = eyeHeightM - SEATED_EYE_HEIGHT_M;
|
|
8027
|
+
let topPitch = Math.atan2(STAGE_TOP_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8028
|
+
let basePitch = Math.atan2(STAGE_BASE_M - extraEyeM, distM) * 180 / Math.PI;
|
|
8029
|
+
if (basePitch < -MAX_LOOKDOWN_DEG) {
|
|
8030
|
+
topPitch += -MAX_LOOKDOWN_DEG - basePitch;
|
|
8031
|
+
basePitch = -MAX_LOOKDOWN_DEG;
|
|
8032
|
+
}
|
|
8033
|
+
return { topPitch, basePitch };
|
|
8034
|
+
}
|
|
8035
|
+
|
|
6259
8036
|
// src/view/generatePanorama.ts
|
|
6260
8037
|
var W = 2048;
|
|
6261
8038
|
var H = 1024;
|
|
6262
|
-
var UNIT =
|
|
8039
|
+
var UNIT = METRES_PER_CHART_UNIT;
|
|
6263
8040
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
6264
8041
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
6265
8042
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
@@ -6279,9 +8056,11 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
6279
8056
|
sky.addColorStop(1, "#07090f");
|
|
6280
8057
|
ctx.fillStyle = sky;
|
|
6281
8058
|
ctx.fillRect(0, 0, W, H);
|
|
8059
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
6282
8060
|
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6283
|
-
const
|
|
6284
|
-
const
|
|
8061
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8062
|
+
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8063
|
+
const stageBasePitch = sightline.basePitch;
|
|
6285
8064
|
const sx0 = yawToX(-stageHalfYaw);
|
|
6286
8065
|
const sx1 = yawToX(stageHalfYaw);
|
|
6287
8066
|
const sy0 = pitchToY(stageTopPitch);
|
|
@@ -6388,9 +8167,11 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6388
8167
|
sky.addColorStop(1, "#070910");
|
|
6389
8168
|
ctx.fillStyle = sky;
|
|
6390
8169
|
ctx.fillRect(0, 0, TW, TH);
|
|
8170
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8171
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
6391
8172
|
const stageHalfYaw = Math.min(HALF_HFOV - 2, Math.atan2(4, distM) * 180 / Math.PI);
|
|
6392
|
-
const stageTopPitch = Math.min(HALF_VFOV - 2,
|
|
6393
|
-
const stageBasePitch = Math.max(-HALF_VFOV + 2,
|
|
8173
|
+
const stageTopPitch = Math.min(HALF_VFOV - 2, sightline.topPitch);
|
|
8174
|
+
const stageBasePitch = Math.max(-HALF_VFOV + 2, sightline.basePitch);
|
|
6394
8175
|
const sx0 = yawToTX(-stageHalfYaw);
|
|
6395
8176
|
const sx1 = yawToTX(stageHalfYaw);
|
|
6396
8177
|
const sy0 = pitchToTY(stageTopPitch);
|
|
@@ -6487,7 +8268,9 @@ export {
|
|
|
6487
8268
|
PickerController,
|
|
6488
8269
|
RENDERED_QUALITY_REPORT_VERSION,
|
|
6489
8270
|
SUPPORTED_LOCALES,
|
|
8271
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
6490
8272
|
SeatmapRenderer,
|
|
8273
|
+
TIER_HEIGHT_M,
|
|
6491
8274
|
UNGROUPED_ID,
|
|
6492
8275
|
accessibilityMeta,
|
|
6493
8276
|
accessibilityRingColor,
|
|
@@ -6502,16 +8285,19 @@ export {
|
|
|
6502
8285
|
expandRow,
|
|
6503
8286
|
expandRowSlots,
|
|
6504
8287
|
expandTable,
|
|
8288
|
+
expandTableSlots,
|
|
6505
8289
|
floorObjects,
|
|
6506
8290
|
floorsOf,
|
|
6507
8291
|
formatDate,
|
|
6508
8292
|
formatMoney,
|
|
6509
8293
|
gaAreasOf,
|
|
8294
|
+
gaInventorySegments,
|
|
6510
8295
|
gaUnitLabel,
|
|
6511
8296
|
gaUnitLabels,
|
|
6512
8297
|
generateSeatPanorama,
|
|
6513
8298
|
generateSeatThumb,
|
|
6514
8299
|
getLocale,
|
|
8300
|
+
growJoinedGAInventory,
|
|
6515
8301
|
hiddenObjectIds,
|
|
6516
8302
|
inspectRenderedQualityEvidence,
|
|
6517
8303
|
isGaUnitLabel,
|
|
@@ -6519,17 +8305,23 @@ export {
|
|
|
6519
8305
|
layerOf,
|
|
6520
8306
|
loadLocale,
|
|
6521
8307
|
objectCenter,
|
|
8308
|
+
owningSectionForObject,
|
|
6522
8309
|
pointInPolygon,
|
|
6523
8310
|
pointInPolygonWithHoles,
|
|
6524
8311
|
polygonLabelPoint,
|
|
6525
8312
|
resolveLocale,
|
|
8313
|
+
rowInventoryCount,
|
|
6526
8314
|
rowSeatPositions,
|
|
6527
8315
|
seatLabelPart,
|
|
8316
|
+
sectionGeometry,
|
|
6528
8317
|
setLocale,
|
|
6529
8318
|
setMoneyLocale,
|
|
6530
8319
|
setStringOverrides,
|
|
6531
8320
|
stackFloors,
|
|
6532
8321
|
t,
|
|
6533
|
-
tCount
|
|
8322
|
+
tCount,
|
|
8323
|
+
tableInventoryCount,
|
|
8324
|
+
tableSeatCountsBySide,
|
|
8325
|
+
validGAInventorySegments
|
|
6534
8326
|
};
|
|
6535
8327
|
//# sourceMappingURL=index.js.map
|