@willcgage/module-schematic 0.49.6 → 0.51.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
@@ -1032,6 +1032,49 @@ interface ModuleFeatures {
1032
1032
  * right-hand throws to the opposite side. `kind` is the source of truth for the
1033
1033
  * drawn side (#bug1) — the stored lane's sign is reconciled to match it.
1034
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;
1035
1078
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1036
1079
  /** The turnout is installed the other way round — the points face the far
1037
1080
  * direction, which swaps the side the diverging route leaves on. */
@@ -1190,4 +1233,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1190
1233
  heading: number;
1191
1234
  }>;
1192
1235
 
1193
- 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
@@ -1032,6 +1032,49 @@ interface ModuleFeatures {
1032
1032
  * right-hand throws to the opposite side. `kind` is the source of truth for the
1033
1033
  * drawn side (#bug1) — the stored lane's sign is reconciled to match it.
1034
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;
1035
1078
  declare function divergeSideForHand(kind: TurnoutKind | undefined, stubDir: number,
1036
1079
  /** The turnout is installed the other way round — the points face the far
1037
1080
  * direction, which swaps the side the diverging route leaves on. */
@@ -1190,4 +1233,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1190
1233
  heading: number;
1191
1234
  }>;
1192
1235
 
1193
- 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
@@ -829,7 +829,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
829
829
  const len = fallbackLength > 0 ? fallbackLength : hasDoc ? d.lengthInches : 24;
830
830
  const docLen = hasDoc && d.lengthInches > 0 ? d.lengthInches : len;
831
831
  const scale = docLen > 0 ? len / docLen : 1;
832
- const sc = (p) => Math.round(p * scale);
832
+ const sc = (p) => Math.round(p * scale * 100) / 100;
833
833
  const nameOf = (id) => {
834
834
  const mt = id != null ? moduleTracks.find((m) => m.id === id) : void 0;
835
835
  return mt?.track_name ?? "";
@@ -1088,6 +1088,24 @@ function buildCrossover(state) {
1088
1088
  ];
1089
1089
  return { track, turnouts };
1090
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
+ }
1091
1109
  function divergeSideForHand(kind, stubDir, flipped) {
1092
1110
  if (kind !== "left" && kind !== "right") return 0;
1093
1111
  const s = (stubDir >= 0 ? 1 : -1) * (flipped ? -1 : 1);
@@ -1601,6 +1619,6 @@ function poseOverridesFromDoc(doc) {
1601
1619
  return out;
1602
1620
  }
1603
1621
 
1604
- 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 };
1605
1623
  //# sourceMappingURL=index.js.map
1606
1624
  //# sourceMappingURL=index.js.map