@willcgage/module-schematic 0.69.0 → 0.71.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
@@ -428,10 +428,18 @@ interface ModuleFootprintInput {
428
428
  /** Where each endplate's CENTRE sits relative to the main centre-line at that
429
429
  * end, inches (signed, along the +normal). Free-moN puts a **double**-track
430
430
  * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
431
- * centre-line the plate centre is half a track spacing up pass
432
- * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
433
- * (the default), and an off-centre track is a signed value. */
431
+ * centre-line the plate centre is half a track spacing toward Main 2. Single
432
+ * track crosses at the centre ⇒ 0, and an off-centre track is a signed value.
433
+ * {@link endplateCentreOffsetInches} computes it from what the owner authored.
434
+ *
435
+ * ⚠️ OMITTING an end no longer means 0 — it means "use §2.0", which is a
436
+ * straddle on a double end. Centring a double plate on Main 1 drew its pair
437
+ * wholly to one side, which is what the read-only and catalog views did by
438
+ * passing nothing at all (#190). Pass an explicit number to place a plate. */
434
439
  endplateTrackOffsets?: Record<string, number>;
440
+ /** Whether Main 2 runs BELOW Main 1 (`mainsSwapped`, #131). Only affects which
441
+ * way an unauthored double plate straddles. */
442
+ mainsSwapped?: boolean;
435
443
  /** Authored benchwork outline (module-local inches), or absent for the band. */
436
444
  outline?: BenchworkPoint[] | null;
437
445
  /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
@@ -447,6 +455,10 @@ interface ModuleFootprintInput {
447
455
  /** A balloon / return loop — the centre-line turns back on itself, so its far
448
456
  * end is the THROAT, not an endplate; only endplate A's face is emitted (#loop). */
449
457
  loop?: boolean;
458
+ /** Axial endplate configs (A first, then B), same as
459
+ * {@link ModuleGeometryInput.endplateConfigs}. `"none"` at B means the module
460
+ * presents no far endplate, so no face is emitted there (#184/#191). */
461
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
450
462
  }
451
463
  interface OutlineFace {
452
464
  /** The endplate face's two corners + midpoint (the track point). */
@@ -633,23 +645,45 @@ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number,
633
645
  */
634
646
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
635
647
  /**
636
- * The offset from the main centre-line to an endplate's CENTRE, inches — the
637
- * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
638
- * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
639
- * plate centre is half a track spacing up. A single track crosses at the centre.
648
+ * Where an endplate's CENTRE sits relative to MAIN 1 — the renderer's framing,
649
+ * and the one number a drawing needs to place the plate.
650
+ *
651
+ * THE single definition. Two callers had their own: the builder computed it
652
+ * inline (swap-aware), and the read-only/catalog footprint **passed nothing at
653
+ * all**, so every plate there was centred on Main 1 — a double end drew its pair
654
+ * entirely to one side of the plate instead of straddling it (#190).
655
+ *
656
+ * §2.0 puts a double end's two tracks 0.5625″ either side of the plate centre,
657
+ * so the plate centre is half a track spacing away from Main 1 — **toward Main
658
+ * 2**, which is why the swap matters: with Main 2 below, the plate centre is
659
+ * below too. An authored offset always wins (an off-centre end is legal since
660
+ * the 20220628 revision), and is given in the standard's own framing — Main 1's
661
+ * distance from the plate centre — so it comes back negated here.
640
662
  */
641
- declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
663
+ declare function endplateCentreOffsetInches(input: {
664
+ config?: TrackConfig | "none" | null;
665
+ /** Main 1's signed distance from the plate centre, as authored. */
666
+ authoredTrackOffsetInches?: number | null;
667
+ /** Whether Main 2 runs below Main 1 (the mains are swapped). */
668
+ main2Below?: boolean;
669
+ }): number;
642
670
  /**
643
671
  * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
644
672
  * the plate's CENTRE — the standard's own framing. Authored value wins; absent
645
673
  * falls back to the §2.0 recommendations: a single track centred (0), a double
646
674
  * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
647
675
  */
648
- declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
676
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined,
677
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). The pair
678
+ * straddles the plate centre either way, so Main 1 sits on the side AWAY from
679
+ * Main 2 — low when Main 2 is high, high when it's low. Omitting this
680
+ * hard-coded "Main 2 is above", which is the assumption behind #190. */
681
+ main2Below?: boolean): number;
649
682
  /** A Free-moN conformance problem with an endplate's width/track placement. */
650
683
  interface EndplateWidthIssue {
651
- /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
652
- code: "narrow" | "clearance";
684
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia;
685
+ * "offcentre" = a double end whose pair doesn't straddle the plate centre. */
686
+ code: "narrow" | "clearance" | "offcentre";
653
687
  /** Plain-language problem, for the author. */
654
688
  message: string;
655
689
  /** The width that would satisfy this rule, inches. */
@@ -672,7 +706,32 @@ declare function checkEndplateWidth(input: {
672
706
  widthInches?: number | null;
673
707
  config?: TrackConfig | "none" | null;
674
708
  trackOffsetInches?: number | null;
709
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). Without it a
710
+ * double end's second track was assumed to be one spacing ABOVE Main 1, so a
711
+ * swapped pair was measured on the wrong side of the plate (#190). */
712
+ main2Below?: boolean;
675
713
  }): EndplateWidthIssue[];
714
+ /**
715
+ * Whether a module presents NO far endplate — one conforming face only.
716
+ *
717
+ * ⭐ THE single definition. Three places had grown their own answer to this and
718
+ * drifted: `deriveEndplatePoses` (which knew about `dead_end` and `"none"`), the
719
+ * operations preview's end label, and `moduleFootprint`'s endplate faces — the
720
+ * last two using "is it a loop?" as a stand-in for "has it got two ends?", which
721
+ * was true until #184 and silently wrong after. Anything asking this must call
722
+ * THIS.
723
+ *
724
+ * Three ways to have one end, and they are genuinely different things: the
725
+ * geometry is a dead end; it's a balloon loop, whose far end is the throat and
726
+ * not a plate at all; or the owner said this end has no plate, which is how an
727
+ * *end of the line* or a *pocket* is authored — ordinary straight boards that
728
+ * simply stop, so no geometry type would ever say it for them.
729
+ */
730
+ declare function hasNoFarEndplate(input: {
731
+ geometryType?: string | null;
732
+ loop?: boolean;
733
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
734
+ }): boolean;
676
735
  /**
677
736
  * Whether a doc is a balloon loop — **the authored flag, and only that**.
678
737
  *
@@ -1785,4 +1844,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1785
1844
  heading: number;
1786
1845
  }>;
1787
1846
 
1788
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1847
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
package/dist/index.d.ts CHANGED
@@ -428,10 +428,18 @@ interface ModuleFootprintInput {
428
428
  /** Where each endplate's CENTRE sits relative to the main centre-line at that
429
429
  * end, inches (signed, along the +normal). Free-moN puts a **double**-track
430
430
  * plate's two tracks 9/16″ either side of its centre, so with Main 1 on the
431
- * centre-line the plate centre is half a track spacing up pass
432
- * `FREEMO_TRACK_SPACING_INCHES / 2`. Single track crosses at the centre ⇒ 0
433
- * (the default), and an off-centre track is a signed value. */
431
+ * centre-line the plate centre is half a track spacing toward Main 2. Single
432
+ * track crosses at the centre ⇒ 0, and an off-centre track is a signed value.
433
+ * {@link endplateCentreOffsetInches} computes it from what the owner authored.
434
+ *
435
+ * ⚠️ OMITTING an end no longer means 0 — it means "use §2.0", which is a
436
+ * straddle on a double end. Centring a double plate on Main 1 drew its pair
437
+ * wholly to one side, which is what the read-only and catalog views did by
438
+ * passing nothing at all (#190). Pass an explicit number to place a plate. */
434
439
  endplateTrackOffsets?: Record<string, number>;
440
+ /** Whether Main 2 runs BELOW Main 1 (`mainsSwapped`, #131). Only affects which
441
+ * way an unauthored double plate straddles. */
442
+ mainsSwapped?: boolean;
435
443
  /** Authored benchwork outline (module-local inches), or absent for the band. */
436
444
  outline?: BenchworkPoint[] | null;
437
445
  /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
@@ -447,6 +455,10 @@ interface ModuleFootprintInput {
447
455
  /** A balloon / return loop — the centre-line turns back on itself, so its far
448
456
  * end is the THROAT, not an endplate; only endplate A's face is emitted (#loop). */
449
457
  loop?: boolean;
458
+ /** Axial endplate configs (A first, then B), same as
459
+ * {@link ModuleGeometryInput.endplateConfigs}. `"none"` at B means the module
460
+ * presents no far endplate, so no face is emitted there (#184/#191). */
461
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
450
462
  }
451
463
  interface OutlineFace {
452
464
  /** The endplate face's two corners + midpoint (the track point). */
@@ -633,23 +645,45 @@ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number,
633
645
  */
634
646
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
635
647
  /**
636
- * The offset from the main centre-line to an endplate's CENTRE, inches — the
637
- * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
638
- * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
639
- * plate centre is half a track spacing up. A single track crosses at the centre.
648
+ * Where an endplate's CENTRE sits relative to MAIN 1 — the renderer's framing,
649
+ * and the one number a drawing needs to place the plate.
650
+ *
651
+ * THE single definition. Two callers had their own: the builder computed it
652
+ * inline (swap-aware), and the read-only/catalog footprint **passed nothing at
653
+ * all**, so every plate there was centred on Main 1 — a double end drew its pair
654
+ * entirely to one side of the plate instead of straddling it (#190).
655
+ *
656
+ * §2.0 puts a double end's two tracks 0.5625″ either side of the plate centre,
657
+ * so the plate centre is half a track spacing away from Main 1 — **toward Main
658
+ * 2**, which is why the swap matters: with Main 2 below, the plate centre is
659
+ * below too. An authored offset always wins (an off-centre end is legal since
660
+ * the 20220628 revision), and is given in the standard's own framing — Main 1's
661
+ * distance from the plate centre — so it comes back negated here.
640
662
  */
641
- declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
663
+ declare function endplateCentreOffsetInches(input: {
664
+ config?: TrackConfig | "none" | null;
665
+ /** Main 1's signed distance from the plate centre, as authored. */
666
+ authoredTrackOffsetInches?: number | null;
667
+ /** Whether Main 2 runs below Main 1 (the mains are swapped). */
668
+ main2Below?: boolean;
669
+ }): number;
642
670
  /**
643
671
  * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
644
672
  * the plate's CENTRE — the standard's own framing. Authored value wins; absent
645
673
  * falls back to the §2.0 recommendations: a single track centred (0), a double
646
674
  * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
647
675
  */
648
- declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
676
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined,
677
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). The pair
678
+ * straddles the plate centre either way, so Main 1 sits on the side AWAY from
679
+ * Main 2 — low when Main 2 is high, high when it's low. Omitting this
680
+ * hard-coded "Main 2 is above", which is the assumption behind #190. */
681
+ main2Below?: boolean): number;
649
682
  /** A Free-moN conformance problem with an endplate's width/track placement. */
650
683
  interface EndplateWidthIssue {
651
- /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
652
- code: "narrow" | "clearance";
684
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia;
685
+ * "offcentre" = a double end whose pair doesn't straddle the plate centre. */
686
+ code: "narrow" | "clearance" | "offcentre";
653
687
  /** Plain-language problem, for the author. */
654
688
  message: string;
655
689
  /** The width that would satisfy this rule, inches. */
@@ -672,7 +706,32 @@ declare function checkEndplateWidth(input: {
672
706
  widthInches?: number | null;
673
707
  config?: TrackConfig | "none" | null;
674
708
  trackOffsetInches?: number | null;
709
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). Without it a
710
+ * double end's second track was assumed to be one spacing ABOVE Main 1, so a
711
+ * swapped pair was measured on the wrong side of the plate (#190). */
712
+ main2Below?: boolean;
675
713
  }): EndplateWidthIssue[];
714
+ /**
715
+ * Whether a module presents NO far endplate — one conforming face only.
716
+ *
717
+ * ⭐ THE single definition. Three places had grown their own answer to this and
718
+ * drifted: `deriveEndplatePoses` (which knew about `dead_end` and `"none"`), the
719
+ * operations preview's end label, and `moduleFootprint`'s endplate faces — the
720
+ * last two using "is it a loop?" as a stand-in for "has it got two ends?", which
721
+ * was true until #184 and silently wrong after. Anything asking this must call
722
+ * THIS.
723
+ *
724
+ * Three ways to have one end, and they are genuinely different things: the
725
+ * geometry is a dead end; it's a balloon loop, whose far end is the throat and
726
+ * not a plate at all; or the owner said this end has no plate, which is how an
727
+ * *end of the line* or a *pocket* is authored — ordinary straight boards that
728
+ * simply stop, so no geometry type would ever say it for them.
729
+ */
730
+ declare function hasNoFarEndplate(input: {
731
+ geometryType?: string | null;
732
+ loop?: boolean;
733
+ endplateConfigs?: ("single" | "double" | "none" | null | undefined)[];
734
+ }): boolean;
676
735
  /**
677
736
  * Whether a doc is a balloon loop — **the authored flag, and only that**.
678
737
  *
@@ -1785,4 +1844,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1785
1844
  heading: number;
1786
1845
  }>;
1787
1846
 
1788
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1847
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
package/dist/index.js CHANGED
@@ -469,8 +469,12 @@ function moduleFootprint(input) {
469
469
  const widthA = endplateWidthFor(input.endplateWidths, "A");
470
470
  const widthB = endplateWidthFor(input.endplateWidths, "B");
471
471
  const authored = benchworkOutline(input);
472
- const offA = input.endplateTrackOffsets?.["A"] ?? 0;
473
- const offB = input.endplateTrackOffsets?.["B"] ?? 0;
472
+ const offOf = (i, id) => input.endplateTrackOffsets?.[id] ?? endplateCentreOffsetInches({
473
+ config: input.endplateConfigs?.[i],
474
+ main2Below: input.mainsSwapped === true
475
+ });
476
+ const offA = offOf(0, "A");
477
+ const offB = offOf(1, "B");
474
478
  const secs = input.sections ?? [];
475
479
  const sectionsOwnShape = secs.length > 1 || secs.some((s) => (s.outline?.length ?? 0) >= 3);
476
480
  const sectionOutlines = sectionsOwnShape ? sectionFootprints(input, {
@@ -483,9 +487,10 @@ function moduleFootprint(input) {
483
487
  return {
484
488
  centerline,
485
489
  band: benchworkBand(centerline, widthA, widthB, offA, offB),
486
- // A loop's centre-line ends at the throat, not a far endplate — keep only
487
- // endplate A's face (the far face would be a spurious plate at the throat).
488
- endplateFaces: input.loop ? endplateFaceSegments(centerline, widthA, widthB, offA, offB).slice(0, 1) : endplateFaceSegments(centerline, widthA, widthB, offA, offB),
490
+ // Only emit a face where the module actually presents one. A loop's
491
+ // centre-line ends at the THROAT, and an end of the line / pocket simply
492
+ // stops a far face there is a plate the module hasn't got (#191).
493
+ endplateFaces: hasNoFarEndplate(input) ? endplateFaceSegments(centerline, widthA, widthB, offA, offB).slice(0, 1) : endplateFaceSegments(centerline, widthA, widthB, offA, offB),
489
494
  outline: sectionOutlines.length || !authored ? null : sampleBenchworkOutline(authored),
490
495
  // The donut hole, arc-sampled — only when there's a solid outline to punch it
491
496
  // out of (a sectioned module isn't a donut). Renderers cut it from `outline`.
@@ -493,13 +498,19 @@ function moduleFootprint(input) {
493
498
  sectionOutlines
494
499
  };
495
500
  }
496
- function endplateTrackOffsetFor(config, authoredTrackOffset) {
497
- const v = -endplateTrackOffsetInches(authoredTrackOffset, config);
501
+ function endplateCentreOffsetInches(input) {
502
+ const v = -endplateTrackOffsetInches(
503
+ input.authoredTrackOffsetInches,
504
+ input.config ?? void 0,
505
+ input.main2Below
506
+ );
498
507
  return v === 0 ? 0 : v;
499
508
  }
500
- function endplateTrackOffsetInches(authored, config) {
509
+ function endplateTrackOffsetInches(authored, config, main2Below = false) {
501
510
  if (typeof authored === "number" && Number.isFinite(authored)) return authored;
502
- return config === "double" ? -FREEMO_TRACK_SPACING_INCHES / 2 : 0;
511
+ if (config !== "double") return 0;
512
+ const half = FREEMO_TRACK_SPACING_INCHES / 2;
513
+ return main2Below ? half : -half;
503
514
  }
504
515
  function checkEndplateWidth(input) {
505
516
  const width = endplateWidthInches(input);
@@ -511,8 +522,13 @@ function checkEndplateWidth(input) {
511
522
  requiredInches: FREEMO_ENDPLATE_WIDTH_MIN_INCHES
512
523
  });
513
524
  }
514
- const off = endplateTrackOffsetInches(input.trackOffsetInches, input.config ?? void 0);
515
- const centres = input.config === "double" ? [off, off + FREEMO_TRACK_SPACING_INCHES] : [off];
525
+ const off = endplateTrackOffsetInches(
526
+ input.trackOffsetInches,
527
+ input.config ?? void 0,
528
+ input.main2Below
529
+ );
530
+ const second = off + (input.main2Below ? -1 : 1) * FREEMO_TRACK_SPACING_INCHES;
531
+ const centres = input.config === "double" ? [off, second] : [off];
516
532
  const worst = Math.max(...centres.map((c) => Math.abs(c)));
517
533
  const clearance = width / 2 - worst;
518
534
  if (clearance < FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES) {
@@ -523,6 +539,18 @@ function checkEndplateWidth(input) {
523
539
  requiredInches: required
524
540
  });
525
541
  }
542
+ if (input.config === "double") {
543
+ const mid = (off + second) / 2;
544
+ if (Math.abs(mid) > 0.01) {
545
+ issues.push({
546
+ code: "offcentre",
547
+ message: `The two tracks sit ${round2(Math.abs(mid))}\u2033 off the centre of this endplate. The standard recommends they straddle it \u2014 Main 1 at ${round2((input.main2Below ? 1 : -1) * (FREEMO_TRACK_SPACING_INCHES / 2))}\u2033. Clear the offset to use that.`,
548
+ // Not a width problem — no wider plate fixes it — so hand back the
549
+ // width unchanged rather than imply one would.
550
+ requiredInches: width
551
+ });
552
+ }
553
+ }
526
554
  return issues;
527
555
  }
528
556
  var round2 = (n) => Math.round(n * 100) / 100;
@@ -531,6 +559,9 @@ function endplateWidthFor(widths, id) {
531
559
  const w = widths?.[id];
532
560
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
533
561
  }
562
+ function hasNoFarEndplate(input) {
563
+ return input.geometryType === "dead_end" || input.loop === true || input.endplateConfigs?.[1] === "none";
564
+ }
534
565
  function isLoopDoc(doc) {
535
566
  return doc.loop === true;
536
567
  }
@@ -1991,7 +2022,7 @@ function deriveEndplatePoses(geo) {
1991
2022
  trackOffsets: offsetsFor(cfg(0), half)
1992
2023
  })
1993
2024
  );
1994
- const noB = geo.geometryType === "dead_end" || geo.loop === true || geo.endplateConfigs?.[1] === "none";
2025
+ const noB = hasNoFarEndplate(geo);
1995
2026
  const end = sectionedEndPose({ sections: geo.sections });
1996
2027
  if (!noB && end) {
1997
2028
  poses.push(
@@ -2142,6 +2173,6 @@ function poseOverridesFromDoc(doc) {
2142
2173
  return out;
2143
2174
  }
2144
2175
 
2145
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
2176
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
2146
2177
  //# sourceMappingURL=index.js.map
2147
2178
  //# sourceMappingURL=index.js.map