@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.cjs
CHANGED
|
@@ -467,7 +467,7 @@ var OrbitCamera = class {
|
|
|
467
467
|
* then the damped `update()` eases it up into the 3/4 architectural angle and
|
|
468
468
|
* dollies in — the venue "stands up" instead of teleporting (~600ms).
|
|
469
469
|
*/
|
|
470
|
-
frame(bounds, intro = false, stageAzimuth, portraitCrop = false) {
|
|
470
|
+
frame(bounds, intro = false, stageAzimuth, portraitCrop = false, framing) {
|
|
471
471
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
472
472
|
this.targetT.copy(this.target);
|
|
473
473
|
this.panAnchor.copy(this.target);
|
|
@@ -477,9 +477,9 @@ var OrbitCamera = class {
|
|
|
477
477
|
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
478
478
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
479
479
|
this.azT = stageAzimuth ?? -30 * DEG;
|
|
480
|
-
this.polT = 55 * DEG;
|
|
480
|
+
this.polT = Math.max(15, Math.min(80, framing?.polarDeg ?? 55)) * DEG;
|
|
481
481
|
const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
|
|
482
|
-
this.distT = fit * FRAME_MARGIN;
|
|
482
|
+
this.distT = fit * Math.max(1.05, Math.min(2, framing?.margin ?? FRAME_MARGIN));
|
|
483
483
|
this.minDist = Math.max(2, r * 0.12);
|
|
484
484
|
this.maxDist = fit * 4;
|
|
485
485
|
if (intro) {
|
|
@@ -501,7 +501,7 @@ var OrbitCamera = class {
|
|
|
501
501
|
* seat, behind the shell, anywhere): re-pivot on the venue centre without
|
|
502
502
|
* moving the camera, then glide targets back to the 3/4 architectural pose.
|
|
503
503
|
*/
|
|
504
|
-
frameSoft(bounds, stageAzimuth, portraitCrop = false) {
|
|
504
|
+
frameSoft(bounds, stageAzimuth, portraitCrop = false, framing) {
|
|
505
505
|
this.target.set(bounds.center[0], bounds.center[1], bounds.center[2]);
|
|
506
506
|
this.targetT.copy(this.target);
|
|
507
507
|
this.panAnchor.copy(this.target);
|
|
@@ -512,9 +512,9 @@ var OrbitCamera = class {
|
|
|
512
512
|
const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
|
|
513
513
|
const halfH = Math.atan(Math.tan(halfV) * aspect);
|
|
514
514
|
this.azT = stageAzimuth ?? this.azimuth;
|
|
515
|
-
this.polT = 55 * DEG;
|
|
515
|
+
this.polT = Math.max(15, Math.min(80, framing?.polarDeg ?? 55)) * DEG;
|
|
516
516
|
const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
|
|
517
|
-
this.distT = fit * FRAME_MARGIN;
|
|
517
|
+
this.distT = fit * Math.max(1.05, Math.min(2, framing?.margin ?? FRAME_MARGIN));
|
|
518
518
|
}
|
|
519
519
|
/**
|
|
520
520
|
* Distance from the bounds centre at which the venue fits the frustum at
|
|
@@ -530,14 +530,14 @@ var OrbitCamera = class {
|
|
|
530
530
|
* every corner does. Clamped to the sphere fit (which always contains the
|
|
531
531
|
* box) and floored so a degenerate box cannot glue the camera to the floor.
|
|
532
532
|
*/
|
|
533
|
-
fitDistance(bounds, halfV, halfH, azimuth,
|
|
533
|
+
fitDistance(bounds, halfV, halfH, azimuth, polar2) {
|
|
534
534
|
const r = Math.max(1, bounds.radius);
|
|
535
535
|
const sphereFit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
|
|
536
536
|
const box = bounds.box;
|
|
537
537
|
if (!box) return sphereFit;
|
|
538
|
-
const sp = Math.sin(
|
|
538
|
+
const sp = Math.sin(polar2);
|
|
539
539
|
const fx = -sp * Math.sin(azimuth);
|
|
540
|
-
const fy = -Math.cos(
|
|
540
|
+
const fy = -Math.cos(polar2);
|
|
541
541
|
const fz = -sp * Math.cos(azimuth);
|
|
542
542
|
if (Math.abs(fy) > 0.999) return sphereFit;
|
|
543
543
|
let rx = -fz, rz = fx;
|
|
@@ -614,9 +614,9 @@ var OrbitCamera = class {
|
|
|
614
614
|
const dy = this.camera.position.y - this.target.y;
|
|
615
615
|
const dz = this.camera.position.z - this.target.z;
|
|
616
616
|
const dist = Math.hypot(dx, dy, dz) || 1;
|
|
617
|
-
const
|
|
617
|
+
const polar2 = Math.max(POLAR_MIN, Math.min(POLAR_MAX, Math.acos(Math.max(-1, Math.min(1, dy / dist)))));
|
|
618
618
|
this.distance = this.distT = dist;
|
|
619
|
-
this.polar = this.polT =
|
|
619
|
+
this.polar = this.polT = polar2;
|
|
620
620
|
this.azimuth = this.azT = Math.atan2(dx, dz);
|
|
621
621
|
}
|
|
622
622
|
/** Point the orbit pivot at a new world target without moving the camera. */
|
|
@@ -722,6 +722,20 @@ function computeSeatLod(distance, radius) {
|
|
|
722
722
|
minPixels: SEAT_MIN_PIXELS_NEAR + t * (SEAT_MIN_PIXELS_FAR - SEAT_MIN_PIXELS_NEAR)
|
|
723
723
|
};
|
|
724
724
|
}
|
|
725
|
+
function floorOverviewSeatDetailVisible(seatCount, viewportWidth, viewportHeight) {
|
|
726
|
+
if (!(seatCount > 0) || !(viewportWidth > 0) || !(viewportHeight > 0)) return false;
|
|
727
|
+
const pixelsPerSeat = viewportWidth * viewportHeight / seatCount;
|
|
728
|
+
return pixelsPerSeat >= 64;
|
|
729
|
+
}
|
|
730
|
+
function projectedSeatDetailVisible(seatCount, venueRadius, cameraDistance, fovYDeg, viewportHeightPx) {
|
|
731
|
+
if (!(seatCount > 0) || !(venueRadius > 0) || !(cameraDistance > 0)) return false;
|
|
732
|
+
if (!(fovYDeg > 0) || !(viewportHeightPx > 0)) return false;
|
|
733
|
+
const halfFov = Math.tan(fovYDeg / 2 * Math.PI / 180);
|
|
734
|
+
if (!(halfFov > 0)) return false;
|
|
735
|
+
const rPx = venueRadius / (cameraDistance * halfFov) * (viewportHeightPx / 2);
|
|
736
|
+
const projectedArea = Math.PI * rPx * rPx;
|
|
737
|
+
return projectedArea / seatCount >= 64;
|
|
738
|
+
}
|
|
725
739
|
|
|
726
740
|
// src/view3d/scene/nearField.ts
|
|
727
741
|
var SEATS_PER_CELL = 4;
|
|
@@ -1758,17 +1772,45 @@ function themeSeatColorLUT(theme, order) {
|
|
|
1758
1772
|
}
|
|
1759
1773
|
|
|
1760
1774
|
// src/view3d/labels.ts
|
|
1775
|
+
function labelsForFloorView(labels, floorCount, focusedFloor) {
|
|
1776
|
+
if (focusedFloor < 0) {
|
|
1777
|
+
return floorCount > 1 ? labels.filter((label) => label.kind === "floor") : [...labels];
|
|
1778
|
+
}
|
|
1779
|
+
return labels.filter((label) => label.floorIndex === void 0 || label.floorIndex === focusedFloor);
|
|
1780
|
+
}
|
|
1781
|
+
function spreadFloorLabels(items, viewportWidth) {
|
|
1782
|
+
const ordered = [...items].sort((left, right) => left.screen.y - right.screen.y);
|
|
1783
|
+
const clusters = [];
|
|
1784
|
+
for (const item of ordered) {
|
|
1785
|
+
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));
|
|
1786
|
+
if (cluster) cluster.push(item);
|
|
1787
|
+
else clusters.push([item]);
|
|
1788
|
+
}
|
|
1789
|
+
return clusters.flatMap((cluster) => {
|
|
1790
|
+
if (cluster.length < 2) return cluster;
|
|
1791
|
+
return cluster.map((item, index) => ({
|
|
1792
|
+
...item,
|
|
1793
|
+
screen: {
|
|
1794
|
+
...item.screen,
|
|
1795
|
+
x: Math.max(54, Math.min(viewportWidth - 54, item.screen.x + (index - (cluster.length - 1) / 2) * 104))
|
|
1796
|
+
}
|
|
1797
|
+
}));
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1761
1800
|
var ZONE_MIN_DISTANCE = 1.15;
|
|
1762
|
-
var SECTION_MAX_DISTANCE =
|
|
1801
|
+
var SECTION_MAX_DISTANCE = 3.25;
|
|
1802
|
+
var SECTION_MAX_DISTANCE_NO_ZONES = 4.5;
|
|
1763
1803
|
var NEAR_MAX_DISTANCE = 0.85;
|
|
1764
1804
|
var ROW_MAX_DISTANCE = 0.5;
|
|
1765
1805
|
var SEAT_MAX_DISTANCE = 0.26;
|
|
1766
|
-
function visibleLabelKinds(distance, venueRadius) {
|
|
1806
|
+
function visibleLabelKinds(distance, venueRadius, opts) {
|
|
1767
1807
|
const r = Math.max(1e-6, venueRadius);
|
|
1768
1808
|
const d = distance / r;
|
|
1809
|
+
const hasZones = opts?.hasZoneLabels ?? true;
|
|
1769
1810
|
const out = /* @__PURE__ */ new Set();
|
|
1811
|
+
if (d >= 0.75) out.add("floor");
|
|
1770
1812
|
if (d >= ZONE_MIN_DISTANCE) out.add("zone");
|
|
1771
|
-
if (d <= SECTION_MAX_DISTANCE) {
|
|
1813
|
+
if (d <= (hasZones ? SECTION_MAX_DISTANCE : SECTION_MAX_DISTANCE_NO_ZONES)) {
|
|
1772
1814
|
out.add("section");
|
|
1773
1815
|
out.add("ga");
|
|
1774
1816
|
}
|
|
@@ -2919,7 +2961,7 @@ function pointInRing2(ring, x, y) {
|
|
|
2919
2961
|
return inside;
|
|
2920
2962
|
}
|
|
2921
2963
|
var MAX_TIER_RISE_M = 25;
|
|
2922
|
-
function buildVenueSurfaces(units, seats) {
|
|
2964
|
+
function buildVenueSurfaces(units, seats, seatFloorIndices) {
|
|
2923
2965
|
const bySection = /* @__PURE__ */ new Map();
|
|
2924
2966
|
const seatOwner = new Array(seats.length).fill(null);
|
|
2925
2967
|
const seatDeck = new Float64Array(seats.length);
|
|
@@ -2928,18 +2970,20 @@ function buildVenueSurfaces(units, seats) {
|
|
|
2928
2970
|
const acc = /* @__PURE__ */ new Map();
|
|
2929
2971
|
const boxes = [];
|
|
2930
2972
|
const tableRowIds = /* @__PURE__ */ new Set();
|
|
2931
|
-
for (
|
|
2973
|
+
for (let floorIndex = 0; floorIndex < units.length; floorIndex += 1) {
|
|
2974
|
+
const unit = units[floorIndex];
|
|
2932
2975
|
for (const o of unit.objects) {
|
|
2933
2976
|
if (o.type === "table") tableRowIds.add(o.id);
|
|
2934
2977
|
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
2935
2978
|
const owned = outsetRing(o.outline, SEAT_OWNERSHIP_PAD_U);
|
|
2936
2979
|
acc.set(o.id, { section: o, unit, frontU: Infinity, hasSeats: false, rows: /* @__PURE__ */ new Map(), seatIndices: [] });
|
|
2937
|
-
boxes.push({ id: o.id, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
2980
|
+
boxes.push({ id: o.id, floorIndex, box: bboxOf(owned), section: o, unit, outline: owned });
|
|
2938
2981
|
}
|
|
2939
2982
|
}
|
|
2940
2983
|
for (let i = 0; i < seats.length; i++) {
|
|
2941
2984
|
const s = seats[i];
|
|
2942
2985
|
for (const b of boxes) {
|
|
2986
|
+
if (seatFloorIndices && b.floorIndex !== seatFloorIndices[i]) continue;
|
|
2943
2987
|
if (s.x < b.box.minX || s.x > b.box.maxX || s.y < b.box.minY || s.y > b.box.maxY) continue;
|
|
2944
2988
|
if (!pointInPolygonWithHoles({ x: s.x, y: s.y }, b.outline, b.section.holes)) continue;
|
|
2945
2989
|
seatOwner[i] = b.id;
|
|
@@ -3097,6 +3141,634 @@ function buildVenueSurfaces(units, seats) {
|
|
|
3097
3141
|
};
|
|
3098
3142
|
}
|
|
3099
3143
|
|
|
3144
|
+
// src/core/bowlProfile.ts
|
|
3145
|
+
var DEG3 = Math.PI / 180;
|
|
3146
|
+
var BIN_COUNT = 96;
|
|
3147
|
+
var GAP_FILL_BINS = 6;
|
|
3148
|
+
var MIN_ARC_DEG = 60;
|
|
3149
|
+
var MIN_RING_DEG = 150;
|
|
3150
|
+
var MIN_HOLE_RATIO = 0.2;
|
|
3151
|
+
var RING_OVERLAP_RATIO = 0.35;
|
|
3152
|
+
function bearingDeg(c, p) {
|
|
3153
|
+
const d = Math.atan2(p.x - c.x, p.y - c.y) / DEG3;
|
|
3154
|
+
return d < 0 ? d + 360 : d;
|
|
3155
|
+
}
|
|
3156
|
+
function median(values) {
|
|
3157
|
+
if (!values.length) return NaN;
|
|
3158
|
+
const s = [...values].sort((a, b) => a - b);
|
|
3159
|
+
return s[Math.floor(s.length / 2)];
|
|
3160
|
+
}
|
|
3161
|
+
function smoothRun(values, wrap, window2 = 2) {
|
|
3162
|
+
const n = values.length;
|
|
3163
|
+
const out = new Float64Array(n);
|
|
3164
|
+
for (let i = 0; i < n; i++) {
|
|
3165
|
+
let sum = 0, cnt = 0;
|
|
3166
|
+
for (let k = -window2; k <= window2; k++) {
|
|
3167
|
+
let j = i + k;
|
|
3168
|
+
if (wrap) j = (j % n + n) % n;
|
|
3169
|
+
else if (j < 0 || j >= n) continue;
|
|
3170
|
+
sum += values[j];
|
|
3171
|
+
cnt++;
|
|
3172
|
+
}
|
|
3173
|
+
out[i] = sum / cnt;
|
|
3174
|
+
}
|
|
3175
|
+
return out;
|
|
3176
|
+
}
|
|
3177
|
+
function fitBowlProfile(sections, focal) {
|
|
3178
|
+
const usable = sections.filter((s) => s.rows.length >= 1);
|
|
3179
|
+
if (usable.length < 1) return null;
|
|
3180
|
+
const centres = [];
|
|
3181
|
+
let cx = 0, cy = 0, np = 0;
|
|
3182
|
+
for (const s of usable) {
|
|
3183
|
+
for (const row of s.rows) {
|
|
3184
|
+
for (const p of row.pts) {
|
|
3185
|
+
cx += p.x;
|
|
3186
|
+
cy += p.y;
|
|
3187
|
+
np++;
|
|
3188
|
+
}
|
|
3189
|
+
if (row.pts.length >= 4) {
|
|
3190
|
+
const c = fitCircle(row.pts);
|
|
3191
|
+
if (c && Number.isFinite(c.cx) && Number.isFinite(c.cy)) centres.push({ x: c.cx, y: c.cy });
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
if (!np) return null;
|
|
3196
|
+
const centroid2 = { x: cx / np, y: cy / np };
|
|
3197
|
+
let centre = centroid2;
|
|
3198
|
+
if (centres.length >= 8) {
|
|
3199
|
+
const mx = median(centres.map((c) => c.x));
|
|
3200
|
+
const my = median(centres.map((c) => c.y));
|
|
3201
|
+
let spread = 0;
|
|
3202
|
+
for (const s of usable) for (const row of s.rows) for (const p of row.pts) {
|
|
3203
|
+
const d = Math.hypot(p.x - centroid2.x, p.y - centroid2.y);
|
|
3204
|
+
if (d > spread) spread = d;
|
|
3205
|
+
}
|
|
3206
|
+
let near = 0;
|
|
3207
|
+
const nearR = spread * 0.2;
|
|
3208
|
+
for (const s of usable) for (const row of s.rows) for (const p of row.pts) {
|
|
3209
|
+
if (Math.hypot(p.x - mx, p.y - my) < nearR) near++;
|
|
3210
|
+
}
|
|
3211
|
+
if (Math.hypot(mx - centroid2.x, my - centroid2.y) <= spread && near / np < 0.01) centre = { x: mx, y: my };
|
|
3212
|
+
}
|
|
3213
|
+
const stats = [];
|
|
3214
|
+
for (const s of usable) {
|
|
3215
|
+
const st = { sectionId: s.sectionId, rMin: Infinity, rMax: -Infinity, samples: [] };
|
|
3216
|
+
for (const row of s.rows) {
|
|
3217
|
+
for (const p of row.pts) {
|
|
3218
|
+
const r = Math.hypot(p.x - centre.x, p.y - centre.y);
|
|
3219
|
+
const bin = Math.min(BIN_COUNT - 1, Math.floor(bearingDeg(centre, p) / (360 / BIN_COUNT)));
|
|
3220
|
+
st.samples.push({ r, bin, levelM: row.levelM });
|
|
3221
|
+
if (r < st.rMin) st.rMin = r;
|
|
3222
|
+
if (r > st.rMax) st.rMax = r;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (st.samples.length) stats.push(st);
|
|
3226
|
+
}
|
|
3227
|
+
if (!stats.length) return null;
|
|
3228
|
+
stats.sort((a, b) => a.rMin + a.rMax - (b.rMin + b.rMax));
|
|
3229
|
+
const ringAccs = [];
|
|
3230
|
+
for (const st of stats) {
|
|
3231
|
+
let joined = false;
|
|
3232
|
+
for (const ring of ringAccs) {
|
|
3233
|
+
const overlap = Math.min(st.rMax, ring.rMax) - Math.max(st.rMin, ring.rMin);
|
|
3234
|
+
const thinner = Math.min(st.rMax - st.rMin, ring.rMax - ring.rMin);
|
|
3235
|
+
if (thinner > 1e-9 && overlap >= RING_OVERLAP_RATIO * thinner) {
|
|
3236
|
+
ring.members.push(st);
|
|
3237
|
+
ring.rMin = Math.min(ring.rMin, st.rMin);
|
|
3238
|
+
ring.rMax = Math.max(ring.rMax, st.rMax);
|
|
3239
|
+
joined = true;
|
|
3240
|
+
break;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (!joined) ringAccs.push({ members: [st], rMin: st.rMin, rMax: st.rMax });
|
|
3244
|
+
}
|
|
3245
|
+
const rings = [];
|
|
3246
|
+
for (const ring of ringAccs) {
|
|
3247
|
+
const bins = new Array(BIN_COUNT).fill(null);
|
|
3248
|
+
for (const st of ring.members) {
|
|
3249
|
+
for (const s of st.samples) {
|
|
3250
|
+
let b = bins[s.bin];
|
|
3251
|
+
if (!b) {
|
|
3252
|
+
b = { samples: [] };
|
|
3253
|
+
bins[s.bin] = b;
|
|
3254
|
+
}
|
|
3255
|
+
b.samples.push({ r: s.r, levelM: s.levelM });
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
const covered = bins.map((b) => !!b);
|
|
3259
|
+
const coveredCount = covered.filter(Boolean).length;
|
|
3260
|
+
if (!coveredCount) continue;
|
|
3261
|
+
const innerSeed = [];
|
|
3262
|
+
const outerSeed = [];
|
|
3263
|
+
for (const b of bins) {
|
|
3264
|
+
if (!b) continue;
|
|
3265
|
+
let lo = Infinity, hi = -Infinity;
|
|
3266
|
+
for (const s of b.samples) {
|
|
3267
|
+
if (s.r < lo) lo = s.r;
|
|
3268
|
+
if (s.r > hi) hi = s.r;
|
|
3269
|
+
}
|
|
3270
|
+
innerSeed.push(lo);
|
|
3271
|
+
outerSeed.push(hi);
|
|
3272
|
+
}
|
|
3273
|
+
const innerRMedian = median(innerSeed);
|
|
3274
|
+
const outerRMedian = median(outerSeed);
|
|
3275
|
+
if (!(innerRMedian >= MIN_HOLE_RATIO * outerRMedian) || !(outerRMedian > 0)) continue;
|
|
3276
|
+
const sortedInner = [...innerSeed].sort((a, b) => a - b);
|
|
3277
|
+
const innerP10 = sortedInner[Math.floor(sortedInner.length * 0.1)];
|
|
3278
|
+
if (!(innerP10 >= MIN_HOLE_RATIO * outerRMedian)) continue;
|
|
3279
|
+
const vals = bins.map((b) => {
|
|
3280
|
+
if (!b) return null;
|
|
3281
|
+
const sorted = [...b.samples].sort((u, v) => u.r - v.r);
|
|
3282
|
+
const inner = sorted[0].r;
|
|
3283
|
+
const outer = sorted[sorted.length - 1].r;
|
|
3284
|
+
const front = sorted[0].levelM;
|
|
3285
|
+
let top = -Infinity;
|
|
3286
|
+
for (const s of sorted) if (s.levelM > top) top = s.levelM;
|
|
3287
|
+
const hull = [];
|
|
3288
|
+
for (const s of sorted) {
|
|
3289
|
+
const pt = { r: s.r, l: s.levelM };
|
|
3290
|
+
while (hull.length && Math.abs(hull[hull.length - 1].r - pt.r) < 1e-9) {
|
|
3291
|
+
if (hull[hull.length - 1].l <= pt.l) {
|
|
3292
|
+
pt.skip = true;
|
|
3293
|
+
break;
|
|
3294
|
+
}
|
|
3295
|
+
hull.pop();
|
|
3296
|
+
}
|
|
3297
|
+
if (pt.skip) continue;
|
|
3298
|
+
while (hull.length >= 2) {
|
|
3299
|
+
const a = hull[hull.length - 2], b2 = hull[hull.length - 1];
|
|
3300
|
+
if ((b2.r - a.r) * (pt.l - a.l) - (b2.l - a.l) * (pt.r - a.r) <= 0) hull.pop();
|
|
3301
|
+
else break;
|
|
3302
|
+
}
|
|
3303
|
+
hull.push(pt);
|
|
3304
|
+
}
|
|
3305
|
+
const rs = hull.map((p) => p.r);
|
|
3306
|
+
const ls = hull.map((p) => p.l);
|
|
3307
|
+
return { inner, outer, front, top, profile: { rs, ls } };
|
|
3308
|
+
});
|
|
3309
|
+
const filled = covered.slice();
|
|
3310
|
+
for (let i = 0; i < BIN_COUNT; i++) {
|
|
3311
|
+
if (filled[i]) continue;
|
|
3312
|
+
let before = -1, after = -1, gap = 0;
|
|
3313
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
3314
|
+
const j = (i - k + BIN_COUNT * 2) % BIN_COUNT;
|
|
3315
|
+
if (filled[j]) {
|
|
3316
|
+
before = j;
|
|
3317
|
+
break;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
3321
|
+
const j = (i + k) % BIN_COUNT;
|
|
3322
|
+
if (filled[j]) {
|
|
3323
|
+
after = j;
|
|
3324
|
+
gap = k;
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (before < 0 || after < 0) continue;
|
|
3329
|
+
let back = 0;
|
|
3330
|
+
for (let k = 1; k <= BIN_COUNT; k++) {
|
|
3331
|
+
const j = (i - k + BIN_COUNT * 2) % BIN_COUNT;
|
|
3332
|
+
if (covered[j]) {
|
|
3333
|
+
back = k;
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
const gapLen = back - 1 + gap;
|
|
3338
|
+
if (gapLen > GAP_FILL_BINS) continue;
|
|
3339
|
+
const a = vals[before], b = vals[after];
|
|
3340
|
+
const t = back / (back + gap);
|
|
3341
|
+
const inner = a.inner + (b.inner - a.inner) * t;
|
|
3342
|
+
const outer = a.outer + (b.outer - a.outer) * t;
|
|
3343
|
+
const front = a.front + (b.front - a.front) * t;
|
|
3344
|
+
const top = a.top + (b.top - a.top) * t;
|
|
3345
|
+
vals[i] = { inner, outer, front, top, profile: { rs: [inner, outer], ls: [front, top] } };
|
|
3346
|
+
}
|
|
3347
|
+
for (let i = 0; i < BIN_COUNT; i++) if (vals[i]) filled[i] = true;
|
|
3348
|
+
const stepDeg = 360 / BIN_COUNT;
|
|
3349
|
+
const arcs = [];
|
|
3350
|
+
const filledCount = filled.filter(Boolean).length;
|
|
3351
|
+
const runs = [];
|
|
3352
|
+
if (filledCount === BIN_COUNT) {
|
|
3353
|
+
runs.push({ start: 0, len: BIN_COUNT });
|
|
3354
|
+
} else {
|
|
3355
|
+
let scan = 0;
|
|
3356
|
+
while (filled[scan]) scan = (scan + 1) % BIN_COUNT;
|
|
3357
|
+
let runStart = -1;
|
|
3358
|
+
for (let k = 0; k <= BIN_COUNT; k++) {
|
|
3359
|
+
const j = (scan + k) % BIN_COUNT;
|
|
3360
|
+
const on = k < BIN_COUNT && filled[j];
|
|
3361
|
+
if (on && runStart < 0) runStart = j;
|
|
3362
|
+
else if (!on && runStart >= 0) {
|
|
3363
|
+
const len = (j - runStart + BIN_COUNT) % BIN_COUNT || BIN_COUNT;
|
|
3364
|
+
runs.push({ start: runStart, len });
|
|
3365
|
+
runStart = -1;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
for (const run of runs) {
|
|
3370
|
+
if (run.len * stepDeg < MIN_ARC_DEG && run.len < BIN_COUNT) continue;
|
|
3371
|
+
const closed = run.len === BIN_COUNT;
|
|
3372
|
+
const innerR = new Float64Array(run.len);
|
|
3373
|
+
const outerR = new Float64Array(run.len);
|
|
3374
|
+
const frontL = new Float64Array(run.len);
|
|
3375
|
+
const topL = new Float64Array(run.len);
|
|
3376
|
+
const profiles = [];
|
|
3377
|
+
for (let k = 0; k < run.len; k++) {
|
|
3378
|
+
const v = vals[(run.start + k) % BIN_COUNT];
|
|
3379
|
+
innerR[k] = v.inner;
|
|
3380
|
+
outerR[k] = v.outer;
|
|
3381
|
+
frontL[k] = v.front;
|
|
3382
|
+
topL[k] = v.top;
|
|
3383
|
+
profiles.push(v.profile ?? { rs: [v.inner, v.outer], ls: [v.front, v.top] });
|
|
3384
|
+
}
|
|
3385
|
+
const sInner = smoothRun(innerR, closed);
|
|
3386
|
+
const sOuter = smoothRun(outerR, closed);
|
|
3387
|
+
const sFront = smoothRun(frontL, closed);
|
|
3388
|
+
const sTop = smoothRun(topL, closed);
|
|
3389
|
+
for (let k = 0; k < run.len; k++) {
|
|
3390
|
+
if (sInner[k] > innerR[k]) sInner[k] = innerR[k];
|
|
3391
|
+
if (sOuter[k] < outerR[k]) sOuter[k] = outerR[k];
|
|
3392
|
+
if (sInner[k] > sOuter[k] - 1e-6) sInner[k] = Math.max(0, sOuter[k] - 1e-6);
|
|
3393
|
+
if (sTop[k] < sFront[k]) sTop[k] = sFront[k];
|
|
3394
|
+
}
|
|
3395
|
+
const arc = { startBin: run.start, binCount: run.len, closed, innerR: sInner, outerR: sOuter, frontLevelM: sFront, topLevelM: sTop, profiles };
|
|
3396
|
+
const clampProfile = (k, r, levelM) => {
|
|
3397
|
+
const prof = profiles[k];
|
|
3398
|
+
const rs = prof.rs, ls = prof.ls;
|
|
3399
|
+
if (r <= rs[0]) {
|
|
3400
|
+
if (ls[0] > levelM) ls[0] = levelM;
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
if (r >= rs[rs.length - 1]) {
|
|
3404
|
+
if (ls[ls.length - 1] > levelM) ls[ls.length - 1] = levelM;
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
for (let j = 0; j + 1 < rs.length; j++) {
|
|
3408
|
+
if (r <= rs[j + 1]) {
|
|
3409
|
+
if (ls[j] > levelM) ls[j] = levelM;
|
|
3410
|
+
if (ls[j + 1] > levelM) ls[j + 1] = levelM;
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
for (let k = 0; k < run.len; k++) {
|
|
3416
|
+
const b = bins[(run.start + k) % BIN_COUNT];
|
|
3417
|
+
if (!b) continue;
|
|
3418
|
+
for (const s of b.samples) {
|
|
3419
|
+
clampProfile(k, s.r, s.levelM);
|
|
3420
|
+
const prev = closed ? (k - 1 + run.len) % run.len : k - 1;
|
|
3421
|
+
const next = closed ? (k + 1) % run.len : k + 1;
|
|
3422
|
+
if (prev >= 0) clampProfile(prev, s.r, s.levelM);
|
|
3423
|
+
if (next < run.len) clampProfile(next, s.r, s.levelM);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
arcs.push(arc);
|
|
3427
|
+
}
|
|
3428
|
+
const totalDeg = arcs.reduce((sum, a) => sum + a.binCount * stepDeg, 0);
|
|
3429
|
+
if (totalDeg < MIN_RING_DEG) continue;
|
|
3430
|
+
rings.push({
|
|
3431
|
+
arcs,
|
|
3432
|
+
sectionIds: ring.members.map((m) => m.sectionId),
|
|
3433
|
+
innerRMedian,
|
|
3434
|
+
outerRMedian
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
if (!rings.length) return null;
|
|
3438
|
+
rings.sort((a, b) => a.innerRMedian - b.innerRMedian);
|
|
3439
|
+
if (focal) {
|
|
3440
|
+
const outermost = rings[rings.length - 1].outerRMedian;
|
|
3441
|
+
if (Math.hypot(focal.x - centre.x, focal.y - centre.y) > 0.9 * outermost) return null;
|
|
3442
|
+
}
|
|
3443
|
+
return { centre, binCount: BIN_COUNT, binStepDeg: 360 / BIN_COUNT, rings };
|
|
3444
|
+
}
|
|
3445
|
+
function arcLevelAt(fit, arc, bearing, r) {
|
|
3446
|
+
const step = fit.binStepDeg;
|
|
3447
|
+
let f = bearing / step - 0.5 - arc.startBin;
|
|
3448
|
+
f = (f % fit.binCount + fit.binCount) % fit.binCount;
|
|
3449
|
+
if (f > arc.binCount - 1) {
|
|
3450
|
+
if (arc.closed) f = f - arc.binCount < arc.binCount - f ? 0 : arc.binCount - 1;
|
|
3451
|
+
else f = f - (arc.binCount - 1) < fit.binCount - f ? arc.binCount - 1 : 0;
|
|
3452
|
+
}
|
|
3453
|
+
const i0 = Math.floor(f);
|
|
3454
|
+
const i1 = arc.closed ? (i0 + 1) % arc.binCount : Math.min(arc.binCount - 1, i0 + 1);
|
|
3455
|
+
const t = f - i0;
|
|
3456
|
+
const sample = (i) => {
|
|
3457
|
+
const prof = arc.profiles[i];
|
|
3458
|
+
const rs = prof.rs, ls = prof.ls;
|
|
3459
|
+
if (r <= rs[0]) return ls[0];
|
|
3460
|
+
if (r >= rs[rs.length - 1]) return ls[ls.length - 1];
|
|
3461
|
+
for (let k = 0; k + 1 < rs.length; k++) {
|
|
3462
|
+
if (r <= rs[k + 1]) {
|
|
3463
|
+
const span = rs[k + 1] - rs[k];
|
|
3464
|
+
const u = span > 1e-9 ? (r - rs[k]) / span : 0;
|
|
3465
|
+
return ls[k] + (ls[k + 1] - ls[k]) * u;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return ls[ls.length - 1];
|
|
3469
|
+
};
|
|
3470
|
+
return sample(i0) * (1 - t) + sample(i1) * t;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// src/view3d/scene/bowlShell.ts
|
|
3474
|
+
var BAND_RECESS_M = 0.25;
|
|
3475
|
+
var BAND_MIN_RELIEF_M = 1.2;
|
|
3476
|
+
var SHELL_GAP_M = 0.6;
|
|
3477
|
+
var SHELL_THICKNESS_M = 0.45;
|
|
3478
|
+
var SHELL_PARAPET_M = 2.3;
|
|
3479
|
+
var DEG4 = Math.PI / 180;
|
|
3480
|
+
function polar(c, r, thetaDeg) {
|
|
3481
|
+
const t = thetaDeg * DEG4;
|
|
3482
|
+
return { x: c.x + r * Math.sin(t), y: c.y + r * Math.cos(t) };
|
|
3483
|
+
}
|
|
3484
|
+
function hasAuthoredPerimeter(shapes, fit) {
|
|
3485
|
+
const outer = fit.rings[fit.rings.length - 1];
|
|
3486
|
+
if (!outer) return false;
|
|
3487
|
+
const threshold = outer.outerRMedian * 0.85;
|
|
3488
|
+
for (const s of shapes) {
|
|
3489
|
+
if (s.role !== "wall") continue;
|
|
3490
|
+
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 ? [
|
|
3491
|
+
{ x: s.x, y: s.y },
|
|
3492
|
+
{ x: s.x + s.width, y: s.y },
|
|
3493
|
+
{ x: s.x + s.width, y: s.y + s.height },
|
|
3494
|
+
{ x: s.x, y: s.y + s.height }
|
|
3495
|
+
] : [];
|
|
3496
|
+
if (pts.length < 2) continue;
|
|
3497
|
+
let beyond = 0;
|
|
3498
|
+
for (const p of pts) {
|
|
3499
|
+
if (Math.hypot(p.x - fit.centre.x, p.y - fit.centre.y) >= threshold) beyond++;
|
|
3500
|
+
}
|
|
3501
|
+
if (beyond >= pts.length / 2) return true;
|
|
3502
|
+
}
|
|
3503
|
+
return false;
|
|
3504
|
+
}
|
|
3505
|
+
var BAND_RADIAL_CELLS = 24;
|
|
3506
|
+
function emitBandGrid(builder, fit, arc, innerEdge, outerEdge, bottomY, colors, ao, seatGuards) {
|
|
3507
|
+
const K = arc.binCount;
|
|
3508
|
+
const M2 = BAND_RADIAL_CELLS;
|
|
3509
|
+
const U = METRES_PER_CHART_UNIT;
|
|
3510
|
+
const floorY = bottomY + 0.02;
|
|
3511
|
+
const h = [];
|
|
3512
|
+
for (let k = 0; k < K; k++) {
|
|
3513
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3514
|
+
const row = new Float64Array(M2 + 1);
|
|
3515
|
+
for (let j = 0; j <= M2; j++) {
|
|
3516
|
+
const r = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3517
|
+
row[j] = Math.max(floorY, arcLevelAt(fit, arc, bearing, r) - BAND_RECESS_M);
|
|
3518
|
+
}
|
|
3519
|
+
h.push(row);
|
|
3520
|
+
}
|
|
3521
|
+
for (const g of seatGuards) {
|
|
3522
|
+
const bearing = bearingDeg(fit.centre, g);
|
|
3523
|
+
const r = Math.hypot(g.x - fit.centre.x, g.y - fit.centre.y);
|
|
3524
|
+
let f = bearing / fit.binStepDeg - 0.5 - arc.startBin;
|
|
3525
|
+
f = (f % fit.binCount + fit.binCount) % fit.binCount;
|
|
3526
|
+
if (f > K - 1 && !arc.closed) {
|
|
3527
|
+
if (f < K - 0.5 || f > fit.binCount - 0.5) {
|
|
3528
|
+
} else continue;
|
|
3529
|
+
}
|
|
3530
|
+
const cap = g.deckY - BAND_RECESS_M;
|
|
3531
|
+
const k0 = Math.min(K - 1, Math.max(0, Math.floor(f)));
|
|
3532
|
+
const kk = [k0, arc.closed ? (k0 + 1) % K : Math.min(K - 1, k0 + 1)];
|
|
3533
|
+
for (const k of kk) {
|
|
3534
|
+
const span = outerEdge[k] - innerEdge[k];
|
|
3535
|
+
if (!(span > 1e-9)) continue;
|
|
3536
|
+
const jf = (r - innerEdge[k]) / span * M2;
|
|
3537
|
+
if (jf < -0.75 || jf > M2 + 0.75) continue;
|
|
3538
|
+
const j0 = Math.min(M2, Math.max(0, Math.floor(jf)));
|
|
3539
|
+
const j1 = Math.min(M2, j0 + 1);
|
|
3540
|
+
if (h[k][j0] > cap) h[k][j0] = Math.max(floorY, cap);
|
|
3541
|
+
if (h[k][j1] > cap) h[k][j1] = Math.max(floorY, cap);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const cTop = [colors.bandTop[0] * ao.top, colors.bandTop[1] * ao.top, colors.bandTop[2] * ao.top];
|
|
3545
|
+
const cBot = [colors.bandTop[0] * ao.bottomCap, colors.bandTop[1] * ao.bottomCap, colors.bandTop[2] * ao.bottomCap];
|
|
3546
|
+
const cWallTop = [colors.bandWall[0] * ao.top, colors.bandWall[1] * ao.top, colors.bandWall[2] * ao.top];
|
|
3547
|
+
const cWallBot = [colors.bandWall[0] * ao.wallBottom, colors.bandWall[1] * ao.wallBottom, colors.bandWall[2] * ao.wallBottom];
|
|
3548
|
+
const point = (k, j) => {
|
|
3549
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3550
|
+
const r = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3551
|
+
const p = polar(fit.centre, r, bearing);
|
|
3552
|
+
return [p.x * U, h[k][j], p.y * U];
|
|
3553
|
+
};
|
|
3554
|
+
{
|
|
3555
|
+
const TAN_LIMIT = Math.tan(12 * Math.PI / 180);
|
|
3556
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
3557
|
+
let changed = false;
|
|
3558
|
+
for (let k = 0; k < K; k++) {
|
|
3559
|
+
for (let j = M2 - 1; j >= 0; j--) {
|
|
3560
|
+
if (h[k][j] > h[k][j + 1]) {
|
|
3561
|
+
h[k][j] = h[k][j + 1];
|
|
3562
|
+
changed = true;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const lastPair = arc.closed ? K : K - 1;
|
|
3567
|
+
for (let k = 0; k < lastPair; k++) {
|
|
3568
|
+
const k1 = (k + 1) % K;
|
|
3569
|
+
for (let j = 0; j <= M2; j++) {
|
|
3570
|
+
const d = h[k1][j] - h[k][j];
|
|
3571
|
+
if (Math.abs(d) > 1) continue;
|
|
3572
|
+
const r0 = innerEdge[k] + (outerEdge[k] - innerEdge[k]) * j / M2;
|
|
3573
|
+
const r1 = innerEdge[k1] + (outerEdge[k1] - innerEdge[k1]) * j / M2;
|
|
3574
|
+
const b0 = polar(fit.centre, r0, (arc.startBin + k + 0.5) * fit.binStepDeg);
|
|
3575
|
+
const b1 = polar(fit.centre, r1, (arc.startBin + k1 + 0.5) * fit.binStepDeg);
|
|
3576
|
+
const lim = TAN_LIMIT * Math.hypot(b1.x - b0.x, b1.y - b0.y) * U;
|
|
3577
|
+
if (d > lim) {
|
|
3578
|
+
h[k1][j] = h[k][j] + lim;
|
|
3579
|
+
changed = true;
|
|
3580
|
+
} else if (-d > lim) {
|
|
3581
|
+
h[k][j] = h[k1][j] + lim;
|
|
3582
|
+
changed = true;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
if (!changed) break;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
const JUMP_M = 1;
|
|
3590
|
+
const cliffBetween = (k, k1) => {
|
|
3591
|
+
for (let j = 0; j <= M2; j += 4) {
|
|
3592
|
+
if (Math.abs(h[k][j] - h[k1][j]) > JUMP_M) return true;
|
|
3593
|
+
}
|
|
3594
|
+
return Math.abs(h[k][M2] - h[k1][M2]) > JUMP_M;
|
|
3595
|
+
};
|
|
3596
|
+
const TAN_WALL = Math.tan(38 * Math.PI / 180);
|
|
3597
|
+
const radialCellM = (k) => (outerEdge[k] - innerEdge[k]) / M2 * U;
|
|
3598
|
+
const radialStep = (k, j) => h[k][j + 1] - h[k][j] > TAN_WALL * Math.max(1e-6, radialCellM(k));
|
|
3599
|
+
const vertexNormal = (k, j) => {
|
|
3600
|
+
let kPrev = arc.closed ? (k - 1 + K) % K : Math.max(0, k - 1);
|
|
3601
|
+
let kNext = arc.closed ? (k + 1) % K : Math.min(K - 1, k + 1);
|
|
3602
|
+
if (Math.abs(h[kPrev][j] - h[k][j]) > JUMP_M) kPrev = k;
|
|
3603
|
+
if (Math.abs(h[kNext][j] - h[k][j]) > JUMP_M) kNext = k;
|
|
3604
|
+
let jPrev = Math.max(0, j - 1);
|
|
3605
|
+
let jNext = Math.min(M2, j + 1);
|
|
3606
|
+
if (jPrev < j && radialStep(k, jPrev)) jPrev = j;
|
|
3607
|
+
if (jNext > j && radialStep(k, j)) jNext = j;
|
|
3608
|
+
const pk0 = point(kPrev, j), pk1 = point(kNext, j);
|
|
3609
|
+
const pj0 = point(k, jPrev), pj1 = point(k, jNext);
|
|
3610
|
+
const ux = pk1[0] - pk0[0], uy = pk1[1] - pk0[1], uz = pk1[2] - pk0[2];
|
|
3611
|
+
const vx = pj1[0] - pj0[0], vy = pj1[1] - pj0[1], vz = pj1[2] - pj0[2];
|
|
3612
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
3613
|
+
const len = Math.hypot(nx, ny, nz);
|
|
3614
|
+
if (!(len > 1e-12)) return [0, 1, 0];
|
|
3615
|
+
nx /= len;
|
|
3616
|
+
ny /= len;
|
|
3617
|
+
nz /= len;
|
|
3618
|
+
if (ny < 0) {
|
|
3619
|
+
nx = -nx;
|
|
3620
|
+
ny = -ny;
|
|
3621
|
+
nz = -nz;
|
|
3622
|
+
}
|
|
3623
|
+
return [nx, ny, nz];
|
|
3624
|
+
};
|
|
3625
|
+
const wallQuad = (a, b) => {
|
|
3626
|
+
const a0 = [a[0], bottomY, a[2]];
|
|
3627
|
+
const b0 = [b[0], bottomY, b[2]];
|
|
3628
|
+
const ex = b[0] - a[0], ez = b[2] - a[2];
|
|
3629
|
+
const len = Math.hypot(ex, ez);
|
|
3630
|
+
if (!(len > 1e-12)) return;
|
|
3631
|
+
const n = [ez / len, 0, -ex / len];
|
|
3632
|
+
builder.tri(a, b, b0, n, cWallTop, cWallTop, cWallBot);
|
|
3633
|
+
builder.tri(a, b0, a0, n, cWallTop, cWallBot, cWallBot);
|
|
3634
|
+
};
|
|
3635
|
+
const MIN_SPAN_U = 0.05 * CHART_UNITS_PER_METRE;
|
|
3636
|
+
const pinched = (k) => outerEdge[k] - innerEdge[k] < MIN_SPAN_U;
|
|
3637
|
+
const lastK = arc.closed ? K : K - 1;
|
|
3638
|
+
for (let k = 0; k < lastK; k++) {
|
|
3639
|
+
const k1 = (k + 1) % K;
|
|
3640
|
+
if (pinched(k) || pinched(k1)) continue;
|
|
3641
|
+
if (cliffBetween(k, k1)) {
|
|
3642
|
+
const bearingMid = (arc.startBin + k + 1) * fit.binStepDeg;
|
|
3643
|
+
const midAt = (side, j) => {
|
|
3644
|
+
const rIn = (innerEdge[k] + innerEdge[k1]) / 2;
|
|
3645
|
+
const rOut = (outerEdge[k] + outerEdge[k1]) / 2;
|
|
3646
|
+
const r = rIn + (rOut - rIn) * j / M2;
|
|
3647
|
+
const p = polar(fit.centre, r, bearingMid);
|
|
3648
|
+
return [p.x * U, h[side][j], p.y * U];
|
|
3649
|
+
};
|
|
3650
|
+
for (let j = 0; j < M2; j++) {
|
|
3651
|
+
const aL = point(k, j), dL = point(k, j + 1);
|
|
3652
|
+
const bL = midAt(k, j), cL = midAt(k, j + 1);
|
|
3653
|
+
const nL0 = vertexNormal(k, j), nL1 = vertexNormal(k, j + 1);
|
|
3654
|
+
builder.triN(aL, bL, cL, nL0, nL0, nL1, cTop);
|
|
3655
|
+
builder.triN(aL, cL, dL, nL0, nL1, nL1, cTop);
|
|
3656
|
+
const bR = point(k1, j), cR = point(k1, j + 1);
|
|
3657
|
+
const aR = midAt(k1, j), dR = midAt(k1, j + 1);
|
|
3658
|
+
const nR0 = vertexNormal(k1, j), nR1 = vertexNormal(k1, j + 1);
|
|
3659
|
+
builder.triN(aR, bR, cR, nR0, nR0, nR1, cTop);
|
|
3660
|
+
builder.triN(aR, cR, dR, nR0, nR1, nR1, cTop);
|
|
3661
|
+
const w0 = midAt(k, j), w1 = midAt(k, j + 1), w2 = midAt(k1, j + 1), w3 = midAt(k1, j);
|
|
3662
|
+
const ex = w1[0] - w0[0], ez = w1[2] - w0[2];
|
|
3663
|
+
const el = Math.hypot(ex, ez);
|
|
3664
|
+
if (el > 1e-12) {
|
|
3665
|
+
const n = [ez / el, 0, -ex / el];
|
|
3666
|
+
builder.tri(w0, w1, w2, n, cWallTop);
|
|
3667
|
+
builder.tri(w0, w2, w3, n, cWallTop);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
const ai2 = point(k, 0), bi2 = point(k1, 0), ao_2 = point(k1, M2), di2 = point(k, M2);
|
|
3671
|
+
const flat2 = (p) => [p[0], bottomY, p[2]];
|
|
3672
|
+
builder.tri(flat2(ai2), flat2(ao_2), flat2(bi2), [0, -1, 0], cBot);
|
|
3673
|
+
builder.tri(flat2(ai2), flat2(di2), flat2(ao_2), [0, -1, 0], cBot);
|
|
3674
|
+
wallQuad(ai2, bi2);
|
|
3675
|
+
wallQuad(di2, ao_2);
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3678
|
+
for (let j = 0; j < M2; j++) {
|
|
3679
|
+
const na = vertexNormal(k, j), nb = vertexNormal(k1, j), nc = vertexNormal(k1, j + 1), nd = vertexNormal(k, j + 1);
|
|
3680
|
+
if (radialStep(k, j) || radialStep(k1, j)) {
|
|
3681
|
+
const midPt = (kk, hh) => {
|
|
3682
|
+
const bearing = (arc.startBin + kk + 0.5) * fit.binStepDeg;
|
|
3683
|
+
const r = innerEdge[kk] + (outerEdge[kk] - innerEdge[kk]) * (j + 0.5) / M2;
|
|
3684
|
+
const p = polar(fit.centre, r, bearing);
|
|
3685
|
+
return [p.x * U, hh, p.y * U];
|
|
3686
|
+
};
|
|
3687
|
+
const a2 = point(k, j), b2 = point(k1, j);
|
|
3688
|
+
const mLo0 = midPt(k, h[k][j]), mLo1 = midPt(k1, h[k1][j]);
|
|
3689
|
+
builder.triN(a2, b2, mLo1, na, nb, nb, cTop);
|
|
3690
|
+
builder.triN(a2, mLo1, mLo0, na, nb, na, cTop);
|
|
3691
|
+
const c2 = point(k1, j + 1), d2 = point(k, j + 1);
|
|
3692
|
+
const mHi0 = midPt(k, h[k][j + 1]), mHi1 = midPt(k1, h[k1][j + 1]);
|
|
3693
|
+
builder.triN(mHi0, mHi1, c2, nd, nc, nc, cTop);
|
|
3694
|
+
builder.triN(mHi0, c2, d2, nd, nc, nd, cTop);
|
|
3695
|
+
const ex = mLo1[0] - mLo0[0], ez = mLo1[2] - mLo0[2];
|
|
3696
|
+
const el = Math.hypot(ex, ez);
|
|
3697
|
+
if (el > 1e-12) {
|
|
3698
|
+
const n = [-ez / el, 0, ex / el];
|
|
3699
|
+
builder.tri(mLo0, mLo1, mHi1, n, cWallTop);
|
|
3700
|
+
builder.tri(mLo0, mHi1, mHi0, n, cWallTop);
|
|
3701
|
+
}
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
const a = point(k, j), b = point(k1, j), c = point(k1, j + 1), d = point(k, j + 1);
|
|
3705
|
+
builder.triN(a, b, c, na, nb, nc, cTop);
|
|
3706
|
+
builder.triN(a, c, d, na, nc, nd, cTop);
|
|
3707
|
+
}
|
|
3708
|
+
const ai = point(k, 0), bi = point(k1, 0), ao_ = point(k1, M2), di = point(k, M2);
|
|
3709
|
+
const flat = (p) => [p[0], bottomY, p[2]];
|
|
3710
|
+
builder.tri(flat(ai), flat(ao_), flat(bi), [0, -1, 0], cBot);
|
|
3711
|
+
builder.tri(flat(ai), flat(di), flat(ao_), [0, -1, 0], cBot);
|
|
3712
|
+
wallQuad(ai, bi);
|
|
3713
|
+
wallQuad(di, ao_);
|
|
3714
|
+
}
|
|
3715
|
+
if (!arc.closed) {
|
|
3716
|
+
for (const k of [0, K - 1]) {
|
|
3717
|
+
for (let j = 0; j < M2; j++) wallQuad(point(k, j), point(k, j + 1));
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
function emitBowlGeometry(builder, fit, opts) {
|
|
3722
|
+
const { bottomY, colors, ao } = opts;
|
|
3723
|
+
const drawnOuter = new Float64Array(fit.binCount).fill(-Infinity);
|
|
3724
|
+
for (const ring of fit.rings) {
|
|
3725
|
+
for (const arc of ring.arcs) {
|
|
3726
|
+
const innerEdge = new Float64Array(arc.binCount);
|
|
3727
|
+
const outerEdge = new Float64Array(arc.binCount);
|
|
3728
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3729
|
+
const bin = (arc.startBin + k) % fit.binCount;
|
|
3730
|
+
const inner = Math.max(0, arc.innerR[k] - SEAT_OWNERSHIP_PAD_U);
|
|
3731
|
+
innerEdge[k] = Math.max(inner, drawnOuter[bin] + 1e-3);
|
|
3732
|
+
outerEdge[k] = Math.max(innerEdge[k] + 1e-3, arc.outerR[k] + SEAT_OWNERSHIP_PAD_U);
|
|
3733
|
+
}
|
|
3734
|
+
let relief = 0;
|
|
3735
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3736
|
+
const d = arc.topLevelM[k] - bottomY;
|
|
3737
|
+
if (d > relief) relief = d;
|
|
3738
|
+
}
|
|
3739
|
+
if (relief >= BAND_MIN_RELIEF_M) {
|
|
3740
|
+
emitBandGrid(builder, fit, arc, innerEdge, outerEdge, bottomY, colors, ao, opts.seatGuards);
|
|
3741
|
+
}
|
|
3742
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3743
|
+
const bin = (arc.startBin + k) % fit.binCount;
|
|
3744
|
+
if (outerEdge[k] > drawnOuter[bin]) drawnOuter[bin] = outerEdge[k];
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
if (hasAuthoredPerimeter(opts.shapes, fit)) return;
|
|
3749
|
+
const outerRing = fit.rings[fit.rings.length - 1];
|
|
3750
|
+
const gapU = SHELL_GAP_M * CHART_UNITS_PER_METRE;
|
|
3751
|
+
const thickU = SHELL_THICKNESS_M * CHART_UNITS_PER_METRE;
|
|
3752
|
+
for (const arc of outerRing.arcs) {
|
|
3753
|
+
const inner = [];
|
|
3754
|
+
const outer = [];
|
|
3755
|
+
for (let k = 0; k < arc.binCount; k++) {
|
|
3756
|
+
const bearing = (arc.startBin + k + 0.5) * fit.binStepDeg;
|
|
3757
|
+
const r0 = arc.outerR[k] + SEAT_OWNERSHIP_PAD_U + gapU;
|
|
3758
|
+
inner.push(polar(fit.centre, r0, bearing));
|
|
3759
|
+
outer.push(polar(fit.centre, r0 + thickU, bearing));
|
|
3760
|
+
}
|
|
3761
|
+
let crest = -Infinity;
|
|
3762
|
+
for (let k = 0; k < arc.binCount; k++) if (arc.topLevelM[k] > crest) crest = arc.topLevelM[k];
|
|
3763
|
+
const topY = () => crest + SHELL_PARAPET_M;
|
|
3764
|
+
if (arc.closed) {
|
|
3765
|
+
extrudePrism(builder, outer, [inner.slice().reverse()], topY, bottomY, colors.shellTop, colors.shellWall, ao);
|
|
3766
|
+
} else {
|
|
3767
|
+
extrudePrism(builder, [...outer, ...inner.slice().reverse()], void 0, topY, bottomY, colors.shellTop, colors.shellWall, ao);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3100
3772
|
// src/view3d/scene/sceneModel.ts
|
|
3101
3773
|
function shapeVisibleInConfiguration(shape, activeConfigurationId) {
|
|
3102
3774
|
const ids = shape.eventConfigurationIds;
|
|
@@ -3106,14 +3778,28 @@ function visibleObjects(objects, activeConfigurationId) {
|
|
|
3106
3778
|
return objects.filter((object) => object.type !== "shape" || shapeVisibleInConfiguration(object, activeConfigurationId));
|
|
3107
3779
|
}
|
|
3108
3780
|
function floorUnits(doc, activeConfigurationId) {
|
|
3781
|
+
const docFocal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
3109
3782
|
if (doc.floors?.length) {
|
|
3110
|
-
return doc.floors.map((f) => ({
|
|
3783
|
+
return doc.floors.map((f, index) => ({
|
|
3111
3784
|
objects: visibleObjects(f.objects, activeConfigurationId),
|
|
3112
|
-
focal:
|
|
3113
|
-
|
|
3785
|
+
focal: {
|
|
3786
|
+
x: (f.focalPoint ?? docFocal).x,
|
|
3787
|
+
y: (f.focalPoint ?? docFocal).y
|
|
3788
|
+
},
|
|
3789
|
+
baseHeightM: f.baseHeightM ?? 0,
|
|
3790
|
+
index
|
|
3791
|
+
// 2D deliberately packs levels into a non-overlapping grid. In 3D the
|
|
3792
|
+
// physical datum already separates them, so retaining each floor's
|
|
3793
|
+
// authored x/z registration is what turns independent drawings into one
|
|
3794
|
+
// coherent building stack. The source scene graph is never rewritten.
|
|
3114
3795
|
}));
|
|
3115
3796
|
}
|
|
3116
|
-
return [{
|
|
3797
|
+
return [{
|
|
3798
|
+
objects: visibleObjects(doc.objects, activeConfigurationId),
|
|
3799
|
+
focal: docFocal,
|
|
3800
|
+
baseHeightM: 0,
|
|
3801
|
+
index: 0
|
|
3802
|
+
}];
|
|
3117
3803
|
}
|
|
3118
3804
|
var AO = { top: 1, wallBottom: 0.5, bottomCap: 0.4 };
|
|
3119
3805
|
function tintTop(fill, neutral) {
|
|
@@ -3288,6 +3974,79 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
3288
3974
|
extrudePrism(builder, ring, section.holes, topY, bottomY, colTop, S.tierWall, AO, maxErr, topN);
|
|
3289
3975
|
}
|
|
3290
3976
|
}
|
|
3977
|
+
function collectBowlSamples(unit, seats, surfaces) {
|
|
3978
|
+
const unitSections = /* @__PURE__ */ new Set();
|
|
3979
|
+
for (const o of unit.objects) if (o.type === "section") unitSections.add(o.id);
|
|
3980
|
+
const flatRows = /* @__PURE__ */ new Map();
|
|
3981
|
+
for (let i = 0; i < seats.length; i++) {
|
|
3982
|
+
const owner = surfaces.seatOwner[i];
|
|
3983
|
+
if (!owner || !unitSections.has(owner)) continue;
|
|
3984
|
+
const surf = surfaces.bySection.get(owner);
|
|
3985
|
+
if (surf && surf.rowLevels.length >= 2) continue;
|
|
3986
|
+
const s = seats[i];
|
|
3987
|
+
let rows = flatRows.get(owner);
|
|
3988
|
+
if (!rows) {
|
|
3989
|
+
rows = /* @__PURE__ */ new Map();
|
|
3990
|
+
flatRows.set(owner, rows);
|
|
3991
|
+
}
|
|
3992
|
+
const key = s.rowId || `__seat-${i}`;
|
|
3993
|
+
let row = rows.get(key);
|
|
3994
|
+
if (!row) {
|
|
3995
|
+
row = { pts: [], levelM: 0, n: 0 };
|
|
3996
|
+
rows.set(key, row);
|
|
3997
|
+
}
|
|
3998
|
+
row.pts.push({ x: s.x, y: s.y });
|
|
3999
|
+
row.levelM += surfaces.seatDeckY(i);
|
|
4000
|
+
row.n++;
|
|
4001
|
+
}
|
|
4002
|
+
const samples = [];
|
|
4003
|
+
for (const o of unit.objects) {
|
|
4004
|
+
if (o.type !== "section") continue;
|
|
4005
|
+
const surf = surfaces.bySection.get(o.id);
|
|
4006
|
+
if (surf && surf.rowLevels.length >= 2) {
|
|
4007
|
+
const rows2 = surf.rowLevels.filter((r) => !r.patch).map((r) => ({ pts: r.pts, levelM: r.y }));
|
|
4008
|
+
if (rows2.length >= 2) samples.push({ sectionId: o.id, rows: rows2 });
|
|
4009
|
+
continue;
|
|
4010
|
+
}
|
|
4011
|
+
const rows = flatRows.get(o.id);
|
|
4012
|
+
if (!rows) continue;
|
|
4013
|
+
samples.push({
|
|
4014
|
+
sectionId: o.id,
|
|
4015
|
+
rows: [...rows.values()].map((r) => ({ pts: r.pts, levelM: r.levelM / r.n }))
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
return samples;
|
|
4019
|
+
}
|
|
4020
|
+
function buildBowl(builder, unit, seats, surfaces, S) {
|
|
4021
|
+
const samples = collectBowlSamples(unit, seats, surfaces);
|
|
4022
|
+
if (!samples.length) return;
|
|
4023
|
+
const fit = fitBowlProfile(samples, unit.focal);
|
|
4024
|
+
if (!fit) return;
|
|
4025
|
+
const unitSections = /* @__PURE__ */ new Set();
|
|
4026
|
+
for (const o of unit.objects) if (o.type === "section") unitSections.add(o.id);
|
|
4027
|
+
const seatGuards = [];
|
|
4028
|
+
for (let i = 0; i < seats.length; i++) {
|
|
4029
|
+
const owner = surfaces.seatOwner[i];
|
|
4030
|
+
if (owner ? unitSections.has(owner) : true) {
|
|
4031
|
+
seatGuards.push({ x: seats[i].x, y: seats[i].y, deckY: surfaces.seatDeckY(i) });
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
builder.setMaterial(0);
|
|
4035
|
+
emitBowlGeometry(builder, fit, {
|
|
4036
|
+
bottomY: unit.baseHeightM,
|
|
4037
|
+
colors: {
|
|
4038
|
+
// A shade below the tier caps, so stands read as decks resting ON the
|
|
4039
|
+
// bowl rather than the bowl swallowing them.
|
|
4040
|
+
bandTop: scaleRgb(S.tierTop, 0.8),
|
|
4041
|
+
bandWall: S.tierWall,
|
|
4042
|
+
shellTop: scaleRgb(S.tierWall, 1.15),
|
|
4043
|
+
shellWall: S.tierWall
|
|
4044
|
+
},
|
|
4045
|
+
ao: AO,
|
|
4046
|
+
shapes: unit.objects.filter((o) => o.type === "shape"),
|
|
4047
|
+
seatGuards
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
3291
4050
|
function coplanarLevels(a, b) {
|
|
3292
4051
|
if (a === void 0 || b === void 0) return true;
|
|
3293
4052
|
return Math.abs(a - b) < 1e-3;
|
|
@@ -3453,7 +4212,11 @@ function architectureForShape(shape) {
|
|
|
3453
4212
|
restroom: { kind: "solid", heightM: 2.4 },
|
|
3454
4213
|
bar: { kind: "solid", heightM: 1.1 },
|
|
3455
4214
|
concession: { kind: "solid", heightM: 1.1 },
|
|
3456
|
-
coat: { kind: "solid", heightM: 1.1 }
|
|
4215
|
+
coat: { kind: "solid", heightM: 1.1 },
|
|
4216
|
+
// Sports-surface imports describe a goal as one thin rectangular portal.
|
|
4217
|
+
// It therefore becomes a real open goal frame in 3D without a bespoke
|
|
4218
|
+
// chart model or a copied bitmap.
|
|
4219
|
+
goal: { kind: "portal", heightM: 1.8 }
|
|
3457
4220
|
};
|
|
3458
4221
|
const resolved = defaults[role] ?? { kind: "plate", heightM: 0.25 };
|
|
3459
4222
|
return { ...resolved, heightM: shape.heightM ?? resolved.heightM };
|
|
@@ -3652,20 +4415,46 @@ function chartFootprint(units, seats) {
|
|
|
3652
4415
|
return { minX, minY, maxX, maxY };
|
|
3653
4416
|
}
|
|
3654
4417
|
function buildSceneModel(input) {
|
|
3655
|
-
const { doc
|
|
4418
|
+
const { doc } = input;
|
|
3656
4419
|
const theme = resolveTheme3D(doc.theme);
|
|
3657
4420
|
const S = theme.structure;
|
|
3658
4421
|
const units = floorUnits(doc, input.eventConfigurationId ?? doc.eventConfigurationId);
|
|
4422
|
+
const seats = input.seats;
|
|
3659
4423
|
const builder = new MeshBuilder();
|
|
4424
|
+
const floorByOwner = /* @__PURE__ */ new Map();
|
|
4425
|
+
for (const unit of units) {
|
|
4426
|
+
for (const object of unit.objects) {
|
|
4427
|
+
floorByOwner.set(object.id, unit.index);
|
|
4428
|
+
if (object.type === "section") floorByOwner.set(object.logicalSectionId ?? object.id, unit.index);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
const hintedSeatFloor = seats.map((seat) => floorByOwner.get(seat.sectionId ?? "") ?? floorByOwner.get(seat.rowId ?? "") ?? 0);
|
|
3660
4432
|
const fp = chartFootprint(units, seats);
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
4433
|
+
if (units.length === 1) {
|
|
4434
|
+
const padU = Math.max(60, (fp.maxX - fp.minX + fp.maxY - fp.minY) * 0.06);
|
|
4435
|
+
const groundPoly = rectPolygon(fp.minX - padU, fp.minY - padU, fp.maxX - fp.minX + padU * 2, fp.maxY - fp.minY + padU * 2);
|
|
4436
|
+
extrudePrism(builder, groundPoly, void 0, () => 0, -0.4, S.ground, S.ground, AO);
|
|
4437
|
+
} else {
|
|
4438
|
+
const unit = units.reduce((lowest, candidate) => candidate.baseHeightM < lowest.baseHeightM ? candidate : lowest);
|
|
4439
|
+
builder.setFloor(unit.index);
|
|
4440
|
+
const unitSeats = seats.filter((_seat, index) => hintedSeatFloor[index] === unit.index);
|
|
4441
|
+
const floorFp = chartFootprint([unit], unitSeats);
|
|
4442
|
+
const span = Math.hypot(floorFp.maxX - floorFp.minX, floorFp.maxY - floorFp.minY);
|
|
4443
|
+
const padU = Math.max(28, Math.min(80, span * 0.04));
|
|
4444
|
+
const plinth = rectPolygon(
|
|
4445
|
+
floorFp.minX - padU,
|
|
4446
|
+
floorFp.minY - padU,
|
|
4447
|
+
floorFp.maxX - floorFp.minX + padU * 2,
|
|
4448
|
+
floorFp.maxY - floorFp.minY + padU * 2
|
|
4449
|
+
);
|
|
4450
|
+
const top = unit.baseHeightM - 0.03;
|
|
4451
|
+
extrudePrism(builder, plinth, void 0, () => top, top - 0.22, S.ground, S.ground, AO);
|
|
4452
|
+
}
|
|
3664
4453
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
3665
4454
|
const catColor = /* @__PURE__ */ new Map();
|
|
3666
4455
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
3667
4456
|
const stageBounds = [];
|
|
3668
|
-
const surfaces = buildVenueSurfaces(units, seats);
|
|
4457
|
+
const surfaces = buildVenueSurfaces(units, seats, hintedSeatFloor);
|
|
3669
4458
|
const seatFloor = new Float32Array(seats.length);
|
|
3670
4459
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
3671
4460
|
const unit = units[unitIndex];
|
|
@@ -3692,8 +4481,12 @@ function buildSceneModel(input) {
|
|
|
3692
4481
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3693
4482
|
else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
|
|
3694
4483
|
}
|
|
4484
|
+
buildBowl(builder, unit, seats, surfaces, S);
|
|
3695
4485
|
}
|
|
3696
|
-
const focal =
|
|
4486
|
+
const focal = units.length > 1 ? {
|
|
4487
|
+
x: units.reduce((sum, unit) => sum + unit.focal.x, 0) / units.length,
|
|
4488
|
+
y: units.reduce((sum, unit) => sum + unit.focal.y, 0) / units.length
|
|
4489
|
+
} : doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
3697
4490
|
const solids = mergeMeshData([builder.build()]);
|
|
3698
4491
|
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor, catColor);
|
|
3699
4492
|
const zoneDefs = doc.zones ?? [];
|
|
@@ -3799,9 +4592,11 @@ function buildSceneModel(input) {
|
|
|
3799
4592
|
if (!a || a.n === 0) continue;
|
|
3800
4593
|
const e = ext.get(o.id);
|
|
3801
4594
|
const centre = [a.x / a.n * M, a.deck / a.n, a.y / a.n * M];
|
|
4595
|
+
const sectionLabel = o.displayLabel || o.label || o.id;
|
|
3802
4596
|
sections.push({
|
|
3803
4597
|
id: o.id,
|
|
3804
|
-
|
|
4598
|
+
floorIndex: unit.index,
|
|
4599
|
+
label: sectionLabel,
|
|
3805
4600
|
seatCount: a.n,
|
|
3806
4601
|
center: centre,
|
|
3807
4602
|
// Half-diagonal of the seat extent — the same fit the zones use.
|
|
@@ -3810,13 +4605,17 @@ function buildSceneModel(input) {
|
|
|
3810
4605
|
// look along so the seats present their fronts.
|
|
3811
4606
|
focalWorld: [unit.focal.x * M, unit.baseHeightM + 1.5, unit.focal.y * M]
|
|
3812
4607
|
});
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
4608
|
+
const floorLabel = doc.floors?.[unit.index]?.name?.trim().toLocaleLowerCase();
|
|
4609
|
+
if (!(units.length > 1 && floorLabel === sectionLabel.trim().toLocaleLowerCase())) {
|
|
4610
|
+
labels.push({
|
|
4611
|
+
id: `section:${o.id}`,
|
|
4612
|
+
kind: "section",
|
|
4613
|
+
// The buyer-facing name wins over the technical one, as it does in 2D.
|
|
4614
|
+
text: sectionLabel,
|
|
4615
|
+
anchor: [centre[0], centre[1] + SECTION_LABEL_LIFT_M, centre[2]],
|
|
4616
|
+
floorIndex: unit.index
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
3820
4619
|
}
|
|
3821
4620
|
}
|
|
3822
4621
|
}
|
|
@@ -3831,7 +4630,8 @@ function buildSceneModel(input) {
|
|
|
3831
4630
|
id: `ga:${o.id}`,
|
|
3832
4631
|
kind: "ga",
|
|
3833
4632
|
text: o.capacity > 0 ? `${name} \xB7 ${o.capacity.toLocaleString()}` : name,
|
|
3834
|
-
anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M]
|
|
4633
|
+
anchor: [c.x * M, unit.baseHeightM + GA_LABEL_LIFT_M, c.y * M],
|
|
4634
|
+
floorIndex: unit.index
|
|
3835
4635
|
});
|
|
3836
4636
|
}
|
|
3837
4637
|
}
|
|
@@ -3850,7 +4650,8 @@ function buildSceneModel(input) {
|
|
|
3850
4650
|
id: `row:${rowId}`,
|
|
3851
4651
|
kind: "row",
|
|
3852
4652
|
text,
|
|
3853
|
-
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M]
|
|
4653
|
+
anchor: [end.seat.x * M, surfaces.seatDeckY(end.index) + ROW_LABEL_LIFT_M, end.seat.y * M],
|
|
4654
|
+
floorIndex: hintedSeatFloor[end.index]
|
|
3854
4655
|
});
|
|
3855
4656
|
}
|
|
3856
4657
|
const rowAcc = /* @__PURE__ */ new Map();
|
|
@@ -3905,7 +4706,8 @@ function buildSceneModel(input) {
|
|
|
3905
4706
|
id: `seat:${s.id}`,
|
|
3906
4707
|
kind: "seat",
|
|
3907
4708
|
text,
|
|
3908
|
-
anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M]
|
|
4709
|
+
anchor: [s.x * M, surfaces.seatDeckY(i) + SEAT_LABEL_LIFT_M, s.y * M],
|
|
4710
|
+
floorIndex: hintedSeatFloor[i]
|
|
3909
4711
|
});
|
|
3910
4712
|
}
|
|
3911
4713
|
}
|
|
@@ -3921,7 +4723,8 @@ function buildSceneModel(input) {
|
|
|
3921
4723
|
kind: "annotation",
|
|
3922
4724
|
text: o.label,
|
|
3923
4725
|
anchor: [centre.x * M, unit.baseHeightM + architectureForShape(o).heightM + ANNOTATION_LIFT_M, centre.y * M],
|
|
3924
|
-
color: o.fill
|
|
4726
|
+
color: o.fill,
|
|
4727
|
+
floorIndex: unit.index
|
|
3925
4728
|
});
|
|
3926
4729
|
} else if (o.type === "text") {
|
|
3927
4730
|
if (!o.text) continue;
|
|
@@ -3931,7 +4734,8 @@ function buildSceneModel(input) {
|
|
|
3931
4734
|
text: o.text,
|
|
3932
4735
|
anchor: [o.position.x * M, unit.baseHeightM + ANNOTATION_LIFT_M, o.position.y * M],
|
|
3933
4736
|
color: o.color,
|
|
3934
|
-
rotation: o.rotation
|
|
4737
|
+
rotation: o.rotation,
|
|
4738
|
+
floorIndex: unit.index
|
|
3935
4739
|
});
|
|
3936
4740
|
} else if (o.type === "booth") {
|
|
3937
4741
|
const poly = boothPolygon(o);
|
|
@@ -3942,21 +4746,52 @@ function buildSceneModel(input) {
|
|
|
3942
4746
|
id: `booth:${o.id}`,
|
|
3943
4747
|
kind: "booth",
|
|
3944
4748
|
text: o.displayLabel || o.label || o.id,
|
|
3945
|
-
anchor: [c.x * M, unit.baseHeightM + BOOTH_HEIGHT_M + BOOTH_LABEL_LIFT_M, c.y * M]
|
|
4749
|
+
anchor: [c.x * M, unit.baseHeightM + BOOTH_HEIGHT_M + BOOTH_LABEL_LIFT_M, c.y * M],
|
|
4750
|
+
floorIndex: unit.index
|
|
3946
4751
|
});
|
|
3947
4752
|
}
|
|
3948
4753
|
}
|
|
3949
4754
|
}
|
|
3950
4755
|
const floors = [];
|
|
3951
4756
|
{
|
|
4757
|
+
const solidBounds = units.map(() => ({
|
|
4758
|
+
minX: Infinity,
|
|
4759
|
+
minY: Infinity,
|
|
4760
|
+
minZ: Infinity,
|
|
4761
|
+
maxX: -Infinity,
|
|
4762
|
+
maxY: -Infinity,
|
|
4763
|
+
maxZ: -Infinity
|
|
4764
|
+
}));
|
|
4765
|
+
for (let vertex = 0; vertex < solids.floor.length; vertex += 1) {
|
|
4766
|
+
const floorIndex = Math.max(0, Math.min(units.length - 1, Math.round(solids.floor[vertex])));
|
|
4767
|
+
const offset = vertex * 3;
|
|
4768
|
+
const bound = solidBounds[floorIndex];
|
|
4769
|
+
bound.minX = Math.min(bound.minX, solids.position[offset]);
|
|
4770
|
+
bound.minY = Math.min(bound.minY, solids.position[offset + 1]);
|
|
4771
|
+
bound.minZ = Math.min(bound.minZ, solids.position[offset + 2]);
|
|
4772
|
+
bound.maxX = Math.max(bound.maxX, solids.position[offset]);
|
|
4773
|
+
bound.maxY = Math.max(bound.maxY, solids.position[offset + 1]);
|
|
4774
|
+
bound.maxZ = Math.max(bound.maxZ, solids.position[offset + 2]);
|
|
4775
|
+
}
|
|
3952
4776
|
const sectionFloor = /* @__PURE__ */ new Map();
|
|
3953
4777
|
for (let ui = 0; ui < units.length; ui++) {
|
|
3954
4778
|
for (const o of units[ui].objects) if (o.type === "section") sectionFloor.set(o.id, ui);
|
|
3955
4779
|
}
|
|
3956
|
-
const acc = units.map(() => ({
|
|
4780
|
+
const acc = units.map(() => ({
|
|
4781
|
+
n: 0,
|
|
4782
|
+
minX: Infinity,
|
|
4783
|
+
minY: Infinity,
|
|
4784
|
+
minDeck: Infinity,
|
|
4785
|
+
maxX: -Infinity,
|
|
4786
|
+
maxY: -Infinity,
|
|
4787
|
+
maxDeck: -Infinity,
|
|
4788
|
+
sumX: 0,
|
|
4789
|
+
sumDeck: 0,
|
|
4790
|
+
sumZ: 0
|
|
4791
|
+
}));
|
|
3957
4792
|
for (let i = 0; i < seats.length; i++) {
|
|
3958
4793
|
const owner = surfaces.seatOwner[i];
|
|
3959
|
-
const ui = owner !== null ? sectionFloor.get(owner) : void 0;
|
|
4794
|
+
const ui = (owner !== null ? sectionFloor.get(owner) : void 0) ?? hintedSeatFloor[i];
|
|
3960
4795
|
if (ui === void 0) continue;
|
|
3961
4796
|
const a = acc[ui];
|
|
3962
4797
|
const s = seats[i];
|
|
@@ -3965,19 +4800,78 @@ function buildSceneModel(input) {
|
|
|
3965
4800
|
if (s.y < a.minY) a.minY = s.y;
|
|
3966
4801
|
if (s.x > a.maxX) a.maxX = s.x;
|
|
3967
4802
|
if (s.y > a.maxY) a.maxY = s.y;
|
|
3968
|
-
|
|
4803
|
+
const deck = surfaces.seatDeckY(i);
|
|
4804
|
+
a.sumX += s.x * M;
|
|
4805
|
+
a.sumDeck += deck;
|
|
4806
|
+
a.sumZ += s.y * M;
|
|
4807
|
+
a.minDeck = Math.min(a.minDeck, deck);
|
|
4808
|
+
a.maxDeck = Math.max(a.maxDeck, deck + 1.6);
|
|
3969
4809
|
seatFloor[i] = ui;
|
|
3970
4810
|
}
|
|
3971
4811
|
for (let ui = 0; ui < units.length; ui++) {
|
|
3972
4812
|
const a = acc[ui];
|
|
3973
4813
|
const f = doc.floors?.[ui];
|
|
4814
|
+
const solid = solidBounds[ui];
|
|
4815
|
+
const minWorldX = Math.min(
|
|
4816
|
+
a.n ? a.minX * M : units[ui].focal.x * M - 1,
|
|
4817
|
+
Number.isFinite(solid.minX) ? solid.minX : Infinity
|
|
4818
|
+
);
|
|
4819
|
+
const maxWorldX = Math.max(
|
|
4820
|
+
a.n ? a.maxX * M : units[ui].focal.x * M + 1,
|
|
4821
|
+
Number.isFinite(solid.maxX) ? solid.maxX : -Infinity
|
|
4822
|
+
);
|
|
4823
|
+
const minWorldZ = Math.min(
|
|
4824
|
+
a.n ? a.minY * M : units[ui].focal.y * M - 1,
|
|
4825
|
+
Number.isFinite(solid.minZ) ? solid.minZ : Infinity
|
|
4826
|
+
);
|
|
4827
|
+
const maxWorldZ = Math.max(
|
|
4828
|
+
a.n ? a.maxY * M : units[ui].focal.y * M + 1,
|
|
4829
|
+
Number.isFinite(solid.maxZ) ? solid.maxZ : -Infinity
|
|
4830
|
+
);
|
|
4831
|
+
const minWorldY = Math.min(
|
|
4832
|
+
a.n ? Math.min(units[ui].baseHeightM, a.minDeck) : units[ui].baseHeightM,
|
|
4833
|
+
Number.isFinite(solid.minY) ? solid.minY : Infinity
|
|
4834
|
+
);
|
|
4835
|
+
const maxWorldY = Math.max(
|
|
4836
|
+
a.n ? Math.max(a.maxDeck, minWorldY + 0.5) : minWorldY + 0.5,
|
|
4837
|
+
Number.isFinite(solid.maxY) ? solid.maxY : -Infinity
|
|
4838
|
+
);
|
|
4839
|
+
const center = [
|
|
4840
|
+
// Aim the floor camera at its sellable topology rather than the centre
|
|
4841
|
+
// of a generous imported outline/plinth. The complete box is still
|
|
4842
|
+
// fitted below, so stages, annotations and deck edges cannot clip.
|
|
4843
|
+
a.n ? a.sumX / a.n : (minWorldX + maxWorldX) / 2,
|
|
4844
|
+
a.n ? a.sumDeck / a.n : (minWorldY + maxWorldY) / 2,
|
|
4845
|
+
a.n ? a.sumZ / a.n : (minWorldZ + maxWorldZ) / 2
|
|
4846
|
+
];
|
|
4847
|
+
const radius2 = Math.max(1, 0.5 * Math.hypot(
|
|
4848
|
+
maxWorldX - minWorldX,
|
|
4849
|
+
maxWorldY - minWorldY,
|
|
4850
|
+
maxWorldZ - minWorldZ
|
|
4851
|
+
));
|
|
3974
4852
|
floors.push({
|
|
3975
4853
|
index: ui,
|
|
3976
4854
|
id: f?.id ?? `floor-${ui}`,
|
|
3977
4855
|
label: f?.name ?? (units.length > 1 ? `Floor ${ui + 1}` : "Venue"),
|
|
3978
4856
|
seatCount: a.n,
|
|
3979
|
-
center
|
|
3980
|
-
radius:
|
|
4857
|
+
center,
|
|
4858
|
+
radius: radius2,
|
|
4859
|
+
box: {
|
|
4860
|
+
min: [minWorldX, minWorldY, minWorldZ],
|
|
4861
|
+
max: [maxWorldX, maxWorldY, maxWorldZ]
|
|
4862
|
+
},
|
|
4863
|
+
focalWorld: [units[ui].focal.x * M, units[ui].baseHeightM + 1.5, units[ui].focal.y * M]
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
if (floors.length > 1) {
|
|
4868
|
+
for (const floor of floors) {
|
|
4869
|
+
labels.push({
|
|
4870
|
+
id: `floor:${floor.id}`,
|
|
4871
|
+
kind: "floor",
|
|
4872
|
+
text: floor.label,
|
|
4873
|
+
anchor: [floor.center[0], floor.box.max[1] + 1.2, floor.center[2]],
|
|
4874
|
+
floorIndex: floor.index
|
|
3981
4875
|
});
|
|
3982
4876
|
}
|
|
3983
4877
|
}
|
|
@@ -4055,18 +4949,20 @@ function buildSceneModel(input) {
|
|
|
4055
4949
|
boxMinZ = Math.max(fpMinZ, contentMinZ - pad);
|
|
4056
4950
|
boxMaxZ = Math.min(fpMaxZ, contentMaxZ + pad);
|
|
4057
4951
|
}
|
|
4952
|
+
const boxMinY = Math.min(0, renderMinY);
|
|
4953
|
+
const boxMaxY = Math.max(renderMaxY, radius * 0.05);
|
|
4058
4954
|
return {
|
|
4059
4955
|
solids,
|
|
4060
4956
|
seats: seatData,
|
|
4061
4957
|
bounds: {
|
|
4062
4958
|
// Centred on the CONTENT box, so an off-centre table cluster arrives
|
|
4063
4959
|
// centred on screen instead of wherever the floor's midpoint put it.
|
|
4064
|
-
center: [(boxMinX + boxMaxX) / 2,
|
|
4960
|
+
center: [(boxMinX + boxMaxX) / 2, (boxMinY + boxMaxY) / 2, (boxMinZ + boxMaxZ) / 2],
|
|
4065
4961
|
radius,
|
|
4066
4962
|
groundY: 0,
|
|
4067
4963
|
box: {
|
|
4068
|
-
min: [boxMinX,
|
|
4069
|
-
max: [boxMaxX,
|
|
4964
|
+
min: [boxMinX, boxMinY, boxMinZ],
|
|
4965
|
+
max: [boxMaxX, boxMaxY, boxMaxZ]
|
|
4070
4966
|
}
|
|
4071
4967
|
},
|
|
4072
4968
|
renderBounds: { center: renderCenter, radius: renderRadius },
|
|
@@ -4088,6 +4984,7 @@ function buildSceneModel(input) {
|
|
|
4088
4984
|
var SEPARATION_X_PX = 88;
|
|
4089
4985
|
var SEPARATION_Y_PX = 20;
|
|
4090
4986
|
var KIND_STYLE = {
|
|
4987
|
+
floor: { size: 14, weight: "700", opacity: 0.98 },
|
|
4091
4988
|
zone: { size: 15, weight: "600", opacity: 0.95 },
|
|
4092
4989
|
section: { size: 12, weight: "500", opacity: 0.88 },
|
|
4093
4990
|
ga: { size: 12, weight: "500", opacity: 0.88 },
|
|
@@ -4108,6 +5005,27 @@ var DENSE_SEPARATION = {
|
|
|
4108
5005
|
// seating instead of clustering into one stack.
|
|
4109
5006
|
seat: { x: 46, y: 18 }
|
|
4110
5007
|
};
|
|
5008
|
+
function hexRgb(value) {
|
|
5009
|
+
const match = /^#([0-9a-f]{6})$/i.exec(value?.trim() ?? "");
|
|
5010
|
+
if (!match) return null;
|
|
5011
|
+
return [0, 2, 4].map((offset) => Number.parseInt(match[1].slice(offset, offset + 2), 16) / 255);
|
|
5012
|
+
}
|
|
5013
|
+
function luminance(rgb) {
|
|
5014
|
+
const linear = (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
|
5015
|
+
return 0.2126 * linear(rgb[0]) + 0.7152 * linear(rgb[1]) + 0.0722 * linear(rgb[2]);
|
|
5016
|
+
}
|
|
5017
|
+
function ratio(left, right) {
|
|
5018
|
+
const a = luminance(left);
|
|
5019
|
+
const b = luminance(right);
|
|
5020
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
5021
|
+
}
|
|
5022
|
+
function resolve3dLabelTreatment(preferredInk, backgroundRgb = [0.04, 0.06, 0.1]) {
|
|
5023
|
+
const darkScene = luminance(backgroundRgb) < 0.42;
|
|
5024
|
+
const fallbackInk = darkScene ? "#f4f7fb" : "#0b1220";
|
|
5025
|
+
const authored = hexRgb(preferredInk);
|
|
5026
|
+
const ink = authored && ratio(authored, backgroundRgb) >= 4.5 ? preferredInk : fallbackInk;
|
|
5027
|
+
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)" };
|
|
5028
|
+
}
|
|
4111
5029
|
var LabelOverlay = class {
|
|
4112
5030
|
constructor(container, opts = {}) {
|
|
4113
5031
|
this.nodes = /* @__PURE__ */ new Map();
|
|
@@ -4166,7 +5084,9 @@ var LabelOverlay = class {
|
|
|
4166
5084
|
*/
|
|
4167
5085
|
update(viewProjection, width, height, cameraDistance, venueRadius, cameraWorld) {
|
|
4168
5086
|
if (!this.labels.length) return;
|
|
4169
|
-
const kinds = visibleLabelKinds(cameraDistance, venueRadius
|
|
5087
|
+
const kinds = visibleLabelKinds(cameraDistance, venueRadius, {
|
|
5088
|
+
hasZoneLabels: this.labels.some((label) => label.kind === "zone")
|
|
5089
|
+
});
|
|
4170
5090
|
if (this.forcedDense) {
|
|
4171
5091
|
kinds.add("row");
|
|
4172
5092
|
kinds.add("seat");
|
|
@@ -4188,7 +5108,14 @@ var LabelOverlay = class {
|
|
|
4188
5108
|
...this.selectedSeatLabelIds.has(label.id) ? { priority: 1 } : {}
|
|
4189
5109
|
});
|
|
4190
5110
|
}
|
|
4191
|
-
const
|
|
5111
|
+
const floorLabels = spreadFloorLabels(
|
|
5112
|
+
candidates.filter((candidate) => candidate.label.kind === "floor"),
|
|
5113
|
+
width
|
|
5114
|
+
);
|
|
5115
|
+
const structure = [
|
|
5116
|
+
...candidates.filter((candidate) => !DENSE_KINDS.has(candidate.label.kind) && candidate.label.kind !== "floor"),
|
|
5117
|
+
...floorLabels
|
|
5118
|
+
];
|
|
4192
5119
|
const kept = [
|
|
4193
5120
|
...cullOverlapping(structure, SEPARATION_X_PX, SEPARATION_Y_PX),
|
|
4194
5121
|
// The dense rungs are BUDGETED, not merely deduplicated — see
|
|
@@ -4235,14 +5162,16 @@ var LabelOverlay = class {
|
|
|
4235
5162
|
s.fontSize = `${style.size}px`;
|
|
4236
5163
|
s.fontWeight = selected ? "800" : style.weight;
|
|
4237
5164
|
s.opacity = selected ? "1" : String(style.opacity);
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
s.
|
|
4241
|
-
s.
|
|
4242
|
-
s.
|
|
4243
|
-
s.
|
|
4244
|
-
s.
|
|
4245
|
-
|
|
5165
|
+
const treatment = resolve3dLabelTreatment(label.color ?? this.opts.ink, this.opts.backgroundRgb);
|
|
5166
|
+
const backed = !DENSE_KINDS.has(label.kind);
|
|
5167
|
+
s.color = selected ? "#ecf8ff" : treatment.ink;
|
|
5168
|
+
s.background = selected ? "rgba(10,28,45,.9)" : backed ? treatment.backing : "";
|
|
5169
|
+
s.border = selected ? "1px solid rgba(89,199,255,.92)" : backed ? `1px solid ${treatment.border}` : "";
|
|
5170
|
+
s.borderRadius = selected || backed ? "999px" : "";
|
|
5171
|
+
s.padding = selected ? "3px 7px" : backed ? "3px 7px" : "";
|
|
5172
|
+
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)" : "";
|
|
5173
|
+
s.letterSpacing = label.kind === "zone" || label.kind === "floor" ? "0.08em" : "0.02em";
|
|
5174
|
+
if (label.kind === "zone" || label.kind === "floor") s.textTransform = "uppercase";
|
|
4246
5175
|
return node;
|
|
4247
5176
|
}
|
|
4248
5177
|
dispose() {
|
|
@@ -4296,8 +5225,7 @@ out float vDim;
|
|
|
4296
5225
|
out float vMaterial;
|
|
4297
5226
|
void main() {
|
|
4298
5227
|
// Per-floor isolation without splitting the merged mesh into a draw call per
|
|
4299
|
-
// floor
|
|
4300
|
-
// buyer keeps the whole venue as context while looking at one level.
|
|
5228
|
+
// floor. The fragment pass removes non-focused floors for a clean level view.
|
|
4301
5229
|
vDim = (uFocusFloor < -0.5 || abs(floorIndex - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
4302
5230
|
vMaterial = materialIndex;
|
|
4303
5231
|
vec4 world = modelMatrix * vec4(position, 1.0);
|
|
@@ -4328,13 +5256,19 @@ uniform vec3 uKeyDir; // WORLD space, unit, points at
|
|
|
4328
5256
|
uniform float uStructureDetail;
|
|
4329
5257
|
out vec4 fragColor;
|
|
4330
5258
|
void main() {
|
|
5259
|
+
if (vDim > 0.5) discard;
|
|
4331
5260
|
// Row treads/risers are honest and useful when the buyer is inside a stand.
|
|
4332
5261
|
// At whole-venue scale their overlapping centimetre-scale surfaces quantise
|
|
4333
5262
|
// and alias into camera-dependent radial combs. The clean structural block
|
|
4334
5263
|
// cap remains, so overview keeps every stand without pretending to show row
|
|
4335
5264
|
// detail below a useful pixel size.
|
|
4336
5265
|
float rowDetail = step(1.5, vMaterial);
|
|
4337
|
-
|
|
5266
|
+
// The rung DISSOLVES rather than popping: uStructureDetail eases 0..1 over a
|
|
5267
|
+
// few frames (see the render loop) and each pixel holds a stable screen-door
|
|
5268
|
+
// threshold against it, so free-zooming across the handover reads as detail
|
|
5269
|
+
// developing instead of the venue changing in a single frame.
|
|
5270
|
+
float stipple = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
|
5271
|
+
if (rowDetail > 0.5 && stipple >= uStructureDetail) discard;
|
|
4338
5272
|
vec3 N = normalize(vNormalWorld);
|
|
4339
5273
|
vec3 V = normalize(-vPosView);
|
|
4340
5274
|
float hemi = 0.5 + 0.5 * N.y; // sky/ground gradient about WORLD up
|
|
@@ -4370,8 +5304,6 @@ void main() {
|
|
|
4370
5304
|
+ vec3(0.62, 0.46, 0.20) * band * 0.28
|
|
4371
5305
|
+ vec3(0.20, 0.10, 0.34) * focalHalo;
|
|
4372
5306
|
base = mix(base, display, poweredDisplay * verticalFace * 0.92);
|
|
4373
|
-
// Unfocused floors fall back toward the background rather than vanishing.
|
|
4374
|
-
base = mix(base, base * 0.45, vDim);
|
|
4375
5307
|
fragColor = vec4(base, 1.0);
|
|
4376
5308
|
}`
|
|
4377
5309
|
);
|
|
@@ -4455,7 +5387,11 @@ uniform float uSeatDetail; // 0 for venue/area overview; 1 in section/row/sea
|
|
|
4455
5387
|
uniform vec3 uFadeColor;
|
|
4456
5388
|
out vec4 fragColor;
|
|
4457
5389
|
void main() {
|
|
4458
|
-
if (
|
|
5390
|
+
if (vDim > 0.5) discard;
|
|
5391
|
+
// Same screen-door dissolve as the structure rung (uStructureDetail): the
|
|
5392
|
+
// dot layer develops over ~a quarter second instead of appearing at once.
|
|
5393
|
+
float stipple = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
|
5394
|
+
if (stipple >= uSeatDetail) discard;
|
|
4459
5395
|
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
4460
5396
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4461
5397
|
if (d > 1.0) discard;
|
|
@@ -4488,9 +5424,6 @@ void main() {
|
|
|
4488
5424
|
// while zooming. uSeatFade already measures this exact LOD transition: let
|
|
4489
5425
|
// the category-tinted tier carry the overview, then reveal individual live
|
|
4490
5426
|
// availability as the buyer approaches a section.
|
|
4491
|
-
// Seats on an unfocused floor recede with their structure.
|
|
4492
|
-
c = mix(c, uFadeColor, vDim * 0.75);
|
|
4493
|
-
alpha *= mix(1.0, 0.30, vDim);
|
|
4494
5427
|
// Yield to the chair. The chair grows out of this exact point, so through the
|
|
4495
5428
|
// band the dot is always at least as big as the chair inside it and the seat
|
|
4496
5429
|
// never thins out to nothing in between.
|
|
@@ -4635,6 +5568,7 @@ uniform vec3 uKeyDir;
|
|
|
4635
5568
|
uniform vec3 uFadeColor;
|
|
4636
5569
|
out vec4 fragColor;
|
|
4637
5570
|
void main() {
|
|
5571
|
+
if (vDim > 0.5) discard;
|
|
4638
5572
|
vec3 N = normalize(vNormalWorld);
|
|
4639
5573
|
vec3 V = normalize(-vPosView);
|
|
4640
5574
|
// The solids' rig, unchanged, so the chairs sit in the venue's light.
|
|
@@ -4667,7 +5601,6 @@ void main() {
|
|
|
4667
5601
|
// A brighter rim than the solids get: it picks out every chair's own edge,
|
|
4668
5602
|
// which is what stops a block of them merging into one mass up close.
|
|
4669
5603
|
base += vec3(0.26, 0.31, 0.38) * fres * 0.55;
|
|
4670
|
-
base = mix(base, uFadeColor, vDim * 0.75);
|
|
4671
5604
|
fragColor = vec4(base, 1.0);
|
|
4672
5605
|
}`
|
|
4673
5606
|
);
|
|
@@ -4705,6 +5638,7 @@ in vec2 position;
|
|
|
4705
5638
|
in vec3 iOffset;
|
|
4706
5639
|
in float iMaxRadius;
|
|
4707
5640
|
in float iPhysicalSeat;
|
|
5641
|
+
in float iFloor;
|
|
4708
5642
|
uniform mat4 modelViewMatrix;
|
|
4709
5643
|
uniform mat4 projectionMatrix;
|
|
4710
5644
|
uniform float uSeatRadius;
|
|
@@ -4713,9 +5647,11 @@ uniform float uMinPixels;
|
|
|
4713
5647
|
uniform float uPixelToWorld;
|
|
4714
5648
|
uniform float uChairFull;
|
|
4715
5649
|
uniform float uChairNone;
|
|
5650
|
+
uniform float uFocusFloor;
|
|
4716
5651
|
out vec2 vUv;
|
|
4717
5652
|
out float vPhysicalSeat;
|
|
4718
5653
|
out float vDotWeight;
|
|
5654
|
+
out float vHidden;
|
|
4719
5655
|
flat out vec3 vPick;
|
|
4720
5656
|
${CHAIR_WEIGHT_GLSL}
|
|
4721
5657
|
void main() {
|
|
@@ -4731,6 +5667,7 @@ void main() {
|
|
|
4731
5667
|
mv.xy += normalize(vec3(modelViewMatrix * vec4(0.0, 1.0, 0.0, 0.0))).xy * r;
|
|
4732
5668
|
vUv = position;
|
|
4733
5669
|
vPhysicalSeat = iPhysicalSeat;
|
|
5670
|
+
vHidden = (uFocusFloor > -0.5 && abs(iFloor - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4734
5671
|
gl_Position = projectionMatrix * mv;
|
|
4735
5672
|
}`
|
|
4736
5673
|
);
|
|
@@ -4741,9 +5678,11 @@ precision highp float;
|
|
|
4741
5678
|
in vec2 vUv;
|
|
4742
5679
|
in float vPhysicalSeat;
|
|
4743
5680
|
in float vDotWeight;
|
|
5681
|
+
in float vHidden;
|
|
4744
5682
|
flat in vec3 vPick;
|
|
4745
5683
|
out vec4 fragColor;
|
|
4746
5684
|
void main() {
|
|
5685
|
+
if (vHidden > 0.5) discard;
|
|
4747
5686
|
if (vDotWeight <= 0.02) discard; // invisible dot cannot steal a chair tap
|
|
4748
5687
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4749
5688
|
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
@@ -4762,18 +5701,22 @@ in float iYaw;
|
|
|
4762
5701
|
in float iSeed;
|
|
4763
5702
|
in float iPhysicalSeat;
|
|
4764
5703
|
in float iPickIndex;
|
|
5704
|
+
in float iFloor;
|
|
4765
5705
|
uniform mat4 modelViewMatrix;
|
|
4766
5706
|
uniform mat4 projectionMatrix;
|
|
4767
5707
|
uniform float uChairFull;
|
|
4768
5708
|
uniform float uChairNone;
|
|
4769
5709
|
uniform float uBackRake;
|
|
4770
5710
|
uniform float uBackBase;
|
|
5711
|
+
uniform float uFocusFloor;
|
|
4771
5712
|
flat out vec3 vPick;
|
|
5713
|
+
out float vHidden;
|
|
4772
5714
|
${CHAIR_WEIGHT_GLSL}
|
|
4773
5715
|
${OCCUPANT_WEIGHT_GLSL}
|
|
4774
5716
|
void main() {
|
|
4775
5717
|
int id = int(iPickIndex + 0.5); // already global index + 1
|
|
4776
5718
|
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
5719
|
+
vHidden = (uFocusFloor > -0.5 && abs(iFloor - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4777
5720
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4778
5721
|
float viewDepth = -anchor.z;
|
|
4779
5722
|
float depth = max(viewDepth, 0.001);
|
|
@@ -4806,17 +5749,25 @@ var CHAIR_PICK_FRAG = (
|
|
|
4806
5749
|
`#version 300 es
|
|
4807
5750
|
precision highp float;
|
|
4808
5751
|
flat in vec3 vPick;
|
|
5752
|
+
in float vHidden;
|
|
4809
5753
|
out vec4 fragColor;
|
|
4810
|
-
void main() {
|
|
5754
|
+
void main() {
|
|
5755
|
+
if (vHidden > 0.5) discard;
|
|
5756
|
+
fragColor = vec4(vPick, 1.0);
|
|
5757
|
+
}`
|
|
4811
5758
|
);
|
|
4812
5759
|
var PICK_DEPTH_VERT = (
|
|
4813
5760
|
/* glsl */
|
|
4814
5761
|
`#version 300 es
|
|
4815
5762
|
precision highp float;
|
|
4816
5763
|
in vec3 position;
|
|
5764
|
+
in float floorIndex;
|
|
4817
5765
|
uniform mat4 modelViewMatrix;
|
|
4818
5766
|
uniform mat4 projectionMatrix;
|
|
5767
|
+
uniform float uFocusFloor;
|
|
5768
|
+
out float vHidden;
|
|
4819
5769
|
void main() {
|
|
5770
|
+
vHidden = (uFocusFloor > -0.5 && abs(floorIndex - uFocusFloor) > 0.5) ? 1.0 : 0.0;
|
|
4820
5771
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
4821
5772
|
}`
|
|
4822
5773
|
);
|
|
@@ -4824,8 +5775,12 @@ var PICK_DEPTH_FRAG = (
|
|
|
4824
5775
|
/* glsl */
|
|
4825
5776
|
`#version 300 es
|
|
4826
5777
|
precision highp float;
|
|
5778
|
+
in float vHidden;
|
|
4827
5779
|
out vec4 fragColor;
|
|
4828
|
-
void main() {
|
|
5780
|
+
void main() {
|
|
5781
|
+
if (vHidden > 0.5) discard;
|
|
5782
|
+
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
|
5783
|
+
}`
|
|
4829
5784
|
);
|
|
4830
5785
|
function createSeatPickProgram(gl) {
|
|
4831
5786
|
return new import_ogl3.Program(gl, {
|
|
@@ -4841,7 +5796,8 @@ function createSeatPickProgram(gl) {
|
|
|
4841
5796
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
4842
5797
|
uPixelToWorld: { value: 2e-3 },
|
|
4843
5798
|
uChairFull: { value: CHAIR_FULL_M },
|
|
4844
|
-
uChairNone: { value: CHAIR_NONE_M }
|
|
5799
|
+
uChairNone: { value: CHAIR_NONE_M },
|
|
5800
|
+
uFocusFloor: { value: -1 }
|
|
4845
5801
|
}
|
|
4846
5802
|
});
|
|
4847
5803
|
}
|
|
@@ -4857,7 +5813,8 @@ function createChairPickProgram(gl) {
|
|
|
4857
5813
|
uChairFull: { value: CHAIR_FULL_M },
|
|
4858
5814
|
uChairNone: { value: CHAIR_NONE_M },
|
|
4859
5815
|
uBackRake: { value: BACK_RAKE_SLOPE },
|
|
4860
|
-
uBackBase: { value: BACK_BASE_M }
|
|
5816
|
+
uBackBase: { value: BACK_BASE_M },
|
|
5817
|
+
uFocusFloor: { value: -1 }
|
|
4861
5818
|
}
|
|
4862
5819
|
});
|
|
4863
5820
|
}
|
|
@@ -4868,7 +5825,10 @@ function createPickDepthProgram(gl) {
|
|
|
4868
5825
|
transparent: false,
|
|
4869
5826
|
depthTest: true,
|
|
4870
5827
|
depthWrite: true,
|
|
4871
|
-
cullFace: false
|
|
5828
|
+
cullFace: false,
|
|
5829
|
+
uniforms: {
|
|
5830
|
+
uFocusFloor: { value: -1 }
|
|
5831
|
+
}
|
|
4872
5832
|
});
|
|
4873
5833
|
}
|
|
4874
5834
|
function createSolidProgram(gl) {
|
|
@@ -5202,6 +6162,10 @@ var PickPipeline = class {
|
|
|
5202
6162
|
/** Match the display seat sizing so the pick mask lines up with the dots. */
|
|
5203
6163
|
syncFromSeatProgram(seatProgram) {
|
|
5204
6164
|
for (const k of SYNC_KEYS) this.seatProg.uniforms[k].value = seatProgram.uniforms[k].value;
|
|
6165
|
+
const focusFloor = seatProgram.uniforms.uFocusFloor.value;
|
|
6166
|
+
this.seatProg.uniforms.uFocusFloor.value = focusFloor;
|
|
6167
|
+
this.chairProg.uniforms.uFocusFloor.value = focusFloor;
|
|
6168
|
+
this.depthProg.uniforms.uFocusFloor.value = focusFloor;
|
|
5205
6169
|
}
|
|
5206
6170
|
ensureTarget() {
|
|
5207
6171
|
const w = this.gl.drawingBufferWidth;
|
|
@@ -5548,8 +6512,8 @@ function clampPanoramaFov(fovDeg) {
|
|
|
5548
6512
|
return Math.max(MIN_VFOV_DEG, Math.min(MAX_VFOV_DEG, fovDeg));
|
|
5549
6513
|
}
|
|
5550
6514
|
var MAX_PITCH_DEG = 35;
|
|
5551
|
-
function bearingToOffsetPx(
|
|
5552
|
-
const col = (0.5 +
|
|
6515
|
+
function bearingToOffsetPx(bearingDeg2, viewportW, bgW) {
|
|
6516
|
+
const col = (0.5 + bearingDeg2 / 360) * bgW;
|
|
5553
6517
|
return viewportW / 2 - col;
|
|
5554
6518
|
}
|
|
5555
6519
|
function windowedBgHeight(viewportH, vfovDeg = VFOV_DEG) {
|
|
@@ -5861,9 +6825,9 @@ function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEG
|
|
|
5861
6825
|
const uv = new Float32Array(vertexCount * 2);
|
|
5862
6826
|
for (let iLat = 0; iLat < latCount; iLat++) {
|
|
5863
6827
|
const v = iLat / segmentsLat;
|
|
5864
|
-
const
|
|
5865
|
-
const sinPolar = Math.sin(
|
|
5866
|
-
const cosPolar = Math.cos(
|
|
6828
|
+
const polar2 = v * Math.PI;
|
|
6829
|
+
const sinPolar = Math.sin(polar2);
|
|
6830
|
+
const cosPolar = Math.cos(polar2);
|
|
5867
6831
|
for (let iLon = 0; iLon < lonCount; iLon++) {
|
|
5868
6832
|
const u = iLon / segmentsLon;
|
|
5869
6833
|
const azimuth = (u - 0.5) * Math.PI * 2;
|
|
@@ -5891,8 +6855,8 @@ function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEG
|
|
|
5891
6855
|
}
|
|
5892
6856
|
return { position, uv, index };
|
|
5893
6857
|
}
|
|
5894
|
-
function bearingPitchToDirection(
|
|
5895
|
-
const yaw =
|
|
6858
|
+
function bearingPitchToDirection(bearingDeg2, pitchDeg) {
|
|
6859
|
+
const yaw = bearingDeg2 * Math.PI / 180;
|
|
5896
6860
|
const pitch = pitchDeg * Math.PI / 180;
|
|
5897
6861
|
const cosPitch = Math.cos(pitch);
|
|
5898
6862
|
return [cosPitch * Math.sin(yaw), Math.sin(pitch), -cosPitch * Math.cos(yaw)];
|
|
@@ -6397,7 +7361,7 @@ function prepareVenue3D(input) {
|
|
|
6397
7361
|
}
|
|
6398
7362
|
|
|
6399
7363
|
// src/view3d/index.ts
|
|
6400
|
-
var
|
|
7364
|
+
var DEG5 = Math.PI / 180;
|
|
6401
7365
|
var TAP_SLOP = 6;
|
|
6402
7366
|
var TAP_MS = 500;
|
|
6403
7367
|
function mountVenue3D(container, input, opts = {}) {
|
|
@@ -6465,11 +7429,11 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6465
7429
|
const nearBuf = new Int32Array(CHAIR_MAX_INSTANCES);
|
|
6466
7430
|
let lastGatherX = Infinity;
|
|
6467
7431
|
let lastGatherZ = Infinity;
|
|
6468
|
-
let nearFieldDetailEnabled = false;
|
|
6469
7432
|
let cameraDetailVisible = false;
|
|
7433
|
+
let seatDetailMix = 0;
|
|
7434
|
+
let structureDetailMix = 0;
|
|
6470
7435
|
let panoramaSphereVisible = false;
|
|
6471
7436
|
const setNearFieldDetailEnabled = (enabled) => {
|
|
6472
|
-
nearFieldDetailEnabled = enabled;
|
|
6473
7437
|
if (!enabled) cameraDetailVisible = false;
|
|
6474
7438
|
lastGatherX = Infinity;
|
|
6475
7439
|
lastGatherZ = Infinity;
|
|
@@ -6518,27 +7482,36 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6518
7482
|
// first real drag/wheel/pinch (not the intro ease)
|
|
6519
7483
|
);
|
|
6520
7484
|
orbit.setAspect(glctx.aspect);
|
|
7485
|
+
const inferredImportedGeometry = (input.doc.floors?.flatMap((floor) => floor.objects) ?? input.doc.objects).some((object) => object.type === "section" && object.physical3dProvenance?.surfaceKind?.status === "inferred");
|
|
6521
7486
|
const stageAzimuth = (() => {
|
|
7487
|
+
if (inferredImportedGeometry && model.stages.length === 0) return void 0;
|
|
6522
7488
|
const dx = model.focalWorld[0] - model.bounds.center[0];
|
|
6523
7489
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
6524
7490
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
6525
7491
|
})();
|
|
7492
|
+
const overviewFraming = model.floors.length > 1 ? { polarDeg: 48, margin: 1.1 } : void 0;
|
|
6526
7493
|
const reduceMotionOnMount = typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6527
|
-
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
7494
|
+
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
6528
7495
|
const cinematic = new Cinematic(orbit.camera);
|
|
6529
7496
|
const restoreContainerPosition = establishPositioningContext(container);
|
|
6530
7497
|
const labelOverlay = new LabelOverlay(container, {
|
|
6531
7498
|
fontFamily: input.doc.theme?.fontFamily,
|
|
6532
|
-
ink: input.doc.theme?.textColor
|
|
7499
|
+
ink: input.doc.theme?.textColor,
|
|
7500
|
+
backgroundRgb: model.theme.background.top
|
|
6533
7501
|
});
|
|
6534
|
-
labelOverlay.setLabels(model.labels);
|
|
6535
7502
|
const baseLabels = model.labels;
|
|
7503
|
+
const labelsForFocusedFloor = () => labelsForFloorView(
|
|
7504
|
+
baseLabels,
|
|
7505
|
+
model.floors.length,
|
|
7506
|
+
focusedFloor
|
|
7507
|
+
);
|
|
7508
|
+
labelOverlay.setLabels(labelsForFocusedFloor());
|
|
6536
7509
|
let focusedSectionId = null;
|
|
6537
7510
|
const showSectionSeatLabels = (sectionId) => {
|
|
6538
7511
|
focusedSectionId = sectionId;
|
|
6539
7512
|
labelOverlay.setForcedDense(!!sectionId);
|
|
6540
7513
|
if (!sectionId) {
|
|
6541
|
-
labelOverlay.setLabels(
|
|
7514
|
+
labelOverlay.setLabels(labelsForFocusedFloor());
|
|
6542
7515
|
return;
|
|
6543
7516
|
}
|
|
6544
7517
|
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
@@ -6546,7 +7519,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6546
7519
|
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
6547
7520
|
const selectedIds = new Set(selection.keys());
|
|
6548
7521
|
const selectedInSection = new Set(sectionSeats.filter((seat) => selectedIds.has(seat.id)).map((seat) => seat.id));
|
|
6549
|
-
const focusedBase =
|
|
7522
|
+
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)))));
|
|
6550
7523
|
if (model.seatCount <= 6e3 && selectedInSection.size === 0) {
|
|
6551
7524
|
labelOverlay.setLabels(focusedBase);
|
|
6552
7525
|
return;
|
|
@@ -6577,29 +7550,58 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6577
7550
|
]);
|
|
6578
7551
|
};
|
|
6579
7552
|
rebuildGpu();
|
|
6580
|
-
const loop = new RenderLoop(() => {
|
|
7553
|
+
const loop = new RenderLoop((dt) => {
|
|
6581
7554
|
if (contextLost || !gpu || frozen) return false;
|
|
6582
7555
|
const flying = cinematic.active;
|
|
6583
7556
|
const moving = flying ? cinematic.update(performance.now()) : orbit.update();
|
|
6584
|
-
const
|
|
7557
|
+
const activeFloorBounds = focusedFloor >= 0 ? model.floors[focusedFloor] : null;
|
|
7558
|
+
const lodRadius = activeFloorBounds?.radius ?? model.bounds.radius;
|
|
7559
|
+
const lod = computeSeatLod(orbit.currentDistance, lodRadius);
|
|
6585
7560
|
const clippingBounds = panoramaSphereVisible ? { center: [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z], radius: 520 } : model.renderBounds;
|
|
6586
7561
|
orbit.updateClipping(clippingBounds);
|
|
6587
|
-
const
|
|
6588
|
-
const
|
|
6589
|
-
const
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
7562
|
+
const detailCenter = activeFloorBounds?.center ?? model.renderBounds.center;
|
|
7563
|
+
const detailRadius = activeFloorBounds?.radius ?? model.renderBounds.radius;
|
|
7564
|
+
const withinScope = (center, radius) => {
|
|
7565
|
+
const d = Math.hypot(
|
|
7566
|
+
orbit.camera.position.x - center[0],
|
|
7567
|
+
orbit.camera.position.y - center[1],
|
|
7568
|
+
orbit.camera.position.z - center[2]
|
|
7569
|
+
);
|
|
7570
|
+
return d <= radius * (cameraDetailVisible ? 1.8 : 1.55);
|
|
7571
|
+
};
|
|
7572
|
+
let withinDetail = withinScope(detailCenter, detailRadius);
|
|
7573
|
+
if (!withinDetail && focusedSectionId) {
|
|
7574
|
+
const sec = model.sections.find((candidate) => candidate.id === focusedSectionId);
|
|
7575
|
+
if (sec) withinDetail = withinScope(sec.center, Math.max(12, sec.radius * 2));
|
|
7576
|
+
}
|
|
7577
|
+
cameraDetailVisible = withinDetail;
|
|
6596
7578
|
const u = gpu.seatProgram.uniforms;
|
|
6597
7579
|
u.uSeatScale.value = lod.scale;
|
|
6598
7580
|
u.uSeatFade.value = lod.fade;
|
|
6599
|
-
|
|
6600
|
-
|
|
7581
|
+
const floorSeatDetail = !!activeFloorBounds && floorOverviewSeatDetailVisible(
|
|
7582
|
+
activeFloorBounds.seatCount,
|
|
7583
|
+
glctx.canvas.clientWidth || 1,
|
|
7584
|
+
glctx.canvas.clientHeight || 1
|
|
7585
|
+
);
|
|
7586
|
+
const venueSeatDetail = projectedSeatDetailVisible(
|
|
7587
|
+
model.seats.count,
|
|
7588
|
+
lodRadius,
|
|
7589
|
+
orbit.currentDistance,
|
|
7590
|
+
orbit.camera.fov,
|
|
7591
|
+
glctx.canvas.clientHeight || 1
|
|
7592
|
+
);
|
|
7593
|
+
const seatDetailTarget = cameraDetailVisible || floorSeatDetail || venueSeatDetail ? 1 : 0;
|
|
7594
|
+
const structureDetailTarget = cameraDetailVisible || focusedFloor >= 0 || model.floors.length > 1 ? 1 : 0;
|
|
7595
|
+
const easeIn = 1 - Math.exp(-dt * 9);
|
|
7596
|
+
const easeOut = 1 - Math.exp(-dt * 20);
|
|
7597
|
+
seatDetailMix += (seatDetailTarget - seatDetailMix) * (seatDetailTarget > seatDetailMix ? easeIn : easeOut);
|
|
7598
|
+
structureDetailMix += (structureDetailTarget - structureDetailMix) * (structureDetailTarget > structureDetailMix ? easeIn : easeOut);
|
|
7599
|
+
if (Math.abs(seatDetailTarget - seatDetailMix) < 0.01) seatDetailMix = seatDetailTarget;
|
|
7600
|
+
if (Math.abs(structureDetailTarget - structureDetailMix) < 0.01) structureDetailMix = structureDetailTarget;
|
|
7601
|
+
u.uSeatDetail.value = seatDetailMix;
|
|
7602
|
+
gpu.solidProgram.uniforms.uStructureDetail.value = structureDetailMix;
|
|
6601
7603
|
u.uMinPixels.value = lod.minPixels;
|
|
6602
|
-
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov *
|
|
7604
|
+
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov * DEG5 / 2) / Math.max(1, glctx.pixelHeight);
|
|
6603
7605
|
updateNearField(cameraDetailVisible);
|
|
6604
7606
|
glctx.renderer.render({ scene: gpu.background, clear: true });
|
|
6605
7607
|
glctx.renderer.render({ scene: gpu.main, camera: orbit.camera, clear: false });
|
|
@@ -6608,12 +7610,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6608
7610
|
glctx.canvas.clientWidth || 1,
|
|
6609
7611
|
glctx.canvas.clientHeight || 1,
|
|
6610
7612
|
orbit.currentDistance,
|
|
6611
|
-
|
|
7613
|
+
lodRadius,
|
|
6612
7614
|
// The dense label rungs rank by real distance from the eye, not by the
|
|
6613
7615
|
// orbit radius — at the arrival pose those are wildly different numbers.
|
|
6614
7616
|
[orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z]
|
|
6615
7617
|
);
|
|
6616
|
-
return moving;
|
|
7618
|
+
return moving || seatDetailMix !== seatDetailTarget || structureDetailMix !== structureDetailTarget;
|
|
6617
7619
|
});
|
|
6618
7620
|
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => handle.resize()) : null;
|
|
6619
7621
|
ro?.observe(container);
|
|
@@ -6657,14 +7659,20 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6657
7659
|
const resolvedSeatViewPose = (seatId, idx) => {
|
|
6658
7660
|
const seat = expandedSeatById.get(seatId);
|
|
6659
7661
|
const floorIndex = Math.max(0, Math.round(model.seats.iFloor[idx] ?? 0));
|
|
6660
|
-
const
|
|
6661
|
-
const
|
|
7662
|
+
const sourceFloor = input.doc.floors?.[floorIndex];
|
|
7663
|
+
const sceneFloor = model.floors[floorIndex];
|
|
7664
|
+
const sourceFocal = sourceFloor?.focalPoint ?? input.doc.focalPoint;
|
|
7665
|
+
const sceneFocal = sceneFloor ? { x: sceneFloor.focalWorld[0] / M, y: sceneFloor.focalWorld[2] / M } : sourceFocal;
|
|
7666
|
+
const focalPoint = seat?.focalPoint ? {
|
|
7667
|
+
x: seat.focalPoint.x + sceneFocal.x - sourceFocal.x,
|
|
7668
|
+
y: seat.focalPoint.y + sceneFocal.y - sourceFocal.y
|
|
7669
|
+
} : sceneFocal;
|
|
6662
7670
|
const deck = [
|
|
6663
7671
|
model.seats.iPosition[idx * 3],
|
|
6664
7672
|
model.seats.iPosition[idx * 3 + 1],
|
|
6665
7673
|
model.seats.iPosition[idx * 3 + 2]
|
|
6666
7674
|
];
|
|
6667
|
-
return seatViewPose(deck, focalPoint,
|
|
7675
|
+
return seatViewPose(deck, focalPoint, sourceFloor?.baseHeightM ?? 0, seat?.eyeHeightM);
|
|
6668
7676
|
};
|
|
6669
7677
|
const placeCameraFinal = (finalPos, focal, fov = FOV_END) => {
|
|
6670
7678
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
@@ -6894,8 +7902,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6894
7902
|
overviewChip.className = "sl-3d-overview-control sl-3d-icon-control";
|
|
6895
7903
|
overviewChip.type = "button";
|
|
6896
7904
|
overviewChip.textContent = "\u2302";
|
|
6897
|
-
overviewChip.setAttribute("aria-label", "Fit the whole venue in view");
|
|
6898
|
-
overviewChip.dataset.tooltip = "Fit
|
|
7905
|
+
overviewChip.setAttribute("aria-label", "Fit the active level or whole venue in view");
|
|
7906
|
+
overviewChip.dataset.tooltip = "Fit view";
|
|
6899
7907
|
Object.assign(overviewChip.style, {
|
|
6900
7908
|
width: "44px",
|
|
6901
7909
|
minWidth: "44px",
|
|
@@ -6914,6 +7922,15 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6914
7922
|
overviewChip.textContent = "\u2302";
|
|
6915
7923
|
};
|
|
6916
7924
|
layoutOverviewChip();
|
|
7925
|
+
const applyFloorFocus = (value) => {
|
|
7926
|
+
focusedFloor = value;
|
|
7927
|
+
if (gpu) {
|
|
7928
|
+
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
7929
|
+
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
7930
|
+
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
7931
|
+
}
|
|
7932
|
+
showSectionSeatLabels(null);
|
|
7933
|
+
};
|
|
6917
7934
|
const focusOverview = () => {
|
|
6918
7935
|
if (disposed) return;
|
|
6919
7936
|
opts.onViewTargetChange?.(null);
|
|
@@ -6930,13 +7947,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6930
7947
|
overviewChip.style.display = "";
|
|
6931
7948
|
labelOverlay.setVisible(true);
|
|
6932
7949
|
frozen = false;
|
|
6933
|
-
|
|
7950
|
+
const activeFloor = focusedFloor >= 0 ? model.floors[focusedFloor] : null;
|
|
7951
|
+
setNearFieldDetailEnabled(!!activeFloor);
|
|
6934
7952
|
setSeatViewLocked(false);
|
|
6935
7953
|
cancelFlight();
|
|
6936
7954
|
removeArriveChip();
|
|
6937
7955
|
setNavigationMode("orbit");
|
|
6938
|
-
if (
|
|
6939
|
-
|
|
7956
|
+
if (activeFloor) {
|
|
7957
|
+
const dx = activeFloor.focalWorld[0] - activeFloor.center[0];
|
|
7958
|
+
const dz = activeFloor.focalWorld[2] - activeFloor.center[2];
|
|
7959
|
+
const azimuth = Math.hypot(dx, dz) > activeFloor.radius * 0.12 ? Math.atan2(dx, dz) : stageAzimuth;
|
|
7960
|
+
const floorBounds = { center: activeFloor.center, radius: activeFloor.radius, box: activeFloor.box };
|
|
7961
|
+
const floorFraming = { polarDeg: 58, margin: 1.12 };
|
|
7962
|
+
if (reducedMotion()) orbit.frame(floorBounds, false, azimuth, false, floorFraming);
|
|
7963
|
+
else orbit.frameSoft(floorBounds, azimuth, false, floorFraming);
|
|
7964
|
+
} else if (reducedMotion()) {
|
|
7965
|
+
orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7966
|
+
} else {
|
|
7967
|
+
orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
7968
|
+
}
|
|
6940
7969
|
showSectionSeatLabels(null);
|
|
6941
7970
|
loop.requestRender();
|
|
6942
7971
|
};
|
|
@@ -7123,6 +8152,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
7123
8152
|
setSeatViewLocked(false);
|
|
7124
8153
|
cinematic.cancel();
|
|
7125
8154
|
removeArriveChip();
|
|
8155
|
+
if (model.floors.length > 1 && focusedFloor !== sec.floorIndex) applyFloorFocus(sec.floorIndex);
|
|
7126
8156
|
setNavigationMode("pan");
|
|
7127
8157
|
showSectionSeatLabels(sectionId);
|
|
7128
8158
|
const dx = sec.focalWorld[0] - sec.center[0];
|
|
@@ -7305,20 +8335,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
7305
8335
|
focusFloor(index) {
|
|
7306
8336
|
if (index !== null && !model.floors.some((f) => f.index === index)) return false;
|
|
7307
8337
|
const value = index ?? -1;
|
|
7308
|
-
setNearFieldDetailEnabled(
|
|
8338
|
+
setNearFieldDetailEnabled(index !== null);
|
|
7309
8339
|
setSeatViewLocked(false);
|
|
7310
|
-
|
|
7311
|
-
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
7312
|
-
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
7313
|
-
gpu.chairProgram.uniforms.uFocusFloor.value = value;
|
|
7314
|
-
}
|
|
7315
|
-
focusedFloor = value;
|
|
8340
|
+
applyFloorFocus(value);
|
|
7316
8341
|
setNavigationMode("orbit");
|
|
7317
|
-
if (index
|
|
8342
|
+
if (index === null) {
|
|
8343
|
+
cinematic.cancel();
|
|
8344
|
+
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
8345
|
+
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true, overviewFraming);
|
|
8346
|
+
} else {
|
|
7318
8347
|
const f = model.floors[index];
|
|
7319
8348
|
if (f.seatCount > 0) {
|
|
7320
8349
|
cinematic.cancel();
|
|
7321
|
-
|
|
8350
|
+
const dx = f.focalWorld[0] - f.center[0];
|
|
8351
|
+
const dz = f.focalWorld[2] - f.center[2];
|
|
8352
|
+
const azimuth = Math.hypot(dx, dz) > f.radius * 0.12 ? Math.atan2(dx, dz) : stageAzimuth;
|
|
8353
|
+
orbit.frame({ center: f.center, radius: f.radius, box: f.box }, false, azimuth, false, {
|
|
8354
|
+
polarDeg: 58,
|
|
8355
|
+
margin: 1.12
|
|
8356
|
+
});
|
|
7322
8357
|
}
|
|
7323
8358
|
}
|
|
7324
8359
|
loop.requestRender();
|