@seatlayer/core 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{de-EIG65UFU.js → de-73TIXYJH.js} +2 -1
- package/dist/{de-EIG65UFU.js.map → de-73TIXYJH.js.map} +1 -1
- package/dist/{es-47HFAWS6.js → es-SKAODLTR.js} +2 -1
- package/dist/{es-47HFAWS6.js.map → es-SKAODLTR.js.map} +1 -1
- package/dist/{fr-7IXNNFBS.js → fr-J4637T6V.js} +2 -1
- package/dist/{fr-7IXNNFBS.js.map → fr-J4637T6V.js.map} +1 -1
- package/dist/index.cjs +2575 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +784 -51
- package/dist/index.d.ts +784 -51
- package/dist/index.js +2561 -313
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.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);
|
|
@@ -723,20 +1020,6 @@ function applyHidden(doc, hidden) {
|
|
|
723
1020
|
return { ...doc, objects };
|
|
724
1021
|
}
|
|
725
1022
|
|
|
726
|
-
// src/engine/SeatmapRenderer.ts
|
|
727
|
-
import { Konva } from "konva/lib/Core";
|
|
728
|
-
import { Stage } from "konva/lib/Stage";
|
|
729
|
-
import { Layer } from "konva/lib/Layer";
|
|
730
|
-
import { Group } from "konva/lib/Group";
|
|
731
|
-
import { Circle } from "konva/lib/shapes/Circle";
|
|
732
|
-
import { Rect } from "konva/lib/shapes/Rect";
|
|
733
|
-
import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
734
|
-
import { Line } from "konva/lib/shapes/Line";
|
|
735
|
-
import { Text } from "konva/lib/shapes/Text";
|
|
736
|
-
import { Path } from "konva/lib/shapes/Path";
|
|
737
|
-
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
738
|
-
import { Shape } from "konva/lib/Shape";
|
|
739
|
-
|
|
740
1023
|
// src/core/chartRenderRules.ts
|
|
741
1024
|
var SEAT_LABEL_FONT_SIZE = 7;
|
|
742
1025
|
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
@@ -793,6 +1076,769 @@ function stateAwareBookableLabelInk(fill, preferred) {
|
|
|
793
1076
|
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
794
1077
|
}
|
|
795
1078
|
|
|
1079
|
+
// src/core/renderedQuality.ts
|
|
1080
|
+
var RENDERED_QUALITY_REPORT_VERSION = 3;
|
|
1081
|
+
var MIN_TEXT_CONTRAST = 4.5;
|
|
1082
|
+
var MIN_GRAPHICAL_CONTRAST = 3;
|
|
1083
|
+
var MIN_POINTER_TARGET_PX = 24;
|
|
1084
|
+
var MAX_SAMPLES_PER_FINDING = 20;
|
|
1085
|
+
function visibleWithBox(items) {
|
|
1086
|
+
return items.filter((item) => item.visible && Boolean(item.screenBox));
|
|
1087
|
+
}
|
|
1088
|
+
function intersects(left, right) {
|
|
1089
|
+
return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
|
|
1090
|
+
}
|
|
1091
|
+
function minimum(values) {
|
|
1092
|
+
return values.length ? Math.round(Math.min(...values) * 100) / 100 : null;
|
|
1093
|
+
}
|
|
1094
|
+
function finding(code, message, samples) {
|
|
1095
|
+
if (!samples.length) return null;
|
|
1096
|
+
return {
|
|
1097
|
+
code,
|
|
1098
|
+
message,
|
|
1099
|
+
count: samples.length,
|
|
1100
|
+
samples: samples.slice(0, MAX_SAMPLES_PER_FINDING)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function labelSample(label, measured, minimumValue) {
|
|
1104
|
+
return {
|
|
1105
|
+
primaryId: label.seatId,
|
|
1106
|
+
primaryLabel: label.label,
|
|
1107
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1108
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function hierarchySample(label, measured, minimumValue) {
|
|
1112
|
+
return {
|
|
1113
|
+
primaryId: label.id,
|
|
1114
|
+
primaryLabel: label.label,
|
|
1115
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1116
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function freeTextSample(label, measured, minimumValue) {
|
|
1120
|
+
return {
|
|
1121
|
+
primaryId: label.objectId,
|
|
1122
|
+
primaryLabel: label.text,
|
|
1123
|
+
...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
|
|
1124
|
+
...minimumValue == null ? {} : { minimum: minimumValue }
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function collisionSample(primaryId, primaryLabel, secondaryId, secondaryLabel) {
|
|
1128
|
+
return { primaryId, primaryLabel, secondaryId, secondaryLabel };
|
|
1129
|
+
}
|
|
1130
|
+
function inspectRenderedQualityEvidence(evidence, state = "overview", targetCategoryKey = null) {
|
|
1131
|
+
const bookable = visibleWithBox(evidence.labels);
|
|
1132
|
+
const hierarchy = visibleWithBox(evidence.hierarchyLabels);
|
|
1133
|
+
const freeText = visibleWithBox(evidence.freeTextLabels);
|
|
1134
|
+
const visibleGA = evidence.gaAreas.filter((area) => area.visible);
|
|
1135
|
+
const bookableContrast = bookable.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
|
|
1136
|
+
const hierarchyContrast = hierarchy.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
|
|
1137
|
+
const freeTextContrast = freeText.map((label) => renderedTextContrast(label.ink, label.background) ?? 0);
|
|
1138
|
+
const gaContrast = visibleGA.map((area) => renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0);
|
|
1139
|
+
const targetLabels = targetCategoryKey ? evidence.labels.filter((label) => label.categoryKey === targetCategoryKey) : evidence.labels;
|
|
1140
|
+
const targetGAAreas = targetCategoryKey ? evidence.gaAreas.filter((area) => area.categoryKey === targetCategoryKey) : evidence.gaAreas;
|
|
1141
|
+
const selected = targetLabels.filter((label) => label.selected);
|
|
1142
|
+
const held = targetLabels.filter((label) => label.status === "held");
|
|
1143
|
+
const booked = targetLabels.filter((label) => label.status === "booked");
|
|
1144
|
+
const activePointerTargets = targetLabels.filter((label) => label.pointerTarget.active);
|
|
1145
|
+
const activeCategoryKeys = /* @__PURE__ */ new Set([
|
|
1146
|
+
...evidence.labels.filter((label) => label.visible && (label.opacity > 0.25 || label.selected || label.status !== "free")).map((label) => label.categoryKey),
|
|
1147
|
+
...evidence.gaAreas.filter((area) => area.visible && area.opacity > 0.1).map((area) => area.categoryKey)
|
|
1148
|
+
]);
|
|
1149
|
+
const selectedRingContrast = selected.length ? minimum(selected.flatMap((label) => [
|
|
1150
|
+
renderedTextContrast(evidence.selectionRingColor, evidence.canvasBackground) ?? 0,
|
|
1151
|
+
renderedTextContrast(evidence.selectionRingColor, label.fill) ?? 0
|
|
1152
|
+
])) : null;
|
|
1153
|
+
const findings = [];
|
|
1154
|
+
const overviewCountSamples = (count, label) => Array.from({ length: count }, (_, index) => ({
|
|
1155
|
+
primaryId: `overview:${index + 1}`,
|
|
1156
|
+
primaryLabel: label
|
|
1157
|
+
}));
|
|
1158
|
+
if (state === "overview" && evidence.rung === "sections") {
|
|
1159
|
+
findings.push(finding(
|
|
1160
|
+
"overview-section-category-paint",
|
|
1161
|
+
"Section overview shells must use the neutral hierarchy palette, not category paint",
|
|
1162
|
+
overviewCountSamples(evidence.overviewStyle.categoryPaintedSectionShells, "category-painted section shell")
|
|
1163
|
+
));
|
|
1164
|
+
findings.push(finding(
|
|
1165
|
+
"overview-category-detail-visible",
|
|
1166
|
+
"Category-tinted section detail must wait until section focus or seat zoom",
|
|
1167
|
+
overviewCountSamples(evidence.overviewStyle.visibleCategoryDetailOutlines, "category detail outline")
|
|
1168
|
+
));
|
|
1169
|
+
findings.push(finding(
|
|
1170
|
+
"overview-row-hints-visible",
|
|
1171
|
+
"Row and seat patterns must not clutter the section overview",
|
|
1172
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionRowHints, "section row hint")
|
|
1173
|
+
));
|
|
1174
|
+
findings.push(finding(
|
|
1175
|
+
"overview-availability-clutter",
|
|
1176
|
+
"Live availability counts belong in focused detail, not on section overview shells",
|
|
1177
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionAvailabilityLabels, "section availability label")
|
|
1178
|
+
));
|
|
1179
|
+
findings.push(finding(
|
|
1180
|
+
"overview-ga-detail-visible",
|
|
1181
|
+
"Section-contained standing paint belongs in focused detail, not on section overview shells",
|
|
1182
|
+
overviewCountSamples(evidence.overviewStyle.visibleSectionGADetails, "section-contained GA detail")
|
|
1183
|
+
));
|
|
1184
|
+
}
|
|
1185
|
+
findings.push(finding(
|
|
1186
|
+
"bookable-label-undersized",
|
|
1187
|
+
"Visible bookable labels must meet the rendered small-text size floor",
|
|
1188
|
+
bookable.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => labelSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
|
|
1189
|
+
));
|
|
1190
|
+
if (state === "interaction") {
|
|
1191
|
+
const labelledInventory = targetLabels.length > 0;
|
|
1192
|
+
const gaInventory = targetGAAreas.length > 0;
|
|
1193
|
+
const detailInventory = labelledInventory || gaInventory;
|
|
1194
|
+
const visibleTargetGA = targetGAAreas.filter((area) => area.visible);
|
|
1195
|
+
const sections = /* @__PURE__ */ new Set([
|
|
1196
|
+
...targetLabels.flatMap((label) => label.sectionId ? [label.sectionId] : []),
|
|
1197
|
+
...targetGAAreas.flatMap((area) => area.sectionId ? [area.sectionId] : [])
|
|
1198
|
+
]);
|
|
1199
|
+
const categories = /* @__PURE__ */ new Set([
|
|
1200
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1201
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1202
|
+
]);
|
|
1203
|
+
const inViewport = (label) => label.screenCenter.x >= 0 && label.screenCenter.x <= evidence.viewport.width && label.screenCenter.y >= 0 && label.screenCenter.y <= evidence.viewport.height;
|
|
1204
|
+
findings.push(finding(
|
|
1205
|
+
"detail-rung-missing",
|
|
1206
|
+
"Interaction evidence with bookable inventory must render the seat-detail rung",
|
|
1207
|
+
detailInventory && evidence.rung !== "seats" ? [{ primaryId: "renderer", primaryLabel: evidence.rung }] : []
|
|
1208
|
+
));
|
|
1209
|
+
findings.push(finding(
|
|
1210
|
+
"detail-inventory-not-visible",
|
|
1211
|
+
"Interaction evidence must frame at least one real bookable unit",
|
|
1212
|
+
detailInventory && !targetLabels.some(inViewport) && !visibleTargetGA.length ? [{ primaryId: "renderer", primaryLabel: "no target inventory in viewport" }] : []
|
|
1213
|
+
));
|
|
1214
|
+
findings.push(finding(
|
|
1215
|
+
"pointer-target-inactive",
|
|
1216
|
+
"Interaction evidence must expose a live production pointer target",
|
|
1217
|
+
labelledInventory && !activePointerTargets.some(inViewport) || !labelledInventory && gaInventory && !visibleTargetGA.some((area) => area.interactive) ? [{ primaryId: "renderer", primaryLabel: "no active pointer target in viewport" }] : []
|
|
1218
|
+
));
|
|
1219
|
+
findings.push(finding(
|
|
1220
|
+
"pointer-target-undersized",
|
|
1221
|
+
"Every active production pointer target must reach at least 24 CSS pixels",
|
|
1222
|
+
activePointerTargets.filter((label) => inViewport(label) && label.pointerTarget.effectiveMinimumPx < MIN_POINTER_TARGET_PX).map((label) => labelSample(label, label.pointerTarget.effectiveMinimumPx, MIN_POINTER_TARGET_PX))
|
|
1223
|
+
));
|
|
1224
|
+
findings.push(finding(
|
|
1225
|
+
"selected-state-missing",
|
|
1226
|
+
"Interaction evidence must paint a selected unit and its renderer-owned ring",
|
|
1227
|
+
labelledInventory && (!selected.length || !selected.some((label) => evidence.selectionRingSeatIds.includes(label.seatId))) ? [{ primaryId: "renderer", primaryLabel: "selected state" }] : []
|
|
1228
|
+
));
|
|
1229
|
+
findings.push(finding(
|
|
1230
|
+
"selected-state-contrast-low",
|
|
1231
|
+
"The selected-state ring must maintain 3:1 graphical contrast with the canvas",
|
|
1232
|
+
selected.length && (selectedRingContrast ?? 0) < MIN_GRAPHICAL_CONTRAST ? [{
|
|
1233
|
+
primaryId: selected[0].seatId,
|
|
1234
|
+
primaryLabel: selected[0].label,
|
|
1235
|
+
measured: Math.round((selectedRingContrast ?? 0) * 100) / 100,
|
|
1236
|
+
minimum: MIN_GRAPHICAL_CONTRAST
|
|
1237
|
+
}] : []
|
|
1238
|
+
));
|
|
1239
|
+
findings.push(finding(
|
|
1240
|
+
"held-state-missing",
|
|
1241
|
+
"Interaction evidence must paint a held state when the floor has at least two status-managed units",
|
|
1242
|
+
targetLabels.length >= 2 && !held.length ? [{ primaryId: "renderer", primaryLabel: "held state" }] : []
|
|
1243
|
+
));
|
|
1244
|
+
findings.push(finding(
|
|
1245
|
+
"booked-state-missing",
|
|
1246
|
+
"Interaction evidence must paint a taken state when the floor has at least three status-managed units",
|
|
1247
|
+
targetLabels.length >= 3 && !booked.length ? [{ primaryId: "renderer", primaryLabel: "booked state" }] : []
|
|
1248
|
+
));
|
|
1249
|
+
const heldSignatures = new Set(held.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
|
|
1250
|
+
const bookedSignatures = new Set(booked.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
|
|
1251
|
+
findings.push(finding(
|
|
1252
|
+
"status-state-indistinct",
|
|
1253
|
+
"Held and taken evidence must resolve to distinct renderer paint",
|
|
1254
|
+
held.length && booked.length && [...heldSignatures].some((signature) => bookedSignatures.has(signature)) ? [{ primaryId: held[0].seatId, primaryLabel: held[0].label, secondaryId: booked[0].seatId, secondaryLabel: booked[0].label }] : []
|
|
1255
|
+
));
|
|
1256
|
+
findings.push(finding(
|
|
1257
|
+
"section-focus-missing",
|
|
1258
|
+
"Interaction evidence must exercise section focus and its backdrop when section membership exists",
|
|
1259
|
+
sections.size && (!evidence.focusedSectionId || !evidence.focusBackdropVisible) ? [{ primaryId: "renderer", primaryLabel: "section focus" }] : []
|
|
1260
|
+
));
|
|
1261
|
+
findings.push(finding(
|
|
1262
|
+
"category-filter-missing",
|
|
1263
|
+
"Interaction evidence must exercise a category filter when multiple categories exist",
|
|
1264
|
+
categories.size >= 2 && (!evidence.categoryFilterKeys || !evidence.categoryFilterKeys.length) ? [{ primaryId: "renderer", primaryLabel: "category filter" }] : []
|
|
1265
|
+
));
|
|
1266
|
+
const excludedFree = evidence.labels.filter((label) => label.status === "free" && !label.selected && evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(label.categoryKey));
|
|
1267
|
+
const excludedGA = evidence.gaAreas.filter((area) => evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(area.categoryKey));
|
|
1268
|
+
findings.push(finding(
|
|
1269
|
+
"category-filter-ineffective",
|
|
1270
|
+
"The active category filter must visibly dim excluded free inventory",
|
|
1271
|
+
(excludedFree.length || excludedGA.length) && !excludedFree.some((label) => label.opacity <= 0.25) && !excludedGA.some((area) => area.opacity <= 0.1) ? [
|
|
1272
|
+
...excludedFree.map((label) => labelSample(label, label.opacity)),
|
|
1273
|
+
...excludedGA.map((area) => ({ primaryId: area.areaId, primaryLabel: area.label, measured: area.opacity }))
|
|
1274
|
+
] : []
|
|
1275
|
+
));
|
|
1276
|
+
findings.push(finding(
|
|
1277
|
+
"target-category-not-visible",
|
|
1278
|
+
"A category-specific interaction scene must visibly paint its exact target category",
|
|
1279
|
+
targetCategoryKey && !activeCategoryKeys.has(targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
|
|
1280
|
+
));
|
|
1281
|
+
findings.push(finding(
|
|
1282
|
+
"target-category-filter-mismatch",
|
|
1283
|
+
"A category-specific interaction scene must bind its filter to only the exact target category",
|
|
1284
|
+
targetCategoryKey && (evidence.categoryFilterKeys?.length !== 1 || evidence.categoryFilterKeys[0] !== targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
|
|
1285
|
+
));
|
|
1286
|
+
}
|
|
1287
|
+
findings.push(finding(
|
|
1288
|
+
"bookable-label-contrast-low",
|
|
1289
|
+
"Visible bookable labels must meet 4.5:1 contrast against their actual paint",
|
|
1290
|
+
bookable.flatMap((label) => {
|
|
1291
|
+
const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
|
|
1292
|
+
return ratio < MIN_TEXT_CONTRAST ? [labelSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1293
|
+
})
|
|
1294
|
+
));
|
|
1295
|
+
const bookableCollisions = [];
|
|
1296
|
+
for (let index = 0; index < bookable.length; index += 1) {
|
|
1297
|
+
for (let other = 0; other < index; other += 1) {
|
|
1298
|
+
if (intersects(bookable[index].screenBox, bookable[other].screenBox)) {
|
|
1299
|
+
bookableCollisions.push(collisionSample(
|
|
1300
|
+
bookable[other].seatId,
|
|
1301
|
+
bookable[other].label,
|
|
1302
|
+
bookable[index].seatId,
|
|
1303
|
+
bookable[index].label
|
|
1304
|
+
));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
findings.push(finding(
|
|
1309
|
+
"bookable-label-collision",
|
|
1310
|
+
"Visible bookable labels must not overlap",
|
|
1311
|
+
bookableCollisions
|
|
1312
|
+
));
|
|
1313
|
+
findings.push(finding(
|
|
1314
|
+
"hierarchy-label-undersized",
|
|
1315
|
+
"Visible section and zone labels must meet the rendered small-text size floor",
|
|
1316
|
+
hierarchy.filter((label) => label.renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX).map((label) => hierarchySample(label, label.renderedFontPx, MIN_VISIBLE_BOOKABLE_LABEL_PX))
|
|
1317
|
+
));
|
|
1318
|
+
findings.push(finding(
|
|
1319
|
+
"hierarchy-label-contrast-low",
|
|
1320
|
+
"Visible section and zone labels must meet 4.5:1 contrast against their backing paint",
|
|
1321
|
+
hierarchy.flatMap((label) => {
|
|
1322
|
+
const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
|
|
1323
|
+
return ratio < MIN_TEXT_CONTRAST ? [hierarchySample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1324
|
+
})
|
|
1325
|
+
));
|
|
1326
|
+
findings.push(finding(
|
|
1327
|
+
"hierarchy-label-outside-section",
|
|
1328
|
+
"Visible section labels must remain inside the filled section surface and outside holes",
|
|
1329
|
+
hierarchy.filter((label) => label.kind === "section" && label.fitsContainer === false).map((label) => hierarchySample(label))
|
|
1330
|
+
));
|
|
1331
|
+
const hierarchyCollisions = [];
|
|
1332
|
+
for (let index = 0; index < hierarchy.length; index += 1) {
|
|
1333
|
+
for (let other = 0; other < index; other += 1) {
|
|
1334
|
+
if (intersects(hierarchy[index].screenBox, hierarchy[other].screenBox)) {
|
|
1335
|
+
hierarchyCollisions.push(collisionSample(
|
|
1336
|
+
hierarchy[other].id,
|
|
1337
|
+
hierarchy[other].label,
|
|
1338
|
+
hierarchy[index].id,
|
|
1339
|
+
hierarchy[index].label
|
|
1340
|
+
));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
findings.push(finding(
|
|
1345
|
+
"hierarchy-label-collision",
|
|
1346
|
+
"Visible hierarchy labels must not overlap each other",
|
|
1347
|
+
hierarchyCollisions
|
|
1348
|
+
));
|
|
1349
|
+
const hierarchyBookableCollisions = [];
|
|
1350
|
+
for (const upper of hierarchy) {
|
|
1351
|
+
for (const unit of bookable) {
|
|
1352
|
+
if (intersects(upper.screenBox, unit.screenBox)) {
|
|
1353
|
+
hierarchyBookableCollisions.push(collisionSample(upper.id, upper.label, unit.seatId, unit.label));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
findings.push(finding(
|
|
1358
|
+
"hierarchy-bookable-collision",
|
|
1359
|
+
"Visible hierarchy labels must not overlap bookable labels",
|
|
1360
|
+
hierarchyBookableCollisions
|
|
1361
|
+
));
|
|
1362
|
+
findings.push(finding(
|
|
1363
|
+
"free-text-undersized",
|
|
1364
|
+
"Visible chart text must meet the rendered small-text size floor",
|
|
1365
|
+
freeText.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => freeTextSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
|
|
1366
|
+
));
|
|
1367
|
+
findings.push(finding(
|
|
1368
|
+
"free-text-contrast-low",
|
|
1369
|
+
"Visible chart text must meet 4.5:1 contrast against its measured background",
|
|
1370
|
+
freeText.flatMap((label) => {
|
|
1371
|
+
const ratio = renderedTextContrast(label.ink, label.background) ?? 0;
|
|
1372
|
+
return ratio < MIN_TEXT_CONTRAST ? [freeTextSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
|
|
1373
|
+
})
|
|
1374
|
+
));
|
|
1375
|
+
const freeTextCollisions = [];
|
|
1376
|
+
for (let index = 0; index < freeText.length; index += 1) {
|
|
1377
|
+
for (let other = 0; other < index; other += 1) {
|
|
1378
|
+
if (intersects(freeText[index].screenBox, freeText[other].screenBox)) {
|
|
1379
|
+
freeTextCollisions.push(collisionSample(
|
|
1380
|
+
freeText[other].objectId,
|
|
1381
|
+
freeText[other].text,
|
|
1382
|
+
freeText[index].objectId,
|
|
1383
|
+
freeText[index].text
|
|
1384
|
+
));
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
findings.push(finding(
|
|
1389
|
+
"free-text-collision",
|
|
1390
|
+
"Visible chart text labels must not overlap each other",
|
|
1391
|
+
freeTextCollisions
|
|
1392
|
+
));
|
|
1393
|
+
const freeTextBookableCollisions = [];
|
|
1394
|
+
for (const text of freeText) {
|
|
1395
|
+
for (const unit of bookable) {
|
|
1396
|
+
if (intersects(text.screenBox, unit.screenBox)) {
|
|
1397
|
+
freeTextBookableCollisions.push(collisionSample(text.objectId, text.text, unit.seatId, unit.label));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
findings.push(finding(
|
|
1402
|
+
"free-text-bookable-collision",
|
|
1403
|
+
"Visible chart text must not overlap bookable labels",
|
|
1404
|
+
freeTextBookableCollisions
|
|
1405
|
+
));
|
|
1406
|
+
const freeTextHierarchyCollisions = [];
|
|
1407
|
+
for (const text of freeText) {
|
|
1408
|
+
for (const upper of hierarchy) {
|
|
1409
|
+
if (intersects(text.screenBox, upper.screenBox)) {
|
|
1410
|
+
freeTextHierarchyCollisions.push(collisionSample(text.objectId, text.text, upper.id, upper.label));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
findings.push(finding(
|
|
1415
|
+
"free-text-hierarchy-collision",
|
|
1416
|
+
"Visible chart text must not overlap hierarchy labels",
|
|
1417
|
+
freeTextHierarchyCollisions
|
|
1418
|
+
));
|
|
1419
|
+
findings.push(finding(
|
|
1420
|
+
"ga-contrast-low",
|
|
1421
|
+
"Visible GA surfaces must maintain 3:1 graphical contrast with the canvas",
|
|
1422
|
+
visibleGA.flatMap((area) => {
|
|
1423
|
+
const ratio = renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0;
|
|
1424
|
+
return ratio < MIN_GRAPHICAL_CONTRAST ? [{
|
|
1425
|
+
primaryId: area.areaId,
|
|
1426
|
+
primaryLabel: area.label,
|
|
1427
|
+
measured: Math.round(ratio * 100) / 100,
|
|
1428
|
+
minimum: MIN_GRAPHICAL_CONTRAST
|
|
1429
|
+
}] : [];
|
|
1430
|
+
})
|
|
1431
|
+
));
|
|
1432
|
+
const materialFindings = findings.filter((item) => Boolean(item));
|
|
1433
|
+
return {
|
|
1434
|
+
version: RENDERED_QUALITY_REPORT_VERSION,
|
|
1435
|
+
passed: materialFindings.length === 0,
|
|
1436
|
+
state,
|
|
1437
|
+
targetCategoryKey,
|
|
1438
|
+
resolvedRules: [
|
|
1439
|
+
"rendered-overview-label-size",
|
|
1440
|
+
"rendered-overview-label-contrast",
|
|
1441
|
+
"rendered-overview-label-collision",
|
|
1442
|
+
"rendered-overview-hierarchy-containment",
|
|
1443
|
+
"rendered-overview-section-first-style",
|
|
1444
|
+
"rendered-overview-ga-contrast",
|
|
1445
|
+
...state === "interaction" ? [
|
|
1446
|
+
"rendered-detail-inventory",
|
|
1447
|
+
"rendered-pointer-target",
|
|
1448
|
+
"rendered-selected-held-taken-states",
|
|
1449
|
+
"rendered-section-focus",
|
|
1450
|
+
"rendered-category-filter"
|
|
1451
|
+
] : []
|
|
1452
|
+
],
|
|
1453
|
+
viewport: evidence.viewport,
|
|
1454
|
+
canvasBackground: evidence.canvasBackground,
|
|
1455
|
+
effectiveScale: evidence.effectiveScale,
|
|
1456
|
+
rung: evidence.rung,
|
|
1457
|
+
inventory: {
|
|
1458
|
+
totalBookableUnits: evidence.totalBookableUnits,
|
|
1459
|
+
totalLabelledBookableUnits: evidence.totalLabelledBookableUnits,
|
|
1460
|
+
visibleBookableLabels: bookable.length,
|
|
1461
|
+
hiddenBookableLabels: evidence.hiddenLabels,
|
|
1462
|
+
visibleHierarchyLabels: hierarchy.length,
|
|
1463
|
+
visibleFreeTextLabels: freeText.length,
|
|
1464
|
+
visibleGAAreas: visibleGA.length
|
|
1465
|
+
},
|
|
1466
|
+
overviewStyle: evidence.overviewStyle,
|
|
1467
|
+
composition: {
|
|
1468
|
+
hierarchy: evidence.hierarchyLabels.filter((label) => label.role === "name").map((label) => ({
|
|
1469
|
+
id: label.id,
|
|
1470
|
+
kind: label.kind,
|
|
1471
|
+
label: label.label,
|
|
1472
|
+
visible: label.visible
|
|
1473
|
+
})).sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)),
|
|
1474
|
+
labelledObjects: evidence.freeTextLabels.map((label) => ({
|
|
1475
|
+
objectId: label.objectId,
|
|
1476
|
+
kind: label.kind,
|
|
1477
|
+
text: label.text,
|
|
1478
|
+
visible: label.visible
|
|
1479
|
+
})).sort((left, right) => left.objectId.localeCompare(right.objectId) || left.kind.localeCompare(right.kind)),
|
|
1480
|
+
gaAreas: evidence.gaAreas.map((area) => ({
|
|
1481
|
+
areaId: area.areaId,
|
|
1482
|
+
label: area.label,
|
|
1483
|
+
categoryKey: area.categoryKey,
|
|
1484
|
+
...area.sectionId ? { sectionId: area.sectionId } : {},
|
|
1485
|
+
visible: area.visible
|
|
1486
|
+
})).sort((left, right) => left.areaId.localeCompare(right.areaId)),
|
|
1487
|
+
bookableSectionIds: [...new Set(evidence.labels.flatMap((label) => label.sectionId ? [label.sectionId] : []))].sort(),
|
|
1488
|
+
categoryKeys: [.../* @__PURE__ */ new Set([
|
|
1489
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1490
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1491
|
+
])].sort(),
|
|
1492
|
+
activeCategoryKeys: [...activeCategoryKeys].sort()
|
|
1493
|
+
},
|
|
1494
|
+
metrics: {
|
|
1495
|
+
minimumRenderedBookableLabelPx: minimum(bookable.map((label) => label.renderedFontPx)),
|
|
1496
|
+
minimumBookableLabelContrast: minimum(bookableContrast),
|
|
1497
|
+
minimumRenderedHierarchyLabelPx: minimum(hierarchy.map((label) => label.renderedFontPx)),
|
|
1498
|
+
minimumHierarchyLabelContrast: minimum(hierarchyContrast),
|
|
1499
|
+
minimumRenderedFreeTextPx: minimum(freeText.map((label) => label.renderedFontPx)),
|
|
1500
|
+
minimumFreeTextContrast: minimum(freeTextContrast),
|
|
1501
|
+
minimumGAContrast: minimum(gaContrast),
|
|
1502
|
+
minimumEffectivePointerTargetPx: minimum(activePointerTargets.map((label) => label.pointerTarget.effectiveMinimumPx)),
|
|
1503
|
+
selectedRingContrast: selectedRingContrast == null ? null : Math.round(selectedRingContrast * 100) / 100
|
|
1504
|
+
},
|
|
1505
|
+
interaction: {
|
|
1506
|
+
applicable: {
|
|
1507
|
+
detail: targetLabels.length > 0 || targetGAAreas.length > 0,
|
|
1508
|
+
pointer: targetLabels.length > 0 || targetGAAreas.length > 0,
|
|
1509
|
+
held: targetLabels.length >= 2,
|
|
1510
|
+
booked: targetLabels.length >= 3,
|
|
1511
|
+
sectionFocus: targetLabels.some((label) => Boolean(label.sectionId)) || targetGAAreas.some((area) => Boolean(area.sectionId)),
|
|
1512
|
+
categoryFilter: (/* @__PURE__ */ new Set([
|
|
1513
|
+
...evidence.labels.map((label) => label.categoryKey),
|
|
1514
|
+
...evidence.gaAreas.map((area) => area.categoryKey)
|
|
1515
|
+
])).size >= 2
|
|
1516
|
+
},
|
|
1517
|
+
selectedUnits: selected.length,
|
|
1518
|
+
heldUnits: held.length,
|
|
1519
|
+
bookedUnits: booked.length,
|
|
1520
|
+
activePointerTargets: activePointerTargets.length,
|
|
1521
|
+
focusedSectionId: evidence.focusedSectionId,
|
|
1522
|
+
focusBackdropVisible: evidence.focusBackdropVisible,
|
|
1523
|
+
categoryFilterKeys: evidence.categoryFilterKeys
|
|
1524
|
+
},
|
|
1525
|
+
findings: materialFindings
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/engine/SeatmapRenderer.ts
|
|
1530
|
+
import { Konva } from "konva/lib/Core";
|
|
1531
|
+
import { Stage } from "konva/lib/Stage";
|
|
1532
|
+
import { Layer } from "konva/lib/Layer";
|
|
1533
|
+
import { Group } from "konva/lib/Group";
|
|
1534
|
+
import { Circle } from "konva/lib/shapes/Circle";
|
|
1535
|
+
import { Rect } from "konva/lib/shapes/Rect";
|
|
1536
|
+
import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
1537
|
+
import { Line } from "konva/lib/shapes/Line";
|
|
1538
|
+
import { Arrow } from "konva/lib/shapes/Arrow";
|
|
1539
|
+
import { Text } from "konva/lib/shapes/Text";
|
|
1540
|
+
import { Path } from "konva/lib/shapes/Path";
|
|
1541
|
+
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
1542
|
+
import { Shape } from "konva/lib/Shape";
|
|
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
|
+
|
|
796
1842
|
// src/lib/money.ts
|
|
797
1843
|
var DEFAULT_CURRENCY = "USD";
|
|
798
1844
|
var displayLocale;
|
|
@@ -855,6 +1901,7 @@ var en = {
|
|
|
855
1901
|
"picker.seatsLeftInSection.one": "{count} seat left",
|
|
856
1902
|
"picker.seatsLeftInSection.other": "{count} seats left",
|
|
857
1903
|
"picker.overview": "Overview",
|
|
1904
|
+
"picker.entrance": "Entrance",
|
|
858
1905
|
"picker.tapSeatHint": "Tap any seat to check its view",
|
|
859
1906
|
"picker.ticketTierFor": "Ticket tier for {label}",
|
|
860
1907
|
"picker.viewFromSeat": "View from seat {label}",
|
|
@@ -912,6 +1959,25 @@ function formatDate(value, opts) {
|
|
|
912
1959
|
return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
|
|
913
1960
|
}
|
|
914
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
|
+
|
|
915
1981
|
// src/engine/SeatmapRenderer.ts
|
|
916
1982
|
var SEAT_RADIUS = 9;
|
|
917
1983
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
@@ -919,17 +1985,18 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
919
1985
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
920
1986
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
921
1987
|
var SEAT_TAP_SLOP_PX = 14;
|
|
922
|
-
var
|
|
1988
|
+
var WHEELCHAIR_GLYPH_MIN_PX = 10;
|
|
1989
|
+
var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
|
|
923
1990
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
924
1991
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
925
1992
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
926
1993
|
var PAN_START_SLOP_PX = 8;
|
|
1994
|
+
var GHOST_CLICK_MS = 700;
|
|
927
1995
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
928
1996
|
var MAX_LABELS = 700;
|
|
929
1997
|
var MARQUEE_RING_CAP = 2500;
|
|
930
1998
|
var ISO_ANGLE_DEG = -11.5;
|
|
931
1999
|
var ISO_SQUASH = 0.58;
|
|
932
|
-
var LIFT_PER_STEP = 58;
|
|
933
2000
|
var ISO_TWEEN_MS = 320;
|
|
934
2001
|
var CAMERA_GLIDE_MS = 650;
|
|
935
2002
|
var BLOCK_FILL_ALPHA = 1;
|
|
@@ -966,6 +2033,10 @@ var DEF_SELECTION = "#ffffff";
|
|
|
966
2033
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
967
2034
|
var DEF_DECOR_FILL = "#232c40";
|
|
968
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
|
+
}
|
|
969
2040
|
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
970
2041
|
function colorLuminance(color) {
|
|
971
2042
|
const s = color.trim();
|
|
@@ -1071,8 +2142,8 @@ function rotatedRectPoints(center, width, height, rotation) {
|
|
|
1071
2142
|
}));
|
|
1072
2143
|
}
|
|
1073
2144
|
function pointsBounds(points) {
|
|
1074
|
-
const
|
|
1075
|
-
return { x:
|
|
2145
|
+
const bounds2 = polyBounds(points);
|
|
2146
|
+
return { x: bounds2.x, y: bounds2.y, width: bounds2.width, height: bounds2.height };
|
|
1076
2147
|
}
|
|
1077
2148
|
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1078
2149
|
const radians = rotation * Math.PI / 180;
|
|
@@ -1092,14 +2163,14 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
|
1092
2163
|
return true;
|
|
1093
2164
|
}
|
|
1094
2165
|
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1095
|
-
const
|
|
1096
|
-
const centre = { x:
|
|
2166
|
+
const bounds2 = polyBounds(outer);
|
|
2167
|
+
const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
|
|
1097
2168
|
const points = [preferred];
|
|
1098
2169
|
for (let row = 1; row < 12; row += 1) {
|
|
1099
2170
|
for (let column = 1; column < 12; column += 1) {
|
|
1100
2171
|
const point = {
|
|
1101
|
-
x:
|
|
1102
|
-
y:
|
|
2172
|
+
x: bounds2.x + bounds2.width * column / 12,
|
|
2173
|
+
y: bounds2.y + bounds2.height * row / 12
|
|
1103
2174
|
};
|
|
1104
2175
|
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1105
2176
|
}
|
|
@@ -1164,6 +2235,33 @@ function rgba(hex, a) {
|
|
|
1164
2235
|
const n = parseInt(m[1], 16);
|
|
1165
2236
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1166
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
|
+
};
|
|
1167
2265
|
function hexToRgb(hex) {
|
|
1168
2266
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1169
2267
|
if (!m) return null;
|
|
@@ -1194,8 +2292,15 @@ function lerpColor(a, b, t2) {
|
|
|
1194
2292
|
}
|
|
1195
2293
|
var _SeatmapRenderer = class _SeatmapRenderer {
|
|
1196
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();
|
|
1197
2299
|
this.seats = [];
|
|
1198
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;
|
|
1199
2304
|
/** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
|
|
1200
2305
|
this.chartDoc = null;
|
|
1201
2306
|
this.activeFloorId = "";
|
|
@@ -1211,13 +2316,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1211
2316
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1212
2317
|
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1213
2318
|
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1214
|
-
/** Centred
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
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. */
|
|
1218
2323
|
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1219
|
-
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1220
|
-
this.accessGlyphVisible = false;
|
|
1221
2324
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1222
2325
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1223
2326
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
@@ -1242,6 +2345,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1242
2345
|
this.ownedHold = /* @__PURE__ */ new Set();
|
|
1243
2346
|
/** One selected seat being inspected before it is committed to the cart. */
|
|
1244
2347
|
this.selectionFocusId = null;
|
|
2348
|
+
/** Seat currently owning the shared hover ring (null while not hovering). */
|
|
2349
|
+
this.hoveredId = null;
|
|
1245
2350
|
this.focusedId = null;
|
|
1246
2351
|
/**
|
|
1247
2352
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -1260,6 +2365,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1260
2365
|
this.sections = [];
|
|
1261
2366
|
this.zones = [];
|
|
1262
2367
|
this.seatSection = /* @__PURE__ */ new Map();
|
|
2368
|
+
/** Seats outside every authored section retain the legacy path in one group. */
|
|
2369
|
+
this.unsectionedSeatGroup = null;
|
|
1263
2370
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1264
2371
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1265
2372
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
@@ -1272,6 +2379,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1272
2379
|
this.focusedSectionId = null;
|
|
1273
2380
|
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1274
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;
|
|
1275
2385
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1276
2386
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1277
2387
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1284,10 +2394,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1284
2394
|
this.isoT = 0;
|
|
1285
2395
|
this.isoTarget = 0;
|
|
1286
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;
|
|
1287
2416
|
/** Chart centre the iso projection pivots about (bounds centre). */
|
|
1288
2417
|
this.isoCentre = { x: 0, y: 0 };
|
|
1289
2418
|
/** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
|
|
1290
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;
|
|
1291
2424
|
/** Set in destroy() so an in-flight iso tween bails. */
|
|
1292
2425
|
this.destroyed = false;
|
|
1293
2426
|
/** Cached scale the section/zone labels were last sized for (scale-compensation). */
|
|
@@ -1317,6 +2450,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1317
2450
|
this.panStarted = false;
|
|
1318
2451
|
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
1319
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;
|
|
1320
2466
|
/**
|
|
1321
2467
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
1322
2468
|
* points (overlayLayer rides the stage transform); `rect` is the on-canvas
|
|
@@ -1413,6 +2559,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1413
2559
|
x: mid.x - this.pinch.worldMid.x * scale,
|
|
1414
2560
|
y: mid.y - this.pinch.worldMid.y * scale
|
|
1415
2561
|
});
|
|
2562
|
+
this.updateSeatGroupVisibility();
|
|
1416
2563
|
this.stage.batchDraw();
|
|
1417
2564
|
this.scheduleViewChange();
|
|
1418
2565
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
@@ -1425,6 +2572,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1425
2572
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
1426
2573
|
});
|
|
1427
2574
|
this.panLast = p;
|
|
2575
|
+
this.updateSeatGroupVisibility();
|
|
1428
2576
|
this.stage.batchDraw();
|
|
1429
2577
|
this.scheduleViewChange();
|
|
1430
2578
|
}
|
|
@@ -1477,6 +2625,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1477
2625
|
this.overlayLayer = new Layer({ listening: false });
|
|
1478
2626
|
this.labelGroup = new Group({ listening: false });
|
|
1479
2627
|
this.overlayLayer.add(this.labelGroup);
|
|
2628
|
+
this.fgDecorGroup = new Group({ listening: false });
|
|
2629
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1480
2630
|
this.hoverRing = new Circle({
|
|
1481
2631
|
radius: SEAT_RADIUS + 2,
|
|
1482
2632
|
stroke: "#ffffff",
|
|
@@ -1511,6 +2661,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1511
2661
|
this.resizeObs.observe(container);
|
|
1512
2662
|
}
|
|
1513
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
|
+
}
|
|
1514
2679
|
// ---- ISeatmapRenderer -----------------------------------------------------
|
|
1515
2680
|
setChart(doc, opts) {
|
|
1516
2681
|
if (doc !== this.chartDoc) this.stacked = false;
|
|
@@ -1524,8 +2689,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1524
2689
|
this.focusedId = null;
|
|
1525
2690
|
this.focusRing.visible(false);
|
|
1526
2691
|
this.bgLayer.destroyChildren();
|
|
2692
|
+
this.focusDimOverlay?.destroy();
|
|
2693
|
+
this.focusDimOverlay = null;
|
|
2694
|
+
this.seatLayer.clearCache();
|
|
2695
|
+
this.seatLayer.listening(true);
|
|
1527
2696
|
this.seatLayer.destroyChildren();
|
|
2697
|
+
this.unsectionedSeatGroup = null;
|
|
1528
2698
|
this.labelGroup.destroyChildren();
|
|
2699
|
+
this.labelLiftGroups.clear();
|
|
2700
|
+
this.fgDecorGroup.destroyChildren();
|
|
1529
2701
|
this.circleById.clear();
|
|
1530
2702
|
this.boothDims.clear();
|
|
1531
2703
|
this.boothLabelById.clear();
|
|
@@ -1542,6 +2714,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1542
2714
|
this.statusById.clear();
|
|
1543
2715
|
this.selection.clear();
|
|
1544
2716
|
this.seatById.clear();
|
|
2717
|
+
this.seatIndex = null;
|
|
1545
2718
|
this.cached = false;
|
|
1546
2719
|
this.accessFilter = null;
|
|
1547
2720
|
this.sections = [];
|
|
@@ -1559,11 +2732,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1559
2732
|
}
|
|
1560
2733
|
this.isoT = 0;
|
|
1561
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;
|
|
1562
2745
|
this.resetLayerTransforms();
|
|
1563
2746
|
this.hasSections = view.objects.some((o) => o.type === "section");
|
|
1564
2747
|
for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
|
|
1565
2748
|
this.seatLayer.opacity(1);
|
|
1566
2749
|
this.hoverRing.visible(false);
|
|
2750
|
+
this.hoveredId = null;
|
|
1567
2751
|
this.theme = doc.theme ?? {};
|
|
1568
2752
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1569
2753
|
this.container.style.background = "";
|
|
@@ -1584,26 +2768,61 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1584
2768
|
this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
|
|
1585
2769
|
}
|
|
1586
2770
|
}
|
|
1587
|
-
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
|
+
});
|
|
1588
2776
|
for (const s of this.seats) {
|
|
1589
2777
|
this.seatById.set(s.id, s);
|
|
1590
2778
|
this.statusById.set(s.id, "free");
|
|
1591
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);
|
|
1592
2784
|
this.renderBackground(view);
|
|
2785
|
+
this.unsectionedSeatGroup = new Group({ listening: true });
|
|
2786
|
+
this.seatLayer.add(this.unsectionedSeatGroup);
|
|
1593
2787
|
this.renderSeats();
|
|
1594
2788
|
this.overlayLayer.add(this.labelGroup);
|
|
2789
|
+
this.overlayLayer.add(this.fgDecorGroup);
|
|
1595
2790
|
this.overlayLayer.add(this.hoverRing);
|
|
1596
|
-
this.
|
|
1597
|
-
this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
|
|
2791
|
+
this.overlayLayer.add(this.focusRing);
|
|
1598
2792
|
this.zoomToFit();
|
|
1599
2793
|
}
|
|
1600
2794
|
/** The chart to render: all floors stacked (3D overview), the active floor, or
|
|
1601
2795
|
* the whole chart for single-floor charts. */
|
|
1602
2796
|
floorView(doc) {
|
|
1603
|
-
if (!doc.floors || !doc.floors.length) return
|
|
1604
|
-
|
|
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
|
+
}
|
|
1605
2809
|
const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
|
|
1606
|
-
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;
|
|
1607
2826
|
}
|
|
1608
2827
|
/** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
|
|
1609
2828
|
* single-floor charts. The caller re-applies statuses + animates the iso view. */
|
|
@@ -1916,7 +3135,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1916
3135
|
}
|
|
1917
3136
|
const ids = [];
|
|
1918
3137
|
for (const seat of this.seats) {
|
|
1919
|
-
|
|
3138
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3139
|
+
if (rendered.x < x0 || rendered.x > x1 || rendered.y < y0 || rendered.y > y1) continue;
|
|
1920
3140
|
if (!this.isSelectable(seat.id)) continue;
|
|
1921
3141
|
ids.push(seat.id);
|
|
1922
3142
|
}
|
|
@@ -1926,9 +3146,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1926
3146
|
flashSeat(seatId, color = "#f43f5e") {
|
|
1927
3147
|
const seat = this.seatById.get(seatId);
|
|
1928
3148
|
if (!seat) return;
|
|
3149
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
1929
3150
|
const ring = new Circle({
|
|
1930
|
-
x:
|
|
1931
|
-
y:
|
|
3151
|
+
x: rendered.x,
|
|
3152
|
+
y: rendered.y,
|
|
1932
3153
|
radius: this.seatR,
|
|
1933
3154
|
stroke: color,
|
|
1934
3155
|
strokeWidth: 3,
|
|
@@ -1937,6 +3158,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1937
3158
|
perfectDrawEnabled: false,
|
|
1938
3159
|
shadowForStrokeEnabled: false
|
|
1939
3160
|
});
|
|
3161
|
+
const projectionScale = this.viewMode === "perspective" ? this.perspectiveSeatScale.get(seatId) ?? 1 : 1;
|
|
3162
|
+
ring.scale({ x: projectionScale, y: projectionScale });
|
|
1940
3163
|
this.overlayLayer.add(ring);
|
|
1941
3164
|
const start = performance.now();
|
|
1942
3165
|
const dur = 620;
|
|
@@ -1963,17 +3186,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1963
3186
|
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
1964
3187
|
if (!matches.length) return;
|
|
1965
3188
|
for (const section of matches) {
|
|
1966
|
-
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(
|
|
1967
3194
|
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
1968
3195
|
{ x: 0, y: 0 }
|
|
1969
3196
|
);
|
|
1970
|
-
centre.x /=
|
|
1971
|
-
centre.y /=
|
|
1972
|
-
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
3197
|
+
centre.x /= outline.length;
|
|
3198
|
+
centre.y /= outline.length;
|
|
1973
3199
|
const halo = new Line({
|
|
1974
|
-
x: centre.x
|
|
1975
|
-
y: centre.y
|
|
1976
|
-
points:
|
|
3200
|
+
x: centre.x,
|
|
3201
|
+
y: centre.y,
|
|
3202
|
+
points: outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
1977
3203
|
closed: true,
|
|
1978
3204
|
stroke: color,
|
|
1979
3205
|
strokeWidth: 3,
|
|
@@ -2017,12 +3243,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2017
3243
|
nearestSeat(fromId, dir) {
|
|
2018
3244
|
const from = this.seatById.get(fromId);
|
|
2019
3245
|
if (!from) return null;
|
|
3246
|
+
const fromPoint = this.viewMode === "perspective" ? this.perspectiveSeatProjected.get(from.id) ?? from : from;
|
|
2020
3247
|
let best = null;
|
|
2021
3248
|
let bestScore = Infinity;
|
|
2022
3249
|
for (const s of this.seats) {
|
|
2023
3250
|
if (s.id === fromId) continue;
|
|
2024
|
-
const
|
|
2025
|
-
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;
|
|
2026
3254
|
const proj = dx * dir.x + dy * dir.y;
|
|
2027
3255
|
if (proj <= 0.5) continue;
|
|
2028
3256
|
const perp = Math.abs(dx * dir.y - dy * dir.x);
|
|
@@ -2039,7 +3267,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2039
3267
|
if (!seat) return;
|
|
2040
3268
|
this.focusedId = id;
|
|
2041
3269
|
this.focusRing.radius(this.seatR + 3);
|
|
2042
|
-
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 });
|
|
2043
3273
|
this.focusRing.visible(true);
|
|
2044
3274
|
this.ensureVisible(seat);
|
|
2045
3275
|
this.overlayLayer.batchDraw();
|
|
@@ -2054,7 +3284,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2054
3284
|
const margin = 70;
|
|
2055
3285
|
const offscreen = p.x < margin || p.x > w - margin || p.y < margin || p.y > h - margin;
|
|
2056
3286
|
if (this.stage.scaleX() < target || offscreen) {
|
|
2057
|
-
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);
|
|
2058
3289
|
this.stage.scale({ x: target, y: target });
|
|
2059
3290
|
this.stage.position({ x: w / 2 - ip.x * target, y: h / 2 - ip.y * target });
|
|
2060
3291
|
this.afterViewChange();
|
|
@@ -2064,7 +3295,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2064
3295
|
zoomToFit() {
|
|
2065
3296
|
const w = this.stage.width();
|
|
2066
3297
|
const h = this.stage.height();
|
|
2067
|
-
const b = this.bounds;
|
|
3298
|
+
const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
|
|
2068
3299
|
this.fitScale = Math.min(w / b.width, h / b.height) || 1;
|
|
2069
3300
|
const s = this.fitScale;
|
|
2070
3301
|
this.stage.scale({ x: s, y: s });
|
|
@@ -2093,7 +3324,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2093
3324
|
}
|
|
2094
3325
|
worldToScreen(point) {
|
|
2095
3326
|
const s = this.stage.scaleX();
|
|
2096
|
-
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);
|
|
2097
3335
|
return { x: p.x * s + this.stage.x(), y: p.y * s + this.stage.y() };
|
|
2098
3336
|
}
|
|
2099
3337
|
setAccessibleFilter(on) {
|
|
@@ -2103,6 +3341,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2103
3341
|
const next = types === null ? null : [...types];
|
|
2104
3342
|
if (this.sameAccessFilter(next)) return;
|
|
2105
3343
|
this.accessFilter = next;
|
|
3344
|
+
this.updateAccessGlyphs(this.effScale());
|
|
2106
3345
|
for (const seat of this.seats) {
|
|
2107
3346
|
const c = this.circleById.get(seat.id);
|
|
2108
3347
|
if (c) this.paintSeat(c, seat.id);
|
|
@@ -2201,10 +3440,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2201
3440
|
let minY = Infinity;
|
|
2202
3441
|
let maxY = -Infinity;
|
|
2203
3442
|
for (const seat of matching) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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);
|
|
2208
3448
|
}
|
|
2209
3449
|
const pad = Math.max(24, this.seatR * 3);
|
|
2210
3450
|
minX -= pad;
|
|
@@ -2225,12 +3465,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2225
3465
|
* inverse) keeps landing on the projected seats/sections.
|
|
2226
3466
|
*/
|
|
2227
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
|
+
}
|
|
2228
3485
|
const target = mode === "isometric" ? 1 : 0;
|
|
3486
|
+
const leavingPerspective = this.viewMode === "perspective";
|
|
3487
|
+
this.viewMode = mode;
|
|
2229
3488
|
this.isoTarget = target;
|
|
2230
3489
|
if (this.isoRaf) {
|
|
2231
3490
|
cancelAnimationFrame(this.isoRaf);
|
|
2232
3491
|
this.isoRaf = 0;
|
|
2233
3492
|
}
|
|
3493
|
+
if (leavingPerspective) {
|
|
3494
|
+
this.isoT = target;
|
|
3495
|
+
this.applyIso();
|
|
3496
|
+
this.afterViewChange();
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
2234
3499
|
if (this.reducedMotion) {
|
|
2235
3500
|
this.isoT = target;
|
|
2236
3501
|
this.applyIso();
|
|
@@ -2264,7 +3529,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2264
3529
|
}
|
|
2265
3530
|
/** Current projection — reflects the tween target, not the mid-tween isoT. */
|
|
2266
3531
|
getViewMode() {
|
|
2267
|
-
return this.
|
|
3532
|
+
return this.viewMode;
|
|
2268
3533
|
}
|
|
2269
3534
|
/** Iso angle (rad) + y-squash for the current isoT. */
|
|
2270
3535
|
isoParams() {
|
|
@@ -2272,6 +3537,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2272
3537
|
}
|
|
2273
3538
|
/** Effective vertical scale = stage scale × iso squash — legibility math uses this. */
|
|
2274
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
|
+
}
|
|
2275
3545
|
return this.stage.scaleX() * (1 - (1 - ISO_SQUASH) * this.isoT);
|
|
2276
3546
|
}
|
|
2277
3547
|
/** Project a world point through the iso affine about the chart centre (→ iso-world). */
|
|
@@ -2295,19 +3565,113 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2295
3565
|
const wy = -dx * Math.sin(th) + uy * Math.cos(th);
|
|
2296
3566
|
return { x: c.x + wx, y: c.y + wy };
|
|
2297
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
|
+
}
|
|
2298
3657
|
/**
|
|
2299
3658
|
* Local-space offset that, once the layer applies the iso affine, lifts an
|
|
2300
|
-
* object straight UP in iso-world by `
|
|
2301
|
-
* (= 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).
|
|
2302
3662
|
*/
|
|
2303
|
-
isoLiftLocal(
|
|
3663
|
+
isoLiftLocal(liftWorld) {
|
|
2304
3664
|
const { th, sg } = this.isoParams();
|
|
2305
|
-
const delta =
|
|
3665
|
+
const delta = liftWorld * this.isoT;
|
|
2306
3666
|
return { x: -(delta / sg) * Math.sin(th), y: -(delta / sg) * Math.cos(th) };
|
|
2307
3667
|
}
|
|
2308
|
-
/** Restore
|
|
2309
|
-
|
|
2310
|
-
|
|
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) {
|
|
2311
3675
|
layer.position({ x: 0, y: 0 });
|
|
2312
3676
|
layer.offset({ x: 0, y: 0 });
|
|
2313
3677
|
layer.scale({ x: 1, y: 1 });
|
|
@@ -2316,6 +3680,117 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2316
3680
|
layer.skewY(0);
|
|
2317
3681
|
}
|
|
2318
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
|
+
}
|
|
2319
3794
|
/**
|
|
2320
3795
|
* Apply the current isoT to the scene: the base rotate+squash as a decomposed
|
|
2321
3796
|
* layer transform (so seats/décor/rings project together and Konva's own
|
|
@@ -2323,6 +3798,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2323
3798
|
* lift + extruded side faces on elevated sections.
|
|
2324
3799
|
*/
|
|
2325
3800
|
applyIso() {
|
|
3801
|
+
this.resetSectionProjection();
|
|
3802
|
+
this.applyExactSeatAnchors(false);
|
|
2326
3803
|
const t2 = this.isoT;
|
|
2327
3804
|
if (t2 === 0) {
|
|
2328
3805
|
this.resetLayerTransforms();
|
|
@@ -2351,6 +3828,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2351
3828
|
}
|
|
2352
3829
|
this.applyUprightLabels();
|
|
2353
3830
|
this.applyElevation();
|
|
3831
|
+
this.updateSeatGroupVisibility();
|
|
2354
3832
|
this.bgLayer.batchDraw();
|
|
2355
3833
|
this.seatLayer.batchDraw();
|
|
2356
3834
|
this.overlayLayer.batchDraw();
|
|
@@ -2379,10 +3857,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2379
3857
|
applyElevation() {
|
|
2380
3858
|
const t2 = this.isoT;
|
|
2381
3859
|
for (const sec of this.sections) {
|
|
2382
|
-
if (sec.
|
|
2383
|
-
const off = this.isoLiftLocal(sec.
|
|
3860
|
+
if (sec.liftWorld <= 0) continue;
|
|
3861
|
+
const off = this.isoLiftLocal(sec.liftWorld);
|
|
2384
3862
|
sec.liftGroupBg?.position(off);
|
|
2385
|
-
sec.liftGroupSeat
|
|
3863
|
+
sec.liftGroupSeat.position(off);
|
|
3864
|
+
this.labelLiftGroups.get(sec.id)?.position(off);
|
|
2386
3865
|
const alpha = 0.9 * t2;
|
|
2387
3866
|
for (let i = 0; i < sec.sideFaces.length; i++) {
|
|
2388
3867
|
const face = sec.sideFaces[i];
|
|
@@ -2392,6 +3871,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2392
3871
|
face.opacity(alpha);
|
|
2393
3872
|
}
|
|
2394
3873
|
}
|
|
3874
|
+
this.syncSeatOverlayPositions();
|
|
2395
3875
|
}
|
|
2396
3876
|
destroy() {
|
|
2397
3877
|
this.destroyed = true;
|
|
@@ -2418,16 +3898,166 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2418
3898
|
* tier shifts with one offset in iso view) or the seat layer directly.
|
|
2419
3899
|
*/
|
|
2420
3900
|
seatContainer(id) {
|
|
2421
|
-
return this.seatSection.get(id)?.liftGroupSeat ?? this.seatLayer;
|
|
3901
|
+
return this.seatSection.get(id)?.liftGroupSeat ?? this.unsectionedSeatGroup ?? this.seatLayer;
|
|
2422
3902
|
}
|
|
2423
|
-
|
|
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
|
+
}
|
|
2424
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) {
|
|
2425
4044
|
if (seat.kind === "booth") {
|
|
2426
4045
|
this.renderBoothUnit(seat);
|
|
2427
4046
|
continue;
|
|
2428
4047
|
}
|
|
2429
4048
|
const target = this.seatContainer(seat.id);
|
|
2430
|
-
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({
|
|
2431
4061
|
x: seat.x,
|
|
2432
4062
|
y: seat.y,
|
|
2433
4063
|
radius: this.seatR,
|
|
@@ -2440,21 +4070,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2440
4070
|
this.paintSeat(c, seat.id);
|
|
2441
4071
|
target.add(c);
|
|
2442
4072
|
if (seat.accessible) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
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
|
+
}
|
|
2458
4092
|
}
|
|
2459
4093
|
}
|
|
2460
4094
|
}
|
|
@@ -2492,7 +4126,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2492
4126
|
t2.offsetX(t2.width() / 2);
|
|
2493
4127
|
t2.offsetY(t2.height() / 2);
|
|
2494
4128
|
t2.visible(false);
|
|
2495
|
-
this.boothLabelById.set(seat.id, t2);
|
|
2496
4129
|
this.hasBoothText = true;
|
|
2497
4130
|
this.boothLabelById.set(seat.id, t2);
|
|
2498
4131
|
target.add(t2);
|
|
@@ -2515,24 +4148,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2515
4148
|
scaleY: k,
|
|
2516
4149
|
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
2517
4150
|
listening: false,
|
|
2518
|
-
visible:
|
|
4151
|
+
visible: false,
|
|
2519
4152
|
perfectDrawEnabled: false,
|
|
2520
4153
|
shadowForStrokeEnabled: false
|
|
2521
4154
|
});
|
|
2522
4155
|
}
|
|
2523
4156
|
/**
|
|
2524
|
-
*
|
|
2525
|
-
*
|
|
2526
|
-
*
|
|
2527
|
-
* 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.
|
|
2528
4160
|
*/
|
|
2529
4161
|
updateAccessGlyphs(scale) {
|
|
2530
4162
|
if (!this.accessGlyphById.size) return;
|
|
2531
|
-
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
2532
4163
|
for (const [id, glyph] of this.accessGlyphById) {
|
|
2533
|
-
|
|
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));
|
|
2534
4174
|
}
|
|
2535
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
|
+
}
|
|
2536
4185
|
/**
|
|
2537
4186
|
* Whether an accessible seat's glyph should show for its current status. It is
|
|
2538
4187
|
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
@@ -2612,6 +4261,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2612
4261
|
c.dash([2, 2]);
|
|
2613
4262
|
break;
|
|
2614
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
|
+
}
|
|
2615
4269
|
if (boothLabel) {
|
|
2616
4270
|
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? "HELD" : seat.label);
|
|
2617
4271
|
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
@@ -2645,11 +4299,6 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2645
4299
|
c.dash([]);
|
|
2646
4300
|
c.opacity(CLOSED_SEAT_OPACITY);
|
|
2647
4301
|
}
|
|
2648
|
-
if (this.focusedSectionId) {
|
|
2649
|
-
const sec = this.seatSection.get(id);
|
|
2650
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2651
|
-
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
2652
|
-
}
|
|
2653
4302
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2654
4303
|
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2655
4304
|
if (bookableLabel) {
|
|
@@ -2663,7 +4312,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2663
4312
|
const fill = c.fill();
|
|
2664
4313
|
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
2665
4314
|
accessGlyph.opacity(c.opacity());
|
|
2666
|
-
accessGlyph.visible(this.
|
|
4315
|
+
accessGlyph.visible(this.accessGlyphShouldShow(id));
|
|
2667
4316
|
}
|
|
2668
4317
|
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
2669
4318
|
}
|
|
@@ -2732,7 +4381,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2732
4381
|
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2733
4382
|
this.focusedSectionId = id;
|
|
2734
4383
|
this.drawFocusBackdrop(id);
|
|
2735
|
-
this.
|
|
4384
|
+
this.bgLayer.batchDraw();
|
|
4385
|
+
this.overlayLayer.batchDraw();
|
|
2736
4386
|
this.updateLOD();
|
|
2737
4387
|
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
2738
4388
|
}
|
|
@@ -2744,7 +4394,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2744
4394
|
this.focusBackdrop.destroy();
|
|
2745
4395
|
this.focusBackdrop = null;
|
|
2746
4396
|
}
|
|
2747
|
-
this.
|
|
4397
|
+
this.focusDimOverlay?.destroy();
|
|
4398
|
+
this.focusDimOverlay = null;
|
|
4399
|
+
this.bgLayer.batchDraw();
|
|
4400
|
+
this.overlayLayer.batchDraw();
|
|
2748
4401
|
this.updateLOD();
|
|
2749
4402
|
}
|
|
2750
4403
|
/** The currently AXS-focused section id, or null. */
|
|
@@ -2761,16 +4414,69 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2761
4414
|
if (!sections.length) return;
|
|
2762
4415
|
const backdrop = new Group({ listening: false });
|
|
2763
4416
|
for (const section of sections) {
|
|
2764
|
-
|
|
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, {
|
|
2765
4421
|
fill: FOCUS_BACKDROP_FILL,
|
|
2766
4422
|
stroke: rgba("#ffffff", 0.1),
|
|
2767
4423
|
strokeWidth: 1,
|
|
2768
4424
|
listening: false
|
|
2769
|
-
}, section.outlinePath));
|
|
4425
|
+
}, perspective ? void 0 : section.outlinePath));
|
|
2770
4426
|
}
|
|
2771
4427
|
this.bgLayer.add(backdrop);
|
|
2772
4428
|
backdrop.moveToTop();
|
|
2773
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;
|
|
2774
4480
|
}
|
|
2775
4481
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2776
4482
|
repaintSectionsAndSeats() {
|
|
@@ -2834,7 +4540,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2834
4540
|
this.applyGAFilterState();
|
|
2835
4541
|
}
|
|
2836
4542
|
renderBackground(doc) {
|
|
2837
|
-
|
|
4543
|
+
const buyerBackground = buyerBackgroundImage(doc);
|
|
4544
|
+
if (buyerBackground) this.renderBackgroundImage(buyerBackground);
|
|
2838
4545
|
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
2839
4546
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
2840
4547
|
this.renderZones(doc);
|
|
@@ -2914,10 +4621,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2914
4621
|
listening: false,
|
|
2915
4622
|
perfectDrawEnabled: false
|
|
2916
4623
|
});
|
|
2917
|
-
this.
|
|
4624
|
+
if (obj.layer === "foreground") this.fgDecorGroup.add(node);
|
|
4625
|
+
else this.bgLayer.add(node);
|
|
2918
4626
|
img.onload = () => {
|
|
2919
|
-
|
|
2920
|
-
|
|
4627
|
+
const layer = node.getLayer();
|
|
4628
|
+
if (!layer) return;
|
|
4629
|
+
layer.batchDraw();
|
|
2921
4630
|
};
|
|
2922
4631
|
img.src = obj.href;
|
|
2923
4632
|
}
|
|
@@ -2954,7 +4663,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2954
4663
|
})
|
|
2955
4664
|
);
|
|
2956
4665
|
}
|
|
2957
|
-
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);
|
|
2958
4667
|
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
2959
4668
|
}
|
|
2960
4669
|
renderText(obj) {
|
|
@@ -2966,11 +4675,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2966
4675
|
text: obj.text,
|
|
2967
4676
|
fontSize: obj.fontSize,
|
|
2968
4677
|
rotation: obj.rotation,
|
|
4678
|
+
fontStyle: konvaFontStyle(obj.bold, obj.italic, "normal"),
|
|
2969
4679
|
// Authored ink remains preferred, but an embed/theme surface can change
|
|
2970
4680
|
// the actual canvas. Fail over to readable black/white instead of
|
|
2971
4681
|
// painting an otherwise valid caption invisibly on that active surface.
|
|
2972
4682
|
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
2973
|
-
fontFamily: this.labelFont(),
|
|
4683
|
+
fontFamily: obj.fontFamily ?? this.labelFont(),
|
|
2974
4684
|
listening: false,
|
|
2975
4685
|
perfectDrawEnabled: false
|
|
2976
4686
|
});
|
|
@@ -2988,11 +4698,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2988
4698
|
const isDecor = !!obj.role && !isStage;
|
|
2989
4699
|
const palette = overviewPalette(this.canvasBackground);
|
|
2990
4700
|
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
2991
|
-
const
|
|
2992
|
-
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;
|
|
2993
4705
|
let cx = 0;
|
|
2994
4706
|
let cy = 0;
|
|
2995
|
-
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) {
|
|
2996
4735
|
const grad = isStage ? {
|
|
2997
4736
|
fillLinearGradientStartPoint: { x: 0, y: 0 },
|
|
2998
4737
|
fillLinearGradientEndPoint: { x: 0, y: obj.height },
|
|
@@ -3012,7 +4751,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3012
4751
|
...grad,
|
|
3013
4752
|
stroke,
|
|
3014
4753
|
strokeWidth,
|
|
3015
|
-
cornerRadius: 4,
|
|
4754
|
+
cornerRadius: clamp(obj.cornerRadius ?? 4, 0, Math.min(obj.width, obj.height) / 2),
|
|
4755
|
+
opacity,
|
|
3016
4756
|
listening: false
|
|
3017
4757
|
})
|
|
3018
4758
|
);
|
|
@@ -3025,7 +4765,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3025
4765
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3026
4766
|
} : { fill };
|
|
3027
4767
|
this.bgLayer.add(
|
|
3028
|
-
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 })
|
|
3029
4769
|
);
|
|
3030
4770
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
3031
4771
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -3040,7 +4780,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3040
4780
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
3041
4781
|
} : { fill };
|
|
3042
4782
|
this.bgLayer.add(
|
|
3043
|
-
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 })
|
|
3044
4784
|
);
|
|
3045
4785
|
}
|
|
3046
4786
|
if (obj.label) {
|
|
@@ -3099,7 +4839,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3099
4839
|
strokeWidth: 1.5
|
|
3100
4840
|
});
|
|
3101
4841
|
poly.setAttr("gaId", obj.id);
|
|
3102
|
-
poly.on("click tap", () =>
|
|
4842
|
+
poly.on("click tap", (e) => {
|
|
4843
|
+
if (this.isGhostClick(e)) return;
|
|
4844
|
+
this.opts.onGAClick?.(obj.id);
|
|
4845
|
+
});
|
|
3103
4846
|
poly.on("mouseenter", () => {
|
|
3104
4847
|
this.container.style.cursor = "pointer";
|
|
3105
4848
|
});
|
|
@@ -3109,7 +4852,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3109
4852
|
this.bgLayer.add(poly);
|
|
3110
4853
|
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3111
4854
|
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3112
|
-
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);
|
|
3113
4856
|
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3114
4857
|
this.freeTextById.set(`${obj.id}:label`, {
|
|
3115
4858
|
objectId: obj.id,
|
|
@@ -3126,7 +4869,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3126
4869
|
categoryKey: obj.categoryKey
|
|
3127
4870
|
});
|
|
3128
4871
|
this.gaById.set(obj.id, {
|
|
3129
|
-
label: obj.label,
|
|
4872
|
+
label: obj.displayLabel ?? obj.label,
|
|
3130
4873
|
capacity: obj.capacity,
|
|
3131
4874
|
categoryKey: obj.categoryKey,
|
|
3132
4875
|
points: obj.points,
|
|
@@ -3149,7 +4892,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3149
4892
|
const memberIds = [];
|
|
3150
4893
|
const catCounts = /* @__PURE__ */ new Map();
|
|
3151
4894
|
let free = 0;
|
|
3152
|
-
|
|
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) {
|
|
3153
4898
|
if (this.seatSection.has(seat.id)) continue;
|
|
3154
4899
|
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
3155
4900
|
memberIds.push(seat.id);
|
|
@@ -3160,11 +4905,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3160
4905
|
[...catCounts].map(([key, w]) => ({ hex: this.catColor.get(key) ?? "#6e7bff", w })),
|
|
3161
4906
|
"#3a4358"
|
|
3162
4907
|
);
|
|
3163
|
-
const elevation =
|
|
3164
|
-
|
|
3165
|
-
|
|
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);
|
|
3166
4919
|
const sideFaces = [];
|
|
3167
|
-
if (
|
|
4920
|
+
if (liftWorld > 0) {
|
|
3168
4921
|
const faceFill = darken(this.zoneColor.get(obj.zone ?? "") ?? baseFill, 0.42);
|
|
3169
4922
|
for (let i = 0; i < obj.outline.length; i++) {
|
|
3170
4923
|
const face = new Line({
|
|
@@ -3178,14 +4931,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3178
4931
|
perfectDrawEnabled: false
|
|
3179
4932
|
});
|
|
3180
4933
|
sideFaces.push(face);
|
|
3181
|
-
|
|
4934
|
+
rootGroupBg.add(face);
|
|
3182
4935
|
}
|
|
3183
|
-
liftGroupBg = new Group({ listening: false });
|
|
3184
|
-
this.bgLayer.add(liftGroupBg);
|
|
3185
|
-
liftGroupSeat = new Group({ listening: false });
|
|
3186
|
-
this.seatLayer.add(liftGroupSeat);
|
|
3187
4936
|
}
|
|
3188
|
-
|
|
4937
|
+
liftGroupBg.moveToTop();
|
|
4938
|
+
const bgTarget = liftGroupBg;
|
|
3189
4939
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
3190
4940
|
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3191
4941
|
stroke: rgba(outlineTint, 0.5),
|
|
@@ -3259,10 +5009,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3259
5009
|
nameLabelFits: true,
|
|
3260
5010
|
subLabelFits: true,
|
|
3261
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(),
|
|
3262
5019
|
liftGroupBg,
|
|
3263
5020
|
liftGroupSeat,
|
|
3264
5021
|
sideFaces
|
|
3265
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
|
+
)));
|
|
3266
5029
|
for (const id of memberIds) this.seatSection.set(id, sec);
|
|
3267
5030
|
this.refreshSectionFill(sec);
|
|
3268
5031
|
this.refreshSectionHeat(sec);
|
|
@@ -3408,15 +5171,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3408
5171
|
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3409
5172
|
if (sectionOverview) blockT = 1;
|
|
3410
5173
|
if (!this.zones.length) zoneT = 0;
|
|
3411
|
-
this.
|
|
5174
|
+
const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
|
|
5175
|
+
this.seatLayer.opacity(sectionOverview || deferPerspectiveSeatReveal ? 0 : 1 - blockT);
|
|
3412
5176
|
const sx = this.stage.scaleX();
|
|
3413
|
-
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);
|
|
3414
5178
|
if (rescale) this.lodScale = scale;
|
|
3415
5179
|
const focus = this.focusedSectionId;
|
|
3416
5180
|
const palette = overviewPalette(this.canvasBackground);
|
|
3417
5181
|
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3418
5182
|
for (const sec of this.sections) {
|
|
3419
|
-
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;
|
|
3420
5184
|
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3421
5185
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3422
5186
|
sec.blockPoly.stroke(palette.sectionStroke);
|
|
@@ -3428,7 +5192,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3428
5192
|
);
|
|
3429
5193
|
sec.nameLabel.fill(sectionInk);
|
|
3430
5194
|
sec.subLabel.fill(sectionInk);
|
|
3431
|
-
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
5195
|
+
if (rescale && sectionLabelT > 0.01) this.fitSectionRungLabels(sec, sx);
|
|
3432
5196
|
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3433
5197
|
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3434
5198
|
sec.subLabel.opacity(0);
|
|
@@ -3440,12 +5204,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3440
5204
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3441
5205
|
if (rescale) this.sizeZonePill(zone, sx);
|
|
3442
5206
|
}
|
|
3443
|
-
this.
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
const
|
|
3447
|
-
|
|
3448
|
-
|
|
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
|
+
}
|
|
3449
5215
|
}
|
|
3450
5216
|
this.bgLayer.batchDraw();
|
|
3451
5217
|
}
|
|
@@ -3572,13 +5338,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3572
5338
|
screenToWorld(clientPoint) {
|
|
3573
5339
|
const s = this.stage.scaleX();
|
|
3574
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
|
+
}
|
|
3575
5344
|
return this.isoT === 0 ? iso : this.isoInverse(iso);
|
|
3576
5345
|
}
|
|
3577
5346
|
/** Section id under a container-relative screen point, or null (Slice 5 tap-to-zoom). */
|
|
3578
5347
|
sectionAt(clientPoint) {
|
|
3579
5348
|
if (!this.sections.length) return null;
|
|
3580
5349
|
const world = this.screenToWorld(clientPoint);
|
|
3581
|
-
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
|
+
});
|
|
3582
5365
|
return hit ? hit.logicalId : null;
|
|
3583
5366
|
}
|
|
3584
5367
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
@@ -3674,15 +5457,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3674
5457
|
if (!seat) return;
|
|
3675
5458
|
const candidate = this.selectionFocusId === id;
|
|
3676
5459
|
const dims = this.boothDims.get(seat.rowId);
|
|
5460
|
+
const rendered = this.renderedSeatPoint(seat);
|
|
3677
5461
|
const marker = new Group({
|
|
3678
5462
|
name: "selection-ring",
|
|
3679
|
-
x:
|
|
3680
|
-
y:
|
|
5463
|
+
x: rendered.x,
|
|
5464
|
+
y: rendered.y,
|
|
3681
5465
|
rotation: dims?.rotation ?? 0,
|
|
3682
5466
|
listening: false,
|
|
3683
5467
|
perfectDrawEnabled: false,
|
|
3684
5468
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
3685
5469
|
});
|
|
5470
|
+
if (this.viewMode === "perspective") {
|
|
5471
|
+
const projectionScale = this.perspectiveSeatScale.get(id) ?? 1;
|
|
5472
|
+
marker.scale({ x: projectionScale, y: projectionScale });
|
|
5473
|
+
}
|
|
3686
5474
|
marker.setAttr("seatId", id);
|
|
3687
5475
|
const common = {
|
|
3688
5476
|
stroke: this.effSelection,
|
|
@@ -3761,12 +5549,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3761
5549
|
* must fall through and pick.
|
|
3762
5550
|
*/
|
|
3763
5551
|
sectionBounds(id) {
|
|
3764
|
-
const
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
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));
|
|
3770
5568
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
3771
5569
|
}
|
|
3772
5570
|
sectionFrameScale(id) {
|
|
@@ -3819,15 +5617,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3819
5617
|
* hijacking a clean tap on empty space beyond the slop.
|
|
3820
5618
|
*/
|
|
3821
5619
|
nearestSeatToScreen(screen, slopPx) {
|
|
3822
|
-
const s = this.stage.scaleX() || 1;
|
|
3823
|
-
const reachWorld = this.seatR + slopPx / s;
|
|
3824
|
-
const world = this.screenToWorld(screen);
|
|
3825
5620
|
let best = null;
|
|
3826
|
-
let bestD =
|
|
5621
|
+
let bestD = Infinity;
|
|
5622
|
+
const stageScale = this.stage.scaleX() || 1;
|
|
3827
5623
|
for (const seat of this.seats) {
|
|
3828
5624
|
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
3829
|
-
const
|
|
3830
|
-
|
|
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) {
|
|
3831
5630
|
bestD = d;
|
|
3832
5631
|
best = seat.id;
|
|
3833
5632
|
}
|
|
@@ -3837,6 +5636,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3837
5636
|
wireInteraction() {
|
|
3838
5637
|
this.seatLayer.on("click tap", (e) => {
|
|
3839
5638
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5639
|
+
if (this.isGhostClick(e)) return;
|
|
3840
5640
|
const id = seatIdOf(e.target);
|
|
3841
5641
|
if (!id) return;
|
|
3842
5642
|
this.handleSeatTap(id);
|
|
@@ -3846,13 +5646,17 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3846
5646
|
if (!id) return;
|
|
3847
5647
|
const seat = this.seatById.get(id);
|
|
3848
5648
|
if (!seat) return;
|
|
3849
|
-
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 });
|
|
3850
5653
|
this.hoverRing.visible(true);
|
|
3851
5654
|
this.overlayLayer.batchDraw();
|
|
3852
5655
|
this.container.style.cursor = "pointer";
|
|
3853
5656
|
this.opts.onHover?.(seat);
|
|
3854
5657
|
});
|
|
3855
5658
|
this.seatLayer.on("mouseout", () => {
|
|
5659
|
+
this.hoveredId = null;
|
|
3856
5660
|
this.hoverRing.visible(false);
|
|
3857
5661
|
this.overlayLayer.batchDraw();
|
|
3858
5662
|
this.container.style.cursor = "default";
|
|
@@ -3867,6 +5671,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3867
5671
|
});
|
|
3868
5672
|
this.stage.on("click tap", (e) => {
|
|
3869
5673
|
if (this.moved > PAN_START_SLOP_PX) return;
|
|
5674
|
+
if (this.isGhostClick(e)) return;
|
|
3870
5675
|
const pointer = this.stage.getPointerPosition();
|
|
3871
5676
|
if (!pointer) return;
|
|
3872
5677
|
if (!this.cached) {
|
|
@@ -3883,11 +5688,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3883
5688
|
}
|
|
3884
5689
|
}
|
|
3885
5690
|
if (this.sections.length) {
|
|
3886
|
-
const
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
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);
|
|
3891
5695
|
return;
|
|
3892
5696
|
}
|
|
3893
5697
|
}
|
|
@@ -3904,13 +5708,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3904
5708
|
*/
|
|
3905
5709
|
deckFloorAt() {
|
|
3906
5710
|
if (!this.objectFloor.size) return null;
|
|
3907
|
-
const
|
|
3908
|
-
if (!
|
|
5711
|
+
const pointer = this.stage.getPointerPosition();
|
|
5712
|
+
if (!pointer) return null;
|
|
5713
|
+
const p = this.screenToWorld(pointer);
|
|
3909
5714
|
let best = null;
|
|
3910
5715
|
let bestD = Infinity;
|
|
3911
5716
|
for (const s of this.seats) {
|
|
3912
|
-
const
|
|
3913
|
-
const
|
|
5717
|
+
const rendered = this.renderedSeatPoint(s);
|
|
5718
|
+
const dx = rendered.x - p.x;
|
|
5719
|
+
const dy = rendered.y - p.y;
|
|
3914
5720
|
const d = dx * dx + dy * dy;
|
|
3915
5721
|
if (d < bestD) {
|
|
3916
5722
|
bestD = d;
|
|
@@ -3997,9 +5803,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3997
5803
|
}
|
|
3998
5804
|
const start = performance.now();
|
|
3999
5805
|
const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
|
|
5806
|
+
this.glideInProgress = true;
|
|
4000
5807
|
const step = (now) => {
|
|
4001
5808
|
if (this.destroyed) {
|
|
4002
5809
|
this.glideRaf = 0;
|
|
5810
|
+
this.glideInProgress = false;
|
|
4003
5811
|
return;
|
|
4004
5812
|
}
|
|
4005
5813
|
const raw = Math.min(1, (now - start) / duration);
|
|
@@ -4007,6 +5815,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4007
5815
|
const sc = fromScale + (toScale - fromScale) * e;
|
|
4008
5816
|
this.stage.scale({ x: sc, y: sc });
|
|
4009
5817
|
this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
|
|
5818
|
+
this.updateSeatGroupVisibility();
|
|
4010
5819
|
this.updateLOD();
|
|
4011
5820
|
this.scheduleViewChange();
|
|
4012
5821
|
this.stage.batchDraw();
|
|
@@ -4014,6 +5823,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4014
5823
|
this.glideRaf = requestAnimationFrame(step);
|
|
4015
5824
|
} else {
|
|
4016
5825
|
this.glideRaf = 0;
|
|
5826
|
+
this.glideInProgress = false;
|
|
4017
5827
|
this.afterViewChange();
|
|
4018
5828
|
}
|
|
4019
5829
|
};
|
|
@@ -4025,6 +5835,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4025
5835
|
cancelAnimationFrame(this.glideRaf);
|
|
4026
5836
|
this.glideRaf = 0;
|
|
4027
5837
|
}
|
|
5838
|
+
this.glideInProgress = false;
|
|
4028
5839
|
}
|
|
4029
5840
|
/** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
|
|
4030
5841
|
getRung() {
|
|
@@ -4041,11 +5852,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4041
5852
|
const labels = this.seats.map((seat) => {
|
|
4042
5853
|
const shape = this.circleById.get(seat.id);
|
|
4043
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;
|
|
4044
5857
|
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4045
|
-
const
|
|
5858
|
+
const labelEffectiveScale = this.viewMode === "perspective" && seat.kind !== "booth" ? effectiveScale * localPerspectiveScale : seatEffectiveScale;
|
|
5859
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * labelEffectiveScale);
|
|
4046
5860
|
const screen = this.worldToScreen(seat);
|
|
4047
5861
|
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4048
|
-
const opacity = shape?.
|
|
5862
|
+
const opacity = this.focusedSeatOpacity(seat.id, shape?.getAbsoluteOpacity() ?? 0);
|
|
4049
5863
|
const section = this.seatSection.get(seat.id);
|
|
4050
5864
|
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4051
5865
|
let hiddenReason;
|
|
@@ -4056,17 +5870,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4056
5870
|
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4057
5871
|
else hiddenReason = "renderer-hidden";
|
|
4058
5872
|
}
|
|
4059
|
-
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4060
|
-
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4061
|
-
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale : this.seatR * 2 *
|
|
4062
|
-
const directHeightPx = shape instanceof Rect ? shape.height() * stageScale : this.seatR * 2 *
|
|
4063
|
-
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);
|
|
4064
5878
|
const fill = shape?.fill();
|
|
4065
5879
|
const ink = label?.fill();
|
|
5880
|
+
const accessGlyph = this.accessGlyphById.get(seat.id);
|
|
5881
|
+
const accessGlyphScale = accessGlyph?.getAbsoluteScale();
|
|
4066
5882
|
return {
|
|
4067
5883
|
seatId: seat.id,
|
|
4068
5884
|
label: seat.label,
|
|
4069
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 } : {},
|
|
4070
5888
|
categoryKey: seat.categoryKey,
|
|
4071
5889
|
...section ? { sectionId: section.id } : {},
|
|
4072
5890
|
...section?.zone ? { zoneId: section.zone } : {},
|
|
@@ -4077,8 +5895,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4077
5895
|
fill: typeof fill === "string" ? fill : "",
|
|
4078
5896
|
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4079
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
|
+
} : {},
|
|
4080
5905
|
pointerTarget: {
|
|
4081
|
-
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),
|
|
4082
5907
|
directWidthPx: rounded(directWidthPx),
|
|
4083
5908
|
directHeightPx: rounded(directHeightPx),
|
|
4084
5909
|
effectiveMinimumPx: rounded(Math.max(
|
|
@@ -4106,7 +5931,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4106
5931
|
node.height(),
|
|
4107
5932
|
node.rotation()
|
|
4108
5933
|
);
|
|
4109
|
-
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
|
+
})));
|
|
4110
5939
|
const opacity = rounded(node.opacity());
|
|
4111
5940
|
const ink = node.fill();
|
|
4112
5941
|
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
@@ -4245,6 +6074,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4245
6074
|
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4246
6075
|
return {
|
|
4247
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
|
+
} : {},
|
|
4248
6086
|
canvasBackground: this.canvasBackground,
|
|
4249
6087
|
effectiveScale: rounded(effectiveScale),
|
|
4250
6088
|
rung: this.getRung(),
|
|
@@ -4299,10 +6137,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4299
6137
|
let cy = viewCentre.y;
|
|
4300
6138
|
if (rung === "sections" && this.sections.length > 0) {
|
|
4301
6139
|
const sectionCentres = this.sections.map((section) => {
|
|
4302
|
-
const
|
|
6140
|
+
const bounds2 = polyBounds(section.outline);
|
|
4303
6141
|
return {
|
|
4304
|
-
x:
|
|
4305
|
-
y:
|
|
6142
|
+
x: bounds2.x + bounds2.width / 2,
|
|
6143
|
+
y: bounds2.y + bounds2.height / 2
|
|
4306
6144
|
};
|
|
4307
6145
|
});
|
|
4308
6146
|
const halfWidth = w / (target * 2);
|
|
@@ -4369,6 +6207,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4369
6207
|
}
|
|
4370
6208
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
4371
6209
|
afterViewChange() {
|
|
6210
|
+
this.updateSeatGroupVisibility();
|
|
4372
6211
|
this.updateLOD();
|
|
4373
6212
|
this.updateFreeTextVisibility();
|
|
4374
6213
|
this.updateLabels();
|
|
@@ -4393,10 +6232,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4393
6232
|
this.paintGAStateForView();
|
|
4394
6233
|
this.updateAccessGlyphs(scale);
|
|
4395
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
|
+
}
|
|
4396
6240
|
if (shouldCache && !this.cached) {
|
|
4397
6241
|
this.cacheSeatLayer();
|
|
4398
|
-
} else if (!shouldCache && this.cached) {
|
|
4399
|
-
this.seatLayer.clearCache();
|
|
6242
|
+
} else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
|
|
6243
|
+
if (this.cached) this.seatLayer.clearCache();
|
|
4400
6244
|
this.seatLayer.listening(true);
|
|
4401
6245
|
this.cached = false;
|
|
4402
6246
|
this.seatLayer.batchDraw();
|
|
@@ -4405,6 +6249,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4405
6249
|
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
4406
6250
|
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
4407
6251
|
rebuildSeatCache() {
|
|
6252
|
+
this.updateSeatGroupVisibility();
|
|
4408
6253
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
4409
6254
|
this.seatLayer.clearCache();
|
|
4410
6255
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
@@ -4456,31 +6301,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4456
6301
|
for (const [id, label] of this.boothLabelById) {
|
|
4457
6302
|
const shape = this.circleById.get(id);
|
|
4458
6303
|
label.visible(
|
|
4459
|
-
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.
|
|
6304
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && Boolean(shape?.isVisible()) && this.focusedSeatOpacity(id, shape?.getAbsoluteOpacity() ?? 1) >= 0.5
|
|
4460
6305
|
);
|
|
4461
6306
|
}
|
|
4462
6307
|
this.labelGroup.destroyChildren();
|
|
6308
|
+
this.labelLiftGroups.clear();
|
|
4463
6309
|
this.seatLabelById.clear();
|
|
4464
6310
|
if (!show) {
|
|
4465
6311
|
this.overlayLayer.batchDraw();
|
|
4466
6312
|
return;
|
|
4467
6313
|
}
|
|
4468
|
-
const s = this.stage.scaleX();
|
|
4469
|
-
const x0 = -this.stage.x() / s;
|
|
4470
|
-
const y0 = -this.stage.y() / s;
|
|
4471
|
-
const x1 = (this.stage.width() - this.stage.x()) / s;
|
|
4472
|
-
const y1 = (this.stage.height() - this.stage.y()) / s;
|
|
4473
6314
|
let count = 0;
|
|
4474
|
-
for (const seat of this.
|
|
6315
|
+
for (const seat of this.visibleSeatCandidates()) {
|
|
4475
6316
|
if (seat.kind === "booth") continue;
|
|
4476
|
-
|
|
4477
|
-
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;
|
|
4478
6320
|
const shape = this.circleById.get(seat.id);
|
|
4479
|
-
if (
|
|
6321
|
+
if (!shape?.isVisible() || this.focusedSeatOpacity(seat.id, shape.getAbsoluteOpacity()) < 0.5) continue;
|
|
4480
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;
|
|
4481
6325
|
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
4482
6326
|
if (unavailable) {
|
|
4483
|
-
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 });
|
|
4484
6329
|
if (status === "held") {
|
|
4485
6330
|
cue.add(new Rect({
|
|
4486
6331
|
x: -4.2,
|
|
@@ -4509,14 +6354,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4509
6354
|
listening: false
|
|
4510
6355
|
}));
|
|
4511
6356
|
}
|
|
4512
|
-
this.
|
|
6357
|
+
this.seatLabelContainer(seat.id).add(cue);
|
|
4513
6358
|
if (++count >= MAX_LABELS) break;
|
|
4514
6359
|
continue;
|
|
4515
6360
|
}
|
|
4516
6361
|
const authoredFontSize = SEAT_LABEL_FONT_SIZE * this.seatLabelScale(seat);
|
|
4517
6362
|
const t2 = new Text({
|
|
4518
|
-
x:
|
|
4519
|
-
y:
|
|
6363
|
+
x: labelPoint.x,
|
|
6364
|
+
y: labelPoint.y,
|
|
4520
6365
|
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4521
6366
|
fontSize: authoredFontSize,
|
|
4522
6367
|
fontStyle: "600",
|
|
@@ -4537,11 +6382,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4537
6382
|
}
|
|
4538
6383
|
t2.offsetX(t2.width() / 2);
|
|
4539
6384
|
t2.offsetY(t2.height() / 2);
|
|
4540
|
-
|
|
6385
|
+
t2.scale({ x: perspectiveScale, y: perspectiveScale });
|
|
6386
|
+
this.seatLabelContainer(seat.id).add(t2);
|
|
4541
6387
|
this.seatLabelById.set(seat.id, t2);
|
|
4542
6388
|
if (++count >= MAX_LABELS) break;
|
|
4543
6389
|
}
|
|
4544
|
-
if (this.isoT > 0) this.applyUprightLabels();
|
|
6390
|
+
if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
|
|
4545
6391
|
this.overlayLayer.batchDraw();
|
|
4546
6392
|
}
|
|
4547
6393
|
handleResize() {
|
|
@@ -4607,6 +6453,7 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
4607
6453
|
|
|
4608
6454
|
// src/core/bestAvailable.ts
|
|
4609
6455
|
function seatIndex2(seat, fallback) {
|
|
6456
|
+
if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
|
|
4610
6457
|
const i = seat.id.lastIndexOf(":");
|
|
4611
6458
|
if (i < 0) return fallback;
|
|
4612
6459
|
const n = Number(seat.id.slice(i + 1));
|
|
@@ -4626,21 +6473,25 @@ function centroid(seats) {
|
|
|
4626
6473
|
}
|
|
4627
6474
|
return { x: x / seats.length, y: y / seats.length };
|
|
4628
6475
|
}
|
|
6476
|
+
function candidateFocal(seats, fallback) {
|
|
6477
|
+
return seats.find((seat) => seat.focalPoint)?.focalPoint ?? fallback;
|
|
6478
|
+
}
|
|
4629
6479
|
function isPremium(seat) {
|
|
4630
6480
|
return seat.commercial?.premium === true;
|
|
4631
6481
|
}
|
|
4632
6482
|
function pickBestAvailable(seats, available, opts) {
|
|
4633
6483
|
const qty = Math.floor(opts.qty);
|
|
4634
6484
|
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
4635
|
-
const { categoryKey, focal, preferPremium } = opts;
|
|
6485
|
+
const { categoryKey, zoneId, focal, preferPremium } = opts;
|
|
4636
6486
|
const rows = /* @__PURE__ */ new Map();
|
|
4637
6487
|
const eligibleAll = [];
|
|
4638
6488
|
seats.forEach((seat, i) => {
|
|
4639
|
-
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
4640
|
-
|
|
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);
|
|
4641
6492
|
if (!arr) {
|
|
4642
6493
|
arr = [];
|
|
4643
|
-
rows.set(
|
|
6494
|
+
rows.set(rowId, arr);
|
|
4644
6495
|
}
|
|
4645
6496
|
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
4646
6497
|
if (elig) eligibleAll.push(seat);
|
|
@@ -4665,7 +6516,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4665
6516
|
continue;
|
|
4666
6517
|
}
|
|
4667
6518
|
let j = i;
|
|
4668
|
-
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++;
|
|
4669
6520
|
const segLen = j - i;
|
|
4670
6521
|
for (let p = 0; p + qty <= segLen; p++) {
|
|
4671
6522
|
const leftRem = p;
|
|
@@ -4679,7 +6530,7 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4679
6530
|
startIndex: slots[i + p].index,
|
|
4680
6531
|
orphan,
|
|
4681
6532
|
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
4682
|
-
d2: dist2(centroid(runSeats), focal)
|
|
6533
|
+
d2: dist2(centroid(runSeats), candidateFocal(runSeats, focal))
|
|
4683
6534
|
};
|
|
4684
6535
|
if (better(c)) best = c;
|
|
4685
6536
|
}
|
|
@@ -4688,20 +6539,71 @@ function pickBestAvailable(seats, available, opts) {
|
|
|
4688
6539
|
}
|
|
4689
6540
|
if (best) return { labels: best.labels };
|
|
4690
6541
|
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
4691
|
-
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) => {
|
|
4692
6543
|
if (preferPremium) {
|
|
4693
6544
|
const pa = isPremium(a.seat) ? 0 : 1;
|
|
4694
6545
|
const pb = isPremium(b.seat) ? 0 : 1;
|
|
4695
6546
|
if (pa !== pb) return pa - pb;
|
|
4696
6547
|
}
|
|
4697
6548
|
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
4698
|
-
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);
|
|
4699
6550
|
if (r !== 0) return r;
|
|
4700
6551
|
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
4701
6552
|
});
|
|
4702
6553
|
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
4703
6554
|
}
|
|
4704
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
|
+
|
|
4705
6607
|
// src/picker/PickerController.ts
|
|
4706
6608
|
var DEFAULT_MAX_SELECTION = 10;
|
|
4707
6609
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -4733,6 +6635,14 @@ var PickerController = class {
|
|
|
4733
6635
|
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
4734
6636
|
this.seatContext = /* @__PURE__ */ new Map();
|
|
4735
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;
|
|
4736
6646
|
// realtime socket
|
|
4737
6647
|
this.ws = null;
|
|
4738
6648
|
this.reconnectTimer = null;
|
|
@@ -4794,6 +6704,20 @@ var PickerController = class {
|
|
|
4794
6704
|
idForLabel(label) {
|
|
4795
6705
|
return this.labelToId.get(label);
|
|
4796
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
|
+
}
|
|
4797
6721
|
/** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
|
|
4798
6722
|
async render(host) {
|
|
4799
6723
|
if (this.renderer) return null;
|
|
@@ -4807,6 +6731,29 @@ var PickerController = class {
|
|
|
4807
6731
|
}
|
|
4808
6732
|
if (this.closed) return null;
|
|
4809
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
|
+
}
|
|
4810
6757
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
4811
6758
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
4812
6759
|
this.seatById = /* @__PURE__ */ new Map();
|
|
@@ -4824,30 +6771,43 @@ var PickerController = class {
|
|
|
4824
6771
|
this.allIds.push(s.id);
|
|
4825
6772
|
const source = chartObjects.get(s.rowId);
|
|
4826
6773
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
4827
|
-
const
|
|
4828
|
-
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);
|
|
4829
6780
|
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
4830
6781
|
const labelParts = visibleSeatLabel.split("-");
|
|
4831
|
-
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;
|
|
4832
6783
|
this.seatContext.set(s.id, {
|
|
6784
|
+
objectId: s.rowId,
|
|
6785
|
+
objectType,
|
|
4833
6786
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
4834
6787
|
rowLabel,
|
|
4835
|
-
seatNumber
|
|
6788
|
+
seatNumber,
|
|
6789
|
+
...rowType ? { displayType: rowType, rowType } : {}
|
|
4836
6790
|
});
|
|
4837
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
|
+
}
|
|
4838
6798
|
const currency = res.event.currency ?? this.opts.currency;
|
|
4839
6799
|
this.currency = currency ?? "USD";
|
|
4840
6800
|
const renderer = createRenderer(host, {
|
|
4841
|
-
|
|
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(),
|
|
4842
6804
|
confirmSelection: this.opts.confirmSelection,
|
|
4843
6805
|
currency,
|
|
4844
6806
|
onSelect: (seat) => {
|
|
4845
|
-
this.
|
|
4846
|
-
this.emitSelectionChange();
|
|
6807
|
+
this.handleRendererSelect(seat);
|
|
4847
6808
|
},
|
|
4848
6809
|
onDeselect: (seat) => {
|
|
4849
|
-
this.
|
|
4850
|
-
this.emitSelectionChange();
|
|
6810
|
+
this.handleRendererDeselect(seat);
|
|
4851
6811
|
},
|
|
4852
6812
|
onSelectionLimit: this.opts.onSelectionLimit,
|
|
4853
6813
|
onHover: (seat) => {
|
|
@@ -4902,7 +6862,20 @@ var PickerController = class {
|
|
|
4902
6862
|
// ---- selection ------------------------------------------------------------
|
|
4903
6863
|
getSelection() {
|
|
4904
6864
|
if (!this.renderer) return [];
|
|
4905
|
-
|
|
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;
|
|
4906
6879
|
}
|
|
4907
6880
|
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
4908
6881
|
seatDetails(seatId) {
|
|
@@ -4911,20 +6884,168 @@ var PickerController = class {
|
|
|
4911
6884
|
}
|
|
4912
6885
|
clearSelection() {
|
|
4913
6886
|
this.renderer?.clearSelection();
|
|
6887
|
+
this.resetUnheldTableQuantities();
|
|
4914
6888
|
this.emitSelectionChange();
|
|
4915
6889
|
}
|
|
4916
6890
|
deselect(ids) {
|
|
4917
|
-
|
|
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]);
|
|
4918
6906
|
this.emitSelectionChange();
|
|
4919
6907
|
}
|
|
4920
6908
|
setMaxSelection(maxSelection) {
|
|
4921
6909
|
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
4922
|
-
this.renderer?.setMaxSelection?.(this.
|
|
6910
|
+
this.renderer?.setMaxSelection?.(this.rendererSelectionCap());
|
|
4923
6911
|
}
|
|
4924
6912
|
select(ids) {
|
|
4925
|
-
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));
|
|
4926
6927
|
if (added.length) this.emitSelectionChange();
|
|
4927
|
-
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 };
|
|
4928
7049
|
}
|
|
4929
7050
|
// ---- booking machine ------------------------------------------------------
|
|
4930
7051
|
/**
|
|
@@ -4936,7 +7057,7 @@ var PickerController = class {
|
|
|
4936
7057
|
async hold(labelsArg, ttlMs) {
|
|
4937
7058
|
const r = this.renderer;
|
|
4938
7059
|
if (!r) return null;
|
|
4939
|
-
const labels = labelsArg ??
|
|
7060
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
4940
7061
|
const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
|
|
4941
7062
|
const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
|
|
4942
7063
|
if (!combinedLabels.length) return null;
|
|
@@ -4944,10 +7065,14 @@ var PickerController = class {
|
|
|
4944
7065
|
try {
|
|
4945
7066
|
const selections = combinedLabels.map((label) => {
|
|
4946
7067
|
const ga = existingGA.find((item) => item.label === label);
|
|
4947
|
-
if (ga) return { label, tierId: ga.tierId };
|
|
7068
|
+
if (ga) return { label, tierId: ga.tierId, quantity: ga.quantity };
|
|
4948
7069
|
const seat = this.labelToSeat.get(label);
|
|
4949
7070
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
4950
|
-
return {
|
|
7071
|
+
return {
|
|
7072
|
+
label,
|
|
7073
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7074
|
+
...this.tableQuantityRequest(resolved)
|
|
7075
|
+
};
|
|
4951
7076
|
});
|
|
4952
7077
|
const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
|
|
4953
7078
|
this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
|
|
@@ -4999,11 +7124,19 @@ var PickerController = class {
|
|
|
4999
7124
|
const out = {};
|
|
5000
7125
|
const closedMembers = this.closedMemberIds();
|
|
5001
7126
|
for (const [id, s] of this.seatById) {
|
|
7127
|
+
if (this.groupedTableBySeatId.has(id)) continue;
|
|
5002
7128
|
if (closedMembers.has(id)) continue;
|
|
5003
7129
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
5004
7130
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
5005
7131
|
}
|
|
5006
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
|
+
}
|
|
5007
7140
|
return out;
|
|
5008
7141
|
}
|
|
5009
7142
|
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
@@ -5022,6 +7155,8 @@ var PickerController = class {
|
|
|
5022
7155
|
return {
|
|
5023
7156
|
id: area.id,
|
|
5024
7157
|
label: area.label,
|
|
7158
|
+
...area.displayLabel ? { displayLabel: area.displayLabel } : {},
|
|
7159
|
+
...area.displayType ? { displayType: area.displayType } : {},
|
|
5025
7160
|
capacity: Math.max(0, Math.floor(area.capacity)),
|
|
5026
7161
|
available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
|
|
5027
7162
|
categoryKey: area.categoryKey,
|
|
@@ -5040,14 +7175,20 @@ var PickerController = class {
|
|
|
5040
7175
|
const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
|
|
5041
7176
|
if (labels.length !== Math.floor(qty)) return null;
|
|
5042
7177
|
const selections = /* @__PURE__ */ new Map();
|
|
5043
|
-
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
|
+
});
|
|
5044
7183
|
if (this.hold_ && !this.hold_?.items?.length) {
|
|
5045
7184
|
for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
|
|
5046
7185
|
}
|
|
5047
|
-
for (const seat of this.
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
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
|
+
});
|
|
5051
7192
|
}
|
|
5052
7193
|
for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
|
|
5053
7194
|
const combined = [...selections.values()];
|
|
@@ -5064,6 +7205,15 @@ var PickerController = class {
|
|
|
5064
7205
|
}
|
|
5065
7206
|
return false;
|
|
5066
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
|
+
}
|
|
5067
7217
|
/**
|
|
5068
7218
|
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5069
7219
|
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
@@ -5072,11 +7222,12 @@ var PickerController = class {
|
|
|
5072
7222
|
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5073
7223
|
* exists (the caller then falls back to the normal server pick).
|
|
5074
7224
|
*/
|
|
5075
|
-
pickPremiumBlock(qty, categoryKey) {
|
|
7225
|
+
pickPremiumBlock(qty, categoryKey, zoneId) {
|
|
5076
7226
|
const doc = this.visibleDoc();
|
|
5077
7227
|
if (!doc?.objects?.length) return null;
|
|
5078
7228
|
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5079
|
-
const
|
|
7229
|
+
const groupedObjectIds = new Set(this.groupedTablesByObject.keys());
|
|
7230
|
+
const seats = expandChart(doc).filter((seat) => !groupedObjectIds.has(seat.rowId));
|
|
5080
7231
|
const held = new Set(this.hold_?.labels ?? []);
|
|
5081
7232
|
const available = /* @__PURE__ */ new Set();
|
|
5082
7233
|
for (const seat of seats) {
|
|
@@ -5085,7 +7236,7 @@ var PickerController = class {
|
|
|
5085
7236
|
const status = id ? this.getStatus(id) : void 0;
|
|
5086
7237
|
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5087
7238
|
}
|
|
5088
|
-
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
7239
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, zoneId, focal, preferPremium: true });
|
|
5089
7240
|
if (pick.labels.length !== qty) return null;
|
|
5090
7241
|
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5091
7242
|
return allPremium ? [...pick.labels] : null;
|
|
@@ -5100,7 +7251,7 @@ var PickerController = class {
|
|
|
5100
7251
|
const r = this.renderer;
|
|
5101
7252
|
if (!r) return null;
|
|
5102
7253
|
if (opts.preferPremium) {
|
|
5103
|
-
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
7254
|
+
const block = this.pickPremiumBlock(qty, categoryKey, opts.zoneId);
|
|
5104
7255
|
if (block) {
|
|
5105
7256
|
if (this.hold_ && !await this.release()) return null;
|
|
5106
7257
|
try {
|
|
@@ -5111,7 +7262,7 @@ var PickerController = class {
|
|
|
5111
7262
|
});
|
|
5112
7263
|
const result = await this.api.hold(this.key, selection);
|
|
5113
7264
|
r.clearSelection();
|
|
5114
|
-
const ids =
|
|
7265
|
+
const ids = this.idsForLabels(block);
|
|
5115
7266
|
if (ids.length) r.setStatus(ids, "held");
|
|
5116
7267
|
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5117
7268
|
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5128,9 +7279,9 @@ var PickerController = class {
|
|
|
5128
7279
|
}
|
|
5129
7280
|
if (this.hold_ && !await this.release()) return null;
|
|
5130
7281
|
try {
|
|
5131
|
-
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
7282
|
+
const result = await this.api.bestAvailable(this.key, qty, categoryKey, opts.zoneId);
|
|
5132
7283
|
r.clearSelection();
|
|
5133
|
-
const ids =
|
|
7284
|
+
const ids = this.idsForLabels(result.labels);
|
|
5134
7285
|
if (ids.length) r.setStatus(ids, "held");
|
|
5135
7286
|
this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
|
|
5136
7287
|
const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
@@ -5154,7 +7305,7 @@ var PickerController = class {
|
|
|
5154
7305
|
const r = this.renderer;
|
|
5155
7306
|
if (!r) return null;
|
|
5156
7307
|
if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
|
|
5157
|
-
const labels = labelsArg ??
|
|
7308
|
+
const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
|
|
5158
7309
|
if (!labels.length) return null;
|
|
5159
7310
|
let holdId;
|
|
5160
7311
|
try {
|
|
@@ -5167,7 +7318,11 @@ var PickerController = class {
|
|
|
5167
7318
|
labels.map((label) => {
|
|
5168
7319
|
const seat = this.labelToSeat.get(label);
|
|
5169
7320
|
const resolved = seat ? this.toSeat(seat) : null;
|
|
5170
|
-
return {
|
|
7321
|
+
return {
|
|
7322
|
+
label,
|
|
7323
|
+
...resolved?.tierId ? { tierId: resolved.tierId } : {},
|
|
7324
|
+
...this.tableQuantityRequest(resolved)
|
|
7325
|
+
};
|
|
5171
7326
|
}),
|
|
5172
7327
|
void 0,
|
|
5173
7328
|
replaceHoldId
|
|
@@ -5183,10 +7338,11 @@ var PickerController = class {
|
|
|
5183
7338
|
this.opts.onSalesClosed?.();
|
|
5184
7339
|
} else if (status === 409 && conflicts?.length) {
|
|
5185
7340
|
const takenLabels = new Set(conflicts.map((c) => c.label));
|
|
5186
|
-
const takenIds =
|
|
7341
|
+
const takenIds = this.idsForLabels(takenLabels);
|
|
5187
7342
|
if (takenIds.length) {
|
|
5188
7343
|
r.setStatus(takenIds, "booked");
|
|
5189
7344
|
r.deselect(takenIds);
|
|
7345
|
+
this.resetTableQuantitiesForLabels(takenLabels);
|
|
5190
7346
|
}
|
|
5191
7347
|
const stillFree = labels.filter((l) => !takenLabels.has(l));
|
|
5192
7348
|
if (stillFree.length && holdId) void this.api.release(this.key, stillFree, holdId).catch(() => {
|
|
@@ -5198,12 +7354,13 @@ var PickerController = class {
|
|
|
5198
7354
|
}
|
|
5199
7355
|
throw err;
|
|
5200
7356
|
}
|
|
5201
|
-
const ids =
|
|
7357
|
+
const ids = this.idsForLabels(labels);
|
|
5202
7358
|
if (ids.length) {
|
|
5203
7359
|
r.setStatus(ids, "booked");
|
|
5204
7360
|
r.deselect(ids);
|
|
5205
7361
|
}
|
|
5206
7362
|
this.clearHold();
|
|
7363
|
+
this.resetTableQuantitiesForLabels(labels);
|
|
5207
7364
|
this.emitSelectionChange();
|
|
5208
7365
|
this.opts.onBook?.(bookingRef);
|
|
5209
7366
|
return labels;
|
|
@@ -5224,7 +7381,8 @@ var PickerController = class {
|
|
|
5224
7381
|
}
|
|
5225
7382
|
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
5226
7383
|
this.clearHold();
|
|
5227
|
-
|
|
7384
|
+
this.resetTableQuantitiesForLabels(hold.labels);
|
|
7385
|
+
const ids = this.idsForLabels(hold.labels);
|
|
5228
7386
|
if (ids.length) {
|
|
5229
7387
|
this.renderer?.deselect(ids);
|
|
5230
7388
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5255,7 +7413,8 @@ var PickerController = class {
|
|
|
5255
7413
|
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
5256
7414
|
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
5257
7415
|
else this.clearHold();
|
|
5258
|
-
|
|
7416
|
+
this.resetTableQuantitiesForLabels(drop);
|
|
7417
|
+
const ids = this.idsForLabels(drop);
|
|
5259
7418
|
if (ids.length) {
|
|
5260
7419
|
this.renderer?.deselect(ids);
|
|
5261
7420
|
this.renderer?.setStatus(ids, "free");
|
|
@@ -5344,7 +7503,7 @@ var PickerController = class {
|
|
|
5344
7503
|
this.opts.onDeckTap?.(floorId);
|
|
5345
7504
|
}
|
|
5346
7505
|
// ---- big-venue: sections / rungs / projection (Slice 5) -------------------
|
|
5347
|
-
/** Switch the map projection
|
|
7506
|
+
/** Switch the map projection. No-op on a flat-only renderer. */
|
|
5348
7507
|
setViewMode(mode) {
|
|
5349
7508
|
this.renderer?.setViewMode?.(mode);
|
|
5350
7509
|
}
|
|
@@ -5443,12 +7602,21 @@ var PickerController = class {
|
|
|
5443
7602
|
if (!sec) return null;
|
|
5444
7603
|
const memberIds = r.sectionMembers?.(id) ?? [];
|
|
5445
7604
|
const byCat = /* @__PURE__ */ new Map();
|
|
7605
|
+
const seenTables = /* @__PURE__ */ new Set();
|
|
5446
7606
|
let seatsLeft = 0;
|
|
5447
7607
|
for (const sid of memberIds) {
|
|
5448
7608
|
const seat = this.seatById.get(sid);
|
|
5449
7609
|
if (!seat) continue;
|
|
5450
|
-
|
|
5451
|
-
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
|
+
}
|
|
5452
7620
|
}
|
|
5453
7621
|
const categories = [...byCat.entries()].map(([key, count]) => {
|
|
5454
7622
|
const cat = doc.categories.find((c) => c.key === key);
|
|
@@ -5467,6 +7635,7 @@ var PickerController = class {
|
|
|
5467
7635
|
id,
|
|
5468
7636
|
label: sec.label,
|
|
5469
7637
|
zoneLabel: zone?.label ?? "",
|
|
7638
|
+
...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
|
|
5470
7639
|
color,
|
|
5471
7640
|
seatsLeft,
|
|
5472
7641
|
priceMin: prices.length ? prices[0] : 0,
|
|
@@ -5497,22 +7666,54 @@ var PickerController = class {
|
|
|
5497
7666
|
}
|
|
5498
7667
|
// ---- internals ------------------------------------------------------------
|
|
5499
7668
|
toSeat(s) {
|
|
7669
|
+
const table = this.groupedTableBySeatId.get(s.id);
|
|
7670
|
+
if (table) return this.toTableSeat(table);
|
|
5500
7671
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
5501
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);
|
|
5502
7676
|
const tiers = this.tiersFor(s.categoryKey);
|
|
5503
7677
|
if (!tiers) {
|
|
5504
|
-
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 };
|
|
5505
7679
|
}
|
|
5506
7680
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
5507
7681
|
return {
|
|
5508
7682
|
id: s.id,
|
|
5509
7683
|
label: s.label,
|
|
5510
7684
|
...display,
|
|
7685
|
+
...context,
|
|
5511
7686
|
categoryKey: s.categoryKey,
|
|
5512
7687
|
price: chosen.price,
|
|
5513
7688
|
tiers,
|
|
5514
7689
|
tierId: chosen.id,
|
|
5515
|
-
...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)
|
|
5516
7717
|
};
|
|
5517
7718
|
}
|
|
5518
7719
|
/** Tooltip payload for a hovered seat — see PickerCallbacks.onSeatHover. */
|
|
@@ -5596,10 +7797,13 @@ var PickerController = class {
|
|
|
5596
7797
|
const h = this.hold_;
|
|
5597
7798
|
if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
|
|
5598
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]));
|
|
5599
7801
|
return labels.every((label) => {
|
|
5600
7802
|
const seat = this.labelToSeat.get(label);
|
|
5601
|
-
const
|
|
5602
|
-
|
|
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);
|
|
5603
7807
|
});
|
|
5604
7808
|
}
|
|
5605
7809
|
handle409Conflicts(err) {
|
|
@@ -5607,19 +7811,28 @@ var PickerController = class {
|
|
|
5607
7811
|
if (status !== 409 || !conflicts?.length) return;
|
|
5608
7812
|
const r = this.renderer;
|
|
5609
7813
|
if (!r) return;
|
|
5610
|
-
const takenIds = conflicts.map((
|
|
7814
|
+
const takenIds = this.idsForLabels(conflicts.map((conflict) => conflict.label));
|
|
5611
7815
|
if (takenIds.length) {
|
|
5612
7816
|
r.deselect(takenIds);
|
|
5613
7817
|
r.setStatus(takenIds, "held");
|
|
7818
|
+
this.resetTableQuantitiesForLabels(conflicts.map((conflict) => conflict.label));
|
|
5614
7819
|
}
|
|
5615
7820
|
this.emitSelectionChange();
|
|
5616
7821
|
}
|
|
5617
7822
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
5618
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
|
+
}
|
|
5619
7832
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
5620
7833
|
this.hold_ = full;
|
|
5621
7834
|
this.renderer?.setOwnedHold?.(
|
|
5622
|
-
|
|
7835
|
+
this.idsForLabels(full.labels)
|
|
5623
7836
|
);
|
|
5624
7837
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
5625
7838
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
@@ -5662,12 +7875,11 @@ var PickerController = class {
|
|
|
5662
7875
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
5663
7876
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
5664
7877
|
r.setOwnedHold?.(
|
|
5665
|
-
(this.hold_?.labels ?? [])
|
|
7878
|
+
this.idsForLabels(this.hold_?.labels ?? [])
|
|
5666
7879
|
);
|
|
5667
7880
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
5668
7881
|
for (const [label, st] of Object.entries(seats)) {
|
|
5669
|
-
|
|
5670
|
-
if (id) byStatus[mapStatus(st)].push(id);
|
|
7882
|
+
byStatus[mapStatus(st)].push(...this.idsForLabel(label));
|
|
5671
7883
|
}
|
|
5672
7884
|
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
5673
7885
|
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
@@ -5676,7 +7888,11 @@ var PickerController = class {
|
|
|
5676
7888
|
this.clearBookedHoldIfSettled();
|
|
5677
7889
|
}
|
|
5678
7890
|
clearBookedHoldIfSettled() {
|
|
5679
|
-
|
|
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
|
+
}
|
|
5680
7896
|
}
|
|
5681
7897
|
async resnapshot() {
|
|
5682
7898
|
try {
|
|
@@ -5765,13 +7981,13 @@ var PickerController = class {
|
|
|
5765
7981
|
} else if (Array.isArray(m.changes)) {
|
|
5766
7982
|
for (const ch of m.changes) {
|
|
5767
7983
|
this.liveStatuses.set(ch.label, ch.status);
|
|
5768
|
-
const
|
|
5769
|
-
if (!
|
|
7984
|
+
const ids = this.idsForLabel(ch.label);
|
|
7985
|
+
if (!ids.length) continue;
|
|
5770
7986
|
const next = mapStatus(ch.status);
|
|
5771
|
-
if (this.opts.flashOnLiveChange && next !== "free" && r.getStatus(id) === "free" && !this.hold_?.labels.includes(ch.label)) {
|
|
5772
|
-
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"));
|
|
5773
7989
|
}
|
|
5774
|
-
r.setStatus(
|
|
7990
|
+
r.setStatus(ids, next);
|
|
5775
7991
|
}
|
|
5776
7992
|
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
5777
7993
|
r.forceDraw();
|
|
@@ -5802,10 +8018,25 @@ var PickerController = class {
|
|
|
5802
8018
|
}
|
|
5803
8019
|
};
|
|
5804
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
|
+
|
|
5805
8036
|
// src/view/generatePanorama.ts
|
|
5806
8037
|
var W = 2048;
|
|
5807
8038
|
var H = 1024;
|
|
5808
|
-
var UNIT =
|
|
8039
|
+
var UNIT = METRES_PER_CHART_UNIT;
|
|
5809
8040
|
var yawToX = (yawDeg) => (yawDeg + 180) / 360 * W;
|
|
5810
8041
|
var pitchToY = (pitchDeg) => (90 - pitchDeg) / 180 * H;
|
|
5811
8042
|
function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
@@ -5825,9 +8056,11 @@ function generateSeatPanorama(seat, focalPoint, neighborSeats) {
|
|
|
5825
8056
|
sky.addColorStop(1, "#07090f");
|
|
5826
8057
|
ctx.fillStyle = sky;
|
|
5827
8058
|
ctx.fillRect(0, 0, W, H);
|
|
8059
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
5828
8060
|
const stageHalfYaw = Math.min(80, Math.atan2(4, distM) * 180 / Math.PI);
|
|
5829
|
-
const
|
|
5830
|
-
const
|
|
8061
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
8062
|
+
const stageTopPitch = Math.min(45, sightline.topPitch);
|
|
8063
|
+
const stageBasePitch = sightline.basePitch;
|
|
5831
8064
|
const sx0 = yawToX(-stageHalfYaw);
|
|
5832
8065
|
const sx1 = yawToX(stageHalfYaw);
|
|
5833
8066
|
const sy0 = pitchToY(stageTopPitch);
|
|
@@ -5934,9 +8167,11 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
5934
8167
|
sky.addColorStop(1, "#070910");
|
|
5935
8168
|
ctx.fillStyle = sky;
|
|
5936
8169
|
ctx.fillRect(0, 0, TW, TH);
|
|
8170
|
+
const eyeM = seat.eyeHeightM ?? SEATED_EYE_HEIGHT_M;
|
|
8171
|
+
const sightline = stageSightlinePitch(eyeM, distM);
|
|
5937
8172
|
const stageHalfYaw = Math.min(HALF_HFOV - 2, Math.atan2(4, distM) * 180 / Math.PI);
|
|
5938
|
-
const stageTopPitch = Math.min(HALF_VFOV - 2,
|
|
5939
|
-
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);
|
|
5940
8175
|
const sx0 = yawToTX(-stageHalfYaw);
|
|
5941
8176
|
const sx1 = yawToTX(stageHalfYaw);
|
|
5942
8177
|
const sy0 = pitchToTY(stageTopPitch);
|
|
@@ -6000,9 +8235,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
6000
8235
|
|
|
6001
8236
|
// src/i18n/bundles.ts
|
|
6002
8237
|
var LOADERS = {
|
|
6003
|
-
es: () => import("./es-
|
|
6004
|
-
de: () => import("./de-
|
|
6005
|
-
fr: () => import("./fr-
|
|
8238
|
+
es: () => import("./es-SKAODLTR.js").then((m) => ({ default: m.es })),
|
|
8239
|
+
de: () => import("./de-73TIXYJH.js").then((m) => ({ default: m.de })),
|
|
8240
|
+
fr: () => import("./fr-J4637T6V.js").then((m) => ({ default: m.fr }))
|
|
6006
8241
|
};
|
|
6007
8242
|
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
6008
8243
|
async function loadLocale(code) {
|
|
@@ -6031,8 +8266,11 @@ export {
|
|
|
6031
8266
|
MAX_EVENT_INVENTORY,
|
|
6032
8267
|
MAX_GA_CAPACITY,
|
|
6033
8268
|
PickerController,
|
|
8269
|
+
RENDERED_QUALITY_REPORT_VERSION,
|
|
6034
8270
|
SUPPORTED_LOCALES,
|
|
8271
|
+
SURROUNDINGS_SHAPE_ROLES,
|
|
6035
8272
|
SeatmapRenderer,
|
|
8273
|
+
TIER_HEIGHT_M,
|
|
6036
8274
|
UNGROUPED_ID,
|
|
6037
8275
|
accessibilityMeta,
|
|
6038
8276
|
accessibilityRingColor,
|
|
@@ -6047,33 +8285,43 @@ export {
|
|
|
6047
8285
|
expandRow,
|
|
6048
8286
|
expandRowSlots,
|
|
6049
8287
|
expandTable,
|
|
8288
|
+
expandTableSlots,
|
|
6050
8289
|
floorObjects,
|
|
6051
8290
|
floorsOf,
|
|
6052
8291
|
formatDate,
|
|
6053
8292
|
formatMoney,
|
|
6054
8293
|
gaAreasOf,
|
|
8294
|
+
gaInventorySegments,
|
|
6055
8295
|
gaUnitLabel,
|
|
6056
8296
|
gaUnitLabels,
|
|
6057
8297
|
generateSeatPanorama,
|
|
6058
8298
|
generateSeatThumb,
|
|
6059
8299
|
getLocale,
|
|
8300
|
+
growJoinedGAInventory,
|
|
6060
8301
|
hiddenObjectIds,
|
|
8302
|
+
inspectRenderedQualityEvidence,
|
|
6061
8303
|
isGaUnitLabel,
|
|
6062
8304
|
isSectionHidden,
|
|
6063
8305
|
layerOf,
|
|
6064
8306
|
loadLocale,
|
|
6065
8307
|
objectCenter,
|
|
8308
|
+
owningSectionForObject,
|
|
6066
8309
|
pointInPolygon,
|
|
6067
8310
|
pointInPolygonWithHoles,
|
|
6068
8311
|
polygonLabelPoint,
|
|
6069
8312
|
resolveLocale,
|
|
8313
|
+
rowInventoryCount,
|
|
6070
8314
|
rowSeatPositions,
|
|
6071
8315
|
seatLabelPart,
|
|
8316
|
+
sectionGeometry,
|
|
6072
8317
|
setLocale,
|
|
6073
8318
|
setMoneyLocale,
|
|
6074
8319
|
setStringOverrides,
|
|
6075
8320
|
stackFloors,
|
|
6076
8321
|
t,
|
|
6077
|
-
tCount
|
|
8322
|
+
tCount,
|
|
8323
|
+
tableInventoryCount,
|
|
8324
|
+
tableSeatCountsBySide,
|
|
8325
|
+
validGAInventorySegments
|
|
6078
8326
|
};
|
|
6079
8327
|
//# sourceMappingURL=index.js.map
|