@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 +137 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -3
- package/dist/index.d.ts +57 -3
- package/dist/index.js +133 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -165,17 +165,71 @@ interface ModuleSchematicDoc {
|
|
|
165
165
|
/** @deprecated pre-grouping flat signals; read for back-compat. */
|
|
166
166
|
signals?: SchematicSignal[];
|
|
167
167
|
}
|
|
168
|
-
/** A benchwork-outline vertex, module-local inches.
|
|
168
|
+
/** A benchwork-outline vertex, module-local inches. The edge from this vertex
|
|
169
|
+
* to the NEXT one is a straight line, unless `bulge` is set — then it's a
|
|
170
|
+
* circular arc whose midpoint is offset `bulge` inches (signed: + bows to the
|
|
171
|
+
* left of the P→next direction) perpendicular from the chord. */
|
|
169
172
|
interface BenchworkPoint {
|
|
170
173
|
x: number;
|
|
171
174
|
y: number;
|
|
175
|
+
bulge?: number;
|
|
172
176
|
}
|
|
173
177
|
/** The authored benchwork outline, or null when a module hasn't drawn one
|
|
174
178
|
* (renderers then fall back to a band derived from the endplate widths). A
|
|
175
|
-
* valid outline needs at least 3 points. */
|
|
179
|
+
* valid outline needs at least 3 points. Normalises each vertex to {x, y, bulge?}. */
|
|
176
180
|
declare function benchworkOutline(doc: {
|
|
177
181
|
outline?: BenchworkPoint[] | null;
|
|
178
182
|
} | null | undefined): BenchworkPoint[] | null;
|
|
183
|
+
/**
|
|
184
|
+
* Expand a benchwork outline (whose edges may be arcs) into a dense closed
|
|
185
|
+
* polyline for rendering — the SAME sampling both the Repository preview and
|
|
186
|
+
* Free-Dispatcher use, so a curve looks identical in both. Straight edges emit
|
|
187
|
+
* just their start vertex; a bulged edge emits `segsPerArc` points along the
|
|
188
|
+
* circular arc through the two endpoints and the bulged midpoint.
|
|
189
|
+
*/
|
|
190
|
+
declare function sampleBenchworkOutline(pts: BenchworkPoint[], segsPerArc?: number): {
|
|
191
|
+
x: number;
|
|
192
|
+
y: number;
|
|
193
|
+
}[];
|
|
194
|
+
interface ModuleFootprintInput {
|
|
195
|
+
/** Mainline length (falls back to footprint length), inches. */
|
|
196
|
+
lengthInches: number;
|
|
197
|
+
geometryType?: string | null;
|
|
198
|
+
geometryDegrees?: number | null;
|
|
199
|
+
geometryOffsetInches?: number | null;
|
|
200
|
+
/** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
|
|
201
|
+
endplateWidths?: Record<string, number>;
|
|
202
|
+
/** Authored benchwork outline (module-local inches), or absent for the band. */
|
|
203
|
+
outline?: BenchworkPoint[] | null;
|
|
204
|
+
}
|
|
205
|
+
interface OutlineFace {
|
|
206
|
+
/** The endplate face's two corners + midpoint (the track point). */
|
|
207
|
+
p1: BenchworkPoint;
|
|
208
|
+
p2: BenchworkPoint;
|
|
209
|
+
mid: BenchworkPoint;
|
|
210
|
+
}
|
|
211
|
+
interface ModuleFootprint {
|
|
212
|
+
/** Main track centre-line A→B (arcs sampled). */
|
|
213
|
+
centerline: BenchworkPoint[];
|
|
214
|
+
/** Derived benchwork band (endplate-width ribbon); the outline fallback. */
|
|
215
|
+
band: BenchworkPoint[];
|
|
216
|
+
/** Endplate faces: [A end, B end]. */
|
|
217
|
+
endplateFaces: OutlineFace[];
|
|
218
|
+
/** Authored outline (arc-sampled closed ring) or null → render the band. */
|
|
219
|
+
outline: BenchworkPoint[] | null;
|
|
220
|
+
}
|
|
221
|
+
/** Module-local main track centre-line (A→B), sampling arcs for curves/corners. */
|
|
222
|
+
declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
|
|
223
|
+
/** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
|
|
224
|
+
declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
|
|
225
|
+
/** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
|
|
226
|
+
declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
|
|
227
|
+
/**
|
|
228
|
+
* The full single-module physical footprint: centre-line + derived band +
|
|
229
|
+
* endplate faces + the authored outline (arc-sampled), all in module-local
|
|
230
|
+
* inches. Renderers draw `outline ?? band`.
|
|
231
|
+
*/
|
|
232
|
+
declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
|
|
179
233
|
/** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
|
|
180
234
|
declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
|
|
181
235
|
declare const MAIN_TRACK_ID = "main";
|
|
@@ -544,4 +598,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
|
|
|
544
598
|
heading: number;
|
|
545
599
|
}>;
|
|
546
600
|
|
|
547
|
-
export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleFeatures, nextId, poseNeedsManual, poseOverridesFromDoc, scaleFeetToInches, stateToDoc };
|
|
601
|
+
export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
|
package/dist/index.js
CHANGED
|
@@ -6,11 +6,135 @@ function endplateWidthInches(ep) {
|
|
|
6
6
|
return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
|
|
7
7
|
}
|
|
8
8
|
function benchworkOutline(doc) {
|
|
9
|
-
const pts = (doc?.outline ?? []).filter(
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const pts = (doc?.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
10
|
+
x: p.x,
|
|
11
|
+
y: p.y,
|
|
12
|
+
...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
|
|
13
|
+
}));
|
|
12
14
|
return pts.length >= 3 ? pts : null;
|
|
13
15
|
}
|
|
16
|
+
function sampleBenchworkOutline(pts, segsPerArc = 20) {
|
|
17
|
+
const n = pts.length;
|
|
18
|
+
if (n < 2) return pts.map((p) => ({ x: p.x, y: p.y }));
|
|
19
|
+
const out = [];
|
|
20
|
+
for (let i = 0; i < n; i++) {
|
|
21
|
+
const p0 = pts[i];
|
|
22
|
+
const p1 = pts[(i + 1) % n];
|
|
23
|
+
out.push({ x: p0.x, y: p0.y });
|
|
24
|
+
const bulge = p0.bulge ?? 0;
|
|
25
|
+
if (!bulge) continue;
|
|
26
|
+
const dx = p1.x - p0.x;
|
|
27
|
+
const dy = p1.y - p0.y;
|
|
28
|
+
const c = Math.hypot(dx, dy);
|
|
29
|
+
if (c < 1e-6) continue;
|
|
30
|
+
const nx = -dy / c;
|
|
31
|
+
const ny = dx / c;
|
|
32
|
+
const mid = { x: (p0.x + p1.x) / 2 + nx * bulge, y: (p0.y + p1.y) / 2 + ny * bulge };
|
|
33
|
+
const circ = circleThrough(p0, mid, p1);
|
|
34
|
+
if (!circ) continue;
|
|
35
|
+
const a0 = Math.atan2(p0.y - circ.cy, p0.x - circ.cx);
|
|
36
|
+
const am = Math.atan2(mid.y - circ.cy, mid.x - circ.cx);
|
|
37
|
+
const a1 = Math.atan2(p1.y - circ.cy, p1.x - circ.cx);
|
|
38
|
+
const sweep = arcSweep(a0, a1, am);
|
|
39
|
+
for (let s = 1; s < segsPerArc; s++) {
|
|
40
|
+
const a = a0 + sweep * s / segsPerArc;
|
|
41
|
+
out.push({ x: circ.cx + circ.r * Math.cos(a), y: circ.cy + circ.r * Math.sin(a) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function circleThrough(a, b, c) {
|
|
47
|
+
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
|
48
|
+
if (Math.abs(d) < 1e-9) return null;
|
|
49
|
+
const a2 = a.x * a.x + a.y * a.y;
|
|
50
|
+
const b2 = b.x * b.x + b.y * b.y;
|
|
51
|
+
const c2 = c.x * c.x + c.y * c.y;
|
|
52
|
+
const cx = (a2 * (b.y - c.y) + b2 * (c.y - a.y) + c2 * (a.y - b.y)) / d;
|
|
53
|
+
const cy = (a2 * (c.x - b.x) + b2 * (a.x - c.x) + c2 * (b.x - a.x)) / d;
|
|
54
|
+
return { cx, cy, r: Math.hypot(a.x - cx, a.y - cy) };
|
|
55
|
+
}
|
|
56
|
+
function arcSweep(a0, a1, am) {
|
|
57
|
+
const norm = (x) => {
|
|
58
|
+
let v = (x - a0) % (2 * Math.PI);
|
|
59
|
+
if (v < 0) v += 2 * Math.PI;
|
|
60
|
+
return v;
|
|
61
|
+
};
|
|
62
|
+
const m = norm(am);
|
|
63
|
+
const one = norm(a1);
|
|
64
|
+
return m <= one ? one : one - 2 * Math.PI;
|
|
65
|
+
}
|
|
66
|
+
var DEG_FP = Math.PI / 180;
|
|
67
|
+
function moduleCenterline(input) {
|
|
68
|
+
const L = input.lengthInches > 0 ? input.lengthInches : 24;
|
|
69
|
+
const gt = input.geometryType;
|
|
70
|
+
if (gt === "dead_end") return [{ x: 0, y: 0 }];
|
|
71
|
+
if (gt === "offset") return [{ x: 0, y: 0 }, { x: L, y: input.geometryOffsetInches ?? 0 }];
|
|
72
|
+
const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? input.geometryDegrees ?? 0 : 0;
|
|
73
|
+
if (turn === 0) return [{ x: 0, y: 0 }, { x: L, y: 0 }];
|
|
74
|
+
const t = turn * DEG_FP;
|
|
75
|
+
const r = L / t;
|
|
76
|
+
const steps = 12;
|
|
77
|
+
const pts = [];
|
|
78
|
+
for (let i = 0; i <= steps; i++) {
|
|
79
|
+
const a = t * i / steps;
|
|
80
|
+
pts.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
|
|
81
|
+
}
|
|
82
|
+
return pts;
|
|
83
|
+
}
|
|
84
|
+
function centerlineNormals(center) {
|
|
85
|
+
return center.map((_, i) => {
|
|
86
|
+
const a = center[Math.max(0, i - 1)];
|
|
87
|
+
const b = center[Math.min(center.length - 1, i + 1)];
|
|
88
|
+
let dx = b.x - a.x;
|
|
89
|
+
let dy = b.y - a.y;
|
|
90
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
91
|
+
dx /= len;
|
|
92
|
+
dy /= len;
|
|
93
|
+
return { x: -dy, y: dx };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function centerlineFractions(center) {
|
|
97
|
+
const cum = [0];
|
|
98
|
+
for (let i = 1; i < center.length; i++)
|
|
99
|
+
cum.push(cum[i - 1] + Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y));
|
|
100
|
+
const total = cum[cum.length - 1] || 1;
|
|
101
|
+
return cum.map((d) => d / total);
|
|
102
|
+
}
|
|
103
|
+
function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
|
|
104
|
+
if (center.length < 2) return [];
|
|
105
|
+
const n = centerlineNormals(center);
|
|
106
|
+
const f = centerlineFractions(center);
|
|
107
|
+
const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
|
|
108
|
+
const left = center.map((p, i) => ({ x: p.x + n[i].x * half(i), y: p.y + n[i].y * half(i) }));
|
|
109
|
+
const right = center.map((p, i) => ({ x: p.x - n[i].x * half(i), y: p.y - n[i].y * half(i) }));
|
|
110
|
+
return [...left, ...right.reverse()];
|
|
111
|
+
}
|
|
112
|
+
function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
|
|
113
|
+
if (center.length < 2) return [];
|
|
114
|
+
const n = centerlineNormals(center);
|
|
115
|
+
const face = (i, w) => ({
|
|
116
|
+
p1: { x: center[i].x + n[i].x * (w / 2), y: center[i].y + n[i].y * (w / 2) },
|
|
117
|
+
p2: { x: center[i].x - n[i].x * (w / 2), y: center[i].y - n[i].y * (w / 2) },
|
|
118
|
+
mid: { x: center[i].x, y: center[i].y }
|
|
119
|
+
});
|
|
120
|
+
return [face(0, widthA), face(center.length - 1, widthB)];
|
|
121
|
+
}
|
|
122
|
+
function moduleFootprint(input) {
|
|
123
|
+
const centerline = moduleCenterline(input);
|
|
124
|
+
const widthA = endplateWidthFor(input.endplateWidths, "A");
|
|
125
|
+
const widthB = endplateWidthFor(input.endplateWidths, "B");
|
|
126
|
+
const authored = benchworkOutline(input);
|
|
127
|
+
return {
|
|
128
|
+
centerline,
|
|
129
|
+
band: benchworkBand(centerline, widthA, widthB),
|
|
130
|
+
endplateFaces: endplateFaceSegments(centerline, widthA, widthB),
|
|
131
|
+
outline: authored ? sampleBenchworkOutline(authored) : null
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function endplateWidthFor(widths, id) {
|
|
135
|
+
const w = widths?.[id];
|
|
136
|
+
return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
|
|
137
|
+
}
|
|
14
138
|
function isLoopDoc(doc) {
|
|
15
139
|
return doc.loop === true || doc.endplates.length === 1;
|
|
16
140
|
}
|
|
@@ -290,9 +414,11 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
290
414
|
if (typeof e.widthInches === "number" && e.widthInches > 0)
|
|
291
415
|
endplateWidths[e.id] = e.widthInches;
|
|
292
416
|
}
|
|
293
|
-
const outline = (d.outline ?? []).filter(
|
|
294
|
-
|
|
295
|
-
|
|
417
|
+
const outline = (d.outline ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
|
|
418
|
+
x: p.x,
|
|
419
|
+
y: p.y,
|
|
420
|
+
...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
|
|
421
|
+
}));
|
|
296
422
|
return {
|
|
297
423
|
lengthInches: len,
|
|
298
424
|
loop,
|
|
@@ -766,6 +892,6 @@ function poseOverridesFromDoc(doc) {
|
|
|
766
892
|
return out;
|
|
767
893
|
}
|
|
768
894
|
|
|
769
|
-
export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_SCALE_RATIO, asModuleSchematic, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleFeatures, nextId, poseNeedsManual, poseOverridesFromDoc, scaleFeetToInches, stateToDoc };
|
|
895
|
+
export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
|
|
770
896
|
//# sourceMappingURL=index.js.map
|
|
771
897
|
//# sourceMappingURL=index.js.map
|