@seatlayer/core 0.49.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.
@@ -325,6 +325,17 @@ interface SceneModel {
325
325
  /** Half-diagonal of the horizontal footprint, metres (camera fit). */
326
326
  radius: number;
327
327
  groundY: number;
328
+ /**
329
+ * Axis-aligned venue extents (horizontal from the chart footprint,
330
+ * vertical from the rendered geometry). The overview camera fits this
331
+ * projected box rather than the radius sphere — a flat wide floor's
332
+ * sphere over-estimates wildly and framed such venues tiny (see
333
+ * OrbitCamera.fitDistance).
334
+ */
335
+ box: {
336
+ min: [number, number, number];
337
+ max: [number, number, number];
338
+ };
328
339
  };
329
340
  /** Exact enclosing sphere for rendered solids plus near-field seat detail. */
330
341
  renderBounds: {
@@ -325,6 +325,17 @@ interface SceneModel {
325
325
  /** Half-diagonal of the horizontal footprint, metres (camera fit). */
326
326
  radius: number;
327
327
  groundY: number;
328
+ /**
329
+ * Axis-aligned venue extents (horizontal from the chart footprint,
330
+ * vertical from the rendered geometry). The overview camera fits this
331
+ * projected box rather than the radius sphere — a flat wide floor's
332
+ * sphere over-estimates wildly and framed such venues tiny (see
333
+ * OrbitCamera.fitDistance).
334
+ */
335
+ box: {
336
+ min: [number, number, number];
337
+ max: [number, number, number];
338
+ };
328
339
  };
329
340
  /** Exact enclosing sphere for rendered solids plus near-field seat detail. */
330
341
  renderBounds: {
@@ -464,9 +464,9 @@ var OrbitCamera = class {
464
464
  const halfV = this.fovY * DEG / 2;
465
465
  const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
466
466
  const halfH = Math.atan(Math.tan(halfV) * aspect);
467
- const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
468
467
  this.azT = stageAzimuth ?? -30 * DEG;
469
468
  this.polT = 55 * DEG;
469
+ const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
470
470
  this.distT = fit * FRAME_MARGIN;
471
471
  this.minDist = Math.max(2, r * 0.12);
472
472
  this.maxDist = fit * 4;
@@ -496,15 +496,59 @@ var OrbitCamera = class {
496
496
  this.panLimit = Math.max(1, bounds.radius) * 1.5;
497
497
  this.syncFromCamera();
498
498
  this.camera.perspective({ fov: this.fovY, aspect: this.camera.aspect });
499
- const r = Math.max(1, bounds.radius);
500
499
  const halfV = this.fovY * DEG / 2;
501
500
  const aspect = portraitCrop ? Math.max(0.82, this.camera.aspect || 1) : this.camera.aspect || 1;
502
501
  const halfH = Math.atan(Math.tan(halfV) * aspect);
503
- const fit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
504
502
  this.azT = stageAzimuth ?? this.azimuth;
505
503
  this.polT = 55 * DEG;
504
+ const fit = this.fitDistance(bounds, halfV, halfH, this.azT, this.polT);
506
505
  this.distT = fit * FRAME_MARGIN;
507
506
  }
507
+ /**
508
+ * Distance from the bounds centre at which the venue fits the frustum at
509
+ * the given pose.
510
+ *
511
+ * Radius-only callers (section / zone / row focus) get the sphere fit —
512
+ * exact enough for chunky targets, and their radii are hand-tuned to it.
513
+ * When the caller supplies the venue's axis-aligned box, fit the box's
514
+ * PROJECTED corners instead: a flat banquet floor's enclosing sphere is
515
+ * nearly all horizontal extent, and sphere-fitting it backed the camera off
516
+ * until the venue used a fraction of the canvas. Each corner demands
517
+ * `lateral/tan(half-angle) − depth` of camera distance; the venue fits when
518
+ * every corner does. Clamped to the sphere fit (which always contains the
519
+ * box) and floored so a degenerate box cannot glue the camera to the floor.
520
+ */
521
+ fitDistance(bounds, halfV, halfH, azimuth, polar) {
522
+ const r = Math.max(1, bounds.radius);
523
+ const sphereFit = Math.max(r / Math.tan(halfV), r / Math.tan(halfH));
524
+ const box = bounds.box;
525
+ if (!box) return sphereFit;
526
+ const sp = Math.sin(polar);
527
+ const fx = -sp * Math.sin(azimuth);
528
+ const fy = -Math.cos(polar);
529
+ const fz = -sp * Math.cos(azimuth);
530
+ if (Math.abs(fy) > 0.999) return sphereFit;
531
+ let rx = -fz, rz = fx;
532
+ const rl = Math.hypot(rx, rz);
533
+ rx /= rl;
534
+ rz /= rl;
535
+ const ux = -rz * fy;
536
+ const uy = rz * fx - rx * fz;
537
+ const uz = rx * fy;
538
+ const tanH = Math.tan(halfH);
539
+ const tanV = Math.tan(halfV);
540
+ let fit = 1;
541
+ for (let i = 0; i < 8; i++) {
542
+ const px = (i & 1 ? box.max[0] : box.min[0]) - bounds.center[0];
543
+ const py = (i & 2 ? box.max[1] : box.min[1]) - bounds.center[1];
544
+ const pz = (i & 4 ? box.max[2] : box.min[2]) - bounds.center[2];
545
+ const depth = px * fx + py * fy + pz * fz;
546
+ const lx = Math.abs(px * rx + pz * rz);
547
+ const ly = Math.abs(px * ux + py * uy + pz * uz);
548
+ fit = Math.max(fit, lx / tanH - depth, ly / tanV - depth);
549
+ }
550
+ return Math.min(sphereFit, Math.max(fit, r * 0.35));
551
+ }
508
552
  /**
509
553
  * Tighten the depth range around the actual venue bounds for the current pose.
510
554
  *
@@ -3523,10 +3567,44 @@ function buildSceneModel(input) {
3523
3567
  renderMaxY - renderMinY,
3524
3568
  renderMaxZ - renderMinZ
3525
3569
  ));
3570
+ let contentMinX = Infinity, contentMinZ = Infinity, contentMaxX = -Infinity, contentMaxZ = -Infinity;
3571
+ for (let i = 0; i < seatData.count; i++) {
3572
+ const o = i * 3;
3573
+ const x = seatData.iPosition[o], z = seatData.iPosition[o + 2];
3574
+ if (x < contentMinX) contentMinX = x;
3575
+ if (x > contentMaxX) contentMaxX = x;
3576
+ if (z < contentMinZ) contentMinZ = z;
3577
+ if (z > contentMaxZ) contentMaxZ = z;
3578
+ }
3579
+ for (const s of stageBounds) {
3580
+ contentMinX = Math.min(contentMinX, s.cx - s.halfX);
3581
+ contentMaxX = Math.max(contentMaxX, s.cx + s.halfX);
3582
+ contentMinZ = Math.min(contentMinZ, s.cz - s.halfZ);
3583
+ contentMaxZ = Math.max(contentMaxZ, s.cz + s.halfZ);
3584
+ }
3585
+ const fpMinX = fp.minX * M, fpMaxX = fp.maxX * M, fpMinZ = fp.minY * M, fpMaxZ = fp.maxY * M;
3586
+ let boxMinX = fpMinX, boxMaxX = fpMaxX, boxMinZ = fpMinZ, boxMaxZ = fpMaxZ;
3587
+ if (Number.isFinite(contentMinX)) {
3588
+ const pad = Math.max(2, 0.06 * Math.hypot(contentMaxX - contentMinX, contentMaxZ - contentMinZ));
3589
+ boxMinX = Math.max(fpMinX, contentMinX - pad);
3590
+ boxMaxX = Math.min(fpMaxX, contentMaxX + pad);
3591
+ boxMinZ = Math.max(fpMinZ, contentMinZ - pad);
3592
+ boxMaxZ = Math.min(fpMaxZ, contentMaxZ + pad);
3593
+ }
3526
3594
  return {
3527
3595
  solids,
3528
3596
  seats: seatData,
3529
- bounds: { center: [cx, radius * 0.08, cz], radius, groundY: 0 },
3597
+ bounds: {
3598
+ // Centred on the CONTENT box, so an off-centre table cluster arrives
3599
+ // centred on screen instead of wherever the floor's midpoint put it.
3600
+ center: [(boxMinX + boxMaxX) / 2, radius * 0.08, (boxMinZ + boxMaxZ) / 2],
3601
+ radius,
3602
+ groundY: 0,
3603
+ box: {
3604
+ min: [boxMinX, Math.min(0, renderMinY), boxMinZ],
3605
+ max: [boxMaxX, Math.max(renderMaxY, radius * 0.05), boxMaxZ]
3606
+ }
3607
+ },
3530
3608
  renderBounds: { center: renderCenter, radius: renderRadius },
3531
3609
  stateColorLUT: themeSeatColorLUT(theme, SEAT_STATES),
3532
3610
  theme,