@willcgage/module-schematic 0.40.0 → 0.42.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
@@ -13,7 +13,7 @@
13
13
  * from Next.js (server + client) and Electron alike.
14
14
  */
15
15
  type TrackConfig = "single" | "double";
16
- type TrackRole = "main" | "siding" | "spur" | "yard" | "crossover";
16
+ type TrackRole = "main" | "siding" | "spur" | "yard" | "crossover" | "branch";
17
17
  type TurnoutKind = "left" | "right" | "wye";
18
18
  type SignalFacing = "AtoB" | "BtoA";
19
19
  type SignalSide = "above" | "below";
@@ -57,6 +57,14 @@ interface SchematicEndplate {
57
57
  * of the two tracks at its double end. Absent = the recommended default
58
58
  * (single centred; double straddling at ∓ half the track spacing). */
59
59
  trackOffsetInches?: number | null;
60
+ /** Branch endplates (#170) only — what the route reaching this endplate IS:
61
+ * a secondary "branch" line, or a "main" (a diverging / split main). Drives
62
+ * labels + drawn weight, not geometry. Absent = "branch". */
63
+ kind?: "branch" | "main" | null;
64
+ /** Branch endplates only — the id of the drawn diverging track that reaches
65
+ * this endplate (its authored path ends at the plate face). Lets remove/
66
+ * round-trip pair the endplate with its track. Absent = not yet connected. */
67
+ trackId?: string | null;
60
68
  }
61
69
  /** Free-moN endplate face width, inches — the connection interface size. */
62
70
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
@@ -732,6 +740,12 @@ interface EditorBranch {
732
740
  pos: number;
733
741
  side: "up" | "down";
734
742
  config: TrackConfig;
743
+ /** What the route to this endplate is — a secondary "branch" or a diverging
744
+ * "main". Drives labels/weight, not geometry. Default "branch". */
745
+ kind?: "branch" | "main";
746
+ /** The drawn diverging track that reaches this endplate (set when the owner
747
+ * draws track to it; the plate is placed first, connected later). */
748
+ trackId?: string | null;
735
749
  }
736
750
  /** Endplate B on a loop module: a standard endplate makes the balloon an
737
751
  * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
@@ -1064,6 +1078,50 @@ declare function geometryTurnDegrees(geometryType?: string | null, geometryDegre
1064
1078
  * side. A `dead_end`/loop module has no B. Overrides replace any derived pose.
1065
1079
  */
1066
1080
  declare function deriveEndplatePoses(geo: ModuleGeometryInput): EndplatePose[];
1081
+ /** Free-moN §2.0 — track crossing an endplate must be perpendicular, straight
1082
+ * and level for at least this many inches from the outside face. */
1083
+ declare const ENDPLATE_LEAD_INCHES = 4;
1084
+ /** Free-moN §2.0 — the crossing must stay at least this far from either fascia. */
1085
+ declare const ENDPLATE_FASCIA_CLEAR_INCHES = 4;
1086
+ /** The mandated straight, perpendicular lead a connecting track must follow
1087
+ * leaving an endplate (§2.0). Given the endplate's track point + outward-normal
1088
+ * heading, returns the face point and the point `leadInches` inboard, plus the
1089
+ * inward heading. A track meeting this plate must be collinear with face→inboard
1090
+ * for its first `leadInches`. */
1091
+ declare function endplateLead(pose: {
1092
+ x: number;
1093
+ y: number;
1094
+ heading: number;
1095
+ }, leadInches?: number): {
1096
+ face: BenchworkPoint;
1097
+ inboard: BenchworkPoint;
1098
+ inwardHeading: number;
1099
+ };
1100
+ interface EndplateTrackIssue {
1101
+ /** "not-perpendicular" — the track doesn't cross square to the face;
1102
+ * "short-lead" — it curves/bends within the required lead;
1103
+ * "fascia-clearance" — the crossing is <4″ from a fascia. */
1104
+ code: "not-perpendicular" | "short-lead" | "fascia-clearance";
1105
+ message: string;
1106
+ }
1107
+ /**
1108
+ * Validate a drawn path meeting an endplate against Free-moN §2.0: perpendicular
1109
+ * crossing, straight + level for ≥4″ from the face, ≥4″ from either fascia.
1110
+ * `end` says which end of the authored path touches the plate ("last" default).
1111
+ * Fascia clearance is checked only when both the face width and the track's
1112
+ * offset from the plate centre are supplied. Pure; empty array = compliant.
1113
+ */
1114
+ declare function trackMeetsEndplateIssues(path: BenchworkPoint[], pose: {
1115
+ x: number;
1116
+ y: number;
1117
+ heading: number;
1118
+ }, opts?: {
1119
+ end?: "first" | "last";
1120
+ faceWidthInches?: number;
1121
+ trackOffsetInches?: number;
1122
+ leadInches?: number;
1123
+ toleranceDeg?: number;
1124
+ }): EndplateTrackIssue[];
1067
1125
  /** Whether a module shape's poses are fully derivable, or need manual entry
1068
1126
  * (a helpful cue for the authoring UI). */
