@seatlayer/core 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{de-PQ5BE3PL.js → de-ZTKJOFHT.js} +3 -1
- package/dist/de-ZTKJOFHT.js.map +1 -0
- package/dist/{es-EWHL5CZR.js → es-NLD3NL3W.js} +3 -1
- package/dist/es-NLD3NL3W.js.map +1 -0
- package/dist/{fr-FIEG227H.js → fr-KSFKWSAT.js} +3 -1
- package/dist/fr-KSFKWSAT.js.map +1 -0
- package/dist/index.cjs +1248 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +411 -30
- package/dist/index.d.ts +411 -30
- package/dist/index.js +1243 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-PQ5BE3PL.js.map +0 -1
- package/dist/es-EWHL5CZR.js.map +0 -1
- package/dist/fr-FIEG227H.js.map +0 -1
package/dist/index.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",
|
|
@@ -307,6 +313,8 @@ __export(index_exports, {
|
|
|
307
313
|
loadLocale: () => loadLocale,
|
|
308
314
|
objectCenter: () => objectCenter,
|
|
309
315
|
pointInPolygon: () => pointInPolygon,
|
|
316
|
+
pointInPolygonWithHoles: () => pointInPolygonWithHoles,
|
|
317
|
+
polygonLabelPoint: () => polygonLabelPoint,
|
|
310
318
|
resolveLocale: () => resolveLocale,
|
|
311
319
|
setLocale: () => setLocale,
|
|
312
320
|
setMoneyLocale: () => setMoneyLocale,
|
|
@@ -350,6 +358,71 @@ function layerOf(obj) {
|
|
|
350
358
|
}
|
|
351
359
|
var CHART_STORAGE_KEY = "seatmap.chart";
|
|
352
360
|
|
|
361
|
+
// src/core/complexGeometry.ts
|
|
362
|
+
function cubicPoint(path, t2) {
|
|
363
|
+
const u = 1 - t2;
|
|
364
|
+
const a = u * u * u;
|
|
365
|
+
const b = 3 * u * u * t2;
|
|
366
|
+
const c = 3 * u * t2 * t2;
|
|
367
|
+
const d = t2 * t2 * t2;
|
|
368
|
+
return {
|
|
369
|
+
x: a * path.start.x + b * path.control1.x + c * path.control2.x + d * path.end.x,
|
|
370
|
+
y: a * path.start.y + b * path.control1.y + c * path.control2.y + d * path.end.y
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function distributeAlongCubic(path, count, resolution = 192) {
|
|
374
|
+
if (!Number.isInteger(count) || count < 1) throw new Error("Path point count must be a positive integer");
|
|
375
|
+
if (count === 1) return [cubicPoint(path, 0.5)];
|
|
376
|
+
const samples = Array.from({ length: resolution + 1 }, (_, index) => cubicPoint(path, index / resolution));
|
|
377
|
+
const lengths = new Float64Array(samples.length);
|
|
378
|
+
for (let index = 1; index < samples.length; index += 1) {
|
|
379
|
+
const dx = samples[index].x - samples[index - 1].x;
|
|
380
|
+
const dy = samples[index].y - samples[index - 1].y;
|
|
381
|
+
lengths[index] = lengths[index - 1] + Math.hypot(dx, dy);
|
|
382
|
+
}
|
|
383
|
+
const total = lengths[lengths.length - 1];
|
|
384
|
+
if (total <= 1e-9) return Array.from({ length: count }, () => ({ ...path.start }));
|
|
385
|
+
const output = [];
|
|
386
|
+
let segment = 1;
|
|
387
|
+
for (let index = 0; index < count; index += 1) {
|
|
388
|
+
const target = total * index / (count - 1);
|
|
389
|
+
while (segment < lengths.length - 1 && lengths[segment] < target) segment += 1;
|
|
390
|
+
const before = lengths[segment - 1];
|
|
391
|
+
const after = lengths[segment];
|
|
392
|
+
const ratio = after === before ? 0 : (target - before) / (after - before);
|
|
393
|
+
output.push({
|
|
394
|
+
x: samples[segment - 1].x + (samples[segment].x - samples[segment - 1].x) * ratio,
|
|
395
|
+
y: samples[segment - 1].y + (samples[segment].y - samples[segment - 1].y) * ratio
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return output;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/core/sectionPath.ts
|
|
402
|
+
var TAU = Math.PI * 2;
|
|
403
|
+
function translateSectionOutlinePath(path, dx, dy) {
|
|
404
|
+
const translate = (point) => ({ x: point.x + dx, y: point.y + dy });
|
|
405
|
+
return transformSectionOutlinePath(path, translate);
|
|
406
|
+
}
|
|
407
|
+
function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected = false) {
|
|
408
|
+
return {
|
|
409
|
+
...path,
|
|
410
|
+
start: transform(path.start),
|
|
411
|
+
segments: path.segments.map((segment) => segment.kind === "line" ? { ...segment, end: transform(segment.end) } : segment.kind === "arc" ? {
|
|
412
|
+
...segment,
|
|
413
|
+
center: transform(segment.center),
|
|
414
|
+
radius: segment.radius * Math.abs(radiusScale),
|
|
415
|
+
clockwise: reflected ? !segment.clockwise : segment.clockwise,
|
|
416
|
+
end: transform(segment.end)
|
|
417
|
+
} : {
|
|
418
|
+
...segment,
|
|
419
|
+
control1: transform(segment.control1),
|
|
420
|
+
control2: transform(segment.control2),
|
|
421
|
+
end: transform(segment.end)
|
|
422
|
+
})
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
353
426
|
// src/core/layout.ts
|
|
354
427
|
function overrideAccessibility(o) {
|
|
355
428
|
if (!o) return [];
|
|
@@ -371,6 +444,7 @@ function place(lx, ly, deg, origin) {
|
|
|
371
444
|
function rowSeatPositions(row) {
|
|
372
445
|
const { seatCount, seatSpacing, curve, rotation, origin } = row;
|
|
373
446
|
const out = [];
|
|
447
|
+
if (row.path) return distributeAlongCubic(row.path, seatCount);
|
|
374
448
|
if (seatCount <= 1) {
|
|
375
449
|
if (seatCount === 1) out.push({ x: origin.x, y: origin.y });
|
|
376
450
|
return out;
|
|
@@ -533,6 +607,53 @@ function pointInPolygon(p, poly) {
|
|
|
533
607
|
}
|
|
534
608
|
return inside;
|
|
535
609
|
}
|
|
610
|
+
function pointOnPolygonBoundary(p, poly) {
|
|
611
|
+
return poly.some((start, index) => {
|
|
612
|
+
const end = poly[(index + 1) % poly.length];
|
|
613
|
+
const cross = (p.y - start.y) * (end.x - start.x) - (p.x - start.x) * (end.y - start.y);
|
|
614
|
+
if (Math.abs(cross) > 1e-7) return false;
|
|
615
|
+
const dot = (p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y);
|
|
616
|
+
const lengthSquared = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
617
|
+
return dot >= -1e-7 && dot <= lengthSquared + 1e-7;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function pointInPolygonWithHoles(p, outer, holes) {
|
|
621
|
+
return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
|
|
622
|
+
}
|
|
623
|
+
function polygonLabelPoint(outer, holes) {
|
|
624
|
+
if (!outer.length) return { x: 0, y: 0 };
|
|
625
|
+
const xs = outer.map((point) => point.x);
|
|
626
|
+
const ys = outer.map((point) => point.y);
|
|
627
|
+
const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
628
|
+
const centroid = polygonCentroid(outer);
|
|
629
|
+
if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
|
|
630
|
+
let best = outer[0];
|
|
631
|
+
let bestScore = -Infinity;
|
|
632
|
+
const rings = [outer, ...holes ?? []];
|
|
633
|
+
for (let row = 1; row < 24; row += 1) {
|
|
634
|
+
for (let column = 1; column < 24; column += 1) {
|
|
635
|
+
const point = {
|
|
636
|
+
x: bounds.minX + (bounds.maxX - bounds.minX) * column / 24,
|
|
637
|
+
y: bounds.minY + (bounds.maxY - bounds.minY) * row / 24
|
|
638
|
+
};
|
|
639
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) continue;
|
|
640
|
+
const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
|
|
641
|
+
const end = ring[(index + 1) % ring.length];
|
|
642
|
+
const dx = end.x - start.x;
|
|
643
|
+
const dy = end.y - start.y;
|
|
644
|
+
const denominator = dx * dx + dy * dy;
|
|
645
|
+
const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
|
|
646
|
+
const t2 = Math.max(0, Math.min(1, projection));
|
|
647
|
+
return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
|
|
648
|
+
})));
|
|
649
|
+
if (score > bestScore) {
|
|
650
|
+
best = point;
|
|
651
|
+
bestScore = score;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return best;
|
|
656
|
+
}
|
|
536
657
|
function polygonCentroid(pts) {
|
|
537
658
|
if (!pts.length) return { x: 0, y: 0 };
|
|
538
659
|
let x = 0;
|
|
@@ -596,9 +717,14 @@ function translateObject(o, dx, dy) {
|
|
|
596
717
|
case "booth":
|
|
597
718
|
return { ...o, center: p(o.center) };
|
|
598
719
|
case "gaArea":
|
|
599
|
-
return { ...o, points: pts(o.points) };
|
|
720
|
+
return { ...o, points: pts(o.points), ...o.holes ? { holes: o.holes.map(pts) } : {} };
|
|
600
721
|
case "section":
|
|
601
|
-
return {
|
|
722
|
+
return {
|
|
723
|
+
...o,
|
|
724
|
+
outline: pts(o.outline),
|
|
725
|
+
...o.outlinePath ? { outlinePath: translateSectionOutlinePath(o.outlinePath, dx, dy) } : {},
|
|
726
|
+
...o.holes ? { holes: o.holes.map(pts) } : {}
|
|
727
|
+
};
|
|
602
728
|
case "text":
|
|
603
729
|
return { ...o, position: p(o.position) };
|
|
604
730
|
case "shape":
|
|
@@ -721,12 +847,33 @@ function objectSeatLabels(o) {
|
|
|
721
847
|
function isSeatObject(o) {
|
|
722
848
|
return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
|
|
723
849
|
}
|
|
850
|
+
function samePoints(left, right) {
|
|
851
|
+
return left.length === right.length && left.every((point, index) => point.x === right[index].x && point.y === right[index].y);
|
|
852
|
+
}
|
|
853
|
+
function sameGASurfaceAsSection(object, section) {
|
|
854
|
+
if (object.type !== "gaArea" || !samePoints(object.points, section.outline)) return false;
|
|
855
|
+
const objectHoles = object.holes ?? [];
|
|
856
|
+
const sectionHoles = section.holes ?? [];
|
|
857
|
+
return objectHoles.length === sectionHoles.length && objectHoles.every((hole, index) => samePoints(hole, sectionHoles[index]));
|
|
858
|
+
}
|
|
724
859
|
function computeSections(doc) {
|
|
725
860
|
const objs = allObjects(doc);
|
|
726
861
|
const sectionObjs = objs.filter((o) => o.type === "section");
|
|
727
862
|
const nodes = /* @__PURE__ */ new Map();
|
|
728
863
|
for (const s of sectionObjs) {
|
|
729
|
-
|
|
864
|
+
const logicalId = s.logicalSectionId ?? s.id;
|
|
865
|
+
const existing = nodes.get(logicalId);
|
|
866
|
+
if (existing) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
nodes.set(logicalId, {
|
|
870
|
+
id: logicalId,
|
|
871
|
+
label: s.label || "Section",
|
|
872
|
+
zone: s.zone,
|
|
873
|
+
seatCount: 0,
|
|
874
|
+
objectIds: [],
|
|
875
|
+
seatLabels: []
|
|
876
|
+
});
|
|
730
877
|
}
|
|
731
878
|
const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
|
|
732
879
|
const objectToSection = /* @__PURE__ */ new Map();
|
|
@@ -734,22 +881,24 @@ function computeSections(doc) {
|
|
|
734
881
|
if (!isSeatObject(obj)) continue;
|
|
735
882
|
const labels = objectSeatLabels(obj);
|
|
736
883
|
if (labels.length === 0) continue;
|
|
884
|
+
const referencedLogicalId = obj.referenceInventorySource?.logicalSectionId;
|
|
737
885
|
const c = objectCenter(obj);
|
|
738
|
-
const
|
|
739
|
-
const
|
|
886
|
+
const referencedOwner = referencedLogicalId ? sectionObjs.find((section) => (section.logicalSectionId ?? section.id) === referencedLogicalId && (sameGASurfaceAsSection(obj, section) || pointInPolygonWithHoles(c, section.outline, section.holes))) : void 0;
|
|
887
|
+
const owner = referencedOwner ?? sectionObjs.find((s) => pointInPolygonWithHoles(c, s.outline, s.holes));
|
|
888
|
+
const node = owner ? nodes.get(owner.logicalSectionId ?? owner.id) : ungrouped;
|
|
740
889
|
node.seatCount += labels.length;
|
|
741
890
|
node.objectIds.push(obj.id);
|
|
742
891
|
node.seatLabels.push(...labels);
|
|
743
892
|
objectToSection.set(obj.id, node.id);
|
|
744
893
|
}
|
|
745
894
|
return {
|
|
746
|
-
sections:
|
|
895
|
+
sections: [...nodes.values()],
|
|
747
896
|
ungrouped: ungrouped.objectIds.length ? ungrouped : null,
|
|
748
897
|
objectToSection
|
|
749
898
|
};
|
|
750
899
|
}
|
|
751
900
|
function isSectionHidden(s, hidden) {
|
|
752
|
-
return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
|
|
901
|
+
return hidden.has(s.id) || !!s.logicalSectionId && hidden.has(s.logicalSectionId) || !!s.zone && hidden.has(s.zone);
|
|
753
902
|
}
|
|
754
903
|
function hiddenObjectIds(doc, hidden) {
|
|
755
904
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -802,6 +951,61 @@ var import_Ellipse = require("konva/lib/shapes/Ellipse");
|
|
|
802
951
|
var import_Line = require("konva/lib/shapes/Line");
|
|
803
952
|
var import_Text = require("konva/lib/shapes/Text");
|
|
804
953
|
var import_Image = require("konva/lib/shapes/Image");
|
|
954
|
+
var import_Shape = require("konva/lib/Shape");
|
|
955
|
+
|
|
956
|
+
// src/core/chartRenderRules.ts
|
|
957
|
+
var SEAT_LABEL_FONT_SIZE = 7;
|
|
958
|
+
var BOOTH_LABEL_FONT_SIZE = 10;
|
|
959
|
+
var GA_LABEL_FONT_SIZE = 15;
|
|
960
|
+
var GA_CAPACITY_LABEL_FONT_SIZE = 11;
|
|
961
|
+
var GA_FILL_OPACITY = 0.85;
|
|
962
|
+
var MIN_VISIBLE_BOOKABLE_LABEL_PX = 12;
|
|
963
|
+
var SMALL_TEXT_CONTRAST = 4.5;
|
|
964
|
+
var DARK_BOOKABLE_LABEL_INK = "#000000";
|
|
965
|
+
var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
|
|
966
|
+
function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
|
|
967
|
+
return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
|
|
968
|
+
}
|
|
969
|
+
function bookableMarkerLabel(publicLabel) {
|
|
970
|
+
return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
|
|
971
|
+
}
|
|
972
|
+
function luminance(value) {
|
|
973
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
974
|
+
if (!match) return null;
|
|
975
|
+
const channel = (offset) => {
|
|
976
|
+
const encoded = Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255;
|
|
977
|
+
return encoded <= 0.04045 ? encoded / 12.92 : ((encoded + 0.055) / 1.055) ** 2.4;
|
|
978
|
+
};
|
|
979
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
980
|
+
}
|
|
981
|
+
function renderedTextContrast(ink, fill) {
|
|
982
|
+
const inkLuminance = luminance(ink);
|
|
983
|
+
const fillLuminance = luminance(fill);
|
|
984
|
+
if (inkLuminance == null || fillLuminance == null) return null;
|
|
985
|
+
return (Math.max(inkLuminance, fillLuminance) + 0.05) / (Math.min(inkLuminance, fillLuminance) + 0.05);
|
|
986
|
+
}
|
|
987
|
+
function rgb(value) {
|
|
988
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
989
|
+
if (!match) return null;
|
|
990
|
+
const packed = Number.parseInt(match[1], 16);
|
|
991
|
+
return [packed >> 16 & 255, packed >> 8 & 255, packed & 255];
|
|
992
|
+
}
|
|
993
|
+
function compositeHexOver(foreground, background, opacity) {
|
|
994
|
+
const front = rgb(foreground);
|
|
995
|
+
const back = rgb(background);
|
|
996
|
+
if (!front || !back) return background;
|
|
997
|
+
const alpha = Math.max(0, Math.min(1, opacity));
|
|
998
|
+
const channels = front.map((value, index) => Math.round(value * alpha + back[index] * (1 - alpha)));
|
|
999
|
+
return `#${channels.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
1000
|
+
}
|
|
1001
|
+
function stateAwareBookableLabelInk(fill, preferred) {
|
|
1002
|
+
const preferredContrast = renderedTextContrast(preferred, fill);
|
|
1003
|
+
if (preferredContrast != null && preferredContrast >= SMALL_TEXT_CONTRAST) return preferred;
|
|
1004
|
+
const darkContrast = renderedTextContrast(DARK_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
1005
|
+
const lightContrast = renderedTextContrast(LIGHT_BOOKABLE_LABEL_INK, fill) ?? 0;
|
|
1006
|
+
if (darkContrast === 0 && lightContrast === 0) return preferred;
|
|
1007
|
+
return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
|
|
1008
|
+
}
|
|
805
1009
|
|
|
806
1010
|
// src/lib/money.ts
|
|
807
1011
|
var DEFAULT_CURRENCY = "USD";
|
|
@@ -861,6 +1065,8 @@ var en = {
|
|
|
861
1065
|
// buyer picker page (src/pages/PickerPage.tsx)
|
|
862
1066
|
"picker.language": "Language",
|
|
863
1067
|
"picker.zoomToFit": "Zoom to fit",
|
|
1068
|
+
"picker.seatCountLabel": "seats",
|
|
1069
|
+
"picker.capacity": "capacity",
|
|
864
1070
|
"picker.viewMode": "View mode",
|
|
865
1071
|
"picker.floor": "Floor",
|
|
866
1072
|
"picker.zoomLevel": "Zoom level",
|
|
@@ -950,7 +1156,8 @@ function formatDate(value, opts) {
|
|
|
950
1156
|
var SEAT_RADIUS = 9;
|
|
951
1157
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
952
1158
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
953
|
-
var LABEL_SCALE =
|
|
1159
|
+
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
1160
|
+
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
954
1161
|
var SEAT_TAP_SLOP_PX = 14;
|
|
955
1162
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
956
1163
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
@@ -964,21 +1171,29 @@ var ISO_SQUASH = 0.58;
|
|
|
964
1171
|
var LIFT_PER_STEP = 58;
|
|
965
1172
|
var ISO_TWEEN_MS = 320;
|
|
966
1173
|
var CAMERA_GLIDE_MS = 650;
|
|
967
|
-
var BLOCK_FILL_ALPHA =
|
|
968
|
-
var
|
|
1174
|
+
var BLOCK_FILL_ALPHA = 1;
|
|
1175
|
+
var SECTION_STROKE_PX = 2;
|
|
1176
|
+
var LIGHT_OVERVIEW_SECTION_FILL = "#e5e7eb";
|
|
1177
|
+
var LIGHT_OVERVIEW_SECTION_STROKE = "#c7cbd1";
|
|
1178
|
+
var LIGHT_OVERVIEW_SECTION_INK = "#595f69";
|
|
1179
|
+
var LIGHT_OVERVIEW_FOCAL_FILL = "#d1d5db";
|
|
1180
|
+
var LIGHT_OVERVIEW_FOCAL_STROKE = "#b8bdc4";
|
|
1181
|
+
var DARK_OVERVIEW_SECTION_FILL = "#273142";
|
|
1182
|
+
var DARK_OVERVIEW_SECTION_STROKE = "#526078";
|
|
1183
|
+
var DARK_OVERVIEW_SECTION_INK = "#f1f5f9";
|
|
1184
|
+
var DARK_OVERVIEW_FOCAL_FILL = "#374151";
|
|
1185
|
+
var DARK_OVERVIEW_FOCAL_STROKE = "#64748b";
|
|
969
1186
|
var SECTION_LABEL_PX = 20;
|
|
970
|
-
var
|
|
971
|
-
var ZONE_LABEL_PX =
|
|
1187
|
+
var MIN_SECTION_LABEL_PX = 12;
|
|
1188
|
+
var ZONE_LABEL_PX = 18;
|
|
972
1189
|
var ZONE_SUB_PX = 12;
|
|
1190
|
+
var HIERARCHY_PILL_BACKGROUND = "#111827";
|
|
973
1191
|
var HELD_FILL = "#6b7280";
|
|
974
1192
|
var TAKEN_FILL = "#374151";
|
|
975
1193
|
var NFS_STROKE = "#4b5563";
|
|
976
|
-
var CLOSED_SECTION_FILL = "#586070";
|
|
977
1194
|
var CLOSED_SEAT_FILL = "#4b5563";
|
|
978
1195
|
var CLOSED_SEAT_OPACITY = 0.4;
|
|
979
1196
|
var FOCUS_DIM_OPACITY = 0.16;
|
|
980
|
-
var FOCUS_DESATURATE = 0.72;
|
|
981
|
-
var FOCUS_NEUTRAL = "#6b7280";
|
|
982
1197
|
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
983
1198
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
984
1199
|
var ACCESS_RING = {
|
|
@@ -999,6 +1214,7 @@ var DEF_SELECTION = "#ffffff";
|
|
|
999
1214
|
var DEF_SELECTION_ON_LIGHT = "#0b1220";
|
|
1000
1215
|
var DEF_DECOR_FILL = "#232c40";
|
|
1001
1216
|
var DEF_TEXT = "#8b93a7";
|
|
1217
|
+
var DEF_CANVAS_BACKGROUND = "#0e1117";
|
|
1002
1218
|
function colorLuminance(color) {
|
|
1003
1219
|
const s = color.trim();
|
|
1004
1220
|
let r = NaN;
|
|
@@ -1011,11 +1227,11 @@ function colorLuminance(color) {
|
|
|
1011
1227
|
g = parseInt(h.slice(2, 4), 16);
|
|
1012
1228
|
b = parseInt(h.slice(4, 6), 16);
|
|
1013
1229
|
} else {
|
|
1014
|
-
const
|
|
1015
|
-
if (
|
|
1016
|
-
r = +
|
|
1017
|
-
g = +
|
|
1018
|
-
b = +
|
|
1230
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
|
|
1231
|
+
if (rgb2) {
|
|
1232
|
+
r = +rgb2[1];
|
|
1233
|
+
g = +rgb2[2];
|
|
1234
|
+
b = +rgb2[3];
|
|
1019
1235
|
}
|
|
1020
1236
|
}
|
|
1021
1237
|
if (Number.isNaN(r)) return NaN;
|
|
@@ -1025,6 +1241,34 @@ function isLightColor(color) {
|
|
|
1025
1241
|
const lum = colorLuminance(color);
|
|
1026
1242
|
return !Number.isNaN(lum) && lum > 0.6;
|
|
1027
1243
|
}
|
|
1244
|
+
function opaqueColorHex(color) {
|
|
1245
|
+
const value = color.trim();
|
|
1246
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
|
|
1247
|
+
if (hex) {
|
|
1248
|
+
const expanded = hex[1].length === 3 ? hex[1].split("").map((channel) => channel + channel).join("") : hex[1];
|
|
1249
|
+
return `#${expanded.toLowerCase()}`;
|
|
1250
|
+
}
|
|
1251
|
+
const rgb2 = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
|
|
1252
|
+
if (!rgb2 || rgb2[4] != null && Number(rgb2[4]) < 0.999) return null;
|
|
1253
|
+
const channels = [Number(rgb2[1]), Number(rgb2[2]), Number(rgb2[3])];
|
|
1254
|
+
if (channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
|
|
1255
|
+
return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
1256
|
+
}
|
|
1257
|
+
function overviewPalette(canvasBackground) {
|
|
1258
|
+
return isLightColor(canvasBackground) ? {
|
|
1259
|
+
sectionFill: LIGHT_OVERVIEW_SECTION_FILL,
|
|
1260
|
+
sectionStroke: LIGHT_OVERVIEW_SECTION_STROKE,
|
|
1261
|
+
sectionInk: LIGHT_OVERVIEW_SECTION_INK,
|
|
1262
|
+
focalFill: LIGHT_OVERVIEW_FOCAL_FILL,
|
|
1263
|
+
focalStroke: LIGHT_OVERVIEW_FOCAL_STROKE
|
|
1264
|
+
} : {
|
|
1265
|
+
sectionFill: DARK_OVERVIEW_SECTION_FILL,
|
|
1266
|
+
sectionStroke: DARK_OVERVIEW_SECTION_STROKE,
|
|
1267
|
+
sectionInk: DARK_OVERVIEW_SECTION_INK,
|
|
1268
|
+
focalFill: DARK_OVERVIEW_FOCAL_FILL,
|
|
1269
|
+
focalStroke: DARK_OVERVIEW_FOCAL_STROKE
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1028
1272
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
1029
1273
|
function seatIdOf(target) {
|
|
1030
1274
|
const n = target;
|
|
@@ -1060,6 +1304,108 @@ function polyBounds(pts) {
|
|
|
1060
1304
|
}
|
|
1061
1305
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
1062
1306
|
}
|
|
1307
|
+
function rotatedRectPoints(center, width, height, rotation) {
|
|
1308
|
+
const radians = rotation * Math.PI / 180;
|
|
1309
|
+
const cos = Math.cos(radians);
|
|
1310
|
+
const sin = Math.sin(radians);
|
|
1311
|
+
return [
|
|
1312
|
+
{ x: -width / 2, y: -height / 2 },
|
|
1313
|
+
{ x: width / 2, y: -height / 2 },
|
|
1314
|
+
{ x: width / 2, y: height / 2 },
|
|
1315
|
+
{ x: -width / 2, y: height / 2 }
|
|
1316
|
+
].map((point) => ({
|
|
1317
|
+
x: center.x + point.x * cos - point.y * sin,
|
|
1318
|
+
y: center.y + point.x * sin + point.y * cos
|
|
1319
|
+
}));
|
|
1320
|
+
}
|
|
1321
|
+
function pointsBounds(points) {
|
|
1322
|
+
const bounds = polyBounds(points);
|
|
1323
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
1324
|
+
}
|
|
1325
|
+
function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
|
|
1326
|
+
const radians = rotation * Math.PI / 180;
|
|
1327
|
+
const cos = Math.cos(radians);
|
|
1328
|
+
const sin = Math.sin(radians);
|
|
1329
|
+
for (let yStep = 0; yStep <= 4; yStep++) {
|
|
1330
|
+
for (let xStep = 0; xStep <= 6; xStep++) {
|
|
1331
|
+
const localX = width * (xStep / 6 - 0.5);
|
|
1332
|
+
const localY = height * (yStep / 4 - 0.5);
|
|
1333
|
+
const point = {
|
|
1334
|
+
x: center.x + localX * cos - localY * sin,
|
|
1335
|
+
y: center.y + localX * sin + localY * cos
|
|
1336
|
+
};
|
|
1337
|
+
if (!pointInPolygonWithHoles(point, outer, holes)) return false;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
function polygonLabelCandidates(outer, holes, preferred) {
|
|
1343
|
+
const bounds = polyBounds(outer);
|
|
1344
|
+
const centre = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
1345
|
+
const points = [preferred];
|
|
1346
|
+
for (let row = 1; row < 12; row += 1) {
|
|
1347
|
+
for (let column = 1; column < 12; column += 1) {
|
|
1348
|
+
const point = {
|
|
1349
|
+
x: bounds.x + bounds.width * column / 12,
|
|
1350
|
+
y: bounds.y + bounds.height * row / 12
|
|
1351
|
+
};
|
|
1352
|
+
if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
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));
|
|
1356
|
+
}
|
|
1357
|
+
function polygonWithHolesShape(outer, holes, attrs, outerPath) {
|
|
1358
|
+
const signedArea = (points) => points.reduce((sum, point, index) => {
|
|
1359
|
+
const next = points[(index + 1) % points.length];
|
|
1360
|
+
return sum + point.x * next.y - next.x * point.y;
|
|
1361
|
+
}, 0);
|
|
1362
|
+
const outerClockwise = signedArea(outer) > 0;
|
|
1363
|
+
return new import_Shape.Shape({
|
|
1364
|
+
...attrs,
|
|
1365
|
+
sceneFunc(context, shape) {
|
|
1366
|
+
context.beginPath();
|
|
1367
|
+
const polygonPath = (points) => {
|
|
1368
|
+
if (!points.length) return;
|
|
1369
|
+
context.moveTo(points[0].x, points[0].y);
|
|
1370
|
+
for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
|
|
1371
|
+
context.closePath();
|
|
1372
|
+
};
|
|
1373
|
+
const vectorPath = (path) => {
|
|
1374
|
+
context.moveTo(path.start.x, path.start.y);
|
|
1375
|
+
let current = path.start;
|
|
1376
|
+
for (const segment of path.segments) {
|
|
1377
|
+
if (segment.kind === "line") context.lineTo(segment.end.x, segment.end.y);
|
|
1378
|
+
else if (segment.kind === "arc") context.arc(
|
|
1379
|
+
segment.center.x,
|
|
1380
|
+
segment.center.y,
|
|
1381
|
+
segment.radius,
|
|
1382
|
+
Math.atan2(current.y - segment.center.y, current.x - segment.center.x),
|
|
1383
|
+
Math.atan2(segment.end.y - segment.center.y, segment.end.x - segment.center.x),
|
|
1384
|
+
!segment.clockwise
|
|
1385
|
+
);
|
|
1386
|
+
else context.bezierCurveTo(
|
|
1387
|
+
segment.control1.x,
|
|
1388
|
+
segment.control1.y,
|
|
1389
|
+
segment.control2.x,
|
|
1390
|
+
segment.control2.y,
|
|
1391
|
+
segment.end.x,
|
|
1392
|
+
segment.end.y
|
|
1393
|
+
);
|
|
1394
|
+
current = segment.end;
|
|
1395
|
+
}
|
|
1396
|
+
context.closePath();
|
|
1397
|
+
};
|
|
1398
|
+
if (outerPath) vectorPath(outerPath);
|
|
1399
|
+
else polygonPath(outer);
|
|
1400
|
+
for (const hole of holes ?? []) {
|
|
1401
|
+
const holeClockwise = signedArea(hole) > 0;
|
|
1402
|
+
polygonPath(holeClockwise === outerClockwise ? [...hole].reverse() : hole);
|
|
1403
|
+
}
|
|
1404
|
+
context.fillStrokeShape(shape);
|
|
1405
|
+
},
|
|
1406
|
+
perfectDrawEnabled: false
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1063
1409
|
function rgba(hex, a) {
|
|
1064
1410
|
const m = /^#?([\da-f]{6})$/i.exec(hex.trim());
|
|
1065
1411
|
if (!m) return hex;
|
|
@@ -1079,11 +1425,11 @@ function mixColors(parts, fallback) {
|
|
|
1079
1425
|
let b = 0;
|
|
1080
1426
|
let tw = 0;
|
|
1081
1427
|
for (const p of parts) {
|
|
1082
|
-
const
|
|
1083
|
-
if (!
|
|
1084
|
-
r +=
|
|
1085
|
-
g +=
|
|
1086
|
-
b +=
|
|
1428
|
+
const rgb2 = hexToRgb(p.hex);
|
|
1429
|
+
if (!rgb2 || p.w <= 0) continue;
|
|
1430
|
+
r += rgb2[0] * p.w;
|
|
1431
|
+
g += rgb2[1] * p.w;
|
|
1432
|
+
b += rgb2[2] * p.w;
|
|
1087
1433
|
tw += p.w;
|
|
1088
1434
|
}
|
|
1089
1435
|
return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
|
|
@@ -1107,11 +1453,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1107
1453
|
this.circleById = /* @__PURE__ */ new Map();
|
|
1108
1454
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
1109
1455
|
this.boothDims = /* @__PURE__ */ new Map();
|
|
1110
|
-
/** Booth
|
|
1456
|
+
/** Booth labels live with the booth shape but obey the shared rendered-size LOD. */
|
|
1111
1457
|
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
1458
|
+
/** Viewport seat labels are rebuilt after each settled camera change. */
|
|
1459
|
+
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1460
|
+
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1461
|
+
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1462
|
+
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
1463
|
+
this.primaryFocalLabels = /* @__PURE__ */ new Map();
|
|
1464
|
+
/** GA paint and text share price/highlight filter state. */
|
|
1465
|
+
this.gaById = /* @__PURE__ */ new Map();
|
|
1112
1466
|
this.statusById = /* @__PURE__ */ new Map();
|
|
1113
1467
|
this.catColor = /* @__PURE__ */ new Map();
|
|
1114
1468
|
this.theme = {};
|
|
1469
|
+
/** Opaque paint actually visible behind transparent Konva canvases. */
|
|
1470
|
+
this.canvasBackground = DEF_CANVAS_BACKGROUND;
|
|
1115
1471
|
/** Effective selection/hover ring color — resolved per chart in setChart(). */
|
|
1116
1472
|
this.effSelection = DEF_SELECTION;
|
|
1117
1473
|
/** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
|
|
@@ -1409,6 +1765,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1409
1765
|
this.circleById.clear();
|
|
1410
1766
|
this.boothDims.clear();
|
|
1411
1767
|
this.boothLabelById.clear();
|
|
1768
|
+
this.seatLabelById.clear();
|
|
1769
|
+
this.freeTextById.clear();
|
|
1770
|
+
this.primaryFocalLabels.clear();
|
|
1771
|
+
this.gaById.clear();
|
|
1772
|
+
for (const marker of this.selectionMarkers.values()) marker.destroy();
|
|
1412
1773
|
this.selectionMarkers.clear();
|
|
1413
1774
|
this.ownedHold.clear();
|
|
1414
1775
|
this.selectionFocusId = null;
|
|
@@ -1420,6 +1781,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1420
1781
|
this.sections = [];
|
|
1421
1782
|
this.zones = [];
|
|
1422
1783
|
this.seatSection.clear();
|
|
1784
|
+
this.focusedSectionId = null;
|
|
1785
|
+
this.focusBackdrop = null;
|
|
1423
1786
|
this.catPrice.clear();
|
|
1424
1787
|
this.zoneColor.clear();
|
|
1425
1788
|
this.lodScale = 0;
|
|
@@ -1437,7 +1800,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1437
1800
|
this.hoverRing.visible(false);
|
|
1438
1801
|
this.theme = doc.theme ?? {};
|
|
1439
1802
|
this.seatR = clamp(this.theme.seatScale ?? 1, 0.7, 1.6) * SEAT_RADIUS;
|
|
1440
|
-
this.container.style.background =
|
|
1803
|
+
this.container.style.background = "";
|
|
1804
|
+
this.canvasBackground = this.resolveCanvasBackground();
|
|
1805
|
+
this.container.style.background = this.canvasBackground;
|
|
1441
1806
|
this.effSelection = this.resolveSelectionColor();
|
|
1442
1807
|
this.hoverRing.stroke(this.effSelection);
|
|
1443
1808
|
this.hoverRing.radius(this.seatR + 2);
|
|
@@ -1590,6 +1955,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1590
1955
|
for (const id of ids) this.setSelected(id, false, true);
|
|
1591
1956
|
this.overlayLayer.batchDraw();
|
|
1592
1957
|
}
|
|
1958
|
+
setMaxSelection(maxSelection) {
|
|
1959
|
+
this.opts.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
1960
|
+
}
|
|
1961
|
+
select(seatIds) {
|
|
1962
|
+
const added = [];
|
|
1963
|
+
for (const id of seatIds) {
|
|
1964
|
+
if (this.selection.has(id) || !this.isSelectable(id)) continue;
|
|
1965
|
+
if (this.selection.size >= this.opts.maxSelection) {
|
|
1966
|
+
this.opts.onSelectionLimit?.(this.opts.maxSelection);
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
this.setSelected(id, true, true);
|
|
1970
|
+
const seat = this.seatById.get(id);
|
|
1971
|
+
if (seat) added.push(seat);
|
|
1972
|
+
}
|
|
1973
|
+
if (added.length) this.overlayLayer.batchDraw();
|
|
1974
|
+
return added;
|
|
1975
|
+
}
|
|
1593
1976
|
/** Switch organizer interaction in place so the host preserves camera, LOD,
|
|
1594
1977
|
* focus and live status state while moving between Monitor and Block. */
|
|
1595
1978
|
setManageInteraction(options) {
|
|
@@ -1648,12 +2031,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1648
2031
|
}
|
|
1649
2032
|
return this.selectMany(ids);
|
|
1650
2033
|
}
|
|
2034
|
+
/** Exact SDK capture helper. MCP never accepts this id; the SDK derives it
|
|
2035
|
+
* from the persisted floor and uses the normal selected paint/ring path. */
|
|
2036
|
+
setEvidenceSelection(seatId) {
|
|
2037
|
+
if (!this.seatById.has(seatId) || !this.isSelectable(seatId)) return false;
|
|
2038
|
+
if (this.selection.size) this.clearSelection();
|
|
2039
|
+
this.setSelected(seatId, true);
|
|
2040
|
+
this.overlayLayer.batchDraw();
|
|
2041
|
+
return this.selection.has(seatId);
|
|
2042
|
+
}
|
|
1651
2043
|
/** Selectable seats in a section OR zone id — pure read (no selection change). */
|
|
1652
2044
|
getSelectableInSection(sectionId) {
|
|
1653
2045
|
const out = [];
|
|
1654
2046
|
const seen = /* @__PURE__ */ new Set();
|
|
1655
2047
|
for (const sec of this.sections) {
|
|
1656
|
-
if (sec.id !== sectionId && sec.zone !== sectionId) continue;
|
|
2048
|
+
if (sec.id !== sectionId && sec.logicalId !== sectionId && sec.zone !== sectionId) continue;
|
|
1657
2049
|
for (const id of sec.memberIds) {
|
|
1658
2050
|
if (seen.has(id)) continue;
|
|
1659
2051
|
seen.add(id);
|
|
@@ -1796,6 +2188,65 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1796
2188
|
};
|
|
1797
2189
|
requestAnimationFrame(step);
|
|
1798
2190
|
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Pulse a section outline without moving the camera or mutating the authored
|
|
2193
|
+
* geometry. The temporary halo is drawn in the non-listening overlay layer,
|
|
2194
|
+
* so the apparent 4% lift never changes hit testing or selection bounds.
|
|
2195
|
+
*/
|
|
2196
|
+
flashSection(sectionId, color = "#22a06b") {
|
|
2197
|
+
const matches = this.sections.filter((section) => section.id === sectionId || section.zone === sectionId);
|
|
2198
|
+
if (!matches.length) return;
|
|
2199
|
+
for (const section of matches) {
|
|
2200
|
+
const centre = section.outline.reduce(
|
|
2201
|
+
(sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }),
|
|
2202
|
+
{ x: 0, y: 0 }
|
|
2203
|
+
);
|
|
2204
|
+
centre.x /= section.outline.length;
|
|
2205
|
+
centre.y /= section.outline.length;
|
|
2206
|
+
const lift = section.elevation > 0 ? this.isoLiftLocal(section.elevation) : { x: 0, y: 0 };
|
|
2207
|
+
const halo = new import_Line.Line({
|
|
2208
|
+
x: centre.x + lift.x,
|
|
2209
|
+
y: centre.y + lift.y,
|
|
2210
|
+
points: section.outline.flatMap((point) => [point.x - centre.x, point.y - centre.y]),
|
|
2211
|
+
closed: true,
|
|
2212
|
+
stroke: color,
|
|
2213
|
+
strokeWidth: 3,
|
|
2214
|
+
strokeScaleEnabled: false,
|
|
2215
|
+
opacity: 0.92,
|
|
2216
|
+
listening: false,
|
|
2217
|
+
perfectDrawEnabled: false,
|
|
2218
|
+
shadowForStrokeEnabled: true,
|
|
2219
|
+
shadowColor: color,
|
|
2220
|
+
shadowBlur: 14,
|
|
2221
|
+
shadowOpacity: 0.7
|
|
2222
|
+
});
|
|
2223
|
+
this.overlayLayer.add(halo);
|
|
2224
|
+
this.overlayLayer.batchDraw();
|
|
2225
|
+
const remove = () => {
|
|
2226
|
+
if (!halo.getLayer()) return;
|
|
2227
|
+
halo.destroy();
|
|
2228
|
+
this.overlayLayer.batchDraw();
|
|
2229
|
+
};
|
|
2230
|
+
if (this.reducedMotion || typeof document !== "undefined" && document.hidden) {
|
|
2231
|
+
setTimeout(remove, 520);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
const start = performance.now();
|
|
2235
|
+
const duration = 820;
|
|
2236
|
+
const step = (now) => {
|
|
2237
|
+
if (this.destroyed || !halo.getLayer()) return;
|
|
2238
|
+
const t2 = Math.min(1, (now - start) / duration);
|
|
2239
|
+
const eased = 1 - Math.pow(1 - t2, 3);
|
|
2240
|
+
const scale = 1 + eased * 0.04;
|
|
2241
|
+
halo.scale({ x: scale, y: scale });
|
|
2242
|
+
halo.opacity(0.92 * (1 - t2));
|
|
2243
|
+
this.overlayLayer.batchDraw();
|
|
2244
|
+
if (t2 < 1) requestAnimationFrame(step);
|
|
2245
|
+
else remove();
|
|
2246
|
+
};
|
|
2247
|
+
requestAnimationFrame(step);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1799
2250
|
/** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
|
|
1800
2251
|
nearestSeat(fromId, dir) {
|
|
1801
2252
|
const from = this.seatById.get(fromId);
|
|
@@ -1869,6 +2320,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1869
2320
|
seatCount() {
|
|
1870
2321
|
return this.seats.length;
|
|
1871
2322
|
}
|
|
2323
|
+
bookableCount() {
|
|
2324
|
+
let total = this.seats.length;
|
|
2325
|
+
for (const area of this.gaById.values()) total += area.capacity;
|
|
2326
|
+
return total;
|
|
2327
|
+
}
|
|
1872
2328
|
worldToScreen(point) {
|
|
1873
2329
|
const s = this.stage.scaleX();
|
|
1874
2330
|
const p = this.isoT === 0 ? point : this.isoForward(point);
|
|
@@ -1885,6 +2341,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1885
2341
|
const c = this.circleById.get(seat.id);
|
|
1886
2342
|
if (c) this.paintSeat(c, seat.id);
|
|
1887
2343
|
}
|
|
2344
|
+
this.updateLabels();
|
|
1888
2345
|
if (this.cached) {
|
|
1889
2346
|
this.seatLayer.clearCache();
|
|
1890
2347
|
this.cacheSeatLayer();
|
|
@@ -1911,12 +2368,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1911
2368
|
const c = this.circleById.get(seat.id);
|
|
1912
2369
|
if (c) this.paintSeat(c, seat.id);
|
|
1913
2370
|
}
|
|
2371
|
+
this.updateLabels();
|
|
1914
2372
|
if (this.cached) {
|
|
1915
2373
|
this.seatLayer.clearCache();
|
|
1916
2374
|
this.cacheSeatLayer();
|
|
1917
2375
|
} else {
|
|
1918
2376
|
this.seatLayer.batchDraw();
|
|
1919
2377
|
}
|
|
2378
|
+
this.applyGAFilterState();
|
|
2379
|
+
}
|
|
2380
|
+
gaCategoryDimmed(categoryKey) {
|
|
2381
|
+
return Boolean(
|
|
2382
|
+
this.categoryHighlight && categoryKey !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(categoryKey)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
/** Keep GA paint and its two labels in the same legend/price-filter state. */
|
|
2386
|
+
applyGAFilterState() {
|
|
2387
|
+
this.paintGAStateForView();
|
|
2388
|
+
this.updateFreeTextVisibility();
|
|
2389
|
+
this.bgLayer.batchDraw();
|
|
2390
|
+
}
|
|
2391
|
+
paintGAStateForView() {
|
|
2392
|
+
for (const ga of this.gaById.values()) {
|
|
2393
|
+
const filteredOut = Boolean(this.categoryFilter && !this.categoryFilter.has(ga.categoryKey));
|
|
2394
|
+
const overviewHidden = ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
|
|
2395
|
+
ga.polygon.opacity(overviewHidden ? 0 : this.gaCategoryDimmed(ga.categoryKey) ? GA_FILL_OPACITY * 0.08 : GA_FILL_OPACITY);
|
|
2396
|
+
ga.polygon.listening(!overviewHidden && !filteredOut);
|
|
2397
|
+
}
|
|
1920
2398
|
}
|
|
1921
2399
|
/** Frame the currently available inventory that survived a buyer price
|
|
1922
2400
|
* filter. Clearing the filter glides back to the full venue. */
|
|
@@ -2211,13 +2689,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2211
2689
|
});
|
|
2212
2690
|
rect.setAttr("seatId", seat.id);
|
|
2213
2691
|
this.circleById.set(seat.id, rect);
|
|
2214
|
-
this.paintSeat(rect, seat.id);
|
|
2215
2692
|
target.add(rect);
|
|
2216
2693
|
const t2 = new import_Text.Text({
|
|
2217
2694
|
x: seat.x,
|
|
2218
2695
|
y: seat.y,
|
|
2219
2696
|
text: seat.label,
|
|
2220
|
-
fontSize:
|
|
2697
|
+
fontSize: BOOTH_LABEL_FONT_SIZE,
|
|
2221
2698
|
fontStyle: "600",
|
|
2222
2699
|
fontFamily: this.labelFont(),
|
|
2223
2700
|
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
@@ -2226,9 +2703,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2226
2703
|
});
|
|
2227
2704
|
t2.offsetX(t2.width() / 2);
|
|
2228
2705
|
t2.offsetY(t2.height() / 2);
|
|
2706
|
+
t2.visible(false);
|
|
2707
|
+
this.boothLabelById.set(seat.id, t2);
|
|
2229
2708
|
this.hasBoothText = true;
|
|
2230
2709
|
this.boothLabelById.set(seat.id, t2);
|
|
2231
2710
|
target.add(t2);
|
|
2711
|
+
this.paintSeat(rect, seat.id);
|
|
2232
2712
|
}
|
|
2233
2713
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
2234
2714
|
seatBaseColor(categoryKey) {
|
|
@@ -2236,6 +2716,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2236
2716
|
const idx = this.catOrder.indexOf(categoryKey);
|
|
2237
2717
|
return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
|
|
2238
2718
|
}
|
|
2719
|
+
/** Authored free fills retain the chart's validated ink. Renderer-owned
|
|
2720
|
+
* transient fills choose an ink against the paint that is actually visible. */
|
|
2721
|
+
renderedBookableLabelInk(id, shape) {
|
|
2722
|
+
const preferred = this.theme.seatLabelColor ?? DEF_SEAT_LABEL;
|
|
2723
|
+
const status = this.statusById.get(id) ?? "free";
|
|
2724
|
+
if (status === "free" && !this.selection.has(id)) return preferred;
|
|
2725
|
+
const fill = shape.fill();
|
|
2726
|
+
return stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", preferred);
|
|
2727
|
+
}
|
|
2239
2728
|
/** Apply fill/stroke/opacity for a seat's current status + selection. */
|
|
2240
2729
|
paintSeat(c, id) {
|
|
2241
2730
|
const seat = this.seatById.get(id);
|
|
@@ -2299,7 +2788,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2299
2788
|
}
|
|
2300
2789
|
if (this.dimmedSections.size) {
|
|
2301
2790
|
const sec = this.seatSection.get(id);
|
|
2302
|
-
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2791
|
+
if (sec && (this.dimmedSections.has(sec.id) || this.dimmedSections.has(sec.logicalId) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
2303
2792
|
c.opacity(0.18);
|
|
2304
2793
|
}
|
|
2305
2794
|
}
|
|
@@ -2312,16 +2801,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2312
2801
|
}
|
|
2313
2802
|
if (this.focusedSectionId) {
|
|
2314
2803
|
const sec = this.seatSection.get(id);
|
|
2315
|
-
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2804
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.logicalId === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
2316
2805
|
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
2317
2806
|
}
|
|
2318
2807
|
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
2808
|
+
const bookableLabel = this.boothLabelById.get(id) ?? this.seatLabelById.get(id);
|
|
2809
|
+
if (bookableLabel) {
|
|
2810
|
+
bookableLabel.fill(this.renderedBookableLabelInk(id, c));
|
|
2811
|
+
bookableLabel.visible(
|
|
2812
|
+
isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2319
2815
|
}
|
|
2320
2816
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
2321
2817
|
seatInClosedSection(id) {
|
|
2322
2818
|
if (!this.closedSections.size) return false;
|
|
2323
2819
|
const sec = this.seatSection.get(id);
|
|
2324
|
-
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2820
|
+
return !!sec && (this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
2325
2821
|
}
|
|
2326
2822
|
/**
|
|
2327
2823
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -2334,6 +2830,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2334
2830
|
const c = this.circleById.get(seat.id);
|
|
2335
2831
|
if (c) this.paintSeat(c, seat.id);
|
|
2336
2832
|
}
|
|
2833
|
+
this.updateLabels();
|
|
2337
2834
|
if (this.cached) {
|
|
2338
2835
|
this.seatLayer.clearCache();
|
|
2339
2836
|
this.cacheSeatLayer();
|
|
@@ -2378,7 +2875,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2378
2875
|
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2379
2876
|
*/
|
|
2380
2877
|
focusSection(id) {
|
|
2381
|
-
if (!this.sections.some((
|
|
2878
|
+
if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
|
|
2382
2879
|
this.focusedSectionId = id;
|
|
2383
2880
|
this.drawFocusBackdrop(id);
|
|
2384
2881
|
this.repaintSectionsAndSeats();
|
|
@@ -2406,20 +2903,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2406
2903
|
this.focusBackdrop.destroy();
|
|
2407
2904
|
this.focusBackdrop = null;
|
|
2408
2905
|
}
|
|
2409
|
-
const
|
|
2410
|
-
if (!
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
}
|
|
2420
|
-
this.bgLayer.add(
|
|
2421
|
-
|
|
2422
|
-
this.focusBackdrop =
|
|
2906
|
+
const sections = this.sections.filter((section) => section.id === id || section.logicalId === id);
|
|
2907
|
+
if (!sections.length) return;
|
|
2908
|
+
const backdrop = new import_Group.Group({ listening: false });
|
|
2909
|
+
for (const section of sections) {
|
|
2910
|
+
backdrop.add(polygonWithHolesShape(section.outline, section.holes, {
|
|
2911
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2912
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2913
|
+
strokeWidth: 1,
|
|
2914
|
+
listening: false
|
|
2915
|
+
}, section.outlinePath));
|
|
2916
|
+
}
|
|
2917
|
+
this.bgLayer.add(backdrop);
|
|
2918
|
+
backdrop.moveToTop();
|
|
2919
|
+
this.focusBackdrop = backdrop;
|
|
2423
2920
|
}
|
|
2424
2921
|
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2425
2922
|
repaintSectionsAndSeats() {
|
|
@@ -2447,7 +2944,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2447
2944
|
height: Math.abs(br.y - tl.y)
|
|
2448
2945
|
};
|
|
2449
2946
|
}
|
|
2450
|
-
/** Axis-aligned world bounds of
|
|
2947
|
+
/** Axis-aligned world bounds of seats, section outlines, and GA polygons (minimap F3 frame). */
|
|
2451
2948
|
getWorldBounds() {
|
|
2452
2949
|
let minX = Infinity;
|
|
2453
2950
|
let minY = Infinity;
|
|
@@ -2461,6 +2958,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2461
2958
|
};
|
|
2462
2959
|
for (const s of this.seats) grow(s.x, s.y);
|
|
2463
2960
|
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2961
|
+
for (const area of this.gaById.values()) for (const p of area.points) grow(p.x, p.y);
|
|
2464
2962
|
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2465
2963
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2466
2964
|
}
|
|
@@ -2472,12 +2970,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2472
2970
|
const c = this.circleById.get(seat.id);
|
|
2473
2971
|
if (c) this.paintSeat(c, seat.id);
|
|
2474
2972
|
}
|
|
2973
|
+
this.updateLabels();
|
|
2475
2974
|
if (this.cached) {
|
|
2476
2975
|
this.seatLayer.clearCache();
|
|
2477
2976
|
this.cacheSeatLayer();
|
|
2478
2977
|
} else {
|
|
2479
2978
|
this.seatLayer.batchDraw();
|
|
2480
2979
|
}
|
|
2980
|
+
this.applyGAFilterState();
|
|
2481
2981
|
}
|
|
2482
2982
|
renderBackground(doc) {
|
|
2483
2983
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
@@ -2495,32 +2995,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2495
2995
|
this.renderText(obj);
|
|
2496
2996
|
}
|
|
2497
2997
|
}
|
|
2498
|
-
const f = doc.focalPoint;
|
|
2499
|
-
if (f) {
|
|
2500
|
-
const size = 14;
|
|
2501
|
-
const cross = new import_Group.Group({ listening: false });
|
|
2502
|
-
cross.add(
|
|
2503
|
-
new import_Line.Line({ points: [f.x - size, f.y, f.x + size, f.y], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2504
|
-
new import_Line.Line({ points: [f.x, f.y - size, f.x, f.y + size], stroke: "#4b5563", strokeWidth: 1.5 }),
|
|
2505
|
-
new import_Circle.Circle({ x: f.x, y: f.y, radius: 3, fill: "#4b5563" })
|
|
2506
|
-
);
|
|
2507
|
-
this.bgLayer.add(cross);
|
|
2508
|
-
}
|
|
2509
2998
|
}
|
|
2510
2999
|
/** Organizer floor-plan photo, dimmed, at the very bottom of the bg layer. */
|
|
2511
3000
|
renderBackgroundImage(bg) {
|
|
3001
|
+
if (!bg.url || bg.visible === false) return;
|
|
2512
3002
|
const img = new window.Image();
|
|
2513
3003
|
img.onload = () => {
|
|
2514
3004
|
const natW = img.naturalWidth || 4;
|
|
2515
3005
|
const natH = img.naturalHeight || 3;
|
|
3006
|
+
const rawCrop = bg.crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3007
|
+
const cropX = Math.max(0, Math.min(0.99, rawCrop.x));
|
|
3008
|
+
const cropY = Math.max(0, Math.min(0.99, rawCrop.y));
|
|
3009
|
+
const crop = {
|
|
3010
|
+
x: cropX,
|
|
3011
|
+
y: cropY,
|
|
3012
|
+
width: Math.max(0.01, Math.min(1 - cropX, rawCrop.width)),
|
|
3013
|
+
height: Math.max(0.01, Math.min(1 - cropY, rawCrop.height))
|
|
3014
|
+
};
|
|
2516
3015
|
const w = bg.width;
|
|
2517
|
-
const h = w * (natH / natW);
|
|
3016
|
+
const h = w * (natH * crop.height / (natW * crop.width));
|
|
2518
3017
|
const node = new import_Image.Image({
|
|
2519
3018
|
image: img,
|
|
2520
|
-
x: bg.center.x
|
|
2521
|
-
y: bg.center.y
|
|
3019
|
+
x: bg.center.x,
|
|
3020
|
+
y: bg.center.y,
|
|
3021
|
+
offsetX: w / 2,
|
|
3022
|
+
offsetY: h / 2,
|
|
2522
3023
|
width: w,
|
|
2523
3024
|
height: h,
|
|
3025
|
+
rotation: bg.rotation ?? 0,
|
|
3026
|
+
crop: {
|
|
3027
|
+
x: crop.x * natW,
|
|
3028
|
+
y: crop.y * natH,
|
|
3029
|
+
width: crop.width * natW,
|
|
3030
|
+
height: crop.height * natH
|
|
3031
|
+
},
|
|
2524
3032
|
opacity: bg.opacity,
|
|
2525
3033
|
listening: false
|
|
2526
3034
|
});
|
|
@@ -2592,28 +3100,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2592
3100
|
})
|
|
2593
3101
|
);
|
|
2594
3102
|
}
|
|
2595
|
-
this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3103
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, obj.center.x, obj.center.y, "#cbd5e1", 12, true);
|
|
3104
|
+
this.freeTextById.set(obj.id, { node: label, background: "#232c40", kind: "table" });
|
|
2596
3105
|
}
|
|
2597
3106
|
renderText(obj) {
|
|
2598
|
-
this.
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
3107
|
+
const background = this.canvasBackground;
|
|
3108
|
+
const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
|
|
3109
|
+
const node = new import_Text.Text({
|
|
3110
|
+
x: obj.position.x,
|
|
3111
|
+
y: obj.position.y,
|
|
3112
|
+
text: obj.text,
|
|
3113
|
+
fontSize: obj.fontSize,
|
|
3114
|
+
rotation: obj.rotation,
|
|
3115
|
+
// Authored ink remains preferred, but an embed/theme surface can change
|
|
3116
|
+
// the actual canvas. Fail over to readable black/white instead of
|
|
3117
|
+
// painting an otherwise valid caption invisibly on that active surface.
|
|
3118
|
+
fill: stateAwareBookableLabelInk(background, preferredInk),
|
|
3119
|
+
fontFamily: this.labelFont(),
|
|
3120
|
+
listening: false,
|
|
3121
|
+
perfectDrawEnabled: false
|
|
3122
|
+
});
|
|
3123
|
+
this.freeTextById.set(obj.id, {
|
|
3124
|
+
node,
|
|
3125
|
+
background,
|
|
3126
|
+
kind: "free-text"
|
|
3127
|
+
});
|
|
3128
|
+
this.bgLayer.add(node);
|
|
2611
3129
|
}
|
|
2612
3130
|
renderShape(obj) {
|
|
2613
|
-
const
|
|
3131
|
+
const authoredFill = obj.fill ?? this.theme.decorFill ?? DEF_DECOR_FILL;
|
|
2614
3132
|
const isStage = obj.role === "stage";
|
|
3133
|
+
const referenceFocal = obj.role === "reference-focal";
|
|
2615
3134
|
const isDecor = !!obj.role && !isStage;
|
|
2616
|
-
const
|
|
3135
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3136
|
+
const fill = referenceFocal ? palette.focalFill : authoredFill;
|
|
3137
|
+
const stroke = isStage ? lighten(fill, 0.28) : referenceFocal ? palette.focalStroke : void 0;
|
|
3138
|
+
const strokeWidth = isStage ? 1 : referenceFocal ? 2 : 0;
|
|
2617
3139
|
let cx = 0;
|
|
2618
3140
|
let cy = 0;
|
|
2619
3141
|
if (obj.kind === "rect" && obj.x != null && obj.y != null && obj.width != null && obj.height != null) {
|
|
@@ -2635,7 +3157,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2635
3157
|
height: obj.height,
|
|
2636
3158
|
...grad,
|
|
2637
3159
|
stroke,
|
|
2638
|
-
strokeWidth
|
|
3160
|
+
strokeWidth,
|
|
2639
3161
|
cornerRadius: 4,
|
|
2640
3162
|
listening: false
|
|
2641
3163
|
})
|
|
@@ -2649,7 +3171,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2649
3171
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2650
3172
|
} : { fill };
|
|
2651
3173
|
this.bgLayer.add(
|
|
2652
|
-
new import_Ellipse.Ellipse({ x: cx, y: cy, rotation: obj.rotation ?? 0, radiusX: obj.width / 2, radiusY: obj.height / 2, ...grad, stroke, strokeWidth
|
|
3174
|
+
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 })
|
|
2653
3175
|
);
|
|
2654
3176
|
} else if (obj.kind === "polygon" && obj.points && obj.points.length) {
|
|
2655
3177
|
const pts = obj.points.flatMap((p) => [p.x, p.y]);
|
|
@@ -2664,17 +3186,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2664
3186
|
fillLinearGradientColorStops: [0, darken(fill, 0.3), 1, lighten(fill, 0.12)]
|
|
2665
3187
|
} : { fill };
|
|
2666
3188
|
this.bgLayer.add(
|
|
2667
|
-
new import_Line.Line({ points: pts, closed: true, x: cx, y: cy, offsetX: cx, offsetY: cy, rotation: obj.rotation ?? 0, ...grad, stroke, strokeWidth
|
|
3189
|
+
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 })
|
|
2668
3190
|
);
|
|
2669
3191
|
}
|
|
2670
3192
|
if (obj.label) {
|
|
2671
|
-
if (isStage)
|
|
2672
|
-
|
|
2673
|
-
|
|
3193
|
+
if (isStage) {
|
|
3194
|
+
const node = this.addStageLabel(cx, cy, obj.label, fill);
|
|
3195
|
+
this.primaryFocalLabels.set(node, 22);
|
|
3196
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "stage" });
|
|
3197
|
+
} else if (isDecor) {
|
|
3198
|
+
const node = this.addCentredLabel(
|
|
3199
|
+
this.bgLayer,
|
|
3200
|
+
obj.label,
|
|
3201
|
+
cx,
|
|
3202
|
+
cy,
|
|
3203
|
+
referenceFocal ? stateAwareBookableLabelInk(fill, "#e6e9f0") : "#9aa3b5",
|
|
3204
|
+
referenceFocal ? 18 : 12,
|
|
3205
|
+
false
|
|
3206
|
+
);
|
|
3207
|
+
if (referenceFocal) this.primaryFocalLabels.set(node, 18);
|
|
3208
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
3209
|
+
} else {
|
|
3210
|
+
const node = this.addCentredLabel(this.bgLayer, obj.label, cx, cy, "#cbd5e1", 16, true);
|
|
3211
|
+
this.freeTextById.set(obj.id, { node, background: fill, kind: "decor" });
|
|
3212
|
+
}
|
|
2674
3213
|
}
|
|
2675
3214
|
}
|
|
2676
3215
|
/** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
|
|
2677
|
-
addStageLabel(x, y, text) {
|
|
3216
|
+
addStageLabel(x, y, text, background) {
|
|
3217
|
+
const ink = stateAwareBookableLabelInk(background, "#e6e9f0");
|
|
2678
3218
|
const t2 = new import_Text.Text({
|
|
2679
3219
|
x,
|
|
2680
3220
|
y,
|
|
@@ -2683,22 +3223,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2683
3223
|
fontStyle: "700",
|
|
2684
3224
|
letterSpacing: 4,
|
|
2685
3225
|
fontFamily: this.labelFont(),
|
|
2686
|
-
fill:
|
|
3226
|
+
fill: ink,
|
|
2687
3227
|
listening: false,
|
|
2688
3228
|
perfectDrawEnabled: false
|
|
2689
3229
|
});
|
|
2690
3230
|
t2.offsetX(t2.width() / 2);
|
|
2691
3231
|
t2.offsetY(t2.height() / 2);
|
|
2692
3232
|
this.bgLayer.add(t2);
|
|
3233
|
+
return t2;
|
|
2693
3234
|
}
|
|
2694
3235
|
renderGA(obj) {
|
|
2695
3236
|
const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
|
|
2696
|
-
const
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
3237
|
+
const canvas = this.canvasBackground;
|
|
3238
|
+
const effectiveBackground = compositeHexOver(color, canvas, GA_FILL_OPACITY);
|
|
3239
|
+
const preferredInk = this.theme.textColor ?? "#e6e9f0";
|
|
3240
|
+
const ink = stateAwareBookableLabelInk(effectiveBackground, preferredInk);
|
|
3241
|
+
const poly = polygonWithHolesShape(obj.points, obj.holes, {
|
|
2700
3242
|
fill: color,
|
|
2701
|
-
opacity:
|
|
3243
|
+
opacity: GA_FILL_OPACITY,
|
|
2702
3244
|
stroke: color,
|
|
2703
3245
|
strokeWidth: 1.5
|
|
2704
3246
|
});
|
|
@@ -2711,31 +3253,51 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2711
3253
|
this.container.style.cursor = "default";
|
|
2712
3254
|
});
|
|
2713
3255
|
this.bgLayer.add(poly);
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
this.addCentredLabel(this.bgLayer, obj.label,
|
|
2717
|
-
this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`,
|
|
3256
|
+
const labelPoint = polygonLabelPoint(obj.points, obj.holes);
|
|
3257
|
+
const containingSection = this.sections.find((section) => pointInPolygonWithHoles(labelPoint, section.outline, section.holes));
|
|
3258
|
+
const label = this.addCentredLabel(this.bgLayer, obj.label, labelPoint.x, labelPoint.y - 8, ink, GA_LABEL_FONT_SIZE, false);
|
|
3259
|
+
const capacity = this.addCentredLabel(this.bgLayer, `cap ${obj.capacity}`, labelPoint.x, labelPoint.y + 10, ink, GA_CAPACITY_LABEL_FONT_SIZE, false);
|
|
3260
|
+
this.freeTextById.set(`${obj.id}:label`, {
|
|
3261
|
+
objectId: obj.id,
|
|
3262
|
+
node: label,
|
|
3263
|
+
background: effectiveBackground,
|
|
3264
|
+
kind: "ga-label",
|
|
3265
|
+
categoryKey: obj.categoryKey
|
|
3266
|
+
});
|
|
3267
|
+
this.freeTextById.set(`${obj.id}:capacity`, {
|
|
3268
|
+
objectId: obj.id,
|
|
3269
|
+
node: capacity,
|
|
3270
|
+
background: effectiveBackground,
|
|
3271
|
+
kind: "ga-capacity",
|
|
3272
|
+
categoryKey: obj.categoryKey
|
|
3273
|
+
});
|
|
3274
|
+
this.gaById.set(obj.id, {
|
|
3275
|
+
label: obj.label,
|
|
3276
|
+
capacity: obj.capacity,
|
|
3277
|
+
categoryKey: obj.categoryKey,
|
|
3278
|
+
points: obj.points,
|
|
3279
|
+
polygon: poly,
|
|
3280
|
+
effectiveBackground,
|
|
3281
|
+
...containingSection ? { sectionId: containingSection.logicalId } : {}
|
|
3282
|
+
});
|
|
2718
3283
|
}
|
|
2719
3284
|
/**
|
|
2720
3285
|
* A section renders in three coordinated layers driven by the LOD melt:
|
|
2721
3286
|
* • a faint outline (the existing near-zoom look, untouched),
|
|
2722
|
-
* • a solid
|
|
2723
|
-
* •
|
|
2724
|
-
*
|
|
2725
|
-
*
|
|
3287
|
+
* • a neutral solid shell that fades in at the overview rung, and
|
|
3288
|
+
* • one readable, contained section name.
|
|
3289
|
+
* Category, row, seat, and availability detail belongs to section focus/zoom.
|
|
3290
|
+
* Membership and category mix are still precomputed for the detailed state.
|
|
2726
3291
|
*/
|
|
2727
3292
|
renderSection(obj) {
|
|
2728
|
-
const
|
|
2729
|
-
const
|
|
2730
|
-
x: obj.outline.reduce((a, p) => a + p.x, 0) / obj.outline.length,
|
|
2731
|
-
y: obj.outline.reduce((a, p) => a + p.y, 0) / obj.outline.length
|
|
2732
|
-
};
|
|
3293
|
+
const centroid = polygonLabelPoint(obj.outline, obj.holes);
|
|
3294
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
2733
3295
|
const memberIds = [];
|
|
2734
3296
|
const catCounts = /* @__PURE__ */ new Map();
|
|
2735
3297
|
let free = 0;
|
|
2736
3298
|
for (const seat of this.seats) {
|
|
2737
3299
|
if (this.seatSection.has(seat.id)) continue;
|
|
2738
|
-
if (!
|
|
3300
|
+
if (!pointInPolygonWithHoles(seat, obj.outline, obj.holes)) continue;
|
|
2739
3301
|
memberIds.push(seat.id);
|
|
2740
3302
|
catCounts.set(seat.categoryKey, (catCounts.get(seat.categoryKey) ?? 0) + 1);
|
|
2741
3303
|
if ((this.statusById.get(seat.id) ?? "free") === "free") free++;
|
|
@@ -2771,52 +3333,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2771
3333
|
}
|
|
2772
3334
|
const bgTarget = liftGroupBg ?? this.bgLayer;
|
|
2773
3335
|
const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
|
|
2774
|
-
const outlinePoly =
|
|
2775
|
-
points: pts,
|
|
2776
|
-
closed: true,
|
|
3336
|
+
const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
2777
3337
|
stroke: rgba(outlineTint, 0.5),
|
|
2778
3338
|
strokeWidth: 1.75,
|
|
2779
3339
|
fill: rgba(outlineTint, 0.08),
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
perfectDrawEnabled: false
|
|
2783
|
-
});
|
|
3340
|
+
listening: false
|
|
3341
|
+
}, obj.outlinePath);
|
|
2784
3342
|
bgTarget.add(outlinePoly);
|
|
2785
|
-
const blockPoly =
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
stroke: rgba("#ffffff", 0.12),
|
|
2790
|
-
strokeWidth: 1,
|
|
3343
|
+
const blockPoly = polygonWithHolesShape(obj.outline, obj.holes, {
|
|
3344
|
+
fill: palette.sectionFill,
|
|
3345
|
+
stroke: palette.sectionStroke,
|
|
3346
|
+
strokeWidth: SECTION_STROKE_PX,
|
|
2791
3347
|
opacity: 0,
|
|
2792
|
-
listening: false
|
|
2793
|
-
|
|
2794
|
-
});
|
|
3348
|
+
listening: false
|
|
3349
|
+
}, obj.outlinePath);
|
|
2795
3350
|
bgTarget.add(blockPoly);
|
|
2796
|
-
const rowSeats = /* @__PURE__ */ new Map();
|
|
2797
|
-
for (const id of memberIds) {
|
|
2798
|
-
const s = this.seatById.get(id);
|
|
2799
|
-
if (!s) continue;
|
|
2800
|
-
const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
|
|
2801
|
-
(rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
|
|
2802
|
-
}
|
|
2803
|
-
const rowLines = [];
|
|
2804
|
-
for (const arr of rowSeats.values()) {
|
|
2805
|
-
if (arr.length < 2) continue;
|
|
2806
|
-
arr.sort((a, b) => a.i - b.i);
|
|
2807
|
-
const line = new import_Line.Line({
|
|
2808
|
-
points: arr.flatMap((p) => [p.x, p.y]),
|
|
2809
|
-
stroke: rgba("#ffffff", 0.34),
|
|
2810
|
-
strokeWidth: SEAT_RADIUS * 0.55,
|
|
2811
|
-
lineCap: "round",
|
|
2812
|
-
lineJoin: "round",
|
|
2813
|
-
opacity: 0,
|
|
2814
|
-
listening: false,
|
|
2815
|
-
perfectDrawEnabled: false
|
|
2816
|
-
});
|
|
2817
|
-
rowLines.push(line);
|
|
2818
|
-
bgTarget.add(line);
|
|
2819
|
-
}
|
|
2820
3351
|
const nameLabel = new import_Text.Text({
|
|
2821
3352
|
x: centroid.x,
|
|
2822
3353
|
y: centroid.y,
|
|
@@ -2824,12 +3355,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2824
3355
|
fontSize: 22,
|
|
2825
3356
|
fontStyle: "700",
|
|
2826
3357
|
fontFamily: this.labelFont(),
|
|
2827
|
-
fill:
|
|
2828
|
-
// Dark halo so the label reads over the seat dots at any zoom.
|
|
2829
|
-
shadowColor: "#05070c",
|
|
2830
|
-
shadowBlur: 6,
|
|
2831
|
-
shadowOpacity: 0.9,
|
|
2832
|
-
shadowForStrokeEnabled: false,
|
|
3358
|
+
fill: palette.sectionInk,
|
|
2833
3359
|
listening: false,
|
|
2834
3360
|
perfectDrawEnabled: false
|
|
2835
3361
|
});
|
|
@@ -2843,11 +3369,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2843
3369
|
fontSize: 12,
|
|
2844
3370
|
fontStyle: "700",
|
|
2845
3371
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2846
|
-
fill:
|
|
2847
|
-
shadowColor: "#05070c",
|
|
2848
|
-
shadowBlur: 5,
|
|
2849
|
-
shadowOpacity: 0.9,
|
|
2850
|
-
shadowForStrokeEnabled: false,
|
|
3372
|
+
fill: palette.sectionInk,
|
|
2851
3373
|
opacity: 0,
|
|
2852
3374
|
listening: false,
|
|
2853
3375
|
perfectDrawEnabled: false
|
|
@@ -2856,9 +3378,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2856
3378
|
bgTarget.add(subLabel);
|
|
2857
3379
|
const sec = {
|
|
2858
3380
|
id: obj.id,
|
|
3381
|
+
logicalId: obj.logicalSectionId ?? obj.id,
|
|
2859
3382
|
label: obj.label,
|
|
2860
3383
|
outline: obj.outline,
|
|
3384
|
+
...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
|
|
3385
|
+
holes: obj.holes ?? [],
|
|
2861
3386
|
centroid,
|
|
3387
|
+
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
|
|
2862
3388
|
zone: obj.zone,
|
|
2863
3389
|
memberIds,
|
|
2864
3390
|
total: memberIds.length,
|
|
@@ -2867,9 +3393,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2867
3393
|
outlineTint,
|
|
2868
3394
|
outlinePoly,
|
|
2869
3395
|
blockPoly,
|
|
2870
|
-
rowLines,
|
|
2871
3396
|
nameLabel,
|
|
2872
3397
|
subLabel,
|
|
3398
|
+
nameLabelFits: true,
|
|
3399
|
+
subLabelFits: true,
|
|
2873
3400
|
elevation,
|
|
2874
3401
|
liftGroupBg,
|
|
2875
3402
|
liftGroupSeat,
|
|
@@ -2881,7 +3408,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2881
3408
|
this.sections.push(sec);
|
|
2882
3409
|
}
|
|
2883
3410
|
refreshSectionHeat(sec) {
|
|
2884
|
-
const raw = this.sectionHeat.get(sec.id) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
3411
|
+
const raw = this.sectionHeat.get(sec.id) ?? this.sectionHeat.get(sec.logicalId) ?? (sec.zone ? this.sectionHeat.get(sec.zone) : void 0);
|
|
2885
3412
|
if (raw == null || raw <= 0) {
|
|
2886
3413
|
sec.outlinePoly.stroke(rgba(sec.outlineTint, 0.5));
|
|
2887
3414
|
sec.outlinePoly.fill(rgba(sec.outlineTint, 0.08));
|
|
@@ -2897,7 +3424,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2897
3424
|
sec.outlinePoly.shadowBlur(4 + raw * 12);
|
|
2898
3425
|
sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
|
|
2899
3426
|
}
|
|
2900
|
-
/** Recompute a section's
|
|
3427
|
+
/** Recompute a section's neutral overview state and retained detail count. */
|
|
2901
3428
|
refreshSectionFill(sec) {
|
|
2902
3429
|
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2903
3430
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
@@ -2905,25 +3432,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2905
3432
|
}
|
|
2906
3433
|
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2907
3434
|
isSectionClosed(sec) {
|
|
2908
|
-
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
3435
|
+
return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2909
3436
|
}
|
|
2910
|
-
/**
|
|
2911
|
-
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2912
|
-
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2913
|
-
* when another section holds focus (AXS dim treatment).
|
|
2914
|
-
*/
|
|
3437
|
+
/** Clean overview shells never leak category, price, or live availability paint. */
|
|
2915
3438
|
sectionBlockFill(sec) {
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
fill = CLOSED_SECTION_FILL;
|
|
2919
|
-
} else {
|
|
2920
|
-
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2921
|
-
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2922
|
-
}
|
|
2923
|
-
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2924
|
-
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2925
|
-
}
|
|
2926
|
-
return fill;
|
|
3439
|
+
const fill = overviewPalette(this.canvasBackground).sectionFill;
|
|
3440
|
+
return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
|
|
2927
3441
|
}
|
|
2928
3442
|
/**
|
|
2929
3443
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
@@ -2952,19 +3466,31 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2952
3466
|
if (typeof p === "number" && p < minPrice) minPrice = p;
|
|
2953
3467
|
}
|
|
2954
3468
|
}
|
|
3469
|
+
const back = new import_Rect.Rect({
|
|
3470
|
+
x: cx,
|
|
3471
|
+
y: cy,
|
|
3472
|
+
width: 1,
|
|
3473
|
+
height: 1,
|
|
3474
|
+
offsetX: 0.5,
|
|
3475
|
+
offsetY: 0.5,
|
|
3476
|
+
cornerRadius: 1,
|
|
3477
|
+
fill: HIERARCHY_PILL_BACKGROUND,
|
|
3478
|
+
stroke: z.color ?? anchor.outlineTint,
|
|
3479
|
+
strokeWidth: 1,
|
|
3480
|
+
opacity: 0,
|
|
3481
|
+
listening: false,
|
|
3482
|
+
perfectDrawEnabled: false
|
|
3483
|
+
});
|
|
3484
|
+
this.bgLayer.add(back);
|
|
2955
3485
|
const label = new import_Text.Text({
|
|
2956
3486
|
x: cx,
|
|
2957
3487
|
y: cy,
|
|
2958
3488
|
text: z.label.toUpperCase(),
|
|
2959
|
-
fontSize:
|
|
3489
|
+
fontSize: ZONE_LABEL_PX,
|
|
2960
3490
|
fontStyle: "800",
|
|
2961
|
-
letterSpacing:
|
|
3491
|
+
letterSpacing: 0.5,
|
|
2962
3492
|
fontFamily: this.labelFont(),
|
|
2963
|
-
fill:
|
|
2964
|
-
shadowColor: "#05070c",
|
|
2965
|
-
shadowBlur: 10,
|
|
2966
|
-
shadowOpacity: 0.95,
|
|
2967
|
-
shadowForStrokeEnabled: false,
|
|
3493
|
+
fill: "#f4f6fb",
|
|
2968
3494
|
opacity: 0,
|
|
2969
3495
|
listening: false,
|
|
2970
3496
|
perfectDrawEnabled: false
|
|
@@ -2981,7 +3507,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2981
3507
|
fontSize: 14,
|
|
2982
3508
|
fontStyle: "600",
|
|
2983
3509
|
fontFamily: "JetBrains Mono, ui-monospace, monospace",
|
|
2984
|
-
fill:
|
|
3510
|
+
fill: "#cbd5e1",
|
|
2985
3511
|
opacity: 0,
|
|
2986
3512
|
listening: false,
|
|
2987
3513
|
perfectDrawEnabled: false
|
|
@@ -2989,7 +3515,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2989
3515
|
sub.offsetX(sub.width() / 2);
|
|
2990
3516
|
this.bgLayer.add(sub);
|
|
2991
3517
|
}
|
|
2992
|
-
this.zones.push({
|
|
3518
|
+
this.zones.push({
|
|
3519
|
+
id: z.id,
|
|
3520
|
+
anchor: { x: cx, y: cy },
|
|
3521
|
+
back,
|
|
3522
|
+
background: HIERARCHY_PILL_BACKGROUND,
|
|
3523
|
+
label,
|
|
3524
|
+
sub
|
|
3525
|
+
});
|
|
2993
3526
|
}
|
|
2994
3527
|
}
|
|
2995
3528
|
/**
|
|
@@ -3011,74 +3544,106 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3011
3544
|
blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
|
|
3012
3545
|
zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
|
|
3013
3546
|
}
|
|
3547
|
+
const sectionOverview = scale < CACHE_THRESHOLD;
|
|
3548
|
+
if (sectionOverview) blockT = 1;
|
|
3014
3549
|
if (!this.zones.length) zoneT = 0;
|
|
3015
|
-
this.seatLayer.opacity(1 - blockT);
|
|
3550
|
+
this.seatLayer.opacity(sectionOverview ? 0 : 1 - blockT);
|
|
3016
3551
|
const sx = this.stage.scaleX();
|
|
3017
3552
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
3018
3553
|
if (rescale) this.lodScale = scale;
|
|
3019
3554
|
const focus = this.focusedSectionId;
|
|
3555
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
3556
|
+
const sectionLabelT = clamp((blockT - 0.2) / 0.8, 0, 1);
|
|
3020
3557
|
for (const sec of this.sections) {
|
|
3021
|
-
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3558
|
+
const dim = focus && sec.id !== focus && sec.logicalId !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
3559
|
+
sec.outlinePoly.opacity(sectionOverview ? 0 : (1 - blockT) * dim);
|
|
3022
3560
|
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
3023
|
-
|
|
3024
|
-
sec.
|
|
3025
|
-
sec.
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3561
|
+
sec.blockPoly.stroke(palette.sectionStroke);
|
|
3562
|
+
sec.blockPoly.strokeWidth(SECTION_STROKE_PX / Math.max(sx, 1e-4));
|
|
3563
|
+
const sectionFill = sec.blockPoly.fill();
|
|
3564
|
+
const sectionInk = stateAwareBookableLabelInk(
|
|
3565
|
+
typeof sectionFill === "string" ? sectionFill : sec.baseFill,
|
|
3566
|
+
palette.sectionInk
|
|
3567
|
+
);
|
|
3568
|
+
sec.nameLabel.fill(sectionInk);
|
|
3569
|
+
sec.subLabel.fill(sectionInk);
|
|
3570
|
+
if (rescale) this.fitSectionRungLabels(sec, sx);
|
|
3571
|
+
const labelOpacity = sectionLabelT * (1 - zoneT) * dim;
|
|
3572
|
+
sec.nameLabel.opacity(sec.nameLabelFits ? labelOpacity : 0);
|
|
3573
|
+
sec.subLabel.opacity(0);
|
|
3031
3574
|
}
|
|
3032
3575
|
const zoneOpacity = zoneT * (1 - this.isoT);
|
|
3033
3576
|
for (const zone of this.zones) {
|
|
3577
|
+
zone.back.opacity(zoneOpacity);
|
|
3034
3578
|
zone.label.opacity(zoneOpacity);
|
|
3035
3579
|
if (zone.sub) zone.sub.opacity(zoneOpacity);
|
|
3036
|
-
if (rescale)
|
|
3037
|
-
const cy = zone.label.y();
|
|
3038
|
-
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
|
|
3039
|
-
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
3040
|
-
}
|
|
3580
|
+
if (rescale) this.sizeZonePill(zone, sx);
|
|
3041
3581
|
}
|
|
3042
3582
|
this.decollideRungLabels(sx);
|
|
3583
|
+
this.dedupeLogicalSectionLabels();
|
|
3584
|
+
for (const zone of this.zones) {
|
|
3585
|
+
const opacity = zone.label.opacity();
|
|
3586
|
+
zone.back.opacity(opacity);
|
|
3587
|
+
if (zone.sub) zone.sub.opacity(opacity);
|
|
3588
|
+
}
|
|
3043
3589
|
this.bgLayer.batchDraw();
|
|
3044
3590
|
}
|
|
3591
|
+
/** One semantic section gets one overview label, even across split contours. */
|
|
3592
|
+
dedupeLogicalSectionLabels() {
|
|
3593
|
+
const byLogical = /* @__PURE__ */ new Map();
|
|
3594
|
+
for (const section of this.sections) {
|
|
3595
|
+
(byLogical.get(section.logicalId) ?? byLogical.set(section.logicalId, []).get(section.logicalId)).push(section);
|
|
3596
|
+
}
|
|
3597
|
+
for (const components of byLogical.values()) {
|
|
3598
|
+
if (components.length < 2) continue;
|
|
3599
|
+
const visible = components.filter((component) => component.nameLabel.opacity() > 0.05).sort((left, right) => {
|
|
3600
|
+
const leftBounds = polyBounds(left.outline);
|
|
3601
|
+
const rightBounds = polyBounds(right.outline);
|
|
3602
|
+
return rightBounds.width * rightBounds.height - leftBounds.width * leftBounds.height;
|
|
3603
|
+
});
|
|
3604
|
+
for (const component of visible.slice(1)) component.nameLabel.opacity(0);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3045
3607
|
/**
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3048
|
-
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
3049
|
-
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
3050
|
-
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
3051
|
-
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
3608
|
+
* Keep transitional zone pills from covering section names. Section names
|
|
3609
|
+
* are already proven inside disjoint shells, so they must not cull each other.
|
|
3052
3610
|
*/
|
|
3053
3611
|
decollideRungLabels(sx) {
|
|
3054
3612
|
const GAP = 4;
|
|
3055
3613
|
const cands = [];
|
|
3056
3614
|
const boxOf = (t2) => {
|
|
3057
3615
|
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3616
|
+
const rotated = pointsBounds(rotatedRectPoints(
|
|
3617
|
+
{ x: 0, y: 0 },
|
|
3618
|
+
t2.width() * sx,
|
|
3619
|
+
t2.height() * sx,
|
|
3620
|
+
t2.rotation()
|
|
3621
|
+
));
|
|
3622
|
+
const w = rotated.width;
|
|
3623
|
+
const h = rotated.height;
|
|
3060
3624
|
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
3061
3625
|
};
|
|
3062
3626
|
for (const zone of this.zones) {
|
|
3063
|
-
if (zone.label.opacity() > 0.05)
|
|
3064
|
-
|
|
3627
|
+
if (zone.label.opacity() > 0.05) {
|
|
3628
|
+
const p = this.worldToScreen(zone.anchor);
|
|
3629
|
+
const w = zone.back.width() * sx;
|
|
3630
|
+
const h = zone.back.height() * sx;
|
|
3631
|
+
cands.push({ node: zone.label, tier: 0, section: false, box: { x: p.x - w / 2, y: p.y - h / 2, w, h } });
|
|
3632
|
+
}
|
|
3065
3633
|
}
|
|
3066
3634
|
for (const sec of this.sections) {
|
|
3067
|
-
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
3068
|
-
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
3635
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, section: true, box: boxOf(sec.nameLabel) });
|
|
3069
3636
|
}
|
|
3070
3637
|
if (cands.length < 2) return;
|
|
3071
3638
|
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
3072
3639
|
const kept = [];
|
|
3073
|
-
const culled = /* @__PURE__ */ new Set();
|
|
3074
3640
|
const collides = (b) => kept.some(
|
|
3075
3641
|
(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
|
|
3076
3642
|
);
|
|
3077
3643
|
for (const c of cands) {
|
|
3078
|
-
if (
|
|
3644
|
+
if (collides(c.box)) {
|
|
3079
3645
|
c.node.opacity(0);
|
|
3080
|
-
|
|
3081
|
-
} else {
|
|
3646
|
+
} else if (!c.section) {
|
|
3082
3647
|
kept.push(c.box);
|
|
3083
3648
|
}
|
|
3084
3649
|
}
|
|
@@ -3090,6 +3655,52 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3090
3655
|
t2.offsetY(t2.height() / 2);
|
|
3091
3656
|
t2.y(y);
|
|
3092
3657
|
}
|
|
3658
|
+
/** Fit one centred section name, rotating narrow shells like the target chart. */
|
|
3659
|
+
fitSectionRungLabels(sec, sx) {
|
|
3660
|
+
const paddingPx = 8;
|
|
3661
|
+
sec.subLabelFits = false;
|
|
3662
|
+
for (let fontPx = SECTION_LABEL_PX; fontPx >= MIN_SECTION_LABEL_PX; fontPx -= 1) {
|
|
3663
|
+
for (const rotation of [0, -90]) {
|
|
3664
|
+
sec.nameLabel.rotation(rotation);
|
|
3665
|
+
this.sizeLabel(sec.nameLabel, fontPx / sx, sec.nameLabel.y());
|
|
3666
|
+
for (const anchor of sec.labelAnchors) {
|
|
3667
|
+
sec.nameLabel.position(anchor);
|
|
3668
|
+
const paddingWorld = paddingPx / sx;
|
|
3669
|
+
if (rotatedRectFitsPolygon(
|
|
3670
|
+
anchor,
|
|
3671
|
+
sec.nameLabel.width() + paddingWorld,
|
|
3672
|
+
sec.nameLabel.height() + paddingWorld,
|
|
3673
|
+
rotation,
|
|
3674
|
+
sec.outline,
|
|
3675
|
+
sec.holes
|
|
3676
|
+
)) {
|
|
3677
|
+
sec.nameLabelFits = true;
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
sec.nameLabel.position(sec.centroid);
|
|
3684
|
+
sec.nameLabel.rotation(0);
|
|
3685
|
+
sec.nameLabelFits = false;
|
|
3686
|
+
}
|
|
3687
|
+
/** Size one screen-constant zone name/price pill around its shared anchor. */
|
|
3688
|
+
sizeZonePill(zone, sx) {
|
|
3689
|
+
const padX = 10 / sx;
|
|
3690
|
+
const padY = 6 / sx;
|
|
3691
|
+
const gap = zone.sub ? 3 / sx : 0;
|
|
3692
|
+
this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, zone.anchor.y);
|
|
3693
|
+
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, zone.anchor.y);
|
|
3694
|
+
const width = Math.max(zone.label.width(), zone.sub?.width() ?? 0) + padX * 2;
|
|
3695
|
+
const height = zone.label.height() + (zone.sub ? gap + zone.sub.height() : 0) + padY * 2;
|
|
3696
|
+
zone.label.y(zone.anchor.y - (zone.sub ? (gap + zone.sub.height()) / 2 : 0));
|
|
3697
|
+
if (zone.sub) zone.sub.y(zone.anchor.y + (zone.label.height() + gap) / 2);
|
|
3698
|
+
zone.back.position(zone.anchor);
|
|
3699
|
+
zone.back.size({ width, height });
|
|
3700
|
+
zone.back.offset({ x: width / 2, y: height / 2 });
|
|
3701
|
+
zone.back.cornerRadius(7 / sx);
|
|
3702
|
+
zone.back.strokeWidth(1 / sx);
|
|
3703
|
+
}
|
|
3093
3704
|
/**
|
|
3094
3705
|
* Map a container-relative screen point back to world coords. Inverts the
|
|
3095
3706
|
* stage (scale/pos) and, in iso view, the iso affine — so screen-space taps
|
|
@@ -3104,12 +3715,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3104
3715
|
sectionAt(clientPoint) {
|
|
3105
3716
|
if (!this.sections.length) return null;
|
|
3106
3717
|
const world = this.screenToWorld(clientPoint);
|
|
3107
|
-
const hit = this.sections.find((sec) =>
|
|
3108
|
-
return hit ? hit.
|
|
3718
|
+
const hit = this.sections.find((sec) => pointInPolygonWithHoles(world, sec.outline, sec.holes));
|
|
3719
|
+
return hit ? hit.logicalId : null;
|
|
3109
3720
|
}
|
|
3110
3721
|
/** Seat ids belonging to a section (Slice 5 section-summary card). */
|
|
3111
3722
|
sectionMembers(id) {
|
|
3112
|
-
return this.sections.
|
|
3723
|
+
return [...new Set(this.sections.filter((section) => section.id === id || section.logicalId === id || section.zone === id).flatMap((section) => section.memberIds))];
|
|
3113
3724
|
}
|
|
3114
3725
|
addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
|
|
3115
3726
|
const t2 = new import_Text.Text({
|
|
@@ -3126,6 +3737,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3126
3737
|
t2.offsetX(t2.width() / 2);
|
|
3127
3738
|
t2.offsetY(t2.height() / 2);
|
|
3128
3739
|
layer.add(t2);
|
|
3740
|
+
return t2;
|
|
3129
3741
|
}
|
|
3130
3742
|
// ---- selection ------------------------------------------------------------
|
|
3131
3743
|
isSelectable(id) {
|
|
@@ -3140,7 +3752,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3140
3752
|
if (seat) this.opts.onDeselect?.(seat);
|
|
3141
3753
|
} else {
|
|
3142
3754
|
if (!this.isSelectable(id)) return;
|
|
3143
|
-
if (this.selection.size >= this.opts.maxSelection)
|
|
3755
|
+
if (this.selection.size >= this.opts.maxSelection) {
|
|
3756
|
+
this.opts.onSelectionLimit?.(this.opts.maxSelection);
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3144
3759
|
this.setSelected(id, true);
|
|
3145
3760
|
const seat = this.seatById.get(id);
|
|
3146
3761
|
if (seat) this.opts.onSelect?.(seat);
|
|
@@ -3156,21 +3771,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3156
3771
|
* the container's computed CSS background (walking up past transparent
|
|
3157
3772
|
* ancestors). Unknown/unparseable backgrounds keep the dark default.
|
|
3158
3773
|
*/
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
if (
|
|
3163
|
-
let
|
|
3164
|
-
while (
|
|
3165
|
-
const
|
|
3166
|
-
if (
|
|
3167
|
-
|
|
3168
|
-
break;
|
|
3169
|
-
}
|
|
3170
|
-
el = el.parentElement;
|
|
3774
|
+
resolveCanvasBackground() {
|
|
3775
|
+
const themed = this.theme.background ? opaqueColorHex(this.theme.background) : null;
|
|
3776
|
+
if (themed) return themed;
|
|
3777
|
+
if (typeof getComputedStyle === "function") {
|
|
3778
|
+
let element = this.container;
|
|
3779
|
+
while (element) {
|
|
3780
|
+
const resolved = opaqueColorHex(getComputedStyle(element).backgroundColor);
|
|
3781
|
+
if (resolved) return resolved;
|
|
3782
|
+
element = element.parentElement;
|
|
3171
3783
|
}
|
|
3172
3784
|
}
|
|
3173
|
-
return
|
|
3785
|
+
return DEF_CANVAS_BACKGROUND;
|
|
3786
|
+
}
|
|
3787
|
+
resolveSelectionColor() {
|
|
3788
|
+
if (this.theme.selectionColor) return this.theme.selectionColor;
|
|
3789
|
+
return isLightColor(this.canvasBackground) ? DEF_SELECTION_ON_LIGHT : DEF_SELECTION;
|
|
3174
3790
|
}
|
|
3175
3791
|
setSelected(id, on, silent = false) {
|
|
3176
3792
|
const c = this.circleById.get(id);
|
|
@@ -3196,6 +3812,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3196
3812
|
const candidate = this.selectionFocusId === id;
|
|
3197
3813
|
const dims = this.boothDims.get(seat.rowId);
|
|
3198
3814
|
const marker = new import_Group.Group({
|
|
3815
|
+
name: "selection-ring",
|
|
3199
3816
|
x: seat.x,
|
|
3200
3817
|
y: seat.y,
|
|
3201
3818
|
rotation: dims?.rotation ?? 0,
|
|
@@ -3203,6 +3820,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3203
3820
|
perfectDrawEnabled: false,
|
|
3204
3821
|
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
3205
3822
|
});
|
|
3823
|
+
marker.setAttr("seatId", id);
|
|
3206
3824
|
const common = {
|
|
3207
3825
|
stroke: this.effSelection,
|
|
3208
3826
|
listening: false,
|
|
@@ -3279,10 +3897,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3279
3897
|
* whether the section already fills the viewport (small container) so the tap
|
|
3280
3898
|
* must fall through and pick.
|
|
3281
3899
|
*/
|
|
3900
|
+
sectionBounds(id) {
|
|
3901
|
+
const bounds = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => polyBounds(section.outline));
|
|
3902
|
+
if (!bounds.length) return null;
|
|
3903
|
+
const left = Math.min(...bounds.map((box) => box.x));
|
|
3904
|
+
const top = Math.min(...bounds.map((box) => box.y));
|
|
3905
|
+
const right = Math.max(...bounds.map((box) => box.x + box.width));
|
|
3906
|
+
const bottom = Math.max(...bounds.map((box) => box.y + box.height));
|
|
3907
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
3908
|
+
}
|
|
3282
3909
|
sectionFrameScale(id) {
|
|
3283
|
-
const
|
|
3284
|
-
if (!
|
|
3285
|
-
const b = polyBounds(sec.outline);
|
|
3910
|
+
const b = this.sectionBounds(id);
|
|
3911
|
+
if (!b) return this.stage.scaleX();
|
|
3286
3912
|
const w = this.stage.width();
|
|
3287
3913
|
const h = this.stage.height();
|
|
3288
3914
|
const { min, max } = this.zoomBounds();
|
|
@@ -3312,11 +3938,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3312
3938
|
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
3313
3939
|
const sec = this.seatSection.get(id);
|
|
3314
3940
|
if (sec) {
|
|
3315
|
-
const alreadyFocused = this.focusedSectionId === sec.
|
|
3316
|
-
const canZoomInFurther = this.sectionFrameScale(sec.
|
|
3941
|
+
const alreadyFocused = this.focusedSectionId === sec.logicalId;
|
|
3942
|
+
const canZoomInFurther = this.sectionFrameScale(sec.logicalId) > this.stage.scaleX() * 1.02;
|
|
3317
3943
|
if (!alreadyFocused && canZoomInFurther) {
|
|
3318
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.
|
|
3319
|
-
else this.focusSection(sec.
|
|
3944
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.logicalId);
|
|
3945
|
+
else this.focusSection(sec.logicalId);
|
|
3320
3946
|
return;
|
|
3321
3947
|
}
|
|
3322
3948
|
}
|
|
@@ -3395,10 +4021,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3395
4021
|
}
|
|
3396
4022
|
if (this.sections.length) {
|
|
3397
4023
|
const world = this.screenToWorld(pointer);
|
|
3398
|
-
const hit = this.sections.find((sn) =>
|
|
4024
|
+
const hit = this.sections.find((sn) => pointInPolygonWithHoles(world, sn.outline, sn.holes));
|
|
3399
4025
|
if (hit) {
|
|
3400
|
-
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.
|
|
3401
|
-
else this.focusRegion(hit.
|
|
4026
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(hit.logicalId);
|
|
4027
|
+
else this.focusRegion(hit.logicalId);
|
|
3402
4028
|
return;
|
|
3403
4029
|
}
|
|
3404
4030
|
}
|
|
@@ -3484,10 +4110,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3484
4110
|
* newer glide cancels an in-flight one.
|
|
3485
4111
|
*/
|
|
3486
4112
|
focusRegion(target, opts) {
|
|
3487
|
-
const b = typeof target === "string" ? (
|
|
3488
|
-
const sec = this.sections.find((s) => s.id === target);
|
|
3489
|
-
return sec ? polyBounds(sec.outline) : null;
|
|
3490
|
-
})() : target;
|
|
4113
|
+
const b = typeof target === "string" ? this.sectionBounds(target) : target;
|
|
3491
4114
|
if (!b) return;
|
|
3492
4115
|
this.cancelGlide();
|
|
3493
4116
|
if (opts?.animate === false || this.reducedMotion) {
|
|
@@ -3547,25 +4170,343 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3547
4170
|
if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
|
|
3548
4171
|
return "sections";
|
|
3549
4172
|
}
|
|
3550
|
-
|
|
4173
|
+
getRenderedQualityEvidence() {
|
|
4174
|
+
const effectiveScale = this.effScale();
|
|
4175
|
+
const stageScale = this.stage.scaleX();
|
|
4176
|
+
const viewport = { width: this.stage.width(), height: this.stage.height() };
|
|
4177
|
+
const rounded = (value) => Math.round(value * 100) / 100;
|
|
4178
|
+
const labels = this.seats.map((seat) => {
|
|
4179
|
+
const shape = this.circleById.get(seat.id);
|
|
4180
|
+
const label = this.boothLabelById.get(seat.id) ?? this.seatLabelById.get(seat.id);
|
|
4181
|
+
const authoredFontSize = seat.kind === "booth" ? BOOTH_LABEL_FONT_SIZE : SEAT_LABEL_FONT_SIZE;
|
|
4182
|
+
const renderedFontPx = rounded((label?.fontSize() ?? authoredFontSize) * effectiveScale);
|
|
4183
|
+
const screen = this.worldToScreen(seat);
|
|
4184
|
+
const outside = screen.x < 0 || screen.x > viewport.width || screen.y < 0 || screen.y > viewport.height;
|
|
4185
|
+
const opacity = shape?.opacity() ?? 0;
|
|
4186
|
+
const section = this.seatSection.get(seat.id);
|
|
4187
|
+
const visible = Boolean(label?.isVisible()) && opacity >= 0.5 && !outside;
|
|
4188
|
+
let hiddenReason;
|
|
4189
|
+
if (!visible) {
|
|
4190
|
+
if (opacity < 0.5) hiddenReason = "dimmed-or-unavailable";
|
|
4191
|
+
else if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4192
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4193
|
+
else if (!label) hiddenReason = "clutter-or-fit";
|
|
4194
|
+
else hiddenReason = "renderer-hidden";
|
|
4195
|
+
}
|
|
4196
|
+
const labelWidth = label ? label.width() * stageScale : 0;
|
|
4197
|
+
const labelHeight = label ? label.height() * effectiveScale : 0;
|
|
4198
|
+
const directWidthPx = shape instanceof import_Rect.Rect ? shape.width() * stageScale : this.seatR * 2 * effectiveScale;
|
|
4199
|
+
const directHeightPx = shape instanceof import_Rect.Rect ? shape.height() * stageScale : this.seatR * 2 * effectiveScale;
|
|
4200
|
+
const assistedDiameterPx = 2 * (this.seatR * effectiveScale + SEAT_TAP_SLOP_PX);
|
|
4201
|
+
const fill = shape?.fill();
|
|
4202
|
+
const ink = label?.fill();
|
|
4203
|
+
return {
|
|
4204
|
+
seatId: seat.id,
|
|
4205
|
+
label: seat.label,
|
|
4206
|
+
kind: seat.kind === "booth" ? "booth" : "seat",
|
|
4207
|
+
categoryKey: seat.categoryKey,
|
|
4208
|
+
...section ? { sectionId: section.id } : {},
|
|
4209
|
+
...section?.zone ? { zoneId: section.zone } : {},
|
|
4210
|
+
status: this.statusById.get(seat.id) ?? "free",
|
|
4211
|
+
selected: this.selection.has(seat.id),
|
|
4212
|
+
visible,
|
|
4213
|
+
renderedFontPx,
|
|
4214
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4215
|
+
ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4216
|
+
opacity: rounded(opacity),
|
|
4217
|
+
pointerTarget: {
|
|
4218
|
+
active: !this.cached && this.isSelectable(seat.id),
|
|
4219
|
+
directWidthPx: rounded(directWidthPx),
|
|
4220
|
+
directHeightPx: rounded(directHeightPx),
|
|
4221
|
+
effectiveMinimumPx: rounded(Math.max(
|
|
4222
|
+
Math.min(directWidthPx, directHeightPx),
|
|
4223
|
+
assistedDiameterPx
|
|
4224
|
+
))
|
|
4225
|
+
},
|
|
4226
|
+
screenCenter: { x: rounded(screen.x), y: rounded(screen.y) },
|
|
4227
|
+
...visible ? {
|
|
4228
|
+
screenBox: {
|
|
4229
|
+
x: rounded(screen.x - labelWidth / 2),
|
|
4230
|
+
y: rounded(screen.y - labelHeight / 2),
|
|
4231
|
+
width: rounded(labelWidth),
|
|
4232
|
+
height: rounded(labelHeight)
|
|
4233
|
+
}
|
|
4234
|
+
} : {},
|
|
4235
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4236
|
+
};
|
|
4237
|
+
});
|
|
4238
|
+
const visibleLabels = labels.filter((label) => label.visible).length;
|
|
4239
|
+
const hierarchyEvidence = (id, kind, role, node, backgroundFill, section) => {
|
|
4240
|
+
const worldCorners = rotatedRectPoints(
|
|
4241
|
+
{ x: node.x(), y: node.y() },
|
|
4242
|
+
node.width(),
|
|
4243
|
+
node.height(),
|
|
4244
|
+
node.rotation()
|
|
4245
|
+
);
|
|
4246
|
+
const screenBounds = pointsBounds(worldCorners.map((corner) => this.worldToScreen(corner)));
|
|
4247
|
+
const opacity = rounded(node.opacity());
|
|
4248
|
+
const ink = node.fill();
|
|
4249
|
+
const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
|
|
4250
|
+
const visible = node.isVisible() && opacity > 0.05 && !outside;
|
|
4251
|
+
const fitsContainer = section ? rotatedRectFitsPolygon(
|
|
4252
|
+
{ x: node.x(), y: node.y() },
|
|
4253
|
+
node.width(),
|
|
4254
|
+
node.height(),
|
|
4255
|
+
node.rotation(),
|
|
4256
|
+
section.outline,
|
|
4257
|
+
section.holes
|
|
4258
|
+
) : void 0;
|
|
4259
|
+
return {
|
|
4260
|
+
id,
|
|
4261
|
+
kind,
|
|
4262
|
+
role,
|
|
4263
|
+
label: node.text(),
|
|
4264
|
+
visible,
|
|
4265
|
+
renderedFontPx: rounded(node.fontSize() * stageScale),
|
|
4266
|
+
opacity,
|
|
4267
|
+
fill: backgroundFill,
|
|
4268
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4269
|
+
...fitsContainer == null ? {} : { fitsContainer },
|
|
4270
|
+
...visible ? {
|
|
4271
|
+
screenBox: {
|
|
4272
|
+
x: rounded(screenBounds.x),
|
|
4273
|
+
y: rounded(screenBounds.y),
|
|
4274
|
+
width: rounded(screenBounds.width),
|
|
4275
|
+
height: rounded(screenBounds.height)
|
|
4276
|
+
}
|
|
4277
|
+
} : {}
|
|
4278
|
+
};
|
|
4279
|
+
};
|
|
4280
|
+
const hierarchyLabels = [
|
|
4281
|
+
...this.sections.map((section) => {
|
|
4282
|
+
const fill = section.blockPoly.fill();
|
|
4283
|
+
return hierarchyEvidence(
|
|
4284
|
+
section.id,
|
|
4285
|
+
"section",
|
|
4286
|
+
"name",
|
|
4287
|
+
section.nameLabel,
|
|
4288
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
4289
|
+
section
|
|
4290
|
+
);
|
|
4291
|
+
}),
|
|
4292
|
+
...this.sections.map((section) => {
|
|
4293
|
+
const fill = section.blockPoly.fill();
|
|
4294
|
+
return hierarchyEvidence(
|
|
4295
|
+
`${section.id}:availability`,
|
|
4296
|
+
"section",
|
|
4297
|
+
"availability",
|
|
4298
|
+
section.subLabel,
|
|
4299
|
+
typeof fill === "string" ? fill : section.baseFill,
|
|
4300
|
+
section
|
|
4301
|
+
);
|
|
4302
|
+
}),
|
|
4303
|
+
...this.zones.flatMap((zone) => [
|
|
4304
|
+
hierarchyEvidence(zone.id, "zone", "name", zone.label, zone.background),
|
|
4305
|
+
...zone.sub ? [hierarchyEvidence(`${zone.id}:price`, "zone", "price", zone.sub, zone.background)] : []
|
|
4306
|
+
])
|
|
4307
|
+
];
|
|
4308
|
+
const gaAreas = [...this.gaById].map(([areaId, ga]) => {
|
|
4309
|
+
const screenPoints = ga.points.map((point) => this.worldToScreen(point));
|
|
4310
|
+
const left = Math.min(...screenPoints.map((point) => point.x));
|
|
4311
|
+
const top = Math.min(...screenPoints.map((point) => point.y));
|
|
4312
|
+
const right = Math.max(...screenPoints.map((point) => point.x));
|
|
4313
|
+
const bottom = Math.max(...screenPoints.map((point) => point.y));
|
|
4314
|
+
const outside = right < 0 || left > viewport.width || bottom < 0 || top > viewport.height;
|
|
4315
|
+
const opacity = rounded(ga.polygon.opacity());
|
|
4316
|
+
const visible = opacity >= 0.1 && !outside;
|
|
4317
|
+
const fill = ga.polygon.fill();
|
|
4318
|
+
return {
|
|
4319
|
+
areaId,
|
|
4320
|
+
label: ga.label,
|
|
4321
|
+
capacity: ga.capacity,
|
|
4322
|
+
categoryKey: ga.categoryKey,
|
|
4323
|
+
...ga.sectionId ? { sectionId: ga.sectionId } : {},
|
|
4324
|
+
visible,
|
|
4325
|
+
interactive: ga.polygon.listening(),
|
|
4326
|
+
opacity,
|
|
4327
|
+
fill: typeof fill === "string" ? fill : "",
|
|
4328
|
+
effectiveBackground: ga.effectiveBackground,
|
|
4329
|
+
...visible ? {
|
|
4330
|
+
screenBox: {
|
|
4331
|
+
x: rounded(left),
|
|
4332
|
+
y: rounded(top),
|
|
4333
|
+
width: rounded(right - left),
|
|
4334
|
+
height: rounded(bottom - top)
|
|
4335
|
+
}
|
|
4336
|
+
} : {}
|
|
4337
|
+
};
|
|
4338
|
+
});
|
|
4339
|
+
const freeTextLabels = [...this.freeTextById].map(([recordKey, record]) => {
|
|
4340
|
+
const { node, background, kind } = record;
|
|
4341
|
+
const point = this.worldToScreen({ x: node.x(), y: node.y() });
|
|
4342
|
+
const width = node.width() * stageScale;
|
|
4343
|
+
const height = node.height() * effectiveScale;
|
|
4344
|
+
const left = point.x - node.offsetX() * stageScale;
|
|
4345
|
+
const top = point.y - node.offsetY() * effectiveScale;
|
|
4346
|
+
const renderedFontPx = rounded(node.fontSize() * effectiveScale);
|
|
4347
|
+
const outside = left + width < 0 || left > viewport.width || top + height < 0 || top > viewport.height;
|
|
4348
|
+
const visible = node.isVisible() && !outside;
|
|
4349
|
+
const ink = node.fill();
|
|
4350
|
+
const opacity = rounded(node.getAbsoluteOpacity());
|
|
4351
|
+
let hiddenReason;
|
|
4352
|
+
if (!visible) {
|
|
4353
|
+
if (renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX) hiddenReason = "below-minimum-size";
|
|
4354
|
+
else if (outside) hiddenReason = "outside-viewport";
|
|
4355
|
+
else hiddenReason = "renderer-hidden";
|
|
4356
|
+
}
|
|
4357
|
+
return {
|
|
4358
|
+
objectId: record.objectId ?? recordKey,
|
|
4359
|
+
kind,
|
|
4360
|
+
text: node.text(),
|
|
4361
|
+
visible,
|
|
4362
|
+
renderedFontPx,
|
|
4363
|
+
ink: typeof ink === "string" ? ink : "",
|
|
4364
|
+
background,
|
|
4365
|
+
opacity,
|
|
4366
|
+
...visible ? {
|
|
4367
|
+
screenBox: {
|
|
4368
|
+
x: rounded(left),
|
|
4369
|
+
y: rounded(top),
|
|
4370
|
+
width: rounded(width),
|
|
4371
|
+
height: rounded(height)
|
|
4372
|
+
}
|
|
4373
|
+
} : {},
|
|
4374
|
+
...hiddenReason ? { hiddenReason } : {}
|
|
4375
|
+
};
|
|
4376
|
+
});
|
|
4377
|
+
const palette = overviewPalette(this.canvasBackground);
|
|
4378
|
+
const neutralSectionFills = /* @__PURE__ */ new Set([
|
|
4379
|
+
palette.sectionFill.toLowerCase(),
|
|
4380
|
+
darken(palette.sectionFill, 0.12).toLowerCase()
|
|
4381
|
+
]);
|
|
4382
|
+
const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
|
|
4383
|
+
return {
|
|
4384
|
+
viewport,
|
|
4385
|
+
canvasBackground: this.canvasBackground,
|
|
4386
|
+
effectiveScale: rounded(effectiveScale),
|
|
4387
|
+
rung: this.getRung(),
|
|
4388
|
+
minimumVisibleLabelPx: MIN_VISIBLE_BOOKABLE_LABEL_PX,
|
|
4389
|
+
totalLabelledBookableUnits: labels.length,
|
|
4390
|
+
visibleLabels,
|
|
4391
|
+
hiddenLabels: labels.length - visibleLabels,
|
|
4392
|
+
totalBookableUnits: labels.length + gaAreas.reduce((sum, area) => sum + area.capacity, 0),
|
|
4393
|
+
selectionRingSeatIds: this.overlayLayer.find(".selection-ring").map((node) => String(node.getAttr("seatId") ?? "")).filter(Boolean),
|
|
4394
|
+
selectionRingColor: this.effSelection,
|
|
4395
|
+
focusedSectionId: this.focusedSectionId,
|
|
4396
|
+
focusBackdropVisible: Boolean(this.focusBackdrop?.isVisible()),
|
|
4397
|
+
categoryFilterKeys: this.categoryFilter ? [...this.categoryFilter].sort() : null,
|
|
4398
|
+
overviewStyle: {
|
|
4399
|
+
visibleSectionShells: visibleSectionShells.length,
|
|
4400
|
+
categoryPaintedSectionShells: visibleSectionShells.filter((section) => {
|
|
4401
|
+
const fill = section.blockPoly.fill();
|
|
4402
|
+
return typeof fill !== "string" || !neutralSectionFills.has(fill.toLowerCase());
|
|
4403
|
+
}).length,
|
|
4404
|
+
visibleCategoryDetailOutlines: this.sections.filter((section) => section.outlinePoly.opacity() > 0.05).length,
|
|
4405
|
+
// Row-hint nodes no longer exist in the production overview scene.
|
|
4406
|
+
visibleSectionRowHints: 0,
|
|
4407
|
+
visibleSectionAvailabilityLabels: this.sections.filter((section) => section.subLabel.opacity() > 0.05).length,
|
|
4408
|
+
visibleSectionGADetails: [...this.gaById.values()].filter((area) => area.sectionId != null && area.polygon.opacity() > 0.05).length
|
|
4409
|
+
},
|
|
4410
|
+
labels,
|
|
4411
|
+
gaAreas,
|
|
4412
|
+
hierarchyLabels,
|
|
4413
|
+
freeTextLabels
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
/** Jump the camera to a rung's zoom band (glided). */
|
|
3551
4417
|
setRung(rung) {
|
|
3552
4418
|
if (rung === "zones") {
|
|
3553
4419
|
this.cancelGlide();
|
|
3554
4420
|
this.zoomToFit();
|
|
3555
4421
|
return;
|
|
3556
4422
|
}
|
|
3557
|
-
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4423
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
4424
|
+
this.seatLabelTargetScale() * 1.05,
|
|
4425
|
+
SEAT_FOCUS_SCALE,
|
|
4426
|
+
CACHE_THRESHOLD * 1.3
|
|
4427
|
+
);
|
|
3558
4428
|
const w = this.stage.width();
|
|
3559
4429
|
const h = this.stage.height();
|
|
3560
|
-
const
|
|
3561
|
-
const
|
|
4430
|
+
const visible = this.getVisibleWorldRect();
|
|
4431
|
+
const viewCentre = {
|
|
4432
|
+
x: visible.x + visible.width / 2,
|
|
4433
|
+
y: visible.y + visible.height / 2
|
|
4434
|
+
};
|
|
4435
|
+
let cx = viewCentre.x;
|
|
4436
|
+
let cy = viewCentre.y;
|
|
4437
|
+
if (rung === "sections" && this.sections.length > 0) {
|
|
4438
|
+
const sectionCentres = this.sections.map((section) => {
|
|
4439
|
+
const bounds = polyBounds(section.outline);
|
|
4440
|
+
return {
|
|
4441
|
+
x: bounds.x + bounds.width / 2,
|
|
4442
|
+
y: bounds.y + bounds.height / 2
|
|
4443
|
+
};
|
|
4444
|
+
});
|
|
4445
|
+
const halfWidth = w / (target * 2);
|
|
4446
|
+
const halfHeight = h / (target * 2);
|
|
4447
|
+
const hierarchyWillBeVisible = sectionCentres.some((point) => Math.abs(point.x - viewCentre.x) <= halfWidth && Math.abs(point.y - viewCentre.y) <= halfHeight);
|
|
4448
|
+
if (!hierarchyWillBeVisible) {
|
|
4449
|
+
const nearest = sectionCentres.reduce((best, point) => {
|
|
4450
|
+
const distance = (point.x - viewCentre.x) ** 2 + (point.y - viewCentre.y) ** 2;
|
|
4451
|
+
return distance < best.distance ? { point, distance } : best;
|
|
4452
|
+
}, { point: sectionCentres[0], distance: Infinity });
|
|
4453
|
+
cx = nearest.point.x;
|
|
4454
|
+
cy = nearest.point.y;
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
const seatAnchors = rung === "seats" ? this.seats.filter((seat) => seat.kind !== "booth") : [];
|
|
4458
|
+
if (seatAnchors.length > 0) {
|
|
4459
|
+
let nearest = seatAnchors[0];
|
|
4460
|
+
let nearestDistance = Infinity;
|
|
4461
|
+
for (const seat of seatAnchors) {
|
|
4462
|
+
const dx = seat.x - viewCentre.x;
|
|
4463
|
+
const dy = seat.y - viewCentre.y;
|
|
4464
|
+
const distance = dx * dx + dy * dy;
|
|
4465
|
+
if (distance < nearestDistance) {
|
|
4466
|
+
nearest = seat;
|
|
4467
|
+
nearestDistance = distance;
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
cx = nearest.x;
|
|
4471
|
+
cy = nearest.y;
|
|
4472
|
+
}
|
|
3562
4473
|
const bw = w / (target * 1.12);
|
|
3563
4474
|
const bh = h / (target * 1.12);
|
|
3564
4475
|
this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
|
|
3565
4476
|
}
|
|
4477
|
+
/**
|
|
4478
|
+
* The seat rung must account for labels that auto-fit inside a seat circle.
|
|
4479
|
+
* A short `A-1` remains at the normal 7u target; a table label such as
|
|
4480
|
+
* `T13-10` may fit at 4u and therefore needs a deeper camera target to reach
|
|
4481
|
+
* the same 12 CSS-pixel floor. Measurement happens only on explicit rung
|
|
4482
|
+
* navigation, never during pan/zoom frames.
|
|
4483
|
+
*/
|
|
4484
|
+
seatLabelTargetScale() {
|
|
4485
|
+
let minimumFont = BOOTH_LABEL_FONT_SIZE;
|
|
4486
|
+
const measure = new import_Text.Text({
|
|
4487
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
4488
|
+
fontStyle: "600",
|
|
4489
|
+
fontFamily: this.labelFont(),
|
|
4490
|
+
listening: false
|
|
4491
|
+
});
|
|
4492
|
+
const maxWidth = this.seatR * 2 - 3;
|
|
4493
|
+
for (const seat of this.seats) {
|
|
4494
|
+
if (seat.kind === "booth") {
|
|
4495
|
+
minimumFont = Math.min(minimumFont, BOOTH_LABEL_FONT_SIZE);
|
|
4496
|
+
continue;
|
|
4497
|
+
}
|
|
4498
|
+
measure.fontSize(SEAT_LABEL_FONT_SIZE);
|
|
4499
|
+
measure.text(bookableMarkerLabel(seat.label));
|
|
4500
|
+
const fitted = measure.width() > maxWidth ? Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxWidth / measure.width()) : SEAT_LABEL_FONT_SIZE;
|
|
4501
|
+
minimumFont = Math.min(minimumFont, fitted);
|
|
4502
|
+
}
|
|
4503
|
+
measure.destroy();
|
|
4504
|
+
return MIN_VISIBLE_BOOKABLE_LABEL_PX / Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, minimumFont);
|
|
4505
|
+
}
|
|
3566
4506
|
/** Recompute LOD (cache/labels) after any pan/zoom settles. */
|
|
3567
4507
|
afterViewChange() {
|
|
3568
4508
|
this.updateLOD();
|
|
4509
|
+
this.updateFreeTextVisibility();
|
|
3569
4510
|
this.updateLabels();
|
|
3570
4511
|
this.scheduleViewChange();
|
|
3571
4512
|
}
|
|
@@ -3579,7 +4520,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3579
4520
|
}
|
|
3580
4521
|
updateLOD() {
|
|
3581
4522
|
const scale = this.effScale();
|
|
4523
|
+
const focalScale = Math.max(scale, 1e-4);
|
|
4524
|
+
for (const [label, targetPx] of this.primaryFocalLabels) {
|
|
4525
|
+
this.sizeLabel(label, targetPx / focalScale, label.y());
|
|
4526
|
+
}
|
|
3582
4527
|
if (this.hasSections) this.applySectionLod(scale);
|
|
4528
|
+
else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
|
|
4529
|
+
this.paintGAStateForView();
|
|
3583
4530
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
3584
4531
|
if (shouldCache && !this.cached) {
|
|
3585
4532
|
this.cacheSeatLayer();
|
|
@@ -3618,15 +4565,37 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3618
4565
|
if (this.recacheTimer) {
|
|
3619
4566
|
clearTimeout(this.recacheTimer);
|
|
3620
4567
|
this.recacheTimer = null;
|
|
3621
|
-
this.
|
|
4568
|
+
if (this.effScale() < CACHE_THRESHOLD) {
|
|
4569
|
+
this.rebuildSeatCache();
|
|
4570
|
+
} else if (this.cached) {
|
|
4571
|
+
this.seatLayer.clearCache();
|
|
4572
|
+
this.seatLayer.listening(true);
|
|
4573
|
+
this.cached = false;
|
|
4574
|
+
}
|
|
3622
4575
|
}
|
|
3623
4576
|
this.bgLayer.draw();
|
|
3624
4577
|
this.seatLayer.draw();
|
|
3625
4578
|
this.overlayLayer.draw();
|
|
3626
4579
|
}
|
|
4580
|
+
updateFreeTextVisibility() {
|
|
4581
|
+
const effectiveScale = this.effScale();
|
|
4582
|
+
for (const { objectId, node, categoryKey, kind } of this.freeTextById.values()) {
|
|
4583
|
+
const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
|
|
4584
|
+
const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
|
|
4585
|
+
node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
3627
4588
|
updateLabels() {
|
|
3628
|
-
const
|
|
4589
|
+
const effectiveScale = this.effScale();
|
|
4590
|
+
const show = effectiveScale >= LABEL_SCALE;
|
|
4591
|
+
for (const [id, label] of this.boothLabelById) {
|
|
4592
|
+
const shape = this.circleById.get(id);
|
|
4593
|
+
label.visible(
|
|
4594
|
+
isBookableLabelLegibleAtScale(label.fontSize(), effectiveScale) && (shape?.opacity() ?? 1) >= 0.5
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
3629
4597
|
this.labelGroup.destroyChildren();
|
|
4598
|
+
this.seatLabelById.clear();
|
|
3630
4599
|
if (!show) {
|
|
3631
4600
|
this.overlayLayer.batchDraw();
|
|
3632
4601
|
return;
|
|
@@ -3640,8 +4609,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3640
4609
|
for (const seat of this.seats) {
|
|
3641
4610
|
if (seat.kind === "booth") continue;
|
|
3642
4611
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
4612
|
+
const shape = this.circleById.get(seat.id);
|
|
4613
|
+
if ((shape?.opacity() ?? 1) < 0.5) continue;
|
|
3643
4614
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
3644
|
-
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id);
|
|
4615
|
+
const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id) && !this.opts.manageMode;
|
|
3645
4616
|
if (unavailable) {
|
|
3646
4617
|
const cue = new import_Group.Group({ x: seat.x, y: seat.y, listening: false });
|
|
3647
4618
|
if (status === "held") {
|
|
@@ -3679,23 +4650,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3679
4650
|
const t2 = new import_Text.Text({
|
|
3680
4651
|
x: seat.x,
|
|
3681
4652
|
y: seat.y,
|
|
3682
|
-
text: seat.label,
|
|
3683
|
-
fontSize:
|
|
4653
|
+
text: bookableMarkerLabel(seat.label),
|
|
4654
|
+
fontSize: SEAT_LABEL_FONT_SIZE,
|
|
3684
4655
|
fontStyle: "600",
|
|
3685
4656
|
fontFamily: this.labelFont(),
|
|
3686
|
-
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
4657
|
+
fill: shape ? this.renderedBookableLabelInk(seat.id, shape) : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3687
4658
|
listening: false,
|
|
3688
4659
|
perfectDrawEnabled: false
|
|
3689
4660
|
});
|
|
3690
4661
|
const maxW = this.seatR * 2 - 3;
|
|
3691
|
-
if (t2.width() > maxW) t2.fontSize(Math.max(
|
|
3692
|
-
if (t2.
|
|
4662
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(MIN_FITTED_SEAT_LABEL_FONT_SIZE, SEAT_LABEL_FONT_SIZE * maxW / t2.width()));
|
|
4663
|
+
if (t2.width() > maxW + 0.01) {
|
|
4664
|
+
t2.destroy();
|
|
4665
|
+
continue;
|
|
4666
|
+
}
|
|
4667
|
+
if (!isBookableLabelLegibleAtScale(t2.fontSize(), effectiveScale)) {
|
|
3693
4668
|
t2.destroy();
|
|
3694
4669
|
continue;
|
|
3695
4670
|
}
|
|
3696
4671
|
t2.offsetX(t2.width() / 2);
|
|
3697
4672
|
t2.offsetY(t2.height() / 2);
|
|
3698
4673
|
this.labelGroup.add(t2);
|
|
4674
|
+
this.seatLabelById.set(seat.id, t2);
|
|
3699
4675
|
if (++count >= MAX_LABELS) break;
|
|
3700
4676
|
}
|
|
3701
4677
|
if (this.isoT > 0) this.applyUprightLabels();
|
|
@@ -3907,6 +4883,7 @@ var PickerController = class {
|
|
|
3907
4883
|
this.opts.onDeselect?.(seat);
|
|
3908
4884
|
this.emitSelectionChange();
|
|
3909
4885
|
},
|
|
4886
|
+
onSelectionLimit: this.opts.onSelectionLimit,
|
|
3910
4887
|
onHover: (seat) => {
|
|
3911
4888
|
this.opts.onHover?.(seat);
|
|
3912
4889
|
if (this.opts.onSeatHover) this.opts.onSeatHover(seat ? this.describeSeat(seat) : null);
|
|
@@ -3974,6 +4951,15 @@ var PickerController = class {
|
|
|
3974
4951
|
this.renderer?.deselect(ids);
|
|
3975
4952
|
this.emitSelectionChange();
|
|
3976
4953
|
}
|
|
4954
|
+
setMaxSelection(maxSelection) {
|
|
4955
|
+
this.maxSelection = Math.max(0, Math.floor(maxSelection));
|
|
4956
|
+
this.renderer?.setMaxSelection?.(this.maxSelection);
|
|
4957
|
+
}
|
|
4958
|
+
select(ids) {
|
|
4959
|
+
const added = this.renderer?.select?.(ids) ?? [];
|
|
4960
|
+
if (added.length) this.emitSelectionChange();
|
|
4961
|
+
return added.map((seat) => this.toSeat(seat));
|
|
4962
|
+
}
|
|
3977
4963
|
// ---- booking machine ------------------------------------------------------
|
|
3978
4964
|
/**
|
|
3979
4965
|
* Hold the current selection (or a given label set). The controller does the
|
|
@@ -4998,6 +5984,8 @@ async function loadLocale(code) {
|
|
|
4998
5984
|
loadLocale,
|
|
4999
5985
|
objectCenter,
|
|
5000
5986
|
pointInPolygon,
|
|
5987
|
+
pointInPolygonWithHoles,
|
|
5988
|
+
polygonLabelPoint,
|
|
5001
5989
|
resolveLocale,
|
|
5002
5990
|
setLocale,
|
|
5003
5991
|
setMoneyLocale,
|