@kerusiweb/core 2.0.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.
Files changed (95) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +68 -0
  3. package/dist/kerusi/index.d.ts +19 -0
  4. package/dist/kerusi/index.d.ts.map +1 -0
  5. package/dist/kerusi/index.js +19 -0
  6. package/dist/kerusi/index.js.map +1 -0
  7. package/dist/kerusi/kerusi-layout-mode.d.ts +58 -0
  8. package/dist/kerusi/kerusi-layout-mode.d.ts.map +1 -0
  9. package/dist/kerusi/kerusi-layout-mode.js +117 -0
  10. package/dist/kerusi/kerusi-layout-mode.js.map +1 -0
  11. package/dist/kerusi/kerusi-locale.d.ts +43 -0
  12. package/dist/kerusi/kerusi-locale.d.ts.map +1 -0
  13. package/dist/kerusi/kerusi-locale.js +111 -0
  14. package/dist/kerusi/kerusi-locale.js.map +1 -0
  15. package/dist/kerusi/kerusi-map.model.d.ts +258 -0
  16. package/dist/kerusi/kerusi-map.model.d.ts.map +1 -0
  17. package/dist/kerusi/kerusi-map.model.js +11 -0
  18. package/dist/kerusi/kerusi-map.model.js.map +1 -0
  19. package/dist/kerusi/kerusi-price.d.ts +22 -0
  20. package/dist/kerusi/kerusi-price.d.ts.map +1 -0
  21. package/dist/kerusi/kerusi-price.js +48 -0
  22. package/dist/kerusi/kerusi-price.js.map +1 -0
  23. package/dist/kerusi/kerusi-rows.d.ts +25 -0
  24. package/dist/kerusi/kerusi-rows.d.ts.map +1 -0
  25. package/dist/kerusi/kerusi-rows.js +36 -0
  26. package/dist/kerusi/kerusi-rows.js.map +1 -0
  27. package/dist/kerusi/kerusi-state.d.ts +85 -0
  28. package/dist/kerusi/kerusi-state.d.ts.map +1 -0
  29. package/dist/kerusi/kerusi-state.js +117 -0
  30. package/dist/kerusi/kerusi-state.js.map +1 -0
  31. package/dist/kerusi/kerusi-state.model.d.ts +79 -0
  32. package/dist/kerusi/kerusi-state.model.d.ts.map +1 -0
  33. package/dist/kerusi/kerusi-state.model.js +7 -0
  34. package/dist/kerusi/kerusi-state.model.js.map +1 -0
  35. package/dist/kerusi/kerusi-validator.d.ts +96 -0
  36. package/dist/kerusi/kerusi-validator.d.ts.map +1 -0
  37. package/dist/kerusi/kerusi-validator.js +525 -0
  38. package/dist/kerusi/kerusi-validator.js.map +1 -0
  39. package/dist/kerusi/kerusi-violation.d.ts +36 -0
  40. package/dist/kerusi/kerusi-violation.d.ts.map +1 -0
  41. package/dist/kerusi/kerusi-violation.js +13 -0
  42. package/dist/kerusi/kerusi-violation.js.map +1 -0
  43. package/dist/public-api.d.ts +4 -0
  44. package/dist/public-api.d.ts.map +1 -0
  45. package/dist/public-api.js +13 -0
  46. package/dist/public-api.js.map +1 -0
  47. package/dist/render/build-render-model.d.ts +36 -0
  48. package/dist/render/build-render-model.d.ts.map +1 -0
  49. package/dist/render/build-render-model.js +258 -0
  50. package/dist/render/build-render-model.js.map +1 -0
  51. package/dist/render/element-shapes.d.ts +34 -0
  52. package/dist/render/element-shapes.d.ts.map +1 -0
  53. package/dist/render/element-shapes.js +41 -0
  54. package/dist/render/element-shapes.js.map +1 -0
  55. package/dist/render/index.d.ts +14 -0
  56. package/dist/render/index.d.ts.map +1 -0
  57. package/dist/render/index.js +14 -0
  58. package/dist/render/index.js.map +1 -0
  59. package/dist/render/navigation-order.d.ts +49 -0
  60. package/dist/render/navigation-order.d.ts.map +1 -0
  61. package/dist/render/navigation-order.js +109 -0
  62. package/dist/render/navigation-order.js.map +1 -0
  63. package/dist/render/render-model.d.ts +151 -0
  64. package/dist/render/render-model.d.ts.map +1 -0
  65. package/dist/render/render-model.js +2 -0
  66. package/dist/render/render-model.js.map +1 -0
  67. package/dist/render/seat-shapes.d.ts +72 -0
  68. package/dist/render/seat-shapes.d.ts.map +1 -0
  69. package/dist/render/seat-shapes.js +161 -0
  70. package/dist/render/seat-shapes.js.map +1 -0
  71. package/dist/render/section-layout.d.ts +66 -0
  72. package/dist/render/section-layout.d.ts.map +1 -0
  73. package/dist/render/section-layout.js +173 -0
  74. package/dist/render/section-layout.js.map +1 -0
  75. package/dist/view/index.d.ts +12 -0
  76. package/dist/view/index.d.ts.map +1 -0
  77. package/dist/view/index.js +12 -0
  78. package/dist/view/index.js.map +1 -0
  79. package/dist/view/kerusi-seatmap-colors.d.ts +176 -0
  80. package/dist/view/kerusi-seatmap-colors.d.ts.map +1 -0
  81. package/dist/view/kerusi-seatmap-colors.js +215 -0
  82. package/dist/view/kerusi-seatmap-colors.js.map +1 -0
  83. package/dist/view/rtl.d.ts +17 -0
  84. package/dist/view/rtl.d.ts.map +1 -0
  85. package/dist/view/rtl.js +29 -0
  86. package/dist/view/rtl.js.map +1 -0
  87. package/dist/view/seat-aria.d.ts +44 -0
  88. package/dist/view/seat-aria.d.ts.map +1 -0
  89. package/dist/view/seat-aria.js +103 -0
  90. package/dist/view/seat-aria.js.map +1 -0
  91. package/dist/view/selection.d.ts +84 -0
  92. package/dist/view/selection.d.ts.map +1 -0
  93. package/dist/view/selection.js +98 -0
  94. package/dist/view/selection.js.map +1 -0
  95. package/package.json +42 -0
