@willcgage/module-schematic 0.60.0 → 0.62.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
@@ -43,6 +43,19 @@ interface SchematicEndplate {
43
43
  y: number;
44
44
  heading: number;
45
45
  };
46
+ /** Whether that pose was AUTHORED — placed or typed by the owner — rather
47
+ * than a derived one that found its way into the doc (#182).
48
+ *
49
+ * Authorship used to be inferred from `pose` merely existing, so a derived
50
+ * pose written back silently PINNED the plate: it stopped following the
51
+ * module and went stale the moment the length changed (FMN-0068 ended up with
52
+ * endplate B at x=48 on a 47.9″ board). A pose is only an override when the
53
+ * owner meant it.
54
+ *
55
+ * Absent on A/B = derived residue, ignored. Absent on a placed branch
56
+ * endplate (one with `at`) = still authored — placing it IS the gesture, and
57
+ * docs predate this flag. */
58
+ poseAuthored?: boolean;
46
59
  /** Free-moN endplate FACE width across the track, inches — the physical size
47
60
  * of the standard interface at this end. Free-moN spec: 12″ minimum, 24″
48
61
  * recommended. Absent = the recommended default (modules may differ end to
@@ -394,6 +407,13 @@ declare function samplePath(pts: BenchworkPoint[], segsPerArc?: number): {
394
407
  x: number;
395
408
  y: number;
396
409
  }[];
410
+ /**
411
+ * Arc length of an authored path, inches — the path's OWN length, independent of
412
+ * where it sits or which way it runs. A branch route to a third endplate (#181)
413
+ * has no meaningful extent along the module axis (a square 90° exit projects to
414
+ * zero), so its own length is the only honest measure of it.
415
+ */
416
+ declare function pathLengthInches(path: BenchworkPoint[] | null | undefined): number;
397
417
  /** Normalise an authored track path from a doc, or null if it isn't a real path
398
418
  * (needs ≥ 2 valid points). Keeps per-vertex bulge. */
399
419
  declare function trackPath(path: BenchworkPoint[] | null | undefined): BenchworkPoint[] | null;
@@ -951,13 +971,47 @@ interface DrawCrossover {
951
971
  toPosFrac: number;
952
972
  toLane: number;
953
973
  }
954
- /** A branch endplate — draw a connector stub + arrow + label off the given
955
- * side (the CATS/US&S off-band idiom, #170). */
974
+ /**
975
+ * A route leaving the module at a third endplate (#170) — Main 3, whatever the
976
+ * owner labels it (#181). Drawn as an ELBOW: it diverges from its host main at
977
+ * `posFrac`, drops to its own `lane`, then runs to `endFrac` and ends at an
978
+ * endplate face.
979
+ *
980
+ * The run is the branch's OWN arc length laid along the strip, not its
981
+ * projection onto the module axis — a square 90° exit projects to zero, which
982
+ * is exactly the case that made branches invisible here. Straightening the
983
+ * route out at its true length is the same idiom this whole view applies to the
984
+ * main; `posFrac → endFrac` is therefore a real axis, and `lengthInches` maps
985
+ * onto it, so anything positioned along the branch has somewhere to draw.
986
+ */
956
987
  interface BranchConnector {
988
+ /** Endplate id — "C", "D", … */
957
989
  id: string;
990
+ /** The endplate's label. MR draws no text (the destination depends on what's
991
+ * physically attached, so FD derives the panel label at runtime) — this is
992
+ * the owner's local name, for tooltips and FD's fallback. */
958
993
  label: string;
994
+ /** The route's own name, from the track. */
995
+ name: string;
996
+ /** The branch track's id. */
997
+ trackId: string;
998
+ /** Whether the route IS a main (a diverging/split main) or a secondary
999
+ * branch line. Drives drawn weight, not geometry. */
1000
+ kind: "branch" | "main";
1001
+ /** Where it leaves its host main. */
959
1002
  posFrac: number;
1003
+ /** The host track's lane — a branch need not leave Main 1. Renderers must
1004
+ * start the diverge HERE, not at a hard-coded lane 0. */
1005
+ fromLane: number;
1006
+ /** Which side of the module it exits. */
960
1007
  side: "up" | "down";
1008
+ /** The branch's own lane, signed by `side` and placed clear of every other
1009
+ * lane (so it never collides) — already folded into laneMin/laneMax. */
1010
+ lane: number;
1011
+ /** Where the run ends, and the endplate face is drawn. */
1012
+ endFrac: number;
1013
+ /** The route's own arc length, inches. */
1014
+ lengthInches: number;
961
1015
  }
