circuit-json-to-lbrn 0.0.20 → 0.0.22

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.
Files changed (81) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +784 -345
  3. package/lib/ConvertContext.ts +6 -2
  4. package/lib/element-handlers/addPcbCutout/addCirclePcbCutout.ts +10 -2
  5. package/lib/element-handlers/addPcbCutout/addPolygonPcbCutout.ts +10 -6
  6. package/lib/element-handlers/addPcbCutout/addRectPcbCutout.ts +12 -12
  7. package/lib/element-handlers/addPcbHole/addCirclePcbHole.ts +10 -2
  8. package/lib/element-handlers/addPcbHole/addOvalPcbHole.ts +8 -8
  9. package/lib/element-handlers/addPcbHole/addPillPcbHole.ts +8 -8
  10. package/lib/element-handlers/addPcbHole/addRectPcbHole.ts +10 -10
  11. package/lib/element-handlers/addPcbHole/addRotatedPillPcbHole.ts +8 -8
  12. package/lib/element-handlers/addPcbTrace/index.ts +145 -61
  13. package/lib/element-handlers/addPcbVia/index.ts +49 -15
  14. package/lib/element-handlers/addPlatedHole/addCirclePlatedHole.ts +49 -15
  15. package/lib/element-handlers/addPlatedHole/addCircularHoleWithRectPad.ts +38 -18
  16. package/lib/element-handlers/addPlatedHole/addHoleWithPolygonPad.ts +41 -16
  17. package/lib/element-handlers/addPlatedHole/addOvalPlatedHole.ts +39 -18
  18. package/lib/element-handlers/addPlatedHole/addPillHoleWithRectPad.ts +38 -23
  19. package/lib/element-handlers/addPlatedHole/addPillPlatedHole.ts +39 -18
  20. package/lib/element-handlers/addPlatedHole/addRotatedPillHoleWithRectPad.ts +43 -28
  21. package/lib/element-handlers/addSmtPad/addCircleSmtPad.ts +31 -4
  22. package/lib/element-handlers/addSmtPad/addPillSmtPad.ts +33 -4
  23. package/lib/element-handlers/addSmtPad/addPolygonSmtPad.ts +25 -3
  24. package/lib/element-handlers/addSmtPad/addRectSmtPad.ts +20 -3
  25. package/lib/element-handlers/addSmtPad/addRotatedPillSmtPad.ts +31 -12
  26. package/lib/element-handlers/addSmtPad/addRotatedRectSmtPad.ts +31 -12
  27. package/lib/helpers/circleShape.ts +13 -6
  28. package/lib/helpers/ovalShape.ts +17 -8
  29. package/lib/helpers/pathPointUtils.ts +11 -5
  30. package/lib/helpers/pillShape.ts +24 -11
  31. package/lib/helpers/polygonShape.ts +11 -5
  32. package/lib/helpers/roundedRectShape.ts +19 -9
  33. package/lib/index.ts +92 -41
  34. package/package.json +1 -1
  35. package/tests/assets/keyboard-default60.json +92565 -0
  36. package/tests/examples/__snapshots__/board-outline-soldermask-preset.snap.svg +1 -1
  37. package/tests/examples/__snapshots__/board-outline.snap.svg +1 -1
  38. package/tests/examples/__snapshots__/lga-interconnect.snap.svg +1 -1
  39. package/tests/examples/__snapshots__/single-trace.snap.svg +1 -1
  40. package/tests/examples/addPcbCutout/__snapshots__/pcb-cutout-circle.snap.svg +1 -1
  41. package/tests/examples/addPcbCutout/__snapshots__/pcb-cutout-path.snap.svg +1 -1
  42. package/tests/examples/addPcbCutout/__snapshots__/pcb-cutout-polygon.snap.svg +1 -1
  43. package/tests/examples/addPcbCutout/__snapshots__/pcb-cutout-rect.snap.svg +1 -1
  44. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-circle.snap.svg +1 -1
  45. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-oval.snap.svg +1 -1
  46. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-pill.snap.svg +1 -1
  47. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-rect.snap.svg +1 -1
  48. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-rotated-pill.snap.svg +2 -2
  49. package/tests/examples/addPcbHole/__snapshots__/pcb-hole-with-soldermask.snap.svg +1 -1
  50. package/tests/examples/addPcbVia/__snapshots__/pcb-via-basic.snap.svg +1 -1
  51. package/tests/examples/addPcbVia/__snapshots__/pcb-via-with-net.snap.svg +1 -1
  52. package/tests/examples/addPcbVia/__snapshots__/pcb-via-with-soldermask.snap.svg +1 -1
  53. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-circle.snap.svg +1 -1
  54. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-circular-hole-with-rect-pad.snap.svg +1 -1
  55. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-oval.snap.svg +1 -1
  56. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-pill-with-rect-pad.snap.svg +1 -1
  57. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-pill.snap.svg +1 -1
  58. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-polygon.snap.svg +1 -1
  59. package/tests/examples/addPlatedHole/__snapshots__/pcb-plated-hole-rotated-pill-with-rect-pad.snap.svg +1 -1
  60. package/tests/examples/addSmtPad/__snapshots__/circleSmtPad.snap.svg +1 -1
  61. package/tests/examples/addSmtPad/__snapshots__/pillSmtPad.snap.svg +1 -1
  62. package/tests/examples/addSmtPad/__snapshots__/polygonSmtPad.snap.svg +1 -1
  63. package/tests/examples/addSmtPad/__snapshots__/rotatedPillSmtPad.snap.svg +1 -1
  64. package/tests/examples/addSmtPad/__snapshots__/rotatedRectSmtPad.snap.svg +1 -1
  65. package/tests/examples/keyboard-defaul60/__snapshots__/keyboard-both-layer-includeSoldermask.snap.svg +8 -0
  66. package/tests/examples/keyboard-defaul60/__snapshots__/keyboard-both-layers.snap.svg +8 -0
  67. package/tests/examples/keyboard-defaul60/__snapshots__/keyboard-bottom-layer.snap.svg +8 -0
  68. package/tests/examples/keyboard-defaul60/__snapshots__/keyboard-top-layer.snap.svg +8 -0
  69. package/tests/examples/keyboard-defaul60/keyboard-both-layer-includeSoldermask.test.ts +27 -0
  70. package/tests/examples/keyboard-defaul60/keyboard-both-layers.test.ts +26 -0
  71. package/tests/examples/keyboard-defaul60/keyboard-bottom-layer.test.ts +26 -0
  72. package/tests/examples/keyboard-defaul60/keyboard-top-layer.test.ts +26 -0
  73. package/tests/examples/lga-interconnect.test.ts +3 -2
  74. package/tests/examples/soldermask/__snapshots__/copper-and-soldermask.snap.svg +1 -1
  75. package/tests/examples/soldermask/__snapshots__/copper-only.snap.svg +1 -1
  76. package/tests/examples/soldermask/__snapshots__/soldermask-only.snap.svg +1 -1
  77. package/tests/examples/soldermask/copper-and-soldermask.test.ts +18 -10
  78. package/tests/examples/soldermask/soldermask-only.test.ts +3 -3
  79. package/tests/examples/soldermask-margin/__snapshots__/negative-soldermask-margin.snap.svg +1 -1
  80. package/tests/examples/soldermask-margin/__snapshots__/positive-soldermask-margin.snap.svg +1 -1
  81. package/tsconfig.json +2 -1
