circuit-json-to-lbrn 0.0.42 → 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.
- package/dist/index.d.ts +12 -1
- package/dist/index.js +191 -4
- package/lib/ConvertContext.ts +7 -0
- package/lib/createCopperCutFillForLayer.ts +232 -0
- package/lib/index.ts +70 -4
- package/package.json +3 -2
- package/site/index.html +24 -1
- package/site/main.tsx +8 -0
- package/tests/basics/__snapshots__/board-outline-does-not-appear-in soldermask.snap.svg +1 -1
- package/tests/basics/__snapshots__/board-outline.snap.svg +1 -1
- package/tests/basics/__snapshots__/mixed-soldermask-margins.snap.svg +1 -1
- package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-circle.snap.svg +1 -1
- package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-path.snap.svg +1 -1
- package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-polygon.snap.svg +1 -1
- package/tests/basics/addPcbCutout/__snapshots__/pcb-cutout-rect.snap.svg +1 -1
- package/tests/basics/addPcbCutout/pcb-cutout-circle.test.ts +1 -1
- package/tests/basics/addPcbCutout/pcb-cutout-path.test.ts +1 -1
- package/tests/basics/addPcbCutout/pcb-cutout-polygon.test.ts +1 -1
- package/tests/basics/addPcbCutout/pcb-cutout-rect.test.ts +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-circle.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-oval.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-pill.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-rect.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-rotated-pill.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-soldermask-margin.snap.svg +1 -1
- package/tests/basics/addPcbHole/__snapshots__/pcb-hole-with-soldermask.snap.svg +1 -1
- package/tests/basics/addPcbHole/pcb-hole-circle.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-oval.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-pill.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-rect.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-rotated-pill.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-soldermask-margin.test.ts +1 -1
- package/tests/basics/addPcbHole/pcb-hole-with-soldermask.test.ts +1 -1
- package/tests/basics/addPcbVia/__snapshots__/pcb-via-basic.snap.svg +1 -1
- package/tests/basics/addPcbVia/__snapshots__/pcb-via-with-net.snap.svg +1 -1
- package/tests/basics/addPcbVia/__snapshots__/pcb-via-with-soldermask.snap.svg +1 -1
- package/tests/basics/addPcbVia/pcb-via-basic.test.ts +1 -1
- package/tests/basics/addPcbVia/pcb-via-with-net.test.ts +1 -1
- package/tests/basics/addPcbVia/pcb-via-with-soldermask.test.ts +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-circular-hole-with-rect-pad.snap.svg +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-oval.snap.svg +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-pill-with-rect-pad.snap.svg +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-pill.snap.svg +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-polygon.snap.svg +1 -1
- package/tests/basics/addPlatedHole/__snapshots__/pcb-plated-hole-rotated-pill-with-rect-pad.snap.svg +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-circle.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-circular-hole-with-rect-pad.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-oval.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-pill-with-rect-pad.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-pill.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-polygon.test.ts +1 -1
- package/tests/basics/addPlatedHole/pcb-plated-hole-rotated-pill-with-rect-pad.test.ts +1 -1
- package/tests/basics/addSmtPad/__snapshots__/circleSmtPad.snap.svg +1 -1
- package/tests/basics/addSmtPad/__snapshots__/pillSmtPad.snap.svg +1 -1
- package/tests/basics/addSmtPad/__snapshots__/polygonSmtPad.snap.svg +1 -1
- package/tests/basics/addSmtPad/__snapshots__/rotatedPillSmtPad.snap.svg +1 -1
- package/tests/basics/addSmtPad/__snapshots__/rotatedRectSmtPad.snap.svg +1 -1
- package/tests/basics/addSmtPad/circleSmtPad.test.ts +1 -1
- package/tests/basics/addSmtPad/pillSmtPad.test.ts +1 -1
- package/tests/basics/addSmtPad/polygonSmtPad.test.ts +1 -1
- package/tests/basics/addSmtPad/rotatedPillSmtPad.test.ts +1 -1
- package/tests/basics/addSmtPad/rotatedRectSmtPad.test.ts +1 -1
- package/tests/basics/board-outline-does-not-appear-in soldermask.test.ts +1 -1
- package/tests/basics/board-outline.test.ts +1 -1
- package/tests/basics/copper-cut-fill/__snapshots__/copper-cut-fill-basic.snap.svg +8 -0
- package/tests/basics/copper-cut-fill/__snapshots__/copper-cut-fill-with-pads.snap.svg +8 -0
- package/tests/basics/copper-cut-fill/copper-cut-fill-basic.test.ts +73 -0
- package/tests/basics/copper-cut-fill/copper-cut-fill-with-pads.test.ts +128 -0
- package/tests/basics/keyboard-defaul60/keyboard-both-layer-includeSoldermask.test.ts +1 -1
- package/tests/basics/keyboard-defaul60/keyboard-both-layers.test.ts +1 -1
- package/tests/basics/keyboard-defaul60/keyboard-bottom-layer.test.ts +1 -1
- package/tests/basics/keyboard-defaul60/keyboard-top-layer.test.ts +1 -1
- package/tests/basics/laser-profile.test.ts +4 -4
- package/tests/basics/lga-interconnect.test.ts +1 -1
- package/tests/basics/mixed-soldermask-margins.test.ts +1 -1
- package/tests/basics/single-trace.test.ts +1 -1
- package/tests/basics/soldermask/copper-and-soldermask.test.ts +1 -1
- package/tests/basics/soldermask/copper-only.test.ts +1 -1
- package/tests/basics/soldermask/soldermask-only.test.ts +1 -1
- package/tests/basics/soldermask-margin/__snapshots__/percent-soldermask-margin.snap.svg +1 -1
- package/tests/basics/soldermask-margin/negative-soldermask-margin.test.ts +1 -1
- package/tests/basics/soldermask-margin/percent-soldermask-margin.test.ts +1 -1
- package/tests/basics/soldermask-margin/positive-soldermask-margin.test.ts +1 -1
- package/tests/basics/trace-margin/pico-w-3x5-led-matrix-trace-margin.test.ts +1 -1
- package/tests/basics/trace-margin/trace-margin-basic.test.ts +1 -1
- package/tests/basics/trace-margin/trace-margin-error.test.ts +4 -4
- package/tests/examples/example01/__snapshots__/example01.snap.svg +1 -1
- package/tests/examples/example01/example01.test.ts +1 -1
- package/tests/examples/example02/example02.test.ts +1 -1
- package/tests/examples/example03/example03.test.ts +1 -1
- 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:
|
|
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:
|
|
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 {
|
package/lib/ConvertContext.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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.
|
|
31
|
+
"lbrnts": "^0.0.13",
|
|
32
|
+
"manifold-3d": "^3.3.2"
|
|
32
33
|
}
|
|
33
34
|
}
|