@willcgage/module-schematic 0.57.0 → 0.59.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
@@ -769,7 +769,8 @@ function stateToDoc(state, recordNumber) {
769
769
  name: t.name || void 0,
770
770
  ...t.size ? { size: t.size } : {},
771
771
  ...t.curved ? { curved: true } : {},
772
- ...t.flipped ? { flipped: true } : {}
772
+ ...t.flipped ? { flipped: true } : {},
773
+ ...t.partId ? { partId: t.partId } : {}
773
774
  })),
774
775
  ...state.crossings.length > 0 ? {
775
776
  crossings: state.crossings.map((x) => ({
@@ -957,7 +958,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
957
958
  kind: t.kind ?? "right",
958
959
  ...t.size ? { size: t.size } : {},
959
960
  ...t.curved ? { curved: true } : {},
960
- ...t.flipped ? { flipped: true } : {}
961
+ ...t.flipped ? { flipped: true } : {},
962
+ ...t.partId ? { partId: t.partId } : {}
961
963
  })),
962
964
  controlPoints: readControlPoints(d, sc),
963
965
  industries: (d.industries ?? []).map((ind) => ({
@@ -1399,6 +1401,44 @@ function mergeImportedParts(imported, library = BUILT_IN_TRACK_PARTS, sourceName
1399
1401
  }
1400
1402
  return out;
1401
1403
  }
1404
+ function partOutlineAtFrog(part, leadInches, stepsPerCurve = 16) {
1405
+ const ends = part.ends ?? [];
1406
+ const segs = part.segments ?? [];
1407
+ if (!segs.length || ends.length < 2) return null;
1408
+ let points = ends[0];
1409
+ if (ends.length >= 3) {
1410
+ let bestPair = Infinity;
1411
+ let pairIdx = [1, 2];
1412
+ for (let i = 0; i < ends.length; i++) {
1413
+ for (let j = i + 1; j < ends.length; j++) {
1414
+ const d = Math.hypot(ends[i].x - ends[j].x, ends[i].y - ends[j].y);
1415
+ if (d < bestPair) {
1416
+ bestPair = d;
1417
+ pairIdx = [i, j];
1418
+ }
1419
+ }
1420
+ }
1421
+ const odd = ends.findIndex((_, k) => k !== pairIdx[0] && k !== pairIdx[1]);
1422
+ if (odd >= 0) points = ends[odd];
1423
+ }
1424
+ const a = points.angleDeg * Math.PI / 180;
1425
+ const ux = -Math.sin(a);
1426
+ const uy = -Math.cos(a);
1427
+ const toLocal = (p) => {
1428
+ const dx = p.x - points.x;
1429
+ const dy = p.y - points.y;
1430
+ return { x: dx * ux + dy * uy, y: dx * -uy + dy * ux };
1431
+ };
1432
+ let sign = 1;
1433
+ const offAxis = ends.filter((e) => e !== points).map(toLocal).sort((p, q) => Math.abs(q.y) - Math.abs(p.y))[0];
1434
+ if (offAxis && offAxis.y < 0) sign = -1;
1435
+ return samplePartSegments(segs, stepsPerCurve).map(
1436
+ (poly) => poly.map((p) => {
1437
+ const l = toLocal(p);
1438
+ return { x: l.x - leadInches, y: l.y * sign };
1439
+ })
1440
+ );
1441
+ }
1402
1442
  function turnoutClosure(size, opts = {}) {
1403
1443
  const N = size > 0 ? size : 6;
1404
1444
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
@@ -1406,13 +1446,33 @@ function turnoutClosure(size, opts = {}) {
1406
1446
  const frogSlope = 1 / N;
1407
1447
  const k = (lead / N - g) / (lead * lead);
1408
1448
  const switchSlope = Math.max(0, frogSlope - 2 * k * lead);
1449
+ const target = opts.arriveAtInches;
1450
+ let a = Infinity;
1451
+ let b = 0;
1452
+ if (target != null && target > g) {
1453
+ b = Math.max(0.01, opts.easeInches ?? lead);
1454
+ a = (target - g) / frogSlope - b / 2;
1455
+ if (a < 0) {
1456
+ a = 0;
1457
+ b = 2 * (target - g) / frogSlope;
1458
+ }
1459
+ }
1460
+ const span = target != null && target > g ? lead + a + b : Infinity;
1409
1461
  return {
1410
1462
  lead,
1411
1463
  switchSlope,
1412
1464
  frogSlope,
1413
- // Past the frog the route is straight at the frog angle — the closure curve
1414
- // has done its work, so don't keep bending (that's the tangent Option 1).
1415
- offsetAt: (s) => s <= lead ? switchSlope * s + k * s * s : g + frogSlope * (s - lead)
1465
+ span,
1466
+ easeInches: b,
1467
+ offsetAt: (s) => {
1468
+ if (s <= lead) return switchSlope * s + k * s * s;
1469
+ const past = s - lead;
1470
+ if (past <= a) return g + frogSlope * past;
1471
+ if (target == null) return g + frogSlope * past;
1472
+ const u = Math.min(past - a, b);
1473
+ const eased = g + frogSlope * a + frogSlope * u - frogSlope * u * u / (2 * b);
1474
+ return past - a >= b ? target : eased;
1475
+ }
1416
1476
  };
1417
1477
  }
1418
1478
  function divergeSideForHand(kind, stubDir, flipped) {
@@ -1977,6 +2037,7 @@ exports.moduleLengthFromSections = moduleLengthFromSections;
1977
2037
  exports.moduleSections = moduleSections;
1978
2038
  exports.nextId = nextId;
1979
2039
  exports.parseXtpLibrary = parseXtpLibrary;
2040
+ exports.partOutlineAtFrog = partOutlineAtFrog;
1980
2041
  exports.poseNeedsManual = poseNeedsManual;
1981
2042
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
1982
2043
  exports.remapPos = remapPos;