@johnmorrisdotca/kyogijo-stadiums 0.17.0 → 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 CHANGED
@@ -7,6 +7,17 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.18.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **Shiai judging assignments** (`style: "shiai"` of `@johnmorrisdotca/kyogijo-stadiums/judging`). A contest area's officials by seat, with a name and an ID, drawn as the kata area's are (a card in the seat where the name fits, the chart of seat, role, name and ID that never cuts a name, 2D and 3D, light and dark, English and Japanese, `mat`, `focus`, `chart`, `orientation`, `judgingTable`). From the IJF Sport and Organisation Rules of 21 January 2026: **the referee**, on the tatami at its centre 2 m back from the line the athletes start on, facing the technical table (Article 9; the Refereeing Rules' Article 2); **the timekeeper and the scorekeeper**, the two technical officials a tatami should have for timing and scoring (the Refereeing Rules' opening article); **the referee supervisor**, on the technical table with the CARE replay system (the same article); and **two side judges**, for an event with no CARE system (Article 13, 5), drawn only with the new `sideJudges: true` or when a mat names them. Which chair at the table is whose, the side judges' places (two opposite corners of the contest area, 2 m out: the Rules give none), the chairs, the table and the cards are ours. In the close-up of the bench (`focus: "bench"`) only the table's seats are drawn and the referee's and side judges' names are in the chart; in 3D the referee's and a side judge's cards stand on posts. Seat words: `referee`, `timekeeper`, `scorekeeper`, `supervisor`, `side-judge-1`, `side-judge-2` (or their numbers 1 to 6, or `judge-1`, `scorer`, `jury`...). The demo, [judging assignments](https://johnmorrisdotca.github.io/kyogijo-stadiums/judging.html), has a Style choice (`style=shiai`), the side judges' checkbox and a row in its editor for each seat; the docs have [a section](./docs/judging.md#the-seats-of-a-contest) and the gallery [a contest](https://johnmorrisdotca.github.io/kyogijo-stadiums/docs/gallery-judging.html#shiai).
15
+
16
+ ### Changed
17
+
18
+ - `JudgingStyle` is `"kata" | "shiai"`; `JudgingSeat` has `where` (`table`, `mat` or `corner`) and `optional`, and its `role` the contest's roles; `seatsOf(style, "listed")` leaves out the optional seats; `judgingPlan(style, count)` gives each mat's `places` (a chair and a card for each seat, in metres) and its bench; `fitSeats` takes a box for each seat. A kata assignment draws byte for byte what it did.
19
+ - `JudoLayout` (the judo plan) also gives `desks` (the technical tables) and `free` (the free zone), as data only: no drawing changes.
20
+
10
21
  ## [0.17.0] - 2026-10-20
11
22
 
12
23
  ### Added
@@ -43,6 +43,10 @@ export interface JudoLayout {
43
43
  seats: MBox[];
44
44
  coaches: MBox[];
45
45
  };
46
+ /** A contest area's technical tables (ours: 3 m by 0.8 m), one for each area of the top row; empty for a kata area. */
47
+ desks: MBox[];
48
+ /** The free zone round the whole competition area. */
49
+ free: MBox;
46
50
  }
47
51
  /** The mats of a `tatami` option, or the league's own when none. @throws Error for a name that is not a size of mat. */
48
52
  export declare function matOf(asked: BudoStyle["tatami"], fig: JudoFigures): MatSize;
package/dist/budo/judo.js CHANGED
@@ -139,7 +139,7 @@ export function judoParts(o) {
139
139
  if (judges.coaches.length > 0)
140
140
  marks.push({ id: "coach-chair", x: (judges.coaches[0].x0 + judges.coaches[0].x1) / 2, y: (judges.coaches[0].y0 + judges.coaches[0].y1) / 2, side: "bottom" });
141
141
  const extent = union([free, ...desks.map((d) => grow(d, 0.3)), ...[...judges.tables, ...judges.seats, ...judges.coaches].map((d) => grow(d, 0.3))]);
142
- return { parts, tatami, extent, contests, gap, pieces, mat, marks, danger: band, judges };
142
+ return { parts, tatami, extent, contests, gap, pieces, mat, marks, danger: band, judges, desks, free };
143
143
  }
144
144
  /** The number of mats of the competition area: a half mat counts a half. */
145
145
  export const mats = (layout, mat) => Math.round(matCount(layout.pieces, mat) * 100) / 100;
@@ -1,12 +1,12 @@
1
1
  export { drawJudging, judgingChart, judgingDrawing, judgingTable, judgingText } from "./judging.draw.ts";
2
2
  export { JUDGING_HEIGHTS, JUDGING_PRESETS, buildJudgingTurn, judgingTurnMarkup, mountJudgingTurn } from "./judging.turn.ts";
3
- export { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_COUNT, JUDGING_STYLES } from "./judging.data.ts";
4
- export { JUDGING_LIMITS, judgingPlan, judgingSetting, matLabel, seatOf, seatsOf } from "./judging.ts";
3
+ export { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_COUNT, JUDGING_SEAT_WORDS, JUDGING_STYLES } from "./judging.data.ts";
4
+ export { JUDGING_LIMITS, benchOf, judgingPlan, judgingSetting, matLabel, seatIn, seatOf, seatsOf } from "./judging.ts";
5
5
  export { fitCard, fitChart, fitSeats, wrapText } from "./judging.fit.ts";
6
6
  export { KYOGIJO_JUDGING_STYLE } from "./judging.style.ts";
7
7
  export { JUDGING_WORDS } from "./judging.words.ts";
8
8
  export type { JudgingWords } from "./judging.words.ts";
9
9
  export type { JudgingAssignment, JudgingMat, JudgingOfficial, JudgingOptions, JudgingResolved, JudgingSeat, JudgingStyle } from "./judging.types.ts";
10
10
  export type { JudgingDrawing } from "./judging.draw.ts";
