@willcgage/module-schematic 0.71.0 → 0.73.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 +182 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +209 -2
- package/dist/index.d.ts +209 -2
- package/dist/index.js +172 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -575,6 +575,20 @@ function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
|
|
|
575
575
|
return feet * 12 / ratio;
|
|
576
576
|
}
|
|
577
577
|
var N_CAR_LENGTH_INCHES = 3.3;
|
|
578
|
+
function spanOverhang(input) {
|
|
579
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
580
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
581
|
+
const tLo = Math.min(input.trackFromPos, input.trackToPos);
|
|
582
|
+
const tHi = Math.max(input.trackFromPos, input.trackToPos);
|
|
583
|
+
const before = Math.max(0, tLo - lo);
|
|
584
|
+
const after = Math.max(0, hi - tHi);
|
|
585
|
+
return {
|
|
586
|
+
beforeInches: before,
|
|
587
|
+
afterInches: after,
|
|
588
|
+
onTrackInches: Math.max(0, Math.min(hi, tHi) - Math.max(lo, tLo)),
|
|
589
|
+
overhangInches: before + after
|
|
590
|
+
};
|
|
591
|
+
}
|
|
578
592
|
function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
|
|
579
593
|
if (!(carLengthInches > 0)) return 0;
|
|
580
594
|
return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
|
|
@@ -610,6 +624,7 @@ function emptyEditorState(lengthInches) {
|
|
|
610
624
|
crossings: [],
|
|
611
625
|
branches: [],
|
|
612
626
|
poseOverrides: {},
|
|
627
|
+
flexByTrack: {},
|
|
613
628
|
endplateWidths: {},
|
|
614
629
|
endplateTrackOffsets: {},
|
|
615
630
|
outline: [],
|
|
@@ -711,6 +726,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
711
726
|
return out;
|
|
712
727
|
});
|
|
713
728
|
}
|
|
729
|
+
function withFlex(state, tracks) {
|
|
730
|
+
return tracks.map((t) => {
|
|
731
|
+
const f = state.flexByTrack?.[t.id];
|
|
732
|
+
if (!f) return t;
|
|
733
|
+
const cuts = f.cuts?.filter((c) => Number.isFinite(c)).sort((a, b) => a - b);
|
|
734
|
+
return {
|
|
735
|
+
...t,
|
|
736
|
+
...f.partId ? { flexPartId: f.partId } : {},
|
|
737
|
+
...cuts ? { flexCuts: cuts } : {}
|
|
738
|
+
};
|
|
739
|
+
});
|
|
740
|
+
}
|
|
714
741
|
function stateToDoc(state, recordNumber) {
|
|
715
742
|
return {
|
|
716
743
|
version: 1,
|
|
@@ -764,7 +791,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
764
791
|
state.endplateWidths,
|
|
765
792
|
state.endplateTrackOffsets
|
|
766
793
|
),
|
|
767
|
-
|
|
794
|
+
// Flex settings are attached to every track at once, at the end — the array
|
|
795
|
+
// below has half a dozen branches and adding two fields to each of them is
|
|
796
|
+
// how they'd end up disagreeing (#193).
|
|
797
|
+
tracks: withFlex(state, [
|
|
768
798
|
state.loop ? (
|
|
769
799
|
// The main runs the lead from A and turns back at the balloon.
|
|
770
800
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -799,7 +829,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
799
829
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
800
830
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
801
831
|
}))
|
|
802
|
-
],
|
|
832
|
+
]),
|
|
803
833
|
turnouts: state.turnouts.map((t) => ({
|
|
804
834
|
id: t.id,
|
|
805
835
|
pos: t.pos,
|
|
@@ -941,6 +971,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
941
971
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
942
972
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
943
973
|
}
|
|
974
|
+
const flexByTrack = {};
|
|
975
|
+
for (const t of d.tracks ?? []) {
|
|
976
|
+
const partId = typeof t.flexPartId === "string" && t.flexPartId ? t.flexPartId : void 0;
|
|
977
|
+
const cuts = Array.isArray(t.flexCuts) ? t.flexCuts.filter((c) => Number.isFinite(c)).map(sc).sort((a, b) => a - b) : void 0;
|
|
978
|
+
if (partId || cuts) flexByTrack[t.id] = { ...partId ? { partId } : {}, ...cuts ? { cuts } : {} };
|
|
979
|
+
}
|
|
944
980
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
945
981
|
x: p.x,
|
|
946
982
|
y: p.y,
|
|
@@ -977,6 +1013,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
977
1013
|
trackId: ep.trackId ?? null
|
|
978
1014
|
})),
|
|
979
1015
|
poseOverrides,
|
|
1016
|
+
flexByTrack,
|
|
980
1017
|
endplateWidths,
|
|
981
1018
|
endplateTrackOffsets,
|
|
982
1019
|
outline,
|
|
@@ -1274,10 +1311,141 @@ var ATLAS_CODE55_N = [
|
|
|
1274
1311
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1275
1312
|
}
|
|
1276
1313
|
];
|
|
1277
|
-
var
|
|
1314
|
+
var FLEX_TRACK_PARTS = [
|
|
1315
|
+
{
|
|
1316
|
+
id: "atlas-c55-n-flex",
|
|
1317
|
+
manufacturer: "Atlas",
|
|
1318
|
+
line: "Code 55",
|
|
1319
|
+
scale: "N",
|
|
1320
|
+
name: "Code 55 Flex Track",
|
|
1321
|
+
kind: "flex",
|
|
1322
|
+
overallLength: {
|
|
1323
|
+
inches: 30,
|
|
1324
|
+
source: "manufacturer",
|
|
1325
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
id: "me-c55-n-flex",
|
|
1330
|
+
manufacturer: "Micro Engineering",
|
|
1331
|
+
line: "Code 55",
|
|
1332
|
+
scale: "N",
|
|
1333
|
+
name: "Code 55 Flex Track",
|
|
1334
|
+
kind: "flex",
|
|
1335
|
+
overallLength: {
|
|
1336
|
+
inches: 36,
|
|
1337
|
+
source: "manufacturer",
|
|
1338
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
];
|
|
1342
|
+
var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
|
|
1343
|
+
var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
|
|
1344
|
+
function flexParts(library = BUILT_IN_TRACK_PARTS) {
|
|
1345
|
+
return library.filter((p) => p.kind === "flex");
|
|
1346
|
+
}
|
|
1347
|
+
function flexPartFor(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1348
|
+
const chosen = id ? library.find((p) => p.id === id && p.kind === "flex") : null;
|
|
1349
|
+
return chosen ?? library.find((p) => p.id === DEFAULT_FLEX_PART_ID) ?? flexParts(library)[0] ?? null;
|
|
1350
|
+
}
|
|
1351
|
+
function maxFlexPieceInches(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1352
|
+
return flexPartFor(id, library)?.overallLength?.inches ?? 30;
|
|
1353
|
+
}
|
|
1278
1354
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1279
1355
|
return library.find((p) => p.id === id) ?? null;
|
|
1280
1356
|
}
|
|
1357
|
+
var FLEX_EPS = 1e-6;
|
|
1358
|
+
var FLEX_MIN_PIECE_INCHES = 1;
|
|
1359
|
+
function flexPieces(input) {
|
|
1360
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
1361
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
1362
|
+
if (!(hi - lo > FLEX_EPS)) return [];
|
|
1363
|
+
const max = input.maxPieceInches > FLEX_EPS ? input.maxPieceInches : Infinity;
|
|
1364
|
+
const blocks = (input.occupied ?? []).map((s) => ({
|
|
1365
|
+
from: Math.max(lo, Math.min(s.fromPos, s.toPos)),
|
|
1366
|
+
to: Math.min(hi, Math.max(s.fromPos, s.toPos))
|
|
1367
|
+
})).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);
|
|
1368
|
+
const merged = [];
|
|
1369
|
+
for (const b of blocks) {
|
|
1370
|
+
const last = merged[merged.length - 1];
|
|
1371
|
+
if (last && b.from <= last.to + FLEX_EPS) last.to = Math.max(last.to, b.to);
|
|
1372
|
+
else merged.push({ ...b });
|
|
1373
|
+
}
|
|
1374
|
+
const gaps = [];
|
|
1375
|
+
let cursor = lo;
|
|
1376
|
+
for (const b of merged) {
|
|
1377
|
+
if (b.from - cursor > FLEX_EPS) gaps.push({ from: cursor, to: b.from });
|
|
1378
|
+
cursor = Math.max(cursor, b.to);
|
|
1379
|
+
}
|
|
1380
|
+
if (hi - cursor > FLEX_EPS) gaps.push({ from: cursor, to: hi });
|
|
1381
|
+
const authored = input.cuts != null;
|
|
1382
|
+
const out = [];
|
|
1383
|
+
for (const g of gaps) {
|
|
1384
|
+
let inner;
|
|
1385
|
+
if (authored) {
|
|
1386
|
+
inner = [...new Set(input.cuts)].filter((c) => c > g.from + FLEX_EPS && c < g.to - FLEX_EPS).sort((a, b) => a - b);
|
|
1387
|
+
} else {
|
|
1388
|
+
inner = [];
|
|
1389
|
+
for (let at = g.from + max; at < g.to - FLEX_EPS; at += max) inner.push(at);
|
|
1390
|
+
const tail = g.to - (inner[inner.length - 1] ?? g.from);
|
|
1391
|
+
if (inner.length && tail < FLEX_MIN_PIECE_INCHES) {
|
|
1392
|
+
const start = inner.length >= 2 ? inner[inner.length - 2] : g.from;
|
|
1393
|
+
inner[inner.length - 1] = start + (g.to - start) / 2;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const bounds = [g.from, ...inner, g.to];
|
|
1397
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
1398
|
+
const from = bounds[i];
|
|
1399
|
+
const to = bounds[i + 1];
|
|
1400
|
+
out.push({
|
|
1401
|
+
index: out.length,
|
|
1402
|
+
fromPos: from,
|
|
1403
|
+
toPos: to,
|
|
1404
|
+
lengthInches: to - from,
|
|
1405
|
+
overlong: to - from > max + FLEX_EPS,
|
|
1406
|
+
// The run's own ends are runEnd; anything else is a part body, except
|
|
1407
|
+
// where a cut put a neighbouring piece there.
|
|
1408
|
+
fromEnd: i > 0 ? "piece" : from <= lo + FLEX_EPS ? "runEnd" : "part",
|
|
1409
|
+
toEnd: i < bounds.length - 2 ? "piece" : to >= hi - FLEX_EPS ? "runEnd" : "part"
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return out;
|
|
1414
|
+
}
|
|
1415
|
+
function turnoutFacing(input) {
|
|
1416
|
+
const far = input.divergeFarPos;
|
|
1417
|
+
const geometric = typeof far === "number" && Number.isFinite(far) ? Math.sign(far - input.pos) || 1 : 1;
|
|
1418
|
+
return input.flipped ? -geometric : geometric;
|
|
1419
|
+
}
|
|
1420
|
+
function turnoutOccupiedSpan(input) {
|
|
1421
|
+
const e = input.extent;
|
|
1422
|
+
if (!e) return null;
|
|
1423
|
+
const a = input.pos - input.facing * e.behindPoints;
|
|
1424
|
+
const b = input.pos + input.facing * e.aheadOfPoints;
|
|
1425
|
+
return { fromPos: Math.min(a, b), toPos: Math.max(a, b) };
|
|
1426
|
+
}
|
|
1427
|
+
function resizeFlexPiece(pieces, index, nextLengthInches) {
|
|
1428
|
+
const piece = pieces[index];
|
|
1429
|
+
const next = pieces[index + 1];
|
|
1430
|
+
if (!piece || !next) return null;
|
|
1431
|
+
if (piece.toEnd !== "piece") return null;
|
|
1432
|
+
if (!Number.isFinite(nextLengthInches)) return null;
|
|
1433
|
+
const pair = piece.lengthInches + next.lengthInches;
|
|
1434
|
+
if (pair < 2 * FLEX_MIN_PIECE_INCHES) return null;
|
|
1435
|
+
const want = Math.max(
|
|
1436
|
+
FLEX_MIN_PIECE_INCHES,
|
|
1437
|
+
Math.min(pair - FLEX_MIN_PIECE_INCHES, nextLengthInches)
|
|
1438
|
+
);
|
|
1439
|
+
const moved = piece.fromPos + want;
|
|
1440
|
+
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);
|
|
1441
|
+
}
|
|
1442
|
+
function flexUsage(pieces) {
|
|
1443
|
+
return {
|
|
1444
|
+
pieces: pieces.length,
|
|
1445
|
+
totalInches: pieces.reduce((a, p) => a + p.lengthInches, 0),
|
|
1446
|
+
overlong: pieces.filter((p) => p.overlong).length
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1281
1449
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1282
1450
|
function partExtent(part) {
|
|
1283
1451
|
const pts = part?.pointsOffset;
|
|
@@ -2173,6 +2341,6 @@ function poseOverridesFromDoc(doc) {
|
|
|
2173
2341
|
return out;
|
|
2174
2342
|
}
|
|
2175
2343
|
|
|
2176
|
-
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, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
|
|
2344
|
+
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, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize };
|
|
2177
2345
|
//# sourceMappingURL=index.js.map
|
|
2178
2346
|
//# sourceMappingURL=index.js.map
|