@johnmorrisdotca/kyogijo-stadiums 0.43.0 → 0.44.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.
@@ -0,0 +1,34 @@
1
+ import { SEATTLE_CENTER, SEATTLE_CITY, SEATTLE_SODO, VANCOUVER_NATURE } from "./cityctx.data.js";
2
+ import { VANCOUVER_CENTRE } from "./osm.context.data.js";
3
+ /** The venues each area is drawn round, by their ids (the part of a district's key before the colon). */
4
+ export const CITYCTX_VENUES = Object.freeze({
5
+ vancouver: ["bc-place", "rogers-arena"],
6
+ sodo: ["lumen-field", "t-mobile-park"],
7
+ center: ["climate-pledge-arena"],
8
+ });
9
+ /**
10
+ * The areas of context that go with some venues, for a district's `context` option: downtown Vancouver's buildings and streets (`VANCOUVER_CENTRE`, of `./osm`) with its water, green and trees (`VANCOUVER_NATURE`) for BC Place and
11
+ * Rogers Arena; Seattle's SoDo for Lumen Field and T-Mobile Park, Seattle Center's for Climate Pledge Arena, and the city's own, with streets and the buildings that matter from far, for a district that holds venues of both (they are
12
+ * three kilometres apart, so the detail of either would be a speck). A venue of neither city has none.
13
+ * @example
14
+ * ```ts
15
+ * import { contextFor } from "@johnmorrisdotca/kyogijo-stadiums/cityctx";
16
+ * console.log(contextFor(["bc-place:cfl", "rogers-arena"]).map((area) => area.key));
17
+ * // [ 'vancouver', 'vancouver-nature' ]
18
+ * ```
19
+ */
20
+ export function contextFor(venues) {
21
+ const ids = new Set(venues.map((key) => key.split(":")[0]));
22
+ const has = (list) => list.some((id) => ids.has(id));
23
+ const areas = [];
24
+ if (has(CITYCTX_VENUES.vancouver))
25
+ areas.push(VANCOUVER_CENTRE, VANCOUVER_NATURE);
26
+ const [sodo, center] = [has(CITYCTX_VENUES.sodo), has(CITYCTX_VENUES.center)];
27
+ if (sodo && center)
28
+ areas.push(SEATTLE_CITY);
29
+ else if (sodo)
30
+ areas.push(SEATTLE_SODO);
31
+ else if (center)
32
+ areas.push(SEATTLE_CENTER);
33
+ return areas;
34
+ }
package/dist/context.d.ts CHANGED
@@ -17,9 +17,12 @@ export interface ContextLayer {
17
17
  rows: readonly (readonly number[])[];
18
18
  }
19
19
  /**
20
- * An area of context round a district: ONE coordinate frame (the origin, the grid step and the radius, in metres east and north of the origin) and separate layers laid in it. Today there is one layer, `buildings`; trees and land
21
- * cover are added beside it later as layers of their own, each with its own source and licence, with no change to the frame or to the layers that are there. A layer a caller does not ask for or an area lacks is simply not drawn.
22
- * The row of a `buildings` layer: the height and the rise of the roof (0 for a flat one) in grid steps, then the first point (east, north) and each step to the next, in grid steps from the origin.
20
+ * An area of context round a district: ONE coordinate frame (the origin, the grid step and the radius, in metres east and north of the origin) and separate layers laid in it, each with its own source and licence. A layer an
21
+ * area lacks is simply not drawn. The rows of each layer, all in grid steps from the origin:
22
+ * - `buildings`: the height and the rise of the roof (0 for a flat one), then the first point (east, north) and each step to the next.
23
+ * - `streets`: the kind (0 a carriageway at street level, 1 one on a viaduct or a bridge, 2 a railway, 3 a sidewalk or a path) and the width in grid steps, then the first point and each step to the next, a line.
24
+ * - `landCover`: the class (0 water, 1 green, 2 wood), then the first point and each step to the next, an outline (a hole is an outline of its own, filled even-odd).
25
+ * - `trees`: one row: the first tree's east, north and crown radius, then each next tree's step in east and north from the one before and its crown radius.
23
26
  */
