@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.js
CHANGED
|
@@ -469,8 +469,12 @@ function moduleFootprint(input) {
|
|
|
469
469
|
const widthA = endplateWidthFor(input.endplateWidths, "A");
|
|
470
470
|
const widthB = endplateWidthFor(input.endplateWidths, "B");
|
|
471
471
|
const authored = benchworkOutline(input);
|
|
472
|
-
const
|
|
473
|
-
|
|
472
|
+
const offOf = (i, id) => input.endplateTrackOffsets?.[id] ?? endplateCentreOffsetInches({
|
|
473
|
+
config: input.endplateConfigs?.[i],
|
|
474
|
+
main2Below: input.mainsSwapped === true
|
|
475
|
+
});
|
|
476
|
+
const offA = offOf(0, "A");
|
|
477
|
+
const offB = offOf(1, "B");
|
|
474
478
|
const secs = input.sections ?? [];
|
|
475
479
|
const sectionsOwnShape = secs.length > 1 || secs.some((s) => (s.outline?.length ?? 0) >= 3);
|
|
476
480
|
const sectionOutlines = sectionsOwnShape ? sectionFootprints(input, {
|
|
@@ -494,13 +498,19 @@ function moduleFootprint(input) {
|
|
|
494
498
|
sectionOutlines
|
|
495
499
|
};
|
|
496
500
|
}
|
|
497
|
-
function
|
|
498
|
-
const v = -endplateTrackOffsetInches(
|
|
501
|
+
function endplateCentreOffsetInches(input) {
|
|
502
|
+
const v = -endplateTrackOffsetInches(
|
|
503
|
+
input.authoredTrackOffsetInches,
|
|
504
|
+
input.config ?? void 0,
|
|
505
|
+
input.main2Below
|
|
506
|
+
);
|
|
499
507
|
return v === 0 ? 0 : v;
|
|
500
508
|
}
|
|
501
|
-
function endplateTrackOffsetInches(authored, config) {
|
|
509
|
+
function endplateTrackOffsetInches(authored, config, main2Below = false) {
|
|
502
510
|
if (typeof authored === "number" && Number.isFinite(authored)) return authored;
|
|
503
|
-
|
|
511
|
+
if (config !== "double") return 0;
|
|
512
|
+
const half = FREEMO_TRACK_SPACING_INCHES / 2;
|
|
513
|
+
return main2Below ? half : -half;
|
|
504
514
|
}
|
|
505
515
|
function checkEndplateWidth(input) {
|
|
506
516
|
const width = endplateWidthInches(input);
|
|
@@ -512,8 +522,13 @@ function checkEndplateWidth(input) {
|
|
|
512
522
|
requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
|
|
513
523
|
});
|
|
514
524
|
}
|
|
515
|
-
const off = endplateTrackOffsetInches(
|
|
516
|
-
|
|
525
|
+
const off = endplateTrackOffsetInches(
|
|
526
|
+
input.trackOffsetInches,
|
|
527
|
+
input.config ?? void 0,
|
|
528
|
+
input.main2Below
|
|
529
|
+
);
|
|
530
|
+
const second = off + (input.main2Below ? -1 : 1) * FREEMO_TRACK_SPACING_INCHES;
|
|
531
|
+
const centres = input.config === "double" ? [off, second] : [off];
|
|
517
532
|
const worst = Math.max(...centres.map((c) => Math.abs(c)));
|
|
518
533
|
const clearance = width / 2 - worst;
|
|
519
534
|
if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
|
|
@@ -524,6 +539,18 @@ function checkEndplateWidth(input) {
|
|
|
524
539
|
requiredInches: required
|
|
525
540
|
});
|
|
526
541
|
}
|
|
542
|
+
if (input.config === "double") {
|
|
543
|
+
const mid = (off + second) / 2;
|
|
544
|
+
if (Math.abs(mid) > 0.01) {
|
|
545
|
+
issues.push({
|
|
546
|
+
code: "offcentre",
|
|
547
|
+
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.`,
|
|
548
|
+
// Not a width problem — no wider plate fixes it — so hand back the
|
|
549
|
+
// width unchanged rather than imply one would.
|
|
550
|
+
requiredInches: width
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
527
554
|
return issues;
|
|
528
555
|
}
|
|
529
556
|
var round2 = (n) => Math.round(n * 100) / 100;
|
|
@@ -583,6 +610,7 @@ function emptyEditorState(lengthInches) {
|
|
|
583
610
|
crossings: [],
|
|
584
611
|
branches: [],
|
|
585
612
|
poseOverrides: {},
|
|
613
|
+
flexByTrack: {},
|
|
586
614
|
endplateWidths: {},
|
|
587
615
|
endplateTrackOffsets: {},
|
|
588
616
|
outline: [],
|
|
@@ -684,6 +712,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
684
712
|
return out;
|
|
685
713
|
});
|
|
686
714
|
}
|
|
715
|
+
function withFlex(state, tracks) {
|
|
716
|
+
return tracks.map((t) => {
|
|
717
|
+
const f = state.flexByTrack?.[t.id];
|
|
718
|
+
if (!f) return t;
|
|
719
|
+
const cuts = f.cuts?.filter((c) => Number.isFinite(c)).sort((a, b) => a - b);
|
|
720
|
+
return {
|
|
721
|
+
...t,
|
|
722
|
+
...f.partId ? { flexPartId: f.partId } : {},
|
|
723
|
+
...cuts ? { flexCuts: cuts } : {}
|
|
724
|
+
};
|
|
725
|
+
});
|
|
726
|
+
}
|
|
687
727
|
function stateToDoc(state, recordNumber) {
|
|
688
728
|
return {
|
|
689
729
|
version: 1,
|
|
@@ -737,7 +777,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
737
777
|
state.endplateWidths,
|
|
738
778
|
state.endplateTrackOffsets
|
|
739
779
|
),
|
|
740
|
-
|
|
780
|
+
// Flex settings are attached to every track at once, at the end — the array
|
|
781
|
+
// below has half a dozen branches and adding two fields to each of them is
|
|
782
|
+
// how they'd end up disagreeing (#193).
|
|
783
|
+
tracks: withFlex(state, [
|
|
741
784
|
state.loop ? (
|
|
742
785
|
// The main runs the lead from A and turns back at the balloon.
|
|
743
786
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -772,7 +815,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
772
815
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
773
816
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
774
817
|
}))
|
|
775
|
-
],
|
|
818
|
+
]),
|
|
776
819
|
turnouts: state.turnouts.map((t) => ({
|
|
777
820
|
id: t.id,
|
|
778
821
|
pos: t.pos,
|
|
@@ -914,6 +957,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
914
957
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
915
958
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
916
959
|
}
|
|
960
|
+
const flexByTrack = {};
|
|
961
|
+
for (const t of d.tracks ?? []) {
|
|
962
|
+
const partId = typeof t.flexPartId === "string" && t.flexPartId ? t.flexPartId : void 0;
|
|
963
|
+
const cuts = Array.isArray(t.flexCuts) ? t.flexCuts.filter((c) => Number.isFinite(c)).map(sc).sort((a, b) => a - b) : void 0;
|
|
964
|
+
if (partId || cuts) flexByTrack[t.id] = { ...partId ? { partId } : {}, ...cuts ? { cuts } : {} };
|
|
965
|
+
}
|
|
917
966
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
918
967
|
x: p.x,
|
|
919
968
|
y: p.y,
|
|
@@ -950,6 +999,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
950
999
|
trackId: ep.trackId ?? null
|
|
951
1000
|
})),
|
|
952
1001
|
poseOverrides,
|
|
1002
|
+
flexByTrack,
|
|
953
1003
|
endplateWidths,
|
|
954
1004
|
endplateTrackOffsets,
|
|
955
1005
|
outline,
|
|
@@ -1247,10 +1297,141 @@ var ATLAS_CODE55_N = [
|
|
|
1247
1297
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1248
1298
|
}
|
|
1249
1299
|
];
|
|
1250
|
-
var
|
|
1300
|
+
var FLEX_TRACK_PARTS = [
|
|
1301
|
+
{
|
|
1302
|
+
id: "atlas-c55-n-flex",
|
|
1303
|
+
manufacturer: "Atlas",
|
|
1304
|
+
line: "Code 55",
|
|
1305
|
+
scale: "N",
|
|
1306
|
+
name: "Code 55 Flex Track",
|
|
1307
|
+
kind: "flex",
|
|
1308
|
+
overallLength: {
|
|
1309
|
+
inches: 30,
|
|
1310
|
+
source: "manufacturer",
|
|
1311
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
id: "me-c55-n-flex",
|
|
1316
|
+
manufacturer: "Micro Engineering",
|
|
1317
|
+
line: "Code 55",
|
|
1318
|
+
scale: "N",
|
|
1319
|
+
name: "Code 55 Flex Track",
|
|
1320
|
+
kind: "flex",
|
|
1321
|
+
overallLength: {
|
|
1322
|
+
inches: 36,
|
|
1323
|
+
source: "manufacturer",
|
|
1324
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
];
|
|
1328
|
+
var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
|
|
1329
|
+
var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
|
|
1330
|
+
function flexParts(library = BUILT_IN_TRACK_PARTS) {
|
|
1331
|
+
return library.filter((p) => p.kind === "flex");
|
|
1332
|
+
}
|
|
1333
|
+
function flexPartFor(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1334
|
+
const chosen = id ? library.find((p) => p.id === id && p.kind === "flex") : null;
|
|
1335
|
+
return chosen ?? library.find((p) => p.id === DEFAULT_FLEX_PART_ID) ?? flexParts(library)[0] ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
function maxFlexPieceInches(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1338
|
+
return flexPartFor(id, library)?.overallLength?.inches ?? 30;
|
|
1339
|
+
}
|
|
1251
1340
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1252
1341
|
return library.find((p) => p.id === id) ?? null;
|
|
1253
1342
|
}
|
|
1343
|
+
var FLEX_EPS = 1e-6;
|
|
1344
|
+
var FLEX_MIN_PIECE_INCHES = 1;
|
|
1345
|
+
function flexPieces(input) {
|
|
1346
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
1347
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
1348
|
+
if (!(hi - lo > FLEX_EPS)) return [];
|
|
1349
|
+
const max = input.maxPieceInches > FLEX_EPS ? input.maxPieceInches : Infinity;
|
|
1350
|
+
const blocks = (input.occupied ?? []).map((s) => ({
|
|
1351
|
+
from: Math.max(lo, Math.min(s.fromPos, s.toPos)),
|
|
1352
|
+
to: Math.min(hi, Math.max(s.fromPos, s.toPos))
|
|
1353
|
+
})).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);
|
|
1354
|
+
const merged = [];
|
|
1355
|
+
for (const b of blocks) {
|
|
1356
|
+
const last = merged[merged.length - 1];
|
|
1357
|
+
if (last && b.from <= last.to + FLEX_EPS) last.to = Math.max(last.to, b.to);
|
|
1358
|
+
else merged.push({ ...b });
|
|
1359
|
+
}
|
|
1360
|
+
const gaps = [];
|
|
1361
|
+
let cursor = lo;
|
|
1362
|
+
for (const b of merged) {
|
|
1363
|
+
if (b.from - cursor > FLEX_EPS) gaps.push({ from: cursor, to: b.from });
|
|
1364
|
+
cursor = Math.max(cursor, b.to);
|
|
1365
|
+
}
|
|
1366
|
+
if (hi - cursor > FLEX_EPS) gaps.push({ from: cursor, to: hi });
|
|
1367
|
+
const authored = input.cuts != null;
|
|
1368
|
+
const out = [];
|
|
1369
|
+
for (const g of gaps) {
|
|
1370
|
+
let inner;
|
|
1371
|
+
if (authored) {
|
|
1372
|
+
inner = [...new Set(input.cuts)].filter((c) => c > g.from + FLEX_EPS && c < g.to - FLEX_EPS).sort((a, b) => a - b);
|
|
1373
|
+
} else {
|
|
1374
|
+
inner = [];
|
|
1375
|
+
for (let at = g.from + max; at < g.to - FLEX_EPS; at += max) inner.push(at);
|
|
1376
|
+
const tail = g.to - (inner[inner.length - 1] ?? g.from);
|
|
1377
|
+
if (inner.length && tail < FLEX_MIN_PIECE_INCHES) {
|
|
1378
|
+
const start = inner.length >= 2 ? inner[inner.length - 2] : g.from;
|
|
1379
|
+
inner[inner.length - 1] = start + (g.to - start) / 2;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const bounds = [g.from, ...inner, g.to];
|
|
1383
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
1384
|
+
const from = bounds[i];
|
|
1385
|
+
const to = bounds[i + 1];
|
|
1386
|
+
out.push({
|
|
1387
|
+
index: out.length,
|
|
1388
|
+
fromPos: from,
|
|
1389
|
+
toPos: to,
|
|
1390
|
+
lengthInches: to - from,
|
|
1391
|
+
overlong: to - from > max + FLEX_EPS,
|
|
1392
|
+
// The run's own ends are runEnd; anything else is a part body, except
|
|
1393
|
+
// where a cut put a neighbouring piece there.
|
|
1394
|
+
fromEnd: i > 0 ? "piece" : from <= lo + FLEX_EPS ? "runEnd" : "part",
|
|
1395
|
+
toEnd: i < bounds.length - 2 ? "piece" : to >= hi - FLEX_EPS ? "runEnd" : "part"
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return out;
|
|
1400
|
+
}
|
|
1401
|
+
function turnoutFacing(input) {
|
|
1402
|
+
const far = input.divergeFarPos;
|
|
1403
|
+
const geometric = typeof far === "number" && Number.isFinite(far) ? Math.sign(far - input.pos) || 1 : 1;
|
|
1404
|
+
return input.flipped ? -geometric : geometric;
|
|
1405
|
+
}
|
|
1406
|
+
function turnoutOccupiedSpan(input) {
|
|
1407
|
+
const e = input.extent;
|
|
1408
|
+
if (!e) return null;
|
|
1409
|
+
const a = input.pos - input.facing * e.behindPoints;
|
|
1410
|
+
const b = input.pos + input.facing * e.aheadOfPoints;
|
|
1411
|
+
return { fromPos: Math.min(a, b), toPos: Math.max(a, b) };
|
|
1412
|
+
}
|
|
1413
|
+
function resizeFlexPiece(pieces, index, nextLengthInches) {
|
|
1414
|
+
const piece = pieces[index];
|
|
1415
|
+
const next = pieces[index + 1];
|
|
1416
|
+
if (!piece || !next) return null;
|
|
1417
|
+
if (piece.toEnd !== "piece") return null;
|
|
1418
|
+
if (!Number.isFinite(nextLengthInches)) return null;
|
|
1419
|
+
const pair = piece.lengthInches + next.lengthInches;
|
|
1420
|
+
if (pair < 2 * FLEX_MIN_PIECE_INCHES) return null;
|
|
1421
|
+
const want = Math.max(
|
|
1422
|
+
FLEX_MIN_PIECE_INCHES,
|
|
1423
|
+
Math.min(pair - FLEX_MIN_PIECE_INCHES, nextLengthInches)
|
|
1424
|
+
);
|
|
1425
|
+
const moved = piece.fromPos + want;
|
|
1426
|
+
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);
|
|
1427
|
+
}
|
|
1428
|
+
function flexUsage(pieces) {
|
|
1429
|
+
return {
|
|
1430
|
+
pieces: pieces.length,
|
|
1431
|
+
totalInches: pieces.reduce((a, p) => a + p.lengthInches, 0),
|
|
1432
|
+
overlong: pieces.filter((p) => p.overlong).length
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1254
1435
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1255
1436
|
function partExtent(part) {
|
|
1256
1437
|
const pts = part?.pointsOffset;
|
|
@@ -2146,6 +2327,6 @@ function poseOverridesFromDoc(doc) {
|
|
|
2146
2327
|
return out;
|
|
2147
2328
|
}
|
|
2148
2329
|
|
|
2149
|
-
export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, 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, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead,
|
|
2330
|
+
export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FLEX_TRACK_PARTS, 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, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize };
|
|
2150
2331
|
//# sourceMappingURL=index.js.map
|
|
2151
2332
|
//# sourceMappingURL=index.js.map
|