@willcgage/module-schematic 0.86.0 → 0.89.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
@@ -1046,6 +1046,10 @@ function stateToDoc(state, recordNumber) {
1046
1046
  track: ind.track,
1047
1047
  fromPos: ind.fromPos,
1048
1048
  toPos: ind.toPos,
1049
+ // Carried, not interpreted. An anchor is what HOLDS the span (ADR
1050
+ // 0001); dropping it here would quietly convert an anchored
1051
+ // industry back into a typed number the next time anyone saved.
1052
+ ...ind.anchor ? { anchor: ind.anchor } : {},
1049
1053
  ...ind.spots?.length ? { spots: ind.spots } : {},
1050
1054
  side: ind.side,
1051
1055
  ...ind.labelMode && ind.labelMode !== "none" ? { labelMode: ind.labelMode } : {},
@@ -1065,7 +1069,11 @@ function stateToDoc(state, recordNumber) {
1065
1069
  ...state.sections.length ? { sections: moduleSections({ sections: state.sections }) } : {},
1066
1070
  // Authored mainline path (module-local inches); only when it's a real path.
1067
1071
  ...state.mainPath.length >= 2 ? { mainPath: state.mainPath } : {},
1068
- ...state.main2Path.length >= 2 ? { main2Path: state.main2Path } : {}
1072
+ ...state.main2Path.length >= 2 ? { main2Path: state.main2Path } : {},
1073
+ // The pieces the owner drew, carried verbatim (ADR 0001). This function
1074
+ // stays dumb about them on purpose: deriving here would put the derivation
1075
+ // in every save path in both apps. `deriveGraphDoc` is the one place.
1076
+ ...state.graph?.pieces?.length ? { graph: state.graph } : {}
1069
1077
  };
1070
1078
  }
1071
1079
  function docToState(doc, fallbackLength, moduleTracks = []) {
@@ -1210,6 +1218,10 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
1210
1218
  sections: moduleSections(d),
1211
1219
  mainPath,
1212
1220
  main2Path,
1221
+ // ⚠️ NOT scaled by `sc()` like every position above it. A piece is placed in
1222
+ // real inches on the board; stretching the module's length does not move it,
1223
+ // and the length is DERIVED from the pieces anyway when a graph is present.
1224
+ ...d.graph?.pieces?.length ? { graph: d.graph } : {},
1213
1225
  crossings: (d.crossings ?? []).map((x) => ({
1214
1226
  id: x.id,
1215
1227
  name: x.name ?? "",
@@ -1244,12 +1256,14 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
1244
1256
  track: s.track,
1245
1257
  fromPos: sc(s.fromPos ?? 0),
1246
1258
  toPos: s.toPos != null ? sc(s.toPos) : len,
1247
- ...s.side ? { side: s.side } : {}
1259
+ ...s.side ? { side: s.side } : {},
1260
+ ...s.anchor ? { anchor: s.anchor } : {}
1248
1261
  })),
1249
1262
  side: ind.side ?? "above",
1250
1263
  labelMode: ind.labelMode ?? "none",
1251
1264
  carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : [],
1252
- moduleIndustryId: ind.moduleIndustryId ?? null
1265
+ moduleIndustryId: ind.moduleIndustryId ?? null,
1266
+ ...ind.anchor ? { anchor: ind.anchor } : {}
1253
1267
  }))
1254
1268
  };
1255
1269
  }
@@ -2348,6 +2362,7 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2348
2362
  bornAt,
2349
2363
  endsAt: null,
2350
2364
  pieces: [],
2365
+ spans: [],
2351
2366
  lateral: 0
2352
2367
  };
2353
2368
  let cur = startKey;
@@ -2405,7 +2420,15 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2405
2420
  for (const j of graph.joints)
2406
2421
  if (j.piece === here.piece && Math.abs(j.y) > Math.abs(route.lateral))
2407
2422
  route.lateral = j.y;
2423
+ const entered = pos;
2408
2424
  pos += gap(here, exit);
2425
+ route.spans.push({
2426
+ piece: here.piece,
2427
+ entryJoint: here.joint,
2428
+ exitJoint,
2429
+ fromPos: entered,
2430
+ toPos: pos
2431
+ });
2409
2432
  route.toPos = pos;
2410
2433
  const next = exit ? link.get(exit.key) : void 0;
2411
2434
  if (!next) break;
@@ -2422,6 +2445,12 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2422
2445
  warnings.push(`the route diverging at ${b.from} is not connected to anything`);
2423
2446
  continue;
2424
2447
  }
2448
+ const already = routes.find((r2) => r2.pieces.includes(byKey.get(start).piece));
2449
+ if (already) {
2450
+ const swFar = turnouts.find((t) => t.id === b.from);
2451
+ if (swFar && !swFar.divergeRoute) swFar.divergeRoute = already.id;
2452
+ continue;
2453
+ }
2425
2454
  n += 1;
2426
2455
  const r = walk(start, b.at + b.skew, `route${n}`, b.from);
2427
2456
  r.fromPos = b.at;
