circuit-json-to-lbrn 0.0.41 → 0.0.43

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.
Files changed (91) hide show
  1. package/dist/index.d.ts +12 -1
  2. package/dist/index.js +191 -4
  3. package/lib/ConvertContext.ts +7 -0
  4. package/lib/createCopperCutFillForLayer.ts +232 -0
  5. package/lib/index.ts +70 -4
  6. package/package.json +3 -2
  7. package/site/index.html +40 -5
  8. package/site/main.tsx +13 -0
  9. package/tests/basics/__snapshots__/board-outline-does-not-appear-in soldermask.snap.svg +1 -1
  10. package/tests/basics/__snapshots__/board-outline.snap.svg +1 -1
  11. package/tests/basics/__snapshots__/mixed-soldermask-margins.snap.svg +1 -1
  12. package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-circle.snap.svg +1 -1
  13. package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-path.snap.svg +1 -1
  14. package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-polygon.snap.svg +1 -1
  15. package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-rect.snap.svg +1 -1
  16. package/tests/basics/addPcbCutout/pcb-cutout-circle.test.ts +1 -1
  17. package/tests/basics/addPcbCutout/pcb-cutout-path.test.ts +1 -1
  18. package/tests/basics/addPcbCutout/pcb-cutout-polygon.test.ts +1 -1
  19. package/tests/basics/addPcbCutout/pcb-cutout-rect.test.ts +1 -1
  20. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-circle.snap.svg +1 -1
  21. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-oval.snap.svg +1 -1
  22. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-pill.snap.svg +1 -1
  23. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-rect.snap.svg +1 -1
  24. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-rotated-pill.snap.svg +1 -1
  25. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-soldermask-margin.snap.svg +1 -1
  26. package/tests/basics/addPcbHole/__snapshots__/pcb-hole-with-soldermask.snap.svg +1 -1
  27. package/tests/basics/addPcbHole/pcb-hole-circle.test.ts +1 -1
  28. package/tests/basics/addPcbHole/pcb-hole-oval.test.ts +1 -1
  29. package/tests/basics/addPcbHole/pcb-hole-pill.test.ts +1 -1
  30. package/tests/basics/addPcbHole/pcb-hole-rect.test.ts +1 -1
  31. package/tests/basics/addPcbHole/pcb-hole-rotated-pill.test.ts +1 -1
  32. package/tests/basics/addPcbHole/pcb-hole-soldermask-margin.test.ts +1 -1
  33. package/tests/basics/addPcbHole/pcb-hole-with-soldermask.test.ts +1 -1
  34. package/tests/basics/addPcbVia/__snapshots__/pcb-via-basic.snap.svg +1 -1
  35. package/tests/basics/addPcbVia/__snapshots__/pcb-via-with-net.snap.svg +1 -1
  36. package/tests/basics/addPcbVia/__snapshots__/pcb-via-with-soldermask.snap.svg +1 -1
  37. package/tests/basics/addPcbVia/pcb-via-basic.test.ts +1 -1
  38. package/tests/basics/addPcbVia/pcb-via-with-net.test.ts +1 -1
  39. package/tests/basics/addPcbVia/pcb-via-with-soldermask.test.ts +1 -1
  40. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-circular-hole-with-rect-pad.snap.svg +1 -1
  41. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-oval.snap.svg +1 -1
  42. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-pill-with-rect-pad.snap.svg +1 -1
  43. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-pill.snap.svg +1 -1
  44. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-polygon.snap.svg +1 -1
  45. package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-rotated-pill-with-rect-pad.snap.svg +1 -1
  46. package/tests/basics/addPlatedHole/pcb-plated-hole-circle.test.ts +1 -1
  47. package/tests/basics/addPlatedHole/pcb-plated-hole-circular-hole-with-rect-pad.test.ts +1 -1
  48. package/tests/basics/addPlatedHole/pcb-plated-hole-oval.test.ts +1 -1
  49. package/tests/basics/addPlatedHole/pcb-plated-hole-pill-with-rect-pad.test.ts +1 -1
  50. package/tests/basics/addPlatedHole/pcb-plated-hole-pill.test.ts +1 -1
  51. package/tests/basics/addPlatedHole/pcb-plated-hole-polygon.test.ts +1 -1
  52. package/tests/basics/addPlatedHole/pcb-plated-hole-rotated-pill-with-rect-pad.test.ts +1 -1
  53. package/tests/basics/addSmtPad/__snapshots__/circleSmtPad.snap.svg +1 -1
  54. package/tests/basics/addSmtPad/__snapshots__/pillSmtPad.snap.svg +1 -1
  55. package/tests/basics/addSmtPad/__snapshots__/polygonSmtPad.snap.svg +1 -1
  56. package/tests/basics/addSmtPad/__snapshots__/rotatedPillSmtPad.snap.svg +1 -1
  57. package/tests/basics/addSmtPad/__snapshots__/rotatedRectSmtPad.snap.svg +1 -1
  58. package/tests/basics/addSmtPad/circleSmtPad.test.ts +1 -1
  59. package/tests/basics/addSmtPad/pillSmtPad.test.ts +1 -1
  60. package/tests/basics/addSmtPad/polygonSmtPad.test.ts +1 -1
  61. package/tests/basics/addSmtPad/rotatedPillSmtPad.test.ts +1 -1
  62. package/tests/basics/addSmtPad/rotatedRectSmtPad.test.ts +1 -1
  63. package/tests/basics/board-outline-does-not-appear-in soldermask.test.ts +1 -1
  64. package/tests/basics/board-outline.test.ts +1 -1
  65. package/tests/basics/copper-cut-fill/__snapshots__/copper-cut-fill-basic.snap.svg +8 -0
  66. package/tests/basics/copper-cut-fill/__snapshots__/copper-cut-fill-with-pads.snap.svg +8 -0
  67. package/tests/basics/copper-cut-fill/copper-cut-fill-basic.test.ts +73 -0
  68. package/tests/basics/copper-cut-fill/copper-cut-fill-with-pads.test.ts +128 -0
  69. package/tests/basics/keyboard-defaul60/keyboard-both-layer-includeSoldermask.test.ts +1 -1
  70. package/tests/basics/keyboard-defaul60/keyboard-both-layers.test.ts +1 -1
  71. package/tests/basics/keyboard-defaul60/keyboard-bottom-layer.test.ts +1 -1
  72. package/tests/basics/keyboard-defaul60/keyboard-top-layer.test.ts +1 -1
  73. package/tests/basics/laser-profile.test.ts +4 -4
  74. package/tests/basics/lga-interconnect.test.ts +1 -1
  75. package/tests/basics/mixed-soldermask-margins.test.ts +1 -1
  76. package/tests/basics/single-trace.test.ts +1 -1
  77. package/tests/basics/soldermask/copper-and-soldermask.test.ts +1 -1
  78. package/tests/basics/soldermask/copper-only.test.ts +1 -1
  79. package/tests/basics/soldermask/soldermask-only.test.ts +1 -1
  80. package/tests/basics/soldermask-margin/__snapshots__/percent-soldermask-margin.snap.svg +1 -1
  81. package/tests/basics/soldermask-margin/negative-soldermask-margin.test.ts +1 -1
  82. package/tests/basics/soldermask-margin/percent-soldermask-margin.test.ts +1 -1
  83. package/tests/basics/soldermask-margin/positive-soldermask-margin.test.ts +1 -1
  84. package/tests/basics/trace-margin/pico-w-3x5-led-matrix-trace-margin.test.ts +1 -1
  85. package/tests/basics/trace-margin/trace-margin-basic.test.ts +1 -1
  86. package/tests/basics/trace-margin/trace-margin-error.test.ts +4 -4
  87. package/tests/examples/example01/__snapshots__/example01.snap.svg +1 -1
  88. package/tests/examples/example01/example01.test.ts +1 -1
  89. package/tests/examples/example02/example02.test.ts +1 -1
  90. package/tests/examples/example03/example03.test.ts +1 -1
  91. package/tests/examples/example04/example04.test.ts +1 -1
