@willcgage/module-schematic 0.25.0 → 0.27.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.d.cts CHANGED
@@ -51,7 +51,17 @@ interface SchematicEndplate {
51
51
  }
52
52
  /** Free-moN endplate face width, inches — the connection interface size. */
53
53
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
54
+ /** NB: our own default, NOT from the standard — §1.1 states only the 12″ minimum
55
+ * ("Endplates shall be 6 inches high and a minimum 12 inches wide"). 24″ is
56
+ * simply a common real-world width. Don't present it as required. */
54
57
  declare const FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
58
+ /** Free-moN §2.0 **standard**: "Double track endplates must have a track spacing
59
+ * of 1.125 inches (1 1/8 inches). Track spacing shall be measured along the
60
+ * track center line." The one definition both apps read. */
61
+ declare const FREEMO_TRACK_SPACING_INCHES = 1.125;
62
+ /** Free-moN §2.0 **standard**: track crossing an endplate must be "not less than
63
+ * 4 inches from either fascia" (and perpendicular, straight and level for 4″). */
64
+ declare const FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
55
65
  /**
56
66
  * The authored face width for an endplate, or the recommended default when a
57
67
  * module hasn't authored one. The single source of truth both apps read so a
@@ -201,6 +211,10 @@ interface ModuleSchematicDoc {
201
211
  * tracks unrolled as a ladder past the throat — the default when inLoop
202
212
  * tracks exist), "geometric" (drawn balloon, AL&E-style). */
203
213
  loopRender?: "bulb" | "fan" | "geometric";
214
+ /** The two mains' positions are swapped: Main 1 draws above (lane 1), Main 2
215
+ * on the centre line (lane 0). Absent/false = the default (Main 1 below).
216
+ * Identities are unchanged — only which lane each is drawn in (#FMN-0043). */
217
+ mainsSwapped?: boolean;
204
218
  endplates: SchematicEndplate[];
205
219
  tracks: SchematicTrack[];
206
220
  turnouts?: SchematicTurnout[];
@@ -273,6 +287,13 @@ interface ModuleFootprintInput {
273
287
  geometryOffsetInches?: number | null;
274
288
  /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
275
289
  endplateWidths?: Record<string, number>;
290
+ /** Where each endplate's CENTRE sits relative to the main centre-line at that
291
+ * end, inches (signed, along the +normal). Free-moN puts a **double**-track
292
+ * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
293
+ * centre-line the plate centre is half a track spacing up — pass
294
+ * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
295
+ * (the default), and an off-centre track is a signed value. */
296
+ endplateTrackOffsets?: Record<string, number>;
276
297
  /** Authored benchwork outline (module-local inches), or absent for the band. */
277
298
  outline?: BenchworkPoint[] | null;
278
299
  /** Authored mainline centre-line (module-local inches, open path with arcs).
@@ -301,15 +322,49 @@ interface ModuleFootprint {
301
322
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
302
323
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
303
324
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
304
- declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
325
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
305
326
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
306
- declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
327
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
307
328
  /**
308
329
  * The full single-module physical footprint: centre-line + derived band +
309
330
  * endplate faces + the authored outline (arc-sampled), all in module-local
310
331
  * inches. Renderers draw `outline ?? band`.
311
332
  */
312
333
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
334
+ /**
335
+ * The offset from the main centre-line to an endplate's CENTRE, inches — the
336
+ * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
337
+ * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
338
+ * plate centre is half a track spacing up. A single track crosses at the centre.
339
+ */
340
+ declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined): number;
341
+ /** A Free-moN conformance problem with an endplate's width/track placement. */
342
+ interface EndplateWidthIssue {
343
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
344
+ code: "narrow" | "clearance";
345
+ /** Plain-language problem, for the author. */
346
+ message: string;
347
+ /** The width that would satisfy this rule, inches. */
348
+ requiredInches: number;
349
+ }
350
+ /**
351
+ * Check an endplate against the two Free-moN **standards** that bound its width:
352
+ *
353
+ * - §1.1 "Endplates shall be 6 inches high and a **minimum 12 inches wide**."
354
+ * - §2.0 "At the endplate, track shall cross near center on the width, **not less
355
+ * than 4 inches from either fascia**."
356
+ *
357
+ * With the tracks centred (the §2.0 recommendation) a double-track end needs
358
+ * 4 + 1.125 + 4 = 9.125″ for clearance alone, so the 12″ minimum governs — but an
359
+ * **off-centre** track can breach the 4″ rule on a plate that is otherwise wide
360
+ * enough, which is why both are checked. `trackOffsetInches` is the signed
361
+ * distance from the plate's centre to the main's crossing point (0 = centred).
362
+ */
363
+ declare function checkEndplateWidth(input: {
364
+ widthInches?: number | null;
365
+ config?: TrackConfig | "none" | null;
366
+ trackOffsetInches?: number | null;
367
+ }): EndplateWidthIssue[];
313
368
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
314
369
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
315
370
  declare const MAIN_TRACK_ID = "main";
