@seatlayer/core 0.34.0 → 0.36.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/index.cjs CHANGED
@@ -265,7 +265,6 @@ __export(index_exports, {
265
265
  rowInventoryCount: () => rowInventoryCount,
266
266
  rowSeatPositions: () => rowSeatPositions,
267
267
  seatCommercialMeta: () => seatCommercialMeta,
268
- seatLabelPart: () => seatLabelPart,
269
268
  sectionGeometry: () => sectionGeometry,
270
269
  setLocale: () => setLocale,
271
270
  setMoneyLocale: () => setMoneyLocale,
@@ -565,6 +564,44 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
565
564
  }
566
565
  var SAGITTA_TO_CONTROL = 4 / 3;
567
566
 
567
+ // src/core/segmentedRowModel.ts
568
+ function resolveSegmentedRowGroups(objects) {
569
+ const segmented = /* @__PURE__ */ new Map();
570
+ const grouped = /* @__PURE__ */ new Map();
571
+ for (const object of objects) {
572
+ if (object.type !== "row" || !object.segmentedRow) continue;
573
+ const list = grouped.get(object.segmentedRow.groupId) ?? [];
574
+ list.push(object);
575
+ grouped.set(object.segmentedRow.groupId, list);
576
+ }
577
+ for (const [groupId, members] of grouped) {
578
+ const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
579
+ const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
580
+ const first = ordered[0]?.segmentedRow;
581
+ if (!first) continue;
582
+ const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
583
+ if (!valid) continue;
584
+ const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
585
+ let adjacencyOffset = 0;
586
+ let displayOffset = 0;
587
+ for (const row of ordered) {
588
+ if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
589
+ segmented.set(row.id, {
590
+ groupId,
591
+ adjacencyOffset,
592
+ displayOffset,
593
+ displayLabel: first.displayLabel,
594
+ totalSeats,
595
+ canonical: ordered[0],
596
+ viewFromSeatUrl: first.viewFromSeatUrl
597
+ });
598
+ adjacencyOffset += row.seatCount;
599
+ displayOffset += row.seatCount;
600
+ }
601
+ }
602
+ return segmented;
603
+ }
604
+
568
605
  // src/core/labeling.ts
569
606
  var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
570
607
  var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
@@ -612,6 +649,78 @@ function toRoman(value) {
612
649
  return out;
613
650
  }
614
651
 
652
+ // src/core/seatNumbering.ts
653
+ function centerRank(n) {
654
+ const rank = new Array(n);
655
+ Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
656
+ return rank;
657
+ }
658
+ function seatLabelPart(row, i) {
659
+ const rawStart = row.seatLabelStart ?? 1;
660
+ const dir = row.seatNumbering?.direction ?? "ltr";
661
+ const step = row.seatNumbering?.step ?? 1;
662
+ const scheme = row.seatNumbering?.scheme ?? "decimal";
663
+ const prefix = row.seatNumbering?.prefix ?? "";
664
+ const endAt = row.seatNumbering?.endAt;
665
+ const n = row.seatCount;
666
+ if (scheme === "updown" || scheme === "updown-descending") {
667
+ const half = Math.ceil(n / 2);
668
+ const core2 = scheme === "updown" ? i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i) : i < half ? rawStart + 2 * (half - 1 - i) : rawStart + 1 + 2 * (i - half);
669
+ return `${prefix}${core2}`;
670
+ }
671
+ const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
672
+ const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
673
+ const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
674
+ let core;
675
+ switch (scheme) {
676
+ case "odd": {
677
+ const firstOdd = start % 2 === 1 ? start : start + 1;
678
+ core = String(firstOdd + p * 2);
679
+ break;
680
+ }
681
+ case "even": {
682
+ const firstEven = start % 2 === 0 ? start : start + 1;
683
+ core = String(firstEven + p * 2);
684
+ break;
685
+ }
686
+ case "roman":
687
+ core = toRoman(start + p * step);
688
+ break;
689
+ case "letters-upper":
690
+ core = toLetters(start + p * step, false);
691
+ break;
692
+ case "letters-lower":
693
+ core = toLetters(start + p * step, true);
694
+ break;
695
+ case "decimal":
696
+ default:
697
+ core = String(start + p * step);
698
+ break;
699
+ }
700
+ return `${prefix}${core}`;
701
+ }
702
+ function logicalNumberingRow(placement) {
703
+ return {
704
+ ...placement.canonical,
705
+ seatCount: placement.totalSeats,
706
+ label: placement.displayLabel,
707
+ displayLabel: placement.displayLabel
708
+ };
709
+ }
710
+ function seatInventoryLabel(row, physicalIndex, override) {
711
+ return override?.label ?? `${row.label}-${seatLabelPart(row, physicalIndex)}`;
712
+ }
713
+ function seatDisplayLabel(row, physicalIndex, placement, override) {
714
+ if (override?.displayLabel) return override.displayLabel;
715
+ if (!placement) {
716
+ const prefix = row.displayLabel ?? row.label;
717
+ return `${prefix}-${seatLabelPart(row, physicalIndex)}`;
718
+ }
719
+ const numberingRow = logicalNumberingRow(placement);
720
+ const ordinal = placement.displayOffset + physicalIndex;
721
+ return `${placement.displayLabel}-${seatLabelPart(numberingRow, ordinal)}`;
722
+ }
723
+
615
724
  // src/core/units.ts
