@willcgage/module-schematic 0.66.0 → 0.68.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
@@ -673,7 +673,21 @@ declare function checkEndplateWidth(input: {
673
673
  config?: TrackConfig | "none" | null;
674
674
  trackOffsetInches?: number | null;
675
675
  }): EndplateWidthIssue[];
676
- /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
676
+ /**
677
+ * Whether a doc is a balloon loop — **the authored flag, and only that**.
678
+ *
679
+ * ⚠️ This used to ALSO infer a loop from `endplates.length === 1`, which was
680
+ * safe only while the sole way to have one endplate was to be a turnback. #184
681
+ * ended that: an *end of the line* or a *pocket* presents one conforming face
682
+ * too, so every single-ended module was silently classified as a loop — drawn
683
+ * with a bulb, its endplate A relabelled "Entry", and positions past the throat
684
+ * read as being inside a balloon that doesn't exist (#191).
685
+ *
686
+ * A loop is now only ever a loop because someone said so: the Loop checkbox and
687
+ * the return-loop generator both set `loop: true`. Checked before removing the
688
+ * inference — no stored doc relied on it (no module in the catalogue has one
689
+ * endplate without the flag, and none is `category:"loop"` or `dead_end`).
690
+ */
677
691
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
678
692
  declare const MAIN_TRACK_ID = "main";
679
693
  /** The second main on double-track modules — a real track entity so turnouts
@@ -789,9 +803,11 @@ interface EditorBranch {
789
803
  * draws track to it; the plate is placed first, connected later). */
790
804
  trackId?: string | null;
791
805
  }
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. */
806
+ /** Endplate B's configuration or `"none"`, meaning **the module has no far
807
+ * endplate**: an end of the line, a pocket, or a loop's pure turnback. On a
808
+ * loop, a standard endplate B instead makes the balloon an INTERCHANGE (a second
809
+ * route connects there, e.g. Seaford). A module presents one conforming face,
810
+ * or two, or more; the standard governs the faces it offers, not how many (#184). */
795
811
  type EndplateBConfig = TrackConfig | "none";
