@willcgage/module-schematic 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +76 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +76 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,8 @@ function emptyEditorState(lengthInches) {
|
|
|
29
29
|
configB: "single",
|
|
30
30
|
extraTracks: [],
|
|
31
31
|
turnouts: [],
|
|
32
|
+
crossings: [],
|
|
33
|
+
branch: null,
|
|
32
34
|
controlPoints: []
|
|
33
35
|
};
|
|
34
36
|
}
|
|
@@ -39,28 +41,39 @@ function stateToDoc(state, recordNumber) {
|
|
|
39
41
|
lengthInches: state.lengthInches,
|
|
40
42
|
...state.loop ? { loop: true } : {},
|
|
41
43
|
...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
|
|
42
|
-
endplates:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
) : [
|
|
51
|
-
{ id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
52
|
-
// Non-loop modules always have a real B ("none" never applies).
|
|
53
|
-
{
|
|
54
|
-
id: "B",
|
|
55
|
-
label: "East",
|
|
56
|
-
tracks: [
|
|
57
|
-
{
|
|
58
|
-
trackId: MAIN_TRACK_ID,
|
|
59
|
-
lane: 0,
|
|
60
|
-
config: state.configB === "none" ? "single" : state.configB
|
|
61
|
-
}
|
|
44
|
+
endplates: [
|
|
45
|
+
...state.loop ? (
|
|
46
|
+
// Balloon loop: A is the entry. A standard endplate B on the balloon
|
|
47
|
+
// makes it an INTERCHANGE (second route connects at the loop, e.g.
|
|
48
|
+
// Seaford); configB "none" makes it a pure turnback.
|
|
49
|
+
[
|
|
50
|
+
{ id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
51
|
+
...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
|
|
62
52
|
]
|
|
63
|
-
|
|
53
|
+
) : [
|
|
54
|
+
{ id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
55
|
+
// Non-loop modules always have a real B ("none" never applies).
|
|
56
|
+
{
|
|
57
|
+
id: "B",
|
|
58
|
+
label: "East",
|
|
59
|
+
tracks: [
|
|
60
|
+
{
|
|
61
|
+
trackId: MAIN_TRACK_ID,
|
|
62
|
+
lane: 0,
|
|
63
|
+
config: state.configB === "none" ? "single" : state.configB
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
// Branch endplate C — a junction connection at pos, off one side (#170).
|
|
69
|
+
...state.branch ? [
|
|
70
|
+
{
|
|
71
|
+
id: "C",
|
|
72
|
+
label: state.branch.label || "Branch",
|
|
73
|
+
tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.branch.config }],
|
|
74
|
+
at: { pos: state.branch.pos, side: state.branch.side }
|
|
75
|
+
}
|
|
76
|
+
] : []
|
|
64
77
|
],
|
|
65
78
|
tracks: [
|
|
66
79
|
state.loop ? (
|
|
@@ -93,10 +106,19 @@ function stateToDoc(state, recordNumber) {
|
|
|
93
106
|
kind: t.kind,
|
|
94
107
|
name: t.name || void 0
|
|
95
108
|
})),
|
|
109
|
+
...state.crossings.length > 0 ? {
|
|
110
|
+
crossings: state.crossings.map((x) => ({
|
|
111
|
+
id: x.id,
|
|
112
|
+
pos: x.pos,
|
|
113
|
+
tracks: [x.trackA, x.trackB],
|
|
114
|
+
name: x.name || void 0
|
|
115
|
+
}))
|
|
116
|
+
} : {},
|
|
96
117
|
controlPoints: state.controlPoints.map((c) => ({
|
|
97
118
|
id: c.id,
|
|
98
119
|
name: c.name,
|
|
99
120
|
turnouts: c.turnouts,
|
|
121
|
+
...c.crossings?.length ? { crossings: c.crossings } : {},
|
|
100
122
|
signals: c.signals.map((s) => ({
|
|
101
123
|
id: s.id,
|
|
102
124
|
pos: s.pos,
|
|
@@ -170,6 +192,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
170
192
|
};
|
|
171
193
|
const loop = isLoopDoc(d);
|
|
172
194
|
const hasB = (d.endplates ?? []).some((e) => e.id === "B");
|
|
195
|
+
const epC = (d.endplates ?? []).find((e) => e.id !== "A" && e.id !== "B" && e.at);
|
|
173
196
|
return {
|
|
174
197
|
lengthInches: len,
|
|
175
198
|
loop,
|
|
@@ -177,6 +200,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
177
200
|
configA: configOf("A"),
|
|
178
201
|
// On a loop, a missing B means pure turnback; present = interchange loop.
|
|
179
202
|
configB: loop && !hasB ? "none" : configOf("B"),
|
|
203
|
+
branch: epC ? {
|
|
204
|
+
label: epC.label ?? "Branch",
|
|
205
|
+
pos: sc(epC.at.pos),
|
|
206
|
+
side: epC.at.side === "down" ? "down" : "up",
|
|
207
|
+
config: epC.tracks?.[0]?.config === "double" ? "double" : "single"
|
|
208
|
+
} : null,
|
|
209
|
+
crossings: (d.crossings ?? []).map((x) => ({
|
|
210
|
+
id: x.id,
|
|
211
|
+
name: x.name ?? "",
|
|
212
|
+
pos: sc(x.pos),
|
|
213
|
+
trackA: x.tracks?.[0] ?? MAIN_TRACK_ID,
|
|
214
|
+
trackB: x.tracks?.[1] ?? MAIN_TRACK_ID
|
|
215
|
+
})),
|
|
180
216
|
extraTracks,
|
|
181
217
|
turnouts: (d.turnouts ?? []).map((t) => ({
|
|
182
218
|
id: t.id,
|
|
@@ -195,6 +231,7 @@ function readControlPoints(d, sc = (p) => p) {
|
|
|
195
231
|
id: c.id,
|
|
196
232
|
name: c.name ?? "",
|
|
197
233
|
turnouts: c.turnouts ?? [],
|
|
234
|
+
...c.crossings?.length ? { crossings: c.crossings } : {},
|
|
198
235
|
signals: (c.signals ?? []).map((s) => ({
|
|
199
236
|
id: s.id,
|
|
200
237
|
pos: sc(s.pos),
|
|
@@ -332,22 +369,38 @@ function moduleFeatures(doc) {
|
|
|
332
369
|
const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
|
|
333
370
|
(c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
|
|
334
371
|
) : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? ""));
|
|
372
|
+
const crossings = (doc.crossings ?? []).map((x) => ({
|
|
373
|
+
id: x.id,
|
|
374
|
+
name: x.name ?? "",
|
|
375
|
+
posFrac: clampFrac(x.pos),
|
|
376
|
+
laneA: trackLane.get(x.tracks?.[0] ?? "") ?? 0,
|
|
377
|
+
laneB: trackLane.get(x.tracks?.[1] ?? "") ?? 1
|
|
378
|
+
}));
|
|
379
|
+
const branchConnectors = doc.endplates.filter((e) => e.id !== "A" && e.id !== "B" && e.at).map((e) => ({
|
|
380
|
+
id: e.id,
|
|
381
|
+
label: e.label ?? e.id,
|
|
382
|
+
posFrac: clampFrac(e.at.pos),
|
|
383
|
+
side: e.at.side === "down" ? "down" : "up"
|
|
384
|
+
}));
|
|
335
385
|
const allLanes = [
|
|
336
386
|
0,
|
|
337
387
|
doubleMain ? 1 : 0,
|
|
338
388
|
...extraTracks.map((t) => t.lane),
|
|
339
|
-
...signals.map((s) => s.lane)
|
|
389
|
+
...signals.map((s) => s.lane),
|
|
390
|
+
...crossings.flatMap((x) => [x.laneA, x.laneB])
|
|
340
391
|
];
|
|
341
392
|
const loop = isLoopDoc(doc);
|
|
342
393
|
return {
|
|
343
394
|
doubleMain,
|
|
344
395
|
loop,
|
|
345
|
-
loopInterchange: loop && doc.endplates.length >= 2,
|
|
396
|
+
loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,
|
|
346
397
|
loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
|
|
347
398
|
loopRender: loop ? doc.loopRender ?? null : null,
|
|
348
399
|
extraTracks,
|
|
349
400
|
turnouts,
|
|
350
401
|
signals,
|
|
402
|
+
crossings,
|
|
403
|
+
branchConnectors,
|
|
351
404
|
laneMin: Math.min(...allLanes),
|
|
352
405
|
laneMax: Math.max(...allLanes)
|
|
353
406
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n signals: EditorCpSignal[];\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East) | \"C\"… (branch, #170)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n /** Branch endplates (#170): where a 3rd+ endplate sits — pos inches from A,\n * on the up (north/above) or down side. Absent = axial (A at 0, B at\n * lengthInches). Renderers draw a named connector arrow (the CATS/US&S\n * off-band idiom) until branch spines land. */\n at?: { pos: number; side: \"up\" | \"down\" };\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/** A grade crossing / diamond (#170): two tracks cross with no route choice —\n * a conflict node, not a turnout. A connected diamond adds normal turnouts\n * alongside. Drawn as an X between the two tracks' lanes. */\nexport interface SchematicCrossing {\n id: string;\n pos: number;\n /** The two crossing tracks, by id. */\n tracks: [string, string];\n name?: string | null;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n /** Crossings this interlocking protects (#170). */\n crossings?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n /** Grade crossings / diamonds (#170). */\n crossings?: SchematicCrossing[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n /** Crossing ids this interlocking protects (#170). */\n crossings?: string[];\n signals: EditorCpSignal[];\n}\nexport interface EditorCrossing {\n id: string;\n name: string;\n pos: number;\n trackA: string;\n trackB: string;\n}\n/** A 3rd endplate — a branch/junction connection off the module (#170). */\nexport interface EditorBranch {\n label: string;\n pos: number;\n side: \"up\" | \"down\";\n config: TrackConfig;\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n /** Grade crossings / diamonds (#170). */\n crossings: EditorCrossing[];\n /** Branch endplate C — a junction connection (#170); null = through module. */\n branch: EditorBranch | null;\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n crossings: [],\n branch: null,\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: [\n ...(state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ]),\n // Branch endplate C — a junction connection at pos, off one side (#170).\n ...(state.branch\n ? [\n {\n id: \"C\",\n label: state.branch.label || \"Branch\",\n tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.branch.config }],\n at: { pos: state.branch.pos, side: state.branch.side },\n },\n ]\n : []),\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n ...(state.crossings.length > 0\n ? {\n crossings: state.crossings.map((x) => ({\n id: x.id,\n pos: x.pos,\n tracks: [x.trackA, x.trackB] as [string, string],\n name: x.name || undefined,\n })),\n }\n : {}),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n ...(c.crossings?.length ? { crossings: c.crossings } : {}),\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n // Branch endplate C (junction connection, #170).\n const epC = (d!.endplates ?? []).find((e) => e.id !== \"A\" && e.id !== \"B\" && e.at);\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n branch: epC\n ? {\n label: epC.label ?? \"Branch\",\n pos: sc(epC.at!.pos),\n side: epC.at!.side === \"down\" ? \"down\" : \"up\",\n config: epC.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\",\n }\n : null,\n crossings: (d!.crossings ?? []).map((x) => ({\n id: x.id,\n name: x.name ?? \"\",\n pos: sc(x.pos),\n trackA: x.tracks?.[0] ?? MAIN_TRACK_ID,\n trackB: x.tracks?.[1] ?? MAIN_TRACK_ID,\n })),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n ...(c.crossings?.length ? { crossings: c.crossings } : {}),\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\n/** A grade crossing / diamond — draw an X spanning the two lanes (#170). */\nexport interface DrawCrossing {\n id: string;\n name: string;\n posFrac: number;\n laneA: number;\n laneB: number;\n}\n/** A branch endplate — draw a connector stub + arrow + label off the given\n * side (the CATS/US&S off-band idiom, #170). */\nexport interface BranchConnector {\n id: string;\n label: string;\n posFrac: number;\n side: \"up\" | \"down\";\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Grade crossings / diamonds (#170). */\n crossings: DrawCrossing[];\n /** Branch endplates — junction connectors off the module (#170). */\n branchConnectors: BranchConnector[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const crossings: DrawCrossing[] = (doc.crossings ?? []).map((x) => ({\n id: x.id,\n name: x.name ?? \"\",\n posFrac: clampFrac(x.pos),\n laneA: trackLane.get(x.tracks?.[0] ?? \"\") ?? 0,\n laneB: trackLane.get(x.tracks?.[1] ?? \"\") ?? 1,\n }));\n\n // Branch endplates (any beyond A/B with a placement) → connector arrows.\n const branchConnectors: BranchConnector[] = doc.endplates\n .filter((e) => e.id !== \"A\" && e.id !== \"B\" && e.at)\n .map((e) => ({\n id: e.id,\n label: e.label ?? e.id,\n posFrac: clampFrac(e.at!.pos),\n side: e.at!.side === \"down\" ? \"down\" : \"up\",\n }));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ...crossings.flatMap((x) => [x.laneA, x.laneB]),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n crossings,\n branchConnectors,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -26,6 +26,14 @@ interface SchematicEndplate {
|
|
|
26
26
|
id: string;
|
|
27
27
|
label?: string | null;
|
|
28
28
|
tracks?: SchematicEndplateTrack[];
|
|
29
|
+
/** Branch endplates (#170): where a 3rd+ endplate sits — pos inches from A,
|
|
30
|
+
* on the up (north/above) or down side. Absent = axial (A at 0, B at
|
|
31
|
+
* lengthInches). Renderers draw a named connector arrow (the CATS/US&S
|
|
32
|
+
* off-band idiom) until branch spines land. */
|
|
33
|
+
at?: {
|
|
34
|
+
pos: number;
|
|
35
|
+
side: "up" | "down";
|
|
36
|
+
};
|
|
29
37
|
}
|
|
30
38
|
interface SchematicTrack {
|
|
31
39
|
id: string;
|
|
@@ -76,6 +84,16 @@ interface SchematicBlock {
|
|
|
76
84
|
from: number;
|
|
77
85
|
to: number;
|
|
78
86
|
}
|
|
87
|
+
/** A grade crossing / diamond (#170): two tracks cross with no route choice —
|
|
88
|
+
* a conflict node, not a turnout. A connected diamond adds normal turnouts
|
|
89
|
+
* alongside. Drawn as an X between the two tracks' lanes. */
|
|
90
|
+
interface SchematicCrossing {
|
|
91
|
+
id: string;
|
|
92
|
+
pos: number;
|
|
93
|
+
/** The two crossing tracks, by id. */
|
|
94
|
+
tracks: [string, string];
|
|
95
|
+
name?: string | null;
|
|
96
|
+
}
|
|
79
97
|
/**
|
|
80
98
|
* A control point is an interlocking: a named group of one or more signals and
|
|
81
99
|
* zero or more turnouts. A passing siding has two (West/East); a lone block
|
|
@@ -85,6 +103,8 @@ interface SchematicControlPoint {
|
|
|
85
103
|
id: string;
|
|
86
104
|
name?: string | null;
|
|
87
105
|
turnouts?: string[];
|
|
106
|
+
/** Crossings this interlocking protects (#170). */
|
|
107
|
+
crossings?: string[];
|
|
88
108
|
signals?: SchematicSignal[];
|
|
89
109
|
}
|
|
90
110
|
interface ModuleSchematicDoc {
|
|
@@ -108,6 +128,8 @@ interface ModuleSchematicDoc {
|
|
|
108
128
|
endplates: SchematicEndplate[];
|
|
109
129
|
tracks: SchematicTrack[];
|
|
110
130
|
turnouts?: SchematicTurnout[];
|
|
131
|
+
/** Grade crossings / diamonds (#170). */
|
|
132
|
+
crossings?: SchematicCrossing[];
|
|
111
133
|
controlPoints?: SchematicControlPoint[];
|
|
112
134
|
/** @deprecated pre-grouping flat signals; read for back-compat. */
|
|
113
135
|
signals?: SchematicSignal[];
|
|
@@ -165,8 +187,24 @@ interface EditorControlPoint {
|
|
|
165
187
|
id: string;
|
|
166
188
|
name: string;
|
|
167
189
|
turnouts: string[];
|
|
190
|
+
/** Crossing ids this interlocking protects (#170). */
|
|
191
|
+
crossings?: string[];
|
|
168
192
|
signals: EditorCpSignal[];
|
|
169
193
|
}
|
|
194
|
+
interface EditorCrossing {
|
|
195
|
+
id: string;
|
|
196
|
+
name: string;
|
|
197
|
+
pos: number;
|
|
198
|
+
trackA: string;
|
|
199
|
+
trackB: string;
|
|
200
|
+
}
|
|
201
|
+
/** A 3rd endplate — a branch/junction connection off the module (#170). */
|
|
202
|
+
interface EditorBranch {
|
|
203
|
+
label: string;
|
|
204
|
+
pos: number;
|
|
205
|
+
side: "up" | "down";
|
|
206
|
+
config: TrackConfig;
|
|
207
|
+
}
|
|
170
208
|
/** Endplate B on a loop module: a standard endplate makes the balloon an
|
|
171
209
|
* INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
|
|
172
210
|
* makes it a pure turnback. Non-loop modules always have a real B. */
|
|
@@ -184,6 +222,10 @@ interface EditorState {
|
|
|
184
222
|
configB: EndplateBConfig;
|
|
185
223
|
extraTracks: EditorTrack[];
|
|
186
224
|
turnouts: EditorTurnout[];
|
|
225
|
+
/** Grade crossings / diamonds (#170). */
|
|
226
|
+
crossings: EditorCrossing[];
|
|
227
|
+
/** Branch endplate C — a junction connection (#170); null = through module. */
|
|
228
|
+
branch: EditorBranch | null;
|
|
187
229
|
controlPoints: EditorControlPoint[];
|
|
188
230
|
}
|
|
189
231
|
/** Build the empty editor state for a module of the given length. */
|
|
@@ -242,6 +284,22 @@ interface DrawSignal {
|
|
|
242
284
|
* renderer join the drawn signal back to interlocking-level state (aspects). */
|
|
243
285
|
cp?: string;
|
|
244
286
|
}
|
|
287
|
+
/** A grade crossing / diamond — draw an X spanning the two lanes (#170). */
|
|
288
|
+
interface DrawCrossing {
|
|
289
|
+
id: string;
|
|
290
|
+
name: string;
|
|
291
|
+
posFrac: number;
|
|
292
|
+
laneA: number;
|
|
293
|
+
laneB: number;
|
|
294
|
+
}
|
|
295
|
+
/** A branch endplate — draw a connector stub + arrow + label off the given
|
|
296
|
+
* side (the CATS/US&S off-band idiom, #170). */
|
|
297
|
+
interface BranchConnector {
|
|
298
|
+
id: string;
|
|
299
|
+
label: string;
|
|
300
|
+
posFrac: number;
|
|
301
|
+
side: "up" | "down";
|
|
302
|
+
}
|
|
245
303
|
interface ModuleFeatures {
|
|
246
304
|
/** Whether either endplate declares a double-track main. */
|
|
247
305
|
doubleMain: boolean;
|
|
@@ -260,6 +318,10 @@ interface ModuleFeatures {
|
|
|
260
318
|
extraTracks: DrawTrack[];
|
|
261
319
|
turnouts: DrawTurnout[];
|
|
262
320
|
signals: DrawSignal[];
|
|
321
|
+
/** Grade crossings / diamonds (#170). */
|
|
322
|
+
crossings: DrawCrossing[];
|
|
323
|
+
/** Branch endplates — junction connectors off the module (#170). */
|
|
324
|
+
branchConnectors: BranchConnector[];
|
|
263
325
|
/** Lane extents across every feature (mains included; negative = outside
|
|
264
326
|
* Main 1). Renderers size their vertical space from these. */
|
|
265
327
|
laneMin: number;
|
|
@@ -275,4 +337,4 @@ interface ModuleFeatures {
|
|
|
275
337
|
*/
|
|
276
338
|
declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
|
|
277
339
|
|
|
278
|
-
export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
|
|
340
|
+
export { type BranchConnector, type DrawCrossing, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ interface SchematicEndplate {
|
|
|
26
26
|
id: string;
|
|
27
27
|
label?: string | null;
|
|
28
28
|
tracks?: SchematicEndplateTrack[];
|
|
29
|
+
/** Branch endplates (#170): where a 3rd+ endplate sits — pos inches from A,
|
|
30
|
+
* on the up (north/above) or down side. Absent = axial (A at 0, B at
|
|
31
|
+
* lengthInches). Renderers draw a named connector arrow (the CATS/US&S
|
|
32
|
+
* off-band idiom) until branch spines land. */
|
|
33
|
+
at?: {
|
|
34
|
+
pos: number;
|
|
35
|
+
side: "up" | "down";
|
|
36
|
+
};
|
|
29
37
|
}
|
|
30
38
|
interface SchematicTrack {
|
|
31
39
|
id: string;
|
|
@@ -76,6 +84,16 @@ interface SchematicBlock {
|
|
|
76
84
|
from: number;
|
|
77
85
|
to: number;
|
|
78
86
|
}
|
|
87
|
+
/** A grade crossing / diamond (#170): two tracks cross with no route choice —
|
|
88
|
+
* a conflict node, not a turnout. A connected diamond adds normal turnouts
|
|
89
|
+
* alongside. Drawn as an X between the two tracks' lanes. */
|
|
90
|
+
interface SchematicCrossing {
|
|
91
|
+
id: string;
|
|
92
|
+
pos: number;
|
|
93
|
+
/** The two crossing tracks, by id. */
|
|
94
|
+
tracks: [string, string];
|
|
95
|
+
name?: string | null;
|
|
96
|
+
}
|
|
79
97
|
/**
|
|
80
98
|
* A control point is an interlocking: a named group of one or more signals and
|
|
81
99
|
* zero or more turnouts. A passing siding has two (West/East); a lone block
|
|
@@ -85,6 +103,8 @@ interface SchematicControlPoint {
|
|
|
85
103
|
id: string;
|
|
86
104
|
name?: string | null;
|
|
87
105
|
turnouts?: string[];
|
|
106
|
+
/** Crossings this interlocking protects (#170). */
|
|
107
|
+
crossings?: string[];
|
|
88
108
|
signals?: SchematicSignal[];
|
|
89
109
|
}
|
|
90
110
|
interface ModuleSchematicDoc {
|
|
@@ -108,6 +128,8 @@ interface ModuleSchematicDoc {
|
|
|
108
128
|
endplates: SchematicEndplate[];
|
|
109
129
|
tracks: SchematicTrack[];
|
|
110
130
|
turnouts?: SchematicTurnout[];
|
|
131
|
+
/** Grade crossings / diamonds (#170). */
|
|
132
|
+
crossings?: SchematicCrossing[];
|
|
111
133
|
controlPoints?: SchematicControlPoint[];
|
|
112
134
|
/** @deprecated pre-grouping flat signals; read for back-compat. */
|
|
113
135
|
signals?: SchematicSignal[];
|
|
@@ -165,8 +187,24 @@ interface EditorControlPoint {
|
|
|
165
187
|
id: string;
|
|
166
188
|
name: string;
|
|
167
189
|
turnouts: string[];
|
|
190
|
+
/** Crossing ids this interlocking protects (#170). */
|
|
191
|
+
crossings?: string[];
|
|
168
192
|
signals: EditorCpSignal[];
|
|
169
193
|
}
|
|
194
|
+
interface EditorCrossing {
|
|
195
|
+
id: string;
|
|
196
|
+
name: string;
|
|
197
|
+
pos: number;
|
|
198
|
+
trackA: string;
|
|
199
|
+
trackB: string;
|
|
200
|
+
}
|
|
201
|
+
/** A 3rd endplate — a branch/junction connection off the module (#170). */
|
|
202
|
+
interface EditorBranch {
|
|
203
|
+
label: string;
|
|
204
|
+
pos: number;
|
|
205
|
+
side: "up" | "down";
|
|
206
|
+
config: TrackConfig;
|
|
207
|
+
}
|
|
170
208
|
/** Endplate B on a loop module: a standard endplate makes the balloon an
|
|
171
209
|
* INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
|
|
172
210
|
* makes it a pure turnback. Non-loop modules always have a real B. */
|
|
@@ -184,6 +222,10 @@ interface EditorState {
|
|
|
184
222
|
configB: EndplateBConfig;
|
|
185
223
|
extraTracks: EditorTrack[];
|
|
186
224
|
turnouts: EditorTurnout[];
|
|
225
|
+
/** Grade crossings / diamonds (#170). */
|
|
226
|
+
crossings: EditorCrossing[];
|
|
227
|
+
/** Branch endplate C — a junction connection (#170); null = through module. */
|
|
228
|
+
branch: EditorBranch | null;
|
|
187
229
|
controlPoints: EditorControlPoint[];
|
|
188
230
|
}
|
|
189
231
|
/** Build the empty editor state for a module of the given length. */
|
|
@@ -242,6 +284,22 @@ interface DrawSignal {
|
|
|
242
284
|
* renderer join the drawn signal back to interlocking-level state (aspects). */
|
|
243
285
|
cp?: string;
|
|
244
286
|
}
|
|
287
|
+
/** A grade crossing / diamond — draw an X spanning the two lanes (#170). */
|
|
288
|
+
interface DrawCrossing {
|
|
289
|
+
id: string;
|
|
290
|
+
name: string;
|
|
291
|
+
posFrac: number;
|
|
292
|
+
laneA: number;
|
|
293
|
+
laneB: number;
|
|
294
|
+
}
|
|
295
|
+
/** A branch endplate — draw a connector stub + arrow + label off the given
|
|
296
|
+
* side (the CATS/US&S off-band idiom, #170). */
|
|
297
|
+
interface BranchConnector {
|
|
298
|
+
id: string;
|
|
299
|
+
label: string;
|
|
300
|
+
posFrac: number;
|
|
301
|
+
side: "up" | "down";
|
|
302
|
+
}
|
|
245
303
|
interface ModuleFeatures {
|
|
246
304
|
/** Whether either endplate declares a double-track main. */
|
|
247
305
|
doubleMain: boolean;
|
|
@@ -260,6 +318,10 @@ interface ModuleFeatures {
|
|
|
260
318
|
extraTracks: DrawTrack[];
|
|
261
319
|
turnouts: DrawTurnout[];
|
|
262
320
|
signals: DrawSignal[];
|
|
321
|
+
/** Grade crossings / diamonds (#170). */
|
|
322
|
+
crossings: DrawCrossing[];
|
|
323
|
+
/** Branch endplates — junction connectors off the module (#170). */
|
|
324
|
+
branchConnectors: BranchConnector[];
|
|
263
325
|
/** Lane extents across every feature (mains included; negative = outside
|
|
264
326
|
* Main 1). Renderers size their vertical space from these. */
|
|
265
327
|
laneMin: number;
|
|
@@ -275,4 +337,4 @@ interface ModuleFeatures {
|
|
|
275
337
|
*/
|
|
276
338
|
declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
|
|
277
339
|
|
|
278
|
-
export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
|
|
340
|
+
export { type BranchConnector, type DrawCrossing, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,8 @@ function emptyEditorState(lengthInches) {
|
|
|
27
27
|
configB: "single",
|
|
28
28
|
extraTracks: [],
|
|
29
29
|
turnouts: [],
|
|
30
|
+
crossings: [],
|
|
31
|
+
branch: null,
|
|
30
32
|
controlPoints: []
|
|
31
33
|
};
|
|
32
34
|
}
|
|
@@ -37,28 +39,39 @@ function stateToDoc(state, recordNumber) {
|
|
|
37
39
|
lengthInches: state.lengthInches,
|
|
38
40
|
...state.loop ? { loop: true } : {},
|
|
39
41
|
...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
|
|
40
|
-
endplates:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
) : [
|
|
49
|
-
{ id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
50
|
-
// Non-loop modules always have a real B ("none" never applies).
|
|
51
|
-
{
|
|
52
|
-
id: "B",
|
|
53
|
-
label: "East",
|
|
54
|
-
tracks: [
|
|
55
|
-
{
|
|
56
|
-
trackId: MAIN_TRACK_ID,
|
|
57
|
-
lane: 0,
|
|
58
|
-
config: state.configB === "none" ? "single" : state.configB
|
|
59
|
-
}
|
|
42
|
+
endplates: [
|
|
43
|
+
...state.loop ? (
|
|
44
|
+
// Balloon loop: A is the entry. A standard endplate B on the balloon
|
|
45
|
+
// makes it an INTERCHANGE (second route connects at the loop, e.g.
|
|
46
|
+
// Seaford); configB "none" makes it a pure turnback.
|
|
47
|
+
[
|
|
48
|
+
{ id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
49
|
+
...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
|
|
60
50
|
]
|
|
61
|
-
|
|
51
|
+
) : [
|
|
52
|
+
{ id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
53
|
+
// Non-loop modules always have a real B ("none" never applies).
|
|
54
|
+
{
|
|
55
|
+
id: "B",
|
|
56
|
+
label: "East",
|
|
57
|
+
tracks: [
|
|
58
|
+
{
|
|
59
|
+
trackId: MAIN_TRACK_ID,
|
|
60
|
+
lane: 0,
|
|
61
|
+
config: state.configB === "none" ? "single" : state.configB
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
// Branch endplate C — a junction connection at pos, off one side (#170).
|
|
67
|
+
...state.branch ? [
|
|
68
|
+
{
|
|
69
|
+
id: "C",
|
|
70
|
+
label: state.branch.label || "Branch",
|
|
71
|
+
tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.branch.config }],
|
|
72
|
+
at: { pos: state.branch.pos, side: state.branch.side }
|
|
73
|
+
}
|
|
74
|
+
] : []
|
|
62
75
|
],
|
|
63
76
|
tracks: [
|
|
64
77
|
state.loop ? (
|
|
@@ -91,10 +104,19 @@ function stateToDoc(state, recordNumber) {
|
|
|
91
104
|
kind: t.kind,
|
|
92
105
|
name: t.name || void 0
|
|
93
106
|
})),
|
|
107
|
+
...state.crossings.length > 0 ? {
|
|
108
|
+
crossings: state.crossings.map((x) => ({
|
|
109
|
+
id: x.id,
|
|
110
|
+
pos: x.pos,
|
|
111
|
+
tracks: [x.trackA, x.trackB],
|
|
112
|
+
name: x.name || void 0
|
|
113
|
+
}))
|
|
114
|
+
} : {},
|
|
94
115
|
controlPoints: state.controlPoints.map((c) => ({
|
|
95
116
|
id: c.id,
|
|
96
117
|
name: c.name,
|
|
97
118
|
turnouts: c.turnouts,
|
|
119
|
+
...c.crossings?.length ? { crossings: c.crossings } : {},
|
|
98
120
|
signals: c.signals.map((s) => ({
|
|
99
121
|
id: s.id,
|
|
100
122
|
pos: s.pos,
|
|
@@ -168,6 +190,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
168
190
|
};
|
|
169
191
|
const loop = isLoopDoc(d);
|
|
170
192
|
const hasB = (d.endplates ?? []).some((e) => e.id === "B");
|
|
193
|
+
const epC = (d.endplates ?? []).find((e) => e.id !== "A" && e.id !== "B" && e.at);
|
|
171
194
|
return {
|
|
172
195
|
lengthInches: len,
|
|
173
196
|
loop,
|
|
@@ -175,6 +198,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
175
198
|
configA: configOf("A"),
|
|
176
199
|
// On a loop, a missing B means pure turnback; present = interchange loop.
|
|
177
200
|
configB: loop && !hasB ? "none" : configOf("B"),
|
|
201
|
+
branch: epC ? {
|
|
202
|
+
label: epC.label ?? "Branch",
|
|
203
|
+
pos: sc(epC.at.pos),
|
|
204
|
+
side: epC.at.side === "down" ? "down" : "up",
|
|
205
|
+
config: epC.tracks?.[0]?.config === "double" ? "double" : "single"
|
|
206
|
+
} : null,
|
|
207
|
+
crossings: (d.crossings ?? []).map((x) => ({
|
|
208
|
+
id: x.id,
|
|
209
|
+
name: x.name ?? "",
|
|
210
|
+
pos: sc(x.pos),
|
|
211
|
+
trackA: x.tracks?.[0] ?? MAIN_TRACK_ID,
|
|
212
|
+
trackB: x.tracks?.[1] ?? MAIN_TRACK_ID
|
|
213
|
+
})),
|
|
178
214
|
extraTracks,
|
|
179
215
|
turnouts: (d.turnouts ?? []).map((t) => ({
|
|
180
216
|
id: t.id,
|
|
@@ -193,6 +229,7 @@ function readControlPoints(d, sc = (p) => p) {
|
|
|
193
229
|
id: c.id,
|
|
194
230
|
name: c.name ?? "",
|
|
195
231
|
turnouts: c.turnouts ?? [],
|
|
232
|
+
...c.crossings?.length ? { crossings: c.crossings } : {},
|
|
196
233
|
signals: (c.signals ?? []).map((s) => ({
|
|
197
234
|
id: s.id,
|
|
198
235
|
pos: sc(s.pos),
|
|
@@ -330,22 +367,38 @@ function moduleFeatures(doc) {
|
|
|
330
367
|
const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
|
|
331
368
|
(c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
|
|
332
369
|
) : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? ""));
|
|
370
|
+
const crossings = (doc.crossings ?? []).map((x) => ({
|
|
371
|
+
id: x.id,
|
|
372
|
+
name: x.name ?? "",
|
|
373
|
+
posFrac: clampFrac(x.pos),
|
|
374
|
+
laneA: trackLane.get(x.tracks?.[0] ?? "") ?? 0,
|
|
375
|
+
laneB: trackLane.get(x.tracks?.[1] ?? "") ?? 1
|
|
376
|
+
}));
|
|
377
|
+
const branchConnectors = doc.endplates.filter((e) => e.id !== "A" && e.id !== "B" && e.at).map((e) => ({
|
|
378
|
+
id: e.id,
|
|
379
|
+
label: e.label ?? e.id,
|
|
380
|
+
posFrac: clampFrac(e.at.pos),
|
|
381
|
+
side: e.at.side === "down" ? "down" : "up"
|
|
382
|
+
}));
|
|
333
383
|
const allLanes = [
|
|
334
384
|
0,
|
|
335
385
|
doubleMain ? 1 : 0,
|
|
336
386
|
...extraTracks.map((t) => t.lane),
|
|
337
|
-
...signals.map((s) => s.lane)
|
|
387
|
+
...signals.map((s) => s.lane),
|
|
388
|
+
...crossings.flatMap((x) => [x.laneA, x.laneB])
|
|
338
389
|
];
|
|
339
390
|
const loop = isLoopDoc(doc);
|
|
340
391
|
return {
|
|
341
392
|
doubleMain,
|
|
342
393
|
loop,
|
|
343
|
-
loopInterchange: loop && doc.endplates.length >= 2,
|
|
394
|
+
loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,
|
|
344
395
|
loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
|
|
345
396
|
loopRender: loop ? doc.loopRender ?? null : null,
|
|
346
397
|
extraTracks,
|
|
347
398
|
turnouts,
|
|
348
399
|
signals,
|
|
400
|
+
crossings,
|
|
401
|
+
branchConnectors,
|
|
349
402
|
laneMin: Math.min(...allLanes),
|
|
350
403
|
laneMax: Math.max(...allLanes)
|
|
351
404
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n signals: EditorCpSignal[];\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East) | \"C\"… (branch, #170)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n /** Branch endplates (#170): where a 3rd+ endplate sits — pos inches from A,\n * on the up (north/above) or down side. Absent = axial (A at 0, B at\n * lengthInches). Renderers draw a named connector arrow (the CATS/US&S\n * off-band idiom) until branch spines land. */\n at?: { pos: number; side: \"up\" | \"down\" };\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/** A grade crossing / diamond (#170): two tracks cross with no route choice —\n * a conflict node, not a turnout. A connected diamond adds normal turnouts\n * alongside. Drawn as an X between the two tracks' lanes. */\nexport interface SchematicCrossing {\n id: string;\n pos: number;\n /** The two crossing tracks, by id. */\n tracks: [string, string];\n name?: string | null;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n /** Crossings this interlocking protects (#170). */\n crossings?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n /** Grade crossings / diamonds (#170). */\n crossings?: SchematicCrossing[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n /** Crossing ids this interlocking protects (#170). */\n crossings?: string[];\n signals: EditorCpSignal[];\n}\nexport interface EditorCrossing {\n id: string;\n name: string;\n pos: number;\n trackA: string;\n trackB: string;\n}\n/** A 3rd endplate — a branch/junction connection off the module (#170). */\nexport interface EditorBranch {\n label: string;\n pos: number;\n side: \"up\" | \"down\";\n config: TrackConfig;\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n /** Grade crossings / diamonds (#170). */\n crossings: EditorCrossing[];\n /** Branch endplate C — a junction connection (#170); null = through module. */\n branch: EditorBranch | null;\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n crossings: [],\n branch: null,\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: [\n ...(state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ]),\n // Branch endplate C — a junction connection at pos, off one side (#170).\n ...(state.branch\n ? [\n {\n id: \"C\",\n label: state.branch.label || \"Branch\",\n tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.branch.config }],\n at: { pos: state.branch.pos, side: state.branch.side },\n },\n ]\n : []),\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n ...(state.crossings.length > 0\n ? {\n crossings: state.crossings.map((x) => ({\n id: x.id,\n pos: x.pos,\n tracks: [x.trackA, x.trackB] as [string, string],\n name: x.name || undefined,\n })),\n }\n : {}),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n ...(c.crossings?.length ? { crossings: c.crossings } : {}),\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n // Branch endplate C (junction connection, #170).\n const epC = (d!.endplates ?? []).find((e) => e.id !== \"A\" && e.id !== \"B\" && e.at);\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n branch: epC\n ? {\n label: epC.label ?? \"Branch\",\n pos: sc(epC.at!.pos),\n side: epC.at!.side === \"down\" ? \"down\" : \"up\",\n config: epC.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\",\n }\n : null,\n crossings: (d!.crossings ?? []).map((x) => ({\n id: x.id,\n name: x.name ?? \"\",\n pos: sc(x.pos),\n trackA: x.tracks?.[0] ?? MAIN_TRACK_ID,\n trackB: x.tracks?.[1] ?? MAIN_TRACK_ID,\n })),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n ...(c.crossings?.length ? { crossings: c.crossings } : {}),\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\n/** A grade crossing / diamond — draw an X spanning the two lanes (#170). */\nexport interface DrawCrossing {\n id: string;\n name: string;\n posFrac: number;\n laneA: number;\n laneB: number;\n}\n/** A branch endplate — draw a connector stub + arrow + label off the given\n * side (the CATS/US&S off-band idiom, #170). */\nexport interface BranchConnector {\n id: string;\n label: string;\n posFrac: number;\n side: \"up\" | \"down\";\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Grade crossings / diamonds (#170). */\n crossings: DrawCrossing[];\n /** Branch endplates — junction connectors off the module (#170). */\n branchConnectors: BranchConnector[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const crossings: DrawCrossing[] = (doc.crossings ?? []).map((x) => ({\n id: x.id,\n name: x.name ?? \"\",\n posFrac: clampFrac(x.pos),\n laneA: trackLane.get(x.tracks?.[0] ?? \"\") ?? 0,\n laneB: trackLane.get(x.tracks?.[1] ?? \"\") ?? 1,\n }));\n\n // Branch endplates (any beyond A/B with a placement) → connector arrows.\n const branchConnectors: BranchConnector[] = doc.endplates\n .filter((e) => e.id !== \"A\" && e.id !== \"B\" && e.at)\n .map((e) => ({\n id: e.id,\n label: e.label ?? e.id,\n posFrac: clampFrac(e.at!.pos),\n side: e.at!.side === \"down\" ? \"down\" : \"up\",\n }));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ...crossings.flatMap((x) => [x.laneA, x.laneB]),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n crossings,\n branchConnectors,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willcgage/module-schematic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Shared FreeMo module operations-schematic (track-graph): doc types, parser, feature resolver, N-scale helpers, and the editor state machine. Authored by the Module Repository, imported by Free-Dispatcher.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"freemo",
|