@willcgage/module-schematic 0.84.0 → 0.85.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). */
@@ -2414,6 +2425,62 @@ flipped?: boolean | null): -1 | 0 | 1;
2414
2425
  */
2415
2426
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
2416
2427
  type GeometryType = "straight" | "corner_45" | "corner_90" | "curve" | "offset" | "dead_end" | "wye" | "other";
2428
+ /**
2429
+ * An endplate bound to a BENCHWORK EDGE (ADR 0001).
2430
+ *
2431
+ * Will Gage, 2026-07-26: *"Endplates are a part of the benchwork. We have
2432
+ * separated them causing some challenges."* A {@link SchematicEndplate.pose} is
2433
+ * a point floating in module space, so it can disagree with the board — and has:
2434
+ * a junction plate landed on the module CENTRE LINE rather than the fascia, a
2435
+ * derived pose written back silently PINNED a plate so it went stale when the
2436
+ * length changed (which is the only reason `poseAuthored` exists), and a face
2437
+ * drawn flat across a tapered edge is flush only at its midpoint.
2438
+ *
2439
+ * Bind it to an edge and none of those are expressible. Position, heading and
2440
+ * WIDTH all come from the edge, so they cannot drift apart.
2441
+ */
2442
+ interface EndplateEdge {
2443
+ /** The section whose outline owns this edge; absent = the module outline. */
2444
+ section?: string | null;
2445
+ /** Which edge: the segment from vertex `index` to vertex `index + 1`. */
2446
+ index: number;
2447
+ /** Optional span along that edge, 0…1. Absent = the whole edge — which is the
2448
+ * common case, because a board's end IS the endplate. */
2449
+ fromT?: number | null;
2450
+ toT?: number | null;
2451
+ }
2452
+ /** What an {@link EndplateEdge} resolves to. Everything here is READ OFF the
2453
+ * polygon; nothing is stored, so nothing can go stale. */
2454
+ interface EndplateEdgePose {
2455
+ x: number;
2456
+ y: number;
2457
+ /** The edge's OUTWARD normal, degrees. */
2458
+ heading: number;
2459
+ /** The span's length — the face's real width, not a separate number that
2460
+ * might disagree with the board. */
2461
+ widthInches: number;
2462
+ /** The face's two ends. On a tapered board this follows the slope, which a
2463
+ * stored pose + width could never do. */
2464
+ face: [{
2465
+ x: number;
2466
+ y: number;
2467
+ }, {
2468
+ x: number;
2469
+ y: number;
2470
+ }];
2471
+ }
2472
+ /**
2473
+ * Resolve an endplate's edge binding against a benchwork polygon.
2474
+ *
2475
+ * ⚠️ REFUSES A CURVED EDGE. Free-moN §2.0 requires the track crossing an
2476
+ * endplate to be perpendicular, straight and level for 4″ — so an endplate face
2477
+ * is flat by the standard, and a bulged edge is not somewhere one can go. That
2478
+ * is a rule, not a missing feature.
2479
+ *
2480
+ * Returns null when the edge doesn't exist or isn't usable, so a caller can say
2481
+ * why rather than drawing something wrong.
2482
+ */
2483
+ declare function endplateEdgePose(outline: BenchworkPoint[] | null | undefined, edge: EndplateEdge): EndplateEdgePose | null;
2417
2484
  interface EndplatePose {
2418
2485
  /** Endplate id — "A"/"B" axial, "C"/"D"… branch. */
2419
2486
  id: string;
@@ -2426,6 +2493,19 @@ interface EndplatePose {
2426
2493
  trackConfig: "single" | "double";
2427
2494
  /** Lateral track offsets from the crossing anchor (0 = centred single). */
2428
2495
  trackOffsets: number[];
2496
+ /** Set when this pose came from an {@link EndplateEdge} — read off the
2497
+ * benchwork rather than stored. Its width and face are the board's own. */
2498
+ boundToEdge?: boolean;
2499
+ /** The face's real width, when bound to an edge. */
2500
+ widthInches?: number;
2501
+ /** The face's two ends, when bound to an edge — follows a tapered board. */
2502
+ face?: [{
2503
+ x: number;
2504
+ y: number;
2505
+ }, {
2506
+ x: number;
2507
+ y: number;
2508
+ }];
2429
2509
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
2430
2510
  manual?: boolean;
2431
2511
  }
@@ -2476,6 +2556,12 @@ interface ModuleGeometryInput {
2476
2556
  side: "up" | "down";
2477
2557
  config?: "single" | "double" | null;
2478
2558
  }[];
2559
+ /** ⭐ Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of
2560
+ * the benchwork rather than a point floating beside it. Wins over
2561
+ * `poseOverrides` and over derivation. */
2562
+ endplateEdges?: Record<string, EndplateEdge>;
2563
+ /** The module's benchwork polygon, needed to resolve `endplateEdges`. */
2564
+ outline?: BenchworkPoint[] | null;
2479
2565
  /** Hand-entered pose overrides by endplate id — win over derivation. */
2480
2566
  poseOverrides?: Record<string, {
2481
2567
  x: number;
@@ -2564,4 +2650,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2564
2650
  heading: number;
2565
2651
  }>;
2566
2652
 
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 };
2653
+ 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 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, 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, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
package/dist/index.d.ts CHANGED
@@ -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). */
@@ -2414,6 +2425,62 @@ flipped?: boolean | null): -1 | 0 | 1;
2414
2425
  */
