@willcgage/module-schematic 0.65.0 → 0.67.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
@@ -789,9 +789,11 @@ interface EditorBranch {
789
789
  * draws track to it; the plate is placed first, connected later). */
790
790
  trackId?: string | null;
791
791
  }
792
- /** Endplate B on a loop module: a standard endplate makes the balloon an
793
- * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
794
- * makes it a pure turnback. Non-loop modules always have a real B. */
792
+ /** Endplate B's configuration or `"none"`, meaning **the module has no far
793
+ * endplate**: an end of the line, a pocket, or a loop's pure turnback. On a
794
+ * loop, a standard endplate B instead makes the balloon an INTERCHANGE (a second
795
+ * route connects there, e.g. Seaford). A module presents one conforming face,
796
+ * or two, or more; the standard governs the faces it offers, not how many (#184). */
795
797
  type EndplateBConfig = TrackConfig | "none";
796
798
  interface EditorState {
797
799
  lengthInches: number;
@@ -972,24 +974,32 @@ interface DrawCrossover {
972
974
  toLane: number;
973
975
  }
974
976
  /**
975
- * A route leaving the module at a third endplate (#170) Main 3, whatever the
976
- * owner labels it (#181). Drawn as an ELBOW: it diverges from its host main at
977
- * `posFrac`, drops to its own `lane`, then runs to `endFrac` and ends at an
978
- * endplate face.
977
+ * A route leaving the module at a third endplate — and an endplate is an
978
+ * endplate, whatever letter it carries (#183).
979
979
  *
980
- * The run is the branch's OWN arc length laid along the strip, not its
981
- * projection onto the module axis a square 90° exit projects to zero, which
982
- * is exactly the case that made branches invisible here. Straightening the
983
- * route out at its true length is the same idiom this whole view applies to the
984
- * main; `posFrac endFrac` is therefore a real axis, and `lengthInches` maps
985
- * onto it, so anything positioned along the branch has somewhere to draw.
980
+ * There is no split in the standard between "the two ends" and "the others": an
981
+ * endplate is the standardised face where a module joins another module, and a
982
+ * module may present two, three, or one. So a route to C is drawn the way a
983
+ * route to A or B is drawn **it runs to the edge of the module and terminates
984
+ * in an endplate face carrying its letter**, rather than stopping partway along
985
+ * the strip as a stub.
986
+ *
987
+ * ⚠️ Because it runs the full width it would otherwise read as just another
988
+ * parallel main, which is exactly the confusion a dispatcher must not have. Its
989
+ * lane is therefore placed with a clear GAP beyond every other lane (see
990
+ * `laneGapFromOthers`), and it ends at a plate rather than running off the edge.
991
+ * The two requirements — full width, and not-a-parallel-main — are in tension by
992
+ * design, and the separation is what resolves it.
993
+ *
994
+ * The LETTER is the module's own fact and is fine to draw. The DESTINATION
995
+ * ("to Fillmore") depends on which module is physically attached at that
996
+ * junction, so it stays Free-Dispatcher's to derive at runtime.
986
997
  */
987
998
  interface BranchConnector {
988
999
  /** Endplate id — "C", "D", … */
989
1000
  id: string;
990
- /** The endplate's label. MR draws no text (the destination depends on what's
991
- * physically attached, so FD derives the panel label at runtime) — this is
992
- * the owner's local name, for tooltips and FD's fallback. */
1001
+ /** The owner's local name for the plate. NOT the destination that depends
1002
+ * on what's physically attached, so FD derives it at runtime. */
993
1003
  label: string;
994
1004
  /** The route's own name, from the track. */
995
1005
  name: string;
@@ -1005,12 +1015,15 @@ interface BranchConnector {
1005
1015
  fromLane: number;
1006
1016
  /** Which side of the module it exits. */
1007
1017
  side: "up" | "down";
1008
- /** The branch's own lane, signed by `side` and placed clear of every other
1009
- * lane (so it never collides) — already folded into laneMin/laneMax. */
1018
+ /** The branch's own lane, signed by `side`, placed a clear GAP beyond every
1019
+ * other lane so it can't be mistaken for a parallel main — already folded
1020
+ * into laneMin/laneMax. */
1010
1021
  lane: number;
1011
- /** Where the run ends, and the endplate face is drawn. */
1022
+ /** Where the run ends and the endplate face is drawn: the module EDGE, 0 or
1023
+ * 1, because this is an end of the module like any other (#183). */
1012
1024
  endFrac: number;
1013
- /** The route's own arc length, inches. */
1025
+ /** The route's own arc length on this module, inches — for the tooltip. The
1026
+ * drawn run is the strip's width, not this. */
1014
1027
  lengthInches: number;
1015
1028
  }
1016
1029
  /** An industry — draw a car-spot span beside its track's lane, on `side`, with
@@ -1656,8 +1669,10 @@ interface ModuleGeometryInput {
1656
1669
  geometryType?: string | null;
1657
1670
  geometryDegrees?: number | null;
1658
1671
  geometryOffsetInches?: number | null;
1659
- /** Axial endplate configs (A first, then B). Missing → single. */
1660
- endplateConfigs?: ("single" | "double" | null | undefined)[];
1672
+ /** Axial endplate configs (A first, then B). Missing → single. `"none"` at B
1673
+ * means the module has no far endplate at all — an end of the line, a pocket,
1674
+ * or a turnback (#184). */
1675
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
1661
1676
  /** Branch endplates (from the schematic doc, #170), positioned along the
1662
1677
  * mainline axis. */
1663
1678
  branches?: {
package/dist/index.d.ts CHANGED
@@ -789,9 +789,11 @@ interface EditorBranch {
789
789
  * draws track to it; the plate is placed first, connected later). */
790
790
  trackId?: string | null;
791
791
  }
792
- /** Endplate B on a loop module: a standard endplate makes the balloon an
793
- * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
794
- * makes it a pure turnback. Non-loop modules always have a real B. */
792
+ /** Endplate B's configuration or `"none"`, meaning **the module has no far
793
+ * endplate**: an end of the line, a pocket, or a loop's pure turnback. On a
794
+ * loop, a standard endplate B instead makes the balloon an INTERCHANGE (a second
795
+ * route connects there, e.g. Seaford). A module presents one conforming face,
796
+ * or two, or more; the standard governs the faces it offers, not how many (#184). */
795
797
  type EndplateBConfig = TrackConfig | "none";
796
798
  interface EditorState {
797
799
  lengthInches: number;
@@ -972,24 +974,32 @@ interface DrawCrossover {
972
974
  toLane: number;
973
975
  }
974
976
  /**
975
- * A route leaving the module at a third endplate (#170) Main 3, whatever the
976
- * owner labels it (#181). Drawn as an ELBOW: it diverges from its host main at
977
- * `posFrac`, drops to its own `lane`, then runs to `endFrac` and ends at an
978
- * endplate face.
977
+ * A route leaving the module at a third endplate — and an endplate is an
978
+ * endplate, whatever letter it carries (#183).
979
979
  *
980
- * The run is the branch's OWN arc length laid along the strip, not its
981
- * projection onto the module axis a square 90° exit projects to zero, which
982
- * is exactly the case that made branches invisible here. Straightening the
983
- * route out at its true length is the same idiom this whole view applies to the
984
- * main; `posFrac endFrac` is therefore a real axis, and `lengthInches` maps
985
- * onto it, so anything positioned along the branch has somewhere to draw.
980
+ * There is no split in the standard between "the two ends" and "the others": an
981
+ * endplate is the standardised face where a module joins another module, and a
982
+ * module may present two, three, or one. So a route to C is drawn the way a
983
+ * route to A or B is drawn **it runs to the edge of the module and terminates
984
+ * in an endplate face carrying its letter**, rather than stopping partway along
985
+ * the strip as a stub.
986
+ *
987
+ * ⚠️ Because it runs the full width it would otherwise read as just another
988
+ * parallel main, which is exactly the confusion a dispatcher must not have. Its
989
+ * lane is therefore placed with a clear GAP beyond every other lane (see
990
+ * `laneGapFromOthers`), and it ends at a plate rather than running off the edge.
991
+ * The two requirements — full width, and not-a-parallel-main — are in tension by
992
+ * design, and the separation is what resolves it.
993
+ *
994
+ * The LETTER is the module's own fact and is fine to draw. The DESTINATION
995
+ * ("to Fillmore") depends on which module is physically attached at that
996
+ * junction, so it stays Free-Dispatcher's to derive at runtime.
986
997
  */
987
998
  interface BranchConnector {
988
999
  /** Endplate id — "C", "D", … */
989
1000
  id: string;
990
- /** The endplate's label. MR draws no text (the destination depends on what's
991
- * physically attached, so FD derives the panel label at runtime) — this is
992
- * the owner's local name, for tooltips and FD's fallback. */
1001
+ /** The owner's local name for the plate. NOT the destination that depends
1002
+ * on what's physically attached, so FD derives it at runtime. */
993
1003
  label: string;
994
1004
  /** The route's own name, from the track. */
995
1005
  name: string;
@@ -1005,12 +1015,15 @@ interface BranchConnector {
1005
1015
  fromLane: number;
1006
1016
  /** Which side of the module it exits. */
1007
1017
  side: "up" | "down";
1008
- /** The branch's own lane, signed by `side` and placed clear of every other
1009
- * lane (so it never collides) — already folded into laneMin/laneMax. */
1018
+ /** The branch's own lane, signed by `side`, placed a clear GAP beyond every
1019
+ * other lane so it can't be mistaken for a parallel main — already folded
1020
+ * into laneMin/laneMax. */
1010
1021
  lane: number;
1011
- /** Where the run ends, and the endplate face is drawn. */
1022
+ /** Where the run ends and the endplate face is drawn: the module EDGE, 0 or
1023
+ * 1, because this is an end of the module like any other (#183). */
1012
1024
  endFrac: number;
1013
- /** The route's own arc length, inches. */
1025
+ /** The route's own arc length on this module, inches — for the tooltip. The
1026
+ * drawn run is the strip's width, not this. */
1014
1027
  lengthInches: number;
1015
1028
  }
1016
1029
  /** An industry — draw a car-spot span beside its track's lane, on `side`, with
@@ -1656,8 +1669,10 @@ interface ModuleGeometryInput {
1656
1669
  geometryType?: string | null;
1657
1670
  geometryDegrees?: number | null;
1658
1671
  geometryOffsetInches?: number | null;
1659
- /** Axial endplate configs (A first, then B). Missing → single. */
1660
- endplateConfigs?: ("single" | "double" | null | undefined)[];
1672
+ /** Axial endplate configs (A first, then B). Missing → single. `"none"` at B
1673
+ * means the module has no far endplate at all — an end of the line, a pocket,
1674
+ * or a turnback (#184). */
1675
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
1661
1676
  /** Branch endplates (from the schematic doc, #170), positioned along the
1662
1677
  * mainline axis. */
1663
1678
  branches?: {
package/dist/index.js CHANGED
@@ -701,18 +701,20 @@ function stateToDoc(state, recordNumber) {
701
701
  ]
702
702
  ) : [
703
703
  { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
704
- // Non-loop modules always have a real B ("none" never applies).
705
- {
706
- id: "B",
707
- label: "East",
708
- tracks: [
709
- {
710
- trackId: MAIN_TRACK_ID,
711
- lane: 0,
712
- config: state.configB === "none" ? "single" : state.configB
713
- }
714
- ]
715
- }
704
+ // ⚠️ `configB: "none"` means the module HAS NO FAR ENDPLATE, and
705
+ // that is not loop-only (#184). An *end of the line* or a *pocket*
706
+ // presents one conforming face and the track simply stops. The
707
+ // standard governs the ends a module offers for joining; it never
708
+ // required a module to offer two. This used to coerce "none" to
709
+ // "single" and emit a B regardless, so a single-ended module was
710
+ // impossible to author.
711
+ ...state.configB !== "none" ? [
712
+ {
713
+ id: "B",
714
+ label: "East",
715
+ tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }]
716
+ }
717
+ ] : []
716
718
  ],
717
719
  // Branch endplates C, D, … — junction connections at pos, off one side
718
720
  // (#170). A set can carry several (e.g. a second railroad through).
@@ -1777,8 +1779,9 @@ function moduleFeatures(doc) {
1777
1779
  ...crossings.flatMap((x) => [x.laneA, x.laneB]),
1778
1780
  ...crossovers.flatMap((x) => [x.fromLane, x.toLane])
1779
1781
  ];
1780
- let upLane = Math.max(...baseLanes, 0);
1781
- let downLane = Math.min(...baseLanes, 0);
1782
+ const LANE_GAP_FROM_OTHERS = 2;
1783
+ let upLane = Math.max(...baseLanes, 0) + (LANE_GAP_FROM_OTHERS - 1);
1784
+ let downLane = Math.min(...baseLanes, 0) - (LANE_GAP_FROM_OTHERS - 1);
1782
1785
  const branchConnectors = doc.endplates.filter(
1783
1786
  (e) => e.id !== "A" && e.id !== "B" && e.at && !!e.trackId && (doc.tracks ?? []).some((t) => t.id === e.trackId)
1784
1787
  ).map((e) => {
@@ -1789,10 +1792,6 @@ function moduleFeatures(doc) {
1789
1792
  const lane = side === "down" ? --downLane : ++upLane;
1790
1793
  const runInches = pathLengthInches(trk.path) || Math.abs(e.at.pos - startPos) || FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
1791
1794
  const toB = e.at.pos >= startPos;
1792
- const fits = (p) => p >= 0 && p <= len;
1793
- const ahead = startPos + runInches;
1794
- const behind = startPos - runInches;
1795
- const endPos = toB && fits(ahead) ? ahead : !toB && fits(behind) ? behind : fits(toB ? behind : ahead) ? toB ? behind : ahead : ahead;
1796
1795
  return {
1797
1796
  id: e.id,
1798
1797
  label: e.label ?? e.id,
@@ -1803,7 +1802,7 @@ function moduleFeatures(doc) {
1803
1802
  fromLane: sw ? trackLane.get(sw.onTrack) ?? 0 : 0,
1804
1803
  side,
1805
1804
  lane,
1806
- endFrac: clampFrac(endPos),
1805
+ endFrac: toB ? 1 : 0,
1807
1806
  lengthInches: runInches
1808
1807
  };
1809
1808
  });
@@ -1985,7 +1984,7 @@ function deriveEndplatePoses(geo) {
1985
1984
  trackOffsets: offsetsFor(cfg(0), half)
1986
1985
  })
1987
1986
  );
1988
- const noB = geo.geometryType === "dead_end" || geo.loop === true;
1987
+ const noB = geo.geometryType === "dead_end" || geo.loop === true || geo.endplateConfigs?.[1] === "none";
1989
1988
  const end = sectionedEndPose({ sections: geo.sections });
1990
1989
  if (!noB && end) {
1991
1990
  poses.push(