@willcgage/module-schematic 0.84.0 → 0.86.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
@@ -34,6 +34,14 @@ interface SchematicEndplate {
34
34
  pos: number;
35
35
  side: "up" | "down";
36
36
  };
37
+ /** ⭐ THE ENDPLATE'S EDGE OF THE BENCHWORK (ADR 0001). When present it WINS
38
+ * over `pose` and over derivation, and position, heading and width are all
39
+ * read off the polygon — so they cannot drift apart from the board.
40
+ *
41
+ * `pose` remains for modules authored before this and for shapes with no
42
+ * benchwork polygon to bind to. Nothing is auto-converted: guessing which
43
+ * edge an owner's freehand pose meant would be inventing an intent. */
44
+ edge?: EndplateEdge | null;
37
45
  /** Manual pose override (#175 phase 1b) — the endplate's module-local track
38
46
  * point (x, y inches) + outward-normal heading (°). Hand-entered for shapes
39
47
  * the geometry fields can't derive (wye, freeform, loop); wins over
@@ -1184,6 +1192,9 @@ interface EditorState {
1184
1192
  /** Authored per-endplate TRACK offsets by id — the primary track's signed
1185
1193
  * distance from the plate centre, inches. Absent id = the §2.0 default. */
1186
1194
  endplateTrackOffsets: Record<string, number>;
1195
+ /** Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of the
1196
+ * benchwork. Wins over a pose; nothing is auto-converted from one. */
1197
+ endplateEdges?: Record<string, EndplateEdge>;
1187
1198
  /** Benchwork footprint outline — polygon vertices in module-local inches
1188
1199
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
1189
1200
  * Empty = no authored outline (fall back to the endplate-width band). */
@@ -2358,6 +2369,125 @@ declare function partsPlaceable(library?: TrackPart[]): {
2358
2369
  why: string;
2359
2370
  }[];
2360
2371
  };
