@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 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
- tracks: [
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 BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N];
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