@johnmorrisdotca/kyogijo-stadiums 0.24.0 → 0.27.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 +39 -0
- package/NOTICE.md +27 -0
- package/README.md +95 -59
- package/dist/crowd.d.ts +56 -2
- package/dist/crowd.draw.d.ts +3 -1
- package/dist/crowd.draw.js +27 -3
- package/dist/crowd.js +18 -10
- package/dist/district-entry.d.ts +5 -0
- package/dist/district-entry.js +4 -0
- package/dist/district.d.ts +167 -0
- package/dist/district.js +291 -0
- package/dist/district.turn.d.ts +27 -0
- package/dist/district.turn.js +157 -0
- package/dist/draw.js +22 -2
- package/dist/flythrough-entry.d.ts +8 -0
- package/dist/flythrough-entry.js +6 -0
- package/dist/flythrough.dom.d.ts +103 -0
- package/dist/flythrough.dom.js +517 -0
- package/dist/flythrough.markup.d.ts +48 -0
- package/dist/flythrough.markup.js +86 -0
- package/dist/flythrough.path.d.ts +145 -0
- package/dist/flythrough.path.js +253 -0
- package/dist/flythrough.style.d.ts +2 -0
- package/dist/flythrough.style.js +40 -0
- package/dist/flythrough.words.d.ts +33 -0
- package/dist/flythrough.words.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/stadiums-entry.d.ts +2 -0
- package/dist/stadiums-entry.js +2 -0
- package/dist/stadiums.d.ts +158 -0
- package/dist/stadiums.data.d.ts +2 -0
- package/dist/stadiums.data.js +11 -0
- package/dist/stadiums.js +210 -0
- package/dist/stadiums.osm.data.d.ts +2 -0
- package/dist/stadiums.osm.data.js +58 -0
- package/dist/strings.d.ts +17 -0
- package/dist/strings.js +2 -2
- package/dist/turn.kit.d.ts +5 -0
- package/dist/turn.markup.d.ts +7 -0
- package/dist/turn.markup.js +38 -2
- package/dist/turn.model.d.ts +4 -0
- package/dist/turn.model.js +3 -1
- package/dist/turn.style.js +6 -0
- package/dist/types.d.ts +11 -0
- package/dist/venue.d.ts +2 -0
- package/dist/venue.js +7 -4
- package/dist/venue.real.d.ts +21 -0
- package/dist/venue.real.js +150 -0
- package/dist/venues-entry.d.ts +2 -0
- package/dist/venues-entry.js +2 -0
- package/dist/venues.d.ts +153 -0
- package/dist/venues.data.d.ts +2 -0
- package/dist/venues.data.js +74 -0
- package/dist/venues.geo.d.ts +60 -0
- package/dist/venues.geo.js +211 -0
- package/dist/venues.js +213 -0
- package/dist/venues.osm.data.d.ts +4 -0
- package/dist/venues.osm.data.js +363 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type Box, type Pt, type RealBowl } from "./crowd.ts";
|
|
2
|
+
import { type Ground } from "./grounds.ts";
|
|
3
|
+
import { VENUE_ATTRIBUTION } from "./venues.ts";
|
|
4
|
+
export { VENUE_ATTRIBUTION };
|
|
5
|
+
/** How a fact is known: `documented` is what a source cited with it says; `plausible` is ours, for a bowl no source describes. */
|
|
6
|
+
export type StadiumBasis = "documented" | "plausible";
|
|
7
|
+
/** The configurations of a stadium: the field the seats are counted for. `cfl` is the football field of the Canadian league, `soccer` a soccer pitch. */
|
|
8
|
+
export type StadiumConfigId = "cfl" | "soccer";
|
|
9
|
+
/** The kinds of roof the stadiums have: `cable` is a cable-supported roof with an opening over the field that a retractable fabric closes; `fixed` a lid over the bowl. */
|
|
10
|
+
export type StadiumRoof = "cable" | "fixed";
|
|
11
|
+
/** What one configuration of a stadium is: its sport and its seats, and where the figure was read. */
|
|
12
|
+
export interface StadiumConfigData {
|
|
13
|
+
sport: "cfl" | "soccer";
|
|
14
|
+
/** The seats for this configuration: the infobox's figure for it, or Wikidata's. */
|
|
15
|
+
seats: number;
|
|
16
|
+
source: "wikidata" | "wikipedia";
|
|
17
|
+
/** What was read, in a few words of ours. */
|
|
18
|
+
read: string;
|
|
19
|
+
}
|
|
20
|
+
/** The facts of a stadium: the seats of each configuration, the number of bowls and the kind of roof, each with what a source says. No text of an article is kept. */
|
|
21
|
+
export interface RealStadiumData {
|
|
22
|
+
/** The key: `bc-place`, the key of the stadium's ground in `GROUNDS`. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** The Wikidata item of the stadium. */
|
|
25
|
+
wikidata: string;
|
|
26
|
+
/** The key of the stadium's ground in `GROUNDS` (its name, its place, its pitch). */
|
|
27
|
+
ground: string;
|
|
28
|
+
/** The English Wikipedia article the facts that are not Wikidata's were read from, at a revision. */
|
|
29
|
+
en: {
|
|
30
|
+
title: string;
|
|
31
|
+
revision: number;
|
|
32
|
+
};
|
|
33
|
+
configs: Record<StadiumConfigId, StadiumConfigData>;
|
|
34
|
+
tiers: {
|
|
35
|
+
/** The seating levels (bowls). */
|
|
36
|
+
count: 1 | 2 | 3;
|
|
37
|
+
basis: StadiumBasis;
|
|
38
|
+
/** The seats of the lowest bowl, where a source gives them: the lower tier's band holds this many whatever the configuration (the upper bowl is what is opened or closed for the others). */
|
|
39
|
+
lowerSeats?: number;
|
|
40
|
+
read: string;
|
|
41
|
+
};
|
|
42
|
+
roof: {
|
|
43
|
+
kind: StadiumRoof;
|
|
44
|
+
basis: StadiumBasis;
|
|
45
|
+
read: string; /** A cable roof's opening over the field, as the article gives it, in metres. */
|
|
46
|
+
opening?: {
|
|
47
|
+
length: number;
|
|
48
|
+
width: number;
|
|
49
|
+
unit: "m";
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** What OpenStreetMap gave of a stadium: ODbL 1.0 data, "(c) OpenStreetMap contributors". */
|
|
54
|
+
export interface StadiumOsm {
|
|
55
|
+
/** The element the outline is: `way/24705904`. */
|
|
56
|
+
source: string;
|
|
57
|
+
read: string;
|
|
58
|
+
base: string;
|
|
59
|
+
licence: "ODbL-1.0";
|
|
60
|
+
attribution: string;
|
|
61
|
+
/** The roof OpenStreetMap maps over the building: its heights in metres (`height` to the top of the roof, `rise` of that the roof's own) and the opening in it as `[longitude, latitude]`. */
|
|
62
|
+
roof?: {
|
|
63
|
+
source: string;
|
|
64
|
+
height?: number;
|
|
65
|
+
rise?: number;
|
|
66
|
+
outer?: string;
|
|
67
|
+
opening?: {
|
|
68
|
+
source: string;
|
|
69
|
+
ring: [number, number][];
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** The building's outline as `[longitude, latitude]`, not repeating its first point. */
|
|
73
|
+
ring: [number, number][];
|
|
74
|
+
/**
|
|
75
|
+
* How the field lies, worked out when the data was made: the heading of its long side (degrees clockwise from north, 0 to under 180), how that is known (`opening`: from the roof's opening, which the article says is
|
|
76
|
+
* the field's size; `building`: from the long axis of the building's main hall, an inference; `none`: no direction, 90 is stood in), how much of a direction the building has (`strength`) and how much longer than
|
|
77
|
+
* wide the opening or the hall is (`elongation`), and the middle of the field as `[longitude, latitude]`.
|
|
78
|
+
*/
|
|
79
|
+
axis: {
|
|
80
|
+
heading: number;
|
|
81
|
+
basis: "opening" | "building" | "none";
|
|
82
|
+
strength: number;
|
|
83
|
+
elongation: number;
|
|
84
|
+
centre: [number, number];
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** A stadium: its facts and what OpenStreetMap gave. */
|
|
88
|
+
export interface RealStadium extends RealStadiumData {
|
|
89
|
+
osm: StadiumOsm;
|
|
90
|
+
}
|
|
91
|
+
/** How the field lies in the building: its long side's heading, how that is known, and the way north points on the page (degrees clockwise from the top). */
|
|
92
|
+
export interface StadiumOrientation {
|
|
93
|
+
heading: number;
|
|
94
|
+
basis: "opening" | "building" | "none";
|
|
95
|
+
north: number;
|
|
96
|
+
}
|
|
97
|
+
/** The stadiums by key. */
|
|
98
|
+
export declare const STADIUMS: Readonly<Record<string, RealStadium>>;
|
|
99
|
+
/** The keys of `STADIUMS`, in the order of the data. */
|
|
100
|
+
export declare const STADIUM_IDS: readonly string[];
|
|
101
|
+
/** The stadium with a key, or `undefined`. */
|
|
102
|
+
export declare const stadiumById: (id: string | undefined) => RealStadium | undefined;
|
|
103
|
+
/** The stadium whose Wikidata item is `Q612227`, or `undefined`. */
|
|
104
|
+
export declare const stadiumByWikidata: (wikidata: string | undefined) => RealStadium | undefined;
|
|
105
|
+
/** The ground a stadium is (its name, place and pitch). */
|
|
106
|
+
export declare const groundOfStadium: (stadium: RealStadium) => Ground;
|
|
107
|
+
/** How the field lies in a stadium's building (see `StadiumOsm.axis`). */
|
|
108
|
+
export declare const stadiumOrientation: (stadium: RealStadium) => StadiumOrientation;
|
|
109
|
+
/** The configuration a sport is: `cfl` or `soccer`, or `undefined` for a sport a stadium has no configuration for. */
|
|
110
|
+
export declare const stadiumConfigOf: (sport: string | undefined) => StadiumConfigId | undefined;
|
|
111
|
+
/** The seats of a stadium for a configuration. */
|
|
112
|
+
export declare const stadiumSeats: (stadium: RealStadium, config: StadiumConfigId) => number;
|
|
113
|
+
/** The height of the first row above the field, in feet, the rise of a bowl's rows by the number of bowls (what the lowest, a middle and the upper rise, at most), and the rise of the walk between two bowls (a suite level): ours, no source gives them. */
|
|
114
|
+
export declare const STADIUM_FIRST_HEIGHT = 5;
|
|
115
|
+
export declare const STADIUM_RISES: Readonly<Record<1 | 2 | 3, readonly number[]>>;
|
|
116
|
+
export declare const STADIUM_WALK_RISE = 24;
|
|
117
|
+
/** The height a roof's rim stands over the top row, at the least, in feet. */
|
|
118
|
+
export declare const STADIUM_CLEARANCE = 22;
|
|
119
|
+
/** The plan of a stadium for a configuration: its building in the drawing's frame, the bowl fitted inside, and how it lies. */
|
|
120
|
+
export interface StadiumPlan {
|
|
121
|
+
stadium: RealStadium;
|
|
122
|
+
config: StadiumConfigId;
|
|
123
|
+
orientation: StadiumOrientation;
|
|
124
|
+
/** The field, in the drawing's frame. */
|
|
125
|
+
box: Box;
|
|
126
|
+
/** The building's outline in feet, in the drawing's frame (the field's long side along x, in the middle of the building's hall). */
|
|
127
|
+
footprint: Pt[];
|
|
128
|
+
/** The roof's opening in feet, in the drawing's frame, where OpenStreetMap maps it. */
|
|
129
|
+
opening: Pt[] | undefined;
|
|
130
|
+
/** The stands for `layoutOf`: bowls, first row, ground, and the building and roof. */
|
|
131
|
+
bowl: RealBowl;
|
|
132
|
+
/** The seats the bowls hold, by bowl, and in all: equal to the configuration's seats where the building has room (`fit` is `fits`). */
|
|
133
|
+
seats: {
|
|
134
|
+
tier: string;
|
|
135
|
+
seats: number;
|
|
136
|
+
}[];
|
|
137
|
+
total: number;
|
|
138
|
+
/** The floor a seat takes in the plan, in square feet (one of `SEAT_AREAS`). */
|
|
139
|
+
seatArea: number;
|
|
140
|
+
fit: "fits" | "tight";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The plan of a stadium's bowl for a configuration: the building's outline turned so that the field lies along x, a first row round a football field's footprint as a gridiron's stands have it, the largest bowl-shaped
|
|
144
|
+
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the configuration's seats the sources give it, with a walk
|
|
145
|
+
* between them. The bowl is the same building for football and soccer (the seats of the configuration differ, and the field).
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
|
|
149
|
+
* const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
|
|
150
|
+
* console.log(plan.orientation.basis, plan.seats.map((t) => t.tier).join(" "), plan.total);
|
|
151
|
+
* // opening lower upper 52497
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export declare function planStadium(stadium: RealStadium, config: StadiumConfigId, box: Box): StadiumPlan;
|
|
155
|
+
/** The stands of a stadium for `layoutOf` and the 3D view, round a field: `planStadium`'s bowl. */
|
|
156
|
+
export declare const stadiumBowl: (stadium: RealStadium, config: StadiumConfigId, box: Box) => RealBowl;
|
|
157
|
+
/** Whether the bowl lies inside the building: every corner of every bowl's outer edge is inside the footprint. */
|
|
158
|
+
export declare function stadiumBowlInside(plan: StadiumPlan): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const STADIUM_DATA = [
|
|
2
|
+
{
|
|
3
|
+
"id": "bc-place",
|
|
4
|
+
"wikidata": "Q612227",
|
|
5
|
+
"ground": "bc-place",
|
|
6
|
+
"en": { "title": "BC Place", "revision": 1372065649 },
|
|
7
|
+
"configs": { "cfl": { "sport": "cfl", "seats": 54500, "source": "wikipedia", "read": "the infobox's first capacity, 54,500, which Wikidata's capacity (P1083) also gives; the article's stand for football of the CFL's club" }, "soccer": { "sport": "soccer", "seats": 52497, "source": "wikipedia", "read": "the infobox's capacity for the 2026 world cup of soccer, 52,497, 2,003 fewer than its first" } },
|
|
8
|
+
"tiers": { "count": 2, "basis": "documented", "lowerSeats": 27695, "read": "the infobox's lower bowl of 27,695 seats, and the article's lower and upper bowls (the club that plays soccer there sold only the lower bowl)" },
|
|
9
|
+
"roof": { "kind": "cable", "basis": "documented", "read": "the article: a retractable roof supported by cables, the largest of its kind, its opening 100 by 85 metres (the size of the field), the fabric retracting into a pod in the middle of the opening above the suspended video board", "opening": { "length": 100, "width": 85, "unit": "m" } },
|
|
10
|
+
},
|
|
11
|
+
];
|
package/dist/stadiums.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// The real stadiums: for a stadium that stages football and soccer, the outline of its building and the way its field lies in it (from OpenStreetMap, ODbL 1.0), and a bowl of the seats of each configuration, the
|
|
2
|
+
// number of bowls and the kind of roof the sources give (Wikidata, CC0, and the English Wikipedia article each fact cites), fitted inside the outline. It is the stadium's counterpart of `venues.ts`, for a surface that is a
|
|
3
|
+
// field and not a rink: level one is the building, level two a plausible bowl, and it stops there (no team's, ticket seller's or federation's seat map is read or copied, no section or row is named, and no mark of a club, a
|
|
4
|
+
// league or a tournament is drawn). The drawing is `drawKyogijo({ sport: "cfl", stadium, crowd: { real: true } })` (or `sport: "soccer"` with a `ground` or a `stadium`) and, in 3D, the same options in `buildTurn`.
|
|
5
|
+
import { NORTH_ROOM, aislesOf, areaOfOutline, outlinePoints } from "./crowd.js";
|
|
6
|
+
import { FEET_PER_METRE, FOOTBALL } from "./geometry.js";
|
|
7
|
+
import { groundById } from "./grounds.js";
|
|
8
|
+
import { GAP, HALL, MAX_RAKE, SEAT_AREAS, WALL, VENUE_ATTRIBUTION } from "./venues.js";
|
|
9
|
+
import { boxOf, edgeDistance, inside, metresFrom, middleOf, northOn, turnedTo } from "./venues.geo.js";
|
|
10
|
+
import { STADIUM_DATA } from "./stadiums.data.js";
|
|
11
|
+
import { STADIUM_OSM } from "./stadiums.osm.data.js";
|
|
12
|
+
export { VENUE_ATTRIBUTION };
|
|
13
|
+
/** The stadiums by key. */
|
|
14
|
+
export const STADIUMS = Object.freeze(Object.fromEntries(STADIUM_DATA.map((data) => [data.id, Object.freeze({ ...data, osm: STADIUM_OSM[data.id] })])));
|
|
15
|
+
/** The keys of `STADIUMS`, in the order of the data. */
|
|
16
|
+
export const STADIUM_IDS = Object.freeze(STADIUM_DATA.map((data) => data.id));
|
|
17
|
+
/** The stadium with a key, or `undefined`. */
|
|
18
|
+
export const stadiumById = (id) => (id !== undefined && Object.prototype.hasOwnProperty.call(STADIUMS, id) ? STADIUMS[id] : undefined);
|
|
19
|
+
/** The stadium whose Wikidata item is `Q612227`, or `undefined`. */
|
|
20
|
+
export const stadiumByWikidata = (wikidata) => Object.values(STADIUMS).find((s) => s.wikidata === wikidata);
|
|
21
|
+
/** The ground a stadium is (its name, place and pitch). */
|
|
22
|
+
export const groundOfStadium = (stadium) => {
|
|
23
|
+
const ground = groundById(stadium.ground);
|
|
24
|
+
if (ground === undefined)
|
|
25
|
+
throw new Error(`${stadium.id} has no ground ${stadium.ground}`);
|
|
26
|
+
return ground;
|
|
27
|
+
};
|
|
28
|
+
const round = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
|
|
29
|
+
const lonLat = (ring) => ring.map(([lon, lat]) => [lon, lat]);
|
|
30
|
+
/** How the field lies in a stadium's building (see `StadiumOsm.axis`). */
|
|
31
|
+
export const stadiumOrientation = (stadium) => ({ heading: stadium.osm.axis.heading, basis: stadium.osm.axis.basis, north: round(northOn(stadium.osm.axis.heading)) });
|
|
32
|
+
/** The configuration a sport is: `cfl` or `soccer`, or `undefined` for a sport a stadium has no configuration for. */
|
|
33
|
+
export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "soccer" ? sport : undefined);
|
|
34
|
+
/** The seats of a stadium for a configuration. */
|
|
35
|
+
export const stadiumSeats = (stadium, config) => stadium.configs[config].seats;
|
|
36
|
+
// ---- The bowl. ----
|
|
37
|
+
/** How far the first row is from a football field's footprint (the CFL's, the field and its end zones) along a side and at an end, and the corner's radius: the package's own gridiron profile (`crowd.ts`), so that a real stadium's first row is the usual one. */
|
|
38
|
+
const FIRST_ROW = { side: 42, end: 28, corner: 60 };
|
|
39
|
+
/** The height of the first row above the field, in feet, the rise of a bowl's rows by the number of bowls (what the lowest, a middle and the upper rise, at most), and the rise of the walk between two bowls (a suite level): ours, no source gives them. */
|
|
40
|
+
export const STADIUM_FIRST_HEIGHT = 5;
|
|
41
|
+
export const STADIUM_RISES = { 1: [60], 2: [38, 62], 3: [32, 26, 56] };
|
|
42
|
+
export const STADIUM_WALK_RISE = 24;
|
|
43
|
+
/** The height a roof's rim stands over the top row, at the least, in feet. */
|
|
44
|
+
export const STADIUM_CLEARANCE = 22;
|
|
45
|
+
/** The share of the seats in the lowest bowl where no source says. */
|
|
46
|
+
const LOWER_SHARE = 0.5;
|
|
47
|
+
const CORNER = 0.7;
|
|
48
|
+
/** How many of a bowl's seats are taken, relative to the stands', as `crowd.ts` has them. */
|
|
49
|
+
const FILL = { lower: 1, middle: 0.94, upper: 0.88 };
|
|
50
|
+
const TIER_IDS = { 1: ["lower"], 2: ["lower", "upper"], 3: ["lower", "middle", "upper"] };
|
|
51
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
52
|
+
const between = (a, b, t) => ({ kind: "rrect", x0: lerp(a.x0, b.x0, t), y0: lerp(a.y0, b.y0, t), x1: lerp(a.x1, b.x1, t), y1: lerp(a.y1, b.y1, t), r: lerp(a.r, b.r, t) });
|
|
53
|
+
/** The parameter in `[lo, hi]` at which an increasing function reaches `target` (bisection), or `hi` where it does not. */
|
|
54
|
+
function solveFor(f, lo, hi, target) {
|
|
55
|
+
if (f(hi) <= target)
|
|
56
|
+
return hi;
|
|
57
|
+
let [a, b] = [lo, hi];
|
|
58
|
+
for (let i = 0; i < 48; i += 1) {
|
|
59
|
+
const m = (a + b) / 2;
|
|
60
|
+
if (f(m) < target)
|
|
61
|
+
a = m;
|
|
62
|
+
else
|
|
63
|
+
b = m;
|
|
64
|
+
}
|
|
65
|
+
return (a + b) / 2;
|
|
66
|
+
}
|
|
67
|
+
/** Plans already made, by stadium, configuration and field: a plan is a pure function of them, and a drawing asks for it more than once. */
|
|
68
|
+
const PLANS = new WeakMap();
|
|
69
|
+
/**
|
|
70
|
+
* The plan of a stadium's bowl for a configuration: the building's outline turned so that the field lies along x, a first row round a football field's footprint as a gridiron's stands have it, the largest bowl-shaped
|
|
71
|
+
* outline that fits the building round the middle of the field, and between the two the bowls, the outer edge of each placed so that its band holds the share of the configuration's seats the sources give it, with a walk
|
|
72
|
+
* between them. The bowl is the same building for football and soccer (the seats of the configuration differ, and the field).
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
|
|
76
|
+
* const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
|
|
77
|
+
* console.log(plan.orientation.basis, plan.seats.map((t) => t.tier).join(" "), plan.total);
|
|
78
|
+
* // opening lower upper 52497
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function planStadium(stadium, config, box) {
|
|
82
|
+
const key = `${config}|${box.x}|${box.y}|${box.w}|${box.h}`;
|
|
83
|
+
const mine = PLANS.get(stadium) ?? new Map();
|
|
84
|
+
PLANS.set(stadium, mine);
|
|
85
|
+
const known = mine.get(key);
|
|
86
|
+
if (known !== undefined)
|
|
87
|
+
return known;
|
|
88
|
+
const made = plan(stadium, config, box);
|
|
89
|
+
mine.set(key, made);
|
|
90
|
+
return made;
|
|
91
|
+
}
|
|
92
|
+
function plan(stadium, config, rink) {
|
|
93
|
+
const orientation = stadiumOrientation(stadium);
|
|
94
|
+
const origin = middleOf(lonLat(stadium.osm.ring));
|
|
95
|
+
const turned = (ring) => metresFrom(origin, lonLat(ring)).map((p) => turnedTo(orientation.heading, p));
|
|
96
|
+
const outline = turned(stadium.osm.ring);
|
|
97
|
+
const [middle] = turned([stadium.osm.axis.centre]);
|
|
98
|
+
const at = [rink.x + rink.w / 2, rink.y + rink.h / 2];
|
|
99
|
+
const place = (ring) => ring.map(([x, y]) => [round((x - middle[0]) * FEET_PER_METRE + at[0], 2), round((y - middle[1]) * FEET_PER_METRE + at[1], 2)]);
|
|
100
|
+
const footprint = place(outline);
|
|
101
|
+
const fb = boxOf(footprint);
|
|
102
|
+
const openingRing = stadium.osm.roof?.opening?.ring;
|
|
103
|
+
const opening = openingRing === undefined ? undefined : place(turned(openingRing));
|
|
104
|
+
// The first row round a football field's footprint, and the largest bowl-shaped outline that fits inside the building round the middle of the field.
|
|
105
|
+
const gridiron = FOOTBALL.cfl;
|
|
106
|
+
const length = gridiron.length + 2 * gridiron.endZone;
|
|
107
|
+
const first = { kind: "rrect", x0: at[0] - length / 2 - FIRST_ROW.end, y0: at[1] - gridiron.width / 2 - FIRST_ROW.side, x1: at[0] + length / 2 + FIRST_ROW.end, y1: at[1] + gridiron.width / 2 + FIRST_ROW.side, r: FIRST_ROW.corner };
|
|
108
|
+
const shapedAs = (long, s) => ({ kind: "rrect", x0: at[0] - long * s, y0: at[1] - s, x1: at[0] + long * s, y1: at[1] + s, r: CORNER * s });
|
|
109
|
+
const largest = (long) => {
|
|
110
|
+
let [lo, hi] = [60, Math.max(fb.x1 - fb.x0, fb.y1 - fb.y0)];
|
|
111
|
+
for (let i = 0; i < 24; i += 1) {
|
|
112
|
+
const m = (lo + hi) / 2;
|
|
113
|
+
if (outlinePoints(shapedAs(long, m)).every((p) => inside(footprint, p) && edgeDistance(footprint, p) >= WALL))
|
|
114
|
+
lo = m;
|
|
115
|
+
else
|
|
116
|
+
hi = m;
|
|
117
|
+
}
|
|
118
|
+
return lo;
|
|
119
|
+
};
|
|
120
|
+
const tries = [1.15, 1.25, 1.35, 1.45, 1.6].map((long) => ({ long, s: largest(long) }));
|
|
121
|
+
const best = tries.reduce((a, b) => (a.long * a.s * a.s >= b.long * b.s * b.s ? a : b));
|
|
122
|
+
const wall = shapedAs(best.long, best.s);
|
|
123
|
+
const outer = { ...wall, x0: Math.min(wall.x0, first.x0 - 40), y0: Math.min(wall.y0, first.y0 - 40), x1: Math.max(wall.x1, first.x1 + 40), y1: Math.max(wall.y1, first.y1 + 40) };
|
|
124
|
+
const depth = (first.x0 - outer.x0) + (first.y0 - outer.y0) + (outer.x1 - first.x1) + (outer.y1 - first.y1);
|
|
125
|
+
const gap = GAP / Math.max(20, depth / 4);
|
|
126
|
+
const seatsTotal = stadium.configs[config].seats;
|
|
127
|
+
const count = stadium.tiers.count;
|
|
128
|
+
const lower = stadium.tiers.lowerSeats === undefined ? (count === 1 ? 1 : count === 2 ? LOWER_SHARE : 0.36) : stadium.tiers.lowerSeats / seatsTotal;
|
|
129
|
+
const shares = count === 1 ? [1] : count === 2 ? [lower, 1 - lower] : [lower, (1 - lower) * 0.36, (1 - lower) * 0.64];
|
|
130
|
+
const outlineAt = (t) => between(first, outer, t);
|
|
131
|
+
const build = (area) => {
|
|
132
|
+
const seatsOf = (region, hole) => Math.round((areaOfOutline(region) - areaOfOutline(hole)) / area);
|
|
133
|
+
const rings = [];
|
|
134
|
+
let t0 = 0;
|
|
135
|
+
let tight = false;
|
|
136
|
+
let up = STADIUM_FIRST_HEIGHT;
|
|
137
|
+
shares.forEach((share, k) => {
|
|
138
|
+
const wanted = seatsTotal * share;
|
|
139
|
+
const t1 = solveFor((t) => seatsOf(outlineAt(t), outlineAt(t0)), t0, 1, wanted);
|
|
140
|
+
if (seatsOf(outlineAt(t1), outlineAt(t0)) < wanted - 1)
|
|
141
|
+
tight = true;
|
|
142
|
+
const region = outlineAt(t1);
|
|
143
|
+
const hole = outlineAt(t0);
|
|
144
|
+
const tier = TIER_IDS[count][k];
|
|
145
|
+
const run = (region.y1 - hole.y1 + (hole.y0 - region.y0)) / 2;
|
|
146
|
+
const rise = Math.min(STADIUM_RISES[count][k], Math.max(6, run * Math.tan((MAX_RAKE * Math.PI) / 180)));
|
|
147
|
+
const h0 = k === 0 ? STADIUM_FIRST_HEIGHT : up + STADIUM_WALK_RISE;
|
|
148
|
+
const h1 = h0 + rise;
|
|
149
|
+
up = h1;
|
|
150
|
+
rings.push({
|
|
151
|
+
id: tier,
|
|
152
|
+
tier,
|
|
153
|
+
region,
|
|
154
|
+
hole,
|
|
155
|
+
from: round(hole.y1 - first.y1, 2),
|
|
156
|
+
to: round(region.y1 - first.y1, 2),
|
|
157
|
+
height: [round(h0), round(h1)],
|
|
158
|
+
rake: round((Math.atan2(h1 - h0, Math.max(1, run)) * 180) / Math.PI),
|
|
159
|
+
fill: FILL[tier],
|
|
160
|
+
seats: seatsOf(region, hole),
|
|
161
|
+
aisles: aislesOf(region, hole),
|
|
162
|
+
});
|
|
163
|
+
t0 = Math.min(1, t1 + gap);
|
|
164
|
+
});
|
|
165
|
+
return { rings, tight };
|
|
166
|
+
};
|
|
167
|
+
let seatArea = SEAT_AREAS[0];
|
|
168
|
+
let made = build(seatArea);
|
|
169
|
+
for (const area of SEAT_AREAS.slice(1)) {
|
|
170
|
+
if (!made.tight)
|
|
171
|
+
break;
|
|
172
|
+
seatArea = area;
|
|
173
|
+
made = build(area);
|
|
174
|
+
}
|
|
175
|
+
const { rings, tight } = made;
|
|
176
|
+
// The rounding of each bowl's seats leaves the total a seat or two off the configuration's: the last bowl takes the difference.
|
|
177
|
+
const missing = seatsTotal - rings.reduce((sum, ring) => sum + ring.seats, 0);
|
|
178
|
+
if (!tight && Math.abs(missing) <= 3)
|
|
179
|
+
rings[rings.length - 1].seats += missing;
|
|
180
|
+
const total = rings.reduce((sum, ring) => sum + ring.seats, 0);
|
|
181
|
+
const top = rings[rings.length - 1].height[1];
|
|
182
|
+
const roof = stadium.osm.roof;
|
|
183
|
+
const rise = roof?.rise === undefined ? 0 : round(roof.rise * FEET_PER_METRE);
|
|
184
|
+
// The rim of the roof stands where OpenStreetMap maps it (its height less the roof's own rise), and at the least over the top row by a roof's clearance.
|
|
185
|
+
const mapped = roof?.height === undefined ? 0 : round((roof.height - (roof.rise ?? 0)) * FEET_PER_METRE);
|
|
186
|
+
const edge = Math.max(mapped, top + STADIUM_CLEARANCE);
|
|
187
|
+
const cable = stadium.roof.kind === "cable" && opening !== undefined && rise > 0;
|
|
188
|
+
const north = orientation.basis === "none" ? undefined : orientation.north;
|
|
189
|
+
const room = north === undefined ? 0 : NORTH_ROOM;
|
|
190
|
+
const bowl = {
|
|
191
|
+
rings,
|
|
192
|
+
off: { left: rink.x - first.x0, right: first.x1 - rink.x - rink.w, top: rink.y - first.y0, bottom: first.y1 - rink.y - rink.h },
|
|
193
|
+
reach: { left: Math.max(0, rink.x - fb.x0), right: Math.max(0, fb.x1 - rink.x - rink.w), top: Math.max(0, rink.y - fb.y0) + room, bottom: Math.max(0, fb.y1 - rink.y - rink.h) },
|
|
194
|
+
shell: {
|
|
195
|
+
footprint,
|
|
196
|
+
hall: { ...outer, x0: outer.x0 - HALL, y0: outer.y0 - HALL, x1: outer.x1 + HALL, y1: outer.y1 + HALL, r: outer.r + HALL },
|
|
197
|
+
...(north === undefined ? {} : { north }),
|
|
198
|
+
roof: cable ? "cable" : "fixed",
|
|
199
|
+
edge,
|
|
200
|
+
...(cable ? { crown: { rise, opening } } : {}),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
return { stadium, config, orientation, box: rink, footprint, opening, bowl, seats: rings.map((ring) => ({ tier: ring.tier, seats: ring.seats })), total, seatArea, fit: tight ? "tight" : "fits" };
|
|
204
|
+
}
|
|
205
|
+
/** The stands of a stadium for `layoutOf` and the 3D view, round a field: `planStadium`'s bowl. */
|
|
206
|
+
export const stadiumBowl = (stadium, config, box) => planStadium(stadium, config, box).bowl;
|
|
207
|
+
/** Whether the bowl lies inside the building: every corner of every bowl's outer edge is inside the footprint. */
|
|
208
|
+
export function stadiumBowlInside(plan) {
|
|
209
|
+
return outlinePoints(plan.bowl.rings[plan.bowl.rings.length - 1].region).every((p) => inside(plan.footprint, p));
|
|
210
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const STADIUM_OSM = {
|
|
2
|
+
"bc-place": {
|
|
3
|
+
"source": "way/24705904",
|
|
4
|
+
"read": "2026-10-20",
|
|
5
|
+
"base": "2026-10-20T21:46:16Z",
|
|
6
|
+
"licence": "ODbL-1.0",
|
|
7
|
+
"attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
|
|
8
|
+
"axis": { "heading": 52.8, "basis": "opening", "strength": 1.47, "elongation": 1.19, "centre": [-123.112002, 49.276698] },
|
|
9
|
+
"roof": {
|
|
10
|
+
"source": "relation/5836463",
|
|
11
|
+
"height": 61,
|
|
12
|
+
"rise": 13,
|
|
13
|
+
"outer": "way/390133856",
|
|
14
|
+
"opening": { "source": "way/390133855", "ring": [
|
|
15
|
+
[-123.111972, 49.277139],
|
|
16
|
+
[-123.112337, 49.277016],
|
|
17
|
+
[-123.112599, 49.276841],
|
|
18
|
+
[-123.112723, 49.276687],
|
|
19
|
+
[-123.112664, 49.276512],
|
|
20
|
+
[-123.11246, 49.276337],
|
|
21
|
+
[-123.112245, 49.276253],
|
|
22
|
+
[-123.112009, 49.276257],
|
|
23
|
+
[-123.111607, 49.276414],
|
|
24
|
+
[-123.111323, 49.276614],
|
|
25
|
+
[-123.111296, 49.276806],
|
|
26
|
+
[-123.111403, 49.276943],
|
|
27
|
+
[-123.111564, 49.277065],
|
|
28
|
+
[-123.111757, 49.277146],
|
|
29
|
+
] },
|
|
30
|
+
},
|
|
31
|
+
"ring": [
|
|
32
|
+
[-123.113517, 49.276312],
|
|
33
|
+
[-123.113285, 49.276073],
|
|
34
|
+
[-123.112893, 49.275773],
|
|
35
|
+
[-123.112614, 49.275683],
|
|
36
|
+
[-123.112399, 49.27566],
|
|
37
|
+
[-123.112137, 49.275686],
|
|
38
|
+
[-123.11166, 49.275795],
|
|
39
|
+
[-123.111225, 49.275999],
|
|
40
|
+
[-123.110853, 49.276199],
|
|
41
|
+
[-123.110572, 49.276409],
|
|
42
|
+
[-123.110433, 49.276556],
|
|
43
|
+
[-123.110371, 49.276724],
|
|
44
|
+
[-123.110396, 49.276902],
|
|
45
|
+
[-123.110692, 49.277303],
|
|
46
|
+
[-123.111152, 49.277629],
|
|
47
|
+
[-123.111379, 49.277714],
|
|
48
|
+
[-123.111649, 49.277751],
|
|
49
|
+
[-123.111894, 49.277728],
|
|
50
|
+
[-123.112475, 49.277543],
|
|
51
|
+
[-123.112833, 49.277379],
|
|
52
|
+
[-123.113189, 49.27718],
|
|
53
|
+
[-123.113573, 49.276831],
|
|
54
|
+
[-123.113643, 49.276657],
|
|
55
|
+
[-123.113623, 49.276478],
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
package/dist/strings.d.ts
CHANGED
|
@@ -504,6 +504,23 @@ export interface KyogijoWords {
|
|
|
504
504
|
arenaSize: string;
|
|
505
505
|
/** An arena whose rink is said to have been shorter than the NHL's, with no size given: {arena}. */
|
|
506
506
|
arenaUnknown: string;
|
|
507
|
+
/** The stands of an arena's real venue: {seats} seats in {tiers} tiers, fitted inside the outline of {arena}'s building from OpenStreetMap. */
|
|
508
|
+
real: string;
|
|
509
|
+
/** How the rink lies in it: along the building's long axis (an inference), as OpenStreetMap maps the ice, or not known. */
|
|
510
|
+
realBuilding: string;
|
|
511
|
+
realMapped: string;
|
|
512
|
+
realNone: string;
|
|
513
|
+
/** The stands of a stadium's real venue: {seats} seats in {tiers} bowls, fitted inside the outline of {stadium}'s building from OpenStreetMap. */
|
|
514
|
+
stadium: string;
|
|
515
|
+
/** How the field lies in it: along the roof's opening (which is the field's size), along the building's long axis (an inference), or not known. */
|
|
516
|
+
stadiumOpening: string;
|
|
517
|
+
stadiumBuilding: string;
|
|
518
|
+
stadiumNone: string;
|
|
519
|
+
/** A district: a plan of {count} real venues at their real positions, {mode}; and what `mode` says for the two kinds of plan, and the word before the credit. */
|
|
520
|
+
district: string;
|
|
521
|
+
districtNeighbours: string;
|
|
522
|
+
districtCity: string;
|
|
523
|
+
districtCredit: string;
|
|
507
524
|
/** A game decided on penalties: {name}, and the shoot-out score {a}-{b}, winner first. */
|
|
508
525
|
wonPens: string;
|
|
509
526
|
/** The shoot-out, as it is written beside a status: {a} and {b}, in the order of the sides on the drawing. */
|
package/dist/strings.js
CHANGED
|
@@ -199,7 +199,7 @@ export const KYOGIJO_STRINGS = {
|
|
|
199
199
|
},
|
|
200
200
|
},
|
|
201
201
|
scoreTitle: "{away} {awayScore}, {home} {homeScore}",
|
|
202
|
-
said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
|
|
202
|
+
said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
|
|
203
203
|
budo: {
|
|
204
204
|
sides: { north: "north", south: "south", east: "east", west: "west" },
|
|
205
205
|
judo: "a judo contest area of {contest} by {contest} metres in a safety area {safety} metres wide, laid with tatami of {tatami}",
|
|
@@ -758,7 +758,7 @@ export const KYOGIJO_STRINGS = {
|
|
|
758
758
|
},
|
|
759
759
|
},
|
|
760
760
|
scoreTitle: "{away} {awayScore}、{home} {homeScore}",
|
|
761
|
-
said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
|
|
761
|
+
said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
|
|
762
762
|
budo: {
|
|
763
763
|
sides: { north: "北", south: "南", east: "東", west: "西" },
|
|
764
764
|
judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
|
package/dist/turn.kit.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface Plane {
|
|
|
35
35
|
fadeTo?: number;
|
|
36
36
|
/** How low the eye must be for a `shy` plane to begin to give way: the share (of the plane's side being towards the eye, times the sine of the pitch) at which the fade starts; 0.1 when missing. A low barrier takes a high one, so that it stays whole until the eye is nearly level with it. */
|
|
37
37
|
fadeAt?: number;
|
|
38
|
+
/**
|
|
39
|
+
* The building (a `TurnModel.groups` index) a plane belongs to, in a scene of several that stand apart (a district's): the view draws one building's planes all before another's, by which side of the line between
|
|
40
|
+
* them the eye is on, and the planes of one building among themselves by depth.
|
|
41
|
+
*/
|
|
42
|
+
group?: number;
|
|
38
43
|
}
|
|
39
44
|
/** What stands on the surface as a thing a reader would name: a net, a basket, a goal, a pair of posts. */
|
|
40
45
|
export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts";
|
package/dist/turn.markup.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
|
7
7
|
* (`centre`, in feet).
|
|
8
8
|
*/
|
|
9
9
|
export declare function planeMatrix(p: Plane, scale: number, centre?: readonly [number, number]): number[];
|
|
10
|
+
/**
|
|
11
|
+
* The order to draw buildings that stand apart in, far first: `rank[g]` is how many of the others `g` stands in front of. One building is in front of another when a line can be put between them and the eye is on its
|
|
12
|
+
* side (from the side of a line, the eye sees what is on its own side of it first, whatever the heights); two that no line separates go by how near their middles are.
|
|
13
|
+
*/
|
|
14
|
+
export declare function groupRanks(groups: readonly {
|
|
15
|
+
hull: readonly (readonly [number, number])[];
|
|
16
|
+
}[], yaw: number): number[];
|
|
10
17
|
/** Where everything is for a view in a container: the placement, the matrix of each element and its place in the stack. */
|
|
11
18
|
export interface Layout {
|
|
12
19
|
at: Placement;
|
package/dist/turn.markup.js
CHANGED
|
@@ -19,6 +19,40 @@ export function planeMatrix(p, scale, centre = [0, 0]) {
|
|
|
19
19
|
const w = unit(cross(u, v));
|
|
20
20
|
return [u[0], u[1], u[2], 0, v[0], v[1], v[2], 0, w[0], w[1], w[2], 0, (p.o[0] - centre[0]) * scale, (p.o[1] - centre[1]) * scale, p.o[2] * scale, 1];
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The order to draw buildings that stand apart in, far first: `rank[g]` is how many of the others `g` stands in front of. One building is in front of another when a line can be put between them and the eye is on its
|
|
24
|
+
* side (from the side of a line, the eye sees what is on its own side of it first, whatever the heights); two that no line separates go by how near their middles are.
|
|
25
|
+
*/
|
|
26
|
+
export function groupRanks(groups, yaw) {
|
|
27
|
+
const a = (yaw * Math.PI) / 180;
|
|
28
|
+
// The direction on the plan towards the eye: depth grows with `x sin(yaw) + y cos(yaw)`.
|
|
29
|
+
const toward = [Math.sin(a), Math.cos(a)];
|
|
30
|
+
const middle = groups.map((g) => [g.hull.reduce((s, p) => s + p[0], 0) / g.hull.length, g.hull.reduce((s, p) => s + p[1], 0) / g.hull.length]);
|
|
31
|
+
const rank = new Array(groups.length).fill(0);
|
|
32
|
+
groups.forEach((g, i) => groups.forEach((h, j) => {
|
|
33
|
+
if (j <= i)
|
|
34
|
+
return;
|
|
35
|
+
// The axes tried for a line between them: the line through the middles' centre at right angles to the way from one to the other, then others round the circle.
|
|
36
|
+
const base = Math.atan2(middle[j][1] - middle[i][1], middle[j][0] - middle[i][0]);
|
|
37
|
+
let side = 0;
|
|
38
|
+
for (let k = 0; k < 36 && side === 0; k += 1) {
|
|
39
|
+
const t = base + (k % 2 === 0 ? 1 : -1) * Math.ceil(k / 2) * (Math.PI / 36);
|
|
40
|
+
const axis = [Math.cos(t), Math.sin(t)];
|
|
41
|
+
const along = (p) => p[0] * axis[0] + p[1] * axis[1];
|
|
42
|
+
if (Math.max(...g.hull.map(along)) < Math.min(...h.hull.map(along)))
|
|
43
|
+
side = axis[0] * toward[0] + axis[1] * toward[1] > 0 ? 1 : -1;
|
|
44
|
+
}
|
|
45
|
+
// No line between them: the nearer middle is in front.
|
|
46
|
+
if (side === 0)
|
|
47
|
+
side = middle[j][0] * toward[0] + middle[j][1] * toward[1] > middle[i][0] * toward[0] + middle[i][1] * toward[1] ? 1 : -1;
|
|
48
|
+
// `side` 1: `h` is nearer to the eye than `g`.
|
|
49
|
+
if (side > 0)
|
|
50
|
+
rank[j] += 1;
|
|
51
|
+
else
|
|
52
|
+
rank[i] += 1;
|
|
53
|
+
}));
|
|
54
|
+
return rank;
|
|
55
|
+
}
|
|
22
56
|
/** Work a view out in a container `width` by `height` pixels. */
|
|
23
57
|
export function layoutOf(model, view, width, height, lens) {
|
|
24
58
|
const distance = eyeDistance(width, height, lens ?? model.lens);
|
|
@@ -34,8 +68,10 @@ export function layoutOf(model, view, width, height, lens) {
|
|
|
34
68
|
// sorted with the rest, by its middle, so that the legs and the skirt on the far side are drawn under it and the ones on the near side over it, and nothing under the top shows through it.
|
|
35
69
|
const depth = (p) => depthOf([(p.o[0] + (p.u[0] + p.v[0]) / 2 - centre[0]) * s, (p.o[1] + (p.u[1] + p.v[1]) / 2 - centre[1]) * s, (p.o[2] + (p.u[2] + p.v[2]) / 2) * s], view, at);
|
|
36
70
|
const raised = model.uprights.lift > 0;
|
|
37
|
-
const order = [...planes.map((p, i) => ({ i: i + 1, ground: p.kind === "ground", depth: depth(p) })), ...(raised ? [{ i: 0, ground: false, depth: depth(floor) }] : [])].filter((one) => !one.ground);
|
|
38
|
-
|
|
71
|
+
const order = [...planes.map((p, i) => ({ i: i + 1, ground: p.kind === "ground", depth: depth(p), group: p.group })), ...(raised ? [{ i: 0, ground: false, depth: depth(floor), group: undefined }] : [])].filter((one) => !one.ground);
|
|
72
|
+
// Buildings that stand apart are drawn one after another, the far one first; a plane of none (the floor) goes by its depth alone.
|
|
73
|
+
const rank = model.groups === undefined ? undefined : groupRanks(model.groups, view.yaw);
|
|
74
|
+
order.sort((a, b) => (rank === undefined || a.group === undefined || b.group === undefined ? 0 : rank[a.group] - rank[b.group]) || a.depth - b.depth);
|
|
39
75
|
const stack = new Array(model.planes.length + 1).fill(0);
|
|
40
76
|
stack[0] = 1;
|
|
41
77
|
order.forEach((one, rank) => {
|
package/dist/turn.model.d.ts
CHANGED
|
@@ -100,6 +100,10 @@ export interface TurnModel {
|
|
|
100
100
|
lens?: number;
|
|
101
101
|
/** The flat drawing to show where the 3D view cannot be drawn, when it is not `drawKyogijo`'s of `options` (a tournament layout's). */
|
|
102
102
|
flat?: string;
|
|
103
|
+
/** The buildings of a scene that has several standing apart (a district's), each as the convex hull of its outline in the drawing's frame, in feet: the planes that name one (`Plane.group`) are stacked building by building. */
|
|
104
|
+
groups?: {
|
|
105
|
+
hull: readonly (readonly [number, number])[];
|
|
106
|
+
}[];
|
|
103
107
|
/** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
|
|
104
108
|
board?: ResolvedBoard;
|
|
105
109
|
}
|
package/dist/turn.model.js
CHANGED
|
@@ -89,7 +89,9 @@ export function buildTurn(options, settings = {}) {
|
|
|
89
89
|
const reach = uprights.reach;
|
|
90
90
|
const margin = reach === undefined ? undefined : convertLength(reach, "ft", lengthUnit(asked));
|
|
91
91
|
const own = asked.margin;
|
|
92
|
-
|
|
92
|
+
// (A real venue's building stays on the floor, with the arrow that says where north is: the crowd drawn without its seats.)
|
|
93
|
+
const keep = resolveCrowd(crowdOpt)?.real === true ? { real: true, stands: false } : undefined;
|
|
94
|
+
const base = reach !== undefined && crowdOpt !== undefined && crowdOpt !== false ? { ...asked, crowd: keep, margin: Math.max(margin ?? 0, typeof own === "number" && Number.isFinite(own) ? own : 0) } : asked;
|
|
93
95
|
const o = boarded ? { ...base, plate: false, size: asked.size === "compact" ? "full" : asked.size } : base;
|
|
94
96
|
const floor = settings.floor === undefined ? drawKyogijo(o) : settings.floor(o);
|
|
95
97
|
const found = VIEWBOX.exec(floor);
|
package/dist/turn.style.js
CHANGED
|
@@ -66,7 +66,13 @@ export const KYOGIJO_TURN_STYLE = `
|
|
|
66
66
|
.ky-turn .u-th{fill:var(--ky-cr-home)}
|
|
67
67
|
.ky-turn .u-conc{fill:var(--ky-cr-conc)}
|
|
68
68
|
.ky-turn .u-fascia{fill:var(--ky-up-fascia)}
|
|
69
|
+
.ky-turn .u-shell{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
|
|
69
70
|
.ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
|
|
71
|
+
.ky-turn .u-signplate{fill:var(--ky-turn-bg);fill-opacity:.9;stroke:var(--ky-turn-rule);stroke-width:.5}
|
|
72
|
+
.ky-turn .u-sign{fill:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600}
|
|
73
|
+
.ky-turn .u-cable{fill:none;stroke:var(--ky-up-roofline);stroke-width:1}
|
|
74
|
+
.ky-turn .u-fabric{fill:var(--ky-up-roof);fill-opacity:.55;stroke:none}
|
|
75
|
+
.ky-turn .u-pod{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
|
|
70
76
|
.ky-turn .ky-up-seats svg{opacity:var(--ky-cr-dim)}
|
|
71
77
|
.ky-turn .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
|
|
72
78
|
.ky-turn .ky-cr-h{stroke:var(--ky-cr-home)}
|