@seatlayer/core 0.9.0 → 0.10.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 +335 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -5
- package/dist/index.d.ts +172 -5
- package/dist/index.js +335 -10
- 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);
|
|
@@ -960,6 +968,13 @@ var ZONE_SUB_PX = 12;
|
|
|
960
968
|
var HELD_FILL = "#6b7280";
|
|
961
969
|
var TAKEN_FILL = "#374151";
|
|
962
970
|
var NFS_STROKE = "#4b5563";
|
|
971
|
+
var CLOSED_SECTION_FILL = "#586070";
|
|
972
|
+
var CLOSED_SEAT_FILL = "#4b5563";
|
|
973
|
+
var CLOSED_SEAT_OPACITY = 0.4;
|
|
974
|
+
var FOCUS_DIM_OPACITY = 0.16;
|
|
975
|
+
var FOCUS_DESATURATE = 0.72;
|
|
976
|
+
var FOCUS_NEUTRAL = "#6b7280";
|
|
977
|
+
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
963
978
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
964
979
|
var ACCESS_RING = {
|
|
965
980
|
wheelchair: "#3b82f6",
|
|
@@ -1106,6 +1121,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1106
1121
|
this.accessFilter = null;
|
|
1107
1122
|
/** Category highlight (legend hover): dims free seats NOT of this category. */
|
|
1108
1123
|
this.categoryHighlight = null;
|
|
1124
|
+
/** Price-band filter (F4): dim free seats whose category is NOT in this set. */
|
|
1125
|
+
this.categoryFilter = null;
|
|
1109
1126
|
// Section/zone overlays (bgLayer) — the 3-rung LOD: seats → section blocks →
|
|
1110
1127
|
// zone blocks. Kept for the melt restyle and for hit-testing a zoomed-out tap.
|
|
1111
1128
|
this.sections = [];
|
|
@@ -1114,6 +1131,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1114
1131
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
1115
1132
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
1116
1133
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
1134
|
+
/** Phase 2: section/zone ids in the event-level `closed` state — flat grey
|
|
1135
|
+
* block, seats greyed + not pickable, but the section stays rendered. */
|
|
1136
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
1137
|
+
/** AXS section-focus: the currently-focused section id (others dim), or null. */
|
|
1138
|
+
this.focusedSectionId = null;
|
|
1139
|
+
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
1140
|
+
this.focusBackdrop = null;
|
|
1117
1141
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
1118
1142
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
1119
1143
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1594,6 +1618,27 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1594
1618
|
if (cur === null || next === null) return cur === next;
|
|
1595
1619
|
return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
|
|
1596
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Price-band filter (F4): dim free, unselected seats whose category key is NOT
|
|
1623
|
+
* in `keys`. `null` clears the filter (all categories fully visible). The
|
|
1624
|
+
* widget resolves which categories fall inside the buyer's chosen band.
|
|
1625
|
+
*/
|
|
1626
|
+
setCategoryFilter(keys) {
|
|
1627
|
+
const next = keys === null ? null : new Set(keys);
|
|
1628
|
+
const same = next === null && this.categoryFilter === null || next !== null && this.categoryFilter !== null && next.size === this.categoryFilter.size && [...next].every((k) => this.categoryFilter.has(k));
|
|
1629
|
+
if (same) return;
|
|
1630
|
+
this.categoryFilter = next;
|
|
1631
|
+
for (const seat of this.seats) {
|
|
1632
|
+
const c = this.circleById.get(seat.id);
|
|
1633
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1634
|
+
}
|
|
1635
|
+
if (this.cached) {
|
|
1636
|
+
this.seatLayer.clearCache();
|
|
1637
|
+
this.cacheSeatLayer();
|
|
1638
|
+
} else {
|
|
1639
|
+
this.seatLayer.batchDraw();
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1597
1642
|
// ---- isometric ("3D") view mode -------------------------------------------
|
|
1598
1643
|
/**
|
|
1599
1644
|
* Switch the projection between flat top-down and the isometric "3D" view
|
|
@@ -1920,12 +1965,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1920
1965
|
if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
|
|
1921
1966
|
c.opacity(0.25);
|
|
1922
1967
|
}
|
|
1968
|
+
if (this.categoryFilter && status === "free" && !selected && !this.categoryFilter.has(seat.categoryKey)) {
|
|
1969
|
+
c.opacity(0.22);
|
|
1970
|
+
}
|
|
1923
1971
|
if (this.dimmedSections.size) {
|
|
1924
1972
|
const sec = this.seatSection.get(id);
|
|
1925
1973
|
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1926
1974
|
c.opacity(0.18);
|
|
1927
1975
|
}
|
|
1928
1976
|
}
|
|
1977
|
+
if (this.closedSections.size && this.seatInClosedSection(id)) {
|
|
1978
|
+
c.fill(CLOSED_SEAT_FILL);
|
|
1979
|
+
c.stroke("");
|
|
1980
|
+
c.strokeWidth(0);
|
|
1981
|
+
c.dash([]);
|
|
1982
|
+
c.opacity(CLOSED_SEAT_OPACITY);
|
|
1983
|
+
}
|
|
1984
|
+
if (this.focusedSectionId) {
|
|
1985
|
+
const sec = this.seatSection.get(id);
|
|
1986
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
1987
|
+
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
1991
|
+
seatInClosedSection(id) {
|
|
1992
|
+
if (!this.closedSections.size) return false;
|
|
1993
|
+
const sec = this.seatSection.get(id);
|
|
1994
|
+
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
1929
1995
|
}
|
|
1930
1996
|
/**
|
|
1931
1997
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -1965,6 +2031,109 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1965
2031
|
this.seatLayer.batchDraw();
|
|
1966
2032
|
}
|
|
1967
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Phase 2 event-level section states: mark these section/zone ids `closed` —
|
|
2036
|
+
* flat grey block, seats greyed + not pickable, but the section stays rendered
|
|
2037
|
+
* (unlike the buyer's applyHidden which strips it). `null`/empty clears.
|
|
2038
|
+
*/
|
|
2039
|
+
setClosedSections(ids) {
|
|
2040
|
+
const next = new Set(ids ?? []);
|
|
2041
|
+
if (next.size === this.closedSections.size && [...next].every((i) => this.closedSections.has(i))) return;
|
|
2042
|
+
this.closedSections = next;
|
|
2043
|
+
this.repaintSectionsAndSeats();
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* AXS section-focus: dim + desaturate every other section, draw a calm backdrop
|
|
2047
|
+
* panel behind this section's seats, and glide the camera in to frame it (the
|
|
2048
|
+
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
2049
|
+
*/
|
|
2050
|
+
focusSection(id) {
|
|
2051
|
+
if (!this.sections.some((s) => s.id === id)) return;
|
|
2052
|
+
this.focusedSectionId = id;
|
|
2053
|
+
this.drawFocusBackdrop(id);
|
|
2054
|
+
this.repaintSectionsAndSeats();
|
|
2055
|
+
this.updateLOD();
|
|
2056
|
+
this.focusRegion(id);
|
|
2057
|
+
}
|
|
2058
|
+
/** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
|
|
2059
|
+
clearSectionFocus() {
|
|
2060
|
+
if (!this.focusedSectionId) return;
|
|
2061
|
+
this.focusedSectionId = null;
|
|
2062
|
+
if (this.focusBackdrop) {
|
|
2063
|
+
this.focusBackdrop.destroy();
|
|
2064
|
+
this.focusBackdrop = null;
|
|
2065
|
+
}
|
|
2066
|
+
this.repaintSectionsAndSeats();
|
|
2067
|
+
this.updateLOD();
|
|
2068
|
+
}
|
|
2069
|
+
/** The currently AXS-focused section id, or null. */
|
|
2070
|
+
getFocusedSection() {
|
|
2071
|
+
return this.focusedSectionId;
|
|
2072
|
+
}
|
|
2073
|
+
/** Draw (or replace) the light backdrop panel behind the focused section. */
|
|
2074
|
+
drawFocusBackdrop(id) {
|
|
2075
|
+
if (this.focusBackdrop) {
|
|
2076
|
+
this.focusBackdrop.destroy();
|
|
2077
|
+
this.focusBackdrop = null;
|
|
2078
|
+
}
|
|
2079
|
+
const sec = this.sections.find((s) => s.id === id);
|
|
2080
|
+
if (!sec) return;
|
|
2081
|
+
const panel = new import_Line.Line({
|
|
2082
|
+
points: sec.outline.flatMap((p) => [p.x, p.y]),
|
|
2083
|
+
closed: true,
|
|
2084
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
2085
|
+
stroke: rgba("#ffffff", 0.1),
|
|
2086
|
+
strokeWidth: 1,
|
|
2087
|
+
listening: false,
|
|
2088
|
+
perfectDrawEnabled: false
|
|
2089
|
+
});
|
|
2090
|
+
this.bgLayer.add(panel);
|
|
2091
|
+
panel.moveToTop();
|
|
2092
|
+
this.focusBackdrop = panel;
|
|
2093
|
+
}
|
|
2094
|
+
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
2095
|
+
repaintSectionsAndSeats() {
|
|
2096
|
+
for (const seat of this.seats) {
|
|
2097
|
+
const c = this.circleById.get(seat.id);
|
|
2098
|
+
if (c) this.paintSeat(c, seat.id);
|
|
2099
|
+
}
|
|
2100
|
+
for (const sec of this.sections) sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2101
|
+
if (this.cached) {
|
|
2102
|
+
this.seatLayer.clearCache();
|
|
2103
|
+
this.cacheSeatLayer();
|
|
2104
|
+
} else {
|
|
2105
|
+
this.seatLayer.batchDraw();
|
|
2106
|
+
}
|
|
2107
|
+
this.bgLayer.batchDraw();
|
|
2108
|
+
}
|
|
2109
|
+
/** The world-space rectangle currently visible in the viewport (minimap F3). */
|
|
2110
|
+
getVisibleWorldRect() {
|
|
2111
|
+
const tl = this.screenToWorld({ x: 0, y: 0 });
|
|
2112
|
+
const br = this.screenToWorld({ x: this.stage.width(), y: this.stage.height() });
|
|
2113
|
+
return {
|
|
2114
|
+
x: Math.min(tl.x, br.x),
|
|
2115
|
+
y: Math.min(tl.y, br.y),
|
|
2116
|
+
width: Math.abs(br.x - tl.x),
|
|
2117
|
+
height: Math.abs(br.y - tl.y)
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/** Axis-aligned world bounds of all seats + section outlines (minimap F3 frame). */
|
|
2121
|
+
getWorldBounds() {
|
|
2122
|
+
let minX = Infinity;
|
|
2123
|
+
let minY = Infinity;
|
|
2124
|
+
let maxX = -Infinity;
|
|
2125
|
+
let maxY = -Infinity;
|
|
2126
|
+
const grow = (x, y) => {
|
|
2127
|
+
if (x < minX) minX = x;
|
|
2128
|
+
if (y < minY) minY = y;
|
|
2129
|
+
if (x > maxX) maxX = x;
|
|
2130
|
+
if (y > maxY) maxY = y;
|
|
2131
|
+
};
|
|
2132
|
+
for (const s of this.seats) grow(s.x, s.y);
|
|
2133
|
+
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
2134
|
+
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
2135
|
+
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
2136
|
+
}
|
|
1968
2137
|
/** Legend hover: highlight one category (dim the rest), or null to clear. */
|
|
1969
2138
|
setCategoryHighlight(key) {
|
|
1970
2139
|
if (this.categoryHighlight === key) return;
|
|
@@ -1982,6 +2151,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1982
2151
|
}
|
|
1983
2152
|
renderBackground(doc) {
|
|
1984
2153
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
2154
|
+
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
1985
2155
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
1986
2156
|
this.renderZones(doc);
|
|
1987
2157
|
for (const obj of doc.objects) {
|
|
@@ -2030,6 +2200,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2030
2200
|
};
|
|
2031
2201
|
img.src = bg.url;
|
|
2032
2202
|
}
|
|
2203
|
+
/**
|
|
2204
|
+
* A decor graphic (rink / court / stage art). The KImage node is added to the
|
|
2205
|
+
* bgLayer synchronously so it keeps its z-slot beneath the sections drawn right
|
|
2206
|
+
* after; the bitmap is decoded async and pasted in on load. A single node = a
|
|
2207
|
+
* single drawImage per frame, and it rides the same layer cache — effectively
|
|
2208
|
+
* zero per-frame cost. Never listens, so it can't intercept a seat click.
|
|
2209
|
+
*/
|
|
2210
|
+
renderDecorImage(obj) {
|
|
2211
|
+
const img = new window.Image();
|
|
2212
|
+
const node = new import_Image.Image({
|
|
2213
|
+
image: img,
|
|
2214
|
+
x: obj.x + obj.width / 2,
|
|
2215
|
+
y: obj.y + obj.height / 2,
|
|
2216
|
+
offsetX: obj.width / 2,
|
|
2217
|
+
offsetY: obj.height / 2,
|
|
2218
|
+
width: obj.width,
|
|
2219
|
+
height: obj.height,
|
|
2220
|
+
rotation: obj.rotation ?? 0,
|
|
2221
|
+
opacity: clamp(obj.opacity ?? 1, 0, 1),
|
|
2222
|
+
listening: false,
|
|
2223
|
+
perfectDrawEnabled: false
|
|
2224
|
+
});
|
|
2225
|
+
this.bgLayer.add(node);
|
|
2226
|
+
img.onload = () => {
|
|
2227
|
+
if (!node.getLayer()) return;
|
|
2228
|
+
this.bgLayer.batchDraw();
|
|
2229
|
+
};
|
|
2230
|
+
img.src = obj.href;
|
|
2231
|
+
}
|
|
2033
2232
|
renderTable(obj) {
|
|
2034
2233
|
if (obj.shape === "round") {
|
|
2035
2234
|
this.bgLayer.add(
|
|
@@ -2351,11 +2550,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2351
2550
|
}
|
|
2352
2551
|
/** Recompute a section's availability-tinted fill + "N LEFT" (cheap; on status change). */
|
|
2353
2552
|
refreshSectionFill(sec) {
|
|
2354
|
-
|
|
2355
|
-
sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
|
|
2553
|
+
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2356
2554
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
2357
2555
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
2358
2556
|
}
|
|
2557
|
+
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2558
|
+
isSectionClosed(sec) {
|
|
2559
|
+
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2563
|
+
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2564
|
+
* when another section holds focus (AXS dim treatment).
|
|
2565
|
+
*/
|
|
2566
|
+
sectionBlockFill(sec) {
|
|
2567
|
+
let fill;
|
|
2568
|
+
if (this.isSectionClosed(sec)) {
|
|
2569
|
+
fill = CLOSED_SECTION_FILL;
|
|
2570
|
+
} else {
|
|
2571
|
+
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2572
|
+
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2573
|
+
}
|
|
2574
|
+
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2575
|
+
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2576
|
+
}
|
|
2577
|
+
return fill;
|
|
2578
|
+
}
|
|
2359
2579
|
/**
|
|
2360
2580
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
2361
2581
|
* shown at the farthest zoom in place of per-section detail. Skipped entirely
|
|
@@ -2447,12 +2667,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2447
2667
|
const sx = this.stage.scaleX();
|
|
2448
2668
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
2449
2669
|
if (rescale) this.lodScale = scale;
|
|
2670
|
+
const focus = this.focusedSectionId;
|
|
2450
2671
|
for (const sec of this.sections) {
|
|
2451
|
-
sec.
|
|
2452
|
-
|
|
2672
|
+
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
2673
|
+
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
2674
|
+
for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT) * dim);
|
|
2453
2675
|
sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
|
|
2454
|
-
sec.nameLabel.opacity(1 - zoneT);
|
|
2455
|
-
sec.subLabel.opacity(blockT * (1 - zoneT));
|
|
2676
|
+
sec.nameLabel.opacity((1 - zoneT) * dim);
|
|
2677
|
+
sec.subLabel.opacity(blockT * (1 - zoneT) * dim);
|
|
2456
2678
|
if (rescale) {
|
|
2457
2679
|
this.sizeLabel(sec.nameLabel, SECTION_LABEL_PX / sx, sec.centroid.y - SECTION_SUB_PX / sx);
|
|
2458
2680
|
this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
|
|
@@ -2468,8 +2690,50 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2468
2690
|
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
2469
2691
|
}
|
|
2470
2692
|
}
|
|
2693
|
+
this.decollideRungLabels(sx);
|
|
2471
2694
|
this.bgLayer.batchDraw();
|
|
2472
2695
|
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Greedy label de-collision for the zone/section rungs (same approach as the
|
|
2698
|
+
* designer's cullRowLabels): price/"N LEFT" sublabels are lowest priority and
|
|
2699
|
+
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
2700
|
+
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
2701
|
+
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
2702
|
+
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
2703
|
+
*/
|
|
2704
|
+
decollideRungLabels(sx) {
|
|
2705
|
+
const GAP = 4;
|
|
2706
|
+
const cands = [];
|
|
2707
|
+
const boxOf = (t2) => {
|
|
2708
|
+
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
2709
|
+
const w = t2.width() * sx;
|
|
2710
|
+
const h = t2.height() * sx;
|
|
2711
|
+
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
2712
|
+
};
|
|
2713
|
+
for (const zone of this.zones) {
|
|
2714
|
+
if (zone.label.opacity() > 0.05) cands.push({ node: zone.label, tier: 0, box: boxOf(zone.label) });
|
|
2715
|
+
if (zone.sub && zone.sub.opacity() > 0.05) cands.push({ node: zone.sub, tier: 2, owner: zone.label, box: boxOf(zone.sub) });
|
|
2716
|
+
}
|
|
2717
|
+
for (const sec of this.sections) {
|
|
2718
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
2719
|
+
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
2720
|
+
}
|
|
2721
|
+
if (cands.length < 2) return;
|
|
2722
|
+
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
2723
|
+
const kept = [];
|
|
2724
|
+
const culled = /* @__PURE__ */ new Set();
|
|
2725
|
+
const collides = (b) => kept.some(
|
|
2726
|
+
(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
|
|
2727
|
+
);
|
|
2728
|
+
for (const c of cands) {
|
|
2729
|
+
if (c.owner && culled.has(c.owner) || collides(c.box)) {
|
|
2730
|
+
c.node.opacity(0);
|
|
2731
|
+
culled.add(c.node);
|
|
2732
|
+
} else {
|
|
2733
|
+
kept.push(c.box);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2473
2737
|
/** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
|
|
2474
2738
|
sizeLabel(t2, fontSize, y) {
|
|
2475
2739
|
t2.fontSize(Math.max(1, fontSize));
|
|
@@ -2516,6 +2780,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2516
2780
|
}
|
|
2517
2781
|
// ---- selection ------------------------------------------------------------
|
|
2518
2782
|
isSelectable(id) {
|
|
2783
|
+
if (this.seatInClosedSection(id)) return false;
|
|
2519
2784
|
const statuses = this.opts.selectableStatuses ?? ["free"];
|
|
2520
2785
|
return statuses.includes(this.statusById.get(id) ?? "free");
|
|
2521
2786
|
}
|
|
@@ -2599,6 +2864,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2599
2864
|
return;
|
|
2600
2865
|
}
|
|
2601
2866
|
}
|
|
2867
|
+
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
2868
|
+
const sec = this.seatSection.get(id);
|
|
2869
|
+
if (sec) {
|
|
2870
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.id);
|
|
2871
|
+
else this.focusSection(sec.id);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2602
2875
|
this.toggleSeat(id);
|
|
2603
2876
|
});
|
|
2604
2877
|
this.seatLayer.on("mouseover", (e) => {
|
|
@@ -2958,6 +3231,9 @@ var PickerController = class {
|
|
|
2958
3231
|
this._doc = null;
|
|
2959
3232
|
/** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
|
|
2960
3233
|
this.hidden = /* @__PURE__ */ new Set();
|
|
3234
|
+
/** Section/zone ids in the `closed` event-state (Phase 2) — seats stay, greyed
|
|
3235
|
+
* + not pickable. Kept separate from `hidden` (which strips them). */
|
|
3236
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
2961
3237
|
/** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
|
|
2962
3238
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
2963
3239
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
@@ -2998,6 +3274,19 @@ var PickerController = class {
|
|
|
2998
3274
|
this.renderer?.setChart(this.visibleDoc());
|
|
2999
3275
|
return true;
|
|
3000
3276
|
}
|
|
3277
|
+
/** Adopt a new closed-section set; restyle (grey + non-pickable) if it differs.
|
|
3278
|
+
* Cheap restyle — no chart rebuild — so mid-sale open/close repaints live. */
|
|
3279
|
+
syncClosed(ids) {
|
|
3280
|
+
const next = (ids ?? []).filter((x) => typeof x === "string");
|
|
3281
|
+
if (next.length === this.closedSections.size && next.every((id) => this.closedSections.has(id))) return false;
|
|
3282
|
+
this.closedSections = new Set(next);
|
|
3283
|
+
this.renderer?.setClosedSections?.(next);
|
|
3284
|
+
return true;
|
|
3285
|
+
}
|
|
3286
|
+
/** Whether a section is currently in the `closed` state (card must not open). */
|
|
3287
|
+
isSectionClosed(id) {
|
|
3288
|
+
return this.closedSections.has(id);
|
|
3289
|
+
}
|
|
3001
3290
|
currentHold() {
|
|
3002
3291
|
return this.hold_;
|
|
3003
3292
|
}
|
|
@@ -3065,9 +3354,11 @@ var PickerController = class {
|
|
|
3065
3354
|
}
|
|
3066
3355
|
this.renderer = renderer;
|
|
3067
3356
|
let seats = null;
|
|
3357
|
+
let closedIds = [];
|
|
3068
3358
|
try {
|
|
3069
3359
|
const objs = await this.api.objects(this.key);
|
|
3070
3360
|
this.hidden = new Set(objs.hidden ?? []);
|
|
3361
|
+
closedIds = (objs.closed ?? []).filter((x) => typeof x === "string");
|
|
3071
3362
|
seats = objs.seats;
|
|
3072
3363
|
} catch {
|
|
3073
3364
|
}
|
|
@@ -3078,6 +3369,8 @@ var PickerController = class {
|
|
|
3078
3369
|
}
|
|
3079
3370
|
renderer.setChart(this.visibleDoc());
|
|
3080
3371
|
if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
|
|
3372
|
+
this.closedSections = new Set(closedIds);
|
|
3373
|
+
if (closedIds.length) renderer.setClosedSections?.(closedIds);
|
|
3081
3374
|
if (seats) this.applySeatsMap(seats);
|
|
3082
3375
|
this.connect();
|
|
3083
3376
|
return {
|
|
@@ -3162,13 +3455,23 @@ var PickerController = class {
|
|
|
3162
3455
|
*/
|
|
3163
3456
|
categoryAvailability() {
|
|
3164
3457
|
const out = {};
|
|
3458
|
+
const closedMembers = this.closedMemberIds();
|
|
3165
3459
|
for (const [id, s] of this.seatById) {
|
|
3460
|
+
if (closedMembers.has(id)) continue;
|
|
3166
3461
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
3167
3462
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
3168
3463
|
}
|
|
3169
3464
|
}
|
|
3170
3465
|
return out;
|
|
3171
3466
|
}
|
|
3467
|
+
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
3468
|
+
closedMemberIds() {
|
|
3469
|
+
const out = /* @__PURE__ */ new Set();
|
|
3470
|
+
const r = this.renderer;
|
|
3471
|
+
if (!r || !this.closedSections.size) return out;
|
|
3472
|
+
for (const id of this.closedSections) for (const sid of r.sectionMembers?.(id) ?? []) out.add(sid);
|
|
3473
|
+
return out;
|
|
3474
|
+
}
|
|
3172
3475
|
getGAAreas() {
|
|
3173
3476
|
const doc = this.visibleDoc();
|
|
3174
3477
|
if (!doc) return [];
|
|
@@ -3413,20 +3716,39 @@ var PickerController = class {
|
|
|
3413
3716
|
}
|
|
3414
3717
|
this.renderer?.setRung?.(rung);
|
|
3415
3718
|
}
|
|
3719
|
+
/** Price-band filter (F4): dim free seats whose category is outside `keys`
|
|
3720
|
+
* (null clears). The widget resolves which categories fall in the band. */
|
|
3721
|
+
setCategoryFilter(keys) {
|
|
3722
|
+
this.renderer?.setCategoryFilter?.(keys);
|
|
3723
|
+
}
|
|
3724
|
+
/** World-space rect currently visible + full chart bounds (F3 minimap frame). */
|
|
3725
|
+
getViewport() {
|
|
3726
|
+
const r = this.renderer;
|
|
3727
|
+
if (!r?.getVisibleWorldRect || !r.getWorldBounds) return null;
|
|
3728
|
+
return { visible: r.getVisibleWorldRect(), bounds: r.getWorldBounds() };
|
|
3729
|
+
}
|
|
3416
3730
|
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
3417
3731
|
focusSection(id) {
|
|
3418
3732
|
this.handleSectionTap(id);
|
|
3419
3733
|
}
|
|
3420
|
-
/** Zoom back out to the whole chart and clear the section-summary card. */
|
|
3734
|
+
/** Zoom back out to the whole chart and clear the section-summary card + focus. */
|
|
3421
3735
|
overview() {
|
|
3736
|
+
this.renderer?.clearSectionFocus?.();
|
|
3422
3737
|
this.renderer?.zoomToFit();
|
|
3423
3738
|
this.opts.onSectionFocus?.(null);
|
|
3424
3739
|
}
|
|
3425
|
-
/** Glide the camera into a tapped section and emit its computed summary.
|
|
3740
|
+
/** Glide the camera into a tapped section and emit its computed summary. Uses
|
|
3741
|
+
* the AXS focus treatment (dim + backdrop) when the engine supports it; a
|
|
3742
|
+
* closed section is framed but never opens a buyer card. */
|
|
3426
3743
|
handleSectionTap(id) {
|
|
3427
3744
|
const r = this.renderer;
|
|
3428
3745
|
if (!r) return;
|
|
3429
|
-
r.
|
|
3746
|
+
if (r.focusSection) r.focusSection(id);
|
|
3747
|
+
else r.focusRegion?.(id);
|
|
3748
|
+
if (this.isSectionClosed(id)) {
|
|
3749
|
+
this.opts.onSectionFocus?.(null);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3430
3752
|
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
3431
3753
|
}
|
|
3432
3754
|
/**
|
|
@@ -3700,6 +4022,9 @@ var PickerController = class {
|
|
|
3700
4022
|
void this.resnapshot();
|
|
3701
4023
|
this.opts.onStatusChange?.();
|
|
3702
4024
|
}
|
|
4025
|
+
if (Array.isArray(m.closed) && this.syncClosed(m.closed)) {
|
|
4026
|
+
this.opts.onStatusChange?.();
|
|
4027
|
+
}
|
|
3703
4028
|
if (m.type === "hidden") return;
|
|
3704
4029
|
if (m.seats && typeof m.seats === "object") {
|
|
3705
4030
|
this.applySeatsMap(m.seats);
|