11
- export type { JudgingPlan, JudgingSetting } from "./judging.ts";
11
+ export type { JudgingPlan, JudgingSetting, MatPlaces, SeatPlace } from "./judging.ts";
12
12
  export type { CardAsk, CardFit } from "./judging.fit.ts";
@@ -1,8 +1,8 @@
1
1
  // A judging assignment: the officials of each mat area of a judo tournament, by seat, with their names and IDs, in the seat where they fit and in a chart where they do not; in 2D and in 3D.
2
2
  export { drawJudging, judgingChart, judgingDrawing, judgingTable, judgingText } from "./judging.draw.js";
3
3
  export { JUDGING_HEIGHTS, JUDGING_PRESETS, buildJudgingTurn, judgingTurnMarkup, mountJudgingTurn } from "./judging.turn.js";
4
- export { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_COUNT, JUDGING_STYLES } from "./judging.data.js";
5
- export { JUDGING_LIMITS, judgingPlan, judgingSetting, matLabel, seatOf, seatsOf } from "./judging.js";
4
+ export { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_COUNT, JUDGING_SEAT_WORDS, JUDGING_STYLES } from "./judging.data.js";
5
+ export { JUDGING_LIMITS, benchOf, judgingPlan, judgingSetting, matLabel, seatIn, seatOf, seatsOf } from "./judging.js";
6
6
  export { fitCard, fitChart, fitSeats, wrapText } from "./judging.fit.js";
7
7
  export { KYOGIJO_JUDGING_STYLE } from "./judging.style.js";
8
8
  export { JUDGING_WORDS } from "./judging.words.js";
package/dist/judging.d.ts CHANGED
@@ -7,10 +7,10 @@ export declare const JUDGING_LIMITS: Readonly<{
7
7
  id: 64;
8
8
  label: 40;
9
9
  }>;
10
- /** The seats of a style. @throws TypeError for a style that is not one. */
11
- export declare function seatsOf(style: unknown): readonly JudgingSeat[];
10
+ /** 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. */
11
+ export declare function seatsOf(style: unknown, only?: "all" | "listed"): readonly JudgingSeat[];
12
12
  /**
13
- * The seat a name stands for: its number (`3`, `"3"`), its key (`"judge-3"`, `"Judge 3"`) or `"central"`.
13
+ * The seat a name stands for: its number (`3`, `"3"`), its key (`"judge-3"`, `"Judge 3"`, `"referee"`, `"timekeeper"`) or a word (`"central"`, `"scorer"`, `"jury"`).
14
14
  * @throws RangeError for a seat the style does not have, naming the seats it has.
15
15
  */
16
16
  export declare function seatOf(style: JudgingStyle, seat: unknown): JudgingSeat;
