@willcgage/module-schematic 0.71.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 +167 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -2
- package/dist/index.d.ts +180 -2
- package/dist/index.js +158 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -612,6 +612,7 @@ function emptyEditorState(lengthInches) {
|
|
|
612
612
|
crossings: [],
|
|
613
613
|
branches: [],
|
|
614
614
|
poseOverrides: {},
|
|
615
|
+
flexByTrack: {},
|
|
615
616
|
endplateWidths: {},
|
|
616
617
|
endplateTrackOffsets: {},
|
|
617
618
|
outline: [],
|
|
@@ -713,6 +714,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
713
714
|
return out;
|
|
714
715
|
});
|
|
715
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
|
+
}
|
|
716
729
|
function stateToDoc(state, recordNumber) {
|
|
717
730
|
return {
|
|
718
731
|
version: 1,
|
|
@@ -766,7 +779,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
766
779
|
state.endplateWidths,
|
|
767
780
|
state.endplateTrackOffsets
|
|
768
781
|
),
|
|
769
|
-
|
|
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, [
|
|
770
786
|
state.loop ? (
|
|
771
787
|
// The main runs the lead from A and turns back at the balloon.
|
|
772
788
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -801,7 +817,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
801
817
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
802
818
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
803
819
|
}))
|
|
804
|
-
],
|
|
820
|
+
]),
|
|
805
821
|
turnouts: state.turnouts.map((t) => ({
|
|
806
822
|
id: t.id,
|
|
807
823
|
pos: t.pos,
|
|
@@ -943,6 +959,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
943
959
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
944
960
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
945
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
|
+
}
|
|
946
968
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
947
969
|
x: p.x,
|
|
948
970
|
y: p.y,
|
|
@@ -979,6 +1001,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
979
1001
|
trackId: ep.trackId ?? null
|
|
980
1002
|
})),
|
|
981
1003
|
poseOverrides,
|
|
1004
|
+
flexByTrack,
|
|
982
1005
|
endplateWidths,
|
|
983
1006
|
endplateTrackOffsets,
|
|
984
1007
|
outline,
|
|
@@ -1276,10 +1299,141 @@ var ATLAS_CODE55_N = [
|
|
|
1276
1299
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1277
1300
|
}
|
|
1278
1301
|
];
|
|
1279
|
-
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
|
+
}
|
|
1280
1342
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1281
1343
|
return library.find((p) => p.id === id) ?? null;
|
|
1282
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
|
+
}
|
|
1283
1437
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1284
1438
|
function partExtent(part) {
|
|
1285
1439
|
const pts = part?.pointsOffset;
|
|
@@ -2178,8 +2332,10 @@ function poseOverridesFromDoc(doc) {
|
|
|
2178
2332
|
exports.ATLAS_CODE55_N = ATLAS_CODE55_N;
|
|
2179
2333
|
exports.BUILT_IN_TRACK_PARTS = BUILT_IN_TRACK_PARTS;
|
|
2180
2334
|
exports.CODE55_RAIL_HEIGHT_INCHES = CODE55_RAIL_HEIGHT_INCHES;
|
|
2335
|
+
exports.DEFAULT_FLEX_PART_ID = DEFAULT_FLEX_PART_ID;
|
|
2181
2336
|
exports.ENDPLATE_FASCIA_CLEAR_INCHES = ENDPLATE_FASCIA_CLEAR_INCHES;
|
|
2182
2337
|
exports.ENDPLATE_LEAD_INCHES = ENDPLATE_LEAD_INCHES;
|
|
2338
|
+
exports.FLEX_TRACK_PARTS = FLEX_TRACK_PARTS;
|
|
2183
2339
|
exports.FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES;
|
|
2184
2340
|
exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
|
|
2185
2341
|
exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
|
|
@@ -2209,6 +2365,10 @@ exports.endplateFaceSegments = endplateFaceSegments;
|
|
|
2209
2365
|
exports.endplateLead = endplateLead;
|
|
2210
2366
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|
|
2211
2367
|
exports.endplateWidthInches = endplateWidthInches;
|
|
2368
|
+
exports.flexPartFor = flexPartFor;
|
|
2369
|
+
exports.flexParts = flexParts;
|
|
2370
|
+
exports.flexPieces = flexPieces;
|
|
2371
|
+
exports.flexUsage = flexUsage;
|
|
2212
2372
|
exports.frogCasting = frogCasting;
|
|
2213
2373
|
exports.frogNumberFromName = frogNumberFromName;
|
|
2214
2374
|
exports.fromSectionRelative = fromSectionRelative;
|
|
@@ -2219,6 +2379,7 @@ exports.inchesToScaleFeet = inchesToScaleFeet;
|
|
|
2219
2379
|
exports.isLoopDoc = isLoopDoc;
|
|
2220
2380
|
exports.isTransitionTurnout = isTransitionTurnout;
|
|
2221
2381
|
exports.leadInchesForSize = leadInchesForSize;
|
|
2382
|
+
exports.maxFlexPieceInches = maxFlexPieceInches;
|
|
2222
2383
|
exports.mergeImportedParts = mergeImportedParts;
|
|
2223
2384
|
exports.mergeStoredParts = mergeStoredParts;
|
|
2224
2385
|
exports.moduleCenterline = moduleCenterline;
|
|
@@ -2236,6 +2397,7 @@ exports.pathLengthInches = pathLengthInches;
|
|
|
2236
2397
|
exports.poseNeedsManual = poseNeedsManual;
|
|
2237
2398
|
exports.poseOverridesFromDoc = poseOverridesFromDoc;
|
|
2238
2399
|
exports.remapPos = remapPos;
|
|
2400
|
+
exports.resizeFlexPiece = resizeFlexPiece;
|
|
2239
2401
|
exports.returnLoop = returnLoop;
|
|
2240
2402
|
exports.sampleBenchworkOutline = sampleBenchworkOutline;
|
|
2241
2403
|
exports.samplePartSegments = samplePartSegments;
|
|
@@ -2259,6 +2421,8 @@ exports.trackMeetsEndplateIssues = trackMeetsEndplateIssues;
|
|
|
2259
2421
|
exports.trackPart = trackPart;
|
|
2260
2422
|
exports.trackPath = trackPath;
|
|
2261
2423
|
exports.turnoutClosure = turnoutClosure;
|
|
2424
|
+
exports.turnoutFacing = turnoutFacing;
|
|
2425
|
+
exports.turnoutOccupiedSpan = turnoutOccupiedSpan;
|
|
2262
2426
|
exports.turnoutPartForSize = turnoutPartForSize;
|
|
2263
2427
|
//# sourceMappingURL=index.cjs.map
|
|
2264
2428
|
//# sourceMappingURL=index.cjs.map
|