@willcgage/module-schematic 0.28.1 → 0.30.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
@@ -17,6 +17,31 @@ function benchworkOutline(doc) {
17
17
  }));
18
18
  return pts.length >= 3 ? pts : null;
19
19
  }
20
+ function moduleSections(doc) {
21
+ return (doc?.sections ?? []).filter((sec) => sec && typeof sec.id === "string" && sec.id !== "").map((sec) => {
22
+ const outline = benchworkOutline({ outline: sec.outline });
23
+ const name = typeof sec.name === "string" ? sec.name.trim() : "";
24
+ const len = sec.lengthInches;
25
+ const deg = sec.geometryDegrees;
26
+ const off = sec.geometryOffsetInches;
27
+ return {
28
+ id: sec.id,
29
+ ...name ? { name } : {},
30
+ ...outline ? { outline } : {},
31
+ ...typeof len === "number" && Number.isFinite(len) && len > 0 ? { lengthInches: len } : {},
32
+ ...sec.geometryType ? { geometryType: sec.geometryType } : {},
33
+ ...typeof deg === "number" && Number.isFinite(deg) ? { geometryDegrees: deg } : {},
34
+ ...typeof off === "number" && Number.isFinite(off) ? { geometryOffsetInches: off } : {}
35
+ };
36
+ });
37
+ }
38
+ function sectionFootprints(doc) {
39
+ return moduleSections(doc).filter((sec) => sec.outline).map((sec) => ({
40
+ id: sec.id,
41
+ ...sec.name ? { name: sec.name } : {},
42
+ outline: sampleBenchworkOutline(sec.outline)
43
+ }));
44
+ }
20
45
  function sampleBenchworkOutline(pts, segsPerArc = 20) {
21
46
  const n = pts.length;
22
47
  if (n < 2) return pts.map((p) => ({ x: p.x, y: p.y }));
@@ -110,6 +135,8 @@ var DEG_FP = Math.PI / 180;
110
135
  function moduleCenterline(input) {
111
136
  const drawn = trackPath(input.mainPath);
112
137
  if (drawn) return samplePath(drawn);
138
+ const chained = sectionedCenterline(input);
139
+ if (chained.length >= 2) return chained;
113
140
  if (!input.geometryType) return [];
114
141
  const L = input.lengthInches > 0 ? input.lengthInches : 24;
115
142
  const gt = input.geometryType;
@@ -127,6 +154,73 @@ function moduleCenterline(input) {
127
154
  }
128
155
  return pts;
129
156
  }
157
+ function sectionCenterlineLocal(sec) {
158
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
159
+ const gt = sec.geometryType || "straight";
160
+ if (L <= 0) return { points: [{ x: 0, y: 0 }], endX: 0, endY: 0, endHeadingDeg: 0 };
161
+ if (gt === "offset") {
162
+ const dy = sec.geometryOffsetInches ?? 0;
163
+ return { points: [{ x: 0, y: 0 }, { x: L, y: dy }], endX: L, endY: dy, endHeadingDeg: 0 };
164
+ }
165
+ const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? sec.geometryDegrees ?? 0 : 0;
166
+ if (turn === 0) return { points: [{ x: 0, y: 0 }, { x: L, y: 0 }], endX: L, endY: 0, endHeadingDeg: 0 };
167
+ const t = turn * DEG_FP;
168
+ const r = L / t;
169
+ const steps = 12;
170
+ const points = [];
171
+ for (let i = 0; i <= steps; i++) {
172
+ const a = t * i / steps;
173
+ points.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
174
+ }
175
+ const last = points[points.length - 1];
176
+ return { points, endX: last.x, endY: last.y, endHeadingDeg: turn };
177
+ }
178
+ function moduleLengthFromSections(doc) {
179
+ const secs = moduleSections(doc).filter(
180
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
181
+ );
182
+ if (!secs.length) return null;
183
+ return secs.reduce((a, sec) => a + sec.lengthInches, 0);
184
+ }
185
+ function sectionSpans(doc) {
186
+ let acc = 0;
187
+ const out = [];
188
+ for (const sec of moduleSections(doc)) {
189
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
190
+ if (L <= 0) continue;
191
+ out.push({ id: sec.id, ...sec.name ? { name: sec.name } : {}, fromPos: acc, toPos: acc + L });
192
+ acc += L;
193
+ }
194
+ return out;
195
+ }
196
+ function sectionBreaksFromSections(doc) {
197
+ const spans = sectionSpans(doc);
198
+ return spans.slice(0, -1).map((sp) => sp.toPos);
199
+ }
200
+ function sectionedCenterline(doc) {
201
+ const secs = moduleSections(doc).filter(
202
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
203
+ );
204
+ if (!secs.length) return [];
205
+ const out = [];
206
+ let ox = 0;
207
+ let oy = 0;
208
+ let heading = 0;
209
+ for (const sec of secs) {
210
+ const local = sectionCenterlineLocal(sec);
211
+ const c = Math.cos(heading * DEG_FP);
212
+ const sn = Math.sin(heading * DEG_FP);
213
+ for (let i = 0; i < local.points.length; i++) {
214
+ if (i === 0 && out.length) continue;
215
+ const p = local.points[i];
216
+ out.push({ x: ox + p.x * c - p.y * sn, y: oy + p.x * sn + p.y * c });
217
+ }
218
+ ox += local.endX * c - local.endY * sn;
219
+ oy += local.endX * sn + local.endY * c;
220
+ heading += local.endHeadingDeg;
221
+ }
222
+ return out;
223
+ }
130
224
  function centerlineNormals(center) {
131
225
  return center.map((_, i) => {
132
226
  const a = center[Math.max(0, i - 1)];
@@ -179,11 +273,13 @@ function moduleFootprint(input) {
179
273
  const authored = benchworkOutline(input);
180
274
  const offA = input.endplateTrackOffsets?.["A"] ?? 0;
181
275
  const offB = input.endplateTrackOffsets?.["B"] ?? 0;
276
+ const sectionOutlines = sectionFootprints(input);
182
277
  return {
183
278
  centerline,
184
279
  band: benchworkBand(centerline, widthA, widthB, offA, offB),
185
280
  endplateFaces: endplateFaceSegments(centerline, widthA, widthB, offA, offB),
186
- outline: authored ? sampleBenchworkOutline(authored) : null
281
+ outline: sectionOutlines.length || !authored ? null : sampleBenchworkOutline(authored),
282
+ sectionOutlines
187
283
  };
188
284
  }
189
285
  function endplateTrackOffsetFor(config, authoredTrackOffset) {
@@ -264,6 +360,7 @@ function emptyEditorState(lengthInches) {
264
360
  endplateTrackOffsets: {},
265
361
  outline: [],
266
362
  sectionBreaks: [],
363
+ sections: [],
267
364
  controlPoints: [],
268
365
  industries: [],
269
366
  mainPath: []
@@ -487,6 +584,9 @@ function stateToDoc(state, recordNumber) {
487
584
  ...state.outline.length >= 3 ? { outline: state.outline } : {},
488
585
  // Internal section joints (inches from A), when the module has more than one.
489
586
  ...state.sectionBreaks.length ? { sectionBreaks: state.sectionBreaks } : {},
587
+ // Sections as objects — emitted only once the owner has some, so a module
588
+ // that never used them keeps exactly the doc it had before (#96 phase 2).
589
+ ...state.sections.length ? { sections: moduleSections({ sections: state.sections }) } : {},
490
590
  // Authored mainline path (module-local inches); only when it's a real path.
491
591
  ...state.mainPath.length >= 2 ? { mainPath: state.mainPath } : {}
492
592
  };
@@ -592,6 +692,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
592
692
  endplateTrackOffsets,
593
693
  outline,
594
694
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
695
+ sections: moduleSections(d),
595
696
  mainPath,
596
697
  crossings: (d.crossings ?? []).map((x) => ({
597
698
  id: x.id,
@@ -1124,12 +1225,18 @@ exports.isTransitionTurnout = isTransitionTurnout;
1124
1225
  exports.moduleCenterline = moduleCenterline;
1125
1226
  exports.moduleFeatures = moduleFeatures;
1126
1227
  exports.moduleFootprint = moduleFootprint;
1228
+ exports.moduleLengthFromSections = moduleLengthFromSections;
1229
+ exports.moduleSections = moduleSections;
1127
1230
  exports.nextId = nextId;
1128
1231
  exports.poseNeedsManual = poseNeedsManual;
1129
1232
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
1130
1233
  exports.sampleBenchworkOutline = sampleBenchworkOutline;
1131
1234
  exports.samplePath = samplePath;
1132
1235
  exports.scaleFeetToInches = scaleFeetToInches;
1236
+ exports.sectionBreaksFromSections = sectionBreaksFromSections;
1237
+ exports.sectionFootprints = sectionFootprints;
1238
+ exports.sectionSpans = sectionSpans;
1239
+ exports.sectionedCenterline = sectionedCenterline;
1133
1240
  exports.stateToDoc = stateToDoc;
1134
1241
  exports.trackPath = trackPath;
1135
1242
  //# sourceMappingURL=index.cjs.map