@willcgage/module-schematic 0.92.1 → 0.94.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 +112 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +112 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2442,7 +2442,35 @@ function snapPiece(moving, others, library = BUILT_IN_TRACK_PARTS, withinInches
|
|
|
2442
2442
|
to: best.t.key
|
|
2443
2443
|
};
|
|
2444
2444
|
}
|
|
2445
|
+
function fitFlexBetween(piece, others, library = BUILT_IN_TRACK_PARTS, withinInches = 0.5) {
|
|
2446
|
+
const part = library.find((p) => p.id === piece.partId);
|
|
2447
|
+
if (part?.kind !== "flex" || piece.radiusInches) return null;
|
|
2448
|
+
const mine = placedJoints([piece], library);
|
|
2449
|
+
const far = mine.find((j) => j.joint === "b");
|
|
2450
|
+
const near = mine.find((j) => j.joint === "a");
|
|
2451
|
+
if (!far || !near) return null;
|
|
2452
|
+
const graph = buildTrackGraph(others, library);
|
|
2453
|
+
const taken = new Set(graph.connections.flatMap((c) => [c.a, c.b]));
|
|
2454
|
+
let best = null;
|
|
2455
|
+
for (const j of graph.joints) {
|
|
2456
|
+
if (taken.has(j.key)) continue;
|
|
2457
|
+
if (Math.hypot(j.x - near.x, j.y - near.y) <= JOINT_SNAP_INCHES) continue;
|
|
2458
|
+
const d = Math.hypot(j.x - far.x, j.y - far.y);
|
|
2459
|
+
if (d <= withinInches && (!best || d < best.d)) best = { j, d };
|
|
2460
|
+
}
|
|
2461
|
+
if (!best) return null;
|
|
2462
|
+
const dx = best.j.x - piece.x;
|
|
2463
|
+
const dy = best.j.y - piece.y;
|
|
2464
|
+
const len = Math.hypot(dx, dy);
|
|
2465
|
+
if (!(len > 0)) return null;
|
|
2466
|
+
return {
|
|
2467
|
+
...piece,
|
|
2468
|
+
rotationDeg: norm360(Math.atan2(dy, dx) * 180 / Math.PI),
|
|
2469
|
+
lengthInches: Math.round(len * 1e3) / 1e3
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2445
2472
|
var danglingDivergeWarning = (id) => `the route diverging at ${id} is not connected to anything`;
|
|
2473
|
+
var unreachedWarning = (id) => `${id} is not reachable from the endplate \u2014 nothing connects it`;
|
|
2446
2474
|
function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS) {
|
|
2447
2475
|
const byKey = new Map(graph.joints.map((j) => [j.key, j]));
|
|
2448
2476
|
const pieceById = new Map(pieces.map((p) => [p.id, p]));
|
|
@@ -2580,11 +2608,14 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
|
|
|
2580
2608
|
if (sw && !sw.divergeRoute) sw.divergeRoute = r.id;
|
|
2581
2609
|
}
|
|
2582
2610
|
const reached = new Set(routes.flatMap((r) => r.pieces));
|
|
2611
|
+
const unreached = [];
|
|
2583
2612
|
for (const p of pieces)
|
|
2584
|
-
if (!reached.has(p.id) && !graph.unplaceable.some((u) => u.piece === p.id))
|
|
2585
|
-
warnings.push(
|
|
2613
|
+
if (!reached.has(p.id) && !graph.unplaceable.some((u) => u.piece === p.id)) {
|
|
2614
|
+
warnings.push(unreachedWarning(p.id));
|
|
2615
|
+
unreached.push(p.id);
|
|
2616
|
+
}
|
|
2586
2617
|
for (const c of graph.conflicts) warnings.push(c.reason);
|
|
2587
|
-
return { routes, turnouts, warnings, danglingDiverges };
|
|
2618
|
+
return { routes, turnouts, warnings, danglingDiverges, unreached };
|
|
2588
2619
|
}
|
|
2589
2620
|
function resolveGraphAnchor(anchor, walk, pieces, library = BUILT_IN_TRACK_PARTS) {
|
|
2590
2621
|
const piece = pieces.find((p) => p.id === anchor.piece);
|
|
@@ -2613,14 +2644,36 @@ function graphToDoc(pieces, input) {
|
|
|
2613
2644
|
const epA = endplates[0];
|
|
2614
2645
|
const epB = endplates[1];
|
|
2615
2646
|
const main = walk.routes.find((r) => r.id === "main");
|
|
2616
|
-
const
|
|
2617
|
-
const
|
|
2647
|
+
const walk2 = input.start2 ? walkTrackGraph(graph, pieces, input.start2, library) : null;
|
|
2648
|
+
const main2 = walk2?.routes.find((r) => r.id === "main") ?? null;
|
|
2649
|
+
const claimed = new Set(walk.routes.flatMap((r) => r.pieces));
|
|
2650
|
+
if (main2 && main2.pieces.some((p) => claimed.has(p))) {
|
|
2651
|
+
warnings.push(
|
|
2652
|
+
"Main 2 starts on track that Main 1 already runs along \u2014 they are one run, not two"
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
const twoMains = !!main2 && !main2.pieces.some((p) => claimed.has(p));
|
|
2656
|
+
const main2Lane = twoMains ? (graph.joints.find((j) => j.key === `${input.start2.piece}.${input.start2.joint}`)?.y ?? 1) >= 0 ? 1 : -1 : 0;
|
|
2657
|
+
const lengthInches = round(Math.max(main.toPos, twoMains ? main2.toPos : 0));
|
|
2658
|
+
const branches = [
|
|
2659
|
+
...walk.routes.filter((r) => r.id !== "main" && r.pieces.length),
|
|
2660
|
+
...twoMains ? walk2.routes.filter((r) => r.id !== "main" && r.pieces.length).map((r) => ({ ...r, id: `main2:${r.id}`, bornAt: r.bornAt })) : []
|
|
2661
|
+
];
|
|
2618
2662
|
const trackIdOf = /* @__PURE__ */ new Map([["main", MAIN_TRACK_ID]]);
|
|
2663
|
+
if (twoMains) trackIdOf.set("main2:main", MAIN2_TRACK_ID);
|
|
2619
2664
|
for (const r of branches) trackIdOf.set(r.id, r.pieces[0]);
|
|
2620
|
-
const
|
|
2665
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2666
|
+
const uniqueBranches = branches.filter((r) => {
|
|
2667
|
+
const id = trackIdOf.get(r.id);
|
|
2668
|
+
if (emitted.has(id)) return false;
|
|
2669
|
+
emitted.add(id);
|
|
2670
|
+
return true;
|
|
2671
|
+
});
|
|
2672
|
+
const laneOf = (r, among) => {
|
|
2621
2673
|
const side = Math.sign(r.lateral) || 1;
|
|
2622
|
-
const sameSide =
|
|
2623
|
-
|
|
2674
|
+
const sameSide = among.filter((x) => (Math.sign(x.lateral) || 1) === side).sort((a, b) => Math.abs(a.lateral) - Math.abs(b.lateral));
|
|
2675
|
+
const taken = side === main2Lane ? 1 : 0;
|
|
2676
|
+
return side * (sameSide.indexOf(r) + 1 + taken);
|
|
2624
2677
|
};
|
|
2625
2678
|
const tracks = [
|
|
2626
2679
|
{
|
|
@@ -2629,9 +2682,22 @@ function graphToDoc(pieces, input) {
|
|
|
2629
2682
|
lane: 0,
|
|
2630
2683
|
from: epA?.id ?? "A",
|
|
2631
2684
|
...epB ? { to: epB.id } : {}
|
|
2632
|
-
}
|
|
2685
|
+
},
|
|
2686
|
+
...twoMains ? [
|
|
2687
|
+
{
|
|
2688
|
+
id: MAIN2_TRACK_ID,
|
|
2689
|
+
role: "main",
|
|
2690
|
+
lane: main2Lane,
|
|
2691
|
+
from: epA?.id ?? "A",
|
|
2692
|
+
...epB ? { to: epB.id } : {},
|
|
2693
|
+
// ⚠️ Its own extent, because a second main need not run the whole
|
|
2694
|
+
// module: on a transition module it starts or stops partway.
|
|
2695
|
+
fromPos: round(Math.min(main2.fromPos, main2.toPos)),
|
|
2696
|
+
toPos: round(Math.max(main2.fromPos, main2.toPos))
|
|
2697
|
+
}
|
|
2698
|
+
] : []
|
|
2633
2699
|
];
|
|
2634
|
-
for (const r of
|
|
2700
|
+
for (const r of uniqueBranches) {
|
|
2635
2701
|
const id = trackIdOf.get(r.id);
|
|
2636
2702
|
const meta = input.meta?.[id] ?? {};
|
|
2637
2703
|
tracks.push({
|
|
@@ -2639,7 +2705,7 @@ function graphToDoc(pieces, input) {
|
|
|
2639
2705
|
// It runs back into a second turnout, or it doesn't. That is the whole
|
|
2640
2706
|
// difference between a siding and a spur, and it is read, not declared.
|
|
2641
2707
|
role: r.endsAt ? "siding" : "spur",
|
|
2642
|
-
lane: laneOf(r),
|
|
2708
|
+
lane: laneOf(r, uniqueBranches),
|
|
2643
2709
|
fromPos: round(Math.min(r.fromPos, r.toPos)),
|
|
2644
2710
|
toPos: round(Math.max(r.fromPos, r.toPos)),
|
|
2645
2711
|
...meta.trackName ? { trackName: meta.trackName } : {},
|
|
@@ -2647,9 +2713,29 @@ function graphToDoc(pieces, input) {
|
|
|
2647
2713
|
...meta.moduleTrackId != null ? { moduleTrackId: meta.moduleTrackId } : {}
|
|
2648
2714
|
});
|
|
2649
2715
|
}
|
|
2716
|
+
if (walk2) {
|
|
2717
|
+
const seen = /* @__PURE__ */ new Set([
|
|
2718
|
+
...walk.routes.flatMap((r) => r.pieces),
|
|
2719
|
+
...walk2.routes.flatMap((r) => r.pieces)
|
|
2720
|
+
]);
|
|
2721
|
+
const covered = new Set([...walk.unreached, ...walk2.unreached].filter((id) => seen.has(id)));
|
|
2722
|
+
const drop = new Set([...covered].map(unreachedWarning));
|
|
2723
|
+
const d2 = new Set(walk2.danglingDiverges.map(danglingDivergeWarning));
|
|
2724
|
+
for (let i = warnings.length - 1; i >= 0; i--) if (drop.has(warnings[i])) warnings.splice(i, 1);
|
|
2725
|
+
for (const w of walk2.warnings)
|
|
2726
|
+
if (!d2.has(w) && !drop.has(w) && !warnings.includes(w)) warnings.push(w);
|
|
2727
|
+
}
|
|
2650
2728
|
const pieceById = new Map(pieces.map((p) => [p.id, p]));
|
|
2651
2729
|
const turnouts = [];
|
|
2652
|
-
|
|
2730
|
+
const allTurnouts = [
|
|
2731
|
+
...walk.turnouts,
|
|
2732
|
+
...twoMains ? walk2.turnouts.map((t) => ({
|
|
2733
|
+
...t,
|
|
2734
|
+
onRoute: t.onRoute === "main" ? "main2:main" : `main2:${t.onRoute}`,
|
|
2735
|
+
divergeRoute: t.divergeRoute ? `main2:${t.divergeRoute}` : null
|
|
2736
|
+
})) : []
|
|
2737
|
+
];
|
|
2738
|
+
for (const t of allTurnouts) {
|
|
2653
2739
|
const diverge = t.divergeRoute ? trackIdOf.get(t.divergeRoute) : void 0;
|
|
2654
2740
|
if (!diverge) {
|
|
2655
2741
|
warnings.push(
|
|
@@ -2670,6 +2756,12 @@ function graphToDoc(pieces, input) {
|
|
|
2670
2756
|
});
|
|
2671
2757
|
}
|
|
2672
2758
|
turnouts.sort((a, b) => a.pos - b.pos);
|
|
2759
|
+
for (const t of tracks) {
|
|
2760
|
+
if (t.role === "main") continue;
|
|
2761
|
+
const ends = turnouts.filter((sw) => sw.divergeTrack === t.id);
|
|
2762
|
+
if (ends.length >= 2 && new Set(ends.map((sw) => sw.onTrack)).size >= 2)
|
|
2763
|
+
t.role = "crossover";
|
|
2764
|
+
}
|
|
2673
2765
|
const place = (a, what) => {
|
|
2674
2766
|
if (!a) return null;
|
|
2675
2767
|
const hit = resolveGraphAnchor(a, walk, pieces, library);
|
|
@@ -2731,7 +2823,13 @@ function deriveGraphDoc(doc, library = BUILT_IN_TRACK_PARTS) {
|
|
|
2731
2823
|
...t.moduleTrackId != null ? { moduleTrackId: t.moduleTrackId } : {}
|
|
2732
2824
|
};
|
|
2733
2825
|
}
|
|
2734
|
-
const out = graphToDoc(g.pieces, {
|
|
2826
|
+
const out = graphToDoc(g.pieces, {
|
|
2827
|
+
startAt: g.startAt,
|
|
2828
|
+
start2: g.start2 ?? null,
|
|
2829
|
+
base: doc,
|
|
2830
|
+
meta,
|
|
2831
|
+
library
|
|
2832
|
+
});
|
|
2735
2833
|
return { doc: out.doc, warnings: out.warnings };
|
|
2736
2834
|
}
|
|
2737
2835
|
function frogCasting(cl, opts = {}) {
|
|
@@ -3417,6 +3515,7 @@ exports.endplateFaceSegments = endplateFaceSegments;
|
|
|
3417
3515
|
exports.endplateLead = endplateLead;
|
|
3418
3516
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|
|
3419
3517
|
exports.endplateWidthInches = endplateWidthInches;
|
|
3518
|
+
exports.fitFlexBetween = fitFlexBetween;
|
|
3420
3519
|
exports.flexPartFor = flexPartFor;
|
|
3421
3520
|
exports.flexParts = flexParts;
|
|
3422
3521
|
exports.flexPieces = flexPieces;
|