@willcgage/module-schematic 0.47.0 → 0.49.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
@@ -478,6 +478,9 @@ function moduleFootprint(input) {
478
478
  // endplate A's face (the far face would be a spurious plate at the throat).
479
479
  endplateFaces: input.loop ? endplateFaceSegments(centerline, widthA, widthB, offA, offB).slice(0, 1) : endplateFaceSegments(centerline, widthA, widthB, offA, offB),
480
480
  outline: sectionOutlines.length || !authored ? null : sampleBenchworkOutline(authored),
481
+ // The donut hole, arc-sampled — only when there's a solid outline to punch it
482
+ // out of (a sectioned module isn't a donut). Renderers cut it from `outline`.
483
+ outlineInner: sectionOutlines.length || !authored || !input.outlineInner || input.outlineInner.length < 3 ? null : sampleBenchworkOutline(input.outlineInner),
481
484
  sectionOutlines
482
485
  };
483
486
  }
@@ -559,6 +562,7 @@ function emptyEditorState(lengthInches) {
559
562
  endplateWidths: {},
560
563
  endplateTrackOffsets: {},
561
564
  outline: [],
565
+ outlineInner: [],
562
566
  sectionBreaks: [],
563
567
  sections: [],
564
568
  controlPoints: [],
@@ -795,6 +799,8 @@ function stateToDoc(state, recordNumber) {
795
799
  // Benchwork footprint outline (module-local inches); only when it's a real
796
800
  // ring (≥ 3 vertices).
797
801
  ...state.outline.length >= 3 ? { outline: state.outline } : {},
802
+ // Benchwork hole (donut inner boundary), when it's a real ring.
803
+ ...state.outlineInner.length >= 3 ? { outlineInner: state.outlineInner } : {},
798
804
  // Internal section joints (inches from A), when the module has more than one.
799
805
  ...state.sectionBreaks.length ? { sectionBreaks: state.sectionBreaks } : {},
800
806
  // Sections as objects — emitted only once the owner has some, so a module
@@ -886,6 +892,11 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
886
892
  y: p.y,
887
893
  ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
888
894
  }));
895
+ const outlineInner = (d.outlineInner ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
896
+ x: p.x,
897
+ y: p.y,
898
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
899
+ }));
889
900
  const mainPath = trackPath(d.mainPath) ?? [];
890
901
  const main2Track_ = (d.tracks ?? []).find((t) => t.id === MAIN2_TRACK_ID);
891
902
  const main2Path = trackPath(d.main2Path ?? main2Track_?.path) ?? [];
@@ -909,6 +920,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
909
920
  endplateWidths,
910
921
  endplateTrackOffsets,
911
922
  outline,
923
+ outlineInner,
912
924
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
913
925
  sections: moduleSections(d),
914
926
  mainPath,
@@ -1322,6 +1334,84 @@ function moduleFeatures(doc) {
1322
1334
  laneMax: Math.max(...allLanes)
1323
1335
  };
1324
1336
  }
