@johnmorrisdotca/kyogijo-stadiums 0.12.2 → 0.14.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.
@@ -14,6 +14,7 @@ import { MULTI_WORDS } from "./multi.words.js";
14
14
  import { outlineOf } from "./outline.js";
15
15
  import { fill } from "./strings.js";
16
16
  import { textWidth } from "./text.js";
17
+ import { uprightsFor } from "./uprights.js";
17
18
  import { VERSION } from "./version.js";
18
19
  const metres = (ft) => ft / FEET_PER_METRE;
19
20
  const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
@@ -46,6 +47,14 @@ const floorClass = (layout) => (layout.spec.kind === "budo" ? "ky-bud-t2" : "ky-
46
47
  * ```
47
48
  */
48
49
  export function drawMulti(options = {}) {
50
+ return multiDrawing(options, false).svg;
51
+ }
52
+ /**
53
+ * The drawing of a layout, with what it was worked out from. `raise` makes the floor of a 3D view instead of the flat picture: the barriers, the officials' chairs, the scoreboard and the stands are left out (the 3D
54
+ * view stands them up), and a surface that stands over the floor (a table tennis table) is left off it too, with a soft shadow in its place. Everything else is the same string. With `uprights` false the 3D view stands
55
+ * nothing but the stands up, so the barriers and the rest stay on the floor.
56
+ */
57
+ export function multiDrawing(options, raise, uprights = true) {
49
58
  const o = options;
50
59
  const language = o.language === "ja" ? "ja" : "en";
51
60
  const words = MULTI_WORDS[language];
@@ -80,8 +89,13 @@ export function drawMulti(options = {}) {
80
89
  let style = "";
81
90
  let look;
82
91
  let cellsMarkup = "";
92
+ const lifted = [];
83
93
  const surfaceBox = outlineOf(spec.surface).box;
84
94
  const common = { ...spec.surface, margin: 0, language };
95
+ // A surface that stands over the floor (a table tennis table's top, 76 cm up) is raised by the 3D view as a plane of its own.
96
+ const up = raise && uprights;
97
+ const lift = up && spec.kind === "cells" ? uprightsFor(common).lift : 0;
98
+ const raised = lift > 0;
85
99
  if (spec.kind === "budo") {
86
100
  const drawn = inner(drawKyogijo({ ...common, id: `${gid}-s`, ...(o.style === "none" ? { style: "none" } : {}) }));
87
101
  style = drawn.style;
@@ -97,6 +111,13 @@ export function drawMulti(options = {}) {
97
111
  style = drawn.style;
98
112
  look = drawn.look;
99
113
  }
114
+ if (raised) {
115
+ // The floor has its shadow, soft and a little larger; the top is a plane of the 3D view.
116
+ const grow = 0.8;
117
+ cellsMarkup += el("rect", { class: "ky-mu-shadow", x: num(cell.x - grow / 2), y: num(cell.y - grow / 2), width: num(cell.w + grow), height: num(cell.h + grow), rx: num(grow) });
118
+ lifted.push({ cell, body: drawn.body, box: { x: surfaceBox.x, y: surfaceBox.y, w: surfaceBox.w, h: surfaceBox.h }, lift });
119
+ return;
120
+ }
100
121
  // A surface turned a quarter stands across the layout: the drawing's own top left goes to the cell's top right, and the drawing is turned.
101
122
  const place = cell.turn ? `translate(${num(cell.x + cell.w)} ${num(cell.y)}) rotate(90) translate(${num(-surfaceBox.x)} ${num(-surfaceBox.y)})` : `translate(${num(cell.x - surfaceBox.x)} ${num(cell.y - surfaceBox.y)})`;
102
123
  cellsMarkup += el("g", { class: "ky-mu-cell", "data-cell": cell.n, transform: place }, drawn.body);
@@ -106,10 +127,10 @@ export function drawMulti(options = {}) {
106
127
  let floorMarkup = "";
107
128
  floorMarkup += el("rect", { class: floorClass(layout), x: 0, y: 0, width: num(layout.floor.w), height: num(layout.floor.h) });
108
129
  let over = "";
109
- if (spec.parts.barriers === true)
130
+ if (!up && spec.parts.barriers === true)
110
131
  for (const c of layout.cells)
111
132
  over += el("rect", { class: "ky-mu-barrier", x: num(c.bay.x + 0.5), y: num(c.bay.y + 0.5), width: num(c.bay.w - 1), height: num(c.bay.h - 1), rx: 0.4 });
112
- if (spec.parts.officials === true) {
133
+ if (!up && spec.parts.officials === true) {
113
134
  for (const c of layout.cells) {
114
135
  if (c.turn) {
115
136
  // Beside the net, on the right of a surface that stands across the layout.
@@ -123,10 +144,10 @@ export function drawMulti(options = {}) {
123
144
  over += el("rect", { class: "ky-mu-desk", x: num(cx - 5), y: num(c.y - 3.1), width: 3.6, height: 1.1, rx: 0.2 });
124
145
  }
125
146
  }
126
- if (spec.parts.scoreboard === "each")
147
+ if (!up && spec.parts.scoreboard === "each")
127
148
  for (const c of layout.cells)
128
149
  over += el("rect", { class: "ky-mu-sb", x: num(c.x + c.w / 2 - 2), y: num(c.y + c.h + 1.2), width: 4, height: 1.4, rx: 0.2 });
129
- const sb = layout.shared;
150
+ const sb = up ? undefined : layout.shared;
130
151
  if (sb !== undefined) {
131
152
  over += el("rect", { class: "ky-mu-sb", x: num(sb.x), y: num(sb.y), width: num(sb.w), height: num(sb.h), rx: 0.5 });
132
153
  // One scoreboard for all: a quarter of it for each of four areas, or one panel for each when they are fewer or more.
@@ -186,6 +207,7 @@ export function drawMulti(options = {}) {
186
207
  let reach = { left: 0, right: 0, top: 0, bottom: 0 };
187
208
  let defs = "";
188
209
  let crowdStyle = "";
210
+ let standing;
189
211
  const wide = { x: 0, y: 0, w: hall.w, h: hall.h };
190
212
  const boards = layout.slots.filter((s) => s.kind === "board");
191
213
  const boardsMargin = withSlots && boards.length > 0 ? 5 : 0;
@@ -202,13 +224,18 @@ export function drawMulti(options = {}) {
202
224
  const standsLike = venue?.standsLike ?? spec.sport;
203
225
  const crowd = resolveCrowd({ gaps: false, awayEnd: "none", roof: stadium.roof, shape: stadium.shape, density: stadium.density, detail: stadium.detail === "plain" ? "plain" : "fine" });
204
226
  const stands = layoutOf({ sport: standsLike, box: wide, crowd, awaySide: "right", arena: venue?.shape === "octagon" });
205
- const density = stadium.crowd === false ? 0 : densityOf(crowd, stands.seats, undefined);
206
- const drawn = drawCrowd(stands, { id: `${gid}-cr`, crowd, density, fine: crowd.detail !== "plain", ground: "ky-mu-apron" });
207
- defs += drawn.defs;
208
227
  const wall = venue?.shape === "octagon" ? el("path", { class: "ky-mu-wall", d: octagonD(hall.w / 2, hall.w / 2, hall.w / 2) }) : el("rect", { class: "ky-mu-wall", x: 0, y: 0, width: num(hall.w), height: num(hall.h), rx: 0.5 });
209
- hallMarkup = drawn.markup + hallMarkup + wall;
228
+ if (raise)
229
+ hallMarkup += wall;
230
+ else {
231
+ const density = stadium.crowd === false ? 0 : densityOf(crowd, stands.seats, undefined);
232
+ const drawn = drawCrowd(stands, { id: `${gid}-cr`, crowd, density, fine: crowd.detail !== "plain", ground: "ky-mu-apron" });
233
+ defs += drawn.defs;
234
+ hallMarkup = drawn.markup + hallMarkup + wall;
235
+ crowdStyle = KYOGIJO_CROWD_STYLE;
236
+ }
210
237
  reach = stands.reach;
211
- crowdStyle = KYOGIJO_CROWD_STYLE;
238
+ standing = { stands, crowd, standsLike };
212
239
  }
213
240
  else if (venue !== undefined) {
214
241
  hallMarkup += el("rect", { class: "ky-mu-hall", x: 0, y: 0, width: num(hall.w), height: num(hall.h) });
@@ -267,7 +294,8 @@ export function drawMulti(options = {}) {
267
294
  "data-theme": o.theme === "light" || o.theme === "dark" ? o.theme : undefined,
268
295
  "data-version": VERSION,
269
296
  }, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` + css + (defs === "" ? "" : `<defs>${defs}</defs>`) + el("rect", { class: "ky-mu-apron", x: num(view.x), y: num(view.y), width: num(vw), height: num(vh) }) + hallLayer + group + overhang + slotTexts + labelsMarkup);
