@willcgage/module-schematic 0.92.1 → 0.93.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.d.cts CHANGED
@@ -459,6 +459,16 @@ interface ModuleSchematicDoc {
459
459
  piece: string;
460
460
  joint: string;
461
461
  };
462
+ /** Where MAIN 2 starts, on a double-track module — the joint the endplate's
463
+ * SECOND track arrives at. Absent = single track.
464
+ *
465
+ * Two starts rather than a list because a document has exactly two mains,
466
+ * `main` and `main2`. This mirrors the thing downstream instead of inventing
467
+ * a generality nothing can read. */
468
+ start2?: {
469
+ piece: string;
470
+ joint: string;
471
+ } | null;
462
472
  } | null;
463
473
  }
464
474
  /** A benchwork-outline vertex, module-local inches. The edge from this vertex
@@ -2644,6 +2654,11 @@ interface GraphWalk {
2644
2654
  * sentence, so a caller can act on it (and say it ONCE) instead of matching
2645
2655
  * on prose. */
2646
2656
  danglingDiverges: string[];
2657
+ /** Pieces this walk never got to. ⚠️ "Unreached BY THIS WALK" is not the same
2658
+ * as "unreachable": a double-track module's second main is unreached by the
2659
+ * first walk and perfectly well connected. Ids, so a caller running more than
2660
+ * one walk can tell the difference. */
2661
+ unreached: string[];
2647
2662
  }
2648
2663
  /**
2649
2664
  * Walk the graph from an endplate and read the topology off it.
@@ -2686,6 +2701,11 @@ interface GraphDocInput {
2686
2701
  piece: string;
2687
2702
  joint: string;
2688
2703
  };
2704
+ /** Where MAIN 2 begins, on a double-track module. Absent = single track. */
2705
+ start2?: {
2706
+ piece: string;
2707
+ joint: string;
2708
+ } | null;
2689
2709
  /**
2690
2710
  * The rest of the document — module id, endplate identities, the benchwork,
2691
2711
  * industries and signals. Merged UNDERNEATH the derived keys, so the graph
package/dist/index.d.ts CHANGED
@@ -459,6 +459,16 @@ interface ModuleSchematicDoc {
459
459
  piece: string;
460
460
  joint: string;
461
461
  };
462
+ /** Where MAIN 2 starts, on a double-track module — the joint the endplate's
463
+ * SECOND track arrives at. Absent = single track.
464
+ *
465
+ * Two starts rather than a list because a document has exactly two mains,
466
+ * `main` and `main2`. This mirrors the thing downstream instead of inventing
467
+ * a generality nothing can read. */
468
+ start2?: {
469
+ piece: string;
470
+ joint: string;
471
+ } | null;
462
472
  } | null;
463
473
  }
464
474
  /** A benchwork-outline vertex, module-local inches. The edge from this vertex
@@ -2644,6 +2654,11 @@ interface GraphWalk {
2644
2654
  * sentence, so a caller can act on it (and say it ONCE) instead of matching
2645
2655
  * on prose. */
2646
2656
  danglingDiverges: string[];
2657
+ /** Pieces this walk never got to. ⚠️ "Unreached BY THIS WALK" is not the same
2658
+ * as "unreachable": a double-track module's second main is unreached by the
2659
+ * first walk and perfectly well connected. Ids, so a caller running more than
2660
+ * one walk can tell the difference. */
2661
+ unreached: string[];
2647
2662
  }
