@seatlayer/core 0.48.2 → 0.49.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-I6DPX3EM.js → chunk-BE3F7CT3.js} +6 -5
- package/dist/chunk-BE3F7CT3.js.map +1 -0
- package/dist/{chunk-TCTAS4Z2.js → chunk-GD267BFB.js} +36 -8
- package/dist/chunk-GD267BFB.js.map +1 -0
- package/dist/core/seatConfidence.cjs +76 -0
- package/dist/core/seatConfidence.cjs.map +1 -0
- package/dist/core/seatConfidence.d.cts +18 -0
- package/dist/core/seatConfidence.d.ts +18 -0
- package/dist/core/seatConfidence.js +51 -0
- package/dist/core/seatConfidence.js.map +1 -0
- package/dist/index.cjs +35 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/{types-CE63BK0j.d.cts → types-DR54nqKL.d.cts} +58 -1
- package/dist/{types-CE63BK0j.d.ts → types-DR54nqKL.d.ts} +58 -1
- package/dist/view3d/crossfade/panorama.cjs +5 -4
- package/dist/view3d/crossfade/panorama.cjs.map +1 -1
- package/dist/view3d/crossfade/panorama.d.cts +5 -1
- package/dist/view3d/crossfade/panorama.d.ts +5 -1
- package/dist/view3d/crossfade/panorama.js +1 -1
- package/dist/view3d/index.cjs +781 -255
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +22 -1
- package/dist/view3d/index.d.ts +22 -1
- package/dist/view3d/index.js +779 -254
- package/dist/view3d/index.js.map +1 -1
- package/package.json +15 -2
- package/dist/chunk-I6DPX3EM.js.map +0 -1
- package/dist/chunk-TCTAS4Z2.js.map +0 -1
package/dist/view3d/index.js
CHANGED
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
pointInPolygonWithHoles,
|
|
8
8
|
resolveSection,
|
|
9
9
|
sectionGeometry
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-GD267BFB.js";
|
|
11
11
|
import {
|
|
12
12
|
MAX_PITCH_DEG,
|
|
13
13
|
VFOV_DEG,
|
|
14
14
|
clampPanoramaFov,
|
|
15
15
|
mountPanorama,
|
|
16
16
|
seatViewDisclosure
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-BE3F7CT3.js";
|
|
18
18
|
import {
|
|
19
19
|
browserPanoramaConstraints,
|
|
20
20
|
loadPanoramaImage,
|
|
@@ -69,8 +69,8 @@ var STRUCTURE = {
|
|
|
69
69
|
* so an organizer's own stage colour survives — it is just no longer lit like
|
|
70
70
|
* a basement.
|
|
71
71
|
*/
|
|
72
|
-
stageTop: [0.
|
|
73
|
-
stageWall: [0.
|
|
72
|
+
stageTop: [0.94, 0.7, 0.38],
|
|
73
|
+
stageWall: [0.48, 0.33, 0.18],
|
|
74
74
|
decorTop: [0.22, 0.25, 0.29],
|
|
75
75
|
decorWall: [0.15, 0.17, 0.2],
|
|
76
76
|
gaTop: [0.24, 0.28, 0.33],
|
|
@@ -224,6 +224,10 @@ var OrbitCamera = class {
|
|
|
224
224
|
this.minDist = 1;
|
|
225
225
|
this.maxDist = 100;
|
|
226
226
|
this.gestureFired = false;
|
|
227
|
+
/** Seat-eye arrival is a fixed physical origin. The buyer may opt into the
|
|
228
|
+
* dedicated look-around viewer, but venue orbit/dolly/pan must not pull the
|
|
229
|
+
* camera outside the selected seat. */
|
|
230
|
+
this.interactionEnabled = true;
|
|
227
231
|
this.dragging = false;
|
|
228
232
|
this.panning = false;
|
|
229
233
|
this.lastX = 0;
|
|
@@ -246,6 +250,7 @@ var OrbitCamera = class {
|
|
|
246
250
|
this.requestRender = requestRender;
|
|
247
251
|
this.onGesture = onGesture;
|
|
248
252
|
this.onPointerDown = (e) => {
|
|
253
|
+
if (!this.interactionEnabled) return;
|
|
249
254
|
try {
|
|
250
255
|
this.canvas.setPointerCapture?.(e.pointerId);
|
|
251
256
|
} catch {
|
|
@@ -268,6 +273,7 @@ var OrbitCamera = class {
|
|
|
268
273
|
}
|
|
269
274
|
};
|
|
270
275
|
this.onPointerMove = (e) => {
|
|
276
|
+
if (!this.interactionEnabled) return;
|
|
271
277
|
if (!this.activePointers.has(e.pointerId)) return;
|
|
272
278
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
273
279
|
if (this.activePointers.size >= 2) {
|
|
@@ -319,12 +325,14 @@ var OrbitCamera = class {
|
|
|
319
325
|
};
|
|
320
326
|
this.onWheel = (e) => {
|
|
321
327
|
e.preventDefault();
|
|
328
|
+
if (!this.interactionEnabled) return;
|
|
322
329
|
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
323
330
|
const norm2 = Math.max(-2, Math.min(2, e.deltaY * unit / 100));
|
|
324
331
|
this.dollyBy(Math.exp(norm2 * 0.22));
|
|
325
332
|
this.fireGesture();
|
|
326
333
|
};
|
|
327
334
|
this.onKeyDown = (e) => {
|
|
335
|
+
if (!this.interactionEnabled) return;
|
|
328
336
|
const step = 7 * DEG;
|
|
329
337
|
if (e.shiftKey && (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
330
338
|
this.panBy(
|
|
@@ -422,6 +430,7 @@ var OrbitCamera = class {
|
|
|
422
430
|
/** Programmatic zoom used by the visible camera controls. Keeping it on the
|
|
423
431
|
* same dolly path as wheel, pinch and keyboard preserves all distance limits. */
|
|
424
432
|
zoomBy(factor) {
|
|
433
|
+
if (!this.interactionEnabled) return;
|
|
425
434
|
this.dollyBy(factor);
|
|
426
435
|
this.fireGesture();
|
|
427
436
|
}
|
|
@@ -431,6 +440,15 @@ var OrbitCamera = class {
|
|
|
431
440
|
setPrimaryDragMode(mode) {
|
|
432
441
|
this.primaryDragMode = mode;
|
|
433
442
|
}
|
|
443
|
+
/** Enable venue navigation or pin the current camera pose for seat-eye mode. */
|
|
444
|
+
setInteractionEnabled(enabled) {
|
|
445
|
+
this.interactionEnabled = enabled;
|
|
446
|
+
if (enabled) return;
|
|
447
|
+
this.activePointers.clear();
|
|
448
|
+
this.dragging = false;
|
|
449
|
+
this.panning = false;
|
|
450
|
+
this.pinchDist = 0;
|
|
451
|
+
}
|
|
434
452
|
/**
|
|
435
453
|
* Fit a flattering 3/4 view to the bounds sphere. With `intro`, the camera
|
|
436
454
|
* STARTS nearly top-down (matching the 2D map's orientation) and further out,
|
|
@@ -487,6 +505,26 @@ var OrbitCamera = class {
|
|
|
487
505
|
this.polT = 55 * DEG;
|
|
488
506
|
this.distT = fit * FRAME_MARGIN;
|
|
489
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Tighten the depth range around the actual venue bounds for the current pose.
|
|
510
|
+
*
|
|
511
|
+
* A fixed 0.1..5000 m frustum gives an ordinary arena centimetre-scale depth
|
|
512
|
+
* precision at overview distance—coarser than the separation between a row
|
|
513
|
+
* tread and its structural cap. The resulting z quantisation is the black
|
|
514
|
+
* radial comb previously seen while zooming. A padded bounds sphere contains
|
|
515
|
+
* every venue solid while preserving useful precision outside the bowl; when
|
|
516
|
+
* the camera enters that sphere, the near plane safely returns to 5 cm.
|
|
517
|
+
*/
|
|
518
|
+
updateClipping(bounds) {
|
|
519
|
+
const dx = this.camera.position.x - bounds.center[0];
|
|
520
|
+
const dy = this.camera.position.y - bounds.center[1];
|
|
521
|
+
const dz = this.camera.position.z - bounds.center[2];
|
|
522
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
523
|
+
const paddedRadius = Math.max(1, bounds.radius) * 1.18;
|
|
524
|
+
const near = Math.max(0.05, distance - paddedRadius);
|
|
525
|
+
const far = Math.max(near + 10, distance + paddedRadius);
|
|
526
|
+
this.camera.perspective({ near, far, aspect: this.camera.aspect });
|
|
527
|
+
}
|
|
490
528
|
/** Damp toward targets; returns true while still moving. */
|
|
491
529
|
update() {
|
|
492
530
|
const da = this.azT - this.azimuth;
|
|
@@ -605,8 +643,12 @@ var RenderLoop = class {
|
|
|
605
643
|
// src/view3d/lod.ts
|
|
606
644
|
var SEAT_MIN_PIXELS_NEAR = 2.5;
|
|
607
645
|
var SEAT_MIN_PIXELS_FAR = 1.15;
|
|
608
|
-
var CHAIR_FULL_M =
|
|
609
|
-
var CHAIR_NONE_M =
|
|
646
|
+
var CHAIR_FULL_M = 6;
|
|
647
|
+
var CHAIR_NONE_M = 10;
|
|
648
|
+
var OCCUPANT_FULL_M = 6;
|
|
649
|
+
var OCCUPANT_NONE_M = 9.5;
|
|
650
|
+
var OCCUPANT_NEAR_NONE_M = 0.9;
|
|
651
|
+
var OCCUPANT_NEAR_FULL_M = 2.2;
|
|
610
652
|
var CHAIR_GATHER_M = 30;
|
|
611
653
|
var CHAIR_REBUILD_M = 4;
|
|
612
654
|
var CHAIR_MAX_INSTANCES = 8192;
|
|
@@ -777,13 +819,19 @@ var MeshBuilder = class {
|
|
|
777
819
|
this.nor = new F32Buffer();
|
|
778
820
|
this.col = new F32Buffer();
|
|
779
821
|
this.flr = new F32Buffer();
|
|
822
|
+
this.mat = new F32Buffer();
|
|
780
823
|
/** Floor index stamped onto every triangle emitted from now on. */
|
|
781
824
|
this.currentFloor = 0;
|
|
825
|
+
this.currentMaterial = 0;
|
|
782
826
|
}
|
|
783
827
|
/** Stamp subsequent triangles as belonging to `index`. */
|
|
784
828
|
setFloor(index) {
|
|
785
829
|
this.currentFloor = index;
|
|
786
830
|
}
|
|
831
|
+
/** Stamp subsequent triangles with a small renderer material class. */
|
|
832
|
+
setMaterial(index) {
|
|
833
|
+
this.currentMaterial = index;
|
|
834
|
+
}
|
|
787
835
|
/** One triangle with a shared (flat) normal and per-vertex colours. */
|
|
788
836
|
tri(p0, p1, p2, n, c0, c1 = c0, c2 = c0) {
|
|
789
837
|
if (isDegenerate(p0, p1, p2)) return;
|
|
@@ -799,10 +847,13 @@ var MeshBuilder = class {
|
|
|
799
847
|
this.flr.push1(this.currentFloor);
|
|
800
848
|
this.flr.push1(this.currentFloor);
|
|
801
849
|
this.flr.push1(this.currentFloor);
|
|
850
|
+
this.mat.push1(this.currentMaterial);
|
|
851
|
+
this.mat.push1(this.currentMaterial);
|
|
852
|
+
this.mat.push1(this.currentMaterial);
|
|
802
853
|
}
|
|
803
854
|
/** One triangle with independent per-vertex normals (smooth shading). */
|
|
804
855
|
triN(p0, p1, p2, n0, n1, n2, c0, c1 = c0, c2 = c0) {
|
|
805
|
-
if (
|
|
856
|
+
if (![...p0, ...p1, ...p2].every(Number.isFinite)) return;
|
|
806
857
|
this.pos.push3(p0[0], p0[1], p0[2]);
|
|
807
858
|
this.pos.push3(p1[0], p1[1], p1[2]);
|
|
808
859
|
this.pos.push3(p2[0], p2[1], p2[2]);
|
|
@@ -815,6 +866,9 @@ var MeshBuilder = class {
|
|
|
815
866
|
this.flr.push1(this.currentFloor);
|
|
816
867
|
this.flr.push1(this.currentFloor);
|
|
817
868
|
this.flr.push1(this.currentFloor);
|
|
869
|
+
this.mat.push1(this.currentMaterial);
|
|
870
|
+
this.mat.push1(this.currentMaterial);
|
|
871
|
+
this.mat.push1(this.currentMaterial);
|
|
818
872
|
}
|
|
819
873
|
get vertexCount() {
|
|
820
874
|
return this.pos.length / 3;
|
|
@@ -825,6 +879,7 @@ var MeshBuilder = class {
|
|
|
825
879
|
normal: this.nor.toArray(),
|
|
826
880
|
color: this.col.toArray(),
|
|
827
881
|
floor: this.flr.toArray(),
|
|
882
|
+
material: this.mat.toArray(),
|
|
828
883
|
count: this.pos.length / 3
|
|
829
884
|
};
|
|
830
885
|
}
|
|
@@ -1067,15 +1122,17 @@ function mergeMeshData(parts) {
|
|
|
1067
1122
|
const normal = new Float32Array(total * 3);
|
|
1068
1123
|
const color = new Float32Array(total * 3);
|
|
1069
1124
|
const floor = new Float32Array(total);
|
|
1125
|
+
const material = new Float32Array(total);
|
|
1070
1126
|
let off = 0;
|
|
1071
1127
|
for (const p of parts) {
|
|
1072
1128
|
position.set(p.position, off * 3);
|
|
1073
1129
|
normal.set(p.normal, off * 3);
|
|
1074
1130
|
color.set(p.color, off * 3);
|
|
1075
1131
|
floor.set(p.floor, off);
|
|
1132
|
+
material.set(p.material, off);
|
|
1076
1133
|
off += p.count;
|
|
1077
1134
|
}
|
|
1078
|
-
return { position, normal, color, floor, count: total };
|
|
1135
|
+
return { position, normal, color, floor, material, count: total };
|
|
1079
1136
|
}
|
|
1080
1137
|
function outsetRing(ring, d, miterLimit = 2.5) {
|
|
1081
1138
|
if (d <= 0 || ring.length < 3) return ring;
|
|
@@ -1169,7 +1226,7 @@ var BACK_RAKE_SLOPE = 0.21;
|
|
|
1169
1226
|
var PAD_BACK_GAP_M = 0.05;
|
|
1170
1227
|
var PAD_TOP_M = 0.45;
|
|
1171
1228
|
var BACK_BASE_M = PAD_TOP_M + PAD_BACK_GAP_M;
|
|
1172
|
-
var
|
|
1229
|
+
var CHAIR_BOXES = [
|
|
1173
1230
|
// Pedestal — a plain column under the pad. Without it the pad floats 0.36 m
|
|
1174
1231
|
// over the deck and the row reads as hovering trays.
|
|
1175
1232
|
{ part: CHAIR_PART.pedestal, min: [-0.3, 0, -0.3], max: [0.3, 0.36, 0.3] },
|
|
@@ -1184,39 +1241,22 @@ var BOXES = [
|
|
|
1184
1241
|
part: CHAIR_PART.back,
|
|
1185
1242
|
min: [-1, BACK_BASE_M, -1],
|
|
1186
1243
|
max: [1, 0.92, -0.72]
|
|
1187
|
-
},
|
|
1188
|
-
// --- the occupant ---------------------------------------------------------
|
|
1189
|
-
//
|
|
1190
|
-
// A hall with every seat empty reads as an architectural model, not a venue.
|
|
1191
|
-
// The 2048-px panorama this replaced drew a crowd; losing it was the price of
|
|
1192
|
-
// sharpness, and this is how it is bought back — in geometry, where there is
|
|
1193
|
-
// no resolution ceiling.
|
|
1194
|
-
//
|
|
1195
|
-
// Deliberately two blocks and no limbs. At the range these are visible a
|
|
1196
|
-
// person is a torso and a head, and every extra part multiplies by the number
|
|
1197
|
-
// of occupied seats in view. The silhouette is what carries it, exactly as it
|
|
1198
|
-
// does in the generated panorama's head-and-shoulder figures.
|
|
1199
|
-
//
|
|
1200
|
-
// Sized as a seated adult against the 0.92 m chair back: hips at the pad top,
|
|
1201
|
-
// shoulders just above the back panel, head clear of it. Torso is narrower
|
|
1202
|
-
// than the chair so neighbours never interpenetrate at any pitch, and it sits
|
|
1203
|
-
// forward of the back panel rather than inside it.
|
|
1204
|
-
{
|
|
1205
|
-
part: CHAIR_PART.body,
|
|
1206
|
-
min: [-0.66, PAD_TOP_M, -0.58],
|
|
1207
|
-
max: [0.66, 1, 0.26]
|
|
1208
|
-
},
|
|
1209
|
-
// The head is deliberately SMALL. Sized by eye against the chair it came out
|
|
1210
|
-
// near-cubic and read as Lego; a real head is about 0.16 m across and 0.22 m
|
|
1211
|
-
// tall, which against a 0.24 m chair half-width is roughly a third of the
|
|
1212
|
-
// chair's width and clearly taller than it is wide. Getting this ratio wrong
|
|
1213
|
-
// is what makes a crowd look like toys rather than people.
|
|
1214
|
-
{
|
|
1215
|
-
part: CHAIR_PART.head,
|
|
1216
|
-
min: [-0.34, 1.03, -0.4],
|
|
1217
|
-
max: [0.34, 1.27, 0.02]
|
|
1218
1244
|
}
|
|
1219
1245
|
];
|
|
1246
|
+
var OCCUPANT_BOXES = [
|
|
1247
|
+
// Thighs project forward from the pad. A single lap slab brought back the
|
|
1248
|
+
// same toy-block problem as the old torso, so each leg keeps its own outline.
|
|
1249
|
+
{ part: CHAIR_PART.body, min: [-0.52, 0.39, -0.08], max: [-0.08, 0.55, 0.82] },
|
|
1250
|
+
{ part: CHAIR_PART.body, min: [0.08, 0.39, -0.08], max: [0.52, 0.55, 0.82] },
|
|
1251
|
+
// Lower legs drop from the knees, making the pose unmistakably seated from
|
|
1252
|
+
// the side and from the elevated arena camera.
|
|
1253
|
+
{ part: CHAIR_PART.body, min: [-0.47, 0.05, 0.58], max: [-0.13, 0.43, 0.82] },
|
|
1254
|
+
{ part: CHAIR_PART.body, min: [0.13, 0.05, 0.58], max: [0.47, 0.43, 0.82] },
|
|
1255
|
+
// A short neck keeps the head connected to the shoulders from behind. Most
|
|
1256
|
+
// of it sits inside the torso/head overlap: a long exposed cuboid made the
|
|
1257
|
+
// close-range figure read as a mannequin on a post.
|
|
1258
|
+
{ part: CHAIR_PART.head, min: [-0.16, 1.04, -0.33], max: [0.16, 1.1, -0.13] }
|
|
1259
|
+
];
|
|
1220
1260
|
var FACES = [
|
|
1221
1261
|
// +X
|
|
1222
1262
|
{ n: [1, 0, 0], c: [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]] },
|
|
@@ -1232,63 +1272,111 @@ var FACES = [
|
|
|
1232
1272
|
{ n: [0, 0, -1], c: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]] }
|
|
1233
1273
|
];
|
|
1234
1274
|
function buildChairMesh() {
|
|
1235
|
-
const
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1275
|
+
const positions = [];
|
|
1276
|
+
const normals = [];
|
|
1277
|
+
const parts = [];
|
|
1278
|
+
const indices = [];
|
|
1279
|
+
const addQuad = (corners, outward, partId) => {
|
|
1280
|
+
const base = parts.length;
|
|
1281
|
+
const [a, b, c] = corners;
|
|
1282
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
1283
|
+
const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
1284
|
+
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
|
1285
|
+
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
|
1286
|
+
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
|
1287
|
+
const length = Math.hypot(nx, ny, nz);
|
|
1288
|
+
if (length > 1e-9) {
|
|
1289
|
+
nx /= length;
|
|
1290
|
+
ny /= length;
|
|
1291
|
+
nz /= length;
|
|
1292
|
+
} else {
|
|
1293
|
+
[nx, ny, nz] = outward;
|
|
1294
|
+
}
|
|
1295
|
+
if (nx * outward[0] + ny * outward[1] + nz * outward[2] < 0) {
|
|
1296
|
+
nx = -nx;
|
|
1297
|
+
ny = -ny;
|
|
1298
|
+
nz = -nz;
|
|
1299
|
+
}
|
|
1300
|
+
for (const corner of corners) {
|
|
1301
|
+
positions.push(...corner);
|
|
1302
|
+
normals.push(nx, ny, nz);
|
|
1303
|
+
parts.push(partId);
|
|
1304
|
+
}
|
|
1305
|
+
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
1306
|
+
};
|
|
1307
|
+
const addBox = (box) => {
|
|
1244
1308
|
for (const face of FACES) {
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
for (let a = 0; a < 3; a++) {
|
|
1250
|
-
corner3[ci * 3 + a] = corner[a] ? box.max[a] : box.min[a];
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
const ax = corner3[3] - corner3[0], ay = corner3[4] - corner3[1], az = corner3[5] - corner3[2];
|
|
1254
|
-
const bx = corner3[6] - corner3[0], by = corner3[7] - corner3[1], bz = corner3[8] - corner3[2];
|
|
1255
|
-
let nx = ay * bz - az * by;
|
|
1256
|
-
let ny = az * bx - ax * bz;
|
|
1257
|
-
let nz = ax * by - ay * bx;
|
|
1258
|
-
const nl = Math.hypot(nx, ny, nz);
|
|
1259
|
-
if (nl > 1e-9) {
|
|
1260
|
-
nx /= nl;
|
|
1261
|
-
ny /= nl;
|
|
1262
|
-
nz /= nl;
|
|
1263
|
-
} else {
|
|
1264
|
-
nx = face.n[0];
|
|
1265
|
-
ny = face.n[1];
|
|
1266
|
-
nz = face.n[2];
|
|
1267
|
-
}
|
|
1268
|
-
if (nx * face.n[0] + ny * face.n[1] + nz * face.n[2] < 0) {
|
|
1269
|
-
nx = -nx;
|
|
1270
|
-
ny = -ny;
|
|
1271
|
-
nz = -nz;
|
|
1272
|
-
}
|
|
1273
|
-
for (let ci = 0; ci < 4; ci++) {
|
|
1274
|
-
position[v * 3] = corner3[ci * 3];
|
|
1275
|
-
position[v * 3 + 1] = corner3[ci * 3 + 1];
|
|
1276
|
-
position[v * 3 + 2] = corner3[ci * 3 + 2];
|
|
1277
|
-
normal[v * 3] = nx;
|
|
1278
|
-
normal[v * 3 + 1] = ny;
|
|
1279
|
-
normal[v * 3 + 2] = nz;
|
|
1280
|
-
part[v] = box.part;
|
|
1281
|
-
v++;
|
|
1282
|
-
}
|
|
1283
|
-
index[t++] = base;
|
|
1284
|
-
index[t++] = base + 1;
|
|
1285
|
-
index[t++] = base + 2;
|
|
1286
|
-
index[t++] = base;
|
|
1287
|
-
index[t++] = base + 2;
|
|
1288
|
-
index[t++] = base + 3;
|
|
1309
|
+
const corners = face.c.map((corner) => corner.map(
|
|
1310
|
+
(side, axis) => side ? box.max[axis] : box.min[axis]
|
|
1311
|
+
));
|
|
1312
|
+
addQuad(corners, face.n, box.part);
|
|
1289
1313
|
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1314
|
+
};
|
|
1315
|
+
for (const box of CHAIR_BOXES) addBox(box);
|
|
1316
|
+
for (const box of OCCUPANT_BOXES) addBox(box);
|
|
1317
|
+
const waistY = PAD_TOP_M;
|
|
1318
|
+
const shoulderY = 1.08;
|
|
1319
|
+
const wb = 0.47, wt = 0.69;
|
|
1320
|
+
const backBottom = -0.55, frontBottom = 0.08;
|
|
1321
|
+
const backTop = -0.48, frontTop = 0.01;
|
|
1322
|
+
const blb = [-wb, waistY, backBottom], brb = [wb, waistY, backBottom];
|
|
1323
|
+
const blf = [-wb, waistY, frontBottom], brf = [wb, waistY, frontBottom];
|
|
1324
|
+
const tlb = [-wt, shoulderY, backTop], trb = [wt, shoulderY, backTop];
|
|
1325
|
+
const tlf = [-wt, shoulderY, frontTop], trf = [wt, shoulderY, frontTop];
|
|
1326
|
+
addQuad([brb, trb, trf, brf], [1, 0, 0], CHAIR_PART.body);
|
|
1327
|
+
addQuad([blf, tlf, tlb, blb], [-1, 0, 0], CHAIR_PART.body);
|
|
1328
|
+
addQuad([tlf, trf, trb, tlb], [0, 1, 0], CHAIR_PART.body);
|
|
1329
|
+
addQuad([blb, brb, brf, blf], [0, -1, 0], CHAIR_PART.body);
|
|
1330
|
+
addQuad([brf, trf, tlf, blf], [0, 0, 1], CHAIR_PART.body);
|
|
1331
|
+
addQuad([blb, tlb, trb, brb], [0, 0, -1], CHAIR_PART.body);
|
|
1332
|
+
const lonSegments = 10;
|
|
1333
|
+
const latSegments = 8;
|
|
1334
|
+
const centre = [0, 1.205, -0.23];
|
|
1335
|
+
const radii = [0.42, 0.115, 0.38];
|
|
1336
|
+
const headBase = parts.length;
|
|
1337
|
+
for (let lat = 0; lat <= latSegments; lat++) {
|
|
1338
|
+
const theta = Math.PI * lat / latSegments;
|
|
1339
|
+
const sy = Math.cos(theta);
|
|
1340
|
+
const ring = Math.sin(theta);
|
|
1341
|
+
for (let lon = 0; lon <= lonSegments; lon++) {
|
|
1342
|
+
const phi = Math.PI * 2 * lon / lonSegments;
|
|
1343
|
+
const dx = ring * Math.cos(phi);
|
|
1344
|
+
const dz = ring * Math.sin(phi);
|
|
1345
|
+
positions.push(
|
|
1346
|
+
centre[0] + radii[0] * dx,
|
|
1347
|
+
centre[1] + radii[1] * sy,
|
|
1348
|
+
centre[2] + radii[2] * dz
|
|
1349
|
+
);
|
|
1350
|
+
let nx = dx / radii[0];
|
|
1351
|
+
let ny = sy / radii[1];
|
|
1352
|
+
let nz = dz / radii[2];
|
|
1353
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
1354
|
+
nx /= length;
|
|
1355
|
+
ny /= length;
|
|
1356
|
+
nz /= length;
|
|
1357
|
+
normals.push(nx, ny, nz);
|
|
1358
|
+
parts.push(CHAIR_PART.head);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
for (let lat = 0; lat < latSegments; lat++) {
|
|
1362
|
+
for (let lon = 0; lon < lonSegments; lon++) {
|
|
1363
|
+
const a = headBase + lat * (lonSegments + 1) + lon;
|
|
1364
|
+
const b = a + lonSegments + 1;
|
|
1365
|
+
indices.push(a, b, a + 1, a + 1, b, b + 1);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const position = new Float32Array(positions);
|
|
1369
|
+
const normal = new Float32Array(normals);
|
|
1370
|
+
const part = new Float32Array(parts);
|
|
1371
|
+
const index = new Uint16Array(indices);
|
|
1372
|
+
return {
|
|
1373
|
+
position,
|
|
1374
|
+
normal,
|
|
1375
|
+
part,
|
|
1376
|
+
index,
|
|
1377
|
+
vertexCount: parts.length,
|
|
1378
|
+
indexCount: indices.length
|
|
1379
|
+
};
|
|
1292
1380
|
}
|
|
1293
1381
|
function computeSeatYaw(iPosition, count, rowIdAt, focal) {
|
|
1294
1382
|
const yaw = new Float32Array(count);
|
|
@@ -1592,7 +1680,7 @@ function focusScore(screen, worldDistance, width, height) {
|
|
|
1592
1680
|
return worldDistance * (1 + 1.5 * off);
|
|
1593
1681
|
}
|
|
1594
1682
|
function pickDenseLabels(items, separationX, separationY, budget) {
|
|
1595
|
-
const ordered = [...items].sort((a, b) => a.focus - b.focus);
|
|
1683
|
+
const ordered = [...items].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.focus - b.focus);
|
|
1596
1684
|
const kept = [];
|
|
1597
1685
|
for (const item of ordered) {
|
|
1598
1686
|
if (kept.length >= budget) break;
|
|
@@ -2204,19 +2292,31 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2204
2292
|
const UP = [0, 1, 0];
|
|
2205
2293
|
const clipRing = clip && clip.length >= 3 ? [[...clip.map((p) => [p.x, p.y]), [clip[0].x, clip[0].y]]] : null;
|
|
2206
2294
|
const clipTest = clip && clip.length >= 3 ? new ClipTest([[...clip]]) : null;
|
|
2295
|
+
const emitRiser = (draw) => {
|
|
2296
|
+
builder.setMaterial(2);
|
|
2297
|
+
draw();
|
|
2298
|
+
builder.setMaterial(0);
|
|
2299
|
+
};
|
|
2300
|
+
const emitTread = (draw) => {
|
|
2301
|
+
builder.setMaterial(3);
|
|
2302
|
+
draw();
|
|
2303
|
+
builder.setMaterial(0);
|
|
2304
|
+
};
|
|
2207
2305
|
for (let i = 0; i < rows.length; i++) {
|
|
2208
2306
|
const row = rows[i];
|
|
2209
2307
|
if (row.patch && row.patch.length >= 3) {
|
|
2210
2308
|
const patch = [...row.patch];
|
|
2211
2309
|
const belowY2 = nbrs[i].belowY ?? landingY;
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2310
|
+
emitTread(() => {
|
|
2311
|
+
if (clipRing && clipTest) emitClippedPoly(builder, clipRing, clipTest, patch, row.y, colors.tread);
|
|
2312
|
+
else {
|
|
2313
|
+
const a = patch[0];
|
|
2314
|
+
for (let k = 1; k + 1 < patch.length; k++) {
|
|
2315
|
+
const b = patch[k], c = patch[k + 1];
|
|
2316
|
+
builder.tri([a.x * M, row.y, a.y * M], [b.x * M, row.y, b.y * M], [c.x * M, row.y, c.y * M], UP, colors.tread);
|
|
2317
|
+
}
|
|
2218
2318
|
}
|
|
2219
|
-
}
|
|
2319
|
+
});
|
|
2220
2320
|
if (row.y > belowY2 + MIN_RISER_M) {
|
|
2221
2321
|
let cx = 0, cy = 0;
|
|
2222
2322
|
for (const p of patch) {
|
|
@@ -2225,24 +2325,26 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2225
2325
|
}
|
|
2226
2326
|
cx /= patch.length;
|
|
2227
2327
|
cy /= patch.length;
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
nx
|
|
2236
|
-
|
|
2328
|
+
emitRiser(() => {
|
|
2329
|
+
for (let k = 0; k < patch.length; k++) {
|
|
2330
|
+
const p = patch[k], q = patch[(k + 1) % patch.length];
|
|
2331
|
+
const dx = q.x - p.x, dy = q.y - p.y;
|
|
2332
|
+
const len = Math.hypot(dx, dy);
|
|
2333
|
+
if (len < 1e-6) continue;
|
|
2334
|
+
let nx = dy / len, ny = -dx / len;
|
|
2335
|
+
if ((p.x - cx) * nx + (p.y - cy) * ny < 0) {
|
|
2336
|
+
nx = -nx;
|
|
2337
|
+
ny = -ny;
|
|
2338
|
+
}
|
|
2339
|
+
const rn = [nx, 0, ny];
|
|
2340
|
+
const pt = [p.x * M, row.y, p.y * M];
|
|
2341
|
+
const qt = [q.x * M, row.y, q.y * M];
|
|
2342
|
+
const pb = [p.x * M, belowY2, p.y * M];
|
|
2343
|
+
const qb = [q.x * M, belowY2, q.y * M];
|
|
2344
|
+
builder.tri(pt, qt, qb, rn, colors.riser);
|
|
2345
|
+
builder.tri(pt, qb, pb, rn, colors.riser);
|
|
2237
2346
|
}
|
|
2238
|
-
|
|
2239
|
-
const pt = [p.x * M, row.y, p.y * M];
|
|
2240
|
-
const qt = [q.x * M, row.y, q.y * M];
|
|
2241
|
-
const pb = [p.x * M, belowY2, p.y * M];
|
|
2242
|
-
const qb = [q.x * M, belowY2, q.y * M];
|
|
2243
|
-
builder.tri(pt, qt, qb, rn, colors.riser);
|
|
2244
|
-
builder.tri(pt, qb, pb, rn, colors.riser);
|
|
2245
|
-
}
|
|
2347
|
+
});
|
|
2246
2348
|
}
|
|
2247
2349
|
continue;
|
|
2248
2350
|
}
|
|
@@ -2259,23 +2361,27 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2259
2361
|
const qF = [(q.x - nq[0] * front) * M, row.y, (q.y - nq[1] * front) * M];
|
|
2260
2362
|
const pB = [(p.x + np[0] * back) * M, row.y, (p.y + np[1] * back) * M];
|
|
2261
2363
|
const qB = [(q.x + nq[0] * back) * M, row.y, (q.y + nq[1] * back) * M];
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2364
|
+
emitTread(() => {
|
|
2365
|
+
if (clipRing && clipTest) {
|
|
2366
|
+
emitClippedPoly(builder, clipRing, clipTest, [
|
|
2367
|
+
{ x: p.x - np[0] * front, y: p.y - np[1] * front },
|
|
2368
|
+
{ x: q.x - nq[0] * front, y: q.y - nq[1] * front },
|
|
2369
|
+
{ x: q.x + nq[0] * back, y: q.y + nq[1] * back },
|
|
2370
|
+
{ x: p.x + np[0] * back, y: p.y + np[1] * back }
|
|
2371
|
+
], row.y, colors.tread);
|
|
2372
|
+
} else {
|
|
2373
|
+
builder.tri(pF, qF, qB, UP, colors.tread);
|
|
2374
|
+
builder.tri(pF, qB, pB, UP, colors.tread);
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2273
2377
|
if (row.y > belowY + MIN_RISER_M) {
|
|
2274
2378
|
const pFd = [pF[0], belowY, pF[2]];
|
|
2275
2379
|
const qFd = [qF[0], belowY, qF[2]];
|
|
2276
2380
|
const rn = [-np[0], 0, -np[1]];
|
|
2277
|
-
|
|
2278
|
-
|
|
2381
|
+
emitRiser(() => {
|
|
2382
|
+
builder.tri(pF, qF, qFd, rn, colors.riser);
|
|
2383
|
+
builder.tri(pF, qFd, pFd, rn, colors.riser);
|
|
2384
|
+
});
|
|
2279
2385
|
}
|
|
2280
2386
|
}
|
|
2281
2387
|
}
|
|
@@ -2620,18 +2726,21 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2620
2726
|
const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal, nbrs), footprint);
|
|
2621
2727
|
const capUp = () => [0, 1, 0];
|
|
2622
2728
|
for (const b of blocks) {
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2729
|
+
const baseCapY = Math.max(bottomY, b.topY - 0.02);
|
|
2730
|
+
for (const ring of claimed.subtract(b.outline, baseCapY)) {
|
|
2731
|
+
extrudePrism(
|
|
2732
|
+
builder,
|
|
2733
|
+
ring,
|
|
2734
|
+
b.holes,
|
|
2735
|
+
() => baseCapY,
|
|
2736
|
+
bottomY,
|
|
2737
|
+
colTop,
|
|
2738
|
+
S.tierWall,
|
|
2739
|
+
AO,
|
|
2740
|
+
void 0,
|
|
2741
|
+
capUp
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2635
2744
|
}
|
|
2636
2745
|
if (!blocks.length) {
|
|
2637
2746
|
extrudePrism(
|
|
@@ -2645,12 +2754,23 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
2645
2754
|
AO
|
|
2646
2755
|
);
|
|
2647
2756
|
}
|
|
2648
|
-
|
|
2757
|
+
const bandColors = {
|
|
2649
2758
|
tread: [colTop[0] * AO.top, colTop[1] * AO.top, colTop[2] * AO.top],
|
|
2650
2759
|
// Risers read as the structure they are, a shade below their tread, which
|
|
2651
2760
|
// is what makes the stepping legible from a low angle.
|
|
2652
2761
|
riser: [colTop[0] * 0.72, colTop[1] * 0.72, colTop[2] * 0.72]
|
|
2653
|
-
}
|
|
2762
|
+
};
|
|
2763
|
+
for (const allowed of footprint.length ? footprint : [outline]) {
|
|
2764
|
+
emitDeckBands(
|
|
2765
|
+
builder,
|
|
2766
|
+
surface.rowLevels,
|
|
2767
|
+
unit.focal,
|
|
2768
|
+
surface.landingY,
|
|
2769
|
+
bandColors,
|
|
2770
|
+
allowed,
|
|
2771
|
+
nbrs
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2654
2774
|
return;
|
|
2655
2775
|
}
|
|
2656
2776
|
const maxErr = surface.flat ? Infinity : CAP_MAX_ERROR_M;
|
|
@@ -2904,8 +3024,10 @@ function buildShape(builder, shape, base, topOffsetM, S, focal, stages) {
|
|
|
2904
3024
|
halfZ: Math.max((maxZ - minZ) / 2 * M, 0.5)
|
|
2905
3025
|
});
|
|
2906
3026
|
}
|
|
2907
|
-
const
|
|
2908
|
-
const
|
|
3027
|
+
const authoredFill = hexToRgb(shape.fill);
|
|
3028
|
+
const colTop = isStage && authoredFill ? mix(S.stageTop, scaleRgb(desaturate(authoredFill, 0.18), 0.9), 0.9) : tintTop(authoredFill, isStage ? S.stageTop : S.decorTop);
|
|
3029
|
+
const screenFill = shape.role === "screen" ? authoredFill : null;
|
|
3030
|
+
const colWall = isStage ? authoredFill ? mix(S.stageWall, scaleRgb(authoredFill, 0.58), 0.82) : S.stageWall : shape.role === "screen" ? screenFill ? mix(scaleRgb(screenFill, 1.42), [0.24, 0.46, 0.82], 0.08) : [0.24, 0.46, 0.82] : tintTop(hexToRgb(shape.fill), S.decorWall);
|
|
2909
3031
|
if (architecture.kind === "portal" && poly.length === 4) {
|
|
2910
3032
|
const quad = orientQuadLong(poly);
|
|
2911
3033
|
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.16), height, base, colTop, colWall);
|
|
@@ -3045,16 +3167,19 @@ function buildSceneModel(input) {
|
|
|
3045
3167
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
3046
3168
|
for (const o of unit.objects) {
|
|
3047
3169
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
3048
|
-
else if (o.type === "shape")
|
|
3049
|
-
builder
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3170
|
+
else if (o.type === "shape") {
|
|
3171
|
+
builder.setMaterial(o.role === "screen" ? 1 : 0);
|
|
3172
|
+
buildShape(
|
|
3173
|
+
builder,
|
|
3174
|
+
o,
|
|
3175
|
+
unit.baseHeightM,
|
|
3176
|
+
claimShapeTopOffset(o, claimedShapeLayers),
|
|
3177
|
+
S,
|
|
3178
|
+
unit.focal,
|
|
3179
|
+
stageBounds
|
|
3180
|
+
);
|
|
3181
|
+
builder.setMaterial(0);
|
|
3182
|
+
} else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3058
3183
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3059
3184
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3060
3185
|
else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
|
|
@@ -3360,10 +3485,49 @@ function buildSceneModel(input) {
|
|
|
3360
3485
|
const cx = (fp.minX + fp.maxX) / 2 * M;
|
|
3361
3486
|
const cz = (fp.minY + fp.maxY) / 2 * M;
|
|
3362
3487
|
const radius = 0.5 * Math.hypot((fp.maxX - fp.minX) * M, (fp.maxY - fp.minY) * M) || 10;
|
|
3488
|
+
let renderMinX = Infinity, renderMinY = Infinity, renderMinZ = Infinity;
|
|
3489
|
+
let renderMaxX = -Infinity, renderMaxY = -Infinity, renderMaxZ = -Infinity;
|
|
3490
|
+
const includeRenderPoint = (x, y, z) => {
|
|
3491
|
+
renderMinX = Math.min(renderMinX, x);
|
|
3492
|
+
renderMaxX = Math.max(renderMaxX, x);
|
|
3493
|
+
renderMinY = Math.min(renderMinY, y);
|
|
3494
|
+
renderMaxY = Math.max(renderMaxY, y);
|
|
3495
|
+
renderMinZ = Math.min(renderMinZ, z);
|
|
3496
|
+
renderMaxZ = Math.max(renderMaxZ, z);
|
|
3497
|
+
};
|
|
3498
|
+
for (let i = 0; i < solids.position.length; i += 3) {
|
|
3499
|
+
includeRenderPoint(solids.position[i], solids.position[i + 1], solids.position[i + 2]);
|
|
3500
|
+
}
|
|
3501
|
+
for (let i = 0; i < seatData.count; i++) {
|
|
3502
|
+
const o = i * 3;
|
|
3503
|
+
const x = seatData.iPosition[o], y = seatData.iPosition[o + 1], z = seatData.iPosition[o + 2];
|
|
3504
|
+
const r = Math.max(0.35, seatData.iMaxRadius[i] ?? 0.35);
|
|
3505
|
+
includeRenderPoint(x - r, y, z - r);
|
|
3506
|
+
includeRenderPoint(x + r, y + 1.6, z + r);
|
|
3507
|
+
}
|
|
3508
|
+
if (!Number.isFinite(renderMinX)) {
|
|
3509
|
+
renderMinX = cx - radius;
|
|
3510
|
+
renderMaxX = cx + radius;
|
|
3511
|
+
renderMinY = 0;
|
|
3512
|
+
renderMaxY = radius * 0.16;
|
|
3513
|
+
renderMinZ = cz - radius;
|
|
3514
|
+
renderMaxZ = cz + radius;
|
|
3515
|
+
}
|
|
3516
|
+
const renderCenter = [
|
|
3517
|
+
(renderMinX + renderMaxX) / 2,
|
|
3518
|
+
(renderMinY + renderMaxY) / 2,
|
|
3519
|
+
(renderMinZ + renderMaxZ) / 2
|
|
3520
|
+
];
|
|
3521
|
+
const renderRadius = Math.max(1, 0.5 * Math.hypot(
|
|
3522
|
+
renderMaxX - renderMinX,
|
|
3523
|
+
renderMaxY - renderMinY,
|
|
3524
|
+
renderMaxZ - renderMinZ
|
|
3525
|
+
));
|
|
3363
3526
|
return {
|
|
3364
3527
|
solids,
|
|
3365
3528
|
seats: seatData,
|
|
3366
3529
|
bounds: { center: [cx, radius * 0.08, cz], radius, groundY: 0 },
|
|
3530
|
+
renderBounds: { center: renderCenter, radius: renderRadius },
|
|
3367
3531
|
stateColorLUT: themeSeatColorLUT(theme, SEAT_STATES),
|
|
3368
3532
|
theme,
|
|
3369
3533
|
seatCount: seats.length,
|
|
@@ -3407,6 +3571,7 @@ var LabelOverlay = class {
|
|
|
3407
3571
|
this.nodes = /* @__PURE__ */ new Map();
|
|
3408
3572
|
this.labels = [];
|
|
3409
3573
|
this.forcedDense = false;
|
|
3574
|
+
this.selectedSeatLabelIds = /* @__PURE__ */ new Set();
|
|
3410
3575
|
this.opts = opts;
|
|
3411
3576
|
this.root = document.createElement("div");
|
|
3412
3577
|
this.root.setAttribute("data-view3d-labels", "");
|
|
@@ -3446,6 +3611,12 @@ var LabelOverlay = class {
|
|
|
3446
3611
|
setForcedDense(enabled) {
|
|
3447
3612
|
this.forcedDense = enabled;
|
|
3448
3613
|
}
|
|
3614
|
+
/** Keep the authoritative selected identities pinned above nearby dense
|
|
3615
|
+
* labels. The seat mesh and its DOM label must never describe different
|
|
3616
|
+
* rows merely because collision suppression preferred the neighbour. */
|
|
3617
|
+
setSelectedSeatIds(ids) {
|
|
3618
|
+
this.selectedSeatLabelIds = new Set(Array.from(ids, (id) => `seat:${id}`));
|
|
3619
|
+
}
|
|
3449
3620
|
/**
|
|
3450
3621
|
* Reposition every label for the current camera.
|
|
3451
3622
|
*
|
|
@@ -3468,7 +3639,12 @@ var LabelOverlay = class {
|
|
|
3468
3639
|
label.anchor[1] - cameraWorld[1],
|
|
3469
3640
|
label.anchor[2] - cameraWorld[2]
|
|
3470
3641
|
) : 1;
|
|
3471
|
-
candidates.push({
|
|
3642
|
+
candidates.push({
|
|
3643
|
+
label,
|
|
3644
|
+
screen,
|
|
3645
|
+
focus: focusScore(screen, world, width, height),
|
|
3646
|
+
...this.selectedSeatLabelIds.has(label.id) ? { priority: 1 } : {}
|
|
3647
|
+
});
|
|
3472
3648
|
}
|
|
3473
3649
|
const structure = candidates.filter((c) => !DENSE_KINDS.has(c.label.kind));
|
|
3474
3650
|
const kept = [
|
|
@@ -3496,26 +3672,35 @@ var LabelOverlay = class {
|
|
|
3496
3672
|
}
|
|
3497
3673
|
nodeFor(label) {
|
|
3498
3674
|
let node = this.nodes.get(label.id);
|
|
3499
|
-
if (node)
|
|
3500
|
-
|
|
3675
|
+
if (!node) {
|
|
3676
|
+
node = document.createElement("div");
|
|
3677
|
+
const s2 = node.style;
|
|
3678
|
+
s2.position = "absolute";
|
|
3679
|
+
s2.left = "0";
|
|
3680
|
+
s2.top = "0";
|
|
3681
|
+
s2.whiteSpace = "nowrap";
|
|
3682
|
+
s2.textShadow = "0 1px 3px rgba(0,0,0,0.85), 0 0 8px rgba(0,0,0,0.55)";
|
|
3683
|
+
s2.display = "none";
|
|
3684
|
+
this.root.appendChild(node);
|
|
3685
|
+
this.nodes.set(label.id, node);
|
|
3686
|
+
}
|
|
3501
3687
|
node.textContent = label.text;
|
|
3502
3688
|
node.setAttribute("data-label-kind", label.kind);
|
|
3689
|
+
const selected = this.selectedSeatLabelIds.has(label.id);
|
|
3690
|
+
node.toggleAttribute("data-selected-seat", selected);
|
|
3503
3691
|
const style = KIND_STYLE[label.kind];
|
|
3504
3692
|
const s = node.style;
|
|
3505
|
-
s.position = "absolute";
|
|
3506
|
-
s.left = "0";
|
|
3507
|
-
s.top = "0";
|
|
3508
|
-
s.whiteSpace = "nowrap";
|
|
3509
3693
|
s.fontSize = `${style.size}px`;
|
|
3510
|
-
s.fontWeight = style.weight;
|
|
3511
|
-
s.opacity = String(style.opacity);
|
|
3512
|
-
s.color = label.color ?? this.opts.ink ?? "#e8edf5";
|
|
3513
|
-
s.
|
|
3694
|
+
s.fontWeight = selected ? "800" : style.weight;
|
|
3695
|
+
s.opacity = selected ? "1" : String(style.opacity);
|
|
3696
|
+
s.color = selected ? "#ecf8ff" : label.color ?? this.opts.ink ?? "#e8edf5";
|
|
3697
|
+
s.background = selected ? "rgba(10,28,45,.9)" : "";
|
|
3698
|
+
s.border = selected ? "1px solid rgba(89,199,255,.92)" : "";
|
|
3699
|
+
s.borderRadius = selected ? "999px" : "";
|
|
3700
|
+
s.padding = selected ? "3px 7px" : "";
|
|
3701
|
+
s.boxShadow = selected ? "0 0 0 2px rgba(41,176,255,.18), 0 5px 14px rgba(0,0,0,.35)" : "";
|
|
3514
3702
|
s.letterSpacing = label.kind === "zone" ? "0.08em" : "0.02em";
|
|
3515
3703
|
if (label.kind === "zone") s.textTransform = "uppercase";
|
|
3516
|
-
s.display = "none";
|
|
3517
|
-
this.root.appendChild(node);
|
|
3518
|
-
this.nodes.set(label.id, node);
|
|
3519
3704
|
return node;
|
|
3520
3705
|
}
|
|
3521
3706
|
dispose() {
|
|
@@ -3536,6 +3721,15 @@ float chairWeight(float depth) {
|
|
|
3536
3721
|
return 1.0 - smoothstep(uChairFull, uChairNone, depth);
|
|
3537
3722
|
}`
|
|
3538
3723
|
);
|
|
3724
|
+
var OCCUPANT_WEIGHT_GLSL = (
|
|
3725
|
+
/* glsl */
|
|
3726
|
+
`
|
|
3727
|
+
float occupantWeight(float depth) {
|
|
3728
|
+
float nearWeight = smoothstep(${OCCUPANT_NEAR_NONE_M.toFixed(1)}, ${OCCUPANT_NEAR_FULL_M.toFixed(1)}, depth);
|
|
3729
|
+
float farWeight = 1.0 - smoothstep(${OCCUPANT_FULL_M.toFixed(1)}, ${OCCUPANT_NONE_M.toFixed(1)}, depth);
|
|
3730
|
+
return nearWeight * farWeight;
|
|
3731
|
+
}`
|
|
3732
|
+
);
|
|
3539
3733
|
var SOLID_VERT = (
|
|
3540
3734
|
/* glsl */
|
|
3541
3735
|
`#version 300 es
|
|
@@ -3544,8 +3738,10 @@ in vec3 position;
|
|
|
3544
3738
|
in vec3 normal;
|
|
3545
3739
|
in vec3 color;
|
|
3546
3740
|
in float floorIndex;
|
|
3741
|
+
in float materialIndex;
|
|
3547
3742
|
uniform mat4 modelMatrix;
|
|
3548
3743
|
uniform float uFocusFloor; // -1 = show every floor
|
|
3744
|
+
uniform float uStructureDetail; // 0 = venue overview, 1 = section/row/seat
|
|
3549
3745
|
uniform mat4 modelViewMatrix;
|
|
3550
3746
|
uniform mat4 projectionMatrix;
|
|
3551
3747
|
uniform mat3 normalMatrix;
|
|
@@ -3553,14 +3749,19 @@ out vec3 vColor;
|
|
|
3553
3749
|
out vec3 vNormalWorld;
|
|
3554
3750
|
out vec3 vNormalView;
|
|
3555
3751
|
out vec3 vPosView;
|
|
3752
|
+
out vec3 vPosWorld;
|
|
3556
3753
|
out float vDim;
|
|
3754
|
+
out float vMaterial;
|
|
3557
3755
|
void main() {
|
|
3558
3756
|
// Per-floor isolation without splitting the merged mesh into a draw call per
|
|
3559
3757
|
// floor: a floor that is not the focused one is dimmed, not hidden, so the
|
|
3560
3758
|
// buyer keeps the whole venue as context while looking at one level.
|
|
3561
3759
|
vDim = (uFocusFloor < -0.5 || abs(floorIndex - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
3760
|
+
vMaterial = materialIndex;
|
|
3761
|
+
vec4 world = modelMatrix * vec4(position, 1.0);
|
|
3562
3762
|
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
3563
3763
|
vPosView = mv.xyz;
|
|
3764
|
+
vPosWorld = world.xyz;
|
|
3564
3765
|
vNormalView = normalize(normalMatrix * normal);
|
|
3565
3766
|
// World normal drives the key + hemisphere so the lighting stays welded to the
|
|
3566
3767
|
// venue as the camera orbits (the scene has no non-uniform scale, so mat3 of
|
|
@@ -3578,10 +3779,20 @@ in vec3 vColor;
|
|
|
3578
3779
|
in vec3 vNormalWorld;
|
|
3579
3780
|
in vec3 vNormalView;
|
|
3580
3781
|
in vec3 vPosView;
|
|
3782
|
+
in vec3 vPosWorld;
|
|
3581
3783
|
in float vDim;
|
|
3784
|
+
in float vMaterial;
|
|
3582
3785
|
uniform vec3 uKeyDir; // WORLD space, unit, points at the light
|
|
3786
|
+
uniform float uStructureDetail;
|
|
3583
3787
|
out vec4 fragColor;
|
|
3584
3788
|
void main() {
|
|
3789
|
+
// Row treads/risers are honest and useful when the buyer is inside a stand.
|
|
3790
|
+
// At whole-venue scale their overlapping centimetre-scale surfaces quantise
|
|
3791
|
+
// and alias into camera-dependent radial combs. The clean structural block
|
|
3792
|
+
// cap remains, so overview keeps every stand without pretending to show row
|
|
3793
|
+
// detail below a useful pixel size.
|
|
3794
|
+
float rowDetail = step(1.5, vMaterial);
|
|
3795
|
+
if (rowDetail > 0.5 && uStructureDetail < 0.5) discard;
|
|
3585
3796
|
vec3 N = normalize(vNormalWorld);
|
|
3586
3797
|
vec3 V = normalize(-vPosView);
|
|
3587
3798
|
float hemi = 0.5 + 0.5 * N.y; // sky/ground gradient about WORLD up
|
|
@@ -3593,6 +3804,30 @@ void main() {
|
|
|
3593
3804
|
vec3 base = vColor * (0.52 + 0.34 * hemi) + vColor * key * 0.34 + vColor * fill * 0.10;
|
|
3594
3805
|
float fres = pow(1.0 - max(dot(normalize(vNormalView), V), 0.0), 3.0);
|
|
3595
3806
|
base += vec3(0.26, 0.31, 0.38) * fres * 0.35; // cool rim, restrained (view-dependent by design)
|
|
3807
|
+
// Bright, saturated authored screen faces should read as powered displays,
|
|
3808
|
+
// not painted slabs. Vertex colour is deliberately the material signal here:
|
|
3809
|
+
// muted tiers and structure never cross these thresholds. The broad waves
|
|
3810
|
+
// supply a restrained procedural LED treatment without textures, downloads,
|
|
3811
|
+
// solid fake-light geometry or post-processing.
|
|
3812
|
+
float maxChannel = max(max(vColor.r, vColor.g), vColor.b);
|
|
3813
|
+
float minChannel = min(min(vColor.r, vColor.g), vColor.b);
|
|
3814
|
+
float poweredDisplay = (1.0 - step(0.5, abs(vMaterial - 1.0)))
|
|
3815
|
+
* smoothstep(0.58, 0.72, maxChannel)
|
|
3816
|
+
* smoothstep(0.28, 0.48, maxChannel - minChannel);
|
|
3817
|
+
float verticalFace = 1.0 - smoothstep(0.20, 0.62, abs(N.y));
|
|
3818
|
+
float wave = 0.5 + 0.5 * sin(vPosWorld.x * 0.44 + vPosWorld.y * 0.28);
|
|
3819
|
+
float localX = vPosWorld.x;
|
|
3820
|
+
float localY = vPosWorld.y;
|
|
3821
|
+
float beamWest = exp(-pow((localX + 2.6 - localY * 0.22) * 0.44, 2.0));
|
|
3822
|
+
float beamEast = exp(-pow((localX - 2.6 + localY * 0.22) * 0.44, 2.0));
|
|
3823
|
+
float band = smoothstep(0.02, 0.16, 0.18 - abs(localY - 3.9));
|
|
3824
|
+
float focalHalo = exp(-pow(localX * 0.26, 2.0) - pow((localY - 3.9) * 0.38, 2.0));
|
|
3825
|
+
vec3 display = vColor * (0.74 + wave * 0.26)
|
|
3826
|
+
+ vec3(0.10, 0.30, 0.52) * beamWest
|
|
3827
|
+
+ vec3(0.34, 0.13, 0.42) * beamEast
|
|
3828
|
+
+ vec3(0.62, 0.46, 0.20) * band * 0.28
|
|
3829
|
+
+ vec3(0.20, 0.10, 0.34) * focalHalo;
|
|
3830
|
+
base = mix(base, display, poweredDisplay * verticalFace * 0.92);
|
|
3596
3831
|
// Unfocused floors fall back toward the background rather than vanishing.
|
|
3597
3832
|
base = mix(base, base * 0.45, vDim);
|
|
3598
3833
|
fragColor = vec4(base, 1.0);
|
|
@@ -3625,6 +3860,7 @@ out vec3 vRing;
|
|
|
3625
3860
|
out float vDim;
|
|
3626
3861
|
out float vDotWeight; // 1 = the dot IS this seat, 0 = the chair has taken over
|
|
3627
3862
|
out float vPhysicalSeat;
|
|
3863
|
+
out float vPixelRadius;
|
|
3628
3864
|
${CHAIR_WEIGHT_GLSL}
|
|
3629
3865
|
void main() {
|
|
3630
3866
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -3638,6 +3874,7 @@ void main() {
|
|
|
3638
3874
|
// Grow to hold the pixel floor, but never past this seat's own pitch ceiling:
|
|
3639
3875
|
// unbounded growth is what merges neighbouring rows into one mass at range.
|
|
3640
3876
|
float r = min(max(uSeatRadius * uSeatScale, minR), iMaxRadius);
|
|
3877
|
+
vPixelRadius = r / max(depth * uPixelToWorld, 0.000001);
|
|
3641
3878
|
// How much of the requested pixel floor the dot could actually afford. Below 1
|
|
3642
3879
|
// it is losing legibility to distance, and the fragment stage dissolves it
|
|
3643
3880
|
// toward the tier top rather than letting a sub-pixel dot alias and shimmer.
|
|
@@ -3670,10 +3907,13 @@ in vec3 vRing;
|
|
|
3670
3907
|
in float vDim;
|
|
3671
3908
|
in float vDotWeight;
|
|
3672
3909
|
in float vPhysicalSeat;
|
|
3910
|
+
in float vPixelRadius;
|
|
3673
3911
|
uniform float uSeatFade; // fade toward tier colour with distance (LOD)
|
|
3912
|
+
uniform float uSeatDetail; // 0 for venue/area overview; 1 in section/row/seat
|
|
3674
3913
|
uniform vec3 uFadeColor;
|
|
3675
3914
|
out vec4 fragColor;
|
|
3676
3915
|
void main() {
|
|
3916
|
+
if (uSeatDetail < 0.5) discard;
|
|
3677
3917
|
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
3678
3918
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3679
3919
|
if (d > 1.0) discard;
|
|
@@ -3692,6 +3932,20 @@ void main() {
|
|
|
3692
3932
|
// aliasing; the tier cap underneath already carries the section's category
|
|
3693
3933
|
// tint, so the block reads as coloured seating rather than empty concrete.
|
|
3694
3934
|
alpha *= smoothstep(0.35, 1.0, vBudget);
|
|
3935
|
+
// Do not rasterise inventory states until a dot is large enough to read as a
|
|
3936
|
+
// seat rather than a sample in a dense screen-space pattern. This is based on
|
|
3937
|
+
// actual projected pixels, so it remains correct for unusually large or small
|
|
3938
|
+
// venues where a radius-normalised camera distance is misleading.
|
|
3939
|
+
alpha *= smoothstep(2.4, 3.2, vPixelRadius);
|
|
3940
|
+
// Inventory states are a section-level decision aid. At whole-venue/area
|
|
3941
|
+
// scale the category-tinted structural surface is the honest, stable signal;
|
|
3942
|
+
// thousands of enlarged status dots only form a moir\xE9 pattern. The journey
|
|
3943
|
+
// state enables them when the buyer enters a section, row or exact seat.
|
|
3944
|
+
// Whole-venue status detail is below a useful pixel size. Drawing every dot
|
|
3945
|
+
// there turns concentric rows into camera-dependent moir\xE9 streaks, especially
|
|
3946
|
+
// while zooming. uSeatFade already measures this exact LOD transition: let
|
|
3947
|
+
// the category-tinted tier carry the overview, then reveal individual live
|
|
3948
|
+
// availability as the buyer approaches a section.
|
|
3695
3949
|
// Seats on an unfocused floor recede with their structure.
|
|
3696
3950
|
c = mix(c, uFadeColor, vDim * 0.75);
|
|
3697
3951
|
alpha *= mix(1.0, 0.30, vDim);
|
|
@@ -3736,9 +3990,15 @@ out float vDim;
|
|
|
3736
3990
|
out float vOccupant; // 1 = this vertex belongs to a person, not to the chair
|
|
3737
3991
|
out vec3 vOccupantTint;
|
|
3738
3992
|
${CHAIR_WEIGHT_GLSL}
|
|
3993
|
+
${OCCUPANT_WEIGHT_GLSL}
|
|
3739
3994
|
void main() {
|
|
3740
3995
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
3741
|
-
float
|
|
3996
|
+
float viewDepth = -anchor.z;
|
|
3997
|
+
float depth = max(viewDepth, 0.001);
|
|
3998
|
+
// A stale near-field gather can contain seats now behind the camera after a
|
|
3999
|
+
// row\u2192overview move. Treating negative depth as 0.001 made those chairs full
|
|
4000
|
+
// size across the near plane, producing the long black zoom streaks.
|
|
4001
|
+
float w = viewDepth > 0.05 ? chairWeight(depth) : 0.0;
|
|
3742
4002
|
// 1. Local units -> world metres. Only x/z scale: narrow rows get narrow
|
|
3743
4003
|
// chairs, but nobody gets a short one (people are the same height at every
|
|
3744
4004
|
// seat pitch).
|
|
@@ -3763,6 +4023,10 @@ void main() {
|
|
|
3763
4023
|
// identical mannequins: +/-6% height and +/-8% width off the hash.
|
|
3764
4024
|
p.y *= 0.94 + 0.12 * iSeed;
|
|
3765
4025
|
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
4026
|
+
// People only appear once they are large enough to read as people. At
|
|
4027
|
+
// section/overview distance their tiny dark geometry aliases into radial
|
|
4028
|
+
// streaks; seat dots already communicate held/booked state there.
|
|
4029
|
+
p *= occupantWeight(depth);
|
|
3766
4030
|
}
|
|
3767
4031
|
}
|
|
3768
4032
|
p.xz *= iRadius;
|
|
@@ -3801,7 +4065,7 @@ void main() {
|
|
|
3801
4065
|
// not an audience. Hair/clothing for the body, a warm tone for the head,
|
|
3802
4066
|
// both varied by the same per-person hash.
|
|
3803
4067
|
float t = fract(iSeed * 3.71);
|
|
3804
|
-
vec3 clothes = mix(vec3(0.
|
|
4068
|
+
vec3 clothes = mix(vec3(0.18, 0.22, 0.30), vec3(0.42, 0.29, 0.34), t);
|
|
3805
4069
|
vec3 skin = mix(vec3(0.52, 0.38, 0.29), vec3(0.86, 0.70, 0.58), fract(iSeed * 11.3));
|
|
3806
4070
|
vOccupantTint = (part > 3.5) ? skin : clothes;
|
|
3807
4071
|
vPart = part;
|
|
@@ -3905,14 +4169,19 @@ uniform float uSeatRadius;
|
|
|
3905
4169
|
uniform float uSeatScale;
|
|
3906
4170
|
uniform float uMinPixels;
|
|
3907
4171
|
uniform float uPixelToWorld;
|
|
4172
|
+
uniform float uChairFull;
|
|
4173
|
+
uniform float uChairNone;
|
|
3908
4174
|
out vec2 vUv;
|
|
3909
4175
|
out float vPhysicalSeat;
|
|
4176
|
+
out float vDotWeight;
|
|
3910
4177
|
flat out vec3 vPick;
|
|
4178
|
+
${CHAIR_WEIGHT_GLSL}
|
|
3911
4179
|
void main() {
|
|
3912
4180
|
int id = gl_InstanceID + 1; // 0 reserved for no-hit
|
|
3913
4181
|
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
3914
4182
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
3915
4183
|
float depth = max(-mv.z, 0.001);
|
|
4184
|
+
vDotWeight = 1.0 - chairWeight(depth);
|
|
3916
4185
|
float minR = uMinPixels * depth * uPixelToWorld;
|
|
3917
4186
|
float r = min(max(uSeatRadius * uSeatScale, minR), iMaxRadius);
|
|
3918
4187
|
mv.xy += position * r;
|
|
@@ -3929,14 +4198,75 @@ var SEAT_PICK_FRAG = (
|
|
|
3929
4198
|
precision highp float;
|
|
3930
4199
|
in vec2 vUv;
|
|
3931
4200
|
in float vPhysicalSeat;
|
|
4201
|
+
in float vDotWeight;
|
|
3932
4202
|
flat in vec3 vPick;
|
|
3933
4203
|
out vec4 fragColor;
|
|
3934
4204
|
void main() {
|
|
4205
|
+
if (vDotWeight <= 0.02) discard; // invisible dot cannot steal a chair tap
|
|
3935
4206
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
3936
4207
|
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
3937
4208
|
fragColor = vec4(vPick, 1.0);
|
|
3938
4209
|
}`
|
|
3939
4210
|
);
|
|
4211
|
+
var CHAIR_PICK_VERT = (
|
|
4212
|
+
/* glsl */
|
|
4213
|
+
`#version 300 es
|
|
4214
|
+
precision highp float;
|
|
4215
|
+
in vec3 position;
|
|
4216
|
+
in float part;
|
|
4217
|
+
in vec3 iOffset;
|
|
4218
|
+
in float iRadius;
|
|
4219
|
+
in float iYaw;
|
|
4220
|
+
in float iSeed;
|
|
4221
|
+
in float iPhysicalSeat;
|
|
4222
|
+
in float iPickIndex;
|
|
4223
|
+
uniform mat4 modelViewMatrix;
|
|
4224
|
+
uniform mat4 projectionMatrix;
|
|
4225
|
+
uniform float uChairFull;
|
|
4226
|
+
uniform float uChairNone;
|
|
4227
|
+
uniform float uBackRake;
|
|
4228
|
+
uniform float uBackBase;
|
|
4229
|
+
flat out vec3 vPick;
|
|
4230
|
+
${CHAIR_WEIGHT_GLSL}
|
|
4231
|
+
${OCCUPANT_WEIGHT_GLSL}
|
|
4232
|
+
void main() {
|
|
4233
|
+
int id = int(iPickIndex + 0.5); // already global index + 1
|
|
4234
|
+
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
4235
|
+
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4236
|
+
float viewDepth = -anchor.z;
|
|
4237
|
+
float depth = max(viewDepth, 0.001);
|
|
4238
|
+
float w = viewDepth > 0.05 ? chairWeight(depth) : 0.0;
|
|
4239
|
+
vec3 p = position;
|
|
4240
|
+
float occupant = step(2.5, part);
|
|
4241
|
+
float taken = step(0.0, iSeed);
|
|
4242
|
+
if (iPhysicalSeat < 0.5) {
|
|
4243
|
+
p = vec3(0.0);
|
|
4244
|
+
} else if (occupant > 0.5) {
|
|
4245
|
+
if (taken < 0.5) {
|
|
4246
|
+
p = vec3(0.0);
|
|
4247
|
+
} else {
|
|
4248
|
+
p.y *= 0.94 + 0.12 * iSeed;
|
|
4249
|
+
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
4250
|
+
p *= occupantWeight(depth);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
p.xz *= iRadius;
|
|
4254
|
+
float rake = (part > 1.5 && part < 2.5) ? max(p.y - uBackBase, 0.0) * uBackRake : 0.0;
|
|
4255
|
+
p.z -= rake;
|
|
4256
|
+
p *= sqrt(w);
|
|
4257
|
+
float c = cos(iYaw), s = sin(iYaw);
|
|
4258
|
+
vec3 rp = vec3(p.x * c + p.z * s, p.y, -p.x * s + p.z * c);
|
|
4259
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(iOffset + rp, 1.0);
|
|
4260
|
+
}`
|
|
4261
|
+
);
|
|
4262
|
+
var CHAIR_PICK_FRAG = (
|
|
4263
|
+
/* glsl */
|
|
4264
|
+
`#version 300 es
|
|
4265
|
+
precision highp float;
|
|
4266
|
+
flat in vec3 vPick;
|
|
4267
|
+
out vec4 fragColor;
|
|
4268
|
+
void main() { fragColor = vec4(vPick, 1.0); }`
|
|
4269
|
+
);
|
|
3940
4270
|
var PICK_DEPTH_VERT = (
|
|
3941
4271
|
/* glsl */
|
|
3942
4272
|
`#version 300 es
|
|
@@ -3967,7 +4297,25 @@ function createSeatPickProgram(gl) {
|
|
|
3967
4297
|
uSeatRadius: { value: SEAT_DOT_RADIUS_M },
|
|
3968
4298
|
uSeatScale: { value: 1 },
|
|
3969
4299
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
3970
|
-
uPixelToWorld: { value: 2e-3 }
|
|
4300
|
+
uPixelToWorld: { value: 2e-3 },
|
|
4301
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4302
|
+
uChairNone: { value: CHAIR_NONE_M }
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
function createChairPickProgram(gl) {
|
|
4307
|
+
return new Program(gl, {
|
|
4308
|
+
vertex: CHAIR_PICK_VERT,
|
|
4309
|
+
fragment: CHAIR_PICK_FRAG,
|
|
4310
|
+
transparent: false,
|
|
4311
|
+
depthTest: true,
|
|
4312
|
+
depthWrite: true,
|
|
4313
|
+
cullFace: false,
|
|
4314
|
+
uniforms: {
|
|
4315
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4316
|
+
uChairNone: { value: CHAIR_NONE_M },
|
|
4317
|
+
uBackRake: { value: BACK_RAKE_SLOPE },
|
|
4318
|
+
uBackBase: { value: BACK_BASE_M }
|
|
3971
4319
|
}
|
|
3972
4320
|
});
|
|
3973
4321
|
}
|
|
@@ -3996,7 +4344,8 @@ function createSolidProgram(gl) {
|
|
|
3996
4344
|
// High and off-axis, in world space: reads as a house rig rather than a
|
|
3997
4345
|
// headlamp welded to the camera.
|
|
3998
4346
|
uKeyDir: { value: new Float32Array([0.38, 0.86, 0.34]) },
|
|
3999
|
-
uFocusFloor: { value: -1 }
|
|
4347
|
+
uFocusFloor: { value: -1 },
|
|
4348
|
+
uStructureDetail: { value: 0 }
|
|
4000
4349
|
}
|
|
4001
4350
|
});
|
|
4002
4351
|
}
|
|
@@ -4014,6 +4363,7 @@ function createSeatProgram(gl) {
|
|
|
4014
4363
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
4015
4364
|
uPixelToWorld: { value: 2e-3 },
|
|
4016
4365
|
uSeatFade: { value: 0 },
|
|
4366
|
+
uSeatDetail: { value: 0 },
|
|
4017
4367
|
uFocusFloor: { value: -1 },
|
|
4018
4368
|
uFadeColor: { value: new Float32Array([0.32, 0.37, 0.43]) },
|
|
4019
4369
|
uChairFull: { value: CHAIR_FULL_M },
|
|
@@ -4078,7 +4428,8 @@ function buildGpuScene(gl, model) {
|
|
|
4078
4428
|
position: { size: 3, data: model.solids.position },
|
|
4079
4429
|
normal: { size: 3, data: model.solids.normal },
|
|
4080
4430
|
color: { size: 3, data: model.solids.color },
|
|
4081
|
-
floorIndex: { size: 1, data: model.solids.floor }
|
|
4431
|
+
floorIndex: { size: 1, data: model.solids.floor },
|
|
4432
|
+
materialIndex: { size: 1, data: model.solids.material }
|
|
4082
4433
|
});
|
|
4083
4434
|
const solidProg = createSolidProgram(gl);
|
|
4084
4435
|
const solidMesh = new Mesh(gl, { geometry: solidGeo, program: solidProg });
|
|
@@ -4114,6 +4465,7 @@ function buildGpuScene(gl, model) {
|
|
|
4114
4465
|
const cRing = new Float32Array(CAP * 3);
|
|
4115
4466
|
const cFloor = new Float32Array(CAP);
|
|
4116
4467
|
const cPhysicalSeat = new Float32Array(CAP);
|
|
4468
|
+
const cPickIndex = new Float32Array(CAP);
|
|
4117
4469
|
const cSeed = new Float32Array(CAP);
|
|
4118
4470
|
const chairGeo = new Geometry(gl, {
|
|
4119
4471
|
position: { size: 3, data: chairBase.position },
|
|
@@ -4127,7 +4479,8 @@ function buildGpuScene(gl, model) {
|
|
|
4127
4479
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
4128
4480
|
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4129
4481
|
iPhysicalSeat: { size: 1, data: cPhysicalSeat, instanced: 1 },
|
|
4130
|
-
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
4482
|
+
iSeed: { size: 1, data: cSeed, instanced: 1 },
|
|
4483
|
+
iPickIndex: { size: 1, data: cPickIndex, instanced: 1 }
|
|
4131
4484
|
});
|
|
4132
4485
|
const chairMesh = new Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
4133
4486
|
chairMesh.frustumCulled = false;
|
|
@@ -4137,6 +4490,7 @@ function buildGpuScene(gl, model) {
|
|
|
4137
4490
|
const src = model.seats;
|
|
4138
4491
|
for (let k = 0; k < nearCount; k++) {
|
|
4139
4492
|
const i = nearIndices[k];
|
|
4493
|
+
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
4140
4494
|
const useCategory = src.iCategory && src.iState[i] === 0;
|
|
4141
4495
|
const c = useCategory ? null : stateColors[src.iState[i]] ?? stateColors[0];
|
|
4142
4496
|
if (c) {
|
|
@@ -4151,6 +4505,7 @@ function buildGpuScene(gl, model) {
|
|
|
4151
4505
|
}
|
|
4152
4506
|
}
|
|
4153
4507
|
chairGeo.attributes.iColor.needsUpdate = true;
|
|
4508
|
+
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4154
4509
|
};
|
|
4155
4510
|
const scene = {
|
|
4156
4511
|
main,
|
|
@@ -4159,6 +4514,7 @@ function buildGpuScene(gl, model) {
|
|
|
4159
4514
|
solidProgram: solidProg,
|
|
4160
4515
|
chairProgram: chairProg,
|
|
4161
4516
|
seatGeometry: seatGeo,
|
|
4517
|
+
chairGeometry: chairGeo,
|
|
4162
4518
|
solidGeometry: solidGeo,
|
|
4163
4519
|
drawCalls: 3,
|
|
4164
4520
|
setNearSeats(indices, count) {
|
|
@@ -4184,7 +4540,7 @@ function buildGpuScene(gl, model) {
|
|
|
4184
4540
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
4185
4541
|
cFloor[k] = src.iFloor[i];
|
|
4186
4542
|
cPhysicalSeat[k] = src.iPhysicalSeat[i];
|
|
4187
|
-
|
|
4543
|
+
cPickIndex[k] = i + 1;
|
|
4188
4544
|
}
|
|
4189
4545
|
writeChairColors();
|
|
4190
4546
|
chairGeo.attributes.iOffset.needsUpdate = true;
|
|
@@ -4194,6 +4550,7 @@ function buildGpuScene(gl, model) {
|
|
|
4194
4550
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4195
4551
|
chairGeo.attributes.iPhysicalSeat.needsUpdate = true;
|
|
4196
4552
|
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4553
|
+
chairGeo.attributes.iPickIndex.needsUpdate = true;
|
|
4197
4554
|
chairGeo.instancedCount = n;
|
|
4198
4555
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
4199
4556
|
scene.drawCalls = 4;
|
|
@@ -4273,8 +4630,9 @@ function pickPixelCoords(clientX, clientY, rect, dpr, bufferWidth, bufferHeight)
|
|
|
4273
4630
|
// src/view3d/pick/pickPipeline.ts
|
|
4274
4631
|
var SYNC_KEYS = ["uSeatRadius", "uSeatScale", "uMinPixels", "uPixelToWorld"];
|
|
4275
4632
|
var PickPipeline = class {
|
|
4276
|
-
constructor(renderer, seatGeo, solidGeo, seatCount) {
|
|
4633
|
+
constructor(renderer, seatGeo, chairGeo, solidGeo, seatCount) {
|
|
4277
4634
|
this.seatScene = new Transform2();
|
|
4635
|
+
this.chairScene = new Transform2();
|
|
4278
4636
|
this.solidScene = new Transform2();
|
|
4279
4637
|
this.target = null;
|
|
4280
4638
|
/** Display clear colour to restore after the pick pass (theme-dependent). */
|
|
@@ -4283,10 +4641,14 @@ var PickPipeline = class {
|
|
|
4283
4641
|
this.gl = renderer.gl;
|
|
4284
4642
|
this.maxIndex = seatCount;
|
|
4285
4643
|
this.seatProg = createSeatPickProgram(this.gl);
|
|
4644
|
+
this.chairProg = createChairPickProgram(this.gl);
|
|
4286
4645
|
this.depthProg = createPickDepthProgram(this.gl);
|
|
4287
4646
|
const seatMesh = new Mesh2(this.gl, { geometry: seatGeo, program: this.seatProg });
|
|
4288
4647
|
seatMesh.frustumCulled = false;
|
|
4289
4648
|
seatMesh.setParent(this.seatScene);
|
|
4649
|
+
const chairMesh = new Mesh2(this.gl, { geometry: chairGeo, program: this.chairProg });
|
|
4650
|
+
chairMesh.frustumCulled = false;
|
|
4651
|
+
chairMesh.setParent(this.chairScene);
|
|
4290
4652
|
const solidMesh = new Mesh2(this.gl, { geometry: solidGeo, program: this.depthProg });
|
|
4291
4653
|
solidMesh.frustumCulled = false;
|
|
4292
4654
|
solidMesh.setParent(this.solidScene);
|
|
@@ -4339,6 +4701,7 @@ var PickPipeline = class {
|
|
|
4339
4701
|
gl.clearColor(0, 0, 0, 1);
|
|
4340
4702
|
this.renderer.render({ scene: this.solidScene, camera, target, clear: true });
|
|
4341
4703
|
this.renderer.render({ scene: this.seatScene, camera, target, clear: false });
|
|
4704
|
+
this.renderer.render({ scene: this.chairScene, camera, target, clear: false });
|
|
4342
4705
|
gl.clearColor(br, bg, bb, 1);
|
|
4343
4706
|
gl.disable(gl.SCISSOR_TEST);
|
|
4344
4707
|
const buf = new Uint8Array(boxW * boxH * 4);
|
|
@@ -4350,6 +4713,7 @@ var PickPipeline = class {
|
|
|
4350
4713
|
dispose() {
|
|
4351
4714
|
this.destroyTarget();
|
|
4352
4715
|
this.seatProg.remove();
|
|
4716
|
+
this.chairProg.remove();
|
|
4353
4717
|
this.depthProg.remove();
|
|
4354
4718
|
}
|
|
4355
4719
|
};
|
|
@@ -4479,16 +4843,18 @@ var Cinematic = class {
|
|
|
4479
4843
|
this.outQuat = new Quat();
|
|
4480
4844
|
this.startTime = 0;
|
|
4481
4845
|
this.duration = FLIGHT_DURATION_MS;
|
|
4846
|
+
this.endFov = FOV_END;
|
|
4482
4847
|
this.resolveFn = null;
|
|
4483
4848
|
this.camera = camera;
|
|
4484
4849
|
}
|
|
4485
4850
|
/** Begin (or retarget) a flight. Resolves when it lands or is cancelled. */
|
|
4486
|
-
start(waypoints, startQuat, endQuat, duration = FLIGHT_DURATION_MS) {
|
|
4851
|
+
start(waypoints, startQuat, endQuat, duration = FLIGHT_DURATION_MS, endFov = FOV_END) {
|
|
4487
4852
|
this.settle();
|
|
4488
4853
|
this.waypoints = waypoints;
|
|
4489
4854
|
this.startQuat.copy(startQuat);
|
|
4490
4855
|
this.endQuat.copy(endQuat);
|
|
4491
4856
|
this.duration = duration;
|
|
4857
|
+
this.endFov = endFov;
|
|
4492
4858
|
this.startTime = performance.now();
|
|
4493
4859
|
this.active = true;
|
|
4494
4860
|
return new Promise((res) => {
|
|
@@ -4499,7 +4865,7 @@ var Cinematic = class {
|
|
|
4499
4865
|
update(now2) {
|
|
4500
4866
|
if (!this.active) return false;
|
|
4501
4867
|
const u = Math.min(1, (now2 - this.startTime) / this.duration);
|
|
4502
|
-
const { pos, fov, eased } = sampleFlight(this.waypoints, u);
|
|
4868
|
+
const { pos, fov, eased } = sampleFlight(this.waypoints, u, FOV_START, this.endFov);
|
|
4503
4869
|
this.camera.position.set(pos[0], pos[1], pos[2]);
|
|
4504
4870
|
this.outQuat.copy(this.startQuat).slerp(this.endQuat, orientationLeadT(eased, ORIENTATION_LEAD));
|
|
4505
4871
|
this.camera.quaternion.copy(this.outQuat);
|
|
@@ -4526,9 +4892,10 @@ var Cinematic = class {
|
|
|
4526
4892
|
// src/view3d/camera/seatViewPose.ts
|
|
4527
4893
|
var SEAT_EYE_ABOVE_DECK_M = 1.02;
|
|
4528
4894
|
var FOCAL_LOOK_HEIGHT_M = 1.5;
|
|
4529
|
-
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0) {
|
|
4895
|
+
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0, authoredEyeHeightM) {
|
|
4896
|
+
const eyeY = Number.isFinite(authoredEyeHeightM) ? floorBaseHeightM + authoredEyeHeightM : seatDeckWorld[1] + SEAT_EYE_ABOVE_DECK_M;
|
|
4530
4897
|
return {
|
|
4531
|
-
eye: [seatDeckWorld[0],
|
|
4898
|
+
eye: [seatDeckWorld[0], eyeY, seatDeckWorld[2]],
|
|
4532
4899
|
focal: [
|
|
4533
4900
|
focalPoint.x * M,
|
|
4534
4901
|
floorBaseHeightM + FOCAL_LOOK_HEIGHT_M,
|
|
@@ -4791,7 +5158,7 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
4791
5158
|
root.appendChild(closeBtn);
|
|
4792
5159
|
if (opts.disclosure) {
|
|
4793
5160
|
const disclosure = document.createElement("div");
|
|
4794
|
-
disclosure.textContent =
|
|
5161
|
+
disclosure.textContent = `${opts.disclosurePrefix ?? "360\xB0 panorama"} \xB7 ${opts.disclosure}`;
|
|
4795
5162
|
Object.assign(disclosure.style, {
|
|
4796
5163
|
position: "absolute",
|
|
4797
5164
|
top: "12px",
|
|
@@ -5134,7 +5501,13 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5134
5501
|
};
|
|
5135
5502
|
const rebuildGpu = () => {
|
|
5136
5503
|
gpu = buildGpuScene(glctx.gl, model);
|
|
5137
|
-
pick = new PickPipeline(
|
|
5504
|
+
pick = new PickPipeline(
|
|
5505
|
+
glctx.renderer,
|
|
5506
|
+
gpu.seatGeometry,
|
|
5507
|
+
gpu.chairGeometry,
|
|
5508
|
+
gpu.solidGeometry,
|
|
5509
|
+
model.seats.count
|
|
5510
|
+
);
|
|
5138
5511
|
glctx.setClearColor(model.theme.background.top);
|
|
5139
5512
|
pick.setRestoreClear(model.theme.background.top);
|
|
5140
5513
|
gpu.seatProgram.uniforms.uSeatRadius.value = SEAT_DOT_RADIUS_M * model.theme.seatScale;
|
|
@@ -5147,8 +5520,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5147
5520
|
const nearBuf = new Int32Array(CHAIR_MAX_INSTANCES);
|
|
5148
5521
|
let lastGatherX = Infinity;
|
|
5149
5522
|
let lastGatherZ = Infinity;
|
|
5150
|
-
|
|
5523
|
+
let nearFieldDetailEnabled = false;
|
|
5524
|
+
let cameraDetailVisible = false;
|
|
5525
|
+
let panoramaSphereVisible = false;
|
|
5526
|
+
const setNearFieldDetailEnabled = (enabled) => {
|
|
5527
|
+
nearFieldDetailEnabled = enabled;
|
|
5528
|
+
if (!enabled) cameraDetailVisible = false;
|
|
5529
|
+
lastGatherX = Infinity;
|
|
5530
|
+
lastGatherZ = Infinity;
|
|
5531
|
+
if (!enabled && gpu?.nearSeatCount()) gpu.setNearSeats(nearBuf, 0);
|
|
5532
|
+
};
|
|
5533
|
+
const updateNearField = (detailVisible) => {
|
|
5151
5534
|
if (!gpu) return;
|
|
5535
|
+
if (!detailVisible) {
|
|
5536
|
+
if (gpu.nearSeatCount()) gpu.setNearSeats(nearBuf, 0);
|
|
5537
|
+
return;
|
|
5538
|
+
}
|
|
5152
5539
|
const cam = orbit.camera.position;
|
|
5153
5540
|
const moved2 = Math.hypot(cam.x - lastGatherX, cam.z - lastGatherZ);
|
|
5154
5541
|
if (moved2 < CHAIR_REBUILD_M) return;
|
|
@@ -5191,7 +5578,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5191
5578
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
5192
5579
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
5193
5580
|
})();
|
|
5194
|
-
|
|
5581
|
+
const reduceMotionOnMount = typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5582
|
+
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5195
5583
|
const cinematic = new Cinematic(orbit.camera);
|
|
5196
5584
|
const restoreContainerPosition = establishPositioningContext(container);
|
|
5197
5585
|
const labelOverlay = new LabelOverlay(container, {
|
|
@@ -5211,12 +5599,16 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5211
5599
|
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
5212
5600
|
const seatIds = new Set(sectionSeats.map((seat) => seat.id));
|
|
5213
5601
|
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
5214
|
-
const
|
|
5215
|
-
|
|
5602
|
+
const selectedIds = new Set(selection.keys());
|
|
5603
|
+
const selectedInSection = new Set(sectionSeats.filter((seat) => selectedIds.has(seat.id)).map((seat) => seat.id));
|
|
5604
|
+
const focusedBase = baseLabels.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)))));
|
|
5605
|
+
if (model.seatCount <= 6e3 && selectedInSection.size === 0) {
|
|
5216
5606
|
labelOverlay.setLabels(focusedBase);
|
|
5217
5607
|
return;
|
|
5218
5608
|
}
|
|
5219
|
-
const
|
|
5609
|
+
const labelSeats = selectedInSection.size ? sectionSeats.filter((seat) => selectedInSection.has(seat.id)) : sectionSeats;
|
|
5610
|
+
const generatedIds = new Set(labelSeats.map((seat) => `seat:${seat.id}`));
|
|
5611
|
+
const labels = labelSeats.flatMap((seat) => {
|
|
5220
5612
|
const index = model.seats.idToIndex.get(seat.id);
|
|
5221
5613
|
if (index === void 0) return [];
|
|
5222
5614
|
const offset = index * 3;
|
|
@@ -5226,12 +5618,18 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5226
5618
|
text: seat.displayLabel || seat.label,
|
|
5227
5619
|
anchor: [
|
|
5228
5620
|
model.seats.iPosition[offset],
|
|
5229
|
-
|
|
5621
|
+
// Put the active identity on the chair back. A deck-level label can
|
|
5622
|
+
// sit a full row below a foreground chair in perspective, while DOM
|
|
5623
|
+
// labels for rows behind remain visible through the WebGL geometry.
|
|
5624
|
+
model.seats.iPosition[offset + 1] + (selectedIds.has(seat.id) ? 1.05 : 0.55),
|
|
5230
5625
|
model.seats.iPosition[offset + 2]
|
|
5231
5626
|
]
|
|
5232
5627
|
}];
|
|
5233
5628
|
});
|
|
5234
|
-
labelOverlay.setLabels([
|
|
5629
|
+
labelOverlay.setLabels([
|
|
5630
|
+
...focusedBase.filter((label) => !generatedIds.has(label.id)),
|
|
5631
|
+
...labels
|
|
5632
|
+
]);
|
|
5235
5633
|
};
|
|
5236
5634
|
rebuildGpu();
|
|
5237
5635
|
const loop = new RenderLoop(() => {
|
|
@@ -5239,12 +5637,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5239
5637
|
const flying = cinematic.active;
|
|
5240
5638
|
const moving = flying ? cinematic.update(performance.now()) : orbit.update();
|
|
5241
5639
|
const lod = computeSeatLod(orbit.currentDistance, model.bounds.radius);
|
|
5640
|
+
const clippingBounds = panoramaSphereVisible ? { center: [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z], radius: 520 } : model.renderBounds;
|
|
5641
|
+
orbit.updateClipping(clippingBounds);
|
|
5642
|
+
const cameraDx = orbit.camera.position.x - model.renderBounds.center[0];
|
|
5643
|
+
const cameraDy = orbit.camera.position.y - model.renderBounds.center[1];
|
|
5644
|
+
const cameraDz = orbit.camera.position.z - model.renderBounds.center[2];
|
|
5645
|
+
const cameraVenueDistance = Math.hypot(cameraDx, cameraDy, cameraDz);
|
|
5646
|
+
const detailEnter = model.renderBounds.radius * 1.55;
|
|
5647
|
+
const detailExit = model.renderBounds.radius * 1.8;
|
|
5648
|
+
if (!nearFieldDetailEnabled) cameraDetailVisible = false;
|
|
5649
|
+
else if (cameraDetailVisible) cameraDetailVisible = cameraVenueDistance <= detailExit;
|
|
5650
|
+
else cameraDetailVisible = cameraVenueDistance <= detailEnter;
|
|
5242
5651
|
const u = gpu.seatProgram.uniforms;
|
|
5243
5652
|
u.uSeatScale.value = lod.scale;
|
|
5244
5653
|
u.uSeatFade.value = lod.fade;
|
|
5654
|
+
u.uSeatDetail.value = cameraDetailVisible ? 1 : 0;
|
|
5655
|
+
gpu.solidProgram.uniforms.uStructureDetail.value = cameraDetailVisible ? 1 : 0;
|
|
5245
5656
|
u.uMinPixels.value = lod.minPixels;
|
|
5246
5657
|
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov * DEG2 / 2) / Math.max(1, glctx.pixelHeight);
|
|
5247
|
-
updateNearField();
|
|
5658
|
+
updateNearField(cameraDetailVisible);
|
|
5248
5659
|
glctx.renderer.render({ scene: gpu.background, clear: true });
|
|
5249
5660
|
glctx.renderer.render({ scene: gpu.main, camera: orbit.camera, clear: false });
|
|
5250
5661
|
labelOverlay.update(
|
|
@@ -5268,6 +5679,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5268
5679
|
};
|
|
5269
5680
|
const { updates, next } = diffSelection(selection, ids, baseStateIndex);
|
|
5270
5681
|
selection = next;
|
|
5682
|
+
labelOverlay.setSelectedSeatIds(selection.keys());
|
|
5683
|
+
if (focusedSectionId) showSectionSeatLabels(focusedSectionId);
|
|
5271
5684
|
if (updates.length) {
|
|
5272
5685
|
const runs = applySeatStates(model.seats, updates);
|
|
5273
5686
|
if (gpu) gpu.uploadSeatStateRuns(runs);
|
|
@@ -5306,12 +5719,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5306
5719
|
model.seats.iPosition[idx * 3 + 1],
|
|
5307
5720
|
model.seats.iPosition[idx * 3 + 2]
|
|
5308
5721
|
];
|
|
5309
|
-
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0);
|
|
5722
|
+
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0, seat?.eyeHeightM);
|
|
5310
5723
|
};
|
|
5311
|
-
const placeCameraFinal = (finalPos, focal) => {
|
|
5724
|
+
const placeCameraFinal = (finalPos, focal, fov = FOV_END) => {
|
|
5312
5725
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
5313
5726
|
orbit.camera.lookAt(new Vec33(focal[0], focal[1], focal[2]));
|
|
5314
|
-
orbit.camera.fov =
|
|
5727
|
+
orbit.camera.fov = fov;
|
|
5315
5728
|
orbit.camera.updateProjectionMatrix();
|
|
5316
5729
|
};
|
|
5317
5730
|
const isNarrow = () => (container.clientWidth || glctx.canvas.clientWidth) <= 520;
|
|
@@ -5409,7 +5822,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5409
5822
|
});
|
|
5410
5823
|
}
|
|
5411
5824
|
if (opts.getSeatView) {
|
|
5412
|
-
|
|
5825
|
+
const sourceAction = opts.seatViewActionLabel?.(seatId) ?? "Open 360\xB0 panorama";
|
|
5826
|
+
addButton(retry ? `\u21BB Retry ${sourceAction}` : `\u25C9 ${sourceAction}`, `${retry ? "Retry" : sourceAction} from seat ${seatLabel}`, () => {
|
|
5413
5827
|
if (disposed || gen !== flightGen) {
|
|
5414
5828
|
removeArriveChip();
|
|
5415
5829
|
return;
|
|
@@ -5431,121 +5845,158 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5431
5845
|
return controls;
|
|
5432
5846
|
};
|
|
5433
5847
|
const navigationModeChip = document.createElement("div");
|
|
5434
|
-
|
|
5435
|
-
navigationModeChip.
|
|
5848
|
+
let seatViewLocked = false;
|
|
5849
|
+
navigationModeChip.className = "sl-3d-navigation-controls";
|
|
5436
5850
|
Object.assign(navigationModeChip.style, {
|
|
5437
5851
|
position: "absolute",
|
|
5438
|
-
|
|
5439
|
-
|
|
5852
|
+
right: "14px",
|
|
5853
|
+
bottom: "14px",
|
|
5440
5854
|
display: "flex",
|
|
5441
|
-
gap: "3px",
|
|
5442
|
-
padding: "3px",
|
|
5443
|
-
borderRadius: "999px",
|
|
5444
5855
|
zIndex: "4",
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5856
|
+
flexDirection: "column",
|
|
5857
|
+
alignItems: "flex-end",
|
|
5858
|
+
gap: "7px"
|
|
5859
|
+
});
|
|
5860
|
+
const modeControls = document.createElement("div");
|
|
5861
|
+
modeControls.setAttribute("role", "group");
|
|
5862
|
+
modeControls.setAttribute("aria-label", "3D drag mode");
|
|
5863
|
+
Object.assign(modeControls.style, {
|
|
5864
|
+
display: "flex",
|
|
5865
|
+
flexDirection: "column",
|
|
5866
|
+
gap: "6px",
|
|
5867
|
+
alignItems: "flex-end"
|
|
5868
|
+
});
|
|
5869
|
+
const utilityControls = document.createElement("div");
|
|
5870
|
+
utilityControls.setAttribute("role", "group");
|
|
5871
|
+
utilityControls.setAttribute("aria-label", "3D view controls");
|
|
5872
|
+
Object.assign(utilityControls.style, {
|
|
5873
|
+
display: "flex",
|
|
5874
|
+
flexDirection: "column",
|
|
5875
|
+
gap: "6px",
|
|
5876
|
+
alignItems: "flex-end"
|
|
5448
5877
|
});
|
|
5449
5878
|
const rotateModeButton = document.createElement("button");
|
|
5450
5879
|
rotateModeButton.type = "button";
|
|
5451
|
-
rotateModeButton.textContent = "\u21BB
|
|
5880
|
+
rotateModeButton.textContent = "\u21BB";
|
|
5452
5881
|
rotateModeButton.setAttribute("aria-label", "Drag to rotate the 3D venue");
|
|
5882
|
+
rotateModeButton.dataset.tooltip = "Rotate venue";
|
|
5453
5883
|
const moveModeButton = document.createElement("button");
|
|
5454
5884
|
moveModeButton.type = "button";
|
|
5455
|
-
moveModeButton.textContent = "\u2725
|
|
5885
|
+
moveModeButton.textContent = "\u2725";
|
|
5456
5886
|
moveModeButton.setAttribute("aria-label", "Drag to move the 3D venue left, right, up or down");
|
|
5887
|
+
moveModeButton.dataset.tooltip = "Move venue";
|
|
5457
5888
|
const zoomOutButton = document.createElement("button");
|
|
5458
5889
|
zoomOutButton.type = "button";
|
|
5459
5890
|
zoomOutButton.textContent = "\u2212";
|
|
5460
5891
|
zoomOutButton.setAttribute("aria-label", "Zoom out of the 3D venue");
|
|
5461
|
-
zoomOutButton.
|
|
5892
|
+
zoomOutButton.dataset.tooltip = "Zoom out";
|
|
5462
5893
|
const zoomInButton = document.createElement("button");
|
|
5463
5894
|
zoomInButton.type = "button";
|
|
5464
5895
|
zoomInButton.textContent = "+";
|
|
5465
5896
|
zoomInButton.setAttribute("aria-label", "Zoom into the 3D venue");
|
|
5466
|
-
zoomInButton.
|
|
5897
|
+
zoomInButton.dataset.tooltip = "Zoom in";
|
|
5467
5898
|
for (const button of [rotateModeButton, moveModeButton, zoomOutButton, zoomInButton]) {
|
|
5899
|
+
button.className = "sl-3d-icon-control";
|
|
5468
5900
|
Object.assign(button.style, {
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5901
|
+
width: "44px",
|
|
5902
|
+
minWidth: "44px",
|
|
5903
|
+
minHeight: "44px",
|
|
5904
|
+
padding: "9px",
|
|
5905
|
+
border: "1px solid rgba(150,165,205,0.35)",
|
|
5472
5906
|
borderRadius: "999px",
|
|
5473
5907
|
color: "#c9d4ea",
|
|
5474
|
-
background: "
|
|
5475
|
-
font: "600
|
|
5908
|
+
background: "rgba(12,18,32,0.82)",
|
|
5909
|
+
font: "600 17px/1 inherit",
|
|
5910
|
+
backdropFilter: "blur(6px)",
|
|
5476
5911
|
cursor: "pointer",
|
|
5477
5912
|
whiteSpace: "nowrap"
|
|
5478
5913
|
});
|
|
5479
|
-
if (button === zoomOutButton || button === zoomInButton) {
|
|
5480
|
-
button.style.minWidth = "34px";
|
|
5481
|
-
button.style.padding = "6px";
|
|
5482
|
-
button.style.fontSize = "17px";
|
|
5483
|
-
}
|
|
5484
|
-
navigationModeChip.appendChild(button);
|
|
5485
5914
|
}
|
|
5915
|
+
modeControls.append(rotateModeButton, moveModeButton);
|
|
5916
|
+
utilityControls.append(zoomInButton, zoomOutButton);
|
|
5486
5917
|
const setNavigationMode = (mode) => {
|
|
5487
5918
|
orbit.setPrimaryDragMode(mode);
|
|
5488
5919
|
const rotateActive = mode === "orbit";
|
|
5489
5920
|
rotateModeButton.setAttribute("aria-pressed", String(rotateActive));
|
|
5490
5921
|
moveModeButton.setAttribute("aria-pressed", String(!rotateActive));
|
|
5491
|
-
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.
|
|
5492
|
-
moveModeButton.style.background = rotateActive ? "
|
|
5922
|
+
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.68)" : "rgba(12,18,32,0.82)";
|
|
5923
|
+
moveModeButton.style.background = rotateActive ? "rgba(12,18,32,0.82)" : "rgba(96,110,150,0.68)";
|
|
5493
5924
|
navigationModeChip.title = rotateActive ? "Drag to rotate \xB7 Shift-drag to move" : "Drag to move \xB7 Shift-drag to rotate";
|
|
5494
5925
|
};
|
|
5495
5926
|
rotateModeButton.addEventListener("click", () => setNavigationMode("orbit"));
|
|
5496
5927
|
moveModeButton.addEventListener("click", () => setNavigationMode("pan"));
|
|
5497
5928
|
zoomOutButton.addEventListener("click", () => orbit.zoomBy(1.22));
|
|
5498
5929
|
zoomInButton.addEventListener("click", () => orbit.zoomBy(0.82));
|
|
5930
|
+
navigationModeChip.append(modeControls, utilityControls);
|
|
5499
5931
|
setNavigationMode("orbit");
|
|
5500
5932
|
container.appendChild(navigationModeChip);
|
|
5501
5933
|
const layoutNavigationChip = () => {
|
|
5502
|
-
|
|
5934
|
+
const narrow = isNarrow();
|
|
5935
|
+
navigationModeChip.style.display = seatViewLocked ? "none" : "flex";
|
|
5936
|
+
navigationModeChip.style.right = narrow ? "12px" : "14px";
|
|
5937
|
+
navigationModeChip.style.bottom = narrow ? "calc(env(safe-area-inset-bottom, 0px) + 76px)" : "14px";
|
|
5938
|
+
modeControls.style.display = narrow ? "none" : "flex";
|
|
5939
|
+
zoomInButton.style.display = narrow ? "none" : "";
|
|
5940
|
+
zoomOutButton.style.display = narrow ? "none" : "";
|
|
5503
5941
|
};
|
|
5504
5942
|
layoutNavigationChip();
|
|
5943
|
+
const setSeatViewLocked = (locked) => {
|
|
5944
|
+
seatViewLocked = locked;
|
|
5945
|
+
orbit.setInteractionEnabled(!locked);
|
|
5946
|
+
layoutNavigationChip();
|
|
5947
|
+
};
|
|
5505
5948
|
const overviewChip = document.createElement("button");
|
|
5949
|
+
overviewChip.className = "sl-3d-overview-control sl-3d-icon-control";
|
|
5506
5950
|
overviewChip.type = "button";
|
|
5507
|
-
overviewChip.textContent = "\u2302
|
|
5508
|
-
overviewChip.setAttribute("aria-label", "
|
|
5951
|
+
overviewChip.textContent = "\u2302";
|
|
5952
|
+
overviewChip.setAttribute("aria-label", "Fit the whole venue in view");
|
|
5953
|
+
overviewChip.dataset.tooltip = "Fit venue";
|
|
5509
5954
|
Object.assign(overviewChip.style, {
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
padding: "8px 14px",
|
|
5955
|
+
width: "44px",
|
|
5956
|
+
minWidth: "44px",
|
|
5957
|
+
minHeight: "44px",
|
|
5958
|
+
padding: "9px",
|
|
5515
5959
|
borderRadius: "999px",
|
|
5516
5960
|
background: "rgba(12,18,32,0.72)",
|
|
5517
5961
|
color: "#c9d4ea",
|
|
5518
5962
|
border: "1px solid rgba(150,165,205,0.35)",
|
|
5519
5963
|
backdropFilter: "blur(6px)",
|
|
5520
|
-
font: "600
|
|
5964
|
+
font: "600 11px/1 inherit",
|
|
5521
5965
|
cursor: "pointer",
|
|
5522
|
-
|
|
5966
|
+
whiteSpace: "nowrap"
|
|
5523
5967
|
});
|
|
5968
|
+
const layoutOverviewChip = () => {
|
|
5969
|
+
overviewChip.textContent = "\u2302";
|
|
5970
|
+
};
|
|
5971
|
+
layoutOverviewChip();
|
|
5524
5972
|
const focusOverview = () => {
|
|
5525
5973
|
if (disposed) return;
|
|
5974
|
+
opts.onViewTargetChange?.(null);
|
|
5975
|
+
opts.onSectionFocusChange?.(null);
|
|
5526
5976
|
if (panorama) {
|
|
5527
5977
|
panorama.dispose();
|
|
5528
5978
|
panorama = null;
|
|
5529
5979
|
analytics.panoramaClosed();
|
|
5530
5980
|
}
|
|
5981
|
+
panoramaSphereVisible = false;
|
|
5531
5982
|
panoramaLoadAbort?.abort();
|
|
5532
5983
|
panoramaLoadAbort = null;
|
|
5533
5984
|
restorePanoramaLayer();
|
|
5534
5985
|
overviewChip.style.display = "";
|
|
5535
5986
|
labelOverlay.setVisible(true);
|
|
5536
5987
|
frozen = false;
|
|
5988
|
+
setNearFieldDetailEnabled(false);
|
|
5989
|
+
setSeatViewLocked(false);
|
|
5537
5990
|
cancelFlight();
|
|
5538
5991
|
removeArriveChip();
|
|
5539
5992
|
setNavigationMode("orbit");
|
|
5540
5993
|
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5541
5994
|
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
5542
|
-
opts.onViewTargetChange?.(null);
|
|
5543
5995
|
showSectionSeatLabels(null);
|
|
5544
|
-
opts.onSectionFocusChange?.(null);
|
|
5545
5996
|
loop.requestRender();
|
|
5546
5997
|
};
|
|
5547
5998
|
overviewChip.addEventListener("click", focusOverview);
|
|
5548
|
-
|
|
5999
|
+
utilityControls.appendChild(overviewChip);
|
|
5549
6000
|
const openPanorama = async (seatId, fadeMs, gen) => {
|
|
5550
6001
|
panoramaLoadAbort?.abort();
|
|
5551
6002
|
const loadAbort = new AbortController();
|
|
@@ -5591,9 +6042,10 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5591
6042
|
loadAbort.abort();
|
|
5592
6043
|
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5593
6044
|
panorama = null;
|
|
6045
|
+
panoramaSphereVisible = false;
|
|
5594
6046
|
restorePanoramaLayer();
|
|
5595
6047
|
overviewChip.style.display = "";
|
|
5596
|
-
labelOverlay.setVisible(
|
|
6048
|
+
labelOverlay.setVisible(!opts.arriveAtSeatEye);
|
|
5597
6049
|
frozen = false;
|
|
5598
6050
|
analytics.panoramaClosed();
|
|
5599
6051
|
orbit.resumeAfterFlight(seatPose.focal);
|
|
@@ -5604,6 +6056,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5604
6056
|
const sceneMode = view.generated === true;
|
|
5605
6057
|
const disclosure = seatViewDisclosure(view);
|
|
5606
6058
|
raisePanoramaLayer();
|
|
6059
|
+
panoramaSphereVisible = !sceneMode;
|
|
5607
6060
|
overviewChip.style.display = "none";
|
|
5608
6061
|
const effectiveFadeMs = reducedMotion() ? 0 : fadeMs;
|
|
5609
6062
|
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
@@ -5613,9 +6066,17 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5613
6066
|
requestRender: () => loop.requestRender(),
|
|
5614
6067
|
cameraOriginWorld: sceneMode ? seatPose.eye : void 0,
|
|
5615
6068
|
focalWorld: seatPose.focal
|
|
5616
|
-
}, {
|
|
6069
|
+
}, {
|
|
6070
|
+
fadeMs: effectiveFadeMs,
|
|
6071
|
+
seatLabel,
|
|
6072
|
+
disclosure,
|
|
6073
|
+
disclosurePrefix: sceneMode ? "Live seat-eye view" : "360\xB0 panorama",
|
|
6074
|
+
onClose,
|
|
6075
|
+
signal: loadAbort.signal
|
|
6076
|
+
}) : null;
|
|
5617
6077
|
if (disposed || gen !== flightGen) {
|
|
5618
6078
|
loadAbort.abort();
|
|
6079
|
+
panoramaSphereVisible = false;
|
|
5619
6080
|
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
5620
6081
|
spherical?.dispose();
|
|
5621
6082
|
overviewChip.style.display = "";
|
|
@@ -5628,7 +6089,21 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5628
6089
|
frozen = false;
|
|
5629
6090
|
loop.requestRender();
|
|
5630
6091
|
panorama = spherical;
|
|
6092
|
+
} else if (sceneMode) {
|
|
6093
|
+
restorePanoramaLayer();
|
|
6094
|
+
overviewChip.style.display = "";
|
|
6095
|
+
frozen = false;
|
|
6096
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
6097
|
+
loop.requestRender();
|
|
6098
|
+
panorama = null;
|
|
6099
|
+
panoramaSphereVisible = false;
|
|
6100
|
+
analytics.panoramaFailed(seatId, "mount");
|
|
6101
|
+
const controls = showArriveChip(seatId, flightGen, true);
|
|
6102
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
6103
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6104
|
+
return;
|
|
5631
6105
|
} else {
|
|
6106
|
+
panoramaSphereVisible = false;
|
|
5632
6107
|
analytics.panoramaFallback(seatId);
|
|
5633
6108
|
panorama = mountPanorama(container, view, {
|
|
5634
6109
|
fadeMs: effectiveFadeMs,
|
|
@@ -5653,36 +6128,45 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5653
6128
|
if (disposed || !gpu) return Promise.resolve();
|
|
5654
6129
|
const idx = model.seats.idToIndex.get(seatId);
|
|
5655
6130
|
if (idx === void 0) return Promise.resolve();
|
|
6131
|
+
setNearFieldDetailEnabled(true);
|
|
5656
6132
|
opts.onViewTargetChange?.(seatId);
|
|
5657
6133
|
if (panorama) {
|
|
5658
6134
|
panorama.dispose();
|
|
5659
6135
|
panorama = null;
|
|
6136
|
+
panoramaSphereVisible = false;
|
|
5660
6137
|
overviewChip.style.display = "";
|
|
5661
6138
|
restorePanoramaLayer();
|
|
5662
6139
|
}
|
|
5663
6140
|
panoramaLoadAbort?.abort();
|
|
5664
6141
|
panoramaLoadAbort = null;
|
|
5665
6142
|
frozen = false;
|
|
6143
|
+
setSeatViewLocked(false);
|
|
5666
6144
|
const gen = ++flightGen;
|
|
5667
6145
|
removeArriveChip();
|
|
5668
6146
|
const { eye: seatEye, focal } = resolvedSeatViewPose(seatId, idx);
|
|
5669
6147
|
const start = [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z];
|
|
5670
|
-
const
|
|
6148
|
+
const flight = buildWaypoints(start, seatEye, focal, model.bounds.center, model.bounds.radius);
|
|
6149
|
+
const finalPos = opts.arriveAtSeatEye ? seatEye : flight.finalPos;
|
|
6150
|
+
const arrivalFov = opts.arriveAtSeatEye ? container.clientWidth / Math.max(1, container.clientHeight) < 0.7 ? 90 : 65 : FOV_END;
|
|
6151
|
+
const waypoints = [flight.waypoints[0], flight.waypoints[1], finalPos];
|
|
6152
|
+
labelOverlay.setVisible(false);
|
|
5671
6153
|
if (reducedMotion()) {
|
|
5672
|
-
placeCameraFinal(finalPos, focal);
|
|
6154
|
+
placeCameraFinal(finalPos, focal, arrivalFov);
|
|
5673
6155
|
loop.requestRender();
|
|
5674
6156
|
analytics.cinematicSkipped();
|
|
5675
6157
|
if (!disposed) orbit.syncFromCamera();
|
|
6158
|
+
setSeatViewLocked(opts.arriveAtSeatEye === true);
|
|
5676
6159
|
showArriveChip(seatId, gen);
|
|
5677
6160
|
return Promise.resolve();
|
|
5678
6161
|
}
|
|
5679
6162
|
const startQuat = new Quat2().copy(orbit.camera.quaternion);
|
|
5680
6163
|
const endQuat = lookAtQuat(orbit.camera, finalPos, focal);
|
|
5681
6164
|
loop.requestRender();
|
|
5682
|
-
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
6165
|
+
return cinematic.start(waypoints, startQuat, endQuat, FLIGHT_DURATION_MS, arrivalFov).then(() => {
|
|
5683
6166
|
if (disposed || gen !== flightGen) return;
|
|
5684
6167
|
analytics.cinematicPlayed(FLIGHT_DURATION_MS);
|
|
5685
6168
|
orbit.resumeAfterFlight(focal);
|
|
6169
|
+
setSeatViewLocked(opts.arriveAtSeatEye === true);
|
|
5686
6170
|
loop.requestRender();
|
|
5687
6171
|
showArriveChip(seatId, gen);
|
|
5688
6172
|
});
|
|
@@ -5690,6 +6174,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5690
6174
|
const focusSectionCamera = (sectionId) => {
|
|
5691
6175
|
const sec = model.sections.find((candidate) => candidate.id === sectionId);
|
|
5692
6176
|
if (!sec || sec.seatCount === 0) return false;
|
|
6177
|
+
setNearFieldDetailEnabled(true);
|
|
6178
|
+
setSeatViewLocked(false);
|
|
5693
6179
|
cinematic.cancel();
|
|
5694
6180
|
removeArriveChip();
|
|
5695
6181
|
setNavigationMode("pan");
|
|
@@ -5704,6 +6190,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5704
6190
|
};
|
|
5705
6191
|
let downX = 0, downY = 0, downT = 0, downId = -1, moved = false, suppressTap = false;
|
|
5706
6192
|
const onDown = (e) => {
|
|
6193
|
+
if (seatViewLocked) return;
|
|
5707
6194
|
if (downId !== -1) return;
|
|
5708
6195
|
downId = e.pointerId;
|
|
5709
6196
|
downX = e.clientX;
|
|
@@ -5714,6 +6201,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5714
6201
|
if (cinematic.active) {
|
|
5715
6202
|
analytics.cinematicCancelled();
|
|
5716
6203
|
cancelFlight();
|
|
6204
|
+
opts.onViewTargetChange?.(null);
|
|
5717
6205
|
}
|
|
5718
6206
|
};
|
|
5719
6207
|
const onMove = (e) => {
|
|
@@ -5764,6 +6252,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5764
6252
|
}
|
|
5765
6253
|
return bestIndex;
|
|
5766
6254
|
};
|
|
6255
|
+
const pickVisibleGeometry = (clientX, clientY) => {
|
|
6256
|
+
if (!gpu || !pick) return -1;
|
|
6257
|
+
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
6258
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
6259
|
+
const dpr = glctx.renderer.dpr;
|
|
6260
|
+
const { x, y } = pickPixelCoords(
|
|
6261
|
+
clientX,
|
|
6262
|
+
clientY,
|
|
6263
|
+
rect,
|
|
6264
|
+
dpr,
|
|
6265
|
+
glctx.gl.drawingBufferWidth,
|
|
6266
|
+
glctx.gl.drawingBufferHeight
|
|
6267
|
+
);
|
|
6268
|
+
const radius = Math.max(2, Math.round(8 * dpr));
|
|
6269
|
+
return pick.pick(orbit.camera, x, y, radius);
|
|
6270
|
+
};
|
|
5767
6271
|
const onUp = (e) => {
|
|
5768
6272
|
if (e.pointerId !== downId) return;
|
|
5769
6273
|
const isTap = !moved && performance.now() - downT < TAP_MS;
|
|
@@ -5773,7 +6277,15 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5773
6277
|
return;
|
|
5774
6278
|
}
|
|
5775
6279
|
if (!isTap || !gpu || !pick) return;
|
|
5776
|
-
let idx =
|
|
6280
|
+
let idx = pickVisibleGeometry(e.clientX, e.clientY);
|
|
6281
|
+
if (focusedSectionId && idx >= 0) {
|
|
6282
|
+
const visibleSeatId = seatIdByIndex[idx];
|
|
6283
|
+
const visibleSectionId = visibleSeatId ? sectionIdBySeatId.get(visibleSeatId) : void 0;
|
|
6284
|
+
if (visibleSectionId && visibleSectionId !== focusedSectionId && focusSectionCamera(visibleSectionId)) return;
|
|
6285
|
+
}
|
|
6286
|
+
if (focusedSectionId && idx < 0) {
|
|
6287
|
+
idx = pickNearestProjectedSeat(e.clientX, e.clientY, focusedSectionId, 44, false);
|
|
6288
|
+
}
|
|
5777
6289
|
if (focusedSectionId && idx < 0) {
|
|
5778
6290
|
const sectionIndex = pickNearestProjectedSeat(e.clientX, e.clientY, null, 72, false);
|
|
5779
6291
|
const sectionSeatId = sectionIndex >= 0 ? seatIdByIndex[sectionIndex] : void 0;
|
|
@@ -5781,24 +6293,30 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5781
6293
|
if (nextSectionId && nextSectionId !== focusedSectionId && focusSectionCamera(nextSectionId)) return;
|
|
5782
6294
|
}
|
|
5783
6295
|
if (!focusedSectionId) {
|
|
5784
|
-
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
5785
|
-
const rect = glctx.canvas.getBoundingClientRect();
|
|
5786
|
-
const dpr = glctx.renderer.dpr;
|
|
5787
|
-
const { x, y } = pickPixelCoords(e.clientX, e.clientY, rect, dpr, glctx.gl.drawingBufferWidth, glctx.gl.drawingBufferHeight);
|
|
5788
|
-
const radius = Math.max(2, Math.round(8 * dpr));
|
|
5789
|
-
idx = pick.pick(orbit.camera, x, y, radius);
|
|
5790
6296
|
if (idx < 0) idx = pickNearestProjectedSeat(e.clientX, e.clientY, null, 42, false);
|
|
5791
6297
|
const overviewSeatId = idx >= 0 ? seatIdByIndex[idx] : void 0;
|
|
5792
6298
|
const sectionId = overviewSeatId ? sectionIdBySeatId.get(overviewSeatId) : void 0;
|
|
5793
6299
|
if (sectionId && focusSectionCamera(sectionId)) return;
|
|
5794
6300
|
}
|
|
5795
6301
|
if (idx < 0 || idx >= seatIdByIndex.length) {
|
|
5796
|
-
if (selection.size)
|
|
6302
|
+
if (selection.size) {
|
|
6303
|
+
setSelection([]);
|
|
6304
|
+
opts.onViewTargetChange?.(null);
|
|
6305
|
+
}
|
|
5797
6306
|
return;
|
|
5798
6307
|
}
|
|
5799
6308
|
const seatId = seatIdByIndex[idx];
|
|
5800
|
-
|
|
5801
|
-
|
|
6309
|
+
const seatState = SEAT_STATES[Math.round(model.seats.iState[idx] ?? 0)] ?? "available";
|
|
6310
|
+
if (seatState !== "available" && seatState !== "selected") {
|
|
6311
|
+
opts.onSeatInspect?.(seatId, seatState);
|
|
6312
|
+
return;
|
|
6313
|
+
}
|
|
6314
|
+
if (selection.has(seatId) && selection.size === 1) {
|
|
6315
|
+
setSelection([]);
|
|
6316
|
+
opts.onViewTargetChange?.(null);
|
|
6317
|
+
return;
|
|
6318
|
+
}
|
|
6319
|
+
setSelection([seatId]);
|
|
5802
6320
|
ensureSeatView(seatId);
|
|
5803
6321
|
analytics.seatPicked(seatId, sectionIdBySeatId.get(seatId));
|
|
5804
6322
|
opts.onSeatPick?.(seatId);
|
|
@@ -5823,6 +6341,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5823
6341
|
orbit.setAspect(width / Math.max(1, height));
|
|
5824
6342
|
layoutArriveChip();
|
|
5825
6343
|
layoutNavigationChip();
|
|
6344
|
+
layoutOverviewChip();
|
|
5826
6345
|
loop.requestRender();
|
|
5827
6346
|
},
|
|
5828
6347
|
stats() {
|
|
@@ -5841,6 +6360,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5841
6360
|
focusFloor(index) {
|
|
5842
6361
|
if (index !== null && !model.floors.some((f) => f.index === index)) return false;
|
|
5843
6362
|
const value = index ?? -1;
|
|
6363
|
+
setNearFieldDetailEnabled(false);
|
|
6364
|
+
setSeatViewLocked(false);
|
|
5844
6365
|
if (gpu) {
|
|
5845
6366
|
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
5846
6367
|
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
@@ -5864,6 +6385,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5864
6385
|
focusZone(zoneId) {
|
|
5865
6386
|
const zone = model.zones.find((z) => z.id === zoneId);
|
|
5866
6387
|
if (!zone || zone.seatCount === 0) return false;
|
|
6388
|
+
setNearFieldDetailEnabled(false);
|
|
6389
|
+
setSeatViewLocked(false);
|
|
5867
6390
|
cinematic.cancel();
|
|
5868
6391
|
setNavigationMode("orbit");
|
|
5869
6392
|
showSectionSeatLabels(null);
|
|
@@ -5890,6 +6413,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5890
6413
|
focusRow(rowId) {
|
|
5891
6414
|
const row = model.rows.find((candidate) => candidate.id === rowId);
|
|
5892
6415
|
if (!row || row.seatCount === 0) return false;
|
|
6416
|
+
setNearFieldDetailEnabled(true);
|
|
6417
|
+
setSeatViewLocked(false);
|
|
5893
6418
|
cinematic.cancel();
|
|
5894
6419
|
setNavigationMode("pan");
|
|
5895
6420
|
if (row.sectionId) {
|