@seatlayer/core 0.30.1 → 0.31.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/view3d/index.cjs +382 -119
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +29 -1
- package/dist/view3d/index.d.ts +29 -1
- package/dist/view3d/index.js +382 -119
- package/dist/view3d/index.js.map +1 -1
- package/package.json +1 -1
package/dist/view3d/index.cjs
CHANGED
|
@@ -548,12 +548,42 @@ function isDegenerate(p0, p1, p2) {
|
|
|
548
548
|
const area = Math.sqrt(Math.max(0, s * (s - e0) * (s - e1) * (s - e2)));
|
|
549
549
|
return 2 * area / longest < MIN_TRI_ALTITUDE_M;
|
|
550
550
|
}
|
|
551
|
+
var F32Buffer = class {
|
|
552
|
+
constructor(initial = 1 << 14) {
|
|
553
|
+
this.len = 0;
|
|
554
|
+
this.buf = new Float32Array(initial);
|
|
555
|
+
}
|
|
556
|
+
push3(a, b, c) {
|
|
557
|
+
if (this.len + 3 > this.buf.length) this.grow(this.len + 3);
|
|
558
|
+
this.buf[this.len++] = a;
|
|
559
|
+
this.buf[this.len++] = b;
|
|
560
|
+
this.buf[this.len++] = c;
|
|
561
|
+
}
|
|
562
|
+
push1(a) {
|
|
563
|
+
if (this.len + 1 > this.buf.length) this.grow(this.len + 1);
|
|
564
|
+
this.buf[this.len++] = a;
|
|
565
|
+
}
|
|
566
|
+
grow(need) {
|
|
567
|
+
let cap = this.buf.length * 2;
|
|
568
|
+
while (cap < need) cap *= 2;
|
|
569
|
+
const next = new Float32Array(cap);
|
|
570
|
+
next.set(this.buf.subarray(0, this.len));
|
|
571
|
+
this.buf = next;
|
|
572
|
+
}
|
|
573
|
+
get length() {
|
|
574
|
+
return this.len;
|
|
575
|
+
}
|
|
576
|
+
/** A copy trimmed to the used length. */
|
|
577
|
+
toArray() {
|
|
578
|
+
return this.buf.slice(0, this.len);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
551
581
|
var MeshBuilder = class {
|
|
552
582
|
constructor() {
|
|
553
|
-
this.pos =
|
|
554
|
-
this.nor =
|
|
555
|
-
this.col =
|
|
556
|
-
this.flr =
|
|
583
|
+
this.pos = new F32Buffer();
|
|
584
|
+
this.nor = new F32Buffer();
|
|
585
|
+
this.col = new F32Buffer();
|
|
586
|
+
this.flr = new F32Buffer();
|
|
557
587
|
/** Floor index stamped onto every triangle emitted from now on. */
|
|
558
588
|
this.currentFloor = 0;
|
|
559
589
|
}
|
|
@@ -564,28 +594,44 @@ var MeshBuilder = class {
|
|
|
564
594
|
/** One triangle with a shared (flat) normal and per-vertex colours. */
|
|
565
595
|
tri(p0, p1, p2, n, c0, c1 = c0, c2 = c0) {
|
|
566
596
|
if (isDegenerate(p0, p1, p2)) return;
|
|
567
|
-
this.pos.
|
|
568
|
-
this.
|
|
569
|
-
this.
|
|
570
|
-
this.
|
|
597
|
+
this.pos.push3(p0[0], p0[1], p0[2]);
|
|
598
|
+
this.pos.push3(p1[0], p1[1], p1[2]);
|
|
599
|
+
this.pos.push3(p2[0], p2[1], p2[2]);
|
|
600
|
+
this.nor.push3(n[0], n[1], n[2]);
|
|
601
|
+
this.nor.push3(n[0], n[1], n[2]);
|
|
602
|
+
this.nor.push3(n[0], n[1], n[2]);
|
|
603
|
+
this.col.push3(c0[0], c0[1], c0[2]);
|
|
604
|
+
this.col.push3(c1[0], c1[1], c1[2]);
|
|
605
|
+
this.col.push3(c2[0], c2[1], c2[2]);
|
|
606
|
+
this.flr.push1(this.currentFloor);
|
|
607
|
+
this.flr.push1(this.currentFloor);
|
|
608
|
+
this.flr.push1(this.currentFloor);
|
|
571
609
|
}
|
|
572
610
|
/** One triangle with independent per-vertex normals (smooth shading). */
|
|
573
611
|
triN(p0, p1, p2, n0, n1, n2, c0, c1 = c0, c2 = c0) {
|
|
574
612
|
if (isDegenerate(p0, p1, p2)) return;
|
|
575
|
-
this.pos.
|
|
576
|
-
this.
|
|
577
|
-
this.
|
|
578
|
-
this.
|
|
613
|
+
this.pos.push3(p0[0], p0[1], p0[2]);
|
|
614
|
+
this.pos.push3(p1[0], p1[1], p1[2]);
|
|
615
|
+
this.pos.push3(p2[0], p2[1], p2[2]);
|
|
616
|
+
this.nor.push3(n0[0], n0[1], n0[2]);
|
|
617
|
+
this.nor.push3(n1[0], n1[1], n1[2]);
|
|
618
|
+
this.nor.push3(n2[0], n2[1], n2[2]);
|
|
619
|
+
this.col.push3(c0[0], c0[1], c0[2]);
|
|
620
|
+
this.col.push3(c1[0], c1[1], c1[2]);
|
|
621
|
+
this.col.push3(c2[0], c2[1], c2[2]);
|
|
622
|
+
this.flr.push1(this.currentFloor);
|
|
623
|
+
this.flr.push1(this.currentFloor);
|
|
624
|
+
this.flr.push1(this.currentFloor);
|
|
579
625
|
}
|
|
580
626
|
get vertexCount() {
|
|
581
627
|
return this.pos.length / 3;
|
|
582
628
|
}
|
|
583
629
|
build() {
|
|
584
630
|
return {
|
|
585
|
-
position:
|
|
586
|
-
normal:
|
|
587
|
-
color:
|
|
588
|
-
floor:
|
|
631
|
+
position: this.pos.toArray(),
|
|
632
|
+
normal: this.nor.toArray(),
|
|
633
|
+
color: this.col.toArray(),
|
|
634
|
+
floor: this.flr.toArray(),
|
|
589
635
|
count: this.pos.length / 3
|
|
590
636
|
};
|
|
591
637
|
}
|
|
@@ -1093,16 +1139,23 @@ function themeSeatColorLUT(theme, order) {
|
|
|
1093
1139
|
var ZONE_MIN_DISTANCE = 1.15;
|
|
1094
1140
|
var SECTION_MAX_DISTANCE = 2.2;
|
|
1095
1141
|
var NEAR_MAX_DISTANCE = 0.85;
|
|
1142
|
+
var ROW_MAX_DISTANCE = 0.5;
|
|
1143
|
+
var SEAT_MAX_DISTANCE = 0.26;
|
|
1096
1144
|
function visibleLabelKinds(distance, venueRadius) {
|
|
1097
1145
|
const r = Math.max(1e-6, venueRadius);
|
|
1098
1146
|
const d = distance / r;
|
|
1099
1147
|
const out = /* @__PURE__ */ new Set();
|
|
1100
1148
|
if (d >= ZONE_MIN_DISTANCE) out.add("zone");
|
|
1101
|
-
if (d <= SECTION_MAX_DISTANCE)
|
|
1149
|
+
if (d <= SECTION_MAX_DISTANCE) {
|
|
1150
|
+
out.add("section");
|
|
1151
|
+
out.add("ga");
|
|
1152
|
+
}
|
|
1102
1153
|
if (d <= NEAR_MAX_DISTANCE) {
|
|
1103
1154
|
out.add("annotation");
|
|
1104
1155
|
out.add("booth");
|
|
1105
1156
|
}
|
|
1157
|
+
if (d <= ROW_MAX_DISTANCE) out.add("row");
|
|
1158
|
+
if (d <= SEAT_MAX_DISTANCE) out.add("seat");
|
|
1106
1159
|
return out;
|
|
1107
1160
|
}
|
|
1108
1161
|
function projectToScreen(viewProjection, p, width, height) {
|
|
@@ -1737,23 +1790,27 @@ function buildVenueSurfaces(units, seats) {
|
|
|
1737
1790
|
const inferredFlat = geo.rake <= 0.01 && geo.height <= bottomY + 1e-3;
|
|
1738
1791
|
const kind = a.section.surfaceKind;
|
|
1739
1792
|
const flat = kind === "flat" ? true : kind === "rakedRows" ? geo.rake > 0.01 ? false : inferredFlat : inferredFlat;
|
|
1740
|
-
const
|
|
1793
|
+
const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
|
|
1794
|
+
const needsRake = structure.rows.length < 2;
|
|
1795
|
+
const rake = needsRake ? buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal) : null;
|
|
1741
1796
|
let frontU = Infinity;
|
|
1742
|
-
if (
|
|
1743
|
-
|
|
1744
|
-
for (const
|
|
1797
|
+
if (rake) {
|
|
1798
|
+
if (a.hasSeats) {
|
|
1799
|
+
for (const pts of a.rows.values()) {
|
|
1800
|
+
for (const p of pts) {
|
|
1801
|
+
const d = rake.depthAt(p.x, p.y);
|
|
1802
|
+
if (d < frontU) frontU = d;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
if (!a.hasSeats || !Number.isFinite(frontU)) {
|
|
1807
|
+
frontU = Infinity;
|
|
1808
|
+
for (const p of a.section.outline) {
|
|
1745
1809
|
const d = rake.depthAt(p.x, p.y);
|
|
1746
1810
|
if (d < frontU) frontU = d;
|
|
1747
1811
|
}
|
|
1748
1812
|
}
|
|
1749
1813
|
}
|
|
1750
|
-
if (!a.hasSeats || !Number.isFinite(frontU)) {
|
|
1751
|
-
frontU = Infinity;
|
|
1752
|
-
for (const p of a.section.outline) {
|
|
1753
|
-
const d = rake.depthAt(p.x, p.y);
|
|
1754
|
-
if (d < frontU) frontU = d;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
1814
|
const flatTop = bottomY + FLAT_SLAB_TOP_M;
|
|
1758
1815
|
const baseFloor = bottomY + FLAT_SLAB_TOP_M;
|
|
1759
1816
|
const levelFor = (depthU) => {
|
|
@@ -1766,7 +1823,6 @@ function buildVenueSurfaces(units, seats) {
|
|
|
1766
1823
|
const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
|
|
1767
1824
|
return Math.max(baseFloor, geo.height + rise);
|
|
1768
1825
|
};
|
|
1769
|
-
const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
|
|
1770
1826
|
const rowLevels = flat ? [] : structure.rows.map((r) => ({
|
|
1771
1827
|
pts: r.pts,
|
|
1772
1828
|
y: levelForBlockDepth(r.blockDepth),
|
|
@@ -1802,9 +1858,10 @@ function buildVenueSurfaces(units, seats) {
|
|
|
1802
1858
|
}
|
|
1803
1859
|
}
|
|
1804
1860
|
return bestY;
|
|
1805
|
-
} : (x, y) => levelFor(rake.depthAt(x, y));
|
|
1861
|
+
} : (x, y) => rake ? levelFor(rake.depthAt(x, y)) : flatTop;
|
|
1806
1862
|
const UP = [0, 1, 0];
|
|
1807
1863
|
const normalAt = rowLevels.length >= 2 || flat ? () => UP : (x, y) => {
|
|
1864
|
+
if (!rake) return UP;
|
|
1808
1865
|
const d = rake.depthAt(x, y);
|
|
1809
1866
|
if (d <= frontU) return UP;
|
|
1810
1867
|
const depthM = (d - frontU) * METRES_PER_CHART_UNIT;
|
|
@@ -1927,7 +1984,23 @@ function distToPolyline(pts, x, y) {
|
|
|
1927
1984
|
}
|
|
1928
1985
|
return best;
|
|
1929
1986
|
}
|
|
1930
|
-
function
|
|
1987
|
+
function rowNeighbourhoods(rows) {
|
|
1988
|
+
const bounds = rows.map((r) => {
|
|
1989
|
+
let cx = 0, cy = 0;
|
|
1990
|
+
for (const p of r.pts) {
|
|
1991
|
+
cx += p.x;
|
|
1992
|
+
cy += p.y;
|
|
1993
|
+
}
|
|
1994
|
+
const n = r.pts.length || 1;
|
|
1995
|
+
cx /= n;
|
|
1996
|
+
cy /= n;
|
|
1997
|
+
let rad = 0;
|
|
1998
|
+
for (const p of r.pts) {
|
|
1999
|
+
const d = Math.hypot(p.x - cx, p.y - cy);
|
|
2000
|
+
if (d > rad) rad = d;
|
|
2001
|
+
}
|
|
2002
|
+
return { cx, cy, rad };
|
|
2003
|
+
});
|
|
1931
2004
|
const probesOf2 = (pts) => {
|
|
1932
2005
|
const n = pts.length;
|
|
1933
2006
|
if (n <= 2) return [...pts];
|
|
@@ -1942,6 +2015,9 @@ function neighbourhoods(rows) {
|
|
|
1942
2015
|
let nearest = Infinity;
|
|
1943
2016
|
for (let j = 0; j < rows.length; j++) {
|
|
1944
2017
|
if (j === i || Math.abs(rows[j].y - rows[i].y) < 1e-3) continue;
|
|
2018
|
+
const b = bounds[j];
|
|
2019
|
+
const lower = Math.hypot(c.x - b.cx, c.y - b.cy) - b.rad;
|
|
2020
|
+
if (lower >= nearest && lower >= bestFrontD) continue;
|
|
1945
2021
|
const d = distToPolyline(rows[j].pts, c.x, c.y);
|
|
1946
2022
|
if (d < nearest) nearest = d;
|
|
1947
2023
|
if (rows[j].depth < rows[i].depth && d < bestFrontD) {
|
|
@@ -1986,65 +2062,8 @@ function ribbonOf(rows, i, nbrs, focal) {
|
|
|
1986
2062
|
back: Math.max(nbrs[i].pitch * BACK_REACH, MIN_REACH_U)
|
|
1987
2063
|
};
|
|
1988
2064
|
}
|
|
1989
|
-
function deckFootprints(rows, focal) {
|
|
1990
|
-
if (rows.length < 2) return [];
|
|
1991
|
-
const nbrs = neighbourhoods(rows);
|
|
1992
|
-
const rings = [];
|
|
1993
|
-
const ribbons = [];
|
|
1994
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1995
|
-
const r = ribbonOf(rows, i, nbrs, focal);
|
|
1996
|
-
ribbons.push(r);
|
|
1997
|
-
if (!r) continue;
|
|
1998
|
-
const f = [];
|
|
1999
|
-
const b = [];
|
|
2000
|
-
const rf = r.front + FOOTPRINT_MARGIN_U;
|
|
2001
|
-
const rb = r.back + FOOTPRINT_MARGIN_U;
|
|
2002
|
-
for (let k = 0; k < r.pts.length; k++) {
|
|
2003
|
-
const p = r.pts[k], n = r.nrm[k];
|
|
2004
|
-
f.push([p.x - n[0] * rf, p.y - n[1] * rf]);
|
|
2005
|
-
b.push([p.x + n[0] * rb, p.y + n[1] * rb]);
|
|
2006
|
-
}
|
|
2007
|
-
const ring = [...f, ...b.reverse()];
|
|
2008
|
-
if (ring.length < 3) continue;
|
|
2009
|
-
ring.push(ring[0]);
|
|
2010
|
-
rings.push([ring]);
|
|
2011
|
-
}
|
|
2012
|
-
if (!rings.length) return [];
|
|
2013
|
-
let merged;
|
|
2014
|
-
try {
|
|
2015
|
-
merged = import_polygon_clipping2.default.union(rings[0], ...rings.slice(1));
|
|
2016
|
-
} catch {
|
|
2017
|
-
return [];
|
|
2018
|
-
}
|
|
2019
|
-
const out = [];
|
|
2020
|
-
for (const poly of merged) {
|
|
2021
|
-
if (!poly.length || poly[0].length < 4) continue;
|
|
2022
|
-
const toPts = (ring) => {
|
|
2023
|
-
const pts = ring.map(([x, y]) => ({ x, y }));
|
|
2024
|
-
const first = pts[0], last = pts[pts.length - 1];
|
|
2025
|
-
if (pts.length > 1 && Math.abs(first.x - last.x) < 1e-9 && Math.abs(first.y - last.y) < 1e-9) pts.pop();
|
|
2026
|
-
return pts;
|
|
2027
|
-
};
|
|
2028
|
-
const outline = toPts(poly[0]);
|
|
2029
|
-
if (outline.length < 3) continue;
|
|
2030
|
-
let topY = Infinity;
|
|
2031
|
-
for (let i = 0; i < rows.length; i++) {
|
|
2032
|
-
const r = ribbons[i];
|
|
2033
|
-
if (!r) continue;
|
|
2034
|
-
const mid = r.pts[Math.floor(r.pts.length / 2)];
|
|
2035
|
-
if (pointInRing(outline, mid.x, mid.y) && rows[i].y < topY) topY = rows[i].y;
|
|
2036
|
-
}
|
|
2037
|
-
if (!Number.isFinite(topY)) continue;
|
|
2038
|
-
out.push({
|
|
2039
|
-
outline: simplifyRing(outline, FOOTPRINT_TOLERANCE_U),
|
|
2040
|
-
holes: poly.slice(1).map(toPts).map((h) => simplifyRing(h, FOOTPRINT_TOLERANCE_U)).filter((h) => h.length >= 3),
|
|
2041
|
-
topY
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
return out;
|
|
2045
|
-
}
|
|
2046
|
-
var FOOTPRINT_TOLERANCE_U = 0.3;
|
|
2047
2065
|
var FOOTPRINT_MARGIN_U = 1.5;
|
|
2066
|
+
var FOOTPRINT_TOLERANCE_U = 0.3;
|
|
2048
2067
|
function simplifyRun(pts, tol) {
|
|
2049
2068
|
if (pts.length < 3) return pts;
|
|
2050
2069
|
const a = pts[0], b = pts[pts.length - 1];
|
|
@@ -2070,6 +2089,102 @@ function simplifyRing(ring, tol) {
|
|
|
2070
2089
|
out.pop();
|
|
2071
2090
|
return out.length >= 3 ? out : ring;
|
|
2072
2091
|
}
|
|
2092
|
+
function deckFootprints(rows, focal, shared) {
|
|
2093
|
+
if (rows.length < 2) return [];
|
|
2094
|
+
const nbrs = shared ?? rowNeighbourhoods(rows);
|
|
2095
|
+
const byBlock = /* @__PURE__ */ new Map();
|
|
2096
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2097
|
+
const a = byBlock.get(rows[i].blockId);
|
|
2098
|
+
if (a) a.push(i);
|
|
2099
|
+
else byBlock.set(rows[i].blockId, [i]);
|
|
2100
|
+
}
|
|
2101
|
+
const out = [];
|
|
2102
|
+
for (const [, indices] of byBlock) {
|
|
2103
|
+
indices.sort((a, b) => rows[a].depth - rows[b].depth);
|
|
2104
|
+
const ribbons = indices.map((i) => ribbonOf(rows, i, nbrs, focal));
|
|
2105
|
+
const usable = ribbons.filter((r) => r !== null);
|
|
2106
|
+
if (!usable.length) continue;
|
|
2107
|
+
const frontOf = (r, k) => ({
|
|
2108
|
+
x: r.pts[k].x - r.nrm[k][0] * (r.front + FOOTPRINT_MARGIN_U),
|
|
2109
|
+
y: r.pts[k].y - r.nrm[k][1] * (r.front + FOOTPRINT_MARGIN_U)
|
|
2110
|
+
});
|
|
2111
|
+
const backOf = (r, k) => ({
|
|
2112
|
+
x: r.pts[k].x + r.nrm[k][0] * (r.back + FOOTPRINT_MARGIN_U),
|
|
2113
|
+
y: r.pts[k].y + r.nrm[k][1] * (r.back + FOOTPRINT_MARGIN_U)
|
|
2114
|
+
});
|
|
2115
|
+
const first = usable[0], last = usable[usable.length - 1];
|
|
2116
|
+
const ring = [];
|
|
2117
|
+
for (let k = 0; k < first.pts.length; k++) ring.push(frontOf(first, k));
|
|
2118
|
+
for (const r of usable) ring.push(backOf(r, r.pts.length - 1));
|
|
2119
|
+
for (let k = last.pts.length - 1; k >= 0; k--) ring.push(backOf(last, k));
|
|
2120
|
+
for (let i = usable.length - 1; i >= 0; i--) ring.push(frontOf(usable[i], 0));
|
|
2121
|
+
const deduped = dedupeAdjacent(ring);
|
|
2122
|
+
let topY = Infinity;
|
|
2123
|
+
for (const i of indices) if (rows[i].y < topY) topY = rows[i].y;
|
|
2124
|
+
if (!Number.isFinite(topY)) continue;
|
|
2125
|
+
const covers = deduped.length >= 3 && Math.abs(ringArea(deduped)) > 1e-6 && usable.every((r) => {
|
|
2126
|
+
const mid = r.pts[Math.floor(r.pts.length / 2)];
|
|
2127
|
+
return pointInRing(deduped, mid.x, mid.y);
|
|
2128
|
+
});
|
|
2129
|
+
if (covers) {
|
|
2130
|
+
out.push({ outline: simplifyRing(deduped, FOOTPRINT_TOLERANCE_U), holes: [], topY });
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
for (const poly of unionRibbons(usable)) out.push({ outline: poly, holes: [], topY });
|
|
2134
|
+
}
|
|
2135
|
+
return out;
|
|
2136
|
+
}
|
|
2137
|
+
function unionRibbons(ribbons) {
|
|
2138
|
+
const rings = [];
|
|
2139
|
+
for (const r of ribbons) {
|
|
2140
|
+
const f = [];
|
|
2141
|
+
const b = [];
|
|
2142
|
+
const rf = r.front + FOOTPRINT_MARGIN_U;
|
|
2143
|
+
const rb = r.back + FOOTPRINT_MARGIN_U;
|
|
2144
|
+
for (let k = 0; k < r.pts.length; k++) {
|
|
2145
|
+
const p = r.pts[k], n = r.nrm[k];
|
|
2146
|
+
f.push([p.x - n[0] * rf, p.y - n[1] * rf]);
|
|
2147
|
+
b.push([p.x + n[0] * rb, p.y + n[1] * rb]);
|
|
2148
|
+
}
|
|
2149
|
+
const ring = [...f, ...b.reverse()];
|
|
2150
|
+
if (ring.length < 3) continue;
|
|
2151
|
+
ring.push(ring[0]);
|
|
2152
|
+
rings.push([ring]);
|
|
2153
|
+
}
|
|
2154
|
+
if (!rings.length) return [];
|
|
2155
|
+
try {
|
|
2156
|
+
const merged = import_polygon_clipping2.default.union(rings[0], ...rings.slice(1));
|
|
2157
|
+
const out = [];
|
|
2158
|
+
for (const poly of merged) {
|
|
2159
|
+
if (!poly.length || poly[0].length < 4) continue;
|
|
2160
|
+
const pts = poly[0].map(([x, y]) => ({ x, y }));
|
|
2161
|
+
const first = pts[0], last = pts[pts.length - 1];
|
|
2162
|
+
if (pts.length > 1 && Math.abs(first.x - last.x) < 1e-9 && Math.abs(first.y - last.y) < 1e-9) pts.pop();
|
|
2163
|
+
if (pts.length >= 3) out.push(simplifyRing(pts, FOOTPRINT_TOLERANCE_U));
|
|
2164
|
+
}
|
|
2165
|
+
return out;
|
|
2166
|
+
} catch {
|
|
2167
|
+
return [];
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
function ringArea(ring) {
|
|
2171
|
+
let a = 0;
|
|
2172
|
+
for (let i = 0, n = ring.length; i < n; i++) {
|
|
2173
|
+
const p = ring[i], q = ring[(i + 1) % n];
|
|
2174
|
+
a += p.x * q.y - q.x * p.y;
|
|
2175
|
+
}
|
|
2176
|
+
return a / 2;
|
|
2177
|
+
}
|
|
2178
|
+
function dedupeAdjacent(ring) {
|
|
2179
|
+
const out = [];
|
|
2180
|
+
for (const p of ring) {
|
|
2181
|
+
const last = out[out.length - 1];
|
|
2182
|
+
if (last && Math.hypot(p.x - last.x, p.y - last.y) < 1e-9) continue;
|
|
2183
|
+
out.push(p);
|
|
2184
|
+
}
|
|
2185
|
+
while (out.length > 2 && Math.hypot(out[0].x - out[out.length - 1].x, out[0].y - out[out.length - 1].y) < 1e-9) out.pop();
|
|
2186
|
+
return out;
|
|
2187
|
+
}
|
|
2073
2188
|
function pointInRing(ring, x, y) {
|
|
2074
2189
|
let inside = false;
|
|
2075
2190
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
@@ -2152,9 +2267,9 @@ function emitClippedQuad(builder, clipRing, clipTest, quad, y, color) {
|
|
|
2152
2267
|
}
|
|
2153
2268
|
}
|
|
2154
2269
|
}
|
|
2155
|
-
function emitDeckBands(builder, rows, focal, landingY, colors, clip) {
|
|
2270
|
+
function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
2156
2271
|
if (rows.length < 2) return;
|
|
2157
|
-
const nbrs =
|
|
2272
|
+
const nbrs = shared ?? rowNeighbourhoods(rows);
|
|
2158
2273
|
const UP = [0, 1, 0];
|
|
2159
2274
|
const clipRing = clip && clip.length >= 3 ? [[...clip.map((p) => [p.x, p.y]), [clip[0].x, clip[0].y]]] : null;
|
|
2160
2275
|
const clipTest = clip && clip.length >= 3 ? new ClipTest([[...clip]]) : null;
|
|
@@ -2257,9 +2372,7 @@ function boxesOverlap(a, b) {
|
|
|
2257
2372
|
return a.minX <= b.maxX && b.minX <= a.maxX && a.minY <= b.maxY && b.minY <= a.maxY;
|
|
2258
2373
|
}
|
|
2259
2374
|
function paddedFootprint(section, siblings) {
|
|
2260
|
-
const __tp = performance.now();
|
|
2261
2375
|
const padded = outsetRing(section.outline, SEAT_DOT_RADIUS_M * 1.5 * CHART_UNITS_PER_METRE);
|
|
2262
|
-
__t("outsetRing", __tp);
|
|
2263
2376
|
const box = bboxOfRing(padded);
|
|
2264
2377
|
const others = siblings.filter((o) => o !== section && o.outline && o.outline.length >= 3 && boxesOverlap(box, bboxOfRing(o.outline)));
|
|
2265
2378
|
if (!others.length) return [padded];
|
|
@@ -2323,9 +2436,8 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2323
2436
|
const footprint = paddedFootprint(section, siblings);
|
|
2324
2437
|
const outline = footprint[0] ?? section.outline;
|
|
2325
2438
|
if (surface.rowLevels.length >= 2) {
|
|
2326
|
-
const
|
|
2327
|
-
const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal), footprint);
|
|
2328
|
-
__t("deckFootprints", __tf);
|
|
2439
|
+
const nbrs = rowNeighbourhoods(surface.rowLevels);
|
|
2440
|
+
const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal, nbrs), footprint);
|
|
2329
2441
|
const capUp = () => [0, 1, 0];
|
|
2330
2442
|
for (const b of blocks) {
|
|
2331
2443
|
extrudePrism(
|
|
@@ -2353,14 +2465,12 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2353
2465
|
AO
|
|
2354
2466
|
);
|
|
2355
2467
|
}
|
|
2356
|
-
const __tb = performance.now();
|
|
2357
2468
|
emitDeckBands(builder, surface.rowLevels, unit.focal, surface.landingY, {
|
|
2358
2469
|
tread: [colTop[0] * AO.top, colTop[1] * AO.top, colTop[2] * AO.top],
|
|
2359
2470
|
// Risers read as the structure they are, a shade below their tread, which
|
|
2360
2471
|
// is what makes the stepping legible from a low angle.
|
|
2361
2472
|
riser: [colTop[0] * 0.72, colTop[1] * 0.72, colTop[2] * 0.72]
|
|
2362
|
-
}, footprint.length === 1 ? outline : footprint.flat());
|
|
2363
|
-
__t("emitDeckBands", __tb);
|
|
2473
|
+
}, footprint.length === 1 ? outline : footprint.flat(), nbrs);
|
|
2364
2474
|
return;
|
|
2365
2475
|
}
|
|
2366
2476
|
const maxErr = surface.flat ? Infinity : CAP_MAX_ERROR_M;
|
|
@@ -2410,7 +2520,12 @@ var ZONE_LABEL_LIFT_M = 6;
|
|
|
2410
2520
|
var SECTION_LABEL_LIFT_M = 2.2;
|
|
2411
2521
|
var ANNOTATION_LIFT_M = 0.1;
|
|
2412
2522
|
var BOOTH_LABEL_LIFT_M = 0.3;
|
|
2523
|
+
var GA_LABEL_LIFT_M = 1.8;
|
|
2524
|
+
var ROW_LABEL_LIFT_M = 0.9;
|
|
2525
|
+
var SEAT_LABEL_LIFT_M = 0.55;
|
|
2526
|
+
var SEAT_LABEL_MAX = 6e3;
|
|
2413
2527
|
var TABLE_HEIGHT_M = 0.75;
|
|
2528
|
+
var DECOR_PLATE_M = 0.15;
|
|
2414
2529
|
function boothPolygon(booth) {
|
|
2415
2530
|
if (booth.points && booth.points.length >= 3) return booth.points;
|
|
2416
2531
|
const { center, width, height, rotation } = booth;
|
|
@@ -2484,10 +2599,63 @@ function buildShape(builder, shape, base, S) {
|
|
|
2484
2599
|
if (!poly) return;
|
|
2485
2600
|
const isStage = shape.role === "stage";
|
|
2486
2601
|
const height = isStage ? base + 1 : base + 0.25;
|
|
2487
|
-
const colTop = isStage ? S.stageTop : S.decorTop;
|
|
2602
|
+
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
2488
2603
|
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
2489
2604
|
extrudePrism(builder, poly, void 0, () => height, base, colTop, colWall, AO);
|
|
2490
2605
|
}
|
|
2606
|
+
function decorPolygon(decor) {
|
|
2607
|
+
const { x, y, width, height } = decor;
|
|
2608
|
+
if (!width || !height) return null;
|
|
2609
|
+
const cx = x + width / 2, cy = y + height / 2;
|
|
2610
|
+
const a = (decor.rotation ?? 0) * Math.PI / 180;
|
|
2611
|
+
const cos = Math.cos(a), sin = Math.sin(a);
|
|
2612
|
+
const hw = width / 2, hh = height / 2;
|
|
2613
|
+
return [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([lx, ly]) => ({
|
|
2614
|
+
x: cx + lx * cos - ly * sin,
|
|
2615
|
+
y: cy + lx * sin + ly * cos
|
|
2616
|
+
}));
|
|
2617
|
+
}
|
|
2618
|
+
function decorPlatePaint(href) {
|
|
2619
|
+
if (!href || !href.startsWith("data:image/svg+xml")) return null;
|
|
2620
|
+
let svg = href.slice(href.indexOf(",") + 1);
|
|
2621
|
+
if (/;base64/i.test(href.slice(0, href.indexOf(",")))) {
|
|
2622
|
+
try {
|
|
2623
|
+
svg = atob(svg);
|
|
2624
|
+
} catch {
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
} else {
|
|
2628
|
+
try {
|
|
2629
|
+
svg = decodeURIComponent(svg);
|
|
2630
|
+
} catch {
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
const covering = svg.match(/<(?:rect|path)\b[^>]*\bfill="([^"]+)"/i);
|
|
2634
|
+
if (!covering) return null;
|
|
2635
|
+
const paint = covering[1].trim();
|
|
2636
|
+
if (paint === "none" || paint === "transparent") return null;
|
|
2637
|
+
const gradient = paint.match(/^url\(#([^)]+)\)$/);
|
|
2638
|
+
if (!gradient) return hexToRgb(paint);
|
|
2639
|
+
const def = svg.match(new RegExp(`<(?:linear|radial)Gradient\\b[^>]*\\bid="${gradient[1]}"[^>]*>([\\s\\S]*?)</(?:linear|radial)Gradient>`, "i"));
|
|
2640
|
+
if (!def) return null;
|
|
2641
|
+
const stops = [];
|
|
2642
|
+
for (const m of def[1].matchAll(/stop-color="([^"]+)"/gi)) {
|
|
2643
|
+
const c = hexToRgb(m[1].trim());
|
|
2644
|
+
if (c) stops.push(c);
|
|
2645
|
+
}
|
|
2646
|
+
if (!stops.length) return null;
|
|
2647
|
+
return stops.reduce((acc, c, i) => mix(acc, c, 1 / (i + 1)), stops[0]);
|
|
2648
|
+
}
|
|
2649
|
+
function buildDecorImage(builder, decor, base, S) {
|
|
2650
|
+
if (decor.layer === "foreground") return;
|
|
2651
|
+
const poly = decorPolygon(decor);
|
|
2652
|
+
if (!poly) return;
|
|
2653
|
+
const top = base + DECOR_PLATE_M;
|
|
2654
|
+
let paint = tintTop(decorPlatePaint(decor.href), S.decorTop);
|
|
2655
|
+
const opacity = Math.max(0, Math.min(1, decor.opacity ?? 1));
|
|
2656
|
+
if (opacity < 1) paint = mix(S.ground, paint, opacity);
|
|
2657
|
+
extrudePrism(builder, poly, void 0, () => top, base, paint, mix(paint, S.decorWall, 0.5), AO);
|
|
2658
|
+
}
|
|
2491
2659
|
function buildGa(builder, ga, base, fill, S) {
|
|
2492
2660
|
if (!ga.points || ga.points.length < 3) return;
|
|
2493
2661
|
const colTop = tintTop(fill, S.gaTop);
|
|
@@ -2513,6 +2681,9 @@ function chartFootprint(units, seats) {
|
|
|
2513
2681
|
} else if (o.type === "table") {
|
|
2514
2682
|
const t = tablePolygon(o);
|
|
2515
2683
|
if (t) for (const p of t) acc(p.x, p.y);
|
|
2684
|
+
} else if (o.type === "decorImage") {
|
|
2685
|
+
const d = decorPolygon(o);
|
|
2686
|
+
if (d) for (const p of d) acc(p.x, p.y);
|
|
2516
2687
|
}
|
|
2517
2688
|
}
|
|
2518
2689
|
}
|
|
@@ -2524,12 +2695,7 @@ function chartFootprint(units, seats) {
|
|
|
2524
2695
|
}
|
|
2525
2696
|
return { minX, minY, maxX, maxY };
|
|
2526
2697
|
}
|
|
2527
|
-
var __PROF = {};
|
|
2528
|
-
var __t = (k, t0) => {
|
|
2529
|
-
__PROF[k] = (__PROF[k] ?? 0) + (performance.now() - t0);
|
|
2530
|
-
};
|
|
2531
2698
|
function buildSceneModel(input) {
|
|
2532
|
-
for (const k of Object.keys(__PROF)) delete __PROF[k];
|
|
2533
2699
|
const { doc, seats } = input;
|
|
2534
2700
|
const theme = resolveTheme3D(doc.theme);
|
|
2535
2701
|
const S = theme.structure;
|
|
@@ -2542,9 +2708,7 @@ function buildSceneModel(input) {
|
|
|
2542
2708
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
2543
2709
|
const catColor = /* @__PURE__ */ new Map();
|
|
2544
2710
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
2545
|
-
const __t0 = performance.now();
|
|
2546
2711
|
const surfaces = buildVenueSurfaces(units, seats);
|
|
2547
|
-
__t("surfaces", __t0);
|
|
2548
2712
|
const seatFloor = new Float32Array(seats.length);
|
|
2549
2713
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
2550
2714
|
const unit = units[unitIndex];
|
|
@@ -2552,23 +2716,17 @@ function buildSceneModel(input) {
|
|
|
2552
2716
|
const claimed = new ClaimedArea();
|
|
2553
2717
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
2554
2718
|
for (const o of unit.objects) {
|
|
2555
|
-
if (o.type === "section")
|
|
2556
|
-
|
|
2557
|
-
buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
2558
|
-
__t("buildTier", t);
|
|
2559
|
-
} else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
|
|
2719
|
+
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
2720
|
+
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
|
|
2560
2721
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2561
2722
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2562
2723
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2724
|
+
else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
|
|
2563
2725
|
}
|
|
2564
2726
|
}
|
|
2565
2727
|
const focal = doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
2566
|
-
const __tm = performance.now();
|
|
2567
2728
|
const solids = mergeMeshData([builder.build()]);
|
|
2568
|
-
__t("meshBuild+merge", __tm);
|
|
2569
|
-
const __ts = performance.now();
|
|
2570
2729
|
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor);
|
|
2571
|
-
__t("seatInstances", __ts);
|
|
2572
2730
|
const zoneDefs = doc.zones ?? [];
|
|
2573
2731
|
const zones = [];
|
|
2574
2732
|
if (zoneDefs.length) {
|
|
@@ -2628,6 +2786,7 @@ function buildSceneModel(input) {
|
|
|
2628
2786
|
}
|
|
2629
2787
|
}
|
|
2630
2788
|
const labels = [];
|
|
2789
|
+
const sections = [];
|
|
2631
2790
|
for (const z of zones) {
|
|
2632
2791
|
if (z.seatCount === 0) continue;
|
|
2633
2792
|
labels.push({
|
|
@@ -2640,6 +2799,7 @@ function buildSceneModel(input) {
|
|
|
2640
2799
|
}
|
|
2641
2800
|
{
|
|
2642
2801
|
const acc = /* @__PURE__ */ new Map();
|
|
2802
|
+
const ext = /* @__PURE__ */ new Map();
|
|
2643
2803
|
for (let i = 0; i < seats.length; i++) {
|
|
2644
2804
|
const owner = surfaces.seatOwner[i];
|
|
2645
2805
|
if (!owner) continue;
|
|
@@ -2652,18 +2812,87 @@ function buildSceneModel(input) {
|
|
|
2652
2812
|
a.x += seats[i].x;
|
|
2653
2813
|
a.y += seats[i].y;
|
|
2654
2814
|
a.deck += surfaces.seatDeckY(i);
|
|
2815
|
+
let e = ext.get(owner);
|
|
2816
|
+
if (!e) {
|
|
2817
|
+
e = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
|
|
2818
|
+
ext.set(owner, e);
|
|
2819
|
+
}
|
|
2820
|
+
if (seats[i].x < e.minX) e.minX = seats[i].x;
|
|
2821
|
+
if (seats[i].x > e.maxX) e.maxX = seats[i].x;
|
|
2822
|
+
if (seats[i].y < e.minY) e.minY = seats[i].y;
|
|
2823
|
+
if (seats[i].y > e.maxY) e.maxY = seats[i].y;
|
|
2655
2824
|
}
|
|
2656
2825
|
for (const unit of units) {
|
|
2657
2826
|
for (const o of unit.objects) {
|
|
2658
2827
|
if (o.type !== "section") continue;
|
|
2659
2828
|
const a = acc.get(o.id);
|
|
2660
2829
|
if (!a || a.n === 0) continue;
|
|
2830
|
+
const e = ext.get(o.id);
|
|
2831
|
+
const centre = [a.x / a.n * M, a.deck / a.n, a.y / a.n * M];
|
|
2832
|
+
sections.push({
|
|
2833
|
+
id: o.id,
|
|
2834
|
+
label: o.displayLabel || o.label || o.id,
|
|
2835
|
+
seatCount: a.n,
|
|
2836
|
+
center: centre,
|
|
2837
|
+
// Half-diagonal of the seat extent — the same fit the zones use.
|
|
2838
|
+
radius: Math.max(1, Math.hypot(e.maxX - e.minX, e.maxY - e.minY) * 0.5 * M),
|
|
2839
|
+
// A section faces its floor's focal, which is what the camera should
|
|
2840
|
+
// look along so the seats present their fronts.
|
|
2841
|
+
focalWorld: [unit.focal.x * M, unit.baseHeightM + 1.5, unit.focal.y * M]
|
|
2842
|
+
});
|
|
2661
2843
|
labels.push({
|
|
2662
2844
|
id: `section:${o.id}`,
|
|
2663
2845
|
kind: "section",
|
|
2664
2846
|
// The buyer-facing name wins over the technical one, as it does in 2D.
|
|
2665
2847
|
text: o.displayLabel || o.label || o.id,
|
|
2666
|
-
anchor: [
|
|
2848
|
+
anchor: [centre[0], centre[1] + SECTION_LABEL_LIFT_M, centre[2]]
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
{
|
|
2854
|
+
for (const unit of units) {
|
|
2855
|
+
for (const o of unit.objects) {
|
|
2856
|
+
if (o.type !== "gaArea") continue;
|
|
2857
|
+
const c = centroidOf(o.points);
|
|
2858
|
+
if (!c) continue;
|
|
2859
|
+
const name = o.displayLabel || o.label || o.id;
|
|
2860
|
+
labels.push({
|
|
2861
|
+
id: `ga:${o.id}`,
|
|
2862
|
+
kind: "ga",
|
|
2863
|
+
text: o.capacity > 0 ? `${name} \xB7 ${o.capacity.toLocaleString()}` : name,
|
|
2864
|
+
anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M]
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
const rowEnds = /* @__PURE__ */ new Map();
|
|
2869
|
+
for (let i = 0; i < seats.length; i++) {
|
|
2870
|
+
const s = seats[i];
|
|
2871
|
+
const far = Math.hypot(s.x - focal.x, s.y - focal.y);
|
|
2872
|
+
const cur = rowEnds.get(s.rowId);
|
|
2873
|
+
if (!cur || far > cur.far) rowEnds.set(s.rowId, { seat: s, index: i, far });
|
|
2874
|
+
}
|
|
2875
|
+
for (const [rowId, end] of rowEnds) {
|
|
2876
|
+
const cut = end.seat.label ? end.seat.label.lastIndexOf("-") : -1;
|
|
2877
|
+
const text = cut > 0 ? end.seat.label.slice(0, cut) : rowId;
|
|
2878
|
+
if (!text) continue;
|
|
2879
|
+
labels.push({
|
|
2880
|
+
id: `row:${rowId}`,
|
|
2881
|
+
kind: "row",
|
|
2882
|
+
text,
|
|
2883
|
+
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
if (seats.length <= SEAT_LABEL_MAX) {
|
|
2887
|
+
for (let i = 0; i < seats.length; i++) {
|
|
2888
|
+
const s = seats[i];
|
|
2889
|
+
const text = s.displayLabel || s.label;
|
|
2890
|
+
if (!text) continue;
|
|
2891
|
+
labels.push({
|
|
2892
|
+
id: `seat:${s.id}`,
|
|
2893
|
+
kind: "seat",
|
|
2894
|
+
text,
|
|
2895
|
+
anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M]
|
|
2667
2896
|
});
|
|
2668
2897
|
}
|
|
2669
2898
|
}
|
|
@@ -2741,6 +2970,7 @@ function buildSceneModel(input) {
|
|
|
2741
2970
|
// Look-at target ~1.5 m up so a seated camera aims slightly down at the stage.
|
|
2742
2971
|
focalWorld: [focal.x * M, 1.5, focal.y * M],
|
|
2743
2972
|
zones,
|
|
2973
|
+
sections,
|
|
2744
2974
|
labels,
|
|
2745
2975
|
floors
|
|
2746
2976
|
};
|
|
@@ -2752,8 +2982,19 @@ var SEPARATION_Y_PX = 20;
|
|
|
2752
2982
|
var KIND_STYLE = {
|
|
2753
2983
|
zone: { size: 15, weight: "600", opacity: 0.95 },
|
|
2754
2984
|
section: { size: 12, weight: "500", opacity: 0.88 },
|
|
2985
|
+
ga: { size: 12, weight: "500", opacity: 0.88 },
|
|
2755
2986
|
booth: { size: 11, weight: "500", opacity: 0.85 },
|
|
2756
|
-
annotation: { size: 11, weight: "400", opacity: 0.75 }
|
|
2987
|
+
annotation: { size: 11, weight: "400", opacity: 0.75 },
|
|
2988
|
+
// Row and seat identity are quieter than the structure they sit inside: at
|
|
2989
|
+
// this range the venue is already understood and the label is a detail, so it
|
|
2990
|
+
// must not compete with the seating it is printed over.
|
|
2991
|
+
row: { size: 10.5, weight: "600", opacity: 0.8 },
|
|
2992
|
+
seat: { size: 9.5, weight: "500", opacity: 0.72 }
|
|
2993
|
+
};
|
|
2994
|
+
var DENSE_KINDS = /* @__PURE__ */ new Set(["row", "seat"]);
|
|
2995
|
+
var DENSE_SEPARATION = {
|
|
2996
|
+
row: { x: 62, y: 16 },
|
|
2997
|
+
seat: { x: 24, y: 13 }
|
|
2757
2998
|
};
|
|
2758
2999
|
var LabelOverlay = class {
|
|
2759
3000
|
constructor(container, opts = {}) {
|
|
@@ -2794,7 +3035,15 @@ var LabelOverlay = class {
|
|
|
2794
3035
|
if (!screen.visible) continue;
|
|
2795
3036
|
candidates.push({ label, screen });
|
|
2796
3037
|
}
|
|
2797
|
-
const
|
|
3038
|
+
const structure = candidates.filter((c) => !DENSE_KINDS.has(c.label.kind));
|
|
3039
|
+
const kept = [
|
|
3040
|
+
...cullOverlapping(structure, SEPARATION_X_PX, SEPARATION_Y_PX),
|
|
3041
|
+
...["row", "seat"].flatMap((kind) => cullOverlapping(
|
|
3042
|
+
candidates.filter((c) => c.label.kind === kind),
|
|
3043
|
+
DENSE_SEPARATION[kind].x,
|
|
3044
|
+
DENSE_SEPARATION[kind].y
|
|
3045
|
+
))
|
|
3046
|
+
];
|
|
2798
3047
|
const keptIds = new Set(kept.map((k) => k.label.id));
|
|
2799
3048
|
for (const { label, screen } of kept) {
|
|
2800
3049
|
const node = this.nodeFor(label);
|
|
@@ -4150,6 +4399,20 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4150
4399
|
loop.requestRender();
|
|
4151
4400
|
return true;
|
|
4152
4401
|
},
|
|
4402
|
+
sections() {
|
|
4403
|
+
return model.sections;
|
|
4404
|
+
},
|
|
4405
|
+
focusSection(sectionId) {
|
|
4406
|
+
const sec = model.sections.find((s) => s.id === sectionId);
|
|
4407
|
+
if (!sec || sec.seatCount === 0) return false;
|
|
4408
|
+
cinematic.cancel();
|
|
4409
|
+
const dx = sec.focalWorld[0] - sec.center[0];
|
|
4410
|
+
const dz = sec.focalWorld[2] - sec.center[2];
|
|
4411
|
+
const azimuth = Math.hypot(dx, dz) > sec.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
4412
|
+
orbit.frame({ center: sec.center, radius: sec.radius * 1.45 }, false, azimuth);
|
|
4413
|
+
loop.requestRender();
|
|
4414
|
+
return true;
|
|
4415
|
+
},
|
|
4153
4416
|
setReducedMotionForTest(value) {
|
|
4154
4417
|
reducedForced = value;
|
|
4155
4418
|
},
|