@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.js
CHANGED
|
@@ -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.
|
|
468
|
-
this.
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
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.
|
|
476
|
-
this.
|
|
477
|
-
this.
|
|
478
|
-
this.
|
|
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:
|
|
486
|
-
normal:
|
|
487
|
-
color:
|
|
488
|
-
floor:
|
|
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)
|
|
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
|
|
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 (
|
|
1276
|
-
|
|
1277
|
-
for (const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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: [
|
|
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
|
|
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
|
},
|