962
1016
  /** An industry — draw a car-spot span beside its track's lane, on `side`, with
963
1017
  * a name label + an optional car/length readout (#industries). */
@@ -1583,4 +1637,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1583
1637
  heading: number;
1584
1638
  }>;
1585
1639
 
1586
- 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 FrogCasting, 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, frogCasting, 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 };
1640
+ 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 FrogCasting, 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, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, pathLengthInches, 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
@@ -43,6 +43,19 @@ interface SchematicEndplate {
43
43
  y: number;
44
44
  heading: number;
45
45
  };
46
+ /** Whether that pose was AUTHORED — placed or typed by the owner — rather
47
+ * than a derived one that found its way into the doc (#182).
48
+ *
49
+ * Authorship used to be inferred from `pose` merely existing, so a derived
50
+ * pose written back silently PINNED the plate: it stopped following the
51
+ * module and went stale the moment the length changed (FMN-0068 ended up with
52
+ * endplate B at x=48 on a 47.9″ board). A pose is only an override when the
53
+ * owner meant it.
54
+ *
55
+ * Absent on A/B = derived residue, ignored. Absent on a placed branch
56
+ * endplate (one with `at`) = still authored — placing it IS the gesture, and
57
+ * docs predate this flag. */
58
+ poseAuthored?: boolean;
46
59
  /** Free-moN endplate FACE width across the track, inches — the physical size
47
60
  * of the standard interface at this end. Free-moN spec: 12″ minimum, 24″
48
61
  * recommended. Absent = the recommended default (modules may differ end to
@@ -394,6 +407,13 @@ declare function samplePath(pts: BenchworkPoint[], segsPerArc?: number): {
394
407
  x: number;
395
408
  y: number;
396
409
  }[];
410
+ /**
411
+ * Arc length of an authored path, inches — the path's OWN length, independent of
412
+ * where it sits or which way it runs. A branch route to a third endplate (#181)
413
+ * has no meaningful extent along the module axis (a square 90° exit projects to
414
+ * zero), so its own length is the only honest measure of it.
415
+ */
416
+ declare function pathLengthInches(path: BenchworkPoint[] | null | undefined): number;
397
417
  /** Normalise an authored track path from a doc, or null if it isn't a real path
398
418
  * (needs ≥ 2 valid points). Keeps per-vertex bulge. */
399
419
  declare function trackPath(path: BenchworkPoint[] | null | undefined): BenchworkPoint[] | null;
@@ -951,13 +971,47 @@ interface DrawCrossover {
951
971
  toPosFrac: number;
952
972
  toLane: number;
953
973
  }
954
- /** A branch endplate — draw a connector stub + arrow + label off the given
955
- * side (the CATS/US&S off-band idiom, #170). */
974
+ /**
975
+ * A route leaving the module at a third endplate (#170) — Main 3, whatever the
976
+ * owner labels it (#181). Drawn as an ELBOW: it diverges from its host main at
977
+ * `posFrac`, drops to its own `lane`, then runs to `endFrac` and ends at an
978
+ * endplate face.
979
+ *
980
+ * The run is the branch's OWN arc length laid along the strip, not its
981
+ * projection onto the module axis — a square 90° exit projects to zero, which
982
+ * is exactly the case that made branches invisible here. Straightening the
983
+ * route out at its true length is the same idiom this whole view applies to the
984
+ * main; `posFrac → endFrac` is therefore a real axis, and `lengthInches` maps
985
+ * onto it, so anything positioned along the branch has somewhere to draw.
986
+ */
956
987
  interface BranchConnector {
988
+ /** Endplate id — "C", "D", … */
957
989
  id: string;
990
+ /** The endplate's label. MR draws no text (the destination depends on what's
991
+ * physically attached, so FD derives the panel label at runtime) — this is
992
+ * the owner's local name, for tooltips and FD's fallback. */
958
993
  label: string;
994
+ /** The route's own name, from the track. */
995
+ name: string;
996
+ /** The branch track's id. */
997
+ trackId: string;
998
+ /** Whether the route IS a main (a diverging/split main) or a secondary
999
+ * branch line. Drives drawn weight, not geometry. */
1000
+ kind: "branch" | "main";
1001
+ /** Where it leaves its host main. */
959
1002
  posFrac: number;
1003
+ /** The host track's lane — a branch need not leave Main 1. Renderers must
1004
+ * start the diverge HERE, not at a hard-coded lane 0. */
1005
+ fromLane: number;
1006
+ /** Which side of the module it exits. */
960
1007
  side: "up" | "down";
1008
+ /** The branch's own lane, signed by `side` and placed clear of every other
1009
+ * lane (so it never collides) — already folded into laneMin/laneMax. */
1010
+ lane: number;
1011
+ /** Where the run ends, and the endplate face is drawn. */
1012
+ endFrac: number;
1013
+ /** The route's own arc length, inches. */
1014
+ lengthInches: number;
961
1015
  }
