@johnmorrisdotca/kyogijo-stadiums 0.34.0 → 0.34.1

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 CHANGED
@@ -7,6 +7,12 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.34.1] - 2026-10-20
11
+
12
+ ### Fixed
13
+
14
+ - **No gray empty areas in the 3D view of a real venue** (`src/venue.fill.ts`, [the venues](./docs/venues.md), [the stadiums](./docs/stadiums.md)): the 3D arenas showed flat gray nothing in three places. **The shelf between the bowls** is now the suite level: a facing wall with a band of windows, a pane to every 7 feet, and a ledge. **The apron between the surface and the first row** is a thin dark floor, with the players' benches, the penalty boxes and the timekeeper's table at the usual places on the bench side of an ice rink. **The slab between the back of the bowl and the building** is closed by a back wall with a coping (it gives way to the eye as the building's walls do, keeping a little of itself) and a darker concourse floor with its lanes and joints, out to the building's outline, the annexes included. Every arena of the NHL, every stadium and the hall, and a tournament layout's stadium (`VenueSettings.fill`) have the fill; the usual stands (`crowd: true`) are unchanged. New part kinds `apron`, `bench` and `floor`; a ballpark gets the suite wall and the back wall, and no apron or floor of its own.
15
+
10
16
  ## [0.34.0] - 2026-10-20
11
17
 
12
18
  ### Added
@@ -177,7 +177,7 @@ export function buildMultiTurn(options, settings = {}) {
177
177
  let crowdStyle = "";
178
178
  if (drawn.stands !== undefined && keep("stands")) {
179
179
  const roof = typeof settings.venue === "object" && settings.venue.roof !== undefined ? settings.venue.roof : typeof options.stadium === "object" && options.stadium.roof === true;
180
- const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
180
+ const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { fill: true, ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
181
181
  items.push(...venue.items.filter((item) => item.kind !== "roof" || roof));
182
182
  defs = venue.defs;
183
183
  crowdStyle = "--ky-cr-home:#8b94a3;--ky-cr-away:#8b94a3";
@@ -1,7 +1,7 @@
1
1
  /** A point or a direction: x, y, z in feet. */
2
2
  export type V3 = readonly [number, number, number];
3
3
  /** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
4
- export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign";
4
+ export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor";
5
5
  /** One flat rectangle. Its own x runs along `u` (its whole length) and its own y along `v`; `o` is the corner both start from. `svg` is what is drawn on it, in feet along `u` and `v`. */
6
6
  export interface Plane {
7
7
  /** A name that says what it is and where: `net`, `post-left`, `goal-right-back`. */
@@ -74,6 +74,24 @@ export const KYOGIJO_TURN_STYLE = `
74
74
  .ky-turn .u-cable{fill:none;stroke:var(--ky-up-roofline);stroke-width:1}
75
75
  .ky-turn .u-fabric{fill:var(--ky-up-roof);fill-opacity:.55;stroke:none}
76
76
  .ky-turn .u-pod{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
77
+ /* A real venue's empty places, filled: the suite level between two bowls, the apron round the surface, the back of each bowl and the concourse floor behind it. */
78
+ .ky-turn .u-suite{fill:color-mix(in srgb,var(--ky-up-fascia) 78%,var(--ky-cr-bed))}
79
+ .ky-turn .u-suite-win{fill:color-mix(in srgb,var(--ky-cr-gap) 55%,#6f93b3)}
80
+ .ky-turn .u-suite-bar{stroke:var(--ky-up-fascia);stroke-width:.5}
81
+ .ky-turn .u-suite-ledge{fill:color-mix(in srgb,var(--ky-up-fascia) 60%,var(--ky-cr-conc))}
82
+ .ky-turn .u-apron{fill:color-mix(in srgb,var(--ky-cr-bed) 82%,#000)}
83
+ .ky-turn .u-timeb{fill:color-mix(in srgb,var(--ky-cr-conc) 70%,#3a3f46)}
84
+ .ky-turn .u-timeb-side{fill:color-mix(in srgb,var(--ky-cr-conc) 70%,#3a3f46);filter:brightness(.7)}
85
+ .ky-turn .u-penb{fill:color-mix(in srgb,var(--ky-up-glass,#bcd2e0) 80%,#5d7a8f)}
86
+ .ky-turn .u-penb-side{fill:color-mix(in srgb,var(--ky-up-glass,#bcd2e0) 80%,#5d7a8f);filter:brightness(.72)}
87
+ .ky-turn .u-teamb{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36)}
88
+ .ky-turn .u-teamb-side{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36);filter:brightness(.7)}
89
+ .ky-turn .u-back{fill:color-mix(in srgb,var(--ky-up-fascia) 66%,var(--ky-cr-bed))}
90
+ .ky-turn .u-backrib{stroke:var(--ky-cr-bed);stroke-width:.7;stroke-opacity:.7}
91
+ .ky-turn .u-coping{fill:color-mix(in srgb,var(--ky-up-fascia) 70%,var(--ky-cr-conc))}
92
+ .ky-turn .u-cfloor{fill:color-mix(in srgb,var(--ky-cr-bed) 60%,var(--ky-cr-conc))}
93
+ .ky-turn .u-clane{stroke:color-mix(in srgb,var(--ky-cr-conc) 55%,#fff);stroke-opacity:.6;stroke-width:1.1}
94
+ .ky-turn .kyogijo .ky-cr-shell:not(.ky-cr-ivy){fill:color-mix(in srgb,var(--ky-cr-bed) 70%,var(--ky-cr-conc));fill-opacity:.9}
77
95
  .ky-turn .ky-up-seats svg{opacity:var(--ky-cr-dim)}
78
96
  .ky-turn .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
79
97
  .ky-turn .ky-cr-h{stroke:var(--ky-cr-home)}
package/dist/venue.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface VenueSettings {
10
10
  roofOpen?: boolean;
11
11
  /** The walls of a real venue's building (`crowd: { real: true }`), raised on its outline: they follow `roof` where left out (a roof and walls together, as before), and `walls: true` with `roof` off raises the walls alone, `roof: true` with `walls: false` the roof alone. */
12
12
  walls?: boolean;
13
+ /** Fill the empty places of the stands (the apron round the surface, the back of each bowl): on for a real venue's, off for the usual stands (where it is a tournament layout's hall that asks for it). */
14
+ fill?: boolean;
13
15
  /** How finely the curves are cut: `low` a facet to each corner, `auto` (the default) three, `high` six; an oval is 12, 28 or 56 facets round. */
14
16
  detail?: Cut;
15
17
  }
@@ -0,0 +1,40 @@
1
+ import { type CrowdStands, type Outline, type Pt, type StandRing } from "./crowd.ts";
2
+ import { type Plane } from "./turn.kit.ts";
3
+ import { type Cut } from "./venue.geom.ts";
4
+ /** The suite level's windows stand between these shares of the way across the facing wall (the bottom is a parapet, the top a ledge), and a pane is about this wide, in feet (ours). */
5
+ export declare const SUITE: {
6
+ readonly from: 0.3;
7
+ readonly to: 0.82;
8
+ readonly pane: 7;
9
+ };
10
+ /** The thin floor of the apron lies this far over the surface, in feet, so that it shows over the floor's own drawing. */
11
+ export declare const APRON_RISE = 0.06;
12
+ /** The bench side's furniture, in feet along the boards from the middle of the rink: the timekeeper's table, the penalty boxes either side of it, and each team's bench beyond (ours: the usual layout of a rink, no plan of a real one). */
13
+ export declare const BENCH_SIDE: {
14
+ readonly table: 6;
15
+ readonly penalty: readonly [8, 20];
16
+ readonly bench: readonly [24, 62];
17
+ readonly depth: 5;
18
+ readonly gap: 1.5;
19
+ readonly height: 3.4;
20
+ };
21
+ /** How much of a bowl's back wall is left where it is between the eye and the surface (the walls of the building take 0.02: the back of a bowl keeps a little of its body). */
22
+ export declare const BACK_FADE = 0.08;
23
+ /** A piece of the concourse floor smaller than this, in square feet, is a sliver a corner leaves (where the hall's rim meets the building's outline): it is left to the floor drawn under it, which has the same dark tone. */
24
+ export declare const SLIVER = 80;
25
+ /** The suite level seen square on: the facing wall of a quad `[inner a, inner b, outer b, outer a]` (the plane's own corners), with a band of windows and a pane to every `SUITE.pane` feet. */
26
+ export declare function suiteWall(corners: Pt[]): string;
27
+ /** The apron: the floor between the playing surface and the first row, a thin dark floor, and where the bowl is an ice rink's the benches, the penalty boxes and the timekeeper's table on the bench side. */
28
+ export declare function apronPlanes(stands: CrowdStands, first: StandRing, cut: Cut, lift: number): Plane[];
29
+ /** The plan of a ring's back: the outer edge of every strip of a tier that no walk leads on from, as the loop of points the wall is raised on. */
30
+ export declare const backEdges: (ring: StandRing, cut: Cut) => Pt[][];
31
+ /** The back wall of one bowl: from the floor up to the top row and a coping over it, on each stretch of the edge, painted with its ribs. It gives way to the eye as the walls of the building do, but keeps a little of itself. */
32
+ export declare function backWall(id: string, edge: readonly Pt[], top: number, lift: number, centre: readonly [number, number]): Plane[];
33
+ /**
34
+ * The concourse floor behind a bowl: flat quads, painted dark with two lanes, in rays from the middle: from the bowl's outer edge out to the rim of the hall (the nearer of the hall's rounded outline and the
35
+ * building's own), and from there on out to the building's outline, where the building has annexes beyond the hall (the walls and the roof stand on the hall only; the rest is floor too, not a bare sheet).
36
+ * The two rings are cut at the rim, so that a wall standing on it is drawn between them in the right order.
37
+ */
38
+ export declare function concourseFloor(id: string, edge: Outline, middle: Pt, rim: (angle: number) => Pt, far: (angle: number) => Pt | undefined, steps: number, lift: number): Plane[];
39
+ /** The rim of a real venue's hall along an angle from `middle`: the nearer of the hall's rounded outline and the building's own outline. */
40
+ export declare const rimAt: (hall: Outline, footprint: readonly Pt[], middle: Pt) => (angle: number) => Pt;
@@ -0,0 +1,174 @@
1
+ // What fills the empty places of a real venue's 3D view, so that no big flat sheet is left in it: the suite level between two bowls (a facing wall with a band of windows, not a bare shelf), the apron between the
2
+ // playing surface and the first row (a thin dark floor, and for ice hockey the players' benches, the penalty boxes and the timekeeper's table), the back of each bowl (a wall that closes it) and, between that wall and
3
+ // the walls of the hall, a darker concourse floor with its lanes. All of it is ours, plausible and no plan of a real building: where a source gives a figure for none of it, the sizes are the usual ones.
4
+ // Pure: data in, planes out.
5
+ import { rayAt } from "./crowd.js";
6
+ import { NHL } from "./geometry.js";
7
+ import { n, shy, wall, box } from "./turn.kit.js";
8
+ import { areaOf, pointList, raked, stripsOf, tidy } from "./venue.geom.js";
9
+ import { firstHit } from "./venue.real.js";
10
+ /** The suite level's windows stand between these shares of the way across the facing wall (the bottom is a parapet, the top a ledge), and a pane is about this wide, in feet (ours). */
11
+ export const SUITE = { from: 0.3, to: 0.82, pane: 7 };
12
+ /** The thin floor of the apron lies this far over the surface, in feet, so that it shows over the floor's own drawing. */
13
+ export const APRON_RISE = 0.06;
14
+ /** The bench side's furniture, in feet along the boards from the middle of the rink: the timekeeper's table, the penalty boxes either side of it, and each team's bench beyond (ours: the usual layout of a rink, no plan of a real one). */
15
+ export const BENCH_SIDE = { table: 6, penalty: [8, 20], bench: [24, 62], depth: 5, gap: 1.5, height: 3.4 };
16
+ /** How much of a bowl's back wall is left where it is between the eye and the surface (the walls of the building take 0.02: the back of a bowl keeps a little of its body). */
17
+ export const BACK_FADE = 0.08;
18
+ /** How high the coping stands over the top row on the back wall, in feet, and the walks of the concourse floor are this many feet from its two edges (ours). */
19
+ const COPING = 3;
20
+ /** The joints of the concourse floor: one about every this many feet along it, at most so many to a quad (ours). */
21
+ const JOINT = 22;
22
+ /** A piece of the concourse floor smaller than this, in square feet, is a sliver a corner leaves (where the hall's rim meets the building's outline): it is left to the floor drawn under it, which has the same dark tone. */
23
+ export const SLIVER = 80;
24
+ const JOINTS_MOST = 8;
25
+ const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
26
+ const flatAt = (z) => ({ x0: 0, y0: 0, z0: z, gx: 0, gy: 0 });
27
+ /** The suite level seen square on: the facing wall of a quad `[inner a, inner b, outer b, outer a]` (the plane's own corners), with a band of windows and a pane to every `SUITE.pane` feet. */
28
+ export function suiteWall(corners) {
29
+ const base = `<polygon class="u-suite" points="${pointList(corners)}"/>`;
30
+ if (corners.length !== 4)
31
+ return base;
32
+ const [a, b, c, d] = corners;
33
+ const across = (s, t) => lerp(lerp(a, b, s), lerp(d, c, s), t);
34
+ const panes = Math.max(1, Math.round(Math.hypot(b[0] - a[0], b[1] - a[1]) / SUITE.pane));
35
+ const band = `<polygon class="u-suite-win" points="${pointList([across(0, SUITE.from), across(1, SUITE.from), across(1, SUITE.to), across(0, SUITE.to)])}"/>`;
36
+ let bars = "";
37
+ for (let j = 1; j < panes; j += 1) {
38
+ const [p, q] = [across(j / panes, SUITE.from), across(j / panes, SUITE.to)];
39
+ bars += `M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`;
40
+ }
41
+ const ledge = `<polygon class="u-suite-ledge" points="${pointList([across(0, SUITE.to), across(1, SUITE.to), across(1, SUITE.to + 0.07), across(0, SUITE.to + 0.07)])}"/>`;
42
+ return `${base}${band}${bars === "" ? "" : `<path class="u-suite-bar" fill="none" d="${bars}"/>`}${ledge}`;
43
+ }
44
+ /** The floor strips of a ring of quads painted as a floor: a base and the walks along it, at the shares `lanes` of the way across. */
45
+ function floorOf(cls, lanes) {
46
+ return (corners) => {
47
+ const base = `<polygon class="${cls}" points="${pointList(corners)}"/>`;
48
+ if (corners.length === 3) {
49
+ // A triangle (where two rays meet at a corner of the building): its outline drawn again, smaller, round its middle.
50
+ const mid = [(corners[0][0] + corners[1][0] + corners[2][0]) / 3, (corners[0][1] + corners[1][1] + corners[2][1]) / 3];
51
+ const inner = lanes.map((t) => `M${corners.map((p) => `${n(mid[0] + (p[0] - mid[0]) * (1 - t))} ${n(mid[1] + (p[1] - mid[1]) * (1 - t))}`).join("L")}Z`);
52
+ return `${base}<path class="u-clane" fill="none" d="${inner.join("")}"/>`;
53
+ }
54
+ if (corners.length !== 4)
55
+ return base;
56
+ const [a, b, c, d] = corners;
57
+ const lines = lanes.map((t) => {
58
+ const [p, q] = [lerp(a, d, t), lerp(b, c, t)];
59
+ return `M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`;
60
+ });
61
+ // Joints across the floor, a paving's worth, so that a wide floor is not one flat sheet.
62
+ const joints = Math.min(JOINTS_MOST, Math.max(0, Math.round(Math.hypot(b[0] - a[0], b[1] - a[1]) / JOINT)));
63
+ for (let j = 1; j < joints; j += 1) {
64
+ const [p, q] = [lerp(a, b, j / joints), lerp(d, c, j / joints)];
65
+ lines.push(`M${n(p[0])} ${n(p[1])}L${n(q[0])} ${n(q[1])}`);
66
+ }
67
+ return `${base}<path class="u-clane" fill="none" d="${lines.join("")}"/>`;
68
+ };
69
+ }
70
+ /** The quads of a strip, as `[inner i, inner i + 1, outer i + 1, outer i]`, those with a width. */
71
+ function* quadsOf(strip) {
72
+ for (let i = 0; i + 1 < strip.inner.length; i += 1) {
73
+ const q = [strip.inner[i], strip.inner[i + 1], strip.outer[i + 1], strip.outer[i]];
74
+ if (Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]) < 1e-6 && Math.hypot(q[2][0] - q[3][0], q[2][1] - q[3][1]) < 1e-6)
75
+ continue;
76
+ yield q;
77
+ }
78
+ }
79
+ /** The direction along a quad's first edge (or its outer edge where the inner has shrunk to a point). */
80
+ const alongOf = (q) => {
81
+ const [from, to] = Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]) >= Math.hypot(q[2][0] - q[3][0], q[2][1] - q[3][1]) ? [q[0], q[1]] : [q[3], q[2]];
82
+ const long = Math.hypot(to[0] - from[0], to[1] - from[1]);
83
+ return long < 1e-9 ? [1, 0] : [(to[0] - from[0]) / long, (to[1] - from[1]) / long];
84
+ };
85
+ /** A ring of flat quads lying at `z`, between two outlines, painted as `paint` says, with the ids `${id}-${i}`. */
86
+ function floorRing(id, kind, strips, z, paint, least = 0.05) {
87
+ const planes = [];
88
+ let count = 0;
89
+ for (const strip of strips) {
90
+ for (const q of quadsOf(strip)) {
91
+ const poly = tidy(q);
92
+ if (poly.length < 3 || Math.abs(areaOf(poly)) < least)
93
+ continue;
94
+ count += 1;
95
+ const p = raked(`${id}-${count}`, kind, poly, flatAt(z), alongOf(q), (corners) => paint(corners));
96
+ if (p !== undefined)
97
+ planes.push(p);
98
+ }
99
+ }
100
+ return planes;
101
+ }
102
+ /** The apron: the floor between the playing surface and the first row, a thin dark floor, and where the bowl is an ice rink's the benches, the penalty boxes and the timekeeper's table on the bench side. */
103
+ export function apronPlanes(stands, first, cut, lift) {
104
+ const hole = first.hole;
105
+ if (hole === null || hole.kind !== "rrect")
106
+ return [];
107
+ const b = stands.box;
108
+ const rink = { kind: "rrect", x0: b.x, y0: b.y, x1: b.x + b.w, y1: b.y + b.h, r: stands.sport === "nhl" ? NHL.cornerRadius : 0 };
109
+ const planes = floorRing("apron", "apron", stripsOf(hole, rink, [], cut), lift + APRON_RISE, (corners) => `<polygon class="u-apron" points="${pointList(corners)}"/>`);
110
+ if (stands.sport !== "nhl")
111
+ return planes;
112
+ // The bench side is the one with the deeper apron (the NHL's first row stands further from that side); the furniture stands in the clear between the boards and the first row.
113
+ const bottom = hole.y1 - (b.y + b.h) >= b.y - hole.y0;
114
+ const depth = bottom ? hole.y1 - (b.y + b.h) : b.y - hole.y0;
115
+ const need = BENCH_SIDE.gap + BENCH_SIDE.depth;
116
+ if (depth < need + 1)
117
+ return planes;
118
+ const [y0, y1] = bottom ? [b.y + b.h + BENCH_SIDE.gap, b.y + b.h + need] : [b.y - need, b.y - BENCH_SIDE.gap];
119
+ const mid = b.x + b.w / 2;
120
+ const put = (id, from, to, cls) => box(id, "bench", { x0: mid + from, y0, x1: mid + to, y1 }, BENCH_SIDE.height, cls);
121
+ const furniture = [...put("timekeeper", -BENCH_SIDE.table, BENCH_SIDE.table, "u-timeb")];
122
+ for (const sign of [-1, 1]) {
123
+ const span = (lo, hi) => (sign < 0 ? [-hi, -lo] : [lo, hi]);
124
+ furniture.push(...put(`penalty-${sign < 0 ? "a" : "b"}`, ...span(BENCH_SIDE.penalty[0], BENCH_SIDE.penalty[1]), "u-penb"));
125
+ furniture.push(...put(`bench-${sign < 0 ? "a" : "b"}`, ...span(BENCH_SIDE.bench[0], BENCH_SIDE.bench[1]), "u-teamb"));
126
+ }
127
+ return [...planes, ...furniture];
128
+ }
129
+ /** The plan of a ring's back: the outer edge of every strip of a tier that no walk leads on from, as the loop of points the wall is raised on. */
130
+ export const backEdges = (ring, cut) => stripsOf(ring.region, ring.hole, ring.more ?? [], cut).map((strip) => strip.outer);
131
+ /** The back wall of one bowl: from the floor up to the top row and a coping over it, on each stretch of the edge, painted with its ribs. It gives way to the eye as the walls of the building do, but keeps a little of itself. */
132
+ export function backWall(id, edge, top, lift, centre) {
133
+ const planes = [];
134
+ for (let i = 0; i + 1 < edge.length; i += 1) {
135
+ const [a, b] = [edge[i], edge[i + 1]];
136
+ const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
137
+ if (long < 0.5)
138
+ continue;
139
+ const high = lift + top;
140
+ const ribs = Math.max(1, Math.round(long / 14));
141
+ let lines = "";
142
+ for (let j = 1; j < ribs; j += 1)
143
+ lines += `M${n((long * j) / ribs)} ${n(COPING)}V${n(high)}`;
144
+ const svg = `<rect class="u-back" width="${n(long)}" height="${n(high)}"/>${lines === "" ? "" : `<path class="u-backrib" fill="none" d="${lines}"/>`}<rect class="u-coping" width="${n(long)}" height="${n(COPING)}"/>`;
145
+ planes.push({ ...shy(wall(`${id}-${i}`, "wall", a[0], a[1], b[0], b[1], 0, high, svg), centre), fadeTo: BACK_FADE });
146
+ }
147
+ return planes;
148
+ }
149
+ /**
150
+ * The concourse floor behind a bowl: flat quads, painted dark with two lanes, in rays from the middle: from the bowl's outer edge out to the rim of the hall (the nearer of the hall's rounded outline and the
151
+ * building's own), and from there on out to the building's outline, where the building has annexes beyond the hall (the walls and the roof stand on the hall only; the rest is floor too, not a bare sheet).
152
+ * The two rings are cut at the rim, so that a wall standing on it is drawn between them in the right order.
153
+ */
154
+ export function concourseFloor(id, edge, middle, rim, far, steps, lift) {
155
+ const dist = (p) => Math.hypot(p[0] - middle[0], p[1] - middle[1]);
156
+ const angles = Array.from({ length: steps + 1 }, (_, k) => (2 * Math.PI * k) / steps);
157
+ const inner = angles.map((a) => rayAt(edge, middle[0], middle[1], a));
158
+ const rims = angles.map((a, k) => {
159
+ const r = rim(a);
160
+ return dist(r) > dist(inner[k]) + 0.5 ? r : inner[k];
161
+ });
162
+ const outs = angles.map((a, k) => {
163
+ const f = far(a);
164
+ return f !== undefined && dist(f) > dist(rims[k]) + 0.5 ? f : rims[k];
165
+ });
166
+ const paint = floorOf("u-cfloor", [0.3, 0.62]);
167
+ return [...floorRing(`${id}-in`, "floor", [{ inner, outer: rims }], lift + APRON_RISE, paint, SLIVER), ...floorRing(`${id}-out`, "floor", [{ inner: rims, outer: outs }], lift + APRON_RISE, paint, SLIVER)];
168
+ }
169
+ /** The rim of a real venue's hall along an angle from `middle`: the nearer of the hall's rounded outline and the building's own outline. */
170
+ export const rimAt = (hall, footprint, middle) => (angle) => {
171
+ const cap = rayAt(hall, middle[0], middle[1], angle);
172
+ const out = firstHit(footprint, middle, angle);
173
+ return out !== undefined && Math.hypot(out[0] - middle[0], out[1] - middle[1]) < Math.hypot(cap[0] - middle[0], cap[1] - middle[1]) ? out : cap;
174
+ };
package/dist/venue.js CHANGED
@@ -7,7 +7,8 @@ import { patterns } from "./crowd.draw.js";
7
7
  import { crowdStands } from "./draw.js";
8
8
  import { groundById } from "./grounds.js";
9
9
  import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
10
- import { realBuilding } from "./venue.real.js";
10
+ import { apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
11
+ import { firstHit, realBuilding } from "./venue.real.js";
11
12
  import { n, shy, wall } from "./turn.kit.js";
12
13
  export const NO_VENUE = { items: [], defs: "", reach: 0 };
13
14
  /** The sports played under a roof: their stands have a lid and not a canopy. */
@@ -51,6 +52,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
51
52
  const covers = stands.covers;
52
53
  const inCover = (p) => covers.some(([x0, y0, x1, y1]) => p[0] >= x0 && p[0] <= x1 && p[1] >= y0 && p[1] <= y1);
53
54
  const pieces = (poly) => (covers.length === 0 ? [poly] : clipAround(poly, covers));
55
+ const filled = v.fill ?? stands.real !== undefined;
54
56
  const seatPlanes = [];
55
57
  const wallSeats = [];
56
58
  let count = 0;
@@ -138,12 +140,31 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
138
140
  continue;
139
141
  for (const piece of pieces(tidy(q))) {
140
142
  count += 1;
141
- const p = raked(`walk-${count}`, "concourse", piece, slope.slope, slope.along, (corners) => `<polygon class="u-conc" points="${pointList(corners)}"/>`);
143
+ // Where the empty places are filled, the walk between two tiers is the suite level: a facing wall with a band of windows, not a bare shelf.
144
+ const p = raked(`walk-${count}`, "concourse", piece, slope.slope, slope.along, (corners) => (filled ? suiteWall(corners) : `<polygon class="u-conc" points="${pointList(corners)}"/>`));
142
145
  if (p !== undefined)
143
146
  seatPlanes.push({ ...p, shy: slope.out });
144
147
  }
145
148
  }
146
149
  }
150
+ // The empty places are filled (see `venue.fill.ts`): the apron round the surface and the back of each bowl, and, for a real venue's building, the concourse floor behind the bowl.
151
+ if (filled) {
152
+ const first = stands.rings.find((r) => r.tier !== "wall" && r.tier !== "bleachers") ?? stands.rings[0];
153
+ if (first !== undefined)
154
+ seatPlanes.push(...apronPlanes(stands, first, cut, lift));
155
+ const real = stands.real;
156
+ const middle = [centre[0], centre[1]];
157
+ const behind = new Set(links.map((l) => l.before));
158
+ for (const ring of stands.rings) {
159
+ if (ring.tier === "wall" || behind.has(ring))
160
+ continue;
161
+ backEdges(ring, cut).forEach((edge, k) => {
162
+ seatPlanes.push(...backWall(`back-${ring.id}${k === 0 ? "" : `-s${k}`}`, edge, ring.height[1], lift, centre));
163
+ if (real !== undefined && ring.region.kind !== "poly" && k === 0)
164
+ seatPlanes.push(...concourseFloor(`floor-${ring.id}`, ring.region, middle, rimAt(real.hall, real.footprint, middle), (a) => firstHit(real.footprint, middle, a), ARC_STEPS[cut].oval, lift));
165
+ });
166
+ }
167
+ }
147
168
  const items = [];
148
169
  if (seatPlanes.length > 0)
149
170
  items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
@@ -7,6 +7,8 @@ export declare const SADDLE_RISE = 30;
7
7
  export declare function saddleAt(real: Pick<RealShell, "footprint">, [x, y]: Pt): number;
8
8
  /** The height of the roof over a point of the building's edge, above the surface, in feet. */
9
9
  export declare const roofAt: (real: RealShell, p: Pt) => number;
10
+ /** The point where a ray from `from` first meets a polygon, or `undefined`. */
11
+ export declare function firstHit(poly: readonly Pt[], from: Pt, angle: number): Pt | undefined;
10
12
  /**
11
13
  * The walls and the roof of a real venue (`parts` says which are made). The hall is the building's outline cut to the bowl's wall and a margin (`RealShell.hall`): its rim is where a ray from the middle meets the one or the other first, in `steps`
12
14
  * rays. A wall stands on each stretch of the rim, up to the roof's height over it; the roof is a band from the rim in to the inner edge of the top tier (the middle stays open, as the 3D view's roofs are).
@@ -21,7 +21,7 @@ export function saddleAt(real, [x, y]) {
21
21
  /** The height of the roof over a point of the building's edge, above the surface, in feet. */
22
22
  export const roofAt = (real, p) => real.edge + (real.roof === "saddle" ? SADDLE_RISE * saddleAt(real, p) : 0);
23
23
  /** The point where a ray from `from` first meets a polygon, or `undefined`. */
24
- function firstHit(poly, from, angle) {
24
+ export function firstHit(poly, from, angle) {
25
25
  const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
26
26
  let best;
27
27
  poly.forEach(([ax, ay], i) => {
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.34.0";
2
+ export declare const VERSION = "0.34.1";
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.34.0";
2
+ export const VERSION = "0.34.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "Kyogijo Stadiums 競技場, 40 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, the MMA cage, 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",