@willcgage/module-schematic 0.71.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;
@@ -897,6 +905,19 @@ interface EditorState {
897
905
  y: number;
898
906
  heading: number;
899
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
+ }>;
900
921
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
901
922
  * 24″ recommended). Absent id = the recommended default. */
902
923
  endplateWidths: Record<string, number>;
@@ -1242,7 +1263,10 @@ interface TrackPart {
1242
1263
  line: string;
1243
1264
  scale: "N";
1244
1265
  name: string;
1245
- 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";
1246
1270
  /** Manufacturer part numbers by hand, where the part has a hand. */
1247
1271
  partNumbers?: {
1248
1272
  left?: string;
@@ -1350,10 +1374,164 @@ declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
1350
1374
  * everything else is a tooling decision. **Measure the part. Do not model it.**
1351
1375
  */
1352
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";
1353
1395
  /** Every built-in part, across manufacturers. */
1354
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;
1355
1407
  /** Look a part up by its slug. */
1356
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
+ };
1357
1535
  /** Half an N-scale tie, inches — a 8′6″ tie is 102″ prototype, /160 ≈ 0.638″.
1358
1536
  * The half-width a tie strip extends either side of the rail it carries, which
1359
1537
  * is what gives a drawn turnout body its width. */
@@ -1844,4 +2022,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1844
2022
  heading: number;
1845
2023
  }>;
1846
2024
 
1847
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
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;
@@ -897,6 +905,19 @@ interface EditorState {
897
905
  y: number;
898
906
  heading: number;
899
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
+ }>;
900
921
  /** Authored endplate face widths by endplate id, inches (Free-moN 12″ min,
901
922
  * 24″ recommended). Absent id = the recommended default. */
902
923
  endplateWidths: Record<string, number>;
@@ -1242,7 +1263,10 @@ interface TrackPart {
1242
1263
  line: string;
1243
1264
  scale: "N";
1244
1265
  name: string;
1245
- 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";
1246
1270
  /** Manufacturer part numbers by hand, where the part has a hand. */
1247
1271
  partNumbers?: {
1248
1272
  left?: string;
@@ -1350,10 +1374,164 @@ declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
1350
1374
  * everything else is a tooling decision. **Measure the part. Do not model it.**
1351
1375
  */
1352
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";
1353
1395
  /** Every built-in part, across manufacturers. */
1354
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;
1355
1407
  /** Look a part up by its slug. */
1356
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
+ };
1357
1535
  /** Half an N-scale tie, inches — a 8′6″ tie is 102″ prototype, /160 ≈ 0.638″.
1358
1536
  * The half-width a tie strip extends either side of the rail it carries, which
1359
1537
  * is what gives a drawn turnout body its width. */
@@ -1844,4 +2022,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1844
2022
  heading: number;
1845
2023
  }>;
1846
2024
 
1847
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CODE55_RAIL_HEIGHT_INCHES, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartSegment, RAIL_GAUGE_INCHES, type ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type StoredTrackPart, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partOutlineAtFrog, pastFrogInchesForSize, pathLengthInches, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
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 };