@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.d.cts CHANGED
@@ -1035,6 +1035,32 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1035
1035
  */
1036
1036
  private resolveSelectionColor;
1037
1037
  private setSelected;
1038
+ /**
1039
+ * The stage scale `focusRegion(id)` would settle at — i.e. the zoom that
1040
+ * frames this section in the current viewport. Used to decide whether
1041
+ * redirecting a seat tap to section-focus would actually zoom in FURTHER, or
1042
+ * whether the section already fills the viewport (small container) so the tap
1043
+ * must fall through and pick.
1044
+ */
1045
+ private sectionFrameScale;
1046
+ /**
1047
+ * Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
1048
+ * then toggle. The gate (§4) redirects small on-screen seats to section-focus
1049
+ * to avoid blind mis-picks at overview scales — but ONLY when focusing would
1050
+ * zoom the seat up to a safe size. If we're already focused on the section, or
1051
+ * the section already fills the viewport (a narrow — OR even a wide — container
1052
+ * frames a big section BELOW LABEL_SCALE), redirecting is a no-op that would
1053
+ * leave the seat permanently unpickable (§5 `picking-gate-unreachable`), so we
1054
+ * fall through and PICK. This is the invariant: seats are never unpickable.
1055
+ */
1056
+ private handleSeatTap;
1057
+ /**
1058
+ * Nearest SELECTABLE seat whose centre is within `slopPx` (screen space) of a
1059
+ * seat circle edge, or null. Enlarges the effective tap target for small seats
1060
+ * so a near-miss on mobile still lands on the intended seat, without ever
1061
+ * hijacking a clean tap on empty space beyond the slop.
1062
+ */
1063
+ private nearestSeatToScreen;
1038
1064
  private wireInteraction;
1039
1065
  /**
1040
1066
  * Which deck (floor) a screen tap landed on in the 3D stacked overview. The
package/dist/index.d.ts CHANGED
@@ -1035,6 +1035,32 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1035
1035
  */
1036
1036
  private resolveSelectionColor;
1037
1037
  private setSelected;
1038
+ /**
1039
+ * The stage scale `focusRegion(id)` would settle at — i.e. the zoom that
1040
+ * frames this section in the current viewport. Used to decide whether
1041
+ * redirecting a seat tap to section-focus would actually zoom in FURTHER, or
1042
+ * whether the section already fills the viewport (small container) so the tap
1043
+ * must fall through and pick.
1044
+ */
1045
+ private sectionFrameScale;
1046
+ /**
1047
+ * Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
1048
+ * then toggle. The gate (§4) redirects small on-screen seats to section-focus
1049
+ * to avoid blind mis-picks at overview scales — but ONLY when focusing would
1050
+ * zoom the seat up to a safe size. If we're already focused on the section, or
1051
+ * the section already fills the viewport (a narrow — OR even a wide — container
1052
+ * frames a big section BELOW LABEL_SCALE), redirecting is a no-op that would
1053
+ * leave the seat permanently unpickable (§5 `picking-gate-unreachable`), so we
1054
+ * fall through and PICK. This is the invariant: seats are never unpickable.
1055
+ */
1056
+ private handleSeatTap;
1057
+ /**
1058
+ * Nearest SELECTABLE seat whose centre is within `slopPx` (screen space) of a
1059
+ * seat circle edge, or null. Enlarges the effective tap target for small seats
1060
+ * so a near-miss on mobile still lands on the intended seat, without ever
1061
+ * hijacking a clean tap on empty space beyond the slop.
1062
+ */
1063
+ private nearestSeatToScreen;
1038
1064
  private wireInteraction;
