@willcgage/module-schematic 0.79.0 → 0.81.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
@@ -1454,7 +1454,11 @@ interface TrackPart {
1454
1454
  /** `flex` is track sold by the length rather than as a fixed geometry — its
1455
1455
  * {@link overallLength} is the LONGEST piece you can lay from it, not a shape
1456
1456
  * (#193). */
1457
- kind: "turnout" | "wye" | "curved-turnout" | "crossing" | "flex";
1457
+ /** ⚠️ `crossover` is an ASSEMBLY, not a single turnout two turnouts plus the
1458
+ * diagonal between two parallel tracks, sold as one fixture. It therefore
1459
+ * carries {@link trackSpacing}, which no single turnout has, and its
1460
+ * `frogNumber` describes the two turnouts in it. */
1461
+ kind: "turnout" | "wye" | "curved-turnout" | "crossover" | "crossing" | "flex";
1458
1462
  /** Manufacturer part numbers by hand, where the part has a hand. */
1459
1463
  partNumbers?: {
1460
1464
  left?: string;
@@ -1484,8 +1488,27 @@ interface TrackPart {
1484
1488
  * prefer these when both are known: they're what someone can actually put a
1485
1489
  * rule against. */
1486
1490
  frogOffset?: PartDimension;
1487
- /** End-to-end length of the part. */
1491
+ /** End-to-end length of the part.
1492
+ *
1493
+ * ⚠️ On a {@link buildable} part this is the manufacturer's DEFAULT length,
1494
+ * not a property of the part — the modeller cuts the rail, so their turnout
1495
+ * is whatever they built between {@link minimumLength} and as long as they
1496
+ * like. */
1488
1497
  overallLength?: PartDimension;
1498
+ /** True when the product is a FIXTURE or template rather than a finished
1499
+ * turnout — Fast Tracks sell jigs, not parts. The distinction is not cosmetic:
1500
+ * a buildable part has no single length, so `overallLength` becomes a default
1501
+ * and `minimumLength` a floor, and nothing may infer where an owner's actual
1502
+ * turnout ends from either of them. */
1503
+ buildable?: boolean;
1504
+ /** The shortest the part can be built. Buildable parts only. */
1505
+ minimumLength?: PartDimension;
1506
+ /** The radius of plain curve this turnout can stand in for, as a
1507
+ * layout-planning figure. Fast Tracks publish it; Atlas do not. Nothing draws
1508
+ * with it — it is here because it is a real published dimension and dropping
1509
+ * it would mean the library could not represent what a manufacturer actually
1510
+ * says about their own product. */
1511
+ substitutionRadius?: PartDimension;
1489
1512
  /** Frog apex → the end of the diverging rail, measured ALONG that rail. The
1490
1513
  * independent cross-check on {@link frogOffset}: it must be slightly LONGER
1491
1514
  * than the axial `overallLength − frogOffset`, because the rail is the
@@ -1502,6 +1525,20 @@ interface TrackPart {
1502
1525
  innerRadius?: PartDimension;
1503
1526
  /** Crossing angle, degrees. */
1504
1527
  crossingAngleDeg?: number;
1528
+ /** Centre-to-centre distance of the two parallel tracks a {@link kind}
1529
+ * `crossover` joins. A crossover fixture is BUILT for one spacing — it is not
1530
+ * adjustable — so this decides whether the part suits a given standard at all.
1531
+ *
1532
+ * ⚠️ READ {@link FREEMO_TRACK_SPACING_INCHES} ALONGSIDE THIS. Free-moN §2.0
1533
+ * fixes double-track spacing at exactly 1.125″; the Fast Tracks N crossovers
1534
+ * are built to 1.09″. That 0.035″ is small but it is REAL and it is not a
1535
+ * tolerance — the fixture cannot be built to another spacing. Recorded rather
1536
+ * than reconciled: it is a fact about the product, and an owner deciding what
1537
+ * to buy is better served by the true number than by a convenient one. */
1538
+ trackSpacing?: PartDimension;
1539
+ /** The SECOND frog angle on a part that has two — the diamond where the
1540
+ * diagonals of a scissors crossover cross. Published in degrees. */
1541
+ secondaryFrogAngle?: PartAngle;
1505
1542
  /** The part's drawn geometry in its own frame, when it came from a library
1506
1543
  * file. This is the payload worth importing — real outlines we can draw
1507
1544
  * instead of deriving a turnout's shape from a frog number. */
@@ -1596,6 +1633,93 @@ declare const ATLAS_CODE55_N: TrackPart[];
1596
1633
  * worse than none — someone would order against it.
1597
1634
  */
1598
1635
  declare const FLEX_TRACK_PARTS: TrackPart[];
1636
+ /**
1637
+ * Fast Tracks N-scale assembly fixtures, Micro Engineering Code 55 rail.
1638
+ *
1639
+ * ⚠️ A DIFFERENT MANUFACTURER PUBLISHES DIFFERENT NUMBERS, and the library has
1640
+ * to hold what each one actually says rather than a lowest common denominator.
1641
+ * Fast Tracks publish none of Atlas's three landmarks — no points offset, no
1642
+ * frog offset, no lead — and four things Atlas never state:
1643
+ *
1644
+ * straight angle div R default minimum substitution R
1645
+ * #4 14.04° 8″ 4.57″ 3.43″ 11″
1646
+ * #4.5 12.53° 11″ 5.00″ 3.90″ 14″
1647
+ * #5 11.31° 14″ 5.39″ 3.92″ 17″
1648
+ * #6 9.46° 23″ 6.26″ 4.30″ 24″
1649
+ * #7 8.13° 27″ 7.40″ 5.44″ 34″
1650
+ * #8 7.13° 36″ 8.00″ 5.93″ 42″
1651
+ * #9 6.34° 50″ 8.38″ 6.42″ 57″
1652
+ * #10 5.71° 64″ 9.15″ 6.80″ 70″
1653
+ * #12 4.76° 82″ 10.34″ 7.95″ 90″
1654
+ *
1655
+ * wye angle div R default minimum substitution R
1656
+ * #4 14.04° 23″ 4.63″ 3.48″ 27″
1657
+ * #5 11.31° 25″ 5.23″ 3.81″ 30″
1658
+ * #6 9.46° 35″ 5.99″ 4.30″ 47″
1659
+ * #8 7.13° 70″ 8.22″ 5.93″ 94″
1660
+ * #10 5.71° 89″ 9.47″ 6.97″ 130″
1661
+ *
1662
+ * ⭐ **FAST TRACKS BUILD TO TRUE FROG RATIOS.** Every angle above is `atan(1/N)`
1663
+ * to the digits published — 14.04, 11.31, 9.46, 8.13, 7.13, 6.34, 5.71, 4.76.
1664
+ * Atlas do NOT: they build to SECTIONAL angles (a "#5" is 11.25°, a 1/32 turn,
1665
+ * against theory's 11.310°). Two manufacturers, two different meanings for the
1666
+ * same number on the box, and the difference is real if you ever check whether
1667
+ * a part mates with sectional track.
1668
+ *
1669
+ * ⚠️ THESE ARE FIXTURES, SO THEY HAVE NO LENGTH. The modeller cuts the rail:
1670
+ * `overallLength` here is the manufacturer's DEFAULT and `minimumLength` the
1671
+ * floor. Because Fast Tracks publish no points offset, {@link partExtent}
1672
+ * returns null for every one of them, so none claims a body and flex still runs
1673
+ * through it (#193). That is the honest answer — we do not know where an
1674
+ * owner's hand-built turnout stops, and guessing from a default length would be
1675
+ * inventing a measurement. An owner who measures their own build can enter it,
1676
+ * and the admin form's cross-checks will catch a bad reading.
1677
+ *
1678
+ * Source: handlaidtrack.com product pages, "Detailed Specifications" tables,
1679
+ * read 2026-07-26. ⚠️ The PDF templates in Will's local reference folder are
1680
+ * ENCRYPTED (`/Encrypt /Standard`), so they are deliberately not the source
1681
+ * here — the public product pages say the same thing and can be cited.
1682
+ */
1683
+ declare const FAST_TRACKS_N_ME55: TrackPart[];
1684
+ /**
1685
+ * Fast Tracks N-scale CROSSOVER fixtures, ME Code 55.
1686
+ *
1687
+ * crossover angle 2nd frog default minimum track spacing
1688
+ * #6 9.46° 19° 10.07″ 9.31″ 1.09″
1689
+ * #8 7.13° 14.3° 13.61″ 13.07″ 1.09″
1690
+ *
1691
+ * ⚠️⚠️ **THE TRACK SPACING IS 1.09″, AND FREE-moN §2.0 REQUIRES 1.125″.**
1692
+ * A crossover fixture is machined for ONE spacing; it is not adjustable. So a
1693
+ * crossover hand-built on either of these puts the parallel track 0.035″ closer
1694
+ * than the standard, and that difference has to go somewhere — the tracks pinch
1695
+ * through the crossover and open back out to meet the endplates, which is fixed
1696
+ * at both ends by {@link FREEMO_TRACK_SPACING_INCHES}.
1697
+ *
1698
+ * Recorded, NOT reconciled. It is 0.9 mm and most builders will absorb it, but
1699
+ * it is a property of the product rather than a tolerance, and someone choosing
1700
+ * what to buy for a double-track Free-moN module is better served by the true
1701
+ * number than a convenient one. Nothing here warns or blocks — that would be a
1702
+ * decision for the app, not for a data table.
1703
+ *
1704
+ * ⭐ THE SECOND FROG IS EXACTLY TWICE THE FIRST — 19° against 2 × 9.46 = 18.92,
1705
+ * and 14.3° against 2 × 7.13 = 14.26, both inside the published rounding. That
1706
+ * is the diamond in a scissors crossover, where two diagonals each leaving at
1707
+ * the frog angle meet each other. It is a free cross-check on the pair, and it
1708
+ * passes.
1709
+ *
1710
+ * ⚠️ A CROSSOVER IS AN ASSEMBLY, so `partExtent` means nothing for it and the
1711
+ * turnout size lookups must never see it: `kind` is `"crossover"`, and every
1712
+ * one of them filters `kind === "turnout"`.
1713
+ *
1714
+ * Atlas make crossover parts too — Will has no figures for them as of
1715
+ * 2026-07-26, so there are deliberately no Atlas entries here rather than
1716
+ * guessed ones.
1717
+ *
1718
+ * Source: handlaidtrack.com product pages, read 2026-07-26. Their tables also
1719
+ * carry tie size and tie spacing; those describe the QuickSticks tie strip's
1720
+ * appearance rather than the track's geometry, so they are not carried here.
1721
+ */
1722
+ declare const FAST_TRACKS_N_ME55_CROSSOVERS: TrackPart[];
1599
1723
  /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1600
1724
  declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1601
1725
  /** Every built-in part, across manufacturers. */
@@ -1774,8 +1898,18 @@ declare function partExtent(part: TrackPart | null | undefined): PartExtent | nu
1774
1898
  * measured part matches EXACTLY. A #6 is not a #5 or a #7 — the nearest part's
1775
1899
  * length says nothing about it (see {@link partExtent}). */
1776
1900
  declare function partExtentForSize(size: number, library?: TrackPart[]): PartExtent | null;
1777
- /** The closest built-in turnout for a frog number — what a bare `size` maps to
1778
- * when a turnout names no part. Exact match wins; otherwise the nearest frog. */
1901
+ /**
1902
+ * The closest built-in turnout for a frog number what a bare `size` maps to
1903
+ * when a turnout names no part. Exact match wins; otherwise the nearest frog.
1904
+ *
1905
+ * ⚠️ TIES ARE BROKEN TOWARD A PART WE CAN ACTUALLY DRAW. Two parts can share a
1906
+ * frog number — Atlas sell a #5 and Fast Tracks make a #5 fixture — and only one
1907
+ * of them may carry the measured offsets {@link partExtent} needs. Picking by
1908
+ * frog number alone would let a part with no geometry win on array order and
1909
+ * take the other's body away with it, which is the #193 failure (a turnout that
1910
+ * claims no extent has flex drawn straight through it). So: nearest frog first,
1911
+ * and among equals, one with a real extent.
1912
+ */
1779
1913
  declare function turnoutPartForSize(size: number, library?: TrackPart[]): TrackPart | null;
1780
1914
  /**
1781
1915
  * The lead to draw a turnout of this size with. Keeps `frogLegOf` honest without
@@ -1903,6 +2037,15 @@ interface StoredTrackPart {
1903
2037
  overallLengthSource?: string | null;
1904
2038
  divergingLengthInches?: number | null;
1905
2039
  divergingLengthSource?: string | null;
2040
+ minimumLengthInches?: number | null;
2041
+ minimumLengthSource?: string | null;
2042
+ substitutionRadiusInches?: number | null;
2043
+ substitutionRadiusSource?: string | null;
2044
+ trackSpacingInches?: number | null;
2045
+ trackSpacingSource?: string | null;
2046
+ secondaryFrogAngleDeg?: number | null;
2047
+ secondaryFrogAngleSource?: string | null;
2048
+ buildable?: boolean | null;
1906
2049
  leadInches?: number | null;
1907
2050
  leadSource?: string | null;
1908
2051
  outerRadiusInches?: number | null;
@@ -2245,4 +2388,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2245
2388
  heading: number;
2246
2389
  }>;
2247
2390
 
2248
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, 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, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, 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 OccupiedSpan, 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 SectionEnd, type SectionEndAssessment, type SectionFootprint, type SectionJointAssessment, type SectionRelativePos, type SignalFacing, type SignalSide, type SpanOverhang, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
2391
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, 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, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, 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 OccupiedSpan, 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 SectionEnd, type SectionEndAssessment, type SectionFootprint, type SectionJointAssessment, type SectionRelativePos, type SignalFacing, type SignalSide, type SpanOverhang, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
package/dist/index.d.ts CHANGED
@@ -1454,7 +1454,11 @@ interface TrackPart {
1454
1454
  /** `flex` is track sold by the length rather than as a fixed geometry — its
1455
1455
  * {@link overallLength} is the LONGEST piece you can lay from it, not a shape
1456
1456
  * (#193). */
1457
- kind: "turnout" | "wye" | "curved-turnout" | "crossing" | "flex";
1457
+ /** ⚠️ `crossover` is an ASSEMBLY, not a single turnout two turnouts plus the
1458
+ * diagonal between two parallel tracks, sold as one fixture. It therefore
1459
+ * carries {@link trackSpacing}, which no single turnout has, and its
1460
+ * `frogNumber` describes the two turnouts in it. */
1461
+ kind: "turnout" | "wye" | "curved-turnout" | "crossover" | "crossing" | "flex";
1458
1462
  /** Manufacturer part numbers by hand, where the part has a hand. */
1459
1463
  partNumbers?: {
1460
1464
  left?: string;
@@ -1484,8 +1488,27 @@ interface TrackPart {
1484
1488
  * prefer these when both are known: they're what someone can actually put a
1485
1489
  * rule against. */
1486
1490
  frogOffset?: PartDimension;
1487
- /** End-to-end length of the part. */
1491
+ /** End-to-end length of the part.
1492
+ *
1493
+ * ⚠️ On a {@link buildable} part this is the manufacturer's DEFAULT length,
1494
+ * not a property of the part — the modeller cuts the rail, so their turnout
1495
+ * is whatever they built between {@link minimumLength} and as long as they
1496
+ * like. */
1488
1497
  overallLength?: PartDimension;
1498
+ /** True when the product is a FIXTURE or template rather than a finished
1499
+ * turnout — Fast Tracks sell jigs, not parts. The distinction is not cosmetic:
1500
+ * a buildable part has no single length, so `overallLength` becomes a default
1501
+ * and `minimumLength` a floor, and nothing may infer where an owner's actual
1502
+ * turnout ends from either of them. */
1503
+ buildable?: boolean;
1504
+ /** The shortest the part can be built. Buildable parts only. */
1505
+ minimumLength?: PartDimension;
1506
+ /** The radius of plain curve this turnout can stand in for, as a
1507
+ * layout-planning figure. Fast Tracks publish it; Atlas do not. Nothing draws
1508
+ * with it — it is here because it is a real published dimension and dropping
1509
+ * it would mean the library could not represent what a manufacturer actually
1510
+ * says about their own product. */
1511
+ substitutionRadius?: PartDimension;
1489
1512
  /** Frog apex → the end of the diverging rail, measured ALONG that rail. The
1490
1513
  * independent cross-check on {@link frogOffset}: it must be slightly LONGER
1491
1514
  * than the axial `overallLength − frogOffset`, because the rail is the
@@ -1502,6 +1525,20 @@ interface TrackPart {
1502
1525
  innerRadius?: PartDimension;
1503
1526
  /** Crossing angle, degrees. */
1504
1527
  crossingAngleDeg?: number;
1528
+ /** Centre-to-centre distance of the two parallel tracks a {@link kind}
1529
+ * `crossover` joins. A crossover fixture is BUILT for one spacing — it is not
1530
+ * adjustable — so this decides whether the part suits a given standard at all.
1531
+ *
1532
+ * ⚠️ READ {@link FREEMO_TRACK_SPACING_INCHES} ALONGSIDE THIS. Free-moN §2.0
1533
+ * fixes double-track spacing at exactly 1.125″; the Fast Tracks N crossovers
1534
+ * are built to 1.09″. That 0.035″ is small but it is REAL and it is not a
1535
+ * tolerance — the fixture cannot be built to another spacing. Recorded rather
1536
+ * than reconciled: it is a fact about the product, and an owner deciding what
1537
+ * to buy is better served by the true number than by a convenient one. */
1538
+ trackSpacing?: PartDimension;
1539
+ /** The SECOND frog angle on a part that has two — the diamond where the
1540
+ * diagonals of a scissors crossover cross. Published in degrees. */
1541
+ secondaryFrogAngle?: PartAngle;
1505
1542
  /** The part's drawn geometry in its own frame, when it came from a library
1506
1543
  * file. This is the payload worth importing — real outlines we can draw
1507
1544
  * instead of deriving a turnout's shape from a frog number. */
@@ -1596,6 +1633,93 @@ declare const ATLAS_CODE55_N: TrackPart[];
1596
1633
  * worse than none — someone would order against it.
1597
1634
  */
1598
1635
  declare const FLEX_TRACK_PARTS: TrackPart[];
1636
+ /**
1637
+ * Fast Tracks N-scale assembly fixtures, Micro Engineering Code 55 rail.
1638
+ *
1639
+ * ⚠️ A DIFFERENT MANUFACTURER PUBLISHES DIFFERENT NUMBERS, and the library has
1640
+ * to hold what each one actually says rather than a lowest common denominator.
1641
+ * Fast Tracks publish none of Atlas's three landmarks — no points offset, no
1642
+ * frog offset, no lead — and four things Atlas never state:
1643
+ *
1644
+ * straight angle div R default minimum substitution R
1645
+ * #4 14.04° 8″ 4.57″ 3.43″ 11″
1646
+ * #4.5 12.53° 11″ 5.00″ 3.90″ 14″
1647
+ * #5 11.31° 14″ 5.39″ 3.92″ 17″
1648
+ * #6 9.46° 23″ 6.26″ 4.30″ 24″
1649
+ * #7 8.13° 27″ 7.40″ 5.44″ 34″
1650
+ * #8 7.13° 36″ 8.00″ 5.93″ 42″
1651
+ * #9 6.34° 50″ 8.38″ 6.42″ 57″
1652
+ * #10 5.71° 64″ 9.15″ 6.80″ 70″
1653
+ * #12 4.76° 82″ 10.34″ 7.95″ 90″
1654
+ *
1655
+ * wye angle div R default minimum substitution R
1656
+ * #4 14.04° 23″ 4.63″ 3.48″ 27″
1657
+ * #5 11.31° 25″ 5.23″ 3.81″ 30″
1658
+ * #6 9.46° 35″ 5.99″ 4.30″ 47″
1659
+ * #8 7.13° 70″ 8.22″ 5.93″ 94″
1660
+ * #10 5.71° 89″ 9.47″ 6.97″ 130″
1661
+ *
1662
+ * ⭐ **FAST TRACKS BUILD TO TRUE FROG RATIOS.** Every angle above is `atan(1/N)`
1663
+ * to the digits published — 14.04, 11.31, 9.46, 8.13, 7.13, 6.34, 5.71, 4.76.
1664
+ * Atlas do NOT: they build to SECTIONAL angles (a "#5" is 11.25°, a 1/32 turn,
1665
+ * against theory's 11.310°). Two manufacturers, two different meanings for the
1666
+ * same number on the box, and the difference is real if you ever check whether
1667
+ * a part mates with sectional track.
1668
+ *
1669
+ * ⚠️ THESE ARE FIXTURES, SO THEY HAVE NO LENGTH. The modeller cuts the rail:
1670
+ * `overallLength` here is the manufacturer's DEFAULT and `minimumLength` the
1671
+ * floor. Because Fast Tracks publish no points offset, {@link partExtent}
1672
+ * returns null for every one of them, so none claims a body and flex still runs
1673
+ * through it (#193). That is the honest answer — we do not know where an
1674
+ * owner's hand-built turnout stops, and guessing from a default length would be
1675
+ * inventing a measurement. An owner who measures their own build can enter it,
1676
+ * and the admin form's cross-checks will catch a bad reading.
1677
+ *
1678
+ * Source: handlaidtrack.com product pages, "Detailed Specifications" tables,
1679
+ * read 2026-07-26. ⚠️ The PDF templates in Will's local reference folder are
1680
+ * ENCRYPTED (`/Encrypt /Standard`), so they are deliberately not the source
1681
+ * here — the public product pages say the same thing and can be cited.
1682
+ */
1683
+ declare const FAST_TRACKS_N_ME55: TrackPart[];
1684
+ /**
1685
+ * Fast Tracks N-scale CROSSOVER fixtures, ME Code 55.
1686
+ *
1687
+ * crossover angle 2nd frog default minimum track spacing
1688
+ * #6 9.46° 19° 10.07″ 9.31″ 1.09″
1689
+ * #8 7.13° 14.3° 13.61″ 13.07″ 1.09″
1690
+ *
1691
+ * ⚠️⚠️ **THE TRACK SPACING IS 1.09″, AND FREE-moN §2.0 REQUIRES 1.125″.**
1692
+ * A crossover fixture is machined for ONE spacing; it is not adjustable. So a
1693
+ * crossover hand-built on either of these puts the parallel track 0.035″ closer
1694
+ * than the standard, and that difference has to go somewhere — the tracks pinch
1695
+ * through the crossover and open back out to meet the endplates, which is fixed
1696
+ * at both ends by {@link FREEMO_TRACK_SPACING_INCHES}.
1697
+ *
1698
+ * Recorded, NOT reconciled. It is 0.9 mm and most builders will absorb it, but
1699
+ * it is a property of the product rather than a tolerance, and someone choosing
1700
+ * what to buy for a double-track Free-moN module is better served by the true
1701
+ * number than a convenient one. Nothing here warns or blocks — that would be a
1702
+ * decision for the app, not for a data table.
1703
+ *
1704
+ * ⭐ THE SECOND FROG IS EXACTLY TWICE THE FIRST — 19° against 2 × 9.46 = 18.92,
1705
+ * and 14.3° against 2 × 7.13 = 14.26, both inside the published rounding. That
1706
+ * is the diamond in a scissors crossover, where two diagonals each leaving at
1707
+ * the frog angle meet each other. It is a free cross-check on the pair, and it
1708
+ * passes.
1709
+ *
1710
+ * ⚠️ A CROSSOVER IS AN ASSEMBLY, so `partExtent` means nothing for it and the
1711
+ * turnout size lookups must never see it: `kind` is `"crossover"`, and every
1712
+ * one of them filters `kind === "turnout"`.
1713
+ *
1714
+ * Atlas make crossover parts too — Will has no figures for them as of
1715
+ * 2026-07-26, so there are deliberately no Atlas entries here rather than
1716
+ * guessed ones.
1717
+ *
1718
+ * Source: handlaidtrack.com product pages, read 2026-07-26. Their tables also
1719
+ * carry tie size and tie spacing; those describe the QuickSticks tie strip's
1720
+ * appearance rather than the track's geometry, so they are not carried here.
1721
+ */
1722
+ declare const FAST_TRACKS_N_ME55_CROSSOVERS: TrackPart[];
1599
1723
  /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1600
1724
  declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1601
1725
  /** Every built-in part, across manufacturers. */
@@ -1774,8 +1898,18 @@ declare function partExtent(part: TrackPart | null | undefined): PartExtent | nu
1774
1898
  * measured part matches EXACTLY. A #6 is not a #5 or a #7 — the nearest part's
1775
1899
  * length says nothing about it (see {@link partExtent}). */
1776
1900
  declare function partExtentForSize(size: number, library?: TrackPart[]): PartExtent | null;
1777
- /** The closest built-in turnout for a frog number — what a bare `size` maps to
1778
- * when a turnout names no part. Exact match wins; otherwise the nearest frog. */
1901
+ /**
1902
+ * The closest built-in turnout for a frog number what a bare `size` maps to
1903
+ * when a turnout names no part. Exact match wins; otherwise the nearest frog.
1904
+ *
1905
+ * ⚠️ TIES ARE BROKEN TOWARD A PART WE CAN ACTUALLY DRAW. Two parts can share a
1906
+ * frog number — Atlas sell a #5 and Fast Tracks make a #5 fixture — and only one
1907
+ * of them may carry the measured offsets {@link partExtent} needs. Picking by
1908
+ * frog number alone would let a part with no geometry win on array order and
1909
+ * take the other's body away with it, which is the #193 failure (a turnout that
1910
+ * claims no extent has flex drawn straight through it). So: nearest frog first,
1911
+ * and among equals, one with a real extent.
1912
+ */
1779
1913
  declare function turnoutPartForSize(size: number, library?: TrackPart[]): TrackPart | null;
1780
1914
  /**
1781
1915
  * The lead to draw a turnout of this size with. Keeps `frogLegOf` honest without
@@ -1903,6 +2037,15 @@ interface StoredTrackPart {
1903
2037
  overallLengthSource?: string | null;
1904
2038
  divergingLengthInches?: number | null;
1905
2039
  divergingLengthSource?: string | null;
2040
+ minimumLengthInches?: number | null;
2041
+ minimumLengthSource?: string | null;
2042
+ substitutionRadiusInches?: number | null;
2043
+ substitutionRadiusSource?: string | null;
2044
+ trackSpacingInches?: number | null;
2045
+ trackSpacingSource?: string | null;
2046
+ secondaryFrogAngleDeg?: number | null;
2047
+ secondaryFrogAngleSource?: string | null;
2048
+ buildable?: boolean | null;
1906
2049
  leadInches?: number | null;
1907
2050
  leadSource?: string | null;
1908
2051
  outerRadiusInches?: number | null;
@@ -2245,4 +2388,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2245
2388
  heading: number;
2246
2389
  }>;
2247
2390
 
2248
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, 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, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, 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 OccupiedSpan, 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 SectionEnd, type SectionEndAssessment, type SectionFootprint, type SectionJointAssessment, type SectionRelativePos, type SignalFacing, type SignalSide, type SpanOverhang, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
2391
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, 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, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, 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 OccupiedSpan, 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 SectionEnd, type SectionEndAssessment, type SectionFootprint, type SectionJointAssessment, type SectionRelativePos, type SignalFacing, type SignalSide, type SpanOverhang, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
package/dist/index.js CHANGED
@@ -1526,8 +1526,97 @@ var FLEX_TRACK_PARTS = [
1526
1526
  }
1527
1527
  }
1528
1528
  ];
1529
+ var FAST_TRACKS_N_ME55 = [
1530
+ // [kind, N, angle°, divergingR, defaultLength, minLength, substitutionR]
1531
+ ["turnout", 4, 14.04, 8, 4.57, 3.43, 11],
1532
+ ["turnout", 4.5, 12.53, 11, 5, 3.9, 14],
1533
+ ["turnout", 5, 11.31, 14, 5.39, 3.92, 17],
1534
+ ["turnout", 6, 9.46, 23, 6.26, 4.3, 24],
1535
+ ["turnout", 7, 8.13, 27, 7.4, 5.44, 34],
1536
+ ["turnout", 8, 7.13, 36, 8, 5.93, 42],
1537
+ ["turnout", 9, 6.34, 50, 8.38, 6.42, 57],
1538
+ ["turnout", 10, 5.71, 64, 9.15, 6.8, 70],
1539
+ ["turnout", 12, 4.76, 82, 10.34, 7.95, 90],
1540
+ ["wye", 4, 14.04, 23, 4.63, 3.48, 27],
1541
+ ["wye", 5, 11.31, 25, 5.23, 3.81, 30],
1542
+ ["wye", 6, 9.46, 35, 5.99, 4.3, 47],
1543
+ ["wye", 8, 7.13, 70, 8.22, 5.93, 94],
1544
+ ["wye", 10, 5.71, 89, 9.47, 6.97, 130]
1545
+ ].map(([kind, n, deg, divR, dflt, min, subR]) => {
1546
+ const code = kind === "wye" ? "y" : "t";
1547
+ const spec = "handlaidtrack.com Detailed Specifications, read 2026-07-26";
1548
+ const manufacturer = "manufacturer";
1549
+ return {
1550
+ id: `fast-tracks-n-me55-${code}-${n}`,
1551
+ manufacturer: "Fast Tracks",
1552
+ line: "Code 55",
1553
+ scale: "N",
1554
+ // Named for what the OWNER has, not what Fast Tracks sell. They sell a jig;
1555
+ // the person picking this in the editor is holding the turnout they built
1556
+ // on it. `buildable` and the AF-… part number carry the fixture fact.
1557
+ name: `#${n} ${kind === "wye" ? "Wye" : "Turnout"}`,
1558
+ kind,
1559
+ partNumbers: { single: `AF-N-${code.toUpperCase()}-${n}-ME55` },
1560
+ frogNumber: n,
1561
+ buildable: true,
1562
+ actualAngle: {
1563
+ deg,
1564
+ source: manufacturer,
1565
+ note: `${spec}. Matches atan(1/${n}) to the published digits \u2014 Fast Tracks build to TRUE frog ratios, unlike Atlas's sectional angles.`
1566
+ },
1567
+ divergingRadius: { inches: divR, source: manufacturer, note: spec },
1568
+ overallLength: {
1569
+ inches: dflt,
1570
+ source: manufacturer,
1571
+ note: `${spec}. The DEFAULT length \u2014 this is a fixture, so the builder chooses.`
1572
+ },
1573
+ minimumLength: { inches: min, source: manufacturer, note: spec },
1574
+ substitutionRadius: { inches: subR, source: manufacturer, note: spec }
1575
+ };
1576
+ });
1577
+ var FAST_TRACKS_N_ME55_CROSSOVERS = [
1578
+ // [N, angle°, secondFrog°, defaultLength, minLength, trackSpacing]
1579
+ [6, 9.46, 19, 10.07, 9.31, 1.09],
1580
+ [8, 7.13, 14.3, 13.61, 13.07, 1.09]
1581
+ ].map(([n, deg, second, dflt, min, spacing]) => {
1582
+ const spec = "handlaidtrack.com Detailed Specifications, read 2026-07-26";
1583
+ const manufacturer = "manufacturer";
1584
+ return {
1585
+ id: `fast-tracks-n-me55-c-${n}`,
1586
+ manufacturer: "Fast Tracks",
1587
+ line: "Code 55",
1588
+ scale: "N",
1589
+ name: `#${n} Crossover`,
1590
+ kind: "crossover",
1591
+ partNumbers: { single: `AF-N-C-${n}-ME55` },
1592
+ frogNumber: n,
1593
+ buildable: true,
1594
+ actualAngle: { deg, source: manufacturer, note: `${spec}. atan(1/${n}) exactly.` },
1595
+ secondaryFrogAngle: {
1596
+ deg: second,
1597
+ source: manufacturer,
1598
+ note: `${spec}. The diamond of a scissors crossover \u2014 2 \xD7 ${deg}\xB0 within the published rounding.`
1599
+ },
1600
+ overallLength: {
1601
+ inches: dflt,
1602
+ source: manufacturer,
1603
+ note: `${spec}. The DEFAULT length of the whole assembly \u2014 a fixture, so the builder chooses.`
1604
+ },
1605
+ minimumLength: { inches: min, source: manufacturer, note: spec },
1606
+ trackSpacing: {
1607
+ inches: spacing,
1608
+ source: manufacturer,
1609
+ note: `${spec}. \u26A0\uFE0F Free-moN \xA72.0 requires ${FREEMO_TRACK_SPACING_INCHES}\u2033 \u2014 this fixture is built to ${spacing}\u2033, ${(FREEMO_TRACK_SPACING_INCHES - spacing).toFixed(3)}\u2033 tighter, and cannot be built to another spacing.`
1610
+ }
1611
+ };
1612
+ });
1529
1613
  var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1530
- var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
1614
+ var BUILT_IN_TRACK_PARTS = [
1615
+ ...ATLAS_CODE55_N,
1616
+ ...FAST_TRACKS_N_ME55,
1617
+ ...FAST_TRACKS_N_ME55_CROSSOVERS,
1618
+ ...FLEX_TRACK_PARTS
1619
+ ];
1531
1620
  function flexParts(library = BUILT_IN_TRACK_PARTS) {
1532
1621
  return library.filter((p) => p.kind === "flex");
1533
1622
  }
@@ -1654,9 +1743,14 @@ function partExtentForSize(size, library = BUILT_IN_TRACK_PARTS) {
1654
1743
  function turnoutPartForSize(size, library = BUILT_IN_TRACK_PARTS) {
1655
1744
  const turnouts = library.filter((p) => p.kind === "turnout" && p.frogNumber != null);
1656
1745
  if (!turnouts.length) return null;
1657
- return turnouts.reduce(
1658
- (best, p) => Math.abs(p.frogNumber - size) < Math.abs(best.frogNumber - size) ? p : best
1659
- );
1746
+ const dist = (p) => Math.abs(p.frogNumber - size);
1747
+ return turnouts.reduce((best, p) => {
1748
+ const d = dist(p);
1749
+ const bd = dist(best);
1750
+ if (d !== bd) return d < bd ? p : best;
1751
+ if (!partExtent(best) && partExtent(p)) return p;
1752
+ return best;
1753
+ });
1660
1754
  }
1661
1755
  function measuredLeadPoints(library) {
1662
1756
  return library.filter(
@@ -1664,8 +1758,10 @@ function measuredLeadPoints(library) {
1664
1758
  ).map((p) => ({ n: p.frogNumber, lead: p.lead.inches })).sort((a, b) => a.n - b.n);
1665
1759
  }
1666
1760
  function leadInchesForSize(size, library = BUILT_IN_TRACK_PARTS) {
1667
- const part = turnoutPartForSize(size, library);
1668
- if (part?.lead && part.frogNumber === size) return part.lead.inches;
1761
+ const exact = library.find(
1762
+ (p) => p.kind === "turnout" && p.frogNumber === size && p.lead != null
1763
+ );
1764
+ if (exact) return exact.lead.inches;
1669
1765
  const pts = measuredLeadPoints(library);
1670
1766
  if (!pts.length) return size * TURNOUT_LEAD_INCHES_PER_FROG;
1671
1767
  if (pts.length === 1) return size / pts[0].n * pts[0].lead;
@@ -1842,6 +1938,19 @@ function storedPartToTrackPart(row) {
1842
1938
  if (overall) part.overallLength = overall;
1843
1939
  const diverging = dim(row.divergingLengthInches, row.divergingLengthSource);
1844
1940
  if (diverging) part.divergingLength = diverging;
1941
+ const minimum = dim(row.minimumLengthInches, row.minimumLengthSource);
1942
+ if (minimum) part.minimumLength = minimum;
1943
+ const substitution = dim(row.substitutionRadiusInches, row.substitutionRadiusSource);
1944
+ if (substitution) part.substitutionRadius = substitution;
1945
+ const spacing = dim(row.trackSpacingInches, row.trackSpacingSource);
1946
+ if (spacing) part.trackSpacing = spacing;
1947
+ if (typeof row.secondaryFrogAngleDeg === "number")
1948
+ part.secondaryFrogAngle = {
1949
+ deg: row.secondaryFrogAngleDeg,
1950
+ source: asSource(row.secondaryFrogAngleSource),
1951
+ ...note ? { note } : {}
1952
+ };
1953
+ if (row.buildable) part.buildable = true;
1845
1954
  if (lead) part.lead = lead;
1846
1955
  const outer = dim(row.outerRadiusInches, row.radiusSource);
1847
1956
  const inner = dim(row.innerRadiusInches, row.radiusSource);
@@ -2536,6 +2645,6 @@ function poseOverridesFromDoc(doc) {
2536
2645
  return out;
2537
2646
  }
2538
2647
 
2539
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FLEX_TRACK_PARTS, 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, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
2648
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, 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, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
2540
2649
  //# sourceMappingURL=index.js.map
2541
2650
  //# sourceMappingURL=index.js.map