@seatlayer/core 0.30.1 → 0.32.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/index.cjs CHANGED
@@ -218,6 +218,7 @@ __export(index_exports, {
218
218
  MAX_GA_CAPACITY: () => MAX_GA_CAPACITY,
219
219
  PickerController: () => PickerController,
220
220
  RENDERED_QUALITY_REPORT_VERSION: () => RENDERED_QUALITY_REPORT_VERSION,
221
+ SEAT_COMMERCIAL_MARKS: () => SEAT_COMMERCIAL_MARKS,
221
222
  SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
222
223
  SURROUNDINGS_SHAPE_ROLES: () => SURROUNDINGS_SHAPE_ROLES,
223
224
  SeatmapRenderer: () => SeatmapRenderer,
@@ -263,6 +264,7 @@ __export(index_exports, {
263
264
  resolveLocale: () => resolveLocale,
264
265
  rowInventoryCount: () => rowInventoryCount,
265
266
  rowSeatPositions: () => rowSeatPositions,
267
+ seatCommercialMeta: () => seatCommercialMeta,
266
268
  seatLabelPart: () => seatLabelPart,
267
269
  sectionGeometry: () => sectionGeometry,
268
270
  setLocale: () => setLocale,
@@ -306,6 +308,15 @@ function accessibilityRingColor(types) {
306
308
  const primary = types?.[0];
307
309
  return primary && ACCESSIBILITY_RING_COLOR[primary] || "#3b82f6";
308
310
  }
311
+ var SEAT_COMMERCIAL_MARKS = [
312
+ { key: "obstructedView", label: "Obstructed view", short: "Obstructed", icon: "\u26D4" },
313
+ { key: "restrictedView", label: "Restricted view", short: "Restricted", icon: "\u{1F441}" },
314
+ { key: "premium", label: "Premium seat", short: "Premium", icon: "\u2605" }
315
+ ];
316
+ var SEAT_COMMERCIAL_LABEL = new Map(SEAT_COMMERCIAL_MARKS.map((mark) => [mark.key, mark]));
317
+ function seatCommercialMeta(key) {
318
+ return SEAT_COMMERCIAL_LABEL.get(key);
319
+ }
309
320
  var LABEL_STYLE_MIN_SIZE = 8;
310
321
  var LABEL_STYLE_MAX_SIZE = 24;
311
322
  var SURROUNDINGS_SHAPE_ROLES = [
@@ -387,6 +398,147 @@ function distributeAlongCubic(path, count, resolution = 192) {
387
398
  return output;
388
399
  }
389
400
 
401
+ // src/core/polygonAnchor.ts
402
+ var MAX_CELLS = 4e3;
403
+ function distanceToSegment(p, a, b) {
404
+ const dx = b.x - a.x;
405
+ const dy = b.y - a.y;
406
+ const denominator = dx * dx + dy * dy;
407
+ const t2 = denominator ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / denominator)) : 0;
408
+ return Math.hypot(p.x - (a.x + t2 * dx), p.y - (a.y + t2 * dy));
409
+ }
410
+ function distanceToRings(p, rings) {
411
+ let best = Infinity;
412
+ for (const ring of rings) {
413
+ if (ring.length < 2) continue;
414
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
415
+ const d = distanceToSegment(p, ring[j], ring[i]);
416
+ if (d < best) best = d;
417
+ }
418
+ }
419
+ return best;
420
+ }
421
+ function inRing(p, ring) {
422
+ let inside = false;
423
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
424
+ const xi = ring[i].x;
425
+ const yi = ring[i].y;
426
+ const xj = ring[j].x;
427
+ const yj = ring[j].y;
428
+ const hit = yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi;
429
+ if (hit) inside = !inside;
430
+ }
431
+ return inside;
432
+ }
433
+ function signedDistance(p, outer, holes) {
434
+ const inside = inRing(p, outer) && !holes.some((hole) => inRing(p, hole));
435
+ const distance = distanceToRings(p, [outer, ...holes]);
436
+ return inside ? distance : -distance;
437
+ }
438
+ function cellAt(x, y, h, outer, holes) {
439
+ const d = signedDistance({ x, y }, outer, holes);
440
+ return { x, y, h, d, max: d + h * Math.SQRT2 };
441
+ }
442
+ var CellQueue = class {
443
+ constructor() {
444
+ this.items = [];
445
+ }
446
+ get size() {
447
+ return this.items.length;
448
+ }
449
+ push(cell) {
450
+ const items = this.items;
451
+ items.push(cell);
452
+ let index = items.length - 1;
453
+ while (index > 0) {
454
+ const parent = index - 1 >> 1;
455
+ if (items[parent].max >= items[index].max) break;
456
+ [items[parent], items[index]] = [items[index], items[parent]];
457
+ index = parent;
458
+ }
459
+ }
460
+ pop() {
461
+ const items = this.items;
462
+ const top = items[0];
463
+ const last = items.pop();
464
+ if (items.length && last) {
465
+ items[0] = last;
466
+ let index = 0;
467
+ for (; ; ) {
468
+ const left = index * 2 + 1;
469
+ const right = left + 1;
470
+ let best = index;
471
+ if (left < items.length && items[left].max > items[best].max) best = left;
472
+ if (right < items.length && items[right].max > items[best].max) best = right;
473
+ if (best === index) break;
474
+ [items[best], items[index]] = [items[index], items[best]];
475
+ index = best;
476
+ }
477
+ }
478
+ return top;
479
+ }
480
+ };
481
+ function polygonArea(ring) {
482
+ if (ring.length < 3) return 0;
483
+ let sum = 0;
484
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
485
+ sum += (ring[j].x + ring[i].x) * (ring[j].y - ring[i].y);
486
+ }
487
+ return Math.abs(sum) / 2;
488
+ }
489
+ function polygonNetArea(outer, holes) {
490
+ return Math.max(0, polygonArea(outer) - (holes ?? []).reduce((total, hole) => total + polygonArea(hole), 0));
491
+ }
492
+ function polygonInscribedAnchor(outer, holes) {
493
+ const rings = holes ?? [];
494
+ if (!outer.length) return { point: { x: 0, y: 0 }, radius: 0 };
495
+ if (outer.length < 3) {
496
+ const x = outer.reduce((total, p) => total + p.x, 0) / outer.length;
497
+ const y = outer.reduce((total, p) => total + p.y, 0) / outer.length;
498
+ return { point: { x, y }, radius: 0 };
499
+ }
500
+ let minX = Infinity;
501
+ let minY = Infinity;
502
+ let maxX = -Infinity;
503
+ let maxY = -Infinity;
504
+ for (const p of outer) {
505
+ if (p.x < minX) minX = p.x;
506
+ if (p.y < minY) minY = p.y;
507
+ if (p.x > maxX) maxX = p.x;
508
+ if (p.y > maxY) maxY = p.y;
509
+ }
510
+ const width = maxX - minX;
511
+ const height = maxY - minY;
512
+ const cellSize = Math.min(width, height);
513
+ if (!(cellSize > 0)) {
514
+ return { point: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }, radius: 0 };
515
+ }
516
+ const precision = Math.max(width, height) / 150;
517
+ const queue = new CellQueue();
518
+ let h = cellSize / 2;
519
+ for (let x = minX; x < maxX; x += cellSize) {
520
+ for (let y = minY; y < maxY; y += cellSize) {
521
+ queue.push(cellAt(x + h, y + h, h, outer, rings));
522
+ }
523
+ }
524
+ let best = cellAt(minX + width / 2, minY + height / 2, 0, outer, rings);
525
+ let explored = 0;
526
+ while (queue.size) {
527
+ const cell = queue.pop();
528
+ if (!cell) break;
529
+ if (cell.d > best.d) best = cell;
530
+ if (cell.max - best.d <= precision) continue;
531
+ if (explored >= MAX_CELLS) continue;
532
+ explored += 1;
533
+ h = cell.h / 2;
534
+ queue.push(cellAt(cell.x - h, cell.y - h, h, outer, rings));
535
+ queue.push(cellAt(cell.x + h, cell.y - h, h, outer, rings));
536
+ queue.push(cellAt(cell.x - h, cell.y + h, h, outer, rings));
537
+ queue.push(cellAt(cell.x + h, cell.y + h, h, outer, rings));
538
+ }
539
+ return { point: { x: best.x, y: best.y }, radius: Math.max(0, best.d) };
540
+ }
541
+
390
542
  // src/core/sectionPath.ts
