circuit-json-to-lbrn 0.0.53 → 0.0.55
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/README.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +153 -6
- package/lib/ConvertContext.ts +2 -0
- package/lib/createSoldermaskCureLayer.ts +160 -0
- package/lib/index.ts +31 -5
- package/lib/layer-indexes.ts +1 -0
- package/package.json +2 -2
- package/site/main.tsx +2 -7
- package/tests/basics/laser-profile.test.ts +7 -7
- package/tests/basics/soldermask-cure/__snapshots__/soldermask-cure-with-solderMaskMarginPercent.snap.svg +8 -0
- package/tests/basics/soldermask-cure/__snapshots__/soldermask-cure.snap.svg +8 -0
- package/tests/basics/soldermask-cure/soldermask-cure-with-solderMaskMarginPercent.test.ts +30 -0
- package/tests/basics/soldermask-cure/soldermask-cure.test.ts +30 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ const defaultLbrn = convertCircuitJsonToLbrn(circuitJson)
|
|
|
38
38
|
- `margin?: number` - Set the margin around the PCB
|
|
39
39
|
- `traceMargin?: number` - Clearance margin around traces in mm (requires `includeCopper: true`)
|
|
40
40
|
- `laserSpotSize?: number` - Laser spot size in mm for crosshatch spacing (default: `0.005`)
|
|
41
|
-
- `laserProfile?: { copper?: { speed?: number; numPasses?: number; frequency?: number; pulseWidth?: number }; board?: { speed?: number; numPasses?: number; frequency?: number; pulseWidth?: number } }` - Custom laser cut settings for copper and board operations. Defaults from GitHub issue: copper (speed: 300 mm/s, numPasses: 100, frequency: 20 kHz, pulseWidth: 1 ns), board (speed: 20 mm/s, numPasses: 100, frequency: 20 kHz, pulseWidth: 1 ns). Allows per-user customization for different lasers/lenses.
|
|
41
|
+
- `laserProfile?: { copper?: { speed?: number; numPasses?: number; frequency?: number; pulseWidth?: number }; board?: { speed?: number; numPasses?: number; frequency?: number; pulseWidth?: number } }` - Custom laser cut settings for copper and board operations. Defaults from GitHub issue: copper (speed: 300 mm/s, numPasses: 100, frequency: 20 kHz, pulseWidth: 1 ns), board (speed: 20 mm/s, numPasses: 100, frequency: 20 kHz, pulseWidth: 1 ns). Pulse width is specified in ns. Allows per-user customization for different lasers/lenses.
|
|
42
42
|
- `includeLayers?: Array<"top" | "bottom">` - Specify which layers to include (default: `["top", "bottom"]`)
|
|
43
43
|
|
|
44
44
|
## Soldermask Support
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface ConvertCircuitJsonToLbrnOptions {
|
|
|
10
10
|
margin?: number;
|
|
11
11
|
includeCopper?: boolean;
|
|
12
12
|
includeSoldermask?: boolean;
|
|
13
|
+
includeSoldermaskCure?: boolean;
|
|
13
14
|
globalCopperSoldermaskMarginAdjustment?: number;
|
|
14
15
|
solderMaskMarginPercent?: number;
|
|
15
16
|
includeLayers?: Array<"top" | "bottom">;
|
package/dist/index.js
CHANGED
|
@@ -2749,7 +2749,132 @@ var LAYER_INDEXES = {
|
|
|
2749
2749
|
topCopperCutFill: 6,
|
|
2750
2750
|
bottomCopperCutFill: 7,
|
|
2751
2751
|
topOxidationCleaning: 8,
|
|
2752
|
-
bottomOxidationCleaning: 9
|
|
2752
|
+
bottomOxidationCleaning: 9,
|
|
2753
|
+
soldermaskCure: 10
|
|
2754
|
+
};
|
|
2755
|
+
|
|
2756
|
+
// lib/createSoldermaskCureLayer.ts
|
|
2757
|
+
import { Point as Point4, Polygon as Polygon7 } from "@flatten-js/core";
|
|
2758
|
+
import { ShapeGroup as ShapeGroup3, ShapePath as ShapePath30 } from "lbrnts";
|
|
2759
|
+
var contourToPolygon3 = (contour) => {
|
|
2760
|
+
if (contour.length < 3) return null;
|
|
2761
|
+
const points = contour.map(([x, y]) => new Point4(x, y));
|
|
2762
|
+
try {
|
|
2763
|
+
const polygon = new Polygon7(points);
|
|
2764
|
+
if (polygon.faces.size > 0) {
|
|
2765
|
+
return polygon;
|
|
2766
|
+
}
|
|
2767
|
+
} catch {
|
|
2768
|
+
}
|
|
2769
|
+
return null;
|
|
2770
|
+
};
|
|
2771
|
+
var shapePathToContour = (shapePath) => {
|
|
2772
|
+
if (!shapePath.verts || shapePath.verts.length < 3) return null;
|
|
2773
|
+
const contour = shapePath.verts.map(
|
|
2774
|
+
(vert) => [vert.x, vert.y]
|
|
2775
|
+
);
|
|
2776
|
+
if (contour.length > 3) {
|
|
2777
|
+
const first = contour[0];
|
|
2778
|
+
const last = contour[contour.length - 1];
|
|
2779
|
+
if (first && last && first[0] === last[0] && first[1] === last[1]) {
|
|
2780
|
+
contour.pop();
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return contour.length >= 3 ? contour : null;
|
|
2784
|
+
};
|
|
2785
|
+
var collectSoldermaskContours = (project, soldermaskCutIndex) => {
|
|
2786
|
+
const contours = [];
|
|
2787
|
+
for (const child of project.children) {
|
|
2788
|
+
if (child instanceof ShapePath30) {
|
|
2789
|
+
if (child.cutIndex !== soldermaskCutIndex) {
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
const contour = shapePathToContour(child);
|
|
2793
|
+
if (contour) {
|
|
2794
|
+
contours.push(contour);
|
|
2795
|
+
}
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
if (child instanceof ShapeGroup3) {
|
|
2799
|
+
for (const groupChild of child.children) {
|
|
2800
|
+
if (groupChild instanceof ShapePath30) {
|
|
2801
|
+
if (groupChild.cutIndex !== soldermaskCutIndex) {
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const contour = shapePathToContour(groupChild);
|
|
2805
|
+
if (contour) {
|
|
2806
|
+
contours.push(contour);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return contours;
|
|
2813
|
+
};
|
|
2814
|
+
var createSoldermaskCureLayer = async ({
|
|
2815
|
+
ctx
|
|
2816
|
+
}) => {
|
|
2817
|
+
const { project, boardOutlineContour, soldermaskCureCutSetting } = ctx;
|
|
2818
|
+
if (!soldermaskCureCutSetting) {
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
if (ctx.soldermaskCutSetting.index === void 0) {
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
const soldermaskCutIndex = ctx.soldermaskCutSetting.index;
|
|
2825
|
+
if (!boardOutlineContour || boardOutlineContour.length < 3) {
|
|
2826
|
+
console.warn(
|
|
2827
|
+
"Cannot create soldermask cure layer: no board outline available"
|
|
2828
|
+
);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
try {
|
|
2832
|
+
const manifold = await getManifold();
|
|
2833
|
+
const { CrossSection } = manifold;
|
|
2834
|
+
const allContours = collectSoldermaskContours(project, soldermaskCutIndex);
|
|
2835
|
+
if (allContours.length === 0) {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
const boardOutline = new CrossSection([boardOutlineContour], "Positive");
|
|
2839
|
+
const soldermaskOpenings = new CrossSection(allContours, "Positive");
|
|
2840
|
+
const cureArea = boardOutline.subtract(soldermaskOpenings);
|
|
2841
|
+
const simplifiedArea = cureArea.simplify(1e-3);
|
|
2842
|
+
const resultContours = simplifiedArea.toPolygons();
|
|
2843
|
+
if (resultContours.length === 0) {
|
|
2844
|
+
boardOutline.delete();
|
|
2845
|
+
soldermaskOpenings.delete();
|
|
2846
|
+
cureArea.delete();
|
|
2847
|
+
simplifiedArea.delete();
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const shapeGroup = new ShapeGroup3();
|
|
2851
|
+
for (const contour of resultContours) {
|
|
2852
|
+
const polygon = contourToPolygon3(contour);
|
|
2853
|
+
if (!polygon) continue;
|
|
2854
|
+
for (const island of polygon.splitToIslands()) {
|
|
2855
|
+
const { verts, prims } = polygonToShapePathData(island);
|
|
2856
|
+
if (verts.length > 0) {
|
|
2857
|
+
shapeGroup.children.push(
|
|
2858
|
+
new ShapePath30({
|
|
2859
|
+
cutIndex: soldermaskCureCutSetting.index,
|
|
2860
|
+
verts,
|
|
2861
|
+
prims,
|
|
2862
|
+
isClosed: true
|
|
2863
|
+
})
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
if (shapeGroup.children.length > 0) {
|
|
2869
|
+
project.children.push(shapeGroup);
|
|
2870
|
+
}
|
|
2871
|
+
boardOutline.delete();
|
|
2872
|
+
soldermaskOpenings.delete();
|
|
2873
|
+
cureArea.delete();
|
|
2874
|
+
simplifiedArea.delete();
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
console.warn("Failed to create soldermask cure layer:", error);
|
|
2877
|
+
}
|
|
2753
2878
|
};
|
|
2754
2879
|
|
|
2755
2880
|
// lib/index.ts
|
|
@@ -2764,6 +2889,8 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2764
2889
|
const laserSpotSize = options.laserSpotSize ?? 5e-3;
|
|
2765
2890
|
const includeCopper = options.includeCopper ?? true;
|
|
2766
2891
|
const includeSoldermask = options.includeSoldermask ?? false;
|
|
2892
|
+
const includeSoldermaskCure = options.includeSoldermaskCure ?? false;
|
|
2893
|
+
const shouldIncludeSoldermaskCure = includeSoldermask && includeSoldermaskCure;
|
|
2767
2894
|
const globalCopperSoldermaskMarginAdjustment = options.globalCopperSoldermaskMarginAdjustment ?? 0;
|
|
2768
2895
|
const solderMaskMarginPercent = options.solderMaskMarginPercent ?? 0;
|
|
2769
2896
|
const laserProfile = options.laserProfile;
|
|
@@ -2774,13 +2901,13 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2774
2901
|
speed: 300,
|
|
2775
2902
|
numPasses: 100,
|
|
2776
2903
|
frequency: 2e4,
|
|
2777
|
-
pulseWidth:
|
|
2904
|
+
pulseWidth: 1
|
|
2778
2905
|
};
|
|
2779
2906
|
const defaultBoardSettings = {
|
|
2780
2907
|
speed: 20,
|
|
2781
2908
|
numPasses: 100,
|
|
2782
2909
|
frequency: 2e4,
|
|
2783
|
-
pulseWidth:
|
|
2910
|
+
pulseWidth: 1
|
|
2784
2911
|
};
|
|
2785
2912
|
const copperSettings = { ...defaultCopperSettings, ...laserProfile?.copper };
|
|
2786
2913
|
const boardSettings = { ...defaultBoardSettings, ...laserProfile?.board };
|
|
@@ -2794,7 +2921,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2794
2921
|
numPasses: copperSettings.numPasses,
|
|
2795
2922
|
speed: copperSettings.speed,
|
|
2796
2923
|
frequency: copperSettings.frequency,
|
|
2797
|
-
|
|
2924
|
+
qPulseWidth: copperSettings.pulseWidth
|
|
2798
2925
|
});
|
|
2799
2926
|
project.children.push(topCopperCutSetting);
|
|
2800
2927
|
const bottomCopperCutSetting = new CutSetting({
|
|
@@ -2803,7 +2930,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2803
2930
|
numPasses: copperSettings.numPasses,
|
|
2804
2931
|
speed: copperSettings.speed,
|
|
2805
2932
|
frequency: copperSettings.frequency,
|
|
2806
|
-
|
|
2933
|
+
qPulseWidth: copperSettings.pulseWidth
|
|
2807
2934
|
});
|
|
2808
2935
|
project.children.push(bottomCopperCutSetting);
|
|
2809
2936
|
const throughBoardCutSetting = new CutSetting({
|
|
@@ -2812,7 +2939,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2812
2939
|
numPasses: boardSettings.numPasses,
|
|
2813
2940
|
speed: boardSettings.speed,
|
|
2814
2941
|
frequency: boardSettings.frequency,
|
|
2815
|
-
|
|
2942
|
+
qPulseWidth: boardSettings.pulseWidth
|
|
2816
2943
|
});
|
|
2817
2944
|
project.children.push(throughBoardCutSetting);
|
|
2818
2945
|
const soldermaskCutSetting = new CutSetting({
|
|
@@ -2827,6 +2954,21 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2827
2954
|
crossHatch: true
|
|
2828
2955
|
});
|
|
2829
2956
|
project.children.push(soldermaskCutSetting);
|
|
2957
|
+
let soldermaskCureCutSetting;
|
|
2958
|
+
if (shouldIncludeSoldermaskCure) {
|
|
2959
|
+
soldermaskCureCutSetting = new CutSetting({
|
|
2960
|
+
type: "Scan",
|
|
2961
|
+
index: LAYER_INDEXES.soldermaskCure,
|
|
2962
|
+
name: "Cut Soldermask Cure",
|
|
2963
|
+
numPasses: 1,
|
|
2964
|
+
speed: 150,
|
|
2965
|
+
scanOpt: "individual",
|
|
2966
|
+
interval: 0.18,
|
|
2967
|
+
angle: 45,
|
|
2968
|
+
crossHatch: true
|
|
2969
|
+
});
|
|
2970
|
+
project.children.push(soldermaskCureCutSetting);
|
|
2971
|
+
}
|
|
2830
2972
|
let topTraceClearanceAreaCutSetting;
|
|
2831
2973
|
let bottomTraceClearanceAreaCutSetting;
|
|
2832
2974
|
if (shouldGenerateTraceClearanceZones) {
|
|
@@ -2936,6 +3078,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2936
3078
|
bottomCopperCutSetting,
|
|
2937
3079
|
throughBoardCutSetting,
|
|
2938
3080
|
soldermaskCutSetting,
|
|
3081
|
+
soldermaskCureCutSetting,
|
|
2939
3082
|
connMap,
|
|
2940
3083
|
topCutNetGeoms: /* @__PURE__ */ new Map(),
|
|
2941
3084
|
bottomCutNetGeoms: /* @__PURE__ */ new Map(),
|
|
@@ -2944,6 +3087,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2944
3087
|
origin,
|
|
2945
3088
|
includeCopper,
|
|
2946
3089
|
includeSoldermask,
|
|
3090
|
+
includeSoldermaskCure,
|
|
2947
3091
|
globalCopperSoldermaskMarginAdjustment,
|
|
2948
3092
|
includeLayers,
|
|
2949
3093
|
traceMargin,
|
|
@@ -3040,6 +3184,9 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3040
3184
|
await createOxidationCleaningLayerForLayer({ layer: "bottom", ctx });
|
|
3041
3185
|
}
|
|
3042
3186
|
}
|
|
3187
|
+
if (shouldIncludeSoldermaskCure) {
|
|
3188
|
+
await createSoldermaskCureLayer({ ctx });
|
|
3189
|
+
}
|
|
3043
3190
|
return project;
|
|
3044
3191
|
};
|
|
3045
3192
|
export {
|
package/lib/ConvertContext.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface ConvertContext {
|
|
|
15
15
|
bottomCopperCutSetting: CutSetting
|
|
16
16
|
throughBoardCutSetting: CutSetting
|
|
17
17
|
soldermaskCutSetting: CutSetting
|
|
18
|
+
soldermaskCureCutSetting?: CutSetting
|
|
18
19
|
|
|
19
20
|
connMap: ConnectivityMap
|
|
20
21
|
|
|
@@ -33,6 +34,7 @@ export interface ConvertContext {
|
|
|
33
34
|
// Include flags
|
|
34
35
|
includeCopper: boolean
|
|
35
36
|
includeSoldermask: boolean
|
|
37
|
+
includeSoldermaskCure: boolean
|
|
36
38
|
includeLayers: Array<"top" | "bottom">
|
|
37
39
|
|
|
38
40
|
// Global copper soldermask margin adjustment (can be negative)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Point, Polygon } from "@flatten-js/core"
|
|
2
|
+
import { ShapeGroup, ShapePath } from "lbrnts"
|
|
3
|
+
import type { ConvertContext } from "./ConvertContext"
|
|
4
|
+
import { getManifold } from "./getManifold"
|
|
5
|
+
import { polygonToShapePathData } from "./polygon-to-shape-path"
|
|
6
|
+
|
|
7
|
+
type Contour = Array<[number, number]>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a single contour to a flatten-js Polygon
|
|
11
|
+
*/
|
|
12
|
+
const contourToPolygon = (contour: Contour): Polygon | null => {
|
|
13
|
+
if (contour.length < 3) return null
|
|
14
|
+
|
|
15
|
+
const points = contour.map(([x, y]) => new Point(x, y))
|
|
16
|
+
try {
|
|
17
|
+
const polygon = new Polygon(points)
|
|
18
|
+
if (polygon.faces.size > 0) {
|
|
19
|
+
return polygon
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Skip invalid polygons
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const shapePathToContour = (shapePath: ShapePath): Contour | null => {
|
|
28
|
+
if (!shapePath.verts || shapePath.verts.length < 3) return null
|
|
29
|
+
const contour = shapePath.verts.map(
|
|
30
|
+
(vert) => [vert.x, vert.y] as [number, number],
|
|
31
|
+
)
|
|
32
|
+
if (contour.length > 3) {
|
|
33
|
+
const first = contour[0]
|
|
34
|
+
const last = contour[contour.length - 1]
|
|
35
|
+
if (first && last && first[0] === last[0] && first[1] === last[1]) {
|
|
36
|
+
contour.pop()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return contour.length >= 3 ? contour : null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const collectSoldermaskContours = (
|
|
43
|
+
project: ConvertContext["project"],
|
|
44
|
+
soldermaskCutIndex: number,
|
|
45
|
+
) => {
|
|
46
|
+
const contours: Contour[] = []
|
|
47
|
+
|
|
48
|
+
for (const child of project.children) {
|
|
49
|
+
if (child instanceof ShapePath) {
|
|
50
|
+
if (child.cutIndex !== soldermaskCutIndex) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const contour = shapePathToContour(child)
|
|
54
|
+
if (contour) {
|
|
55
|
+
contours.push(contour)
|
|
56
|
+
}
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (child instanceof ShapeGroup) {
|
|
61
|
+
for (const groupChild of child.children) {
|
|
62
|
+
if (groupChild instanceof ShapePath) {
|
|
63
|
+
if (groupChild.cutIndex !== soldermaskCutIndex) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
const contour = shapePathToContour(groupChild)
|
|
67
|
+
if (contour) {
|
|
68
|
+
contours.push(contour)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return contours
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a soldermask cure layer by subtracting soldermask openings from the board outline.
|
|
80
|
+
*/
|
|
81
|
+
export const createSoldermaskCureLayer = async ({
|
|
82
|
+
ctx,
|
|
83
|
+
}: {
|
|
84
|
+
ctx: ConvertContext
|
|
85
|
+
}): Promise<void> => {
|
|
86
|
+
const { project, boardOutlineContour, soldermaskCureCutSetting } = ctx
|
|
87
|
+
|
|
88
|
+
if (!soldermaskCureCutSetting) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (ctx.soldermaskCutSetting.index === undefined) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const soldermaskCutIndex = ctx.soldermaskCutSetting.index
|
|
96
|
+
|
|
97
|
+
if (!boardOutlineContour || boardOutlineContour.length < 3) {
|
|
98
|
+
console.warn(
|
|
99
|
+
"Cannot create soldermask cure layer: no board outline available",
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const manifold = await getManifold()
|
|
106
|
+
const { CrossSection } = manifold
|
|
107
|
+
|
|
108
|
+
const allContours = collectSoldermaskContours(project, soldermaskCutIndex)
|
|
109
|
+
if (allContours.length === 0) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const boardOutline = new CrossSection([boardOutlineContour], "Positive")
|
|
114
|
+
const soldermaskOpenings = new CrossSection(allContours, "Positive")
|
|
115
|
+
const cureArea = boardOutline.subtract(soldermaskOpenings)
|
|
116
|
+
const simplifiedArea = cureArea.simplify(0.001)
|
|
117
|
+
const resultContours: Contour[] = simplifiedArea.toPolygons()
|
|
118
|
+
|
|
119
|
+
if (resultContours.length === 0) {
|
|
120
|
+
boardOutline.delete()
|
|
121
|
+
soldermaskOpenings.delete()
|
|
122
|
+
cureArea.delete()
|
|
123
|
+
simplifiedArea.delete()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const shapeGroup = new ShapeGroup()
|
|
128
|
+
|
|
129
|
+
for (const contour of resultContours) {
|
|
130
|
+
const polygon = contourToPolygon(contour)
|
|
131
|
+
if (!polygon) continue
|
|
132
|
+
|
|
133
|
+
for (const island of polygon.splitToIslands()) {
|
|
134
|
+
const { verts, prims } = polygonToShapePathData(island)
|
|
135
|
+
|
|
136
|
+
if (verts.length > 0) {
|
|
137
|
+
shapeGroup.children.push(
|
|
138
|
+
new ShapePath({
|
|
139
|
+
cutIndex: soldermaskCureCutSetting.index,
|
|
140
|
+
verts,
|
|
141
|
+
prims,
|
|
142
|
+
isClosed: true,
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (shapeGroup.children.length > 0) {
|
|
150
|
+
project.children.push(shapeGroup)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
boardOutline.delete()
|
|
154
|
+
soldermaskOpenings.delete()
|
|
155
|
+
cureArea.delete()
|
|
156
|
+
simplifiedArea.delete()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn("Failed to create soldermask cure layer:", error)
|
|
159
|
+
}
|
|
160
|
+
}
|
package/lib/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { createTraceClearanceAreasForLayer } from "./createTraceClearanceAreasFo
|
|
|
19
19
|
import { createCopperCutFillForLayer } from "./createCopperCutFillForLayer"
|
|
20
20
|
import { createOxidationCleaningLayerForLayer } from "./createOxidationCleaningLayerForLayer"
|
|
21
21
|
import { LAYER_INDEXES } from "./layer-indexes"
|
|
22
|
+
import { createSoldermaskCureLayer } from "./createSoldermaskCureLayer"
|
|
22
23
|
|
|
23
24
|
export interface ConvertCircuitJsonToLbrnOptions {
|
|
24
25
|
includeSilkscreen?: boolean
|
|
@@ -26,6 +27,7 @@ export interface ConvertCircuitJsonToLbrnOptions {
|
|
|
26
27
|
margin?: number
|
|
27
28
|
includeCopper?: boolean
|
|
28
29
|
includeSoldermask?: boolean
|
|
30
|
+
includeSoldermaskCure?: boolean
|
|
29
31
|
globalCopperSoldermaskMarginAdjustment?: number
|
|
30
32
|
solderMaskMarginPercent?: number
|
|
31
33
|
includeLayers?: Array<"top" | "bottom">
|
|
@@ -79,6 +81,8 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
79
81
|
const laserSpotSize = options.laserSpotSize ?? 0.005
|
|
80
82
|
const includeCopper = options.includeCopper ?? true
|
|
81
83
|
const includeSoldermask = options.includeSoldermask ?? false
|
|
84
|
+
const includeSoldermaskCure = options.includeSoldermaskCure ?? false
|
|
85
|
+
const shouldIncludeSoldermaskCure = includeSoldermask && includeSoldermaskCure
|
|
82
86
|
const globalCopperSoldermaskMarginAdjustment =
|
|
83
87
|
options.globalCopperSoldermaskMarginAdjustment ?? 0
|
|
84
88
|
const solderMaskMarginPercent = options.solderMaskMarginPercent ?? 0
|
|
@@ -93,13 +97,13 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
93
97
|
speed: 300,
|
|
94
98
|
numPasses: 100,
|
|
95
99
|
frequency: 20000,
|
|
96
|
-
pulseWidth:
|
|
100
|
+
pulseWidth: 1,
|
|
97
101
|
}
|
|
98
102
|
const defaultBoardSettings = {
|
|
99
103
|
speed: 20,
|
|
100
104
|
numPasses: 100,
|
|
101
105
|
frequency: 20000,
|
|
102
|
-
pulseWidth:
|
|
106
|
+
pulseWidth: 1,
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
// Merge user settings with defaults
|
|
@@ -121,7 +125,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
121
125
|
numPasses: copperSettings.numPasses,
|
|
122
126
|
speed: copperSettings.speed,
|
|
123
127
|
frequency: copperSettings.frequency,
|
|
124
|
-
|
|
128
|
+
qPulseWidth: copperSettings.pulseWidth,
|
|
125
129
|
})
|
|
126
130
|
project.children.push(topCopperCutSetting)
|
|
127
131
|
|
|
@@ -131,7 +135,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
131
135
|
numPasses: copperSettings.numPasses,
|
|
132
136
|
speed: copperSettings.speed,
|
|
133
137
|
frequency: copperSettings.frequency,
|
|
134
|
-
|
|
138
|
+
qPulseWidth: copperSettings.pulseWidth,
|
|
135
139
|
})
|
|
136
140
|
project.children.push(bottomCopperCutSetting)
|
|
137
141
|
|
|
@@ -141,7 +145,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
141
145
|
numPasses: boardSettings.numPasses,
|
|
142
146
|
speed: boardSettings.speed,
|
|
143
147
|
frequency: boardSettings.frequency,
|
|
144
|
-
|
|
148
|
+
qPulseWidth: boardSettings.pulseWidth,
|
|
145
149
|
})
|
|
146
150
|
project.children.push(throughBoardCutSetting)
|
|
147
151
|
|
|
@@ -158,6 +162,22 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
158
162
|
})
|
|
159
163
|
project.children.push(soldermaskCutSetting)
|
|
160
164
|
|
|
165
|
+
let soldermaskCureCutSetting: CutSetting | undefined
|
|
166
|
+
if (shouldIncludeSoldermaskCure) {
|
|
167
|
+
soldermaskCureCutSetting = new CutSetting({
|
|
168
|
+
type: "Scan",
|
|
169
|
+
index: LAYER_INDEXES.soldermaskCure,
|
|
170
|
+
name: "Cut Soldermask Cure",
|
|
171
|
+
numPasses: 1,
|
|
172
|
+
speed: 150,
|
|
173
|
+
scanOpt: "individual",
|
|
174
|
+
interval: 0.18,
|
|
175
|
+
angle: 45,
|
|
176
|
+
crossHatch: true,
|
|
177
|
+
})
|
|
178
|
+
project.children.push(soldermaskCureCutSetting)
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
// Create trace clearance cut settings if needed
|
|
162
182
|
let topTraceClearanceAreaCutSetting: CutSetting | undefined
|
|
163
183
|
let bottomTraceClearanceAreaCutSetting: CutSetting | undefined
|
|
@@ -282,6 +302,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
282
302
|
bottomCopperCutSetting,
|
|
283
303
|
throughBoardCutSetting,
|
|
284
304
|
soldermaskCutSetting,
|
|
305
|
+
soldermaskCureCutSetting,
|
|
285
306
|
connMap,
|
|
286
307
|
topCutNetGeoms: new Map(),
|
|
287
308
|
bottomCutNetGeoms: new Map(),
|
|
@@ -290,6 +311,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
290
311
|
origin,
|
|
291
312
|
includeCopper,
|
|
292
313
|
includeSoldermask,
|
|
314
|
+
includeSoldermaskCure,
|
|
293
315
|
globalCopperSoldermaskMarginAdjustment,
|
|
294
316
|
includeLayers,
|
|
295
317
|
traceMargin,
|
|
@@ -411,5 +433,9 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
411
433
|
}
|
|
412
434
|
}
|
|
413
435
|
|
|
436
|
+
if (shouldIncludeSoldermaskCure) {
|
|
437
|
+
await createSoldermaskCureLayer({ ctx })
|
|
438
|
+
}
|
|
439
|
+
|
|
414
440
|
return project
|
|
415
441
|
}
|
package/lib/layer-indexes.ts
CHANGED
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.55",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "bun run site/index.html",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"typescript": "^5"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"lbrnts": "^0.0.
|
|
31
|
+
"lbrnts": "^0.0.17",
|
|
32
32
|
"manifold-3d": "^3.3.2"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/site/main.tsx
CHANGED
|
@@ -102,11 +102,6 @@ function kHzToHz(khz: number): number {
|
|
|
102
102
|
return khz * 1000
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// Convert pulse width from ns to seconds
|
|
106
|
-
function nsToSeconds(ns: number): number {
|
|
107
|
-
return ns * 1e-9
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
// Load Omni X 6W 150x150 preset
|
|
111
106
|
function loadOmniX6W150x150Preset() {
|
|
112
107
|
copperSpeedInput.value = "300"
|
|
@@ -188,13 +183,13 @@ function getConversionOptions() {
|
|
|
188
183
|
speed: parseFloat(copperSpeedInput.value) || 300,
|
|
189
184
|
numPasses: parseInt(copperNumPassesInput.value) || 100,
|
|
190
185
|
frequency: kHzToHz(parseFloat(copperFrequencyInput.value) || 20),
|
|
191
|
-
pulseWidth:
|
|
186
|
+
pulseWidth: parseFloat(copperPulseWidthInput.value) || 1,
|
|
192
187
|
},
|
|
193
188
|
board: {
|
|
194
189
|
speed: parseFloat(boardSpeedInput.value) || 20,
|
|
195
190
|
numPasses: parseInt(boardNumPassesInput.value) || 100,
|
|
196
191
|
frequency: kHzToHz(parseFloat(boardFrequencyInput.value) || 20),
|
|
197
|
-
pulseWidth:
|
|
192
|
+
pulseWidth: parseFloat(boardPulseWidthInput.value) || 1,
|
|
198
193
|
},
|
|
199
194
|
},
|
|
200
195
|
}
|
|
@@ -27,13 +27,13 @@ test("applies custom laserProfile settings", async () => {
|
|
|
27
27
|
speed: 350,
|
|
28
28
|
numPasses: 150,
|
|
29
29
|
frequency: 25000,
|
|
30
|
-
pulseWidth:
|
|
30
|
+
pulseWidth: 2,
|
|
31
31
|
},
|
|
32
32
|
board: {
|
|
33
33
|
speed: 25,
|
|
34
34
|
numPasses: 120,
|
|
35
35
|
frequency: 21000,
|
|
36
|
-
pulseWidth: 1.
|
|
36
|
+
pulseWidth: 1.5,
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
})
|
|
@@ -49,21 +49,21 @@ test("applies custom laserProfile settings", async () => {
|
|
|
49
49
|
expect(topCopper.speed).toBe(350)
|
|
50
50
|
expect(topCopper.numPasses).toBe(150)
|
|
51
51
|
expect(topCopper.frequency).toBe(25000)
|
|
52
|
-
expect(topCopper.
|
|
52
|
+
expect(topCopper.qPulseWidth).toBe(2)
|
|
53
53
|
|
|
54
54
|
// Verify bottom copper settings (same as top)
|
|
55
55
|
const bottomCopper = cutSettings[1]!
|
|
56
56
|
expect(bottomCopper.speed).toBe(350)
|
|
57
57
|
expect(bottomCopper.numPasses).toBe(150)
|
|
58
58
|
expect(bottomCopper.frequency).toBe(25000)
|
|
59
|
-
expect(bottomCopper.
|
|
59
|
+
expect(bottomCopper.qPulseWidth).toBe(2)
|
|
60
60
|
|
|
61
61
|
// Verify through board settings
|
|
62
62
|
const throughBoard = cutSettings[2]!
|
|
63
63
|
expect(throughBoard.speed).toBe(25)
|
|
64
64
|
expect(throughBoard.numPasses).toBe(120)
|
|
65
65
|
expect(throughBoard.frequency).toBe(21000)
|
|
66
|
-
expect(throughBoard.
|
|
66
|
+
expect(throughBoard.qPulseWidth).toBe(1.5)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
test("uses default laserProfile settings when not provided", async () => {
|
|
@@ -80,12 +80,12 @@ test("uses default laserProfile settings when not provided", async () => {
|
|
|
80
80
|
expect(topCopper.speed).toBe(300)
|
|
81
81
|
expect(topCopper.numPasses).toBe(100)
|
|
82
82
|
expect(topCopper.frequency).toBe(20000)
|
|
83
|
-
expect(topCopper.
|
|
83
|
+
expect(topCopper.qPulseWidth).toBe(1)
|
|
84
84
|
|
|
85
85
|
// Verify through board defaults
|
|
86
86
|
const throughBoard = cutSettings[2]!
|
|
87
87
|
expect(throughBoard.speed).toBe(20)
|
|
88
88
|
expect(throughBoard.numPasses).toBe(100)
|
|
89
89
|
expect(throughBoard.frequency).toBe(20000)
|
|
90
|
-
expect(throughBoard.
|
|
90
|
+
expect(throughBoard.qPulseWidth).toBe(1)
|
|
91
91
|
})
|