@willcgage/module-schematic 0.26.0 → 0.28.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
@@ -48,10 +48,29 @@ interface SchematicEndplate {
48
48
  * recommended. Absent = the recommended default (modules may differ end to
49
49
  * end, e.g. a transition). */
50
50
  widthInches?: number | null;
51
+ /** Where this end's PRIMARY track (Main 1) crosses, as a signed distance from
52
+ * the plate's CENTRE in inches — the standard's own framing ("each track
53
+ * 0.5625 inches from the center of the endplate"). §2.0 requires only that
54
+ * every track stay ≥4″ from either fascia; centring is a recommendation the
55
+ * 20220628 revision relaxed, so an offset is legal — a transition SECTION
56
+ * commonly offsets its single-track end so the through main lines up with one
57
+ * of the two tracks at its double end. Absent = the recommended default
58
+ * (single centred; double straddling at ∓ half the track spacing). */
59
+ trackOffsetInches?: number | null;
51
60
  }
52
61
  /** Free-moN endplate face width, inches — the connection interface size. */
53
62
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
63
+ /** NB: our own default, NOT from the standard — §1.1 states only the 12″ minimum
64
+ * ("Endplates shall be 6 inches high and a minimum 12 inches wide"). 24″ is
65
+ * simply a common real-world width. Don't present it as required. */
54
66
  declare const FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
67
+ /** Free-moN §2.0 **standard**: "Double track endplates must have a track spacing
68
+ * of 1.125 inches (1 1/8 inches). Track spacing shall be measured along the
69
+ * track center line." The one definition both apps read. */
70
+ declare const FREEMO_TRACK_SPACING_INCHES = 1.125;
71
+ /** Free-moN §2.0 **standard**: track crossing an endplate must be "not less than
72
+ * 4 inches from either fascia" (and perpendicular, straight and level for 4″). */
73
+ declare const FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
55
74
  /**
56
75
  * The authored face width for an endplate, or the recommended default when a
57
76
  * module hasn't authored one. The single source of truth both apps read so a
@@ -277,6 +296,13 @@ interface ModuleFootprintInput {
277
296
  geometryOffsetInches?: number | null;
278
297
  /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
279
298
  endplateWidths?: Record<string, number>;
299
+ /** Where each endplate's CENTRE sits relative to the main centre-line at that
300
+ * end, inches (signed, along the +normal). Free-moN puts a **double**-track
301
+ * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
302
+ * centre-line the plate centre is half a track spacing up — pass
303
+ * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
304
+ * (the default), and an off-centre track is a signed value. */
305
+ endplateTrackOffsets?: Record<string, number>;
280
306
  /** Authored benchwork outline (module-local inches), or absent for the band. */
281
307
  outline?: BenchworkPoint[] | null;
282
308
  /** Authored mainline centre-line (module-local inches, open path with arcs).
@@ -305,15 +331,56 @@ interface ModuleFootprint {
305
331
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
306
332
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
307
333
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
308
- declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
334
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
309
335
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
310
- declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
336
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
311
337
  /**
312
338
  * The full single-module physical footprint: centre-line + derived band +
313
339
  * endplate faces + the authored outline (arc-sampled), all in module-local
314
340
  * inches. Renderers draw `outline ?? band`.
315
341
  */
