@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.6.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 +133 -0
- package/NOTICE.md +182 -2
- package/README.md +106 -19
- package/dist/annotate.js +1 -1
- package/dist/arenas-entry.d.ts +4 -0
- package/dist/arenas-entry.js +3 -0
- package/dist/arenas.d.ts +125 -0
- package/dist/arenas.data.d.ts +2 -0
- package/dist/arenas.data.js +3763 -0
- package/dist/arenas.js +72 -0
- package/dist/athletics/describe.d.ts +8 -0
- package/dist/athletics/describe.js +41 -0
- package/dist/athletics/events.d.ts +17 -0
- package/dist/athletics/events.js +48 -0
- package/dist/athletics/facilities.d.ts +80 -0
- package/dist/athletics/facilities.js +176 -0
- package/dist/athletics/infield.d.ts +79 -0
- package/dist/athletics/infield.js +166 -0
- package/dist/athletics/layout.d.ts +13 -0
- package/dist/athletics/layout.js +98 -0
- package/dist/athletics/oval.d.ts +57 -0
- package/dist/athletics/oval.js +116 -0
- package/dist/athletics/plan.d.ts +97 -0
- package/dist/athletics/plan.js +355 -0
- package/dist/athletics/races.d.ts +102 -0
- package/dist/athletics/races.js +355 -0
- package/dist/athletics/steeplechase.d.ts +72 -0
- package/dist/athletics/steeplechase.js +196 -0
- package/dist/athletics/stencil.d.ts +13 -0
- package/dist/athletics/stencil.js +85 -0
- package/dist/athletics/track.d.ts +54 -0
- package/dist/athletics/track.js +105 -0
- package/dist/athletics/types.d.ts +77 -0
- package/dist/athletics/types.js +1 -0
- package/dist/athletics-entry.d.ts +17 -0
- package/dist/athletics-entry.js +11 -0
- package/dist/athletics.style.d.ts +16 -0
- package/dist/athletics.style.js +51 -0
- package/dist/atlas-entry.d.ts +6 -2
- package/dist/atlas-entry.js +4 -2
- package/dist/atlas.d.ts +117 -23
- package/dist/atlas.js +197 -12
- package/dist/background.js +2 -1
- package/dist/budo/describe.d.ts +13 -0
- package/dist/budo/describe.js +120 -0
- package/dist/budo/dojo.api.d.ts +64 -0
- package/dist/budo/dojo.api.js +46 -0
- package/dist/budo/dojo.d.ts +72 -0
- package/dist/budo/dojo.js +300 -0
- package/dist/budo/judo.d.ts +61 -0
- package/dist/budo/judo.js +109 -0
- package/dist/budo/karate.d.ts +29 -0
- package/dist/budo/karate.js +80 -0
- package/dist/budo/kendo.d.ts +18 -0
- package/dist/budo/kendo.js +44 -0
- package/dist/budo/kimarite.d.ts +19 -0
- package/dist/budo/kimarite.js +33 -0
- package/dist/budo/kit.d.ts +16 -0
- package/dist/budo/kit.js +24 -0
- package/dist/budo/kyudo.d.ts +28 -0
- package/dist/budo/kyudo.js +66 -0
- package/dist/budo/looks.d.ts +28 -0
- package/dist/budo/looks.js +58 -0
- package/dist/budo/parts.d.ts +32 -0
- package/dist/budo/parts.js +10 -0
- package/dist/budo/plan.d.ts +64 -0
- package/dist/budo/plan.js +139 -0
- package/dist/budo/room.d.ts +133 -0
- package/dist/budo/room.js +111 -0
- package/dist/budo/sumo.d.ts +31 -0
- package/dist/budo/sumo.js +71 -0
- package/dist/budo/tatami.d.ts +55 -0
- package/dist/budo/tatami.js +156 -0
- package/dist/budo/types.d.ts +192 -0
- package/dist/budo/types.js +2 -0
- package/dist/budo-entry.d.ts +22 -0
- package/dist/budo-entry.js +13 -0
- package/dist/budo.board.d.ts +28 -0
- package/dist/budo.board.js +81 -0
- package/dist/budo.score.d.ts +18 -0
- package/dist/budo.score.js +52 -0
- package/dist/budo.style.d.ts +6 -0
- package/dist/budo.style.js +67 -0
- package/dist/charts-entry.d.ts +6 -1
- package/dist/charts-entry.js +3 -0
- package/dist/charts.athletics.d.ts +183 -0
- package/dist/charts.athletics.js +455 -0
- package/dist/charts.baseball.js +1 -1
- package/dist/charts.curling.heat.d.ts +24 -0
- package/dist/charts.curling.heat.js +116 -0
- package/dist/charts.curling.js +14 -3
- package/dist/charts.js +4 -4
- package/dist/charts.pool.d.ts +67 -0
- package/dist/charts.pool.js +76 -0
- package/dist/charts.skating.d.ts +204 -0
- package/dist/charts.skating.js +426 -0
- package/dist/charts.soccer.js +3 -3
- package/dist/charts.types.d.ts +63 -1
- package/dist/colour.d.ts +4 -4
- package/dist/colour.js +4 -4
- package/dist/court.d.ts +85 -2
- package/dist/court.js +102 -7
- package/dist/court.more.d.ts +46 -0
- package/dist/court.more.js +219 -0
- package/dist/courts.style.d.ts +14 -2
- package/dist/courts.style.js +51 -4
- package/dist/crowd.d.ts +164 -0
- package/dist/crowd.draw.d.ts +17 -0
- package/dist/crowd.draw.js +204 -0
- package/dist/crowd.js +480 -0
- package/dist/crowd.park.d.ts +19 -0
- package/dist/crowd.park.js +153 -0
- package/dist/curling.stats.d.ts +271 -0
- package/dist/curling.stats.js +301 -0
- package/dist/draw.d.ts +16 -0
- package/dist/draw.js +462 -54
- package/dist/element-entry.d.ts +3 -1
- package/dist/element-entry.js +2 -1
- package/dist/element.d.ts +13 -2
- package/dist/element.js +79 -4
- package/dist/emblems-source.d.ts +50 -0
- package/dist/emblems-source.js +56 -0
- package/dist/eras.d.ts +3 -1
- package/dist/eras.js +8 -3
- package/dist/features.js +204 -0
- package/dist/field.data.d.ts +16 -0
- package/dist/field.data.js +50 -0
- package/dist/fields.kit.d.ts +2 -2
- package/dist/fields.types.d.ts +24 -4
- package/dist/formations.js +18 -7
- package/dist/formations.sports.d.ts +8 -0
- package/dist/formations.sports.js +251 -0
- package/dist/frames.providers.js +71 -8
- package/dist/geojson.js +3 -0
- package/dist/geometry-entry.d.ts +2 -1
- package/dist/geometry-entry.js +2 -1
- package/dist/geometry.d.ts +20 -0
- package/dist/geometry.js +78 -1
- package/dist/grounds.d.ts +4 -0
- package/dist/grounds.data.js +12 -0
- package/dist/halls.d.ts +65 -0
- package/dist/halls.js +122 -0
- package/dist/hardwood.d.ts +31 -0
- package/dist/hardwood.js +157 -0
- package/dist/heat.js +21 -13
- package/dist/heat.types.d.ts +5 -0
- package/dist/heat.zones.d.ts +4 -2
- package/dist/heat.zones.js +207 -7
- package/dist/homeside.js +24 -0
- package/dist/index.d.ts +30 -4
- package/dist/index.js +18 -2
- package/dist/layers-entry.d.ts +2 -1
- package/dist/layers-entry.js +1 -0
- package/dist/layers.d.ts +2 -1
- package/dist/layers.env.d.ts +12 -1
- package/dist/layers.env.js +36 -4
- package/dist/layers.js +32 -13
- package/dist/layers.types.d.ts +9 -2
- package/dist/leagues/athletics.d.ts +63 -0
- package/dist/leagues/athletics.js +87 -0
- package/dist/leagues/budo.d.ts +196 -0
- package/dist/leagues/budo.js +105 -0
- package/dist/leagues/courts.d.ts +269 -4
- package/dist/leagues/courts.js +155 -1
- package/dist/leagues/hockey.js +41 -19
- package/dist/leagues/skating.d.ts +145 -0
- package/dist/leagues/skating.js +73 -0
- package/dist/leagues/sources.js +41 -0
- package/dist/leagues/types.d.ts +1 -1
- package/dist/leagues-entry.d.ts +4 -2
- package/dist/leagues-entry.js +2 -1
- package/dist/leagues.d.ts +17 -4
- package/dist/leagues.js +246 -5
- package/dist/lineup.codes.d.ts +23 -0
- package/dist/lineup.codes.js +122 -0
- package/dist/logo.d.ts +25 -0
- package/dist/logo.js +47 -0
- package/dist/look.d.ts +3 -3
- package/dist/look.js +39 -6
- package/dist/outline.d.ts +1 -1
- package/dist/outline.js +46 -4
- package/dist/pageant.d.ts +120 -0
- package/dist/pageant.draw.d.ts +21 -0
- package/dist/pageant.draw.js +171 -0
- package/dist/pageant.js +211 -0
- package/dist/pageant.style.d.ts +8 -0
- package/dist/pageant.style.js +42 -0
- package/dist/png.d.ts +8 -1
- package/dist/png.js +21 -1
- package/dist/pointer-entry.d.ts +2 -0
- package/dist/pointer-entry.js +2 -0
- package/dist/pointer.d.ts +113 -0
- package/dist/pointer.js +151 -0
- package/dist/pool.d.ts +176 -0
- package/dist/pool.figures.d.ts +306 -0
- package/dist/pool.figures.js +148 -0
- package/dist/pool.js +304 -0
- package/dist/pool.kit.d.ts +48 -0
- package/dist/pool.kit.js +141 -0
- package/dist/pool.labels.d.ts +20 -0
- package/dist/pool.labels.js +258 -0
- package/dist/pool.race.d.ts +65 -0
- package/dist/pool.race.js +140 -0
- package/dist/pool.replay.d.ts +46 -0
- package/dist/pool.replay.js +127 -0
- package/dist/pool.section.d.ts +13 -0
- package/dist/pool.section.js +83 -0
- package/dist/pool.style.d.ts +20 -0
- package/dist/pool.style.js +169 -0
- package/dist/pool.types.d.ts +145 -0
- package/dist/pool.types.js +3 -0
- package/dist/pool.words.d.ts +13 -0
- package/dist/pool.words.js +101 -0
- package/dist/ranges.names.js +53 -3
- package/dist/replay-entry.d.ts +3 -0
- package/dist/replay-entry.js +2 -0
- package/dist/restyle.js +1 -1
- package/dist/road.draw.js +3 -2
- package/dist/road.js +2 -1
- package/dist/road.types.d.ts +3 -0
- package/dist/rules.d.ts +138 -0
- package/dist/rules.js +511 -0
- package/dist/rules.types.d.ts +4 -0
- package/dist/rules.types.js +64 -0
- package/dist/score.d.ts +3 -0
- package/dist/score.js +93 -6
- package/dist/sheet.d.ts +3 -0
- package/dist/sheet.js +7 -3
- package/dist/situation.d.ts +135 -0
- package/dist/situation.draw.d.ts +34 -0
- package/dist/situation.draw.js +202 -0
- package/dist/situation.js +337 -0
- package/dist/situation.style.d.ts +2 -0
- package/dist/situation.style.js +45 -0
- package/dist/situation.types.d.ts +77 -0
- package/dist/situation.types.js +3 -0
- package/dist/situation.widgets.d.ts +39 -0
- package/dist/situation.widgets.js +129 -0
- package/dist/skating/describe.d.ts +7 -0
- package/dist/skating/describe.js +44 -0
- package/dist/skating/events.d.ts +24 -0
- package/dist/skating/events.js +47 -0
- package/dist/skating/figure.d.ts +24 -0
- package/dist/skating/figure.js +37 -0
- package/dist/skating/long.d.ts +54 -0
- package/dist/skating/long.js +111 -0
- package/dist/skating/parts.d.ts +43 -0
- package/dist/skating/parts.js +40 -0
- package/dist/skating/plan.d.ts +55 -0
- package/dist/skating/plan.js +197 -0
- package/dist/skating/short.d.ts +47 -0
- package/dist/skating/short.js +89 -0
- package/dist/skating/track.d.ts +66 -0
- package/dist/skating/track.js +94 -0
- package/dist/skating/types.d.ts +57 -0
- package/dist/skating/types.js +3 -0
- package/dist/skating-entry.d.ts +17 -0
- package/dist/skating-entry.js +11 -0
- package/dist/skating.style.d.ts +4 -0
- package/dist/skating.style.js +24 -0
- package/dist/strings.d.ts +470 -0
- package/dist/strings.js +674 -13
- package/dist/style.d.ts +2 -0
- package/dist/style.js +19 -2
- package/dist/surfaces/artistic.d.ts +9 -0
- package/dist/surfaces/artistic.js +94 -0
- package/dist/surfaces/athletics.d.ts +16 -0
- package/dist/surfaces/athletics.js +132 -0
- package/dist/surfaces/baseball.js +17 -0
- package/dist/surfaces/basketball.floor.d.ts +27 -0
- package/dist/surfaces/basketball.floor.js +83 -0
- package/dist/surfaces/basketball.gear.d.ts +27 -0
- package/dist/surfaces/basketball.gear.js +90 -0
- package/dist/surfaces/basketball.js +44 -22
- package/dist/surfaces/budo.d.ts +6 -0
- package/dist/surfaces/budo.js +104 -0
- package/dist/surfaces/curling.d.ts +16 -0
- package/dist/surfaces/curling.fold.d.ts +67 -0
- package/dist/surfaces/curling.fold.js +107 -0
- package/dist/surfaces/curling.js +37 -3
- package/dist/surfaces/dive.d.ts +9 -0
- package/dist/surfaces/dive.js +128 -0
- package/dist/surfaces/field.d.ts +7 -2
- package/dist/surfaces/field.js +15 -9
- package/dist/surfaces/football.js +7 -0
- package/dist/surfaces/hockey.js +4 -2
- package/dist/surfaces/net.d.ts +2 -1
- package/dist/surfaces/net.js +160 -15
- package/dist/surfaces/polo.d.ts +12 -0
- package/dist/surfaces/polo.js +159 -0
- package/dist/surfaces/pool.d.ts +27 -0
- package/dist/surfaces/pool.js +198 -0
- package/dist/surfaces/skating.d.ts +6 -0
- package/dist/surfaces/skating.js +62 -0
- package/dist/surfaces/soccer.js +2 -1
- package/dist/surfaces/surface.d.ts +76 -0
- package/dist/surfaces/venue.d.ts +15 -0
- package/dist/surfaces/venue.js +95 -0
- package/dist/target-entry.d.ts +2 -0
- package/dist/target-entry.js +2 -0
- package/dist/target.d.ts +200 -0
- package/dist/target.js +525 -0
- package/dist/team.common.d.ts +1 -1
- package/dist/team.common.js +3 -3
- package/dist/team.formation.d.ts +3 -1
- package/dist/team.formation.js +3 -3
- package/dist/team.layers.d.ts +2 -2
- package/dist/team.layers.js +4 -1
- package/dist/team.lineup.d.ts +27 -0
- package/dist/team.lineup.js +207 -0
- package/dist/team.network.js +6 -5
- package/dist/team.shape.js +5 -5
- package/dist/team.types.d.ts +40 -1
- package/dist/tenants.d.ts +77 -0
- package/dist/tenants.data.d.ts +8 -0
- package/dist/tenants.data.js +1793 -0
- package/dist/tenants.js +46 -0
- package/dist/turn-entry.d.ts +12 -0
- package/dist/turn-entry.js +7 -0
- package/dist/turn.camera.d.ts +116 -0
- package/dist/turn.camera.js +184 -0
- package/dist/turn.dom.d.ts +40 -0
- package/dist/turn.dom.js +353 -0
- package/dist/turn.kit.d.ts +86 -0
- package/dist/turn.kit.js +110 -0
- package/dist/turn.markup.d.ts +48 -0
- package/dist/turn.markup.js +108 -0
- package/dist/turn.model.d.ts +78 -0
- package/dist/turn.model.js +91 -0
- package/dist/turn.style.d.ts +5 -0
- package/dist/turn.style.js +56 -0
- package/dist/types.d.ts +221 -20
- package/dist/units.d.ts +1 -1
- package/dist/units.js +22 -1
- package/dist/uprights.board.d.ts +17 -0
- package/dist/uprights.board.js +59 -0
- package/dist/uprights.budo.d.ts +17 -0
- package/dist/uprights.budo.js +87 -0
- package/dist/uprights.d.ts +37 -0
- package/dist/uprights.fields.d.ts +32 -0
- package/dist/uprights.fields.js +172 -0
- package/dist/uprights.goal.d.ts +49 -0
- package/dist/uprights.goal.js +69 -0
- package/dist/uprights.js +169 -0
- package/dist/uprights.more.d.ts +7 -0
- package/dist/uprights.more.js +183 -0
- package/dist/uprights.net.d.ts +12 -0
- package/dist/uprights.net.js +109 -0
- package/dist/uprights.netkit.d.ts +28 -0
- package/dist/uprights.netkit.js +67 -0
- package/dist/uprights.pools.d.ts +8 -0
- package/dist/uprights.pools.js +190 -0
- package/dist/uprights.rinks.d.ts +27 -0
- package/dist/uprights.rinks.js +106 -0
- package/dist/uprights.track.d.ts +50 -0
- package/dist/uprights.track.js +246 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/zones-entry.d.ts +2 -0
- package/dist/zones-entry.js +2 -0
- package/package.json +56 -3
package/dist/crowd.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// The stands round a surface and the crowd in them (`crowd`): where the seating bowls stand, as data (rings with their outlines, heights and rake, the places that are left
|
|
2
|
+
// open for tunnels and benches, the blocks of visitors and of a card display), before any of it is drawn (crowd.draw.ts). Written for this package: each sport has a plain
|
|
3
|
+
// profile of the distances a venue of its kind keeps between the surface and its first row and how deep its tiers are; they are conventions for a drawing, not a rulebook's
|
|
4
|
+
// figures (no rulebook gives them), except the footprint of the gridiron a soccer pitch can stand inside, which is the field's own.
|
|
5
|
+
import { FOOTBALL } from "./geometry.js";
|
|
6
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
7
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
8
|
+
/** The crowd the options ask for, or `undefined` for none. */
|
|
9
|
+
export function resolveCrowd(value) {
|
|
10
|
+
if (value === undefined || value === false)
|
|
11
|
+
return undefined;
|
|
12
|
+
const o = value === true ? {} : value;
|
|
13
|
+
const shapes = ["bowl", "sides", "horseshoe", "grandstand"];
|
|
14
|
+
const awayEnds = ["auto", "left", "right", "corner", "none"];
|
|
15
|
+
return {
|
|
16
|
+
shape: shapes.includes(o.shape) ? o.shape : undefined,
|
|
17
|
+
open: o.open === "left" ? "left" : "right",
|
|
18
|
+
stadium: o.stadium === "purpose" || o.stadium === "nfl" || o.stadium === "cfl" ? o.stadium : "auto",
|
|
19
|
+
density: finite(o.density) ? clamp(o.density, 0, 1) : undefined,
|
|
20
|
+
attendance: finite(o.attendance) && o.attendance >= 0 ? o.attendance : undefined,
|
|
21
|
+
capacity: finite(o.capacity) && o.capacity > 0 ? o.capacity : undefined,
|
|
22
|
+
seed: finite(o.seed) ? Math.trunc(o.seed) : 1,
|
|
23
|
+
home: typeof o.home === "string" ? o.home : undefined,
|
|
24
|
+
away: typeof o.away === "string" ? o.away : undefined,
|
|
25
|
+
awayEnd: awayEnds.includes(o.awayEnd) ? o.awayEnd : "auto",
|
|
26
|
+
tifo: o.tifo === true ? "home" : o.tifo === "home" || o.tifo === "away" || o.tifo === "both" ? o.tifo : undefined,
|
|
27
|
+
tifoDesign: o.tifoDesign === "checks" ? "checks" : "stripes",
|
|
28
|
+
gaps: o.gaps !== false,
|
|
29
|
+
roof: o.roof !== false,
|
|
30
|
+
wallSeats: typeof o.wallSeats === "boolean" ? o.wallSeats : undefined,
|
|
31
|
+
detail: o.detail === "fine" || o.detail === "plain" ? o.detail : "auto",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** How full the stands are: a density, else attendance over capacity (the ground's, else the model's seats), else 0.85. */
|
|
35
|
+
export function densityOf(crowd, seats, groundCapacity) {
|
|
36
|
+
if (crowd.density !== undefined)
|
|
37
|
+
return crowd.density;
|
|
38
|
+
if (crowd.attendance !== undefined)
|
|
39
|
+
return clamp(crowd.attendance / (crowd.capacity ?? groundCapacity ?? seats), 0, 1);
|
|
40
|
+
return 0.85;
|
|
41
|
+
}
|
|
42
|
+
/** A small, fast generator (mulberry32): the same seed gives the same numbers on every machine. */
|
|
43
|
+
export function generator(seed) {
|
|
44
|
+
let a = seed >>> 0;
|
|
45
|
+
return () => {
|
|
46
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
47
|
+
let t = a;
|
|
48
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
49
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
50
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const lower = (to, h0 = 5, h1 = 45) => ({ id: "lower", from: 0, to, height: [h0, h1], fill: 1 });
|
|
54
|
+
const upper = (from, to, h0 = 62, h1 = 118) => ({ id: "upper", from, to, height: [h0, h1], fill: 0.88 });
|
|
55
|
+
const courtside = (to = 7) => ({ id: "courtside", from: 0, to, height: [1, 4], fill: 1 });
|
|
56
|
+
/** The venues, by sport, in feet. */
|
|
57
|
+
const PROFILES = {
|
|
58
|
+
nfl: { shape: "bowl", side: 40, end: 26, bench: 0, corner: 60, tiers: [lower(85), upper(92, 165)], away: "end" },
|
|
59
|
+
cfl: { shape: "bowl", side: 42, end: 28, bench: 0, corner: 60, tiers: [lower(85), upper(92, 165)], away: "end" },
|
|
60
|
+
ifaf: { shape: "bowl", side: 40, end: 26, bench: 0, corner: 55, tiers: [lower(75), upper(82, 144)], away: "end" },
|
|
61
|
+
soccer: { shape: "bowl", side: 19.7, end: 27.9, bench: 8, corner: 40, tiers: [lower(75), upper(82, 144)], away: "end" },
|
|
62
|
+
nhl: { shape: "bowl", side: 8, end: 8, bench: 6, corner: 40, tiers: [lower(58, 3, 28), upper(65, 121, 38, 74)], away: "corner" },
|
|
63
|
+
nba: { shape: "bowl", side: 5, end: 10, bench: 9, corner: 30, tiers: [courtside(7), { id: "lower", from: 13, to: 70, height: [3, 32], fill: 1 }, upper(77, 131, 42, 80)], away: "corner" },
|
|
64
|
+
mlb: { shape: "horseshoe", side: 0, end: 0, bench: 0, corner: 0, tiers: [], away: "corner" },
|
|
65
|
+
curling: { shape: "sides", side: 14, end: 10, bench: 0, corner: 6, tiers: [lower(30, 2, 12)], away: "corner" },
|
|
66
|
+
tennis: { shape: "bowl", side: 22, end: 32, bench: 4, corner: 24, tiers: [lower(44, 3, 24), upper(51, 95, 32, 62)], away: "corner" },
|
|
67
|
+
volleyball: { shape: "bowl", side: 20, end: 24, bench: 4, corner: 22, tiers: [lower(40, 3, 22), upper(47, 87, 30, 58)], away: "corner" },
|
|
68
|
+
badminton: { shape: "bowl", side: 16, end: 22, bench: 4, corner: 20, tiers: [lower(40, 3, 22), upper(47, 87, 30, 58)], away: "corner" },
|
|
69
|
+
pickleball: { shape: "sides", side: 18, end: 16, bench: 0, corner: 8, tiers: [lower(32, 2, 12)], away: "corner" },
|
|
70
|
+
tabletennis: { shape: "bowl", side: 16, end: 16, bench: 2, corner: 14, tiers: [lower(34, 2, 16), upper(41, 73, 22, 40)], away: "corner" },
|
|
71
|
+
// The court sports after those: indoor halls and show courts, ours (no rulebook gives the stands), the size of the stands following the size of the court.
|
|
72
|
+
padel: { shape: "bowl", side: 20, end: 22, bench: 4, corner: 20, tiers: [lower(40, 3, 22), upper(47, 87, 30, 58)], away: "corner" },
|
|
73
|
+
squash: { shape: "sides", side: 12, end: 10, bench: 0, corner: 8, tiers: [lower(26, 2, 12), upper(32, 56, 18, 32)], away: "corner" },
|
|
74
|
+
racquetball: { shape: "sides", side: 12, end: 10, bench: 0, corner: 8, tiers: [lower(26, 2, 12), upper(32, 56, 18, 32)], away: "corner" },
|
|
75
|
+
sepaktakraw: { shape: "bowl", side: 18, end: 22, bench: 4, corner: 20, tiers: [lower(40, 3, 22), upper(47, 87, 30, 58)], away: "corner" },
|
|
76
|
+
teqball: { shape: "bowl", side: 18, end: 20, bench: 3, corner: 18, tiers: [lower(38, 3, 20), upper(45, 79, 28, 52)], away: "corner" },
|
|
77
|
+
roundnet: { shape: "sides", side: 16, end: 12, bench: 0, corner: 8, tiers: [lower(26, 2, 12), upper(32, 56, 18, 32)], away: "corner" },
|
|
78
|
+
netball: { shape: "bowl", side: 20, end: 22, bench: 4, corner: 20, tiers: [lower(44, 3, 24), upper(51, 95, 32, 62)], away: "corner" },
|
|
79
|
+
// The pools: a natatorium has its stands along the long sides, a deck's width back from the water (ours: no rulebook gives the stands).
|
|
80
|
+
swimming: { shape: "sides", side: 26, end: 20, bench: 0, corner: 10, tiers: [lower(40, 3, 20), upper(47, 87, 28, 52)], away: "corner" },
|
|
81
|
+
waterpolo: { shape: "sides", side: 22, end: 18, bench: 0, corner: 10, tiers: [lower(36, 3, 18), upper(43, 79, 26, 48)], away: "corner" },
|
|
82
|
+
diving: { shape: "sides", side: 26, end: 20, bench: 0, corner: 10, tiers: [lower(40, 3, 20), upper(47, 87, 28, 52)], away: "corner" },
|
|
83
|
+
artisticswimming: { shape: "sides", side: 26, end: 20, bench: 0, corner: 10, tiers: [lower(40, 3, 20), upper(47, 87, 28, 52)], away: "corner" },
|
|
84
|
+
// Athletics: an oval bowl round the track, a stadium's two tiers (ours); skating: an arena's, close to the ice as a hockey rink is, rounded at the corners.
|
|
85
|
+
athletics: { shape: "bowl", side: 30, end: 30, bench: 0, corner: 0, tiers: [lower(70), upper(77, 135)], oval: true, away: "end" },
|
|
86
|
+
skating: { shape: "bowl", side: 12, end: 14, bench: 0, corner: 60, tiers: [lower(58, 3, 28), upper(65, 121, 38, 74)], away: "corner" },
|
|
87
|
+
lacrosse: { shape: "bowl", side: 24, end: 30, bench: 6, corner: 40, tiers: [lower(70), upper(77, 125, 58, 100)], away: "end" },
|
|
88
|
+
rugby: { shape: "bowl", side: 26, end: 36, bench: 6, corner: 48, tiers: [lower(78), upper(85, 145)], away: "end" },
|
|
89
|
+
afl: { shape: "bowl", side: 30, end: 30, bench: 0, corner: 0, tiers: [lower(70), upper(77, 135)], oval: true, away: "end" },
|
|
90
|
+
gaelic: { shape: "bowl", side: 34, end: 40, bench: 6, corner: 50, tiers: [lower(80), upper(87, 151)], away: "end" },
|
|
91
|
+
cricket: { shape: "bowl", side: 30, end: 30, bench: 0, corner: 0, tiers: [lower(66), upper(73, 125)], oval: true, away: "end" },
|
|
92
|
+
fieldhockey: { shape: "bowl", side: 20, end: 26, bench: 5, corner: 30, tiers: [lower(46, 3, 24), upper(53, 87, 32, 54)], away: "end" },
|
|
93
|
+
handball: { shape: "bowl", side: 14, end: 18, bench: 4, corner: 16, tiers: [courtside(6), { id: "lower", from: 10, to: 46, height: [3, 22], fill: 1 }, upper(53, 97, 30, 56)], away: "corner" },
|
|
94
|
+
// The budō (ours: no rule gives the stands). The competition areas of judo, karate and kendo stand in an arena's bowl, as the Nippon Budokan's does round its octagonal floor (the Budokan's leaflet: 2,946 temporary
|
|
95
|
+
// seats on the arena, 2,959 to 3,199 on the first floor, 7,876 on the second and third); sumo's dohyō stands in the middle of a hall of box seats and a gallery (the Ryōgoku Kokugikan's); a kyūdōjō's
|
|
96
|
+
// spectators stand along the yamichi; a dojo's gallery looks down on the mats from above, as the Kodokan's does from the eighth floor.
|
|
97
|
+
judo: { shape: "bowl", side: 36, end: 36, bench: 6, corner: 50, tiers: [lower(60, 3, 24), upper(67, 120, 32, 60)], away: "corner", arena: { side: 8, end: 8, bench: 0, corner: 56, tiers: [lower(34, 3, 24), upper(40, 74, 32, 62)] } },
|
|
98
|
+
karate: { shape: "bowl", side: 36, end: 36, bench: 6, corner: 50, tiers: [lower(60, 3, 24), upper(67, 120, 32, 60)], away: "corner", arena: { side: 8, end: 8, bench: 0, corner: 56, tiers: [lower(34, 3, 24), upper(40, 74, 32, 62)] } },
|
|
99
|
+
kendo: { shape: "bowl", side: 36, end: 36, bench: 6, corner: 50, tiers: [lower(60, 3, 24), upper(67, 120, 32, 60)], away: "corner", arena: { side: 8, end: 8, bench: 0, corner: 56, tiers: [lower(34, 3, 24), upper(40, 74, 32, 62)] } },
|
|
100
|
+
sumo: { shape: "bowl", side: 58, end: 58, bench: 0, corner: 56, tiers: [{ id: "lower", from: 0, to: 54, height: [1, 8], fill: 1 }, upper(62, 128, 26, 62)], away: "corner" },
|
|
101
|
+
kyudo: { shape: "sides", side: 10, end: 8, bench: 0, corner: 6, tiers: [lower(24, 2, 8), upper(30, 52, 12, 22)], away: "corner" },
|
|
102
|
+
dojo: { shape: "bowl", side: 5, end: 5, bench: 0, corner: 8, tiers: [lower(26, 2, 14), upper(32, 58, 20, 36)], away: "corner" },
|
|
103
|
+
};
|
|
104
|
+
/** The grounds of the package that are the home of an NFL or a CFL club, where a soccer pitch stands inside a gridiron's stands. From each ground's article. */
|
|
105
|
+
export const GRIDIRON_GROUNDS = Object.freeze({
|
|
106
|
+
"metlife-stadium": "nfl",
|
|
107
|
+
"att-stadium": "nfl",
|
|
108
|
+
"arrowhead-stadium": "nfl",
|
|
109
|
+
"nrg-stadium": "nfl",
|
|
110
|
+
"mercedes-benz-stadium": "nfl",
|
|
111
|
+
"hard-rock-stadium": "nfl",
|
|
112
|
+
"sofi-stadium": "nfl",
|
|
113
|
+
"levis-stadium": "nfl",
|
|
114
|
+
"lumen-field": "nfl",
|
|
115
|
+
"gillette-stadium": "nfl",
|
|
116
|
+
"lincoln-financial-field": "nfl",
|
|
117
|
+
"rose-bowl": "nfl",
|
|
118
|
+
"bc-place": "cfl",
|
|
119
|
+
});
|
|
120
|
+
/** A gridiron's field, with its end zones, in feet, from the rulebook figures of the package: the NFL's 360 by 160, the CFL's 450 by 195. */
|
|
121
|
+
function gridiron(kind) {
|
|
122
|
+
const f = FOOTBALL[kind];
|
|
123
|
+
return { length: f.length + 2 * f.endZone, width: f.width, endZone: f.endZone };
|
|
124
|
+
}
|
|
125
|
+
/** The walls the package knows have seats on top of them, by park and the wall's label: Fenway Park's Green Monster (the Monster seats, 2003). */
|
|
126
|
+
export const WALL_SEATS = Object.freeze({ "fenway-park": "Green Monster" });
|
|
127
|
+
// ---- Outlines. ----
|
|
128
|
+
const sq = (v) => v * v;
|
|
129
|
+
/** The point of an outline hit by a ray from `(cx, cy)` at angle `a` (screen angle, y down). */
|
|
130
|
+
export function rayAt(o, cx, cy, a) {
|
|
131
|
+
const dx = Math.cos(a);
|
|
132
|
+
const dy = Math.sin(a);
|
|
133
|
+
if (o.kind === "ellipse") {
|
|
134
|
+
const t = 1 / Math.sqrt(sq(dx / o.rx) + sq(dy / o.ry));
|
|
135
|
+
return [cx + dx * t, cy + dy * t];
|
|
136
|
+
}
|
|
137
|
+
if (o.kind === "poly")
|
|
138
|
+
return o.points[0];
|
|
139
|
+
const w = (o.x1 - o.x0) / 2;
|
|
140
|
+
const h = (o.y1 - o.y0) / 2;
|
|
141
|
+
const mx = (o.x0 + o.x1) / 2;
|
|
142
|
+
const my = (o.y0 + o.y1) / 2;
|
|
143
|
+
// The ray from the middle of the rectangle, to the sides, and the corner circle where it lands in one.
|
|
144
|
+
const t = Math.min(Math.abs(dx) < 1e-9 ? Infinity : w / Math.abs(dx), Math.abs(dy) < 1e-9 ? Infinity : h / Math.abs(dy));
|
|
145
|
+
let px = cx + dx * t;
|
|
146
|
+
let py = cy + dy * t;
|
|
147
|
+
const r = Math.min(o.r, w, h);
|
|
148
|
+
const kx = Math.abs(px - mx) - (w - r);
|
|
149
|
+
const ky = Math.abs(py - my) - (h - r);
|
|
150
|
+
if (r > 0 && kx > 0 && ky > 0) {
|
|
151
|
+
// Solve |(c + d t) - corner centre| = r for the larger t.
|
|
152
|
+
const ccx = mx + Math.sign(px - mx) * (w - r);
|
|
153
|
+
const ccy = my + Math.sign(py - my) * (h - r);
|
|
154
|
+
const fx = cx - ccx;
|
|
155
|
+
const fy = cy - ccy;
|
|
156
|
+
const b = fx * dx + fy * dy;
|
|
157
|
+
const c = fx * fx + fy * fy - r * r;
|
|
158
|
+
const t2 = -b + Math.sqrt(Math.max(0, b * b - c));
|
|
159
|
+
px = cx + dx * t2;
|
|
160
|
+
py = cy + dy * t2;
|
|
161
|
+
}
|
|
162
|
+
return [px, py];
|
|
163
|
+
}
|
|
164
|
+
const n1 = (v) => {
|
|
165
|
+
const t = (Math.round(v * 10) / 10).toString();
|
|
166
|
+
return t === "-0" ? "0" : t;
|
|
167
|
+
};
|
|
168
|
+
/** An outline as a path, drawn clockwise, or counter-clockwise when `reverse` (a hole in a band that is filled with the non-zero rule). */
|
|
169
|
+
export function outlinePath(o, reverse = false) {
|
|
170
|
+
if (o.kind === "poly") {
|
|
171
|
+
const pts = reverse ? [...o.points].reverse() : o.points;
|
|
172
|
+
return `M${pts.map(([x, y]) => `${Math.round(x)} ${Math.round(y)}`).join("L")}Z`;
|
|
173
|
+
}
|
|
174
|
+
if (o.kind === "ellipse") {
|
|
175
|
+
const sweep = reverse ? 0 : 1;
|
|
176
|
+
return `M${n1(o.cx + o.rx)} ${n1(o.cy)}A${n1(o.rx)} ${n1(o.ry)} 0 1 ${sweep} ${n1(o.cx - o.rx)} ${n1(o.cy)}A${n1(o.rx)} ${n1(o.ry)} 0 1 ${sweep} ${n1(o.cx + o.rx)} ${n1(o.cy)}Z`;
|
|
177
|
+
}
|
|
178
|
+
const r = Math.min(o.r, (o.x1 - o.x0) / 2, (o.y1 - o.y0) / 2);
|
|
179
|
+
const { x0, y0, x1, y1 } = o;
|
|
180
|
+
const a = (x, y, s) => (r > 0 ? `A${n1(r)} ${n1(r)} 0 0 ${s} ${n1(x)} ${n1(y)}` : "");
|
|
181
|
+
if (!reverse)
|
|
182
|
+
return `M${n1(x0 + r)} ${n1(y0)}H${n1(x1 - r)}${a(x1, y0 + r, 1)}V${n1(y1 - r)}${a(x1 - r, y1, 1)}H${n1(x0 + r)}${a(x0, y1 - r, 1)}V${n1(y0 + r)}${a(x0 + r, y0, 1)}Z`;
|
|
183
|
+
return `M${n1(x0 + r)} ${n1(y0)}${a(x0, y0 + r, 0)}V${n1(y1 - r)}${a(x0 + r, y1, 0)}H${n1(x1 - r)}${a(x1, y1 - r, 0)}V${n1(y0 + r)}${a(x1 - r, y0, 0)}Z`;
|
|
184
|
+
}
|
|
185
|
+
/** An outline as points, for a view that raises the stands: arcs are cut into steps of about 7.5 degrees. */
|
|
186
|
+
export function outlinePoints(o) {
|
|
187
|
+
if (o.kind === "poly")
|
|
188
|
+
return o.points.map(([x, y]) => [x, y]);
|
|
189
|
+
const out = [];
|
|
190
|
+
if (o.kind === "ellipse") {
|
|
191
|
+
for (let i = 0; i < 48; i += 1)
|
|
192
|
+
out.push([o.cx + o.rx * Math.cos((i * Math.PI) / 24), o.cy + o.ry * Math.sin((i * Math.PI) / 24)]);
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
const r = Math.min(o.r, (o.x1 - o.x0) / 2, (o.y1 - o.y0) / 2);
|
|
196
|
+
const corners = [
|
|
197
|
+
[o.x1 - r, o.y0 + r, -90],
|
|
198
|
+
[o.x1 - r, o.y1 - r, 0],
|
|
199
|
+
[o.x0 + r, o.y1 - r, 90],
|
|
200
|
+
[o.x0 + r, o.y0 + r, 180],
|
|
201
|
+
];
|
|
202
|
+
for (const [cx, cy, a0] of corners) {
|
|
203
|
+
const steps = r > 0 ? 12 : 1;
|
|
204
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
205
|
+
const a = ((a0 + (i * 90) / steps) * Math.PI) / 180;
|
|
206
|
+
out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
const area = (o) => {
|
|
212
|
+
if (o.kind === "ellipse")
|
|
213
|
+
return Math.PI * o.rx * o.ry;
|
|
214
|
+
if (o.kind === "rrect") {
|
|
215
|
+
const r = Math.min(o.r, (o.x1 - o.x0) / 2, (o.y1 - o.y0) / 2);
|
|
216
|
+
return (o.x1 - o.x0) * (o.y1 - o.y0) - (4 - Math.PI) * r * r;
|
|
217
|
+
}
|
|
218
|
+
let s = 0;
|
|
219
|
+
o.points.forEach(([x, y], i) => {
|
|
220
|
+
const [x2, y2] = o.points[(i + 1) % o.points.length];
|
|
221
|
+
s += x * y2 - x2 * y;
|
|
222
|
+
});
|
|
223
|
+
return Math.abs(s) / 2;
|
|
224
|
+
};
|
|
225
|
+
/** The seats of a band of the stands: its area at a seat's 2 by 2.3 feet. */
|
|
226
|
+
export const seatsIn = (region, hole) => Math.round((area(region) - (hole === null ? 0 : area(hole))) / 4.6);
|
|
227
|
+
/** A point of a rounded rectangle and the outward normal there, by its parameter `u` (any real number, taken modulo 8): the four straight sides are the even whole numbers (the top from left to right, the right from top to bottom and so on round) and the four corners the odd ones. Concentric outlines (the same corners' centres) have the same parameter at the points an aisle joins. */
|
|
228
|
+
export function rrectAt(o, u) {
|
|
229
|
+
const m = ((u % 8) + 8) % 8;
|
|
230
|
+
const k = Math.floor(m);
|
|
231
|
+
const f = m - k;
|
|
232
|
+
const w = o.x1 - o.x0;
|
|
233
|
+
const h = o.y1 - o.y0;
|
|
234
|
+
const r = Math.min(o.r, w / 2, h / 2);
|
|
235
|
+
const lh = w - 2 * r;
|
|
236
|
+
const lv = h - 2 * r;
|
|
237
|
+
const arc = (cx, cy, a0) => {
|
|
238
|
+
const a = ((a0 + 90 * f) * Math.PI) / 180;
|
|
239
|
+
return { p: [cx + r * Math.cos(a), cy + r * Math.sin(a)], n: [Math.cos(a), Math.sin(a)] };
|
|
240
|
+
};
|
|
241
|
+
switch (k) {
|
|
242
|
+
case 0:
|
|
243
|
+
return { p: [o.x0 + r + f * lh, o.y0], n: [0, -1] };
|
|
244
|
+
case 1:
|
|
245
|
+
return arc(o.x1 - r, o.y0 + r, -90);
|
|
246
|
+
case 2:
|
|
247
|
+
return { p: [o.x1, o.y0 + r + f * lv], n: [1, 0] };
|
|
248
|
+
case 3:
|
|
249
|
+
return arc(o.x1 - r, o.y1 - r, 0);
|
|
250
|
+
case 4:
|
|
251
|
+
return { p: [o.x1 - r - f * lh, o.y1], n: [0, 1] };
|
|
252
|
+
case 5:
|
|
253
|
+
return arc(o.x0 + r, o.y1 - r, 90);
|
|
254
|
+
case 6:
|
|
255
|
+
return { p: [o.x0, o.y1 - r - f * lv], n: [-1, 0] };
|
|
256
|
+
default:
|
|
257
|
+
return arc(o.x0 + r, o.y0 + r, 180);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** The aisles a band has, as parameters of its inner outline: one at each end of every straight side (about every 30 feet along it) and one in the middle of every corner. */
|
|
261
|
+
function aisleParameters(hole) {
|
|
262
|
+
const r = Math.min(hole.r, (hole.x1 - hole.x0) / 2, (hole.y1 - hole.y0) / 2);
|
|
263
|
+
const nh = Math.max(2, Math.round((hole.x1 - hole.x0 - 2 * r) / 30));
|
|
264
|
+
const nv = Math.max(2, Math.round((hole.y1 - hole.y0 - 2 * r) / 30));
|
|
265
|
+
const us = [];
|
|
266
|
+
for (let k = 0; k < 8; k += 1) {
|
|
267
|
+
if (k % 2 === 1)
|
|
268
|
+
us.push(k + 0.5);
|
|
269
|
+
else
|
|
270
|
+
for (let i = 0; i < (k % 4 === 0 ? nh : nv); i += 1)
|
|
271
|
+
us.push(k + i / (k % 4 === 0 ? nh : nv));
|
|
272
|
+
}
|
|
273
|
+
return { us, nh, nv };
|
|
274
|
+
}
|
|
275
|
+
/** The aisles of a band as lines from its inner edge to its outer. */
|
|
276
|
+
function aislesOf(region, hole) {
|
|
277
|
+
if (region.kind === "rrect" && hole !== null && hole.kind === "rrect")
|
|
278
|
+
return aisleParameters(hole).us.map((u) => [rrectAt(hole, u).p, rrectAt(region, u).p]);
|
|
279
|
+
if (region.kind === "ellipse" && hole !== null && hole.kind === "ellipse") {
|
|
280
|
+
return Array.from({ length: 48 }, (_, i) => {
|
|
281
|
+
const t = (i * Math.PI) / 24;
|
|
282
|
+
return [[hole.cx + hole.rx * Math.cos(t), hole.cy + hole.ry * Math.sin(t)], [region.cx + region.rx * Math.cos(t), region.cy + region.ry * Math.sin(t)]];
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
/** The polygon of a band between two aisle parameters: along the outer edge, then back along the inner. */
|
|
288
|
+
function sectionBetween(region, hole, u0, u1) {
|
|
289
|
+
const us = [u0];
|
|
290
|
+
for (let u = Math.floor(u0 * 4) / 4 + 0.25; u < u1 - 1e-9; u += 0.25)
|
|
291
|
+
if (u > u0 + 1e-9)
|
|
292
|
+
us.push(u);
|
|
293
|
+
us.push(u1);
|
|
294
|
+
return [...us.map((u) => rrectAt(region, u).p), ...us.map((u) => rrectAt(hole, u).p).reverse()];
|
|
295
|
+
}
|
|
296
|
+
/** The offsets of the first row from the surface's edge at each side, for the venue the sport has (and a gridiron's footprint for a soccer pitch in one). */
|
|
297
|
+
function offsets(p, box, stadium) {
|
|
298
|
+
if (stadium === "purpose")
|
|
299
|
+
return { left: p.end, right: p.end, top: p.side, bottom: p.side + p.bench };
|
|
300
|
+
// A gridiron stadium keeps a football field's footprint, with the stands where a football field's stands are: the pitch stands inside it, in the middle.
|
|
301
|
+
const g = PROFILES[stadium];
|
|
302
|
+
const f = gridiron(stadium);
|
|
303
|
+
const end = g.end + (f.length - box.w) / 2;
|
|
304
|
+
const side = g.side + (f.width - box.h) / 2;
|
|
305
|
+
return { left: end, right: end, top: side, bottom: side + g.bench };
|
|
306
|
+
}
|
|
307
|
+
/** The layout of a sport's stands round its surface: rings, covers, sectors and gaps. For a ballpark see `parkLayout`. */
|
|
308
|
+
export function layoutOf(input) {
|
|
309
|
+
const { sport, box } = input;
|
|
310
|
+
const gal = input.gallery;
|
|
311
|
+
// A gallery has no home and no visitors, no card display and no tunnel, camera or benches.
|
|
312
|
+
const crowd = gal === undefined ? input.crowd : { ...input.crowd, awayEnd: "none", tifo: undefined, gaps: false, shape: input.crowd.shape === "sides" ? "sides" : "bowl" };
|
|
313
|
+
const base0 = gal === undefined ? PROFILES[sport] : { shape: "bowl", side: gal.offset, end: gal.offset, bench: 0, corner: 0, tiers: [{ id: "gallery", from: 0, to: gal.depth, height: gal.height, fill: 1 }], away: "corner" };
|
|
314
|
+
const base = input.arena === true && base0.arena !== undefined ? { ...base0, ...base0.arena, tiers: base0.arena.tiers ?? base0.tiers } : base0;
|
|
315
|
+
const host = sport === "soccer" ? (crowd.stadium === "nfl" || crowd.stadium === "cfl" ? crowd.stadium : crowd.stadium === "auto" && input.ground !== undefined ? GRIDIRON_GROUNDS[input.ground] : undefined) : undefined;
|
|
316
|
+
const stadium = sport === "soccer" ? host ?? "purpose" : undefined;
|
|
317
|
+
const p = stadium === "nfl" || stadium === "cfl" ? { ...PROFILES[stadium], shape: base.shape, away: base.away } : base;
|
|
318
|
+
const shape = crowd.shape ?? p.shape;
|
|
319
|
+
const off = offsets(base, box, stadium ?? "purpose");
|
|
320
|
+
const cx = box.x + box.w / 2;
|
|
321
|
+
const cy = box.y + box.h / 2;
|
|
322
|
+
const rings = [];
|
|
323
|
+
const reachOf = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
324
|
+
for (const t of p.tiers) {
|
|
325
|
+
const at = (k) => {
|
|
326
|
+
if (base.oval === true)
|
|
327
|
+
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 };
|
|
328
|
+
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 };
|
|
329
|
+
};
|
|
330
|
+
const region = at(t.to);
|
|
331
|
+
const hole = at(t.from);
|
|
332
|
+
const rise = t.height[1] - t.height[0];
|
|
333
|
+
rings.push({
|
|
334
|
+
id: t.id,
|
|
335
|
+
tier: t.id,
|
|
336
|
+
region,
|
|
337
|
+
hole,
|
|
338
|
+
from: t.from,
|
|
339
|
+
to: t.to,
|
|
340
|
+
height: [t.height[0], t.height[1]],
|
|
341
|
+
rake: Math.round((Math.atan2(rise, Math.max(1, t.to - t.from)) * 180) / Math.PI * 10) / 10,
|
|
342
|
+
fill: t.fill,
|
|
343
|
+
seats: seatsIn(region, hole),
|
|
344
|
+
aisles: aislesOf(region, hole),
|
|
345
|
+
});
|
|
346
|
+
reachOf.left = Math.max(reachOf.left, off.left + t.to);
|
|
347
|
+
reachOf.right = Math.max(reachOf.right, off.right + t.to);
|
|
348
|
+
reachOf.top = Math.max(reachOf.top, off.top + t.to);
|
|
349
|
+
reachOf.bottom = Math.max(reachOf.bottom, off.bottom + t.to);
|
|
350
|
+
}
|
|
351
|
+
const last = rings[rings.length - 1];
|
|
352
|
+
const covers = [];
|
|
353
|
+
const x0 = box.x - off.left;
|
|
354
|
+
const x1 = box.x + box.w + off.right;
|
|
355
|
+
const yTop = last === undefined ? box.y : box.y - reachOf.top - 4;
|
|
356
|
+
const yBot = last === undefined ? box.y + box.h : box.y + box.h + reachOf.bottom + 4;
|
|
357
|
+
if (shape === "horseshoe") {
|
|
358
|
+
// The open end: everything beyond the surface's end line and the gap that is kept clear.
|
|
359
|
+
if (crowd.open === "left")
|
|
360
|
+
covers.push([box.x - reachOf.left - 4, yTop, x0 + p.corner * 0.5, yBot]);
|
|
361
|
+
else
|
|
362
|
+
covers.push([x1 - p.corner * 0.5, yTop, box.x + box.w + reachOf.right + 4, yBot]);
|
|
363
|
+
}
|
|
364
|
+
else if (shape === "sides") {
|
|
365
|
+
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]);
|
|
366
|
+
}
|
|
367
|
+
const concourses = [];
|
|
368
|
+
rings.forEach((ring, i) => {
|
|
369
|
+
const before = rings[i - 1];
|
|
370
|
+
if (before !== undefined && ring.hole !== null && ring.from > before.to)
|
|
371
|
+
concourses.push({ region: ring.hole, hole: before.region });
|
|
372
|
+
});
|
|
373
|
+
const stands = { sport, shape, box, rings, covers, concourses, sectors: [], gaps: [], reach: reachOf, seats: rings.reduce((n, r) => n + r.seats, 0) };
|
|
374
|
+
if (stadium !== undefined)
|
|
375
|
+
stands.stadium = stadium;
|
|
376
|
+
if (gal !== undefined)
|
|
377
|
+
stands.gallery = true;
|
|
378
|
+
if (stadium === "nfl" || stadium === "cfl") {
|
|
379
|
+
const f = gridiron(stadium);
|
|
380
|
+
const field = { x: cx - f.length / 2, y: cy - f.width / 2, w: f.length, h: f.width };
|
|
381
|
+
stands.footprint = { field, endZones: [{ x: field.x, y: field.y, w: f.endZone, h: f.width }, { x: field.x + f.length - f.endZone, y: field.y, w: f.endZone, h: f.width }] };
|
|
382
|
+
}
|
|
383
|
+
if (shape === "sides" && last !== undefined) {
|
|
384
|
+
// Two sides: nothing beyond the ends.
|
|
385
|
+
reachOf.left = Math.min(reachOf.left, off.left + 4);
|
|
386
|
+
reachOf.right = Math.min(reachOf.right, off.right + 4);
|
|
387
|
+
}
|
|
388
|
+
// The visitors, a card display, and the places left open.
|
|
389
|
+
const lowerRing = rings.find((r) => r.tier === "lower") ?? rings[0];
|
|
390
|
+
if (lowerRing !== undefined) {
|
|
391
|
+
const side = crowd.awayEnd === "left" || crowd.awayEnd === "right" ? crowd.awayEnd : input.awaySide;
|
|
392
|
+
const sign = side === "right" ? 1 : -1;
|
|
393
|
+
const mode = crowd.awayEnd === "auto" ? (p.away === "end" ? "end" : "corner") : crowd.awayEnd === "corner" ? "corner" : "end";
|
|
394
|
+
const tiers = rings.filter((r) => r.tier === "lower" || r.tier === "upper");
|
|
395
|
+
const hole = lowerRing.hole;
|
|
396
|
+
if (lowerRing.region.kind === "rrect" && hole !== null && hole.kind === "rrect") {
|
|
397
|
+
// Round a rectangle a block is whole sections: it starts and ends on aisles, so that it is a section of the stands and not a wedge.
|
|
398
|
+
const { nh, nv } = aisleParameters(hole);
|
|
399
|
+
const clear = (u) => {
|
|
400
|
+
const [px, py] = rrectAt(lowerRing.region, u).p;
|
|
401
|
+
return !covers.some(([bx0, by0, bx1, by1]) => px >= bx0 && px <= bx1 && py >= by0 && py <= by1);
|
|
402
|
+
};
|
|
403
|
+
const section = (kind, u0, u1, only) => {
|
|
404
|
+
if (!clear((u0 + u1) / 2) || !clear(u0) || !clear(u1))
|
|
405
|
+
return;
|
|
406
|
+
for (const ring of only === undefined ? tiers : [only])
|
|
407
|
+
if (ring.region.kind === "rrect" && ring.hole?.kind === "rrect")
|
|
408
|
+
stands.sectors.push({ ring: ring.id, kind, points: sectionBetween(ring.region, ring.hole, u0, u1) });
|
|
409
|
+
};
|
|
410
|
+
// An end's side is the right (segment 2) or the left (segment 6); a block on it is from one aisle to another near its middle.
|
|
411
|
+
const along = (seg, from, to) => [seg + Math.max(0, Math.min(nv - 1, Math.round(nv * from))) / nv, seg + Math.max(1, Math.min(nv, Math.round(nv * to))) / nv];
|
|
412
|
+
if (crowd.awayEnd !== "none") {
|
|
413
|
+
if (mode === "end")
|
|
414
|
+
section("away", ...along(sign > 0 ? 2 : 6, 0.28, 0.72));
|
|
415
|
+
else if (sign > 0)
|
|
416
|
+
section("away", (nh - 2) / nh, 1.5);
|
|
417
|
+
else
|
|
418
|
+
section("away", 7.5, 8 + 2 / nh);
|
|
419
|
+
}
|
|
420
|
+
if (crowd.tifo !== undefined) {
|
|
421
|
+
const homeSideIsLeft = input.awaySide === "right";
|
|
422
|
+
const mk = (team) => {
|
|
423
|
+
const left = (team === "home") === homeSideIsLeft;
|
|
424
|
+
section(team === "home" ? "tifo-home" : "tifo-away", ...along(left ? 6 : 2, 0.32, 0.68), lowerRing);
|
|
425
|
+
};
|
|
426
|
+
if (crowd.tifo === "home" || crowd.tifo === "both")
|
|
427
|
+
mk("home");
|
|
428
|
+
if (crowd.tifo === "away" || crowd.tifo === "both")
|
|
429
|
+
mk("away");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Round an oval a block is a wedge of the ring, between two rays.
|
|
434
|
+
const corner = Math.atan2(box.h / 2 + off.top + lowerRing.to / 2, box.w / 2 + off.left + lowerRing.to / 2);
|
|
435
|
+
const wedge = (ring, from, to) => {
|
|
436
|
+
const outer = [];
|
|
437
|
+
const inner = [];
|
|
438
|
+
for (let i = 0; i <= 8; i += 1) {
|
|
439
|
+
const a = from + ((to - from) * i) / 8;
|
|
440
|
+
outer.push(rayAt(ring.region, cx, cy, a));
|
|
441
|
+
inner.push(rayAt(ring.hole ?? ring.region, cx, cy, a));
|
|
442
|
+
}
|
|
443
|
+
return [...outer, ...inner.reverse()];
|
|
444
|
+
};
|
|
445
|
+
const place = (kind, a0, a1, only) => {
|
|
446
|
+
for (const ring of only === undefined ? tiers : [only])
|
|
447
|
+
stands.sectors.push({ ring: ring.id, kind, points: wedge(ring, a0, a1) });
|
|
448
|
+
};
|
|
449
|
+
const centre = side === "right" ? 0 : Math.PI;
|
|
450
|
+
if (crowd.awayEnd !== "none")
|
|
451
|
+
place("away", centre - corner * 0.5, centre + corner * 0.5);
|
|
452
|
+
if (crowd.tifo !== undefined) {
|
|
453
|
+
const homeSideIsLeft = input.awaySide === "right";
|
|
454
|
+
const mk = (team) => place(team === "home" ? "tifo-home" : "tifo-away", ((team === "home") === homeSideIsLeft ? Math.PI : 0) - corner * 0.42, ((team === "home") === homeSideIsLeft ? Math.PI : 0) + corner * 0.42, lowerRing);
|
|
455
|
+
if (crowd.tifo === "home" || crowd.tifo === "both")
|
|
456
|
+
mk("home");
|
|
457
|
+
if (crowd.tifo === "away" || crowd.tifo === "both")
|
|
458
|
+
mk("away");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (crowd.gaps && shape !== "grandstand") {
|
|
462
|
+
const bottom = box.y + box.h;
|
|
463
|
+
const mid = box.x + box.w / 2;
|
|
464
|
+
// A tunnel through the lower stands at the middle of the bench side, the camera platform above them, and the benches in the gap.
|
|
465
|
+
stands.gaps.push({ kind: "tunnel", box: [mid - 6, bottom + off.bottom - 1, mid + 6, bottom + off.bottom + lowerRing.to + 1] });
|
|
466
|
+
const media = rings[rings.length - 1];
|
|
467
|
+
if (media !== undefined && media.tier === "upper")
|
|
468
|
+
stands.gaps.push({ kind: "camera", box: [mid - 22, bottom + off.bottom + media.from + 3, mid + 22, bottom + off.bottom + media.from + 13] });
|
|
469
|
+
if (off.bottom - off.top >= 4 || sport === "soccer" || sport === "nfl" || sport === "cfl" || sport === "ifaf" || sport === "rugby" || sport === "gaelic") {
|
|
470
|
+
const benchLen = Math.min(34, box.w * 0.12);
|
|
471
|
+
for (const s of [-1, 1]) {
|
|
472
|
+
const bx = mid + s * (box.w * 0.1 + benchLen / 2 + 4);
|
|
473
|
+
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)] });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
stands.gaps.push({ kind: "media", box: [mid - 55, bottom + off.bottom + 2, mid + 55, bottom + off.bottom + 6] });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return stands;
|
|
480
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Crowd, CrowdStands, Pt } from "./crowd.ts";
|
|
2
|
+
/** A stretch of wall with seats on top of it: its bearings from dead centre (the left field line is -45) and its height in feet. */
|
|
3
|
+
export interface WallSeats {
|
|
4
|
+
from: number;
|
|
5
|
+
to: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/** What a ballpark's stands are laid from. */
|
|
9
|
+
export interface ParkInput {
|
|
10
|
+
/** The fence as points on the field: u across (negative to the left), v out from the back of home plate. */
|
|
11
|
+
fence: readonly Pt[];
|
|
12
|
+
/** How far behind home plate the backstop stands, in feet. */
|
|
13
|
+
backstop: number;
|
|
14
|
+
crowd: Crowd;
|
|
15
|
+
/** The walls that have seats on top of them. */
|
|
16
|
+
seatsOnWalls?: readonly WallSeats[];
|
|
17
|
+
}
|
|
18
|
+
/** The stands of a ballpark, as data. */
|
|
19
|
+
export declare function parkLayout(input: ParkInput): CrowdStands;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { seatsIn } from "./crowd.js";
|
|
2
|
+
const dist = ([x, y]) => Math.hypot(x, y);
|
|
3
|
+
/** The unit normals of a polyline pointing away from home, averaged at each vertex, with the length the offset grows by at a corner (limited to 1.6). */
|
|
4
|
+
function normals(points) {
|
|
5
|
+
const segments = points.slice(1).map(([x, y], i) => {
|
|
6
|
+
const [px, py] = points[i];
|
|
7
|
+
const dx = x - px;
|
|
8
|
+
const dy = y - py;
|
|
9
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
10
|
+
let nx = dy / len;
|
|
11
|
+
let ny = -dx / len;
|
|
12
|
+
if (nx * (x + px) + ny * (y + py) < 0) {
|
|
13
|
+
nx = -nx;
|
|
14
|
+
ny = -ny;
|
|
15
|
+
}
|
|
16
|
+
return [nx, ny];
|
|
17
|
+
});
|
|
18
|
+
return points.map((_, i) => {
|
|
19
|
+
const a = segments[Math.max(0, i - 1)];
|
|
20
|
+
const b = segments[Math.min(segments.length - 1, i)];
|
|
21
|
+
const sx = a[0] + b[0];
|
|
22
|
+
const sy = a[1] + b[1];
|
|
23
|
+
const len = Math.hypot(sx, sy) || 1;
|
|
24
|
+
const n = [sx / len, sy / len];
|
|
25
|
+
return { n, k: 1 / Math.max(0.62, a[0] * n[0] + a[1] * n[1]) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** The polygon of a band along a polyline between two offsets, over a range of vertices. */
|
|
29
|
+
function band(points, nrm, inner, outer, i0 = 0, i1 = points.length - 1) {
|
|
30
|
+
const side = (offset) => {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = i0; i <= i1; i += 1)
|
|
33
|
+
out.push([points[i][0] + nrm[i].n[0] * offset * nrm[i].k, points[i][1] + nrm[i].n[1] * offset * nrm[i].k]);
|
|
34
|
+
return out;
|
|
35
|
+
};
|
|
36
|
+
return [...side(inner), ...side(outer).reverse()];
|
|
37
|
+
}
|
|
38
|
+
/** The aisles of a band polygon made by `band`: lines from every third vertex of its inner edge to the matching one of its outer, so that blocks begin and end on them. */
|
|
39
|
+
function aislesOf(polygon) {
|
|
40
|
+
const n = polygon.length;
|
|
41
|
+
const out = [];
|
|
42
|
+
for (let i = 2; i < n / 2 - 1; i += 3)
|
|
43
|
+
out.push([polygon[i], polygon[n - 1 - i]]);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** The stands of a ballpark, as data. */
|
|
47
|
+
export function parkLayout(input) {
|
|
48
|
+
const { crowd } = input;
|
|
49
|
+
const shape = crowd.shape ?? "horseshoe";
|
|
50
|
+
// The fence in the drawing's own frame (y is minus the distance out), every second point, with its bearing from dead centre.
|
|
51
|
+
const fence = input.fence.filter((_, i) => i % 2 === 0 || i === input.fence.length - 1).map(([u, v]) => [u, -v]);
|
|
52
|
+
const bearing = ([x, y]) => (Math.atan2(x, -y) * 180) / Math.PI;
|
|
53
|
+
const pole = (dist(fence[0]) + dist(fence[fence.length - 1])) / 2;
|
|
54
|
+
const R = input.backstop + 22;
|
|
55
|
+
const S = pole + (shape === "grandstand" ? 0 : 25);
|
|
56
|
+
const q = Math.SQRT1_2;
|
|
57
|
+
// The infield's inner edge from the left pole in to home plate and out to the right pole: along each foul line at a distance that eases from the pole's 14 feet to the curve's
|
|
58
|
+
// radius behind home plate (a smooth step, so that it meets the curve with no corner), round behind home plate on a circle about it.
|
|
59
|
+
const gap = (s) => 14 + (R - 14) * (0.5 + 0.5 * Math.cos(Math.PI * Math.min(1, s / pole)));
|
|
60
|
+
const inner = [];
|
|
61
|
+
const lineSteps = 8;
|
|
62
|
+
for (let i = lineSteps; i >= 0; i -= 1) {
|
|
63
|
+
const s = (S * i) / lineSteps;
|
|
64
|
+
inner.push([-q * s - q * gap(s), -q * s + q * gap(s)]);
|
|
65
|
+
}
|
|
66
|
+
const arcSteps = 8;
|
|
67
|
+
for (let k = 1; k < arcSteps; k += 1) {
|
|
68
|
+
const a = ((135 - (90 * k) / arcSteps) * Math.PI) / 180;
|
|
69
|
+
inner.push([R * Math.cos(a), R * Math.sin(a)]);
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i <= lineSteps; i += 1) {
|
|
72
|
+
const s = (S * i) / lineSteps;
|
|
73
|
+
inner.push([q * s + q * gap(s), -q * s + q * gap(s)]);
|
|
74
|
+
}
|
|
75
|
+
const nI = normals(inner);
|
|
76
|
+
const rings = [];
|
|
77
|
+
const add = (id, tier, pieces, from, to, height, fill) => {
|
|
78
|
+
const [first, ...rest] = pieces;
|
|
79
|
+
rings.push({
|
|
80
|
+
id,
|
|
81
|
+
tier,
|
|
82
|
+
region: { kind: "poly", points: first },
|
|
83
|
+
...(rest.length > 0 ? { more: rest.map((points) => ({ kind: "poly", points })) } : {}),
|
|
84
|
+
hole: null,
|
|
85
|
+
from,
|
|
86
|
+
to,
|
|
87
|
+
height,
|
|
88
|
+
rake: Math.round((Math.atan2(height[1] - height[0], Math.max(1, to - from)) * 180) / Math.PI * 10) / 10,
|
|
89
|
+
fill,
|
|
90
|
+
seats: pieces.reduce((n, points) => n + seatsIn({ kind: "poly", points }, null), 0),
|
|
91
|
+
aisles: pieces.flatMap((points) => aislesOf(points)),
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const lowerDepth = 60;
|
|
95
|
+
const walk = 7;
|
|
96
|
+
const upperDepth = 56;
|
|
97
|
+
add("lower", "lower", [band(inner, nI, 0, lowerDepth)], 0, lowerDepth, [4, 40], 1);
|
|
98
|
+
add("upper", "upper", [band(inner, nI, lowerDepth + walk, lowerDepth + walk + upperDepth)], lowerDepth + walk, lowerDepth + walk + upperDepth, [52, 104], 0.88);
|
|
99
|
+
const concourses = [{ region: { kind: "poly", points: band(inner, nI, lowerDepth - 0.5, lowerDepth + walk + 0.5) }, hole: null }];
|
|
100
|
+
// The outfield: bleachers right behind the fence, radially from home plate, but for a gap at the batter's eye in the horseshoe; and, where the park has seats on top of a wall,
|
|
101
|
+
// a band of them along the wall at its height.
|
|
102
|
+
const wall = 12;
|
|
103
|
+
const depth = 54;
|
|
104
|
+
if (shape !== "grandstand") {
|
|
105
|
+
const nF = fence.map((p) => ({ n: [p[0] / (dist(p) || 1), p[1] / (dist(p) || 1)], k: 1 }));
|
|
106
|
+
const eye = (i) => Math.abs(fence[i][0]) < pole * 0.07 && shape === "horseshoe";
|
|
107
|
+
const runs = [];
|
|
108
|
+
let start = 0;
|
|
109
|
+
for (let i = 0; i <= fence.length; i += 1) {
|
|
110
|
+
if (i === fence.length || eye(i)) {
|
|
111
|
+
if (i - 1 > start)
|
|
112
|
+
runs.push([start, i - 1]);
|
|
113
|
+
start = i + 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const pieces = runs.map(([a, b]) => band(fence, nF, wall, wall + depth, a, b));
|
|
117
|
+
if (pieces.length > 0)
|
|
118
|
+
add("bleachers", "bleachers", pieces, wall, wall + depth, [4, 30], 0.9);
|
|
119
|
+
for (const w of input.seatsOnWalls ?? []) {
|
|
120
|
+
const on = fence.map((p, i) => ({ i, b: bearing(p) })).filter(({ i, b }) => b >= w.from - 0.01 && b <= w.to + 0.01 && !eye(i));
|
|
121
|
+
if (on.length > 2)
|
|
122
|
+
add("wall-seats", "wall", [band(fence, nF, 0, 8, on[0].i, on[on.length - 1].i)], 0, 8, [w.height, w.height + 5], 0.95);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// The visitors: a block along the third-base line, from aisle to aisle, in the two tiers; and a card display round behind home plate.
|
|
126
|
+
const sectors = [];
|
|
127
|
+
if (crowd.awayEnd !== "none") {
|
|
128
|
+
const awayLeft = crowd.awayEnd !== "right";
|
|
129
|
+
const a0 = awayLeft ? 2 : 17;
|
|
130
|
+
const a1 = awayLeft ? 8 : 23;
|
|
131
|
+
sectors.push({ ring: "lower", kind: "away", points: band(inner, nI, 0, lowerDepth, a0, a1) });
|
|
132
|
+
sectors.push({ ring: "upper", kind: "away", points: band(inner, nI, lowerDepth + walk, lowerDepth + walk + upperDepth, a0, a1) });
|
|
133
|
+
}
|
|
134
|
+
if (crowd.tifo !== undefined)
|
|
135
|
+
sectors.push({ ring: "lower", kind: crowd.tifo === "away" ? "tifo-away" : "tifo-home", points: band(inner, nI, 0, lowerDepth, 11, 14) });
|
|
136
|
+
const gaps = [];
|
|
137
|
+
if (crowd.gaps) {
|
|
138
|
+
gaps.push({ kind: "tunnel", box: [-8, R - 1, 8, R + lowerDepth + 1] });
|
|
139
|
+
gaps.push({ kind: "camera", box: [-20, R + lowerDepth + walk + 8, 20, R + lowerDepth + walk + 18] });
|
|
140
|
+
}
|
|
141
|
+
// The field's box and how far the stands reach beyond it.
|
|
142
|
+
const us = input.fence.map(([u]) => u);
|
|
143
|
+
const vs = input.fence.map(([, v]) => v);
|
|
144
|
+
const box = { x: Math.min(...us), y: -Math.max(...vs), w: Math.max(...us) - Math.min(...us), h: Math.max(...vs) + input.backstop };
|
|
145
|
+
const all = rings.flatMap((r) => [r.region, ...(r.more ?? [])].flatMap((o) => (o.kind === "poly" ? o.points : [])));
|
|
146
|
+
const reach = {
|
|
147
|
+
left: Math.max(0, box.x - Math.min(...all.map(([x]) => x))),
|
|
148
|
+
right: Math.max(0, Math.max(...all.map(([x]) => x)) - (box.x + box.w)),
|
|
149
|
+
top: Math.max(0, box.y - Math.min(...all.map(([, y]) => y))),
|
|
150
|
+
bottom: Math.max(0, Math.max(...all.map(([, y]) => y)) - (box.y + box.h)),
|
|
151
|
+
};
|
|
152
|
+
return { sport: "mlb", shape, box, rings, covers: [], concourses, sectors, gaps, reach, seats: rings.reduce((n, r) => n + r.seats, 0) };
|
|
153
|
+
}
|