2648
2663
  /**
2649
2664
  * Walk the graph from an endplate and read the topology off it.
@@ -2686,6 +2701,11 @@ interface GraphDocInput {
2686
2701
  piece: string;
2687
2702
  joint: string;
2688
2703
  };
2704
+ /** Where MAIN 2 begins, on a double-track module. Absent = single track. */
2705
+ start2?: {
2706
+ piece: string;
2707
+ joint: string;
2708
+ } | null;
2689
2709
  /**
2690
2710
  * The rest of the document — module id, endplate identities, the benchwork,
2691
2711
  * industries and signals. Merged UNDERNEATH the derived keys, so the graph
package/dist/index.js CHANGED
@@ -2441,6 +2441,7 @@ function snapPiece(moving, others, library = BUILT_IN_TRACK_PARTS, withinInches
2441
2441
  };
2442
2442
  }
2443
2443
  var danglingDivergeWarning = (id) => `the route diverging at ${id} is not connected to anything`;
2444
+ var unreachedWarning = (id) => `${id} is not reachable from the endplate \u2014 nothing connects it`;
2444
2445
  function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS) {
2445
2446
  const byKey = new Map(graph.joints.map((j) => [j.key, j]));
2446
2447
  const pieceById = new Map(pieces.map((p) => [p.id, p]));
@@ -2578,11 +2579,14 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2578
2579
  if (sw && !sw.divergeRoute) sw.divergeRoute = r.id;
2579
2580
  }
2580
2581
  const reached = new Set(routes.flatMap((r) => r.pieces));
2582
+ const unreached = [];
2581
2583
  for (const p of pieces)
2582
- if (!reached.has(p.id) && !graph.unplaceable.some((u) => u.piece === p.id))
2583
- warnings.push(`${p.id} is not reachable from the endplate \u2014 nothing connects it`);
2584
+ if (!reached.has(p.id) && !graph.unplaceable.some((u) => u.piece === p.id)) {
2585
+ warnings.push(unreachedWarning(p.id));
2586
+ unreached.push(p.id);
2587
+ }
2584
2588
  for (const c of graph.conflicts) warnings.push(c.reason);
2585
- return { routes, turnouts, warnings, danglingDiverges };
2589
+ return { routes, turnouts, warnings, danglingDiverges, unreached };
2586
2590
  }
2587
2591
  function resolveGraphAnchor(anchor, walk, pieces, library = BUILT_IN_TRACK_PARTS) {
2588
2592
  const piece = pieces.find((p) => p.id === anchor.piece);
@@ -2611,14 +2615,29 @@ function graphToDoc(pieces, input) {
2611
2615
  const epA = endplates[0];
2612
2616
  const epB = endplates[1];
2613
2617
  const main = walk.routes.find((r) => r.id === "main");
2614
- const lengthInches = round(main.toPos);
2615
- const branches = walk.routes.filter((r) => r.id !== "main" && r.pieces.length);
2618
+ const walk2 = input.start2 ? walkTrackGraph(graph, pieces, input.start2, library) : null;
2619
+ const main2 = walk2?.routes.find((r) => r.id === "main") ?? null;
2620
+ const claimed = new Set(walk.routes.flatMap((r) => r.pieces));
2621
+ if (main2 && main2.pieces.some((p) => claimed.has(p))) {
2622
+ warnings.push(
2623
+ "Main 2 starts on track that Main 1 already runs along \u2014 they are one run, not two"
2624
+ );
2625
+ }
2626
+ const twoMains = !!main2 && !main2.pieces.some((p) => claimed.has(p));
2627
+ const main2Lane = twoMains ? (graph.joints.find((j) => j.key === `${input.start2.piece}.${input.start2.joint}`)?.y ?? 1) >= 0 ? 1 : -1 : 0;
2628
+ const lengthInches = round(Math.max(main.toPos, twoMains ? main2.toPos : 0));
2629
+ const branches = [
2630
+ ...walk.routes.filter((r) => r.id !== "main" && r.pieces.length),
2631
+ ...twoMains ? walk2.routes.filter((r) => r.id !== "main" && r.pieces.length).map((r) => ({ ...r, id: `main2:${r.id}`, bornAt: r.bornAt })) : []
2632
+ ];
2616
2633
  const trackIdOf = /* @__PURE__ */ new Map([["main", MAIN_TRACK_ID]]);
2634
+ if (twoMains) trackIdOf.set("main2:main", MAIN2_TRACK_ID);
2617
2635
  for (const r of branches) trackIdOf.set(r.id, r.pieces[0]);
