@seatlayer/core 0.48.1 → 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.cjs
CHANGED
|
@@ -81,8 +81,8 @@ var STRUCTURE = {
|
|
|
81
81
|
* so an organizer's own stage colour survives — it is just no longer lit like
|
|
82
82
|
* a basement.
|
|
83
83
|
*/
|
|
84
|
-
stageTop: [0.
|
|
85
|
-
stageWall: [0.
|
|
84
|
+
stageTop: [0.94, 0.7, 0.38],
|
|
85
|
+
stageWall: [0.48, 0.33, 0.18],
|
|
86
86
|
decorTop: [0.22, 0.25, 0.29],
|
|
87
87
|
decorWall: [0.15, 0.17, 0.2],
|
|
88
88
|
gaTop: [0.24, 0.28, 0.33],
|
|
@@ -236,6 +236,10 @@ var OrbitCamera = class {
|
|
|
236
236
|
this.minDist = 1;
|
|
237
237
|
this.maxDist = 100;
|
|
238
238
|
this.gestureFired = false;
|
|
239
|
+
/** Seat-eye arrival is a fixed physical origin. The buyer may opt into the
|
|
240
|
+
* dedicated look-around viewer, but venue orbit/dolly/pan must not pull the
|
|
241
|
+
* camera outside the selected seat. */
|
|
242
|
+
this.interactionEnabled = true;
|
|
239
243
|
this.dragging = false;
|
|
240
244
|
this.panning = false;
|
|
241
245
|
this.lastX = 0;
|
|
@@ -258,6 +262,7 @@ var OrbitCamera = class {
|
|
|
258
262
|
this.requestRender = requestRender;
|
|
259
263
|
this.onGesture = onGesture;
|
|
260
264
|
this.onPointerDown = (e) => {
|
|
265
|
+
if (!this.interactionEnabled) return;
|
|
261
266
|
try {
|
|
262
267
|
this.canvas.setPointerCapture?.(e.pointerId);
|
|
263
268
|
} catch {
|
|
@@ -280,6 +285,7 @@ var OrbitCamera = class {
|
|
|
280
285
|
}
|
|
281
286
|
};
|
|
282
287
|
this.onPointerMove = (e) => {
|
|
288
|
+
if (!this.interactionEnabled) return;
|
|
283
289
|
if (!this.activePointers.has(e.pointerId)) return;
|
|
284
290
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
285
291
|
if (this.activePointers.size >= 2) {
|
|
@@ -331,12 +337,14 @@ var OrbitCamera = class {
|
|
|
331
337
|
};
|
|
332
338
|
this.onWheel = (e) => {
|
|
333
339
|
e.preventDefault();
|
|
340
|
+
if (!this.interactionEnabled) return;
|
|
334
341
|
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
335
342
|
const norm2 = Math.max(-2, Math.min(2, e.deltaY * unit / 100));
|
|
336
343
|
this.dollyBy(Math.exp(norm2 * 0.22));
|
|
337
344
|
this.fireGesture();
|
|
338
345
|
};
|
|
339
346
|
this.onKeyDown = (e) => {
|
|
347
|
+
if (!this.interactionEnabled) return;
|
|
340
348
|
const step = 7 * DEG;
|
|
341
349
|
if (e.shiftKey && (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
342
350
|
this.panBy(
|
|
@@ -434,6 +442,7 @@ var OrbitCamera = class {
|
|
|
434
442
|
/** Programmatic zoom used by the visible camera controls. Keeping it on the
|
|
435
443
|
* same dolly path as wheel, pinch and keyboard preserves all distance limits. */
|
|
436
444
|
zoomBy(factor) {
|
|
445
|
+
if (!this.interactionEnabled) return;
|
|
437
446
|
this.dollyBy(factor);
|
|
438
447
|
this.fireGesture();
|
|
439
448
|
}
|
|
@@ -443,6 +452,15 @@ var OrbitCamera = class {
|
|
|
443
452
|
setPrimaryDragMode(mode) {
|
|
444
453
|
this.primaryDragMode = mode;
|
|
445
454
|
}
|
|
455
|
+
/** Enable venue navigation or pin the current camera pose for seat-eye mode. */
|
|
456
|
+
setInteractionEnabled(enabled) {
|
|
457
|
+
this.interactionEnabled = enabled;
|
|
458
|
+
if (enabled) return;
|
|
459
|
+
this.activePointers.clear();
|
|
460
|
+
this.dragging = false;
|
|
461
|
+
this.panning = false;
|
|
462
|
+
this.pinchDist = 0;
|
|
463
|
+
}
|
|
446
464
|
/**
|
|
447
465
|
* Fit a flattering 3/4 view to the bounds sphere. With `intro`, the camera
|
|
448
466
|
* STARTS nearly top-down (matching the 2D map's orientation) and further out,
|
|
@@ -499,6 +517,26 @@ var OrbitCamera = class {
|
|
|
499
517
|
this.polT = 55 * DEG;
|
|
500
518
|
this.distT = fit * FRAME_MARGIN;
|
|
501
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Tighten the depth range around the actual venue bounds for the current pose.
|
|
522
|
+
*
|
|
523
|
+
* A fixed 0.1..5000 m frustum gives an ordinary arena centimetre-scale depth
|
|
524
|
+
* precision at overview distance—coarser than the separation between a row
|
|
525
|
+
* tread and its structural cap. The resulting z quantisation is the black
|
|
526
|
+
* radial comb previously seen while zooming. A padded bounds sphere contains
|
|
527
|
+
* every venue solid while preserving useful precision outside the bowl; when
|
|
528
|
+
* the camera enters that sphere, the near plane safely returns to 5 cm.
|
|
529
|
+
*/
|
|
530
|
+
updateClipping(bounds) {
|
|
531
|
+
const dx = this.camera.position.x - bounds.center[0];
|
|
532
|
+
const dy = this.camera.position.y - bounds.center[1];
|
|
533
|
+
const dz = this.camera.position.z - bounds.center[2];
|
|
534
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
535
|
+
const paddedRadius = Math.max(1, bounds.radius) * 1.18;
|
|
536
|
+
const near = Math.max(0.05, distance - paddedRadius);
|
|
537
|
+
const far = Math.max(near + 10, distance + paddedRadius);
|
|
538
|
+
this.camera.perspective({ near, far, aspect: this.camera.aspect });
|
|
539
|
+
}
|
|
502
540
|
/** Damp toward targets; returns true while still moving. */
|
|
503
541
|
update() {
|
|
504
542
|
const da = this.azT - this.azimuth;
|
|
@@ -617,8 +655,12 @@ var RenderLoop = class {
|
|
|
617
655
|
// src/view3d/lod.ts
|
|
618
656
|
var SEAT_MIN_PIXELS_NEAR = 2.5;
|
|
619
657
|
var SEAT_MIN_PIXELS_FAR = 1.15;
|
|
620
|
-
var CHAIR_FULL_M =
|
|
621
|
-
var CHAIR_NONE_M =
|
|
658
|
+
var CHAIR_FULL_M = 6;
|
|
659
|
+
var CHAIR_NONE_M = 10;
|
|
660
|
+
var OCCUPANT_FULL_M = 6;
|
|
661
|
+
var OCCUPANT_NONE_M = 9.5;
|
|
662
|
+
var OCCUPANT_NEAR_NONE_M = 0.9;
|
|
663
|
+
var OCCUPANT_NEAR_FULL_M = 2.2;
|
|
622
664
|
var CHAIR_GATHER_M = 30;
|
|
623
665
|
var CHAIR_REBUILD_M = 4;
|
|
624
666
|
var CHAIR_MAX_INSTANCES = 8192;
|
|
@@ -874,13 +916,19 @@ var MeshBuilder = class {
|
|
|
874
916
|
this.nor = new F32Buffer();
|
|
875
917
|
this.col = new F32Buffer();
|
|
876
918
|
this.flr = new F32Buffer();
|
|
919
|
+
this.mat = new F32Buffer();
|
|
877
920
|
/** Floor index stamped onto every triangle emitted from now on. */
|
|
878
921
|
this.currentFloor = 0;
|
|
922
|
+
this.currentMaterial = 0;
|
|
879
923
|
}
|
|
880
924
|
/** Stamp subsequent triangles as belonging to `index`. */
|
|
881
925
|
setFloor(index) {
|
|
882
926
|
this.currentFloor = index;
|
|
883
927
|
}
|
|
928
|
+
/** Stamp subsequent triangles with a small renderer material class. */
|
|
929
|
+
setMaterial(index) {
|
|
930
|
+
this.currentMaterial = index;
|
|
931
|
+
}
|
|
884
932
|
/** One triangle with a shared (flat) normal and per-vertex colours. */
|
|
885
933
|
tri(p0, p1, p2, n, c0, c1 = c0, c2 = c0) {
|
|
886
934
|
if (isDegenerate(p0, p1, p2)) return;
|
|
@@ -896,10 +944,13 @@ var MeshBuilder = class {
|
|
|
896
944
|
this.flr.push1(this.currentFloor);
|
|
897
945
|
this.flr.push1(this.currentFloor);
|
|
898
946
|
this.flr.push1(this.currentFloor);
|
|
947
|
+
this.mat.push1(this.currentMaterial);
|
|
948
|
+
this.mat.push1(this.currentMaterial);
|
|
949
|
+
this.mat.push1(this.currentMaterial);
|
|
899
950
|
}
|
|
900
951
|
/** One triangle with independent per-vertex normals (smooth shading). */
|
|
901
952
|
triN(p0, p1, p2, n0, n1, n2, c0, c1 = c0, c2 = c0) {
|
|
902
|
-
if (
|
|
953
|
+
if (![...p0, ...p1, ...p2].every(Number.isFinite)) return;
|
|
903
954
|
this.pos.push3(p0[0], p0[1], p0[2]);
|
|
904
955
|
this.pos.push3(p1[0], p1[1], p1[2]);
|
|
905
956
|
this.pos.push3(p2[0], p2[1], p2[2]);
|
|
@@ -912,6 +963,9 @@ var MeshBuilder = class {
|
|
|
912
963
|
this.flr.push1(this.currentFloor);
|
|
913
964
|
this.flr.push1(this.currentFloor);
|
|
914
965
|
this.flr.push1(this.currentFloor);
|
|
966
|
+
this.mat.push1(this.currentMaterial);
|
|
967
|
+
this.mat.push1(this.currentMaterial);
|
|
968
|
+
this.mat.push1(this.currentMaterial);
|
|
915
969
|
}
|
|
916
970
|
get vertexCount() {
|
|
917
971
|
return this.pos.length / 3;
|
|
@@ -922,6 +976,7 @@ var MeshBuilder = class {
|
|
|
922
976
|
normal: this.nor.toArray(),
|
|
923
977
|
color: this.col.toArray(),
|
|
924
978
|
floor: this.flr.toArray(),
|
|
979
|
+
material: this.mat.toArray(),
|
|
925
980
|
count: this.pos.length / 3
|
|
926
981
|
};
|
|
927
982
|
}
|
|
@@ -1164,15 +1219,17 @@ function mergeMeshData(parts) {
|
|
|
1164
1219
|
const normal = new Float32Array(total * 3);
|
|
1165
1220
|
const color = new Float32Array(total * 3);
|
|
1166
1221
|
const floor = new Float32Array(total);
|
|
1222
|
+
const material = new Float32Array(total);
|
|
1167
1223
|
let off = 0;
|
|
1168
1224
|
for (const p of parts) {
|
|
1169
1225
|
position.set(p.position, off * 3);
|
|
1170
1226
|
normal.set(p.normal, off * 3);
|
|
1171
1227
|
color.set(p.color, off * 3);
|
|
1172
1228
|
floor.set(p.floor, off);
|
|
1229
|
+
material.set(p.material, off);
|
|
1173
1230
|
off += p.count;
|
|
1174
1231
|
}
|
|
1175
|
-
return { position, normal, color, floor, count: total };
|
|
1232
|
+
return { position, normal, color, floor, material, count: total };
|
|
1176
1233
|
}
|
|
1177
1234
|
function outsetRing(ring, d, miterLimit = 2.5) {
|
|
1178
1235
|
if (d <= 0 || ring.length < 3) return ring;
|
|
@@ -1266,7 +1323,7 @@ var BACK_RAKE_SLOPE = 0.21;
|
|
|
1266
1323
|
var PAD_BACK_GAP_M = 0.05;
|
|
1267
1324
|
var PAD_TOP_M = 0.45;
|
|
1268
1325
|
var BACK_BASE_M = PAD_TOP_M + PAD_BACK_GAP_M;
|
|
1269
|
-
var
|
|
1326
|
+
var CHAIR_BOXES = [
|
|
1270
1327
|
// Pedestal — a plain column under the pad. Without it the pad floats 0.36 m
|
|
1271
1328
|
// over the deck and the row reads as hovering trays.
|
|
1272
1329
|
{ part: CHAIR_PART.pedestal, min: [-0.3, 0, -0.3], max: [0.3, 0.36, 0.3] },
|
|
@@ -1281,39 +1338,22 @@ var BOXES = [
|
|
|
1281
1338
|
part: CHAIR_PART.back,
|
|
1282
1339
|
min: [-1, BACK_BASE_M, -1],
|
|
1283
1340
|
max: [1, 0.92, -0.72]
|
|
1284
|
-
},
|
|
1285
|
-
// --- the occupant ---------------------------------------------------------
|
|
1286
|
-
//
|
|
1287
|
-
// A hall with every seat empty reads as an architectural model, not a venue.
|
|
1288
|
-
// The 2048-px panorama this replaced drew a crowd; losing it was the price of
|
|
1289
|
-
// sharpness, and this is how it is bought back — in geometry, where there is
|
|
1290
|
-
// no resolution ceiling.
|
|
1291
|
-
//
|
|
1292
|
-
// Deliberately two blocks and no limbs. At the range these are visible a
|
|
1293
|
-
// person is a torso and a head, and every extra part multiplies by the number
|
|
1294
|
-
// of occupied seats in view. The silhouette is what carries it, exactly as it
|
|
1295
|
-
// does in the generated panorama's head-and-shoulder figures.
|
|
1296
|
-
//
|
|
1297
|
-
// Sized as a seated adult against the 0.92 m chair back: hips at the pad top,
|
|
1298
|
-
// shoulders just above the back panel, head clear of it. Torso is narrower
|
|
1299
|
-
// than the chair so neighbours never interpenetrate at any pitch, and it sits
|
|
1300
|
-
// forward of the back panel rather than inside it.
|
|
1301
|
-
{
|
|
1302
|
-
part: CHAIR_PART.body,
|
|
1303
|
-
min: [-0.66, PAD_TOP_M, -0.58],
|
|
1304
|
-
max: [0.66, 1, 0.26]
|
|
1305
|
-
},
|
|
1306
|
-
// The head is deliberately SMALL. Sized by eye against the chair it came out
|
|
1307
|
-
// near-cubic and read as Lego; a real head is about 0.16 m across and 0.22 m
|
|
1308
|
-
// tall, which against a 0.24 m chair half-width is roughly a third of the
|
|
1309
|
-
// chair's width and clearly taller than it is wide. Getting this ratio wrong
|
|
1310
|
-
// is what makes a crowd look like toys rather than people.
|
|
1311
|
-
{
|
|
1312
|
-
part: CHAIR_PART.head,
|
|
1313
|
-
min: [-0.34, 1.03, -0.4],
|
|
1314
|
-
max: [0.34, 1.27, 0.02]
|
|
1315
1341
|
}
|
|
1316
1342
|
];
|
|
1343
|
+
var OCCUPANT_BOXES = [
|
|
1344
|
+
// Thighs project forward from the pad. A single lap slab brought back the
|
|
1345
|
+
// same toy-block problem as the old torso, so each leg keeps its own outline.
|
|
1346
|
+
{ part: CHAIR_PART.body, min: [-0.52, 0.39, -0.08], max: [-0.08, 0.55, 0.82] },
|
|
1347
|
+
{ part: CHAIR_PART.body, min: [0.08, 0.39, -0.08], max: [0.52, 0.55, 0.82] },
|
|
1348
|
+
// Lower legs drop from the knees, making the pose unmistakably seated from
|
|
1349
|
+
// the side and from the elevated arena camera.
|
|
1350
|
+
{ part: CHAIR_PART.body, min: [-0.47, 0.05, 0.58], max: [-0.13, 0.43, 0.82] },
|
|
1351
|
+
{ part: CHAIR_PART.body, min: [0.13, 0.05, 0.58], max: [0.47, 0.43, 0.82] },
|
|
1352
|
+
// A short neck keeps the head connected to the shoulders from behind. Most
|
|
1353
|
+
// of it sits inside the torso/head overlap: a long exposed cuboid made the
|
|
1354
|
+
// close-range figure read as a mannequin on a post.
|
|
1355
|
+
{ part: CHAIR_PART.head, min: [-0.16, 1.04, -0.33], max: [0.16, 1.1, -0.13] }
|
|
1356
|
+
];
|
|
1317
1357
|
var FACES = [
|
|
1318
1358
|
// +X
|
|
1319
1359
|
{ n: [1, 0, 0], c: [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]] },
|
|
@@ -1329,63 +1369,111 @@ var FACES = [
|
|
|
1329
1369
|
{ n: [0, 0, -1], c: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]] }
|
|
1330
1370
|
];
|
|
1331
1371
|
function buildChairMesh() {
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1372
|
+
const positions = [];
|
|
1373
|
+
const normals = [];
|
|
1374
|
+
const parts = [];
|
|
1375
|
+
const indices = [];
|
|
1376
|
+
const addQuad = (corners, outward, partId) => {
|
|
1377
|
+
const base = parts.length;
|
|
1378
|
+
const [a, b, c] = corners;
|
|
1379
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
1380
|
+
const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
1381
|
+
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
|
1382
|
+
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
|
1383
|
+
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
|
1384
|
+
const length = Math.hypot(nx, ny, nz);
|
|
1385
|
+
if (length > 1e-9) {
|
|
1386
|
+
nx /= length;
|
|
1387
|
+
ny /= length;
|
|
1388
|
+
nz /= length;
|
|
1389
|
+
} else {
|
|
1390
|
+
[nx, ny, nz] = outward;
|
|
1391
|
+
}
|
|
1392
|
+
if (nx * outward[0] + ny * outward[1] + nz * outward[2] < 0) {
|
|
1393
|
+
nx = -nx;
|
|
1394
|
+
ny = -ny;
|
|
1395
|
+
nz = -nz;
|
|
1396
|
+
}
|
|
1397
|
+
for (const corner of corners) {
|
|
1398
|
+
positions.push(...corner);
|
|
1399
|
+
normals.push(nx, ny, nz);
|
|
1400
|
+
parts.push(partId);
|
|
1401
|
+
}
|
|
1402
|
+
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
1403
|
+
};
|
|
1404
|
+
const addBox = (box) => {
|
|
1341
1405
|
for (const face of FACES) {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
for (let a = 0; a < 3; a++) {
|
|
1347
|
-
corner3[ci * 3 + a] = corner[a] ? box.max[a] : box.min[a];
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
const ax = corner3[3] - corner3[0], ay = corner3[4] - corner3[1], az = corner3[5] - corner3[2];
|
|
1351
|
-
const bx = corner3[6] - corner3[0], by = corner3[7] - corner3[1], bz = corner3[8] - corner3[2];
|
|
1352
|
-
let nx = ay * bz - az * by;
|
|
1353
|
-
let ny = az * bx - ax * bz;
|
|
1354
|
-
let nz = ax * by - ay * bx;
|
|
1355
|
-
const nl = Math.hypot(nx, ny, nz);
|
|
1356
|
-
if (nl > 1e-9) {
|
|
1357
|
-
nx /= nl;
|
|
1358
|
-
ny /= nl;
|
|
1359
|
-
nz /= nl;
|
|
1360
|
-
} else {
|
|
1361
|
-
nx = face.n[0];
|
|
1362
|
-
ny = face.n[1];
|
|
1363
|
-
nz = face.n[2];
|
|
1364
|
-
}
|
|
1365
|
-
if (nx * face.n[0] + ny * face.n[1] + nz * face.n[2] < 0) {
|
|
1366
|
-
nx = -nx;
|
|
1367
|
-
ny = -ny;
|
|
1368
|
-
nz = -nz;
|
|
1369
|
-
}
|
|
1370
|
-
for (let ci = 0; ci < 4; ci++) {
|
|
1371
|
-
position[v * 3] = corner3[ci * 3];
|
|
1372
|
-
position[v * 3 + 1] = corner3[ci * 3 + 1];
|
|
1373
|
-
position[v * 3 + 2] = corner3[ci * 3 + 2];
|
|
1374
|
-
normal[v * 3] = nx;
|
|
1375
|
-
normal[v * 3 + 1] = ny;
|
|
1376
|
-
normal[v * 3 + 2] = nz;
|
|
1377
|
-
part[v] = box.part;
|
|
1378
|
-
v++;
|
|
1379
|
-
}
|
|
1380
|
-
index[t++] = base;
|
|
1381
|
-
index[t++] = base + 1;
|
|
1382
|
-
index[t++] = base + 2;
|
|
1383
|
-
index[t++] = base;
|
|
1384
|
-
index[t++] = base + 2;
|
|
1385
|
-
index[t++] = base + 3;
|
|
1406
|
+
const corners = face.c.map((corner) => corner.map(
|
|
1407
|
+
(side, axis) => side ? box.max[axis] : box.min[axis]
|
|
1408
|
+
));
|
|
1409
|
+
addQuad(corners, face.n, box.part);
|
|
1386
1410
|
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1411
|
+
};
|
|
1412
|
+
for (const box of CHAIR_BOXES) addBox(box);
|
|
1413
|
+
for (const box of OCCUPANT_BOXES) addBox(box);
|
|
1414
|
+
const waistY = PAD_TOP_M;
|
|
1415
|
+
const shoulderY = 1.08;
|
|
1416
|
+
const wb = 0.47, wt = 0.69;
|
|
1417
|
+
const backBottom = -0.55, frontBottom = 0.08;
|
|
1418
|
+
const backTop = -0.48, frontTop = 0.01;
|
|
1419
|
+
const blb = [-wb, waistY, backBottom], brb = [wb, waistY, backBottom];
|
|
1420
|
+
const blf = [-wb, waistY, frontBottom], brf = [wb, waistY, frontBottom];
|
|
1421
|
+
const tlb = [-wt, shoulderY, backTop], trb = [wt, shoulderY, backTop];
|
|
1422
|
+
const tlf = [-wt, shoulderY, frontTop], trf = [wt, shoulderY, frontTop];
|
|
1423
|
+
addQuad([brb, trb, trf, brf], [1, 0, 0], CHAIR_PART.body);
|
|
1424
|
+
addQuad([blf, tlf, tlb, blb], [-1, 0, 0], CHAIR_PART.body);
|
|
1425
|
+
addQuad([tlf, trf, trb, tlb], [0, 1, 0], CHAIR_PART.body);
|
|
1426
|
+
addQuad([blb, brb, brf, blf], [0, -1, 0], CHAIR_PART.body);
|
|
1427
|
+
addQuad([brf, trf, tlf, blf], [0, 0, 1], CHAIR_PART.body);
|
|
1428
|
+
addQuad([blb, tlb, trb, brb], [0, 0, -1], CHAIR_PART.body);
|
|
1429
|
+
const lonSegments = 10;
|
|
1430
|
+
const latSegments = 8;
|
|
1431
|
+
const centre = [0, 1.205, -0.23];
|
|
1432
|
+
const radii = [0.42, 0.115, 0.38];
|
|
1433
|
+
const headBase = parts.length;
|
|
1434
|
+
for (let lat = 0; lat <= latSegments; lat++) {
|
|
1435
|
+
const theta = Math.PI * lat / latSegments;
|
|
1436
|
+
const sy = Math.cos(theta);
|
|
1437
|
+
const ring = Math.sin(theta);
|
|
1438
|
+
for (let lon = 0; lon <= lonSegments; lon++) {
|
|
1439
|
+
const phi = Math.PI * 2 * lon / lonSegments;
|
|
1440
|
+
const dx = ring * Math.cos(phi);
|
|
1441
|
+
const dz = ring * Math.sin(phi);
|
|
1442
|
+
positions.push(
|
|
1443
|
+
centre[0] + radii[0] * dx,
|
|
1444
|
+
centre[1] + radii[1] * sy,
|
|
1445
|
+
centre[2] + radii[2] * dz
|
|
1446
|
+
);
|
|
1447
|
+
let nx = dx / radii[0];
|
|
1448
|
+
let ny = sy / radii[1];
|
|
1449
|
+
let nz = dz / radii[2];
|
|
1450
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
1451
|
+
nx /= length;
|
|
1452
|
+
ny /= length;
|
|
1453
|
+
nz /= length;
|
|
1454
|
+
normals.push(nx, ny, nz);
|
|
1455
|
+
parts.push(CHAIR_PART.head);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
for (let lat = 0; lat < latSegments; lat++) {
|
|
1459
|
+
for (let lon = 0; lon < lonSegments; lon++) {
|
|
1460
|
+
const a = headBase + lat * (lonSegments + 1) + lon;
|
|
1461
|
+
const b = a + lonSegments + 1;
|
|
1462
|
+
indices.push(a, b, a + 1, a + 1, b, b + 1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const position = new Float32Array(positions);
|
|
1466
|
+
const normal = new Float32Array(normals);
|
|
1467
|
+
const part = new Float32Array(parts);
|
|
1468
|
+
const index = new Uint16Array(indices);
|
|
1469
|
+
return {
|
|
1470
|
+
position,
|
|
1471
|
+
normal,
|
|
1472
|
+
part,
|
|
1473
|
+
index,
|
|
1474
|
+
vertexCount: parts.length,
|
|
1475
|
+
indexCount: indices.length
|
|
1476
|
+
};
|
|
1389
1477
|
}
|
|
1390
1478
|
function computeSeatYaw(iPosition, count, rowIdAt, focal) {
|
|
1391
1479
|
const yaw = new Float32Array(count);
|
|
@@ -1689,7 +1777,7 @@ function focusScore(screen, worldDistance, width, height) {
|
|
|
1689
1777
|
return worldDistance * (1 + 1.5 * off);
|
|
1690
1778
|
}
|
|
1691
1779
|
function pickDenseLabels(items, separationX, separationY, budget) {
|
|
1692
|
-
const ordered = [...items].sort((a, b) => a.focus - b.focus);
|
|
1780
|
+
const ordered = [...items].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.focus - b.focus);
|
|
1693
1781
|
const kept = [];
|
|
1694
1782
|
for (const item of ordered) {
|
|
1695
1783
|
if (kept.length >= budget) break;
|
|
@@ -2668,19 +2756,31 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2668
2756
|
const UP = [0, 1, 0];
|
|
2669
2757
|
const clipRing = clip && clip.length >= 3 ? [[...clip.map((p) => [p.x, p.y]), [clip[0].x, clip[0].y]]] : null;
|
|
2670
2758
|
const clipTest = clip && clip.length >= 3 ? new ClipTest([[...clip]]) : null;
|
|
2759
|
+
const emitRiser = (draw) => {
|
|
2760
|
+
builder.setMaterial(2);
|
|
2761
|
+
draw();
|
|
2762
|
+
builder.setMaterial(0);
|
|
2763
|
+
};
|
|
2764
|
+
const emitTread = (draw) => {
|
|
2765
|
+
builder.setMaterial(3);
|
|
2766
|
+
draw();
|
|
2767
|
+
builder.setMaterial(0);
|
|
2768
|
+
};
|
|
2671
2769
|
for (let i = 0; i < rows.length; i++) {
|
|
2672
2770
|
const row = rows[i];
|
|
2673
2771
|
if (row.patch && row.patch.length >= 3) {
|
|
2674
2772
|
const patch = [...row.patch];
|
|
2675
2773
|
const belowY2 = nbrs[i].belowY ?? landingY;
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2774
|
+
emitTread(() => {
|
|
2775
|
+
if (clipRing && clipTest) emitClippedPoly(builder, clipRing, clipTest, patch, row.y, colors.tread);
|
|
2776
|
+
else {
|
|
2777
|
+
const a = patch[0];
|
|
2778
|
+
for (let k = 1; k + 1 < patch.length; k++) {
|
|
2779
|
+
const b = patch[k], c = patch[k + 1];
|
|
2780
|
+
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);
|
|
2781
|
+
}
|
|
2682
2782
|
}
|
|
2683
|
-
}
|
|
2783
|
+
});
|
|
2684
2784
|
if (row.y > belowY2 + MIN_RISER_M) {
|
|
2685
2785
|
let cx = 0, cy = 0;
|
|
2686
2786
|
for (const p of patch) {
|
|
@@ -2689,24 +2789,26 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2689
2789
|
}
|
|
2690
2790
|
cx /= patch.length;
|
|
2691
2791
|
cy /= patch.length;
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
nx
|
|
2700
|
-
|
|
2792
|
+
emitRiser(() => {
|
|
2793
|
+
for (let k = 0; k < patch.length; k++) {
|
|
2794
|
+
const p = patch[k], q = patch[(k + 1) % patch.length];
|
|
2795
|
+
const dx = q.x - p.x, dy = q.y - p.y;
|
|
2796
|
+
const len = Math.hypot(dx, dy);
|
|
2797
|
+
if (len < 1e-6) continue;
|
|
2798
|
+
let nx = dy / len, ny = -dx / len;
|
|
2799
|
+
if ((p.x - cx) * nx + (p.y - cy) * ny < 0) {
|
|
2800
|
+
nx = -nx;
|
|
2801
|
+
ny = -ny;
|
|
2802
|
+
}
|
|
2803
|
+
const rn = [nx, 0, ny];
|
|
2804
|
+
const pt = [p.x * M, row.y, p.y * M];
|
|
2805
|
+
const qt = [q.x * M, row.y, q.y * M];
|
|
2806
|
+
const pb = [p.x * M, belowY2, p.y * M];
|
|
2807
|
+
const qb = [q.x * M, belowY2, q.y * M];
|
|
2808
|
+
builder.tri(pt, qt, qb, rn, colors.riser);
|
|
2809
|
+
builder.tri(pt, qb, pb, rn, colors.riser);
|
|
2701
2810
|
}
|
|
2702
|
-
|
|
2703
|
-
const pt = [p.x * M, row.y, p.y * M];
|
|
2704
|
-
const qt = [q.x * M, row.y, q.y * M];
|
|
2705
|
-
const pb = [p.x * M, belowY2, p.y * M];
|
|
2706
|
-
const qb = [q.x * M, belowY2, q.y * M];
|
|
2707
|
-
builder.tri(pt, qt, qb, rn, colors.riser);
|
|
2708
|
-
builder.tri(pt, qb, pb, rn, colors.riser);
|
|
2709
|
-
}
|
|
2811
|
+
});
|
|
2710
2812
|
}
|
|
2711
2813
|
continue;
|
|
2712
2814
|
}
|
|
@@ -2723,23 +2825,27 @@ function emitDeckBands(builder, rows, focal, landingY, colors, clip, shared) {
|
|
|
2723
2825
|
const qF = [(q.x - nq[0] * front) * M, row.y, (q.y - nq[1] * front) * M];
|
|
2724
2826
|
const pB = [(p.x + np[0] * back) * M, row.y, (p.y + np[1] * back) * M];
|
|
2725
2827
|
const qB = [(q.x + nq[0] * back) * M, row.y, (q.y + nq[1] * back) * M];
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2828
|
+
emitTread(() => {
|
|
2829
|
+
if (clipRing && clipTest) {
|
|
2830
|
+
emitClippedPoly(builder, clipRing, clipTest, [
|
|
2831
|
+
{ x: p.x - np[0] * front, y: p.y - np[1] * front },
|
|
2832
|
+
{ x: q.x - nq[0] * front, y: q.y - nq[1] * front },
|
|
2833
|
+
{ x: q.x + nq[0] * back, y: q.y + nq[1] * back },
|
|
2834
|
+
{ x: p.x + np[0] * back, y: p.y + np[1] * back }
|
|
2835
|
+
], row.y, colors.tread);
|
|
2836
|
+
} else {
|
|
2837
|
+
builder.tri(pF, qF, qB, UP, colors.tread);
|
|
2838
|
+
builder.tri(pF, qB, pB, UP, colors.tread);
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2737
2841
|
if (row.y > belowY + MIN_RISER_M) {
|
|
2738
2842
|
const pFd = [pF[0], belowY, pF[2]];
|
|
2739
2843
|
const qFd = [qF[0], belowY, qF[2]];
|
|
2740
2844
|
const rn = [-np[0], 0, -np[1]];
|
|
2741
|
-
|
|
2742
|
-
|
|
2845
|
+
emitRiser(() => {
|
|
2846
|
+
builder.tri(pF, qF, qFd, rn, colors.riser);
|
|
2847
|
+
builder.tri(pF, qFd, pFd, rn, colors.riser);
|
|
2848
|
+
});
|
|
2743
2849
|
}
|
|
2744
2850
|
}
|
|
2745
2851
|
}
|
|
@@ -3084,18 +3190,21 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
3084
3190
|
const blocks = clipFootprints(deckFootprints(surface.rowLevels, unit.focal, nbrs), footprint);
|
|
3085
3191
|
const capUp = () => [0, 1, 0];
|
|
3086
3192
|
for (const b of blocks) {
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3193
|
+
const baseCapY = Math.max(bottomY, b.topY - 0.02);
|
|
3194
|
+
for (const ring of claimed.subtract(b.outline, baseCapY)) {
|
|
3195
|
+
extrudePrism(
|
|
3196
|
+
builder,
|
|
3197
|
+
ring,
|
|
3198
|
+
b.holes,
|
|
3199
|
+
() => baseCapY,
|
|
3200
|
+
bottomY,
|
|
3201
|
+
colTop,
|
|
3202
|
+
S.tierWall,
|
|
3203
|
+
AO,
|
|
3204
|
+
void 0,
|
|
3205
|
+
capUp
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3099
3208
|
}
|
|
3100
3209
|
if (!blocks.length) {
|
|
3101
3210
|
extrudePrism(
|
|
@@ -3109,12 +3218,23 @@ function buildTier(builder, section, unit, fill, surface, claimed, siblings, S)
|
|
|
3109
3218
|
AO
|
|
3110
3219
|
);
|
|
3111
3220
|
}
|
|
3112
|
-
|
|
3221
|
+
const bandColors = {
|
|
3113
3222
|
tread: [colTop[0] * AO.top, colTop[1] * AO.top, colTop[2] * AO.top],
|
|
3114
3223
|
// Risers read as the structure they are, a shade below their tread, which
|
|
3115
3224
|
// is what makes the stepping legible from a low angle.
|
|
3116
3225
|
riser: [colTop[0] * 0.72, colTop[1] * 0.72, colTop[2] * 0.72]
|
|
3117
|
-
}
|
|
3226
|
+
};
|
|
3227
|
+
for (const allowed of footprint.length ? footprint : [outline]) {
|
|
3228
|
+
emitDeckBands(
|
|
3229
|
+
builder,
|
|
3230
|
+
surface.rowLevels,
|
|
3231
|
+
unit.focal,
|
|
3232
|
+
surface.landingY,
|
|
3233
|
+
bandColors,
|
|
3234
|
+
allowed,
|
|
3235
|
+
nbrs
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3118
3238
|
return;
|
|
3119
3239
|
}
|
|
3120
3240
|
const maxErr = surface.flat ? Infinity : CAP_MAX_ERROR_M;
|
|
@@ -3368,8 +3488,10 @@ function buildShape(builder, shape, base, topOffsetM, S, focal, stages) {
|
|
|
3368
3488
|
halfZ: Math.max((maxZ - minZ) / 2 * M, 0.5)
|
|
3369
3489
|
});
|
|
3370
3490
|
}
|
|
3371
|
-
const
|
|
3372
|
-
const
|
|
3491
|
+
const authoredFill = hexToRgb(shape.fill);
|
|
3492
|
+
const colTop = isStage && authoredFill ? mix(S.stageTop, scaleRgb(desaturate(authoredFill, 0.18), 0.9), 0.9) : tintTop(authoredFill, isStage ? S.stageTop : S.decorTop);
|
|
3493
|
+
const screenFill = shape.role === "screen" ? authoredFill : null;
|
|
3494
|
+
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);
|
|
3373
3495
|
if (architecture.kind === "portal" && poly.length === 4) {
|
|
3374
3496
|
const quad = orientQuadLong(poly);
|
|
3375
3497
|
emitArchitecturalPrism(builder, quadWidthSlice(quad, 0, 0.16), height, base, colTop, colWall);
|
|
@@ -3509,16 +3631,19 @@ function buildSceneModel(input) {
|
|
|
3509
3631
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
3510
3632
|
for (const o of unit.objects) {
|
|
3511
3633
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
3512
|
-
else if (o.type === "shape")
|
|
3513
|
-
builder
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3634
|
+
else if (o.type === "shape") {
|
|
3635
|
+
builder.setMaterial(o.role === "screen" ? 1 : 0);
|
|
3636
|
+
buildShape(
|
|
3637
|
+
builder,
|
|
3638
|
+
o,
|
|
3639
|
+
unit.baseHeightM,
|
|
3640
|
+
claimShapeTopOffset(o, claimedShapeLayers),
|
|
3641
|
+
S,
|
|
3642
|
+
unit.focal,
|
|
3643
|
+
stageBounds
|
|
3644
|
+
);
|
|
3645
|
+
builder.setMaterial(0);
|
|
3646
|
+
} else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3522
3647
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3523
3648
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3524
3649
|
else if (o.type === "decorImage") buildDecorImage(builder, o, unit.baseHeightM, S);
|
|
@@ -3824,10 +3949,49 @@ function buildSceneModel(input) {
|
|
|
3824
3949
|
const cx = (fp.minX + fp.maxX) / 2 * M;
|
|
3825
3950
|
const cz = (fp.minY + fp.maxY) / 2 * M;
|
|
3826
3951
|
const radius = 0.5 * Math.hypot((fp.maxX - fp.minX) * M, (fp.maxY - fp.minY) * M) || 10;
|
|
3952
|
+
let renderMinX = Infinity, renderMinY = Infinity, renderMinZ = Infinity;
|
|
3953
|
+
let renderMaxX = -Infinity, renderMaxY = -Infinity, renderMaxZ = -Infinity;
|
|
3954
|
+
const includeRenderPoint = (x, y, z) => {
|
|
3955
|
+
renderMinX = Math.min(renderMinX, x);
|
|
3956
|
+
renderMaxX = Math.max(renderMaxX, x);
|
|
3957
|
+
renderMinY = Math.min(renderMinY, y);
|
|
3958
|
+
renderMaxY = Math.max(renderMaxY, y);
|
|
3959
|
+
renderMinZ = Math.min(renderMinZ, z);
|
|
3960
|
+
renderMaxZ = Math.max(renderMaxZ, z);
|
|
3961
|
+
};
|
|
3962
|
+
for (let i = 0; i < solids.position.length; i += 3) {
|
|
3963
|
+
includeRenderPoint(solids.position[i], solids.position[i + 1], solids.position[i + 2]);
|
|
3964
|
+
}
|
|
3965
|
+
for (let i = 0; i < seatData.count; i++) {
|
|
3966
|
+
const o = i * 3;
|
|
3967
|
+
const x = seatData.iPosition[o], y = seatData.iPosition[o + 1], z = seatData.iPosition[o + 2];
|
|
3968
|
+
const r = Math.max(0.35, seatData.iMaxRadius[i] ?? 0.35);
|
|
3969
|
+
includeRenderPoint(x - r, y, z - r);
|
|
3970
|
+
includeRenderPoint(x + r, y + 1.6, z + r);
|
|
3971
|
+
}
|
|
3972
|
+
if (!Number.isFinite(renderMinX)) {
|
|
3973
|
+
renderMinX = cx - radius;
|
|
3974
|
+
renderMaxX = cx + radius;
|
|
3975
|
+
renderMinY = 0;
|
|
3976
|
+
renderMaxY = radius * 0.16;
|
|
3977
|
+
renderMinZ = cz - radius;
|
|
3978
|
+
renderMaxZ = cz + radius;
|
|
3979
|
+
}
|
|
3980
|
+
const renderCenter = [
|
|
3981
|
+
(renderMinX + renderMaxX) / 2,
|
|
3982
|
+
(renderMinY + renderMaxY) / 2,
|
|
3983
|
+
(renderMinZ + renderMaxZ) / 2
|
|
3984
|
+
];
|
|
3985
|
+
const renderRadius = Math.max(1, 0.5 * Math.hypot(
|
|
3986
|
+
renderMaxX - renderMinX,
|
|
3987
|
+
renderMaxY - renderMinY,
|
|
3988
|
+
renderMaxZ - renderMinZ
|
|
3989
|
+
));
|
|
3827
3990
|
return {
|
|
3828
3991
|
solids,
|
|
3829
3992
|
seats: seatData,
|
|
3830
3993
|
bounds: { center: [cx, radius * 0.08, cz], radius, groundY: 0 },
|
|
3994
|
+
renderBounds: { center: renderCenter, radius: renderRadius },
|
|
3831
3995
|
stateColorLUT: themeSeatColorLUT(theme, SEAT_STATES),
|
|
3832
3996
|
theme,
|
|
3833
3997
|
seatCount: seats.length,
|
|
@@ -3871,6 +4035,7 @@ var LabelOverlay = class {
|
|
|
3871
4035
|
this.nodes = /* @__PURE__ */ new Map();
|
|
3872
4036
|
this.labels = [];
|
|
3873
4037
|
this.forcedDense = false;
|
|
4038
|
+
this.selectedSeatLabelIds = /* @__PURE__ */ new Set();
|
|
3874
4039
|
this.opts = opts;
|
|
3875
4040
|
this.root = document.createElement("div");
|
|
3876
4041
|
this.root.setAttribute("data-view3d-labels", "");
|
|
@@ -3910,6 +4075,12 @@ var LabelOverlay = class {
|
|
|
3910
4075
|
setForcedDense(enabled) {
|
|
3911
4076
|
this.forcedDense = enabled;
|
|
3912
4077
|
}
|
|
4078
|
+
/** Keep the authoritative selected identities pinned above nearby dense
|
|
4079
|
+
* labels. The seat mesh and its DOM label must never describe different
|
|
4080
|
+
* rows merely because collision suppression preferred the neighbour. */
|
|
4081
|
+
setSelectedSeatIds(ids) {
|
|
4082
|
+
this.selectedSeatLabelIds = new Set(Array.from(ids, (id) => `seat:${id}`));
|
|
4083
|
+
}
|
|
3913
4084
|
/**
|
|
3914
4085
|
* Reposition every label for the current camera.
|
|
3915
4086
|
*
|
|
@@ -3932,7 +4103,12 @@ var LabelOverlay = class {
|
|
|
3932
4103
|
label.anchor[1] - cameraWorld[1],
|
|
3933
4104
|
label.anchor[2] - cameraWorld[2]
|
|
3934
4105
|
) : 1;
|
|
3935
|
-
candidates.push({
|
|
4106
|
+
candidates.push({
|
|
4107
|
+
label,
|
|
4108
|
+
screen,
|
|
4109
|
+
focus: focusScore(screen, world, width, height),
|
|
4110
|
+
...this.selectedSeatLabelIds.has(label.id) ? { priority: 1 } : {}
|
|
4111
|
+
});
|
|
3936
4112
|
}
|
|
3937
4113
|
const structure = candidates.filter((c) => !DENSE_KINDS.has(c.label.kind));
|
|
3938
4114
|
const kept = [
|
|
@@ -3960,26 +4136,35 @@ var LabelOverlay = class {
|
|
|
3960
4136
|
}
|
|
3961
4137
|
nodeFor(label) {
|
|
3962
4138
|
let node = this.nodes.get(label.id);
|
|
3963
|
-
if (node)
|
|
3964
|
-
|
|
4139
|
+
if (!node) {
|
|
4140
|
+
node = document.createElement("div");
|
|
4141
|
+
const s2 = node.style;
|
|
4142
|
+
s2.position = "absolute";
|
|
4143
|
+
s2.left = "0";
|
|
4144
|
+
s2.top = "0";
|
|
4145
|
+
s2.whiteSpace = "nowrap";
|
|
4146
|
+
s2.textShadow = "0 1px 3px rgba(0,0,0,0.85), 0 0 8px rgba(0,0,0,0.55)";
|
|
4147
|
+
s2.display = "none";
|
|
4148
|
+
this.root.appendChild(node);
|
|
4149
|
+
this.nodes.set(label.id, node);
|
|
4150
|
+
}
|
|
3965
4151
|
node.textContent = label.text;
|
|
3966
4152
|
node.setAttribute("data-label-kind", label.kind);
|
|
4153
|
+
const selected = this.selectedSeatLabelIds.has(label.id);
|
|
4154
|
+
node.toggleAttribute("data-selected-seat", selected);
|
|
3967
4155
|
const style = KIND_STYLE[label.kind];
|
|
3968
4156
|
const s = node.style;
|
|
3969
|
-
s.position = "absolute";
|
|
3970
|
-
s.left = "0";
|
|
3971
|
-
s.top = "0";
|
|
3972
|
-
s.whiteSpace = "nowrap";
|
|
3973
4157
|
s.fontSize = `${style.size}px`;
|
|
3974
|
-
s.fontWeight = style.weight;
|
|
3975
|
-
s.opacity = String(style.opacity);
|
|
3976
|
-
s.color = label.color ?? this.opts.ink ?? "#e8edf5";
|
|
3977
|
-
s.
|
|
4158
|
+
s.fontWeight = selected ? "800" : style.weight;
|
|
4159
|
+
s.opacity = selected ? "1" : String(style.opacity);
|
|
4160
|
+
s.color = selected ? "#ecf8ff" : label.color ?? this.opts.ink ?? "#e8edf5";
|
|
4161
|
+
s.background = selected ? "rgba(10,28,45,.9)" : "";
|
|
4162
|
+
s.border = selected ? "1px solid rgba(89,199,255,.92)" : "";
|
|
4163
|
+
s.borderRadius = selected ? "999px" : "";
|
|
4164
|
+
s.padding = selected ? "3px 7px" : "";
|
|
4165
|
+
s.boxShadow = selected ? "0 0 0 2px rgba(41,176,255,.18), 0 5px 14px rgba(0,0,0,.35)" : "";
|
|
3978
4166
|
s.letterSpacing = label.kind === "zone" ? "0.08em" : "0.02em";
|
|
3979
4167
|
if (label.kind === "zone") s.textTransform = "uppercase";
|
|
3980
|
-
s.display = "none";
|
|
3981
|
-
this.root.appendChild(node);
|
|
3982
|
-
this.nodes.set(label.id, node);
|
|
3983
4168
|
return node;
|
|
3984
4169
|
}
|
|
3985
4170
|
dispose() {
|
|
@@ -4000,6 +4185,15 @@ float chairWeight(float depth) {
|
|
|
4000
4185
|
return 1.0 - smoothstep(uChairFull, uChairNone, depth);
|
|
4001
4186
|
}`
|
|
4002
4187
|
);
|
|
4188
|
+
var OCCUPANT_WEIGHT_GLSL = (
|
|
4189
|
+
/* glsl */
|
|
4190
|
+
`
|
|
4191
|
+
float occupantWeight(float depth) {
|
|
4192
|
+
float nearWeight = smoothstep(${OCCUPANT_NEAR_NONE_M.toFixed(1)}, ${OCCUPANT_NEAR_FULL_M.toFixed(1)}, depth);
|
|
4193
|
+
float farWeight = 1.0 - smoothstep(${OCCUPANT_FULL_M.toFixed(1)}, ${OCCUPANT_NONE_M.toFixed(1)}, depth);
|
|
4194
|
+
return nearWeight * farWeight;
|
|
4195
|
+
}`
|
|
4196
|
+
);
|
|
4003
4197
|
var SOLID_VERT = (
|
|
4004
4198
|
/* glsl */
|
|
4005
4199
|
`#version 300 es
|
|
@@ -4008,8 +4202,10 @@ in vec3 position;
|
|
|
4008
4202
|
in vec3 normal;
|
|
4009
4203
|
in vec3 color;
|
|
4010
4204
|
in float floorIndex;
|
|
4205
|
+
in float materialIndex;
|
|
4011
4206
|
uniform mat4 modelMatrix;
|
|
4012
4207
|
uniform float uFocusFloor; // -1 = show every floor
|
|
4208
|
+
uniform float uStructureDetail; // 0 = venue overview, 1 = section/row/seat
|
|
4013
4209
|
uniform mat4 modelViewMatrix;
|
|
4014
4210
|
uniform mat4 projectionMatrix;
|
|
4015
4211
|
uniform mat3 normalMatrix;
|
|
@@ -4017,14 +4213,19 @@ out vec3 vColor;
|
|
|
4017
4213
|
out vec3 vNormalWorld;
|
|
4018
4214
|
out vec3 vNormalView;
|
|
4019
4215
|
out vec3 vPosView;
|
|
4216
|
+
out vec3 vPosWorld;
|
|
4020
4217
|
out float vDim;
|
|
4218
|
+
out float vMaterial;
|
|
4021
4219
|
void main() {
|
|
4022
4220
|
// Per-floor isolation without splitting the merged mesh into a draw call per
|
|
4023
4221
|
// floor: a floor that is not the focused one is dimmed, not hidden, so the
|
|
4024
4222
|
// buyer keeps the whole venue as context while looking at one level.
|
|
4025
4223
|
vDim = (uFocusFloor < -0.5 || abs(floorIndex - uFocusFloor) < 0.5) ? 0.0 : 1.0;
|
|
4224
|
+
vMaterial = materialIndex;
|
|
4225
|
+
vec4 world = modelMatrix * vec4(position, 1.0);
|
|
4026
4226
|
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
4027
4227
|
vPosView = mv.xyz;
|
|
4228
|
+
vPosWorld = world.xyz;
|
|
4028
4229
|
vNormalView = normalize(normalMatrix * normal);
|
|
4029
4230
|
// World normal drives the key + hemisphere so the lighting stays welded to the
|
|
4030
4231
|
// venue as the camera orbits (the scene has no non-uniform scale, so mat3 of
|
|
@@ -4042,10 +4243,20 @@ in vec3 vColor;
|
|
|
4042
4243
|
in vec3 vNormalWorld;
|
|
4043
4244
|
in vec3 vNormalView;
|
|
4044
4245
|
in vec3 vPosView;
|
|
4246
|
+
in vec3 vPosWorld;
|
|
4045
4247
|
in float vDim;
|
|
4248
|
+
in float vMaterial;
|
|
4046
4249
|
uniform vec3 uKeyDir; // WORLD space, unit, points at the light
|
|
4250
|
+
uniform float uStructureDetail;
|
|
4047
4251
|
out vec4 fragColor;
|
|
4048
4252
|
void main() {
|
|
4253
|
+
// Row treads/risers are honest and useful when the buyer is inside a stand.
|
|
4254
|
+
// At whole-venue scale their overlapping centimetre-scale surfaces quantise
|
|
4255
|
+
// and alias into camera-dependent radial combs. The clean structural block
|
|
4256
|
+
// cap remains, so overview keeps every stand without pretending to show row
|
|
4257
|
+
// detail below a useful pixel size.
|
|
4258
|
+
float rowDetail = step(1.5, vMaterial);
|
|
4259
|
+
if (rowDetail > 0.5 && uStructureDetail < 0.5) discard;
|
|
4049
4260
|
vec3 N = normalize(vNormalWorld);
|
|
4050
4261
|
vec3 V = normalize(-vPosView);
|
|
4051
4262
|
float hemi = 0.5 + 0.5 * N.y; // sky/ground gradient about WORLD up
|
|
@@ -4057,6 +4268,30 @@ void main() {
|
|
|
4057
4268
|
vec3 base = vColor * (0.52 + 0.34 * hemi) + vColor * key * 0.34 + vColor * fill * 0.10;
|
|
4058
4269
|
float fres = pow(1.0 - max(dot(normalize(vNormalView), V), 0.0), 3.0);
|
|
4059
4270
|
base += vec3(0.26, 0.31, 0.38) * fres * 0.35; // cool rim, restrained (view-dependent by design)
|
|
4271
|
+
// Bright, saturated authored screen faces should read as powered displays,
|
|
4272
|
+
// not painted slabs. Vertex colour is deliberately the material signal here:
|
|
4273
|
+
// muted tiers and structure never cross these thresholds. The broad waves
|
|
4274
|
+
// supply a restrained procedural LED treatment without textures, downloads,
|
|
4275
|
+
// solid fake-light geometry or post-processing.
|
|
4276
|
+
float maxChannel = max(max(vColor.r, vColor.g), vColor.b);
|
|
4277
|
+
float minChannel = min(min(vColor.r, vColor.g), vColor.b);
|
|
4278
|
+
float poweredDisplay = (1.0 - step(0.5, abs(vMaterial - 1.0)))
|
|
4279
|
+
* smoothstep(0.58, 0.72, maxChannel)
|
|
4280
|
+
* smoothstep(0.28, 0.48, maxChannel - minChannel);
|
|
4281
|
+
float verticalFace = 1.0 - smoothstep(0.20, 0.62, abs(N.y));
|
|
4282
|
+
float wave = 0.5 + 0.5 * sin(vPosWorld.x * 0.44 + vPosWorld.y * 0.28);
|
|
4283
|
+
float localX = vPosWorld.x;
|
|
4284
|
+
float localY = vPosWorld.y;
|
|
4285
|
+
float beamWest = exp(-pow((localX + 2.6 - localY * 0.22) * 0.44, 2.0));
|
|
4286
|
+
float beamEast = exp(-pow((localX - 2.6 + localY * 0.22) * 0.44, 2.0));
|
|
4287
|
+
float band = smoothstep(0.02, 0.16, 0.18 - abs(localY - 3.9));
|
|
4288
|
+
float focalHalo = exp(-pow(localX * 0.26, 2.0) - pow((localY - 3.9) * 0.38, 2.0));
|
|
4289
|
+
vec3 display = vColor * (0.74 + wave * 0.26)
|
|
4290
|
+
+ vec3(0.10, 0.30, 0.52) * beamWest
|
|
4291
|
+
+ vec3(0.34, 0.13, 0.42) * beamEast
|
|
4292
|
+
+ vec3(0.62, 0.46, 0.20) * band * 0.28
|
|
4293
|
+
+ vec3(0.20, 0.10, 0.34) * focalHalo;
|
|
4294
|
+
base = mix(base, display, poweredDisplay * verticalFace * 0.92);
|
|
4060
4295
|
// Unfocused floors fall back toward the background rather than vanishing.
|
|
4061
4296
|
base = mix(base, base * 0.45, vDim);
|
|
4062
4297
|
fragColor = vec4(base, 1.0);
|
|
@@ -4089,6 +4324,7 @@ out vec3 vRing;
|
|
|
4089
4324
|
out float vDim;
|
|
4090
4325
|
out float vDotWeight; // 1 = the dot IS this seat, 0 = the chair has taken over
|
|
4091
4326
|
out float vPhysicalSeat;
|
|
4327
|
+
out float vPixelRadius;
|
|
4092
4328
|
${CHAIR_WEIGHT_GLSL}
|
|
4093
4329
|
void main() {
|
|
4094
4330
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -4102,6 +4338,7 @@ void main() {
|
|
|
4102
4338
|
// Grow to hold the pixel floor, but never past this seat's own pitch ceiling:
|
|
4103
4339
|
// unbounded growth is what merges neighbouring rows into one mass at range.
|
|
4104
4340
|
float r = min(max(uSeatRadius * uSeatScale, minR), iMaxRadius);
|
|
4341
|
+
vPixelRadius = r / max(depth * uPixelToWorld, 0.000001);
|
|
4105
4342
|
// How much of the requested pixel floor the dot could actually afford. Below 1
|
|
4106
4343
|
// it is losing legibility to distance, and the fragment stage dissolves it
|
|
4107
4344
|
// toward the tier top rather than letting a sub-pixel dot alias and shimmer.
|
|
@@ -4134,10 +4371,13 @@ in vec3 vRing;
|
|
|
4134
4371
|
in float vDim;
|
|
4135
4372
|
in float vDotWeight;
|
|
4136
4373
|
in float vPhysicalSeat;
|
|
4374
|
+
in float vPixelRadius;
|
|
4137
4375
|
uniform float uSeatFade; // fade toward tier colour with distance (LOD)
|
|
4376
|
+
uniform float uSeatDetail; // 0 for venue/area overview; 1 in section/row/seat
|
|
4138
4377
|
uniform vec3 uFadeColor;
|
|
4139
4378
|
out vec4 fragColor;
|
|
4140
4379
|
void main() {
|
|
4380
|
+
if (uSeatDetail < 0.5) discard;
|
|
4141
4381
|
// Empty wheelchair provision is a square bay, never a round chair marker.
|
|
4142
4382
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4143
4383
|
if (d > 1.0) discard;
|
|
@@ -4156,6 +4396,20 @@ void main() {
|
|
|
4156
4396
|
// aliasing; the tier cap underneath already carries the section's category
|
|
4157
4397
|
// tint, so the block reads as coloured seating rather than empty concrete.
|
|
4158
4398
|
alpha *= smoothstep(0.35, 1.0, vBudget);
|
|
4399
|
+
// Do not rasterise inventory states until a dot is large enough to read as a
|
|
4400
|
+
// seat rather than a sample in a dense screen-space pattern. This is based on
|
|
4401
|
+
// actual projected pixels, so it remains correct for unusually large or small
|
|
4402
|
+
// venues where a radius-normalised camera distance is misleading.
|
|
4403
|
+
alpha *= smoothstep(2.4, 3.2, vPixelRadius);
|
|
4404
|
+
// Inventory states are a section-level decision aid. At whole-venue/area
|
|
4405
|
+
// scale the category-tinted structural surface is the honest, stable signal;
|
|
4406
|
+
// thousands of enlarged status dots only form a moir\xE9 pattern. The journey
|
|
4407
|
+
// state enables them when the buyer enters a section, row or exact seat.
|
|
4408
|
+
// Whole-venue status detail is below a useful pixel size. Drawing every dot
|
|
4409
|
+
// there turns concentric rows into camera-dependent moir\xE9 streaks, especially
|
|
4410
|
+
// while zooming. uSeatFade already measures this exact LOD transition: let
|
|
4411
|
+
// the category-tinted tier carry the overview, then reveal individual live
|
|
4412
|
+
// availability as the buyer approaches a section.
|
|
4159
4413
|
// Seats on an unfocused floor recede with their structure.
|
|
4160
4414
|
c = mix(c, uFadeColor, vDim * 0.75);
|
|
4161
4415
|
alpha *= mix(1.0, 0.30, vDim);
|
|
@@ -4200,9 +4454,15 @@ out float vDim;
|
|
|
4200
4454
|
out float vOccupant; // 1 = this vertex belongs to a person, not to the chair
|
|
4201
4455
|
out vec3 vOccupantTint;
|
|
4202
4456
|
${CHAIR_WEIGHT_GLSL}
|
|
4457
|
+
${OCCUPANT_WEIGHT_GLSL}
|
|
4203
4458
|
void main() {
|
|
4204
4459
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4205
|
-
float
|
|
4460
|
+
float viewDepth = -anchor.z;
|
|
4461
|
+
float depth = max(viewDepth, 0.001);
|
|
4462
|
+
// A stale near-field gather can contain seats now behind the camera after a
|
|
4463
|
+
// row\u2192overview move. Treating negative depth as 0.001 made those chairs full
|
|
4464
|
+
// size across the near plane, producing the long black zoom streaks.
|
|
4465
|
+
float w = viewDepth > 0.05 ? chairWeight(depth) : 0.0;
|
|
4206
4466
|
// 1. Local units -> world metres. Only x/z scale: narrow rows get narrow
|
|
4207
4467
|
// chairs, but nobody gets a short one (people are the same height at every
|
|
4208
4468
|
// seat pitch).
|
|
@@ -4227,6 +4487,10 @@ void main() {
|
|
|
4227
4487
|
// identical mannequins: +/-6% height and +/-8% width off the hash.
|
|
4228
4488
|
p.y *= 0.94 + 0.12 * iSeed;
|
|
4229
4489
|
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
4490
|
+
// People only appear once they are large enough to read as people. At
|
|
4491
|
+
// section/overview distance their tiny dark geometry aliases into radial
|
|
4492
|
+
// streaks; seat dots already communicate held/booked state there.
|
|
4493
|
+
p *= occupantWeight(depth);
|
|
4230
4494
|
}
|
|
4231
4495
|
}
|
|
4232
4496
|
p.xz *= iRadius;
|
|
@@ -4265,7 +4529,7 @@ void main() {
|
|
|
4265
4529
|
// not an audience. Hair/clothing for the body, a warm tone for the head,
|
|
4266
4530
|
// both varied by the same per-person hash.
|
|
4267
4531
|
float t = fract(iSeed * 3.71);
|
|
4268
|
-
vec3 clothes = mix(vec3(0.
|
|
4532
|
+
vec3 clothes = mix(vec3(0.18, 0.22, 0.30), vec3(0.42, 0.29, 0.34), t);
|
|
4269
4533
|
vec3 skin = mix(vec3(0.52, 0.38, 0.29), vec3(0.86, 0.70, 0.58), fract(iSeed * 11.3));
|
|
4270
4534
|
vOccupantTint = (part > 3.5) ? skin : clothes;
|
|
4271
4535
|
vPart = part;
|
|
@@ -4369,14 +4633,19 @@ uniform float uSeatRadius;
|
|
|
4369
4633
|
uniform float uSeatScale;
|
|
4370
4634
|
uniform float uMinPixels;
|
|
4371
4635
|
uniform float uPixelToWorld;
|
|
4636
|
+
uniform float uChairFull;
|
|
4637
|
+
uniform float uChairNone;
|
|
4372
4638
|
out vec2 vUv;
|
|
4373
4639
|
out float vPhysicalSeat;
|
|
4640
|
+
out float vDotWeight;
|
|
4374
4641
|
flat out vec3 vPick;
|
|
4642
|
+
${CHAIR_WEIGHT_GLSL}
|
|
4375
4643
|
void main() {
|
|
4376
4644
|
int id = gl_InstanceID + 1; // 0 reserved for no-hit
|
|
4377
4645
|
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
4378
4646
|
vec4 mv = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4379
4647
|
float depth = max(-mv.z, 0.001);
|
|
4648
|
+
vDotWeight = 1.0 - chairWeight(depth);
|
|
4380
4649
|
float minR = uMinPixels * depth * uPixelToWorld;
|
|
4381
4650
|
float r = min(max(uSeatRadius * uSeatScale, minR), iMaxRadius);
|
|
4382
4651
|
mv.xy += position * r;
|
|
@@ -4393,14 +4662,75 @@ var SEAT_PICK_FRAG = (
|
|
|
4393
4662
|
precision highp float;
|
|
4394
4663
|
in vec2 vUv;
|
|
4395
4664
|
in float vPhysicalSeat;
|
|
4665
|
+
in float vDotWeight;
|
|
4396
4666
|
flat in vec3 vPick;
|
|
4397
4667
|
out vec4 fragColor;
|
|
4398
4668
|
void main() {
|
|
4669
|
+
if (vDotWeight <= 0.02) discard; // invisible dot cannot steal a chair tap
|
|
4399
4670
|
float d = mix(max(abs(vUv.x), abs(vUv.y)), length(vUv), vPhysicalSeat);
|
|
4400
4671
|
if (d > 1.0) discard; // hit-mask matches chair/bay shape
|
|
4401
4672
|
fragColor = vec4(vPick, 1.0);
|
|
4402
4673
|
}`
|
|
4403
4674
|
);
|
|
4675
|
+
var CHAIR_PICK_VERT = (
|
|
4676
|
+
/* glsl */
|
|
4677
|
+
`#version 300 es
|
|
4678
|
+
precision highp float;
|
|
4679
|
+
in vec3 position;
|
|
4680
|
+
in float part;
|
|
4681
|
+
in vec3 iOffset;
|
|
4682
|
+
in float iRadius;
|
|
4683
|
+
in float iYaw;
|
|
4684
|
+
in float iSeed;
|
|
4685
|
+
in float iPhysicalSeat;
|
|
4686
|
+
in float iPickIndex;
|
|
4687
|
+
uniform mat4 modelViewMatrix;
|
|
4688
|
+
uniform mat4 projectionMatrix;
|
|
4689
|
+
uniform float uChairFull;
|
|
4690
|
+
uniform float uChairNone;
|
|
4691
|
+
uniform float uBackRake;
|
|
4692
|
+
uniform float uBackBase;
|
|
4693
|
+
flat out vec3 vPick;
|
|
4694
|
+
${CHAIR_WEIGHT_GLSL}
|
|
4695
|
+
${OCCUPANT_WEIGHT_GLSL}
|
|
4696
|
+
void main() {
|
|
4697
|
+
int id = int(iPickIndex + 0.5); // already global index + 1
|
|
4698
|
+
vPick = vec3(float(id & 255), float((id >> 8) & 255), float((id >> 16) & 255)) / 255.0;
|
|
4699
|
+
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
4700
|
+
float viewDepth = -anchor.z;
|
|
4701
|
+
float depth = max(viewDepth, 0.001);
|
|
4702
|
+
float w = viewDepth > 0.05 ? chairWeight(depth) : 0.0;
|
|
4703
|
+
vec3 p = position;
|
|
4704
|
+
float occupant = step(2.5, part);
|
|
4705
|
+
float taken = step(0.0, iSeed);
|
|
4706
|
+
if (iPhysicalSeat < 0.5) {
|
|
4707
|
+
p = vec3(0.0);
|
|
4708
|
+
} else if (occupant > 0.5) {
|
|
4709
|
+
if (taken < 0.5) {
|
|
4710
|
+
p = vec3(0.0);
|
|
4711
|
+
} else {
|
|
4712
|
+
p.y *= 0.94 + 0.12 * iSeed;
|
|
4713
|
+
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
4714
|
+
p *= occupantWeight(depth);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
p.xz *= iRadius;
|
|
4718
|
+
float rake = (part > 1.5 && part < 2.5) ? max(p.y - uBackBase, 0.0) * uBackRake : 0.0;
|
|
4719
|
+
p.z -= rake;
|
|
4720
|
+
p *= sqrt(w);
|
|
4721
|
+
float c = cos(iYaw), s = sin(iYaw);
|
|
4722
|
+
vec3 rp = vec3(p.x * c + p.z * s, p.y, -p.x * s + p.z * c);
|
|
4723
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(iOffset + rp, 1.0);
|
|
4724
|
+
}`
|
|
4725
|
+
);
|
|
4726
|
+
var CHAIR_PICK_FRAG = (
|
|
4727
|
+
/* glsl */
|
|
4728
|
+
`#version 300 es
|
|
4729
|
+
precision highp float;
|
|
4730
|
+
flat in vec3 vPick;
|
|
4731
|
+
out vec4 fragColor;
|
|
4732
|
+
void main() { fragColor = vec4(vPick, 1.0); }`
|
|
4733
|
+
);
|
|
4404
4734
|
var PICK_DEPTH_VERT = (
|
|
4405
4735
|
/* glsl */
|
|
4406
4736
|
`#version 300 es
|
|
@@ -4431,7 +4761,25 @@ function createSeatPickProgram(gl) {
|
|
|
4431
4761
|
uSeatRadius: { value: SEAT_DOT_RADIUS_M },
|
|
4432
4762
|
uSeatScale: { value: 1 },
|
|
4433
4763
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
4434
|
-
uPixelToWorld: { value: 2e-3 }
|
|
4764
|
+
uPixelToWorld: { value: 2e-3 },
|
|
4765
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4766
|
+
uChairNone: { value: CHAIR_NONE_M }
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
function createChairPickProgram(gl) {
|
|
4771
|
+
return new import_ogl3.Program(gl, {
|
|
4772
|
+
vertex: CHAIR_PICK_VERT,
|
|
4773
|
+
fragment: CHAIR_PICK_FRAG,
|
|
4774
|
+
transparent: false,
|
|
4775
|
+
depthTest: true,
|
|
4776
|
+
depthWrite: true,
|
|
4777
|
+
cullFace: false,
|
|
4778
|
+
uniforms: {
|
|
4779
|
+
uChairFull: { value: CHAIR_FULL_M },
|
|
4780
|
+
uChairNone: { value: CHAIR_NONE_M },
|
|
4781
|
+
uBackRake: { value: BACK_RAKE_SLOPE },
|
|
4782
|
+
uBackBase: { value: BACK_BASE_M }
|
|
4435
4783
|
}
|
|
4436
4784
|
});
|
|
4437
4785
|
}
|
|
@@ -4460,7 +4808,8 @@ function createSolidProgram(gl) {
|
|
|
4460
4808
|
// High and off-axis, in world space: reads as a house rig rather than a
|
|
4461
4809
|
// headlamp welded to the camera.
|
|
4462
4810
|
uKeyDir: { value: new Float32Array([0.38, 0.86, 0.34]) },
|
|
4463
|
-
uFocusFloor: { value: -1 }
|
|
4811
|
+
uFocusFloor: { value: -1 },
|
|
4812
|
+
uStructureDetail: { value: 0 }
|
|
4464
4813
|
}
|
|
4465
4814
|
});
|
|
4466
4815
|
}
|
|
@@ -4478,6 +4827,7 @@ function createSeatProgram(gl) {
|
|
|
4478
4827
|
uMinPixels: { value: SEAT_MIN_PIXELS_NEAR },
|
|
4479
4828
|
uPixelToWorld: { value: 2e-3 },
|
|
4480
4829
|
uSeatFade: { value: 0 },
|
|
4830
|
+
uSeatDetail: { value: 0 },
|
|
4481
4831
|
uFocusFloor: { value: -1 },
|
|
4482
4832
|
uFadeColor: { value: new Float32Array([0.32, 0.37, 0.43]) },
|
|
4483
4833
|
uChairFull: { value: CHAIR_FULL_M },
|
|
@@ -4542,7 +4892,8 @@ function buildGpuScene(gl, model) {
|
|
|
4542
4892
|
position: { size: 3, data: model.solids.position },
|
|
4543
4893
|
normal: { size: 3, data: model.solids.normal },
|
|
4544
4894
|
color: { size: 3, data: model.solids.color },
|
|
4545
|
-
floorIndex: { size: 1, data: model.solids.floor }
|
|
4895
|
+
floorIndex: { size: 1, data: model.solids.floor },
|
|
4896
|
+
materialIndex: { size: 1, data: model.solids.material }
|
|
4546
4897
|
});
|
|
4547
4898
|
const solidProg = createSolidProgram(gl);
|
|
4548
4899
|
const solidMesh = new import_ogl4.Mesh(gl, { geometry: solidGeo, program: solidProg });
|
|
@@ -4578,6 +4929,7 @@ function buildGpuScene(gl, model) {
|
|
|
4578
4929
|
const cRing = new Float32Array(CAP * 3);
|
|
4579
4930
|
const cFloor = new Float32Array(CAP);
|
|
4580
4931
|
const cPhysicalSeat = new Float32Array(CAP);
|
|
4932
|
+
const cPickIndex = new Float32Array(CAP);
|
|
4581
4933
|
const cSeed = new Float32Array(CAP);
|
|
4582
4934
|
const chairGeo = new import_ogl4.Geometry(gl, {
|
|
4583
4935
|
position: { size: 3, data: chairBase.position },
|
|
@@ -4591,7 +4943,8 @@ function buildGpuScene(gl, model) {
|
|
|
4591
4943
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
4592
4944
|
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4593
4945
|
iPhysicalSeat: { size: 1, data: cPhysicalSeat, instanced: 1 },
|
|
4594
|
-
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
4946
|
+
iSeed: { size: 1, data: cSeed, instanced: 1 },
|
|
4947
|
+
iPickIndex: { size: 1, data: cPickIndex, instanced: 1 }
|
|
4595
4948
|
});
|
|
4596
4949
|
const chairMesh = new import_ogl4.Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
4597
4950
|
chairMesh.frustumCulled = false;
|
|
@@ -4601,6 +4954,7 @@ function buildGpuScene(gl, model) {
|
|
|
4601
4954
|
const src = model.seats;
|
|
4602
4955
|
for (let k = 0; k < nearCount; k++) {
|
|
4603
4956
|
const i = nearIndices[k];
|
|
4957
|
+
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
4604
4958
|
const useCategory = src.iCategory && src.iState[i] === 0;
|
|
4605
4959
|
const c = useCategory ? null : stateColors[src.iState[i]] ?? stateColors[0];
|
|
4606
4960
|
if (c) {
|
|
@@ -4615,6 +4969,7 @@ function buildGpuScene(gl, model) {
|
|
|
4615
4969
|
}
|
|
4616
4970
|
}
|
|
4617
4971
|
chairGeo.attributes.iColor.needsUpdate = true;
|
|
4972
|
+
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4618
4973
|
};
|
|
4619
4974
|
const scene = {
|
|
4620
4975
|
main,
|
|
@@ -4623,6 +4978,7 @@ function buildGpuScene(gl, model) {
|
|
|
4623
4978
|
solidProgram: solidProg,
|
|
4624
4979
|
chairProgram: chairProg,
|
|
4625
4980
|
seatGeometry: seatGeo,
|
|
4981
|
+
chairGeometry: chairGeo,
|
|
4626
4982
|
solidGeometry: solidGeo,
|
|
4627
4983
|
drawCalls: 3,
|
|
4628
4984
|
setNearSeats(indices, count) {
|
|
@@ -4648,7 +5004,7 @@ function buildGpuScene(gl, model) {
|
|
|
4648
5004
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
4649
5005
|
cFloor[k] = src.iFloor[i];
|
|
4650
5006
|
cPhysicalSeat[k] = src.iPhysicalSeat[i];
|
|
4651
|
-
|
|
5007
|
+
cPickIndex[k] = i + 1;
|
|
4652
5008
|
}
|
|
4653
5009
|
writeChairColors();
|
|
4654
5010
|
chairGeo.attributes.iOffset.needsUpdate = true;
|
|
@@ -4658,6 +5014,7 @@ function buildGpuScene(gl, model) {
|
|
|
4658
5014
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4659
5015
|
chairGeo.attributes.iPhysicalSeat.needsUpdate = true;
|
|
4660
5016
|
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
5017
|
+
chairGeo.attributes.iPickIndex.needsUpdate = true;
|
|
4661
5018
|
chairGeo.instancedCount = n;
|
|
4662
5019
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
4663
5020
|
scene.drawCalls = 4;
|
|
@@ -4737,8 +5094,9 @@ function pickPixelCoords(clientX, clientY, rect, dpr, bufferWidth, bufferHeight)
|
|
|
4737
5094
|
// src/view3d/pick/pickPipeline.ts
|
|
4738
5095
|
var SYNC_KEYS = ["uSeatRadius", "uSeatScale", "uMinPixels", "uPixelToWorld"];
|
|
4739
5096
|
var PickPipeline = class {
|
|
4740
|
-
constructor(renderer, seatGeo, solidGeo, seatCount) {
|
|
5097
|
+
constructor(renderer, seatGeo, chairGeo, solidGeo, seatCount) {
|
|
4741
5098
|
this.seatScene = new import_ogl5.Transform();
|
|
5099
|
+
this.chairScene = new import_ogl5.Transform();
|
|
4742
5100
|
this.solidScene = new import_ogl5.Transform();
|
|
4743
5101
|
this.target = null;
|
|
4744
5102
|
/** Display clear colour to restore after the pick pass (theme-dependent). */
|
|
@@ -4747,10 +5105,14 @@ var PickPipeline = class {
|
|
|
4747
5105
|
this.gl = renderer.gl;
|
|
4748
5106
|
this.maxIndex = seatCount;
|
|
4749
5107
|
this.seatProg = createSeatPickProgram(this.gl);
|
|
5108
|
+
this.chairProg = createChairPickProgram(this.gl);
|
|
4750
5109
|
this.depthProg = createPickDepthProgram(this.gl);
|
|
4751
5110
|
const seatMesh = new import_ogl5.Mesh(this.gl, { geometry: seatGeo, program: this.seatProg });
|
|
4752
5111
|
seatMesh.frustumCulled = false;
|
|
4753
5112
|
seatMesh.setParent(this.seatScene);
|
|
5113
|
+
const chairMesh = new import_ogl5.Mesh(this.gl, { geometry: chairGeo, program: this.chairProg });
|
|
5114
|
+
chairMesh.frustumCulled = false;
|
|
5115
|
+
chairMesh.setParent(this.chairScene);
|
|
4754
5116
|
const solidMesh = new import_ogl5.Mesh(this.gl, { geometry: solidGeo, program: this.depthProg });
|
|
4755
5117
|
solidMesh.frustumCulled = false;
|
|
4756
5118
|
solidMesh.setParent(this.solidScene);
|
|
@@ -4803,6 +5165,7 @@ var PickPipeline = class {
|
|
|
4803
5165
|
gl.clearColor(0, 0, 0, 1);
|
|
4804
5166
|
this.renderer.render({ scene: this.solidScene, camera, target, clear: true });
|
|
4805
5167
|
this.renderer.render({ scene: this.seatScene, camera, target, clear: false });
|
|
5168
|
+
this.renderer.render({ scene: this.chairScene, camera, target, clear: false });
|
|
4806
5169
|
gl.clearColor(br, bg, bb, 1);
|
|
4807
5170
|
gl.disable(gl.SCISSOR_TEST);
|
|
4808
5171
|
const buf = new Uint8Array(boxW * boxH * 4);
|
|
@@ -4814,6 +5177,7 @@ var PickPipeline = class {
|
|
|
4814
5177
|
dispose() {
|
|
4815
5178
|
this.destroyTarget();
|
|
4816
5179
|
this.seatProg.remove();
|
|
5180
|
+
this.chairProg.remove();
|
|
4817
5181
|
this.depthProg.remove();
|
|
4818
5182
|
}
|
|
4819
5183
|
};
|
|
@@ -4943,16 +5307,18 @@ var Cinematic = class {
|
|
|
4943
5307
|
this.outQuat = new import_ogl6.Quat();
|
|
4944
5308
|
this.startTime = 0;
|
|
4945
5309
|
this.duration = FLIGHT_DURATION_MS;
|
|
5310
|
+
this.endFov = FOV_END;
|
|
4946
5311
|
this.resolveFn = null;
|
|
4947
5312
|
this.camera = camera;
|
|
4948
5313
|
}
|
|
4949
5314
|
/** Begin (or retarget) a flight. Resolves when it lands or is cancelled. */
|
|
4950
|
-
start(waypoints, startQuat, endQuat, duration = FLIGHT_DURATION_MS) {
|
|
5315
|
+
start(waypoints, startQuat, endQuat, duration = FLIGHT_DURATION_MS, endFov = FOV_END) {
|
|
4951
5316
|
this.settle();
|
|
4952
5317
|
this.waypoints = waypoints;
|
|
4953
5318
|
this.startQuat.copy(startQuat);
|
|
4954
5319
|
this.endQuat.copy(endQuat);
|
|
4955
5320
|
this.duration = duration;
|
|
5321
|
+
this.endFov = endFov;
|
|
4956
5322
|
this.startTime = performance.now();
|
|
4957
5323
|
this.active = true;
|
|
4958
5324
|
return new Promise((res) => {
|
|
@@ -4963,7 +5329,7 @@ var Cinematic = class {
|
|
|
4963
5329
|
update(now2) {
|
|
4964
5330
|
if (!this.active) return false;
|
|
4965
5331
|
const u = Math.min(1, (now2 - this.startTime) / this.duration);
|
|
4966
|
-
const { pos, fov, eased } = sampleFlight(this.waypoints, u);
|
|
5332
|
+
const { pos, fov, eased } = sampleFlight(this.waypoints, u, FOV_START, this.endFov);
|
|
4967
5333
|
this.camera.position.set(pos[0], pos[1], pos[2]);
|
|
4968
5334
|
this.outQuat.copy(this.startQuat).slerp(this.endQuat, orientationLeadT(eased, ORIENTATION_LEAD));
|
|
4969
5335
|
this.camera.quaternion.copy(this.outQuat);
|
|
@@ -4990,9 +5356,10 @@ var Cinematic = class {
|
|
|
4990
5356
|
// src/view3d/camera/seatViewPose.ts
|
|
4991
5357
|
var SEAT_EYE_ABOVE_DECK_M = 1.02;
|
|
4992
5358
|
var FOCAL_LOOK_HEIGHT_M = 1.5;
|
|
4993
|
-
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0) {
|
|
5359
|
+
function seatViewPose(seatDeckWorld, focalPoint, floorBaseHeightM = 0, authoredEyeHeightM) {
|
|
5360
|
+
const eyeY = Number.isFinite(authoredEyeHeightM) ? floorBaseHeightM + authoredEyeHeightM : seatDeckWorld[1] + SEAT_EYE_ABOVE_DECK_M;
|
|
4994
5361
|
return {
|
|
4995
|
-
eye: [seatDeckWorld[0],
|
|
5362
|
+
eye: [seatDeckWorld[0], eyeY, seatDeckWorld[2]],
|
|
4996
5363
|
focal: [
|
|
4997
5364
|
focalPoint.x * M,
|
|
4998
5365
|
floorBaseHeightM + FOCAL_LOOK_HEIGHT_M,
|
|
@@ -5091,9 +5458,10 @@ function schedulePanoramaUpgrade(work) {
|
|
|
5091
5458
|
|
|
5092
5459
|
// src/view3d/crossfade/panorama.ts
|
|
5093
5460
|
function seatViewDisclosure(view) {
|
|
5094
|
-
const
|
|
5461
|
+
const demo = view.mediaKind === "demo-render";
|
|
5462
|
+
const coverage = view.generated || view.mediaKind === "model" ? "Live 3D \xB7 chart-derived seat-eye" : demo ? view.coverage === "exact-seat" ? "AI-generated exact-seat demo" : view.coverage === "row-representative" ? "AI-generated representative row demo" : view.coverage === "section-representative" ? "AI-generated representative section demo" : "AI-generated illustrative venue demo" : view.coverage === "exact-seat" ? "Exact seat photo" : view.coverage === "row-representative" ? "Representative row view" : view.coverage === "section-representative" ? "Representative section view" : view.coverage === "venue-representative" ? "Representative venue view" : "Venue photo";
|
|
5095
5463
|
const year = view.capturedAt && /^\d{4}/.test(view.capturedAt) ? view.capturedAt.slice(0, 4) : "";
|
|
5096
|
-
return [coverage, year ?
|
|
5464
|
+
return [coverage, year ? `${demo ? "created" : "captured"} ${year}` : "", view.sourceLabel ?? ""].filter(Boolean).join(" \xB7 ");
|
|
5097
5465
|
}
|
|
5098
5466
|
var VFOV_DEG = 70;
|
|
5099
5467
|
var MIN_VFOV_DEG = 35;
|
|
@@ -5170,7 +5538,7 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
5170
5538
|
root.appendChild(closeBtn);
|
|
5171
5539
|
if (opts.disclosure) {
|
|
5172
5540
|
const disclosure = document.createElement("div");
|
|
5173
|
-
disclosure.textContent =
|
|
5541
|
+
disclosure.textContent = `${opts.disclosurePrefix ?? "360\xB0 panorama"} \xB7 ${opts.disclosure}`;
|
|
5174
5542
|
Object.assign(disclosure.style, {
|
|
5175
5543
|
position: "absolute",
|
|
5176
5544
|
top: "12px",
|
|
@@ -5656,7 +6024,7 @@ async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
|
5656
6024
|
root.appendChild(closeBtn);
|
|
5657
6025
|
if (opts.disclosure) {
|
|
5658
6026
|
const disclosure = document.createElement("div");
|
|
5659
|
-
disclosure.textContent =
|
|
6027
|
+
disclosure.textContent = `${opts.disclosurePrefix ?? "360\xB0 panorama"} \xB7 ${opts.disclosure}`;
|
|
5660
6028
|
Object.assign(disclosure.style, {
|
|
5661
6029
|
position: "absolute",
|
|
5662
6030
|
top: "12px",
|
|
@@ -6000,7 +6368,13 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6000
6368
|
};
|
|
6001
6369
|
const rebuildGpu = () => {
|
|
6002
6370
|
gpu = buildGpuScene(glctx.gl, model);
|
|
6003
|
-
pick = new PickPipeline(
|
|
6371
|
+
pick = new PickPipeline(
|
|
6372
|
+
glctx.renderer,
|
|
6373
|
+
gpu.seatGeometry,
|
|
6374
|
+
gpu.chairGeometry,
|
|
6375
|
+
gpu.solidGeometry,
|
|
6376
|
+
model.seats.count
|
|
6377
|
+
);
|
|
6004
6378
|
glctx.setClearColor(model.theme.background.top);
|
|
6005
6379
|
pick.setRestoreClear(model.theme.background.top);
|
|
6006
6380
|
gpu.seatProgram.uniforms.uSeatRadius.value = SEAT_DOT_RADIUS_M * model.theme.seatScale;
|
|
@@ -6013,8 +6387,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6013
6387
|
const nearBuf = new Int32Array(CHAIR_MAX_INSTANCES);
|
|
6014
6388
|
let lastGatherX = Infinity;
|
|
6015
6389
|
let lastGatherZ = Infinity;
|
|
6016
|
-
|
|
6390
|
+
let nearFieldDetailEnabled = false;
|
|
6391
|
+
let cameraDetailVisible = false;
|
|
6392
|
+
let panoramaSphereVisible = false;
|
|
6393
|
+
const setNearFieldDetailEnabled = (enabled) => {
|
|
6394
|
+
nearFieldDetailEnabled = enabled;
|
|
6395
|
+
if (!enabled) cameraDetailVisible = false;
|
|
6396
|
+
lastGatherX = Infinity;
|
|
6397
|
+
lastGatherZ = Infinity;
|
|
6398
|
+
if (!enabled && gpu?.nearSeatCount()) gpu.setNearSeats(nearBuf, 0);
|
|
6399
|
+
};
|
|
6400
|
+
const updateNearField = (detailVisible) => {
|
|
6017
6401
|
if (!gpu) return;
|
|
6402
|
+
if (!detailVisible) {
|
|
6403
|
+
if (gpu.nearSeatCount()) gpu.setNearSeats(nearBuf, 0);
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6018
6406
|
const cam = orbit.camera.position;
|
|
6019
6407
|
const moved2 = Math.hypot(cam.x - lastGatherX, cam.z - lastGatherZ);
|
|
6020
6408
|
if (moved2 < CHAIR_REBUILD_M) return;
|
|
@@ -6057,7 +6445,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6057
6445
|
const dz = model.focalWorld[2] - model.bounds.center[2];
|
|
6058
6446
|
return Math.hypot(dx, dz) > model.bounds.radius * 0.12 ? Math.atan2(dx, dz) : void 0;
|
|
6059
6447
|
})();
|
|
6060
|
-
|
|
6448
|
+
const reduceMotionOnMount = typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6449
|
+
orbit.frame(model.bounds, !reduceMotionOnMount, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6061
6450
|
const cinematic = new Cinematic(orbit.camera);
|
|
6062
6451
|
const restoreContainerPosition = establishPositioningContext(container);
|
|
6063
6452
|
const labelOverlay = new LabelOverlay(container, {
|
|
@@ -6077,12 +6466,16 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6077
6466
|
const sectionSeats = input.seats.filter((seat) => seat.sectionId === sectionId);
|
|
6078
6467
|
const seatIds = new Set(sectionSeats.map((seat) => seat.id));
|
|
6079
6468
|
const rowIds = new Set(sectionSeats.map((seat) => seat.rowId));
|
|
6080
|
-
const
|
|
6081
|
-
|
|
6469
|
+
const selectedIds = new Set(selection.keys());
|
|
6470
|
+
const selectedInSection = new Set(sectionSeats.filter((seat) => selectedIds.has(seat.id)).map((seat) => seat.id));
|
|
6471
|
+
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)))));
|
|
6472
|
+
if (model.seatCount <= 6e3 && selectedInSection.size === 0) {
|
|
6082
6473
|
labelOverlay.setLabels(focusedBase);
|
|
6083
6474
|
return;
|
|
6084
6475
|
}
|
|
6085
|
-
const
|
|
6476
|
+
const labelSeats = selectedInSection.size ? sectionSeats.filter((seat) => selectedInSection.has(seat.id)) : sectionSeats;
|
|
6477
|
+
const generatedIds = new Set(labelSeats.map((seat) => `seat:${seat.id}`));
|
|
6478
|
+
const labels = labelSeats.flatMap((seat) => {
|
|
6086
6479
|
const index = model.seats.idToIndex.get(seat.id);
|
|
6087
6480
|
if (index === void 0) return [];
|
|
6088
6481
|
const offset = index * 3;
|
|
@@ -6092,12 +6485,18 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6092
6485
|
text: seat.displayLabel || seat.label,
|
|
6093
6486
|
anchor: [
|
|
6094
6487
|
model.seats.iPosition[offset],
|
|
6095
|
-
|
|
6488
|
+
// Put the active identity on the chair back. A deck-level label can
|
|
6489
|
+
// sit a full row below a foreground chair in perspective, while DOM
|
|
6490
|
+
// labels for rows behind remain visible through the WebGL geometry.
|
|
6491
|
+
model.seats.iPosition[offset + 1] + (selectedIds.has(seat.id) ? 1.05 : 0.55),
|
|
6096
6492
|
model.seats.iPosition[offset + 2]
|
|
6097
6493
|
]
|
|
6098
6494
|
}];
|
|
6099
6495
|
});
|
|
6100
|
-
labelOverlay.setLabels([
|
|
6496
|
+
labelOverlay.setLabels([
|
|
6497
|
+
...focusedBase.filter((label) => !generatedIds.has(label.id)),
|
|
6498
|
+
...labels
|
|
6499
|
+
]);
|
|
6101
6500
|
};
|
|
6102
6501
|
rebuildGpu();
|
|
6103
6502
|
const loop = new RenderLoop(() => {
|
|
@@ -6105,12 +6504,25 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6105
6504
|
const flying = cinematic.active;
|
|
6106
6505
|
const moving = flying ? cinematic.update(performance.now()) : orbit.update();
|
|
6107
6506
|
const lod = computeSeatLod(orbit.currentDistance, model.bounds.radius);
|
|
6507
|
+
const clippingBounds = panoramaSphereVisible ? { center: [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z], radius: 520 } : model.renderBounds;
|
|
6508
|
+
orbit.updateClipping(clippingBounds);
|
|
6509
|
+
const cameraDx = orbit.camera.position.x - model.renderBounds.center[0];
|
|
6510
|
+
const cameraDy = orbit.camera.position.y - model.renderBounds.center[1];
|
|
6511
|
+
const cameraDz = orbit.camera.position.z - model.renderBounds.center[2];
|
|
6512
|
+
const cameraVenueDistance = Math.hypot(cameraDx, cameraDy, cameraDz);
|
|
6513
|
+
const detailEnter = model.renderBounds.radius * 1.55;
|
|
6514
|
+
const detailExit = model.renderBounds.radius * 1.8;
|
|
6515
|
+
if (!nearFieldDetailEnabled) cameraDetailVisible = false;
|
|
6516
|
+
else if (cameraDetailVisible) cameraDetailVisible = cameraVenueDistance <= detailExit;
|
|
6517
|
+
else cameraDetailVisible = cameraVenueDistance <= detailEnter;
|
|
6108
6518
|
const u = gpu.seatProgram.uniforms;
|
|
6109
6519
|
u.uSeatScale.value = lod.scale;
|
|
6110
6520
|
u.uSeatFade.value = lod.fade;
|
|
6521
|
+
u.uSeatDetail.value = cameraDetailVisible ? 1 : 0;
|
|
6522
|
+
gpu.solidProgram.uniforms.uStructureDetail.value = cameraDetailVisible ? 1 : 0;
|
|
6111
6523
|
u.uMinPixels.value = lod.minPixels;
|
|
6112
6524
|
u.uPixelToWorld.value = 2 * Math.tan(orbit.camera.fov * DEG3 / 2) / Math.max(1, glctx.pixelHeight);
|
|
6113
|
-
updateNearField();
|
|
6525
|
+
updateNearField(cameraDetailVisible);
|
|
6114
6526
|
glctx.renderer.render({ scene: gpu.background, clear: true });
|
|
6115
6527
|
glctx.renderer.render({ scene: gpu.main, camera: orbit.camera, clear: false });
|
|
6116
6528
|
labelOverlay.update(
|
|
@@ -6134,6 +6546,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6134
6546
|
};
|
|
6135
6547
|
const { updates, next } = diffSelection(selection, ids, baseStateIndex);
|
|
6136
6548
|
selection = next;
|
|
6549
|
+
labelOverlay.setSelectedSeatIds(selection.keys());
|
|
6550
|
+
if (focusedSectionId) showSectionSeatLabels(focusedSectionId);
|
|
6137
6551
|
if (updates.length) {
|
|
6138
6552
|
const runs = applySeatStates(model.seats, updates);
|
|
6139
6553
|
if (gpu) gpu.uploadSeatStateRuns(runs);
|
|
@@ -6172,12 +6586,12 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6172
6586
|
model.seats.iPosition[idx * 3 + 1],
|
|
6173
6587
|
model.seats.iPosition[idx * 3 + 2]
|
|
6174
6588
|
];
|
|
6175
|
-
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0);
|
|
6589
|
+
return seatViewPose(deck, focalPoint, floor?.baseHeightM ?? 0, seat?.eyeHeightM);
|
|
6176
6590
|
};
|
|
6177
|
-
const placeCameraFinal = (finalPos, focal) => {
|
|
6591
|
+
const placeCameraFinal = (finalPos, focal, fov = FOV_END) => {
|
|
6178
6592
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
6179
6593
|
orbit.camera.lookAt(new import_ogl8.Vec3(focal[0], focal[1], focal[2]));
|
|
6180
|
-
orbit.camera.fov =
|
|
6594
|
+
orbit.camera.fov = fov;
|
|
6181
6595
|
orbit.camera.updateProjectionMatrix();
|
|
6182
6596
|
};
|
|
6183
6597
|
const isNarrow = () => (container.clientWidth || glctx.canvas.clientWidth) <= 520;
|
|
@@ -6275,7 +6689,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6275
6689
|
});
|
|
6276
6690
|
}
|
|
6277
6691
|
if (opts.getSeatView) {
|
|
6278
|
-
|
|
6692
|
+
const sourceAction = opts.seatViewActionLabel?.(seatId) ?? "Open 360\xB0 panorama";
|
|
6693
|
+
addButton(retry ? `\u21BB Retry ${sourceAction}` : `\u25C9 ${sourceAction}`, `${retry ? "Retry" : sourceAction} from seat ${seatLabel}`, () => {
|
|
6279
6694
|
if (disposed || gen !== flightGen) {
|
|
6280
6695
|
removeArriveChip();
|
|
6281
6696
|
return;
|
|
@@ -6297,121 +6712,158 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6297
6712
|
return controls;
|
|
6298
6713
|
};
|
|
6299
6714
|
const navigationModeChip = document.createElement("div");
|
|
6300
|
-
|
|
6301
|
-
navigationModeChip.
|
|
6715
|
+
let seatViewLocked = false;
|
|
6716
|
+
navigationModeChip.className = "sl-3d-navigation-controls";
|
|
6302
6717
|
Object.assign(navigationModeChip.style, {
|
|
6303
6718
|
position: "absolute",
|
|
6304
|
-
|
|
6305
|
-
|
|
6719
|
+
right: "14px",
|
|
6720
|
+
bottom: "14px",
|
|
6306
6721
|
display: "flex",
|
|
6307
|
-
gap: "3px",
|
|
6308
|
-
padding: "3px",
|
|
6309
|
-
borderRadius: "999px",
|
|
6310
6722
|
zIndex: "4",
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6723
|
+
flexDirection: "column",
|
|
6724
|
+
alignItems: "flex-end",
|
|
6725
|
+
gap: "7px"
|
|
6726
|
+
});
|
|
6727
|
+
const modeControls = document.createElement("div");
|
|
6728
|
+
modeControls.setAttribute("role", "group");
|
|
6729
|
+
modeControls.setAttribute("aria-label", "3D drag mode");
|
|
6730
|
+
Object.assign(modeControls.style, {
|
|
6731
|
+
display: "flex",
|
|
6732
|
+
flexDirection: "column",
|
|
6733
|
+
gap: "6px",
|
|
6734
|
+
alignItems: "flex-end"
|
|
6735
|
+
});
|
|
6736
|
+
const utilityControls = document.createElement("div");
|
|
6737
|
+
utilityControls.setAttribute("role", "group");
|
|
6738
|
+
utilityControls.setAttribute("aria-label", "3D view controls");
|
|
6739
|
+
Object.assign(utilityControls.style, {
|
|
6740
|
+
display: "flex",
|
|
6741
|
+
flexDirection: "column",
|
|
6742
|
+
gap: "6px",
|
|
6743
|
+
alignItems: "flex-end"
|
|
6314
6744
|
});
|
|
6315
6745
|
const rotateModeButton = document.createElement("button");
|
|
6316
6746
|
rotateModeButton.type = "button";
|
|
6317
|
-
rotateModeButton.textContent = "\u21BB
|
|
6747
|
+
rotateModeButton.textContent = "\u21BB";
|
|
6318
6748
|
rotateModeButton.setAttribute("aria-label", "Drag to rotate the 3D venue");
|
|
6749
|
+
rotateModeButton.dataset.tooltip = "Rotate venue";
|
|
6319
6750
|
const moveModeButton = document.createElement("button");
|
|
6320
6751
|
moveModeButton.type = "button";
|
|
6321
|
-
moveModeButton.textContent = "\u2725
|
|
6752
|
+
moveModeButton.textContent = "\u2725";
|
|
6322
6753
|
moveModeButton.setAttribute("aria-label", "Drag to move the 3D venue left, right, up or down");
|
|
6754
|
+
moveModeButton.dataset.tooltip = "Move venue";
|
|
6323
6755
|
const zoomOutButton = document.createElement("button");
|
|
6324
6756
|
zoomOutButton.type = "button";
|
|
6325
6757
|
zoomOutButton.textContent = "\u2212";
|
|
6326
6758
|
zoomOutButton.setAttribute("aria-label", "Zoom out of the 3D venue");
|
|
6327
|
-
zoomOutButton.
|
|
6759
|
+
zoomOutButton.dataset.tooltip = "Zoom out";
|
|
6328
6760
|
const zoomInButton = document.createElement("button");
|
|
6329
6761
|
zoomInButton.type = "button";
|
|
6330
6762
|
zoomInButton.textContent = "+";
|
|
6331
6763
|
zoomInButton.setAttribute("aria-label", "Zoom into the 3D venue");
|
|
6332
|
-
zoomInButton.
|
|
6764
|
+
zoomInButton.dataset.tooltip = "Zoom in";
|
|
6333
6765
|
for (const button of [rotateModeButton, moveModeButton, zoomOutButton, zoomInButton]) {
|
|
6766
|
+
button.className = "sl-3d-icon-control";
|
|
6334
6767
|
Object.assign(button.style, {
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6768
|
+
width: "44px",
|
|
6769
|
+
minWidth: "44px",
|
|
6770
|
+
minHeight: "44px",
|
|
6771
|
+
padding: "9px",
|
|
6772
|
+
border: "1px solid rgba(150,165,205,0.35)",
|
|
6338
6773
|
borderRadius: "999px",
|
|
6339
6774
|
color: "#c9d4ea",
|
|
6340
|
-
background: "
|
|
6341
|
-
font: "600
|
|
6775
|
+
background: "rgba(12,18,32,0.82)",
|
|
6776
|
+
font: "600 17px/1 inherit",
|
|
6777
|
+
backdropFilter: "blur(6px)",
|
|
6342
6778
|
cursor: "pointer",
|
|
6343
6779
|
whiteSpace: "nowrap"
|
|
6344
6780
|
});
|
|
6345
|
-
if (button === zoomOutButton || button === zoomInButton) {
|
|
6346
|
-
button.style.minWidth = "34px";
|
|
6347
|
-
button.style.padding = "6px";
|
|
6348
|
-
button.style.fontSize = "17px";
|
|
6349
|
-
}
|
|
6350
|
-
navigationModeChip.appendChild(button);
|
|
6351
6781
|
}
|
|
6782
|
+
modeControls.append(rotateModeButton, moveModeButton);
|
|
6783
|
+
utilityControls.append(zoomInButton, zoomOutButton);
|
|
6352
6784
|
const setNavigationMode = (mode) => {
|
|
6353
6785
|
orbit.setPrimaryDragMode(mode);
|
|
6354
6786
|
const rotateActive = mode === "orbit";
|
|
6355
6787
|
rotateModeButton.setAttribute("aria-pressed", String(rotateActive));
|
|
6356
6788
|
moveModeButton.setAttribute("aria-pressed", String(!rotateActive));
|
|
6357
|
-
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.
|
|
6358
|
-
moveModeButton.style.background = rotateActive ? "
|
|
6789
|
+
rotateModeButton.style.background = rotateActive ? "rgba(96,110,150,0.68)" : "rgba(12,18,32,0.82)";
|
|
6790
|
+
moveModeButton.style.background = rotateActive ? "rgba(12,18,32,0.82)" : "rgba(96,110,150,0.68)";
|
|
6359
6791
|
navigationModeChip.title = rotateActive ? "Drag to rotate \xB7 Shift-drag to move" : "Drag to move \xB7 Shift-drag to rotate";
|
|
6360
6792
|
};
|
|
6361
6793
|
rotateModeButton.addEventListener("click", () => setNavigationMode("orbit"));
|
|
6362
6794
|
moveModeButton.addEventListener("click", () => setNavigationMode("pan"));
|
|
6363
6795
|
zoomOutButton.addEventListener("click", () => orbit.zoomBy(1.22));
|
|
6364
6796
|
zoomInButton.addEventListener("click", () => orbit.zoomBy(0.82));
|
|
6797
|
+
navigationModeChip.append(modeControls, utilityControls);
|
|
6365
6798
|
setNavigationMode("orbit");
|
|
6366
6799
|
container.appendChild(navigationModeChip);
|
|
6367
6800
|
const layoutNavigationChip = () => {
|
|
6368
|
-
|
|
6801
|
+
const narrow = isNarrow();
|
|
6802
|
+
navigationModeChip.style.display = seatViewLocked ? "none" : "flex";
|
|
6803
|
+
navigationModeChip.style.right = narrow ? "12px" : "14px";
|
|
6804
|
+
navigationModeChip.style.bottom = narrow ? "calc(env(safe-area-inset-bottom, 0px) + 76px)" : "14px";
|
|
6805
|
+
modeControls.style.display = narrow ? "none" : "flex";
|
|
6806
|
+
zoomInButton.style.display = narrow ? "none" : "";
|
|
6807
|
+
zoomOutButton.style.display = narrow ? "none" : "";
|
|
6369
6808
|
};
|
|
6370
6809
|
layoutNavigationChip();
|
|
6810
|
+
const setSeatViewLocked = (locked) => {
|
|
6811
|
+
seatViewLocked = locked;
|
|
6812
|
+
orbit.setInteractionEnabled(!locked);
|
|
6813
|
+
layoutNavigationChip();
|
|
6814
|
+
};
|
|
6371
6815
|
const overviewChip = document.createElement("button");
|
|
6816
|
+
overviewChip.className = "sl-3d-overview-control sl-3d-icon-control";
|
|
6372
6817
|
overviewChip.type = "button";
|
|
6373
|
-
overviewChip.textContent = "\u2302
|
|
6374
|
-
overviewChip.setAttribute("aria-label", "
|
|
6818
|
+
overviewChip.textContent = "\u2302";
|
|
6819
|
+
overviewChip.setAttribute("aria-label", "Fit the whole venue in view");
|
|
6820
|
+
overviewChip.dataset.tooltip = "Fit venue";
|
|
6375
6821
|
Object.assign(overviewChip.style, {
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
padding: "8px 14px",
|
|
6822
|
+
width: "44px",
|
|
6823
|
+
minWidth: "44px",
|
|
6824
|
+
minHeight: "44px",
|
|
6825
|
+
padding: "9px",
|
|
6381
6826
|
borderRadius: "999px",
|
|
6382
6827
|
background: "rgba(12,18,32,0.72)",
|
|
6383
6828
|
color: "#c9d4ea",
|
|
6384
6829
|
border: "1px solid rgba(150,165,205,0.35)",
|
|
6385
6830
|
backdropFilter: "blur(6px)",
|
|
6386
|
-
font: "600
|
|
6831
|
+
font: "600 11px/1 inherit",
|
|
6387
6832
|
cursor: "pointer",
|
|
6388
|
-
|
|
6833
|
+
whiteSpace: "nowrap"
|
|
6389
6834
|
});
|
|
6835
|
+
const layoutOverviewChip = () => {
|
|
6836
|
+
overviewChip.textContent = "\u2302";
|
|
6837
|
+
};
|
|
6838
|
+
layoutOverviewChip();
|
|
6390
6839
|
const focusOverview = () => {
|
|
6391
6840
|
if (disposed) return;
|
|
6841
|
+
opts.onViewTargetChange?.(null);
|
|
6842
|
+
opts.onSectionFocusChange?.(null);
|
|
6392
6843
|
if (panorama) {
|
|
6393
6844
|
panorama.dispose();
|
|
6394
6845
|
panorama = null;
|
|
6395
6846
|
analytics.panoramaClosed();
|
|
6396
6847
|
}
|
|
6848
|
+
panoramaSphereVisible = false;
|
|
6397
6849
|
panoramaLoadAbort?.abort();
|
|
6398
6850
|
panoramaLoadAbort = null;
|
|
6399
6851
|
restorePanoramaLayer();
|
|
6400
6852
|
overviewChip.style.display = "";
|
|
6401
6853
|
labelOverlay.setVisible(true);
|
|
6402
6854
|
frozen = false;
|
|
6855
|
+
setNearFieldDetailEnabled(false);
|
|
6856
|
+
setSeatViewLocked(false);
|
|
6403
6857
|
cancelFlight();
|
|
6404
6858
|
removeArriveChip();
|
|
6405
6859
|
setNavigationMode("orbit");
|
|
6406
6860
|
if (reducedMotion()) orbit.frame(model.bounds, false, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6407
6861
|
else orbit.frameSoft(model.bounds, stageAzimuth, opts.portraitOverviewCrop === true);
|
|
6408
|
-
opts.onViewTargetChange?.(null);
|
|
6409
6862
|
showSectionSeatLabels(null);
|
|
6410
|
-
opts.onSectionFocusChange?.(null);
|
|
6411
6863
|
loop.requestRender();
|
|
6412
6864
|
};
|
|
6413
6865
|
overviewChip.addEventListener("click", focusOverview);
|
|
6414
|
-
|
|
6866
|
+
utilityControls.appendChild(overviewChip);
|
|
6415
6867
|
const openPanorama = async (seatId, fadeMs, gen) => {
|
|
6416
6868
|
panoramaLoadAbort?.abort();
|
|
6417
6869
|
const loadAbort = new AbortController();
|
|
@@ -6457,9 +6909,10 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6457
6909
|
loadAbort.abort();
|
|
6458
6910
|
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6459
6911
|
panorama = null;
|
|
6912
|
+
panoramaSphereVisible = false;
|
|
6460
6913
|
restorePanoramaLayer();
|
|
6461
6914
|
overviewChip.style.display = "";
|
|
6462
|
-
labelOverlay.setVisible(
|
|
6915
|
+
labelOverlay.setVisible(!opts.arriveAtSeatEye);
|
|
6463
6916
|
frozen = false;
|
|
6464
6917
|
analytics.panoramaClosed();
|
|
6465
6918
|
orbit.resumeAfterFlight(seatPose.focal);
|
|
@@ -6470,6 +6923,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6470
6923
|
const sceneMode = view.generated === true;
|
|
6471
6924
|
const disclosure = seatViewDisclosure(view);
|
|
6472
6925
|
raisePanoramaLayer();
|
|
6926
|
+
panoramaSphereVisible = !sceneMode;
|
|
6473
6927
|
overviewChip.style.display = "none";
|
|
6474
6928
|
const effectiveFadeMs = reducedMotion() ? 0 : fadeMs;
|
|
6475
6929
|
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
@@ -6479,9 +6933,17 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6479
6933
|
requestRender: () => loop.requestRender(),
|
|
6480
6934
|
cameraOriginWorld: sceneMode ? seatPose.eye : void 0,
|
|
6481
6935
|
focalWorld: seatPose.focal
|
|
6482
|
-
}, {
|
|
6936
|
+
}, {
|
|
6937
|
+
fadeMs: effectiveFadeMs,
|
|
6938
|
+
seatLabel,
|
|
6939
|
+
disclosure,
|
|
6940
|
+
disclosurePrefix: sceneMode ? "Live seat-eye view" : "360\xB0 panorama",
|
|
6941
|
+
onClose,
|
|
6942
|
+
signal: loadAbort.signal
|
|
6943
|
+
}) : null;
|
|
6483
6944
|
if (disposed || gen !== flightGen) {
|
|
6484
6945
|
loadAbort.abort();
|
|
6946
|
+
panoramaSphereVisible = false;
|
|
6485
6947
|
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6486
6948
|
spherical?.dispose();
|
|
6487
6949
|
overviewChip.style.display = "";
|
|
@@ -6494,7 +6956,21 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6494
6956
|
frozen = false;
|
|
6495
6957
|
loop.requestRender();
|
|
6496
6958
|
panorama = spherical;
|
|
6959
|
+
} else if (sceneMode) {
|
|
6960
|
+
restorePanoramaLayer();
|
|
6961
|
+
overviewChip.style.display = "";
|
|
6962
|
+
frozen = false;
|
|
6963
|
+
orbit.resumeAfterFlight(seatPose.focal);
|
|
6964
|
+
loop.requestRender();
|
|
6965
|
+
panorama = null;
|
|
6966
|
+
panoramaSphereVisible = false;
|
|
6967
|
+
analytics.panoramaFailed(seatId, "mount");
|
|
6968
|
+
const controls = showArriveChip(seatId, flightGen, true);
|
|
6969
|
+
requestAnimationFrame(() => controls?.querySelector("[data-panorama-trigger]")?.focus());
|
|
6970
|
+
if (panoramaLoadAbort === loadAbort) panoramaLoadAbort = null;
|
|
6971
|
+
return;
|
|
6497
6972
|
} else {
|
|
6973
|
+
panoramaSphereVisible = false;
|
|
6498
6974
|
analytics.panoramaFallback(seatId);
|
|
6499
6975
|
panorama = mountPanorama(container, view, {
|
|
6500
6976
|
fadeMs: effectiveFadeMs,
|
|
@@ -6519,36 +6995,45 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6519
6995
|
if (disposed || !gpu) return Promise.resolve();
|
|
6520
6996
|
const idx = model.seats.idToIndex.get(seatId);
|
|
6521
6997
|
if (idx === void 0) return Promise.resolve();
|
|
6998
|
+
setNearFieldDetailEnabled(true);
|
|
6522
6999
|
opts.onViewTargetChange?.(seatId);
|
|
6523
7000
|
if (panorama) {
|
|
6524
7001
|
panorama.dispose();
|
|
6525
7002
|
panorama = null;
|
|
7003
|
+
panoramaSphereVisible = false;
|
|
6526
7004
|
overviewChip.style.display = "";
|
|
6527
7005
|
restorePanoramaLayer();
|
|
6528
7006
|
}
|
|
6529
7007
|
panoramaLoadAbort?.abort();
|
|
6530
7008
|
panoramaLoadAbort = null;
|
|
6531
7009
|
frozen = false;
|
|
7010
|
+
setSeatViewLocked(false);
|
|
6532
7011
|
const gen = ++flightGen;
|
|
6533
7012
|
removeArriveChip();
|
|
6534
7013
|
const { eye: seatEye, focal } = resolvedSeatViewPose(seatId, idx);
|
|
6535
7014
|
const start = [orbit.camera.position.x, orbit.camera.position.y, orbit.camera.position.z];
|
|
6536
|
-
const
|
|
7015
|
+
const flight = buildWaypoints(start, seatEye, focal, model.bounds.center, model.bounds.radius);
|
|
7016
|
+
const finalPos = opts.arriveAtSeatEye ? seatEye : flight.finalPos;
|
|
7017
|
+
const arrivalFov = opts.arriveAtSeatEye ? container.clientWidth / Math.max(1, container.clientHeight) < 0.7 ? 90 : 65 : FOV_END;
|
|
7018
|
+
const waypoints = [flight.waypoints[0], flight.waypoints[1], finalPos];
|
|
7019
|
+
labelOverlay.setVisible(false);
|
|
6537
7020
|
if (reducedMotion()) {
|
|
6538
|
-
placeCameraFinal(finalPos, focal);
|
|
7021
|
+
placeCameraFinal(finalPos, focal, arrivalFov);
|
|
6539
7022
|
loop.requestRender();
|
|
6540
7023
|
analytics.cinematicSkipped();
|
|
6541
7024
|
if (!disposed) orbit.syncFromCamera();
|
|
7025
|
+
setSeatViewLocked(opts.arriveAtSeatEye === true);
|
|
6542
7026
|
showArriveChip(seatId, gen);
|
|
6543
7027
|
return Promise.resolve();
|
|
6544
7028
|
}
|
|
6545
7029
|
const startQuat = new import_ogl8.Quat().copy(orbit.camera.quaternion);
|
|
6546
7030
|
const endQuat = lookAtQuat(orbit.camera, finalPos, focal);
|
|
6547
7031
|
loop.requestRender();
|
|
6548
|
-
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|
|
7032
|
+
return cinematic.start(waypoints, startQuat, endQuat, FLIGHT_DURATION_MS, arrivalFov).then(() => {
|
|
6549
7033
|
if (disposed || gen !== flightGen) return;
|
|
6550
7034
|
analytics.cinematicPlayed(FLIGHT_DURATION_MS);
|
|
6551
7035
|
orbit.resumeAfterFlight(focal);
|
|
7036
|
+
setSeatViewLocked(opts.arriveAtSeatEye === true);
|
|
6552
7037
|
loop.requestRender();
|
|
6553
7038
|
showArriveChip(seatId, gen);
|
|
6554
7039
|
});
|
|
@@ -6556,6 +7041,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6556
7041
|
const focusSectionCamera = (sectionId) => {
|
|
6557
7042
|
const sec = model.sections.find((candidate) => candidate.id === sectionId);
|
|
6558
7043
|
if (!sec || sec.seatCount === 0) return false;
|
|
7044
|
+
setNearFieldDetailEnabled(true);
|
|
7045
|
+
setSeatViewLocked(false);
|
|
6559
7046
|
cinematic.cancel();
|
|
6560
7047
|
removeArriveChip();
|
|
6561
7048
|
setNavigationMode("pan");
|
|
@@ -6570,6 +7057,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6570
7057
|
};
|
|
6571
7058
|
let downX = 0, downY = 0, downT = 0, downId = -1, moved = false, suppressTap = false;
|
|
6572
7059
|
const onDown = (e) => {
|
|
7060
|
+
if (seatViewLocked) return;
|
|
6573
7061
|
if (downId !== -1) return;
|
|
6574
7062
|
downId = e.pointerId;
|
|
6575
7063
|
downX = e.clientX;
|
|
@@ -6580,6 +7068,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6580
7068
|
if (cinematic.active) {
|
|
6581
7069
|
analytics.cinematicCancelled();
|
|
6582
7070
|
cancelFlight();
|
|
7071
|
+
opts.onViewTargetChange?.(null);
|
|
6583
7072
|
}
|
|
6584
7073
|
};
|
|
6585
7074
|
const onMove = (e) => {
|
|
@@ -6630,6 +7119,22 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6630
7119
|
}
|
|
6631
7120
|
return bestIndex;
|
|
6632
7121
|
};
|
|
7122
|
+
const pickVisibleGeometry = (clientX, clientY) => {
|
|
7123
|
+
if (!gpu || !pick) return -1;
|
|
7124
|
+
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
7125
|
+
const rect = glctx.canvas.getBoundingClientRect();
|
|
7126
|
+
const dpr = glctx.renderer.dpr;
|
|
7127
|
+
const { x, y } = pickPixelCoords(
|
|
7128
|
+
clientX,
|
|
7129
|
+
clientY,
|
|
7130
|
+
rect,
|
|
7131
|
+
dpr,
|
|
7132
|
+
glctx.gl.drawingBufferWidth,
|
|
7133
|
+
glctx.gl.drawingBufferHeight
|
|
7134
|
+
);
|
|
7135
|
+
const radius = Math.max(2, Math.round(8 * dpr));
|
|
7136
|
+
return pick.pick(orbit.camera, x, y, radius);
|
|
7137
|
+
};
|
|
6633
7138
|
const onUp = (e) => {
|
|
6634
7139
|
if (e.pointerId !== downId) return;
|
|
6635
7140
|
const isTap = !moved && performance.now() - downT < TAP_MS;
|
|
@@ -6639,7 +7144,15 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6639
7144
|
return;
|
|
6640
7145
|
}
|
|
6641
7146
|
if (!isTap || !gpu || !pick) return;
|
|
6642
|
-
let idx =
|
|
7147
|
+
let idx = pickVisibleGeometry(e.clientX, e.clientY);
|
|
7148
|
+
if (focusedSectionId && idx >= 0) {
|
|
7149
|
+
const visibleSeatId = seatIdByIndex[idx];
|
|
7150
|
+
const visibleSectionId = visibleSeatId ? sectionIdBySeatId.get(visibleSeatId) : void 0;
|
|
7151
|
+
if (visibleSectionId && visibleSectionId !== focusedSectionId && focusSectionCamera(visibleSectionId)) return;
|
|
7152
|
+
}
|
|
7153
|
+
if (focusedSectionId && idx < 0) {
|
|
7154
|
+
idx = pickNearestProjectedSeat(e.clientX, e.clientY, focusedSectionId, 44, false);
|
|
7155
|
+
}
|
|
6643
7156
|
if (focusedSectionId && idx < 0) {
|
|
6644
7157
|
const sectionIndex = pickNearestProjectedSeat(e.clientX, e.clientY, null, 72, false);
|
|
6645
7158
|
const sectionSeatId = sectionIndex >= 0 ? seatIdByIndex[sectionIndex] : void 0;
|
|
@@ -6647,24 +7160,30 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6647
7160
|
if (nextSectionId && nextSectionId !== focusedSectionId && focusSectionCamera(nextSectionId)) return;
|
|
6648
7161
|
}
|
|
6649
7162
|
if (!focusedSectionId) {
|
|
6650
|
-
pick.syncFromSeatProgram(gpu.seatProgram);
|
|
6651
|
-
const rect = glctx.canvas.getBoundingClientRect();
|
|
6652
|
-
const dpr = glctx.renderer.dpr;
|
|
6653
|
-
const { x, y } = pickPixelCoords(e.clientX, e.clientY, rect, dpr, glctx.gl.drawingBufferWidth, glctx.gl.drawingBufferHeight);
|
|
6654
|
-
const radius = Math.max(2, Math.round(8 * dpr));
|
|
6655
|
-
idx = pick.pick(orbit.camera, x, y, radius);
|
|
6656
7163
|
if (idx < 0) idx = pickNearestProjectedSeat(e.clientX, e.clientY, null, 42, false);
|
|
6657
7164
|
const overviewSeatId = idx >= 0 ? seatIdByIndex[idx] : void 0;
|
|
6658
7165
|
const sectionId = overviewSeatId ? sectionIdBySeatId.get(overviewSeatId) : void 0;
|
|
6659
7166
|
if (sectionId && focusSectionCamera(sectionId)) return;
|
|
6660
7167
|
}
|
|
6661
7168
|
if (idx < 0 || idx >= seatIdByIndex.length) {
|
|
6662
|
-
if (selection.size)
|
|
7169
|
+
if (selection.size) {
|
|
7170
|
+
setSelection([]);
|
|
7171
|
+
opts.onViewTargetChange?.(null);
|
|
7172
|
+
}
|
|
6663
7173
|
return;
|
|
6664
7174
|
}
|
|
6665
7175
|
const seatId = seatIdByIndex[idx];
|
|
6666
|
-
|
|
6667
|
-
|
|
7176
|
+
const seatState = SEAT_STATES[Math.round(model.seats.iState[idx] ?? 0)] ?? "available";
|
|
7177
|
+
if (seatState !== "available" && seatState !== "selected") {
|
|
7178
|
+
opts.onSeatInspect?.(seatId, seatState);
|
|
7179
|
+
return;
|
|
7180
|
+
}
|
|
7181
|
+
if (selection.has(seatId) && selection.size === 1) {
|
|
7182
|
+
setSelection([]);
|
|
7183
|
+
opts.onViewTargetChange?.(null);
|
|
7184
|
+
return;
|
|
7185
|
+
}
|
|
7186
|
+
setSelection([seatId]);
|
|
6668
7187
|
ensureSeatView(seatId);
|
|
6669
7188
|
analytics.seatPicked(seatId, sectionIdBySeatId.get(seatId));
|
|
6670
7189
|
opts.onSeatPick?.(seatId);
|
|
@@ -6689,6 +7208,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6689
7208
|
orbit.setAspect(width / Math.max(1, height));
|
|
6690
7209
|
layoutArriveChip();
|
|
6691
7210
|
layoutNavigationChip();
|
|
7211
|
+
layoutOverviewChip();
|
|
6692
7212
|
loop.requestRender();
|
|
6693
7213
|
},
|
|
6694
7214
|
stats() {
|
|
@@ -6707,6 +7227,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6707
7227
|
focusFloor(index) {
|
|
6708
7228
|
if (index !== null && !model.floors.some((f) => f.index === index)) return false;
|
|
6709
7229
|
const value = index ?? -1;
|
|
7230
|
+
setNearFieldDetailEnabled(false);
|
|
7231
|
+
setSeatViewLocked(false);
|
|
6710
7232
|
if (gpu) {
|
|
6711
7233
|
gpu.seatProgram.uniforms.uFocusFloor.value = value;
|
|
6712
7234
|
gpu.solidProgram.uniforms.uFocusFloor.value = value;
|
|
@@ -6730,6 +7252,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6730
7252
|
focusZone(zoneId) {
|
|
6731
7253
|
const zone = model.zones.find((z) => z.id === zoneId);
|
|
6732
7254
|
if (!zone || zone.seatCount === 0) return false;
|
|
7255
|
+
setNearFieldDetailEnabled(false);
|
|
7256
|
+
setSeatViewLocked(false);
|
|
6733
7257
|
cinematic.cancel();
|
|
6734
7258
|
setNavigationMode("orbit");
|
|
6735
7259
|
showSectionSeatLabels(null);
|
|
@@ -6756,6 +7280,8 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
6756
7280
|
focusRow(rowId) {
|
|
6757
7281
|
const row = model.rows.find((candidate) => candidate.id === rowId);
|
|
6758
7282
|
if (!row || row.seatCount === 0) return false;
|
|
7283
|
+
setNearFieldDetailEnabled(true);
|
|
7284
|
+
setSeatViewLocked(false);
|
|
6759
7285
|
cinematic.cancel();
|
|
6760
7286
|
setNavigationMode("pan");
|
|
6761
7287
|
if (row.sectionId) {
|