1069
1127
  declare function poseNeedsManual(geometryType?: string | null): boolean;
@@ -1075,4 +1133,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1075
1133
  heading: number;
1076
1134
  }>;
1077
1135
 
1078
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, 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, 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, trackPath };
1136
+ 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 };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * from Next.js (server + client) and Electron alike.
14
14
  */
15
15
  type TrackConfig = "single" | "double";
16
- type TrackRole = "main" | "siding" | "spur" | "yard" | "crossover";
16
+ type TrackRole = "main" | "siding" | "spur" | "yard" | "crossover" | "branch";
17
17
  type TurnoutKind = "left" | "right" | "wye";
18
18
  type SignalFacing = "AtoB" | "BtoA";
19
19
  type SignalSide = "above" | "below";
@@ -57,6 +57,14 @@ interface SchematicEndplate {
57
57
  * of the two tracks at its double end. Absent = the recommended default
58
58
  * (single centred; double straddling at ∓ half the track spacing). */
59
59
  trackOffsetInches?: number | null;
60
+ /** Branch endplates (#170) only — what the route reaching this endplate IS:
61
+ * a secondary "branch" line, or a "main" (a diverging / split main). Drives
62
+ * labels + drawn weight, not geometry. Absent = "branch". */
63
+ kind?: "branch" | "main" | null;
64
+ /** Branch endplates only — the id of the drawn diverging track that reaches
65
+ * this endplate (its authored path ends at the plate face). Lets remove/
66
+ * round-trip pair the endplate with its track. Absent = not yet connected. */
67
+ trackId?: string | null;
60
68
  }
61
69
  /** Free-moN endplate face width, inches — the connection interface size. */
62
70
  declare const FREEMO_ENDPLATE_WIDTH_MIN_INCHES = 12;
@@ -732,6 +740,12 @@ interface EditorBranch {
732
740
  pos: number;
733
741
  side: "up" | "down";
734
742
  config: TrackConfig;
743
+ /** What the route to this endplate is — a secondary "branch" or a diverging
744
+ * "main". Drives labels/weight, not geometry. Default "branch". */
745
+ kind?: "branch" | "main";
746
+ /** The drawn diverging track that reaches this endplate (set when the owner
747
+ * draws track to it; the plate is placed first, connected later). */
748
+ trackId?: string | null;
735
749
  }
736
750
  /** Endplate B on a loop module: a standard endplate makes the balloon an
737
751
  * INTERCHANGE (a second route connects at the loop, e.g. Seaford); "none"
@@ -1064,6 +1078,50 @@ declare function geometryTurnDegrees(geometryType?: string | null, geometryDegre
1064
1078
  * side. A `dead_end`/loop module has no B. Overrides replace any derived pose.
1065
1079
  */
1066
1080
  declare function deriveEndplatePoses(geo: ModuleGeometryInput): EndplatePose[];