24
27
  export interface ContextArea {
25
28
  key: string;
@@ -55,7 +58,7 @@ export interface ContextBlock {
55
58
  area: number;
56
59
  /** A venue stands in this building (Korakuen Hall's building, which would hide it): it is drawn as a ghost. */
57
60
  ghost: boolean;
58
- /** The middle a see-through building's walls give way from, in feet: the venue raised in it (the Kodokan's main dojo). Set only by `shellBlock`. */
61
+ /** The middle of what is seen through a see-through building (the room or rooms raised in it), in feet: each wall gives way to the eye when the eye is on its side of that middle. Set only by `shellBlock`. */
59
62
  xray?: Pt;
60
63
  /** Whether the building lies wholly inside the plan's frame: only such a building stands up in the 3D view, one the frame cuts is a part of the floor. */
61
64
  whole: boolean;
@@ -66,11 +69,26 @@ export interface ContextStreet {
66
69
  width: number;
67
70
  points: Pt[];
68
71
  }
72
+ /** A water, green or wood outline in feet in the district's frame (a hole is an outline of its own). */
73
+ export interface ContextShape {
74
+ /** 0 water, 1 green, 2 wood. */
75
+ cls: number;
76
+ ring: Pt[];
77
+ }
78
+ /** A tree in feet in the district's frame, its crown's radius in feet. */
79
+ export interface ContextTree {
80
+ at: Pt;
81
+ r: number;
82
+ }
69
83
  /** What a district's context is: the buildings kept, and how many were left out because they are a venue's own. */
70
84
  export interface ContextPlan {
71
85
  blocks: ContextBlock[];
72
86
  /** The streets, from a `streets` layer: drawn flat on the floor, under the buildings. */
73
87
  streets?: ContextStreet[];
88
+ /** The water, green and wood outlines of a `landCover` layer that reach the frame: drawn flat, under the streets. */
89
+ land?: ContextShape[];
90
+ /** The trees of a `trees` layer inside the frame: drawn as dots, over the streets and under the buildings. */
91
+ trees?: ContextTree[];
74
92
  /** The buildings of the model that are a venue (the Dome's own parts), left out: the venue draws them. */
75
93
  owned: number;
76
94
  /** The credits of the areas used, in each language. */
@@ -131,11 +149,27 @@ export declare function placeContext(areas: readonly ContextArea[], origin: LonL
131
149
  export declare const toneOf: (feet: number) => number;
132
150
  /** The streets flat on the floor, under the buildings: a path for each kind and width (a foot), widest and lowest kinds first, so that a sidewalk lies over the carriageway beside it and a viaduct's deck over the street under it. */
133
151
  export declare function streetsFloor(streets: readonly ContextStreet[]): string;
134
- /** The buildings flat on the floor of a district: one path for each tone, and one for the ghosts, drawn under the venues. Empty for a district with no context. */
152
+ /** Water, green and wood flat on the floor, the lowest of what a context draws: one path for each class, filled even-odd so that a hole is a hole. */
153
+ export declare function landFloor(land: readonly ContextShape[]): string;
154
+ /** Trees as dots: one path for each of three sizes (the crown's diameter on the floor, in feet), a dot being a round cap on a line of no length, so that a thousand trees are three elements. */
155
+ export declare function treesFloor(trees: readonly ContextTree[]): string;
156
+ /**
157
+ * What lies flat on the floor of a district under the venues, from the lowest up: water, green and wood, the streets, the trees as dots, and the buildings in four tones by height with the ghosts. All of it faint, so that a crowd
158
+ * reads on it. Empty for a district with no context.
159
+ */
135
160
  export declare function contextFloor(plan: ContextPlan): string;
136
161
  /** One building as the planes of a block: its walls up to the eaves, and over them a flat roof, or a hip roof where it has a pitched one. A ghost's planes are always mostly cut away, so that a venue inside it shows. */
137
162
  export declare function blockPlanes(b: ContextBlock, prefix: string): Plane[];
163
+ /**
164
+ * A floor of a building as a slab: a top face at `level` and sides down `thickness` below it, shaded by the way each side faces, so that it is a solid piece with an edge from every side and never a sheet one pixel thick.
165
+ * `look`: `chosen` is picked out (its top is the blue plate, drawn under whatever stands on it), `faint` is a floor of the building that is not the one chosen, `plain` has no top (a floor that is drawn by something else, a dojo's mats).
166
+ * `top` and `side` are the ids of the planes (a side's has its place in the ring after it).
167
+ */
168
+ export declare function slabPlanes(ring: readonly Pt[], level: number, thickness: number, ids: {
169
+ top: string;
170
+ side: string;
171
+ }, look: "chosen" | "faint" | "plain"): Plane[];
138
172
  /** The blocks of the 3D view: the `most` buildings that matter most (the tallest and widest, by height times the side of their ground), each a few planes, in the order the plan lists them. */
139
173
  export declare function contextUprights(plan: ContextPlan, most?: number): Upright[];
140
- /** The building a raised venue stands in as a block: its outline in feet, its height, `xray` the middle its walls give way from (or none for a solid one). */
174
+ /** The building a raised venue stands in as a block: its outline in feet, its height, `xray` the middle of what is seen through its walls (none for a solid one). */
141
175
  export declare function shellBlock(key: string, ring: readonly Pt[], height: number, xray: Pt | undefined): ContextBlock;
package/dist/context.js CHANGED
@@ -111,8 +111,43 @@ export function placeContext(areas, origin, turn, frame, venues) {
111
111
  streets.push({ kind: row[0], width: row[1] * area.grid * FEET_PER_METRE, points });
112
112
  }
113
113
  }
114
+ // The water, green and wood of the areas, and their trees.
115
+ const land = [];
116
+ const trees = [];
117
+ for (const area of areas) {
118
+ const [east, north] = metresFrom(origin, [area.origin])[0];
119
+ const g = area.grid;
120
+ const put = (x, y) => rotate([(x * g + east) * FEET_PER_METRE, -(y * g + north) * FEET_PER_METRE], turn);
121
+ for (const row of area.layers.landCover?.rows ?? []) {
122
+ const ring = [];
123
+ let [x, y] = [row[1], row[2]];
124
+ ring.push(put(x, y));
125
+ for (let i = 3; i + 1 < row.length; i += 2) {
126
+ x += row[i];
127
+ y += row[i + 1];
128
+ ring.push(put(x, y));
129
+ }
130
+ const box = boxOf(ring);
131
+ if (!(box.x1 < fx0 || box.x0 > fx1 || box.y1 < fy0 || box.y0 > fy1))
132
+ land.push({ cls: row[0], ring });
133
+ }
134
+ for (const row of area.layers.trees?.rows ?? []) {
135
+ let [x, y] = [row[0], row[1]];
136
+ for (let i = 0; i + 2 < row.length; i += 3) {
137
+ if (i > 0) {
138
+ x += row[i];
139
+ y += row[i + 1];
140
+ }
141
+ const at = put(x, y);
142
+ if (at[0] >= fx0 && at[0] <= fx1 && at[1] >= fy0 && at[1] <= fy1)
143
+ trees.push({ at, r: row[i + 2] * g * FEET_PER_METRE });
144
+ }
145
+ }
146
+ }
147
+ // An empty credit (a layer whose credit another layer of the area has said) adds nothing.
114
148
  const layers = areas.flatMap((a) => Object.values(a.layers));
115
- return { blocks, streets, owned, attribution: { en: [...new Set(layers.map((l) => l.attribution.en))], ja: [...new Set(layers.map((l) => l.attribution.ja))] } };
149
+ const credits = (language) => [...new Set(layers.map((l) => l.attribution[language]).filter((text) => text !== ""))];
150
+ return { blocks, streets, land, trees, owned, attribution: { en: credits("en"), ja: credits("ja") } };
116
151
  }
117
152
  /** The tone (0 low to 3 tall) of a building of `feet` height. */
118
153
  export const toneOf = (feet) => CONTEXT_TONES.filter((m) => feet / FEET_PER_METRE >= m).length;
@@ -133,16 +168,36 @@ export function streetsFloor(streets) {
133
168
  const keys = [...groups.keys()].sort((a, b) => order.indexOf(Number(a.split(" ")[0])) - order.indexOf(Number(b.split(" ")[0])) || Number(b.split(" ")[1]) - Number(a.split(" ")[1]));
134
169
  return `<g class="ky-dist-streets" aria-hidden="true" fill="none">${keys.map((key) => `<path class="ky-dist-street ky-dist-street-${key.split(" ")[0]}" stroke-width="${key.split(" ")[1]}" d="${groups.get(key).join("")}"/>`).join("")}</g>`;
135
170
  }
136
- /** The buildings flat on the floor of a district: one path for each tone, and one for the ghosts, drawn under the venues. Empty for a district with no context. */
171
+ const one = (value) => String(Math.round(value * 10) / 10);
172
+ /** Water, green and wood flat on the floor, the lowest of what a context draws: one path for each class, filled even-odd so that a hole is a hole. */
173
+ export function landFloor(land) {
174
+ const classes = ["ky-dist-water", "ky-dist-green", "ky-dist-wood"];
175
+ return classes.map((cls, i) => {
176
+ const rings = land.filter((s) => s.cls === i);
177
+ return rings.length === 0 ? "" : `<path class="ky-dist-land ${cls}" fill-rule="evenodd" d="${rings.map((s) => `M${s.ring.map(([x, y]) => `${one(x)} ${one(y)}`).join("L")}Z`).join("")}"/>`;
178
+ }).join("");
179
+ }
180
+ /** Trees as dots: one path for each of three sizes (the crown's diameter on the floor, in feet), a dot being a round cap on a line of no length, so that a thousand trees are three elements. */
181
+ export function treesFloor(trees) {
182
+ const sizes = [[0, 11, 9], [11, 17, 14], [17, Infinity, 20]];
183
+ return sizes.map(([from, to, width]) => {
184
+ const dots = trees.filter((t) => t.r * 2 >= from && t.r * 2 < to);
185
+ return dots.length === 0 ? "" : `<path class="ky-dist-tree" stroke-width="${width}" d="${dots.map((t) => `M${one(t.at[0])} ${one(t.at[1])}h0`).join("")}"/>`;
186
+ }).join("");
187
+ }
188
+ /**
189
+ * What lies flat on the floor of a district under the venues, from the lowest up: water, green and wood, the streets, the trees as dots, and the buildings in four tones by height with the ghosts. All of it faint, so that a crowd
190
+ * reads on it. Empty for a district with no context.
191
+ */
137
192
  export function contextFloor(plan) {
138
- const roads = streetsFloor(plan.streets ?? []);
193
+ const under = `${landFloor(plan.land ?? [])}${streetsFloor(plan.streets ?? [])}${treesFloor(plan.trees ?? [])}`;
139
194
  if (plan.blocks.length === 0)
140
- return roads;
195
+ return under;
141
196
  const paths = [[], [], [], [], []];
142
197
  for (const b of plan.blocks)
143
198
  paths[b.ghost ? 4 : toneOf(b.height)].push(`M${b.ring.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z`);
144
199
  const classes = ["ky-dist-b0", "ky-dist-b1", "ky-dist-b2", "ky-dist-b3", "ky-dist-ghost"];
145
- return `${roads}<g class="ky-dist-context" aria-hidden="true">${paths.map((d, i) => (d.length === 0 ? "" : `<path class="ky-dist-block ${classes[i]}" d="${d.join("")}"/>`)).join("")}</g>`;
200
+ return `${under}<g class="ky-dist-context" aria-hidden="true">${paths.map((d, i) => (d.length === 0 ? "" : `<path class="ky-dist-block ${classes[i]}" d="${d.join("")}"/>`)).join("")}</g>`;
146
201
  }
147
202
  // ---- The blocks of the 3D view. ----
148
203
  /** How much a building matters to the view: tall and wide first. */
@@ -269,6 +324,30 @@ export function blockPlanes(b, prefix) {
269
324
  });
270
325
  return planes;
271
326
  }
