circuit-to-canvas 0.0.29 → 0.0.31
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 +29 -3
- package/dist/index.js +415 -22
- package/lib/drawer/CircuitToCanvasDrawer.ts +119 -9
- package/lib/drawer/elements/index.ts +5 -0
- package/lib/drawer/elements/pcb-smtpad.ts +174 -0
- package/lib/drawer/elements/soldermask-margin.ts +266 -0
- package/lib/drawer/pcb-render-layer-filter.ts +30 -0
- package/package.json +3 -3
- package/tests/elements/__snapshots__/layer-filter.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
- package/tests/elements/layer-filter.test.ts +42 -0
- package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +163 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyCircuitElement,
|
|
3
3
|
PcbPlatedHole,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
PcbVia,
|
|
5
|
+
PcbHole,
|
|
6
6
|
PcbSmtPad,
|
|
7
|
-
|
|
7
|
+
PcbTrace,
|
|
8
8
|
PcbBoard,
|
|
9
9
|
PcbSilkscreenText,
|
|
10
10
|
PcbSilkscreenRect,
|
|
@@ -23,8 +23,16 @@ import type {
|
|
|
23
23
|
PcbNoteText,
|
|
24
24
|
PcbNoteDimension,
|
|
25
25
|
PcbNoteLine,
|
|
26
|
+
PcbRenderLayer,
|
|
26
27
|
} from "circuit-json"
|
|
27
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
identity,
|
|
30
|
+
compose,
|
|
31
|
+
translate,
|
|
32
|
+
scale,
|
|
33
|
+
applyToPoint,
|
|
34
|
+
} from "transformation-matrix"
|
|
35
|
+
import { shouldDrawElement } from "./pcb-render-layer-filter"
|
|
28
36
|
import type { Matrix } from "transformation-matrix"
|
|
29
37
|
import {
|
|
30
38
|
type CanvasContext,
|
|
@@ -39,6 +47,8 @@ import { drawPcbHole } from "./elements/pcb-hole"
|
|
|
39
47
|
import { drawPcbSmtPad } from "./elements/pcb-smtpad"
|
|
40
48
|
import { drawPcbTrace } from "./elements/pcb-trace"
|
|
41
49
|
import { drawPcbBoard } from "./elements/pcb-board"
|
|
50
|
+
import { drawPath } from "./shapes/path"
|
|
51
|
+
import { drawRect } from "./shapes/rect"
|
|
42
52
|
import { drawPcbSilkscreenText } from "./elements/pcb-silkscreen-text"
|
|
43
53
|
import { drawPcbSilkscreenRect } from "./elements/pcb-silkscreen-rect"
|
|
44
54
|
import { drawPcbSilkscreenCircle } from "./elements/pcb-silkscreen-circle"
|
|
@@ -58,7 +68,7 @@ import { drawPcbNoteDimension } from "./elements/pcb-note-dimension"
|
|
|
58
68
|
import { drawPcbNoteLine } from "./elements/pcb-note-line"
|
|
59
69
|
|
|
60
70
|
export interface DrawElementsOptions {
|
|
61
|
-
layers?:
|
|
71
|
+
layers?: PcbRenderLayer[]
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
interface CanvasLike {
|
|
@@ -146,8 +156,103 @@ export class CircuitToCanvasDrawer {
|
|
|
146
156
|
elements: AnyCircuitElement[],
|
|
147
157
|
options: DrawElementsOptions = {},
|
|
148
158
|
): void {
|
|
159
|
+
// Check if any pad has is_covered_with_solder_mask: true
|
|
160
|
+
const hasSoldermaskPads = elements.some(
|
|
161
|
+
(el) =>
|
|
162
|
+
el.type === "pcb_smtpad" &&
|
|
163
|
+
(el as PcbSmtPad).is_covered_with_solder_mask === true,
|
|
164
|
+
)
|
|
165
|
+
|
|
149
166
|
for (const element of elements) {
|
|
150
|
-
|
|
167
|
+
if (element.type === "pcb_board" && hasSoldermaskPads) {
|
|
168
|
+
// Draw board with soldermask fill when pads have soldermask
|
|
169
|
+
this.drawBoardWithSoldermask(element as PcbBoard)
|
|
170
|
+
} else {
|
|
171
|
+
this.drawElement(element, options)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private drawBoardWithSoldermask(board: PcbBoard): void {
|
|
177
|
+
const { width, height, center, outline } = board
|
|
178
|
+
const layer = "top" // Default to top layer for soldermask color
|
|
179
|
+
|
|
180
|
+
// If the board has a custom outline, draw it as a path with soldermask fill
|
|
181
|
+
if (outline && Array.isArray(outline) && outline.length >= 3) {
|
|
182
|
+
const soldermaskColor =
|
|
183
|
+
this.colorMap.soldermask[
|
|
184
|
+
layer as keyof typeof this.colorMap.soldermask
|
|
185
|
+
] ?? this.colorMap.soldermask.top
|
|
186
|
+
|
|
187
|
+
// Draw filled path
|
|
188
|
+
const canvasPoints = outline.map((p) => {
|
|
189
|
+
const [x, y] = applyToPoint(this.realToCanvasMat, [p.x, p.y])
|
|
190
|
+
return { x, y }
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
this.ctx.beginPath()
|
|
194
|
+
const firstPoint = canvasPoints[0]
|
|
195
|
+
if (firstPoint) {
|
|
196
|
+
this.ctx.moveTo(firstPoint.x, firstPoint.y)
|
|
197
|
+
for (let i = 1; i < canvasPoints.length; i++) {
|
|
198
|
+
const point = canvasPoints[i]
|
|
199
|
+
if (point) {
|
|
200
|
+
this.ctx.lineTo(point.x, point.y)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
this.ctx.closePath()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.ctx.fillStyle = soldermaskColor
|
|
207
|
+
this.ctx.fill()
|
|
208
|
+
|
|
209
|
+
// Draw outline stroke
|
|
210
|
+
drawPath({
|
|
211
|
+
ctx: this.ctx,
|
|
212
|
+
points: outline.map((p) => ({ x: p.x, y: p.y })),
|
|
213
|
+
stroke: this.colorMap.boardOutline,
|
|
214
|
+
strokeWidth: 0.1,
|
|
215
|
+
realToCanvasMat: this.realToCanvasMat,
|
|
216
|
+
closePath: true,
|
|
217
|
+
})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Otherwise draw a rectangle with soldermask fill
|
|
222
|
+
if (width !== undefined && height !== undefined && center) {
|
|
223
|
+
const soldermaskColor =
|
|
224
|
+
this.colorMap.soldermask[
|
|
225
|
+
layer as keyof typeof this.colorMap.soldermask
|
|
226
|
+
] ?? this.colorMap.soldermask.top
|
|
227
|
+
|
|
228
|
+
// Draw filled rectangle
|
|
229
|
+
drawRect({
|
|
230
|
+
ctx: this.ctx,
|
|
231
|
+
center,
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
fill: soldermaskColor,
|
|
235
|
+
realToCanvasMat: this.realToCanvasMat,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Draw the outline stroke separately using path
|
|
239
|
+
const halfWidth = width / 2
|
|
240
|
+
const halfHeight = height / 2
|
|
241
|
+
const corners = [
|
|
242
|
+
{ x: center.x - halfWidth, y: center.y - halfHeight },
|
|
243
|
+
{ x: center.x + halfWidth, y: center.y - halfHeight },
|
|
244
|
+
{ x: center.x + halfWidth, y: center.y + halfHeight },
|
|
245
|
+
{ x: center.x - halfWidth, y: center.y + halfHeight },
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
drawPath({
|
|
249
|
+
ctx: this.ctx,
|
|
250
|
+
points: corners,
|
|
251
|
+
stroke: this.colorMap.boardOutline,
|
|
252
|
+
strokeWidth: 0.1,
|
|
253
|
+
realToCanvasMat: this.realToCanvasMat,
|
|
254
|
+
closePath: true,
|
|
255
|
+
})
|
|
151
256
|
}
|
|
152
257
|
}
|
|
153
258
|
|
|
@@ -155,6 +260,11 @@ export class CircuitToCanvasDrawer {
|
|
|
155
260
|
element: AnyCircuitElement,
|
|
156
261
|
options: DrawElementsOptions,
|
|
157
262
|
): void {
|
|
263
|
+
// Check if element should be drawn based on layer options
|
|
264
|
+
if (!shouldDrawElement(element, options)) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
158
268
|
if (element.type === "pcb_plated_hole") {
|
|
159
269
|
drawPcbPlatedHole({
|
|
160
270
|
ctx: this.ctx,
|
|
@@ -167,7 +277,7 @@ export class CircuitToCanvasDrawer {
|
|
|
167
277
|
if (element.type === "pcb_via") {
|
|
168
278
|
drawPcbVia({
|
|
169
279
|
ctx: this.ctx,
|
|
170
|
-
via: element as
|
|
280
|
+
via: element as PcbVia,
|
|
171
281
|
realToCanvasMat: this.realToCanvasMat,
|
|
172
282
|
colorMap: this.colorMap,
|
|
173
283
|
})
|
|
@@ -176,7 +286,7 @@ export class CircuitToCanvasDrawer {
|
|
|
176
286
|
if (element.type === "pcb_hole") {
|
|
177
287
|
drawPcbHole({
|
|
178
288
|
ctx: this.ctx,
|
|
179
|
-
hole: element as
|
|
289
|
+
hole: element as PcbHole,
|
|
180
290
|
realToCanvasMat: this.realToCanvasMat,
|
|
181
291
|
colorMap: this.colorMap,
|
|
182
292
|
})
|
|
@@ -194,7 +304,7 @@ export class CircuitToCanvasDrawer {
|
|
|
194
304
|
if (element.type === "pcb_trace") {
|
|
195
305
|
drawPcbTrace({
|
|
196
306
|
ctx: this.ctx,
|
|
197
|
-
trace: element as
|
|
307
|
+
trace: element as PcbTrace,
|
|
198
308
|
realToCanvasMat: this.realToCanvasMat,
|
|
199
309
|
colorMap: this.colorMap,
|
|
200
310
|
})
|
|
@@ -8,6 +8,11 @@ export { drawPcbVia, type DrawPcbViaParams } from "./pcb-via"
|
|
|
8
8
|
export { drawPcbHole, type DrawPcbHoleParams } from "./pcb-hole"
|
|
9
9
|
|
|
10
10
|
export { drawPcbSmtPad, type DrawPcbSmtPadParams } from "./pcb-smtpad"
|
|
11
|
+
export {
|
|
12
|
+
drawSoldermaskRingForRect,
|
|
13
|
+
drawSoldermaskRingForCircle,
|
|
14
|
+
drawSoldermaskRingForPill,
|
|
15
|
+
} from "./soldermask-margin"
|
|
11
16
|
|
|
12
17
|
export { drawPcbTrace, type DrawPcbTraceParams } from "./pcb-trace"
|
|
13
18
|
|
|
@@ -5,6 +5,11 @@ import { drawCircle } from "../shapes/circle"
|
|
|
5
5
|
import { drawRect } from "../shapes/rect"
|
|
6
6
|
import { drawPill } from "../shapes/pill"
|
|
7
7
|
import { drawPolygon } from "../shapes/polygon"
|
|
8
|
+
import {
|
|
9
|
+
drawSoldermaskRingForRect,
|
|
10
|
+
drawSoldermaskRingForCircle,
|
|
11
|
+
drawSoldermaskRingForPill,
|
|
12
|
+
} from "./soldermask-margin"
|
|
8
13
|
|
|
9
14
|
export interface DrawPcbSmtPadParams {
|
|
10
15
|
ctx: CanvasContext
|
|
@@ -20,12 +25,45 @@ function layerToColor(layer: string, colorMap: PcbColorMap): string {
|
|
|
20
25
|
)
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
function getSoldermaskColor(layer: string, colorMap: PcbColorMap): string {
|
|
29
|
+
return (
|
|
30
|
+
colorMap.soldermaskOverCopper[
|
|
31
|
+
layer as keyof typeof colorMap.soldermaskOverCopper
|
|
32
|
+
] ?? colorMap.soldermaskOverCopper.top
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
24
37
|
const { ctx, pad, realToCanvasMat, colorMap } = params
|
|
25
38
|
|
|
26
39
|
const color = layerToColor(pad.layer, colorMap)
|
|
40
|
+
const hasSoldermask =
|
|
41
|
+
pad.is_covered_with_solder_mask === true &&
|
|
42
|
+
pad.soldermask_margin !== undefined &&
|
|
43
|
+
pad.soldermask_margin !== 0
|
|
44
|
+
const margin = hasSoldermask ? pad.soldermask_margin! : 0
|
|
45
|
+
const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap)
|
|
46
|
+
const positiveMarginColor = colorMap.substrate
|
|
27
47
|
|
|
48
|
+
// Draw the copper pad
|
|
28
49
|
if (pad.shape === "rect") {
|
|
50
|
+
// For positive margins, draw extended mask area first
|
|
51
|
+
if (hasSoldermask && margin > 0) {
|
|
52
|
+
drawRect({
|
|
53
|
+
ctx,
|
|
54
|
+
center: { x: pad.x, y: pad.y },
|
|
55
|
+
width: pad.width + margin * 2,
|
|
56
|
+
height: pad.height + margin * 2,
|
|
57
|
+
fill: positiveMarginColor,
|
|
58
|
+
realToCanvasMat,
|
|
59
|
+
borderRadius:
|
|
60
|
+
((pad as { corner_radius?: number }).corner_radius ??
|
|
61
|
+
pad.rect_border_radius ??
|
|
62
|
+
0) + margin,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Draw the pad on top
|
|
29
67
|
drawRect({
|
|
30
68
|
ctx,
|
|
31
69
|
center: { x: pad.x, y: pad.y },
|
|
@@ -38,10 +76,46 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
|
38
76
|
pad.rect_border_radius ??
|
|
39
77
|
0,
|
|
40
78
|
})
|
|
79
|
+
|
|
80
|
+
// For negative margins, draw soldermask ring on top of the pad
|
|
81
|
+
if (hasSoldermask && margin < 0) {
|
|
82
|
+
drawSoldermaskRingForRect(
|
|
83
|
+
ctx,
|
|
84
|
+
{ x: pad.x, y: pad.y },
|
|
85
|
+
pad.width,
|
|
86
|
+
pad.height,
|
|
87
|
+
margin,
|
|
88
|
+
(pad as { corner_radius?: number }).corner_radius ??
|
|
89
|
+
pad.rect_border_radius ??
|
|
90
|
+
0,
|
|
91
|
+
0,
|
|
92
|
+
realToCanvasMat,
|
|
93
|
+
soldermaskRingColor,
|
|
94
|
+
color,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
41
97
|
return
|
|
42
98
|
}
|
|
43
99
|
|
|
44
100
|
if (pad.shape === "rotated_rect") {
|
|
101
|
+
// For positive margins, draw extended mask area first
|
|
102
|
+
if (hasSoldermask && margin > 0) {
|
|
103
|
+
drawRect({
|
|
104
|
+
ctx,
|
|
105
|
+
center: { x: pad.x, y: pad.y },
|
|
106
|
+
width: pad.width + margin * 2,
|
|
107
|
+
height: pad.height + margin * 2,
|
|
108
|
+
fill: positiveMarginColor,
|
|
109
|
+
realToCanvasMat,
|
|
110
|
+
borderRadius:
|
|
111
|
+
((pad as { corner_radius?: number }).corner_radius ??
|
|
112
|
+
pad.rect_border_radius ??
|
|
113
|
+
0) + margin,
|
|
114
|
+
rotation: pad.ccw_rotation ?? 0,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Draw the pad on top
|
|
45
119
|
drawRect({
|
|
46
120
|
ctx,
|
|
47
121
|
center: { x: pad.x, y: pad.y },
|
|
@@ -55,10 +129,40 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
|
55
129
|
0,
|
|
56
130
|
rotation: pad.ccw_rotation ?? 0,
|
|
57
131
|
})
|
|
132
|
+
|
|
133
|
+
// For negative margins, draw soldermask ring on top of the pad
|
|
134
|
+
if (hasSoldermask && margin < 0) {
|
|
135
|
+
drawSoldermaskRingForRect(
|
|
136
|
+
ctx,
|
|
137
|
+
{ x: pad.x, y: pad.y },
|
|
138
|
+
pad.width,
|
|
139
|
+
pad.height,
|
|
140
|
+
margin,
|
|
141
|
+
(pad as { corner_radius?: number }).corner_radius ??
|
|
142
|
+
pad.rect_border_radius ??
|
|
143
|
+
0,
|
|
144
|
+
pad.ccw_rotation ?? 0,
|
|
145
|
+
realToCanvasMat,
|
|
146
|
+
soldermaskRingColor,
|
|
147
|
+
color,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
58
150
|
return
|
|
59
151
|
}
|
|
60
152
|
|
|
61
153
|
if (pad.shape === "circle") {
|
|
154
|
+
// For positive margins, draw extended mask area first
|
|
155
|
+
if (hasSoldermask && margin > 0) {
|
|
156
|
+
drawCircle({
|
|
157
|
+
ctx,
|
|
158
|
+
center: { x: pad.x, y: pad.y },
|
|
159
|
+
radius: pad.radius + margin,
|
|
160
|
+
fill: positiveMarginColor,
|
|
161
|
+
realToCanvasMat,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Draw the pad on top
|
|
62
166
|
drawCircle({
|
|
63
167
|
ctx,
|
|
64
168
|
center: { x: pad.x, y: pad.y },
|
|
@@ -66,10 +170,36 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
|
66
170
|
fill: color,
|
|
67
171
|
realToCanvasMat,
|
|
68
172
|
})
|
|
173
|
+
|
|
174
|
+
// For negative margins, draw soldermask ring on top of the pad
|
|
175
|
+
if (hasSoldermask && margin < 0) {
|
|
176
|
+
drawSoldermaskRingForCircle(
|
|
177
|
+
ctx,
|
|
178
|
+
{ x: pad.x, y: pad.y },
|
|
179
|
+
pad.radius,
|
|
180
|
+
margin,
|
|
181
|
+
realToCanvasMat,
|
|
182
|
+
soldermaskRingColor,
|
|
183
|
+
color,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
69
186
|
return
|
|
70
187
|
}
|
|
71
188
|
|
|
72
189
|
if (pad.shape === "pill") {
|
|
190
|
+
// For positive margins, draw extended mask area first
|
|
191
|
+
if (hasSoldermask && margin > 0) {
|
|
192
|
+
drawPill({
|
|
193
|
+
ctx,
|
|
194
|
+
center: { x: pad.x, y: pad.y },
|
|
195
|
+
width: pad.width + margin * 2,
|
|
196
|
+
height: pad.height + margin * 2,
|
|
197
|
+
fill: positiveMarginColor,
|
|
198
|
+
realToCanvasMat,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Draw the pad on top
|
|
73
203
|
drawPill({
|
|
74
204
|
ctx,
|
|
75
205
|
center: { x: pad.x, y: pad.y },
|
|
@@ -78,10 +208,39 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
|
78
208
|
fill: color,
|
|
79
209
|
realToCanvasMat,
|
|
80
210
|
})
|
|
211
|
+
|
|
212
|
+
// For negative margins, draw soldermask ring on top of the pad
|
|
213
|
+
if (hasSoldermask && margin < 0) {
|
|
214
|
+
drawSoldermaskRingForPill(
|
|
215
|
+
ctx,
|
|
216
|
+
{ x: pad.x, y: pad.y },
|
|
217
|
+
pad.width,
|
|
218
|
+
pad.height,
|
|
219
|
+
margin,
|
|
220
|
+
0,
|
|
221
|
+
realToCanvasMat,
|
|
222
|
+
soldermaskRingColor,
|
|
223
|
+
color,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
81
226
|
return
|
|
82
227
|
}
|
|
83
228
|
|
|
84
229
|
if (pad.shape === "rotated_pill") {
|
|
230
|
+
// For positive margins, draw extended mask area first
|
|
231
|
+
if (hasSoldermask && margin > 0) {
|
|
232
|
+
drawPill({
|
|
233
|
+
ctx,
|
|
234
|
+
center: { x: pad.x, y: pad.y },
|
|
235
|
+
width: pad.width + margin * 2,
|
|
236
|
+
height: pad.height + margin * 2,
|
|
237
|
+
fill: positiveMarginColor,
|
|
238
|
+
realToCanvasMat,
|
|
239
|
+
rotation: pad.ccw_rotation ?? 0,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Draw the pad on top
|
|
85
244
|
drawPill({
|
|
86
245
|
ctx,
|
|
87
246
|
center: { x: pad.x, y: pad.y },
|
|
@@ -91,6 +250,21 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
|
|
|
91
250
|
realToCanvasMat,
|
|
92
251
|
rotation: pad.ccw_rotation ?? 0,
|
|
93
252
|
})
|
|
253
|
+
|
|
254
|
+
// For negative margins, draw soldermask ring on top of the pad
|
|
255
|
+
if (hasSoldermask && margin < 0) {
|
|
256
|
+
drawSoldermaskRingForPill(
|
|
257
|
+
ctx,
|
|
258
|
+
{ x: pad.x, y: pad.y },
|
|
259
|
+
pad.width,
|
|
260
|
+
pad.height,
|
|
261
|
+
margin,
|
|
262
|
+
pad.ccw_rotation ?? 0,
|
|
263
|
+
realToCanvasMat,
|
|
264
|
+
soldermaskRingColor,
|
|
265
|
+
color,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
94
268
|
return
|
|
95
269
|
}
|
|
96
270
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
import type { CanvasContext } from "../types"
|
|
3
|
+
import { applyToPoint } from "transformation-matrix"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Draws a soldermask ring for rectangular shapes with negative margin
|
|
7
|
+
* (soldermask appears inside the pad boundary)
|
|
8
|
+
*/
|
|
9
|
+
export function drawSoldermaskRingForRect(
|
|
10
|
+
ctx: CanvasContext,
|
|
11
|
+
center: { x: number; y: number },
|
|
12
|
+
width: number,
|
|
13
|
+
height: number,
|
|
14
|
+
margin: number,
|
|
15
|
+
borderRadius: number,
|
|
16
|
+
rotation: number,
|
|
17
|
+
realToCanvasMat: Matrix,
|
|
18
|
+
soldermaskColor: string,
|
|
19
|
+
padColor: string,
|
|
20
|
+
): void {
|
|
21
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
|
|
22
|
+
const scaledWidth = width * Math.abs(realToCanvasMat.a)
|
|
23
|
+
const scaledHeight = height * Math.abs(realToCanvasMat.a)
|
|
24
|
+
const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
|
|
25
|
+
const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a)
|
|
26
|
+
|
|
27
|
+
ctx.save()
|
|
28
|
+
ctx.translate(cx, cy)
|
|
29
|
+
|
|
30
|
+
if (rotation !== 0) {
|
|
31
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// For negative margins, outer is pad boundary, inner is reduced by margin
|
|
35
|
+
// Use source-atop so the ring only appears on the pad
|
|
36
|
+
const prevCompositeOp = ctx.globalCompositeOperation
|
|
37
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
38
|
+
ctx.globalCompositeOperation = "source-atop"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Draw outer rectangle filled (at pad boundary)
|
|
42
|
+
const outerWidth = scaledWidth
|
|
43
|
+
const outerHeight = scaledHeight
|
|
44
|
+
const outerRadius = scaledRadius
|
|
45
|
+
|
|
46
|
+
ctx.beginPath()
|
|
47
|
+
if (outerRadius > 0) {
|
|
48
|
+
const x = -outerWidth / 2
|
|
49
|
+
const y = -outerHeight / 2
|
|
50
|
+
const r = Math.min(outerRadius, outerWidth / 2, outerHeight / 2)
|
|
51
|
+
|
|
52
|
+
ctx.moveTo(x + r, y)
|
|
53
|
+
ctx.lineTo(x + outerWidth - r, y)
|
|
54
|
+
ctx.arcTo(x + outerWidth, y, x + outerWidth, y + r, r)
|
|
55
|
+
ctx.lineTo(x + outerWidth, y + outerHeight - r)
|
|
56
|
+
ctx.arcTo(
|
|
57
|
+
x + outerWidth,
|
|
58
|
+
y + outerHeight,
|
|
59
|
+
x + outerWidth - r,
|
|
60
|
+
y + outerHeight,
|
|
61
|
+
r,
|
|
62
|
+
)
|
|
63
|
+
ctx.lineTo(x + r, y + outerHeight)
|
|
64
|
+
ctx.arcTo(x, y + outerHeight, x, y + outerHeight - r, r)
|
|
65
|
+
ctx.lineTo(x, y + r)
|
|
66
|
+
ctx.arcTo(x, y, x + r, y, r)
|
|
67
|
+
} else {
|
|
68
|
+
ctx.rect(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ctx.fillStyle = soldermaskColor
|
|
72
|
+
ctx.fill()
|
|
73
|
+
|
|
74
|
+
// Reset composite operation and restore pad color in inner area
|
|
75
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
76
|
+
ctx.globalCompositeOperation = prevCompositeOp || "source-over"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Restore pad color in inner rectangle (reduced by margin)
|
|
80
|
+
const innerWidth = scaledWidth - scaledMargin * 2
|
|
81
|
+
const innerHeight = scaledHeight - scaledMargin * 2
|
|
82
|
+
const innerRadius = Math.max(0, scaledRadius - scaledMargin)
|
|
83
|
+
|
|
84
|
+
if (innerWidth > 0 && innerHeight > 0) {
|
|
85
|
+
ctx.beginPath()
|
|
86
|
+
if (innerRadius > 0) {
|
|
87
|
+
const x = -innerWidth / 2
|
|
88
|
+
const y = -innerHeight / 2
|
|
89
|
+
const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2)
|
|
90
|
+
|
|
91
|
+
ctx.moveTo(x + r, y)
|
|
92
|
+
ctx.lineTo(x + innerWidth - r, y)
|
|
93
|
+
ctx.arcTo(x + innerWidth, y, x + innerWidth, y + r, r)
|
|
94
|
+
ctx.lineTo(x + innerWidth, y + innerHeight - r)
|
|
95
|
+
ctx.arcTo(
|
|
96
|
+
x + innerWidth,
|
|
97
|
+
y + innerHeight,
|
|
98
|
+
x + innerWidth - r,
|
|
99
|
+
y + innerHeight,
|
|
100
|
+
r,
|
|
101
|
+
)
|
|
102
|
+
ctx.lineTo(x + r, y + innerHeight)
|
|
103
|
+
ctx.arcTo(x, y + innerHeight, x, y + innerHeight - r, r)
|
|
104
|
+
ctx.lineTo(x, y + r)
|
|
105
|
+
ctx.arcTo(x, y, x + r, y, r)
|
|
106
|
+
} else {
|
|
107
|
+
ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ctx.fillStyle = padColor
|
|
111
|
+
ctx.fill()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ctx.restore()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Draws a soldermask ring for circular shapes with negative margin
|
|
119
|
+
* (soldermask appears inside the pad boundary)
|
|
120
|
+
*/
|
|
121
|
+
export function drawSoldermaskRingForCircle(
|
|
122
|
+
ctx: CanvasContext,
|
|
123
|
+
center: { x: number; y: number },
|
|
124
|
+
radius: number,
|
|
125
|
+
margin: number,
|
|
126
|
+
realToCanvasMat: Matrix,
|
|
127
|
+
soldermaskColor: string,
|
|
128
|
+
padColor: string,
|
|
129
|
+
): void {
|
|
130
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
|
|
131
|
+
const scaledRadius = radius * Math.abs(realToCanvasMat.a)
|
|
132
|
+
const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
|
|
133
|
+
|
|
134
|
+
ctx.save()
|
|
135
|
+
|
|
136
|
+
// For negative margins, outer is pad boundary, inner is reduced by margin
|
|
137
|
+
// Use source-atop so the ring only appears on the pad
|
|
138
|
+
const prevCompositeOp = ctx.globalCompositeOperation
|
|
139
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
140
|
+
ctx.globalCompositeOperation = "source-atop"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Draw outer circle filled (at pad boundary)
|
|
144
|
+
ctx.beginPath()
|
|
145
|
+
ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
|
|
146
|
+
ctx.fillStyle = soldermaskColor
|
|
147
|
+
ctx.fill()
|
|
148
|
+
|
|
149
|
+
// Reset composite operation and restore pad color in inner area
|
|
150
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
151
|
+
ctx.globalCompositeOperation = prevCompositeOp || "source-over"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Restore pad color in inner circle (reduced by margin)
|
|
155
|
+
const innerRadius = Math.max(0, scaledRadius - scaledMargin)
|
|
156
|
+
if (innerRadius > 0) {
|
|
157
|
+
ctx.beginPath()
|
|
158
|
+
ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2)
|
|
159
|
+
ctx.fillStyle = padColor
|
|
160
|
+
ctx.fill()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
ctx.restore()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Draws a soldermask ring for pill shapes with negative margin
|
|
168
|
+
* (soldermask appears inside the pad boundary)
|
|
169
|
+
*/
|
|
170
|
+
export function drawSoldermaskRingForPill(
|
|
171
|
+
ctx: CanvasContext,
|
|
172
|
+
center: { x: number; y: number },
|
|
173
|
+
width: number,
|
|
174
|
+
height: number,
|
|
175
|
+
margin: number,
|
|
176
|
+
rotation: number,
|
|
177
|
+
realToCanvasMat: Matrix,
|
|
178
|
+
soldermaskColor: string,
|
|
179
|
+
padColor: string,
|
|
180
|
+
): void {
|
|
181
|
+
const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
|
|
182
|
+
const scaledWidth = width * Math.abs(realToCanvasMat.a)
|
|
183
|
+
const scaledHeight = height * Math.abs(realToCanvasMat.a)
|
|
184
|
+
const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
|
|
185
|
+
|
|
186
|
+
ctx.save()
|
|
187
|
+
ctx.translate(cx, cy)
|
|
188
|
+
|
|
189
|
+
if (rotation !== 0) {
|
|
190
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// For negative margins, outer is pad boundary, inner is reduced by margin
|
|
194
|
+
// Use source-atop so the ring only appears on the pad
|
|
195
|
+
const prevCompositeOp = ctx.globalCompositeOperation
|
|
196
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
197
|
+
ctx.globalCompositeOperation = "source-atop"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Draw outer pill filled (at pad boundary)
|
|
201
|
+
const outerWidth = scaledWidth
|
|
202
|
+
const outerHeight = scaledHeight
|
|
203
|
+
|
|
204
|
+
ctx.beginPath()
|
|
205
|
+
|
|
206
|
+
if (outerWidth > outerHeight) {
|
|
207
|
+
const radius = outerHeight / 2
|
|
208
|
+
const straightLength = outerWidth - outerHeight
|
|
209
|
+
ctx.moveTo(-straightLength / 2, -radius)
|
|
210
|
+
ctx.lineTo(straightLength / 2, -radius)
|
|
211
|
+
ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2)
|
|
212
|
+
ctx.lineTo(-straightLength / 2, radius)
|
|
213
|
+
ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2)
|
|
214
|
+
} else if (outerHeight > outerWidth) {
|
|
215
|
+
const radius = outerWidth / 2
|
|
216
|
+
const straightLength = outerHeight - outerWidth
|
|
217
|
+
ctx.moveTo(radius, -straightLength / 2)
|
|
218
|
+
ctx.lineTo(radius, straightLength / 2)
|
|
219
|
+
ctx.arc(0, straightLength / 2, radius, 0, Math.PI)
|
|
220
|
+
ctx.lineTo(-radius, -straightLength / 2)
|
|
221
|
+
ctx.arc(0, -straightLength / 2, radius, Math.PI, 0)
|
|
222
|
+
} else {
|
|
223
|
+
ctx.arc(0, 0, outerWidth / 2, 0, Math.PI * 2)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ctx.fillStyle = soldermaskColor
|
|
227
|
+
ctx.fill()
|
|
228
|
+
|
|
229
|
+
// Reset composite operation and restore pad color in inner area
|
|
230
|
+
if (ctx.globalCompositeOperation !== undefined) {
|
|
231
|
+
ctx.globalCompositeOperation = prevCompositeOp || "source-over"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Restore pad color in inner pill (reduced by margin)
|
|
235
|
+
const innerWidth = scaledWidth - scaledMargin * 2
|
|
236
|
+
const innerHeight = scaledHeight - scaledMargin * 2
|
|
237
|
+
|
|
238
|
+
if (innerWidth > 0 && innerHeight > 0) {
|
|
239
|
+
ctx.beginPath()
|
|
240
|
+
|
|
241
|
+
if (innerWidth > innerHeight) {
|
|
242
|
+
const radius = innerHeight / 2
|
|
243
|
+
const straightLength = innerWidth - innerHeight
|
|
244
|
+
ctx.moveTo(-straightLength / 2, -radius)
|
|
245
|
+
ctx.lineTo(straightLength / 2, -radius)
|
|
246
|
+
ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2)
|
|
247
|
+
ctx.lineTo(-straightLength / 2, radius)
|
|
248
|
+
ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2)
|
|
249
|
+
} else if (innerHeight > innerWidth) {
|
|
250
|
+
const radius = innerWidth / 2
|
|
251
|
+
const straightLength = innerHeight - innerWidth
|
|
252
|
+
ctx.moveTo(radius, -straightLength / 2)
|
|
253
|
+
ctx.lineTo(radius, straightLength / 2)
|
|
254
|
+
ctx.arc(0, straightLength / 2, radius, 0, Math.PI)
|
|
255
|
+
ctx.lineTo(-radius, -straightLength / 2)
|
|
256
|
+
ctx.arc(0, -straightLength / 2, radius, Math.PI, 0)
|
|
257
|
+
} else {
|
|
258
|
+
ctx.arc(0, 0, innerWidth / 2, 0, Math.PI * 2)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ctx.fillStyle = padColor
|
|
262
|
+
ctx.fill()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ctx.restore()
|
|
266
|
+
}
|