1081
+ /** Free-moN §2.0 — track crossing an endplate must be perpendicular, straight
1082
+ * and level for at least this many inches from the outside face. */
1083
+ declare const ENDPLATE_LEAD_INCHES = 4;
1084
+ /** Free-moN §2.0 — the crossing must stay at least this far from either fascia. */
1085
+ declare const ENDPLATE_FASCIA_CLEAR_INCHES = 4;
1086
+ /** The mandated straight, perpendicular lead a connecting track must follow
1087
+ * leaving an endplate (§2.0). Given the endplate's track point + outward-normal
1088
+ * heading, returns the face point and the point `leadInches` inboard, plus the
1089
+ * inward heading. A track meeting this plate must be collinear with face→inboard
1090
+ * for its first `leadInches`. */
1091
+ declare function endplateLead(pose: {
1092
+ x: number;
1093
+ y: number;
1094
+ heading: number;
1095
+ }, leadInches?: number): {
1096
+ face: BenchworkPoint;
1097
+ inboard: BenchworkPoint;
1098
+ inwardHeading: number;
1099
+ };
1100
+ interface EndplateTrackIssue {
1101
+ /** "not-perpendicular" — the track doesn't cross square to the face;
1102
+ * "short-lead" — it curves/bends within the required lead;
1103
+ * "fascia-clearance" — the crossing is <4″ from a fascia. */
1104
+ code: "not-perpendicular" | "short-lead" | "fascia-clearance";
1105
+ message: string;
1106
+ }
1107
+ /**
1108
+ * Validate a drawn path meeting an endplate against Free-moN §2.0: perpendicular
1109
+ * crossing, straight + level for ≥4″ from the face, ≥4″ from either fascia.
1110
+ * `end` says which end of the authored path touches the plate ("last" default).
1111
+ * Fascia clearance is checked only when both the face width and the track's
1112
+ * offset from the plate centre are supplied. Pure; empty array = compliant.
1113
+ */
1114
+ declare function trackMeetsEndplateIssues(path: BenchworkPoint[], pose: {
1115
+ x: number;
1116
+ y: number;
1117
+ heading: number;
1118
+ }, opts?: {
1119
+ end?: "first" | "last";
1120
+ faceWidthInches?: number;
1121
+ trackOffsetInches?: number;
1122
+ leadInches?: number;
1123
+ toleranceDeg?: number;
1124
+ }): EndplateTrackIssue[];
1067
1125
  /** Whether a module shape's poses are fully derivable, or need manual entry
1068
1126
  * (a helpful cue for the authoring UI). */
1069
1127
  declare function poseNeedsManual(geometryType?: string | null): boolean;
@@ -1075,4 +1133,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1075
1133
  heading: number;
1076
1134
  }>;
1077
1135
 
1078
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, 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, 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, trackPath };
1136
+ 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 };
package/dist/index.js CHANGED
@@ -615,7 +615,11 @@ function buildTransition(state) {
615
615
  // Branton, #131). Main 1 runs the full module; Main 2 is the branch.
616
616
  onTrack: MAIN_TRACK_ID,
617
617
  divergeTrack: MAIN2_TRACK_ID,
618
- kind: aDouble ? "left" : "right"
618
+ // Hand so the diverging leg lands on Main 2's side. Main 2 extends toward
619
+ // the double end (sign −1 west / +1 east) and sits above (+1) or, when the
620
+ // mains are swapped, below (−1). divergeSideForHand(left)=sign(toward),
621
+ // (right)=−sign(toward), so pick the hand whose side matches Main 2's (#131).
622
+ kind: (aDouble ? -1 : 1) === (state.mainsSwapped ? -1 : 1) ? "left" : "right"
619
623
  };
620
624
  const cpId = nextId("cp", state.controlPoints.map((c) => c.id));
621
625
  const sig = (facing) => ({
@@ -689,7 +693,9 @@ function stateToDoc(state, recordNumber) {
689
693
  // C, D, E…
690
694
  label: b.label || `Branch ${i + 1}`,
691
695
  tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: b.config }],
692
- at: { pos: b.pos, side: b.side }
696
+ at: { pos: b.pos, side: b.side },
697
+ kind: b.kind ?? "branch",
698
+ ...b.trackId ? { trackId: b.trackId } : {}
693
699
  }))
694
700
  ],
695
701
  state.poseOverrides
@@ -891,7 +897,9 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
891
897
  label: ep.label ?? "Branch",
892
898
  pos: sc(ep.at.pos),
893
899
  side: ep.at.side === "down" ? "down" : "up",
894
- config: ep.tracks?.[0]?.config === "double" ? "double" : "single"
900
+ config: ep.tracks?.[0]?.config === "double" ? "double" : "single",
901
+ kind: ep.kind === "main" ? "main" : "branch",
902
+ trackId: ep.trackId ?? null
895
903
  })),
