@willcgage/module-schematic 0.4.0 → 0.6.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,6 +24,7 @@ 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: [],
@@ -37,12 +38,29 @@ function stateToDoc(state, recordNumber) {
37
38
  module: recordNumber,
38
39
  lengthInches: state.lengthInches,
39
40
  ...state.loop ? { loop: true } : {},
41
+ ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
40
42
  endplates: state.loop ? (
41
- // Single-endplate turnback: only A exists; the far end is the balloon.
42
- [{ id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] }]
43
+ // Balloon loop: A is the entry. A standard endplate B on the balloon
44
+ // makes it an INTERCHANGE (second route connects at the loop, e.g.
45
+ // Seaford); configB "none" makes it a pure turnback.
46
+ [
47
+ { id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
48
+ ...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
49
+ ]
43
50
  ) : [
44
51
  { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
45
- { id: "B", label: "East", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }
52
+ // Non-loop modules always have a real B ("none" never applies).
53
+ {
54
+ id: "B",
55
+ label: "East",
56
+ tracks: [
57
+ {
58
+ trackId: MAIN_TRACK_ID,
59
+ lane: 0,
60
+ config: state.configB === "none" ? "single" : state.configB
61
+ }
62
+ ]
63
+ }
46
64
  ],
47
65
  tracks: [
48
66
  state.loop ? (
@@ -50,7 +68,11 @@ function stateToDoc(state, recordNumber) {
50
68
  { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
51
69
  ) : { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" },
52
70
  // Double track: Main 2 is a real entity so turnouts/signals can attach.
71
+ // On a loop it exists only for a Main 2 directional return (the U joins
72
+ // the two lanes at the balloon); a same-main loop's parallel lead legs
73
+ // are ONE main.
53
74
  ...!state.loop && (state.configA === "double" || state.configB === "double") ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" }] : [],
75
+ ...state.loop && state.loopReturn === "main2" ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: state.lengthInches }] : [],
54
76
  ...state.extraTracks.map((t) => ({
55
77
  id: t.id,
56
78
  role: t.role,
@@ -59,7 +81,8 @@ function stateToDoc(state, recordNumber) {
59
81
  toPos: t.toPos,
60
82
  moduleTrackId: t.moduleTrackId,
61
83
  trackName: t.trackName || void 0,
62
- capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos)))
84
+ capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
85
+ ...state.loop && t.inLoop ? { inLoop: true } : {}
63
86
  }))
64
87
  ],
65
88
  turnouts: state.turnouts.map((t) => ({
@@ -111,7 +134,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
111
134
  fromPos: sc(t.fromPos ?? 0),
112
135
  toPos: t.toPos != null ? sc(t.toPos) : len,
113
136
  moduleTrackId,
114
- trackName: t.trackName ?? nameOf(moduleTrackId)
137
+ trackName: t.trackName ?? nameOf(moduleTrackId),
138
+ ...t.inLoop ? { inLoop: true } : {}
115
139
  });
116
140
  }
117
141
  }
@@ -144,11 +168,15 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
144
168
  const ep = (d.endplates ?? []).find((e) => e.id === id);
145
169
  return ep?.tracks?.[0]?.config === "double" ? "double" : "single";
146
170
  };
