circuit-json-to-lbrn 0.0.43 → 0.0.45
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.js +85 -19
- package/lib/ConvertContext.ts +10 -0
- package/lib/createCopperCutFillForLayer.ts +12 -17
- package/lib/element-handlers/addPcbTrace/index.ts +35 -13
- package/lib/getManifold.ts +31 -0
- package/lib/index.ts +30 -0
- package/package.json +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 +71 -0
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
|
}
|
|
@@ -2484,19 +2499,38 @@ var createTraceClearanceAreasForLayer = ({
|
|
|
2484
2499
|
// lib/createCopperCutFillForLayer.ts
|
|
2485
2500
|
import { Polygon as Polygon5, Box as Box4, Point as Point2 } from "@flatten-js/core";
|
|
2486
2501
|
import { ShapePath as ShapePath28, ShapeGroup } from "lbrnts";
|
|
2502
|
+
|
|
2503
|
+
// lib/getManifold.ts
|
|
2487
2504
|
var manifoldInstance = null;
|
|
2488
2505
|
var getManifold = async () => {
|
|
2489
2506
|
if (!manifoldInstance) {
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
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
|
+
}
|
|
2495
2527
|
manifoldInstance = await ManifoldModule();
|
|
2496
2528
|
manifoldInstance.setup();
|
|
2497
2529
|
}
|
|
2498
2530
|
return manifoldInstance;
|
|
2499
2531
|
};
|
|
2532
|
+
|
|
2533
|
+
// lib/createCopperCutFillForLayer.ts
|
|
2500
2534
|
var polygonToContours = (polygon) => {
|
|
2501
2535
|
const contours = [];
|
|
2502
2536
|
if (polygon instanceof Box4) {
|
|
@@ -2543,7 +2577,8 @@ var createCopperCutFillForLayer = async ({
|
|
|
2543
2577
|
bottomCutNetGeoms,
|
|
2544
2578
|
topCopperCutFillCutSetting,
|
|
2545
2579
|
bottomCopperCutFillCutSetting,
|
|
2546
|
-
copperCutFillMargin
|
|
2580
|
+
copperCutFillMargin,
|
|
2581
|
+
boardOutlineContour
|
|
2547
2582
|
} = ctx;
|
|
2548
2583
|
const cutSetting = layer === "top" ? topCopperCutFillCutSetting : bottomCopperCutFillCutSetting;
|
|
2549
2584
|
if (!cutSetting) {
|
|
@@ -2581,7 +2616,14 @@ var createCopperCutFillForLayer = async ({
|
|
|
2581
2616
|
32
|
|
2582
2617
|
// circularSegments for round corners
|
|
2583
2618
|
);
|
|
2584
|
-
|
|
2619
|
+
let cutFillArea = outerBoundary.subtract(copperInside);
|
|
2620
|
+
if (boardOutlineContour && boardOutlineContour.length >= 3) {
|
|
2621
|
+
const boardOutline = new CrossSection([boardOutlineContour], "Positive");
|
|
2622
|
+
const clippedArea = cutFillArea.intersect(boardOutline);
|
|
2623
|
+
cutFillArea.delete();
|
|
2624
|
+
cutFillArea = clippedArea;
|
|
2625
|
+
boardOutline.delete();
|
|
2626
|
+
}
|
|
2585
2627
|
const simplifiedArea = cutFillArea.simplify(1e-3);
|
|
2586
2628
|
const resultContours = simplifiedArea.toPolygons();
|
|
2587
2629
|
if (resultContours.length === 0) {
|
|
@@ -2791,7 +2833,9 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2791
2833
|
solderMaskMarginPercent,
|
|
2792
2834
|
topCopperCutFillCutSetting,
|
|
2793
2835
|
bottomCopperCutFillCutSetting,
|
|
2794
|
-
copperCutFillMargin
|
|
2836
|
+
copperCutFillMargin,
|
|
2837
|
+
topTraceEndpoints: /* @__PURE__ */ new Set(),
|
|
2838
|
+
bottomTraceEndpoints: /* @__PURE__ */ new Set()
|
|
2795
2839
|
};
|
|
2796
2840
|
for (const net of Object.keys(connMap.netMap)) {
|
|
2797
2841
|
ctx.topCutNetGeoms.set(net, []);
|
|
@@ -2799,6 +2843,28 @@ var convertCircuitJsonToLbrn = async (circuitJson, options = {}) => {
|
|
|
2799
2843
|
ctx.topScanNetGeoms.set(net, []);
|
|
2800
2844
|
ctx.bottomScanNetGeoms.set(net, []);
|
|
2801
2845
|
}
|
|
2846
|
+
for (const board of db.pcb_board.list()) {
|
|
2847
|
+
if (board.outline?.length) {
|
|
2848
|
+
ctx.boardOutlineContour = board.outline.map((outlinePoint) => [
|
|
2849
|
+
outlinePoint.x + origin.x,
|
|
2850
|
+
outlinePoint.y + origin.y
|
|
2851
|
+
]);
|
|
2852
|
+
} else if (typeof board.width === "number" && typeof board.height === "number" && board.center) {
|
|
2853
|
+
const halfWidth = board.width / 2;
|
|
2854
|
+
const halfHeight = board.height / 2;
|
|
2855
|
+
const minX = board.center.x - halfWidth + origin.x;
|
|
2856
|
+
const minY = board.center.y - halfHeight + origin.y;
|
|
2857
|
+
const maxX = board.center.x + halfWidth + origin.x;
|
|
2858
|
+
const maxY = board.center.y + halfHeight + origin.y;
|
|
2859
|
+
ctx.boardOutlineContour = [
|
|
2860
|
+
[minX, minY],
|
|
2861
|
+
[maxX, minY],
|
|
2862
|
+
[maxX, maxY],
|
|
2863
|
+
[minX, maxY]
|
|
2864
|
+
];
|
|
2865
|
+
}
|
|
2866
|
+
break;
|
|
2867
|
+
}
|
|
2802
2868
|
for (const smtpad of db.pcb_smtpad.list()) {
|
|
2803
2869
|
addSmtPad(smtpad, ctx);
|
|
2804
2870
|
}
|
package/lib/ConvertContext.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { CutSetting, LightBurnProject } from "lbrnts"
|
|
|
3
3
|
import type { Box, Polygon } from "@flatten-js/core"
|
|
4
4
|
import type { ConnectivityMap } from "circuit-json-to-connectivity-map"
|
|
5
5
|
|
|
6
|
+
type Contour = Array<[number, number]>
|
|
7
|
+
|
|
6
8
|
export type ConnectivityMapKey = string
|
|
7
9
|
|
|
8
10
|
export interface ConvertContext {
|
|
@@ -53,4 +55,12 @@ export interface ConvertContext {
|
|
|
53
55
|
|
|
54
56
|
// Copper cut fill margin (how far to expand the copper outline for the cut fill band)
|
|
55
57
|
copperCutFillMargin: number
|
|
58
|
+
|
|
59
|
+
// Track trace endpoint positions to avoid duplicate circles
|
|
60
|
+
// Key is "x,y" rounded to 6 decimal places
|
|
61
|
+
topTraceEndpoints: Set<string>
|
|
62
|
+
bottomTraceEndpoints: Set<string>
|
|
63
|
+
|
|
64
|
+
// Board outline as a contour for clipping copper cut fill
|
|
65
|
+
boardOutlineContour?: Contour
|
|
56
66
|
}
|
|
@@ -2,22 +2,7 @@ import { Polygon, Box, Point } from "@flatten-js/core"
|
|
|
2
2
|
import type { ConvertContext } from "./ConvertContext"
|
|
3
3
|
import { polygonToShapePathData } from "./polygon-to-shape-path"
|
|
4
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
|
-
}
|
|
5
|
+
import { getManifold } from "./getManifold"
|
|
21
6
|
|
|
22
7
|
type Contour = Array<[number, number]>
|
|
23
8
|
|
|
@@ -115,6 +100,7 @@ export const createCopperCutFillForLayer = async ({
|
|
|
115
100
|
topCopperCutFillCutSetting,
|
|
116
101
|
bottomCopperCutFillCutSetting,
|
|
117
102
|
copperCutFillMargin,
|
|
103
|
+
boardOutlineContour,
|
|
118
104
|
} = ctx
|
|
119
105
|
|
|
120
106
|
// Get the appropriate cut setting for this layer
|
|
@@ -172,7 +158,16 @@ export const createCopperCutFillForLayer = async ({
|
|
|
172
158
|
|
|
173
159
|
// Subtract the inside (original copper) from the outer boundary (expanded copper)
|
|
174
160
|
// This gives us the ring/band area to laser cut
|
|
175
|
-
|
|
161
|
+
let cutFillArea = outerBoundary.subtract(copperInside)
|
|
162
|
+
|
|
163
|
+
// Clip to board outline if available
|
|
164
|
+
if (boardOutlineContour && boardOutlineContour.length >= 3) {
|
|
165
|
+
const boardOutline = new CrossSection([boardOutlineContour], "Positive")
|
|
166
|
+
const clippedArea = cutFillArea.intersect(boardOutline)
|
|
167
|
+
cutFillArea.delete()
|
|
168
|
+
cutFillArea = clippedArea
|
|
169
|
+
boardOutline.delete()
|
|
170
|
+
}
|
|
176
171
|
|
|
177
172
|
// Simplify to clean up any spurious tiny segments
|
|
178
173
|
const simplifiedArea = cutFillArea.simplify(0.001)
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Lazy-load the manifold WASM module
|
|
2
|
+
// Supports both Node.js and browser environments with CDN fallback
|
|
3
|
+
|
|
4
|
+
let manifoldInstance: any = null
|
|
5
|
+
|
|
6
|
+
export const getManifold = async () => {
|
|
7
|
+
if (!manifoldInstance) {
|
|
8
|
+
let ManifoldModule: any
|
|
9
|
+
|
|
10
|
+
// Try Node.js import first (for CLI/tests)
|
|
11
|
+
try {
|
|
12
|
+
const moduleName = "manifold-3d"
|
|
13
|
+
ManifoldModule = (await import(/* @vite-ignore */ moduleName)).default
|
|
14
|
+
} catch {
|
|
15
|
+
// Fallback to CDN for browser environments
|
|
16
|
+
try {
|
|
17
|
+
const cdnUrl =
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/manifold-3d@3.0.0/manifold.js"
|
|
19
|
+
ManifoldModule = (await import(/* @vite-ignore */ cdnUrl)).default
|
|
20
|
+
} catch (cdnError) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Failed to load manifold-3d: not available in Node.js or via CDN. ${cdnError}`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
manifoldInstance = await ManifoldModule()
|
|
28
|
+
manifoldInstance.setup() // Initialize the JS-friendly API
|
|
29
|
+
}
|
|
30
|
+
return manifoldInstance
|
|
31
|
+
}
|
package/lib/index.ts
CHANGED
|
@@ -257,6 +257,8 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
257
257
|
topCopperCutFillCutSetting,
|
|
258
258
|
bottomCopperCutFillCutSetting,
|
|
259
259
|
copperCutFillMargin,
|
|
260
|
+
topTraceEndpoints: new Set(),
|
|
261
|
+
bottomTraceEndpoints: new Set(),
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
// Initialize net geometry maps
|
|
@@ -267,6 +269,34 @@ export const convertCircuitJsonToLbrn = async (
|
|
|
267
269
|
ctx.bottomScanNetGeoms.set(net, [])
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
// Extract board outline for clipping copper cut fill
|
|
273
|
+
for (const board of db.pcb_board.list()) {
|
|
274
|
+
if (board.outline?.length) {
|
|
275
|
+
ctx.boardOutlineContour = board.outline.map((outlinePoint) => [
|
|
276
|
+
outlinePoint.x + origin.x,
|
|
277
|
+
outlinePoint.y + origin.y,
|
|
278
|
+
])
|
|
279
|
+
} else if (
|
|
280
|
+
typeof board.width === "number" &&
|
|
281
|
+
typeof board.height === "number" &&
|
|
282
|
+
board.center
|
|
283
|
+
) {
|
|
284
|
+
const halfWidth = board.width / 2
|
|
285
|
+
const halfHeight = board.height / 2
|
|
286
|
+
const minX = board.center.x - halfWidth + origin.x
|
|
287
|
+
const minY = board.center.y - halfHeight + origin.y
|
|
288
|
+
const maxX = board.center.x + halfWidth + origin.x
|
|
289
|
+
const maxY = board.center.y + halfHeight + origin.y
|
|
290
|
+
ctx.boardOutlineContour = [
|
|
291
|
+
[minX, minY],
|
|
292
|
+
[maxX, minY],
|
|
293
|
+
[maxX, maxY],
|
|
294
|
+
[minX, maxY],
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
break // Only use the first board
|
|
298
|
+
}
|
|
299
|
+
|
|
270
300
|
// Process all PCB elements
|
|
271
301
|
for (const smtpad of db.pcb_smtpad.list()) {
|
|
272
302
|
addSmtPad(smtpad, ctx)
|