@willcgage/module-schematic 0.16.0 → 0.18.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.cjs CHANGED
@@ -65,6 +65,78 @@ function arcSweep(a0, a1, am) {
65
65
  const one = norm(a1);
66
66
  return m <= one ? one : one - 2 * Math.PI;
67
67
  }
68
+ var DEG_FP = Math.PI / 180;
69
+ function moduleCenterline(input) {
70
+ const L = input.lengthInches > 0 ? input.lengthInches : 24;
71
+ const gt = input.geometryType;
72
+ if (gt === "dead_end") return [{ x: 0, y: 0 }];
73
+ if (gt === "offset") return [{ x: 0, y: 0 }, { x: L, y: input.geometryOffsetInches ?? 0 }];
74
+ const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? input.geometryDegrees ?? 0 : 0;
75
+ if (turn === 0) return [{ x: 0, y: 0 }, { x: L, y: 0 }];
76
+ const t = turn * DEG_FP;
77
+ const r = L / t;
78
+ const steps = 12;
79
+ const pts = [];
80
+ for (let i = 0; i <= steps; i++) {
81
+ const a = t * i / steps;
82
+ pts.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
83
+ }
84
+ return pts;
85
+ }
86
+ function centerlineNormals(center) {
87
+ return center.map((_, i) => {
88
+ const a = center[Math.max(0, i - 1)];
89
+ const b = center[Math.min(center.length - 1, i + 1)];
90
+ let dx = b.x - a.x;
91
+ let dy = b.y - a.y;
92
+ const len = Math.hypot(dx, dy) || 1;
93
+ dx /= len;
94
+ dy /= len;
95
+ return { x: -dy, y: dx };
96
+ });
97
+ }
98
+ function centerlineFractions(center) {
99
+ const cum = [0];
100
+ for (let i = 1; i < center.length; i++)
101
+ cum.push(cum[i - 1] + Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y));
102
+ const total = cum[cum.length - 1] || 1;
103
+ return cum.map((d) => d / total);
104
+ }
105
+ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
106
+ if (center.length < 2) return [];
107
+ const n = centerlineNormals(center);
108
+ const f = centerlineFractions(center);
109
+ const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
110
+ const left = center.map((p, i) => ({ x: p.x + n[i].x * half(i), y: p.y + n[i].y * half(i) }));
111
+ const right = center.map((p, i) => ({ x: p.x - n[i].x * half(i), y: p.y - n[i].y * half(i) }));
112
+ return [...left, ...right.reverse()];
113
+ }
114
+ function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
115
+ if (center.length < 2) return [];
116
+ const n = centerlineNormals(center);
117
+ const face = (i, w) => ({
118
+ p1: { x: center[i].x + n[i].x * (w / 2), y: center[i].y + n[i].y * (w / 2) },
119
+ p2: { x: center[i].x - n[i].x * (w / 2), y: center[i].y - n[i].y * (w / 2) },
120
+ mid: { x: center[i].x, y: center[i].y }
121
+ });
122
+ return [face(0, widthA), face(center.length - 1, widthB)];
123
+ }
124
+ function moduleFootprint(input) {
125
+ const centerline = moduleCenterline(input);
126
+ const widthA = endplateWidthFor(input.endplateWidths, "A");
127
+ const widthB = endplateWidthFor(input.endplateWidths, "B");
128
+ const authored = benchworkOutline(input);
129
+ return {
130
+ centerline,
131
+ band: benchworkBand(centerline, widthA, widthB),
132
+ endplateFaces: endplateFaceSegments(centerline, widthA, widthB),
133
+ outline: authored ? sampleBenchworkOutline(authored) : null
134
+ };
135
+ }
136
+ function endplateWidthFor(widths, id) {
137
+ const w = widths?.[id];
138
+ return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
139
+ }
68
140
  function isLoopDoc(doc) {
69
141
  return doc.loop === true || doc.endplates.length === 1;
70
142
  }
@@ -77,6 +149,11 @@ function inchesToScaleFeet(inches, ratio = N_SCALE_RATIO) {
77
149
  function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
78
150
  return feet * 12 / ratio;
79
151
  }
152
+ var N_CAR_LENGTH_INCHES = 3.3;
153
+ function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
154
+ if (!(carLengthInches > 0)) return 0;
155
+ return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
156
+ }
80
157
  function asModuleSchematic(x) {
81
158
  if (!x || typeof x !== "object") return null;
82
159
  const d = x;
@@ -98,7 +175,8 @@ function emptyEditorState(lengthInches) {
98
175
  poseOverrides: {},
99
176
  endplateWidths: {},
100
177
  outline: [],
101
- controlPoints: []
178
+ controlPoints: [],
179
+ industries: []
102
180
  };
103
181
  }
104
182
  function isTransitionTurnout(t) {
@@ -268,6 +346,21 @@ function stateToDoc(state, recordNumber) {
268
346
  side: s.side
269
347
  }))
270
348
  })),
