@seatlayer/core 0.50.2 → 0.52.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/{chunk-GD267BFB.js → chunk-UID7KN44.js} +75 -2
- package/dist/chunk-UID7KN44.js.map +1 -0
- package/dist/core/seatConfidence.d.cts +1 -1
- package/dist/core/seatConfidence.d.ts +1 -1
- package/dist/de-YGODYJ3E.js +292 -0
- package/dist/de-YGODYJ3E.js.map +1 -0
- package/dist/es-VMBTGVFQ.js +292 -0
- package/dist/es-VMBTGVFQ.js.map +1 -0
- package/dist/fr-SLBE2ULB.js +292 -0
- package/dist/fr-SLBE2ULB.js.map +1 -0
- package/dist/index.cjs +1606 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -17
- package/dist/index.d.ts +138 -17
- package/dist/index.js +829 -65
- package/dist/index.js.map +1 -1
- package/dist/picker/minimapGeometry.cjs +80 -0
- package/dist/picker/minimapGeometry.cjs.map +1 -0
- package/dist/picker/minimapGeometry.d.cts +37 -0
- package/dist/picker/minimapGeometry.d.ts +37 -0
- package/dist/picker/minimapGeometry.js +52 -0
- package/dist/picker/minimapGeometry.js.map +1 -0
- package/dist/{types-DR54nqKL.d.cts → types-tLOunI-B.d.cts} +131 -1
- package/dist/{types-DR54nqKL.d.ts → types-tLOunI-B.d.ts} +131 -1
- package/dist/view3d/crossfade/panorama.d.cts +1 -1
- package/dist/view3d/crossfade/panorama.d.ts +1 -1
- package/dist/view3d/index.cjs +1158 -123
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +20 -12
- package/dist/view3d/index.d.ts +20 -12
- package/dist/view3d/index.js +1158 -122
- package/dist/view3d/index.js.map +1 -1
- package/package.json +14 -1
- package/dist/chunk-GD267BFB.js.map +0 -1
- package/dist/de-ST4H423D.js +0 -56
- package/dist/de-ST4H423D.js.map +0 -1
- package/dist/es-WEC5NHRT.js +0 -56
- package/dist/es-WEC5NHRT.js.map +0 -1
- package/dist/fr-SGBFNAQV.js +0 -56
- package/dist/fr-SGBFNAQV.js.map +0 -1
package/dist/view3d/index.js
CHANGED
|
@@ -4,10 +4,11 @@ import {
|
|
|
4
4
|
SEATED_EYE_HEIGHT_M,
|
|
5
5
|
accessibilityRingColor,
|
|
6
6
|
distanceToPolyline,
|
|
7
|
+
fitCircle,
|
|
7
8
|
pointInPolygonWithHoles,
|
|
8
9
|
resolveSection,
|
|
9
10
|
sectionGeometry
|
|
10
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-UID7KN44.js";
|
|
11
12
|
import {
|
|
12
13
|
MAX_PITCH_DEG,
|
|
13
14
|
VFOV_DEG,
|
|
@@ -455,7 +456,7 @@ var OrbitCamera = class {
|
|
|
455
456
|
* then the damped `update()` eases it up into the 3/4 architectural angle and
|
|
456
457
|
* dollies in — the venue "stands up" instead of teleporting (~600ms).
|
|
457
458
|
*/
|
|
458
|
-
frame(bounds, intro = false, stageAzimuth, portraitCrop = false) {
|
|
459
|
+
frame(bounds, intro = false, stageAzimuth, portraitCrop = false, framing) {
|
|
459
460
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
460
461
|
this.targetT.copy(this.target);
|
|
461
462
|
this.panAnchor.copy(this.target);
|
|
@@ -465,9 +466,9 @@ var OrbitCamera = class {
|
|
|
465
466
|
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
466
467
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
467
468
|
this.azT = stageAzimuth ?? -30 * DEG;
|
|
468
|
-
this.polT = 55 * DEG;
|
|
469
|
+
this.polT = Math.max(15, Math.min(80, framing?.polarDeg ?? 55)) * DEG;
|
|
469
470
|
const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
|
|
470
|
-
this.distT = fit * FRAME_MARGIN;
|
|
471
|
+
this.distT = fit * Math.max(1.05, Math.min(2, framing?.margin ?? FRAME_MARGIN));
|
|
471
472
|
this.minDist = Math.max(2, r * 0.12);
|
|
472
473
|
this.maxDist = fit * 4;
|
|
473
474
|
if (intro) {
|
|
@@ -489,7 +490,7 @@ var OrbitCamera = class {
|
|
|
489
490
|
* seat, behind the shell, anywhere): re-pivot on the venue centre without
|
|
490
491
|
* moving the camera, then glide targets back to the 3/4 architectural pose.
|
|
491
492
|
*/
|
|
492
|
-
frameSoft(bounds, stageAzimuth, portraitCrop = false) {
|
|
493
|
+
frameSoft(bounds, stageAzimuth, portraitCrop = false, framing) {
|
|
493
494
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
494
495
|
this.targetT.copy(this.target);
|
|
495
496
|
this.panAnchor.copy(this.target);
|
|
@@ -500,9 +501,9 @@ var OrbitCamera = class {
|
|
|
500
501
|
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
501
502
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
502
503
|
this.azT = stageAzimuth ?? this.azimuth;
|
|
503
|
-
this.polT = 55 * DEG;
|
|
504
|
+
this.polT = Math.max(15, Math.min(80, framing?.polarDeg ?? 55)) * DEG;
|
|
504
505
|
const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
|
|
505
|
-
this.distT = fit * FRAME_MARGIN;
|
|
506
|
+
this.distT = fit * Math.max(1.05, Math.min(2, framing?.margin ?? FRAME_MARGIN));
|
|
506
507
|
}
|
|
507
508
|
/**
|
|
508
509
|
* Distance from the bounds centre at which the venue fits the frustum at
|
|
@@ -518,14 +519,14 @@ var OrbitCamera = class {
|
|
|
518
519
|
* every corner does. Clamped to the sphere fit (which always contains the
|
|
519
520
|
* box) and floored so a degenerate box cannot glue the camera to the floor.
|
|
520
521
|
*/
|
|
521
|
-
fitDistance(bounds, halfV, halfH, azimuth,
|
|
522
|
+
fitDistance(bounds, halfV, halfH, azimuth, polar2) {
|
|
522
523
|
const r = Math.max(1, bounds.radius);
|
|
523
524
|
const sphereFit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
524
525
|
const box = bounds.box;
|
|
525
526
|
if (!box) return sphereFit;
|
|
526
|
-
const sp = Math.sin(
|
|
527
|
+
const sp = Math.sin(polar2);
|
|
527
528
|
const fx = -sp * Math.sin(azimuth);
|
|
528
|
-
const fy = -Math.cos(
|
|
529
|
+
const fy = -Math.cos(polar2);
|
|
529
530
|
const fz = -sp * Math.cos(azimuth);
|
|
530
531
|
if (Math.abs(fy) > 0.999) return sphereFit;
|
|
531
532
|
let rx = -fz, rz = fx;
|
|
@@ -602,9 +603,9 @@ var OrbitCamera = class {
|
|
|
602
603
|
const dy = this.camera.position.y - this.target.y;
|
|
603
604
|
const dz = this.camera.position.z - this.target.z;
|
|
604
605
|
const dist = Math.hypot(dx, dy, dz) || 1;
|
|
605
|
-
const
|
|
606
|
+
const polar2 = Math.max(POLAR_MIN, Math.min(POLAR_MAX, Math.acos(Math.max(-1, Math.min(1, dy / dist)))));
|
|
606
607
|
this.distance = this.distT = dist;
|
|
607
|
-
this.polar = this.polT =
|
|
608
|
+
this.polar = this.polT = polar2;
|
|
608
609
|
this.azimuth = this.azT = Math.atan2(dx, dz);
|
|
609
610
|
}
|
|
610
611
|
/** Point the orbit pivot at a new world target without moving the camera. */
|
|
@@ -710,6 +711,20 @@ function computeSeatLod(distance, radius) {
|
|
|
710
711
|
minPixels: SEAT_MIN_PIXELS_NEAR + t * (SEAT_MIN_PIXELS_FAR - SEAT_MIN_PIXELS_NEAR)
|
|
711
712
|
};
|
|
712
713
|
}
|
|
714
|
+
function floorOverviewSeatDetailVisible(seatCount, viewportWidth, viewportHeight) {
|
|
715
|
+
if (!(seatCount > 0) || !(viewportWidth > 0) || !(viewportHeight > 0)) return false;
|
|
716
|
+
const pixelsPerSeat = viewportWidth * viewportHeight / seatCount;
|
|
717
|
+
return pixelsPerSeat >= 64;
|
|
718
|
+
}
|
|
719
|
+
function projectedSeatDetailVisible(seatCount, venueRadius, cameraDistance, fovYDeg, viewportHeightPx) {
|
|
720
|
+
if (!(seatCount > 0) || !(venueRadius > 0) || !(cameraDistance > 0)) return false;
|
|
721
|
+
if (!(fovYDeg > 0) || !(viewportHeightPx > 0)) return false;
|
|
722
|
+
const halfFov = Math.tan(fovYDeg / 2 * Math.PI / 180);
|
|
723
|
+
if (!(halfFov > 0)) return false;
|
|
724
|
+
const rPx = venueRadius / (cameraDistance * halfFov) * (viewportHeightPx / 2);
|
|
725
|
+
const projectedArea = Math.PI * rPx * rPx;
|
|
726
|
+
return projectedArea / seatCount >= 64;
|
|
727
|
+
}
|
|
713
728
|
|
|
714
729
|
// src/view3d/scene/nearField.ts
|
|
715
730
|
var SEATS_PER_CELL = 4;
|
|
@@ -1661,17 +1676,45 @@ function themeSeatColorLUT(theme, order) {
|
|
|
1661
1676
|
}
|
|
1662
1677
|
|
|
1663
1678
|
// src/view3d/labels.ts
|
|
1679
|
+
function labelsForFloorView(labels, floorCount, focusedFloor) {
|
|
1680
|
+
if (focusedFloor < 0) {
|
|
1681
|
+
return floorCount > 1 ? labels.filter((label) => label.kind === "floor") : [...labels];
|
|
1682
|
+
}
|
|
1683
|
+
return labels.filter((label) => label.floorIndex === void 0 || label.floorIndex === focusedFloor);
|
|
1684
|
+
}
|
|
1685
|
+
function spreadFloorLabels(items, viewportWidth) {
|
|
1686
|
+
const ordered = [...items].sort((left, right) => left.screen.y - right.screen.y);
|
|
1687
|
+
const clusters = [];
|
|
1688
|
+
for (const item of ordered) {
|
|
1689
|
+
const cluster = clusters.find((candidate) => candidate.some((member) => Math.abs(member.screen.x - item.screen.x) < 104 && Math.abs(member.screen.y - item.screen.y) < 34));
|
|
1690
|
+
if (cluster) cluster.push(item);
|
|
1691
|
+
else clusters.push([item]);
|
|
1692
|
+
}
|
|
1693
|
+
return clusters.flatMap((cluster) => {
|
|
1694
|
+
if (cluster.length < 2) return cluster;
|
|
1695
|
+
return cluster.map((item, index) => ({
|
|
1696
|
+
...item,
|
|
1697
|
+
screen: {
|
|
1698
|
+
...item.screen,
|
|
1699
|
+
x: Math.max(54, Math.min(viewportWidth - 54, item.screen.x + (index - (cluster.length - 1) / 2) * 104))
|
|
1700
|
+
}
|
|
1701
|
+
}));
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1664
1704
|
var ZONE_MIN_DISTANCE = 1.15;
|
|
1665
|
-
var SECTION_MAX_DISTANCE =
|
|
1705
|
+
var SECTION_MAX_DISTANCE = 3.25;
|
|
1706
|
+
var SECTION_MAX_DISTANCE_NO_ZONES = 4.5;
|
|
1666
1707
|
var NEAR_MAX_DISTANCE = 0.85;
|
|
1667
1708
|
var ROW_MAX_DISTANCE = 0.5;
|
|
1668
1709
|
var SEAT_MAX_DISTANCE = 0.26;
|
|
1669
|
-
function visibleLabelKinds(distance, venueRadius) {
|
|
1710
|
+
function visibleLabelKinds(distance, venueRadius, opts) {
|
|
1670
1711
|
const r = Math.max(1e-6, venueRadius);
|
|
1671
1712
|
const d = distance / r;
|
|
1713
|
+
const hasZones = opts?.hasZoneLabels ?? true;
|
|
1672
1714
|
const out = /* @__PURE__ */ new Set();
|
|
1715
|
+
if (d >= 0.75) out.add("floor");
|
|
1673
1716
|
if (d >= ZONE_MIN_DISTANCE) out.add("zone");
|
|
1674
|
-
if (d <= SECTION_MAX_DISTANCE) {
|
|
1717
|
+
if (d <= (hasZones ? SECTION_MAX_DISTANCE : SECTION_MAX_DISTANCE_NO_ZONES)) {
|
|
1675
1718
|
out.add("section");
|
|
1676
1719
|
out.add("ga");
|
|
1677
1720
|
}
|
|
@@ -2455,7 +2498,7 @@ function pointInRing2(ring, x, y) {
|
|
|
2455
2498
|
return inside;
|
|
2456
2499
|
}
|
|
2457
2500
|
var MAX_TIER_RISE_M = 25;
|
|
2458
|
-
function buildVenueSurfaces(units, seats) {
|
|
2501
|
+
function buildVenueSurfaces(units, seats, seatFloorIndices) {
|
|
2459
2502
|
const bySection = /* @__PURE__ */ new Map();
|
|
2460
2503
|
const seatOwner = new Array(seats.length).fill(null);
|
|
2461
2504
|
const seatDeck = new Float64Array(seats.length);
|
|
@@ -2464,18 +2507,20 @@ function buildVenueSurfaces(units, seats) {
|
|
|
2464
2507
|
const acc = /* @__PURE__ */ new Map();
|
|
2465
2508
|
const boxes = [];
|
|
2466
2509
|
const tableRowIds = /* @__PURE__ */ new Set();
|
|
2467
|
-
for (
|
|
2510
|
+
for (let floorIndex = 0; floorIndex < units.length; floorIndex += 1) {
|
|
2511
|
+
const unit = units[floorIndex];
|
|
2468
2512
|
for (const o of unit.objects) {
|
|
2469
2513
|
if (o.type === "table") tableRowIds.add(o.id);
|
|
2470
2514
|
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
2471
2515
|
const owned = outsetRing(o.outline, SEAT_OWNERSHIP_PAD_U);
|
|
2472
2516
|
acc.set(o.id, { section: o, unit, frontU: Infinity, hasSeats: false, rows: /* @__PURE__ */ new Map(), seatIndices: [] });
|
|
2473
|
-
boxes.push({ id: o.id, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
2517
|
+
boxes.push({ id: o.id, floorIndex, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
2474
2518
|
}
|
|
2475
2519
|
}
|
|
2476
2520
|
for (let i = 0; i < seats.length; i++) {
|
|
2477
2521
|
const s = seats[i];
|
|
2478
2522
|
for (const b of boxes) {
|
|
2523
|
+
if (seatFloorIndices && b.floorIndex !== seatFloorIndices[i]) continue;
|
|
2479
2524
|
if (s.x < b.box.minX || s.x > b.box.maxX || s.y < b.box.minY || s.y > b.box.maxY) continue;
|
|
2480
2525
|
if (!pointInPolygonWithHoles({ x: s.x, y: s.y }, b.outline, b.section.holes)) continue;
|
|
2481
2526
|
seatOwner[i] = b.id;
|
|
@@ -2633,6 +2678,634 @@ function buildVenueSurfaces(units, seats) {
|
|
|
2633
2678
|
};
|
|
2634
2679
|
}
|
|
2635
2680
|
|
|
2681
|
+
// src/core/bowlProfile.ts
|
|
2682
|
+
var DEG2 = Math.PI / 180;
|
|
2683
|
+
var BIN_COUNT = 96;
|
|
2684
|
+
var GAP_FILL_BINS = 6;
|
|
2685
|
+
var MIN_ARC_DEG = 60;
|
|
2686
|
+
var MIN_RING_DEG = 150;
|
|
2687
|
+
var MIN_HOLE_RATIO = 0.2;
|
|
2688
|
+
var RING_OVERLAP_RATIO = 0.35;
|
|
2689
|
+
function bearingDeg(c, p) {
|
|
2690
|
+
const d = Math.atan2(p.x - c.x, p.y - c.y) / DEG2;
|
|
2691
|
+
return d < 0 ? d + 360 : d;
|
|
2692
|
+
}
|
|
2693
|
+
function median(values) {
|
|
2694
|
+
if (!values.length) return NaN;
|
|
2695
|
+
const s = [...values].sort((a, b) => a - b);
|
|
2696
|
+
return s[Math.floor(s.length / 2)];
|
|
2697
|
+
}
|
|
2698
|
+
function smoothRun(values, wrap, window2 = 2) {
|
|
2699
|
+
const n = values.length;
|
|
2700
|
+
const out = new Float64Array(n);
|
|
2701
|
+
for (let i = 0; i < n; i++) {
|
|
2702
|
+
let sum = 0, cnt = 0;
|
|
2703
|
+
for (let k = -window2; k <= window2; k++) {
|
|
2704
|
+
let j = i + k;
|
|
2705
|
+
if (wrap) j = (j % n + n) % n;
|
|
2706
|
+
else if (j < 0 || j >= n) continue;
|
|
2707
|
+
sum += values[j];
|
|
2708
|
+
cnt++;
|
|
2709
|
+
}
|
|
2710
|
+
out[i] = sum / cnt;
|
|
2711
|
+
}
|
|
2712
|
+
return out;
|
|
2713
|
+
}
|
|
2714
|
+
function fitBowlProfile(sections, focal) {
|
|
2715
|
+
const usable = sections.filter((s) => s.rows.length >= 1);
|
|
2716
|
+
if (usable.length < 1) return null;
|
|
2717
|
+
const centres = [];
|
|
2718
|
+
let cx = 0, cy = 0, np = 0;
|
|
2719
|
+
for (const s of usable) {
|
|
2720
|
+
for (const row of s.rows) {
|
|
2721
|
+
for (const p of row.pts) {
|
|
2722
|
+
cx += p.x;
|
|
2723
|
+
cy += p.y;
|
|
2724
|
+
np++;
|
|
2725
|
+
}
|
|
2726
|
+
if (row.pts.length >= 4) {
|
|
2727
|
+
const c = fitCircle(row.pts);
|
|
2728
|
+
if (c && Number.isFinite(c.cx) && Number.isFinite(c.cy)) centres.push({ x: c.cx, y: c.cy });
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (!np) return null;
|
|
2733
|
+
const centroid2 = { x: cx / np, y: cy / np };
|
|
2734
|
+
let centre = centroid2;
|
|
2735
|
+
if (centres.length >= 8) {
|
|
2736
|
+
const mx = median(centres.map((c) => c.x));
|
|
2737
|
+
const my = median(centres.map((c) => c.y));
|
|
2738
|
+
let spread = 0;
|
|
2739
|
+
for (const s of usable) for (const row of s.rows) for (const p of row.pts) {
|
|
2740
|
+
const d = Math.hypot(p.x - centroid2.x, p.y - centroid2.y);
|
|
2741
|
+
if (d > spread) spread = d;
|
|
2742
|
+
}
|
|
2743
|
+
let near = 0;
|
|
2744
|
+
const nearR = spread * 0.2;
|
|
2745
|
+
for (const s of usable) for (const row of s.rows) for (const p of row.pts) {
|
|
2746
|
+
if (Math.hypot(p.x - mx, p.y - my) < nearR) near++;
|
|
2747
|
+
}
|
|
2748
|
+
if (Math.hypot(mx - centroid2.x, my - centroid2.y) <= spread && near / np < 0.01) centre = { x: mx, y: my };
|
|
2749
|
+
}
|
|
2750
|
+
const stats = [];
|
|
2751
|
+
for (const s of usable) {
|
|
2752
|
+
const st = { sectionId: s.sectionId, rMin: Infinity, rMax: -Infinity, samples: [] };
|
|
2753
|
+
for (const row of s.rows) {
|
|
2754
|
+
for (const p of row.pts) {
|
|
2755
|
+
const r = Math.hypot(p.x - centre.x, p.y - centre.y);
|
|
2756
|
+
const bin = Math.min(BIN_COUNT - 1, Math.floor(bearingDeg(centre, p) / (360 / BIN_COUNT)));
|
|
2757
|
+
st.samples.push({ r, bin, levelM: row.levelM });
|
|
2758
|
+
if (r < st.rMin) st.rMin = r;
|
|
2759
|
+
if (r > st.rMax) st.rMax = r;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (st.samples.length) stats.push(st);
|
|
2763
|
+
}
|
|
2764
|
+
if (!stats.length) return null;
|
|
2765
|
+
stats.sort((a, b) => a.rMin + a.rMax - (b.rMin + b.rMax));
|
|
2766
|
+
const ringAccs = [];
|
|
2767
|
+
for (const st of stats) {
|
|
2768
|
+
let joined = false;
|
|
2769
|
+
for (const ring of ringAccs) {
|
|
2770
|
+
const overlap = Math.min(st.rMax, ring.rMax) - Math.max(st.rMin, ring.rMin);
|
|
2771
|
+
const thinner = Math.min(st.rMax - st.rMin, ring.rMax - ring.rMin);
|
|
2772
|
+
if (thinner > 1e-9 && overlap >= RING_OVERLAP_RATIO * thinner) {
|
|
2773
|
+
ring.members.push(st);
|
|
2774
|
+
ring.rMin = Math.min(ring.rMin, st.rMin);
|
|
2775
|
+
ring.rMax = Math.max(ring.rMax, st.rMax);
|
|
2776
|
+
joined = true;
|
|
2777
|
+
break;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (!joined) ringAccs.push({ members: [st], rMin: st.rMin, rMax: st.rMax });
|
|
2781
|
+
}
|
|
2782
|
+
const rings = [];
|
|
2783
|
+
for (const ring of ringAccs) {
|
|
2784
|
+
const bins = new Array(BIN_COUNT).fill(null);
|
|
2785
|
+
for (const st of ring.members) {
|
|
2786
|
+
for (const s of st.samples) {
|
|
2787
|
+
let b = bins[s.bin];
|
|
2788
|
+
if (!b) {
|
|
2789
|
+
b = { samples: [] };
|
|
2790
|
+
bins[s.bin] = b;
|
|
2791
|
+
}
|
|
2792
|
+
b.samples.push({ r: s.r, levelM: s.levelM });
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
const covered = bins.map((b) => !!b);
|
|
2796
|
+
const coveredCount = covered.filter(Boolean).length;
|
|
2797
|
+
if (!coveredCount) continue;
|
|
2798
|
+
const innerSeed = [];
|
|
2799
|
+
const outerSeed = [];
|
|
2800
|
+
for (const b of bins) {
|
|
2801
|
+
if (!b) continue;
|
|
2802
|
+
let lo = Infinity, hi = -Infinity;
|
|
2803
|
+
for (const s of b.samples) {
|
|
2804
|
+
if (s.r < lo) lo = s.r;
|
|
2805
|
+
if (s.r > hi) hi = s.r;
|
|
2806
|
+
}
|
|
2807
|
+
innerSeed.push(lo);
|
|
2808
|
+
outerSeed.push(hi);
|
|
2809
|
+
}
|
|
2810
|
+
const innerRMedian = median(innerSeed);
|
|
2811
|
+
const outerRMedian = median(outerSeed);
|
|
2812
|
+
if (!(innerRMedian >= MIN_HOLE_RATIO * outerRMedian) || !(outerRMedian > 0)) continue;
|
|
2813
|
+
const sortedInner = [...innerSeed].sort((a, b) => a - b);
|
|
2814
|
+
const innerP10 = sortedInner[Math.floor(sortedInner.length * 0.1)];
|
|
2815
|
+
if (!(innerP10 >= MIN_HOLE_RATIO * outerRMedian)) continue;
|
|
2816
|
+
const vals = bins.map((b) => {
|
|
2817
|
+
if (!b) return null;
|
|
2818
|
+
const sorted = [...b.samples].sort((u, v) => u.r - v.r);
|
|
2819
|
+
const inner = sorted[0].r;
|
|
2820
|
+
const outer = sorted[sorted.length - 1].r;
|
|
2821
|
+
const front = sorted[0].levelM;
|
|
2822
|
+
let top = -Infinity;
|
|
2823
|
+
for (const s of sorted) if (s.levelM > top) top = s.levelM;
|
|
2824
|
+
const hull = [];
|
|
2825
|
+
for (const s of sorted) {
|
|
2826
|
+
const pt = { r: s.r, l: s.levelM };
|
|
2827
|
+
while (hull.length && Math.abs(hull[hull.length - 1].r - pt.r) < 1e-9) {
|
|
2828
|
+
if (hull[hull.length - 1].l <= pt.l) {
|
|
2829
|
+
pt.skip = true;
|
|
2830
|
+
break;
|
|
2831
|
+
}
|
|
2832
|
+
hull.pop();
|
|
2833
|
+
}
|
|
2834
|
+
if (pt.skip) continue;
|
|
2835
|
+
while (hull.length >= 2) {
|
|
2836
|
+
const a = hull[hull.length - 2], b2 = hull[hull.length - 1];
|
|
2837
|
+
if ((b2.r - a.r) * (pt.l - a.l) - (b2.l - a.l) * (pt.r - a.r) <= 0) hull.pop();
|
|
2838
|
+
else break;
|
|
2839
|
+
}
|
|
2840
|
+
hull.push(pt);
|
|
2841
|
+
}
|
|
2842
|
+
const rs = hull.map((p) => p.r);
|
|
2843
|
+
const ls = hull.map((p) => p.l);
|
|
2844
|
+
return { inner, outer, front, top, profile: { rs, ls } };
|
|
2845
|
+
});
|
|
2846
|
+
const filled = covered.slice();
|
|
2847
|
+
for (let i = 0; i < BIN_COUNT; i++) {
|
|
2848
|
+
if (filled[i]) continue;
|
|
2849
|
+
let before = -1, after = -1, gap = 0;
|
|
2850
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
2851
|
+
const j = (i - k + BIN_COUNT * 2) % BIN_COUNT;
|
|
2852
|
+
if (filled[j]) {
|
|
2853
|
+
before = j;
|
|
2854
|
+
break;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
2858
|
+
const j = (i + k) % BIN_COUNT;
|
|
2859
|
+
if (filled[j]) {
|
|
2860
|
+
after = j;
|
|
2861
|
+
gap = k;
|
|
2862
|
+
break;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (before < 0 || after < 0) continue;
|
|
2866
|
+
let back = 0;
|
|
2867
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
2868
|
+
const j = (i - k + BIN_COUNT * 2) % BIN_COUNT;
|
|
2869
|
+
if (covered[j]) {
|
|
2870
|
+
back = k;
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
const gapLen = back - 1 + gap;
|
|
2875
|
+
if (gapLen > GAP_FILL_BINS) continue;
|
|
2876
|
+
const a = vals[before], b = vals[after];
|
|
2877
|
+
const t = back / (back + gap);
|
|
2878
|
+
const inner = a.inner + (b.inner - a.inner) * t;
|
|
2879
|
+
const outer = a.outer + (b.outer - a.outer) * t;
|
|
2880
|
+
const front = a.front + (b.front - a.front) * t;
|
|
2881
|
+
const top = a.top + (b.top - a.top) * t;
|
|
2882
|
+
vals[i] = { inner, outer, front, top, profile: { rs: [inner, outer], ls: [front, top] } };
|
|
2883
|
+
}
|
|
2884
|
+
for (let i = 0; i < BIN_COUNT; i++) if (vals[i]) filled[i] = true;
|
|
2885
|
+
const stepDeg = 360 / BIN_COUNT;
|
|
2886
|
+
const arcs = [];
|
|
2887
|
+
const filledCount = filled.filter(Boolean).length;
|
|
2888
|
+
const runs = [];
|
|
2889
|
+
if (filledCount === BIN_COUNT) {
|
|
2890
|
+
runs.push({ start: 0, len: BIN_COUNT });
|
|
2891
|
+
} else {
|
|
2892
|
+
let scan = 0;
|
|
2893
|
+
while (filled[scan]) scan = (scan + 1) % BIN_COUNT;
|
|
2894
|
+
let runStart = -1;
|
|
2895
|
+
for (let k = 0; k <= BIN_COUNT; k++) {
|
|
2896
|
+
const j = (scan + k) % BIN_COUNT;
|
|
2897
|
+
const on = k < BIN_COUNT && filled[j];
|
|
2898
|
+
if (on && runStart < 0) runStart = j;
|
|
2899
|
+
else if (!on && runStart >= 0) {
|
|
2900
|
+
const len = (j - runStart + BIN_COUNT) % BIN_COUNT || BIN_COUNT;
|
|
2901
|
+
runs.push({ start: runStart, len });
|
|
2902
|
+
runStart = -1;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
for (const run of runs) {
|
|
2907
|
+
if (run.len * stepDeg < MIN_ARC_DEG && run.len < BIN_COUNT) continue;
|
|
2908
|
+
const closed = run.len === BIN_COUNT;
|
|
2909
|
+
const innerR = new Float64Array(run.len);
|
|
2910
|
+
const outerR = new Float64Array(run.len);
|
|
2911
|
+
const frontL = new Float64Array(run.len);
|
|
2912
|
+
const topL = new Float64Array(run.len);
|
|
2913
|
+
const profiles = [];
|
|
2914
|
+
for (let k = 0; k < run.len; k++) {
|
|
2915
|
+
const v = vals[(run.start + k) % BIN_COUNT];
|
|
2916
|
+
innerR[k] = v.inner;
|
|
2917
|
+
outerR[k] = v.outer;
|
|
2918
|
+
frontL[k] = v.front;
|
|
2919
|
+
topL[k] = v.top;
|
|
2920
|
+
profiles.push(v.profile ?? { rs: [v.inner, v.outer], ls: [v.front, v.top] });
|
|
2921
|
+
}
|
|
2922
|
+
const sInner = smoothRun(innerR, closed);
|
|
2923
|
+
const sOuter = smoothRun(outerR, closed);
|
|
2924
|
+
const sFront = smoothRun(frontL, closed);
|
|
2925
|
+
const sTop = smoothRun(topL, closed);
|
|
2926
|
+
for (let k = 0; k < run.len; k++) {
|
|
2927
|
+
if (sInner[k] > innerR[k]) sInner[k] = innerR[k];
|
|
2928
|
+
if (sOuter[k] < outerR[k]) sOuter[k] = outerR[k];
|
|
2929
|
+
if (sInner[k] > sOuter[k] - 1e-6) sInner[k] = Math.max(0, sOuter[k] - 1e-6);
|
|
2930
|
+
if (sTop[k] < sFront[k]) sTop[k] = sFront[k];
|
|
2931
|
+
}
|
|
2932
|
+
const arc = { startBin: run.start, binCount: run.len, closed, innerR: sInner, outerR: sOuter, frontLevelM: sFront, topLevelM: sTop, profiles };
|
|
2933
|
+
const clampProfile = (k, r, levelM) => {
|
|
2934
|
+
const prof = profiles[k];
|
|
2935
|
+
const rs = prof.rs, ls = prof.ls;
|
|
2936
|
+
if (r <= rs[0]) {
|
|
2937
|
+
if (ls[0] > levelM) ls[0] = levelM;
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (r >= rs[rs.length - 1]) {
|
|
2941
|
+
if (ls[ls.length - 1] > levelM) ls[ls.length - 1] = levelM;
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
for (let j = 0; j + 1 < rs.length; j++) {
|
|
2945
|
+
if (r <= rs[j + 1]) {
|
|
2946
|
+
if (ls[j] > levelM) ls[j] = levelM;
|
|
2947
|
+
if (ls[j + 1] > levelM) ls[j + 1] = levelM;
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
for (let k = 0; k < run.len; k++) {
|
|
2953
|
+
const b = bins[(run.start + k) % BIN_COUNT];
|
|
2954
|
+
if (!b) continue;
|
|
2955
|
+
for (const s of b.samples) {
|
|
2956
|
+
clampProfile(k, s.r, s.levelM);
|
|
2957
|
+
const prev = closed ? (k - 1 + run.len) % run.len : k - 1;
|
|
2958
|
+
const next = closed ? (k + 1) % run.len : k + 1;
|
|
2959
|
+
if (prev >= 0) clampProfile(prev, s.r, s.levelM);
|
|
2960
|
+
if (next < run.len) clampProfile(next, s.r, s.levelM);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
arcs.push(arc);
|
|
2964
|
+
}
|
|
2965
|
+
const totalDeg = arcs.reduce((sum, a) => sum + a.binCount * stepDeg, 0);
|
|
2966
|
+
if (totalDeg < MIN_RING_DEG) continue;
|
|
2967
|
+
rings.push({
|
|
2968
|
+
arcs,
|
|
2969
|
+
sectionIds: ring.members.map((m) => m.sectionId),
|
|
2970
|
+
innerRMedian,
|
|
2971
|
+
outerRMedian
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
if (!rings.length) return null;
|
|
2975
|
+
rings.sort((a, b) => a.innerRMedian - b.innerRMedian);
|
|
2976
|
+
if (focal) {
|
|
2977
|
+
const outermost = rings[rings.length - 1].outerRMedian;
|
|
2978
|
+
if (Math.hypot(focal.x - centre.x, focal.y - centre.y) > 0.9 * outermost) return null;
|
|
2979
|
+
}
|
|
2980
|
+
return { centre, binCount: BIN_COUNT, binStepDeg: 360 / BIN_COUNT, rings };
|
|
2981
|
+
}
|
|
2982
|
+
function arcLevelAt(fit, arc, bearing, r) {
|
|
2983
|
+
const step = fit.binStepDeg;
|
|
2984
|
+
let f = bearing / step - 0.5 - arc.startBin;
|
|
2985
|
+
f = (f % fit.binCount + fit.binCount) % fit.binCount;
|
|
2986
|
+
if (f > arc.binCount - 1) {
|
|
2987
|
+
if (arc.closed) f = f - arc.binCount < arc.binCount - f ? 0 : arc.binCount - 1;
|
|
2988
|
+
else f = f - (arc.binCount - 1) < fit.binCount - f ? arc.binCount - 1 : 0;
|
|
2989
|
+
}
|
|
2990
|
+
const i0 = Math.floor(f);
|
|
2991
|
+
const i1 = arc.closed ? (i0 + 1) % arc.binCount : Math.min(arc.binCount - 1, i0 + 1);
|
|
2992
|
+
const t = f - i0;
|
|
2993
|
+
const sample = (i) => {
|
|
2994
|
+
const prof = arc.profiles[i];
|
|
2995
|
+
const rs = prof.rs, ls = prof.ls;
|
|
2996
|
+
if (r <= rs[0]) return ls[0];
|
|
2997
|
+
if (r >= rs[rs.length - 1]) return ls[ls.length - 1];
|
|
2998
|
+
for (let k = 0; k + 1 < rs.length; k++) {
|
|
2999
|
+
if (r <= rs[k + 1]) {
|
|
3000
|
+
const span = rs[k + 1] - rs[k];
|
|
3001
|
+
const u = span > 1e-9 ? (r - rs[k]) / span : 0;
|
|
3002
|
+
return ls[k] + (ls[k + 1] - ls[k]) * u;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return ls[ls.length - 1];
|
|
3006
|
+
};
|
|
3007
|
+
return sample(i0) * (1 - t) + sample(i1) * t;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/view3d/scene/bowlShell.ts
|
|
3011
|
+
var BAND_RECESS_M = 0.25;
|
|
3012
|
+
var BAND_MIN_RELIEF_M = 1.2;
|
|
3013
|
+
var SHELL_GAP_M = 0.6;
|
|
3014
|
+
var SHELL_THICKNESS_M = 0.45;
|
|
3015
|
+
var SHELL_PARAPET_M = 2.3;
|
|
3016
|
+
var DEG3 = Math.PI / 180;
|
|
3017
|
+
function polar(c, r, thetaDeg) {
|
|
3018
|
+
const t = thetaDeg * DEG3;
|
|
3019
|
+
return { x: c.x + r * Math.sin(t), y: c.y + r * Math.cos(t) };
|
|
3020
|
+
}
|
|
3021
|
+
function hasAuthoredPerimeter(shapes, fit) {
|
|
3022
|
+
const outer = fit.rings[fit.rings.length - 1];
|
|
3023
|
+
if (!outer) return false;
|
|
3024
|
+
const threshold = outer.outerRMedian * 0.85;
|
|
3025
|
+
for (const s of shapes) {
|
|
3026
|
+
if (s.role !== "wall") continue;
|
|
3027
|
+
const pts = s.points && s.points.length >= 2 ? s.points : s.x !== void 0 && s.y !== void 0 && s.width !== void 0 && s.height !== void 0 ? [
|
|
3028
|
+
{ x: s.x, y: s.y },
|
|
3029
|
+
{ x: s.x + s.width, y: s.y },
|
|
3030
|
+
{ x: s.x + s.width, y: s.y + s.height },
|
|
3031
|
+
{ x: s.x, y: s.y + s.height }
|
|
3032
|
+
] : [];
|
|
3033
|
+
if (pts.length < 2) continue;
|
|
3034
|
+
let beyond = 0;
|
|
3035
|
+
for (const p of pts) {
|
|
3036
|
+
if (Math.hypot(p.x - fit.centre.x, p.y - fit.centre.y) >= threshold) beyond++;
|
|
3037
|
+
}
|
|
3038
|
+
if (beyond >= pts.length / 2) return true;
|
|
3039
|
+
}
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
var BAND_RADIAL_CELLS = 24;
|
|
3043
|
+
function emitBandGrid(builder, fit, arc, innerEdge, outerEdge, bottomY, colors, ao, seatGuards) {
|
|
3044
|
+
const K = arc.binCount;
|
|
3045
|
+
const M2 = BAND_RADIAL_CELLS;
|
|
3046
|
+
const U = METRES_PER_CHART_UNIT;
|
|
3047
|
+
const floorY = bottomY + 0.02;
|
|
3048
|
+
const h = [];
|
|
3049
|
+
for (let k = 0; k < K; k++) {
|
|
3050
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3051
|
+
const row = new Float64Array(M2 + 1);
|
|
3052
|
+
for (let j = 0; j <= M2; j++) {
|
|
3053
|
+
const r = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3054
|
+
row[j] = Math.max(floorY, arcLevelAt(fit, arc, bearing, r) - BAND_RECESS_M);
|
|
3055
|
+
}
|
|
3056
|
+
h.push(row);
|
|
3057
|
+
}
|
|
3058
|
+
for (const g of seatGuards) {
|
|
3059
|
+
const bearing = bearingDeg(fit.centre, g);
|
|
3060
|
+
const r = Math.hypot(g.x - fit.centre.x, g.y - fit.centre.y);
|
|
3061
|
+
let f = bearing / fit.binStepDeg - 0.5 - arc.startBin;
|
|
3062
|
+
f = (f % fit.binCount + fit.binCount) % fit.binCount;
|
|
3063
|
+
if (f > K - 1 && !arc.closed) {
|
|
3064
|
+
if (f < K - 0.5 || f > fit.binCount - 0.5) {
|
|
3065
|
+
} else continue;
|
|
3066
|
+
}
|
|
3067
|
+
const cap = g.deckY - BAND_RECESS_M;
|
|
3068
|
+
const k0 = Math.min(K - 1, Math.max(0, Math.floor(f)));
|
|
3069
|
+
const kk = [k0, arc.closed ? (k0 + 1) % K : Math.min(K - 1, k0 + 1)];
|
|
3070
|
+
for (const k of kk) {
|
|
3071
|
+
const span = outerEdge[k] - innerEdge[k];
|
|
3072
|
+
if (!(span > 1e-9)) continue;
|
|
3073
|
+
const jf = (r - innerEdge[k]) / span * M2;
|
|
3074
|
+
if (jf < -0.75 || jf > M2 + 0.75) continue;
|
|
3075
|
+
const j0 = Math.min(M2, Math.max(0, Math.floor(jf)));
|
|
3076
|
+
const j1 = Math.min(M2, j0 + 1);
|
|
3077
|
+
if (h[k][j0] > cap) h[k][j0] = Math.max(floorY, cap);
|
|
3078
|
+
if (h[k][j1] > cap) h[k][j1] = Math.max(floorY, cap);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const cTop = [colors.bandTop[0] * ao.top, colors.bandTop[1] * ao.top, colors.bandTop[2] * ao.top];
|
|
3082
|
+
const cBot = [colors.bandTop[0] * ao.bottomCap, colors.bandTop[1] * ao.bottomCap, colors.bandTop[2] * ao.bottomCap];
|
|
3083
|
+
const cWallTop = [colors.bandWall[0] * ao.top, colors.bandWall[1] * ao.top, colors.bandWall[2] * ao.top];
|
|
3084
|
+
const cWallBot = [colors.bandWall[0] * ao.wallBottom, colors.bandWall[1] * ao.wallBottom, colors.bandWall[2] * ao.wallBottom];
|
|
3085
|
+
const point = (k, j) => {
|
|
3086
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3087
|
+
const r = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3088
|
+
const p = polar(fit.centre, r, bearing);
|
|
3089
|
+
return [p.x * U, h[k][j], p.y * U];
|
|
3090
|
+
};
|
|
3091
|
+
{
|
|
3092
|
+
const TAN_LIMIT = Math.tan(12 * Math.PI / 180);
|
|
3093
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
3094
|
+
let changed = false;
|
|
3095
|
+
for (let k = 0; k < K; k++) {
|
|
3096
|
+
for (let j = M2 - 1; j >= 0; j--) {
|
|
3097
|
+
if (h[k][j] > h[k][j + 1]) {
|
|
3098
|
+
h[k][j] = h[k][j + 1];
|
|
3099
|
+
changed = true;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
const lastPair = arc.closed ? K : K - 1;
|
|
3104
|
+
for (let k = 0; k < lastPair; k++) {
|
|
3105
|
+
const k1 = (k + 1) % K;
|
|
3106
|
+
for (let j = 0; j <= M2; j++) {
|
|
3107
|
+
const d = h[k1][j] - h[k][j];
|
|
3108
|
+
if (Math.abs(d) > 1) continue;
|
|
3109
|
+
const r0 = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3110
|
+
const r1 = innerEdge[k1] + (outerEdge[k1] - innerEdge[k1]) * j / M2;
|
|
3111
|
+
const b0 = polar(fit.centre, r0, (arc.startBin + k + 0.5) * fit.binStepDeg);
|
|
3112
|
+
const b1 = polar(fit.centre, r1, (arc.startBin + k1 + 0.5) * fit.binStepDeg);
|
|
3113
|
+
const lim = TAN_LIMIT * Math.hypot(b1.x - b0.x, b1.y - b0.y) * U;
|
|
3114
|
+
if (d > lim) {
|
|
3115
|
+
h[k1][j] = h[k][j] + lim;
|
|
3116
|
+
changed = true;
|
|
3117
|
+
} else if (-d > lim) {
|
|
3118
|
+
h[k][j] = h[k1][j] + lim;
|
|
3119
|
+
changed = true;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (!changed) break;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const JUMP_M = 1;
|
|
3127
|
+
const cliffBetween = (k, k1) => {
|
|
3128
|
+
for (let j = 0; j <= M2; j += 4) {
|
|
3129
|
+
if (Math.abs(h[k][j] - h[k1][j]) > JUMP_M) return true;
|
|
3130
|
+
}
|
|
3131
|
+
return Math.abs(h[k][M2] - h[k1][M2]) > JUMP_M;
|
|
3132
|
+
};
|
|
3133
|
+
const TAN_WALL = Math.tan(38 * Math.PI / 180);
|
|
3134
|
+
const radialCellM = (k) => (outerEdge[k] - innerEdge[k]) / M2 * U;
|
|
3135
|
+
const radialStep = (k, j) => h[k][j + 1] - h[k][j] > TAN_WALL * Math.max(1e-6, radialCellM(k));
|
|
3136
|
+
const vertexNormal = (k, j) => {
|
|
3137
|
+
let kPrev = arc.closed ? (k - 1 + K) % K : Math.max(0, k - 1);
|
|
3138
|
+
let kNext = arc.closed ? (k + 1) % K : Math.min(K - 1, k + 1);
|
|
3139
|
+
if (Math.abs(h[kPrev][j] - h[k][j]) > JUMP_M) kPrev = k;
|
|
3140
|
+
if (Math.abs(h[kNext][j] - h[k][j]) > JUMP_M) kNext = k;
|
|
3141
|
+
let jPrev = Math.max(0, j - 1);
|
|
3142
|
+
let jNext = Math.min(M2, j + 1);
|
|
3143
|
+
if (jPrev < j && radialStep(k, jPrev)) jPrev = j;
|
|
3144
|
+
if (jNext > j && radialStep(k, j)) jNext = j;
|
|
3145
|
+
const pk0 = point(kPrev, j), pk1 = point(kNext, j);
|
|
3146
|
+
const pj0 = point(k, jPrev), pj1 = point(k, jNext);
|
|
3147
|
+
const ux = pk1[0] - pk0[0], uy = pk1[1] - pk0[1], uz = pk1[2] - pk0[2];
|
|
3148
|
+
const vx = pj1[0] - pj0[0], vy = pj1[1] - pj0[1], vz = pj1[2] - pj0[2];
|
|
3149
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
3150
|
+
const len = Math.hypot(nx, ny, nz);
|
|
3151
|
+
if (!(len > 1e-12)) return [0, 1, 0];
|
|
3152
|
+
nx /= len;
|
|
3153
|
+
ny /= len;
|
|
3154
|
+
nz /= len;
|
|
3155
|
+
if (ny < 0) {
|
|
3156
|
+
nx = -nx;
|
|
3157
|
+
ny = -ny;
|
|
3158
|
+
nz = -nz;
|
|
3159
|
+
}
|
|
3160
|
+
return [nx, ny, nz];
|
|
3161
|
+
};
|
|
3162
|
+
const wallQuad = (a, b) => {
|
|
3163
|
+
const a0 = [a[0], bottomY, a[2]];
|
|
3164
|
+
const b0 = [b[0], bottomY, b[2]];
|
|
3165
|
+
const ex = b[0] - a[0], ez = b[2] - a[2];
|
|
3166
|
+
const len = Math.hypot(ex, ez);
|
|
3167
|
+
if (!(len > 1e-12)) return;
|
|
3168
|
+
const n = [ez / len, 0, -ex / len];
|
|
3169
|
+
builder.tri(a, b, b0, n, cWallTop, cWallTop, cWallBot);
|
|
3170
|
+
builder.tri(a, b0, a0, n, cWallTop, cWallBot, cWallBot);
|
|
3171
|
+
};
|
|
3172
|
+
const MIN_SPAN_U = 0.05 * CHART_UNITS_PER_METRE;
|
|
3173
|
+
const pinched = (k) => outerEdge[k] - innerEdge[k] < MIN_SPAN_U;
|
|
3174
|
+
const lastK = arc.closed ? K : K - 1;
|
|
3175
|
+
for (let k = 0; k < lastK; k++) {
|
|
3176
|
+
const k1 = (k + 1) % K;
|
|
3177
|
+
if (pinched(k) || pinched(k1)) continue;
|
|
3178
|
+
if (cliffBetween(k, k1)) {
|
|
3179
|
+
const bearingMid = (arc.startBin + k + 1) * fit.binStepDeg;
|
|
3180
|
+
const midAt = (side, j) => {
|
|
3181
|
+
const rIn = (innerEdge[k] + innerEdge[k1]) / 2;
|
|
3182
|
+
const rOut = (outerEdge[k] + outerEdge[k1]) / 2;
|
|
3183
|
+
const r = rIn + (rOut - rIn) * j / M2;
|
|
3184
|
+
const p = polar(fit.centre, r, bearingMid);
|
|
3185
|
+
return [p.x * U, h[side][j], p.y * U];
|
|
3186
|
+
};
|
|
3187
|
+
for (let j = 0; j < M2; j++) {
|
|
3188
|
+
const aL = point(k, j), dL = point(k, j + 1);
|
|
3189
|
+
const bL = midAt(k, j), cL = midAt(k, j + 1);
|
|
3190
|
+
const nL0 = vertexNormal(k, j), nL1 = vertexNormal(k, j + 1);
|
|
3191
|
+
builder.triN(aL, bL, cL, nL0, nL0, nL1, cTop);
|
|
3192
|
+
builder.triN(aL, cL, dL, nL0, nL1, nL1, cTop);
|
|
3193
|
+
const bR = point(k1, j), cR = point(k1, j + 1);
|
|
3194
|
+
const aR = midAt(k1, j), dR = midAt(k1, j + 1);
|
|
3195
|
+
const nR0 = vertexNormal(k1, j), nR1 = vertexNormal(k1, j + 1);
|
|
3196
|
+
builder.triN(aR, bR, cR, nR0, nR0, nR1, cTop);
|
|
3197
|
+
builder.triN(aR, cR, dR, nR0, nR1, nR1, cTop);
|
|
3198
|
+
const w0 = midAt(k, j), w1 = midAt(k, j + 1), w2 = midAt(k1, j + 1), w3 = midAt(k1, j);
|
|
3199
|
+
const ex = w1[0] - w0[0], ez = w1[2] - w0[2];
|
|
3200
|
+
const el = Math.hypot(ex, ez);
|
|
3201
|
+
if (el > 1e-12) {
|
|
3202
|
+
const n = [ez / el, 0, -ex / el];
|
|
3203
|
+
builder.tri(w0, w1, w2, n, cWallTop);
|
|
3204
|
+
builder.tri(w0, w2, w3, n, cWallTop);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const ai2 = point(k, 0), bi2 = point(k1, 0), ao_2 = point(k1, M2), di2 = point(k, M2);
|
|
3208
|
+
const flat2 = (p) => [p[0], bottomY, p[2]];
|
|
3209
|
+
builder.tri(flat2(ai2), flat2(ao_2), flat2(bi2), [0, -1, 0], cBot);
|
|
3210
|
+
builder.tri(flat2(ai2), flat2(di2), flat2(ao_2), [0, -1, 0], cBot);
|
|
3211
|
+
wallQuad(ai2, bi2);
|
|
3212
|
+
wallQuad(di2, ao_2);
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
for (let j = 0; j < M2; j++) {
|
|
3216
|
+
const na = vertexNormal(k, j), nb = vertexNormal(k1, j), nc = vertexNormal(k1, j + 1), nd = vertexNormal(k, j + 1);
|
|
3217
|
+
if (radialStep(k, j) || radialStep(k1, j)) {
|
|
3218
|
+
const midPt = (kk, hh) => {
|
|
3219
|
+
const bearing = (arc.startBin + kk + 0.5) * fit.binStepDeg;
|
|
3220
|
+
const r = innerEdge[kk] + (outerEdge[kk] - innerEdge[kk]) * (j + 0.5) / M2;
|
|
3221
|
+
const p = polar(fit.centre, r, bearing);
|
|
3222
|
+
return [p.x * U, hh, p.y * U];
|
|
3223
|
+
};
|
|
3224
|
+
const a2 = point(k, j), b2 = point(k1, j);
|
|
3225
|
+
const mLo0 = midPt(k, h[k][j]), mLo1 = midPt(k1, h[k1][j]);
|
|
3226
|
+
builder.triN(a2, b2, mLo1, na, nb, nb, cTop);
|
|
3227
|
+
builder.triN(a2, mLo1, mLo0, na, nb, na, cTop);
|
|
3228
|
+
const c2 = point(k1, j + 1), d2 = point(k, j + 1);
|
|
3229
|
+
const mHi0 = midPt(k, h[k][j + 1]), mHi1 = midPt(k1, h[k1][j + 1]);
|
|
3230
|
+
builder.triN(mHi0, mHi1, c2, nd, nc, nc, cTop);
|
|
3231
|
+
builder.triN(mHi0, c2, d2, nd, nc, nd, cTop);
|
|
3232
|
+
const ex = mLo1[0] - mLo0[0], ez = mLo1[2] - mLo0[2];
|
|
3233
|
+
const el = Math.hypot(ex, ez);
|
|
3234
|
+
if (el > 1e-12) {
|
|
3235
|
+
const n = [-ez / el, 0, ex / el];
|
|
3236
|
+
builder.tri(mLo0, mLo1, mHi1, n, cWallTop);
|
|
3237
|
+
builder.tri(mLo0, mHi1, mHi0, n, cWallTop);
|
|
3238
|
+
}
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
const a = point(k, j), b = point(k1, j), c = point(k1, j + 1), d = point(k, j + 1);
|
|
3242
|
+
builder.triN(a, b, c, na, nb, nc, cTop);
|
|
3243
|
+
builder.triN(a, c, d, na, nc, nd, cTop);
|
|
3244
|
+
}
|
|
3245
|
+
const ai = point(k, 0), bi = point(k1, 0), ao_ = point(k1, M2), di = point(k, M2);
|
|
3246
|
+
const flat = (p) => [p[0], bottomY, p[2]];
|
|
3247
|
+
builder.tri(flat(ai), flat(ao_), flat(bi), [0, -1, 0], cBot);
|
|
3248
|
+
builder.tri(flat(ai), flat(di), flat(ao_), [0, -1, 0], cBot);
|
|
3249
|
+
wallQuad(ai, bi);
|
|
3250
|
+
wallQuad(di, ao_);
|
|
3251
|
+
}
|
|
3252
|
+
if (!arc.closed) {
|
|
3253
|
+
for (const k of [0, K - 1]) {
|
|
3254
|
+
for (let j = 0; j < M2; j++) wallQuad(point(k, j), point(k, j + 1));
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
function emitBowlGeometry(builder, fit, opts) {
|
|
3259
|
+
const { bottomY, colors, ao } = opts;
|
|
3260
|
+
const drawnOuter = new Float64Array(fit.binCount).fill(-Infinity);
|
|
3261
|
+
for (const ring of fit.rings) {
|
|
3262
|
+
for (const arc of ring.arcs) {
|
|
3263
|
+
const innerEdge = new Float64Array(arc.binCount);
|
|
3264
|
+
const outerEdge = new Float64Array(arc.binCount);
|
|
3265
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3266
|
+
const bin = (arc.startBin + k) % fit.binCount;
|
|
3267
|
+
const inner = Math.max(0, arc.innerR[k] - SEAT_OWNERSHIP_PAD_U);
|
|
3268
|
+
innerEdge[k] = Math.max(inner, drawnOuter[bin] + 1e-3);
|
|
3269
|
+
outerEdge[k] = Math.max(innerEdge[k] + 1e-3, arc.outerR[k] + SEAT_OWNERSHIP_PAD_U);
|
|
3270
|
+
}
|
|
3271
|
+
let relief = 0;
|
|
3272
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3273
|
+
const d = arc.topLevelM[k] - bottomY;
|
|
3274
|
+
if (d > relief) relief = d;
|
|
3275
|
+
}
|
|
3276
|
+
if (relief >= BAND_MIN_RELIEF_M) {
|
|
3277
|
+
emitBandGrid(builder, fit, arc, innerEdge, outerEdge, bottomY, colors, ao, opts.seatGuards);
|
|
3278
|
+
}
|
|
3279
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3280
|
+
const bin = (arc.startBin + k) % fit.binCount;
|
|
3281
|
+
if (outerEdge[k] > drawnOuter[bin]) drawnOuter[bin] = outerEdge[k];
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
if (hasAuthoredPerimeter(opts.shapes, fit)) return;
|
|
3286
|
+
const outerRing = fit.rings[fit.rings.length - 1];
|
|
3287
|
+
const gapU = SHELL_GAP_M * CHART_UNITS_PER_METRE;
|
|
3288
|
+
const thickU = SHELL_THICKNESS_M * CHART_UNITS_PER_METRE;
|
|
3289
|
+
for (const arc of outerRing.arcs) {
|
|
3290
|
+
const inner = [];
|
|
3291
|
+
const outer = [];
|
|
3292
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3293
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3294
|
+
const r0 = arc.outerR[k] + SEAT_OWNERSHIP_PAD_U + gapU;
|
|
3295
|
+
inner.push(polar(fit.centre, r0, bearing));
|
|
3296
|
+
outer.push(polar(fit.centre, r0 + thickU, bearing));
|
|
3297
|
+
}
|
|
3298
|
+
let crest = -Infinity;
|
|
3299
|
+
for (let k = 0; k < arc.binCount; k++) if (arc.topLevelM[k] > crest) crest = arc.topLevelM[k];
|
|
3300
|
+
const topY = () => crest + SHELL_PARAPET_M;
|
|
3301
|
+
if (arc.closed) {
|
|
3302
|
+
extrudePrism(builder, outer, [inner.slice().reverse()], topY, bottomY, colors.shellTop, colors.shellWall, ao);
|
|
3303
|
+
} else {
|
|
3304
|
+
extrudePrism(builder, [...outer, ...inner.slice().reverse()], void 0, topY, bottomY, colors.shellTop, colors.shellWall, ao);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
2636
3309
|
// src/view3d/scene/sceneModel.ts
|
|
2637
3310
|
function shapeVisibleInConfiguration(shape, activeConfigurationId) {
|
|
2638
3311
|
const ids = shape.eventConfigurationIds;
|
|
@@ -2642,14 +3315,28 @@ function visibleObjects(objects, activeConfigurationId) {
|
|
|
2642
3315
|
return objects.filter((object) => object.type !== "shape" || shapeVisibleInConfiguration(object, activeConfigurationId));
|
|
2643
3316
|
}
|
|
2644
3317
|
function floorUnits(doc, activeConfigurationId) {
|
|
3318
|
+
const docFocal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
2645
3319
|
if (doc.floors?.length) {
|
|
2646
|
-
return doc.floors.map((f) => ({
|
|
3320
|
+
return doc.floors.map((f, index) => ({
|
|
2647
3321
|
objects: visibleObjects(f.objects, activeConfigurationId),
|
|
2648
|
-
focal:
|
|
2649
|
-
|
|
3322
|
+
focal: {
|
|
3323
|
+
x: (f.focalPoint ?? docFocal).x,
|
|
3324
|
+
y: (f.focalPoint ?? docFocal).y
|
|
3325
|
+
},
|
|
3326
|
+
baseHeightM: f.baseHeightM ?? 0,
|
|
3327
|
+
index
|
|
3328
|
+
// 2D deliberately packs levels into a non-overlapping grid. In 3D the
|
|
3329
|
+
// physical datum already separates them, so retaining each floor's
|
|
3330
|
+
// authored x/z registration is what turns independent drawings into one
|
|
3331
|
+
// coherent building stack. The source scene graph is never rewritten.
|
|
2650
3332
|
}));
|
|
2651
3333
|
}
|
|
2652
|
-
return [{
|
|
3334
|
+
return [{
|
|
3335
|
+
objects: visibleObjects(doc.objects, activeConfigurationId),
|
|
3336
|
+
focal: docFocal,
|
|
3337
|
+
baseHeightM: 0,
|
|
3338
|
+
index: 0
|
|
3339
|
+
}];
|
|
2653
3340
|
}
|
|
2654
3341
|
var AO = { top: 1, wallBottom: 0.5, bottomCap: 0.4 };
|
|
2655
3342
|
function tintTop(fill, neutral) {
|
|
@@ -2824,6 +3511,79 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2824
3511
|
extrudePrism(builder, ring, section.holes, topY, bottomY, colTop, S.tierWall, AO, maxErr, topN);
|
|
2825
3512
|
}
|
|
2826
3513
|
}
|
|
3514
|
+
function collectBowlSamples(unit, seats, surfaces) {
|
|
3515
|
+
const unitSections = /* @__PURE__ */ new Set();
|
|
3516
|
+
for (const o of unit.objects) if (o.type === "section") unitSections.add(o.id);
|
|
3517
|
+
const flatRows = /* @__PURE__ */ new Map();
|
|
3518
|
+
for (let i = 0; i < seats.length; i++) {
|
|
3519
|
+
const owner = surfaces.seatOwner[i];
|
|
3520
|
+
if (!owner || !unitSections.has(owner)) continue;
|
|
3521
|
+
const surf = surfaces.bySection.get(owner);
|
|
3522
|
+
if (surf && surf.rowLevels.length >= 2) continue;
|
|
3523
|
+
const s = seats[i];
|
|
3524
|
+
let rows = flatRows.get(owner);
|
|
3525
|
+
if (!rows) {
|
|
3526
|
+
rows = /* @__PURE__ */ new Map();
|
|
3527
|
+
flatRows.set(owner, rows);
|
|
3528
|
+
}
|
|
3529
|
+
const key = s.rowId || `__seat-${i}`;
|
|
3530
|
+
let row = rows.get(key);
|
|
3531
|
+
if (!row) {
|
|
3532
|
+
row = { pts: [], levelM: 0, n: 0 };
|
|
3533
|
+
rows.set(key, row);
|
|
3534
|
+
}
|
|
3535
|
+
row.pts.push({ x: s.x, y: s.y });
|
|
3536
|
+
row.levelM += surfaces.seatDeckY(i);
|
|
3537
|
+
row.n++;
|
|
3538
|
+
}
|
|
3539
|
+
const samples = [];
|
|
3540
|
+
for (const o of unit.objects) {
|
|
3541
|
+
if (o.type !== "section") continue;
|
|
3542
|
+
const surf = surfaces.bySection.get(o.id);
|
|
3543
|
+
if (surf && surf.rowLevels.length >= 2) {
|
|
3544
|
+
const rows2 = surf.rowLevels.filter((r) => !r.patch).map((r) => ({ pts: r.pts, levelM: r.y }));
|
|
3545
|
+
if (rows2.length >= 2) samples.push({ sectionId: o.id, rows: rows2 });
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
const rows = flatRows.get(o.id);
|
|
3549
|
+
if (!rows) continue;
|
|
3550
|
+
samples.push({
|
|
3551
|
+
sectionId: o.id,
|
|
3552
|
+
rows: [...rows.values()].map((r) => ({ pts: r.pts, levelM: r.levelM / r.n }))
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
return samples;
|
|
3556
|
+
}
|
|
3557
|
+
function buildBowl(builder, unit, seats, surfaces, S) {
|
|
3558
|
+
const samples = collectBowlSamples(unit, seats, surfaces);
|
|
3559
|
+
if (!samples.length) return;
|
|
3560
|
+
const fit = fitBowlProfile(samples, unit.focal);
|
|
3561
|
+
if (!fit) return;
|
|
3562
|
+
const unitSections = /* @__PURE__ */ new Set();
|
|
3563
|
+
for (const o of unit.objects) if (o.type === "section") unitSections.add(o.id);
|
|
3564
|
+
const seatGuards = [];
|
|
3565
|
+
for (let i = 0; i < seats.length; i++) {
|
|
3566
|
+
const owner = surfaces.seatOwner[i];
|
|
3567
|
+
if (owner ? unitSections.has(owner) : true) {
|
|
3568
|
+
seatGuards.push({ x: seats[i].x, y: seats[i].y, deckY: surfaces.seatDeckY(i) });
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
builder.setMaterial(0);
|
|
3572
|
+
emitBowlGeometry(builder, fit, {
|
|
3573
|
+
bottomY: unit.baseHeightM,
|
|
3574
|
+
colors: {
|
|
3575
|
+
// A shade below the tier caps, so stands read as decks resting ON the
|
|
3576
|
+
// bowl rather than the bowl swallowing them.
|
|
3577
|
+
bandTop: scaleRgb(S.tierTop, 0.8),
|
|
3578
|
+
bandWall: S.tierWall,
|
|
3579
|
+
shellTop: scaleRgb(S.tierWall, 1.15),
|
|
3580
|
+
shellWall: S.tierWall
|
|
3581
|
+
},
|
|
3582
|
+
ao: AO,
|
|
3583
|
+
shapes: unit.objects.filter((o) => o.type === "shape"),
|
|
3584
|
+
seatGuards
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
2827
3587
|
function coplanarLevels(a, b) {
|
|
2828
3588
|
if (a === void 0 || b === void 0) return true;
|
|
2829
3589
|
return Math.abs(a - b) < 1e-3;
|
|
@@ -2989,7 +3749,11 @@ function architectureForShape(shape) {
|
|
|
2989
3749
|
restroom: { kind: "solid", heightM: 2.4 },
|
|
2990
3750
|
bar: { kind: "solid", heightM: 1.1 },
|
|
2991
3751
|
concession: { kind: "solid", heightM: 1.1 },
|
|
2992
|
-
coat: { kind: "solid", heightM: 1.1 }
|
|
3752
|
+
coat: { kind: "solid", heightM: 1.1 },
|
|
3753
|
+
// Sports-surface imports describe a goal as one thin rectangular portal.
|
|
3754
|
+
// It therefore becomes a real open goal frame in 3D without a bespoke
|
|
3755
|
+
// chart model or a copied bitmap.
|
|
3756
|
+
goal: { kind: "portal", heightM: 1.8 }
|
|
2993
3757
|
};
|
|
2994
3758
|
const resolved = defaults[role] ?? { kind: "plate", heightM: 0.25 };
|
|
2995
3759
|
return { ...resolved, heightM: shape.heightM ?? resolved.heightM };
|
|
@@ -3188,20 +3952,46 @@ function chartFootprint(units, seats) {
|
|
|
3188
3952
|
return { minX, minY, maxX, maxY };
|
|
3189
3953
|
}
|
|
3190
3954
|
function buildSceneModel(input) {
|
|
3191
|
-
const { doc
|
|
3955
|
+
const { doc } = input;
|
|
3192
3956
|
const theme = resolveTheme3D(doc.theme);
|
|
3193
3957
|
const S = theme.structure;
|
|
3194
3958
|
const units = floorUnits(doc, input.eventConfigurationId ?? doc.eventConfigurationId);
|
|
3959
|
+
const seats = input.seats;
|
|
3195
3960
|
const builder = new MeshBuilder();
|
|
3961
|
+
const floorByOwner = /* @__PURE__ */ new Map();
|
|
3962
|
+
for (const unit of units) {
|
|
3963
|
+
for (const object of unit.objects) {
|
|
3964
|
+
floorByOwner.set(object.id, unit.index);
|
|
3965
|
+
if (object.type === "section") floorByOwner.set(object.logicalSectionId ?? object.id, unit.index);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
const hintedSeatFloor = seats.map((seat) => floorByOwner.get(seat.sectionId ?? "") ?? floorByOwner.get(seat.rowId ?? "") ?? 0);
|
|
3196
3969
|
const fp = chartFootprint(units, seats);
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3970
|
+
if (units.length === 1) {
|
|
3971
|
+
const padU = Math.max(60, (fp.maxX - fp.minX + fp.maxY - fp.minY) * 0.06);
|
|
3972
|
+
const groundPoly = rectPolygon(fp.minX - padU, fp.minY - padU, fp.maxX - fp.minX + padU * 2, fp.maxY - fp.minY + padU * 2);
|
|
3973
|
+
extrudePrism(builder, groundPoly, void 0, () => 0, -0.4, S.ground, S.ground, AO);
|
|
3974
|
+
} else {
|
|
3975
|
+
const unit = units.reduce((lowest, candidate) => candidate.baseHeightM < lowest.baseHeightM ? candidate : lowest);
|
|
3976
|
+
builder.setFloor(unit.index);
|
|
3977
|
+
const unitSeats = seats.filter((_seat, index) => hintedSeatFloor[index] === unit.index);
|
|
3978
|
+
const floorFp = chartFootprint([unit], unitSeats);
|
|
3979
|
+
const span = Math.hypot(floorFp.maxX - floorFp.minX, floorFp.maxY - floorFp.minY);
|
|
3980
|
+
const padU = Math.max(28, Math.min(80, span * 0.04));
|
|
3981
|
+
const plinth = rectPolygon(
|
|
3982
|
+
floorFp.minX - padU,
|
|
3983
|
+
floorFp.minY - padU,
|
|
3984
|
+
floorFp.maxX - floorFp.minX + padU * 2,
|
|
3985
|
+
floorFp.maxY - floorFp.minY + padU * 2
|
|
3986
|
+
);
|
|
3987
|
+
const top = unit.baseHeightM - 0.03;
|
|
3988
|
+
extrudePrism(builder, plinth, void 0, () => top, top - 0.22, S.ground, S.ground, AO);
|
|
3989
|
+
}
|
|
3200
3990
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
3201
3991
|
const catColor = /* @__PURE__ */ new Map();
|
|
3202
3992
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
3203
3993
|
const stageBounds = [];
|
|
3204
|
-
const surfaces = buildVenueSurfaces(units, seats);
|
|
3994
|
+
const surfaces = buildVenueSurfaces(units, seats, hintedSeatFloor);
|
|
3205
3995
|
const seatFloor = new Float32Array(seats.length);
|
|
3206
3996
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
3207
3997
|
const unit = units[unitIndex];
|
|
@@ -3228,8 +4018,12 @@ function buildSceneModel(input) {
|
|
|
3228
4018
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3229
4019
|
else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
|
|
3230
4020
|
}
|
|
4021
|
+
buildBowl(builder, unit, seats, surfaces, S);
|
|
3231
4022
|
}
|
|
3232
|
-
const focal =
|
|
4023
|
+
const focal = units.length > 1 ? {
|
|
4024
|
+
x: units.reduce((sum, unit) => sum + unit.focal.x, 0) / units.length,
|
|
4025
|
+
y: units.reduce((sum, unit) => sum + unit.focal.y, 0) / units.length
|
|
4026
|
+
} : doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
3233
4027
|
const solids = mergeMeshData([builder.build()]);
|
|
3234
4028
|
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor, catColor);
|
|
3235
4029
|
const zoneDefs = doc.zones ?? [];
|
|
@@ -3335,9 +4129,11 @@ function buildSceneModel(input) {
|
|
|
3335
4129
|
if (!a || a.n === 0) continue;
|
|
3336
4130
|
const e = ext.get(o.id);
|
|
3337
4131
|
const centre = [a.x / a.n * M, a.deck / a.n, a.y / a.n * M];
|
|
4132
|
+
const sectionLabel = o.displayLabel || o.label || o.id;
|
|
3338
4133
|
sections.push({
|
|
3339
4134
|
id: o.id,
|
|
3340
|
-
|
|
4135
|
+
floorIndex: unit.index,
|
|
4136
|
+
label: sectionLabel,
|
|
3341
4137
|
seatCount: a.n,
|
|
3342
4138
|
center: centre,
|
|
3343
4139
|
// Half-diagonal of the seat extent — the same fit the zones use.
|
|
@@ -3346,13 +4142,17 @@ function buildSceneModel(input) {
|
|
|
3346
4142
|
// look along so the seats present their fronts.
|
|
3347
4143
|
focalWorld: [unit.focal.x * M, unit.baseHeightM + 1.5, unit.focal.y * M]
|
|
3348
4144
|
});
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
4145
|
+
const floorLabel = doc.floors?.[unit.index]?.name?.trim().toLocaleLowerCase();
|
|
4146
|
+
if (!(units.length > 1 && floorLabel === sectionLabel.trim().toLocaleLowerCase())) {
|
|
4147
|
+
labels.push({
|
|
4148
|
+
id: `section:${o.id}`,
|
|
4149
|
+
kind: "section",
|
|
4150
|
+
// The buyer-facing name wins over the technical one, as it does in 2D.
|
|
4151
|
+
text: sectionLabel,
|
|
4152
|
+
anchor: [centre[0], centre[1] + SECTION_LABEL_LIFT_M, centre[2]],
|
|
4153
|
+
floorIndex: unit.index
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
3356
4156
|
}
|
|
3357
4157
|
}
|
|
3358
4158
|
}
|
|
@@ -3367,7 +4167,8 @@ function buildSceneModel(input) {
|
|
|
3367
4167
|
id: `ga:${o.id}`,
|
|
3368
4168
|
kind: "ga",
|
|
3369
4169
|
text: o.capacity > 0 ? `${name} \xB7 ${o.capacity.toLocaleString()}` : name,
|
|
3370
|
-
anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M]
|
|
4170
|
+
anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M],
|
|
4171
|
+
floorIndex: unit.index
|
|
3371
4172
|
});
|
|
3372
4173
|
}
|
|
3373
4174
|
}
|
|
@@ -3386,7 +4187,8 @@ function buildSceneModel(input) {
|
|
|
3386
4187
|
id: `row:${rowId}`,
|
|
3387
4188
|
kind: "row",
|
|
3388
4189
|
text,
|
|
3389
|
-
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
|
|
4190
|
+
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M],
|
|
4191
|
+
floorIndex: hintedSeatFloor[end.index]
|
|
3390
4192
|
});
|
|
3391
4193
|
}
|
|
3392
4194
|
const rowAcc = /* @__PURE__ */ new Map();
|
|
@@ -3441,7 +4243,8 @@ function buildSceneModel(input) {
|
|
|
3441
4243
|
id: `seat:${s.id}`,
|
|
3442
4244
|
kind: "seat",
|
|
3443
4245
|
text,
|
|
3444
|
-
anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M]
|
|
4246
|
+
anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M],
|
|
4247
|
+
floorIndex: hintedSeatFloor[i]
|
|
3445
4248
|
});
|
|
3446
4249
|
}
|
|
3447
4250
|
}
|
|
@@ -3457,7 +4260,8 @@ function buildSceneModel(input) {
|
|
|
3457
4260
|
kind: "annotation",
|
|
3458
4261
|
text: o.label,
|
|
3459
4262
|
anchor: [centre.x * M, unit.baseHeightM + architectureForShape(o).heightM + ANNOTATION_LIFT_M, centre.y * M],
|
|
3460
|
-
color: o.fill
|
|
4263
|
+
color: o.fill,
|
|
4264
|
+
floorIndex: unit.index
|
|
3461
4265
|
});
|
|
3462
4266
|
} else if (o.type === "text") {
|
|
3463
4267
|
if (!o.text) continue;
|
|
@@ -3467,7 +4271,8 @@ function buildSceneModel(input) {
|
|
|
3467
4271
|
text: o.text,
|
|
3468
4272
|
anchor: [o.position.x * M, unit.baseHeightM + ANNOTATION_LIFT_M, o.position.y * M],
|
|
3469
4273
|
color: o.color,
|
|
3470
|
-
rotation: o.rotation
|
|
4274
|
+
rotation: o.rotation,
|
|
4275
|
+
floorIndex: unit.index
|
|
3471
4276
|
});
|
|
3472
4277
|
} else if (o.type === "booth") {
|
|
3473
4278
|
const poly = boothPolygon(o);
|
|
@@ -3478,21 +4283,52 @@ function buildSceneModel(input) {
|
|
|
3478
4283
|
id: `booth:${o.id}`,
|
|
3479
4284
|
kind: "booth",
|
|
3480
4285
|
text: o.displayLabel || o.label || o.id,
|
|
3481
|
-
anchor: [c.x * M, unit.baseHeightM + BOOTH_HEIGHT_M + BOOTH_LABEL_LIFT_M, c.y * M]
|
|
4286
|
+
anchor: [c.x * M, unit.baseHeightM + BOOTH_HEIGHT_M + BOOTH_LABEL_LIFT_M, c.y * M],
|
|
4287
|
+
floorIndex: unit.index
|
|
3482
4288
|
});
|
|
3483
4289
|
}
|
|
3484
4290
|
}
|
|
3485
4291
|
}
|
|
3486
4292
|
const floors = [];
|
|
3487
4293
|
{
|
|
4294
|
+
const solidBounds = units.map(() => ({
|
|
4295
|
+
minX: Infinity,
|
|
4296
|
+
minY: Infinity,
|
|
4297
|
+
minZ: Infinity,
|
|
4298
|
+
maxX: -Infinity,
|
|
4299
|
+
maxY: -Infinity,
|
|
4300
|
+
maxZ: -Infinity
|
|
4301
|
+
}));
|
|
4302
|
+
for (let vertex = 0; vertex < solids.floor.length; vertex += 1) {
|
|
4303
|
+
const floorIndex = Math.max(0, Math.min(units.length - 1, Math.round(solids.floor[vertex])));
|
|
4304
|
+
const offset = vertex * 3;
|
|
4305
|
+
const bound = solidBounds[floorIndex];
|
|
4306
|
+
bound.minX = Math.min(bound.minX, solids.position[offset]);
|
|
4307
|
+
bound.minY = Math.min(bound.minY, solids.position[offset + 1]);
|
|
4308
|
+
bound.minZ = Math.min(bound.minZ, solids.position[offset + 2]);
|
|
4309
|
+
bound.maxX = Math.max(bound.maxX, solids.position[offset]);
|
|
4310
|
+
bound.maxY = Math.max(bound.maxY, solids.position[offset + 1]);
|
|
4311
|
+
bound.maxZ = Math.max(bound.maxZ, solids.position[offset + 2]);
|
|
4312
|
+
}
|
|
3488
4313
|
const sectionFloor = /* @__PURE__ */ new Map();
|
|
3489
4314
|
for (let ui = 0; ui < units.length; ui++) {
|
|
3490
4315
|
for (const o of units[ui].objects) if (o.type === "section") sectionFloor.set(o.id, ui);
|
|
3491
4316
|
}
|
|
3492
|
-
const acc = units.map(() => ({
|
|
4317
|
+
const acc = units.map(() => ({
|
|
4318
|
+
n: 0,
|
|
4319
|
+
minX: Infinity,
|
|
4320
|
+
minY: Infinity,
|
|
4321
|
+
minDeck: Infinity,
|
|
4322
|
+
maxX: -Infinity,
|
|
4323
|
+
maxY: -Infinity,
|
|
4324
|
+
maxDeck: -Infinity,
|
|
4325
|
+
sumX: 0,
|
|
4326
|
+
sumDeck: 0,
|
|
4327
|
+
sumZ: 0
|
|
4328
|
+
}));
|
|
3493
4329
|
for (let i = 0; i < seats.length; i++) {
|
|
3494
4330
|
const owner = surfaces.seatOwner[i];
|
|
3495
|
-
const ui = owner !== null ? sectionFloor.get(owner) : void 0;
|
|
4331
|
+
const ui = (owner !== null ? sectionFloor.get(owner) : void 0) ?? hintedSeatFloor[i];
|
|
3496
4332
|
if (ui === void 0) continue;
|
|
3497
4333
|
const a = acc[ui];
|
|
3498
4334
|
const s = seats[i];
|
|
@@ -3501,19 +4337,78 @@ function buildSceneModel(input) {
|
|
|
3501
4337
|
if (s.y < a.minY) a.minY = s.y;
|
|
3502
4338
|
if (s.x > a.maxX) a.maxX = s.x;
|
|
3503
4339
|
if (s.y > a.maxY) a.maxY = s.y;
|
|
3504
|
-
|
|
4340
|
+
const deck = surfaces.seatDeckY(i);
|
|
4341
|
+
a.sumX += s.x * M;
|
|
4342
|
+
a.sumDeck += deck;
|
|
4343
|
+
a.sumZ += s.y * M;
|
|
4344
|
+
a.minDeck = Math.min(a.minDeck, deck);
|
|
4345
|
+
a.maxDeck = Math.max(a.maxDeck, deck + 1.6);
|
|
3505
4346
|
seatFloor[i] = ui;
|
|
3506
4347
|
}
|
|
3507
4348
|
for (let ui = 0; ui < units.length; ui++) {
|
|
3508
4349
|
const a = acc[ui];
|
|
3509
4350
|
const f = doc.floors?.[ui];
|
|
4351
|
+
const solid = solidBounds[ui];
|
|
4352
|
+
const minWorldX = Math.min(
|
|
4353
|
+
a.n ? a.minX * M : units[ui].focal.x * M - 1,
|
|
4354
|
+
Number.isFinite(solid.minX) ? solid.minX : Infinity
|
|
4355
|
+
);
|
|
4356
|
+
const maxWorldX = Math.max(
|
|
4357
|
+
a.n ? a.maxX * M : units[ui].focal.x * M + 1,
|
|
4358
|
+
Number.isFinite(solid.maxX) ? solid.maxX : -Infinity
|
|
4359
|
+
);
|
|
4360
|
+
const minWorldZ = Math.min(
|
|
4361
|
+
a.n ? a.minY * M : units[ui].focal.y * M - 1,
|
|
4362
|
+
Number.isFinite(solid.minZ) ? solid.minZ : Infinity
|
|
4363
|
+
);
|
|
4364
|
+
const maxWorldZ = Math.max(
|
|
4365
|
+
a.n ? a.maxY * M : units[ui].focal.y * M + 1,
|
|
4366
|
+
Number.isFinite(solid.maxZ) ? solid.maxZ : -Infinity
|
|
4367
|
+
);
|
|
4368
|
+
const minWorldY = Math.min(
|
|
4369
|
+
a.n ? Math.min(units[ui].baseHeightM, a.minDeck) : units[ui].baseHeightM,
|
|
4370
|
+
Number.isFinite(solid.minY) ? solid.minY : Infinity
|
|
4371
|
+
);
|
|
4372
|
+
const maxWorldY = Math.max(
|
|
4373
|
+
a.n ? Math.max(a.maxDeck, minWorldY + 0.5) : minWorldY + 0.5,
|
|
4374
|
+
Number.isFinite(solid.maxY) ? solid.maxY : -Infinity
|
|
4375
|
+
);
|
|
4376
|
+
const center = [
|
|
4377
|
+
// Aim the floor camera at its sellable topology rather than the centre
|
|
4378
|
+
// of a generous imported outline/plinth. The complete box is still
|
|
4379
|
+
// fitted below, so stages, annotations and deck edges cannot clip.
|
|
4380
|
+
a.n ? a.sumX / a.n : (minWorldX + maxWorldX) / 2,
|
|
4381
|
+
a.n ? a.sumDeck / a.n : (minWorldY + maxWorldY) / 2,
|
|
4382
|
+
a.n ? a.sumZ / a.n : (minWorldZ + maxWorldZ) / 2
|
|
4383
|
+
];
|
|
4384
|
+
const radius2 = Math.max(1, 0.5 * Math.hypot(
|
|
4385
|
+
maxWorldX - minWorldX,
|
|
4386
|
+
maxWorldY - minWorldY,
|
|
4387
|
+
maxWorldZ - minWorldZ
|
|
4388
|
+
));
|
|
3510
4389
|
floors.push({
|
|
3511
4390
|
index: ui,
|
|
3512
4391
|
id: f?.id ?? `floor-${ui}`,
|
|
3513
4392
|
label: f?.name ?? (units.length > 1 ? `Floor ${ui + 1}` : "Venue"),
|
|
3514
4393
|
seatCount: a.n,
|
|
3515
|
-
center
|
|
3516
|
-
radius:
|
|
4394
|
+
center,
|
|
4395
|
+
radius: radius2,
|
|
4396
|
+
box: {
|
|
4397
|
+
min: [minWorldX, minWorldY, minWorldZ],
|
|
4398
|
+
max: [maxWorldX, maxWorldY, maxWorldZ]
|
|
4399
|
+
},
|
|
4400
|
+
focalWorld: [units[ui].focal.x * M, units[ui].baseHeightM + 1.5, units[ui].focal.y * M]
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
if (floors.length > 1) {
|
|
4405
|
+
for (const floor of floors) {
|
|
4406
|
+
labels.push({
|
|
4407
|
+
id: `floor:${floor.id}`,
|
|
4408
|
+
kind: "floor",
|
|
4409
|
+
text: floor.label,
|
|
4410
|
+
anchor: [floor.center[0], floor.box.max[1] + 1.2, floor.center[2]],
|
|
4411
|
+
floorIndex: floor.index
|
|
3517
4412
|
});
|
|
3518
4413
|
}
|
|
3519
4414
|
}
|
|
@@ -3591,18 +4486,20 @@ function buildSceneModel(input) {
|
|
|
3591
4486
|
boxMinZ = Math.max(fpMinZ, contentMinZ - pad);
|
|
3592
4487
|
boxMaxZ = Math.min(fpMaxZ, contentMaxZ + pad);
|
|
3593
4488
|
}
|
|
4489
|
+
const boxMinY = Math.min(0, renderMinY);
|
|
4490
|
+
const boxMaxY = Math.max(renderMaxY, radius * 0.05);
|
|
3594
4491
|
return {
|
|
3595
4492
|
solids,
|
|
3596
4493
|
seats: seatData,
|
|
3597
4494
|
bounds: {
|
|
3598
4495
|
// Centred on the CONTENT box, so an off-centre table cluster arrives
|
|
3599
4496
|
// centred on screen instead of wherever the floor's midpoint put it.
|
|
3600
|
-
center: [(boxMinX + boxMaxX) / 2,
|
|
4497
|
+
center: [(boxMinX + boxMaxX) / 2, (boxMinY + boxMaxY) / 2, (boxMinZ + boxMaxZ) / 2],
|
|
3601
4498
|
radius,
|
|
3602
4499
|
groundY: 0,
|
|
3603
4500
|
box: {
|
|
3604
|
-
min: [boxMinX,
|
|
3605
|
-
max: [boxMaxX,
|
|
4501
|
+
min: [boxMinX, boxMinY, boxMinZ],
|
|
4502
|
+
max: [boxMaxX, boxMaxY, boxMaxZ]
|
|
3606
4503
|
}
|
|
3607
4504
|
},
|
|
3608
4505
|
renderBounds: { center: renderCenter, radius: renderRadius },
|
|
@@ -3624,6 +4521,7 @@ function buildSceneModel(input) {
|
|
|
3624
4521
|
var SEPARATION_X_PX = 88;
|
|
3625
4522
|
var SEPARATION_Y_PX = 20;
|
|
3626
4523
|
var KIND_STYLE = {
|
|
4524
|
+
floor: { size: 14, weight: "700", opacity: 0.98 },
|
|
3627
4525
|
zone: { size: 15, weight: "600", opacity: 0.95 },
|
|
3628
4526
|
section: { size: 12, weight: "500", opacity: 0.88 },
|
|
3629
4527
|
ga: { size: 12, weight: "500", opacity: 0.88 },
|
|
@@ -3644,6 +4542,27 @@ var DENSE_SEPARATION = {
|
|
|
3644
4542
|
// seating instead of clustering into one stack.
|
|
3645
4543
|
seat: { x: 46, y: 18 }
|
|
3646
4544
|
};
|
|
4545
|
+
function hexRgb(value) {
|
|
4546
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value?.trim() ?? "");
|
|
4547
|
+
if (!match) return null;
|
|
4548
|
+
return [0, 2, 4].map((offset) => Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255);
|
|
4549
|
+
}
|
|
4550
|
+
function luminance(rgb) {
|
|
4551
|
+
const linear = (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
|
4552
|
+
return 0.2126 * linear(rgb[0]) + 0.7152 * linear(rgb[1]) + 0.0722 * linear(rgb[2]);
|
|
4553
|
+
}
|
|
4554
|
+
function ratio(left, right) {
|
|
4555
|
+
const a = luminance(left);
|
|
4556
|
+
const b = luminance(right);
|
|
4557
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
4558
|
+
}
|
|
4559
|
+
function resolve3dLabelTreatment(preferredInk, backgroundRgb = [0.04, 0.06, 0.1]) {
|
|
4560
|
+
const darkScene = luminance(backgroundRgb) < 0.42;
|
|
4561
|
+
const fallbackInk = darkScene ? "#f4f7fb" : "#0b1220";
|
|
4562
|
+
const authored = hexRgb(preferredInk);
|
|
4563
|
+
const ink = authored && ratio(authored, backgroundRgb) >= 4.5 ? preferredInk : fallbackInk;
|
|
4564
|
+
return darkScene ? { ink, backing: "rgba(8,12,22,.78)", border: "rgba(210,222,245,.24)" } : { ink, backing: "rgba(255,255,255,.84)", border: "rgba(11,18,32,.22)" };
|
|
4565
|
+
}
|
|
3647
4566
|
var LabelOverlay = class {
|
|
3648
4567
|
constructor(container, opts = {}) {
|
|
3649
4568
|
this.nodes = /* @__PURE__ */ new Map();
|
|
@@ -3702,7 +4621,9 @@ var LabelOverlay = class {
|
|
|
3702
4621
|
*/
|
|
3703
4622
|
update(viewProjection, width, height, cameraDistance, venueRadius, cameraWorld) {
|
|
3704
4623
|
if (!this.labels.length) return;
|
|
3705
|
-
const kinds = visibleLabelKinds(cameraDistance, venueRadius
|
|
4624
|
+
const kinds = visibleLabelKinds(cameraDistance, venueRadius, {
|
|
4625
|
+
hasZoneLabels: this.labels.some((label) => label.kind === "zone")
|
|
4626
|
+
});
|
|
3706
4627
|
if (this.forcedDense) {
|
|
3707
4628
|
kinds.add("row");
|
|
3708
4629
|
kinds.add("seat");
|
|
@@ -3724,7 +4645,14 @@ var LabelOverlay = class {
|
|
|
3724
4645
|
...this.selectedSeatLabelIds.has(label.id) ? { priority: 1 } : {}
|
|
3725
4646
|
});
|
|
3726
4647
|
}
|
|
3727
|
-
const
|
|
4648
|
+
const floorLabels = spreadFloorLabels(
|
|
4649
|
+
candidates.filter((candidate) => candidate.label.kind === "floor"),
|
|
4650
|
+
width
|
|
4651
|
+
);
|
|
4652
|
+
const structure = [
|
|
4653
|
+
...candidates.filter((candidate) => !DENSE_KINDS.has(candidate.label.kind) && candidate.label.kind !== "floor"),
|
|
4654
|
+
...floorLabels
|
|
4655
|
+
];
|
|
3728
4656
|
const kept = [
|
|
3729
4657
|
...cullOverlapping(structure, SEPARATION_X_PX, SEPARATION_Y_PX),
|
|
3730
4658
|
// The dense rungs are BUDGETED, not merely deduplicated — see
|
|
@@ -3771,14 +4699,16 @@ var LabelOverlay = class {
|
|
|
3771
4699
|
s.fontSize = `${style.size}px`;
|
|
3772
4700
|
s.fontWeight = selected ? "800" : style.weight;
|
|
3773
4701
|
s.opacity = selected ? "1" : String(style.opacity);
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
s.
|
|
3777
|
-
s.
|
|
3778
|
-
s.
|
|
3779
|
-
s.
|
|
3780
|
-
s.
|
|
3781
|
-
|
|
4702
|
+
const treatment = resolve3dLabelTreatment(label.color ?? this.opts.ink, this.opts.backgroundRgb);
|
|
4703
|
+
const backed = !DENSE_KINDS.has(label.kind);
|
|
4704
|
+
s.color = selected ? "#ecf8ff" : treatment.ink;
|
|
4705
|
+
s.background = selected ? "rgba(10,28,45,.9)" : backed ? treatment.backing : "";
|
|
4706
|
+
s.border = selected ? "1px solid rgba(89,199,255,.92)" : backed ? `1px solid ${treatment.border}` : "";
|
|
4707
|
+
s.borderRadius = selected || backed ? "999px" : "";
|
|
4708
|
+
s.padding = selected ? "3px 7px" : backed ? "3px 7px" : "";
|
|
4709
|
+
s.boxShadow = selected ? "0 0 0 2px rgba(41,176,255,.18), 0 5px 14px rgba(0,0,0,.35)" : backed ? "0 3px 10px rgba(0,0,0,.24)" : "";
|
|
4710
|
+
s.letterSpacing = label.kind === "zone" || label.kind === "floor" ? "0.08em" : "0.02em";
|
|
4711
|
+
if (label.kind === "zone" || label.kind === "floor") s.textTransform = "uppercase";
|
|
3782
4712
|
return node;
|
|
3783
4713
|
}
|
|
3784
4714
|
dispose() {
|
|
@@ -3832,8 +4762,7 @@ out float vDim;
|
|
|
3832
4762
|
out float vMaterial;
|
|
3833
4763
|
void main() {
|
|
3834
4764
|
// Per-floor isolation without splitting the merged mesh into a draw call per
|
|
3835
|
-
// floor
|
|
3836
|
-
// buyer keeps the whole venue as context while looking at one level.
|
|
4765
|
+
// floor. The fragment pass removes non-focused floors for a clean level view.
|
|
3837
4766
|
vDim = (uFocusFloor < -0.5 || abs(floorIndex - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
3838
4767
|
vMaterial = materialIndex;
|
|
3839
4768
|
vec4 world = modelMatrix * vec4(position, 1.0);
|
|
@@ -3864,13 +4793,19 @@ uniform vec3 uKeyDir; // WORLD space, unit, points at
|
|
|
3864
4793
|
uniform float uStructureDetail;
|
|
3865
4794
|
out vec4 fragColor;
|
|
3866
4795
|
void main() {
|
|
4796
|
+
if (vDim > 0.5) discard;
|
|
3867
4797
|
// Row treads/risers are honest and useful when the buyer is inside a stand.
|
|
3868
4798
|
// At whole-venue scale their overlapping centimetre-scale surfaces quantise
|
|
3869
4799
|
// and alias into camera-dependent radial combs. The clean structural block
|
|
3870
4800
|
// cap remains, so overview keeps every stand without pretending to show row
|
|
3871
4801
|
// detail below a useful pixel size.
|
|
3872
4802
|
float rowDetail = step(1.5, vMaterial);
|
|
3873
|
-
|
|
4803
|
+
// The rung DISSOLVES rather than popping: uStructureDetail eases 0..1 over a
|
|
4804
|
+
// few frames (see the render loop) and each pixel holds a stable screen-door
|
|
4805
|
+
// threshold against it, so free-zooming across the handover reads as detail
|
|
4806
|
+
// developing instead of the venue changing in a single frame.
|
|
4807
|
+
float stipple = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
|
4808
|
+
if (rowDetail > 0.5 && stipple >= uStructureDetail) discard;
|
|
3874
4809
|
vec3 N = normalize(vNormalWorld);
|
|
3875
4810
|
vec3 V = normalize(-vPosView);
|
|
3876
4811
|
float hemi = 0.5 + 0.5 * N.y; // sky/ground gradient about WORLD up
|
|
@@ -3906,8 +4841,6 @@ void main() {
|
|
|
3906
4841
|
+ vec3(0.62, 0.46, 0.20) * band * 0.28
|
|
3907
4842
|
+ vec3(0.20, 0.10, 0.34) * focalHalo;
|
|
3908
4843
|
base = mix(base, display, poweredDisplay * verticalFace * 0.92);
|
|
3909
|
-
// Unfocused floors fall back toward the background rather than vanishing.
|
|
3910
|
-
base = mix(base, base * 0.45, vDim);
|
|
3911
4844
|
fragColor = vec4(base, 1.0);
|
|
3912
4845
|
}`
|
|
3913
4846
|
);
|
|
@@ -3991,7 +4924,11 @@ uniform float uSeatDetail; // 0 for venue/area overview; 1 in section/row/sea
|
|
|
3991
4924
|
uniform vec3 uFadeColor;
|
|
3992
4925
|
out vec4 fragColor;
|
|
3993
4926
|
void main() {
|
|
3994
|
-
if (
|
|
4927
|
+
if (vDim > 0.5) discard;
|
|
4928
|
+
// Same screen-door dissolve as the structure rung (uStructureDetail): the
|
|
4929
|
+
// dot layer develops over ~a quarter second instead of appearing at once.
|
|
4930
|
+
float stipple = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
|
4931
|
+
if (stipple >= uSeatDetail) discard;
|
|
3995
4932
|
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
3996
4933
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3997
4934
|
if (d > 1.0) discard;
|
|
@@ -4024,9 +4961,6 @@ void main() {
|
|
|
4024
4961
|
// while zooming. uSeatFade already measures this exact LOD transition: let
|
|
4025
4962
|
// the category-tinted tier carry the overview, then reveal individual live
|
|
4026
4963
|
// availability as the buyer approaches a section.
|
|
4027
|
-
// Seats on an unfocused floor recede with their structure.
|
|
4028
|
-
c = mix(c, uFadeColor, vDim * 0.75);
|
|
4029
|
-
alpha *= mix(1.0, 0.30, vDim);
|
|
4030
4964
|
// Yield to the chair. The chair grows out of this exact point, so through the
|
|
4031
4965
|
// band the dot is always at least as big as the chair inside it and the seat
|
|
4032
4966
|
// never thins out to nothing in between.
|
|
@@ -4171,6 +5105,7 @@ uniform vec3 uKeyDir;
|
|
|
4171
5105
|
uniform vec3 uFadeColor;
|
|
4172
5106
|
out vec4 fragColor;
|
|
4173
5107
|
void main() {
|
|
5108
|
+
if (vDim > 0.5) discard;
|
|
4174
5109
|
vec3 N = normalize(vNormalWorld);
|
|
4175
5110
|
vec3 V = normalize(-vPosView);
|
|
4176
5111
|
// The solids' rig, unchanged, so the chairs sit in the venue's light.
|
|
@@ -4203,7 +5138,6 @@ void main() {
|
|
|
4203
5138
|
// A brighter rim than the solids get: it picks out every chair's own edge,
|
|
4204
5139
|
// which is what stops a block of them merging into one mass up close.
|
|
4205
5140
|
base += vec3(0.26, 0.31, 0.38) * fres * 0.55;
|
|
4206
|
-
base = mix(base, uFadeColor, vDim * 0.75);
|
|
4207
5141
|
fragColor = vec4(base, 1.0);
|
|
4208
5142
|
}`
|
|
4209
5143
|
);
|
|
@@ -4241,6 +5175,7 @@ in vec2 position;
|
|
|
4241
5175
|
in vec3 iOffset;
|
|
4242
5176
|
in float iMaxRadius;
|
|
4243
5177
|
in float iPhysicalSeat;
|
|
5178
|
+
in float iFloor;
|
|
4244
5179
|
uniform mat4 modelViewMatrix;
|
|
4245
5180
|
uniform mat4 projectionMatrix;
|
|
4246
5181
|
uniform float uSeatRadius;
|
|
@@ -4249,9 +5184,11 @@ uniform float uMinPixels;
|
|
|
4249
5184
|
uniform float uPixelToWorld;
|
|
4250
5185
|
uniform float uChairFull;
|
|
4251
5186
|
uniform float uChairNone;
|
|
5187
|
+
uniform float uFocusFloor;
|
|
4252
5188
|
out vec2 vUv;
|
|
4253
5189
|
out float vPhysicalSeat;
|
|
4254
5190
|
out float vDotWeight;
|
|
5191
|
+
out float vHidden;
|
|
4255
5192
|
flat out vec3 vPick;
|
|
4256
5193
|
${CHAIR_WEIGHT_GLSL}
|
|
4257
5194
|
void main() {
|
|
@@ -4267,6 +5204,7 @@ void main() {
|
|
|
4267
5204
|
mv.xy += normalize(vec3(modelViewMatrix * vec4(0.0, 1.0, 0.0, 0.0))).xy * r;
|
|
4268
5205
|
vUv = position;
|
|
4269
5206
|
vPhysicalSeat = iPhysicalSeat;
|
|
5207
|
+
vHidden = (uFocusFloor > -0.5 && abs(iFloor - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4270
5208
|
gl_Position = projectionMatrix * mv;
|
|
4271
5209
|
}`
|
|
4272
5210
|
);
|
|
@@ -4277,9 +5215,11 @@ precision highp float;
|
|
|
4277
5215
|
in vec2 vUv;
|
|
4278
5216
|
in float vPhysicalSeat;
|
|
4279
5217
|
in float vDotWeight;
|
|
5218
|
+
in float vHidden;
|
|
4280
5219
|
flat in vec3 vPick;
|
|
4281
5220
|
out vec4 fragColor;
|
|
4282
5221
|
void main() {
|
|
5222
|
+
if (vHidden > 0.5) discard;
|
|
4283
5223
|
if (vDotWeight <= 0.02) discard; // invisible dot cannot steal a chair tap
|
|
4284
5224
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4285
5225
|
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
@@ -4298,18 +5238,22 @@ in float iYaw;
|
|
|
4298
5238
|
in float iSeed;
|
|
4299
5239
|
in float iPhysicalSeat;
|
|
4300
5240
|
in float iPickIndex;
|
|
5241
|
+
in float iFloor;
|
|
4301
5242
|
uniform mat4 modelViewMatrix;
|
|
4302
5243
|
uniform mat4 projectionMatrix;
|
|
4303
5244
|
uniform float uChairFull;
|
|
4304
5245
|
uniform float uChairNone;
|
|
4305
5246
|
uniform float uBackRake;
|
|
4306
5247
|
uniform float uBackBase;
|
|
5248
|
+
uniform float uFocusFloor;
|
|
4307
5249
|
flat out vec3 vPick;
|
|
5250
|
+
out float vHidden;
|
|
4308
5251
|
${CHAIR_WEIGHT_GLSL}
|
|
4309
5252
|
${OCCUPANT_WEIGHT_GLSL}
|
|
4310
5253
|
void main() {
|
|
4311
5254
|
int id = int(iPickIndex + 0.5); // already global index + 1
|
|
4312
5255
|
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
5256
|
+
vHidden = (uFocusFloor > -0.5 && abs(iFloor - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4313
5257
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4314
5258
|
float viewDepth = -anchor.z;
|
|
4315
5259
|
float depth = max(viewDepth, 0.001);
|
|
@@ -4342,17 +5286,25 @@ var CHAIR_PICK_FRAG = (
|
|
|
4342
5286
|
`#version 300 es
|
|
4343
5287
|
precision highp float;
|
|
4344
5288
|
flat in vec3 vPick;
|
|
5289
|
+
in float vHidden;
|
|
4345
5290
|
out vec4 fragColor;
|
|
4346
|
-
void main() {
|
|
5291
|
+
void main() {
|
|
5292
|
+
if (vHidden > 0.5) discard;
|
|
5293
|
+
fragColor = vec4(vPick, 1.0);
|
|
5294
|
+
}`
|
|
4347
5295
|
);
|
|
4348
5296
|
var PICK_DEPTH_VERT = (
|
|
4349
5297
|
/* glsl */
|
|
4350
5298
|
`#version 300 es
|
|
4351
5299
|
precision highp float;
|
|
4352
5300
|
in vec3 position;
|
|
5301
|
+
in float floorIndex;
|
|
4353
5302
|
uniform mat4 modelViewMatrix;
|
|
4354
5303
|
uniform mat4 projectionMatrix;
|
|
5304
|
+
uniform float uFocusFloor;
|
|
5305
|
+
out float vHidden;
|
|
4355
5306
|
void main() {
|
|
5307
|
+
vHidden = (uFocusFloor > -0.5 && abs(floorIndex - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4356
5308
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
4357
5309
|
}`
|
|
4358
5310
|
);
|
|
@@ -4360,8 +5312,12 @@ var PICK_DEPTH_FRAG = (
|
|
|
4360
5312
|
/* glsl */
|
|
4361
5313
|
`#version 300 es
|
|
4362
5314
|
precision highp float;
|
|
5315
|
+
in float vHidden;
|
|
4363
5316
|
out vec4 fragColor;
|
|
4364
|
-
void main() {
|
|
5317
|
+
void main() {
|
|
5318
|
+
if (vHidden > 0.5) discard;
|
|
5319
|
+
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
|
5320
|
+
}`
|
|
4365
5321
|
);
|
|
4366
5322
|
function createSeatPickProgram(gl) {
|
|
4367
5323
|
return new Program(gl, {
|
|
@@ -4377,7 +5333,8 @@ function createSeatPickProgram(gl) {
|
|
|
4377
5333
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
4378
5334
|
uPixelToWorld: { value: 2e-3 },
|
|
4379
5335
|
uChairFull: { value: CHAIR_FULL_M },
|
|
4380
|
-
uChairNone: { value: CHAIR_NONE_M }
|
|
5336
|
+
uChairNone: { value: CHAIR_NONE_M },
|
|
5337
|
+
uFocusFloor: { value: -1 }
|
|
4381
5338
|
}
|
|
4382
5339
|
});
|
|
4383
5340
|
}
|
|
@@ -4393,7 +5350,8 @@ function createChairPickProgram(gl) {
|
|
|
4393
5350
|
uChairFull: { value: CHAIR_FULL_M },
|
|
4394
5351
|
uChairNone: { value: CHAIR_NONE_M },
|
|
4395
5352
|
uBackRake: { value: BACK_RAKE_SLOPE },
|
|
4396
|
-
uBackBase: { value: BACK_BASE_M }
|
|
5353
|
+
uBackBase: { value: BACK_BASE_M },
|
|
5354
|
+
uFocusFloor: { value: -1 }
|
|
4397
5355
|
}
|
|
4398
5356
|
});
|
|
4399
5357
|
}
|
|
@@ -4404,7 +5362,10 @@ function createPickDepthProgram(gl) {
|
|
|
4404
5362
|
transparent: false,
|
|
4405
5363
|
depthTest: true,
|
|
4406
5364
|
depthWrite: true,
|
|
4407
|
-
cullFace: false
|
|
5365
|
+
cullFace: false,
|
|
5366
|
+
uniforms: {
|
|
5367
|
+
uFocusFloor: { value: -1 }
|
|
5368
|
+
}
|
|
4408
5369
|
});
|
|
4409
5370
|
}
|
|
4410
5371
|
function createSolidProgram(gl) {
|
|
@@ -4738,6 +5699,10 @@ var PickPipeline = class {
|
|
|
4738
5699
|
/** Match the display seat sizing so the pick mask lines up with the dots. */
|
|
4739
5700
|
syncFromSeatProgram(seatProgram) {
|
|
4740
5701
|
for (const k of SYNC_KEYS) this.seatProg.uniforms[k].value = seatProgram.uniforms[k].value;
|
|
5702
|
+
const focusFloor = seatProgram.uniforms.uFocusFloor.value;
|
|
5703
|
+
this.seatProg.uniforms.uFocusFloor.value = focusFloor;
|
|
5704
|
+
this.chairProg.uniforms.uFocusFloor.value = focusFloor;
|
|
5705
|
+
this.depthProg.uniforms.uFocusFloor.value = focusFloor;
|
|
4741
5706
|
}
|
|
4742
5707
|
ensureTarget() {
|
|
4743
5708
|
const w = this.gl.drawingBufferWidth;
|
|
@@ -4995,9 +5960,9 @@ function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEG
|
|
|
4995
5960
|
const uv = new Float32Array(vertexCount * 2);
|
|
4996
5961
|
for (let iLat = 0; iLat < latCount; iLat++) {
|
|
4997
5962
|
const v = iLat / segmentsLat;
|
|
4998
|
-
const
|
|
4999
|
-
const sinPolar = Math.sin(
|
|
5000
|
-
const cosPolar = Math.cos(
|
|
5963
|
+
const polar2 = v * Math.PI;
|
|
5964
|
+
const sinPolar = Math.sin(polar2);
|
|
5965
|
+
const cosPolar = Math.cos(polar2);
|
|
5001
5966
|
for (let iLon = 0; iLon < lonCount; iLon++) {
|
|
5002
5967
|
const u = iLon / segmentsLon;
|
|
5003
5968
|
const azimuth = (u - 0.5) * Math.PI * 2;
|
|
@@ -5025,8 +5990,8 @@ function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEG
|
|
|
5025
5990
|
}
|
|
5026
5991
|
return { position, uv, index };
|
|
5027
5992
|
}
|
|
5028
|
-
function bearingPitchToDirection(
|
|
5029
|
-
const yaw =
|
|
5993
|
+
function bearingPitchToDirection(bearingDeg2, pitchDeg) {
|
|
5994
|
+
const yaw = bearingDeg2 * Math.PI / 180;
|
|
5030
5995
|
const pitch = pitchDeg * Math.PI / 180;
|
|
5031
5996
|
const cosPitch = Math.cos(pitch);
|
|
5032
5997
|
return [cosPitch * Math.sin(yaw), Math.sin(pitch), -cosPitch * Math.cos(yaw)];
|
|
@@ -5530,7 +6495,7 @@ function prepareVenue3D(input) {
|
|
|
5530
6495
|
}
|
|
5531
6496
|
|
|
5532
6497
|
// src/view3d/index.ts
|
|
5533
|
-
var
|
|
6498
|
+
var DEG4 = Math.PI / 180;
|
|
5534
6499
|
var TAP_SLOP = 6;
|
|
5535
6500
|
var TAP_MS = 500;
|
|
5536
6501
|
function mountVenue3D(container, input, opts = {}) {
|
|
@@ -5598,11 +6563,11 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5598
6563
|
const nearBuf = new Int32Array(CHAIR_MAX_INSTANCES);
|
|
5599
6564
|
let lastGatherX = Infinity;
|
|
5600
6565
|
let lastGatherZ = Infinity;
|
|
5601
|
-
let nearFieldDetailEnabled = false;
|
|
5602
6566
|
let cameraDetailVisible = false;
|
|
6567
|
+
let seatDetailMix = 0;
|
|
6568
|
+
let structureDetailMix = 0;
|
|
5603
6569
|
let panoramaSphereVisible = false;
|
|
5604
6570
|
const setNearFieldDetailEnabled = (enabled) => {
|
|
5605
|
-
nearFieldDetailEnabled = enabled;
|
|
5606
6571
|
if (!enabled) cameraDetailVisible = false;
|
|
5607
6572
|
lastGatherX = Infinity;
|
|
5608
6573
|
lastGatherZ = Infinity;
|
|
@@ -5651,27 +6616,36 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5651
6616
|
// first real drag/wheel/pinch (not the intro ease)
|
|
5652
6617
|
);
|
|
5653
6618
|
orbit.setAspect(glctx.aspect);
|
|
6619
|
+
const inferredImportedGeometry = (input.doc.floors?.flatMap((floor) => floor.objects) ?? input.doc.objects).some((object) => object.type === "section" && object.physical3dProvenance?.surfaceKind?.status === "inferred");
|
|
5654
6620
|
const stageAzimuth = (() => {
|
|
6621
|
+
if (inferredImportedGeometry && model.stages.length === 0) return void 0;
|
|
5655
6622
|
const dx = model.focalWorld[0] - model.bounds.center[0];
|
|
5656
6623
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
5657
6624
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
5658
6625
|
})();
|
|
6626
|
+
const overviewFraming = model.floors.length > 1 ? { polarDeg: 48, margin: 1.1 } : void 0;
|
|
5659
6627
|
const reduceMotionOnMount = typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5660
|
-
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6628
|
+
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
5661
6629
|
const cinematic = new Cinematic(orbit.camera);
|
|
5662
6630
|
const restoreContainerPosition = establishPositioningContext(container);
|
|
5663
6631
|
const labelOverlay = new LabelOverlay(container, {
|
|
5664
6632
|
fontFamily: input.doc.theme?.fontFamily,
|
|
5665
|
-
ink: input.doc.theme?.textColor
|
|
6633
|
+
ink: input.doc.theme?.textColor,
|
|
6634
|
+
backgroundRgb: model.theme.background.top
|
|
5666
6635
|
});
|
|
5667
|
-
labelOverlay.setLabels(model.labels);
|
|
5668
6636
|
const baseLabels = model.labels;
|
|
6637
|
+
const labelsForFocusedFloor = () => labelsForFloorView(
|
|
6638
|
+
baseLabels,
|
|
6639
|
+
model.floors.length,
|
|
6640
|
+
focusedFloor
|
|
6641
|
+
);
|
|
6642
|
+
labelOverlay.setLabels(labelsForFocusedFloor());
|
|
5669
6643
|
let focusedSectionId = null;
|
|
5670
6644
|
const showSectionSeatLabels = (sectionId) => {
|
|
5671
6645
|
focusedSectionId = sectionId;
|
|
5672
6646
|
labelOverlay.setForcedDense(!!sectionId);
|
|
5673
6647
|
if (!sectionId) {
|
|
5674
|
-
labelOverlay.setLabels(
|
|
6648
|
+
labelOverlay.setLabels(labelsForFocusedFloor());
|
|
5675
6649
|
return;
|
|
5676
6650
|
}
|
|
5677
6651
|
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
@@ -5679,7 +6653,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5679
6653
|
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
5680
6654
|
const selectedIds = new Set(selection.keys());
|
|
5681
6655
|
const selectedInSection = new Set(sectionSeats.filter((seat) => selectedIds.has(seat.id)).map((seat) => seat.id));
|
|
5682
|
-
const focusedBase =
|
|
6656
|
+
const focusedBase = labelsForFocusedFloor().filter((label) => label.kind !== "row" && label.kind !== "seat" || (label.kind === "row" ? rowIds.has(label.id.slice("row:".length)) : seatIds.has(label.id.slice("seat:".length)) && (selectedInSection.size === 0 || selectedInSection.has(label.id.slice("seat:".length)))));
|
|
5683
6657
|
if (model.seatCount <= 6e3 && selectedInSection.size === 0) {
|
|
5684
6658
|
labelOverlay.setLabels(focusedBase);
|
|
5685
6659
|
return;
|
|
@@ -5710,29 +6684,58 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5710
6684
|
]);
|
|
5711
6685
|
};
|
|
5712
6686
|
rebuildGpu();
|
|
5713
|
-
const loop = new RenderLoop(() => {
|
|
6687
|
+
const loop = new RenderLoop((dt) => {
|
|
5714
6688
|
if (contextLost || !gpu || frozen) return false;
|
|
5715
6689
|
const flying = cinematic.active;
|
|
5716
6690
|
const moving = flying ? cinematic.update(performance.now()) : orbit.update();
|
|
5717
|
-
const
|
|
6691
|
+
const activeFloorBounds = focusedFloor >= 0 ? model.floors[focusedFloor] : null;
|
|
6692
|
+
const lodRadius = activeFloorBounds?.radius ?? model.bounds.radius;
|
|
6693
|
+
const lod = computeSeatLod(orbit.currentDistance, lodRadius);
|
|
5718
6694
|
const clippingBounds = panoramaSphereVisible ? { center: [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z], radius: 520 } : model.renderBounds;
|
|
5719
6695
|
orbit.updateClipping(clippingBounds);
|
|
5720
|
-
const
|
|
5721
|
-
const
|
|
5722
|
-
const
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
6696
|
+
const detailCenter = activeFloorBounds?.center ?? model.renderBounds.center;
|
|
6697
|
+
const detailRadius = activeFloorBounds?.radius ?? model.renderBounds.radius;
|
|
6698
|
+
const withinScope = (center, radius) => {
|
|
6699
|
+
const d = Math.hypot(
|
|
6700
|
+
orbit.camera.position.x - center[0],
|
|
6701
|
+
orbit.camera.position.y - center[1],
|
|
6702
|
+
orbit.camera.position.z - center[2]
|
|
6703
|
+
);
|
|
6704
|
+
return d <= radius * (cameraDetailVisible ? 1.8 : 1.55);
|
|
6705
|
+
};
|
|
6706
|
+
let withinDetail = withinScope(detailCenter, detailRadius);
|
|
6707
|
+
if (!withinDetail && focusedSectionId) {
|
|
6708
|
+
const sec = model.sections.find((candidate) => candidate.id === focusedSectionId);
|
|
6709
|
+
if (sec) withinDetail = withinScope(sec.center, Math.max(12, sec.radius * 2));
|
|
6710
|
+
}
|
|
6711
|
+
cameraDetailVisible = withinDetail;
|
|
5729
6712
|
const u = gpu.seatProgram.uniforms;
|
|
5730
6713
|
u.uSeatScale.value = lod.scale;
|
|
5731
6714
|
u.uSeatFade.value = lod.fade;
|
|
5732
|
-
|
|
5733
|
-
|
|
6715
|
+
const floorSeatDetail = !!activeFloorBounds && floorOverviewSeatDetailVisible(
|
|
6716
|
+
activeFloorBounds.seatCount,
|
|
6717
|
+
glctx.canvas.clientWidth || 1,
|
|
6718
|
+
glctx.canvas.clientHeight || 1
|
|
6719
|
+
);
|
|
6720
|
+
const venueSeatDetail = projectedSeatDetailVisible(
|
|
6721
|
+
model.seats.count,
|
|
6722
|
+
lodRadius,
|
|
6723
|
+
orbit.currentDistance,
|
|
6724
|
+
orbit.camera.fov,
|
|
6725
|
+
glctx.canvas.clientHeight || 1
|
|
6726
|
+
);
|
|
6727
|
+
const seatDetailTarget = cameraDetailVisible || floorSeatDetail || venueSeatDetail ? 1 : 0;
|
|
6728
|
+
const structureDetailTarget = cameraDetailVisible || focusedFloor >= 0 || model.floors.length > 1 ? 1 : 0;
|
|
6729
|
+
const easeIn = 1 - Math.exp(-dt * 9);
|
|
6730
|
+
const easeOut = 1 - Math.exp(-dt * 20);
|
|
6731
|
+
seatDetailMix += (seatDetailTarget - seatDetailMix) * (seatDetailTarget > seatDetailMix ? easeIn : easeOut);
|
|
6732
|
+
structureDetailMix += (structureDetailTarget - structureDetailMix) * (structureDetailTarget > structureDetailMix ? easeIn : easeOut);
|
|
6733
|
+
if (Math.abs(seatDetailTarget - seatDetailMix) < 0.01) seatDetailMix = seatDetailTarget;
|
|
6734
|
+
if (Math.abs(structureDetailTarget - structureDetailMix) < 0.01) structureDetailMix = structureDetailTarget;
|
|
6735
|
+
u.uSeatDetail.value = seatDetailMix;
|
|
6736
|
+
gpu.solidProgram.uniforms.uStructureDetail.value = structureDetailMix;
|
|
5734
6737
|
u.uMinPixels.value = lod.minPixels;
|
|
5735
|
-
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov *
|
|
6738
|
+
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov * DEG4 / 2) / Math.max(1, glctx.pixelHeight);
|
|
5736
6739
|
updateNearField(cameraDetailVisible);
|
|
5737
6740
|
glctx.renderer.render({ scene: gpu.background, clear: true });
|
|
5738
6741
|
glctx.renderer.render({ scene: gpu.main, camera: orbit.camera, clear: false });
|
|
@@ -5741,12 +6744,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5741
6744
|
glctx.canvas.clientWidth || 1,
|
|
5742
6745
|
glctx.canvas.clientHeight || 1,
|
|
5743
6746
|
orbit.currentDistance,
|
|
5744
|
-
|
|
6747
|
+
lodRadius,
|
|
5745
6748
|
// The dense label rungs rank by real distance from the eye, not by the
|
|
5746
6749
|
// orbit radius — at the arrival pose those are wildly different numbers.
|
|
5747
6750
|
[orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z]
|
|
5748
6751
|
);
|
|
5749
|
-
return moving;
|
|
6752
|
+
return moving || seatDetailMix !== seatDetailTarget || structureDetailMix !== structureDetailTarget;
|
|
5750
6753
|
});
|
|
5751
6754
|
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => handle.resize()) : null;
|
|
5752
6755
|
ro?.observe(container);
|
|
@@ -5790,14 +6793,20 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5790
6793
|
const resolvedSeatViewPose = (seatId, idx) => {
|
|
5791
6794
|
const seat = expandedSeatById.get(seatId);
|
|
5792
6795
|
const floorIndex = Math.max(0, Math.round(model.seats.iFloor[idx] ?? 0));
|
|
5793
|
-
const
|
|
5794
|
-
const
|
|
6796
|
+
const sourceFloor = input.doc.floors?.[floorIndex];
|
|
6797
|
+
const sceneFloor = model.floors[floorIndex];
|
|
6798
|
+
const sourceFocal = sourceFloor?.focalPoint ?? input.doc.focalPoint;
|
|
6799
|
+
const sceneFocal = sceneFloor ? { x: sceneFloor.focalWorld[0] / M, y: sceneFloor.focalWorld[2] / M } : sourceFocal;
|
|
6800
|
+
const focalPoint = seat?.focalPoint ? {
|
|
6801
|
+
x: seat.focalPoint.x + sceneFocal.x - sourceFocal.x,
|
|
6802
|
+
y: seat.focalPoint.y + sceneFocal.y - sourceFocal.y
|
|
6803
|
+
} : sceneFocal;
|
|
5795
6804
|
const deck = [
|
|
5796
6805
|
model.seats.iPosition[idx * 3],
|
|
5797
6806
|
model.seats.iPosition[idx * 3 + 1],
|
|
5798
6807
|
model.seats.iPosition[idx * 3 + 2]
|
|
5799
6808
|
];
|
|
5800
|
-
return seatViewPose(deck, focalPoint,
|
|
6809
|
+
return seatViewPose(deck, focalPoint, sourceFloor?.baseHeightM ?? 0, seat?.eyeHeightM);
|
|
5801
6810
|
};
|
|
5802
6811
|
const placeCameraFinal = (finalPos, focal, fov = FOV_END) => {
|
|
5803
6812
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
@@ -6027,8 +7036,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6027
7036
|
overviewChip.className = "sl-3d-overview-control sl-3d-icon-control";
|
|
6028
7037
|
overviewChip.type = "button";
|
|
6029
7038
|
overviewChip.textContent = "\u2302";
|
|
6030
|
-
overviewChip.setAttribute("aria-label", "Fit the whole venue in view");
|
|
6031
|
-
overviewChip.dataset.tooltip = "Fit
|
|
7039
|
+
overviewChip.setAttribute("aria-label", "Fit the active level or whole venue in view");
|
|
7040
|
+
overviewChip.dataset.tooltip = "Fit view";
|
|
6032
7041
|
Object.assign(overviewChip.style, {
|
|
6033
7042
|
width: "44px",
|
|
6034
7043
|
minWidth: "44px",
|
|
@@ -6047,6 +7056,15 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6047
7056
|
overviewChip.textContent = "\u2302";
|
|
6048
7057
|
};
|
|
6049
7058
|
layoutOverviewChip();
|
|
7059
|
+
const applyFloorFocus = (value) => {
|
|
7060
|
+
focusedFloor = value;
|
|
7061
|
+
if (gpu) {
|
|
7062
|
+
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
7063
|
+
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
7064
|
+
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
7065
|
+
}
|
|
7066
|
+
showSectionSeatLabels(null);
|
|
7067
|
+
};
|
|
6050
7068
|
const focusOverview = () => {
|
|
6051
7069
|
if (disposed) return;
|
|
6052
7070
|
opts.onViewTargetChange?.(null);
|
|
@@ -6063,13 +7081,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6063
7081
|
overviewChip.style.display = "";
|
|
6064
7082
|
labelOverlay.setVisible(true);
|
|
6065
7083
|
frozen = false;
|
|
6066
|
-
|
|
7084
|
+
const activeFloor = focusedFloor >= 0 ? model.floors[focusedFloor] : null;
|
|
7085
|
+
setNearFieldDetailEnabled(!!activeFloor);
|
|
6067
7086
|
setSeatViewLocked(false);
|
|
6068
7087
|
cancelFlight();
|
|
6069
7088
|
removeArriveChip();
|
|
6070
7089
|
setNavigationMode("orbit");
|
|
6071
|
-
if (
|
|
6072
|
-
|
|
7090
|
+
if (activeFloor) {
|
|
7091
|
+
const dx = activeFloor.focalWorld[0] - activeFloor.center[0];
|
|
7092
|
+
const dz = activeFloor.focalWorld[2] - activeFloor.center[2];
|
|
7093
|
+
const azimuth = Math.hypot(dx, dz) > activeFloor.radius * 0.12 ? Math.atan2(dx, dz) : stageAzimuth;
|
|
7094
|
+
const floorBounds = { center: activeFloor.center, radius: activeFloor.radius, box: activeFloor.box };
|
|
7095
|
+
const floorFraming = { polarDeg: 58, margin: 1.12 };
|
|
7096
|
+
if (reducedMotion()) orbit.frame(floorBounds, false, azimuth, false, floorFraming);
|
|
7097
|
+
else orbit.frameSoft(floorBounds, azimuth, false, floorFraming);
|
|
7098
|
+
} else if (reducedMotion()) {
|
|
7099
|
+
orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7100
|
+
} else {
|
|
7101
|
+
orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7102
|
+
}
|
|
6073
7103
|
showSectionSeatLabels(null);
|
|
6074
7104
|
loop.requestRender();
|
|
6075
7105
|
};
|
|
@@ -6256,6 +7286,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6256
7286
|
setSeatViewLocked(false);
|
|
6257
7287
|
cinematic.cancel();
|
|
6258
7288
|
removeArriveChip();
|
|
7289
|
+
if (model.floors.length > 1 && focusedFloor !== sec.floorIndex) applyFloorFocus(sec.floorIndex);
|
|
6259
7290
|
setNavigationMode("pan");
|
|
6260
7291
|
showSectionSeatLabels(sectionId);
|
|
6261
7292
|
const dx = sec.focalWorld[0] - sec.center[0];
|
|
@@ -6438,20 +7469,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6438
7469
|
focusFloor(index) {
|
|
6439
7470
|
if (index !== null && !model.floors.some((f) => f.index === index)) return false;
|
|
6440
7471
|
const value = index ?? -1;
|
|
6441
|
-
setNearFieldDetailEnabled(
|
|
7472
|
+
setNearFieldDetailEnabled(index !== null);
|
|
6442
7473
|
setSeatViewLocked(false);
|
|
6443
|
-
|
|
6444
|
-
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
6445
|
-
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
6446
|
-
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
6447
|
-
}
|
|
6448
|
-
focusedFloor = value;
|
|
7474
|
+
applyFloorFocus(value);
|
|
6449
7475
|
setNavigationMode("orbit");
|
|
6450
|
-
if (index
|
|
7476
|
+
if (index === null) {
|
|
7477
|
+
cinematic.cancel();
|
|
7478
|
+
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7479
|
+
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7480
|
+
} else {
|
|
6451
7481
|
const f = model.floors[index];
|
|
6452
7482
|
if (f.seatCount > 0) {
|
|
6453
7483
|
cinematic.cancel();
|
|
6454
|
-
|
|
7484
|
+
const dx = f.focalWorld[0] - f.center[0];
|
|
7485
|
+
const dz = f.focalWorld[2] - f.center[2];
|
|
7486
|
+
const azimuth = Math.hypot(dx, dz) > f.radius * 0.12 ? Math.atan2(dx, dz) : stageAzimuth;
|
|
7487
|
+
orbit.frame({ center: f.center, radius: f.radius, box: f.box }, false, azimuth, false, {
|
|
7488
|
+
polarDeg: 58,
|
|
7489
|
+
margin: 1.12
|
|
7490
|
+
});
|
|
6455
7491
|
}
|
|
6456
7492
|
}
|
|
6457
7493
|
loop.requestRender();
|