@@ -0,0 +1,151 @@
1
+ import { Accessibility, Element, Money, PriceTier, Seat, SeatType, Section } from '../kerusi/kerusi-map.model.js';
2
+ import { SectionLayoutMode } from '../kerusi/kerusi-layout-mode.js';
3
+ import { SeatStatus } from '../kerusi/kerusi-state.model.js';
4
+ /**
5
+ * The renderer's view of a Kerusi document pair: a `KerusiMap` with its
6
+ * `KerusiState` merged in, every reference resolved, every label localized, and
7
+ * every section carrying its own layout mode.
8
+ *
9
+ * This is deliberately not the source documents — a renderer should never have
10
+ * to re-resolve a price tier or re-read a locale map while drawing. It is also
11
+ * deliberately framework-agnostic: nothing in this folder imports a framework,
12
+ * so the model can be built in a test, a script, or on a server.
13
+ */
14
+ /** A seat's availability, from the merged state (§5.1). */
15
+ export type SeatRenderStatus = SeatStatus['status'];
16
+ export interface RenderSeat {
17
+ /** `Seat.id` — globally unique within the map, and the selection key. */
18
+ id: string;
19
+ /** The section this seat belongs to. */
20
+ sectionId: string;
21
+ /** `Seat.label`, defaulting to `id`. */
22
+ label: string;
23
+ /** `Seat.row` — a `RowMeta.id` or free text. */
24
+ row?: string;
25
+ /** The row's resolved display label, defaulting to `row`. */
26
+ rowLabel?: string;
27
+ /** The row's position among the section's rows, for vertical navigation. */
28
+ rowIndex: number;
29
+ /** `Seat.col` — the adjacency ordinal, meaningful in grid and mixed (§4.3.1). */
30
+ col?: number;
31
+ /** `Seat.x`, 0–100 percent of the section canvas. */
32
+ x?: number;
33
+ /** `Seat.y`, 0–100 percent of the section canvas. */
34
+ y?: number;
35
+ /** `Seat.rotation` in degrees. */
36
+ rotation?: number;
37
+ /** The resolved legend entry, not merely its id (§4.7). */
38
+ type: SeatType;
39
+ /** `SeatType.label` localized, defaulting to the type id. */
40
+ typeLabel: string;
41
+ /** `SeatType.color`, the spec's suggested render color. */
42
+ typeColor?: string;
43
+ /** Non-exclusive descriptive tags (§4.3.3). Never affects price. */
44
+ attributes: readonly string[];
45
+ /** Structured accessibility properties (§4.3.4). */
46
+ accessibility?: Accessibility;
47
+ /** Resolved per the §4.9 precedence order; absent when the seat is unpriced. */
48
+ price?: Money;
49
+ /** ids of seats that must be booked together (§4.6). Symmetric. */
50
+ companions: readonly string[];
51
+ /** From the merged state; `"available"` when the seat is absent (§5.1). */
52
+ status: SeatRenderStatus;
53
+ /** RFC 3339 (§5.1.1); meaningful when `status === "held"`. */
54
+ holdExpires?: string;
55
+ /** Whether this seat may currently be picked, per the caller's rules. */
56
+ selectable: boolean;
57
+ /** The originating document object, for callers that need `metadata`. */
58
+ source: Seat;
59
+ }
60
+ export interface RenderElement {
61
+ /** `Element.id` — REQUIRED by §4.4, and preserved here unlike in the adapter. */
62
+ id: string;
63
+ /** `Element.kind`: "screen" | "stage" | "exit" | "lavatory" | "gap" | ... */
64
+ kind: string;
65
+ label?: string;
66
+ row?: string;
67
+ rowIndex?: number;
68
+ col?: number;
69
+ x?: number;
70
+ y?: number;
71
+ width?: number;
72
+ height?: number;
73
+ rotation?: number;
74
+ source: Element;
75
+ }
76
+ /** A row within a section: metadata plus the seats that reference it. */
77
+ export interface RenderRow {
78
+ /** `RowMeta.id`, the seat's `row` value, or `''` for seats with no row. */
79
+ id: string;
80
+ /** The display label, defaulting to `id`. */
81
+ label?: string;
82
+ /** Position among the section's rows, after the §4.2.1 ordering. */
83
+ index: number;
84
+ /** The seats in this row, ordered by `col` (grid/mixed) or `x` (freeform). */
85
+ seats: readonly RenderSeat[];
86
+ /**
87
+ * An **empty row** (§4.2.2): declared by `Section.rows`, referenced by no
88
+ * seat, and still occupying a slot — it reserves the vertical space one row
89
+ * of seats would. This is what a document uses to open the throw between a
90
+ * cinema screen and row A, or a cross-aisle mid-section.
91
+ */
92
+ empty: boolean;
93
+ }
94
+ /** A `Direction` (§4.10) with both ends localized. Informational only. */
95
+ export interface RenderDirection {
96
+ axis: 'row' | 'col' | 'x' | 'y';
97
+ low: string;
98
+ high: string;
99
+ }
100
+ export interface RenderSection {
101
+ id: string;
102
+ /** `Section.label` localized; absent when the section declares none. */
103
+ label?: string;
104
+ /** The resolved mode — declared, or inferred per §4.5. */
105
+ layoutMode: SectionLayoutMode;
106
+ /** "width:height"; meaningful for freeform and mixed sections. */
107
+ aspectRatio: string;
108
+ /** Rows in display order. Grouping only — seats do not live inside rows. */
109
+ rows: readonly RenderRow[];
110
+ /** Every seat in the section, flat, in row-then-position order. */
111
+ seats: readonly RenderSeat[];
112
+ /** Non-bookable features (§4.4). */
113
+ elements: readonly RenderElement[];
114
+ /**
115
+ * `Section.directions` localized (§4.10) — what an axis means in the physical
116
+ * world. Informational only: nothing in this library reads it to decide
117
+ * layout or screen placement, exactly as nothing branches on `RenderMap.domain`.
118
+ */
119
+ directions?: readonly RenderDirection[];
120
+ source: Section;
121
+ }
122
+ export interface RenderMap {
123
+ id: string;
124
+ /** `KerusiMap.name`, for the map's accessible label. */
125
+ name?: string;
126
+ /** The resolved BCP-47 locale everything above was localized against. */
127
+ locale: string;
128
+ /** `KerusiMap.domain` — informational only; never used to reject or branch. */
129
+ domain?: string;
130
+ /** Sections in `Section.index` order. */
131
+ sections: readonly RenderSection[];
132
+ /** The legend, with labels localized, for a key or legend component. */
133
+ legend: readonly RenderLegendEntry[];
134
+ /** The map's single currency, when any seat or tier is priced (§4.9). */
135
+ currency?: string;
136
+ /** Every seat across every section, keyed by id — the merge and lookup index. */
137
+ seatsById: ReadonlyMap<string, RenderSeat>;
138
+ }
139
+ /** A legend row: a seat type with its label and default price resolved. */
140
+ export interface RenderLegendEntry {
141
+ id: string;
142
+ label: string;
143
+ color?: string;
144
+ /** The type's default tier, resolved (§4.7 → §4.8). */
145
+ defaultTier?: PriceTier;
146
+ /** That tier's price, for a legend that shows prices. */
147
+ price?: Money;
148
+ /** How many seats in the map carry this type. */
149
+ seatCount: number;
150
+ }
151
+ //# sourceMappingURL=render-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-model.d.ts","sourceRoot":"","sources":["../../src/render/render-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=render-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-model.js","sourceRoot":"","sources":["../../src/render/render-model.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The seat glyph: the marks drawn inside a placed seat's box.
3
+ *
4
+ * A seat's box is always a rounded square, so the box alone cannot say which
5
+ * way the seat faces. These shapes do. Every coordinate is relative to the
6
+ * placed box, and **front is the top edge** when `rotation` is 0 — the seat
7
+ * `<g>` carries the rotation, so a glyph drawn to that convention turns with it
8
+ * for free.
9
+ *
10
+ * Kept here, next to `element-shapes`, rather than inlined in the template: the
11
+ * ratios below are the whole design, and they are worth testing.
12
+ */
13
+ /**
14
+ * The seat outline: a rounded trapezoid, square-ish at the front and tapered
15
+ * at the back, so which way a seat faces is legible from the outline alone —
16
+ * no separate frame mark is drawn on top of it.
17
+ *
18
+ * `inset` shrinks the whole shape uniformly (used to derive the selected
19
+ * seat's core, which must stay just inside this outline). It shrinks the
20
+ * bounding box rather than offsetting each edge along its own normal, so the
21
+ * tapered sides move in slightly less than the front and back do — at a 0.12
22
+ * taper that is a ~0.7% (cos 7°) difference, well under a viewBox unit and not
23
+ * worth a true polygon offset for.
24
+ *
25
+ * Because a seat is square, an inset here is exactly a scaled copy about the
26
+ * centre: `seatBodyPath(x, y, s, s, k)` and `seatBodyPath(x + k, y + k, s - 2k,
27
+ * s - 2k)` emit the same `d`. {@link seatSelectedFrame} leans on that.
28
+ */
29
+ export declare function seatBodyPath(x: number, y: number, width: number, height: number, inset?: number): string;
30
+ /**
31
+ * The width of the frame around a selected seat's core — feed it straight to
32
+ * {@link seatBodyPath}'s `inset` to get the core.
33
+ *
34
+ * A seat is always square: `section-layout` places every one of them as
35
+ * `size × size`, in both the grid and the freeform mode. That makes the inset
36
+ * shape an *exact* scaled copy of the body about its centre, not merely a
37
+ * similar-looking one, so any mark that clears the body also clears the core
38
+ * when it is drawn to the core's own box. That is what lets the frame width
39
+ * stay a free tuning knob: nothing downstream has to be re-checked when it
40
+ * changes.
41
+ */
42
+ export declare function seatSelectedFrame(width: number, height: number): number;
43
+ /**
44
+ * @deprecated Renamed to {@link seatSelectedFrame}. The selected mark is a
45
+ * frame around a filled core now, not a stroked ring, so "stroke" no longer
46
+ * describes what the number is for.
47
+ */
48
+ export declare const seatRingStroke: typeof seatSelectedFrame;
49
+ /**
50
+ * A person seated in the seat, head toward the front: the non-colour cue for
51
+ * "you have this one", "someone is holding this one" or "someone has this
52
+ * one" — see {@link seatFill}'s note on why colour no longer carries status.
53
+ *
54
+ * Head and torso are two subpaths of one path, so the silhouette is a single
55
+ * node taking a single fill (or, for the held/"in progress" variant, a single
56
+ * stroke). It is drawn *without* the label's counter-rotation, so the
57
+ * occupant leans with the seat and reinforces which way it faces.
58
+ *
59
+ * It clears the tapered body on both axes: the torso spans 0.28–0.72 of the
60
+ * width, well inside the back edge, which even at its narrowest (a 0.12
61
+ * taper) is still 0.12–0.88. The margin is slim at the two hip corners,
62
+ * though, so pass the box the figure is meant to sit *on*: a selected seat
63
+ * draws it to the core's box, not the seat's, or the hips cross the frame.
64
+ *
65
+ * The ratios assume a square box, which is what `section-layout` always
66
+ * places. On a wide box the head, sized from the width, would outgrow the
67
+ * height.
68
+ */
69
+ export declare function seatOccupantPath(x: number, y: number, width: number, height: number): string;
70
+ /** The stroke width for the held variant of {@link seatOccupantPath} — drawn hollow. */
71
+ export declare function seatOccupantStroke(width: number, height: number): number;
72
+ //# sourceMappingURL=seat-shapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seat-shapes.d.ts","sourceRoot":"","sources":["../../src/render/seat-shapes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA4DH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,SAAI,GACR,MAAM,CAgBR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,0BAAoB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAwB5F;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAExE"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The seat glyph: the marks drawn inside a placed seat's box.
3
+ *
4
+ * A seat's box is always a rounded square, so the box alone cannot say which
5
+ * way the seat faces. These shapes do. Every coordinate is relative to the
6
+ * placed box, and **front is the top edge** when `rotation` is 0 — the seat
7
+ * `<g>` carries the rotation, so a glyph drawn to that convention turns with it
8
+ * for free.
9
+ *
10
+ * Kept here, next to `element-shapes`, rather than inlined in the template: the
11
+ * ratios below are the whole design, and they are worth testing.
12
+ */
13
+ /**
14
+ * Rounds a coordinate for the `d` attribute.
15
+ *
16
+ * The ratios below are decimals, so 100 * 0.28 lands on 28.000000000000004.
17
+ * Three places is far finer than a viewBox unit ever renders and keeps the
18
+ * emitted path readable.
19
+ */
20
+ function r(value) {
21
+ return Math.round(value * 1000) / 1000;
22
+ }
23
+ /** How far the back edge tapers in, per side, as a fraction of the front width. */
24
+ const BACK_TAPER = 0.12;
25
+ /** Corner radius at the front (square-ish), as a fraction of the short edge. */
26
+ const FRONT_RADIUS = 0.08;
27
+ /** Corner radius at the back (soft, so the taper reads as a back), same units. */
28
+ const BACK_RADIUS = 0.34;
29
+ /** The selected seat's frame, as a fraction of the seat's short edge. */
30
+ const SELECTED_FRAME = 0.1;
31
+ /** The held figure's outline stroke, same units. */
32
+ const OCCUPANT_STROKE = 0.05;
33
+ /**
34
+ * A closed path through `points`, each corner rounded to its own radius.
35
+ *
36
+ * For each vertex: step back along the incoming edge by `radius`, then arc to
37
+ * a point the same distance along the outgoing edge. A radius is clamped to
38
+ * half the shorter of its two adjacent edges, so a tiny seat cannot fold a
39
+ * corner back on itself. `points` must run clockwise in screen coordinates
40
+ * (y down) — that is what makes every corner the same `sweep-flag`.
41
+ */
42
+ function roundedPolygon(points, radii) {
43
+ const n = points.length;
44
+ const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
45
+ const along = (from, to, d) => {
46
+ const len = dist(from, to);
47
+ if (len === 0)
48
+ return from;
49
+ const t = d / len;
50
+ return [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t];
51
+ };
52
+ let d = '';
53
+ for (let i = 0; i < n; i++) {
54
+ const prev = points[(i - 1 + n) % n];
55
+ const curr = points[i];
56
+ const next = points[(i + 1) % n];
57
+ const radius = Math.min(radii[i], dist(prev, curr) / 2, dist(curr, next) / 2);
58
+ const a = along(curr, prev, radius);
59
+ const b = along(curr, next, radius);
60
+ d += i === 0 ? `M ${r(a[0])} ${r(a[1])} ` : `L ${r(a[0])} ${r(a[1])} `;
61
+ d += `A ${r(radius)} ${r(radius)} 0 0 1 ${r(b[0])} ${r(b[1])} `;
62
+ }
63
+ return `${d.trim()} Z`;
64
+ }
65
+ /**
66
+ * The seat outline: a rounded trapezoid, square-ish at the front and tapered
67
+ * at the back, so which way a seat faces is legible from the outline alone —
68
+ * no separate frame mark is drawn on top of it.
69
+ *
70
+ * `inset` shrinks the whole shape uniformly (used to derive the selected
71
+ * seat's core, which must stay just inside this outline). It shrinks the
72
+ * bounding box rather than offsetting each edge along its own normal, so the
73
+ * tapered sides move in slightly less than the front and back do — at a 0.12
74
+ * taper that is a ~0.7% (cos 7°) difference, well under a viewBox unit and not
75
+ * worth a true polygon offset for.
76
+ *
77
+ * Because a seat is square, an inset here is exactly a scaled copy about the
78
+ * centre: `seatBodyPath(x, y, s, s, k)` and `seatBodyPath(x + k, y + k, s - 2k,
79
+ * s - 2k)` emit the same `d`. {@link seatSelectedFrame} leans on that.
80
+ */
81
+ export function seatBodyPath(x, y, width, height, inset = 0) {
82
+ const ix = x + inset;
83
+ const iy = y + inset;
84
+ const iw = width - inset * 2;
85
+ const ih = height - inset * 2;
86
+ const edge = Math.min(iw, ih);
87
+ const taper = iw * BACK_TAPER;
88
+ const points = [
89
+ [ix, iy], // front-left
90
+ [ix + iw, iy], // front-right
91
+ [ix + iw - taper, iy + ih], // back-right
92
+ [ix + taper, iy + ih], // back-left
93
+ ];
94
+ const radii = [edge * FRONT_RADIUS, edge * FRONT_RADIUS, edge * BACK_RADIUS, edge * BACK_RADIUS];
95
+ return roundedPolygon(points, radii);
96
+ }
97
+ /**
98
+ * The width of the frame around a selected seat's core — feed it straight to
99
+ * {@link seatBodyPath}'s `inset` to get the core.
100
+ *
101
+ * A seat is always square: `section-layout` places every one of them as
102
+ * `size × size`, in both the grid and the freeform mode. That makes the inset
103
+ * shape an *exact* scaled copy of the body about its centre, not merely a
104
+ * similar-looking one, so any mark that clears the body also clears the core
105
+ * when it is drawn to the core's own box. That is what lets the frame width
106
+ * stay a free tuning knob: nothing downstream has to be re-checked when it
107
+ * changes.
108
+ */
109
+ export function seatSelectedFrame(width, height) {
110
+ return Math.min(width, height) * SELECTED_FRAME;
111
+ }
112
+ /**
113
+ * @deprecated Renamed to {@link seatSelectedFrame}. The selected mark is a
114
+ * frame around a filled core now, not a stroked ring, so "stroke" no longer
115
+ * describes what the number is for.
116
+ */
117
+ export const seatRingStroke = seatSelectedFrame;
118
+ /**
119
+ * A person seated in the seat, head toward the front: the non-colour cue for
120
+ * "you have this one", "someone is holding this one" or "someone has this
121
+ * one" — see {@link seatFill}'s note on why colour no longer carries status.
122
+ *
123
+ * Head and torso are two subpaths of one path, so the silhouette is a single
124
+ * node taking a single fill (or, for the held/"in progress" variant, a single
125
+ * stroke). It is drawn *without* the label's counter-rotation, so the
126
+ * occupant leans with the seat and reinforces which way it faces.
127
+ *
128
+ * It clears the tapered body on both axes: the torso spans 0.28–0.72 of the
129
+ * width, well inside the back edge, which even at its narrowest (a 0.12
130
+ * taper) is still 0.12–0.88. The margin is slim at the two hip corners,
131
+ * though, so pass the box the figure is meant to sit *on*: a selected seat
132
+ * draws it to the core's box, not the seat's, or the hips cross the frame.
133
+ *
134
+ * The ratios assume a square box, which is what `section-layout` always
135
+ * places. On a wide box the head, sized from the width, would outgrow the
136
+ * height.
137
+ */
138
+ export function seatOccupantPath(x, y, width, height) {
139
+ const cx = r(x + width * 0.5);
140
+ const headY = r(y + height * 0.3);
141
+ const radius = r(width * 0.155);
142
+ // Two 180° arcs, because an SVG circle cannot be a subpath.
143
+ const head = `M ${r(cx - radius)} ${headY} ` +
144
+ `a ${radius} ${radius} 0 1 1 ${r(radius * 2)} 0 ` +
145
+ `a ${radius} ${radius} 0 1 1 ${r(-radius * 2)} 0 Z`;
146
+ const shoulderY = r(y + height * 0.5);
147
+ const hipY = r(y + height * 0.86);
148
+ const leftX = r(x + width * 0.28);
149
+ const rightX = r(x + width * 0.72);
150
+ const easeY = r(y + height * 0.58);
151
+ const shoulderIn = width * 0.08;
152
+ const torso = `M ${leftX} ${hipY} ` +
153
+ `C ${leftX} ${easeY} ${r(leftX + shoulderIn)} ${shoulderY} ${cx} ${shoulderY} ` +
154
+ `C ${r(rightX - shoulderIn)} ${shoulderY} ${rightX} ${easeY} ${rightX} ${hipY} Z`;
155
+ return `${head} ${torso}`;
156
+ }
157
+ /** The stroke width for the held variant of {@link seatOccupantPath} — drawn hollow. */
158
+ export function seatOccupantStroke(width, height) {
159
+ return Math.min(width, height) * OCCUPANT_STROKE;
160
+ }
161
+ //# sourceMappingURL=seat-shapes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seat-shapes.js","sourceRoot":"","sources":["../../src/render/seat-shapes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { RenderElement, RenderSeat, RenderSection } from './render-model.js';
2
+ /**
3
+ * Section geometry: turning a `RenderSection` into placed rectangles.
4
+ *
5
+ * Each section computes its own *intrinsic* size — a grid section is as wide as
6
+ * its widest row and as tall as its row count; a freeform section takes its
7
+ * proportions from `Section.aspectRatio`. The component then hands that size to
8
+ * the SVG as a `viewBox` and lets CSS scale it. The pre-1.0 renderer instead
9
+ * stretched every layout to fill a fixed 500x500 canvas, which squashed a wide
10
+ * auditorium into flattened rectangles.
11
+ */
12
+ export interface PlacedSeat {
13
+ seat: RenderSeat;
14
+ /** Top-left corner, in viewBox units. */
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ centerX: number;
20
+ centerY: number;
21
+ fontSize: number;
22
+ /** Degrees about the seat center; 0 unless the document asked for a tilt. */
23
+ rotation: number;
24
+ }
25
+ export interface PlacedElement {
26
+ element: RenderElement;
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ centerX: number;
32
+ centerY: number;
33
+ fontSize: number;
34
+ rotation: number;
35
+ }
36
+ export interface SectionLayout {
37
+ /** Intrinsic viewBox width. */
38
+ width: number;
39
+ /** Intrinsic viewBox height. */
40
+ height: number;
41
+ seats: readonly PlacedSeat[];
42
+ elements: readonly PlacedElement[];
43
+ }
44
+ export interface SectionLayoutOptions {
45
+ /** Grid cell edge, in viewBox units. */
46
+ seatSize?: number;
47
+ /** Gap between adjacent cells, in viewBox units. */
48
+ seatGap?: number;
49
+ /** Freeform viewBox width; height follows from the aspect ratio. */
50
+ freeformBasis?: number;
51
+ /** Overrides `Section.aspectRatio`. */
52
+ aspectRatio?: string;
53
+ /** Mirrors the grid horizontally, for right-to-left locales. */
54
+ rtl?: boolean;
55
+ }
56
+ /**
57
+ * Places a section's seats and elements. Dispatches on the section's own
58
+ * resolved layout mode, so a map may mix grid, freeform and mixed sections —
59
+ * the pre-1.0 adapter collapsed the whole map to a single mode.
60
+ *
61
+ * `mixed` is laid out as freeform: §4.3.1 makes `x`/`y` authoritative for
62
+ * visual placement when both are present. Its `col` survives on the seat and is
63
+ * consumed by `navigation-order.ts`.
64
+ */
65
+ export declare function computeSectionLayout(section: RenderSection, options?: SectionLayoutOptions): SectionLayout;
66
+ //# sourceMappingURL=section-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-layout.d.ts","sourceRoot":"","sources":["../../src/render/section-layout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE7E;;;;;;;;;GASG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,CAAC;IACjB,yCAAyC;IACzC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAWD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE,oBAAyB,GACjC,aAAa,CAIf"}
@@ -0,0 +1,173 @@
1
+ const DEFAULTS = {
2
+ seatSize: 28,
3
+ seatGap: 6,
4
+ freeformBasis: 1000,
5
+ };
6
+ /** Padding around the seating area, as a multiple of the seat size. */
7
+ const PADDING_RATIO = 0.5;
8
+ /**
9
+ * Places a section's seats and elements. Dispatches on the section's own
10
+ * resolved layout mode, so a map may mix grid, freeform and mixed sections —
11
+ * the pre-1.0 adapter collapsed the whole map to a single mode.
12
+ *
13
+ * `mixed` is laid out as freeform: §4.3.1 makes `x`/`y` authoritative for
14
+ * visual placement when both are present. Its `col` survives on the seat and is
15
+ * consumed by `navigation-order.ts`.
16
+ */
17
+ export function computeSectionLayout(section, options = {}) {
18
+ return section.layoutMode === 'grid'
19
+ ? computeGridLayout(section, options)
20
+ : computeFreeformLayout(section, options);
21
+ }
22
+ // --- grid -------------------------------------------------------------------
23
+ /**
24
+ * Places seats and elements on the `(row, col)` lattice.
25
+ *
26
+ * Both axes reserve space the same way. A column no seat occupies is simply
27
+ * empty space — §4.3.2's "no filler objects": the gap that is an aisle needs no
28
+ * placeholder seat, and none is synthesized. A row no seat occupies is an empty
29
+ * row the document declared (§4.2.2), already materialized in `section.rows`,
30
+ * so it costs a full row pitch here without this function knowing it is empty.
31
+ * That is what opens the throw between a cinema screen and row A.
32
+ */
33
+ function computeGridLayout(section, options) {
34
+ const size = options.seatSize ?? DEFAULTS.seatSize;
35
+ const gap = options.seatGap ?? DEFAULTS.seatGap;
36
+ const pitch = size + gap;
37
+ const padding = size * PADDING_RATIO;
38
+ const columns = columnExtent(section);
39
+ const rowCount = Math.max(section.rows.length, 1);
40
+ const width = padding * 2 + columns.count * pitch - gap;
41
+ const height = padding * 2 + rowCount * pitch - gap;
42
+ /** Column index → x, mirrored when the locale reads right-to-left. */
43
+ const colX = (col) => {
44
+ const offset = options.rtl ? columns.max - col : col - columns.min;
45
+ return padding + offset * pitch;
46
+ };
47
+ const seats = [];
48
+ for (const row of section.rows) {
49
+ const y = padding + row.index * pitch;
50
+ row.seats.forEach((seat, i) => {
51
+ // A grid seat is required to carry `col` (§4.5); fall back to its index
52
+ // so an unvalidated document still draws something in order.
53
+ const x = colX(seat.col ?? columns.min + i);
54
+ seats.push(box(seat, x, y, size, size, seat.rotation ?? 0));
55
+ });
56
+ }
57
+ const elements = section.elements.map((element) => {
58
+ // §4.4.1: in grid mode width/height are column and row spans in cells, not
59
+ // percentages — a two-seat-wide lavatory is `{ col: 1, width: 2 }`, and a
60
+ // screen filling two empty rows is `{ row: "screen-1", height: 2 }`. The
61
+ // spec requires positive integers; a fractional span is an
62
+ // `element-span-invalid` the validator rejects, and is rounded here so an
63
+ // unvalidated document still draws something.
64
+ const spanY = gridSpan(element.height);
65
+ // Omitting `col` spans the section's full column extent and ignores
66
+ // `width` — the usual form for a screen or a stage (§4.4.1).
67
+ const elWidth = element.col !== undefined
68
+ ? gridSpan(element.width) * pitch - gap
69
+ : columns.count * pitch - gap;
70
+ const elHeight = spanY * pitch - gap;
71
+ // An element carrying x/y in a grid section violates §4.4.1 and the
72
+ // validator rejects it; it still places, by percentage of the section box,
73
+ // rather than collapsing into the corner.
74
+ const x = element.col !== undefined
75
+ ? colX(element.col)
76
+ : element.x !== undefined
77
+ ? (element.x / 100) * width - elWidth / 2
78
+ : padding;
79
+ const y = element.rowIndex !== undefined
80
+ ? padding + element.rowIndex * pitch
81
+ : element.y !== undefined
82
+ ? (element.y / 100) * height - elHeight / 2
83
+ : padding;
84
+ return elementBox(element, x, y, elWidth, elHeight);
85
+ });
86
+ return { width, height, seats, elements };
87
+ }
88
+ /**
89
+ * A grid `width`/`height`, normalized. §4.4.1 makes these positive integers
90
+ * defaulting to 1; rounding rather than rejecting keeps an invalid document
91
+ * drawable, since the validator is what refuses it.
92
+ */
93
+ function gridSpan(span) {
94
+ return Math.max(Math.round(span ?? 1), 1);
95
+ }
96
+ /** The lowest and highest column any seat occupies, and how many that spans. */
97
+ function columnExtent(section) {
98
+ const cols = section.seats
99
+ .map((s) => s.col)
100
+ .filter((c) => c !== undefined && c !== null);
101
+ // An element's `width` is a column span in grid mode, so a two-cell-wide
102
+ // lavatory at column 4 reaches column 5 and the canvas must cover it.
103
+ const elementCols = section.elements
104
+ .filter((e) => e.col !== undefined && e.col !== null)
105
+ .flatMap((e) => [e.col, e.col + gridSpan(e.width) - 1]);
106
+ const all = [...cols, ...elementCols];
107
+ if (all.length === 0) {
108
+ const fallback = Math.max(...section.rows.map((r) => r.seats.length), 1);
109
+ return { min: 0, max: fallback - 1, count: fallback };
110
+ }
111
+ const min = Math.min(...all);
112
+ const max = Math.max(...all);
113
+ return { min, max, count: max - min + 1 };
114
+ }
115
+ // --- freeform / mixed -------------------------------------------------------
116
+ /**
117
+ * Places seats from percentage coordinates inside a box of the section's
118
+ * aspect ratio. Because the box *is* the viewBox, percentages map linearly and
119
+ * no letterboxing is needed — the SVG's `preserveAspectRatio` handles fitting
120
+ * the box into whatever space CSS gives it.
121
+ */
122
+ function computeFreeformLayout(section, options) {
123
+ const width = options.freeformBasis ?? DEFAULTS.freeformBasis;
124
+ const height = width / parseAspectRatio(options.aspectRatio ?? section.aspectRatio);
125
+ const size = options.seatSize ?? Math.min(width, height) * 0.05;
126
+ const seats = section.seats.map((seat) => {
127
+ const centerX = ((seat.x ?? 0) / 100) * width;
128
+ const centerY = ((seat.y ?? 0) / 100) * height;
129
+ return box(seat, centerX - size / 2, centerY - size / 2, size, size, seat.rotation ?? 0);
130
+ });
131
+ const elements = section.elements.map((element) => {
132
+ // In freeform mode width/height are percentages of the section box.
133
+ const elWidth = ((element.width ?? 6) / 100) * width;
134
+ const elHeight = ((element.height ?? 6) / 100) * height;
135
+ const centerX = ((element.x ?? 50) / 100) * width;
136
+ const centerY = ((element.y ?? 50) / 100) * height;
137
+ return elementBox(element, centerX - elWidth / 2, centerY - elHeight / 2, elWidth, elHeight);
138
+ });
139
+ return { width, height, seats, elements };
140
+ }
141
+ /** "16:9" → 1.777…. Falls back to square for anything unparseable. */
142
+ function parseAspectRatio(ratio) {
143
+ const [w, h] = ratio.split(':').map(Number);
144
+ return w > 0 && h > 0 ? w / h : 1;
145
+ }
146
+ // --- shared -----------------------------------------------------------------
147
+ function box(seat, x, y, width, height, rotation) {
148
+ return {
149
+ seat,
150
+ x,
151
+ y,
152
+ width,
153
+ height,
154
+ centerX: x + width / 2,
155
+ centerY: y + height / 2,
156
+ fontSize: width * 0.4,
157
+ rotation,
158
+ };
159
+ }
160
+ function elementBox(element, x, y, width, height) {
161
+ return {
162
+ element,
163
+ x,
164
+ y,
165
+ width,
166
+ height,
167
+ centerX: x + width / 2,
168
+ centerY: y + height / 2,
169
+ fontSize: Math.max(Math.min(height * 0.5, width * 0.18), 6),
170
+ rotation: element.rotation ?? 0,
171
+ };
172
+ }
173
+ //# sourceMappingURL=section-layout.js.map