@@ -2436,6 +2465,153 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
2436
2465
  for (const c of graph.conflicts) warnings.push(c.reason);
2437
2466
  return { routes, turnouts, warnings };
2438
2467
  }
2468
+ function resolveGraphAnchor(anchor, walk, pieces, library = BUILT_IN_TRACK_PARTS) {
2469
+ const piece = pieces.find((p) => p.id === anchor.piece);
2470
+ const part = piece ? library.find((x) => x.id === piece.partId) : void 0;
2471
+ const origin = part?.kind === "flex" ? "a" : "throat";
2472
+ for (const r of walk.routes) {
2473
+ const span = r.spans.find((s) => s.piece === anchor.piece);
2474
+ if (!span) continue;
2475
+ if (span.entryJoint === origin)
2476
+ return { pos: span.fromPos + anchor.atInches, routeId: r.id, reversed: false };
2477
+ if (span.exitJoint === origin)
2478
+ return { pos: span.toPos - anchor.atInches, routeId: r.id, reversed: true };
2479
+ return null;
2480
+ }
2481
+ return null;
2482
+ }
2483
+ function graphToDoc(pieces, input) {
2484
+ const library = input.library ?? BUILT_IN_TRACK_PARTS;
2485
+ const graph = buildTrackGraph(pieces, library);
2486
+ const walk = walkTrackGraph(graph, pieces, input.startAt, library);
2487
+ const warnings = [...walk.warnings];
2488
+ const round = (n) => Math.round(n * 100) / 100;
2489
+ const base = input.base ?? {};
2490
+ const endplates = base.endplates && base.endplates.length ? base.endplates : [{ id: "A", label: "West" }, { id: "B", label: "East" }];
2491
+ const epA = endplates[0];
2492
+ const epB = endplates[1];
2493
+ const main = walk.routes.find((r) => r.id === "main");
2494
+ const lengthInches = round(main.toPos);
2495
+ const branches = walk.routes.filter((r) => r.id !== "main" && r.pieces.length);
2496
+ const trackIdOf = /* @__PURE__ */ new Map([["main", MAIN_TRACK_ID]]);
2497
+ for (const r of branches) trackIdOf.set(r.id, r.pieces[0]);
2498
+ const laneOf = (r) => {
2499
+ const side = Math.sign(r.lateral) || 1;
2500
+ const sameSide = branches.filter((x) => (Math.sign(x.lateral) || 1) === side).sort((a, b) => Math.abs(a.lateral) - Math.abs(b.lateral));
2501
+ return side * (sameSide.indexOf(r) + 1);
2502
+ };
2503
+ const tracks = [
2504
+ {
2505
+ id: MAIN_TRACK_ID,
2506
+ role: "main",
2507
+ lane: 0,
2508
+ from: epA?.id ?? "A",
2509
+ ...epB ? { to: epB.id } : {}
2510
+ }
2511
+ ];
2512
+ for (const r of branches) {
2513
+ const id = trackIdOf.get(r.id);
2514
+ const meta = input.meta?.[id] ?? {};
2515
+ tracks.push({
2516
+ id,
2517
+ // It runs back into a second turnout, or it doesn't. That is the whole
2518
+ // difference between a siding and a spur, and it is read, not declared.
2519
+ role: r.endsAt ? "siding" : "spur",
2520
+ lane: laneOf(r),
2521
+ fromPos: round(Math.min(r.fromPos, r.toPos)),
2522
+ toPos: round(Math.max(r.fromPos, r.toPos)),
2523
+ ...meta.trackName ? { trackName: meta.trackName } : {},
2524
+ ...meta.capacityFeet != null ? { capacityFeet: meta.capacityFeet } : {},
2525
+ ...meta.moduleTrackId != null ? { moduleTrackId: meta.moduleTrackId } : {}
2526
+ });
2527
+ }
2528
+ const pieceById = new Map(pieces.map((p) => [p.id, p]));
2529
+ const turnouts = [];
2530
+ for (const t of walk.turnouts) {
2531
+ const diverge = t.divergeRoute ? trackIdOf.get(t.divergeRoute) : void 0;
2532
+ if (!diverge) {
2533
+ warnings.push(
2534
+ `${t.id} is placed but its diverging route goes nowhere, so it is not in the operations view`
2535
+ );
2536
+ continue;
2537
+ }
2538
+ const piece = pieceById.get(t.id);
2539
+ const part = piece ? library.find((p) => p.id === piece.partId) : void 0;
2540
+ turnouts.push({
2541
+ id: t.id,
2542
+ pos: round(t.pos),
2543
+ onTrack: trackIdOf.get(t.onRoute) ?? MAIN_TRACK_ID,
2544
+ divergeTrack: diverge,
2545
+ ...piece?.name ? { name: piece.name } : {},
2546
+ ...part?.frogNumber != null ? { size: part.frogNumber } : {},
2547
+ ...part ? { partId: part.id } : {}
2548
+ });
2549
+ }
2550
+ turnouts.sort((a, b) => a.pos - b.pos);
2551
+ const place = (a, what) => {
2552
+ if (!a) return null;
2553
+ const hit = resolveGraphAnchor(a, walk, pieces, library);
2554
+ if (!hit) {
2555
+ warnings.push(
2556
+ `${what} is anchored to ${a.piece}, which is not on any route \u2014 its authored position stands`
2557
+ );
2558
+ return null;
2559
+ }
2560
+ return hit;
2561
+ };
2562
+ const spanAt = (hit, from, to) => {
2563
+ const len = Math.abs(to - from);
2564
+ return hit.reversed ? [round(hit.pos - len), round(hit.pos)] : [round(hit.pos), round(hit.pos + len)];
2565
+ };
2566
+ const industries = base.industries?.map((ind) => {
2567
+ const hit = place(ind.anchor, `industry "${ind.name}"`);
2568
+ const spots = ind.spots?.map((s) => {
2569
+ const sh = place(s.anchor, `a spot of industry "${ind.name}"`);
2570
+ if (!sh) return s;
2571
+ const [from, to] = spanAt(sh, s.fromPos, s.toPos);
2572
+ return { ...s, track: trackIdOf.get(sh.routeId) ?? s.track, fromPos: from, toPos: to };
2573
+ });
2574
+ if (!hit) return spots ? { ...ind, spots } : ind;
2575
+ const [fromPos, toPos] = spanAt(hit, ind.fromPos, ind.toPos);
2576
+ return {
2577
+ ...ind,
2578
+ track: trackIdOf.get(hit.routeId) ?? ind.track,
2579
+ fromPos,
2580
+ toPos,
2581
+ ...spots ? { spots } : {}
2582
+ };
2583
+ });
2584
+ const signals = base.signals?.map((sig) => {
2585
+ const hit = place(sig.anchor, `signal "${sig.name ?? sig.id}"`);
2586
+ return hit ? { ...sig, pos: round(hit.pos), track: trackIdOf.get(hit.routeId) ?? sig.track } : sig;
2587
+ });
2588
+ const doc = {
2589
+ version: 1,
2590
+ ...base,
2591
+ lengthInches,
2592
+ endplates,
2593
+ tracks,
2594
+ turnouts,
2595
+ ...industries ? { industries } : {},
2596
+ ...signals ? { signals } : {}
2597
+ };
2598
+ return { doc, graph, walk, warnings };
2599
+ }
2600
+ function deriveGraphDoc(doc, library = BUILT_IN_TRACK_PARTS) {
2601
+ const g = doc.graph;
2602
+ if (!g || !g.pieces?.length || !g.startAt) return { doc, warnings: [] };
2603
+ const meta = {};
2604
+ for (const t of doc.tracks ?? []) {
2605
+ if (t.role === "main") continue;
2606
+ meta[t.id] = {
2607
+ ...t.trackName ? { trackName: t.trackName } : {},
2608
+ ...t.capacityFeet != null ? { capacityFeet: t.capacityFeet } : {},
2609
+ ...t.moduleTrackId != null ? { moduleTrackId: t.moduleTrackId } : {}
2610
+ };
2611
+ }
2612
+ const out = graphToDoc(g.pieces, { startAt: g.startAt, base: doc, meta, library });
2613
+ return { doc: out.doc, warnings: out.warnings };
2614
+ }
2439
2615
  function frogCasting(cl, opts = {}) {
2440
2616
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
2441
2617
  const w = opts.reachInches ?? g * 1.5;
@@ -3107,6 +3283,7 @@ exports.checkEndplateWidth = checkEndplateWidth;
3107
3283
  exports.clearancePointPastFrogInches = clearancePointPastFrogInches;
3108
3284
  exports.crossoverPinches = crossoverPinches;
3109
3285
  exports.deriveEndplatePoses = deriveEndplatePoses;
3286
+ exports.deriveGraphDoc = deriveGraphDoc;
3110
3287
  exports.divergeSideForHand = divergeSideForHand;
3111
3288
  exports.docToState = docToState;
3112
3289
  exports.emptyEditorState = emptyEditorState;
@@ -3124,6 +3301,7 @@ exports.frogCasting = frogCasting;
3124
3301
  exports.frogNumberFromName = frogNumberFromName;
3125
3302
  exports.fromSectionRelative = fromSectionRelative;
3126
3303
  exports.geometryTurnDegrees = geometryTurnDegrees;
3304
+ exports.graphToDoc = graphToDoc;
3127
3305
  exports.hasNoFarEndplate = hasNoFarEndplate;
3128
3306
  exports.importedPartToTrackPart = importedPartToTrackPart;
3129
3307
  exports.inchesToScaleFeet = inchesToScaleFeet;
@@ -3154,6 +3332,7 @@ exports.poseNeedsManual = poseNeedsManual;
3154
3332
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
3155
3333
  exports.remapPos = remapPos;
3156
3334
  exports.resizeFlexPiece = resizeFlexPiece;
3335
+ exports.resolveGraphAnchor = resolveGraphAnchor;
3157
3336
  exports.returnLoop = returnLoop;
3158
3337
  exports.sampleBenchworkOutline = sampleBenchworkOutline;
3159
3338
  exports.samplePartSegments = samplePartSegments;