@willcgage/module-schematic 0.49.5 → 0.50.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
@@ -668,7 +668,9 @@ declare const N_CAR_LENGTH_INCHES = 3.3;
668
668
  /** How many cars fit in a span, from its drawn length — the derived capacity a
669
669
  * siding or an industry spot holds (never typed). */
670
670
  declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
671
- /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
671
+ /** Parse a jsonb value into a schematic doc, or null if it isn't one.
672
+ * EVERY read path goes through here (catalog, module page, my-modules, FD), so
673
+ * it's also where a stored doc gets healed — see healSelfDivergingTurnouts. */
672
674
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
673
675
  interface EditorTrack {
674
676
  id: string;
@@ -1030,6 +1032,49 @@ interface ModuleFeatures {
1030
1032
  * right-hand throws to the opposite side. `kind` is the source of truth for the
1031
1033
  * drawn side (#bug1) — the stored lane's sign is reconciled to match it.
1032
1034
  */
1035
+ /** N-scale track gauge, inches (9 mm). */
1036
+ declare const RAIL_GAUGE_INCHES = 0.354;
1037
+ /** Points → FROG for a #1 frog, inches — a turnout's LEAD, scaled by frog
1038
+ * number. Calibrated COMMERCIALLY rather than on the prototype, since owners lay
1039
+ * real product: an Atlas code 55 #7 measures 3⅜″ points→frog (Steve Branton,
1040
+ * #173) ⇒ 3.375 / 7. (The prototype #7 lead is 62′-1″ ≈ 4.66″ in N; commercial
1041
+ * turnouts are compressed against that.) */
1042
+ declare const TURNOUT_LEAD_INCHES_PER_FROG = 0.482;
1043
+ /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1044
+ * route, from the points (s = 0) to the frog (s = lead) and beyond.
1045
+ *
1046
+ * Anchored on one geometric fact: the FROG is where the two routes' inner rails
1047
+ * cross, which happens where the centre-lines are exactly ONE GAUGE apart. So
1048
+ * `pos` (which means the frog, #132) must be where d = gauge — otherwise the
1049
+ * drawn crossing and the frog marker disagree.
1050
+ *
1051
+ * The curve is `d(s) = α·s + k·s²`, fixed by three conditions:
1052
+ * d(0) = 0 — the points start ON the stock rail
1053
+ * d(lead) = gauge — the frog lands exactly on `pos`
1054
+ * d'(lead) = 1/N — it leaves at the frog angle
1055
+ *
1056
+ * ⚠️ `α` is the SWITCH ANGLE and is deliberately non-zero. A curve leaving
1057
+ * TANGENT to the stock rail only reaches `lead/2N` of lateral by the frog
1058
+ * (0.24″ on a #7) — it could never reach a gauge within a commercial lead; you'd
1059
+ * need `lead = 2·gauge·N` ≈ the prototype figure. Real points leave the stock
1060
+ * rail at a finite angle, which is exactly why a commercial turnout can be
1061
+ * shorter than prototype. Valid while `lead < 2·gauge·N`; α is clamped at 0
1062
+ * otherwise so an absurd lead degrades to a tangent start instead of bending
1063
+ * backwards. */
1064
+ interface TurnoutClosure {
1065
+ /** Lateral offset from the through route at arc length `s` past the points. */
1066
+ offsetAt: (s: number) => number;
1067
+ /** Points→frog, inches. */
1068
+ lead: number;
1069
+ /** Slope at the points (the switch angle), rise over run. */
1070
+ switchSlope: number;
1071
+ /** Slope at and beyond the frog — the frog angle, 1/N. */
1072
+ frogSlope: number;
1073
+ }
1074
+ declare function turnoutClosure(size: number, opts?: {
1075
+ leadInches?: number;
1076
+ gaugeInches?: number;
1077
+ }): TurnoutClosure;
1033
1078
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1034
1079
  /** The turnout is installed the other way round — the points face the far
1035
1080
  * direction, which swaps the side the diverging route leaves on. */
@@ -1188,4 +1233,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1188
1233
  heading: number;
1189
1234
  }>;
1190
1235
 
1191
- export { type BenchworkPoint, type BranchConnector, 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 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 ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1236
+ export { type BenchworkPoint, type BranchConnector, 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 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, 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 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath, turnoutClosure };
package/dist/index.d.ts CHANGED
@@ -668,7 +668,9 @@ declare const N_CAR_LENGTH_INCHES = 3.3;
668
668
  /** How many cars fit in a span, from its drawn length — the derived capacity a
669
669
  * siding or an industry spot holds (never typed). */
670
670
  declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
671
- /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
671
+ /** Parse a jsonb value into a schematic doc, or null if it isn't one.
672
+ * EVERY read path goes through here (catalog, module page, my-modules, FD), so
673
+ * it's also where a stored doc gets healed — see healSelfDivergingTurnouts. */
672
674
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
673
675
  interface EditorTrack {
674
676
  id: string;
@@ -1030,6 +1032,49 @@ interface ModuleFeatures {
1030
1032
  * right-hand throws to the opposite side. `kind` is the source of truth for the
1031
1033
  * drawn side (#bug1) — the stored lane's sign is reconciled to match it.
1032
1034
  */
1035
+ /** N-scale track gauge, inches (9 mm). */
1036
+ declare const RAIL_GAUGE_INCHES = 0.354;
1037
+ /** Points → FROG for a #1 frog, inches — a turnout's LEAD, scaled by frog
1038
+ * number. Calibrated COMMERCIALLY rather than on the prototype, since owners lay
1039
+ * real product: an Atlas code 55 #7 measures 3⅜″ points→frog (Steve Branton,
1040
+ * #173) ⇒ 3.375 / 7. (The prototype #7 lead is 62′-1″ ≈ 4.66″ in N; commercial
1041
+ * turnouts are compressed against that.) */
1042
+ declare const TURNOUT_LEAD_INCHES_PER_FROG = 0.482;
1043
+ /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1044
+ * route, from the points (s = 0) to the frog (s = lead) and beyond.
1045
+ *
1046
+ * Anchored on one geometric fact: the FROG is where the two routes' inner rails
1047
+ * cross, which happens where the centre-lines are exactly ONE GAUGE apart. So
1048
+ * `pos` (which means the frog, #132) must be where d = gauge — otherwise the
1049
+ * drawn crossing and the frog marker disagree.
1050
+ *
1051
+ * The curve is `d(s) = α·s + k·s²`, fixed by three conditions:
1052
+ * d(0) = 0 — the points start ON the stock rail
1053
+ * d(lead) = gauge — the frog lands exactly on `pos`
1054
+ * d'(lead) = 1/N — it leaves at the frog angle
1055
+ *
1056
+ * ⚠️ `α` is the SWITCH ANGLE and is deliberately non-zero. A curve leaving
1057
+ * TANGENT to the stock rail only reaches `lead/2N` of lateral by the frog
1058
+ * (0.24″ on a #7) — it could never reach a gauge within a commercial lead; you'd
1059
+ * need `lead = 2·gauge·N` ≈ the prototype figure. Real points leave the stock
1060
+ * rail at a finite angle, which is exactly why a commercial turnout can be
1061
+ * shorter than prototype. Valid while `lead < 2·gauge·N`; α is clamped at 0
1062
+ * otherwise so an absurd lead degrades to a tangent start instead of bending
1063
+ * backwards. */
1064
+ interface TurnoutClosure {
1065
+ /** Lateral offset from the through route at arc length `s` past the points. */
1066
+ offsetAt: (s: number) => number;
1067
+ /** Points→frog, inches. */
1068
+ lead: number;
1069
+ /** Slope at the points (the switch angle), rise over run. */
1070
+ switchSlope: number;
1071
+ /** Slope at and beyond the frog — the frog angle, 1/N. */
1072
+ frogSlope: number;
1073
+ }
1074
+ declare function turnoutClosure(size: number, opts?: {
1075
+ leadInches?: number;
1076
+ gaugeInches?: number;
1077
+ }): TurnoutClosure;
1033
1078
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1034
1079
  /** The turnout is installed the other way round — the points face the far
1035
1080
  * direction, which swaps the side the diverging route leaves on. */
@@ -1188,4 +1233,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1188
1233
  heading: number;
1189
1234
  }>;
1190
1235
 
1191
- export { type BenchworkPoint, type BranchConnector, 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 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 ReturnLoopGeometry, type ReturnLoopShape, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionAdjacency, type SectionFootprint, type SectionRelativePos, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1236
+ export { type BenchworkPoint, type BranchConnector, 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 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, 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 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath, turnoutClosure };
package/dist/index.js CHANGED
@@ -539,12 +539,23 @@ function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
539
539
  if (!(carLengthInches > 0)) return 0;
540
540
  return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
541
541
  }
542
+ function healSelfDivergingTurnouts(turnouts) {
543
+ if (!turnouts?.some((t) => t.onTrack === t.divergeTrack)) return turnouts;
544
+ return turnouts.map((t) => {
545
+ if (t.onTrack !== t.divergeTrack) return t;
546
+ if (t.onTrack === MAIN_TRACK_ID) return { ...t, divergeTrack: MAIN2_TRACK_ID };
547
+ if (t.onTrack === MAIN2_TRACK_ID) return { ...t, divergeTrack: MAIN_TRACK_ID };
548
+ return t;
549
+ });
550
+ }
542
551
  function asModuleSchematic(x) {
543
552
  if (!x || typeof x !== "object") return null;
544
553
  const d = x;
545
554
  if (typeof d.version !== "number") return null;
546
555
  if (!Array.isArray(d.endplates) || !Array.isArray(d.tracks)) return null;
547
- return d;
556
+ const doc = d;
557
+ const healed = healSelfDivergingTurnouts(doc.turnouts);
558
+ return healed === doc.turnouts ? doc : { ...doc, turnouts: healed };
548
559
  }
549
560
  function emptyEditorState(lengthInches) {
550
561
  return {
@@ -933,24 +944,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
933
944
  trackB: x.tracks?.[1] ?? MAIN_TRACK_ID
934
945
  })),
935
946
  extraTracks,
936
- turnouts: (d.turnouts ?? []).map((t) => {
937
- let divergeTrack = t.divergeTrack;
938
- if (divergeTrack === t.onTrack) {
939
- if (t.onTrack === MAIN_TRACK_ID) divergeTrack = MAIN2_TRACK_ID;
940
- else if (t.onTrack === MAIN2_TRACK_ID) divergeTrack = MAIN_TRACK_ID;
941
- }
942
- return {
943
- id: t.id,
944
- name: t.name ?? "",
945
- pos: sc(t.pos),
946
- onTrack: t.onTrack,
947
- divergeTrack,
948
- kind: t.kind ?? "right",
949
- ...t.size ? { size: t.size } : {},
950
- ...t.curved ? { curved: true } : {},
951
- ...t.flipped ? { flipped: true } : {}
952
- };
953
- }),
947
+ // Heal a turnout that diverges into the track it sits on (#172) with the
948
+ // same helper the read path uses, so the editor and every renderer agree.
949
+ turnouts: (healSelfDivergingTurnouts(d.turnouts) ?? []).map((t) => ({
950
+ id: t.id,
951
+ name: t.name ?? "",
952
+ pos: sc(t.pos),
953
+ onTrack: t.onTrack,
954
+ divergeTrack: t.divergeTrack,
955
+ kind: t.kind ?? "right",
956
+ ...t.size ? { size: t.size } : {},
957
+ ...t.curved ? { curved: true } : {},
958
+ ...t.flipped ? { flipped: true } : {}
959
+ })),
954
960
  controlPoints: readControlPoints(d, sc),
955
961
  industries: (d.industries ?? []).map((ind) => ({
956
962
  id: ind.id,
@@ -1082,6 +1088,24 @@ function buildCrossover(state) {
1082
1088
  ];
1083
1089
  return { track, turnouts };
1084
1090
  }
1091
+ var RAIL_GAUGE_INCHES = 0.354;
1092
+ var TURNOUT_LEAD_INCHES_PER_FROG = 0.482;
1093
+ function turnoutClosure(size, opts = {}) {
1094
+ const N = size > 0 ? size : 6;
1095
+ const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
1096
+ const lead = Math.max(0.01, opts.leadInches ?? N * TURNOUT_LEAD_INCHES_PER_FROG);
1097
+ const frogSlope = 1 / N;
1098
+ const k = (lead / N - g) / (lead * lead);
1099
+ const switchSlope = Math.max(0, frogSlope - 2 * k * lead);
1100
+ return {
1101
+ lead,
1102
+ switchSlope,
1103
+ frogSlope,
1104
+ // Past the frog the route is straight at the frog angle — the closure curve
1105
+ // has done its work, so don't keep bending (that's the tangent Option 1).
1106
+ offsetAt: (s) => s <= lead ? switchSlope * s + k * s * s : g + frogSlope * (s - lead)
1107
+ };
1108
+ }
1085
1109
  function divergeSideForHand(kind, stubDir, flipped) {
1086
1110
  if (kind !== "left" && kind !== "right") return 0;
1087
1111
  const s = (stubDir >= 0 ? 1 : -1) * (flipped ? -1 : 1);
@@ -1595,6 +1619,6 @@ function poseOverridesFromDoc(doc) {
1595
1619
  return out;
1596
1620
  }
1597
1621
 
1598
- export { 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, WHOLE_MODULE_SECTION_ID, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateLead, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1622
+ export { 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, remapPos, returnLoop, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath, turnoutClosure };
1599
1623
  //# sourceMappingURL=index.js.map
1600
1624
  //# sourceMappingURL=index.js.map