@@ -429,6 +484,11 @@ interface EditorState {
429
484
  loopReturn: "same" | "main2";
430
485
  configA: TrackConfig;
431
486
  configB: EndplateBConfig;
487
+ /** Swap the two mains' POSITIONS: Main 1 draws above (lane 1) and Main 2 on
488
+ * the centre line (lane 0). The module decides which physical track is which
489
+ * main — on some modules the upper track is the through/primary main
490
+ * (#FMN-0043). Identities and references are unchanged; only the lanes swap. */
491
+ mainsSwapped: boolean;
432
492
  extraTracks: EditorTrack[];
433
493
  turnouts: EditorTurnout[];
434
494
  /** Grade crossings / diamonds (#170). */
@@ -736,4 +796,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
736
796
  heading: number;
737
797
  }>;
738
798
 
739
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
799
+ export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
package/dist/index.d.ts CHANGED
@@ -51,7 +51,17 @@ interface SchematicEndplate {
51
51
  }
52
52
  /** Free-moN endplate face width, inches — the connection interface size. */
53
53
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
54
+ /** NB: our own default, NOT from the standard — §1.1 states only the 12″ minimum
55
+ * ("Endplates shall be 6 inches high and a minimum 12 inches wide"). 24″ is
56
+ * simply a common real-world width. Don't present it as required. */
54
57
  declare const FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
58
+ /** Free-moN §2.0 **standard**: "Double track endplates must have a track spacing
59
+ * of 1.125 inches (1 1/8 inches). Track spacing shall be measured along the
60
+ * track center line." The one definition both apps read. */
61
+ declare const FREEMO_TRACK_SPACING_INCHES = 1.125;
62
+ /** Free-moN §2.0 **standard**: track crossing an endplate must be "not less than
63
+ * 4 inches from either fascia" (and perpendicular, straight and level for 4″). */
64
+ declare const FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
55
65
  /**
56
66
  * The authored face width for an endplate, or the recommended default when a
57
67
  * module hasn't authored one. The single source of truth both apps read so a
@@ -201,6 +211,10 @@ interface ModuleSchematicDoc {
201
211
  * tracks unrolled as a ladder past the throat — the default when inLoop
202
212
  * tracks exist), "geometric" (drawn balloon, AL&E-style). */
203
213
  loopRender?: "bulb" | "fan" | "geometric";
214
+ /** The two mains' positions are swapped: Main 1 draws above (lane 1), Main 2
215
+ * on the centre line (lane 0). Absent/false = the default (Main 1 below).
216
+ * Identities are unchanged — only which lane each is drawn in (#FMN-0043). */
217
+ mainsSwapped?: boolean;
204
218
  endplates: SchematicEndplate[];
205
219
  tracks: SchematicTrack[];
206
220
  turnouts?: SchematicTurnout[];
