@seatlayer/core 0.30.0 → 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.
@@ -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.push(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]);
568
- this.nor.push(n[0], n[1], n[2], n[0], n[1], n[2], n[0], n[1], n[2]);
569
- this.col.push(c0[0], c0[1], c0[2], c1[0], c1[1], c1[2], c2[0], c2[1], c2[2]);
570
- this.flr.push(this.currentFloor, this.currentFloor, this.currentFloor);
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.push(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]);
576
- this.nor.push(n0[0], n0[1], n0[2], n1[0], n1[1], n1[2], n2[0], n2[1], n2[2]);
577
- this.col.push(c0[0], c0[1], c0[2], c1[0], c1[1], c1[2], c2[0], c2[1], c2[2]);
578
- this.flr.push(this.currentFloor, this.currentFloor, this.currentFloor);
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: new Float32Array(this.pos),
586
- normal: new Float32Array(this.nor),
587
- color: new Float32Array(this.col),
588
- floor: new Float32Array(this.flr),
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) out.add("section");
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 rake = buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal);
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 (a.hasSeats) {
1743
- for (const pts of a.rows.values()) {
1744
- for (const p of pts) {
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 neighbourhoods(rows) {
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 = neighbourhoods(rows);
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 __tf = performance.now();
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
- const t = performance.now();
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: [a.x / a.n * M, a.deck / a.n + SECTION_LABEL_LIFT_M, a.y / a.n * M]
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 kept = cullOverlapping(candidates, SEPARATION_X_PX, SEPARATION_Y_PX);
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
  },