2618
2636
  const laneOf = (r) => {
2619
2637
  const side = Math.sign(r.lateral) || 1;
2620
2638
  const sameSide = branches.filter((x) => (Math.sign(x.lateral) || 1) === side).sort((a, b) => Math.abs(a.lateral) - Math.abs(b.lateral));
2621
- return side * (sameSide.indexOf(r) + 1);
2639
+ const taken = side === main2Lane ? 1 : 0;
2640
+ return side * (sameSide.indexOf(r) + 1 + taken);
2622
2641
  };
2623
2642
  const tracks = [
2624
2643
  {
@@ -2627,7 +2646,20 @@ function graphToDoc(pieces, input) {
2627
2646
  lane: 0,
2628
2647
  from: epA?.id ?? "A",
2629
2648
  ...epB ? { to: epB.id } : {}
2630
- }
2649
+ },
2650
+ ...twoMains ? [
2651
+ {
2652
+ id: MAIN2_TRACK_ID,
2653
+ role: "main",
2654
+ lane: main2Lane,
2655
+ from: epA?.id ?? "A",
2656
+ ...epB ? { to: epB.id } : {},
2657
+ // ⚠️ Its own extent, because a second main need not run the whole
2658
+ // module: on a transition module it starts or stops partway.
2659
+ fromPos: round(Math.min(main2.fromPos, main2.toPos)),
2660
+ toPos: round(Math.max(main2.fromPos, main2.toPos))
2661
+ }
2662
+ ] : []
2631
2663
  ];
2632
2664
  for (const r of branches) {
2633
2665
  const id = trackIdOf.get(r.id);
@@ -2645,9 +2677,29 @@ function graphToDoc(pieces, input) {
2645
2677
  ...meta.moduleTrackId != null ? { moduleTrackId: meta.moduleTrackId } : {}
2646
2678
  });
2647
2679
  }
2680
+ if (walk2) {
2681
+ const seen = /* @__PURE__ */ new Set([
2682
+ ...walk.routes.flatMap((r) => r.pieces),
2683
+ ...walk2.routes.flatMap((r) => r.pieces)
2684
+ ]);
2685
+ const covered = new Set([...walk.unreached, ...walk2.unreached].filter((id) => seen.has(id)));
2686
+ const drop = new Set([...covered].map(unreachedWarning));
2687
+ const d2 = new Set(walk2.danglingDiverges.map(danglingDivergeWarning));
2688
+ for (let i = warnings.length - 1; i >= 0; i--) if (drop.has(warnings[i])) warnings.splice(i, 1);
2689
+ for (const w of walk2.warnings)
2690
+ if (!d2.has(w) && !drop.has(w) && !warnings.includes(w)) warnings.push(w);
2691
+ }
2648
2692
  const pieceById = new Map(pieces.map((p) => [p.id, p]));
2649
2693
  const turnouts = [];
2650
- for (const t of walk.turnouts) {
2694
+ const allTurnouts = [
2695
+ ...walk.turnouts,
2696
+ ...twoMains ? walk2.turnouts.map((t) => ({
2697
+ ...t,
2698
+ onRoute: t.onRoute === "main" ? "main2:main" : `main2:${t.onRoute}`,
2699
+ divergeRoute: t.divergeRoute ? `main2:${t.divergeRoute}` : null
2700
+ })) : []
2701
+ ];
2702
+ for (const t of allTurnouts) {
2651
2703
  const diverge = t.divergeRoute ? trackIdOf.get(t.divergeRoute) : void 0;
2652
2704
  if (!diverge) {
2653
2705
  warnings.push(
@@ -2729,7 +2781,13 @@ function deriveGraphDoc(doc, library = BUILT_IN_TRACK_PARTS) {
2729
2781
  ...t.moduleTrackId != null ? { moduleTrackId: t.moduleTrackId } : {}
2730
2782
  };
2731
2783
  }
2732
- const out = graphToDoc(g.pieces, { startAt: g.startAt, base: doc, meta, library });
2784
+ const out = graphToDoc(g.pieces, {
2785
+ startAt: g.startAt,
2786
+ start2: g.start2 ?? null,
2787
+ base: doc,
2788
+ meta,
2789
+ library
2790
+ });
2733
2791
  return { doc: out.doc, warnings: out.warnings };
2734
2792
  }
2735
2793
  function frogCasting(cl, opts = {}) {