@@ -273,6 +287,13 @@ interface ModuleFootprintInput {
273
287
  geometryOffsetInches?: number | null;
274
288
  /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
275
289
  endplateWidths?: Record<string, number>;
290
+ /** Where each endplate's CENTRE sits relative to the main centre-line at that
291
+ * end, inches (signed, along the +normal). Free-moN puts a **double**-track
292
+ * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
293
+ * centre-line the plate centre is half a track spacing up — pass
294
+ * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
295
+ * (the default), and an off-centre track is a signed value. */
296
+ endplateTrackOffsets?: Record<string, number>;
276
297
  /** Authored benchwork outline (module-local inches), or absent for the band. */
277
298
  outline?: BenchworkPoint[] | null;
278
299
  /** Authored mainline centre-line (module-local inches, open path with arcs).
@@ -301,15 +322,49 @@ interface ModuleFootprint {
301
322
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
302
323
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
303
324
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
304
- declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
325
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
305
326
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
306
- declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
327
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
307
328
  /**
308
329
  * The full single-module physical footprint: centre-line + derived band +
309
330
  * endplate faces + the authored outline (arc-sampled), all in module-local
310
331
  * inches. Renderers draw `outline ?? band`.
311
332
  */
312
333
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
334
+ /**
335
+ * The offset from the main centre-line to an endplate's CENTRE, inches — the
336
+ * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
337
+ * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
338
+ * plate centre is half a track spacing up. A single track crosses at the centre.
339
+ */
340
+ declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined): number;
341
+ /** A Free-moN conformance problem with an endplate's width/track placement. */
342
+ interface EndplateWidthIssue {
343
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
344
+ code: "narrow" | "clearance";
345
+ /** Plain-language problem, for the author. */
346
+ message: string;
347
+ /** The width that would satisfy this rule, inches. */
348
+ requiredInches: number;
349
+ }
350
+ /**
351
+ * Check an endplate against the two Free-moN **standards** that bound its width:
352
+ *
353
+ * - §1.1 "Endplates shall be 6 inches high and a **minimum 12 inches wide**."
354
+ * - §2.0 "At the endplate, track shall cross near center on the width, **not less
355
+ * than 4 inches from either fascia**."
356
+ *
357
+ * With the tracks centred (the §2.0 recommendation) a double-track end needs
358
+ * 4 + 1.125 + 4 = 9.125″ for clearance alone, so the 12″ minimum governs — but an
359
+ * **off-centre** track can breach the 4″ rule on a plate that is otherwise wide
360
+ * enough, which is why both are checked. `trackOffsetInches` is the signed
361
+ * distance from the plate's centre to the main's crossing point (0 = centred).
362
+ */
363
+ declare function checkEndplateWidth(input: {
364
+ widthInches?: number | null;
365
+ config?: TrackConfig | "none" | null;
366
+ trackOffsetInches?: number | null;
367
+ }): EndplateWidthIssue[];
313
368
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
314
369
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
315
370
  declare const MAIN_TRACK_ID = "main";
@@ -429,6 +484,11 @@ interface EditorState {
429
484
  loopReturn: "same" | "main2";
430
485
  configA: TrackConfig;
431
486
  configB: EndplateBConfig;
487
+ /** Swap the two mains' POSITIONS: Main 1 draws above (lane 1) and Main 2 on
488
+ * the centre line (lane 0). The module decides which physical track is which
489
+ * main — on some modules the upper track is the through/primary main
490
+ * (#FMN-0043). Identities and references are unchanged; only the lanes swap. */
491
+ mainsSwapped: boolean;
432
492
  extraTracks: EditorTrack[];
433
493
  turnouts: EditorTurnout[];
434
494
  /** Grade crossings / diamonds (#170). */
@@ -736,4 +796,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
736
796
  heading: number;
737
797
  }>;
738
798
 
739
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
799
+ export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // src/index.ts
2
2
  var FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
3
3
  var FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
