@willcgage/module-schematic 0.48.0 → 0.49.1

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.d.cts CHANGED
@@ -253,6 +253,10 @@ interface ModuleSchematicDoc {
253
253
  * +y up). Stored as an open ring; renderers close it. Absent = derive an
254
254
  * approximate band from the endplate widths. */
255
255
  outline?: BenchworkPoint[];
256
+ /** Benchwork HOLE — an inner boundary punched out of `outline`, making the
257
+ * board a DONUT (a return loop's open middle). Same frame as `outline`; stored
258
+ * as an open ring, renderers close it. Absent = a solid board. */
259
+ outlineInner?: BenchworkPoint[];
256
260
  /** Internal section joints — inches from endplate A where the module's boards
257
261
  * split into sections. Operationally one unit; these mark construction/transport
258
262
  * seams (exempt from the end-interface standards). Empty/absent = one section.
@@ -405,6 +409,9 @@ interface ModuleFootprintInput {
405
409
  endplateTrackOffsets?: Record<string, number>;
406
410
  /** Authored benchwork outline (module-local inches), or absent for the band. */
407
411
  outline?: BenchworkPoint[] | null;
412
+ /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
413
+ * make a donut. Absent/short = a solid board. */
414
+ outlineInner?: BenchworkPoint[] | null;
408
415
  /** The module's sections (#96 phase 2). When any carries an outline, the
409
416
  * module's footprint is the union of those — `outline` is then ignored. */
410
417
  sections?: SchematicSection[] | null;
@@ -432,6 +439,9 @@ interface ModuleFootprint {
432
439
  /** Authored outline (arc-sampled closed ring) or null → render the band.
433
440
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
434
441
  outline: BenchworkPoint[] | null;
442
+ /** Authored HOLE (arc-sampled closed ring) punched out of `outline` → a donut
443
+ * board. Null = solid. Renderers fill `outline` with `outlineInner` cut out. */
444
+ outlineInner: BenchworkPoint[] | null;
435
445
  /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
436
446
  * they are the module's footprint. Empty = this module doesn't use sections,
437
447
  * so fall back to `outline ?? band` exactly as before. */
@@ -793,6 +803,9 @@ interface EditorState {
793
803
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
794
804
  * Empty = no authored outline (fall back to the endplate-width band). */
795
805
  outline: BenchworkPoint[];
806
+ /** Benchwork HOLE (donut inner boundary), module-local inches. Empty = a solid
807
+ * board. Set by the return-loop generator; punched out of `outline`. */
808
+ outlineInner: BenchworkPoint[];
796
809
  /** Internal section joints — inches from endplate A where the boards split
797
810
  * into sections. Empty = a single section (#48). */
798
811
  sectionBreaks: number[];
package/dist/index.d.ts CHANGED
@@ -253,6 +253,10 @@ interface ModuleSchematicDoc {
253
253
  * +y up). Stored as an open ring; renderers close it. Absent = derive an
254
254
  * approximate band from the endplate widths. */
255
255
  outline?: BenchworkPoint[];
256
+ /** Benchwork HOLE — an inner boundary punched out of `outline`, making the
257
+ * board a DONUT (a return loop's open middle). Same frame as `outline`; stored
258
+ * as an open ring, renderers close it. Absent = a solid board. */
259
+ outlineInner?: BenchworkPoint[];
256
260
  /** Internal section joints — inches from endplate A where the module's boards
257
261
  * split into sections. Operationally one unit; these mark construction/transport
258
262
  * seams (exempt from the end-interface standards). Empty/absent = one section.
@@ -405,6 +409,9 @@ interface ModuleFootprintInput {
405
409
  endplateTrackOffsets?: Record<string, number>;
406
410
  /** Authored benchwork outline (module-local inches), or absent for the band. */
407
411
  outline?: BenchworkPoint[] | null;
412
+ /** Authored benchwork HOLE — the inner boundary punched out of `outline` to
413
+ * make a donut. Absent/short = a solid board. */
414
+ outlineInner?: BenchworkPoint[] | null;
408
415
  /** The module's sections (#96 phase 2). When any carries an outline, the
409
416
  * module's footprint is the union of those — `outline` is then ignored. */
410
417
  sections?: SchematicSection[] | null;
@@ -432,6 +439,9 @@ interface ModuleFootprint {
432
439
  /** Authored outline (arc-sampled closed ring) or null → render the band.
433
440
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
434
441
  outline: BenchworkPoint[] | null;
442
+ /** Authored HOLE (arc-sampled closed ring) punched out of `outline` → a donut
443
+ * board. Null = solid. Renderers fill `outline` with `outlineInner` cut out. */
444
+ outlineInner: BenchworkPoint[] | null;
435
445
  /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
436
446
  * they are the module's footprint. Empty = this module doesn't use sections,
437
447
  * so fall back to `outline ?? band` exactly as before. */
@@ -793,6 +803,9 @@ interface EditorState {
793
803
  * (endplate A's track point at the origin, mainline +x, perpendicular +y up).
794
804
  * Empty = no authored outline (fall back to the endplate-width band). */
795
805
  outline: BenchworkPoint[];
806
+ /** Benchwork HOLE (donut inner boundary), module-local inches. Empty = a solid
807
+ * board. Set by the return-loop generator; punched out of `outline`. */
808
+ outlineInner: BenchworkPoint[];
796
809
  /** Internal section joints — inches from endplate A where the boards split
797
810
  * into sections. Empty = a single section (#48). */
798
811
  sectionBreaks: number[];
package/dist/index.js CHANGED
@@ -476,6 +476,9 @@ function moduleFootprint(input) {
476
476
  // endplate A's face (the far face would be a spurious plate at the throat).
477
477
  endplateFaces: input.loop ? endplateFaceSegments(centerline, widthA, widthB, offA, offB).slice(0, 1) : endplateFaceSegments(centerline, widthA, widthB, offA, offB),
478
478
  outline: sectionOutlines.length || !authored ? null : sampleBenchworkOutline(authored),
479
+ // The donut hole, arc-sampled — only when there's a solid outline to punch it
480
+ // out of (a sectioned module isn't a donut). Renderers cut it from `outline`.
481
+ outlineInner: sectionOutlines.length || !authored || !input.outlineInner || input.outlineInner.length < 3 ? null : sampleBenchworkOutline(input.outlineInner),
479
482
  sectionOutlines
480
483
  };
481
484
  }
@@ -557,6 +560,7 @@ function emptyEditorState(lengthInches) {
557
560
  endplateWidths: {},
558
561
  endplateTrackOffsets: {},
559
562
  outline: [],
563
+ outlineInner: [],
560
564
  sectionBreaks: [],
561
565
  sections: [],
562
566
  controlPoints: [],
@@ -793,6 +797,8 @@ function stateToDoc(state, recordNumber) {
793
797
  // Benchwork footprint outline (module-local inches); only when it's a real
794
798
  // ring (≥ 3 vertices).
795
799
  ...state.outline.length >= 3 ? { outline: state.outline } : {},
800
+ // Benchwork hole (donut inner boundary), when it's a real ring.
801
+ ...state.outlineInner.length >= 3 ? { outlineInner: state.outlineInner } : {},
796
802
  // Internal section joints (inches from A), when the module has more than one.
797
803
  ...state.sectionBreaks.length ? { sectionBreaks: state.sectionBreaks } : {},
798
804
  // Sections as objects — emitted only once the owner has some, so a module
@@ -884,6 +890,11 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
884
890
  y: p.y,
885
891
  ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
886
892
  }));
893
+ const outlineInner = (d.outlineInner ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
894
+ x: p.x,
895
+ y: p.y,
896
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
897
+ }));
887
898
  const mainPath = trackPath(d.mainPath) ?? [];
888
899
  const main2Track_ = (d.tracks ?? []).find((t) => t.id === MAIN2_TRACK_ID);
889
900
  const main2Path = trackPath(d.main2Path ?? main2Track_?.path) ?? [];
@@ -907,6 +918,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
907
918
  endplateWidths,
908
919
  endplateTrackOffsets,
909
920
  outline,
921
+ outlineInner,
910
922
  sectionBreaks: (d.sectionBreaks ?? []).filter((n) => Number.isFinite(n)).map((n) => sc(n)),
911
923
  sections: moduleSections(d),
912
924
  mainPath,
@@ -1332,36 +1344,7 @@ function returnLoop(shape, opts) {
1332
1344
  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) });
