@seatlayer/core 0.10.0 → 0.10.1

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
@@ -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
- wireInteraction() {
2856
- this.seatLayer.on("click tap", (e) => {
2857
- if (this.moved > 8) return;
2858
- const id = seatIdOf(e.target);
2859
- if (!id) return;
2860
- if (this.stacked && this.opts.onDeckTap) {
2861
- const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2862
- if (floorId) {
2863
- this.opts.onDeckTap(floorId);
2864
- return;
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
- if (this.effScale() < LABEL_SCALE && this.sections.length) {
2868
- const sec = this.seatSection.get(id);
2869
- if (sec) {
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
- this.toggleSeat(id);
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 (!this.cached || this.moved > 8) return;
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) {