270
- return root;
297
+ const frame = { layout, hall, lw, lh, ox, oy, turned, to };
298
+ return { svg: root, frame, fit, ...(standing === undefined ? {} : { stands: standing }), lifted, view: { x: view.x, y: view.y, w: vw, h: vh }, title, description };
271
299
  }
272
300
  /** Where an empty place is, in words: the surface it belongs to, the side or end it is at, and whether it is on the mats. */
273
301
  function slotWhere(slot, layout, words) {
@@ -6,8 +6,8 @@ const FLOORS = {
6
6
  red: { light: ["#b3362b", "#fdf3f1"], dark: ["#7a241d", "#f3d8d4"] },
7
7
  ice: { light: ["#e4eef5", "#1d2b36"], dark: ["#a9bccb", "#10181f"] },
8
8
  };
9
- const COMMON_LIGHT = "--ky-mu-neutral:#bdb9ad;--ky-mu-barrier:#161a1f;--ky-mu-board:#1d2430;--ky-mu-board-ink:#f4f1ea;--ky-mu-chair:#ece7da;--ky-mu-chair-edge:#4a4f56;--ky-mu-sb:#10151b;--ky-mu-sb-ink:#f6f6f6;--ky-mu-wall:#3a4048;--ky-mu-hall:#d9d5c8;--ky-mu-over:#c0392b";
10
- const COMMON_DARK = "--ky-mu-neutral:#2b2f2c;--ky-mu-barrier:#05070a;--ky-mu-board:#0d1218;--ky-mu-board-ink:#e8e4da;--ky-mu-chair:#b9b4a6;--ky-mu-chair-edge:#050607;--ky-mu-sb:#05080b;--ky-mu-sb-ink:#e9e9e9;--ky-mu-wall:#0a0c0f;--ky-mu-hall:#1b1f1c;--ky-mu-over:#e0614f";
9
+ const COMMON_LIGHT = "--ky-mu-neutral:#bdb9ad;--ky-mu-barrier:#161a1f;--ky-mu-board:#1d2430;--ky-mu-board-ink:#f4f1ea;--ky-mu-chair:#ece7da;--ky-mu-chair-edge:#4a4f56;--ky-mu-sb:#10151b;--ky-mu-sb-ink:#f6f6f6;--ky-mu-wall:#3a4048;--ky-mu-hall:#d9d5c8;--ky-mu-over:#c0392b;--ky-mu-shadow:rgba(0,0,0,.2)";
10
+ const COMMON_DARK = "--ky-mu-neutral:#2b2f2c;--ky-mu-barrier:#05070a;--ky-mu-board:#0d1218;--ky-mu-board-ink:#e8e4da;--ky-mu-chair:#b9b4a6;--ky-mu-chair-edge:#050607;--ky-mu-sb:#05080b;--ky-mu-sb-ink:#e9e9e9;--ky-mu-wall:#0a0c0f;--ky-mu-hall:#1b1f1c;--ky-mu-over:#e0614f;--ky-mu-shadow:rgba(0,0,0,.45)";
11
11
  const hallVars = (which) => Object.entries(FLOORS).map(([name, f]) => `--ky-mu-f-${name}:${f[which][0]}`).join(";");
12
12
  const looks = (which) => Object.entries(FLOORS).map(([name, f]) => `.kyogijo.ky-multi[data-floor=${name}]{--ky-mu-floor:${f[which][0]};--ky-mu-slot:${f[which][1]}}`).join("");
13
13
  /**
@@ -27,6 +27,7 @@ ${Object.entries(FLOORS).map(([name, f]) => `html[data-theme=dark] .kyogijo.ky-m
27
27
  .kyogijo .ky-mu-hallfloor{fill:var(--ky-mu-neutral);stroke:var(--ky-mu-wall);stroke-width:.8}
28
28
  ${Object.keys(FLOORS).map((name) => `.kyogijo .ky-mu-hf-${name}{fill:var(--ky-mu-f-${name})}`).join("\n")}
29
29
  .kyogijo .ky-mu-wall{fill:none;stroke:var(--ky-mu-wall);stroke-width:2.2;stroke-linejoin:round}
30
+ .kyogijo .ky-mu-shadow{fill:var(--ky-mu-shadow)}
30
31
  .kyogijo .ky-mu-over{fill:none;stroke:var(--ky-mu-over);stroke-width:1.4;stroke-dasharray:5 3}
31
32
  .kyogijo .ky-mu-barrier{fill:none;stroke:var(--ky-mu-barrier);stroke-width:1;stroke-opacity:.9}
32
33
  .kyogijo .ky-mu-board{fill:var(--ky-mu-board)}
@@ -0,0 +1,32 @@
1
+ import type { MultiOptions } from "./multi.types.ts";
2
+ import { type PresetTable } from "./turn.camera.ts";
3
+ import { type KyogijoTurn } from "./turn.dom.ts";
4
+ import { type TurnModel, type TurnSettings } from "./turn.model.ts";
5
+ import type { Sport } from "./types.ts";
6
+ /** How high a surround stands: the ITTF's "about 75 cm" (Handbook 3.2.3.3). A layout of another sport with surrounds has the same (ours: no other rulebook is read for it). */
7
+ export declare const SURROUND_HEIGHT: number;
8
+ /**
9
+ * The viewpoints of a layout. Overhead, broadcast and the low corner are a single surface's (`TURN_PRESETS`); the end-on view of a net sport looks along its surfaces' length, where the nets are seen whole, as `NET_PRESETS` does. All are turned a quarter where
10
+ * the floor or the surfaces stand across the picture, so that the long way of the floor runs across the screen. `floor` is the picture's frame (the hall's, turned if the hall asks) and `lengthAlongX` says whether the surfaces' length runs along its x.
11
+ */
12
+ export declare function multiPresets(sport: Sport, floor: {
13
+ w: number;
14
+ h: number;
15
+ }, lengthAlongX: boolean): PresetTable;
16
+ /**
17
+ * A tournament layout as a 3D model: the floor is the layout's flat drawing, and on it stand each surface as it stands on its own page, the surrounds, the umpires' chairs and score tables, the scoreboards and, with `stadium`, the stands
18
+ * of the hall. A `TurnModel` like any other: `turnMarkupOf` and `mountMultiTurn` make a view of it.
19
+ */
20
+ export declare function buildMultiTurn(options: MultiOptions, settings?: TurnSettings): TurnModel;
21
+ /**
22
+ * Mount a tournament layout as a 3D view in a container: the same view as `mountKyogijoTurn`'s, with the controls, the pointer, the keys and the auto-turn of every 3D view, over the layout's floor with each surface standing on it.
23
+ * @example
24
+ * ```ts no-run
25
+ * import { mountMultiTurn } from "@johnmorrisdotca/kyogijo-stadiums/multi";
26
+ * const view = mountMultiTurn(document.querySelector("#hall")!, { preset: "tabletennis-4", stadium: true }, { preset: "broadcast" });
27
+ * view.set({ yaw: 40 });
28
+ * ```
29
+ */
30
+ export declare function mountMultiTurn(host: HTMLElement, options: MultiOptions, settings?: TurnSettings): KyogijoTurn<MultiOptions>;
31
+ /** A tournament layout as the markup of a 3D view, still until a page's script makes it turn: pure, so a server can make it. */
32
+ export declare function multiTurnMarkup(options: MultiOptions, settings?: TurnSettings): string;
@@ -0,0 +1,253 @@
1
+ // A tournament layout in 3D: the 3D view of `turn.ts`, with a layout's several surfaces on its floor. Nothing here is drawn a second way. Each surface stands as it does on its own page (`uprightsFor`: the nets and posts, a table
2
+ // tennis table's legs and skirt), moved to where the layout puts it, and the stands of the stadium round the hall are the 3D view's own (`standsVenueOf`); what only a layout has (the surrounds between the areas, the umpires'
3
+ // chairs and score tables, the scoreboards) is stood up from the same figures the flat drawing draws, and the floor is the flat drawing itself, `multiDrawing` made for raising. The model it makes is a `TurnModel`, so the
4
+ // camera, the controls, the pointer, the auto-turn and the tests of every other 3D view are this one's.
5
+ import { FEET_PER_METRE } from "./geometry.js";
6
+ import { multiDrawing } from "./multi.draw.js";
7
+ import { outlineOf } from "./outline.js";
8
+ import { hash } from "./format.js";
9
+ import { TURN_PRESETS, NET_PRESETS, wrap } from "./turn.camera.js";
10
+ import { flat, n, plane, shy, thinnest, wall } from "./turn.kit.js";
11
+ import { mountTurn } from "./turn.dom.js";
12
+ import { turnMarkupOf } from "./turn.markup.js";
13
+ import { openingView, sceneBox } from "./turn.model.js";
14
+ import { describeUprights, planesOf, uprightsFor } from "./uprights.js";
15
+ import { standsVenueOf } from "./venue.js";
16
+ import { isNetSport } from "./court.js";
17
+ /** How high a surround stands: the ITTF's "about 75 cm" (Handbook 3.2.3.3). A layout of another sport with surrounds has the same (ours: no other rulebook is read for it). */
18
+ export const SURROUND_HEIGHT = 0.75 * FEET_PER_METRE;
19
+ /** The longest piece a surround is cut into, in feet: a long wall is several planes, so that each is drawn in its place among the surfaces and the chairs and not as one slab sorted by its middle. */
20
+ const SURROUND_PIECE = 10;
21
+ /** An umpire's chair's seat and a score table's top, in feet over the floor (ours: the rule gives no height for either). */
22
+ const CHAIR = 1.5;
23
+ const DESK = 2.5;
24
+ /** Where the scoreboards hang (ours): the one over the middle of the floor has its lower edge 13 feet up and its cables run 8 feet above its top; the one at each surface stands 2.6 feet off the floor. */
25
+ const HUNG = { bottom: 13, cables: 8 };
26
+ const STANDING = { legs: 2.6, tall: 1.4 };
27
+ const mapOf = (to) => {
28
+ const [e, f] = to(0, 0);
29
+ const [px, py] = to(1, 0);
30
+ const [qx, qy] = to(0, 1);
31
+ return { a: px - e, b: qx - e, c: py - f, d: qy - f, e, f };
32
+ };
33
+ const at = (m, x, y) => [m.a * x + m.b * y + m.e, m.c * x + m.d * y + m.f];
34
+ const dir = (m, x, y) => [m.a * x + m.b * y, m.c * x + m.d * y];
35
+ /** A plane moved by a map, under a new id. A plane that stands in a frame turned a quarter turns with it: its corner, its two sides and the way it faces out. */
36
+ function place(p, m, id) {
37
+ const [ox, oy] = at(m, p.o[0], p.o[1]);
38
+ const [ux, uy] = dir(m, p.u[0], p.u[1]);
39
+ const [vx, vy] = dir(m, p.v[0], p.v[1]);
40
+ const moved = { ...p, id, o: [ox, oy, p.o[2]], u: [ux, uy, p.u[2]], v: [vx, vy, p.v[2]] };
41
+ if (p.shy !== undefined)
42
+ moved.shy = dir(m, p.shy[0], p.shy[1]);
43
+ if (p.crop === true) {
44
+ // A picture of the floor over a rectangle keeps its sides along x and y.
45
+ const corners = [at(m, p.o[0], p.o[1]), at(m, p.o[0] + p.u[0], p.o[1] + p.u[1]), at(m, p.o[0] + p.v[0], p.o[1] + p.v[1]), at(m, p.o[0] + p.u[0] + p.v[0], p.o[1] + p.u[1] + p.v[1])];
46
+ const [x0, x1] = [Math.min(...corners.map((c) => c[0])), Math.max(...corners.map((c) => c[0]))];
47
+ const [y0, y1] = [Math.min(...corners.map((c) => c[1])), Math.max(...corners.map((c) => c[1]))];
48
+ moved.o = [x0, y0, p.o[2]];
49
+ moved.u = [x1 - x0, 0, 0];
50
+ moved.v = [0, y1 - y0, 0];
51
+ }
52
+ return moved;
53
+ }
54
+ const rect = (x, y, w, h) => ({ x0: x, y0: y, x1: x + w, y1: y + h });
55
+ /** A solid box on the floor: four sides up to `z1` and a flat top. `cls` paints the top; the sides are one tone darker (`cls-side`). */
56
+ function box(id, kind, r, z1, cls) {
57
+ const sides = [[r.x0, r.y0, r.x1, r.y0], [r.x1, r.y0, r.x1, r.y1], [r.x1, r.y1, r.x0, r.y1], [r.x0, r.y1, r.x0, r.y0]];
58
+ const planes = sides.map(([a, b, c, d], i) => wall(`${id}-${i + 1}`, kind, a, b, c, d, 0, z1, `<rect width="${n(Math.hypot(c - a, d - b))}" height="${n(z1)}" class="${cls}-side"/>`));
59
+ planes.push(flat(`${id}-top`, kind, r.x0, r.y0, r.x1, r.y1, z1, `<rect width="${n(r.x1 - r.x0)}" height="${n(r.y1 - r.y0)}" class="${cls}"/>`));
60
+ return planes;
61
+ }
62
+ /** The surround of one area: four sides over the rectangle, each in pieces of at most `SURROUND_PIECE`, facing out from the area's middle (it gives way to a nearly level eye, as the glass of a rink does, but only then). */
63
+ function surround(id, r) {
64
+ const centre = [(r.x0 + r.x1) / 2, (r.y0 + r.y1) / 2];
65
+ const sides = [[r.x0, r.y0, r.x1, r.y0], [r.x1, r.y0, r.x1, r.y1], [r.x1, r.y1, r.x0, r.y1], [r.x0, r.y1, r.x0, r.y0]];
66
+ const planes = [];
67
+ sides.forEach(([xa, ya, xb, yb], side) => {
68
+ const long = Math.hypot(xb - xa, yb - ya);
69
+ const pieces = Math.max(1, Math.ceil(long / SURROUND_PIECE));
70
+ const rim = Math.min(0.3, SURROUND_HEIGHT * 0.12);
71
+ for (let i = 0; i < pieces; i += 1) {
72
+ const [t0, t1] = [i / pieces, (i + 1) / pieces];
73
+ const run = long / pieces;
74
+ const svg = `<rect width="${n(run)}" height="${n(SURROUND_HEIGHT)}" class="u-surround"/><rect width="${n(run)}" height="${n(rim)}" class="u-surround-rim"/>`;
75
+ const p = wall(`${id}-${side + 1}-${i + 1}`, "surround", xa + (xb - xa) * t0, ya + (yb - ya) * t0, xa + (xb - xa) * t1, ya + (yb - ya) * t1, 0, SURROUND_HEIGHT, svg);
76
+ planes.push({ ...shy(p, centre), fadeTo: 0.35, fadeAt: 0.85 });
77
+ }
78
+ });
79
+ return planes;
80
+ }
81
+ /** A scoreboard that faces the eye: its panel (and where it is divided into the panels of the areas), hung on cables from over its top, or standing on legs. */
82
+ function board(id, cx, cy, wide, tall, bottom, extra, hung, cuts) {
83
+ const total = tall + extra;
84
+ const top = hung ? extra : 0;
85
+ const line = Math.max(0.12, wide * 0.008);
86
+ const holds = hung
87
+ ? `<path d="M${n(wide * 0.12)} 0V${n(extra)}M${n(wide * 0.88)} 0V${n(extra)}" class="u-sbline" fill="none" stroke-width="${n(line)}"/>`
88
+ : `<path d="M${n(wide * 0.14)} ${n(tall)}V${n(total)}M${n(wide * 0.86)} ${n(tall)}V${n(total)}" class="u-sbline" fill="none" stroke-width="${n(line * 1.6)}"/>`;
89
+ const panel = `<rect x="0" y="${n(top)}" width="${n(wide)}" height="${n(tall)}" rx="${n(wide * 0.02)}" class="u-sbframe"/>${cuts === "" ? "" : `<path d="${cuts}" transform="translate(0 ${n(top)})" class="u-sbline" fill="none" stroke-width="${n(line * 0.7)}"/>`}`;
90
+ return { ...plane(id, "screen", [cx - wide / 2, cy, bottom + total], [wide, 0, 0], [0, 0, -total], `${holds}${panel}`), billboard: true };
91
+ }
92
+ /**
93
+ * The viewpoints of a layout. Overhead, broadcast and the low corner are a single surface's (`TURN_PRESETS`); the end-on view of a net sport looks along its surfaces' length, where the nets are seen whole, as `NET_PRESETS` does. All are turned a quarter where
94
+ * the floor or the surfaces stand across the picture, so that the long way of the floor runs across the screen. `floor` is the picture's frame (the hall's, turned if the hall asks) and `lengthAlongX` says whether the surfaces' length runs along its x.
95
+ */
96
+ export function multiPresets(sport, floor, lengthAlongX) {
97
+ const portrait = floor.h > floor.w ? 90 : 0;
98
+ const nets = isNetSport(sport);
99
+ const end = nets ? NET_PRESETS.end : TURN_PRESETS.end;
100
+ const along = nets ? (lengthAlongX ? 0 : 90) : portrait;
101
+ return {
102
+ overhead: { ...TURN_PRESETS.overhead, yaw: portrait },
103
+ broadcast: { ...TURN_PRESETS.broadcast, yaw: wrap(TURN_PRESETS.broadcast.yaw + portrait) },
104
+ end: { ...end, yaw: wrap(end.yaw + along) },
105
+ corner: { ...TURN_PRESETS.corner, yaw: wrap(TURN_PRESETS.corner.yaw + portrait) },
106
+ };
107
+ }
108
+ /**
109
+ * A tournament layout as a 3D model: the floor is the layout's flat drawing, and on it stand each surface as it stands on its own page, the surrounds, the umpires' chairs and score tables, the scoreboards and, with `stadium`, the stands
110
+ * of the hall. A `TurnModel` like any other: `turnMarkupOf` and `mountMultiTurn` make a view of it.
111
+ */
112
+ export function buildMultiTurn(options, settings = {}) {
113
+ const wanted = settings.uprights;
114
+ const upOptions = wanted === false || wanted === true || wanted === undefined ? {} : wanted;
115
+ const on = wanted !== false;
116
+ const stadiumOn = settings.venue !== false && options.stadium !== undefined && options.stadium !== false;
117
+ const drawn = multiDrawing(stadiumOn ? options : { ...options, stadium: undefined }, true, on);
118
+ const { frame, title, description } = drawn;
119
+ const layout = frame.layout;
120
+ const spec = layout.spec;
121
+ const language = options.language === "ja" ? "ja" : "en";
122
+ const id = settings.id ?? (typeof options.id === "string" && options.id !== "" ? `${options.id}-turn` : `ky-turn-${hash(JSON.stringify([options, settings.uprights, settings.preset, settings.venue]))}`);
123
+ const only = upOptions.only;
124
+ const keep = (kind) => only === undefined || only.includes(kind);
125
+ const items = [];
126
+ const toPicture = mapOf(frame.to);
127
+ if (on) {
128
+ // ---- Each surface, as it stands on its own page, moved to its cell. ----
129
+ const common = { ...spec.surface, margin: 0, language };
130
+ if (spec.kind === "cells") {
131
+ const own = uprightsFor(common, { ...upOptions, venue: undefined, score: "floor" });
132
+ const surfaceBox = outlineOf(spec.surface).box;
133
+ for (const cell of layout.cells) {
134
+ const into = mapOf((x, y) => frame.to(...(cell.turn ? [cell.x + cell.w - y, cell.y + x] : [cell.x + x, cell.y + y])));
135
+ for (const item of own.items) {
136
+ // The hall's floor under a table is the layout's own, so the planes that stood in for it are left behind.
137
+ const planes = item.planes.filter((p) => p.kind !== "ground").map((p) => place(p, into, `c${cell.n}-${p.id}`));
138
+ if (planes.length > 0)
139
+ items.push({ ...item, id: `c${cell.n}-${item.id}`, at: at(into, item.at[0], item.at[1]), planes });
140
+ }
141
+ }
142
+ // A surface that stands over the floor (a table's top) is a plane of its own, with the surface's own drawing on it.
143
+ for (const lifted of drawn.lifted) {
144
+ const local = flat("table-top", "table", 0, 0, surfaceBox.w, surfaceBox.h, lifted.lift, `<g transform="translate(${n(-lifted.box.x)} ${n(-lifted.box.y)})">${lifted.body}</g>`);
145
+ const into = mapOf((x, y) => frame.to(...(lifted.cell.turn ? [lifted.cell.x + lifted.cell.w - y, lifted.cell.y + x] : [lifted.cell.x + x, lifted.cell.y + y])));
146
+ items.push({ id: `c${lifted.cell.n}-table-top`, kind: "table-legs", at: at(into, surfaceBox.w / 2, surfaceBox.h / 2), height: lifted.lift, planes: [place(local, into, `c${lifted.cell.n}-table-top`)] });
147
+ }
148
+ }
149
+ // ---- What only a layout has. ----
150
+ const parts = spec.parts;
151
+ const layoutPlanes = (ps) => ps.map((p) => place(p, toPicture, p.id));
152
+ if (parts.barriers === true && upOptions.boards !== false && keep("surrounds")) {
153
+ const planes = layout.cells.flatMap((c) => surround(`c${c.n}-surround`, { x0: c.bay.x + 0.5, y0: c.bay.y + 0.5, x1: c.bay.x + c.bay.w - 0.5, y1: c.bay.y + c.bay.h - 0.5 }));
154
+ items.push({ id: "surrounds", kind: "surrounds", at: at(toPicture, layout.floor.w / 2, layout.floor.h / 2), height: SURROUND_HEIGHT, planes: layoutPlanes(planes) });
155
+ }
156
+ if (parts.officials === true && keep("officials")) {
157
+ const planes = [];
158
+ for (const c of layout.cells)
159
+ planes.push(...officialsOf(c));
160
+ items.push({ id: "officials", kind: "officials", at: at(toPicture, layout.floor.w / 2, layout.floor.h / 2), height: DESK, planes: layoutPlanes(planes) });
161
+ }
162
+ if (keep("scoreboard")) {
163
+ const boards = [];
164
+ if (parts.scoreboard === "each")
165
+ for (const c of layout.cells)
166
+ boards.push(board(`c${c.n}-scoreboard`, c.x + c.w / 2, c.y + c.h + 1.9, 4, STANDING.tall, 0, STANDING.legs, false, ""));
167
+ const sb = layout.shared;
168
+ if (sb !== undefined) {
169
+ const count = layout.cells.length;
170
+ const cols = count <= 4 ? Math.min(2, count) : count;
171
+ const rows = count <= 4 && count > 2 ? 2 : 1;
172
+ let cuts = "";
173
+ for (let i = 1; i < cols; i += 1)
174
+ cuts += `M${n((sb.w * i) / cols)} 0v${n(sb.h)}`;
175
+ for (let j = 1; j < rows; j += 1)
176
+ cuts += `M0 ${n((sb.h * j) / rows)}h${n(sb.w)}`;
177
+ boards.push(board("scoreboard", sb.x + sb.w / 2, sb.y + sb.h / 2, sb.w, sb.h, HUNG.bottom, HUNG.cables, true, cuts));
178
+ }
179
+ if (boards.length > 0)
180
+ items.push({ id: "scoreboard", kind: "scoreboard", at: at(toPicture, layout.floor.w / 2, layout.floor.h / 2), height: parts.scoreboard === "shared" ? HUNG.bottom : STANDING.legs, planes: layoutPlanes(boards) });
181
+ }
182
+ }
183
+ // ---- The stands of the stadium round the hall, in the picture's own frame. ----
184
+ let defs = "";
185
+ let crowdStyle = "";
186
+ if (drawn.stands !== undefined && keep("stands")) {
187
+ const roof = typeof settings.venue === "object" && settings.venue.roof !== undefined ? settings.venue.roof : typeof options.stadium === "object" && options.stadium.roof === true;
188
+ const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
189
+ items.push(...venue.items.filter((item) => item.kind !== "roof" || roof));
190
+ defs = venue.defs;
191
+ crowdStyle = "--ky-cr-home:#8b94a3;--ky-cr-away:#8b94a3";
192
+ }
193
+ // ---- The model. ----
194
+ const uprights = { items, lift: 0, thin: thinnest(drawn.view.w, drawn.view.h), area: { x0: drawn.view.x, y0: drawn.view.y, x1: drawn.view.x + drawn.view.w, y1: drawn.view.y + drawn.view.h }, whole: true, ...(defs === "" ? {} : { defs }) };
195
+ const planes = planesOf(uprights);
196
+ const scale = 1400 / Math.max(drawn.view.w, drawn.view.h);
197
+ const centre = [drawn.view.x + drawn.view.w / 2, drawn.view.y + drawn.view.h / 2];
198
+ const box3 = sceneBox(planes, uprights.area, 0, centre, scale);
199
+ const look = /^<svg[^>]* data-look="([^"]*)"/.exec(drawn.svg)?.[1];
200
+ const surfaceLike = { sport: spec.sport, language, ...(options.theme === undefined ? {} : { theme: options.theme }), ...(options.style === undefined ? {} : { style: options.style }) };
201
+ const standing = describeUprights(surfaceLike, uprights, language);
202
+ const first = layout.cells[0];
203
+ const presets = multiPresets(spec.sport, { w: frame.lw, h: frame.lh }, first === undefined || first.turn === frame.turned);
204
+ return {
205
+ id,
206
+ language,
207
+ options: surfaceLike,
208
+ title,
209
+ description: standing === "" ? description : `${description}${language === "ja" ? "" : " "}${standing}`,
210
+ standing,
211
+ floor: drawn.svg,
212
+ viewBox: { x: drawn.view.x, y: drawn.view.y, w: drawn.view.w, h: drawn.view.h },
213
+ scale,
214
+ uprights,
215
+ defs,
216
+ crowdStyle,
217
+ venue: drawn.stands !== undefined,
218
+ look: { sport: spec.sport, look: look ?? "plain" },
219
+ planes,
220
+ box: box3,
221
+ centre,
222
+ view: openingView(settings, presets),
223
+ turnable: true,
224
+ presets,
225
+ flat: multiDrawing(options, false).svg,
226
+ };
227
+ }
228
+ /** The umpire's chair and the score table beside a surface, where the flat drawing has them (feet, in the layout's frame). */
229
+ function officialsOf(c) {
230
+ if (c.turn) {
231
+ // Beside the net, on the right of a surface that stands across the layout.
232
+ const cy = c.y + c.h / 2;
233
+ return [...box(`c${c.n}-chair`, "chair", rect(c.x + c.w + 1.8, cy - 3.2, 1.6, 1.6), CHAIR, "u-chair"), ...box(`c${c.n}-desk`, "desk", rect(c.x + c.w + 1.5, cy - 0.8, 1.1, 3.6), DESK, "u-chair")];
234
+ }
235
+ const cx = c.x + c.w / 2;
236
+ return [...box(`c${c.n}-chair`, "chair", rect(cx + 1.6, c.y - 3.4, 1.6, 1.6), CHAIR, "u-chair"), ...box(`c${c.n}-desk`, "desk", rect(cx - 5, c.y - 3.1, 3.6, 1.1), DESK, "u-chair")];
237
+ }
238
+ /**
239
+ * Mount a tournament layout as a 3D view in a container: the same view as `mountKyogijoTurn`'s, with the controls, the pointer, the keys and the auto-turn of every 3D view, over the layout's floor with each surface standing on it.
240
+ * @example
241
+ * ```ts no-run
242
+ * import { mountMultiTurn } from "@johnmorrisdotca/kyogijo-stadiums/multi";
243
+ * const view = mountMultiTurn(document.querySelector("#hall")!, { preset: "tabletennis-4", stadium: true }, { preset: "broadcast" });
244
+ * view.set({ yaw: 40 });
245
+ * ```
246
+ */
247
+ export function mountMultiTurn(host, options, settings = {}) {
248
+ return mountTurn(host, options, settings, buildMultiTurn);
249
+ }
250
+ /** A tournament layout as the markup of a 3D view, still until a page's script makes it turn: pure, so a server can make it. */
251
+ export function multiTurnMarkup(options, settings = {}) {
252
+ return turnMarkupOf(buildMultiTurn(options, settings), settings);
253
+ }
package/dist/strings.d.ts CHANGED
@@ -993,7 +993,7 @@ export interface KyogijoWords {
993
993
  item: string;
994
994
  itemSag: string;
995
995
  /** The names of what stands on the surface, by kind. */
996
- parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge", string>;
996
+ parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials", string>;
997
997
  };
