@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 +64 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +61 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
153
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|