package/dist/index.d.ts CHANGED
@@ -15,6 +15,17 @@ interface ConvertCircuitJsonToLbrnOptions {
15
15
  includeLayers?: Array<"top" | "bottom">;
16
16
  traceMargin?: number;
17
17
  laserSpotSize?: number;
18
+ /**
19
+ * Whether to generate copper cut fill layers.
20
+ * Creates a ring/band around traces and pads that will be laser cut
21
+ * to remove copper, without cutting into the traces or pads themselves.
22
+ */
23
+ includeCopperCutFill?: boolean;
24
+ /**
25
+ * Margin to expand the copper outline for the cut fill band (in mm).
26
+ * This determines how wide the band of copper removal will be around traces/pads.
27
+ */
28
+ copperCutFillMargin?: number;
18
29
  laserProfile?: {
19
30
  copper?: {
20
31
  speed?: number;
@@ -30,6 +41,6 @@ interface ConvertCircuitJsonToLbrnOptions {
30
41
  };
31
42
  };
32
43
  }
33
- declare const convertCircuitJsonToLbrn: (circuitJson: CircuitJson, options?: ConvertCircuitJsonToLbrnOptions) => LightBurnProject;
44
+ declare const convertCircuitJsonToLbrn: (circuitJson: CircuitJson, options?: ConvertCircuitJsonToLbrnOptions) => Promise<LightBurnProject>;
34
45
 
35
46
  export { type ConvertCircuitJsonToLbrnOptions, convertCircuitJsonToLbrn };
package/dist/index.js CHANGED
@@ -2481,8 +2481,149 @@ var createTraceClearanceAreasForLayer = ({
2481
2481
  }
2482
2482
  };
2483
2483
 
2484
+ // lib/createCopperCutFillForLayer.ts
2485
+ import { Polygon as Polygon5, Box as Box4, Point as Point2 } from "@flatten-js/core";
2486
+ import { ShapePath as ShapePath28, ShapeGroup } from "lbrnts";
2487
+ var manifoldInstance = null;
2488
+ var getManifold = async () => {
2489
+ if (!manifoldInstance) {
2490
+ const moduleName = "manifold-3d";
2491
+ const ManifoldModule = (await import(
2492
+ /* @vite-ignore */
2493
+ moduleName
2494
+ )).default;
2495
+ manifoldInstance = await ManifoldModule();
2496
+ manifoldInstance.setup();
2497
+ }
2498
+ return manifoldInstance;
2499
+ };
2500
+ var polygonToContours = (polygon) => {
2501
+ const contours = [];
2502
+ if (polygon instanceof Box4) {
2503
+ const { xmin, ymin, xmax, ymax } = polygon;
2504
+ contours.push([
2505
+ [xmin, ymin],
2506
+ [xmax, ymin],
2507
+ [xmax, ymax],
2508
+ [xmin, ymax]
2509
+ ]);
2510
+ } else {
2511
+ for (const face of polygon.faces) {
2512
+ const contour = [];
2513
+ for (const edge of face) {
2514
+ contour.push([edge.start.x, edge.start.y]);
2515
+ }
2516
+ if (contour.length >= 3) {
2517
+ contours.push(contour);
2518
+ }
2519
+ }
2520
+ }
2521
+ return contours;
2522
+ };
2523
+ var contourToPolygon = (contour) => {
2524
+ if (contour.length < 3) return null;
2525
+ const points = contour.map(([x, y]) => new Point2(x, y));
2526
+ try {
2527
+ const polygon = new Polygon5(points);
2528
+ if (polygon.faces.size > 0) {
2529
+ return polygon;
2530
+ }
2531
+ } catch {
2532
+ }
2533
+ return null;
2534
+ };
2535
+ var createCopperCutFillForLayer = async ({
2536
+ layer,
2537
+ ctx
2538
+ }) => {
2539
+ const {
2540
+ project,
2541
+ connMap,
2542
+ topCutNetGeoms,
2543
+ bottomCutNetGeoms,
2544
+ topCopperCutFillCutSetting,
2545
+ bottomCopperCutFillCutSetting,
2546
+ copperCutFillMargin
2547
+ } = ctx;
2548
+ const cutSetting = layer === "top" ? topCopperCutFillCutSetting : bottomCopperCutFillCutSetting;
2549
+ if (!cutSetting) {
2550
+ return;
2551
+ }
2552
+ const netGeomMap = layer === "top" ? topCutNetGeoms : bottomCutNetGeoms;
2553
+ const allGeoms = [];
2554
+ for (const net of Object.keys(connMap.netMap)) {
2555
+ const netGeoms = netGeomMap.get(net);
2556
+ if (netGeoms && netGeoms.length > 0) {
2557
+ allGeoms.push(...netGeoms);
2558
+ }
2559
+ }
2560
+ if (allGeoms.length === 0) {
2561
+ return;
2562
+ }
2563
+ try {
2564
+ const manifold = await getManifold();
2565
+ const { CrossSection } = manifold;
2566
+ const allContours = [];
2567
+ for (const geom of allGeoms) {
2568
+ const contours = polygonToContours(geom);
2569
+ allContours.push(...contours);
2570
+ }
2571
+ if (allContours.length === 0) {
2572
+ return;
2573
+ }
2574
+ const copperInside = new CrossSection(allContours, "Positive");
2575
+ const outerBoundary = copperInside.offset(
2576
+ copperCutFillMargin,
2577
+ "Round",
2578
+ // joinType for smooth corners
2579
+ 2,
2580
+ // miterLimit
2581
+ 32
2582
+ // circularSegments for round corners
2583
+ );
2584
+ const cutFillArea = outerBoundary.subtract(copperInside);
2585
+ const simplifiedArea = cutFillArea.simplify(1e-3);
2586
+ const resultContours = simplifiedArea.toPolygons();
2587
+ if (resultContours.length === 0) {
2588
+ copperInside.delete();
2589
+ outerBoundary.delete();
2590
+ cutFillArea.delete();
2591
+ simplifiedArea.delete();
2592
+ return;
2593
+ }
2594
+ const shapeGroup = new ShapeGroup();
2595
+ for (const contour of resultContours) {
2596
+ const polygon = contourToPolygon(contour);
2597
+ if (!polygon) continue;
2598
+ for (const island of polygon.splitToIslands()) {
2599
+ const { verts, prims } = polygonToShapePathData(island);
2600
+ if (verts.length > 0) {
2601
+ shapeGroup.children.push(
2602
+ new ShapePath28({
2603
+ cutIndex: cutSetting.index,
2604
+ verts,
2605
+ prims,
2606
+ isClosed: true
2607
+ // Filled shapes should be closed
2608
+ })
2609
+ );
2610
+ }
2611
+ }
2612
+ }
2613
+ if (shapeGroup.children.length > 0) {
2614
+ project.children.push(shapeGroup);
2615
+ }
2616
+ copperInside.delete();
2617
+ outerBoundary.delete();
2618
+ cutFillArea.delete();
2619
+ simplifiedArea.delete();
2620
+ } catch (error) {
2621
+ console.warn(`Failed to create copper cut fill for ${layer} layer:`, error);
2622
+ }
2623
+ };
2624
+
2484
2625
  // lib/index.ts
2485
- var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2626
+ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
2486
2627
  const db = cju2(circuitJson);
2487
2628
  const project = new LightBurnProject({
2488
2629
  appVersion: "1.7.03",
@@ -2496,6 +2637,8 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2496
2637
  const globalCopperSoldermaskMarginAdjustment = options.globalCopperSoldermaskMarginAdjustment ?? 0;
2497
2638
  const solderMaskMarginPercent = options.solderMaskMarginPercent ?? 0;
2498
2639
  const laserProfile = options.laserProfile;
2640
+ const includeCopperCutFill = options.includeCopperCutFill ?? false;
2641
+ const copperCutFillMargin = options.copperCutFillMargin ?? 0.5;
2499
2642
  const defaultCopperSettings = {
2500
2643
  speed: 300,
2501
2644
  numPasses: 100,
@@ -2553,13 +2696,14 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2553
2696
  crossHatch: true
2554
2697
  });
2555
2698
  project.children.push(soldermaskCutSetting);
2699
+ let nextCutIndex = 4;
2556
2700
  let topTraceClearanceAreaCutSetting;
2557
2701
  let bottomTraceClearanceAreaCutSetting;
2558
2702
  if (shouldGenerateTraceClearanceZones) {
2559
2703
  if (includeLayers.includes("top")) {
2560
2704
  topTraceClearanceAreaCutSetting = new CutSetting({
2561
2705
  type: "Scan",
2562
- index: 4,
2706
+ index: nextCutIndex++,
2563
2707
  name: "Clear Top Trace Clearance Areas",
2564
2708
  numPasses: 12,
2565
2709
  speed: 100,
@@ -2573,7 +2717,7 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2573
2717
  if (includeLayers.includes("bottom")) {
2574
2718
  bottomTraceClearanceAreaCutSetting = new CutSetting({
2575
2719
  type: "Scan",
2576
- index: 5,
2720
+ index: nextCutIndex++,
2577
2721
  name: "Clear Bottom Trace Clearance Areas",
2578
2722
  numPasses: 12,
2579
2723
  speed: 100,
@@ -2585,6 +2729,38 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2585
2729
  project.children.push(bottomTraceClearanceAreaCutSetting);
2586
2730
  }
2587
2731
  }
2732
+ let topCopperCutFillCutSetting;
2733
+ let bottomCopperCutFillCutSetting;
2734
+ if (includeCopperCutFill && includeCopper) {
2735
+ if (includeLayers.includes("top")) {
2736
+ topCopperCutFillCutSetting = new CutSetting({
2737
+ type: "Scan",
2738
+ index: nextCutIndex++,
2739
+ name: "Top Copper Cut Fill",
2740
+ numPasses: copperSettings.numPasses,
2741
+ speed: copperSettings.speed,
2742
+ scanOpt: "individual",
2743
+ interval: laserSpotSize,
2744
+ angle: 45,
2745
+ crossHatch: true
2746
+ });
2747
+ project.children.push(topCopperCutFillCutSetting);
2748
+ }
2749
+ if (includeLayers.includes("bottom")) {
2750
+ bottomCopperCutFillCutSetting = new CutSetting({
2751
+ type: "Scan",
2752
+ index: nextCutIndex++,
2753
+ name: "Bottom Copper Cut Fill",
2754
+ numPasses: copperSettings.numPasses,
2755
+ speed: copperSettings.speed,
2756
+ scanOpt: "individual",
2757
+ interval: laserSpotSize,
2758
+ angle: 45,
2759
+ crossHatch: true
2760
+ });
2761
+ project.children.push(bottomCopperCutFillCutSetting);
2762
+ }
2763
+ }
2588
2764
  const connMap = getFullConnectivityMapFromCircuitJson(circuitJson);
2589
2765
  let origin = options.origin;
2590
2766
  if (!origin) {
@@ -2612,7 +2788,10 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2612
2788
  laserSpotSize,
2613
2789
  topTraceClearanceAreaCutSetting,
2614
2790
  bottomTraceClearanceAreaCutSetting,
2615
- solderMaskMarginPercent
2791
+ solderMaskMarginPercent,
2792
+ topCopperCutFillCutSetting,
2793
+ bottomCopperCutFillCutSetting,
2794
+ copperCutFillMargin
2616
2795
  };
2617
2796
  for (const net of Object.keys(connMap.netMap)) {
2618
2797
  ctx.topCutNetGeoms.set(net, []);
@@ -2657,6 +2836,14 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
2657
2836
  createTraceClearanceAreasForLayer({ layer: "bottom", ctx });
2658
2837
  }
2659
2838
  }
2839
+ if (includeCopperCutFill && includeCopper) {
2840
+ if (includeLayers.includes("top")) {
2841
+ await createCopperCutFillForLayer({ layer: "top", ctx });
2842
+ }
2843
+ if (includeLayers.includes("bottom")) {
2844
+ await createCopperCutFillForLayer({ layer: "bottom", ctx });
2845
+ }
2846
+ }
2660
2847
  return project;
2661
2848
  };
2662
2849
  export {
@@ -44,6 +44,13 @@ export interface ConvertContext {
44
44
  topTraceClearanceAreaCutSetting?: CutSetting
45
45
  bottomTraceClearanceAreaCutSetting?: CutSetting
46
46
 
47
+ // Cut settings for copper cut fill layers
48
+ topCopperCutFillCutSetting?: CutSetting
49
+ bottomCopperCutFillCutSetting?: CutSetting
50
+
47
51
  // Percent-based solder mask margin (scales with element size)
48
52
  solderMaskMarginPercent: number
53
+
54
+ // Copper cut fill margin (how far to expand the copper outline for the cut fill band)
55
+ copperCutFillMargin: number
49
56
  }
@@ -0,0 +1,232 @@
1
+ import { Polygon, Box, Point } from "@flatten-js/core"
2
+ import type { ConvertContext } from "./ConvertContext"
3
+ import { polygonToShapePathData } from "./polygon-to-shape-path"
4
+ import { ShapePath, ShapeGroup } from "lbrnts"
5
+
6
+ // Lazy-load the manifold WASM module
7
+ // Use dynamic import with computed string to prevent bundler from analyzing the import
8
+ // This allows the browser build to succeed - the feature will fail gracefully at runtime
9
+ let manifoldInstance: any = null
10
+
11
+ const getManifold = async () => {
12
+ if (!manifoldInstance) {
13
+ // Use computed module name to prevent bundler from tracing the import
14
+ const moduleName = "manifold-3d"
15
+ const ManifoldModule = (await import(/* @vite-ignore */ moduleName)).default
16
+ manifoldInstance = await ManifoldModule()
17
+ manifoldInstance.setup() // Initialize the JS-friendly API
18
+ }
19
+ return manifoldInstance
20
+ }
21
+
22
+ type Contour = Array<[number, number]>
23
+
24
+ /**
25
+ * Calculates the signed area of a contour.
26
+ * Positive area = counter-clockwise winding (outer boundary)
27
+ * Negative area = clockwise winding (hole)
28
+ */
29
+ const getSignedArea = (contour: Contour): number => {
30
+ let area = 0
31
+ const n = contour.length
32
+ for (let i = 0; i < n; i++) {
33
+ const j = (i + 1) % n
34
+ area += contour[i]![0] * contour[j]![1]
35
+ area -= contour[j]![0] * contour[i]![1]
36
+ }
37
+ return area / 2
38
+ }
39
+
40
+ /**
41
+ * Converts a flatten-js Polygon to an array of contours for manifold CrossSection
42
+ * Each contour is an array of [x, y] coordinates
43
+ */
44
+ const polygonToContours = (polygon: Polygon | Box): Contour[] => {
45
+ const contours: Contour[] = []
46
+
47
+ if (polygon instanceof Box) {
48
+ // Convert Box to contour
49
+ const { xmin, ymin, xmax, ymax } = polygon
50
+ contours.push([
51
+ [xmin, ymin],
52
+ [xmax, ymin],
53
+ [xmax, ymax],
54
+ [xmin, ymax],
55
+ ])
56
+ } else {
57
+ // Handle Polygon faces
58
+ for (const face of polygon.faces) {
59
+ const contour: Contour = []
60
+ for (const edge of face) {
61
+ contour.push([edge.start.x, edge.start.y])
62
+ }
63
+ if (contour.length >= 3) {
64
+ contours.push(contour)
65
+ }
66
+ }
67
+ }
68
+
69
+ return contours
70
+ }
71
+
72
+ /**
73
+ * Converts a single contour to a flatten-js Polygon
74
+ */
75
+ const contourToPolygon = (contour: Contour): Polygon | null => {
76
+ if (contour.length < 3) return null
77
+
78
+ const points = contour.map(([x, y]) => new Point(x, y))
79
+ try {
80
+ const polygon = new Polygon(points)
81
+ if (polygon.faces.size > 0) {
82
+ return polygon
83
+ }
84
+ } catch {
85
+ // Skip invalid polygons
86
+ }
87
+ return null
88
+ }
89
+
90
+ /**
91
+ * Creates copper cut fill for a given layer using manifold3d CrossSection for offset operations.
92
+ *
93
+ * The algorithm:
94
+ * 1. Collect all copper geometries for the layer (traces, pads, vias, plated holes)
95
+ * 2. Union all copper geometries into a single CrossSection (the "inside")
96
+ * 3. Offset (expand) the unified copper outward by copperCutFillMargin (the "outer boundary")
97
+ * 4. Subtract the inside (original copper) from the outer boundary (expanded copper)
98
+ * 5. The result is the ring/band area around copper that needs to be lasered to remove more copper
99
+ *
100
+ * This creates a laser cut path that removes copper around traces and pads but never
101
+ * cuts into the traces or pads themselves.
102
+ */
103
+ export const createCopperCutFillForLayer = async ({
104
+ layer,
105
+ ctx,
106
+ }: {
107
+ layer: "top" | "bottom"
108
+ ctx: ConvertContext
109
+ }): Promise<void> => {
110
+ const {
111
+ project,
112
+ connMap,
113
+ topCutNetGeoms,
114
+ bottomCutNetGeoms,
115
+ topCopperCutFillCutSetting,
116
+ bottomCopperCutFillCutSetting,
117
+ copperCutFillMargin,
118
+ } = ctx
119
+
120
+ // Get the appropriate cut setting for this layer
121
+ const cutSetting =
122
+ layer === "top" ? topCopperCutFillCutSetting : bottomCopperCutFillCutSetting
123
+
124
+ if (!cutSetting) {
125
+ return
126
+ }
127
+
128
+ // Get the appropriate geometry map
129
+ const netGeomMap = layer === "top" ? topCutNetGeoms : bottomCutNetGeoms
130
+
131
+ // Collect all geometries for this layer across all nets
132
+ const allGeoms: Array<Polygon | Box> = []
133
+ for (const net of Object.keys(connMap.netMap)) {
134
+ const netGeoms = netGeomMap.get(net)
135
+ if (netGeoms && netGeoms.length > 0) {
136
+ allGeoms.push(...netGeoms)
137
+ }
138
+ }
139
+
140
+ if (allGeoms.length === 0) {
141
+ return
142
+ }
143
+
144
+ try {
145
+ // Initialize manifold
146
+ const manifold = await getManifold()
147
+ const { CrossSection } = manifold
148
+
149
+ // Collect all contours from all geometries
150
+ const allContours: Contour[] = []
151
+ for (const geom of allGeoms) {
152
+ const contours = polygonToContours(geom)
153
+ allContours.push(...contours)
154
+ }
155
+
156
+ if (allContours.length === 0) {
157
+ return
158
+ }
159
+
160
+ // Create a unified CrossSection from all copper contours (the "inside")
161
+ // The constructor performs a boolean union with Positive fill rule
162
+ const copperInside = new CrossSection(allContours, "Positive")
163
+
164
+ // Offset (expand) the copper outward by the margin to get the "outer boundary"
165
+ // Positive delta expands outward
166
+ const outerBoundary = copperInside.offset(
167
+ copperCutFillMargin,
168
+ "Round", // joinType for smooth corners
169
+ 2.0, // miterLimit
170
+ 32, // circularSegments for round corners
171
+ )
172
+
173
+ // Subtract the inside (original copper) from the outer boundary (expanded copper)
174
+ // This gives us the ring/band area to laser cut
175
+ const cutFillArea = outerBoundary.subtract(copperInside)
176
+
177
+ // Simplify to clean up any spurious tiny segments
178
+ const simplifiedArea = cutFillArea.simplify(0.001)
179
+
180
+ // Get the resulting contours
181
+ const resultContours: Contour[] = simplifiedArea.toPolygons()
182
+
183
+ if (resultContours.length === 0) {
184
+ // Clean up WASM memory
185
+ copperInside.delete()
186
+ outerBoundary.delete()
187
+ cutFillArea.delete()
188
+ simplifiedArea.delete()
189
+ return
190
+ }
191
+
192
+ // Create a ShapeGroup to hold all contours
193
+ // LightBurn uses nonzero winding rule: CCW (positive area) = outer boundary, CW (negative area) = hole
194
+ // By grouping all contours together, LightBurn should properly fill only the ring area
195
+ const shapeGroup = new ShapeGroup()
196
+
197
+ // Convert all contours to ShapePaths and add to the group
198
+ // Preserve winding order: positive area (CCW) = outer boundary, negative area (CW) = hole
199
+ for (const contour of resultContours) {
200
+ const polygon = contourToPolygon(contour)
201
+ if (!polygon) continue
202
+
203
+ for (const island of polygon.splitToIslands()) {
204
+ const { verts, prims } = polygonToShapePathData(island)
205
+
206
+ if (verts.length > 0) {
207
+ shapeGroup.children.push(
208
+ new ShapePath({
209
+ cutIndex: cutSetting.index,
210
+ verts,
211
+ prims,
212
+ isClosed: true, // Filled shapes should be closed
213
+ }),
214
+ )
215
+ }
216
+ }
217
+ }
218
+
219
+ // Add the group to the project if it has shapes
220
+ if (shapeGroup.children.length > 0) {
221
+ project.children.push(shapeGroup)
222
+ }
223
+
224
+ // Clean up WASM memory
225
+ copperInside.delete()
226
+ outerBoundary.delete()
227
+ cutFillArea.delete()
228
+ simplifiedArea.delete()
229
+ } catch (error) {
230
+ console.warn(`Failed to create copper cut fill for ${layer} layer:`, error)
231
+ }
232
+ }
package/lib/index.ts CHANGED
@@ -16,6 +16,7 @@ import { addPcbHole } from "./element-handlers/addPcbHole"
16
16
  import { addPcbCutout } from "./element-handlers/addPcbCutout"
17
17
  import { createCopperShapesForLayer } from "./createCopperShapesForLayer"
18
18
  import { createTraceClearanceAreasForLayer } from "./createTraceClearanceAreasForLayer"
19
+ import { createCopperCutFillForLayer } from "./createCopperCutFillForLayer"
19
20
 
20
21
  export interface ConvertCircuitJsonToLbrnOptions {
21
22
  includeSilkscreen?: boolean
@@ -28,6 +29,17 @@ export interface ConvertCircuitJsonToLbrnOptions {
28
29
  includeLayers?: Array<"top" | "bottom">
29
30
  traceMargin?: number
30
31
  laserSpotSize?: number
32
+ /**
33
+ * Whether to generate copper cut fill layers.
34
+ * Creates a ring/band around traces and pads that will be laser cut
35
+ * to remove copper, without cutting into the traces or pads themselves.
36
+ */
37
+ includeCopperCutFill?: boolean
38
+ /**
39
+ * Margin to expand the copper outline for the cut fill band (in mm).
40
+ * This determines how wide the band of copper removal will be around traces/pads.
41
+ */
42
+ copperCutFillMargin?: number
31
43
  laserProfile?: {
32
44
  copper?: {
33
45
  speed?: number
@@ -43,10 +55,10 @@ export interface ConvertCircuitJsonToLbrnOptions {
43
55
  }
44
56
  }
45
57
  }
46
- export const convertCircuitJsonToLbrn = (
58
+ export const convertCircuitJsonToLbrn = async (
47
59
  circuitJson: CircuitJson,
48
60
  options: ConvertCircuitJsonToLbrnOptions = {},
49
- ): LightBurnProject => {
61
+ ): Promise<LightBurnProject> => {
50
62
  const db = cju(circuitJson)
51
63
  const project = new LightBurnProject({
52
64
  appVersion: "1.7.03",
@@ -63,6 +75,8 @@ export const convertCircuitJsonToLbrn = (
63
75
  options.globalCopperSoldermaskMarginAdjustment ?? 0
64
76
  const solderMaskMarginPercent = options.solderMaskMarginPercent ?? 0
65
77
  const laserProfile = options.laserProfile
78
+ const includeCopperCutFill = options.includeCopperCutFill ?? false
79
+ const copperCutFillMargin = options.copperCutFillMargin ?? 0.5
66
80
 
67
81
  // Default laser settings from GitHub issue
68
82
  const defaultCopperSettings = {
@@ -134,6 +148,9 @@ export const convertCircuitJsonToLbrn = (
134
148
  })
135
149
  project.children.push(soldermaskCutSetting)
136
150
 
151
+ // Track the next available cut setting index
152
+ let nextCutIndex = 4
153
+
137
154
  // Create trace clearance cut settings if needed
138
155
  let topTraceClearanceAreaCutSetting: CutSetting | undefined
139
156
  let bottomTraceClearanceAreaCutSetting: CutSetting | undefined
@@ -142,7 +159,7 @@ export const convertCircuitJsonToLbrn = (
142
159
  if (includeLayers.includes("top")) {
143
160
  topTraceClearanceAreaCutSetting = new CutSetting({
144
161
  type: "Scan",
145
- index: 4,
162
+ index: nextCutIndex++,
146
163
  name: "Clear Top Trace Clearance Areas",
147
164
  numPasses: 12,
148
165
  speed: 100,
@@ -157,7 +174,7 @@ export const convertCircuitJsonToLbrn = (
157
174
  if (includeLayers.includes("bottom")) {
158
175
  bottomTraceClearanceAreaCutSetting = new CutSetting({
159
176
  type: "Scan",
160
- index: 5,
177
+ index: nextCutIndex++,
161
178
  name: "Clear Bottom Trace Clearance Areas",
162
179
  numPasses: 12,
163
180
  speed: 100,
@@ -170,6 +187,42 @@ export const convertCircuitJsonToLbrn = (
170
187
  }
171
188
  }
172
189
 
190
+ // Create copper cut fill cut settings if needed
191
+ let topCopperCutFillCutSetting: CutSetting | undefined
192
+ let bottomCopperCutFillCutSetting: CutSetting | undefined
193
+
194
+ if (includeCopperCutFill && includeCopper) {
195
+ if (includeLayers.includes("top")) {
196
+ topCopperCutFillCutSetting = new CutSetting({
197
+ type: "Scan",
198
+ index: nextCutIndex++,
199
+ name: "Top Copper Cut Fill",
200
+ numPasses: copperSettings.numPasses,
201
+ speed: copperSettings.speed,
202
+ scanOpt: "individual",
203
+ interval: laserSpotSize,
204
+ angle: 45,
205
+ crossHatch: true,
206
+ })
207
+ project.children.push(topCopperCutFillCutSetting)
208
+ }
209
+
210
+ if (includeLayers.includes("bottom")) {
211
+ bottomCopperCutFillCutSetting = new CutSetting({
212
+ type: "Scan",
213
+ index: nextCutIndex++,
214
+ name: "Bottom Copper Cut Fill",
215
+ numPasses: copperSettings.numPasses,
216
+ speed: copperSettings.speed,
217
+ scanOpt: "individual",
218
+ interval: laserSpotSize,
219
+ angle: 45,
220
+ crossHatch: true,
221
+ })
222
+ project.children.push(bottomCopperCutFillCutSetting)
223
+ }
224
+ }
225
+
173
226
  // Build connectivity map and origin
174
227
  const connMap = getFullConnectivityMapFromCircuitJson(circuitJson)
175
228
  let origin = options.origin
@@ -201,6 +254,9 @@ export const convertCircuitJsonToLbrn = (
201
254
  topTraceClearanceAreaCutSetting,
202
255
  bottomTraceClearanceAreaCutSetting,
203
256
  solderMaskMarginPercent,
257
+ topCopperCutFillCutSetting,
258
+ bottomCopperCutFillCutSetting,
259
+ copperCutFillMargin,
204
260
  }
205
261
 
206
262
  // Initialize net geometry maps
@@ -260,5 +316,15 @@ export const convertCircuitJsonToLbrn = (
260
316
  }
261
317
  }
262
318
 
319
+ // Create copper cut fill for each layer
320
+ if (includeCopperCutFill && includeCopper) {
321
+ if (includeLayers.includes("top")) {
322
+ await createCopperCutFillForLayer({ layer: "top", ctx })
323
+ }
324
+ if (includeLayers.includes("bottom")) {
325
+ await createCopperCutFillForLayer({ layer: "bottom", ctx })
326
+ }
327
+ }
328
+
263
329
  return project
264
330
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-json-to-lbrn",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.41",
4
+ "version": "0.0.43",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "bun run site/index.html",
@@ -28,6 +28,7 @@
28
28
  "typescript": "^5"
29
29
  },
30
30
  "dependencies": {
31
- "lbrnts": "^0.0.12"
31
+ "lbrnts": "^0.0.13",
32
+ "manifold-3d": "^3.3.2"
32
33
  }
33
34
  }