@willcgage/module-schematic 0.74.0 → 0.76.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
@@ -31,10 +31,28 @@ function moduleSections(doc) {
31
31
  ...typeof len === "number" && Number.isFinite(len) && len > 0 ? { lengthInches: len } : {},
32
32
  ...sec.geometryType ? { geometryType: sec.geometryType } : {},
33
33
  ...typeof deg === "number" && Number.isFinite(deg) ? { geometryDegrees: deg } : {},
34
- ...typeof off === "number" && Number.isFinite(off) ? { geometryOffsetInches: off } : {}
34
+ ...typeof off === "number" && Number.isFinite(off) ? { geometryOffsetInches: off } : {},
35
+ // The board's two ends (#130). Written only when actually described —
36
+ // an absent end is an ordinary internal joint, and an empty object
37
+ // would claim the owner had said something about it.
38
+ ...sectionEnd(sec.endA) ? { endA: sectionEnd(sec.endA) } : {},
39
+ ...sectionEnd(sec.endB) ? { endB: sectionEnd(sec.endB) } : {}
35
40
  };
36
41
  });
37
42
  }
43
+ function sectionEnd(end) {
44
+ if (!end) return null;
45
+ const config = end.config === "single" || end.config === "double" || end.config === "none" ? end.config : null;
46
+ const w = end.widthInches;
47
+ const o = end.trackOffsetInches;
48
+ const out = {
49
+ ...config ? { config } : {},
50
+ ...typeof w === "number" && Number.isFinite(w) && w > 0 ? { widthInches: w } : {},
51
+ // Signed, and 0 is meaningful ("explicitly centred", #93) — keep any finite.
52
+ ...typeof o === "number" && Number.isFinite(o) ? { trackOffsetInches: o } : {}
53
+ };
54
+ return Object.keys(out).length ? out : null;
55
+ }
38
56
  function sectionFootprints(doc, derive) {
39
57
  const spans = derive ? sectionSpans(doc) : [];
40
58
  const spanOf = new Map(spans.map((sp) => [sp.id, sp]));
@@ -577,6 +595,92 @@ function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
577
595
  return feet * 12 / ratio;
578
596
  }
579
597
  var N_CAR_LENGTH_INCHES = 3.3;
