@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.cjs CHANGED
@@ -3,6 +3,8 @@
3
3
  // src/index.ts
4
4
  var FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
5
5
  var FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = 24;
6
+ var FREEMO_TRACK_SPACING_INCHES = 1.125;
7
+ var FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = 4;
6
8
  function endplateWidthInches(ep) {
7
9
  const w = ep?.widthInches;
8
10
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
@@ -144,37 +146,74 @@ function centerlineFractions(center) {
144
146
  const total = cum[cum.length - 1] || 1;
145
147
  return cum.map((d) => d / total);
146
148
  }
147
- function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
149
+ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
148
150
  if (center.length < 2) return [];
149
151
  const n = centerlineNormals(center);
150
152
  const f = centerlineFractions(center);
151
153
  const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
152
- const left = center.map((p, i) => ({ x: p.x + n[i].x * half(i), y: p.y + n[i].y * half(i) }));
153
- const right = center.map((p, i) => ({ x: p.x - n[i].x * half(i), y: p.y - n[i].y * half(i) }));
154
+ const off = (i) => offsetA * (1 - f[i]) + offsetB * f[i];
155
+ const left = center.map((p, i) => ({
156
+ x: p.x + n[i].x * (off(i) + half(i)),
157
+ y: p.y + n[i].y * (off(i) + half(i))
158
+ }));
159
+ const right = center.map((p, i) => ({
160
+ x: p.x + n[i].x * (off(i) - half(i)),
161
+ y: p.y + n[i].y * (off(i) - half(i))
162
+ }));
154
163
  return [...left, ...right.reverse()];
155
164
  }
156
- function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
165
+ function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
157
166
  if (center.length < 2) return [];
158
167
  const n = centerlineNormals(center);
