@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.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 {
|
|
@@ -2843,13 +3136,23 @@ var PickerController = class {
|
|
|
2843
3136
|
*/
|
|
2844
3137
|
categoryAvailability() {
|
|
2845
3138
|
const out = {};
|
|
3139
|
+
const closedMembers = this.closedMemberIds();
|
|
2846
3140
|
for (const [id, s] of this.seatById) {
|
|
3141
|
+
if (closedMembers.has(id)) continue;
|
|
2847
3142
|
if ((this.getStatus(id) ?? "free") === "free") {
|
|
2848
3143
|
out[s.categoryKey] = (out[s.categoryKey] ?? 0) + 1;
|
|
2849
3144
|
}
|
|
2850
3145
|
}
|
|
2851
3146
|
return out;
|
|
2852
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
|
+
}
|
|
2853
3156
|
getGAAreas() {
|
|
2854
3157
|
const doc = this.visibleDoc();
|
|
2855
3158
|
if (!doc) return [];
|
|
@@ -3094,20 +3397,39 @@ var PickerController = class {
|
|
|
3094
3397
|
}
|
|
3095
3398
|
this.renderer?.setRung?.(rung);
|
|
3096
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
|
+
}
|
|
3097
3411
|
/** Glide in on a section and surface its summary (same path as a section tap). */
|
|
3098
3412
|
focusSection(id) {
|
|
3099
3413
|
this.handleSectionTap(id);
|
|
3100
3414
|
}
|
|
3101
|
-
/** 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. */
|
|
3102
3416
|
overview() {
|
|
3417
|
+
this.renderer?.clearSectionFocus?.();
|
|
3103
3418
|
this.renderer?.zoomToFit();
|
|
3104
3419
|
this.opts.onSectionFocus?.(null);
|
|
3105
3420
|
}
|
|
3106
|
-
/** 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. */
|
|
3107
3424
|
handleSectionTap(id) {
|
|
3108
3425
|
const r = this.renderer;
|
|
3109
3426
|
if (!r) return;
|
|
3110
|
-
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
|
+
}
|
|
3111
3433
|
this.opts.onSectionFocus?.(this.sectionSummary(id));
|
|
3112
3434
|
}
|
|
3113
3435
|
/**
|
|
@@ -3381,6 +3703,9 @@ var PickerController = class {
|
|
|
3381
3703
|
void this.resnapshot();
|
|
3382
3704
|
this.opts.onStatusChange?.();
|
|
3383
3705
|
}
|
|
3706
|
+
if (Array.isArray(m.closed) && this.syncClosed(m.closed)) {
|
|
3707
|
+
this.opts.onStatusChange?.();
|
|
3708
|
+
}
|
|
3384
3709
|
if (m.type === "hidden") return;
|
|
3385
3710
|
if (m.seats && typeof m.seats === "object") {
|
|
3386
3711
|
this.applySeatsMap(m.seats);
|