598
+ var CLEARANCE_SPACING_INCHES = FREEMO_TRACK_SPACING_INCHES;
599
+ function clearancePointPastFrogInches(size, library = BUILT_IN_TRACK_PARTS, clearanceInches = CLEARANCE_SPACING_INCHES) {
600
+ const N = size > 0 ? size : 6;
601
+ const lead = leadInchesForSize(N, library);
602
+ const cl = turnoutClosure(N, { leadInches: lead });
603
+ let lo = 0;
604
+ let hi = Math.max(4 * lead, 4 * clearanceInches * N);
605
+ if (cl.offsetAt(hi) < clearanceInches) return hi - lead;
606
+ for (let i = 0; i < 60; i++) {
607
+ const mid = (lo + hi) / 2;
608
+ if (cl.offsetAt(mid) < clearanceInches) lo = mid;
609
+ else hi = mid;
610
+ }
611
+ return Math.max(0, hi - lead);
612
+ }
613
+ function usableCapacity(input) {
614
+ const lo = Math.min(input.fromPos, input.toPos);
615
+ const hi = Math.max(input.fromPos, input.toPos);
616
+ const drawn = hi - lo;
617
+ const carLen = input.carLengthInches ?? N_CAR_LENGTH_INCHES;
618
+ if (typeof input.measuredUsableInches === "number" && input.measuredUsableInches >= 0) {
619
+ const m = input.measuredUsableInches;
620
+ return {
621
+ fromPos: lo,
622
+ toPos: lo + m,
623
+ usableInches: m,
624
+ drawnInches: drawn,
625
+ givenUpInches: Math.max(0, drawn - m),
626
+ cars: carCapacity(0, m, carLen),
627
+ scaleFeet: Math.round(inchesToScaleFeet(m))
628
+ };
629
+ }
630
+ let a = lo;
631
+ let b = hi;
632
+ for (const sw of input.governing ?? []) {
633
+ const c = clearancePointPastFrogInches(sw.size ?? 6, input.library);
634
+ if (Math.abs(sw.pos - lo) <= Math.abs(sw.pos - hi)) a = Math.max(a, lo + c);
635
+ else b = Math.min(b, hi - c);
636
+ }
637
+ const usable = Math.max(0, b - a);
638
+ return {
639
+ fromPos: a,
640
+ toPos: Math.max(a, b),
641
+ usableInches: usable,
642
+ drawnInches: drawn,
643
+ givenUpInches: Math.max(0, drawn - usable),
644
+ cars: carCapacity(0, usable, carLen),
645
+ scaleFeet: Math.round(inchesToScaleFeet(usable))
646
+ };
647
+ }
648
+ function assessSectionEnd(end, opts = {}) {
649
+ const config = end?.config ?? "none";
650
+ const widthInches = endplateWidthInches({ widthInches: end?.widthInches });
651
+ const trackOffsetInches = endplateTrackOffsetInches(
652
+ end?.trackOffsetInches,
653
+ config,
654
+ opts.main2Below
655
+ );
656
+ const described = config === "single" || config === "double";
657
+ if (!described) {
658
+ return { described: false, conforming: false, issues: [], config, widthInches, trackOffsetInches };
659
+ }
660
+ const issues = checkEndplateWidth({
661
+ widthInches: end?.widthInches,
662
+ config,
663
+ trackOffsetInches: end?.trackOffsetInches,
664
+ main2Below: opts.main2Below
665
+ });
666
+ return { described: true, conforming: issues.length === 0, issues, config, widthInches, trackOffsetInches };
667
+ }
668
+ function assessSectionJoint(west, east, opts = {}) {
669
+ const w = assessSectionEnd(west, opts);
670
+ const e = assessSectionEnd(east, opts);
671
+ let reason = null;
672
+ if (!w.described || !e.described) {
673
+ reason = "an internal joint \u2014 neither end has been described as an interface";
674
+ if (w.described !== e.described)
675
+ reason = `only the ${w.described ? "west" : "east"} side is described as an endplate`;
676
+ } else if (!w.conforming || !e.conforming) {
677
+ const bad = !w.conforming ? w : e;
678
+ reason = bad.issues[0]?.message ?? "an end doesn't meet the standard";
679
+ } else if (w.config !== e.config) {
680
+ reason = `${w.config} meets ${e.config} \u2014 the track counts differ`;
681
+ }
682
+ return { west: w, east: e, standardInterface: reason === null, reason };
683
+ }
580
684
  function spanOverhang(input) {
581
685
  const lo = Math.min(input.fromPos, input.toPos);
582
686
  const hi = Math.max(input.fromPos, input.toPos);
@@ -827,7 +931,17 @@ function stateToDoc(state, recordNumber) {
827
931
  toPos: t.toPos,
828
932
  moduleTrackId: t.moduleTrackId,
829
933
  trackName: t.trackName || void 0,
830
- capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
934
+ // ⚠️ USABLE capacity, measured from the governing turnouts' CLEARANCE
935
+ // POINTS (#19) — not the drawn rail-to-rail length, which counts track
936
+ // a car can't stand on without fouling the route it diverged from.
937
+ // FMN-0040's 70″ siding is 21 cars drawn and 17 usable.
938
+ capacityFeet: usableCapacity({
939
+ fromPos: t.fromPos,
940
+ toPos: t.toPos,
941
+ governing: state.turnouts.filter((sw) => sw.divergeTrack === t.id),
942
+ measuredUsableInches: t.measuredUsableInches
943
+ }).scaleFeet,
944
+ ...typeof t.measuredUsableInches === "number" && t.measuredUsableInches >= 0 ? { measuredUsableInches: t.measuredUsableInches } : {},
831
945
  ...state.loop && t.inLoop ? { inLoop: true } : {},
832
946
  ...t.path && t.path.length >= 2 ? { path: t.path } : {}
833
947
  }))
