@johnmorrisdotca/kyogijo-stadiums 0.7.0 → 0.8.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 +18 -0
- package/README.md +1 -1
- package/dist/budo/looks.js +1 -1
- package/dist/budo/room.d.ts +34 -2
- package/dist/budo/room.js +39 -7
- package/dist/budo/types.d.ts +6 -1
- package/dist/budo.style.d.ts +1 -1
- package/dist/budo.style.js +6 -3
- package/dist/crowd.d.ts +4 -0
- package/dist/crowd.draw.d.ts +5 -0
- package/dist/crowd.draw.js +34 -5
- package/dist/crowd.js +6 -1
- package/dist/draw.js +6 -3
- package/dist/element.d.ts +3 -3
- package/dist/element.js +16 -2
- package/dist/halls.js +4 -3
- package/dist/strings.d.ts +7 -1
- package/dist/strings.js +6 -4
- package/dist/surfaces/budo.js +22 -2
- package/dist/turn-entry.d.ts +2 -0
- package/dist/turn-entry.js +1 -0
- package/dist/turn.dom.d.ts +8 -0
- package/dist/turn.dom.js +147 -8
- package/dist/turn.kit.d.ts +5 -3
- package/dist/turn.kit.js +1 -1
- package/dist/turn.markup.d.ts +8 -1
- package/dist/turn.markup.js +18 -4
- package/dist/turn.model.d.ts +26 -0
- package/dist/turn.model.js +34 -3
- package/dist/turn.style.js +58 -4
- package/dist/uprights.d.ts +7 -0
- package/dist/uprights.js +15 -1
- package/dist/venue.d.ts +31 -0
- package/dist/venue.geom.d.ts +53 -0
- package/dist/venue.geom.js +225 -0
- package/dist/venue.js +172 -0
- package/dist/venue.room.d.ts +9 -0
- package/dist/venue.room.js +240 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/venue.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// The stands round a surface, raised: `crowdStands()` says where each tier of seats is (its outline, the height of its first and last row, its aisles, the blocks of visitors and the places left
|
|
2
|
+
// open), and this makes of it what the 3D view stands on the floor: every tier a strip of raked planes (a quad to every straight side and a few to every corner, so that a whole bowl is a few dozen
|
|
3
|
+
// elements, however many seats it has), textured with the crowd's own pattern and the lines of its rows, the concourses between the tiers, the low wall under the first row, the seats on top of a
|
|
4
|
+
// ballpark's wall, and, when asked, the roof or canopy over the top tier. Pure: data in, planes out.
|
|
5
|
+
import { densityOf, resolveCrowd } from "./crowd.js";
|
|
6
|
+
import { patterns } from "./crowd.draw.js";
|
|
7
|
+
import { crowdStands } from "./draw.js";
|
|
8
|
+
import { groundById } from "./grounds.js";
|
|
9
|
+
import { clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
|
|
10
|
+
import { n, shy, wall } from "./turn.kit.js";
|
|
11
|
+
export const NO_VENUE = { items: [], defs: "", reach: 0 };
|
|
12
|
+
/** The sports played under a roof: their stands have a lid and not a canopy. */
|
|
13
|
+
const INDOOR = ["nhl", "nba", "volleyball", "badminton", "tabletennis", "handball", "judo", "karate", "kendo", "sumo", "dojo", "curling", "squash", "racquetball", "pickleball", "netball", "skating", "swimming", "waterpolo", "diving", "artisticswimming", "teqball", "roundnet", "sepaktakraw"];
|
|
14
|
+
/** How far the roof stands over the top row at its edge, in feet (ours: no rule gives it), and how far it reaches over the seats. */
|
|
15
|
+
export const ROOF = { clearance: 26, canopyReach: 0.62, canopyDrop: 6, lidRise: 26 };
|
|
16
|
+
const round = (v) => Math.round(v * 1000) / 1000;
|
|
17
|
+
/** The top of a set of planes. */
|
|
18
|
+
const topOf = (planes) => Math.max(0, ...planes.flatMap((p) => [p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]]));
|
|
19
|
+
/** The tiers a roof covers: the highest of those that are not a ballpark's wall seats or bleachers. */
|
|
20
|
+
function topRing(stands) {
|
|
21
|
+
const rings = stands.rings.filter((r) => r.tier !== "wall" && r.tier !== "bleachers");
|
|
22
|
+
return rings.reduce((best, r) => (best === undefined || r.height[1] > best.height[1] ? r : best), undefined);
|
|
23
|
+
}
|
|
24
|
+
/** The quads of the strips of a ring, with their slopes. */
|
|
25
|
+
function* quadsOf(strips) {
|
|
26
|
+
for (const strip of strips)
|
|
27
|
+
for (let i = 0; i + 1 < strip.inner.length; i += 1)
|
|
28
|
+
yield [strip.inner[i], strip.inner[i + 1], strip.outer[i + 1], strip.outer[i]];
|
|
29
|
+
}
|
|
30
|
+
/** The stands of a surface as planes: `undefined`-free, an empty venue when the options have no crowd (or the crowd is a hall's gallery, which `venue.room.ts` raises). */
|
|
31
|
+
export function standsVenue(o, v, lift) {
|
|
32
|
+
const stands = crowdStands(o);
|
|
33
|
+
const crowd = resolveCrowd(o.crowd);
|
|
34
|
+
if (stands === undefined || crowd === undefined || stands.gallery === true)
|
|
35
|
+
return NO_VENUE;
|
|
36
|
+
const cut = v.detail ?? "auto";
|
|
37
|
+
const fine = crowd.detail !== "plain" && o.detail !== "simple";
|
|
38
|
+
const density = densityOf(crowd, stands.seats, groundById(o.ground)?.capacity);
|
|
39
|
+
const tifo = { home: stands.sectors.some((s) => s.kind === "tifo-home"), away: stands.sectors.some((s) => s.kind === "tifo-away") };
|
|
40
|
+
const pid = `${v.id}-v`;
|
|
41
|
+
const defs = fine ? patterns(pid, crowd, density, tifo.home || tifo.away ? tifo : undefined, false, true) : "";
|
|
42
|
+
const centre = [stands.box.x + stands.box.w / 2, stands.box.y + stands.box.h / 2];
|
|
43
|
+
const covers = stands.covers;
|
|
44
|
+
const inCover = (p) => covers.some(([x0, y0, x1, y1]) => p[0] >= x0 && p[0] <= x1 && p[1] >= y0 && p[1] <= y1);
|
|
45
|
+
const pieces = (poly) => (covers.length === 0 ? [poly] : clipAround(poly, covers));
|
|
46
|
+
const seatPlanes = [];
|
|
47
|
+
const wallSeats = [];
|
|
48
|
+
let count = 0;
|
|
49
|
+
const paint = (ring, piece) => (corners, local) => {
|
|
50
|
+
let svg = `<polygon class="u-bed" points="${pointList(corners)}"/>`;
|
|
51
|
+
const people = ring.fill < 0.95 ? ` fill-opacity="${round(1 - Math.min(0.5, (1 - ring.fill) * 1.6))}"` : "";
|
|
52
|
+
svg += fine ? `<polygon fill="url(#${pid}-cr-h)"${people} points="${pointList(corners)}"/>` : `<polygon class="u-blk" fill-opacity="${round(0.28 + 0.6 * density * ring.fill)}" points="${pointList(corners)}"/>`;
|
|
53
|
+
for (const s of stands.sectors) {
|
|
54
|
+
if (s.ring !== ring.id)
|
|
55
|
+
continue;
|
|
56
|
+
const part = tidy(clipConvex(s.points, piece));
|
|
57
|
+
if (part.length < 3 || Math.abs(areaOf(part)) < 0.05)
|
|
58
|
+
continue;
|
|
59
|
+
const points = pointList(part.map(local));
|
|
60
|
+
if (s.kind === "away")
|
|
61
|
+
svg += fine ? `<polygon fill="url(#${pid}-cr-a)" points="${points}"/>` : `<polygon class="u-blk-a" fill-opacity="${round(0.3 + 0.6 * density)}" points="${points}"/>`;
|
|
62
|
+
else
|
|
63
|
+
svg += fine ? `<polygon fill="url(#${pid}-cr-${s.kind === "tifo-home" ? "th" : "ta"})" points="${points}"/>` : `<polygon class="${s.kind === "tifo-home" ? "u-th" : "u-ta"}" points="${points}"/>`;
|
|
64
|
+
}
|
|
65
|
+
if (fine) {
|
|
66
|
+
let plain = "";
|
|
67
|
+
let vomitory = "";
|
|
68
|
+
ring.aisles.forEach(([a, b], i) => {
|
|
69
|
+
const seg = clipSegment(a, b, piece);
|
|
70
|
+
if (seg === undefined || Math.hypot(seg[1][0] - seg[0][0], seg[1][1] - seg[0][1]) < 0.5)
|
|
71
|
+
return;
|
|
72
|
+
const [p, q] = [local(seg[0]), local(seg[1])];
|
|
73
|
+
const d = `M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`;
|
|
74
|
+
if (i % 4 === 0)
|
|
75
|
+
vomitory += d;
|
|
76
|
+
else
|
|
77
|
+
plain += d;
|
|
78
|
+
});
|
|
79
|
+
if (plain !== "")
|
|
80
|
+
svg += `<path class="ky-cr-aisle" fill="none" stroke-width="1.1" d="${plain}"/>`;
|
|
81
|
+
if (vomitory !== "")
|
|
82
|
+
svg += `<path class="ky-cr-aisle" fill="none" stroke-width="2.4" d="${vomitory}"/>`;
|
|
83
|
+
}
|
|
84
|
+
return svg;
|
|
85
|
+
};
|
|
86
|
+
// The tiers. A ring that no concourse leads to is the first of its chain: a low wall stands under its first row.
|
|
87
|
+
const chained = new Set();
|
|
88
|
+
const links = stands.concourses.map((c, ci) => {
|
|
89
|
+
let at = stands.rings.findIndex((r) => r.hole === c.region);
|
|
90
|
+
if (at < 1)
|
|
91
|
+
at = Math.min(ci + 1, stands.rings.length - 1);
|
|
92
|
+
chained.add(stands.rings[at]);
|
|
93
|
+
return { c, before: stands.rings[at - 1], after: stands.rings[at] };
|
|
94
|
+
});
|
|
95
|
+
for (const ring of stands.rings) {
|
|
96
|
+
const strips = stripsOf(ring.region, ring.hole, ring.more ?? [], cut);
|
|
97
|
+
const into = ring.tier === "wall" ? wallSeats : seatPlanes;
|
|
98
|
+
for (const q of quadsOf(strips)) {
|
|
99
|
+
const slope = slopeOf(q, lift + ring.height[0], lift + ring.height[1]);
|
|
100
|
+
if (slope === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
for (const piece of pieces(tidy(q))) {
|
|
103
|
+
count += 1;
|
|
104
|
+
const p = raked(`${ring.id}-${count}`, "seats", piece, slope.slope, slope.along, paint(ring, piece));
|
|
105
|
+
if (p !== undefined)
|
|
106
|
+
into.push({ ...p, shy: slope.out });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// The low wall under the first row of a tier the walks do not lead to (a ballpark's wall seats stand on the fence, which the fence's own planes make).
|
|
110
|
+
if (ring.tier !== "wall" && !chained.has(ring) && ring.height[0] > 0.5) {
|
|
111
|
+
for (const strip of strips) {
|
|
112
|
+
for (let i = 0; i + 1 < strip.inner.length; i += 1) {
|
|
113
|
+
const [a, b] = [strip.inner[i], strip.inner[i + 1]];
|
|
114
|
+
if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 0.5 || inCover([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]))
|
|
115
|
+
continue;
|
|
116
|
+
const h = lift + ring.height[0];
|
|
117
|
+
const w = wall(`${ring.id}-front-${i}`, "fascia", a[0], a[1], b[0], b[1], 0, h, `<rect class="u-fascia" width="${n(Math.hypot(b[0] - a[0], b[1] - a[1]))}" height="${n(h)}"/>`);
|
|
118
|
+
seatPlanes.push(shy(w, centre));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// The walks between the tiers: a plane from the outer edge of one tier up to the first row of the next.
|
|
124
|
+
for (const { c, before, after } of links) {
|
|
125
|
+
if (before === undefined || after === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
for (const q of quadsOf(stripsOf(c.region, c.hole, [], cut))) {
|
|
128
|
+
const slope = slopeOf(q, lift + before.height[1], lift + after.height[0]);
|
|
129
|
+
if (slope === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
for (const piece of pieces(tidy(q))) {
|
|
132
|
+
count += 1;
|
|
133
|
+
const p = raked(`walk-${count}`, "concourse", piece, slope.slope, slope.along, (corners) => `<polygon class="u-conc" points="${pointList(corners)}"/>`);
|
|
134
|
+
if (p !== undefined)
|
|
135
|
+
seatPlanes.push({ ...p, shy: slope.out });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const items = [];
|
|
140
|
+
if (seatPlanes.length > 0)
|
|
141
|
+
items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
|
|
142
|
+
if (wallSeats.length > 0)
|
|
143
|
+
items.push({ id: "wall-seats", kind: "wall-seats", at: centre, height: topOf(wallSeats), planes: wallSeats });
|
|
144
|
+
// The roof: a canopy over the back of the top tier, or a lid over it in a hall.
|
|
145
|
+
const top = v.roof === true && crowd.roof ? topRing(stands) : undefined;
|
|
146
|
+
if (top !== undefined) {
|
|
147
|
+
const lid = INDOOR.includes(String(o.sport));
|
|
148
|
+
const reach = lid ? 1 : ROOF.canopyReach;
|
|
149
|
+
const zOut = lift + top.height[1] + ROOF.clearance;
|
|
150
|
+
const zIn = lid ? zOut + ROOF.lidRise : zOut - ROOF.canopyDrop;
|
|
151
|
+
const planes = [];
|
|
152
|
+
for (const strip of stripsOf(top.region, top.hole, top.more ?? [], cut)) {
|
|
153
|
+
// The roof's inner edge is part of the way in from the tier's outer edge to its inner.
|
|
154
|
+
const inner = strip.outer.map((p, i) => [p[0] + (strip.inner[i][0] - p[0]) * reach, p[1] + (strip.inner[i][1] - p[1]) * reach]);
|
|
155
|
+
for (const q of quadsOf([{ inner, outer: strip.outer }])) {
|
|
156
|
+
const slope = slopeOf(q, zIn, zOut);
|
|
157
|
+
if (slope === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
for (const piece of pieces(tidy(q))) {
|
|
160
|
+
count += 1;
|
|
161
|
+
const p = raked(`roof-${count}`, "roof", piece, slope.slope, slope.along, (corners) => `<polygon class="u-roof" points="${pointList(corners)}"/>`);
|
|
162
|
+
if (p !== undefined)
|
|
163
|
+
planes.push({ ...p, shy: slope.out });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (planes.length > 0)
|
|
168
|
+
items.push({ id: "roof", kind: "roof", at: centre, height: topOf(planes), planes });
|
|
169
|
+
}
|
|
170
|
+
const r = stands.reach;
|
|
171
|
+
return { items, defs, reach: Math.max(r.left, r.right, r.top, r.bottom) + 6 };
|
|
172
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Venue, type VenueBuild } from "./venue.ts";
|
|
2
|
+
import type { SurfaceOptions } from "./types.ts";
|
|
3
|
+
/** The height of the wood on a dojo's walls, in metres (ours: no source gives it). */
|
|
4
|
+
export declare const WAINSCOT = 0.9;
|
|
5
|
+
/** How far a gallery's floor slab projects in front of the first row beyond the wall's own thickness, and how thick it is, in metres (ours). */
|
|
6
|
+
export declare const SLAB_PROJECTION = 0.5;
|
|
7
|
+
export declare const SLAB = 0.35;
|
|
8
|
+
/** The raised parts of a budō surface's room: what `dojoRoom()` says, for the options. An empty venue for a surface with no room. */
|
|
9
|
+
export declare function roomVenue(o: SurfaceOptions, v: VenueBuild, lift: number): Venue;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// The room round a budō surface, raised from `dojoRoom()`: the walls of a dojo with the doors and windows in them, the raised seat of honour (the kamiza's dais), the banner, the shrine, the balconies with their
|
|
2
|
+
// rows of seats and their rails, and the edge of the mats, a few centimetres over the floor. The sumo dohyō's mound is the budō uprights' own (from the plan's data). Metres in the room's data, feet here.
|
|
3
|
+
import { densityOf, resolveCrowd } from "./crowd.js";
|
|
4
|
+
import { patterns } from "./crowd.draw.js";
|
|
5
|
+
import { dojoRoom } from "./budo/room.js";
|
|
6
|
+
import { NO_VENUE } from "./venue.js";
|
|
7
|
+
import { flat, n, plane, shy as shyOf, wall } from "./turn.kit.js";
|
|
8
|
+
const K = 1 / 0.3048;
|
|
9
|
+
/** The height of the wood on a dojo's walls, in metres (ours: no source gives it). */
|
|
10
|
+
export const WAINSCOT = 0.9;
|
|
11
|
+
/** How far a gallery's floor slab projects in front of the first row beyond the wall's own thickness, and how thick it is, in metres (ours). */
|
|
12
|
+
export const SLAB_PROJECTION = 0.5;
|
|
13
|
+
export const SLAB = 0.35;
|
|
14
|
+
/**
|
|
15
|
+
* The tile of the gallery's seats: one seat wide and one row deep, a cushion at the front and its back behind it, read by every tread. A seat is as wide as the side's seats make it (`RoomBalcony.seatWidth`).
|
|
16
|
+
* The blocks of seats come from `seatBlocks`.
|
|
17
|
+
*/
|
|
18
|
+
const seatTile = (id, width, row) => `<pattern id="${id}-seat" width="${n(width)}" height="${n(row)}" patternUnits="userSpaceOnUse"><rect class="u-seatcush" x="${n(width * 0.1)}" y="${n(row * 0.1)}" width="${n(width * 0.8)}" height="${n(row * 0.55)}" rx=".2"/><rect class="u-seatback" x="${n(width * 0.1)}" y="${n(row * 0.72)}" width="${n(width * 0.8)}" height="${n(row * 0.2)}" rx=".12"/></pattern>`;
|
|
19
|
+
/** The blocks of seats along a row as one path of rectangles, and the aisles between them as another, from the balcony's own sections and seat width. Feet along the row, `row` deep. */
|
|
20
|
+
function seatBlocks(b, row) {
|
|
21
|
+
const w = b.seatWidth * K;
|
|
22
|
+
const aisle = b.aisle * K;
|
|
23
|
+
let x = 0;
|
|
24
|
+
let seats = "";
|
|
25
|
+
let aisles = "";
|
|
26
|
+
const pieces = [];
|
|
27
|
+
b.sections.forEach((k, i) => {
|
|
28
|
+
seats += `M${n(x)} 0h${n(k * w)}v${n(row)}h${n(-k * w)}Z`;
|
|
29
|
+
x += k * w;
|
|
30
|
+
if (i < b.sections.length - 1) {
|
|
31
|
+
aisles += `M${n(x)} 0h${n(aisle)}v${n(row)}h${n(-aisle)}Z`;
|
|
32
|
+
pieces.push([x, aisle]);
|
|
33
|
+
x += aisle;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return { seats, aisles, pieces };
|
|
37
|
+
}
|
|
38
|
+
/** The thinnest a mat's edge is drawn, in feet, so that the 5 cm of a competition mat does not vanish on a large floor. */
|
|
39
|
+
const LEAST_EDGE = 0.12;
|
|
40
|
+
/** A plane of the room that gives way to the eye: down to a hundredth, because the gallery's steps and its walls lie over one another on the screen and would add up to a veil. */
|
|
41
|
+
const shy = (p, centre) => ({ ...shyOf(p, centre), fadeTo: 0.01 });
|
|
42
|
+
const box = (b) => [b.x0 * K, b.y0 * K, b.x1 * K, b.y1 * K];
|
|
43
|
+
/** The planes of a box standing on the floor: four sides and a top, in feet. */
|
|
44
|
+
function block(id, kind, b, z0, z1, side, top, centre) {
|
|
45
|
+
const [x0, y0, x1, y1] = box(b);
|
|
46
|
+
const edge = (a, b2, c, d, i) => shy(wall(`${id}-${i}`, kind, a, b2, c, d, z0, z1, `<rect class="${side}" width="${n(Math.hypot(c - a, d - b2))}" height="${n(z1 - z0)}"/>`), centre);
|
|
47
|
+
return [edge(x0, y0, x1, y0, 1), edge(x1, y0, x1, y1, 2), edge(x1, y1, x0, y1, 3), edge(x0, y1, x0, y0, 4), flat(`${id}-top`, kind, x0, y0, x1, y1, z1, `<rect class="${top}" width="${n(x1 - x0)}" height="${n(y1 - y0)}"/>`)];
|
|
48
|
+
}
|
|
49
|
+
/** A wall of the room with its doors and windows drawn on it. */
|
|
50
|
+
function wallPlane(w, openings, centre, room) {
|
|
51
|
+
if (w.height === undefined || w.thickness === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const [ax, ay] = [w.from[0] * K, w.from[1] * K];
|
|
54
|
+
const [bx, by] = [w.to[0] * K, w.to[1] * K];
|
|
55
|
+
const long = Math.hypot(bx - ax, by - ay);
|
|
56
|
+
const dx = (bx - ax) / long;
|
|
57
|
+
const dy = (by - ay) / long;
|
|
58
|
+
const H = w.height * K;
|
|
59
|
+
// A dojo with a gallery has honey wood panelling the whole height of its walls, up to the gallery's floor (boards a metre or so wide, ours); one without has a cream wall over a wainscot of wood a belt high.
|
|
60
|
+
const panelled = room.balconies.length > 0;
|
|
61
|
+
const band = panelled ? H : Math.min(WAINSCOT * K, H);
|
|
62
|
+
let svg = panelled ? `<rect class="u-wainscot" width="${n(long)}" height="${n(H)}"/>` : `<rect class="u-room" width="${n(long)}" height="${n(H)}"/><rect class="u-wainscot" y="${n(H - band)}" width="${n(long)}" height="${n(band)}"/><path class="u-wainline" fill="none" d="M0 ${n(H - band)}H${n(long)}"/>`;
|
|
63
|
+
if (panelled) {
|
|
64
|
+
let seams = "";
|
|
65
|
+
for (let at = 1.2 * K; at < long; at += 1.2 * K)
|
|
66
|
+
seams += `M${n(at)} 0V${n(H)}`;
|
|
67
|
+
svg += `<path class="u-panel" fill="none" d="${seams}"/>`;
|
|
68
|
+
}
|
|
69
|
+
// The kamiza: on its wall a recessed alcove of darker wood with a white-framed shrine and a hanging scroll in it, a tall purple banner (plain) beside it; the dark banner of the room's data stands in front (ours).
|
|
70
|
+
const kamiza = room.banners.find((m) => m.wall === w.side);
|
|
71
|
+
if (panelled && kamiza !== undefined) {
|
|
72
|
+
const [bx0, by0, bx1, by1] = box(kamiza.box);
|
|
73
|
+
const centreAt = ((bx0 + bx1) / 2 - ax) * dx + ((by0 + by1) / 2 - ay) * dy;
|
|
74
|
+
const z = (m) => H - m * K;
|
|
75
|
+
const m2 = K;
|
|
76
|
+
svg += `<rect class="u-alcove" x="${n(centreAt - 2.2 * m2)}" y="${n(z(3.4))}" width="${n(4.4 * m2)}" height="${n(3.2 * m2)}"/><path class="u-frame" fill="none" d="M${n(centreAt - 2.2 * m2)} ${n(z(0.2))}V${n(z(3.4))}H${n(centreAt + 2.2 * m2)}V${n(z(0.2))}"/>`;
|
|
77
|
+
svg += `<rect class="u-shrinef" x="${n(centreAt - 1.7 * m2)}" y="${n(z(1.9))}" width="${n(1.1 * m2)}" height="${n(0.9 * m2)}"/><rect class="u-scroll" x="${n(centreAt + 0.9 * m2)}" y="${n(z(2.9))}" width="${n(0.55 * m2)}" height="${n(1.5 * m2)}"/>`;
|
|
78
|
+
svg += `<rect class="u-purple" x="${n(centreAt + 2.9 * m2)}" y="${n(z(3.5))}" width="${n(0.9 * m2)}" height="${n(2.7 * m2)}"/>`;
|
|
79
|
+
}
|
|
80
|
+
for (const { o, kind } of openings) {
|
|
81
|
+
if (o.wall !== w.side)
|
|
82
|
+
continue;
|
|
83
|
+
const [x0, y0, x1, y1] = box(o.box);
|
|
84
|
+
const s = [(x0 - ax) * dx + (y0 - ay) * dy, (x1 - ax) * dx + (y1 - ay) * dy].sort((p, q) => p - q);
|
|
85
|
+
const top = H - o.head * K;
|
|
86
|
+
const height = (o.head - o.sill) * K;
|
|
87
|
+
const width = s[1] - s[0];
|
|
88
|
+
svg += `<rect class="u-${kind}" x="${n(s[0])}" y="${n(top)}" width="${n(width)}" height="${n(height)}"/>`;
|
|
89
|
+
// A door has a leaf's panels; a window its sash, a cross.
|
|
90
|
+
svg += kind === "window" ? `<path class="u-sash" fill="none" d="M${n(s[0] + width / 2)} ${n(top)}v${n(height)}M${n(s[0])} ${n(top + height / 2)}h${n(width)}"/>` : `<path class="u-sash" fill="none" d="M${n(s[0] + width / 2)} ${n(top)}v${n(height)}"/>`;
|
|
91
|
+
}
|
|
92
|
+
return shy(wall(`wall-${w.side}`, "wall", ax, ay, bx, by, 0, H, svg), centre);
|
|
93
|
+
}
|
|
94
|
+
/** The banner on the kamiza's wall: a plain dark band, drawn on its own plane in front of the wall's inner face. */
|
|
95
|
+
function bannerPlane(room, centre) {
|
|
96
|
+
return room.banners.map((b) => {
|
|
97
|
+
const [x0, y0, x1, y1] = box(b.box);
|
|
98
|
+
const alongX = x1 - x0 >= y1 - y0;
|
|
99
|
+
// The face of the banner's footprint that is nearest the room.
|
|
100
|
+
const face = b.wall === "north" ? y1 : b.wall === "south" ? y0 : b.wall === "west" ? x1 : x0;
|
|
101
|
+
const [ax, ay, bx, by] = alongX ? [x0, face, x1, face] : [face, y0, face, y1];
|
|
102
|
+
const long = Math.hypot(bx - ax, by - ay);
|
|
103
|
+
const h = (b.top - b.bottom) * K;
|
|
104
|
+
return shy(wall(b.id, "banner", ax, ay, bx, by, b.bottom * K, b.top * K, `<rect class="u-banner" width="${n(long)}" height="${n(h)}"/><path class="u-bannerline" fill="none" d="M${n(long * 0.08)} ${n(h * 0.06)}H${n(long * 0.92)}M${n(long * 0.08)} ${n(h * 0.94)}H${n(long * 0.92)}"/>`), centre);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One balcony: its rows of seats as steps (a tread of seats and a riser to each row, the first row a step over the gallery's floor and the last at the height the gallery's data gives), a rail along its front
|
|
109
|
+
* edge, and a wall behind the last row. The room's wall stays below the gallery's floor: the gallery overlooks the floor over it.
|
|
110
|
+
*/
|
|
111
|
+
function balconyPlanes(b, pid, people, centre, room) {
|
|
112
|
+
const [x0, y0, x1, y1] = box(b.box);
|
|
113
|
+
// The direction away from the room, and the front edge of the balcony: the line along it nearest the room.
|
|
114
|
+
const out = b.side === "north" ? [0, -1] : b.side === "south" ? [0, 1] : b.side === "west" ? [-1, 0] : [1, 0];
|
|
115
|
+
const front = b.side === "north" ? [[x0, y1], [x1, y1]] : b.side === "south" ? [[x0, y0], [x1, y0]] : b.side === "west" ? [[x1, y0], [x1, y1]] : [[x0, y0], [x0, y1]];
|
|
116
|
+
const at = (offset) => [[front[0][0] + out[0] * offset, front[0][1] + out[1] * offset], [front[1][0] + out[0] * offset, front[1][1] + out[1] * offset]];
|
|
117
|
+
const floor = b.height * K;
|
|
118
|
+
const rise = b.rowRise * K;
|
|
119
|
+
const row = (b.box.x1 - b.box.x0 > b.box.y1 - b.box.y0 ? b.box.y1 - b.box.y0 : b.box.x1 - b.box.x0) * K / b.rows;
|
|
120
|
+
const planes = [];
|
|
121
|
+
const long = Math.hypot(front[1][0] - front[0][0], front[1][1] - front[0][1]);
|
|
122
|
+
for (let r = 0; r < b.rows; r += 1) {
|
|
123
|
+
const [a, c] = at(r * row);
|
|
124
|
+
const [d] = at((r + 1) * row);
|
|
125
|
+
const z = floor + (r + 1) * rise;
|
|
126
|
+
// The tread: a dark floor with the aisles across it in lighter wood and the blocks of seats between them, flat at the row's height. The riser under its front edge is dark too, with the stairs of the aisles in lighter wood.
|
|
127
|
+
const blocks = seatBlocks(b, row);
|
|
128
|
+
const tread = plane(`gallery-${b.side}-${r + 1}`, "seats", [a[0], a[1], z], [c[0] - a[0], c[1] - a[1], 0], [d[0] - a[0], d[1] - a[1], 0], `<rect class="u-gbed" width="${n(long)}" height="${n(row)}"/><path class="u-aisle" d="${blocks.aisles}"/><path fill="url(#${pid}-${b.side}-seat)" d="${blocks.seats}"/>${people ? `<path fill="url(#${pid}-cr-h)" d="${blocks.seats}"/>` : ""}`);
|
|
129
|
+
planes.push({ ...tread, shy: out, fadeTo: 0.01 });
|
|
130
|
+
const stairs = blocks.pieces.map(([x, w]) => `M${n(x)} 0h${n(w)}v${n(rise)}h${n(-w)}Z`).join("");
|
|
131
|
+
planes.push(shy(wall(`gallery-${b.side}-riser-${r + 1}`, "fascia", a[0], a[1], c[0], c[1], z - rise, z, `<rect class="u-gbed u-riser" width="${n(long)}" height="${n(rise)}"/><path class="u-aisle u-stair" d="${stairs}"/>`), centre));
|
|
132
|
+
}
|
|
133
|
+
// The floor slab projects from the wall over the room: its edge is a wall's thickness and half a metre in front of the first row, with a band of its thickness under it, and the rail stands on that edge.
|
|
134
|
+
const wallT = room.walls[0]?.thickness ?? 0.3;
|
|
135
|
+
const reach = (wallT + SLAB_PROJECTION) * K;
|
|
136
|
+
const [fa, fc] = at(-reach);
|
|
137
|
+
const slabT = SLAB * K;
|
|
138
|
+
planes.push({ ...plane(`gallery-${b.side}-slab`, "platform", [fa[0], fa[1], floor], [fc[0] - fa[0], fc[1] - fa[1], 0], [front[0][0] - fa[0], front[0][1] - fa[1], 0], `<rect class="u-slab" width="${n(long)}" height="${n(reach)}"/>`), shy: out, fadeTo: 0.01 });
|
|
139
|
+
planes.push(shy(wall(`gallery-${b.side}-slab-edge`, "fascia", fa[0], fa[1], fc[0], fc[1], floor - slabT, floor, `<rect class="u-slabedge" width="${n(long)}" height="${n(slabT)}"/>`), centre));
|
|
140
|
+
// The rail along the slab's edge, white: a top bar and posts about every half metre.
|
|
141
|
+
const rail = b.rail * K;
|
|
142
|
+
let posts = "";
|
|
143
|
+
for (let s2 = 0; s2 <= long + 0.01; s2 += (1 / 0.3048) / 2)
|
|
144
|
+
posts += `M${n(Math.min(s2, long))} 0V${n(rail)}`;
|
|
145
|
+
planes.push(shy(wall(`rail-${b.side}`, "rail", fa[0], fa[1], fc[0], fc[1], floor, floor + rail, `<path class="u-railline" fill="none" d="M0 0H${n(long)}M0 ${n(rail / 2)}H${n(long)}${posts}"/>`), centre));
|
|
146
|
+
// The wall behind the last row, up to the ceiling, with the large windows high in it on the side that has them.
|
|
147
|
+
const [e0, e1] = at(b.rows * row);
|
|
148
|
+
const top = room.ceiling === undefined ? floor + b.rows * rise + rail : room.ceiling.height * K;
|
|
149
|
+
// Plain off-white above the seats; on the kamiza's side a round clock at the middle and a door at each end, high in the wall (ours).
|
|
150
|
+
let back = `<rect class="u-room" width="${n(long)}" height="${n(top)}"/>`;
|
|
151
|
+
if (room.banners.some((m) => m.wall === b.side)) {
|
|
152
|
+
const seatsTop = floor + b.rows * rise + rail;
|
|
153
|
+
const cr = 0.6 * K;
|
|
154
|
+
const cy = top - (room.ceiling === undefined ? 1.6 : 2.2) * K;
|
|
155
|
+
back += `<circle class="u-clockface" cx="${n(long / 2)}" cy="${n(cy)}" r="${n(cr)}"/><path class="u-clockhands" fill="none" d="M${n(long / 2)} ${n(cy)}v${n(-cr * 0.7)}M${n(long / 2)} ${n(cy)}l${n(cr * 0.45)} ${n(cr * 0.2)}"/>`;
|
|
156
|
+
for (const at of [long * 0.06, long * 0.94 - 1.8 * K])
|
|
157
|
+
back += `<rect class="u-door" x="${n(at)}" y="${n(top - seatsTop > 0 ? top - (seatsTop + 0.4 * K + 2.1 * K) : 0)}" width="${n(1.8 * K)}" height="${n(2.1 * K)}"/>`;
|
|
158
|
+
}
|
|
159
|
+
for (const w of room.upperWindows) {
|
|
160
|
+
if (w.side !== b.side)
|
|
161
|
+
continue;
|
|
162
|
+
// The stretch along the wall is measured from the balcony's start, which is the wall's start side of the box.
|
|
163
|
+
const s0 = w.from * K;
|
|
164
|
+
const s1 = w.to * K;
|
|
165
|
+
back += `<rect class="u-window" x="${n(s0)}" y="${n(top - w.head * K)}" width="${n(s1 - s0)}" height="${n((w.head - w.sill) * K)}"/><path class="u-sash" fill="none" d="M${n((s0 + s1) / 2)} ${n(top - w.head * K)}v${n((w.head - w.sill) * K)}M${n(s0)} ${n(top - ((w.head + w.sill) / 2) * K)}h${n(s1 - s0)}"/>`;
|
|
166
|
+
}
|
|
167
|
+
planes.push(shy(wall(`gallery-back-${b.side}`, "fascia", e0[0], e0[1], e1[0], e1[1], 0, top, back), centre));
|
|
168
|
+
return planes;
|
|
169
|
+
}
|
|
170
|
+
/** The raised parts of a budō surface's room: what `dojoRoom()` says, for the options. An empty venue for a surface with no room. */
|
|
171
|
+
export function roomVenue(o, v, lift) {
|
|
172
|
+
const room = dojoRoom(o);
|
|
173
|
+
if (room === undefined)
|
|
174
|
+
return NO_VENUE;
|
|
175
|
+
const centre = [((room.floor.x0 + room.floor.x1) / 2) * K, ((room.floor.y0 + room.floor.y1) / 2) * K];
|
|
176
|
+
const items = [];
|
|
177
|
+
let defs = "";
|
|
178
|
+
// Walls with their openings.
|
|
179
|
+
const openings = [...room.doors.map((o2) => ({ o: o2, kind: "door" })), ...room.windows.map((o2) => ({ o: o2, kind: "window" }))];
|
|
180
|
+
const walls = room.walls.flatMap((w) => wallPlane(w, openings, centre, room) ?? []);
|
|
181
|
+
if (walls.length > 0)
|
|
182
|
+
items.push({ id: "walls", kind: "walls", at: centre, height: Math.max(...room.walls.map((w) => (w.height ?? 0) * K)), planes: walls });
|
|
183
|
+
// The seat of honour's dais, the banner and the shrine.
|
|
184
|
+
for (const p of room.platforms) {
|
|
185
|
+
if (p.id !== "dais")
|
|
186
|
+
continue;
|
|
187
|
+
items.push({ id: "dais", kind: "dais", at: [((p.box.x0 + p.box.x1) / 2) * K, ((p.box.y0 + p.box.y1) / 2) * K], height: p.height * K, planes: block("dais", "platform", p.box, 0, p.height * K, "u-dais-side", "u-dais", centre) });
|
|
188
|
+
}
|
|
189
|
+
if (room.banners.length > 0)
|
|
190
|
+
items.push({ id: "banner", kind: "banner", at: centre, height: Math.max(...room.banners.map((b) => b.top)) * K, planes: bannerPlane(room, centre) });
|
|
191
|
+
if (room.shrine !== undefined)
|
|
192
|
+
items.push({ id: "shrine", kind: "shrine", at: [((room.shrine.box.x0 + room.shrine.box.x1) / 2) * K, ((room.shrine.box.y0 + room.shrine.box.y1) / 2) * K], height: room.shrine.height * K, planes: block("shrine", "shrine", room.shrine.box, 0, room.shrine.height * K, "u-shrine-side", "u-shrine", centre) });
|
|
193
|
+
// The balconies, with the crowd of the drawing in them (a few people when it has one, the empty seats when it has none).
|
|
194
|
+
if (room.balconies.length > 0) {
|
|
195
|
+
const crowd = resolveCrowd(o.crowd);
|
|
196
|
+
const fine = (crowd === undefined || crowd.detail !== "plain") && o.detail !== "simple";
|
|
197
|
+
const own = crowd ?? resolveCrowd(true);
|
|
198
|
+
// The seats at a seat's 2 by 2.3 feet; a gallery is empty unless the options say how full.
|
|
199
|
+
const seats = room.balconies.reduce((sum, b) => sum + ((b.box.x1 - b.box.x0) * (b.box.y1 - b.box.y0) * K * K) / 4.6, 0);
|
|
200
|
+
const density = crowd === undefined ? 0 : crowd.density !== undefined || crowd.attendance !== undefined ? densityOf(own, seats, undefined) : 0;
|
|
201
|
+
const pid = `${v.id}-v`;
|
|
202
|
+
for (const bal of room.balconies)
|
|
203
|
+
defs += seatTile(`${pid}-${bal.side}`, bal.seatWidth * K, bal.rowDepth * K);
|
|
204
|
+
if (fine && density > 0)
|
|
205
|
+
defs += patterns(pid, own, density, undefined, true, true);
|
|
206
|
+
const planes = room.balconies.flatMap((b) => balconyPlanes(b, pid, density > 0 && fine, centre, room));
|
|
207
|
+
const top = Math.max(...room.balconies.map((b) => (b.height + b.rows * b.rowRise) * K));
|
|
208
|
+
items.push({ id: "gallery", kind: "gallery", at: centre, height: top, planes });
|
|
209
|
+
// A pale ceiling over the galleries when a roof is asked for: a strip over each, open over the floor so that the floor is never hidden.
|
|
210
|
+
if (v.roof === true && room.ceiling !== undefined) {
|
|
211
|
+
const z = room.ceiling.height * K;
|
|
212
|
+
// A pale grey band, with a box light every 6 m or so along it.
|
|
213
|
+
const ceiling = room.balconies.map((b) => {
|
|
214
|
+
const w = (b.box.x1 - b.box.x0) * K;
|
|
215
|
+
const h = (b.box.y1 - b.box.y0) * K;
|
|
216
|
+
const alongX = w >= h;
|
|
217
|
+
const long = alongX ? w : h;
|
|
218
|
+
const wide = alongX ? h : w;
|
|
219
|
+
let lights = "";
|
|
220
|
+
for (let at = 3 * K; at < long - 2 * K; at += 6 * K)
|
|
221
|
+
lights += alongX ? `<rect class="u-light" x="${n(at)}" y="${n(wide * 0.3)}" width="${n(1.2 * K)}" height="${n(wide * 0.4)}"/>` : `<rect class="u-light" x="${n(wide * 0.3)}" y="${n(at)}" width="${n(wide * 0.4)}" height="${n(1.2 * K)}"/>`;
|
|
222
|
+
return shy(flat(`ceiling-${b.side}`, "roof", ...box(b.box), z, `<rect class="u-ceiling" width="${n(w)}" height="${n(h)}"/>${lights}`), centre);
|
|
223
|
+
});
|
|
224
|
+
items.push({ id: "roof", kind: "roof", at: centre, height: z, planes: ceiling });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// The edge of the mats: they stand a few centimetres over the floor round them (the IJF's 5 cm), so that their thickness shows at the outer edge.
|
|
228
|
+
if (room.mats !== undefined) {
|
|
229
|
+
const m = room.surround?.inner ?? room.floor;
|
|
230
|
+
const t = Math.max(room.mats.thickness * K, LEAST_EDGE);
|
|
231
|
+
const [x0, y0, x1, y1] = box(m);
|
|
232
|
+
const side = room.mats.edge === "rounded" ? "u-matedge u-rounded" : "u-matedge";
|
|
233
|
+
const edge = (a, b, c, d, i) => shy(wall(`mat-edge-${i}`, "matedge", a, b, c, d, lift - t, lift, `<rect class="${side}" width="${n(Math.hypot(c - a, d - b))}" height="${n(t)}"/>`), centre);
|
|
234
|
+
items.push({ id: "mat-edge", kind: "mat-edge", at: centre, height: room.mats.thickness * K, planes: [edge(x0, y0, x1, y0, 1), edge(x1, y0, x1, y1, 2), edge(x1, y1, x0, y1, 3), edge(x0, y1, x0, y0, 4)] });
|
|
235
|
+
}
|
|
236
|
+
// The ground the balconies stand on: how far they reach beyond the floor.
|
|
237
|
+
const f = room.floor;
|
|
238
|
+
const reach = room.balconies.reduce((most, b) => Math.max(most, f.x0 - b.box.x0, f.y0 - b.box.y0, b.box.x1 - f.x1, b.box.y1 - f.y1), 0) * K;
|
|
239
|
+
return { items, defs, reach: reach > 0 ? reach + 3 : 0 };
|
|
240
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.8.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.8.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Kyogijo Stadiums 競技場, 39 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sports",
|