316
342
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
343
+ /**
344
+ * The offset from the main centre-line to an endplate's CENTRE, inches — the
345
+ * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
346
+ * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
347
+ * plate centre is half a track spacing up. A single track crosses at the centre.
348
+ */
349
+ declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
350
+ /**
351
+ * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
352
+ * the plate's CENTRE — the standard's own framing. Authored value wins; absent
353
+ * falls back to the §2.0 recommendations: a single track centred (0), a double
354
+ * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
355
+ */
356
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
357
+ /** A Free-moN conformance problem with an endplate's width/track placement. */
358
+ interface EndplateWidthIssue {
359
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
360
+ code: "narrow" | "clearance";
361
+ /** Plain-language problem, for the author. */
362
+ message: string;
363
+ /** The width that would satisfy this rule, inches. */
364
+ requiredInches: number;
365
+ }
366
+ /**
367
+ * Check an endplate against the two Free-moN **standards** that bound its width:
368
+ *
369
+ * - §1.1 "Endplates shall be 6 inches high and a **minimum 12 inches wide**."
370
+ * - §2.0 "At the endplate, track shall cross near center on the width, **not less
371
+ * than 4 inches from either fascia**."
372
+ *
373
+ * With the tracks centred (the §2.0 recommendation) a double-track end needs
374
+ * 4 + 1.125 + 4 = 9.125″ for clearance alone, so the 12″ minimum governs — but an
375
+ * **off-centre** track can breach the 4″ rule on a plate that is otherwise wide
376
+ * enough, which is why both are checked. `trackOffsetInches` is the signed
377
+ * distance from the plate's centre to the main's crossing point (0 = centred).
378
+ */
379
+ declare function checkEndplateWidth(input: {
380
+ widthInches?: number | null;
381
+ config?: TrackConfig | "none" | null;
382
+ trackOffsetInches?: number | null;
383
+ }): EndplateWidthIssue[];
317
384
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
318
385
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
319
386
  declare const MAIN_TRACK_ID = "main";
@@ -454,6 +521,9 @@ interface EditorState {
454
521
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
455
522
  * 24″ recommended). Absent id = the recommended default. */
456
523
  endplateWidths: Record<string, number>;
524
+ /** Authored per-endplate TRACK offsets by id — the primary track's signed
525
+ * distance from the plate centre, inches. Absent id = the §2.0 default. */
526
+ endplateTrackOffsets: Record<string, number>;
457
527
  /** Benchwork footprint outline — polygon vertices in module-local inches
458
528
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
459
529
  * Empty = no authored outline (fall back to the endplate-width band). */
@@ -745,4 +815,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
745
815
  heading: number;
746
816
  }>;
747
817
 
748
- 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 };
818
+ 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, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
package/dist/index.d.ts CHANGED
@@ -48,10 +48,29 @@ interface SchematicEndplate {
48
48
  * recommended. Absent = the recommended default (modules may differ end to
49
49
  * end, e.g. a transition). */
50
50
  widthInches?: number | null;
51
+ /** Where this end's PRIMARY track (Main 1) crosses, as a signed distance from
52
+ * the plate's CENTRE in inches — the standard's own framing ("each track
53
+ * 0.5625 inches from the center of the endplate"). §2.0 requires only that
54
+ * every track stay ≥4″ from either fascia; centring is a recommendation the
55
+ * 20220628 revision relaxed, so an offset is legal — a transition SECTION
56
+ * commonly offsets its single-track end so the through main lines up with one
57
+ * of the two tracks at its double end. Absent = the recommended default
58
+ * (single centred; double straddling at ∓ half the track spacing). */
59
+ trackOffsetInches?: number | null;
51
60
  }
52
61
  /** Free-moN endplate face width, inches — the connection interface size. */
53
62
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
63
+ /** NB: our own default, NOT from the standard — §1.1 states only the 12″ minimum
64
+ * ("Endplates shall be 6 inches high and a minimum 12 inches wide"). 24″ is
65
+ * simply a common real-world width. Don't present it as required. */
54
66
  declare const FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
67
+ /** Free-moN §2.0 **standard**: "Double track endplates must have a track spacing
68
+ * of 1.125 inches (1 1/8 inches). Track spacing shall be measured along the
69
+ * track center line." The one definition both apps read. */
70
+ declare const FREEMO_TRACK_SPACING_INCHES = 1.125;
71
+ /** Free-moN §2.0 **standard**: track crossing an endplate must be "not less than
72
+ * 4 inches from either fascia" (and perpendicular, straight and level for 4″). */
73
+ declare const FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
55
74
  /**
56
75
  * The authored face width for an endplate, or the recommended default when a
57
76
  * module hasn't authored one. The single source of truth both apps read so a
@@ -277,6 +296,13 @@ interface ModuleFootprintInput {
277
296
  geometryOffsetInches?: number | null;
278
297
  /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
279
298
  endplateWidths?: Record<string, number>;
299
+ /** Where each endplate's CENTRE sits relative to the main centre-line at that
300
+ * end, inches (signed, along the +normal). Free-moN puts a **double**-track
301
+ * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
302
+ * centre-line the plate centre is half a track spacing up — pass
303
+ * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
304
+ * (the default), and an off-centre track is a signed value. */
305
+ endplateTrackOffsets?: Record<string, number>;
280
306
  /** Authored benchwork outline (module-local inches), or absent for the band. */
