@willcgage/module-schematic 0.108.0 → 0.110.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 +144 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +144 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2927,27 +2927,49 @@ function graphToDoc(pieces, input) {
|
|
|
2927
2927
|
const epA = endplates[0];
|
|
2928
2928
|
const epB = endplates[1];
|
|
2929
2929
|
const main = walk.routes.find((r) => r.id === "main");
|
|
2930
|
-
const
|
|
2931
|
-
const
|
|
2930
|
+
const start2Key = input.start2 ? `${input.start2.piece}.${input.start2.joint}` : null;
|
|
2931
|
+
const start2IsOpenEnd = !!start2Key && graph.open.includes(start2Key);
|
|
2932
|
+
const walk2 = input.start2 && start2IsOpenEnd ? walkTrackGraph(graph, pieces, input.start2, library) : null;
|
|
2933
|
+
const main2FromBranch = (() => {
|
|
2934
|
+
if (!start2Key || start2IsOpenEnd) return null;
|
|
2935
|
+
const conn = graph.connections.find((c) => c.a === start2Key || c.b === start2Key);
|
|
2936
|
+
if (!conn) return null;
|
|
2937
|
+
const otherKey = conn.a === start2Key ? conn.b : conn.a;
|
|
2938
|
+
const piece = graph.joints.find((j) => j.key === otherKey)?.piece;
|
|
2939
|
+
return walk.routes.find((r) => r.id !== "main" && r.pieces[0] === piece) ?? null;
|
|
2940
|
+
})();
|
|
2941
|
+
const main2 = walk2?.routes.find((r) => r.id === "main") ?? main2FromBranch;
|
|
2932
2942
|
const sharedByDesign = (id) => {
|
|
2933
2943
|
const piece = pieces.find((p) => p.id === id);
|
|
2934
2944
|
return library.find((p) => p.id === piece?.partId)?.kind === "crossover";
|
|
2935
2945
|
};
|
|
2936
|
-
const
|
|
2937
|
-
if (main2 && main2.pieces.some((p) =>
|
|
2946
|
+
const mainPieces = new Set(main.pieces.filter((p) => !sharedByDesign(p)));
|
|
2947
|
+
if (!main2FromBranch && main2 && main2.pieces.some((p) => mainPieces.has(p))) {
|
|
2938
2948
|
warnings.push(
|
|
2939
2949
|
"Main 2 starts on track that Main 1 already runs along \u2014 they are one run, not two"
|
|
2940
2950
|
);
|
|
2941
2951
|
}
|
|
2942
|
-
const twoMains = !!main2 && !main2.pieces.some((p) =>
|
|
2943
|
-
const
|
|
2952
|
+
const twoMains = !!main2 && (!!main2FromBranch || !main2.pieces.some((p) => mainPieces.has(p)));
|
|
2953
|
+
const main2AsBranch = walk2 && main2 && twoMains ? walk.routes.find(
|
|
2954
|
+
(r) => r.id !== "main" && r.pieces.some((p) => main2.pieces.includes(p))
|
|
2955
|
+
) ?? null : null;
|
|
2956
|
+
const main2Lane = twoMains ? (
|
|
2957
|
+
// A branch-born main reaches its lane after an easement, so its SIDE is
|
|
2958
|
+
// how far the run gets laterally, not where its first joint sits.
|
|
2959
|
+
(main2FromBranch ? main2FromBranch.lateral : graph.joints.find((j) => j.key === start2Key)?.y ?? 1) >= 0 ? 1 : -1
|
|
2960
|
+
) : 0;
|
|
2944
2961
|
const lengthInches = round(Math.max(main.toPos, twoMains ? main2.toPos : 0));
|
|
2945
2962
|
const branches = [
|
|
2946
|
-
...walk.routes.filter(
|
|
2947
|
-
|
|
2963
|
+
...walk.routes.filter(
|
|
2964
|
+
(r) => r.id !== "main" && r.pieces.length && r.id !== main2FromBranch?.id && r.id !== main2AsBranch?.id
|
|
2965
|
+
),
|
|
2966
|
+
...walk2 ? walk2.routes.filter((r) => r.id !== "main" && r.pieces.length).map((r) => ({ ...r, id: `main2:${r.id}`, bornAt: r.bornAt })) : []
|
|
2948
2967
|
];
|
|
2949
2968
|
const trackIdOf = /* @__PURE__ */ new Map([["main", MAIN_TRACK_ID]]);
|
|
2950
|
-
if (twoMains)
|
|
2969
|
+
if (twoMains) {
|
|
2970
|
+
trackIdOf.set(main2FromBranch ? main2FromBranch.id : "main2:main", MAIN2_TRACK_ID);
|
|
2971
|
+
if (main2AsBranch) trackIdOf.set(main2AsBranch.id, MAIN2_TRACK_ID);
|
|
2972
|
+
}
|
|
2951
2973
|
for (const r of branches) trackIdOf.set(r.id, r.pieces[0]);
|
|
2952
2974
|
const emitted = /* @__PURE__ */ new Set();
|
|
2953
2975
|
const uniqueBranches = branches.filter((r) => {
|
|
@@ -2978,8 +3000,11 @@ function graphToDoc(pieces, input) {
|
|
|
2978
3000
|
id: MAIN2_TRACK_ID,
|
|
2979
3001
|
role: "main",
|
|
2980
3002
|
lane: main2Lane,
|
|
2981
|
-
|
|
2982
|
-
|
|
3003
|
+
// ⚠️ Only a main that CROSSES the endplates says so. A transition
|
|
3004
|
+
// module's second main begins at a turnout partway along and ends
|
|
3005
|
+
// at one endplate or neither; claiming A→B would tell the catalogue
|
|
3006
|
+
// it presents two tracks at an end where it presents one.
|
|
3007
|
+
...main2FromBranch ? {} : { from: epA?.id ?? "A", ...epB ? { to: epB.id } : {} },
|
|
2983
3008
|
// ⚠️ Its own extent, because a second main need not run the whole
|
|
2984
3009
|
// module: on a transition module it starts or stops partway.
|
|
2985
3010
|
fromPos: round(Math.min(main2.fromPos, main2.toPos)),
|
|
@@ -3024,7 +3049,7 @@ function graphToDoc(pieces, input) {
|
|
|
3024
3049
|
const turnouts = [];
|
|
3025
3050
|
const allTurnouts = [
|
|
3026
3051
|
...walk.turnouts,
|
|
3027
|
-
...
|
|
3052
|
+
...walk2 ? walk2.turnouts.map((t) => ({
|
|
3028
3053
|
...t,
|
|
3029
3054
|
onRoute: t.onRoute === "main" ? "main2:main" : `main2:${t.onRoute}`,
|
|
3030
3055
|
divergeRoute: t.divergeRoute ? `main2:${t.divergeRoute}` : null
|
|
@@ -3054,7 +3079,7 @@ function graphToDoc(pieces, input) {
|
|
|
3054
3079
|
const found = [];
|
|
3055
3080
|
const routesAll = [
|
|
3056
3081
|
...walk.routes.map((r) => ({ r, prefix: "" })),
|
|
3057
|
-
...
|
|
3082
|
+
...walk2 ? walk2.routes.map((r) => ({ r, prefix: "main2:" })) : []
|
|
3058
3083
|
];
|
|
3059
3084
|
const seen = /* @__PURE__ */ new Map();
|
|
3060
3085
|
for (const { r, prefix } of routesAll) {
|
|
@@ -3122,8 +3147,12 @@ function graphToDoc(pieces, input) {
|
|
|
3122
3147
|
for (const t of tracks) {
|
|
3123
3148
|
if (t.role === "main") continue;
|
|
3124
3149
|
const ends = turnouts.filter((sw) => sw.divergeTrack === t.id);
|
|
3125
|
-
if (ends.length >= 2 && new Set(ends.map((sw) => sw.onTrack)).size >= 2)
|
|
3126
|
-
|
|
3150
|
+
if (!(ends.length >= 2 && new Set(ends.map((sw) => sw.onTrack)).size >= 2)) continue;
|
|
3151
|
+
t.role = "crossover";
|
|
3152
|
+
const at = ends.map((sw) => sw.pos);
|
|
3153
|
+
t.fromPos = round(Math.min(...at));
|
|
3154
|
+
t.toPos = round(Math.max(...at));
|
|
3155
|
+
t.lane = main2Lane || 1;
|
|
3127
3156
|
}
|
|
3128
3157
|
const place = (a, what) => {
|
|
3129
3158
|
if (!a) return null;
|
|
@@ -3279,15 +3308,6 @@ function moduleConversionReport(doc, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3279
3308
|
candidates
|
|
3280
3309
|
};
|
|
3281
3310
|
});
|
|
3282
|
-
const mainIds = new Set((doc.tracks ?? []).filter((t) => t.role === "main").map((t) => t.id));
|
|
3283
|
-
for (const t of doc.turnouts ?? []) {
|
|
3284
|
-
if (!mainIds.has(t.divergeTrack)) continue;
|
|
3285
|
-
blockers.push({
|
|
3286
|
-
kind: "turnout-opens-a-main",
|
|
3287
|
-
ref: t.id,
|
|
3288
|
-
why: `${t.name || t.id} opens ${t.divergeTrack}, which is a main \u2014 a main that begins at a turnout cannot be laid as pieces yet, and converting anyway would drop both it and the turnout`
|
|
3289
|
-
});
|
|
3290
|
-
}
|
|
3291
3311
|
for (const c of doc.crossings ?? [])
|
|
3292
3312
|
blockers.push({
|
|
3293
3313
|
kind: "crossing",
|
|
@@ -3399,7 +3419,12 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3399
3419
|
const ends = [branch?.fromPos, branch?.toPos].filter(
|
|
3400
3420
|
(n) => typeof n === "number" && Number.isFinite(n)
|
|
3401
3421
|
);
|
|
3402
|
-
const divergeFarPos =
|
|
3422
|
+
const divergeFarPos = (
|
|
3423
|
+
// ⭐ A crossover half diverges TOWARD its partner. Its diverging track is a
|
|
3424
|
+
// MAIN, whose ends are the whole module, so the ordinary rule has nothing
|
|
3425
|
+
// useful to sign and would point it away from the turnout it feeds.
|
|
3426
|
+
divergeFarOverride.get(t.id) ?? (ends.length ? ends.reduce((a, b) => Math.abs(b - t.pos) > Math.abs(a - t.pos) ? b : a) : void 0)
|
|
3427
|
+
);
|
|
3403
3428
|
const facing = turnoutFacing({
|
|
3404
3429
|
pos: t.pos,
|
|
3405
3430
|
divergeFarPos,
|
|
@@ -3516,18 +3541,69 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3516
3541
|
fromPos: a.x0,
|
|
3517
3542
|
toPos: a.x0 + a.geom.lengthInches
|
|
3518
3543
|
}));
|
|
3544
|
+
const mainIdsAll = new Set(mains.map((m) => m.id));
|
|
3545
|
+
const attachment = /* @__PURE__ */ new Map();
|
|
3546
|
+
for (const t of turnouts) {
|
|
3547
|
+
const m = mains.find((x) => x.id === t.divergeTrack && x.id !== main.id);
|
|
3548
|
+
if (!m) continue;
|
|
3549
|
+
const from = m.fromPos ?? 0;
|
|
3550
|
+
const to = m.toPos ?? mainLen;
|
|
3551
|
+
if (from > 0 && Math.abs(t.pos - from) < 0.01) attachment.set(m.id, { sw: t, at: "from" });
|
|
3552
|
+
else if (Math.abs(t.pos - to) < 0.01) attachment.set(m.id, { sw: t, at: "to" });
|
|
3553
|
+
}
|
|
3554
|
+
const mainToMainPairs = [];
|
|
3555
|
+
{
|
|
3556
|
+
const isMain = (id) => mainIdsAll.has(id);
|
|
3557
|
+
const used = /* @__PURE__ */ new Set();
|
|
3558
|
+
for (const t of turnouts) {
|
|
3559
|
+
if (used.has(t.id) || !isMain(t.onTrack) || !isMain(t.divergeTrack)) continue;
|
|
3560
|
+
if (t.onTrack === t.divergeTrack) continue;
|
|
3561
|
+
const partner = turnouts.filter(
|
|
3562
|
+
(u) => !used.has(u.id) && u.id !== t.id && u.onTrack === t.divergeTrack && u.divergeTrack === t.onTrack
|
|
3563
|
+
).sort((x, y) => Math.abs(x.pos - t.pos) - Math.abs(y.pos - t.pos))[0];
|
|
3564
|
+
if (!partner) continue;
|
|
3565
|
+
used.add(t.id);
|
|
3566
|
+
used.add(partner.id);
|
|
3567
|
+
mainToMainPairs.push({ a: t, b: partner });
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const divergeFarOverride = /* @__PURE__ */ new Map();
|
|
3571
|
+
for (const { a, b } of mainToMainPairs) {
|
|
3572
|
+
divergeFarOverride.set(a.id, b.pos);
|
|
3573
|
+
divergeFarOverride.set(b.id, a.pos);
|
|
3574
|
+
}
|
|
3519
3575
|
for (const m of mains) {
|
|
3576
|
+
if (attachment.get(m.id)?.at === "from") continue;
|
|
3520
3577
|
const y = laneOffsetAt(m.lane ?? 0, 0);
|
|
3521
3578
|
const on = turnoutsOn(m.id, y);
|
|
3522
3579
|
for (const l of on) {
|
|
3523
3580
|
pieces.push(l.piece);
|
|
3524
3581
|
laidTurnouts.set(l.t.id, l);
|
|
3525
3582
|
}
|
|
3583
|
+
let endPos = m.toPos ?? mainLen;
|
|
3584
|
+
const closeAt = attachment.get(m.id);
|
|
3585
|
+
if (closeAt?.at === "to") {
|
|
3586
|
+
const l = laidTurnouts.get(closeAt.sw.id);
|
|
3587
|
+
const fj = l ? jointAt(l.piece, l.divergeId) : null;
|
|
3588
|
+
if (fj) {
|
|
3589
|
+
const closing = transition(
|
|
3590
|
+
`${m.id}-close`,
|
|
3591
|
+
{ x: fj.x, y: fj.y, headingDeg: fj.headingDeg },
|
|
3592
|
+
y,
|
|
3593
|
+
-1
|
|
3594
|
+
);
|
|
3595
|
+
if (closing) {
|
|
3596
|
+
pieces.push(closing);
|
|
3597
|
+
const e = jointAt(closing, "b");
|
|
3598
|
+
if (e) endPos = e.x;
|
|
3599
|
+
} else endPos = fj.x;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3526
3602
|
const laidFlex = layFlex(
|
|
3527
3603
|
m.id,
|
|
3528
3604
|
y,
|
|
3529
3605
|
m.fromPos ?? 0,
|
|
3530
|
-
|
|
3606
|
+
endPos,
|
|
3531
3607
|
[...on.map((l) => l.span), ...assemblySpans],
|
|
3532
3608
|
m.flexCuts
|
|
3533
3609
|
);
|
|
@@ -3549,6 +3625,35 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3549
3625
|
done.add(m.id);
|
|
3550
3626
|
}
|
|
3551
3627
|
for (const id of assemblyTracks) done.add(id);
|
|
3628
|
+
for (const [i, pair] of mainToMainPairs.entries()) {
|
|
3629
|
+
const la = laidTurnouts.get(pair.a.id);
|
|
3630
|
+
const lb = laidTurnouts.get(pair.b.id);
|
|
3631
|
+
if (!la || !lb) continue;
|
|
3632
|
+
const ja = jointAt(la.piece, la.divergeId);
|
|
3633
|
+
const jb = jointAt(lb.piece, lb.divergeId);
|
|
3634
|
+
if (!ja || !jb) continue;
|
|
3635
|
+
const dx = jb.x - ja.x;
|
|
3636
|
+
const dy = jb.y - ja.y;
|
|
3637
|
+
const len = Math.hypot(dx, dy);
|
|
3638
|
+
if (!(len > 1e-6)) {
|
|
3639
|
+
notLaid.push({
|
|
3640
|
+
id: pair.a.divergeTrack,
|
|
3641
|
+
why: `${pair.a.name || pair.a.id} and ${pair.b.name || pair.b.id} meet at the same point, so there is no track between them to lay`
|
|
3642
|
+
});
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
pieces.push({
|
|
3646
|
+
id: `xc-${i}`,
|
|
3647
|
+
partId: flexId,
|
|
3648
|
+
x: ja.x,
|
|
3649
|
+
y: ja.y,
|
|
3650
|
+
rotationDeg: Math.atan2(dy, dx) * 180 / Math.PI,
|
|
3651
|
+
lengthInches: r3(len)
|
|
3652
|
+
});
|
|
3653
|
+
warnings.push(
|
|
3654
|
+
`${pair.a.name || pair.a.id} and ${pair.b.name || pair.b.id} are laid as two separate turnouts with a ${len.toFixed(1)}\u2033 piece of track between them, which is what the document describes. If they are really one crossover assembly, name the product on a connector track and it will be laid as the single piece it is.`
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3552
3657
|
let progressed = true;
|
|
3553
3658
|
while (progressed) {
|
|
3554
3659
|
progressed = false;
|
|
@@ -3661,13 +3766,19 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3661
3766
|
};
|
|
3662
3767
|
const startAt = startOf(main);
|
|
3663
3768
|
if (!startAt) return refuse("nothing was laid on the mainline, so the module has no starting point");
|
|
3664
|
-
const second = mains[1] ?
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3769
|
+
const second = mains[1] ? attachment.get(mains[1].id)?.at === "from" ? (() => {
|
|
3770
|
+
const l = laidTurnouts.get(attachment.get(mains[1].id).sw.id);
|
|
3771
|
+
return l ? { piece: l.piece.id, joint: l.divergeId } : null;
|
|
3772
|
+
})() : startOf(mains[1]) : null;
|
|
3773
|
+
const graph = { pieces, startAt, ...second ? { start2: second } : {} };
|
|
3774
|
+
const derived = graphToDoc(pieces, {
|
|
3775
|
+
startAt,
|
|
3776
|
+
start2: second ?? null,
|
|
3777
|
+
base: doc,
|
|
3778
|
+
library
|
|
3779
|
+
});
|
|
3780
|
+
warnings.push(...derived.warnings);
|
|
3781
|
+
return { graph, refused: null, notLaid, warnings };
|
|
3671
3782
|
}
|
|
3672
3783
|
function frogCasting(cl, opts = {}) {
|
|
3673
3784
|
const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
|