@willcgage/module-schematic 0.15.0 → 0.17.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
@@ -8,11 +8,135 @@ function endplateWidthInches(ep) {
8
8
  return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
9
9
  }
10
10
  function benchworkOutline(doc) {
11
- const pts = (doc?.outline ?? []).filter(
12
- (p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)
13
- );
11
+ const pts = (doc?.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
12
+ x: p.x,
13
+ y: p.y,
14
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
15
+ }));
14
16
  return pts.length >= 3 ? pts : null;
15
17
  }
18
+ function sampleBenchworkOutline(pts, segsPerArc = 20) {
19
+ const n = pts.length;
20
+ if (n < 2) return pts.map((p) => ({ x: p.x, y: p.y }));
21
+ const out = [];
22
+ for (let i = 0; i < n; i++) {
23
+ const p0 = pts[i];
24
+ const p1 = pts[(i + 1) % n];
25
+ out.push({ x: p0.x, y: p0.y });
26
+ const bulge = p0.bulge ?? 0;
27
+ if (!bulge) continue;
28
+ const dx = p1.x - p0.x;
29
+ const dy = p1.y - p0.y;
30
+ const c = Math.hypot(dx, dy);
31
+ if (c < 1e-6) continue;
32
+ const nx = -dy / c;
33
+ const ny = dx / c;
34
+ const mid = { x: (p0.x + p1.x) / 2 + nx * bulge, y: (p0.y + p1.y) / 2 + ny * bulge };
35
+ const circ = circleThrough(p0, mid, p1);
36
+ if (!circ) continue;
37
+ const a0 = Math.atan2(p0.y - circ.cy, p0.x - circ.cx);
38
+ const am = Math.atan2(mid.y - circ.cy, mid.x - circ.cx);
39
+ const a1 = Math.atan2(p1.y - circ.cy, p1.x - circ.cx);
40
+ const sweep = arcSweep(a0, a1, am);
41
+ for (let s = 1; s < segsPerArc; s++) {
42
+ const a = a0 + sweep * s / segsPerArc;
43
+ out.push({ x: circ.cx + circ.r * Math.cos(a), y: circ.cy + circ.r * Math.sin(a) });
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ function circleThrough(a, b, c) {
49
+ const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
50
+ if (Math.abs(d) < 1e-9) return null;
51
+ const a2 = a.x * a.x + a.y * a.y;
52
+ const b2 = b.x * b.x + b.y * b.y;
53
+ const c2 = c.x * c.x + c.y * c.y;
54
+ const cx = (a2 * (b.y - c.y) + b2 * (c.y - a.y) + c2 * (a.y - b.y)) / d;
55
+ const cy = (a2 * (c.x - b.x) + b2 * (a.x - c.x) + c2 * (b.x - a.x)) / d;
56
+ return { cx, cy, r: Math.hypot(a.x - cx, a.y - cy) };
57
+ }
58
+ function arcSweep(a0, a1, am) {
59
+ const norm = (x) => {
60
+ let v = (x - a0) % (2 * Math.PI);
61
+ if (v < 0) v += 2 * Math.PI;
62
+ return v;
63
+ };
64
+ const m = norm(am);
65
+ const one = norm(a1);
66
+ return m <= one ? one : one - 2 * Math.PI;
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
+ }
16
140
  function isLoopDoc(doc) {
17
141
  return doc.loop === true || doc.endplates.length === 1;
18
142
  }
@@ -292,9 +416,11 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
292
416
  if (typeof e.widthInches === "number" && e.widthInches > 0)
293
417
  endplateWidths[e.id] = e.widthInches;
294
418
  }
295
- const outline = (d.outline ?? []).filter(
296
- (p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)
297
- );
419
+ const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
420
+ x: p.x,
421
+ y: p.y,
422
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
423
+ }));
298
424
  return {
299
425
  lengthInches: len,
300
426
  loop,
@@ -774,6 +900,7 @@ exports.MAIN2_TRACK_ID = MAIN2_TRACK_ID;
774
900
  exports.MAIN_TRACK_ID = MAIN_TRACK_ID;
775
901
  exports.N_SCALE_RATIO = N_SCALE_RATIO;
776
902
  exports.asModuleSchematic = asModuleSchematic;
903
+ exports.benchworkBand = benchworkBand;
777
904
  exports.benchworkOutline = benchworkOutline;
778
905
  exports.buildCrossover = buildCrossover;
779
906
  exports.buildPassingSiding = buildPassingSiding;
@@ -782,15 +909,19 @@ exports.deriveEndplatePoses = deriveEndplatePoses;
782
909
  exports.divergeSideForHand = divergeSideForHand;
783
910
  exports.docToState = docToState;
784
911
  exports.emptyEditorState = emptyEditorState;
912
+ exports.endplateFaceSegments = endplateFaceSegments;
785
913
  exports.endplateWidthInches = endplateWidthInches;
786
914
  exports.geometryTurnDegrees = geometryTurnDegrees;
787
915
  exports.inchesToScaleFeet = inchesToScaleFeet;
788
916
  exports.isLoopDoc = isLoopDoc;
789
917
  exports.isTransitionTurnout = isTransitionTurnout;
918
+ exports.moduleCenterline = moduleCenterline;
790
919
  exports.moduleFeatures = moduleFeatures;
920
+ exports.moduleFootprint = moduleFootprint;
791
921
  exports.nextId = nextId;
792
922
  exports.poseNeedsManual = poseNeedsManual;
793
923
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
924
+ exports.sampleBenchworkOutline = sampleBenchworkOutline;
794
925
  exports.scaleFeetToInches = scaleFeetToInches;
795
926
  exports.stateToDoc = stateToDoc;
796
927
  //# sourceMappingURL=index.cjs.map