@johnmorrisdotca/kyogijo-stadiums 0.58.0 → 0.59.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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.59.0] - 2026-11-03
11
+
12
+ ### Fixed
13
+
14
+ - **The stands clear the whole floor, centred (KYOGIJO-STADIUMS-44)** ([the stands and the crowd](./docs/crowd.md#the-venues)): with `crowd`, the bowl's opening was laid out from the playing surface's box alone, so the floor drawn round it ran into the first rows: a court's run-off at the corners (volleyball, sepak takraw, netball), a table tennis area at its ends, a rink's boards, and an athletics track's sprint straight, jumps' runways and pits beyond the oval. The opening is now fitted to everything the surface draws round its playing area (its view with no margin, and an outline where the floor is not a rectangle: the track with its straight and runways, a rink's rounded boards), clear of it all round, corners included, by 2 feet; a floor bigger than the sport's distances (table tennis) gets an opening of its own shape. The bench side's extra room is shared by the two sides, so the opening is centred on the floor and the bowl in the drawing (a soccer ground's touchlines are 23.7 feet from the first row on both sides, where they were 19.7 and 27.7), and the margin holds the roof's shade. The tunnel, the camera platform and the media's desks stay in the seats, the media's desks no longer run round a small opening's corners, and the benches stand between the floor and the first row only where the gap holds them. An artistic swimming area with a crowd keeps the margin the stands need along its sides (it gave them four tenths of it). The flat drawing and the 3D view share the one layout (`crowdStands`), which now gives the `floor` it was fitted to. In 3D, a table tennis table's raised floor (the drawing lifted to the table's height, with the ground the stands stand on) was sorted by its middle with the stands and drawn over the far ones, so from Low corner or Broadcast the bowl looked cut open round a small tile: the bowl is now always drawn over a raised floor, and the faint near stands at a low viewpoint are the walls giving way to the eye, as before. New tests walk every sport with a crowd, at every size and orientation, light and dark: no part of the floor's outline within 2 feet of a seat, nothing of the stands on the floor, the opening centred on the floor and the bowl centred in the drawing and wholly inside it; and in 3D, at every viewpoint, zoom and box, the eye outside the whole scene, nothing near the eye's plane, and no plane of the bowl under the floor. Pictures of a court, a track, a rink, a ring and a table tennis area in their stands are in the docs.
15
+
10
16
  ## [0.58.0] - 2026-11-03
11
17
 
12
18
  ### Added
package/catalogue.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "generatedAt": "2026-11-03",
4
4
  "package": {
5
5
  "name": "@johnmorrisdotca/kyogijo-stadiums",
6
- "version": "0.58.0"
6
+ "version": "0.59.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
@@ -42,6 +42,8 @@ export interface AthleticsPlan {
42
42
  box: MBox;
43
43
  /** Everything drawn: the box and what lies beyond it (the jump strip, the runways' ends, the straight). */
44
44
  extent: MBox;
45
+ /** What `extent` holds as points (the oval and what lies beyond it): what a crowd's stands clear. */
46
+ outline?: [number, number][];
45
47
  areas: Area[];
46
48
  shapes: Shape[];
47
49
  digits: Digits[];
@@ -224,13 +224,28 @@ export function athleticsPlan(o) {
224
224
  const box = { x0: -S / 2 - Rout, y0: -Rout, x1: S / 2 + Rout, y1: Rout };
225
225
  let extent = grow(box, 1);
226
226
  const edgeStraight = R + straight.lanes * straight.lane;
227
+ // What lies beyond the oval: the sprint straight, the jumps' strip, the javelin's runways.
228
+ const beyond = [];
227
229
  if (!indoor)
228
- extent = union([extent, { x0: straight.x0, y0: -Rout, x1: straight.x1, y1: edgeStraight }]);
230
+ beyond.push({ x0: straight.x0, y0: -Rout, x1: straight.x1, y1: edgeStraight });
229
231
  if (events.includes("long-jump") || events.includes("triple-jump"))
230
- extent = union([extent, jumpStrip()]);
231
- if (events.includes("javelin-throw"))
232
- extent = union([extent, ...inf.facilities.filter((f) => f.event === "javelin-throw").map((f) => f.box)].map((b) => ({ x0: Math.max(b.x0, -S / 2 - Rout - 6), y0: b.y0, x1: Math.min(b.x1, S / 2 + Rout + 6), y1: b.y1 })));
232
+ beyond.push(jumpStrip());
233
+ extent = union([extent, ...beyond]);
234
+ if (events.includes("javelin-throw")) {
235
+ const runways = inf.facilities.filter((f) => f.event === "javelin-throw").map((f) => f.box);
236
+ const cut = (b) => ({ x0: Math.max(b.x0, -S / 2 - Rout - 6), y0: b.y0, x1: Math.min(b.x1, S / 2 + Rout + 6), y1: b.y1 });
237
+ extent = union([extent, ...runways].map(cut));
238
+ beyond.push(...runways.map(cut));
239
+ }
233
240
  extent = grow(extent, 0);
241
+ const rim = Rout + 1;
242
+ const outline = [];
243
+ for (let i = 0; i <= 32; i += 1) {
244
+ const a = -Math.PI / 2 + (Math.PI * i) / 32;
245
+ outline.push([S / 2 + rim * Math.cos(a), rim * Math.sin(a)], [-S / 2 - rim * Math.cos(a), rim * Math.sin(a)]);
246
+ }
247
+ for (const b of beyond)
248
+ outline.push([b.x0, b.y0], [b.x1, b.y0], [b.x1, b.y1], [b.x0, b.y1]);
234
249
  const plan = {
235
250
  sport: "athletics",
236
251
  league: league.id,
@@ -243,6 +258,7 @@ export function athleticsPlan(o) {
243
258
  infield,
244
259
  box,
245
260
  extent,
261
+ outline,
246
262
  areas: parts.areas,
247
263
  shapes: parts.shapes,
248
264
  digits: parts.digits,
@@ -3,7 +3,7 @@ export const CATALOGUE_DATA = {
3
3
  "generatedAt": "2026-11-03",
4
4
  "package": {
5
5
  "name": "@johnmorrisdotca/kyogijo-stadiums",
6
- "version": "0.58.0"
6
+ "version": "0.59.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
package/dist/crowd.d.ts CHANGED
@@ -70,6 +70,14 @@ export interface CrowdStands {
70
70
  stadium?: "purpose" | "nfl" | "cfl";
71
71
  /** The playing surface, in the wide frame. */
72
72
  box: Box;
73
+ /**
74
+ * The floor round the playing surface, where the surface draws more than its box (a court's run-off, a track's sprint straight, runways and pits): its bounding box and its outline as points, in the wide frame.
75
+ * The first row of seats clears all of it by `FLOOR_CLEAR`, and the opening is centred on its box. Missing for a real venue's stands and a ballpark's, which are laid out from their own data.
76
+ */
77
+ floor?: {
78
+ box: Box;
79
+ points: Pt[];
80
+ };
73
81
  rings: StandRing[];
74
82
  /** Boxes where the stands are left out (the open end of a horseshoe, the ends of two sides). */
75
83
  covers: [x0: number, y0: number, x1: number, y1: number][];
@@ -310,10 +318,20 @@ export interface LayoutInput {
310
318
  };
311
319
  /** A real venue's stands in place of the sport's usual tiers (see `venueBowl`). */
312
320
  real?: RealBowl;
321
+ /**
322
+ * Everything the surface draws round its playing area, in the wide frame: the bounding box (the surface's own view with no margin) and, where the floor is not that rectangle (an athletics track, an oval
323
+ * ground), its outline as points. The opening of the stands is centred on it and clears it. Without it the floor is the box.
324
+ */
325
+ floor?: {
326
+ box: Box;
327
+ points?: readonly (readonly [number, number])[];
328
+ };
313
329
  /** The ballpark's fence as points on the field, for a ballpark (see `parkLayout`). */
314
330
  fence?: readonly Pt[];
315
331
  /** The foul lines' length at the poles and the backstop's distance, for a ballpark. */
316
332
  backstop?: number;
317
333
  }
334
+ /** The room kept between the floor (everything the surface draws round its playing area) and the first row of seats, in feet. */
335
+ export declare const FLOOR_CLEAR = 2;
318
336
  /** The layout of a sport's stands round its surface: rings, covers, sectors and gaps. For a ballpark see `parkLayout`. */
319
337
  export declare function layoutOf(input: LayoutInput): CrowdStands;
package/dist/crowd.js CHANGED
@@ -330,6 +330,62 @@ function offsets(p, box, stadium) {
330
330
  const side = g.side + (f.width - box.h) / 2;
331
331
  return { left: end, right: end, top: side, bottom: side + g.bench };
332
332
  }
333
+ /** The room kept between the floor (everything the surface draws round its playing area) and the first row of seats, in feet. */
334
+ export const FLOOR_CLEAR = 2;
335
+ /** A point inside a rounded rectangle or an ellipse of half sizes `hx`, `hy` about the origin, with `clear` to spare. */
336
+ function insideOpening(ax, ay, hx, hy, r, oval, clear) {
337
+ const [w, h] = [hx - clear, hy - clear];
338
+ if (w <= 0 || h <= 0)
339
+ return false;
340
+ if (oval)
341
+ return sq(ax / w) + sq(ay / h) <= 1 + 1e-9;
342
+ const rr = Math.max(0, Math.min(r, hx, hy) - clear);
343
+ if (ax > w + 1e-9 || ay > h + 1e-9)
344
+ return false;
345
+ const kx = ax - (w - rr);
346
+ const ky = ay - (h - rr);
347
+ return kx <= 0 || ky <= 0 || sq(kx) + sq(ky) <= sq(rr) + 1e-9;
348
+ }
349
+ /** The first row's opening round a floor, centred on it (the bench room shared by both sides): the sport's distances, or the floor's own shape where they do not clear it, grown alike until its whole outline clears by `FLOOR_CLEAR`. */
350
+ function openingOf(input) {
351
+ const { off, corner, oval, box } = input;
352
+ const floor = input.floor?.box ?? box;
353
+ const cx = floor.x + floor.w / 2;
354
+ const cy = floor.y + floor.h / 2;
355
+ let hx = Math.max(cx - box.x, box.x + box.w - cx) + (off.left + off.right) / 2;
356
+ let hy = Math.max(cy - box.y, box.y + box.h - cy) + (off.top + off.bottom) / 2;
357
+ // A floor the sport's distances do not clear (table tennis): the floor's own shape.
358
+ if (Math.min(hx - floor.w / 2, hy - floor.h / 2) < FLOOR_CLEAR) {
359
+ hx = floor.w / 2 + FLOOR_CLEAR;
360
+ hy = floor.h / 2 + FLOOR_CLEAR;
361
+ }
362
+ const given = input.floor?.points;
363
+ const points = given !== undefined && given.length > 0
364
+ ? given.map(([x, y]) => [x, y])
365
+ : oval
366
+ ? Array.from({ length: 64 }, (_, i) => [cx + (floor.w / 2) * Math.cos((i * Math.PI) / 32), cy + (floor.h / 2) * Math.sin((i * Math.PI) / 32)])
367
+ : [[floor.x, floor.y], [floor.x + floor.w, floor.y], [floor.x + floor.w, floor.y + floor.h], [floor.x, floor.y + floor.h]];
368
+ // A sampled curve: a little to spare.
369
+ const clear = FLOOR_CLEAR + (given !== undefined || oval ? 0.5 : 0);
370
+ const fits = (d) => points.every(([x, y]) => insideOpening(Math.abs(x - cx), Math.abs(y - cy), hx + d, hy + d, corner, oval, clear));
371
+ if (!fits(0)) {
372
+ let lo = 0;
373
+ let hi = 4;
374
+ while (!fits(hi) && hi < 4096)
375
+ hi *= 2;
376
+ for (let i = 0; i < 40; i += 1) {
377
+ const mid = (lo + hi) / 2;
378
+ if (fits(mid))
379
+ hi = mid;
380
+ else
381
+ lo = mid;
382
+ }
383
+ hx += hi;
384
+ hy += hi;
385
+ }
386
+ const round = (v) => Math.round(v * 1e6) / 1e6;
387
+ return { off: { left: round(box.x - (cx - hx)), right: round(cx + hx - box.x - box.w), top: round(box.y - (cy - hy)), bottom: round(cy + hy - box.y - box.h) }, cx, cy, floor, points };
388
+ }
333
389
  /** The layout of a sport's stands round its surface: rings, covers, sectors and gaps. For a ballpark see `parkLayout`. */
334
390
  export function layoutOf(input) {
335
391
  const { sport, box } = input;
@@ -342,15 +398,16 @@ export function layoutOf(input) {
342
398
  const stadium = sport === "soccer" && input.real === undefined ? host ?? "purpose" : undefined;
343
399
  const p = stadium === "nfl" || stadium === "cfl" ? { ...PROFILES[stadium], shape: base.shape, away: base.away } : base;
344
400
  const shape = input.real !== undefined ? "bowl" : crowd.shape ?? p.shape;
345
- const off = input.real?.off ?? offsets(base, box, stadium ?? "purpose");
346
- const cx = box.x + box.w / 2;
347
- const cy = box.y + box.h / 2;
401
+ const fit = input.real === undefined ? openingOf({ off: offsets(base, box, stadium ?? "purpose"), corner: p.corner, oval: base.oval === true, box, floor: input.floor }) : undefined;
402
+ const off = input.real?.off ?? fit?.off ?? offsets(base, box, stadium ?? "purpose");
403
+ const cx = fit?.cx ?? box.x + box.w / 2;
404
+ const cy = fit?.cy ?? box.y + box.h / 2;
348
405
  const rings = input.real === undefined ? [] : input.real.rings.map((ring) => ({ ...ring }));
349
406
  const reachOf = input.real === undefined ? { left: 0, right: 0, top: 0, bottom: 0 } : { ...input.real.reach };
350
407
  for (const t of input.real === undefined ? p.tiers : []) {
351
408
  const at = (k) => {
352
409
  if (base.oval === true)
353
- return { kind: "ellipse", cx, cy, rx: box.w / 2 + (off.left + off.right) / 2 + k, ry: box.h / 2 + (off.top + off.bottom) / 2 + k };
410
+ return { kind: "ellipse", cx, cy, rx: (box.w + off.left + off.right) / 2 + k, ry: (box.h + off.top + off.bottom) / 2 + k };
354
411
  return { kind: "rrect", x0: box.x - off.left - k, y0: box.y - off.top - k, x1: box.x + box.w + off.right + k, y1: box.y + box.h + off.bottom + k, r: p.corner + k };
355
412
  };
356
413
  const region = at(t.to);
@@ -379,17 +436,18 @@ export function layoutOf(input) {
379
436
  const covers = [];
380
437
  const x0 = box.x - off.left;
381
438
  const x1 = box.x + box.w + off.right;
382
- const yTop = last === undefined ? box.y : box.y - reachOf.top - 4;
383
- const yBot = last === undefined ? box.y + box.h : box.y + box.h + reachOf.bottom + 4;
439
+ // The covers reach past the roof's shade (9 feet beyond the upper tier) too.
440
+ const yTop = last === undefined ? box.y : box.y - reachOf.top - 10;
441
+ const yBot = last === undefined ? box.y + box.h : box.y + box.h + reachOf.bottom + 10;
384
442
  if (shape === "horseshoe") {
385
443
  // The open end: everything beyond the surface's end line and the gap that is kept clear.
386
444
  if (crowd.open === "left")
387
- covers.push([box.x - reachOf.left - 4, yTop, x0 + p.corner * 0.5, yBot]);
445
+ covers.push([box.x - reachOf.left - 10, yTop, x0 + p.corner * 0.5, yBot]);
388
446
  else
389
- covers.push([x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 4, yBot]);
447
+ covers.push([x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 10, yBot]);
390
448
  }
391
449
  else if (shape === "sides") {
392
- covers.push([box.x - reachOf.left - 4, yTop, x0 + p.corner * 0.5, yBot], [x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 4, yBot]);
450
+ covers.push([box.x - reachOf.left - 10, yTop, x0 + p.corner * 0.5, yBot], [x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 10, yBot]);
393
451
  }
394
452
  const concourses = [];
395
453
  rings.forEach((ring, i) => {
@@ -398,6 +456,8 @@ export function layoutOf(input) {
398
456
  concourses.push({ region: ring.hole, hole: before.region });
399
457
  });
400
458
  const stands = { sport, shape, box, rings, covers, concourses, sectors: [], gaps: [], reach: reachOf, seats: rings.reduce((n, r) => n + r.seats, 0) };
459
+ if (fit !== undefined)
460
+ stands.floor = { box: fit.floor, points: fit.points };
401
461
  if (input.real !== undefined)
402
462
  stands.real = input.real.shell;
403
463
  if (input.real?.closedSectors !== undefined)
@@ -490,21 +550,31 @@ export function layoutOf(input) {
490
550
  }
491
551
  }
492
552
  if (crowd.gaps && shape !== "grandstand") {
493
- const bottom = box.y + box.h;
494
- const mid = box.x + box.w / 2;
553
+ // Nothing on the floor: the benches stand between the floor and the first row.
554
+ const edge = box.y + box.h + off.bottom;
555
+ const floorEdge = Math.max(box.y + box.h, fit === undefined ? -Infinity : fit.floor.y + fit.floor.h);
556
+ const mid = cx;
495
557
  // A tunnel through the lower stands at the middle of the bench side, the camera platform above them, and the benches in the gap.
496
- stands.gaps.push({ kind: "tunnel", box: [mid - 6, bottom + off.bottom - 1, mid + 6, bottom + off.bottom + lowerRing.to + 1] });
558
+ stands.gaps.push({ kind: "tunnel", box: [mid - 6, edge, mid + 6, edge + lowerRing.to + 1] });
497
559
  const media = rings[rings.length - 1];
498
560
  if (media !== undefined && media.tier === "upper")
499
- stands.gaps.push({ kind: "camera", box: [mid - 22, bottom + off.bottom + media.from + 3, mid + 22, bottom + off.bottom + media.from + 13] });
500
- if (off.bottom - off.top >= 4 || sport === "soccer" || sport === "nfl" || sport === "cfl" || sport === "ifaf" || sport === "rugby" || sport === "gaelic") {
561
+ stands.gaps.push({ kind: "camera", box: [mid - 22, edge + media.from + 3, mid + 22, edge + media.from + 13] });
562
+ if (base.bench >= 4 || sport === "soccer" || sport === "nfl" || sport === "cfl" || sport === "ifaf" || sport === "rugby" || sport === "gaelic") {
501
563
  const benchLen = Math.min(34, box.w * 0.12);
502
- for (const s of [-1, 1]) {
503
- const bx = mid + s * (box.w * 0.1 + benchLen / 2 + 4);
504
- stands.gaps.push({ kind: "bench", box: [bx - benchLen / 2, bottom + 2.5, bx + benchLen / 2, bottom + 2.5 + Math.min(5, off.bottom * 0.3)] });
564
+ const depth = Math.min(5, off.bottom * 0.3);
565
+ if (floorEdge + 2.5 + depth <= edge - 1) {
566
+ for (const s of [-1, 1]) {
567
+ const bx = box.x + box.w / 2 + s * (box.w * 0.1 + benchLen / 2 + 4);
568
+ stands.gaps.push({ kind: "bench", box: [bx - benchLen / 2, floorEdge + 2.5, bx + benchLen / 2, floorEdge + 2.5 + depth] });
569
+ }
505
570
  }
506
571
  }
507
- stands.gaps.push({ kind: "media", box: [mid - 55, bottom + off.bottom + 2, mid + 55, bottom + off.bottom + 6] });
572
+ // The media's desks: along the straight of that side only.
573
+ const hole = lowerRing.hole;
574
+ const straight = hole?.kind === "rrect" ? (hole.x1 - hole.x0) / 2 - Math.min(hole.r, (hole.x1 - hole.x0) / 2, (hole.y1 - hole.y0) / 2) : 55;
575
+ const half = Math.min(55, straight);
576
+ if (half >= 8)
577
+ stands.gaps.push({ kind: "media", box: [mid - half, edge + 2, mid + half, edge + 6] });
508
578
  }
509
579
  }
510
580
  return stands;
package/dist/draw.js CHANGED
@@ -436,7 +436,7 @@ function realStadiumOf(o, sport, real) {
436
436
  return stadium !== undefined && stadium.configs[config] !== undefined && (stadiumKind(stadium) === "park") === (config === "mlb") ? { stadium, config } : undefined;
437
437
  }
438
438
  /** The stands round a surface, laid out from its box (or, for a ballpark, from its fence). */
439
- function standsFor(o, surface, crowd) {
439
+ function standsFor(o, surface, crowd, floor) {
440
440
  const { sport } = chooseLeague(o);
441
441
  const awaySide = resolveHomeSide({ ...o, sport }) === "left" ? "right" : "left";
442
442
  if (sport === "mlb") {
@@ -469,7 +469,19 @@ function standsFor(o, surface, crowd) {
469
469
  // A dojo's crowd sits in its gallery, from the room's balcony data (in feet here); the arenas (the Budokan, the Kokugikan) keep their bowls.
470
470
  const hall = sport === "dojo" ? dojoGallery(o) : undefined;
471
471
  const gallery = hall === undefined ? undefined : { offset: hall.offset / 0.3048, depth: hall.depth / 0.3048, height: [hall.height[0] / 0.3048, hall.height[1] / 0.3048], rows: hall.rows, aisles: hall.aisles };
472
- return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }) });
472
+ return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }), ...(floor === undefined ? {} : { floor }) });
473
+ }
474
+ /** A surface's floor with no margin (its view, and its outline where it has one); none for a close-up. */
475
+ function floorOf(probe) {
476
+ if (probe.whole !== undefined)
477
+ return undefined;
478
+ const view = wideView(probe);
479
+ if (probe.floor === undefined)
480
+ return { box: view };
481
+ const xs = [view.x, view.x + view.w, ...probe.floor.map((p) => p[0])];
482
+ const ys = [view.y, view.y + view.h, ...probe.floor.map((p) => p[1])];
483
+ const [x0, y0] = [Math.min(...xs), Math.min(...ys)];
484
+ return { box: { x: x0, y: y0, w: Math.max(...xs) - x0, h: Math.max(...ys) - y0 }, points: probe.floor };
473
485
  }
474
486
  /** True when the drawing is asked to be small: `detail: "simple"` or a `width` under 480 pixels. */
475
487
  const smallDrawing = (o) => {
@@ -493,17 +505,20 @@ function venueFrame(o, crowd, surface) {
493
505
  /** A surface with the stands and the crowd round it: the ground is made as wide as the stands need, then the stands are laid on it under the surface. */
494
506
  function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
495
507
  let margin = extra.margin;
508
+ // The floor the stands' opening clears and is centred on.
509
+ const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
510
+ const floor = floorOf(probe);
496
511
  if (!(typeof o.margin === "number" && Number.isFinite(o.margin) && o.margin >= 0)) {
497
512
  // How much ground the stands need beyond what the surface has with none, then the surface is made again with it.
498
- const probe = buildWhole({ ...o, margin: 0 }, portrait, id, score, scored, { ...extra, margin: undefined });
499
- const stands = standsFor(o, probe, crowd);
513
+ const stands = standsFor(o, probe, crowd, floor);
500
514
  const view = wideView(probe);
501
515
  const have = { left: stands.box.x - view.x, right: view.x + view.w - stands.box.x - stands.box.w, top: stands.box.y - view.y, bottom: view.y + view.h - stands.box.y - stands.box.h };
502
516
  const need = Math.max(stands.reach.left - have.left, stands.reach.right - have.right, stands.reach.top - have.top, stands.reach.bottom - have.bottom);
503
- margin = Math.max(margin ?? 0, need + 6);
517
+ // (and the roof's shade, 9 feet beyond the upper tier)
518
+ margin = Math.max(margin ?? 0, need + (crowd.roof ? 10 : 6));
504
519
  }
505
520
  const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
506
- const stands = standsFor(o, surface, crowd);
521
+ const stands = standsFor(o, surface, crowd, floor);
507
522
  const seats = stands.seats;
508
523
  const ground = heldGround(o.ground)?.capacity;
509
524
  const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
@@ -1220,5 +1235,5 @@ export function crowdStands(o) {
1220
1235
  if (crowd === undefined)
1221
1236
  return undefined;
1222
1237
  const probe = buildWhole({ ...o, margin: 0 }, false, drawingId(o), false, false, { features: undefined });
1223
- return standsFor(o, probe, crowd);
1238
+ return standsFor(o, probe, crowd, floorOf(probe));
1224
1239
  }
@@ -59,7 +59,9 @@ export function artistic(o, plan, ctx = {}) {
59
59
  }
60
60
  const top = -jy + 0.4;
61
61
  const bottom = sy + start.depth + 0.4 - W;
62
- const view0 = { x: -m, y: -feet(top) - m * 0.4, w: feet(L) + 2 * m, h: feet(W) + feet(top + bottom) + m * 0.8 };
62
+ // The deck is narrower along the sides than at the ends; a margin that is asked for (the ground the stands of a crowd stand on) is the same all round.
63
+ const across = o.margin === undefined ? m * 0.4 : m;
64
+ const view0 = { x: -m, y: -feet(top) - across, w: feet(L) + 2 * m, h: feet(W) + feet(top + bottom) + 2 * across };
63
65
  base = (ctx.bare === true ? "" : deckMarkup(o, ids, plan.deck, view0.x, view0.y, view0.w, view0.h)) + base + deck;
64
66
  const Lf = feet(L);
65
67
  const Wf = feet(W);
@@ -96,6 +96,8 @@ export function athleticsSurface(o, plan, embedded, margin) {
96
96
  onColour: false,
97
97
  };
98
98
  surface.box = { x: 0, y: 0, w: W, h: H };
99
+ if (plan.outline !== undefined && sv === undefined)
100
+ surface.floor = plan.outline.map(([x, y]) => [(x - ox) * K, (y - oy) * K]);
99
101
  surface.lineRef = { width: thick, feet: K };
100
102
  const ranges = {};
101
103
  for (const [name, r] of Object.entries(plan.ranges))
@@ -2,7 +2,7 @@ import { el, num } from "../format.js";
2
2
  import { rulesFor } from "../eras.js";
3
3
  import { NHL } from "../geometry.js";
4
4
  import { NHL_RINK } from "../leagues/hockey.js";
5
- import { atEnds, edges, ft, quarterTurn, roundedRect } from "./surface.js";
5
+ import { atEnds, edges, ft, quarterTurn, roundedOutline, roundedRect } from "./surface.js";
6
6
  /** Where the boards' curve is at a distance x from the end: the y at which it meets a line across the rink. The corner's radius is the NHL's unless given. */
7
7
  export function boardsY(x, radius = NHL.cornerRadius) {
8
8
  const r = radius;
@@ -128,6 +128,8 @@ export function hockey(o, era, rink = NHL_RINK) {
128
128
  };
129
129
  surface.lineRef = { width: thin, feet: 1 };
130
130
  surface.box = { x: 0, y: 0, w: L, h: W };
131
+ // The floor the stands of a crowd clear: the boards round the rounded corners, not the apron's square corners.
132
+ surface.floor = roundedOutline(-bw, -bw, L + 2 * bw, W + 2 * bw, r + bw);
131
133
  surface.centreRadius = rink.circleRadius;
132
134
  surface.anchors = { kind: "hockey", length: L, width: W, blueLine, goalLine: g, cornerRadius: r, net: net_ };
133
135
  // The zones of the ice: from the end boards to a blue line, and between the blue lines.
@@ -153,6 +153,11 @@ export interface Surface {
153
153
  width: number;
154
154
  feet: number;
155
155
  };
156
+ /**
157
+ * For a surface whose floor is not the rectangle of its view (an athletics track, its sprint straight and runways): the outline of everything it draws round its playing area, as points in the wide frame
158
+ * in feet. The stands of a crowd clear it.
159
+ */
160
+ floor?: (readonly [number, number])[];
156
161
  /** For a surface with a crowd: the margin it was drawn with, which holds the stands. */
157
162
  crowdMargin?: number;
158
163
  /** A real stadium of more than one configuration laid out in the venue's own frame: where the middle of its field is in the surface's frame (the surface is shifted to lie there), and the ground, building, stands and crowd under it, in the venue's frame. */
@@ -254,6 +259,8 @@ export declare function quarterTurn(view: {
254
259
  };
255
260
  /** A box from its edges: the left, the top, the right and the bottom. */
256
261
  export declare const edges: (x0: number, y0: number, x1: number, y1: number) => Box;
262
+ /** A rounded rectangle's outline as points (each corner in 12 steps), for a surface's `floor`. */
263
+ export declare function roundedOutline(x: number, y: number, w: number, h: number, r: number): [number, number][];
257
264
  /** A range that has an end: its box at the left end and at the right, made by a function of the end. */
258
265
  export declare const atEnds: (box: (end: "left" | "right") => Box) => {
259
266
  left: Box;
@@ -45,5 +45,14 @@ export function quarterTurn(view) {
45
45
  }
46
46
  /** A box from its edges: the left, the top, the right and the bottom. */
47
47
  export const edges = (x0, y0, x1, y1) => ({ x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) });
48
+ /** A rounded rectangle's outline as points (each corner in 12 steps), for a surface's `floor`. */
49
+ export function roundedOutline(x, y, w, h, r) {
50
+ const rr = Math.max(0, Math.min(r, w / 2, h / 2));
51
+ const out = [];
52
+ for (const [cx, cy, a0] of [[x + w - rr, y + rr, -90], [x + w - rr, y + h - rr, 0], [x + rr, y + h - rr, 90], [x + rr, y + rr, 180]])
53
+ for (let i = 0; i <= 12; i += 1)
54
+ out.push([cx + rr * Math.cos(((a0 + i * 7.5) * Math.PI) / 180), cy + rr * Math.sin(((a0 + i * 7.5) * Math.PI) / 180)]);
55
+ return out;
56
+ }
48
57
  /** A range that has an end: its box at the left end and at the right, made by a function of the end. */
49
58
  export const atEnds = (box) => ({ left: box("left"), right: box("right") });
@@ -56,6 +56,8 @@ export function groupRanks(groups, yaw) {
56
56
  return rank;
57
57
  }
58
58
  const RAD = Math.PI / 180;
59
+ /** The things of a venue's bowl: they stand on the ground round the surface, never under a raised floor. */
60
+ const BOWL = new Set(["stands", "wall-seats", "gallery", "roof", "ribbon-boards", "hung-board"]);
59
61
  const prepared = new WeakMap();
60
62
  function prepare(model) {
61
63
  const known = prepared.get(model.planes);
@@ -79,7 +81,8 @@ function prepare(model) {
79
81
  const zs = [p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]];
80
82
  mid.push([(p.o[0] + (p.u[0] + p.v[0]) / 2 - centre[0]) * s, (p.o[1] + (p.u[1] + p.v[1]) / 2 - centre[1]) * s, (p.o[2] + (p.u[2] + p.v[2]) / 2) * s, Math.min(...zs) * s, Math.max(...zs) * s]);
81
83
  }
82
- const made = { planes: model.planes, scale: s, centre, base, mid };
84
+ const ofBowl = new Set(model.uprights.items.filter((item) => BOWL.has(item.kind)).flatMap((item) => item.planes));
85
+ const made = { planes: model.planes, scale: s, centre, base, mid, bowl: model.planes.map((p) => ofBowl.has(p)) };
83
86
  prepared.set(model.planes, made);
84
87
  return made;
85
88
  }
@@ -159,6 +162,14 @@ export function layoutOf(model, view, width, height, lens, cull = false, raster
159
162
  order.length = 0;
160
163
  for (let i = 0, j = 0; i < own.length || j < apart.length;)
161
164
  order.push(j >= apart.length || (i < own.length && own[i].depth <= apart[j].depth) ? own[i++] : apart[j++]);
165
+ // A raised floor (a table's top) carries the ground the bowl stands on, so it is sorted by its middle among the surface's own things only: the parts of the bowl that are farther than its middle
166
+ // come after it, still in their own order, or the floor would be drawn over the far stands.
167
+ if (raised) {
168
+ const at = order.findIndex((one) => one.i === 0);
169
+ const far = order.slice(0, at).filter((one) => known.bowl[one.i - 1]);
170
+ if (far.length > 0)
171
+ order.splice(0, at + 1, ...order.slice(0, at).filter((one) => !known.bowl[one.i - 1]), order[at], ...far);
172
+ }
162
173
  const stack = new Array(count + 1).fill(0);
163
174
  stack[0] = 1;
164
175
  order.forEach((one, rank) => {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export declare const VERSION = "0.58.0";
2
+ export declare const VERSION = "0.59.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.58.0";
2
+ export const VERSION = "0.59.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",