2415
2426
  declare function moduleFeatures(doc: ModuleSchematicDoc): ModuleFeatures;
2416
2427
  type GeometryType = "straight" | "corner_45" | "corner_90" | "curve" | "offset" | "dead_end" | "wye" | "other";
2428
+ /**
2429
+ * An endplate bound to a BENCHWORK EDGE (ADR 0001).
2430
+ *
2431
+ * Will Gage, 2026-07-26: *"Endplates are a part of the benchwork. We have
2432
+ * separated them causing some challenges."* A {@link SchematicEndplate.pose} is
2433
+ * a point floating in module space, so it can disagree with the board — and has:
2434
+ * a junction plate landed on the module CENTRE LINE rather than the fascia, a
2435
+ * derived pose written back silently PINNED a plate so it went stale when the
2436
+ * length changed (which is the only reason `poseAuthored` exists), and a face
2437
+ * drawn flat across a tapered edge is flush only at its midpoint.
2438
+ *
2439
+ * Bind it to an edge and none of those are expressible. Position, heading and
2440
+ * WIDTH all come from the edge, so they cannot drift apart.
2441
+ */
2442
+ interface EndplateEdge {
2443
+ /** The section whose outline owns this edge; absent = the module outline. */
2444
+ section?: string | null;
2445
+ /** Which edge: the segment from vertex `index` to vertex `index + 1`. */
2446
+ index: number;
2447
+ /** Optional span along that edge, 0…1. Absent = the whole edge — which is the
2448
+ * common case, because a board's end IS the endplate. */
2449
+ fromT?: number | null;
2450
+ toT?: number | null;
2451
+ }
2452
+ /** What an {@link EndplateEdge} resolves to. Everything here is READ OFF the
2453
+ * polygon; nothing is stored, so nothing can go stale. */
2454
+ interface EndplateEdgePose {
2455
+ x: number;
2456
+ y: number;
2457
+ /** The edge's OUTWARD normal, degrees. */
2458
+ heading: number;
2459
+ /** The span's length — the face's real width, not a separate number that
2460
+ * might disagree with the board. */
2461
+ widthInches: number;
2462
+ /** The face's two ends. On a tapered board this follows the slope, which a
2463
+ * stored pose + width could never do. */
2464
+ face: [{
2465
+ x: number;
2466
+ y: number;
2467
+ }, {
2468
+ x: number;
2469
+ y: number;
2470
+ }];
2471
+ }
2472
+ /**
2473
+ * Resolve an endplate's edge binding against a benchwork polygon.
2474
+ *
2475
+ * ⚠️ REFUSES A CURVED EDGE. Free-moN §2.0 requires the track crossing an
2476
+ * endplate to be perpendicular, straight and level for 4″ — so an endplate face
2477
+ * is flat by the standard, and a bulged edge is not somewhere one can go. That
2478
+ * is a rule, not a missing feature.
2479
+ *
2480
+ * Returns null when the edge doesn't exist or isn't usable, so a caller can say
2481
+ * why rather than drawing something wrong.
2482
+ */
2483
+ declare function endplateEdgePose(outline: BenchworkPoint[] | null | undefined, edge: EndplateEdge): EndplateEdgePose | null;
2417
2484
  interface EndplatePose {
2418
2485
  /** Endplate id — "A"/"B" axial, "C"/"D"… branch. */
2419
2486
  id: string;
@@ -2426,6 +2493,19 @@ interface EndplatePose {
2426
2493
  trackConfig: "single" | "double";
2427
2494
  /** Lateral track offsets from the crossing anchor (0 = centred single). */
2428
2495
  trackOffsets: number[];
2496
+ /** Set when this pose came from an {@link EndplateEdge} — read off the
2497
+ * benchwork rather than stored. Its width and face are the board's own. */
2498
+ boundToEdge?: boolean;
2499
+ /** The face's real width, when bound to an edge. */
2500
+ widthInches?: number;
2501
+ /** The face's two ends, when bound to an edge — follows a tapered board. */
2502
+ face?: [{
2503
+ x: number;
2504
+ y: number;
2505
+ }, {
2506
+ x: number;
2507
+ y: number;
2508
+ }];
2429
2509
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
2430
2510
  manual?: boolean;
2431
2511
  }
@@ -2476,6 +2556,12 @@ interface ModuleGeometryInput {
2476
2556
  side: "up" | "down";
2477
2557
  config?: "single" | "double" | null;
2478
2558
  }[];
2559
+ /** ⭐ Endplate EDGE bindings by id (ADR 0001) — an endplate that is part of
2560
+ * the benchwork rather than a point floating beside it. Wins over
2561
+ * `poseOverrides` and over derivation. */
2562
+ endplateEdges?: Record<string, EndplateEdge>;
2563
+ /** The module's benchwork polygon, needed to resolve `endplateEdges`. */
2564
+ outline?: BenchworkPoint[] | null;
2479
2565
  /** Hand-entered pose overrides by endplate id — win over derivation. */
2480
2566
  poseOverrides?: Record<string, {
2481
2567
  x: number;
@@ -2564,4 +2650,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
2564
2650
  heading: number;
2565
2651
  }>;
2566
2652
 
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 };
2653
+ 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 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, 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, poseNeedsManual, poseOverridesFromDoc, remapPos, resizeFlexPiece, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, spanOverhang, stateToDoc, storedPartToTrackPart, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutFacing, turnoutOccupiedSpan, turnoutPartForSize, usableCapacity };
package/dist/index.js CHANGED
@@ -869,6 +869,10 @@ function withPoses(endplates, overrides) {
869
869
  (e) => overrides[e.id] ? { ...e, pose: overrides[e.id], poseAuthored: true } : e
870
870
  );