@@ -34,22 +34,41 @@ export interface JudgingSetting {
34
34
  * @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.
35
35
  */
36
36
  export declare function judgingSetting(options: JudgingOptions): JudgingSetting;
37
- /** Where the seats are on one mat's plan, in metres from the top left of the tatami, and the plan they are on. */
37
+ /** Where one seat is on a mat's plan, in metres from the top left of the tatami: its chair (or the place where the official stands) and the card that carries the name, which may lie over the table in front of it. */
38
+ export interface SeatPlace {
39
+ chair: MBox;
40
+ card: MBox;
41
+ }
42
+ /** One mat's seats, in the order the style lists them (`JUDGING_SEATS`, optional seats included), its table and its bench. */
43
+ export interface MatPlaces {
44
+ places: SeatPlace[];
45
+ /** The judges' table or the technical table behind the seats at it. */
46
+ table: MBox;
47
+ /** The seats at the table close up: the picture of `focus: "bench"`. */
48
+ bench: MBox;
49
+ }
50
+ /** Where the seats are on the plan of a style, and the plan they are on. */
38
51
  export interface JudgingPlan {
39
52
  plan: BudoPlan;
40
- /** The chair of each seat, in seat order, and the judges' table behind them. */
53
+ /** Each mat's seats, for the mats side by side the plan has (`count`). */
54
+ mats: MatPlaces[];
55
+ /** The first mat's seats, its table and its bench, as `mats[0]` has them (the chairs of the seats, in order). */
56
+ places: SeatPlace[];
41
57
  chairs: MBox[];
42
58
  table: MBox;
43
- /** The distance between two seats. */
59
+ bench: MBox;
60
+ /** The distance between two seats at the table. */
44
61
  pitch: number;
45
- /** The whole picture of a mat (the free zone, the table, the seats, the coach's chair) and the judges' bench close up. */
62
+ /** The whole picture of a mat (the free zone, the table, the seats, their cards): what `focus: "mat"` shows, for the first mat. */
46
63
  extent: MBox;
47
- bench: MBox;
48
64
  }
49
65
  /**
50
- * The judges' bench close up: the seats and the 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
51
- * 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 will do).
66
+ * 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
67
+ * 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
68
+ * will do).
52
69
  */
53
70
  export declare function benchOf(chairs: readonly MBox[], table: MBox, pitch: number, matTop: number, front?: number): MBox;
54
- /** The plan of one mat of a style, with its seats: the package's own judo area, so that the seats named are the seats drawn. */
55
- export declare function judgingPlan(style: JudgingStyle): JudgingPlan;
71
+ /** 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. */
72
+ export declare function judgingPlan(style: JudgingStyle, count?: number): JudgingPlan;
73
+ /** 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. */
74
+ export declare function seatIn(place: SeatPlace, box: MBox): boolean;
@@ -1,8 +1,10 @@
1
1
  import type { JudgingSeat, JudgingStyle } from "./judging.types.ts";
2
- /** How many officials a style seats. */
2
+ /** How many officials a style seats at the most (a contest's two side judges included). */
3
3
  export declare const JUDGING_SEAT_COUNT: Readonly<Record<JudgingStyle, number>>;
4
- /** The seats of every style, in the order they are drawn. */
4
+ /** The seats of every style, in the order they are listed. */
5
5
  export declare const JUDGING_SEATS: Readonly<Record<JudgingStyle, readonly JudgingSeat[]>>;
6
+ /** The other words a seat is asked for by, besides its key and its number. */
7
+ export declare const JUDGING_SEAT_WORDS: Readonly<Record<string, readonly string[]>>;
6
8
  /** The styles that can be asked for. */
7
9
  export declare const JUDGING_STYLES: readonly JudgingStyle[];
8
10
  /** The league each style is drawn in (`drawKyogijo`'s `league`), and the sources its seats are read from (keys of `LEAGUE_SOURCES`). */
@@ -1,24 +1,53 @@
1
- // The seats of each judging style, as the rulebook has them. A kata area is judged by five judges on the shomen side, 1.25 m apart, the central judge at the centre of the area, and no referee (IJF Sport and
2
- // Organisation Rules of 21 January 2026, B1.8 and the judges' article; the Kata Commission's Tournament Handbook 2024). The rules do not number the judges: the numbers here are ours, from the left as the plan lies
3
- // with the shomen at the top (so the central judge is 3), and are said so in the docs. The positions are the plan's own (`budoPlan`), so that what is drawn and what is named cannot differ.
1
+ // The seats of each judging style, as the rulebook has them.
2
+ // A kata area is judged by five judges on the shomen side, 1.25 m apart, the central judge at the centre of the area, and no referee (IJF Sport and Organisation Rules of 21 January 2026, B1.8 and the judges' article; the
3
+ // Kata Commission's Tournament Handbook 2024). The rules do not number the judges: the numbers here are ours, from the left as the plan lies with the shomen at the top (so the central judge is 3).
4
+ // A contest (shiai) has one referee, on the tatami at its centre 2 m back from the line the athletes start on, facing the technical table (SOR, Refereeing Rules, Article 9 and Article 2); two technical officials a
5
+ // tatami for timing and scoring (the Refereeing Rules' opening article); the IJF Referee Supervisor(s) on the technical table with the CARE replay system, and the referees not officiating seated at it (Article 3);
6
+ // and, for an event with no CARE system, two side judges with a majority of three (Article 13, 5). The Rules give no place for a side judge: the corners of the contest area are ours. The positions are the plan's own
7
+ // (`budoPlan`) and the figures round them in `judging.ts`, so that what is drawn and what is named cannot differ.
4
8
  import { IJF_KATA } from "./leagues/budo.js";
5
- /** How many officials a style seats. */
6
- export const JUDGING_SEAT_COUNT = Object.freeze({ kata: IJF_KATA.panel?.count ?? 5 });
9
+ /** How many officials a style seats at the most (a contest's two side judges included). */
10
+ export const JUDGING_SEAT_COUNT = Object.freeze({ kata: IJF_KATA.panel?.count ?? 5, shiai: 6 });
7
11
  /** The seats of a kata area: five judges, the middle one the central judge. */
8
12
  function kata() {
9
- const count = JUDGING_SEAT_COUNT.kata;
13
+ const count = IJF_KATA.panel?.count ?? 5;
10
14
  const middle = Math.ceil(count / 2);
11
15
  return Array.from({ length: count }, (_, i) => {
12
16
  const n = i + 1;
13
17
  const central = n === middle;
14
- return { id: `judge-${n}`, n, role: central ? "central" : "judge", label: { en: central ? `Judge ${n}, central` : `Judge ${n}`, ja: central ? `審判員${n}(中央)` : `審判員${n}` } };
18
+ return { id: `judge-${n}`, n, role: central ? "central" : "judge", where: "table", label: { en: central ? `Judge ${n}, central` : `Judge ${n}`, ja: central ? `審判員${n}(中央)` : `審判員${n}` } };
15
19
  });
16
20
  }
17
- /** The seats of every style, in the order they are drawn. */
18
- export const JUDGING_SEATS = Object.freeze({ kata: Object.freeze(kata()) });
21
+ /** The seats of a contest: the referee, then the technical table's three, then the side judges (drawn only when asked for or named). */
22
+ function shiai() {
23
+ const seat = (n, id, role, where, en, ja, optional = false) => ({ id, n, role, where, label: { en, ja }, ...(optional ? { optional: true } : {}) });
24
+ return [
25
+ seat(1, "referee", "referee", "mat", "Referee", "主審"),
26
+ seat(2, "timekeeper", "timekeeper", "table", "Timekeeper", "計時係"),
27
+ seat(3, "scorekeeper", "scorekeeper", "table", "Scorekeeper", "記録係"),
28
+ seat(4, "supervisor", "supervisor", "table", "Referee supervisor", "審判委員"),
29
+ seat(5, "side-judge-1", "side-judge", "corner", "Side judge 1", "副審1", true),
30
+ seat(6, "side-judge-2", "side-judge", "corner", "Side judge 2", "副審2", true),
31
+ ];
32
+ }
33
+ /** The seats of every style, in the order they are listed. */
34
+ export const JUDGING_SEATS = Object.freeze({ kata: Object.freeze(kata()), shiai: Object.freeze(shiai()) });
35
+ /** The other words a seat is asked for by, besides its key and its number. */
36
+ export const JUDGING_SEAT_WORDS = Object.freeze({
37
+ "judge-3": ["central", "centre", "center"],
38
+ referee: ["main", "main-referee", "central-referee", "shimpan"],
39
+ timekeeper: ["time", "timer", "timing"],
40
+ scorekeeper: ["score", "scorer", "scoring", "recorder"],
41
+ supervisor: ["referee-supervisor", "jury", "care"],
42
+ "side-judge-1": ["judge-1", "side-1"],
43
+ "side-judge-2": ["judge-2", "side-2"],
44
+ });
19
45
  /** The styles that can be asked for. */
20
- export const JUDGING_STYLES = Object.freeze(["kata"]);
46
+ export const JUDGING_STYLES = Object.freeze(["kata", "shiai"]);
21
47
  /** The league each style is drawn in (`drawKyogijo`'s `league`), and the sources its seats are read from (keys of `LEAGUE_SOURCES`). */
22
- export const JUDGING_LEAGUE = Object.freeze({ kata: { league: "ijf-kata", sources: ["ijf-sor", "ijf-kata-handbook", "ijf-kata-criteria"] } });
48
+ export const JUDGING_LEAGUE = Object.freeze({
49
+ kata: { league: "ijf-kata", sources: ["ijf-sor", "ijf-kata-handbook", "ijf-kata-criteria"] },
50
+ shiai: { league: "ijf", sources: ["ijf-sor"] },
51
+ });
23
52
  /** The most mat areas an assignment names: the plan's own limit for areas side by side. */
24
53
  export const JUDGING_MATS_MAX = 6;
@@ -7,7 +7,7 @@ import { el, esc, hash, num } from "./format.js";
7
7
  import { PLAN_CARD, badge, card } from "./judging.card.js";
8
8
  import { fitChart, fitSeats } from "./judging.fit.js";
9
9
  import { JUDGING_LEAGUE } from "./judging.data.js";
10
- import { judgingPlan, judgingSetting } from "./judging.js";
10
+ import { judgingPlan, judgingSetting, seatIn } from "./judging.js";
11
11
  import { KYOGIJO_JUDGING_STYLE } from "./judging.style.js";
12
12
  import { JUDGING_WORDS } from "./judging.words.js";
13
13
  import { fill } from "./strings.js";
@@ -117,26 +117,31 @@ function planMarkup(mat, surface, geom, region, box, m, setting, clip) {
117
117
  markup += el("rect", { class: "ky-apron", x, y, width: w, height: h });
118
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
119
  markup += el("rect", { class: "ky-ju-plan-frame", x, y, width: w, height: h });
120
- // The card of a seat is its chair and the table in front of it: a seat wide and as deep as both.
121
- const cardW = (geom.pitch - 0.06) * s;
122
- const cardH = (geom.table.y1 - (geom.chairs[0].y0 - 0.05)) * s;
123
- const pad = Math.max(2 * m.k, cardW * 0.05);
124
- const r = clamp(cardW * 0.2, 6 * m.k, 14 * m.k);
125
- const innerW = cardW - 2 * pad;
126
- const innerH = cardH - 2 * pad - r;
127
- const fitted = fitSeats(mat.seats, { width: innerW, height: innerH, max: Math.min(cardW * 0.3, 26 * m.k), min: m.cardMin, idLabel: words.chart.idLabel });
128
- const fits = mat.seats.map((a, i) => !a.assigned || fitted[i] !== undefined);
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));
129
133
  mat.seats.forEach((a, i) => {
130
- const chair = geom.chairs[i];
131
- const cx = X((chair.x0 + chair.x1) / 2);
132
- const top = Y(chair.y0 - 0.05);
134
+ if (!shown[i])
135
+ return;
136
+ const place = geom.places[a.seat.n - 1];
137
+ const b = boxes[i];
133
138
  const fit = fitted[i];
134
139
  let g = "";
135
140
  if (fit !== undefined)
136
- g += card(PLAN_CARD, fit, a.seat.n, cx - cardW / 2, top, cardW, cardH, pad, r, 3 * m.k);
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);
137
142
  else {
138
- const br = clamp(geom.pitch * s * 0.3, 7 * m.k, 16 * m.k);
139
- g += badge(PLAN_CARD, cx, Y((chair.y0 + chair.y1) / 2), br, a.seat.n, a.assigned);
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);
140
145
  }