1333
1345
  return out;
1334
1346
  };
1335
- if (shape === "square") {
1336
- const s = 2 * R;
1337
- const loop2 = [
1338
- { x: L, y: 0 },
1339
- { x: L, y: R },
1340
- { x: L + s, y: R },
1341
- { x: L + s, y: -R },
1342
- { x: L, y: -R },
1343
- { x: L, y: 0 }
1344
- ];
1345
- return {
1346
- throat: T,
1347
- loop: rd(loop2),
1348
- wyeLegs: [rd([T, { x: L, y: R }]), rd([T, { x: L, y: -R }])],
1349
- wyeHalfAngleDeg: 90,
1350
- outlineOuter: rd([
1351
- { x: 0, y: hw },
1352
- { x: L + s + hw, y: hw },
1353
- { x: L + s + hw, y: -R - hw },
1354
- { x: 0, y: -R - hw }
1355
- ]),
1356
- outlineInner: rd([
1357
- { x: L + hw, y: R - hw },
1358
- { x: L + s - hw, y: R - hw },
1359
- { x: L + s - hw, y: -R + hw },
1360
- { x: L + hw, y: -R + hw }
1361
- ])
1362
- };
1363
- }
1364
- const D = shape === "circle" ? R * 1.15 : R * 1.6;
1347
+ const D = shape === "circle" || shape === "square" ? R * 1.15 : R * 1.6;
1365
1348
  const offY = shape === "offset-teardrop" ? R * 0.7 : 0;
1366
1349
  const C = { x: L + D, y: offY };
1367
1350
  const dist = Math.hypot(C.x - T.x, C.y - T.y);
@@ -1379,15 +1362,47 @@ function returnLoop(shape, opts) {
1379
1362
  const xBot = C.x - Math.sqrt(Math.max(0, rr * rr - (-hw - C.y) ** 2));
1380
1363
  const tTop = Math.atan2(hw - C.y, xTop - C.x);
1381
1364
  const tBot = Math.atan2(-hw - C.y, xBot - C.x);
1365
+ let end = tBot;
1366
+ while (end >= tTop) end -= 2 * Math.PI;
1382
1367
  return [
1383
1368
  { x: 0, y: hw },
1384
1369
  { x: xTop, y: hw },
1385
- ...arc(C.x, C.y, rr, tTop, tBot - 2 * Math.PI, 48).slice(1, -1),
1370
+ ...arc(C.x, C.y, rr, tTop, end, 48).slice(1, -1),
1386
1371
  { x: xBot, y: -hw },
1387
1372
  { x: 0, y: -hw }
1388
1373
  ];
1389
1374
  };
1390
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
+ }
1391
1406
  return {
1392
1407
  throat: T,
1393
1408
  loop: rd(loop),