@seatlayer/core 0.10.0 → 0.10.2
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 +148 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +148 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -951,6 +951,7 @@ var SEAT_RADIUS = 9;
|
|
|
951
951
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
952
952
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
953
953
|
var LABEL_SCALE = 1;
|
|
954
|
+
var SEAT_TAP_SLOP_PX = 14;
|
|
954
955
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
955
956
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
956
957
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
@@ -2852,27 +2853,84 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2852
2853
|
}
|
|
2853
2854
|
}
|
|
2854
2855
|
// ---- interaction ----------------------------------------------------------
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2856
|
+
/**
|
|
2857
|
+
* The stage scale `focusRegion(id)` would settle at — i.e. the zoom that
|
|
2858
|
+
* frames this section in the current viewport. Used to decide whether
|
|
2859
|
+
* redirecting a seat tap to section-focus would actually zoom in FURTHER, or
|
|
2860
|
+
* whether the section already fills the viewport (small container) so the tap
|
|
2861
|
+
* must fall through and pick.
|
|
2862
|
+
*/
|
|
2863
|
+
sectionFrameScale(id) {
|
|
2864
|
+
const sec = this.sections.find((s) => s.id === id);
|
|
2865
|
+
if (!sec) return this.stage.scaleX();
|
|
2866
|
+
const b = polyBounds(sec.outline);
|
|
2867
|
+
const w = this.stage.width();
|
|
2868
|
+
const h = this.stage.height();
|
|
2869
|
+
const { min, max } = this.zoomBounds();
|
|
2870
|
+
const margin = 1.12;
|
|
2871
|
+
if (b.width <= 0 || b.height <= 0) return this.stage.scaleX();
|
|
2872
|
+
return clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
|
|
2876
|
+
* then toggle. The gate (§4) redirects small on-screen seats to section-focus
|
|
2877
|
+
* to avoid blind mis-picks at overview scales — but ONLY when focusing would
|
|
2878
|
+
* zoom the seat up to a safe size. If we're already focused on the section, or
|
|
2879
|
+
* the section already fills the viewport (a narrow — OR even a wide — container
|
|
2880
|
+
* frames a big section BELOW LABEL_SCALE), redirecting is a no-op that would
|
|
2881
|
+
* leave the seat permanently unpickable (§5 `picking-gate-unreachable`), so we
|
|
2882
|
+
* fall through and PICK. This is the invariant: seats are never unpickable.
|
|
2883
|
+
*/
|
|
2884
|
+
handleSeatTap(id) {
|
|
2885
|
+
if (this.stacked && this.opts.onDeckTap) {
|
|
2886
|
+
const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
|
|
2887
|
+
if (floorId) {
|
|
2888
|
+
this.opts.onDeckTap(floorId);
|
|
2889
|
+
return;
|
|
2866
2890
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2891
|
+
}
|
|
2892
|
+
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
2893
|
+
const sec = this.seatSection.get(id);
|
|
2894
|
+
if (sec) {
|
|
2895
|
+
const alreadyFocused = this.focusedSectionId === sec.id;
|
|
2896
|
+
const canZoomInFurther = this.sectionFrameScale(sec.id) > this.stage.scaleX() * 1.02;
|
|
2897
|
+
if (!alreadyFocused && canZoomInFurther) {
|
|
2870
2898
|
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.id);
|
|
2871
2899
|
else this.focusSection(sec.id);
|
|
2872
2900
|
return;
|
|
2873
2901
|
}
|
|
2874
2902
|
}
|
|
2875
|
-
|
|
2903
|
+
}
|
|
2904
|
+
this.toggleSeat(id);
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Nearest SELECTABLE seat whose centre is within `slopPx` (screen space) of a
|
|
2908
|
+
* seat circle edge, or null. Enlarges the effective tap target for small seats
|
|
2909
|
+
* so a near-miss on mobile still lands on the intended seat, without ever
|
|
2910
|
+
* hijacking a clean tap on empty space beyond the slop.
|
|
2911
|
+
*/
|
|
2912
|
+
nearestSeatToScreen(screen, slopPx) {
|
|
2913
|
+
const s = this.stage.scaleX() || 1;
|
|
2914
|
+
const reachWorld = this.seatR + slopPx / s;
|
|
2915
|
+
const world = this.screenToWorld(screen);
|
|
2916
|
+
let best = null;
|
|
2917
|
+
let bestD = reachWorld;
|
|
2918
|
+
for (const seat of this.seats) {
|
|
2919
|
+
if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
|
|
2920
|
+
const d = Math.hypot(seat.x - world.x, seat.y - world.y);
|
|
2921
|
+
if (d < bestD) {
|
|
2922
|
+
bestD = d;
|
|
2923
|
+
best = seat.id;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
return best;
|
|
2927
|
+
}
|
|
2928
|
+
wireInteraction() {
|
|
2929
|
+
this.seatLayer.on("click tap", (e) => {
|
|
2930
|
+
if (this.moved > 8) return;
|
|
2931
|
+
const id = seatIdOf(e.target);
|
|
2932
|
+
if (!id) return;
|
|
2933
|
+
this.handleSeatTap(id);
|
|
2876
2934
|
});
|
|
2877
2935
|
this.seatLayer.on("mouseover", (e) => {
|
|
2878
2936
|
const id = seatIdOf(e.target);
|
|
@@ -2898,10 +2956,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2898
2956
|
const factor = Math.exp(-e.evt.deltaY * 2e-3);
|
|
2899
2957
|
this.zoomAbout(this.stage.scaleX() * clamp(factor, 0.5, 2), pointer);
|
|
2900
2958
|
});
|
|
2901
|
-
this.stage.on("click tap", () => {
|
|
2902
|
-
if (
|
|
2959
|
+
this.stage.on("click tap", (e) => {
|
|
2960
|
+
if (this.moved > 8) return;
|
|
2903
2961
|
const pointer = this.stage.getPointerPosition();
|
|
2904
2962
|
if (!pointer) return;
|
|
2963
|
+
if (!this.cached) {
|
|
2964
|
+
if (seatIdOf(e.target)) return;
|
|
2965
|
+
const near = this.nearestSeatToScreen(pointer, SEAT_TAP_SLOP_PX);
|
|
2966
|
+
if (near) this.handleSeatTap(near);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2905
2969
|
if (this.stacked && this.opts.onDeckTap) {
|
|
2906
2970
|
const floorId = this.deckFloorAt();
|
|
2907
2971
|
if (floorId) {
|
|
@@ -3104,14 +3168,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3104
3168
|
this.seatLayer.batchDraw();
|
|
3105
3169
|
}
|
|
3106
3170
|
}
|
|
3107
|
-
|
|
3171
|
+
/** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
|
|
3172
|
+
* debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
|
|
3173
|
+
rebuildSeatCache() {
|
|
3108
3174
|
const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
|
|
3109
3175
|
this.seatLayer.clearCache();
|
|
3110
3176
|
this.seatLayer.cache({ pixelRatio: pr });
|
|
3111
3177
|
this.seatLayer.listening(false);
|
|
3112
3178
|
this.cached = true;
|
|
3179
|
+
}
|
|
3180
|
+
cacheSeatLayer() {
|
|
3181
|
+
this.rebuildSeatCache();
|
|
3113
3182
|
this.seatLayer.batchDraw();
|
|
3114
3183
|
}
|
|
3184
|
+
/**
|
|
3185
|
+
* SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
|
|
3186
|
+
* ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
|
|
3187
|
+
* Konva's batchDraw) on hidden/backgrounded/occluded tabs, so seat-status
|
|
3188
|
+
* deltas applied via setStatus() mutate the scene graph but never reach the
|
|
3189
|
+
* canvas until the tab is foregrounded. This flushes the cache-debounce and
|
|
3190
|
+
* paints every layer setStatus() can touch, immediately.
|
|
3191
|
+
*
|
|
3192
|
+
* Additive: the foreground path never calls this, so foreground behaviour is
|
|
3193
|
+
* byte-identical.
|
|
3194
|
+
*/
|
|
3195
|
+
forceDraw() {
|
|
3196
|
+
if (this.recacheTimer) {
|
|
3197
|
+
clearTimeout(this.recacheTimer);
|
|
3198
|
+
this.recacheTimer = null;
|
|
3199
|
+
this.rebuildSeatCache();
|
|
3200
|
+
}
|
|
3201
|
+
this.bgLayer.draw();
|
|
3202
|
+
this.seatLayer.draw();
|
|
3203
|
+
this.overlayLayer.draw();
|
|
3204
|
+
}
|
|
3115
3205
|
updateLabels() {
|
|
3116
3206
|
const show = this.effScale() > LABEL_SCALE;
|
|
3117
3207
|
this.labelGroup.destroyChildren();
|
|
@@ -3247,6 +3337,10 @@ var PickerController = class {
|
|
|
3247
3337
|
this.reconnectTimer = null;
|
|
3248
3338
|
this.attempt = 0;
|
|
3249
3339
|
this.closed = false;
|
|
3340
|
+
/** Bound visibilitychange listener (buyer catch-up on tab regain). Kept on the
|
|
3341
|
+
* instance so destroy() can detach it; null when not attached (guards double
|
|
3342
|
+
* mounts / non-DOM environments). */
|
|
3343
|
+
this.onVisibilityChange = null;
|
|
3250
3344
|
// hold state
|
|
3251
3345
|
this.hold_ = null;
|
|
3252
3346
|
this.liveStatuses = /* @__PURE__ */ new Map();
|
|
@@ -3372,6 +3466,7 @@ var PickerController = class {
|
|
|
3372
3466
|
this.closedSections = new Set(closedIds);
|
|
3373
3467
|
if (closedIds.length) renderer.setClosedSections?.(closedIds);
|
|
3374
3468
|
if (seats) this.applySeatsMap(seats);
|
|
3469
|
+
this.attachVisibilityListener();
|
|
3375
3470
|
this.connect();
|
|
3376
3471
|
return {
|
|
3377
3472
|
doc: res.doc,
|
|
@@ -3798,6 +3893,7 @@ var PickerController = class {
|
|
|
3798
3893
|
}
|
|
3799
3894
|
destroy() {
|
|
3800
3895
|
this.closed = true;
|
|
3896
|
+
this.detachVisibilityListener();
|
|
3801
3897
|
if (this.reconnectTimer) {
|
|
3802
3898
|
clearTimeout(this.reconnectTimer);
|
|
3803
3899
|
this.reconnectTimer = null;
|
|
@@ -3994,6 +4090,37 @@ var PickerController = class {
|
|
|
3994
4090
|
}
|
|
3995
4091
|
}
|
|
3996
4092
|
// ---- realtime socket ------------------------------------------------------
|
|
4093
|
+
/**
|
|
4094
|
+
* Buyer catch-up on tab regain. While a tab is hidden Chrome pauses rAF, so
|
|
4095
|
+
* seat-status deltas that arrived over the WS mutated the scene graph but the
|
|
4096
|
+
* canvas colors never repainted. When the tab becomes visible again we (1)
|
|
4097
|
+
* resnapshot to pull authoritative state for anything the socket may have
|
|
4098
|
+
* missed while backgrounded, then (2) forceDraw() a synchronous repaint so the
|
|
4099
|
+
* seat colors are correct immediately rather than on the next incidental draw.
|
|
4100
|
+
*
|
|
4101
|
+
* Attached once per mount; guarded against double-attach and non-DOM
|
|
4102
|
+
* environments; detached in destroy() (no leaks).
|
|
4103
|
+
*/
|
|
4104
|
+
attachVisibilityListener() {
|
|
4105
|
+
if (this.onVisibilityChange) return;
|
|
4106
|
+
if (typeof document === "undefined" || typeof document.addEventListener !== "function") return;
|
|
4107
|
+
const handler = () => {
|
|
4108
|
+
if (this.closed) return;
|
|
4109
|
+
if (document.visibilityState !== "visible") return;
|
|
4110
|
+
void this.resnapshot().then(() => {
|
|
4111
|
+
if (!this.closed) this.renderer?.forceDraw();
|
|
4112
|
+
});
|
|
4113
|
+
};
|
|
4114
|
+
this.onVisibilityChange = handler;
|
|
4115
|
+
document.addEventListener("visibilitychange", handler);
|
|
4116
|
+
}
|
|
4117
|
+
detachVisibilityListener() {
|
|
4118
|
+
if (!this.onVisibilityChange) return;
|
|
4119
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
|
|
4120
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
4121
|
+
}
|
|
4122
|
+
this.onVisibilityChange = null;
|
|
4123
|
+
}
|
|
3997
4124
|
connect() {
|
|
3998
4125
|
if (this.closed) return;
|
|
3999
4126
|
let ws;
|
|
@@ -4039,6 +4166,9 @@ var PickerController = class {
|
|
|
4039
4166
|
}
|
|
4040
4167
|
r.setStatus([id], next);
|
|
4041
4168
|
}
|
|
4169
|
+
if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
4170
|
+
r.forceDraw();
|
|
4171
|
+
}
|
|
4042
4172
|
this.clearBookedHoldIfSettled();
|
|
4043
4173
|
this.opts.onStatusChange?.();
|
|
4044
4174
|
}
|