327
+ const SLAB_SIDES = ["u-slab-a", "u-slab-b", "u-slab-c"];
328
+ /**
329
+ * A floor of a building as a slab: a top face at `level` and sides down `thickness` below it, shaded by the way each side faces, so that it is a solid piece with an edge from every side and never a sheet one pixel thick.
330
+ * `look`: `chosen` is picked out (its top is the blue plate, drawn under whatever stands on it), `faint` is a floor of the building that is not the one chosen, `plain` has no top (a floor that is drawn by something else, a dojo's mats).
331
+ * `top` and `side` are the ids of the planes (a side's has its place in the ring after it).
332
+ */
333
+ export function slabPlanes(ring, level, thickness, ids, look) {
334
+ const wound = signedArea(ring);
335
+ const planes = [];
336
+ const faint = (p) => (look === "faint" ? { ...p, shy: [0, 0], fadeAt: -10, fadeTo: 0.18 } : p);
337
+ ring.forEach((p, i) => {
338
+ const q = ring[(i + 1) % ring.length];
339
+ const long = Math.hypot(q[0] - p[0], q[1] - p[1]);
340
+ if (long < 1)
341
+ return;
342
+ planes.push(faint(wall(`${ids.side}${i}`, "floor", p[0], p[1], q[0], q[1], level - thickness, level, `<rect class="u-slab ${SLAB_SIDES[sideOf(p, q, wound)]}" width="${n(long)}" height="${n(thickness)}"/>`)));
343
+ });
344
+ if (look !== "plain") {
345
+ const top = raked(ids.top, "floor", ring, { x0: ring[0][0], y0: ring[0][1], z0: level, gx: 0, gy: 0 }, [1, 0], (corners) => `<polygon class="${look === "chosen" ? "u-floorplate" : "u-slab-top"}" points="${pointList(corners)}"/>`);
346
+ if (top !== undefined)
347
+ planes.push(faint(look === "chosen" ? { ...top, under: true } : top));
348
+ }
349
+ return planes;
350
+ }
272
351
  /** The blocks of the 3D view: the `most` buildings that matter most (the tallest and widest, by height times the side of their ground), each a few planes, in the order the plan lists them. */
