@willcgage/module-schematic 0.29.0 → 0.31.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
@@ -21,19 +21,40 @@ function moduleSections(doc) {
21
21
  return (doc?.sections ?? []).filter((sec) => sec && typeof sec.id === "string" && sec.id !== "").map((sec) => {
22
22
  const outline = benchworkOutline({ outline: sec.outline });
23
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;
24
27
  return {
25
28
  id: sec.id,
26
29
  ...name ? { name } : {},
27
- ...outline ? { outline } : {}
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 } : {}
28
35
  };
29
36
  });
30
37
  }
31
- function sectionFootprints(doc) {
32
- return moduleSections(doc).filter((sec) => sec.outline).map((sec) => ({
33
- id: sec.id,
34
- ...sec.name ? { name: sec.name } : {},
35
- outline: sampleBenchworkOutline(sec.outline)
36
- }));
38
+ function sectionFootprints(doc, derive) {
39
+ const spans = derive ? sectionSpans(doc) : [];
40
+ const spanOf = new Map(spans.map((sp) => [sp.id, sp]));
41
+ return moduleSections(doc).map((sec) => {
42
+ const name = sec.name ? { name: sec.name } : {};
43
+ if (sec.outline)
44
+ return { id: sec.id, ...name, outline: sampleBenchworkOutline(sec.outline), derived: false };
45
+ const sp = spanOf.get(sec.id);
46
+ if (!sp || !derive) return null;
47
+ const band = sectionBand(
48
+ derive.centerline,
49
+ sp.fromPos,
50
+ sp.toPos,
51
+ derive.widthA,
52
+ derive.widthB,
53
+ derive.offsetA,
54
+ derive.offsetB
55
+ );
56
+ return band.length >= 3 ? { id: sec.id, ...name, outline: band, derived: true } : null;
57
+ }).filter((x) => x !== null);
37
58
  }
38
59
  function sampleBenchworkOutline(pts, segsPerArc = 20) {
39
60
  const n = pts.length;
@@ -128,6 +149,8 @@ var DEG_FP = Math.PI / 180;
128
149
  function moduleCenterline(input) {
129
150
  const drawn = trackPath(input.mainPath);
130
151
  if (drawn) return samplePath(drawn);
152
+ const chained = sectionedCenterline(input);
153
+ if (chained.length >= 2) return chained;
131
154
  if (!input.geometryType) return [];
132
155
  const L = input.lengthInches > 0 ? input.lengthInches : 24;
133
156
  const gt = input.geometryType;
@@ -145,6 +168,73 @@ function moduleCenterline(input) {
145
168
  }
146
169
  return pts;
147
170
  }
171
+ function sectionCenterlineLocal(sec) {
172
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
173
+ const gt = sec.geometryType || "straight";
174
+ if (L <= 0) return { points: [{ x: 0, y: 0 }], endX: 0, endY: 0, endHeadingDeg: 0 };
175
+ if (gt === "offset") {
176
+ const dy = sec.geometryOffsetInches ?? 0;
177
+ return { points: [{ x: 0, y: 0 }, { x: L, y: dy }], endX: L, endY: dy, endHeadingDeg: 0 };
178
+ }
179
+ const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? sec.geometryDegrees ?? 0 : 0;
180
+ if (turn === 0) return { points: [{ x: 0, y: 0 }, { x: L, y: 0 }], endX: L, endY: 0, endHeadingDeg: 0 };
181
+ const t = turn * DEG_FP;
182
+ const r = L / t;
183
+ const steps = 12;
184
+ const points = [];
185
+ for (let i = 0; i <= steps; i++) {
186
+ const a = t * i / steps;
187
+ points.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
188
+ }
189
+ const last = points[points.length - 1];
190
+ return { points, endX: last.x, endY: last.y, endHeadingDeg: turn };
191
+ }
192
+ function moduleLengthFromSections(doc) {
193
+ const secs = moduleSections(doc).filter(
194
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
195
+ );
196
+ if (!secs.length) return null;
197
+ return secs.reduce((a, sec) => a + sec.lengthInches, 0);
198
+ }
199
+ function sectionSpans(doc) {
200
+ let acc = 0;
201
+ const out = [];
202
+ for (const sec of moduleSections(doc)) {
203
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
204
+ if (L <= 0) continue;
205
+ out.push({ id: sec.id, ...sec.name ? { name: sec.name } : {}, fromPos: acc, toPos: acc + L });
206
+ acc += L;
207
+ }
208
+ return out;
209
+ }
210
+ function sectionBreaksFromSections(doc) {
211
+ const spans = sectionSpans(doc);
212
+ return spans.slice(0, -1).map((sp) => sp.toPos);
213
+ }
214
+ function sectionedCenterline(doc) {
215
+ const secs = moduleSections(doc).filter(
216
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
217
+ );
218
+ if (!secs.length) return [];
219
+ const out = [];
220
+ let ox = 0;
221
+ let oy = 0;
222
+ let heading = 0;
223
+ for (const sec of secs) {
224
+ const local = sectionCenterlineLocal(sec);
225
+ const c = Math.cos(heading * DEG_FP);
226
+ const sn = Math.sin(heading * DEG_FP);
227
+ for (let i = 0; i < local.points.length; i++) {
228
+ if (i === 0 && out.length) continue;
229
+ const p = local.points[i];
230
+ out.push({ x: ox + p.x * c - p.y * sn, y: oy + p.x * sn + p.y * c });
231
+ }
232
+ ox += local.endX * c - local.endY * sn;
233
+ oy += local.endX * sn + local.endY * c;
234
+ heading += local.endHeadingDeg;
235
+ }
236
+ return out;
237
+ }
148
238
  function centerlineNormals(center) {
149
239
  return center.map((_, i) => {
150
240
  const a = center[Math.max(0, i - 1)];
@@ -180,6 +270,63 @@ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES
180
270
  }));
181
271
  return [...left, ...right.reverse()];
182
272
  }
273
+ function sliceCenterline(center, fromPos, toPos) {
274
+ if (center.length < 2) return [];
275
+ const cum = [0];
276
+ for (let i = 1; i < center.length; i++)
277
+ cum.push(cum[i - 1] + Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y));
278
+ const total = cum[cum.length - 1];
279
+ const a = Math.max(0, Math.min(total, Math.min(fromPos, toPos)));
280
+ const b = Math.max(0, Math.min(total, Math.max(fromPos, toPos)));
281
+ if (b - a <= 0) return [];
282
+ const at = (d) => {
283
+ for (let i = 1; i < center.length; i++) {
284
+ if (d <= cum[i] || i === center.length - 1) {
285
+ const seg = cum[i] - cum[i - 1] || 1;
286
+ const t = Math.max(0, Math.min(1, (d - cum[i - 1]) / seg));
287
+ return {
288
+ x: center[i - 1].x + (center[i].x - center[i - 1].x) * t,
289
+ y: center[i - 1].y + (center[i].y - center[i - 1].y) * t
290
+ };
291
+ }
292
+ }
293
+ return center[center.length - 1];
294
+ };
295
+ const out = [at(a)];
296
+ for (let i = 0; i < center.length; i++) {
297
+ if (cum[i] > a && cum[i] < b) out.push({ x: center[i].x, y: center[i].y });
298
+ }
299
+ out.push(at(b));
300
+ return out;
301
+ }
302
+ function sectionBand(center, fromPos, toPos, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
303
+ const slice = sliceCenterline(center, fromPos, toPos);
304
+ if (slice.length < 2) return [];
305
+ let total = 0;
306
+ for (let i = 1; i < center.length; i++)
307
+ total += Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y);
308
+ total = total || 1;
309
+ const lo = Math.min(fromPos, toPos);
310
+ let acc = 0;
311
+ const fr = [0];
312
+ for (let i = 1; i < slice.length; i++) {
313
+ acc += Math.hypot(slice[i].x - slice[i - 1].x, slice[i].y - slice[i - 1].y);
314
+ fr.push(acc);
315
+ }
316
+ const f = fr.map((d) => Math.max(0, Math.min(1, (lo + d) / total)));
317
+ const n = centerlineNormals(slice);
318
+ const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
319
+ const off = (i) => offsetA * (1 - f[i]) + offsetB * f[i];
320
+ const left = slice.map((p, i) => ({
321
+ x: p.x + n[i].x * (off(i) + half(i)),
322
+ y: p.y + n[i].y * (off(i) + half(i))
323
+ }));
324
+ const right = slice.map((p, i) => ({
325
+ x: p.x + n[i].x * (off(i) - half(i)),
326
+ y: p.y + n[i].y * (off(i) - half(i))
327
+ }));
328
+ return [...left, ...right.reverse()];
329
+ }
183
330
  function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