281
307
  outline?: BenchworkPoint[] | null;
282
308
  /** Authored mainline centre-line (module-local inches, open path with arcs).
@@ -305,15 +331,56 @@ interface ModuleFootprint {
305
331
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
306
332
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
307
333
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
308
- declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
334
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
309
335
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
310
- declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
336
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
311
337
  /**
312
338
  * The full single-module physical footprint: centre-line + derived band +
313
339
  * endplate faces + the authored outline (arc-sampled), all in module-local
314
340
  * inches. Renderers draw `outline ?? band`.
315
341
  */
316
342
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
343
+ /**
344
+ * The offset from the main centre-line to an endplate's CENTRE, inches — the
345
+ * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
346
+ * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
347
+ * plate centre is half a track spacing up. A single track crosses at the centre.
348
+ */
349
+ declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
350
+ /**
351
+ * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
352
+ * the plate's CENTRE — the standard's own framing. Authored value wins; absent
353
+ * falls back to the §2.0 recommendations: a single track centred (0), a double
354
+ * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
355
+ */
356
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
357
+ /** A Free-moN conformance problem with an endplate's width/track placement. */
358
+ interface EndplateWidthIssue {
359
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
360
+ code: "narrow" | "clearance";
361
+ /** Plain-language problem, for the author. */
362
+ message: string;
363
+ /** The width that would satisfy this rule, inches. */
364
+ requiredInches: number;
365
+ }
366
+ /**
367
+ * Check an endplate against the two Free-moN **standards** that bound its width:
368
+ *
369
+ * - §1.1 "Endplates shall be 6 inches high and a **minimum 12 inches wide**."
370
+ * - §2.0 "At the endplate, track shall cross near center on the width, **not less
371
+ * than 4 inches from either fascia**."
372
+ *
373
+ * With the tracks centred (the §2.0 recommendation) a double-track end needs
374
+ * 4 + 1.125 + 4 = 9.125″ for clearance alone, so the 12″ minimum governs — but an
375
+ * **off-centre** track can breach the 4″ rule on a plate that is otherwise wide
376
+ * enough, which is why both are checked. `trackOffsetInches` is the signed
377
+ * distance from the plate's centre to the main's crossing point (0 = centred).
378
+ */
379
+ declare function checkEndplateWidth(input: {
380
+ widthInches?: number | null;
381
+ config?: TrackConfig | "none" | null;
382
+ trackOffsetInches?: number | null;
383
+ }): EndplateWidthIssue[];
317
384
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
318
385
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
319
386
  declare const MAIN_TRACK_ID = "main";
@@ -454,6 +521,9 @@ interface EditorState {
454
521
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
455
522
  * 24″ recommended). Absent id = the recommended default. */
456
523
  endplateWidths: Record<string, number>;
524
+ /** Authored per-endplate TRACK offsets by id — the primary track's signed
525
+ * distance from the plate centre, inches. Absent id = the §2.0 default. */
526
+ endplateTrackOffsets: Record<string, number>;
457
527
  /** Benchwork footprint outline — polygon vertices in module-local inches
458
528
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
459
529
  * Empty = no authored outline (fall back to the endplate-width band). */
@@ -745,4 +815,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
745
815
  heading: number;
746
816
  }>;
747
817
 