141
146
  markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
142
147
  });
@@ -38,14 +38,17 @@ export declare function fitCard(ask: CardAsk): CardFit | undefined;
38
38
  * It always fits, whatever the text, because the row grows.
39
39
  */
40
40
  export declare function fitChart(ask: Omit<CardAsk, "maxLines" | "breakWords" | "height">): CardFit;
41
- /**
42
- * Fit the name and ID of every seat of a mat into a card of one size, 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
43
- * taken, and 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).
44
- */
45
- export declare function fitSeats(seats: readonly JudgingAssignment[], box: {
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 {
46
43
  width: number;
47
44
  height: number;
48
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: {
49
52
  min: number;
50
53
  idLabel: string;
51
54
  }): (CardFit | undefined)[];
@@ -83,13 +83,13 @@ export function fitChart(ask) {
83
83
  return fit;
84
84
  }
85
85
  /**
86
- * Fit the name and ID of every seat of a mat into a card of one size, 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
87
- * taken, and 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).
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
88
  */
89
- export function fitSeats(seats, box) {
90
- const ask = (a, max) => (a.assigned ? fitCard({ name: a.name, id: a.id, idLabel: box.idLabel, width: box.width, height: box.height, max, min: box.min, maxLines: 3, breakWords: false }) : undefined);
91
- const alone = seats.map((a) => ask(a, box.max));
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
92
  const sizes = alone.filter((f) => f !== undefined).map((f) => f.nameSize);
93
- const common = sizes.length === 0 ? box.max : Math.min(...sizes);
94
- return seats.map((a, i) => (alone[i] === undefined ? undefined : ask(a, common)));
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
95
  }
package/dist/judging.js CHANGED
@@ -1,36 +1,34 @@
1
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
2
  // seats named here are the seats drawn, in metres from the top left of the tatami. Nothing here draws.
3
3
  import { budoPlan } from "./budo/plan.js";
4
- import { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_STYLES } from "./judging.data.js";
4
+ import { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_WORDS, JUDGING_STYLES } from "./judging.data.js";
5
5
  import { JUDGING_WORDS } from "./judging.words.js";
6
6
  import { fill } from "./strings.js";
7
7
  /** The longest name, ID and mat label that are taken, in characters. */
8
8
  export const JUDGING_LIMITS = Object.freeze({ name: 120, id: 64, label: 40 });
9
9
  /** Text made safe to show: control characters become spaces, runs of spaces one, the ends trimmed. */
10
10
  const clean = (text) => text.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
11
- /** The seats of a style. @throws TypeError for a style that is not one. */
12
- export function seatsOf(style) {
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
13
  if (typeof style !== "string" || !JUDGING_STYLES.includes(style))
14
14
  throw new TypeError(`judging style is ${JUDGING_STYLES.map((s) => `"${s}"`).join(" or ")}`);
15
- return JUDGING_SEATS[style];
15
+ const seats = JUDGING_SEATS[style];
16
+ return only === "all" ? seats : seats.filter((s) => s.optional !== true);
16
17
  }
17
18
  /**
18
- * The seat a name stands for: its number (`3`, `"3"`), its key (`"judge-3"`, `"Judge 3"`) or `"central"`.
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"`).
19
20
  * @throws RangeError for a seat the style does not have, naming the seats it has.
20
21
  */
21
22
  export function seatOf(style, seat) {
22
23
  const seats = seatsOf(style);
23
- const text = typeof seat === "number" ? String(seat) : typeof seat === "string" ? seat.trim().toLowerCase() : "";
24
+ const text = typeof seat === "number" ? String(seat) : typeof seat === "string" ? seat.trim().toLowerCase().replace(/\s+/g, "-") : "";
24
25
  let found;
25
- if (text === "central" || text === "centre" || text === "center")
26
- found = seats.find((s) => s.role === "central");
27
- else {
28
- const m = /^(?:judge)?[-\s]?(\d{1,2})$/.exec(text);
29
- if (m !== null)
30
- found = seats.find((s) => s.n === Number(m[1]));
31
- }
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);
32
30
  if (found === undefined)
33
- throw new RangeError(`The ${style} area has no seat ${JSON.stringify(seat)}: its seats are ${seats.map((s) => s.id).join(", ")} (or their numbers, or "central")`);
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)`);
34
32
  return found;
35
33
  }
36
34
  /** The label of a mat: the caller's, held to its length, else the language's `Mat {n}`. */
@@ -61,15 +59,15 @@ export function judgingSetting(options) {
61
59
  if (!Array.isArray(options.mats) || options.mats.length < 1 || options.mats.length > JUDGING_MATS_MAX)
62
60
  throw new RangeError(`An assignment has 1 to ${JUDGING_MATS_MAX} mats`);
63
61
  const language = options.language === "ja" ? "ja" : "en";
64
- const all = options.mats.map((mat, i) => {
62
+ const given = options.mats.map((mat, i) => {
65
63
  if (typeof mat !== "object" || mat === null)
66
64
  throw new TypeError("a mat is { label, officials }");
67
- const given = new Map();
65
+ const byId = new Map();
68
66
  for (const official of mat.officials ?? []) {
69
67
  if (typeof official !== "object" || official === null)
70
68
  throw new TypeError("an official is { seat, name, id }");
71
69
  const seat = seatOf(style, official.seat);
72
- if (given.has(seat.id))
70
+ if (byId.has(seat.id))
73
71
  throw new RangeError(`Mat ${i + 1} names seat ${seat.id} twice`);
74
72
  if (official.name !== undefined && typeof official.name !== "string")
75
73
  throw new TypeError("an official's name is text");
@@ -81,10 +79,15 @@ export function judgingSetting(options) {
81
79
  throw new RangeError(`A name is at most ${JUDGING_LIMITS.name} characters`);
82
80
  if ([...id].length > JUDGING_LIMITS.id)
83
81
  throw new RangeError(`An ID is at most ${JUDGING_LIMITS.id} characters`);
84
- given.set(seat.id, { seat, name, id, assigned: name !== "" || id !== "" });
82
+ byId.set(seat.id, { seat, name, id, assigned: name !== "" || id !== "" });
85
83
  }
86
- return { n: i + 1, label: matLabel(mat, i + 1, language), seats: seats.map((seat) => given.get(seat.id) ?? { seat, name: "", id: "", assigned: false }) };
84
+ return byId;
87
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 }) }));
88
91
  let mats = all;
89
92
  if (options.mat !== undefined) {
90
93
  if (!Number.isInteger(options.mat) || options.mat < 1 || options.mat > all.length)
@@ -106,30 +109,89 @@ export function judgingSetting(options) {
106
109
  };
107
110
  }
108
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 };
109
117
  /**
110
- * The judges' bench close up: the seats and the 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
111
- * 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 will do).
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).
112
121
  */
113
122
  export function benchOf(chairs, table, pitch, matTop, front) {
114
123
  const side = (pitch - 0.5) / 2 + 0.15;
115
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 };
116
125
  }
117
- /** The plan of one mat of a style, with its seats: the package's own judo area, so that the seats named are the seats drawn. */
118
- export function judgingPlan(style) {
119
- const hit = plans.get(style);
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);
120
156
  if (hit !== undefined)
121
157
  return hit;
122
158
  seatsOf(style);
123
- const plan = budoPlan({ sport: "judo", league: JUDGING_LEAGUE[style].league, budo: { style, officials: true } });
124
- const judges = plan.judo?.judges;
125
- if (judges === undefined || judges.seats.length !== JUDGING_SEATS[style].length || judges.tables.length === 0)
126
- throw new Error(`The ${style} plan has no judges' seats`);
127
- const chairs = judges.seats.slice(0, JUDGING_SEATS[style].length);
128
- const table = judges.tables[0];
129
- const pitch = (chairs[chairs.length - 1].x0 - chairs[0].x0) / (chairs.length - 1);
130
- const first = plan.judo.contests[0];
131
- const bench = benchOf(chairs, table, pitch, first.y0);
132
- const built = { plan, chairs, table, pitch, extent: plan.extent, bench };
133
- plans.set(style, built);
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);
134
190
  return built;