273
352
  export function contextUprights(plan, most = CONTEXT_BLOCKS) {
274
353
  const wanted = new Set(plan.blocks.filter((b) => b.whole).sort((a, b) => weight(b) - weight(a) || (a.id < b.id ? -1 : 1)).slice(0, Math.max(0, most)).map((b) => b.id));
@@ -284,7 +363,7 @@ export function contextUprights(plan, most = CONTEXT_BLOCKS) {
284
363
  }
285
364
  return items;
286
365
  }
287
- /** The building a raised venue stands in as a block: its outline in feet, its height, `xray` the middle its walls give way from (or none for a solid one). */
366
+ /** The building a raised venue stands in as a block: its outline in feet, its height, `xray` the middle of what is seen through its walls (none for a solid one). */
288
367
  export function shellBlock(key, ring, height, xray) {
289
368
  return { id: `shell-${key}`, ring: [...ring], height, rise: 0, area: Math.abs(signedArea(ring)), ghost: false, whole: true, ...(xray === undefined ? {} : { xray }) };
290
369
  }
@@ -1,9 +1,10 @@
1
1
  export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, describeDistrict, districtId, districtMember, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.ts";
2
- export type { DistrictMember, DistrictMode, DistrictOptions, DistrictPlaced, DistrictPlan, DistrictVenueInput } from "./district.ts";
2
+ export type { DistrictMember, DistrictMode, DistrictOptions, DistrictPart, DistrictPlaced, DistrictPlan, DistrictVenueInput, PlacedPart } from "./district.ts";
3
3
  export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.ts";
4
4
  export type { DistrictTurnSettings } from "./district.turn.ts";
5
5
  export type { ContextArea } from "./context.ts";
