@seatlayer/core 0.9.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 +409 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -5
- package/dist/index.d.ts +198 -5
- package/dist/index.js +409 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -339,9 +339,10 @@ function layerOf(obj) {
|
|
|
339
339
|
case "booth":
|
|
340
340
|
case "section":
|
|
341
341
|
return "interactive";
|
|
342
|
-
// stage / décor live on 'shape'; free text
|
|
342
|
+
// stage / décor live on 'shape'; free text + decor images are background furniture.
|
|
343
343
|
case "shape":
|
|
344
344
|
case "text":
|
|
345
|
+
case "decorImage":
|
|
345
346
|
return "background";
|
|
346
347
|
default:
|
|
347
348
|
return "interactive";
|
|
@@ -570,6 +571,8 @@ function objectCenter(o) {
|
|
|
570
571
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
571
572
|
}
|
|
572
573
|
return { x: o.x ?? 0, y: o.y ?? 0 };
|
|
574
|
+
case "decorImage":
|
|
575
|
+
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
573
576
|
}
|
|
574
577
|
}
|
|
575
578
|
function floorsOf(doc) {
|
|
@@ -605,6 +608,8 @@ function translateObject(o, dx, dy) {
|
|
|
605
608
|
...o.x != null ? { x: o.x + dx } : {},
|
|
606
609
|
...o.y != null ? { y: o.y + dy } : {}
|
|
607
610
|
};
|
|
611
|
+
case "decorImage":
|
|
612
|
+
return { ...o, x: o.x + dx, y: o.y + dy };
|
|
608
613
|
}
|
|
609
614
|
}
|
|
610
615
|
function stackFloors(doc, spread = 900) {
|
|
@@ -643,6 +648,9 @@ function chartBounds(doc) {
|
|
|
643
648
|
for (const p of obj.points) acc(p.x, p.y);
|
|
644
649
|
} else if (obj.type === "section") {
|
|
645
650
|
for (const p of obj.outline) acc(p.x, p.y);
|
|
651
|
+
} else if (obj.type === "decorImage") {
|
|
652
|
+
acc(obj.x, obj.y);
|
|
653
|
+
acc(obj.x + obj.width, obj.y + obj.height);
|
|
646
654
|
} else if (obj.type === "shape") {
|
|
647
655
|
if (obj.points && obj.points.length) {
|
|
648
656
|
for (const p of obj.points) acc(p.x, p.y);
|
|
@@ -943,6 +951,7 @@ var SEAT_RADIUS = 9;
|
|
|
943
951
|
var SEAT_LEGIBLE_SCALE = 0.9;
|
|
944
952
|
var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
945
953
|
var LABEL_SCALE = 1;
|
|
954
|
+
var SEAT_TAP_SLOP_PX = 14;
|
|
946
955
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
947
956
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
948
957
|
var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
|
|
@@ -960,6 +969,13 @@ var ZONE_SUB_PX = 12;
|
|
|
960
969
|
var HELD_FILL = "#6b7280";
|
|
961
970
|
var TAKEN_FILL = "#374151";
|
|
962
971
|
var NFS_STROKE = "#4b5563";
|
|
972
|
+
var CLOSED_SECTION_FILL = "#586070";
|
|
973
|
+
var CLOSED_SEAT_FILL = "#4b5563";
|
|
974
|
+
var CLOSED_SEAT_OPACITY = 0.4;
|
|
975
|
+
var FOCUS_DIM_OPACITY = 0.16;
|
|
976
|
+
var FOCUS_DESATURATE = 0.72;
|
|
977
|
+
var FOCUS_NEUTRAL = "#6b7280";
|
|
978
|
+
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
963
979
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
964
980
|
var ACCESS_RING = {
|
|
965
981
|
wheelchair: "#3b82f6",
|
|
@@ -1106,6 +1122,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1106
1122
|
this.accessFilter = null;
|
|
1107
1123
|
/** Category highlight (legend hover): dims free seats NOT of this category. */
|
|
1108
1124
|
this.categoryHighlight = null;
|
|
1125
|
+
/** Price-band filter (F4): dim free seats whose category is NOT in this set. */
|
|
1126
|
+
this.categoryFilter = null;
|
|
1109
1127
|
// Section/zone overlays (bgLayer) — the 3-rung LOD: seats → section blocks →
|
|
1110
1128
|
// zone blocks. Kept for the melt restyle and for hit-testing a zoomed-out tap.
|
|
1111
1129
|
this.sections = [];
|
|
@@ -1114,6 +1132,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1114
1132
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1115
1133
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1116
1134
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
1135
|
+
/** Phase 2: section/zone ids in the event-level `closed` state — flat grey
|
|
1136
|
+
* block, seats greyed + not pickable, but the section stays rendered. */
|
|
1137
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
1138
|
+
/** AXS section-focus: the currently-focused section id (others dim), or null. */
|
|
1139
|
+
this.focusedSectionId = null;
|
|
1140
|
+
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1141
|
+
this.focusBackdrop = null;
|
|
1117
1142
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1118
1143
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1119
1144
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1594,6 +1619,27 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1594
1619
|
if (cur === null || next === null) return cur === next;
|
|
1595
1620
|
return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
|
|
1596
1621
|
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Price-band filter (F4): dim free, unselected seats whose category key is NOT
|
|
1624
|
+
* in `keys`. `null` clears the filter (all categories fully visible). The
|
|
1625
|
+
* widget resolves which categories fall inside the buyer's chosen band.
|
|
1626
|
+
*/
|
|
1627
|
+
setCategoryFilter(keys) {
|
|
1628
|
+
const next = keys === null ? null : new Set(keys);
|
|
1629
|
+
const same = next === null && this.categoryFilter === null || next !== null && this.categoryFilter !== null && next.size === this.categoryFilter.size && [...next].every((k) => this.categoryFilter.has(k));
|
|
1630
|
+
if (same) return;
|
|
1631
|
+
this.categoryFilter = next;
|
|
1632
|
+
for (const seat of this.seats) {
|
|
1633
|
+
const c = this.circleById.get(seat.id);
|
|
1634
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1635
|
+
}
|
|
1636
|
+
if (this.cached) {
|
|
1637
|
+
this.seatLayer.clearCache();
|
|
1638
|
+
this.cacheSeatLayer();
|
|
1639
|
+
} else {
|
|
1640
|
+
this.seatLayer.batchDraw();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1597
1643
|
// ---- isometric ("3D") view mode -------------------------------------------
|
|
1598
1644
|
/**
|
|
1599
1645
|
* Switch the projection between flat top-down and the isometric "3D" view
|
|
@@ -1920,12 +1966,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1920
1966
|
if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
|
|
1921
1967
|
c.opacity(0.25);
|
|
1922
1968
|
}
|
|
1969
|
+
if (this.categoryFilter && status === "free" && !selected && !this.categoryFilter.has(seat.categoryKey)) {
|
|
1970
|
+
c.opacity(0.22);
|
|
1971
|
+
}
|
|
1923
1972
|
if (this.dimmedSections.size) {
|
|
1924
1973
|
const sec = this.seatSection.get(id);
|
|
1925
1974
|
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1926
1975
|
c.opacity(0.18);
|
|
1927
1976
|
}
|
|
1928
1977
|
}
|
|
1978
|
+
if (this.closedSections.size && this.seatInClosedSection(id)) {
|
|
1979
|
+
c.fill(CLOSED_SEAT_FILL);
|
|
1980
|
+
c.stroke("");
|
|
1981
|
+
c.strokeWidth(0);
|
|
1982
|
+
c.dash([]);
|
|
1983
|
+
c.opacity(CLOSED_SEAT_OPACITY);
|
|
1984
|
+
}
|
|
1985
|
+
if (this.focusedSectionId) {
|
|
1986
|
+
const sec = this.seatSection.get(id);
|
|
1987
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
1988
|
+
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
1992
|
+
seatInClosedSection(id) {
|
|
1993
|
+
if (!this.closedSections.size) return false;
|
|
1994
|
+
const sec = this.seatSection.get(id);
|
|
1995
|
+
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
1929
1996
|
}
|
|
1930
1997
|
/**
|
|
1931
1998
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -1965,6 +2032,109 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1965
2032
|
this.seatLayer.batchDraw();
|
|
1966
2033
|
}
|
|
1967
2034
|
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Phase 2 event-level section states: mark these section/zone ids `closed` —
|
|
2037
|
+
* flat grey block, seats greyed + not pickable, but the section stays rendered
|
|
2038
|
+
* (unlike the buyer's applyHidden which strips it). `null`/empty clears.
|
|
2039
|
+
*/
|
|
2040
|
+
setClosedSections(ids) {
|
|
2041
|
+
const next = new Set(ids ?? []);
|
|
2042
|
+
if (next.size === this.closedSections.size && [...next].every((i) => this.closedSections.has(i))) return;
|
|
2043
|
+
this.closedSections = next;
|
|
2044
|
+
this.repaintSectionsAndSeats();
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* AXS section-focus: dim + desaturate every other section, draw a calm backdrop
|
|
2048
|
+
* panel behind this section's seats, and glide the camera in to frame it (the
|
|
2049
|
+
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2050
|
+
*/
|
|
2051
|
+
focusSection(id) {
|
|
2052
|
+
if (!this.sections.some((s) => s.id === id)) return;
|
|
2053
|
+
this.focusedSectionId = id;
|
|
2054
|
+
this.drawFocusBackdrop(id);
|
|
2055
|
+
this.repaintSectionsAndSeats();
|
|
2056
|
+
this.updateLOD();
|
|
2057
|
+
this.focusRegion(id);
|
|
2058
|
+
}
|
|
2059
|
+
/** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
|
|
2060
|
+
clearSectionFocus() {
|
|
2061
|
+
if (!this.focusedSectionId) return;
|
|
2062
|
+
this.focusedSectionId = null;
|
|
2063
|
+
if (this.focusBackdrop) {
|
|
2064
|
+
this.focusBackdrop.destroy();
|
|
2065
|
+
this.focusBackdrop = null;
|
|
2066
|
+
}
|
|
2067
|
+
this.repaintSectionsAndSeats();
|
|
2068
|
+
this.updateLOD();
|
|
2069
|
+
}
|
|
2070
|
+
/** The currently AXS-focused section id, or null. */
|
|
2071
|
+
getFocusedSection() {
|
|
2072
|
+
return this.focusedSectionId;
|
|
2073
|
+
}
|
|
2074
|
+
/** Draw (or replace) the light backdrop panel behind the focused section. */
|
|
2075
|
+
drawFocusBackdrop(id) {
|
|
2076
|
+
if (this.focusBackdrop) {
|
|
2077
|
+
this.focusBackdrop.destroy();
|
|
2078
|
+
this.focusBackdrop = null;
|
|
2079
|
+
}
|
|
2080
|
+
const sec = this.sections.find((s) => s.id === id);
|
|
2081
|
+
if (!sec) return;
|
|
2082
|
+
const panel = new import_Line.Line({
|
|
2083
|
+
points: sec.outline.flatMap((p) => [p.x, p.y]),
|
|
2084
|
+
closed: true,
|
|
2085
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2086
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2087
|
+
strokeWidth: 1,
|
|
2088
|
+
listening: false,
|
|
2089
|
+
perfectDrawEnabled: false
|
|
2090
|
+
});
|
|
2091
|
+
this.bgLayer.add(panel);
|
|
2092
|
+
panel.moveToTop();
|
|
2093
|
+
this.focusBackdrop = panel;
|
|
2094
|
+
}
|
|
2095
|
+
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2096
|
+
repaintSectionsAndSeats() {
|
|
2097
|
+
for (const seat of this.seats) {
|
|
2098
|
+
const c = this.circleById.get(seat.id);
|
|
2099
|
+
if (c) this.paintSeat(c, seat.id);
|
|
2100
|
+
}
|
|
2101
|
+
for (const sec of this.sections) sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2102
|
+
if (this.cached) {
|
|
2103
|
+
this.seatLayer.clearCache();
|
|
2104
|
+
this.cacheSeatLayer();
|
|
2105
|
+
} else {
|
|
2106
|
+
this.seatLayer.batchDraw();
|
|
2107
|
+
}
|
|
2108
|
+
this.bgLayer.batchDraw();
|
|
2109
|
+
}
|
|
2110
|
+
/** The world-space rectangle currently visible in the viewport (minimap F3). */
|
|
2111
|
+
getVisibleWorldRect() {
|
|
2112
|
+
const tl = this.screenToWorld({ x: 0, y: 0 });
|
|
2113
|
+
const br = this.screenToWorld({ x: this.stage.width(), y: this.stage.height() });
|
|
2114
|
+
return {
|
|
2115
|
+
x: Math.min(tl.x, br.x),
|
|
2116
|
+
y: Math.min(tl.y, br.y),
|
|
2117
|
+
width: Math.abs(br.x - tl.x),
|
|
2118
|
+
height: Math.abs(br.y - tl.y)
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
/** Axis-aligned world bounds of all seats + section outlines (minimap F3 frame). */
|
|
2122
|
+
getWorldBounds() {
|
|
2123
|
+
let minX = Infinity;
|
|
2124
|
+
let minY = Infinity;
|
|
2125
|
+
let maxX = -Infinity;
|
|
2126
|
+
let maxY = -Infinity;
|
|
2127
|
+
const grow = (x, y) => {
|
|
2128
|
+
if (x < minX) minX = x;
|
|
2129
|
+
if (y < minY) minY = y;
|
|
2130
|
+
if (x > maxX) maxX = x;
|
|
2131
|
+
if (y > maxY) maxY = y;
|
|
2132
|
+
};
|
|
2133
|
+
for (const s of this.seats) grow(s.x, s.y);
|
|
2134
|
+
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2135
|
+
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2136
|
+
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2137
|
+
}
|
|
1968
2138
|
/** Legend hover: highlight one category (dim the rest), or null to clear. */
|
|
1969
2139
|
setCategoryHighlight(key) {
|
|
1970
2140
|
if (this.categoryHighlight === key) return;
|
|
@@ -1982,6 +2152,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1982
2152
|
}
|
|
1983
2153
|
renderBackground(doc) {
|
|
1984
2154
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
2155
|
+
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
1985
2156
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
1986
2157
|
this.renderZones(doc);
|
|
1987
2158
|
for (const obj of doc.objects) {
|
|
@@ -2030,6 +2201,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2030
2201
|
};
|
|
2031
2202
|
img.src = bg.url;
|
|
2032
2203
|
}
|
|
2204
|
+
/**
|
|
2205
|
+
* A decor graphic (rink / court / stage art). The KImage node is added to the
|
|
2206
|
+
* bgLayer synchronously so it keeps its z-slot beneath the sections drawn right
|
|
2207
|
+
* after; the bitmap is decoded async and pasted in on load. A single node = a
|
|
2208
|
+
* single drawImage per frame, and it rides the same layer cache — effectively
|
|
2209
|
+
* zero per-frame cost. Never listens, so it can't intercept a seat click.
|
|
2210
|
+
*/
|
|
2211
|
+
renderDecorImage(obj) {
|
|
2212
|
+
const img = new window.Image();
|
|
2213
|
+
const node = new import_Image.Image({
|
|
2214
|
+
image: img,
|
|
2215
|
+
x: obj.x + obj.width / 2,
|
|
2216
|
+
y: obj.y + obj.height / 2,
|
|
2217
|
+
offsetX: obj.width / 2,
|
|
2218
|
+
offsetY: obj.height / 2,
|
|
2219
|
+
width: obj.width,
|
|
2220
|
+
height: obj.height,
|
|
2221
|
+
rotation: obj.rotation ?? 0,
|
|
2222
|
+
opacity: clamp(obj.opacity ?? 1, 0, 1),
|
|
2223
|
+
listening: false,
|
|
2224
|
+
perfectDrawEnabled: false
|
|
2225
|
+
});
|
|
2226
|
+
this.bgLayer.add(node);
|
|
2227
|
+
img.onload = () => {
|
|
2228
|
+
if (!node.getLayer()) return;
|
|
2229
|
+
this.bgLayer.batchDraw();
|
|
2230
|
+
};
|
|
2231
|
+
img.src = obj.href;
|
|
2232
|
+
}
|
|
2033
2233
|
renderTable(obj) {
|
|
2034
2234
|
if (obj.shape === "round") {
|
|
2035
2235
|
this.bgLayer.add(
|
|
@@ -2351,11 +2551,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2351
2551
|
}
|
|
2352
2552
|
/** Recompute a section's availability-tinted fill + "N LEFT" (cheap; on status change). */
|
|
2353
2553
|
refreshSectionFill(sec) {
|
|
2354
|
-
|
|
2355
|
-
sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
|
|
2554
|
+
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2356
2555
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
2357
2556
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
2358
2557
|
}
|
|
2558
|
+
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2559
|
+
isSectionClosed(sec) {
|
|
2560
|
+
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2564
|
+
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2565
|
+
* when another section holds focus (AXS dim treatment).
|
|
2566
|
+
*/
|
|
2567
|
+
sectionBlockFill(sec) {
|
|
2568
|
+
let fill;
|
|
2569
|
+
if (this.isSectionClosed(sec)) {
|
|
2570
|
+
fill = CLOSED_SECTION_FILL;
|
|
2571
|
+
} else {
|
|
2572
|
+
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2573
|
+
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2574
|
+
}
|
|
2575
|
+
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2576
|
+
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2577
|
+
}
|
|
2578
|
+
return fill;
|
|
2579
|
+
}
|
|
2359
2580
|
/**
|
|
2360
2581
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
2361
2582
|
* shown at the farthest zoom in place of per-section detail. Skipped entirely
|
|
@@ -2447,12 +2668,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2447
2668
|
const sx = this.stage.scaleX();
|
|
2448
2669
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
2449
2670
|
if (rescale) this.lodScale = scale;
|
|
2671
|
+
const focus = this.focusedSectionId;
|
|
2450
2672
|
for (const sec of this.sections) {
|
|
2451
|
-
sec.
|
|
2452
|
-
|
|
2673
|
+
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
2674
|
+
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
2675
|
+
for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT) * dim);
|
|
2453
2676
|
sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
|
|
2454
|
-
sec.nameLabel.opacity(1 - zoneT);
|
|
2455
|
-
sec.subLabel.opacity(blockT * (1 - zoneT));
|
|
2677
|
+
sec.nameLabel.opacity((1 - zoneT) * dim);
|
|
2678
|
+
sec.subLabel.opacity(blockT * (1 - zoneT) * dim);
|
|
2456
2679
|
if (rescale) {
|
|
2457
2680
|
this.sizeLabel(sec.nameLabel, SECTION_LABEL_PX / sx, sec.centroid.y - SECTION_SUB_PX / sx);
|
|
2458
2681
|
this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
|
|
@@ -2468,8 +2691,50 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2468
2691
|
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
2469
2692
|
}
|
|
2470
2693
|
}
|
|
2694
|
+
this.decollideRungLabels(sx);
|
|
2471
2695
|
this.bgLayer.batchDraw();
|
|
2472
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Greedy label de-collision for the zone/section rungs (same approach as the
|
|
2699
|
+
* designer's cullRowLabels): price/"N LEFT" sublabels are lowest priority and
|
|
2700
|
+
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
2701
|
+
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
2702
|
+
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
2703
|
+
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
2704
|
+
*/
|
|
2705
|
+
decollideRungLabels(sx) {
|
|
2706
|
+
const GAP = 4;
|
|
2707
|
+
const cands = [];
|
|
2708
|
+
const boxOf = (t2) => {
|
|
2709
|
+
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
2710
|
+
const w = t2.width() * sx;
|
|
2711
|
+
const h = t2.height() * sx;
|
|
2712
|
+
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
2713
|
+
};
|
|
2714
|
+
for (const zone of this.zones) {
|
|
2715
|
+
if (zone.label.opacity() > 0.05) cands.push({ node: zone.label, tier: 0, box: boxOf(zone.label) });
|
|
2716
|
+
if (zone.sub && zone.sub.opacity() > 0.05) cands.push({ node: zone.sub, tier: 2, owner: zone.label, box: boxOf(zone.sub) });
|
|
2717
|
+
}
|
|
2718
|
+
for (const sec of this.sections) {
|
|
2719
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
2720
|
+
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
2721
|
+
}
|
|
2722
|
+
if (cands.length < 2) return;
|
|
2723
|
+
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
2724
|
+
const kept = [];
|
|
2725
|
+
const culled = /* @__PURE__ */ new Set();
|
|
2726
|
+
const collides = (b) => kept.some(
|
|
2727
|
+
(k) => b.x < k.x + k.w + GAP && k.x < b.x + b.w + GAP && b.y < k.y + k.h + GAP && k.y < b.y + b.h + GAP
|
|
2728
|
+
);
|
|
2729
|
+
for (const c of cands) {
|
|
2730
|
+
if (c.owner && culled.has(c.owner) || collides(c.box)) {
|
|
2731
|
+
c.node.opacity(0);
|
|
2732
|
+
culled.add(c.node);
|
|
2733
|
+
} else {
|
|
2734
|
+
kept.push(c.box);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2473
2738
|
/** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
|
|
2474
2739
|
sizeLabel(t2, fontSize, y) {
|
|
2475
2740
|
t2.fontSize(Math.max(1, fontSize));
|
|
@@ -2516,6 +2781,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2516
2781
|
}
|
|
2517
2782
|
// ---- selection ------------------------------------------------------------
|
|
2518
2783
|
isSelectable(id) {
|
|
2784
|
+
if (this.seatInClosedSection(id)) return false;
|
|
2519
2785
|
const statuses = this.opts.selectableStatuses ?? ["free"];
|
|
2520
2786
|
return statuses.includes(this.statusById.get(id) ?? "free");
|
|
2521
2787
|
}
|
|
@@ -2587,19 +2853,84 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2587
2853
|
}
|
|
2588
2854
|
}
|
|
2589
2855
|
// ---- interaction ----------------------------------------------------------
|
|
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;
|
|
2890
|
+
}
|
|
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) {
|
|
2898
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.id);
|
|
2899
|
+
else this.focusSection(sec.id);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
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
|
+
}
|
|
2590
2928
|
wireInteraction() {
|
|
2591
2929
|
this.seatLayer.on("click tap", (e) => {
|
|
2592
2930
|
if (this.moved > 8) return;
|
|
2593
2931
|
const id = seatIdOf(e.target);
|
|
2594
2932
|
if (!id) return;
|
|
2595
|
-
|
|
2596
|
-
const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
|
|
2597
|
-
if (floorId) {
|
|
2598
|
-
this.opts.onDeckTap(floorId);
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
this.toggleSeat(id);
|
|
2933
|
+
this.handleSeatTap(id);
|
|
2603
2934
|
});
|
|
2604
2935
|
this.seatLayer.on("mouseover", (e) => {
|
|
2605
2936
|
const id = seatIdOf(e.target);
|
|
@@ -2625,10 +2956,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2625
2956
|
const factor = Math.exp(-e.evt.deltaY * 2e-3);
|
|
2626
2957
|
this.zoomAbout(this.stage.scaleX() * clamp(factor, 0.5, 2), pointer);
|
|
2627
2958
|
});
|
|
2628
|
-
this.stage.on("click tap", () => {
|
|
2629
|
-
if (
|
|
2959
|
+
this.stage.on("click tap", (e) => {
|
|
2960
|
+
if (this.moved > 8) return;
|
|
2630
2961
|
const pointer = this.stage.getPointerPosition();
|
|
2631
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
|
+
}
|
|
2632
2969
|
if (this.stacked && this.opts.onDeckTap) {
|
|
2633
2970
|
const floorId = this.deckFloorAt();
|
|
2634
2971
|
if (floorId) {
|
|
@@ -2958,6 +3295,9 @@ var PickerController = class {
|
|
|
2958
3295
|
this._doc = null;
|
|
2959
3296
|
/** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
|
|
2960
3297
|
this.hidden = /* @__PURE__ */ new Set();
|
|
3298
|
+
/** Section/zone ids in the `closed` event-state (Phase 2) — seats stay, greyed
|
|
3299
|
+
* + not pickable. Kept separate from `hidden` (which strips them). */
|
|
3300
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
2961
3301
|
/** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
|
|
2962
3302
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
2963
3303
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
@@ -2998,6 +3338,19 @@ var PickerController = class {
|
|
|
2998
3338
|
this.renderer?.setChart(this.visibleDoc());
|
|
2999
3339
|
return true;
|
|
3000
3340
|
}
|
|
3341
|
+
/** Adopt a new closed-section set; restyle (grey + non-pickable) if it differs.
|
|
3342
|
+
* Cheap restyle — no chart rebuild — so mid-sale open/close repaints live. */
|
|
3343
|
+
syncClosed(ids) {
|
|
3344
|
+
const next = (ids ?? []).filter((x) => typeof x === "string");
|
|
3345
|
+
if (next.length === this.closedSections.size && next.every((id) => this.closedSections.has(id))) return false;
|
|
3346
|
+
this.closedSections = new Set(next);
|
|
3347
|
+
this.renderer?.setClosedSections?.(next);
|
|
3348
|
+
return true;
|
|
3349
|
+
}
|
|
3350
|
+
/** Whether a section is currently in the `closed` state (card must not open). */
|
|
3351
|
+
isSectionClosed(id) {
|
|
3352
|
+
return this.closedSections.has(id);
|
|
3353
|
+
}
|
|
3001
3354
|
currentHold() {
|
|
3002
3355
|
return this.hold_;
|
|
3003
3356
|
}
|
|
@@ -3065,9 +3418,11 @@ var PickerController = class {
|
|
|
3065
3418
|
}
|
|
3066
3419
|
this.renderer = renderer;
|
|
3067
3420
|
let seats = null;
|
|
3421
|
+
let closedIds = [];
|
|
3068
3422
|
try {
|
|
3069
3423
|
const objs = await this.api.objects(this.key);
|
|
3070
3424
|
this.hidden = new Set(objs.hidden ?? []);
|
|
3425
|
+
closedIds = (objs.closed ?? []).filter((x) => typeof x === "string");
|
|
3071
3426
|
seats = objs.seats;
|
|
3072
3427
|
} catch {
|
|
3073
3428
|
}
|
|
@@ -3078,6 +3433,8 @@ var PickerController = class {
|
|
|
3078
3433
|
}
|
|
3079
3434
|
renderer.setChart(this.visibleDoc());
|
|
3080
3435
|
if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
|
|
3436
|
+
this.closedSections = new Set(closedIds);
|
|
3437
|
+
if (closedIds.length) renderer.setClosedSections?.(closedIds);
|
|
3081
3438
|
if (seats) this.applySeatsMap(seats);
|
|
3082
3439
|
this.connect();
|
|
3083
3440
|
return {
|
|
@@ -3162,13 +3519,23 @@ var PickerController = class {
|
|
|
3162
3519
|
*/
|
|
3163
3520
|
categoryAvailability() {
|
|
3164
3521
|
const out = {};
|
|
3522
|
+
const closedMembers = this.closedMemberIds();
|
|
3165
3523
|
for (const [id, s] of this.seatById) {
|
|
3524
|
+
if (closedMembers.has(id)) continue;
|
|
3166
3525
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
3167
3526
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
3168
3527
|
}
|
|
3169
3528
|
}
|
|
3170
3529
|
return out;
|
|
3171
3530
|
}
|
|
3531
|
+
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
3532
|
+
closedMemberIds() {
|
|
3533
|
+
const out = /* @__PURE__ */ new Set();
|
|
3534
|
+
const r = this.renderer;
|
|
3535
|
+
if (!r || !this.closedSections.size) return out;
|
|
3536
|
+
for (const id of this.closedSections) for (const sid of r.sectionMembers?.(id) ?? []) out.add(sid);
|
|
3537
|
+
return out;
|
|
3538
|
+
}
|
|
3172
3539
|
getGAAreas() {
|
|
3173
3540
|
const doc = this.visibleDoc();
|
|
3174
3541
|
if (!doc) return [];
|
|
@@ -3413,20 +3780,39 @@ var PickerController = class {
|
|
|
3413
3780
|
}
|
|
3414
3781
|
this.renderer?.setRung?.(rung);
|
|
3415
3782
|
}
|
|
3783
|
+
/** Price-band filter (F4): dim free seats whose category is outside `keys`
|
|
3784
|
+
* (null clears). The widget resolves which categories fall in the band. */
|
|
3785
|
+
setCategoryFilter(keys) {
|
|
3786
|
+
this.renderer?.setCategoryFilter?.(keys);
|
|
3787
|
+
}
|
|
3788
|
+
/** World-space rect currently visible + full chart bounds (F3 minimap frame). */
|
|
3789
|
+
getViewport() {
|
|
3790
|
+
const r = this.renderer;
|
|
3791
|
+
if (!r?.getVisibleWorldRect || !r.getWorldBounds) return null;
|
|
3792
|
+
return { visible: r.getVisibleWorldRect(), bounds: r.getWorldBounds() };
|
|
3793
|
+
}
|
|
3416
3794
|
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
3417
3795
|
focusSection(id) {
|
|
3418
3796
|
this.handleSectionTap(id);
|
|
3419
3797
|
}
|
|
3420
|
-
/** Zoom back out to the whole chart and clear the section-summary card. */
|
|
3798
|
+
/** Zoom back out to the whole chart and clear the section-summary card + focus. */
|
|
3421
3799
|
overview() {
|
|
3800
|
+
this.renderer?.clearSectionFocus?.();
|
|
3422
3801
|
this.renderer?.zoomToFit();
|
|
3423
3802
|
this.opts.onSectionFocus?.(null);
|
|
3424
3803
|
}
|
|
3425
|
-
/** Glide the camera into a tapped section and emit its computed summary.
|
|
3804
|
+
/** Glide the camera into a tapped section and emit its computed summary. Uses
|
|
3805
|
+
* the AXS focus treatment (dim + backdrop) when the engine supports it; a
|
|
3806
|
+
* closed section is framed but never opens a buyer card. */
|
|
3426
3807
|
handleSectionTap(id) {
|
|
3427
3808
|
const r = this.renderer;
|
|
3428
3809
|
if (!r) return;
|
|
3429
|
-
r.
|
|
3810
|
+
if (r.focusSection) r.focusSection(id);
|
|
3811
|
+
else r.focusRegion?.(id);
|
|
3812
|
+
if (this.isSectionClosed(id)) {
|
|
3813
|
+
this.opts.onSectionFocus?.(null);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3430
3816
|
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
3431
3817
|
}
|
|
3432
3818
|
/**
|
|
@@ -3700,6 +4086,9 @@ var PickerController = class {
|
|
|
3700
4086
|
void this.resnapshot();
|
|
3701
4087
|
this.opts.onStatusChange?.();
|
|
3702
4088
|
}
|
|
4089
|
+
if (Array.isArray(m.closed) && this.syncClosed(m.closed)) {
|
|
4090
|
+
this.opts.onStatusChange?.();
|
|
4091
|
+
}
|
|
3703
4092
|
if (m.type === "hidden") return;
|
|
3704
4093
|
if (m.seats && typeof m.seats === "object") {
|
|
3705
4094
|
this.applySeatsMap(m.seats);
|