159
- const face = (i, w) => ({
160
- p1: { x: center[i].x + n[i].x * (w / 2), y: center[i].y + n[i].y * (w / 2) },
161
- p2: { x: center[i].x - n[i].x * (w / 2), y: center[i].y - n[i].y * (w / 2) },
168
+ const face = (i, w, o) => ({
169
+ p1: { x: center[i].x + n[i].x * (o + w / 2), y: center[i].y + n[i].y * (o + w / 2) },
170
+ p2: { x: center[i].x + n[i].x * (o - w / 2), y: center[i].y + n[i].y * (o - w / 2) },
162
171
  mid: { x: center[i].x, y: center[i].y }
163
172
  });
164
- return [face(0, widthA), face(center.length - 1, widthB)];
173
+ return [face(0, widthA, offsetA), face(center.length - 1, widthB, offsetB)];
165
174
  }
166
175
  function moduleFootprint(input) {
167
176
  const centerline = moduleCenterline(input);
168
177
  const widthA = endplateWidthFor(input.endplateWidths, "A");
169
178
  const widthB = endplateWidthFor(input.endplateWidths, "B");
170
179
  const authored = benchworkOutline(input);
180
+ const offA = input.endplateTrackOffsets?.["A"] ?? 0;
181
+ const offB = input.endplateTrackOffsets?.["B"] ?? 0;
171
182
  return {
172
183
  centerline,
173
- band: benchworkBand(centerline, widthA, widthB),
174
- endplateFaces: endplateFaceSegments(centerline, widthA, widthB),
184
+ band: benchworkBand(centerline, widthA, widthB, offA, offB),
185
+ endplateFaces: endplateFaceSegments(centerline, widthA, widthB, offA, offB),
175
186
  outline: authored ? sampleBenchworkOutline(authored) : null
176
187
  };
177
188
  }
189
+ function endplateTrackOffsetFor(config) {
190
+ return config === "double" ? FREEMO_TRACK_SPACING_INCHES / 2 : 0;
191
+ }
192
+ function checkEndplateWidth(input) {
193
+ const width = endplateWidthInches(input);
194
+ const issues = [];
195
+ if (width < FREEMO_ENDPLATE_WIDTH_MIN_INCHES) {
196
+ issues.push({
197
+ code: "narrow",
198
+ message: `Endplate is ${round2(width)}\u2033 wide \u2014 the standard requires at least ${FREEMO_ENDPLATE_WIDTH_MIN_INCHES}\u2033.`,
199
+ requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
200
+ });
201
+ }
202
+ const off = input.trackOffsetInches ?? 0;
203
+ const centres = input.config === "double" ? [off - FREEMO_TRACK_SPACING_INCHES / 2, off + FREEMO_TRACK_SPACING_INCHES / 2] : [off];
204
+ const worst = Math.max(...centres.map((c) => Math.abs(c)));
205
+ const clearance = width / 2 - worst;
206
+ if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
207
+ const required = 2 * (worst + FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES);
208
+ issues.push({
209
+ code: "clearance",
210
+ 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." : "."),
211
+ requiredInches: required
212
+ });
213
+ }
214
+ return issues;
215
+ }
216
+ var round2 = (n) => Math.round(n * 100) / 100;
178
217
  function endplateWidthFor(widths, id) {
179
218
  const w = widths?.[id];
180
219
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
@@ -210,6 +249,7 @@ function emptyEditorState(lengthInches) {
210
249
  loopReturn: "same",
211
250
  configA: "single",
212
251
  configB: "single",
252
+ mainsSwapped: false,
213
253
  extraTracks: [],
214
254
  turnouts: [],
215
255
  crossings: [],
@@ -230,29 +270,31 @@ function main1Track(state) {
230
270
  const bothDouble = state.configA === "double" && state.configB === "double";
231
271
  const isDouble = state.configA === "double" || state.configB === "double";
232
272
  const sw = state.turnouts.find(isTransitionTurnout);
273
+ const lane = state.mainsSwapped && isDouble ? 1 : 0;
233
274
  if (!isDouble || bothDouble || !sw || sw.onTrack !== MAIN2_TRACK_ID) {
234
- return { id: MAIN_TRACK_ID, role: "main", lane: 0, from: "A", to: "B" };
275
+ return { id: MAIN_TRACK_ID, role: "main", lane, from: "A", to: "B" };
235
276
  }
236
277
  return state.configA === "double" ? (
237
278
  // Double at A: Main 1 runs from A and ends at the turnout.
238
- { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: sw.pos }
279
+ { id: MAIN_TRACK_ID, role: "main", lane, fromPos: 0, toPos: sw.pos }
239
280
  ) : (
240
281
  // Double at B: Main 1 begins at the turnout and runs to B.
241
- { id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: sw.pos, toPos: state.lengthInches }
282
+ { id: MAIN_TRACK_ID, role: "main", lane, fromPos: sw.pos, toPos: state.lengthInches }
242
283
  );
243
284
  }
244
285
  function main2Track(state) {
245
286
  const bothDouble = state.configA === "double" && state.configB === "double";
246
287
  const sw = state.turnouts.find(isTransitionTurnout);
288
+ const lane = state.mainsSwapped ? 0 : 1;
247
289
  if (bothDouble || !sw || sw.divergeTrack !== MAIN2_TRACK_ID) {
248
- return { id: MAIN2_TRACK_ID, role: "main", lane: 1, from: "A", to: "B" };
290
+ return { id: MAIN2_TRACK_ID, role: "main", lane, from: "A", to: "B" };
249
291
  }
250
292
  return state.configA === "double" ? (
251
293
  // Double at A: Main 2 runs from A and ends at the turnout.
252
- { id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: 0, toPos: sw.pos }
294
+ { id: MAIN2_TRACK_ID, role: "main", lane, fromPos: 0, toPos: sw.pos }
253
295
  ) : (
254
296
  // Double at B: Main 2 begins at the turnout and runs to B.
255
- { id: MAIN2_TRACK_ID, role: "main", lane: 1, fromPos: sw.pos, toPos: state.lengthInches }
297
+ { id: MAIN2_TRACK_ID, role: "main", lane, fromPos: sw.pos, toPos: state.lengthInches }
256
298
  );
257
299
  }
258
300
  function buildTransition(state) {
@@ -309,6 +351,7 @@ function stateToDoc(state, recordNumber) {
309
351
  lengthInches: state.lengthInches,
310
352
  ...state.loop ? { loop: true } : {},
311
353
  ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
354
+ ...state.mainsSwapped ? { mainsSwapped: true } : {},
312
355
  endplates: withWidths(
313
356
  withPoses(
314
357
  [
@@ -520,6 +563,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
520
563
  lengthInches: len,
521
564
  loop,
522
565
  loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
566
+ mainsSwapped: d.mainsSwapped === true,
523
567
  configA: configOf("A"),
524
568
  // On a loop, a missing B means pure turnback; present = interchange loop.
525
569
  configB: loop && !hasB ? "none" : configOf("B"),
@@ -1034,8 +1078,10 @@ function poseOverridesFromDoc(doc) {
1034
1078
  return out;
1035
1079
  }
1036
1080
 
1081
+ exports.FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES;
1037
1082
  exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
1038
1083
  exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
1084
+ exports.FREEMO_TRACK_SPACING_INCHES = FREEMO_TRACK_SPACING_INCHES;
1039
1085
  exports.MAIN2_TRACK_ID = MAIN2_TRACK_ID;
1040
1086
  exports.MAIN_TRACK_ID = MAIN_TRACK_ID;
1041
1087
  exports.N_CAR_LENGTH_INCHES = N_CAR_LENGTH_INCHES;
@@ -1047,11 +1093,13 @@ exports.buildCrossover = buildCrossover;
1047
1093
  exports.buildPassingSiding = buildPassingSiding;
1048
1094
  exports.buildTransition = buildTransition;
1049
1095
  exports.carCapacity = carCapacity;
1096
+ exports.checkEndplateWidth = checkEndplateWidth;
1050
1097
  exports.deriveEndplatePoses = deriveEndplatePoses;
1051
1098
  exports.divergeSideForHand = divergeSideForHand;
1052
1099
  exports.docToState = docToState;
1053
1100
  exports.emptyEditorState = emptyEditorState;
1054
1101
  exports.endplateFaceSegments = endplateFaceSegments;
1102
+ exports.endplateTrackOffsetFor = endplateTrackOffsetFor;
1055
1103
  exports.endplateWidthInches = endplateWidthInches;
1056
1104
  exports.geometryTurnDegrees = geometryTurnDegrees;
1057
1105
  exports.inchesToScaleFeet = inchesToScaleFeet;