6
- export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_ROOM_HEIGHT, kodokanMember } from "./kodokan.ts";
6
+ export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_FLOOR_IDS, KODOKAN_FLOORS, KODOKAN_ROOM_HEIGHT, kodokanLevel, kodokanMember } from "./kodokan.ts";
7
+ export type { KodokanFloor, KodokanRoom } from "./kodokan.ts";
7
8
  export { VENUE_ATTRIBUTION } from "./venues.ts";
8
9
  export { mountDistrict } from "./district.dom.ts";
9
10
  export type { KyogijoDistrict } from "./district.dom.ts";
@@ -1,7 +1,7 @@
1
1
  // A district: several real venues of one city on one plan at their real relative positions and orientations (OpenStreetMap, ODbL 1.0), in 2D and in 3D, tight round neighbours or at the real distances of a city.
2
2
  export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, describeDistrict, districtId, districtMember, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.js";
3
3
  export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.js";
4
- export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_ROOM_HEIGHT, kodokanMember } from "./kodokan.js";
4
+ export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_FLOOR_IDS, KODOKAN_FLOORS, KODOKAN_ROOM_HEIGHT, kodokanLevel, kodokanMember } from "./kodokan.js";
5
5
  export { VENUE_ATTRIBUTION } from "./venues.js";
6
6
  export { mountDistrict } from "./district.dom.js";
7
7
  // The crowd outside the venues, on ground people may stand on (OpenStreetMap, ODbL 1.0), at 0, 25, 50, 75 or 100 percent of the seats.
@@ -46,6 +46,11 @@ export interface DistrictOptions {
46
46
  * alone. Only a venue that stands in a building (`DistrictMember.shell`) is changed.
47
47
  */
48
48
  building?: "xray" | "solid" | "none";
49
+ /**
50
+ * The floor of the Kodokan's building to show, 0 (the basement) to 8: the 7th is the great dojo, the 8th its gallery, the 5th and 6th their dojos as the Kodokan's floor guide counts them, the others a plain floor plate; the chosen floor's plate is picked out
51
+ * and what is drawn on the other floors is left out. Missing: the great dojo as it stands, on its floor, with nothing picked out. Only a venue that is a building of floors (`kodokan`) is changed.
52
+ */
53
+ floor?: number;
49
54
  }
50
55
  /** A venue of a district as the district needs it: what it is called, where it stands, how it lies, and how it is drawn. */
51
56
  export interface DistrictMember {
@@ -82,6 +87,35 @@ export interface DistrictMember {
82
87
  en: string;
83
88
  ja: string;
84
89
  };
90
+ /**
91
+ * What is drawn inside the venue, when it is not one surface: the rooms of a floor of a building (the Kodokan's 5th and 6th floors have two or three dojos), each its own surface with its own place. Missing: the venue's one surface, `options`
92
+ * at `centre` turned `heading`. An empty list draws no surface at all (a floor of plain rooms).
93
+ */
94
+ parts?: DistrictPart[];
95
+ /** The floor of the building the venue is drawn at, and the height of its level above the ground in feet: the building's plate at that level is drawn in 3D, picked out from the see-through walls. */
96
+ plate?: {
97
+ floor: number;
98
+ level: number;
99
+ };
100
+ /** Every floor of the building with the height of its level in feet, and how thick a floor is in metres: each is drawn as a slab with an edge, the chosen one (`plate`) picked out and the rest faint. */
101
+ slabs?: {
102
+ floor: number;
103
+ level: number;
104
+ }[];
105
+ slabThickness?: number;
106
+ }
107
+ /** One surface of a venue that has several (`DistrictMember.parts`): what it is drawn from, where it stands, how it lies and how high its floor is. */
108
+ export interface DistrictPart {
109
+ key: string;
110
+ options: SurfaceOptions;
111
+ /** The middle of the surface, \`[longitude, latitude]\`. */
112
+ centre: readonly [number, number];
113
+ /** The compass heading of the surface's x axis, 0 to under 180. */
114
+ heading: number;
115
+ /** How high its floor stands above the ground, in feet. */
116
+ elevation?: number;
117
+ /** Which part of the surface's room is drawn: `room` (the floor, the walls and what stands on it, not the gallery over it) or `gallery` (what stands above the room's walls: the seats, the rail, the roof). Everything when missing. */
118
+ show?: "room" | "gallery";
85
119
  }
86
120
  /** Every key a district can be asked for: the arenas of `VENUES` and the stadiums of `STADIUMS`. */
87
121
  export declare const DISTRICT_IDS: readonly string[];
@@ -97,16 +131,24 @@ export declare const isDistrictId: (id: string) => boolean;
97
131
  * // bc-place:soccer stadium 52497
