@willcgage/module-schematic 0.5.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 CHANGED
@@ -24,10 +24,13 @@ function emptyEditorState(lengthInches) {
24
24
  return {
25
25
  lengthInches: lengthInches > 0 ? lengthInches : 24,
26
26
  loop: false,
27
+ loopReturn: "same",
27
28
  configA: "single",
28
29
  configB: "single",
29
30
  extraTracks: [],
30
31
  turnouts: [],
32
+ crossings: [],
33
+ branch: null,
31
34
  controlPoints: []
32
35
  };
33
36
  }
@@ -37,28 +40,40 @@ function stateToDoc(state, recordNumber) {
37
40
  module: recordNumber,
38
41
  lengthInches: state.lengthInches,
39
42
  ...state.loop ? { loop: true } : {},
40
- endplates: state.loop ? (
41
- // Balloon loop: A is the entry. A standard endplate B on the balloon
42
- // makes it an INTERCHANGE (second route connects at the loop, e.g.
43
- // Seaford); configB "none" makes it a pure turnback.
44
- [
45
- { id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
46
- ...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
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
- }
43
+ ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
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 }] }] : []
60
52
  ]
61
- }
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
+ ] : []
62
77
  ],
63
78
  tracks: [
64
79
  state.loop ? (
@@ -66,7 +81,11 @@ function stateToDoc(state, recordNumber) {
66
81
  { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
67
82
  ) : { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" },
68
83
  // Double track: Main 2 is a real entity so turnouts/signals can attach.
84
+ // On a loop it exists only for a Main 2 directional return (the U joins
85
+ // the two lanes at the balloon); a same-main loop's parallel lead legs
86
+ // are ONE main.
69
87
  ...!state.loop && (state.configA === "double" || state.configB === "double") ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" }] : [],
88
+ ...state.loop && state.loopReturn === "main2" ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: state.lengthInches }] : [],
70
89
  ...state.extraTracks.map((t) => ({
71
90
  id: t.id,
72
91
  role: t.role,
@@ -75,7 +94,8 @@ function stateToDoc(state, recordNumber) {
75
94
  toPos: t.toPos,
76
95
  moduleTrackId: t.moduleTrackId,
77
96
  trackName: t.trackName || void 0,
78
- capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos)))
97
+ capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
98
+ ...state.loop && t.inLoop ? { inLoop: true } : {}
79
99
  }))
80
100
  ],
