@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.js
CHANGED
|
@@ -610,6 +610,7 @@ function emptyEditorState(lengthInches) {
|
|
|
610
610
|
crossings: [],
|
|
611
611
|
branches: [],
|
|
612
612
|
poseOverrides: {},
|
|
613
|
+
flexByTrack: {},
|
|
613
614
|
endplateWidths: {},
|
|
614
615
|
endplateTrackOffsets: {},
|
|
615
616
|
outline: [],
|
|
@@ -711,6 +712,18 @@ function withWidths(endplates, widths, offsets = {}) {
|
|
|
711
712
|
return out;
|
|
712
713
|
});
|
|
713
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
|
+
}
|
|
714
727
|
function stateToDoc(state, recordNumber) {
|
|
715
728
|
return {
|
|
716
729
|
version: 1,
|
|
@@ -764,7 +777,10 @@ function stateToDoc(state, recordNumber) {
|
|
|
764
777
|
state.endplateWidths,
|
|
765
778
|
state.endplateTrackOffsets
|
|
766
779
|
),
|
|
767
|
-
|
|
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, [
|
|
768
784
|
state.loop ? (
|
|
769
785
|
// The main runs the lead from A and turns back at the balloon.
|
|
770
786
|
{ id: MAIN_TRACK_ID, role: "main", lane: 0, fromPos: 0, toPos: state.lengthInches }
|
|
@@ -799,7 +815,7 @@ function stateToDoc(state, recordNumber) {
|
|
|
799
815
|
...state.loop && t.inLoop ? { inLoop: true } : {},
|
|
800
816
|
...t.path && t.path.length >= 2 ? { path: t.path } : {}
|
|
801
817
|
}))
|
|
802
|
-
],
|
|
818
|
+
]),
|
|
803
819
|
turnouts: state.turnouts.map((t) => ({
|
|
804
820
|
id: t.id,
|
|
805
821
|
pos: t.pos,
|
|
@@ -941,6 +957,12 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
941
957
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
942
958
|
endplateTrackOffsets[e.id] = e.trackOffsetInches;
|
|
943
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
|
+
}
|
|
944
966
|
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
945
967
|
x: p.x,
|
|
946
968
|
y: p.y,
|
|
@@ -977,6 +999,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
977
999
|
trackId: ep.trackId ?? null
|
|
978
1000
|
})),
|
|
979
1001
|
poseOverrides,
|
|
1002
|
+
flexByTrack,
|
|
980
1003
|
endplateWidths,
|
|
981
1004
|
endplateTrackOffsets,
|
|
982
1005
|
outline,
|
|
@@ -1274,10 +1297,141 @@ var ATLAS_CODE55_N = [
|
|
|
1274
1297
|
innerRadius: { inches: 15, source: "manufacturer", note: "Atlas product listing" }
|
|
1275
1298
|
}
|
|
1276
1299
|
];
|
|
1277
|
-
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
|
+
}
|
|
1278
1340
|
function trackPart(id, library = BUILT_IN_TRACK_PARTS) {
|
|
1279
1341
|
return library.find((p) => p.id === id) ?? null;
|
|
1280
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
|
+
}
|
|
1281
1435
|
var TIE_HALF_LENGTH_INCHES = 0.319;
|
|
1282
1436
|
function partExtent(part) {
|
|
1283
1437
|
const pts = part?.pointsOffset;
|
|
@@ -2173,6 +2327,6 @@ function poseOverridesFromDoc(doc) {
|
|
|
2173
2327
|
return out;
|
|
2174
2328
|
}
|
|
2175
2329
|
|
|
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 };
|
|
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 };
|
|
2177
2331
|
//# sourceMappingURL=index.js.map
|
|
2178
2332
|
//# sourceMappingURL=index.js.map
|