@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.5.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 +75 -0
- package/NOTICE.md +4 -0
- package/README.md +71 -16
- package/dist/annotate.js +1 -1
- 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 +4 -2
- package/dist/atlas-entry.js +3 -2
- package/dist/atlas.d.ts +83 -7
- package/dist/atlas.js +108 -8
- 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.curling.heat.d.ts +24 -0
- package/dist/charts.curling.heat.js +116 -0
- package/dist/charts.curling.js +14 -3
- 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.types.d.ts +53 -1
- 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 +154 -0
- package/dist/crowd.draw.d.ts +17 -0
- package/dist/crowd.draw.js +198 -0
- package/dist/crowd.js +465 -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 +318 -35
- package/dist/element-entry.d.ts +1 -1
- package/dist/element-entry.js +1 -1
- package/dist/element.d.ts +13 -2
- package/dist/element.js +77 -3
- package/dist/features.js +131 -0
- package/dist/fields.kit.d.ts +2 -2
- package/dist/fields.types.d.ts +18 -4
- package/dist/frames.providers.js +63 -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/hardwood.d.ts +31 -0
- package/dist/hardwood.js +157 -0
- package/dist/heat.js +4 -3
- package/dist/heat.types.d.ts +5 -0
- package/dist/heat.zones.d.ts +2 -2
- package/dist/heat.zones.js +154 -6
- package/dist/homeside.js +17 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +10 -1
- package/dist/layers.d.ts +1 -1
- package/dist/layers.env.d.ts +3 -1
- package/dist/layers.env.js +4 -2
- package/dist/layers.js +4 -4
- package/dist/leagues/athletics.d.ts +63 -0
- package/dist/leagues/athletics.js +87 -0
- package/dist/leagues/courts.d.ts +269 -4
- package/dist/leagues/courts.js +155 -1
- package/dist/leagues/skating.d.ts +145 -0
- package/dist/leagues/skating.js +73 -0
- package/dist/leagues/sources.js +20 -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 +14 -4
- package/dist/leagues.js +146 -5
- package/dist/look.d.ts +3 -3
- package/dist/look.js +39 -6
- package/dist/outline.d.ts +1 -1
- package/dist/outline.js +30 -2
- 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 +207 -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/pool.d.ts +176 -0
- package/dist/pool.figures.d.ts +304 -0
- package/dist/pool.figures.js +144 -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 +257 -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 +47 -3
- package/dist/replay-entry.d.ts +3 -0
- package/dist/replay-entry.js +2 -0
- package/dist/restyle.js +1 -1
- package/dist/score.js +70 -5
- package/dist/sheet.d.ts +3 -0
- package/dist/sheet.js +7 -3
- 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 +296 -0
- package/dist/strings.js +328 -11
- package/dist/style.d.ts +2 -0
- package/dist/style.js +11 -1
- 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 +15 -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 +43 -21
- 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/hockey.js +1 -0
- 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 +44 -0
- package/dist/surfaces/venue.d.ts +15 -0
- package/dist/surfaces/venue.js +95 -0
- package/dist/tenants.d.ts +77 -0
- package/dist/tenants.data.d.ts +8 -0
- package/dist/tenants.data.js +945 -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 +105 -0
- package/dist/turn.camera.js +168 -0
- package/dist/turn.dom.d.ts +40 -0
- package/dist/turn.dom.js +351 -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 +55 -0
- package/dist/types.d.ts +183 -18
- package/dist/units.d.ts +1 -1
- package/dist/units.js +15 -1
- package/dist/uprights.board.d.ts +17 -0
- package/dist/uprights.board.js +59 -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 +160 -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/package.json +19 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { FEET_PER_METRE, fenceAt, footballLength } from "./geometry.js";
|
|
2
|
+
import { fenceOutline } from "./surfaces/baseball.js";
|
|
3
|
+
import { flagPole, goalCage, postPair } from "./uprights.goal.js";
|
|
4
|
+
import { mesh, n, pole, threads, wall } from "./turn.kit.js";
|
|
5
|
+
const M = FEET_PER_METRE;
|
|
6
|
+
/**
|
|
7
|
+
* The crossbar of a football post is 10 ft over the ground (NFL 2026 Rule 1-3-1: "the top face of which is 10 feet above the ground"; CFL 2026 Rule 1-1-1:
|
|
8
|
+
* "a height of 10 feet"; NCAA 2025 Rule 1-2-5-a: "the top of which is 10 feet above the ground"). The uprights reach 35 ft over the bar in the NFL (Rule 1-3-2: 45 ft
|
|
9
|
+
* in all), 40 ft above the ground in the CFL (Rule 1-1-1: "extend 40 feet above ground level"), and at least 30 ft above the ground in the NCAA (Rule 1-2-5-a), the
|
|
10
|
+
* figure drawn for every other league.
|
|
11
|
+
*/
|
|
12
|
+
export const FOOTBALL_CROSSBAR = 10;
|
|
13
|
+
/** The pylon is 18 in high: the NFL's Rule 1-2-3 names pylons but not their size; BAFA's Appendix D (14) recommends 4 in by 4 in and 18 in. */
|
|
14
|
+
export const PYLON_HEIGHT = 1.5;
|
|
15
|
+
/** A football field's uprights: the post at each end and the eight pylons. `shape` is the post of the season, `top` the height of the uprights above the ground. */
|
|
16
|
+
export function footballUprights(spec, shape, top, thin) {
|
|
17
|
+
const L = footballLength(spec);
|
|
18
|
+
const W = spec.width;
|
|
19
|
+
const e = spec.endZone;
|
|
20
|
+
const items = [];
|
|
21
|
+
const post = Math.max(thin * 2.6, 0.8);
|
|
22
|
+
for (const end of ["left", "right"]) {
|
|
23
|
+
const left = end === "left";
|
|
24
|
+
const x = spec.posts === "end-line" ? (left ? 0 : L) : left ? e : L - e;
|
|
25
|
+
items.push(postPair({ id: `posts-${end}`, x, y: W / 2, out: left ? -1 : 1, gap: spec.postWidth ?? 18.5, crossbar: FOOTBALL_CROSSBAR, top, post, support: shape === "slingshot" ? "slingshot" : "centre", stem: 4, cls: "u-yellow" }));
|
|
26
|
+
}
|
|
27
|
+
if (spec.pylons !== false) {
|
|
28
|
+
// Eight pylons: at the four corners of the goal lines and two on each end line (NFL Rule 1-2-3), as the drawing puts them.
|
|
29
|
+
const corners = [[e, 0], [e, W], [L - e, 0], [L - e, W], [0, 0], [0, W], [L, 0], [L, W]];
|
|
30
|
+
const side = Math.max(thin * 1.6, 0.33);
|
|
31
|
+
items.push({ id: "pylons", kind: "pylons", at: [L / 2, W / 2], height: PYLON_HEIGHT, planes: corners.flatMap(([x, y], i) => pole(`pylon-${i}`, "pylon", x, y, 0, PYLON_HEIGHT, side, "u-orange")) });
|
|
32
|
+
}
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
/** A soccer pitch's uprights: both goals and, unless the options turn them off, the four corner flags. */
|
|
36
|
+
export function soccerUprights(spec, flags, thin) {
|
|
37
|
+
const L = spec.length * M;
|
|
38
|
+
const W = spec.width * M;
|
|
39
|
+
const items = [];
|
|
40
|
+
const depth = spec.goalDepth * M;
|
|
41
|
+
for (const end of ["left", "right"]) {
|
|
42
|
+
const left = end === "left";
|
|
43
|
+
items.push(goalCage({ id: `goal-${end}`, x: left ? 0 : L, y: W / 2, out: left ? -1 : 1, width: spec.goalWidth * M, height: spec.goalHeight * M, depth, top: depth * 0.55, post: Math.max(thin * 1.3, 0.35), cell: Math.max(thin * 6, 0.9), tone: "white" }));
|
|
44
|
+
}
|
|
45
|
+
if (flags) {
|
|
46
|
+
// A corner flagpost is at least 1.5 m (5 ft) high (IFAB Laws of the Game 2025/26, Law 1, section 8); the goal is 7.32 m wide and 2.44 m to the crossbar's lower edge (section 10).
|
|
47
|
+
const planes = [[0, 0], [L, 0], [0, W], [L, W]].flatMap(([x, y], i) => flagPole(`flag-${i}`, x, y, 1.5 * M, Math.max(thin * 0.9, 0.15)));
|
|
48
|
+
items.push({ id: "flags", kind: "flags", at: [L / 2, W / 2], height: 1.5 * M, planes });
|
|
49
|
+
}
|
|
50
|
+
return items;
|
|
51
|
+
}
|
|
52
|
+
/** The dots of a plan with these ids: the posts, flags and stumps the plan places. */
|
|
53
|
+
const dots = (plan, ...ids) => plan.shapes.flatMap((s) => (s.kind === "dot" && ids.includes(s.id) ? [{ x: s.cx, y: s.cy }] : []));
|
|
54
|
+
/** The pairs of posts a plan places by their x: two dots at one x, the uprights of one end. */
|
|
55
|
+
function pairs(points) {
|
|
56
|
+
const by = new Map();
|
|
57
|
+
for (const p of points)
|
|
58
|
+
by.set(n(p.x), [...(by.get(n(p.x)) ?? []), p]);
|
|
59
|
+
return [...by.values()].filter((list) => list.length === 2).map(([a, b]) => ({ x: a.x, y: (a.y + b.y) / 2, gap: Math.abs(a.y - b.y) }));
|
|
60
|
+
}
|
|
61
|
+
/** The rugby posts, Gaelic goal posts, Australian football posts and cricket stumps; a goal with a net for the sports that have one. */
|
|
62
|
+
export function fieldUprights(plan, thin) {
|
|
63
|
+
const items = [];
|
|
64
|
+
const post = Math.max(thin * 1.4, 0.3);
|
|
65
|
+
const midX = plan.length / 2;
|
|
66
|
+
const outOf = (x) => (x < midX ? -1 : 1);
|
|
67
|
+
const flags = (points, height, id) => {
|
|
68
|
+
if (points.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
items.push({ id, kind: "flags", at: [midX, plan.width / 2], height, planes: points.flatMap((p, i) => flagPole(`${id}-${i}`, p.x, p.y, height, Math.max(thin * 0.9, 0.15))) });
|
|
71
|
+
};
|
|
72
|
+
switch (plan.sport) {
|
|
73
|
+
case "rugby": {
|
|
74
|
+
// Union (World Rugby 2026 Law 1, goal posts and crossbar): the crossbar's top edge 3 m over the ground, a minimum post height of 3.4 m, which the Law's diagram
|
|
75
|
+
// draws over the bar and is drawn so (6.4 m); the flag posts at least 1.2 m (Law 1, flag posts 8). League (NRL Section 18, IRL Section 1): posts 16 m high.
|
|
76
|
+
const league = plan.league === "nrl" || plan.league === "irl";
|
|
77
|
+
for (const p of pairs(dots(plan, "goal-post")))
|
|
78
|
+
items.push(postPair({ id: p.x < midX ? "posts-left" : "posts-right", x: p.x, y: p.y, out: outOf(p.x), gap: p.gap, crossbar: 3 * M, top: league ? 16 * M : 3 * M + 3.4 * M, post, support: "none", cls: "u-white" }));
|
|
79
|
+
flags(dots(plan, "flag-post", "corner-post"), 1.2 * M, "flags");
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "gaelic": {
|
|
83
|
+
// GAA Official Guide Part 2 (2026) Rule 1.3 (i), (ii): posts not less than 7 m above the ground and 6.5 m apart, the crossbar 2.5 m above the ground. Rule 1.7 gives
|
|
84
|
+
// the flags no height; 1.5 m is ours.
|
|
85
|
+
for (const p of pairs(dots(plan, "goalpost")))
|
|
86
|
+
items.push(postPair({ id: p.x < midX ? "posts-left" : "posts-right", x: p.x, y: p.y, out: outOf(p.x), gap: p.gap, crossbar: 2.5 * M, top: 7 * M, post, support: "none", cls: "u-white" }));
|
|
87
|
+
flags(dots(plan, "flag"), 1.5 * M, "flags");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "afl": {
|
|
91
|
+
// Laws of Australian Football 2026, Law 3.3.1: goal posts 6 to 15 m high and 6.4 m apart, behind posts 3 to 10 m high, 6.4 m either side of them: the least is drawn.
|
|
92
|
+
const goals = dots(plan, "goal-post");
|
|
93
|
+
const behinds = dots(plan, "behind-post");
|
|
94
|
+
for (const end of ["left", "right"]) {
|
|
95
|
+
const left = end === "left";
|
|
96
|
+
const mine = (p) => (p.x < midX) === left;
|
|
97
|
+
const planes = [
|
|
98
|
+
...goals.filter(mine).flatMap((p, i) => pole(`goal-post-${i}`, "post", p.x, p.y, 0, 6 * M, post, "u-white")),
|
|
99
|
+
...behinds.filter(mine).flatMap((p, i) => pole(`behind-post-${i}`, "post", p.x, p.y, 0, 3 * M, post, "u-white")),
|
|
100
|
+
];
|
|
101
|
+
items.push({ id: `posts-${end}`, kind: "goalposts", at: [left ? 0 : plan.length, plan.width / 2], height: 6 * M, low: 3 * M, planes });
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "cricket": {
|
|
106
|
+
// MCC Laws of Cricket, Law 8.1 and 8.2: three stumps across a wicket 9 in wide, their tops 28 in over the playing surface.
|
|
107
|
+
for (const s of plan.shapes) {
|
|
108
|
+
if (s.kind !== "bar" || s.id !== "wicket")
|
|
109
|
+
continue;
|
|
110
|
+
const x = (s.x0 + s.x1) / 2;
|
|
111
|
+
const ys = [s.y0, (s.y0 + s.y1) / 2, s.y1];
|
|
112
|
+
items.push({ id: x < midX ? "stumps-left" : "stumps-right", kind: "stumps", at: [x, (s.y0 + s.y1) / 2], height: 28 / 12, planes: ys.flatMap((y, i) => pole(`stump-${i}`, "stump", x, y, 0, 28 / 12, Math.max(thin * 0.9, 0.12), "u-wood")) });
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "lacrosse":
|
|
117
|
+
case "fieldhockey":
|
|
118
|
+
case "handball": {
|
|
119
|
+
// The goal: World Lacrosse 6 ft apart and high on a field, 4 ft high in a box; FIH 2.14 m to the crossbar's lower edge; IHF interior height 2 m (docs/sources.md).
|
|
120
|
+
const box = plan.league === "nll" || plan.league === "world-box";
|
|
121
|
+
for (const g of plan.goals) {
|
|
122
|
+
const height = plan.sport === "lacrosse" ? (box ? 4 : 6) : plan.sport === "handball" ? 2 * M : 2.14 * M;
|
|
123
|
+
items.push(goalCage({ id: `goal-${g.end}`, x: g.x, y: g.y, out: g.out, width: g.width, height, depth: g.depth, top: g.depth * (plan.sport === "lacrosse" ? 0.7 : 0.85), post: Math.max(thin * 1.3, 0.3), cell: Math.max(thin * 6, 0.5), tone: "white" }));
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
}
|
|
129
|
+
return items;
|
|
130
|
+
}
|
|
131
|
+
/** The fence's height where no wall is listed: 8 feet. */
|
|
132
|
+
export const FENCE_HEIGHT = 8;
|
|
133
|
+
/** How high the wall stands at an angle from dead centre, the wall that names it or the usual 8 feet. */
|
|
134
|
+
export const wallHeightAt = (walls, angle) => walls.find((w) => angle >= Math.min(w.from, w.to) && angle <= Math.max(w.from, w.to))?.height_ft ?? FENCE_HEIGHT;
|
|
135
|
+
/** The planes of a fence: one standing wall over each stretch of its line, as high as the wall there. */
|
|
136
|
+
export function fencePlanes(points, walls, thin) {
|
|
137
|
+
const line = fenceOutline(points, 3).map(([u, v]) => [u, -v]);
|
|
138
|
+
const planes = [];
|
|
139
|
+
for (let i = 0; i < line.length - 1; i += 1) {
|
|
140
|
+
const [x0, y0] = line[i];
|
|
141
|
+
const [x1, y1] = line[i + 1];
|
|
142
|
+
const run = Math.hypot(x1 - x0, y1 - y0);
|
|
143
|
+
if (run < 1e-6)
|
|
144
|
+
continue;
|
|
145
|
+
const angle = Math.atan2((x0 + x1) / 2, -(y0 + y1) / 2) * (180 / Math.PI);
|
|
146
|
+
const height = wallHeightAt(walls, angle);
|
|
147
|
+
planes.push(wall(`fence-${i}`, "wall", x0, y0, x1, y1, 0, height, `<rect width="${n(run)}" height="${n(height)}" class="u-wall"/><rect width="${n(run)}" height="${n(Math.max(thin * 0.8, 0.2))}" class="u-cap"/>`));
|
|
148
|
+
}
|
|
149
|
+
return planes;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A ballpark's uprights: the fence, the two foul poles where the foul lines meet it, and the backstop behind home plate. Official Baseball Rules 2025, 2.01, gives no height
|
|
153
|
+
* for a fence or a foul pole, and only recommends the backstop 60 ft or more from home base: the 8 ft of the fence where no wall is listed, the poles' 30 ft and the
|
|
154
|
+
* screen's 70 ft by 20 ft are ours.
|
|
155
|
+
*/
|
|
156
|
+
export function ballparkUprights(points, walls, thin) {
|
|
157
|
+
const items = [];
|
|
158
|
+
const fence = fencePlanes(points, walls, thin);
|
|
159
|
+
const tops = fence.map((p) => -p.v[2]);
|
|
160
|
+
items.push({ id: "fence", kind: "fence", at: [0, -400], height: Math.min(...tops), low: Math.max(...tops), planes: fence });
|
|
161
|
+
const poles = [];
|
|
162
|
+
// The foul poles stand where the foul lines meet the fence, 45 degrees either side of dead centre.
|
|
163
|
+
for (const angle of [-45, 45]) {
|
|
164
|
+
const d = fenceAt(points, angle);
|
|
165
|
+
const t = (angle * Math.PI) / 180;
|
|
166
|
+
poles.push(...pole(`foul-pole-${angle < 0 ? "left" : "right"}`, "pole", d * Math.sin(t), -d * Math.cos(t), 0, 30, Math.max(thin * 1.6, 0.5), "u-yellow"));
|
|
167
|
+
}
|
|
168
|
+
items.push({ id: "foul-poles", kind: "foul-poles", at: [0, -330], height: 30, planes: poles });
|
|
169
|
+
// The backstop: the rules put it at least 60 feet behind home plate. A screen 70 feet wide and 20 high.
|
|
170
|
+
items.push({ id: "backstop", kind: "backstop", at: [0, 60], height: 20, planes: [wall("backstop", "screen", -35, 60, 35, 60, 0, 20, `<rect width="70" height="20" class="u-netfill"/>${threads(mesh(70, 20, 3), 3)}`)] });
|
|
171
|
+
return items;
|
|
172
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Plane, type Upright } from "./turn.kit.ts";
|
|
2
|
+
/** A goal with a net: the mouth on the line x, the net behind it. */
|
|
3
|
+
export interface GoalCage {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Where the middle of the mouth stands on its goal line, and which way the net lies behind it: +1 to larger x, -1 to smaller. */
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
out: 1 | -1;
|
|
9
|
+
/** The mouth: its width across the surface and its height. */
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
/** How far the net reaches behind the line at the ground, and at the top of the back. */
|
|
13
|
+
depth: number;
|
|
14
|
+
top: number;
|
|
15
|
+
/** The frame's thickness, and the mesh's cell, in feet. */
|
|
16
|
+
post: number;
|
|
17
|
+
cell: number;
|
|
18
|
+
/** The frame's paint: white, or red as a hockey goal's. */
|
|
19
|
+
tone: "white" | "red";
|
|
20
|
+
}
|
|
21
|
+
/** A goal: its mouth as three strips (two posts and the crossbar), and its net as four planes, framed. */
|
|
22
|
+
export declare function goalCage(g: GoalCage): Upright;
|
|
23
|
+
/** A pair of uprights with a crossbar between them, standing on the line x (a rugby, Gaelic football or football post). */
|
|
24
|
+
export interface PostPair {
|
|
25
|
+
id: string;
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
/** Which way is behind the goal line, for a post's support. */
|
|
29
|
+
out: 1 | -1;
|
|
30
|
+
/** The space between the two uprights, to the middle of each. */
|
|
31
|
+
gap: number;
|
|
32
|
+
/** The crossbar's height, and the uprights' top, above the surface. */
|
|
33
|
+
crossbar: number;
|
|
34
|
+
top: number;
|
|
35
|
+
/** The strips' thickness. */
|
|
36
|
+
post: number;
|
|
37
|
+
/** `slingshot`: one support behind the goal, bent to carry the crossbar (a football post); `centre`: one pole straight under the crossbar's middle; `none`: the uprights alone. */
|
|
38
|
+
support: "slingshot" | "centre" | "none";
|
|
39
|
+
/** How far behind the goal line a slingshot's support stands. */
|
|
40
|
+
stem?: number;
|
|
41
|
+
/** The class of the paint: `u-yellow` for a football post, `u-white` for the others. */
|
|
42
|
+
cls: string;
|
|
43
|
+
}
|
|
44
|
+
/** A pair of uprights, a crossbar, and the support under them. */
|
|
45
|
+
export declare function postPair(p: PostPair): Upright;
|
|
46
|
+
/** A flag on a pole, the flag lying along x. */
|
|
47
|
+
export declare function flagPole(id: string, x: number, y: number, height: number, post: number, cls?: string): Plane[];
|
|
48
|
+
/** A short rectangle lying flat, such as the foot of a stanchion or the shadow of a table: used where a pole needs a base. */
|
|
49
|
+
export declare function foot(id: string, x: number, y: number, w: number, d: number): Plane;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The parts that more than one sport has: a goal with its net behind it (soccer, hockey, handball, field hockey, lacrosse), a pair of upright posts with a
|
|
2
|
+
// crossbar (football, rugby, Gaelic football), the posts of Australian football, a flag on a pole. Every number is in feet, in the drawing's own frame.
|
|
3
|
+
import { bar, flat, mesh, n, plane, pole, polygon, threads, wall } from "./turn.kit.js";
|
|
4
|
+
/** The mesh over the side of a net, a trapezoid: the front edge is straight up, the back leans out from `top` at the head to `depth` at the ground. */
|
|
5
|
+
function sideMesh(depth, top, height, cell) {
|
|
6
|
+
const wide = Math.max(1, Math.round(depth / cell));
|
|
7
|
+
const high = Math.max(1, Math.round(height / cell));
|
|
8
|
+
let d = "";
|
|
9
|
+
for (let i = 1; i < wide; i += 1)
|
|
10
|
+
d += `M${n((top * i) / wide)} 0L${n((depth * i) / wide)} ${n(height)}`;
|
|
11
|
+
for (let j = 1; j < high; j += 1) {
|
|
12
|
+
const b = (height * j) / high;
|
|
13
|
+
d += `M0 ${n(b)}H${n(top + ((depth - top) * j) / high)}`;
|
|
14
|
+
}
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
/** A goal: its mouth as three strips (two posts and the crossbar), and its net as four planes, framed. */
|
|
18
|
+
export function goalCage(g) {
|
|
19
|
+
const { id, x, y, out, width, height, depth, top, post, cell } = g;
|
|
20
|
+
const half = width / 2;
|
|
21
|
+
const cls = g.tone === "red" ? "u-red" : "u-white";
|
|
22
|
+
const line = `class="u-frame u-frame-${g.tone}" stroke-width="${n(post * 0.7)}" fill="none" stroke-linejoin="round"`;
|
|
23
|
+
const planes = [];
|
|
24
|
+
// The mouth.
|
|
25
|
+
planes.push(...pole(`${id}-post-a`, "post", x, y - half, 0, height, post, cls), ...pole(`${id}-post-b`, "post", x, y + half, 0, height, post, cls));
|
|
26
|
+
planes.push(...bar(`${id}-bar`, "post", [x, y - half, height], [x, y + half, height], post, cls));
|
|
27
|
+
// The two sides: a trapezoid hanging from the head, leaning out to the back.
|
|
28
|
+
for (const [tag, ys] of [["a", y - half], ["b", y + half]]) {
|
|
29
|
+
const shape = polygon([[0, 0], [top, 0], [depth, height], [0, height]]);
|
|
30
|
+
planes.push(plane(`${id}-side-${tag}`, "goal", [x, ys, height], [out * depth, 0, 0], [0, 0, -height], `<path d="${shape}" class="u-netfill"/>${threads(sideMesh(depth, top, height, cell), cell)}<path d="${shape}" ${line}/>`));
|
|
31
|
+
}
|
|
32
|
+
// The back leans from the head's depth out to the ground's.
|
|
33
|
+
const lean = out * (depth - top);
|
|
34
|
+
planes.push(plane(`${id}-back`, "goal", [x + out * top, y - half, height], [0, width, 0], [lean, 0, -height], `<rect width="${n(width)}" height="${n(Math.hypot(lean, height))}" class="u-netfill"/>${threads(mesh(width, Math.hypot(lean, height), cell), cell)}<rect width="${n(width)}" height="${n(Math.hypot(lean, height))}" ${line}/>`));
|
|
35
|
+
// The roof.
|
|
36
|
+
planes.push(plane(`${id}-roof`, "goal", [x, y - half, height], [0, width, 0], [out * top, 0, 0], `<rect width="${n(width)}" height="${n(top)}" class="u-netfill"/>${threads(mesh(width, top, cell), cell)}<rect width="${n(width)}" height="${n(top)}" ${line}/>`));
|
|
37
|
+
return { id, kind: "goal", at: [x, y], height, planes };
|
|
38
|
+
}
|
|
39
|
+
/** A pair of uprights, a crossbar, and the support under them. */
|
|
40
|
+
export function postPair(p) {
|
|
41
|
+
const { id, x, y, gap, crossbar, top, post, cls } = p;
|
|
42
|
+
const half = gap / 2;
|
|
43
|
+
// A slingshot's uprights begin at the crossbar; the others stand on the ground.
|
|
44
|
+
const from = p.support === "slingshot" ? crossbar : 0;
|
|
45
|
+
const planes = [
|
|
46
|
+
...pole(`${id}-upright-a`, "post", x, y - half, from, top, post, cls),
|
|
47
|
+
...pole(`${id}-upright-b`, "post", x, y + half, from, top, post, cls),
|
|
48
|
+
...bar(`${id}-bar`, "post", [x, y - half, crossbar], [x, y + half, crossbar], post, cls),
|
|
49
|
+
];
|
|
50
|
+
if (p.support === "slingshot") {
|
|
51
|
+
const sx = x + p.out * (p.stem ?? 4);
|
|
52
|
+
planes.push(...pole(`${id}-stem`, "stanchion", sx, y, 0, crossbar, post * 1.4, cls), ...bar(`${id}-neck`, "stanchion", [sx, y, crossbar], [x, y, crossbar], post, cls));
|
|
53
|
+
}
|
|
54
|
+
else if (p.support === "centre")
|
|
55
|
+
planes.push(...pole(`${id}-stem`, "stanchion", x, y, 0, crossbar, post * 1.4, cls));
|
|
56
|
+
return { id, kind: "goalposts", at: [x, y], height: top, low: crossbar, planes };
|
|
57
|
+
}
|
|
58
|
+
/** A flag on a pole, the flag lying along x. */
|
|
59
|
+
export function flagPole(id, x, y, height, post, cls = "u-white") {
|
|
60
|
+
const flag = Math.max(height * 0.28, post * 3);
|
|
61
|
+
return [
|
|
62
|
+
...pole(id, "pole", x, y, 0, height, post, cls),
|
|
63
|
+
wall(`${id}-flag`, "flag", x, y, x + flag, y, height - flag * 0.7, height, `<path d="${polygon([[0, 0], [flag, flag * 0.35], [0, flag * 0.7]])}" class="u-flag"/>`),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/** A short rectangle lying flat, such as the foot of a stanchion or the shadow of a table: used where a pole needs a base. */
|
|
67
|
+
export function foot(id, x, y, w, d) {
|
|
68
|
+
return flat(id, "leg", x - w / 2, y - d / 2, x + w / 2, y + d / 2, 0.01, `<rect width="${n(w)}" height="${n(d)}" class="u-foot"/>`);
|
|
69
|
+
}
|
package/dist/uprights.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// What stands on a surface, in 3D: `uprightsFor` reads the options as `drawKyogijo` does (the sport, the league, the season, the park) and says what stands on the
|
|
2
|
+
// surface, where and how high, as planes in the drawing's own frame (x along the surface and y across it, in feet, z the height above it). Pure: no page, no
|
|
3
|
+
// stylesheet. `describeUprights` says the same in words, for a reader who cannot see it.
|
|
4
|
+
import { courtFor, courtGeometry, isNetSport } from "./court.js";
|
|
5
|
+
import { drawingId, feetFence, lengthUnit } from "./common.js";
|
|
6
|
+
import { drawKyogijo, pitchOf } from "./draw.js";
|
|
7
|
+
import { rulesFor, footballIn } from "./eras.js";
|
|
8
|
+
import { fieldPlan, isFieldSport } from "./fields.js";
|
|
9
|
+
import { footballLength } from "./geometry.js";
|
|
10
|
+
import { diamondFence } from "./leagues/baseball.js";
|
|
11
|
+
import { NBA_COURT } from "./leagues/basketball.js";
|
|
12
|
+
import { NHL_RINK } from "./leagues/hockey.js";
|
|
13
|
+
import { chooseLeague, courtOf, diamondOf, fieldOf, rinkOf } from "./leagues.js";
|
|
14
|
+
import { fenceFor, parkById, parkWallsIn } from "./parks.js";
|
|
15
|
+
import { fenceOutline } from "./surfaces/baseball.js";
|
|
16
|
+
import { KYOGIJO_STRINGS, fill } from "./strings.js";
|
|
17
|
+
import { soccerSpec } from "./surfaces/soccer-spec.js";
|
|
18
|
+
import { thinnest } from "./turn.kit.js";
|
|
19
|
+
import { scoreboard } from "./uprights.board.js";
|
|
20
|
+
import { ballparkUprights, fieldUprights, footballUprights, soccerUprights } from "./uprights.fields.js";
|
|
21
|
+
import { netUprights } from "./uprights.net.js";
|
|
22
|
+
import { isPoolSport, poolPlan, feet as poolFeet } from "./pool.js";
|
|
23
|
+
import { poolUprights } from "./uprights.pools.js";
|
|
24
|
+
import { athleticsUprights, skatingUprights } from "./uprights.track.js";
|
|
25
|
+
import { courtUprights, rinkUprights } from "./uprights.rinks.js";
|
|
26
|
+
import { convertLength } from "./units.js";
|
|
27
|
+
import { sheetFor } from "./sheet.js";
|
|
28
|
+
/** All the planes of all the things. */
|
|
29
|
+
export const planesOf = (u) => u.items.flatMap((item) => item.planes);
|
|
30
|
+
/** The unit each sport's rulebook gives its heights in, when the options name none. */
|
|
31
|
+
const RULEBOOK_UNIT = { volleyball: "m", badminton: "m", tabletennis: "m", padel: "m", squash: "m", athletics: "m", skating: "m", swimming: "m", waterpolo: "m", diving: "m", artisticswimming: "m", sepaktakraw: "m", teqball: "m", roundnet: "m", netball: "m", soccer: "m", handball: "m", fieldhockey: "m", rugby: "m", afl: "m", gaelic: "m" };
|
|
32
|
+
/** What stands on the surface the options draw: nothing on a curling sheet, which is flat. */
|
|
33
|
+
export function uprightsFor(o, settings = {}) {
|
|
34
|
+
const { sport, league } = chooseLeague(o);
|
|
35
|
+
const unit = lengthUnit(o);
|
|
36
|
+
let items = [];
|
|
37
|
+
let lift = 0;
|
|
38
|
+
let thin = 0.2;
|
|
39
|
+
let area;
|
|
40
|
+
if (sport === "nfl" || sport === "cfl" || sport === "ifaf") {
|
|
41
|
+
const spec = league?.family === "football" ? fieldOf(league, o.year) : footballIn(sport, o.year);
|
|
42
|
+
const shape = league?.family === "football" || sport === "ifaf" ? "slingshot" : rulesFor(sport, o.year).rules.postShape;
|
|
43
|
+
thin = thinnest(footballLength(spec), spec.width);
|
|
44
|
+
area = { x0: 0, y0: 0, x1: footballLength(spec), y1: spec.width };
|
|
45
|
+
// Over the ground: the NFL's 10 + 35 ft, the CFL's 40 ft, and the NCAA's least 30 ft for the rest.
|
|
46
|
+
items = footballUprights(spec, shape, league !== undefined ? 30 : sport === "nfl" ? 45 : sport === "cfl" ? 40 : 30, thin);
|
|
47
|
+
}
|
|
48
|
+
else if (sport === "nhl") {
|
|
49
|
+
const rink = league?.family === "hockey" ? rinkOf(league, o.year) : NHL_RINK;
|
|
50
|
+
const goalLine = league?.family === "hockey" ? rink.goalLine : rulesFor("nhl", o.year).rules.goalLine;
|
|
51
|
+
thin = thinnest(rink.length, rink.width);
|
|
52
|
+
area = { x0: 0, y0: 0, x1: rink.length, y1: rink.width };
|
|
53
|
+
items = rinkUprights(rink, goalLine, thin, settings.boards !== false);
|
|
54
|
+
}
|
|
55
|
+
else if (sport === "nba") {
|
|
56
|
+
const court = league?.family === "basketball" ? courtOf(league, o.year) : NBA_COURT;
|
|
57
|
+
thin = thinnest(court.length, court.width);
|
|
58
|
+
area = { x0: 0, y0: 0, x1: court.length, y1: court.width };
|
|
59
|
+
items = courtUprights(court, thin, league?.id === "fiba");
|
|
60
|
+
}
|
|
61
|
+
else if (sport === "mlb") {
|
|
62
|
+
const diamond = league?.family === "baseball" ? diamondOf(league, o.year) : undefined;
|
|
63
|
+
const park = parkById(o.park);
|
|
64
|
+
const points = fenceFor(park, feetFence(o.fence, unit), o.year, diamond === undefined ? undefined : diamondFence(diamond));
|
|
65
|
+
thin = 0.4;
|
|
66
|
+
items = ballparkUprights(points, park === undefined ? [] : parkWallsIn(park, o.year), thin);
|
|
67
|
+
// The surface is the field out to the fence and the backstop behind home plate.
|
|
68
|
+
const out = fenceOutline(points, 3).map(([u, v]) => [u, -v]);
|
|
69
|
+
area = { x0: Math.min(...out.map((p) => p[0])), x1: Math.max(...out.map((p) => p[0])), y0: Math.min(...out.map((p) => p[1])), y1: 70 };
|
|
70
|
+
}
|
|
71
|
+
else if (sport === "soccer") {
|
|
72
|
+
const spec = soccerSpec(pitchOf(o), o.units === undefined ? "m" : unit);
|
|
73
|
+
thin = thinnest(spec.length * 3.2808, spec.width * 3.2808);
|
|
74
|
+
area = { x0: 0, y0: 0, x1: spec.length * 3.2808, y1: spec.width * 3.2808 };
|
|
75
|
+
items = soccerUprights(spec, o.cornerFlags !== false, thin);
|
|
76
|
+
}
|
|
77
|
+
else if (isNetSport(sport)) {
|
|
78
|
+
const plan = courtFor({ ...o, sport });
|
|
79
|
+
const geo = courtGeometry(plan);
|
|
80
|
+
thin = thinnest(geo.length, geo.width);
|
|
81
|
+
area = { x0: 0, y0: 0, x1: geo.length, y1: geo.width };
|
|
82
|
+
const built = netUprights(plan, geo, { netHeight: settings.netHeight, boards: settings.boards }, thin);
|
|
83
|
+
items = built.items;
|
|
84
|
+
lift = built.lift;
|
|
85
|
+
}
|
|
86
|
+
else if (isPoolSport(sport)) {
|
|
87
|
+
const plan = poolPlan({ sport, pool: o.pool });
|
|
88
|
+
const [w, h] = [poolFeet(plan.length), poolFeet(plan.width)];
|
|
89
|
+
thin = thinnest(w, h);
|
|
90
|
+
area = { x0: 0, y0: 0, x1: w, y1: h };
|
|
91
|
+
items = poolUprights({ ...o, sport }, thin).items;
|
|
92
|
+
}
|
|
93
|
+
else if (sport === "athletics") {
|
|
94
|
+
const built = athleticsUprights({ ...o, sport }, 0.3);
|
|
95
|
+
area = built.area;
|
|
96
|
+
thin = thinnest(area.x1, area.y1);
|
|
97
|
+
items = athleticsUprights({ ...o, sport }, thin).items;
|
|
98
|
+
}
|
|
99
|
+
else if (sport === "skating") {
|
|
100
|
+
const built = skatingUprights({ ...o, sport }, 0.3);
|
|
101
|
+
area = built.area;
|
|
102
|
+
thin = thinnest(area.x1, area.y1);
|
|
103
|
+
items = skatingUprights({ ...o, sport }, thin).items;
|
|
104
|
+
}
|
|
105
|
+
else if (isFieldSport(sport)) {
|
|
106
|
+
const plan = fieldPlan({ ...o, sport });
|
|
107
|
+
thin = thinnest(plan.length, plan.width);
|
|
108
|
+
area = { x0: 0, y0: 0, x1: plan.length, y1: plan.width };
|
|
109
|
+
items = fieldUprights(plan, thin);
|
|
110
|
+
}
|
|
111
|
+
if (sport === "curling") {
|
|
112
|
+
const sheet = sheetFor(o).spec;
|
|
113
|
+
thin = thinnest(sheet.length, sheet.width);
|
|
114
|
+
area = { x0: 0, y0: 0, x1: sheet.length, y1: sheet.width };
|
|
115
|
+
}
|
|
116
|
+
// The score, hung up: the badge on a board that turns to face the eye.
|
|
117
|
+
const scored = o.home !== undefined && o.away !== undefined && o.size !== "badge";
|
|
118
|
+
if (scored && settings.score !== "floor" && area !== undefined) {
|
|
119
|
+
const board = scoreboard(drawKyogijo({ ...o, size: "badge", style: "none", id: `${drawingId(o)}-board` }), sport, area, thin);
|
|
120
|
+
if (board !== undefined)
|
|
121
|
+
items.push(board);
|
|
122
|
+
}
|
|
123
|
+
if (settings.only !== undefined)
|
|
124
|
+
items = items.filter((item) => settings.only.includes(item.kind));
|
|
125
|
+
return { items, lift, thin, area };
|
|
126
|
+
}
|
|
127
|
+
/** A length in feet as a rulebook would print it in `unit`: metres to the centimetre, feet to a tenth. */
|
|
128
|
+
function said(feet, unit, language) {
|
|
129
|
+
const value = convertLength(feet, "ft", unit);
|
|
130
|
+
const text = String(Math.round(value * (unit === "m" ? 100 : 10)) / (unit === "m" ? 100 : 10));
|
|
131
|
+
const units = KYOGIJO_STRINGS[language].league.units;
|
|
132
|
+
return `${text} ${units[unit === "m" || unit === "yd" ? unit : "ft"]}`;
|
|
133
|
+
}
|
|
134
|
+
/** The unit the heights are said in: the options' own, else the rulebook's of the sport. */
|
|
135
|
+
export function heightUnit(o) {
|
|
136
|
+
if (o.units !== undefined)
|
|
137
|
+
return lengthUnit(o);
|
|
138
|
+
const { sport, league } = chooseLeague(o);
|
|
139
|
+
return league?.unit ?? RULEBOOK_UNIT[sport] ?? "ft";
|
|
140
|
+
}
|
|
141
|
+
/** What stands on the surface, in a sentence, with heights: for a reader who cannot see the 3D view. An empty string when nothing stands on it. */
|
|
142
|
+
export function describeUprights(o, uprights, language = "en") {
|
|
143
|
+
const words = KYOGIJO_STRINGS[language].turn;
|
|
144
|
+
const unit = heightUnit(o);
|
|
145
|
+
// One entry for each kind, and for a kind that has two (a goal at each end) the first: both are the same.
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
const list = [];
|
|
148
|
+
for (const item of uprights.items) {
|
|
149
|
+
if (seen.has(item.kind))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(item.kind);
|
|
152
|
+
if (item.height <= 0)
|
|
153
|
+
continue;
|
|
154
|
+
const name = words.parts[item.kind];
|
|
155
|
+
// A rink's boards are said by the top of the glass; a table by the height of its top.
|
|
156
|
+
const height = item.kind === "boards" ? (item.low ?? item.height) : item.height;
|
|
157
|
+
list.push(item.kind === "net" && item.low !== undefined && Math.abs(item.low - item.height) > 0.02 ? fill(words.itemSag, { name, h: said(height, unit, language), low: said(item.low, unit, language) }) : fill(words.item, { name, h: said(height, unit, language) }));
|
|
158
|
+
}
|
|
159
|
+
return list.length === 0 ? "" : fill(words.standing, { list: list.join(KYOGIJO_STRINGS[language].layers.listSep) });
|
|
160
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CourtGeometry, CourtPlan } from "./court.ts";
|
|
2
|
+
import type { NetEnd } from "./uprights.net.ts";
|
|
3
|
+
/** The uprights of the court sports that have walls, a table, a ring or a net of their own: padel, squash, racquetball, sepak takraw, teqball, roundnet and netball. */
|
|
4
|
+
export declare function moreUprights(plan: CourtPlan, geo: CourtGeometry, settings: {
|
|
5
|
+
netHeight?: "men" | "women";
|
|
6
|
+
boards?: boolean;
|
|
7
|
+
}, thin: number): NetEnd;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { bar, mesh, n, plane, pole, polygon, shy, threads, wall } from "./turn.kit.js";
|
|
2
|
+
import { netPlane, sag, tableBody } from "./uprights.netkit.js";
|
|
3
|
+
import { convertLength } from "./units.js";
|
|
4
|
+
const feet = (metres) => convertLength(metres, "m", "ft");
|
|
5
|
+
/** The walls of a court, each giving way when it is between the eye and the court. */
|
|
6
|
+
const shyAll = (planes, geo) => planes.map((p) => shy(p, [geo.length / 2, geo.width / 2]));
|
|
7
|
+
/** The posts of a court at the fixtures' places, `height` high. */
|
|
8
|
+
const postsOf = (geo, x, y0, y1, height, thin) => ({
|
|
9
|
+
id: "posts",
|
|
10
|
+
kind: "netposts",
|
|
11
|
+
at: [x, (y0 + y1) / 2],
|
|
12
|
+
height,
|
|
13
|
+
planes: geo.posts.flatMap((p, i) => pole(`post-${i === 0 ? "a" : "b"}`, "post", p.x, p.y, 0, height, Math.max(p.size, thin * 1.4), "u-pole")),
|
|
14
|
+
});
|
|
15
|
+
/** The line a wall stands on: the inner edge of the band that stands for it, from end to end of the court's side. */
|
|
16
|
+
function standsOn(band, L, W) {
|
|
17
|
+
const [a, b, c, d] = band.rect;
|
|
18
|
+
if (c - a < d - b) {
|
|
19
|
+
const x = c <= L / 2 ? c : a;
|
|
20
|
+
return [x, Math.max(b, 0), x, Math.min(d, W)];
|
|
21
|
+
}
|
|
22
|
+
const y = d <= W / 2 ? d : b;
|
|
23
|
+
return [Math.max(a, 0), y, Math.min(c, L), y];
|
|
24
|
+
}
|
|
25
|
+
/** A solid or glass wall between two heights along a line, with a cap along its top. */
|
|
26
|
+
function solidWall(id, x0, y0, x1, y1, z0, z1, cls, thin) {
|
|
27
|
+
const run = Math.hypot(x1 - x0, y1 - y0);
|
|
28
|
+
return wall(id, "wall", x0, y0, x1, y1, z0, z1, `<rect width="${n(run)}" height="${n(z1 - z0)}" class="${cls}"/><rect width="${n(run)}" height="${n(Math.max(thin * 0.8, 0.1))}" class="u-cap"/>`);
|
|
29
|
+
}
|
|
30
|
+
/** Metal fence between two heights: a mesh. */
|
|
31
|
+
function fencing(id, x0, y0, x1, y1, z0, z1, cell) {
|
|
32
|
+
const run = Math.hypot(x1 - x0, y1 - y0);
|
|
33
|
+
const h = z1 - z0;
|
|
34
|
+
return wall(id, "wall", x0, y0, x1, y1, z0, z1, `<rect width="${n(run)}" height="${n(h)}" class="u-netfill"/>${threads(mesh(run, h, cell), cell)}`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A padel court's enclosure (The Court: Enclosures): the end walls 3 m of wall with 1 m of metal fence over them, 4 m in all; at each end of each side a wall 3 m high and
|
|
38
|
+
* 4 m long, and the fence between. The part of the Rules read gives the fence along the sides no height of its own: it is drawn as high as the side walls, 3 m (ours).
|
|
39
|
+
*/
|
|
40
|
+
function padelWalls(f, geo, thin) {
|
|
41
|
+
const { length: L, width: W } = geo;
|
|
42
|
+
const cell = Math.max(feet(0.045), thin * 5);
|
|
43
|
+
const planes = [];
|
|
44
|
+
geo.bands.forEach((band, i) => {
|
|
45
|
+
const [x0, y0, x1, y1] = standsOn(band, L, W);
|
|
46
|
+
if (Math.hypot(x1 - x0, y1 - y0) <= 0)
|
|
47
|
+
return;
|
|
48
|
+
if (band.kind === "mesh")
|
|
49
|
+
planes.push(fencing(`fence-${i}`, x0, y0, x1, y1, 0, f.enclosure.sideHeight, cell));
|
|
50
|
+
else if (x0 === x1)
|
|
51
|
+
planes.push(solidWall(`wall-${i}`, x0, y0, x1, y1, 0, f.enclosure.endWall, "u-glass", thin), fencing(`fence-${i}`, x0, y0, x1, y1, f.enclosure.endWall, f.enclosure.endHeight, cell));
|
|
52
|
+
else
|
|
53
|
+
planes.push(solidWall(`wall-${i}`, x0, y0, x1, y1, 0, f.enclosure.sideHeight, "u-glass", thin));
|
|
54
|
+
});
|
|
55
|
+
return { id: "walls", kind: "walls", at: [L / 2, W / 2], height: f.enclosure.endHeight, planes: shyAll(planes, geo) };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A squash court's walls, up to their out lines: the front wall to the front wall line (4.57 m), the back wall to the back wall line (2.13 m), and each side wall between
|
|
59
|
+
* them, its top running straight from the one to the other (only its two ends are among the figures; the straight line is ours). The front wall is solid, the rest glass.
|
|
60
|
+
*/
|
|
61
|
+
function squashWalls(f, geo, thin) {
|
|
62
|
+
const { length: L, width: W } = geo;
|
|
63
|
+
const front = f.wall.frontWallLine;
|
|
64
|
+
const back = f.wall.backWallLine;
|
|
65
|
+
const planes = [solidWall("wall-front", 0, 0, 0, W, 0, front, "u-wall", thin), solidWall("wall-back", L, 0, L, W, 0, back, "u-glass", thin)];
|
|
66
|
+
const side = polygon([[0, 0], [L, front - back], [L, front], [0, front]]);
|
|
67
|
+
for (const [id, y] of [["wall-side-a", 0], ["wall-side-b", W]])
|
|
68
|
+
planes.push(wall(id, "wall", 0, y, L, y, 0, front, `<path d="${side}" class="u-glass"/>`));
|
|
69
|
+
return { id: "walls", kind: "walls", at: [L / 2, W / 2], height: front, planes: shyAll(planes, geo) };
|
|
70
|
+
}
|
|
71
|
+
/** A racquetball court's walls, 20 feet high all round (Rule 2.1(a): the court is 20 feet high, the back wall at least 12), the front wall solid and the others glass. */
|
|
72
|
+
function racquetballWalls(f, geo, thin) {
|
|
73
|
+
const { length: L, width: W } = geo;
|
|
74
|
+
const h = f.height;
|
|
75
|
+
const planes = [
|
|
76
|
+
solidWall("wall-front", 0, 0, 0, W, 0, h, "u-wall", thin),
|
|
77
|
+
solidWall("wall-back", L, 0, L, W, 0, h, "u-glass", thin),
|
|
78
|
+
solidWall("wall-side-a", 0, 0, L, 0, 0, h, "u-glass", thin),
|
|
79
|
+
solidWall("wall-side-b", 0, W, L, W, 0, h, "u-glass", thin),
|
|
80
|
+
];
|
|
81
|
+
return { id: "walls", kind: "walls", at: [L / 2, W / 2], height: h, planes: shyAll(planes, geo) };
|
|
82
|
+
}
|
|
83
|
+
/** A roundnet set: a net stretched in a ring 91.4 cm across, its rim 20.3 cm over the ground, on four legs (Rule 1.1.2). */
|
|
84
|
+
function roundnetSet(f, geo, thin) {
|
|
85
|
+
const solid = geo.solids.find((one) => one.kind === "set");
|
|
86
|
+
const cx = solid?.kind === "set" ? solid.cx : geo.length / 2;
|
|
87
|
+
const cy = solid?.kind === "set" ? solid.cy : geo.width / 2;
|
|
88
|
+
const r = f.set.diameter / 2;
|
|
89
|
+
const h = f.set.height;
|
|
90
|
+
const rim = Math.max(feet(0.02), thin * 1.5);
|
|
91
|
+
const ring = (rr) => `M${n(r - rr)} ${n(r)}a${n(rr)} ${n(rr)} 0 1 0 ${n(2 * rr)} 0a${n(rr)} ${n(rr)} 0 1 0 ${n(-2 * rr)} 0`;
|
|
92
|
+
let lines = [0.34, 0.67].map((k) => ring(r * k)).join("");
|
|
93
|
+
for (let i = 0; i < 8; i += 1) {
|
|
94
|
+
const a = (Math.PI * i) / 8;
|
|
95
|
+
lines += `M${n(r - r * Math.cos(a))} ${n(r - r * Math.sin(a))}L${n(r + r * Math.cos(a))} ${n(r + r * Math.sin(a))}`;
|
|
96
|
+
}
|
|
97
|
+
const top = plane("set-net", "net", [cx - r, cy - r, h], [2 * r, 0, 0], [0, 2 * r, 0], `<circle cx="${n(r)}" cy="${n(r)}" r="${n(r)}" class="u-netfill"/>${threads(lines, r / 3)}<circle cx="${n(r)}" cy="${n(r)}" r="${n(r - rim / 2)}" class="u-rimline" stroke-width="${n(rim)}" fill="none"/>`);
|
|
98
|
+
const legs = [];
|
|
99
|
+
for (let i = 0; i < 4; i += 1) {
|
|
100
|
+
const a = Math.PI / 4 + (Math.PI * i) / 2;
|
|
101
|
+
legs.push(...pole(`leg-${i}`, "leg", cx + r * 0.9 * Math.cos(a), cy + r * 0.9 * Math.sin(a), 0, h, Math.max(feet(0.02), thin * 1.2), "u-leg"));
|
|
102
|
+
}
|
|
103
|
+
return { id: "net", kind: "net", at: [cx, cy], height: h, planes: [top, ...legs] };
|
|
104
|
+
}
|
|
105
|
+
/** A netball goalpost, 3.05 m high, at the middle of each goal line, with its ring on a bar that projects 150 mm from the pole (Rule 1, items 13 and 14). */
|
|
106
|
+
function netballPosts(f, geo, thin) {
|
|
107
|
+
const { length: L, width: W } = geo;
|
|
108
|
+
const h = f.post.height;
|
|
109
|
+
const { inside, rod, bar: reach } = f.post.ring;
|
|
110
|
+
const d = f.post.poleDiameter;
|
|
111
|
+
const R = inside / 2 + rod;
|
|
112
|
+
const cy = W / 2;
|
|
113
|
+
const planes = [];
|
|
114
|
+
for (const left of [true, false]) {
|
|
115
|
+
const tag = left ? "left" : "right";
|
|
116
|
+
const sign = left ? 1 : -1;
|
|
117
|
+
const x = left ? d / 2 : L - d / 2;
|
|
118
|
+
const cx = x + sign * (d / 2 + reach + R);
|
|
119
|
+
planes.push(...pole(`pole-${tag}`, "post", x, cy, 0, h, Math.max(d, thin * 1.4), "u-pole"));
|
|
120
|
+
planes.push(...bar(`bar-${tag}`, "post", [x, cy, h], [cx - sign * R, cy, h], Math.max(rod, thin * 1.2), "u-pole"));
|
|
121
|
+
const line = Math.max(rod, thin * 1.6);
|
|
122
|
+
planes.push(plane(`ring-${tag}`, "rim", [cx - R, cy - R, h], [2 * R, 0, 0], [0, 2 * R, 0], `<circle cx="${n(R)}" cy="${n(R)}" r="${n(R - line / 2)}" class="u-rimline" stroke-width="${n(line)}" fill="none"/>`));
|
|
123
|
+
}
|
|
124
|
+
return { id: "goalposts", kind: "goalposts", at: [L / 2, cy], height: h, planes };
|
|
125
|
+
}
|
|
126
|
+
/** The uprights of the court sports that have walls, a table, a ring or a net of their own: padel, squash, racquetball, sepak takraw, teqball, roundnet and netball. */
|
|
127
|
+
export function moreUprights(plan, geo, settings, thin) {
|
|
128
|
+
const items = [];
|
|
129
|
+
const { x, y0, y1 } = geo.net ?? { x: geo.length / 2, y0: 0, y1: geo.width };
|
|
130
|
+
const at = [x, (y0 + y1) / 2];
|
|
131
|
+
const walls = settings.boards !== false;
|
|
132
|
+
switch (plan.sport) {
|
|
133
|
+
case "padel": {
|
|
134
|
+
const f = plan.figures;
|
|
135
|
+
const { heightCentre: c, heightEnds: e, postHeight: p } = f.net;
|
|
136
|
+
const cell = Math.max(feet(0.045), thin * 5);
|
|
137
|
+
items.push({ id: "net", kind: "net", at, height: e, low: c, planes: [netPlane({ id: "net", x, y0, y1, top: sag(y0, y1, c, e), bottom: () => 0, cell, tape: Math.max(feet(0.05), thin) }, 0)] });
|
|
138
|
+
items.push(postsOf(geo, x, y0, y1, p, thin));
|
|
139
|
+
if (walls)
|
|
140
|
+
items.push(padelWalls(f, geo, thin));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "squash":
|
|
144
|
+
if (walls)
|
|
145
|
+
items.push(squashWalls(plan.figures, geo, thin));
|
|
146
|
+
break;
|
|
147
|
+
case "racquetball":
|
|
148
|
+
if (walls)
|
|
149
|
+
items.push(racquetballWalls(plan.figures, geo, thin));
|
|
150
|
+
break;
|
|
151
|
+
case "sepaktakraw": {
|
|
152
|
+
const f = plan.figures;
|
|
153
|
+
const women = settings.netHeight === "women";
|
|
154
|
+
const centre = women ? f.net.heightWomen : f.net.heightMen;
|
|
155
|
+
const post = women ? f.net.postWomen : f.net.postMen;
|
|
156
|
+
const top = sag(y0, y1, centre, post);
|
|
157
|
+
const cell = Math.max(feet(0.045), thin * 5);
|
|
158
|
+
items.push({ id: "net", kind: "net", at, height: post, low: centre, planes: [netPlane({ id: "net", x, y0, y1, top, bottom: (y) => top(y) - f.net.width, cell, tape: Math.max(feet(0.05), thin) }, 0)] });
|
|
159
|
+
items.push(postsOf(geo, x, y0, y1, post, thin));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "teqball": {
|
|
163
|
+
const f = plan.figures;
|
|
164
|
+
// The table is 900 mm high with its net (1.1.4) and the net stands 140 mm over it (1.2.3): the top is 760 mm up.
|
|
165
|
+
const lift = f.table.height - f.net.height;
|
|
166
|
+
const solid = geo.solids.find((one) => one.kind === "table");
|
|
167
|
+
const [tx0, ty0, tx1, ty1] = solid?.kind === "table" ? solid.rect : [0, 0, geo.length, geo.width];
|
|
168
|
+
const cell = Math.max(feet(0.02), thin * 3);
|
|
169
|
+
items.push({ id: "net", kind: "net", at, height: lift + f.net.height, planes: [netPlane({ id: "net", x, y0, y1, top: () => f.net.height, bottom: () => 0, cell, tape: Math.max(feet(0.02), thin * 0.8) }, lift)] });
|
|
170
|
+
items.push({ id: "table", kind: "table-legs", at: [(tx0 + tx1) / 2, (ty0 + ty1) / 2], height: lift, planes: tableBody(tx0, ty0, tx1, ty1, lift, thin, true) });
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "roundnet":
|
|
174
|
+
items.push(roundnetSet(plan.figures, geo, thin));
|
|
175
|
+
break;
|
|
176
|
+
case "netball":
|
|
177
|
+
items.push(netballPosts(plan.figures, geo, thin));
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
return { items, lift: 0 };
|
|
183
|
+
}
|