circuit-json-to-lbrn 0.0.84 → 0.0.85
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 +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +105 -3
- package/lib/ConvertContext.ts +2 -0
- package/lib/create-soldermask-ablation-outline.ts +99 -0
- package/lib/createCopperCutFillForLayer.ts +5 -5
- package/lib/index.ts +43 -0
- package/lib/layer-indexes.ts +1 -0
- package/package.json +2 -2
- package/tests/basics/soldermask-ablation.test.ts +111 -0
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ const defaultLbrn = convertCircuitJsonToLbrn(circuitJson)
|
|
|
43
43
|
- `includeLayers?: Array<"top" | "bottom">` - Specify which layers to include (default: `["top", "bottom"]`)
|
|
44
44
|
- `mirrorBottomLayer?: boolean` - Mirror bottom layer across the board X-center for flipped-board cutting (default: `false`)
|
|
45
45
|
- `includeHolePunch?: boolean` - Include "Hole Punch Top" / "Hole Punch Bottom" layers that mark hole centers for drilling (default: `true`)
|
|
46
|
+
- `includeSoldermaskAblation?: boolean` - Include a top-layer scan outline around all copper for soldermask ablation (default: `false`)
|
|
47
|
+
- `soldermaskAblationClearance?: number` - Clearance from copper to the soldermask ablation outline in mm (default: `1`)
|
|
46
48
|
- `toolingLayerIncludeRefs?: string[]` - Copy the PCB copper lands for component selectors such as `".U1"` or `".TP1"` to LightBurn's native, non-output T1 tooling layer (default: `[]`)
|
|
47
49
|
|
|
48
50
|
## Soldermask Support
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ interface ConvertCircuitJsonToLbrnOptions {
|
|
|
11
11
|
includeCopper?: boolean;
|
|
12
12
|
includeSoldermask?: boolean;
|
|
13
13
|
includeSoldermaskCure?: boolean;
|
|
14
|
+
/** Generate a top-layer outer contour around copper for soldermask ablation. */
|
|
15
|
+
includeSoldermaskAblation?: boolean;
|
|
16
|
+
/** Clearance between copper and the soldermask ablation outline, in mm. Defaults to 1. */
|
|
17
|
+
soldermaskAblationClearance?: number;
|
|
14
18
|
globalCopperSoldermaskMarginAdjustment?: number;
|
|
15
19
|
solderMaskMarginPercent?: number;
|
|
16
20
|
includeLayers?: Array<"top" | "bottom">;
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var LAYER_INDEXES = {
|
|
|
26
26
|
reflectedBottomBoardCut: 13,
|
|
27
27
|
topHolePunch: 14,
|
|
28
28
|
bottomHolePunch: 15,
|
|
29
|
+
topSoldermaskAblation: 16,
|
|
29
30
|
// Native LightBurn non-output tool layers
|
|
30
31
|
tool1: 30
|
|
31
32
|
};
|
|
@@ -116,6 +117,9 @@ var calculateOriginFromBounds = (bounds, margin) => {
|
|
|
116
117
|
return { x: originX, y: originY };
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
// lib/create-soldermask-ablation-outline.ts
|
|
121
|
+
import { ShapeGroup as ShapeGroup2 } from "lbrnts";
|
|
122
|
+
|
|
119
123
|
// lib/createCopperCutFillForLayer.ts
|
|
120
124
|
import { Box, Point, Polygon } from "@flatten-js/core";
|
|
121
125
|
import { ShapeGroup } from "lbrnts";
|
|
@@ -338,6 +342,76 @@ var createCopperCutFillForLayer = async ({
|
|
|
338
342
|
}
|
|
339
343
|
};
|
|
340
344
|
|
|
345
|
+
// lib/create-soldermask-ablation-outline.ts
|
|
346
|
+
var createSoldermaskAblationOutline = async (ctx) => {
|
|
347
|
+
const {
|
|
348
|
+
boardOutlineContour,
|
|
349
|
+
clipCopperCutFillToBoardOutline,
|
|
350
|
+
project,
|
|
351
|
+
soldermaskAblationClearance,
|
|
352
|
+
topCutNetGeoms,
|
|
353
|
+
topSoldermaskAblationCutSetting
|
|
354
|
+
} = ctx;
|
|
355
|
+
if (!topSoldermaskAblationCutSetting) return;
|
|
356
|
+
const copperContours = [];
|
|
357
|
+
for (const netGeometries of topCutNetGeoms.values()) {
|
|
358
|
+
for (const geometry of netGeometries) {
|
|
359
|
+
copperContours.push(
|
|
360
|
+
...polygonToContours(geometry).map(normalizeContourToCcw)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (copperContours.length === 0) return;
|
|
365
|
+
try {
|
|
366
|
+
const { CrossSection } = await getManifold();
|
|
367
|
+
const copperArea = new CrossSection(copperContours, "NonZero");
|
|
368
|
+
const expandedCopperArea = copperArea.offset(
|
|
369
|
+
soldermaskAblationClearance,
|
|
370
|
+
"Round",
|
|
371
|
+
2,
|
|
372
|
+
32
|
|
373
|
+
);
|
|
374
|
+
let outlineArea = expandedCopperArea;
|
|
375
|
+
if (clipCopperCutFillToBoardOutline && boardOutlineContour && boardOutlineContour.length >= 3) {
|
|
376
|
+
const boardArea = new CrossSection(
|
|
377
|
+
[normalizeContourToCcw(boardOutlineContour)],
|
|
378
|
+
"NonZero"
|
|
379
|
+
);
|
|
380
|
+
outlineArea = expandedCopperArea.intersect(boardArea);
|
|
381
|
+
expandedCopperArea.delete();
|
|
382
|
+
boardArea.delete();
|
|
383
|
+
}
|
|
384
|
+
const simplifiedOutline = outlineArea.simplify(1e-3);
|
|
385
|
+
const outerContours = simplifiedOutline.toPolygons().filter((contour) => signedArea(contour) > 0);
|
|
386
|
+
const outlineGroup = new ShapeGroup2();
|
|
387
|
+
for (const contour of outerContours) {
|
|
388
|
+
const polygon = contourToPolygon(contour);
|
|
389
|
+
if (!polygon) continue;
|
|
390
|
+
for (const island of polygon.splitToIslands()) {
|
|
391
|
+
const pathData = polygonToShapePathData(island);
|
|
392
|
+
if (pathData.verts.length === 0) continue;
|
|
393
|
+
outlineGroup.children.push(
|
|
394
|
+
createLayerShapePath({
|
|
395
|
+
cutIndex: topSoldermaskAblationCutSetting.index,
|
|
396
|
+
pathData,
|
|
397
|
+
layer: "top",
|
|
398
|
+
isClosed: true,
|
|
399
|
+
ctx
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (outlineGroup.children.length > 0) {
|
|
405
|
+
project.children.push(outlineGroup);
|
|
406
|
+
}
|
|
407
|
+
copperArea.delete();
|
|
408
|
+
outlineArea.delete();
|
|
409
|
+
simplifiedOutline.delete();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.warn("Failed to create top soldermask ablation outline:", error);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
341
415
|
// lib/createCopperShapesForLayer.ts
|
|
342
416
|
import { Box as Box2, Point as Point2, Polygon as Polygon2 } from "@flatten-js/core";
|
|
343
417
|
var signedArea2 = (contour) => {
|
|
@@ -708,7 +782,7 @@ var createOxidationCleaningLayerForLayer = async ({
|
|
|
708
782
|
|
|
709
783
|
// lib/createSoldermaskCureLayer.ts
|
|
710
784
|
import { Point as Point3, Polygon as Polygon3 } from "@flatten-js/core";
|
|
711
|
-
import { ShapeGroup as
|
|
785
|
+
import { ShapeGroup as ShapeGroup3, ShapePath as ShapePath3 } from "lbrnts";
|
|
712
786
|
var contourToPolygon3 = (contour) => {
|
|
713
787
|
if (contour.length < 3) return null;
|
|
714
788
|
const points = contour.map(([x, y]) => new Point3(x, y));
|
|
@@ -748,7 +822,7 @@ var collectSoldermaskContours = (project, soldermaskCutIndex) => {
|
|
|
748
822
|
}
|
|
749
823
|
continue;
|
|
750
824
|
}
|
|
751
|
-
if (child instanceof
|
|
825
|
+
if (child instanceof ShapeGroup3) {
|
|
752
826
|
for (const groupChild of child.children) {
|
|
753
827
|
if (groupChild instanceof ShapePath3) {
|
|
754
828
|
if (groupChild.cutIndex !== soldermaskCutIndex) {
|
|
@@ -800,7 +874,7 @@ var createSoldermaskCureLayerForLayer = async ({
|
|
|
800
874
|
simplifiedArea.delete();
|
|
801
875
|
return;
|
|
802
876
|
}
|
|
803
|
-
const shapeGroup = new
|
|
877
|
+
const shapeGroup = new ShapeGroup3();
|
|
804
878
|
for (const contour of resultContours) {
|
|
805
879
|
const polygon = contourToPolygon3(contour);
|
|
806
880
|
if (!polygon) continue;
|
|
@@ -3306,6 +3380,8 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3306
3380
|
const includeCopper = options.includeCopper ?? true;
|
|
3307
3381
|
const includeSoldermask = options.includeSoldermask ?? false;
|
|
3308
3382
|
const includeSoldermaskCure = options.includeSoldermaskCure ?? false;
|
|
3383
|
+
const includeSoldermaskAblation = options.includeSoldermaskAblation ?? false;
|
|
3384
|
+
const soldermaskAblationClearance = options.soldermaskAblationClearance ?? 1;
|
|
3309
3385
|
const includeHolePunch = options.includeHolePunch ?? true;
|
|
3310
3386
|
const shouldIncludeSoldermaskCure = includeSoldermask && includeSoldermaskCure;
|
|
3311
3387
|
const globalCopperSoldermaskMarginAdjustment = options.globalCopperSoldermaskMarginAdjustment ?? 0;
|
|
@@ -3342,6 +3418,9 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3342
3418
|
if (traceMargin > 0 && !includeCopper) {
|
|
3343
3419
|
throw new Error("traceMargin requires includeCopper to be true");
|
|
3344
3420
|
}
|
|
3421
|
+
if (soldermaskAblationClearance < 0) {
|
|
3422
|
+
throw new Error("soldermaskAblationClearance must not be negative");
|
|
3423
|
+
}
|
|
3345
3424
|
const topCopperCutSetting = new CutSetting2({
|
|
3346
3425
|
index: LAYER_INDEXES.topCopper,
|
|
3347
3426
|
name: "Cut Top Copper",
|
|
@@ -3552,6 +3631,24 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3552
3631
|
project.children.push(bottomCopperCutFillCutSetting);
|
|
3553
3632
|
}
|
|
3554
3633
|
}
|
|
3634
|
+
let topSoldermaskAblationCutSetting;
|
|
3635
|
+
if (includeSoldermaskAblation && includeCopper && includeLayers.includes("top")) {
|
|
3636
|
+
topSoldermaskAblationCutSetting = new CutSetting2({
|
|
3637
|
+
type: "Scan",
|
|
3638
|
+
index: LAYER_INDEXES.topSoldermaskAblation,
|
|
3639
|
+
name: "Top Soldermask Ablation",
|
|
3640
|
+
numPasses: 5,
|
|
3641
|
+
speed: 7e3,
|
|
3642
|
+
frequency: 4e4,
|
|
3643
|
+
scanOpt: "mergeAll",
|
|
3644
|
+
interval: 0.1,
|
|
3645
|
+
qPulseWidth: 1,
|
|
3646
|
+
crossHatch: true,
|
|
3647
|
+
wobbleEnable: true,
|
|
3648
|
+
anglePerPass: 1
|
|
3649
|
+
});
|
|
3650
|
+
project.children.push(topSoldermaskAblationCutSetting);
|
|
3651
|
+
}
|
|
3555
3652
|
let topOxidationCleaningCutSetting;
|
|
3556
3653
|
let bottomOxidationCleaningCutSetting;
|
|
3557
3654
|
if (includeOxidationCleaningLayer) {
|
|
@@ -3600,6 +3697,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3600
3697
|
topSoldermaskCureCutSetting,
|
|
3601
3698
|
bottomSoldermaskCureCutSetting,
|
|
3602
3699
|
reflectedBottomBoardCutSetting,
|
|
3700
|
+
topSoldermaskAblationCutSetting,
|
|
3603
3701
|
tool1CutSetting,
|
|
3604
3702
|
connMap,
|
|
3605
3703
|
topCutNetGeoms: /* @__PURE__ */ new Map(),
|
|
@@ -3621,6 +3719,7 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3621
3719
|
bottomCopperCutFillCutSetting,
|
|
3622
3720
|
copperCutFillMargin,
|
|
3623
3721
|
clipCopperCutFillToBoardOutline,
|
|
3722
|
+
soldermaskAblationClearance,
|
|
3624
3723
|
topOxidationCleaningCutSetting,
|
|
3625
3724
|
bottomOxidationCleaningCutSetting,
|
|
3626
3725
|
topTraceEndpoints: /* @__PURE__ */ new Set(),
|
|
@@ -3728,6 +3827,9 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
3728
3827
|
await createCopperCutFillForLayer({ layer: "bottom", ctx });
|
|
3729
3828
|
}
|
|
3730
3829
|
}
|
|
3830
|
+
if (includeSoldermaskAblation && includeCopper && includeLayers.includes("top")) {
|
|
3831
|
+
await createSoldermaskAblationOutline(ctx);
|
|
3832
|
+
}
|
|
3731
3833
|
if (includeOxidationCleaningLayer) {
|
|
3732
3834
|
if (includeLayers.includes("top")) {
|
|
3733
3835
|
await createOxidationCleaningLayerForLayer({ layer: "top", ctx });
|
package/lib/ConvertContext.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface ConvertContext {
|
|
|
24
24
|
topSoldermaskCureCutSetting?: CutSetting
|
|
25
25
|
bottomSoldermaskCureCutSetting?: CutSetting
|
|
26
26
|
reflectedBottomBoardCutSetting?: CutSetting
|
|
27
|
+
topSoldermaskAblationCutSetting?: CutSetting
|
|
27
28
|
tool1CutSetting?: CutSetting
|
|
28
29
|
|
|
29
30
|
connMap: ConnectivityMap
|
|
@@ -73,6 +74,7 @@ export interface ConvertContext {
|
|
|
73
74
|
// Copper cut fill margin (how far to expand the copper outline for the cut fill band)
|
|
74
75
|
copperCutFillMargin: number
|
|
75
76
|
clipCopperCutFillToBoardOutline: boolean
|
|
77
|
+
soldermaskAblationClearance: number
|
|
76
78
|
|
|
77
79
|
// Track trace endpoint positions to avoid duplicate circles
|
|
78
80
|
// Key is "x,y" rounded to 6 decimal places
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ShapeGroup } from "lbrnts"
|
|
2
|
+
import type { ConvertContext } from "./ConvertContext"
|
|
3
|
+
import {
|
|
4
|
+
type Contour,
|
|
5
|
+
contourToPolygon,
|
|
6
|
+
normalizeContourToCcw,
|
|
7
|
+
polygonToContours,
|
|
8
|
+
signedArea,
|
|
9
|
+
} from "./createCopperCutFillForLayer"
|
|
10
|
+
import { getManifold } from "./getManifold"
|
|
11
|
+
import { createLayerShapePath } from "./helpers/createLayerShapePath"
|
|
12
|
+
import { polygonToShapePathData } from "./polygon-to-shape-path"
|
|
13
|
+
|
|
14
|
+
export const createSoldermaskAblationOutline = async (
|
|
15
|
+
ctx: ConvertContext,
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
const {
|
|
18
|
+
boardOutlineContour,
|
|
19
|
+
clipCopperCutFillToBoardOutline,
|
|
20
|
+
project,
|
|
21
|
+
soldermaskAblationClearance,
|
|
22
|
+
topCutNetGeoms,
|
|
23
|
+
topSoldermaskAblationCutSetting,
|
|
24
|
+
} = ctx
|
|
25
|
+
|
|
26
|
+
if (!topSoldermaskAblationCutSetting) return
|
|
27
|
+
|
|
28
|
+
const copperContours: Contour[] = []
|
|
29
|
+
for (const netGeometries of topCutNetGeoms.values()) {
|
|
30
|
+
for (const geometry of netGeometries) {
|
|
31
|
+
copperContours.push(
|
|
32
|
+
...polygonToContours(geometry).map(normalizeContourToCcw),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (copperContours.length === 0) return
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { CrossSection } = await getManifold()
|
|
40
|
+
const copperArea = new CrossSection(copperContours, "NonZero")
|
|
41
|
+
const expandedCopperArea = copperArea.offset(
|
|
42
|
+
soldermaskAblationClearance,
|
|
43
|
+
"Round",
|
|
44
|
+
2,
|
|
45
|
+
32,
|
|
46
|
+
)
|
|
47
|
+
let outlineArea = expandedCopperArea
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
clipCopperCutFillToBoardOutline &&
|
|
51
|
+
boardOutlineContour &&
|
|
52
|
+
boardOutlineContour.length >= 3
|
|
53
|
+
) {
|
|
54
|
+
const boardArea = new CrossSection(
|
|
55
|
+
[normalizeContourToCcw(boardOutlineContour)],
|
|
56
|
+
"NonZero",
|
|
57
|
+
)
|
|
58
|
+
outlineArea = expandedCopperArea.intersect(boardArea)
|
|
59
|
+
expandedCopperArea.delete()
|
|
60
|
+
boardArea.delete()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const simplifiedOutline = outlineArea.simplify(0.001)
|
|
64
|
+
const outerContours = simplifiedOutline
|
|
65
|
+
.toPolygons()
|
|
66
|
+
.filter((contour: Contour) => signedArea(contour) > 0)
|
|
67
|
+
const outlineGroup = new ShapeGroup()
|
|
68
|
+
|
|
69
|
+
for (const contour of outerContours) {
|
|
70
|
+
const polygon = contourToPolygon(contour)
|
|
71
|
+
if (!polygon) continue
|
|
72
|
+
|
|
73
|
+
for (const island of polygon.splitToIslands()) {
|
|
74
|
+
const pathData = polygonToShapePathData(island)
|
|
75
|
+
if (pathData.verts.length === 0) continue
|
|
76
|
+
|
|
77
|
+
outlineGroup.children.push(
|
|
78
|
+
createLayerShapePath({
|
|
79
|
+
cutIndex: topSoldermaskAblationCutSetting.index,
|
|
80
|
+
pathData,
|
|
81
|
+
layer: "top",
|
|
82
|
+
isClosed: true,
|
|
83
|
+
ctx,
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (outlineGroup.children.length > 0) {
|
|
90
|
+
project.children.push(outlineGroup)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
copperArea.delete()
|
|
94
|
+
outlineArea.delete()
|
|
95
|
+
simplifiedOutline.delete()
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn("Failed to create top soldermask ablation outline:", error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -5,9 +5,9 @@ import { getManifold } from "./getManifold"
|
|
|
5
5
|
import { createLayerShapePath } from "./helpers/createLayerShapePath"
|
|
6
6
|
import { polygonToShapePathData } from "./polygon-to-shape-path"
|
|
7
7
|
|
|
8
|
-
type Contour = Array<[number, number]>
|
|
8
|
+
export type Contour = Array<[number, number]>
|
|
9
9
|
|
|
10
|
-
const signedArea = (contour: Contour): number => {
|
|
10
|
+
export const signedArea = (contour: Contour): number => {
|
|
11
11
|
let area = 0
|
|
12
12
|
for (let i = 0; i < contour.length; i++) {
|
|
13
13
|
const [x1, y1] = contour[i]!
|
|
@@ -17,7 +17,7 @@ const signedArea = (contour: Contour): number => {
|
|
|
17
17
|
return area / 2
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const normalizeContourToCcw = (contour: Contour): Contour =>
|
|
20
|
+
export const normalizeContourToCcw = (contour: Contour): Contour =>
|
|
21
21
|
contour.length >= 3 && signedArea(contour) < 0
|
|
22
22
|
? [...contour].reverse()
|
|
23
23
|
: contour
|
|
@@ -26,7 +26,7 @@ const normalizeContourToCcw = (contour: Contour): Contour =>
|
|
|
26
26
|
* Converts a flatten-js Polygon to an array of contours for manifold CrossSection
|
|
27
27
|
* Each contour is an array of [x, y] coordinates
|
|
28
28
|
*/
|
|
29
|
-
const polygonToContours = (polygon: Polygon | Box): Contour[] => {
|
|
29
|
+
export const polygonToContours = (polygon: Polygon | Box): Contour[] => {
|
|
30
30
|
const contours: Contour[] = []
|
|
31
31
|
|
|
32
32
|
if (polygon instanceof Box) {
|
|
@@ -57,7 +57,7 @@ const polygonToContours = (polygon: Polygon | Box): Contour[] => {
|
|
|
57
57
|
/**
|
|
58
58
|
* Converts a single contour to a flatten-js Polygon
|
|
59
59
|
*/
|
|
60
|
-
const contourToPolygon = (contour: Contour): Polygon | null => {
|
|
60
|
+
export const contourToPolygon = (contour: Contour): Polygon | null => {
|
|
61
61
|
if (contour.length < 3) return null
|
|
62
62
|
|
|
63
63
|
const points = contour.map(([x, y]) => new Point(x, y))
|
package/lib/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
calculateCircuitBounds,
|
|
9
9
|
calculateOriginFromBounds,
|
|
10
10
|
} from "./calculateBounds"
|
|
11
|
+
import { createSoldermaskAblationOutline } from "./create-soldermask-ablation-outline"
|
|
11
12
|
import { createCopperCutFillForLayer } from "./createCopperCutFillForLayer"
|
|
12
13
|
import { createCopperShapesForLayer } from "./createCopperShapesForLayer"
|
|
13
14
|
import { createHolePunchLayers } from "./createHolePunchLayers"
|
|
@@ -34,6 +35,10 @@ export interface ConvertCircuitJsonToLbrnOptions {
|
|
|
34
35
|
includeCopper?: boolean
|
|
35
36
|
includeSoldermask?: boolean
|
|
36
37
|
includeSoldermaskCure?: boolean
|
|
38
|
+
/** Generate a top-layer outer contour around copper for soldermask ablation. */
|
|
39
|
+
includeSoldermaskAblation?: boolean
|
|
40
|
+
/** Clearance between copper and the soldermask ablation outline, in mm. Defaults to 1. */
|
|
41
|
+
soldermaskAblationClearance?: number
|
|
37
42
|
globalCopperSoldermaskMarginAdjustment?: number
|
|
38
43
|
solderMaskMarginPercent?: number
|
|
39
44
|
includeLayers?: Array<"top" | "bottom">
|
|
@@ -102,6 +107,8 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
102
107
|
const includeCopper = options.includeCopper ?? true
|
|
103
108
|
const includeSoldermask = options.includeSoldermask ?? false
|
|
104
109
|
const includeSoldermaskCure = options.includeSoldermaskCure ?? false
|
|
110
|
+
const includeSoldermaskAblation = options.includeSoldermaskAblation ?? false
|
|
111
|
+
const soldermaskAblationClearance = options.soldermaskAblationClearance ?? 1
|
|
105
112
|
const includeHolePunch = options.includeHolePunch ?? true
|
|
106
113
|
const shouldIncludeSoldermaskCure = includeSoldermask && includeSoldermaskCure
|
|
107
114
|
const globalCopperSoldermaskMarginAdjustment =
|
|
@@ -153,6 +160,9 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
153
160
|
if (traceMargin > 0 && !includeCopper) {
|
|
154
161
|
throw new Error("traceMargin requires includeCopper to be true")
|
|
155
162
|
}
|
|
163
|
+
if (soldermaskAblationClearance < 0) {
|
|
164
|
+
throw new Error("soldermaskAblationClearance must not be negative")
|
|
165
|
+
}
|
|
156
166
|
|
|
157
167
|
// Create cut settings
|
|
158
168
|
const topCopperCutSetting = new CutSetting({
|
|
@@ -385,6 +395,29 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
385
395
|
}
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
let topSoldermaskAblationCutSetting: CutSetting | undefined
|
|
399
|
+
if (
|
|
400
|
+
includeSoldermaskAblation &&
|
|
401
|
+
includeCopper &&
|
|
402
|
+
includeLayers.includes("top")
|
|
403
|
+
) {
|
|
404
|
+
topSoldermaskAblationCutSetting = new CutSetting({
|
|
405
|
+
type: "Scan",
|
|
406
|
+
index: LAYER_INDEXES.topSoldermaskAblation,
|
|
407
|
+
name: "Top Soldermask Ablation",
|
|
408
|
+
numPasses: 5,
|
|
409
|
+
speed: 7000,
|
|
410
|
+
frequency: 40000,
|
|
411
|
+
scanOpt: "mergeAll",
|
|
412
|
+
interval: 0.1,
|
|
413
|
+
qPulseWidth: 1,
|
|
414
|
+
crossHatch: true,
|
|
415
|
+
wobbleEnable: true,
|
|
416
|
+
anglePerPass: 1,
|
|
417
|
+
})
|
|
418
|
+
project.children.push(topSoldermaskAblationCutSetting)
|
|
419
|
+
}
|
|
420
|
+
|
|
388
421
|
// Create oxidation cleaning layer cut settings if needed
|
|
389
422
|
let topOxidationCleaningCutSetting: CutSetting | undefined
|
|
390
423
|
let bottomOxidationCleaningCutSetting: CutSetting | undefined
|
|
@@ -441,6 +474,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
441
474
|
topSoldermaskCureCutSetting,
|
|
442
475
|
bottomSoldermaskCureCutSetting,
|
|
443
476
|
reflectedBottomBoardCutSetting,
|
|
477
|
+
topSoldermaskAblationCutSetting,
|
|
444
478
|
tool1CutSetting,
|
|
445
479
|
connMap,
|
|
446
480
|
topCutNetGeoms: new Map(),
|
|
@@ -462,6 +496,7 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
462
496
|
bottomCopperCutFillCutSetting,
|
|
463
497
|
copperCutFillMargin,
|
|
464
498
|
clipCopperCutFillToBoardOutline,
|
|
499
|
+
soldermaskAblationClearance,
|
|
465
500
|
topOxidationCleaningCutSetting,
|
|
466
501
|
bottomOxidationCleaningCutSetting,
|
|
467
502
|
topTraceEndpoints: new Set(),
|
|
@@ -605,6 +640,14 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
605
640
|
}
|
|
606
641
|
}
|
|
607
642
|
|
|
643
|
+
if (
|
|
644
|
+
includeSoldermaskAblation &&
|
|
645
|
+
includeCopper &&
|
|
646
|
+
includeLayers.includes("top")
|
|
647
|
+
) {
|
|
648
|
+
await createSoldermaskAblationOutline(ctx)
|
|
649
|
+
}
|
|
650
|
+
|
|
608
651
|
// Create oxidation cleaning layer for each layer
|
|
609
652
|
if (includeOxidationCleaningLayer) {
|
|
610
653
|
if (includeLayers.includes("top")) {
|
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.85",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "bun run site/index.html",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"typescript": "^5"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"lbrnts": "^0.0.
|
|
33
|
+
"lbrnts": "^0.0.22",
|
|
34
34
|
"manifold-3d": "^3.3.2"
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import type { CircuitJson } from "circuit-json"
|
|
3
|
+
import { CutSetting } from "lbrnts"
|
|
4
|
+
import { convertCircuitJsonToLbrn } from "../../lib"
|
|
5
|
+
import { LAYER_INDEXES } from "../../lib/layer-indexes"
|
|
6
|
+
import circuitJson from "../examples/example05/example05.circuit.json" with {
|
|
7
|
+
type: "json",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ProjectNode = {
|
|
11
|
+
children?: ProjectNode[]
|
|
12
|
+
cutIndex?: number
|
|
13
|
+
index?: number
|
|
14
|
+
name?: string
|
|
15
|
+
type?: string
|
|
16
|
+
verts?: Array<{ x: number; y: number }>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const flattenProject = (nodes: ProjectNode[]): ProjectNode[] =>
|
|
20
|
+
nodes.flatMap((node) => [node, ...flattenProject(node.children ?? [])])
|
|
21
|
+
|
|
22
|
+
const getBounds = (nodes: ProjectNode[]) => {
|
|
23
|
+
const vertices = nodes.flatMap((node) => node.verts ?? [])
|
|
24
|
+
return {
|
|
25
|
+
minX: Math.min(...vertices.map((vertex) => vertex.x)),
|
|
26
|
+
maxX: Math.max(...vertices.map((vertex) => vertex.x)),
|
|
27
|
+
minY: Math.min(...vertices.map((vertex) => vertex.y)),
|
|
28
|
+
maxY: Math.max(...vertices.map((vertex) => vertex.y)),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("creates a 1mm top soldermask ablation outer outline", async () => {
|
|
33
|
+
const project = await convertCircuitJsonToLbrn(circuitJson as CircuitJson, {
|
|
34
|
+
includeLayers: ["top"],
|
|
35
|
+
includeCopper: true,
|
|
36
|
+
includeSoldermaskAblation: true,
|
|
37
|
+
soldermaskAblationClearance: 1,
|
|
38
|
+
origin: { x: 0, y: 0 },
|
|
39
|
+
})
|
|
40
|
+
const projectNodes = flattenProject(project.children as ProjectNode[])
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
projectNodes.find(
|
|
44
|
+
(node) => node.index === LAYER_INDEXES.topSoldermaskAblation,
|
|
45
|
+
),
|
|
46
|
+
).toMatchObject({
|
|
47
|
+
name: "Top Soldermask Ablation",
|
|
48
|
+
type: "Scan",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const copperShapes = projectNodes.filter(
|
|
52
|
+
(node) => node.cutIndex === LAYER_INDEXES.topCopper,
|
|
53
|
+
)
|
|
54
|
+
const ablationOutlineShapes = projectNodes.filter(
|
|
55
|
+
(node) => node.cutIndex === LAYER_INDEXES.topSoldermaskAblation,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect(ablationOutlineShapes).toHaveLength(1)
|
|
59
|
+
|
|
60
|
+
const copperBounds = getBounds(copperShapes)
|
|
61
|
+
const outlineBounds = getBounds(ablationOutlineShapes)
|
|
62
|
+
expect(copperBounds.minX - outlineBounds.minX).toBeCloseTo(1, 1)
|
|
63
|
+
expect(outlineBounds.maxX - copperBounds.maxX).toBeCloseTo(1, 1)
|
|
64
|
+
expect(copperBounds.minY - outlineBounds.minY).toBeCloseTo(1, 1)
|
|
65
|
+
expect(outlineBounds.maxY - copperBounds.maxY).toBeCloseTo(1, 1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("uses the production soldermask ablation scan settings", async () => {
|
|
69
|
+
const project = await convertCircuitJsonToLbrn(circuitJson as CircuitJson, {
|
|
70
|
+
includeLayers: ["top"],
|
|
71
|
+
includeCopper: true,
|
|
72
|
+
includeSoldermaskAblation: true,
|
|
73
|
+
})
|
|
74
|
+
const soldermaskAblation = project.children.find(
|
|
75
|
+
(child): child is CutSetting =>
|
|
76
|
+
child instanceof CutSetting &&
|
|
77
|
+
child.index === LAYER_INDEXES.topSoldermaskAblation,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(soldermaskAblation).toMatchObject({
|
|
81
|
+
type: "Scan",
|
|
82
|
+
speed: 7000,
|
|
83
|
+
frequency: 40000,
|
|
84
|
+
qPulseWidth: 1,
|
|
85
|
+
interval: 0.1,
|
|
86
|
+
numPasses: 5,
|
|
87
|
+
scanOpt: "mergeAll",
|
|
88
|
+
crossHatch: true,
|
|
89
|
+
wobbleEnable: true,
|
|
90
|
+
anglePerPass: 1,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const xml = project.getString()
|
|
94
|
+
const soldermaskAblationXml = xml.match(
|
|
95
|
+
/<CutSetting type="Scan">\s*<index Value="16"\/>[\s\S]*?<\/CutSetting>/,
|
|
96
|
+
)?.[0]
|
|
97
|
+
|
|
98
|
+
expect(soldermaskAblationXml).toContain('<scanOpt Value="mergeAll"/>')
|
|
99
|
+
expect(soldermaskAblationXml).toContain('<crossHatch Value="1"/>')
|
|
100
|
+
expect(soldermaskAblationXml).toContain('<wobbleEnable Value="1"/>')
|
|
101
|
+
expect(soldermaskAblationXml).toContain('<anglePerPass Value="1"/>')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test("rejects a negative soldermask ablation clearance", async () => {
|
|
105
|
+
await expect(
|
|
106
|
+
convertCircuitJsonToLbrn(circuitJson as CircuitJson, {
|
|
107
|
+
includeSoldermaskAblation: true,
|
|
108
|
+
soldermaskAblationClearance: -1,
|
|
109
|
+
}),
|
|
110
|
+
).rejects.toThrow("soldermaskAblationClearance must not be negative")
|
|
111
|
+
})
|