98
132
  * ```
99
133
  */
100
- export declare function districtMember(input: DistrictVenueInput, language?: Language): DistrictMember;
134
+ export declare function districtMember(input: DistrictVenueInput, language?: Language, floor?: number): DistrictMember;
101
135
  /** Turn a point of the map (east, down the page) by `degrees` clockwise on the page. */
102
136
  export declare const turnBy: ([x, y]: Pt, degrees: number) => Pt;
103
137
  /** A venue placed in the district: where its surface's middle is, how far its drawing is turned, and where its building stands, all in feet in the district's frame (x to the right, y down the page). */
138
+ export interface PlacedPart {
139
+ part: DistrictPart;
140
+ /** The middle of the surface, and the degrees its drawing is turned clockwise, as for the venue's own. */
141
+ at: Pt;
142
+ angle: number;
143
+ }
104
144
  export interface DistrictPlaced {
105
145
  member: DistrictMember;
106
146
  /** The middle of the surface. */
107
147
  at: Pt;
108
148
  /** The degrees the venue's own drawing (its field along x) is turned clockwise to lie as it does in the world. */
109
149
  angle: number;
150
+ /** The surfaces of the venue as the plan places them (one, unless the venue has `parts`). */
151
+ parts: PlacedPart[];
110
152
  /** The building's outline. */
111
153
  footprint: Pt[];
112
154
  /** Its convex hull, which the depth order of the 3D view separates buildings by. */
@@ -170,7 +212,7 @@ export declare const OUTSIDE_ROOM = 160;
170
212
  */
171
213
  export declare function planDistrict(options: DistrictOptions): DistrictPlan;
172
214
  /** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
173
- export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}\n.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}\n.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}\n.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}\n.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}\n.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}\n.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}\n.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}\n.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}\n.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}\n.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}\n.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}\n.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}\n.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
215
+ export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8;--ky-dist-water:#c7d5dc;--ky-dist-green:#d2dcc4;--ky-dist-wood:#c3d0b6;--ky-dist-tree:#b3c2a3;--ky-dist-floor:#2f6db0}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}\n.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}\n.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}\n.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}\n.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}\n.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}\n.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}\n.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}\n.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}\n.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}\n.ky-district .ky-dist-land{stroke:none}\n.ky-district .ky-dist-water{fill:var(--ky-dist-water)}\n.ky-district .ky-dist-green{fill:var(--ky-dist-green)}\n.ky-district .ky-dist-wood{fill:var(--ky-dist-wood)}\n.ky-district .ky-dist-tree{fill:none;stroke:var(--ky-dist-tree);stroke-linecap:round}\n.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}\n.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}\n.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}\n.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}\n.ky-district .ky-dist-floor{stroke:var(--ky-dist-floor);stroke-width:2}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
174
216
  /**
175
217
  * A venue's drawing as a part of the district's: its root made a nested `svg` that keeps the class, the sport and the colours the drawing's stylesheet reads, its box laid in its own frame (so that the group's turn
176
218
  * and shift put it in place), and its title, description, apron, arrow and stylesheet taken out (the stylesheet is given back in `styles`).
@@ -203,7 +245,7 @@ export declare function describeDistrict(options: DistrictOptions, plan?: Distri
203
245
  /** How a venue is drawn for a district: its own options with the district's language, theme and ids, in 2D with its stands or as the floor of the 3D view (its building only). */
204
246
  export declare function drawMember(member: DistrictMember, options: DistrictOptions, index: number, id: string, how?: {
205
247
  floor?: boolean;
206
- }): string;
248
+ }, partIndex?: number): string;
207
249
  /**
208
250
  * The plan of a district as one SVG: the ground, a grid and a scale, a north arrow, and each venue's own drawing (its stands, or its building only for `stands: false`) put where it stands and turned as it lies, with a name beside each
209
251
  * where `labels` says. `drawing` is how each venue is drawn; the 3D view gives it the building-only floor.
@@ -215,6 +257,6 @@ export declare function drawMember(member: DistrictMember, options: DistrictOpti
215
257
  * // true true
216
258
  * ```
217
259
  */
218
- export declare function drawDistrict(options: DistrictOptions, plan?: DistrictPlan, drawing?: (member: DistrictMember, index: number) => string): string;
260
+ export declare function drawDistrict(options: DistrictOptions, plan?: DistrictPlan, drawing?: (member: DistrictMember, index: number, part: number) => string): string;
219
261
  /** The venues a district was asked for that the package has no key for: none when it can be drawn. */
220
262
  export declare const unknownVenues: (venues: readonly DistrictVenueInput[]) => string[];
package/dist/district.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // apart, with a grid, a scale and a name beside each. Nothing is drawn but what the venues are drawn from: no street, no park and no mark of a team, a league or a tournament; the credit
5
5
  // `VENUE_ATTRIBUTION` goes with it. Pure: options in, a plan, and markup, out; `district.turn.ts` raises it in 3D.
6
6
  import { arenaById, arenaNameIn } from "./arenas.js";
7
+ import { dojo } from "./budo/dojo.api.js";
7
8
  import { kodokanMember } from "./kodokan.js";
8
9
  import { contextFloor, contextFrame, placeContext, toneOf } from "./context.js";
9
10
  import { crowdStands, drawKyogijo } from "./draw.js";
@@ -63,7 +64,7 @@ export const isDistrictId = (id) => DISTRICT_IDS.includes(id);
63
64
  * // bc-place:soccer stadium 52497