2372
+ /** A part placed on the benchwork. */
2373
+ interface TrackPiece {
2374
+ id: string;
2375
+ /** A slug from the parts library. */
2376
+ partId: string;
2377
+ /** Where the part's own origin sits, in module-local inches. */
2378
+ x: number;
2379
+ y: number;
2380
+ /** Rotation about that origin, degrees. */
2381
+ rotationDeg: number;
2382
+ /** Mirrored across its own through route — a left-hand turnout from a right. */
2383
+ flipped?: boolean;
2384
+ /** FLEX ONLY: how long this run is. The one piece a builder cuts (ADR 0001). */
2385
+ lengthInches?: number;
2386
+ /** Owner's label, carried onto whatever route this piece ends up in. */
2387
+ name?: string;
2388
+ }
2389
+ /** A piece's joint, in MODULE coordinates. */
2390
+ interface PlacedJoint {
2391
+ /** `"<pieceId>.<jointId>"` — stable, and what connections refer to. */
2392
+ key: string;
2393
+ piece: string;
2394
+ joint: string;
2395
+ role: PartJointRole;
2396
+ x: number;
2397
+ y: number;
2398
+ headingDeg: number;
2399
+ }
2400
+ /** Two joints in the same place. That is the entire connection rule. */
2401
+ interface GraphConnection {
2402
+ a: string;
2403
+ b: string;
2404
+ }
2405
+ /**
2406
+ * More than two joints in one place — REFUSED, not resolved.
2407
+ *
2408
+ * ⚠️ THIS IS THE GAP THE SPIKE FOUND, and the one that would bite owners. With
2409
+ * three ends stacked on a point, picking a pair silently drops a piece out of
2410
+ * the layout: the walk never reaches it, and nothing says so. So none of them
2411
+ * connect and the ambiguity is reported. Refusing is the only honest answer —
2412
+ * the model cannot know which two the owner meant.
2413
+ */
2414
+ interface GraphConflict {
2415
+ x: number;
2416
+ y: number;
2417
+ joints: string[];
2418
+ reason: string;
2419
+ }
2420
+ interface TrackGraph {
2421
+ joints: PlacedJoint[];
2422
+ connections: GraphConnection[];
2423
+ /** Joints connected to nothing — an unfinished layout, which is a real thing
2424
+ * to show an owner rather than quietly draw as if it were joined. */
2425
+ open: string[];
2426
+ conflicts: GraphConflict[];
2427
+ /** Pieces whose part has no derivable geometry (see {@link partGeometryGap}). */
2428
+ unplaceable: {
2429
+ piece: string;
2430
+ partId: string;
2431
+ why: string;
2432
+ }[];
2433
+ }
2434
+ /** How close two joints must be to count as connected. A hundredth of an inch:
2435
+ * tight enough that nothing joins by accident, loose enough to absorb the
2436
+ * rounding of a drag. */
2437
+ declare const JOINT_SNAP_INCHES = 0.01;
2438
+ /** Every piece's joints, transformed onto the board. */
2439
+ declare function placedJoints(pieces: TrackPiece[], library?: TrackPart[]): PlacedJoint[];
2440
+ /**
2441
+ * Build the graph: which joints are connected, which are open, and where the
2442
+ * layout is ambiguous.
2443
+ *
2444
+ * ⭐ A JOINT HOLDS AT MOST ONE CONNECTION. Rail has two ends; a place where
2445
+ * three meet is a mistake, not a junction — a junction is a TURNOUT, which is a
2446
+ * part carrying three joints of its own.
2447
+ */
2448
+ declare function buildTrackGraph(pieces: TrackPiece[], library?: TrackPart[], snapInches?: number): TrackGraph;
2449
+ /** A route the walk found: a continuous run of pieces a train can travel. */
2450
+ interface GraphRoute {
2451
+ id: string;
2452
+ /** Inches from endplate A, ALONG THE RAIL, to where this route begins. */
2453
+ fromPos: number;
2454
+ toPos: number;
2455
+ /** The turnout this route branched from; null for the main. */
2456
+ bornAt: string | null;
2457
+ /** The turnout it runs back into. Set = a SIDING; null = it dead-ends. */
2458
+ endsAt: string | null;
2459
+ pieces: string[];
2460
+ /** Furthest lateral offset reached — what gives a lane its side. */
2461
+ lateral: number;
2462
+ }
2463
+ interface GraphTurnout {
2464
+ id: string;
2465
+ /** ⚠️ The FROG's distance from endplate A (#132), measured along the rail. */
2466
+ pos: number;
2467
+ onRoute: string;
2468
+ divergeRoute: string | null;
2469
+ }
2470
+ interface GraphWalk {
2471
+ routes: GraphRoute[];
2472
+ turnouts: GraphTurnout[];
2473
+ /** Every reason this walk is not the whole layout. */
2474
+ warnings: string[];
2475
+ }
2476
+ /**
2477
+ * Walk the graph from an endplate and read the topology off it.
2478
+ *
2479
+ * ⚠️ POSITIONS ARE ARC LENGTH ALONG THE RAIL, never x. On a curved module the
2480
+ * two differ by inches — a 90°/R30 corner runs 47.1″ of rail across a 42.4″
2481
+ * chord — and it is the rail a train travels.
2482
+ *
2483
+ * Nesting falls out for free: a turnout found on a branch queues the branch IT
2484
+ * opens, so a yard ladder resolves to whatever depth it actually has. That is
2485
+ * the case the 1-D model can only approximate.
2486
+ */
2487
+ declare function walkTrackGraph(graph: TrackGraph, pieces: TrackPiece[], startAt: {
2488
+ piece: string;
2489
+ joint: string;
2490
+ }, library?: TrackPart[]): GraphWalk;
2361
2491
  /** A frog casting's parts, in TURNOUT-LOCAL inches: `x` = distance past the
2362
2492
  * points, `y` = lateral offset from the through route. */
2363
2493
  interface FrogCasting {
@@ -2414,6 +2544,62 @@ flipped?: boolean | null): -1 | 0 | 1;
2414
2544
  */
2415
2545
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
2416
2546
  type GeometryType = "straight" | "corner_45" | "corner_90" | "curve" | "offset" | "dead_end" | "wye" | "other";