81
101
  turnouts: state.turnouts.map((t) => ({
@@ -86,10 +106,19 @@ function stateToDoc(state, recordNumber) {
86
106
  kind: t.kind,
87
107
  name: t.name || void 0
88
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
+ } : {},
89
117
  controlPoints: state.controlPoints.map((c) => ({
90
118
  id: c.id,
91
119
  name: c.name,
92
120
  turnouts: c.turnouts,
121
+ ...c.crossings?.length ? { crossings: c.crossings } : {},
93
122
  signals: c.signals.map((s) => ({
94
123
  id: s.id,
95
124
  pos: s.pos,
@@ -127,7 +156,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
127
156
  fromPos: sc(t.fromPos ?? 0),
128
157
  toPos: t.toPos != null ? sc(t.toPos) : len,
129
158
  moduleTrackId,
130
- trackName: t.trackName ?? nameOf(moduleTrackId)
159
+ trackName: t.trackName ?? nameOf(moduleTrackId),
160
+ ...t.inLoop ? { inLoop: true } : {}
131
161
  });
132
162
  }
133
163
  }
@@ -162,12 +192,27 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
162
192
  };
163
193
  const loop = isLoopDoc(d);
164
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);
165
196
  return {
166
197
  lengthInches: len,
167
198
  loop,
199
+ loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
168
200
  configA: configOf("A"),
169
201
  // On a loop, a missing B means pure turnback; present = interchange loop.
170
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
+ })),
171
216
  extraTracks,
172
217
  turnouts: (d.turnouts ?? []).map((t) => ({
173
218
  id: t.id,
@@ -186,6 +231,7 @@ function readControlPoints(d, sc = (p) => p) {
186
231
  id: c.id,
187
232
  name: c.name ?? "",
188
233
  turnouts: c.turnouts ?? [],
234
+ ...c.crossings?.length ? { crossings: c.crossings } : {},
189
235
  signals: (c.signals ?? []).map((s) => ({
190
236
  id: s.id,
191
237
  pos: sc(s.pos),
@@ -300,7 +346,8 @@ function moduleFeatures(doc) {
300
346
  fromFrac: clampFrac(Math.min(from, to)),
301
347
  toFrac: clampFrac(Math.max(from, to)),
302
348
  capacityFeet: t.capacityFeet ?? null,
303
- divergesFromLane: divergeOrigin(t.id)
349
+ divergesFromLane: divergeOrigin(t.id),
350
+ inLoop: t.inLoop === true
304
351
  });
305
352
  }
306
353
  const turnouts = (doc.turnouts ?? []).map((t) => ({
@@ -322,20 +369,38 @@ function moduleFeatures(doc) {
322
369
  const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
323
370
  (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
324
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
+ }));
325
385
  const allLanes = [
326
386
  0,
327
387
  doubleMain ? 1 : 0,
328
388
  ...extraTracks.map((t) => t.lane),
329
- ...signals.map((s) => s.lane)
389
+ ...signals.map((s) => s.lane),
390
+ ...crossings.flatMap((x) => [x.laneA, x.laneB])
330
391
  ];
331
392
  const loop = isLoopDoc(doc);
332
393
  return {
333
394
  doubleMain,
334
395
  loop,
335
- loopInterchange: loop && doc.endplates.length >= 2,
396
+ loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,
397
+ loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
398
+ loopRender: loop ? doc.loopRender ?? null : null,
336
399
  extraTracks,
337
400
  turnouts,
338
401
  signals,
402
+ crossings,
403
+ branchConnectors,
339
404
  laneMin: Math.min(...allLanes),
340
405
  laneMax: Math.max(...allLanes)
341
406
  };
@@ -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}\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 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}\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 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 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 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 ...(!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.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 })),\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 });\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 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}\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 /** 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 });\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 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":";;;AAyIO,SAAS,UAAU,GAAA,EAAkC;AAC1D,EAAA,OAAO,GAAA,CAAI,IAAA,KAAS,IAAA,IAAQ,GAAA,CAAI,UAAU,MAAA,KAAW,CAAA;AACvD;AAEO,IAAM,aAAA,GAAgB;AAKtB,IAAM,cAAA,GAAiB;AAGvB,IAAM,aAAA,GAAgB;AAEtB,SAAS,iBAAA,CAAkB,MAAA,EAAgB,KAAA,GAAQ,aAAA,EAAuB;AAC/E,EAAA,OAAQ,SAAS,KAAA,GAAS,EAAA;AAC5B;AAEO,SAAS,iBAAA,CAAkB,IAAA,EAAc,KAAA,GAAQ,aAAA,EAAuB;AAC7E,EAAA,OAAQ,OAAO,EAAA,GAAM,KAAA;AACvB;AAGO,SAAS,kBAAkB,CAAA,EAAuC;AACvE,EAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,KAAM,UAAU,OAAO,IAAA;AACxC,EAAA,MAAM,CAAA,GAAI,CAAA;AACV,EAAA,IAAI,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,EAAU,OAAO,IAAA;AAC1C,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,SAAS,CAAA,IAAK,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,MAAM,CAAA,EAAG,OAAO,IAAA;AACpE,EAAA,OAAO,CAAA;AACT;AAuFO,SAAS,iBAAiB,YAAA,EAAmC;AAClE,EAAA,OAAO;AAAA,IACL,YAAA,EAAc,YAAA,GAAe,CAAA,GAAI,YAAA,GAAe,EAAA;AAAA,IAChD,IAAA,EAAM,KAAA;AAAA,IACN,UAAA,EAAY,MAAA;AAAA,IACZ,OAAA,EAAS,QAAA;AAAA,IACT,OAAA,EAAS,QAAA;AAAA,IACT,aAAa,EAAC;AAAA,IACd,UAAU,EAAC;AAAA,IACX,WAAW,EAAC;AAAA,IACZ,MAAA,EAAQ,IAAA;AAAA,IACR,eAAe;AAAC,GAClB;AACF;AAGO,SAAS,UAAA,CACd,OACA,YAAA,EACoB;AACpB,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAA;AAAA,IACT,MAAA,EAAQ,YAAA;AAAA,IACR,cAAc,KAAA,CAAM,YAAA;AAAA,IACpB,GAAI,KAAA,CAAM,IAAA,GAAO,EAAE,IAAA,EAAM,IAAA,KAAS,EAAC;AAAA,IACnC,GAAI,KAAA,CAAM,IAAA,IAAQ,KAAA,CAAM,UAAA,KAAe,UAAU,EAAE,UAAA,EAAY,OAAA,EAAiB,GAAI,EAAC;AAAA,IACrF,SAAA,EAAW;AAAA,MACT,GAAI,KAAA,CAAM,IAAA;AAAA;AAAA;AAAA;AAAA,QAIN;AAAA,UACE,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,QAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,MAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,OAAA,EAAS,CAAA,EAAE;AAAA,UAChG,GAAI,KAAA,CAAM,OAAA,KAAY,MAAA,GAClB,CAAC,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,aAAA,EAAe,MAAA,EAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,SAAS,CAAA,EAAG,CAAA,GACxG;AAAC;AACP,UACA;AAAA,QACE,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,MAAA,EAAQ,QAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,MAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,OAAA,EAAS,CAAA,EAAE;AAAA;AAAA,QAE/F;AAAA,UACE,EAAA,EAAI,GAAA;AAAA,UACJ,KAAA,EAAO,MAAA;AAAA,UACP,MAAA,EAAQ;AAAA,YACN;AAAA,cACE,OAAA,EAAS,aAAA;AAAA,cACT,IAAA,EAAM,CAAA;AAAA,cACN,MAAA,EAAQ,KAAA,CAAM,OAAA,KAAY,MAAA,GAAS,WAAW,KAAA,CAAM;AAAA;AACtD;AACF;AACF,OACF;AAAA;AAAA,MAEJ,GAAI,MAAM,MAAA,GACN;AAAA,QACE;AAAA,UACE,EAAA,EAAI,GAAA;AAAA,UACJ,KAAA,EAAO,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,QAAA;AAAA,UAC7B,MAAA,EAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,MAAA,EAAQ,CAAA;AAAA,UACzE,EAAA,EAAI,EAAE,GAAA,EAAK,KAAA,CAAM,OAAO,GAAA,EAAK,IAAA,EAAM,KAAA,CAAM,MAAA,CAAO,IAAA;AAAK;AACvD,UAEF;AAAC,KACP;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,KAAA,CAAM,IAAA;AAAA;AAAA,QAEF,EAAE,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,MAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,KAAA,CAAM,YAAA;AAAa,UAC3F,EAAE,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,MAAA,EAAiB,MAAM,CAAA,EAAG,IAAA,EAAM,GAAA,EAAK,EAAA,EAAI,GAAA,EAAI;AAAA;AAAA;AAAA;AAAA;AAAA,MAK5E,GAAI,CAAC,KAAA,CAAM,IAAA,KAAS,KAAA,CAAM,YAAY,QAAA,IAAY,KAAA,CAAM,OAAA,KAAY,QAAA,CAAA,GAChE,CAAC,EAAE,IAAI,cAAA,EAAgB,IAAA,EAAM,MAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,KAAK,EAAA,EAAI,GAAA,EAAK,CAAA,GAC3E,EAAC;AAAA,MACL,GAAI,MAAM,IAAA,IAAQ,KAAA,CAAM,eAAe,OAAA,GACnC,CAAC,EAAE,EAAA,EAAI,cAAA,EAAgB,MAAM,MAAA,EAAiB,IAAA,EAAM,GAAG,OAAA,EAAS,CAAA,EAAG,OAAO,KAAA,CAAM,YAAA,EAAc,CAAA,GAC9F,EAAC;AAAA,MACL,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QAC/B,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,SAAS,CAAA,CAAE,OAAA;AAAA,QACX,OAAO,CAAA,CAAE,KAAA;AAAA,QACT,eAAe,CAAA,CAAE,aAAA;AAAA,QACjB,SAAA,EAAW,EAAE,SAAA,IAAa,MAAA;AAAA,QAC1B,YAAA,EAAc,IAAA,CAAK,KAAA,CAAM,iBAAA,CAAkB,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,OAAO,CAAC,CAAC,CAAA;AAAA,QACzE,GAAI,MAAM,IAAA,IAAQ,CAAA,CAAE,SAAS,EAAE,MAAA,EAAQ,IAAA,EAAK,GAAI;AAAC,OACnD,CAAE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACnC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,KAAK,CAAA,CAAE,GAAA;AAAA,MACP,SAAS,CAAA,CAAE,OAAA;AAAA,MACX,cAAc,CAAA,CAAE,YAAA;AAAA,MAChB,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,IAAA,EAAM,EAAE,IAAA,IAAQ;AAAA,KAClB,CAAE,CAAA;AAAA,IACF,GAAI,KAAA,CAAM,SAAA,CAAU,MAAA,GAAS,CAAA,GACzB;AAAA,MACE,SAAA,EAAW,KAAA,CAAM,SAAA,CAAU,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACrC,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,KAAK,CAAA,CAAE,GAAA;AAAA,QACP,MAAA,EAAQ,CAAC,CAAA,CAAE,MAAA,EAAQ,EAAE,MAAM,CAAA;AAAA,QAC3B,IAAA,EAAM,EAAE,IAAA,IAAQ;AAAA,OAClB,CAAE;AAAA,QAEJ,EAAC;AAAA,IACL,aAAA,EAAe,KAAA,CAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC7C,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,UAAU,CAAA,CAAE,QAAA;AAAA,MACZ,GAAI,EAAE,SAAA,EAAW,MAAA,GAAS,EAAE,SAAA,EAAW,CAAA,CAAE,SAAA,EAAU,GAAI,EAAC;AAAA,MACxD,OAAA,EAAS,CAAA,CAAE,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QAC7B,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,KAAK,CAAA,CAAE,GAAA;AAAA,QACP,OAAO,CAAA,CAAE,KAAA;AAAA,QACT,QAAQ,CAAA,CAAE,MAAA;AAAA,QACV,IAAA,EAAM,MAAA;AAAA,QACN,MAAM,CAAA,CAAE;AAAA,OACV,CAAE;AAAA,KACJ,CAAE;AAAA,GACJ;AACF;AASO,SAAS,UAAA,CACd,GAAA,EACA,cAAA,EACA,YAAA,GAAiC,EAAC,EACrB;AACb,EAAA,MAAM,IAAA,GAAO,iBAAiB,cAAc,CAAA;AAC5C,EAAA,MAAM,CAAA,GACJ,GAAA,IAAO,OAAO,GAAA,KAAQ,WAAY,GAAA,GAA6B,IAAA;AACjE,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,CAAA,IAAK,OAAO,CAAA,CAAE,YAAA,KAAiB,QAAA,IAAY,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,MAAM,CAAA;AAIlF,EAAA,MAAM,MAAM,cAAA,GAAiB,CAAA,GAAI,cAAA,GAAiB,MAAA,GAAS,EAAG,YAAA,GAAgB,EAAA;AAC9E,EAAA,MAAM,SAAS,MAAA,IAAU,CAAA,CAAG,YAAA,GAAgB,CAAA,GAAI,EAAG,YAAA,GAAgB,GAAA;AACnE,EAAA,MAAM,KAAA,GAAQ,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,MAAA,GAAS,CAAA;AAC1C,EAAA,MAAM,KAAK,CAAC,CAAA,KAAc,IAAA,CAAK,KAAA,CAAM,IAAI,KAAK,CAAA;AAE9C,EAAA,MAAM,MAAA,GAAS,CAAC,EAAA,KAA0C;AACxD,IAAA,MAAM,EAAA,GAAK,EAAA,IAAM,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,GAAI,MAAA;AAChE,IAAA,OAAO,IAAI,UAAA,IAAc,EAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,cAA6B,EAAC;AACpC,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,KAAA,MAAW,CAAA,IAAK,EAAG,MAAA,EAAQ;AACzB,MAAA,IAAI,CAAA,CAAE,SAAS,MAAA,EAAQ;AACvB,MAAA,MAAM,aAAA,GAAgB,EAAE,aAAA,IAAiB,IAAA;AACzC,MAAA,IAAI,aAAA,IAAiB,IAAA,EAAM,MAAA,CAAO,GAAA,CAAI,aAAa,CAAA;AACnD,MAAA,WAAA,CAAY,IAAA,CAAK;AAAA,QACf,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,IAAA,EAAO,EAAE,IAAA,IAAsB,QAAA;AAAA,QAC/B,IAAA,EAAM,EAAE,IAAA,IAAQ,CAAA;AAAA,QAChB,OAAA,EAAS,EAAA,CAAG,CAAA,CAAE,OAAA,IAAW,CAAC,CAAA;AAAA,QAC1B,OAAO,CAAA,CAAE,KAAA,IAAS,OAAO,EAAA,CAAG,CAAA,CAAE,KAAK,CAAA,GAAI,GAAA;AAAA,QACvC,aAAA;AAAA,QACA,SAAA,EAAW,CAAA,CAAE,SAAA,IAAa,MAAA,CAAO,aAAa,CAAA;AAAA,QAC9C,GAAI,CAAA,CAAE,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS;AAAC,OACpC,CAAA;AAAA,IACH;AAAA,EACF;AAIA,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,MAAA,CAAO,CAAC,EAAA,KAAO,CAAC,MAAA,CAAO,GAAA,CAAI,EAAA,CAAG,EAAE,CAAC,CAAA;AAC7D,EAAA,IAAI,EAAA,GAAK,CAAA;AACT,EAAA,KAAA,MAAW,MAAM,WAAA,EAAa;AAC5B,IAAA,IAAI,EAAA,CAAG,aAAA,IAAiB,IAAA,IAAQ,EAAA,GAAK,OAAO,MAAA,EAAQ;AAClD,MAAA,MAAM,EAAA,GAAK,OAAO,EAAA,EAAI,CAAA;AACtB,MAAA,EAAA,CAAG,gBAAgB,EAAA,CAAG,EAAA;AACtB,MAAA,IAAI,CAAC,EAAA,CAAG,SAAA,EAAW,EAAA,CAAG,SAAA,GAAY,GAAG,UAAA,IAAc,EAAA;AACnD,MAAA,MAAA,CAAO,GAAA,CAAI,GAAG,EAAE,CAAA;AAAA,IAClB;AAAA,EACF;AACA,EAAA,IAAI,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA;AACxD,EAAA,KAAA,MAAW,MAAM,YAAA,EAAc;AAC7B,IAAA,IAAI,MAAA,CAAO,GAAA,CAAI,EAAA,CAAG,EAAE,CAAA,EAAG;AACvB,IAAA,IAAA,IAAQ,CAAA;AACR,IAAA,WAAA,CAAY,IAAA,CAAK;AAAA,MACf,EAAA,EAAI,CAAA,EAAA,EAAK,EAAA,CAAG,EAAE,CAAA,CAAA;AAAA,MACd,IAAA,EAAM,QAAA;AAAA,MACN,IAAA;AAAA,MACA,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,GAAG,CAAA;AAAA,MAC7B,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,GAAG,CAAA;AAAA,MAC3B,eAAe,EAAA,CAAG,EAAA;AAAA,MAClB,SAAA,EAAW,GAAG,UAAA,IAAc;AAAA,KAC7B,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,OAAO,EAAE,GAAG,IAAA,EAAM,YAAA,EAAc,KAAK,WAAA,EAAY;AAE9D,EAAA,MAAM,QAAA,GAAW,CAAC,EAAA,KAA4B;AAC5C,IAAA,MAAM,EAAA,GAAA,CAAM,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA;AACvD,IAAA,OAAO,IAAI,MAAA,GAAS,CAAC,CAAA,EAAG,MAAA,KAAW,WAAW,QAAA,GAAW,QAAA;AAAA,EAC3D,CAAA;AACA,EAAA,MAAM,IAAA,GAAO,UAAU,CAAE,CAAA;AACzB,EAAA,MAAM,IAAA,GAAA,CAAQ,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAG,CAAA;AAE1D,EAAA,MAAM,GAAA,GAAA,CAAO,CAAA,CAAG,SAAA,IAAa,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,GAAA,IAAO,CAAA,CAAE,EAAA,KAAO,GAAA,IAAO,EAAE,EAAE,CAAA;AACjF,EAAA,OAAO;AAAA,IACL,YAAA,EAAc,GAAA;AAAA,IACd,IAAA;AAAA,IACA,UAAA,EAAY,IAAA,IAAQ,CAAA,CAAG,UAAA,KAAe,UAAU,OAAA,GAAU,MAAA;AAAA,IAC1D,OAAA,EAAS,SAAS,GAAG,CAAA;AAAA;AAAA,IAErB,SAAS,IAAA,IAAQ,CAAC,IAAA,GAAO,MAAA,GAAS,SAAS,GAAG,CAAA;AAAA,IAC9C,QAAQ,GAAA,GACJ;AAAA,MACE,KAAA,EAAO,IAAI,KAAA,IAAS,QAAA;AAAA,MACpB,GAAA,EAAK,EAAA,CAAG,GAAA,CAAI,EAAA,CAAI,GAAG,CAAA;AAAA,MACnB,IAAA,EAAM,GAAA,CAAI,EAAA,CAAI,IAAA,KAAS,SAAS,MAAA,GAAS,IAAA;AAAA,MACzC,QAAQ,GAAA,CAAI,MAAA,GAAS,CAAC,CAAA,EAAG,MAAA,KAAW,WAAW,QAAA,GAAW;AAAA,KAC5D,GACA,IAAA;AAAA,IACJ,YAAY,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC1C,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,MAAA,EAAQ,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK,aAAA;AAAA,MACzB,MAAA,EAAQ,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK;AAAA,KAC3B,CAAE,CAAA;AAAA,IACF,WAAA;AAAA,IACA,WAAW,CAAA,CAAG,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACxC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,SAAS,CAAA,CAAE,OAAA;AAAA,MACX,cAAc,CAAA,CAAE,YAAA;AAAA,MAChB,IAAA,EAAO,EAAE,IAAA,IAAwB;AAAA,KACnC,CAAE,CAAA;AAAA,IACF,aAAA,EAAe,iBAAA,CAAkB,CAAA,EAAI,EAAE;AAAA,GACzC;AACF;AAGA,SAAS,iBAAA,CACP,CAAA,EACA,EAAA,GAA4B,CAAC,MAAM,CAAA,EACb;AACtB,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,aAAa,CAAA,EAAG;AAClC,IAAA,OAAO,CAAA,CAAE,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACjC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,QAAA,EAAU,CAAA,CAAE,QAAA,IAAY,EAAC;AAAA,MACzB,GAAI,EAAE,SAAA,EAAW,MAAA,GAAS,EAAE,SAAA,EAAW,CAAA,CAAE,SAAA,EAAU,GAAI,EAAC;AAAA,MACxD,UAAU,CAAA,CAAE,OAAA,IAAW,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACrC,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,QACb,KAAA,EAAO,EAAE,KAAA,IAAS,aAAA;AAAA,QAClB,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,QACtC,IAAA,EAAO,EAAE,IAAA,IAAuB;AAAA,OAClC,CAAE;AAAA,KACJ,CAAE,CAAA;AAAA,EACJ;AAEA,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAgC;AACnD,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,MAAW,CAAA,IAAK,CAAA,CAAE,OAAA,IAAW,EAAC,EAAG;AAC/B,IAAA,MAAM,GAAA,GAAM,CAAA,CAAE,OAAA,IAAW,CAAA,IAAA,EAAO,EAAE,EAAE,CAAA,CAAA;AACpC,IAAA,IAAI,EAAA,GAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AACvB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,EAAA,GAAK,EAAE,IAAI,CAAA,EAAA,EAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA,EAAI,UAAU,CAAA,CAAE,OAAA,GAAU,CAAC,CAAA,CAAE,OAAO,IAAI,EAAC,EAAG,OAAA,EAAS,EAAC,EAAE;AAC/F,MAAA,MAAA,CAAO,GAAA,CAAI,KAAK,EAAE,CAAA;AAAA,IACpB;AACA,IAAA,EAAA,CAAG,QAAQ,IAAA,CAAK;AAAA,MACd,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,KAAA,EAAO,EAAE,KAAA,IAAS,aAAA;AAAA,MAClB,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,MACtC,IAAA,EAAO,EAAE,IAAA,IAAuB;AAAA,KACjC,CAAA;AAAA,EACH;AACA,EAAA,OAAO,CAAC,GAAG,MAAA,CAAO,MAAA,EAAQ,CAAA;AAC5B;AAGO,SAAS,MAAA,CAAO,QAAgB,QAAA,EAA4B;AACjE,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAO,QAAA,CAAS,SAAS,CAAA,EAAG,MAAM,GAAG,CAAC,CAAA,CAAE,GAAG,CAAA,IAAK,CAAA;AAChD,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,CAAC,CAAA,CAAA;AACtB;AAOO,SAAS,mBAAmB,KAAA,EAIjC;AACA,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,YAAA,GAAe,CAAA,GAAI,MAAM,YAAA,GAAe,EAAA;AAC1D,EAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,GAAA,GAAM,IAAI,CAAC,CAAA;AAChD,EAAA,MAAM,OAAA,GAAU,KAAA;AAChB,EAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,OAAA,GAAU,CAAA,EAAG,MAAM,KAAK,CAAA;AAE/C,EAAA,MAAM,WACJ,KAAA,CAAM,OAAA,KAAY,YAAY,KAAA,CAAM,OAAA,KAAY,WAAW,CAAA,GAAI,CAAA;AACjE,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,GAAO,CAAC,CAAC,CAAA;AAE3E,EAAA,MAAM,QAAA,GAAW,CAAC,aAAA,EAAe,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AACtE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,EAAO,QAAQ,CAAA;AACpC,EAAA,MAAM,KAAA,GAAqB;AAAA,IACzB,EAAA,EAAI,KAAA;AAAA,IACJ,IAAA,EAAM,QAAA;AAAA,IACN,IAAA;AAAA,IACA,OAAA;AAAA,IACA,KAAA;AAAA,IACA,aAAA,EAAe,IAAA;AAAA,IACf,SAAA,EAAW;AAAA,GACb;AAEA,EAAA,MAAM,QAAQ,KAAA,CAAM,QAAA,CAAS,IAAI,CAAC,CAAA,KAAM,EAAE,EAAE,CAAA;AAC5C,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAC9B,EAAA,MAAM,MAAM,MAAA,CAAO,IAAA,EAAM,CAAC,GAAG,KAAA,EAAO,GAAG,CAAC,CAAA;AACxC,EAAA,MAAM,QAAA,GAA4B;AAAA,IAChC,EAAE,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,aAAA,EAAe,GAAA,EAAK,OAAA,EAAS,OAAA,EAAS,aAAA,EAAe,YAAA,EAAc,KAAA,EAAO,IAAA,EAAM,OAAA,EAAQ;AAAA,IACzG,EAAE,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,aAAA,EAAe,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,aAAA,EAAe,YAAA,EAAc,KAAA,EAAO,IAAA,EAAM,MAAA;AAAO,GACxG;AAIA,EAAA,MAAM,QAAQ,KAAA,CAAM,aAAA,CAAc,IAAI,CAAC,CAAA,KAAM,EAAE,EAAE,CAAA;AACjD,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAC9B,EAAA,MAAM,MAAM,MAAA,CAAO,IAAA,EAAM,CAAC,GAAG,KAAA,EAAO,GAAG,CAAC,CAAA;AACxC,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,EAAc,GAAA,EAAa,MAAA,MAA0C;AAAA,IAChF,EAAA,EAAI,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAAA,IACrB,GAAA;AAAA,IACA,KAAA,EAAO,aAAA;AAAA,IACP,MAAA;AAAA;AAAA,IAEA,IAAA,EAAM,MAAA,KAAW,MAAA,GAAS,OAAA,GAAU;AAAA,GACtC,CAAA;AACA,EAAA,MAAM,aAAA,GAAsC;AAAA,IAC1C,EAAE,IAAI,GAAA,EAAK,IAAA,EAAM,eAAe,QAAA,EAAU,CAAC,GAAG,CAAA,EAAG,OAAA,EAAS,CAAC,GAAA,CAAI,GAAA,EAAK,SAAS,MAAM,CAAA,EAAG,IAAI,GAAA,EAAK,OAAA,EAAS,MAAM,CAAC,CAAA,EAAE;AAAA,IACjH,EAAE,IAAI,GAAA,EAAK,IAAA,EAAM,eAAe,QAAA,EAAU,CAAC,GAAG,CAAA,EAAG,OAAA,EAAS,CAAC,GAAA,CAAI,GAAA,EAAK,OAAO,MAAM,CAAA,EAAG,IAAI,GAAA,EAAK,KAAA,EAAO,MAAM,CAAC,CAAA;AAAE,GAC/G;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,aAAA,EAAc;AAC1C;AAwFO,SAAS,eAAe,GAAA,EAAyC;AACtE,EAAA,MAAM,GAAA,GACJ,IAAI,YAAA,IAAgB,GAAA,CAAI,eAAe,CAAA,GACnC,GAAA,CAAI,eACJ,IAAA,CAAK,GAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,OAAA,IAAW,CAAA,EAAG,CAAA,CAAE,KAAA,IAAS,CAAC,CAAC,CAAA;AAAA,IAC/D,GAAA,CAAI,IAAI,QAAA,IAAY,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG;AAAA,GAC1C;AACN,EAAA,MAAM,SAAA,GAAY,CAAC,CAAA,KAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,GAAI,GAAG,CAAC,CAAA;AAEjE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,KAAA,MAAW,CAAA,IAAK,IAAI,MAAA,EAAQ,SAAA,CAAU,IAAI,CAAA,CAAE,EAAA,EAAI,EAAE,IAAI,CAAA;AAGtD,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAoB;AAC5C,EAAA,GAAA,CAAI,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,WAAA,CAAY,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,KAAM,CAAA,GAAI,CAAA,GAAI,GAAG,CAAC,CAAA;AACxE,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoB;AAC3C,EAAA,KAAA,MAAW,CAAA,IAAK,GAAA,CAAI,QAAA,IAAY,EAAC,aAAc,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,GAAG,CAAA;AAC9D,EAAA,MAAM,KAAA,GAAQ,CAAC,MAAA,KAAmC;AAChD,IAAA,IAAI,MAAA,IAAU,MAAM,OAAO,IAAA;AAC3B,IAAA,IAAI,YAAY,GAAA,CAAI,MAAM,GAAG,OAAO,WAAA,CAAY,IAAI,MAAM,CAAA;AAC1D,IAAA,IAAI,WAAW,GAAA,CAAI,MAAM,GAAG,OAAO,UAAA,CAAW,IAAI,MAAM,CAAA;AACxD,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,UAAA,GAAa,IAAI,SAAA,CAAU,IAAA;AAAA,IAAK,CAAC,MACrC,CAAA,CAAE,MAAA,EAAQ,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,QAAQ;AAAA,GAC7C;AAGA,EAAA,MAAM,aAAA,GAAgB,CAAC,OAAA,KAA4B;AACjD,IAAA,MAAM,EAAA,GAAA,CAAM,GAAA,CAAI,QAAA,IAAY,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,YAAA,KAAiB,OAAO,CAAA;AACtE,IAAA,OAAO,KAAM,SAAA,CAAU,GAAA,CAAI,EAAA,CAAG,OAAO,KAAK,CAAA,GAAK,CAAA;AAAA,EACjD,CAAA;AAEA,EAAA,MAAM,cAA2B,EAAC;AAClC,EAAA,KAAA,MAAW,CAAA,IAAK,IAAI,MAAA,EAAQ;AAC1B,IAAA,IAAI,CAAA,CAAE,SAAS,MAAA,EAAQ;AACvB,IAAA,MAAM,IAAA,GAAO,CAAA,CAAE,OAAA,IAAW,KAAA,CAAM,EAAE,IAAI,CAAA;AACtC,IAAA,MAAM,EAAA,GAAK,CAAA,CAAE,KAAA,IAAS,KAAA,CAAM,EAAE,EAAE,CAAA;AAChC,IAAA,IAAI,IAAA,IAAQ,IAAA,IAAQ,EAAA,IAAM,IAAA,EAAM;AAChC,IAAA,WAAA,CAAY,IAAA,CAAK;AAAA,MACf,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,UAAU,SAAA,CAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,EAAE,CAAC,CAAA;AAAA,MACtC,QAAQ,SAAA,CAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,EAAE,CAAC,CAAA;AAAA,MACpC,YAAA,EAAc,EAAE,YAAA,IAAgB,IAAA;AAAA,MAChC,gBAAA,EAAkB,aAAA,CAAc,CAAA,CAAE,EAAE,CAAA;AAAA,MACpC,MAAA,EAAQ,EAAE,MAAA,KAAW;AAAA,KACtB,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,YAA2B,GAAA,CAAI,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAC/D,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,IAChB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,MAAA,EAAQ,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,OAAO,CAAA,IAAK,CAAA;AAAA,IACpC,WAAA,EAAa,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,YAAY,CAAA,IAAK;AAAA,GAChD,CAAE,CAAA;AAEF,EAAA,MAAM,UAAA,GAAa,CACjB,CAAA,EACA,IAAA,EACA,EAAA,MACgB;AAAA,IAChB,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA;AAAA,IACA,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,IAAA,EAAM,EAAE,KAAA,GAAS,SAAA,CAAU,IAAI,CAAA,CAAE,KAAK,KAAK,CAAA,GAAK,CAAA;AAAA,IAChD,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,IACtC,IAAA,EAAM,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,OAAA,GAAU,OAAA;AAAA,IACrC,GAAI,EAAA,GAAK,EAAE,EAAA,KAAO;AAAC,GACrB,CAAA;AAGA,EAAA,MAAM,UAAwB,KAAA,CAAM,OAAA,CAAQ,IAAI,aAAa,CAAA,GACzD,IAAI,aAAA,CAAc,OAAA;AAAA,IAAQ,CAAC,CAAA,KAAA,CACxB,CAAA,CAAE,OAAA,IAAW,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,UAAA,CAAW,GAAG,CAAA,CAAE,IAAA,IAAQ,EAAA,EAAI,CAAA,CAAE,EAAE,CAAC;AAAA,GAChE,GAAA,CACC,GAAA,CAAI,OAAA,IAAW,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,EAAG,CAAA,CAAE,IAAA,IAAQ,EAAE,CAAC,CAAA;AAE9D,EAAA,MAAM,aAA6B,GAAA,CAAI,SAAA,IAAa,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAClE,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,IAChB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,KAAA,EAAO,UAAU,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,CAAA,IAAK,EAAE,CAAA,IAAK,CAAA;AAAA,IAC7C,KAAA,EAAO,UAAU,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,CAAA,IAAK,EAAE,CAAA,IAAK;AAAA,GAC/C,CAAE,CAAA;AAGF,EAAA,MAAM,mBAAsC,GAAA,CAAI,SAAA,CAC7C,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,GAAA,IAAO,CAAA,CAAE,OAAO,GAAA,IAAO,CAAA,CAAE,EAAE,CAAA,CAClD,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IACX,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,KAAA,EAAO,CAAA,CAAE,KAAA,IAAS,CAAA,CAAE,EAAA;AAAA,IACpB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,EAAA,CAAI,GAAG,CAAA;AAAA,IAC5B,IAAA,EAAM,CAAA,CAAE,EAAA,CAAI,IAAA,KAAS,SAAS,MAAA,GAAS;AAAA,GACzC,CAAE,CAAA;AAEJ,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,CAAA;AAAA,IACA,aAAa,CAAA,GAAI,CAAA;AAAA,IACjB,GAAG,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,IAChC,GAAG,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,IAC5B,GAAG,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,KAAA,EAAO,CAAA,CAAE,KAAK,CAAC;AAAA,GAChD;AACA,EAAA,MAAM,IAAA,GAAO,UAAU,GAAG,CAAA;AAC1B,EAAA,OAAO;AAAA,IACL,UAAA;AAAA,IACA,IAAA;AAAA,IACA,eAAA,EAAiB,IAAA,IAAQ,GAAA,CAAI,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAE,CAAA,CAAE,MAAA,IAAU,CAAA;AAAA,IACtE,UAAA,EAAY,IAAA,IAAQ,GAAA,CAAI,UAAA,KAAe,UAAU,OAAA,GAAU,MAAA;AAAA,IAC3D,UAAA,EAAY,IAAA,GAAQ,GAAA,CAAI,UAAA,IAAc,IAAA,GAAQ,IAAA;AAAA,IAC9C,WAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA;AAAA,IACA,gBAAA;AAAA,IACA,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,QAAQ,CAAA;AAAA,IAC7B,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,QAAQ;AAAA,GAC/B;AACF","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;
@@ -41,6 +49,10 @@ interface SchematicTrack {
41
49
  moduleTrackId?: number | null;
42
50
  /** Owner's track name, mirrored to module_tracks.track_name. */
43
51
  trackName?: string;
52
+ /** Inside the balloon of a loop module (#165): pos still measures from
53
+ * endplate A (past the throat = in the loop), lane is the ladder/arc index —
54
+ * one record drives both the unrolled fan and a geometric render. */
55
+ inLoop?: boolean;
44
56
  }