171
+ const loop = isLoopDoc(d);
172
+ const hasB = (d.endplates ?? []).some((e) => e.id === "B");
147
173
  return {
148
174
  lengthInches: len,
149
- loop: isLoopDoc(d),
175
+ loop,
176
+ loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
150
177
  configA: configOf("A"),
151
- configB: configOf("B"),
178
+ // On a loop, a missing B means pure turnback; present = interchange loop.
179
+ configB: loop && !hasB ? "none" : configOf("B"),
152
180
  extraTracks,
153
181
  turnouts: (d.turnouts ?? []).map((t) => ({
154
182
  id: t.id,
@@ -281,7 +309,8 @@ function moduleFeatures(doc) {
281
309
  fromFrac: clampFrac(Math.min(from, to)),
282
310
  toFrac: clampFrac(Math.max(from, to)),
283
311
  capacityFeet: t.capacityFeet ?? null,
284
- divergesFromLane: divergeOrigin(t.id)
312
+ divergesFromLane: divergeOrigin(t.id),
313
+ inLoop: t.inLoop === true
285
314
  });
286
315
  }
287
316
  const turnouts = (doc.turnouts ?? []).map((t) => ({
@@ -309,9 +338,13 @@ function moduleFeatures(doc) {
309
338
  ...extraTracks.map((t) => t.lane),
310
339
  ...signals.map((s) => s.lane)
311
340
  ];
341
+ const loop = isLoopDoc(doc);
312
342
  return {
313
343
  doubleMain,
314
- loop: isLoopDoc(doc),
344
+ loop,
345
+ loopInterchange: loop && doc.endplates.length >= 2,
346
+ loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
347
+ loopRender: loop ? doc.loopRender ?? null : null,
315
348
  extraTracks,
316
349
  turnouts,
317
350
  signals,
@@ -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}\nexport interface EditorState {\n lengthInches: number;\n /** Single-endplate turnback (balloon loop): endplate B is replaced by the\n * loop; positions past the throat are inside the balloon. */\n loop: boolean;\n configA: TrackConfig;\n configB: TrackConfig;\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 ? // Single-endplate turnback: only A exists; the far end is the balloon.\n [{ id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] }]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n { id: \"B\", label: \"East\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] },\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 return {\n lengthInches: len,\n loop: isLoopDoc(d!),\n configA: configOf(\"A\"),\n configB: 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 /** Single-endplate turnback — draw a terminal bulb at the far end. */\n loop: 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 return {\n doubleMain,\n loop: isLoopDoc(doc),\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":";;;AAsHO,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;AAmEO,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,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,WAAW,KAAA,CAAM,IAAA;AAAA;AAAA;AAAA;AAAA,MAIb;AAAA,QACE,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,QAChG,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,QACA;AAAA,MACE,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,MAE/F;AAAA,QACE,EAAA,EAAI,GAAA;AAAA,QACJ,KAAA,EAAO,MAAA;AAAA,QACP,MAAA,EAAQ;AAAA,UACN;AAAA,YACE,OAAA,EAAS,aAAA;AAAA,YACT,IAAA,EAAM,CAAA;AAAA,YACN,MAAA,EAAQ,KAAA,CAAM,OAAA,KAAY,MAAA,GAAS,WAAW,KAAA,CAAM;AAAA;AACtD;AACF;AACF,KACF;AAAA,IACJ,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,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,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;AAC1D,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,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,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;AAoEO,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,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;AAAA,GAC9B;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,IAAU,CAAA;AAAA,IACjD,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,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)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n signals: EditorCpSignal[];\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -41,6 +41,10 @@ interface SchematicTrack {
41
41
  moduleTrackId?: number | null;
42
42
  /** Owner's track name, mirrored to module_tracks.track_name. */
43
43
  trackName?: string;
44
+ /** Inside the balloon of a loop module (#165): pos still measures from
45
+ * endplate A (past the throat = in the loop), lane is the ladder/arc index —
46
+ * one record drives both the unrolled fan and a geometric render. */
47
+ inLoop?: boolean;
44
48
  }
45
49
  interface SchematicTurnout {
46
50
  id: string;
@@ -92,6 +96,15 @@ interface ModuleSchematicDoc {
92
96
  * instead of a second endplate. Also implied by a single-entry endplates
93
97
  * array (a category:"loop" module like Seaford). */
94
98
  loop?: boolean;
99
+ /** Where the balloon returns (#165): "same" (default) turns back onto the
100
+ * same main; "main2" is a directional return on a double-track main —
101
+ * out on Main 1, back on Main 2 — drawn as a U joining the two lanes
102
+ * (the transit terminal-loop idiom). */
103
+ loopReturn?: "same" | "main2";
104
+ /** Optional rendering override: "bulb" (abstract terminal), "fan" (interior
105
+ * tracks unrolled as a ladder past the throat — the default when inLoop
106
+ * tracks exist), "geometric" (drawn balloon, AL&E-style). */
107
+ loopRender?: "bulb" | "fan" | "geometric";
95
108
  endplates: SchematicEndplate[];
96
109
  tracks: SchematicTrack[];
97
110
  turnouts?: SchematicTurnout[];
@@ -124,6 +137,8 @@ interface EditorTrack {
124
137
  moduleTrackId: number | null;
125
138
  /** Owner's track name → module_tracks.track_name. */
126
139
  trackName: string;
140
+ /** Inside the balloon of a loop module (#165). */
141
+ inLoop?: boolean;
127
142
  }
128
143
  /** A module_tracks row as loaded for the editor. */
129
144
  interface ModuleTrackRow {
@@ -152,13 +167,21 @@ interface EditorControlPoint {
152
167
  turnouts: string[];
153
168
  signals: EditorCpSignal[];
154
169
  }
170
+ /** Endplate B on a loop module: a standard endplate makes the balloon an
171
+ * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
172
+ * makes it a pure turnback. Non-loop modules always have a real B. */
173
+ type EndplateBConfig = TrackConfig | "none";
155
174
  interface EditorState {
156
175
  lengthInches: number;
157
- /** Single-endplate turnback (balloon loop): endplate B is replaced by the
158
- * loop; positions past the throat are inside the balloon. */
176
+ /** Balloon loop: the main runs the lead and turns back; positions past the
177
+ * throat are inside the balloon. Endplate B stays independently available —
178
+ * present = interchange loop, "none" = pure turnback. */
159
179
  loop: boolean;
180
+ /** Where the balloon returns: same main, or Main 2 (directional return on a
181
+ * double-track main — drawn as a U joining the two lanes). */
182
+ loopReturn: "same" | "main2";
160
183
  configA: TrackConfig;
161
- configB: TrackConfig;
184
+ configB: EndplateBConfig;
162
185
  extraTracks: EditorTrack[];
163
186
  turnouts: EditorTurnout[];
164
187
  controlPoints: EditorControlPoint[];
@@ -198,6 +221,8 @@ interface DrawTrack {
198
221
  * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;
199
222
  * without this, renderers draw what looks like a crossover. */
200
223
  divergesFromLane: number;
224
+ /** Inside the balloon of a loop module (#165). */
225
+ inLoop: boolean;
201
226
  }
202
227
  interface DrawTurnout {
203
228
  id: string;
@@ -220,8 +245,17 @@ interface DrawSignal {
220
245
  interface ModuleFeatures {
221
246
  /** Whether either endplate declares a double-track main. */
222
247
  doubleMain: boolean;
223
- /** Single-endplate turnback — draw a terminal bulb at the far end. */
248
+ /** Balloon loop — draw a terminal bulb at the far end. */
224
249
  loop: boolean;
250
+ /** Loop with a standard endplate B on the balloon: an interchange — a
251
+ * second route connects at the loop (draw an endplate branch off the bulb). */
252
+ loopInterchange: boolean;
253
+ /** Where the balloon returns: "main2" = directional return on a double-track
254
+ * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */
255
+ loopReturn: "same" | "main2";
256
+ /** Rendering override from the doc; renderers may ignore modes they don't
257
+ * implement yet ("geometric"). */
258
+ loopRender: "bulb" | "fan" | "geometric" | null;
225
259
  /** Non-main tracks (sidings/spurs/yard/crossover). */
226
260
  extraTracks: DrawTrack[];
227
261
  turnouts: DrawTurnout[];
@@ -241,4 +275,4 @@ interface ModuleFeatures {
241
275
  */
242
276
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
243
277
 
244
- export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, 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 };
278
+ export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,10 @@ interface SchematicTrack {
41
41
  moduleTrackId?: number | null;
42
42
  /** Owner's track name, mirrored to module_tracks.track_name. */
43
43
  trackName?: string;
44
+ /** Inside the balloon of a loop module (#165): pos still measures from
45
+ * endplate A (past the throat = in the loop), lane is the ladder/arc index —
46
+ * one record drives both the unrolled fan and a geometric render. */
47
+ inLoop?: boolean;
44
48
  }
45
49
  interface SchematicTurnout {
46
50
  id: string;
@@ -92,6 +96,15 @@ interface ModuleSchematicDoc {
92
96
  * instead of a second endplate. Also implied by a single-entry endplates
93
97
  * array (a category:"loop" module like Seaford). */
94
98
  loop?: boolean;
99
+ /** Where the balloon returns (#165): "same" (default) turns back onto the
100
+ * same main; "main2" is a directional return on a double-track main —
101
+ * out on Main 1, back on Main 2 — drawn as a U joining the two lanes
102
+ * (the transit terminal-loop idiom). */
103
+ loopReturn?: "same" | "main2";
104
+ /** Optional rendering override: "bulb" (abstract terminal), "fan" (interior
105
+ * tracks unrolled as a ladder past the throat — the default when inLoop
106
+ * tracks exist), "geometric" (drawn balloon, AL&E-style). */
107
+ loopRender?: "bulb" | "fan" | "geometric";
95
108
  endplates: SchematicEndplate[];
96
109
  tracks: SchematicTrack[];
97
110
  turnouts?: SchematicTurnout[];
@@ -124,6 +137,8 @@ interface EditorTrack {
124
137
  moduleTrackId: number | null;
125
138
  /** Owner's track name → module_tracks.track_name. */
126
139
  trackName: string;
140
+ /** Inside the balloon of a loop module (#165). */
141
+ inLoop?: boolean;
127
142
  }
128
143
  /** A module_tracks row as loaded for the editor. */
129
144
  interface ModuleTrackRow {
@@ -152,13 +167,21 @@ interface EditorControlPoint {
152
167
  turnouts: string[];
153
168
  signals: EditorCpSignal[];
154
169
  }
170
+ /** Endplate B on a loop module: a standard endplate makes the balloon an
171
+ * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
172
+ * makes it a pure turnback. Non-loop modules always have a real B. */
173
+ type EndplateBConfig = TrackConfig | "none";
155
174
  interface EditorState {
156
175
  lengthInches: number;
157
- /** Single-endplate turnback (balloon loop): endplate B is replaced by the
158
- * loop; positions past the throat are inside the balloon. */
176
+ /** Balloon loop: the main runs the lead and turns back; positions past the
177
+ * throat are inside the balloon. Endplate B stays independently available —
178
+ * present = interchange loop, "none" = pure turnback. */
159
179
  loop: boolean;
180
+ /** Where the balloon returns: same main, or Main 2 (directional return on a
181
+ * double-track main — drawn as a U joining the two lanes). */
182
+ loopReturn: "same" | "main2";
160
183
  configA: TrackConfig;
161
- configB: TrackConfig;
184
+ configB: EndplateBConfig;
162
185
  extraTracks: EditorTrack[];
163
186
  turnouts: EditorTurnout[];
164
187
  controlPoints: EditorControlPoint[];
@@ -198,6 +221,8 @@ interface DrawTrack {
198
221
  * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;
199
222
  * without this, renderers draw what looks like a crossover. */
200
223
  divergesFromLane: number;
224
+ /** Inside the balloon of a loop module (#165). */
225
+ inLoop: boolean;
201
226
  }
202
227
  interface DrawTurnout {
203
228
  id: string;
@@ -220,8 +245,17 @@ interface DrawSignal {
220
245
  interface ModuleFeatures {
221
246
  /** Whether either endplate declares a double-track main. */
222
247
  doubleMain: boolean;
223
- /** Single-endplate turnback — draw a terminal bulb at the far end. */
248
+ /** Balloon loop — draw a terminal bulb at the far end. */
224
249
  loop: boolean;
250
+ /** Loop with a standard endplate B on the balloon: an interchange — a
251
+ * second route connects at the loop (draw an endplate branch off the bulb). */
252
+ loopInterchange: boolean;
253
+ /** Where the balloon returns: "main2" = directional return on a double-track
254
+ * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */
255
+ loopReturn: "same" | "main2";
256
+ /** Rendering override from the doc; renderers may ignore modes they don't
257
+ * implement yet ("geometric"). */
258
+ loopRender: "bulb" | "fan" | "geometric" | null;
225
259
  /** Non-main tracks (sidings/spurs/yard/crossover). */
226
260
  extraTracks: DrawTrack[];
227
261
  turnouts: DrawTurnout[];
@@ -241,4 +275,4 @@ interface ModuleFeatures {
241
275
  */
242
276
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
243
277
 
244
- export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, 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 };
278
+ export { type DrawSignal, type DrawTrack, type DrawTurnout, type EditorControlPoint, type EditorCpSignal, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, buildPassingSiding, docToState, emptyEditorState, inchesToScaleFeet, isLoopDoc, moduleFeatures, nextId, scaleFeetToInches, stateToDoc };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ 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: [],
@@ -35,12 +36,29 @@ function stateToDoc(state, recordNumber) {
35
36
  module: recordNumber,
36
37
  lengthInches: state.lengthInches,
37
38
  ...state.loop ? { loop: true } : {},
39
+ ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
38
40
  endplates: state.loop ? (
39
- // Single-endplate turnback: only A exists; the far end is the balloon.
40
- [{ id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] }]
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
+ ]
41
48
  ) : [
42
49
  { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
43
- { id: "B", label: "East", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }
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
+ }
60
+ ]
61
+ }
44
62
  ],
45
63
  tracks: [
46
64
  state.loop ? (
@@ -48,7 +66,11 @@ function stateToDoc(state, recordNumber) {
48
66
  { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
49
67
  ) : { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" },
50
68
  // Double track: Main 2 is a real entity so turnouts/signals can attach.
69
+ // On a loop it exists only for a Main 2 directional return (the U joins
70
+ // the two lanes at the balloon); a same-main loop's parallel lead legs
71
+ // are ONE main.
51
72
  ...!state.loop && (state.configA === "double" || state.configB === "double") ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" }] : [],
73
+ ...state.loop && state.loopReturn === "main2" ? [{ id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: state.lengthInches }] : [],
52
74
  ...state.extraTracks.map((t) => ({
53
75
  id: t.id,
54
76
  role: t.role,
@@ -57,7 +79,8 @@ function stateToDoc(state, recordNumber) {
57
79
  toPos: t.toPos,
58
80
  moduleTrackId: t.moduleTrackId,
59
81
  trackName: t.trackName || void 0,
60
- capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos)))
82
+ capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
83
+ ...state.loop && t.inLoop ? { inLoop: true } : {}
61
84
  }))
62
85
  ],
63
86
  turnouts: state.turnouts.map((t) => ({
@@ -109,7 +132,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
109
132
  fromPos: sc(t.fromPos ?? 0),
110
133
  toPos: t.toPos != null ? sc(t.toPos) : len,
111
134
  moduleTrackId,
112
- trackName: t.trackName ?? nameOf(moduleTrackId)
135
+ trackName: t.trackName ?? nameOf(moduleTrackId),
136
+ ...t.inLoop ? { inLoop: true } : {}
113
137
  });
114
138
  }
115
139
  }
@@ -142,11 +166,15 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
142
166
  const ep = (d.endplates ?? []).find((e) => e.id === id);
143
167
  return ep?.tracks?.[0]?.config === "double" ? "double" : "single";
144
168
  };
169
+ const loop = isLoopDoc(d);
170
+ const hasB = (d.endplates ?? []).some((e) => e.id === "B");
145
171
  return {
146
172
  lengthInches: len,
147
- loop: isLoopDoc(d),
173
+ loop,
174
+ loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
148
175
  configA: configOf("A"),
149
- configB: configOf("B"),
176
+ // On a loop, a missing B means pure turnback; present = interchange loop.
177
+ configB: loop && !hasB ? "none" : configOf("B"),
150
178
  extraTracks,
151
179
  turnouts: (d.turnouts ?? []).map((t) => ({
152
180
  id: t.id,
@@ -279,7 +307,8 @@ function moduleFeatures(doc) {
279
307
  fromFrac: clampFrac(Math.min(from, to)),
280
308
  toFrac: clampFrac(Math.max(from, to)),
281
309
  capacityFeet: t.capacityFeet ?? null,
282
- divergesFromLane: divergeOrigin(t.id)
310
+ divergesFromLane: divergeOrigin(t.id),
311
+ inLoop: t.inLoop === true
283
312
  });
284
313
  }
285
314
  const turnouts = (doc.turnouts ?? []).map((t) => ({
@@ -307,9 +336,13 @@ function moduleFeatures(doc) {
307
336
  ...extraTracks.map((t) => t.lane),
308
337
  ...signals.map((s) => s.lane)
309
338
  ];
339
+ const loop = isLoopDoc(doc);
310
340
  return {
311
341
  doubleMain,
312
- loop: isLoopDoc(doc),
342
+ loop,
343
+ loopInterchange: loop && doc.endplates.length >= 2,
344
+ loopReturn: loop && doc.loopReturn === "main2" ? "main2" : "same",
345
+ loopRender: loop ? doc.loopRender ?? null : null,
313
346
  extraTracks,
314
347
  turnouts,
315
348
  signals,
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}\nexport interface EditorState {\n lengthInches: number;\n /** Single-endplate turnback (balloon loop): endplate B is replaced by the\n * loop; positions past the throat are inside the balloon. */\n loop: boolean;\n configA: TrackConfig;\n configB: TrackConfig;\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 ? // Single-endplate turnback: only A exists; the far end is the balloon.\n [{ id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] }]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n { id: \"B\", label: \"East\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] },\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 return {\n lengthInches: len,\n loop: isLoopDoc(d!),\n configA: configOf(\"A\"),\n configB: 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 /** Single-endplate turnback — draw a terminal bulb at the far end. */\n loop: 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 return {\n doubleMain,\n loop: isLoopDoc(doc),\n extraTracks,\n turnouts,\n signals,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East)\n label?: string | null;\n tracks?: SchematicEndplateTrack[];\n}\nexport interface SchematicTrack {\n id: string;\n role: TrackRole;\n lane: number;\n from?: string;\n to?: string;\n fromPos?: number | null;\n toPos?: number | null;\n capacityFeet?: number | null;\n industryRef?: number | null;\n /** The module_tracks row this track is (single source of truth); null = new. */\n moduleTrackId?: number | null;\n /** Owner's track name, mirrored to module_tracks.track_name. */\n trackName?: string;\n /** Inside the balloon of a loop module (#165): pos still measures from\n * endplate A (past the throat = in the loop), lane is the ladder/arc index —\n * one record drives both the unrolled fan and a geometric render. */\n inLoop?: boolean;\n}\nexport interface SchematicTurnout {\n id: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind?: TurnoutKind;\n name?: string | null;\n address?: string | null;\n}\nexport interface SchematicSignal {\n id: string;\n pos: number;\n /** Track the signal governs; absent = the primary main (lane 0). */\n track?: string;\n facing?: SignalFacing;\n kind?: \"mast\" | \"dwarf\";\n name?: string | null;\n aspects?: string[];\n /** Which side of the track the signal sits on (#122). */\n side?: SignalSide;\n /** Turnout this control point governs; absent = standalone block signal. */\n turnout?: string;\n}\nexport interface SchematicBlock {\n id: string;\n name: string;\n tracks?: string[];\n from: number;\n to: number;\n}\n/**\n * A control point is an interlocking: a named group of one or more signals and\n * zero or more turnouts. A passing siding has two (West/East); a lone block\n * signal is a control point with one signal and no turnouts.\n */\nexport interface SchematicControlPoint {\n id: string;\n name?: string | null;\n turnouts?: string[];\n signals?: SchematicSignal[];\n}\nexport interface ModuleSchematicDoc {\n version: number;\n module?: string;\n lengthInches?: number;\n /** Single-endplate turnback (balloon loop): the main enters at A, runs the\n * lead, and turns back — renderers draw a terminal bulb at the far end\n * instead of a second endplate. Also implied by a single-entry endplates\n * array (a category:\"loop\" module like Seaford). */\n loop?: boolean;\n /** Where the balloon returns (#165): \"same\" (default) turns back onto the\n * same main; \"main2\" is a directional return on a double-track main —\n * out on Main 1, back on Main 2 — drawn as a U joining the two lanes\n * (the transit terminal-loop idiom). */\n loopReturn?: \"same\" | \"main2\";\n /** Optional rendering override: \"bulb\" (abstract terminal), \"fan\" (interior\n * tracks unrolled as a ladder past the throat — the default when inLoop\n * tracks exist), \"geometric\" (drawn balloon, AL&E-style). */\n loopRender?: \"bulb\" | \"fan\" | \"geometric\";\n endplates: SchematicEndplate[];\n tracks: SchematicTrack[];\n turnouts?: SchematicTurnout[];\n controlPoints?: SchematicControlPoint[];\n /** @deprecated pre-grouping flat signals; read for back-compat. */\n signals?: SchematicSignal[];\n}\n\n/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */\nexport function isLoopDoc(doc: ModuleSchematicDoc): boolean {\n return doc.loop === true || doc.endplates.length === 1;\n}\n\nexport const MAIN_TRACK_ID = \"main\";\n/** The second main on double-track modules — a real track entity so turnouts\n * and signals can attach to it (a spur off Main 2 must diverge from lane 1,\n * not draw a crossover from Main 1). Emitted by stateToDoc when either\n * endplate is double; legacy docs without it still parse. */\nexport const MAIN2_TRACK_ID = \"main2\";\n\n// North American N scale (1:160): 396 real inches → 5280 scale feet = one mile.\nexport const N_SCALE_RATIO = 160;\n/** Real inches on the module → scale feet of prototype track represented. */\nexport function inchesToScaleFeet(inches: number, ratio = N_SCALE_RATIO): number {\n return (inches * ratio) / 12;\n}\n/** Scale feet of prototype track → real inches on the module. */\nexport function scaleFeetToInches(feet: number, ratio = N_SCALE_RATIO): number {\n return (feet * 12) / ratio;\n}\n\n/** Parse a jsonb value into a schematic doc, or null if it isn't one. */\nexport function asModuleSchematic(x: unknown): ModuleSchematicDoc | null {\n if (!x || typeof x !== \"object\") return null;\n const d = x as Record<string, unknown>;\n if (typeof d.version !== \"number\") return null;\n if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;\n return d as unknown as ModuleSchematicDoc;\n}\n\n// ---- Editor state (a flatter shape an authoring form binds to) -------------\n\nexport interface EditorTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromPos: number;\n toPos: number;\n /** module_tracks row id (single source of truth), or null for a new track. */\n moduleTrackId: number | null;\n /** Owner's track name → module_tracks.track_name. */\n trackName: string;\n /** Inside the balloon of a loop module (#165). */\n inLoop?: boolean;\n}\n\n/** A module_tracks row as loaded for the editor. */\nexport interface ModuleTrackRow {\n id: number;\n track_name: string | null;\n capacity_scale_feet: number | null;\n}\nexport interface EditorTurnout {\n id: string;\n name: string;\n pos: number;\n onTrack: string;\n divergeTrack: string;\n kind: TurnoutKind;\n}\nexport interface EditorCpSignal {\n id: string;\n pos: number;\n track: string;\n facing: SignalFacing;\n side: SignalSide;\n}\nexport interface EditorControlPoint {\n id: string;\n name: string;\n turnouts: string[]; // turnout ids grouped under this control point\n signals: EditorCpSignal[];\n}\n/** Endplate B on a loop module: a standard endplate makes the balloon an\n * INTERCHANGE (a second route connects at the loop, e.g. Seaford); \"none\"\n * makes it a pure turnback. Non-loop modules always have a real B. */\nexport type EndplateBConfig = TrackConfig | \"none\";\n\nexport interface EditorState {\n lengthInches: number;\n /** Balloon loop: the main runs the lead and turns back; positions past the\n * throat are inside the balloon. Endplate B stays independently available —\n * present = interchange loop, \"none\" = pure turnback. */\n loop: boolean;\n /** Where the balloon returns: same main, or Main 2 (directional return on a\n * double-track main — drawn as a U joining the two lanes). */\n loopReturn: \"same\" | \"main2\";\n configA: TrackConfig;\n configB: EndplateBConfig;\n extraTracks: EditorTrack[]; // sidings/spurs/…; the main track is implicit\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n}\n\n/** Build the empty editor state for a module of the given length. */\nexport function emptyEditorState(lengthInches: number): EditorState {\n return {\n lengthInches: lengthInches > 0 ? lengthInches : 24,\n loop: false,\n loopReturn: \"same\",\n configA: \"single\",\n configB: \"single\",\n extraTracks: [],\n turnouts: [],\n controlPoints: [],\n };\n}\n\n/** Assemble a spec-conformant doc from the editor state. */\nexport function stateToDoc(\n state: EditorState,\n recordNumber: string,\n): ModuleSchematicDoc {\n return {\n version: 1,\n module: recordNumber,\n lengthInches: state.lengthInches,\n ...(state.loop ? { loop: true } : {}),\n ...(state.loop && state.loopReturn === \"main2\" ? { loopReturn: \"main2\" as const } : {}),\n endplates: state.loop\n ? // Balloon loop: A is the entry. A standard endplate B on the balloon\n // makes it an INTERCHANGE (second route connects at the loop, e.g.\n // Seaford); configB \"none\" makes it a pure turnback.\n [\n { id: \"A\", label: \"Entry\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n ...(state.configB !== \"none\"\n ? [{ id: \"B\", label: \"Interchange\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }]\n : []),\n ]\n : [\n { id: \"A\", label: \"West\", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },\n // Non-loop modules always have a real B (\"none\" never applies).\n {\n id: \"B\",\n label: \"East\",\n tracks: [\n {\n trackId: MAIN_TRACK_ID,\n lane: 0,\n config: state.configB === \"none\" ? \"single\" : state.configB,\n },\n ],\n },\n ],\n tracks: [\n state.loop\n ? // The main runs the lead from A and turns back at the balloon.\n { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, fromPos: 0, toPos: state.lengthInches }\n : { id: MAIN_TRACK_ID, role: \"main\" as const, lane: 0, from: \"A\", to: \"B\" },\n // Double track: Main 2 is a real entity so turnouts/signals can attach.\n // On a loop it exists only for a Main 2 directional return (the U joins\n // the two lanes at the balloon); a same-main loop's parallel lead legs\n // are ONE main.\n ...(!state.loop && (state.configA === \"double\" || state.configB === \"double\")\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, from: \"A\", to: \"B\" }]\n : []),\n ...(state.loop && state.loopReturn === \"main2\"\n ? [{ id: MAIN2_TRACK_ID, role: \"main\" as const, lane: 1, fromPos: 0, toPos: state.lengthInches }]\n : []),\n ...state.extraTracks.map((t) => ({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromPos: t.fromPos,\n toPos: t.toPos,\n moduleTrackId: t.moduleTrackId,\n trackName: t.trackName || undefined,\n capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),\n ...(state.loop && t.inLoop ? { inLoop: true } : {}),\n })),\n ],\n turnouts: state.turnouts.map((t) => ({\n id: t.id,\n pos: t.pos,\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: t.kind,\n name: t.name || undefined,\n })),\n controlPoints: state.controlPoints.map((c) => ({\n id: c.id,\n name: c.name,\n turnouts: c.turnouts,\n signals: c.signals.map((s) => ({\n id: s.id,\n pos: s.pos,\n track: s.track,\n facing: s.facing,\n kind: \"mast\" as const,\n side: s.side,\n })),\n })),\n };\n}\n\n/**\n * Derive editor state from the doc and the module's Track section rows. Tracks\n * are the single source of truth for name/capacity (module_tracks), while the\n * schematic doc adds geometry (lane, positions). We merge: doc tracks first\n * (they carry geometry + their moduleTrackId link), then any module_tracks not\n * yet positioned in the schematic.\n */\nexport function docToState(\n doc: unknown,\n fallbackLength: number,\n moduleTracks: ModuleTrackRow[] = [],\n): EditorState {\n const base = emptyEditorState(fallbackLength);\n const d =\n doc && typeof doc === \"object\" ? (doc as ModuleSchematicDoc) : null;\n const hasDoc = !!d && typeof d.lengthInches === \"number\" && Array.isArray(d.tracks);\n // The module's length is authoritative (the mainline is the module). If the\n // saved doc used a different length, rescale its feature positions to fit so\n // the mainline always reads as the module's true length.\n const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d!.lengthInches! : 24;\n const docLen = hasDoc && d!.lengthInches! > 0 ? d!.lengthInches! : len;\n const scale = docLen > 0 ? len / docLen : 1;\n const sc = (p: number) => Math.round(p * scale);\n\n const nameOf = (id: number | null | undefined): string => {\n const mt = id != null ? moduleTracks.find((m) => m.id === id) : undefined;\n return mt?.track_name ?? \"\";\n };\n\n const extraTracks: EditorTrack[] = [];\n const usedMt = new Set<number>();\n if (hasDoc) {\n for (const t of d!.tracks) {\n if (t.role === \"main\") continue;\n const moduleTrackId = t.moduleTrackId ?? null;\n if (moduleTrackId != null) usedMt.add(moduleTrackId);\n extraTracks.push({\n id: t.id,\n role: (t.role as TrackRole) ?? \"siding\",\n lane: t.lane ?? 1,\n fromPos: sc(t.fromPos ?? 0),\n toPos: t.toPos != null ? sc(t.toPos) : len,\n moduleTrackId,\n trackName: t.trackName ?? nameOf(moduleTrackId),\n ...(t.inLoop ? { inLoop: true } : {}),\n });\n }\n }\n // Link pre-migration doc tracks (no moduleTrackId yet) to unused module_tracks\n // by order — keeping the doc track's id so turnout/signal references stay\n // valid. Only after that do leftover module_tracks become new tracks.\n const unused = moduleTracks.filter((mt) => !usedMt.has(mt.id));\n let ui = 0;\n for (const et of extraTracks) {\n if (et.moduleTrackId == null && ui < unused.length) {\n const mt = unused[ui++];\n et.moduleTrackId = mt.id;\n if (!et.trackName) et.trackName = mt.track_name ?? \"\";\n usedMt.add(mt.id);\n }\n }\n let lane = Math.max(0, ...extraTracks.map((t) => t.lane));\n for (const mt of moduleTracks) {\n if (usedMt.has(mt.id)) continue;\n lane += 1;\n extraTracks.push({\n id: `mt${mt.id}`,\n role: \"siding\",\n lane,\n fromPos: Math.round(len * 0.2),\n toPos: Math.round(len * 0.8),\n moduleTrackId: mt.id,\n trackName: mt.track_name ?? \"\",\n });\n }\n\n if (!hasDoc) return { ...base, lengthInches: len, extraTracks };\n\n const configOf = (id: string): TrackConfig => {\n const ep = (d!.endplates ?? []).find((e) => e.id === id);\n return ep?.tracks?.[0]?.config === \"double\" ? \"double\" : \"single\";\n };\n const loop = isLoopDoc(d!);\n const hasB = (d!.endplates ?? []).some((e) => e.id === \"B\");\n return {\n lengthInches: len,\n loop,\n loopReturn: loop && d!.loopReturn === \"main2\" ? \"main2\" : \"same\",\n configA: configOf(\"A\"),\n // On a loop, a missing B means pure turnback; present = interchange loop.\n configB: loop && !hasB ? \"none\" : configOf(\"B\"),\n extraTracks,\n turnouts: (d!.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n pos: sc(t.pos),\n onTrack: t.onTrack,\n divergeTrack: t.divergeTrack,\n kind: (t.kind as TurnoutKind) ?? \"right\",\n })),\n controlPoints: readControlPoints(d!, sc),\n };\n}\n\n/** Control points from a doc, migrating pre-grouping flat signals into groups. */\nfunction readControlPoints(\n d: ModuleSchematicDoc,\n sc: (p: number) => number = (p) => p,\n): EditorControlPoint[] {\n if (Array.isArray(d.controlPoints)) {\n return d.controlPoints.map((c) => ({\n id: c.id,\n name: c.name ?? \"\",\n turnouts: c.turnouts ?? [],\n signals: (c.signals ?? []).map((s) => ({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n })),\n }));\n }\n // Back-compat: group old flat signals by their turnout (or standalone).\n const groups = new Map<string, EditorControlPoint>();\n let n = 0;\n for (const s of d.signals ?? []) {\n const key = s.turnout || `blk-${s.id}`;\n let cp = groups.get(key);\n if (!cp) {\n cp = { id: `cp${++n}`, name: s.name ?? \"\", turnouts: s.turnout ? [s.turnout] : [], signals: [] };\n groups.set(key, cp);\n }\n cp.signals.push({\n id: s.id,\n pos: sc(s.pos),\n track: s.track ?? MAIN_TRACK_ID,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: (s.side as SignalSide) ?? \"above\",\n });\n }\n return [...groups.values()];\n}\n\n/** Find an unused `${prefix}${n}` id given the ones already present. */\nexport function nextId(prefix: string, existing: string[]): string {\n let n = 1;\n while (existing.includes(`${prefix}${n}`)) n += 1;\n return `${prefix}${n}`;\n}\n\n/**\n * Build a passing siding as one unit: the siding track, a switch at each end,\n * and control-point signals for both directions at each end (prototype Station\n * Entering Signal). Returns the new items to merge into the editor state.\n */\nexport function buildPassingSiding(state: EditorState): {\n track: EditorTrack;\n turnouts: EditorTurnout[];\n controlPoints: EditorControlPoint[];\n} {\n const len = state.lengthInches > 0 ? state.lengthInches : 24;\n const inset = Math.max(6, Math.round(len * 0.08));\n const fromPos = inset;\n const toPos = Math.max(fromPos + 1, len - inset);\n // First free lane above the main(s): lane 1 is Main 2 on double modules.\n const baseLane =\n state.configA === \"double\" || state.configB === \"double\" ? 2 : 1;\n const lane = Math.max(baseLane, ...state.extraTracks.map((t) => t.lane + 1));\n\n const trackIds = [MAIN_TRACK_ID, ...state.extraTracks.map((t) => t.id)];\n const sidId = nextId(\"sid\", trackIds);\n const track: EditorTrack = {\n id: sidId,\n role: \"siding\",\n lane,\n fromPos,\n toPos,\n moduleTrackId: null,\n trackName: \"Passing siding\",\n };\n\n const swIds = state.turnouts.map((t) => t.id);\n const swW = nextId(\"sw\", swIds);\n const swE = nextId(\"sw\", [...swIds, swW]);\n const turnouts: EditorTurnout[] = [\n { id: swW, name: \"West Siding\", pos: fromPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"right\" },\n { id: swE, name: \"East Siding\", pos: toPos, onTrack: MAIN_TRACK_ID, divergeTrack: sidId, kind: \"left\" },\n ];\n\n // One control point at each end, each grouping its switch and both-direction\n // signals on the main (prototype Station Entering Signal).\n const cpIds = state.controlPoints.map((c) => c.id);\n const cpW = nextId(\"cp\", cpIds);\n const cpE = nextId(\"cp\", [...cpIds, cpW]);\n const sig = (cpId: string, pos: number, facing: SignalFacing): EditorCpSignal => ({\n id: `${cpId}-${facing}`,\n pos,\n track: MAIN_TRACK_ID,\n facing,\n // opposite directions on opposite sides so they never overlap\n side: facing === \"AtoB\" ? \"above\" : \"below\",\n });\n const controlPoints: EditorControlPoint[] = [\n { id: cpW, name: \"West Siding\", turnouts: [swW], signals: [sig(cpW, fromPos, \"AtoB\"), sig(cpW, fromPos, \"BtoA\")] },\n { id: cpE, name: \"East Siding\", turnouts: [swE], signals: [sig(cpE, toPos, \"AtoB\"), sig(cpE, toPos, \"BtoA\")] },\n ];\n\n return { track, turnouts, controlPoints };\n}\n\n// ---- Pure feature resolver (both renderers draw these) --------------------\n\nexport interface DrawTrack {\n id: string;\n role: TrackRole;\n lane: number;\n fromFrac: number;\n toFrac: number;\n capacityFeet: number | null;\n /** Lane of the main this track diverges from (via its turnout) — the origin\n * of the diverge diagonal. A spur off Main 2 starts at lane 1, not lane 0;\n * without this, renderers draw what looks like a crossover. */\n divergesFromLane: number;\n /** Inside the balloon of a loop module (#165). */\n inLoop: boolean;\n}\nexport interface DrawTurnout {\n id: string;\n name: string;\n posFrac: number;\n onLane: number;\n divergeLane: number;\n}\nexport interface DrawSignal {\n id: string;\n name: string;\n posFrac: number;\n lane: number;\n facing: SignalFacing;\n side: SignalSide;\n /** Owning control point's id, when the signal came from a CP group — lets a\n * renderer join the drawn signal back to interlocking-level state (aspects). */\n cp?: string;\n}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Balloon loop — draw a terminal bulb at the far end. */\n loop: boolean;\n /** Loop with a standard endplate B on the balloon: an interchange — a\n * second route connects at the loop (draw an endplate branch off the bulb). */\n loopInterchange: boolean;\n /** Where the balloon returns: \"main2\" = directional return on a double-track\n * main — draw a U joining lanes 0 and 1 instead of the bulb (#165). */\n loopReturn: \"same\" | \"main2\";\n /** Rendering override from the doc; renderers may ignore modes they don't\n * implement yet (\"geometric\"). */\n loopRender: \"bulb\" | \"fan\" | \"geometric\" | null;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\n /** Lane extents across every feature (mains included; negative = outside\n * Main 1). Renderers size their vertical space from these. */\n laneMin: number;\n laneMax: number;\n}\n\n/**\n * Resolve a schematic doc into positioned drawables. `pos` (inches) becomes a\n * fraction of the module length; endplate A = 0, B = length; turnouts sit at\n * their pos. Tracks may carry explicit fromPos/toPos (overriding node lookup).\n * To-scale: a feature renders at its true position, clamped only to the\n * module's own extent — so signals near an end read at their real spot, not\n * bunched at an inset (#122).\n */\nexport function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures {\n const len =\n doc.lengthInches && doc.lengthInches > 0\n ? doc.lengthInches\n : Math.max(\n 1,\n ...doc.tracks.map((t) => Math.max(t.fromPos ?? 0, t.toPos ?? 0)),\n ...(doc.turnouts ?? []).map((t) => t.pos),\n );\n const clampFrac = (p: number) => Math.min(1, Math.max(0, p / len));\n\n const trackLane = new Map<string, number>();\n for (const t of doc.tracks) trackLane.set(t.id, t.lane);\n\n // Endplate positions: first endplate = West (0), the rest = East (len).\n const endplatePos = new Map<string, number>();\n doc.endplates.forEach((e, i) => endplatePos.set(e.id, i === 0 ? 0 : len));\n const turnoutPos = new Map<string, number>();\n for (const t of doc.turnouts ?? []) turnoutPos.set(t.id, t.pos);\n const posOf = (nodeId?: string): number | null => {\n if (nodeId == null) return null;\n if (endplatePos.has(nodeId)) return endplatePos.get(nodeId)!;\n if (turnoutPos.has(nodeId)) return turnoutPos.get(nodeId)!;\n return null;\n };\n\n const doubleMain = doc.endplates.some((e) =>\n e.tracks?.some((t) => t.config === \"double\"),\n );\n\n // The lane a track diverges from = the main its turnout sits on.\n const divergeOrigin = (trackId: string): number => {\n const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === trackId);\n return sw ? (trackLane.get(sw.onTrack) ?? 0) : 0;\n };\n\n const extraTracks: DrawTrack[] = [];\n for (const t of doc.tracks) {\n if (t.role === \"main\") continue; // the spine draws mains\n const from = t.fromPos ?? posOf(t.from);\n const to = t.toPos ?? posOf(t.to);\n if (from == null || to == null) continue; // can't place it\n extraTracks.push({\n id: t.id,\n role: t.role,\n lane: t.lane,\n fromFrac: clampFrac(Math.min(from, to)),\n toFrac: clampFrac(Math.max(from, to)),\n capacityFeet: t.capacityFeet ?? null,\n divergesFromLane: divergeOrigin(t.id),\n inLoop: t.inLoop === true,\n });\n }\n\n const turnouts: DrawTurnout[] = (doc.turnouts ?? []).map((t) => ({\n id: t.id,\n name: t.name ?? \"\",\n posFrac: clampFrac(t.pos),\n onLane: trackLane.get(t.onTrack) ?? 0,\n divergeLane: trackLane.get(t.divergeTrack) ?? 1,\n }));\n\n const drawSignal = (\n s: SchematicSignal,\n name: string,\n cp?: string,\n ): DrawSignal => ({\n id: s.id,\n name,\n posFrac: clampFrac(s.pos),\n lane: s.track ? (trackLane.get(s.track) ?? 0) : 0,\n facing: (s.facing as SignalFacing) ?? \"AtoB\",\n side: s.side === \"below\" ? \"below\" : \"above\",\n ...(cp ? { cp } : {}),\n });\n // Signals come from control-point groups; fall back to pre-grouping flat\n // signals for docs authored before the model changed.\n const signals: DrawSignal[] = Array.isArray(doc.controlPoints)\n ? doc.controlPoints.flatMap((c) =>\n (c.signals ?? []).map((s) => drawSignal(s, c.name ?? \"\", c.id)),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n const allLanes = [\n 0,\n doubleMain ? 1 : 0,\n ...extraTracks.map((t) => t.lane),\n ...signals.map((s) => s.lane),\n ];\n const loop = isLoopDoc(doc);\n return {\n doubleMain,\n loop,\n loopInterchange: loop && doc.endplates.length >= 2,\n loopReturn: loop && doc.loopReturn === \"main2\" ? \"main2\" : \"same\",\n loopRender: loop ? (doc.loopRender ?? null) : null,\n extraTracks,\n turnouts,\n signals,\n laneMin: Math.min(...allLanes),\n laneMax: Math.max(...allLanes),\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willcgage/module-schematic",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",