2547
+ /**
2548
+ * An endplate bound to a BENCHWORK EDGE (ADR 0001).
2549
+ *
2550
+ * Will Gage, 2026-07-26: *"Endplates are a part of the benchwork. We have
2551
+ * separated them causing some challenges."* A {@link SchematicEndplate.pose} is
2552
+ * a point floating in module space, so it can disagree with the board — and has:
2553
+ * a junction plate landed on the module CENTRE LINE rather than the fascia, a
2554
+ * derived pose written back silently PINNED a plate so it went stale when the
2555
+ * length changed (which is the only reason `poseAuthored` exists), and a face
2556
+ * drawn flat across a tapered edge is flush only at its midpoint.
2557
+ *
2558
+ * Bind it to an edge and none of those are expressible. Position, heading and
2559
+ * WIDTH all come from the edge, so they cannot drift apart.
2560
+ */
2561
+ interface EndplateEdge {
2562
+ /** The section whose outline owns this edge; absent = the module outline. */
2563
+ section?: string | null;
2564
+ /** Which edge: the segment from vertex `index` to vertex `index + 1`. */
2565
+ index: number;
2566
+ /** Optional span along that edge, 0…1. Absent = the whole edge — which is the
2567
+ * common case, because a board's end IS the endplate. */
2568
+ fromT?: number | null;
2569
+ toT?: number | null;
2570
+ }
2571
+ /** What an {@link EndplateEdge} resolves to. Everything here is READ OFF the
2572
+ * polygon; nothing is stored, so nothing can go stale. */
2573
+ interface EndplateEdgePose {
2574
+ x: number;
2575
+ y: number;
2576
+ /** The edge's OUTWARD normal, degrees. */
2577
+ heading: number;
2578
+ /** The span's length — the face's real width, not a separate number that
2579
+ * might disagree with the board. */
2580
+ widthInches: number;
2581
+ /** The face's two ends. On a tapered board this follows the slope, which a
2582
+ * stored pose + width could never do. */
2583
+ face: [{
2584
+ x: number;
2585
+ y: number;
2586
+ }, {
2587
+ x: number;
2588
+ y: number;
2589
+ }];
2590
+ }
2591
+ /**
2592
+ * Resolve an endplate's edge binding against a benchwork polygon.
2593
+ *
2594
+ * ⚠️ REFUSES A CURVED EDGE. Free-moN §2.0 requires the track crossing an
2595
+ * endplate to be perpendicular, straight and level for 4″ — so an endplate face
2596
+ * is flat by the standard, and a bulged edge is not somewhere one can go. That
2597
+ * is a rule, not a missing feature.
2598
+ *
2599
+ * Returns null when the edge doesn't exist or isn't usable, so a caller can say
2600
+ * why rather than drawing something wrong.
2601
+ */
2602
+ declare function endplateEdgePose(outline: BenchworkPoint[] | null | undefined, edge: EndplateEdge): EndplateEdgePose | null;
2417
2603
  interface EndplatePose {
2418
2604
  /** Endplate id — "A"/"B" axial, "C"/"D"… branch. */
2419
2605
  id: string;
@@ -2426,6 +2612,19 @@ interface EndplatePose {
2426
2612
  trackConfig: "single" | "double";
2427
2613
  /** Lateral track offsets from the crossing anchor (0 = centred single). */
2428
2614
  trackOffsets: number[];
2615
+ /** Set when this pose came from an {@link EndplateEdge} — read off the
2616
+ * benchwork rather than stored. Its width and face are the board's own. */
2617
+ boundToEdge?: boolean;
2618
+ /** The face's real width, when bound to an edge. */
2619
+ widthInches?: number;
2620
+ /** The face's two ends, when bound to an edge — follows a tapered board. */
2621
+ face?: [{
2622
+ x: number;
2623
+ y: number;
2624
+ }, {
2625
+ x: number;
2626
+ y: number;
2627
+ }];
2429
2628
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
2430
2629
  manual?: boolean;
2431
2630
  }
@@ -2476,6 +2675,12 @@ interface ModuleGeometryInput {
2476
2675
  side: "up" | "down";
2477
2676
  config?: "single" | "double" | null;
2478
2677
  }[];
2678
+ /** ⭐ Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of
2679
+ * the benchwork rather than a point floating beside it. Wins over
2680
+ * `poseOverrides` and over derivation. */
2681
+ endplateEdges?: Record<string, EndplateEdge>;
2682
+ /** The module's benchwork polygon, needed to resolve `endplateEdges`. */
2683
+ outline?: BenchworkPoint[] | null;
2479
2684
  /** Hand-entered pose overrides by endplate id — win over derivation. */
2480
2685
  poseOverrides?: Record<string, {
2481
2686
  x: number;
@@ -2564,4 +2769,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2564
2769
  heading: number;
2565
2770
  }>;
2566
2771
 
2567
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, type LanePinch, 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, PINCH_EASE_INCHES, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartGeometry, type PartJoint, type PartJointRole, 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, crossoverPinches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, laneOffsetAt, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partGeometry, partGeometryGap, partOutlineAtFrog, partsPlaceable, 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 };
2772
+ 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 EndplateEdge, type EndplateEdgePose, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, type FrogCasting, type GeometryType, type GraphConflict, type GraphConnection, type GraphRoute, type GraphTurnout, type GraphWalk, type ImportedPart, type IndustryLabelMode, type IndustrySpot, JOINT_SNAP_INCHES, type LanePinch, 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, PINCH_EASE_INCHES, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartGeometry, type PartJoint, type PartJointRole, type PartSegment, type PlacedJoint, 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 TrackGraph, type TrackPart, type TrackPiece, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTrackGraph, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, crossoverPinches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateEdgePose, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, laneOffsetAt, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partGeometry, partGeometryGap, partOutlineAtFrog, partsPlaceable, pastFrogInchesForSize, pathLengthInches, placedJoints, 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, walkTrackGraph };
package/dist/index.d.ts CHANGED
@@ -34,6 +34,14 @@ interface SchematicEndplate {
34
34
  pos: number;
35
35
  side: "up" | "down";
36
36
  };
