@johnmorrisdotca/kyogijo-stadiums 0.18.0 → 0.19.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.
@@ -0,0 +1,7 @@
1
+ import type { JudgingSetting } from "./judging.ts";
2
+ import type { JudgingOptions } from "./judging.types.ts";
3
+ /** The title and the description of an assignment: each mat with each seat and who is in it. */
4
+ export declare function judgingText(setting: JudgingSetting, options: Pick<JudgingOptions, "title" | "description">, chart: boolean, view?: "plan" | "broadcast"): {
5
+ title: string;
6
+ description: string;
7
+ };
@@ -0,0 +1,23 @@
1
+ // What a judging picture says in words: the title and the description of an assignment, each mat with each seat and who is in it. Shared by the plan view, the broadcast view and the 3D view.
2
+ import { JUDGING_WORDS } from "./judging.words.js";
3
+ import { fill } from "./strings.js";
4
+ /** The sentence that names one seat and who sits in it. */
5
+ function seatSentence(a, setting) {
6
+ const w = JUDGING_WORDS[setting.language].say;
7
+ const seat = a.seat.label[setting.language];
8
+ if (!a.assigned)
9
+ return fill(w.seatVacant, { seat });
10
+ if (a.id === "")
11
+ return fill(w.seatNoId, { seat, name: a.name });
12
+ return fill(w.seat, { seat, name: a.name === "" ? "—" : a.name, id: a.id });
13
+ }
14
+ /** The title and the description of an assignment: each mat with each seat and who is in it. */
15
+ export function judgingText(setting, options, chart, view = setting.view) {
16
+ const words = JUDGING_WORDS[setting.language];
17
+ const style = fill(words.styleName[setting.style], { count: setting.mats[0].seats.length });
18
+ const mats = setting.total === 1 ? words.say.matsOne : fill(words.say.matsMany, { n: setting.total });
19
+ const title = options.title ?? fill(words.say.title, { style, mats });
20
+ 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) }));
21
+ const description = options.description ?? [...sentences, view === "broadcast" ? words.say.board : chart && setting.seatNames ? words.say.names : words.say.chartOnly].join(setting.language === "ja" ? "" : " ");
22
+ return { title, description };
23
+ }
@@ -89,16 +89,16 @@ export function buildJudgingTurn(options, settings = {}) {
89
89
  const base = atTable ? tableTop : ft(H.post);
90
90
  if (!atTable)
91
91
  stands.push(...pole(`m${mat.n}-post-${a.seat.n}`, "pole", cx, cy, 0, base, ft(0.06), "u-pole"));
92
- const fit = fitted[si];
92
+ const fit = setting.seatNames ? fitted[si] : undefined;
93
93
  if (fit !== undefined) {
94
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 });
95
+ cards.push({ ...p, billboard: true, lean: true });
96
96
  }
97
97
  else {
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.
99
- const br = 0.55;
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
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 });
101
+ cards.push({ ...p, billboard: true, lean: true });
102
102
  }
103
103
  });
104
104
  // The mat's name on a sign over the whole table, on a pole at each end of it.
@@ -111,7 +111,7 @@ export function buildJudgingTurn(options, settings = {}) {
111
111
  const lines = text?.name ?? [mat.label];
112
112
  const size = text?.nameSize ?? signH * 0.3;
113
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("");
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 };
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 };
115
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"));
116
116
  items.push({ id: `m${mat.n}-furniture`, kind: "officials", at: [midX, midY], height: tableTop, planes: furniture });
117
117
  items.push({ id: `m${mat.n}-judges`, kind: "judges", at: [midX, midY], height: tableTop + cardH, planes: [...cards, ...stands, ...poles, sign] });