45
57
  interface SchematicTurnout {
46
58
  id: string;
@@ -72,6 +84,16 @@ interface SchematicBlock {
72
84
  from: number;
73
85
  to: number;
74
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
+ }
75
97
  /**
76
98
  * A control point is an interlocking: a named group of one or more signals and
77
99
  * zero or more turnouts. A passing siding has two (West/East); a lone block
@@ -81,6 +103,8 @@ interface SchematicControlPoint {
81
103
  id: string;
82
104
  name?: string | null;
83
105
  turnouts?: string[];
106
+ /** Crossings this interlocking protects (#170). */
107
+ crossings?: string[];
84
108
  signals?: SchematicSignal[];
85
109
  }
86
110
  interface ModuleSchematicDoc {
@@ -92,9 +116,20 @@ interface ModuleSchematicDoc {
92
116
  * instead of a second endplate. Also implied by a single-entry endplates
93
117
  * array (a category:"loop" module like Seaford). */
94
118
  loop?: boolean;
119
+ /** Where the balloon returns (#165): "same" (default) turns back onto the
120
+ * same main; "main2" is a directional return on a double-track main —
121
+ * out on Main 1, back on Main 2 — drawn as a U joining the two lanes
122
+ * (the transit terminal-loop idiom). */
123
+ loopReturn?: "same" | "main2";
124
+ /** Optional rendering override: "bulb" (abstract terminal), "fan" (interior
125
+ * tracks unrolled as a ladder past the throat — the default when inLoop
126
+ * tracks exist), "geometric" (drawn balloon, AL&E-style). */
127
+ loopRender?: "bulb" | "fan" | "geometric";
95
128
  endplates: SchematicEndplate[];
96
129
  tracks: SchematicTrack[];
97
130
  turnouts?: SchematicTurnout[];
131
+ /** Grade crossings / diamonds (#170). */
132
+ crossings?: SchematicCrossing[];
98
133
  controlPoints?: SchematicControlPoint[];
99
134
  /** @deprecated pre-grouping flat signals; read for back-compat. */
100
135
  signals?: SchematicSignal[];
@@ -124,6 +159,8 @@ interface EditorTrack {
124
159
  moduleTrackId: number | null;
125
160
  /** Owner's track name → module_tracks.track_name. */
126
161
  trackName: string;
162
+ /** Inside the balloon of a loop module (#165). */
163
+ inLoop?: boolean;
127
164
  }
128
165
  /** A module_tracks row as loaded for the editor. */
129
166
  interface ModuleTrackRow {
@@ -150,8 +187,24 @@ interface EditorControlPoint {
150
187
  id: string;
151
188
  name: string;
152
189
  turnouts: string[];
190
+ /** Crossing ids this interlocking protects (#170). */
191
+ crossings?: string[];
153
192
  signals: EditorCpSignal[];
154
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
+ }
155
208
  /** Endplate B on a loop module: a standard endplate makes the balloon an
156
209
  * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
157
210
  * makes it a pure turnback. Non-loop modules always have a real B. */
@@ -162,10 +215,17 @@ interface EditorState {
162
215
  * throat are inside the balloon. Endplate B stays independently available —
163
216
  * present = interchange loop, "none" = pure turnback. */
164
217
  loop: boolean;
218
+ /** Where the balloon returns: same main, or Main 2 (directional return on a
219
+ * double-track main — drawn as a U joining the two lanes). */
220
+ loopReturn: "same" | "main2";
165
221
  configA: TrackConfig;
166
222
  configB: EndplateBConfig;
167
223
  extraTracks: EditorTrack[];
168
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;
169
229
  controlPoints: EditorControlPoint[];
170
230
  }
171
231
  /** Build the empty editor state for a module of the given length. */
@@ -203,6 +263,8 @@ interface DrawTrack {
203
263
  * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;
204
264
  * without this, renderers draw what looks like a crossover. */
205
265
  divergesFromLane: number;
266
+ /** Inside the balloon of a loop module (#165). */
267
+ inLoop: boolean;
206
268
  }
207
269
  interface DrawTurnout {
208
270
  id: string;
@@ -222,6 +284,22 @@ interface DrawSignal {
222
284
  * renderer join the drawn signal back to interlocking-level state (aspects). */
223
285
  cp?: string;
224
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
+ }
225
303
  interface ModuleFeatures {
226
304
  /** Whether either endplate declares a double-track main. */
227
305
  doubleMain: boolean;
@@ -230,10 +308,20 @@ interface ModuleFeatures {
230
308
  /** Loop with a standard endplate B on the balloon: an interchange — a
231
309
  * second route connects at the loop (draw an endplate branch off the bulb). */
232
310
  loopInterchange: boolean;
311
+ /** Where the balloon returns: "main2" = directional return on a double-track
312
+ * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */
313
+ loopReturn: "same" | "main2";
314
+ /** Rendering override from the doc; renderers may ignore modes they don't
315
+ * implement yet ("geometric"). */
316
+ loopRender: "bulb" | "fan" | "geometric" | null;
233
317
  /** Non-main tracks (sidings/spurs/yard/crossover). */
234
318
  extraTracks: DrawTrack[];
235
319
  turnouts: DrawTurnout[];
236
320
  signals: DrawSignal[];
321
+ /** Grade crossings / diamonds (#170). */
322
+ crossings: DrawCrossing[];
323
+ /** Branch endplates — junction connectors off the module (#170). */
324
+ branchConnectors: BranchConnector[];
237
325
  /** Lane extents across every feature (mains included; negative = outside
238
326
  * Main 1). Renderers size their vertical space from these. */
239
327
  laneMin: number;
@@ -249,4 +337,4 @@ interface ModuleFeatures {
249
337
  */
250
338
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
251
339
 
252
- 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;
@@ -41,6 +49,10 @@ interface SchematicTrack {
41
49
  moduleTrackId?: number | null;
42
50
  /** Owner's track name, mirrored to module_tracks.track_name. */
43
51
  trackName?: string;
52
+ /** Inside the balloon of a loop module (#165): pos still measures from
53
+ * endplate A (past the throat = in the loop), lane is the ladder/arc index —
54
+ * one record drives both the unrolled fan and a geometric render. */
55
+ inLoop?: boolean;
44
56
  }
45
57
  interface SchematicTurnout {
46
58
  id: string;
@@ -72,6 +84,16 @@ interface SchematicBlock {
72
84
  from: number;
73
85
  to: number;
74
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
+ }
75
97
  /**
76
98
  * A control point is an interlocking: a named group of one or more signals and
77
99
  * zero or more turnouts. A passing siding has two (West/East); a lone block
@@ -81,6 +103,8 @@ interface SchematicControlPoint {
81
103
  id: string;
82
104
  name?: string | null;
83
105
  turnouts?: string[];
106
+ /** Crossings this interlocking protects (#170). */
107
+ crossings?: string[];
84
108
  signals?: SchematicSignal[];
85
109
  }
86
110
  interface ModuleSchematicDoc {
@@ -92,9 +116,20 @@ interface ModuleSchematicDoc {
92
116
  * instead of a second endplate. Also implied by a single-entry endplates
93
117
  * array (a category:"loop" module like Seaford). */
94
118
  loop?: boolean;
119
+ /** Where the balloon returns (#165): "same" (default) turns back onto the
120
+ * same main; "main2" is a directional return on a double-track main —
121
+ * out on Main 1, back on Main 2 — drawn as a U joining the two lanes
122
+ * (the transit terminal-loop idiom). */
123
+ loopReturn?: "same" | "main2";
124
+ /** Optional rendering override: "bulb" (abstract terminal), "fan" (interior
125
+ * tracks unrolled as a ladder past the throat — the default when inLoop
126
+ * tracks exist), "geometric" (drawn balloon, AL&E-style). */
127
+ loopRender?: "bulb" | "fan" | "geometric";
95
128
  endplates: SchematicEndplate[];
96
129
  tracks: SchematicTrack[];
97
130
  turnouts?: SchematicTurnout[];
131
+ /** Grade crossings / diamonds (#170). */
132
+ crossings?: SchematicCrossing[];
98
133
  controlPoints?: SchematicControlPoint[];
99
134
  /** @deprecated pre-grouping flat signals; read for back-compat. */
100
135
  signals?: SchematicSignal[];
@@ -124,6 +159,8 @@ interface EditorTrack {
124
159
  moduleTrackId: number | null;
125
160
  /** Owner's track name → module_tracks.track_name. */
126
161
  trackName: string;
162
+ /** Inside the balloon of a loop module (#165). */
163
+ inLoop?: boolean;
127
164
  }
128
165
  /** A module_tracks row as loaded for the editor. */
129
166
  interface ModuleTrackRow {
@@ -150,8 +187,24 @@ interface EditorControlPoint {
150
187
  id: string;
151
188
  name: string;
152
189
  turnouts: string[];
190
+ /** Crossing ids this interlocking protects (#170). */
191
+ crossings?: string[];
153
192
  signals: EditorCpSignal[];
154
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
+ }
155
208
  /** Endplate B on a loop module: a standard endplate makes the balloon an
156
209
  * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
157
210
  * makes it a pure turnback. Non-loop modules always have a real B. */
@@ -162,10 +215,17 @@ interface EditorState {
162
215
  * throat are inside the balloon. Endplate B stays independently available —
163
216
  * present = interchange loop, "none" = pure turnback. */
164
217
  loop: boolean;
218
+ /** Where the balloon returns: same main, or Main 2 (directional return on a
219
+ * double-track main — drawn as a U joining the two lanes). */
220
+ loopReturn: "same" | "main2";
165
221
  configA: TrackConfig;
166
222
  configB: EndplateBConfig;
167
223
  extraTracks: EditorTrack[];
168
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;
169
229
  controlPoints: EditorControlPoint[];
170
230
  }
171
231
  /** Build the empty editor state for a module of the given length. */
@@ -203,6 +263,8 @@ interface DrawTrack {
203
263
  * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;
204
264
  * without this, renderers draw what looks like a crossover. */
205
265
  divergesFromLane: number;
266
+ /** Inside the balloon of a loop module (#165). */
267
+ inLoop: boolean;
206
268
  }
207
269
  interface DrawTurnout {
208
270
  id: string;
@@ -222,6 +284,22 @@ interface DrawSignal {
222
284
  * renderer join the drawn signal back to interlocking-level state (aspects). */
223
285
  cp?: string;
224
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
+ }
225
303
  interface ModuleFeatures {
226
304
  /** Whether either endplate declares a double-track main. */
227
305
  doubleMain: boolean;
@@ -230,10 +308,20 @@ interface ModuleFeatures {
230
308
  /** Loop with a standard endplate B on the balloon: an interchange — a
231
309
  * second route connects at the loop (draw an endplate branch off the bulb). */
232
310
  loopInterchange: boolean;
311
+ /** Where the balloon returns: "main2" = directional return on a double-track
312
+ * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */
313
+ loopReturn: "same" | "main2";
314
+ /** Rendering override from the doc; renderers may ignore modes they don't
315
+ * implement yet ("geometric"). */
316
+ loopRender: "bulb" | "fan" | "geometric" | null;
233
317
  /** Non-main tracks (sidings/spurs/yard/crossover). */
234
318
  extraTracks: DrawTrack[];
235
319
  turnouts: DrawTurnout[];
236
320
  signals: DrawSignal[];
321
+ /** Grade crossings / diamonds (#170). */
322
+ crossings: DrawCrossing[];
323
+ /** Branch endplates — junction connectors off the module (#170). */
324
+ branchConnectors: BranchConnector[];
237
325
  /** Lane extents across every feature (mains included; negative = outside
238
326
  * Main 1). Renderers size their vertical space from these. */
239
327
  laneMin: number;
@@ -249,4 +337,4 @@ interface ModuleFeatures {
249
337
  */
250
338
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
251
339
 
252
- 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
@@ -22,10 +22,13 @@ function emptyEditorState(lengthInches) {
22
22
  return {
23
23
  lengthInches: lengthInches > 0 ? lengthInches : 24,
24
24
  loop: false,
25
+ loopReturn: "same",
25
26
  configA: "single",
26
27
  configB: "single",
27
28
  extraTracks: [],
28
29
  turnouts: [],
30
+ crossings: [],
31
+ branch: null,
29
32
  controlPoints: []
30
33
  };
31
34
  }
@@ -35,28 +38,40 @@ function stateToDoc(state, recordNumber) {
35
38
  module: recordNumber,
36
39
  lengthInches: state.lengthInches,
37
40
  ...state.loop ? { loop: true } : {},
38
- endplates: state.loop ? (
39
- // Balloon loop: A is the entry. A standard endplate B on the balloon
40
- // makes it an INTERCHANGE (second route connects at the loop, e.g.
41
- // Seaford); configB "none" makes it a pure turnback.
42
- [
43
- { id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
44
- ...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
45
- ]
46
- ) : [
47
- { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
48
- // Non-loop modules always have a real B ("none" never applies).
49
- {
50
- id: "B",
51
- label: "East",
52
- tracks: [
53
- {
54
- trackId: MAIN_TRACK_ID,
55
- lane: 0,
56
- config: state.configB === "none" ? "single" : state.configB
57
- }
41
+ ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
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 }] }] : []
58
50
  ]
59
- }
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
+ ] : []
60
75
  ],
61
76
  tracks: [
62
77
  state.loop ? (
@@ -64,7 +79,11 @@ function stateToDoc(state, recordNumber) {
64
79
  { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
65
80
  ) : { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" },
66
81
  // Double track: Main 2 is a real entity so turnouts/signals can attach.
82
+ // On a loop it exists only for a Main 2 directional return (the U joins
83
+ // the two lanes at the balloon); a same-main loop's parallel lead legs
84
+ // are ONE main.
67
85
  ...!state.loop && (state.configA === "double" || state.configB === "double") ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" }] : [],
86
+ ...state.loop && state.loopReturn === "main2" ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: state.lengthInches }] : [],
68
87
  ...state.extraTracks.map((t) => ({
69
88
  id: t.id,
70
89
  role: t.role,
@@ -73,7 +92,8 @@ function stateToDoc(state, recordNumber) {
73
92
  toPos: t.toPos,
74
93
  moduleTrackId: t.moduleTrackId,
75
94
  trackName: t.trackName || void 0,
76
- capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos)))
95
+ capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
96
+ ...state.loop && t.inLoop ? { inLoop: true } : {}
77
97
  }))
78
98
  ],
79
99
  turnouts: state.turnouts.map((t) => ({
@@ -84,10 +104,19 @@ function stateToDoc(state, recordNumber) {
84
104
  kind: t.kind,
85
105
  name: t.name || void 0
86
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
+ } : {},
87
115
  controlPoints: state.controlPoints.map((c) => ({
88
116
  id: c.id,
89
117
  name: c.name,
90
118
  turnouts: c.turnouts,
119
+ ...c.crossings?.length ? { crossings: c.crossings } : {},
91
120
  signals: c.signals.map((s) => ({
92
121
  id: s.id,
93
122
  pos: s.pos,
@@ -125,7 +154,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
125
154
  fromPos: sc(t.fromPos ?? 0),
126
155
  toPos: t.toPos != null ? sc(t.toPos) : len,
127
156
  moduleTrackId,
128
- trackName: t.trackName ?? nameOf(moduleTrackId)
157
+ trackName: t.trackName ?? nameOf(moduleTrackId),
158
+ ...t.inLoop ? { inLoop: true } : {}
129
159
  });
130
160
  }
131
161
  }
@@ -160,12 +190,27 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
160
190
  };
161
191
  const loop = isLoopDoc(d);
162
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);
163
194
  return {
164
195
  lengthInches: len,
165
196
  loop,
197
+ loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
166
198
  configA: configOf("A"),
167
199
  // On a loop, a missing B means pure turnback; present = interchange loop.
168
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
+ })),
169
214
  extraTracks,
170
215
  turnouts: (d.turnouts ?? []).map((t) => ({
171
216
  id: t.id,
@@ -184,6 +229,7 @@ function readControlPoints(d, sc = (p) => p) {
184
229
  id: c.id,
185
230
  name: c.name ?? "",
186
231
  turnouts: c.turnouts ?? [],
232
+ ...c.crossings?.length ? { crossings: c.crossings } : {},
187
233
  signals: (c.signals ?? []).map((s) => ({
188
234
  id: s.id,
189
235
  pos: sc(s.pos),
@@ -298,7 +344,8 @@ function moduleFeatures(doc) {
298
344
  fromFrac: clampFrac(Math.min(from, to)),
299
345
  toFrac: clampFrac(Math.max(from, to)),
300
346
  capacityFeet: t.capacityFeet ?? null,
301
- divergesFromLane: divergeOrigin(t.id)
347
+ divergesFromLane: divergeOrigin(t.id),
348
+ inLoop: t.inLoop === true
302
349
  });
303
350
  }
304
351
  const turnouts = (doc.turnouts ?? []).map((t) => ({
@@ -320,20 +367,38 @@ function moduleFeatures(doc) {
320
367
  const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
321
368
  (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
322
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
+ }));
323
383
  const allLanes = [
324
384
  0,
325
385
  doubleMain ? 1 : 0,
326
386
  ...extraTracks.map((t) => t.lane),
327
- ...signals.map((s) => s.lane)
387
+ ...signals.map((s) => s.lane),
388
+ ...crossings.flatMap((x) => [x.laneA, x.laneB])
328
389
  ];
329
390
  const loop = isLoopDoc(doc);
330
391
  return {
331
392
  doubleMain,
332
393
  loop,
333
- loopInterchange: loop && doc.endplates.length >= 2,
394
+ loopInterchange: loop && doc.endplates.filter((e) => !e.at).length >= 2,
395
+ loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
396
+ loopRender: loop ? doc.loopRender ?? null : null,
334
397
  extraTracks,
335
398
  turnouts,
336
399
  signals,
400
+ crossings,
401
+ branchConnectors,
337
402
  laneMin: Math.min(...allLanes),
338
403
  laneMax: Math.max(...allLanes)
339
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}\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 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}\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 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 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 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 ...(!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.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 })),\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 });\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 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}\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 /** 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 });\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 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":";AAyIO,SAAS,UAAU,GAAA,EAAkC;AAC1D,EAAA,OAAO,GAAA,CAAI,IAAA,KAAS,IAAA,IAAQ,GAAA,CAAI,UAAU,MAAA,KAAW,CAAA;AACvD;AAEO,IAAM,aAAA,GAAgB;AAKtB,IAAM,cAAA,GAAiB;AAGvB,IAAM,aAAA,GAAgB;AAEtB,SAAS,iBAAA,CAAkB,MAAA,EAAgB,KAAA,GAAQ,aAAA,EAAuB;AAC/E,EAAA,OAAQ,SAAS,KAAA,GAAS,EAAA;AAC5B;AAEO,SAAS,iBAAA,CAAkB,IAAA,EAAc,KAAA,GAAQ,aAAA,EAAuB;AAC7E,EAAA,OAAQ,OAAO,EAAA,GAAM,KAAA;AACvB;AAGO,SAAS,kBAAkB,CAAA,EAAuC;AACvE,EAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,KAAM,UAAU,OAAO,IAAA;AACxC,EAAA,MAAM,CAAA,GAAI,CAAA;AACV,EAAA,IAAI,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,EAAU,OAAO,IAAA;AAC1C,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,SAAS,CAAA,IAAK,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,MAAM,CAAA,EAAG,OAAO,IAAA;AACpE,EAAA,OAAO,CAAA;AACT;AAuFO,SAAS,iBAAiB,YAAA,EAAmC;AAClE,EAAA,OAAO;AAAA,IACL,YAAA,EAAc,YAAA,GAAe,CAAA,GAAI,YAAA,GAAe,EAAA;AAAA,IAChD,IAAA,EAAM,KAAA;AAAA,IACN,UAAA,EAAY,MAAA;AAAA,IACZ,OAAA,EAAS,QAAA;AAAA,IACT,OAAA,EAAS,QAAA;AAAA,IACT,aAAa,EAAC;AAAA,IACd,UAAU,EAAC;AAAA,IACX,WAAW,EAAC;AAAA,IACZ,MAAA,EAAQ,IAAA;AAAA,IACR,eAAe;AAAC,GAClB;AACF;AAGO,SAAS,UAAA,CACd,OACA,YAAA,EACoB;AACpB,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAA;AAAA,IACT,MAAA,EAAQ,YAAA;AAAA,IACR,cAAc,KAAA,CAAM,YAAA;AAAA,IACpB,GAAI,KAAA,CAAM,IAAA,GAAO,EAAE,IAAA,EAAM,IAAA,KAAS,EAAC;AAAA,IACnC,GAAI,KAAA,CAAM,IAAA,IAAQ,KAAA,CAAM,UAAA,KAAe,UAAU,EAAE,UAAA,EAAY,OAAA,EAAiB,GAAI,EAAC;AAAA,IACrF,SAAA,EAAW;AAAA,MACT,GAAI,KAAA,CAAM,IAAA;AAAA;AAAA;AAAA;AAAA,QAIN;AAAA,UACE,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,QAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,MAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,OAAA,EAAS,CAAA,EAAE;AAAA,UAChG,GAAI,KAAA,CAAM,OAAA,KAAY,MAAA,GAClB,CAAC,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,aAAA,EAAe,MAAA,EAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,SAAS,CAAA,EAAG,CAAA,GACxG;AAAC;AACP,UACA;AAAA,QACE,EAAE,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,MAAA,EAAQ,QAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,MAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,OAAA,EAAS,CAAA,EAAE;AAAA;AAAA,QAE/F;AAAA,UACE,EAAA,EAAI,GAAA;AAAA,UACJ,KAAA,EAAO,MAAA;AAAA,UACP,MAAA,EAAQ;AAAA,YACN;AAAA,cACE,OAAA,EAAS,aAAA;AAAA,cACT,IAAA,EAAM,CAAA;AAAA,cACN,MAAA,EAAQ,KAAA,CAAM,OAAA,KAAY,MAAA,GAAS,WAAW,KAAA,CAAM;AAAA;AACtD;AACF;AACF,OACF;AAAA;AAAA,MAEJ,GAAI,MAAM,MAAA,GACN;AAAA,QACE;AAAA,UACE,EAAA,EAAI,GAAA;AAAA,UACJ,KAAA,EAAO,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,QAAA;AAAA,UAC7B,MAAA,EAAQ,CAAC,EAAE,OAAA,EAAS,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,MAAA,EAAQ,CAAA;AAAA,UACzE,EAAA,EAAI,EAAE,GAAA,EAAK,KAAA,CAAM,OAAO,GAAA,EAAK,IAAA,EAAM,KAAA,CAAM,MAAA,CAAO,IAAA;AAAK;AACvD,UAEF;AAAC,KACP;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,KAAA,CAAM,IAAA;AAAA;AAAA,QAEF,EAAE,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,MAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,KAAA,CAAM,YAAA;AAAa,UAC3F,EAAE,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,MAAA,EAAiB,MAAM,CAAA,EAAG,IAAA,EAAM,GAAA,EAAK,EAAA,EAAI,GAAA,EAAI;AAAA;AAAA;AAAA;AAAA;AAAA,MAK5E,GAAI,CAAC,KAAA,CAAM,IAAA,KAAS,KAAA,CAAM,YAAY,QAAA,IAAY,KAAA,CAAM,OAAA,KAAY,QAAA,CAAA,GAChE,CAAC,EAAE,IAAI,cAAA,EAAgB,IAAA,EAAM,MAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,KAAK,EAAA,EAAI,GAAA,EAAK,CAAA,GAC3E,EAAC;AAAA,MACL,GAAI,MAAM,IAAA,IAAQ,KAAA,CAAM,eAAe,OAAA,GACnC,CAAC,EAAE,EAAA,EAAI,cAAA,EAAgB,MAAM,MAAA,EAAiB,IAAA,EAAM,GAAG,OAAA,EAAS,CAAA,EAAG,OAAO,KAAA,CAAM,YAAA,EAAc,CAAA,GAC9F,EAAC;AAAA,MACL,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QAC/B,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,SAAS,CAAA,CAAE,OAAA;AAAA,QACX,OAAO,CAAA,CAAE,KAAA;AAAA,QACT,eAAe,CAAA,CAAE,aAAA;AAAA,QACjB,SAAA,EAAW,EAAE,SAAA,IAAa,MAAA;AAAA,QAC1B,YAAA,EAAc,IAAA,CAAK,KAAA,CAAM,iBAAA,CAAkB,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,OAAO,CAAC,CAAC,CAAA;AAAA,QACzE,GAAI,MAAM,IAAA,IAAQ,CAAA,CAAE,SAAS,EAAE,MAAA,EAAQ,IAAA,EAAK,GAAI;AAAC,OACnD,CAAE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACnC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,KAAK,CAAA,CAAE,GAAA;AAAA,MACP,SAAS,CAAA,CAAE,OAAA;AAAA,MACX,cAAc,CAAA,CAAE,YAAA;AAAA,MAChB,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,IAAA,EAAM,EAAE,IAAA,IAAQ;AAAA,KAClB,CAAE,CAAA;AAAA,IACF,GAAI,KAAA,CAAM,SAAA,CAAU,MAAA,GAAS,CAAA,GACzB;AAAA,MACE,SAAA,EAAW,KAAA,CAAM,SAAA,CAAU,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACrC,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,KAAK,CAAA,CAAE,GAAA;AAAA,QACP,MAAA,EAAQ,CAAC,CAAA,CAAE,MAAA,EAAQ,EAAE,MAAM,CAAA;AAAA,QAC3B,IAAA,EAAM,EAAE,IAAA,IAAQ;AAAA,OAClB,CAAE;AAAA,QAEJ,EAAC;AAAA,IACL,aAAA,EAAe,KAAA,CAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC7C,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,UAAU,CAAA,CAAE,QAAA;AAAA,MACZ,GAAI,EAAE,SAAA,EAAW,MAAA,GAAS,EAAE,SAAA,EAAW,CAAA,CAAE,SAAA,EAAU,GAAI,EAAC;AAAA,MACxD,OAAA,EAAS,CAAA,CAAE,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QAC7B,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,KAAK,CAAA,CAAE,GAAA;AAAA,QACP,OAAO,CAAA,CAAE,KAAA;AAAA,QACT,QAAQ,CAAA,CAAE,MAAA;AAAA,QACV,IAAA,EAAM,MAAA;AAAA,QACN,MAAM,CAAA,CAAE;AAAA,OACV,CAAE;AAAA,KACJ,CAAE;AAAA,GACJ;AACF;AASO,SAAS,UAAA,CACd,GAAA,EACA,cAAA,EACA,YAAA,GAAiC,EAAC,EACrB;AACb,EAAA,MAAM,IAAA,GAAO,iBAAiB,cAAc,CAAA;AAC5C,EAAA,MAAM,CAAA,GACJ,GAAA,IAAO,OAAO,GAAA,KAAQ,WAAY,GAAA,GAA6B,IAAA;AACjE,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,CAAA,IAAK,OAAO,CAAA,CAAE,YAAA,KAAiB,QAAA,IAAY,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,MAAM,CAAA;AAIlF,EAAA,MAAM,MAAM,cAAA,GAAiB,CAAA,GAAI,cAAA,GAAiB,MAAA,GAAS,EAAG,YAAA,GAAgB,EAAA;AAC9E,EAAA,MAAM,SAAS,MAAA,IAAU,CAAA,CAAG,YAAA,GAAgB,CAAA,GAAI,EAAG,YAAA,GAAgB,GAAA;AACnE,EAAA,MAAM,KAAA,GAAQ,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,MAAA,GAAS,CAAA;AAC1C,EAAA,MAAM,KAAK,CAAC,CAAA,KAAc,IAAA,CAAK,KAAA,CAAM,IAAI,KAAK,CAAA;AAE9C,EAAA,MAAM,MAAA,GAAS,CAAC,EAAA,KAA0C;AACxD,IAAA,MAAM,EAAA,GAAK,EAAA,IAAM,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,GAAI,MAAA;AAChE,IAAA,OAAO,IAAI,UAAA,IAAc,EAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,cAA6B,EAAC;AACpC,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,KAAA,MAAW,CAAA,IAAK,EAAG,MAAA,EAAQ;AACzB,MAAA,IAAI,CAAA,CAAE,SAAS,MAAA,EAAQ;AACvB,MAAA,MAAM,aAAA,GAAgB,EAAE,aAAA,IAAiB,IAAA;AACzC,MAAA,IAAI,aAAA,IAAiB,IAAA,EAAM,MAAA,CAAO,GAAA,CAAI,aAAa,CAAA;AACnD,MAAA,WAAA,CAAY,IAAA,CAAK;AAAA,QACf,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,IAAA,EAAO,EAAE,IAAA,IAAsB,QAAA;AAAA,QAC/B,IAAA,EAAM,EAAE,IAAA,IAAQ,CAAA;AAAA,QAChB,OAAA,EAAS,EAAA,CAAG,CAAA,CAAE,OAAA,IAAW,CAAC,CAAA;AAAA,QAC1B,OAAO,CAAA,CAAE,KAAA,IAAS,OAAO,EAAA,CAAG,CAAA,CAAE,KAAK,CAAA,GAAI,GAAA;AAAA,QACvC,aAAA;AAAA,QACA,SAAA,EAAW,CAAA,CAAE,SAAA,IAAa,MAAA,CAAO,aAAa,CAAA;AAAA,QAC9C,GAAI,CAAA,CAAE,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS;AAAC,OACpC,CAAA;AAAA,IACH;AAAA,EACF;AAIA,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,MAAA,CAAO,CAAC,EAAA,KAAO,CAAC,MAAA,CAAO,GAAA,CAAI,EAAA,CAAG,EAAE,CAAC,CAAA;AAC7D,EAAA,IAAI,EAAA,GAAK,CAAA;AACT,EAAA,KAAA,MAAW,MAAM,WAAA,EAAa;AAC5B,IAAA,IAAI,EAAA,CAAG,aAAA,IAAiB,IAAA,IAAQ,EAAA,GAAK,OAAO,MAAA,EAAQ;AAClD,MAAA,MAAM,EAAA,GAAK,OAAO,EAAA,EAAI,CAAA;AACtB,MAAA,EAAA,CAAG,gBAAgB,EAAA,CAAG,EAAA;AACtB,MAAA,IAAI,CAAC,EAAA,CAAG,SAAA,EAAW,EAAA,CAAG,SAAA,GAAY,GAAG,UAAA,IAAc,EAAA;AACnD,MAAA,MAAA,CAAO,GAAA,CAAI,GAAG,EAAE,CAAA;AAAA,IAClB;AAAA,EACF;AACA,EAAA,IAAI,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA;AACxD,EAAA,KAAA,MAAW,MAAM,YAAA,EAAc;AAC7B,IAAA,IAAI,MAAA,CAAO,GAAA,CAAI,EAAA,CAAG,EAAE,CAAA,EAAG;AACvB,IAAA,IAAA,IAAQ,CAAA;AACR,IAAA,WAAA,CAAY,IAAA,CAAK;AAAA,MACf,EAAA,EAAI,CAAA,EAAA,EAAK,EAAA,CAAG,EAAE,CAAA,CAAA;AAAA,MACd,IAAA,EAAM,QAAA;AAAA,MACN,IAAA;AAAA,MACA,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,GAAG,CAAA;AAAA,MAC7B,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,GAAG,CAAA;AAAA,MAC3B,eAAe,EAAA,CAAG,EAAA;AAAA,MAClB,SAAA,EAAW,GAAG,UAAA,IAAc;AAAA,KAC7B,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,OAAO,EAAE,GAAG,IAAA,EAAM,YAAA,EAAc,KAAK,WAAA,EAAY;AAE9D,EAAA,MAAM,QAAA,GAAW,CAAC,EAAA,KAA4B;AAC5C,IAAA,MAAM,EAAA,GAAA,CAAM,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA;AACvD,IAAA,OAAO,IAAI,MAAA,GAAS,CAAC,CAAA,EAAG,MAAA,KAAW,WAAW,QAAA,GAAW,QAAA;AAAA,EAC3D,CAAA;AACA,EAAA,MAAM,IAAA,GAAO,UAAU,CAAE,CAAA;AACzB,EAAA,MAAM,IAAA,GAAA,CAAQ,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAG,CAAA;AAE1D,EAAA,MAAM,GAAA,GAAA,CAAO,CAAA,CAAG,SAAA,IAAa,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,GAAA,IAAO,CAAA,CAAE,EAAA,KAAO,GAAA,IAAO,EAAE,EAAE,CAAA;AACjF,EAAA,OAAO;AAAA,IACL,YAAA,EAAc,GAAA;AAAA,IACd,IAAA;AAAA,IACA,UAAA,EAAY,IAAA,IAAQ,CAAA,CAAG,UAAA,KAAe,UAAU,OAAA,GAAU,MAAA;AAAA,IAC1D,OAAA,EAAS,SAAS,GAAG,CAAA;AAAA;AAAA,IAErB,SAAS,IAAA,IAAQ,CAAC,IAAA,GAAO,MAAA,GAAS,SAAS,GAAG,CAAA;AAAA,IAC9C,QAAQ,GAAA,GACJ;AAAA,MACE,KAAA,EAAO,IAAI,KAAA,IAAS,QAAA;AAAA,MACpB,GAAA,EAAK,EAAA,CAAG,GAAA,CAAI,EAAA,CAAI,GAAG,CAAA;AAAA,MACnB,IAAA,EAAM,GAAA,CAAI,EAAA,CAAI,IAAA,KAAS,SAAS,MAAA,GAAS,IAAA;AAAA,MACzC,QAAQ,GAAA,CAAI,MAAA,GAAS,CAAC,CAAA,EAAG,MAAA,KAAW,WAAW,QAAA,GAAW;AAAA,KAC5D,GACA,IAAA;AAAA,IACJ,YAAY,CAAA,CAAG,SAAA,IAAa,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC1C,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,MAAA,EAAQ,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK,aAAA;AAAA,MACzB,MAAA,EAAQ,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK;AAAA,KAC3B,CAAE,CAAA;AAAA,IACF,WAAA;AAAA,IACA,WAAW,CAAA,CAAG,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACxC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,SAAS,CAAA,CAAE,OAAA;AAAA,MACX,cAAc,CAAA,CAAE,YAAA;AAAA,MAChB,IAAA,EAAO,EAAE,IAAA,IAAwB;AAAA,KACnC,CAAE,CAAA;AAAA,IACF,aAAA,EAAe,iBAAA,CAAkB,CAAA,EAAI,EAAE;AAAA,GACzC;AACF;AAGA,SAAS,iBAAA,CACP,CAAA,EACA,EAAA,GAA4B,CAAC,MAAM,CAAA,EACb;AACtB,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,aAAa,CAAA,EAAG;AAClC,IAAA,OAAO,CAAA,CAAE,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACjC,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,MAChB,QAAA,EAAU,CAAA,CAAE,QAAA,IAAY,EAAC;AAAA,MACzB,GAAI,EAAE,SAAA,EAAW,MAAA,GAAS,EAAE,SAAA,EAAW,CAAA,CAAE,SAAA,EAAU,GAAI,EAAC;AAAA,MACxD,UAAU,CAAA,CAAE,OAAA,IAAW,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACrC,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,QACb,KAAA,EAAO,EAAE,KAAA,IAAS,aAAA;AAAA,QAClB,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,QACtC,IAAA,EAAO,EAAE,IAAA,IAAuB;AAAA,OAClC,CAAE;AAAA,KACJ,CAAE,CAAA;AAAA,EACJ;AAEA,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAgC;AACnD,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,MAAW,CAAA,IAAK,CAAA,CAAE,OAAA,IAAW,EAAC,EAAG;AAC/B,IAAA,MAAM,GAAA,GAAM,CAAA,CAAE,OAAA,IAAW,CAAA,IAAA,EAAO,EAAE,EAAE,CAAA,CAAA;AACpC,IAAA,IAAI,EAAA,GAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AACvB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,EAAA,GAAK,EAAE,IAAI,CAAA,EAAA,EAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA,EAAI,UAAU,CAAA,CAAE,OAAA,GAAU,CAAC,CAAA,CAAE,OAAO,IAAI,EAAC,EAAG,OAAA,EAAS,EAAC,EAAE;AAC/F,MAAA,MAAA,CAAO,GAAA,CAAI,KAAK,EAAE,CAAA;AAAA,IACpB;AACA,IAAA,EAAA,CAAG,QAAQ,IAAA,CAAK;AAAA,MACd,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,GAAA,EAAK,EAAA,CAAG,CAAA,CAAE,GAAG,CAAA;AAAA,MACb,KAAA,EAAO,EAAE,KAAA,IAAS,aAAA;AAAA,MAClB,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,MACtC,IAAA,EAAO,EAAE,IAAA,IAAuB;AAAA,KACjC,CAAA;AAAA,EACH;AACA,EAAA,OAAO,CAAC,GAAG,MAAA,CAAO,MAAA,EAAQ,CAAA;AAC5B;AAGO,SAAS,MAAA,CAAO,QAAgB,QAAA,EAA4B;AACjE,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAO,QAAA,CAAS,SAAS,CAAA,EAAG,MAAM,GAAG,CAAC,CAAA,CAAE,GAAG,CAAA,IAAK,CAAA;AAChD,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,CAAC,CAAA,CAAA;AACtB;AAOO,SAAS,mBAAmB,KAAA,EAIjC;AACA,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,YAAA,GAAe,CAAA,GAAI,MAAM,YAAA,GAAe,EAAA;AAC1D,EAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,GAAA,GAAM,IAAI,CAAC,CAAA;AAChD,EAAA,MAAM,OAAA,GAAU,KAAA;AAChB,EAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,OAAA,GAAU,CAAA,EAAG,MAAM,KAAK,CAAA;AAE/C,EAAA,MAAM,WACJ,KAAA,CAAM,OAAA,KAAY,YAAY,KAAA,CAAM,OAAA,KAAY,WAAW,CAAA,GAAI,CAAA;AACjE,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,GAAO,CAAC,CAAC,CAAA;AAE3E,EAAA,MAAM,QAAA,GAAW,CAAC,aAAA,EAAe,GAAG,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AACtE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,EAAO,QAAQ,CAAA;AACpC,EAAA,MAAM,KAAA,GAAqB;AAAA,IACzB,EAAA,EAAI,KAAA;AAAA,IACJ,IAAA,EAAM,QAAA;AAAA,IACN,IAAA;AAAA,IACA,OAAA;AAAA,IACA,KAAA;AAAA,IACA,aAAA,EAAe,IAAA;AAAA,IACf,SAAA,EAAW;AAAA,GACb;AAEA,EAAA,MAAM,QAAQ,KAAA,CAAM,QAAA,CAAS,IAAI,CAAC,CAAA,KAAM,EAAE,EAAE,CAAA;AAC5C,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAC9B,EAAA,MAAM,MAAM,MAAA,CAAO,IAAA,EAAM,CAAC,GAAG,KAAA,EAAO,GAAG,CAAC,CAAA;AACxC,EAAA,MAAM,QAAA,GAA4B;AAAA,IAChC,EAAE,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,aAAA,EAAe,GAAA,EAAK,OAAA,EAAS,OAAA,EAAS,aAAA,EAAe,YAAA,EAAc,KAAA,EAAO,IAAA,EAAM,OAAA,EAAQ;AAAA,IACzG,EAAE,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,aAAA,EAAe,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,aAAA,EAAe,YAAA,EAAc,KAAA,EAAO,IAAA,EAAM,MAAA;AAAO,GACxG;AAIA,EAAA,MAAM,QAAQ,KAAA,CAAM,aAAA,CAAc,IAAI,CAAC,CAAA,KAAM,EAAE,EAAE,CAAA;AACjD,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAC9B,EAAA,MAAM,MAAM,MAAA,CAAO,IAAA,EAAM,CAAC,GAAG,KAAA,EAAO,GAAG,CAAC,CAAA;AACxC,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,EAAc,GAAA,EAAa,MAAA,MAA0C;AAAA,IAChF,EAAA,EAAI,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAAA,IACrB,GAAA;AAAA,IACA,KAAA,EAAO,aAAA;AAAA,IACP,MAAA;AAAA;AAAA,IAEA,IAAA,EAAM,MAAA,KAAW,MAAA,GAAS,OAAA,GAAU;AAAA,GACtC,CAAA;AACA,EAAA,MAAM,aAAA,GAAsC;AAAA,IAC1C,EAAE,IAAI,GAAA,EAAK,IAAA,EAAM,eAAe,QAAA,EAAU,CAAC,GAAG,CAAA,EAAG,OAAA,EAAS,CAAC,GAAA,CAAI,GAAA,EAAK,SAAS,MAAM,CAAA,EAAG,IAAI,GAAA,EAAK,OAAA,EAAS,MAAM,CAAC,CAAA,EAAE;AAAA,IACjH,EAAE,IAAI,GAAA,EAAK,IAAA,EAAM,eAAe,QAAA,EAAU,CAAC,GAAG,CAAA,EAAG,OAAA,EAAS,CAAC,GAAA,CAAI,GAAA,EAAK,OAAO,MAAM,CAAA,EAAG,IAAI,GAAA,EAAK,KAAA,EAAO,MAAM,CAAC,CAAA;AAAE,GAC/G;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,aAAA,EAAc;AAC1C;AAwFO,SAAS,eAAe,GAAA,EAAyC;AACtE,EAAA,MAAM,GAAA,GACJ,IAAI,YAAA,IAAgB,GAAA,CAAI,eAAe,CAAA,GACnC,GAAA,CAAI,eACJ,IAAA,CAAK,GAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,OAAA,IAAW,CAAA,EAAG,CAAA,CAAE,KAAA,IAAS,CAAC,CAAC,CAAA;AAAA,IAC/D,GAAA,CAAI,IAAI,QAAA,IAAY,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG;AAAA,GAC1C;AACN,EAAA,MAAM,SAAA,GAAY,CAAC,CAAA,KAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,GAAI,GAAG,CAAC,CAAA;AAEjE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,KAAA,MAAW,CAAA,IAAK,IAAI,MAAA,EAAQ,SAAA,CAAU,IAAI,CAAA,CAAE,EAAA,EAAI,EAAE,IAAI,CAAA;AAGtD,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAoB;AAC5C,EAAA,GAAA,CAAI,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,WAAA,CAAY,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,KAAM,CAAA,GAAI,CAAA,GAAI,GAAG,CAAC,CAAA;AACxE,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoB;AAC3C,EAAA,KAAA,MAAW,CAAA,IAAK,GAAA,CAAI,QAAA,IAAY,EAAC,aAAc,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,GAAG,CAAA;AAC9D,EAAA,MAAM,KAAA,GAAQ,CAAC,MAAA,KAAmC;AAChD,IAAA,IAAI,MAAA,IAAU,MAAM,OAAO,IAAA;AAC3B,IAAA,IAAI,YAAY,GAAA,CAAI,MAAM,GAAG,OAAO,WAAA,CAAY,IAAI,MAAM,CAAA;AAC1D,IAAA,IAAI,WAAW,GAAA,CAAI,MAAM,GAAG,OAAO,UAAA,CAAW,IAAI,MAAM,CAAA;AACxD,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,UAAA,GAAa,IAAI,SAAA,CAAU,IAAA;AAAA,IAAK,CAAC,MACrC,CAAA,CAAE,MAAA,EAAQ,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,QAAQ;AAAA,GAC7C;AAGA,EAAA,MAAM,aAAA,GAAgB,CAAC,OAAA,KAA4B;AACjD,IAAA,MAAM,EAAA,GAAA,CAAM,GAAA,CAAI,QAAA,IAAY,EAAC,EAAG,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,YAAA,KAAiB,OAAO,CAAA;AACtE,IAAA,OAAO,KAAM,SAAA,CAAU,GAAA,CAAI,EAAA,CAAG,OAAO,KAAK,CAAA,GAAK,CAAA;AAAA,EACjD,CAAA;AAEA,EAAA,MAAM,cAA2B,EAAC;AAClC,EAAA,KAAA,MAAW,CAAA,IAAK,IAAI,MAAA,EAAQ;AAC1B,IAAA,IAAI,CAAA,CAAE,SAAS,MAAA,EAAQ;AACvB,IAAA,MAAM,IAAA,GAAO,CAAA,CAAE,OAAA,IAAW,KAAA,CAAM,EAAE,IAAI,CAAA;AACtC,IAAA,MAAM,EAAA,GAAK,CAAA,CAAE,KAAA,IAAS,KAAA,CAAM,EAAE,EAAE,CAAA;AAChC,IAAA,IAAI,IAAA,IAAQ,IAAA,IAAQ,EAAA,IAAM,IAAA,EAAM;AAChC,IAAA,WAAA,CAAY,IAAA,CAAK;AAAA,MACf,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,UAAU,SAAA,CAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,EAAE,CAAC,CAAA;AAAA,MACtC,QAAQ,SAAA,CAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,EAAE,CAAC,CAAA;AAAA,MACpC,YAAA,EAAc,EAAE,YAAA,IAAgB,IAAA;AAAA,MAChC,gBAAA,EAAkB,aAAA,CAAc,CAAA,CAAE,EAAE,CAAA;AAAA,MACpC,MAAA,EAAQ,EAAE,MAAA,KAAW;AAAA,KACtB,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,YAA2B,GAAA,CAAI,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAC/D,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,IAChB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,MAAA,EAAQ,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,OAAO,CAAA,IAAK,CAAA;AAAA,IACpC,WAAA,EAAa,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,YAAY,CAAA,IAAK;AAAA,GAChD,CAAE,CAAA;AAEF,EAAA,MAAM,UAAA,GAAa,CACjB,CAAA,EACA,IAAA,EACA,EAAA,MACgB;AAAA,IAChB,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA;AAAA,IACA,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,IAAA,EAAM,EAAE,KAAA,GAAS,SAAA,CAAU,IAAI,CAAA,CAAE,KAAK,KAAK,CAAA,GAAK,CAAA;AAAA,IAChD,MAAA,EAAS,EAAE,MAAA,IAA2B,MAAA;AAAA,IACtC,IAAA,EAAM,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,OAAA,GAAU,OAAA;AAAA,IACrC,GAAI,EAAA,GAAK,EAAE,EAAA,KAAO;AAAC,GACrB,CAAA;AAGA,EAAA,MAAM,UAAwB,KAAA,CAAM,OAAA,CAAQ,IAAI,aAAa,CAAA,GACzD,IAAI,aAAA,CAAc,OAAA;AAAA,IAAQ,CAAC,CAAA,KAAA,CACxB,CAAA,CAAE,OAAA,IAAW,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,UAAA,CAAW,GAAG,CAAA,CAAE,IAAA,IAAQ,EAAA,EAAI,CAAA,CAAE,EAAE,CAAC;AAAA,GAChE,GAAA,CACC,GAAA,CAAI,OAAA,IAAW,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,EAAG,CAAA,CAAE,IAAA,IAAQ,EAAE,CAAC,CAAA;AAE9D,EAAA,MAAM,aAA6B,GAAA,CAAI,SAAA,IAAa,EAAC,EAAG,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAClE,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,IAAA,EAAM,EAAE,IAAA,IAAQ,EAAA;AAAA,IAChB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,GAAG,CAAA;AAAA,IACxB,KAAA,EAAO,UAAU,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,CAAA,IAAK,EAAE,CAAA,IAAK,CAAA;AAAA,IAC7C,KAAA,EAAO,UAAU,GAAA,CAAI,CAAA,CAAE,SAAS,CAAC,CAAA,IAAK,EAAE,CAAA,IAAK;AAAA,GAC/C,CAAE,CAAA;AAGF,EAAA,MAAM,mBAAsC,GAAA,CAAI,SAAA,CAC7C,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,GAAA,IAAO,CAAA,CAAE,OAAO,GAAA,IAAO,CAAA,CAAE,EAAE,CAAA,CAClD,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IACX,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,KAAA,EAAO,CAAA,CAAE,KAAA,IAAS,CAAA,CAAE,EAAA;AAAA,IACpB,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,EAAA,CAAI,GAAG,CAAA;AAAA,IAC5B,IAAA,EAAM,CAAA,CAAE,EAAA,CAAI,IAAA,KAAS,SAAS,MAAA,GAAS;AAAA,GACzC,CAAE,CAAA;AAEJ,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,CAAA;AAAA,IACA,aAAa,CAAA,GAAI,CAAA;AAAA,IACjB,GAAG,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,IAChC,GAAG,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,IAC5B,GAAG,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,KAAA,EAAO,CAAA,CAAE,KAAK,CAAC;AAAA,GAChD;AACA,EAAA,MAAM,IAAA,GAAO,UAAU,GAAG,CAAA;AAC1B,EAAA,OAAO;AAAA,IACL,UAAA;AAAA,IACA,IAAA;AAAA,IACA,eAAA,EAAiB,IAAA,IAAQ,GAAA,CAAI,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAE,CAAA,CAAE,MAAA,IAAU,CAAA;AAAA,IACtE,UAAA,EAAY,IAAA,IAAQ,GAAA,CAAI,UAAA,KAAe,UAAU,OAAA,GAAU,MAAA;AAAA,IAC3D,UAAA,EAAY,IAAA,GAAQ,GAAA,CAAI,UAAA,IAAc,IAAA,GAAQ,IAAA;AAAA,IAC9C,WAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA;AAAA,IACA,gBAAA;AAAA,IACA,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,QAAQ,CAAA;AAAA,IAC7B,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,QAAQ;AAAA,GAC/B;AACF","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.5.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",