circuit-to-canvas 0.0.49 → 0.0.51
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.d.ts +13 -5
- package/dist/index.js +1450 -1226
- package/lib/drawer/CircuitToCanvasDrawer.ts +262 -312
- package/lib/drawer/elements/helper-functions/draw-pill.ts +39 -0
- package/lib/drawer/elements/helper-functions/draw-polygon.ts +25 -0
- package/lib/drawer/elements/helper-functions/draw-rounded-rect.ts +34 -0
- package/lib/drawer/elements/helper-functions/index.ts +3 -0
- package/lib/drawer/elements/pcb-board.ts +13 -3
- package/lib/drawer/elements/pcb-hole.ts +56 -338
- package/lib/drawer/elements/pcb-plated-hole.ts +154 -442
- package/lib/drawer/elements/pcb-smtpad.ts +5 -271
- package/lib/drawer/elements/pcb-soldermask/board.ts +44 -0
- package/lib/drawer/elements/pcb-soldermask/cutout.ts +74 -0
- package/lib/drawer/elements/pcb-soldermask/hole.ts +288 -0
- package/lib/drawer/elements/pcb-soldermask/index.ts +140 -0
- package/lib/drawer/elements/pcb-soldermask/plated-hole.ts +365 -0
- package/lib/drawer/elements/pcb-soldermask/smt-pad.ts +354 -0
- package/lib/drawer/elements/pcb-soldermask/via.ts +27 -0
- package/lib/drawer/elements/soldermask-margin.ts +39 -8
- package/package.json +2 -2
- package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
- package/tests/board-snapshot/usb-c-flashlight-board.test.ts +1 -0
- package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
- package/tests/elements/__snapshots__/brep-copper-pours.snap.png +0 -0
- package/tests/elements/__snapshots__/custom-outline-board.snap.png +0 -0
- package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-board.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-comprehensive-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-keepout-with-group-id.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-no-soldermask.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-silkscreen-on-component.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-asymmetric-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-coverage.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
- package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +2 -2
- package/tests/elements/pcb-hole-soldermask-margin.test.ts +155 -2
- package/tests/elements/pcb-no-soldermask.test.ts +1281 -0
- package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +1 -1
- package/tests/elements/pcb-plated-hole.test.ts +40 -4
- package/tests/elements/pcb-smtpad-asymmetric-soldermask-margin.test.ts +140 -0
- package/tests/elements/pcb-smtpad-soldermask-coverage.test.ts +1 -1
- package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +18 -2
- package/tests/fixtures/getStackedPngSvgComparison.ts +8 -2
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type { PcbHole } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import { applyToPoint } from "transformation-matrix"
|
|
4
|
+
import type { CanvasContext, PcbColorMap } from "../../types"
|
|
5
|
+
import { drawPillPath } from "../helper-functions/draw-pill"
|
|
6
|
+
/**
|
|
7
|
+
* Process soldermask for a non-plated hole.
|
|
8
|
+
*/
|
|
9
|
+
export function processHoleSoldermask(params: {
|
|
10
|
+
ctx: CanvasContext
|
|
11
|
+
hole: PcbHole
|
|
12
|
+
realToCanvasMat: Matrix
|
|
13
|
+
colorMap: PcbColorMap
|
|
14
|
+
soldermaskOverCopperColor: string
|
|
15
|
+
showSoldermask: boolean
|
|
16
|
+
}): void {
|
|
17
|
+
const {
|
|
18
|
+
ctx,
|
|
19
|
+
hole,
|
|
20
|
+
realToCanvasMat,
|
|
21
|
+
colorMap,
|
|
22
|
+
soldermaskOverCopperColor,
|
|
23
|
+
showSoldermask,
|
|
24
|
+
} = params
|
|
25
|
+
// When soldermask is disabled, treat all holes as not covered with soldermask
|
|
26
|
+
// and use zero margin (normal rendering)
|
|
27
|
+
const isCoveredWithSoldermask =
|
|
28
|
+
showSoldermask && hole.is_covered_with_solder_mask === true
|
|
29
|
+
const margin = showSoldermask ? (hole.soldermask_margin ?? 0) : 0
|
|
30
|
+
|
|
31
|
+
if (isCoveredWithSoldermask) {
|
|
32
|
+
// Draw light green over the entire hole
|
|
33
|
+
ctx.fillStyle = soldermaskOverCopperColor
|
|
34
|
+
drawHoleShapePath({ ctx, hole, realToCanvasMat, margin: 0 })
|
|
35
|
+
ctx.fill()
|
|
36
|
+
} else if (margin < 0) {
|
|
37
|
+
// Negative margin: draw drill color for hole, then light green ring
|
|
38
|
+
ctx.fillStyle = colorMap.drill
|
|
39
|
+
drawHoleShapePath({ ctx, hole, realToCanvasMat, margin: 0 })
|
|
40
|
+
ctx.fill()
|
|
41
|
+
drawNegativeMarginRingForHole({
|
|
42
|
+
ctx,
|
|
43
|
+
hole,
|
|
44
|
+
realToCanvasMat,
|
|
45
|
+
soldermaskOverCopperColor,
|
|
46
|
+
margin,
|
|
47
|
+
})
|
|
48
|
+
} else if (margin > 0) {
|
|
49
|
+
// Positive margin: draw substrate for larger area, then drill for hole
|
|
50
|
+
ctx.fillStyle = colorMap.substrate
|
|
51
|
+
drawHoleShapePath({ ctx, hole, realToCanvasMat, margin })
|
|
52
|
+
ctx.fill()
|
|
53
|
+
ctx.fillStyle = colorMap.drill
|
|
54
|
+
drawHoleShapePath({ ctx, hole, realToCanvasMat, margin: 0 })
|
|
55
|
+
ctx.fill()
|
|
56
|
+
} else {
|
|
57
|
+
// Zero margin: just draw drill color for the hole
|
|
58
|
+
ctx.fillStyle = colorMap.drill
|
|
59
|
+
drawHoleShapePath({ ctx, hole, realToCanvasMat, margin: 0 })
|
|
60
|
+
ctx.fill()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getHoleRotation(hole: PcbHole): number {
|
|
65
|
+
if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
|
|
66
|
+
return hole.ccw_rotation
|
|
67
|
+
}
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function drawHoleShapePath(params: {
|
|
72
|
+
ctx: CanvasContext
|
|
73
|
+
hole: PcbHole
|
|
74
|
+
realToCanvasMat: Matrix
|
|
75
|
+
margin: number
|
|
76
|
+
}): void {
|
|
77
|
+
const { ctx, hole, realToCanvasMat, margin } = params
|
|
78
|
+
const rotation = getHoleRotation(hole)
|
|
79
|
+
|
|
80
|
+
if (hole.hole_shape === "circle") {
|
|
81
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
82
|
+
const scaledRadius =
|
|
83
|
+
(hole.hole_diameter / 2 + margin) * Math.abs(realToCanvasMat.a)
|
|
84
|
+
|
|
85
|
+
ctx.beginPath()
|
|
86
|
+
ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
|
|
87
|
+
ctx.closePath()
|
|
88
|
+
} else if (hole.hole_shape === "square") {
|
|
89
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
90
|
+
const scaledSize =
|
|
91
|
+
(hole.hole_diameter + margin * 2) * Math.abs(realToCanvasMat.a)
|
|
92
|
+
|
|
93
|
+
ctx.save()
|
|
94
|
+
ctx.translate(cx, cy)
|
|
95
|
+
if (rotation !== 0) {
|
|
96
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ctx.beginPath()
|
|
100
|
+
ctx.rect(-scaledSize / 2, -scaledSize / 2, scaledSize, scaledSize)
|
|
101
|
+
ctx.closePath()
|
|
102
|
+
ctx.restore()
|
|
103
|
+
} else if (hole.hole_shape === "oval") {
|
|
104
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
105
|
+
const scaledRadiusX =
|
|
106
|
+
(hole.hole_width / 2 + margin) * Math.abs(realToCanvasMat.a)
|
|
107
|
+
const scaledRadiusY =
|
|
108
|
+
(hole.hole_height / 2 + margin) * Math.abs(realToCanvasMat.a)
|
|
109
|
+
|
|
110
|
+
ctx.save()
|
|
111
|
+
ctx.translate(cx, cy)
|
|
112
|
+
if (rotation !== 0) {
|
|
113
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctx.beginPath()
|
|
117
|
+
ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2)
|
|
118
|
+
ctx.closePath()
|
|
119
|
+
ctx.restore()
|
|
120
|
+
} else if (hole.hole_shape === "rect") {
|
|
121
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
122
|
+
const scaledWidth =
|
|
123
|
+
(hole.hole_width + margin * 2) * Math.abs(realToCanvasMat.a)
|
|
124
|
+
const scaledHeight =
|
|
125
|
+
(hole.hole_height + margin * 2) * Math.abs(realToCanvasMat.a)
|
|
126
|
+
|
|
127
|
+
ctx.save()
|
|
128
|
+
ctx.translate(cx, cy)
|
|
129
|
+
if (rotation !== 0) {
|
|
130
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ctx.beginPath()
|
|
134
|
+
ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
|
|
135
|
+
ctx.closePath()
|
|
136
|
+
ctx.restore()
|
|
137
|
+
} else if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
|
|
138
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
139
|
+
const scaledWidth =
|
|
140
|
+
(hole.hole_width + margin * 2) * Math.abs(realToCanvasMat.a)
|
|
141
|
+
const scaledHeight =
|
|
142
|
+
(hole.hole_height + margin * 2) * Math.abs(realToCanvasMat.a)
|
|
143
|
+
|
|
144
|
+
ctx.save()
|
|
145
|
+
ctx.translate(cx, cy)
|
|
146
|
+
if (rotation !== 0) {
|
|
147
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ctx.beginPath()
|
|
151
|
+
drawPillPath({
|
|
152
|
+
ctx,
|
|
153
|
+
cx: 0,
|
|
154
|
+
cy: 0,
|
|
155
|
+
width: scaledWidth,
|
|
156
|
+
height: scaledHeight,
|
|
157
|
+
})
|
|
158
|
+
ctx.restore()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function drawNegativeMarginRingForHole(params: {
|
|
163
|
+
ctx: CanvasContext
|
|
164
|
+
hole: PcbHole
|
|
165
|
+
realToCanvasMat: Matrix
|
|
166
|
+
soldermaskOverCopperColor: string
|
|
167
|
+
margin: number
|
|
168
|
+
}): void {
|
|
169
|
+
const { ctx, hole, realToCanvasMat, soldermaskOverCopperColor, margin } =
|
|
170
|
+
params
|
|
171
|
+
const thickness = Math.abs(margin)
|
|
172
|
+
const rotation = getHoleRotation(hole)
|
|
173
|
+
|
|
174
|
+
ctx.fillStyle = soldermaskOverCopperColor
|
|
175
|
+
|
|
176
|
+
if (hole.hole_shape === "circle") {
|
|
177
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
178
|
+
const scaledRadius = (hole.hole_diameter / 2) * Math.abs(realToCanvasMat.a)
|
|
179
|
+
const scaledThickness = thickness * Math.abs(realToCanvasMat.a)
|
|
180
|
+
const innerRadius = Math.max(0, scaledRadius - scaledThickness)
|
|
181
|
+
|
|
182
|
+
ctx.save()
|
|
183
|
+
ctx.beginPath()
|
|
184
|
+
ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
|
|
185
|
+
if (innerRadius > 0) {
|
|
186
|
+
ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2, true)
|
|
187
|
+
}
|
|
188
|
+
ctx.fill("evenodd")
|
|
189
|
+
ctx.restore()
|
|
190
|
+
} else if (hole.hole_shape === "square") {
|
|
191
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
192
|
+
const scaledSize = hole.hole_diameter * Math.abs(realToCanvasMat.a)
|
|
193
|
+
const scaledThickness = thickness * Math.abs(realToCanvasMat.a)
|
|
194
|
+
const innerSize = Math.max(0, scaledSize - scaledThickness * 2)
|
|
195
|
+
|
|
196
|
+
ctx.save()
|
|
197
|
+
ctx.translate(cx, cy)
|
|
198
|
+
if (rotation !== 0) {
|
|
199
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ctx.beginPath()
|
|
203
|
+
ctx.rect(-scaledSize / 2, -scaledSize / 2, scaledSize, scaledSize)
|
|
204
|
+
if (innerSize > 0) {
|
|
205
|
+
ctx.rect(-innerSize / 2, -innerSize / 2, innerSize, innerSize)
|
|
206
|
+
}
|
|
207
|
+
ctx.fill("evenodd")
|
|
208
|
+
ctx.restore()
|
|
209
|
+
} else if (hole.hole_shape === "oval") {
|
|
210
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
211
|
+
const scaledRadiusX = (hole.hole_width / 2) * Math.abs(realToCanvasMat.a)
|
|
212
|
+
const scaledRadiusY = (hole.hole_height / 2) * Math.abs(realToCanvasMat.a)
|
|
213
|
+
const scaledThickness = thickness * Math.abs(realToCanvasMat.a)
|
|
214
|
+
const innerRadiusX = Math.max(0, scaledRadiusX - scaledThickness)
|
|
215
|
+
const innerRadiusY = Math.max(0, scaledRadiusY - scaledThickness)
|
|
216
|
+
|
|
217
|
+
ctx.save()
|
|
218
|
+
ctx.translate(cx, cy)
|
|
219
|
+
if (rotation !== 0) {
|
|
220
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ctx.beginPath()
|
|
224
|
+
ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2)
|
|
225
|
+
if (innerRadiusX > 0 && innerRadiusY > 0) {
|
|
226
|
+
ctx.moveTo(innerRadiusX, 0)
|
|
227
|
+
ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2)
|
|
228
|
+
}
|
|
229
|
+
ctx.fill("evenodd")
|
|
230
|
+
ctx.restore()
|
|
231
|
+
} else if (hole.hole_shape === "rect") {
|
|
232
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
233
|
+
const scaledWidth = hole.hole_width * Math.abs(realToCanvasMat.a)
|
|
234
|
+
const scaledHeight = hole.hole_height * Math.abs(realToCanvasMat.a)
|
|
235
|
+
const scaledThickness = thickness * Math.abs(realToCanvasMat.a)
|
|
236
|
+
const innerWidth = Math.max(0, scaledWidth - scaledThickness * 2)
|
|
237
|
+
const innerHeight = Math.max(0, scaledHeight - scaledThickness * 2)
|
|
238
|
+
|
|
239
|
+
ctx.save()
|
|
240
|
+
ctx.translate(cx, cy)
|
|
241
|
+
if (rotation !== 0) {
|
|
242
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ctx.beginPath()
|
|
246
|
+
ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
|
|
247
|
+
if (innerWidth > 0 && innerHeight > 0) {
|
|
248
|
+
ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight)
|
|
249
|
+
}
|
|
250
|
+
ctx.fill("evenodd")
|
|
251
|
+
ctx.restore()
|
|
252
|
+
} else if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
|
|
253
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [hole.x, hole.y])
|
|
254
|
+
const scaledWidth = hole.hole_width * Math.abs(realToCanvasMat.a)
|
|
255
|
+
const scaledHeight = hole.hole_height * Math.abs(realToCanvasMat.a)
|
|
256
|
+
const scaledThickness = thickness * Math.abs(realToCanvasMat.a)
|
|
257
|
+
|
|
258
|
+
ctx.save()
|
|
259
|
+
ctx.translate(cx, cy)
|
|
260
|
+
if (rotation !== 0) {
|
|
261
|
+
ctx.rotate((-rotation * Math.PI) / 180)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
ctx.beginPath()
|
|
265
|
+
drawPillPath({
|
|
266
|
+
ctx,
|
|
267
|
+
cx: 0,
|
|
268
|
+
cy: 0,
|
|
269
|
+
width: scaledWidth,
|
|
270
|
+
height: scaledHeight,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const innerWidth = scaledWidth - scaledThickness * 2
|
|
274
|
+
const innerHeight = scaledHeight - scaledThickness * 2
|
|
275
|
+
if (innerWidth > 0 && innerHeight > 0) {
|
|
276
|
+
drawPillPath({
|
|
277
|
+
ctx,
|
|
278
|
+
cx: 0,
|
|
279
|
+
cy: 0,
|
|
280
|
+
width: innerWidth,
|
|
281
|
+
height: innerHeight,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ctx.fill("evenodd")
|
|
286
|
+
ctx.restore()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { AnyCircuitElement } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import type { CanvasContext, PcbColorMap } from "../../types"
|
|
4
|
+
import { drawBoardSoldermask } from "./board"
|
|
5
|
+
import { processCutoutSoldermask } from "./cutout"
|
|
6
|
+
import { processHoleSoldermask } from "./hole"
|
|
7
|
+
import { processPlatedHoleSoldermask } from "./plated-hole"
|
|
8
|
+
import { processSmtPadSoldermask } from "./smt-pad"
|
|
9
|
+
import { processViaSoldermask } from "./via"
|
|
10
|
+
|
|
11
|
+
export interface DrawPcbSoldermaskParams {
|
|
12
|
+
ctx: CanvasContext
|
|
13
|
+
board: import("circuit-json").PcbBoard
|
|
14
|
+
elements: AnyCircuitElement[]
|
|
15
|
+
realToCanvasMat: Matrix
|
|
16
|
+
colorMap: PcbColorMap
|
|
17
|
+
layer: "top" | "bottom"
|
|
18
|
+
showSoldermask: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Draws the soldermask layer for the PCB as a unified geometry.
|
|
23
|
+
*
|
|
24
|
+
* The soldermask is drawn as a single unified layer that covers the entire board.
|
|
25
|
+
* Elements "cut through" the soldermask by drawing on top with appropriate colors:
|
|
26
|
+
*
|
|
27
|
+
* 1. Draw full soldermask covering the board (dark green)
|
|
28
|
+
* 2. For each element that needs a soldermask opening:
|
|
29
|
+
* - If positive margin: draw substrate color for the larger area, then copper color for pad
|
|
30
|
+
* - If zero margin: draw copper color for the pad area
|
|
31
|
+
* - If negative margin: draw copper color for the pad, then light green ring for margin
|
|
32
|
+
* 3. For elements with is_covered_with_soldermask: draw light green soldermask over them
|
|
33
|
+
*
|
|
34
|
+
* Note: This approach draws colors ON TOP of the soldermask rather than using
|
|
35
|
+
* destination-out compositing. This is necessary because some elements (plated holes,
|
|
36
|
+
* vias, non-plated holes) are drawn AFTER the soldermask layer, so cutting through
|
|
37
|
+
* the soldermask wouldn't reveal anything useful underneath.
|
|
38
|
+
*/
|
|
39
|
+
export function drawPcbSoldermask(params: DrawPcbSoldermaskParams): void {
|
|
40
|
+
const {
|
|
41
|
+
ctx,
|
|
42
|
+
board,
|
|
43
|
+
elements,
|
|
44
|
+
realToCanvasMat,
|
|
45
|
+
colorMap,
|
|
46
|
+
layer,
|
|
47
|
+
showSoldermask,
|
|
48
|
+
} = params
|
|
49
|
+
|
|
50
|
+
const soldermaskColor = colorMap.soldermask[layer] ?? colorMap.soldermask.top
|
|
51
|
+
const soldermaskOverCopperColor =
|
|
52
|
+
colorMap.soldermaskOverCopper[layer] ?? colorMap.soldermaskOverCopper.top
|
|
53
|
+
|
|
54
|
+
// Step 1: Draw the full soldermask covering the board (only if enabled)
|
|
55
|
+
if (showSoldermask) {
|
|
56
|
+
drawBoardSoldermask({ ctx, board, realToCanvasMat, soldermaskColor })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 2: Process each element - draw cutouts and light green areas as needed
|
|
60
|
+
for (const element of elements) {
|
|
61
|
+
processElementSoldermask({
|
|
62
|
+
ctx,
|
|
63
|
+
element,
|
|
64
|
+
realToCanvasMat,
|
|
65
|
+
colorMap,
|
|
66
|
+
soldermaskOverCopperColor,
|
|
67
|
+
layer,
|
|
68
|
+
showSoldermask,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Process soldermask for an element by drawing on top of the soldermask layer.
|
|
75
|
+
* This simulates cutouts by drawing substrate/copper colors over the soldermask.
|
|
76
|
+
*/
|
|
77
|
+
function processElementSoldermask(params: {
|
|
78
|
+
ctx: CanvasContext
|
|
79
|
+
element: AnyCircuitElement
|
|
80
|
+
realToCanvasMat: Matrix
|
|
81
|
+
colorMap: PcbColorMap
|
|
82
|
+
soldermaskOverCopperColor: string
|
|
83
|
+
layer: "top" | "bottom"
|
|
84
|
+
showSoldermask: boolean
|
|
85
|
+
}): void {
|
|
86
|
+
const {
|
|
87
|
+
ctx,
|
|
88
|
+
element,
|
|
89
|
+
realToCanvasMat,
|
|
90
|
+
colorMap,
|
|
91
|
+
soldermaskOverCopperColor,
|
|
92
|
+
layer,
|
|
93
|
+
showSoldermask,
|
|
94
|
+
} = params
|
|
95
|
+
|
|
96
|
+
if (element.type === "pcb_smtpad") {
|
|
97
|
+
processSmtPadSoldermask({
|
|
98
|
+
ctx,
|
|
99
|
+
pad: element,
|
|
100
|
+
realToCanvasMat,
|
|
101
|
+
colorMap,
|
|
102
|
+
soldermaskOverCopperColor,
|
|
103
|
+
layer,
|
|
104
|
+
showSoldermask,
|
|
105
|
+
})
|
|
106
|
+
} else if (element.type === "pcb_plated_hole") {
|
|
107
|
+
processPlatedHoleSoldermask({
|
|
108
|
+
ctx,
|
|
109
|
+
hole: element,
|
|
110
|
+
realToCanvasMat,
|
|
111
|
+
colorMap,
|
|
112
|
+
soldermaskOverCopperColor,
|
|
113
|
+
layer,
|
|
114
|
+
showSoldermask,
|
|
115
|
+
})
|
|
116
|
+
} else if (element.type === "pcb_hole") {
|
|
117
|
+
processHoleSoldermask({
|
|
118
|
+
ctx,
|
|
119
|
+
hole: element,
|
|
120
|
+
realToCanvasMat,
|
|
121
|
+
colorMap,
|
|
122
|
+
soldermaskOverCopperColor,
|
|
123
|
+
showSoldermask,
|
|
124
|
+
})
|
|
125
|
+
} else if (element.type === "pcb_via") {
|
|
126
|
+
processViaSoldermask({
|
|
127
|
+
ctx,
|
|
128
|
+
via: element,
|
|
129
|
+
realToCanvasMat,
|
|
130
|
+
colorMap,
|
|
131
|
+
})
|
|
132
|
+
} else if (element.type === "pcb_cutout") {
|
|
133
|
+
processCutoutSoldermask({
|
|
134
|
+
ctx,
|
|
135
|
+
cutout: element,
|
|
136
|
+
realToCanvasMat,
|
|
137
|
+
colorMap,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|