@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,57 @@
|
|
|
1
|
+
import type { Area, Shape } from "../fields.types.ts";
|
|
2
|
+
import type { AthleticsFigures } from "../leagues/athletics.ts";
|
|
3
|
+
import { type Oval } from "./track.ts";
|
|
4
|
+
/** A string of lane numbers painted on the track: the digits, how tall, where they are centred. They stand upright whatever way the page is turned. */
|
|
5
|
+
export interface Digits {
|
|
6
|
+
id: string;
|
|
7
|
+
feature: string;
|
|
8
|
+
text: string;
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
h: number;
|
|
12
|
+
/** Drawn on a disc of the plate's colour, in the plate's ink, at a size that reads in a close-up (a tag), not painted on the track. */
|
|
13
|
+
tag?: boolean;
|
|
14
|
+
/** For a number behind a start line: the point of the line in this lane and the unit vector the athlete runs along, so that a tag can be moved back to clear the line. */
|
|
15
|
+
from?: readonly [x: number, y: number, fx: number, fy: number];
|
|
16
|
+
}
|
|
17
|
+
/** What a part of a plan adds: shapes and areas in painter's order, and digits. */
|
|
18
|
+
export interface Parts {
|
|
19
|
+
areas: Area[];
|
|
20
|
+
shapes: Shape[];
|
|
21
|
+
digits: Digits[];
|
|
22
|
+
}
|
|
23
|
+
export declare const emptyParts: () => Parts;
|
|
24
|
+
/** Add `more` to `into`. */
|
|
25
|
+
export declare function addParts(into: Parts, more: Parts): void;
|
|
26
|
+
/** The path of a stadium: two semicircles of radius `r` joined by straights of length `s`, about the origin. */
|
|
27
|
+
export declare const stadiumD: (s: number, r: number) => string;
|
|
28
|
+
/**
|
|
29
|
+
* A lane line of the oval, cut where the straight's surface lies over its bends (the straight runs on beyond the oval's, and the Marking Plan draws only the straight's own lines there): the bottom
|
|
30
|
+
* straight between the bends, the east bend from where it leaves the straight's run-out round to the top, the top straight, and the west bend as far as the straight's starting area.
|
|
31
|
+
*/
|
|
32
|
+
export declare function laneLineD(o: Oval, r: number, straight: StraightPlan | undefined): string;
|
|
33
|
+
/** The radius of the outer edge of an oval's last lane. */
|
|
34
|
+
export declare const outerRadius: (o: Oval) => number;
|
|
35
|
+
/** The straight's geometry: its first and last x (the starting area before the longest race, the run-out beyond the finish line), its lanes and how wide each is. */
|
|
36
|
+
export interface StraightPlan {
|
|
37
|
+
lanes: number;
|
|
38
|
+
lane: number;
|
|
39
|
+
x0: number;
|
|
40
|
+
x1: number;
|
|
41
|
+
/** The start of the longest race of the straight, in metres before the finish line. */
|
|
42
|
+
longest: number;
|
|
43
|
+
/** The straight's inside edge (the top of lane 1) and its finish line, in the plan's frame: the oval's finish straight runs on, the covered track's lies inside it. */
|
|
44
|
+
y0: number;
|
|
45
|
+
finish: number;
|
|
46
|
+
}
|
|
47
|
+
/** The straight that runs on beyond the oval's finish straight (Manual 2.2.2.1): at least six lanes, a starting area of 3 m before its longest race and a run-out of 17 m. */
|
|
48
|
+
export declare function straightOf(o: Oval, fig: AthleticsFigures, lanes: number, longest: number): StraightPlan;
|
|
49
|
+
/**
|
|
50
|
+
* The straight of a covered track (Rules 41 to 43; the Marking Plan for the Standard Short Track): lanes 1.22 m wide inside the oval, parallel to its straights, with a starting area of 3 m before a
|
|
51
|
+
* 60 m start line and a run-out beyond the finish line. Its place in the infield is ours: the Rules and the plan give its lanes and its length, not where it lies.
|
|
52
|
+
*/
|
|
53
|
+
export declare function indoorStraightOf(o: Oval, fig: AthleticsFigures, lanes: number, length: number): StraightPlan;
|
|
54
|
+
/** The infield straight of a covered track: its surface, its lane lines, its start area's and run-out's ends and its finish line, and the lane numbers before the finish. */
|
|
55
|
+
export declare function indoorStraightParts(st: StraightPlan, numbers: boolean, w: number): Parts;
|
|
56
|
+
/** The surface of the oval and of the straight, the kerb, the lane lines, the finish line and the numbers before it. */
|
|
57
|
+
export declare function ovalParts(o: Oval, fig: AthleticsFigures, straight: StraightPlan | undefined, numbers: boolean, hole?: string): Parts;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The oval, the straight that runs on beyond it and the finish: the surface of the track between its kerb and its outer edge, the lane lines, the kerb, the finish line and the lane numbers
|
|
2
|
+
// before it. Every figure is the Track and Field Facilities Manual's (2019 edition, 2.2.1 and 2.2.2) or Technical Rule 14's of Book C2.1, in metres. Nothing here draws SVG: it makes the shapes.
|
|
3
|
+
import { line, path } from "../fields.kit.js";
|
|
4
|
+
import { num } from "../format.js";
|
|
5
|
+
import { laneEdge } from "./track.js";
|
|
6
|
+
export const emptyParts = () => ({ areas: [], shapes: [], digits: [] });
|
|
7
|
+
/** Add `more` to `into`. */
|
|
8
|
+
export function addParts(into, more) {
|
|
9
|
+
into.areas.push(...more.areas);
|
|
10
|
+
into.shapes.push(...more.shapes);
|
|
11
|
+
into.digits.push(...more.digits);
|
|
12
|
+
}
|
|
13
|
+
/** The path of a stadium: two semicircles of radius `r` joined by straights of length `s`, about the origin. */
|
|
14
|
+
export const stadiumD = (s, r) => `M${num(-s / 2)} ${num(-r)}H${num(s / 2)}A${num(r)} ${num(r)} 0 0 1 ${num(s / 2)} ${num(r)}H${num(-s / 2)}A${num(r)} ${num(r)} 0 0 1 ${num(-s / 2)} ${num(-r)}Z`;
|
|
15
|
+
/**
|
|
16
|
+
* A lane line of the oval, cut where the straight's surface lies over its bends (the straight runs on beyond the oval's, and the Marking Plan draws only the straight's own lines there): the bottom
|
|
17
|
+
* straight between the bends, the east bend from where it leaves the straight's run-out round to the top, the top straight, and the west bend as far as the straight's starting area.
|
|
18
|
+
*/
|
|
19
|
+
export function laneLineD(o, r, straight) {
|
|
20
|
+
if (straight === undefined)
|
|
21
|
+
return stadiumD(o.straight, r);
|
|
22
|
+
const S = o.straight;
|
|
23
|
+
const R = o.radius;
|
|
24
|
+
const wEast = straight.x1 - S / 2;
|
|
25
|
+
const wWest = -S / 2 - straight.x0;
|
|
26
|
+
// East bend, from the bottom (f = 0): inside the straight while its depth below the kerb line is positive and it is no further out than the run-out.
|
|
27
|
+
const fEast = Math.min(Math.acos(Math.min(1, R / r)), Math.asin(Math.min(1, wEast / r)));
|
|
28
|
+
// West bend, from the top: inside the straight from where it comes back below the kerb line and within the starting area.
|
|
29
|
+
const fWest = Math.max(Math.PI - Math.acos(Math.min(1, R / r)), Math.PI - Math.asin(Math.min(1, wWest / r)));
|
|
30
|
+
const e = [S / 2 + r * Math.sin(fEast), r * Math.cos(fEast)];
|
|
31
|
+
const w = [-S / 2 - r * Math.sin(fWest), -r * Math.cos(fWest)];
|
|
32
|
+
return `M${num(e[0])} ${num(e[1])}A${num(r)} ${num(r)} 0 0 0 ${num(S / 2)} ${num(-r)}H${num(-S / 2)}A${num(r)} ${num(r)} 0 0 0 ${num(w[0])} ${num(w[1])}M${num(-S / 2)} ${num(r)}H${num(S / 2)}`;
|
|
33
|
+
}
|
|
34
|
+
/** The radius of the outer edge of an oval's last lane. */
|
|
35
|
+
export const outerRadius = (o) => laneEdge(o, o.lanes);
|
|
36
|
+
/** The straight that runs on beyond the oval's finish straight (Manual 2.2.2.1): at least six lanes, a starting area of 3 m before its longest race and a run-out of 17 m. */
|
|
37
|
+
export function straightOf(o, fig, lanes, longest) {
|
|
38
|
+
return { lanes, lane: fig.straightLane, x0: o.straight / 2 - longest - fig.startArea, x1: o.straight / 2 + fig.runOut, longest, y0: o.radius, finish: o.straight / 2 };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The straight of a covered track (Rules 41 to 43; the Marking Plan for the Standard Short Track): lanes 1.22 m wide inside the oval, parallel to its straights, with a starting area of 3 m before a
|
|
42
|
+
* 60 m start line and a run-out beyond the finish line. Its place in the infield is ours: the Rules and the plan give its lanes and its length, not where it lies.
|
|
43
|
+
*/
|
|
44
|
+
export function indoorStraightOf(o, fig, lanes, length) {
|
|
45
|
+
const total = fig.startArea + length + fig.runOut;
|
|
46
|
+
const x0 = -total / 2;
|
|
47
|
+
const width = lanes * fig.straightLane;
|
|
48
|
+
return { lanes, lane: fig.straightLane, x0, x1: x0 + total, longest: length, y0: -width / 2, finish: x0 + fig.startArea + length };
|
|
49
|
+
}
|
|
50
|
+
/** The infield straight of a covered track: its surface, its lane lines, its start area's and run-out's ends and its finish line, and the lane numbers before the finish. */
|
|
51
|
+
export function indoorStraightParts(st, numbers, w) {
|
|
52
|
+
const parts = emptyParts();
|
|
53
|
+
const edge = st.y0 + st.lanes * st.lane;
|
|
54
|
+
parts.areas.push({ id: "infield-straight", feature: "track", cls: "ky-ath-track", d: `M${num(st.x0)} ${num(st.y0)}H${num(st.x1)}V${num(edge)}H${num(st.x0)}Z` });
|
|
55
|
+
for (let k = 0; k <= st.lanes; k += 1) {
|
|
56
|
+
const y = k === 0 ? st.y0 : st.y0 + k * st.lane - w / 2;
|
|
57
|
+
parts.shapes.push(line_("straight-line", st.x0, y, st.x1, y, w));
|
|
58
|
+
}
|
|
59
|
+
parts.shapes.push(line_("straight-end", st.x0, st.y0, st.x0, edge, w), line_("straight-end", st.x1, st.y0, st.x1, edge, w));
|
|
60
|
+
parts.shapes.push(line_("finish-line", st.finish, st.y0, st.finish, edge, w));
|
|
61
|
+
for (let n = 1; n <= st.lanes; n += 1) {
|
|
62
|
+
const y = st.y0 + (n - 0.5) * st.lane;
|
|
63
|
+
parts.shapes.push(line("finish-tick", "finish-line", st.finish - 1, y - 0.4, st.finish - 1, y + 0.4, { w: 0.03, least: 0.08 }));
|
|
64
|
+
if (numbers)
|
|
65
|
+
parts.digits.push({ id: "finish-number", feature: "lane-numbers", text: String(n), x: st.finish - 2.6, y, h: Math.min(0.6, st.lane * 0.55) });
|
|
66
|
+
}
|
|
67
|
+
return parts;
|
|
68
|
+
}
|
|
69
|
+
const line_ = (id, x0, y0, x1, y1, w) => line(id, "lane-lines", x0, y0, x1, y1, { w });
|
|
70
|
+
/** The surface of the oval and of the straight, the kerb, the lane lines, the finish line and the numbers before it. */
|
|
71
|
+
export function ovalParts(o, fig, straight, numbers, hole) {
|
|
72
|
+
const parts = emptyParts();
|
|
73
|
+
const S = o.straight;
|
|
74
|
+
const R = o.radius;
|
|
75
|
+
const Rout = outerRadius(o);
|
|
76
|
+
const w = o.line;
|
|
77
|
+
// The surface: the ring between the kerb and the outer edge, and the straight's.
|
|
78
|
+
// The surface is one shape: the oval's outer edge less the turf's (or, with none, the kerb's) outline, so that the synthetic surface of the segments joins the oval's with no seam.
|
|
79
|
+
parts.areas.push({ id: "track", feature: "track", cls: "ky-ath-track", d: stadiumD(S, Rout) + (hole ?? stadiumD(S, R)), evenodd: true });
|
|
80
|
+
if (straight !== undefined) {
|
|
81
|
+
const edge = R + straight.lanes * straight.lane;
|
|
82
|
+
parts.areas.push({ id: "straight", feature: "track", cls: "ky-ath-track", d: `M${num(straight.x0)} ${num(R)}H${num(straight.x1)}V${num(edge)}H${num(straight.x0)}Z` });
|
|
83
|
+
}
|
|
84
|
+
// The lane lines: the line on the right of each lane, whose outer edge is at the lane's edge (Rule 14.4), so that a line's middle is half its width inside.
|
|
85
|
+
for (let k = 1; k <= o.lanes; k += 1)
|
|
86
|
+
parts.shapes.push(path(`lane-line-${k}`, "lane-lines", laneLineD(o, laneEdge(o, k) - w / 2, straight), { w }));
|
|
87
|
+
// The kerb, 50 mm wide at the least (Rule 14.1); the rules allow it as wide as 250 mm, and it is drawn a little wider than the lines when the drawing is not at true widths.
|
|
88
|
+
parts.shapes.push(path("kerb", "kerb", stadiumD(S, R - fig.kerb / 2), { w: fig.kerb, least: fig.kerb * 3 }));
|
|
89
|
+
if (straight !== undefined) {
|
|
90
|
+
const sl = straight.lane;
|
|
91
|
+
for (let k = 0; k <= straight.lanes; k += 1) {
|
|
92
|
+
// The inside line of lane 1 is the straight's inside edge; each other line is the outer edge of the lane before it.
|
|
93
|
+
const y = k === 0 ? R : R + k * sl - w / 2;
|
|
94
|
+
if (k <= o.lanes && sl === o.lane) {
|
|
95
|
+
// Between the bends these are the oval's own lines; the straight's run on beyond them.
|
|
96
|
+
parts.shapes.push(line(`straight-line-${k}`, "lane-lines", straight.x0, y, -S / 2, y, { w }), line(`straight-line-${k}`, "lane-lines", S / 2, y, straight.x1, y, { w }));
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
parts.shapes.push(line(`straight-line-${k}`, "lane-lines", straight.x0, y, straight.x1, y, { w }));
|
|
100
|
+
}
|
|
101
|
+
// The ends of the straight, across it.
|
|
102
|
+
parts.shapes.push(line("straight-end", "lane-lines", straight.x0, R, straight.x0, R + straight.lanes * sl, { w }), line("straight-end", "lane-lines", straight.x1, R, straight.x1, R + straight.lanes * sl, { w }));
|
|
103
|
+
}
|
|
104
|
+
// The finish line: all lanes, at right angles to the lane lines, on the axis through the centre of the second semicircle (Manual 2.2.1.1).
|
|
105
|
+
const lanesAt = straight === undefined ? o.lanes : straight.lanes;
|
|
106
|
+
const sl = straight === undefined ? o.lane : straight.lane;
|
|
107
|
+
parts.shapes.push(line("finish-line", "finish-line", S / 2, R, S / 2, R + lanesAt * sl, { w }));
|
|
108
|
+
// A white line 0.03 m wide and 0.80 m long may be marked in each lane 1 m before the finish line (Manual 2.2.1.6, "Finish Line").
|
|
109
|
+
for (let n = 1; n <= lanesAt; n += 1) {
|
|
110
|
+
const y = R + (n - 0.5) * sl;
|
|
111
|
+
parts.shapes.push(line("finish-tick", "finish-line", S / 2 - 1, y - 0.4, S / 2 - 1, y + 0.4, { w: 0.03, least: 0.08 }));
|
|
112
|
+
if (numbers)
|
|
113
|
+
parts.digits.push({ id: "finish-number", feature: "lane-numbers", text: String(n), x: S / 2 - 2.6, y, h: Math.min(0.6, sl * 0.55) });
|
|
114
|
+
}
|
|
115
|
+
return parts;
|
|
116
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Area, Shape } from "../fields.types.ts";
|
|
2
|
+
import { WA_SHORT_TRACK, WA_STANDARD_TRACK, type AthleticsFigures, type AthleticsLeague } from "../leagues/athletics.ts";
|
|
3
|
+
import { type SoccerSpec } from "../surfaces/soccer-spec.ts";
|
|
4
|
+
import type { SurfaceOptions } from "../types.ts";
|
|
5
|
+
import type { Facility } from "./facilities.ts";
|
|
6
|
+
import { type Digits, type StraightPlan } from "./oval.ts";
|
|
7
|
+
import { type RaceMarks } from "./races.ts";
|
|
8
|
+
import { type Oval } from "./track.ts";
|
|
9
|
+
import type { AthleticsFieldEvent, AthleticsInfield, AthleticsRace, AthleticsStyle, AthleticsTrackLook, RaceInfo, WaterJump } from "./types.ts";
|
|
10
|
+
/** A box in metres. */
|
|
11
|
+
export interface MBox {
|
|
12
|
+
x0: number;
|
|
13
|
+
y0: number;
|
|
14
|
+
x1: number;
|
|
15
|
+
y1: number;
|
|
16
|
+
}
|
|
17
|
+
/** A named part of the plan to crop to, in metres: one box, or one at each end (`left` and `right` as the stadium is drawn wide). */
|
|
18
|
+
export type MRange = MBox | {
|
|
19
|
+
left: MBox;
|
|
20
|
+
right: MBox;
|
|
21
|
+
};
|
|
22
|
+
/** A part of the plan to label (`landmarks`): where it is, in metres, and the side of the drawing its label stands on. */
|
|
23
|
+
export interface MMark {
|
|
24
|
+
id: string;
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
side: "top" | "bottom";
|
|
28
|
+
}
|
|
29
|
+
/** What an athletics drawing is. */
|
|
30
|
+
export interface AthleticsPlan {
|
|
31
|
+
sport: "athletics";
|
|
32
|
+
league: string;
|
|
33
|
+
/** True for the covered 200 m track. */
|
|
34
|
+
indoor: boolean;
|
|
35
|
+
figures: AthleticsFigures;
|
|
36
|
+
oval: Oval;
|
|
37
|
+
lanes: number;
|
|
38
|
+
straight: StraightPlan | undefined;
|
|
39
|
+
track: AthleticsTrackLook;
|
|
40
|
+
infield: AthleticsInfield;
|
|
41
|
+
/** The playing surface: the outer edge of the oval. The data frame's own box. */
|
|
42
|
+
box: MBox;
|
|
43
|
+
/** Everything drawn: the box and what lies beyond it (the jump strip, the runways' ends, the straight). */
|
|
44
|
+
extent: MBox;
|
|
45
|
+
areas: Area[];
|
|
46
|
+
shapes: Shape[];
|
|
47
|
+
digits: Digits[];
|
|
48
|
+
/** The turf: its outline and the lighter bands, for the mown look. */
|
|
49
|
+
turf: {
|
|
50
|
+
d: string;
|
|
51
|
+
stripes: string;
|
|
52
|
+
} | undefined;
|
|
53
|
+
/** The soccer pitch laid inside the track, and where its corner is. */
|
|
54
|
+
pitch: {
|
|
55
|
+
spec: SoccerSpec;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
} | undefined;
|
|
59
|
+
facilities: Facility[];
|
|
60
|
+
races: RaceMarks[];
|
|
61
|
+
/** The steeplechase's lap, when it is drawn. */
|
|
62
|
+
waterJump: WaterJump | undefined;
|
|
63
|
+
events: AthleticsFieldEvent[];
|
|
64
|
+
/** The width the ordinary line is painted at by the rules, and the width it is drawn at unless `exactLines` says otherwise. */
|
|
65
|
+
lineWidth: number;
|
|
66
|
+
drawnLine: number;
|
|
67
|
+
ranges: Readonly<Record<string, MRange>>;
|
|
68
|
+
marks: MMark[];
|
|
69
|
+
/** The race a close-up of a start (`range: "start"`) shows. */
|
|
70
|
+
start: AthleticsRace;
|
|
71
|
+
/** The close-up of the start of that race, when the options ask for one (`range: "start"`): where it is turned about, and the part shown, in the frame in which the athletes run to the right. */
|
|
72
|
+
startView: {
|
|
73
|
+
pivot: {
|
|
74
|
+
x: number;
|
|
75
|
+
y: number;
|
|
76
|
+
heading: number;
|
|
77
|
+
};
|
|
78
|
+
window: MBox;
|
|
79
|
+
} | undefined;
|
|
80
|
+
/** True when the lane numbers are painted. */
|
|
81
|
+
numbers: boolean;
|
|
82
|
+
/** The field event drawn alone at a large scale, when the options ask for one: its facility is the whole plan. */
|
|
83
|
+
layout?: AthleticsFieldEvent;
|
|
84
|
+
}
|
|
85
|
+
/** The league a set of options asks for: the outdoor standard track, or the covered one. */
|
|
86
|
+
export declare function athleticsLeague(o: Pick<SurfaceOptions, "sport" | "league">): AthleticsLeague;
|
|
87
|
+
/** The field events the options ask for: all of them, none, or a list. @throws Error for a name that is not a field event. */
|
|
88
|
+
export declare function eventsOf(style: AthleticsStyle | undefined): AthleticsFieldEvent[];
|
|
89
|
+
/** The races the options ask to be marked. @throws Error for a name that is not a race. */
|
|
90
|
+
export declare function racesOf(style: AthleticsStyle | undefined, indoor?: boolean): RaceInfo[];
|
|
91
|
+
/** The races the covered track marks: the 60 m and its hurdles on the infield straight, and the races of 200 m to 5000 m on the oval. */
|
|
92
|
+
export declare const INDOOR_RACES: readonly AthleticsRace[];
|
|
93
|
+
/** The number of lanes of the oval the options ask for. */
|
|
94
|
+
export declare function athleticsLanes(o: Pick<SurfaceOptions, "sport" | "league" | "athletics">): number;
|
|
95
|
+
/** The standard track and everything the options ask to be marked on it, as a plan. */
|
|
96
|
+
export declare function athleticsPlan(o: Pick<SurfaceOptions, "sport" | "league" | "athletics" | "units" | "pitch" | "detail"> & Partial<Pick<SurfaceOptions, "range" | "ground">>): AthleticsPlan;
|
|
97
|
+
export { WA_SHORT_TRACK, WA_STANDARD_TRACK };
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// What an athletics surface is for a set of options: the track's geometry, the infield, the races and the field events marked, and where each can be cropped to, all as plain data in metres, in
|
|
2
|
+
// the frame of track.ts (the origin the middle of the track, x along the straights, y down the page). Kept apart from the drawing so that the outline, the description, the ranges, the replay,
|
|
3
|
+
// the tests and the drawing agree: a test reads a figure from the plan, not from the markup. Nothing here draws.
|
|
4
|
+
import { lengthUnit } from "../common.js";
|
|
5
|
+
import { num } from "../format.js";
|
|
6
|
+
import { ATHLETICS_LEAGUES, WA_SHORT_TRACK, WA_STANDARD_TRACK } from "../leagues/athletics.js";
|
|
7
|
+
import { chooseLeague } from "../leagues.js";
|
|
8
|
+
import { groundById } from "../grounds.js";
|
|
9
|
+
import { soccerSpec } from "../surfaces/soccer-spec.js";
|
|
10
|
+
import { FIELD_EVENT_IDS, RACES, RACE_IDS, raceById } from "./events.js";
|
|
11
|
+
import { infieldParts, jumpStrip, ARENA } from "./infield.js";
|
|
12
|
+
import { addParts, emptyParts, indoorStraightOf, indoorStraightParts, outerRadius, ovalParts, stadiumD, straightOf } from "./oval.js";
|
|
13
|
+
import { layoutOf } from "./layout.js";
|
|
14
|
+
import { markRace, startPivot, startPoints } from "./races.js";
|
|
15
|
+
import { parseRange } from "../ranges.names.js";
|
|
16
|
+
import { steeplechaseParts, waterPitBox } from "./steeplechase.js";
|
|
17
|
+
import { laneEdge } from "./track.js";
|
|
18
|
+
const isAthleticsLeague = (l) => typeof l === "object" && l !== null && l.family === "athletics";
|
|
19
|
+
/** The league a set of options asks for: the outdoor standard track, or the covered one. */
|
|
20
|
+
export function athleticsLeague(o) {
|
|
21
|
+
const { league } = chooseLeague(o);
|
|
22
|
+
return isAthleticsLeague(league) ? league : ATHLETICS_LEAGUES[0];
|
|
23
|
+
}
|
|
24
|
+
const TRACKS = ["red", "blue", "terracotta", "green"];
|
|
25
|
+
const INFIELDS = ["pitch", "grass", "none"];
|
|
26
|
+
/** A whole number of lanes within a range, or the drawn one. @throws RangeError for a number outside the range the rules allow. */
|
|
27
|
+
function lanesIn(asked, range, drawn, label) {
|
|
28
|
+
if (asked === undefined)
|
|
29
|
+
return drawn;
|
|
30
|
+
if (typeof asked !== "number" || !Number.isInteger(asked))
|
|
31
|
+
throw new RangeError(`${label} must be a whole number`);
|
|
32
|
+
if (asked < range.min || asked > range.max)
|
|
33
|
+
throw new RangeError(`${label} of ${asked} is outside what the rules allow: ${range.min} to ${range.max}`);
|
|
34
|
+
return asked;
|
|
35
|
+
}
|
|
36
|
+
/** The field events the options ask for: all of them, none, or a list. @throws Error for a name that is not a field event. */
|
|
37
|
+
export function eventsOf(style) {
|
|
38
|
+
const asked = style?.events;
|
|
39
|
+
if (asked === undefined || asked === true)
|
|
40
|
+
return [...FIELD_EVENT_IDS];
|
|
41
|
+
if (asked === false)
|
|
42
|
+
return [];
|
|
43
|
+
if (!Array.isArray(asked))
|
|
44
|
+
throw new TypeError("athletics.events is true, false or a list of field events");
|
|
45
|
+
for (const e of asked)
|
|
46
|
+
if (!FIELD_EVENT_IDS.includes(e))
|
|
47
|
+
throw new Error(`Unknown field event "${String(e)}": use one of ${FIELD_EVENT_IDS.join(", ")}`);
|
|
48
|
+
return FIELD_EVENT_IDS.filter((e) => asked.includes(e));
|
|
49
|
+
}
|
|
50
|
+
/** The races the options ask to be marked. @throws Error for a name that is not a race. */
|
|
51
|
+
export function racesOf(style, indoor = false) {
|
|
52
|
+
const asked = style?.races;
|
|
53
|
+
if (asked === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
const here = (r) => (indoor ? INDOOR_RACES.includes(r.id) : r.indoor !== true);
|
|
56
|
+
if (asked === "all")
|
|
57
|
+
return RACES.filter(here);
|
|
58
|
+
if (!Array.isArray(asked))
|
|
59
|
+
throw new TypeError('athletics.races is "all" or a list of races');
|
|
60
|
+
for (const r of asked)
|
|
61
|
+
if (raceById(r) === undefined)
|
|
62
|
+
throw new Error(`Unknown race "${String(r)}": use one of ${RACE_IDS.join(", ")}`);
|
|
63
|
+
const list = RACES.filter((r) => asked.includes(r.id));
|
|
64
|
+
for (const r of list)
|
|
65
|
+
if (!here(r))
|
|
66
|
+
throw new RangeError(indoor ? `The covered 200 m track does not mark the ${r.id}: use one of ${INDOOR_RACES.join(", ")}` : `The ${r.id} is run on the covered track: the 400 m track marks the other races`);
|
|
67
|
+
return list;
|
|
68
|
+
}
|
|
69
|
+
/** The races the covered track marks: the 60 m and its hurdles on the infield straight, and the races of 200 m to 5000 m on the oval. */
|
|
70
|
+
export const INDOOR_RACES = Object.freeze(["60m", "60m-hurdles", "200m", "400m", "800m", "1000m", "1500m", "mile", "3000m", "5000m"]);
|
|
71
|
+
/** Round a box out by `m`. */
|
|
72
|
+
const grow = (b, m) => ({ x0: b.x0 - m, y0: b.y0 - m, x1: b.x1 + m, y1: b.y1 + m });
|
|
73
|
+
const union = (boxes) => ({ x0: Math.min(...boxes.map((b) => b.x0)), y0: Math.min(...boxes.map((b) => b.y0)), x1: Math.max(...boxes.map((b) => b.x1)), y1: Math.max(...boxes.map((b) => b.y1)) });
|
|
74
|
+
/** The lighter bands of the mown turf, aligned to a pitch of 105 m in 19 bands, so that the grass round a pitch is mown as the pitch is (the soccer builder's `bands`). */
|
|
75
|
+
function turfBands(x0, x1, y0, y1) {
|
|
76
|
+
const band = 105 / 19;
|
|
77
|
+
const left = -52.5;
|
|
78
|
+
let d = "";
|
|
79
|
+
for (let i = -3; i < 22; i += 1) {
|
|
80
|
+
if (Math.abs(i) % 2 !== 1)
|
|
81
|
+
continue;
|
|
82
|
+
const a = left + i * band;
|
|
83
|
+
const b = a + band;
|
|
84
|
+
if (b <= x0 || a >= x1)
|
|
85
|
+
continue;
|
|
86
|
+
d += `M${num(Math.max(a, x0))} ${num(y0)}H${num(Math.min(b, x1))}V${num(y1)}H${num(Math.max(a, x0))}Z`;
|
|
87
|
+
}
|
|
88
|
+
return d;
|
|
89
|
+
}
|
|
90
|
+
/** The turf of the standard stadium: the oval's inside, cut off at each end where the synthetic surface of the segments begins (Marking Plan; Manual 2.2.1.1). */
|
|
91
|
+
function turfOf(o) {
|
|
92
|
+
const S = o.straight;
|
|
93
|
+
const R = o.radius;
|
|
94
|
+
const xg = S / 2 + ARENA.turf;
|
|
95
|
+
const yc = Math.sqrt(R * R - ARENA.turf * ARENA.turf);
|
|
96
|
+
const d = `M${num(-xg)} ${num(-yc)}A${num(R)} ${num(R)} 0 0 1 ${num(-S / 2)} ${num(-R)}H${num(S / 2)}A${num(R)} ${num(R)} 0 0 1 ${num(xg)} ${num(-yc)}V${num(yc)}A${num(R)} ${num(R)} 0 0 1 ${num(S / 2)} ${num(R)}H${num(-S / 2)}A${num(R)} ${num(R)} 0 0 1 ${num(-xg)} ${num(yc)}Z`;
|
|
97
|
+
return { d };
|
|
98
|
+
}
|
|
99
|
+
/** The number of lanes of the oval the options ask for. */
|
|
100
|
+
export function athleticsLanes(o) {
|
|
101
|
+
const fig = athleticsLeague(o).dimensions;
|
|
102
|
+
return lanesIn(o.athletics?.lanes, fig.lanes, fig.lanes.drawn, "The number of lanes");
|
|
103
|
+
}
|
|
104
|
+
/** The standard track and everything the options ask to be marked on it, as a plan. */
|
|
105
|
+
export function athleticsPlan(o) {
|
|
106
|
+
const league = athleticsLeague(o);
|
|
107
|
+
const fig = league.dimensions;
|
|
108
|
+
const indoor = fig.banking !== null;
|
|
109
|
+
const style = o.athletics;
|
|
110
|
+
const lanes = lanesIn(style?.lanes, fig.lanes, fig.lanes.drawn, "The number of lanes");
|
|
111
|
+
const straightLanes = lanesIn(style?.straightLanes, fig.straightLanes, Math.max(lanes, fig.straightLanes.min), "The number of lanes of the straight");
|
|
112
|
+
const track = style?.track === undefined ? "red" : style.track;
|
|
113
|
+
if (!TRACKS.includes(track))
|
|
114
|
+
throw new Error(`Unknown track look "${String(track)}": use one of ${TRACKS.join(", ")}`);
|
|
115
|
+
const infield = style?.infield === undefined ? (indoor ? "none" : "pitch") : style.infield;
|
|
116
|
+
if (!INFIELDS.includes(infield))
|
|
117
|
+
throw new Error(`Unknown infield "${String(infield)}": use one of ${INFIELDS.join(", ")}`);
|
|
118
|
+
const layoutEvent = style?.layout;
|
|
119
|
+
if (layoutEvent !== undefined && !FIELD_EVENT_IDS.includes(layoutEvent))
|
|
120
|
+
throw new Error(`Unknown field event "${String(layoutEvent)}": use one of ${FIELD_EVENT_IDS.join(", ")}`);
|
|
121
|
+
const events = indoor ? [] : eventsOf(style);
|
|
122
|
+
const races = racesOf(style, indoor);
|
|
123
|
+
const startRace = style?.start === undefined ? "400m" : style.start;
|
|
124
|
+
if (raceById(startRace) === undefined)
|
|
125
|
+
throw new Error(`Unknown race "${String(startRace)}": use one of ${RACE_IDS.join(", ")}`);
|
|
126
|
+
const wj = style?.waterJump === undefined ? "inside" : style.waterJump;
|
|
127
|
+
if (wj !== "inside" && wj !== "outside" && wj !== "none")
|
|
128
|
+
throw new Error(`Unknown water jump "${String(wj)}": use inside, outside or none`);
|
|
129
|
+
const numbers = style?.numbers !== false;
|
|
130
|
+
const parsed = o.range === undefined ? undefined : parseRange({ sport: "athletics", league: o.league, range: o.range });
|
|
131
|
+
const closeUp = parsed?.kind === "name" && parsed.name === "start";
|
|
132
|
+
const oval = { straight: fig.straight, radius: fig.radius, lane: fig.lane, lanes, line: fig.line, inner: fig.inner, outer: fig.outer };
|
|
133
|
+
if (layoutEvent !== undefined)
|
|
134
|
+
return layoutPlan(layoutEvent, league, oval, lanes, track, numbers);
|
|
135
|
+
const R = oval.radius;
|
|
136
|
+
const S = oval.straight;
|
|
137
|
+
const Rout = outerRadius(oval);
|
|
138
|
+
const longest = indoor ? 60 : 110;
|
|
139
|
+
const straight = indoor ? indoorStraightOf(oval, fig, straightLanes, 60) : straightOf(oval, fig, straightLanes, longest);
|
|
140
|
+
const parts = emptyParts();
|
|
141
|
+
const D = turfOf(oval);
|
|
142
|
+
// The ground inside the track: the synthetic surface of the segments, and the turf between them (Marking Plan).
|
|
143
|
+
addParts(parts, ovalParts(oval, fig, indoor ? undefined : straight, numbers, indoor ? undefined : D.d));
|
|
144
|
+
if (indoor) {
|
|
145
|
+
// The covered track's bends are banked (Rule 43): shaded, as a plan shows a slope.
|
|
146
|
+
const rr = Rout;
|
|
147
|
+
parts.areas.push({ id: "banking-east", feature: "banking", cls: "ky-ath-bank", d: `M${num(S / 2)} ${num(-rr)}A${num(rr)} ${num(rr)} 0 0 1 ${num(S / 2)} ${num(rr)}V${num(R)}A${num(R)} ${num(R)} 0 0 0 ${num(S / 2)} ${num(-R)}Z` }, { id: "banking-west", feature: "banking", cls: "ky-ath-bank", d: `M${num(-S / 2)} ${num(rr)}A${num(rr)} ${num(rr)} 0 0 1 ${num(-S / 2)} ${num(-rr)}V${num(-R)}A${num(R)} ${num(R)} 0 0 0 ${num(-S / 2)} ${num(R)}Z` });
|
|
148
|
+
addParts(parts, indoorStraightParts(straight, numbers, fig.line));
|
|
149
|
+
}
|
|
150
|
+
const turf = infield === "none" || indoor ? undefined : { d: D.d, stripes: turfBands(-S / 2 - ARENA.turf, S / 2 + ARENA.turf, -R, R) };
|
|
151
|
+
// The pitch inside the track: the options', else the size the ground's article publishes, else the international 105 by 68 m.
|
|
152
|
+
const ground = groundById(o.ground);
|
|
153
|
+
const groundPitch = ground === undefined || ground.pitchSize === "default" ? undefined : { ...ground.pitch, unit: "m" };
|
|
154
|
+
const spec = soccerSpec(o.athletics?.pitch ?? o.pitch ?? groundPitch, "m");
|
|
155
|
+
let pitch = infield === "pitch" && !indoor ? { spec, x: -spec.length / 2, y: -spec.width / 2 } : undefined;
|
|
156
|
+
void lengthUnit;
|
|
157
|
+
// The races: the starts and marks of each.
|
|
158
|
+
const raceMarks = [];
|
|
159
|
+
const ctx = { oval, fig, lanes, straight, numbers, numberHeight: 0.95 };
|
|
160
|
+
const sc = races.filter((r) => r.kind === "steeplechase");
|
|
161
|
+
const raceIds = new Set(races.map((r) => r.id));
|
|
162
|
+
if (!raceIds.has(startRace) && (o.athletics?.start !== undefined || closeUp))
|
|
163
|
+
raceIds.add(startRace);
|
|
164
|
+
for (const race of RACES) {
|
|
165
|
+
if (!raceIds.has(race.id))
|
|
166
|
+
continue;
|
|
167
|
+
if (race.kind === "steeplechase")
|
|
168
|
+
continue;
|
|
169
|
+
raceMarks.push(markRace(ctx, race));
|
|
170
|
+
}
|
|
171
|
+
let waterJump;
|
|
172
|
+
if (sc.length > 0 && wj !== "none") {
|
|
173
|
+
waterJump = wj;
|
|
174
|
+
let first = true;
|
|
175
|
+
for (const race of sc) {
|
|
176
|
+
const made = steeplechaseParts(ctx, wj, race, first);
|
|
177
|
+
first = false;
|
|
178
|
+
addParts(parts, made.parts);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const r of raceMarks)
|
|
182
|
+
addParts(parts, r.parts);
|
|
183
|
+
let startView;
|
|
184
|
+
if (closeUp) {
|
|
185
|
+
const marks = raceMarks.find((r) => r.race.id === startRace);
|
|
186
|
+
if (marks !== undefined) {
|
|
187
|
+
const pivot = startPivot(oval, marks);
|
|
188
|
+
const pts = startPoints(oval, marks, straight);
|
|
189
|
+
const c = Math.cos(pivot.heading);
|
|
190
|
+
const sn = Math.sin(pivot.heading);
|
|
191
|
+
const rot = pts.map(([x, y]) => [(x - pivot.x) * c + (y - pivot.y) * sn, -(x - pivot.x) * sn + (y - pivot.y) * c]);
|
|
192
|
+
const xs = rot.map((p) => p[0]);
|
|
193
|
+
const ys = rot.map((p) => p[1]);
|
|
194
|
+
const side = marks.race.kind === "straight" ? 0.8 : 3;
|
|
195
|
+
const win = { x0: Math.min(...xs) - (marks.race.kind === "straight" ? 7 : 9), x1: Math.max(...xs) + (marks.race.kind === "straight" ? 7 : 9), y0: Math.min(...ys) - side, y1: Math.max(...ys) + side };
|
|
196
|
+
startView = { pivot, window: win };
|
|
197
|
+
// The pitch is left out of a close-up that does not reach it.
|
|
198
|
+
if (pitch !== undefined) {
|
|
199
|
+
const corners = [[win.x0, win.y0], [win.x1, win.y0], [win.x1, win.y1], [win.x0, win.y1]].map(([x, y]) => [pivot.x + x * c - y * sn, pivot.y + x * sn + y * c]);
|
|
200
|
+
const gx0 = Math.min(...corners.map((p) => p[0]));
|
|
201
|
+
const gx1 = Math.max(...corners.map((p) => p[0]));
|
|
202
|
+
const gy0 = Math.min(...corners.map((p) => p[1]));
|
|
203
|
+
const gy1 = Math.max(...corners.map((p) => p[1]));
|
|
204
|
+
if (gx1 < pitch.x || gx0 > pitch.x + pitch.spec.length || gy1 < pitch.y || gy0 > pitch.y + pitch.spec.width)
|
|
205
|
+
pitch = undefined;
|
|
206
|
+
}
|
|
207
|
+
// On a bend the numbers painted on the track would be specks beside the fan of the starts: they become tags that read at the size of the view.
|
|
208
|
+
if (marks.race.kind !== "straight") {
|
|
209
|
+
// A tag's disc is 1.56 times its figure's height across and must sit inside a lane (1.22 m): the figure is 0.66 m, a disc of 1.03 m.
|
|
210
|
+
const tag = 0.66;
|
|
211
|
+
for (const d of marks.parts.digits) {
|
|
212
|
+
if (d.id !== "start-number")
|
|
213
|
+
continue;
|
|
214
|
+
const back = tag * 0.78 + 0.35;
|
|
215
|
+
const at = d.from === undefined ? [d.x, d.y] : [d.from[0] - d.from[2] * back, d.from[1] - d.from[3] * back];
|
|
216
|
+
Object.assign(d, { h: tag, tag: true, x: at[0], y: at[1] });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// The field events.
|
|
222
|
+
const inf = indoor ? { parts: emptyParts(), facilities: [] } : infieldParts(oval, new Set(events));
|
|
223
|
+
addParts(parts, inf.parts);
|
|
224
|
+
const box = { x0: -S / 2 - Rout, y0: -Rout, x1: S / 2 + Rout, y1: Rout };
|
|
225
|
+
let extent = grow(box, 1);
|
|
226
|
+
const edgeStraight = R + straight.lanes * straight.lane;
|
|
227
|
+
if (!indoor)
|
|
228
|
+
extent = union([extent, { x0: straight.x0, y0: -Rout, x1: straight.x1, y1: edgeStraight }]);
|
|
229
|
+
if (events.includes("long-jump") || events.includes("triple-jump"))
|
|
230
|
+
extent = union([extent, jumpStrip()]);
|
|
231
|
+
if (events.includes("javelin-throw"))
|
|
232
|
+
extent = union([extent, ...inf.facilities.filter((f) => f.event === "javelin-throw").map((f) => f.box)].map((b) => ({ x0: Math.max(b.x0, -S / 2 - Rout - 6), y0: b.y0, x1: Math.min(b.x1, S / 2 + Rout + 6), y1: b.y1 })));
|
|
233
|
+
extent = grow(extent, 0);
|
|
234
|
+
const plan = {
|
|
235
|
+
sport: "athletics",
|
|
236
|
+
league: league.id,
|
|
237
|
+
indoor,
|
|
238
|
+
figures: fig,
|
|
239
|
+
oval,
|
|
240
|
+
lanes,
|
|
241
|
+
straight,
|
|
242
|
+
track,
|
|
243
|
+
infield,
|
|
244
|
+
box,
|
|
245
|
+
extent,
|
|
246
|
+
areas: parts.areas,
|
|
247
|
+
shapes: parts.shapes,
|
|
248
|
+
digits: parts.digits,
|
|
249
|
+
turf,
|
|
250
|
+
pitch,
|
|
251
|
+
facilities: inf.facilities,
|
|
252
|
+
races: raceMarks,
|
|
253
|
+
waterJump,
|
|
254
|
+
events,
|
|
255
|
+
lineWidth: fig.line,
|
|
256
|
+
drawnLine: Math.max(fig.line, (box.x1 - box.x0) * 0.0009),
|
|
257
|
+
ranges: {},
|
|
258
|
+
marks: [],
|
|
259
|
+
start: startRace,
|
|
260
|
+
startView,
|
|
261
|
+
numbers,
|
|
262
|
+
};
|
|
263
|
+
plan.ranges = rangesOf(plan);
|
|
264
|
+
plan.marks.push(...marksOf(plan));
|
|
265
|
+
return plan;
|
|
266
|
+
}
|
|
267
|
+
/** The plan of one field event alone, at a large scale: its facility in its own frame (the athlete throws or jumps to the right from the origin), with the box round it as the playing surface. */
|
|
268
|
+
function layoutPlan(event, league, oval, lanes, track, numbers) {
|
|
269
|
+
const lay = layoutOf(event);
|
|
270
|
+
const box = grow(lay.box, 0);
|
|
271
|
+
const width = box.x1 - box.x0;
|
|
272
|
+
const plan = {
|
|
273
|
+
sport: "athletics",
|
|
274
|
+
league: league.id,
|
|
275
|
+
indoor: false,
|
|
276
|
+
figures: league.dimensions,
|
|
277
|
+
oval,
|
|
278
|
+
lanes,
|
|
279
|
+
straight: undefined,
|
|
280
|
+
track,
|
|
281
|
+
infield: "none",
|
|
282
|
+
box,
|
|
283
|
+
extent: box,
|
|
284
|
+
areas: lay.parts.areas,
|
|
285
|
+
shapes: lay.parts.shapes.map((sh) => (sh.kind !== "dot" && sh.kind !== "bar" && sh.cls === "ky-ath-s" ? { ...sh, least: Math.max(0.07, width * 0.0011) } : sh)),
|
|
286
|
+
digits: [],
|
|
287
|
+
turf: undefined,
|
|
288
|
+
pitch: undefined,
|
|
289
|
+
facilities: [lay.facility],
|
|
290
|
+
races: [],
|
|
291
|
+
waterJump: undefined,
|
|
292
|
+
events: [event],
|
|
293
|
+
lineWidth: 0.05,
|
|
294
|
+
drawnLine: Math.max(0.05, width * 0.0016),
|
|
295
|
+
ranges: {},
|
|
296
|
+
marks: lay.marks,
|
|
297
|
+
start: "400m",
|
|
298
|
+
startView: undefined,
|
|
299
|
+
numbers,
|
|
300
|
+
layout: event,
|
|
301
|
+
};
|
|
302
|
+
return plan;
|
|
303
|
+
}
|
|
304
|
+
/** The union of the boxes of the facilities of one event on one side of the stadium. */
|
|
305
|
+
function facilityBox(plan, event, side) {
|
|
306
|
+
const list = plan.facilities.filter((f) => f.event === event && (side === "all" || (side === "left" ? f.x < 0 : f.x >= 0)));
|
|
307
|
+
return list.length === 0 ? undefined : grow(union(list.map((f) => f.box)), 1.5);
|
|
308
|
+
}
|
|
309
|
+
/** The named ranges of the plan, in metres. */
|
|
310
|
+
function rangesOf(plan) {
|
|
311
|
+
const o = plan.oval;
|
|
312
|
+
const S = o.straight;
|
|
313
|
+
const R = o.radius;
|
|
314
|
+
const Rout = plan.box.y1;
|
|
315
|
+
const out = {};
|
|
316
|
+
const outer = plan.indoor || plan.straight === undefined;
|
|
317
|
+
const edge = outer ? R + plan.lanes * o.lane : R + plan.straight.lanes * plan.straight.lane;
|
|
318
|
+
out.bend = { left: { x0: -S / 2 - Rout, y0: -Rout, x1: -S / 2, y1: Rout }, right: { x0: S / 2, y0: -Rout, x1: S / 2 + Rout, y1: Rout } };
|
|
319
|
+
out["home-straight"] = outer ? { x0: -S / 2, y0: R - 1, x1: S / 2, y1: edge + 1 } : { x0: plan.straight.x0, y0: R - 1, x1: plan.straight.x1, y1: edge + 1 };
|
|
320
|
+
out["back-straight"] = { x0: -S / 2, y0: -Rout, x1: S / 2, y1: -R + 1 };
|
|
321
|
+
out.finish = { x0: S / 2 - 20, y0: R - 2, x1: S / 2 + 10, y1: edge + 2 };
|
|
322
|
+
out.infield = { x0: -S / 2 - R, y0: -R, x1: S / 2 + R, y1: R };
|
|
323
|
+
for (const e of FIELD_EVENT_IDS) {
|
|
324
|
+
const left = facilityBox(plan, e, "left");
|
|
325
|
+
const right = facilityBox(plan, e, "right");
|
|
326
|
+
if (left !== undefined && right !== undefined)
|
|
327
|
+
out[e] = { left, right };
|
|
328
|
+
else if (left !== undefined || right !== undefined)
|
|
329
|
+
out[e] = (left ?? right);
|
|
330
|
+
}
|
|
331
|
+
// The water jump's pit, wherever the jump is (the steeplechase's lap is drawn only when a steeplechase is marked): the inside one by default.
|
|
332
|
+
const wj = plan.waterJump ?? "inside";
|
|
333
|
+
const pit = waterPitBox(plan.oval, wj);
|
|
334
|
+
out["water-jump"] = grow(pit, 5);
|
|
335
|
+
void laneEdge;
|
|
336
|
+
void stadiumD;
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
/** The parts of the plan to label. */
|
|
340
|
+
function marksOf(plan) {
|
|
341
|
+
const o = plan.oval;
|
|
342
|
+
const S = o.straight;
|
|
343
|
+
const R = o.radius;
|
|
344
|
+
const marks = [
|
|
345
|
+
{ id: "finish-line", x: S / 2, y: R + 0.6 * o.lane, side: "bottom" },
|
|
346
|
+
{ id: "kerb", x: -S / 4, y: -R, side: "top" },
|
|
347
|
+
{ id: "lane-lines", x: -S / 4, y: R + o.lane, side: "bottom" },
|
|
348
|
+
{ id: "bend", x: S / 2 + R * 0.7071 + 2, y: -R * 0.7071, side: "top" },
|
|
349
|
+
{ id: "straight", x: 0, y: -R - 3, side: "top" },
|
|
350
|
+
];
|
|
351
|
+
if (plan.pitch !== undefined)
|
|
352
|
+
marks.push({ id: "infield-pitch", x: 0, y: -plan.pitch.spec.width / 2, side: "top" });
|
|
353
|
+
return marks;
|
|
354
|
+
}
|
|
355
|
+
export { WA_SHORT_TRACK, WA_STANDARD_TRACK };
|