@willcgage/module-schematic 0.108.0 → 0.109.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.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 walk2 = input.start2 ? walkTrackGraph(graph, pieces, input.start2, library) : null;
2931
- const main2 = walk2?.routes.find((r) => r.id === "main") ?? null;
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 claimed = new Set(walk.routes.flatMap((r) => r.pieces).filter((p) => !sharedByDesign(p)));
2937
- if (main2 && main2.pieces.some((p) => claimed.has(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) => claimed.has(p));
2943
- const main2Lane = twoMains ? (graph.joints.find((j) => j.key === `${input.start2.piece}.${input.start2.joint}`)?.y ?? 1) >= 0 ? 1 : -1 : 0;
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((r) => r.id !== "main" && r.pieces.length),
2947
- ...twoMains ? walk2.routes.filter((r) => r.id !== "main" && r.pieces.length).map((r) => ({ ...r, id: `main2:${r.id}`, bornAt: r.bornAt })) : []
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) trackIdOf.set("main2:main", MAIN2_TRACK_ID);
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
- from: epA?.id ?? "A",
2982
- ...epB ? { to: epB.id } : {},
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
- ...twoMains ? walk2.turnouts.map((t) => ({
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
- ...twoMains ? walk2.routes.map((r) => ({ r, prefix: "main2:" })) : []
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) {
@@ -3279,15 +3304,6 @@ function moduleConversionReport(doc, library = BUILT_IN_TRACK_PARTS) {
3279
3304
  candidates
3280
3305
  };
3281
3306
  });
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
3307
  for (const c of doc.crossings ?? [])
3292
3308
  blockers.push({
3293
3309
  kind: "crossing",
@@ -3516,18 +3532,52 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
3516
3532
  fromPos: a.x0,
3517
3533
  toPos: a.x0 + a.geom.lengthInches
3518
3534
  }));
3535
+ const attachment = /* @__PURE__ */ new Map();
3536
+ for (const t of turnouts) {
3537
+ const m = mains.find((x) => x.id === t.divergeTrack && x.id !== main.id);
3538
+ if (!m) continue;
3539
+ const from = m.fromPos ?? 0;
3540
+ const to = m.toPos ?? mainLen;
3541
+ if (from > 0 && Math.abs(t.pos - from) < 0.01) attachment.set(m.id, { sw: t, at: "from" });
3542
+ else if (Math.abs(t.pos - to) < 0.01) attachment.set(m.id, { sw: t, at: "to" });
3543
+ else
3544
+ notLaid.push({
3545
+ id: m.id,
3546
+ why: `${t.name || t.id} joins ${m.id} at ${t.pos}\u2033, which is neither end of it \u2014 two mains joined partway along are a crossover, and this document has no connector track between them to lay one from`
3547
+ });
3548
+ }
3519
3549
  for (const m of mains) {
3550
+ if (attachment.get(m.id)?.at === "from") continue;
3520
3551
  const y = laneOffsetAt(m.lane ?? 0, 0);
3521
3552
  const on = turnoutsOn(m.id, y);
3522
3553
  for (const l of on) {
3523
3554
  pieces.push(l.piece);
3524
3555
  laidTurnouts.set(l.t.id, l);
3525
3556
  }
3557
+ let endPos = m.toPos ?? mainLen;
3558
+ const closeAt = attachment.get(m.id);
3559
+ if (closeAt?.at === "to") {
3560
+ const l = laidTurnouts.get(closeAt.sw.id);
3561
+ const fj = l ? jointAt(l.piece, l.divergeId) : null;
3562
+ if (fj) {
3563
+ const closing = transition(
3564
+ `${m.id}-close`,
3565
+ { x: fj.x, y: fj.y, headingDeg: fj.headingDeg },
3566
+ y,
3567
+ -1
3568
+ );
3569
+ if (closing) {
3570
+ pieces.push(closing);
3571
+ const e = jointAt(closing, "b");
3572
+ if (e) endPos = e.x;
3573
+ } else endPos = fj.x;
3574
+ }
3575
+ }
3526
3576
  const laidFlex = layFlex(
3527
3577
  m.id,
3528
3578
  y,
3529
3579
  m.fromPos ?? 0,
3530
- m.toPos ?? mainLen,
3580
+ endPos,
3531
3581
  [...on.map((l) => l.span), ...assemblySpans],
3532
3582
  m.flexCuts
3533
3583
  );
@@ -3661,13 +3711,19 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
3661
3711
  };
3662
3712
  const startAt = startOf(main);
3663
3713
  if (!startAt) return refuse("nothing was laid on the mainline, so the module has no starting point");
3664
- const second = mains[1] ? startOf(mains[1]) : null;
3665
- return {
3666
- graph: { pieces, startAt, ...second ? { start2: second } : {} },
3667
- refused: null,
3668
- notLaid,
3669
- warnings
3670
- };
3714
+ const second = mains[1] ? attachment.get(mains[1].id)?.at === "from" ? (() => {
3715
+ const l = laidTurnouts.get(attachment.get(mains[1].id).sw.id);
3716
+ return l ? { piece: l.piece.id, joint: l.divergeId } : null;
3717
+ })() : startOf(mains[1]) : null;
3718
+ const graph = { pieces, startAt, ...second ? { start2: second } : {} };
3719
+ const derived = graphToDoc(pieces, {
3720
+ startAt,
3721
+ start2: second ?? null,
3722
+ base: doc,
3723
+ library
3724
+ });
3725
+ warnings.push(...derived.warnings);
3726
+ return { graph, refused: null, notLaid, warnings };
3671
3727
  }
3672
3728
  function frogCasting(cl, opts = {}) {
3673
3729
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;