616
725
  var METRES_PER_CHART_UNIT = 0.55 / 24;
617
726
  var CHART_UNITS_PER_METRE = 1 / METRES_PER_CHART_UNIT;
@@ -1047,70 +1156,19 @@ function rowInventoryCount(row) {
1047
1156
  const skipped = new Set((row.overrides ?? []).filter((override) => override.skip && Number.isInteger(override.index) && override.index >= 0 && override.index < row.seatCount).map((override) => override.index));
1048
1157
  return Math.max(0, row.seatCount - skipped.size);
1049
1158
  }
1050
- function centerRank(n) {
1051
- const rank = new Array(n);
1052
- Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
1053
- return rank;
1054
- }
1055
- function seatLabelPart(row, i) {
1056
- const rawStart = row.seatLabelStart ?? 1;
1057
- const dir = row.seatNumbering?.direction ?? "ltr";
1058
- const step = row.seatNumbering?.step ?? 1;
1059
- const scheme = row.seatNumbering?.scheme ?? "decimal";
1060
- const prefix = row.seatNumbering?.prefix ?? "";
1061
- const endAt = row.seatNumbering?.endAt;
1062
- const n = row.seatCount;
1063
- if (scheme === "updown" || scheme === "updown-descending") {
1064
- const half = Math.ceil(n / 2);
1065
- const core2 = scheme === "updown" ? i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i) : i < half ? rawStart + 2 * (half - 1 - i) : rawStart + 1 + 2 * (i - half);
1066
- return `${prefix}${core2}`;
1067
- }
1068
- const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
1069
- const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
1070
- const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
1071
- let core;
1072
- switch (scheme) {
1073
- case "odd": {
1074
- const firstOdd = start % 2 === 1 ? start : start + 1;
1075
- core = String(firstOdd + p * 2);
1076
- break;
1077
- }
1078
- case "even": {
1079
- const firstEven = start % 2 === 0 ? start : start + 1;
1080
- core = String(firstEven + p * 2);
1081
- break;
1082
- }
1083
- case "roman":
1084
- core = toRoman(start + p * step);
1085
- break;
1086
- case "letters-upper":
1087
- core = toLetters(start + p * step, false);
1088
- break;
1089
- case "letters-lower":
1090
- core = toLetters(start + p * step, true);
1091
- break;
1092
- case "decimal":
1093
- default:
1094
- core = String(start + p * step);
1095
- break;
1096
- }
1097
- return `${prefix}${core}`;
1098
- }
1099
- function expandRowSlots(row) {
1159
+ function expandRowSlots(row, placement) {
1100
1160
  const ov = overrideMap(row);
1101
1161
  return rowSeatPositions(row).map((p, i) => {
1102
1162
  const o = ov.get(i);
1103
1163
  const accessibility = overrideAccessibility(o);
1104
- const part = seatLabelPart(row, i);
1105
- const inventoryLabel = o?.label ?? `${row.label}-${part}`;
1106
- const displayPrefix = row.displayLabel ?? row.label;
1164
+ const inventoryLabel = seatInventoryLabel(row, i, o);
1107
1165
  const commercial = { ...row.commercial, ...o?.commercial };
1108
1166
  return {
1109
1167
  index: i,
1110
1168
  x: p.x + (o?.dx ?? 0),
1111
1169
  y: p.y + (o?.dy ?? 0),
1112
1170
  label: inventoryLabel,
1113
- displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
1171
+ displayLabel: seatDisplayLabel(row, i, placement, o),
1114
1172
  categoryKey: o?.categoryKey ?? row.categoryKey,
1115
1173
  skipped: !!o?.skip,
1116
1174
  accessible: accessibility.length > 0,
@@ -1411,39 +1469,7 @@ function stackFloors(doc, spread = 900) {
1411
1469
  }
1412
1470
  function expandFloorObjects(objects, zones, fallbackFocal, viewFallback) {
1413
1471
  const out = [];
1414
- const segmented = /* @__PURE__ */ new Map();
1415
- const grouped = /* @__PURE__ */ new Map();
1416
- for (const object of objects) {
1417
- if (object.type !== "row" || !object.segmentedRow) continue;
1418
- const list = grouped.get(object.segmentedRow.groupId) ?? [];
1419
- list.push(object);
1420
- grouped.set(object.segmentedRow.groupId, list);
1421
- }
1422
- for (const [groupId, members] of grouped) {
1423
- const ordered = members.slice().sort((left, right) => left.segmentedRow.componentIndex - right.segmentedRow.componentIndex);
1424
- const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
1425
- const first = ordered[0]?.segmentedRow;
1426
- if (!first) continue;
1427
- const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
1428
- if (!valid) continue;
1429
- const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
1430
- let adjacencyOffset = 0;
1431
- let displayOffset = 0;
1432
- for (const row of ordered) {
1433
- if (row.segmentedRow.boundaryBefore === "break") adjacencyOffset += 1;
1434
- segmented.set(row.id, {
1435
- groupId,
1436
- adjacencyOffset,
1437
- displayOffset,
1438
- displayLabel: first.displayLabel,
1439
- totalSeats,
1440
- canonical: ordered[0],
1441
- viewFromSeatUrl: first.viewFromSeatUrl
1442
- });
1443
- adjacencyOffset += row.seatCount;
1444
- displayOffset += row.seatCount;
1445
- }
1446
- }
1472
+ const segmented = resolveSegmentedRowGroups(objects);
1447
1473
  for (const obj of objects) {
1448
1474
  let seats = [];
1449
1475
  if (obj.type === "row") seats = expandRow(obj);
@@ -1457,17 +1483,10 @@ function expandFloorObjects(objects, zones, fallbackFocal, viewFallback) {
1457
1483
  for (const seat of seats) {
1458
1484
  const physicalIndex = Number(seat.id.slice(seat.id.lastIndexOf(":") + 1));
1459
1485
  if (!Number.isInteger(physicalIndex)) continue;
1460
- const displayOrdinal = logical.displayOffset + physicalIndex;
1461
1486
  seat.logicalRowId = logical.groupId;
1462
1487
  seat.logicalSeatIndex = logical.adjacencyOffset + physicalIndex;
1463
1488
  if (!overrides2.get(physicalIndex)?.displayLabel) {
1464
- const numberingRow = {
1465
- ...logical.canonical,
1466
- seatCount: logical.totalSeats,
1467
- label: logical.displayLabel,
1468
- displayLabel: logical.displayLabel
1469
- };
1470
- seat.displayLabel = `${logical.displayLabel}-${seatLabelPart(numberingRow, displayOrdinal)}`;
1489
+ seat.displayLabel = seatDisplayLabel(obj, physicalIndex, logical);
1471
1490
  }
1472
1491
  seat.viewUrl ??= logical.viewFromSeatUrl;
1473
1492
  }
@@ -2953,6 +2972,8 @@ var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
2953
2972
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
2954
2973
  var PAN_START_SLOP_PX = 8;
2955
2974
  var GHOST_CLICK_MS = 700;
2975
+ var ROW_LABEL_CLEARANCE = 2;
2976
+ var ROW_LABEL_COLLISION_CELL = 64;
2956
2977
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
2957
2978
  var MAX_LABELS = 700;
2958
2979
  var MARQUEE_RING_CAP = 2500;
@@ -3039,6 +3060,44 @@ function opaqueColorHex(color) {
3039
3060
  if (channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
3040
3061
  return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
3041
3062
  }
3063
+ function worldBoxesOverlap(a, b) {
3064
+ return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
3065
+ }
3066
+ var WorldBoxIndex = class {
3067
+ constructor() {
3068
+ this.cells = /* @__PURE__ */ new Map();
3069
+ }
3070
+ insert(box) {
3071
+ for (const key of this.keys(box)) {
3072
+ const bucket = this.cells.get(key);
3073
+ if (bucket) bucket.push(box);
3074
+ else this.cells.set(key, [box]);
3075
+ }
3076
+ }
3077
+ collisionCount(box) {
3078
+ const seen = /* @__PURE__ */ new Set();
3079
+ let count = 0;
3080
+ for (const key of this.keys(box)) {
3081
+ for (const existing of this.cells.get(key) ?? []) {
3082
+ if (seen.has(existing)) continue;
3083
+ seen.add(existing);
3084
+ if (worldBoxesOverlap(box, existing)) count++;
3085
+ }
3086
+ }
3087
+ return count;
3088
+ }
3089
+ keys(box) {
3090
+ const x0 = Math.floor(box.x / ROW_LABEL_COLLISION_CELL);
3091
+ const x1 = Math.floor((box.x + Math.max(0, box.width)) / ROW_LABEL_COLLISION_CELL);
3092
+ const y0 = Math.floor(box.y / ROW_LABEL_COLLISION_CELL);
3093
+ const y1 = Math.floor((box.y + Math.max(0, box.height)) / ROW_LABEL_COLLISION_CELL);
3094
+ const keys = [];
3095
+ for (let y = y0; y <= y1; y++) {
3096
+ for (let x = x0; x <= x1; x++) keys.push(`${x}:${y}`);
3097
+ }
3098
+ return keys;
3099
+ }
3100
+ };
3042
3101
  function overviewPalette(canvasBackground) {
3043
3102
  return isLightColor(canvasBackground) ? {
3044
3103
  sectionFill: LIGHT_OVERVIEW_SECTION_FILL,
@@ -3444,6 +3503,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3444
3503
  this.bounds = { x: 0, y: 0, width: 1, height: 1 };
3445
3504
  this.cached = false;
3446
3505
  this.dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
3506
+ /** Current chart's buyer-visible image work. Failures are retained until ready(). */
3507
+ this.assetGeneration = 0;
3508
+ this.assetPromises = [];
3509
+ this.assetErrors = [];
3510
+ this.assetCancels = /* @__PURE__ */ new Set();
3447
3511
  this.rafId = 0;
3448
3512
  this.frames = 0;
3449
3513
  this.lastFpsAt = 0;
@@ -3611,7 +3675,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3611
3675
  };
3612
3676
  this.container = container;
3613
3677
  this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
3678
+ this.exportMode = options.exportMode === true;
3614
3679
  this.currency = options.currency;
3680
+ const previousPixelRatio = import_Core.Konva.pixelRatio;
3681
+ if (this.exportMode) this.dpr = 1;
3615
3682
  import_Core.Konva.pixelRatio = this.dpr;
3616
3683
  this.stage = new import_Stage.Stage({
3617
3684
  container,
@@ -3620,19 +3687,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3620
3687
  draggable: false
3621
3688
  // pan/pinch are ours, via pointer events
3622
3689
  });
3623
- container.style.touchAction = "none";
3624
- container.addEventListener("pointerdown", this.onPointerDown, { passive: false });
3625
- container.addEventListener("pointermove", this.onPointerMove, { passive: false });
3626
- container.addEventListener("pointerup", this.onPointerEnd, { passive: false });
3627
- container.addEventListener("pointercancel", this.onPointerEnd, { passive: false });
3628
- if (container.tabIndex < 0) container.tabIndex = 0;
3629
- container.setAttribute("role", "application");
3630
- if (!container.getAttribute("aria-label")) {
3631
- container.setAttribute("aria-label", t("map.aria"));
3632
- }
3633
- container.addEventListener("keydown", this.onKeyDown);
3634
- this.bgLayer = new import_Layer.Layer({ listening: true });
3635
- this.seatLayer = new import_Layer.Layer({ listening: true });
3690
+ if (!this.exportMode) {
3691
+ container.style.touchAction = "none";
3692
+ container.addEventListener("pointerdown", this.onPointerDown, { passive: false });
3693
+ container.addEventListener("pointermove", this.onPointerMove, { passive: false });
3694
+ container.addEventListener("pointerup", this.onPointerEnd, { passive: false });
3695
+ container.addEventListener("pointercancel", this.onPointerEnd, { passive: false });
3696
+ if (container.tabIndex < 0) container.tabIndex = 0;
3697
+ container.setAttribute("role", "application");
3698
+ if (!container.getAttribute("aria-label")) {
3699
+ container.setAttribute("aria-label", t("map.aria"));
3700
+ }
3701
+ container.addEventListener("keydown", this.onKeyDown);
3702
+ }
3703
+ this.bgLayer = new import_Layer.Layer({ listening: !this.exportMode });
3704
+ this.seatLayer = new import_Layer.Layer({ listening: !this.exportMode });
3636
3705
  this.overlayLayer = new import_Layer.Layer({ listening: false });
3637
3706
  this.labelGroup = new import_Group.Group({ listening: false });
3638
3707
  this.overlayLayer.add(this.labelGroup);
@@ -3662,12 +3731,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3662
3731
  });
3663
3732
  this.overlayLayer.add(this.focusRing);
3664
3733
  this.stage.add(this.bgLayer, this.seatLayer, this.overlayLayer);
3665
- this.wireInteraction();
3666
- this.startFpsLoop();
3734
+ if (this.exportMode) import_Core.Konva.pixelRatio = previousPixelRatio;
3735
+ if (!this.exportMode) {
3736
+ this.wireInteraction();
3737
+ this.startFpsLoop();
3738
+ }
3667
3739
  if (false) {
3668
3740
  window.__seatmap = this;
3669
3741
  }
3670
- if (typeof ResizeObserver !== "undefined") {
3742
+ if (!this.exportMode && typeof ResizeObserver !== "undefined") {
3671
3743
  this.resizeObs = new ResizeObserver(() => this.handleResize());
3672
3744
  this.resizeObs.observe(container);
3673
3745
  }
@@ -3690,6 +3762,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3690
3762
  // ---- ISeatmapRenderer -----------------------------------------------------
3691
3763
  setChart(doc, opts) {
3692
3764
  if (doc !== this.chartDoc) this.stacked = false;
3765
+ this.cancelAssetLoads();
3693
3766
  this.chartDoc = doc;
3694
3767
  this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
3695
3768
  this.objectFloor.clear();
@@ -3703,7 +3776,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3703
3776
  this.focusDimOverlay?.destroy();
3704
3777
  this.focusDimOverlay = null;
3705
3778
  this.seatLayer.clearCache();
3706
- this.seatLayer.listening(true);
3779
+ this.seatLayer.listening(!this.exportMode);
3707
3780
  this.seatLayer.destroyChildren();
3708
3781
  this.unsectionedSeatGroup = null;
3709
3782
  this.labelGroup.destroyChildren();
@@ -3803,7 +3876,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3803
3876
  this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
3804
3877
  this.buildPerspectiveProjection(view);
3805
3878
  this.renderBackground(view);
3806
- this.unsectionedSeatGroup = new import_Group.Group({ listening: true });
3879
+ this.unsectionedSeatGroup = new import_Group.Group({ listening: !this.exportMode });
3807
3880
  this.seatLayer.add(this.unsectionedSeatGroup);
3808
3881
  this.renderSeats();
3809
3882
  this.buildRowLabelPlan(view);
@@ -4438,9 +4511,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4438
4511
  paintGAStateForView() {
4439
4512
  for (const ga of this.gaById.values()) {
4440
4513
  const filteredOut = Boolean(this.categoryFilter && !this.categoryFilter.has(ga.categoryKey));
4441
- const overviewHidden = ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
4514
+ const overviewHidden = !this.exportMode && ga.sectionId != null && this.effScale() < CACHE_THRESHOLD;
4442
4515
  ga.polygon.opacity(overviewHidden ? 0 : this.gaCategoryDimmed(ga.categoryKey) ? GA_FILL_OPACITY * 0.08 : GA_FILL_OPACITY);
4443
- ga.polygon.listening(!overviewHidden && !filteredOut);
4516
+ ga.polygon.listening(!this.exportMode && !overviewHidden && !filteredOut);
4444
4517
  }
4445
4518
  }
4446
4519
  /** Frame the currently available inventory that survived a buyer price
@@ -4897,6 +4970,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4897
4970
  }
4898
4971
  destroy() {
4899
4972
  this.destroyed = true;
4973
+ this.cancelAssetLoads();
4900
4974
  if (this.rafId) cancelAnimationFrame(this.rafId);
4901
4975
  if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
4902
4976
  if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
@@ -4913,6 +4987,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4913
4987
  // ---- rendering ------------------------------------------------------------
4914
4988
  /** Theme font stack for all rendered text (falls back to Inter). */
4915
4989
  labelFont() {
4990
+ if (this.exportMode) {
4991
+ return this.theme.fontFamily?.toLowerCase().includes("jetbrains") ? "JetBrains Mono, monospace" : "Inter, sans-serif";
4992
+ }
4916
4993
  return this.theme.fontFamily || "Inter, sans-serif";
4917
4994
  }
4918
4995
  /**
@@ -4960,7 +5037,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4960
5037
  * rung. Overview caching always restores all groups first, so panning a
4961
5038
  * cached whole-venue bitmap can never reveal missing inventory. */
4962
5039
  updateSeatGroupVisibility() {
4963
- const liveSeats = this.effScale() >= CACHE_THRESHOLD;
5040
+ const liveSeats = this.exportMode || this.effScale() >= CACHE_THRESHOLD;
4964
5041
  const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
4965
5042
  const padding = 96;
4966
5043
  const width = this.stage.width();
@@ -5597,11 +5674,67 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5597
5674
  }
5598
5675
  }
5599
5676
  }
5677
+ /**
5678
+ * Track an image through decode without ever leaving a rejected promise
5679
+ * unobserved. Ordinary picker rendering remains best-effort; export ready()
5680
+ * turns the retained failures into one actionable error.
5681
+ */
5682
+ trackAssetImage(image, source, label, onReady) {
5683
+ const generation = this.assetGeneration;
5684
+ if (/^https?:\/\//i.test(source)) image.crossOrigin = "anonymous";
5685
+ const work = new Promise((resolve) => {
5686
+ let settled = false;
5687
+ const finish = () => {
5688
+ if (settled) return;
5689
+ settled = true;
5690
+ this.assetCancels.delete(cancel);
5691
+ resolve();
5692
+ };
5693
+ const cancel = () => {
5694
+ image.onload = null;
5695
+ image.onerror = null;
5696
+ try {
5697
+ image.removeAttribute("src");
5698
+ image.src = "";
5699
+ } catch {
5700
+ }
5701
+ finish();
5702
+ };
5703
+ this.assetCancels.add(cancel);
5704
+ image.onload = () => {
5705
+ if (!this.exportMode) {
5706
+ if (generation === this.assetGeneration) onReady();
5707
+ finish();
5708
+ return;
5709
+ }
5710
+ const decoded = typeof image.decode === "function" ? image.decode() : Promise.resolve();
5711
+ void decoded.then(() => {
5712
+ if (generation === this.assetGeneration) onReady();
5713
+ }).catch(() => {
5714
+ if (generation === this.assetGeneration) this.assetErrors.push(`${label} could not be decoded.`);
5715
+ }).finally(finish);
5716
+ };
5717
+ image.onerror = () => {
5718
+ if (generation === this.assetGeneration) this.assetErrors.push(`${label} could not be loaded.`);
5719
+ finish();
5720
+ };
5721
+ image.src = source;
5722
+ });
5723
+ this.assetPromises.push(work);
5724
+ }
5725
+ /** Cancel and release every image retained by the previous chart/export. */
5726
+ cancelAssetLoads() {
5727
+ this.assetGeneration++;
5728
+ for (const cancel of [...this.assetCancels]) cancel();
5729
+ this.assetCancels.clear();
5730
+ this.assetPromises = [];
5731
+ this.assetErrors = [];
5732
+ }
5600
5733
  /** Organizer floor-plan photo, dimmed, at the very bottom of the bg layer. */
5601
5734
  renderBackgroundImage(bg) {
5602
5735
  if (!bg.url || bg.visible === false) return;
5603
5736
  const img = new window.Image();
5604
- img.onload = () => {
5737
+ this.trackAssetImage(img, bg.url, "Buyer background image", () => {
5605
5738
  const natW = img.naturalWidth || 4;
5606
5739
  const natH = img.naturalHeight || 3;
5607
5740
  const rawCrop = bg.crop ?? { x: 0, y: 0, width: 1, height: 1 };
@@ -5636,8 +5769,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5636
5769
  this.bgLayer.add(node);
5637
5770
  node.moveToBottom();
5638
5771
  this.bgLayer.batchDraw();
5639
- };
5640
- img.src = bg.url;
5772
+ });
5641
5773
  }
5642
5774
  /**
5643
5775
  * A decor graphic (rink / court / stage art). The KImage node is added to the
@@ -5663,12 +5795,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5663
5795
  });
5664
5796
  if (obj.layer === "foreground") this.fgDecorGroup.add(node);
5665
5797
  else this.bgLayer.add(node);
5666
- img.onload = () => {
5798
+ this.trackAssetImage(img, obj.href, `Decor image \u201C${obj.label || obj.id}\u201D`, () => {
5667
5799
  const layer = node.getLayer();
5668
5800
  if (!layer) return;
5669
5801
  layer.batchDraw();
5670
- };
5671
- img.src = obj.href;
5802
+ });
5672
5803
  }
5673
5804
  renderTable(obj) {
5674
5805
  if (obj.shape === "round") {
@@ -7331,6 +7462,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7331
7462
  }
7332
7463
  /** rAF-coalesced `onViewChange` — at most one host callback per animation frame. */
7333
7464
  scheduleViewChange() {
7465
+ if (this.exportMode) return;
7334
7466
  if (this.viewChangeRaf) return;
7335
7467
  this.viewChangeRaf = requestAnimationFrame(() => {
7336
7468
  this.viewChangeRaf = 0;
@@ -7338,7 +7470,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7338
7470
  });
7339
7471
  }
7340
7472
  updateLOD() {
7341
- const scale = this.effScale();
7473
+ const scale = this.exportMode ? Math.max(this.effScale(), BLOCK_MELT_TOP + 0.01, LABEL_SCALE) : this.effScale();
7342
7474
  const focalScale = Math.max(scale, 1e-4);
7343
7475
  for (const [label, targetPx] of this.primaryFocalLabels) {
7344
7476
  this.sizeLabel(label, targetPx / focalScale, label.y());
@@ -7347,7 +7479,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7347
7479
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
7348
7480
  this.paintGAStateForView();
7349
7481
  this.updateAccessGlyphs(scale);
7350
- const shouldCache = scale < CACHE_THRESHOLD;
7482
+ const shouldCache = !this.exportMode && scale < CACHE_THRESHOLD;
7351
7483
  const suppressPerspectiveSeatCache = this.viewMode === "perspective" && this.hasSections && this.seats.length > 2500 && shouldCache;
7352
7484
  if (suppressPerspectiveSeatCache) {
7353
7485
  this.seatLayer.listening(false);
@@ -7357,7 +7489,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7357
7489
  this.cacheSeatLayer();
7358
7490
  } else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
7359
7491
  if (this.cached) this.releaseSeatLayerBitmap();
7360
- this.seatLayer.listening(true);
7492
+ this.seatLayer.listening(!this.exportMode);
7361
7493
  this.cached = false;
7362
7494
  this.seatLayer.batchDraw();
7363
7495
  }
@@ -7425,8 +7557,103 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7425
7557
  this.seatLayer.draw();
7426
7558
  this.overlayLayer.draw();
7427
7559
  }
7560
+ /**
7561
+ * Resolve every buyer-visible image requested by the current chart. Export
7562
+ * callers get a hard error with the affected layer name; the interactive
7563
+ * renderer never calls this and retains its existing best-effort behaviour.
7564
+ */
7565
+ async ready() {
7566
+ const generation = this.assetGeneration;
7567
+ const fontSet = typeof document !== "undefined" && "fonts" in document ? document.fonts : null;
7568
+ await Promise.all([
7569
+ ...this.assetPromises.slice(),
7570
+ ...fontSet ? [
7571
+ fontSet.ready.then(() => void 0),
7572
+ fontSet.load("700 24px Inter").then(() => void 0),
7573
+ fontSet.load('600 12px "JetBrains Mono"').then(() => void 0)
7574
+ ] : []
7575
+ ]);
7576
+ if (generation !== this.assetGeneration) {
7577
+ throw new Error("The chart changed while its export assets were loading.");
7578
+ }
7579
+ if (this.assetErrors.length) {
7580
+ throw new Error(`Export stopped because ${this.assetErrors.join(" ")}`);
7581
+ }
7582
+ if (this.exportMode) {
7583
+ this.fitExportContent();
7584
+ this.forceDraw();
7585
+ }
7586
+ }
7587
+ /** Exact visible scene bounds in chart coordinates after fonts/assets exist. */
7588
+ exportSceneBounds() {
7589
+ const rects = [this.bgLayer, this.seatLayer, this.overlayLayer].map((layer) => layer.getClientRect({ relativeTo: this.stage })).filter((rect) => rect.width > 0 && rect.height > 0);
7590
+ if (!rects.length) return this.bounds;
7591
+ const minX = Math.min(...rects.map((rect) => rect.x));
7592
+ const minY = Math.min(...rects.map((rect) => rect.y));
7593
+ const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));
7594
+ const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));
7595
+ return {
7596
+ x: minX,
7597
+ y: minY,
7598
+ width: Math.max(1, maxX - minX),
7599
+ height: Math.max(1, maxY - minY)
7600
+ };
7601
+ }
7602
+ /** Fit exact rendered nodes, including rotated text/decor, with pixel padding. */
7603
+ fitExportContent() {
7604
+ if (!this.exportMode) return;
7605
+ const width = this.stage.width();
7606
+ const height = this.stage.height();
7607
+ const padding = Math.max(12, Math.min(36, Math.min(width, height) * 0.025));
7608
+ const fit = () => {
7609
+ const bounds2 = this.exportSceneBounds();
7610
+ const availableWidth = Math.max(1, width - padding * 2);
7611
+ const availableHeight = Math.max(1, height - padding * 2);
7612
+ const scale = Math.min(availableWidth / bounds2.width, availableHeight / bounds2.height) || 1;
7613
+ this.fitScale = scale;
7614
+ this.stage.scale({ x: scale, y: scale });
7615
+ this.stage.position({
7616
+ x: (width - bounds2.width * scale) / 2 - bounds2.x * scale,
7617
+ y: (height - bounds2.height * scale) / 2 - bounds2.y * scale
7618
+ });
7619
+ this.afterViewChange();
7620
+ };
7621
+ fit();
7622
+ fit();
7623
+ }
7624
+ /**
7625
+ * Capture a clean, opaque fixed-size canvas. The CSS host background is
7626
+ * painted explicitly because Konva layers themselves are transparent.
7627
+ */
7628
+ captureCanvas() {
7629
+ if (!this.exportMode) {
7630
+ throw new Error("captureCanvas() is available only on a renderer created with exportMode.");
7631
+ }
7632
+ this.forceDraw();
7633
+ let scene;
7634
+ try {
7635
+ scene = this.stage.toCanvas({ pixelRatio: 1, imageSmoothingEnabled: true });
7636
+ } catch (error) {
7637
+ const detail = error instanceof Error ? error.message : String(error);
7638
+ throw new Error(`The chart canvas could not be captured. A buyer-visible image may block export. ${detail}`);
7639
+ }
7640
+ const canvas = document.createElement("canvas");
7641
+ canvas.width = this.stage.width();
7642
+ canvas.height = this.stage.height();
7643
+ const context = canvas.getContext("2d");
7644
+ if (!context) throw new Error("Canvas 2D is unavailable; this browser cannot create an export.");
7645
+ context.fillStyle = this.canvasBackground;
7646
+ context.fillRect(0, 0, canvas.width, canvas.height);
7647
+ try {
7648
+ context.drawImage(scene, 0, 0);
7649
+ } finally {
7650
+ scene.width = 1;
7651
+ scene.height = 1;
7652
+ }
7653
+ return canvas;
7654
+ }
7428
7655
  updateFreeTextVisibility() {
7429
- const effectiveScale = this.effScale();
7656
+ const effectiveScale = this.exportMode ? Math.max(this.effScale(), LABEL_SCALE) : this.effScale();
7430
7657
  for (const { objectId, node, categoryKey, kind } of this.freeTextById.values()) {
7431
7658
  const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
7432
7659
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
@@ -7471,19 +7698,90 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7471
7698
  const rotation = presentation?.rotation ?? 0;
7472
7699
  const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
7473
7700
  const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
7474
- const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
7701
+ const push = (x, y, automaticAway) => this.rowLabelPlan.push({
7702
+ text,
7703
+ x,
7704
+ y,
7705
+ rotation,
7706
+ fontSize,
7707
+ ink,
7708
+ opacity,
7709
+ ...automaticAway ? { automaticAway } : {}
7710
+ });
7475
7711
  if (presentation?.position) {
7476
7712
  push(presentation.position.x, presentation.position.y);
7477
7713
  continue;
7478
7714
  }
7479
- const radians = obj.rotation * Math.PI / 180;
7480
- const along = { x: Math.cos(radians), y: Math.sin(radians) };
7481
7715
  const gap = this.seatR + 12;
7482
7716
  const preset = presentation?.positionPreset ?? "start";
7483
- if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
7484
- if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
7717
+ if (preset === "start" || preset === "both") {
7718
+ const radians = obj.rotation * Math.PI / 180;
7719
+ const away = { x: -Math.cos(radians), y: -Math.sin(radians) };
7720
+ push(first.x + away.x * gap, first.y + away.y * gap, away);
7721
+ }
7722
+ if (preset === "end" || preset === "both") {
7723
+ const radians = (obj.rotation + obj.curve) * Math.PI / 180;
7724
+ const away = { x: Math.cos(radians), y: Math.sin(radians) };
7725
+ push(last.x + away.x * gap, last.y + away.y * gap, away);
7726
+ }
7485
7727
  }
7486
7728
  }
7729
+ /** Axis-aligned world bounds of a centred, potentially rotated row label. */
7730
+ rowLabelBox(label, position, padding = ROW_LABEL_CLEARANCE) {
7731
+ const radians = label.rotation() * Math.PI / 180;
7732
+ const cos = Math.abs(Math.cos(radians));
7733
+ const sin = Math.abs(Math.sin(radians));
7734
+ const halfWidth = (label.width() * cos + label.height() * sin) / 2 + padding;
7735
+ const halfHeight = (label.width() * sin + label.height() * cos) / 2 + padding;
7736
+ return {
7737
+ x: position.x - halfWidth,
7738
+ y: position.y - halfHeight,
7739
+ width: halfWidth * 2,
7740
+ height: halfHeight * 2
7741
+ };
7742
+ }
7743
+ /**
7744
+ * Keep an automatic start/end label out of seat markers and already-painted
7745
+ * text. The authored point is never altered. Candidates are deliberately
7746
+ * bounded and deterministic: the closest clear offset wins; if a chart is so
7747
+ * dense that none is clear, the least-colliding candidate wins so the required
7748
+ * row label remains present rather than being silently dropped.
7749
+ */
7750
+ resolveAutomaticRowLabelPosition(label, plan, occupiedText) {
7751
+ const base = { x: plan.x, y: plan.y };
7752
+ const away = plan.automaticAway;
7753
+ if (!away) return base;
7754
+ const normal = { x: -away.y, y: away.x };
7755
+ const step = Math.max(7, Math.min(16, plan.fontSize * 0.75));
7756
+ const offsets = [{ x: 0, y: 0 }];
7757
+ for (let ring = 1; ring <= 4; ring++) {
7758
+ const distance = step * ring;
7759
+ offsets.push(
7760
+ { x: normal.x * distance, y: normal.y * distance },
7761
+ { x: -normal.x * distance, y: -normal.y * distance },
7762
+ { x: away.x * distance, y: away.y * distance },
7763
+ { x: (away.x + normal.x) * distance, y: (away.y + normal.y) * distance },
7764
+ { x: (away.x - normal.x) * distance, y: (away.y - normal.y) * distance }
7765
+ );
7766
+ }
7767
+ let best = base;
7768
+ let bestCollisions = Infinity;
7769
+ let bestDistance = Infinity;
7770
+ for (const offset of offsets) {
7771
+ const candidate = { x: base.x + offset.x, y: base.y + offset.y };
7772
+ const box = this.rowLabelBox(label, candidate);
7773
+ const seatCollisions = this.seatIndex ? queryRect(this.seatIndex, box, { mode: "overlap" }).length : 0;
7774
+ const collisions = seatCollisions + occupiedText.collisionCount(box);
7775
+ const distance = Math.hypot(offset.x, offset.y);
7776
+ if (collisions < bestCollisions || collisions === bestCollisions && distance < bestDistance) {
7777
+ best = candidate;
7778
+ bestCollisions = collisions;
7779
+ bestDistance = distance;
7780
+ }
7781
+ if (collisions === 0) break;
7782
+ }
7783
+ return best;
7784
+ }
7487
7785
  /** Row labels never carry status; the buyer just dims them under the same
7488
7786
  * category/price filters that dim their seats. */
7489
7787
  objectFilteredOut(obj) {
@@ -7492,7 +7790,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7492
7790
  return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
7493
7791
  }
7494
7792
  updateLabels() {
7495
- const effectiveScale = this.effScale();
7793
+ const effectiveScale = this.exportMode ? Math.max(this.effScale(), LABEL_SCALE) : this.effScale();
7496
7794
  const show = effectiveScale >= LABEL_SCALE;
7497
7795
  for (const [id, label] of this.boothLabelById) {
7498
7796
  const shape = this.circleById.get(id);
@@ -7583,6 +7881,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7583
7881
  this.seatLabelById.set(seat.id, t2);
7584
7882
  if (++count >= MAX_LABELS) break;
7585
7883
  }
7884
+ const occupiedText = new WorldBoxIndex();
7885
+ for (const { node } of this.freeTextById.values()) {
7886
+ if (!node.isVisible()) continue;
7887
+ const box = node.getClientRect({
7888
+ relativeTo: this.bgLayer,
7889
+ skipShadow: true,
7890
+ skipStroke: true
7891
+ });
7892
+ occupiedText.insert({
7893
+ x: box.x - ROW_LABEL_CLEARANCE,
7894
+ y: box.y - ROW_LABEL_CLEARANCE,
7895
+ width: box.width + ROW_LABEL_CLEARANCE * 2,
7896
+ height: box.height + ROW_LABEL_CLEARANCE * 2
7897
+ });
7898
+ }
7586
7899
  for (const rl of this.rowLabelPlan) {
7587
7900
  const screen = this.worldToScreen({ x: rl.x, y: rl.y });
7588
7901
  if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
@@ -7605,6 +7918,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7605
7918
  });
7606
7919
  t2.offsetX(t2.width() / 2);
7607
7920
  t2.offsetY(t2.height() / 2);
7921
+ const resolved = this.resolveAutomaticRowLabelPosition(t2, rl, occupiedText);
7922
+ t2.position(resolved);
7923
+ t2.setAttr("rowLabel", true);
7924
+ occupiedText.insert(this.rowLabelBox(t2, resolved));
7608
7925
  this.labelGroup.add(t2);
7609
7926
  }
7610
7927
  if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
@@ -9208,7 +9525,8 @@ var PickerController = class {
9208
9525
  if (!url) return;
9209
9526
  let ws;
9210
9527
  try {
9211
- ws = new WebSocket(url);
9528
+ const protocols = this.api.socketProtocols?.(this.key);
9529
+ ws = protocols && protocols.length ? new WebSocket(url, protocols) : new WebSocket(url);
9212
9530
  } catch {
9213
9531
  this.scheduleReconnect();
9214
9532
  return;
@@ -10520,7 +10838,6 @@ async function loadLocale(code) {
10520
10838
  rowInventoryCount,
10521
10839
  rowSeatPositions,
10522
10840
  seatCommercialMeta,
10523
- seatLabelPart,
10524
10841
  sectionGeometry,
10525
10842
  setLocale,
10526
10843
  setMoneyLocale,