@seatlayer/core 0.16.1 → 0.18.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-PQ5BE3PL.js → de-ZTKJOFHT.js} +3 -1
- package/dist/de-ZTKJOFHT.js.map +1 -0
- package/dist/{es-EWHL5CZR.js → es-NLD3NL3W.js} +3 -1
- package/dist/es-NLD3NL3W.js.map +1 -0
- package/dist/{fr-FIEG227H.js → fr-KSFKWSAT.js} +3 -1
- package/dist/fr-KSFKWSAT.js.map +1 -0
- package/dist/index.cjs +1266 -279
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -31
- package/dist/index.d.ts +496 -31
- package/dist/index.js +1257 -282
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-PQ5BE3PL.js.map +0 -1
- package/dist/es-EWHL5CZR.js.map +0 -1
- package/dist/fr-FIEG227H.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,21 @@ var ACCESSIBILITY_LABEL = new Map(ACCESSIBILITY_TYPES.map((a) => [a.key, a]));
|
|
|
12
12
|
function accessibilityMeta(key) {
|
|
13
13
|
return ACCESSIBILITY_LABEL.get(key);
|
|
14
14
|
}
|
|
15
|
+
var ACCESSIBILITY_RING_COLOR = {
|
|
16
|
+
wheelchair: "#3b82f6",
|
|
17
|
+
companion: "#8b5cf6",
|
|
18
|
+
"semi-ambulatory": "#0ea5e9",
|
|
19
|
+
hearing: "#14b8a6",
|
|
20
|
+
"sign-language": "#f59e0b",
|
|
21
|
+
"plus-size": "#ec4899",
|
|
22
|
+
"lift-armrest": "#22c55e"
|
|
23
|
+
};
|
|
24
|
+
function accessibilityRingColor(types) {
|
|
25
|
+
const primary = types?.[0];
|
|
26
|
+
return primary && ACCESSIBILITY_RING_COLOR[primary] || "#3b82f6";
|
|
27
|
+
}
|
|
28
|
+
var LABEL_STYLE_MIN_SIZE = 8;
|
|
29
|
+
var LABEL_STYLE_MAX_SIZE = 24;
|
|
15
30
|
function layerOf(obj) {
|
|
16
31
|
switch (obj.type) {
|
|
17
32
|
case "row":
|
|
@@ -31,6 +46,71 @@ function layerOf(obj) {
|
|
|
31
46
|
}
|
|
32
47
|
var CHART_STORAGE_KEY = "seatmap.chart";
|
|
33
48
|
|
|
49
|
+
// src/core/complexGeometry.ts
|
|
50
|
+
function cubicPoint(path, t2) {
|
|
51
|
+
const u = 1 - t2;
|
|
52
|
+
const a = u * u * u;
|
|
53
|
+
const b = 3 * u * u * t2;
|
|
54
|
+
const c = 3 * u * t2 * t2;
|
|
55
|
+
const d = t2 * t2 * t2;
|
|
56
|
+
return {
|
|
57
|
+
x: a * path.start.x + b * path.control1.x + c * path.control2.x + d * path.end.x,
|
|
58
|
+
y: a * path.start.y + b * path.control1.y + c * path.control2.y + d * path.end.y
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function distributeAlongCubic(path, count, resolution = 192) {
|
|
62
|
+
if (!Number.isInteger(count) || count < 1) throw new Error("Path point count must be a positive integer");
|
|
63
|
+
if (count === 1) return [cubicPoint(path, 0.5)];
|
|
64
|
+
const samples = Array.from({ length: resolution + 1 }, (_, index) => cubicPoint(path, index / resolution));
|
|
65
|
+
const lengths = new Float64Array(samples.length);
|
|
66
|
+
for (let index = 1; index < samples.length; index += 1) {
|
|
67
|
+
const dx = samples[index].x - samples[index - 1].x;
|
|
68
|
+
const dy = samples[index].y - samples[index - 1].y;
|
|
69
|
+
lengths[index] = lengths[index - 1] + Math.hypot(dx, dy);
|
|
70
|
+
}
|
|
71
|
+
const total = lengths[lengths.length - 1];
|
|
72
|
+
if (total <= 1e-9) return Array.from({ length: count }, () => ({ ...path.start }));
|
|
73
|
+
const output = [];
|
|
74
|
+
let segment = 1;
|
|
75
|
+
for (let index = 0; index < count; index += 1) {
|
|
76
|
+
const target = total * index / (count - 1);
|
|
77
|
+
while (segment < lengths.length - 1 && lengths[segment] < target) segment += 1;
|
|
78
|
+
const before = lengths[segment - 1];
|
|
79
|
+
const after = lengths[segment];
|
|
80
|
+
const ratio = after === before ? 0 : (target - before) / (after - before);
|
|
81
|
+
output.push({
|
|
82
|
+
x: samples[segment - 1].x + (samples[segment].x - samples[segment - 1].x) * ratio,
|
|
83
|
+
y: samples[segment - 1].y + (samples[segment].y - samples[segment - 1].y) * ratio
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/core/sectionPath.ts
|
|
90
|
+
var TAU = Math.PI * 2;
|
|
91
|
+
function translateSectionOutlinePath(path, dx, dy) {
|
|
92
|
+
const translate = (point) => ({ x: point.x + dx, y: point.y + dy });
|
|
93
|
+
return transformSectionOutlinePath(path, translate);
|
|
94
|
+
}
|
|
95
|
+
function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected = false) {
|
|
96
|
+
return {
|
|
97
|
+
...path,
|
|
98
|
+
start: transform(path.start),
|
|
99
|
+
segments: path.segments.map((segment) => segment.kind === "line" ? { ...segment, end: transform(segment.end) } : segment.kind === "arc" ? {
|
|
100
|
+
...segment,
|
|
101
|
+
center: transform(segment.center),
|
|
102
|
+
radius: segment.radius * Math.abs(radiusScale),
|
|
103
|
+
clockwise: reflected ? !segment.clockwise : segment.clockwise,
|
|
104
|
+
end: transform(segment.end)
|
|
105
|
+
} : {
|
|
106
|
+
...segment,
|
|
107
|
+
control1: transform(segment.control1),
|
|
108
|
+
control2: transform(segment.control2),
|
|
109
|
+
end: transform(segment.end)
|
|
110
|
+
})
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
34
114
|
// src/core/layout.ts
|
|
35
115
|
function overrideAccessibility(o) {
|
|
36
116
|
if (!o) return [];
|
|
@@ -52,6 +132,7 @@ function place(lx, ly, deg, origin) {
|
|
|
52
132
|
function rowSeatPositions(row) {
|
|
53
133
|
const { seatCount, seatSpacing, curve, rotation, origin } = row;
|
|
54
134
|
const out = [];
|
|
135
|
+
if (row.path) return distributeAlongCubic(row.path, seatCount);
|
|
55
136
|
if (seatCount <= 1) {
|
|
56
137
|
if (seatCount === 1) out.push({ x: origin.x, y: origin.y });
|
|
57
138
|
return out;
|
|
@@ -92,15 +173,21 @@ function expandRowSlots(row) {
|
|
|
92
173
|
return rowSeatPositions(row).map((p, i) => {
|
|
93
174
|
const o = ov.get(i);
|
|
94
175
|
const accessibility = overrideAccessibility(o);
|
|
176
|
+
const inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
|
|
177
|
+
const displayPrefix = row.displayLabel ?? row.label;
|
|
178
|
+
const commercial = { ...row.commercial, ...o?.commercial };
|
|
95
179
|
return {
|
|
96
180
|
index: i,
|
|
97
181
|
x: p.x + (o?.dx ?? 0),
|
|
98
182
|
y: p.y + (o?.dy ?? 0),
|
|
99
|
-
label:
|
|
183
|
+
label: inventoryLabel,
|
|
184
|
+
displayLabel: o?.displayLabel ?? `${displayPrefix}-${seatNumber(i)}`,
|
|
100
185
|
categoryKey: o?.categoryKey ?? row.categoryKey,
|
|
101
186
|
skipped: !!o?.skip,
|
|
102
187
|
accessible: accessibility.length > 0,
|
|
103
|
-
accessibility
|
|
188
|
+
accessibility,
|
|
189
|
+
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
190
|
+
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl
|
|
104
191
|
};
|
|
105
192
|
});
|
|
106
193
|
}
|
|
@@ -111,13 +198,15 @@ function expandRow(row) {
|
|
|
111
198
|
seats.push({
|
|
112
199
|
id: `${row.id}:${slot.index}`,
|
|
113
200
|
label: slot.label,
|
|
201
|
+
displayLabel: slot.displayLabel === slot.label ? void 0 : slot.displayLabel,
|
|
114
202
|
x: slot.x,
|
|
115
203
|
y: slot.y,
|
|
116
204
|
rowId: row.id,
|
|
117
205
|
categoryKey: slot.categoryKey,
|
|
118
206
|
accessible: slot.accessible || void 0,
|
|
119
207
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
120
|
-
|
|
208
|
+
commercial: slot.commercial,
|
|
209
|
+
viewUrl: slot.viewUrl
|
|
121
210
|
});
|
|
122
211
|
}
|
|
123
212
|
return seats;
|
|
@@ -214,6 +303,53 @@ function pointInPolygon(p, poly) {
|
|
|
214
303
|
}
|
|
215
304
|
return inside;
|
|
216
305
|
}
|
|
306
|
+
function pointOnPolygonBoundary(p, poly) {
|
|
307
|
+
return poly.some((start, index) => {
|
|
308
|
+
const end = poly[(index + 1) % poly.length];
|
|
309
|
+
const cross = (p.y - start.y) * (end.x - start.x) - (p.x - start.x) * (end.y - start.y);
|
|
310
|
+
if (Math.abs(cross) > 1e-7) return false;
|
|
311
|
+
const dot = (p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y);
|
|
312
|
+
const lengthSquared = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
313
|
+
return dot >= -1e-7 && dot <= lengthSquared + 1e-7;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function pointInPolygonWithHoles(p, outer, holes) {
|
|
317
|
+
return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
|
|
318
|
+
}
|
|
319
|
+
function polygonLabelPoint(outer, holes) {
|
|
320
|
+
if (!outer.length) return { x: 0, y: 0 };
|
|
321
|
+
const xs = outer.map((point) => point.x);
|
|
322
|
+
const ys = outer.map((point) => point.y);
|
|
323
|
+
const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
324
|
+
const centroid = polygonCentroid(outer);
|
|
325
|
+
if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
|
|
326
|
+
let best = outer[0];
|
|
327
|
+
let bestScore = -Infinity;
|
|
328
|
+
const rings = [outer, ...holes ?? []];
|
|
329
|
+
for (let row = 1; row < 24; row += 1) {
|
|
330
|
+
for (let column = 1; column < 24; column += 1) {
|
|
331
|
+
const point = {
|
|
332
|
+
x: bounds.minX + (bounds.maxX - bounds.minX) * column / 24,
|
|
333
|
+
y: bounds.minY + (bounds.maxY - bounds.minY) * row / 24
|
|
334
|
+
};
|
|
335
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
336
|
+
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
337
|
+
const end = ring[(index + 1) % ring.length];
|
|
338
|
+
const dx = end.x - start.x;
|
|
339
|
+
const dy = end.y - start.y;
|
|
340
|
+
const denominator = dx * dx + dy * dy;
|
|
341
|
+
const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
|
|
342
|
+
const t2 = Math.max(0, Math.min(1, projection));
|
|
343
|
+
return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
|
|
344
|
+
})));
|
|
345
|
+
if (score > bestScore) {
|
|
346
|
+
best = point;
|
|
347
|
+
bestScore = score;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return best;
|
|
352
|
+
}
|
|
217
353
|
function polygonCentroid(pts) {
|
|
218
354
|
if (!pts.length) return { x: 0, y: 0 };
|
|
219
355
|
let x = 0;
|
|
@@ -277,9 +413,14 @@ function translateObject(o, dx, dy) {
|
|
|
277
413
|
case "booth":
|
|
278
414
|
return { ...o, center: p(o.center) };
|
|
279
415
|
case "gaArea":
|
|
280
|
-
return { ...o, points: pts(o.points) };
|
|
416
|
+
return { ...o, points: pts(o.points), ...o.holes ? { holes: o.holes.map(pts) } : {} };
|
|
281
417
|
case "section":
|
|
282
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
...o,
|
|
420
|
+
outline: pts(o.outline),
|
|
421
|
+
...o.outlinePath ? { outlinePath: translateSectionOutlinePath(o.outlinePath, dx, dy) } : {},
|
|
422
|
+
...o.holes ? { holes: o.holes.map(pts) } : {}
|
|
423
|
+
};
|
|
283
424
|
case "text":
|
|
284
425
|
return { ...o, position: p(o.position) };
|
|
285
426
|
case "shape":
|
|
@@ -402,12 +543,33 @@ function objectSeatLabels(o) {
|
|
|
402
543
|
function isSeatObject(o) {
|
|
403
544
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
404
545
|
}
|
|
546
|
+
function samePoints(left, right) {
|
|
547
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
548
|
+
}
|
|
549
|
+
function sameGASurfaceAsSection(object, section) {
|
|
550
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
551
|
+
const objectHoles = object.holes ?? [];
|
|
552
|
+
const sectionHoles = section.holes ?? [];
|
|
553
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
554
|
+
}
|
|
405
555
|
function computeSections(doc) {
|
|
406
556
|
const objs = allObjects(doc);
|
|
407
557
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
408
558
|
const nodes = /* @__PURE__ */ new Map();
|
|
409
559
|
for (const s of sectionObjs) {
|
|
410
|
-
|
|
560
|
+
const logicalId = s.logicalSectionId ?? s.id;
|
|
561
|
+
const existing = nodes.get(logicalId);
|
|
562
|
+
if (existing) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
nodes.set(logicalId, {
|
|
566
|
+
id: logicalId,
|
|
567
|
+
label: s.displayLabel || s.label || "Section",
|
|
568
|
+
zone: s.zone,
|
|
569
|
+
seatCount: 0,
|
|
570
|
+
objectIds: [],
|
|
571
|
+
seatLabels: []
|
|
572
|
+
});
|
|
411
573
|
}
|
|
412
574
|
const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
|
|
413
575
|
const objectToSection = /* @__PURE__ */ new Map();
|
|
@@ -415,22 +577,24 @@ function computeSections(doc) {
|
|
|
415
577
|
if (!isSeatObject(obj)) continue;
|
|
416
578
|
const labels = objectSeatLabels(obj);
|
|
417
579
|
if (labels.length === 0) continue;
|
|
580
|
+
const referencedLogicalId = obj.referenceInventorySource?.logicalSectionId;
|
|
418
581
|
const c = objectCenter(obj);
|
|
419
|
-
const
|
|
420
|
-
const
|
|
582
|
+
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
583
|
+
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
584
|
+
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
421
585
|
node.seatCount += labels.length;
|
|
422
586
|
node.objectIds.push(obj.id);
|
|
423
587
|
node.seatLabels.push(...labels);
|
|
424
588
|
objectToSection.set(obj.id, node.id);
|
|
425
589
|
}
|
|
426
590
|
return {
|
|
427
|
-
sections:
|
|
591
|
+
sections: [...nodes.values()],
|
|
428
592
|
ungrouped: ungrouped.objectIds.length ? ungrouped : null,
|
|
429
593
|
objectToSection
|
|
430
594
|
};
|
|
431
595
|
}
|
|
432
596
|
function isSectionHidden(s, hidden) {
|
|
433
|
-
return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
|
|
597
|
+
return hidden.has(s.id) || !!s.logicalSectionId && hidden.has(s.logicalSectionId) || !!s.zone && hidden.has(s.zone);
|
|
434
598
|
}
|
|
435
599
|
function hiddenObjectIds(doc, hidden) {
|
|
436
600
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -483,6 +647,61 @@ import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
|
483
647
|
import { Line } from "konva/lib/shapes/Line";
|
|
484
648
|
import { Text } from "konva/lib/shapes/Text";
|
|
485
649
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
650
|
+
import { Shape } from "konva/lib/Shape";
|
|
651
|
+
|
|
652
|
+
// src/core/chartRenderRules.ts
|
|
653
|
+
var SEAT_LABEL_FONT_SIZE = 7;
|
|
654
|
+
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
655
|
+
var GA_LABEL_FONT_SIZE = 15;
|
|
656
|
+
var GA_CAPACITY_LABEL_FONT_SIZE = 11;
|
|
657
|
+
var GA_FILL_OPACITY = 0.85;
|
|
658
|
+
var MIN_VISIBLE_BOOKABLE_LABEL_PX = 12;
|
|
659
|
+
var SMALL_TEXT_CONTRAST = 4.5;
|
|
660
|
+
var DARK_BOOKABLE_LABEL_INK = "#000000";
|
|
661
|
+
var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
|
|
662
|
+
function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
|
|
663
|
+
return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
|
|
664
|
+
}
|
|
665
|
+
function bookableMarkerLabel(publicLabel) {
|
|
666
|
+
return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
|
|
667
|
+
}
|
|
668
|
+
function luminance(value) {
|
|
669
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
670
|
+
if (!match) return null;
|
|
671
|
+
const channel = (offset) => {
|
|
672
|
+
const encoded = Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255;
|
|
673
|
+
return encoded <= 0.04045 ? encoded / 12.92 : ((encoded + 0.055) / 1.055) ** 2.4;
|
|
674
|
+
};
|
|
675
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
676
|
+
}
|
|
677
|
+
function renderedTextContrast(ink, fill) {
|
|
678
|
+
const inkLuminance = luminance(ink);
|
|
679
|
+
const fillLuminance = luminance(fill);
|
|
680
|
+
if (inkLuminance == null || fillLuminance == null) return null;
|
|
681
|
+
return (Math.max(inkLuminance, fillLuminance) + 0.05) / (Math.min(inkLuminance, fillLuminance) + 0.05);
|
|
682
|
+
}
|
|
683
|
+
function rgb(value) {
|
|
684
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
685
|
+
if (!match) return null;
|
|
686
|
+
const packed = Number.parseInt(match[1], 16);
|
|
687
|
+
return [packed >> 16 & 255, packed >> 8 & 255, packed & 255];
|
|
688
|
+
}
|
|
689
|
+
function compositeHexOver(foreground, background, opacity) {
|
|
690
|
+
const front = rgb(foreground);
|
|
691
|
+
const back = rgb(background);
|
|
692
|
+
if (!front || !back) return background;
|
|
693
|
+
const alpha = Math.max(0, Math.min(1, opacity));
|
|
694
|
+
const channels = front.map((value, index) => Math.round(value * alpha + back[index] * (1 - alpha)));
|
|
695
|
+
return `#${channels.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
696
|
+
}
|
|
697
|
+
function stateAwareBookableLabelInk(fill, preferred) {
|
|
698
|
+
const preferredContrast = renderedTextContrast(preferred, fill);
|
|
699
|
+
if (preferredContrast != null && preferredContrast >= SMALL_TEXT_CONTRAST) return preferred;
|
|
700
|
+
const darkContrast = renderedTextContrast(DARK_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
701
|
+
const lightContrast = renderedTextContrast(LIGHT_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
702
|
+
if (darkContrast === 0 && lightContrast === 0) return preferred;
|
|
703
|
+
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
704
|
+
}
|
|
486
705
|
|
|
487
706
|
// src/lib/money.ts
|
|
488
707
|
var DEFAULT_CURRENCY = "USD";
|
|
@@ -542,6 +761,8 @@ var en = {
|
|
|
542
761
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
543
762
|
"picker.language": "Language",
|
|
544
763
|
"picker.zoomToFit": "Zoom to fit",
|
|
764
|
+
"picker.seatCountLabel": "seats",
|
|
765
|
+
"picker.capacity": "capacity",
|
|
545
766
|
"picker.viewMode": "View mode",
|
|
546
767
|
"picker.floor": "Floor",
|
|
547
768
|
"picker.zoomLevel": "Zoom level",
|
|
@@ -631,7 +852,8 @@ function formatDate(value, opts) {
|
|
|
631
852
|
var SEAT_RADIUS = 9;
|
|
632
853
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
633
854
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
634
|
-
var LABEL_SCALE =
|
|
855
|
+
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
856
|
+
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
635
857
|
var SEAT_TAP_SLOP_PX = 14;
|
|
636
858
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
637
859
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
@@ -645,32 +867,31 @@ var ISO_SQUASH = 0.58;
|
|
|
645
867
|
var LIFT_PER_STEP = 58;
|
|
646
868
|
var ISO_TWEEN_MS = 320;
|
|
647
869
|
var CAMERA_GLIDE_MS = 650;
|
|
648
|
-
var BLOCK_FILL_ALPHA =
|
|
649
|
-
var
|
|
870
|
+
var BLOCK_FILL_ALPHA = 1;
|
|
871
|
+
var SECTION_STROKE_PX = 2;
|
|
872
|
+
var LIGHT_OVERVIEW_SECTION_FILL = "#e5e7eb";
|
|
873
|
+
var LIGHT_OVERVIEW_SECTION_STROKE = "#c7cbd1";
|
|
874
|
+
var LIGHT_OVERVIEW_SECTION_INK = "#595f69";
|
|
875
|
+
var LIGHT_OVERVIEW_FOCAL_FILL = "#d1d5db";
|
|
876
|
+
var LIGHT_OVERVIEW_FOCAL_STROKE = "#b8bdc4";
|
|
877
|
+
var DARK_OVERVIEW_SECTION_FILL = "#273142";
|
|
878
|
+
var DARK_OVERVIEW_SECTION_STROKE = "#526078";
|
|
879
|
+
var DARK_OVERVIEW_SECTION_INK = "#f1f5f9";
|
|
880
|
+
var DARK_OVERVIEW_FOCAL_FILL = "#374151";
|
|
881
|
+
var DARK_OVERVIEW_FOCAL_STROKE = "#64748b";
|
|
650
882
|
var SECTION_LABEL_PX = 20;
|
|
651
|
-
var
|
|
652
|
-
var ZONE_LABEL_PX =
|
|
883
|
+
var MIN_SECTION_LABEL_PX = 12;
|
|
884
|
+
var ZONE_LABEL_PX = 18;
|
|
653
885
|
var ZONE_SUB_PX = 12;
|
|
886
|
+
var HIERARCHY_PILL_BACKGROUND = "#111827";
|
|
654
887
|
var HELD_FILL = "#6b7280";
|
|
655
888
|
var TAKEN_FILL = "#374151";
|
|
656
889
|
var NFS_STROKE = "#4b5563";
|
|
657
|
-
var CLOSED_SECTION_FILL = "#586070";
|
|
658
890
|
var CLOSED_SEAT_FILL = "#4b5563";
|
|
659
891
|
var CLOSED_SEAT_OPACITY = 0.4;
|
|
660
892
|
var FOCUS_DIM_OPACITY = 0.16;
|
|
661
|
-
var FOCUS_DESATURATE = 0.72;
|
|
662
|
-
var FOCUS_NEUTRAL = "#6b7280";
|
|
663
893
|
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
664
894
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
665
|
-
var ACCESS_RING = {
|
|
666
|
-
wheelchair: "#3b82f6",
|
|
667
|
-
companion: "#8b5cf6",
|
|
668
|
-
"semi-ambulatory": "#0ea5e9",
|
|
669
|
-
hearing: "#14b8a6",
|
|
670
|
-
"sign-language": "#f59e0b",
|
|
671
|
-
"plus-size": "#ec4899",
|
|
672
|
-
"lift-armrest": "#22c55e"
|
|
673
|
-
};
|
|
674
895
|
function seatMatchesAccess(seat, filter) {
|
|
675
896
|
if (filter.length === 0) return !!seat.accessible;
|
|
676
897
|
return !!seat.accessibility?.some((t2) => filter.includes(t2));
|
|
@@ -680,6 +901,7 @@ var DEF_SELECTION = "#ffffff";
|
|
|
680
901
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
681
902
|
var DEF_DECOR_FILL = "#232c40";
|
|
682
903
|
var DEF_TEXT = "#8b93a7";
|
|
904
|
+
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
683
905
|
function colorLuminance(color) {
|
|
684
906
|
const s = color.trim();
|
|
685
907
|
let r = NaN;
|
|
@@ -692,11 +914,11 @@ function colorLuminance(color) {
|
|
|
692
914
|
g = parseInt(h.slice(2, 4), 16);
|
|
693
915
|
b = parseInt(h.slice(4, 6), 16);
|
|
694
916
|
} else {
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
697
|
-
r = +
|
|
698
|
-
g = +
|
|
699
|
-
b = +
|
|
917
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
|
|
918
|
+
if (rgb2) {
|
|
919
|
+
r = +rgb2[1];
|
|
920
|
+
g = +rgb2[2];
|
|
921
|
+
b = +rgb2[3];
|
|
700
922
|
}
|
|
701
923
|
}
|
|
702
924
|
if (Number.isNaN(r)) return NaN;
|
|
@@ -706,6 +928,34 @@ function isLightColor(color) {
|
|
|
706
928
|
const lum = colorLuminance(color);
|
|
707
929
|
return !Number.isNaN(lum) && lum > 0.6;
|
|
708
930
|
}
|
|
931
|
+
function opaqueColorHex(color) {
|
|
932
|
+
const value = color.trim();
|
|
933
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
|
|
934
|
+
if (hex) {
|
|
935
|
+
const expanded = hex[1].length === 3 ? hex[1].split("").map((channel) => channel + channel).join("") : hex[1];
|
|
936
|
+
return `#${expanded.toLowerCase()}`;
|
|
937
|
+
}
|
|
938
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
|
|
939
|
+
if (!rgb2 || rgb2[4] != null && Number(rgb2[4]) < 0.999) return null;
|
|
940
|
+
const channels = [Number(rgb2[1]), Number(rgb2[2]), Number(rgb2[3])];
|
|
941
|
+
if (channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
|
|
942
|
+
return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
943
|
+
}
|
|
944
|
+
function overviewPalette(canvasBackground) {
|
|
945
|
+
return isLightColor(canvasBackground) ? {
|
|
946
|
+
sectionFill: LIGHT_OVERVIEW_SECTION_FILL,
|
|
947
|
+
sectionStroke: LIGHT_OVERVIEW_SECTION_STROKE,
|
|
948
|
+
sectionInk: LIGHT_OVERVIEW_SECTION_INK,
|
|
949
|
+
focalFill: LIGHT_OVERVIEW_FOCAL_FILL,
|
|
950
|
+
focalStroke: LIGHT_OVERVIEW_FOCAL_STROKE
|
|
951
|
+
} : {
|
|
952
|
+
sectionFill: DARK_OVERVIEW_SECTION_FILL,
|
|
953
|
+
sectionStroke: DARK_OVERVIEW_SECTION_STROKE,
|
|
954
|
+
sectionInk: DARK_OVERVIEW_SECTION_INK,
|
|
955
|
+
focalFill: DARK_OVERVIEW_FOCAL_FILL,
|
|
956
|
+
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
957
|
+
};
|
|
958
|
+
}
|
|
709
959
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
710
960
|
function seatIdOf(target) {
|
|
711
961
|
const n = target;
|
|
@@ -741,6 +991,108 @@ function polyBounds(pts) {
|
|
|
741
991
|
}
|
|
742
992
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
743
993
|
}
|
|
994
|
+
function rotatedRectPoints(center, width, height, rotation) {
|
|
995
|
+
const radians = rotation * Math.PI / 180;
|
|
996
|
+
const cos = Math.cos(radians);
|
|
997
|
+
const sin = Math.sin(radians);
|
|
998
|
+
return [
|
|
999
|
+
{ x: -width / 2, y: -height / 2 },
|
|
1000
|
+
{ x: width / 2, y: -height / 2 },
|
|
1001
|
+
{ x: width / 2, y: height / 2 },
|
|
1002
|
+
{ x: -width / 2, y: height / 2 }
|
|
1003
|
+
].map((point) => ({
|
|
1004
|
+
x: center.x + point.x * cos - point.y * sin,
|
|
1005
|
+
y: center.y + point.x * sin + point.y * cos
|
|
1006
|
+
}));
|
|
1007
|
+
}
|
|
1008
|
+
function pointsBounds(points) {
|
|
1009
|
+
const bounds = polyBounds(points);
|
|
1010
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
1011
|
+
}
|
|
1012
|
+
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1013
|
+
const radians = rotation * Math.PI / 180;
|
|
1014
|
+
const cos = Math.cos(radians);
|
|
1015
|
+
const sin = Math.sin(radians);
|
|
1016
|
+
for (let yStep = 0; yStep <= 4; yStep++) {
|
|
1017
|
+
for (let xStep = 0; xStep <= 6; xStep++) {
|
|
1018
|
+
const localX = width * (xStep / 6 - 0.5);
|
|
1019
|
+
const localY = height * (yStep / 4 - 0.5);
|
|
1020
|
+
const point = {
|
|
1021
|
+
x: center.x + localX * cos - localY * sin,
|
|
1022
|
+
y: center.y + localX * sin + localY * cos
|
|
1023
|
+
};
|
|
1024
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) return false;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1030
|
+
const bounds = polyBounds(outer);
|
|
1031
|
+
const centre = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
1032
|
+
const points = [preferred];
|
|
1033
|
+
for (let row = 1; row < 12; row += 1) {
|
|
1034
|
+
for (let column = 1; column < 12; column += 1) {
|
|
1035
|
+
const point = {
|
|
1036
|
+
x: bounds.x + bounds.width * column / 12,
|
|
1037
|
+
y: bounds.y + bounds.height * row / 12
|
|
1038
|
+
};
|
|
1039
|
+
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return points.sort((left, right) => Math.hypot(left.x - centre.x, left.y - centre.y) - Math.hypot(right.x - centre.x, right.y - centre.y)).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
|
|
1043
|
+
}
|
|
1044
|
+
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1045
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1046
|
+
const next = points[(index + 1) % points.length];
|
|
1047
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
1048
|
+
}, 0);
|
|
1049
|
+
const outerClockwise = signedArea(outer) > 0;
|
|
1050
|
+
return new Shape({
|
|
1051
|
+
...attrs,
|
|
1052
|
+
sceneFunc(context, shape) {
|
|
1053
|
+
context.beginPath();
|
|
1054
|
+
const polygonPath = (points) => {
|
|
1055
|
+
if (!points.length) return;
|
|
1056
|
+
context.moveTo(points[0].x, points[0].y);
|
|
1057
|
+
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1058
|
+
context.closePath();
|
|
1059
|
+
};
|
|
1060
|
+
const vectorPath = (path) => {
|
|
1061
|
+
context.moveTo(path.start.x, path.start.y);
|
|
1062
|
+
let current = path.start;
|
|
1063
|
+
for (const segment of path.segments) {
|
|
1064
|
+
if (segment.kind === "line") context.lineTo(segment.end.x, segment.end.y);
|
|
1065
|
+
else if (segment.kind === "arc") context.arc(
|
|
1066
|
+
segment.center.x,
|
|
1067
|
+
segment.center.y,
|
|
1068
|
+
segment.radius,
|
|
1069
|
+
Math.atan2(current.y - segment.center.y, current.x - segment.center.x),
|
|
1070
|
+
Math.atan2(segment.end.y - segment.center.y, segment.end.x - segment.center.x),
|
|
1071
|
+
!segment.clockwise
|
|
1072
|
+
);
|
|
1073
|
+
else context.bezierCurveTo(
|
|
1074
|
+
segment.control1.x,
|
|
1075
|
+
segment.control1.y,
|
|
1076
|
+
segment.control2.x,
|
|
1077
|
+
segment.control2.y,
|
|
1078
|
+
segment.end.x,
|
|
1079
|
+
segment.end.y
|
|
1080
|
+
);
|
|
1081
|
+
current = segment.end;
|
|
1082
|
+
}
|
|
1083
|
+
context.closePath();
|
|
1084
|
+
};
|
|
1085
|
+
if (outerPath) vectorPath(outerPath);
|
|
1086
|
+
else polygonPath(outer);
|
|
1087
|
+
for (const hole of holes ?? []) {
|
|
1088
|
+
const holeClockwise = signedArea(hole) > 0;
|
|
1089
|
+
polygonPath(holeClockwise === outerClockwise ? [...hole].reverse() : hole);
|
|
1090
|
+
}
|
|
1091
|
+
context.fillStrokeShape(shape);
|
|
1092
|
+
},
|
|
1093
|
+
perfectDrawEnabled: false
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
744
1096
|
function rgba(hex, a) {
|
|
745
1097
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
746
1098
|
if (!m) return hex;
|
|
@@ -760,11 +1112,11 @@ function mixColors(parts, fallback) {
|
|
|
760
1112
|
let b = 0;
|
|
761
1113
|
let tw = 0;
|
|
762
1114
|
for (const p of parts) {
|
|
763
|
-
const
|
|
764
|
-
if (!
|
|
765
|
-
r +=
|
|
766
|
-
g +=
|
|
767
|
-
b +=
|
|
1115
|
+
const rgb2 = hexToRgb(p.hex);
|
|
1116
|
+
if (!rgb2 || p.w <= 0) continue;
|
|
1117
|
+
r += rgb2[0] * p.w;
|
|
1118
|
+
g += rgb2[1] * p.w;
|
|
1119
|
+
b += rgb2[2] * p.w;
|
|
768
1120
|
tw += p.w;
|
|
769
1121
|
}
|
|
770
1122
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
@@ -788,11 +1140,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
788
1140
|
this.circleById = /* @__PURE__ */ new Map();
|
|
789
1141
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
790
1142
|
this.boothDims = /* @__PURE__ */ new Map();
|
|
791
|
-
/** Booth
|
|
1143
|
+
/** Booth labels live with the booth shape but obey the shared rendered-size LOD. */
|
|
792
1144
|
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
1145
|
+
/** Viewport seat labels are rebuilt after each settled camera change. */
|
|
1146
|
+
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1147
|
+
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1148
|
+
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1149
|
+
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1150
|
+
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1151
|
+
/** GA paint and text share price/highlight filter state. */
|
|
1152
|
+
this.gaById = /* @__PURE__ */ new Map();
|
|
793
1153
|
this.statusById = /* @__PURE__ */ new Map();
|
|
794
1154
|
this.catColor = /* @__PURE__ */ new Map();
|
|
795
1155
|
this.theme = {};
|
|
1156
|
+
/** Opaque paint actually visible behind transparent Konva canvases. */
|
|
1157
|
+
this.canvasBackground = DEF_CANVAS_BACKGROUND;
|
|
796
1158
|
/** Effective selection/hover ring color — resolved per chart in setChart(). */
|
|
797
1159
|
this.effSelection = DEF_SELECTION;
|
|
798
1160
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
@@ -1090,6 +1452,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1090
1452
|
this.circleById.clear();
|
|
1091
1453
|
this.boothDims.clear();
|
|
1092
1454
|
this.boothLabelById.clear();
|
|
1455
|
+
this.seatLabelById.clear();
|
|
1456
|
+
this.freeTextById.clear();
|
|
1457
|
+
this.primaryFocalLabels.clear();
|
|
1458
|
+
this.gaById.clear();
|
|
1459
|
+
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
1093
1460
|
this.selectionMarkers.clear();
|
|
1094
1461
|
this.ownedHold.clear();
|
|
1095
1462
|
this.selectionFocusId = null;
|
|
@@ -1101,6 +1468,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1101
1468
|
this.sections = [];
|
|
1102
1469
|
this.zones = [];
|
|
1103
1470
|
this.seatSection.clear();
|
|
1471
|
+
this.focusedSectionId = null;
|
|
1472
|
+
this.focusBackdrop = null;
|
|
1104
1473
|
this.catPrice.clear();
|
|
1105
1474
|
this.zoneColor.clear();
|
|
1106
1475
|
this.lodScale = 0;
|
|
@@ -1118,7 +1487,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1118
1487
|
this.hoverRing.visible(false);
|
|
1119
1488
|
this.theme = doc.theme ?? {};
|
|
1120
1489
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1121
|
-
this.container.style.background =
|
|
1490
|
+
this.container.style.background = "";
|
|
1491
|
+
this.canvasBackground = this.resolveCanvasBackground();
|
|
1492
|
+
this.container.style.background = this.canvasBackground;
|
|
1122
1493
|
this.effSelection = this.resolveSelectionColor();
|
|
1123
1494
|
this.hoverRing.stroke(this.effSelection);
|
|
1124
1495
|
this.hoverRing.radius(this.seatR + 2);
|
|
@@ -1347,12 +1718,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1347
1718
|
}
|
|
1348
1719
|
return this.selectMany(ids);
|
|
1349
1720
|
}
|
|
1721
|
+
/** Exact SDK capture helper. MCP never accepts this id; the SDK derives it
|
|
1722
|
+
* from the persisted floor and uses the normal selected paint/ring path. */
|
|
1723
|
+
setEvidenceSelection(seatId) {
|
|
1724
|
+
if (!this.seatById.has(seatId) || !this.isSelectable(seatId)) return false;
|
|
1725
|
+
if (this.selection.size) this.clearSelection();
|
|
1726
|
+
this.setSelected(seatId, true);
|
|
1727
|
+
this.overlayLayer.batchDraw();
|
|
1728
|
+
return this.selection.has(seatId);
|
|
1729
|
+
}
|
|
1350
1730
|
/** Selectable seats in a section OR zone id — pure read (no selection change). */
|
|
1351
1731
|
getSelectableInSection(sectionId) {
|
|
1352
1732
|
const out = [];
|
|
1353
1733
|
const seen = /* @__PURE__ */ new Set();
|
|
1354
1734
|
for (const sec of this.sections) {
|
|
1355
|
-
if (sec.id !== sectionId && sec.zone !== sectionId) continue;
|
|
1735
|
+
if (sec.id !== sectionId && sec.logicalId !== sectionId && sec.zone !== sectionId) continue;
|
|
1356
1736
|
for (const id of sec.memberIds) {
|
|
1357
1737
|
if (seen.has(id)) continue;
|
|
1358
1738
|
seen.add(id);
|
|
@@ -1495,6 +1875,65 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1495
1875
|
};
|
|
1496
1876
|
requestAnimationFrame(step);
|
|
1497
1877
|
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Pulse a section outline without moving the camera or mutating the authored
|
|
1880
|
+
* geometry. The temporary halo is drawn in the non-listening overlay layer,
|
|
1881
|
+
* so the apparent 4% lift never changes hit testing or selection bounds.
|
|
1882
|
+
*/
|
|
1883
|
+
flashSection(sectionId, color = "#22a06b") {
|
|
1884
|
+
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
1885
|
+
if (!matches.length) return;
|
|
1886
|
+
for (const section of matches) {
|
|
1887
|
+
const centre = section.outline.reduce(
|
|
1888
|
+
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
1889
|
+
{ x: 0, y: 0 }
|
|
1890
|
+
);
|
|
1891
|
+
centre.x /= section.outline.length;
|
|
1892
|
+
centre.y /= section.outline.length;
|
|
1893
|
+
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
1894
|
+
const halo = new Line({
|
|
1895
|
+
x: centre.x + lift.x,
|
|
1896
|
+
y: centre.y + lift.y,
|
|
1897
|
+
points: section.outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
1898
|
+
closed: true,
|
|
1899
|
+
stroke: color,
|
|
1900
|
+
strokeWidth: 3,
|
|
1901
|
+
strokeScaleEnabled: false,
|
|
1902
|
+
opacity: 0.92,
|
|
1903
|
+
listening: false,
|
|
1904
|
+
perfectDrawEnabled: false,
|
|
1905
|
+
shadowForStrokeEnabled: true,
|
|
1906
|
+
shadowColor: color,
|
|
1907
|
+
shadowBlur: 14,
|
|
1908
|
+
shadowOpacity: 0.7
|
|
1909
|
+
});
|
|
1910
|
+
this.overlayLayer.add(halo);
|
|
1911
|
+
this.overlayLayer.batchDraw();
|
|
1912
|
+
const remove = () => {
|
|
1913
|
+
if (!halo.getLayer()) return;
|
|
1914
|
+
halo.destroy();
|
|
1915
|
+
this.overlayLayer.batchDraw();
|
|
1916
|
+
};
|
|
1917
|
+
if (this.reducedMotion || typeof document !== "undefined" && document.hidden) {
|
|
1918
|
+
setTimeout(remove, 520);
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
const start = performance.now();
|
|
1922
|
+
const duration = 820;
|
|
1923
|
+
const step = (now) => {
|
|
1924
|
+
if (this.destroyed || !halo.getLayer()) return;
|
|
1925
|
+
const t2 = Math.min(1, (now - start) / duration);
|
|
1926
|
+
const eased = 1 - Math.pow(1 - t2, 3);
|
|
1927
|
+
const scale = 1 + eased * 0.04;
|
|
1928
|
+
halo.scale({ x: scale, y: scale });
|
|
1929
|
+
halo.opacity(0.92 * (1 - t2));
|
|
1930
|
+
this.overlayLayer.batchDraw();
|
|
1931
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
1932
|
+
else remove();
|
|
1933
|
+
};
|
|
1934
|
+
requestAnimationFrame(step);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1498
1937
|
/** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
|
|
1499
1938
|
nearestSeat(fromId, dir) {
|
|
1500
1939
|
const from = this.seatById.get(fromId);
|
|
@@ -1568,6 +2007,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1568
2007
|
seatCount() {
|
|
1569
2008
|
return this.seats.length;
|
|
1570
2009
|
}
|
|
2010
|
+
bookableCount() {
|
|
2011
|
+
let total = this.seats.length;
|
|
2012
|
+
for (const area of this.gaById.values()) total += area.capacity;
|
|
2013
|
+
return total;
|
|
2014
|
+
}
|
|
1571
2015
|
worldToScreen(point) {
|
|
1572
2016
|
const s = this.stage.scaleX();
|
|
1573
2017
|
const p = this.isoT === 0 ? point : this.isoForward(point);
|
|
@@ -1584,6 +2028,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1584
2028
|
const c = this.circleById.get(seat.id);
|
|
1585
2029
|
if (c) this.paintSeat(c, seat.id);
|
|
1586
2030
|
}
|
|
2031
|
+
this.updateLabels();
|
|
1587
2032
|
if (this.cached) {
|
|
1588
2033
|
this.seatLayer.clearCache();
|
|
1589
2034
|
this.cacheSeatLayer();
|
|
@@ -1610,12 +2055,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1610
2055
|
const c = this.circleById.get(seat.id);
|
|
1611
2056
|
if (c) this.paintSeat(c, seat.id);
|
|
1612
2057
|
}
|
|
2058
|
+
this.updateLabels();
|
|
1613
2059
|
if (this.cached) {
|
|
1614
2060
|
this.seatLayer.clearCache();
|
|
1615
2061
|
this.cacheSeatLayer();
|
|
1616
2062
|
} else {
|
|
1617
2063
|
this.seatLayer.batchDraw();
|
|
1618
2064
|
}
|
|
2065
|
+
this.applyGAFilterState();
|
|
2066
|
+
}
|
|
2067
|
+
gaCategoryDimmed(categoryKey) {
|
|
2068
|
+
return Boolean(
|
|
2069
|
+
this.categoryHighlight && categoryKey !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(categoryKey)
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
/** Keep GA paint and its two labels in the same legend/price-filter state. */
|
|
2073
|
+
applyGAFilterState() {
|
|
2074
|
+
this.paintGAStateForView();
|
|
2075
|
+
this.updateFreeTextVisibility();
|
|
2076
|
+
this.bgLayer.batchDraw();
|
|
2077
|
+
}
|
|
2078
|
+
paintGAStateForView() {
|
|
2079
|
+
for (const ga of this.gaById.values()) {
|
|
2080
|
+
const filteredOut = Boolean(this.categoryFilter && !this.categoryFilter.has(ga.categoryKey));
|
|
2081
|
+
const overviewHidden = ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
|
|
2082
|
+
ga.polygon.opacity(overviewHidden ? 0 : this.gaCategoryDimmed(ga.categoryKey) ? GA_FILL_OPACITY * 0.08 : GA_FILL_OPACITY);
|
|
2083
|
+
ga.polygon.listening(!overviewHidden && !filteredOut);
|
|
2084
|
+
}
|
|
1619
2085
|
}
|
|
1620
2086
|
/** Frame the currently available inventory that survived a buyer price
|
|
1621
2087
|
* filter. Clearing the filter glides back to the full venue. */
|
|
@@ -1875,13 +2341,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1875
2341
|
this.paintSeat(c, seat.id);
|
|
1876
2342
|
target.add(c);
|
|
1877
2343
|
if (seat.accessible) {
|
|
1878
|
-
const primary = seat.accessibility?.[0];
|
|
1879
2344
|
target.add(
|
|
1880
2345
|
new Circle({
|
|
1881
2346
|
x: seat.x,
|
|
1882
2347
|
y: seat.y,
|
|
1883
2348
|
radius: this.seatR + 1,
|
|
1884
|
-
stroke:
|
|
2349
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
1885
2350
|
strokeWidth: 2,
|
|
1886
2351
|
listening: false,
|
|
1887
2352
|
perfectDrawEnabled: false,
|
|
@@ -1910,13 +2375,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1910
2375
|
});
|
|
1911
2376
|
rect.setAttr("seatId", seat.id);
|
|
1912
2377
|
this.circleById.set(seat.id, rect);
|
|
1913
|
-
this.paintSeat(rect, seat.id);
|
|
1914
2378
|
target.add(rect);
|
|
1915
2379
|
const t2 = new Text({
|
|
1916
2380
|
x: seat.x,
|
|
1917
2381
|
y: seat.y,
|
|
1918
|
-
text: seat.label,
|
|
1919
|
-
fontSize:
|
|
2382
|
+
text: seat.displayLabel ?? seat.label,
|
|
2383
|
+
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
1920
2384
|
fontStyle: "600",
|
|
1921
2385
|
fontFamily: this.labelFont(),
|
|
1922
2386
|
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
@@ -1925,9 +2389,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1925
2389
|
});
|
|
1926
2390
|
t2.offsetX(t2.width() / 2);
|
|
1927
2391
|
t2.offsetY(t2.height() / 2);
|
|
2392
|
+
t2.visible(false);
|
|
2393
|
+
this.boothLabelById.set(seat.id, t2);
|
|
1928
2394
|
this.hasBoothText = true;
|
|
1929
2395
|
this.boothLabelById.set(seat.id, t2);
|
|
1930
2396
|
target.add(t2);
|
|
2397
|
+
this.paintSeat(rect, seat.id);
|
|
1931
2398
|
}
|
|
1932
2399
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
1933
2400
|
seatBaseColor(categoryKey) {
|
|
@@ -1935,6 +2402,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1935
2402
|
const idx = this.catOrder.indexOf(categoryKey);
|
|
1936
2403
|
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
1937
2404
|
}
|
|
2405
|
+
/** Resolve label ink against the paint that is actually visible. The theme
|
|
2406
|
+
* ink remains preferred, but mixed category palettes cannot always share one
|
|
2407
|
+
* accessible text colour, so every free and transient state gets the same
|
|
2408
|
+
* deterministic dark/light fallback used by Designer. */
|
|
2409
|
+
renderedBookableLabelInk(shape) {
|
|
2410
|
+
const preferred = this.theme.seatLabelColor ?? DEF_SEAT_LABEL;
|
|
2411
|
+
const fill = shape.fill();
|
|
2412
|
+
return stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", preferred);
|
|
2413
|
+
}
|
|
1938
2414
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
1939
2415
|
paintSeat(c, id) {
|
|
1940
2416
|
const seat = this.seatById.get(id);
|
|
@@ -1998,7 +2474,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1998
2474
|
}
|
|
1999
2475
|
if (this.dimmedSections.size) {
|
|
2000
2476
|
const sec = this.seatSection.get(id);
|
|
2001
|
-
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2477
|
+
if (sec && (this.dimmedSections.has(sec.id) || this.dimmedSections.has(sec.logicalId) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2002
2478
|
c.opacity(0.18);
|
|
2003
2479
|
}
|
|
2004
2480
|
}
|
|
@@ -2011,16 +2487,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2011
2487
|
}
|
|
2012
2488
|
if (this.focusedSectionId) {
|
|
2013
2489
|
const sec = this.seatSection.get(id);
|
|
2014
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2490
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2015
2491
|
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
2016
2492
|
}
|
|
2017
2493
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2494
|
+
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2495
|
+
if (bookableLabel) {
|
|
2496
|
+
bookableLabel.fill(this.renderedBookableLabelInk(c));
|
|
2497
|
+
bookableLabel.visible(
|
|
2498
|
+
isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2018
2501
|
}
|
|
2019
2502
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
2020
2503
|
seatInClosedSection(id) {
|
|
2021
2504
|
if (!this.closedSections.size) return false;
|
|
2022
2505
|
const sec = this.seatSection.get(id);
|
|
2023
|
-
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2506
|
+
return !!sec && (this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2024
2507
|
}
|
|
2025
2508
|
/**
|
|
2026
2509
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -2033,6 +2516,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2033
2516
|
const c = this.circleById.get(seat.id);
|
|
2034
2517
|
if (c) this.paintSeat(c, seat.id);
|
|
2035
2518
|
}
|
|
2519
|
+
this.updateLabels();
|
|
2036
2520
|
if (this.cached) {
|
|
2037
2521
|
this.seatLayer.clearCache();
|
|
2038
2522
|
this.cacheSeatLayer();
|
|
@@ -2077,7 +2561,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2077
2561
|
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2078
2562
|
*/
|
|
2079
2563
|
focusSection(id) {
|
|
2080
|
-
if (!this.sections.some((
|
|
2564
|
+
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2081
2565
|
this.focusedSectionId = id;
|
|
2082
2566
|
this.drawFocusBackdrop(id);
|
|
2083
2567
|
this.repaintSectionsAndSeats();
|
|
@@ -2105,20 +2589,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2105
2589
|
this.focusBackdrop.destroy();
|
|
2106
2590
|
this.focusBackdrop = null;
|
|
2107
2591
|
}
|
|
2108
|
-
const
|
|
2109
|
-
if (!
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
}
|
|
2119
|
-
this.bgLayer.add(
|
|
2120
|
-
|
|
2121
|
-
this.focusBackdrop =
|
|
2592
|
+
const sections = this.sections.filter((section) => section.id === id || section.logicalId === id);
|
|
2593
|
+
if (!sections.length) return;
|
|
2594
|
+
const backdrop = new Group({ listening: false });
|
|
2595
|
+
for (const section of sections) {
|
|
2596
|
+
backdrop.add(polygonWithHolesShape(section.outline, section.holes, {
|
|
2597
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2598
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2599
|
+
strokeWidth: 1,
|
|
2600
|
+
listening: false
|
|
2601
|
+
}, section.outlinePath));
|
|
2602
|
+
}
|
|
2603
|
+
this.bgLayer.add(backdrop);
|
|
2604
|
+
backdrop.moveToTop();
|
|
2605
|
+
this.focusBackdrop = backdrop;
|
|
2122
2606
|
}
|
|
2123
2607
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2124
2608
|
repaintSectionsAndSeats() {
|
|
@@ -2146,7 +2630,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2146
2630
|
height: Math.abs(br.y - tl.y)
|
|
2147
2631
|
};
|
|
2148
2632
|
}
|
|
2149
|
-
/** Axis-aligned world bounds of
|
|
2633
|
+
/** Axis-aligned world bounds of seats, section outlines, and GA polygons (minimap F3 frame). */
|
|
2150
2634
|
getWorldBounds() {
|
|
2151
2635
|
let minX = Infinity;
|
|
2152
2636
|
let minY = Infinity;
|
|
@@ -2160,6 +2644,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2160
2644
|
};
|
|
2161
2645
|
for (const s of this.seats) grow(s.x, s.y);
|
|
2162
2646
|
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2647
|
+
for (const area of this.gaById.values()) for (const p of area.points) grow(p.x, p.y);
|
|
2163
2648
|
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2164
2649
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2165
2650
|
}
|
|
@@ -2171,12 +2656,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2171
2656
|
const c = this.circleById.get(seat.id);
|
|
2172
2657
|
if (c) this.paintSeat(c, seat.id);
|
|
2173
2658
|
}
|
|
2659
|
+
this.updateLabels();
|
|
2174
2660
|
if (this.cached) {
|
|
2175
2661
|
this.seatLayer.clearCache();
|
|
2176
2662
|
this.cacheSeatLayer();
|
|
2177
2663
|
} else {
|
|
2178
2664
|
this.seatLayer.batchDraw();
|
|
2179
2665
|
}
|
|
2666
|
+
this.applyGAFilterState();
|
|
2180
2667
|
}
|
|
2181
2668
|
renderBackground(doc) {
|
|
2182
2669
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
@@ -2194,32 +2681,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2194
2681
|
this.renderText(obj);
|
|
2195
2682
|
}
|
|
2196
2683
|
}
|
|
2197
|
-
const f = doc.focalPoint;
|
|
2198
|
-
if (f) {
|
|
2199
|
-
const size = 14;
|
|
2200
|
-
const cross = new Group({ listening: false });
|
|
2201
|
-
cross.add(
|
|
2202
|
-
new Line({ points: [f.x - size, f.y, f.x + size, f.y], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2203
|
-
new Line({ points: [f.x, f.y - size, f.x, f.y + size], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2204
|
-
new Circle({ x: f.x, y: f.y, radius: 3, fill: "#4b5563" })
|
|
2205
|
-
);
|
|
2206
|
-
this.bgLayer.add(cross);
|
|
2207
|
-
}
|
|
2208
2684
|
}
|
|
2209
2685
|
/** Organizer floor-plan photo, dimmed, at the very bottom of the bg layer. */
|
|
2210
2686
|
renderBackgroundImage(bg) {
|
|
2687
|
+
if (!bg.url || bg.visible === false) return;
|
|
2211
2688
|
const img = new window.Image();
|
|
2212
2689
|
img.onload = () => {
|
|
2213
2690
|
const natW = img.naturalWidth || 4;
|
|
2214
2691
|
const natH = img.naturalHeight || 3;
|
|
2692
|
+
const rawCrop = bg.crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
2693
|
+
const cropX = Math.max(0, Math.min(0.99, rawCrop.x));
|
|
2694
|
+
const cropY = Math.max(0, Math.min(0.99, rawCrop.y));
|
|
2695
|
+
const crop = {
|
|
2696
|
+
x: cropX,
|
|
2697
|
+
y: cropY,
|
|
2698
|
+
width: Math.max(0.01, Math.min(1 - cropX, rawCrop.width)),
|
|
2699
|
+
height: Math.max(0.01, Math.min(1 - cropY, rawCrop.height))
|
|
2700
|
+
};
|
|
2215
2701
|
const w = bg.width;
|
|
2216
|
-
const h = w * (natH / natW);
|
|
2702
|
+
const h = w * (natH * crop.height / (natW * crop.width));
|
|
2217
2703
|
const node = new KImage({
|
|
2218
2704
|
image: img,
|
|
2219
|
-
x: bg.center.x
|
|
2220
|
-
y: bg.center.y
|
|
2705
|
+
x: bg.center.x,
|
|
2706
|
+
y: bg.center.y,
|
|
2707
|
+
offsetX: w / 2,
|
|
2708
|
+
offsetY: h / 2,
|
|
2221
2709
|
width: w,
|
|
2222
2710
|
height: h,
|
|
2711
|
+
rotation: bg.rotation ?? 0,
|
|
2712
|
+
crop: {
|
|
2713
|
+
x: crop.x * natW,
|
|
2714
|
+
y: crop.y * natH,
|
|
2715
|
+
width: crop.width * natW,
|
|
2716
|
+
height: crop.height * natH
|
|
2717
|
+
},
|
|
2223
2718
|
opacity: bg.opacity,
|
|
2224
2719
|
listening: false
|
|
2225
2720
|
});
|
|
@@ -2291,28 +2786,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2291
2786
|
})
|
|
2292
2787
|
);
|
|
2293
2788
|
}
|
|
2294
|
-
this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
2789
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
2790
|
+
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
2295
2791
|
}
|
|
2296
2792
|
renderText(obj) {
|
|
2297
|
-
this.
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2793
|
+
const background = this.canvasBackground;
|
|
2794
|
+
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
2795
|
+
const node = new Text({
|
|
2796
|
+
x: obj.position.x,
|
|
2797
|
+
y: obj.position.y,
|
|
2798
|
+
text: obj.text,
|
|
2799
|
+
fontSize: obj.fontSize,
|
|
2800
|
+
rotation: obj.rotation,
|
|
2801
|
+
// Authored ink remains preferred, but an embed/theme surface can change
|
|
2802
|
+
// the actual canvas. Fail over to readable black/white instead of
|
|
2803
|
+
// painting an otherwise valid caption invisibly on that active surface.
|
|
2804
|
+
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
2805
|
+
fontFamily: this.labelFont(),
|
|
2806
|
+
listening: false,
|
|
2807
|
+
perfectDrawEnabled: false
|
|
2808
|
+
});
|
|
2809
|
+
this.freeTextById.set(obj.id, {
|
|
2810
|
+
node,
|
|
2811
|
+
background,
|
|
2812
|
+
kind: "free-text"
|
|
2813
|
+
});
|
|
2814
|
+
this.bgLayer.add(node);
|
|
2310
2815
|
}
|
|
2311
2816
|
renderShape(obj) {
|
|
2312
|
-
const
|
|
2817
|
+
const authoredFill = obj.fill ?? this.theme.decorFill ?? DEF_DECOR_FILL;
|
|
2313
2818
|
const isStage = obj.role === "stage";
|
|
2819
|
+
const referenceFocal = obj.role === "reference-focal";
|
|
2314
2820
|
const isDecor = !!obj.role && !isStage;
|
|
2315
|
-
const
|
|
2821
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2822
|
+
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
2823
|
+
const stroke = isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : void 0;
|
|
2824
|
+
const strokeWidth = isStage ? 1 : referenceFocal ? 2 : 0;
|
|
2316
2825
|
let cx = 0;
|
|
2317
2826
|
let cy = 0;
|
|
2318
2827
|
if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
@@ -2334,7 +2843,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2334
2843
|
height: obj.height,
|
|
2335
2844
|
...grad,
|
|
2336
2845
|
stroke,
|
|
2337
|
-
strokeWidth
|
|
2846
|
+
strokeWidth,
|
|
2338
2847
|
cornerRadius: 4,
|
|
2339
2848
|
listening: false
|
|
2340
2849
|
})
|
|
@@ -2348,7 +2857,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2348
2857
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2349
2858
|
} : { fill };
|
|
2350
2859
|
this.bgLayer.add(
|
|
2351
|
-
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth
|
|
2860
|
+
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
2352
2861
|
);
|
|
2353
2862
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
2354
2863
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -2363,17 +2872,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2363
2872
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2364
2873
|
} : { fill };
|
|
2365
2874
|
this.bgLayer.add(
|
|
2366
|
-
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth
|
|
2875
|
+
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
2367
2876
|
);
|
|
2368
2877
|
}
|
|
2369
2878
|
if (obj.label) {
|
|
2370
|
-
if (isStage)
|
|
2371
|
-
|
|
2372
|
-
|
|
2879
|
+
if (isStage) {
|
|
2880
|
+
const node = this.addStageLabel(cx, cy, obj.label, fill);
|
|
2881
|
+
this.primaryFocalLabels.set(node, 22);
|
|
2882
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "stage" });
|
|
2883
|
+
} else if (isDecor) {
|
|
2884
|
+
const node = this.addCentredLabel(
|
|
2885
|
+
this.bgLayer,
|
|
2886
|
+
obj.label,
|
|
2887
|
+
cx,
|
|
2888
|
+
cy,
|
|
2889
|
+
referenceFocal ? stateAwareBookableLabelInk(fill, "#e6e9f0") : "#9aa3b5",
|
|
2890
|
+
referenceFocal ? 18 : 12,
|
|
2891
|
+
false
|
|
2892
|
+
);
|
|
2893
|
+
if (referenceFocal) this.primaryFocalLabels.set(node, 18);
|
|
2894
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
2895
|
+
} else {
|
|
2896
|
+
const node = this.addCentredLabel(this.bgLayer, obj.label, cx, cy, "#cbd5e1", 16, true);
|
|
2897
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
2898
|
+
}
|
|
2373
2899
|
}
|
|
2374
2900
|
}
|
|
2375
2901
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
2376
|
-
addStageLabel(x, y, text) {
|
|
2902
|
+
addStageLabel(x, y, text, background) {
|
|
2903
|
+
const ink = stateAwareBookableLabelInk(background, "#e6e9f0");
|
|
2377
2904
|
const t2 = new Text({
|
|
2378
2905
|
x,
|
|
2379
2906
|
y,
|
|
@@ -2382,22 +2909,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2382
2909
|
fontStyle: "700",
|
|
2383
2910
|
letterSpacing: 4,
|
|
2384
2911
|
fontFamily: this.labelFont(),
|
|
2385
|
-
fill:
|
|
2912
|
+
fill: ink,
|
|
2386
2913
|
listening: false,
|
|
2387
2914
|
perfectDrawEnabled: false
|
|
2388
2915
|
});
|
|
2389
2916
|
t2.offsetX(t2.width() / 2);
|
|
2390
2917
|
t2.offsetY(t2.height() / 2);
|
|
2391
2918
|
this.bgLayer.add(t2);
|
|
2919
|
+
return t2;
|
|
2392
2920
|
}
|
|
2393
2921
|
renderGA(obj) {
|
|
2394
2922
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
2395
|
-
const
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2923
|
+
const canvas = this.canvasBackground;
|
|
2924
|
+
const effectiveBackground = compositeHexOver(color, canvas, GA_FILL_OPACITY);
|
|
2925
|
+
const preferredInk = this.theme.textColor ?? "#e6e9f0";
|
|
2926
|
+
const ink = stateAwareBookableLabelInk(effectiveBackground, preferredInk);
|
|
2927
|
+
const poly = polygonWithHolesShape(obj.points, obj.holes, {
|
|
2399
2928
|
fill: color,
|
|
2400
|
-
opacity:
|
|
2929
|
+
opacity: GA_FILL_OPACITY,
|
|
2401
2930
|
stroke: color,
|
|
2402
2931
|
strokeWidth: 1.5
|
|
2403
2932
|
});
|
|
@@ -2410,31 +2939,51 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2410
2939
|
this.container.style.cursor = "default";
|
|
2411
2940
|
});
|
|
2412
2941
|
this.bgLayer.add(poly);
|
|
2413
|
-
const
|
|
2414
|
-
const
|
|
2415
|
-
this.addCentredLabel(this.bgLayer, obj.label,
|
|
2416
|
-
this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`,
|
|
2942
|
+
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
2943
|
+
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
2944
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
2945
|
+
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
2946
|
+
this.freeTextById.set(`${obj.id}:label`, {
|
|
2947
|
+
objectId: obj.id,
|
|
2948
|
+
node: label,
|
|
2949
|
+
background: effectiveBackground,
|
|
2950
|
+
kind: "ga-label",
|
|
2951
|
+
categoryKey: obj.categoryKey
|
|
2952
|
+
});
|
|
2953
|
+
this.freeTextById.set(`${obj.id}:capacity`, {
|
|
2954
|
+
objectId: obj.id,
|
|
2955
|
+
node: capacity,
|
|
2956
|
+
background: effectiveBackground,
|
|
2957
|
+
kind: "ga-capacity",
|
|
2958
|
+
categoryKey: obj.categoryKey
|
|
2959
|
+
});
|
|
2960
|
+
this.gaById.set(obj.id, {
|
|
2961
|
+
label: obj.label,
|
|
2962
|
+
capacity: obj.capacity,
|
|
2963
|
+
categoryKey: obj.categoryKey,
|
|
2964
|
+
points: obj.points,
|
|
2965
|
+
polygon: poly,
|
|
2966
|
+
effectiveBackground,
|
|
2967
|
+
...containingSection ? { sectionId: containingSection.logicalId } : {}
|
|
2968
|
+
});
|
|
2417
2969
|
}
|
|
2418
2970
|
/**
|
|
2419
2971
|
* A section renders in three coordinated layers driven by the LOD melt:
|
|
2420
2972
|
* • a faint outline (the existing near-zoom look, untouched),
|
|
2421
|
-
* • a solid
|
|
2422
|
-
* •
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2973
|
+
* • a neutral solid shell that fades in at the overview rung, and
|
|
2974
|
+
* • one readable, contained section name.
|
|
2975
|
+
* Category, row, seat, and availability detail belongs to section focus/zoom.
|
|
2976
|
+
* Membership and category mix are still precomputed for the detailed state.
|
|
2425
2977
|
*/
|
|
2426
2978
|
renderSection(obj) {
|
|
2427
|
-
const
|
|
2428
|
-
const
|
|
2429
|
-
x: obj.outline.reduce((a, p) => a + p.x, 0) / obj.outline.length,
|
|
2430
|
-
y: obj.outline.reduce((a, p) => a + p.y, 0) / obj.outline.length
|
|
2431
|
-
};
|
|
2979
|
+
const centroid = polygonLabelPoint(obj.outline, obj.holes);
|
|
2980
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2432
2981
|
const memberIds = [];
|
|
2433
2982
|
const catCounts = /* @__PURE__ */ new Map();
|
|
2434
2983
|
let free = 0;
|
|
2435
2984
|
for (const seat of this.seats) {
|
|
2436
2985
|
if (this.seatSection.has(seat.id)) continue;
|
|
2437
|
-
if (!
|
|
2986
|
+
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
2438
2987
|
memberIds.push(seat.id);
|
|
2439
2988
|
catCounts.set(seat.categoryKey, (catCounts.get(seat.categoryKey) ?? 0) + 1);
|
|
2440
2989
|
if ((this.statusById.get(seat.id) ?? "free") === "free") free++;
|
|
@@ -2470,65 +3019,34 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2470
3019
|
}
|
|
2471
3020
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
2472
3021
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
2473
|
-
const outlinePoly =
|
|
2474
|
-
points: pts,
|
|
2475
|
-
closed: true,
|
|
3022
|
+
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
2476
3023
|
stroke: rgba(outlineTint, 0.5),
|
|
2477
3024
|
strokeWidth: 1.75,
|
|
2478
3025
|
fill: rgba(outlineTint, 0.08),
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
perfectDrawEnabled: false
|
|
2482
|
-
});
|
|
3026
|
+
listening: false
|
|
3027
|
+
}, obj.outlinePath);
|
|
2483
3028
|
bgTarget.add(outlinePoly);
|
|
2484
|
-
const blockPoly =
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
stroke: rgba("#ffffff", 0.12),
|
|
2489
|
-
strokeWidth: 1,
|
|
3029
|
+
const blockPoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3030
|
+
fill: palette.sectionFill,
|
|
3031
|
+
stroke: palette.sectionStroke,
|
|
3032
|
+
strokeWidth: SECTION_STROKE_PX,
|
|
2490
3033
|
opacity: 0,
|
|
2491
|
-
listening: false
|
|
2492
|
-
|
|
2493
|
-
});
|
|
3034
|
+
listening: false
|
|
3035
|
+
}, obj.outlinePath);
|
|
2494
3036
|
bgTarget.add(blockPoly);
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
if (!s) continue;
|
|
2499
|
-
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
2500
|
-
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
2501
|
-
}
|
|
2502
|
-
const rowLines = [];
|
|
2503
|
-
for (const arr of rowSeats.values()) {
|
|
2504
|
-
if (arr.length < 2) continue;
|
|
2505
|
-
arr.sort((a, b) => a.i - b.i);
|
|
2506
|
-
const line = new Line({
|
|
2507
|
-
points: arr.flatMap((p) => [p.x, p.y]),
|
|
2508
|
-
stroke: rgba("#ffffff", 0.34),
|
|
2509
|
-
strokeWidth: SEAT_RADIUS * 0.55,
|
|
2510
|
-
lineCap: "round",
|
|
2511
|
-
lineJoin: "round",
|
|
2512
|
-
opacity: 0,
|
|
2513
|
-
listening: false,
|
|
2514
|
-
perfectDrawEnabled: false
|
|
2515
|
-
});
|
|
2516
|
-
rowLines.push(line);
|
|
2517
|
-
bgTarget.add(line);
|
|
2518
|
-
}
|
|
3037
|
+
const labelStyle = obj.labelPresentation?.labelStyle;
|
|
3038
|
+
const preferredInk = labelStyle?.color ?? palette.sectionInk;
|
|
3039
|
+
const labelScale = (labelStyle?.size ?? 18) / 18;
|
|
2519
3040
|
const nameLabel = new Text({
|
|
2520
|
-
x: centroid.x,
|
|
2521
|
-
y: centroid.y,
|
|
2522
|
-
text: obj.label,
|
|
2523
|
-
|
|
3041
|
+
x: obj.labelPresentation?.position?.x ?? centroid.x,
|
|
3042
|
+
y: obj.labelPresentation?.position?.y ?? centroid.y,
|
|
3043
|
+
text: obj.displayLabel ?? obj.label,
|
|
3044
|
+
rotation: obj.labelPresentation?.rotation ?? 0,
|
|
3045
|
+
visible: obj.labelPresentation?.visible !== false,
|
|
3046
|
+
fontSize: 22 * labelScale,
|
|
2524
3047
|
fontStyle: "700",
|
|
2525
3048
|
fontFamily: this.labelFont(),
|
|
2526
|
-
fill:
|
|
2527
|
-
// Dark halo so the label reads over the seat dots at any zoom.
|
|
2528
|
-
shadowColor: "#05070c",
|
|
2529
|
-
shadowBlur: 6,
|
|
2530
|
-
shadowOpacity: 0.9,
|
|
2531
|
-
shadowForStrokeEnabled: false,
|
|
3049
|
+
fill: stateAwareBookableLabelInk(palette.sectionFill, preferredInk),
|
|
2532
3050
|
listening: false,
|
|
2533
3051
|
perfectDrawEnabled: false
|
|
2534
3052
|
});
|
|
@@ -2542,11 +3060,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2542
3060
|
fontSize: 12,
|
|
2543
3061
|
fontStyle: "700",
|
|
2544
3062
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2545
|
-
fill:
|
|
2546
|
-
shadowColor: "#05070c",
|
|
2547
|
-
shadowBlur: 5,
|
|
2548
|
-
shadowOpacity: 0.9,
|
|
2549
|
-
shadowForStrokeEnabled: false,
|
|
3063
|
+
fill: palette.sectionInk,
|
|
2550
3064
|
opacity: 0,
|
|
2551
3065
|
listening: false,
|
|
2552
3066
|
perfectDrawEnabled: false
|
|
@@ -2555,9 +3069,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2555
3069
|
bgTarget.add(subLabel);
|
|
2556
3070
|
const sec = {
|
|
2557
3071
|
id: obj.id,
|
|
2558
|
-
|
|
3072
|
+
logicalId: obj.logicalSectionId ?? obj.id,
|
|
3073
|
+
label: obj.displayLabel ?? obj.label,
|
|
2559
3074
|
outline: obj.outline,
|
|
3075
|
+
...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
|
|
3076
|
+
holes: obj.holes ?? [],
|
|
2560
3077
|
centroid,
|
|
3078
|
+
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
|
|
2561
3079
|
zone: obj.zone,
|
|
2562
3080
|
memberIds,
|
|
2563
3081
|
total: memberIds.length,
|
|
@@ -2566,9 +3084,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2566
3084
|
outlineTint,
|
|
2567
3085
|
outlinePoly,
|
|
2568
3086
|
blockPoly,
|
|
2569
|
-
rowLines,
|
|
2570
3087
|
nameLabel,
|
|
2571
3088
|
subLabel,
|
|
3089
|
+
preferredInk,
|
|
3090
|
+
labelScale,
|
|
3091
|
+
nameLabelFits: true,
|
|
3092
|
+
subLabelFits: true,
|
|
2572
3093
|
elevation,
|
|
2573
3094
|
liftGroupBg,
|
|
2574
3095
|
liftGroupSeat,
|
|
@@ -2580,7 +3101,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2580
3101
|
this.sections.push(sec);
|
|
2581
3102
|
}
|
|
2582
3103
|
refreshSectionHeat(sec) {
|
|
2583
|
-
const raw = this.sectionHeat.get(sec.id) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
3104
|
+
const raw = this.sectionHeat.get(sec.id) ?? this.sectionHeat.get(sec.logicalId) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
2584
3105
|
if (raw == null || raw <= 0) {
|
|
2585
3106
|
sec.outlinePoly.stroke(rgba(sec.outlineTint, 0.5));
|
|
2586
3107
|
sec.outlinePoly.fill(rgba(sec.outlineTint, 0.08));
|
|
@@ -2596,7 +3117,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2596
3117
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
2597
3118
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
2598
3119
|
}
|
|
2599
|
-
/** Recompute a section's
|
|
3120
|
+
/** Recompute a section's neutral overview state and retained detail count. */
|
|
2600
3121
|
refreshSectionFill(sec) {
|
|
2601
3122
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2602
3123
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
@@ -2604,25 +3125,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2604
3125
|
}
|
|
2605
3126
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2606
3127
|
isSectionClosed(sec) {
|
|
2607
|
-
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
3128
|
+
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2608
3129
|
}
|
|
2609
|
-
/**
|
|
2610
|
-
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2611
|
-
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2612
|
-
* when another section holds focus (AXS dim treatment).
|
|
2613
|
-
*/
|
|
3130
|
+
/** Clean overview shells never leak category, price, or live availability paint. */
|
|
2614
3131
|
sectionBlockFill(sec) {
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
fill = CLOSED_SECTION_FILL;
|
|
2618
|
-
} else {
|
|
2619
|
-
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2620
|
-
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2621
|
-
}
|
|
2622
|
-
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2623
|
-
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2624
|
-
}
|
|
2625
|
-
return fill;
|
|
3132
|
+
const fill = overviewPalette(this.canvasBackground).sectionFill;
|
|
3133
|
+
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
2626
3134
|
}
|
|
2627
3135
|
/**
|
|
2628
3136
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -2651,19 +3159,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2651
3159
|
if (typeof p === "number" && p < minPrice) minPrice = p;
|
|
2652
3160
|
}
|
|
2653
3161
|
}
|
|
3162
|
+
const back = new Rect({
|
|
3163
|
+
x: cx,
|
|
3164
|
+
y: cy,
|
|
3165
|
+
width: 1,
|
|
3166
|
+
height: 1,
|
|
3167
|
+
offsetX: 0.5,
|
|
3168
|
+
offsetY: 0.5,
|
|
3169
|
+
cornerRadius: 1,
|
|
3170
|
+
fill: HIERARCHY_PILL_BACKGROUND,
|
|
3171
|
+
stroke: z.color ?? anchor.outlineTint,
|
|
3172
|
+
strokeWidth: 1,
|
|
3173
|
+
opacity: 0,
|
|
3174
|
+
listening: false,
|
|
3175
|
+
perfectDrawEnabled: false
|
|
3176
|
+
});
|
|
3177
|
+
this.bgLayer.add(back);
|
|
2654
3178
|
const label = new Text({
|
|
2655
3179
|
x: cx,
|
|
2656
3180
|
y: cy,
|
|
2657
3181
|
text: z.label.toUpperCase(),
|
|
2658
|
-
fontSize:
|
|
3182
|
+
fontSize: ZONE_LABEL_PX,
|
|
2659
3183
|
fontStyle: "800",
|
|
2660
|
-
letterSpacing:
|
|
3184
|
+
letterSpacing: 0.5,
|
|
2661
3185
|
fontFamily: this.labelFont(),
|
|
2662
|
-
fill:
|
|
2663
|
-
shadowColor: "#05070c",
|
|
2664
|
-
shadowBlur: 10,
|
|
2665
|
-
shadowOpacity: 0.95,
|
|
2666
|
-
shadowForStrokeEnabled: false,
|
|
3186
|
+
fill: "#f4f6fb",
|
|
2667
3187
|
opacity: 0,
|
|
2668
3188
|
listening: false,
|
|
2669
3189
|
perfectDrawEnabled: false
|
|
@@ -2680,7 +3200,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2680
3200
|
fontSize: 14,
|
|
2681
3201
|
fontStyle: "600",
|
|
2682
3202
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2683
|
-
fill:
|
|
3203
|
+
fill: "#cbd5e1",
|
|
2684
3204
|
opacity: 0,
|
|
2685
3205
|
listening: false,
|
|
2686
3206
|
perfectDrawEnabled: false
|
|
@@ -2688,7 +3208,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2688
3208
|
sub.offsetX(sub.width() / 2);
|
|
2689
3209
|
this.bgLayer.add(sub);
|
|
2690
3210
|
}
|
|
2691
|
-
this.zones.push({
|
|
3211
|
+
this.zones.push({
|
|
3212
|
+
id: z.id,
|
|
3213
|
+
anchor: { x: cx, y: cy },
|
|
3214
|
+
back,
|
|
3215
|
+
background: HIERARCHY_PILL_BACKGROUND,
|
|
3216
|
+
label,
|
|
3217
|
+
sub
|
|
3218
|
+
});
|
|
2692
3219
|
}
|
|
2693
3220
|
}
|
|
2694
3221
|
/**
|
|
@@ -2710,74 +3237,106 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2710
3237
|
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
2711
3238
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
2712
3239
|
}
|
|
3240
|
+
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3241
|
+
if (sectionOverview) blockT = 1;
|
|
2713
3242
|
if (!this.zones.length) zoneT = 0;
|
|
2714
|
-
this.seatLayer.opacity(1 - blockT);
|
|
3243
|
+
this.seatLayer.opacity(sectionOverview ? 0 : 1 - blockT);
|
|
2715
3244
|
const sx = this.stage.scaleX();
|
|
2716
3245
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
2717
3246
|
if (rescale) this.lodScale = scale;
|
|
2718
3247
|
const focus = this.focusedSectionId;
|
|
3248
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3249
|
+
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
2719
3250
|
for (const sec of this.sections) {
|
|
2720
|
-
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3251
|
+
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3252
|
+
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
2721
3253
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
2722
|
-
|
|
2723
|
-
sec.
|
|
2724
|
-
sec.
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3254
|
+
sec.blockPoly.stroke(palette.sectionStroke);
|
|
3255
|
+
sec.blockPoly.strokeWidth(SECTION_STROKE_PX / Math.max(sx, 1e-4));
|
|
3256
|
+
const sectionFill = sec.blockPoly.fill();
|
|
3257
|
+
const sectionInk = stateAwareBookableLabelInk(
|
|
3258
|
+
typeof sectionFill === "string" ? sectionFill : sec.baseFill,
|
|
3259
|
+
sec.preferredInk
|
|
3260
|
+
);
|
|
3261
|
+
sec.nameLabel.fill(sectionInk);
|
|
3262
|
+
sec.subLabel.fill(sectionInk);
|
|
3263
|
+
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
3264
|
+
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3265
|
+
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3266
|
+
sec.subLabel.opacity(0);
|
|
2730
3267
|
}
|
|
2731
3268
|
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
2732
3269
|
for (const zone of this.zones) {
|
|
3270
|
+
zone.back.opacity(zoneOpacity);
|
|
2733
3271
|
zone.label.opacity(zoneOpacity);
|
|
2734
3272
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
2735
|
-
if (rescale)
|
|
2736
|
-
const cy = zone.label.y();
|
|
2737
|
-
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
2738
|
-
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
2739
|
-
}
|
|
3273
|
+
if (rescale) this.sizeZonePill(zone, sx);
|
|
2740
3274
|
}
|
|
2741
3275
|
this.decollideRungLabels(sx);
|
|
3276
|
+
this.dedupeLogicalSectionLabels();
|
|
3277
|
+
for (const zone of this.zones) {
|
|
3278
|
+
const opacity = zone.label.opacity();
|
|
3279
|
+
zone.back.opacity(opacity);
|
|
3280
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
3281
|
+
}
|
|
2742
3282
|
this.bgLayer.batchDraw();
|
|
2743
3283
|
}
|
|
3284
|
+
/** One semantic section gets one overview label, even across split contours. */
|
|
3285
|
+
dedupeLogicalSectionLabels() {
|
|
3286
|
+
const byLogical = /* @__PURE__ */ new Map();
|
|
3287
|
+
for (const section of this.sections) {
|
|
3288
|
+
(byLogical.get(section.logicalId) ?? byLogical.set(section.logicalId, []).get(section.logicalId)).push(section);
|
|
3289
|
+
}
|
|
3290
|
+
for (const components of byLogical.values()) {
|
|
3291
|
+
if (components.length < 2) continue;
|
|
3292
|
+
const visible = components.filter((component) => component.nameLabel.opacity() > 0.05).sort((left, right) => {
|
|
3293
|
+
const leftBounds = polyBounds(left.outline);
|
|
3294
|
+
const rightBounds = polyBounds(right.outline);
|
|
3295
|
+
return rightBounds.width * rightBounds.height - leftBounds.width * leftBounds.height;
|
|
3296
|
+
});
|
|
3297
|
+
for (const component of visible.slice(1)) component.nameLabel.opacity(0);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2744
3300
|
/**
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
2748
|
-
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
2749
|
-
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
2750
|
-
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
3301
|
+
* Keep transitional zone pills from covering section names. Section names
|
|
3302
|
+
* are already proven inside disjoint shells, so they must not cull each other.
|
|
2751
3303
|
*/
|
|
2752
3304
|
decollideRungLabels(sx) {
|
|
2753
3305
|
const GAP = 4;
|
|
2754
3306
|
const cands = [];
|
|
2755
3307
|
const boxOf = (t2) => {
|
|
2756
3308
|
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
2757
|
-
const
|
|
2758
|
-
|
|
3309
|
+
const rotated = pointsBounds(rotatedRectPoints(
|
|
3310
|
+
{ x: 0, y: 0 },
|
|
3311
|
+
t2.width() * sx,
|
|
3312
|
+
t2.height() * sx,
|
|
3313
|
+
t2.rotation()
|
|
3314
|
+
));
|
|
3315
|
+
const w = rotated.width;
|
|
3316
|
+
const h = rotated.height;
|
|
2759
3317
|
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
2760
3318
|
};
|
|
2761
3319
|
for (const zone of this.zones) {
|
|
2762
|
-
if (zone.label.opacity() > 0.05)
|
|
2763
|
-
|
|
3320
|
+
if (zone.label.opacity() > 0.05) {
|
|
3321
|
+
const p = this.worldToScreen(zone.anchor);
|
|
3322
|
+
const w = zone.back.width() * sx;
|
|
3323
|
+
const h = zone.back.height() * sx;
|
|
3324
|
+
cands.push({ node: zone.label, tier: 0, section: false, box: { x: p.x - w / 2, y: p.y - h / 2, w, h } });
|
|
3325
|
+
}
|
|
2764
3326
|
}
|
|
2765
3327
|
for (const sec of this.sections) {
|
|
2766
|
-
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
2767
|
-
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
3328
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, section: true, box: boxOf(sec.nameLabel) });
|
|
2768
3329
|
}
|
|
2769
3330
|
if (cands.length < 2) return;
|
|
2770
3331
|
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
2771
3332
|
const kept = [];
|
|
2772
|
-
const culled = /* @__PURE__ */ new Set();
|
|
2773
3333
|
const collides = (b) => kept.some(
|
|
2774
3334
|
(k) => b.x < k.x + k.w + GAP && k.x < b.x + b.w + GAP && b.y < k.y + k.h + GAP && k.y < b.y + b.h + GAP
|
|
2775
3335
|
);
|
|
2776
3336
|
for (const c of cands) {
|
|
2777
|
-
if (
|
|
3337
|
+
if (collides(c.box)) {
|
|
2778
3338
|
c.node.opacity(0);
|
|
2779
|
-
|
|
2780
|
-
} else {
|
|
3339
|
+
} else if (!c.section) {
|
|
2781
3340
|
kept.push(c.box);
|
|
2782
3341
|
}
|
|
2783
3342
|
}
|
|
@@ -2789,6 +3348,54 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2789
3348
|
t2.offsetY(t2.height() / 2);
|
|
2790
3349
|
t2.y(y);
|
|
2791
3350
|
}
|
|
3351
|
+
/** Fit one centred section name, rotating narrow shells like the target chart. */
|
|
3352
|
+
fitSectionRungLabels(sec, sx) {
|
|
3353
|
+
const paddingPx = 8;
|
|
3354
|
+
sec.subLabelFits = false;
|
|
3355
|
+
const maxPx = SECTION_LABEL_PX * sec.labelScale;
|
|
3356
|
+
const minPx = MIN_SECTION_LABEL_PX * sec.labelScale;
|
|
3357
|
+
for (let fontPx = maxPx; fontPx >= minPx; fontPx -= 1) {
|
|
3358
|
+
for (const rotation of [0, -90]) {
|
|
3359
|
+
sec.nameLabel.rotation(rotation);
|
|
3360
|
+
this.sizeLabel(sec.nameLabel, fontPx / sx, sec.nameLabel.y());
|
|
3361
|
+
for (const anchor of sec.labelAnchors) {
|
|
3362
|
+
sec.nameLabel.position(anchor);
|
|
3363
|
+
const paddingWorld = paddingPx / sx;
|
|
3364
|
+
if (rotatedRectFitsPolygon(
|
|
3365
|
+
anchor,
|
|
3366
|
+
sec.nameLabel.width() + paddingWorld,
|
|
3367
|
+
sec.nameLabel.height() + paddingWorld,
|
|
3368
|
+
rotation,
|
|
3369
|
+
sec.outline,
|
|
3370
|
+
sec.holes
|
|
3371
|
+
)) {
|
|
3372
|
+
sec.nameLabelFits = true;
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
sec.nameLabel.position(sec.centroid);
|
|
3379
|
+
sec.nameLabel.rotation(0);
|
|
3380
|
+
sec.nameLabelFits = false;
|
|
3381
|
+
}
|
|
3382
|
+
/** Size one screen-constant zone name/price pill around its shared anchor. */
|
|
3383
|
+
sizeZonePill(zone, sx) {
|
|
3384
|
+
const padX = 10 / sx;
|
|
3385
|
+
const padY = 6 / sx;
|
|
3386
|
+
const gap = zone.sub ? 3 / sx : 0;
|
|
3387
|
+
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, zone.anchor.y);
|
|
3388
|
+
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, zone.anchor.y);
|
|
3389
|
+
const width = Math.max(zone.label.width(), zone.sub?.width() ?? 0) + padX * 2;
|
|
3390
|
+
const height = zone.label.height() + (zone.sub ? gap + zone.sub.height() : 0) + padY * 2;
|
|
3391
|
+
zone.label.y(zone.anchor.y - (zone.sub ? (gap + zone.sub.height()) / 2 : 0));
|
|
3392
|
+
if (zone.sub) zone.sub.y(zone.anchor.y + (zone.label.height() + gap) / 2);
|
|
3393
|
+
zone.back.position(zone.anchor);
|
|
3394
|
+
zone.back.size({ width, height });
|
|
3395
|
+
zone.back.offset({ x: width / 2, y: height / 2 });
|
|
3396
|
+
zone.back.cornerRadius(7 / sx);
|
|
3397
|
+
zone.back.strokeWidth(1 / sx);
|
|
3398
|
+
}
|
|
2792
3399
|
/**
|
|
2793
3400
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
2794
3401
|
* stage (scale/pos) and, in iso view, the iso affine — so screen-space taps
|
|
@@ -2803,12 +3410,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2803
3410
|
sectionAt(clientPoint) {
|
|
2804
3411
|
if (!this.sections.length) return null;
|
|
2805
3412
|
const world = this.screenToWorld(clientPoint);
|
|
2806
|
-
const hit = this.sections.find((sec) =>
|
|
2807
|
-
return hit ? hit.
|
|
3413
|
+
const hit = this.sections.find((sec) => pointInPolygonWithHoles(world, sec.outline, sec.holes));
|
|
3414
|
+
return hit ? hit.logicalId : null;
|
|
2808
3415
|
}
|
|
2809
3416
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
2810
3417
|
sectionMembers(id) {
|
|
2811
|
-
return this.sections.
|
|
3418
|
+
return [...new Set(this.sections.filter((section) => section.id === id || section.logicalId === id || section.zone === id).flatMap((section) => section.memberIds))];
|
|
2812
3419
|
}
|
|
2813
3420
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
2814
3421
|
const t2 = new Text({
|
|
@@ -2825,6 +3432,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2825
3432
|
t2.offsetX(t2.width() / 2);
|
|
2826
3433
|
t2.offsetY(t2.height() / 2);
|
|
2827
3434
|
layer.add(t2);
|
|
3435
|
+
return t2;
|
|
2828
3436
|
}
|
|
2829
3437
|
// ---- selection ------------------------------------------------------------
|
|
2830
3438
|
isSelectable(id) {
|
|
@@ -2858,21 +3466,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2858
3466
|
* the container's computed CSS background (walking up past transparent
|
|
2859
3467
|
* ancestors). Unknown/unparseable backgrounds keep the dark default.
|
|
2860
3468
|
*/
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
if (
|
|
2865
|
-
let
|
|
2866
|
-
while (
|
|
2867
|
-
const
|
|
2868
|
-
if (
|
|
2869
|
-
|
|
2870
|
-
break;
|
|
2871
|
-
}
|
|
2872
|
-
el = el.parentElement;
|
|
3469
|
+
resolveCanvasBackground() {
|
|
3470
|
+
const themed = this.theme.background ? opaqueColorHex(this.theme.background) : null;
|
|
3471
|
+
if (themed) return themed;
|
|
3472
|
+
if (typeof getComputedStyle === "function") {
|
|
3473
|
+
let element = this.container;
|
|
3474
|
+
while (element) {
|
|
3475
|
+
const resolved = opaqueColorHex(getComputedStyle(element).backgroundColor);
|
|
3476
|
+
if (resolved) return resolved;
|
|
3477
|
+
element = element.parentElement;
|
|
2873
3478
|
}
|
|
2874
3479
|
}
|
|
2875
|
-
return
|
|
3480
|
+
return DEF_CANVAS_BACKGROUND;
|
|
3481
|
+
}
|
|
3482
|
+
resolveSelectionColor() {
|
|
3483
|
+
if (this.theme.selectionColor) return this.theme.selectionColor;
|
|
3484
|
+
return isLightColor(this.canvasBackground) ? DEF_SELECTION_ON_LIGHT : DEF_SELECTION;
|
|
2876
3485
|
}
|
|
2877
3486
|
setSelected(id, on, silent = false) {
|
|
2878
3487
|
const c = this.circleById.get(id);
|
|
@@ -2898,6 +3507,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2898
3507
|
const candidate = this.selectionFocusId === id;
|
|
2899
3508
|
const dims = this.boothDims.get(seat.rowId);
|
|
2900
3509
|
const marker = new Group({
|
|
3510
|
+
name: "selection-ring",
|
|
2901
3511
|
x: seat.x,
|
|
2902
3512
|
y: seat.y,
|
|
2903
3513
|
rotation: dims?.rotation ?? 0,
|
|
@@ -2905,6 +3515,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2905
3515
|
perfectDrawEnabled: false,
|
|
2906
3516
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
2907
3517
|
});
|
|
3518
|
+
marker.setAttr("seatId", id);
|
|
2908
3519
|
const common = {
|
|
2909
3520
|
stroke: this.effSelection,
|
|
2910
3521
|
listening: false,
|
|
@@ -2981,10 +3592,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2981
3592
|
* whether the section already fills the viewport (small container) so the tap
|
|
2982
3593
|
* must fall through and pick.
|
|
2983
3594
|
*/
|
|
3595
|
+
sectionBounds(id) {
|
|
3596
|
+
const bounds = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => polyBounds(section.outline));
|
|
3597
|
+
if (!bounds.length) return null;
|
|
3598
|
+
const left = Math.min(...bounds.map((box) => box.x));
|
|
3599
|
+
const top = Math.min(...bounds.map((box) => box.y));
|
|
3600
|
+
const right = Math.max(...bounds.map((box) => box.x + box.width));
|
|
3601
|
+
const bottom = Math.max(...bounds.map((box) => box.y + box.height));
|
|
3602
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
3603
|
+
}
|
|
2984
3604
|
sectionFrameScale(id) {
|
|
2985
|
-
const
|
|
2986
|
-
if (!
|
|
2987
|
-
const b = polyBounds(sec.outline);
|
|
3605
|
+
const b = this.sectionBounds(id);
|
|
3606
|
+
if (!b) return this.stage.scaleX();
|
|
2988
3607
|
const w = this.stage.width();
|
|
2989
3608
|
const h = this.stage.height();
|
|
2990
3609
|
const { min, max } = this.zoomBounds();
|
|
@@ -3014,11 +3633,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3014
3633
|
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
3015
3634
|
const sec = this.seatSection.get(id);
|
|
3016
3635
|
if (sec) {
|
|
3017
|
-
const alreadyFocused = this.focusedSectionId === sec.
|
|
3018
|
-
const canZoomInFurther = this.sectionFrameScale(sec.
|
|
3636
|
+
const alreadyFocused = this.focusedSectionId === sec.logicalId;
|
|
3637
|
+
const canZoomInFurther = this.sectionFrameScale(sec.logicalId) > this.stage.scaleX() * 1.02;
|
|
3019
3638
|
if (!alreadyFocused && canZoomInFurther) {
|
|
3020
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.
|
|
3021
|
-
else this.focusSection(sec.
|
|
3639
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.logicalId);
|
|
3640
|
+
else this.focusSection(sec.logicalId);
|
|
3022
3641
|
return;
|
|
3023
3642
|
}
|
|
3024
3643
|
}
|
|
@@ -3097,10 +3716,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3097
3716
|
}
|
|
3098
3717
|
if (this.sections.length) {
|
|
3099
3718
|
const world = this.screenToWorld(pointer);
|
|
3100
|
-
const hit = this.sections.find((sn) =>
|
|
3719
|
+
const hit = this.sections.find((sn) => pointInPolygonWithHoles(world, sn.outline, sn.holes));
|
|
3101
3720
|
if (hit) {
|
|
3102
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.
|
|
3103
|
-
else this.focusRegion(hit.
|
|
3721
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.logicalId);
|
|
3722
|
+
else this.focusRegion(hit.logicalId);
|
|
3104
3723
|
return;
|
|
3105
3724
|
}
|
|
3106
3725
|
}
|
|
@@ -3186,10 +3805,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3186
3805
|
* newer glide cancels an in-flight one.
|
|
3187
3806
|
*/
|
|
3188
3807
|
focusRegion(target, opts) {
|
|
3189
|
-
const b = typeof target === "string" ? (
|
|
3190
|
-
const sec = this.sections.find((s) => s.id === target);
|
|
3191
|
-
return sec ? polyBounds(sec.outline) : null;
|
|
3192
|
-
})() : target;
|
|
3808
|
+
const b = typeof target === "string" ? this.sectionBounds(target) : target;
|
|
3193
3809
|
if (!b) return;
|
|
3194
3810
|
this.cancelGlide();
|
|
3195
3811
|
if (opts?.animate === false || this.reducedMotion) {
|
|
@@ -3249,25 +3865,343 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3249
3865
|
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
3250
3866
|
return "sections";
|
|
3251
3867
|
}
|
|
3252
|
-
|
|
3868
|
+
getRenderedQualityEvidence() {
|
|
3869
|
+
const effectiveScale = this.effScale();
|
|
3870
|
+
const stageScale = this.stage.scaleX();
|
|
3871
|
+
const viewport = { width: this.stage.width(), height: this.stage.height() };
|
|
3872
|
+
const rounded = (value) => Math.round(value * 100) / 100;
|
|
3873
|
+
const labels = this.seats.map((seat) => {
|
|
3874
|
+
const shape = this.circleById.get(seat.id);
|
|
3875
|
+
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
3876
|
+
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE;
|
|
3877
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * effectiveScale);
|
|
3878
|
+
const screen = this.worldToScreen(seat);
|
|
3879
|
+
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
3880
|
+
const opacity = shape?.opacity() ?? 0;
|
|
3881
|
+
const section = this.seatSection.get(seat.id);
|
|
3882
|
+
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
3883
|
+
let hiddenReason;
|
|
3884
|
+
if (!visible) {
|
|
3885
|
+
if (opacity < 0.5) hiddenReason = "dimmed-or-unavailable";
|
|
3886
|
+
else if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
3887
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
3888
|
+
else if (!label) hiddenReason = "clutter-or-fit";
|
|
3889
|
+
else hiddenReason = "renderer-hidden";
|
|
3890
|
+
}
|
|
3891
|
+
const labelWidth = label ? label.width() * stageScale : 0;
|
|
3892
|
+
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
3893
|
+
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale : this.seatR * 2 * effectiveScale;
|
|
3894
|
+
const directHeightPx = shape instanceof Rect ? shape.height() * stageScale : this.seatR * 2 * effectiveScale;
|
|
3895
|
+
const assistedDiameterPx = 2 * (this.seatR * effectiveScale + SEAT_TAP_SLOP_PX);
|
|
3896
|
+
const fill = shape?.fill();
|
|
3897
|
+
const ink = label?.fill();
|
|
3898
|
+
return {
|
|
3899
|
+
seatId: seat.id,
|
|
3900
|
+
label: seat.label,
|
|
3901
|
+
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
3902
|
+
categoryKey: seat.categoryKey,
|
|
3903
|
+
...section ? { sectionId: section.id } : {},
|
|
3904
|
+
...section?.zone ? { zoneId: section.zone } : {},
|
|
3905
|
+
status: this.statusById.get(seat.id) ?? "free",
|
|
3906
|
+
selected: this.selection.has(seat.id),
|
|
3907
|
+
visible,
|
|
3908
|
+
renderedFontPx,
|
|
3909
|
+
fill: typeof fill === "string" ? fill : "",
|
|
3910
|
+
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3911
|
+
opacity: rounded(opacity),
|
|
3912
|
+
pointerTarget: {
|
|
3913
|
+
active: !this.cached && this.isSelectable(seat.id),
|
|
3914
|
+
directWidthPx: rounded(directWidthPx),
|
|
3915
|
+
directHeightPx: rounded(directHeightPx),
|
|
3916
|
+
effectiveMinimumPx: rounded(Math.max(
|
|
3917
|
+
Math.min(directWidthPx, directHeightPx),
|
|
3918
|
+
assistedDiameterPx
|
|
3919
|
+
))
|
|
3920
|
+
},
|
|
3921
|
+
screenCenter: { x: rounded(screen.x), y: rounded(screen.y) },
|
|
3922
|
+
...visible ? {
|
|
3923
|
+
screenBox: {
|
|
3924
|
+
x: rounded(screen.x - labelWidth / 2),
|
|
3925
|
+
y: rounded(screen.y - labelHeight / 2),
|
|
3926
|
+
width: rounded(labelWidth),
|
|
3927
|
+
height: rounded(labelHeight)
|
|
3928
|
+
}
|
|
3929
|
+
} : {},
|
|
3930
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
3931
|
+
};
|
|
3932
|
+
});
|
|
3933
|
+
const visibleLabels = labels.filter((label) => label.visible).length;
|
|
3934
|
+
const hierarchyEvidence = (id, kind, role, node, backgroundFill, section) => {
|
|
3935
|
+
const worldCorners = rotatedRectPoints(
|
|
3936
|
+
{ x: node.x(), y: node.y() },
|
|
3937
|
+
node.width(),
|
|
3938
|
+
node.height(),
|
|
3939
|
+
node.rotation()
|
|
3940
|
+
);
|
|
3941
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen(corner)));
|
|
3942
|
+
const opacity = rounded(node.opacity());
|
|
3943
|
+
const ink = node.fill();
|
|
3944
|
+
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
3945
|
+
const visible = node.isVisible() && opacity > 0.05 && !outside;
|
|
3946
|
+
const fitsContainer = section ? rotatedRectFitsPolygon(
|
|
3947
|
+
{ x: node.x(), y: node.y() },
|
|
3948
|
+
node.width(),
|
|
3949
|
+
node.height(),
|
|
3950
|
+
node.rotation(),
|
|
3951
|
+
section.outline,
|
|
3952
|
+
section.holes
|
|
3953
|
+
) : void 0;
|
|
3954
|
+
return {
|
|
3955
|
+
id,
|
|
3956
|
+
kind,
|
|
3957
|
+
role,
|
|
3958
|
+
label: node.text(),
|
|
3959
|
+
visible,
|
|
3960
|
+
renderedFontPx: rounded(node.fontSize() * stageScale),
|
|
3961
|
+
opacity,
|
|
3962
|
+
fill: backgroundFill,
|
|
3963
|
+
ink: typeof ink === "string" ? ink : "",
|
|
3964
|
+
...fitsContainer == null ? {} : { fitsContainer },
|
|
3965
|
+
...visible ? {
|
|
3966
|
+
screenBox: {
|
|
3967
|
+
x: rounded(screenBounds.x),
|
|
3968
|
+
y: rounded(screenBounds.y),
|
|
3969
|
+
width: rounded(screenBounds.width),
|
|
3970
|
+
height: rounded(screenBounds.height)
|
|
3971
|
+
}
|
|
3972
|
+
} : {}
|
|
3973
|
+
};
|
|
3974
|
+
};
|
|
3975
|
+
const hierarchyLabels = [
|
|
3976
|
+
...this.sections.map((section) => {
|
|
3977
|
+
const fill = section.blockPoly.fill();
|
|
3978
|
+
return hierarchyEvidence(
|
|
3979
|
+
section.id,
|
|
3980
|
+
"section",
|
|
3981
|
+
"name",
|
|
3982
|
+
section.nameLabel,
|
|
3983
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
3984
|
+
section
|
|
3985
|
+
);
|
|
3986
|
+
}),
|
|
3987
|
+
...this.sections.map((section) => {
|
|
3988
|
+
const fill = section.blockPoly.fill();
|
|
3989
|
+
return hierarchyEvidence(
|
|
3990
|
+
`${section.id}:availability`,
|
|
3991
|
+
"section",
|
|
3992
|
+
"availability",
|
|
3993
|
+
section.subLabel,
|
|
3994
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
3995
|
+
section
|
|
3996
|
+
);
|
|
3997
|
+
}),
|
|
3998
|
+
...this.zones.flatMap((zone) => [
|
|
3999
|
+
hierarchyEvidence(zone.id, "zone", "name", zone.label, zone.background),
|
|
4000
|
+
...zone.sub ? [hierarchyEvidence(`${zone.id}:price`, "zone", "price", zone.sub, zone.background)] : []
|
|
4001
|
+
])
|
|
4002
|
+
];
|
|
4003
|
+
const gaAreas = [...this.gaById].map(([areaId, ga]) => {
|
|
4004
|
+
const screenPoints = ga.points.map((point) => this.worldToScreen(point));
|
|
4005
|
+
const left = Math.min(...screenPoints.map((point) => point.x));
|
|
4006
|
+
const top = Math.min(...screenPoints.map((point) => point.y));
|
|
4007
|
+
const right = Math.max(...screenPoints.map((point) => point.x));
|
|
4008
|
+
const bottom = Math.max(...screenPoints.map((point) => point.y));
|
|
4009
|
+
const outside = right < 0 || left > viewport.width || bottom < 0 || top > viewport.height;
|
|
4010
|
+
const opacity = rounded(ga.polygon.opacity());
|
|
4011
|
+
const visible = opacity >= 0.1 && !outside;
|
|
4012
|
+
const fill = ga.polygon.fill();
|
|
4013
|
+
return {
|
|
4014
|
+
areaId,
|
|
4015
|
+
label: ga.label,
|
|
4016
|
+
capacity: ga.capacity,
|
|
4017
|
+
categoryKey: ga.categoryKey,
|
|
4018
|
+
...ga.sectionId ? { sectionId: ga.sectionId } : {},
|
|
4019
|
+
visible,
|
|
4020
|
+
interactive: ga.polygon.listening(),
|
|
4021
|
+
opacity,
|
|
4022
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4023
|
+
effectiveBackground: ga.effectiveBackground,
|
|
4024
|
+
...visible ? {
|
|
4025
|
+
screenBox: {
|
|
4026
|
+
x: rounded(left),
|
|
4027
|
+
y: rounded(top),
|
|
4028
|
+
width: rounded(right - left),
|
|
4029
|
+
height: rounded(bottom - top)
|
|
4030
|
+
}
|
|
4031
|
+
} : {}
|
|
4032
|
+
};
|
|
4033
|
+
});
|
|
4034
|
+
const freeTextLabels = [...this.freeTextById].map(([recordKey, record]) => {
|
|
4035
|
+
const { node, background, kind } = record;
|
|
4036
|
+
const point = this.worldToScreen({ x: node.x(), y: node.y() });
|
|
4037
|
+
const width = node.width() * stageScale;
|
|
4038
|
+
const height = node.height() * effectiveScale;
|
|
4039
|
+
const left = point.x - node.offsetX() * stageScale;
|
|
4040
|
+
const top = point.y - node.offsetY() * effectiveScale;
|
|
4041
|
+
const renderedFontPx = rounded(node.fontSize() * effectiveScale);
|
|
4042
|
+
const outside = left + width < 0 || left > viewport.width || top + height < 0 || top > viewport.height;
|
|
4043
|
+
const visible = node.isVisible() && !outside;
|
|
4044
|
+
const ink = node.fill();
|
|
4045
|
+
const opacity = rounded(node.getAbsoluteOpacity());
|
|
4046
|
+
let hiddenReason;
|
|
4047
|
+
if (!visible) {
|
|
4048
|
+
if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4049
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4050
|
+
else hiddenReason = "renderer-hidden";
|
|
4051
|
+
}
|
|
4052
|
+
return {
|
|
4053
|
+
objectId: record.objectId ?? recordKey,
|
|
4054
|
+
kind,
|
|
4055
|
+
text: node.text(),
|
|
4056
|
+
visible,
|
|
4057
|
+
renderedFontPx,
|
|
4058
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4059
|
+
background,
|
|
4060
|
+
opacity,
|
|
4061
|
+
...visible ? {
|
|
4062
|
+
screenBox: {
|
|
4063
|
+
x: rounded(left),
|
|
4064
|
+
y: rounded(top),
|
|
4065
|
+
width: rounded(width),
|
|
4066
|
+
height: rounded(height)
|
|
4067
|
+
}
|
|
4068
|
+
} : {},
|
|
4069
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4070
|
+
};
|
|
4071
|
+
});
|
|
4072
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
4073
|
+
const neutralSectionFills = /* @__PURE__ */ new Set([
|
|
4074
|
+
palette.sectionFill.toLowerCase(),
|
|
4075
|
+
darken(palette.sectionFill, 0.12).toLowerCase()
|
|
4076
|
+
]);
|
|
4077
|
+
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4078
|
+
return {
|
|
4079
|
+
viewport,
|
|
4080
|
+
canvasBackground: this.canvasBackground,
|
|
4081
|
+
effectiveScale: rounded(effectiveScale),
|
|
4082
|
+
rung: this.getRung(),
|
|
4083
|
+
minimumVisibleLabelPx: MIN_VISIBLE_BOOKABLE_LABEL_PX,
|
|
4084
|
+
totalLabelledBookableUnits: labels.length,
|
|
4085
|
+
visibleLabels,
|
|
4086
|
+
hiddenLabels: labels.length - visibleLabels,
|
|
4087
|
+
totalBookableUnits: labels.length + gaAreas.reduce((sum, area) => sum + area.capacity, 0),
|
|
4088
|
+
selectionRingSeatIds: this.overlayLayer.find(".selection-ring").map((node) => String(node.getAttr("seatId") ?? "")).filter(Boolean),
|
|
4089
|
+
selectionRingColor: this.effSelection,
|
|
4090
|
+
focusedSectionId: this.focusedSectionId,
|
|
4091
|
+
focusBackdropVisible: Boolean(this.focusBackdrop?.isVisible()),
|
|
4092
|
+
categoryFilterKeys: this.categoryFilter ? [...this.categoryFilter].sort() : null,
|
|
4093
|
+
overviewStyle: {
|
|
4094
|
+
visibleSectionShells: visibleSectionShells.length,
|
|
4095
|
+
categoryPaintedSectionShells: visibleSectionShells.filter((section) => {
|
|
4096
|
+
const fill = section.blockPoly.fill();
|
|
4097
|
+
return typeof fill !== "string" || !neutralSectionFills.has(fill.toLowerCase());
|
|
4098
|
+
}).length,
|
|
4099
|
+
visibleCategoryDetailOutlines: this.sections.filter((section) => section.outlinePoly.opacity() > 0.05).length,
|
|
4100
|
+
// Row-hint nodes no longer exist in the production overview scene.
|
|
4101
|
+
visibleSectionRowHints: 0,
|
|
4102
|
+
visibleSectionAvailabilityLabels: this.sections.filter((section) => section.subLabel.opacity() > 0.05).length,
|
|
4103
|
+
visibleSectionGADetails: [...this.gaById.values()].filter((area) => area.sectionId != null && area.polygon.opacity() > 0.05).length
|
|
4104
|
+
},
|
|
4105
|
+
labels,
|
|
4106
|
+
gaAreas,
|
|
4107
|
+
hierarchyLabels,
|
|
4108
|
+
freeTextLabels
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
4111
|
+
/** Jump the camera to a rung's zoom band (glided). */
|
|
3253
4112
|
setRung(rung) {
|
|
3254
4113
|
if (rung === "zones") {
|
|
3255
4114
|
this.cancelGlide();
|
|
3256
4115
|
this.zoomToFit();
|
|
3257
4116
|
return;
|
|
3258
4117
|
}
|
|
3259
|
-
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4118
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4119
|
+
this.seatLabelTargetScale() * 1.05,
|
|
4120
|
+
SEAT_FOCUS_SCALE,
|
|
4121
|
+
CACHE_THRESHOLD * 1.3
|
|
4122
|
+
);
|
|
3260
4123
|
const w = this.stage.width();
|
|
3261
4124
|
const h = this.stage.height();
|
|
3262
|
-
const
|
|
3263
|
-
const
|
|
4125
|
+
const visible = this.getVisibleWorldRect();
|
|
4126
|
+
const viewCentre = {
|
|
4127
|
+
x: visible.x + visible.width / 2,
|
|
4128
|
+
y: visible.y + visible.height / 2
|
|
4129
|
+
};
|
|
4130
|
+
let cx = viewCentre.x;
|
|
4131
|
+
let cy = viewCentre.y;
|
|
4132
|
+
if (rung === "sections" && this.sections.length > 0) {
|
|
4133
|
+
const sectionCentres = this.sections.map((section) => {
|
|
4134
|
+
const bounds = polyBounds(section.outline);
|
|
4135
|
+
return {
|
|
4136
|
+
x: bounds.x + bounds.width / 2,
|
|
4137
|
+
y: bounds.y + bounds.height / 2
|
|
4138
|
+
};
|
|
4139
|
+
});
|
|
4140
|
+
const halfWidth = w / (target * 2);
|
|
4141
|
+
const halfHeight = h / (target * 2);
|
|
4142
|
+
const hierarchyWillBeVisible = sectionCentres.some((point) => Math.abs(point.x - viewCentre.x) <= halfWidth && Math.abs(point.y - viewCentre.y) <= halfHeight);
|
|
4143
|
+
if (!hierarchyWillBeVisible) {
|
|
4144
|
+
const nearest = sectionCentres.reduce((best, point) => {
|
|
4145
|
+
const distance = (point.x - viewCentre.x) ** 2 + (point.y - viewCentre.y) ** 2;
|
|
4146
|
+
return distance < best.distance ? { point, distance } : best;
|
|
4147
|
+
}, { point: sectionCentres[0], distance: Infinity });
|
|
4148
|
+
cx = nearest.point.x;
|
|
4149
|
+
cy = nearest.point.y;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
const seatAnchors = rung === "seats" ? this.seats.filter((seat) => seat.kind !== "booth") : [];
|
|
4153
|
+
if (seatAnchors.length > 0) {
|
|
4154
|
+
let nearest = seatAnchors[0];
|
|
4155
|
+
let nearestDistance = Infinity;
|
|
4156
|
+
for (const seat of seatAnchors) {
|
|
4157
|
+
const dx = seat.x - viewCentre.x;
|
|
4158
|
+
const dy = seat.y - viewCentre.y;
|
|
4159
|
+
const distance = dx * dx + dy * dy;
|
|
4160
|
+
if (distance < nearestDistance) {
|
|
4161
|
+
nearest = seat;
|
|
4162
|
+
nearestDistance = distance;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
cx = nearest.x;
|
|
4166
|
+
cy = nearest.y;
|
|
4167
|
+
}
|
|
3264
4168
|
const bw = w / (target * 1.12);
|
|
3265
4169
|
const bh = h / (target * 1.12);
|
|
3266
4170
|
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
3267
4171
|
}
|
|
4172
|
+
/**
|
|
4173
|
+
* The seat rung must account for labels that auto-fit inside a seat circle.
|
|
4174
|
+
* A short `A-1` remains at the normal 7u target; a table label such as
|
|
4175
|
+
* `T13-10` may fit at 4u and therefore needs a deeper camera target to reach
|
|
4176
|
+
* the same 12 CSS-pixel floor. Measurement happens only on explicit rung
|
|
4177
|
+
* navigation, never during pan/zoom frames.
|
|
4178
|
+
*/
|
|
4179
|
+
seatLabelTargetScale() {
|
|
4180
|
+
let minimumFont = BOOTH_LABEL_FONT_SIZE;
|
|
4181
|
+
const measure = new Text({
|
|
4182
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
4183
|
+
fontStyle: "600",
|
|
4184
|
+
fontFamily: this.labelFont(),
|
|
4185
|
+
listening: false
|
|
4186
|
+
});
|
|
4187
|
+
const maxWidth = this.seatR * 2 - 3;
|
|
4188
|
+
for (const seat of this.seats) {
|
|
4189
|
+
if (seat.kind === "booth") {
|
|
4190
|
+
minimumFont = Math.min(minimumFont, BOOTH_LABEL_FONT_SIZE);
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
measure.fontSize(SEAT_LABEL_FONT_SIZE);
|
|
4194
|
+
measure.text(bookableMarkerLabel(seat.displayLabel ?? seat.label));
|
|
4195
|
+
const fitted = measure.width() > maxWidth ? Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxWidth / measure.width()) : SEAT_LABEL_FONT_SIZE;
|
|
4196
|
+
minimumFont = Math.min(minimumFont, fitted);
|
|
4197
|
+
}
|
|
4198
|
+
measure.destroy();
|
|
4199
|
+
return MIN_VISIBLE_BOOKABLE_LABEL_PX / Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, minimumFont);
|
|
4200
|
+
}
|
|
3268
4201
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
3269
4202
|
afterViewChange() {
|
|
3270
4203
|
this.updateLOD();
|
|
4204
|
+
this.updateFreeTextVisibility();
|
|
3271
4205
|
this.updateLabels();
|
|
3272
4206
|
this.scheduleViewChange();
|
|
3273
4207
|
}
|
|
@@ -3281,7 +4215,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3281
4215
|
}
|
|
3282
4216
|
updateLOD() {
|
|
3283
4217
|
const scale = this.effScale();
|
|
4218
|
+
const focalScale = Math.max(scale, 1e-4);
|
|
4219
|
+
for (const [label, targetPx] of this.primaryFocalLabels) {
|
|
4220
|
+
this.sizeLabel(label, targetPx / focalScale, label.y());
|
|
4221
|
+
}
|
|
3284
4222
|
if (this.hasSections) this.applySectionLod(scale);
|
|
4223
|
+
else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
|
|
4224
|
+
this.paintGAStateForView();
|
|
3285
4225
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
3286
4226
|
if (shouldCache && !this.cached) {
|
|
3287
4227
|
this.cacheSeatLayer();
|
|
@@ -3320,15 +4260,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3320
4260
|
if (this.recacheTimer) {
|
|
3321
4261
|
clearTimeout(this.recacheTimer);
|
|
3322
4262
|
this.recacheTimer = null;
|
|
3323
|
-
this.
|
|
4263
|
+
if (this.effScale() < CACHE_THRESHOLD) {
|
|
4264
|
+
this.rebuildSeatCache();
|
|
4265
|
+
} else if (this.cached) {
|
|
4266
|
+
this.seatLayer.clearCache();
|
|
4267
|
+
this.seatLayer.listening(true);
|
|
4268
|
+
this.cached = false;
|
|
4269
|
+
}
|
|
3324
4270
|
}
|
|
3325
4271
|
this.bgLayer.draw();
|
|
3326
4272
|
this.seatLayer.draw();
|
|
3327
4273
|
this.overlayLayer.draw();
|
|
3328
4274
|
}
|
|
4275
|
+
updateFreeTextVisibility() {
|
|
4276
|
+
const effectiveScale = this.effScale();
|
|
4277
|
+
for (const { objectId, node, categoryKey, kind } of this.freeTextById.values()) {
|
|
4278
|
+
const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
|
|
4279
|
+
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
4280
|
+
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
3329
4283
|
updateLabels() {
|
|
3330
|
-
const
|
|
4284
|
+
const effectiveScale = this.effScale();
|
|
4285
|
+
const show = effectiveScale >= LABEL_SCALE;
|
|
4286
|
+
for (const [id, label] of this.boothLabelById) {
|
|
4287
|
+
const shape = this.circleById.get(id);
|
|
4288
|
+
label.visible(
|
|
4289
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.opacity() ?? 1) >= 0.5
|
|
4290
|
+
);
|
|
4291
|
+
}
|
|
3331
4292
|
this.labelGroup.destroyChildren();
|
|
4293
|
+
this.seatLabelById.clear();
|
|
3332
4294
|
if (!show) {
|
|
3333
4295
|
this.overlayLayer.batchDraw();
|
|
3334
4296
|
return;
|
|
@@ -3342,8 +4304,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3342
4304
|
for (const seat of this.seats) {
|
|
3343
4305
|
if (seat.kind === "booth") continue;
|
|
3344
4306
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
4307
|
+
const shape = this.circleById.get(seat.id);
|
|
4308
|
+
if ((shape?.opacity() ?? 1) < 0.5) continue;
|
|
3345
4309
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
3346
|
-
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id);
|
|
4310
|
+
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
3347
4311
|
if (unavailable) {
|
|
3348
4312
|
const cue = new Group({ x: seat.x, y: seat.y, listening: false });
|
|
3349
4313
|
if (status === "held") {
|
|
@@ -3381,23 +4345,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3381
4345
|
const t2 = new Text({
|
|
3382
4346
|
x: seat.x,
|
|
3383
4347
|
y: seat.y,
|
|
3384
|
-
text: seat.label,
|
|
3385
|
-
fontSize:
|
|
4348
|
+
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4349
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
3386
4350
|
fontStyle: "600",
|
|
3387
4351
|
fontFamily: this.labelFont(),
|
|
3388
|
-
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4352
|
+
fill: shape ? this.renderedBookableLabelInk(shape) : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3389
4353
|
listening: false,
|
|
3390
4354
|
perfectDrawEnabled: false
|
|
3391
4355
|
});
|
|
3392
4356
|
const maxW = this.seatR * 2 - 3;
|
|
3393
|
-
if (t2.width() > maxW) t2.fontSize(Math.max(
|
|
3394
|
-
if (t2.
|
|
4357
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxW / t2.width()));
|
|
4358
|
+
if (t2.width() > maxW + 0.01) {
|
|
4359
|
+
t2.destroy();
|
|
4360
|
+
continue;
|
|
4361
|
+
}
|
|
4362
|
+
if (!isBookableLabelLegibleAtScale(t2.fontSize(), effectiveScale)) {
|
|
3395
4363
|
t2.destroy();
|
|
3396
4364
|
continue;
|
|
3397
4365
|
}
|
|
3398
4366
|
t2.offsetX(t2.width() / 2);
|
|
3399
4367
|
t2.offsetY(t2.height() / 2);
|
|
3400
4368
|
this.labelGroup.add(t2);
|
|
4369
|
+
this.seatLabelById.set(seat.id, t2);
|
|
3401
4370
|
if (++count >= MAX_LABELS) break;
|
|
3402
4371
|
}
|
|
3403
4372
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -4649,9 +5618,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
4649
5618
|
|
|
4650
5619
|
// src/i18n/bundles.ts
|
|
4651
5620
|
var LOADERS = {
|
|
4652
|
-
es: () => import("./es-
|
|
4653
|
-
de: () => import("./de-
|
|
4654
|
-
fr: () => import("./fr-
|
|
5621
|
+
es: () => import("./es-NLD3NL3W.js").then((m) => ({ default: m.es })),
|
|
5622
|
+
de: () => import("./de-ZTKJOFHT.js").then((m) => ({ default: m.de })),
|
|
5623
|
+
fr: () => import("./fr-KSFKWSAT.js").then((m) => ({ default: m.fr }))
|
|
4655
5624
|
};
|
|
4656
5625
|
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
4657
5626
|
async function loadLocale(code) {
|
|
@@ -4671,9 +5640,12 @@ async function loadLocale(code) {
|
|
|
4671
5640
|
}
|
|
4672
5641
|
}
|
|
4673
5642
|
export {
|
|
5643
|
+
ACCESSIBILITY_RING_COLOR,
|
|
4674
5644
|
ACCESSIBILITY_TYPES,
|
|
4675
5645
|
CHART_STORAGE_KEY,
|
|
4676
5646
|
DEFAULT_CURRENCY,
|
|
5647
|
+
LABEL_STYLE_MAX_SIZE,
|
|
5648
|
+
LABEL_STYLE_MIN_SIZE,
|
|
4677
5649
|
MAX_EVENT_INVENTORY,
|
|
4678
5650
|
MAX_GA_CAPACITY,
|
|
4679
5651
|
PickerController,
|
|
@@ -4681,6 +5653,7 @@ export {
|
|
|
4681
5653
|
SeatmapRenderer,
|
|
4682
5654
|
UNGROUPED_ID,
|
|
4683
5655
|
accessibilityMeta,
|
|
5656
|
+
accessibilityRingColor,
|
|
4684
5657
|
allObjects,
|
|
4685
5658
|
applyHidden,
|
|
4686
5659
|
chartBounds,
|
|
@@ -4709,6 +5682,8 @@ export {
|
|
|
4709
5682
|
loadLocale,
|
|
4710
5683
|
objectCenter,
|
|
4711
5684
|
pointInPolygon,
|
|
5685
|
+
pointInPolygonWithHoles,
|
|
5686
|
+
polygonLabelPoint,
|
|
4712
5687
|
resolveLocale,
|
|
4713
5688
|
setLocale,
|
|
4714
5689
|
setMoneyLocale,
|