1337
+ function returnLoop(shape, opts) {
1338
+ const L = Math.max(1, opts.leadInches);
1339
+ const R = Math.max(1, opts.radius);
1340
+ const hw = Math.min(opts.boardHalfWidth ?? 6, R - 1);
1341
+ const T = { x: L, y: 0 };
1342
+ const r2 = (v) => Math.round(v * 100) / 100;
1343
+ const rd = (pts) => pts.map((p) => ({ x: r2(p.x), y: r2(p.y) }));
1344
+ const arc = (cx, cy, r, a0, a12, steps) => {
1345
+ const out = [];
1346
+ for (let i = 0; i <= steps; i++) out.push({ x: cx + r * Math.cos(a0 + (a12 - a0) * i / steps), y: cy + r * Math.sin(a0 + (a12 - a0) * i / steps) });
1347
+ return out;
1348
+ };
1349
+ const D = shape === "circle" || shape === "square" ? R * 1.15 : R * 1.6;
1350
+ const offY = shape === "offset-teardrop" ? R * 0.7 : 0;
1351
+ const C = { x: L + D, y: offY };
1352
+ const dist = Math.hypot(C.x - T.x, C.y - T.y);
1353
+ const ac = Math.acos(Math.min(1, R / dist));
1354
+ const base = Math.atan2(T.y - C.y, T.x - C.x);
1355
+ const a1 = base + ac;
1356
+ const a2 = base - ac;
1357
+ const P1 = { x: C.x + R * Math.cos(a1), y: C.y + R * Math.sin(a1) };
1358
+ const P2 = { x: C.x + R * Math.cos(a2), y: C.y + R * Math.sin(a2) };
1359
+ const loop = [T, P1, ...arc(C.x, C.y, R, a1, a2 + 2 * Math.PI, 64).slice(1), T];
1360
+ const legDir = (P) => Math.atan2(P.y - T.y, P.x - T.x);
1361
+ const half = Math.abs((legDir(P1) - legDir(P2) + Math.PI) % (2 * Math.PI) - Math.PI) / 2;
1362
+ const ringOuter = (rr) => {
1363
+ const xTop = C.x - Math.sqrt(Math.max(0, rr * rr - (hw - C.y) ** 2));
1364
+ const xBot = C.x - Math.sqrt(Math.max(0, rr * rr - (-hw - C.y) ** 2));
1365
+ const tTop = Math.atan2(hw - C.y, xTop - C.x);
1366
+ const tBot = Math.atan2(-hw - C.y, xBot - C.x);
1367
+ return [
1368
+ { x: 0, y: hw },
1369
+ { x: xTop, y: hw },
1370
+ ...arc(C.x, C.y, rr, tTop, tBot - 2 * Math.PI, 48).slice(1, -1),
1371
+ { x: xBot, y: -hw },
1372
+ { x: 0, y: -hw }
1373
+ ];
1374
+ };
1375
+ const Ri = R - hw;
1376
+ if (shape === "square") {
1377
+ const xL = C.x - R - hw;
1378
+ const xR = C.x + R + hw;
1379
+ const yT = R + hw;
1380
+ const outerSquare = [
1381
+ { x: 0, y: hw },
1382
+ { x: xL, y: hw },
1383
+ { x: xL, y: yT },
1384
+ { x: xR, y: yT },
1385
+ { x: xR, y: -yT },
1386
+ { x: xL, y: -yT },
1387
+ { x: xL, y: -hw },
1388
+ { x: 0, y: -hw }
1389
+ ];
1390
+ const iHalf = (R - hw) / Math.SQRT2;
1391
+ const holeSquare = iHalf > 2 ? [
1392
+ { x: C.x - iHalf, y: iHalf },
1393
+ { x: C.x + iHalf, y: iHalf },
1394
+ { x: C.x + iHalf, y: -iHalf },
1395
+ { x: C.x - iHalf, y: -iHalf }
1396
+ ] : [];
1397
+ return {
1398
+ throat: T,
1399
+ loop: rd(loop),
1400
+ wyeLegs: [rd([T, P1]), rd([T, P2])],
1401
+ wyeHalfAngleDeg: r2(half * 180 / Math.PI),
1402
+ outlineOuter: rd(outerSquare),
1403
+ outlineInner: rd(holeSquare)
1404
+ };
1405
+ }
1406
+ return {
1407
+ throat: T,
1408
+ loop: rd(loop),
1409
+ wyeLegs: [rd([T, P1]), rd([T, P2])],
1410
+ wyeHalfAngleDeg: r2(half * 180 / Math.PI),
1411
+ outlineOuter: rd(ringOuter(R + hw)),
1412
+ outlineInner: Ri > 2 ? rd(arc(C.x, C.y, Ri, 0, 2 * Math.PI, 40)) : []
1413
+ };
1414
+ }
1325
1415
  function geometryTurnDegrees(geometryType, geometryDegrees) {
1326
1416
  switch (geometryType) {
1327
1417
  case "corner_45":
@@ -1535,6 +1625,7 @@ exports.nextId = nextId;
1535
1625
  exports.poseNeedsManual = poseNeedsManual;
1536
1626
  exports.poseOverridesFromDoc = poseOverridesFromDoc;
1537
1627
  exports.remapPos = remapPos;
1628
+ exports.returnLoop = returnLoop;
1538
1629
  exports.sampleBenchworkOutline = sampleBenchworkOutline;
1539
1630
  exports.samplePath = samplePath;
1540
1631
  exports.scaleFeetToInches = scaleFeetToInches;