@willcgage/module-schematic 0.56.0 → 0.58.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;
@@ -1128,6 +1135,16 @@ interface TrackPart {
1128
1135
  innerRadius?: PartDimension;
1129
1136
  /** Crossing angle, degrees. */
1130
1137
  crossingAngleDeg?: number;
1138
+ /** The part's drawn geometry in its own frame, when it came from a library
1139
+ * file. This is the payload worth importing — real outlines we can draw
1140
+ * instead of deriving a turnout's shape from a frog number. */
1141
+ segments?: PartSegment[];
1142
+ /** Connection points, each carrying position AND tangent. The seam a piece
1143
+ * graph would snap on (#179 stage 1-2). */
1144
+ ends?: PartEnd[];
1145
+ /** Set when the part came from an imported library file rather than our own
1146
+ * data — names the source so a user can tell where a dimension came from. */
1147
+ importedFrom?: string;
1131
1148
  }
1132
1149
  /** N-scale code 55 rail height, inches — Atlas publish .055″. */
1133
1150
  declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
@@ -1257,6 +1274,56 @@ declare function parseXtpLibrary(text: string): ImportedPart[];
1257
1274
  * XTrkCAD angles run CLOCKWISE FROM NORTH, so a point on a curve is
1258
1275
  * `(cx + r·sin a, cy + r·cos a)`, not the usual cos/sin. */
1259
1276
  declare function samplePartSegments(segments: PartSegment[], stepsPerCurve?: number): BenchworkPoint[][];
1277
+ /** Pull a frog number out of a part name — "#5", "No. 5", "Number 7 Left".
1278
+ * Identification, not measurement: it reads the label the maker printed. */
1279
+ declare function frogNumberFromName(name: string | undefined): number | undefined;
1280
+ /**
1281
+ * Convert a parsed `.xtp` part into a library {@link TrackPart}.
1282
+ *
1283
+ * ⚠️⚠️ **DELIBERATELY DERIVES NO `lead` AND NO `frogOffset`.** The shipped Atlas
1284
+ * file's frog positions are internally inconsistent — it puts the #5's frog
1285
+ * FURTHER OUT than the #7's, which cannot be true of physical parts. Every lead
1286
+ * in this library is a physical measurement and must stay that way; an imported
1287
+ * lead would silently outrank {@link leadInchesForSize}'s interpolation across
1288
+ * those measurements, which is exactly the wrong trade.
1289
+ *
1290
+ * What IS taken: identity (manufacturer, name, part number, frog number), the
1291
+ * drawn `segments` and `ends`, and two dimensions the geometry states directly —
1292
+ * overall length along the through axis, and the actual divergence angle. Both
1293
+ * land as `unverified`, because a community CAD file is not a measurement.
1294
+ */
1295
+ declare function importedPartToTrackPart(part: ImportedPart, sourceName?: string): TrackPart;
1296
+ /**
1297
+ * Fold imported parts into a library.
1298
+ *
1299
+ * **Imports never overwrite. Ever.** They may only fill a gap — attach geometry
1300
+ * a built-in lacks, or add a part we have no entry for. A dimension already
1301
+ * present wins regardless of its source, because our worst built-in value is at
1302
+ * least one we can trace, and this library has already been burned once by a
1303
+ * plausible number from a CAD file (the constant-switch-angle model was fitted
1304
+ * to an `.xtp` figure that turned out to be measured to the wrong landmark).
1305
+ *
1306
+ * Matching is by manufacturer part number first, then manufacturer + frog number.
1307
+ */
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;
1260
1327
  /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1261
1328
  * route, from the points (s = 0) to the frog (s = lead) and beyond.
1262
1329
  *
@@ -1450,4 +1517,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1450
1517
  heading: number;
1451
1518
  }>;
1452
1519
 
1453
- 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, 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 };
1520
+ 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;
@@ -1128,6 +1135,16 @@ interface TrackPart {
1128
1135
  innerRadius?: PartDimension;
1129
1136
  /** Crossing angle, degrees. */
1130
1137
  crossingAngleDeg?: number;
1138
+ /** The part's drawn geometry in its own frame, when it came from a library
1139
+ * file. This is the payload worth importing — real outlines we can draw
1140
+ * instead of deriving a turnout's shape from a frog number. */
1141
+ segments?: PartSegment[];
1142
+ /** Connection points, each carrying position AND tangent. The seam a piece
1143
+ * graph would snap on (#179 stage 1-2). */
1144
+ ends?: PartEnd[];
1145
+ /** Set when the part came from an imported library file rather than our own
1146
+ * data — names the source so a user can tell where a dimension came from. */
1147
+ importedFrom?: string;
1131
1148
  }
