@seatlayer/core 0.8.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 +355 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +187 -5
- package/dist/index.d.ts +187 -5
- package/dist/index.js +355 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,9 +20,10 @@ function layerOf(obj) {
|
|
|
20
20
|
case "booth":
|
|
21
21
|
case "section":
|
|
22
22
|
return "interactive";
|
|
23
|
-
// stage / décor live on 'shape'; free text
|
|
23
|
+
// stage / décor live on 'shape'; free text + decor images are background furniture.
|
|
24
24
|
case "shape":
|
|
25
25
|
case "text":
|
|
26
|
+
case "decorImage":
|
|
26
27
|
return "background";
|
|
27
28
|
default:
|
|
28
29
|
return "interactive";
|
|
@@ -251,6 +252,8 @@ function objectCenter(o) {
|
|
|
251
252
|
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
252
253
|
}
|
|
253
254
|
return { x: o.x ?? 0, y: o.y ?? 0 };
|
|
255
|
+
case "decorImage":
|
|
256
|
+
return { x: o.x + o.width / 2, y: o.y + o.height / 2 };
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
function floorsOf(doc) {
|
|
@@ -286,6 +289,8 @@ function translateObject(o, dx, dy) {
|
|
|
286
289
|
...o.x != null ? { x: o.x + dx } : {},
|
|
287
290
|
...o.y != null ? { y: o.y + dy } : {}
|
|
288
291
|
};
|
|
292
|
+
case "decorImage":
|
|
293
|
+
return { ...o, x: o.x + dx, y: o.y + dy };
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
296
|
function stackFloors(doc, spread = 900) {
|
|
@@ -324,6 +329,9 @@ function chartBounds(doc) {
|
|
|
324
329
|
for (const p of obj.points) acc(p.x, p.y);
|
|
325
330
|
} else if (obj.type === "section") {
|
|
326
331
|
for (const p of obj.outline) acc(p.x, p.y);
|
|
332
|
+
} else if (obj.type === "decorImage") {
|
|
333
|
+
acc(obj.x, obj.y);
|
|
334
|
+
acc(obj.x + obj.width, obj.y + obj.height);
|
|
327
335
|
} else if (obj.type === "shape") {
|
|
328
336
|
if (obj.points && obj.points.length) {
|
|
329
337
|
for (const p of obj.points) acc(p.x, p.y);
|
|
@@ -641,6 +649,13 @@ var ZONE_SUB_PX = 12;
|
|
|
641
649
|
var HELD_FILL = "#6b7280";
|
|
642
650
|
var TAKEN_FILL = "#374151";
|
|
643
651
|
var NFS_STROKE = "#4b5563";
|
|
652
|
+
var CLOSED_SECTION_FILL = "#586070";
|
|
653
|
+
var CLOSED_SEAT_FILL = "#4b5563";
|
|
654
|
+
var CLOSED_SEAT_OPACITY = 0.4;
|
|
655
|
+
var FOCUS_DIM_OPACITY = 0.16;
|
|
656
|
+
var FOCUS_DESATURATE = 0.72;
|
|
657
|
+
var FOCUS_NEUTRAL = "#6b7280";
|
|
658
|
+
var FOCUS_BACKDROP_FILL = "rgba(244,246,248,0.06)";
|
|
644
659
|
var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
|
|
645
660
|
var ACCESS_RING = {
|
|
646
661
|
wheelchair: "#3b82f6",
|
|
@@ -787,6 +802,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
787
802
|
this.accessFilter = null;
|
|
788
803
|
/** Category highlight (legend hover): dims free seats NOT of this category. */
|
|
789
804
|
this.categoryHighlight = null;
|
|
805
|
+
/** Price-band filter (F4): dim free seats whose category is NOT in this set. */
|
|
806
|
+
this.categoryFilter = null;
|
|
790
807
|
// Section/zone overlays (bgLayer) — the 3-rung LOD: seats → section blocks →
|
|
791
808
|
// zone blocks. Kept for the melt restyle and for hit-testing a zoomed-out tap.
|
|
792
809
|
this.sections = [];
|
|
@@ -795,6 +812,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
795
812
|
this.catPrice = /* @__PURE__ */ new Map();
|
|
796
813
|
/** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
|
|
797
814
|
this.dimmedSections = /* @__PURE__ */ new Set();
|
|
815
|
+
/** Phase 2: section/zone ids in the event-level `closed` state — flat grey
|
|
816
|
+
* block, seats greyed + not pickable, but the section stays rendered. */
|
|
817
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
818
|
+
/** AXS section-focus: the currently-focused section id (others dim), or null. */
|
|
819
|
+
this.focusedSectionId = null;
|
|
820
|
+
/** Light backdrop panel drawn behind the focused section (removed on clear). */
|
|
821
|
+
this.focusBackdrop = null;
|
|
798
822
|
/** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
|
|
799
823
|
this.objectFloor = /* @__PURE__ */ new Map();
|
|
800
824
|
/** Zone id → colour (drives extruded side faces in iso view). */
|
|
@@ -1275,6 +1299,27 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1275
1299
|
if (cur === null || next === null) return cur === next;
|
|
1276
1300
|
return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
|
|
1277
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Price-band filter (F4): dim free, unselected seats whose category key is NOT
|
|
1304
|
+
* in `keys`. `null` clears the filter (all categories fully visible). The
|
|
1305
|
+
* widget resolves which categories fall inside the buyer's chosen band.
|
|
1306
|
+
*/
|
|
1307
|
+
setCategoryFilter(keys) {
|
|
1308
|
+
const next = keys === null ? null : new Set(keys);
|
|
1309
|
+
const same = next === null && this.categoryFilter === null || next !== null && this.categoryFilter !== null && next.size === this.categoryFilter.size && [...next].every((k) => this.categoryFilter.has(k));
|
|
1310
|
+
if (same) return;
|
|
1311
|
+
this.categoryFilter = next;
|
|
1312
|
+
for (const seat of this.seats) {
|
|
1313
|
+
const c = this.circleById.get(seat.id);
|
|
1314
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1315
|
+
}
|
|
1316
|
+
if (this.cached) {
|
|
1317
|
+
this.seatLayer.clearCache();
|
|
1318
|
+
this.cacheSeatLayer();
|
|
1319
|
+
} else {
|
|
1320
|
+
this.seatLayer.batchDraw();
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1278
1323
|
// ---- isometric ("3D") view mode -------------------------------------------
|
|
1279
1324
|
/**
|
|
1280
1325
|
* Switch the projection between flat top-down and the isometric "3D" view
|
|
@@ -1601,12 +1646,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1601
1646
|
if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
|
|
1602
1647
|
c.opacity(0.25);
|
|
1603
1648
|
}
|
|
1649
|
+
if (this.categoryFilter && status === "free" && !selected && !this.categoryFilter.has(seat.categoryKey)) {
|
|
1650
|
+
c.opacity(0.22);
|
|
1651
|
+
}
|
|
1604
1652
|
if (this.dimmedSections.size) {
|
|
1605
1653
|
const sec = this.seatSection.get(id);
|
|
1606
1654
|
if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
|
|
1607
1655
|
c.opacity(0.18);
|
|
1608
1656
|
}
|
|
1609
1657
|
}
|
|
1658
|
+
if (this.closedSections.size && this.seatInClosedSection(id)) {
|
|
1659
|
+
c.fill(CLOSED_SEAT_FILL);
|
|
1660
|
+
c.stroke("");
|
|
1661
|
+
c.strokeWidth(0);
|
|
1662
|
+
c.dash([]);
|
|
1663
|
+
c.opacity(CLOSED_SEAT_OPACITY);
|
|
1664
|
+
}
|
|
1665
|
+
if (this.focusedSectionId) {
|
|
1666
|
+
const sec = this.seatSection.get(id);
|
|
1667
|
+
const inFocus = !!sec && (sec.id === this.focusedSectionId || sec.zone === this.focusedSectionId);
|
|
1668
|
+
if (!inFocus) c.opacity(FOCUS_DIM_OPACITY);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
1672
|
+
seatInClosedSection(id) {
|
|
1673
|
+
if (!this.closedSections.size) return false;
|
|
1674
|
+
const sec = this.seatSection.get(id);
|
|
1675
|
+
return !!sec && (this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone));
|
|
1610
1676
|
}
|
|
1611
1677
|
/**
|
|
1612
1678
|
* Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
|
|
@@ -1646,6 +1712,109 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1646
1712
|
this.seatLayer.batchDraw();
|
|
1647
1713
|
}
|
|
1648
1714
|
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Phase 2 event-level section states: mark these section/zone ids `closed` —
|
|
1717
|
+
* flat grey block, seats greyed + not pickable, but the section stays rendered
|
|
1718
|
+
* (unlike the buyer's applyHidden which strips it). `null`/empty clears.
|
|
1719
|
+
*/
|
|
1720
|
+
setClosedSections(ids) {
|
|
1721
|
+
const next = new Set(ids ?? []);
|
|
1722
|
+
if (next.size === this.closedSections.size && [...next].every((i) => this.closedSections.has(i))) return;
|
|
1723
|
+
this.closedSections = next;
|
|
1724
|
+
this.repaintSectionsAndSeats();
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* AXS section-focus: dim + desaturate every other section, draw a calm backdrop
|
|
1728
|
+
* panel behind this section's seats, and glide the camera in to frame it (the
|
|
1729
|
+
* seat-pick gate below only lets buyers pick once seats are ≥ LABEL_SCALE big).
|
|
1730
|
+
*/
|
|
1731
|
+
focusSection(id) {
|
|
1732
|
+
if (!this.sections.some((s) => s.id === id)) return;
|
|
1733
|
+
this.focusedSectionId = id;
|
|
1734
|
+
this.drawFocusBackdrop(id);
|
|
1735
|
+
this.repaintSectionsAndSeats();
|
|
1736
|
+
this.updateLOD();
|
|
1737
|
+
this.focusRegion(id);
|
|
1738
|
+
}
|
|
1739
|
+
/** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
|
|
1740
|
+
clearSectionFocus() {
|
|
1741
|
+
if (!this.focusedSectionId) return;
|
|
1742
|
+
this.focusedSectionId = null;
|
|
1743
|
+
if (this.focusBackdrop) {
|
|
1744
|
+
this.focusBackdrop.destroy();
|
|
1745
|
+
this.focusBackdrop = null;
|
|
1746
|
+
}
|
|
1747
|
+
this.repaintSectionsAndSeats();
|
|
1748
|
+
this.updateLOD();
|
|
1749
|
+
}
|
|
1750
|
+
/** The currently AXS-focused section id, or null. */
|
|
1751
|
+
getFocusedSection() {
|
|
1752
|
+
return this.focusedSectionId;
|
|
1753
|
+
}
|
|
1754
|
+
/** Draw (or replace) the light backdrop panel behind the focused section. */
|
|
1755
|
+
drawFocusBackdrop(id) {
|
|
1756
|
+
if (this.focusBackdrop) {
|
|
1757
|
+
this.focusBackdrop.destroy();
|
|
1758
|
+
this.focusBackdrop = null;
|
|
1759
|
+
}
|
|
1760
|
+
const sec = this.sections.find((s) => s.id === id);
|
|
1761
|
+
if (!sec) return;
|
|
1762
|
+
const panel = new Line({
|
|
1763
|
+
points: sec.outline.flatMap((p) => [p.x, p.y]),
|
|
1764
|
+
closed: true,
|
|
1765
|
+
fill: FOCUS_BACKDROP_FILL,
|
|
1766
|
+
stroke: rgba("#ffffff", 0.1),
|
|
1767
|
+
strokeWidth: 1,
|
|
1768
|
+
listening: false,
|
|
1769
|
+
perfectDrawEnabled: false
|
|
1770
|
+
});
|
|
1771
|
+
this.bgLayer.add(panel);
|
|
1772
|
+
panel.moveToTop();
|
|
1773
|
+
this.focusBackdrop = panel;
|
|
1774
|
+
}
|
|
1775
|
+
/** Repaint every seat + section block to reflect closed/focus state, then redraw. */
|
|
1776
|
+
repaintSectionsAndSeats() {
|
|
1777
|
+
for (const seat of this.seats) {
|
|
1778
|
+
const c = this.circleById.get(seat.id);
|
|
1779
|
+
if (c) this.paintSeat(c, seat.id);
|
|
1780
|
+
}
|
|
1781
|
+
for (const sec of this.sections) sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
1782
|
+
if (this.cached) {
|
|
1783
|
+
this.seatLayer.clearCache();
|
|
1784
|
+
this.cacheSeatLayer();
|
|
1785
|
+
} else {
|
|
1786
|
+
this.seatLayer.batchDraw();
|
|
1787
|
+
}
|
|
1788
|
+
this.bgLayer.batchDraw();
|
|
1789
|
+
}
|
|
1790
|
+
/** The world-space rectangle currently visible in the viewport (minimap F3). */
|
|
1791
|
+
getVisibleWorldRect() {
|
|
1792
|
+
const tl = this.screenToWorld({ x: 0, y: 0 });
|
|
1793
|
+
const br = this.screenToWorld({ x: this.stage.width(), y: this.stage.height() });
|
|
1794
|
+
return {
|
|
1795
|
+
x: Math.min(tl.x, br.x),
|
|
1796
|
+
y: Math.min(tl.y, br.y),
|
|
1797
|
+
width: Math.abs(br.x - tl.x),
|
|
1798
|
+
height: Math.abs(br.y - tl.y)
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
/** Axis-aligned world bounds of all seats + section outlines (minimap F3 frame). */
|
|
1802
|
+
getWorldBounds() {
|
|
1803
|
+
let minX = Infinity;
|
|
1804
|
+
let minY = Infinity;
|
|
1805
|
+
let maxX = -Infinity;
|
|
1806
|
+
let maxY = -Infinity;
|
|
1807
|
+
const grow = (x, y) => {
|
|
1808
|
+
if (x < minX) minX = x;
|
|
1809
|
+
if (y < minY) minY = y;
|
|
1810
|
+
if (x > maxX) maxX = x;
|
|
1811
|
+
if (y > maxY) maxY = y;
|
|
1812
|
+
};
|
|
1813
|
+
for (const s of this.seats) grow(s.x, s.y);
|
|
1814
|
+
for (const sec of this.sections) for (const p of sec.outline) grow(p.x, p.y);
|
|
1815
|
+
if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
|
|
1816
|
+
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
|
|
1817
|
+
}
|
|
1649
1818
|
/** Legend hover: highlight one category (dim the rest), or null to clear. */
|
|
1650
1819
|
setCategoryHighlight(key) {
|
|
1651
1820
|
if (this.categoryHighlight === key) return;
|
|
@@ -1663,6 +1832,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1663
1832
|
}
|
|
1664
1833
|
renderBackground(doc) {
|
|
1665
1834
|
if (doc.backgroundImage) this.renderBackgroundImage(doc.backgroundImage);
|
|
1835
|
+
for (const obj of doc.objects) if (obj.type === "decorImage") this.renderDecorImage(obj);
|
|
1666
1836
|
for (const obj of doc.objects) if (obj.type === "section") this.renderSection(obj);
|
|
1667
1837
|
this.renderZones(doc);
|
|
1668
1838
|
for (const obj of doc.objects) {
|
|
@@ -1711,6 +1881,35 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1711
1881
|
};
|
|
1712
1882
|
img.src = bg.url;
|
|
1713
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* A decor graphic (rink / court / stage art). The KImage node is added to the
|
|
1886
|
+
* bgLayer synchronously so it keeps its z-slot beneath the sections drawn right
|
|
1887
|
+
* after; the bitmap is decoded async and pasted in on load. A single node = a
|
|
1888
|
+
* single drawImage per frame, and it rides the same layer cache — effectively
|
|
1889
|
+
* zero per-frame cost. Never listens, so it can't intercept a seat click.
|
|
1890
|
+
*/
|
|
1891
|
+
renderDecorImage(obj) {
|
|
1892
|
+
const img = new window.Image();
|
|
1893
|
+
const node = new KImage({
|
|
1894
|
+
image: img,
|
|
1895
|
+
x: obj.x + obj.width / 2,
|
|
1896
|
+
y: obj.y + obj.height / 2,
|
|
1897
|
+
offsetX: obj.width / 2,
|
|
1898
|
+
offsetY: obj.height / 2,
|
|
1899
|
+
width: obj.width,
|
|
1900
|
+
height: obj.height,
|
|
1901
|
+
rotation: obj.rotation ?? 0,
|
|
1902
|
+
opacity: clamp(obj.opacity ?? 1, 0, 1),
|
|
1903
|
+
listening: false,
|
|
1904
|
+
perfectDrawEnabled: false
|
|
1905
|
+
});
|
|
1906
|
+
this.bgLayer.add(node);
|
|
1907
|
+
img.onload = () => {
|
|
1908
|
+
if (!node.getLayer()) return;
|
|
1909
|
+
this.bgLayer.batchDraw();
|
|
1910
|
+
};
|
|
1911
|
+
img.src = obj.href;
|
|
1912
|
+
}
|
|
1714
1913
|
renderTable(obj) {
|
|
1715
1914
|
if (obj.shape === "round") {
|
|
1716
1915
|
this.bgLayer.add(
|
|
@@ -2032,11 +2231,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2032
2231
|
}
|
|
2033
2232
|
/** Recompute a section's availability-tinted fill + "N LEFT" (cheap; on status change). */
|
|
2034
2233
|
refreshSectionFill(sec) {
|
|
2035
|
-
|
|
2036
|
-
sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
|
|
2234
|
+
sec.blockPoly.fill(this.sectionBlockFill(sec));
|
|
2037
2235
|
sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
|
|
2038
2236
|
sec.subLabel.offsetX(sec.subLabel.width() / 2);
|
|
2039
2237
|
}
|
|
2238
|
+
/** True when a section/zone is currently in the `closed` event-state. */
|
|
2239
|
+
isSectionClosed(sec) {
|
|
2240
|
+
return this.closedSections.has(sec.id) || sec.zone != null && this.closedSections.has(sec.zone);
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* The block-fill colour for a section: flat desaturated grey when `closed`,
|
|
2244
|
+
* else the availability-darkened category mix; then desaturated toward neutral
|
|
2245
|
+
* when another section holds focus (AXS dim treatment).
|
|
2246
|
+
*/
|
|
2247
|
+
sectionBlockFill(sec) {
|
|
2248
|
+
let fill;
|
|
2249
|
+
if (this.isSectionClosed(sec)) {
|
|
2250
|
+
fill = CLOSED_SECTION_FILL;
|
|
2251
|
+
} else {
|
|
2252
|
+
const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
|
|
2253
|
+
fill = darken(sec.baseFill, sold * SOLD_DARKEN);
|
|
2254
|
+
}
|
|
2255
|
+
if (this.focusedSectionId && sec.id !== this.focusedSectionId && sec.zone !== this.focusedSectionId) {
|
|
2256
|
+
fill = lerpColor(fill, FOCUS_NEUTRAL, FOCUS_DESATURATE);
|
|
2257
|
+
}
|
|
2258
|
+
return fill;
|
|
2259
|
+
}
|
|
2040
2260
|
/**
|
|
2041
2261
|
* Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
|
|
2042
2262
|
* shown at the farthest zoom in place of per-section detail. Skipped entirely
|
|
@@ -2128,12 +2348,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2128
2348
|
const sx = this.stage.scaleX();
|
|
2129
2349
|
const rescale = this.lodScale === 0 || Math.abs(scale - this.lodScale) / (this.lodScale || 1) > 0.02;
|
|
2130
2350
|
if (rescale) this.lodScale = scale;
|
|
2351
|
+
const focus = this.focusedSectionId;
|
|
2131
2352
|
for (const sec of this.sections) {
|
|
2132
|
-
sec.
|
|
2133
|
-
|
|
2353
|
+
const dim = focus && sec.id !== focus && sec.zone !== focus ? FOCUS_DIM_OPACITY : 1;
|
|
2354
|
+
sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT * dim);
|
|
2355
|
+
for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT) * dim);
|
|
2134
2356
|
sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
|
|
2135
|
-
sec.nameLabel.opacity(1 - zoneT);
|
|
2136
|
-
sec.subLabel.opacity(blockT * (1 - zoneT));
|
|
2357
|
+
sec.nameLabel.opacity((1 - zoneT) * dim);
|
|
2358
|
+
sec.subLabel.opacity(blockT * (1 - zoneT) * dim);
|
|
2137
2359
|
if (rescale) {
|
|
2138
2360
|
this.sizeLabel(sec.nameLabel, SECTION_LABEL_PX / sx, sec.centroid.y - SECTION_SUB_PX / sx);
|
|
2139
2361
|
this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
|
|
@@ -2149,8 +2371,50 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2149
2371
|
if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, cy + ZONE_LABEL_PX / sx);
|
|
2150
2372
|
}
|
|
2151
2373
|
}
|
|
2374
|
+
this.decollideRungLabels(sx);
|
|
2152
2375
|
this.bgLayer.batchDraw();
|
|
2153
2376
|
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Greedy label de-collision for the zone/section rungs (same approach as the
|
|
2379
|
+
* designer's cullRowLabels): price/"N LEFT" sublabels are lowest priority and
|
|
2380
|
+
* drop first; name labels keep top-to-bottom, left-to-right; anything whose
|
|
2381
|
+
* on-screen box (+4px gap) overlaps an already-kept box hides. Recomputed on
|
|
2382
|
+
* every LOD pass so hidden labels reappear as zoom spreads them apart.
|
|
2383
|
+
* Culling multiplies the opacity applySectionLod just assigned (never raises).
|
|
2384
|
+
*/
|
|
2385
|
+
decollideRungLabels(sx) {
|
|
2386
|
+
const GAP = 4;
|
|
2387
|
+
const cands = [];
|
|
2388
|
+
const boxOf = (t2) => {
|
|
2389
|
+
const p = this.worldToScreen({ x: t2.x(), y: t2.y() });
|
|
2390
|
+
const w = t2.width() * sx;
|
|
2391
|
+
const h = t2.height() * sx;
|
|
2392
|
+
return { x: p.x - w / 2, y: p.y - h / 2, w, h };
|
|
2393
|
+
};
|
|
2394
|
+
for (const zone of this.zones) {
|
|
2395
|
+
if (zone.label.opacity() > 0.05) cands.push({ node: zone.label, tier: 0, box: boxOf(zone.label) });
|
|
2396
|
+
if (zone.sub && zone.sub.opacity() > 0.05) cands.push({ node: zone.sub, tier: 2, owner: zone.label, box: boxOf(zone.sub) });
|
|
2397
|
+
}
|
|
2398
|
+
for (const sec of this.sections) {
|
|
2399
|
+
if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, box: boxOf(sec.nameLabel) });
|
|
2400
|
+
if (sec.subLabel.opacity() > 0.05) cands.push({ node: sec.subLabel, tier: 3, owner: sec.nameLabel, box: boxOf(sec.subLabel) });
|
|
2401
|
+
}
|
|
2402
|
+
if (cands.length < 2) return;
|
|
2403
|
+
cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
|
|
2404
|
+
const kept = [];
|
|
2405
|
+
const culled = /* @__PURE__ */ new Set();
|
|
2406
|
+
const collides = (b) => kept.some(
|
|
2407
|
+
(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
|
|
2408
|
+
);
|
|
2409
|
+
for (const c of cands) {
|
|
2410
|
+
if (c.owner && culled.has(c.owner) || collides(c.box)) {
|
|
2411
|
+
c.node.opacity(0);
|
|
2412
|
+
culled.add(c.node);
|
|
2413
|
+
} else {
|
|
2414
|
+
kept.push(c.box);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2154
2418
|
/** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
|
|
2155
2419
|
sizeLabel(t2, fontSize, y) {
|
|
2156
2420
|
t2.fontSize(Math.max(1, fontSize));
|
|
@@ -2197,6 +2461,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2197
2461
|
}
|
|
2198
2462
|
// ---- selection ------------------------------------------------------------
|
|
2199
2463
|
isSelectable(id) {
|
|
2464
|
+
if (this.seatInClosedSection(id)) return false;
|
|
2200
2465
|
const statuses = this.opts.selectableStatuses ?? ["free"];
|
|
2201
2466
|
return statuses.includes(this.statusById.get(id) ?? "free");
|
|
2202
2467
|
}
|
|
@@ -2280,6 +2545,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2280
2545
|
return;
|
|
2281
2546
|
}
|
|
2282
2547
|
}
|
|
2548
|
+
if (this.effScale() < LABEL_SCALE && this.sections.length) {
|
|
2549
|
+
const sec = this.seatSection.get(id);
|
|
2550
|
+
if (sec) {
|
|
2551
|
+
if (this.opts.onSectionTap) this.opts.onSectionTap(sec.id);
|
|
2552
|
+
else this.focusSection(sec.id);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2283
2556
|
this.toggleSeat(id);
|
|
2284
2557
|
});
|
|
2285
2558
|
this.seatLayer.on("mouseover", (e) => {
|
|
@@ -2639,6 +2912,9 @@ var PickerController = class {
|
|
|
2639
2912
|
this._doc = null;
|
|
2640
2913
|
/** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
|
|
2641
2914
|
this.hidden = /* @__PURE__ */ new Set();
|
|
2915
|
+
/** Section/zone ids in the `closed` event-state (Phase 2) — seats stay, greyed
|
|
2916
|
+
* + not pickable. Kept separate from `hidden` (which strips them). */
|
|
2917
|
+
this.closedSections = /* @__PURE__ */ new Set();
|
|
2642
2918
|
/** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
|
|
2643
2919
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
2644
2920
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
@@ -2679,6 +2955,19 @@ var PickerController = class {
|
|
|
2679
2955
|
this.renderer?.setChart(this.visibleDoc());
|
|
2680
2956
|
return true;
|
|
2681
2957
|
}
|
|
2958
|
+
/** Adopt a new closed-section set; restyle (grey + non-pickable) if it differs.
|
|
2959
|
+
* Cheap restyle — no chart rebuild — so mid-sale open/close repaints live. */
|
|
2960
|
+
syncClosed(ids) {
|
|
2961
|
+
const next = (ids ?? []).filter((x) => typeof x === "string");
|
|
2962
|
+
if (next.length === this.closedSections.size && next.every((id) => this.closedSections.has(id))) return false;
|
|
2963
|
+
this.closedSections = new Set(next);
|
|
2964
|
+
this.renderer?.setClosedSections?.(next);
|
|
2965
|
+
return true;
|
|
2966
|
+
}
|
|
2967
|
+
/** Whether a section is currently in the `closed` state (card must not open). */
|
|
2968
|
+
isSectionClosed(id) {
|
|
2969
|
+
return this.closedSections.has(id);
|
|
2970
|
+
}
|
|
2682
2971
|
currentHold() {
|
|
2683
2972
|
return this.hold_;
|
|
2684
2973
|
}
|
|
@@ -2746,9 +3035,11 @@ var PickerController = class {
|
|
|
2746
3035
|
}
|
|
2747
3036
|
this.renderer = renderer;
|
|
2748
3037
|
let seats = null;
|
|
3038
|
+
let closedIds = [];
|
|
2749
3039
|
try {
|
|
2750
3040
|
const objs = await this.api.objects(this.key);
|
|
2751
3041
|
this.hidden = new Set(objs.hidden ?? []);
|
|
3042
|
+
closedIds = (objs.closed ?? []).filter((x) => typeof x === "string");
|
|
2752
3043
|
seats = objs.seats;
|
|
2753
3044
|
} catch {
|
|
2754
3045
|
}
|
|
@@ -2759,6 +3050,8 @@ var PickerController = class {
|
|
|
2759
3050
|
}
|
|
2760
3051
|
renderer.setChart(this.visibleDoc());
|
|
2761
3052
|
if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
|
|
3053
|
+
this.closedSections = new Set(closedIds);
|
|
3054
|
+
if (closedIds.length) renderer.setClosedSections?.(closedIds);
|
|
2762
3055
|
if (seats) this.applySeatsMap(seats);
|
|
2763
3056
|
this.connect();
|
|
2764
3057
|
return {
|
|
@@ -2816,6 +3109,26 @@ var PickerController = class {
|
|
|
2816
3109
|
throw err;
|
|
2817
3110
|
}
|
|
2818
3111
|
}
|
|
3112
|
+
/**
|
|
3113
|
+
* P4 "need more time?": extend the OPEN hold's server-side expiry and re-arm
|
|
3114
|
+
* the client expiry timer to match (via setHold), so the controller doesn't
|
|
3115
|
+
* fire a false expiry and start polling. Returns the new hold, or null if
|
|
3116
|
+
* there's no open hold or the transport can't extend / the server refused
|
|
3117
|
+
* (hold gone, expired, or at its renewal cap). Never throws for the refusal
|
|
3118
|
+
* case — the caller decides the copy.
|
|
3119
|
+
*/
|
|
3120
|
+
async extendHold(ttlMs) {
|
|
3121
|
+
const current = this.hold_;
|
|
3122
|
+
if (!current || !this.api.extend) return null;
|
|
3123
|
+
try {
|
|
3124
|
+
const result = await this.api.extend(this.key, current.holdId, ttlMs);
|
|
3125
|
+
if (this.hold_?.holdId !== current.holdId) return this.hold_;
|
|
3126
|
+
this.setHold({ holdId: current.holdId, labels: current.labels, expiresAt: result.expiresAt, items: current.items });
|
|
3127
|
+
return this.hold_;
|
|
3128
|
+
} catch {
|
|
3129
|
+
return null;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
2819
3132
|
/** Public GA inventory derived from the live synthetic-unit status stream. */
|
|
2820
3133
|
/**
|
|
2821
3134
|
* Live seats-left per category key (status 'free' right now). Recompute on
|
|
@@ -2823,13 +3136,23 @@ var PickerController = class {
|
|
|
2823
3136
|
*/
|
|
2824
3137
|
categoryAvailability() {
|
|
2825
3138
|
const out = {};
|
|
3139
|
+
const closedMembers = this.closedMemberIds();
|
|
2826
3140
|
for (const [id, s] of this.seatById) {
|
|
3141
|
+
if (closedMembers.has(id)) continue;
|
|
2827
3142
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
2828
3143
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
2829
3144
|
}
|
|
2830
3145
|
}
|
|
2831
3146
|
return out;
|
|
2832
3147
|
}
|
|
3148
|
+
/** Seat ids belonging to a currently-closed section (excluded from counts). */
|
|
3149
|
+
closedMemberIds() {
|
|
3150
|
+
const out = /* @__PURE__ */ new Set();
|
|
3151
|
+
const r = this.renderer;
|
|
3152
|
+
if (!r || !this.closedSections.size) return out;
|
|
3153
|
+
for (const id of this.closedSections) for (const sid of r.sectionMembers?.(id) ?? []) out.add(sid);
|
|
3154
|
+
return out;
|
|
3155
|
+
}
|
|
2833
3156
|
getGAAreas() {
|
|
2834
3157
|
const doc = this.visibleDoc();
|
|
2835
3158
|
if (!doc) return [];
|
|
@@ -3074,20 +3397,39 @@ var PickerController = class {
|
|
|
3074
3397
|
}
|
|
3075
3398
|
this.renderer?.setRung?.(rung);
|
|
3076
3399
|
}
|
|
3400
|
+
/** Price-band filter (F4): dim free seats whose category is outside `keys`
|
|
3401
|
+
* (null clears). The widget resolves which categories fall in the band. */
|
|
3402
|
+
setCategoryFilter(keys) {
|
|
3403
|
+
this.renderer?.setCategoryFilter?.(keys);
|
|
3404
|
+
}
|
|
3405
|
+
/** World-space rect currently visible + full chart bounds (F3 minimap frame). */
|
|
3406
|
+
getViewport() {
|
|
3407
|
+
const r = this.renderer;
|
|
3408
|
+
if (!r?.getVisibleWorldRect || !r.getWorldBounds) return null;
|
|
3409
|
+
return { visible: r.getVisibleWorldRect(), bounds: r.getWorldBounds() };
|
|
3410
|
+
}
|
|
3077
3411
|
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
3078
3412
|
focusSection(id) {
|
|
3079
3413
|
this.handleSectionTap(id);
|
|
3080
3414
|
}
|
|
3081
|
-
/** Zoom back out to the whole chart and clear the section-summary card. */
|
|
3415
|
+
/** Zoom back out to the whole chart and clear the section-summary card + focus. */
|
|
3082
3416
|
overview() {
|
|
3417
|
+
this.renderer?.clearSectionFocus?.();
|
|
3083
3418
|
this.renderer?.zoomToFit();
|
|
3084
3419
|
this.opts.onSectionFocus?.(null);
|
|
3085
3420
|
}
|
|
3086
|
-
/** Glide the camera into a tapped section and emit its computed summary.
|
|
3421
|
+
/** Glide the camera into a tapped section and emit its computed summary. Uses
|
|
3422
|
+
* the AXS focus treatment (dim + backdrop) when the engine supports it; a
|
|
3423
|
+
* closed section is framed but never opens a buyer card. */
|
|
3087
3424
|
handleSectionTap(id) {
|
|
3088
3425
|
const r = this.renderer;
|
|
3089
3426
|
if (!r) return;
|
|
3090
|
-
r.
|
|
3427
|
+
if (r.focusSection) r.focusSection(id);
|
|
3428
|
+
else r.focusRegion?.(id);
|
|
3429
|
+
if (this.isSectionClosed(id)) {
|
|
3430
|
+
this.opts.onSectionFocus?.(null);
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3091
3433
|
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
3092
3434
|
}
|
|
3093
3435
|
/**
|
|
@@ -3361,6 +3703,9 @@ var PickerController = class {
|
|
|
3361
3703
|
void this.resnapshot();
|
|
3362
3704
|
this.opts.onStatusChange?.();
|
|
3363
3705
|
}
|
|
3706
|
+
if (Array.isArray(m.closed) && this.syncClosed(m.closed)) {
|
|
3707
|
+
this.opts.onStatusChange?.();
|
|
3708
|
+
}
|
|
3364
3709
|
if (m.type === "hidden") return;
|
|
3365
3710
|
if (m.seats && typeof m.seats === "object") {
|
|
3366
3711
|
this.applySeatsMap(m.seats);
|