@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 +6 -0
- package/catalogue.json +1 -1
- package/dist/athletics/plan.d.ts +2 -0
- package/dist/athletics/plan.js +20 -4
- package/dist/catalogue.data.js +1 -1
- package/dist/crowd.d.ts +18 -0
- package/dist/crowd.js +88 -18
- package/dist/draw.js +22 -7
- package/dist/surfaces/artistic.js +3 -1
- package/dist/surfaces/athletics.js +2 -0
- package/dist/surfaces/hockey.js +3 -1
- package/dist/surfaces/surface.d.ts +7 -0
- package/dist/surfaces/surface.js +9 -0
- package/dist/turn.markup.js +12 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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.
|
|
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/athletics/plan.d.ts
CHANGED
|
@@ -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[];
|
package/dist/athletics/plan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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,
|
package/dist/catalogue.data.js
CHANGED
|
@@ -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.
|
|
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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
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
|
|
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
|
-
|
|
383
|
-
const
|
|
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 -
|
|
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 +
|
|
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 -
|
|
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
|
-
|
|
494
|
-
const
|
|
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,
|
|
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,
|
|
500
|
-
if (
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
package/dist/surfaces/hockey.js
CHANGED
|
@@ -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;
|
package/dist/surfaces/surface.js
CHANGED
|
@@ -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") });
|
package/dist/turn.markup.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|