391
543
  var TAU = Math.PI * 2;
392
544
  function translateSectionOutlinePath(path, dx, dy) {
@@ -1139,38 +1291,7 @@ function pointInPolygonWithHoles(p, outer, holes) {
1139
1291
  return pointInPolygon(p, outer) && !(holes ?? []).some((hole) => pointInPolygon(p, hole) || pointOnPolygonBoundary(p, hole));
1140
1292
  }
1141
1293
  function polygonLabelPoint(outer, holes) {
1142
- if (!outer.length) return { x: 0, y: 0 };
1143
- const xs = outer.map((point) => point.x);
1144
- const ys = outer.map((point) => point.y);
1145
- const bounds2 = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
1146
- const centroid2 = polygonCentroid(outer);
1147
- if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
1148
- let best = outer[0];
1149
- let bestScore = -Infinity;
1150
- const rings = [outer, ...holes ?? []];
1151
- for (let row = 1; row < 24; row += 1) {
1152
- for (let column = 1; column < 24; column += 1) {
1153
- const point = {
1154
- x: bounds2.minX + (bounds2.maxX - bounds2.minX) * column / 24,
1155
- y: bounds2.minY + (bounds2.maxY - bounds2.minY) * row / 24
1156
- };
1157
- if (!pointInPolygonWithHoles(point, outer, holes)) continue;
1158
- const score = Math.min(...rings.flatMap((ring) => ring.map((start, index) => {
1159
- const end = ring[(index + 1) % ring.length];
1160
- const dx = end.x - start.x;
1161
- const dy = end.y - start.y;
1162
- const denominator = dx * dx + dy * dy;
1163
- const projection = denominator ? ((point.x - start.x) * dx + (point.y - start.y) * dy) / denominator : 0;
1164
- const t2 = Math.max(0, Math.min(1, projection));
1165
- return Math.hypot(point.x - (start.x + t2 * dx), point.y - (start.y + t2 * dy));
1166
- })));
1167
- if (score > bestScore) {
1168
- best = point;
1169
- bestScore = score;
1170
- }
1171
- }
1172
- }
1173
- return best;
1294
+ return polygonInscribedAnchor(outer, holes).point;
1174
1295
  }
1175
1296
  function polygonCentroid(pts) {
1176
1297
  if (!pts.length) return { x: 0, y: 0 };
@@ -2840,7 +2961,7 @@ var ISO_SQUASH = 0.58;
2840
2961
  var ISO_TWEEN_MS = 320;
2841
2962
  var CAMERA_GLIDE_MS = 650;
2842
2963
  var BLOCK_FILL_ALPHA = 1;
2843
- var SECTION_STROKE_PX = 2;
2964
+ var SECTION_STROKE_PX = 1;
2844
2965
  var LIGHT_OVERVIEW_SECTION_FILL = "#e5e7eb";
2845
2966
  var LIGHT_OVERVIEW_SECTION_STROKE = "#c7cbd1";
2846
2967
  var LIGHT_OVERVIEW_SECTION_INK = "#595f69";
@@ -2853,6 +2974,7 @@ var DARK_OVERVIEW_FOCAL_FILL = "#374151";
2853
2974
  var DARK_OVERVIEW_FOCAL_STROKE = "#64748b";
2854
2975
  var SECTION_LABEL_PX = 20;
2855
2976
  var MIN_SECTION_LABEL_PX = 12;
2977
+ var SECTION_LABEL_SHRINK_STEPS = [0.85, 0.72, 0.62];
2856
2978
  var ZONE_LABEL_PX = 18;
2857
2979
  var ZONE_SUB_PX = 12;
2858
2980
  var HIERARCHY_PILL_BACKGROUND = "#111827";
@@ -3027,7 +3149,6 @@ function rotatedRectFitsPolygon(center, width, height, rotation, outer, holes) {
3027
3149
  }
3028
3150
  function polygonLabelCandidates(outer, holes, preferred) {
3029
3151
  const bounds2 = polyBounds(outer);
3030
- const centre = { x: bounds2.x + bounds2.width / 2, y: bounds2.y + bounds2.height / 2 };
3031
3152
  const points = [preferred];
3032
3153
  for (let row = 1; row < 12; row += 1) {
3033
3154
  for (let column = 1; column < 12; column += 1) {
@@ -3038,7 +3159,8 @@ function polygonLabelCandidates(outer, holes, preferred) {
3038
3159
  if (pointInPolygonWithHoles(point, outer, holes)) points.push(point);
3039
3160
  }
3040
3161
  }
3041
- return points.sort((left, right) => Math.hypot(left.x - centre.x, left.y - centre.y) - Math.hypot(right.x - centre.x, right.y - centre.y)).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
3162
+ const rings = [outer, ...holes];
3163
+ return points.map((point) => ({ point, room: distanceToRings(point, rings) })).sort((left, right) => right.room - left.room).map((entry) => entry.point).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
3042
3164
  }
3043
3165
  function roundedPolygonSubpath(context, points, radius) {
3044
3166
  const n = points.length;
@@ -5880,7 +6002,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5880
6002
  const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
5881
6003
  const outlinePoly = polygonWithHolesShape(obj.outline, obj.holes, {
5882
6004
  stroke: rgba(outlineTint, 0.5),
5883
- strokeWidth: 1.75,
6005
+ strokeWidth: 1,
5884
6006
  fill: rgba(outlineTint, 0.08),
5885
6007
  listening: false
5886
6008
  }, obj.outlinePath);
@@ -5935,6 +6057,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5935
6057
  holes: obj.holes ?? [],
5936
6058
  centroid: centroid2,
5937
6059
  labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid2),
6060
+ labelArea: polygonNetArea(obj.outline, obj.holes),
5938
6061
  zone: obj.zone,
5939
6062
  memberIds,
5940
6063
  total: memberIds.length,
@@ -6186,8 +6309,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6186
6309
  }
6187
6310
  }
6188
6311
  /**
6189
- * Keep transitional zone pills from covering section names. Section names
6190
- * are already proven inside disjoint shells, so they must not cull each other.
6312
+ * Keep transitional zone pills from covering section names, and keep section
6313
+ * names off each other.
6314
+ *
6315
+ * This used to assume section labels "do not collide by construction",
6316
+ * because each is proven to fit inside its own shell. That holds only while
6317
+ * shells are disjoint. Hand-traced and imported plans routinely produce
6318
+ * shells that touch or overlap slightly along a shared edge, and a label
6319
+ * fitted into one can then land under its neighbour's — which is exactly the
6320
+ * unreadable smear a 36-section arena showed. Section labels now block each
6321
+ * other too, largest shell first so the big stands keep their names.
6191
6322
  */
6192
6323
  decollideRungLabels(sx) {
6193
6324
  const GAP = 4;
@@ -6213,20 +6344,39 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6213
6344
  }
6214
6345
  }