962
1016
  /** An industry — draw a car-spot span beside its track's lane, on `side`, with
963
1017
  * a name label + an optional car/length readout (#industries). */
@@ -1583,4 +1637,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
1583
1637
  heading: number;
1584
1638
  }>;
1585
1639
 
1586
- 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 FrogCasting, 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, frogCasting, 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 };
1640
+ 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 FrogCasting, 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, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, pathLengthInches, 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
@@ -115,6 +115,15 @@ function samplePath(pts, segsPerArc = 20) {
115
115
  out.push({ x: pts[n - 1].x, y: pts[n - 1].y });
116
116
  return out;
117
117
  }
118
+ function pathLengthInches(path) {
119
+ const pts = trackPath(path);
120
+ if (!pts) return 0;
121
+ const poly = samplePath(pts);
122
+ let total = 0;
123
+ for (let i = 1; i < poly.length; i++)
124
+ total += Math.hypot(poly[i].x - poly[i - 1].x, poly[i].y - poly[i - 1].y);
125
+ return total;
126
+ }
118
127
  function trackPath(path) {
119
128
  const pts = (path ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
120
129
  x: p.x,
@@ -658,7 +667,7 @@ function buildTransition(state) {
658
667
  }
659
668
  function withPoses(endplates, overrides) {
660
669
  return endplates.map(
661
- (e) => overrides[e.id] ? { ...e, pose: overrides[e.id] } : e
670
+ (e) => overrides[e.id] ? { ...e, pose: overrides[e.id], poseAuthored: true } : e
662
671
  );
663
672
  }
664
673
  function withWidths(endplates, widths, offsets = {}) {
@@ -1680,15 +1689,43 @@ function moduleFeatures(doc) {
1680
1689
  laneA: trackLane.get(x.tracks?.[0] ?? "") ?? 0,
1681
1690
  laneB: trackLane.get(x.tracks?.[1] ?? "") ?? 1
1682
1691
  }));
1692
+ const main2Lane = trackLane.has(MAIN2_TRACK_ID) ? trackLane.get(MAIN2_TRACK_ID) : null;
1693
+ const baseLanes = [
1694
+ 0,
1695
+ main2Lane ?? (doubleMain ? 1 : 0),
1696
+ ...extraTracks.map((t) => t.lane),
1697
+ ...signals.map((s) => s.lane),
1698
+ ...crossings.flatMap((x) => [x.laneA, x.laneB]),
1699
+ ...crossovers.flatMap((x) => [x.fromLane, x.toLane])
1700
+ ];
1701
+ let upLane = Math.max(...baseLanes, 0);
1702
+ let downLane = Math.min(...baseLanes, 0);
1683
1703
  const branchConnectors = doc.endplates.filter(
1684
1704
  (e) => e.id !== "A" && e.id !== "B" && e.at && !!e.trackId && (doc.tracks ?? []).some((t) => t.id === e.trackId)
1685
1705
  ).map((e) => {
1706
+ const trk = (doc.tracks ?? []).find((t) => t.id === e.trackId);
1686
1707
  const sw = (doc.turnouts ?? []).find((t) => t.divergeTrack === e.trackId);
1708
+ const startPos = sw ? sw.pos : e.at.pos;
1709
+ const side = e.at.side === "down" ? "down" : "up";
1710
+ const lane = side === "down" ? --downLane : ++upLane;
1711
+ const runInches = pathLengthInches(trk.path) || Math.abs(e.at.pos - startPos) || FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
1712
+ const toB = e.at.pos >= startPos;
1713
+ const fits = (p) => p >= 0 && p <= len;
1714
+ const ahead = startPos + runInches;
1715
+ const behind = startPos - runInches;
1716
+ const endPos = toB && fits(ahead) ? ahead : !toB && fits(behind) ? behind : fits(toB ? behind : ahead) ? toB ? behind : ahead : ahead;
1687
1717
  return {
1688
1718
  id: e.id,
1689
1719
  label: e.label ?? e.id,
1690
- posFrac: clampFrac(sw ? sw.pos : e.at.pos),
1691
- side: e.at.side === "down" ? "down" : "up"
1720
+ name: trk.trackName ?? "",
1721
+ trackId: trk.id,
1722
+ kind: e.kind === "main" ? "main" : "branch",
1723
+ posFrac: clampFrac(startPos),
1724
+ fromLane: sw ? trackLane.get(sw.onTrack) ?? 0 : 0,
1725
+ side,
1726
+ lane,
1727
+ endFrac: clampFrac(endPos),
1728
+ lengthInches: runInches
1692
1729
  };
1693
1730
  });
1694
1731
  const industries = (doc.industries ?? []).flatMap((ind) => {
@@ -1713,15 +1750,7 @@ function moduleFeatures(doc) {
1713
1750
  };
1714
1751
  });
1715
1752
  });
