@seatlayer/core 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/view3d/index.cjs +529 -41
- package/dist/view3d/index.cjs.map +1 -1
- package/dist/view3d/index.d.cts +56 -0
- package/dist/view3d/index.d.ts +56 -0
- package/dist/view3d/index.js +526 -38
- package/dist/view3d/index.js.map +1 -1
- package/package.json +1 -1
package/dist/view3d/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(view3d_exports, {
|
|
|
34
34
|
mountVenue3D: () => mountVenue3D
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(view3d_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_ogl8 = require("ogl");
|
|
38
38
|
|
|
39
39
|
// src/view3d/gl/context.ts
|
|
40
40
|
var import_ogl = require("ogl");
|
|
@@ -52,13 +52,36 @@ var SEAT_STATE_COLORS = {
|
|
|
52
52
|
selected: [0.24, 0.74, 1],
|
|
53
53
|
dimmed: [0.28, 0.32, 0.37]
|
|
54
54
|
};
|
|
55
|
+
function upholsteryTone(rgb) {
|
|
56
|
+
const luma = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
|
57
|
+
const SAT = 0.42;
|
|
58
|
+
const VALUE = 0.62;
|
|
59
|
+
return [
|
|
60
|
+
(luma + (rgb[0] - luma) * SAT) * VALUE,
|
|
61
|
+
(luma + (rgb[1] - luma) * SAT) * VALUE,
|
|
62
|
+
(luma + (rgb[2] - luma) * SAT) * VALUE
|
|
63
|
+
];
|
|
64
|
+
}
|
|
55
65
|
var STRUCTURE = {
|
|
56
66
|
ground: [0.07, 0.085, 0.11],
|
|
57
67
|
tierTop: [0.24, 0.28, 0.34],
|
|
58
68
|
tierWall: [0.17, 0.2, 0.25],
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The lit performance surface.
|
|
71
|
+
*
|
|
72
|
+
* This was [0.42, 0.36, 0.26] and commented "slightly emissive read", which it
|
|
73
|
+
* was not — under the scene's rig it resolved to a mid-brown slab, and from a
|
|
74
|
+
* seat the stage was the DIMMEST large surface in a dark hall. That is exactly
|
|
75
|
+
* backwards: the stage is the one place in a venue with light pointed at it,
|
|
76
|
+
* and it is what a buyer's eye should land on when they arrive at their seat.
|
|
77
|
+
*
|
|
78
|
+
* Bright and warm enough to hold that role against the surrounding structure,
|
|
79
|
+
* which sits around 0.24. An authored `fill` still tints it (see `buildShape`),
|
|
80
|
+
* so an organizer's own stage colour survives — it is just no longer lit like
|
|
81
|
+
* a basement.
|
|
82
|
+
*/
|
|
83
|
+
stageTop: [0.86, 0.64, 0.36],
|
|
84
|
+
stageWall: [0.34, 0.26, 0.18],
|
|
62
85
|
decorTop: [0.22, 0.25, 0.29],
|
|
63
86
|
decorWall: [0.15, 0.17, 0.2],
|
|
64
87
|
gaTop: [0.24, 0.28, 0.33],
|
|
@@ -1175,7 +1198,8 @@ function rectPolygon(x, y, w, h) {
|
|
|
1175
1198
|
}
|
|
1176
1199
|
|
|
1177
1200
|
// src/view3d/scene/seatChair.ts
|
|
1178
|
-
var CHAIR_PART = { pedestal: 0, pad: 1, back: 2 };
|
|
1201
|
+
var CHAIR_PART = { pedestal: 0, pad: 1, back: 2, body: 3, head: 4 };
|
|
1202
|
+
var CHAIR_PART_OCCUPANT_MIN = CHAIR_PART.body;
|
|
1179
1203
|
var CHAIR_PITCH_FRACTION = 0.44;
|
|
1180
1204
|
var CHAIR_HALF_WIDTH_MIN_M = 0.15;
|
|
1181
1205
|
var CHAIR_HALF_WIDTH_MAX_M = 0.3;
|
|
@@ -1205,6 +1229,37 @@ var BOXES = [
|
|
|
1205
1229
|
part: CHAIR_PART.back,
|
|
1206
1230
|
min: [-1, BACK_BASE_M, -1],
|
|
1207
1231
|
max: [1, 0.92, -0.72]
|
|
1232
|
+
},
|
|
1233
|
+
// --- the occupant ---------------------------------------------------------
|
|
1234
|
+
//
|
|
1235
|
+
// A hall with every seat empty reads as an architectural model, not a venue.
|
|
1236
|
+
// The 2048-px panorama this replaced drew a crowd; losing it was the price of
|
|
1237
|
+
// sharpness, and this is how it is bought back — in geometry, where there is
|
|
1238
|
+
// no resolution ceiling.
|
|
1239
|
+
//
|
|
1240
|
+
// Deliberately two blocks and no limbs. At the range these are visible a
|
|
1241
|
+
// person is a torso and a head, and every extra part multiplies by the number
|
|
1242
|
+
// of occupied seats in view. The silhouette is what carries it, exactly as it
|
|
1243
|
+
// does in the generated panorama's head-and-shoulder figures.
|
|
1244
|
+
//
|
|
1245
|
+
// Sized as a seated adult against the 0.92 m chair back: hips at the pad top,
|
|
1246
|
+
// shoulders just above the back panel, head clear of it. Torso is narrower
|
|
1247
|
+
// than the chair so neighbours never interpenetrate at any pitch, and it sits
|
|
1248
|
+
// forward of the back panel rather than inside it.
|
|
1249
|
+
{
|
|
1250
|
+
part: CHAIR_PART.body,
|
|
1251
|
+
min: [-0.66, PAD_TOP_M, -0.58],
|
|
1252
|
+
max: [0.66, 1, 0.26]
|
|
1253
|
+
},
|
|
1254
|
+
// The head is deliberately SMALL. Sized by eye against the chair it came out
|
|
1255
|
+
// near-cubic and read as Lego; a real head is about 0.16 m across and 0.22 m
|
|
1256
|
+
// tall, which against a 0.24 m chair half-width is roughly a third of the
|
|
1257
|
+
// chair's width and clearly taller than it is wide. Getting this ratio wrong
|
|
1258
|
+
// is what makes a crowd look like toys rather than people.
|
|
1259
|
+
{
|
|
1260
|
+
part: CHAIR_PART.head,
|
|
1261
|
+
min: [-0.34, 1.03, -0.4],
|
|
1262
|
+
max: [0.34, 1.27, 0.02]
|
|
1208
1263
|
}
|
|
1209
1264
|
];
|
|
1210
1265
|
var FACES = [
|
|
@@ -1322,6 +1377,15 @@ function computeSeatYaw(iPosition, count, rowIdAt, focal) {
|
|
|
1322
1377
|
|
|
1323
1378
|
// src/view3d/scene/seatInstances.ts
|
|
1324
1379
|
var SEAT_DOT_RADIUS_M = 0.22;
|
|
1380
|
+
function seatOccupantSeed(stateIndex, seatIndex) {
|
|
1381
|
+
const state = SEAT_STATES[stateIndex];
|
|
1382
|
+
if (state !== "sold" && state !== "held") return -1;
|
|
1383
|
+
let h = (seatIndex + 1) * 2654435761;
|
|
1384
|
+
h ^= h >>> 15;
|
|
1385
|
+
h = Math.imul(h, 2246822519);
|
|
1386
|
+
h ^= h >>> 13;
|
|
1387
|
+
return (h >>> 0) % 1e5 / 1e5;
|
|
1388
|
+
}
|
|
1325
1389
|
var SEAT_PITCH_FRACTION = 0.42;
|
|
1326
1390
|
function nearestNeighbourSpacing(seats) {
|
|
1327
1391
|
const n = seats.length;
|
|
@@ -1384,10 +1448,11 @@ function seatSurfaceY(seat) {
|
|
|
1384
1448
|
if (Number.isFinite(eye)) return Math.max(0, eye - SEATED_EYE_HEIGHT_M);
|
|
1385
1449
|
return 0;
|
|
1386
1450
|
}
|
|
1387
|
-
function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
1451
|
+
function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor) {
|
|
1388
1452
|
const count = seats.length;
|
|
1389
1453
|
const iPosition = new Float32Array(count * 3);
|
|
1390
1454
|
const iState = new Float32Array(count);
|
|
1455
|
+
const iCategory = new Float32Array(count * 3);
|
|
1391
1456
|
const iMaxRadius = new Float32Array(count);
|
|
1392
1457
|
const iChairWidth = new Float32Array(count);
|
|
1393
1458
|
const iRing = new Float32Array(count * 3);
|
|
@@ -1403,6 +1468,10 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1403
1468
|
iPosition[i * 3 + 1] = surfaces ? surfaces.seatDeckY(i) : seatSurfaceY(seat);
|
|
1404
1469
|
iPosition[i * 3 + 2] = seat.y * M;
|
|
1405
1470
|
iState[i] = seatStateIndex(initial ? initial(seat) : "available");
|
|
1471
|
+
const cat = hexToRgb(categoryColor?.get(seat.categoryKey) ?? "#6e7bff") ?? [0.43, 0.48, 1];
|
|
1472
|
+
iCategory[i * 3] = cat[0];
|
|
1473
|
+
iCategory[i * 3 + 1] = cat[1];
|
|
1474
|
+
iCategory[i * 3 + 2] = cat[2];
|
|
1406
1475
|
if (seat.accessibility?.length) {
|
|
1407
1476
|
const rgb = hexToRgb(accessibilityRingColor(seat.accessibility));
|
|
1408
1477
|
if (rgb) {
|
|
@@ -1417,6 +1486,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1417
1486
|
count,
|
|
1418
1487
|
iPosition,
|
|
1419
1488
|
iState,
|
|
1489
|
+
iCategory,
|
|
1420
1490
|
iMaxRadius,
|
|
1421
1491
|
iRing,
|
|
1422
1492
|
idToIndex,
|
|
@@ -3124,11 +3194,27 @@ function shapePolygon(shape) {
|
|
|
3124
3194
|
}
|
|
3125
3195
|
return null;
|
|
3126
3196
|
}
|
|
3127
|
-
function buildShape(builder, shape, base, S) {
|
|
3197
|
+
function buildShape(builder, shape, base, S, stages) {
|
|
3128
3198
|
const poly = shapePolygon(shape);
|
|
3129
3199
|
if (!poly) return;
|
|
3130
3200
|
const isStage = shape.role === "stage";
|
|
3131
3201
|
const height = isStage ? base + 1 : base + 0.25;
|
|
3202
|
+
if (isStage && stages) {
|
|
3203
|
+
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
3204
|
+
for (const pt of poly) {
|
|
3205
|
+
if (pt.x < minX) minX = pt.x;
|
|
3206
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
3207
|
+
if (pt.y < minZ) minZ = pt.y;
|
|
3208
|
+
if (pt.y > maxZ) maxZ = pt.y;
|
|
3209
|
+
}
|
|
3210
|
+
stages.push({
|
|
3211
|
+
cx: (minX + maxX) / 2 * M,
|
|
3212
|
+
cz: (minZ + maxZ) / 2 * M,
|
|
3213
|
+
y: height,
|
|
3214
|
+
halfX: Math.max((maxX - minX) / 2 * M, 0.5),
|
|
3215
|
+
halfZ: Math.max((maxZ - minZ) / 2 * M, 0.5)
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3132
3218
|
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
3133
3219
|
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
3134
3220
|
extrudePrism(builder, poly, void 0, () => height, base, colTop, colWall, AO);
|
|
@@ -3238,6 +3324,7 @@ function buildSceneModel(input) {
|
|
|
3238
3324
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
3239
3325
|
const catColor = /* @__PURE__ */ new Map();
|
|
3240
3326
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
3327
|
+
const stageBounds = [];
|
|
3241
3328
|
const surfaces = buildVenueSurfaces(units, seats);
|
|
3242
3329
|
const seatFloor = new Float32Array(seats.length);
|
|
3243
3330
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
@@ -3247,7 +3334,7 @@ function buildSceneModel(input) {
|
|
|
3247
3334
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
3248
3335
|
for (const o of unit.objects) {
|
|
3249
3336
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
3250
|
-
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
|
|
3337
|
+
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S, stageBounds);
|
|
3251
3338
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3252
3339
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
3253
3340
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
@@ -3256,7 +3343,7 @@ function buildSceneModel(input) {
|
|
|
3256
3343
|
}
|
|
3257
3344
|
const focal = doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
3258
3345
|
const solids = mergeMeshData([builder.build()]);
|
|
3259
|
-
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor);
|
|
3346
|
+
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor, catColor);
|
|
3260
3347
|
const zoneDefs = doc.zones ?? [];
|
|
3261
3348
|
const zones = [];
|
|
3262
3349
|
if (zoneDefs.length) {
|
|
@@ -3509,6 +3596,7 @@ function buildSceneModel(input) {
|
|
|
3509
3596
|
// Look-at target ~1.5 m up so a seated camera aims slightly down at the stage.
|
|
3510
3597
|
focalWorld: [focal.x * M, 1.5, focal.y * M],
|
|
3511
3598
|
zones,
|
|
3599
|
+
stages: stageBounds,
|
|
3512
3600
|
sections,
|
|
3513
3601
|
labels,
|
|
3514
3602
|
floors
|
|
@@ -3554,6 +3642,20 @@ var LabelOverlay = class {
|
|
|
3554
3642
|
if (opts.fontFamily) s.fontFamily = opts.fontFamily;
|
|
3555
3643
|
container.appendChild(this.root);
|
|
3556
3644
|
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Hide or show the whole overlay without disturbing which labels exist.
|
|
3647
|
+
*
|
|
3648
|
+
* The in-scene panorama sphere is GL, so it draws BEHIND every DOM label —
|
|
3649
|
+
* row and seat labels floated on top of a 360 photo until this existed. The
|
|
3650
|
+
* old DOM panorama never needed it because its opaque div covered them.
|
|
3651
|
+
*
|
|
3652
|
+
* Deliberately not `setLabels([])`: that destroys the nodes and the declutter
|
|
3653
|
+
* state, so closing the panorama would rebuild and re-rank the whole overlay
|
|
3654
|
+
* and flash. Visibility is a view concern, not a data one.
|
|
3655
|
+
*/
|
|
3656
|
+
setVisible(visible) {
|
|
3657
|
+
this.root.style.visibility = visible ? "" : "hidden";
|
|
3658
|
+
}
|
|
3557
3659
|
setLabels(labels) {
|
|
3558
3660
|
this.labels = labels;
|
|
3559
3661
|
for (const [id, node] of this.nodes) {
|
|
@@ -3817,13 +3919,14 @@ var CHAIR_VERT = (
|
|
|
3817
3919
|
precision highp float;
|
|
3818
3920
|
in vec3 position; // local: x/z in units of the seat radius, y in METRES
|
|
3819
3921
|
in vec3 normal;
|
|
3820
|
-
in float part; // 0 = pedestal, 1 = pad, 2 = back
|
|
3922
|
+
in float part; // 0 = pedestal, 1 = pad, 2 = back, 3 = body, 4 = head
|
|
3821
3923
|
in vec3 iOffset; // per-instance world deck point (identical to the dot's)
|
|
3822
3924
|
in vec3 iColor; // per-instance state colour
|
|
3823
3925
|
in float iRadius; // per-instance horizontal half-width, world metres
|
|
3824
3926
|
in float iYaw; // per-instance facing, radians (local +Z -> facing dir)
|
|
3825
3927
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3826
3928
|
in float iFloor;
|
|
3929
|
+
in float iSeed; // <0 = seat is empty; else per-person hash in [0,1)
|
|
3827
3930
|
uniform mat4 modelViewMatrix;
|
|
3828
3931
|
uniform mat4 projectionMatrix;
|
|
3829
3932
|
uniform float uChairFull;
|
|
@@ -3839,6 +3942,8 @@ out float vPart;
|
|
|
3839
3942
|
out float vHeight; // local height in metres, for the vertical occlusion ramp
|
|
3840
3943
|
out vec3 vRing;
|
|
3841
3944
|
out float vDim;
|
|
3945
|
+
out float vOccupant; // 1 = this vertex belongs to a person, not to the chair
|
|
3946
|
+
out vec3 vOccupantTint;
|
|
3842
3947
|
${CHAIR_WEIGHT_GLSL}
|
|
3843
3948
|
void main() {
|
|
3844
3949
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -3847,12 +3952,35 @@ void main() {
|
|
|
3847
3952
|
// chairs, but nobody gets a short one (people are the same height at every
|
|
3848
3953
|
// seat pitch).
|
|
3849
3954
|
vec3 p = position;
|
|
3955
|
+
// The occupant. Parts 3 and 4 are a person; everything below is furniture.
|
|
3956
|
+
//
|
|
3957
|
+
// One instanced mesh draws both an empty seat and a taken one, because the
|
|
3958
|
+
// alternative \u2014 a second geometry and a second draw call gathered per frame \u2014
|
|
3959
|
+
// would double the near-field cost to show what is already known per instance.
|
|
3960
|
+
// An unoccupied seat collapses its person to a point at the seat, which the
|
|
3961
|
+
// rasteriser discards, exactly as the chair itself collapses at w=0.
|
|
3962
|
+
float occupant = step(2.5, part);
|
|
3963
|
+
float taken = step(0.0, iSeed);
|
|
3964
|
+
vOccupant = occupant;
|
|
3965
|
+
if (occupant > 0.5) {
|
|
3966
|
+
if (taken < 0.5) {
|
|
3967
|
+
p = vec3(0.0); // empty seat: no person
|
|
3968
|
+
} else {
|
|
3969
|
+
// Vary the build so a sold-out row is people rather than a rank of
|
|
3970
|
+
// identical mannequins: +/-6% height and +/-8% width off the hash.
|
|
3971
|
+
p.y *= 0.94 + 0.12 * iSeed;
|
|
3972
|
+
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3850
3975
|
p.xz *= iRadius;
|
|
3851
3976
|
// 2. Lean the back. Done here rather than in the base mesh so the lean is a
|
|
3852
3977
|
// real angle in METRES \u2014 baked into the mesh it would scale with the seat's
|
|
3853
3978
|
// width and the same chair would lean 20 degrees on a wide stadium row and
|
|
3854
3979
|
// 6 on a tight theatre one.
|
|
3855
|
-
|
|
3980
|
+
// Only the BACK panel rakes. The old test (part > 1.5) meant "the back", and
|
|
3981
|
+
// now also catches the occupant \u2014 leaning a person by the panel's rule would
|
|
3982
|
+
// translate their head backwards by a rake measured from the panel's base.
|
|
3983
|
+
float rake = (part > 1.5 && part < 2.5) ? max(p.y - uBackBase, 0.0) * uBackRake : 0.0;
|
|
3856
3984
|
p.z -= rake;
|
|
3857
3985
|
// 3. Scale-in. At w=0 the chair is a point at the seat, under a dot at full
|
|
3858
3986
|
// opacity \u2014 which is what makes the handover invisible. sqrt front-loads
|
|
@@ -3866,7 +3994,7 @@ void main() {
|
|
|
3866
3994
|
// unchanged; and the rake is a shear, whose normal transform adds a y term.
|
|
3867
3995
|
// Skipping either lights the raked back as though it were still vertical.
|
|
3868
3996
|
vec3 n = vec3(normal.x / iRadius, normal.y, normal.z / iRadius);
|
|
3869
|
-
if (part > 1.5) n.y += uBackRake * n.z;
|
|
3997
|
+
if (part > 1.5 && part < 2.5) n.y += uBackRake * n.z;
|
|
3870
3998
|
n = normalize(n);
|
|
3871
3999
|
vec3 rn = vec3(n.x * c + n.z * s, n.y, -n.x * s + n.z * c);
|
|
3872
4000
|
vec4 mv = modelViewMatrix * vec4(iOffset + rp, 1.0);
|
|
@@ -3874,6 +4002,15 @@ void main() {
|
|
|
3874
4002
|
vNormalWorld = rn;
|
|
3875
4003
|
vNormalView = normalize(mat3(modelViewMatrix) * rn);
|
|
3876
4004
|
vColor = iColor;
|
|
4005
|
+
// Occupant colour, resolved here so the fragment stage needs no extra
|
|
4006
|
+
// varyings beyond this one. A person is NOT painted in the seat's state
|
|
4007
|
+
// colour: a sold seat is red, and a hall of red people reads as a warning,
|
|
4008
|
+
// not an audience. Hair/clothing for the body, a warm tone for the head,
|
|
4009
|
+
// both varied by the same per-person hash.
|
|
4010
|
+
float t = fract(iSeed * 3.71);
|
|
4011
|
+
vec3 clothes = mix(vec3(0.13, 0.15, 0.20), vec3(0.34, 0.30, 0.36), t);
|
|
4012
|
+
vec3 skin = mix(vec3(0.52, 0.38, 0.29), vec3(0.86, 0.70, 0.58), fract(iSeed * 11.3));
|
|
4013
|
+
vOccupantTint = (part > 3.5) ? skin : clothes;
|
|
3877
4014
|
vPart = part;
|
|
3878
4015
|
vHeight = position.y;
|
|
3879
4016
|
vRing = iRing;
|
|
@@ -3893,6 +4030,8 @@ in float vPart;
|
|
|
3893
4030
|
in float vHeight;
|
|
3894
4031
|
in vec3 vRing;
|
|
3895
4032
|
in float vDim;
|
|
4033
|
+
in float vOccupant;
|
|
4034
|
+
in vec3 vOccupantTint;
|
|
3896
4035
|
uniform vec3 uKeyDir;
|
|
3897
4036
|
uniform vec3 uFadeColor;
|
|
3898
4037
|
out vec4 fragColor;
|
|
@@ -3905,6 +4044,7 @@ void main() {
|
|
|
3905
4044
|
vec3 fillDir = normalize(vec3(-uKeyDir.x, 0.25, -uKeyDir.z));
|
|
3906
4045
|
float fill = max(dot(N, fillDir), 0.0);
|
|
3907
4046
|
vec3 tint = vColor;
|
|
4047
|
+
if (vOccupant > 0.5) tint = vOccupantTint;
|
|
3908
4048
|
// The accommodation ring, kept legible once the dot (which drew it) is gone:
|
|
3909
4049
|
// an accessible seat's PEDESTAL is painted in the ring colour, so the marker
|
|
3910
4050
|
// survives to close range instead of vanishing exactly when the buyer arrives.
|
|
@@ -3913,12 +4053,16 @@ void main() {
|
|
|
3913
4053
|
// Pad brightest, back a step below it, pedestal darkest. Three untextured
|
|
3914
4054
|
// boxes only read as one object if they are separated tonally \u2014 with a single
|
|
3915
4055
|
// flat colour the chair silhouettes as a crate.
|
|
3916
|
-
|
|
4056
|
+
// A person is lit as a person, not as upholstery: no part shading, and less
|
|
4057
|
+
// of the pad's sheen, so a head does not read as a polished box.
|
|
4058
|
+
float partShade = vOccupant > 0.5 ? 0.95 : (vPart < 0.5 ? 0.50 : (vPart < 1.5 ? 1.10 : 0.80));
|
|
3917
4059
|
// Cheap vertical occlusion: a chair is in a dense row, so the closer a surface
|
|
3918
4060
|
// sits to the deck the less sky it can actually see. This is the depth cue \u2014
|
|
3919
4061
|
// without it the pad top, the back and the deck all resolve to the same flat
|
|
3920
4062
|
// value and the row loses its form entirely.
|
|
3921
|
-
|
|
4063
|
+
// Occupants rise above the 0.92 m chair back, so their ramp uses their own
|
|
4064
|
+
// height or every head would clamp to full brightness and float.
|
|
4065
|
+
float ao = mix(0.58, 1.0, clamp(vHeight / (vOccupant > 0.5 ? 1.30 : 0.92), 0.0, 1.0));
|
|
3922
4066
|
vec3 base = tint * partShade * ao * (0.52 + 0.40 * hemi) + tint * key * 0.38 + tint * fill * 0.10;
|
|
3923
4067
|
float fres = pow(1.0 - max(dot(normalize(vNormalView), V), 0.0), 3.0);
|
|
3924
4068
|
// A brighter rim than the solids get: it picks out every chair's own edge,
|
|
@@ -4113,12 +4257,13 @@ function createBackgroundProgram(gl, top, bottom) {
|
|
|
4113
4257
|
}
|
|
4114
4258
|
|
|
4115
4259
|
// src/view3d/scene/build.ts
|
|
4116
|
-
function writeSeatColors(iColor, iState, start, count, states) {
|
|
4260
|
+
function writeSeatColors(iColor, iState, start, count, states, iCategory) {
|
|
4117
4261
|
for (let i = start; i < start + count; i++) {
|
|
4118
|
-
const
|
|
4119
|
-
|
|
4120
|
-
iColor[i * 3
|
|
4121
|
-
iColor[i * 3 +
|
|
4262
|
+
const useCategory = iCategory && iState[i] === 0;
|
|
4263
|
+
const c = useCategory ? null : states[iState[i]] ?? states[0];
|
|
4264
|
+
iColor[i * 3] = c ? c[0] : iCategory[i * 3];
|
|
4265
|
+
iColor[i * 3 + 1] = c ? c[1] : iCategory[i * 3 + 1];
|
|
4266
|
+
iColor[i * 3 + 2] = c ? c[2] : iCategory[i * 3 + 2];
|
|
4122
4267
|
}
|
|
4123
4268
|
}
|
|
4124
4269
|
var SEAT_QUAD = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
|
|
@@ -4144,7 +4289,7 @@ function buildGpuScene(gl, model) {
|
|
|
4144
4289
|
const seatProg = createSeatProgram(gl);
|
|
4145
4290
|
const iColor = new Float32Array(model.seats.count * 3);
|
|
4146
4291
|
const stateColors = SEAT_STATES.map((st) => model.theme.seatStates[st]);
|
|
4147
|
-
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors);
|
|
4292
|
+
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors, model.seats.iCategory);
|
|
4148
4293
|
const seatGeo = new import_ogl4.Geometry(gl, {
|
|
4149
4294
|
position: { size: 2, data: SEAT_QUAD },
|
|
4150
4295
|
iOffset: { size: 3, data: model.seats.iPosition, instanced: 1 },
|
|
@@ -4169,6 +4314,7 @@ function buildGpuScene(gl, model) {
|
|
|
4169
4314
|
const cYaw = new Float32Array(CAP);
|
|
4170
4315
|
const cRing = new Float32Array(CAP * 3);
|
|
4171
4316
|
const cFloor = new Float32Array(CAP);
|
|
4317
|
+
const cSeed = new Float32Array(CAP);
|
|
4172
4318
|
const chairGeo = new import_ogl4.Geometry(gl, {
|
|
4173
4319
|
position: { size: 3, data: chairBase.position },
|
|
4174
4320
|
normal: { size: 3, data: chairBase.normal },
|
|
@@ -4179,7 +4325,8 @@ function buildGpuScene(gl, model) {
|
|
|
4179
4325
|
iRadius: { size: 1, data: cRadius, instanced: 1 },
|
|
4180
4326
|
iYaw: { size: 1, data: cYaw, instanced: 1 },
|
|
4181
4327
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
4182
|
-
iFloor: { size: 1, data: cFloor, instanced: 1 }
|
|
4328
|
+
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
4329
|
+
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
4183
4330
|
});
|
|
4184
4331
|
const chairMesh = new import_ogl4.Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
4185
4332
|
chairMesh.frustumCulled = false;
|
|
@@ -4189,10 +4336,18 @@ function buildGpuScene(gl, model) {
|
|
|
4189
4336
|
const src = model.seats;
|
|
4190
4337
|
for (let k = 0; k < nearCount; k++) {
|
|
4191
4338
|
const i = nearIndices[k];
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4339
|
+
const useCategory = src.iCategory && src.iState[i] === 0;
|
|
4340
|
+
const c = useCategory ? null : stateColors[src.iState[i]] ?? stateColors[0];
|
|
4341
|
+
if (c) {
|
|
4342
|
+
cColor[k * 3] = c[0];
|
|
4343
|
+
cColor[k * 3 + 1] = c[1];
|
|
4344
|
+
cColor[k * 3 + 2] = c[2];
|
|
4345
|
+
} else {
|
|
4346
|
+
const u = upholsteryTone([src.iCategory[i * 3], src.iCategory[i * 3 + 1], src.iCategory[i * 3 + 2]]);
|
|
4347
|
+
cColor[k * 3] = u[0];
|
|
4348
|
+
cColor[k * 3 + 1] = u[1];
|
|
4349
|
+
cColor[k * 3 + 2] = u[2];
|
|
4350
|
+
}
|
|
4196
4351
|
}
|
|
4197
4352
|
chairGeo.attributes.iColor.needsUpdate = true;
|
|
4198
4353
|
};
|
|
@@ -4227,6 +4382,7 @@ function buildGpuScene(gl, model) {
|
|
|
4227
4382
|
cRing[k * 3 + 1] = src.iRing[i * 3 + 1];
|
|
4228
4383
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
4229
4384
|
cFloor[k] = src.iFloor[i];
|
|
4385
|
+
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
4230
4386
|
}
|
|
4231
4387
|
writeChairColors();
|
|
4232
4388
|
chairGeo.attributes.iOffset.needsUpdate = true;
|
|
@@ -4234,6 +4390,7 @@ function buildGpuScene(gl, model) {
|
|
|
4234
4390
|
chairGeo.attributes.iYaw.needsUpdate = true;
|
|
4235
4391
|
chairGeo.attributes.iRing.needsUpdate = true;
|
|
4236
4392
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
4393
|
+
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
4237
4394
|
chairGeo.instancedCount = n;
|
|
4238
4395
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
4239
4396
|
scene.drawCalls = 4;
|
|
@@ -4244,7 +4401,9 @@ function buildGpuScene(gl, model) {
|
|
|
4244
4401
|
uploadSeatStateRuns(runs) {
|
|
4245
4402
|
if (!runs.length) return;
|
|
4246
4403
|
if (nearCount) writeChairColors();
|
|
4247
|
-
for (const run of runs)
|
|
4404
|
+
for (const run of runs) {
|
|
4405
|
+
writeSeatColors(iColor, model.seats.iState, run.start, run.length, stateColors, model.seats.iCategory);
|
|
4406
|
+
}
|
|
4248
4407
|
const buffer = colorAttr.buffer;
|
|
4249
4408
|
if (!buffer) {
|
|
4250
4409
|
colorAttr.needsUpdate = true;
|
|
@@ -4750,6 +4909,317 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4750
4909
|
};
|
|
4751
4910
|
}
|
|
4752
4911
|
|
|
4912
|
+
// src/view3d/scene/panoSphere.ts
|
|
4913
|
+
var import_ogl7 = require("ogl");
|
|
4914
|
+
var PANO_SEGMENTS_LON = 64;
|
|
4915
|
+
var PANO_SEGMENTS_LAT = 32;
|
|
4916
|
+
var PANO_RADIUS_M = 500;
|
|
4917
|
+
function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEGMENTS_LAT, radius = PANO_RADIUS_M) {
|
|
4918
|
+
const lonCount = segmentsLon + 1;
|
|
4919
|
+
const latCount = segmentsLat + 1;
|
|
4920
|
+
const vertexCount = lonCount * latCount;
|
|
4921
|
+
const position = new Float32Array(vertexCount * 3);
|
|
4922
|
+
const uv = new Float32Array(vertexCount * 2);
|
|
4923
|
+
for (let iLat = 0; iLat < latCount; iLat++) {
|
|
4924
|
+
const v = iLat / segmentsLat;
|
|
4925
|
+
const polar = v * Math.PI;
|
|
4926
|
+
const sinPolar = Math.sin(polar);
|
|
4927
|
+
const cosPolar = Math.cos(polar);
|
|
4928
|
+
for (let iLon = 0; iLon < lonCount; iLon++) {
|
|
4929
|
+
const u = iLon / segmentsLon;
|
|
4930
|
+
const azimuth = (u - 0.5) * Math.PI * 2;
|
|
4931
|
+
const i = iLat * lonCount + iLon;
|
|
4932
|
+
position[i * 3] = radius * sinPolar * Math.sin(azimuth);
|
|
4933
|
+
position[i * 3 + 1] = radius * cosPolar;
|
|
4934
|
+
position[i * 3 + 2] = -radius * sinPolar * Math.cos(azimuth);
|
|
4935
|
+
uv[i * 2] = u;
|
|
4936
|
+
uv[i * 2 + 1] = v;
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
const index = new Uint16Array(segmentsLon * segmentsLat * 6);
|
|
4940
|
+
let w = 0;
|
|
4941
|
+
for (let iLat = 0; iLat < segmentsLat; iLat++) {
|
|
4942
|
+
for (let iLon = 0; iLon < segmentsLon; iLon++) {
|
|
4943
|
+
const a = iLat * lonCount + iLon;
|
|
4944
|
+
const b = a + lonCount;
|
|
4945
|
+
index[w++] = a;
|
|
4946
|
+
index[w++] = b;
|
|
4947
|
+
index[w++] = a + 1;
|
|
4948
|
+
index[w++] = a + 1;
|
|
4949
|
+
index[w++] = b;
|
|
4950
|
+
index[w++] = b + 1;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
return { position, uv, index };
|
|
4954
|
+
}
|
|
4955
|
+
function bearingPitchToDirection(bearingDeg, pitchDeg) {
|
|
4956
|
+
const yaw = bearingDeg * Math.PI / 180;
|
|
4957
|
+
const pitch = pitchDeg * Math.PI / 180;
|
|
4958
|
+
const cosPitch = Math.cos(pitch);
|
|
4959
|
+
return [cosPitch * Math.sin(yaw), Math.sin(pitch), -cosPitch * Math.cos(yaw)];
|
|
4960
|
+
}
|
|
4961
|
+
var PANO_VERT = (
|
|
4962
|
+
/* glsl */
|
|
4963
|
+
`#version 300 es
|
|
4964
|
+
precision highp float;
|
|
4965
|
+
in vec3 position;
|
|
4966
|
+
in vec2 uv;
|
|
4967
|
+
uniform mat4 modelViewMatrix;
|
|
4968
|
+
uniform mat4 projectionMatrix;
|
|
4969
|
+
out vec2 vUv;
|
|
4970
|
+
void main() {
|
|
4971
|
+
vUv = uv;
|
|
4972
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
4973
|
+
}`
|
|
4974
|
+
);
|
|
4975
|
+
var PANO_FRAG = (
|
|
4976
|
+
/* glsl */
|
|
4977
|
+
`#version 300 es
|
|
4978
|
+
precision highp float;
|
|
4979
|
+
in vec2 vUv;
|
|
4980
|
+
uniform sampler2D uPano;
|
|
4981
|
+
uniform float uOpacity;
|
|
4982
|
+
out vec4 fragColor;
|
|
4983
|
+
void main() {
|
|
4984
|
+
vec3 rgb = texture(uPano, vUv).rgb;
|
|
4985
|
+
fragColor = vec4(rgb, uOpacity);
|
|
4986
|
+
}`
|
|
4987
|
+
);
|
|
4988
|
+
function createPanoSphere(gl, image) {
|
|
4989
|
+
const { position, uv, index } = buildPanoSphere();
|
|
4990
|
+
const geometry = new import_ogl7.Geometry(gl, {
|
|
4991
|
+
position: { size: 3, data: position },
|
|
4992
|
+
uv: { size: 2, data: uv },
|
|
4993
|
+
index: { data: index }
|
|
4994
|
+
});
|
|
4995
|
+
const texture = new import_ogl7.Texture(gl, {
|
|
4996
|
+
image,
|
|
4997
|
+
// The seam column is duplicated in geometry, so CLAMP is correct and avoids
|
|
4998
|
+
// a wrapped bilinear tap bleeding the far edge of the image into it.
|
|
4999
|
+
wrapS: gl.CLAMP_TO_EDGE,
|
|
5000
|
+
wrapT: gl.CLAMP_TO_EDGE,
|
|
5001
|
+
generateMipmaps: true,
|
|
5002
|
+
// MUST be false, and this is the one line that decides whether the whole
|
|
5003
|
+
// panorama is upside down.
|
|
5004
|
+
//
|
|
5005
|
+
// OGL defaults `flipY` to true for 2D textures, which is right for the usual
|
|
5006
|
+
// case: a UV of 0 means the BOTTOM of a quad, an image's first row is its
|
|
5007
|
+
// TOP, and flipping on upload reconciles the two. This sphere is the other
|
|
5008
|
+
// case. Its `v` is authored in IMAGE space — v=0 is the top of the equirect
|
|
5009
|
+
// and maps to the top of the sphere (`buildPanoSphere`, and the mapping
|
|
5010
|
+
// `generatePanorama` and the 2D viewer both use). Flipping on upload as well
|
|
5011
|
+
// applies the correction twice: the stadium pitch renders on the ceiling and
|
|
5012
|
+
// the audience sits overhead.
|
|
5013
|
+
//
|
|
5014
|
+
// The geometry tests state the v mapping and pass either way — they never
|
|
5015
|
+
// touch GL — so this is not something a unit test can defend. It was caught
|
|
5016
|
+
// by looking at a stadium.
|
|
5017
|
+
flipY: false
|
|
5018
|
+
});
|
|
5019
|
+
const program = new import_ogl7.Program(gl, {
|
|
5020
|
+
vertex: PANO_VERT,
|
|
5021
|
+
fragment: PANO_FRAG,
|
|
5022
|
+
uniforms: { uPano: { value: texture }, uOpacity: { value: 0 } },
|
|
5023
|
+
transparent: true,
|
|
5024
|
+
depthTest: false,
|
|
5025
|
+
depthWrite: false
|
|
5026
|
+
});
|
|
5027
|
+
const mesh = new import_ogl7.Mesh(gl, { geometry, program });
|
|
5028
|
+
return {
|
|
5029
|
+
mesh,
|
|
5030
|
+
setOpacity(value) {
|
|
5031
|
+
program.uniforms.uOpacity.value = Math.max(0, Math.min(1, value));
|
|
5032
|
+
},
|
|
5033
|
+
dispose() {
|
|
5034
|
+
mesh.setParent(null);
|
|
5035
|
+
geometry.remove();
|
|
5036
|
+
if (texture.texture) gl.deleteTexture(texture.texture);
|
|
5037
|
+
program.remove();
|
|
5038
|
+
}
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
// src/view3d/crossfade/panoramaSphere.ts
|
|
5043
|
+
var DEG_PER_PX = 0.15;
|
|
5044
|
+
function loadImage(url) {
|
|
5045
|
+
return new Promise((resolve, reject) => {
|
|
5046
|
+
const img = new Image();
|
|
5047
|
+
img.crossOrigin = "anonymous";
|
|
5048
|
+
img.onload = () => resolve(img);
|
|
5049
|
+
img.onerror = () => reject(new Error("panorama_image_failed"));
|
|
5050
|
+
img.src = url;
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
5054
|
+
const fadeMs = opts.fadeMs ?? 400;
|
|
5055
|
+
let bearing = view?.initialBearingDeg ?? 0;
|
|
5056
|
+
let pitch = 0;
|
|
5057
|
+
let disposed = false;
|
|
5058
|
+
if (!view && deps.focalWorld) {
|
|
5059
|
+
const p = deps.camera.position;
|
|
5060
|
+
const dx = deps.focalWorld[0] - p.x;
|
|
5061
|
+
const dy = deps.focalWorld[1] - p.y;
|
|
5062
|
+
const dz = deps.focalWorld[2] - p.z;
|
|
5063
|
+
const flat = Math.hypot(dx, dz);
|
|
5064
|
+
if (flat > 1e-3) {
|
|
5065
|
+
bearing = Math.atan2(dx, -dz) * 180 / Math.PI;
|
|
5066
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, Math.atan2(dy, flat) * 180 / Math.PI));
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
let sphere = null;
|
|
5070
|
+
if (view) {
|
|
5071
|
+
let image;
|
|
5072
|
+
try {
|
|
5073
|
+
image = await loadImage(view.url);
|
|
5074
|
+
} catch {
|
|
5075
|
+
return null;
|
|
5076
|
+
}
|
|
5077
|
+
try {
|
|
5078
|
+
sphere = createPanoSphere(deps.gl, image);
|
|
5079
|
+
} catch {
|
|
5080
|
+
return null;
|
|
5081
|
+
}
|
|
5082
|
+
sphere.mesh.renderOrder = 999;
|
|
5083
|
+
sphere.mesh.setParent(deps.scene);
|
|
5084
|
+
}
|
|
5085
|
+
const priorFov = deps.camera.fov;
|
|
5086
|
+
deps.camera.perspective({ fov: VFOV_DEG, aspect: deps.camera.aspect });
|
|
5087
|
+
const priorQuat = deps.camera.quaternion.slice();
|
|
5088
|
+
const aim = () => {
|
|
5089
|
+
const p = deps.camera.position;
|
|
5090
|
+
sphere?.mesh.position.set(p.x, p.y, p.z);
|
|
5091
|
+
const [dx, dy, dz] = bearingPitchToDirection(bearing, pitch);
|
|
5092
|
+
deps.camera.lookAt([p.x + dx, p.y + dy, p.z + dz]);
|
|
5093
|
+
deps.requestRender();
|
|
5094
|
+
};
|
|
5095
|
+
aim();
|
|
5096
|
+
const root = document.createElement("div");
|
|
5097
|
+
root.setAttribute("role", "dialog");
|
|
5098
|
+
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
5099
|
+
Object.assign(root.style, {
|
|
5100
|
+
position: "absolute",
|
|
5101
|
+
inset: "0",
|
|
5102
|
+
zIndex: "10",
|
|
5103
|
+
cursor: "grab",
|
|
5104
|
+
touchAction: "none"
|
|
5105
|
+
});
|
|
5106
|
+
const closeBtn = document.createElement("button");
|
|
5107
|
+
closeBtn.type = "button";
|
|
5108
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
5109
|
+
closeBtn.textContent = "\u2715";
|
|
5110
|
+
Object.assign(closeBtn.style, {
|
|
5111
|
+
position: "absolute",
|
|
5112
|
+
top: "12px",
|
|
5113
|
+
right: "12px",
|
|
5114
|
+
zIndex: "2",
|
|
5115
|
+
width: "34px",
|
|
5116
|
+
height: "34px",
|
|
5117
|
+
borderRadius: "999px",
|
|
5118
|
+
cursor: "pointer",
|
|
5119
|
+
border: "1px solid rgba(255,255,255,0.25)",
|
|
5120
|
+
background: "rgba(8,12,18,0.6)",
|
|
5121
|
+
color: "#e6edf3",
|
|
5122
|
+
fontSize: "15px",
|
|
5123
|
+
lineHeight: "1"
|
|
5124
|
+
});
|
|
5125
|
+
root.appendChild(closeBtn);
|
|
5126
|
+
const hint = document.createElement("div");
|
|
5127
|
+
hint.textContent = "Drag to look around \xB7 Esc to close";
|
|
5128
|
+
Object.assign(hint.style, {
|
|
5129
|
+
position: "absolute",
|
|
5130
|
+
bottom: "12px",
|
|
5131
|
+
left: "0",
|
|
5132
|
+
right: "0",
|
|
5133
|
+
textAlign: "center",
|
|
5134
|
+
color: "rgba(230,237,243,0.7)",
|
|
5135
|
+
font: "12px ui-sans-serif, system-ui, sans-serif",
|
|
5136
|
+
pointerEvents: "none"
|
|
5137
|
+
});
|
|
5138
|
+
root.appendChild(hint);
|
|
5139
|
+
container.appendChild(root);
|
|
5140
|
+
let dragging = false;
|
|
5141
|
+
let lastX = 0;
|
|
5142
|
+
let lastY = 0;
|
|
5143
|
+
const onDown = (e) => {
|
|
5144
|
+
if (e.target === closeBtn) return;
|
|
5145
|
+
dragging = true;
|
|
5146
|
+
lastX = e.clientX;
|
|
5147
|
+
lastY = e.clientY;
|
|
5148
|
+
root.setPointerCapture?.(e.pointerId);
|
|
5149
|
+
root.style.cursor = "grabbing";
|
|
5150
|
+
};
|
|
5151
|
+
const onMove = (e) => {
|
|
5152
|
+
if (!dragging) return;
|
|
5153
|
+
bearing += (e.clientX - lastX) * DEG_PER_PX;
|
|
5154
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.clientY - lastY) * DEG_PER_PX));
|
|
5155
|
+
lastX = e.clientX;
|
|
5156
|
+
lastY = e.clientY;
|
|
5157
|
+
aim();
|
|
5158
|
+
};
|
|
5159
|
+
const onUp = () => {
|
|
5160
|
+
dragging = false;
|
|
5161
|
+
root.style.cursor = "grab";
|
|
5162
|
+
};
|
|
5163
|
+
root.addEventListener("pointerdown", onDown);
|
|
5164
|
+
root.addEventListener("pointermove", onMove);
|
|
5165
|
+
root.addEventListener("pointerup", onUp);
|
|
5166
|
+
root.addEventListener("pointercancel", onUp);
|
|
5167
|
+
const onKey = (e) => {
|
|
5168
|
+
if (e.key !== "Escape" || disposed) return;
|
|
5169
|
+
e.stopPropagation();
|
|
5170
|
+
handle.close();
|
|
5171
|
+
};
|
|
5172
|
+
window.addEventListener("keydown", onKey);
|
|
5173
|
+
let raf = 0;
|
|
5174
|
+
const fadeTo = (target, done) => {
|
|
5175
|
+
if (!sphere) {
|
|
5176
|
+
done?.();
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
const from = target === 1 ? 0 : 1;
|
|
5180
|
+
const start = performance.now();
|
|
5181
|
+
const step = () => {
|
|
5182
|
+
if (disposed) return;
|
|
5183
|
+
const t = fadeMs <= 0 ? 1 : Math.min(1, (performance.now() - start) / fadeMs);
|
|
5184
|
+
sphere?.setOpacity(from + (target - from) * t);
|
|
5185
|
+
deps.requestRender();
|
|
5186
|
+
if (t < 1) raf = requestAnimationFrame(step);
|
|
5187
|
+
else done?.();
|
|
5188
|
+
};
|
|
5189
|
+
raf = requestAnimationFrame(step);
|
|
5190
|
+
};
|
|
5191
|
+
fadeTo(1);
|
|
5192
|
+
const teardown = () => {
|
|
5193
|
+
if (disposed) return;
|
|
5194
|
+
disposed = true;
|
|
5195
|
+
cancelAnimationFrame(raf);
|
|
5196
|
+
root.removeEventListener("pointerdown", onDown);
|
|
5197
|
+
root.removeEventListener("pointermove", onMove);
|
|
5198
|
+
root.removeEventListener("pointerup", onUp);
|
|
5199
|
+
root.removeEventListener("pointercancel", onUp);
|
|
5200
|
+
window.removeEventListener("keydown", onKey);
|
|
5201
|
+
root.remove();
|
|
5202
|
+
sphere?.dispose();
|
|
5203
|
+
deps.camera.perspective({ fov: priorFov, aspect: deps.camera.aspect });
|
|
5204
|
+
deps.camera.quaternion.set(priorQuat[0], priorQuat[1], priorQuat[2], priorQuat[3]);
|
|
5205
|
+
deps.requestRender();
|
|
5206
|
+
};
|
|
5207
|
+
const handle = {
|
|
5208
|
+
close() {
|
|
5209
|
+
if (disposed) return;
|
|
5210
|
+
const finish = () => {
|
|
5211
|
+
teardown();
|
|
5212
|
+
opts.onClose?.();
|
|
5213
|
+
};
|
|
5214
|
+
cancelAnimationFrame(raf);
|
|
5215
|
+
fadeTo(0, finish);
|
|
5216
|
+
},
|
|
5217
|
+
dispose: teardown
|
|
5218
|
+
};
|
|
5219
|
+
closeBtn.addEventListener("click", () => handle.close());
|
|
5220
|
+
return handle;
|
|
5221
|
+
}
|
|
5222
|
+
|
|
4753
5223
|
// src/view3d/analytics.ts
|
|
4754
5224
|
var now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
4755
5225
|
var Analytics3D = class {
|
|
@@ -4961,7 +5431,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4961
5431
|
];
|
|
4962
5432
|
const placeCameraFinal = (finalPos, focal) => {
|
|
4963
5433
|
orbit.camera.position.set(finalPos[0], finalPos[1], finalPos[2]);
|
|
4964
|
-
orbit.camera.lookAt(new
|
|
5434
|
+
orbit.camera.lookAt(new import_ogl8.Vec3(focal[0], focal[1], focal[2]));
|
|
4965
5435
|
orbit.camera.fov = FOV_END;
|
|
4966
5436
|
orbit.camera.updateProjectionMatrix();
|
|
4967
5437
|
};
|
|
@@ -5024,7 +5494,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5024
5494
|
zIndex: "4"
|
|
5025
5495
|
});
|
|
5026
5496
|
overviewChip.addEventListener("click", () => {
|
|
5027
|
-
if (disposed || frozen) return;
|
|
5497
|
+
if (disposed || frozen || panorama) return;
|
|
5028
5498
|
cancelFlight();
|
|
5029
5499
|
removeArriveChip();
|
|
5030
5500
|
orbit.frameSoft(model.bounds, stageAzimuth);
|
|
@@ -5052,18 +5522,36 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5052
5522
|
}
|
|
5053
5523
|
if (disposed || gen !== flightGen) return;
|
|
5054
5524
|
removeArriveChip();
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5525
|
+
const onClose = () => {
|
|
5526
|
+
panorama = null;
|
|
5527
|
+
labelOverlay.setVisible(true);
|
|
5528
|
+
frozen = false;
|
|
5529
|
+
analytics.panoramaClosed();
|
|
5530
|
+
orbit.resumeAfterFlight(model.focalWorld);
|
|
5531
|
+
loop.requestRender();
|
|
5532
|
+
showArriveChip(seatId, flightGen);
|
|
5533
|
+
};
|
|
5534
|
+
const sceneMode = view.generated === true;
|
|
5535
|
+
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5536
|
+
gl: glctx.gl,
|
|
5537
|
+
scene: gpu.main,
|
|
5538
|
+
camera: orbit.camera,
|
|
5539
|
+
requestRender: () => loop.requestRender(),
|
|
5540
|
+
focalWorld: model.focalWorld
|
|
5541
|
+
}, { fadeMs, seatLabel: seatId, onClose }) : null;
|
|
5542
|
+
if (disposed || gen !== flightGen) {
|
|
5543
|
+
spherical?.dispose();
|
|
5544
|
+
return;
|
|
5545
|
+
}
|
|
5546
|
+
if (spherical) {
|
|
5547
|
+
orbit.syncFromCamera();
|
|
5548
|
+
labelOverlay.setVisible(false);
|
|
5549
|
+
frozen = false;
|
|
5550
|
+
loop.requestRender();
|
|
5551
|
+
panorama = spherical;
|
|
5552
|
+
} else {
|
|
5553
|
+
panorama = mountPanorama(container, view, { fadeMs, seatLabel: seatId, onClose });
|
|
5554
|
+
}
|
|
5067
5555
|
analytics.panoramaOpened();
|
|
5068
5556
|
};
|
|
5069
5557
|
const cancelFlight = () => {
|
|
@@ -5096,7 +5584,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
5096
5584
|
showArriveChip(seatId, gen);
|
|
5097
5585
|
return Promise.resolve();
|
|
5098
5586
|
}
|
|
5099
|
-
const startQuat = new
|
|
5587
|
+
const startQuat = new import_ogl8.Quat().copy(orbit.camera.quaternion);
|
|
5100
5588
|
const endQuat = lookAtQuat(orbit.camera, finalPos, focal);
|
|
5101
5589
|
loop.requestRender();
|
|
5102
5590
|
return cinematic.start(waypoints, startQuat, endQuat).then(() => {
|