6215
6346
  for (const sec of this.sections) {
6216
- if (sec.nameLabel.opacity() > 0.05) cands.push({ node: sec.nameLabel, tier: 1, section: true, box: boxOf(sec.nameLabel) });
6347
+ if (sec.nameLabel.opacity() > 0.05) {
6348
+ cands.push({ node: sec.nameLabel, tier: 1, section: true, area: sec.labelArea, box: boxOf(sec.nameLabel) });
6349
+ }
6217
6350
  }
6218
6351
  if (cands.length < 2) return;
6219
- cands.sort((a, b) => a.tier - b.tier || a.box.y - b.box.y || a.box.x - b.box.x);
6352
+ cands.sort((a, b) => a.tier - b.tier || (b.area ?? 0) - (a.area ?? 0) || a.box.y - b.box.y || a.box.x - b.box.x);
6220
6353
  const kept = [];
6221
6354
  const collides = (b) => kept.some(
6222
6355
  (k) => b.x < k.x + k.w + GAP && k.x < b.x + b.w + GAP && b.y < k.y + k.h + GAP && k.y < b.y + b.h + GAP
6223
6356
  );
6224
6357
  for (const c of cands) {
6225
- if (collides(c.box)) {
6226
- c.node.opacity(0);
6227
- } else if (!c.section) {
6358
+ if (!collides(c.box)) {
6228
6359
  kept.push(c.box);
6360
+ continue;
6361
+ }
6362
+ let placed = false;
6363
+ if (c.section) {
6364
+ const fontSize = c.node.fontSize();
6365
+ const y = c.node.y();
6366
+ for (const factor of SECTION_LABEL_SHRINK_STEPS) {
6367
+ const candidate = fontSize * factor;
6368
+ if (candidate * sx < MIN_SECTION_LABEL_PX) break;
6369
+ this.sizeLabel(c.node, candidate, y);
6370
+ const box = boxOf(c.node);
6371
+ if (!collides(box)) {
6372
+ kept.push(box);
6373
+ placed = true;
6374
+ break;
6375
+ }
6376
+ }
6377
+ if (!placed) this.sizeLabel(c.node, fontSize, y);
6229
6378
  }
6379
+ if (!placed) c.node.opacity(0);
6230
6380
  }
6231
6381
  }
