@seatlayer/core 0.50.0 → 0.50.1

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.
@@ -476,9 +476,9 @@ var OrbitCamera = class {
476
476
  const halfV = this.fovY * DEG / 2;
477
477
  const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
478
478
  const halfH = Math.atan(Math.tan(halfV) * aspect);
479
- const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
480
479
  this.azT = stageAzimuth ?? -30 * DEG;
481
480
  this.polT = 55 * DEG;
481
+ const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
482
482
  this.distT = fit * FRAME_MARGIN;
483
483
  this.minDist = Math.max(2, r * 0.12);
484
484
  this.maxDist = fit * 4;
@@ -508,15 +508,59 @@ var OrbitCamera = class {
508
508
  this.panLimit = Math.max(1, bounds.radius) * 1.5;
509
509
  this.syncFromCamera();
510
510
  this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
511
- const r = Math.max(1, bounds.radius);
512
511
  const halfV = this.fovY * DEG / 2;
513
512
  const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
514
513
  const halfH = Math.atan(Math.tan(halfV) * aspect);
515
- const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
516
514
  this.azT = stageAzimuth ?? this.azimuth;
517
515
  this.polT = 55 * DEG;
516
+ const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
518
517
  this.distT = fit * FRAME_MARGIN;
519
518
  }
519
+ /**
520
+ * Distance from the bounds centre at which the venue fits the frustum at
521
+ * the given pose.
522
+ *
523
+ * Radius-only callers (section / zone / row focus) get the sphere fit —
524
+ * exact enough for chunky targets, and their radii are hand-tuned to it.
525
+ * When the caller supplies the venue's axis-aligned box, fit the box's
526
+ * PROJECTED corners instead: a flat banquet floor's enclosing sphere is
527
+ * nearly all horizontal extent, and sphere-fitting it backed the camera off
528
+ * until the venue used a fraction of the canvas. Each corner demands
529
+ * `lateral/tan(half-angle) − depth` of camera distance; the venue fits when
530
+ * every corner does. Clamped to the sphere fit (which always contains the
531
+ * box) and floored so a degenerate box cannot glue the camera to the floor.
532
+ */
533
+ fitDistance(bounds, halfV, halfH, azimuth, polar) {
534
+ const r = Math.max(1, bounds.radius);
535
+ const sphereFit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
536
+ const box = bounds.box;
537
+ if (!box) return sphereFit;
538
+ const sp = Math.sin(polar);
539
+ const fx = -sp * Math.sin(azimuth);
540
+ const fy = -Math.cos(polar);
541
+ const fz = -sp * Math.cos(azimuth);
542
+ if (Math.abs(fy) > 0.999) return sphereFit;
543
+ let rx = -fz, rz = fx;
544
+ const rl = Math.hypot(rx, rz);
545
+ rx /= rl;
546
+ rz /= rl;
547
+ const ux = -rz * fy;
548
+ const uy = rz * fx - rx * fz;
549
+ const uz = rx * fy;
550
+ const tanH = Math.tan(halfH);
551
+ const tanV = Math.tan(halfV);
552
+ let fit = 1;
553
+ for (let i = 0; i < 8; i++) {
554
+ const px = (i & 1 ? box.max[0] : box.min[0]) - bounds.center[0];
555
+ const py = (i & 2 ? box.max[1] : box.min[1]) - bounds.center[1];
556
+ const pz = (i & 4 ? box.max[2] : box.min[2]) - bounds.center[2];
557
+ const depth = px * fx + py * fy + pz * fz;
558
+ const lx = Math.abs(px * rx + pz * rz);
559
+ const ly = Math.abs(px * ux + py * uy + pz * uz);
560
+ fit = Math.max(fit, lx / tanH - depth, ly / tanV - depth);
561
+ }
562
+ return Math.min(sphereFit, Math.max(fit, r * 0.35));
563
+ }
520
564
  /**
521
565
  * Tighten the depth range around the actual venue bounds for the current pose.
522
566
  *
@@ -3987,10 +4031,44 @@ function buildSceneModel(input) {
3987
4031
  renderMaxY - renderMinY,
3988
4032
  renderMaxZ - renderMinZ
3989
4033
  ));
4034
+ let contentMinX = Infinity, contentMinZ = Infinity, contentMaxX = -Infinity, contentMaxZ = -Infinity;
4035
+ for (let i = 0; i < seatData.count; i++) {
4036
+ const o = i * 3;
4037
+ const x = seatData.iPosition[o], z = seatData.iPosition[o + 2];
4038
+ if (x < contentMinX) contentMinX = x;
4039
+ if (x > contentMaxX) contentMaxX = x;
4040
+ if (z < contentMinZ) contentMinZ = z;
4041
+ if (z > contentMaxZ) contentMaxZ = z;
4042
+ }
4043
+ for (const s of stageBounds) {
4044
+ contentMinX = Math.min(contentMinX, s.cx - s.halfX);
4045
+ contentMaxX = Math.max(contentMaxX, s.cx + s.halfX);
4046
+ contentMinZ = Math.min(contentMinZ, s.cz - s.halfZ);
4047
+ contentMaxZ = Math.max(contentMaxZ, s.cz + s.halfZ);
4048
+ }
4049
+ const fpMinX = fp.minX * M, fpMaxX = fp.maxX * M, fpMinZ = fp.minY * M, fpMaxZ = fp.maxY * M;
4050
+ let boxMinX = fpMinX, boxMaxX = fpMaxX, boxMinZ = fpMinZ, boxMaxZ = fpMaxZ;
4051
+ if (Number.isFinite(contentMinX)) {
4052
+ const pad = Math.max(2, 0.06 * Math.hypot(contentMaxX - contentMinX, contentMaxZ - contentMinZ));
4053
+ boxMinX = Math.max(fpMinX, contentMinX - pad);
4054
+ boxMaxX = Math.min(fpMaxX, contentMaxX + pad);
4055
+ boxMinZ = Math.max(fpMinZ, contentMinZ - pad);
4056
+ boxMaxZ = Math.min(fpMaxZ, contentMaxZ + pad);
4057
+ }
3990
4058
  return {
3991
4059
  solids,
3992
4060
  seats: seatData,
3993
- bounds: { center: [cx, radius * 0.08, cz], radius, groundY: 0 },
4061
+ bounds: {
4062
+ // Centred on the CONTENT box, so an off-centre table cluster arrives
4063
+ // centred on screen instead of wherever the floor's midpoint put it.
4064
+ center: [(boxMinX + boxMaxX) / 2, radius * 0.08, (boxMinZ + boxMaxZ) / 2],
4065
+ radius,
4066
+ groundY: 0,
4067
+ box: {
4068
+ min: [boxMinX, Math.min(0, renderMinY), boxMinZ],
4069
+ max: [boxMaxX, Math.max(renderMaxY, radius * 0.05), boxMaxZ]
4070
+ }
4071
+ },
3994
4072
  renderBounds: { center: renderCenter, radius: renderRadius },
3995
4073
  stateColorLUT: themeSeatColorLUT(theme, SEAT_STATES),
3996
4074
  theme,