37
+ /** ⭐ THE ENDPLATE'S EDGE OF THE BENCHWORK (ADR 0001). When present it WINS
38
+ * over `pose` and over derivation, and position, heading and width are all
39
+ * read off the polygon — so they cannot drift apart from the board.
40
+ *
41
+ * `pose` remains for modules authored before this and for shapes with no
42
+ * benchwork polygon to bind to. Nothing is auto-converted: guessing which
43
+ * edge an owner's freehand pose meant would be inventing an intent. */
44
+ edge?: EndplateEdge | null;
37
45
  /** Manual pose override (#175 phase 1b) — the endplate's module-local track
38
46
  * point (x, y inches) + outward-normal heading (°). Hand-entered for shapes
39
47
  * the geometry fields can't derive (wye, freeform, loop); wins over
@@ -1184,6 +1192,9 @@ interface EditorState {
1184
1192
  /** Authored per-endplate TRACK offsets by id — the primary track's signed
1185
1193
  * distance from the plate centre, inches. Absent id = the §2.0 default. */
1186
1194
  endplateTrackOffsets: Record<string, number>;
1195
+ /** Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of the
1196
+ * benchwork. Wins over a pose; nothing is auto-converted from one. */
1197
+ endplateEdges?: Record<string, EndplateEdge>;
1187
1198
  /** Benchwork footprint outline — polygon vertices in module-local inches
1188
1199
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
1189
1200
  * Empty = no authored outline (fall back to the endplate-width band). */
@@ -2358,6 +2369,125 @@ declare function partsPlaceable(library?: TrackPart[]): {
2358
2369
  why: string;
2359
2370
  }[];
2360
2371
  };