@@ -9,19 +9,23 @@ export interface ConvertContext {
9
9
  db: CircuitJsonUtilObjects
10
10
  project: LightBurnProject
11
11
 
12
- copperCutSetting: CutSetting
12
+ topCopperCutSetting: CutSetting
13
+ bottomCopperCutSetting: CutSetting
13
14
  throughBoardCutSetting: CutSetting
14
15
  soldermaskCutSetting: CutSetting
15
16
 
16
17
  connMap: ConnectivityMap
17
18
 
18
- netGeoms: Map<ConnectivityMapKey, Array<Polygon | Box>>
19
+ // Separate net geometries for top and bottom layers
20
+ topNetGeoms: Map<ConnectivityMapKey, Array<Polygon | Box>>
21
+ bottomNetGeoms: Map<ConnectivityMapKey, Array<Polygon | Box>>
19
22
 
20
23
  origin: { x: number; y: number }
21
24
 
22
25
  // Include flags
23
26
  includeCopper: boolean
24
27
  includeSoldermask: boolean
28
+ includeLayers: Array<"top" | "bottom">
25
29
 
26
30
  // Soldermask margin (can be negative)
27
31
  soldermaskMargin: number
@@ -25,7 +25,11 @@ export const addCirclePcbCutout = (
25
25
 
26
26
  // Add the cutout - cut through the board
27
27
  if (cutout.radius > 0 && includeCopper) {
28
- const circlePath = createCirclePath(centerX, centerY, cutout.radius)
28
+ const circlePath = createCirclePath({
29
+ centerX,
30
+ centerY,
31
+ radius: cutout.radius,
32
+ })
29
33
  project.children.push(
30
34
  new ShapePath({
31
35
  cutIndex: throughBoardCutSetting.index,
@@ -39,7 +43,11 @@ export const addCirclePcbCutout = (
39
43
  // Add soldermask opening if drawing soldermask
40
44
  if (cutout.radius > 0 && includeSoldermask) {
41
45
  const smRadius = cutout.radius + soldermaskMargin
42
- const outer = createCirclePath(centerX, centerY, smRadius)
46
+ const outer = createCirclePath({
47
+ centerX,
48
+ centerY,
49
+ radius: smRadius,
50
+ })
43
51
  project.children.push(
44
52
  new ShapePath({
45
53
  cutIndex: soldermaskCutSetting.index,
@@ -23,11 +23,11 @@ export const addPolygonPcbCutout = (
23
23
 
24
24
  // Add the cutout - cut through the board
25
25
  if (cutout.points.length >= 3 && includeCopper) {
26
- const polygonPath = createPolygonPathFromOutline(
27
- cutout.points,
28
- origin.x,
29
- origin.y,
30
- )
26
+ const polygonPath = createPolygonPathFromOutline({
27
+ outline: cutout.points,
28
+ offsetX: origin.x,
29
+ offsetY: origin.y,
30
+ })
31
31
  project.children.push(
32
32
  new ShapePath({
33
33
  cutIndex: throughBoardCutSetting.index,
@@ -55,7 +55,11 @@ export const addPolygonPcbCutout = (
55
55
  }))
56
56
  : cutout.points
57
57
 
58
- const polygonPath = createPolygonPathFromOutline(points, origin.x, origin.y)
58
+ const polygonPath = createPolygonPathFromOutline({
59
+ outline: points,
60
+ offsetX: origin.x,
61
+ offsetY: origin.y,
62
+ })
59
63
  project.children.push(
60
64
  new ShapePath({
61
65
  cutIndex: soldermaskCutSetting.index,
@@ -26,15 +26,15 @@ export const addRectPcbCutout = (
26
26
  // Add the cutout - cut through the board
27
27
  if (cutout.width > 0 && cutout.height > 0 && includeCopper) {
28
28
  const rotation = (cutout.rotation ?? 0) * (Math.PI / 180) // Convert degrees to radians
29
- const rectPath = createRoundedRectPath(
29
+ const rectPath = createRoundedRectPath({
30
30
  centerX,
31
31
  centerY,
32
- cutout.width,
33
- cutout.height,
34
- 0, // no border radius for cutouts
35
- 4, // segments
32
+ width: cutout.width,
33
+ height: cutout.height,
34
+ borderRadius: 0, // no border radius for cutouts
35
+ segments: 4, // segments
36
36
  rotation,
37
- )
37
+ })
38
38
  project.children.push(
39
39
  new ShapePath({
40
40
  cutIndex: throughBoardCutSetting.index,
@@ -50,15 +50,15 @@ export const addRectPcbCutout = (
50
50
  const rotation = (cutout.rotation ?? 0) * (Math.PI / 180) // Convert degrees to radians
51
51
  const smWidth = cutout.width + 2 * soldermaskMargin
52
52
  const smHeight = cutout.height + 2 * soldermaskMargin
53
- const rectPath = createRoundedRectPath(
53
+ const rectPath = createRoundedRectPath({
54
54
  centerX,
55
55
  centerY,
56
- smWidth,
57
- smHeight,
58
- 0, // no border radius for cutouts
59
- 4, // segments
56
+ width: smWidth,
57
+ height: smHeight,
58
+ borderRadius: 0, // no border radius for cutouts
59
+ segments: 4, // segments
60
60
  rotation,
61
- )
61
+ })
62
62
  project.children.push(
63
63
  new ShapePath({
64
64
  cutIndex: soldermaskCutSetting.index,
@@ -25,7 +25,11 @@ export const addCirclePcbHole = (
25
25
  // Add soldermask opening if drawing soldermask
26
26
  if (hole.hole_diameter > 0 && includeSoldermask) {
27
27
  const smRadius = hole.hole_diameter / 2 + soldermaskMargin
28
- const soldermaskPath = createCirclePath(centerX, centerY, smRadius)
28
+ const soldermaskPath = createCirclePath({
29
+ centerX,
30
+ centerY,
31
+ radius: smRadius,
32
+ })
29
33
  project.children.push(
30
34
  new ShapePath({
31
35
  cutIndex: soldermaskCutSetting.index,
@@ -39,7 +43,11 @@ export const addCirclePcbHole = (
39
43
  // Add the hole - cut through the board
40
44
  if (hole.hole_diameter > 0 && includeCopper) {
41
45
  const radius = hole.hole_diameter / 2
42
- const circlePath = createCirclePath(centerX, centerY, radius)
46
+ const circlePath = createCirclePath({
47
+ centerX,
48
+ centerY,
49
+ radius,
50
+ })
43
51
  project.children.push(
44
52
  new ShapePath({
45
53
  cutIndex: throughBoardCutSetting.index,
@@ -24,12 +24,12 @@ export const addOvalPcbHole = (
24
24
 
25
25
  // Add soldermask opening if drawing soldermask
26
26
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeSoldermask) {
27
- const soldermaskPath = createOvalPath(
27
+ const soldermaskPath = createOvalPath({
28
28
  centerX,
29
29
  centerY,
30
- hole.hole_width + soldermaskMargin * 2,
31
- hole.hole_height + soldermaskMargin * 2,
32
- )
30
+ width: hole.hole_width + soldermaskMargin * 2,
31
+ height: hole.hole_height + soldermaskMargin * 2,
32
+ })
33
33
  project.children.push(
34
34
  new ShapePath({
35
35
  cutIndex: soldermaskCutSetting.index,
@@ -42,12 +42,12 @@ export const addOvalPcbHole = (
42
42
 
43
43
  // Add the hole - cut through the board
44
44
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeCopper) {
45
- const ovalPath = createOvalPath(
45
+ const ovalPath = createOvalPath({
46
46
  centerX,
47
47
  centerY,
48
- hole.hole_width,
49
- hole.hole_height,
50
- )
48
+ width: hole.hole_width,
49
+ height: hole.hole_height,
50
+ })
51
51
  project.children.push(
52
52
  new ShapePath({
53
53
  cutIndex: throughBoardCutSetting.index,
@@ -24,12 +24,12 @@ export const addPillPcbHole = (
24
24
 
25
25
  // Add soldermask opening if drawing soldermask
26
26
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeSoldermask) {
27
- const soldermaskPath = createPillPath(
27
+ const soldermaskPath = createPillPath({
28
28
  centerX,
29
29
  centerY,
30
- hole.hole_width + soldermaskMargin * 2,
31
- hole.hole_height + soldermaskMargin * 2,
32
- )
30
+ width: hole.hole_width + soldermaskMargin * 2,
31
+ height: hole.hole_height + soldermaskMargin * 2,
32
+ })
33
33
  project.children.push(
34
34
  new ShapePath({
35
35
  cutIndex: soldermaskCutSetting.index,
@@ -42,12 +42,12 @@ export const addPillPcbHole = (
42
42
 
43
43
  // Add the hole - cut through the board
44
44
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeCopper) {
45
- const pillPath = createPillPath(
45
+ const pillPath = createPillPath({
46
46
  centerX,
47
47
  centerY,
48
- hole.hole_width,
49
- hole.hole_height,
50
- )
48
+ width: hole.hole_width,
49
+ height: hole.hole_height,
50
+ })
51
51
  project.children.push(
52
52
  new ShapePath({
53
53
  cutIndex: throughBoardCutSetting.index,
@@ -24,13 +24,13 @@ export const addRectPcbHole = (
24
24
 
25
25
  // Add soldermask opening if drawing soldermask
26
26
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeSoldermask) {
27
- const soldermaskPath = createRoundedRectPath(
27
+ const soldermaskPath = createRoundedRectPath({
28
28
  centerX,
29
29
  centerY,
30
- hole.hole_width + soldermaskMargin * 2,
31
- hole.hole_height + soldermaskMargin * 2,
32
- 0, // no border radius for rect holes
33
- )
30
+ width: hole.hole_width + soldermaskMargin * 2,
31
+ height: hole.hole_height + soldermaskMargin * 2,
32
+ borderRadius: 0, // no border radius for rect holes
33
+ })
34
34
  project.children.push(
35
35
  new ShapePath({
36
36
  cutIndex: soldermaskCutSetting.index,
@@ -43,13 +43,13 @@ export const addRectPcbHole = (
43
43
 
44
44
  // Add the hole - cut through the board
45
45
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeCopper) {
46
- const rectPath = createRoundedRectPath(
46
+ const rectPath = createRoundedRectPath({
47
47
  centerX,
48
48
  centerY,
49
- hole.hole_width,
50
- hole.hole_height,
51
- 0, // no border radius for rect holes
52
- )
49
+ width: hole.hole_width,
50
+ height: hole.hole_height,
51
+ borderRadius: 0, // no border radius for rect holes
52
+ })
53
53
  project.children.push(
54
54
  new ShapePath({
55
55
  cutIndex: throughBoardCutSetting.index,
@@ -25,13 +25,13 @@ export const addRotatedPillPcbHole = (
25
25
 
26
26
  // Add soldermask opening if drawing soldermask
27
27
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeSoldermask) {
28
- const soldermaskPath = createPillPath(
28
+ const soldermaskPath = createPillPath({
29
29
  centerX,
30
30
  centerY,
31
- hole.hole_width + soldermaskMargin * 2,
32
- hole.hole_height + soldermaskMargin * 2,
31
+ width: hole.hole_width + soldermaskMargin * 2,
32
+ height: hole.hole_height + soldermaskMargin * 2,
33
33
  rotation,
34
- )
34
+ })
35
35
  project.children.push(
36
36
  new ShapePath({
37
37
  cutIndex: soldermaskCutSetting.index,
@@ -44,13 +44,13 @@ export const addRotatedPillPcbHole = (
44
44
 
45
45
  // Add the hole - cut through the board
46
46
  if (hole.hole_width > 0 && hole.hole_height > 0 && includeCopper) {
47
- const pillPath = createPillPath(
47
+ const pillPath = createPillPath({
48
48
  centerX,
49
49
  centerY,
50
- hole.hole_width,
51
- hole.hole_height,
50
+ width: hole.hole_width,
51
+ height: hole.hole_height,
52
52
  rotation,
53
- )
53
+ })
54
54
  project.children.push(
55
55
  new ShapePath({
56
56
  cutIndex: throughBoardCutSetting.index,
@@ -1,10 +1,17 @@
1
- import type { PcbTrace } from "circuit-json"
1
+ import type { PcbTrace, PcbTraceRoutePoint } from "circuit-json"
2
2
  import type { ConvertContext } from "../../ConvertContext"
3
3
  import Flatten, { BooleanOperations } from "@flatten-js/core"
4
4
  import { circleToPolygon } from "./circle-to-polygon"
5
5
 
6
6
  export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
7
- const { netGeoms, connMap, origin, includeCopper, includeSoldermask } = ctx
7
+ const {
8
+ topNetGeoms,
9
+ bottomNetGeoms,
10
+ connMap,
11
+ origin,
12
+ includeCopper,
13
+ includeLayers,
14
+ } = ctx
8
15
 
9
16
  // Only include traces when including copper
10
17
  // Traces are NOT included in soldermask-only mode to prevent accidental bridging
@@ -14,83 +21,160 @@ export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
14
21
 
15
22
  const netId = connMap.getNetConnectedToId(
16
23
  trace.source_trace_id ?? trace.pcb_trace_id,
17
- )!
24
+ )
18
25
 
19
26
  if (!netId) {
20
- console.warn(`Trace ${trace.pcb_trace_id} is not connected to any net`)
27
+ // Skip traces that are not connected to any net
28
+ // This can happen for traces without a source_trace_id
21
29
  return
22
30
  }
23
31
 
24
32
  if (!trace.route || trace.route.length < 2) {
25
- console.warn(`Trace ${trace.pcb_trace_id} has insufficient route points`)
26
33
  return
27
34
  }
28
35
 
29
36
  const { route } = trace
37
+ const wirePoint = route.find((point) => {
38
+ if (!("route_type" in point)) return true
39
+ return point.route_type === "wire"
40
+ })
41
+ const traceWidth = (wirePoint as any)?.width ?? 0.15
42
+
43
+ // Group consecutive wire segments by layer
44
+ // When we hit a via, we start a new segment on the new layer
45
+ const layerSegments = new Map<
46
+ "top" | "bottom",
47
+ Array<Array<{ x: number; y: number }>>
48
+ >()
49
+
50
+ let currentSegment: Array<{ x: number; y: number }> = []
51
+ let currentLayer: "top" | "bottom" | null = null
52
+
53
+ for (const point of route) {
54
+ if ("route_type" in point && point.route_type === "via") {
55
+ // Via marks end of current segment - save it
56
+ if (currentLayer && currentSegment.length > 0) {
57
+ if (!layerSegments.has(currentLayer)) {
58
+ layerSegments.set(currentLayer, [])
59
+ }
60
+ layerSegments.get(currentLayer)!.push(currentSegment)
61
+ }
62
+ // Reset for next segment
63
+ currentSegment = []
64
+ currentLayer = null
65
+ continue
66
+ }
30
67
 
31
- const traceWidth =
32
- route.find((point) => point.route_type === "wire")?.width ?? 0.15
33
-
34
- const polygons: Flatten.Polygon[] = []
35
-
36
- // Add circles for each vertex
37
- for (const routePoint of route) {
38
- const circle = new Flatten.Circle(
39
- new Flatten.Point(routePoint.x + origin.x, routePoint.y + origin.y),
40
- traceWidth / 2,
41
- )
42
- polygons.push(circleToPolygon(circle))
68
+ // Treat points without route_type as wire points (default behavior)
69
+ const isWirePoint = !("route_type" in point) || point.route_type === "wire"
70
+ if (isWirePoint && "layer" in point && point.layer) {
71
+ const pointLayer = point.layer as "top" | "bottom" | string
72
+ if (pointLayer !== "top" && pointLayer !== "bottom") {
73
+ continue // Skip inner layers
74
+ }
75
+
76
+ // If layer changed (shouldn't happen without via, but handle it)
77
+ if (currentLayer !== null && currentLayer !== pointLayer) {
78
+ // Save current segment
79
+ if (currentSegment.length > 0) {
80
+ if (!layerSegments.has(currentLayer)) {
81
+ layerSegments.set(currentLayer, [])
82
+ }
83
+ layerSegments.get(currentLayer)!.push(currentSegment)
84
+ }
85
+ // Start new segment
86
+ currentSegment = []
87
+ }
88
+
89
+ currentLayer = pointLayer
90
+ currentSegment.push({ x: point.x, y: point.y })
91
+ }
43
92
  }
44
93
 
45
- // Add rectangles for each segment
46
- for (let i = 0; i < route.length - 1; i++) {
47
- const p1 = route[i]
48
- const p2 = route[i + 1]
49
-
50
- if (!p1 || !p2) continue
51
-
52
- const segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y)
53
- if (segmentLength === 0) continue
54
-
55
- const centerX = (p1.x + p2.x) / 2 + origin.x
56
- const centerY = (p1.y + p2.y) / 2 + origin.y
57
- const rotationDeg = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI
58
-
59
- const w2 = segmentLength / 2
60
- const h2 = traceWidth / 2
61
-
62
- const angleRad = (rotationDeg * Math.PI) / 180
63
- const cosAngle = Math.cos(angleRad)
64
- const sinAngle = Math.sin(angleRad)
65
-
66
- const corners = [
67
- { x: -w2, y: -h2 },
68
- { x: w2, y: -h2 },
69
- { x: w2, y: h2 },
70
- { x: -w2, y: h2 },
71
- ]
94
+ // Save final segment
95
+ if (currentLayer && currentSegment.length > 0) {
96
+ if (!layerSegments.has(currentLayer)) {
97
+ layerSegments.set(currentLayer, [])
98
+ }
99
+ layerSegments.get(currentLayer)!.push(currentSegment)
100
+ }
72
101
 
73
- const rotatedCorners = corners.map((p) => ({
74
- x: centerX + p.x * cosAngle - p.y * sinAngle,
75
- y: centerY + p.x * sinAngle + p.y * cosAngle,
76
- }))
102
+ // Process each layer
103
+ for (const [layer, segments] of layerSegments.entries()) {
104
+ if (!includeLayers.includes(layer)) {
105
+ continue
106
+ }
77
107
 
78
- polygons.push(
79
- new Flatten.Polygon(rotatedCorners.map((p) => Flatten.point(p.x, p.y))),
80
- )
81
- }
108
+ // Combine all segments for this layer into one set of polygons
109
+ const polygons: Flatten.Polygon[] = []
110
+
111
+ for (const points of segments) {
112
+ if (points.length < 2) continue
113
+
114
+ // Add circles for each vertex in this segment
115
+ for (const routePoint of points) {
116
+ const circle = new Flatten.Circle(
117
+ new Flatten.Point(routePoint.x + origin.x, routePoint.y + origin.y),
118
+ traceWidth / 2,
119
+ )
120
+ polygons.push(circleToPolygon(circle))
121
+ }
122
+
123
+ // Add rectangles for each line segment
124
+ for (let i = 0; i < points.length - 1; i++) {
125
+ const p1 = points[i]
126
+ const p2 = points[i + 1]
127
+
128
+ if (!p1 || !p2) continue
129
+
130
+ const segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y)
131
+ if (segmentLength === 0) continue
132
+
133
+ const centerX = (p1.x + p2.x) / 2 + origin.x
134
+ const centerY = (p1.y + p2.y) / 2 + origin.y
135
+ const rotationDeg =
136
+ (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI
137
+
138
+ const w2 = segmentLength / 2
139
+ const h2 = traceWidth / 2
140
+
141
+ const angleRad = (rotationDeg * Math.PI) / 180
142
+ const cosAngle = Math.cos(angleRad)
143
+ const sinAngle = Math.sin(angleRad)
144
+
145
+ const corners = [
146
+ { x: -w2, y: -h2 },
147
+ { x: w2, y: -h2 },
148
+ { x: w2, y: h2 },
149
+ { x: -w2, y: h2 },
150
+ ]
151
+
152
+ const rotatedCorners = corners.map((p) => ({
153
+ x: centerX + p.x * cosAngle - p.y * sinAngle,
154
+ y: centerY + p.x * sinAngle + p.y * cosAngle,
155
+ }))
156
+
157
+ polygons.push(
158
+ new Flatten.Polygon(
159
+ rotatedCorners.map((p) => Flatten.point(p.x, p.y)),
160
+ ),
161
+ )
162
+ }
163
+ }
82
164
 
83
- // Union all polygons together to create the final trace polygon
84
- if (polygons.length === 0) return
165
+ // Union all polygons together to create the final trace polygon for this layer
166
+ if (polygons.length === 0) continue
85
167
 
86
- let tracePolygon = polygons[0]!
168
+ let tracePolygon = polygons[0]!
87
169
 
88
- for (let i = 1; i < polygons.length; i++) {
89
- const poly = polygons[i]
90
- if (poly) {
91
- tracePolygon = BooleanOperations.unify(tracePolygon, poly)
170
+ for (let i = 1; i < polygons.length; i++) {
171
+ const poly = polygons[i]
172
+ if (poly) {
173
+ tracePolygon = BooleanOperations.unify(tracePolygon, poly)
174
+ }
92
175
  }
93
- }
94
176
 
95
- netGeoms.get(netId)?.push(tracePolygon)
177
+ const netGeoms = layer === "top" ? topNetGeoms : bottomNetGeoms
178
+ netGeoms.get(netId)?.push(tracePolygon)
179
+ }
96
180
  }
@@ -9,19 +9,24 @@ export const addPcbVia = (via: PcbVia, ctx: ConvertContext): void => {
9
9
  const {
10
10
  db,
11
11
  project,
12
- copperCutSetting,
12
+ topCopperCutSetting,
13
+ bottomCopperCutSetting,
13
14
  soldermaskCutSetting,
14
15
  throughBoardCutSetting,
16
+ topNetGeoms,
17
+ bottomNetGeoms,
15
18
  origin,
16
19
  includeCopper,
17
20
  includeSoldermask,
18
21
  connMap,
19
22
  soldermaskMargin,
23
+ includeLayers,
20
24
  } = ctx
21
25
  const centerX = via.x + origin.x
22
26
  const centerY = via.y + origin.y
23
27
 
24
28
  // Add outer circle (copper annulus) if drawing copper - add to netGeoms for merging
29
+ // Vias go through all layers, so add to both top and bottom
25
30
  if (via.outer_diameter > 0 && includeCopper) {
26
31
  // Find the pcb_port associated with this via (vias don't have pcb_port_id property)
27
32
  // We need to find a port at the same location as the via
@@ -38,26 +43,51 @@ export const addPcbVia = (via: PcbVia, ctx: ConvertContext): void => {
38
43
  const polygon = circleToPolygon(circle)
39
44
 
40
45
  if (netId) {
41
- // Add to netGeoms to be merged with other elements on the same net
42
- ctx.netGeoms.get(netId)?.push(polygon)
46
+ // Add to both top and bottom netGeoms since vias go through the board
47
+ if (includeLayers.includes("top")) {
48
+ topNetGeoms.get(netId)?.push(polygon.clone())
49
+ }
50
+ if (includeLayers.includes("bottom")) {
51
+ bottomNetGeoms.get(netId)?.push(polygon.clone())
52
+ }
43
53
  } else {
44
- // No net connection - draw directly
45
- const outer = createCirclePath(centerX, centerY, outerRadius)
46
- project.children.push(
47
- new ShapePath({
48
- cutIndex: copperCutSetting.index,
49
- verts: outer.verts,
50
- prims: outer.prims,
51
- isClosed: true,
52
- }),
53
- )
54
+ // No net connection - draw directly for each included layer
55
+ const outer = createCirclePath({
56
+ centerX,
57
+ centerY,
58
+ radius: outerRadius,
59
+ })
60
+ if (includeLayers.includes("top")) {
61
+ project.children.push(
62
+ new ShapePath({
63
+ cutIndex: topCopperCutSetting.index,
64
+ verts: outer.verts,
65
+ prims: outer.prims,
66
+ isClosed: true,
67
+ }),
68
+ )
69
+ }
70
+ if (includeLayers.includes("bottom")) {
71
+ project.children.push(
72
+ new ShapePath({
73
+ cutIndex: bottomCopperCutSetting.index,
74
+ verts: outer.verts,
75
+ prims: outer.prims,
76
+ isClosed: true,
77
+ }),
78
+ )
79
+ }
54
80
  }
55
81
  }
56
82
 
57
83
  // Add soldermask opening if drawing soldermask
58
84
  if (via.outer_diameter > 0 && includeSoldermask) {
59
85
  const smRadius = via.outer_diameter / 2 + soldermaskMargin
60
- const outer = createCirclePath(centerX, centerY, smRadius)
86
+ const outer = createCirclePath({
87
+ centerX,
88
+ centerY,
89
+ radius: smRadius,
90
+ })
61
91
  project.children.push(
62
92
  new ShapePath({
63
93
  cutIndex: soldermaskCutSetting.index,
@@ -71,7 +101,11 @@ export const addPcbVia = (via: PcbVia, ctx: ConvertContext): void => {
71
101
  // Add inner circle (hole) - always cut through the board regardless of mode
72
102
  if (via.hole_diameter > 0 && includeCopper) {
73
103
  const innerRadius = via.hole_diameter / 2
74
- const inner = createCirclePath(centerX, centerY, innerRadius)
104
+ const inner = createCirclePath({
105
+ centerX,
106
+ centerY,
107
+ radius: innerRadius,
108
+ })
75
109
  project.children.push(
76
110
  new ShapePath({
77
111
  cutIndex: throughBoardCutSetting.index,