@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.
@@ -448,12 +448,42 @@ function isDegenerate(p0, p1, p2) {
448
448
  const area = Math.sqrt(Math.max(0, s * (s - e0) * (s - e1) * (s - e2)));
449
449
  return 2 * area / longest < MIN_TRI_ALTITUDE_M;
450
450
  }
451
+ var F32Buffer = class {
452
+ constructor(initial = 1 << 14) {
453
+ this.len = 0;
454
+ this.buf = new Float32Array(initial);
455
+ }
456
+ push3(a, b, c) {
457
+ if (this.len + 3 > this.buf.length) this.grow(this.len + 3);
458
+ this.buf[this.len++] = a;
459
+ this.buf[this.len++] = b;
460
+ this.buf[this.len++] = c;
461
+ }
462
+ push1(a) {
463
+ if (this.len + 1 > this.buf.length) this.grow(this.len + 1);
464
+ this.buf[this.len++] = a;
465
+ }
466
+ grow(need) {
467
+ let cap = this.buf.length * 2;
468
+ while (cap < need) cap *= 2;
469
+ const next = new Float32Array(cap);
470
+ next.set(this.buf.subarray(0, this.len));
471
+ this.buf = next;
472
+ }
473
+ get length() {
474
+ return this.len;
475
+ }
476
+ /** A copy trimmed to the used length. */
477
+ toArray() {
478
+ return this.buf.slice(0, this.len);
479
+ }
480
+ };
451
481
  var MeshBuilder = class {
452
482
  constructor() {
453
- this.pos = [];
454
- this.nor = [];
455
- this.col = [];
456
- this.flr = [];
483
+ this.pos = new F32Buffer();
484
+ this.nor = new F32Buffer();
485
+ this.col = new F32Buffer();
486
+ this.flr = new F32Buffer();
457
487
  /** Floor index stamped onto every triangle emitted from now on. */
458
488
  this.currentFloor = 0;
459
489
  }
@@ -464,28 +494,44 @@ var MeshBuilder = class {
464
494
  /** One triangle with a shared (flat) normal and per-vertex colours. */
465
495
  tri(p0, p1, p2, n, c0, c1 = c0, c2 = c0) {
466
496
  if (isDegenerate(p0, p1, p2)) return;
467
- this.pos.push(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]);
468
- this.nor.push(n[0], n[1], n[2], n[0], n[1], n[2], n[0], n[1], n[2]);
469
- this.col.push(c0[0], c0[1], c0[2], c1[0], c1[1], c1[2], c2[0], c2[1], c2[2]);
470
- this.flr.push(this.currentFloor, this.currentFloor, this.currentFloor);
497
+ this.pos.push3(p0[0], p0[1], p0[2]);
498
+ this.pos.push3(p1[0], p1[1], p1[2]);
499
+ this.pos.push3(p2[0], p2[1], p2[2]);
500
+ this.nor.push3(n[0], n[1], n[2]);
501
+ this.nor.push3(n[0], n[1], n[2]);
502
+ this.nor.push3(n[0], n[1], n[2]);
503
+ this.col.push3(c0[0], c0[1], c0[2]);
504
+ this.col.push3(c1[0], c1[1], c1[2]);
505
+ this.col.push3(c2[0], c2[1], c2[2]);
506
+ this.flr.push1(this.currentFloor);
507
+ this.flr.push1(this.currentFloor);
508
+ this.flr.push1(this.currentFloor);
471
509
  }
472
510
  /** One triangle with independent per-vertex normals (smooth shading). */
473
511
  triN(p0, p1, p2, n0, n1, n2, c0, c1 = c0, c2 = c0) {
474
512
  if (isDegenerate(p0, p1, p2)) return;
475
- this.pos.push(p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]);
476
- this.nor.push(n0[0], n0[1], n0[2], n1[0], n1[1], n1[2], n2[0], n2[1], n2[2]);
477
- this.col.push(c0[0], c0[1], c0[2], c1[0], c1[1], c1[2], c2[0], c2[1], c2[2]);
478
- this.flr.push(this.currentFloor, this.currentFloor, this.currentFloor);
513
+ this.pos.push3(p0[0], p0[1], p0[2]);
514
+ this.pos.push3(p1[0], p1[1], p1[2]);
515
+ this.pos.push3(p2[0], p2[1], p2[2]);
516
+ this.nor.push3(n0[0], n0[1], n0[2]);
517
+ this.nor.push3(n1[0], n1[1], n1[2]);
518
+ this.nor.push3(n2[0], n2[1], n2[2]);
519
+ this.col.push3(c0[0], c0[1], c0[2]);
520
+ this.col.push3(c1[0], c1[1], c1[2]);
521
+ this.col.push3(c2[0], c2[1], c2[2]);
522
+ this.flr.push1(this.currentFloor);
523
+ this.flr.push1(this.currentFloor);
524
+ this.flr.push1(this.currentFloor);
479
525
  }
480
526
  get vertexCount() {
481
527
  return this.pos.length / 3;
482
528
  }