2372
+ /** A part placed on the benchwork. */
2373
+ interface TrackPiece {
2374
+ id: string;
2375
+ /** A slug from the parts library. */
2376
+ partId: string;
2377
+ /** Where the part's own origin sits, in module-local inches. */
2378
+ x: number;
2379
+ y: number;
2380
+ /** Rotation about that origin, degrees. */
2381
+ rotationDeg: number;
2382
+ /** Mirrored across its own through route — a left-hand turnout from a right. */
2383
+ flipped?: boolean;
2384
+ /** FLEX ONLY: how long this run is. The one piece a builder cuts (ADR 0001). */
2385
+ lengthInches?: number;
2386
+ /** Owner's label, carried onto whatever route this piece ends up in. */
2387
+ name?: string;
2388
+ }
2389
+ /** A piece's joint, in MODULE coordinates. */
2390
+ interface PlacedJoint {
2391
+ /** `"<pieceId>.<jointId>"` — stable, and what connections refer to. */
2392
+ key: string;
2393
+ piece: string;
2394
+ joint: string;
2395
+ role: PartJointRole;
2396
+ x: number;
2397
+ y: number;
2398
+ headingDeg: number;
2399
+ }
2400
+ /** Two joints in the same place. That is the entire connection rule. */
2401
+ interface GraphConnection {
2402
+ a: string;
2403
+ b: string;
2404
+ }
2405
+ /**
2406
+ * More than two joints in one place — REFUSED, not resolved.
2407
+ *
2408
+ * ⚠️ THIS IS THE GAP THE SPIKE FOUND, and the one that would bite owners. With
2409
+ * three ends stacked on a point, picking a pair silently drops a piece out of
2410
+ * the layout: the walk never reaches it, and nothing says so. So none of them
2411
+ * connect and the ambiguity is reported. Refusing is the only honest answer —
2412
+ * the model cannot know which two the owner meant.
2413
+ */
2414
+ interface GraphConflict {
2415
+ x: number;
2416
+ y: number;
2417
+ joints: string[];
2418
+ reason: string;
2419
+ }
2420
+ interface TrackGraph {
2421
+ joints: PlacedJoint[];
2422
+ connections: GraphConnection[];
2423
+ /** Joints connected to nothing — an unfinished layout, which is a real thing
2424
+ * to show an owner rather than quietly draw as if it were joined. */
2425
+ open: string[];
2426
+ conflicts: GraphConflict[];
2427
+ /** Pieces whose part has no derivable geometry (see {@link partGeometryGap}). */
2428
+ unplaceable: {
2429
+ piece: string;
2430
+ partId: string;
2431
+ why: string;
2432
+ }[];
2433
+ }
2434
+ /** How close two joints must be to count as connected. A hundredth of an inch:
2435
+ * tight enough that nothing joins by accident, loose enough to absorb the
2436
+ * rounding of a drag. */
2437
+ declare const JOINT_SNAP_INCHES = 0.01;
2438
+ /** Every piece's joints, transformed onto the board. */
2439
+ declare function placedJoints(pieces: TrackPiece[], library?: TrackPart[]): PlacedJoint[];
2440
+ /**
2441
+ * Build the graph: which joints are connected, which are open, and where the
2442
+ * layout is ambiguous.
2443
+ *
2444
+ * ⭐ A JOINT HOLDS AT MOST ONE CONNECTION. Rail has two ends; a place where
2445
+ * three meet is a mistake, not a junction — a junction is a TURNOUT, which is a
2446
+ * part carrying three joints of its own.
2447
+ */
2448
+ declare function buildTrackGraph(pieces: TrackPiece[], library?: TrackPart[], snapInches?: number): TrackGraph;
2449
+ /** A route the walk found: a continuous run of pieces a train can travel. */
2450
+ interface GraphRoute {
2451
+ id: string;
2452
+ /** Inches from endplate A, ALONG THE RAIL, to where this route begins. */
2453
+ fromPos: number;
2454
+ toPos: number;
2455
+ /** The turnout this route branched from; null for the main. */
2456
+ bornAt: string | null;
2457
+ /** The turnout it runs back into. Set = a SIDING; null = it dead-ends. */
2458
+ endsAt: string | null;
2459
+ pieces: string[];
2460
+ /** Furthest lateral offset reached — what gives a lane its side. */
2461
+ lateral: number;
2462
+ }
2463
+ interface GraphTurnout {
2464
+ id: string;
2465
+ /** ⚠️ The FROG's distance from endplate A (#132), measured along the rail. */
2466
+ pos: number;
2467
+ onRoute: string;
2468
+ divergeRoute: string | null;
2469
+ }
2470
+ interface GraphWalk {
2471
+ routes: GraphRoute[];
2472
+ turnouts: GraphTurnout[];
2473
+ /** Every reason this walk is not the whole layout. */
2474
+ warnings: string[];
2475
+ }
2476
+ /**
2477
+ * Walk the graph from an endplate and read the topology off it.
2478
+ *
2479
+ * ⚠️ POSITIONS ARE ARC LENGTH ALONG THE RAIL, never x. On a curved module the
2480
+ * two differ by inches — a 90°/R30 corner runs 47.1″ of rail across a 42.4″
2481
+ * chord — and it is the rail a train travels.
2482
+ *
2483
+ * Nesting falls out for free: a turnout found on a branch queues the branch IT
2484
+ * opens, so a yard ladder resolves to whatever depth it actually has. That is
2485
+ * the case the 1-D model can only approximate.
2486
+ */
2487
+ declare function walkTrackGraph(graph: TrackGraph, pieces: TrackPiece[], startAt: {
2488
+ piece: string;
2489
+ joint: string;
2490
+ }, library?: TrackPart[]): GraphWalk;
2361
2491
  /** A frog casting's parts, in TURNOUT-LOCAL inches: `x` = distance past the
2362
2492
  * points, `y` = lateral offset from the through route. */
