@johnmorrisdotca/kyogijo-stadiums 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +22 -7
- package/dist/budo/judo.d.ts +4 -0
- package/dist/budo/judo.js +1 -1
- package/dist/judging-entry.d.ts +12 -0
- package/dist/judging-entry.js +8 -0
- package/dist/judging.card.d.ts +21 -0
- package/dist/judging.card.js +34 -0
- package/dist/judging.d.ts +74 -0
- package/dist/judging.data.d.ts +16 -0
- package/dist/judging.data.js +53 -0
- package/dist/judging.draw.d.ts +66 -0
- package/dist/judging.draw.js +293 -0
- package/dist/judging.fit.d.ts +54 -0
- package/dist/judging.fit.js +95 -0
- package/dist/judging.js +197 -0
- package/dist/judging.style.d.ts +8 -0
- package/dist/judging.style.js +30 -0
- package/dist/judging.turn.d.ts +36 -0
- package/dist/judging.turn.js +169 -0
- package/dist/judging.types.d.ts +78 -0
- package/dist/judging.types.js +1 -0
- package/dist/judging.words.d.ts +46 -0
- package/dist/judging.words.js +40 -0
- package/dist/multi.turn.js +1 -9
- package/dist/strings.d.ts +1 -1
- package/dist/strings.js +2 -2
- package/dist/turn.kit.d.ts +13 -2
- package/dist/turn.kit.js +9 -0
- package/dist/turn.markup.js +1 -1
- package/dist/turn.model.d.ts +2 -0
- package/dist/turn.style.js +15 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// A judging assignment drawn: for each mat, the package's own judo area with a badge at every judge's seat and, where a name and its ID fit in the seat, a card with both; beside it (or under it) the chart of mat, seat,
|
|
2
|
+
// name and ID, whose rows grow to hold every name and ID whole. The surface is not drawn a second way: its markup is the drawing's own, moved and clipped. The same options always give the same string. The text
|
|
3
|
+
// alternative is `judgingTable`.
|
|
4
|
+
import { drawKyogijo } from "./draw.js";
|
|
5
|
+
import { safeClass } from "./common.js";
|
|
6
|
+
import { el, esc, hash, num } from "./format.js";
|
|
7
|
+
import { PLAN_CARD, badge, card } from "./judging.card.js";
|
|
8
|
+
import { fitChart, fitSeats } from "./judging.fit.js";
|
|
9
|
+
import { JUDGING_LEAGUE } from "./judging.data.js";
|
|
10
|
+
import { judgingPlan, judgingSetting, seatIn } from "./judging.js";
|
|
11
|
+
import { KYOGIJO_JUDGING_STYLE } from "./judging.style.js";
|
|
12
|
+
import { JUDGING_WORDS } from "./judging.words.js";
|
|
13
|
+
import { fill } from "./strings.js";
|
|
14
|
+
import { LINE_HEIGHT } from "./text.js";
|
|
15
|
+
import { VERSION } from "./version.js";
|
|
16
|
+
/** Feet in a metre: the surface's own drawing is in feet. */
|
|
17
|
+
const FT = 1 / 0.3048;
|
|
18
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
19
|
+
/** One mat's surface as markup: the drawing's own, without its title, its stylesheet and its apron. */
|
|
20
|
+
function inner(svg) {
|
|
21
|
+
const look = /^<svg[^>]* data-look="([^"]*)"/.exec(svg)?.[1];
|
|
22
|
+
const style = /<style>([\s\S]*?)<\/style>/.exec(svg)?.[1] ?? "";
|
|
23
|
+
let body = svg.replace(/<style>[\s\S]*?<\/style>/, "");
|
|
24
|
+
body = body.slice(body.indexOf("</desc>") + "</desc>".length, body.lastIndexOf("</svg>"));
|
|
25
|
+
body = body.replace(/<rect class="ky-apron"[^>]*\/>/g, "");
|
|
26
|
+
return { body, style, look };
|
|
27
|
+
}
|
|
28
|
+
/** The sentence that names one seat and who sits in it. */
|
|
29
|
+
function seatSentence(a, setting) {
|
|
30
|
+
const w = JUDGING_WORDS[setting.language].say;
|
|
31
|
+
const seat = a.seat.label[setting.language];
|
|
32
|
+
if (!a.assigned)
|
|
33
|
+
return fill(w.seatVacant, { seat });
|
|
34
|
+
if (a.id === "")
|
|
35
|
+
return fill(w.seatNoId, { seat, name: a.name });
|
|
36
|
+
return fill(w.seat, { seat, name: a.name === "" ? "—" : a.name, id: a.id });
|
|
37
|
+
}
|
|
38
|
+
/** The title and the description of an assignment: each mat with each seat and who is in it. */
|
|
39
|
+
export function judgingText(setting, options, chart) {
|
|
40
|
+
const words = JUDGING_WORDS[setting.language];
|
|
41
|
+
const style = fill(words.styleName[setting.style], { count: setting.mats[0].seats.length });
|
|
42
|
+
const mats = setting.total === 1 ? words.say.matsOne : fill(words.say.matsMany, { n: setting.total });
|
|
43
|
+
const title = options.title ?? fill(words.say.title, { style, mats });
|
|
44
|
+
const sentences = setting.mats.map((m) => fill(words.say.mat, { label: m.label, seats: m.seats.map((a) => seatSentence(a, setting)).join(words.say.join) }));
|
|
45
|
+
const description = options.description ?? [...sentences, chart ? words.say.names : words.say.chartOnly].join(setting.language === "ja" ? "" : " ");
|
|
46
|
+
return { title, description };
|
|
47
|
+
}
|
|
48
|
+
const metricsOf = (width) => {
|
|
49
|
+
const k = width / 1000;
|
|
50
|
+
return { k, pad: 28 * k, gap: 28 * k, titleH: 62 * k, panelGap: 40 * k, badge: 60 * k, rowPad: 14 * k, role: 17 * k, nameMax: 34 * k, nameMin: 22 * k, cardMin: 12 * k };
|
|
51
|
+
};
|
|
52
|
+
/** The rows of one mat's chart, each as tall as its name and ID need, set in a chart `width` wide. */
|
|
53
|
+
function chartRows(mat, width, m, setting) {
|
|
54
|
+
const words = JUDGING_WORDS[setting.language];
|
|
55
|
+
const textW = width - m.rowPad * 1.6 - m.badge - m.gap * 0.6;
|
|
56
|
+
return mat.seats.map((a) => {
|
|
57
|
+
if (!a.assigned)
|
|
58
|
+
return { a, fit: undefined, height: Math.max(m.badge + 2 * m.rowPad, m.rowPad * 2 + m.role * LINE_HEIGHT + 6 * m.k + m.nameMin * LINE_HEIGHT) };
|
|
59
|
+
const fit = fitChart({ name: a.name, id: a.id, idLabel: words.chart.idLabel, width: textW, max: m.nameMax, min: m.nameMin });
|
|
60
|
+
const height = m.rowPad * 2 + m.role * LINE_HEIGHT + 6 * m.k + fit.height;
|
|
61
|
+
return { a, fit, height: Math.max(height, m.badge + 2 * m.rowPad) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** The chart of one mat as markup at (x, y), `width` wide, and its height. */
|
|
65
|
+
function chartMarkup(mat, x, y, width, m, setting) {
|
|
66
|
+
const words = JUDGING_WORDS[setting.language];
|
|
67
|
+
const rows = chartRows(mat, width, m, setting);
|
|
68
|
+
const gap = 8 * m.k;
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
let out = "";
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const { a, fit } = row;
|
|
73
|
+
const cx = m.rowPad * 0.8 + m.badge / 2;
|
|
74
|
+
const tx = m.rowPad * 0.8 + m.badge + m.gap * 0.6;
|
|
75
|
+
let inside = el("rect", { class: "ky-ju-row", x: 0, y: cursor, width, height: row.height, rx: 14 * m.k });
|
|
76
|
+
inside += el("circle", { class: a.assigned ? "ky-ju-badge" : "ky-ju-badge ky-ju-badge-empty", cx, cy: cursor + row.height / 2, r: m.badge / 2 });
|
|
77
|
+
inside += el("text", { class: "ky-ju-badge-n", x: cx, y: cursor + row.height / 2 + m.badge * 0.18, "font-size": m.badge * 0.52 }, String(a.seat.n));
|
|
78
|
+
let ty = cursor + m.rowPad + m.role * 0.82;
|
|
79
|
+
inside += el("text", { class: "ky-ju-role", x: tx, y: ty, "font-size": m.role }, esc(a.seat.label[setting.language]));
|
|
80
|
+
ty += m.role * 0.3 + 6 * m.k;
|
|
81
|
+
if (fit === undefined) {
|
|
82
|
+
inside += el("text", { class: "ky-ju-vacant", x: tx, y: ty + m.nameMin * 0.95, "font-size": m.nameMin }, esc(words.chart.vacant));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const line of fit.name) {
|
|
86
|
+
inside += el("text", { class: "ky-ju-name", x: tx, y: ty + fit.nameSize * 0.88, "font-size": fit.nameSize }, esc(line));
|
|
87
|
+
ty += fit.nameSize * LINE_HEIGHT;
|
|
88
|
+
}
|
|
89
|
+
if (fit.id.length > 0) {
|
|
90
|
+
ty += fit.name.length > 0 ? fit.nameSize * 0.28 : 0;
|
|
91
|
+
for (const line of fit.id) {
|
|
92
|
+
inside += el("text", { class: "ky-ju-id", x: tx, y: ty + fit.idSize * 0.88, "font-size": fit.idSize }, esc(line));
|
|
93
|
+
ty += fit.idSize * LINE_HEIGHT;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
out += el("g", { class: "ky-ju-chart-row", "data-mat": mat.n, "data-seat": a.seat.id }, inside);
|
|
98
|
+
cursor += row.height + gap;
|
|
99
|
+
}
|
|
100
|
+
const height = Math.max(0, cursor - gap);
|
|
101
|
+
return { markup: el("g", { class: "ky-ju-chart", "data-mat": mat.n, transform: `translate(${num(x)} ${num(y)})` }, out), height };
|
|
102
|
+
}
|
|
103
|
+
/** The plan of one mat at (x, y) in a box: the surface moved and clipped to the region shown, with a badge or a card at every seat. */
|
|
104
|
+
function planMarkup(mat, surface, geom, region, box, m, setting, clip) {
|
|
105
|
+
const rw = region.x1 - region.x0;
|
|
106
|
+
const rh = region.y1 - region.y0;
|
|
107
|
+
const s = Math.min(box.w / rw, box.h / rh);
|
|
108
|
+
const w = rw * s;
|
|
109
|
+
const h = rh * s;
|
|
110
|
+
const x = box.x + (box.w - w) / 2;
|
|
111
|
+
const y = box.y;
|
|
112
|
+
const sf = s / FT;
|
|
113
|
+
const X = (mx) => x + (mx - region.x0) * s;
|
|
114
|
+
const Y = (my) => y + (my - region.y0) * s;
|
|
115
|
+
const words = JUDGING_WORDS[setting.language];
|
|
116
|
+
let markup = el("clipPath", { id: clip }, el("rect", { x: num(region.x0 * FT), y: num(region.y0 * FT), width: num(rw * FT), height: num(rh * FT) }));
|
|
117
|
+
markup += el("rect", { class: "ky-apron", x, y, width: w, height: h });
|
|
118
|
+
markup += el("g", { class: "ky-ju-plan", "clip-path": `url(#${clip})`, transform: `translate(${num(x - region.x0 * FT * sf)} ${num(y - region.y0 * FT * sf)}) scale(${(sf).toFixed(5)})` }, surface.body);
|
|
119
|
+
markup += el("rect", { class: "ky-ju-plan-frame", x, y, width: w, height: h });
|
|
120
|
+
// The card of a seat is the place the plan gives it: for a kata judge its chair and the table in front of it, for a contest's referee and side judges a card of their own.
|
|
121
|
+
const boxes = mat.seats.map((a) => {
|
|
122
|
+
const c = geom.places[a.seat.n - 1].card;
|
|
123
|
+
const cw = (c.x1 - c.x0) * s;
|
|
124
|
+
const ch = (c.y1 - c.y0) * s;
|
|
125
|
+
const pad = Math.max(2 * m.k, cw * 0.05);
|
|
126
|
+
const r = clamp(cw * 0.2, 6 * m.k, 14 * m.k);
|
|
127
|
+
return { cw, ch, pad, r, width: cw - 2 * pad, height: ch - 2 * pad - r, max: Math.min(cw * 0.3, 26 * m.k) };
|
|
128
|
+
});
|
|
129
|
+
const fitted = fitSeats(mat.seats, boxes, { min: m.cardMin, idLabel: words.chart.idLabel });
|
|
130
|
+
// A seat outside the picture (the referee in a close-up of the bench) has its name in the chart only: it does not fit.
|
|
131
|
+
const shown = mat.seats.map((a) => seatIn(geom.places[a.seat.n - 1], region));
|
|
132
|
+
const fits = mat.seats.map((a, i) => !a.assigned || (shown[i] && fitted[i] !== undefined));
|
|
133
|
+
mat.seats.forEach((a, i) => {
|
|
134
|
+
if (!shown[i])
|
|
135
|
+
return;
|
|
136
|
+
const place = geom.places[a.seat.n - 1];
|
|
137
|
+
const b = boxes[i];
|
|
138
|
+
const fit = fitted[i];
|
|
139
|
+
let g = "";
|
|
140
|
+
if (fit !== undefined)
|
|
141
|
+
g += card(PLAN_CARD, fit, a.seat.n, X(place.card.x0), Y(place.card.y0), b.cw, b.ch, b.pad, b.r, 3 * m.k);
|
|
142
|
+
else {
|
|
143
|
+
const br = clamp(Math.min(b.cw, 1.25 * s) * 0.3, 7 * m.k, 16 * m.k);
|
|
144
|
+
g += badge(PLAN_CARD, X((place.chair.x0 + place.chair.x1) / 2), Y((place.chair.y0 + place.chair.y1) / 2), br, a.seat.n, a.assigned);
|
|
145
|
+
}
|
|
146
|
+
markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
|
|
147
|
+
});
|
|
148
|
+
return { markup, width: w, height: h, fits };
|
|
149
|
+
}
|
|
150
|
+
/** The picture of an assignment and what was worked out to make it. `mode` `chart` is the chart alone (for beside a 3D view). */
|
|
151
|
+
export function judgingDrawing(options, mode = "full") {
|
|
152
|
+
const setting = judgingSetting(mode === "chart" && options.width === undefined ? { ...options, width: 600 } : options);
|
|
153
|
+
const language = setting.language;
|
|
154
|
+
const width = setting.width;
|
|
155
|
+
const m = metricsOf(width);
|
|
156
|
+
const geom = judgingPlan(setting.style);
|
|
157
|
+
const region = setting.focus === "bench" ? geom.bench : { x0: geom.extent.x0 - 0.3, y0: geom.extent.y0 - 0.3, x1: geom.extent.x1 + 0.3, y1: geom.extent.y1 + 0.3 };
|
|
158
|
+
const gid = safeClass(options.id).replace(/ /g, "-") || `ky-ju-${hash(JSON.stringify([options, mode, VERSION]))}`;
|
|
159
|
+
const league = JUDGING_LEAGUE[setting.style].league;
|
|
160
|
+
const portrait = setting.orientation === "portrait";
|
|
161
|
+
// The surface, drawn by the package: once for its stylesheet, then once for each mat (its ids differ).
|
|
162
|
+
let style = "";
|
|
163
|
+
let look;
|
|
164
|
+
const surfaces = mode === "chart" ? [] : setting.mats.map((mat, i) => {
|
|
165
|
+
const drawn = inner(drawKyogijo({ sport: "judo", league, budo: { style: setting.style, officials: true }, language, id: `${gid}-s${mat.n}`, style: i === 0 && options.css !== "none" ? "embed" : "none" }));
|
|
166
|
+
if (i === 0) {
|
|
167
|
+
style = drawn.style;
|
|
168
|
+
look = drawn.look;
|
|
169
|
+
}
|
|
170
|
+
return drawn;
|
|
171
|
+
});
|
|
172
|
+
// Whether the chart is drawn: asked for, or every name fits in its seat when the plan has the room that the chart would take.
|
|
173
|
+
const planBox = (withChart) => {
|
|
174
|
+
if (portrait)
|
|
175
|
+
return { w: width - 2 * m.pad, h: (withChart ? 0.66 : 0.86) * width };
|
|
176
|
+
return withChart ? { w: 0.48 * (width - 2 * m.pad - m.gap), h: 640 * m.k } : { w: width - 2 * m.pad, h: 0.82 * width };
|
|
177
|
+
};
|
|
178
|
+
const trial = (withChart) => {
|
|
179
|
+
const box = planBox(withChart);
|
|
180
|
+
return setting.mats.flatMap((mat, i) => planMarkup(mat, surfaces[i], geom, region, { x: 0, y: 0, ...box }, m, setting, "t").fits);
|
|
181
|
+
};
|
|
182
|
+
let chart = mode === "chart" || setting.chart === "always";
|
|
183
|
+
if (mode === "full" && setting.chart === "auto")
|
|
184
|
+
chart = !trial(false).every(Boolean);
|
|
185
|
+
if (mode === "full" && setting.chart === "never")
|
|
186
|
+
chart = false;
|
|
187
|
+
const cardsFit = mode === "chart" ? [] : trial(chart);
|
|
188
|
+
// The panels, one under another.
|
|
189
|
+
let body = "";
|
|
190
|
+
let y = m.pad;
|
|
191
|
+
const cards = [];
|
|
192
|
+
const styleText = fill(JUDGING_WORDS[language].styleName[setting.style], { count: setting.mats[0].seats.length });
|
|
193
|
+
setting.mats.forEach((mat, i) => {
|
|
194
|
+
let panel = el("text", { class: "ky-ju-mat", x: m.pad, y: y + 42 * m.k, "font-size": 38 * m.k }, esc(mat.label));
|
|
195
|
+
panel += el("text", { class: "ky-ju-style", x: width - m.pad, y: y + 40 * m.k, "font-size": 21 * m.k, "text-anchor": "end" }, esc(styleText));
|
|
196
|
+
const top = y + m.titleH;
|
|
197
|
+
let bottom = top;
|
|
198
|
+
if (mode === "full") {
|
|
199
|
+
const box = planBox(chart);
|
|
200
|
+
const plan = planMarkup(mat, surfaces[i], geom, region, { x: m.pad, y: top, ...box }, m, setting, `${gid}-c${mat.n}`);
|
|
201
|
+
panel += plan.markup;
|
|
202
|
+
bottom = top + plan.height;
|
|
203
|
+
mat.seats.forEach((a, k) => cards.push({ mat: mat.n, seat: a.seat.id, fits: plan.fits[k] }));
|
|
204
|
+
if (chart) {
|
|
205
|
+
const cx = portrait ? m.pad : m.pad + box.w + m.gap;
|
|
206
|
+
const cy = portrait ? bottom + m.gap : top;
|
|
207
|
+
const cw = portrait ? width - 2 * m.pad : width - m.pad - cx;
|
|
208
|
+
const c = chartMarkup(mat, cx, cy, cw, m, setting);
|
|
209
|
+
panel += c.markup;
|
|
210
|
+
bottom = Math.max(bottom, cy + c.height);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const c = chartMarkup(mat, m.pad, top, width - 2 * m.pad, m, setting);
|
|
215
|
+
panel += c.markup;
|
|
216
|
+
bottom = top + c.height;
|
|
217
|
+
}
|
|
218
|
+
body += el("g", { class: "ky-ju-panel", "data-mat": mat.n }, panel);
|
|
219
|
+
y = bottom + m.panelGap;
|
|
220
|
+
});
|
|
221
|
+
const height = Math.ceil(y - m.panelGap + m.pad);
|
|
222
|
+
const { title, description } = judgingText(setting, options, chart);
|
|
223
|
+
const titleId = `${gid}-t`;
|
|
224
|
+
const descId = `${gid}-d`;
|
|
225
|
+
const css = options.css === "none" ? "" : `<style>${style}\n${KYOGIJO_JUDGING_STYLE}</style>`;
|
|
226
|
+
const root = el("svg", {
|
|
227
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
228
|
+
class: `kyogijo ky-judging ky-judo${safeClass(options.className) === "" ? "" : ` ${safeClass(options.className)}`}`,
|
|
229
|
+
viewBox: `0 0 ${num(width)} ${num(height)}`,
|
|
230
|
+
role: "img",
|
|
231
|
+
"aria-labelledby": `${titleId} ${descId}`,
|
|
232
|
+
"data-sport": "judo",
|
|
233
|
+
"data-judging": setting.style,
|
|
234
|
+
"data-look": look ?? "yellow-red",
|
|
235
|
+
"data-mats": setting.total,
|
|
236
|
+
"data-chart": chart ? "yes" : "no",
|
|
237
|
+
"data-theme": options.theme === "light" || options.theme === "dark" ? options.theme : undefined,
|
|
238
|
+
"data-version": VERSION,
|
|
239
|
+
}, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` + css + el("rect", { class: "ky-ju-ground", x: 0, y: 0, width, height }) + body);
|
|
240
|
+
void cardsFit;
|
|
241
|
+
return { svg: root, width, height, chart, cards, title, description };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A judging assignment as one SVG string: for each mat, the judo area with a badge at each judge's seat and, where the name and ID fit in the seat, a card with both, and beside it the chart of seat, name and ID, whose rows
|
|
245
|
+
* grow to hold every name whole. `style` is the style the areas are judged in (`kata`: five judges); `mats` the mat areas, each with its `label` (`Mat 1`, `Mat 2` when missing) and its `officials`, each a `seat`, a `name`
|
|
246
|
+
* and an `id`. `mat` draws one mat alone (a tab); `chart` is `auto`, `always` or `never`; `focus: "bench"` shows the judges' bench close up so that the names in the seats are as large as they can be. Light and dark follow the
|
|
247
|
+
* page; `judgingTable` is the same thing as a table.
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { drawJudging } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
251
|
+
* const svg = drawJudging({
|
|
252
|
+
* style: "kata",
|
|
253
|
+
* mats: [{ label: "Mat 1", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }, { seat: "central", name: "Daniel Okafor", id: "J-0377" }] }],
|
|
254
|
+
* });
|
|
255
|
+
* console.log(svg.startsWith("<svg"), svg.includes("Aiko Mori"), svg.includes("J-1042"));
|
|
256
|
+
* // true true true
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export function drawJudging(options) {
|
|
260
|
+
return judgingDrawing(options).svg;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The chart of an assignment alone: for each mat its seats in order, each with its number, its role, its name and, under the name, its ID, in rows that grow so that no name or ID is cut. For a page that draws
|
|
264
|
+
* the 3D view of the mat and puts the chart beside it.
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* import { judgingChart } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
268
|
+
* const svg = judgingChart({ style: "kata", mats: [{ officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] });
|
|
269
|
+
* console.log(svg.includes("ky-ju-chart-row"), svg.includes("Not assigned"));
|
|
270
|
+
* // true true
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export function judgingChart(options) {
|
|
274
|
+
return judgingDrawing(options, "chart").svg;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The same thing as the picture, as a table: each mat, each seat and who sits in it, with the ID. For a reader who cannot see the drawing, and for a page that wants the data.
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* import { judgingTable } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
281
|
+
* const table = judgingTable({ style: "kata", mats: [{ officials: [{ seat: 3, name: "Daniel Okafor", id: "J-0377" }] }] });
|
|
282
|
+
* console.log(table.startsWith("<table"), table.includes("Daniel Okafor"));
|
|
283
|
+
* // true true
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
export function judgingTable(options) {
|
|
287
|
+
const setting = judgingSetting(options);
|
|
288
|
+
const words = JUDGING_WORDS[setting.language];
|
|
289
|
+
const { title } = judgingText(setting, options, true);
|
|
290
|
+
const head = `<thead><tr><th scope="col">${esc(words.table.mat)}</th><th scope="col">${esc(words.table.seat)}</th><th scope="col">${esc(words.table.name)}</th><th scope="col">${esc(words.table.id)}</th></tr></thead>`;
|
|
291
|
+
const rows = setting.mats.flatMap((mat) => mat.seats.map((a) => `<tr><th scope="row">${esc(mat.label)}</th><td>${esc(a.seat.label[setting.language])}</td><td>${a.assigned ? esc(a.name) : esc(words.chart.vacant)}</td><td>${esc(a.id)}</td></tr>`)).join("");
|
|
292
|
+
return `<table class="ky-table" data-kind="judging"><caption>${esc(fill(words.table.caption, { title }))}</caption>${head}<tbody>${rows}</tbody></table>`;
|
|
293
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { JudgingAssignment } from "./judging.types.ts";
|
|
2
|
+
/** What a name and an ID were set as. */
|
|
3
|
+
export interface CardFit {
|
|
4
|
+
name: string[];
|
|
5
|
+
nameSize: number;
|
|
6
|
+
/** The ID's lines (with its label), or none. */
|
|
7
|
+
id: string[];
|
|
8
|
+
idSize: number;
|
|
9
|
+
/** The width of the widest line and the height of the whole, in the box's units. */
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
/** What to fit, and how. Sizes and the box are in the same units. */
|
|
14
|
+
export interface CardAsk {
|
|
15
|
+
name: string;
|
|
16
|
+
id: string;
|
|
17
|
+
/** The word before an ID (`ID`). */
|
|
18
|
+
idLabel: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
/** The biggest and the smallest size of the name; the ID is set at 0.62 of the name's size and no smaller than 0.75 of the least. */
|
|
22
|
+
max: number;
|
|
23
|
+
min: number;
|
|
24
|
+
/** The most lines the name may take. */
|
|
25
|
+
maxLines: number;
|
|
26
|
+
/** Whether a word wider than the box may be broken inside (the chart), or the card does not fit (a seat). */
|
|
27
|
+
breakWords: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** The lines a text is wrapped in at a size, no line wider than `width`, or undefined when a word is wider and breaking is not allowed. */
|
|
30
|
+
export declare function wrapText(text: string, width: number, size: number, breakWords: boolean, measure?: (text: string) => number): string[] | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Set a name, and an ID under it, in the biggest size that fits the box. Returns undefined when nothing from `max` down to `min` fits (for a card in a seat, that is "does not fit"); with `breakWords` and `maxLines` set
|
|
33
|
+
* as large as it takes, it always returns something, which may be taller than the box (the chart grows).
|
|
34
|
+
*/
|
|
35
|
+
export declare function fitCard(ask: CardAsk): CardFit | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* What the chart does: the biggest size at which the name takes three lines at most with no word broken, shrinking to the floor; then, at the floor, as many lines as it takes and a word wider than the row broken inside.
|
|
38
|
+
* It always fits, whatever the text, because the row grows.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fitChart(ask: Omit<CardAsk, "maxLines" | "breakWords" | "height">): CardFit;
|
|
41
|
+
/** The box a card gives a seat's name and ID, and the biggest size its name may be set in. */
|
|
42
|
+
export interface SeatBox {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
max: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fit the name and ID of every seat of a mat into its card, as one size for all of them, so that the row of cards reads evenly: each seat that has a name is fitted alone, the smallest size any of them needs is taken, and
|
|
49
|
+
* each is fitted again at that size. A seat whose name does not fit at the floor, or that has nobody in it, has no card (undefined). `boxes` has the card box of each seat, in order.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fitSeats(seats: readonly JudgingAssignment[], boxes: readonly SeatBox[], limits: {
|
|
52
|
+
min: number;
|
|
53
|
+
idLabel: string;
|
|
54
|
+
}): (CardFit | undefined)[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LINE_HEIGHT, textWidth } from "./text.js";
|
|
2
|
+
/** The gap between the name and the ID, as a share of the name's size. */
|
|
3
|
+
const GAP = 0.28;
|
|
4
|
+
/** An ID's width in ems: a face with fixed-width digits and letters, a little wider than the proportional one. */
|
|
5
|
+
const idWidth = (text) => textWidth(text, false) * 1.12;
|
|
6
|
+
/** The lines a text is wrapped in at a size, no line wider than `width`, or undefined when a word is wider and breaking is not allowed. */
|
|
7
|
+
export function wrapText(text, width, size, breakWords, measure = (t) => textWidth(t)) {
|
|
8
|
+
const words = text.trim().split(/\s+/).filter((w) => w !== "");
|
|
9
|
+
const lines = [];
|
|
10
|
+
let current = "";
|
|
11
|
+
const fits = (t) => measure(t) * size <= width + 1e-9;
|
|
12
|
+
for (const word of words) {
|
|
13
|
+
const joined = current === "" ? word : `${current} ${word}`;
|
|
14
|
+
if (fits(joined)) {
|
|
15
|
+
current = joined;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (current !== "")
|
|
19
|
+
lines.push(current);
|
|
20
|
+
current = "";
|
|
21
|
+
if (fits(word)) {
|
|
22
|
+
current = word;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!breakWords)
|
|
26
|
+
return undefined;
|
|
27
|
+
// A word wider than the box: broken inside, by characters.
|
|
28
|
+
let piece = "";
|
|
29
|
+
for (const ch of word) {
|
|
30
|
+
if (piece !== "" && !fits(piece + ch)) {
|
|
31
|
+
lines.push(piece);
|
|
32
|
+
piece = ch;
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
piece += ch;
|
|
36
|
+
}
|
|
37
|
+
current = piece;
|
|
38
|
+
}
|
|
39
|
+
if (current !== "")
|
|
40
|
+
lines.push(current);
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Set a name, and an ID under it, in the biggest size that fits the box. Returns undefined when nothing from `max` down to `min` fits (for a card in a seat, that is "does not fit"); with `breakWords` and `maxLines` set
|
|
45
|
+
* as large as it takes, it always returns something, which may be taller than the box (the chart grows).
|
|
46
|
+
*/
|
|
47
|
+
export function fitCard(ask) {
|
|
48
|
+
const label = ask.id === "" ? "" : `${ask.idLabel} ${ask.id}`.trim();
|
|
49
|
+
const floor = Math.min(ask.min, ask.max);
|
|
50
|
+
// From the biggest size down to the floor in forty steps (and the floor itself last), in whatever unit the box is in.
|
|
51
|
+
const step = Math.max((ask.max - floor) / 40, 1e-4);
|
|
52
|
+
const sizes = [];
|
|
53
|
+
for (let size = ask.max; size > floor + 1e-9; size -= step)
|
|
54
|
+
sizes.push(size);
|
|
55
|
+
sizes.push(floor);
|
|
56
|
+
for (const size of sizes) {
|
|
57
|
+
const lines = ask.name === "" ? [] : wrapText(ask.name, ask.width, size, ask.breakWords);
|
|
58
|
+
if (lines === undefined || lines.length > ask.maxLines)
|
|
59
|
+
continue;
|
|
60
|
+
const idSize = Math.max(size * 0.62, floor * 0.75);
|
|
61
|
+
const idLines = label === "" ? [] : wrapText(label, ask.width, idSize, ask.breakWords, idWidth);
|
|
62
|
+
if (idLines === undefined || (!ask.breakWords && idLines.length > 1))
|
|
63
|
+
continue;
|
|
64
|
+
const height = lines.length * size * LINE_HEIGHT + (idLines.length > 0 ? (lines.length > 0 ? size * GAP : 0) + idLines.length * idSize * LINE_HEIGHT : 0);
|
|
65
|
+
if (height > ask.height + 1e-9)
|
|
66
|
+
continue;
|
|
67
|
+
const width = Math.max(0, ...lines.map((l) => textWidth(l) * size), ...idLines.map((l) => idWidth(l) * idSize));
|
|
68
|
+
return { name: lines, nameSize: size, id: idLines, idSize, width, height };
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What the chart does: the biggest size at which the name takes three lines at most with no word broken, shrinking to the floor; then, at the floor, as many lines as it takes and a word wider than the row broken inside.
|
|
74
|
+
* It always fits, whatever the text, because the row grows.
|
|
75
|
+
*/
|
|
76
|
+
export function fitChart(ask) {
|
|
77
|
+
const whole = fitCard({ ...ask, height: Infinity, maxLines: 3, breakWords: false });
|
|
78
|
+
if (whole !== undefined)
|
|
79
|
+
return whole;
|
|
80
|
+
const fit = fitCard({ ...ask, max: ask.min, height: Infinity, maxLines: Infinity, breakWords: true });
|
|
81
|
+
if (fit === undefined)
|
|
82
|
+
throw new Error("a chart row always fits");
|
|
83
|
+
return fit;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fit the name and ID of every seat of a mat into its card, as one size for all of them, so that the row of cards reads evenly: each seat that has a name is fitted alone, the smallest size any of them needs is taken, and
|
|
87
|
+
* each is fitted again at that size. A seat whose name does not fit at the floor, or that has nobody in it, has no card (undefined). `boxes` has the card box of each seat, in order.
|
|
88
|
+
*/
|
|
89
|
+
export function fitSeats(seats, boxes, limits) {
|
|
90
|
+
const ask = (a, i, max) => (a.assigned ? fitCard({ name: a.name, id: a.id, idLabel: limits.idLabel, width: boxes[i].width, height: boxes[i].height, max, min: limits.min, maxLines: 3, breakWords: false }) : undefined);
|
|
91
|
+
const alone = seats.map((a, i) => ask(a, i, boxes[i].max));
|
|
92
|
+
const sizes = alone.filter((f) => f !== undefined).map((f) => f.nameSize);
|
|
93
|
+
const common = sizes.length === 0 ? Infinity : Math.min(...sizes);
|
|
94
|
+
return seats.map((a, i) => (alone[i] === undefined ? undefined : ask(a, i, Math.min(common, boxes[i].max))));
|
|
95
|
+
}
|
package/dist/judging.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// A judging assignment worked out: the options held to what they may be, each mat's seats with who sits in them, and where the seats are on the plan. The plan is the package's own judo area (`budoPlan`), so that the
|
|
2
|
+
// seats named here are the seats drawn, in metres from the top left of the tatami. Nothing here draws.
|
|
3
|
+
import { budoPlan } from "./budo/plan.js";
|
|
4
|
+
import { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_WORDS, JUDGING_STYLES } from "./judging.data.js";
|
|
5
|
+
import { JUDGING_WORDS } from "./judging.words.js";
|
|
6
|
+
import { fill } from "./strings.js";
|
|
7
|
+
/** The longest name, ID and mat label that are taken, in characters. */
|
|
8
|
+
export const JUDGING_LIMITS = Object.freeze({ name: 120, id: 64, label: 40 });
|
|
9
|
+
/** Text made safe to show: control characters become spaces, runs of spaces one, the ends trimmed. */
|
|
10
|
+
const clean = (text) => text.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
|
|
11
|
+
/** The seats of a style: every one, or without the optional ones (the side judges of a contest). @throws TypeError for a style that is not one. */
|
|
12
|
+
export function seatsOf(style, only = "all") {
|
|
13
|
+
if (typeof style !== "string" || !JUDGING_STYLES.includes(style))
|
|
14
|
+
throw new TypeError(`judging style is ${JUDGING_STYLES.map((s) => `"${s}"`).join(" or ")}`);
|
|
15
|
+
const seats = JUDGING_SEATS[style];
|
|
16
|
+
return only === "all" ? seats : seats.filter((s) => s.optional !== true);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The seat a name stands for: its number (`3`, `"3"`), its key (`"judge-3"`, `"Judge 3"`, `"referee"`, `"timekeeper"`) or a word (`"central"`, `"scorer"`, `"jury"`).
|
|
20
|
+
* @throws RangeError for a seat the style does not have, naming the seats it has.
|
|
21
|
+
*/
|
|
22
|
+
export function seatOf(style, seat) {
|
|
23
|
+
const seats = seatsOf(style);
|
|
24
|
+
const text = typeof seat === "number" ? String(seat) : typeof seat === "string" ? seat.trim().toLowerCase().replace(/\s+/g, "-") : "";
|
|
25
|
+
let found;
|
|
26
|
+
if (/^\d{1,2}$/.test(text))
|
|
27
|
+
found = seats.find((s) => s.n === Number(text));
|
|
28
|
+
else
|
|
29
|
+
found = seats.find((s) => s.id === text) ?? seats.find((s) => JUDGING_SEAT_WORDS[s.id]?.includes(text) === true);
|
|
30
|
+
if (found === undefined)
|
|
31
|
+
throw new RangeError(`The ${style} area has no seat ${JSON.stringify(seat)}: its seats are ${seats.map((s) => s.id).join(", ")} (or their numbers)`);
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
/** The label of a mat: the caller's, held to its length, else the language's `Mat {n}`. */
|
|
35
|
+
export function matLabel(mat, n, language) {
|
|
36
|
+
if (mat.label === undefined)
|
|
37
|
+
return fill(JUDGING_WORDS[language].mat, { n });
|
|
38
|
+
if (typeof mat.label !== "string")
|
|
39
|
+
throw new TypeError("a mat's label is text");
|
|
40
|
+
const label = clean(mat.label);
|
|
41
|
+
if ([...label].length > JUDGING_LIMITS.label)
|
|
42
|
+
throw new RangeError(`A mat's label is at most ${JUDGING_LIMITS.label} characters`);
|
|
43
|
+
return label === "" ? fill(JUDGING_WORDS[language].mat, { n }) : label;
|
|
44
|
+
}
|
|
45
|
+
const oneOf = (value, allowed, fallback, label) => {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return fallback;
|
|
48
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
49
|
+
throw new TypeError(`${label} is ${allowed.map((a) => `"${a}"`).join(" or ")}`);
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Hold the options to what they may be and put each official in a seat.
|
|
54
|
+
* @throws TypeError for an option of the wrong kind, RangeError for a number of mats outside 1 to 6, a mat that does not exist, a seat the style does not have, a seat named twice on one mat or a text that is too long.
|
|
55
|
+
*/
|
|
56
|
+
export function judgingSetting(options) {
|
|
57
|
+
const style = options.style;
|
|
58
|
+
const seats = seatsOf(style);
|
|
59
|
+
if (!Array.isArray(options.mats) || options.mats.length < 1 || options.mats.length > JUDGING_MATS_MAX)
|
|
60
|
+
throw new RangeError(`An assignment has 1 to ${JUDGING_MATS_MAX} mats`);
|
|
61
|
+
const language = options.language === "ja" ? "ja" : "en";
|
|
62
|
+
const given = options.mats.map((mat, i) => {
|
|
63
|
+
if (typeof mat !== "object" || mat === null)
|
|
64
|
+
throw new TypeError("a mat is { label, officials }");
|
|
65
|
+
const byId = new Map();
|
|
66
|
+
for (const official of mat.officials ?? []) {
|
|
67
|
+
if (typeof official !== "object" || official === null)
|
|
68
|
+
throw new TypeError("an official is { seat, name, id }");
|
|
69
|
+
const seat = seatOf(style, official.seat);
|
|
70
|
+
if (byId.has(seat.id))
|
|
71
|
+
throw new RangeError(`Mat ${i + 1} names seat ${seat.id} twice`);
|
|
72
|
+
if (official.name !== undefined && typeof official.name !== "string")
|
|
73
|
+
throw new TypeError("an official's name is text");
|
|
74
|
+
if (official.id !== undefined && typeof official.id !== "string" && typeof official.id !== "number")
|
|
75
|
+
throw new TypeError("an official's id is text or a number");
|
|
76
|
+
const name = clean(official.name ?? "");
|
|
77
|
+
const id = clean(official.id === undefined ? "" : String(official.id));
|
|
78
|
+
if ([...name].length > JUDGING_LIMITS.name)
|
|
79
|
+
throw new RangeError(`A name is at most ${JUDGING_LIMITS.name} characters`);
|
|
80
|
+
if ([...id].length > JUDGING_LIMITS.id)
|
|
81
|
+
throw new RangeError(`An ID is at most ${JUDGING_LIMITS.id} characters`);
|
|
82
|
+
byId.set(seat.id, { seat, name, id, assigned: name !== "" || id !== "" });
|
|
83
|
+
}
|
|
84
|
+
return byId;
|
|
85
|
+
});
|
|
86
|
+
if (options.sideJudges !== undefined && typeof options.sideJudges !== "boolean")
|
|
87
|
+
throw new TypeError("sideJudges is true or false");
|
|
88
|
+
// The seats that are drawn: every one that is not optional, and an optional one when asked for or when a mat names it.
|
|
89
|
+
const shown = seats.filter((seat) => seat.optional !== true || options.sideJudges === true || given.some((m) => m.has(seat.id)));
|
|
90
|
+
const all = options.mats.map((mat, i) => ({ n: i + 1, label: matLabel(mat, i + 1, language), seats: shown.map((seat) => given[i].get(seat.id) ?? { seat, name: "", id: "", assigned: false }) }));
|
|
91
|
+
let mats = all;
|
|
92
|
+
if (options.mat !== undefined) {
|
|
93
|
+
if (!Number.isInteger(options.mat) || options.mat < 1 || options.mat > all.length)
|
|
94
|
+
throw new RangeError(`mat is a whole number from 1 to ${all.length}`);
|
|
95
|
+
mats = [all[options.mat - 1]];
|
|
96
|
+
}
|
|
97
|
+
const width = options.width ?? 1000;
|
|
98
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width < 480 || width > 2000)
|
|
99
|
+
throw new RangeError("width is a number from 480 to 2000");
|
|
100
|
+
return {
|
|
101
|
+
style,
|
|
102
|
+
language,
|
|
103
|
+
mats,
|
|
104
|
+
total: all.length,
|
|
105
|
+
chart: oneOf(options.chart, ["auto", "always", "never"], "auto", "chart"),
|
|
106
|
+
orientation: oneOf(options.orientation, ["landscape", "portrait"], "landscape", "orientation"),
|
|
107
|
+
focus: oneOf(options.focus, ["mat", "bench"], "mat", "focus"),
|
|
108
|
+
width,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const plans = new Map();
|
|
112
|
+
/** A box of a given centre and size. */
|
|
113
|
+
const around = (cx, cy, w, h) => ({ x0: cx - w / 2, y0: cy - h / 2, x1: cx + w / 2, y1: cy + h / 2 });
|
|
114
|
+
const union = (boxes) => ({ x0: Math.min(...boxes.map((b) => b.x0)), y0: Math.min(...boxes.map((b) => b.y0)), x1: Math.max(...boxes.map((b) => b.x1)), y1: Math.max(...boxes.map((b) => b.y1)) });
|
|
115
|
+
/** A card for a seat that is not at a table (the referee on the mat, a side judge at a corner): 2.6 m by 1.8 m, since there is room round them (ours). */
|
|
116
|
+
const FREE_CARD = { w: 2.6, h: 1.8 };
|
|
117
|
+
/**
|
|
118
|
+
* The judges' bench close up: the seats and their table with a little round them and a few rows of mats in front, from the seats of one mat or of several side by side. `matTop` is where the mats start (the top of the first
|
|
119
|
+
* contest area), so that the picture shows how the bench sits at the mat's edge; `front` instead says how far in front of the table the picture reaches, in metres (the 3D view, which turns, wants as little of the floor as
|
|
120
|
+
* will do).
|
|
121
|
+
*/
|
|
122
|
+
export function benchOf(chairs, table, pitch, matTop, front) {
|
|
123
|
+
const side = (pitch - 0.5) / 2 + 0.15;
|
|
124
|
+
return { x0: Math.min(...chairs.map((c) => c.x0)) - side, y0: chairs[0].y0 - 0.9, x1: Math.max(...chairs.map((c) => c.x1)) + side, y1: front === undefined ? Math.max(table.y1 + 1.8, matTop + 1.2) : table.y1 + front };
|
|
125
|
+
}
|
|
126
|
+
/** A kata area's seats: five chairs behind one table, each with the card that lies over its chair and the table in front of it. */
|
|
127
|
+
function kataPlaces(chairs, table, pitch) {
|
|
128
|
+
return chairs.map((chair) => {
|
|
129
|
+
const cx = (chair.x0 + chair.x1) / 2;
|
|
130
|
+
return { chair, card: { x0: cx - (pitch / 2 - 0.03), y0: chair.y0 - 0.05, x1: cx + (pitch / 2 - 0.03), y1: table.y1 } };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A contest's seats round one contest area (ours, but for the referee's: 2 m back from the line the athletes start on, at the centre, facing the table): the timekeeper, the scorekeeper and the referee supervisor in chairs
|
|
135
|
+
* behind the technical table, 1 m apart, and the side judges at two opposite corners of the contest area, 2 m out from them.
|
|
136
|
+
*/
|
|
137
|
+
function shiaiPlaces(contest, desk) {
|
|
138
|
+
const cx = (contest.x0 + contest.x1) / 2;
|
|
139
|
+
const cy = (contest.y0 + contest.y1) / 2;
|
|
140
|
+
const seat = (x, y, w, h) => ({ chair: around(x, y, 0.5, 0.5), card: around(x, y, w, h) });
|
|
141
|
+
const chairY = desk.y0 - 0.35;
|
|
142
|
+
const tableSeat = (x) => ({ chair: around(x, chairY, 0.5, 0.5), card: { x0: x - 0.47, y0: chairY - 0.3, x1: x + 0.47, y1: desk.y1 } });
|
|
143
|
+
return [
|
|
144
|
+
seat(cx, cy + 2, FREE_CARD.w, FREE_CARD.h),
|
|
145
|
+
tableSeat(cx - 1),
|
|
146
|
+
tableSeat(cx),
|
|
147
|
+
tableSeat(cx + 1),
|
|
148
|
+
seat(contest.x0 - 2, contest.y0 - 2, FREE_CARD.w, FREE_CARD.h),
|
|
149
|
+
seat(contest.x1 + 2, contest.y1 + 2, FREE_CARD.w, FREE_CARD.h),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
/** The plan of `count` mats side by side of a style, with its seats: the package's own judo area, so that the seats named are the seats drawn. */
|
|
153
|
+
export function judgingPlan(style, count = 1) {
|
|
154
|
+
const key = `${style}:${count}`;
|
|
155
|
+
const hit = plans.get(key);
|
|
156
|
+
if (hit !== undefined)
|
|
157
|
+
return hit;
|
|
158
|
+
seatsOf(style);
|
|
159
|
+
const plan = budoPlan({ sport: "judo", league: JUDGING_LEAGUE[style].league, budo: { style, officials: true, areas: count } });
|
|
160
|
+
const judo = plan.judo;
|
|
161
|
+
const mats = [];
|
|
162
|
+
for (let mi = 0; mi < count; mi += 1) {
|
|
163
|
+
const contest = judo.contests[mi];
|
|
164
|
+
if (style === "kata") {
|
|
165
|
+
const per = JUDGING_SEATS.kata.length;
|
|
166
|
+
const chairs = judo.judges.seats.slice(mi * per, (mi + 1) * per);
|
|
167
|
+
const table = judo.judges.tables[mi];
|
|
168
|
+
if (chairs.length !== per || table === undefined)
|
|
169
|
+
throw new Error("The kata plan has no judges' seats");
|
|
170
|
+
const pitch = (chairs[per - 1].x0 - chairs[0].x0) / (per - 1);
|
|
171
|
+
mats.push({ places: kataPlaces(chairs, table, pitch), table, bench: benchOf(chairs, table, pitch, contest.y0) });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const desk = judo.desks[mi];
|
|
175
|
+
if (desk === undefined)
|
|
176
|
+
throw new Error("The contest plan has no technical table");
|
|
177
|
+
const places = shiaiPlaces(contest, desk);
|
|
178
|
+
const atTable = places.slice(1, 4);
|
|
179
|
+
// The bench of a contest is its technical table with the three officials at it; the referee and the side judges are on the mat and at its corners, and are not in the picture of the bench.
|
|
180
|
+
const bench = { x0: Math.min(...atTable.map((p) => p.card.x0)) - 0.4, y0: atTable[0].chair.y0 - 0.9, x1: Math.max(...atTable.map((p) => p.card.x1)) + 0.4, y1: desk.y1 + 1.8 };
|
|
181
|
+
mats.push({ places, table: desk, bench });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const first = mats[0];
|
|
185
|
+
const chairs = first.places.map((p) => p.chair);
|
|
186
|
+
const pitch = style === "kata" ? (first.places[first.places.length - 1].chair.x0 - first.places[0].chair.x0) / (first.places.length - 1) : 1;
|
|
187
|
+
const extent = union([plan.extent, ...first.places.map((p) => p.card), ...first.places.map((p) => p.chair)]);
|
|
188
|
+
const built = { plan, mats, places: first.places, chairs, table: first.table, bench: first.bench, pitch, extent };
|
|
189
|
+
plans.set(key, built);
|
|
190
|
+
return built;
|
|
191
|
+
}
|
|
192
|
+
/** Whether a seat is in a box: its card's middle is inside it. A seat that is not is not drawn in a close-up of the bench, and its name is in the chart only. */
|
|
193
|
+
export function seatIn(place, box) {
|
|
194
|
+
const x = (place.card.x0 + place.card.x1) / 2;
|
|
195
|
+
const y = (place.card.y0 + place.card.y1) / 2;
|
|
196
|
+
return x >= box.x0 && x <= box.x1 && y >= box.y0 && y <= box.y1;
|
|
197
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The colours of a judging picture in light and in dark, as custom properties (the 3D view's stylesheet carries them too, for its name cards). */
|
|
2
|
+
export declare const JUDGING_LIGHT = "--ky-ju-bg:#f7f5ef;--ky-ju-ink:#10151b;--ky-ju-muted:#434b54;--ky-ju-row:#ffffff;--ky-ju-rule:#b9b3a3;--ky-ju-badge:#10151b;--ky-ju-badge-ink:#ffffff;--ky-ju-card:#ffffff;--ky-ju-card-ink:#10151b;--ky-ju-card-edge:#10151b;--ky-ju-vacant:#6a717a;--ky-ju-desk:#e9e2d0;--ky-ju-desk-edge:#4a4f56";
|
|
3
|
+
export declare const JUDGING_DARK = "--ky-ju-bg:#0d1218;--ky-ju-ink:#f6f3ea;--ky-ju-muted:#bcc3cb;--ky-ju-row:#18212a;--ky-ju-rule:#475463;--ky-ju-badge:#f6f3ea;--ky-ju-badge-ink:#0d1218;--ky-ju-card:#18212a;--ky-ju-card-ink:#f6f3ea;--ky-ju-card-edge:#f6f3ea;--ky-ju-vacant:#98a1ab;--ky-ju-desk:#6f6859;--ky-ju-desk-edge:#05070a";
|
|
4
|
+
/**
|
|
5
|
+
* The rules of a judging assignment. `:where` keeps the specificity at nothing so that a page's own custom properties win; the picture follows the device and the page like a drawing does, and a `data-theme` of light or
|
|
6
|
+
* dark forces it. The colours are custom properties (`--ky-ju-*`) a page can set.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KYOGIJO_JUDGING_STYLE: string;
|