6232
6382
  /** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
@@ -6847,7 +6997,20 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6847
6997
  visible,
6848
6998
  renderedFontPx,
6849
6999
  fill: typeof fill === "string" ? fill : "",
6850
- ink: typeof ink === "string" ? ink : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
7000
+ // When no label node exists (the seat is unlabelled at this zoom), fall
7001
+ // back to the ink this seat WOULD be painted with — the same expression
7002
+ // the seat-level evidence below uses — not to the raw theme default.
7003
+ //
7004
+ // The old fallback reported `DEF_SEAT_LABEL` (#0b1220) regardless of the
7005
+ // seat's paint, so a held seat with no label read as dark ink on
7006
+ // HELD_FILL (#6b7280) = 3.87:1 and a booked one as 1.82:1 against
7007
+ // TAKEN_FILL. That is a contrast violation the renderer never commits:
7008
+ // `seatLabelInk` runs every real label through `stateAwareBookableLabelInk`,
7009
+ // which returns #ffffff for both of those fills. A catalog visual audit
7010
+ // read this fallback as though it were painted and reported a
7011
+ // bookable-state contrast failure on 46 of 50 templates that does not
7012
+ // exist on screen.
7013
+ ink: typeof ink === "string" ? ink : shape ? this.seatLabelInk(seat, shape) : this.seatPreferredLabelInk(seat),
6851
7014
  opacity: rounded(opacity),
6852
7015
  ...accessGlyph ? {
6853
7016
  accessibilityMarker: {
@@ -10310,6 +10473,7 @@ async function loadLocale(code) {
10310
10473
  MAX_GA_CAPACITY,
10311
10474
  PickerController,
10312
10475
  RENDERED_QUALITY_REPORT_VERSION,
10476
+ SEAT_COMMERCIAL_MARKS,
10313
10477
  SUPPORTED_LOCALES,
10314
10478
  SURROUNDINGS_SHAPE_ROLES,
10315
10479
  SeatmapRenderer,
@@ -10355,6 +10519,7 @@ async function loadLocale(code) {
10355
10519
  resolveLocale,
10356
10520
  rowInventoryCount,
10357
10521
  rowSeatPositions,
10522
+ seatCommercialMeta,
10358
10523
  seatLabelPart,
10359
10524
  sectionGeometry,
10360
10525
  setLocale,