871
871
  }
872
+ function withEdges(endplates, edges) {
873
+ if (!edges) return endplates;
874
+ return endplates.map((e) => edges[e.id] ? { ...e, edge: edges[e.id] } : e);
875
+ }
872
876
  function withWidths(endplates, widths, offsets = {}) {
873
877
  return endplates.map((e) => {
874
878
  const w = widths[e.id];
@@ -900,46 +904,49 @@ function stateToDoc(state, recordNumber) {
900
904
  ...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
901
905
  ...state.mainsSwapped ? { mainsSwapped: true } : {},
902
906
  endplates: withWidths(
903
- withPoses(
904
- [
905
- ...state.loop ? (
906
- // Balloon loop: A is the entry. A standard endplate B on the balloon
907
- // makes it an INTERCHANGE (second route connects at the loop, e.g.
908
- // Seaford); configB "none" makes it a pure turnback.
909
- [
910
- { id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
911
- ...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
912
- ]
913
- ) : [
914
- { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
915
- // ⚠️ `configB: "none"` means the module HAS NO FAR ENDPLATE, and
916
- // that is not loop-only (#184). An *end of the line* or a *pocket*
917
- // presents one conforming face and the track simply stops. The
918
- // standard governs the ends a module offers for joining; it never
919
- // required a module to offer two. This used to coerce "none" to
920
- // "single" and emit a B regardless, so a single-ended module was
921
- // impossible to author.
922
- ...state.configB !== "none" ? [
923
- {
924
- id: "B",
925
- label: "East",
926
- tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }]
927
- }
928
- ] : []
907
+ withEdges(
908
+ withPoses(
909
+ [
910
+ ...state.loop ? (
911
+ // Balloon loop: A is the entry. A standard endplate B on the balloon
912
+ // makes it an INTERCHANGE (second route connects at the loop, e.g.
913
+ // Seaford); configB "none" makes it a pure turnback.
914
+ [
915
+ { id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
916
+ ...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
917
+ ]
918
+ ) : [
919
+ { id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
920
+ // ⚠️ `configB: "none"` means the module HAS NO FAR ENDPLATE, and
921
+ // that is not loop-only (#184). An *end of the line* or a *pocket*
922
+ // presents one conforming face and the track simply stops. The
923
+ // standard governs the ends a module offers for joining; it never
924
+ // required a module to offer two. This used to coerce "none" to
925
+ // "single" and emit a B regardless, so a single-ended module was
926
+ // impossible to author.
927
+ ...state.configB !== "none" ? [
928
+ {
929
+ id: "B",
930
+ label: "East",
931
+ tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }]
932
+ }
933
+ ] : []
934
+ ],
935
+ // Branch endplates C, D, … — junction connections at pos, off one side
936
+ // (#170). A set can carry several (e.g. a second railroad through).
937
+ ...state.branches.map((b, i) => ({
938
+ id: String.fromCharCode(67 + i),
939
+ // C, D, E…
940
+ label: b.label || `Branch ${i + 1}`,
941
+ tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: b.config }],
942
+ at: { pos: b.pos, side: b.side },
943
+ kind: b.kind ?? "branch",
944
+ ...b.trackId ? { trackId: b.trackId } : {}
945
+ }))
929
946
  ],
930
- // Branch endplates C, D, … — junction connections at pos, off one side
931
- // (#170). A set can carry several (e.g. a second railroad through).
932
- ...state.branches.map((b, i) => ({
933
- id: String.fromCharCode(67 + i),
934
- // C, D, E…
935
- label: b.label || `Branch ${i + 1}`,
936
- tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: b.config }],
937
- at: { pos: b.pos, side: b.side },
938
- kind: b.kind ?? "branch",
939
- ...b.trackId ? { trackId: b.trackId } : {}
940
- }))
941
- ],
942
- state.poseOverrides
947
+ state.poseOverrides
948
+ ),
949
+ state.endplateEdges
943
950
  ),
944
951
  state.endplateWidths,
945
952
  state.endplateTrackOffsets
@@ -1135,7 +1142,15 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
1135
1142
  const poseOverrides = poseOverridesFromDoc(d);
1136
1143
  const endplateWidths = {};
1137
1144
  const endplateTrackOffsets = {};
1145
+ const endplateEdges = {};
1138
1146
  for (const e of d.endplates ?? []) {
1147
+ if (e.edge && Number.isFinite(e.edge.index) && e.edge.index >= 0)
1148
+ endplateEdges[e.id] = {
1149
+ index: Math.trunc(e.edge.index),
1150
+ ...e.edge.section ? { section: e.edge.section } : {},
1151
+ ...Number.isFinite(e.edge.fromT) ? { fromT: e.edge.fromT } : {},
1152
+ ...Number.isFinite(e.edge.toT) ? { toT: e.edge.toT } : {}
1153
+ };
1139
1154
  if (typeof e.widthInches === "number" && e.widthInches > 0)
1140
1155
  endplateWidths[e.id] = e.widthInches;
1141
1156
  if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
@@ -1186,6 +1201,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
1186
1201
  flexByTrack,
1187
1202
  endplateWidths,
1188
1203
  endplateTrackOffsets,
1204
+ endplateEdges,
1189
1205
  outline,
1190
1206
  outlineInner,
1191
1207
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
@@ -2532,6 +2548,45 @@ function moduleFeatures(doc) {
2532
2548
  laneMax: Math.max(...allLanes)
2533
2549
  };
2534
2550
  }
2551
+ function endplateEdgePose(outline, edge) {
2552
+ if (!outline || outline.length < 3) return null;
2553
+ const n = outline.length;
2554
+ const i = Math.trunc(edge.index);
2555
+ if (!Number.isFinite(i) || i < 0 || i >= n) return null;
2556
+ const p0 = outline[i];
2557
+ const p1 = outline[(i + 1) % n];
2558
+ if (p0.bulge) return null;
2559
+ const t0 = Math.max(0, Math.min(1, edge.fromT ?? 0));
2560
+ const t1 = Math.max(0, Math.min(1, edge.toT ?? 1));
2561
+ const a = { x: p0.x + (p1.x - p0.x) * Math.min(t0, t1), y: p0.y + (p1.y - p0.y) * Math.min(t0, t1) };
2562
+ const b = { x: p0.x + (p1.x - p0.x) * Math.max(t0, t1), y: p0.y + (p1.y - p0.y) * Math.max(t0, t1) };
2563
+ const w = Math.hypot(b.x - a.x, b.y - a.y);
2564
+ if (!(w > 0)) return null;
2565
+ let cx = 0;
2566
+ let cy = 0;
2567
+ for (const v of outline) {
2568
+ cx += v.x;
2569
+ cy += v.y;
2570
+ }
2571
+ cx /= n;
2572
+ cy /= n;
2573
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2574
+ const ex = (b.x - a.x) / w;
2575
+ const ey = (b.y - a.y) / w;
2576
+ let nx = ey;
2577
+ let ny = -ex;
2578
+ if ((mid.x - cx) * nx + (mid.y - cy) * ny < 0) {
2579
+ nx = -nx;
2580
+ ny = -ny;
2581
+ }
2582
+ return {
2583
+ x: mid.x,
2584
+ y: mid.y,
2585
+ heading: norm360(Math.atan2(ny, nx) * 180 / Math.PI),
2586
+ widthInches: w,
2587
+ face: [a, b]
2588
+ };
2589
+ }
2535
2590
  function returnLoop(shape, opts) {
2536
2591
  const L = Math.max(1, opts.leadInches);
2537
2592
  const R = Math.max(1, opts.radius);
@@ -2635,6 +2690,21 @@ function deriveEndplatePoses(geo) {
2635
2690
  const half = geo.trackHalfSpacingInches ?? 1;
2636
2691
  const cfg = (i) => geo.endplateConfigs?.[i] === "double" ? "double" : "single";
2637
2692
  const withOverride = (p) => {
2693
+ const bound = geo.endplateEdges?.[p.id];
2694
+ if (bound) {
2695
+ const outline = (bound.section ? geo.sections?.find((s) => s.id === bound.section)?.outline : geo.outline) ?? geo.outline;
2696
+ const e = endplateEdgePose(outline, bound);
2697
+ if (e)
2698
+ return {
2699
+ ...p,
2700
+ x: e.x,
2701
+ y: e.y,
2702
+ heading: e.heading,
2703
+ widthInches: e.widthInches,
2704
+ face: e.face,
2705
+ boundToEdge: true
2706
+ };
2707
+ }
2638
2708
  const o = geo.poseOverrides?.[p.id];
2639
2709
  return o ? { ...p, x: o.x, y: o.y, heading: norm360(o.heading), manual: true } : p;
2640
2710
  };
@@ -2806,6 +2876,6 @@ function poseOverridesFromDoc(doc) {
2806
2876
  return out;
2807
2877
  }
2808
2878
 
2809
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, PINCH_EASE_INCHES, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, 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 };
2879
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CLEARANCE_SPACING_INCHES, CODE55_RAIL_HEIGHT_INCHES, DEFAULT_FLEX_PART_ID, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FAST_TRACKS_N_ME55, FAST_TRACKS_N_ME55_CROSSOVERS, FLEX_TRACK_PARTS, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, PINCH_EASE_INCHES, RAIL_GAUGE_INCHES, TIE_HALF_LENGTH_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, assessSectionEnd, assessSectionJoint, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, clearancePointPastFrogInches, 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, 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 };
2810
2880
  //# sourceMappingURL=index.js.map
2811
2881
  //# sourceMappingURL=index.js.map