1039
1065
  /**
1040
1066
  * Which deck (floor) a screen tap landed on in the 3D stacked overview. The
package/dist/index.js CHANGED
@@ -632,6 +632,7 @@ var SEAT_RADIUS = 9;
632
632
  var SEAT_LEGIBLE_SCALE = 0.9;
633
633
  var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
634
634
  var LABEL_SCALE = 1;
635
+ var SEAT_TAP_SLOP_PX = 14;
635
636
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
636
637
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
637
638
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
@@ -2533,27 +2534,84 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2533
2534
  }
2534
2535
  }
2535
2536
  // ---- interaction ----------------------------------------------------------
2536
- wireInteraction() {
2537
- this.seatLayer.on("click tap", (e) => {
2538
- if (this.moved > 8) return;
2539
- const id = seatIdOf(e.target);
2540
- if (!id) return;
2541
- if (this.stacked && this.opts.onDeckTap) {
2542
- const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2543
- if (floorId) {
2544
- this.opts.onDeckTap(floorId);
2545
- return;
2546
- }
2537
+ /**
2538
+ * The stage scale `focusRegion(id)` would settle at — i.e. the zoom that
2539
+ * frames this section in the current viewport. Used to decide whether
2540
+ * redirecting a seat tap to section-focus would actually zoom in FURTHER, or
2541
+ * whether the section already fills the viewport (small container) so the tap
2542
+ * must fall through and pick.
2543
+ */
2544
+ sectionFrameScale(id) {
2545
+ const sec = this.sections.find((s) => s.id === id);
2546
+ if (!sec) return this.stage.scaleX();
2547
+ const b = polyBounds(sec.outline);
2548
+ const w = this.stage.width();
2549
+ const h = this.stage.height();
2550
+ const { min, max } = this.zoomBounds();
2551
+ const margin = 1.12;
2552
+ if (b.width <= 0 || b.height <= 0) return this.stage.scaleX();
2553
+ return clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
2554
+ }
2555
+ /**
2556
+ * Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
2557
+ * then toggle. The gate (§4) redirects small on-screen seats to section-focus
2558
+ * to avoid blind mis-picks at overview scales — but ONLY when focusing would
2559
+ * zoom the seat up to a safe size. If we're already focused on the section, or
2560
+ * the section already fills the viewport (a narrow — OR even a wide — container
2561
+ * frames a big section BELOW LABEL_SCALE), redirecting is a no-op that would
2562
+ * leave the seat permanently unpickable (§5 `picking-gate-unreachable`), so we
2563
+ * fall through and PICK. This is the invariant: seats are never unpickable.
2564
+ */
2565
+ handleSeatTap(id) {
2566
+ if (this.stacked && this.opts.onDeckTap) {
2567
+ const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2568
+ if (floorId) {
2569
+ this.opts.onDeckTap(floorId);
2570
+ return;
2547
2571
  }
2548
- if (this.effScale() < LABEL_SCALE && this.sections.length) {
2549
- const sec = this.seatSection.get(id);
2550
- if (sec) {
2572
+ }
2573
+ if (this.effScale() < LABEL_SCALE && this.sections.length) {
2574
+ const sec = this.seatSection.get(id);
2575
+ if (sec) {
2576
+ const alreadyFocused = this.focusedSectionId === sec.id;
2577
+ const canZoomInFurther = this.sectionFrameScale(sec.id) > this.stage.scaleX() * 1.02;
2578
+ if (!alreadyFocused && canZoomInFurther) {
2551
2579
  if (this.opts.onSectionTap) this.opts.onSectionTap(sec.id);
2552
2580
  else this.focusSection(sec.id);
2553
2581
  return;
2554
2582
  }
2555
2583
  }
2556
- this.toggleSeat(id);
2584
+ }
2585
+ this.toggleSeat(id);
2586
+ }
2587
+ /**
2588
+ * Nearest SELECTABLE seat whose centre is within `slopPx` (screen space) of a
2589
+ * seat circle edge, or null. Enlarges the effective tap target for small seats
2590
+ * so a near-miss on mobile still lands on the intended seat, without ever
2591
+ * hijacking a clean tap on empty space beyond the slop.
2592
+ */
2593
+ nearestSeatToScreen(screen, slopPx) {
2594
+ const s = this.stage.scaleX() || 1;
2595
+ const reachWorld = this.seatR + slopPx / s;
2596
+ const world = this.screenToWorld(screen);
2597
+ let best = null;
2598
+ let bestD = reachWorld;
2599
+ for (const seat of this.seats) {
2600
+ if (!this.selection.has(seat.id) && !this.isSelectable(seat.id)) continue;
2601
+ const d = Math.hypot(seat.x - world.x, seat.y - world.y);
2602
+ if (d < bestD) {
2603
+ bestD = d;
2604
+ best = seat.id;
2605
+ }
2606
+ }
2607
+ return best;
2608
+ }
2609
+ wireInteraction() {
2610
+ this.seatLayer.on("click tap", (e) => {
2611
+ if (this.moved > 8) return;
2612
+ const id = seatIdOf(e.target);
2613
+ if (!id) return;
2614
+ this.handleSeatTap(id);
2557
2615
  });
2558
2616
  this.seatLayer.on("mouseover", (e) => {
2559
2617
  const id = seatIdOf(e.target);
@@ -2579,10 +2637,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2579
2637
  const factor = Math.exp(-e.evt.deltaY * 2e-3);
2580
2638
  this.zoomAbout(this.stage.scaleX() * clamp(factor, 0.5, 2), pointer);
2581
2639
  });
2582
- this.stage.on("click tap", () => {
2583
- if (!this.cached || this.moved > 8) return;
2640
+ this.stage.on("click tap", (e) => {
2641
+ if (this.moved > 8) return;
2584
2642
  const pointer = this.stage.getPointerPosition();
2585
2643
  if (!pointer) return;
2644
+ if (!this.cached) {
2645
+ if (seatIdOf(e.target)) return;
2646
+ const near = this.nearestSeatToScreen(pointer, SEAT_TAP_SLOP_PX);
2647
+ if (near) this.handleSeatTap(near);
2648
+ return;
2649
+ }
2586
2650
  if (this.stacked && this.opts.onDeckTap) {
2587
2651
  const floorId = this.deckFloorAt();
2588
2652
  if (floorId) {