184
331
  if (center.length < 2) return [];
185
332
  const n = centerlineNormals(center);
@@ -197,7 +344,13 @@ function moduleFootprint(input) {
197
344
  const authored = benchworkOutline(input);
198
345
  const offA = input.endplateTrackOffsets?.["A"] ?? 0;
199
346
  const offB = input.endplateTrackOffsets?.["B"] ?? 0;
200
- const sectionOutlines = sectionFootprints(input);
347
+ const sectionOutlines = sectionFootprints(input, {
348
+ centerline,
349
+ widthA,
350
+ widthB,
351
+ offsetA: offA,
352
+ offsetB: offB
353
+ });
201
354
  return {
202
355
  centerline,
203
356
  band: benchworkBand(centerline, widthA, widthB, offA, offB),
@@ -1149,6 +1302,7 @@ exports.isTransitionTurnout = isTransitionTurnout;
1149
1302
  exports.moduleCenterline = moduleCenterline;
1150
1303
  exports.moduleFeatures = moduleFeatures;
1151
1304
  exports.moduleFootprint = moduleFootprint;
1305
+ exports.moduleLengthFromSections = moduleLengthFromSections;
1152
1306
  exports.moduleSections = moduleSections;
1153
1307
  exports.nextId = nextId;
1154
1308
  exports.poseNeedsManual = poseNeedsManual;
@@ -1156,7 +1310,12 @@ exports.poseOverridesFromDoc = poseOverridesFromDoc;
1156
1310
  exports.sampleBenchworkOutline = sampleBenchworkOutline;
1157
1311
  exports.samplePath = samplePath;
1158
1312
  exports.scaleFeetToInches = scaleFeetToInches;
1313
+ exports.sectionBand = sectionBand;
1314
+ exports.sectionBreaksFromSections = sectionBreaksFromSections;
1159
1315
  exports.sectionFootprints = sectionFootprints;
1316
+ exports.sectionSpans = sectionSpans;
1317
+ exports.sectionedCenterline = sectionedCenterline;
1318
+ exports.sliceCenterline = sliceCenterline;
1160
1319
  exports.stateToDoc = stateToDoc;
1161
1320
  exports.trackPath = trackPath;
1162
1321
  //# sourceMappingURL=index.cjs.map