@seatlayer/core 0.16.0 → 0.17.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 +1248 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +411 -30
- package/dist/index.d.ts +411 -30
- package/dist/index.js +1243 -263
- 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
|
@@ -31,6 +31,71 @@ function layerOf(obj) {
|
|
|
31
31
|
}
|
|
32
32
|
var CHART_STORAGE_KEY = "seatmap.chart";
|
|
33
33
|
|
|
34
|
+
// src/core/complexGeometry.ts
|
|
35
|
+
function cubicPoint(path, t2) {
|
|
36
|
+
const u = 1 - t2;
|
|
37
|
+
const a = u * u * u;
|
|
38
|
+
const b = 3 * u * u * t2;
|
|
39
|
+
const c = 3 * u * t2 * t2;
|
|
40
|
+
const d = t2 * t2 * t2;
|
|
41
|
+
return {
|
|
42
|
+
x: a * path.start.x + b * path.control1.x + c * path.control2.x + d * path.end.x,
|
|
43
|
+
y: a * path.start.y + b * path.control1.y + c * path.control2.y + d * path.end.y
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function distributeAlongCubic(path, count, resolution = 192) {
|
|
47
|
+
if (!Number.isInteger(count) || count < 1) throw new Error("Path point count must be a positive integer");
|
|
48
|
+
if (count === 1) return [cubicPoint(path, 0.5)];
|
|
49
|
+
const samples = Array.from({ length: resolution + 1 }, (_, index) => cubicPoint(path, index / resolution));
|
|
50
|
+
const lengths = new Float64Array(samples.length);
|
|
51
|
+
for (let index = 1; index < samples.length; index += 1) {
|
|
52
|
+
const dx = samples[index].x - samples[index - 1].x;
|
|
53
|
+
const dy = samples[index].y - samples[index - 1].y;
|
|
54
|
+
lengths[index] = lengths[index - 1] + Math.hypot(dx, dy);
|
|
55
|
+
}
|
|
56
|
+
const total = lengths[lengths.length - 1];
|
|
57
|
+
if (total <= 1e-9) return Array.from({ length: count }, () => ({ ...path.start }));
|
|
58
|
+
const output = [];
|
|
59
|
+
let segment = 1;
|
|
60
|
+
for (let index = 0; index < count; index += 1) {
|
|
61
|
+
const target = total * index / (count - 1);
|
|
62
|
+
while (segment < lengths.length - 1 && lengths[segment] < target) segment += 1;
|
|
63
|
+
const before = lengths[segment - 1];
|
|
64
|
+
const after = lengths[segment];
|
|
65
|
+
const ratio = after === before ? 0 : (target - before) / (after - before);
|
|
66
|
+
output.push({
|
|
67
|
+
x: samples[segment - 1].x + (samples[segment].x - samples[segment - 1].x) * ratio,
|
|
68
|
+
y: samples[segment - 1].y + (samples[segment].y - samples[segment - 1].y) * ratio
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/core/sectionPath.ts
|
|
75
|
+
var TAU = Math.PI * 2;
|
|
76
|
+
function translateSectionOutlinePath(path, dx, dy) {
|
|
77
|
+
const translate = (point) => ({ x: point.x + dx, y: point.y + dy });
|
|
78
|
+
return transformSectionOutlinePath(path, translate);
|
|
79
|
+
}
|
|
80
|
+
function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected = false) {
|
|
81
|
+
return {
|
|
82
|
+
...path,
|
|
83
|
+
start: transform(path.start),
|
|
84
|
+
segments: path.segments.map((segment) => segment.kind === "line" ? { ...segment, end: transform(segment.end) } : segment.kind === "arc" ? {
|
|
85
|
+
...segment,
|
|
86
|
+
center: transform(segment.center),
|
|
87
|
+
radius: segment.radius * Math.abs(radiusScale),
|
|
88
|
+
clockwise: reflected ? !segment.clockwise : segment.clockwise,
|
|
89
|
+
end: transform(segment.end)
|
|
90
|
+
} : {
|
|
91
|
+
...segment,
|
|
92
|
+
control1: transform(segment.control1),
|
|
93
|
+
control2: transform(segment.control2),
|
|
94
|
+
end: transform(segment.end)
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
34
99
|
// src/core/layout.ts
|
|
35
100
|
function overrideAccessibility(o) {
|
|
36
101
|
if (!o) return [];
|
|
@@ -52,6 +117,7 @@ function place(lx, ly, deg, origin) {
|
|
|
52
117
|
function rowSeatPositions(row) {
|
|
53
118
|
const { seatCount, seatSpacing, curve, rotation, origin } = row;
|
|
54
119
|
const out = [];
|
|
120
|
+
if (row.path) return distributeAlongCubic(row.path, seatCount);
|
|
55
121
|
if (seatCount <= 1) {
|
|
56
122
|
if (seatCount === 1) out.push({ x: origin.x, y: origin.y });
|
|
57
123
|
return out;
|
|
@@ -214,6 +280,53 @@ function pointInPolygon(p, poly) {
|
|
|
214
280
|
}
|
|
215
281
|
return inside;
|
|
216
282
|
}
|
|
283
|
+
function pointOnPolygonBoundary(p, poly) {
|
|
284
|
+
return poly.some((start, index) => {
|
|
285
|
+
const end = poly[(index + 1) % poly.length];
|
|
286
|
+
const cross = (p.y - start.y) * (end.x - start.x) - (p.x - start.x) * (end.y - start.y);
|
|
287
|
+
if (Math.abs(cross) > 1e-7) return false;
|
|
288
|
+
const dot = (p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y);
|
|
289
|
+
const lengthSquared = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
290
|
+
return dot >= -1e-7 && dot <= lengthSquared + 1e-7;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function pointInPolygonWithHoles(p, outer, holes) {
|
|
294
|
+
return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
|
|
295
|
+
}
|
|
296
|
+
function polygonLabelPoint(outer, holes) {
|
|
297
|
+
if (!outer.length) return { x: 0, y: 0 };
|
|
298
|
+
const xs = outer.map((point) => point.x);
|
|
299
|
+
const ys = outer.map((point) => point.y);
|
|
300
|
+
const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
301
|
+
const centroid = polygonCentroid(outer);
|
|
302
|
+
if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
|
|
303
|
+
let best = outer[0];
|
|
304
|
+
let bestScore = -Infinity;
|
|
305
|
+
const rings = [outer, ...holes ?? []];
|
|
306
|
+
for (let row = 1; row < 24; row += 1) {
|
|
307
|
+
for (let column = 1; column < 24; column += 1) {
|
|
308
|
+
const point = {
|
|
309
|
+
x: bounds.minX + (bounds.maxX - bounds.minX) * column / 24,
|
|
310
|
+
y: bounds.minY + (bounds.maxY - bounds.minY) * row / 24
|
|
311
|
+
};
|
|
312
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
313
|
+
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
314
|
+
const end = ring[(index + 1) % ring.length];
|
|
315
|
+
const dx = end.x - start.x;
|
|
316
|
+
const dy = end.y - start.y;
|
|
317
|
+
const denominator = dx * dx + dy * dy;
|
|
318
|
+
const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
|
|
319
|
+
const t2 = Math.max(0, Math.min(1, projection));
|
|
320
|
+
return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
|
|
321
|
+
})));
|
|
322
|
+
if (score > bestScore) {
|
|
323
|
+
best = point;
|
|
324
|
+
bestScore = score;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best;
|
|
329
|
+
}
|
|
217
330
|
function polygonCentroid(pts) {
|
|
218
331
|
if (!pts.length) return { x: 0, y: 0 };
|
|
219
332
|
let x = 0;
|
|
@@ -277,9 +390,14 @@ function translateObject(o, dx, dy) {
|
|
|
277
390
|
case "booth":
|
|
278
391
|
return { ...o, center: p(o.center) };
|
|
279
392
|
case "gaArea":
|
|
280
|
-
return { ...o, points: pts(o.points) };
|
|
393
|
+
return { ...o, points: pts(o.points), ...o.holes ? { holes: o.holes.map(pts) } : {} };
|
|
281
394
|
case "section":
|
|
282
|
-
return {
|
|
395
|
+
return {
|
|
396
|
+
...o,
|
|
397
|
+
outline: pts(o.outline),
|
|
398
|
+
...o.outlinePath ? { outlinePath: translateSectionOutlinePath(o.outlinePath, dx, dy) } : {},
|
|
399
|
+
...o.holes ? { holes: o.holes.map(pts) } : {}
|
|
400
|
+
};
|
|
283
401
|
case "text":
|
|
284
402
|
return { ...o, position: p(o.position) };
|
|
285
403
|
case "shape":
|
|
@@ -402,12 +520,33 @@ function objectSeatLabels(o) {
|
|
|
402
520
|
function isSeatObject(o) {
|
|
403
521
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
404
522
|
}
|
|
523
|
+
function samePoints(left, right) {
|
|
524
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
525
|
+
}
|
|
526
|
+
function sameGASurfaceAsSection(object, section) {
|
|
527
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
528
|
+
const objectHoles = object.holes ?? [];
|
|
529
|
+
const sectionHoles = section.holes ?? [];
|
|
530
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
531
|
+
}
|
|
405
532
|
function computeSections(doc) {
|
|
406
533
|
const objs = allObjects(doc);
|
|
407
534
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
408
535
|
const nodes = /* @__PURE__ */ new Map();
|
|
409
536
|
for (const s of sectionObjs) {
|
|
410
|
-
|
|
537
|
+
const logicalId = s.logicalSectionId ?? s.id;
|
|
538
|
+
const existing = nodes.get(logicalId);
|
|
539
|
+
if (existing) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
nodes.set(logicalId, {
|
|
543
|
+
id: logicalId,
|
|
544
|
+
label: s.label || "Section",
|
|
545
|
+
zone: s.zone,
|
|
546
|
+
seatCount: 0,
|
|
547
|
+
objectIds: [],
|
|
548
|
+
seatLabels: []
|
|
549
|
+
});
|
|
411
550
|
}
|
|
412
551
|
const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
|
|
413
552
|
const objectToSection = /* @__PURE__ */ new Map();
|
|
@@ -415,22 +554,24 @@ function computeSections(doc) {
|
|
|
415
554
|
if (!isSeatObject(obj)) continue;
|
|
416
555
|
const labels = objectSeatLabels(obj);
|
|
417
556
|
if (labels.length === 0) continue;
|
|
557
|
+
const referencedLogicalId = obj.referenceInventorySource?.logicalSectionId;
|
|
418
558
|
const c = objectCenter(obj);
|
|
419
|
-
const
|
|
420
|
-
const
|
|
559
|
+
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
560
|
+
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
561
|
+
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
421
562
|
node.seatCount += labels.length;
|
|
422
563
|
node.objectIds.push(obj.id);
|
|
423
564
|
node.seatLabels.push(...labels);
|
|
424
565
|
objectToSection.set(obj.id, node.id);
|
|
425
566
|
}
|
|
426
567
|
return {
|
|
427
|
-
sections:
|
|
568
|
+
sections: [...nodes.values()],
|
|
428
569
|
ungrouped: ungrouped.objectIds.length ? ungrouped : null,
|
|
429
570
|
objectToSection
|
|
430
571
|
};
|
|
431
572
|
}
|
|
432
573
|
function isSectionHidden(s, hidden) {
|
|
433
|
-
return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
|
|
574
|
+
return hidden.has(s.id) || !!s.logicalSectionId && hidden.has(s.logicalSectionId) || !!s.zone && hidden.has(s.zone);
|
|
434
575
|
}
|
|
435
576
|
function hiddenObjectIds(doc, hidden) {
|
|
436
577
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -483,6 +624,61 @@ import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
|
483
624
|
import { Line } from "konva/lib/shapes/Line";
|
|
484
625
|
import { Text } from "konva/lib/shapes/Text";
|
|
485
626
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
627
|
+
import { Shape } from "konva/lib/Shape";
|
|
628
|
+
|
|
629
|
+
// src/core/chartRenderRules.ts
|
|
630
|
+
var SEAT_LABEL_FONT_SIZE = 7;
|
|
631
|
+
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
632
|
+
var GA_LABEL_FONT_SIZE = 15;
|
|
633
|
+
var GA_CAPACITY_LABEL_FONT_SIZE = 11;
|
|
634
|
+
var GA_FILL_OPACITY = 0.85;
|
|
635
|
+
var MIN_VISIBLE_BOOKABLE_LABEL_PX = 12;
|
|
636
|
+
var SMALL_TEXT_CONTRAST = 4.5;
|
|
637
|
+
var DARK_BOOKABLE_LABEL_INK = "#000000";
|
|
638
|
+
var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
|
|
639
|
+
function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
|
|
640
|
+
return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
|
|
641
|
+
}
|
|
642
|
+
function bookableMarkerLabel(publicLabel) {
|
|
643
|
+
return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
|
|
644
|
+
}
|
|
645
|
+
function luminance(value) {
|
|
646
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
647
|
+
if (!match) return null;
|
|
648
|
+
const channel = (offset) => {
|
|
649
|
+
const encoded = Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255;
|
|
650
|
+
return encoded <= 0.04045 ? encoded / 12.92 : ((encoded + 0.055) / 1.055) ** 2.4;
|
|
651
|
+
};
|
|
652
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
653
|
+
}
|
|
654
|
+
function renderedTextContrast(ink, fill) {
|
|
655
|
+
const inkLuminance = luminance(ink);
|
|
656
|
+
const fillLuminance = luminance(fill);
|
|
657
|
+
if (inkLuminance == null || fillLuminance == null) return null;
|
|
658
|
+
return (Math.max(inkLuminance, fillLuminance) + 0.05) / (Math.min(inkLuminance, fillLuminance) + 0.05);
|
|
659
|
+
}
|
|
660
|
+
function rgb(value) {
|
|
661
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
662
|
+
if (!match) return null;
|
|
663
|
+
const packed = Number.parseInt(match[1], 16);
|
|
664
|
+
return [packed >> 16 & 255, packed >> 8 & 255, packed & 255];
|
|
665
|
+
}
|
|
666
|
+
function compositeHexOver(foreground, background, opacity) {
|
|
667
|
+
const front = rgb(foreground);
|
|
668
|
+
const back = rgb(background);
|
|
669
|
+
if (!front || !back) return background;
|
|
670
|
+
const alpha = Math.max(0, Math.min(1, opacity));
|
|
671
|
+
const channels = front.map((value, index) => Math.round(value * alpha + back[index] * (1 - alpha)));
|
|
672
|
+
return `#${channels.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
673
|
+
}
|
|
674
|
+
function stateAwareBookableLabelInk(fill, preferred) {
|
|
675
|
+
const preferredContrast = renderedTextContrast(preferred, fill);
|
|
676
|
+
if (preferredContrast != null && preferredContrast >= SMALL_TEXT_CONTRAST) return preferred;
|
|
677
|
+
const darkContrast = renderedTextContrast(DARK_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
678
|
+
const lightContrast = renderedTextContrast(LIGHT_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
679
|
+
if (darkContrast === 0 && lightContrast === 0) return preferred;
|
|
680
|
+
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
681
|
+
}
|
|
486
682
|
|
|
487
683
|
// src/lib/money.ts
|
|
488
684
|
var DEFAULT_CURRENCY = "USD";
|
|
@@ -542,6 +738,8 @@ var en = {
|
|
|
542
738
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
543
739
|
"picker.language": "Language",
|
|
544
740
|
"picker.zoomToFit": "Zoom to fit",
|
|
741
|
+
"picker.seatCountLabel": "seats",
|
|
742
|
+
"picker.capacity": "capacity",
|
|
545
743
|
"picker.viewMode": "View mode",
|
|
546
744
|
"picker.floor": "Floor",
|
|
547
745
|
"picker.zoomLevel": "Zoom level",
|
|
@@ -631,7 +829,8 @@ function formatDate(value, opts) {
|
|
|
631
829
|
var SEAT_RADIUS = 9;
|
|
632
830
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
633
831
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
634
|
-
var LABEL_SCALE =
|
|
832
|
+
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
833
|
+
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
635
834
|
var SEAT_TAP_SLOP_PX = 14;
|
|
636
835
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
637
836
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
@@ -645,21 +844,29 @@ var ISO_SQUASH = 0.58;
|
|
|
645
844
|
var LIFT_PER_STEP = 58;
|
|
646
845
|
var ISO_TWEEN_MS = 320;
|
|
647
846
|
var CAMERA_GLIDE_MS = 650;
|
|
648
|
-
var BLOCK_FILL_ALPHA =
|
|
649
|
-
var
|
|
847
|
+
var BLOCK_FILL_ALPHA = 1;
|
|
848
|
+
var SECTION_STROKE_PX = 2;
|
|
849
|
+
var LIGHT_OVERVIEW_SECTION_FILL = "#e5e7eb";
|
|
850
|
+
var LIGHT_OVERVIEW_SECTION_STROKE = "#c7cbd1";
|
|
851
|
+
var LIGHT_OVERVIEW_SECTION_INK = "#595f69";
|
|
852
|
+
var LIGHT_OVERVIEW_FOCAL_FILL = "#d1d5db";
|
|
853
|
+
var LIGHT_OVERVIEW_FOCAL_STROKE = "#b8bdc4";
|
|
854
|
+
var DARK_OVERVIEW_SECTION_FILL = "#273142";
|
|
855
|
+
var DARK_OVERVIEW_SECTION_STROKE = "#526078";
|
|
856
|
+
var DARK_OVERVIEW_SECTION_INK = "#f1f5f9";
|
|
857
|
+
var DARK_OVERVIEW_FOCAL_FILL = "#374151";
|
|
858
|
+
var DARK_OVERVIEW_FOCAL_STROKE = "#64748b";
|
|
650
859
|
var SECTION_LABEL_PX = 20;
|
|
651
|
-
var
|
|
652
|
-
var ZONE_LABEL_PX =
|
|
860
|
+
var MIN_SECTION_LABEL_PX = 12;
|
|
861
|
+
var ZONE_LABEL_PX = 18;
|
|
653
862
|
var ZONE_SUB_PX = 12;
|
|
863
|
+
var HIERARCHY_PILL_BACKGROUND = "#111827";
|
|
654
864
|
var HELD_FILL = "#6b7280";
|
|
655
865
|
var TAKEN_FILL = "#374151";
|
|
656
866
|
var NFS_STROKE = "#4b5563";
|
|
657
|
-
var CLOSED_SECTION_FILL = "#586070";
|
|
658
867
|
var CLOSED_SEAT_FILL = "#4b5563";
|
|
659
868
|
var CLOSED_SEAT_OPACITY = 0.4;
|
|
660
869
|
var FOCUS_DIM_OPACITY = 0.16;
|
|
661
|
-
var FOCUS_DESATURATE = 0.72;
|
|
662
|
-
var FOCUS_NEUTRAL = "#6b7280";
|
|
663
870
|
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
664
871
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
665
872
|
var ACCESS_RING = {
|
|
@@ -680,6 +887,7 @@ var DEF_SELECTION = "#ffffff";
|
|
|
680
887
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
681
888
|
var DEF_DECOR_FILL = "#232c40";
|
|
682
889
|
var DEF_TEXT = "#8b93a7";
|
|
890
|
+
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
683
891
|
function colorLuminance(color) {
|
|
684
892
|
const s = color.trim();
|
|
685
893
|
let r = NaN;
|
|
@@ -692,11 +900,11 @@ function colorLuminance(color) {
|
|
|
692
900
|
g = parseInt(h.slice(2, 4), 16);
|
|
693
901
|
b = parseInt(h.slice(4, 6), 16);
|
|
694
902
|
} else {
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
697
|
-
r = +
|
|
698
|
-
g = +
|
|
699
|
-
b = +
|
|
903
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
|
|
904
|
+
if (rgb2) {
|
|
905
|
+
r = +rgb2[1];
|
|
906
|
+
g = +rgb2[2];
|
|
907
|
+
b = +rgb2[3];
|
|
700
908
|
}
|
|
701
909
|
}
|
|
702
910
|
if (Number.isNaN(r)) return NaN;
|
|
@@ -706,6 +914,34 @@ function isLightColor(color) {
|
|
|
706
914
|
const lum = colorLuminance(color);
|
|
707
915
|
return !Number.isNaN(lum) && lum > 0.6;
|
|
708
916
|
}
|
|
917
|
+
function opaqueColorHex(color) {
|
|
918
|
+
const value = color.trim();
|
|
919
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
|
|
920
|
+
if (hex) {
|
|
921
|
+
const expanded = hex[1].length === 3 ? hex[1].split("").map((channel) => channel + channel).join("") : hex[1];
|
|
922
|
+
return `#${expanded.toLowerCase()}`;
|
|
923
|
+
}
|
|
924
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
|
|
925
|
+
if (!rgb2 || rgb2[4] != null && Number(rgb2[4]) < 0.999) return null;
|
|
926
|
+
const channels = [Number(rgb2[1]), Number(rgb2[2]), Number(rgb2[3])];
|
|
927
|
+
if (channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
|
|
928
|
+
return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
929
|
+
}
|
|
930
|
+
function overviewPalette(canvasBackground) {
|
|
931
|
+
return isLightColor(canvasBackground) ? {
|
|
932
|
+
sectionFill: LIGHT_OVERVIEW_SECTION_FILL,
|
|
933
|
+
sectionStroke: LIGHT_OVERVIEW_SECTION_STROKE,
|
|
934
|
+
sectionInk: LIGHT_OVERVIEW_SECTION_INK,
|
|
935
|
+
focalFill: LIGHT_OVERVIEW_FOCAL_FILL,
|
|
936
|
+
focalStroke: LIGHT_OVERVIEW_FOCAL_STROKE
|
|
937
|
+
} : {
|
|
938
|
+
sectionFill: DARK_OVERVIEW_SECTION_FILL,
|
|
939
|
+
sectionStroke: DARK_OVERVIEW_SECTION_STROKE,
|
|
940
|
+
sectionInk: DARK_OVERVIEW_SECTION_INK,
|
|
941
|
+
focalFill: DARK_OVERVIEW_FOCAL_FILL,
|
|
942
|
+
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
943
|
+
};
|
|
944
|
+
}
|
|
709
945
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
710
946
|
function seatIdOf(target) {
|
|
711
947
|
const n = target;
|
|
@@ -741,6 +977,108 @@ function polyBounds(pts) {
|
|
|
741
977
|
}
|
|
742
978
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
743
979
|
}
|
|
980
|
+
function rotatedRectPoints(center, width, height, rotation) {
|
|
981
|
+
const radians = rotation * Math.PI / 180;
|
|
982
|
+
const cos = Math.cos(radians);
|
|
983
|
+
const sin = Math.sin(radians);
|
|
984
|
+
return [
|
|
985
|
+
{ x: -width / 2, y: -height / 2 },
|
|
986
|
+
{ x: width / 2, y: -height / 2 },
|
|
987
|
+
{ x: width / 2, y: height / 2 },
|
|
988
|
+
{ x: -width / 2, y: height / 2 }
|
|
989
|
+
].map((point) => ({
|
|
990
|
+
x: center.x + point.x * cos - point.y * sin,
|
|
991
|
+
y: center.y + point.x * sin + point.y * cos
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
function pointsBounds(points) {
|
|
995
|
+
const bounds = polyBounds(points);
|
|
996
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
997
|
+
}
|
|
998
|
+
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
999
|
+
const radians = rotation * Math.PI / 180;
|
|
1000
|
+
const cos = Math.cos(radians);
|
|
1001
|
+
const sin = Math.sin(radians);
|
|
1002
|
+
for (let yStep = 0; yStep <= 4; yStep++) {
|
|
1003
|
+
for (let xStep = 0; xStep <= 6; xStep++) {
|
|
1004
|
+
const localX = width * (xStep / 6 - 0.5);
|
|
1005
|
+
const localY = height * (yStep / 4 - 0.5);
|
|
1006
|
+
const point = {
|
|
1007
|
+
x: center.x + localX * cos - localY * sin,
|
|
1008
|
+
y: center.y + localX * sin + localY * cos
|
|
1009
|
+
};
|
|
1010
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) return false;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1016
|
+
const bounds = polyBounds(outer);
|
|
1017
|
+
const centre = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
1018
|
+
const points = [preferred];
|
|
1019
|
+
for (let row = 1; row < 12; row += 1) {
|
|
1020
|
+
for (let column = 1; column < 12; column += 1) {
|
|
1021
|
+
const point = {
|
|
1022
|
+
x: bounds.x + bounds.width * column / 12,
|
|
1023
|
+
y: bounds.y + bounds.height * row / 12
|
|
1024
|
+
};
|
|
1025
|
+
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
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));
|
|
1029
|
+
}
|
|
1030
|
+
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1031
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1032
|
+
const next = points[(index + 1) % points.length];
|
|
1033
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
1034
|
+
}, 0);
|
|
1035
|
+
const outerClockwise = signedArea(outer) > 0;
|
|
1036
|
+
return new Shape({
|
|
1037
|
+
...attrs,
|
|
1038
|
+
sceneFunc(context, shape) {
|
|
1039
|
+
context.beginPath();
|
|
1040
|
+
const polygonPath = (points) => {
|
|
1041
|
+
if (!points.length) return;
|
|
1042
|
+
context.moveTo(points[0].x, points[0].y);
|
|
1043
|
+
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1044
|
+
context.closePath();
|
|
1045
|
+
};
|
|
1046
|
+
const vectorPath = (path) => {
|
|
1047
|
+
context.moveTo(path.start.x, path.start.y);
|
|
1048
|
+
let current = path.start;
|
|
1049
|
+
for (const segment of path.segments) {
|
|
1050
|
+
if (segment.kind === "line") context.lineTo(segment.end.x, segment.end.y);
|
|
1051
|
+
else if (segment.kind === "arc") context.arc(
|
|
1052
|
+
segment.center.x,
|
|
1053
|
+
segment.center.y,
|
|
1054
|
+
segment.radius,
|
|
1055
|
+
Math.atan2(current.y - segment.center.y, current.x - segment.center.x),
|
|
1056
|
+
Math.atan2(segment.end.y - segment.center.y, segment.end.x - segment.center.x),
|
|
1057
|
+
!segment.clockwise
|
|
1058
|
+
);
|
|
1059
|
+
else context.bezierCurveTo(
|
|
1060
|
+
segment.control1.x,
|
|
1061
|
+
segment.control1.y,
|
|
1062
|
+
segment.control2.x,
|
|
1063
|
+
segment.control2.y,
|
|
1064
|
+
segment.end.x,
|
|
1065
|
+
segment.end.y
|
|
1066
|
+
);
|
|
1067
|
+
current = segment.end;
|
|
1068
|
+
}
|
|
1069
|
+
context.closePath();
|
|
1070
|
+
};
|
|
1071
|
+
if (outerPath) vectorPath(outerPath);
|
|
1072
|
+
else polygonPath(outer);
|
|
1073
|
+
for (const hole of holes ?? []) {
|
|
1074
|
+
const holeClockwise = signedArea(hole) > 0;
|
|
1075
|
+
polygonPath(holeClockwise === outerClockwise ? [...hole].reverse() : hole);
|
|
1076
|
+
}
|
|
1077
|
+
context.fillStrokeShape(shape);
|
|
1078
|
+
},
|
|
1079
|
+
perfectDrawEnabled: false
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
744
1082
|
function rgba(hex, a) {
|
|
745
1083
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
746
1084
|
if (!m) return hex;
|
|
@@ -760,11 +1098,11 @@ function mixColors(parts, fallback) {
|
|
|
760
1098
|
let b = 0;
|
|
761
1099
|
let tw = 0;
|
|
762
1100
|
for (const p of parts) {
|
|
763
|
-
const
|
|
764
|
-
if (!
|
|
765
|
-
r +=
|
|
766
|
-
g +=
|
|
767
|
-
b +=
|
|
1101
|
+
const rgb2 = hexToRgb(p.hex);
|
|
1102
|
+
if (!rgb2 || p.w <= 0) continue;
|
|
1103
|
+
r += rgb2[0] * p.w;
|
|
1104
|
+
g += rgb2[1] * p.w;
|
|
1105
|
+
b += rgb2[2] * p.w;
|
|
768
1106
|
tw += p.w;
|
|
769
1107
|
}
|
|
770
1108
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
@@ -788,11 +1126,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
788
1126
|
this.circleById = /* @__PURE__ */ new Map();
|
|
789
1127
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
790
1128
|
this.boothDims = /* @__PURE__ */ new Map();
|
|
791
|
-
/** Booth
|
|
1129
|
+
/** Booth labels live with the booth shape but obey the shared rendered-size LOD. */
|
|
792
1130
|
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
1131
|
+
/** Viewport seat labels are rebuilt after each settled camera change. */
|
|
1132
|
+
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1133
|
+
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1134
|
+
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1135
|
+
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1136
|
+
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1137
|
+
/** GA paint and text share price/highlight filter state. */
|
|
1138
|
+
this.gaById = /* @__PURE__ */ new Map();
|
|
793
1139
|
this.statusById = /* @__PURE__ */ new Map();
|
|
794
1140
|
this.catColor = /* @__PURE__ */ new Map();
|
|
795
1141
|
this.theme = {};
|
|
1142
|
+
/** Opaque paint actually visible behind transparent Konva canvases. */
|
|
1143
|
+
this.canvasBackground = DEF_CANVAS_BACKGROUND;
|
|
796
1144
|
/** Effective selection/hover ring color — resolved per chart in setChart(). */
|
|
797
1145
|
this.effSelection = DEF_SELECTION;
|
|
798
1146
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
@@ -1090,6 +1438,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1090
1438
|
this.circleById.clear();
|
|
1091
1439
|
this.boothDims.clear();
|
|
1092
1440
|
this.boothLabelById.clear();
|
|
1441
|
+
this.seatLabelById.clear();
|
|
1442
|
+
this.freeTextById.clear();
|
|
1443
|
+
this.primaryFocalLabels.clear();
|
|
1444
|
+
this.gaById.clear();
|
|
1445
|
+
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
1093
1446
|
this.selectionMarkers.clear();
|
|
1094
1447
|
this.ownedHold.clear();
|
|
1095
1448
|
this.selectionFocusId = null;
|
|
@@ -1101,6 +1454,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1101
1454
|
this.sections = [];
|
|
1102
1455
|
this.zones = [];
|
|
1103
1456
|
this.seatSection.clear();
|
|
1457
|
+
this.focusedSectionId = null;
|
|
1458
|
+
this.focusBackdrop = null;
|
|
1104
1459
|
this.catPrice.clear();
|
|
1105
1460
|
this.zoneColor.clear();
|
|
1106
1461
|
this.lodScale = 0;
|
|
@@ -1118,7 +1473,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1118
1473
|
this.hoverRing.visible(false);
|
|
1119
1474
|
this.theme = doc.theme ?? {};
|
|
1120
1475
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1121
|
-
this.container.style.background =
|
|
1476
|
+
this.container.style.background = "";
|
|
1477
|
+
this.canvasBackground = this.resolveCanvasBackground();
|
|
1478
|
+
this.container.style.background = this.canvasBackground;
|
|
1122
1479
|
this.effSelection = this.resolveSelectionColor();
|
|
1123
1480
|
this.hoverRing.stroke(this.effSelection);
|
|
1124
1481
|
this.hoverRing.radius(this.seatR + 2);
|
|
@@ -1271,6 +1628,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1271
1628
|
for (const id of ids) this.setSelected(id, false, true);
|
|
1272
1629
|
this.overlayLayer.batchDraw();
|
|
1273
1630
|
}
|
|
1631
|
+
setMaxSelection(maxSelection) {
|
|
1632
|
+
this.opts.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
1633
|
+
}
|
|
1634
|
+
select(seatIds) {
|
|
1635
|
+
const added = [];
|
|
1636
|
+
for (const id of seatIds) {
|
|
1637
|
+
if (this.selection.has(id) || !this.isSelectable(id)) continue;
|
|
1638
|
+
if (this.selection.size >= this.opts.maxSelection) {
|
|
1639
|
+
this.opts.onSelectionLimit?.(this.opts.maxSelection);
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
this.setSelected(id, true, true);
|
|
1643
|
+
const seat = this.seatById.get(id);
|
|
1644
|
+
if (seat) added.push(seat);
|
|
1645
|
+
}
|
|
1646
|
+
if (added.length) this.overlayLayer.batchDraw();
|
|
1647
|
+
return added;
|
|
1648
|
+
}
|
|
1274
1649
|
/** Switch organizer interaction in place so the host preserves camera, LOD,
|
|
1275
1650
|
* focus and live status state while moving between Monitor and Block. */
|
|
1276
1651
|
setManageInteraction(options) {
|
|
@@ -1329,12 +1704,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1329
1704
|
}
|
|
1330
1705
|
return this.selectMany(ids);
|
|
1331
1706
|
}
|
|
1707
|
+
/** Exact SDK capture helper. MCP never accepts this id; the SDK derives it
|
|
1708
|
+
* from the persisted floor and uses the normal selected paint/ring path. */
|
|
1709
|
+
setEvidenceSelection(seatId) {
|
|
1710
|
+
if (!this.seatById.has(seatId) || !this.isSelectable(seatId)) return false;
|
|
1711
|
+
if (this.selection.size) this.clearSelection();
|
|
1712
|
+
this.setSelected(seatId, true);
|
|
1713
|
+
this.overlayLayer.batchDraw();
|
|
1714
|
+
return this.selection.has(seatId);
|
|
1715
|
+
}
|
|
1332
1716
|
/** Selectable seats in a section OR zone id — pure read (no selection change). */
|
|
1333
1717
|
getSelectableInSection(sectionId) {
|
|
1334
1718
|
const out = [];
|
|
1335
1719
|
const seen = /* @__PURE__ */ new Set();
|
|
1336
1720
|
for (const sec of this.sections) {
|
|
1337
|
-
if (sec.id !== sectionId && sec.zone !== sectionId) continue;
|
|
1721
|
+
if (sec.id !== sectionId && sec.logicalId !== sectionId && sec.zone !== sectionId) continue;
|
|
1338
1722
|
for (const id of sec.memberIds) {
|
|
1339
1723
|
if (seen.has(id)) continue;
|
|
1340
1724
|
seen.add(id);
|
|
@@ -1477,6 +1861,65 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1477
1861
|
};
|
|
1478
1862
|
requestAnimationFrame(step);
|
|
1479
1863
|
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Pulse a section outline without moving the camera or mutating the authored
|
|
1866
|
+
* geometry. The temporary halo is drawn in the non-listening overlay layer,
|
|
1867
|
+
* so the apparent 4% lift never changes hit testing or selection bounds.
|
|
1868
|
+
*/
|
|
1869
|
+
flashSection(sectionId, color = "#22a06b") {
|
|
1870
|
+
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
1871
|
+
if (!matches.length) return;
|
|
1872
|
+
for (const section of matches) {
|
|
1873
|
+
const centre = section.outline.reduce(
|
|
1874
|
+
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
1875
|
+
{ x: 0, y: 0 }
|
|
1876
|
+
);
|
|
1877
|
+
centre.x /= section.outline.length;
|
|
1878
|
+
centre.y /= section.outline.length;
|
|
1879
|
+
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
1880
|
+
const halo = new Line({
|
|
1881
|
+
x: centre.x + lift.x,
|
|
1882
|
+
y: centre.y + lift.y,
|
|
1883
|
+
points: section.outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
1884
|
+
closed: true,
|
|
1885
|
+
stroke: color,
|
|
1886
|
+
strokeWidth: 3,
|
|
1887
|
+
strokeScaleEnabled: false,
|
|
1888
|
+
opacity: 0.92,
|
|
1889
|
+
listening: false,
|
|
1890
|
+
perfectDrawEnabled: false,
|
|
1891
|
+
shadowForStrokeEnabled: true,
|
|
1892
|
+
shadowColor: color,
|
|
1893
|
+
shadowBlur: 14,
|
|
1894
|
+
shadowOpacity: 0.7
|
|
1895
|
+
});
|
|
1896
|
+
this.overlayLayer.add(halo);
|
|
1897
|
+
this.overlayLayer.batchDraw();
|
|
1898
|
+
const remove = () => {
|
|
1899
|
+
if (!halo.getLayer()) return;
|
|
1900
|
+
halo.destroy();
|
|
1901
|
+
this.overlayLayer.batchDraw();
|
|
1902
|
+
};
|
|
1903
|
+
if (this.reducedMotion || typeof document !== "undefined" && document.hidden) {
|
|
1904
|
+
setTimeout(remove, 520);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const start = performance.now();
|
|
1908
|
+
const duration = 820;
|
|
1909
|
+
const step = (now) => {
|
|
1910
|
+
if (this.destroyed || !halo.getLayer()) return;
|
|
1911
|
+
const t2 = Math.min(1, (now - start) / duration);
|
|
1912
|
+
const eased = 1 - Math.pow(1 - t2, 3);
|
|
1913
|
+
const scale = 1 + eased * 0.04;
|
|
1914
|
+
halo.scale({ x: scale, y: scale });
|
|
1915
|
+
halo.opacity(0.92 * (1 - t2));
|
|
1916
|
+
this.overlayLayer.batchDraw();
|
|
1917
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
1918
|
+
else remove();
|
|
1919
|
+
};
|
|
1920
|
+
requestAnimationFrame(step);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1480
1923
|
/** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
|
|
1481
1924
|
nearestSeat(fromId, dir) {
|
|
1482
1925
|
const from = this.seatById.get(fromId);
|
|
@@ -1550,6 +1993,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1550
1993
|
seatCount() {
|
|
1551
1994
|
return this.seats.length;
|
|
1552
1995
|
}
|
|
1996
|
+
bookableCount() {
|
|
1997
|
+
let total = this.seats.length;
|
|
1998
|
+
for (const area of this.gaById.values()) total += area.capacity;
|
|
1999
|
+
return total;
|
|
2000
|
+
}
|
|
1553
2001
|
worldToScreen(point) {
|
|
1554
2002
|
const s = this.stage.scaleX();
|
|
1555
2003
|
const p = this.isoT === 0 ? point : this.isoForward(point);
|
|
@@ -1566,6 +2014,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1566
2014
|
const c = this.circleById.get(seat.id);
|
|
1567
2015
|
if (c) this.paintSeat(c, seat.id);
|
|
1568
2016
|
}
|
|
2017
|
+
this.updateLabels();
|
|
1569
2018
|
if (this.cached) {
|
|
1570
2019
|
this.seatLayer.clearCache();
|
|
1571
2020
|
this.cacheSeatLayer();
|
|
@@ -1592,12 +2041,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1592
2041
|
const c = this.circleById.get(seat.id);
|
|
1593
2042
|
if (c) this.paintSeat(c, seat.id);
|
|
1594
2043
|
}
|
|
2044
|
+
this.updateLabels();
|
|
1595
2045
|
if (this.cached) {
|
|
1596
2046
|
this.seatLayer.clearCache();
|
|
1597
2047
|
this.cacheSeatLayer();
|
|
1598
2048
|
} else {
|
|
1599
2049
|
this.seatLayer.batchDraw();
|
|
1600
2050
|
}
|
|
2051
|
+
this.applyGAFilterState();
|
|
2052
|
+
}
|
|
2053
|
+
gaCategoryDimmed(categoryKey) {
|
|
2054
|
+
return Boolean(
|
|
2055
|
+
this.categoryHighlight && categoryKey !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(categoryKey)
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
/** Keep GA paint and its two labels in the same legend/price-filter state. */
|
|
2059
|
+
applyGAFilterState() {
|
|
2060
|
+
this.paintGAStateForView();
|
|
2061
|
+
this.updateFreeTextVisibility();
|
|
2062
|
+
this.bgLayer.batchDraw();
|
|
2063
|
+
}
|
|
2064
|
+
paintGAStateForView() {
|
|
2065
|
+
for (const ga of this.gaById.values()) {
|
|
2066
|
+
const filteredOut = Boolean(this.categoryFilter && !this.categoryFilter.has(ga.categoryKey));
|
|
2067
|
+
const overviewHidden = ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
|
|
2068
|
+
ga.polygon.opacity(overviewHidden ? 0 : this.gaCategoryDimmed(ga.categoryKey) ? GA_FILL_OPACITY * 0.08 : GA_FILL_OPACITY);
|
|
2069
|
+
ga.polygon.listening(!overviewHidden && !filteredOut);
|
|
2070
|
+
}
|
|
1601
2071
|
}
|
|
1602
2072
|
/** Frame the currently available inventory that survived a buyer price
|
|
1603
2073
|
* filter. Clearing the filter glides back to the full venue. */
|
|
@@ -1892,13 +2362,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1892
2362
|
});
|
|
1893
2363
|
rect.setAttr("seatId", seat.id);
|
|
1894
2364
|
this.circleById.set(seat.id, rect);
|
|
1895
|
-
this.paintSeat(rect, seat.id);
|
|
1896
2365
|
target.add(rect);
|
|
1897
2366
|
const t2 = new Text({
|
|
1898
2367
|
x: seat.x,
|
|
1899
2368
|
y: seat.y,
|
|
1900
2369
|
text: seat.label,
|
|
1901
|
-
fontSize:
|
|
2370
|
+
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
1902
2371
|
fontStyle: "600",
|
|
1903
2372
|
fontFamily: this.labelFont(),
|
|
1904
2373
|
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
@@ -1907,9 +2376,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1907
2376
|
});
|
|
1908
2377
|
t2.offsetX(t2.width() / 2);
|
|
1909
2378
|
t2.offsetY(t2.height() / 2);
|
|
2379
|
+
t2.visible(false);
|
|
2380
|
+
this.boothLabelById.set(seat.id, t2);
|
|
1910
2381
|
this.hasBoothText = true;
|
|
1911
2382
|
this.boothLabelById.set(seat.id, t2);
|
|
1912
2383
|
target.add(t2);
|
|
2384
|
+
this.paintSeat(rect, seat.id);
|
|
1913
2385
|
}
|
|
1914
2386
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
1915
2387
|
seatBaseColor(categoryKey) {
|
|
@@ -1917,6 +2389,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1917
2389
|
const idx = this.catOrder.indexOf(categoryKey);
|
|
1918
2390
|
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
1919
2391
|
}
|
|
2392
|
+
/** Authored free fills retain the chart's validated ink. Renderer-owned
|
|
2393
|
+
* transient fills choose an ink against the paint that is actually visible. */
|
|
2394
|
+
renderedBookableLabelInk(id, shape) {
|
|
2395
|
+
const preferred = this.theme.seatLabelColor ?? DEF_SEAT_LABEL;
|
|
2396
|
+
const status = this.statusById.get(id) ?? "free";
|
|
2397
|
+
if (status === "free" && !this.selection.has(id)) return preferred;
|
|
2398
|
+
const fill = shape.fill();
|
|
2399
|
+
return stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", preferred);
|
|
2400
|
+
}
|
|
1920
2401
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
1921
2402
|
paintSeat(c, id) {
|
|
1922
2403
|
const seat = this.seatById.get(id);
|
|
@@ -1980,7 +2461,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1980
2461
|
}
|
|
1981
2462
|
if (this.dimmedSections.size) {
|
|
1982
2463
|
const sec = this.seatSection.get(id);
|
|
1983
|
-
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2464
|
+
if (sec && (this.dimmedSections.has(sec.id) || this.dimmedSections.has(sec.logicalId) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1984
2465
|
c.opacity(0.18);
|
|
1985
2466
|
}
|
|
1986
2467
|
}
|
|
@@ -1993,16 +2474,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1993
2474
|
}
|
|
1994
2475
|
if (this.focusedSectionId) {
|
|
1995
2476
|
const sec = this.seatSection.get(id);
|
|
1996
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2477
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
1997
2478
|
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
1998
2479
|
}
|
|
1999
2480
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2481
|
+
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2482
|
+
if (bookableLabel) {
|
|
2483
|
+
bookableLabel.fill(this.renderedBookableLabelInk(id, c));
|
|
2484
|
+
bookableLabel.visible(
|
|
2485
|
+
isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2000
2488
|
}
|
|
2001
2489
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
2002
2490
|
seatInClosedSection(id) {
|
|
2003
2491
|
if (!this.closedSections.size) return false;
|
|
2004
2492
|
const sec = this.seatSection.get(id);
|
|
2005
|
-
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2493
|
+
return !!sec && (this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2006
2494
|
}
|
|
2007
2495
|
/**
|
|
2008
2496
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -2015,6 +2503,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2015
2503
|
const c = this.circleById.get(seat.id);
|
|
2016
2504
|
if (c) this.paintSeat(c, seat.id);
|
|
2017
2505
|
}
|
|
2506
|
+
this.updateLabels();
|
|
2018
2507
|
if (this.cached) {
|
|
2019
2508
|
this.seatLayer.clearCache();
|
|
2020
2509
|
this.cacheSeatLayer();
|
|
@@ -2059,7 +2548,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2059
2548
|
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2060
2549
|
*/
|
|
2061
2550
|
focusSection(id) {
|
|
2062
|
-
if (!this.sections.some((
|
|
2551
|
+
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2063
2552
|
this.focusedSectionId = id;
|
|
2064
2553
|
this.drawFocusBackdrop(id);
|
|
2065
2554
|
this.repaintSectionsAndSeats();
|
|
@@ -2087,20 +2576,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2087
2576
|
this.focusBackdrop.destroy();
|
|
2088
2577
|
this.focusBackdrop = null;
|
|
2089
2578
|
}
|
|
2090
|
-
const
|
|
2091
|
-
if (!
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
}
|
|
2101
|
-
this.bgLayer.add(
|
|
2102
|
-
|
|
2103
|
-
this.focusBackdrop =
|
|
2579
|
+
const sections = this.sections.filter((section) => section.id === id || section.logicalId === id);
|
|
2580
|
+
if (!sections.length) return;
|
|
2581
|
+
const backdrop = new Group({ listening: false });
|
|
2582
|
+
for (const section of sections) {
|
|
2583
|
+
backdrop.add(polygonWithHolesShape(section.outline, section.holes, {
|
|
2584
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2585
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2586
|
+
strokeWidth: 1,
|
|
2587
|
+
listening: false
|
|
2588
|
+
}, section.outlinePath));
|
|
2589
|
+
}
|
|
2590
|
+
this.bgLayer.add(backdrop);
|
|
2591
|
+
backdrop.moveToTop();
|
|
2592
|
+
this.focusBackdrop = backdrop;
|
|
2104
2593
|
}
|
|
2105
2594
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2106
2595
|
repaintSectionsAndSeats() {
|
|
@@ -2128,7 +2617,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2128
2617
|
height: Math.abs(br.y - tl.y)
|
|
2129
2618
|
};
|
|
2130
2619
|
}
|
|
2131
|
-
/** Axis-aligned world bounds of
|
|
2620
|
+
/** Axis-aligned world bounds of seats, section outlines, and GA polygons (minimap F3 frame). */
|
|
2132
2621
|
getWorldBounds() {
|
|
2133
2622
|
let minX = Infinity;
|
|
2134
2623
|
let minY = Infinity;
|
|
@@ -2142,6 +2631,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2142
2631
|
};
|
|
2143
2632
|
for (const s of this.seats) grow(s.x, s.y);
|
|
2144
2633
|
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2634
|
+
for (const area of this.gaById.values()) for (const p of area.points) grow(p.x, p.y);
|
|
2145
2635
|
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2146
2636
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2147
2637
|
}
|
|
@@ -2153,12 +2643,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2153
2643
|
const c = this.circleById.get(seat.id);
|
|
2154
2644
|
if (c) this.paintSeat(c, seat.id);
|
|
2155
2645
|
}
|
|
2646
|
+
this.updateLabels();
|
|
2156
2647
|
if (this.cached) {
|
|
2157
2648
|
this.seatLayer.clearCache();
|
|
2158
2649
|
this.cacheSeatLayer();
|
|
2159
2650
|
} else {
|
|
2160
2651
|
this.seatLayer.batchDraw();
|
|
2161
2652
|
}
|
|
2653
|
+
this.applyGAFilterState();
|
|
2162
2654
|
}
|
|
2163
2655
|
renderBackground(doc) {
|
|
2164
2656
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
@@ -2176,32 +2668,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2176
2668
|
this.renderText(obj);
|
|
2177
2669
|
}
|
|
2178
2670
|
}
|
|
2179
|
-
const f = doc.focalPoint;
|
|
2180
|
-
if (f) {
|
|
2181
|
-
const size = 14;
|
|
2182
|
-
const cross = new Group({ listening: false });
|
|
2183
|
-
cross.add(
|
|
2184
|
-
new Line({ points: [f.x - size, f.y, f.x + size, f.y], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2185
|
-
new Line({ points: [f.x, f.y - size, f.x, f.y + size], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2186
|
-
new Circle({ x: f.x, y: f.y, radius: 3, fill: "#4b5563" })
|
|
2187
|
-
);
|
|
2188
|
-
this.bgLayer.add(cross);
|
|
2189
|
-
}
|
|
2190
2671
|
}
|
|
2191
2672
|
/** Organizer floor-plan photo, dimmed, at the very bottom of the bg layer. */
|
|
2192
2673
|
renderBackgroundImage(bg) {
|
|
2674
|
+
if (!bg.url || bg.visible === false) return;
|
|
2193
2675
|
const img = new window.Image();
|
|
2194
2676
|
img.onload = () => {
|
|
2195
2677
|
const natW = img.naturalWidth || 4;
|
|
2196
2678
|
const natH = img.naturalHeight || 3;
|
|
2679
|
+
const rawCrop = bg.crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
2680
|
+
const cropX = Math.max(0, Math.min(0.99, rawCrop.x));
|
|
2681
|
+
const cropY = Math.max(0, Math.min(0.99, rawCrop.y));
|
|
2682
|
+
const crop = {
|
|
2683
|
+
x: cropX,
|
|
2684
|
+
y: cropY,
|
|
2685
|
+
width: Math.max(0.01, Math.min(1 - cropX, rawCrop.width)),
|
|
2686
|
+
height: Math.max(0.01, Math.min(1 - cropY, rawCrop.height))
|
|
2687
|
+
};
|
|
2197
2688
|
const w = bg.width;
|
|
2198
|
-
const h = w * (natH / natW);
|
|
2689
|
+
const h = w * (natH * crop.height / (natW * crop.width));
|
|
2199
2690
|
const node = new KImage({
|
|
2200
2691
|
image: img,
|
|
2201
|
-
x: bg.center.x
|
|
2202
|
-
y: bg.center.y
|
|
2692
|
+
x: bg.center.x,
|
|
2693
|
+
y: bg.center.y,
|
|
2694
|
+
offsetX: w / 2,
|
|
2695
|
+
offsetY: h / 2,
|
|
2203
2696
|
width: w,
|
|
2204
2697
|
height: h,
|
|
2698
|
+
rotation: bg.rotation ?? 0,
|
|
2699
|
+
crop: {
|
|
2700
|
+
x: crop.x * natW,
|
|
2701
|
+
y: crop.y * natH,
|
|
2702
|
+
width: crop.width * natW,
|
|
2703
|
+
height: crop.height * natH
|
|
2704
|
+
},
|
|
2205
2705
|
opacity: bg.opacity,
|
|
2206
2706
|
listening: false
|
|
2207
2707
|
});
|
|
@@ -2273,28 +2773,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2273
2773
|
})
|
|
2274
2774
|
);
|
|
2275
2775
|
}
|
|
2276
|
-
this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
2776
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
2777
|
+
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
2277
2778
|
}
|
|
2278
2779
|
renderText(obj) {
|
|
2279
|
-
this.
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2780
|
+
const background = this.canvasBackground;
|
|
2781
|
+
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
2782
|
+
const node = new Text({
|
|
2783
|
+
x: obj.position.x,
|
|
2784
|
+
y: obj.position.y,
|
|
2785
|
+
text: obj.text,
|
|
2786
|
+
fontSize: obj.fontSize,
|
|
2787
|
+
rotation: obj.rotation,
|
|
2788
|
+
// Authored ink remains preferred, but an embed/theme surface can change
|
|
2789
|
+
// the actual canvas. Fail over to readable black/white instead of
|
|
2790
|
+
// painting an otherwise valid caption invisibly on that active surface.
|
|
2791
|
+
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
2792
|
+
fontFamily: this.labelFont(),
|
|
2793
|
+
listening: false,
|
|
2794
|
+
perfectDrawEnabled: false
|
|
2795
|
+
});
|
|
2796
|
+
this.freeTextById.set(obj.id, {
|
|
2797
|
+
node,
|
|
2798
|
+
background,
|
|
2799
|
+
kind: "free-text"
|
|
2800
|
+
});
|
|
2801
|
+
this.bgLayer.add(node);
|
|
2292
2802
|
}
|
|
2293
2803
|
renderShape(obj) {
|
|
2294
|
-
const
|
|
2804
|
+
const authoredFill = obj.fill ?? this.theme.decorFill ?? DEF_DECOR_FILL;
|
|
2295
2805
|
const isStage = obj.role === "stage";
|
|
2806
|
+
const referenceFocal = obj.role === "reference-focal";
|
|
2296
2807
|
const isDecor = !!obj.role && !isStage;
|
|
2297
|
-
const
|
|
2808
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2809
|
+
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
2810
|
+
const stroke = isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : void 0;
|
|
2811
|
+
const strokeWidth = isStage ? 1 : referenceFocal ? 2 : 0;
|
|
2298
2812
|
let cx = 0;
|
|
2299
2813
|
let cy = 0;
|
|
2300
2814
|
if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
@@ -2316,7 +2830,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2316
2830
|
height: obj.height,
|
|
2317
2831
|
...grad,
|
|
2318
2832
|
stroke,
|
|
2319
|
-
strokeWidth
|
|
2833
|
+
strokeWidth,
|
|
2320
2834
|
cornerRadius: 4,
|
|
2321
2835
|
listening: false
|
|
2322
2836
|
})
|
|
@@ -2330,7 +2844,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2330
2844
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2331
2845
|
} : { fill };
|
|
2332
2846
|
this.bgLayer.add(
|
|
2333
|
-
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth
|
|
2847
|
+
new Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
2334
2848
|
);
|
|
2335
2849
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
2336
2850
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -2345,17 +2859,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2345
2859
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2346
2860
|
} : { fill };
|
|
2347
2861
|
this.bgLayer.add(
|
|
2348
|
-
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth
|
|
2862
|
+
new Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
2349
2863
|
);
|
|
2350
2864
|
}
|
|
2351
2865
|
if (obj.label) {
|
|
2352
|
-
if (isStage)
|
|
2353
|
-
|
|
2354
|
-
|
|
2866
|
+
if (isStage) {
|
|
2867
|
+
const node = this.addStageLabel(cx, cy, obj.label, fill);
|
|
2868
|
+
this.primaryFocalLabels.set(node, 22);
|
|
2869
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "stage" });
|
|
2870
|
+
} else if (isDecor) {
|
|
2871
|
+
const node = this.addCentredLabel(
|
|
2872
|
+
this.bgLayer,
|
|
2873
|
+
obj.label,
|
|
2874
|
+
cx,
|
|
2875
|
+
cy,
|
|
2876
|
+
referenceFocal ? stateAwareBookableLabelInk(fill, "#e6e9f0") : "#9aa3b5",
|
|
2877
|
+
referenceFocal ? 18 : 12,
|
|
2878
|
+
false
|
|
2879
|
+
);
|
|
2880
|
+
if (referenceFocal) this.primaryFocalLabels.set(node, 18);
|
|
2881
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
2882
|
+
} else {
|
|
2883
|
+
const node = this.addCentredLabel(this.bgLayer, obj.label, cx, cy, "#cbd5e1", 16, true);
|
|
2884
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
2885
|
+
}
|
|
2355
2886
|
}
|
|
2356
2887
|
}
|
|
2357
2888
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
2358
|
-
addStageLabel(x, y, text) {
|
|
2889
|
+
addStageLabel(x, y, text, background) {
|
|
2890
|
+
const ink = stateAwareBookableLabelInk(background, "#e6e9f0");
|
|
2359
2891
|
const t2 = new Text({
|
|
2360
2892
|
x,
|
|
2361
2893
|
y,
|
|
@@ -2364,22 +2896,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2364
2896
|
fontStyle: "700",
|
|
2365
2897
|
letterSpacing: 4,
|
|
2366
2898
|
fontFamily: this.labelFont(),
|
|
2367
|
-
fill:
|
|
2899
|
+
fill: ink,
|
|
2368
2900
|
listening: false,
|
|
2369
2901
|
perfectDrawEnabled: false
|
|
2370
2902
|
});
|
|
2371
2903
|
t2.offsetX(t2.width() / 2);
|
|
2372
2904
|
t2.offsetY(t2.height() / 2);
|
|
2373
2905
|
this.bgLayer.add(t2);
|
|
2906
|
+
return t2;
|
|
2374
2907
|
}
|
|
2375
2908
|
renderGA(obj) {
|
|
2376
2909
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
2377
|
-
const
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2910
|
+
const canvas = this.canvasBackground;
|
|
2911
|
+
const effectiveBackground = compositeHexOver(color, canvas, GA_FILL_OPACITY);
|
|
2912
|
+
const preferredInk = this.theme.textColor ?? "#e6e9f0";
|
|
2913
|
+
const ink = stateAwareBookableLabelInk(effectiveBackground, preferredInk);
|
|
2914
|
+
const poly = polygonWithHolesShape(obj.points, obj.holes, {
|
|
2381
2915
|
fill: color,
|
|
2382
|
-
opacity:
|
|
2916
|
+
opacity: GA_FILL_OPACITY,
|
|
2383
2917
|
stroke: color,
|
|
2384
2918
|
strokeWidth: 1.5
|
|
2385
2919
|
});
|
|
@@ -2392,31 +2926,51 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2392
2926
|
this.container.style.cursor = "default";
|
|
2393
2927
|
});
|
|
2394
2928
|
this.bgLayer.add(poly);
|
|
2395
|
-
const
|
|
2396
|
-
const
|
|
2397
|
-
this.addCentredLabel(this.bgLayer, obj.label,
|
|
2398
|
-
this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`,
|
|
2929
|
+
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
2930
|
+
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
2931
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
2932
|
+
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
2933
|
+
this.freeTextById.set(`${obj.id}:label`, {
|
|
2934
|
+
objectId: obj.id,
|
|
2935
|
+
node: label,
|
|
2936
|
+
background: effectiveBackground,
|
|
2937
|
+
kind: "ga-label",
|
|
2938
|
+
categoryKey: obj.categoryKey
|
|
2939
|
+
});
|
|
2940
|
+
this.freeTextById.set(`${obj.id}:capacity`, {
|
|
2941
|
+
objectId: obj.id,
|
|
2942
|
+
node: capacity,
|
|
2943
|
+
background: effectiveBackground,
|
|
2944
|
+
kind: "ga-capacity",
|
|
2945
|
+
categoryKey: obj.categoryKey
|
|
2946
|
+
});
|
|
2947
|
+
this.gaById.set(obj.id, {
|
|
2948
|
+
label: obj.label,
|
|
2949
|
+
capacity: obj.capacity,
|
|
2950
|
+
categoryKey: obj.categoryKey,
|
|
2951
|
+
points: obj.points,
|
|
2952
|
+
polygon: poly,
|
|
2953
|
+
effectiveBackground,
|
|
2954
|
+
...containingSection ? { sectionId: containingSection.logicalId } : {}
|
|
2955
|
+
});
|
|
2399
2956
|
}
|
|
2400
2957
|
/**
|
|
2401
2958
|
* A section renders in three coordinated layers driven by the LOD melt:
|
|
2402
2959
|
* • a faint outline (the existing near-zoom look, untouched),
|
|
2403
|
-
* • a solid
|
|
2404
|
-
* •
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2960
|
+
* • a neutral solid shell that fades in at the overview rung, and
|
|
2961
|
+
* • one readable, contained section name.
|
|
2962
|
+
* Category, row, seat, and availability detail belongs to section focus/zoom.
|
|
2963
|
+
* Membership and category mix are still precomputed for the detailed state.
|
|
2407
2964
|
*/
|
|
2408
2965
|
renderSection(obj) {
|
|
2409
|
-
const
|
|
2410
|
-
const
|
|
2411
|
-
x: obj.outline.reduce((a, p) => a + p.x, 0) / obj.outline.length,
|
|
2412
|
-
y: obj.outline.reduce((a, p) => a + p.y, 0) / obj.outline.length
|
|
2413
|
-
};
|
|
2966
|
+
const centroid = polygonLabelPoint(obj.outline, obj.holes);
|
|
2967
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2414
2968
|
const memberIds = [];
|
|
2415
2969
|
const catCounts = /* @__PURE__ */ new Map();
|
|
2416
2970
|
let free = 0;
|
|
2417
2971
|
for (const seat of this.seats) {
|
|
2418
2972
|
if (this.seatSection.has(seat.id)) continue;
|
|
2419
|
-
if (!
|
|
2973
|
+
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
2420
2974
|
memberIds.push(seat.id);
|
|
2421
2975
|
catCounts.set(seat.categoryKey, (catCounts.get(seat.categoryKey) ?? 0) + 1);
|
|
2422
2976
|
if ((this.statusById.get(seat.id) ?? "free") === "free") free++;
|
|
@@ -2452,52 +3006,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2452
3006
|
}
|
|
2453
3007
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
2454
3008
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
2455
|
-
const outlinePoly =
|
|
2456
|
-
points: pts,
|
|
2457
|
-
closed: true,
|
|
3009
|
+
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
2458
3010
|
stroke: rgba(outlineTint, 0.5),
|
|
2459
3011
|
strokeWidth: 1.75,
|
|
2460
3012
|
fill: rgba(outlineTint, 0.08),
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
perfectDrawEnabled: false
|
|
2464
|
-
});
|
|
3013
|
+
listening: false
|
|
3014
|
+
}, obj.outlinePath);
|
|
2465
3015
|
bgTarget.add(outlinePoly);
|
|
2466
|
-
const blockPoly =
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
stroke: rgba("#ffffff", 0.12),
|
|
2471
|
-
strokeWidth: 1,
|
|
3016
|
+
const blockPoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3017
|
+
fill: palette.sectionFill,
|
|
3018
|
+
stroke: palette.sectionStroke,
|
|
3019
|
+
strokeWidth: SECTION_STROKE_PX,
|
|
2472
3020
|
opacity: 0,
|
|
2473
|
-
listening: false
|
|
2474
|
-
|
|
2475
|
-
});
|
|
3021
|
+
listening: false
|
|
3022
|
+
}, obj.outlinePath);
|
|
2476
3023
|
bgTarget.add(blockPoly);
|
|
2477
|
-
const rowSeats = /* @__PURE__ */ new Map();
|
|
2478
|
-
for (const id of memberIds) {
|
|
2479
|
-
const s = this.seatById.get(id);
|
|
2480
|
-
if (!s) continue;
|
|
2481
|
-
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
2482
|
-
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
2483
|
-
}
|
|
2484
|
-
const rowLines = [];
|
|
2485
|
-
for (const arr of rowSeats.values()) {
|
|
2486
|
-
if (arr.length < 2) continue;
|
|
2487
|
-
arr.sort((a, b) => a.i - b.i);
|
|
2488
|
-
const line = new Line({
|
|
2489
|
-
points: arr.flatMap((p) => [p.x, p.y]),
|
|
2490
|
-
stroke: rgba("#ffffff", 0.34),
|
|
2491
|
-
strokeWidth: SEAT_RADIUS * 0.55,
|
|
2492
|
-
lineCap: "round",
|
|
2493
|
-
lineJoin: "round",
|
|
2494
|
-
opacity: 0,
|
|
2495
|
-
listening: false,
|
|
2496
|
-
perfectDrawEnabled: false
|
|
2497
|
-
});
|
|
2498
|
-
rowLines.push(line);
|
|
2499
|
-
bgTarget.add(line);
|
|
2500
|
-
}
|
|
2501
3024
|
const nameLabel = new Text({
|
|
2502
3025
|
x: centroid.x,
|
|
2503
3026
|
y: centroid.y,
|
|
@@ -2505,12 +3028,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2505
3028
|
fontSize: 22,
|
|
2506
3029
|
fontStyle: "700",
|
|
2507
3030
|
fontFamily: this.labelFont(),
|
|
2508
|
-
fill:
|
|
2509
|
-
// Dark halo so the label reads over the seat dots at any zoom.
|
|
2510
|
-
shadowColor: "#05070c",
|
|
2511
|
-
shadowBlur: 6,
|
|
2512
|
-
shadowOpacity: 0.9,
|
|
2513
|
-
shadowForStrokeEnabled: false,
|
|
3031
|
+
fill: palette.sectionInk,
|
|
2514
3032
|
listening: false,
|
|
2515
3033
|
perfectDrawEnabled: false
|
|
2516
3034
|
});
|
|
@@ -2524,11 +3042,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2524
3042
|
fontSize: 12,
|
|
2525
3043
|
fontStyle: "700",
|
|
2526
3044
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2527
|
-
fill:
|
|
2528
|
-
shadowColor: "#05070c",
|
|
2529
|
-
shadowBlur: 5,
|
|
2530
|
-
shadowOpacity: 0.9,
|
|
2531
|
-
shadowForStrokeEnabled: false,
|
|
3045
|
+
fill: palette.sectionInk,
|
|
2532
3046
|
opacity: 0,
|
|
2533
3047
|
listening: false,
|
|
2534
3048
|
perfectDrawEnabled: false
|
|
@@ -2537,9 +3051,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2537
3051
|
bgTarget.add(subLabel);
|
|
2538
3052
|
const sec = {
|
|
2539
3053
|
id: obj.id,
|
|
3054
|
+
logicalId: obj.logicalSectionId ?? obj.id,
|
|
2540
3055
|
label: obj.label,
|
|
2541
3056
|
outline: obj.outline,
|
|
3057
|
+
...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
|
|
3058
|
+
holes: obj.holes ?? [],
|
|
2542
3059
|
centroid,
|
|
3060
|
+
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
|
|
2543
3061
|
zone: obj.zone,
|
|
2544
3062
|
memberIds,
|
|
2545
3063
|
total: memberIds.length,
|
|
@@ -2548,9 +3066,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2548
3066
|
outlineTint,
|
|
2549
3067
|
outlinePoly,
|
|
2550
3068
|
blockPoly,
|
|
2551
|
-
rowLines,
|
|
2552
3069
|
nameLabel,
|
|
2553
3070
|
subLabel,
|
|
3071
|
+
nameLabelFits: true,
|
|
3072
|
+
subLabelFits: true,
|
|
2554
3073
|
elevation,
|
|
2555
3074
|
liftGroupBg,
|
|
2556
3075
|
liftGroupSeat,
|
|
@@ -2562,7 +3081,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2562
3081
|
this.sections.push(sec);
|
|
2563
3082
|
}
|
|
2564
3083
|
refreshSectionHeat(sec) {
|
|
2565
|
-
const raw = this.sectionHeat.get(sec.id) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
3084
|
+
const raw = this.sectionHeat.get(sec.id) ?? this.sectionHeat.get(sec.logicalId) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
2566
3085
|
if (raw == null || raw <= 0) {
|
|
2567
3086
|
sec.outlinePoly.stroke(rgba(sec.outlineTint, 0.5));
|
|
2568
3087
|
sec.outlinePoly.fill(rgba(sec.outlineTint, 0.08));
|
|
@@ -2578,7 +3097,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2578
3097
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
2579
3098
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
2580
3099
|
}
|
|
2581
|
-
/** Recompute a section's
|
|
3100
|
+
/** Recompute a section's neutral overview state and retained detail count. */
|
|
2582
3101
|
refreshSectionFill(sec) {
|
|
2583
3102
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2584
3103
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
@@ -2586,25 +3105,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2586
3105
|
}
|
|
2587
3106
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2588
3107
|
isSectionClosed(sec) {
|
|
2589
|
-
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
3108
|
+
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2590
3109
|
}
|
|
2591
|
-
/**
|
|
2592
|
-
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2593
|
-
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2594
|
-
* when another section holds focus (AXS dim treatment).
|
|
2595
|
-
*/
|
|
3110
|
+
/** Clean overview shells never leak category, price, or live availability paint. */
|
|
2596
3111
|
sectionBlockFill(sec) {
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
fill = CLOSED_SECTION_FILL;
|
|
2600
|
-
} else {
|
|
2601
|
-
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2602
|
-
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2603
|
-
}
|
|
2604
|
-
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2605
|
-
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2606
|
-
}
|
|
2607
|
-
return fill;
|
|
3112
|
+
const fill = overviewPalette(this.canvasBackground).sectionFill;
|
|
3113
|
+
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
2608
3114
|
}
|
|
2609
3115
|
/**
|
|
2610
3116
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -2633,19 +3139,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2633
3139
|
if (typeof p === "number" && p < minPrice) minPrice = p;
|
|
2634
3140
|
}
|
|
2635
3141
|
}
|
|
3142
|
+
const back = new Rect({
|
|
3143
|
+
x: cx,
|
|
3144
|
+
y: cy,
|
|
3145
|
+
width: 1,
|
|
3146
|
+
height: 1,
|
|
3147
|
+
offsetX: 0.5,
|
|
3148
|
+
offsetY: 0.5,
|
|
3149
|
+
cornerRadius: 1,
|
|
3150
|
+
fill: HIERARCHY_PILL_BACKGROUND,
|
|
3151
|
+
stroke: z.color ?? anchor.outlineTint,
|
|
3152
|
+
strokeWidth: 1,
|
|
3153
|
+
opacity: 0,
|
|
3154
|
+
listening: false,
|
|
3155
|
+
perfectDrawEnabled: false
|
|
3156
|
+
});
|
|
3157
|
+
this.bgLayer.add(back);
|
|
2636
3158
|
const label = new Text({
|
|
2637
3159
|
x: cx,
|
|
2638
3160
|
y: cy,
|
|
2639
3161
|
text: z.label.toUpperCase(),
|
|
2640
|
-
fontSize:
|
|
3162
|
+
fontSize: ZONE_LABEL_PX,
|
|
2641
3163
|
fontStyle: "800",
|
|
2642
|
-
letterSpacing:
|
|
3164
|
+
letterSpacing: 0.5,
|
|
2643
3165
|
fontFamily: this.labelFont(),
|
|
2644
|
-
fill:
|
|
2645
|
-
shadowColor: "#05070c",
|
|
2646
|
-
shadowBlur: 10,
|
|
2647
|
-
shadowOpacity: 0.95,
|
|
2648
|
-
shadowForStrokeEnabled: false,
|
|
3166
|
+
fill: "#f4f6fb",
|
|
2649
3167
|
opacity: 0,
|
|
2650
3168
|
listening: false,
|
|
2651
3169
|
perfectDrawEnabled: false
|
|
@@ -2662,7 +3180,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2662
3180
|
fontSize: 14,
|
|
2663
3181
|
fontStyle: "600",
|
|
2664
3182
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2665
|
-
fill:
|
|
3183
|
+
fill: "#cbd5e1",
|
|
2666
3184
|
opacity: 0,
|
|
2667
3185
|
listening: false,
|
|
2668
3186
|
perfectDrawEnabled: false
|
|
@@ -2670,7 +3188,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2670
3188
|
sub.offsetX(sub.width() / 2);
|
|
2671
3189
|
this.bgLayer.add(sub);
|
|
2672
3190
|
}
|
|
2673
|
-
this.zones.push({
|
|
3191
|
+
this.zones.push({
|
|
3192
|
+
id: z.id,
|
|
3193
|
+
anchor: { x: cx, y: cy },
|
|
3194
|
+
back,
|
|
3195
|
+
background: HIERARCHY_PILL_BACKGROUND,
|
|
3196
|
+
label,
|
|
3197
|
+
sub
|
|
3198
|
+
});
|
|
2674
3199
|
}
|
|
2675
3200
|
}
|
|
2676
3201
|
/**
|
|
@@ -2692,74 +3217,106 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2692
3217
|
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
2693
3218
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
2694
3219
|
}
|
|
3220
|
+
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3221
|
+
if (sectionOverview) blockT = 1;
|
|
2695
3222
|
if (!this.zones.length) zoneT = 0;
|
|
2696
|
-
this.seatLayer.opacity(1 - blockT);
|
|
3223
|
+
this.seatLayer.opacity(sectionOverview ? 0 : 1 - blockT);
|
|
2697
3224
|
const sx = this.stage.scaleX();
|
|
2698
3225
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
2699
3226
|
if (rescale) this.lodScale = scale;
|
|
2700
3227
|
const focus = this.focusedSectionId;
|
|
3228
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3229
|
+
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
2701
3230
|
for (const sec of this.sections) {
|
|
2702
|
-
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3231
|
+
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3232
|
+
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
2703
3233
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
2704
|
-
|
|
2705
|
-
sec.
|
|
2706
|
-
sec.
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
3234
|
+
sec.blockPoly.stroke(palette.sectionStroke);
|
|
3235
|
+
sec.blockPoly.strokeWidth(SECTION_STROKE_PX / Math.max(sx, 1e-4));
|
|
3236
|
+
const sectionFill = sec.blockPoly.fill();
|
|
3237
|
+
const sectionInk = stateAwareBookableLabelInk(
|
|
3238
|
+
typeof sectionFill === "string" ? sectionFill : sec.baseFill,
|
|
3239
|
+
palette.sectionInk
|
|
3240
|
+
);
|
|
3241
|
+
sec.nameLabel.fill(sectionInk);
|
|
3242
|
+
sec.subLabel.fill(sectionInk);
|
|
3243
|
+
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
3244
|
+
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3245
|
+
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3246
|
+
sec.subLabel.opacity(0);
|
|
2712
3247
|
}
|
|
2713
3248
|
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
2714
3249
|
for (const zone of this.zones) {
|
|
3250
|
+
zone.back.opacity(zoneOpacity);
|
|
2715
3251
|
zone.label.opacity(zoneOpacity);
|
|
2716
3252
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
2717
|
-
if (rescale)
|
|
2718
|
-
const cy = zone.label.y();
|
|
2719
|
-
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
2720
|
-
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
2721
|
-
}
|
|
3253
|
+
if (rescale) this.sizeZonePill(zone, sx);
|
|
2722
3254
|
}
|
|
2723
3255
|
this.decollideRungLabels(sx);
|
|
3256
|
+
this.dedupeLogicalSectionLabels();
|
|
3257
|
+
for (const zone of this.zones) {
|
|
3258
|
+
const opacity = zone.label.opacity();
|
|
3259
|
+
zone.back.opacity(opacity);
|
|
3260
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
3261
|
+
}
|
|
2724
3262
|
this.bgLayer.batchDraw();
|
|
2725
3263
|
}
|
|
3264
|
+
/** One semantic section gets one overview label, even across split contours. */
|
|
3265
|
+
dedupeLogicalSectionLabels() {
|
|
3266
|
+
const byLogical = /* @__PURE__ */ new Map();
|
|
3267
|
+
for (const section of this.sections) {
|
|
3268
|
+
(byLogical.get(section.logicalId) ?? byLogical.set(section.logicalId, []).get(section.logicalId)).push(section);
|
|
3269
|
+
}
|
|
3270
|
+
for (const components of byLogical.values()) {
|
|
3271
|
+
if (components.length < 2) continue;
|
|
3272
|
+
const visible = components.filter((component) => component.nameLabel.opacity() > 0.05).sort((left, right) => {
|
|
3273
|
+
const leftBounds = polyBounds(left.outline);
|
|
3274
|
+
const rightBounds = polyBounds(right.outline);
|
|
3275
|
+
return rightBounds.width * rightBounds.height - leftBounds.width * leftBounds.height;
|
|
3276
|
+
});
|
|
3277
|
+
for (const component of visible.slice(1)) component.nameLabel.opacity(0);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
2726
3280
|
/**
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
2729
|
-
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
2730
|
-
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
2731
|
-
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
2732
|
-
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
3281
|
+
* Keep transitional zone pills from covering section names. Section names
|
|
3282
|
+
* are already proven inside disjoint shells, so they must not cull each other.
|
|
2733
3283
|
*/
|
|
2734
3284
|
decollideRungLabels(sx) {
|
|
2735
3285
|
const GAP = 4;
|
|
2736
3286
|
const cands = [];
|
|
2737
3287
|
const boxOf = (t2) => {
|
|
2738
3288
|
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
2739
|
-
const
|
|
2740
|
-
|
|
3289
|
+
const rotated = pointsBounds(rotatedRectPoints(
|
|
3290
|
+
{ x: 0, y: 0 },
|
|
3291
|
+
t2.width() * sx,
|
|
3292
|
+
t2.height() * sx,
|
|
3293
|
+
t2.rotation()
|
|
3294
|
+
));
|
|
3295
|
+
const w = rotated.width;
|
|
3296
|
+
const h = rotated.height;
|
|
2741
3297
|
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
2742
3298
|
};
|
|
2743
3299
|
for (const zone of this.zones) {
|
|
2744
|
-
if (zone.label.opacity() > 0.05)
|
|
2745
|
-
|
|
3300
|
+
if (zone.label.opacity() > 0.05) {
|
|
3301
|
+
const p = this.worldToScreen(zone.anchor);
|
|
3302
|
+
const w = zone.back.width() * sx;
|
|
3303
|
+
const h = zone.back.height() * sx;
|
|
3304
|
+
cands.push({ node: zone.label, tier: 0, section: false, box: { x: p.x - w / 2, y: p.y - h / 2, w, h } });
|
|
3305
|
+
}
|
|
2746
3306
|
}
|
|
2747
3307
|
for (const sec of this.sections) {
|
|
2748
|
-
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
2749
|
-
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
3308
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, section: true, box: boxOf(sec.nameLabel) });
|
|
2750
3309
|
}
|
|
2751
3310
|
if (cands.length < 2) return;
|
|
2752
3311
|
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
2753
3312
|
const kept = [];
|
|
2754
|
-
const culled = /* @__PURE__ */ new Set();
|
|
2755
3313
|
const collides = (b) => kept.some(
|
|
2756
3314
|
(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
|
|
2757
3315
|
);
|
|
2758
3316
|
for (const c of cands) {
|
|
2759
|
-
if (
|
|
3317
|
+
if (collides(c.box)) {
|
|
2760
3318
|
c.node.opacity(0);
|
|
2761
|
-
|
|
2762
|
-
} else {
|
|
3319
|
+
} else if (!c.section) {
|
|
2763
3320
|
kept.push(c.box);
|
|
2764
3321
|
}
|
|
2765
3322
|
}
|
|
@@ -2771,6 +3328,52 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2771
3328
|
t2.offsetY(t2.height() / 2);
|
|
2772
3329
|
t2.y(y);
|
|
2773
3330
|
}
|
|
3331
|
+
/** Fit one centred section name, rotating narrow shells like the target chart. */
|
|
3332
|
+
fitSectionRungLabels(sec, sx) {
|
|
3333
|
+
const paddingPx = 8;
|
|
3334
|
+
sec.subLabelFits = false;
|
|
3335
|
+
for (let fontPx = SECTION_LABEL_PX; fontPx >= MIN_SECTION_LABEL_PX; fontPx -= 1) {
|
|
3336
|
+
for (const rotation of [0, -90]) {
|
|
3337
|
+
sec.nameLabel.rotation(rotation);
|
|
3338
|
+
this.sizeLabel(sec.nameLabel, fontPx / sx, sec.nameLabel.y());
|
|
3339
|
+
for (const anchor of sec.labelAnchors) {
|
|
3340
|
+
sec.nameLabel.position(anchor);
|
|
3341
|
+
const paddingWorld = paddingPx / sx;
|
|
3342
|
+
if (rotatedRectFitsPolygon(
|
|
3343
|
+
anchor,
|
|
3344
|
+
sec.nameLabel.width() + paddingWorld,
|
|
3345
|
+
sec.nameLabel.height() + paddingWorld,
|
|
3346
|
+
rotation,
|
|
3347
|
+
sec.outline,
|
|
3348
|
+
sec.holes
|
|
3349
|
+
)) {
|
|
3350
|
+
sec.nameLabelFits = true;
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
sec.nameLabel.position(sec.centroid);
|
|
3357
|
+
sec.nameLabel.rotation(0);
|
|
3358
|
+
sec.nameLabelFits = false;
|
|
3359
|
+
}
|
|
3360
|
+
/** Size one screen-constant zone name/price pill around its shared anchor. */
|
|
3361
|
+
sizeZonePill(zone, sx) {
|
|
3362
|
+
const padX = 10 / sx;
|
|
3363
|
+
const padY = 6 / sx;
|
|
3364
|
+
const gap = zone.sub ? 3 / sx : 0;
|
|
3365
|
+
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, zone.anchor.y);
|
|
3366
|
+
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, zone.anchor.y);
|
|
3367
|
+
const width = Math.max(zone.label.width(), zone.sub?.width() ?? 0) + padX * 2;
|
|
3368
|
+
const height = zone.label.height() + (zone.sub ? gap + zone.sub.height() : 0) + padY * 2;
|
|
3369
|
+
zone.label.y(zone.anchor.y - (zone.sub ? (gap + zone.sub.height()) / 2 : 0));
|
|
3370
|
+
if (zone.sub) zone.sub.y(zone.anchor.y + (zone.label.height() + gap) / 2);
|
|
3371
|
+
zone.back.position(zone.anchor);
|
|
3372
|
+
zone.back.size({ width, height });
|
|
3373
|
+
zone.back.offset({ x: width / 2, y: height / 2 });
|
|
3374
|
+
zone.back.cornerRadius(7 / sx);
|
|
3375
|
+
zone.back.strokeWidth(1 / sx);
|
|
3376
|
+
}
|
|
2774
3377
|
/**
|
|
2775
3378
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
2776
3379
|
* stage (scale/pos) and, in iso view, the iso affine — so screen-space taps
|
|
@@ -2785,12 +3388,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2785
3388
|
sectionAt(clientPoint) {
|
|
2786
3389
|
if (!this.sections.length) return null;
|
|
2787
3390
|
const world = this.screenToWorld(clientPoint);
|
|
2788
|
-
const hit = this.sections.find((sec) =>
|
|
2789
|
-
return hit ? hit.
|
|
3391
|
+
const hit = this.sections.find((sec) => pointInPolygonWithHoles(world, sec.outline, sec.holes));
|
|
3392
|
+
return hit ? hit.logicalId : null;
|
|
2790
3393
|
}
|
|
2791
3394
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
2792
3395
|
sectionMembers(id) {
|
|
2793
|
-
return this.sections.
|
|
3396
|
+
return [...new Set(this.sections.filter((section) => section.id === id || section.logicalId === id || section.zone === id).flatMap((section) => section.memberIds))];
|
|
2794
3397
|
}
|
|
2795
3398
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
2796
3399
|
const t2 = new Text({
|
|
@@ -2807,6 +3410,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2807
3410
|
t2.offsetX(t2.width() / 2);
|
|
2808
3411
|
t2.offsetY(t2.height() / 2);
|
|
2809
3412
|
layer.add(t2);
|
|
3413
|
+
return t2;
|
|
2810
3414
|
}
|
|
2811
3415
|
// ---- selection ------------------------------------------------------------
|
|
2812
3416
|
isSelectable(id) {
|
|
@@ -2821,7 +3425,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2821
3425
|
if (seat) this.opts.onDeselect?.(seat);
|
|
2822
3426
|
} else {
|
|
2823
3427
|
if (!this.isSelectable(id)) return;
|
|
2824
|
-
if (this.selection.size >= this.opts.maxSelection)
|
|
3428
|
+
if (this.selection.size >= this.opts.maxSelection) {
|
|
3429
|
+
this.opts.onSelectionLimit?.(this.opts.maxSelection);
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
2825
3432
|
this.setSelected(id, true);
|
|
2826
3433
|
const seat = this.seatById.get(id);
|
|
2827
3434
|
if (seat) this.opts.onSelect?.(seat);
|
|
@@ -2837,21 +3444,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2837
3444
|
* the container's computed CSS background (walking up past transparent
|
|
2838
3445
|
* ancestors). Unknown/unparseable backgrounds keep the dark default.
|
|
2839
3446
|
*/
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
if (
|
|
2844
|
-
let
|
|
2845
|
-
while (
|
|
2846
|
-
const
|
|
2847
|
-
if (
|
|
2848
|
-
|
|
2849
|
-
break;
|
|
2850
|
-
}
|
|
2851
|
-
el = el.parentElement;
|
|
3447
|
+
resolveCanvasBackground() {
|
|
3448
|
+
const themed = this.theme.background ? opaqueColorHex(this.theme.background) : null;
|
|
3449
|
+
if (themed) return themed;
|
|
3450
|
+
if (typeof getComputedStyle === "function") {
|
|
3451
|
+
let element = this.container;
|
|
3452
|
+
while (element) {
|
|
3453
|
+
const resolved = opaqueColorHex(getComputedStyle(element).backgroundColor);
|
|
3454
|
+
if (resolved) return resolved;
|
|
3455
|
+
element = element.parentElement;
|
|
2852
3456
|
}
|
|
2853
3457
|
}
|
|
2854
|
-
return
|
|
3458
|
+
return DEF_CANVAS_BACKGROUND;
|
|
3459
|
+
}
|
|
3460
|
+
resolveSelectionColor() {
|
|
3461
|
+
if (this.theme.selectionColor) return this.theme.selectionColor;
|
|
3462
|
+
return isLightColor(this.canvasBackground) ? DEF_SELECTION_ON_LIGHT : DEF_SELECTION;
|
|
2855
3463
|
}
|
|
2856
3464
|
setSelected(id, on, silent = false) {
|
|
2857
3465
|
const c = this.circleById.get(id);
|
|
@@ -2877,6 +3485,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2877
3485
|
const candidate = this.selectionFocusId === id;
|
|
2878
3486
|
const dims = this.boothDims.get(seat.rowId);
|
|
2879
3487
|
const marker = new Group({
|
|
3488
|
+
name: "selection-ring",
|
|
2880
3489
|
x: seat.x,
|
|
2881
3490
|
y: seat.y,
|
|
2882
3491
|
rotation: dims?.rotation ?? 0,
|
|
@@ -2884,6 +3493,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2884
3493
|
perfectDrawEnabled: false,
|
|
2885
3494
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
2886
3495
|
});
|
|
3496
|
+
marker.setAttr("seatId", id);
|
|
2887
3497
|
const common = {
|
|
2888
3498
|
stroke: this.effSelection,
|
|
2889
3499
|
listening: false,
|
|
@@ -2960,10 +3570,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2960
3570
|
* whether the section already fills the viewport (small container) so the tap
|
|
2961
3571
|
* must fall through and pick.
|
|
2962
3572
|
*/
|
|
3573
|
+
sectionBounds(id) {
|
|
3574
|
+
const bounds = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => polyBounds(section.outline));
|
|
3575
|
+
if (!bounds.length) return null;
|
|
3576
|
+
const left = Math.min(...bounds.map((box) => box.x));
|
|
3577
|
+
const top = Math.min(...bounds.map((box) => box.y));
|
|
3578
|
+
const right = Math.max(...bounds.map((box) => box.x + box.width));
|
|
3579
|
+
const bottom = Math.max(...bounds.map((box) => box.y + box.height));
|
|
3580
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
3581
|
+
}
|
|
2963
3582
|
sectionFrameScale(id) {
|
|
2964
|
-
const
|
|
2965
|
-
if (!
|
|
2966
|
-
const b = polyBounds(sec.outline);
|
|
3583
|
+
const b = this.sectionBounds(id);
|
|
3584
|
+
if (!b) return this.stage.scaleX();
|
|
2967
3585
|
const w = this.stage.width();
|
|
2968
3586
|
const h = this.stage.height();
|
|
2969
3587
|
const { min, max } = this.zoomBounds();
|
|
@@ -2993,11 +3611,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2993
3611
|
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
2994
3612
|
const sec = this.seatSection.get(id);
|
|
2995
3613
|
if (sec) {
|
|
2996
|
-
const alreadyFocused = this.focusedSectionId === sec.
|
|
2997
|
-
const canZoomInFurther = this.sectionFrameScale(sec.
|
|
3614
|
+
const alreadyFocused = this.focusedSectionId === sec.logicalId;
|
|
3615
|
+
const canZoomInFurther = this.sectionFrameScale(sec.logicalId) > this.stage.scaleX() * 1.02;
|
|
2998
3616
|
if (!alreadyFocused && canZoomInFurther) {
|
|
2999
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.
|
|
3000
|
-
else this.focusSection(sec.
|
|
3617
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.logicalId);
|
|
3618
|
+
else this.focusSection(sec.logicalId);
|
|
3001
3619
|
return;
|
|
3002
3620
|
}
|
|
3003
3621
|
}
|
|
@@ -3076,10 +3694,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3076
3694
|
}
|
|
3077
3695
|
if (this.sections.length) {
|
|
3078
3696
|
const world = this.screenToWorld(pointer);
|
|
3079
|
-
const hit = this.sections.find((sn) =>
|
|
3697
|
+
const hit = this.sections.find((sn) => pointInPolygonWithHoles(world, sn.outline, sn.holes));
|
|
3080
3698
|
if (hit) {
|
|
3081
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.
|
|
3082
|
-
else this.focusRegion(hit.
|
|
3699
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.logicalId);
|
|
3700
|
+
else this.focusRegion(hit.logicalId);
|
|
3083
3701
|
return;
|
|
3084
3702
|
}
|
|
3085
3703
|
}
|
|
@@ -3165,10 +3783,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3165
3783
|
* newer glide cancels an in-flight one.
|
|
3166
3784
|
*/
|
|
3167
3785
|
focusRegion(target, opts) {
|
|
3168
|
-
const b = typeof target === "string" ? (
|
|
3169
|
-
const sec = this.sections.find((s) => s.id === target);
|
|
3170
|
-
return sec ? polyBounds(sec.outline) : null;
|
|
3171
|
-
})() : target;
|
|
3786
|
+
const b = typeof target === "string" ? this.sectionBounds(target) : target;
|
|
3172
3787
|
if (!b) return;
|
|
3173
3788
|
this.cancelGlide();
|
|
3174
3789
|
if (opts?.animate === false || this.reducedMotion) {
|
|
@@ -3228,25 +3843,343 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3228
3843
|
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
3229
3844
|
return "sections";
|
|
3230
3845
|
}
|
|
3231
|
-
|
|
3846
|
+
getRenderedQualityEvidence() {
|
|
3847
|
+
const effectiveScale = this.effScale();
|
|
3848
|
+
const stageScale = this.stage.scaleX();
|
|
3849
|
+
const viewport = { width: this.stage.width(), height: this.stage.height() };
|
|
3850
|
+
const rounded = (value) => Math.round(value * 100) / 100;
|
|
3851
|
+
const labels = this.seats.map((seat) => {
|
|
3852
|
+
const shape = this.circleById.get(seat.id);
|
|
3853
|
+
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
3854
|
+
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE;
|
|
3855
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * effectiveScale);
|
|
3856
|
+
const screen = this.worldToScreen(seat);
|
|
3857
|
+
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
3858
|
+
const opacity = shape?.opacity() ?? 0;
|
|
3859
|
+
const section = this.seatSection.get(seat.id);
|
|
3860
|
+
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
3861
|
+
let hiddenReason;
|
|
3862
|
+
if (!visible) {
|
|
3863
|
+
if (opacity < 0.5) hiddenReason = "dimmed-or-unavailable";
|
|
3864
|
+
else if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
3865
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
3866
|
+
else if (!label) hiddenReason = "clutter-or-fit";
|
|
3867
|
+
else hiddenReason = "renderer-hidden";
|
|
3868
|
+
}
|
|
3869
|
+
const labelWidth = label ? label.width() * stageScale : 0;
|
|
3870
|
+
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
3871
|
+
const directWidthPx = shape instanceof Rect ? shape.width() * stageScale : this.seatR * 2 * effectiveScale;
|
|
3872
|
+
const directHeightPx = shape instanceof Rect ? shape.height() * stageScale : this.seatR * 2 * effectiveScale;
|
|
3873
|
+
const assistedDiameterPx = 2 * (this.seatR * effectiveScale + SEAT_TAP_SLOP_PX);
|
|
3874
|
+
const fill = shape?.fill();
|
|
3875
|
+
const ink = label?.fill();
|
|
3876
|
+
return {
|
|
3877
|
+
seatId: seat.id,
|
|
3878
|
+
label: seat.label,
|
|
3879
|
+
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
3880
|
+
categoryKey: seat.categoryKey,
|
|
3881
|
+
...section ? { sectionId: section.id } : {},
|
|
3882
|
+
...section?.zone ? { zoneId: section.zone } : {},
|
|
3883
|
+
status: this.statusById.get(seat.id) ?? "free",
|
|
3884
|
+
selected: this.selection.has(seat.id),
|
|
3885
|
+
visible,
|
|
3886
|
+
renderedFontPx,
|
|
3887
|
+
fill: typeof fill === "string" ? fill : "",
|
|
3888
|
+
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3889
|
+
opacity: rounded(opacity),
|
|
3890
|
+
pointerTarget: {
|
|
3891
|
+
active: !this.cached && this.isSelectable(seat.id),
|
|
3892
|
+
directWidthPx: rounded(directWidthPx),
|
|
3893
|
+
directHeightPx: rounded(directHeightPx),
|
|
3894
|
+
effectiveMinimumPx: rounded(Math.max(
|
|
3895
|
+
Math.min(directWidthPx, directHeightPx),
|
|
3896
|
+
assistedDiameterPx
|
|
3897
|
+
))
|
|
3898
|
+
},
|
|
3899
|
+
screenCenter: { x: rounded(screen.x), y: rounded(screen.y) },
|
|
3900
|
+
...visible ? {
|
|
3901
|
+
screenBox: {
|
|
3902
|
+
x: rounded(screen.x - labelWidth / 2),
|
|
3903
|
+
y: rounded(screen.y - labelHeight / 2),
|
|
3904
|
+
width: rounded(labelWidth),
|
|
3905
|
+
height: rounded(labelHeight)
|
|
3906
|
+
}
|
|
3907
|
+
} : {},
|
|
3908
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
3909
|
+
};
|
|
3910
|
+
});
|
|
3911
|
+
const visibleLabels = labels.filter((label) => label.visible).length;
|
|
3912
|
+
const hierarchyEvidence = (id, kind, role, node, backgroundFill, section) => {
|
|
3913
|
+
const worldCorners = rotatedRectPoints(
|
|
3914
|
+
{ x: node.x(), y: node.y() },
|
|
3915
|
+
node.width(),
|
|
3916
|
+
node.height(),
|
|
3917
|
+
node.rotation()
|
|
3918
|
+
);
|
|
3919
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen(corner)));
|
|
3920
|
+
const opacity = rounded(node.opacity());
|
|
3921
|
+
const ink = node.fill();
|
|
3922
|
+
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
3923
|
+
const visible = node.isVisible() && opacity > 0.05 && !outside;
|
|
3924
|
+
const fitsContainer = section ? rotatedRectFitsPolygon(
|
|
3925
|
+
{ x: node.x(), y: node.y() },
|
|
3926
|
+
node.width(),
|
|
3927
|
+
node.height(),
|
|
3928
|
+
node.rotation(),
|
|
3929
|
+
section.outline,
|
|
3930
|
+
section.holes
|
|
3931
|
+
) : void 0;
|
|
3932
|
+
return {
|
|
3933
|
+
id,
|
|
3934
|
+
kind,
|
|
3935
|
+
role,
|
|
3936
|
+
label: node.text(),
|
|
3937
|
+
visible,
|
|
3938
|
+
renderedFontPx: rounded(node.fontSize() * stageScale),
|
|
3939
|
+
opacity,
|
|
3940
|
+
fill: backgroundFill,
|
|
3941
|
+
ink: typeof ink === "string" ? ink : "",
|
|
3942
|
+
...fitsContainer == null ? {} : { fitsContainer },
|
|
3943
|
+
...visible ? {
|
|
3944
|
+
screenBox: {
|
|
3945
|
+
x: rounded(screenBounds.x),
|
|
3946
|
+
y: rounded(screenBounds.y),
|
|
3947
|
+
width: rounded(screenBounds.width),
|
|
3948
|
+
height: rounded(screenBounds.height)
|
|
3949
|
+
}
|
|
3950
|
+
} : {}
|
|
3951
|
+
};
|
|
3952
|
+
};
|
|
3953
|
+
const hierarchyLabels = [
|
|
3954
|
+
...this.sections.map((section) => {
|
|
3955
|
+
const fill = section.blockPoly.fill();
|
|
3956
|
+
return hierarchyEvidence(
|
|
3957
|
+
section.id,
|
|
3958
|
+
"section",
|
|
3959
|
+
"name",
|
|
3960
|
+
section.nameLabel,
|
|
3961
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
3962
|
+
section
|
|
3963
|
+
);
|
|
3964
|
+
}),
|
|
3965
|
+
...this.sections.map((section) => {
|
|
3966
|
+
const fill = section.blockPoly.fill();
|
|
3967
|
+
return hierarchyEvidence(
|
|
3968
|
+
`${section.id}:availability`,
|
|
3969
|
+
"section",
|
|
3970
|
+
"availability",
|
|
3971
|
+
section.subLabel,
|
|
3972
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
3973
|
+
section
|
|
3974
|
+
);
|
|
3975
|
+
}),
|
|
3976
|
+
...this.zones.flatMap((zone) => [
|
|
3977
|
+
hierarchyEvidence(zone.id, "zone", "name", zone.label, zone.background),
|
|
3978
|
+
...zone.sub ? [hierarchyEvidence(`${zone.id}:price`, "zone", "price", zone.sub, zone.background)] : []
|
|
3979
|
+
])
|
|
3980
|
+
];
|
|
3981
|
+
const gaAreas = [...this.gaById].map(([areaId, ga]) => {
|
|
3982
|
+
const screenPoints = ga.points.map((point) => this.worldToScreen(point));
|
|
3983
|
+
const left = Math.min(...screenPoints.map((point) => point.x));
|
|
3984
|
+
const top = Math.min(...screenPoints.map((point) => point.y));
|
|
3985
|
+
const right = Math.max(...screenPoints.map((point) => point.x));
|
|
3986
|
+
const bottom = Math.max(...screenPoints.map((point) => point.y));
|
|
3987
|
+
const outside = right < 0 || left > viewport.width || bottom < 0 || top > viewport.height;
|
|
3988
|
+
const opacity = rounded(ga.polygon.opacity());
|
|
3989
|
+
const visible = opacity >= 0.1 && !outside;
|
|
3990
|
+
const fill = ga.polygon.fill();
|
|
3991
|
+
return {
|
|
3992
|
+
areaId,
|
|
3993
|
+
label: ga.label,
|
|
3994
|
+
capacity: ga.capacity,
|
|
3995
|
+
categoryKey: ga.categoryKey,
|
|
3996
|
+
...ga.sectionId ? { sectionId: ga.sectionId } : {},
|
|
3997
|
+
visible,
|
|
3998
|
+
interactive: ga.polygon.listening(),
|
|
3999
|
+
opacity,
|
|
4000
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4001
|
+
effectiveBackground: ga.effectiveBackground,
|
|
4002
|
+
...visible ? {
|
|
4003
|
+
screenBox: {
|
|
4004
|
+
x: rounded(left),
|
|
4005
|
+
y: rounded(top),
|
|
4006
|
+
width: rounded(right - left),
|
|
4007
|
+
height: rounded(bottom - top)
|
|
4008
|
+
}
|
|
4009
|
+
} : {}
|
|
4010
|
+
};
|
|
4011
|
+
});
|
|
4012
|
+
const freeTextLabels = [...this.freeTextById].map(([recordKey, record]) => {
|
|
4013
|
+
const { node, background, kind } = record;
|
|
4014
|
+
const point = this.worldToScreen({ x: node.x(), y: node.y() });
|
|
4015
|
+
const width = node.width() * stageScale;
|
|
4016
|
+
const height = node.height() * effectiveScale;
|
|
4017
|
+
const left = point.x - node.offsetX() * stageScale;
|
|
4018
|
+
const top = point.y - node.offsetY() * effectiveScale;
|
|
4019
|
+
const renderedFontPx = rounded(node.fontSize() * effectiveScale);
|
|
4020
|
+
const outside = left + width < 0 || left > viewport.width || top + height < 0 || top > viewport.height;
|
|
4021
|
+
const visible = node.isVisible() && !outside;
|
|
4022
|
+
const ink = node.fill();
|
|
4023
|
+
const opacity = rounded(node.getAbsoluteOpacity());
|
|
4024
|
+
let hiddenReason;
|
|
4025
|
+
if (!visible) {
|
|
4026
|
+
if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4027
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4028
|
+
else hiddenReason = "renderer-hidden";
|
|
4029
|
+
}
|
|
4030
|
+
return {
|
|
4031
|
+
objectId: record.objectId ?? recordKey,
|
|
4032
|
+
kind,
|
|
4033
|
+
text: node.text(),
|
|
4034
|
+
visible,
|
|
4035
|
+
renderedFontPx,
|
|
4036
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4037
|
+
background,
|
|
4038
|
+
opacity,
|
|
4039
|
+
...visible ? {
|
|
4040
|
+
screenBox: {
|
|
4041
|
+
x: rounded(left),
|
|
4042
|
+
y: rounded(top),
|
|
4043
|
+
width: rounded(width),
|
|
4044
|
+
height: rounded(height)
|
|
4045
|
+
}
|
|
4046
|
+
} : {},
|
|
4047
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4048
|
+
};
|
|
4049
|
+
});
|
|
4050
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
4051
|
+
const neutralSectionFills = /* @__PURE__ */ new Set([
|
|
4052
|
+
palette.sectionFill.toLowerCase(),
|
|
4053
|
+
darken(palette.sectionFill, 0.12).toLowerCase()
|
|
4054
|
+
]);
|
|
4055
|
+
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4056
|
+
return {
|
|
4057
|
+
viewport,
|
|
4058
|
+
canvasBackground: this.canvasBackground,
|
|
4059
|
+
effectiveScale: rounded(effectiveScale),
|
|
4060
|
+
rung: this.getRung(),
|
|
4061
|
+
minimumVisibleLabelPx: MIN_VISIBLE_BOOKABLE_LABEL_PX,
|
|
4062
|
+
totalLabelledBookableUnits: labels.length,
|
|
4063
|
+
visibleLabels,
|
|
4064
|
+
hiddenLabels: labels.length - visibleLabels,
|
|
4065
|
+
totalBookableUnits: labels.length + gaAreas.reduce((sum, area) => sum + area.capacity, 0),
|
|
4066
|
+
selectionRingSeatIds: this.overlayLayer.find(".selection-ring").map((node) => String(node.getAttr("seatId") ?? "")).filter(Boolean),
|
|
4067
|
+
selectionRingColor: this.effSelection,
|
|
4068
|
+
focusedSectionId: this.focusedSectionId,
|
|
4069
|
+
focusBackdropVisible: Boolean(this.focusBackdrop?.isVisible()),
|
|
4070
|
+
categoryFilterKeys: this.categoryFilter ? [...this.categoryFilter].sort() : null,
|
|
4071
|
+
overviewStyle: {
|
|
4072
|
+
visibleSectionShells: visibleSectionShells.length,
|
|
4073
|
+
categoryPaintedSectionShells: visibleSectionShells.filter((section) => {
|
|
4074
|
+
const fill = section.blockPoly.fill();
|
|
4075
|
+
return typeof fill !== "string" || !neutralSectionFills.has(fill.toLowerCase());
|
|
4076
|
+
}).length,
|
|
4077
|
+
visibleCategoryDetailOutlines: this.sections.filter((section) => section.outlinePoly.opacity() > 0.05).length,
|
|
4078
|
+
// Row-hint nodes no longer exist in the production overview scene.
|
|
4079
|
+
visibleSectionRowHints: 0,
|
|
4080
|
+
visibleSectionAvailabilityLabels: this.sections.filter((section) => section.subLabel.opacity() > 0.05).length,
|
|
4081
|
+
visibleSectionGADetails: [...this.gaById.values()].filter((area) => area.sectionId != null && area.polygon.opacity() > 0.05).length
|
|
4082
|
+
},
|
|
4083
|
+
labels,
|
|
4084
|
+
gaAreas,
|
|
4085
|
+
hierarchyLabels,
|
|
4086
|
+
freeTextLabels
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
/** Jump the camera to a rung's zoom band (glided). */
|
|
3232
4090
|
setRung(rung) {
|
|
3233
4091
|
if (rung === "zones") {
|
|
3234
4092
|
this.cancelGlide();
|
|
3235
4093
|
this.zoomToFit();
|
|
3236
4094
|
return;
|
|
3237
4095
|
}
|
|
3238
|
-
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4096
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4097
|
+
this.seatLabelTargetScale() * 1.05,
|
|
4098
|
+
SEAT_FOCUS_SCALE,
|
|
4099
|
+
CACHE_THRESHOLD * 1.3
|
|
4100
|
+
);
|
|
3239
4101
|
const w = this.stage.width();
|
|
3240
4102
|
const h = this.stage.height();
|
|
3241
|
-
const
|
|
3242
|
-
const
|
|
4103
|
+
const visible = this.getVisibleWorldRect();
|
|
4104
|
+
const viewCentre = {
|
|
4105
|
+
x: visible.x + visible.width / 2,
|
|
4106
|
+
y: visible.y + visible.height / 2
|
|
4107
|
+
};
|
|
4108
|
+
let cx = viewCentre.x;
|
|
4109
|
+
let cy = viewCentre.y;
|
|
4110
|
+
if (rung === "sections" && this.sections.length > 0) {
|
|
4111
|
+
const sectionCentres = this.sections.map((section) => {
|
|
4112
|
+
const bounds = polyBounds(section.outline);
|
|
4113
|
+
return {
|
|
4114
|
+
x: bounds.x + bounds.width / 2,
|
|
4115
|
+
y: bounds.y + bounds.height / 2
|
|
4116
|
+
};
|
|
4117
|
+
});
|
|
4118
|
+
const halfWidth = w / (target * 2);
|
|
4119
|
+
const halfHeight = h / (target * 2);
|
|
4120
|
+
const hierarchyWillBeVisible = sectionCentres.some((point) => Math.abs(point.x - viewCentre.x) <= halfWidth && Math.abs(point.y - viewCentre.y) <= halfHeight);
|
|
4121
|
+
if (!hierarchyWillBeVisible) {
|
|
4122
|
+
const nearest = sectionCentres.reduce((best, point) => {
|
|
4123
|
+
const distance = (point.x - viewCentre.x) ** 2 + (point.y - viewCentre.y) ** 2;
|
|
4124
|
+
return distance < best.distance ? { point, distance } : best;
|
|
4125
|
+
}, { point: sectionCentres[0], distance: Infinity });
|
|
4126
|
+
cx = nearest.point.x;
|
|
4127
|
+
cy = nearest.point.y;
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
const seatAnchors = rung === "seats" ? this.seats.filter((seat) => seat.kind !== "booth") : [];
|
|
4131
|
+
if (seatAnchors.length > 0) {
|
|
4132
|
+
let nearest = seatAnchors[0];
|
|
4133
|
+
let nearestDistance = Infinity;
|
|
4134
|
+
for (const seat of seatAnchors) {
|
|
4135
|
+
const dx = seat.x - viewCentre.x;
|
|
4136
|
+
const dy = seat.y - viewCentre.y;
|
|
4137
|
+
const distance = dx * dx + dy * dy;
|
|
4138
|
+
if (distance < nearestDistance) {
|
|
4139
|
+
nearest = seat;
|
|
4140
|
+
nearestDistance = distance;
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
cx = nearest.x;
|
|
4144
|
+
cy = nearest.y;
|
|
4145
|
+
}
|
|
3243
4146
|
const bw = w / (target * 1.12);
|
|
3244
4147
|
const bh = h / (target * 1.12);
|
|
3245
4148
|
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
3246
4149
|
}
|
|
4150
|
+
/**
|
|
4151
|
+
* The seat rung must account for labels that auto-fit inside a seat circle.
|
|
4152
|
+
* A short `A-1` remains at the normal 7u target; a table label such as
|
|
4153
|
+
* `T13-10` may fit at 4u and therefore needs a deeper camera target to reach
|
|
4154
|
+
* the same 12 CSS-pixel floor. Measurement happens only on explicit rung
|
|
4155
|
+
* navigation, never during pan/zoom frames.
|
|
4156
|
+
*/
|
|
4157
|
+
seatLabelTargetScale() {
|
|
4158
|
+
let minimumFont = BOOTH_LABEL_FONT_SIZE;
|
|
4159
|
+
const measure = new Text({
|
|
4160
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
4161
|
+
fontStyle: "600",
|
|
4162
|
+
fontFamily: this.labelFont(),
|
|
4163
|
+
listening: false
|
|
4164
|
+
});
|
|
4165
|
+
const maxWidth = this.seatR * 2 - 3;
|
|
4166
|
+
for (const seat of this.seats) {
|
|
4167
|
+
if (seat.kind === "booth") {
|
|
4168
|
+
minimumFont = Math.min(minimumFont, BOOTH_LABEL_FONT_SIZE);
|
|
4169
|
+
continue;
|
|
4170
|
+
}
|
|
4171
|
+
measure.fontSize(SEAT_LABEL_FONT_SIZE);
|
|
4172
|
+
measure.text(bookableMarkerLabel(seat.label));
|
|
4173
|
+
const fitted = measure.width() > maxWidth ? Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxWidth / measure.width()) : SEAT_LABEL_FONT_SIZE;
|
|
4174
|
+
minimumFont = Math.min(minimumFont, fitted);
|
|
4175
|
+
}
|
|
4176
|
+
measure.destroy();
|
|
4177
|
+
return MIN_VISIBLE_BOOKABLE_LABEL_PX / Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, minimumFont);
|
|
4178
|
+
}
|
|
3247
4179
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
3248
4180
|
afterViewChange() {
|
|
3249
4181
|
this.updateLOD();
|
|
4182
|
+
this.updateFreeTextVisibility();
|
|
3250
4183
|
this.updateLabels();
|
|
3251
4184
|
this.scheduleViewChange();
|
|
3252
4185
|
}
|
|
@@ -3260,7 +4193,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3260
4193
|
}
|
|
3261
4194
|
updateLOD() {
|
|
3262
4195
|
const scale = this.effScale();
|
|
4196
|
+
const focalScale = Math.max(scale, 1e-4);
|
|
4197
|
+
for (const [label, targetPx] of this.primaryFocalLabels) {
|
|
4198
|
+
this.sizeLabel(label, targetPx / focalScale, label.y());
|
|
4199
|
+
}
|
|
3263
4200
|
if (this.hasSections) this.applySectionLod(scale);
|
|
4201
|
+
else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
|
|
4202
|
+
this.paintGAStateForView();
|
|
3264
4203
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
3265
4204
|
if (shouldCache && !this.cached) {
|
|
3266
4205
|
this.cacheSeatLayer();
|
|
@@ -3299,15 +4238,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3299
4238
|
if (this.recacheTimer) {
|
|
3300
4239
|
clearTimeout(this.recacheTimer);
|
|
3301
4240
|
this.recacheTimer = null;
|
|
3302
|
-
this.
|
|
4241
|
+
if (this.effScale() < CACHE_THRESHOLD) {
|
|
4242
|
+
this.rebuildSeatCache();
|
|
4243
|
+
} else if (this.cached) {
|
|
4244
|
+
this.seatLayer.clearCache();
|
|
4245
|
+
this.seatLayer.listening(true);
|
|
4246
|
+
this.cached = false;
|
|
4247
|
+
}
|
|
3303
4248
|
}
|
|
3304
4249
|
this.bgLayer.draw();
|
|
3305
4250
|
this.seatLayer.draw();
|
|
3306
4251
|
this.overlayLayer.draw();
|
|
3307
4252
|
}
|
|
4253
|
+
updateFreeTextVisibility() {
|
|
4254
|
+
const effectiveScale = this.effScale();
|
|
4255
|
+
for (const { objectId, node, categoryKey, kind } of this.freeTextById.values()) {
|
|
4256
|
+
const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
|
|
4257
|
+
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
4258
|
+
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
3308
4261
|
updateLabels() {
|
|
3309
|
-
const
|
|
4262
|
+
const effectiveScale = this.effScale();
|
|
4263
|
+
const show = effectiveScale >= LABEL_SCALE;
|
|
4264
|
+
for (const [id, label] of this.boothLabelById) {
|
|
4265
|
+
const shape = this.circleById.get(id);
|
|
4266
|
+
label.visible(
|
|
4267
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.opacity() ?? 1) >= 0.5
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
3310
4270
|
this.labelGroup.destroyChildren();
|
|
4271
|
+
this.seatLabelById.clear();
|
|
3311
4272
|
if (!show) {
|
|
3312
4273
|
this.overlayLayer.batchDraw();
|
|
3313
4274
|
return;
|
|
@@ -3321,8 +4282,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3321
4282
|
for (const seat of this.seats) {
|
|
3322
4283
|
if (seat.kind === "booth") continue;
|
|
3323
4284
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
4285
|
+
const shape = this.circleById.get(seat.id);
|
|
4286
|
+
if ((shape?.opacity() ?? 1) < 0.5) continue;
|
|
3324
4287
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
3325
|
-
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id);
|
|
4288
|
+
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
3326
4289
|
if (unavailable) {
|
|
3327
4290
|
const cue = new Group({ x: seat.x, y: seat.y, listening: false });
|
|
3328
4291
|
if (status === "held") {
|
|
@@ -3360,23 +4323,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3360
4323
|
const t2 = new Text({
|
|
3361
4324
|
x: seat.x,
|
|
3362
4325
|
y: seat.y,
|
|
3363
|
-
text: seat.label,
|
|
3364
|
-
fontSize:
|
|
4326
|
+
text: bookableMarkerLabel(seat.label),
|
|
4327
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
3365
4328
|
fontStyle: "600",
|
|
3366
4329
|
fontFamily: this.labelFont(),
|
|
3367
|
-
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4330
|
+
fill: shape ? this.renderedBookableLabelInk(seat.id, shape) : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3368
4331
|
listening: false,
|
|
3369
4332
|
perfectDrawEnabled: false
|
|
3370
4333
|
});
|
|
3371
4334
|
const maxW = this.seatR * 2 - 3;
|
|
3372
|
-
if (t2.width() > maxW) t2.fontSize(Math.max(
|
|
3373
|
-
if (t2.
|
|
4335
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxW / t2.width()));
|
|
4336
|
+
if (t2.width() > maxW + 0.01) {
|
|
4337
|
+
t2.destroy();
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4340
|
+
if (!isBookableLabelLegibleAtScale(t2.fontSize(), effectiveScale)) {
|
|
3374
4341
|
t2.destroy();
|
|
3375
4342
|
continue;
|
|
3376
4343
|
}
|
|
3377
4344
|
t2.offsetX(t2.width() / 2);
|
|
3378
4345
|
t2.offsetY(t2.height() / 2);
|
|
3379
4346
|
this.labelGroup.add(t2);
|
|
4347
|
+
this.seatLabelById.set(seat.id, t2);
|
|
3380
4348
|
if (++count >= MAX_LABELS) break;
|
|
3381
4349
|
}
|
|
3382
4350
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -3588,6 +4556,7 @@ var PickerController = class {
|
|
|
3588
4556
|
this.opts.onDeselect?.(seat);
|
|
3589
4557
|
this.emitSelectionChange();
|
|
3590
4558
|
},
|
|
4559
|
+
onSelectionLimit: this.opts.onSelectionLimit,
|
|
3591
4560
|
onHover: (seat) => {
|
|
3592
4561
|
this.opts.onHover?.(seat);
|
|
3593
4562
|
if (this.opts.onSeatHover) this.opts.onSeatHover(seat ? this.describeSeat(seat) : null);
|
|
@@ -3655,6 +4624,15 @@ var PickerController = class {
|
|
|
3655
4624
|
this.renderer?.deselect(ids);
|
|
3656
4625
|
this.emitSelectionChange();
|
|
3657
4626
|
}
|
|
4627
|
+
setMaxSelection(maxSelection) {
|
|
4628
|
+
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
4629
|
+
this.renderer?.setMaxSelection?.(this.maxSelection);
|
|
4630
|
+
}
|
|
4631
|
+
select(ids) {
|
|
4632
|
+
const added = this.renderer?.select?.(ids) ?? [];
|
|
4633
|
+
if (added.length) this.emitSelectionChange();
|
|
4634
|
+
return added.map((seat) => this.toSeat(seat));
|
|
4635
|
+
}
|
|
3658
4636
|
// ---- booking machine ------------------------------------------------------
|
|
3659
4637
|
/**
|
|
3660
4638
|
* Hold the current selection (or a given label set). The controller does the
|
|
@@ -4618,9 +5596,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
4618
5596
|
|
|
4619
5597
|
// src/i18n/bundles.ts
|
|
4620
5598
|
var LOADERS = {
|
|
4621
|
-
es: () => import("./es-
|
|
4622
|
-
de: () => import("./de-
|
|
4623
|
-
fr: () => import("./fr-
|
|
5599
|
+
es: () => import("./es-NLD3NL3W.js").then((m) => ({ default: m.es })),
|
|
5600
|
+
de: () => import("./de-ZTKJOFHT.js").then((m) => ({ default: m.de })),
|
|
5601
|
+
fr: () => import("./fr-KSFKWSAT.js").then((m) => ({ default: m.fr }))
|
|
4624
5602
|
};
|
|
4625
5603
|
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
4626
5604
|
async function loadLocale(code) {
|
|
@@ -4678,6 +5656,8 @@ export {
|
|
|
4678
5656
|
loadLocale,
|
|
4679
5657
|
objectCenter,
|
|
4680
5658
|
pointInPolygon,
|
|
5659
|
+
pointInPolygonWithHoles,
|
|
5660
|
+
polygonLabelPoint,
|
|
4681
5661
|
resolveLocale,
|
|
4682
5662
|
setLocale,
|
|
4683
5663
|
setMoneyLocale,
|