4
+ var FREEMO_TRACK_SPACING_INCHES = 1.125;
5
+ var FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
4
6
  function endplateWidthInches(ep) {
5
7
  const w = ep?.widthInches;
6
8
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
@@ -142,37 +144,74 @@ function centerlineFractions(center) {
142
144
  const total = cum[cum.length - 1] || 1;
143
145
  return cum.map((d) => d / total);
144
146
  }
145
- function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
147
+ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
146
148
  if (center.length < 2) return [];
147
149
  const n = centerlineNormals(center);
148
150
  const f = centerlineFractions(center);
149
151
  const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
150
- const left = center.map((p, i) => ({ x: p.x + n[i].x * half(i), y: p.y + n[i].y * half(i) }));
151
- const right = center.map((p, i) => ({ x: p.x - n[i].x * half(i), y: p.y - n[i].y * half(i) }));
152
+ const off = (i) => offsetA * (1 - f[i]) + offsetB * f[i];
153
+ const left = center.map((p, i) => ({
154
+ x: p.x + n[i].x * (off(i) + half(i)),
155
+ y: p.y + n[i].y * (off(i) + half(i))
156
+ }));
157
+ const right = center.map((p, i) => ({
158
+ x: p.x + n[i].x * (off(i) - half(i)),
159
+ y: p.y + n[i].y * (off(i) - half(i))
160
+ }));
152
161
  return [...left, ...right.reverse()];
153
162
  }
154
- function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
163
+ function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
155
164
  if (center.length < 2) return [];
156
165
  const n = centerlineNormals(center);