1132
1149
  /** N-scale code 55 rail height, inches — Atlas publish .055″. */
1133
1150
  declare const CODE55_RAIL_HEIGHT_INCHES = 0.055;
@@ -1257,6 +1274,56 @@ declare function parseXtpLibrary(text: string): ImportedPart[];
1257
1274
  * XTrkCAD angles run CLOCKWISE FROM NORTH, so a point on a curve is
1258
1275
  * `(cx + r·sin a, cy + r·cos a)`, not the usual cos/sin. */
1259
1276
  declare function samplePartSegments(segments: PartSegment[], stepsPerCurve?: number): BenchworkPoint[][];
1277
+ /** Pull a frog number out of a part name — "#5", "No. 5", "Number 7 Left".
1278
+ * Identification, not measurement: it reads the label the maker printed. */
1279
+ declare function frogNumberFromName(name: string | undefined): number | undefined;
1280
+ /**
1281
+ * Convert a parsed `.xtp` part into a library {@link TrackPart}.
1282
+ *
1283
+ * ⚠️⚠️ **DELIBERATELY DERIVES NO `lead` AND NO `frogOffset`.** The shipped Atlas
1284
+ * file's frog positions are internally inconsistent — it puts the #5's frog
1285
+ * FURTHER OUT than the #7's, which cannot be true of physical parts. Every lead
1286
+ * in this library is a physical measurement and must stay that way; an imported
1287
+ * lead would silently outrank {@link leadInchesForSize}'s interpolation across
1288
+ * those measurements, which is exactly the wrong trade.
1289
+ *
1290
+ * What IS taken: identity (manufacturer, name, part number, frog number), the
1291
+ * drawn `segments` and `ends`, and two dimensions the geometry states directly —
1292
+ * overall length along the through axis, and the actual divergence angle. Both
1293
+ * land as `unverified`, because a community CAD file is not a measurement.
1294
+ */
1295
+ declare function importedPartToTrackPart(part: ImportedPart, sourceName?: string): TrackPart;
1296
+ /**
1297
+ * Fold imported parts into a library.
1298
+ *
1299
+ * **Imports never overwrite. Ever.** They may only fill a gap — attach geometry
1300
+ * a built-in lacks, or add a part we have no entry for. A dimension already
1301
+ * present wins regardless of its source, because our worst built-in value is at
1302
+ * least one we can trace, and this library has already been burned once by a
1303
+ * plausible number from a CAD file (the constant-switch-angle model was fitted
1304
+ * to an `.xtp` figure that turned out to be measured to the wrong landmark).
1305
+ *
1306
+ * Matching is by manufacturer part number first, then manufacturer + frog number.
1307
+ */
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;
1260
1327
  /** A turnout's CLOSURE — the diverging route's lateral offset from the through
1261
1328
  * route, from the points (s = 0) to the frog (s = lead) and beyond.
1262
1329
  *
@@ -1450,4 +1517,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1450
1517
  heading: number;
1451
1518
  }>;
1452
1519
 
1453
- 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, 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 };
1520
+ 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) => ({
@@ -1318,6 +1320,123 @@ function samplePartSegments(segments, stepsPerCurve = 16) {
1318
1320
  return out;
1319
1321
  });
1320
1322
  }
1323
+ function angleDeltaDeg(a, b) {
1324
+ let d = ((a - b) % 360 + 540) % 360 - 180;
1325
+ if (d === -180) d = 180;
1326
+ return d;
1327
+ }
1328
+ function frogNumberFromName(name) {
1329
+ if (!name) return void 0;
1330
+ const m = name.match(/(?:#|no\.?\s*|number\s+)(\d+(?:\.\d+)?)/i);
1331
+ if (!m) return void 0;
1332
+ const n = Number(m[1]);
1333
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1334
+ }
1335
+ function importedKind(name) {
1336
+ const s = (name ?? "").toLowerCase();
1337
+ if (s.includes("wye")) return "wye";
1338
+ if (s.includes("crossing")) return "crossing";
1339
+ if (s.includes("curved")) return "curved-turnout";
1340
+ return "turnout";
1341
+ }
1342
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1343
+ function importedPartToTrackPart(part, sourceName = "imported .xtp") {
1344
+ const name = part.name ?? part.title ?? "Imported part";
1345
+ const manufacturer = part.manufacturer ?? "Unknown";
1346
+ const note = `from ${sourceName} \u2014 community CAD data, not a measurement`;
1347
+ const out = {
1348
+ id: `xtp-${slug(manufacturer)}-${slug(part.partNumber || name)}`,
1349
+ manufacturer,
1350
+ name,
1351
+ line: "",
1352
+ scale: "N",
1353
+ kind: importedKind(name),
1354
+ frogNumber: frogNumberFromName(name),
1355
+ segments: part.segments,
1356
+ ends: part.ends,
1357
+ importedFrom: sourceName
1358
+ };
1359
+ if (part.partNumber) out.partNumbers = { single: part.partNumber };
1360
+ const ends = part.ends;
1361
+ if (ends.length >= 2) {
1362
+ const a = ends[0].angleDeg * Math.PI / 180;
1363
+ const ux = Math.sin(a);
1364
+ const uy = Math.cos(a);
1365
+ const proj = ends.map((e) => (e.x - ends[0].x) * ux + (e.y - ends[0].y) * uy);
1366
+ const span = Math.max(...proj) - Math.min(...proj);
1367
+ if (span > 0) out.overallLength = { inches: span, source: "unverified", note };
1368
+ }
1369
+ if (ends.length >= 3) {
1370
+ const rest = ends.slice(1);
1371
+ const opposite = rest.map((e) => ({ e, off: Math.abs(angleDeltaDeg(e.angleDeg, ends[0].angleDeg + 180)) })).sort((p, q) => p.off - q.off);
1372
+ const through = opposite[0].e;
1373
+ const diverging = opposite[opposite.length - 1].e;
1374
+ const deg = Math.abs(angleDeltaDeg(diverging.angleDeg, through.angleDeg));
1375
+ if (deg > 0.01) out.actualAngle = { deg, source: "unverified", note };
1376
+ }
1377
+ return out;
1378
+ }
1379
+ function mergeImportedParts(imported, library = BUILT_IN_TRACK_PARTS, sourceName = "imported .xtp") {
1380
+ const out = library.map((p) => ({ ...p }));
1381
+ const numbersOf = (p) => [p.partNumbers?.left, p.partNumbers?.right, p.partNumbers?.single].filter(Boolean).map((s) => s.trim().toLowerCase());
1382
+ for (const raw of imported) {
1383
+ const conv = importedPartToTrackPart(raw, sourceName);
1384
+ const pn = raw.partNumber?.trim().toLowerCase();
1385
+ const match = (pn ? out.find((p) => numbersOf(p).includes(pn)) : void 0) ?? (conv.frogNumber != null ? out.find(
1386
+ (p) => p.frogNumber === conv.frogNumber && p.kind === conv.kind && p.manufacturer.toLowerCase() === conv.manufacturer.toLowerCase()
1387
+ ) : void 0);
1388
+ if (!match) {
1389
+ out.push(conv);
1390
+ continue;
1391
+ }
1392
+ if (!match.segments?.length && conv.segments?.length) match.segments = conv.segments;
1393
+ if (!match.ends?.length && conv.ends?.length) match.ends = conv.ends;
1394
+ if (!match.importedFrom && (conv.segments?.length || conv.ends?.length)) {
1395
+ match.importedFrom = sourceName;
1396
+ }
1397
+ if (!match.overallLength && conv.overallLength) match.overallLength = conv.overallLength;
1398
+ if (!match.actualAngle && conv.actualAngle) match.actualAngle = conv.actualAngle;
1399
+ }
1400
+ return out;
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
+ }
1321
1440
  function turnoutClosure(size, opts = {}) {
1322
1441
  const N = size > 0 ? size : 6;
1323
1442
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
@@ -1847,6 +1966,6 @@ function poseOverridesFromDoc(doc) {
1847
1966
  return out;
1848
1967
  }
1849
1968
 
1850
- 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, fromSectionRelative, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, 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 };
1969
+ 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 };
1851
1970
  //# sourceMappingURL=index.js.map
1852
1971
  //# sourceMappingURL=index.js.map