@johnmorrisdotca/kyogijo-stadiums 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.18.1] - 2026-10-20
11
+
12
+ ### Changed
13
+
14
+ - **The seats show their number, and the names are in the chart.** By default every seat of a judging assignment is a large round numbered badge (the chart's own), in 2D and in 3D, and the chart beside it is the key (`chart: "auto"` now always draws it, unless `seatNames` is on and every name fits). Putting the names on the seats is the new option `seatNames: true` (off by default), which sets them in the chart's own face and weight, scaled evenly, in the biggest size that fits and never below a readable floor, and falls back to the number alone when a name does not fit. The demo has a "Names on the seats too" box (`seatnames=1`).
15
+
16
+ ### Fixed
17
+
18
+ - **Nothing is squeezed or stretched in a judging picture.** The 3D badges, cards and the mat's sign stood upright, so a tipped view foreshortened them and their type looked squeezed: they now lean back to face the eye squarely (`Plane.lean`, turned and never stretched), at every viewpoint. A test measures each one's shape on the screen against its own, and that no text anywhere has `textLength`, `lengthAdjust` or a two-number `scale`.
19
+ - **The tapes of a judo area were hairlines.** The kata area's centre and starting tapes (5 cm by 50 cm) and a contest area's two start tapes, and the sumo's shikiri-sen, are drawn at least 0.75 % of the plan's width across (10.5 cm on a kata area's 14 m, about 4 pixels on a plan 560 pixels across) unless `exactLines: true`, which draws the rule's 5 cm. The floor is a readability aid, not a measurement: a tape's length is always the rule's, and it is widened about its middle. In 3D (the same floor) and in tournament layouts too.
20
+
21
+ ## [0.18.0] - 2026-10-20
22
+
23
+ ### Added
24
+
25
+ - **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).
26
+
27
+ ### Changed
28
+
29
+ - `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.
30
+ - `JudoLayout` (the judo plan) also gives `desks` (the technical tables) and `free` (the free zone), as data only: no drawing changes.
31
+
10
32
  ## [0.17.0] - 2026-10-20
11
33
 
12
34
  ### Added
package/README.md CHANGED
@@ -550,9 +550,9 @@ The [demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/halls.html) turns
550
550
  <p align="center">
551
551
  <picture>
552
552
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-3d-desk-dark.webp">
553
- <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-3d-desk-light.webp" alt="A kata area's judges' table in 3D, with a name card before each judge." width="640">
553
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/judging-3d-desk-light.webp" alt="A kata area's judges' table in 3D, numbered badges, and the chart." width="640">
554
554
  </picture>
555
- <br><em>Judges in 3D.</em>
555
+ <br><em>Judges, in 3D.</em>
556
556
  </p>
557
557
 
558
558
 
@@ -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;
@@ -28,28 +28,49 @@ export interface JudgingSetting {
28
28
  orientation: "landscape" | "portrait";
29
29
  focus: "mat" | "bench";
30
30
  width: number;
31
+ /** Whether the names are on the seats (`seatNames`). */
32
+ seatNames: boolean;
31
33
  }
32
34
  /**
33
35
  * Hold the options to what they may be and put each official in a seat.
34
36
  * @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
37
  */
36
38
  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. */
39
+ /** 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. */
40
+ export interface SeatPlace {
41
+ chair: MBox;
42
+ card: MBox;
43
+ }
44
+ /** One mat's seats, in the order the style lists them (`JUDGING_SEATS`, optional seats included), its table and its bench. */
45
+ export interface MatPlaces {
46
+ places: SeatPlace[];
47
+ /** The judges' table or the technical table behind the seats at it. */
48
+ table: MBox;
49
+ /** The seats at the table close up: the picture of `focus: "bench"`. */
50
+ bench: MBox;
51
+ }
52
+ /** Where the seats are on the plan of a style, and the plan they are on. */
38
53
  export interface JudgingPlan {
39
54
  plan: BudoPlan;
40
- /** The chair of each seat, in seat order, and the judges' table behind them. */
55
+ /** Each mat's seats, for the mats side by side the plan has (`count`). */
56
+ mats: MatPlaces[];
57
+ /** The first mat's seats, its table and its bench, as `mats[0]` has them (the chairs of the seats, in order). */
58
+ places: SeatPlace[];
41
59
  chairs: MBox[];
42
60
  table: MBox;
43
- /** The distance between two seats. */
61
+ bench: MBox;
62
+ /** The distance between two seats at the table. */
44
63
  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. */
64
+ /** The whole picture of a mat (the free zone, the table, the seats, their cards): what `focus: "mat"` shows, for the first mat. */
46
65
  extent: MBox;
47
- bench: MBox;
48
66
  }
49
67
  /**
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).
68
+ * 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
69
+ * 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
70
+ * will do).
52
71
  */
53
72
  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;
73
+ /** 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. */
74
+ export declare function judgingPlan(style: JudgingStyle, count?: number): JudgingPlan;
75
+ /** 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. */
76
+ 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";
@@ -42,7 +42,7 @@ export function judgingText(setting, options, chart) {
42
42
  const mats = setting.total === 1 ? words.say.matsOne : fill(words.say.matsMany, { n: setting.total });
43
43
  const title = options.title ?? fill(words.say.title, { style, mats });
44
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" ? "" : " ");
45
+ const description = options.description ?? [...sentences, chart && setting.seatNames ? words.say.names : words.say.chartOnly].join(setting.language === "ja" ? "" : " ");
46
46
  return { title, description };
47
47
  }
48
48
  const metricsOf = (width) => {
@@ -117,26 +117,32 @@ 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
+ // The names are on the seats only when asked for; otherwise every seat is its number, and the chart is the key.
130
+ const fitted = setting.seatNames ? fitSeats(mat.seats, boxes, { min: m.cardMin, idLabel: words.chart.idLabel }) : mat.seats.map(() => undefined);
131
+ // 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.
132
+ const shown = mat.seats.map((a) => seatIn(geom.places[a.seat.n - 1], region));
133
+ const fits = mat.seats.map((a, i) => !a.assigned || (shown[i] && fitted[i] !== undefined));
129
134
  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);
135
+ if (!shown[i])
136
+ return;
137
+ const place = geom.places[a.seat.n - 1];
138
+ const b = boxes[i];
133
139
  const fit = fitted[i];
134
140
  let g = "";
135
141
  if (fit !== undefined)
136
- g += card(PLAN_CARD, fit, a.seat.n, cx - cardW / 2, top, cardW, cardH, pad, r, 3 * m.k);
142
+ 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
143
  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);
144
+ const br = clamp(Math.min(b.cw, 1.25 * s) * 0.42, 10 * m.k, 30 * m.k);
145
+ 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
146
  }
141
147
  markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
142
148
  });
@@ -175,8 +181,9 @@ export function judgingDrawing(options, mode = "full") {
175
181
  return setting.mats.flatMap((mat, i) => planMarkup(mat, surfaces[i], geom, region, { x: 0, y: 0, ...box }, m, setting, "t").fits);
176
182
  };
177
183
  let chart = mode === "chart" || setting.chart === "always";
184
+ // `auto`: the chart is the key to the numbers, so it is drawn unless the names are on the seats and every one fits there.
178
185
  if (mode === "full" && setting.chart === "auto")
179
- chart = !trial(false).every(Boolean);
186
+ chart = !setting.seatNames || !trial(false).every(Boolean);
180
187
  if (mode === "full" && setting.chart === "never")
181
188
  chart = false;
182
189
  const cardsFit = mode === "chart" ? [] : trial(chart);
@@ -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,17 @@ 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
+ if (options.seatNames !== undefined && typeof options.seatNames !== "boolean")
89
+ throw new TypeError("seatNames is true or false");
90
+ // The seats that are drawn: every one that is not optional, and an optional one when asked for or when a mat names it.
91
+ const shown = seats.filter((seat) => seat.optional !== true || options.sideJudges === true || given.some((m) => m.has(seat.id)));
92
+ 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
93
  let mats = all;
89
94
  if (options.mat !== undefined) {
90
95
  if (!Number.isInteger(options.mat) || options.mat < 1 || options.mat > all.length)
@@ -103,33 +108,93 @@ export function judgingSetting(options) {
103
108
  orientation: oneOf(options.orientation, ["landscape", "portrait"], "landscape", "orientation"),
104
109
  focus: oneOf(options.focus, ["mat", "bench"], "mat", "focus"),
105
110
  width,
111
+ seatNames: options.seatNames === true,
106
112
  };
107
113
  }
108
114
  const plans = new Map();
115
+ /** A box of a given centre and size. */
116
+ const around = (cx, cy, w, h) => ({ x0: cx - w / 2, y0: cy - h / 2, x1: cx + w / 2, y1: cy + h / 2 });
117
+ 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)) });
118
+ /** 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). */
119
+ const FREE_CARD = { w: 2.6, h: 1.8 };
109
120
  /**
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).
121
+ * 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
122
+ * 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
123
+ * will do).
112
124
  */
113
125
  export function benchOf(chairs, table, pitch, matTop, front) {
114
126
  const side = (pitch - 0.5) / 2 + 0.15;
115
127
  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
128
  }
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);
129
+ /** 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. */
130
+ function kataPlaces(chairs, table, pitch) {
131
+ return chairs.map((chair) => {
132
+ const cx = (chair.x0 + chair.x1) / 2;
133
+ return { chair, card: { x0: cx - (pitch / 2 - 0.03), y0: chair.y0 - 0.05, x1: cx + (pitch / 2 - 0.03), y1: table.y1 } };
134
+ });
135
+ }
136
+ /**
137
+ * 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
138
+ * behind the technical table, 1 m apart, and the side judges at two opposite corners of the contest area, 2 m out from them.
139
+ */
140
+ function shiaiPlaces(contest, desk) {
141
+ const cx = (contest.x0 + contest.x1) / 2;
142
+ const cy = (contest.y0 + contest.y1) / 2;
143
+ const seat = (x, y, w, h) => ({ chair: around(x, y, 0.5, 0.5), card: around(x, y, w, h) });
144
+ const chairY = desk.y0 - 0.35;
145
+ 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 } });
146
+ return [
147
+ seat(cx, cy + 2, FREE_CARD.w, FREE_CARD.h),
148
+ tableSeat(cx - 1),
149
+ tableSeat(cx),
150
+ tableSeat(cx + 1),
151
+ seat(contest.x0 - 2, contest.y0 - 2, FREE_CARD.w, FREE_CARD.h),
152
+ seat(contest.x1 + 2, contest.y1 + 2, FREE_CARD.w, FREE_CARD.h),
153
+ ];
154
+ }
155
+ /** 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. */
156
+ export function judgingPlan(style, count = 1) {
157
+ const key = `${style}:${count}`;
158
+ const hit = plans.get(key);
120
159
  if (hit !== undefined)
121
160
  return hit;
122
161
  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);
162
+ const plan = budoPlan({ sport: "judo", league: JUDGING_LEAGUE[style].league, budo: { style, officials: true, areas: count } });
163
+ const judo = plan.judo;
164
+ const mats = [];
165
+ for (let mi = 0; mi < count; mi += 1) {
166
+ const contest = judo.contests[mi];
167
+ if (style === "kata") {
168
+ const per = JUDGING_SEATS.kata.length;
169
+ const chairs = judo.judges.seats.slice(mi * per, (mi + 1) * per);
170
+ const table = judo.judges.tables[mi];
171
+ if (chairs.length !== per || table === undefined)
172
+ throw new Error("The kata plan has no judges' seats");
173
+ const pitch = (chairs[per - 1].x0 - chairs[0].x0) / (per - 1);
174
+ mats.push({ places: kataPlaces(chairs, table, pitch), table, bench: benchOf(chairs, table, pitch, contest.y0) });
175
+ }
176
+ else {
177
+ const desk = judo.desks[mi];
178
+ if (desk === undefined)
179
+ throw new Error("The contest plan has no technical table");
180
+ const places = shiaiPlaces(contest, desk);
181
+ const atTable = places.slice(1, 4);
182
+ // 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.
183
+ 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 };
184
+ mats.push({ places, table: desk, bench });
185
+ }
186
+ }
187
+ const first = mats[0];
188
+ const chairs = first.places.map((p) => p.chair);
189
+ const pitch = style === "kata" ? (first.places[first.places.length - 1].chair.x0 - first.places[0].chair.x0) / (first.places.length - 1) : 1;
190
+ const extent = union([plan.extent, ...first.places.map((p) => p.card), ...first.places.map((p) => p.chair)]);
191
+ const built = { plan, mats, places: first.places, chairs, table: first.table, bench: first.bench, pitch, extent };
192
+ plans.set(key, built);
134
193
  return built;
135
194
  }
195
+ /** 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. */
196
+ export function seatIn(place, box) {
197
+ const x = (place.card.x0 + place.card.x1) / 2;
198
+ const y = (place.card.y0 + place.card.y1) / 2;
199
+ return x >= box.x0 && x <= box.x1 && y >= box.y0 && y <= box.y1;
200
+ }
@@ -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,62 +49,74 @@ 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);
74
- const fit = fitted[si];
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"));
92
+ const fit = setting.seatNames ? fitted[si] : undefined;
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));
77
- cards.push({ ...p, billboard: true });
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));
95
+ cards.push({ ...p, billboard: true, lean: true });
78
96
  }
79
97
  else {
80
- // 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
- 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));
83
- cards.push({ ...p, billboard: true });
98
+ // The names are not on the seats, or this one does not fit a card: the seat's number alone, on a round badge like the chart's, with the name in the chart.
99
+ const br = 0.9;
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));
101
+ cards.push({ ...p, billboard: true, lean: 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 });
93
111
  const lines = text?.name ?? [mat.label];
94
112
  const size = text?.nameSize ?? signH * 0.3;
95
113
  const label = lines.map((line, i) => `<text class="u-sign-text" x="${n(signW / 2)}" y="${n(signH / 2 + (i - (lines.length - 1) / 2) * size * 1.1 + size * 0.34)}" font-size="${n(size)}">${line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</text>`).join("");
96
- 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 };
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, lean: 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);
@@ -113,8 +130,8 @@ export function buildJudgingTurn(options, settings = {}) {
113
130
  language,
114
131
  options: surfaceLike,
115
132
  title,
116
- description: `${description}${language === "ja" ? "" : " "}${words.turn.standing}`,
117
- standing: words.turn.standing,
133
+ description: `${description}${language === "ja" ? "" : " "}${setting.seatNames ? words.turn.standing : words.turn.standingBadges}`,
134
+ standing: setting.seatNames ? words.turn.standing : words.turn.standingBadges,
118
135
  floor,
119
136
  viewBox: { x: vx, y: vy, w: vw, h: vh },
120
137
  scale,
@@ -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,13 @@ 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;
31
+ /**
32
+ * Put the names on the seats as well as in the chart. Off by default: a seat then shows its number alone, on the round badge the chart's rows have, and the chart is the key. On, a name is set in the chart's own face, scaled
33
+ * evenly (never stretched or condensed), in the biggest size that fits its card and no smaller than a readable one; a seat whose name does not fit shows its number alone.
34
+ */
35
+ seatNames?: boolean;
29
36
  /** Where the chart is laid: beside the plan (`landscape`, the default) or under it (`portrait`). */
30
37
  orientation?: "landscape" | "portrait";
31
38
  /** 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 +53,14 @@ export interface JudgingOptions {
46
53
  export interface JudgingSeat {
47
54
  /** The key: `judge-3`. */
48
55
  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). */
56
+ /** 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
57
  n: number;
51
- /** `central` for the middle seat of a kata area. */
52
- role: "judge" | "central";
58
+ /** 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. */
59
+ role: "judge" | "central" | "referee" | "side-judge" | "timekeeper" | "scorekeeper" | "supervisor";
60
+ /** 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). */
61
+ where: "table" | "mat" | "corner";
62
+ /** True for a seat that is drawn only when asked for or named (the side judges). */
63
+ optional?: true;
53
64
  label: {
54
65
  en: string;
55
66
  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: {
@@ -39,6 +40,7 @@ export interface JudgingWords {
39
40
  /** What the 3D view says of the seats' name cards. */
40
41
  turn: {
41
42
  standing: string;
43
+ standingBadges: string;
42
44
  board: string;
43
45
  };
44
46
  }
@@ -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}",
@@ -16,11 +16,11 @@ export const JUDGING_WORDS = {
16
16
  join: "; ",
17
17
  },
18
18
  table: { caption: "{title}", mat: "Mat", seat: "Seat", name: "Name", id: "ID" },
19
- turn: { standing: "A name card stands at each judge's seat, turned to face the eye, and the name of each mat stands over its judges' table.", board: "Mat" },
19
+ turn: { standing: "A name card stands at each judge's seat, turned to face the eye, and the name of each mat stands over its judges' table.", standingBadges: "A numbered badge stands at each judge's seat, leaning back to face the eye, and the name of each mat stands over its judges' table.", board: "Mat" },
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}",
@@ -35,6 +35,6 @@ export const JUDGING_WORDS = {
35
35
  join: "、",
36
36
  },
37
37
  table: { caption: "{title}", mat: "試合場", seat: "席", name: "氏名", id: "ID" },
38
- turn: { standing: "各審判員の席に名札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", board: "試合場" },
38
+ turn: { standing: "各審判員の席に名札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", standingBadges: "各審判員の席に番号の丸い札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", board: "試合場" },
39
39
  },
40
40
  };
@@ -3,4 +3,6 @@ import { type BuildOptions, type Surface } from "./surface.ts";
3
3
  /** Feet in a metre. */
4
4
  export declare const K: number;
5
5
  /** The surface of the plan lying wide. `margin` is the ground round what is drawn, in feet. */
6
+ /** The least width a tape is drawn at, as a share of the plan's width (0.75 %: about 4 pixels on a plan 560 pixels across), unless `exactLines` is set. */
7
+ export declare const TAPE_LEAST = 0.0075;
6
8
  export declare function budoSurface(o: BuildOptions, plan: BudoPlan, margin: number): Surface;
@@ -14,6 +14,18 @@ function rangeOf(r, ox, oy) {
14
14
  return "left" in r ? atEnds((end) => feetBox(end === "left" ? r.left : r.right, ox, oy)) : feetBox(r, ox, oy);
15
15
  }
16
16
  /** The surface of the plan lying wide. `margin` is the ground round what is drawn, in feet. */
17
+ /** The least width a tape is drawn at, as a share of the plan's width (0.75 %: about 4 pixels on a plan 560 pixels across), unless `exactLines` is set. */
18
+ export const TAPE_LEAST = 0.0075;
19
+ /** A bar widened across its shorter side to at least `least` metres, evenly about its middle; a bar that is wide enough is the same bar. */
20
+ function widen(bar, least) {
21
+ const w = bar.x1 - bar.x0;
22
+ const h = bar.y1 - bar.y0;
23
+ if (Math.min(w, h) >= least)
24
+ return bar;
25
+ if (w < h)
26
+ return { ...bar, x0: (bar.x0 + bar.x1) / 2 - least / 2, x1: (bar.x0 + bar.x1) / 2 + least / 2 };
27
+ return { ...bar, y0: (bar.y0 + bar.y1) / 2 - least / 2, y1: (bar.y0 + bar.y1) / 2 + least / 2 };
28
+ }
17
29
  export function budoSurface(o, plan, margin) {
18
30
  const ox = plan.box.x0;
19
31
  const oy = plan.box.y0;
@@ -81,7 +93,11 @@ export function budoSurface(o, plan, margin) {
81
93
  const thick = o.exact ? plan.lineWidth : plan.drawnLine;
82
94
  // The seams are the plan's finest lines: left out of a drawing that asks for the simple detail.
83
95
  const shown = full ? plan : { ...plan, shapes: plan.shapes.filter((s) => s.feature !== "seams" && s.feature !== "boards") };
84
- const lines = group(strokes(shown, o) + marks(shown, o), "ky-bud-lines");
96
+ // A tape is 5 cm to 6 cm wide: at the size of a drawing that is a hairline. Unless the lines are asked for at their true widths (`exactLines`), a tape is drawn at least `TAPE_LEAST` of the plan's width across (a readability floor,
97
+ // not a measurement: its length is always the rule's), widened evenly about its middle.
98
+ const floor = (plan.box.x1 - plan.box.x0) * TAPE_LEAST;
99
+ const taped = o.exact ? shown : { ...shown, shapes: shown.shapes.map((s) => (s.kind === "bar" ? widen(s, floor) : s)) };
100
+ const lines = group(strokes(taped, o) + marks(taped, o), "ky-bud-lines");
85
101
  const W = (plan.box.x1 - plan.box.x0) * K;
86
102
  const H = (plan.box.y1 - plan.box.y0) * K;
87
103
  const cx = (plan.centre.x - ox) * K;
@@ -16,6 +16,11 @@ export interface Plane {
16
16
  * place as it lies at a yaw of 0; `v` must be vertical.
17
17
  */
18
18
  billboard?: boolean;
19
+ /**
20
+ * A billboard that also leans back to face the eye squarely, as the pitch tips the view: a card that stands upright is seen foreshortened from above, and its type looks squeezed, so a plane that carries type leans
21
+ * till its face is square to the eye. It is turned only (never stretched): its width and height stay what they are. At an overhead view it lies flat. `v` must be vertical, as for a billboard.
22
+ */
23
+ lean?: boolean;
19
24
  /**
20
25
  * A wall round the surface that gives way when it is between the eye and what it surrounds: the horizontal direction (x, y, a unit vector) that points from the surface out through the
21
26
  * wall. The nearer the eye is to that side, and the lower it is, the more the wall is cut away (see `fadeOf`), so that the court is never hidden behind its own glass.
@@ -56,7 +61,7 @@ export declare const cross: (a: V3, b: V3) => V3;
56
61
  /** A number for markup: millimetres of a foot, no more. */
57
62
  export declare const n: (value: number) => string;
58
63
  /** A billboard turned to face an eye whose view is turned `yaw` degrees: its width runs along the screen's x, which in the scene is (cos yaw, -sin yaw, 0). */
59
- export declare function facing(p: Plane, yaw: number): Plane;
64
+ export declare function facing(p: Plane, yaw: number, pitch?: number): Plane;
60
65
  /** A plane. */
61
66
  export declare const plane: (id: string, kind: PartKind, o: V3, u: V3, v: V3, svg: string) => Plane;
62
67
  /**
package/dist/turn.kit.js CHANGED
@@ -18,14 +18,18 @@ export const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] *
18
18
  /** A number for markup: millimetres of a foot, no more. */
19
19
  export const n = (value) => String(Math.round(value * 1000) / 1000);
20
20
  /** A billboard turned to face an eye whose view is turned `yaw` degrees: its width runs along the screen's x, which in the scene is (cos yaw, -sin yaw, 0). */
21
- export function facing(p, yaw) {
21
+ export function facing(p, yaw, pitch = 0) {
22
22
  if (p.billboard !== true)
23
23
  return p;
24
24
  const [w, h] = size(p);
25
25
  const a = (yaw * Math.PI) / 180;
26
26
  const u = [w * Math.cos(a), -w * Math.sin(a), 0];
27
27
  const middle = [p.o[0] + (p.u[0] + p.v[0]) / 2, p.o[1] + (p.u[1] + p.v[1]) / 2, p.o[2] + (p.u[2] + p.v[2]) / 2];
28
- const v = [0, 0, p.v[2] < 0 ? -h : h];
28
+ // A plane that leans has its height along the screen's vertical, which at a pitch tips back from the vertical: (cos p sin yaw, cos p cos yaw, -sin p) is the way down the screen, in the scene.
29
+ const b = (pitch * Math.PI) / 180;
30
+ const down = p.lean === true ? [Math.cos(b) * Math.sin(a), Math.cos(b) * Math.cos(a), -Math.sin(b)] : [0, 0, -1];
31
+ const sign = p.v[2] < 0 ? 1 : -1;
32
+ const v = [down[0] * h * sign, down[1] * h * sign, down[2] * h * sign];
29
33
  return { ...p, o: [middle[0] - (u[0] + v[0]) / 2, middle[1] - (u[1] + v[1]) / 2, middle[2] - (u[2] + v[2]) / 2], u, v };
30
34
  }
31
35
  /** A plane. */
@@ -27,7 +27,7 @@ export function layoutOf(model, view, width, height, lens) {
27
27
  const centre = model.centre;
28
28
  const floor = { id: "floor", kind: "ground", o: [model.viewBox.x, model.viewBox.y, model.uprights.lift], u: [model.viewBox.w, 0, 0], v: [0, model.viewBox.h, 0], svg: "" };
29
29
  // A billboard faces the eye at this view.
30
- const planes = model.planes.map((p) => facing(p, view.yaw));
30
+ const planes = model.planes.map((p) => facing(p, view.yaw, view.pitch));
31
31
  const transforms = [floor, ...planes].map((p) => matrix3d(multiply(camera, planeMatrix(p, s, centre))));
32
32
  // Far to near: a plane's depth is that of its middle. The ground of a hall is under everything. The floor is next, unless it stands raised on something (a table tennis table's top, on its legs): then it is
33
33
  // sorted with the rest, by its middle, so that the legs and the skirt on the far side are drawn under it and the ones on the near side over it, and nothing under the top shows through it.
@@ -42,7 +42,7 @@ export function sceneBox(planes, area, lift, centre, scale) {
42
42
  let top = lift;
43
43
  for (const p of planes) {
44
44
  // A billboard turns about its middle: whichever way it faces, it lies within a circle as wide as it is.
45
- const reach = p.billboard === true ? Math.hypot(...p.u) / 2 : 0;
45
+ const reach = p.billboard === true ? (p.lean === true ? Math.hypot(...p.u, ...p.v) / 2 : Math.hypot(...p.u) / 2) : 0;
46
46
  for (const [a, b] of [[0, 0], [1, 0], [0, 1], [1, 1]]) {
47
47
  const point = [p.o[0] + a * p.u[0] + b * p.v[0], p.o[1] + a * p.u[1] + b * p.v[1], p.o[2] + a * p.u[2] + b * p.v[2]];
48
48
  if (reach > 0) {
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.1";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.17.0";
2
+ export const VERSION = "0.18.1";
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.1",
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",