998
998
  /** The names of the zones that `zoneAt` answers with (rules.ts), by id. */
999
999
  rules: Record<RuleId, string>;
package/dist/strings.js CHANGED
@@ -382,7 +382,7 @@ export const KYOGIJO_STRINGS = {
382
382
  boundary: "Boundary", pitch: "Pitch", "bowling-crease": "Bowling crease", "popping-crease": "Popping crease", "return-crease": "Return crease", wicket: "Wicket", "thirty-yard-circle": "30-yard circle",
383
383
  "twenty-three-line": "23-metre line", "shooting-circle": "Shooting circle", "penalty-spot": "Penalty spot", "broken-circle-line": "Broken line round the circle", "goal-area-line": "Goal-area line",
384
384
  "finish-line": "Finish line", kerb: "Kerb", "lane-lines": "Lane lines", bend: "Bend", straight: "Straight", "infield-pitch": "Soccer pitch", "start-line": "Start line", breakline: "Breakline", "water-jump": "Water jump", "exchange-zone": "Takeover zone",
385
- "referee-position": "Referee's place", "shiai-jo": "Shiai-jo", floorboard: "Floorboard", heri: "Heri, the mat's edge", "contest-area": "Contest area", "safety-area": "Safety area", "danger-zone": "Danger zone", "start-mark": "Start mark", "common-safe-zone": "Common safe zone", "free-zone": "Free zone", platform: "Platform", "technical-table": "Technical table", arena: "Arena floor", "competition-area": "Competition area", "boundary-band": "Boundary band", "start-mat": "Start mat", judge: "Judge", "between-areas": "Between the areas", "centre-cross": "Centre cross", "kaishi-sen": "Kaishi-sen", "outer-boundary": "Outer boundary", dohyo: "Dohyō", ring: "Ring", tawara: "Tawara", tokudawara: "Tokudawara", "shikiri-sen": "Shikiri-sen", "jano-me": "Jano-me", shajo: "Shajō", "shooting-position": "Shooting position", honza: "Honza", yamichi: "Yamichi", "yatori-michi": "Path for the arrows", azuchi: "Azuchi", mato: "Target", kamiza: "Kamiza", shimoza: "Shimoza", shinzen: "Shinzen", dais: "Dais, the seat of honour", banner: "Banner", entrance: "Entrance", window: "Window", tatami: "Tatami", wall: "Wall", "inner-lane": "Inner lane", "outer-lane": "Outer lane", "crossing-straight": "Crossing straight", "warm-up-lane": "Warm-up lane", padding: "Padding", infield: "Infield", "coaches-area": "Coaches' area", blocks: "Blocks", "track-line": "Line of the track", "start-lines": "Starting lines", ice: "Ice", "technical-panel": "Technical panel", referee: "Referee", "kiss-and-cry": "Kiss and cry", judges: "Judges", runway: "Runway", "take-off-board": "Take-off board", "landing-area": "Landing area", "throwing-circle": "Throwing circle", sector: "Landing sector", "stop-board": "Stop board", cage: "Cage", "throwing-arc": "Throwing arc", crossbar: "Crossbar", "landing-mat": "Landing mat", "run-up": "Run-up", "zero-line": "Zero line", box: "Box",
385
+ "referee-position": "Referee's place", "shiai-jo": "Shiai-jo", floorboard: "Floorboard", heri: "Heri, the mat's edge", "contest-area": "Contest area", "safety-area": "Safety area", "danger-zone": "Danger zone", "start-mark": "Start mark", "common-safe-zone": "Common safe zone", "free-zone": "Free zone", platform: "Platform", "technical-table": "Technical table", "centre-tape": "Centre tape", "kata-judges": "Judges, shomen side", "coach-chair": "Coach's chair", "kata-entry": "Entry and exit, opposite shomen", arena: "Arena floor", "competition-area": "Competition area", "boundary-band": "Boundary band", "start-mat": "Start mat", judge: "Judge", "between-areas": "Between the areas", "centre-cross": "Centre cross", "kaishi-sen": "Kaishi-sen", "outer-boundary": "Outer boundary", dohyo: "Dohyō", ring: "Ring", tawara: "Tawara", tokudawara: "Tokudawara", "shikiri-sen": "Shikiri-sen", "jano-me": "Jano-me", shajo: "Shajō", "shooting-position": "Shooting position", honza: "Honza", yamichi: "Yamichi", "yatori-michi": "Path for the arrows", azuchi: "Azuchi", mato: "Target", kamiza: "Kamiza", shimoza: "Shimoza", shinzen: "Shinzen", dais: "Dais, the seat of honour", banner: "Banner", entrance: "Entrance", window: "Window", tatami: "Tatami", wall: "Wall", "inner-lane": "Inner lane", "outer-lane": "Outer lane", "crossing-straight": "Crossing straight", "warm-up-lane": "Warm-up lane", padding: "Padding", infield: "Infield", "coaches-area": "Coaches' area", blocks: "Blocks", "track-line": "Line of the track", "start-lines": "Starting lines", ice: "Ice", "technical-panel": "Technical panel", referee: "Referee", "kiss-and-cry": "Kiss and cry", judges: "Judges", runway: "Runway", "take-off-board": "Take-off board", "landing-area": "Landing area", "throwing-circle": "Throwing circle", sector: "Landing sector", "stop-board": "Stop board", cage: "Cage", "throwing-arc": "Throwing arc", crossbar: "Crossbar", "landing-mat": "Landing mat", "run-up": "Run-up", "zero-line": "Zero line", box: "Box",
386
386
  "seven-metre-line": "7-metre line", "four-metre-line": "4-metre line", "throw-off-area": "Throw-off area", "restraining-line": "Restraining line", "wing-line": "Wing line", "marking-area": "Marking area", "two-point-arc": "Two-point arc",
387
387
  "lane-rope": "Lane rope", "lane-line": "Lane marking on the floor", "lane-end-cross": "Cross line at the end of the lane marking", "fifteen-metre-mark": "15 m mark", "touch-pad": "Touch panel", "starting-block": "Starting block", "backstroke-flags": "Backstroke flags", "false-start-rope": "False start rope", gutter: "Gutter",
388
388
  "two-metre-line": "2 m line", "five-metre-mark": "5 m mark", "six-metre-line": "6 m line", "half-distance-line": "Half distance line", "re-entry-area": "Re-entry area", "referee-platform": "Referee platform", "official-table": "Official table", "substitution-area": "Substitution area",
@@ -432,7 +432,7 @@ export const KYOGIJO_STRINGS = {
432
432
  announce: "{name} view", announceAngle: "Turned to {yaw} degrees, seen from {pitch} degrees above the floor.",
433
433
  flat: "This browser cannot draw the 3D view, so the flat drawing is shown.",
434
434
  standing: "Standing on it, in the 3D view: {list}.", item: "{name} {h}", itemSag: "{name} {h} at the posts, {low} at the middle",
435
- parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness" },
435
+ parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness", surrounds: "surrounds", officials: "umpires' chairs and score tables" },
436
436
  },
437
437
  rules: {
438
438
  "defensive-zone": "Defensive zone",
@@ -917,7 +917,7 @@ export const KYOGIJO_STRINGS = {
917
917
  "distance-line": "距離を示すライン", "large-rectangle": "大きい長方形", "small-rectangle": "小さい長方形", "thirteen-metre-arc": "13メートルアーク", "forty-metre-arc": "40メートルアーク", "penalty-point": "ペナルティーマーク",
918
918
  boundary: "バウンダリー", pitch: "ピッチ", "bowling-crease": "ボウリングクリース", "popping-crease": "ポッピングクリース", "return-crease": "リターンクリース", wicket: "ウィケット", "thirty-yard-circle": "30ヤードサークル",
919
919
  "twenty-three-line": "23メートルライン", "shooting-circle": "シューティングサークル", "penalty-spot": "ペナルティースポット", "broken-circle-line": "サークルを囲む破線", "goal-area-line": "ゴールエリアライン",
920
- "referee-position": "主審の位置", "shiai-jo": "試合場", floorboard: "床板", heri: "畳縁", "contest-area": "試合場内", "safety-area": "安全地帯", "danger-zone": "危険地帯", "start-mark": "開始線", "common-safe-zone": "共通の安全地帯", "free-zone": "フリーゾーン", platform: "畳の台", "technical-table": "係員席", arena: "アリーナの床", "competition-area": "試合場", "boundary-band": "境界の帯", "start-mat": "開始位置のマット", judge: "副審", "between-areas": "試合場の間", "centre-cross": "中央の×印", "kaishi-sen": "開始線", "outer-boundary": "場外の余地", dohyo: "土俵", ring: "土俵の円", tawara: "俵", tokudawara: "徳俵", "shikiri-sen": "仕切り線", "jano-me": "蛇の目", shajo: "射場", "shooting-position": "射位", honza: "本座", yamichi: "矢道", "yatori-michi": "矢取道", azuchi: "安土", mato: "的", kamiza: "上座", shimoza: "下座", shinzen: "神棚", dais: "上座の台", banner: "垂れ幕", entrance: "入口", window: "窓", tatami: "畳", wall: "壁", "inner-lane": "インレーン", "outer-lane": "アウトレーン", "crossing-straight": "交差ストレート", "warm-up-lane": "ウォームアップレーン", padding: "防護マット", infield: "インフィールド", "coaches-area": "コーチエリア", blocks: "ブロック", "track-line": "トラックのライン", "start-lines": "スタートライン", ice: "氷面", "technical-panel": "テクニカルパネル", referee: "レフェリー", "kiss-and-cry": "キス・アンド・クライ", judges: "ジャッジ席", runway: "助走路", "take-off-board": "踏切板", "landing-area": "砂場", "throwing-circle": "投てきサークル", sector: "着地場所", "stop-board": "足留材", cage: "囲い", "throwing-arc": "スターティング・ライン", crossbar: "バー", "landing-mat": "着地マット", "run-up": "助走路", "zero-line": "ゼロライン", box: "ボックス",
920
+ "referee-position": "主審の位置", "shiai-jo": "試合場", floorboard: "床板", heri: "畳縁", "contest-area": "試合場内", "safety-area": "安全地帯", "danger-zone": "危険地帯", "start-mark": "開始線", "common-safe-zone": "共通の安全地帯", "free-zone": "フリーゾーン", platform: "畳の台", "technical-table": "係員席", "centre-tape": "中央のテープ", "kata-judges": "審判員席(正面側)", "coach-chair": "コーチ席", "kata-entry": "入退場口(正面の反対側)", arena: "アリーナの床", "competition-area": "試合場", "boundary-band": "境界の帯", "start-mat": "開始位置のマット", judge: "副審", "between-areas": "試合場の間", "centre-cross": "中央の×印", "kaishi-sen": "開始線", "outer-boundary": "場外の余地", dohyo: "土俵", ring: "土俵の円", tawara: "俵", tokudawara: "徳俵", "shikiri-sen": "仕切り線", "jano-me": "蛇の目", shajo: "射場", "shooting-position": "射位", honza: "本座", yamichi: "矢道", "yatori-michi": "矢取道", azuchi: "安土", mato: "的", kamiza: "上座", shimoza: "下座", shinzen: "神棚", dais: "上座の台", banner: "垂れ幕", entrance: "入口", window: "窓", tatami: "畳", wall: "壁", "inner-lane": "インレーン", "outer-lane": "アウトレーン", "crossing-straight": "交差ストレート", "warm-up-lane": "ウォームアップレーン", padding: "防護マット", infield: "インフィールド", "coaches-area": "コーチエリア", blocks: "ブロック", "track-line": "トラックのライン", "start-lines": "スタートライン", ice: "氷面", "technical-panel": "テクニカルパネル", referee: "レフェリー", "kiss-and-cry": "キス・アンド・クライ", judges: "ジャッジ席", runway: "助走路", "take-off-board": "踏切板", "landing-area": "砂場", "throwing-circle": "投てきサークル", sector: "着地場所", "stop-board": "足留材", cage: "囲い", "throwing-arc": "スターティング・ライン", crossbar: "バー", "landing-mat": "着地マット", "run-up": "助走路", "zero-line": "ゼロライン", box: "ボックス",
921
921
  "finish-line": "フィニッシュライン", kerb: "縁石", "lane-lines": "レーンライン", bend: "曲走路", straight: "直走路", "infield-pitch": "サッカーのピッチ", "start-line": "スタートライン", breakline: "ブレイクライン", "water-jump": "水濠", "exchange-zone": "テークオーバーゾーン",
922
922
  "seven-metre-line": "7mスローライン", "four-metre-line": "4mライン", "throw-off-area": "スローオフエリア", "restraining-line": "リストレイニングライン", "wing-line": "ウイングライン", "marking-area": "マーキングエリア", "two-point-arc": "2ポイントアーク",
923
923
  "lane-rope": "レーンロープ", "lane-line": "床面のレーンライン", "lane-end-cross": "クロスライン", "fifteen-metre-mark": "15mマーク", "touch-pad": "タッチ板", "starting-block": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", gutter: "ガター",
@@ -968,7 +968,7 @@ export const KYOGIJO_STRINGS = {
968
968
  announce: "{name}の視点", announceAngle: "向きは{yaw}度、見下ろす角度は{pitch}度です。",
969
969
  flat: "このブラウザーでは3D表示ができないため、平面の図を表示しています。",
970
970
  standing: "3D表示で立って見えるもの:{list}。", item: "{name} {h}", itemSag: "{name} 支柱の位置で{h}(中央は{low})",
971
- parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み" },
971
+ parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み", surrounds: "競技エリアのフェンス", officials: "審判席と記録台" },
972
972
  },
973
973
  rules: {
974
974
  "defensive-zone": "ディフェンディングゾーン",
@@ -1,4 +1,4 @@
1
- export { mountKyogijoTurn, turnSupported } from "./turn.dom.ts";
1
+ export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.ts";
2
2
  export type { KyogijoTurn } from "./turn.dom.ts";
3
3
  export { buildTurn, floorOptions, openingView } from "./turn.model.ts";
4
4
  export type { TurnModel, TurnSettings } from "./turn.model.ts";
@@ -1,5 +1,5 @@
1
1
  // The 3D view: a surface as a floor that turns, with a net, a basket, a goal or a pair of posts standing on it.
2
- export { mountKyogijoTurn, turnSupported } from "./turn.dom.js";
2
+ export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.js";
3
3
  export { buildTurn, floorOptions, openingView } from "./turn.model.js";
4
4
  export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./turn.markup.js";
5
5
  export { KYOGIJO_TURN_STYLE } from "./turn.style.js";
@@ -25,8 +25,10 @@ export declare const NET_PRESETS: Readonly<Record<TurnPreset, TurnView>>;
25
25
  * stay nearly square on to it, and the end view looks along it.
26
26
  */
27
27
  export declare const CURLING_PRESETS: Readonly<Record<TurnPreset, TurnView>>;
28
- /** The four viewpoints of a sport. */
29
- export declare const presetsFor: (sport?: string) => Readonly<Record<TurnPreset, TurnView>>;
28
+ /** The four viewpoints as a table. */
29
+ export type PresetTable = Readonly<Record<TurnPreset, TurnView>>;
30
+ /** The four viewpoints of a sport, or the table itself when it is given one (a tournament layout has its own). */
31
+ export declare const presetsFor: (sport?: string | PresetTable) => PresetTable;
30
32
  /** The view a mount opens on. */
31
33
  export declare const TURN_DEFAULT: TurnPreset;
32
34
  export declare const PITCH_MAX = 84;
@@ -39,9 +41,9 @@ export declare function clampView(view: TurnView, fallback?: TurnView): TurnView
39
41
  /** The angle equal to `to` that is nearest to `from`, so that a change of view turns the short way round. */
40
42
  export declare const nearest: (from: number, to: number) => number;
41
43
  /** The view a viewpoint name stands for in a sport, or `undefined` for a name that is not one. */
42
- export declare const presetView: (name: string | undefined, sport?: string) => TurnView | undefined;
44
+ export declare const presetView: (name: string | undefined, sport?: string | PresetTable) => TurnView | undefined;
43
45
  /** The name of the viewpoint a view is in a sport, or `undefined` when it is none of them (it has been turned by hand). */
44
- export declare function presetOf(view: TurnView, sport?: string): TurnPreset | undefined;
46
+ export declare function presetOf(view: TurnView, sport?: string | PresetTable): TurnPreset | undefined;
45
47
  /** The steps of the keys and the buttons, in degrees, and the factor of a zoom step. A held Shift takes the larger. */
46
48
  export declare const STEP: {
47
49
  readonly yaw: 6;
@@ -41,8 +41,8 @@ export const CURLING_PRESETS = {
41
41
  };
42
42
  /** The sports whose net runs across the court and whose viewpoints are `NET_PRESETS`. */
43
43
  const NET_SPORTS = ["tennis", "volleyball", "badminton", "pickleball", "tabletennis", "padel", "sepaktakraw", "teqball"];
44
- /** The four viewpoints of a sport. */
45
- export const presetsFor = (sport) => (sport === "curling" ? CURLING_PRESETS : sport !== undefined && NET_SPORTS.includes(sport) ? NET_PRESETS : sport !== undefined && LONG_SPORTS.includes(sport) ? LONG_PRESETS : TURN_PRESETS);
44
+ /** The four viewpoints of a sport, or the table itself when it is given one (a tournament layout has its own). */
45
+ export const presetsFor = (sport) => typeof sport === "object" ? sport : (sport === "curling" ? CURLING_PRESETS : sport !== undefined && NET_SPORTS.includes(sport) ? NET_PRESETS : sport !== undefined && LONG_SPORTS.includes(sport) ? LONG_PRESETS : TURN_PRESETS);
46
46
  /** The view a mount opens on. */
47
47
  export const TURN_DEFAULT = "corner";
48
48
  export const PITCH_MAX = 84;
@@ -1,8 +1,8 @@
1
1
  import { type TurnPreset, type TurnView } from "./turn.camera.ts";
2
- import { type TurnSettings } from "./turn.model.ts";
2
+ import { type TurnModel, type TurnSettings } from "./turn.model.ts";
3
3
  import type { ScoreOptions, SurfaceOptions } from "./types.ts";
4
- /** What `mountKyogijoTurn` gives back. */
5
- export interface KyogijoTurn {
4
+ /** What `mountKyogijoTurn` gives back. `O` is what `update` takes: the options of a drawing, or (for `mountMultiTurn`) of a tournament layout. */
5
+ export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
6
6
  /** `3d`, or `flat` where the browser cannot draw the 3D view (or there is nothing to turn): the flat drawing is shown and the rest of this does nothing. */
7
7
  readonly mode: "3d" | "flat";
8
8
  /** The view now: where it is going if it is gliding. */
@@ -14,7 +14,7 @@ export interface KyogijoTurn {
14
14
  /** Back to the opening view. */
15
15
  reset(): void;
16
16
  /** Draw again with new options (a score that moved, a look that changed), keeping the view. */
17
- update(options: SurfaceOptions | ScoreOptions): void;
17
+ update(options: O): void;
18
18
  /** Whether the auto-turn is running. */
19
19
  readonly playing: boolean;
20
20
  /** Start the auto-turn, a slow turn of the whole view. It gives whether it is running (a surface that cannot turn gives `false`). Nothing turns by itself until this, the Play button or Space starts it, unless `autoPlay` asked. */
@@ -46,3 +46,8 @@ export declare function turnSupported(win?: {
46
46
  * ```
47
47
  */
48
48
  export declare function mountKyogijoTurn(host: HTMLElement, options: SurfaceOptions | ScoreOptions, settings?: TurnSettings): KyogijoTurn;
49
+ /**
50
+ * Mount any 3D model (`mountKyogijoTurn` is this with the model of one surface; `mountMultiTurn` with a tournament layout's): `build` makes the model from what `update` is given, and the rest, the controls, the pointer, the keys
51
+ * and the auto-turn, is the same.
52
+ */
53
+ export declare function mountTurn<O>(host: HTMLElement, options: O, settings: TurnSettings, build: (options: O, settings: TurnSettings) => TurnModel): KyogijoTurn<O>;