483
529
  build() {
484
530
  return {
485
- position: new Float32Array(this.pos),
486
- normal: new Float32Array(this.nor),
487
- color: new Float32Array(this.col),
488
- floor: new Float32Array(this.flr),
531
+ position: this.pos.toArray(),
532
+ normal: this.nor.toArray(),
533
+ color: this.col.toArray(),
534
+ floor: this.flr.toArray(),
489
535
  count: this.pos.length / 3
490
536
  };
491
537
  }
@@ -993,16 +1039,23 @@ function themeSeatColorLUT(theme, order) {
993
1039
  var ZONE_MIN_DISTANCE = 1.15;
994
1040
  var SECTION_MAX_DISTANCE = 2.2;
995
1041
  var NEAR_MAX_DISTANCE = 0.85;
1042
+ var ROW_MAX_DISTANCE = 0.5;
1043
+ var SEAT_MAX_DISTANCE = 0.26;
996
1044
  function visibleLabelKinds(distance, venueRadius) {
997
1045
  const r = Math.max(1e-6, venueRadius);
998
1046
  const d = distance / r;
999
1047
  const out = /* @__PURE__ */ new Set();
1000
1048
  if (d >= ZONE_MIN_DISTANCE) out.add("zone");
1001
- if (d <= SECTION_MAX_DISTANCE) out.add("section");
1049
+ if (d <= SECTION_MAX_DISTANCE) {
1050
+ out.add("section");
1051
+ out.add("ga");
1052
+ }
1002
1053
  if (d <= NEAR_MAX_DISTANCE) {
1003
1054
  out.add("annotation");
1004
1055
  out.add("booth");
1005
1056
  }
1057
+ if (d <= ROW_MAX_DISTANCE) out.add("row");
1058
+ if (d <= SEAT_MAX_DISTANCE) out.add("seat");
1006
1059
  return out;
1007
1060
  }
1008
1061
  function projectToScreen(viewProjection, p, width, height) {
@@ -1270,23 +1323,27 @@ function buildVenueSurfaces(units, seats) {
1270
1323
  const inferredFlat = geo.rake <= 0.01 && geo.height <= bottomY + 1e-3;
1271
1324
  const kind = a.section.surfaceKind;
1272
1325
  const flat = kind === "flat" ? true : kind === "rakedRows" ? geo.rake > 0.01 ? false : inferredFlat : inferredFlat;
1273
- const rake = buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal);
1326
+ const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
1327
+ const needsRake = structure.rows.length < 2;
1328
+ const rake = needsRake ? buildSectionRake([...a.rows.values()].map((points) => ({ points })), a.unit.focal) : null;
1274
1329
  let frontU = Infinity;
1275
- if (a.hasSeats) {
1276
- for (const pts of a.rows.values()) {
1277
- for (const p of pts) {
1330
+ if (rake) {
1331
+ if (a.hasSeats) {
1332
+ for (const pts of a.rows.values()) {
1333
+ for (const p of pts) {
1334
+ const d = rake.depthAt(p.x, p.y);
1335
+ if (d < frontU) frontU = d;
1336
+ }
1337
+ }
1338
+ }
1339
+ if (!a.hasSeats || !Number.isFinite(frontU)) {
1340
+ frontU = Infinity;
1341
+ for (const p of a.section.outline) {
1278
1342
  const d = rake.depthAt(p.x, p.y);
1279
1343
  if (d < frontU) frontU = d;
1280
1344
  }
1281
1345
  }
1282
1346
  }
1283
- if (!a.hasSeats || !Number.isFinite(frontU)) {
1284
- frontU = Infinity;
1285
- for (const p of a.section.outline) {
1286
- const d = rake.depthAt(p.x, p.y);
1287
- if (d < frontU) frontU = d;
1288
- }
1289
- }
1290
1347
  const flatTop = bottomY + FLAT_SLAB_TOP_M;
1291
1348
  const baseFloor = bottomY + FLAT_SLAB_TOP_M;
1292
1349
  const levelFor = (depthU) => {
@@ -1299,7 +1356,6 @@ function buildVenueSurfaces(units, seats) {
1299
1356
  const rise = Math.min(depthM * rakeTan, MAX_TIER_RISE_M);
1300
1357
  return Math.max(baseFloor, geo.height + rise);
1301
1358
  };
1302
- const structure = a.hasSeats ? resolveSection(id, seats, a.seatIndices, a.unit.focal) : { sectionId: id, rows: [], blockCount: 0 };
1303
1359
  const rowLevels = flat ? [] : structure.rows.map((r) => ({
1304
1360
  pts: r.pts,
1305
1361
  y: levelForBlockDepth(r.blockDepth),
@@ -1335,9 +1391,10 @@ function buildVenueSurfaces(units, seats) {
1335
1391
  }
1336
1392
  }
1337
1393
  return bestY;
1338
- } : (x, y) => levelFor(rake.depthAt(x, y));
1394
+ } : (x, y) => rake ? levelFor(rake.depthAt(x, y)) : flatTop;
1339
1395
  const UP = [0, 1, 0];
1340
1396
  const normalAt = rowLevels.length >= 2 || flat ? () => UP : (x, y) => {
1397
+ if (!rake) return UP;
1341
1398
  const d = rake.depthAt(x, y);
1342
1399
  if (d <= frontU) return UP;
1343
1400
  const depthM = (d - frontU) * METRES_PER_CHART_UNIT;
@@ -1460,7 +1517,23 @@ function distToPolyline(pts, x, y) {
1460
1517
  }
1461
1518
  return best;
1462
1519
  }
1463
- function neighbourhoods(rows) {
1520
+ function rowNeighbourhoods(rows) {
1521
+ const bounds = rows.map((r) => {
1522
+ let cx = 0, cy = 0;
1523
+ for (const p of r.pts) {
1524
+ cx += p.x;
1525
+ cy += p.y;
1526
+ }
1527
+ const n = r.pts.length || 1;
1528
+ cx /= n;
1529
+ cy /= n;
1530
+ let rad = 0;
1531
+ for (const p of r.pts) {
1532
+ const d = Math.hypot(p.x - cx, p.y - cy);
1533
+ if (d > rad) rad = d;
1534
+ }
1535
+ return { cx, cy, rad };
1536
+ });
1464
1537
  const probesOf = (pts) => {
1465
1538
  const n = pts.length;
1466
1539
  if (n <= 2) return [...pts];
@@ -1475,6 +1548,9 @@ function neighbourhoods(rows) {
1475
1548
  let nearest = Infinity;
1476
1549
  for (let j = 0; j < rows.length; j++) {
1477
1550
  if (j === i || Math.abs(rows[j].y - rows[i].y) < 1e-3) continue;
1551
+ const b = bounds[j];
1552
+ const lower = Math.hypot(c.x - b.cx, c.y - b.cy) - b.rad;
1553
+ if (lower >= nearest && lower >= bestFrontD) continue;
1478
1554
  const d = distToPolyline(rows[j].pts, c.x, c.y);
1479
1555
  if (d < nearest) nearest = d;
1480
1556
  if (rows[j].depth < rows[i].depth && d < bestFrontD) {
@@ -1519,65 +1595,8 @@ function ribbonOf(rows, i, nbrs, focal) {
1519
1595
  back: Math.max(nbrs[i].pitch * BACK_REACH, MIN_REACH_U)
1520
1596
  };
1521
1597
  }
1522
- function deckFootprints(rows, focal) {
1523
- if (rows.length < 2) return [];
1524
- const nbrs = neighbourhoods(rows);
1525
- const rings = [];
1526
- const ribbons = [];
1527
- for (let i = 0; i < rows.length; i++) {
1528
- const r = ribbonOf(rows, i, nbrs, focal);
1529
- ribbons.push(r);
1530
- if (!r) continue;
1531
- const f = [];
1532
- const b = [];
1533
- const rf = r.front + FOOTPRINT_MARGIN_U;
1534
- const rb = r.back + FOOTPRINT_MARGIN_U;
1535
- for (let k = 0; k < r.pts.length; k++) {
1536
- const p = r.pts[k], n = r.nrm[k];
1537
- f.push([p.x - n[0] * rf, p.y - n[1] * rf]);
1538
- b.push([p.x + n[0] * rb, p.y + n[1] * rb]);
1539
- }
1540
- const ring = [...f, ...b.reverse()];
1541
- if (ring.length < 3) continue;
1542
- ring.push(ring[0]);
1543
- rings.push([ring]);
1544
- }
1545
- if (!rings.length) return [];
1546
- let merged;
1547
- try {
1548
- merged = polygonClipping2.union(rings[0], ...rings.slice(1));
1549
- } catch {
1550
- return [];
1551
- }
1552
- const out = [];
1553
- for (const poly of merged) {
1554
- if (!poly.length || poly[0].length < 4) continue;
1555
- const toPts = (ring) => {
1556
- const pts = ring.map(([x, y]) => ({ x, y }));
1557
- const first = pts[0], last = pts[pts.length - 1];
1558
- if (pts.length > 1 && Math.abs(first.x - last.x) < 1e-9 && Math.abs(first.y - last.y) < 1e-9) pts.pop();
1559
- return pts;
1560
- };
1561
- const outline = toPts(poly[0]);
1562
- if (outline.length < 3) continue;
1563
- let topY = Infinity;
1564
- for (let i = 0; i < rows.length; i++) {
1565
- const r = ribbons[i];
1566
- if (!r) continue;
1567
- const mid = r.pts[Math.floor(r.pts.length / 2)];
1568
- if (pointInRing(outline, mid.x, mid.y) && rows[i].y < topY) topY = rows[i].y;
1569
- }
1570
- if (!Number.isFinite(topY)) continue;
1571
- out.push({
1572
- outline: simplifyRing(outline, FOOTPRINT_TOLERANCE_U),
1573
- holes: poly.slice(1).map(toPts).map((h) => simplifyRing(h, FOOTPRINT_TOLERANCE_U)).filter((h) => h.length >= 3),
1574
- topY
1575
- });
1576
- }
1577
- return out;
1578
- }
1579
- var FOOTPRINT_TOLERANCE_U = 0.3;
1580
1598
  var FOOTPRINT_MARGIN_U = 1.5;
1599
+ var FOOTPRINT_TOLERANCE_U = 0.3;
1581
1600
  function simplifyRun(pts, tol) {
1582
1601
  if (pts.length < 3) return pts;
1583
1602
  const a = pts[0], b = pts[pts.length - 1];
@@ -1603,6 +1622,102 @@ function simplifyRing(ring, tol) {
1603
1622
  out.pop();
1604
1623
  return out.length >= 3 ? out : ring;
1605
1624
  }
1625
+ function deckFootprints(rows, focal, shared) {
1626
+ if (rows.length < 2) return [];
1627
+ const nbrs = shared ?? rowNeighbourhoods(rows);
1628
+ const byBlock = /* @__PURE__ */ new Map();
1629
+ for (let i = 0; i < rows.length; i++) {
1630
+ const a = byBlock.get(rows[i].blockId);
1631
+ if (a) a.push(i);
1632
+ else byBlock.set(rows[i].blockId, [i]);
1633
+ }
1634
+ const out = [];
1635
+ for (const [, indices] of byBlock) {
1636
+ indices.sort((a, b) => rows[a].depth - rows[b].depth);
1637
+ const ribbons = indices.map((i) => ribbonOf(rows, i, nbrs, focal));
1638
+ const usable = ribbons.filter((r) => r !== null);
1639
+ if (!usable.length) continue;
1640
+ const frontOf = (r, k) => ({
1641
+ x: r.pts[k].x - r.nrm[k][0] * (r.front + FOOTPRINT_MARGIN_U),
1642
+ y: r.pts[k].y - r.nrm[k][1] * (r.front + FOOTPRINT_MARGIN_U)
1643
+ });
1644
+ const backOf = (r, k) => ({
1645
+ x: r.pts[k].x + r.nrm[k][0] * (r.back + FOOTPRINT_MARGIN_U),
1646
+ y: r.pts[k].y + r.nrm[k][1] * (r.back + FOOTPRINT_MARGIN_U)
1647
+ });
1648
+ const first = usable[0], last = usable[usable.length - 1];
1649
+ const ring = [];
1650
+ for (let k = 0; k < first.pts.length; k++) ring.push(frontOf(first, k));
1651
+ for (const r of usable) ring.push(backOf(r, r.pts.length - 1));
1652
+ for (let k = last.pts.length - 1; k >= 0; k--) ring.push(backOf(last, k));
1653
+ for (let i = usable.length - 1; i >= 0; i--) ring.push(frontOf(usable[i], 0));
1654
+ const deduped = dedupeAdjacent(ring);
1655
+ let topY = Infinity;
1656
+ for (const i of indices) if (rows[i].y < topY) topY = rows[i].y;
1657
+ if (!Number.isFinite(topY)) continue;
1658
+ const covers = deduped.length >= 3 && Math.abs(ringArea(deduped)) > 1e-6 && usable.every((r) => {
1659
+ const mid = r.pts[Math.floor(r.pts.length / 2)];
1660
+ return pointInRing(deduped, mid.x, mid.y);
1661
+ });
1662
+ if (covers) {
1663
+ out.push({ outline: simplifyRing(deduped, FOOTPRINT_TOLERANCE_U), holes: [], topY });
1664
+ continue;
1665
+ }
1666
+ for (const poly of unionRibbons(usable)) out.push({ outline: poly, holes: [], topY });
1667
+ }
1668
+ return out;
1669
+ }
1670
+ function unionRibbons(ribbons) {
1671
+ const rings = [];
1672
+ for (const r of ribbons) {
1673
+ const f = [];
1674
+ const b = [];
1675
+ const rf = r.front + FOOTPRINT_MARGIN_U;
1676
+ const rb = r.back + FOOTPRINT_MARGIN_U;
1677
+ for (let k = 0; k < r.pts.length; k++) {
1678
+ const p = r.pts[k], n = r.nrm[k];
1679
+ f.push([p.x - n[0] * rf, p.y - n[1] * rf]);
1680
+ b.push([p.x + n[0] * rb, p.y + n[1] * rb]);
1681
+ }
1682
+ const ring = [...f, ...b.reverse()];
1683
+ if (ring.length < 3) continue;
1684
+ ring.push(ring[0]);
1685
+ rings.push([ring]);
1686
+ }
1687
+ if (!rings.length) return [];
1688
+ try {
1689
+ const merged = polygonClipping2.union(rings[0], ...rings.slice(1));
1690
+ const out = [];
1691
+ for (const poly of merged) {
1692
+ if (!poly.length || poly[0].length < 4) continue;
1693
+ const pts = poly[0].map(([x, y]) => ({ x, y }));
1694
+ const first = pts[0], last = pts[pts.length - 1];
1695
+ if (pts.length > 1 && Math.abs(first.x - last.x) < 1e-9 && Math.abs(first.y - last.y) < 1e-9) pts.pop();
1696
+ if (pts.length >= 3) out.push(simplifyRing(pts, FOOTPRINT_TOLERANCE_U));
1697
+ }
1698
+ return out;
1699
+ } catch {
1700
+ return [];
1701
+ }
1702
+ }
1703
+ function ringArea(ring) {
1704
+ let a = 0;
1705
+ for (let i = 0, n = ring.length; i < n; i++) {
1706
+ const p = ring[i], q = ring[(i + 1) % n];
1707
+ a += p.x * q.y - q.x * p.y;
1708
+ }
1709
+ return a / 2;
1710
+ }
1711
+ function dedupeAdjacent(ring) {
1712
+ const out = [];
1713
+ for (const p of ring) {
1714
+ const last = out[out.length - 1];
1715
+ if (last && Math.hypot(p.x - last.x, p.y - last.y) < 1e-9) continue;
1716
+ out.push(p);
1717
+ }
1718
+ 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();
1719
+ return out;
1720
+ }
1606
1721
  function pointInRing(ring, x, y) {
1607
1722
  let inside = false;
1608
1723
  for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
@@ -1685,9 +1800,9 @@ function emitClippedQuad(builder, clipRing, clipTest, quad, y, color) {
1685
1800
  }
1686
1801
  }
1687
1802
  }
1688
- function emitDeckBands(builder, rows, focal, landingY, colors, clip) {
1803
+ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
1689
1804
  if (rows.length < 2) return;
1690
- const nbrs = neighbourhoods(rows);
1805
+ const nbrs = shared ?? rowNeighbourhoods(rows);
1691
1806
  const UP = [0, 1, 0];
1692
1807
  const clipRing = clip && clip.length >= 3 ? [[...clip.map((p) => [p.x, p.y]), [clip[0].x, clip[0].y]]] : null;
1693
1808
  const clipTest = clip && clip.length >= 3 ? new ClipTest([[...clip]]) : null;
@@ -1790,9 +1905,7 @@ function boxesOverlap(a, b) {
1790
1905
  return a.minX <= b.maxX && b.minX <= a.maxX && a.minY <= b.maxY && b.minY <= a.maxY;
1791
1906
  }
1792
1907
  function paddedFootprint(section, siblings) {
1793
- const __tp = performance.now();
1794
1908
  const padded = outsetRing(section.outline, SEAT_DOT_RADIUS_M * 1.5 * CHART_UNITS_PER_METRE);
1795
- __t("outsetRing", __tp);
1796
1909
  const box = bboxOfRing(padded);
1797
1910
  const others = siblings.filter((o) => o !== section && o.outline && o.outline.length >= 3 && boxesOverlap(box, bboxOfRing(o.outline)));
1798
1911
  if (!others.length) return [padded];
@@ -1856,9 +1969,8 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
1856
1969
  const footprint = paddedFootprint(section, siblings);
1857
1970
  const outline = footprint[0] ?? section.outline;
1858
1971
  if (surface.rowLevels.length >= 2) {
1859
- const __tf = performance.now();
1860
- const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal), footprint);
1861
- __t("deckFootprints", __tf);
1972
+ const nbrs = rowNeighbourhoods(surface.rowLevels);
1973
+ const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal, nbrs), footprint);
1862
1974
  const capUp = () => [0, 1, 0];
1863
1975
  for (const b of blocks) {
1864
1976
  extrudePrism(
@@ -1886,14 +1998,12 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
1886
1998
  AO
1887
1999
  );
1888
2000
  }
1889
- const __tb = performance.now();
1890
2001
  emitDeckBands(builder, surface.rowLevels, unit.focal, surface.landingY, {
1891
2002
  tread: [colTop[0] * AO.top, colTop[1] * AO.top, colTop[2] * AO.top],
1892
2003
  // Risers read as the structure they are, a shade below their tread, which
1893
2004
  // is what makes the stepping legible from a low angle.
1894
2005
  riser: [colTop[0] * 0.72, colTop[1] * 0.72, colTop[2] * 0.72]
1895
- }, footprint.length === 1 ? outline : footprint.flat());
1896
- __t("emitDeckBands", __tb);
2006
+ }, footprint.length === 1 ? outline : footprint.flat(), nbrs);
1897
2007
  return;
1898
2008
  }
1899
2009
  const maxErr = surface.flat ? Infinity : CAP_MAX_ERROR_M;
@@ -1943,7 +2053,12 @@ var ZONE_LABEL_LIFT_M = 6;
1943
2053
  var SECTION_LABEL_LIFT_M = 2.2;
1944
2054
  var ANNOTATION_LIFT_M = 0.1;
1945
2055
  var BOOTH_LABEL_LIFT_M = 0.3;
2056
+ var GA_LABEL_LIFT_M = 1.8;
2057
+ var ROW_LABEL_LIFT_M = 0.9;
2058
+ var SEAT_LABEL_LIFT_M = 0.55;
2059
+ var SEAT_LABEL_MAX = 6e3;
1946
2060
  var TABLE_HEIGHT_M = 0.75;
2061
+ var DECOR_PLATE_M = 0.15;
1947
2062
  function boothPolygon(booth) {
1948
2063
  if (booth.points && booth.points.length >= 3) return booth.points;
1949
2064
  const { center, width, height, rotation } = booth;
@@ -2017,10 +2132,63 @@ function buildShape(builder, shape, base, S) {
2017
2132
  if (!poly) return;
2018
2133
  const isStage = shape.role === "stage";
2019
2134
  const height = isStage ? base + 1 : base + 0.25;
2020
- const colTop = isStage ? S.stageTop : S.decorTop;
2135
+ const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
2021
2136
  const colWall = isStage ? S.stageWall : S.decorWall;
2022
2137
  extrudePrism(builder, poly, void 0, () => height, base, colTop, colWall, AO);
2023
2138
  }
2139
+ function decorPolygon(decor) {
2140
+ const { x, y, width, height } = decor;
2141
+ if (!width || !height) return null;
2142
+ const cx = x + width / 2, cy = y + height / 2;
2143
+ const a = (decor.rotation ?? 0) * Math.PI / 180;
2144
+ const cos = Math.cos(a), sin = Math.sin(a);
2145
+ const hw = width / 2, hh = height / 2;
2146
+ return [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([lx, ly]) => ({
2147
+ x: cx + lx * cos - ly * sin,
2148
+ y: cy + lx * sin + ly * cos
2149
+ }));
2150
+ }
2151
+ function decorPlatePaint(href) {
2152
+ if (!href || !href.startsWith("data:image/svg+xml")) return null;
2153
+ let svg = href.slice(href.indexOf(",") + 1);
2154
+ if (/;base64/i.test(href.slice(0, href.indexOf(",")))) {
2155
+ try {
2156
+ svg = atob(svg);
2157
+ } catch {
2158
+ return null;
2159
+ }
2160
+ } else {
2161
+ try {
2162
+ svg = decodeURIComponent(svg);
2163
+ } catch {
2164
+ }
2165
+ }
2166
+ const covering = svg.match(/<(?:rect|path)\b[^>]*\bfill="([^"]+)"/i);
2167
+ if (!covering) return null;
2168
+ const paint = covering[1].trim();
2169
+ if (paint === "none" || paint === "transparent") return null;
2170
+ const gradient = paint.match(/^url\(#([^)]+)\)$/);
2171
+ if (!gradient) return hexToRgb(paint);
2172
+ const def = svg.match(new RegExp(`<(?:linear|radial)Gradient\\b[^>]*\\bid="${gradient[1]}"[^>]*>([\\s\\S]*?)</(?:linear|radial)Gradient>`, "i"));
2173
+ if (!def) return null;
2174
+ const stops = [];
2175
+ for (const m of def[1].matchAll(/stop-color="([^"]+)"/gi)) {
2176
+ const c = hexToRgb(m[1].trim());
2177
+ if (c) stops.push(c);
2178
+ }
2179
+ if (!stops.length) return null;
2180
+ return stops.reduce((acc, c, i) => mix(acc, c, 1 / (i + 1)), stops[0]);
2181
+ }
2182
+ function buildDecorImage(builder, decor, base, S) {
2183
+ if (decor.layer === "foreground") return;
2184
+ const poly = decorPolygon(decor);
2185
+ if (!poly) return;
2186
+ const top = base + DECOR_PLATE_M;
2187
+ let paint = tintTop(decorPlatePaint(decor.href), S.decorTop);
2188
+ const opacity = Math.max(0, Math.min(1, decor.opacity ?? 1));
2189
+ if (opacity < 1) paint = mix(S.ground, paint, opacity);
2190
+ extrudePrism(builder, poly, void 0, () => top, base, paint, mix(paint, S.decorWall, 0.5), AO);
2191
+ }
2024
2192
  function buildGa(builder, ga, base, fill, S) {
2025
2193
  if (!ga.points || ga.points.length < 3) return;
2026
2194
  const colTop = tintTop(fill, S.gaTop);
@@ -2046,6 +2214,9 @@ function chartFootprint(units, seats) {
2046
2214
  } else if (o.type === "table") {
2047
2215
  const t = tablePolygon(o);
2048
2216
  if (t) for (const p of t) acc(p.x, p.y);
2217
+ } else if (o.type === "decorImage") {
2218
+ const d = decorPolygon(o);
2219
+ if (d) for (const p of d) acc(p.x, p.y);
2049
2220
  }
2050
2221
  }
2051
2222
  }
@@ -2057,12 +2228,7 @@ function chartFootprint(units, seats) {
2057
2228
  }
2058
2229
  return { minX, minY, maxX, maxY };
2059
2230
  }
2060
- var __PROF = {};
2061
- var __t = (k, t0) => {
2062
- __PROF[k] = (__PROF[k] ?? 0) + (performance.now() - t0);
2063
- };
2064
2231
  function buildSceneModel(input) {
2065
- for (const k of Object.keys(__PROF)) delete __PROF[k];
2066
2232
  const { doc, seats } = input;
2067
2233
  const theme = resolveTheme3D(doc.theme);
2068
2234
  const S = theme.structure;
@@ -2075,9 +2241,7 @@ function buildSceneModel(input) {
2075
2241
  const sectionFills = resolveSectionFills(doc, seats);
2076
2242
  const catColor = /* @__PURE__ */ new Map();
2077
2243
  for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
2078
- const __t0 = performance.now();
2079
2244
  const surfaces = buildVenueSurfaces(units, seats);
2080
- __t("surfaces", __t0);
2081
2245
  const seatFloor = new Float32Array(seats.length);
2082
2246
  for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
2083
2247
  const unit = units[unitIndex];
@@ -2085,23 +2249,17 @@ function buildSceneModel(input) {
2085
2249
  const claimed = new ClaimedArea();
2086
2250
  const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
2087
2251
  for (const o of unit.objects) {
2088
- if (o.type === "section") {
2089
- const t = performance.now();
2090
- buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
2091
- __t("buildTier", t);
2092
- } else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
2252
+ if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
2253
+ else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
2093
2254
  else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
2094
2255
  else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
2095
2256
  else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
2257
+ else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
2096
2258
  }
2097
2259
  }
2098
2260
  const focal = doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
2099
- const __tm = performance.now();
2100
2261
  const solids = mergeMeshData([builder.build()]);
2101
- __t("meshBuild+merge", __tm);
2102
- const __ts = performance.now();
2103
2262
  const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor);
2104
- __t("seatInstances", __ts);
2105
2263
  const zoneDefs = doc.zones ?? [];
2106
2264
  const zones = [];
2107
2265
  if (zoneDefs.length) {
@@ -2161,6 +2319,7 @@ function buildSceneModel(input) {
2161
2319
  }
2162
2320
  }
2163
2321
  const labels = [];
2322
+ const sections = [];
2164
2323
  for (const z of zones) {
2165
2324
  if (z.seatCount === 0) continue;
2166
2325
  labels.push({
@@ -2173,6 +2332,7 @@ function buildSceneModel(input) {
2173
2332
  }
2174
2333
  {
2175
2334
  const acc = /* @__PURE__ */ new Map();
2335
+ const ext = /* @__PURE__ */ new Map();
2176
2336
  for (let i = 0; i < seats.length; i++) {
2177
2337
  const owner = surfaces.seatOwner[i];
2178
2338
  if (!owner) continue;
@@ -2185,18 +2345,87 @@ function buildSceneModel(input) {
2185
2345
  a.x += seats[i].x;
2186
2346
  a.y += seats[i].y;
2187
2347
  a.deck += surfaces.seatDeckY(i);
2348
+ let e = ext.get(owner);
2349
+ if (!e) {
2350
+ e = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
2351
+ ext.set(owner, e);
2352
+ }
2353
+ if (seats[i].x < e.minX) e.minX = seats[i].x;
2354
+ if (seats[i].x > e.maxX) e.maxX = seats[i].x;
2355
+ if (seats[i].y < e.minY) e.minY = seats[i].y;
2356
+ if (seats[i].y > e.maxY) e.maxY = seats[i].y;
2188
2357
  }
2189
2358
  for (const unit of units) {
2190
2359
  for (const o of unit.objects) {
2191
2360
  if (o.type !== "section") continue;
2192
2361
  const a = acc.get(o.id);
2193
2362
  if (!a || a.n === 0) continue;
2363
+ const e = ext.get(o.id);
2364
+ const centre = [a.x / a.n * M, a.deck / a.n, a.y / a.n * M];
2365
+ sections.push({
2366
+ id: o.id,
2367
+ label: o.displayLabel || o.label || o.id,
2368
+ seatCount: a.n,
2369
+ center: centre,
2370
+ // Half-diagonal of the seat extent — the same fit the zones use.
2371
+ radius: Math.max(1, Math.hypot(e.maxX - e.minX, e.maxY - e.minY) * 0.5 * M),
2372
+ // A section faces its floor's focal, which is what the camera should
2373
+ // look along so the seats present their fronts.
2374
+ focalWorld: [unit.focal.x * M, unit.baseHeightM + 1.5, unit.focal.y * M]
2375
+ });
2194
2376
  labels.push({
2195
2377
  id: `section:${o.id}`,
2196
2378
  kind: "section",
2197
2379
  // The buyer-facing name wins over the technical one, as it does in 2D.
2198
2380
  text: o.displayLabel || o.label || o.id,
2199
- anchor: [a.x / a.n * M, a.deck / a.n + SECTION_LABEL_LIFT_M, a.y / a.n * M]
2381
+ anchor: [centre[0], centre[1] + SECTION_LABEL_LIFT_M, centre[2]]
2382
+ });
2383
+ }
2384
+ }
2385
+ }
2386
+ {
2387
+ for (const unit of units) {
2388
+ for (const o of unit.objects) {
2389
+ if (o.type !== "gaArea") continue;
2390
+ const c = centroidOf(o.points);
2391
+ if (!c) continue;
2392
+ const name = o.displayLabel || o.label || o.id;
2393
+ labels.push({
2394
+ id: `ga:${o.id}`,
2395
+ kind: "ga",
2396
+ text: o.capacity > 0 ? `${name} \xB7 ${o.capacity.toLocaleString()}` : name,
2397
+ anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M]
2398
+ });
2399
+ }
2400
+ }
2401
+ const rowEnds = /* @__PURE__ */ new Map();
2402
+ for (let i = 0; i < seats.length; i++) {
2403
+ const s = seats[i];
2404
+ const far = Math.hypot(s.x - focal.x, s.y - focal.y);
2405
+ const cur = rowEnds.get(s.rowId);
2406
+ if (!cur || far > cur.far) rowEnds.set(s.rowId, { seat: s, index: i, far });
2407
+ }
2408
+ for (const [rowId, end] of rowEnds) {
2409
+ const cut = end.seat.label ? end.seat.label.lastIndexOf("-") : -1;
2410
+ const text = cut > 0 ? end.seat.label.slice(0, cut) : rowId;
2411
+ if (!text) continue;
2412
+ labels.push({
2413
+ id: `row:${rowId}`,
2414
+ kind: "row",
2415
+ text,
2416
+ anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
2417
+ });
2418
+ }
2419
+ if (seats.length <= SEAT_LABEL_MAX) {
2420
+ for (let i = 0; i < seats.length; i++) {
2421
+ const s = seats[i];
2422
+ const text = s.displayLabel || s.label;
2423
+ if (!text) continue;
2424
+ labels.push({
2425
+ id: `seat:${s.id}`,
2426
+ kind: "seat",
2427
+ text,
2428
+ anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M]
2200
2429
  });
2201
2430
  }
2202
2431
  }
@@ -2274,6 +2503,7 @@ function buildSceneModel(input) {
2274
2503
  // Look-at target ~1.5 m up so a seated camera aims slightly down at the stage.
2275
2504
  focalWorld: [focal.x * M, 1.5, focal.y * M],
2276
2505
  zones,
2506
+ sections,
2277
2507
  labels,
2278
2508
  floors
2279
2509
  };
@@ -2285,8 +2515,19 @@ var SEPARATION_Y_PX = 20;
2285
2515
  var KIND_STYLE = {
2286
2516
  zone: { size: 15, weight: "600", opacity: 0.95 },
2287
2517
  section: { size: 12, weight: "500", opacity: 0.88 },
2518
+ ga: { size: 12, weight: "500", opacity: 0.88 },
2288
2519
  booth: { size: 11, weight: "500", opacity: 0.85 },
2289
- annotation: { size: 11, weight: "400", opacity: 0.75 }
2520
+ annotation: { size: 11, weight: "400", opacity: 0.75 },
2521
+ // Row and seat identity are quieter than the structure they sit inside: at
2522
+ // this range the venue is already understood and the label is a detail, so it
2523
+ // must not compete with the seating it is printed over.
2524
+ row: { size: 10.5, weight: "600", opacity: 0.8 },
2525
+ seat: { size: 9.5, weight: "500", opacity: 0.72 }
2526
+ };
2527
+ var DENSE_KINDS = /* @__PURE__ */ new Set(["row", "seat"]);
2528
+ var DENSE_SEPARATION = {
2529
+ row: { x: 62, y: 16 },
2530
+ seat: { x: 24, y: 13 }
2290
2531
  };
2291
2532
  var LabelOverlay = class {
2292
2533
  constructor(container, opts = {}) {
@@ -2327,7 +2568,15 @@ var LabelOverlay = class {
2327
2568
  if (!screen.visible) continue;
2328
2569
  candidates.push({ label, screen });
2329
2570
  }
2330
- const kept = cullOverlapping(candidates, SEPARATION_X_PX, SEPARATION_Y_PX);
2571
+ const structure = candidates.filter((c) => !DENSE_KINDS.has(c.label.kind));
2572
+ const kept = [
2573
+ ...cullOverlapping(structure, SEPARATION_X_PX, SEPARATION_Y_PX),
2574
+ ...["row", "seat"].flatMap((kind) => cullOverlapping(
2575
+ candidates.filter((c) => c.label.kind === kind),
2576
+ DENSE_SEPARATION[kind].x,
2577
+ DENSE_SEPARATION[kind].y
2578
+ ))
2579
+ ];
2331
2580
  const keptIds = new Set(kept.map((k) => k.label.id));
2332
2581
  for (const { label, screen } of kept) {
2333
2582
  const node = this.nodeFor(label);
@@ -3683,6 +3932,20 @@ function mountVenue3D(container, input, opts = {}) {
3683
3932
  loop.requestRender();
3684
3933
  return true;
3685
3934
  },
3935
+ sections() {
3936
+ return model.sections;
3937
+ },
3938
+ focusSection(sectionId) {
3939
+ const sec = model.sections.find((s) => s.id === sectionId);
3940
+ if (!sec || sec.seatCount === 0) return false;
3941
+ cinematic.cancel();
3942
+ const dx = sec.focalWorld[0] - sec.center[0];
3943
+ const dz = sec.focalWorld[2] - sec.center[2];
3944
+ const azimuth = Math.hypot(dx, dz) > sec.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
3945
+ orbit.frame({ center: sec.center, radius: sec.radius * 1.45 }, false, azimuth);
3946
+ loop.requestRender();
3947
+ return true;
3948
+ },
3686
3949
  setReducedMotionForTest(value) {
3687
3950
  reducedForced = value;
3688
3951
  },