@willcgage/module-schematic 0.70.0 → 0.72.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
@@ -123,6 +123,14 @@ interface SchematicTrack {
123
123
  * the main centre-line + lane, as before. Physical view only; the operations
124
124
  * view stays positional (#2d-track). */
125
125
  path?: BenchworkPoint[] | null;
126
+ /** The flex product this run is laid with — a slug from the parts library
127
+ * (#193). Absent = the default. Per TRACK, so a module can have its mains in
128
+ * one product and a siding in another. */
129
+ flexPartId?: string | null;
130
+ /** Authored rail joints, inches along this run. Absent = derived from the
131
+ * product's maximum piece length. Present = these are the ONLY joints, so an
132
+ * owner's deliberate cut survives a change elsewhere on the module. */
133
+ flexCuts?: number[] | null;
126
134
  }
127
135
  interface SchematicTurnout {
128
136
  id: string;
@@ -428,10 +436,18 @@ interface ModuleFootprintInput {
428
436
  /** Where each endplate's CENTRE sits relative to the main centre-line at that
429
437
  * end, inches (signed, along the +normal). Free-moN puts a **double**-track
430
438
  * 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. */
439
+ * centre-line the plate centre is half a track spacing toward Main 2. Single
440
+ * track crosses at the centre ⇒ 0, and an off-centre track is a signed value.
441
+ * {@link endplateCentreOffsetInches} computes it from what the owner authored.
442
+ *
443
+ * ⚠️ OMITTING an end no longer means 0 — it means "use §2.0", which is a
444
+ * straddle on a double end. Centring a double plate on Main 1 drew its pair
445
+ * wholly to one side, which is what the read-only and catalog views did by
446
+ * passing nothing at all (#190). Pass an explicit number to place a plate. */
434
447
  endplateTrackOffsets?: Record<string, number>;
448
+ /** Whether Main 2 runs BELOW Main 1 (`mainsSwapped`, #131). Only affects which
449
+ * way an unauthored double plate straddles. */
450
+ mainsSwapped?: boolean;
435
451
  /** Authored benchwork outline (module-local inches), or absent for the band. */
436
452
  outline?: BenchworkPoint[] | null;
437
453
  /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
@@ -637,23 +653,45 @@ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number,
637
653
  */
638
654
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
639
655
  /**
640
- * The offset from the main centre-line to an endplate's CENTRE, inches — the
641
- * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
642
- * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
643
- * plate centre is half a track spacing up. A single track crosses at the centre.
656
+ * Where an endplate's CENTRE sits relative to MAIN 1 — the renderer's framing,
657
+ * and the one number a drawing needs to place the plate.
658
+ *
659
+ * THE single definition. Two callers had their own: the builder computed it
660
+ * inline (swap-aware), and the read-only/catalog footprint **passed nothing at
661
+ * all**, so every plate there was centred on Main 1 — a double end drew its pair
662
+ * entirely to one side of the plate instead of straddling it (#190).
663
+ *
664
+ * §2.0 puts a double end's two tracks 0.5625″ either side of the plate centre,
665
+ * so the plate centre is half a track spacing away from Main 1 — **toward Main
666
+ * 2**, which is why the swap matters: with Main 2 below, the plate centre is
667
+ * below too. An authored offset always wins (an off-centre end is legal since
668
+ * the 20220628 revision), and is given in the standard's own framing — Main 1's
669
+ * distance from the plate centre — so it comes back negated here.
644
670
  */
645
- declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
671
+ declare function endplateCentreOffsetInches(input: {
672
+ config?: TrackConfig | "none" | null;
673
+ /** Main 1's signed distance from the plate centre, as authored. */
674
+ authoredTrackOffsetInches?: number | null;
675
+ /** Whether Main 2 runs below Main 1 (the mains are swapped). */
676
+ main2Below?: boolean;
677
+ }): number;
646
678
  /**
647
679
  * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
648
680
  * the plate's CENTRE — the standard's own framing. Authored value wins; absent
649
681
  * falls back to the §2.0 recommendations: a single track centred (0), a double
650
682
  * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
651
683
  */
652
- declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
684
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined,
685
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). The pair
686
+ * straddles the plate centre either way, so Main 1 sits on the side AWAY from
687
+ * Main 2 — low when Main 2 is high, high when it's low. Omitting this
688
+ * hard-coded "Main 2 is above", which is the assumption behind #190. */
689
+ main2Below?: boolean): number;
653
690
  /** A Free-moN conformance problem with an endplate's width/track placement. */
654
691
  interface EndplateWidthIssue {
655
- /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
656
- code: "narrow" | "clearance";
692
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia;
693
+ * "offcentre" = a double end whose pair doesn't straddle the plate centre. */
694
+ code: "narrow" | "clearance" | "offcentre";
657
695
  /** Plain-language problem, for the author. */
658
696
  message: string;
659
697
  /** The width that would satisfy this rule, inches. */
@@ -676,6 +714,10 @@ declare function checkEndplateWidth(input: {
676
714
  widthInches?: number | null;
677
715
  config?: TrackConfig | "none" | null;
678
716
  trackOffsetInches?: number | null;
717
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). Without it a
718
+ * double end's second track was assumed to be one spacing ABOVE Main 1, so a
719
+ * swapped pair was measured on the wrong side of the plate (#190). */
720
+ main2Below?: boolean;
679
721
  }): EndplateWidthIssue[];
680
722
  /**
681
723
  * Whether a module presents NO far endplate — one conforming face only.
@@ -863,6 +905,19 @@ interface EditorState {
863
905
  y: number;
864
906
  heading: number;
865
907
  }>;
908
+ /**
909
+ * Flex track settings by TRACK id (#193) — which product a run is laid with,
910
+ * and where the owner has decided its rail joints fall.
911
+ *
912
+ * One map rather than fields, because the MAINS aren't `extraTracks` (the main
913
+ * IS the centre-line), so per-track settings would otherwise need a separate
914
+ * pair of fields for Main 1 and Main 2 — four ways to say one thing. Round-trips
915
+ * onto each track in the doc, where it's self-describing for Free-Dispatcher.
916
+ */
917
+ flexByTrack: Record<string, {
918
+ partId?: string;
919
+ cuts?: number[];
920
+ }>;
866
921
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
867
922
  * 24″ recommended). Absent id = the recommended default. */
868
923
  endplateWidths: Record<string, number>;
@@ -1208,7 +1263,10 @@ interface TrackPart {
1208
1263
  line: string;
1209
1264
  scale: "N";
1210
1265
  name: string;
1211
- kind: "turnout" | "wye" | "curved-turnout" | "crossing";
1266
+ /** `flex` is track sold by the length rather than as a fixed geometry — its
1267
+ * {@link overallLength} is the LONGEST piece you can lay from it, not a shape
1268
+ * (#193). */
1269
+ kind: "turnout" | "wye" | "curved-turnout" | "crossing" | "flex";
1212
1270
  /** Manufacturer part numbers by hand, where the part has a hand. */
1213
1271
  partNumbers?: {
1214
1272
  left?: string;
@@ -1316,10 +1374,164 @@ declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
1316
1374
  * everything else is a tooling decision. **Measure the part. Do not model it.**
1317
1375
  */
1318
1376
  declare const ATLAS_CODE55_N: TrackPart[];
1377
+ /**
1378
+ * Flex track — the stuff every run that isn't a turnout or a crossing is made
1379
+ * of (#193). A length of flex has no fixed geometry, so the only dimension it
1380
+ * carries is **how long a piece you can lay from one**: past that you have
1381
+ * another length, with a rail joint between them.
1382
+ *
1383
+ * ⚠️ These lengths are the NOMINAL PRODUCT LENGTHS as reported by Will Gage
1384
+ * (2026-07-26), not values read off a spec sheet or measured. They are what the
1385
+ * product is sold as — "30 inch flex track" — which is exactly the number a
1386
+ * builder plans cuts against, so they're recorded as `manufacturer` with the
1387
+ * source named. Correct them if a listing says otherwise.
1388
+ *
1389
+ * No part numbers here: I don't have them confirmed, and a wrong part number is
1390
+ * worse than none — someone would order against it.
1391
+ */
1392
+ declare const FLEX_TRACK_PARTS: TrackPart[];
1393
+ /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1394
+ declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1319
1395
  /** Every built-in part, across manufacturers. */
1320
1396
  declare const BUILT_IN_TRACK_PARTS: TrackPart[];
1397
+ /** Every flex product a track can be laid with. */
1398
+ declare function flexParts(library?: TrackPart[]): TrackPart[];
1399
+ /**
1400
+ * The flex product a track is laid with, falling back to the default rather
1401
+ * than to nothing: every non-turnout inch of track IS some product, and a run
1402
+ * whose owner hasn't chosen still gets cut into buyable lengths.
1403
+ */
1404
+ declare function flexPartFor(id: string | null | undefined, library?: TrackPart[]): TrackPart | null;
1405
+ /** How long a single piece of this product can be, inches. */
1406
+ declare function maxFlexPieceInches(id: string | null | undefined, library?: TrackPart[]): number;
1321
1407
  /** Look a part up by its slug. */
1322
1408
  declare function trackPart(id: string, library?: TrackPart[]): TrackPart | null;
1409
+ /** One length of flex track in a run — a real object you could pick up (#193). */
1410
+ interface FlexPiece {
1411
+ /** Position in the run, west to east. Stable enough to select and label by. */
1412
+ index: number;
1413
+ /** Inches along the RUN, not the module — the same coordinate turnouts,
1414
+ * industries and signals use, so a piece can be placed without a second
1415
+ * geometry to keep in step. */
1416
+ fromPos: number;
1417
+ toPos: number;
1418
+ lengthInches: number;
1419
+ /** Longer than the product allows. Only reachable from AUTHORED cuts, or from
1420
+ * a run that grew after they were authored — which is precisely when someone
1421
+ * needs telling. */
1422
+ overlong: boolean;
1423
+ /** What this piece butts against at each end: another piece, a part (turnout
1424
+ * or crossing), or the end of the run. */
1425
+ fromEnd: FlexPieceEnd;
1426
+ toEnd: FlexPieceEnd;
1427
+ }
1428
+ /** What a flex piece meets at one of its ends. */
1429
+ type FlexPieceEnd = "piece" | "part" | "runEnd";
1430
+ /**
1431
+ * A stretch of a run that is NOT flex — a turnout's or a crossing's own body.
1432
+ *
1433
+ * A **zero-length** span is meaningful: it's a break in the run at a point whose
1434
+ * own length we don't know. That's a crossing today — the run stops and starts
1435
+ * there, giving a rail joint, but we've measured no crossing part so claiming an
1436
+ * extent for it would be inventing one.
1437
+ */
1438
+ interface OccupiedSpan {
1439
+ fromPos: number;
1440
+ toPos: number;
1441
+ }
1442
+ /**
1443
+ * Cut a run into lengths of flex track.
1444
+ *
1445
+ * The model (#193): everything that isn't a turnout or a crossing is flex, flex
1446
+ * comes in pieces of a maximum length, and where two pieces meet is a rail
1447
+ * joint — the same joint a turnout makes with the track past it (#189). A 96″
1448
+ * main isn't one piece of track; it's four lengths of Atlas flex with three
1449
+ * joints in it, which is what someone actually buys and lays.
1450
+ *
1451
+ * Pieces are **spans of the run**, not free-floating geometry. That's deliberate:
1452
+ * `pos` — inches along the run — is what every turnout, industry and signal is
1453
+ * placed in, so a piece that carried its own shape would be a second geometry to
1454
+ * keep in step with the first. As spans they get identity, a length you can
1455
+ * change, and ends that meet their neighbours by construction.
1456
+ *
1457
+ * `cuts` is the owner's authoring and is **complete when present**: those are
1458
+ * the only joints, and a piece that ends up too long is flagged rather than
1459
+ * silently re-cut. Absent, the cuts are derived — full lengths from the start of
1460
+ * each gap, remainder at the end, which is how you lay it. That's what lets
1461
+ * every existing module arrive already cut up without anyone touching it.
1462
+ */
1463
+ declare function flexPieces(input: {
1464
+ fromPos: number;
1465
+ toPos: number;
1466
+ /** Longest single piece of the product this run is laid with. */
1467
+ maxPieceInches: number;
1468
+ /** Stretches the run gives up to parts — turnout bodies, crossings. */
1469
+ occupied?: OccupiedSpan[] | null;
1470
+ /** Authored joint positions, inches along the run. Absent = derive. */
1471
+ cuts?: number[] | null;
1472
+ }): FlexPiece[];
1473
+ /**
1474
+ * Which way a turnout faces along its host track: `+1` = its points look toward
1475
+ * increasing pos, `-1` = the other way.
1476
+ *
1477
+ * ⭐ ONE definition. The canvas had this inline for drawing the leg, and the
1478
+ * flex solver needs the same answer to know which side of `pos` the moulding
1479
+ * extends — a turnout's body is NOT symmetric about its points (an Atlas #7 is
1480
+ * about 0.9″ behind them and 5.1″ ahead), so getting the facing wrong moves a
1481
+ * rail joint four inches (#193).
1482
+ *
1483
+ * The rule: the turnout faces the way its diverging track LEAVES, because that's
1484
+ * where the frog is. `flipped` is the owner's override — rotating the part 180°
1485
+ * faces the points the other way, which is the only thing that can be right for
1486
+ * a siding pinned at a module end where the geometry reads ambiguously.
1487
+ *
1488
+ * `divergeFarPos` is whatever the caller can measure best: the canvas projects
1489
+ * the diverging track's real far end back onto the main, the editor uses its
1490
+ * authored extent. Same rule either way; only the precision of the input differs.
1491
+ */
1492
+ declare function turnoutFacing(input: {
1493
+ pos: number;
1494
+ /** Where the diverging track ends up, in the same coordinate as `pos`. */
1495
+ divergeFarPos?: number | null;
1496
+ flipped?: boolean;
1497
+ }): 1 | -1;
1498
+ /**
1499
+ * The stretch of its host run a turnout's moulding occupies (#193) — what flex
1500
+ * track has to stop short of on each side.
1501
+ *
1502
+ * Null when the part hasn't been measured, exactly as {@link partExtent} is:
1503
+ * without a real length we don't know where the moulding stops, and a guessed
1504
+ * body would put a rail joint on track nobody has checked.
1505
+ */
1506
+ declare function turnoutOccupiedSpan(input: {
1507
+ pos: number;
1508
+ extent: PartExtent | null | undefined;
1509
+ facing: 1 | -1;
1510
+ }): OccupiedSpan | null;
1511
+ /**
1512
+ * Retype one piece's length: move the rail joint at its far end, and let its
1513
+ * NEIGHBOUR take up the difference (#193).
1514
+ *
1515
+ * That's what cutting one piece longer actually does — the run doesn't grow, so
1516
+ * the next piece gets shorter by the same amount. The pair's total is fixed,
1517
+ * which is why the value is clamped to it: without that, asking for a length
1518
+ * past the next joint silently reorders the cuts and you get back something you
1519
+ * didn't ask for.
1520
+ *
1521
+ * Returns the run's COMPLETE new cut list (`flexCuts`), or null when the piece
1522
+ * has no neighbour to trade with — the last piece in a gap butts a turnout or
1523
+ * the endplate, and its length is set by what it meets, not by preference.
1524
+ */
1525
+ declare function resizeFlexPiece(pieces: FlexPiece[], index: number, nextLengthInches: number): number[] | null;
1526
+ /** What a run costs in flex, for someone about to order it (#193). */
1527
+ declare function flexUsage(pieces: FlexPiece[]): {
1528
+ /** How many separate lengths have to be cut. */
1529
+ pieces: number;
1530
+ /** Total flex laid, inches — the run minus whatever the parts occupy. */
1531
+ totalInches: number;
1532
+ /** Pieces the product can't actually supply in one length. */
1533
+ overlong: number;
1534
+ };
1323
1535
  /** Half an N-scale tie, inches — a 8′6″ tie is 102″ prototype, /160 ≈ 0.638″.
1324
1536
  * The half-width a tie strip extends either side of the rail it carries, which
1325
1537
  * is what gives a drawn turnout body its width. */
@@ -1810,4 +2022,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1810
2022
  heading: number;
1811
2023
  }>;
1812
2024
 
1813
- 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, 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 };
2025
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, 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 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, 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, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize };
package/dist/index.d.ts CHANGED
@@ -123,6 +123,14 @@ interface SchematicTrack {
123
123
  * the main centre-line + lane, as before. Physical view only; the operations
124
124
  * view stays positional (#2d-track). */
125
125
  path?: BenchworkPoint[] | null;
126
+ /** The flex product this run is laid with — a slug from the parts library
127
+ * (#193). Absent = the default. Per TRACK, so a module can have its mains in
128
+ * one product and a siding in another. */
129
+ flexPartId?: string | null;
130
+ /** Authored rail joints, inches along this run. Absent = derived from the
131
+ * product's maximum piece length. Present = these are the ONLY joints, so an
132
+ * owner's deliberate cut survives a change elsewhere on the module. */
133
+ flexCuts?: number[] | null;
126
134
  }
127
135
  interface SchematicTurnout {
128
136
  id: string;
@@ -428,10 +436,18 @@ interface ModuleFootprintInput {
428
436
  /** Where each endplate's CENTRE sits relative to the main centre-line at that
429
437
  * end, inches (signed, along the +normal). Free-moN puts a **double**-track
430
438
  * 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. */
439
+ * centre-line the plate centre is half a track spacing toward Main 2. Single
440
+ * track crosses at the centre ⇒ 0, and an off-centre track is a signed value.
441
+ * {@link endplateCentreOffsetInches} computes it from what the owner authored.
442
+ *
443
+ * ⚠️ OMITTING an end no longer means 0 — it means "use §2.0", which is a
444
+ * straddle on a double end. Centring a double plate on Main 1 drew its pair
445
+ * wholly to one side, which is what the read-only and catalog views did by
446
+ * passing nothing at all (#190). Pass an explicit number to place a plate. */
434
447
  endplateTrackOffsets?: Record<string, number>;
448
+ /** Whether Main 2 runs BELOW Main 1 (`mainsSwapped`, #131). Only affects which
449
+ * way an unauthored double plate straddles. */
450
+ mainsSwapped?: boolean;
435
451
  /** Authored benchwork outline (module-local inches), or absent for the band. */
436
452
  outline?: BenchworkPoint[] | null;
437
453
  /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
@@ -637,23 +653,45 @@ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number,
637
653
  */
638
654
  declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
639
655
  /**
640
- * The offset from the main centre-line to an endplate's CENTRE, inches — the
641
- * Free-moN geometry. A **double**-track end carries its two tracks 9/16″ either
642
- * side of the plate centre (§2.0 RP), and Main 1 sits on the centre-line, so the
643
- * plate centre is half a track spacing up. A single track crosses at the centre.
656
+ * Where an endplate's CENTRE sits relative to MAIN 1 — the renderer's framing,
657
+ * and the one number a drawing needs to place the plate.
658
+ *
659
+ * THE single definition. Two callers had their own: the builder computed it
660
+ * inline (swap-aware), and the read-only/catalog footprint **passed nothing at
661
+ * all**, so every plate there was centred on Main 1 — a double end drew its pair
662
+ * entirely to one side of the plate instead of straddling it (#190).
663
+ *
664
+ * §2.0 puts a double end's two tracks 0.5625″ either side of the plate centre,
665
+ * so the plate centre is half a track spacing away from Main 1 — **toward Main
666
+ * 2**, which is why the swap matters: with Main 2 below, the plate centre is
667
+ * below too. An authored offset always wins (an off-centre end is legal since
668
+ * the 20220628 revision), and is given in the standard's own framing — Main 1's
669
+ * distance from the plate centre — so it comes back negated here.
644
670
  */
645
- declare function endplateTrackOffsetFor(config: TrackConfig | "none" | undefined, authoredTrackOffset?: number | null): number;
671
+ declare function endplateCentreOffsetInches(input: {
672
+ config?: TrackConfig | "none" | null;
673
+ /** Main 1's signed distance from the plate centre, as authored. */
674
+ authoredTrackOffsetInches?: number | null;
675
+ /** Whether Main 2 runs below Main 1 (the mains are swapped). */
676
+ main2Below?: boolean;
677
+ }): number;
646
678
  /**
647
679
  * Where an endplate's PRIMARY track (Main 1) crosses, as a signed distance from
648
680
  * the plate's CENTRE — the standard's own framing. Authored value wins; absent
649
681
  * falls back to the §2.0 recommendations: a single track centred (0), a double
650
682
  * straddling so its two tracks land ∓ half the track spacing (Main 1 low).
651
683
  */
652
- declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined): number;
684
+ declare function endplateTrackOffsetInches(authored: number | null | undefined, config: TrackConfig | "none" | undefined,
685
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). The pair
686
+ * straddles the plate centre either way, so Main 1 sits on the side AWAY from
687
+ * Main 2 — low when Main 2 is high, high when it's low. Omitting this
688
+ * hard-coded "Main 2 is above", which is the assumption behind #190. */
689
+ main2Below?: boolean): number;
653
690
  /** A Free-moN conformance problem with an endplate's width/track placement. */
654
691
  interface EndplateWidthIssue {
655
- /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia. */
656
- code: "narrow" | "clearance";
692
+ /** "narrow" = below the 12″ minimum; "clearance" = a track too near a fascia;
693
+ * "offcentre" = a double end whose pair doesn't straddle the plate centre. */
694
+ code: "narrow" | "clearance" | "offcentre";
657
695
  /** Plain-language problem, for the author. */
658
696
  message: string;
659
697
  /** The width that would satisfy this rule, inches. */
@@ -676,6 +714,10 @@ declare function checkEndplateWidth(input: {
676
714
  widthInches?: number | null;
677
715
  config?: TrackConfig | "none" | null;
678
716
  trackOffsetInches?: number | null;
717
+ /** Whether Main 2 runs BELOW Main 1 (the mains are swapped). Without it a
718
+ * double end's second track was assumed to be one spacing ABOVE Main 1, so a
719
+ * swapped pair was measured on the wrong side of the plate (#190). */
720
+ main2Below?: boolean;
679
721
  }): EndplateWidthIssue[];
680
722
  /**
681
723
  * Whether a module presents NO far endplate — one conforming face only.
@@ -863,6 +905,19 @@ interface EditorState {
863
905
  y: number;
864
906
  heading: number;
865
907
  }>;
908
+ /**
909
+ * Flex track settings by TRACK id (#193) — which product a run is laid with,
910
+ * and where the owner has decided its rail joints fall.
911
+ *
912
+ * One map rather than fields, because the MAINS aren't `extraTracks` (the main
913
+ * IS the centre-line), so per-track settings would otherwise need a separate
914
+ * pair of fields for Main 1 and Main 2 — four ways to say one thing. Round-trips
915
+ * onto each track in the doc, where it's self-describing for Free-Dispatcher.
916
+ */
917
+ flexByTrack: Record<string, {
918
+ partId?: string;
919
+ cuts?: number[];
920
+ }>;
866
921
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
867
922
  * 24″ recommended). Absent id = the recommended default. */
868
923
  endplateWidths: Record<string, number>;
@@ -1208,7 +1263,10 @@ interface TrackPart {
1208
1263
  line: string;
1209
1264
  scale: "N";
1210
1265
  name: string;
1211
- kind: "turnout" | "wye" | "curved-turnout" | "crossing";
1266
+ /** `flex` is track sold by the length rather than as a fixed geometry — its
1267
+ * {@link overallLength} is the LONGEST piece you can lay from it, not a shape
1268
+ * (#193). */
1269
+ kind: "turnout" | "wye" | "curved-turnout" | "crossing" | "flex";
1212
1270
  /** Manufacturer part numbers by hand, where the part has a hand. */
1213
1271
  partNumbers?: {
1214
1272
  left?: string;
@@ -1316,10 +1374,164 @@ declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
1316
1374
  * everything else is a tooling decision. **Measure the part. Do not model it.**
1317
1375
  */
1318
1376
  declare const ATLAS_CODE55_N: TrackPart[];
1377
+ /**
1378
+ * Flex track — the stuff every run that isn't a turnout or a crossing is made
1379
+ * of (#193). A length of flex has no fixed geometry, so the only dimension it
1380
+ * carries is **how long a piece you can lay from one**: past that you have
1381
+ * another length, with a rail joint between them.
1382
+ *
1383
+ * ⚠️ These lengths are the NOMINAL PRODUCT LENGTHS as reported by Will Gage
1384
+ * (2026-07-26), not values read off a spec sheet or measured. They are what the
1385
+ * product is sold as — "30 inch flex track" — which is exactly the number a
1386
+ * builder plans cuts against, so they're recorded as `manufacturer` with the
1387
+ * source named. Correct them if a listing says otherwise.
1388
+ *
1389
+ * No part numbers here: I don't have them confirmed, and a wrong part number is
1390
+ * worse than none — someone would order against it.
1391
+ */
1392
+ declare const FLEX_TRACK_PARTS: TrackPart[];
1393
+ /** What a track is laid with when nobody has said — the commonest N-scale flex. */
1394
+ declare const DEFAULT_FLEX_PART_ID = "atlas-c55-n-flex";
1319
1395
  /** Every built-in part, across manufacturers. */
1320
1396
  declare const BUILT_IN_TRACK_PARTS: TrackPart[];
1397
+ /** Every flex product a track can be laid with. */
1398
+ declare function flexParts(library?: TrackPart[]): TrackPart[];
1399
+ /**
1400
+ * The flex product a track is laid with, falling back to the default rather
1401
+ * than to nothing: every non-turnout inch of track IS some product, and a run
1402
+ * whose owner hasn't chosen still gets cut into buyable lengths.
1403
+ */
1404
+ declare function flexPartFor(id: string | null | undefined, library?: TrackPart[]): TrackPart | null;
1405
+ /** How long a single piece of this product can be, inches. */
1406
+ declare function maxFlexPieceInches(id: string | null | undefined, library?: TrackPart[]): number;
1321
1407
  /** Look a part up by its slug. */
1322
1408
  declare function trackPart(id: string, library?: TrackPart[]): TrackPart | null;
1409
+ /** One length of flex track in a run — a real object you could pick up (#193). */
1410
+ interface FlexPiece {
1411
+ /** Position in the run, west to east. Stable enough to select and label by. */
1412
+ index: number;
1413
+ /** Inches along the RUN, not the module — the same coordinate turnouts,
1414
+ * industries and signals use, so a piece can be placed without a second
1415
+ * geometry to keep in step. */
1416
+ fromPos: number;
1417
+ toPos: number;
1418
+ lengthInches: number;
1419
+ /** Longer than the product allows. Only reachable from AUTHORED cuts, or from
1420
+ * a run that grew after they were authored — which is precisely when someone
1421
+ * needs telling. */
1422
+ overlong: boolean;
1423
+ /** What this piece butts against at each end: another piece, a part (turnout
1424
+ * or crossing), or the end of the run. */
1425
+ fromEnd: FlexPieceEnd;
1426
+ toEnd: FlexPieceEnd;
1427
+ }
1428
+ /** What a flex piece meets at one of its ends. */
1429
+ type FlexPieceEnd = "piece" | "part" | "runEnd";
1430
+ /**
1431
+ * A stretch of a run that is NOT flex — a turnout's or a crossing's own body.
1432
+ *
1433
+ * A **zero-length** span is meaningful: it's a break in the run at a point whose
1434
+ * own length we don't know. That's a crossing today — the run stops and starts
1435
+ * there, giving a rail joint, but we've measured no crossing part so claiming an
1436
+ * extent for it would be inventing one.
1437
+ */
1438
+ interface OccupiedSpan {
1439
+ fromPos: number;
1440
+ toPos: number;
1441
+ }
1442
+ /**
1443
+ * Cut a run into lengths of flex track.
1444
+ *
1445
+ * The model (#193): everything that isn't a turnout or a crossing is flex, flex
1446
+ * comes in pieces of a maximum length, and where two pieces meet is a rail
1447
+ * joint — the same joint a turnout makes with the track past it (#189). A 96″
1448
+ * main isn't one piece of track; it's four lengths of Atlas flex with three
1449
+ * joints in it, which is what someone actually buys and lays.
1450
+ *
1451
+ * Pieces are **spans of the run**, not free-floating geometry. That's deliberate:
1452
+ * `pos` — inches along the run — is what every turnout, industry and signal is
1453
+ * placed in, so a piece that carried its own shape would be a second geometry to
1454
+ * keep in step with the first. As spans they get identity, a length you can
1455
+ * change, and ends that meet their neighbours by construction.
1456
+ *
1457
+ * `cuts` is the owner's authoring and is **complete when present**: those are
1458
+ * the only joints, and a piece that ends up too long is flagged rather than
1459
+ * silently re-cut. Absent, the cuts are derived — full lengths from the start of
1460
+ * each gap, remainder at the end, which is how you lay it. That's what lets
1461
+ * every existing module arrive already cut up without anyone touching it.
1462
+ */
1463
+ declare function flexPieces(input: {
1464
+ fromPos: number;
1465
+ toPos: number;
1466
+ /** Longest single piece of the product this run is laid with. */
1467
+ maxPieceInches: number;
1468
+ /** Stretches the run gives up to parts — turnout bodies, crossings. */
1469
+ occupied?: OccupiedSpan[] | null;
1470
+ /** Authored joint positions, inches along the run. Absent = derive. */
1471
+ cuts?: number[] | null;
1472
+ }): FlexPiece[];
1473
+ /**
1474
+ * Which way a turnout faces along its host track: `+1` = its points look toward
1475
+ * increasing pos, `-1` = the other way.
1476
+ *
1477
+ * ⭐ ONE definition. The canvas had this inline for drawing the leg, and the
1478
+ * flex solver needs the same answer to know which side of `pos` the moulding
1479
+ * extends — a turnout's body is NOT symmetric about its points (an Atlas #7 is
1480
+ * about 0.9″ behind them and 5.1″ ahead), so getting the facing wrong moves a
1481
+ * rail joint four inches (#193).
1482
+ *
1483
+ * The rule: the turnout faces the way its diverging track LEAVES, because that's
1484
+ * where the frog is. `flipped` is the owner's override — rotating the part 180°
1485
+ * faces the points the other way, which is the only thing that can be right for
1486
+ * a siding pinned at a module end where the geometry reads ambiguously.
1487
+ *
1488
+ * `divergeFarPos` is whatever the caller can measure best: the canvas projects
1489
+ * the diverging track's real far end back onto the main, the editor uses its
1490
+ * authored extent. Same rule either way; only the precision of the input differs.
1491
+ */
1492
+ declare function turnoutFacing(input: {
1493
+ pos: number;
1494
+ /** Where the diverging track ends up, in the same coordinate as `pos`. */
1495
+ divergeFarPos?: number | null;
1496
+ flipped?: boolean;
1497
+ }): 1 | -1;
1498
+ /**
1499
+ * The stretch of its host run a turnout's moulding occupies (#193) — what flex
1500
+ * track has to stop short of on each side.
1501
+ *
1502
+ * Null when the part hasn't been measured, exactly as {@link partExtent} is:
1503
+ * without a real length we don't know where the moulding stops, and a guessed
1504
+ * body would put a rail joint on track nobody has checked.
1505
+ */
1506
+ declare function turnoutOccupiedSpan(input: {
1507
+ pos: number;
1508
+ extent: PartExtent | null | undefined;
1509
+ facing: 1 | -1;
1510
+ }): OccupiedSpan | null;
1511
+ /**
1512
+ * Retype one piece's length: move the rail joint at its far end, and let its
1513
+ * NEIGHBOUR take up the difference (#193).
1514
+ *
1515
+ * That's what cutting one piece longer actually does — the run doesn't grow, so
1516
+ * the next piece gets shorter by the same amount. The pair's total is fixed,
1517
+ * which is why the value is clamped to it: without that, asking for a length
1518
+ * past the next joint silently reorders the cuts and you get back something you
1519
+ * didn't ask for.
1520
+ *
1521
+ * Returns the run's COMPLETE new cut list (`flexCuts`), or null when the piece
1522
+ * has no neighbour to trade with — the last piece in a gap butts a turnout or
1523
+ * the endplate, and its length is set by what it meets, not by preference.
1524
+ */
1525
+ declare function resizeFlexPiece(pieces: FlexPiece[], index: number, nextLengthInches: number): number[] | null;
1526
+ /** What a run costs in flex, for someone about to order it (#193). */
1527
+ declare function flexUsage(pieces: FlexPiece[]): {
1528
+ /** How many separate lengths have to be cut. */
1529
+ pieces: number;
1530
+ /** Total flex laid, inches — the run minus whatever the parts occupy. */
1531
+ totalInches: number;
1532
+ /** Pieces the product can't actually supply in one length. */
1533
+ overlong: number;
1534
+ };
1323
1535
  /** Half an N-scale tie, inches — a 8′6″ tie is 102″ prototype, /160 ≈ 0.638″.
1324
1536
  * The half-width a tie strip extends either side of the rail it carries, which
1325
1537
  * is what gives a drawn turnout body its width. */
@@ -1810,4 +2022,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1810
2022
  heading: number;
1811
2023
  }>;
1812
2024
 
1813
- 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, 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 };
2025
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, 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 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, 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, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize };