@willcgage/module-schematic 0.47.0 → 0.49.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
@@ -253,6 +253,10 @@ interface ModuleSchematicDoc {
253
253
  * +y up). Stored as an open ring; renderers close it. Absent = derive an
254
254
  * approximate band from the endplate widths. */
255
255
  outline?: BenchworkPoint[];
256
+ /** Benchwork HOLE — an inner boundary punched out of `outline`, making the
257
+ * board a DONUT (a return loop's open middle). Same frame as `outline`; stored
258
+ * as an open ring, renderers close it. Absent = a solid board. */
259
+ outlineInner?: BenchworkPoint[];
256
260
  /** Internal section joints — inches from endplate A where the module's boards
257
261
  * split into sections. Operationally one unit; these mark construction/transport
258
262
  * seams (exempt from the end-interface standards). Empty/absent = one section.
@@ -405,6 +409,9 @@ interface ModuleFootprintInput {
405
409
  endplateTrackOffsets?: Record<string, number>;
406
410
  /** Authored benchwork outline (module-local inches), or absent for the band. */
407
411
  outline?: BenchworkPoint[] | null;
412
+ /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
413
+ * make a donut. Absent/short = a solid board. */
414
+ outlineInner?: BenchworkPoint[] | null;
408
415
  /** The module's sections (#96 phase 2). When any carries an outline, the
409
416
  * module's footprint is the union of those — `outline` is then ignored. */
410
417
  sections?: SchematicSection[] | null;
@@ -432,6 +439,9 @@ interface ModuleFootprint {
432
439
  /** Authored outline (arc-sampled closed ring) or null → render the band.
433
440
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
434
441
  outline: BenchworkPoint[] | null;
442
+ /** Authored HOLE (arc-sampled closed ring) punched out of `outline` → a donut
443
+ * board. Null = solid. Renderers fill `outline` with `outlineInner` cut out. */
444
+ outlineInner: BenchworkPoint[] | null;
435
445
  /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
436
446
  * they are the module's footprint. Empty = this module doesn't use sections,
437
447
  * so fall back to `outline ?? band` exactly as before. */
@@ -793,6 +803,9 @@ interface EditorState {
793
803
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
794
804
  * Empty = no authored outline (fall back to the endplate-width band). */
795
805
  outline: BenchworkPoint[];
806
+ /** Benchwork HOLE (donut inner boundary), module-local inches. Empty = a solid
807
+ * board. Set by the return-loop generator; punched out of `outline`. */
808
+ outlineInner: BenchworkPoint[];
796
809
  /** Internal section joints — inches from endplate A where the boards split
797
810
  * into sections. Empty = a single section (#48). */
798
811
  sectionBreaks: number[];
@@ -1042,6 +1055,31 @@ interface EndplatePose {
1042
1055
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
1043
1056
  manual?: boolean;
1044
1057
  }
1058
+ type ReturnLoopShape = "circle" | "teardrop" | "offset-teardrop" | "square";
1059
+ /** A computed return-loop: a mainline LEAD to the throat, a WYE that splits into
1060
+ * two legs, and the LOOP track those legs form, closing back at the throat. Pure
1061
+ * geometry, module-local inches, endplate A at the origin, lead heading +x. All
1062
+ * shapes are built so the loop CLOSES exactly at the throat and the two wye legs
1063
+ * diverge symmetrically about the lead — the piece the earlier eyeballed versions
1064
+ * kept getting wrong (#loop). */
1065
+ interface ReturnLoopGeometry {
1066
+ throat: BenchworkPoint;
1067
+ /** The loop track, throat → around → throat (starts and ends at the throat). */
1068
+ loop: BenchworkPoint[];
1069
+ /** The two diverging wye legs at the throat (each throat → its tangent point). */
1070
+ wyeLegs: [BenchworkPoint[], BenchworkPoint[]];
1071
+ /** Half the angle between the two wye legs, degrees (0 = tangent/no wye). */
1072
+ wyeHalfAngleDeg: number;
1073
+ /** Benchwork fascia: outer boundary, and an inner boundary for a donut (empty
1074
+ * when the board is solid). */
1075
+ outlineOuter: BenchworkPoint[];
1076
+ outlineInner: BenchworkPoint[];
1077
+ }
1078
+ declare function returnLoop(shape: ReturnLoopShape, opts: {
1079
+ leadInches: number;
1080
+ radius: number;
1081
+ boardHalfWidth?: number;
1082
+ }): ReturnLoopGeometry;
1045
1083
  interface ModuleGeometryInput {
1046
1084
  lengthInches: number;
1047
1085
  geometryType?: string | null;
@@ -1141,4 +1179,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1141
1179
  heading: number;
1142
1180
  }>;
1143
1181
 
1144
- 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 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, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1182
+ 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 };
package/dist/index.d.ts CHANGED
@@ -253,6 +253,10 @@ interface ModuleSchematicDoc {
253
253
  * +y up). Stored as an open ring; renderers close it. Absent = derive an
254
254
  * approximate band from the endplate widths. */
255
255
  outline?: BenchworkPoint[];
256
+ /** Benchwork HOLE — an inner boundary punched out of `outline`, making the
257
+ * board a DONUT (a return loop's open middle). Same frame as `outline`; stored
258
+ * as an open ring, renderers close it. Absent = a solid board. */
259
+ outlineInner?: BenchworkPoint[];
256
260
  /** Internal section joints — inches from endplate A where the module's boards
257
261
  * split into sections. Operationally one unit; these mark construction/transport
258
262
  * seams (exempt from the end-interface standards). Empty/absent = one section.
@@ -405,6 +409,9 @@ interface ModuleFootprintInput {
405
409
  endplateTrackOffsets?: Record<string, number>;
406
410
  /** Authored benchwork outline (module-local inches), or absent for the band. */
407
411
  outline?: BenchworkPoint[] | null;
412
+ /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
413
+ * make a donut. Absent/short = a solid board. */
414
+ outlineInner?: BenchworkPoint[] | null;
408
415
  /** The module's sections (#96 phase 2). When any carries an outline, the
409
416
  * module's footprint is the union of those — `outline` is then ignored. */
410
417
  sections?: SchematicSection[] | null;
@@ -432,6 +439,9 @@ interface ModuleFootprint {
432
439
  /** Authored outline (arc-sampled closed ring) or null → render the band.
433
440
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
434
441
  outline: BenchworkPoint[] | null;
442
+ /** Authored HOLE (arc-sampled closed ring) punched out of `outline` → a donut
443
+ * board. Null = solid. Renderers fill `outline` with `outlineInner` cut out. */
444
+ outlineInner: BenchworkPoint[] | null;
435
445
  /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
436
446
  * they are the module's footprint. Empty = this module doesn't use sections,
437
447
  * so fall back to `outline ?? band` exactly as before. */
@@ -793,6 +803,9 @@ interface EditorState {
793
803
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
794
804
  * Empty = no authored outline (fall back to the endplate-width band). */
795
805
  outline: BenchworkPoint[];
806
+ /** Benchwork HOLE (donut inner boundary), module-local inches. Empty = a solid
807
+ * board. Set by the return-loop generator; punched out of `outline`. */
808
+ outlineInner: BenchworkPoint[];
796
809
  /** Internal section joints — inches from endplate A where the boards split
797
810
  * into sections. Empty = a single section (#48). */
798
811
  sectionBreaks: number[];
@@ -1042,6 +1055,31 @@ interface EndplatePose {
1042
1055
  /** True when the pose was hand-entered, not derived (wye/loop/other). */
1043
1056
  manual?: boolean;
1044
1057
  }
1058
+ type ReturnLoopShape = "circle" | "teardrop" | "offset-teardrop" | "square";
1059
+ /** A computed return-loop: a mainline LEAD to the throat, a WYE that splits into
1060
+ * two legs, and the LOOP track those legs form, closing back at the throat. Pure
1061
+ * geometry, module-local inches, endplate A at the origin, lead heading +x. All
1062
+ * shapes are built so the loop CLOSES exactly at the throat and the two wye legs
1063
+ * diverge symmetrically about the lead — the piece the earlier eyeballed versions
1064
+ * kept getting wrong (#loop). */
1065
+ interface ReturnLoopGeometry {
1066
+ throat: BenchworkPoint;
1067
+ /** The loop track, throat → around → throat (starts and ends at the throat). */
1068
+ loop: BenchworkPoint[];
1069
+ /** The two diverging wye legs at the throat (each throat → its tangent point). */
1070
+ wyeLegs: [BenchworkPoint[], BenchworkPoint[]];
1071
+ /** Half the angle between the two wye legs, degrees (0 = tangent/no wye). */
1072
+ wyeHalfAngleDeg: number;
1073
+ /** Benchwork fascia: outer boundary, and an inner boundary for a donut (empty
1074
+ * when the board is solid). */
1075
+ outlineOuter: BenchworkPoint[];
1076
+ outlineInner: BenchworkPoint[];
1077
+ }
1078
+ declare function returnLoop(shape: ReturnLoopShape, opts: {
1079
+ leadInches: number;
1080
+ radius: number;
1081
+ boardHalfWidth?: number;
1082
+ }): ReturnLoopGeometry;
1045
1083
  interface ModuleGeometryInput {
1046
1084
  lengthInches: number;
1047
1085
  geometryType?: string | null;
@@ -1141,4 +1179,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1141
1179
  heading: number;
1142
1180
  }>;
1143
1181
 
1144
- 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 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, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1182
+ 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 };
package/dist/index.js CHANGED
@@ -476,6 +476,9 @@ function moduleFootprint(input) {
476
476
  // endplate A's face (the far face would be a spurious plate at the throat).
477
477
  endplateFaces: input.loop ? endplateFaceSegments(centerline, widthA, widthB, offA, offB).slice(0, 1) : endplateFaceSegments(centerline, widthA, widthB, offA, offB),
478
478
  outline: sectionOutlines.length || !authored ? null : sampleBenchworkOutline(authored),
479
+ // The donut hole, arc-sampled — only when there's a solid outline to punch it
480
+ // out of (a sectioned module isn't a donut). Renderers cut it from `outline`.
481
+ outlineInner: sectionOutlines.length || !authored || !input.outlineInner || input.outlineInner.length < 3 ? null : sampleBenchworkOutline(input.outlineInner),
479
482
  sectionOutlines
480
483
  };
481
484
  }
@@ -557,6 +560,7 @@ function emptyEditorState(lengthInches) {
557
560
  endplateWidths: {},
558
561
  endplateTrackOffsets: {},
559
562
  outline: [],
563
+ outlineInner: [],
560
564
  sectionBreaks: [],
561
565
  sections: [],
562
566
  controlPoints: [],
@@ -793,6 +797,8 @@ function stateToDoc(state, recordNumber) {
793
797
  // Benchwork footprint outline (module-local inches); only when it's a real
794
798
  // ring (≥ 3 vertices).
795
799
  ...state.outline.length >= 3 ? { outline: state.outline } : {},
800
+ // Benchwork hole (donut inner boundary), when it's a real ring.
801
+ ...state.outlineInner.length >= 3 ? { outlineInner: state.outlineInner } : {},
796
802
  // Internal section joints (inches from A), when the module has more than one.
797
803
  ...state.sectionBreaks.length ? { sectionBreaks: state.sectionBreaks } : {},
798
804
  // Sections as objects — emitted only once the owner has some, so a module
@@ -884,6 +890,11 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
884
890
  y: p.y,
885
891
  ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
886
892
  }));
893
+ const outlineInner = (d.outlineInner ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
894
+ x: p.x,
895
+ y: p.y,
896
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
897
+ }));
887
898
  const mainPath = trackPath(d.mainPath) ?? [];
888
899
  const main2Track_ = (d.tracks ?? []).find((t) => t.id === MAIN2_TRACK_ID);
889
900
  const main2Path = trackPath(d.main2Path ?? main2Track_?.path) ?? [];
@@ -907,6 +918,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
907
918
  endplateWidths,
908
919
  endplateTrackOffsets,
909
920
  outline,
921
+ outlineInner,
910
922
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
911
923
  sections: moduleSections(d),
912
924
  mainPath,
@@ -1320,6 +1332,84 @@ function moduleFeatures(doc) {
1320
1332
  laneMax: Math.max(...allLanes)
1321
1333
  };
1322
1334
  }
1335
+ function returnLoop(shape, opts) {
1336
+ const L = Math.max(1, opts.leadInches);
1337
+ const R = Math.max(1, opts.radius);
1338
+ const hw = Math.min(opts.boardHalfWidth ?? 6, R - 1);
1339
+ const T = { x: L, y: 0 };
1340
+ const r2 = (v) => Math.round(v * 100) / 100;
1341
+ const rd = (pts) => pts.map((p) => ({ x: r2(p.x), y: r2(p.y) }));
1342
+ const arc = (cx, cy, r, a0, a12, steps) => {
1343
+ const out = [];
1344
+ for (let i = 0; i <= steps; i++) out.push({ x: cx + r * Math.cos(a0 + (a12 - a0) * i / steps), y: cy + r * Math.sin(a0 + (a12 - a0) * i / steps) });
1345
+ return out;
1346
+ };
1347
+ const D = shape === "circle" || shape === "square" ? R * 1.15 : R * 1.6;
1348
+ const offY = shape === "offset-teardrop" ? R * 0.7 : 0;
1349
+ const C = { x: L + D, y: offY };
1350
+ const dist = Math.hypot(C.x - T.x, C.y - T.y);
1351
+ const ac = Math.acos(Math.min(1, R / dist));
1352
+ const base = Math.atan2(T.y - C.y, T.x - C.x);
1353
+ const a1 = base + ac;
1354
+ const a2 = base - ac;
1355
+ const P1 = { x: C.x + R * Math.cos(a1), y: C.y + R * Math.sin(a1) };
1356
+ const P2 = { x: C.x + R * Math.cos(a2), y: C.y + R * Math.sin(a2) };
1357
+ const loop = [T, P1, ...arc(C.x, C.y, R, a1, a2 + 2 * Math.PI, 64).slice(1), T];
1358
+ const legDir = (P) => Math.atan2(P.y - T.y, P.x - T.x);
1359
+ const half = Math.abs((legDir(P1) - legDir(P2) + Math.PI) % (2 * Math.PI) - Math.PI) / 2;
1360
+ const ringOuter = (rr) => {
1361
+ const xTop = C.x - Math.sqrt(Math.max(0, rr * rr - (hw - C.y) ** 2));
1362
+ const xBot = C.x - Math.sqrt(Math.max(0, rr * rr - (-hw - C.y) ** 2));
1363
+ const tTop = Math.atan2(hw - C.y, xTop - C.x);
1364
+ const tBot = Math.atan2(-hw - C.y, xBot - C.x);
1365
+ return [
1366
+ { x: 0, y: hw },
1367
+ { x: xTop, y: hw },
1368
+ ...arc(C.x, C.y, rr, tTop, tBot - 2 * Math.PI, 48).slice(1, -1),
1369
+ { x: xBot, y: -hw },
1370
+ { x: 0, y: -hw }
1371
+ ];
1372
+ };
1373
+ const Ri = R - hw;
1374
+ if (shape === "square") {
1375
+ const xL = C.x - R - hw;
1376
+ const xR = C.x + R + hw;
1377
+ const yT = R + hw;
1378
+ const outerSquare = [
1379
+ { x: 0, y: hw },
1380
+ { x: xL, y: hw },
1381
+ { x: xL, y: yT },
1382
+ { x: xR, y: yT },
1383
+ { x: xR, y: -yT },
1384
+ { x: xL, y: -yT },
1385
+ { x: xL, y: -hw },
1386
+ { x: 0, y: -hw }
1387
+ ];
1388
+ const iHalf = (R - hw) / Math.SQRT2;
1389
+ const holeSquare = iHalf > 2 ? [
1390
+ { x: C.x - iHalf, y: iHalf },
1391
+ { x: C.x + iHalf, y: iHalf },
1392
+ { x: C.x + iHalf, y: -iHalf },
1393
+ { x: C.x - iHalf, y: -iHalf }
1394
+ ] : [];
1395
+ return {
1396
+ throat: T,
1397
+ loop: rd(loop),
1398
+ wyeLegs: [rd([T, P1]), rd([T, P2])],
1399
+ wyeHalfAngleDeg: r2(half * 180 / Math.PI),
1400
+ outlineOuter: rd(outerSquare),
1401
+ outlineInner: rd(holeSquare)
1402
+ };
1403
+ }
1404
+ return {
1405
+ throat: T,
1406
+ loop: rd(loop),
1407
+ wyeLegs: [rd([T, P1]), rd([T, P2])],
1408
+ wyeHalfAngleDeg: r2(half * 180 / Math.PI),
1409
+ outlineOuter: rd(ringOuter(R + hw)),
1410
+ outlineInner: Ri > 2 ? rd(arc(C.x, C.y, Ri, 0, 2 * Math.PI, 40)) : []
1411
+ };
1412
+ }
1323
1413
  function geometryTurnDegrees(geometryType, geometryDegrees) {
1324
1414
  switch (geometryType) {
1325
1415
  case "corner_45":
@@ -1491,6 +1581,6 @@ function poseOverridesFromDoc(doc) {
1491
1581
  return out;
1492
1582
  }
1493
1583
 
1494
- 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, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionSpansOrWhole, sectionedCenterline, sectionedEndPose, sliceCenterline, stateToDoc, toSectionRelative, trackMeetsEndplateIssues, trackPath };
1584
+ 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 };
1495
1585
  //# sourceMappingURL=index.js.map
1496
1586
  //# sourceMappingURL=index.js.map