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 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 startCircle = createCirclePolygon(
1789
- { x: firstPoint.x + origin.x, y: firstPoint.y + origin.y },
1790
- radius
1791
- );
1792
- cutNetGeoms.get(netId)?.push(startCircle);
1793
- const endCircle = createCirclePolygon(
1794
- { x: lastPoint.x + origin.x, y: lastPoint.y + origin.y },
1795
- radius
1796
- );
1797
- cutNetGeoms.get(netId)?.push(endCircle);
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
- const moduleName = "manifold-3d";
2491
- const ManifoldModule = (await import(
2492
- /* @vite-ignore */
2493
- moduleName
2494
- )).default;
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
- const cutFillArea = outerBoundary.subtract(copperInside);
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
  }
@@ -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
- const cutFillArea = outerBoundary.subtract(copperInside)
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 startCircle = createCirclePolygon(
108
- { x: firstPoint.x + origin.x, y: firstPoint.y + origin.y },
109
- radius,
110
- )
111
- cutNetGeoms.get(netId)?.push(startCircle)
112
-
113
- // Add circle at end point (translated by origin)
114
- const endCircle = createCirclePolygon(
115
- { x: lastPoint.x + origin.x, y: lastPoint.y + origin.y },
116
- radius,
117
- )
118
- cutNetGeoms.get(netId)?.push(endCircle)
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)
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.43",
4
+ "version": "0.0.45",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "bun run site/index.html",