1716
- const main2Lane = trackLane.has(MAIN2_TRACK_ID) ? trackLane.get(MAIN2_TRACK_ID) : null;
1717
- const allLanes = [
1718
- 0,
1719
- main2Lane ?? (doubleMain ? 1 : 0),
1720
- ...extraTracks.map((t) => t.lane),
1721
- ...signals.map((s) => s.lane),
1722
- ...crossings.flatMap((x) => [x.laneA, x.laneB]),
1723
- ...crossovers.flatMap((x) => [x.fromLane, x.toLane])
1724
- ];
1753
+ const allLanes = [...baseLanes, ...branchConnectors.map((b) => b.lane)];
1725
1754
  const loop = isLoopDoc(doc);
1726
1755
  const main2 = doc.tracks.find((t) => t.id === MAIN2_TRACK_ID);
1727
1756
  const main2Positioned = !!main2 && (main2.fromPos != null || main2.toPos != null) && !loop;
@@ -2002,16 +2031,32 @@ function trackMeetsEndplateIssues(path, pose, opts) {
2002
2031
  function poseNeedsManual(geometryType) {
2003
2032
  return geometryType === "wye" || geometryType === "other";
2004
2033
  }
2034
+ function isAxialPose(p) {
2035
+ const h = norm360(p.heading);
2036
+ return Math.abs(p.y) < 1e-6 && (Math.abs(h) < 1e-6 || Math.abs(h - 180) < 1e-6);
2037
+ }
2005
2038
  function poseOverridesFromDoc(doc) {
2006
2039
  const out = {};
2007
2040
  for (const e of doc.endplates ?? []) {
2008
- if (e.id && e.pose && typeof e.pose.x === "number" && typeof e.pose.y === "number" && typeof e.pose.heading === "number") {
2041
+ if (e.id && e.pose && typeof e.pose.x === "number" && typeof e.pose.y === "number" && typeof e.pose.heading === "number" && // Only an AUTHORED pose overrides derivation (#182) — but docs written
2042
+ // before the flag existed have to keep working, so authorship is inferred
2043
+ // for the two cases that can only BE authored:
2044
+ // · a placed branch endplate (`at`) — dropping it on the board is the
2045
+ // gesture, and it has no derivable position at all;
2046
+ // · an OFF-AXIS pose on A/B — a hand-placed plate is off-axis by
2047
+ // definition (that's why it needed hand placing, e.g. a wye's B).
2048
+ // What's left is an axial A/B pose, which is exactly the shape plain
2049
+ // derivation produces — residue. Honouring it silently pins the plate so
2050
+ // it stops following the module and goes stale (FMN-0068's B sat at 48 on
2051
+ // a 47.9″ board). Anything saved from now on carries the flag, so this
2052
+ // guesswork only ever applies to old docs.
2053
+ (e.poseAuthored === true || !!e.at || !isAxialPose(e.pose))) {
2009
2054
  out[e.id] = { x: e.pose.x, y: e.pose.y, heading: e.pose.heading };
2010
2055
  }
2011
2056
  }
2012
2057
  return out;
2013
2058
  }
2014
2059
 
2015
- 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, frogCasting, 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 };
2060
+ 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, frogCasting, frogNumberFromName, fromSectionRelative, geometryTurnDegrees, importedPartToTrackPart, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, leadInchesForSize, mergeImportedParts, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, parseXtpLibrary, partOutlineAtFrog, pathLengthInches, 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 };
2016
2061
  //# sourceMappingURL=index.js.map
2017
2062
  //# sourceMappingURL=index.js.map