64
65
  * ```
65
66
  */
66
- export function districtMember(input, language = "en") {
67
+ export function districtMember(input, language = "en", floor) {
67
68
  const [id, named] = typeof input === "string" ? input.split(":") : [input.id, input.config];
68
69
  const venue = venueById(id);
69
70
  if (venue !== undefined) {
@@ -82,7 +83,7 @@ export function districtMember(input, language = "en") {
82
83
  if (id === "kodokan") {
83
84
  if (named !== undefined)
84
85
  throw new Error(`kodokan has one configuration, not ${named}`);
85
- return kodokanMember(language);
86
+ return kodokanMember(language, floor);
86
87
  }
87
88
  throw new Error(`no venue ${String(id)}: the venues are ${DISTRICT_IDS.join(", ")}`);
88
89
  }
@@ -116,7 +117,7 @@ export function planDistrict(options) {
116
117
  throw new Error("a district needs a venue");
117
118
  const language = options.language === "ja" ? "ja" : "en";
118
119
  const mode = options.mode === "city" ? "city" : "neighbours";
119
- const members = options.venues.map((v) => districtMember(v, language));
120
+ const members = options.venues.map((v) => districtMember(v, language, options.floor));
120
121
  const keys = new Set();
121
122
  for (const m of members) {
122
123
  if (keys.has(m.key))
@@ -138,7 +139,9 @@ export function planDistrict(options) {
138
139
  }
139
140
  const placed = members.map((member, i) => {
140
141
  const footprint = raw[i].footprint.map((p) => turnBy(p, turn));
141
- return { member, at: turnBy(raw[i].at, turn), angle: member.heading - 90 + turn, footprint, hull: hullOf(footprint) };
142
+ // The venue's surfaces: its own, or the parts it is drawn from, each placed by its own middle and heading.
143
+ const parts = (member.parts ?? [{ key: member.key, options: member.options, centre: member.centre, heading: member.heading, ...(member.elevation === undefined ? {} : { elevation: member.elevation }) }]).map((part) => ({ part, at: turnBy(ground([part.centre])[0], turn), angle: part.heading - 90 + turn }));
144
+ return { member, parts, at: turnBy(raw[i].at, turn), angle: member.heading - 90 + turn, footprint, hull: hullOf(footprint) };
142
145
  });
143
146
  const xs = placed.flatMap((p) => p.footprint.map((q) => q[0]));
144
147
  const ys = placed.flatMap((p) => p.footprint.map((q) => q[1]));
@@ -161,9 +164,9 @@ export function planDistrict(options) {
161
164
  // ---- The drawing. ----
162
165
  /** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
163
166
  export const KYOGIJO_DISTRICT_STYLE = `
164
- :where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8}
165
- @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}}
166
- :where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}
167
+ :where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8;--ky-dist-water:#c7d5dc;--ky-dist-green:#d2dcc4;--ky-dist-wood:#c3d0b6;--ky-dist-tree:#b3c2a3;--ky-dist-floor:#2f6db0}
168
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}}
169
+ :where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee;--ky-dist-water:#1c2d3b;--ky-dist-green:#202d26;--ky-dist-wood:#1c2b23;--ky-dist-tree:#2e4337;--ky-dist-floor:#7fb0e8}
167
170
  .ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}
168
171
  .ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}
169
172
  .ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}
@@ -176,10 +179,16 @@ export const KYOGIJO_DISTRICT_STYLE = `
176
179
  .ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}
177
180
  .ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}
178
181
  .ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}
182
+ .ky-district .ky-dist-land{stroke:none}
183
+ .ky-district .ky-dist-water{fill:var(--ky-dist-water)}
184
+ .ky-district .ky-dist-green{fill:var(--ky-dist-green)}
185
+ .ky-district .ky-dist-wood{fill:var(--ky-dist-wood)}
186
+ .ky-district .ky-dist-tree{fill:none;stroke:var(--ky-dist-tree);stroke-linecap:round}
179
187
  .ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}
180
188
  .ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}
181
189
  .ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}
182
190
  .ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}
191
+ .ky-district .ky-dist-floor{stroke:var(--ky-dist-floor);stroke-width:2}
183
192
  .ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}
184
193
  .ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}`;
185
194
  const ROOT = /^<svg([^>]*)>/;
@@ -214,6 +223,11 @@ export function surfaceCentre(options) {
214
223
  if (options.sport === "mlb")
215
224
  return [0, 0];
216
225
  const stands = crowdStands(options);
226
+ // A room with no crowd (a dojo of a floor of rooms) has its mats from the corner of its drawing.
227
+ if (stands === undefined && options.sport === "dojo" && options.dojo !== undefined) {
228
+ const floor = dojo(options.dojo);
229
+ return [(floor.width * FEET_PER_METRE) / 2, (floor.length * FEET_PER_METRE) / 2];
230
+ }
217
231
  if (stands === undefined)
218
232
  throw new Error("a district's venue has a crowd");
219
233
  return [stands.box.x + stands.box.w / 2, stands.box.y + stands.box.h / 2];
@@ -226,7 +240,7 @@ export function scaleOf(view) {
226
240
  /** The distance a scale says, as it is read: metres under a kilometre, else kilometres. */
227
241
  export const metresText = (metres) => (metres >= 1000 ? `${metres / 1000} km` : `${metres} m`);
228
242
  /** The id of a district's drawing: the option's, or a hash of the options. */
229
- export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands, ...(options.context === undefined || options.context.length === 0 ? [] : [options.context.map((a) => a.key)]), ...(options.building === undefined ? [] : [options.building])]))}`);
243
+ export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands, ...(options.context === undefined || options.context.length === 0 ? [] : [options.context.map((a) => a.key)]), ...(options.building === undefined ? [] : [options.building]), ...(options.floor === undefined ? [] : [`floor${options.floor}`])]))}`);
230
244
  /** The title and the description of a district, in its language: what is drawn, where each venue stands from the first, how far apart they are, and the credit. */
