@willcgage/module-schematic 0.1.0 → 0.2.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
@@ -270,16 +270,17 @@ function moduleFeatures(doc) {
270
270
  onLane: trackLane.get(t.onTrack) ?? 0,
271
271
  divergeLane: trackLane.get(t.divergeTrack) ?? 1
272
272
  }));
273
- const drawSignal = (s, name) => ({
273
+ const drawSignal = (s, name, cp) => ({
274
274
  id: s.id,
275
275
  name,
276
276
  posFrac: clampFrac(s.pos),
277
277
  lane: s.track ? trackLane.get(s.track) ?? 0 : 0,
278
278
  facing: s.facing ?? "AtoB",
279
- side: s.side === "below" ? "below" : "above"
279
+ side: s.side === "below" ? "below" : "above",
280
+ ...cp ? { cp } : {}
280
281
  });
281
282
  const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
282
- (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? ""))
283
+ (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
283
284
  ) : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? ""));
284
285
  return { doubleMain, extraTracks, turnouts, signals };
285
286
  }
@@ -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 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\nexport const MAIN_TRACK_ID = \"main\";\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 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 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 endplates: [\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 { id: MAIN_TRACK_ID, role: \"main\", lane: 0, from: \"A\", to: \"B\" },\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 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 const lane = Math.max(1, ...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}\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}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\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 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 });\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 = (s: SchematicSignal, name: string): 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 });\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 ?? \"\")),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n return { doubleMain, extraTracks, turnouts, signals };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @willcgage/module-schematic — the shared module operations-schematic (track-graph)\n * that the Module Repository authors and Free-Dispatcher imports.\n *\n * Topological, straightened-first: positions are 1-D inches along the module\n * (from endplate A), lanes are integer track indices (0 = primary main). This is\n * the single source of truth for both apps — the doc types, the lenient parser\n * (docs arrive as jsonb / unknown), the pure feature resolver both renderers\n * draw, the N-scale helpers, and the editor <-> doc state machine an authoring\n * UI binds to. See docs/module-schematic-format.md in the free-dispatcher repo.\n *\n * Framework-agnostic and side-effect-free so it can be unit-tested and consumed\n * from Next.js (server + client) and Electron alike.\n */\n\nexport type TrackConfig = \"single\" | \"double\";\nexport type TrackRole = \"main\" | \"siding\" | \"spur\" | \"yard\" | \"crossover\";\nexport type TurnoutKind = \"left\" | \"right\" | \"wye\";\nexport type SignalFacing = \"AtoB\" | \"BtoA\";\nexport type SignalSide = \"above\" | \"below\";\n\nexport interface SchematicEndplateTrack {\n trackId: string;\n lane: number;\n config?: TrackConfig | null;\n}\nexport interface SchematicEndplate {\n id: string; // \"A\" (West) | \"B\" (East)\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 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\nexport const MAIN_TRACK_ID = \"main\";\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 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 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 endplates: [\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 { id: MAIN_TRACK_ID, role: \"main\", lane: 0, from: \"A\", to: \"B\" },\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 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 const lane = Math.max(1, ...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}\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 /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\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 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 });\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 return { doubleMain, extraTracks, turnouts, signals };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -194,6 +194,9 @@ interface DrawSignal {
194
194
  lane: number;
195
195
  facing: SignalFacing;
196
196
  side: SignalSide;
197
+ /** Owning control point's id, when the signal came from a CP group — lets a
198
+ * renderer join the drawn signal back to interlocking-level state (aspects). */
199
+ cp?: string;
197
200
  }
198
201
  interface ModuleFeatures {
199
202
  /** Whether either endplate declares a double-track main. */
package/dist/index.d.ts CHANGED
@@ -194,6 +194,9 @@ interface DrawSignal {
194
194
  lane: number;
195
195
  facing: SignalFacing;
196
196
  side: SignalSide;
197
+ /** Owning control point's id, when the signal came from a CP group — lets a
198
+ * renderer join the drawn signal back to interlocking-level state (aspects). */
199
+ cp?: string;
197
200
  }
198
201
  interface ModuleFeatures {
199
202
  /** Whether either endplate declares a double-track main. */
package/dist/index.js CHANGED
@@ -268,16 +268,17 @@ function moduleFeatures(doc) {
268
268
  onLane: trackLane.get(t.onTrack) ?? 0,
269
269
  divergeLane: trackLane.get(t.divergeTrack) ?? 1
270
270
  }));
271
- const drawSignal = (s, name) => ({
271
+ const drawSignal = (s, name, cp) => ({
272
272
  id: s.id,
273
273
  name,
274
274
  posFrac: clampFrac(s.pos),
275
275
  lane: s.track ? trackLane.get(s.track) ?? 0 : 0,
276
276
  facing: s.facing ?? "AtoB",
277
- side: s.side === "below" ? "below" : "above"
277
+ side: s.side === "below" ? "below" : "above",
278
+ ...cp ? { cp } : {}
278
279
  });
279
280
  const signals = Array.isArray(doc.controlPoints) ? doc.controlPoints.flatMap(
280
- (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? ""))
281
+ (c) => (c.signals ?? []).map((s) => drawSignal(s, c.name ?? "", c.id))
281
282
  ) : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? ""));
282
283
  return { doubleMain, extraTracks, turnouts, signals };
283
284
  }
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 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\nexport const MAIN_TRACK_ID = \"main\";\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 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 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 endplates: [\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 { id: MAIN_TRACK_ID, role: \"main\", lane: 0, from: \"A\", to: \"B\" },\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 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 const lane = Math.max(1, ...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}\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}\nexport interface ModuleFeatures {\n /** Whether either endplate declares a double-track main. */\n doubleMain: boolean;\n /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\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 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 });\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 = (s: SchematicSignal, name: string): 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 });\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 ?? \"\")),\n )\n : (doc.signals ?? []).map((s) => drawSignal(s, s.name ?? \"\"));\n\n return { doubleMain, extraTracks, turnouts, signals };\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}\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 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\nexport const MAIN_TRACK_ID = \"main\";\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 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 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 endplates: [\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 { id: MAIN_TRACK_ID, role: \"main\", lane: 0, from: \"A\", to: \"B\" },\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 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 const lane = Math.max(1, ...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}\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 /** Non-main tracks (sidings/spurs/yard/crossover). */\n extraTracks: DrawTrack[];\n turnouts: DrawTurnout[];\n signals: DrawSignal[];\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 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 });\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 return { doubleMain, extraTracks, turnouts, signals };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willcgage/module-schematic",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",