157
- const face = (i, w) => ({
158
- p1: { x: center[i].x + n[i].x * (w / 2), y: center[i].y + n[i].y * (w / 2) },
159
- p2: { x: center[i].x - n[i].x * (w / 2), y: center[i].y - n[i].y * (w / 2) },
166
+ const face = (i, w, o) => ({
167
+ p1: { x: center[i].x + n[i].x * (o + w / 2), y: center[i].y + n[i].y * (o + w / 2) },
168
+ p2: { x: center[i].x + n[i].x * (o - w / 2), y: center[i].y + n[i].y * (o - w / 2) },
160
169
  mid: { x: center[i].x, y: center[i].y }
161
170
  });
162
- return [face(0, widthA), face(center.length - 1, widthB)];
171
+ return [face(0, widthA, offsetA), face(center.length - 1, widthB, offsetB)];
163
172
  }
164
173
  function moduleFootprint(input) {
165
174
  const centerline = moduleCenterline(input);
166
175
  const widthA = endplateWidthFor(input.endplateWidths, "A");
167
176
  const widthB = endplateWidthFor(input.endplateWidths, "B");
168
177
  const authored = benchworkOutline(input);
178
+ const offA = input.endplateTrackOffsets?.["A"] ?? 0;
179
+ const offB = input.endplateTrackOffsets?.["B"] ?? 0;
169
180
  return {
170
181
  centerline,
171
- band: benchworkBand(centerline, widthA, widthB),
172
- endplateFaces: endplateFaceSegments(centerline, widthA, widthB),
182
+ band: benchworkBand(centerline, widthA, widthB, offA, offB),
183
+ endplateFaces: endplateFaceSegments(centerline, widthA, widthB, offA, offB),
173
184
  outline: authored ? sampleBenchworkOutline(authored) : null
174
185
  };
175
186
  }
187
+ function endplateTrackOffsetFor(config) {
188
+ return config === "double" ? FREEMO_TRACK_SPACING_INCHES / 2 : 0;
189
+ }
190
+ function checkEndplateWidth(input) {
191
+ const width = endplateWidthInches(input);
192
+ const issues = [];
193
+ if (width < FREEMO_ENDPLATE_WIDTH_MIN_INCHES) {
194
+ issues.push({
195
+ code: "narrow",
196
+ message: `Endplate is ${round2(width)}\u2033 wide \u2014 the standard requires at least ${FREEMO_ENDPLATE_WIDTH_MIN_INCHES}\u2033.`,
197
+ requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
198
+ });
199
+ }
200
+ const off = input.trackOffsetInches ?? 0;
201
+ const centres = input.config === "double" ? [off - FREEMO_TRACK_SPACING_INCHES / 2, off + FREEMO_TRACK_SPACING_INCHES / 2] : [off];
202
+ const worst = Math.max(...centres.map((c) => Math.abs(c)));
203
+ const clearance = width / 2 - worst;
204
+ if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
205
+ const required = 2 * (worst + FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES);
206
+ issues.push({
207
+ code: "clearance",
208
+ message: `Track sits ${round2(clearance)}\u2033 from the fascia \u2014 the standard requires at least ${FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES}\u2033. Widen this end to ${round2(required)}\u2033` + (off !== 0 ? " or move the track back toward the centre." : "."),
209
+ requiredInches: required
210
+ });
211
+ }
212
+ return issues;
213
+ }
214
+ var round2 = (n) => Math.round(n * 100) / 100;
176
215
  function endplateWidthFor(widths, id) {
177
216
  const w = widths?.[id];
178
217
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
@@ -208,6 +247,7 @@ function emptyEditorState(lengthInches) {
208
247
  loopReturn: "same",
209
248
  configA: "single",
210
249
  configB: "single",
250
+ mainsSwapped: false,
211
251
  extraTracks: [],
212
252
  turnouts: [],
213
253
  crossings: [],
@@ -228,29 +268,31 @@ function main1Track(state) {
228
268
  const bothDouble = state.configA === "double" && state.configB === "double";
229
269
  const isDouble = state.configA === "double" || state.configB === "double";
230
270
  const sw = state.turnouts.find(isTransitionTurnout);
271
+ const lane = state.mainsSwapped && isDouble ? 1 : 0;
231
272
  if (!isDouble || bothDouble || !sw || sw.onTrack !== MAIN2_TRACK_ID) {
232
- return { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" };
273
+ return { id: MAIN_TRACK_ID, role: "main", lane, from: "A", to: "B" };
233
274
  }
234
275
  return state.configA === "double" ? (
235
276
  // Double at A: Main 1 runs from A and ends at the turnout.
236
- { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: sw.pos }
277
+ { id: MAIN_TRACK_ID, role: "main", lane, fromPos: 0, toPos: sw.pos }
237
278
  ) : (
238
279
  // Double at B: Main 1 begins at the turnout and runs to B.
239
- { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: sw.pos, toPos: state.lengthInches }
280
+ { id: MAIN_TRACK_ID, role: "main", lane, fromPos: sw.pos, toPos: state.lengthInches }
240
281
  );
241
282
  }
242
283
  function main2Track(state) {
243
284
  const bothDouble = state.configA === "double" && state.configB === "double";
244
285
  const sw = state.turnouts.find(isTransitionTurnout);
286
+ const lane = state.mainsSwapped ? 0 : 1;
245
287
  if (bothDouble || !sw || sw.divergeTrack !== MAIN2_TRACK_ID) {
246
- return { id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" };
288
+ return { id: MAIN2_TRACK_ID, role: "main", lane, from: "A", to: "B" };
247
289
  }
248
290
  return state.configA === "double" ? (
249
291
  // Double at A: Main 2 runs from A and ends at the turnout.
250
- { id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: sw.pos }
292
+ { id: MAIN2_TRACK_ID, role: "main", lane, fromPos: 0, toPos: sw.pos }
251
293
  ) : (
252
294
  // Double at B: Main 2 begins at the turnout and runs to B.
253
- { id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: sw.pos, toPos: state.lengthInches }
295
+ { id: MAIN2_TRACK_ID, role: "main", lane, fromPos: sw.pos, toPos: state.lengthInches }
254
296
  );
255
297
  }
256
298
  function buildTransition(state) {
@@ -307,6 +349,7 @@ function stateToDoc(state, recordNumber) {
307
349
  lengthInches: state.lengthInches,
308
350
  ...state.loop ? { loop: true } : {},
309
351
  ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
352
+ ...state.mainsSwapped ? { mainsSwapped: true } : {},
310
353
  endplates: withWidths(
311
354
  withPoses(
312
355
  [
@@ -518,6 +561,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
518
561
  lengthInches: len,
519
562
  loop,
520
563
  loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
564
+ mainsSwapped: d.mainsSwapped === true,
521
565
  configA: configOf("A"),
522
566
  // On a loop, a missing B means pure turnback; present = interchange loop.
523
567
  configB: loop && !hasB ? "none" : configOf("B"),
@@ -1032,6 +1076,6 @@ function poseOverridesFromDoc(doc) {
1032
1076
  return out;
1033
1077
  }
1034
1078
 
1035
- export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
1079
+ export { FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
1036
1080
  //# sourceMappingURL=index.js.map
1037
1081
  //# sourceMappingURL=index.js.map