@willcgage/module-schematic 0.103.0 → 0.106.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 +278 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +278 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1698,7 +1698,7 @@ var FAST_TRACKS_N_ME55_CROSSOVERS = [
|
|
|
1698
1698
|
trackSpacing: {
|
|
1699
1699
|
inches: spacing,
|
|
1700
1700
|
source: manufacturer,
|
|
1701
|
-
note: `${spec}. \u26A0\uFE0F Free-moN \xA72.0
|
|
1701
|
+
note: `${spec}. ${spacing}\u2033, and the fixture cannot be built to another spacing. \u26A0\uFE0F THIS IS NOT A DEPARTURE FROM THE STANDARD. Free-moN \xA72.0 fixes ${FREEMO_TRACK_SPACING_INCHES}\u2033 at the ENDPLATE \u2014 "double track endplates must have a track spacing of 1.125 inches", with track perpendicular, straight and level for 4\u2033 from the outside face. What the two mains do in between is the module builder's business, and every real double crossover pinches them closer. Earlier wording here called the fixture "tighter than the standard", which reads as non-conformance and is wrong.`
|
|
1702
1702
|
}
|
|
1703
1703
|
};
|
|
1704
1704
|
});
|
|
@@ -2176,6 +2176,31 @@ function turnoutClosure(size, opts = {}) {
|
|
|
2176
2176
|
}
|
|
2177
2177
|
};
|
|
2178
2178
|
}
|
|
2179
|
+
function crossoverAssembly(part) {
|
|
2180
|
+
const overall = part.overallLength?.inches;
|
|
2181
|
+
const spacing = part.trackSpacing?.inches;
|
|
2182
|
+
const n = part.frogNumber;
|
|
2183
|
+
if (!overall || !spacing || !n) return null;
|
|
2184
|
+
const lengthInches = overall * (part.piecesPerAssembly ?? 1);
|
|
2185
|
+
const tan = part.actualAngle ? Math.tan(part.actualAngle.deg * Math.PI / 180) : 1 / n;
|
|
2186
|
+
if (!(tan > 0)) return null;
|
|
2187
|
+
const crossingRunInches = spacing / tan;
|
|
2188
|
+
const mid = lengthInches / 2;
|
|
2189
|
+
const half = crossingRunInches / 2;
|
|
2190
|
+
return {
|
|
2191
|
+
lengthInches,
|
|
2192
|
+
spacingInches: spacing,
|
|
2193
|
+
crossingRunInches,
|
|
2194
|
+
pointsAtInches: [mid - half, mid + half],
|
|
2195
|
+
scissorsAtInches: mid
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
function weakestOf(...ds) {
|
|
2199
|
+
const rank = ["measured", "manufacturer", "derived", "unverified"];
|
|
2200
|
+
let worst = 0;
|
|
2201
|
+
for (const d of ds) if (d) worst = Math.max(worst, rank.indexOf(d.source));
|
|
2202
|
+
return rank[worst];
|
|
2203
|
+
}
|
|
2179
2204
|
function partGeometryGap(part) {
|
|
2180
2205
|
if (part.kind === "flex") return null;
|
|
2181
2206
|
if (part.kind === "bumper") return null;
|
|
@@ -2186,8 +2211,13 @@ function partGeometryGap(part) {
|
|
|
2186
2211
|
if (part.arcDegrees == null) return "no arc \u2014 the radius alone does not say how far it turns";
|
|
2187
2212
|
return null;
|
|
2188
2213
|
}
|
|
2189
|
-
if (part.kind === "crossover")
|
|
2190
|
-
|
|
2214
|
+
if (part.kind === "crossover") {
|
|
2215
|
+
if (!part.overallLength) return "no overall length \u2014 nothing says how long the assembly is";
|
|
2216
|
+
if (!part.trackSpacing)
|
|
2217
|
+
return "no track spacing \u2014 a crossover is defined by how far apart the two tracks it joins are";
|
|
2218
|
+
if (part.frogNumber == null) return "no frog number \u2014 the crossing angle is unknown";
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2191
2221
|
if (part.kind === "crossing") return "crossing geometry is not modelled yet";
|
|
2192
2222
|
if (part.kind === "curved-turnout")
|
|
2193
2223
|
return "a curved turnout needs both radii AND its points/frog landmarks; only the radii are published";
|
|
@@ -2230,6 +2260,30 @@ function partGeometry(part, library = BUILT_IN_TRACK_PARTS) {
|
|
|
2230
2260
|
divergingEndMeasured: false
|
|
2231
2261
|
};
|
|
2232
2262
|
}
|
|
2263
|
+
if (part.kind === "crossover") {
|
|
2264
|
+
const g = crossoverAssembly(part);
|
|
2265
|
+
if (!g) return null;
|
|
2266
|
+
return {
|
|
2267
|
+
joints: [
|
|
2268
|
+
{ id: "a1", role: "crossoverEnd", x: 0, y: 0, angleDeg: 180 },
|
|
2269
|
+
{ id: "b1", role: "crossoverEnd", x: g.lengthInches, y: 0, angleDeg: 0 },
|
|
2270
|
+
{ id: "a2", role: "crossoverEnd", x: 0, y: g.spacingInches, angleDeg: 180 },
|
|
2271
|
+
{ id: "b2", role: "crossoverEnd", x: g.lengthInches, y: g.spacingInches, angleDeg: 0 }
|
|
2272
|
+
],
|
|
2273
|
+
// ⭐ FOUR ROUTES, and that is the whole point. From either end of either
|
|
2274
|
+
// track a train can run straight on or cross to the other — which is what
|
|
2275
|
+
// makes this ONE assembly rather than four turnouts that happen to be near
|
|
2276
|
+
// each other.
|
|
2277
|
+
routes: [
|
|
2278
|
+
["a1", "b1"],
|
|
2279
|
+
["a2", "b2"],
|
|
2280
|
+
["a1", "b2"],
|
|
2281
|
+
["a2", "b1"]
|
|
2282
|
+
],
|
|
2283
|
+
source: weakestOf(part.overallLength, part.trackSpacing),
|
|
2284
|
+
divergingEndMeasured: false
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2233
2287
|
const N = part.frogNumber;
|
|
2234
2288
|
const points = part.pointsOffset;
|
|
2235
2289
|
const overall = part.overallLength;
|
|
@@ -2325,6 +2379,30 @@ function pieceRoutePaths(piece, library = BUILT_IN_TRACK_PARTS) {
|
|
|
2325
2379
|
pts[pts.length - 1] = { x: b.x, y: b.y };
|
|
2326
2380
|
return [{ route: ["a", "b"], points: pts }];
|
|
2327
2381
|
}
|
|
2382
|
+
if (part.kind === "crossover") {
|
|
2383
|
+
const g = crossoverAssembly(part);
|
|
2384
|
+
if (g) {
|
|
2385
|
+
const [p1, p2] = g.pointsAtInches;
|
|
2386
|
+
const S = g.spacingInches;
|
|
2387
|
+
const L = g.lengthInches;
|
|
2388
|
+
const yOf = (id) => id.endsWith("2") ? S : 0;
|
|
2389
|
+
return geo.routes.map((route) => {
|
|
2390
|
+
const [from, to] = route;
|
|
2391
|
+
const y0 = yOf(from);
|
|
2392
|
+
const y1 = yOf(to);
|
|
2393
|
+
const x0 = from.startsWith("a") ? 0 : L;
|
|
2394
|
+
const x1 = to.startsWith("a") ? 0 : L;
|
|
2395
|
+
if (y0 === y1)
|
|
2396
|
+
return { route, points: [place(x0, y0), place(x1, y1)] };
|
|
2397
|
+
const west = Math.min(x0, x1) === 0;
|
|
2398
|
+
const [xa, xb] = west ? [p1, p2] : [p2, p1];
|
|
2399
|
+
return {
|
|
2400
|
+
route,
|
|
2401
|
+
points: [place(x0, y0), place(xa, y0), place(xb, y1), place(x1, y1)]
|
|
2402
|
+
};
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2328
2406
|
for (const route of geo.routes) {
|
|
2329
2407
|
const a = at(route[0]);
|
|
2330
2408
|
const b = at(route[1]);
|
|
@@ -2693,7 +2771,11 @@ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS)
|
|
|
2693
2771
|
route.closedBy = here.piece;
|
|
2694
2772
|
break;
|
|
2695
2773
|
}
|
|
2696
|
-
const
|
|
2774
|
+
const sameTrack = (r) => {
|
|
2775
|
+
const other = r[0] === here.joint ? r[1] : r[0];
|
|
2776
|
+
return other.slice(-1) === here.joint.slice(-1);
|
|
2777
|
+
};
|
|
2778
|
+
const pick = (partOf(here.piece)?.kind === "crossover" ? opts.find(sameTrack) : void 0) ?? opts.find((r) => r.includes("through")) ?? opts[0];
|
|
2697
2779
|
const exitJoint = pick[0] === here.joint ? pick[1] : pick[0];
|
|
2698
2780
|
const exit = byKey.get(`${here.piece}.${exitJoint}`);
|
|
2699
2781
|
const part = partOf(here.piece);
|
|
@@ -2816,7 +2898,11 @@ function graphToDoc(pieces, input) {
|
|
|
2816
2898
|
const main = walk.routes.find((r) => r.id === "main");
|
|
2817
2899
|
const walk2 = input.start2 ? walkTrackGraph(graph, pieces, input.start2, library) : null;
|
|
2818
2900
|
const main2 = walk2?.routes.find((r) => r.id === "main") ?? null;
|
|
2819
|
-
const
|
|
2901
|
+
const sharedByDesign = (id) => {
|
|
2902
|
+
const piece = pieces.find((p) => p.id === id);
|
|
2903
|
+
return library.find((p) => p.id === piece?.partId)?.kind === "crossover";
|
|
2904
|
+
};
|
|
2905
|
+
const claimed = new Set(walk.routes.flatMap((r) => r.pieces).filter((p) => !sharedByDesign(p)));
|
|
2820
2906
|
if (main2 && main2.pieces.some((p) => claimed.has(p))) {
|
|
2821
2907
|
warnings.push(
|
|
2822
2908
|
"Main 2 starts on track that Main 1 already runs along \u2014 they are one run, not two"
|
|
@@ -2933,6 +3019,74 @@ function graphToDoc(pieces, input) {
|
|
|
2933
3019
|
...part ? { partId: part.id } : {}
|
|
2934
3020
|
});
|
|
2935
3021
|
}
|
|
3022
|
+
const crossoverSpans = (() => {
|
|
3023
|
+
const found = [];
|
|
3024
|
+
const routesAll = [
|
|
3025
|
+
...walk.routes.map((r) => ({ r, prefix: "" })),
|
|
3026
|
+
...twoMains ? walk2.routes.map((r) => ({ r, prefix: "main2:" })) : []
|
|
3027
|
+
];
|
|
3028
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3029
|
+
for (const { r, prefix } of routesAll) {
|
|
3030
|
+
for (const sp of r.spans) {
|
|
3031
|
+
const piece = pieceById.get(sp.piece);
|
|
3032
|
+
const part = piece ? library.find((p) => p.id === piece.partId) : void 0;
|
|
3033
|
+
if (!part || part.kind !== "crossover") continue;
|
|
3034
|
+
const g = crossoverAssembly(part);
|
|
3035
|
+
if (!g) continue;
|
|
3036
|
+
const [p1, p2] = g.pointsAtInches;
|
|
3037
|
+
const west = sp.entryJoint.startsWith("a");
|
|
3038
|
+
const at = west ? [sp.fromPos + p1, sp.fromPos + p2] : [sp.fromPos + (g.lengthInches - p2), sp.fromPos + (g.lengthInches - p1)];
|
|
3039
|
+
const track = sp.entryJoint.endsWith("2") ? 2 : 1;
|
|
3040
|
+
const routeId = r.id === "main" && prefix ? "main2:main" : `${prefix}${r.id}`;
|
|
3041
|
+
const rec = seen.get(sp.piece) ?? { piece: sp.piece, part, byTrack: /* @__PURE__ */ new Map() };
|
|
3042
|
+
rec.byTrack.set(track, { routeId, at });
|
|
3043
|
+
seen.set(sp.piece, rec);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
for (const rec of seen.values()) if (rec.byTrack.size === 2) found.push(rec);
|
|
3047
|
+
return found;
|
|
3048
|
+
})();
|
|
3049
|
+
for (const xo of crossoverSpans) {
|
|
3050
|
+
const one = xo.byTrack.get(1);
|
|
3051
|
+
const two = xo.byTrack.get(2);
|
|
3052
|
+
const onA = trackIdOf.get(one.routeId) ?? MAIN_TRACK_ID;
|
|
3053
|
+
const onB = trackIdOf.get(two.routeId) ?? MAIN2_TRACK_ID;
|
|
3054
|
+
const legs = [
|
|
3055
|
+
{ id: `${xo.piece}-a`, from: [onA, one.at[0]], to: [onB, two.at[1]] },
|
|
3056
|
+
{ id: `${xo.piece}-b`, from: [onB, two.at[0]], to: [onA, one.at[1]] }
|
|
3057
|
+
];
|
|
3058
|
+
for (const leg of legs) {
|
|
3059
|
+
tracks.push({
|
|
3060
|
+
id: leg.id,
|
|
3061
|
+
role: "crossover",
|
|
3062
|
+
// The connector lives between the mains, so it takes Main 2's lane —
|
|
3063
|
+
// the same lane the 1-D model has always drawn a crossover in.
|
|
3064
|
+
lane: main2Lane ?? 1,
|
|
3065
|
+
fromPos: round(Math.min(leg.from[1], leg.to[1])),
|
|
3066
|
+
toPos: round(Math.max(leg.from[1], leg.to[1])),
|
|
3067
|
+
trackName: xo.part.name ?? "Crossover",
|
|
3068
|
+
crossoverPartId: xo.part.id
|
|
3069
|
+
});
|
|
3070
|
+
turnouts.push(
|
|
3071
|
+
{
|
|
3072
|
+
id: `${leg.id}-1`,
|
|
3073
|
+
pos: round(leg.from[1]),
|
|
3074
|
+
onTrack: leg.from[0],
|
|
3075
|
+
divergeTrack: leg.id,
|
|
3076
|
+
name: xo.part.name ?? "Crossover",
|
|
3077
|
+
...xo.part.frogNumber != null ? { size: xo.part.frogNumber } : {}
|
|
3078
|
+
},
|
|
3079
|
+
{
|
|
3080
|
+
id: `${leg.id}-2`,
|
|
3081
|
+
pos: round(leg.to[1]),
|
|
3082
|
+
onTrack: leg.to[0],
|
|
3083
|
+
divergeTrack: leg.id,
|
|
3084
|
+
name: xo.part.name ?? "Crossover",
|
|
3085
|
+
...xo.part.frogNumber != null ? { size: xo.part.frogNumber } : {}
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2936
3090
|
turnouts.sort((a, b) => a.pos - b.pos);
|
|
2937
3091
|
for (const t of tracks) {
|
|
2938
3092
|
if (t.role === "main") continue;
|
|
@@ -3028,7 +3182,29 @@ function moduleConversionReport(doc, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3028
3182
|
const straight = placeableTurnoutParts(library, ["turnout"]);
|
|
3029
3183
|
const curved = placeableTurnoutParts(library, ["curved-turnout"]);
|
|
3030
3184
|
const blockers = [];
|
|
3185
|
+
const assemblyPart = /* @__PURE__ */ new Map();
|
|
3186
|
+
for (const t of doc.tracks ?? []) {
|
|
3187
|
+
if (t.role !== "crossover" || !t.crossoverPartId) continue;
|
|
3188
|
+
const part = library.find((p) => p.id === t.crossoverPartId);
|
|
3189
|
+
if (!part || part.kind !== "crossover" || partGeometryGap(part)) continue;
|
|
3190
|
+
for (const sw of doc.turnouts ?? [])
|
|
3191
|
+
if (sw.divergeTrack === t.id) assemblyPart.set(sw.id, part);
|
|
3192
|
+
}
|
|
3031
3193
|
const turnouts = (doc.turnouts ?? []).map((t) => {
|
|
3194
|
+
const asm = assemblyPart.get(t.id);
|
|
3195
|
+
if (asm)
|
|
3196
|
+
return {
|
|
3197
|
+
id: t.id,
|
|
3198
|
+
name: t.name,
|
|
3199
|
+
pos: t.pos,
|
|
3200
|
+
size: t.size,
|
|
3201
|
+
statedPartId: t.partId,
|
|
3202
|
+
partId: asm.id,
|
|
3203
|
+
from: "assembly",
|
|
3204
|
+
source: partGeometry(asm, library)?.source ?? null,
|
|
3205
|
+
why: null,
|
|
3206
|
+
candidates: []
|
|
3207
|
+
};
|
|
3032
3208
|
const wantCurved = t.curved === true;
|
|
3033
3209
|
const pool = wantCurved ? curved : straight;
|
|
3034
3210
|
const base = {
|
|
@@ -3115,9 +3291,17 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3115
3291
|
"this module's mainline is drawn with bends, and laying it as pieces would need each bend's radius \u2014 convert it once curved runs are supported"
|
|
3116
3292
|
);
|
|
3117
3293
|
const partById = new Map(library.map((p) => [p.id, p]));
|
|
3294
|
+
const inAssembly = /* @__PURE__ */ new Set();
|
|
3295
|
+
for (const t of doc.tracks ?? []) {
|
|
3296
|
+
if (t.role !== "crossover" || !t.crossoverPartId) continue;
|
|
3297
|
+
const part = partById.get(t.crossoverPartId);
|
|
3298
|
+
if (!part || part.kind !== "crossover" || partGeometryGap(part)) continue;
|
|
3299
|
+
for (const sw of doc.turnouts ?? []) if (sw.divergeTrack === t.id) inAssembly.add(sw.id);
|
|
3300
|
+
}
|
|
3118
3301
|
const chosen = /* @__PURE__ */ new Map();
|
|
3119
3302
|
const unknown = [];
|
|
3120
3303
|
for (const t of report.turnouts) {
|
|
3304
|
+
if (inAssembly.has(t.id)) continue;
|
|
3121
3305
|
const id = answers.overrides?.[t.id] ?? (t.partId ? t.partId : answers.turnoutPartId) ?? null;
|
|
3122
3306
|
const part = id ? partById.get(id) : void 0;
|
|
3123
3307
|
if (!part || partGeometryGap(part)) unknown.push(t.id);
|
|
@@ -3200,19 +3384,26 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3200
3384
|
const divergeId = geom.joints.find((j) => j.role === "diverge")?.id ?? "diverge";
|
|
3201
3385
|
return { t, piece, span, divergeId };
|
|
3202
3386
|
};
|
|
3387
|
+
const aimEndAt = (piece, to) => {
|
|
3388
|
+
const dx = to.x - piece.x;
|
|
3389
|
+
const dy = to.y - piece.y;
|
|
3390
|
+
const len = Math.hypot(dx, dy);
|
|
3391
|
+
if (!(len > 0)) return;
|
|
3392
|
+
piece.rotationDeg = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
3393
|
+
piece.lengthInches = len;
|
|
3394
|
+
};
|
|
3203
3395
|
const layFlex = (label, hostY, fromPos, toPos, occupied, cuts) => {
|
|
3204
3396
|
const out = flexPieces({ fromPos, toPos, maxPieceInches: maxPiece, occupied, cuts: cuts ?? null });
|
|
3205
|
-
out.
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
return out;
|
|
3397
|
+
const placed = out.map((f, i) => ({
|
|
3398
|
+
id: `f-${label}-${i}`,
|
|
3399
|
+
partId: flexId,
|
|
3400
|
+
x: f.fromPos,
|
|
3401
|
+
y: hostY,
|
|
3402
|
+
rotationDeg: 0,
|
|
3403
|
+
lengthInches: r3(f.lengthInches)
|
|
3404
|
+
}));
|
|
3405
|
+
pieces.push(...placed);
|
|
3406
|
+
return placed;
|
|
3216
3407
|
};
|
|
3217
3408
|
const turnoutsOn = (trackId, hostY) => {
|
|
3218
3409
|
const laid = turnouts.filter((t) => t.onTrack === trackId && chosen.has(t.id)).map((t) => layTurnout(t, hostY)).sort((a, b) => a.span.fromPos - b.span.fromPos);
|
|
@@ -3225,19 +3416,64 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3225
3416
|
clashed.add(cur.t.id);
|
|
3226
3417
|
const gap = Math.abs(cur.t.pos - prev.t.pos);
|
|
3227
3418
|
const body = prev.span.toPos - prev.span.fromPos;
|
|
3419
|
+
const asm = trackById.get(cur.t.divergeTrack)?.role === "crossover";
|
|
3228
3420
|
notLaid.push({
|
|
3229
3421
|
id: cur.t.divergeTrack,
|
|
3230
|
-
why: `${prev.t.name || prev.t.id} and ${cur.t.name || cur.t.id} are ${gap.toFixed(1)}\u2033 apart on the same track, but the turnout chosen is ${body.toFixed(1)}\u2033 long \u2014 their mouldings would overlap,
|
|
3422
|
+
why: asm ? `this is a crossover \u2014 one assembly, not ${gap.toFixed(1)}\u2033 between two separate turnouts. Laying it needs the crossover product itself; pick one on the track and it can be placed as the single piece it is` : `${prev.t.name || prev.t.id} and ${cur.t.name || cur.t.id} are ${gap.toFixed(1)}\u2033 apart on the same track, but the turnout chosen is ${body.toFixed(1)}\u2033 long \u2014 their mouldings would overlap, so they cannot both be where the document puts them`
|
|
3231
3423
|
});
|
|
3232
3424
|
}
|
|
3233
3425
|
}
|
|
3234
3426
|
return laid.filter((l) => !clashed.has(l.t.id));
|
|
3235
3427
|
};
|
|
3428
|
+
const assemblies = (() => {
|
|
3429
|
+
const out = [];
|
|
3430
|
+
const byPart = /* @__PURE__ */ new Map();
|
|
3431
|
+
for (const t of tracks) {
|
|
3432
|
+
if (t.role !== "crossover" || !t.crossoverPartId) continue;
|
|
3433
|
+
const list = byPart.get(t.crossoverPartId) ?? [];
|
|
3434
|
+
list.push(t);
|
|
3435
|
+
byPart.set(t.crossoverPartId, list);
|
|
3436
|
+
}
|
|
3437
|
+
for (const [partId, connectors] of byPart) {
|
|
3438
|
+
const part = partById.get(partId);
|
|
3439
|
+
if (!part || part.kind !== "crossover" || partGeometryGap(part)) continue;
|
|
3440
|
+
const geom = crossoverAssembly(part);
|
|
3441
|
+
if (!geom) continue;
|
|
3442
|
+
const trackIds = new Set(connectors.map((c) => c.id));
|
|
3443
|
+
const mine = turnouts.filter((t) => trackIds.has(t.divergeTrack));
|
|
3444
|
+
if (!mine.length) continue;
|
|
3445
|
+
const centre = mine.reduce((a, t) => a + t.pos, 0) / mine.length;
|
|
3446
|
+
const x0 = centre - geom.lengthInches / 2;
|
|
3447
|
+
const spread = Math.max(...mine.map((t) => t.pos)) - Math.min(...mine.map((t) => t.pos));
|
|
3448
|
+
if (Math.abs(spread - geom.crossingRunInches) > 0.05)
|
|
3449
|
+
warnings.push(
|
|
3450
|
+
`this document puts the crossover's point-sets ${spread.toFixed(2)}\u2033 apart, but a ${part.name ?? partId} is ${geom.crossingRunInches.toFixed(2)}\u2033 \u2014 the assembly is laid to its own dimensions, centred where the document has it`
|
|
3451
|
+
);
|
|
3452
|
+
out.push({ part, geom, x0, trackIds, turnoutIds: new Set(mine.map((t) => t.id)) });
|
|
3453
|
+
}
|
|
3454
|
+
return out;
|
|
3455
|
+
})();
|
|
3456
|
+
const assemblyTracks = new Set(assemblies.flatMap((a) => [...a.trackIds]));
|
|
3236
3457
|
const main = trackById.get(MAIN_TRACK_ID) ?? tracks.find((t) => t.role === "main");
|
|
3237
3458
|
if (!main) return refuse("this module's document has no mainline to convert");
|
|
3238
3459
|
const mains = [main, ...tracks.filter((t) => t.role === "main" && t.id !== main.id)];
|
|
3239
3460
|
const laidTurnouts = /* @__PURE__ */ new Map();
|
|
3240
3461
|
const done = /* @__PURE__ */ new Set();
|
|
3462
|
+
const mainY = laneOffsetAt(mains[0]?.lane ?? 0, 0);
|
|
3463
|
+
for (const [i, a] of assemblies.entries()) {
|
|
3464
|
+
pieces.push({
|
|
3465
|
+
id: `xo-${i}`,
|
|
3466
|
+
partId: a.part.id,
|
|
3467
|
+
x: r3(a.x0),
|
|
3468
|
+
y: mainY,
|
|
3469
|
+
rotationDeg: 0,
|
|
3470
|
+
...a.part.name ? { name: a.part.name } : {}
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
const assemblySpans = assemblies.map((a) => ({
|
|
3474
|
+
fromPos: a.x0,
|
|
3475
|
+
toPos: a.x0 + a.geom.lengthInches
|
|
3476
|
+
}));
|
|
3241
3477
|
for (const m of mains) {
|
|
3242
3478
|
const y = laneOffsetAt(m.lane ?? 0, 0);
|
|
3243
3479
|
const on = turnoutsOn(m.id, y);
|
|
@@ -3245,9 +3481,32 @@ function docToGraph(doc, answers = {}, library = BUILT_IN_TRACK_PARTS) {
|
|
|
3245
3481
|
pieces.push(l.piece);
|
|
3246
3482
|
laidTurnouts.set(l.t.id, l);
|
|
3247
3483
|
}
|
|
3248
|
-
|
|
3484
|
+
const laidFlex = layFlex(
|
|
3485
|
+
m.id,
|
|
3486
|
+
y,
|
|
3487
|
+
m.fromPos ?? 0,
|
|
3488
|
+
m.toPos ?? mainLen,
|
|
3489
|
+
[...on.map((l) => l.span), ...assemblySpans],
|
|
3490
|
+
m.flexCuts
|
|
3491
|
+
);
|
|
3492
|
+
if (m !== mains[0]) {
|
|
3493
|
+
for (const a of assemblies) {
|
|
3494
|
+
const west = a.x0;
|
|
3495
|
+
const east = a.x0 + a.geom.lengthInches;
|
|
3496
|
+
const target = mainY + a.geom.spacingInches;
|
|
3497
|
+
for (const f of laidFlex) {
|
|
3498
|
+
const end = f.x + (f.lengthInches ?? 0);
|
|
3499
|
+
if (Math.abs(end - west) < 1e-6) aimEndAt(f, { x: west, y: target });
|
|
3500
|
+
else if (Math.abs(f.x - east) < 1e-6) {
|
|
3501
|
+
f.y = target;
|
|
3502
|
+
aimEndAt(f, { x: end, y });
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3249
3507
|
done.add(m.id);
|
|
3250
3508
|
}
|
|
3509
|
+
for (const id of assemblyTracks) done.add(id);
|
|
3251
3510
|
let progressed = true;
|
|
3252
3511
|
while (progressed) {
|
|
3253
3512
|
progressed = false;
|
|
@@ -4042,6 +4301,7 @@ exports.buildTransition = buildTransition;
|
|
|
4042
4301
|
exports.carCapacity = carCapacity;
|
|
4043
4302
|
exports.checkEndplateWidth = checkEndplateWidth;
|
|
4044
4303
|
exports.clearancePointPastFrogInches = clearancePointPastFrogInches;
|
|
4304
|
+
exports.crossoverAssembly = crossoverAssembly;
|
|
4045
4305
|
exports.crossoverPinches = crossoverPinches;
|
|
4046
4306
|
exports.deriveEndplatePoses = deriveEndplatePoses;
|
|
4047
4307
|
exports.deriveGraphDoc = deriveGraphDoc;
|