231
245
  export function describeDistrict(options, plan = planDistrict(options)) {
232
246
  const language = options.language === "ja" ? "ja" : "en";
@@ -248,14 +262,16 @@ export function describeDistrict(options, plan = planDistrict(options)) {
248
262
  }
249
263
  const fillWords = (text, values) => text.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? "");
250
264
  /** How a venue is drawn for a district: its own options with the district's language, theme and ids, in 2D with its stands or as the floor of the 3D view (its building only). */
251
- export function drawMember(member, options, index, id, how = {}) {
265
+ export function drawMember(member, options, index, id, how = {}, partIndex = 0) {
252
266
  const stands = options.stands !== false && how.floor !== true;
267
+ const own = member.parts?.[partIndex]?.options ?? member.options;
253
268
  return drawKyogijo({
254
- ...member.options,
255
- crowd: { real: true, ...(stands ? {} : { stands: false }) },
269
+ ...own,
270
+ // A room that is drawn without a crowd stays so.
271
+ crowd: own.crowd === false ? false : { real: true, ...(stands ? {} : { stands: false }) },
256
272
  language: options.language === "ja" ? "ja" : "en",
257
273
  ...(options.theme === undefined ? {} : { theme: options.theme }),
258
- id: `${id}-m${index}`,
274
+ id: `${id}-m${index}${partIndex === 0 ? "" : `-p${partIndex}`}`,
259
275
  });
260
276
  }
261
277
  /**
@@ -302,21 +318,23 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
302
318
  if (shell === undefined)
303
319
  continue;
304
320
  const tone = options.building === "solid" ? `ky-dist-b${toneOf(shell.height)}` : "ky-dist-ghost";
305
- parts.push(`<path class="ky-dist-block ${tone}" data-shell="${esc(placed.member.key)}" clip-path="url(#${id}-clip)" d="M${placed.footprint.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z"/>`);
321
+ parts.push(`<path class="ky-dist-block ${tone}${placed.member.plate === undefined ? "" : " ky-dist-floor"}" data-shell="${esc(placed.member.key)}"${placed.member.plate === undefined ? "" : ` data-floor="${placed.member.plate.floor}"`} clip-path="url(#${id}-clip)" d="M${placed.footprint.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z"/>`);
306
322
  }
307
323
  }
308
324
  const styles = [];
309
325
  plan.members.forEach((placed, i) => {
310
- const svg = drawing === undefined ? drawMember(placed.member, options, i, id) : drawing(placed.member, i);
311
- const { markup, styles: own } = nestedDrawing(svg);
312
- for (const block of own)
313
- if (!styles.includes(block))
314
- styles.push(block);
315
- // The 3D view draws a raised floor in the air itself: the ground under it is left bare.
316
- if (drawing !== undefined && (placed.member.elevation ?? 0) > 0)
317
- return;
318
- const centre = surfaceCentre(placed.member.options);
319
- parts.push(`<g class="ky-dist-venue" data-venue="${esc(placed.member.key)}" transform="translate(${num(placed.at[0])} ${num(placed.at[1])}) rotate(${num(placed.angle)}) translate(${num(-centre[0])} ${num(-centre[1])})">${markup}</g>`);
326
+ placed.parts.forEach((pp, j) => {
327
+ const svg = drawing === undefined ? drawMember(placed.member, options, i, id, {}, j) : drawing(placed.member, i, j);
328
+ const { markup, styles: own } = nestedDrawing(svg);
329
+ for (const block of own)
330
+ if (!styles.includes(block))
331
+ styles.push(block);
332
+ // The 3D view draws a raised floor in the air itself: the ground under it is left bare.
333
+ if (drawing !== undefined && (pp.part.elevation ?? 0) > 0)
334
+ return;
335
+ const centre = surfaceCentre(pp.part.options);
336
+ parts.push(`<g class="ky-dist-venue" data-venue="${esc(placed.member.key)}"${j === 0 ? "" : ` data-part="${esc(pp.part.key)}"`} transform="translate(${num(pp.at[0])} ${num(pp.at[1])}) rotate(${num(pp.angle)}) translate(${num(-centre[0])} ${num(-centre[1])})">${markup}</g>`);
337
+ });
320
338
  });
321
339
  if (plan.gates.length > 0) {
322
340
  // A door's bar on the outline, and a dashed stub out from it the way a queue would run; a gate a source names is filled, one that is ours is drawn hollow.