@willcgage/module-schematic 0.70.0 → 0.72.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 +203 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -13
- package/dist/index.d.ts +225 -13
- package/dist/index.js +193 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -471,8 +471,12 @@ function moduleFootprint(input) {
|
|
|
471
471
|
const widthA = endplateWidthFor(input.endplateWidths, "A");
|
|
472
472
|
const widthB = endplateWidthFor(input.endplateWidths, "B");
|
|
473
473
|
const authored = benchworkOutline(input);
|
|
474
|
-
const
|
|
475
|
-
|
|
474
|
+
const offOf = (i, id) => input.endplateTrackOffsets?.[id] ?? endplateCentreOffsetInches({
|
|
475
|
+
config: input.endplateConfigs?.[i],
|
|
476
|
+
main2Below: input.mainsSwapped === true
|
|
477
|
+
});
|
|
478
|
+
const offA = offOf(0, "A");
|
|
479
|
+
const offB = offOf(1, "B");
|
|
476
480
|
const secs = input.sections ?? [];
|
|
477
481
|
const sectionsOwnShape = secs.length > 1 || secs.some((s) => (s.outline?.length ?? 0) >= 3);
|
|
478
482
|
const sectionOutlines = sectionsOwnShape ? sectionFootprints(input, {
|
|
@@ -496,13 +500,19 @@ function moduleFootprint(input) {
|
|
|
496
500
|
sectionOutlines
|
|
497
501
|
};
|
|
498
502
|
}
|
|
499
|
-
function
|
|
500
|
-
const v = -endplateTrackOffsetInches(
|
|
503
|
+
function endplateCentreOffsetInches(input) {
|
|
504
|
+
const v = -endplateTrackOffsetInches(
|
|
505
|
+
input.authoredTrackOffsetInches,
|
|
506
|
+
input.config ?? void 0,
|
|
507
|
+
input.main2Below
|
|
508
|
+
);
|
|
501
509
|
return v === 0 ? 0 : v;
|
|
502
510
|
}
|
|
503
|
-
function endplateTrackOffsetInches(authored, config) {
|
|
511
|
+
function endplateTrackOffsetInches(authored, config, main2Below = false) {
|
|
504
512
|
if (typeof authored === "number" && Number.isFinite(authored)) return authored;
|
|
505
|
-
|
|
513
|
+
if (config !== "double") return 0;
|
|
514
|
+
const half = FREEMO_TRACK_SPACING_INCHES / 2;
|
|
515
|
+
return main2Below ? half : -half;
|
|
506
516
|
}
|
|
507
517
|
function checkEndplateWidth(input) {
|
|
508
518
|
const width = endplateWidthInches(input);
|
|
@@ -514,8 +524,13 @@ function checkEndplateWidth(input) {
|
|
|
514
524
|
requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
|
|
515
525
|
});
|
|
516
526
|
}
|
|
517
|
-
const off = endplateTrackOffsetInches(
|
|
518
|
-
|
|
527
|
+
const off = endplateTrackOffsetInches(
|
|
528
|
+
input.trackOffsetInches,
|
|
529
|
+
input.config ?? void 0,
|
|
530
|
+
input.main2Below
|
|
531
|
+
);
|
|
532
|
+
const second = off + (input.main2Below ? -1 : 1) * FREEMO_TRACK_SPACING_INCHES;
|
|
533
|
+
const centres = input.config === "double" ? [off, second] : [off];
|
|
519
534
|
const worst = Math.max(...centres.map((c) => Math.abs(c)));
|
|
520
535
|
const clearance = width / 2 - worst;
|
|
521
536
|
if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
|
|
@@ -526,6 +541,18 @@ function checkEndplateWidth(input) {
|
|
|
526
541
|
requiredInches: required
|
|
527
542
|
});
|
|
528
543
|
}
|
|
544
|
+
if (input.config === "double") {
|
|
545
|
+
const mid = (off + second) / 2;
|
|
546
|
+
if (Math.abs(mid) > 0.01) {
|
|
547
|
+
issues.push({
|
|
548
|
+
code: "offcentre",
|
|
549
|
+
message: `The two tracks sit ${round2(Math.abs(mid))}\u2033 off the centre of this endplate. The standard recommends they straddle it \u2014 Main 1 at ${round2((input.main2Below ? 1 : -1) * (FREEMO_TRACK_SPACING_INCHES / 2))}\u2033. Clear the offset to use that.`,
|
|
550
|
+
// Not a width problem — no wider plate fixes it — so hand back the
|
|
551
|
+
// width unchanged rather than imply one would.
|
|
552
|
+
requiredInches: width
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
529
556
|
return issues;
|
|
530
557
|
}
|
|
531
558
|
var round2 = (n) => Math.round(n * 100) / 100;
|
|
@@ -585,6 +612,7 @@ function emptyEditorState(lengthInches) {
|
|
|
585
612
|
crossings: [],
|
|
586
613
|
branches: [],
|
|
587
614
|
poseOverrides: {},
|
|
615
|
+
flexByTrack: {},
|
|
588
616
|
endplateWidths: {},
|
|
589
617
|
endplateTrackOffsets: {},
|
|
590
618
|
outline: [],
|
|
@@ -686,6 +714,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
686
714
|
return out;
|
|
687
715
|
});
|
|
688
716
|
}
|
|
717
|
+
function withFlex(state, tracks) {
|
|
718
|
+
return tracks.map((t) => {
|
|
719
|
+
const f = state.flexByTrack?.[t.id];
|
|
720
|
+
if (!f) return t;
|
|
721
|
+
const cuts = f.cuts?.filter((c) => Number.isFinite(c)).sort((a, b) => a - b);
|
|
722
|
+
return {
|
|
723
|
+
...t,
|
|
724
|
+
...f.partId ? { flexPartId: f.partId } : {},
|
|
725
|
+
...cuts ? { flexCuts: cuts } : {}
|
|
726
|
+
};
|
|
727
|
+
});
|
|
728
|
+
}
|
|
689
729
|
function stateToDoc(state, recordNumber) {
|
|
690
730
|
return {
|
|
691
731
|
version: 1,
|
|
@@ -739,7 +779,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
739
779
|
state.endplateWidths,
|
|
740
780
|
state.endplateTrackOffsets
|
|
741
781
|
),
|
|
742
|
-
|
|
782
|
+
// Flex settings are attached to every track at once, at the end — the array
|
|
783
|
+
// below has half a dozen branches and adding two fields to each of them is
|
|
784
|
+
// how they'd end up disagreeing (#193).
|
|
785
|
+
tracks: withFlex(state, [
|
|
743
786
|
state.loop ? (
|
|
744
787
|
// The main runs the lead from A and turns back at the balloon.
|
|
745
788
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -774,7 +817,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
774
817
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
775
818
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
776
819
|
}))
|
|
777
|
-
],
|
|
820
|
+
]),
|
|
778
821
|
turnouts: state.turnouts.map((t) => ({
|
|
779
822
|
id: t.id,
|
|
780
823
|
pos: t.pos,
|
|
@@ -916,6 +959,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
916
959
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
917
960
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
918
961
|
}
|
|
962
|
+
const flexByTrack = {};
|
|
963
|
+
for (const t of d.tracks ?? []) {
|
|
964
|
+
const partId = typeof t.flexPartId === "string" && t.flexPartId ? t.flexPartId : void 0;
|
|
965
|
+
const cuts = Array.isArray(t.flexCuts) ? t.flexCuts.filter((c) => Number.isFinite(c)).map(sc).sort((a, b) => a - b) : void 0;
|
|
966
|
+
if (partId || cuts) flexByTrack[t.id] = { ...partId ? { partId } : {}, ...cuts ? { cuts } : {} };
|
|
967
|
+
}
|
|
919
968
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
920
969
|
x: p.x,
|
|
921
970
|
y: p.y,
|
|
@@ -952,6 +1001,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
952
1001
|
trackId: ep.trackId ?? null
|
|
953
1002
|
})),
|
|
954
1003
|
poseOverrides,
|
|
1004
|
+
flexByTrack,
|
|
955
1005
|
endplateWidths,
|
|
956
1006
|
endplateTrackOffsets,
|
|
957
1007
|
outline,
|
|
@@ -1249,10 +1299,141 @@ var ATLAS_CODE55_N = [
|
|
|
1249
1299
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1250
1300
|
}
|
|
1251
1301
|
];
|
|
1252
|
-
var
|
|
1302
|
+
var FLEX_TRACK_PARTS = [
|
|
1303
|
+
{
|
|
1304
|
+
id: "atlas-c55-n-flex",
|
|
1305
|
+
manufacturer: "Atlas",
|
|
1306
|
+
line: "Code 55",
|
|
1307
|
+
scale: "N",
|
|
1308
|
+
name: "Code 55 Flex Track",
|
|
1309
|
+
kind: "flex",
|
|
1310
|
+
overallLength: {
|
|
1311
|
+
inches: 30,
|
|
1312
|
+
source: "manufacturer",
|
|
1313
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
id: "me-c55-n-flex",
|
|
1318
|
+
manufacturer: "Micro Engineering",
|
|
1319
|
+
line: "Code 55",
|
|
1320
|
+
scale: "N",
|
|
1321
|
+
name: "Code 55 Flex Track",
|
|
1322
|
+
kind: "flex",
|
|
1323
|
+
overallLength: {
|
|
1324
|
+
inches: 36,
|
|
1325
|
+
source: "manufacturer",
|
|
1326
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
];
|
|
1330
|
+
var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
|
|
1331
|
+
var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
|
|
1332
|
+
function flexParts(library = BUILT_IN_TRACK_PARTS) {
|
|
1333
|
+
return library.filter((p) => p.kind === "flex");
|
|
1334
|
+
}
|
|
1335
|
+
function flexPartFor(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1336
|
+
const chosen = id ? library.find((p) => p.id === id && p.kind === "flex") : null;
|
|
1337
|
+
return chosen ?? library.find((p) => p.id === DEFAULT_FLEX_PART_ID) ?? flexParts(library)[0] ?? null;
|
|
1338
|
+
}
|
|
1339
|
+
function maxFlexPieceInches(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1340
|
+
return flexPartFor(id, library)?.overallLength?.inches ?? 30;
|
|
1341
|
+
}
|
|
1253
1342
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1254
1343
|
return library.find((p) => p.id === id) ?? null;
|
|
1255
1344
|
}
|
|
1345
|
+
var FLEX_EPS = 1e-6;
|
|
1346
|
+
var FLEX_MIN_PIECE_INCHES = 1;
|
|
1347
|
+
function flexPieces(input) {
|
|
1348
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
1349
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
1350
|
+
if (!(hi - lo > FLEX_EPS)) return [];
|
|
1351
|
+
const max = input.maxPieceInches > FLEX_EPS ? input.maxPieceInches : Infinity;
|
|
1352
|
+
const blocks = (input.occupied ?? []).map((s) => ({
|
|
1353
|
+
from: Math.max(lo, Math.min(s.fromPos, s.toPos)),
|
|
1354
|
+
to: Math.min(hi, Math.max(s.fromPos, s.toPos))
|
|
1355
|
+
})).filter((s) => s.to - s.from >= -FLEX_EPS && s.from < hi + FLEX_EPS && s.to > lo - FLEX_EPS).sort((a, b) => a.from - b.from);
|
|
1356
|
+
const merged = [];
|
|
1357
|
+
for (const b of blocks) {
|
|
1358
|
+
const last = merged[merged.length - 1];
|
|
1359
|
+
if (last && b.from <= last.to + FLEX_EPS) last.to = Math.max(last.to, b.to);
|
|
1360
|
+
else merged.push({ ...b });
|
|
1361
|
+
}
|
|
1362
|
+
const gaps = [];
|
|
1363
|
+
let cursor = lo;
|
|
1364
|
+
for (const b of merged) {
|
|
1365
|
+
if (b.from - cursor > FLEX_EPS) gaps.push({ from: cursor, to: b.from });
|
|
1366
|
+
cursor = Math.max(cursor, b.to);
|
|
1367
|
+
}
|
|
1368
|
+
if (hi - cursor > FLEX_EPS) gaps.push({ from: cursor, to: hi });
|
|
1369
|
+
const authored = input.cuts != null;
|
|
1370
|
+
const out = [];
|
|
1371
|
+
for (const g of gaps) {
|
|
1372
|
+
let inner;
|
|
1373
|
+
if (authored) {
|
|
1374
|
+
inner = [...new Set(input.cuts)].filter((c) => c > g.from + FLEX_EPS && c < g.to - FLEX_EPS).sort((a, b) => a - b);
|
|
1375
|
+
} else {
|
|
1376
|
+
inner = [];
|
|
1377
|
+
for (let at = g.from + max; at < g.to - FLEX_EPS; at += max) inner.push(at);
|
|
1378
|
+
const tail = g.to - (inner[inner.length - 1] ?? g.from);
|
|
1379
|
+
if (inner.length && tail < FLEX_MIN_PIECE_INCHES) {
|
|
1380
|
+
const start = inner.length >= 2 ? inner[inner.length - 2] : g.from;
|
|
1381
|
+
inner[inner.length - 1] = start + (g.to - start) / 2;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const bounds = [g.from, ...inner, g.to];
|
|
1385
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
1386
|
+
const from = bounds[i];
|
|
1387
|
+
const to = bounds[i + 1];
|
|
1388
|
+
out.push({
|
|
1389
|
+
index: out.length,
|
|
1390
|
+
fromPos: from,
|
|
1391
|
+
toPos: to,
|
|
1392
|
+
lengthInches: to - from,
|
|
1393
|
+
overlong: to - from > max + FLEX_EPS,
|
|
1394
|
+
// The run's own ends are runEnd; anything else is a part body, except
|
|
1395
|
+
// where a cut put a neighbouring piece there.
|
|
1396
|
+
fromEnd: i > 0 ? "piece" : from <= lo + FLEX_EPS ? "runEnd" : "part",
|
|
1397
|
+
toEnd: i < bounds.length - 2 ? "piece" : to >= hi - FLEX_EPS ? "runEnd" : "part"
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return out;
|
|
1402
|
+
}
|
|
1403
|
+
function turnoutFacing(input) {
|
|
1404
|
+
const far = input.divergeFarPos;
|
|
1405
|
+
const geometric = typeof far === "number" && Number.isFinite(far) ? Math.sign(far - input.pos) || 1 : 1;
|
|
1406
|
+
return input.flipped ? -geometric : geometric;
|
|
1407
|
+
}
|
|
1408
|
+
function turnoutOccupiedSpan(input) {
|
|
1409
|
+
const e = input.extent;
|
|
1410
|
+
if (!e) return null;
|
|
1411
|
+
const a = input.pos - input.facing * e.behindPoints;
|
|
1412
|
+
const b = input.pos + input.facing * e.aheadOfPoints;
|
|
1413
|
+
return { fromPos: Math.min(a, b), toPos: Math.max(a, b) };
|
|
1414
|
+
}
|
|
1415
|
+
function resizeFlexPiece(pieces, index, nextLengthInches) {
|
|
1416
|
+
const piece = pieces[index];
|
|
1417
|
+
const next = pieces[index + 1];
|
|
1418
|
+
if (!piece || !next) return null;
|
|
1419
|
+
if (piece.toEnd !== "piece") return null;
|
|
1420
|
+
if (!Number.isFinite(nextLengthInches)) return null;
|
|
1421
|
+
const pair = piece.lengthInches + next.lengthInches;
|
|
1422
|
+
if (pair < 2 * FLEX_MIN_PIECE_INCHES) return null;
|
|
1423
|
+
const want = Math.max(
|
|
1424
|
+
FLEX_MIN_PIECE_INCHES,
|
|
1425
|
+
Math.min(pair - FLEX_MIN_PIECE_INCHES, nextLengthInches)
|
|
1426
|
+
);
|
|
1427
|
+
const moved = piece.fromPos + want;
|
|
1428
|
+
return pieces.filter((p) => p.toEnd === "piece").map((p) => p.index === index ? moved : p.toPos).map((v) => Math.round(v * 1e3) / 1e3).sort((a, b) => a - b);
|
|
1429
|
+
}
|
|
1430
|
+
function flexUsage(pieces) {
|
|
1431
|
+
return {
|
|
1432
|
+
pieces: pieces.length,
|
|
1433
|
+
totalInches: pieces.reduce((a, p) => a + p.lengthInches, 0),
|
|
1434
|
+
overlong: pieces.filter((p) => p.overlong).length
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1256
1437
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1257
1438
|
function partExtent(part) {
|
|
1258
1439
|
const pts = part?.pointsOffset;
|
|
@@ -2151,8 +2332,10 @@ function poseOverridesFromDoc(doc) {
|
|
|
2151
2332
|
exports.ATLAS_CODE55_N = ATLAS_CODE55_N;
|
|
2152
2333
|
exports.BUILT_IN_TRACK_PARTS = BUILT_IN_TRACK_PARTS;
|
|
2153
2334
|
exports.CODE55_RAIL_HEIGHT_INCHES = CODE55_RAIL_HEIGHT_INCHES;
|
|
2335
|
+
exports.DEFAULT_FLEX_PART_ID = DEFAULT_FLEX_PART_ID;
|
|
2154
2336
|
exports.ENDPLATE_FASCIA_CLEAR_INCHES = ENDPLATE_FASCIA_CLEAR_INCHES;
|
|
2155
2337
|
exports.ENDPLATE_LEAD_INCHES = ENDPLATE_LEAD_INCHES;
|
|
2338
|
+
exports.FLEX_TRACK_PARTS = FLEX_TRACK_PARTS;
|
|
2156
2339
|
exports.FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES;
|
|
2157
2340
|
exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
|
|
2158
2341
|
exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
|
|
@@ -2177,11 +2360,15 @@ exports.deriveEndplatePoses = deriveEndplatePoses;
|
|
|
2177
2360
|
exports.divergeSideForHand = divergeSideForHand;
|
|
2178
2361
|
exports.docToState = docToState;
|
|
2179
2362
|
exports.emptyEditorState = emptyEditorState;
|
|
2363
|
+
exports.endplateCentreOffsetInches = endplateCentreOffsetInches;
|
|
2180
2364
|
exports.endplateFaceSegments = endplateFaceSegments;
|
|
2181
2365
|
exports.endplateLead = endplateLead;
|
|
2182
|
-
exports.endplateTrackOffsetFor = endplateTrackOffsetFor;
|
|
2183
2366
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|
|
2184
2367
|
exports.endplateWidthInches = endplateWidthInches;
|
|
2368
|
+
exports.flexPartFor = flexPartFor;
|
|
2369
|
+
exports.flexParts = flexParts;
|
|
2370
|
+
exports.flexPieces = flexPieces;
|
|
2371
|
+
exports.flexUsage = flexUsage;
|
|
2185
2372
|
exports.frogCasting = frogCasting;
|
|
2186
2373
|
exports.frogNumberFromName = frogNumberFromName;
|
|
2187
2374
|
exports.fromSectionRelative = fromSectionRelative;
|
|
@@ -2192,6 +2379,7 @@ exports.inchesToScaleFeet = inchesToScaleFeet;
|
|
|
2192
2379
|
exports.isLoopDoc = isLoopDoc;
|
|
2193
2380
|
exports.isTransitionTurnout = isTransitionTurnout;
|
|
2194
2381
|
exports.leadInchesForSize = leadInchesForSize;
|
|
2382
|
+
exports.maxFlexPieceInches = maxFlexPieceInches;
|
|
2195
2383
|
exports.mergeImportedParts = mergeImportedParts;
|
|
2196
2384
|
exports.mergeStoredParts = mergeStoredParts;
|
|
2197
2385
|
exports.moduleCenterline = moduleCenterline;
|
|
@@ -2209,6 +2397,7 @@ exports.pathLengthInches = pathLengthInches;
|
|
|
2209
2397
|
exports.poseNeedsManual = poseNeedsManual;
|
|
2210
2398
|
exports.poseOverridesFromDoc = poseOverridesFromDoc;
|
|
2211
2399
|
exports.remapPos = remapPos;
|
|
2400
|
+
exports.resizeFlexPiece = resizeFlexPiece;
|
|
2212
2401
|
exports.returnLoop = returnLoop;
|
|
2213
2402
|
exports.sampleBenchworkOutline = sampleBenchworkOutline;
|
|
2214
2403
|
exports.samplePartSegments = samplePartSegments;
|
|
@@ -2232,6 +2421,8 @@ exports.trackMeetsEndplateIssues = trackMeetsEndplateIssues;
|
|
|
2232
2421
|
exports.trackPart = trackPart;
|
|
2233
2422
|
exports.trackPath = trackPath;
|
|
2234
2423
|
exports.turnoutClosure = turnoutClosure;
|
|
2424
|
+
exports.turnoutFacing = turnoutFacing;
|
|
2425
|
+
exports.turnoutOccupiedSpan = turnoutOccupiedSpan;
|
|
2235
2426
|
exports.turnoutPartForSize = turnoutPartForSize;
|
|
2236
2427
|
//# sourceMappingURL=index.cjs.map
|
|
2237
2428
|
//# sourceMappingURL=index.cjs.map
|