349
+ // Industries — car-spot spans on a track; only when any are authored.
350
+ ...state.industries.length > 0 ? {
351
+ industries: state.industries.map((ind) => ({
352
+ id: ind.id,
353
+ name: ind.name,
354
+ ...ind.type ? { type: ind.type } : {},
355
+ track: ind.track,
356
+ fromPos: ind.fromPos,
357
+ toPos: ind.toPos,
358
+ side: ind.side,
359
+ ...ind.labelMode && ind.labelMode !== "none" ? { labelMode: ind.labelMode } : {},
360
+ ...ind.carTypes.length ? { carTypes: ind.carTypes } : {},
361
+ moduleIndustryId: ind.moduleIndustryId
362
+ }))
363
+ } : {},
271
364
  // Benchwork footprint outline (module-local inches); only when it's a real
272
365
  // ring (≥ 3 vertices).
273
366
  ...state.outline.length >= 3 ? { outline: state.outline } : {}
@@ -381,7 +474,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
381
474
  divergeTrack: t.divergeTrack,
382
475
  kind: t.kind ?? "right"
383
476
  })),
384
- controlPoints: readControlPoints(d, sc)
477
+ controlPoints: readControlPoints(d, sc),
478
+ industries: (d.industries ?? []).map((ind) => ({
479
+ id: ind.id,
480
+ name: ind.name ?? "",
481
+ type: ind.type ?? "",
482
+ track: ind.track,
483
+ fromPos: sc(ind.fromPos ?? 0),
484
+ toPos: ind.toPos != null ? sc(ind.toPos) : len,
485
+ side: ind.side ?? "above",
486
+ labelMode: ind.labelMode ?? "none",
487
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : [],
488
+ moduleIndustryId: ind.moduleIndustryId ?? null
489
+ }))
385
490
  };
386
491
  }
387
492
  function readControlPoints(d, sc = (p) => p) {
@@ -680,6 +785,22 @@ function moduleFeatures(doc) {
680
785
  posFrac: clampFrac(e.at.pos),
681
786
  side: e.at.side === "down" ? "down" : "up"
682
787
  }));
788
+ const industries = (doc.industries ?? []).map((ind) => {
789
+ const from = ind.fromPos ?? 0;
790
+ const to = ind.toPos ?? len;
791
+ return {
792
+ id: ind.id,
793
+ name: ind.name ?? "",
794
+ type: ind.type ?? null,
795
+ fromFrac: clampFrac(Math.min(from, to)),
796
+ toFrac: clampFrac(Math.max(from, to)),
797
+ lane: trackLane.get(ind.track) ?? 0,
798
+ side: ind.side ?? "above",
799
+ labelMode: ind.labelMode ?? "none",
800
+ cars: carCapacity(from, to),
801
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : []
802
+ };
803
+ });
683
804
  const allLanes = [
684
805
  0,
685
806
  doubleMain ? 1 : 0,
@@ -720,6 +841,7 @@ function moduleFeatures(doc) {
720
841
  crossings,
721
842
  crossovers,
722
843
  branchConnectors,
844
+ industries,
723
845
  laneMin: Math.min(...allLanes),
724
846
  laneMax: Math.max(...allLanes)
725
847
  };
@@ -826,22 +948,28 @@ exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
826
948
  exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
827
949
  exports.MAIN2_TRACK_ID = MAIN2_TRACK_ID;
828
950
  exports.MAIN_TRACK_ID = MAIN_TRACK_ID;
951
+ exports.N_CAR_LENGTH_INCHES = N_CAR_LENGTH_INCHES;
829
952
  exports.N_SCALE_RATIO = N_SCALE_RATIO;
830
953
  exports.asModuleSchematic = asModuleSchematic;
954
+ exports.benchworkBand = benchworkBand;
831
955
  exports.benchworkOutline = benchworkOutline;
832
956
  exports.buildCrossover = buildCrossover;
833
957
  exports.buildPassingSiding = buildPassingSiding;
834
958
  exports.buildTransition = buildTransition;
959
+ exports.carCapacity = carCapacity;
835
960
  exports.deriveEndplatePoses = deriveEndplatePoses;
836
961
  exports.divergeSideForHand = divergeSideForHand;
837
962
  exports.docToState = docToState;
838
963
  exports.emptyEditorState = emptyEditorState;
964
+ exports.endplateFaceSegments = endplateFaceSegments;
839
965
  exports.endplateWidthInches = endplateWidthInches;
840
966
  exports.geometryTurnDegrees = geometryTurnDegrees;
841
967
  exports.inchesToScaleFeet = inchesToScaleFeet;
842
968
  exports.isLoopDoc = isLoopDoc;
843
969
  exports.isTransitionTurnout = isTransitionTurnout;
970
+ exports.moduleCenterline = moduleCenterline;
844
971
  exports.moduleFeatures = moduleFeatures;
972
+ exports.moduleFootprint = moduleFootprint;
845
973
  exports.nextId = nextId;
846
974
  exports.poseNeedsManual = poseNeedsManual;
847
975
  exports.poseOverridesFromDoc = poseOverridesFromDoc;