@willcgage/module-schematic 0.85.0 → 0.86.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
@@ -2245,6 +2245,197 @@ function partsPlaceable(library = BUILT_IN_TRACK_PARTS) {
2245
2245
  }
2246
2246
  return { placeable, blocked };
2247
2247
  }
2248
+ var JOINT_SNAP_INCHES = 0.01;
2249
+ function placedJoints(pieces, library = BUILT_IN_TRACK_PARTS) {
2250
+ const out = [];
2251
+ const RAD = Math.PI / 180;
2252
+ for (const p of pieces) {
2253
+ const part = library.find((x) => x.id === p.partId);
2254
+ if (!part) continue;
2255
+ const geo = partGeometry(part, library);
2256
+ if (!geo) continue;
2257
+ const c = Math.cos(p.rotationDeg * RAD);
2258
+ const s = Math.sin(p.rotationDeg * RAD);
2259
+ for (const j of geo.joints) {
2260
+ const lx = part.kind === "flex" && j.id === "b" ? p.lengthInches ?? 0 : j.x;
2261
+ const ly = p.flipped ? -j.y : j.y;
2262
+ const h = (p.flipped ? -j.angleDeg : j.angleDeg) + p.rotationDeg;
2263
+ out.push({
2264
+ key: `${p.id}.${j.id}`,
2265
+ piece: p.id,
2266
+ joint: j.id,
2267
+ role: j.role,
2268
+ x: p.x + lx * c - ly * s,
2269
+ y: p.y + lx * s + ly * c,
2270
+ headingDeg: norm360(h)
2271
+ });
2272
+ }
2273
+ }
2274
+ return out;
2275
+ }
2276
+ function buildTrackGraph(pieces, library = BUILT_IN_TRACK_PARTS, snapInches = JOINT_SNAP_INCHES) {
2277
+ const joints = placedJoints(pieces, library);
2278
+ const unplaceable = [];
2279
+ for (const p of pieces) {
2280
+ const part = library.find((x) => x.id === p.partId);
2281
+ if (!part) {
2282
+ unplaceable.push({ piece: p.id, partId: p.partId, why: "no such part in the library" });
2283
+ continue;
2284
+ }
2285
+ const why = partGeometryGap(part);
2286
+ if (why) unplaceable.push({ piece: p.id, partId: p.partId, why });
2287
+ }
2288
+ const groups = [];
2289
+ const taken = /* @__PURE__ */ new Set();
2290
+ for (const j of joints) {
2291
+ if (taken.has(j.key)) continue;
2292
+ const g = [j];
2293
+ taken.add(j.key);
2294
+ for (const k of joints) {
2295
+ if (taken.has(k.key) || k.piece === j.piece) continue;
2296
+ if (Math.hypot(k.x - j.x, k.y - j.y) <= snapInches) {
2297
+ g.push(k);
2298
+ taken.add(k.key);
2299
+ }
2300
+ }
2301
+ groups.push(g);
2302
+ }
2303
+ const connections = [];
2304
+ const open = [];
2305
+ const conflicts = [];
2306
+ for (const g of groups) {
2307
+ if (g.length === 1) open.push(g[0].key);
2308
+ else if (g.length === 2) connections.push({ a: g[0].key, b: g[1].key });
2309
+ else {
2310
+ conflicts.push({
2311
+ x: g[0].x,
2312
+ y: g[0].y,
2313
+ joints: g.map((j) => j.key),
2314
+ reason: `${g.length} track ends are stacked in one place. Rail has two ends, so a junction of three is a turnout, not a joint \u2014 none of these are joined until one is moved.`
2315
+ });
2316
+ for (const j of g) open.push(j.key);
2317
+ }
2318
+ }
2319
+ return { joints, connections, open, conflicts, unplaceable };
2320
+ }
2321
+ function walkTrackGraph(graph, pieces, startAt, library = BUILT_IN_TRACK_PARTS) {
2322
+ const byKey = new Map(graph.joints.map((j) => [j.key, j]));
2323
+ const pieceById = new Map(pieces.map((p) => [p.id, p]));
2324
+ const link = /* @__PURE__ */ new Map();
2325
+ for (const c of graph.connections) {
2326
+ link.set(c.a, c.b);
2327
+ link.set(c.b, c.a);
2328
+ }
2329
+ const partOf = (pid) => {
2330
+ const p = pieceById.get(pid);
2331
+ return p ? library.find((x) => x.id === p.partId) : void 0;
2332
+ };
2333
+ const geoOf = (pid) => {
2334
+ const part = partOf(pid);
2335
+ return part ? partGeometry(part, library) : null;
2336
+ };
2337
+ const gap = (a, b) => a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
2338
+ const routes = [];
2339
+ const turnouts = [];
2340
+ const warnings = [];
2341
+ const queued = /* @__PURE__ */ new Set();
2342
+ const pending = [];
2343
+ const walk = (startKey, startPos, id, bornAt) => {
2344
+ const route = {
2345
+ id,
2346
+ fromPos: startPos,
2347
+ toPos: startPos,
2348
+ bornAt,
2349
+ endsAt: null,
2350
+ pieces: [],
2351
+ lateral: 0
2352
+ };
2353
+ let cur = startKey;
2354
+ let pos = startPos;
2355
+ const guard = /* @__PURE__ */ new Set();
2356
+ while (cur && !guard.has(cur)) {
2357
+ guard.add(cur);
2358
+ const here = byKey.get(cur);
2359
+ if (!here) break;
2360
+ const geo = geoOf(here.piece);
2361
+ if (!geo) break;
2362
+ const opts = geo.routes.filter((r) => r.includes(here.joint));
2363
+ if (!opts.length) break;
2364
+ const pick = opts.find((r) => r.includes("through")) ?? opts[0];
2365
+ const exitJoint = pick[0] === here.joint ? pick[1] : pick[0];
2366
+ const exit = byKey.get(`${here.piece}.${exitJoint}`);
2367
+ const part = partOf(here.piece);
2368
+ if (part && (part.kind === "turnout" || part.kind === "wye")) {
2369
+ const throat = byKey.get(`${here.piece}.throat`);
2370
+ const through = byKey.get(`${here.piece}.through`) ?? byKey.get(`${here.piece}.legA`);
2371
+ const body = gap(throat, through);
2372
+ const lead = part.lead?.inches ?? body / 2;
2373
+ const dvj = byKey.get(`${here.piece}.diverge`) ?? byKey.get(`${here.piece}.legB`);
2374
+ const frogPt = throat && body > 0 && dvj ? {
2375
+ x: throat.x + (dvj.x - throat.x) * lead / body,
2376
+ y: throat.y + (dvj.y - throat.y) * lead / body
2377
+ } : null;
2378
+ const toFrog = here.joint === "throat" ? lead : here.joint === "diverge" || here.joint === "legB" ? frogPt && dvj ? Math.hypot(dvj.x - frogPt.x, dvj.y - frogPt.y) : Math.max(0, body - lead) : Math.max(0, body - lead);
2379
+ const frogPos = pos + toFrog;
2380
+ if (here.joint === "diverge" || here.joint === "legB") {
2381
+ route.endsAt = here.piece;
2382
+ const known = turnouts.find((t) => t.id === here.piece);
2383
+ route.toPos = known ? known.pos : frogPos;
2384
+ break;
2385
+ }
2386
+ turnouts.push({ id: here.piece, pos: frogPos, onRoute: id, divergeRoute: null });
2387
+ for (const dj of ["diverge", "legB"]) {
2388
+ const dk = `${here.piece}.${dj}`;
2389
+ const d = byKey.get(dk);
2390
+ if (!d || queued.has(dk)) continue;
2391
+ queued.add(dk);
2392
+ const fp = throat && body > 0 ? {
2393
+ x: throat.x + (d.x - throat.x) * lead / body,
2394
+ y: throat.y + (d.y - throat.y) * lead / body
2395
+ } : null;
2396
+ pending.push({
2397
+ from: here.piece,
2398
+ at: frogPos,
2399
+ joint: dk,
2400
+ skew: fp ? Math.hypot(d.x - fp.x, d.y - fp.y) : 0
2401
+ });
2402
+ }
2403
+ }
2404
+ route.pieces.push(here.piece);
2405
+ for (const j of graph.joints)
2406
+ if (j.piece === here.piece && Math.abs(j.y) > Math.abs(route.lateral))
2407
+ route.lateral = j.y;
2408
+ pos += gap(here, exit);
2409
+ route.toPos = pos;
2410
+ const next = exit ? link.get(exit.key) : void 0;
2411
+ if (!next) break;
2412
+ cur = next;
2413
+ }
2414
+ return route;
2415
+ };
2416
+ routes.push(walk(`${startAt.piece}.${startAt.joint}`, 0, "main", null));
2417
+ let n = 0;
2418
+ while (pending.length) {
2419
+ const b = pending.shift();
2420
+ const start = link.get(b.joint);
2421
+ if (!start) {
2422
+ warnings.push(`the route diverging at ${b.from} is not connected to anything`);
2423
+ continue;
2424
+ }
2425
+ n += 1;
2426
+ const r = walk(start, b.at + b.skew, `route${n}`, b.from);
2427
+ r.fromPos = b.at;
2428
+ routes.push(r);
2429
+ const sw = turnouts.find((t) => t.id === b.from);
2430
+ if (sw && !sw.divergeRoute) sw.divergeRoute = r.id;
2431
+ }
2432
+ const reached = new Set(routes.flatMap((r) => r.pieces));
2433
+ for (const p of pieces)
2434
+ if (!reached.has(p.id) && !graph.unplaceable.some((u) => u.piece === p.id))
2435
+ warnings.push(`${p.id} is not reachable from the endplate \u2014 nothing connects it`);
2436
+ for (const c of graph.conflicts) warnings.push(c.reason);
2437
+ return { routes, turnouts, warnings };
2438
+ }
2248
2439
  function frogCasting(cl, opts = {}) {
2249
2440
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
2250
2441
  const w = opts.reachInches ?? g * 1.5;
@@ -2892,6 +3083,7 @@ exports.FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES = FREEMO_ENDPLATE_TRACK_FA
2892
3083
  exports.FREEMO_ENDPLATE_WIDTH_MIN_INCHES = FREEMO_ENDPLATE_WIDTH_MIN_INCHES;
2893
3084
  exports.FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
2894
3085
  exports.FREEMO_TRACK_SPACING_INCHES = FREEMO_TRACK_SPACING_INCHES;
3086
+ exports.JOINT_SNAP_INCHES = JOINT_SNAP_INCHES;
2895
3087
  exports.MAIN2_TRACK_ID = MAIN2_TRACK_ID;
2896
3088
  exports.MAIN_TRACK_ID = MAIN_TRACK_ID;
2897
3089
  exports.N_CAR_LENGTH_INCHES = N_CAR_LENGTH_INCHES;
@@ -2908,6 +3100,7 @@ exports.benchworkBand = benchworkBand;
2908
3100
  exports.benchworkOutline = benchworkOutline;
2909
3101
  exports.buildCrossover = buildCrossover;
2910
3102
  exports.buildPassingSiding = buildPassingSiding;
3103
+ exports.buildTrackGraph = buildTrackGraph;
2911
3104
  exports.buildTransition = buildTransition;
2912
3105
  exports.carCapacity = carCapacity;
2913
3106
  exports.checkEndplateWidth = checkEndplateWidth;
@@ -2956,6 +3149,7 @@ exports.partOutlineAtFrog = partOutlineAtFrog;
2956
3149
  exports.partsPlaceable = partsPlaceable;
2957
3150
  exports.pastFrogInchesForSize = pastFrogInchesForSize;
2958
3151
  exports.pathLengthInches = pathLengthInches;
3152
+ exports.placedJoints = placedJoints;
2959
3153
  exports.poseNeedsManual = poseNeedsManual;
2960
3154
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
2961
3155
  exports.remapPos = remapPos;
@@ -2988,5 +3182,6 @@ exports.turnoutFacing = turnoutFacing;
2988
3182
  exports.turnoutOccupiedSpan = turnoutOccupiedSpan;
2989
3183
  exports.turnoutPartForSize = turnoutPartForSize;
2990
3184
  exports.usableCapacity = usableCapacity;
3185
+ exports.walkTrackGraph = walkTrackGraph;
2991
3186
  //# sourceMappingURL=index.cjs.map
2992
3187
  //# sourceMappingURL=index.cjs.map