@@ -926,7 +1040,10 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
926
1040
  trackName: t.trackName ?? nameOf(moduleTrackId),
927
1041
  ...t.inLoop ? { inLoop: true } : {},
928
1042
  // Authored path kept as-drawn (a physical shape, not rescaled with length).
929
- ...trackPath(t.path) ? { path: trackPath(t.path) } : {}
1043
+ ...trackPath(t.path) ? { path: trackPath(t.path) } : {},
1044
+ // A MEASURED length is a real-world fact about the physical track, so it
1045
+ // rescales with the module exactly as its positions do (#20).
1046
+ ...typeof t.measuredUsableInches === "number" && Number.isFinite(t.measuredUsableInches) ? { measuredUsableInches: sc(t.measuredUsableInches) } : {}
930
1047
  });
931
1048
  }
932
1049
  }
@@ -2364,6 +2481,7 @@ function poseOverridesFromDoc(doc) {
2364
2481
 
2365
2482
  exports.ATLAS_CODE55_N = ATLAS_CODE55_N;
2366
2483
  exports.BUILT_IN_TRACK_PARTS = BUILT_IN_TRACK_PARTS;
2484
+ exports.CLEARANCE_SPACING_INCHES = CLEARANCE_SPACING_INCHES;
2367
2485
  exports.CODE55_RAIL_HEIGHT_INCHES = CODE55_RAIL_HEIGHT_INCHES;
2368
2486
  exports.DEFAULT_FLEX_PART_ID = DEFAULT_FLEX_PART_ID;
2369
2487
  exports.ENDPLATE_FASCIA_CLEAR_INCHES = ENDPLATE_FASCIA_CLEAR_INCHES;
@@ -2382,6 +2500,8 @@ exports.TIE_HALF_LENGTH_INCHES = TIE_HALF_LENGTH_INCHES;
2382
2500
  exports.TURNOUT_LEAD_INCHES_PER_FROG = TURNOUT_LEAD_INCHES_PER_FROG;
2383
2501
  exports.WHOLE_MODULE_SECTION_ID = WHOLE_MODULE_SECTION_ID;
2384
2502
  exports.asModuleSchematic = asModuleSchematic;
2503
+ exports.assessSectionEnd = assessSectionEnd;
2504
+ exports.assessSectionJoint = assessSectionJoint;
2385
2505
  exports.benchworkBand = benchworkBand;
2386
2506
  exports.benchworkOutline = benchworkOutline;
2387
2507
  exports.buildCrossover = buildCrossover;
@@ -2389,6 +2509,7 @@ exports.buildPassingSiding = buildPassingSiding;
2389
2509
  exports.buildTransition = buildTransition;
2390
2510
  exports.carCapacity = carCapacity;
2391
2511
  exports.checkEndplateWidth = checkEndplateWidth;
2512
+ exports.clearancePointPastFrogInches = clearancePointPastFrogInches;
2392
2513
  exports.deriveEndplatePoses = deriveEndplatePoses;
2393
2514
  exports.divergeSideForHand = divergeSideForHand;
2394
2515
  exports.docToState = docToState;
@@ -2458,5 +2579,6 @@ exports.turnoutClosure = turnoutClosure;
2458
2579
  exports.turnoutFacing = turnoutFacing;
2459
2580
  exports.turnoutOccupiedSpan = turnoutOccupiedSpan;
2460
2581
  exports.turnoutPartForSize = turnoutPartForSize;
2582
+ exports.usableCapacity = usableCapacity;
2461
2583
  //# sourceMappingURL=index.cjs.map
2462
2584
  //# sourceMappingURL=index.cjs.map