748
- 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 };
818
+ 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, endplateTrackOffsetInches, 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,79 @@ 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, authoredTrackOffset) {
188
+ const v = -endplateTrackOffsetInches(authoredTrackOffset, config);
189
+ return v === 0 ? 0 : v;
190
+ }
191
+ function endplateTrackOffsetInches(authored, config) {
192
+ if (typeof authored === "number" && Number.isFinite(authored)) return authored;
193
+ return config === "double" ? -FREEMO_TRACK_SPACING_INCHES / 2 : 0;
194
+ }
195
+ function checkEndplateWidth(input) {
196
+ const width = endplateWidthInches(input);
197
+ const issues = [];
198
+ if (width < FREEMO_ENDPLATE_WIDTH_MIN_INCHES) {
199
+ issues.push({
200
+ code: "narrow",
201
+ message: `Endplate is ${round2(width)}\u2033 wide \u2014 the standard requires at least ${FREEMO_ENDPLATE_WIDTH_MIN_INCHES}\u2033.`,
202
+ requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
203
+ });
204
+ }
205
+ const off = input.trackOffsetInches ?? 0;
206
+ const centres = input.config === "double" ? [off - FREEMO_TRACK_SPACING_INCHES / 2, off + FREEMO_TRACK_SPACING_INCHES / 2] : [off];
207
+ const worst = Math.max(...centres.map((c) => Math.abs(c)));
208
+ const clearance = width / 2 - worst;
209
+ if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
210
+ const required = 2 * (worst + FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES);
211
+ issues.push({
212
+ code: "clearance",
213
+ 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." : "."),
214
+ requiredInches: required
215
+ });
216
+ }
217
+ return issues;
218
+ }
219
+ var round2 = (n) => Math.round(n * 100) / 100;
176
220
  function endplateWidthFor(widths, id) {
177
221
  const w = widths?.[id];
178
222
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
@@ -215,6 +259,7 @@ function emptyEditorState(lengthInches) {
215
259
  branches: [],
216
260
  poseOverrides: {},
217
261
  endplateWidths: {},
262
+ endplateTrackOffsets: {},
218
263
  outline: [],
219
264
  sectionBreaks: [],
220
265
  controlPoints: [],
@@ -297,10 +342,14 @@ function withPoses(endplates, overrides) {
297
342
  (e) => overrides[e.id] ? { ...e, pose: overrides[e.id] } : e
298
343
  );
299
344
  }
300
- function withWidths(endplates, widths) {
345
+ function withWidths(endplates, widths, offsets = {}) {
301
346
  return endplates.map((e) => {
302
347
  const w = widths[e.id];
303
- return typeof w === "number" && w > 0 ? { ...e, widthInches: w } : e;
348
+ const o = offsets[e.id];
349
+ let out = e;
350
+ if (typeof w === "number" && w > 0) out = { ...out, widthInches: w };
351
+ if (typeof o === "number" && Number.isFinite(o)) out = { ...out, trackOffsetInches: o };
352
+ return out;
304
353
  });
305
354
  }
306
355
  function stateToDoc(state, recordNumber) {
@@ -349,7 +398,8 @@ function stateToDoc(state, recordNumber) {
349
398
  ],
350
399
  state.poseOverrides
351
400
  ),
352
- state.endplateWidths
401
+ state.endplateWidths,
402
+ state.endplateTrackOffsets
353
403
  ),
354
404
  tracks: [
355
405
  state.loop ? (
@@ -508,9 +558,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
508
558
  );
509
559
  const poseOverrides = poseOverridesFromDoc(d);
510
560
  const endplateWidths = {};
561
+ const endplateTrackOffsets = {};
511
562
  for (const e of d.endplates ?? []) {
512
563
  if (typeof e.widthInches === "number" && e.widthInches > 0)
513
564
  endplateWidths[e.id] = e.widthInches;
565
+ if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
566
+ endplateTrackOffsets[e.id] = e.trackOffsetInches;
514
567
  }
515
568
  const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
516
569
  x: p.x,
@@ -534,6 +587,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
534
587
  })),
535
588
  poseOverrides,
536
589
  endplateWidths,
590
+ endplateTrackOffsets,
537
591
  outline,
538
592
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
539
593
  mainPath,
@@ -1037,6 +1091,6 @@ function poseOverridesFromDoc(doc) {
1037
1091
  return out;
1038
1092
  }
1039
1093
 
1040
- 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 };
1094
+ 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, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, stateToDoc, trackPath };
1041
1095
  //# sourceMappingURL=index.js.map
1042
1096
  //# sourceMappingURL=index.js.map