2363
2493
  interface FrogCasting {
@@ -2414,6 +2544,62 @@ flipped?: boolean | null): -1 | 0 | 1;
2414
2544
  */
2415
2545
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
2416
2546
  type GeometryType = "straight" | "corner_45" | "corner_90" | "curve" | "offset" | "dead_end" | "wye" | "other";
2547
+ /**
2548
+ * An endplate bound to a BENCHWORK EDGE (ADR 0001).
2549
+ *
2550
+ * Will Gage, 2026-07-26: *"Endplates are a part of the benchwork. We have
2551
+ * separated them causing some challenges."* A {@link SchematicEndplate.pose} is
2552
+ * a point floating in module space, so it can disagree with the board — and has:
2553
+ * a junction plate landed on the module CENTRE LINE rather than the fascia, a
2554
+ * derived pose written back silently PINNED a plate so it went stale when the
2555
+ * length changed (which is the only reason `poseAuthored` exists), and a face
2556
+ * drawn flat across a tapered edge is flush only at its midpoint.
2557
+ *
2558
+ * Bind it to an edge and none of those are expressible. Position, heading and
2559
+ * WIDTH all come from the edge, so they cannot drift apart.
2560
+ */
2561
+ interface EndplateEdge {
2562
+ /** The section whose outline owns this edge; absent = the module outline. */
2563
+ section?: string | null;
2564
+ /** Which edge: the segment from vertex `index` to vertex `index + 1`. */
2565
+ index: number;
2566
+ /** Optional span along that edge, 0…1. Absent = the whole edge — which is the
2567
+ * common case, because a board's end IS the endplate. */
2568
+ fromT?: number | null;
2569
+ toT?: number | null;
2570
+ }
2571
+ /** What an {@link EndplateEdge} resolves to. Everything here is READ OFF the
2572
+ * polygon; nothing is stored, so nothing can go stale. */
2573
+ interface EndplateEdgePose {
2574
+ x: number;
2575
+ y: number;
2576
+ /** The edge's OUTWARD normal, degrees. */
2577
+ heading: number;
2578
+ /** The span's length — the face's real width, not a separate number that
2579
+ * might disagree with the board. */
2580
+ widthInches: number;
2581
+ /** The face's two ends. On a tapered board this follows the slope, which a
2582
+ * stored pose + width could never do. */
2583
+ face: [{
2584
+ x: number;
2585
+ y: number;
2586
+ }, {
2587
+ x: number;
2588
+ y: number;
2589
+ }];
2590
+ }
2591
+ /**
2592
+ * Resolve an endplate's edge binding against a benchwork polygon.
2593
+ *
2594
+ * ⚠️ REFUSES A CURVED EDGE. Free-moN §2.0 requires the track crossing an
2595
+ * endplate to be perpendicular, straight and level for 4″ — so an endplate face
2596
+ * is flat by the standard, and a bulged edge is not somewhere one can go. That
2597
+ * is a rule, not a missing feature.
2598
+ *
2599
+ * Returns null when the edge doesn't exist or isn't usable, so a caller can say
2600
+ * why rather than drawing something wrong.
2601
+ */
2602
+ declare function endplateEdgePose(outline: BenchworkPoint[] | null | undefined, edge: EndplateEdge): EndplateEdgePose | null;
2417
2603
  interface EndplatePose {
2418
2604
  /** Endplate id — "A"/"B" axial, "C"/"D"… branch. */
2419
2605
  id: string;
@@ -2426,6 +2612,19 @@ interface EndplatePose {
2426
2612
  trackConfig: "single" | "double";
2427
2613
  /** Lateral track offsets from the crossing anchor (0 = centred single). */
2428
2614
  trackOffsets: number[];
2615
+ /** Set when this pose came from an {@link EndplateEdge} — read off the
2616
+ * benchwork rather than stored. Its width and face are the board's own. */
2617
+ boundToEdge?: boolean;
2618
+ /** The face's real width, when bound to an edge. */
2619
+ widthInches?: number;
2620
+ /** The face's two ends, when bound to an edge — follows a tapered board. */
2621
+ face?: [{
2622
+ x: number;
2623
+ y: number;
2624
+ }, {
2625
+ x: number;
2626
+ y: number;
2627
+ }];
2429
2628
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
2430
2629
  manual?: boolean;
2431
2630
  }
@@ -2476,6 +2675,12 @@ interface ModuleGeometryInput {
2476
2675
  side: "up" | "down";
2477
2676
  config?: "single" | "double" | null;
2478
2677
  }[];
2678
+ /** ⭐ Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of
2679
+ * the benchwork rather than a point floating beside it. Wins over
2680
+ * `poseOverrides` and over derivation. */
2681
+ endplateEdges?: Record<string, EndplateEdge>;
2682
+ /** The module's benchwork polygon, needed to resolve `endplateEdges`. */
2683
+ outline?: BenchworkPoint[] | null;
2479
2684
  /** Hand-entered pose overrides by endplate id — win over derivation. */
2480
2685
  poseOverrides?: Record<string, {
2481
2686
  x: number;
@@ -2564,4 +2769,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2564
2769
  heading: number;
2565
2770
  }>;
2566
2771
 
2567
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, type BenchworkPoint, type BranchConnector, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, type DimensionSource, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, type FrogCasting, type GeometryType, type ImportedPart, type IndustryLabelMode, type IndustrySpot, type LanePinch, 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, PINCH_EASE_INCHES, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartGeometry, type PartJoint, type PartJointRole, 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, crossoverPinches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, laneOffsetAt, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partGeometry, partGeometryGap, partOutlineAtFrog, partsPlaceable, 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 };
2772
+ 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 EndplateEdge, type EndplateEdgePose, type EndplatePose, type EndplateTrackIssue, type EndplateWidthIssue, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type FlexPiece, type FlexPieceEnd, type FrogCasting, type GeometryType, type GraphConflict, type GraphConnection, type GraphRoute, type GraphTurnout, type GraphWalk, type ImportedPart, type IndustryLabelMode, type IndustrySpot, JOINT_SNAP_INCHES, type LanePinch, 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, PINCH_EASE_INCHES, type PartAngle, type PartDimension, type PartEnd, type PartExtent, type PartGeometry, type PartJoint, type PartJointRole, type PartSegment, type PlacedJoint, 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 TrackGraph, type TrackPart, type TrackPiece, type TrackRole, type TurnoutClosure, type TurnoutKind, type UsableCapacity, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTrackGraph, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, crossoverPinches, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateCentreOffsetInches, endplateEdgePose, endplateFaceSegments, endplateLead, endplateTrackOffsetInches, endplateWidthInches, flexPartFor, flexParts, flexPieces, flexUsage, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, hasNoFarEndplate, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, laneOffsetAt, leadInchesForSize, maxFlexPieceInches, mergeImportedParts, mergeStoredParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partExtent, partExtentForSize, partGeometry, partGeometryGap, partOutlineAtFrog, partsPlaceable, pastFrogInchesForSize, pathLengthInches, placedJoints, 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, walkTrackGraph };