796
812
  interface EditorState {
797
813
  lengthInches: number;
@@ -1667,8 +1683,10 @@ interface ModuleGeometryInput {
1667
1683
  geometryType?: string | null;
1668
1684
  geometryDegrees?: number | null;
1669
1685
  geometryOffsetInches?: number | null;
1670
- /** Axial endplate configs (A first, then B). Missing → single. */
1671
- endplateConfigs?: ("single" | "double" | null | undefined)[];
1686
+ /** Axial endplate configs (A first, then B). Missing → single. `"none"` at B
1687
+ * means the module has no far endplate at all — an end of the line, a pocket,
1688
+ * or a turnback (#184). */
1689
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
1672
1690
  /** Branch endplates (from the schematic doc, #170), positioned along the
1673
1691
  * mainline axis. */
1674
1692
  branches?: {
package/dist/index.d.ts CHANGED
@@ -673,7 +673,21 @@ declare function checkEndplateWidth(input: {
673
673
  config?: TrackConfig | "none" | null;
674
674
  trackOffsetInches?: number | null;
675
675
  }): EndplateWidthIssue[];
676
- /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
676
+ /**
677
+ * Whether a doc is a balloon loop — **the authored flag, and only that**.
678
+ *
679
+ * ⚠️ This used to ALSO infer a loop from `endplates.length === 1`, which was
680
+ * safe only while the sole way to have one endplate was to be a turnback. #184
681
+ * ended that: an *end of the line* or a *pocket* presents one conforming face
682
+ * too, so every single-ended module was silently classified as a loop — drawn
683
+ * with a bulb, its endplate A relabelled "Entry", and positions past the throat
684
+ * read as being inside a balloon that doesn't exist (#191).
685
+ *
686
+ * A loop is now only ever a loop because someone said so: the Loop checkbox and
687
+ * the return-loop generator both set `loop: true`. Checked before removing the
688
+ * inference — no stored doc relied on it (no module in the catalogue has one
689
+ * endplate without the flag, and none is `category:"loop"` or `dead_end`).
690
+ */
677
691
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
678
692
  declare const MAIN_TRACK_ID = "main";
679
693
  /** The second main on double-track modules — a real track entity so turnouts
@@ -789,9 +803,11 @@ interface EditorBranch {
789
803
  * draws track to it; the plate is placed first, connected later). */
790
804
  trackId?: string | null;
791
805
  }
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. */
806
+ /** Endplate B's configuration or `"none"`, meaning **the module has no far
807
+ * endplate**: an end of the line, a pocket, or a loop's pure turnback. On a
808
+ * loop, a standard endplate B instead makes the balloon an INTERCHANGE (a second
809
+ * route connects there, e.g. Seaford). A module presents one conforming face,
810
+ * or two, or more; the standard governs the faces it offers, not how many (#184). */
795
811
  type EndplateBConfig = TrackConfig | "none";
796
812
  interface EditorState {
797
813
  lengthInches: number;
@@ -1667,8 +1683,10 @@ interface ModuleGeometryInput {
1667
1683
  geometryType?: string | null;
1668
1684
  geometryDegrees?: number | null;
1669
1685
  geometryOffsetInches?: number | null;
1670
- /** Axial endplate configs (A first, then B). Missing → single. */
1671
- endplateConfigs?: ("single" | "double" | null | undefined)[];
1686
+ /** Axial endplate configs (A first, then B). Missing → single. `"none"` at B
1687
+ * means the module has no far endplate at all — an end of the line, a pocket,
1688
+ * or a turnback (#184). */
1689
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
1672
1690
  /** Branch endplates (from the schematic doc, #170), positioned along the
1673
1691
  * mainline axis. */
1674
1692
  branches?: {
package/dist/index.js CHANGED
@@ -532,7 +532,7 @@ function endplateWidthFor(widths, id) {
532
532
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
533
533
  }
534
534
  function isLoopDoc(doc) {
535
- return doc.loop === true || doc.endplates.length === 1;
535
+ return doc.loop === true;
536
536
  }
537
537
  var MAIN_TRACK_ID = "main";
538
538
  var MAIN2_TRACK_ID = "main2";
@@ -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).
@@ -927,8 +929,14 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
927
929
  loopReturn: loop && d.loopReturn === "main2" ? "main2" : "same",
928
930
  mainsSwapped: d.mainsSwapped === true,
929
931
  configA: configOf("A"),
930
- // On a loop, a missing B means pure turnback; present = interchange loop.
931
- configB: loop && !hasB ? "none" : configOf("B"),
932
+ // NO ENDPLATE B ⇒ "none", whatever kind of module this is. On a loop that
933
+ // reads as a pure turnback (a present B makes it an interchange); on an end
934
+ // of the line or a pocket it simply means the module has one face (#184).
935
+ // ⚠️ This used to be `loop && !hasB`, so "none" only survived a round trip on
936
+ // a loop — which passed its test yesterday ONLY because a single-endplate
937
+ // module was still being misclassified as one (#191). The absence of the
938
+ // plate is the fact; why it's absent doesn't change what to read.
939
+ configB: hasB ? configOf("B") : "none",
932
940
  branches: branchEps.map((ep) => ({
933
941
  label: ep.label ?? "Branch",
934
942
  pos: sc(ep.at.pos),
@@ -1982,7 +1990,7 @@ function deriveEndplatePoses(geo) {
1982
1990
  trackOffsets: offsetsFor(cfg(0), half)
1983
1991
  })
1984
1992
  );
1985
- const noB = geo.geometryType === "dead_end" || geo.loop === true;
1993
+ const noB = geo.geometryType === "dead_end" || geo.loop === true || geo.endplateConfigs?.[1] === "none";
1986
1994
  const end = sectionedEndPose({ sections: geo.sections });
1987
1995
  if (!noB && end) {
1988
1996
  poses.push(