@willcgage/module-schematic 0.78.0 → 0.80.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
@@ -1484,8 +1484,27 @@ interface TrackPart {
1484
1484
  * prefer these when both are known: they're what someone can actually put a
1485
1485
  * rule against. */
1486
1486
  frogOffset?: PartDimension;
1487
- /** End-to-end length of the part. */
1487
+ /** End-to-end length of the part.
1488
+ *
1489
+ * ⚠️ On a {@link buildable} part this is the manufacturer's DEFAULT length,
1490
+ * not a property of the part — the modeller cuts the rail, so their turnout
1491
+ * is whatever they built between {@link minimumLength} and as long as they
1492
+ * like. */
1488
1493
  overallLength?: PartDimension;
1494
+ /** True when the product is a FIXTURE or template rather than a finished
1495
+ * turnout — Fast Tracks sell jigs, not parts. The distinction is not cosmetic:
1496
+ * a buildable part has no single length, so `overallLength` becomes a default
1497
+ * and `minimumLength` a floor, and nothing may infer where an owner's actual
1498
+ * turnout ends from either of them. */
1499
+ buildable?: boolean;
1500
+ /** The shortest the part can be built. Buildable parts only. */
1501
+ minimumLength?: PartDimension;
1502
+ /** The radius of plain curve this turnout can stand in for, as a
1503
+ * layout-planning figure. Fast Tracks publish it; Atlas do not. Nothing draws
1504
+ * with it — it is here because it is a real published dimension and dropping
1505
+ * it would mean the library could not represent what a manufacturer actually
1506
+ * says about their own product. */
1507
+ substitutionRadius?: PartDimension;
1489
1508
  /** Frog apex → the end of the diverging rail, measured ALONG that rail. The
1490
1509
  * independent cross-check on {@link frogOffset}: it must be slightly LONGER
1491
1510
  * than the axial `overallLength − frogOffset`, because the rail is the
@@ -1596,6 +1615,54 @@ declare const ATLAS_CODE55_N: TrackPart[];
1596
1615
  * worse than none — someone would order against it.
1597
1616
  */
1598
1617
  declare const FLEX_TRACK_PARTS: TrackPart[];
1618
+ /**
1619
+ * Fast Tracks N-scale assembly fixtures, Micro Engineering Code 55 rail.
1620
+ *
1621
+ * ⚠️ A DIFFERENT MANUFACTURER PUBLISHES DIFFERENT NUMBERS, and the library has
1622
+ * to hold what each one actually says rather than a lowest common denominator.
1623
+ * Fast Tracks publish none of Atlas's three landmarks — no points offset, no
1624
+ * frog offset, no lead — and four things Atlas never state:
1625
+ *
1626
+ * straight angle div R default minimum substitution R
1627
+ * #4 14.04° 8″ 4.57″ 3.43″ 11″
1628
+ * #4.5 12.53° 11″ 5.00″ 3.90″ 14″
1629
+ * #5 11.31° 14″ 5.39″ 3.92″ 17″
1630
+ * #6 9.46° 23″ 6.26″ 4.30″ 24″
1631
+ * #7 8.13° 27″ 7.40″ 5.44″ 34″
1632
+ * #8 7.13° 36″ 8.00″ 5.93″ 42″
1633
+ * #9 6.34° 50″ 8.38″ 6.42″ 57″
1634
+ * #10 5.71° 64″ 9.15″ 6.80″ 70″
1635
+ * #12 4.76° 82″ 10.34″ 7.95″ 90″
1636
+ *
1637
+ * wye angle div R default minimum substitution R
1638
+ * #4 14.04° 23″ 4.63″ 3.48″ 27″
1639
+ * #5 11.31° 25″ 5.23″ 3.81″ 30″
1640
+ * #6 9.46° 35″ 5.99″ 4.30″ 47″
1641
+ * #8 7.13° 70″ 8.22″ 5.93″ 94″
1642
+ * #10 5.71° 89″ 9.47″ 6.97″ 130″
1643
+ *
1644
+ * ⭐ **FAST TRACKS BUILD TO TRUE FROG RATIOS.** Every angle above is `atan(1/N)`
1645
+ * to the digits published — 14.04, 11.31, 9.46, 8.13, 7.13, 6.34, 5.71, 4.76.
1646
+ * Atlas do NOT: they build to SECTIONAL angles (a "#5" is 11.25°, a 1/32 turn,
1647
+ * against theory's 11.310°). Two manufacturers, two different meanings for the
1648
+ * same number on the box, and the difference is real if you ever check whether
1649
+ * a part mates with sectional track.
1650
+ *
1651
+ * ⚠️ THESE ARE FIXTURES, SO THEY HAVE NO LENGTH. The modeller cuts the rail:
1652
+ * `overallLength` here is the manufacturer's DEFAULT and `minimumLength` the
1653
+ * floor. Because Fast Tracks publish no points offset, {@link partExtent}
1654
+ * returns null for every one of them, so none claims a body and flex still runs
1655
+ * through it (#193). That is the honest answer — we do not know where an
1656
+ * owner's hand-built turnout stops, and guessing from a default length would be
1657
+ * inventing a measurement. An owner who measures their own build can enter it,
1658
+ * and the admin form's cross-checks will catch a bad reading.
1659
+ *
1660
+ * Source: handlaidtrack.com product pages, "Detailed Specifications" tables,
1661
+ * read 2026-07-26. ⚠️ The PDF templates in Will's local reference folder are
1662
+ * ENCRYPTED (`/Encrypt /Standard`), so they are deliberately not the source
1663
+ * here — the public product pages say the same thing and can be cited.
1664
+ */
1665
+ declare const FAST_TRACKS_N_ME55: TrackPart[];
1599
1666
  /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1600
1667
  declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1601
1668
  /** Every built-in part, across manufacturers. */
@@ -1774,8 +1841,18 @@ declare function partExtent(part: TrackPart | null | undefined): PartExtent | nu
1774
1841
  * measured part matches EXACTLY. A #6 is not a #5 or a #7 — the nearest part's
1775
1842
  * length says nothing about it (see {@link partExtent}). */
1776
1843
  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. */
1844
+ /**
1845
+ * The closest built-in turnout for a frog number what a bare `size` maps to
1846
+ * when a turnout names no part. Exact match wins; otherwise the nearest frog.
1847
+ *
1848
+ * ⚠️ TIES ARE BROKEN TOWARD A PART WE CAN ACTUALLY DRAW. Two parts can share a
1849
+ * frog number — Atlas sell a #5 and Fast Tracks make a #5 fixture — and only one
1850
+ * of them may carry the measured offsets {@link partExtent} needs. Picking by
1851
+ * frog number alone would let a part with no geometry win on array order and
1852
+ * take the other's body away with it, which is the #193 failure (a turnout that
1853
+ * claims no extent has flex drawn straight through it). So: nearest frog first,
1854
+ * and among equals, one with a real extent.
1855
+ */
1779
1856
  declare function turnoutPartForSize(size: number, library?: TrackPart[]): TrackPart | null;
1780
1857
  /**
1781
1858
  * The lead to draw a turnout of this size with. Keeps `frogLegOf` honest without
@@ -1901,6 +1978,13 @@ interface StoredTrackPart {
1901
1978
  frogOffsetSource?: string | null;
1902
1979
  overallLengthInches?: number | null;
1903
1980
  overallLengthSource?: string | null;
1981
+ divergingLengthInches?: number | null;
1982
+ divergingLengthSource?: string | null;
1983
+ minimumLengthInches?: number | null;
1984
+ minimumLengthSource?: string | null;
1985
+ substitutionRadiusInches?: number | null;
1986
+ substitutionRadiusSource?: string | null;
1987
+ buildable?: boolean | null;
1904
1988
  leadInches?: number | null;
1905
1989
  leadSource?: string | null;
1906
1990
  outerRadiusInches?: number | null;
@@ -1934,6 +2018,17 @@ declare function storedPartToTrackPart(row: StoredTrackPart): TrackPart;
1934
2018
  *
1935
2019
  * The built-ins remain the floor: a part nobody has stored still resolves, so
1936
2020
  * geometry keeps working with no database at all.
2021
+ *
2022
+ * ⛔ THE TRAP THIS CREATES, and it has already been sprung once. Replacement is
2023
+ * WHOLESALE, so a stored row that is merely INCOMPLETE deletes every dimension
2024
+ * the built-in had. When both Atlas wyes were measured in 0.78.0 their stored
2025
+ * rows still held null offsets from the original seed — so in production the
2026
+ * new measurements were invisible and the 2056 still carried a `derived` lead
2027
+ * the release had just retired. Nothing failed; the parts simply had no
2028
+ * dimensions, which is indistinguishable from never having measured them.
2029
+ *
2030
+ * **Measuring a built-in is therefore not finished until the stored row is
2031
+ * updated too.** Adding a dimension here and stopping ships nothing.
1937
2032
  */
1938
2033
  declare function mergeStoredParts(stored: StoredTrackPart[], library?: TrackPart[]): TrackPart[];
1939
2034
  /**
@@ -2232,4 +2327,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2232
2327
  heading: number;
2233
2328
  }>;
2234
2329
 
2235
- 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 };
2330
+ 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, 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
@@ -1484,8 +1484,27 @@ interface TrackPart {
1484
1484
  * prefer these when both are known: they're what someone can actually put a
1485
1485
  * rule against. */
1486
1486
  frogOffset?: PartDimension;
1487
- /** End-to-end length of the part. */
1487
+ /** End-to-end length of the part.
1488
+ *
1489
+ * ⚠️ On a {@link buildable} part this is the manufacturer's DEFAULT length,
1490
+ * not a property of the part — the modeller cuts the rail, so their turnout
1491
+ * is whatever they built between {@link minimumLength} and as long as they
1492
+ * like. */
1488
1493
  overallLength?: PartDimension;
1494
+ /** True when the product is a FIXTURE or template rather than a finished
1495
+ * turnout — Fast Tracks sell jigs, not parts. The distinction is not cosmetic:
1496
+ * a buildable part has no single length, so `overallLength` becomes a default
1497
+ * and `minimumLength` a floor, and nothing may infer where an owner's actual
1498
+ * turnout ends from either of them. */
1499
+ buildable?: boolean;
1500
+ /** The shortest the part can be built. Buildable parts only. */
1501
+ minimumLength?: PartDimension;
1502
+ /** The radius of plain curve this turnout can stand in for, as a
1503
+ * layout-planning figure. Fast Tracks publish it; Atlas do not. Nothing draws
1504
+ * with it — it is here because it is a real published dimension and dropping
1505
+ * it would mean the library could not represent what a manufacturer actually
1506
+ * says about their own product. */
1507
+ substitutionRadius?: PartDimension;
1489
1508
  /** Frog apex → the end of the diverging rail, measured ALONG that rail. The
1490
1509
  * independent cross-check on {@link frogOffset}: it must be slightly LONGER
1491
1510
  * than the axial `overallLength − frogOffset`, because the rail is the
@@ -1596,6 +1615,54 @@ declare const ATLAS_CODE55_N: TrackPart[];
1596
1615
  * worse than none — someone would order against it.
1597
1616
  */
1598
1617
  declare const FLEX_TRACK_PARTS: TrackPart[];
1618
+ /**
1619
+ * Fast Tracks N-scale assembly fixtures, Micro Engineering Code 55 rail.
1620
+ *
1621
+ * ⚠️ A DIFFERENT MANUFACTURER PUBLISHES DIFFERENT NUMBERS, and the library has
1622
+ * to hold what each one actually says rather than a lowest common denominator.
1623
+ * Fast Tracks publish none of Atlas's three landmarks — no points offset, no
1624
+ * frog offset, no lead — and four things Atlas never state:
1625
+ *
1626
+ * straight angle div R default minimum substitution R
1627
+ * #4 14.04° 8″ 4.57″ 3.43″ 11″
1628
+ * #4.5 12.53° 11″ 5.00″ 3.90″ 14″
1629
+ * #5 11.31° 14″ 5.39″ 3.92″ 17″
1630
+ * #6 9.46° 23″ 6.26″ 4.30″ 24″
1631
+ * #7 8.13° 27″ 7.40″ 5.44″ 34″
1632
+ * #8 7.13° 36″ 8.00″ 5.93″ 42″
1633
+ * #9 6.34° 50″ 8.38″ 6.42″ 57″
1634
+ * #10 5.71° 64″ 9.15″ 6.80″ 70″
1635
+ * #12 4.76° 82″ 10.34″ 7.95″ 90″
1636
+ *
1637
+ * wye angle div R default minimum substitution R
1638
+ * #4 14.04° 23″ 4.63″ 3.48″ 27″
1639
+ * #5 11.31° 25″ 5.23″ 3.81″ 30″
1640
+ * #6 9.46° 35″ 5.99″ 4.30″ 47″
1641
+ * #8 7.13° 70″ 8.22″ 5.93″ 94″
1642
+ * #10 5.71° 89″ 9.47″ 6.97″ 130″
1643
+ *
1644
+ * ⭐ **FAST TRACKS BUILD TO TRUE FROG RATIOS.** Every angle above is `atan(1/N)`
1645
+ * to the digits published — 14.04, 11.31, 9.46, 8.13, 7.13, 6.34, 5.71, 4.76.
1646
+ * Atlas do NOT: they build to SECTIONAL angles (a "#5" is 11.25°, a 1/32 turn,
1647
+ * against theory's 11.310°). Two manufacturers, two different meanings for the
1648
+ * same number on the box, and the difference is real if you ever check whether
1649
+ * a part mates with sectional track.
1650
+ *
1651
+ * ⚠️ THESE ARE FIXTURES, SO THEY HAVE NO LENGTH. The modeller cuts the rail:
1652
+ * `overallLength` here is the manufacturer's DEFAULT and `minimumLength` the
1653
+ * floor. Because Fast Tracks publish no points offset, {@link partExtent}
1654
+ * returns null for every one of them, so none claims a body and flex still runs
1655
+ * through it (#193). That is the honest answer — we do not know where an
1656
+ * owner's hand-built turnout stops, and guessing from a default length would be
1657
+ * inventing a measurement. An owner who measures their own build can enter it,
1658
+ * and the admin form's cross-checks will catch a bad reading.
1659
+ *
1660
+ * Source: handlaidtrack.com product pages, "Detailed Specifications" tables,
1661
+ * read 2026-07-26. ⚠️ The PDF templates in Will's local reference folder are
1662
+ * ENCRYPTED (`/Encrypt /Standard`), so they are deliberately not the source
1663
+ * here — the public product pages say the same thing and can be cited.
1664
+ */
1665
+ declare const FAST_TRACKS_N_ME55: TrackPart[];
1599
1666
  /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1600
1667
  declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1601
1668
  /** Every built-in part, across manufacturers. */
@@ -1774,8 +1841,18 @@ declare function partExtent(part: TrackPart | null | undefined): PartExtent | nu
1774
1841
  * measured part matches EXACTLY. A #6 is not a #5 or a #7 — the nearest part's
1775
1842
  * length says nothing about it (see {@link partExtent}). */
1776
1843
  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. */
1844
+ /**
1845
+ * The closest built-in turnout for a frog number what a bare `size` maps to
1846
+ * when a turnout names no part. Exact match wins; otherwise the nearest frog.
1847
+ *
1848
+ * ⚠️ TIES ARE BROKEN TOWARD A PART WE CAN ACTUALLY DRAW. Two parts can share a
1849
+ * frog number — Atlas sell a #5 and Fast Tracks make a #5 fixture — and only one
1850
+ * of them may carry the measured offsets {@link partExtent} needs. Picking by
1851
+ * frog number alone would let a part with no geometry win on array order and
1852
+ * take the other's body away with it, which is the #193 failure (a turnout that
1853
+ * claims no extent has flex drawn straight through it). So: nearest frog first,
1854
+ * and among equals, one with a real extent.
1855
+ */
1779
1856
  declare function turnoutPartForSize(size: number, library?: TrackPart[]): TrackPart | null;
1780
1857
  /**
1781
1858
  * The lead to draw a turnout of this size with. Keeps `frogLegOf` honest without
@@ -1901,6 +1978,13 @@ interface StoredTrackPart {
1901
1978
  frogOffsetSource?: string | null;
1902
1979
  overallLengthInches?: number | null;
1903
1980
  overallLengthSource?: string | null;
1981
+ divergingLengthInches?: number | null;
1982
+ divergingLengthSource?: string | null;
1983
+ minimumLengthInches?: number | null;
1984
+ minimumLengthSource?: string | null;
1985
+ substitutionRadiusInches?: number | null;
1986
+ substitutionRadiusSource?: string | null;
1987
+ buildable?: boolean | null;
1904
1988
  leadInches?: number | null;
1905
1989
  leadSource?: string | null;
1906
1990
  outerRadiusInches?: number | null;
@@ -1934,6 +2018,17 @@ declare function storedPartToTrackPart(row: StoredTrackPart): TrackPart;
1934
2018
  *
1935
2019
  * The built-ins remain the floor: a part nobody has stored still resolves, so
1936
2020
  * geometry keeps working with no database at all.
2021
+ *
2022
+ * ⛔ THE TRAP THIS CREATES, and it has already been sprung once. Replacement is
2023
+ * WHOLESALE, so a stored row that is merely INCOMPLETE deletes every dimension
2024
+ * the built-in had. When both Atlas wyes were measured in 0.78.0 their stored
2025
+ * rows still held null offsets from the original seed — so in production the
2026
+ * new measurements were invisible and the 2056 still carried a `derived` lead
2027
+ * the release had just retired. Nothing failed; the parts simply had no
2028
+ * dimensions, which is indistinguishable from never having measured them.
2029
+ *
2030
+ * **Measuring a built-in is therefore not finished until the stored row is
2031
+ * updated too.** Adding a dimension here and stopping ships nothing.
1937
2032
  */
1938
2033
  declare function mergeStoredParts(stored: StoredTrackPart[], library?: TrackPart[]): TrackPart[];
1939
2034
  /**
@@ -2232,4 +2327,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2232
2327
  heading: number;
2233
2328
  }>;
2234
2329
 
2235
- 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 };
2330
+ 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, 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,60 @@ 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
+ });
1529
1577
  var DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1530
- var BUILT_IN_TRACK_PARTS = [...ATLAS_CODE55_N, ...FLEX_TRACK_PARTS];
1578
+ var BUILT_IN_TRACK_PARTS = [
1579
+ ...ATLAS_CODE55_N,
1580
+ ...FAST_TRACKS_N_ME55,
1581
+ ...FLEX_TRACK_PARTS
1582
+ ];
1531
1583
  function flexParts(library = BUILT_IN_TRACK_PARTS) {
1532
1584
  return library.filter((p) => p.kind === "flex");
1533
1585
  }
@@ -1654,9 +1706,14 @@ function partExtentForSize(size, library = BUILT_IN_TRACK_PARTS) {
1654
1706
  function turnoutPartForSize(size, library = BUILT_IN_TRACK_PARTS) {
1655
1707
  const turnouts = library.filter((p) => p.kind === "turnout" && p.frogNumber != null);
1656
1708
  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
- );
1709
+ const dist = (p) => Math.abs(p.frogNumber - size);
1710
+ return turnouts.reduce((best, p) => {
1711
+ const d = dist(p);
1712
+ const bd = dist(best);
1713
+ if (d !== bd) return d < bd ? p : best;
1714
+ if (!partExtent(best) && partExtent(p)) return p;
1715
+ return best;
1716
+ });
1660
1717
  }
1661
1718
  function measuredLeadPoints(library) {
1662
1719
  return library.filter(
@@ -1664,8 +1721,10 @@ function measuredLeadPoints(library) {
1664
1721
  ).map((p) => ({ n: p.frogNumber, lead: p.lead.inches })).sort((a, b) => a.n - b.n);
1665
1722
  }
1666
1723
  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;
1724
+ const exact = library.find(
1725
+ (p) => p.kind === "turnout" && p.frogNumber === size && p.lead != null
1726
+ );
1727
+ if (exact) return exact.lead.inches;
1669
1728
  const pts = measuredLeadPoints(library);
1670
1729
  if (!pts.length) return size * TURNOUT_LEAD_INCHES_PER_FROG;
1671
1730
  if (pts.length === 1) return size / pts[0].n * pts[0].lead;
@@ -1840,6 +1899,13 @@ function storedPartToTrackPart(row) {
1840
1899
  if (frog) part.frogOffset = frog;
1841
1900
  const overall = dim(row.overallLengthInches, row.overallLengthSource);
1842
1901
  if (overall) part.overallLength = overall;
1902
+ const diverging = dim(row.divergingLengthInches, row.divergingLengthSource);
1903
+ if (diverging) part.divergingLength = diverging;
1904
+ const minimum = dim(row.minimumLengthInches, row.minimumLengthSource);
1905
+ if (minimum) part.minimumLength = minimum;
1906
+ const substitution = dim(row.substitutionRadiusInches, row.substitutionRadiusSource);
1907
+ if (substitution) part.substitutionRadius = substitution;
1908
+ if (row.buildable) part.buildable = true;
1843
1909
  if (lead) part.lead = lead;
1844
1910
  const outer = dim(row.outerRadiusInches, row.radiusSource);
1845
1911
  const inner = dim(row.innerRadiusInches, row.radiusSource);
@@ -2534,6 +2600,6 @@ function poseOverridesFromDoc(doc) {
2534
2600
  return out;
2535
2601
  }
2536
2602
 
2537
- 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 };
2603
+ 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, 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 };
2538
2604
  //# sourceMappingURL=index.js.map
2539
2605
  //# sourceMappingURL=index.js.map