@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.js
CHANGED
|
@@ -28,13 +28,36 @@ var SEAT_STATE_COLORS = {
|
|
|
28
28
|
selected: [0.24, 0.74, 1],
|
|
29
29
|
dimmed: [0.28, 0.32, 0.37]
|
|
30
30
|
};
|
|
31
|
+
function upholsteryTone(rgb) {
|
|
32
|
+
const luma = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
|
33
|
+
const SAT = 0.42;
|
|
34
|
+
const VALUE = 0.62;
|
|
35
|
+
return [
|
|
36
|
+
(luma + (rgb[0] - luma) * SAT) * VALUE,
|
|
37
|
+
(luma + (rgb[1] - luma) * SAT) * VALUE,
|
|
38
|
+
(luma + (rgb[2] - luma) * SAT) * VALUE
|
|
39
|
+
];
|
|
40
|
+
}
|
|
31
41
|
var STRUCTURE = {
|
|
32
42
|
ground: [0.07, 0.085, 0.11],
|
|
33
43
|
tierTop: [0.24, 0.28, 0.34],
|
|
34
44
|
tierWall: [0.17, 0.2, 0.25],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The lit performance surface.
|
|
47
|
+
*
|
|
48
|
+
* This was [0.42, 0.36, 0.26] and commented "slightly emissive read", which it
|
|
49
|
+
* was not — under the scene's rig it resolved to a mid-brown slab, and from a
|
|
50
|
+
* seat the stage was the DIMMEST large surface in a dark hall. That is exactly
|
|
51
|
+
* backwards: the stage is the one place in a venue with light pointed at it,
|
|
52
|
+
* and it is what a buyer's eye should land on when they arrive at their seat.
|
|
53
|
+
*
|
|
54
|
+
* Bright and warm enough to hold that role against the surrounding structure,
|
|
55
|
+
* which sits around 0.24. An authored `fill` still tints it (see `buildShape`),
|
|
56
|
+
* so an organizer's own stage colour survives — it is just no longer lit like
|
|
57
|
+
* a basement.
|
|
58
|
+
*/
|
|
59
|
+
stageTop: [0.86, 0.64, 0.36],
|
|
60
|
+
stageWall: [0.34, 0.26, 0.18],
|
|
38
61
|
decorTop: [0.22, 0.25, 0.29],
|
|
39
62
|
decorWall: [0.15, 0.17, 0.2],
|
|
40
63
|
gaTop: [0.24, 0.28, 0.33],
|
|
@@ -1069,7 +1092,8 @@ function rectPolygon(x, y, w, h) {
|
|
|
1069
1092
|
}
|
|
1070
1093
|
|
|
1071
1094
|
// src/view3d/scene/seatChair.ts
|
|
1072
|
-
var CHAIR_PART = { pedestal: 0, pad: 1, back: 2 };
|
|
1095
|
+
var CHAIR_PART = { pedestal: 0, pad: 1, back: 2, body: 3, head: 4 };
|
|
1096
|
+
var CHAIR_PART_OCCUPANT_MIN = CHAIR_PART.body;
|
|
1073
1097
|
var CHAIR_PITCH_FRACTION = 0.44;
|
|
1074
1098
|
var CHAIR_HALF_WIDTH_MIN_M = 0.15;
|
|
1075
1099
|
var CHAIR_HALF_WIDTH_MAX_M = 0.3;
|
|
@@ -1099,6 +1123,37 @@ var BOXES = [
|
|
|
1099
1123
|
part: CHAIR_PART.back,
|
|
1100
1124
|
min: [-1, BACK_BASE_M, -1],
|
|
1101
1125
|
max: [1, 0.92, -0.72]
|
|
1126
|
+
},
|
|
1127
|
+
// --- the occupant ---------------------------------------------------------
|
|
1128
|
+
//
|
|
1129
|
+
// A hall with every seat empty reads as an architectural model, not a venue.
|
|
1130
|
+
// The 2048-px panorama this replaced drew a crowd; losing it was the price of
|
|
1131
|
+
// sharpness, and this is how it is bought back — in geometry, where there is
|
|
1132
|
+
// no resolution ceiling.
|
|
1133
|
+
//
|
|
1134
|
+
// Deliberately two blocks and no limbs. At the range these are visible a
|
|
1135
|
+
// person is a torso and a head, and every extra part multiplies by the number
|
|
1136
|
+
// of occupied seats in view. The silhouette is what carries it, exactly as it
|
|
1137
|
+
// does in the generated panorama's head-and-shoulder figures.
|
|
1138
|
+
//
|
|
1139
|
+
// Sized as a seated adult against the 0.92 m chair back: hips at the pad top,
|
|
1140
|
+
// shoulders just above the back panel, head clear of it. Torso is narrower
|
|
1141
|
+
// than the chair so neighbours never interpenetrate at any pitch, and it sits
|
|
1142
|
+
// forward of the back panel rather than inside it.
|
|
1143
|
+
{
|
|
1144
|
+
part: CHAIR_PART.body,
|
|
1145
|
+
min: [-0.66, PAD_TOP_M, -0.58],
|
|
1146
|
+
max: [0.66, 1, 0.26]
|
|
1147
|
+
},
|
|
1148
|
+
// The head is deliberately SMALL. Sized by eye against the chair it came out
|
|
1149
|
+
// near-cubic and read as Lego; a real head is about 0.16 m across and 0.22 m
|
|
1150
|
+
// tall, which against a 0.24 m chair half-width is roughly a third of the
|
|
1151
|
+
// chair's width and clearly taller than it is wide. Getting this ratio wrong
|
|
1152
|
+
// is what makes a crowd look like toys rather than people.
|
|
1153
|
+
{
|
|
1154
|
+
part: CHAIR_PART.head,
|
|
1155
|
+
min: [-0.34, 1.03, -0.4],
|
|
1156
|
+
max: [0.34, 1.27, 0.02]
|
|
1102
1157
|
}
|
|
1103
1158
|
];
|
|
1104
1159
|
var FACES = [
|
|
@@ -1216,6 +1271,15 @@ function computeSeatYaw(iPosition, count, rowIdAt, focal) {
|
|
|
1216
1271
|
|
|
1217
1272
|
// src/view3d/scene/seatInstances.ts
|
|
1218
1273
|
var SEAT_DOT_RADIUS_M = 0.22;
|
|
1274
|
+
function seatOccupantSeed(stateIndex, seatIndex) {
|
|
1275
|
+
const state = SEAT_STATES[stateIndex];
|
|
1276
|
+
if (state !== "sold" && state !== "held") return -1;
|
|
1277
|
+
let h = (seatIndex + 1) * 2654435761;
|
|
1278
|
+
h ^= h >>> 15;
|
|
1279
|
+
h = Math.imul(h, 2246822519);
|
|
1280
|
+
h ^= h >>> 13;
|
|
1281
|
+
return (h >>> 0) % 1e5 / 1e5;
|
|
1282
|
+
}
|
|
1219
1283
|
var SEAT_PITCH_FRACTION = 0.42;
|
|
1220
1284
|
function nearestNeighbourSpacing(seats) {
|
|
1221
1285
|
const n = seats.length;
|
|
@@ -1278,10 +1342,11 @@ function seatSurfaceY(seat) {
|
|
|
1278
1342
|
if (Number.isFinite(eye)) return Math.max(0, eye - SEATED_EYE_HEIGHT_M);
|
|
1279
1343
|
return 0;
|
|
1280
1344
|
}
|
|
1281
|
-
function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
1345
|
+
function buildSeatInstances(seats, initial, surfaces, seatFloor, categoryColor) {
|
|
1282
1346
|
const count = seats.length;
|
|
1283
1347
|
const iPosition = new Float32Array(count * 3);
|
|
1284
1348
|
const iState = new Float32Array(count);
|
|
1349
|
+
const iCategory = new Float32Array(count * 3);
|
|
1285
1350
|
const iMaxRadius = new Float32Array(count);
|
|
1286
1351
|
const iChairWidth = new Float32Array(count);
|
|
1287
1352
|
const iRing = new Float32Array(count * 3);
|
|
@@ -1297,6 +1362,10 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1297
1362
|
iPosition[i * 3 + 1] = surfaces ? surfaces.seatDeckY(i) : seatSurfaceY(seat);
|
|
1298
1363
|
iPosition[i * 3 + 2] = seat.y * M;
|
|
1299
1364
|
iState[i] = seatStateIndex(initial ? initial(seat) : "available");
|
|
1365
|
+
const cat = hexToRgb(categoryColor?.get(seat.categoryKey) ?? "#6e7bff") ?? [0.43, 0.48, 1];
|
|
1366
|
+
iCategory[i * 3] = cat[0];
|
|
1367
|
+
iCategory[i * 3 + 1] = cat[1];
|
|
1368
|
+
iCategory[i * 3 + 2] = cat[2];
|
|
1300
1369
|
if (seat.accessibility?.length) {
|
|
1301
1370
|
const rgb = hexToRgb(accessibilityRingColor(seat.accessibility));
|
|
1302
1371
|
if (rgb) {
|
|
@@ -1311,6 +1380,7 @@ function buildSeatInstances(seats, initial, surfaces, seatFloor) {
|
|
|
1311
1380
|
count,
|
|
1312
1381
|
iPosition,
|
|
1313
1382
|
iState,
|
|
1383
|
+
iCategory,
|
|
1314
1384
|
iMaxRadius,
|
|
1315
1385
|
iRing,
|
|
1316
1386
|
idToIndex,
|
|
@@ -2651,11 +2721,27 @@ function shapePolygon(shape) {
|
|
|
2651
2721
|
}
|
|
2652
2722
|
return null;
|
|
2653
2723
|
}
|
|
2654
|
-
function buildShape(builder, shape, base, S) {
|
|
2724
|
+
function buildShape(builder, shape, base, S, stages) {
|
|
2655
2725
|
const poly = shapePolygon(shape);
|
|
2656
2726
|
if (!poly) return;
|
|
2657
2727
|
const isStage = shape.role === "stage";
|
|
2658
2728
|
const height = isStage ? base + 1 : base + 0.25;
|
|
2729
|
+
if (isStage && stages) {
|
|
2730
|
+
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
2731
|
+
for (const pt of poly) {
|
|
2732
|
+
if (pt.x < minX) minX = pt.x;
|
|
2733
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
2734
|
+
if (pt.y < minZ) minZ = pt.y;
|
|
2735
|
+
if (pt.y > maxZ) maxZ = pt.y;
|
|
2736
|
+
}
|
|
2737
|
+
stages.push({
|
|
2738
|
+
cx: (minX + maxX) / 2 * M,
|
|
2739
|
+
cz: (minZ + maxZ) / 2 * M,
|
|
2740
|
+
y: height,
|
|
2741
|
+
halfX: Math.max((maxX - minX) / 2 * M, 0.5),
|
|
2742
|
+
halfZ: Math.max((maxZ - minZ) / 2 * M, 0.5)
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2659
2745
|
const colTop = tintTop(hexToRgb(shape.fill), isStage ? S.stageTop : S.decorTop);
|
|
2660
2746
|
const colWall = isStage ? S.stageWall : S.decorWall;
|
|
2661
2747
|
extrudePrism(builder, poly, void 0, () => height, base, colTop, colWall, AO);
|
|
@@ -2765,6 +2851,7 @@ function buildSceneModel(input) {
|
|
|
2765
2851
|
const sectionFills = resolveSectionFills(doc, seats);
|
|
2766
2852
|
const catColor = /* @__PURE__ */ new Map();
|
|
2767
2853
|
for (const c of doc.categories ?? []) catColor.set(c.key, c.color);
|
|
2854
|
+
const stageBounds = [];
|
|
2768
2855
|
const surfaces = buildVenueSurfaces(units, seats);
|
|
2769
2856
|
const seatFloor = new Float32Array(seats.length);
|
|
2770
2857
|
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
@@ -2774,7 +2861,7 @@ function buildSceneModel(input) {
|
|
|
2774
2861
|
const siblings = unit.objects.filter((o) => o.type === "section" && !!o.outline && o.outline.length >= 3);
|
|
2775
2862
|
for (const o of unit.objects) {
|
|
2776
2863
|
if (o.type === "section") buildTier(builder, o, unit, sectionFill(o, sectionFills), surfaces.bySection.get(o.id), claimed, siblings, S);
|
|
2777
|
-
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S);
|
|
2864
|
+
else if (o.type === "shape") buildShape(builder, o, unit.baseHeightM, S, stageBounds);
|
|
2778
2865
|
else if (o.type === "gaArea") buildGa(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2779
2866
|
else if (o.type === "booth") buildBooth(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
2780
2867
|
else if (o.type === "table") buildTable(builder, o, unit.baseHeightM, hexToRgb(catColor.get(o.categoryKey)), S);
|
|
@@ -2783,7 +2870,7 @@ function buildSceneModel(input) {
|
|
|
2783
2870
|
}
|
|
2784
2871
|
const focal = doc.focalPoint ?? { x: (fp.minX + fp.maxX) / 2, y: (fp.minY + fp.maxY) / 2 };
|
|
2785
2872
|
const solids = mergeMeshData([builder.build()]);
|
|
2786
|
-
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor);
|
|
2873
|
+
const seatData = buildSeatInstances(seats, input.initialState, surfaces, seatFloor, catColor);
|
|
2787
2874
|
const zoneDefs = doc.zones ?? [];
|
|
2788
2875
|
const zones = [];
|
|
2789
2876
|
if (zoneDefs.length) {
|
|
@@ -3036,6 +3123,7 @@ function buildSceneModel(input) {
|
|
|
3036
3123
|
// Look-at target ~1.5 m up so a seated camera aims slightly down at the stage.
|
|
3037
3124
|
focalWorld: [focal.x * M, 1.5, focal.y * M],
|
|
3038
3125
|
zones,
|
|
3126
|
+
stages: stageBounds,
|
|
3039
3127
|
sections,
|
|
3040
3128
|
labels,
|
|
3041
3129
|
floors
|
|
@@ -3081,6 +3169,20 @@ var LabelOverlay = class {
|
|
|
3081
3169
|
if (opts.fontFamily) s.fontFamily = opts.fontFamily;
|
|
3082
3170
|
container.appendChild(this.root);
|
|
3083
3171
|
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Hide or show the whole overlay without disturbing which labels exist.
|
|
3174
|
+
*
|
|
3175
|
+
* The in-scene panorama sphere is GL, so it draws BEHIND every DOM label —
|
|
3176
|
+
* row and seat labels floated on top of a 360 photo until this existed. The
|
|
3177
|
+
* old DOM panorama never needed it because its opaque div covered them.
|
|
3178
|
+
*
|
|
3179
|
+
* Deliberately not `setLabels([])`: that destroys the nodes and the declutter
|
|
3180
|
+
* state, so closing the panorama would rebuild and re-rank the whole overlay
|
|
3181
|
+
* and flash. Visibility is a view concern, not a data one.
|
|
3182
|
+
*/
|
|
3183
|
+
setVisible(visible) {
|
|
3184
|
+
this.root.style.visibility = visible ? "" : "hidden";
|
|
3185
|
+
}
|
|
3084
3186
|
setLabels(labels) {
|
|
3085
3187
|
this.labels = labels;
|
|
3086
3188
|
for (const [id, node] of this.nodes) {
|
|
@@ -3344,13 +3446,14 @@ var CHAIR_VERT = (
|
|
|
3344
3446
|
precision highp float;
|
|
3345
3447
|
in vec3 position; // local: x/z in units of the seat radius, y in METRES
|
|
3346
3448
|
in vec3 normal;
|
|
3347
|
-
in float part; // 0 = pedestal, 1 = pad, 2 = back
|
|
3449
|
+
in float part; // 0 = pedestal, 1 = pad, 2 = back, 3 = body, 4 = head
|
|
3348
3450
|
in vec3 iOffset; // per-instance world deck point (identical to the dot's)
|
|
3349
3451
|
in vec3 iColor; // per-instance state colour
|
|
3350
3452
|
in float iRadius; // per-instance horizontal half-width, world metres
|
|
3351
3453
|
in float iYaw; // per-instance facing, radians (local +Z -> facing dir)
|
|
3352
3454
|
in vec3 iRing; // accommodation ring colour; (0,0,0) = not accessible
|
|
3353
3455
|
in float iFloor;
|
|
3456
|
+
in float iSeed; // <0 = seat is empty; else per-person hash in [0,1)
|
|
3354
3457
|
uniform mat4 modelViewMatrix;
|
|
3355
3458
|
uniform mat4 projectionMatrix;
|
|
3356
3459
|
uniform float uChairFull;
|
|
@@ -3366,6 +3469,8 @@ out float vPart;
|
|
|
3366
3469
|
out float vHeight; // local height in metres, for the vertical occlusion ramp
|
|
3367
3470
|
out vec3 vRing;
|
|
3368
3471
|
out float vDim;
|
|
3472
|
+
out float vOccupant; // 1 = this vertex belongs to a person, not to the chair
|
|
3473
|
+
out vec3 vOccupantTint;
|
|
3369
3474
|
${CHAIR_WEIGHT_GLSL}
|
|
3370
3475
|
void main() {
|
|
3371
3476
|
vec4 anchor = modelViewMatrix * vec4(iOffset, 1.0);
|
|
@@ -3374,12 +3479,35 @@ void main() {
|
|
|
3374
3479
|
// chairs, but nobody gets a short one (people are the same height at every
|
|
3375
3480
|
// seat pitch).
|
|
3376
3481
|
vec3 p = position;
|
|
3482
|
+
// The occupant. Parts 3 and 4 are a person; everything below is furniture.
|
|
3483
|
+
//
|
|
3484
|
+
// One instanced mesh draws both an empty seat and a taken one, because the
|
|
3485
|
+
// alternative \u2014 a second geometry and a second draw call gathered per frame \u2014
|
|
3486
|
+
// would double the near-field cost to show what is already known per instance.
|
|
3487
|
+
// An unoccupied seat collapses its person to a point at the seat, which the
|
|
3488
|
+
// rasteriser discards, exactly as the chair itself collapses at w=0.
|
|
3489
|
+
float occupant = step(2.5, part);
|
|
3490
|
+
float taken = step(0.0, iSeed);
|
|
3491
|
+
vOccupant = occupant;
|
|
3492
|
+
if (occupant > 0.5) {
|
|
3493
|
+
if (taken < 0.5) {
|
|
3494
|
+
p = vec3(0.0); // empty seat: no person
|
|
3495
|
+
} else {
|
|
3496
|
+
// Vary the build so a sold-out row is people rather than a rank of
|
|
3497
|
+
// identical mannequins: +/-6% height and +/-8% width off the hash.
|
|
3498
|
+
p.y *= 0.94 + 0.12 * iSeed;
|
|
3499
|
+
p.xz *= 0.92 + 0.16 * fract(iSeed * 7.13);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3377
3502
|
p.xz *= iRadius;
|
|
3378
3503
|
// 2. Lean the back. Done here rather than in the base mesh so the lean is a
|
|
3379
3504
|
// real angle in METRES \u2014 baked into the mesh it would scale with the seat's
|
|
3380
3505
|
// width and the same chair would lean 20 degrees on a wide stadium row and
|
|
3381
3506
|
// 6 on a tight theatre one.
|
|
3382
|
-
|
|
3507
|
+
// Only the BACK panel rakes. The old test (part > 1.5) meant "the back", and
|
|
3508
|
+
// now also catches the occupant \u2014 leaning a person by the panel's rule would
|
|
3509
|
+
// translate their head backwards by a rake measured from the panel's base.
|
|
3510
|
+
float rake = (part > 1.5 && part < 2.5) ? max(p.y - uBackBase, 0.0) * uBackRake : 0.0;
|
|
3383
3511
|
p.z -= rake;
|
|
3384
3512
|
// 3. Scale-in. At w=0 the chair is a point at the seat, under a dot at full
|
|
3385
3513
|
// opacity \u2014 which is what makes the handover invisible. sqrt front-loads
|
|
@@ -3393,7 +3521,7 @@ void main() {
|
|
|
3393
3521
|
// unchanged; and the rake is a shear, whose normal transform adds a y term.
|
|
3394
3522
|
// Skipping either lights the raked back as though it were still vertical.
|
|
3395
3523
|
vec3 n = vec3(normal.x / iRadius, normal.y, normal.z / iRadius);
|
|
3396
|
-
if (part > 1.5) n.y += uBackRake * n.z;
|
|
3524
|
+
if (part > 1.5 && part < 2.5) n.y += uBackRake * n.z;
|
|
3397
3525
|
n = normalize(n);
|
|
3398
3526
|
vec3 rn = vec3(n.x * c + n.z * s, n.y, -n.x * s + n.z * c);
|
|
3399
3527
|
vec4 mv = modelViewMatrix * vec4(iOffset + rp, 1.0);
|
|
@@ -3401,6 +3529,15 @@ void main() {
|
|
|
3401
3529
|
vNormalWorld = rn;
|
|
3402
3530
|
vNormalView = normalize(mat3(modelViewMatrix) * rn);
|
|
3403
3531
|
vColor = iColor;
|
|
3532
|
+
// Occupant colour, resolved here so the fragment stage needs no extra
|
|
3533
|
+
// varyings beyond this one. A person is NOT painted in the seat's state
|
|
3534
|
+
// colour: a sold seat is red, and a hall of red people reads as a warning,
|
|
3535
|
+
// not an audience. Hair/clothing for the body, a warm tone for the head,
|
|
3536
|
+
// both varied by the same per-person hash.
|
|
3537
|
+
float t = fract(iSeed * 3.71);
|
|
3538
|
+
vec3 clothes = mix(vec3(0.13, 0.15, 0.20), vec3(0.34, 0.30, 0.36), t);
|
|
3539
|
+
vec3 skin = mix(vec3(0.52, 0.38, 0.29), vec3(0.86, 0.70, 0.58), fract(iSeed * 11.3));
|
|
3540
|
+
vOccupantTint = (part > 3.5) ? skin : clothes;
|
|
3404
3541
|
vPart = part;
|
|
3405
3542
|
vHeight = position.y;
|
|
3406
3543
|
vRing = iRing;
|
|
@@ -3420,6 +3557,8 @@ in float vPart;
|
|
|
3420
3557
|
in float vHeight;
|
|
3421
3558
|
in vec3 vRing;
|
|
3422
3559
|
in float vDim;
|
|
3560
|
+
in float vOccupant;
|
|
3561
|
+
in vec3 vOccupantTint;
|
|
3423
3562
|
uniform vec3 uKeyDir;
|
|
3424
3563
|
uniform vec3 uFadeColor;
|
|
3425
3564
|
out vec4 fragColor;
|
|
@@ -3432,6 +3571,7 @@ void main() {
|
|
|
3432
3571
|
vec3 fillDir = normalize(vec3(-uKeyDir.x, 0.25, -uKeyDir.z));
|
|
3433
3572
|
float fill = max(dot(N, fillDir), 0.0);
|
|
3434
3573
|
vec3 tint = vColor;
|
|
3574
|
+
if (vOccupant > 0.5) tint = vOccupantTint;
|
|
3435
3575
|
// The accommodation ring, kept legible once the dot (which drew it) is gone:
|
|
3436
3576
|
// an accessible seat's PEDESTAL is painted in the ring colour, so the marker
|
|
3437
3577
|
// survives to close range instead of vanishing exactly when the buyer arrives.
|
|
@@ -3440,12 +3580,16 @@ void main() {
|
|
|
3440
3580
|
// Pad brightest, back a step below it, pedestal darkest. Three untextured
|
|
3441
3581
|
// boxes only read as one object if they are separated tonally \u2014 with a single
|
|
3442
3582
|
// flat colour the chair silhouettes as a crate.
|
|
3443
|
-
|
|
3583
|
+
// A person is lit as a person, not as upholstery: no part shading, and less
|
|
3584
|
+
// of the pad's sheen, so a head does not read as a polished box.
|
|
3585
|
+
float partShade = vOccupant > 0.5 ? 0.95 : (vPart < 0.5 ? 0.50 : (vPart < 1.5 ? 1.10 : 0.80));
|
|
3444
3586
|
// Cheap vertical occlusion: a chair is in a dense row, so the closer a surface
|
|
3445
3587
|
// sits to the deck the less sky it can actually see. This is the depth cue \u2014
|
|
3446
3588
|
// without it the pad top, the back and the deck all resolve to the same flat
|
|
3447
3589
|
// value and the row loses its form entirely.
|
|
3448
|
-
|
|
3590
|
+
// Occupants rise above the 0.92 m chair back, so their ramp uses their own
|
|
3591
|
+
// height or every head would clamp to full brightness and float.
|
|
3592
|
+
float ao = mix(0.58, 1.0, clamp(vHeight / (vOccupant > 0.5 ? 1.30 : 0.92), 0.0, 1.0));
|
|
3449
3593
|
vec3 base = tint * partShade * ao * (0.52 + 0.40 * hemi) + tint * key * 0.38 + tint * fill * 0.10;
|
|
3450
3594
|
float fres = pow(1.0 - max(dot(normalize(vNormalView), V), 0.0), 3.0);
|
|
3451
3595
|
// A brighter rim than the solids get: it picks out every chair's own edge,
|
|
@@ -3640,12 +3784,13 @@ function createBackgroundProgram(gl, top, bottom) {
|
|
|
3640
3784
|
}
|
|
3641
3785
|
|
|
3642
3786
|
// src/view3d/scene/build.ts
|
|
3643
|
-
function writeSeatColors(iColor, iState, start, count, states) {
|
|
3787
|
+
function writeSeatColors(iColor, iState, start, count, states, iCategory) {
|
|
3644
3788
|
for (let i = start; i < start + count; i++) {
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3647
|
-
iColor[i * 3
|
|
3648
|
-
iColor[i * 3 +
|
|
3789
|
+
const useCategory = iCategory && iState[i] === 0;
|
|
3790
|
+
const c = useCategory ? null : states[iState[i]] ?? states[0];
|
|
3791
|
+
iColor[i * 3] = c ? c[0] : iCategory[i * 3];
|
|
3792
|
+
iColor[i * 3 + 1] = c ? c[1] : iCategory[i * 3 + 1];
|
|
3793
|
+
iColor[i * 3 + 2] = c ? c[2] : iCategory[i * 3 + 2];
|
|
3649
3794
|
}
|
|
3650
3795
|
}
|
|
3651
3796
|
var SEAT_QUAD = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
|
|
@@ -3671,7 +3816,7 @@ function buildGpuScene(gl, model) {
|
|
|
3671
3816
|
const seatProg = createSeatProgram(gl);
|
|
3672
3817
|
const iColor = new Float32Array(model.seats.count * 3);
|
|
3673
3818
|
const stateColors = SEAT_STATES.map((st) => model.theme.seatStates[st]);
|
|
3674
|
-
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors);
|
|
3819
|
+
writeSeatColors(iColor, model.seats.iState, 0, model.seats.count, stateColors, model.seats.iCategory);
|
|
3675
3820
|
const seatGeo = new Geometry(gl, {
|
|
3676
3821
|
position: { size: 2, data: SEAT_QUAD },
|
|
3677
3822
|
iOffset: { size: 3, data: model.seats.iPosition, instanced: 1 },
|
|
@@ -3696,6 +3841,7 @@ function buildGpuScene(gl, model) {
|
|
|
3696
3841
|
const cYaw = new Float32Array(CAP);
|
|
3697
3842
|
const cRing = new Float32Array(CAP * 3);
|
|
3698
3843
|
const cFloor = new Float32Array(CAP);
|
|
3844
|
+
const cSeed = new Float32Array(CAP);
|
|
3699
3845
|
const chairGeo = new Geometry(gl, {
|
|
3700
3846
|
position: { size: 3, data: chairBase.position },
|
|
3701
3847
|
normal: { size: 3, data: chairBase.normal },
|
|
@@ -3706,7 +3852,8 @@ function buildGpuScene(gl, model) {
|
|
|
3706
3852
|
iRadius: { size: 1, data: cRadius, instanced: 1 },
|
|
3707
3853
|
iYaw: { size: 1, data: cYaw, instanced: 1 },
|
|
3708
3854
|
iRing: { size: 3, data: cRing, instanced: 1 },
|
|
3709
|
-
iFloor: { size: 1, data: cFloor, instanced: 1 }
|
|
3855
|
+
iFloor: { size: 1, data: cFloor, instanced: 1 },
|
|
3856
|
+
iSeed: { size: 1, data: cSeed, instanced: 1 }
|
|
3710
3857
|
});
|
|
3711
3858
|
const chairMesh = new Mesh(gl, { geometry: chairGeo, program: chairProg });
|
|
3712
3859
|
chairMesh.frustumCulled = false;
|
|
@@ -3716,10 +3863,18 @@ function buildGpuScene(gl, model) {
|
|
|
3716
3863
|
const src = model.seats;
|
|
3717
3864
|
for (let k = 0; k < nearCount; k++) {
|
|
3718
3865
|
const i = nearIndices[k];
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3866
|
+
const useCategory = src.iCategory && src.iState[i] === 0;
|
|
3867
|
+
const c = useCategory ? null : stateColors[src.iState[i]] ?? stateColors[0];
|
|
3868
|
+
if (c) {
|
|
3869
|
+
cColor[k * 3] = c[0];
|
|
3870
|
+
cColor[k * 3 + 1] = c[1];
|
|
3871
|
+
cColor[k * 3 + 2] = c[2];
|
|
3872
|
+
} else {
|
|
3873
|
+
const u = upholsteryTone([src.iCategory[i * 3], src.iCategory[i * 3 + 1], src.iCategory[i * 3 + 2]]);
|
|
3874
|
+
cColor[k * 3] = u[0];
|
|
3875
|
+
cColor[k * 3 + 1] = u[1];
|
|
3876
|
+
cColor[k * 3 + 2] = u[2];
|
|
3877
|
+
}
|
|
3723
3878
|
}
|
|
3724
3879
|
chairGeo.attributes.iColor.needsUpdate = true;
|
|
3725
3880
|
};
|
|
@@ -3754,6 +3909,7 @@ function buildGpuScene(gl, model) {
|
|
|
3754
3909
|
cRing[k * 3 + 1] = src.iRing[i * 3 + 1];
|
|
3755
3910
|
cRing[k * 3 + 2] = src.iRing[i * 3 + 2];
|
|
3756
3911
|
cFloor[k] = src.iFloor[i];
|
|
3912
|
+
cSeed[k] = seatOccupantSeed(src.iState[i], i);
|
|
3757
3913
|
}
|
|
3758
3914
|
writeChairColors();
|
|
3759
3915
|
chairGeo.attributes.iOffset.needsUpdate = true;
|
|
@@ -3761,6 +3917,7 @@ function buildGpuScene(gl, model) {
|
|
|
3761
3917
|
chairGeo.attributes.iYaw.needsUpdate = true;
|
|
3762
3918
|
chairGeo.attributes.iRing.needsUpdate = true;
|
|
3763
3919
|
chairGeo.attributes.iFloor.needsUpdate = true;
|
|
3920
|
+
chairGeo.attributes.iSeed.needsUpdate = true;
|
|
3764
3921
|
chairGeo.instancedCount = n;
|
|
3765
3922
|
if (!chairMesh.parent) chairMesh.setParent(main);
|
|
3766
3923
|
scene.drawCalls = 4;
|
|
@@ -3771,7 +3928,9 @@ function buildGpuScene(gl, model) {
|
|
|
3771
3928
|
uploadSeatStateRuns(runs) {
|
|
3772
3929
|
if (!runs.length) return;
|
|
3773
3930
|
if (nearCount) writeChairColors();
|
|
3774
|
-
for (const run of runs)
|
|
3931
|
+
for (const run of runs) {
|
|
3932
|
+
writeSeatColors(iColor, model.seats.iState, run.start, run.length, stateColors, model.seats.iCategory);
|
|
3933
|
+
}
|
|
3775
3934
|
const buffer = colorAttr.buffer;
|
|
3776
3935
|
if (!buffer) {
|
|
3777
3936
|
colorAttr.needsUpdate = true;
|
|
@@ -4277,6 +4436,317 @@ function mountPanorama(container, view, opts = {}) {
|
|
|
4277
4436
|
};
|
|
4278
4437
|
}
|
|
4279
4438
|
|
|
4439
|
+
// src/view3d/scene/panoSphere.ts
|
|
4440
|
+
import { Geometry as Geometry3, Mesh as Mesh3, Program as Program4, Texture } from "ogl";
|
|
4441
|
+
var PANO_SEGMENTS_LON = 64;
|
|
4442
|
+
var PANO_SEGMENTS_LAT = 32;
|
|
4443
|
+
var PANO_RADIUS_M = 500;
|
|
4444
|
+
function buildPanoSphere(segmentsLon = PANO_SEGMENTS_LON, segmentsLat = PANO_SEGMENTS_LAT, radius = PANO_RADIUS_M) {
|
|
4445
|
+
const lonCount = segmentsLon + 1;
|
|
4446
|
+
const latCount = segmentsLat + 1;
|
|
4447
|
+
const vertexCount = lonCount * latCount;
|
|
4448
|
+
const position = new Float32Array(vertexCount * 3);
|
|
4449
|
+
const uv = new Float32Array(vertexCount * 2);
|
|
4450
|
+
for (let iLat = 0; iLat < latCount; iLat++) {
|
|
4451
|
+
const v = iLat / segmentsLat;
|
|
4452
|
+
const polar = v * Math.PI;
|
|
4453
|
+
const sinPolar = Math.sin(polar);
|
|
4454
|
+
const cosPolar = Math.cos(polar);
|
|
4455
|
+
for (let iLon = 0; iLon < lonCount; iLon++) {
|
|
4456
|
+
const u = iLon / segmentsLon;
|
|
4457
|
+
const azimuth = (u - 0.5) * Math.PI * 2;
|
|
4458
|
+
const i = iLat * lonCount + iLon;
|
|
4459
|
+
position[i * 3] = radius * sinPolar * Math.sin(azimuth);
|
|
4460
|
+
position[i * 3 + 1] = radius * cosPolar;
|
|
4461
|
+
position[i * 3 + 2] = -radius * sinPolar * Math.cos(azimuth);
|
|
4462
|
+
uv[i * 2] = u;
|
|
4463
|
+
uv[i * 2 + 1] = v;
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
const index = new Uint16Array(segmentsLon * segmentsLat * 6);
|
|
4467
|
+
let w = 0;
|
|
4468
|
+
for (let iLat = 0; iLat < segmentsLat; iLat++) {
|
|
4469
|
+
for (let iLon = 0; iLon < segmentsLon; iLon++) {
|
|
4470
|
+
const a = iLat * lonCount + iLon;
|
|
4471
|
+
const b = a + lonCount;
|
|
4472
|
+
index[w++] = a;
|
|
4473
|
+
index[w++] = b;
|
|
4474
|
+
index[w++] = a + 1;
|
|
4475
|
+
index[w++] = a + 1;
|
|
4476
|
+
index[w++] = b;
|
|
4477
|
+
index[w++] = b + 1;
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
return { position, uv, index };
|
|
4481
|
+
}
|
|
4482
|
+
function bearingPitchToDirection(bearingDeg, pitchDeg) {
|
|
4483
|
+
const yaw = bearingDeg * Math.PI / 180;
|
|
4484
|
+
const pitch = pitchDeg * Math.PI / 180;
|
|
4485
|
+
const cosPitch = Math.cos(pitch);
|
|
4486
|
+
return [cosPitch * Math.sin(yaw), Math.sin(pitch), -cosPitch * Math.cos(yaw)];
|
|
4487
|
+
}
|
|
4488
|
+
var PANO_VERT = (
|
|
4489
|
+
/* glsl */
|
|
4490
|
+
`#version 300 es
|
|
4491
|
+
precision highp float;
|
|
4492
|
+
in vec3 position;
|
|
4493
|
+
in vec2 uv;
|
|
4494
|
+
uniform mat4 modelViewMatrix;
|
|
4495
|
+
uniform mat4 projectionMatrix;
|
|
4496
|
+
out vec2 vUv;
|
|
4497
|
+
void main() {
|
|
4498
|
+
vUv = uv;
|
|
4499
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
4500
|
+
}`
|
|
4501
|
+
);
|
|
4502
|
+
var PANO_FRAG = (
|
|
4503
|
+
/* glsl */
|
|
4504
|
+
`#version 300 es
|
|
4505
|
+
precision highp float;
|
|
4506
|
+
in vec2 vUv;
|
|
4507
|
+
uniform sampler2D uPano;
|
|
4508
|
+
uniform float uOpacity;
|
|
4509
|
+
out vec4 fragColor;
|
|
4510
|
+
void main() {
|
|
4511
|
+
vec3 rgb = texture(uPano, vUv).rgb;
|
|
4512
|
+
fragColor = vec4(rgb, uOpacity);
|
|
4513
|
+
}`
|
|
4514
|
+
);
|
|
4515
|
+
function createPanoSphere(gl, image) {
|
|
4516
|
+
const { position, uv, index } = buildPanoSphere();
|
|
4517
|
+
const geometry = new Geometry3(gl, {
|
|
4518
|
+
position: { size: 3, data: position },
|
|
4519
|
+
uv: { size: 2, data: uv },
|
|
4520
|
+
index: { data: index }
|
|
4521
|
+
});
|
|
4522
|
+
const texture = new Texture(gl, {
|
|
4523
|
+
image,
|
|
4524
|
+
// The seam column is duplicated in geometry, so CLAMP is correct and avoids
|
|
4525
|
+
// a wrapped bilinear tap bleeding the far edge of the image into it.
|
|
4526
|
+
wrapS: gl.CLAMP_TO_EDGE,
|
|
4527
|
+
wrapT: gl.CLAMP_TO_EDGE,
|
|
4528
|
+
generateMipmaps: true,
|
|
4529
|
+
// MUST be false, and this is the one line that decides whether the whole
|
|
4530
|
+
// panorama is upside down.
|
|
4531
|
+
//
|
|
4532
|
+
// OGL defaults `flipY` to true for 2D textures, which is right for the usual
|
|
4533
|
+
// case: a UV of 0 means the BOTTOM of a quad, an image's first row is its
|
|
4534
|
+
// TOP, and flipping on upload reconciles the two. This sphere is the other
|
|
4535
|
+
// case. Its `v` is authored in IMAGE space — v=0 is the top of the equirect
|
|
4536
|
+
// and maps to the top of the sphere (`buildPanoSphere`, and the mapping
|
|
4537
|
+
// `generatePanorama` and the 2D viewer both use). Flipping on upload as well
|
|
4538
|
+
// applies the correction twice: the stadium pitch renders on the ceiling and
|
|
4539
|
+
// the audience sits overhead.
|
|
4540
|
+
//
|
|
4541
|
+
// The geometry tests state the v mapping and pass either way — they never
|
|
4542
|
+
// touch GL — so this is not something a unit test can defend. It was caught
|
|
4543
|
+
// by looking at a stadium.
|
|
4544
|
+
flipY: false
|
|
4545
|
+
});
|
|
4546
|
+
const program = new Program4(gl, {
|
|
4547
|
+
vertex: PANO_VERT,
|
|
4548
|
+
fragment: PANO_FRAG,
|
|
4549
|
+
uniforms: { uPano: { value: texture }, uOpacity: { value: 0 } },
|
|
4550
|
+
transparent: true,
|
|
4551
|
+
depthTest: false,
|
|
4552
|
+
depthWrite: false
|
|
4553
|
+
});
|
|
4554
|
+
const mesh = new Mesh3(gl, { geometry, program });
|
|
4555
|
+
return {
|
|
4556
|
+
mesh,
|
|
4557
|
+
setOpacity(value) {
|
|
4558
|
+
program.uniforms.uOpacity.value = Math.max(0, Math.min(1, value));
|
|
4559
|
+
},
|
|
4560
|
+
dispose() {
|
|
4561
|
+
mesh.setParent(null);
|
|
4562
|
+
geometry.remove();
|
|
4563
|
+
if (texture.texture) gl.deleteTexture(texture.texture);
|
|
4564
|
+
program.remove();
|
|
4565
|
+
}
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
// src/view3d/crossfade/panoramaSphere.ts
|
|
4570
|
+
var DEG_PER_PX = 0.15;
|
|
4571
|
+
function loadImage(url) {
|
|
4572
|
+
return new Promise((resolve, reject) => {
|
|
4573
|
+
const img = new Image();
|
|
4574
|
+
img.crossOrigin = "anonymous";
|
|
4575
|
+
img.onload = () => resolve(img);
|
|
4576
|
+
img.onerror = () => reject(new Error("panorama_image_failed"));
|
|
4577
|
+
img.src = url;
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
async function mountPanoramaSphere(container, view, deps, opts = {}) {
|
|
4581
|
+
const fadeMs = opts.fadeMs ?? 400;
|
|
4582
|
+
let bearing = view?.initialBearingDeg ?? 0;
|
|
4583
|
+
let pitch = 0;
|
|
4584
|
+
let disposed = false;
|
|
4585
|
+
if (!view && deps.focalWorld) {
|
|
4586
|
+
const p = deps.camera.position;
|
|
4587
|
+
const dx = deps.focalWorld[0] - p.x;
|
|
4588
|
+
const dy = deps.focalWorld[1] - p.y;
|
|
4589
|
+
const dz = deps.focalWorld[2] - p.z;
|
|
4590
|
+
const flat = Math.hypot(dx, dz);
|
|
4591
|
+
if (flat > 1e-3) {
|
|
4592
|
+
bearing = Math.atan2(dx, -dz) * 180 / Math.PI;
|
|
4593
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, Math.atan2(dy, flat) * 180 / Math.PI));
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
let sphere = null;
|
|
4597
|
+
if (view) {
|
|
4598
|
+
let image;
|
|
4599
|
+
try {
|
|
4600
|
+
image = await loadImage(view.url);
|
|
4601
|
+
} catch {
|
|
4602
|
+
return null;
|
|
4603
|
+
}
|
|
4604
|
+
try {
|
|
4605
|
+
sphere = createPanoSphere(deps.gl, image);
|
|
4606
|
+
} catch {
|
|
4607
|
+
return null;
|
|
4608
|
+
}
|
|
4609
|
+
sphere.mesh.renderOrder = 999;
|
|
4610
|
+
sphere.mesh.setParent(deps.scene);
|
|
4611
|
+
}
|
|
4612
|
+
const priorFov = deps.camera.fov;
|
|
4613
|
+
deps.camera.perspective({ fov: VFOV_DEG, aspect: deps.camera.aspect });
|
|
4614
|
+
const priorQuat = deps.camera.quaternion.slice();
|
|
4615
|
+
const aim = () => {
|
|
4616
|
+
const p = deps.camera.position;
|
|
4617
|
+
sphere?.mesh.position.set(p.x, p.y, p.z);
|
|
4618
|
+
const [dx, dy, dz] = bearingPitchToDirection(bearing, pitch);
|
|
4619
|
+
deps.camera.lookAt([p.x + dx, p.y + dy, p.z + dz]);
|
|
4620
|
+
deps.requestRender();
|
|
4621
|
+
};
|
|
4622
|
+
aim();
|
|
4623
|
+
const root = document.createElement("div");
|
|
4624
|
+
root.setAttribute("role", "dialog");
|
|
4625
|
+
root.setAttribute("aria-label", opts.seatLabel ? `View from ${opts.seatLabel}` : "View from seat");
|
|
4626
|
+
Object.assign(root.style, {
|
|
4627
|
+
position: "absolute",
|
|
4628
|
+
inset: "0",
|
|
4629
|
+
zIndex: "10",
|
|
4630
|
+
cursor: "grab",
|
|
4631
|
+
touchAction: "none"
|
|
4632
|
+
});
|
|
4633
|
+
const closeBtn = document.createElement("button");
|
|
4634
|
+
closeBtn.type = "button";
|
|
4635
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
4636
|
+
closeBtn.textContent = "\u2715";
|
|
4637
|
+
Object.assign(closeBtn.style, {
|
|
4638
|
+
position: "absolute",
|
|
4639
|
+
top: "12px",
|
|
4640
|
+
right: "12px",
|
|
4641
|
+
zIndex: "2",
|
|
4642
|
+
width: "34px",
|
|
4643
|
+
height: "34px",
|
|
4644
|
+
borderRadius: "999px",
|
|
4645
|
+
cursor: "pointer",
|
|
4646
|
+
border: "1px solid rgba(255,255,255,0.25)",
|
|
4647
|
+
background: "rgba(8,12,18,0.6)",
|
|
4648
|
+
color: "#e6edf3",
|
|
4649
|
+
fontSize: "15px",
|
|
4650
|
+
lineHeight: "1"
|
|
4651
|
+
});
|
|
4652
|
+
root.appendChild(closeBtn);
|
|
4653
|
+
const hint = document.createElement("div");
|
|
4654
|
+
hint.textContent = "Drag to look around \xB7 Esc to close";
|
|
4655
|
+
Object.assign(hint.style, {
|
|
4656
|
+
position: "absolute",
|
|
4657
|
+
bottom: "12px",
|
|
4658
|
+
left: "0",
|
|
4659
|
+
right: "0",
|
|
4660
|
+
textAlign: "center",
|
|
4661
|
+
color: "rgba(230,237,243,0.7)",
|
|
4662
|
+
font: "12px ui-sans-serif, system-ui, sans-serif",
|
|
4663
|
+
pointerEvents: "none"
|
|
4664
|
+
});
|
|
4665
|
+
root.appendChild(hint);
|
|
4666
|
+
container.appendChild(root);
|
|
4667
|
+
let dragging = false;
|
|
4668
|
+
let lastX = 0;
|
|
4669
|
+
let lastY = 0;
|
|
4670
|
+
const onDown = (e) => {
|
|
4671
|
+
if (e.target === closeBtn) return;
|
|
4672
|
+
dragging = true;
|
|
4673
|
+
lastX = e.clientX;
|
|
4674
|
+
lastY = e.clientY;
|
|
4675
|
+
root.setPointerCapture?.(e.pointerId);
|
|
4676
|
+
root.style.cursor = "grabbing";
|
|
4677
|
+
};
|
|
4678
|
+
const onMove = (e) => {
|
|
4679
|
+
if (!dragging) return;
|
|
4680
|
+
bearing += (e.clientX - lastX) * DEG_PER_PX;
|
|
4681
|
+
pitch = Math.max(-MAX_PITCH_DEG, Math.min(MAX_PITCH_DEG, pitch + (e.clientY - lastY) * DEG_PER_PX));
|
|
4682
|
+
lastX = e.clientX;
|
|
4683
|
+
lastY = e.clientY;
|
|
4684
|
+
aim();
|
|
4685
|
+
};
|
|
4686
|
+
const onUp = () => {
|
|
4687
|
+
dragging = false;
|
|
4688
|
+
root.style.cursor = "grab";
|
|
4689
|
+
};
|
|
4690
|
+
root.addEventListener("pointerdown", onDown);
|
|
4691
|
+
root.addEventListener("pointermove", onMove);
|
|
4692
|
+
root.addEventListener("pointerup", onUp);
|
|
4693
|
+
root.addEventListener("pointercancel", onUp);
|
|
4694
|
+
const onKey = (e) => {
|
|
4695
|
+
if (e.key !== "Escape" || disposed) return;
|
|
4696
|
+
e.stopPropagation();
|
|
4697
|
+
handle.close();
|
|
4698
|
+
};
|
|
4699
|
+
window.addEventListener("keydown", onKey);
|
|
4700
|
+
let raf = 0;
|
|
4701
|
+
const fadeTo = (target, done) => {
|
|
4702
|
+
if (!sphere) {
|
|
4703
|
+
done?.();
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
const from = target === 1 ? 0 : 1;
|
|
4707
|
+
const start = performance.now();
|
|
4708
|
+
const step = () => {
|
|
4709
|
+
if (disposed) return;
|
|
4710
|
+
const t = fadeMs <= 0 ? 1 : Math.min(1, (performance.now() - start) / fadeMs);
|
|
4711
|
+
sphere?.setOpacity(from + (target - from) * t);
|
|
4712
|
+
deps.requestRender();
|
|
4713
|
+
if (t < 1) raf = requestAnimationFrame(step);
|
|
4714
|
+
else done?.();
|
|
4715
|
+
};
|
|
4716
|
+
raf = requestAnimationFrame(step);
|
|
4717
|
+
};
|
|
4718
|
+
fadeTo(1);
|
|
4719
|
+
const teardown = () => {
|
|
4720
|
+
if (disposed) return;
|
|
4721
|
+
disposed = true;
|
|
4722
|
+
cancelAnimationFrame(raf);
|
|
4723
|
+
root.removeEventListener("pointerdown", onDown);
|
|
4724
|
+
root.removeEventListener("pointermove", onMove);
|
|
4725
|
+
root.removeEventListener("pointerup", onUp);
|
|
4726
|
+
root.removeEventListener("pointercancel", onUp);
|
|
4727
|
+
window.removeEventListener("keydown", onKey);
|
|
4728
|
+
root.remove();
|
|
4729
|
+
sphere?.dispose();
|
|
4730
|
+
deps.camera.perspective({ fov: priorFov, aspect: deps.camera.aspect });
|
|
4731
|
+
deps.camera.quaternion.set(priorQuat[0], priorQuat[1], priorQuat[2], priorQuat[3]);
|
|
4732
|
+
deps.requestRender();
|
|
4733
|
+
};
|
|
4734
|
+
const handle = {
|
|
4735
|
+
close() {
|
|
4736
|
+
if (disposed) return;
|
|
4737
|
+
const finish = () => {
|
|
4738
|
+
teardown();
|
|
4739
|
+
opts.onClose?.();
|
|
4740
|
+
};
|
|
4741
|
+
cancelAnimationFrame(raf);
|
|
4742
|
+
fadeTo(0, finish);
|
|
4743
|
+
},
|
|
4744
|
+
dispose: teardown
|
|
4745
|
+
};
|
|
4746
|
+
closeBtn.addEventListener("click", () => handle.close());
|
|
4747
|
+
return handle;
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4280
4750
|
// src/view3d/analytics.ts
|
|
4281
4751
|
var now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
4282
4752
|
var Analytics3D = class {
|
|
@@ -4551,7 +5021,7 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4551
5021
|
zIndex: "4"
|
|
4552
5022
|
});
|
|
4553
5023
|
overviewChip.addEventListener("click", () => {
|
|
4554
|
-
if (disposed || frozen) return;
|
|
5024
|
+
if (disposed || frozen || panorama) return;
|
|
4555
5025
|
cancelFlight();
|
|
4556
5026
|
removeArriveChip();
|
|
4557
5027
|
orbit.frameSoft(model.bounds, stageAzimuth);
|
|
@@ -4579,18 +5049,36 @@ function mountVenue3D(container, input, opts = {}) {
|
|
|
4579
5049
|
}
|
|
4580
5050
|
if (disposed || gen !== flightGen) return;
|
|
4581
5051
|
removeArriveChip();
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
5052
|
+
const onClose = () => {
|
|
5053
|
+
panorama = null;
|
|
5054
|
+
labelOverlay.setVisible(true);
|
|
5055
|
+
frozen = false;
|
|
5056
|
+
analytics.panoramaClosed();
|
|
5057
|
+
orbit.resumeAfterFlight(model.focalWorld);
|
|
5058
|
+
loop.requestRender();
|
|
5059
|
+
showArriveChip(seatId, flightGen);
|
|
5060
|
+
};
|
|
5061
|
+
const sceneMode = view.generated === true;
|
|
5062
|
+
const spherical = gpu && !contextLost ? await mountPanoramaSphere(container, sceneMode ? null : view, {
|
|
5063
|
+
gl: glctx.gl,
|
|
5064
|
+
scene: gpu.main,
|
|
5065
|
+
camera: orbit.camera,
|
|
5066
|
+
requestRender: () => loop.requestRender(),
|
|
5067
|
+
focalWorld: model.focalWorld
|
|
5068
|
+
}, { fadeMs, seatLabel: seatId, onClose }) : null;
|
|
5069
|
+
if (disposed || gen !== flightGen) {
|
|
5070
|
+
spherical?.dispose();
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
if (spherical) {
|
|
5074
|
+
orbit.syncFromCamera();
|
|
5075
|
+
labelOverlay.setVisible(false);
|
|
5076
|
+
frozen = false;
|
|
5077
|
+
loop.requestRender();
|
|
5078
|
+
panorama = spherical;
|
|
5079
|
+
} else {
|
|
5080
|
+
panorama = mountPanorama(container, view, { fadeMs, seatLabel: seatId, onClose });
|
|
5081
|
+
}
|
|
4594
5082
|
analytics.panoramaOpened();
|
|
4595
5083
|
};
|
|
4596
5084
|
const cancelFlight = () => {
|