@seatlayer/core 0.14.0 → 0.15.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 +299 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +299 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -635,6 +635,8 @@ var LABEL_SCALE = 1;
|
|
|
635
635
|
var SEAT_TAP_SLOP_PX = 14;
|
|
636
636
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
637
637
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
638
|
+
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
639
|
+
var PAN_START_SLOP_PX = 8;
|
|
638
640
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
639
641
|
var MAX_LABELS = 700;
|
|
640
642
|
var MARQUEE_RING_CAP = 2500;
|
|
@@ -785,6 +787,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
785
787
|
this.circleById = /* @__PURE__ */ new Map();
|
|
786
788
|
/** Booth block geometry, keyed by booth id (= the unit's rowId). */
|
|
787
789
|
this.boothDims = /* @__PURE__ */ new Map();
|
|
790
|
+
/** Booth label node so status changes can say HELD/SOLD on the full block. */
|
|
791
|
+
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
788
792
|
this.statusById = /* @__PURE__ */ new Map();
|
|
789
793
|
this.catColor = /* @__PURE__ */ new Map();
|
|
790
794
|
this.theme = {};
|
|
@@ -795,7 +799,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
795
799
|
/** Category order from the doc — the stable index into the CB palette. */
|
|
796
800
|
this.catOrder = [];
|
|
797
801
|
this.selection = /* @__PURE__ */ new Set();
|
|
798
|
-
|
|
802
|
+
/** Whole-seat/block selection markers: outline + non-colour check cue. */
|
|
803
|
+
this.selectionMarkers = /* @__PURE__ */ new Map();
|
|
804
|
+
/** Held by this picker instance, not by another buyer. */
|
|
805
|
+
this.ownedHold = /* @__PURE__ */ new Set();
|
|
806
|
+
/** One selected seat being inspected before it is committed to the cart. */
|
|
807
|
+
this.selectionFocusId = null;
|
|
799
808
|
this.focusedId = null;
|
|
800
809
|
/**
|
|
801
810
|
* Accessibility filter: `null` = off; `[]` = dim all non-accessible free seats;
|
|
@@ -864,7 +873,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
864
873
|
this.pointers = /* @__PURE__ */ new Map();
|
|
865
874
|
this.pinch = null;
|
|
866
875
|
this.panLast = null;
|
|
867
|
-
|
|
876
|
+
this.panStart = null;
|
|
877
|
+
this.panStarted = false;
|
|
878
|
+
/** Maximum displacement from gesture start — suppress taps only after a real pan/pinch. */
|
|
868
879
|
this.moved = 0;
|
|
869
880
|
/**
|
|
870
881
|
* Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
|
|
@@ -907,15 +918,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
907
918
|
// pipeline. We rely on bubbling instead.
|
|
908
919
|
this.onPointerDown = (e) => {
|
|
909
920
|
this.cancelGlide();
|
|
910
|
-
|
|
921
|
+
const local = this.toLocal(e);
|
|
922
|
+
this.pointers.set(e.pointerId, local);
|
|
911
923
|
if (this.pointers.size === 1) {
|
|
912
924
|
this.moved = 0;
|
|
913
925
|
this.pinch = null;
|
|
926
|
+
this.panStart = local;
|
|
927
|
+
this.panStarted = false;
|
|
914
928
|
if (this.opts.manageMode && this.opts.marqueeSelect && e.pointerType !== "touch" && e.button === 0 && this.getRung() === "seats") {
|
|
915
|
-
this.beginMarquee(
|
|
929
|
+
this.beginMarquee(local);
|
|
916
930
|
this.panLast = null;
|
|
917
931
|
} else {
|
|
918
|
-
this.panLast =
|
|
932
|
+
this.panLast = local;
|
|
919
933
|
}
|
|
920
934
|
} else if (this.pointers.size === 2) {
|
|
921
935
|
if (this.marquee) this.cancelMarquee();
|
|
@@ -928,15 +942,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
928
942
|
worldMid: { x: (mid.x - this.stage.x()) / s, y: (mid.y - this.stage.y()) / s }
|
|
929
943
|
};
|
|
930
944
|
this.panLast = null;
|
|
945
|
+
this.panStart = null;
|
|
946
|
+
this.panStarted = true;
|
|
947
|
+
this.moved = PAN_START_SLOP_PX + 1;
|
|
931
948
|
}
|
|
932
949
|
};
|
|
933
950
|
this.onPointerMove = (e) => {
|
|
934
951
|
if (!this.pointers.has(e.pointerId)) return;
|
|
935
952
|
e.preventDefault();
|
|
936
953
|
const p = this.toLocal(e);
|
|
937
|
-
const prev = this.pointers.get(e.pointerId);
|
|
938
|
-
this.moved += Math.hypot(p.x - prev.x, p.y - prev.y);
|
|
939
954
|
this.pointers.set(e.pointerId, p);
|
|
955
|
+
if (this.pointers.size === 1 && this.panStart) {
|
|
956
|
+
this.moved = Math.max(this.moved, Math.hypot(p.x - this.panStart.x, p.y - this.panStart.y));
|
|
957
|
+
} else if (this.pointers.size >= 2) {
|
|
958
|
+
this.moved = PAN_START_SLOP_PX + 1;
|
|
959
|
+
}
|
|
940
960
|
if (this.marquee) {
|
|
941
961
|
this.updateMarquee(p);
|
|
942
962
|
return;
|
|
@@ -956,6 +976,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
956
976
|
this.stage.batchDraw();
|
|
957
977
|
this.scheduleViewChange();
|
|
958
978
|
} else if (this.panLast && this.pointers.size === 1) {
|
|
979
|
+
if (!this.panStarted) {
|
|
980
|
+
if (this.moved <= PAN_START_SLOP_PX) return;
|
|
981
|
+
this.panStarted = true;
|
|
982
|
+
}
|
|
959
983
|
this.stage.position({
|
|
960
984
|
x: this.stage.x() + (p.x - this.panLast.x),
|
|
961
985
|
y: this.stage.y() + (p.y - this.panLast.y)
|
|
@@ -976,9 +1000,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
976
1000
|
if (this.pointers.size < 2) this.pinch = null;
|
|
977
1001
|
if (this.pointers.size === 1) {
|
|
978
1002
|
this.panLast = [...this.pointers.values()][0];
|
|
1003
|
+
this.panStart = this.panLast;
|
|
1004
|
+
this.panStarted = true;
|
|
979
1005
|
}
|
|
980
1006
|
if (this.pointers.size === 0) {
|
|
981
1007
|
this.panLast = null;
|
|
1008
|
+
this.panStart = null;
|
|
1009
|
+
this.panStarted = false;
|
|
982
1010
|
this.afterViewChange();
|
|
983
1011
|
}
|
|
984
1012
|
};
|
|
@@ -1060,7 +1088,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1060
1088
|
this.labelGroup.destroyChildren();
|
|
1061
1089
|
this.circleById.clear();
|
|
1062
1090
|
this.boothDims.clear();
|
|
1063
|
-
this.
|
|
1091
|
+
this.boothLabelById.clear();
|
|
1092
|
+
this.selectionMarkers.clear();
|
|
1093
|
+
this.ownedHold.clear();
|
|
1094
|
+
this.selectionFocusId = null;
|
|
1064
1095
|
this.statusById.clear();
|
|
1065
1096
|
this.selection.clear();
|
|
1066
1097
|
this.seatById.clear();
|
|
@@ -1180,6 +1211,48 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1180
1211
|
} else {
|
|
1181
1212
|
this.seatLayer.batchDraw();
|
|
1182
1213
|
}
|
|
1214
|
+
if (this.effScale() > LABEL_SCALE) this.updateLabels();
|
|
1215
|
+
}
|
|
1216
|
+
setOwnedHold(seatIds) {
|
|
1217
|
+
const next = new Set((seatIds ?? []).filter((id) => this.statusById.has(id)));
|
|
1218
|
+
const touched = /* @__PURE__ */ new Set([...this.ownedHold, ...next]);
|
|
1219
|
+
this.ownedHold = next;
|
|
1220
|
+
for (const id of touched) {
|
|
1221
|
+
const shape = this.circleById.get(id);
|
|
1222
|
+
if (shape) this.paintSeat(shape, id);
|
|
1223
|
+
this.syncSelectionMarker(id);
|
|
1224
|
+
}
|
|
1225
|
+
if (!touched.size) return;
|
|
1226
|
+
if (this.cached) {
|
|
1227
|
+
if (this.recacheTimer) clearTimeout(this.recacheTimer);
|
|
1228
|
+
this.recacheTimer = setTimeout(() => this.cacheSeatLayer(), 150);
|
|
1229
|
+
} else {
|
|
1230
|
+
this.seatLayer.batchDraw();
|
|
1231
|
+
}
|
|
1232
|
+
if (this.effScale() > LABEL_SCALE) this.updateLabels();
|
|
1233
|
+
this.overlayLayer.batchDraw();
|
|
1234
|
+
}
|
|
1235
|
+
setSelectionFocus(seatId) {
|
|
1236
|
+
const next = seatId && this.selection.has(seatId) ? seatId : null;
|
|
1237
|
+
if (next === this.selectionFocusId) return;
|
|
1238
|
+
const previous = this.selectionFocusId;
|
|
1239
|
+
this.selectionFocusId = next;
|
|
1240
|
+
for (const seat of this.seats) {
|
|
1241
|
+
const shape = this.circleById.get(seat.id);
|
|
1242
|
+
if (shape) this.paintSeat(shape, seat.id);
|
|
1243
|
+
}
|
|
1244
|
+
if (previous) this.syncSelectionMarker(previous);
|
|
1245
|
+
if (next) this.syncSelectionMarker(next);
|
|
1246
|
+
for (const [id, marker] of this.selectionMarkers) {
|
|
1247
|
+
marker.opacity(!next || id === next ? 1 : 0.2);
|
|
1248
|
+
}
|
|
1249
|
+
if (this.cached) {
|
|
1250
|
+
this.seatLayer.clearCache();
|
|
1251
|
+
this.cacheSeatLayer();
|
|
1252
|
+
} else {
|
|
1253
|
+
this.seatLayer.batchDraw();
|
|
1254
|
+
}
|
|
1255
|
+
this.overlayLayer.batchDraw();
|
|
1183
1256
|
}
|
|
1184
1257
|
getStatus(seatId) {
|
|
1185
1258
|
return this.statusById.get(seatId) ?? "free";
|
|
@@ -1202,6 +1275,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1202
1275
|
setManageInteraction(options) {
|
|
1203
1276
|
if (this.marquee) this.cancelMarquee();
|
|
1204
1277
|
this.panLast = null;
|
|
1278
|
+
this.panStart = null;
|
|
1279
|
+
this.panStarted = false;
|
|
1205
1280
|
this.opts.manageMode = options.manageMode;
|
|
1206
1281
|
this.opts.marqueeSelect = options.marqueeSelect;
|
|
1207
1282
|
this.opts.selectableStatuses = [...options.selectableStatuses];
|
|
@@ -1798,6 +1873,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1798
1873
|
t2.offsetX(t2.width() / 2);
|
|
1799
1874
|
t2.offsetY(t2.height() / 2);
|
|
1800
1875
|
this.hasBoothText = true;
|
|
1876
|
+
this.boothLabelById.set(seat.id, t2);
|
|
1801
1877
|
target.add(t2);
|
|
1802
1878
|
}
|
|
1803
1879
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
@@ -1812,6 +1888,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1812
1888
|
const status = this.statusById.get(id) ?? "free";
|
|
1813
1889
|
const selected = this.selection.has(id);
|
|
1814
1890
|
const base = this.seatBaseColor(seat.categoryKey);
|
|
1891
|
+
const boothLabel = this.boothLabelById.get(id);
|
|
1815
1892
|
c.dash([]);
|
|
1816
1893
|
c.strokeWidth(0);
|
|
1817
1894
|
c.stroke("");
|
|
@@ -1823,7 +1900,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1823
1900
|
);
|
|
1824
1901
|
break;
|
|
1825
1902
|
case "held":
|
|
1826
|
-
|
|
1903
|
+
if (this.ownedHold.has(id)) {
|
|
1904
|
+
c.fill(lighten(base, this.effSelection === DEF_SELECTION ? 0.24 : 0.1));
|
|
1905
|
+
c.stroke(this.effSelection);
|
|
1906
|
+
c.strokeWidth(3);
|
|
1907
|
+
} else {
|
|
1908
|
+
c.fill(HELD_FILL);
|
|
1909
|
+
}
|
|
1827
1910
|
break;
|
|
1828
1911
|
case "booked":
|
|
1829
1912
|
if (this.colorblind) {
|
|
@@ -1843,6 +1926,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1843
1926
|
c.dash([2, 2]);
|
|
1844
1927
|
break;
|
|
1845
1928
|
}
|
|
1929
|
+
if (boothLabel) {
|
|
1930
|
+
const owned = status === "held" && this.ownedHold.has(id);
|
|
1931
|
+
boothLabel.text(status === "booked" ? "SOLD" : status === "held" ? owned ? "HELD BY YOU" : "HELD" : seat.label);
|
|
1932
|
+
boothLabel.fontSize(status === "free" ? 10 : Math.min(10, Math.max(6, this.boothDims.get(seat.rowId)?.width ?? 40) / 6));
|
|
1933
|
+
boothLabel.fontStyle(status === "free" ? "600" : "800");
|
|
1934
|
+
boothLabel.fill(status === "free" ? this.theme.seatLabelColor ?? DEF_SEAT_LABEL : "#ffffff");
|
|
1935
|
+
boothLabel.offsetX(boothLabel.width() / 2);
|
|
1936
|
+
boothLabel.offsetY(boothLabel.height() / 2);
|
|
1937
|
+
}
|
|
1846
1938
|
if (this.accessFilter && status === "free" && !selected && !seatMatchesAccess(seat, this.accessFilter)) {
|
|
1847
1939
|
c.opacity(0.25);
|
|
1848
1940
|
}
|
|
@@ -1870,6 +1962,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1870
1962
|
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
1871
1963
|
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
1872
1964
|
}
|
|
1965
|
+
if (this.selectionFocusId && id !== this.selectionFocusId) c.opacity(Math.min(c.opacity(), 0.16));
|
|
1873
1966
|
}
|
|
1874
1967
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
1875
1968
|
seatInClosedSection(id) {
|
|
@@ -1937,7 +2030,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1937
2030
|
this.drawFocusBackdrop(id);
|
|
1938
2031
|
this.repaintSectionsAndSeats();
|
|
1939
2032
|
this.updateLOD();
|
|
1940
|
-
this.focusRegion(id);
|
|
2033
|
+
this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
|
|
1941
2034
|
}
|
|
1942
2035
|
/** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
|
|
1943
2036
|
clearSectionFocus() {
|
|
@@ -2730,30 +2823,101 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2730
2823
|
const c = this.circleById.get(id);
|
|
2731
2824
|
if (on) {
|
|
2732
2825
|
this.selection.add(id);
|
|
2733
|
-
const seat = this.seatById.get(id);
|
|
2734
|
-
const ring = new Circle({
|
|
2735
|
-
x: seat.x,
|
|
2736
|
-
y: seat.y,
|
|
2737
|
-
radius: this.seatR,
|
|
2738
|
-
stroke: this.effSelection,
|
|
2739
|
-
strokeWidth: 3,
|
|
2740
|
-
listening: false,
|
|
2741
|
-
perfectDrawEnabled: false,
|
|
2742
|
-
shadowForStrokeEnabled: false
|
|
2743
|
-
});
|
|
2744
|
-
this.selectionRings.set(id, ring);
|
|
2745
|
-
this.overlayLayer.add(ring);
|
|
2746
2826
|
} else {
|
|
2827
|
+
if (this.selectionFocusId === id) this.setSelectionFocus(null);
|
|
2747
2828
|
this.selection.delete(id);
|
|
2748
|
-
const ring = this.selectionRings.get(id);
|
|
2749
|
-
ring?.destroy();
|
|
2750
|
-
this.selectionRings.delete(id);
|
|
2751
2829
|
}
|
|
2830
|
+
this.syncSelectionMarker(id);
|
|
2752
2831
|
if (c) {
|
|
2753
2832
|
this.paintSeat(c, id);
|
|
2754
2833
|
if (!silent && !this.cached) this.seatLayer.batchDraw();
|
|
2755
2834
|
}
|
|
2756
2835
|
}
|
|
2836
|
+
/** Rebuild one marker after selected/held/candidate state changes. */
|
|
2837
|
+
syncSelectionMarker(id) {
|
|
2838
|
+
this.selectionMarkers.get(id)?.destroy();
|
|
2839
|
+
this.selectionMarkers.delete(id);
|
|
2840
|
+
if (!this.selection.has(id) && !this.ownedHold.has(id)) return;
|
|
2841
|
+
const seat = this.seatById.get(id);
|
|
2842
|
+
if (!seat) return;
|
|
2843
|
+
const candidate = this.selectionFocusId === id;
|
|
2844
|
+
const dims = this.boothDims.get(seat.rowId);
|
|
2845
|
+
const marker = new Group({
|
|
2846
|
+
x: seat.x,
|
|
2847
|
+
y: seat.y,
|
|
2848
|
+
rotation: dims?.rotation ?? 0,
|
|
2849
|
+
listening: false,
|
|
2850
|
+
perfectDrawEnabled: false,
|
|
2851
|
+
opacity: this.selectionFocusId && !candidate ? 0.2 : 1
|
|
2852
|
+
});
|
|
2853
|
+
const common = {
|
|
2854
|
+
stroke: this.effSelection,
|
|
2855
|
+
listening: false,
|
|
2856
|
+
perfectDrawEnabled: false,
|
|
2857
|
+
shadowForStrokeEnabled: false
|
|
2858
|
+
};
|
|
2859
|
+
if (dims) {
|
|
2860
|
+
marker.add(new Rect({
|
|
2861
|
+
...common,
|
|
2862
|
+
width: dims.width,
|
|
2863
|
+
height: dims.height,
|
|
2864
|
+
offsetX: dims.width / 2,
|
|
2865
|
+
offsetY: dims.height / 2,
|
|
2866
|
+
cornerRadius: 4,
|
|
2867
|
+
strokeWidth: candidate ? 4 : 3
|
|
2868
|
+
}));
|
|
2869
|
+
if (candidate) {
|
|
2870
|
+
marker.add(new Rect({
|
|
2871
|
+
...common,
|
|
2872
|
+
width: dims.width + 10,
|
|
2873
|
+
height: dims.height + 10,
|
|
2874
|
+
offsetX: (dims.width + 10) / 2,
|
|
2875
|
+
offsetY: (dims.height + 10) / 2,
|
|
2876
|
+
cornerRadius: 7,
|
|
2877
|
+
strokeWidth: 2,
|
|
2878
|
+
opacity: 0.55
|
|
2879
|
+
}));
|
|
2880
|
+
} else {
|
|
2881
|
+
const badgeX = Math.max(0, dims.width / 2 - 14);
|
|
2882
|
+
const badgeY = -Math.max(0, dims.height / 2 - 14);
|
|
2883
|
+
marker.add(new Circle({ x: badgeX, y: badgeY, radius: 10, fill: this.effSelection, listening: false }));
|
|
2884
|
+
marker.add(new Line({
|
|
2885
|
+
x: badgeX,
|
|
2886
|
+
y: badgeY,
|
|
2887
|
+
points: [-4.5, 0, -1, 3.5, 5.5, -4.5],
|
|
2888
|
+
stroke: isLightColor(this.effSelection) ? "#0b1220" : "#ffffff",
|
|
2889
|
+
strokeWidth: 2.4,
|
|
2890
|
+
lineCap: "round",
|
|
2891
|
+
lineJoin: "round",
|
|
2892
|
+
listening: false
|
|
2893
|
+
}));
|
|
2894
|
+
}
|
|
2895
|
+
} else {
|
|
2896
|
+
marker.add(new Circle({ ...common, radius: this.seatR + (candidate ? 4.5 : 2.5), strokeWidth: candidate ? 3.5 : 2.5 }));
|
|
2897
|
+
if (candidate) {
|
|
2898
|
+
marker.add(new Circle({
|
|
2899
|
+
...common,
|
|
2900
|
+
radius: this.seatR + 8,
|
|
2901
|
+
strokeWidth: 2,
|
|
2902
|
+
opacity: 0.55
|
|
2903
|
+
}));
|
|
2904
|
+
} else {
|
|
2905
|
+
marker.add(new Line({
|
|
2906
|
+
points: [-this.seatR * 0.52, 0, -this.seatR * 0.12, this.seatR * 0.4, this.seatR * 0.6, -this.seatR * 0.46],
|
|
2907
|
+
stroke: "#ffffff",
|
|
2908
|
+
strokeWidth: Math.max(2.6, this.seatR * 0.34),
|
|
2909
|
+
lineCap: "round",
|
|
2910
|
+
lineJoin: "round",
|
|
2911
|
+
shadowColor: "#0b1220",
|
|
2912
|
+
shadowBlur: 1.5,
|
|
2913
|
+
shadowOpacity: 0.55,
|
|
2914
|
+
listening: false
|
|
2915
|
+
}));
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
this.selectionMarkers.set(id, marker);
|
|
2919
|
+
this.overlayLayer.add(marker);
|
|
2920
|
+
}
|
|
2757
2921
|
// ---- interaction ----------------------------------------------------------
|
|
2758
2922
|
/**
|
|
2759
2923
|
* The stage scale `focusRegion(id)` would settle at — i.e. the zoom that
|
|
@@ -2771,7 +2935,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2771
2935
|
const { min, max } = this.zoomBounds();
|
|
2772
2936
|
const margin = 1.12;
|
|
2773
2937
|
if (b.width <= 0 || b.height <= 0) return this.stage.scaleX();
|
|
2774
|
-
|
|
2938
|
+
const frameScale = Math.min(w / (b.width * margin), h / (b.height * margin));
|
|
2939
|
+
return clamp(Math.max(frameScale, SEAT_FOCUS_SCALE), min, max);
|
|
2775
2940
|
}
|
|
2776
2941
|
/**
|
|
2777
2942
|
* Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
|
|
@@ -2829,7 +2994,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2829
2994
|
}
|
|
2830
2995
|
wireInteraction() {
|
|
2831
2996
|
this.seatLayer.on("click tap", (e) => {
|
|
2832
|
-
if (this.moved >
|
|
2997
|
+
if (this.moved > PAN_START_SLOP_PX) return;
|
|
2833
2998
|
const id = seatIdOf(e.target);
|
|
2834
2999
|
if (!id) return;
|
|
2835
3000
|
this.handleSeatTap(id);
|
|
@@ -2859,7 +3024,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2859
3024
|
this.zoomAbout(this.stage.scaleX() * clamp(factor, 0.5, 2), pointer);
|
|
2860
3025
|
});
|
|
2861
3026
|
this.stage.on("click tap", (e) => {
|
|
2862
|
-
if (this.moved >
|
|
3027
|
+
if (this.moved > PAN_START_SLOP_PX) return;
|
|
2863
3028
|
const pointer = this.stage.getPointerPosition();
|
|
2864
3029
|
if (!pointer) return;
|
|
2865
3030
|
if (!this.cached) {
|
|
@@ -2943,12 +3108,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2943
3108
|
this.stage.batchDraw();
|
|
2944
3109
|
}
|
|
2945
3110
|
/** Zoom + pan so world-rect `b` fills the viewport (with a small margin). */
|
|
2946
|
-
zoomToBounds(b) {
|
|
3111
|
+
zoomToBounds(b, minScale) {
|
|
2947
3112
|
const w = this.stage.width();
|
|
2948
3113
|
const h = this.stage.height();
|
|
2949
3114
|
const { min, max } = this.zoomBounds();
|
|
2950
3115
|
const margin = 1.12;
|
|
2951
|
-
const
|
|
3116
|
+
const frameScale = Math.min(w / (b.width * margin), h / (b.height * margin));
|
|
3117
|
+
const scale = clamp(Math.max(frameScale, minScale ?? min), min, max);
|
|
2952
3118
|
this.stage.scale({ x: scale, y: scale });
|
|
2953
3119
|
this.stage.position({
|
|
2954
3120
|
x: w / 2 - (b.x + b.width / 2) * scale,
|
|
@@ -2972,14 +3138,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2972
3138
|
if (!b) return;
|
|
2973
3139
|
this.cancelGlide();
|
|
2974
3140
|
if (opts?.animate === false || this.reducedMotion) {
|
|
2975
|
-
this.zoomToBounds(b);
|
|
3141
|
+
this.zoomToBounds(b, opts?.minScale);
|
|
2976
3142
|
return;
|
|
2977
3143
|
}
|
|
2978
3144
|
const w = this.stage.width();
|
|
2979
3145
|
const h = this.stage.height();
|
|
2980
3146
|
const { min, max } = this.zoomBounds();
|
|
2981
3147
|
const margin = 1.12;
|
|
2982
|
-
const
|
|
3148
|
+
const frameScale = Math.min(w / (b.width * margin), h / (b.height * margin));
|
|
3149
|
+
const toScale = clamp(Math.max(frameScale, opts?.minScale ?? min), min, max);
|
|
2983
3150
|
const toX = w / 2 - (b.x + b.width / 2) * toScale;
|
|
2984
3151
|
const toY = h / 2 - (b.y + b.height / 2) * toScale;
|
|
2985
3152
|
const fromScale = this.stage.scaleX();
|
|
@@ -3034,7 +3201,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3034
3201
|
this.zoomToFit();
|
|
3035
3202
|
return;
|
|
3036
3203
|
}
|
|
3037
|
-
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
|
|
3204
|
+
const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_FOCUS_SCALE, CACHE_THRESHOLD * 1.3);
|
|
3038
3205
|
const w = this.stage.width();
|
|
3039
3206
|
const h = this.stage.height();
|
|
3040
3207
|
const cx = this.bounds.x + this.bounds.width / 2;
|
|
@@ -3120,19 +3287,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3120
3287
|
for (const seat of this.seats) {
|
|
3121
3288
|
if (seat.kind === "booth") continue;
|
|
3122
3289
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
3290
|
+
const status = this.statusById.get(seat.id) ?? "free";
|
|
3291
|
+
const statusCue = status === "booked" ? "\xD7" : status === "held" && !this.ownedHold.has(seat.id) ? "H" : null;
|
|
3123
3292
|
const t2 = new Text({
|
|
3124
3293
|
x: seat.x,
|
|
3125
3294
|
y: seat.y,
|
|
3126
|
-
text: seat.label,
|
|
3127
|
-
fontSize: 7,
|
|
3128
|
-
fontStyle: "600",
|
|
3295
|
+
text: statusCue ?? seat.label,
|
|
3296
|
+
fontSize: statusCue ? status === "booked" ? 13 : 8 : 7,
|
|
3297
|
+
fontStyle: statusCue ? "800" : "600",
|
|
3129
3298
|
fontFamily: this.labelFont(),
|
|
3130
|
-
fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3299
|
+
fill: statusCue ? "#ffffff" : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
|
|
3131
3300
|
listening: false,
|
|
3132
3301
|
perfectDrawEnabled: false
|
|
3133
3302
|
});
|
|
3134
3303
|
const maxW = this.seatR * 2 - 3;
|
|
3135
|
-
if (t2.width() > maxW) t2.fontSize(Math.max(4,
|
|
3304
|
+
if (t2.width() > maxW) t2.fontSize(Math.max(4, t2.fontSize() * maxW / t2.width()));
|
|
3136
3305
|
if (t2.fontSize() < 4.2) {
|
|
3137
3306
|
t2.destroy();
|
|
3138
3307
|
continue;
|
|
@@ -3234,6 +3403,8 @@ var PickerController = class {
|
|
|
3234
3403
|
this.seatTiers = /* @__PURE__ */ new Map();
|
|
3235
3404
|
/** id → seat, for the section-summary breakdown (renderer members are ids). */
|
|
3236
3405
|
this.seatById = /* @__PURE__ */ new Map();
|
|
3406
|
+
/** id → buyer-facing spatial metadata used by every tooltip/confirm surface. */
|
|
3407
|
+
this.seatContext = /* @__PURE__ */ new Map();
|
|
3237
3408
|
this.allIds = [];
|
|
3238
3409
|
// realtime socket
|
|
3239
3410
|
this.ws = null;
|
|
@@ -3312,12 +3483,28 @@ var PickerController = class {
|
|
|
3312
3483
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
3313
3484
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
3314
3485
|
this.seatById = /* @__PURE__ */ new Map();
|
|
3486
|
+
this.seatContext = /* @__PURE__ */ new Map();
|
|
3315
3487
|
this.allIds = [];
|
|
3488
|
+
const chartObjects = new Map(allObjects(res.doc).map((object) => [object.id, object]));
|
|
3489
|
+
const membership = computeSections(res.doc);
|
|
3490
|
+
const sectionLabels = new Map(
|
|
3491
|
+
[...membership.sections, ...membership.ungrouped ? [membership.ungrouped] : []].map((section) => [section.id, section.label])
|
|
3492
|
+
);
|
|
3316
3493
|
for (const s of expandChart(res.doc)) {
|
|
3317
3494
|
this.labelToId.set(s.label, s.id);
|
|
3318
3495
|
this.labelToSeat.set(s.label, s);
|
|
3319
3496
|
this.seatById.set(s.id, s);
|
|
3320
3497
|
this.allIds.push(s.id);
|
|
3498
|
+
const source = chartObjects.get(s.rowId);
|
|
3499
|
+
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
3500
|
+
const rowLabel = s.kind === "booth" ? void 0 : sourceLabel;
|
|
3501
|
+
const labelParts = s.label.split("-");
|
|
3502
|
+
const seatNumber = rowLabel && s.label.startsWith(`${rowLabel}-`) ? s.label.slice(rowLabel.length + 1) : s.kind === "booth" ? s.label : labelParts[labelParts.length - 1] ?? s.label;
|
|
3503
|
+
this.seatContext.set(s.id, {
|
|
3504
|
+
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
3505
|
+
rowLabel,
|
|
3506
|
+
seatNumber
|
|
3507
|
+
});
|
|
3321
3508
|
}
|
|
3322
3509
|
const currency = res.event.currency ?? this.opts.currency;
|
|
3323
3510
|
this.currency = currency ?? "USD";
|
|
@@ -3387,6 +3574,11 @@ var PickerController = class {
|
|
|
3387
3574
|
if (!this.renderer) return [];
|
|
3388
3575
|
return this.renderer.getSelection().map((s) => this.toSeat(s));
|
|
3389
3576
|
}
|
|
3577
|
+
/** Enriched metadata for a seat confirmation card or tooltip. */
|
|
3578
|
+
seatDetails(seatId) {
|
|
3579
|
+
const seat = this.seatById.get(seatId);
|
|
3580
|
+
return seat ? this.describeSeat(seat) : null;
|
|
3581
|
+
}
|
|
3390
3582
|
clearSelection() {
|
|
3391
3583
|
this.renderer?.clearSelection();
|
|
3392
3584
|
this.emitSelectionChange();
|
|
@@ -3426,6 +3618,19 @@ var PickerController = class {
|
|
|
3426
3618
|
throw err;
|
|
3427
3619
|
}
|
|
3428
3620
|
}
|
|
3621
|
+
/** Restore an active server hold without creating or extending inventory. */
|
|
3622
|
+
async resumeHold(holdId) {
|
|
3623
|
+
if (this.closed || !holdId || !this.api.resume) return null;
|
|
3624
|
+
const result = await this.api.resume(this.key, holdId);
|
|
3625
|
+
if (this.closed) return null;
|
|
3626
|
+
const labels = [...new Set(result.items.map((item) => item.label))];
|
|
3627
|
+
if (!labels.length) return null;
|
|
3628
|
+
this.setHold(
|
|
3629
|
+
{ holdId: result.holdId, labels, expiresAt: result.expiresAt, items: result.items },
|
|
3630
|
+
"restored"
|
|
3631
|
+
);
|
|
3632
|
+
return this.hold_;
|
|
3633
|
+
}
|
|
3429
3634
|
/**
|
|
3430
3635
|
* P4 "need more time?": extend the OPEN hold's server-side expiry and re-arm
|
|
3431
3636
|
* the client expiry timer to match (via setHold), so the controller doesn't
|
|
@@ -3515,7 +3720,7 @@ var PickerController = class {
|
|
|
3515
3720
|
async bestAvailable(qty, categoryKey) {
|
|
3516
3721
|
const r = this.renderer;
|
|
3517
3722
|
if (!r) return null;
|
|
3518
|
-
if (this.hold_
|
|
3723
|
+
if (this.hold_ && !await this.release()) return null;
|
|
3519
3724
|
try {
|
|
3520
3725
|
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
3521
3726
|
r.clearSelection();
|
|
@@ -3600,15 +3805,25 @@ var PickerController = class {
|
|
|
3600
3805
|
/** Release the whole open hold (if any), repaint those seats free. */
|
|
3601
3806
|
async release() {
|
|
3602
3807
|
const hold = this.hold_;
|
|
3603
|
-
if (!hold) return;
|
|
3604
|
-
this.clearHold();
|
|
3605
|
-
const ids = hold.labels.map((l) => this.labelToId.get(l)).filter((v) => !!v);
|
|
3606
|
-
if (ids.length) this.renderer?.setStatus(ids, "free");
|
|
3808
|
+
if (!hold) return true;
|
|
3607
3809
|
try {
|
|
3608
|
-
await this.api.release(this.key, hold.labels, hold.holdId);
|
|
3810
|
+
const result = await this.api.release(this.key, hold.labels, hold.holdId);
|
|
3811
|
+
if (!this.releaseConfirmed(result, hold.labels)) {
|
|
3812
|
+
await this.resnapshot();
|
|
3813
|
+
return false;
|
|
3814
|
+
}
|
|
3609
3815
|
} catch (err) {
|
|
3610
3816
|
this.emitError(err);
|
|
3817
|
+
return false;
|
|
3818
|
+
}
|
|
3819
|
+
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
3820
|
+
this.clearHold();
|
|
3821
|
+
const ids = hold.labels.map((l) => this.labelToId.get(l)).filter((v) => !!v);
|
|
3822
|
+
if (ids.length) {
|
|
3823
|
+
this.renderer?.deselect(ids);
|
|
3824
|
+
this.renderer?.setStatus(ids, "free");
|
|
3611
3825
|
}
|
|
3826
|
+
return true;
|
|
3612
3827
|
}
|
|
3613
3828
|
/**
|
|
3614
3829
|
* Release just some labels from the open hold, keeping the rest held (used when
|
|
@@ -3616,19 +3831,35 @@ var PickerController = class {
|
|
|
3616
3831
|
*/
|
|
3617
3832
|
async releaseLabels(labels) {
|
|
3618
3833
|
const hold = this.hold_;
|
|
3619
|
-
if (!hold) return;
|
|
3834
|
+
if (!hold) return true;
|
|
3620
3835
|
const drop = labels.filter((l) => hold.labels.includes(l));
|
|
3621
|
-
if (!drop.length) return;
|
|
3622
|
-
const remaining = hold.labels.filter((l) => !drop.includes(l));
|
|
3623
|
-
if (remaining.length) this.setHold({ ...hold, labels: remaining });
|
|
3624
|
-
else this.clearHold();
|
|
3625
|
-
const ids = drop.map((l) => this.labelToId.get(l)).filter((v) => !!v);
|
|
3626
|
-
if (ids.length) this.renderer?.setStatus(ids, "free");
|
|
3836
|
+
if (!drop.length) return true;
|
|
3627
3837
|
try {
|
|
3628
|
-
await this.api.release(this.key, drop, hold.holdId);
|
|
3838
|
+
const result = await this.api.release(this.key, drop, hold.holdId);
|
|
3839
|
+
if (!this.releaseConfirmed(result, drop)) {
|
|
3840
|
+
await this.resnapshot();
|
|
3841
|
+
return false;
|
|
3842
|
+
}
|
|
3629
3843
|
} catch (err) {
|
|
3630
3844
|
this.emitError(err);
|
|
3845
|
+
return false;
|
|
3631
3846
|
}
|
|
3847
|
+
if (this.hold_?.holdId !== hold.holdId) return true;
|
|
3848
|
+
const remaining = hold.labels.filter((l) => !drop.includes(l));
|
|
3849
|
+
const remainingItems = hold.items?.filter((item) => !drop.includes(item.label));
|
|
3850
|
+
if (remaining.length) this.setHold({ ...hold, labels: remaining, items: remainingItems });
|
|
3851
|
+
else this.clearHold();
|
|
3852
|
+
const ids = drop.map((l) => this.labelToId.get(l)).filter((v) => !!v);
|
|
3853
|
+
if (ids.length) {
|
|
3854
|
+
this.renderer?.deselect(ids);
|
|
3855
|
+
this.renderer?.setStatus(ids, "free");
|
|
3856
|
+
}
|
|
3857
|
+
return true;
|
|
3858
|
+
}
|
|
3859
|
+
/** New transports return the exact labels released; tolerate older adapters. */
|
|
3860
|
+
releaseConfirmed(result, requested) {
|
|
3861
|
+
const released = result?.released;
|
|
3862
|
+
return !Array.isArray(released) || requested.every((label) => released.includes(label));
|
|
3632
3863
|
}
|
|
3633
3864
|
// ---- renderer proxies (so consumers don't reach through) ------------------
|
|
3634
3865
|
setStatus(ids, status) {
|
|
@@ -3652,6 +3883,9 @@ var PickerController = class {
|
|
|
3652
3883
|
worldToScreen(p) {
|
|
3653
3884
|
return this.renderer?.worldToScreen(p) ?? { x: 0, y: 0 };
|
|
3654
3885
|
}
|
|
3886
|
+
setSelectionFocus(seatId) {
|
|
3887
|
+
this.renderer?.setSelectionFocus?.(seatId);
|
|
3888
|
+
}
|
|
3655
3889
|
setAccessibilityFilter(types) {
|
|
3656
3890
|
this.renderer?.setAccessibilityFilter?.(types);
|
|
3657
3891
|
}
|
|
@@ -3839,7 +4073,8 @@ var PickerController = class {
|
|
|
3839
4073
|
categoryLabel: cat?.label ?? s.categoryKey,
|
|
3840
4074
|
categoryColor: cat?.color ?? "#6e7bff",
|
|
3841
4075
|
status: this.renderer?.getStatus(s.id) ?? "free",
|
|
3842
|
-
currency: this.currency
|
|
4076
|
+
currency: this.currency,
|
|
4077
|
+
...this.seatContext.get(s.id)
|
|
3843
4078
|
};
|
|
3844
4079
|
}
|
|
3845
4080
|
priceFor(categoryKey) {
|
|
@@ -3930,16 +4165,21 @@ var PickerController = class {
|
|
|
3930
4165
|
this.emitSelectionChange();
|
|
3931
4166
|
}
|
|
3932
4167
|
/** Set the open hold + (re)arm the server-authoritative expiry timer. */
|
|
3933
|
-
setHold(hold) {
|
|
4168
|
+
setHold(hold, source = "created") {
|
|
3934
4169
|
const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
|
|
3935
4170
|
this.hold_ = full;
|
|
4171
|
+
this.renderer?.setOwnedHold?.(
|
|
4172
|
+
full.labels.map((label) => this.labelToId.get(label)).filter((id) => !!id)
|
|
4173
|
+
);
|
|
3936
4174
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
3937
4175
|
const ms = Math.max(0, full.expiresAt - Date.now());
|
|
3938
4176
|
this.expiryTimer = setTimeout(() => void this.expireActiveHold(), ms);
|
|
3939
|
-
this.opts.
|
|
4177
|
+
if (source === "restored") this.opts.onHoldRestored?.(full);
|
|
4178
|
+
else this.opts.onHold?.(full);
|
|
3940
4179
|
}
|
|
3941
4180
|
clearHold() {
|
|
3942
4181
|
this.hold_ = null;
|
|
4182
|
+
this.renderer?.setOwnedHold?.(null);
|
|
3943
4183
|
if (this.expiryTimer) {
|
|
3944
4184
|
clearTimeout(this.expiryTimer);
|
|
3945
4185
|
this.expiryTimer = null;
|
|
@@ -3971,6 +4211,9 @@ var PickerController = class {
|
|
|
3971
4211
|
if (!r) return;
|
|
3972
4212
|
this.liveStatuses = new Map(Object.entries(seats));
|
|
3973
4213
|
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
4214
|
+
r.setOwnedHold?.(
|
|
4215
|
+
(this.hold_?.labels ?? []).map((label) => this.labelToId.get(label)).filter((id) => !!id)
|
|
4216
|
+
);
|
|
3974
4217
|
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
3975
4218
|
for (const [label, st] of Object.entries(seats)) {
|
|
3976
4219
|
const id = this.labelToId.get(label);
|