@seatlayer/core 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{de-PQ5BE3PL.js → de-ZTKJOFHT.js} +3 -1
- package/dist/de-ZTKJOFHT.js.map +1 -0
- package/dist/{es-EWHL5CZR.js → es-NLD3NL3W.js} +3 -1
- package/dist/es-NLD3NL3W.js.map +1 -0
- package/dist/{fr-FIEG227H.js → fr-KSFKWSAT.js} +3 -1
- package/dist/fr-KSFKWSAT.js.map +1 -0
- package/dist/index.cjs +1266 -279
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -31
- package/dist/index.d.ts +496 -31
- package/dist/index.js +1257 -282
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-PQ5BE3PL.js.map +0 -1
- package/dist/es-EWHL5CZR.js.map +0 -1
- package/dist/fr-FIEG227H.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,8 @@ var init_es = __esm({
|
|
|
56
56
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
57
57
|
"picker.language": "Idioma",
|
|
58
58
|
"picker.zoomToFit": "Ajustar zoom",
|
|
59
|
+
"picker.seatCountLabel": "asientos",
|
|
60
|
+
"picker.capacity": "capacidad",
|
|
59
61
|
"picker.viewMode": "Modo de vista",
|
|
60
62
|
"picker.floor": "Piso",
|
|
61
63
|
"picker.zoomLevel": "Nivel de zoom",
|
|
@@ -138,6 +140,8 @@ var init_de = __esm({
|
|
|
138
140
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
139
141
|
"picker.language": "Sprache",
|
|
140
142
|
"picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
|
|
143
|
+
"picker.seatCountLabel": "Pl\xE4tze",
|
|
144
|
+
"picker.capacity": "Kapazit\xE4t",
|
|
141
145
|
"picker.viewMode": "Ansichtsmodus",
|
|
142
146
|
"picker.floor": "Etage",
|
|
143
147
|
"picker.zoomLevel": "Zoomstufe",
|
|
@@ -220,6 +224,8 @@ var init_fr = __esm({
|
|
|
220
224
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
221
225
|
"picker.language": "Langue",
|
|
222
226
|
"picker.zoomToFit": "Ajuster le zoom",
|
|
227
|
+
"picker.seatCountLabel": "si\xE8ges",
|
|
228
|
+
"picker.capacity": "capacit\xE9",
|
|
223
229
|
"picker.viewMode": "Mode d'affichage",
|
|
224
230
|
"picker.floor": "\xC9tage",
|
|
225
231
|
"picker.zoomLevel": "Niveau de zoom",
|
|
@@ -269,9 +275,12 @@ var init_fr = __esm({
|
|
|
269
275
|
// src/index.ts
|
|
270
276
|
var index_exports = {};
|
|
271
277
|
__export(index_exports, {
|
|
278
|
+
ACCESSIBILITY_RING_COLOR: () => ACCESSIBILITY_RING_COLOR,
|
|
272
279
|
ACCESSIBILITY_TYPES: () => ACCESSIBILITY_TYPES,
|
|
273
280
|
CHART_STORAGE_KEY: () => CHART_STORAGE_KEY,
|
|
274
281
|
DEFAULT_CURRENCY: () => DEFAULT_CURRENCY,
|
|
282
|
+
LABEL_STYLE_MAX_SIZE: () => LABEL_STYLE_MAX_SIZE,
|
|
283
|
+
LABEL_STYLE_MIN_SIZE: () => LABEL_STYLE_MIN_SIZE,
|
|
275
284
|
MAX_EVENT_INVENTORY: () => MAX_EVENT_INVENTORY,
|
|
276
285
|
MAX_GA_CAPACITY: () => MAX_GA_CAPACITY,
|
|
277
286
|
PickerController: () => PickerController,
|
|
@@ -279,6 +288,7 @@ __export(index_exports, {
|
|
|
279
288
|
SeatmapRenderer: () => SeatmapRenderer,
|
|
280
289
|
UNGROUPED_ID: () => UNGROUPED_ID,
|
|
281
290
|
accessibilityMeta: () => accessibilityMeta,
|
|
291
|
+
accessibilityRingColor: () => accessibilityRingColor,
|
|
282
292
|
allObjects: () => allObjects,
|
|
283
293
|
applyHidden: () => applyHidden,
|
|
284
294
|
chartBounds: () => chartBounds,
|
|
@@ -307,6 +317,8 @@ __export(index_exports, {
|
|
|
307
317
|
loadLocale: () => loadLocale,
|
|
308
318
|
objectCenter: () => objectCenter,
|
|
309
319
|
pointInPolygon: () => pointInPolygon,
|
|
320
|
+
pointInPolygonWithHoles: () => pointInPolygonWithHoles,
|
|
321
|
+
polygonLabelPoint: () => polygonLabelPoint,
|
|
310
322
|
resolveLocale: () => resolveLocale,
|
|
311
323
|
setLocale: () => setLocale,
|
|
312
324
|
setMoneyLocale: () => setMoneyLocale,
|
|
@@ -331,6 +343,21 @@ var ACCESSIBILITY_LABEL = new Map(ACCESSIBILITY_TYPES.map((a) => [a.key, a]));
|
|
|
331
343
|
function accessibilityMeta(key) {
|
|
332
344
|
return ACCESSIBILITY_LABEL.get(key);
|
|
333
345
|
}
|
|
346
|
+
var ACCESSIBILITY_RING_COLOR = {
|
|
347
|
+
wheelchair: "#3b82f6",
|
|
348
|
+
companion: "#8b5cf6",
|
|
349
|
+
"semi-ambulatory": "#0ea5e9",
|
|
350
|
+
hearing: "#14b8a6",
|
|
351
|
+
"sign-language": "#f59e0b",
|
|
352
|
+
"plus-size": "#ec4899",
|
|
353
|
+
"lift-armrest": "#22c55e"
|
|
354
|
+
};
|
|
355
|
+
function accessibilityRingColor(types) {
|
|
356
|
+
const primary = types?.[0];
|
|
357
|
+
return primary && ACCESSIBILITY_RING_COLOR[primary] || "#3b82f6";
|
|
358
|
+
}
|
|
359
|
+
var LABEL_STYLE_MIN_SIZE = 8;
|
|
360
|
+
var LABEL_STYLE_MAX_SIZE = 24;
|
|
334
361
|
function layerOf(obj) {
|
|
335
362
|
switch (obj.type) {
|
|
336
363
|
case "row":
|
|
@@ -350,6 +377,71 @@ function layerOf(obj) {
|
|
|
350
377
|
}
|
|
351
378
|
var CHART_STORAGE_KEY = "seatmap.chart";
|
|
352
379
|
|
|
380
|
+
// src/core/complexGeometry.ts
|
|
381
|
+
function cubicPoint(path, t2) {
|
|
382
|
+
const u = 1 - t2;
|
|
383
|
+
const a = u * u * u;
|
|
384
|
+
const b = 3 * u * u * t2;
|
|
385
|
+
const c = 3 * u * t2 * t2;
|
|
386
|
+
const d = t2 * t2 * t2;
|
|
387
|
+
return {
|
|
388
|
+
x: a * path.start.x + b * path.control1.x + c * path.control2.x + d * path.end.x,
|
|
389
|
+
y: a * path.start.y + b * path.control1.y + c * path.control2.y + d * path.end.y
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function distributeAlongCubic(path, count, resolution = 192) {
|
|
393
|
+
if (!Number.isInteger(count) || count < 1) throw new Error("Path point count must be a positive integer");
|
|
394
|
+
if (count === 1) return [cubicPoint(path, 0.5)];
|
|
395
|
+
const samples = Array.from({ length: resolution + 1 }, (_, index) => cubicPoint(path, index / resolution));
|
|
396
|
+
const lengths = new Float64Array(samples.length);
|
|
397
|
+
for (let index = 1; index < samples.length; index += 1) {
|
|
398
|
+
const dx = samples[index].x - samples[index - 1].x;
|
|
399
|
+
const dy = samples[index].y - samples[index - 1].y;
|
|
400
|
+
lengths[index] = lengths[index - 1] + Math.hypot(dx, dy);
|
|
401
|
+
}
|
|
402
|
+
const total = lengths[lengths.length - 1];
|
|
403
|
+
if (total <= 1e-9) return Array.from({ length: count }, () => ({ ...path.start }));
|
|
404
|
+
const output = [];
|
|
405
|
+
let segment = 1;
|
|
406
|
+
for (let index = 0; index < count; index += 1) {
|
|
407
|
+
const target = total * index / (count - 1);
|
|
408
|
+
while (segment < lengths.length - 1 && lengths[segment] < target) segment += 1;
|
|
409
|
+
const before = lengths[segment - 1];
|
|
410
|
+
const after = lengths[segment];
|
|
411
|
+
const ratio = after === before ? 0 : (target - before) / (after - before);
|
|
412
|
+
output.push({
|
|
413
|
+
x: samples[segment - 1].x + (samples[segment].x - samples[segment - 1].x) * ratio,
|
|
414
|
+
y: samples[segment - 1].y + (samples[segment].y - samples[segment - 1].y) * ratio
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return output;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/core/sectionPath.ts
|
|
421
|
+
var TAU = Math.PI * 2;
|
|
422
|
+
function translateSectionOutlinePath(path, dx, dy) {
|
|
423
|
+
const translate = (point) => ({ x: point.x + dx, y: point.y + dy });
|
|
424
|
+
return transformSectionOutlinePath(path, translate);
|
|
425
|
+
}
|
|
426
|
+
function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected = false) {
|
|
427
|
+
return {
|
|
428
|
+
...path,
|
|
429
|
+
start: transform(path.start),
|
|
430
|
+
segments: path.segments.map((segment) => segment.kind === "line" ? { ...segment, end: transform(segment.end) } : segment.kind === "arc" ? {
|
|
431
|
+
...segment,
|
|
432
|
+
center: transform(segment.center),
|
|
433
|
+
radius: segment.radius * Math.abs(radiusScale),
|
|
434
|
+
clockwise: reflected ? !segment.clockwise : segment.clockwise,
|
|
435
|
+
end: transform(segment.end)
|
|
436
|
+
} : {
|
|
437
|
+
...segment,
|
|
438
|
+
control1: transform(segment.control1),
|
|
439
|
+
control2: transform(segment.control2),
|
|
440
|
+
end: transform(segment.end)
|
|
441
|
+
})
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
353
445
|
// src/core/layout.ts
|
|
354
446
|
function overrideAccessibility(o) {
|
|
355
447
|
if (!o) return [];
|
|
@@ -371,6 +463,7 @@ function place(lx, ly, deg, origin) {
|
|
|
371
463
|
function rowSeatPositions(row) {
|
|
372
464
|
const { seatCount, seatSpacing, curve, rotation, origin } = row;
|
|
373
465
|
const out = [];
|
|
466
|
+
if (row.path) return distributeAlongCubic(row.path, seatCount);
|
|
374
467
|
if (seatCount <= 1) {
|
|
375
468
|
if (seatCount === 1) out.push({ x: origin.x, y: origin.y });
|
|
376
469
|
return out;
|
|
@@ -411,15 +504,21 @@ function expandRowSlots(row) {
|
|
|
411
504
|
return rowSeatPositions(row).map((p, i) => {
|
|
412
505
|
const o = ov.get(i);
|
|
413
506
|
const accessibility = overrideAccessibility(o);
|
|
507
|
+
const inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
|
|
508
|
+
const displayPrefix = row.displayLabel ?? row.label;
|
|
509
|
+
const commercial = { ...row.commercial, ...o?.commercial };
|
|
414
510
|
return {
|
|
415
511
|
index: i,
|
|
416
512
|
x: p.x + (o?.dx ?? 0),
|
|
417
513
|
y: p.y + (o?.dy ?? 0),
|
|
418
|
-
label:
|
|
514
|
+
label: inventoryLabel,
|
|
515
|
+
displayLabel: o?.displayLabel ?? `${displayPrefix}-${seatNumber(i)}`,
|
|
419
516
|
categoryKey: o?.categoryKey ?? row.categoryKey,
|
|
420
517
|
skipped: !!o?.skip,
|
|
421
518
|
accessible: accessibility.length > 0,
|
|
422
|
-
accessibility
|
|
519
|
+
accessibility,
|
|
520
|
+
commercial: Object.values(commercial).some((value) => value !== void 0 && value !== false && value !== "") ? commercial : void 0,
|
|
521
|
+
viewUrl: o?.viewFromSeatUrl ?? row.viewFromSeatUrl
|
|
423
522
|
};
|
|
424
523
|
});
|
|
425
524
|
}
|
|
@@ -430,13 +529,15 @@ function expandRow(row) {
|
|
|
430
529
|
seats.push({
|
|
431
530
|
id: `${row.id}:${slot.index}`,
|
|
432
531
|
label: slot.label,
|
|
532
|
+
displayLabel: slot.displayLabel === slot.label ? void 0 : slot.displayLabel,
|
|
433
533
|
x: slot.x,
|
|
434
534
|
y: slot.y,
|
|
435
535
|
rowId: row.id,
|
|
436
536
|
categoryKey: slot.categoryKey,
|
|
437
537
|
accessible: slot.accessible || void 0,
|
|
438
538
|
accessibility: slot.accessibility.length ? slot.accessibility : void 0,
|
|
439
|
-
|
|
539
|
+
commercial: slot.commercial,
|
|
540
|
+
viewUrl: slot.viewUrl
|
|
440
541
|
});
|
|
441
542
|
}
|
|
442
543
|
return seats;
|
|
@@ -533,6 +634,53 @@ function pointInPolygon(p, poly) {
|
|
|
533
634
|
}
|
|
534
635
|
return inside;
|
|
535
636
|
}
|
|
637
|
+
function pointOnPolygonBoundary(p, poly) {
|
|
638
|
+
return poly.some((start, index) => {
|
|
639
|
+
const end = poly[(index + 1) % poly.length];
|
|
640
|
+
const cross = (p.y - start.y) * (end.x - start.x) - (p.x - start.x) * (end.y - start.y);
|
|
641
|
+
if (Math.abs(cross) > 1e-7) return false;
|
|
642
|
+
const dot = (p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y);
|
|
643
|
+
const lengthSquared = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
644
|
+
return dot >= -1e-7 && dot <= lengthSquared + 1e-7;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function pointInPolygonWithHoles(p, outer, holes) {
|
|
648
|
+
return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
|
|
649
|
+
}
|
|
650
|
+
function polygonLabelPoint(outer, holes) {
|
|
651
|
+
if (!outer.length) return { x: 0, y: 0 };
|
|
652
|
+
const xs = outer.map((point) => point.x);
|
|
653
|
+
const ys = outer.map((point) => point.y);
|
|
654
|
+
const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
655
|
+
const centroid = polygonCentroid(outer);
|
|
656
|
+
if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
|
|
657
|
+
let best = outer[0];
|
|
658
|
+
let bestScore = -Infinity;
|
|
659
|
+
const rings = [outer, ...holes ?? []];
|
|
660
|
+
for (let row = 1; row < 24; row += 1) {
|
|
661
|
+
for (let column = 1; column < 24; column += 1) {
|
|
662
|
+
const point = {
|
|
663
|
+
x: bounds.minX + (bounds.maxX - bounds.minX) * column / 24,
|
|
664
|
+
y: bounds.minY + (bounds.maxY - bounds.minY) * row / 24
|
|
665
|
+
};
|
|
666
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
667
|
+
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
668
|
+
const end = ring[(index + 1) % ring.length];
|
|
669
|
+
const dx = end.x - start.x;
|
|
670
|
+
const dy = end.y - start.y;
|
|
671
|
+
const denominator = dx * dx + dy * dy;
|
|
672
|
+
const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
|
|
673
|
+
const t2 = Math.max(0, Math.min(1, projection));
|
|
674
|
+
return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
|
|
675
|
+
})));
|
|
676
|
+
if (score > bestScore) {
|
|
677
|
+
best = point;
|
|
678
|
+
bestScore = score;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return best;
|
|
683
|
+
}
|
|
536
684
|
function polygonCentroid(pts) {
|
|
537
685
|
if (!pts.length) return { x: 0, y: 0 };
|
|
538
686
|
let x = 0;
|
|
@@ -596,9 +744,14 @@ function translateObject(o, dx, dy) {
|
|
|
596
744
|
case "booth":
|
|
597
745
|
return { ...o, center: p(o.center) };
|
|
598
746
|
case "gaArea":
|
|
599
|
-
return { ...o, points: pts(o.points) };
|
|
747
|
+
return { ...o, points: pts(o.points), ...o.holes ? { holes: o.holes.map(pts) } : {} };
|
|
600
748
|
case "section":
|
|
601
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
...o,
|
|
751
|
+
outline: pts(o.outline),
|
|
752
|
+
...o.outlinePath ? { outlinePath: translateSectionOutlinePath(o.outlinePath, dx, dy) } : {},
|
|
753
|
+
...o.holes ? { holes: o.holes.map(pts) } : {}
|
|
754
|
+
};
|
|
602
755
|
case "text":
|
|
603
756
|
return { ...o, position: p(o.position) };
|
|
604
757
|
case "shape":
|
|
@@ -721,12 +874,33 @@ function objectSeatLabels(o) {
|
|
|
721
874
|
function isSeatObject(o) {
|
|
722
875
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
723
876
|
}
|
|
877
|
+
function samePoints(left, right) {
|
|
878
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
879
|
+
}
|
|
880
|
+
function sameGASurfaceAsSection(object, section) {
|
|
881
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
882
|
+
const objectHoles = object.holes ?? [];
|
|
883
|
+
const sectionHoles = section.holes ?? [];
|
|
884
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
885
|
+
}
|
|
724
886
|
function computeSections(doc) {
|
|
725
887
|
const objs = allObjects(doc);
|
|
726
888
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
727
889
|
const nodes = /* @__PURE__ */ new Map();
|
|
728
890
|
for (const s of sectionObjs) {
|
|
729
|
-
|
|
891
|
+
const logicalId = s.logicalSectionId ?? s.id;
|
|
892
|
+
const existing = nodes.get(logicalId);
|
|
893
|
+
if (existing) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
nodes.set(logicalId, {
|
|
897
|
+
id: logicalId,
|
|
898
|
+
label: s.displayLabel || s.label || "Section",
|
|
899
|
+
zone: s.zone,
|
|
900
|
+
seatCount: 0,
|
|
901
|
+
objectIds: [],
|
|
902
|
+
seatLabels: []
|
|
903
|
+
});
|
|
730
904
|
}
|
|
731
905
|
const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
|
|
732
906
|
const objectToSection = /* @__PURE__ */ new Map();
|
|
@@ -734,22 +908,24 @@ function computeSections(doc) {
|
|
|
734
908
|
if (!isSeatObject(obj)) continue;
|
|
735
909
|
const labels = objectSeatLabels(obj);
|
|
736
910
|
if (labels.length === 0) continue;
|
|
911
|
+
const referencedLogicalId = obj.referenceInventorySource?.logicalSectionId;
|
|
737
912
|
const c = objectCenter(obj);
|
|
738
|
-
const
|
|
739
|
-
const
|
|
913
|
+
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
914
|
+
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
915
|
+
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
740
916
|
node.seatCount += labels.length;
|
|
741
917
|
node.objectIds.push(obj.id);
|
|
742
918
|
node.seatLabels.push(...labels);
|
|
743
919
|
objectToSection.set(obj.id, node.id);
|
|
744
920
|
}
|
|
745
921
|
return {
|
|
746
|
-
sections:
|
|
922
|
+
sections: [...nodes.values()],
|
|
747
923
|
ungrouped: ungrouped.objectIds.length ? ungrouped : null,
|
|
748
924
|
objectToSection
|
|
749
925
|
};
|
|
750
926
|
}
|
|
751
927
|
function isSectionHidden(s, hidden) {
|
|
752
|
-
return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
|
|
928
|
+
return hidden.has(s.id) || !!s.logicalSectionId && hidden.has(s.logicalSectionId) || !!s.zone && hidden.has(s.zone);
|
|
753
929
|
}
|
|
754
930
|
function hiddenObjectIds(doc, hidden) {
|
|
755
931
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -802,6 +978,61 @@ var import_Ellipse = require("konva/lib/shapes/Ellipse");
|
|
|
802
978
|
var import_Line = require("konva/lib/shapes/Line");
|
|
803
979
|
var import_Text = require("konva/lib/shapes/Text");
|
|
804
980
|
var import_Image = require("konva/lib/shapes/Image");
|
|
981
|
+
var import_Shape = require("konva/lib/Shape");
|
|
982
|
+
|
|
983
|
+
// src/core/chartRenderRules.ts
|
|
984
|
+
var SEAT_LABEL_FONT_SIZE = 7;
|
|
985
|
+
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
986
|
+
var GA_LABEL_FONT_SIZE = 15;
|
|
987
|
+
var GA_CAPACITY_LABEL_FONT_SIZE = 11;
|
|
988
|
+
var GA_FILL_OPACITY = 0.85;
|
|
989
|
+
var MIN_VISIBLE_BOOKABLE_LABEL_PX = 12;
|
|
990
|
+
var SMALL_TEXT_CONTRAST = 4.5;
|
|
991
|
+
var DARK_BOOKABLE_LABEL_INK = "#000000";
|
|
992
|
+
var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
|
|
993
|
+
function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
|
|
994
|
+
return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
|
|
995
|
+
}
|
|
996
|
+
function bookableMarkerLabel(publicLabel) {
|
|
997
|
+
return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
|
|
998
|
+
}
|
|
999
|
+
function luminance(value) {
|
|
1000
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
1001
|
+
if (!match) return null;
|
|
1002
|
+
const channel = (offset) => {
|
|
1003
|
+
const encoded = Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255;
|
|
1004
|
+
return encoded <= 0.04045 ? encoded / 12.92 : ((encoded + 0.055) / 1.055) ** 2.4;
|
|
1005
|
+
};
|
|
1006
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
1007
|
+
}
|
|
1008
|
+
function renderedTextContrast(ink, fill) {
|
|
1009
|
+
const inkLuminance = luminance(ink);
|
|
1010
|
+
const fillLuminance = luminance(fill);
|
|
1011
|
+
if (inkLuminance == null || fillLuminance == null) return null;
|
|
1012
|
+
return (Math.max(inkLuminance, fillLuminance) + 0.05) / (Math.min(inkLuminance, fillLuminance) + 0.05);
|
|
1013
|
+
}
|
|
1014
|
+
function rgb(value) {
|
|
1015
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
1016
|
+
if (!match) return null;
|
|
1017
|
+
const packed = Number.parseInt(match[1], 16);
|
|
1018
|
+
return [packed >> 16 & 255, packed >> 8 & 255, packed & 255];
|
|
1019
|
+
}
|
|
1020
|
+
function compositeHexOver(foreground, background, opacity) {
|
|
1021
|
+
const front = rgb(foreground);
|
|
1022
|
+
const back = rgb(background);
|
|
1023
|
+
if (!front || !back) return background;
|
|
1024
|
+
const alpha = Math.max(0, Math.min(1, opacity));
|
|
1025
|
+
const channels = front.map((value, index) => Math.round(value * alpha + back[index] * (1 - alpha)));
|
|
1026
|
+
return `#${channels.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
1027
|
+
}
|
|
1028
|
+
function stateAwareBookableLabelInk(fill, preferred) {
|
|
1029
|
+
const preferredContrast = renderedTextContrast(preferred, fill);
|
|
1030
|
+
if (preferredContrast != null && preferredContrast >= SMALL_TEXT_CONTRAST) return preferred;
|
|
1031
|
+
const darkContrast = renderedTextContrast(DARK_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
1032
|
+
const lightContrast = renderedTextContrast(LIGHT_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
1033
|
+
if (darkContrast === 0 && lightContrast === 0) return preferred;
|
|
1034
|
+
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
1035
|
+
}
|
|
805
1036
|
|
|
806
1037
|
// src/lib/money.ts
|
|
807
1038
|
var DEFAULT_CURRENCY = "USD";
|
|
@@ -861,6 +1092,8 @@ var en = {
|
|
|
861
1092
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
862
1093
|
"picker.language": "Language",
|
|
863
1094
|
"picker.zoomToFit": "Zoom to fit",
|
|
1095
|
+
"picker.seatCountLabel": "seats",
|
|
1096
|
+
"picker.capacity": "capacity",
|
|
864
1097
|
"picker.viewMode": "View mode",
|
|
865
1098
|
"picker.floor": "Floor",
|
|
866
1099
|
"picker.zoomLevel": "Zoom level",
|
|
@@ -950,7 +1183,8 @@ function formatDate(value, opts) {
|
|
|
950
1183
|
var SEAT_RADIUS = 9;
|
|
951
1184
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
952
1185
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
953
|
-
var LABEL_SCALE =
|
|
1186
|
+
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1187
|
+
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
954
1188
|
var SEAT_TAP_SLOP_PX = 14;
|
|
955
1189
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
956
1190
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
@@ -964,32 +1198,31 @@ var ISO_SQUASH = 0.58;
|
|
|
964
1198
|
var LIFT_PER_STEP = 58;
|
|
965
1199
|
var ISO_TWEEN_MS = 320;
|
|
966
1200
|
var CAMERA_GLIDE_MS = 650;
|
|
967
|
-
var BLOCK_FILL_ALPHA =
|
|
968
|
-
var
|
|
1201
|
+
var BLOCK_FILL_ALPHA = 1;
|
|
1202
|
+
var SECTION_STROKE_PX = 2;
|
|
1203
|
+
var LIGHT_OVERVIEW_SECTION_FILL = "#e5e7eb";
|
|
1204
|
+
var LIGHT_OVERVIEW_SECTION_STROKE = "#c7cbd1";
|
|
1205
|
+
var LIGHT_OVERVIEW_SECTION_INK = "#595f69";
|
|
1206
|
+
var LIGHT_OVERVIEW_FOCAL_FILL = "#d1d5db";
|
|
1207
|
+
var LIGHT_OVERVIEW_FOCAL_STROKE = "#b8bdc4";
|
|
1208
|
+
var DARK_OVERVIEW_SECTION_FILL = "#273142";
|
|
1209
|
+
var DARK_OVERVIEW_SECTION_STROKE = "#526078";
|
|
1210
|
+
var DARK_OVERVIEW_SECTION_INK = "#f1f5f9";
|
|
1211
|
+
var DARK_OVERVIEW_FOCAL_FILL = "#374151";
|
|
1212
|
+
var DARK_OVERVIEW_FOCAL_STROKE = "#64748b";
|
|
969
1213
|
var SECTION_LABEL_PX = 20;
|
|
970
|
-
var
|
|
971
|
-
var ZONE_LABEL_PX =
|
|
1214
|
+
var MIN_SECTION_LABEL_PX = 12;
|
|
1215
|
+
var ZONE_LABEL_PX = 18;
|
|
972
1216
|
var ZONE_SUB_PX = 12;
|
|
1217
|
+
var HIERARCHY_PILL_BACKGROUND = "#111827";
|
|
973
1218
|
var HELD_FILL = "#6b7280";
|
|
974
1219
|
var TAKEN_FILL = "#374151";
|
|
975
1220
|
var NFS_STROKE = "#4b5563";
|
|
976
|
-
var CLOSED_SECTION_FILL = "#586070";
|
|
977
1221
|
var CLOSED_SEAT_FILL = "#4b5563";
|
|
978
1222
|
var CLOSED_SEAT_OPACITY = 0.4;
|
|
979
1223
|
var FOCUS_DIM_OPACITY = 0.16;
|
|
980
|
-
var FOCUS_DESATURATE = 0.72;
|
|
981
|
-
var FOCUS_NEUTRAL = "#6b7280";
|
|
982
1224
|
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
983
1225
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
984
|
-
var ACCESS_RING = {
|
|
985
|
-
wheelchair: "#3b82f6",
|
|
986
|
-
companion: "#8b5cf6",
|
|
987
|
-
"semi-ambulatory": "#0ea5e9",
|
|
988
|
-
hearing: "#14b8a6",
|
|
989
|
-
"sign-language": "#f59e0b",
|
|
990
|
-
"plus-size": "#ec4899",
|
|
991
|
-
"lift-armrest": "#22c55e"
|
|
992
|
-
};
|
|
993
1226
|
function seatMatchesAccess(seat, filter) {
|
|
994
1227
|
if (filter.length === 0) return !!seat.accessible;
|
|
995
1228
|
return !!seat.accessibility?.some((t2) => filter.includes(t2));
|
|
@@ -999,6 +1232,7 @@ var DEF_SELECTION = "#ffffff";
|
|
|
999
1232
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1000
1233
|
var DEF_DECOR_FILL = "#232c40";
|
|
1001
1234
|
var DEF_TEXT = "#8b93a7";
|
|
1235
|
+
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1002
1236
|
function colorLuminance(color) {
|
|
1003
1237
|
const s = color.trim();
|
|
1004
1238
|
let r = NaN;
|
|
@@ -1011,11 +1245,11 @@ function colorLuminance(color) {
|
|
|
1011
1245
|
g = parseInt(h.slice(2, 4), 16);
|
|
1012
1246
|
b = parseInt(h.slice(4, 6), 16);
|
|
1013
1247
|
} else {
|
|
1014
|
-
const
|
|
1015
|
-
if (
|
|
1016
|
-
r = +
|
|
1017
|
-
g = +
|
|
1018
|
-
b = +
|
|
1248
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
|
|
1249
|
+
if (rgb2) {
|
|
1250
|
+
r = +rgb2[1];
|
|
1251
|
+
g = +rgb2[2];
|
|
1252
|
+
b = +rgb2[3];
|
|
1019
1253
|
}
|
|
1020
1254
|
}
|
|
1021
1255
|
if (Number.isNaN(r)) return NaN;
|
|
@@ -1025,6 +1259,34 @@ function isLightColor(color) {
|
|
|
1025
1259
|
const lum = colorLuminance(color);
|
|
1026
1260
|
return !Number.isNaN(lum) && lum > 0.6;
|
|
1027
1261
|
}
|
|
1262
|
+
function opaqueColorHex(color) {
|
|
1263
|
+
const value = color.trim();
|
|
1264
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
|
|
1265
|
+
if (hex) {
|
|
1266
|
+
const expanded = hex[1].length === 3 ? hex[1].split("").map((channel) => channel + channel).join("") : hex[1];
|
|
1267
|
+
return `#${expanded.toLowerCase()}`;
|
|
1268
|
+
}
|
|
1269
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
|
|
1270
|
+
if (!rgb2 || rgb2[4] != null && Number(rgb2[4]) < 0.999) return null;
|
|
1271
|
+
const channels = [Number(rgb2[1]), Number(rgb2[2]), Number(rgb2[3])];
|
|
1272
|
+
if (channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
|
|
1273
|
+
return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
1274
|
+
}
|
|
1275
|
+
function overviewPalette(canvasBackground) {
|
|
1276
|
+
return isLightColor(canvasBackground) ? {
|
|
1277
|
+
sectionFill: LIGHT_OVERVIEW_SECTION_FILL,
|
|
1278
|
+
sectionStroke: LIGHT_OVERVIEW_SECTION_STROKE,
|
|
1279
|
+
sectionInk: LIGHT_OVERVIEW_SECTION_INK,
|
|
1280
|
+
focalFill: LIGHT_OVERVIEW_FOCAL_FILL,
|
|
1281
|
+
focalStroke: LIGHT_OVERVIEW_FOCAL_STROKE
|
|
1282
|
+
} : {
|
|
1283
|
+
sectionFill: DARK_OVERVIEW_SECTION_FILL,
|
|
1284
|
+
sectionStroke: DARK_OVERVIEW_SECTION_STROKE,
|
|
1285
|
+
sectionInk: DARK_OVERVIEW_SECTION_INK,
|
|
1286
|
+
focalFill: DARK_OVERVIEW_FOCAL_FILL,
|
|
1287
|
+
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1028
1290
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
1029
1291
|
function seatIdOf(target) {
|
|
1030
1292
|
const n = target;
|
|
@@ -1060,6 +1322,108 @@ function polyBounds(pts) {
|
|
|
1060
1322
|
}
|
|
1061
1323
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
1062
1324
|
}
|
|
1325
|
+
function rotatedRectPoints(center, width, height, rotation) {
|
|
1326
|
+
const radians = rotation * Math.PI / 180;
|
|
1327
|
+
const cos = Math.cos(radians);
|
|
1328
|
+
const sin = Math.sin(radians);
|
|
1329
|
+
return [
|
|
1330
|
+
{ x: -width / 2, y: -height / 2 },
|
|
1331
|
+
{ x: width / 2, y: -height / 2 },
|
|
1332
|
+
{ x: width / 2, y: height / 2 },
|
|
1333
|
+
{ x: -width / 2, y: height / 2 }
|
|
1334
|
+
].map((point) => ({
|
|
1335
|
+
x: center.x + point.x * cos - point.y * sin,
|
|
1336
|
+
y: center.y + point.x * sin + point.y * cos
|
|
1337
|
+
}));
|
|
1338
|
+
}
|
|
1339
|
+
function pointsBounds(points) {
|
|
1340
|
+
const bounds = polyBounds(points);
|
|
1341
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
1342
|
+
}
|
|
1343
|
+
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1344
|
+
const radians = rotation * Math.PI / 180;
|
|
1345
|
+
const cos = Math.cos(radians);
|
|
1346
|
+
const sin = Math.sin(radians);
|
|
1347
|
+
for (let yStep = 0; yStep <= 4; yStep++) {
|
|
1348
|
+
for (let xStep = 0; xStep <= 6; xStep++) {
|
|
1349
|
+
const localX = width * (xStep / 6 - 0.5);
|
|
1350
|
+
const localY = height * (yStep / 4 - 0.5);
|
|
1351
|
+
const point = {
|
|
1352
|
+
x: center.x + localX * cos - localY * sin,
|
|
1353
|
+
y: center.y + localX * sin + localY * cos
|
|
1354
|
+
};
|
|
1355
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) return false;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1361
|
+
const bounds = polyBounds(outer);
|
|
1362
|
+
const centre = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
1363
|
+
const points = [preferred];
|
|
1364
|
+
for (let row = 1; row < 12; row += 1) {
|
|
1365
|
+
for (let column = 1; column < 12; column += 1) {
|
|
1366
|
+
const point = {
|
|
1367
|
+
x: bounds.x + bounds.width * column / 12,
|
|
1368
|
+
y: bounds.y + bounds.height * row / 12
|
|
1369
|
+
};
|
|
1370
|
+
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
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));
|
|
1374
|
+
}
|
|
1375
|
+
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1376
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1377
|
+
const next = points[(index + 1) % points.length];
|
|
1378
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
1379
|
+
}, 0);
|
|
1380
|
+
const outerClockwise = signedArea(outer) > 0;
|
|
1381
|
+
return new import_Shape.Shape({
|
|
1382
|
+
...attrs,
|
|
1383
|
+
sceneFunc(context, shape) {
|
|
1384
|
+
context.beginPath();
|
|
1385
|
+
const polygonPath = (points) => {
|
|
1386
|
+
if (!points.length) return;
|
|
1387
|
+
context.moveTo(points[0].x, points[0].y);
|
|
1388
|
+
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1389
|
+
context.closePath();
|
|
1390
|
+
};
|
|
1391
|
+
const vectorPath = (path) => {
|
|
1392
|
+
context.moveTo(path.start.x, path.start.y);
|
|
1393
|
+
let current = path.start;
|
|
1394
|
+
for (const segment of path.segments) {
|
|
1395
|
+
if (segment.kind === "line") context.lineTo(segment.end.x, segment.end.y);
|
|
1396
|
+
else if (segment.kind === "arc") context.arc(
|
|
1397
|
+
segment.center.x,
|
|
1398
|
+
segment.center.y,
|
|
1399
|
+
segment.radius,
|
|
1400
|
+
Math.atan2(current.y - segment.center.y, current.x - segment.center.x),
|
|
1401
|
+
Math.atan2(segment.end.y - segment.center.y, segment.end.x - segment.center.x),
|
|
1402
|
+
!segment.clockwise
|
|
1403
|
+
);
|
|
1404
|
+
else context.bezierCurveTo(
|
|
1405
|
+
segment.control1.x,
|
|
1406
|
+
segment.control1.y,
|
|
1407
|
+
segment.control2.x,
|
|
1408
|
+
segment.control2.y,
|
|
1409
|
+
segment.end.x,
|
|
1410
|
+
segment.end.y
|
|
1411
|
+
);
|
|
1412
|
+
current = segment.end;
|
|
1413
|
+
}
|
|
1414
|
+
context.closePath();
|
|
1415
|
+
};
|
|
1416
|
+
if (outerPath) vectorPath(outerPath);
|
|
1417
|
+
else polygonPath(outer);
|
|
1418
|
+
for (const hole of holes ?? []) {
|
|
1419
|
+
const holeClockwise = signedArea(hole) > 0;
|
|
1420
|
+
polygonPath(holeClockwise === outerClockwise ? [...hole].reverse() : hole);
|
|
1421
|
+
}
|
|
1422
|
+
context.fillStrokeShape(shape);
|
|
1423
|
+
},
|
|
1424
|
+
perfectDrawEnabled: false
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1063
1427
|
function rgba(hex, a) {
|
|
1064
1428
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1065
1429
|
if (!m) return hex;
|
|
@@ -1079,11 +1443,11 @@ function mixColors(parts, fallback) {
|
|
|
1079
1443
|
let b = 0;
|
|
1080
1444
|
let tw = 0;
|
|
1081
1445
|
for (const p of parts) {
|
|
1082
|
-
const
|
|
1083
|
-
if (!
|
|
1084
|
-
r +=
|
|
1085
|
-
g +=
|
|
1086
|
-
b +=
|
|
1446
|
+
const rgb2 = hexToRgb(p.hex);
|
|
1447
|
+
if (!rgb2 || p.w <= 0) continue;
|
|
1448
|
+
r += rgb2[0] * p.w;
|
|
1449
|
+
g += rgb2[1] * p.w;
|
|
1450
|
+
b += rgb2[2] * p.w;
|
|
1087
1451
|
tw += p.w;
|
|
1088
1452
|
}
|
|
1089
1453
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
@@ -1107,11 +1471,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1107
1471
|
this.circleById = /* @__PURE__ */ new Map();
|
|
1108
1472
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
1109
1473
|
this.boothDims = /* @__PURE__ */ new Map();
|
|
1110
|
-
/** Booth
|
|
1474
|
+
/** Booth labels live with the booth shape but obey the shared rendered-size LOD. */
|
|
1111
1475
|
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
1476
|
+
/** Viewport seat labels are rebuilt after each settled camera change. */
|
|
1477
|
+
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1478
|
+
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1479
|
+
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1480
|
+
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1481
|
+
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1482
|
+
/** GA paint and text share price/highlight filter state. */
|
|
1483
|
+
this.gaById = /* @__PURE__ */ new Map();
|
|
1112
1484
|
this.statusById = /* @__PURE__ */ new Map();
|
|
1113
1485
|
this.catColor = /* @__PURE__ */ new Map();
|
|
1114
1486
|
this.theme = {};
|
|
1487
|
+
/** Opaque paint actually visible behind transparent Konva canvases. */
|
|
1488
|
+
this.canvasBackground = DEF_CANVAS_BACKGROUND;
|
|
1115
1489
|
/** Effective selection/hover ring color — resolved per chart in setChart(). */
|
|
1116
1490
|
this.effSelection = DEF_SELECTION;
|
|
1117
1491
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
@@ -1409,6 +1783,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1409
1783
|
this.circleById.clear();
|
|
1410
1784
|
this.boothDims.clear();
|
|
1411
1785
|
this.boothLabelById.clear();
|
|
1786
|
+
this.seatLabelById.clear();
|
|
1787
|
+
this.freeTextById.clear();
|
|
1788
|
+
this.primaryFocalLabels.clear();
|
|
1789
|
+
this.gaById.clear();
|
|
1790
|
+
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
1412
1791
|
this.selectionMarkers.clear();
|
|
1413
1792
|
this.ownedHold.clear();
|
|
1414
1793
|
this.selectionFocusId = null;
|
|
@@ -1420,6 +1799,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1420
1799
|
this.sections = [];
|
|
1421
1800
|
this.zones = [];
|
|
1422
1801
|
this.seatSection.clear();
|
|
1802
|
+
this.focusedSectionId = null;
|
|
1803
|
+
this.focusBackdrop = null;
|
|
1423
1804
|
this.catPrice.clear();
|
|
1424
1805
|
this.zoneColor.clear();
|
|
1425
1806
|
this.lodScale = 0;
|
|
@@ -1437,7 +1818,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1437
1818
|
this.hoverRing.visible(false);
|
|
1438
1819
|
this.theme = doc.theme ?? {};
|
|
1439
1820
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1440
|
-
this.container.style.background =
|
|
1821
|
+
this.container.style.background = "";
|
|
1822
|
+
this.canvasBackground = this.resolveCanvasBackground();
|
|
1823
|
+
this.container.style.background = this.canvasBackground;
|
|
1441
1824
|
this.effSelection = this.resolveSelectionColor();
|
|
1442
1825
|
this.hoverRing.stroke(this.effSelection);
|
|
1443
1826
|
this.hoverRing.radius(this.seatR + 2);
|
|
@@ -1666,12 +2049,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1666
2049
|
}
|
|
1667
2050
|
return this.selectMany(ids);
|
|
1668
2051
|
}
|
|
2052
|
+
/** Exact SDK capture helper. MCP never accepts this id; the SDK derives it
|
|
2053
|
+
* from the persisted floor and uses the normal selected paint/ring path. */
|
|
2054
|
+
setEvidenceSelection(seatId) {
|
|
2055
|
+
if (!this.seatById.has(seatId) || !this.isSelectable(seatId)) return false;
|
|
2056
|
+
if (this.selection.size) this.clearSelection();
|
|
2057
|
+
this.setSelected(seatId, true);
|
|
2058
|
+
this.overlayLayer.batchDraw();
|
|
2059
|
+
return this.selection.has(seatId);
|
|
2060
|
+
}
|
|
1669
2061
|
/** Selectable seats in a section OR zone id — pure read (no selection change). */
|
|
1670
2062
|
getSelectableInSection(sectionId) {
|
|
1671
2063
|
const out = [];
|
|
1672
2064
|
const seen = /* @__PURE__ */ new Set();
|
|
1673
2065
|
for (const sec of this.sections) {
|
|
1674
|
-
if (sec.id !== sectionId && sec.zone !== sectionId) continue;
|
|
2066
|
+
if (sec.id !== sectionId && sec.logicalId !== sectionId && sec.zone !== sectionId) continue;
|
|
1675
2067
|
for (const id of sec.memberIds) {
|
|
1676
2068
|
if (seen.has(id)) continue;
|
|
1677
2069
|
seen.add(id);
|
|
@@ -1814,6 +2206,65 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1814
2206
|
};
|
|
1815
2207
|
requestAnimationFrame(step);
|
|
1816
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Pulse a section outline without moving the camera or mutating the authored
|
|
2211
|
+
* geometry. The temporary halo is drawn in the non-listening overlay layer,
|
|
2212
|
+
* so the apparent 4% lift never changes hit testing or selection bounds.
|
|
2213
|
+
*/
|
|
2214
|
+
flashSection(sectionId, color = "#22a06b") {
|
|
2215
|
+
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2216
|
+
if (!matches.length) return;
|
|
2217
|
+
for (const section of matches) {
|
|
2218
|
+
const centre = section.outline.reduce(
|
|
2219
|
+
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2220
|
+
{ x: 0, y: 0 }
|
|
2221
|
+
);
|
|
2222
|
+
centre.x /= section.outline.length;
|
|
2223
|
+
centre.y /= section.outline.length;
|
|
2224
|
+
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
2225
|
+
const halo = new import_Line.Line({
|
|
2226
|
+
x: centre.x + lift.x,
|
|
2227
|
+
y: centre.y + lift.y,
|
|
2228
|
+
points: section.outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2229
|
+
closed: true,
|
|
2230
|
+
stroke: color,
|
|
2231
|
+
strokeWidth: 3,
|
|
2232
|
+
strokeScaleEnabled: false,
|
|
2233
|
+
opacity: 0.92,
|
|
2234
|
+
listening: false,
|
|
2235
|
+
perfectDrawEnabled: false,
|
|
2236
|
+
shadowForStrokeEnabled: true,
|
|
2237
|
+
shadowColor: color,
|
|
2238
|
+
shadowBlur: 14,
|
|
2239
|
+
shadowOpacity: 0.7
|
|
2240
|
+
});
|
|
2241
|
+
this.overlayLayer.add(halo);
|
|
2242
|
+
this.overlayLayer.batchDraw();
|
|
2243
|
+
const remove = () => {
|
|
2244
|
+
if (!halo.getLayer()) return;
|
|
2245
|
+
halo.destroy();
|
|
2246
|
+
this.overlayLayer.batchDraw();
|
|
2247
|
+
};
|
|
2248
|
+
if (this.reducedMotion || typeof document !== "undefined" && document.hidden) {
|
|
2249
|
+
setTimeout(remove, 520);
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
const start = performance.now();
|
|
2253
|
+
const duration = 820;
|
|
2254
|
+
const step = (now) => {
|
|
2255
|
+
if (this.destroyed || !halo.getLayer()) return;
|
|
2256
|
+
const t2 = Math.min(1, (now - start) / duration);
|
|
2257
|
+
const eased = 1 - Math.pow(1 - t2, 3);
|
|
2258
|
+
const scale = 1 + eased * 0.04;
|
|
2259
|
+
halo.scale({ x: scale, y: scale });
|
|
2260
|
+
halo.opacity(0.92 * (1 - t2));
|
|
2261
|
+
this.overlayLayer.batchDraw();
|
|
2262
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
2263
|
+
else remove();
|
|
2264
|
+
};
|
|
2265
|
+
requestAnimationFrame(step);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1817
2268
|
/** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
|
|
1818
2269
|
nearestSeat(fromId, dir) {
|
|
1819
2270
|
const from = this.seatById.get(fromId);
|
|
@@ -1887,6 +2338,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1887
2338
|
seatCount() {
|
|
1888
2339
|
return this.seats.length;
|
|
1889
2340
|
}
|
|
2341
|
+
bookableCount() {
|
|
2342
|
+
let total = this.seats.length;
|
|
2343
|
+
for (const area of this.gaById.values()) total += area.capacity;
|
|
2344
|
+
return total;
|
|
2345
|
+
}
|
|
1890
2346
|
worldToScreen(point) {
|
|
1891
2347
|
const s = this.stage.scaleX();
|
|
1892
2348
|
const p = this.isoT === 0 ? point : this.isoForward(point);
|
|
@@ -1903,6 +2359,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1903
2359
|
const c = this.circleById.get(seat.id);
|
|
1904
2360
|
if (c) this.paintSeat(c, seat.id);
|
|
1905
2361
|
}
|
|
2362
|
+
this.updateLabels();
|
|
1906
2363
|
if (this.cached) {
|
|
1907
2364
|
this.seatLayer.clearCache();
|
|
1908
2365
|
this.cacheSeatLayer();
|
|
@@ -1929,12 +2386,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1929
2386
|
const c = this.circleById.get(seat.id);
|
|
1930
2387
|
if (c) this.paintSeat(c, seat.id);
|
|
1931
2388
|
}
|
|
2389
|
+
this.updateLabels();
|
|
1932
2390
|
if (this.cached) {
|
|
1933
2391
|
this.seatLayer.clearCache();
|
|
1934
2392
|
this.cacheSeatLayer();
|
|
1935
2393
|
} else {
|
|
1936
2394
|
this.seatLayer.batchDraw();
|
|
1937
2395
|
}
|
|
2396
|
+
this.applyGAFilterState();
|
|
2397
|
+
}
|
|
2398
|
+
gaCategoryDimmed(categoryKey) {
|
|
2399
|
+
return Boolean(
|
|
2400
|
+
this.categoryHighlight && categoryKey !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(categoryKey)
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
/** Keep GA paint and its two labels in the same legend/price-filter state. */
|
|
2404
|
+
applyGAFilterState() {
|
|
2405
|
+
this.paintGAStateForView();
|
|
2406
|
+
this.updateFreeTextVisibility();
|
|
2407
|
+
this.bgLayer.batchDraw();
|
|
2408
|
+
}
|
|
2409
|
+
paintGAStateForView() {
|
|
2410
|
+
for (const ga of this.gaById.values()) {
|
|
2411
|
+
const filteredOut = Boolean(this.categoryFilter && !this.categoryFilter.has(ga.categoryKey));
|
|
2412
|
+
const overviewHidden = ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
|
|
2413
|
+
ga.polygon.opacity(overviewHidden ? 0 : this.gaCategoryDimmed(ga.categoryKey) ? GA_FILL_OPACITY * 0.08 : GA_FILL_OPACITY);
|
|
2414
|
+
ga.polygon.listening(!overviewHidden && !filteredOut);
|
|
2415
|
+
}
|
|
1938
2416
|
}
|
|
1939
2417
|
/** Frame the currently available inventory that survived a buyer price
|
|
1940
2418
|
* filter. Clearing the filter glides back to the full venue. */
|
|
@@ -2194,13 +2672,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2194
2672
|
this.paintSeat(c, seat.id);
|
|
2195
2673
|
target.add(c);
|
|
2196
2674
|
if (seat.accessible) {
|
|
2197
|
-
const primary = seat.accessibility?.[0];
|
|
2198
2675
|
target.add(
|
|
2199
2676
|
new import_Circle.Circle({
|
|
2200
2677
|
x: seat.x,
|
|
2201
2678
|
y: seat.y,
|
|
2202
2679
|
radius: this.seatR + 1,
|
|
2203
|
-
stroke:
|
|
2680
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
2204
2681
|
strokeWidth: 2,
|
|
2205
2682
|
listening: false,
|
|
2206
2683
|
perfectDrawEnabled: false,
|
|
@@ -2229,13 +2706,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2229
2706
|
});
|
|
2230
2707
|
rect.setAttr("seatId", seat.id);
|
|
2231
2708
|
this.circleById.set(seat.id, rect);
|
|
2232
|
-
this.paintSeat(rect, seat.id);
|
|
2233
2709
|
target.add(rect);
|
|
2234
2710
|
const t2 = new import_Text.Text({
|
|
2235
2711
|
x: seat.x,
|
|
2236
2712
|
y: seat.y,
|
|
2237
|
-
text: seat.label,
|
|
2238
|
-
fontSize:
|
|
2713
|
+
text: seat.displayLabel ?? seat.label,
|
|
2714
|
+
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
2239
2715
|
fontStyle: "600",
|
|
2240
2716
|
fontFamily: this.labelFont(),
|
|
2241
2717
|
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
@@ -2244,9 +2720,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2244
2720
|
});
|
|
2245
2721
|
t2.offsetX(t2.width() / 2);
|
|
2246
2722
|
t2.offsetY(t2.height() / 2);
|
|
2723
|
+
t2.visible(false);
|
|
2724
|
+
this.boothLabelById.set(seat.id, t2);
|
|
2247
2725
|
this.hasBoothText = true;
|
|
2248
2726
|
this.boothLabelById.set(seat.id, t2);
|
|
2249
2727
|
target.add(t2);
|
|
2728
|
+
this.paintSeat(rect, seat.id);
|
|
2250
2729
|
}
|
|
2251
2730
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
2252
2731
|
seatBaseColor(categoryKey) {
|
|
@@ -2254,6 +2733,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2254
2733
|
const idx = this.catOrder.indexOf(categoryKey);
|
|
2255
2734
|
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
2256
2735
|
}
|
|
2736
|
+
/** Resolve label ink against the paint that is actually visible. The theme
|
|
2737
|
+
* ink remains preferred, but mixed category palettes cannot always share one
|
|
2738
|
+
* accessible text colour, so every free and transient state gets the same
|
|
2739
|
+
* deterministic dark/light fallback used by Designer. */
|
|
2740
|
+
renderedBookableLabelInk(shape) {
|
|
2741
|
+
const preferred = this.theme.seatLabelColor ?? DEF_SEAT_LABEL;
|
|
2742
|
+
const fill = shape.fill();
|
|
2743
|
+
return stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", preferred);
|
|
2744
|
+
}
|
|
2257
2745
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
2258
2746
|
paintSeat(c, id) {
|
|
2259
2747
|
const seat = this.seatById.get(id);
|
|
@@ -2317,7 +2805,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2317
2805
|
}
|
|
2318
2806
|
if (this.dimmedSections.size) {
|
|
2319
2807
|
const sec = this.seatSection.get(id);
|
|
2320
|
-
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2808
|
+
if (sec && (this.dimmedSections.has(sec.id) || this.dimmedSections.has(sec.logicalId) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2321
2809
|
c.opacity(0.18);
|
|
2322
2810
|
}
|
|
2323
2811
|
}
|
|
@@ -2330,16 +2818,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2330
2818
|
}
|
|
2331
2819
|
if (this.focusedSectionId) {
|
|
2332
2820
|
const sec = this.seatSection.get(id);
|
|
2333
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2821
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2334
2822
|
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
2335
2823
|
}
|
|
2336
2824
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2825
|
+
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2826
|
+
if (bookableLabel) {
|
|
2827
|
+
bookableLabel.fill(this.renderedBookableLabelInk(c));
|
|
2828
|
+
bookableLabel.visible(
|
|
2829
|
+
isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2337
2832
|
}
|
|
2338
2833
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
2339
2834
|
seatInClosedSection(id) {
|
|
2340
2835
|
if (!this.closedSections.size) return false;
|
|
2341
2836
|
const sec = this.seatSection.get(id);
|
|
2342
|
-
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2837
|
+
return !!sec && (this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2343
2838
|
}
|
|
2344
2839
|
/**
|
|
2345
2840
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -2352,6 +2847,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2352
2847
|
const c = this.circleById.get(seat.id);
|
|
2353
2848
|
if (c) this.paintSeat(c, seat.id);
|
|
2354
2849
|
}
|
|
2850
|
+
this.updateLabels();
|
|
2355
2851
|
if (this.cached) {
|
|
2356
2852
|
this.seatLayer.clearCache();
|
|
2357
2853
|
this.cacheSeatLayer();
|
|
@@ -2396,7 +2892,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2396
2892
|
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2397
2893
|
*/
|
|
2398
2894
|
focusSection(id) {
|
|
2399
|
-
if (!this.sections.some((
|
|
2895
|
+
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2400
2896
|
this.focusedSectionId = id;
|
|
2401
2897
|
this.drawFocusBackdrop(id);
|
|
2402
2898
|
this.repaintSectionsAndSeats();
|
|
@@ -2424,20 +2920,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2424
2920
|
this.focusBackdrop.destroy();
|
|
2425
2921
|
this.focusBackdrop = null;
|
|
2426
2922
|
}
|
|
2427
|
-
const
|
|
2428
|
-
if (!
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
}
|
|
2438
|
-
this.bgLayer.add(
|
|
2439
|
-
|
|
2440
|
-
this.focusBackdrop =
|
|
2923
|
+
const sections = this.sections.filter((section) => section.id === id || section.logicalId === id);
|
|
2924
|
+
if (!sections.length) return;
|
|
2925
|
+
const backdrop = new import_Group.Group({ listening: false });
|
|
2926
|
+
for (const section of sections) {
|
|
2927
|
+
backdrop.add(polygonWithHolesShape(section.outline, section.holes, {
|
|
2928
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2929
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2930
|
+
strokeWidth: 1,
|
|
2931
|
+
listening: false
|
|
2932
|
+
}, section.outlinePath));
|
|
2933
|
+
}
|
|
2934
|
+
this.bgLayer.add(backdrop);
|
|
2935
|
+
backdrop.moveToTop();
|
|
2936
|
+
this.focusBackdrop = backdrop;
|
|
2441
2937
|
}
|
|
2442
2938
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2443
2939
|
repaintSectionsAndSeats() {
|
|
@@ -2465,7 +2961,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2465
2961
|
height: Math.abs(br.y - tl.y)
|
|
2466
2962
|
};
|
|
2467
2963
|
}
|
|
2468
|
-
/** Axis-aligned world bounds of
|
|
2964
|
+
/** Axis-aligned world bounds of seats, section outlines, and GA polygons (minimap F3 frame). */
|
|
2469
2965
|
getWorldBounds() {
|
|
2470
2966
|
let minX = Infinity;
|
|
2471
2967
|
let minY = Infinity;
|
|
@@ -2479,6 +2975,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2479
2975
|
};
|
|
2480
2976
|
for (const s of this.seats) grow(s.x, s.y);
|
|
2481
2977
|
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2978
|
+
for (const area of this.gaById.values()) for (const p of area.points) grow(p.x, p.y);
|
|
2482
2979
|
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2483
2980
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2484
2981
|
}
|
|
@@ -2490,12 +2987,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2490
2987
|
const c = this.circleById.get(seat.id);
|
|
2491
2988
|
if (c) this.paintSeat(c, seat.id);
|
|
2492
2989
|
}
|
|
2990
|
+
this.updateLabels();
|
|
2493
2991
|
if (this.cached) {
|
|
2494
2992
|
this.seatLayer.clearCache();
|
|
2495
2993
|
this.cacheSeatLayer();
|
|
2496
2994
|
} else {
|
|
2497
2995
|
this.seatLayer.batchDraw();
|
|
2498
2996
|
}
|
|
2997
|
+
this.applyGAFilterState();
|
|
2499
2998
|
}
|
|
2500
2999
|
renderBackground(doc) {
|
|
2501
3000
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
@@ -2513,32 +3012,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2513
3012
|
this.renderText(obj);
|
|
2514
3013
|
}
|
|
2515
3014
|
}
|
|
2516
|
-
const f = doc.focalPoint;
|
|
2517
|
-
if (f) {
|
|
2518
|
-
const size = 14;
|
|
2519
|
-
const cross = new import_Group.Group({ listening: false });
|
|
2520
|
-
cross.add(
|
|
2521
|
-
new import_Line.Line({ points: [f.x - size, f.y, f.x + size, f.y], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2522
|
-
new import_Line.Line({ points: [f.x, f.y - size, f.x, f.y + size], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2523
|
-
new import_Circle.Circle({ x: f.x, y: f.y, radius: 3, fill: "#4b5563" })
|
|
2524
|
-
);
|
|
2525
|
-
this.bgLayer.add(cross);
|
|
2526
|
-
}
|
|
2527
3015
|
}
|
|
2528
3016
|
/** Organizer floor-plan photo, dimmed, at the very bottom of the bg layer. */
|
|
2529
3017
|
renderBackgroundImage(bg) {
|
|
3018
|
+
if (!bg.url || bg.visible === false) return;
|
|
2530
3019
|
const img = new window.Image();
|
|
2531
3020
|
img.onload = () => {
|
|
2532
3021
|
const natW = img.naturalWidth || 4;
|
|
2533
3022
|
const natH = img.naturalHeight || 3;
|
|
3023
|
+
const rawCrop = bg.crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3024
|
+
const cropX = Math.max(0, Math.min(0.99, rawCrop.x));
|
|
3025
|
+
const cropY = Math.max(0, Math.min(0.99, rawCrop.y));
|
|
3026
|
+
const crop = {
|
|
3027
|
+
x: cropX,
|
|
3028
|
+
y: cropY,
|
|
3029
|
+
width: Math.max(0.01, Math.min(1 - cropX, rawCrop.width)),
|
|
3030
|
+
height: Math.max(0.01, Math.min(1 - cropY, rawCrop.height))
|
|
3031
|
+
};
|
|
2534
3032
|
const w = bg.width;
|
|
2535
|
-
const h = w * (natH / natW);
|
|
3033
|
+
const h = w * (natH * crop.height / (natW * crop.width));
|
|
2536
3034
|
const node = new import_Image.Image({
|
|
2537
3035
|
image: img,
|
|
2538
|
-
x: bg.center.x
|
|
2539
|
-
y: bg.center.y
|
|
3036
|
+
x: bg.center.x,
|
|
3037
|
+
y: bg.center.y,
|
|
3038
|
+
offsetX: w / 2,
|
|
3039
|
+
offsetY: h / 2,
|
|
2540
3040
|
width: w,
|
|
2541
3041
|
height: h,
|
|
3042
|
+
rotation: bg.rotation ?? 0,
|
|
3043
|
+
crop: {
|
|
3044
|
+
x: crop.x * natW,
|
|
3045
|
+
y: crop.y * natH,
|
|
3046
|
+
width: crop.width * natW,
|
|
3047
|
+
height: crop.height * natH
|
|
3048
|
+
},
|
|
2542
3049
|
opacity: bg.opacity,
|
|
2543
3050
|
listening: false
|
|
2544
3051
|
});
|
|
@@ -2610,28 +3117,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2610
3117
|
})
|
|
2611
3118
|
);
|
|
2612
3119
|
}
|
|
2613
|
-
this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3120
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3121
|
+
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
2614
3122
|
}
|
|
2615
3123
|
renderText(obj) {
|
|
2616
|
-
this.
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3124
|
+
const background = this.canvasBackground;
|
|
3125
|
+
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
3126
|
+
const node = new import_Text.Text({
|
|
3127
|
+
x: obj.position.x,
|
|
3128
|
+
y: obj.position.y,
|
|
3129
|
+
text: obj.text,
|
|
3130
|
+
fontSize: obj.fontSize,
|
|
3131
|
+
rotation: obj.rotation,
|
|
3132
|
+
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3133
|
+
// the actual canvas. Fail over to readable black/white instead of
|
|
3134
|
+
// painting an otherwise valid caption invisibly on that active surface.
|
|
3135
|
+
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3136
|
+
fontFamily: this.labelFont(),
|
|
3137
|
+
listening: false,
|
|
3138
|
+
perfectDrawEnabled: false
|
|
3139
|
+
});
|
|
3140
|
+
this.freeTextById.set(obj.id, {
|
|
3141
|
+
node,
|
|
3142
|
+
background,
|
|
3143
|
+
kind: "free-text"
|
|
3144
|
+
});
|
|
3145
|
+
this.bgLayer.add(node);
|
|
2629
3146
|
}
|
|
2630
3147
|
renderShape(obj) {
|
|
2631
|
-
const
|
|
3148
|
+
const authoredFill = obj.fill ?? this.theme.decorFill ?? DEF_DECOR_FILL;
|
|
2632
3149
|
const isStage = obj.role === "stage";
|
|
3150
|
+
const referenceFocal = obj.role === "reference-focal";
|
|
2633
3151
|
const isDecor = !!obj.role && !isStage;
|
|
2634
|
-
const
|
|
3152
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3153
|
+
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3154
|
+
const stroke = isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : void 0;
|
|
3155
|
+
const strokeWidth = isStage ? 1 : referenceFocal ? 2 : 0;
|
|
2635
3156
|
let cx = 0;
|
|
2636
3157
|
let cy = 0;
|
|
2637
3158
|
if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
@@ -2653,7 +3174,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2653
3174
|
height: obj.height,
|
|
2654
3175
|
...grad,
|
|
2655
3176
|
stroke,
|
|
2656
|
-
strokeWidth
|
|
3177
|
+
strokeWidth,
|
|
2657
3178
|
cornerRadius: 4,
|
|
2658
3179
|
listening: false
|
|
2659
3180
|
})
|
|
@@ -2667,7 +3188,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2667
3188
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2668
3189
|
} : { fill };
|
|
2669
3190
|
this.bgLayer.add(
|
|
2670
|
-
new import_Ellipse.Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth
|
|
3191
|
+
new import_Ellipse.Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth, listening: false })
|
|
2671
3192
|
);
|
|
2672
3193
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
2673
3194
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -2682,17 +3203,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2682
3203
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2683
3204
|
} : { fill };
|
|
2684
3205
|
this.bgLayer.add(
|
|
2685
|
-
new import_Line.Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth
|
|
3206
|
+
new import_Line.Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth, listening: false })
|
|
2686
3207
|
);
|
|
2687
3208
|
}
|
|
2688
3209
|
if (obj.label) {
|
|
2689
|
-
if (isStage)
|
|
2690
|
-
|
|
2691
|
-
|
|
3210
|
+
if (isStage) {
|
|
3211
|
+
const node = this.addStageLabel(cx, cy, obj.label, fill);
|
|
3212
|
+
this.primaryFocalLabels.set(node, 22);
|
|
3213
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "stage" });
|
|
3214
|
+
} else if (isDecor) {
|
|
3215
|
+
const node = this.addCentredLabel(
|
|
3216
|
+
this.bgLayer,
|
|
3217
|
+
obj.label,
|
|
3218
|
+
cx,
|
|
3219
|
+
cy,
|
|
3220
|
+
referenceFocal ? stateAwareBookableLabelInk(fill, "#e6e9f0") : "#9aa3b5",
|
|
3221
|
+
referenceFocal ? 18 : 12,
|
|
3222
|
+
false
|
|
3223
|
+
);
|
|
3224
|
+
if (referenceFocal) this.primaryFocalLabels.set(node, 18);
|
|
3225
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
3226
|
+
} else {
|
|
3227
|
+
const node = this.addCentredLabel(this.bgLayer, obj.label, cx, cy, "#cbd5e1", 16, true);
|
|
3228
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
3229
|
+
}
|
|
2692
3230
|
}
|
|
2693
3231
|
}
|
|
2694
3232
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
2695
|
-
addStageLabel(x, y, text) {
|
|
3233
|
+
addStageLabel(x, y, text, background) {
|
|
3234
|
+
const ink = stateAwareBookableLabelInk(background, "#e6e9f0");
|
|
2696
3235
|
const t2 = new import_Text.Text({
|
|
2697
3236
|
x,
|
|
2698
3237
|
y,
|
|
@@ -2701,22 +3240,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2701
3240
|
fontStyle: "700",
|
|
2702
3241
|
letterSpacing: 4,
|
|
2703
3242
|
fontFamily: this.labelFont(),
|
|
2704
|
-
fill:
|
|
3243
|
+
fill: ink,
|
|
2705
3244
|
listening: false,
|
|
2706
3245
|
perfectDrawEnabled: false
|
|
2707
3246
|
});
|
|
2708
3247
|
t2.offsetX(t2.width() / 2);
|
|
2709
3248
|
t2.offsetY(t2.height() / 2);
|
|
2710
3249
|
this.bgLayer.add(t2);
|
|
3250
|
+
return t2;
|
|
2711
3251
|
}
|
|
2712
3252
|
renderGA(obj) {
|
|
2713
3253
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
|
|
3254
|
+
const canvas = this.canvasBackground;
|
|
3255
|
+
const effectiveBackground = compositeHexOver(color, canvas, GA_FILL_OPACITY);
|
|
3256
|
+
const preferredInk = this.theme.textColor ?? "#e6e9f0";
|
|
3257
|
+
const ink = stateAwareBookableLabelInk(effectiveBackground, preferredInk);
|
|
3258
|
+
const poly = polygonWithHolesShape(obj.points, obj.holes, {
|
|
2718
3259
|
fill: color,
|
|
2719
|
-
opacity:
|
|
3260
|
+
opacity: GA_FILL_OPACITY,
|
|
2720
3261
|
stroke: color,
|
|
2721
3262
|
strokeWidth: 1.5
|
|
2722
3263
|
});
|
|
@@ -2729,31 +3270,51 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2729
3270
|
this.container.style.cursor = "default";
|
|
2730
3271
|
});
|
|
2731
3272
|
this.bgLayer.add(poly);
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
2734
|
-
this.addCentredLabel(this.bgLayer, obj.label,
|
|
2735
|
-
this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`,
|
|
3273
|
+
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3274
|
+
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3275
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3276
|
+
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3277
|
+
this.freeTextById.set(`${obj.id}:label`, {
|
|
3278
|
+
objectId: obj.id,
|
|
3279
|
+
node: label,
|
|
3280
|
+
background: effectiveBackground,
|
|
3281
|
+
kind: "ga-label",
|
|
3282
|
+
categoryKey: obj.categoryKey
|
|
3283
|
+
});
|
|
3284
|
+
this.freeTextById.set(`${obj.id}:capacity`, {
|
|
3285
|
+
objectId: obj.id,
|
|
3286
|
+
node: capacity,
|
|
3287
|
+
background: effectiveBackground,
|
|
3288
|
+
kind: "ga-capacity",
|
|
3289
|
+
categoryKey: obj.categoryKey
|
|
3290
|
+
});
|
|
3291
|
+
this.gaById.set(obj.id, {
|
|
3292
|
+
label: obj.label,
|
|
3293
|
+
capacity: obj.capacity,
|
|
3294
|
+
categoryKey: obj.categoryKey,
|
|
3295
|
+
points: obj.points,
|
|
3296
|
+
polygon: poly,
|
|
3297
|
+
effectiveBackground,
|
|
3298
|
+
...containingSection ? { sectionId: containingSection.logicalId } : {}
|
|
3299
|
+
});
|
|
2736
3300
|
}
|
|
2737
3301
|
/**
|
|
2738
3302
|
* A section renders in three coordinated layers driven by the LOD melt:
|
|
2739
3303
|
* • a faint outline (the existing near-zoom look, untouched),
|
|
2740
|
-
* • a solid
|
|
2741
|
-
* •
|
|
2742
|
-
*
|
|
2743
|
-
*
|
|
3304
|
+
* • a neutral solid shell that fades in at the overview rung, and
|
|
3305
|
+
* • one readable, contained section name.
|
|
3306
|
+
* Category, row, seat, and availability detail belongs to section focus/zoom.
|
|
3307
|
+
* Membership and category mix are still precomputed for the detailed state.
|
|
2744
3308
|
*/
|
|
2745
3309
|
renderSection(obj) {
|
|
2746
|
-
const
|
|
2747
|
-
const
|
|
2748
|
-
x: obj.outline.reduce((a, p) => a + p.x, 0) / obj.outline.length,
|
|
2749
|
-
y: obj.outline.reduce((a, p) => a + p.y, 0) / obj.outline.length
|
|
2750
|
-
};
|
|
3310
|
+
const centroid = polygonLabelPoint(obj.outline, obj.holes);
|
|
3311
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2751
3312
|
const memberIds = [];
|
|
2752
3313
|
const catCounts = /* @__PURE__ */ new Map();
|
|
2753
3314
|
let free = 0;
|
|
2754
3315
|
for (const seat of this.seats) {
|
|
2755
3316
|
if (this.seatSection.has(seat.id)) continue;
|
|
2756
|
-
if (!
|
|
3317
|
+
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
2757
3318
|
memberIds.push(seat.id);
|
|
2758
3319
|
catCounts.set(seat.categoryKey, (catCounts.get(seat.categoryKey) ?? 0) + 1);
|
|
2759
3320
|
if ((this.statusById.get(seat.id) ?? "free") === "free") free++;
|
|
@@ -2789,65 +3350,34 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2789
3350
|
}
|
|
2790
3351
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
2791
3352
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
2792
|
-
const outlinePoly =
|
|
2793
|
-
points: pts,
|
|
2794
|
-
closed: true,
|
|
3353
|
+
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
2795
3354
|
stroke: rgba(outlineTint, 0.5),
|
|
2796
3355
|
strokeWidth: 1.75,
|
|
2797
3356
|
fill: rgba(outlineTint, 0.08),
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
perfectDrawEnabled: false
|
|
2801
|
-
});
|
|
3357
|
+
listening: false
|
|
3358
|
+
}, obj.outlinePath);
|
|
2802
3359
|
bgTarget.add(outlinePoly);
|
|
2803
|
-
const blockPoly =
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
stroke: rgba("#ffffff", 0.12),
|
|
2808
|
-
strokeWidth: 1,
|
|
3360
|
+
const blockPoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3361
|
+
fill: palette.sectionFill,
|
|
3362
|
+
stroke: palette.sectionStroke,
|
|
3363
|
+
strokeWidth: SECTION_STROKE_PX,
|
|
2809
3364
|
opacity: 0,
|
|
2810
|
-
listening: false
|
|
2811
|
-
|
|
2812
|
-
});
|
|
3365
|
+
listening: false
|
|
3366
|
+
}, obj.outlinePath);
|
|
2813
3367
|
bgTarget.add(blockPoly);
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
if (!s) continue;
|
|
2818
|
-
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
2819
|
-
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
2820
|
-
}
|
|
2821
|
-
const rowLines = [];
|
|
2822
|
-
for (const arr of rowSeats.values()) {
|
|
2823
|
-
if (arr.length < 2) continue;
|
|
2824
|
-
arr.sort((a, b) => a.i - b.i);
|
|
2825
|
-
const line = new import_Line.Line({
|
|
2826
|
-
points: arr.flatMap((p) => [p.x, p.y]),
|
|
2827
|
-
stroke: rgba("#ffffff", 0.34),
|
|
2828
|
-
strokeWidth: SEAT_RADIUS * 0.55,
|
|
2829
|
-
lineCap: "round",
|
|
2830
|
-
lineJoin: "round",
|
|
2831
|
-
opacity: 0,
|
|
2832
|
-
listening: false,
|
|
2833
|
-
perfectDrawEnabled: false
|
|
2834
|
-
});
|
|
2835
|
-
rowLines.push(line);
|
|
2836
|
-
bgTarget.add(line);
|
|
2837
|
-
}
|
|
3368
|
+
const labelStyle = obj.labelPresentation?.labelStyle;
|
|
3369
|
+
const preferredInk = labelStyle?.color ?? palette.sectionInk;
|
|
3370
|
+
const labelScale = (labelStyle?.size ?? 18) / 18;
|
|
2838
3371
|
const nameLabel = new import_Text.Text({
|
|
2839
|
-
x: centroid.x,
|
|
2840
|
-
y: centroid.y,
|
|
2841
|
-
text: obj.label,
|
|
2842
|
-
|
|
3372
|
+
x: obj.labelPresentation?.position?.x ?? centroid.x,
|
|
3373
|
+
y: obj.labelPresentation?.position?.y ?? centroid.y,
|
|
3374
|
+
text: obj.displayLabel ?? obj.label,
|
|
3375
|
+
rotation: obj.labelPresentation?.rotation ?? 0,
|
|
3376
|
+
visible: obj.labelPresentation?.visible !== false,
|
|
3377
|
+
fontSize: 22 * labelScale,
|
|
2843
3378
|
fontStyle: "700",
|
|
2844
3379
|
fontFamily: this.labelFont(),
|
|
2845
|
-
fill:
|
|
2846
|
-
// Dark halo so the label reads over the seat dots at any zoom.
|
|
2847
|
-
shadowColor: "#05070c",
|
|
2848
|
-
shadowBlur: 6,
|
|
2849
|
-
shadowOpacity: 0.9,
|
|
2850
|
-
shadowForStrokeEnabled: false,
|
|
3380
|
+
fill: stateAwareBookableLabelInk(palette.sectionFill, preferredInk),
|
|
2851
3381
|
listening: false,
|
|
2852
3382
|
perfectDrawEnabled: false
|
|
2853
3383
|
});
|
|
@@ -2861,11 +3391,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2861
3391
|
fontSize: 12,
|
|
2862
3392
|
fontStyle: "700",
|
|
2863
3393
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2864
|
-
fill:
|
|
2865
|
-
shadowColor: "#05070c",
|
|
2866
|
-
shadowBlur: 5,
|
|
2867
|
-
shadowOpacity: 0.9,
|
|
2868
|
-
shadowForStrokeEnabled: false,
|
|
3394
|
+
fill: palette.sectionInk,
|
|
2869
3395
|
opacity: 0,
|
|
2870
3396
|
listening: false,
|
|
2871
3397
|
perfectDrawEnabled: false
|
|
@@ -2874,9 +3400,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2874
3400
|
bgTarget.add(subLabel);
|
|
2875
3401
|
const sec = {
|
|
2876
3402
|
id: obj.id,
|
|
2877
|
-
|
|
3403
|
+
logicalId: obj.logicalSectionId ?? obj.id,
|
|
3404
|
+
label: obj.displayLabel ?? obj.label,
|
|
2878
3405
|
outline: obj.outline,
|
|
3406
|
+
...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
|
|
3407
|
+
holes: obj.holes ?? [],
|
|
2879
3408
|
centroid,
|
|
3409
|
+
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
|
|
2880
3410
|
zone: obj.zone,
|
|
2881
3411
|
memberIds,
|
|
2882
3412
|
total: memberIds.length,
|
|
@@ -2885,9 +3415,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2885
3415
|
outlineTint,
|
|
2886
3416
|
outlinePoly,
|
|
2887
3417
|
blockPoly,
|
|
2888
|
-
rowLines,
|
|
2889
3418
|
nameLabel,
|
|
2890
3419
|
subLabel,
|
|
3420
|
+
preferredInk,
|
|
3421
|
+
labelScale,
|
|
3422
|
+
nameLabelFits: true,
|
|
3423
|
+
subLabelFits: true,
|
|
2891
3424
|
elevation,
|
|
2892
3425
|
liftGroupBg,
|
|
2893
3426
|
liftGroupSeat,
|
|
@@ -2899,7 +3432,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2899
3432
|
this.sections.push(sec);
|
|
2900
3433
|
}
|
|
2901
3434
|
refreshSectionHeat(sec) {
|
|
2902
|
-
const raw = this.sectionHeat.get(sec.id) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
3435
|
+
const raw = this.sectionHeat.get(sec.id) ?? this.sectionHeat.get(sec.logicalId) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
2903
3436
|
if (raw == null || raw <= 0) {
|
|
2904
3437
|
sec.outlinePoly.stroke(rgba(sec.outlineTint, 0.5));
|
|
2905
3438
|
sec.outlinePoly.fill(rgba(sec.outlineTint, 0.08));
|
|
@@ -2915,7 +3448,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2915
3448
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
2916
3449
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
2917
3450
|
}
|
|
2918
|
-
/** Recompute a section's
|
|
3451
|
+
/** Recompute a section's neutral overview state and retained detail count. */
|
|
2919
3452
|
refreshSectionFill(sec) {
|
|
2920
3453
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2921
3454
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
@@ -2923,25 +3456,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2923
3456
|
}
|
|
2924
3457
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2925
3458
|
isSectionClosed(sec) {
|
|
2926
|
-
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
3459
|
+
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2927
3460
|
}
|
|
2928
|
-
/**
|
|
2929
|
-
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2930
|
-
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2931
|
-
* when another section holds focus (AXS dim treatment).
|
|
2932
|
-
*/
|
|
3461
|
+
/** Clean overview shells never leak category, price, or live availability paint. */
|
|
2933
3462
|
sectionBlockFill(sec) {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
fill = CLOSED_SECTION_FILL;
|
|
2937
|
-
} else {
|
|
2938
|
-
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2939
|
-
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2940
|
-
}
|
|
2941
|
-
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2942
|
-
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2943
|
-
}
|
|
2944
|
-
return fill;
|
|
3463
|
+
const fill = overviewPalette(this.canvasBackground).sectionFill;
|
|
3464
|
+
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
2945
3465
|
}
|
|
2946
3466
|
/**
|
|
2947
3467
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -2970,19 +3490,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2970
3490
|
if (typeof p === "number" && p < minPrice) minPrice = p;
|
|
2971
3491
|
}
|
|
2972
3492
|
}
|
|
3493
|
+
const back = new import_Rect.Rect({
|
|
3494
|
+
x: cx,
|
|
3495
|
+
y: cy,
|
|
3496
|
+
width: 1,
|
|
3497
|
+
height: 1,
|
|
3498
|
+
offsetX: 0.5,
|
|
3499
|
+
offsetY: 0.5,
|
|
3500
|
+
cornerRadius: 1,
|
|
3501
|
+
fill: HIERARCHY_PILL_BACKGROUND,
|
|
3502
|
+
stroke: z.color ?? anchor.outlineTint,
|
|
3503
|
+
strokeWidth: 1,
|
|
3504
|
+
opacity: 0,
|
|
3505
|
+
listening: false,
|
|
3506
|
+
perfectDrawEnabled: false
|
|
3507
|
+
});
|
|
3508
|
+
this.bgLayer.add(back);
|
|
2973
3509
|
const label = new import_Text.Text({
|
|
2974
3510
|
x: cx,
|
|
2975
3511
|
y: cy,
|
|
2976
3512
|
text: z.label.toUpperCase(),
|
|
2977
|
-
fontSize:
|
|
3513
|
+
fontSize: ZONE_LABEL_PX,
|
|
2978
3514
|
fontStyle: "800",
|
|
2979
|
-
letterSpacing:
|
|
3515
|
+
letterSpacing: 0.5,
|
|
2980
3516
|
fontFamily: this.labelFont(),
|
|
2981
|
-
fill:
|
|
2982
|
-
shadowColor: "#05070c",
|
|
2983
|
-
shadowBlur: 10,
|
|
2984
|
-
shadowOpacity: 0.95,
|
|
2985
|
-
shadowForStrokeEnabled: false,
|
|
3517
|
+
fill: "#f4f6fb",
|
|
2986
3518
|
opacity: 0,
|
|
2987
3519
|
listening: false,
|
|
2988
3520
|
perfectDrawEnabled: false
|
|
@@ -2999,7 +3531,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2999
3531
|
fontSize: 14,
|
|
3000
3532
|
fontStyle: "600",
|
|
3001
3533
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
3002
|
-
fill:
|
|
3534
|
+
fill: "#cbd5e1",
|
|
3003
3535
|
opacity: 0,
|
|
3004
3536
|
listening: false,
|
|
3005
3537
|
perfectDrawEnabled: false
|
|
@@ -3007,7 +3539,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3007
3539
|
sub.offsetX(sub.width() / 2);
|
|
3008
3540
|
this.bgLayer.add(sub);
|
|
3009
3541
|
}
|
|
3010
|
-
this.zones.push({
|
|
3542
|
+
this.zones.push({
|
|
3543
|
+
id: z.id,
|
|
3544
|
+
anchor: { x: cx, y: cy },
|
|
3545
|
+
back,
|
|
3546
|
+
background: HIERARCHY_PILL_BACKGROUND,
|
|
3547
|
+
label,
|
|
3548
|
+
sub
|
|
3549
|
+
});
|
|
3011
3550
|
}
|
|
3012
3551
|
}
|
|
3013
3552
|
/**
|
|
@@ -3029,74 +3568,106 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3029
3568
|
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
3030
3569
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
3031
3570
|
}
|
|
3571
|
+
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3572
|
+
if (sectionOverview) blockT = 1;
|
|
3032
3573
|
if (!this.zones.length) zoneT = 0;
|
|
3033
|
-
this.seatLayer.opacity(1 - blockT);
|
|
3574
|
+
this.seatLayer.opacity(sectionOverview ? 0 : 1 - blockT);
|
|
3034
3575
|
const sx = this.stage.scaleX();
|
|
3035
3576
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
3036
3577
|
if (rescale) this.lodScale = scale;
|
|
3037
3578
|
const focus = this.focusedSectionId;
|
|
3579
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3580
|
+
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3038
3581
|
for (const sec of this.sections) {
|
|
3039
|
-
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3582
|
+
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3583
|
+
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3040
3584
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3041
|
-
|
|
3042
|
-
sec.
|
|
3043
|
-
sec.
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3585
|
+
sec.blockPoly.stroke(palette.sectionStroke);
|
|
3586
|
+
sec.blockPoly.strokeWidth(SECTION_STROKE_PX / Math.max(sx, 1e-4));
|
|
3587
|
+
const sectionFill = sec.blockPoly.fill();
|
|
3588
|
+
const sectionInk = stateAwareBookableLabelInk(
|
|
3589
|
+
typeof sectionFill === "string" ? sectionFill : sec.baseFill,
|
|
3590
|
+
sec.preferredInk
|
|
3591
|
+
);
|
|
3592
|
+
sec.nameLabel.fill(sectionInk);
|
|
3593
|
+
sec.subLabel.fill(sectionInk);
|
|
3594
|
+
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
3595
|
+
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3596
|
+
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3597
|
+
sec.subLabel.opacity(0);
|
|
3049
3598
|
}
|
|
3050
3599
|
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
3051
3600
|
for (const zone of this.zones) {
|
|
3601
|
+
zone.back.opacity(zoneOpacity);
|
|
3052
3602
|
zone.label.opacity(zoneOpacity);
|
|
3053
3603
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3054
|
-
if (rescale)
|
|
3055
|
-
const cy = zone.label.y();
|
|
3056
|
-
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
3057
|
-
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
3058
|
-
}
|
|
3604
|
+
if (rescale) this.sizeZonePill(zone, sx);
|
|
3059
3605
|
}
|
|
3060
3606
|
this.decollideRungLabels(sx);
|
|
3607
|
+
this.dedupeLogicalSectionLabels();
|
|
3608
|
+
for (const zone of this.zones) {
|
|
3609
|
+
const opacity = zone.label.opacity();
|
|
3610
|
+
zone.back.opacity(opacity);
|
|
3611
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
3612
|
+
}
|
|
3061
3613
|
this.bgLayer.batchDraw();
|
|
3062
3614
|
}
|
|
3615
|
+
/** One semantic section gets one overview label, even across split contours. */
|
|
3616
|
+
dedupeLogicalSectionLabels() {
|
|
3617
|
+
const byLogical = /* @__PURE__ */ new Map();
|
|
3618
|
+
for (const section of this.sections) {
|
|
3619
|
+
(byLogical.get(section.logicalId) ?? byLogical.set(section.logicalId, []).get(section.logicalId)).push(section);
|
|
3620
|
+
}
|
|
3621
|
+
for (const components of byLogical.values()) {
|
|
3622
|
+
if (components.length < 2) continue;
|
|
3623
|
+
const visible = components.filter((component) => component.nameLabel.opacity() > 0.05).sort((left, right) => {
|
|
3624
|
+
const leftBounds = polyBounds(left.outline);
|
|
3625
|
+
const rightBounds = polyBounds(right.outline);
|
|
3626
|
+
return rightBounds.width * rightBounds.height - leftBounds.width * leftBounds.height;
|
|
3627
|
+
});
|
|
3628
|
+
for (const component of visible.slice(1)) component.nameLabel.opacity(0);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3063
3631
|
/**
|
|
3064
|
-
*
|
|
3065
|
-
*
|
|
3066
|
-
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
3067
|
-
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
3068
|
-
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
3069
|
-
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
3632
|
+
* Keep transitional zone pills from covering section names. Section names
|
|
3633
|
+
* are already proven inside disjoint shells, so they must not cull each other.
|
|
3070
3634
|
*/
|
|
3071
3635
|
decollideRungLabels(sx) {
|
|
3072
3636
|
const GAP = 4;
|
|
3073
3637
|
const cands = [];
|
|
3074
3638
|
const boxOf = (t2) => {
|
|
3075
3639
|
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3640
|
+
const rotated = pointsBounds(rotatedRectPoints(
|
|
3641
|
+
{ x: 0, y: 0 },
|
|
3642
|
+
t2.width() * sx,
|
|
3643
|
+
t2.height() * sx,
|
|
3644
|
+
t2.rotation()
|
|
3645
|
+
));
|
|
3646
|
+
const w = rotated.width;
|
|
3647
|
+
const h = rotated.height;
|
|
3078
3648
|
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
3079
3649
|
};
|
|
3080
3650
|
for (const zone of this.zones) {
|
|
3081
|
-
if (zone.label.opacity() > 0.05)
|
|
3082
|
-
|
|
3651
|
+
if (zone.label.opacity() > 0.05) {
|
|
3652
|
+
const p = this.worldToScreen(zone.anchor);
|
|
3653
|
+
const w = zone.back.width() * sx;
|
|
3654
|
+
const h = zone.back.height() * sx;
|
|
3655
|
+
cands.push({ node: zone.label, tier: 0, section: false, box: { x: p.x - w / 2, y: p.y - h / 2, w, h } });
|
|
3656
|
+
}
|
|
3083
3657
|
}
|
|
3084
3658
|
for (const sec of this.sections) {
|
|
3085
|
-
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
3086
|
-
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
3659
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, section: true, box: boxOf(sec.nameLabel) });
|
|
3087
3660
|
}
|
|
3088
3661
|
if (cands.length < 2) return;
|
|
3089
3662
|
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
3090
3663
|
const kept = [];
|
|
3091
|
-
const culled = /* @__PURE__ */ new Set();
|
|
3092
3664
|
const collides = (b) => kept.some(
|
|
3093
3665
|
(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
|
|
3094
3666
|
);
|
|
3095
3667
|
for (const c of cands) {
|
|
3096
|
-
if (
|
|
3668
|
+
if (collides(c.box)) {
|
|
3097
3669
|
c.node.opacity(0);
|
|
3098
|
-
|
|
3099
|
-
} else {
|
|
3670
|
+
} else if (!c.section) {
|
|
3100
3671
|
kept.push(c.box);
|
|
3101
3672
|
}
|
|
3102
3673
|
}
|
|
@@ -3108,6 +3679,54 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3108
3679
|
t2.offsetY(t2.height() / 2);
|
|
3109
3680
|
t2.y(y);
|
|
3110
3681
|
}
|
|
3682
|
+
/** Fit one centred section name, rotating narrow shells like the target chart. */
|
|
3683
|
+
fitSectionRungLabels(sec, sx) {
|
|
3684
|
+
const paddingPx = 8;
|
|
3685
|
+
sec.subLabelFits = false;
|
|
3686
|
+
const maxPx = SECTION_LABEL_PX * sec.labelScale;
|
|
3687
|
+
const minPx = MIN_SECTION_LABEL_PX * sec.labelScale;
|
|
3688
|
+
for (let fontPx = maxPx; fontPx >= minPx; fontPx -= 1) {
|
|
3689
|
+
for (const rotation of [0, -90]) {
|
|
3690
|
+
sec.nameLabel.rotation(rotation);
|
|
3691
|
+
this.sizeLabel(sec.nameLabel, fontPx / sx, sec.nameLabel.y());
|
|
3692
|
+
for (const anchor of sec.labelAnchors) {
|
|
3693
|
+
sec.nameLabel.position(anchor);
|
|
3694
|
+
const paddingWorld = paddingPx / sx;
|
|
3695
|
+
if (rotatedRectFitsPolygon(
|
|
3696
|
+
anchor,
|
|
3697
|
+
sec.nameLabel.width() + paddingWorld,
|
|
3698
|
+
sec.nameLabel.height() + paddingWorld,
|
|
3699
|
+
rotation,
|
|
3700
|
+
sec.outline,
|
|
3701
|
+
sec.holes
|
|
3702
|
+
)) {
|
|
3703
|
+
sec.nameLabelFits = true;
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
sec.nameLabel.position(sec.centroid);
|
|
3710
|
+
sec.nameLabel.rotation(0);
|
|
3711
|
+
sec.nameLabelFits = false;
|
|
3712
|
+
}
|
|
3713
|
+
/** Size one screen-constant zone name/price pill around its shared anchor. */
|
|
3714
|
+
sizeZonePill(zone, sx) {
|
|
3715
|
+
const padX = 10 / sx;
|
|
3716
|
+
const padY = 6 / sx;
|
|
3717
|
+
const gap = zone.sub ? 3 / sx : 0;
|
|
3718
|
+
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, zone.anchor.y);
|
|
3719
|
+
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, zone.anchor.y);
|
|
3720
|
+
const width = Math.max(zone.label.width(), zone.sub?.width() ?? 0) + padX * 2;
|
|
3721
|
+
const height = zone.label.height() + (zone.sub ? gap + zone.sub.height() : 0) + padY * 2;
|
|
3722
|
+
zone.label.y(zone.anchor.y - (zone.sub ? (gap + zone.sub.height()) / 2 : 0));
|
|
3723
|
+
if (zone.sub) zone.sub.y(zone.anchor.y + (zone.label.height() + gap) / 2);
|
|
3724
|
+
zone.back.position(zone.anchor);
|
|
3725
|
+
zone.back.size({ width, height });
|
|
3726
|
+
zone.back.offset({ x: width / 2, y: height / 2 });
|
|
3727
|
+
zone.back.cornerRadius(7 / sx);
|
|
3728
|
+
zone.back.strokeWidth(1 / sx);
|
|
3729
|
+
}
|
|
3111
3730
|
/**
|
|
3112
3731
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
3113
3732
|
* stage (scale/pos) and, in iso view, the iso affine — so screen-space taps
|
|
@@ -3122,12 +3741,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3122
3741
|
sectionAt(clientPoint) {
|
|
3123
3742
|
if (!this.sections.length) return null;
|
|
3124
3743
|
const world = this.screenToWorld(clientPoint);
|
|
3125
|
-
const hit = this.sections.find((sec) =>
|
|
3126
|
-
return hit ? hit.
|
|
3744
|
+
const hit = this.sections.find((sec) => pointInPolygonWithHoles(world, sec.outline, sec.holes));
|
|
3745
|
+
return hit ? hit.logicalId : null;
|
|
3127
3746
|
}
|
|
3128
3747
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
3129
3748
|
sectionMembers(id) {
|
|
3130
|
-
return this.sections.
|
|
3749
|
+
return [...new Set(this.sections.filter((section) => section.id === id || section.logicalId === id || section.zone === id).flatMap((section) => section.memberIds))];
|
|
3131
3750
|
}
|
|
3132
3751
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
3133
3752
|
const t2 = new import_Text.Text({
|
|
@@ -3144,6 +3763,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3144
3763
|
t2.offsetX(t2.width() / 2);
|
|
3145
3764
|
t2.offsetY(t2.height() / 2);
|
|
3146
3765
|
layer.add(t2);
|
|
3766
|
+
return t2;
|
|
3147
3767
|
}
|
|
3148
3768
|
// ---- selection ------------------------------------------------------------
|
|
3149
3769
|
isSelectable(id) {
|
|
@@ -3177,21 +3797,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3177
3797
|
* the container's computed CSS background (walking up past transparent
|
|
3178
3798
|
* ancestors). Unknown/unparseable backgrounds keep the dark default.
|
|
3179
3799
|
*/
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
if (
|
|
3184
|
-
let
|
|
3185
|
-
while (
|
|
3186
|
-
const
|
|
3187
|
-
if (
|
|
3188
|
-
|
|
3189
|
-
break;
|
|
3190
|
-
}
|
|
3191
|
-
el = el.parentElement;
|
|
3800
|
+
resolveCanvasBackground() {
|
|
3801
|
+
const themed = this.theme.background ? opaqueColorHex(this.theme.background) : null;
|
|
3802
|
+
if (themed) return themed;
|
|
3803
|
+
if (typeof getComputedStyle === "function") {
|
|
3804
|
+
let element = this.container;
|
|
3805
|
+
while (element) {
|
|
3806
|
+
const resolved = opaqueColorHex(getComputedStyle(element).backgroundColor);
|
|
3807
|
+
if (resolved) return resolved;
|
|
3808
|
+
element = element.parentElement;
|
|
3192
3809
|
}
|
|
3193
3810
|
}
|
|
3194
|
-
return
|
|
3811
|
+
return DEF_CANVAS_BACKGROUND;
|
|
3812
|
+
}
|
|
3813
|
+
resolveSelectionColor() {
|
|
3814
|
+
if (this.theme.selectionColor) return this.theme.selectionColor;
|
|
3815
|
+
return isLightColor(this.canvasBackground) ? DEF_SELECTION_ON_LIGHT : DEF_SELECTION;
|
|
3195
3816
|
}
|
|
3196
3817
|
setSelected(id, on, silent = false) {
|
|
3197
3818
|
const c = this.circleById.get(id);
|
|
@@ -3217,6 +3838,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3217
3838
|
const candidate = this.selectionFocusId === id;
|
|
3218
3839
|
const dims = this.boothDims.get(seat.rowId);
|
|
3219
3840
|
const marker = new import_Group.Group({
|
|
3841
|
+
name: "selection-ring",
|
|
3220
3842
|
x: seat.x,
|
|
3221
3843
|
y: seat.y,
|
|
3222
3844
|
rotation: dims?.rotation ?? 0,
|
|
@@ -3224,6 +3846,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3224
3846
|
perfectDrawEnabled: false,
|
|
3225
3847
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
3226
3848
|
});
|
|
3849
|
+
marker.setAttr("seatId", id);
|
|
3227
3850
|
const common = {
|
|
3228
3851
|
stroke: this.effSelection,
|
|
3229
3852
|
listening: false,
|
|
@@ -3300,10 +3923,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3300
3923
|
* whether the section already fills the viewport (small container) so the tap
|
|
3301
3924
|
* must fall through and pick.
|
|
3302
3925
|
*/
|
|
3926
|
+
sectionBounds(id) {
|
|
3927
|
+
const bounds = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => polyBounds(section.outline));
|
|
3928
|
+
if (!bounds.length) return null;
|
|
3929
|
+
const left = Math.min(...bounds.map((box) => box.x));
|
|
3930
|
+
const top = Math.min(...bounds.map((box) => box.y));
|
|
3931
|
+
const right = Math.max(...bounds.map((box) => box.x + box.width));
|
|
3932
|
+
const bottom = Math.max(...bounds.map((box) => box.y + box.height));
|
|
3933
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
3934
|
+
}
|
|
3303
3935
|
sectionFrameScale(id) {
|
|
3304
|
-
const
|
|
3305
|
-
if (!
|
|
3306
|
-
const b = polyBounds(sec.outline);
|
|
3936
|
+
const b = this.sectionBounds(id);
|
|
3937
|
+
if (!b) return this.stage.scaleX();
|
|
3307
3938
|
const w = this.stage.width();
|
|
3308
3939
|
const h = this.stage.height();
|
|
3309
3940
|
const { min, max } = this.zoomBounds();
|
|
@@ -3333,11 +3964,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3333
3964
|
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
3334
3965
|
const sec = this.seatSection.get(id);
|
|
3335
3966
|
if (sec) {
|
|
3336
|
-
const alreadyFocused = this.focusedSectionId === sec.
|
|
3337
|
-
const canZoomInFurther = this.sectionFrameScale(sec.
|
|
3967
|
+
const alreadyFocused = this.focusedSectionId === sec.logicalId;
|
|
3968
|
+
const canZoomInFurther = this.sectionFrameScale(sec.logicalId) > this.stage.scaleX() * 1.02;
|
|
3338
3969
|
if (!alreadyFocused && canZoomInFurther) {
|
|
3339
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.
|
|
3340
|
-
else this.focusSection(sec.
|
|
3970
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.logicalId);
|
|
3971
|
+
else this.focusSection(sec.logicalId);
|
|
3341
3972
|
return;
|
|
3342
3973
|
}
|
|
3343
3974
|
}
|
|
@@ -3416,10 +4047,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3416
4047
|
}
|
|
3417
4048
|
if (this.sections.length) {
|
|
3418
4049
|
const world = this.screenToWorld(pointer);
|
|
3419
|
-
const hit = this.sections.find((sn) =>
|
|
4050
|
+
const hit = this.sections.find((sn) => pointInPolygonWithHoles(world, sn.outline, sn.holes));
|
|
3420
4051
|
if (hit) {
|
|
3421
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.
|
|
3422
|
-
else this.focusRegion(hit.
|
|
4052
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.logicalId);
|
|
4053
|
+
else this.focusRegion(hit.logicalId);
|
|
3423
4054
|
return;
|
|
3424
4055
|
}
|
|
3425
4056
|
}
|
|
@@ -3505,10 +4136,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3505
4136
|
* newer glide cancels an in-flight one.
|
|
3506
4137
|
*/
|
|
3507
4138
|
focusRegion(target, opts) {
|
|
3508
|
-
const b = typeof target === "string" ? (
|
|
3509
|
-
const sec = this.sections.find((s) => s.id === target);
|
|
3510
|
-
return sec ? polyBounds(sec.outline) : null;
|
|
3511
|
-
})() : target;
|
|
4139
|
+
const b = typeof target === "string" ? this.sectionBounds(target) : target;
|
|
3512
4140
|
if (!b) return;
|
|
3513
4141
|
this.cancelGlide();
|
|
3514
4142
|
if (opts?.animate === false || this.reducedMotion) {
|
|
@@ -3568,25 +4196,343 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3568
4196
|
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
3569
4197
|
return "sections";
|
|
3570
4198
|
}
|
|
3571
|
-
|
|
4199
|
+
getRenderedQualityEvidence() {
|
|
4200
|
+
const effectiveScale = this.effScale();
|
|
4201
|
+
const stageScale = this.stage.scaleX();
|
|
4202
|
+
const viewport = { width: this.stage.width(), height: this.stage.height() };
|
|
4203
|
+
const rounded = (value) => Math.round(value * 100) / 100;
|
|
4204
|
+
const labels = this.seats.map((seat) => {
|
|
4205
|
+
const shape = this.circleById.get(seat.id);
|
|
4206
|
+
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
4207
|
+
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE;
|
|
4208
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * effectiveScale);
|
|
4209
|
+
const screen = this.worldToScreen(seat);
|
|
4210
|
+
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4211
|
+
const opacity = shape?.opacity() ?? 0;
|
|
4212
|
+
const section = this.seatSection.get(seat.id);
|
|
4213
|
+
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4214
|
+
let hiddenReason;
|
|
4215
|
+
if (!visible) {
|
|
4216
|
+
if (opacity < 0.5) hiddenReason = "dimmed-or-unavailable";
|
|
4217
|
+
else if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4218
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4219
|
+
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4220
|
+
else hiddenReason = "renderer-hidden";
|
|
4221
|
+
}
|
|
4222
|
+
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4223
|
+
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4224
|
+
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale : this.seatR * 2 * effectiveScale;
|
|
4225
|
+
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * stageScale : this.seatR * 2 * effectiveScale;
|
|
4226
|
+
const assistedDiameterPx = 2 * (this.seatR * effectiveScale + SEAT_TAP_SLOP_PX);
|
|
4227
|
+
const fill = shape?.fill();
|
|
4228
|
+
const ink = label?.fill();
|
|
4229
|
+
return {
|
|
4230
|
+
seatId: seat.id,
|
|
4231
|
+
label: seat.label,
|
|
4232
|
+
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
4233
|
+
categoryKey: seat.categoryKey,
|
|
4234
|
+
...section ? { sectionId: section.id } : {},
|
|
4235
|
+
...section?.zone ? { zoneId: section.zone } : {},
|
|
4236
|
+
status: this.statusById.get(seat.id) ?? "free",
|
|
4237
|
+
selected: this.selection.has(seat.id),
|
|
4238
|
+
visible,
|
|
4239
|
+
renderedFontPx,
|
|
4240
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4241
|
+
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4242
|
+
opacity: rounded(opacity),
|
|
4243
|
+
pointerTarget: {
|
|
4244
|
+
active: !this.cached && this.isSelectable(seat.id),
|
|
4245
|
+
directWidthPx: rounded(directWidthPx),
|
|
4246
|
+
directHeightPx: rounded(directHeightPx),
|
|
4247
|
+
effectiveMinimumPx: rounded(Math.max(
|
|
4248
|
+
Math.min(directWidthPx, directHeightPx),
|
|
4249
|
+
assistedDiameterPx
|
|
4250
|
+
))
|
|
4251
|
+
},
|
|
4252
|
+
screenCenter: { x: rounded(screen.x), y: rounded(screen.y) },
|
|
4253
|
+
...visible ? {
|
|
4254
|
+
screenBox: {
|
|
4255
|
+
x: rounded(screen.x - labelWidth / 2),
|
|
4256
|
+
y: rounded(screen.y - labelHeight / 2),
|
|
4257
|
+
width: rounded(labelWidth),
|
|
4258
|
+
height: rounded(labelHeight)
|
|
4259
|
+
}
|
|
4260
|
+
} : {},
|
|
4261
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4262
|
+
};
|
|
4263
|
+
});
|
|
4264
|
+
const visibleLabels = labels.filter((label) => label.visible).length;
|
|
4265
|
+
const hierarchyEvidence = (id, kind, role, node, backgroundFill, section) => {
|
|
4266
|
+
const worldCorners = rotatedRectPoints(
|
|
4267
|
+
{ x: node.x(), y: node.y() },
|
|
4268
|
+
node.width(),
|
|
4269
|
+
node.height(),
|
|
4270
|
+
node.rotation()
|
|
4271
|
+
);
|
|
4272
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen(corner)));
|
|
4273
|
+
const opacity = rounded(node.opacity());
|
|
4274
|
+
const ink = node.fill();
|
|
4275
|
+
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
4276
|
+
const visible = node.isVisible() && opacity > 0.05 && !outside;
|
|
4277
|
+
const fitsContainer = section ? rotatedRectFitsPolygon(
|
|
4278
|
+
{ x: node.x(), y: node.y() },
|
|
4279
|
+
node.width(),
|
|
4280
|
+
node.height(),
|
|
4281
|
+
node.rotation(),
|
|
4282
|
+
section.outline,
|
|
4283
|
+
section.holes
|
|
4284
|
+
) : void 0;
|
|
4285
|
+
return {
|
|
4286
|
+
id,
|
|
4287
|
+
kind,
|
|
4288
|
+
role,
|
|
4289
|
+
label: node.text(),
|
|
4290
|
+
visible,
|
|
4291
|
+
renderedFontPx: rounded(node.fontSize() * stageScale),
|
|
4292
|
+
opacity,
|
|
4293
|
+
fill: backgroundFill,
|
|
4294
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4295
|
+
...fitsContainer == null ? {} : { fitsContainer },
|
|
4296
|
+
...visible ? {
|
|
4297
|
+
screenBox: {
|
|
4298
|
+
x: rounded(screenBounds.x),
|
|
4299
|
+
y: rounded(screenBounds.y),
|
|
4300
|
+
width: rounded(screenBounds.width),
|
|
4301
|
+
height: rounded(screenBounds.height)
|
|
4302
|
+
}
|
|
4303
|
+
} : {}
|
|
4304
|
+
};
|
|
4305
|
+
};
|
|
4306
|
+
const hierarchyLabels = [
|
|
4307
|
+
...this.sections.map((section) => {
|
|
4308
|
+
const fill = section.blockPoly.fill();
|
|
4309
|
+
return hierarchyEvidence(
|
|
4310
|
+
section.id,
|
|
4311
|
+
"section",
|
|
4312
|
+
"name",
|
|
4313
|
+
section.nameLabel,
|
|
4314
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
4315
|
+
section
|
|
4316
|
+
);
|
|
4317
|
+
}),
|
|
4318
|
+
...this.sections.map((section) => {
|
|
4319
|
+
const fill = section.blockPoly.fill();
|
|
4320
|
+
return hierarchyEvidence(
|
|
4321
|
+
`${section.id}:availability`,
|
|
4322
|
+
"section",
|
|
4323
|
+
"availability",
|
|
4324
|
+
section.subLabel,
|
|
4325
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
4326
|
+
section
|
|
4327
|
+
);
|
|
4328
|
+
}),
|
|
4329
|
+
...this.zones.flatMap((zone) => [
|
|
4330
|
+
hierarchyEvidence(zone.id, "zone", "name", zone.label, zone.background),
|
|
4331
|
+
...zone.sub ? [hierarchyEvidence(`${zone.id}:price`, "zone", "price", zone.sub, zone.background)] : []
|
|
4332
|
+
])
|
|
4333
|
+
];
|
|
4334
|
+
const gaAreas = [...this.gaById].map(([areaId, ga]) => {
|
|
4335
|
+
const screenPoints = ga.points.map((point) => this.worldToScreen(point));
|
|
4336
|
+
const left = Math.min(...screenPoints.map((point) => point.x));
|
|
4337
|
+
const top = Math.min(...screenPoints.map((point) => point.y));
|
|
4338
|
+
const right = Math.max(...screenPoints.map((point) => point.x));
|
|
4339
|
+
const bottom = Math.max(...screenPoints.map((point) => point.y));
|
|
4340
|
+
const outside = right < 0 || left > viewport.width || bottom < 0 || top > viewport.height;
|
|
4341
|
+
const opacity = rounded(ga.polygon.opacity());
|
|
4342
|
+
const visible = opacity >= 0.1 && !outside;
|
|
4343
|
+
const fill = ga.polygon.fill();
|
|
4344
|
+
return {
|
|
4345
|
+
areaId,
|
|
4346
|
+
label: ga.label,
|
|
4347
|
+
capacity: ga.capacity,
|
|
4348
|
+
categoryKey: ga.categoryKey,
|
|
4349
|
+
...ga.sectionId ? { sectionId: ga.sectionId } : {},
|
|
4350
|
+
visible,
|
|
4351
|
+
interactive: ga.polygon.listening(),
|
|
4352
|
+
opacity,
|
|
4353
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4354
|
+
effectiveBackground: ga.effectiveBackground,
|
|
4355
|
+
...visible ? {
|
|
4356
|
+
screenBox: {
|
|
4357
|
+
x: rounded(left),
|
|
4358
|
+
y: rounded(top),
|
|
4359
|
+
width: rounded(right - left),
|
|
4360
|
+
height: rounded(bottom - top)
|
|
4361
|
+
}
|
|
4362
|
+
} : {}
|
|
4363
|
+
};
|
|
4364
|
+
});
|
|
4365
|
+
const freeTextLabels = [...this.freeTextById].map(([recordKey, record]) => {
|
|
4366
|
+
const { node, background, kind } = record;
|
|
4367
|
+
const point = this.worldToScreen({ x: node.x(), y: node.y() });
|
|
4368
|
+
const width = node.width() * stageScale;
|
|
4369
|
+
const height = node.height() * effectiveScale;
|
|
4370
|
+
const left = point.x - node.offsetX() * stageScale;
|
|
4371
|
+
const top = point.y - node.offsetY() * effectiveScale;
|
|
4372
|
+
const renderedFontPx = rounded(node.fontSize() * effectiveScale);
|
|
4373
|
+
const outside = left + width < 0 || left > viewport.width || top + height < 0 || top > viewport.height;
|
|
4374
|
+
const visible = node.isVisible() && !outside;
|
|
4375
|
+
const ink = node.fill();
|
|
4376
|
+
const opacity = rounded(node.getAbsoluteOpacity());
|
|
4377
|
+
let hiddenReason;
|
|
4378
|
+
if (!visible) {
|
|
4379
|
+
if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4380
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4381
|
+
else hiddenReason = "renderer-hidden";
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
objectId: record.objectId ?? recordKey,
|
|
4385
|
+
kind,
|
|
4386
|
+
text: node.text(),
|
|
4387
|
+
visible,
|
|
4388
|
+
renderedFontPx,
|
|
4389
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4390
|
+
background,
|
|
4391
|
+
opacity,
|
|
4392
|
+
...visible ? {
|
|
4393
|
+
screenBox: {
|
|
4394
|
+
x: rounded(left),
|
|
4395
|
+
y: rounded(top),
|
|
4396
|
+
width: rounded(width),
|
|
4397
|
+
height: rounded(height)
|
|
4398
|
+
}
|
|
4399
|
+
} : {},
|
|
4400
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4401
|
+
};
|
|
4402
|
+
});
|
|
4403
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
4404
|
+
const neutralSectionFills = /* @__PURE__ */ new Set([
|
|
4405
|
+
palette.sectionFill.toLowerCase(),
|
|
4406
|
+
darken(palette.sectionFill, 0.12).toLowerCase()
|
|
4407
|
+
]);
|
|
4408
|
+
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4409
|
+
return {
|
|
4410
|
+
viewport,
|
|
4411
|
+
canvasBackground: this.canvasBackground,
|
|
4412
|
+
effectiveScale: rounded(effectiveScale),
|
|
4413
|
+
rung: this.getRung(),
|
|
4414
|
+
minimumVisibleLabelPx: MIN_VISIBLE_BOOKABLE_LABEL_PX,
|
|
4415
|
+
totalLabelledBookableUnits: labels.length,
|
|
4416
|
+
visibleLabels,
|
|
4417
|
+
hiddenLabels: labels.length - visibleLabels,
|
|
4418
|
+
totalBookableUnits: labels.length + gaAreas.reduce((sum, area) => sum + area.capacity, 0),
|
|
4419
|
+
selectionRingSeatIds: this.overlayLayer.find(".selection-ring").map((node) => String(node.getAttr("seatId") ?? "")).filter(Boolean),
|
|
4420
|
+
selectionRingColor: this.effSelection,
|
|
4421
|
+
focusedSectionId: this.focusedSectionId,
|
|
4422
|
+
focusBackdropVisible: Boolean(this.focusBackdrop?.isVisible()),
|
|
4423
|
+
categoryFilterKeys: this.categoryFilter ? [...this.categoryFilter].sort() : null,
|
|
4424
|
+
overviewStyle: {
|
|
4425
|
+
visibleSectionShells: visibleSectionShells.length,
|
|
4426
|
+
categoryPaintedSectionShells: visibleSectionShells.filter((section) => {
|
|
4427
|
+
const fill = section.blockPoly.fill();
|
|
4428
|
+
return typeof fill !== "string" || !neutralSectionFills.has(fill.toLowerCase());
|
|
4429
|
+
}).length,
|
|
4430
|
+
visibleCategoryDetailOutlines: this.sections.filter((section) => section.outlinePoly.opacity() > 0.05).length,
|
|
4431
|
+
// Row-hint nodes no longer exist in the production overview scene.
|
|
4432
|
+
visibleSectionRowHints: 0,
|
|
4433
|
+
visibleSectionAvailabilityLabels: this.sections.filter((section) => section.subLabel.opacity() > 0.05).length,
|
|
4434
|
+
visibleSectionGADetails: [...this.gaById.values()].filter((area) => area.sectionId != null && area.polygon.opacity() > 0.05).length
|
|
4435
|
+
},
|
|
4436
|
+
labels,
|
|
4437
|
+
gaAreas,
|
|
4438
|
+
hierarchyLabels,
|
|
4439
|
+
freeTextLabels
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
/** Jump the camera to a rung's zoom band (glided). */
|
|
3572
4443
|
setRung(rung) {
|
|
3573
4444
|
if (rung === "zones") {
|
|
3574
4445
|
this.cancelGlide();
|
|
3575
4446
|
this.zoomToFit();
|
|
3576
4447
|
return;
|
|
3577
4448
|
}
|
|
3578
|
-
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4449
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4450
|
+
this.seatLabelTargetScale() * 1.05,
|
|
4451
|
+
SEAT_FOCUS_SCALE,
|
|
4452
|
+
CACHE_THRESHOLD * 1.3
|
|
4453
|
+
);
|
|
3579
4454
|
const w = this.stage.width();
|
|
3580
4455
|
const h = this.stage.height();
|
|
3581
|
-
const
|
|
3582
|
-
const
|
|
4456
|
+
const visible = this.getVisibleWorldRect();
|
|
4457
|
+
const viewCentre = {
|
|
4458
|
+
x: visible.x + visible.width / 2,
|
|
4459
|
+
y: visible.y + visible.height / 2
|
|
4460
|
+
};
|
|
4461
|
+
let cx = viewCentre.x;
|
|
4462
|
+
let cy = viewCentre.y;
|
|
4463
|
+
if (rung === "sections" && this.sections.length > 0) {
|
|
4464
|
+
const sectionCentres = this.sections.map((section) => {
|
|
4465
|
+
const bounds = polyBounds(section.outline);
|
|
4466
|
+
return {
|
|
4467
|
+
x: bounds.x + bounds.width / 2,
|
|
4468
|
+
y: bounds.y + bounds.height / 2
|
|
4469
|
+
};
|
|
4470
|
+
});
|
|
4471
|
+
const halfWidth = w / (target * 2);
|
|
4472
|
+
const halfHeight = h / (target * 2);
|
|
4473
|
+
const hierarchyWillBeVisible = sectionCentres.some((point) => Math.abs(point.x - viewCentre.x) <= halfWidth && Math.abs(point.y - viewCentre.y) <= halfHeight);
|
|
4474
|
+
if (!hierarchyWillBeVisible) {
|
|
4475
|
+
const nearest = sectionCentres.reduce((best, point) => {
|
|
4476
|
+
const distance = (point.x - viewCentre.x) ** 2 + (point.y - viewCentre.y) ** 2;
|
|
4477
|
+
return distance < best.distance ? { point, distance } : best;
|
|
4478
|
+
}, { point: sectionCentres[0], distance: Infinity });
|
|
4479
|
+
cx = nearest.point.x;
|
|
4480
|
+
cy = nearest.point.y;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
const seatAnchors = rung === "seats" ? this.seats.filter((seat) => seat.kind !== "booth") : [];
|
|
4484
|
+
if (seatAnchors.length > 0) {
|
|
4485
|
+
let nearest = seatAnchors[0];
|
|
4486
|
+
let nearestDistance = Infinity;
|
|
4487
|
+
for (const seat of seatAnchors) {
|
|
4488
|
+
const dx = seat.x - viewCentre.x;
|
|
4489
|
+
const dy = seat.y - viewCentre.y;
|
|
4490
|
+
const distance = dx * dx + dy * dy;
|
|
4491
|
+
if (distance < nearestDistance) {
|
|
4492
|
+
nearest = seat;
|
|
4493
|
+
nearestDistance = distance;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
cx = nearest.x;
|
|
4497
|
+
cy = nearest.y;
|
|
4498
|
+
}
|
|
3583
4499
|
const bw = w / (target * 1.12);
|
|
3584
4500
|
const bh = h / (target * 1.12);
|
|
3585
4501
|
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
3586
4502
|
}
|
|
4503
|
+
/**
|
|
4504
|
+
* The seat rung must account for labels that auto-fit inside a seat circle.
|
|
4505
|
+
* A short `A-1` remains at the normal 7u target; a table label such as
|
|
4506
|
+
* `T13-10` may fit at 4u and therefore needs a deeper camera target to reach
|
|
4507
|
+
* the same 12 CSS-pixel floor. Measurement happens only on explicit rung
|
|
4508
|
+
* navigation, never during pan/zoom frames.
|
|
4509
|
+
*/
|
|
4510
|
+
seatLabelTargetScale() {
|
|
4511
|
+
let minimumFont = BOOTH_LABEL_FONT_SIZE;
|
|
4512
|
+
const measure = new import_Text.Text({
|
|
4513
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
4514
|
+
fontStyle: "600",
|
|
4515
|
+
fontFamily: this.labelFont(),
|
|
4516
|
+
listening: false
|
|
4517
|
+
});
|
|
4518
|
+
const maxWidth = this.seatR * 2 - 3;
|
|
4519
|
+
for (const seat of this.seats) {
|
|
4520
|
+
if (seat.kind === "booth") {
|
|
4521
|
+
minimumFont = Math.min(minimumFont, BOOTH_LABEL_FONT_SIZE);
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
measure.fontSize(SEAT_LABEL_FONT_SIZE);
|
|
4525
|
+
measure.text(bookableMarkerLabel(seat.displayLabel ?? seat.label));
|
|
4526
|
+
const fitted = measure.width() > maxWidth ? Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxWidth / measure.width()) : SEAT_LABEL_FONT_SIZE;
|
|
4527
|
+
minimumFont = Math.min(minimumFont, fitted);
|
|
4528
|
+
}
|
|
4529
|
+
measure.destroy();
|
|
4530
|
+
return MIN_VISIBLE_BOOKABLE_LABEL_PX / Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, minimumFont);
|
|
4531
|
+
}
|
|
3587
4532
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
3588
4533
|
afterViewChange() {
|
|
3589
4534
|
this.updateLOD();
|
|
4535
|
+
this.updateFreeTextVisibility();
|
|
3590
4536
|
this.updateLabels();
|
|
3591
4537
|
this.scheduleViewChange();
|
|
3592
4538
|
}
|
|
@@ -3600,7 +4546,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3600
4546
|
}
|
|
3601
4547
|
updateLOD() {
|
|
3602
4548
|
const scale = this.effScale();
|
|
4549
|
+
const focalScale = Math.max(scale, 1e-4);
|
|
4550
|
+
for (const [label, targetPx] of this.primaryFocalLabels) {
|
|
4551
|
+
this.sizeLabel(label, targetPx / focalScale, label.y());
|
|
4552
|
+
}
|
|
3603
4553
|
if (this.hasSections) this.applySectionLod(scale);
|
|
4554
|
+
else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
|
|
4555
|
+
this.paintGAStateForView();
|
|
3604
4556
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
3605
4557
|
if (shouldCache && !this.cached) {
|
|
3606
4558
|
this.cacheSeatLayer();
|
|
@@ -3639,15 +4591,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3639
4591
|
if (this.recacheTimer) {
|
|
3640
4592
|
clearTimeout(this.recacheTimer);
|
|
3641
4593
|
this.recacheTimer = null;
|
|
3642
|
-
this.
|
|
4594
|
+
if (this.effScale() < CACHE_THRESHOLD) {
|
|
4595
|
+
this.rebuildSeatCache();
|
|
4596
|
+
} else if (this.cached) {
|
|
4597
|
+
this.seatLayer.clearCache();
|
|
4598
|
+
this.seatLayer.listening(true);
|
|
4599
|
+
this.cached = false;
|
|
4600
|
+
}
|
|
3643
4601
|
}
|
|
3644
4602
|
this.bgLayer.draw();
|
|
3645
4603
|
this.seatLayer.draw();
|
|
3646
4604
|
this.overlayLayer.draw();
|
|
3647
4605
|
}
|
|
4606
|
+
updateFreeTextVisibility() {
|
|
4607
|
+
const effectiveScale = this.effScale();
|
|
4608
|
+
for (const { objectId, node, categoryKey, kind } of this.freeTextById.values()) {
|
|
4609
|
+
const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
|
|
4610
|
+
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
4611
|
+
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
3648
4614
|
updateLabels() {
|
|
3649
|
-
const
|
|
4615
|
+
const effectiveScale = this.effScale();
|
|
4616
|
+
const show = effectiveScale >= LABEL_SCALE;
|
|
4617
|
+
for (const [id, label] of this.boothLabelById) {
|
|
4618
|
+
const shape = this.circleById.get(id);
|
|
4619
|
+
label.visible(
|
|
4620
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.opacity() ?? 1) >= 0.5
|
|
4621
|
+
);
|
|
4622
|
+
}
|
|
3650
4623
|
this.labelGroup.destroyChildren();
|
|
4624
|
+
this.seatLabelById.clear();
|
|
3651
4625
|
if (!show) {
|
|
3652
4626
|
this.overlayLayer.batchDraw();
|
|
3653
4627
|
return;
|
|
@@ -3661,8 +4635,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3661
4635
|
for (const seat of this.seats) {
|
|
3662
4636
|
if (seat.kind === "booth") continue;
|
|
3663
4637
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
4638
|
+
const shape = this.circleById.get(seat.id);
|
|
4639
|
+
if ((shape?.opacity() ?? 1) < 0.5) continue;
|
|
3664
4640
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
3665
|
-
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id);
|
|
4641
|
+
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
3666
4642
|
if (unavailable) {
|
|
3667
4643
|
const cue = new import_Group.Group({ x: seat.x, y: seat.y, listening: false });
|
|
3668
4644
|
if (status === "held") {
|
|
@@ -3700,23 +4676,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3700
4676
|
const t2 = new import_Text.Text({
|
|
3701
4677
|
x: seat.x,
|
|
3702
4678
|
y: seat.y,
|
|
3703
|
-
text: seat.label,
|
|
3704
|
-
fontSize:
|
|
4679
|
+
text: bookableMarkerLabel(seat.displayLabel ?? seat.label),
|
|
4680
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
3705
4681
|
fontStyle: "600",
|
|
3706
4682
|
fontFamily: this.labelFont(),
|
|
3707
|
-
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4683
|
+
fill: shape ? this.renderedBookableLabelInk(shape) : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3708
4684
|
listening: false,
|
|
3709
4685
|
perfectDrawEnabled: false
|
|
3710
4686
|
});
|
|
3711
4687
|
const maxW = this.seatR * 2 - 3;
|
|
3712
|
-
if (t2.width() > maxW) t2.fontSize(Math.max(
|
|
3713
|
-
if (t2.
|
|
4688
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxW / t2.width()));
|
|
4689
|
+
if (t2.width() > maxW + 0.01) {
|
|
4690
|
+
t2.destroy();
|
|
4691
|
+
continue;
|
|
4692
|
+
}
|
|
4693
|
+
if (!isBookableLabelLegibleAtScale(t2.fontSize(), effectiveScale)) {
|
|
3714
4694
|
t2.destroy();
|
|
3715
4695
|
continue;
|
|
3716
4696
|
}
|
|
3717
4697
|
t2.offsetX(t2.width() / 2);
|
|
3718
4698
|
t2.offsetY(t2.height() / 2);
|
|
3719
4699
|
this.labelGroup.add(t2);
|
|
4700
|
+
this.seatLabelById.set(seat.id, t2);
|
|
3720
4701
|
if (++count >= MAX_LABELS) break;
|
|
3721
4702
|
}
|
|
3722
4703
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -4991,9 +5972,12 @@ async function loadLocale(code) {
|
|
|
4991
5972
|
}
|
|
4992
5973
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4993
5974
|
0 && (module.exports = {
|
|
5975
|
+
ACCESSIBILITY_RING_COLOR,
|
|
4994
5976
|
ACCESSIBILITY_TYPES,
|
|
4995
5977
|
CHART_STORAGE_KEY,
|
|
4996
5978
|
DEFAULT_CURRENCY,
|
|
5979
|
+
LABEL_STYLE_MAX_SIZE,
|
|
5980
|
+
LABEL_STYLE_MIN_SIZE,
|
|
4997
5981
|
MAX_EVENT_INVENTORY,
|
|
4998
5982
|
MAX_GA_CAPACITY,
|
|
4999
5983
|
PickerController,
|
|
@@ -5001,6 +5985,7 @@ async function loadLocale(code) {
|
|
|
5001
5985
|
SeatmapRenderer,
|
|
5002
5986
|
UNGROUPED_ID,
|
|
5003
5987
|
accessibilityMeta,
|
|
5988
|
+
accessibilityRingColor,
|
|
5004
5989
|
allObjects,
|
|
5005
5990
|
applyHidden,
|
|
5006
5991
|
chartBounds,
|
|
@@ -5029,6 +6014,8 @@ async function loadLocale(code) {
|
|
|
5029
6014
|
loadLocale,
|
|
5030
6015
|
objectCenter,
|
|
5031
6016
|
pointInPolygon,
|
|
6017
|
+
pointInPolygonWithHoles,
|
|
6018
|
+
polygonLabelPoint,
|
|
5032
6019
|
resolveLocale,
|
|
5033
6020
|
setLocale,
|
|
5034
6021
|
setMoneyLocale,
|