135
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
+ }
@@ -9,6 +9,7 @@ export declare const JUDGING_HEIGHTS: Readonly<{
9
9
  card: 0.95;
10
10
  sign: 0.7;
11
11
  signAbove: 0.25;
12
+ post: 1;
12
13
  }>;
13
14
  /**
14
15
  * The four viewpoints of a judging view. A card stands upright, so a high eye squashes its type: the broadcast view is from the mat side, so that the judges' table is at the far side and the cards face the eye, at a
@@ -1,14 +1,10 @@
1
- // A judging assignment in 3D: the 3D view of `turn.ts` over the package's own judo floor (`drawKyogijo`, with as many areas side by side as there are mats), with each judge's chair and the judges' table stood up, a name
2
- // card on the table in front of every judge who has a name (turned to face the eye, so that it is read from wherever the view is) and the name of each mat on a sign over its judges' table. Nothing is drawn a second
3
- // way: the card is the plan's card (`judging.card.ts`), fitted by the same rule. The model it makes is a `TurnModel`, so the camera, the controls, the pointer, the auto-turn and the tests of every other 3D view are this one's.
4
- import { budoPlan } from "./budo/plan.js";
5
1
  import { drawKyogijo } from "./draw.js";
6
2
  import { hash } from "./format.js";
7
3
  import { FEET_PER_METRE as FT } from "./geometry.js";
8
4
  import { TURN_CARD, badge, card } from "./judging.card.js";
9
5
  import { JUDGING_LEAGUE } from "./judging.data.js";
10
6
  import { fitCard, fitSeats } from "./judging.fit.js";
11
- import { benchOf, judgingSetting } from "./judging.js";
7
+ import { benchOf, judgingPlan, judgingSetting, seatIn } from "./judging.js";
12
8
  import { judgingDrawing, judgingText } from "./judging.draw.js";
13
9
  import { JUDGING_WORDS } from "./judging.words.js";
14
10
  import { mountTurn } from "./turn.dom.js";
@@ -18,9 +14,18 @@ import { WORLD_PX, openingView, sceneBox } from "./turn.model.js";
18
14
  import { box, n, plane, pole, rect, thinnest } from "./turn.kit.js";
19
15
  import { planesOf } from "./uprights.js";
20
16
  /** How high things stand, in metres (ours: no rule gives them): the table's top, a chair's seat, a name card on the table, the sign over it. */
