circuit-json-to-lbrn 0.0.42 → 0.0.44
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 +238 -15
- package/lib/ConvertContext.ts +12 -0
- package/lib/createCopperCutFillForLayer.ts +217 -0
- package/lib/element-handlers/addPcbTrace/index.ts +35 -13
- package/lib/getManifold.ts +31 -0
- package/lib/index.ts +72 -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/tests/examples/example05/__snapshots__/example05.snap.svg +1 -0
- package/tests/examples/example05/example05.circuit.json +9562 -0
- package/tests/examples/example05/example05.test.ts +31 -0
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
|
@@ -1728,6 +1728,9 @@ var createCirclePolygon = (center, radius, numPoints = 16) => {
|
|
|
1728
1728
|
}
|
|
1729
1729
|
return new Flatten4.Polygon(points);
|
|
1730
1730
|
};
|
|
1731
|
+
var positionKey = (x, y) => {
|
|
1732
|
+
return `${x.toFixed(6)},${y.toFixed(6)}`;
|
|
1733
|
+
};
|
|
1731
1734
|
var addPcbTrace = (trace, ctx) => {
|
|
1732
1735
|
const {
|
|
1733
1736
|
topCutNetGeoms,
|
|
@@ -1738,7 +1741,9 @@ var addPcbTrace = (trace, ctx) => {
|
|
|
1738
1741
|
origin,
|
|
1739
1742
|
includeCopper,
|
|
1740
1743
|
includeLayers,
|
|
1741
|
-
traceMargin
|
|
1744
|
+
traceMargin,
|
|
1745
|
+
topTraceEndpoints,
|
|
1746
|
+
bottomTraceEndpoints
|
|
1742
1747
|
} = ctx;
|
|
1743
1748
|
if (!includeCopper) {
|
|
1744
1749
|
return;
|
|
@@ -1779,22 +1784,32 @@ var addPcbTrace = (trace, ctx) => {
|
|
|
1779
1784
|
cutNetGeoms.get(netId)?.push(result);
|
|
1780
1785
|
}
|
|
1781
1786
|
const segments = layerSegments.get(layer);
|
|
1787
|
+
const endpointSet = layer === "top" ? topTraceEndpoints : bottomTraceEndpoints;
|
|
1782
1788
|
if (segments) {
|
|
1783
1789
|
for (const segment of segments) {
|
|
1784
1790
|
if (segment.length >= 2) {
|
|
1785
1791
|
const firstPoint = segment[0];
|
|
1786
1792
|
const lastPoint = segment[segment.length - 1];
|
|
1787
1793
|
const radius = traceWidth / 2 * 1.1;
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
)
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1794
|
+
const startX = firstPoint.x + origin.x;
|
|
1795
|
+
const startY = firstPoint.y + origin.y;
|
|
1796
|
+
const startKey = positionKey(startX, startY);
|
|
1797
|
+
if (!endpointSet.has(startKey)) {
|
|
1798
|
+
endpointSet.add(startKey);
|
|
1799
|
+
const startCircle = createCirclePolygon(
|
|
1800
|
+
{ x: startX, y: startY },
|
|
1801
|
+
radius
|
|
1802
|
+
);
|
|
1803
|
+
cutNetGeoms.get(netId)?.push(startCircle);
|
|
1804
|
+
}
|
|
1805
|
+
const endX = lastPoint.x + origin.x;
|
|
1806
|
+
const endY = lastPoint.y + origin.y;
|
|
1807
|
+
const endKey = positionKey(endX, endY);
|
|
1808
|
+
if (!endpointSet.has(endKey)) {
|
|
1809
|
+
endpointSet.add(endKey);
|
|
1810
|
+
const endCircle = createCirclePolygon({ x: endX, y: endY }, radius);
|
|
1811
|
+
cutNetGeoms.get(netId)?.push(endCircle);
|
|
1812
|
+
}
|
|
1798
1813
|
}
|
|
1799
1814
|
}
|
|
1800
1815
|
}
|
|
@@ -2481,8 +2496,168 @@ var createTraceClearanceAreasForLayer = ({
|
|
|
2481
2496
|
}
|
|
2482
2497
|
};
|
|
2483
2498
|
|
|
2499
|
+
// lib/createCopperCutFillForLayer.ts
|
|
2500
|
+
import { Polygon as Polygon5, Box as Box4, Point as Point2 } from "@flatten-js/core";
|
|
2501
|
+
import { ShapePath as ShapePath28, ShapeGroup } from "lbrnts";
|
|
2502
|
+
|
|
2503
|
+
// lib/getManifold.ts
|
|
2504
|
+
var manifoldInstance = null;
|
|
2505
|
+
var getManifold = async () => {
|
|
2506
|
+
if (!manifoldInstance) {
|
|
2507
|
+
let ManifoldModule;
|
|
2508
|
+
try {
|
|
2509
|
+
const moduleName = "manifold-3d";
|
|
2510
|
+
ManifoldModule = (await import(
|
|
2511
|
+
/* @vite-ignore */
|
|
2512
|
+
moduleName
|
|
2513
|
+
)).default;
|
|
2514
|
+
} catch {
|
|
2515
|
+
try {
|
|
2516
|
+
const cdnUrl = "https://cdn.jsdelivr.net/npm/manifold-3d@3.0.0/manifold.js";
|
|
2517
|
+
ManifoldModule = (await import(
|
|
2518
|
+
/* @vite-ignore */
|
|
2519
|
+
cdnUrl
|
|
2520
|
+
)).default;
|
|
2521
|
+
} catch (cdnError) {
|
|
2522
|
+
throw new Error(
|
|
2523
|
+
`Failed to load manifold-3d: not available in Node.js or via CDN. ${cdnError}`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
manifoldInstance = await ManifoldModule();
|
|
2528
|
+
manifoldInstance.setup();
|
|
2529
|
+
}
|
|
2530
|
+
return manifoldInstance;
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// lib/createCopperCutFillForLayer.ts
|
|
2534
|
+
var polygonToContours = (polygon) => {
|
|
2535
|
+
const contours = [];
|
|
2536
|
+
if (polygon instanceof Box4) {
|
|
2537
|
+
const { xmin, ymin, xmax, ymax } = polygon;
|
|
2538
|
+
contours.push([
|
|
2539
|
+
[xmin, ymin],
|
|
2540
|
+
[xmax, ymin],
|
|
2541
|
+
[xmax, ymax],
|
|
2542
|
+
[xmin, ymax]
|
|
2543
|
+
]);
|
|
2544
|
+
} else {
|
|
2545
|
+
for (const face of polygon.faces) {
|
|
2546
|
+
const contour = [];
|
|
2547
|
+
for (const edge of face) {
|
|
2548
|
+
contour.push([edge.start.x, edge.start.y]);
|
|
2549
|
+
}
|
|
2550
|
+
if (contour.length >= 3) {
|
|
2551
|
+
contours.push(contour);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return contours;
|
|
2556
|
+
};
|
|
2557
|
+
var contourToPolygon = (contour) => {
|
|
2558
|
+
if (contour.length < 3) return null;
|
|
2559
|
+
const points = contour.map(([x, y]) => new Point2(x, y));
|
|
2560
|
+
try {
|
|
2561
|
+
const polygon = new Polygon5(points);
|
|
2562
|
+
if (polygon.faces.size > 0) {
|
|
2563
|
+
return polygon;
|
|
2564
|
+
}
|
|
2565
|
+
} catch {
|
|
2566
|
+
}
|
|
2567
|
+
return null;
|
|
2568
|
+
};
|
|
2569
|
+
var createCopperCutFillForLayer = async ({
|
|
2570
|
+
layer,
|
|
2571
|
+
ctx
|
|
2572
|
+
}) => {
|
|
2573
|
+
const {
|
|
2574
|
+
project,
|
|
2575
|
+
connMap,
|
|
2576
|
+
topCutNetGeoms,
|
|
2577
|
+
bottomCutNetGeoms,
|
|
2578
|
+
topCopperCutFillCutSetting,
|
|
2579
|
+
bottomCopperCutFillCutSetting,
|
|
2580
|
+
copperCutFillMargin
|
|
2581
|
+
} = ctx;
|
|
2582
|
+
const cutSetting = layer === "top" ? topCopperCutFillCutSetting : bottomCopperCutFillCutSetting;
|
|
2583
|
+
if (!cutSetting) {
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
const netGeomMap = layer === "top" ? topCutNetGeoms : bottomCutNetGeoms;
|
|
2587
|
+
const allGeoms = [];
|
|
2588
|
+
for (const net of Object.keys(connMap.netMap)) {
|
|
2589
|
+
const netGeoms = netGeomMap.get(net);
|
|
2590
|
+
if (netGeoms && netGeoms.length > 0) {
|
|
2591
|
+
allGeoms.push(...netGeoms);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
if (allGeoms.length === 0) {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
try {
|
|
2598
|
+
const manifold = await getManifold();
|
|
2599
|
+
const { CrossSection } = manifold;
|
|
2600
|
+
const allContours = [];
|
|
2601
|
+
for (const geom of allGeoms) {
|
|
2602
|
+
const contours = polygonToContours(geom);
|
|
2603
|
+
allContours.push(...contours);
|
|
2604
|
+
}
|
|
2605
|
+
if (allContours.length === 0) {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const copperInside = new CrossSection(allContours, "Positive");
|
|
2609
|
+
const outerBoundary = copperInside.offset(
|
|
2610
|
+
copperCutFillMargin,
|
|
2611
|
+
"Round",
|
|
2612
|
+
// joinType for smooth corners
|
|
2613
|
+
2,
|
|
2614
|
+
// miterLimit
|
|
2615
|
+
32
|
|
2616
|
+
// circularSegments for round corners
|
|
2617
|
+
);
|
|
2618
|
+
const cutFillArea = outerBoundary.subtract(copperInside);
|
|
2619
|
+
const simplifiedArea = cutFillArea.simplify(1e-3);
|
|
2620
|
+
const resultContours = simplifiedArea.toPolygons();
|
|
2621
|
+
if (resultContours.length === 0) {
|
|
2622
|
+
copperInside.delete();
|
|
2623
|
+
outerBoundary.delete();
|
|
2624
|
+
cutFillArea.delete();
|
|
2625
|
+
simplifiedArea.delete();
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
const shapeGroup = new ShapeGroup();
|
|
2629
|
+
for (const contour of resultContours) {
|
|
2630
|
+
const polygon = contourToPolygon(contour);
|
|
2631
|
+
if (!polygon) continue;
|
|
2632
|
+
for (const island of polygon.splitToIslands()) {
|
|
2633
|
+
const { verts, prims } = polygonToShapePathData(island);
|
|
2634
|
+
if (verts.length > 0) {
|
|
2635
|
+
shapeGroup.children.push(
|
|
2636
|
+
new ShapePath28({
|
|
2637
|
+
cutIndex: cutSetting.index,
|
|
2638
|
+
verts,
|
|
2639
|
+
prims,
|
|
2640
|
+
isClosed: true
|
|
2641
|
+
// Filled shapes should be closed
|
|
2642
|
+
})
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
if (shapeGroup.children.length > 0) {
|
|
2648
|
+
project.children.push(shapeGroup);
|
|
2649
|
+
}
|
|
2650
|
+
copperInside.delete();
|
|
2651
|
+
outerBoundary.delete();
|
|
2652
|
+
cutFillArea.delete();
|
|
2653
|
+
simplifiedArea.delete();
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
console.warn(`Failed to create copper cut fill for ${layer} layer:`, error);
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2484
2659
|
// lib/index.ts
|
|
2485
|
-
var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
2660
|
+
var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
2486
2661
|
const db = cju2(circuitJson);
|
|
2487
2662
|
const project = new LightBurnProject({
|
|
2488
2663
|
appVersion: "1.7.03",
|
|
@@ -2496,6 +2671,8 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2496
2671
|
const globalCopperSoldermaskMarginAdjustment = options.globalCopperSoldermaskMarginAdjustment ?? 0;
|
|
2497
2672
|
const solderMaskMarginPercent = options.solderMaskMarginPercent ?? 0;
|
|
2498
2673
|
const laserProfile = options.laserProfile;
|
|
2674
|
+
const includeCopperCutFill = options.includeCopperCutFill ?? false;
|
|
2675
|
+
const copperCutFillMargin = options.copperCutFillMargin ?? 0.5;
|
|
2499
2676
|
const defaultCopperSettings = {
|
|
2500
2677
|
speed: 300,
|
|
2501
2678
|
numPasses: 100,
|
|
@@ -2553,13 +2730,14 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2553
2730
|
crossHatch: true
|
|
2554
2731
|
});
|
|
2555
2732
|
project.children.push(soldermaskCutSetting);
|
|
2733
|
+
let nextCutIndex = 4;
|
|
2556
2734
|
let topTraceClearanceAreaCutSetting;
|
|
2557
2735
|
let bottomTraceClearanceAreaCutSetting;
|
|
2558
2736
|
if (shouldGenerateTraceClearanceZones) {
|
|
2559
2737
|
if (includeLayers.includes("top")) {
|
|
2560
2738
|
topTraceClearanceAreaCutSetting = new CutSetting({
|
|
2561
2739
|
type: "Scan",
|
|
2562
|
-
index:
|
|
2740
|
+
index: nextCutIndex++,
|
|
2563
2741
|
name: "Clear Top Trace Clearance Areas",
|
|
2564
2742
|
numPasses: 12,
|
|
2565
2743
|
speed: 100,
|
|
@@ -2573,7 +2751,7 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2573
2751
|
if (includeLayers.includes("bottom")) {
|
|
2574
2752
|
bottomTraceClearanceAreaCutSetting = new CutSetting({
|
|
2575
2753
|
type: "Scan",
|
|
2576
|
-
index:
|
|
2754
|
+
index: nextCutIndex++,
|
|
2577
2755
|
name: "Clear Bottom Trace Clearance Areas",
|
|
2578
2756
|
numPasses: 12,
|
|
2579
2757
|
speed: 100,
|
|
@@ -2585,6 +2763,38 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2585
2763
|
project.children.push(bottomTraceClearanceAreaCutSetting);
|
|
2586
2764
|
}
|
|
2587
2765
|
}
|
|
2766
|
+
let topCopperCutFillCutSetting;
|
|
2767
|
+
let bottomCopperCutFillCutSetting;
|
|
2768
|
+
if (includeCopperCutFill && includeCopper) {
|
|
2769
|
+
if (includeLayers.includes("top")) {
|
|
2770
|
+
topCopperCutFillCutSetting = new CutSetting({
|
|
2771
|
+
type: "Scan",
|
|
2772
|
+
index: nextCutIndex++,
|
|
2773
|
+
name: "Top Copper Cut Fill",
|
|
2774
|
+
numPasses: copperSettings.numPasses,
|
|
2775
|
+
speed: copperSettings.speed,
|
|
2776
|
+
scanOpt: "individual",
|
|
2777
|
+
interval: laserSpotSize,
|
|
2778
|
+
angle: 45,
|
|
2779
|
+
crossHatch: true
|
|
2780
|
+
});
|
|
2781
|
+
project.children.push(topCopperCutFillCutSetting);
|
|
2782
|
+
}
|
|
2783
|
+
if (includeLayers.includes("bottom")) {
|
|
2784
|
+
bottomCopperCutFillCutSetting = new CutSetting({
|
|
2785
|
+
type: "Scan",
|
|
2786
|
+
index: nextCutIndex++,
|
|
2787
|
+
name: "Bottom Copper Cut Fill",
|
|
2788
|
+
numPasses: copperSettings.numPasses,
|
|
2789
|
+
speed: copperSettings.speed,
|
|
2790
|
+
scanOpt: "individual",
|
|
2791
|
+
interval: laserSpotSize,
|
|
2792
|
+
angle: 45,
|
|
2793
|
+
crossHatch: true
|
|
2794
|
+
});
|
|
2795
|
+
project.children.push(bottomCopperCutFillCutSetting);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2588
2798
|
const connMap = getFullConnectivityMapFromCircuitJson(circuitJson);
|
|
2589
2799
|
let origin = options.origin;
|
|
2590
2800
|
if (!origin) {
|
|
@@ -2612,7 +2822,12 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2612
2822
|
laserSpotSize,
|
|
2613
2823
|
topTraceClearanceAreaCutSetting,
|
|
2614
2824
|
bottomTraceClearanceAreaCutSetting,
|
|
2615
|
-
solderMaskMarginPercent
|
|
2825
|
+
solderMaskMarginPercent,
|
|
2826
|
+
topCopperCutFillCutSetting,
|
|
2827
|
+
bottomCopperCutFillCutSetting,
|
|
2828
|
+
copperCutFillMargin,
|
|
2829
|
+
topTraceEndpoints: /* @__PURE__ */ new Set(),
|
|
2830
|
+
bottomTraceEndpoints: /* @__PURE__ */ new Set()
|
|
2616
2831
|
};
|
|
2617
2832
|
for (const net of Object.keys(connMap.netMap)) {
|
|
2618
2833
|
ctx.topCutNetGeoms.set(net, []);
|
|
@@ -2657,6 +2872,14 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
|
|
|
2657
2872
|
createTraceClearanceAreasForLayer({ layer: "bottom", ctx });
|
|
2658
2873
|
}
|
|
2659
2874
|
}
|
|
2875
|
+
if (includeCopperCutFill && includeCopper) {
|
|
2876
|
+
if (includeLayers.includes("top")) {
|
|
2877
|
+
await createCopperCutFillForLayer({ layer: "top", ctx });
|
|
2878
|
+
}
|
|
2879
|
+
if (includeLayers.includes("bottom")) {
|
|
2880
|
+
await createCopperCutFillForLayer({ layer: "bottom", ctx });
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2660
2883
|
return project;
|
|
2661
2884
|
};
|
|
2662
2885
|
export {
|
package/lib/ConvertContext.ts
CHANGED
|
@@ -44,6 +44,18 @@ 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
|
|
56
|
+
|
|
57
|
+
// Track trace endpoint positions to avoid duplicate circles
|
|
58
|
+
// Key is "x,y" rounded to 6 decimal places
|
|
59
|
+
topTraceEndpoints: Set<string>
|
|
60
|
+
bottomTraceEndpoints: Set<string>
|
|
49
61
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
import { getManifold } from "./getManifold"
|
|
6
|
+
|
|
7
|
+
type Contour = Array<[number, number]>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Calculates the signed area of a contour.
|
|
11
|
+
* Positive area = counter-clockwise winding (outer boundary)
|
|
12
|
+
* Negative area = clockwise winding (hole)
|
|
13
|
+
*/
|
|
14
|
+
const getSignedArea = (contour: Contour): number => {
|
|
15
|
+
let area = 0
|
|
16
|
+
const n = contour.length
|
|
17
|
+
for (let i = 0; i < n; i++) {
|
|
18
|
+
const j = (i + 1) % n
|
|
19
|
+
area += contour[i]![0] * contour[j]![1]
|
|
20
|
+
area -= contour[j]![0] * contour[i]![1]
|
|
21
|
+
}
|
|
22
|
+
return area / 2
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts a flatten-js Polygon to an array of contours for manifold CrossSection
|
|
27
|
+
* Each contour is an array of [x, y] coordinates
|
|
28
|
+
*/
|
|
29
|
+
const polygonToContours = (polygon: Polygon | Box): Contour[] => {
|
|
30
|
+
const contours: Contour[] = []
|
|
31
|
+
|
|
32
|
+
if (polygon instanceof Box) {
|
|
33
|
+
// Convert Box to contour
|
|
34
|
+
const { xmin, ymin, xmax, ymax } = polygon
|
|
35
|
+
contours.push([
|
|
36
|
+
[xmin, ymin],
|
|
37
|
+
[xmax, ymin],
|
|
38
|
+
[xmax, ymax],
|
|
39
|
+
[xmin, ymax],
|
|
40
|
+
])
|
|
41
|
+
} else {
|
|
42
|
+
// Handle Polygon faces
|
|
43
|
+
for (const face of polygon.faces) {
|
|
44
|
+
const contour: Contour = []
|
|
45
|
+
for (const edge of face) {
|
|
46
|
+
contour.push([edge.start.x, edge.start.y])
|
|
47
|
+
}
|
|
48
|
+
if (contour.length >= 3) {
|
|
49
|
+
contours.push(contour)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return contours
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts a single contour to a flatten-js Polygon
|
|
59
|
+
*/
|
|
60
|
+
const contourToPolygon = (contour: Contour): Polygon | null => {
|
|
61
|
+
if (contour.length < 3) return null
|
|
62
|
+
|
|
63
|
+
const points = contour.map(([x, y]) => new Point(x, y))
|
|
64
|
+
try {
|
|
65
|
+
const polygon = new Polygon(points)
|
|
66
|
+
if (polygon.faces.size > 0) {
|
|
67
|
+
return polygon
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Skip invalid polygons
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates copper cut fill for a given layer using manifold3d CrossSection for offset operations.
|
|
77
|
+
*
|
|
78
|
+
* The algorithm:
|
|
79
|
+
* 1. Collect all copper geometries for the layer (traces, pads, vias, plated holes)
|
|
80
|
+
* 2. Union all copper geometries into a single CrossSection (the "inside")
|
|
81
|
+
* 3. Offset (expand) the unified copper outward by copperCutFillMargin (the "outer boundary")
|
|
82
|
+
* 4. Subtract the inside (original copper) from the outer boundary (expanded copper)
|
|
83
|
+
* 5. The result is the ring/band area around copper that needs to be lasered to remove more copper
|
|
84
|
+
*
|
|
85
|
+
* This creates a laser cut path that removes copper around traces and pads but never
|
|
86
|
+
* cuts into the traces or pads themselves.
|
|
87
|
+
*/
|
|
88
|
+
export const createCopperCutFillForLayer = async ({
|
|
89
|
+
layer,
|
|
90
|
+
ctx,
|
|
91
|
+
}: {
|
|
92
|
+
layer: "top" | "bottom"
|
|
93
|
+
ctx: ConvertContext
|
|
94
|
+
}): Promise<void> => {
|
|
95
|
+
const {
|
|
96
|
+
project,
|
|
97
|
+
connMap,
|
|
98
|
+
topCutNetGeoms,
|
|
99
|
+
bottomCutNetGeoms,
|
|
100
|
+
topCopperCutFillCutSetting,
|
|
101
|
+
bottomCopperCutFillCutSetting,
|
|
102
|
+
copperCutFillMargin,
|
|
103
|
+
} = ctx
|
|
104
|
+
|
|
105
|
+
// Get the appropriate cut setting for this layer
|
|
106
|
+
const cutSetting =
|
|
107
|
+
layer === "top" ? topCopperCutFillCutSetting : bottomCopperCutFillCutSetting
|
|
108
|
+
|
|
109
|
+
if (!cutSetting) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get the appropriate geometry map
|
|
114
|
+
const netGeomMap = layer === "top" ? topCutNetGeoms : bottomCutNetGeoms
|
|
115
|
+
|
|
116
|
+
// Collect all geometries for this layer across all nets
|
|
117
|
+
const allGeoms: Array<Polygon | Box> = []
|
|
118
|
+
for (const net of Object.keys(connMap.netMap)) {
|
|
119
|
+
const netGeoms = netGeomMap.get(net)
|
|
120
|
+
if (netGeoms && netGeoms.length > 0) {
|
|
121
|
+
allGeoms.push(...netGeoms)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (allGeoms.length === 0) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Initialize manifold
|
|
131
|
+
const manifold = await getManifold()
|
|
132
|
+
const { CrossSection } = manifold
|
|
133
|
+
|
|
134
|
+
// Collect all contours from all geometries
|
|
135
|
+
const allContours: Contour[] = []
|
|
136
|
+
for (const geom of allGeoms) {
|
|
137
|
+
const contours = polygonToContours(geom)
|
|
138
|
+
allContours.push(...contours)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (allContours.length === 0) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Create a unified CrossSection from all copper contours (the "inside")
|
|
146
|
+
// The constructor performs a boolean union with Positive fill rule
|
|
147
|
+
const copperInside = new CrossSection(allContours, "Positive")
|
|
148
|
+
|
|
149
|
+
// Offset (expand) the copper outward by the margin to get the "outer boundary"
|
|
150
|
+
// Positive delta expands outward
|
|
151
|
+
const outerBoundary = copperInside.offset(
|
|
152
|
+
copperCutFillMargin,
|
|
153
|
+
"Round", // joinType for smooth corners
|
|
154
|
+
2.0, // miterLimit
|
|
155
|
+
32, // circularSegments for round corners
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Subtract the inside (original copper) from the outer boundary (expanded copper)
|
|
159
|
+
// This gives us the ring/band area to laser cut
|
|
160
|
+
const cutFillArea = outerBoundary.subtract(copperInside)
|
|
161
|
+
|
|
162
|
+
// Simplify to clean up any spurious tiny segments
|
|
163
|
+
const simplifiedArea = cutFillArea.simplify(0.001)
|
|
164
|
+
|
|
165
|
+
// Get the resulting contours
|
|
166
|
+
const resultContours: Contour[] = simplifiedArea.toPolygons()
|
|
167
|
+
|
|
168
|
+
if (resultContours.length === 0) {
|
|
169
|
+
// Clean up WASM memory
|
|
170
|
+
copperInside.delete()
|
|
171
|
+
outerBoundary.delete()
|
|
172
|
+
cutFillArea.delete()
|
|
173
|
+
simplifiedArea.delete()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create a ShapeGroup to hold all contours
|
|
178
|
+
// LightBurn uses nonzero winding rule: CCW (positive area) = outer boundary, CW (negative area) = hole
|
|
179
|
+
// By grouping all contours together, LightBurn should properly fill only the ring area
|
|
180
|
+
const shapeGroup = new ShapeGroup()
|
|
181
|
+
|
|
182
|
+
// Convert all contours to ShapePaths and add to the group
|
|
183
|
+
// Preserve winding order: positive area (CCW) = outer boundary, negative area (CW) = hole
|
|
184
|
+
for (const contour of resultContours) {
|
|
185
|
+
const polygon = contourToPolygon(contour)
|
|
186
|
+
if (!polygon) continue
|
|
187
|
+
|
|
188
|
+
for (const island of polygon.splitToIslands()) {
|
|
189
|
+
const { verts, prims } = polygonToShapePathData(island)
|
|
190
|
+
|
|
191
|
+
if (verts.length > 0) {
|
|
192
|
+
shapeGroup.children.push(
|
|
193
|
+
new ShapePath({
|
|
194
|
+
cutIndex: cutSetting.index,
|
|
195
|
+
verts,
|
|
196
|
+
prims,
|
|
197
|
+
isClosed: true, // Filled shapes should be closed
|
|
198
|
+
}),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Add the group to the project if it has shapes
|
|
205
|
+
if (shapeGroup.children.length > 0) {
|
|
206
|
+
project.children.push(shapeGroup)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Clean up WASM memory
|
|
210
|
+
copperInside.delete()
|
|
211
|
+
outerBoundary.delete()
|
|
212
|
+
cutFillArea.delete()
|
|
213
|
+
simplifiedArea.delete()
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn(`Failed to create copper cut fill for ${layer} layer:`, error)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -27,6 +27,14 @@ const createCirclePolygon = (
|
|
|
27
27
|
return new Flatten.Polygon(points)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Creates a position key for deduplication.
|
|
32
|
+
* Rounds to 6 decimal places to handle floating point precision.
|
|
33
|
+
*/
|
|
34
|
+
const positionKey = (x: number, y: number): string => {
|
|
35
|
+
return `${x.toFixed(6)},${y.toFixed(6)}`
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
|
|
31
39
|
const {
|
|
32
40
|
topCutNetGeoms,
|
|
@@ -38,6 +46,8 @@ export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
|
|
|
38
46
|
includeCopper,
|
|
39
47
|
includeLayers,
|
|
40
48
|
traceMargin,
|
|
49
|
+
topTraceEndpoints,
|
|
50
|
+
bottomTraceEndpoints,
|
|
41
51
|
} = ctx
|
|
42
52
|
|
|
43
53
|
// Only include traces when including copper
|
|
@@ -95,7 +105,10 @@ export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
|
|
|
95
105
|
// Add circular pads at trace endpoints to ensure overlap at junctions
|
|
96
106
|
// This is crucial for boolean union to properly merge traces that share endpoints
|
|
97
107
|
// Use a slightly larger radius (1.1x) to ensure proper overlap despite floating point issues
|
|
108
|
+
// Deduplicate endpoints to avoid identical polygons that cause boolean operation failures
|
|
98
109
|
const segments = layerSegments.get(layer)
|
|
110
|
+
const endpointSet =
|
|
111
|
+
layer === "top" ? topTraceEndpoints : bottomTraceEndpoints
|
|
99
112
|
if (segments) {
|
|
100
113
|
for (const segment of segments) {
|
|
101
114
|
if (segment.length >= 2) {
|
|
@@ -103,19 +116,28 @@ export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
|
|
|
103
116
|
const lastPoint = segment[segment.length - 1]!
|
|
104
117
|
const radius = (traceWidth / 2) * 1.1 // Slightly larger for reliable overlap
|
|
105
118
|
|
|
106
|
-
// Add circle at start point (translated by origin)
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// Add circle at start point (translated by origin) if not already added
|
|
120
|
+
const startX = firstPoint.x + origin.x
|
|
121
|
+
const startY = firstPoint.y + origin.y
|
|
122
|
+
const startKey = positionKey(startX, startY)
|
|
123
|
+
if (!endpointSet.has(startKey)) {
|
|
124
|
+
endpointSet.add(startKey)
|
|
125
|
+
const startCircle = createCirclePolygon(
|
|
126
|
+
{ x: startX, y: startY },
|
|
127
|
+
radius,
|
|
128
|
+
)
|
|
129
|
+
cutNetGeoms.get(netId)?.push(startCircle)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add circle at end point (translated by origin) if not already added
|
|
133
|
+
const endX = lastPoint.x + origin.x
|
|
134
|
+
const endY = lastPoint.y + origin.y
|
|
135
|
+
const endKey = positionKey(endX, endY)
|
|
136
|
+
if (!endpointSet.has(endKey)) {
|
|
137
|
+
endpointSet.add(endKey)
|
|
138
|
+
const endCircle = createCirclePolygon({ x: endX, y: endY }, radius)
|
|
139
|
+
cutNetGeoms.get(netId)?.push(endCircle)
|
|
140
|
+
}
|
|
119
141
|
}
|
|
120
142
|
}
|
|
121
143
|
}
|