@willcgage/module-schematic 0.84.0 → 0.85.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 +110 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +110 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -871,6 +871,10 @@ function withPoses(endplates, overrides) {
|
|
|
871
871
|
(e) => overrides[e.id] ? { ...e, pose: overrides[e.id], poseAuthored: true } : e
|
|
872
872
|
);
|
|
873
873
|
}
|
|
874
|
+
function withEdges(endplates, edges) {
|
|
875
|
+
if (!edges) return endplates;
|
|
876
|
+
return endplates.map((e) => edges[e.id] ? { ...e, edge: edges[e.id] } : e);
|
|
877
|
+
}
|
|
874
878
|
function withWidths(endplates, widths, offsets = {}) {
|
|
875
879
|
return endplates.map((e) => {
|
|
876
880
|
const w = widths[e.id];
|
|
@@ -902,46 +906,49 @@ function stateToDoc(state, recordNumber) {
|
|
|
902
906
|
...state.loop && state.loopReturn === "main2" ? { loopReturn: "main2" } : {},
|
|
903
907
|
...state.mainsSwapped ? { mainsSwapped: true } : {},
|
|
904
908
|
endplates: withWidths(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
909
|
+
withEdges(
|
|
910
|
+
withPoses(
|
|
911
|
+
[
|
|
912
|
+
...state.loop ? (
|
|
913
|
+
// Balloon loop: A is the entry. A standard endplate B on the balloon
|
|
914
|
+
// makes it an INTERCHANGE (second route connects at the loop, e.g.
|
|
915
|
+
// Seaford); configB "none" makes it a pure turnback.
|
|
916
|
+
[
|
|
917
|
+
{ id: "A", label: "Entry", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
918
|
+
...state.configB !== "none" ? [{ id: "B", label: "Interchange", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }] }] : []
|
|
919
|
+
]
|
|
920
|
+
) : [
|
|
921
|
+
{ id: "A", label: "West", tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configA }] },
|
|
922
|
+
// ⚠️ `configB: "none"` means the module HAS NO FAR ENDPLATE, and
|
|
923
|
+
// that is not loop-only (#184). An *end of the line* or a *pocket*
|
|
924
|
+
// presents one conforming face and the track simply stops. The
|
|
925
|
+
// standard governs the ends a module offers for joining; it never
|
|
926
|
+
// required a module to offer two. This used to coerce "none" to
|
|
927
|
+
// "single" and emit a B regardless, so a single-ended module was
|
|
928
|
+
// impossible to author.
|
|
929
|
+
...state.configB !== "none" ? [
|
|
930
|
+
{
|
|
931
|
+
id: "B",
|
|
932
|
+
label: "East",
|
|
933
|
+
tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: state.configB }]
|
|
934
|
+
}
|
|
935
|
+
] : []
|
|
936
|
+
],
|
|
937
|
+
// Branch endplates C, D, … — junction connections at pos, off one side
|
|
938
|
+
// (#170). A set can carry several (e.g. a second railroad through).
|
|
939
|
+
...state.branches.map((b, i) => ({
|
|
940
|
+
id: String.fromCharCode(67 + i),
|
|
941
|
+
// C, D, E…
|
|
942
|
+
label: b.label || `Branch ${i + 1}`,
|
|
943
|
+
tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: b.config }],
|
|
944
|
+
at: { pos: b.pos, side: b.side },
|
|
945
|
+
kind: b.kind ?? "branch",
|
|
946
|
+
...b.trackId ? { trackId: b.trackId } : {}
|
|
947
|
+
}))
|
|
931
948
|
],
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
id: String.fromCharCode(67 + i),
|
|
936
|
-
// C, D, E…
|
|
937
|
-
label: b.label || `Branch ${i + 1}`,
|
|
938
|
-
tracks: [{ trackId: MAIN_TRACK_ID, lane: 0, config: b.config }],
|
|
939
|
-
at: { pos: b.pos, side: b.side },
|
|
940
|
-
kind: b.kind ?? "branch",
|
|
941
|
-
...b.trackId ? { trackId: b.trackId } : {}
|
|
942
|
-
}))
|
|
943
|
-
],
|
|
944
|
-
state.poseOverrides
|
|
949
|
+
state.poseOverrides
|
|
950
|
+
),
|
|
951
|
+
state.endplateEdges
|
|
945
952
|
),
|
|
946
953
|
state.endplateWidths,
|
|
947
954
|
state.endplateTrackOffsets
|
|
@@ -1137,7 +1144,15 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
1137
1144
|
const poseOverrides = poseOverridesFromDoc(d);
|
|
1138
1145
|
const endplateWidths = {};
|
|
1139
1146
|
const endplateTrackOffsets = {};
|
|
1147
|
+
const endplateEdges = {};
|
|
1140
1148
|
for (const e of d.endplates ?? []) {
|
|
1149
|
+
if (e.edge && Number.isFinite(e.edge.index) && e.edge.index >= 0)
|
|
1150
|
+
endplateEdges[e.id] = {
|
|
1151
|
+
index: Math.trunc(e.edge.index),
|
|
1152
|
+
...e.edge.section ? { section: e.edge.section } : {},
|
|
1153
|
+
...Number.isFinite(e.edge.fromT) ? { fromT: e.edge.fromT } : {},
|
|
1154
|
+
...Number.isFinite(e.edge.toT) ? { toT: e.edge.toT } : {}
|
|
1155
|
+
};
|
|
1141
1156
|
if (typeof e.widthInches === "number" && e.widthInches > 0)
|
|
1142
1157
|
endplateWidths[e.id] = e.widthInches;
|
|
1143
1158
|
if (typeof e.trackOffsetInches === "number" && Number.isFinite(e.trackOffsetInches))
|
|
@@ -1188,6 +1203,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
1188
1203
|
flexByTrack,
|
|
1189
1204
|
endplateWidths,
|
|
1190
1205
|
endplateTrackOffsets,
|
|
1206
|
+
endplateEdges,
|
|
1191
1207
|
outline,
|
|
1192
1208
|
outlineInner,
|
|
1193
1209
|
sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
|
|
@@ -2534,6 +2550,45 @@ function moduleFeatures(doc) {
|
|
|
2534
2550
|
laneMax: Math.max(...allLanes)
|
|
2535
2551
|
};
|
|
2536
2552
|
}
|
|
2553
|
+
function endplateEdgePose(outline, edge) {
|
|
2554
|
+
if (!outline || outline.length < 3) return null;
|
|
2555
|
+
const n = outline.length;
|
|
2556
|
+
const i = Math.trunc(edge.index);
|
|
2557
|
+
if (!Number.isFinite(i) || i < 0 || i >= n) return null;
|
|
2558
|
+
const p0 = outline[i];
|
|
2559
|
+
const p1 = outline[(i + 1) % n];
|
|
2560
|
+
if (p0.bulge) return null;
|
|
2561
|
+
const t0 = Math.max(0, Math.min(1, edge.fromT ?? 0));
|
|
2562
|
+
const t1 = Math.max(0, Math.min(1, edge.toT ?? 1));
|
|
2563
|
+
const a = { x: p0.x + (p1.x - p0.x) * Math.min(t0, t1), y: p0.y + (p1.y - p0.y) * Math.min(t0, t1) };
|
|
2564
|
+
const b = { x: p0.x + (p1.x - p0.x) * Math.max(t0, t1), y: p0.y + (p1.y - p0.y) * Math.max(t0, t1) };
|
|
2565
|
+
const w = Math.hypot(b.x - a.x, b.y - a.y);
|
|
2566
|
+
if (!(w > 0)) return null;
|
|
2567
|
+
let cx = 0;
|
|
2568
|
+
let cy = 0;
|
|
2569
|
+
for (const v of outline) {
|
|
2570
|
+
cx += v.x;
|
|
2571
|
+
cy += v.y;
|
|
2572
|
+
}
|
|
2573
|
+
cx /= n;
|
|
2574
|
+
cy /= n;
|
|
2575
|
+
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2576
|
+
const ex = (b.x - a.x) / w;
|
|
2577
|
+
const ey = (b.y - a.y) / w;
|
|
2578
|
+
let nx = ey;
|
|
2579
|
+
let ny = -ex;
|
|
2580
|
+
if ((mid.x - cx) * nx + (mid.y - cy) * ny < 0) {
|
|
2581
|
+
nx = -nx;
|
|
2582
|
+
ny = -ny;
|
|
2583
|
+
}
|
|
2584
|
+
return {
|
|
2585
|
+
x: mid.x,
|
|
2586
|
+
y: mid.y,
|
|
2587
|
+
heading: norm360(Math.atan2(ny, nx) * 180 / Math.PI),
|
|
2588
|
+
widthInches: w,
|
|
2589
|
+
face: [a, b]
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2537
2592
|
function returnLoop(shape, opts) {
|
|
2538
2593
|
const L = Math.max(1, opts.leadInches);
|
|
2539
2594
|
const R = Math.max(1, opts.radius);
|
|
@@ -2637,6 +2692,21 @@ function deriveEndplatePoses(geo) {
|
|
|
2637
2692
|
const half = geo.trackHalfSpacingInches ?? 1;
|
|
2638
2693
|
const cfg = (i) => geo.endplateConfigs?.[i] === "double" ? "double" : "single";
|
|
2639
2694
|
const withOverride = (p) => {
|
|
2695
|
+
const bound = geo.endplateEdges?.[p.id];
|
|
2696
|
+
if (bound) {
|
|
2697
|
+
const outline = (bound.section ? geo.sections?.find((s) => s.id === bound.section)?.outline : geo.outline) ?? geo.outline;
|
|
2698
|
+
const e = endplateEdgePose(outline, bound);
|
|
2699
|
+
if (e)
|
|
2700
|
+
return {
|
|
2701
|
+
...p,
|
|
2702
|
+
x: e.x,
|
|
2703
|
+
y: e.y,
|
|
2704
|
+
heading: e.heading,
|
|
2705
|
+
widthInches: e.widthInches,
|
|
2706
|
+
face: e.face,
|
|
2707
|
+
boundToEdge: true
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2640
2710
|
const o = geo.poseOverrides?.[p.id];
|
|
2641
2711
|
return o ? { ...p, x: o.x, y: o.y, heading: norm360(o.heading), manual: true } : p;
|
|
2642
2712
|
};
|
|
@@ -2848,6 +2918,7 @@ exports.divergeSideForHand = divergeSideForHand;
|
|
|
2848
2918
|
exports.docToState = docToState;
|
|
2849
2919
|
exports.emptyEditorState = emptyEditorState;
|
|
2850
2920
|
exports.endplateCentreOffsetInches = endplateCentreOffsetInches;
|
|
2921
|
+
exports.endplateEdgePose = endplateEdgePose;
|
|
2851
2922
|
exports.endplateFaceSegments = endplateFaceSegments;
|
|
2852
2923
|
exports.endplateLead = endplateLead;
|
|
2853
2924
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|