@tinylap/track-format 0.0.0-stage → 0.1.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,49 @@
1
+ // The TinyLap track data model. Cells are [col, row], with rows increasing
2
+ // downward like SVG; one cell is one 300 mm Circuit 30 mat.
3
+
4
+ export type Dir = 'N' | 'E' | 'S' | 'W';
5
+ export type Corner = 'NE' | 'SE' | 'SW' | 'NW';
6
+ /** Clockwise on screen. layoutFromData refuses anything else. */
7
+ export type Rotation = 0 | 90 | 180 | 270;
8
+ export type Cell = [number, number];
9
+ export type PieceType = 'straight' | 'cornerR30' | 'cornerR60';
10
+
11
+ export interface Port { cells: Cell[]; dir: Dir }
12
+
13
+ export interface CapWall { kind: 'cap'; vertex: Cell }
14
+ export interface ArcWall { kind: 'arc' | 'arcLarge'; r: number; cell: Cell; corner: Corner }
15
+ export type Wall = CapWall | ArcWall;
16
+
17
+ export interface MatCounts { square?: number; quarterRound?: number; arcLarge?: number }
18
+
19
+ export interface PieceDef {
20
+ label: string;
21
+ cells: Cell[];
22
+ ports: Port[];
23
+ walls: Wall[];
24
+ mats: MatCounts;
25
+ centrelineMm: number;
26
+ }
27
+
28
+ export interface PlacedPiece { type: PieceType; x: number; y: number; rot: Rotation }
29
+ export interface Room { w: number; h: number }
30
+
31
+ /** The persisted layout, format version 1. */
32
+ export interface Layout {
33
+ version: 1;
34
+ unit: 'mat';
35
+ room: Room | null;
36
+ pieces: PlacedPiece[];
37
+ }
38
+
39
+ export interface BBox { x: number; y: number; w: number; h: number }
40
+
41
+ export interface Metrics {
42
+ bbox: BBox | null;
43
+ matCount: number;
44
+ mats: Record<string, number>;
45
+ lapMm: number;
46
+ }
47
+
48
+ export interface BarrierCounts { straight: number; curve: number; curveLarge: number; cornerCap: number }
49
+ export interface Barriers { counts: BarrierCounts; straightEdgeIds: string[]; arcs: ArcWall[] }
package/geometry.js ADDED
@@ -0,0 +1,184 @@
1
+ // SPDX-FileCopyrightText: 2026 The TinyLap contributors
2
+ //
3
+ // SPDX-License-Identifier: MPL-2.0
4
+
5
+ // Kyosho Mini-Z Grand Prix Circuit 30 geometry.
6
+ // Cells are [col, row] with row increasing downward, matching SVG.
7
+ // Rotation is clockwise on screen: 0, 90, 180, 270.
8
+
9
+ /** @import { Dir, Corner, Rotation, Cell, PieceType, PieceDef, PlacedPiece, Port, Wall } from './types.js' */
10
+
11
+ export const MAT_MM = 300;
12
+
13
+ /** @type {Dir[]} */
14
+ const DIRS = ['N', 'E', 'S', 'W'];
15
+ /** @type {Corner[]} */
16
+ const CORNERS = ['NE', 'SE', 'SW', 'NW'];
17
+
18
+ /** @param {number} rot */
19
+ const steps = (rot) => (((rot / 90) % 4) + 4) % 4;
20
+
21
+ /** @param {Dir} dir @param {number} rot @returns {Dir} */
22
+ export function rotateDir(dir, rot) {
23
+ return DIRS[(DIRS.indexOf(dir) + steps(rot)) % 4];
24
+ }
25
+
26
+ /** @param {Corner} corner @param {number} rot @returns {Corner} */
27
+ export function rotateCorner(corner, rot) {
28
+ return CORNERS[(CORNERS.indexOf(corner) + steps(rot)) % 4];
29
+ }
30
+
31
+ // A vertex is a point: (x, y) -> (-y, x) per quarter turn.
32
+ /** @param {Cell} vertex @param {number} rot @returns {Cell} */
33
+ export function rotateVertex([x, y], rot) {
34
+ /** @type {Cell} */
35
+ let p = [x, y];
36
+ for (let i = steps(rot); i > 0; i--) p = [-p[1], p[0]];
37
+ return /** @type {Cell} */ (p.map(v => v === 0 ? 0 : v));
38
+ }
39
+
40
+ // A cell is the unit square (x, y)..(x+1, y+1). Rotating those corners
41
+ // yields the square indexed (-y-1, x).
42
+ /** @param {Cell} cell @param {number} rot @returns {Cell} */
43
+ export function rotateCell([x, y], rot) {
44
+ /** @type {Cell} */
45
+ let p = [x, y];
46
+ for (let i = steps(rot); i > 0; i--) p = [-p[1] - 1, p[0]];
47
+ return p;
48
+ }
49
+
50
+ const QUARTER_TURN_MM = (Math.PI / 2) * MAT_MM;
51
+
52
+ /** @type {Record<PieceType, PieceDef>} */
53
+ export const PIECES = {
54
+ straight: {
55
+ label: 'Straight',
56
+ cells: [[0, 0], [1, 0]],
57
+ ports: [
58
+ { cells: [[0, 0], [1, 0]], dir: 'N' },
59
+ { cells: [[0, 0], [1, 0]], dir: 'S' },
60
+ ],
61
+ walls: [],
62
+ mats: { square: 2 },
63
+ centrelineMm: MAT_MM,
64
+ },
65
+
66
+ cornerR30: {
67
+ label: 'Corner R30',
68
+ cells: [[0, 0], [1, 0], [0, 1], [1, 1]],
69
+ ports: [
70
+ { cells: [[0, 0], [1, 0]], dir: 'N' },
71
+ { cells: [[1, 0], [1, 1]], dir: 'E' },
72
+ ],
73
+ walls: [
74
+ { kind: 'arc', r: 1, cell: [0, 1], corner: 'SW' },
75
+ { kind: 'cap', vertex: [2, 0] },
76
+ ],
77
+ mats: { square: 3, quarterRound: 1 },
78
+ centrelineMm: QUARTER_TURN_MM,
79
+ },
80
+
81
+ cornerR60: { // enter N, exit W
82
+ label: 'Corner R60',
83
+ // The COMPLETE placeable corner, not just the kit's contribution.
84
+ //
85
+ // 87033-02 supplies only the inside of the turn: three mats (two
86
+ // shaped, one plain) and two curve halves, with no outside barriers
87
+ // at all. Those three alone leave a corridor one mat wide, which is
88
+ // not a usable corner — so the piece placed on the canvas is that
89
+ // inner assembly plus an outer layer of plain mats and barriers from
90
+ // a base set, bringing the corridor back to the fixed two mats.
91
+ // This is why the kit's manual says it cannot be used alone.
92
+ cells: [
93
+ [1, 0], [2, 0], // 3x3 minus the swept
94
+ [0, 1], [1, 1], [2, 1], // inner cell [0,0]
95
+ [0, 2], [1, 2], [2, 2],
96
+ ],
97
+ ports: [
98
+ { cells: [[1, 0], [2, 0]], dir: 'N' },
99
+ { cells: [[0, 1], [0, 2]], dir: 'W' },
100
+ ],
101
+ walls: [
102
+ // Inside: the kit's large sweep. An ordinary quarter-circle arc centred
103
+ // on the turn's centre, concentric with the corridor's centreline and
104
+ // tangent to the adjoining straights.
105
+ { kind: 'arcLarge', r: 1, cell: [0, 0], corner: 'SE' },
106
+ // Outside: an ordinary quarter-round from the base set, as on any
107
+ // other corner. The kit supplies nothing for this edge.
108
+ { kind: 'arc', r: 1, cell: [2, 2], corner: 'SE' },
109
+ ],
110
+ mats: { square: 5, quarterRound: 1, arcLarge: 2 },
111
+ centrelineMm: 2 * QUARTER_TURN_MM,
112
+ },
113
+ };
114
+
115
+ export const PIECE_TYPES = /** @type {PieceType[]} */ (Object.keys(PIECES));
116
+ /** @type {Rotation[]} */
117
+ export const ROTATIONS = [0, 90, 180, 270];
118
+
119
+ /** @type {Map<string, PieceDef>} */
120
+ const cache = new Map();
121
+
122
+ // Rotate a definition and translate it so its footprint starts at [0, 0].
123
+ /** @param {PieceType} type @param {number} rot @returns {PieceDef} */
124
+ export function rotatedDef(type, rot) {
125
+ const key = `${type}:${steps(rot) * 90}`;
126
+ const hit = cache.get(key);
127
+ if (hit) return hit;
128
+
129
+ const def = PIECES[type];
130
+ if (!def) throw new Error(`unknown piece type: ${type}`);
131
+
132
+ const spun = def.cells.map((c) => rotateCell(c, rot));
133
+ const dx = -Math.min(...spun.map((c) => c[0]));
134
+ const dy = -Math.min(...spun.map((c) => c[1]));
135
+ /** @type {(cell: Cell) => Cell} */
136
+ const shift = ([x, y]) => [x + dx, y + dy];
137
+
138
+ /** @type {PieceDef} */
139
+ const out = {
140
+ label: def.label,
141
+ mats: def.mats,
142
+ centrelineMm: def.centrelineMm,
143
+ cells: spun.map(shift),
144
+ ports: def.ports.map((p) => ({
145
+ cells: p.cells.map((c) => shift(rotateCell(c, rot))),
146
+ dir: rotateDir(p.dir, rot),
147
+ })),
148
+ walls: def.walls.map((w) =>
149
+ w.kind === 'cap'
150
+ ? { kind: 'cap', vertex: shift(rotateVertex(w.vertex, rot)) }
151
+ : {
152
+ kind: w.kind,
153
+ r: w.r,
154
+ cell: shift(rotateCell(w.cell, rot)),
155
+ corner: rotateCorner(w.corner, rot),
156
+ },
157
+ ),
158
+ };
159
+
160
+ cache.set(key, out);
161
+ return out;
162
+ }
163
+
164
+ /** @param {PlacedPiece} piece @returns {Cell[]} */
165
+ export function placedCells({ type, x, y, rot }) {
166
+ return rotatedDef(type, rot).cells.map(([cx, cy]) => [cx + x, cy + y]);
167
+ }
168
+
169
+ /** @param {PlacedPiece} piece @returns {Port[]} */
170
+ export function placedPorts({ type, x, y, rot }) {
171
+ return rotatedDef(type, rot).ports.map((p) => ({
172
+ cells: p.cells.map(([cx, cy]) => [cx + x, cy + y]),
173
+ dir: p.dir,
174
+ }));
175
+ }
176
+
177
+ /** @param {PlacedPiece} piece @returns {Wall[]} */
178
+ export function placedWalls({ type, x, y, rot }) {
179
+ return rotatedDef(type, rot).walls.map((w) =>
180
+ w.kind === 'cap'
181
+ ? { kind: 'cap', vertex: [w.vertex[0] + x, w.vertex[1] + y] }
182
+ : { ...w, cell: [w.cell[0] + x, w.cell[1] + y] },
183
+ );
184
+ }
package/index.js ADDED
@@ -0,0 +1,29 @@
1
+ // SPDX-FileCopyrightText: 2026 The TinyLap contributors
2
+ //
3
+ // SPDX-License-Identifier: MPL-2.0
4
+
5
+ export * from './geometry.js';
6
+ export * from './model.js';
7
+ export * from './data.js';
8
+ export * from './svg.js';
9
+ export * from './reference-layouts.js';
10
+
11
+ // The data model types, so consumers can import them from the package root.
12
+ /** @typedef {import('./types.js').Dir} Dir */
13
+ /** @typedef {import('./types.js').Corner} Corner */
14
+ /** @typedef {import('./types.js').Rotation} Rotation */
15
+ /** @typedef {import('./types.js').Cell} Cell */
16
+ /** @typedef {import('./types.js').PieceType} PieceType */
17
+ /** @typedef {import('./types.js').Port} Port */
18
+ /** @typedef {import('./types.js').CapWall} CapWall */
19
+ /** @typedef {import('./types.js').ArcWall} ArcWall */
20
+ /** @typedef {import('./types.js').Wall} Wall */
21
+ /** @typedef {import('./types.js').MatCounts} MatCounts */
22
+ /** @typedef {import('./types.js').PieceDef} PieceDef */
23
+ /** @typedef {import('./types.js').PlacedPiece} PlacedPiece */
24
+ /** @typedef {import('./types.js').Room} Room */
25
+ /** @typedef {import('./types.js').Layout} Layout */
26
+ /** @typedef {import('./types.js').BBox} BBox */
27
+ /** @typedef {import('./types.js').Metrics} Metrics */
28
+ /** @typedef {import('./types.js').BarrierCounts} BarrierCounts */
29
+ /** @typedef {import('./types.js').Barriers} Barriers */