21
- export const JUDGING_HEIGHTS = Object.freeze({ table: 0.75, chair: 0.45, card: 0.95, sign: 0.7, signAbove: 0.25 });
17
+ export const JUDGING_HEIGHTS = Object.freeze({ table: 0.75, chair: 0.45, card: 0.95, sign: 0.7, signAbove: 0.25, post: 1.0 });
22
18
  /** The lens of a view close up on several benches at once: the scene turns about the middle of them, and a bench several metres from it would swell as it swung toward the eye, so the lens is long (the default is 8). */
23
19
  const BENCHES_LENS = 60;
20
+ /** The bench the 3D view is fitted to when it is close up: a kata's seats at their tables, a contest's technical tables and referees, all mats in one box, reaching as little of the floor in front as will do. */
21
+ function benchNear(style, geom, count) {
22
+ if (style === "shiai") {
23
+ const b = geom.mats.slice(0, count).map((m) => m.bench);
24
+ return { x0: Math.min(...b.map((x) => x.x0)), y0: Math.min(...b.map((x) => x.y0)), x1: Math.max(...b.map((x) => x.x1)), y1: Math.max(...b.map((x) => x.y1)) };
25
+ }
26
+ const chairs = geom.mats.slice(0, count).flatMap((m) => m.places.map((p) => p.chair));
27
+ return benchOf(chairs, geom.table, geom.pitch, geom.plan.judo.contests[0].y0, 2.2);
28
+ }
24
29
  const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
