@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.cjs
CHANGED
|
@@ -577,6 +577,20 @@ function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
|
|
|
577
577
|
return feet * 12 / ratio;
|
|
578
578
|
}
|
|
579
579
|
var N_CAR_LENGTH_INCHES = 3.3;
|
|
580
|
+
function spanOverhang(input) {
|
|
581
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
582
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
583
|
+
const tLo = Math.min(input.trackFromPos, input.trackToPos);
|
|
584
|
+
const tHi = Math.max(input.trackFromPos, input.trackToPos);
|
|
585
|
+
const before = Math.max(0, tLo - lo);
|
|
586
|
+
const after = Math.max(0, hi - tHi);
|
|
587
|
+
return {
|
|
588
|
+
beforeInches: before,
|
|
589
|
+
afterInches: after,
|
|
590
|
+
onTrackInches: Math.max(0, Math.min(hi, tHi) - Math.max(lo, tLo)),
|
|
591
|
+
overhangInches: before + after
|
|
592
|
+
};
|
|
593
|
+
}
|
|
580
594
|
function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
|
|
581
595
|
if (!(carLengthInches > 0)) return 0;
|
|
582
596
|
return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
|
|
@@ -612,6 +626,7 @@ function emptyEditorState(lengthInches) {
|
|
|
612
626
|
crossings: [],
|
|
613
627
|
branches: [],
|
|
614
628
|
poseOverrides: {},
|
|
629
|
+
flexByTrack: {},
|
|
615
630
|
endplateWidths: {},
|
|
616
631
|
endplateTrackOffsets: {},
|
|
617
632
|
outline: [],
|
|
@@ -713,6 +728,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
713
728
|
return out;
|
|
714
729
|
});
|
|
715
730
|
}
|
|
731
|
+
function withFlex(state, tracks) {
|
|
732
|
+
return tracks.map((t) => {
|
|
733
|
+
const f = state.flexByTrack?.[t.id];
|
|
734
|
+
if (!f) return t;
|
|
735
|
+
const cuts = f.cuts?.filter((c) => Number.isFinite(c)).sort((a, b) => a - b);
|
|
736
|
+
return {
|
|
737
|
+
...t,
|
|
738
|
+
...f.partId ? { flexPartId: f.partId } : {},
|
|
739
|
+
...cuts ? { flexCuts: cuts } : {}
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
}
|
|
716
743
|
function stateToDoc(state, recordNumber) {
|
|
717
744
|
return {
|
|
718
745
|
version: 1,
|
|
@@ -766,7 +793,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
766
793
|
state.endplateWidths,
|
|
767
794
|
state.endplateTrackOffsets
|
|
768
795
|
),
|
|
769
|
-
|
|
796
|
+
// Flex settings are attached to every track at once, at the end — the array
|
|
797
|
+
// below has half a dozen branches and adding two fields to each of them is
|
|
798
|
+
// how they'd end up disagreeing (#193).
|
|
799
|
+
tracks: withFlex(state, [
|
|
770
800
|
state.loop ? (
|
|
771
801
|
// The main runs the lead from A and turns back at the balloon.
|
|
772
802
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -801,7 +831,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
801
831
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
802
832
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
803
833
|
}))
|
|
804
|
-
],
|
|
834
|
+
]),
|
|
805
835
|
turnouts: state.turnouts.map((t) => ({
|
|
806
836
|
id: t.id,
|
|
807
837
|
pos: t.pos,
|
|
@@ -943,6 +973,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
943
973
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
944
974
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
945
975
|
}
|
|
976
|
+
const flexByTrack = {};
|
|
977
|
+
for (const t of d.tracks ?? []) {
|
|
978
|
+
const partId = typeof t.flexPartId === "string" && t.flexPartId ? t.flexPartId : void 0;
|
|
979
|
+
const cuts = Array.isArray(t.flexCuts) ? t.flexCuts.filter((c) => Number.isFinite(c)).map(sc).sort((a, b) => a - b) : void 0;
|
|
980
|
+
if (partId || cuts) flexByTrack[t.id] = { ...partId ? { partId } : {}, ...cuts ? { cuts } : {} };
|
|
981
|
+
}
|
|
946
982
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
947
983
|
x: p.x,
|
|
948
984
|
y: p.y,
|
|
@@ -979,6 +1015,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
979
1015
|
trackId: ep.trackId ?? null
|
|
980
1016
|
})),
|
|
981
1017
|
poseOverrides,
|
|
1018
|
+
flexByTrack,
|
|
982
1019
|
endplateWidths,
|
|
983
1020
|
endplateTrackOffsets,
|
|
984
1021
|
outline,
|
|
@@ -1276,10 +1313,141 @@ var ATLAS_CODE55_N = [
|
|
|
1276
1313
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1277
1314
|
}
|
|
1278
1315
|
];
|
|
1279
|
-
var
|
|
1316
|
+
var FLEX_TRACK_PARTS = [
|
|
1317
|
+
{
|
|
1318
|
+
id: "atlas-c55-n-flex",
|
|
1319
|
+
manufacturer: "Atlas",
|
|
1320
|
+
line: "Code 55",
|
|
1321
|
+
scale: "N",
|
|
1322
|
+
name: "Code 55 Flex Track",
|
|
1323
|
+
kind: "flex",
|
|
1324
|
+
overallLength: {
|
|
1325
|
+
inches: 30,
|
|
1326
|
+
source: "manufacturer",
|
|
1327
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
id: "me-c55-n-flex",
|
|
1332
|
+
manufacturer: "Micro Engineering",
|
|
1333
|
+
line: "Code 55",
|
|
1334
|
+
scale: "N",
|
|
1335
|
+
name: "Code 55 Flex Track",
|
|
1336
|
+
kind: "flex",
|
|
1337
|
+
overallLength: {
|
|
1338
|
+
inches: 36,
|
|
1339
|
+
source: "manufacturer",
|
|
1340
|
+
note: "nominal product length, reported by Will Gage 2026-07-26"
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
];
|
|
1344
|
+
var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
|
|
1345
|
+
var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
|
|
1346
|
+
function flexParts(library = BUILT_IN_TRACK_PARTS) {
|
|
1347
|
+
return library.filter((p) => p.kind === "flex");
|
|
1348
|
+
}
|
|
1349
|
+
function flexPartFor(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1350
|
+
const chosen = id ? library.find((p) => p.id === id && p.kind === "flex") : null;
|
|
1351
|
+
return chosen ?? library.find((p) => p.id === DEFAULT_FLEX_PART_ID) ?? flexParts(library)[0] ?? null;
|
|
1352
|
+
}
|
|
1353
|
+
function maxFlexPieceInches(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1354
|
+
return flexPartFor(id, library)?.overallLength?.inches ?? 30;
|
|
1355
|
+
}
|
|
1280
1356
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1281
1357
|
return library.find((p) => p.id === id) ?? null;
|
|
1282
1358
|
}
|
|
1359
|
+
var FLEX_EPS = 1e-6;
|
|
1360
|
+
var FLEX_MIN_PIECE_INCHES = 1;
|
|
1361
|
+
function flexPieces(input) {
|
|
1362
|
+
const lo = Math.min(input.fromPos, input.toPos);
|
|
1363
|
+
const hi = Math.max(input.fromPos, input.toPos);
|
|
1364
|
+
if (!(hi - lo > FLEX_EPS)) return [];
|
|
1365
|
+
const max = input.maxPieceInches > FLEX_EPS ? input.maxPieceInches : Infinity;
|
|
1366
|
+
const blocks = (input.occupied ?? []).map((s) => ({
|
|
1367
|
+
from: Math.max(lo, Math.min(s.fromPos, s.toPos)),
|
|
1368
|
+
to: Math.min(hi, Math.max(s.fromPos, s.toPos))
|
|
1369
|
+
})).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);
|
|
1370
|
+
const merged = [];
|
|
1371
|
+
for (const b of blocks) {
|
|
1372
|
+
const last = merged[merged.length - 1];
|
|
1373
|
+
if (last && b.from <= last.to + FLEX_EPS) last.to = Math.max(last.to, b.to);
|
|
1374
|
+
else merged.push({ ...b });
|
|
1375
|
+
}
|
|
1376
|
+
const gaps = [];
|
|
1377
|
+
let cursor = lo;
|
|
1378
|
+
for (const b of merged) {
|
|
1379
|
+
if (b.from - cursor > FLEX_EPS) gaps.push({ from: cursor, to: b.from });
|
|
1380
|
+
cursor = Math.max(cursor, b.to);
|
|
1381
|
+
}
|
|
1382
|
+
if (hi - cursor > FLEX_EPS) gaps.push({ from: cursor, to: hi });
|
|
1383
|
+
const authored = input.cuts != null;
|
|
1384
|
+
const out = [];
|
|
1385
|
+
for (const g of gaps) {
|
|
1386
|
+
let inner;
|
|
1387
|
+
if (authored) {
|
|
1388
|
+
inner = [...new Set(input.cuts)].filter((c) => c > g.from + FLEX_EPS && c < g.to - FLEX_EPS).sort((a, b) => a - b);
|
|
1389
|
+
} else {
|
|
1390
|
+
inner = [];
|
|
1391
|
+
for (let at = g.from + max; at < g.to - FLEX_EPS; at += max) inner.push(at);
|
|
1392
|
+
const tail = g.to - (inner[inner.length - 1] ?? g.from);
|
|
1393
|
+
if (inner.length && tail < FLEX_MIN_PIECE_INCHES) {
|
|
1394
|
+
const start = inner.length >= 2 ? inner[inner.length - 2] : g.from;
|
|
1395
|
+
inner[inner.length - 1] = start + (g.to - start) / 2;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const bounds = [g.from, ...inner, g.to];
|
|
1399
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
1400
|
+
const from = bounds[i];
|
|
1401
|
+
const to = bounds[i + 1];
|
|
1402
|
+
out.push({
|
|
1403
|
+
index: out.length,
|
|
1404
|
+
fromPos: from,
|
|
1405
|
+
toPos: to,
|
|
1406
|
+
lengthInches: to - from,
|
|
1407
|
+
overlong: to - from > max + FLEX_EPS,
|
|
1408
|
+
// The run's own ends are runEnd; anything else is a part body, except
|
|
1409
|
+
// where a cut put a neighbouring piece there.
|
|
1410
|
+
fromEnd: i > 0 ? "piece" : from <= lo + FLEX_EPS ? "runEnd" : "part",
|
|
1411
|
+
toEnd: i < bounds.length - 2 ? "piece" : to >= hi - FLEX_EPS ? "runEnd" : "part"
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return out;
|
|
1416
|
+
}
|
|
1417
|
+
function turnoutFacing(input) {
|
|
1418
|
+
const far = input.divergeFarPos;
|
|
1419
|
+
const geometric = typeof far === "number" && Number.isFinite(far) ? Math.sign(far - input.pos) || 1 : 1;
|
|
1420
|
+
return input.flipped ? -geometric : geometric;
|
|
1421
|
+
}
|
|
1422
|
+
function turnoutOccupiedSpan(input) {
|
|
1423
|
+
const e = input.extent;
|
|
1424
|
+
if (!e) return null;
|
|
1425
|
+
const a = input.pos - input.facing * e.behindPoints;
|
|
1426
|
+
const b = input.pos + input.facing * e.aheadOfPoints;
|
|
1427
|
+
return { fromPos: Math.min(a, b), toPos: Math.max(a, b) };
|
|
1428
|
+
}
|
|
1429
|
+
function resizeFlexPiece(pieces, index, nextLengthInches) {
|
|
1430
|
+
const piece = pieces[index];
|
|
1431
|
+
const next = pieces[index + 1];
|
|
1432
|
+
if (!piece || !next) return null;
|
|
1433
|
+
if (piece.toEnd !== "piece") return null;
|
|
1434
|
+
if (!Number.isFinite(nextLengthInches)) return null;
|
|
1435
|
+
const pair = piece.lengthInches + next.lengthInches;
|
|
1436
|
+
if (pair < 2 * FLEX_MIN_PIECE_INCHES) return null;
|
|
1437
|
+
const want = Math.max(
|
|
1438
|
+
FLEX_MIN_PIECE_INCHES,
|
|
1439
|
+
Math.min(pair - FLEX_MIN_PIECE_INCHES, nextLengthInches)
|
|
1440
|
+
);
|
|
1441
|
+
const moved = piece.fromPos + want;
|
|
1442
|
+
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);
|
|
1443
|
+
}
|
|
1444
|
+
function flexUsage(pieces) {
|
|
1445
|
+
return {
|
|
1446
|
+
pieces: pieces.length,
|
|
1447
|
+
totalInches: pieces.reduce((a, p) => a + p.lengthInches, 0),
|
|
1448
|
+
overlong: pieces.filter((p) => p.overlong).length
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1283
1451
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1284
1452
|
function partExtent(part) {
|
|
1285
1453
|
const pts = part?.pointsOffset;
|
|
@@ -2178,8 +2346,10 @@ function poseOverridesFromDoc(doc) {
|
|
|
2178
2346
|
exports.ATLAS_CODE55_N = ATLAS_CODE55_N;
|
|
2179
2347
|
exports.BUILT_IN_TRACK_PARTS = BUILT_IN_TRACK_PARTS;
|
|
2180
2348
|
exports.CODE55_RAIL_HEIGHT_INCHES = CODE55_RAIL_HEIGHT_INCHES;
|
|
2349
|
+
exports.DEFAULT_FLEX_PART_ID = DEFAULT_FLEX_PART_ID;
|
|
2181
2350
|
exports.ENDPLATE_FASCIA_CLEAR_INCHES = ENDPLATE_FASCIA_CLEAR_INCHES;
|
|
2182
2351
|
exports.ENDPLATE_LEAD_INCHES = ENDPLATE_LEAD_INCHES;
|
|
2352
|
+
exports.FLEX_TRACK_PARTS = FLEX_TRACK_PARTS;
|
|
2183
2353
|
exports.FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES;
|
|
2184
2354
|
exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
|
|
2185
2355
|
exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
|
|
@@ -2209,6 +2379,10 @@ exports.endplateFaceSegments = endplateFaceSegments;
|
|
|
2209
2379
|
exports.endplateLead = endplateLead;
|
|
2210
2380
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|
|
2211
2381
|
exports.endplateWidthInches = endplateWidthInches;
|
|
2382
|
+
exports.flexPartFor = flexPartFor;
|
|
2383
|
+
exports.flexParts = flexParts;
|
|
2384
|
+
exports.flexPieces = flexPieces;
|
|
2385
|
+
exports.flexUsage = flexUsage;
|
|
2212
2386
|
exports.frogCasting = frogCasting;
|
|
2213
2387
|
exports.frogNumberFromName = frogNumberFromName;
|
|
2214
2388
|
exports.fromSectionRelative = fromSectionRelative;
|
|
@@ -2219,6 +2393,7 @@ exports.inchesToScaleFeet = inchesToScaleFeet;
|
|
|
2219
2393
|
exports.isLoopDoc = isLoopDoc;
|
|
2220
2394
|
exports.isTransitionTurnout = isTransitionTurnout;
|
|
2221
2395
|
exports.leadInchesForSize = leadInchesForSize;
|
|
2396
|
+
exports.maxFlexPieceInches = maxFlexPieceInches;
|
|
2222
2397
|
exports.mergeImportedParts = mergeImportedParts;
|
|
2223
2398
|
exports.mergeStoredParts = mergeStoredParts;
|
|
2224
2399
|
exports.moduleCenterline = moduleCenterline;
|
|
@@ -2236,6 +2411,7 @@ exports.pathLengthInches = pathLengthInches;
|
|
|
2236
2411
|
exports.poseNeedsManual = poseNeedsManual;
|
|
2237
2412
|
exports.poseOverridesFromDoc = poseOverridesFromDoc;
|
|
2238
2413
|
exports.remapPos = remapPos;
|
|
2414
|
+
exports.resizeFlexPiece = resizeFlexPiece;
|
|
2239
2415
|
exports.returnLoop = returnLoop;
|
|
2240
2416
|
exports.sampleBenchworkOutline = sampleBenchworkOutline;
|
|
2241
2417
|
exports.samplePartSegments = samplePartSegments;
|
|
@@ -2252,6 +2428,7 @@ exports.sectionSpansOrWhole = sectionSpansOrWhole;
|
|
|
2252
2428
|
exports.sectionedCenterline = sectionedCenterline;
|
|
2253
2429
|
exports.sectionedEndPose = sectionedEndPose;
|
|
2254
2430
|
exports.sliceCenterline = sliceCenterline;
|
|
2431
|
+
exports.spanOverhang = spanOverhang;
|
|
2255
2432
|
exports.stateToDoc = stateToDoc;
|
|
2256
2433
|
exports.storedPartToTrackPart = storedPartToTrackPart;
|
|
2257
2434
|
exports.toSectionRelative = toSectionRelative;
|
|
@@ -2259,6 +2436,8 @@ exports.trackMeetsEndplateIssues = trackMeetsEndplateIssues;
|
|
|
2259
2436
|
exports.trackPart = trackPart;
|
|
2260
2437
|
exports.trackPath = trackPath;
|
|
2261
2438
|
exports.turnoutClosure = turnoutClosure;
|
|
2439
|
+
exports.turnoutFacing = turnoutFacing;
|
|
2440
|
+
exports.turnoutOccupiedSpan = turnoutOccupiedSpan;
|
|
2262
2441
|
exports.turnoutPartForSize = turnoutPartForSize;
|
|
2263
2442
|
//# sourceMappingURL=index.cjs.map
|
|
2264
2443
|
//# sourceMappingURL=index.cjs.map
|