@willcgage/module-schematic 0.57.0 → 0.59.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
@@ -132,6 +132,11 @@ interface SchematicTurnout {
132
132
  * forces it: the body has nowhere to go but back toward the module, so the
133
133
  * derived facing comes out backwards. */
134
134
  flipped?: boolean | null;
135
+ /** The library part this turnout IS — e.g. "atlas-c55-n-7". Binds a drawn
136
+ * turnout to real geometry: with it the renderer can draw the part's own
137
+ * outline instead of a shape derived from `size`. Absent means "just a #N",
138
+ * which is what every turnout authored before the parts library was. */
139
+ partId?: string | null;
135
140
  }
136
141
  interface SchematicSignal {
137
142
  id: string;
@@ -705,6 +710,8 @@ interface EditorTurnout {
705
710
  size?: number;
706
711
  /** Rotate the turnout 180° — the points face the other way (#turnout-flip). */
707
712
  flipped?: boolean;
713
+ /** The library part this turnout IS — see SchematicTurnout.partId. */
714
+ partId?: string;
708
715
  /** A curved turnout — the diverging route bows into an arc rather than a
709
716
  * straight diagonal. Physical-render only (the operations view is topological). */
710
717
  curved?: boolean;
@@ -1299,6 +1306,24 @@ declare function importedPartToTrackPart(part: ImportedPart, sourceName?: string
1299
1306
  * Matching is by manufacturer part number first, then manufacturer + frog number.
1300
1307
  */
1301
1308
  declare function mergeImportedParts(imported: ImportedPart[], library?: TrackPart[], sourceName?: string): TrackPart[];
1309
+ /**
1310
+ * A part's drawn outline in TURNOUT-LOCAL coordinates, ready for MR to map onto
1311
+ * a lane: `x` = inches along the through route measured **from the frog**
1312
+ * (negative back toward the points), `y` = lateral offset with the DIVERGING
1313
+ * side positive. That is the same frame {@link turnoutClosure} works in, so a
1314
+ * renderer can swap one for the other.
1315
+ *
1316
+ * ⚠️ **Anchored on OUR measured lead, not the file's frog.** The part is placed
1317
+ * so its points sit `leadInches` back from the frog, because `pos` means the
1318
+ * frog (#132) and our leads are physical measurements while the shipped Atlas
1319
+ * `.xtp`'s frog positions are internally inconsistent. A consequence worth
1320
+ * knowing: if the file's own geometry disagrees, its drawn V will not land on
1321
+ * the frog marker — and that visible gap IS the validation. Don't "fix" it by
1322
+ * anchoring on the file.
1323
+ *
1324
+ * Returns null when the part carries no geometry to draw.
1325
+ */
1326
+ declare function partOutlineAtFrog(part: TrackPart, leadInches: number, stepsPerCurve?: number): BenchworkPoint[][] | null;
1302
1327
  /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1303
1328
  * route, from the points (s = 0) to the frog (s = lead) and beyond.
1304
1329
  *
@@ -1329,10 +1354,34 @@ interface TurnoutClosure {
1329
1354
  switchSlope: number;
1330
1355
  /** Slope at and beyond the frog — the frog angle, 1/N. */
1331
1356
  frogSlope: number;
1357
+ /** Total run from the points to where the route arrives PARALLEL at
1358
+ * `arriveAtInches`. Infinity when no target was given (the legacy
1359
+ * straight-forever behaviour). Use this rather than solving for the offset:
1360
+ * solving finds where the route REACHES the lane, not where it arrives
1361
+ * parallel, and the difference is a visible kink. */
1362
+ span: number;
1363
+ /** Length of the easing curve, 0 when not easing. */
1364
+ easeInches: number;
1332
1365
  }
1333
1366
  declare function turnoutClosure(size: number, opts?: {
1334
1367
  leadInches?: number;
1335
1368
  gaugeInches?: number;
1369
+ /**
1370
+ * Lateral offset the diverging route must ARRIVE AT, PARALLEL — normally
1371
+ * one lane spacing, i.e. the track it feeds.
1372
+ *
1373
+ * ⚠️ Without this the route runs straight at the frog angle forever, and a
1374
+ * caller can only solve for where it first REACHES the offset. Reaching is
1375
+ * not arriving: it gets there still climbing at 1/N while the track it
1376
+ * joins runs parallel, so the two meet with an instantaneous change of
1377
+ * direction. That KINK is what reads as "the rails don't line up" — each
1378
+ * rail is offset perpendicular to its own heading, so at a kink the two
1379
+ * rails meet at different points.
1380
+ */
1381
+ arriveAtInches?: number;
1382
+ /** Length of the easing curve. Defaults to one lead, which puts the radius
1383
+ * around 25″ on a #7 — clear of the Free-moN 22″ minimum. */
1384
+ easeInches?: number;
1336
1385
  }): TurnoutClosure;
1337
1386
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1338
1387
  /** The turnout is installed the other way round — the points face the far
@@ -1492,4 +1541,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1492
1541
  heading: number;
1493
1542
  }>;
1494
1543
 
1495
- 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 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 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, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1544
+ 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 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 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, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
package/dist/index.d.ts CHANGED
@@ -132,6 +132,11 @@ interface SchematicTurnout {
132
132
  * forces it: the body has nowhere to go but back toward the module, so the
133
133
  * derived facing comes out backwards. */
134
134
  flipped?: boolean | null;
135
+ /** The library part this turnout IS — e.g. "atlas-c55-n-7". Binds a drawn
136
+ * turnout to real geometry: with it the renderer can draw the part's own
137
+ * outline instead of a shape derived from `size`. Absent means "just a #N",
138
+ * which is what every turnout authored before the parts library was. */
139
+ partId?: string | null;
135
140
  }
136
141
  interface SchematicSignal {
137
142
  id: string;
@@ -705,6 +710,8 @@ interface EditorTurnout {
705
710
  size?: number;
706
711
  /** Rotate the turnout 180° — the points face the other way (#turnout-flip). */
707
712
  flipped?: boolean;
713
+ /** The library part this turnout IS — see SchematicTurnout.partId. */
714
+ partId?: string;
708
715
  /** A curved turnout — the diverging route bows into an arc rather than a
709
716
  * straight diagonal. Physical-render only (the operations view is topological). */
710
717
  curved?: boolean;
@@ -1299,6 +1306,24 @@ declare function importedPartToTrackPart(part: ImportedPart, sourceName?: string
1299
1306
  * Matching is by manufacturer part number first, then manufacturer + frog number.
1300
1307
  */
1301
1308
  declare function mergeImportedParts(imported: ImportedPart[], library?: TrackPart[], sourceName?: string): TrackPart[];
1309
+ /**
1310
+ * A part's drawn outline in TURNOUT-LOCAL coordinates, ready for MR to map onto
1311
+ * a lane: `x` = inches along the through route measured **from the frog**
1312
+ * (negative back toward the points), `y` = lateral offset with the DIVERGING
1313
+ * side positive. That is the same frame {@link turnoutClosure} works in, so a
1314
+ * renderer can swap one for the other.
1315
+ *
1316
+ * ⚠️ **Anchored on OUR measured lead, not the file's frog.** The part is placed
1317
+ * so its points sit `leadInches` back from the frog, because `pos` means the
1318
+ * frog (#132) and our leads are physical measurements while the shipped Atlas
1319
+ * `.xtp`'s frog positions are internally inconsistent. A consequence worth
1320
+ * knowing: if the file's own geometry disagrees, its drawn V will not land on
1321
+ * the frog marker — and that visible gap IS the validation. Don't "fix" it by
1322
+ * anchoring on the file.
1323
+ *
1324
+ * Returns null when the part carries no geometry to draw.
1325
+ */
1326
+ declare function partOutlineAtFrog(part: TrackPart, leadInches: number, stepsPerCurve?: number): BenchworkPoint[][] | null;
1302
1327
  /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1303
1328
  * route, from the points (s = 0) to the frog (s = lead) and beyond.
1304
1329
  *
@@ -1329,10 +1354,34 @@ interface TurnoutClosure {
1329
1354
  switchSlope: number;
1330
1355
  /** Slope at and beyond the frog — the frog angle, 1/N. */
1331
1356
  frogSlope: number;
1357
+ /** Total run from the points to where the route arrives PARALLEL at
1358
+ * `arriveAtInches`. Infinity when no target was given (the legacy
1359
+ * straight-forever behaviour). Use this rather than solving for the offset:
1360
+ * solving finds where the route REACHES the lane, not where it arrives
1361
+ * parallel, and the difference is a visible kink. */
1362
+ span: number;
1363
+ /** Length of the easing curve, 0 when not easing. */
1364
+ easeInches: number;
1332
1365
  }
1333
1366
  declare function turnoutClosure(size: number, opts?: {
1334
1367
  leadInches?: number;
1335
1368
  gaugeInches?: number;
1369
+ /**
1370
+ * Lateral offset the diverging route must ARRIVE AT, PARALLEL — normally
1371
+ * one lane spacing, i.e. the track it feeds.
1372
+ *
1373
+ * ⚠️ Without this the route runs straight at the frog angle forever, and a
1374
+ * caller can only solve for where it first REACHES the offset. Reaching is
1375
+ * not arriving: it gets there still climbing at 1/N while the track it
1376
+ * joins runs parallel, so the two meet with an instantaneous change of
1377
+ * direction. That KINK is what reads as "the rails don't line up" — each
1378
+ * rail is offset perpendicular to its own heading, so at a kink the two
1379
+ * rails meet at different points.
1380
+ */
1381
+ arriveAtInches?: number;
1382
+ /** Length of the easing curve. Defaults to one lead, which puts the radius
1383
+ * around 25″ on a #7 — clear of the Free-moN 22″ minimum. */
1384
+ easeInches?: number;
1336
1385
  }): TurnoutClosure;
1337
1386
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1338
1387
  /** The turnout is installed the other way round — the points face the far
@@ -1492,4 +1541,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1492
1541
  heading: number;
1493
1542
  }>;
1494
1543
 
1495
- 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 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 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, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1544
+ 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 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 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, TURNOUT_LEAD_INCHES_PER_FROG, type TrackConfig, type TrackPart, type TrackRole, type TurnoutClosure, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
package/dist/index.js CHANGED
@@ -767,7 +767,8 @@ function stateToDoc(state, recordNumber) {
767
767
  name: t.name || void 0,
768
768
  ...t.size ? { size: t.size } : {},
769
769
  ...t.curved ? { curved: true } : {},
770
- ...t.flipped ? { flipped: true } : {}
770
+ ...t.flipped ? { flipped: true } : {},
771
+ ...t.partId ? { partId: t.partId } : {}
771
772
  })),
772
773
  ...state.crossings.length > 0 ? {
773
774
  crossings: state.crossings.map((x) => ({
@@ -955,7 +956,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
955
956
  kind: t.kind ?? "right",
956
957
  ...t.size ? { size: t.size } : {},
957
958
  ...t.curved ? { curved: true } : {},
958
- ...t.flipped ? { flipped: true } : {}
959
+ ...t.flipped ? { flipped: true } : {},
960
+ ...t.partId ? { partId: t.partId } : {}
959
961
  })),
960
962
  controlPoints: readControlPoints(d, sc),
961
963
  industries: (d.industries ?? []).map((ind) => ({
@@ -1397,6 +1399,44 @@ function mergeImportedParts(imported, library = BUILT_IN_TRACK_PARTS, sourceName
1397
1399
  }
1398
1400
  return out;
1399
1401
  }
1402
+ function partOutlineAtFrog(part, leadInches, stepsPerCurve = 16) {
1403
+ const ends = part.ends ?? [];
1404
+ const segs = part.segments ?? [];
1405
+ if (!segs.length || ends.length < 2) return null;
1406
+ let points = ends[0];
1407
+ if (ends.length >= 3) {
1408
+ let bestPair = Infinity;
1409
+ let pairIdx = [1, 2];
1410
+ for (let i = 0; i < ends.length; i++) {
1411
+ for (let j = i + 1; j < ends.length; j++) {
1412
+ const d = Math.hypot(ends[i].x - ends[j].x, ends[i].y - ends[j].y);
1413
+ if (d < bestPair) {
1414
+ bestPair = d;
1415
+ pairIdx = [i, j];
1416
+ }
1417
+ }
1418
+ }
1419
+ const odd = ends.findIndex((_, k) => k !== pairIdx[0] && k !== pairIdx[1]);
1420
+ if (odd >= 0) points = ends[odd];
1421
+ }
1422
+ const a = points.angleDeg * Math.PI / 180;
1423
+ const ux = -Math.sin(a);
1424
+ const uy = -Math.cos(a);
1425
+ const toLocal = (p) => {
1426
+ const dx = p.x - points.x;
1427
+ const dy = p.y - points.y;
1428
+ return { x: dx * ux + dy * uy, y: dx * -uy + dy * ux };
1429
+ };
1430
+ let sign = 1;
1431
+ const offAxis = ends.filter((e) => e !== points).map(toLocal).sort((p, q) => Math.abs(q.y) - Math.abs(p.y))[0];
1432
+ if (offAxis && offAxis.y < 0) sign = -1;
1433
+ return samplePartSegments(segs, stepsPerCurve).map(
1434
+ (poly) => poly.map((p) => {
1435
+ const l = toLocal(p);
1436
+ return { x: l.x - leadInches, y: l.y * sign };
1437
+ })
1438
+ );
1439
+ }
1400
1440
  function turnoutClosure(size, opts = {}) {
1401
1441
  const N = size > 0 ? size : 6;
1402
1442
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
@@ -1404,13 +1444,33 @@ function turnoutClosure(size, opts = {}) {
1404
1444
  const frogSlope = 1 / N;
1405
1445
  const k = (lead / N - g) / (lead * lead);
1406
1446
  const switchSlope = Math.max(0, frogSlope - 2 * k * lead);
1447
+ const target = opts.arriveAtInches;
1448
+ let a = Infinity;
1449
+ let b = 0;
1450
+ if (target != null && target > g) {
1451
+ b = Math.max(0.01, opts.easeInches ?? lead);
1452
+ a = (target - g) / frogSlope - b / 2;
1453
+ if (a < 0) {
1454
+ a = 0;
1455
+ b = 2 * (target - g) / frogSlope;
1456
+ }
1457
+ }
1458
+ const span = target != null && target > g ? lead + a + b : Infinity;
1407
1459
  return {
1408
1460
  lead,
1409
1461
  switchSlope,
1410
1462
  frogSlope,
1411
- // Past the frog the route is straight at the frog angle — the closure curve
1412
- // has done its work, so don't keep bending (that's the tangent Option 1).
1413
- offsetAt: (s) => s <= lead ? switchSlope * s + k * s * s : g + frogSlope * (s - lead)
1463
+ span,
1464
+ easeInches: b,
1465
+ offsetAt: (s) => {
1466
+ if (s <= lead) return switchSlope * s + k * s * s;
1467
+ const past = s - lead;
1468
+ if (past <= a) return g + frogSlope * past;
1469
+ if (target == null) return g + frogSlope * past;
1470
+ const u = Math.min(past - a, b);
1471
+ const eased = g + frogSlope * a + frogSlope * u - frogSlope * u * u / (2 * b);
1472
+ return past - a >= b ? target : eased;
1473
+ }
1414
1474
  };
1415
1475
  }
1416
1476
  function divergeSideForHand(kind, stubDir, flipped) {
@@ -1926,6 +1986,6 @@ function poseOverridesFromDoc(doc) {
1926
1986
  return out;
1927
1987
  }
1928
1988
 
1929
- export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, RAIL_GAUGE_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1989
+ export { ATLAS_CODE55_N, BUILT_IN_TRACK_PARTS, CODE55_RAIL_HEIGHT_INCHES, ENDPLATE_FASCIA_CLEAR_INCHES, ENDPLATE_LEAD_INCHES, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, RAIL_GAUGE_INCHES, TURNOUT_LEAD_INCHES_PER_FROG, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePartSegments, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPart, trackPath, turnoutClosure, turnoutPartForSize };
1930
1990
  //# sourceMappingURL=index.js.map
1931
1991
  //# sourceMappingURL=index.js.map