25
30
  /**
26
31
  * The four viewpoints of a judging view. A card stands upright, so a high eye squashes its type: the broadcast view is from the mat side, so that the judges' table is at the far side and the cards face the eye, at a
@@ -44,49 +49,62 @@ export function buildJudgingTurn(options, settings = {}) {
44
49
  if (found === null)
45
50
  throw new Error("the floor of a 3D view must be an SVG with a viewBox");
46
51
  const [vx, vy, vw, vh] = found.slice(1).map(Number);
47
- const plan = budoPlan({ sport: "judo", league, budo: { style: setting.style, officials: true, areas: count } });
48
- const judges = plan.judo.judges;
49
- const seatsPerMat = setting.mats[0].seats.length;
52
+ const geom = judgingPlan(setting.style, count);
50
53
  // The plan's own frame is metres from the top left of the tatami, and the drawing's is feet from the same corner.
51
54
  const ft = (m) => m * FT;
52
55
  const H = JUDGING_HEIGHTS;
53
56
  const items = [];
54
57
  const tableTop = ft(H.table);
55
- const pitch = judges.seats.length > 1 ? judges.seats[1].x0 - judges.seats[0].x0 : 1.25;
56
- const cardW = ft(pitch - 0.06);
57
58
  const cardH = ft(H.card);
58
- const pad = Math.max(0.08, cardW * 0.05);
59
- const r = clamp(cardW * 0.2, 0.2, 0.45);
60
- const innerW = cardW - 2 * pad;
61
- const innerH = cardH - 2 * pad - r;
62
59
  const minName = 0.62;
60
+ // Close up on the bench, only the seats at it stand (the referee and the side judges of a contest are out of the picture, and in the chart).
61
+ const near = setting.focus === "bench" ? benchNear(setting.style, geom, count) : undefined;
63
62
  setting.mats.forEach((mat, mi) => {
64
- const table = judges.tables[mi];
65
- const fitted = fitSeats(mat.seats, { width: innerW, height: innerH, max: Math.min(cardW * 0.3, 0.75), min: minName, idLabel: words.chart.idLabel });
63
+ const places = geom.mats[mi];
64
+ const table = places.table;
65
+ const boxes = mat.seats.map((a) => {
66
+ const c = places.places[a.seat.n - 1].card;
67
+ const w = ft(c.x1 - c.x0);
68
+ const pad = Math.max(0.08, w * 0.05);
69
+ const r = clamp(w * 0.2, 0.2, 0.45);
70
+ return { w, pad, r, width: w - 2 * pad, height: cardH - 2 * pad - r, max: Math.min(w * 0.3, 0.75) };
71
+ });
72
+ const fitted = fitSeats(mat.seats, boxes, { min: minName, idLabel: words.chart.idLabel });
66
73
  const furniture = [];
67
74
  furniture.push(...box(`m${mat.n}-table`, "desk", rect(ft(table.x0), ft(table.y0), ft(table.x1 - table.x0), ft(table.y1 - table.y0)), tableTop, "u-bench"));
68
75
  const cards = [];
76
+ const stands = [];
69
77
  mat.seats.forEach((a, si) => {
70
- const chair = judges.seats[mi * seatsPerMat + si];
71
- furniture.push(...box(`m${mat.n}-chair-${a.seat.n}`, "chair", rect(ft(chair.x0), ft(chair.y0), ft(chair.x1 - chair.x0), ft(chair.y1 - chair.y0)), ft(H.chair), "u-bench"));
72
- const cx = ft((chair.x0 + chair.x1) / 2);
73
- const cy = ft((table.y0 + table.y1) / 2);
78
+ const place = places.places[a.seat.n - 1];
79
+ if (near !== undefined && !seatIn(place, near))
80
+ return;
81
+ const b = boxes[si];
82
+ // A chair for the seats that have one (the referee stands on the mat).
83
+ if (a.seat.role !== "referee")
84
+ furniture.push(...box(`m${mat.n}-chair-${a.seat.n}`, "chair", rect(ft(place.chair.x0), ft(place.chair.y0), ft(place.chair.x1 - place.chair.x0), ft(place.chair.y1 - place.chair.y0)), ft(H.chair), "u-bench"));
85
+ const cx = ft((place.card.x0 + place.card.x1) / 2);
86
+ // A card at a table stands on the table; any other on a post, at the height of a standing official's chest.
87
+ const atTable = a.seat.where === "table";
88
+ const cy = atTable ? ft((table.y0 + table.y1) / 2) : ft((place.card.y0 + place.card.y1) / 2);
89
+ const base = atTable ? tableTop : ft(H.post);
90
+ if (!atTable)
91
+ stands.push(...pole(`m${mat.n}-post-${a.seat.n}`, "pole", cx, cy, 0, base, ft(0.06), "u-pole"));
74
92
  const fit = fitted[si];
75
93
  if (fit !== undefined) {
76
- const p = plane(`m${mat.n}-card-${a.seat.n}`, "card", [cx - cardW / 2, cy, tableTop + cardH], [cardW, 0, 0], [0, 0, -cardH], card(TURN_CARD, fit, a.seat.n, 0, 0, cardW, cardH, pad, r, 0.1));
94
+ const p = plane(`m${mat.n}-card-${a.seat.n}`, "card", [cx - b.w / 2, cy, base + cardH], [b.w, 0, 0], [0, 0, -cardH], card(TURN_CARD, fit, a.seat.n, 0, 0, b.w, cardH, b.pad, b.r, 0.1));
77
95
  cards.push({ ...p, billboard: true });
78
96
  }
79
97
  else {
80
98
  // Nobody in the seat, or a name that does not fit a card: the seat's number alone, on a small badge, with the name in the chart.
81
99
  const br = 0.55;
82
- const p = plane(`m${mat.n}-badge-${a.seat.n}`, "card", [cx - br, cy, tableTop + 2 * br], [2 * br, 0, 0], [0, 0, -2 * br], badge(TURN_CARD, br, br, br * 0.92, a.seat.n, a.assigned));
100
+ const p = plane(`m${mat.n}-badge-${a.seat.n}`, "card", [cx - br, cy, base + 2 * br], [2 * br, 0, 0], [0, 0, -2 * br], badge(TURN_CARD, br, br, br * 0.92, a.seat.n, a.assigned));
83
101
  cards.push({ ...p, billboard: true });
84
102
  }
85
103
  });
86
- // The mat's name on a sign over the whole bench, on a pole at each end of the table.
104
+ // The mat's name on a sign over the whole table, on a pole at each end of it.
87
105
  const midX = ft((table.x0 + table.x1) / 2);
88
106
  const midY = ft((table.y0 + table.y1) / 2);
89
- const signW = ft(pitch * seatsPerMat);
107
+ const signW = ft(table.x1 - table.x0);
90
108
  const signH = ft(H.sign);
91
109
  const signBottom = tableTop + cardH + ft(H.signAbove);
92
110
  const text = fitCard({ name: mat.label, id: "", idLabel: "", width: signW * 0.9, height: signH * 0.86, max: signH * 0.62, min: signH * 0.3, maxLines: 2, breakWords: true });
@@ -96,10 +114,9 @@ export function buildJudgingTurn(options, settings = {}) {
96
114
  const sign = { ...plane(`m${mat.n}-sign`, "sign", [midX - signW / 2, midY, signBottom + signH], [signW, 0, 0], [0, 0, -signH], `<rect class="u-sign" width="${n(signW)}" height="${n(signH)}" rx="${n(signH * 0.12)}"/>${label}`), billboard: true };
97
115
  const poles = [table.x0, table.x1].flatMap((px, k) => pole(`m${mat.n}-sign-pole-${k + 1}`, "pole", ft(px), midY, tableTop, signBottom + signH * 0.5, ft(0.06), "u-pole"));
98
116
  items.push({ id: `m${mat.n}-furniture`, kind: "officials", at: [midX, midY], height: tableTop, planes: furniture });
99
- items.push({ id: `m${mat.n}-judges`, kind: "judges", at: [midX, midY], height: tableTop + cardH, planes: [...cards, ...poles, sign] });
117
+ items.push({ id: `m${mat.n}-judges`, kind: "judges", at: [midX, midY], height: tableTop + cardH, planes: [...cards, ...stands, ...poles, sign] });
100
118
  });
101
- // The view is fitted to the whole floor, or to the judges' bench close up (the scene then turns about the bench, so that the cards stay where they are read).
102
- const near = setting.focus === "bench" ? benchOf(judges.seats.slice(0, count * seatsPerMat), judges.tables[0], pitch, plan.judo.contests[0].y0, 2.2) : undefined;
119
+ // The view is fitted to the whole floor, or to the officials' bench close up (the scene then turns about the bench, so that the cards stay where they are read).
103
120
  const area = near === undefined ? { x0: vx, y0: vy, x1: vx + vw, y1: vy + vh } : { x0: ft(near.x0), y0: ft(near.y0), x1: ft(near.x1), y1: ft(near.y1) };
104
121
  const uprights = { items, lift: 0, thin: thinnest(vw, vh), area, whole: true };
105
122
  const planes = planesOf(uprights);
@@ -1,7 +1,7 @@
1
1
  import type { Language } from "./types.ts";
2
- /** The style a judo area is judged in. `kata` has five judges; `shiai`, the contest, is the next release. */
3
- export type JudgingStyle = "kata";
4
- /** One official: the seat, and who sits in it. The seat is its number (`3`, `"3"`), its key (`"judge-3"`) or `"central"` (the middle seat of a kata area). */
2
+ /** The style a judo area is officiated in: `kata` has five judges; `shiai`, the contest, a referee on the mat and the officials at the technical table. */
3
+ export type JudgingStyle = "kata" | "shiai";
4
+ /** One official: the seat, and who sits in it. The seat is its number (`3`, `"3"`), its key (`"judge-3"`, `"referee"`, `"timekeeper"`) or a word (`"central"` for the middle seat of a kata area). */
5
5
  export interface JudgingOfficial {
6
6
  seat: string | number;
7
7
  /** The name as it is to be read, at most 120 characters; a seat with no name is shown as not assigned. */
@@ -26,6 +26,8 @@ export interface JudgingOptions {
26
26
  mat?: number;
27
27
  /** The chart of mat, seat, name and ID. `auto` (the default) draws it whenever a name or an ID does not fit in its seat, `always` and `never` as they say. */
28
28
  chart?: "auto" | "always" | "never";
29
+ /** Shiai: draw the two side judges' seats too (the IJF's rules for an event with no CARE system use them; its other events have none), as not assigned where nobody is named. A seat a mat names is drawn whatever this says. Off by default. */
30
+ sideJudges?: boolean;
29
31
  /** Where the chart is laid: beside the plan (`landscape`, the default) or under it (`portrait`). */
30
32
  orientation?: "landscape" | "portrait";
31
33
  /** What the plan shows: the whole `mat` (the default) or the judges' `bench`, close up, so that the names in the seats are as large as they can be. */
@@ -46,10 +48,14 @@ export interface JudgingOptions {
46
48
  export interface JudgingSeat {
47
49
  /** The key: `judge-3`. */
48
50
  id: string;
49
- /** The number, from 1, in the order the seats are drawn (left to right as the plan lies, the shomen at the top). */
51
+ /** The number, from 1, in the order the seats are listed (a kata area's from the left as the plan lies, the shomen at the top). */
50
52
  n: number;
51
- /** `central` for the middle seat of a kata area. */
52
- role: "judge" | "central";
53
+ /** What the seat is: a kata area's judges (`central` the middle one), or a contest's referee, side judges, timekeeper, scorekeeper and referee supervisor. */
54
+ role: "judge" | "central" | "referee" | "side-judge" | "timekeeper" | "scorekeeper" | "supervisor";
55
+ /** Where it is: at the judges' table of a kata area or the technical table of a contest (`table`), on the mat (`mat`: the referee) or at a corner of the area (`corner`: a side judge). */
56
+ where: "table" | "mat" | "corner";
57
+ /** True for a seat that is drawn only when asked for or named (the side judges). */
58
+ optional?: true;
53
59
  label: {
54
60
  en: string;
55
61
  ja: string;
@@ -5,6 +5,7 @@ export interface JudgingWords {
5
5
  /** What a style is called on the plan, `{count}` the number of seats. */
6
6
  styleName: {
7
7
  kata: string;
8
+ shiai: string;
8
9
  };
9
10
  /** The chart: its heading, its columns, the word before an ID and what a seat with nobody in it says. */
10
11
  chart: {
@@ -1,7 +1,7 @@
1
1
  export const JUDGING_WORDS = {
2
2
  en: {
3
3
  mat: "Mat {n}",
4
- styleName: { kata: "Kata, {count} judges" },
4
+ styleName: { kata: "Kata, {count} judges", shiai: "Shiai, {count} officials" },
5
5
  chart: { heading: "Judging assignments", seat: "Seat", name: "Name", id: "ID", idLabel: "ID", vacant: "Not assigned" },
6
6
  say: {
7
7
  title: "Judging assignments: {style}, {mats}",
@@ -20,7 +20,7 @@ export const JUDGING_WORDS = {
20
20
  },
21
21
  ja: {
22
22
  mat: "第{n}試合場",
23
- styleName: { kata: "形({count}人制)" },
23
+ styleName: { kata: "形({count}人制)", shiai: "試合({count}人体制)" },
24
24
  chart: { heading: "審判員の配置", seat: "席", name: "氏名", id: "ID", idLabel: "ID", vacant: "未定" },
25
25
  say: {
26
26
  title: "審判員の配置:{style}、{mats}",
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export declare const VERSION = "0.17.0";
2
+ export declare const VERSION = "0.18.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.17.0";
2
+ export const VERSION = "0.18.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Kyogijo Stadiums 競技場, 39 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",