@willcgage/module-schematic 0.95.1 → 0.97.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
@@ -1703,11 +1703,22 @@ var FAST_TRACKS_N_ME55_CROSSOVERS = [
1703
1703
  };
1704
1704
  });
1705
1705
  var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1706
+ var GENERIC_END_OF_TRACK = [
1707
+ {
1708
+ id: "generic-bumper",
1709
+ manufacturer: "Generic",
1710
+ line: "N scale",
1711
+ scale: "N",
1712
+ name: "Bumper",
1713
+ kind: "bumper"
1714
+ }
1715
+ ];
1706
1716
  var BUILT_IN_TRACK_PARTS = [
1707
1717
  ...ATLAS_CODE55_N,
1708
1718
  ...FAST_TRACKS_N_ME55,
1709
1719
  ...FAST_TRACKS_N_ME55_CROSSOVERS,
1710
- ...FLEX_TRACK_PARTS
1720
+ ...FLEX_TRACK_PARTS,
1721
+ ...GENERIC_END_OF_TRACK
1711
1722
  ];
1712
1723
  function flexParts(library = BUILT_IN_TRACK_PARTS) {
1713
1724
  return library.filter((p) => p.kind === "flex");
@@ -2008,14 +2019,14 @@ function storedPartToTrackPart(row) {
2008
2019
  source: points.source === "measured" && frog.source === "measured" ? "measured" : "derived",
2009
2020
  ...note ? { note } : {}
2010
2021
  } : dim(row.leadInches, row.leadSource);
2011
- const kind = row.kind;
2022
+ const kind = TRACK_PART_KINDS.includes(row.kind) ? row.kind : "turnout";
2012
2023
  const part = {
2013
2024
  id: row.slug,
2014
2025
  manufacturer: row.manufacturer,
2015
2026
  line: row.line,
2016
2027
  scale: "N",
2017
2028
  name: row.name,
2018
- kind: kind === "wye" || kind === "curved-turnout" || kind === "crossing" ? kind : "turnout"
2029
+ kind
2019
2030
  };
2020
2031
  const numbers = {
2021
2032
  ...row.partNumberLeft ? { left: row.partNumberLeft } : {},
@@ -2050,6 +2061,9 @@ function storedPartToTrackPart(row) {
2050
2061
  const inner = dim(row.innerRadiusInches, row.radiusSource);
2051
2062
  if (outer) part.outerRadius = outer;
2052
2063
  if (inner) part.innerRadius = inner;
2064
+ const radius = dim(row.radiusInches, row.radiusSource);
2065
+ if (radius) part.radius = radius;
2066
+ if (typeof row.arcDegrees === "number") part.arcDegrees = row.arcDegrees;
2053
2067
  if (typeof row.actualAngleDeg === "number")
2054
2068
  part.actualAngle = {
2055
2069
  deg: row.actualAngleDeg,
@@ -2162,6 +2176,14 @@ function turnoutClosure(size, opts = {}) {
2162
2176
  }
2163
2177
  function partGeometryGap(part) {
2164
2178
  if (part.kind === "flex") return null;
2179
+ if (part.kind === "bumper") return null;
2180
+ if (part.kind === "straight")
2181
+ return part.overallLength ? null : "no overall length \u2014 a sectional straight IS its length";
2182
+ if (part.kind === "curve") {
2183
+ if (!part.radius) return "no radius \u2014 a sectional curve is a radius and an arc";
2184
+ if (part.arcDegrees == null) return "no arc \u2014 the radius alone does not say how far it turns";
2185
+ return null;
2186
+ }
2165
2187
  if (part.kind === "crossover")
2166
2188
  return "a crossover fixture builds one HALF of the assembly (piecesPerAssembly), so its published lengths describe a piece, not the finished part \u2014 the geometry of the whole crossover is not yet derivable from them";
2167
2189
  if (part.kind === "crossing") return "crossing geometry is not modelled yet";
@@ -2175,6 +2197,26 @@ function partGeometryGap(part) {
2175
2197
  }
2176
2198
  function partGeometry(part, library = BUILT_IN_TRACK_PARTS) {
2177
2199
  if (partGeometryGap(part)) return null;
2200
+ if (part.kind === "straight" || part.kind === "curve") {
2201
+ const end = part.kind === "curve" ? flexRunEnd(sectionalArcInches(part), part.radius.inches) : { x: part.overallLength.inches, y: 0, headingDeg: 0 };
2202
+ return {
2203
+ joints: [
2204
+ { id: "a", role: "throat", x: 0, y: 0, angleDeg: 180 },
2205
+ { id: "b", role: "through", x: end.x, y: end.y, angleDeg: end.headingDeg }
2206
+ ],
2207
+ routes: [["a", "b"]],
2208
+ source: (part.kind === "curve" ? part.radius : part.overallLength)?.source ?? "unverified",
2209
+ divergingEndMeasured: false
2210
+ };
2211
+ }
2212
+ if (part.kind === "bumper") {
2213
+ return {
2214
+ joints: [{ id: "a", role: "throat", x: 0, y: 0, angleDeg: 180 }],
2215
+ routes: [],
2216
+ source: part.overallLength?.source ?? "unverified",
2217
+ divergingEndMeasured: false
2218
+ };
2219
+ }
2178
2220
  if (part.kind === "flex") {
2179
2221
  return {
2180
2222
  joints: [
@@ -2265,6 +2307,12 @@ function pieceRoutePaths(piece, library = BUILT_IN_TRACK_PARTS) {
2265
2307
  return { x: piece.x + x * c - ly * s, y: piece.y + x * s + ly * c };
2266
2308
  };
2267
2309
  const out = [];
2310
+ if (part.kind === "bumper") {
2311
+ const at2 = joints[0];
2312
+ if (!at2) return out;
2313
+ const len = part.overallLength?.inches ?? BUMPER_DRAWN_INCHES;
2314
+ return [{ route: ["a", "a"], points: [{ x: at2.x, y: at2.y }, place(len, 0)] }];
2315
+ }
2268
2316
  for (const route of geo.routes) {
2269
2317
  const a = at(route[0]);
2270
2318
  const b = at(route[1]);
@@ -2328,6 +2376,22 @@ function partsPlaceable(library = BUILT_IN_TRACK_PARTS) {
2328
2376
  return { placeable, blocked };
2329
2377
  }
2330
2378
  var JOINT_SNAP_INCHES = 0.01;
2379
+ var TRACK_PART_KINDS = [
2380
+ "turnout",
2381
+ "wye",
2382
+ "curved-turnout",
2383
+ "crossover",
2384
+ "crossing",
2385
+ "flex",
2386
+ "straight",
2387
+ "curve",
2388
+ "bumper"
2389
+ ];
2390
+ var BUMPER_DRAWN_INCHES = 0.75;
2391
+ function sectionalArcInches(part) {
2392
+ if (part.kind !== "curve" || !part.radius || part.arcDegrees == null) return 0;
2393
+ return part.radius.inches * Math.abs(part.arcDegrees) * Math.PI / 180;
2394
+ }
2331
2395
  function flexRunEnd(lengthInches, radiusInches) {
2332
2396
  const L = lengthInches;
2333
2397
  const R = radiusInches;
@@ -2503,6 +2567,7 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2503
2567
  const piece = pieceById.get(a.piece);
2504
2568
  const part = piece ? partOf(a.piece) : void 0;
2505
2569
  if (piece && part?.kind === "flex") return piece.lengthInches ?? 0;
2570
+ if (part?.kind === "curve") return sectionalArcInches(part);
2506
2571
  }
2507
2572
  return Math.hypot(a.x - b.x, a.y - b.y);
2508
2573
  };
@@ -2519,6 +2584,7 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2519
2584
  toPos: startPos,
2520
2585
  bornAt,
2521
2586
  endsAt: null,
2587
+ closedBy: null,
2522
2588
  pieces: [],
2523
2589
  spans: [],
2524
2590
  lateral: 0
@@ -2533,7 +2599,11 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2533
2599
  const geo = geoOf(here.piece);
2534
2600
  if (!geo) break;
2535
2601
  const opts = geo.routes.filter((r) => r.includes(here.joint));
2536
- if (!opts.length) break;
2602
+ if (!opts.length) {
2603
+ route.pieces.push(here.piece);
2604
+ route.closedBy = here.piece;
2605
+ break;
2606
+ }
2537
2607
  const pick = opts.find((r) => r.includes("through")) ?? opts[0];
2538
2608
  const exitJoint = pick[0] === here.joint ? pick[1] : pick[0];
2539
2609
  const exit = byKey.get(`${here.piece}.${exitJoint}`);
@@ -2691,7 +2761,10 @@ function graphToDoc(pieces, input) {
2691
2761
  role: "main",
2692
2762
  lane: 0,
2693
2763
  from: epA?.id ?? "A",
2694
- ...epB ? { to: epB.id } : {}
2764
+ ...epB ? { to: epB.id } : {},
2765
+ // A main can be closed too — that is a pocket module, where the track
2766
+ // runs in and stops rather than crossing to a second endplate.
2767
+ ...main.closedBy ? { bumperAt: "to" } : {}
2695
2768
  },
2696
2769
  ...twoMains ? [
2697
2770
  {
@@ -2718,6 +2791,11 @@ function graphToDoc(pieces, input) {
2718
2791
  lane: laneOf(r, uniqueBranches),
2719
2792
  fromPos: round(Math.min(r.fromPos, r.toPos)),
2720
2793
  toPos: round(Math.max(r.fromPos, r.toPos)),
2794
+ // ⚠️ ALWAYS THE `to` END, and not because of an assumption: a branch's
2795
+ // positions are ARC LENGTH FROM ITS THROAT, so they only ever grow — a
2796
+ // spur running physically back toward endplate A still ends at the larger
2797
+ // number. The closed end is the far one from the turnout, by definition.
2798
+ ...r.closedBy ? { bumperAt: "to" } : {},
2721
2799
  ...meta.trackName ? { trackName: meta.trackName } : {},
2722
2800
  ...meta.capacityFeet != null ? { capacityFeet: meta.capacityFeet } : {},
2723
2801
  ...meta.moduleTrackId != null ? { moduleTrackId: meta.moduleTrackId } : {}
@@ -3477,6 +3555,7 @@ function poseOverridesFromDoc(doc) {
3477
3555
 
3478
3556
  exports.ATLAS_CODE55_N = ATLAS_CODE55_N;
3479
3557
  exports.BUILT_IN_TRACK_PARTS = BUILT_IN_TRACK_PARTS;
3558
+ exports.BUMPER_DRAWN_INCHES = BUMPER_DRAWN_INCHES;
3480
3559
  exports.CLEARANCE_SPACING_INCHES = CLEARANCE_SPACING_INCHES;
3481
3560
  exports.CODE55_RAIL_HEIGHT_INCHES = CODE55_RAIL_HEIGHT_INCHES;
3482
3561
  exports.DEFAULT_FLEX_PART_ID = DEFAULT_FLEX_PART_ID;
@@ -3491,6 +3570,7 @@ exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMME
3491
3570
  exports.FREEMO_MAIN_MIN_RADIUS_INCHES = FREEMO_MAIN_MIN_RADIUS_INCHES;
3492
3571
  exports.FREEMO_REVERSE_CURVE_STRAIGHT_INCHES = FREEMO_REVERSE_CURVE_STRAIGHT_INCHES;
3493
3572
  exports.FREEMO_TRACK_SPACING_INCHES = FREEMO_TRACK_SPACING_INCHES;
3573
+ exports.GENERIC_END_OF_TRACK = GENERIC_END_OF_TRACK;
3494
3574
  exports.JOINT_SNAP_INCHES = JOINT_SNAP_INCHES;
3495
3575
  exports.MAIN2_TRACK_ID = MAIN2_TRACK_ID;
3496
3576
  exports.MAIN_TRACK_ID = MAIN_TRACK_ID;
@@ -3499,6 +3579,7 @@ exports.N_SCALE_RATIO = N_SCALE_RATIO;
3499
3579
  exports.PINCH_EASE_INCHES = PINCH_EASE_INCHES;
3500
3580
  exports.RAIL_GAUGE_INCHES = RAIL_GAUGE_INCHES;
3501
3581
  exports.TIE_HALF_LENGTH_INCHES = TIE_HALF_LENGTH_INCHES;
3582
+ exports.TRACK_PART_KINDS = TRACK_PART_KINDS;
3502
3583
  exports.TURNOUT_LEAD_INCHES_PER_FROG = TURNOUT_LEAD_INCHES_PER_FROG;
3503
3584
  exports.WHOLE_MODULE_SECTION_ID = WHOLE_MODULE_SECTION_ID;
3504
3585
  exports.asModuleSchematic = asModuleSchematic;
@@ -3583,6 +3664,7 @@ exports.sectionFootprints = sectionFootprints;
3583
3664
  exports.sectionNeighbours = sectionNeighbours;
3584
3665
  exports.sectionSpans = sectionSpans;
3585
3666
  exports.sectionSpansOrWhole = sectionSpansOrWhole;
3667
+ exports.sectionalArcInches = sectionalArcInches;
3586
3668
  exports.sectionedCenterline = sectionedCenterline;
3587
3669
  exports.sectionedEndPose = sectionedEndPose;
3588
3670
  exports.sliceCenterline = sliceCenterline;