@@ -130,8 +130,8 @@ export function buildJudgingTurn(options, settings = {}) {
130
130
  language,
131
131
  options: surfaceLike,
132
132
  title,
133
- description: `${description}${language === "ja" ? "" : " "}${words.turn.standing}`,
134
- 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,
135
135
  floor,
136
136
  viewBox: { x: vx, y: vy, w: vw, h: vh },
137
137
  scale,
@@ -15,6 +15,8 @@ export interface JudgingMat {
15
15
  label?: string;
16
16
  /** Who sits where. A seat that is not named is shown as not assigned. */
17
17
  officials?: readonly JudgingOfficial[];
18
+ /** A line for the broadcast board's foot, before the style: `Round 2`, `Pool B`, `Final block`. At most 60 characters. */
19
+ line?: string;
18
20
  }
19
21
  /** A judging assignment as the drawing asks for it. */
20
22
  export interface JudgingOptions {
@@ -28,6 +30,16 @@ export interface JudgingOptions {
28
30
  chart?: "auto" | "always" | "never";
29
31
  /** 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
32
  sideJudges?: boolean;
33
+ /**
34
+ * 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
35
+ * 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.
36
+ */
37
+ seatNames?: boolean;
38
+ /**
39
+ * What is drawn: the `plan` with its chart (the default, the layout to print) or the `broadcast` picture: the mat large, as a television would show it, with the officials on a board at its side like a graphic, the seats' number
40
+ * badges where the officials sit, and no chart. The picture is 16:9 and scales as a whole, type and all, so it is as sharp on a tablet as on a television.
41
+ */
42
+ view?: "plan" | "broadcast";
31
43
  /** Where the chart is laid: beside the plan (`landscape`, the default) or under it (`portrait`). */
32
44
  orientation?: "landscape" | "portrait";
33
45
  /** 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. */
@@ -74,5 +86,7 @@ export interface JudgingResolved {
74
86
  /** From 1. */
75
87
  n: number;
76
88
  label: string;
89
+ /** The line for the broadcast board's foot; empty when there is none. */
90
+ line: string;
77
91
  seats: JudgingAssignment[];
78
92
  }
@@ -27,6 +27,7 @@ export interface JudgingWords {
27
27
  seatVacant: string;
28
28
  names: string;
29
29
  chartOnly: string;
30
+ board: string;
30
31
  join: string;
31
32
  };
32
33
  /** The table that says the same as the picture. */
@@ -40,6 +41,7 @@ export interface JudgingWords {
40
41
  /** What the 3D view says of the seats' name cards. */
41
42
  turn: {
42
43
  standing: string;
44
+ standingBadges: string;
43
45
  board: string;
44
46
  };
45
47
  }
@@ -13,10 +13,11 @@ export const JUDGING_WORDS = {
13
13
  seatVacant: "{seat} not assigned",
14
14
  names: "The names are in the seats where they fit, and in the chart of mat, seat, name and ID.",
15
15
  chartOnly: "The names are in the chart of mat, seat, name and ID.",
16
+ board: "The officials are listed on a board at the side of the mat, each with the number of the seat, name and ID; the numbers are on the seats.",
16
17
  join: "; ",
17
18
  },
18
19
  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" },
20
+ 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
21
  },
21
22
  ja: {
22
23
  mat: "第{n}試合場",
@@ -32,9 +33,10 @@ export const JUDGING_WORDS = {
32
33
  seatVacant: "{seat}は未定",
33
34
  names: "氏名は、席に収まる場合は各席の中に表示し、試合場・席・氏名・IDの一覧表にも載せています。",
34
35
  chartOnly: "氏名は、試合場・席・氏名・IDの一覧表に載せています。",
36
+ board: "審判員は、席の番号・氏名・IDとともに試合場の横の表示板に載せ、席の番号は各席に表示しています。",
35
37
  join: "、",
36
38
  },
37
39
  table: { caption: "{title}", mat: "試合場", seat: "席", name: "氏名", id: "ID" },
38
- turn: { standing: "各審判員の席に名札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", board: "試合場" },
40
+ turn: { standing: "各審判員の席に名札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", standingBadges: "各審判員の席に番号の丸い札を置き、見ている方向に向くようにしています。各試合場の名前は審判員席の上に掲げています。", board: "試合場" },
39
41
  },
40
42
  };
@@ -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.18.0";
2
+ export declare const VERSION = "0.19.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.18.0";
2
+ export const VERSION = "0.19.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",