896
904
  poseOverrides,
897
905
  endplateWidths,
@@ -1401,6 +1409,67 @@ function deriveEndplatePoses(geo) {
1401
1409
  }
1402
1410
  return poses;
1403
1411
  }
1412
+ var ENDPLATE_LEAD_INCHES = 4;
1413
+ var ENDPLATE_FASCIA_CLEAR_INCHES = 4;
1414
+ function endplateLead(pose, leadInches = ENDPLATE_LEAD_INCHES) {
1415
+ const inwardHeading = norm360(pose.heading + 180);
1416
+ const r = inwardHeading * DEG;
1417
+ return {
1418
+ face: { x: pose.x, y: pose.y },
1419
+ inboard: { x: pose.x + Math.cos(r) * leadInches, y: pose.y + Math.sin(r) * leadInches },
1420
+ inwardHeading
1421
+ };
1422
+ }
1423
+ function trackMeetsEndplateIssues(path, pose, opts) {
1424
+ const issues = [];
1425
+ const lead = opts?.leadInches ?? ENDPLATE_LEAD_INCHES;
1426
+ const tol = opts?.toleranceDeg ?? 5;
1427
+ if (path && path.length >= 2) {
1428
+ const seq = (opts?.end ?? "last") === "last" ? [...path].reverse() : path.slice();
1429
+ const p0 = seq[0];
1430
+ const p1 = seq[1];
1431
+ const wantIn = norm360(pose.heading + 180);
1432
+ const inHead = norm360(Math.atan2(p1.y - p0.y, p1.x - p0.x) / DEG);
1433
+ const diff = Math.abs((inHead - wantIn + 540) % 360 - 180);
1434
+ if (diff > tol)
1435
+ issues.push({
1436
+ code: "not-perpendicular",
1437
+ message: `Track must cross the endplate square (within ${tol}\xB0); it is off by ${Math.round(diff)}\xB0.`
1438
+ });
1439
+ const r = wantIn * DEG;
1440
+ const ux = Math.cos(r);
1441
+ const uy = Math.sin(r);
1442
+ let curved = false;
1443
+ let acc = 0;
1444
+ for (let i = 1; i < seq.length && acc < lead; i++) {
1445
+ const a = seq[i - 1];
1446
+ const b = seq[i];
1447
+ if (a.bulge) curved = true;
1448
+ const rx = b.x - p0.x;
1449
+ const ry = b.y - p0.y;
1450
+ const along = rx * ux + ry * uy;
1451
+ const lat = Math.abs(rx * -uy + ry * ux);
1452
+ if (along <= lead + 0.01 && lat > 0.25) curved = true;
1453
+ acc += Math.hypot(b.x - a.x, b.y - a.y);
1454
+ }
1455
+ if (curved)
1456
+ issues.push({
1457
+ code: "short-lead",
1458
+ message: `The first ${lead}\u2033 from the endplate must be straight and perpendicular.`
1459
+ });
1460
+ }
1461
+ const w = opts?.faceWidthInches;
1462
+ const off = opts?.trackOffsetInches;
1463
+ if (typeof w === "number" && w > 0 && typeof off === "number") {
1464
+ const clear = w / 2 - Math.abs(off);
1465
+ if (clear < ENDPLATE_FASCIA_CLEAR_INCHES)
1466
+ issues.push({
1467
+ code: "fascia-clearance",
1468
+ message: `Track must stay \u2265${ENDPLATE_FASCIA_CLEAR_INCHES}\u2033 from either fascia; it is ${clear.toFixed(1)}\u2033.`
1469
+ });
1470
+ }
1471
+ return issues;
1472
+ }
1404
1473
  function poseNeedsManual(geometryType) {
1405
1474
  return geometryType === "wye" || geometryType === "other";
1406
1475
  }
@@ -1414,6 +1483,6 @@ function poseOverridesFromDoc(doc) {
1414
1483
  return out;
1415
1484
  }
1416
1485
 
1417
- export { 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, 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, trackPath };
1486
+ 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 };
1418
1487
  //# sourceMappingURL=index.js.map
1419
1488
  //# sourceMappingURL=index.js.map