circuit-to-canvas 0.0.30 → 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 +27 -1
- package/dist/index.js +401 -24
- package/lib/drawer/CircuitToCanvasDrawer.ts +105 -2
- 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/package.json +1 -1
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
- package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +163 -0
|
@@ -25,8 +25,14 @@ import type {
|
|
|
25
25
|
PcbNoteLine,
|
|
26
26
|
PcbRenderLayer,
|
|
27
27
|
} from "circuit-json"
|
|
28
|
+
import {
|
|
29
|
+
identity,
|
|
30
|
+
compose,
|
|
31
|
+
translate,
|
|
32
|
+
scale,
|
|
33
|
+
applyToPoint,
|
|
34
|
+
} from "transformation-matrix"
|
|
28
35
|
import { shouldDrawElement } from "./pcb-render-layer-filter"
|
|
29
|
-
import { identity, compose, translate, scale } from "transformation-matrix"
|
|
30
36
|
import type { Matrix } from "transformation-matrix"
|
|
31
37
|
import {
|
|
32
38
|
type CanvasContext,
|
|
@@ -41,6 +47,8 @@ import { drawPcbHole } from "./elements/pcb-hole"
|
|
|
41
47
|
import { drawPcbSmtPad } from "./elements/pcb-smtpad"
|
|
42
48
|
import { drawPcbTrace } from "./elements/pcb-trace"
|
|
43
49
|
import { drawPcbBoard } from "./elements/pcb-board"
|
|
50
|
+
import { drawPath } from "./shapes/path"
|
|
51
|
+
import { drawRect } from "./shapes/rect"
|
|
44
52
|
import { drawPcbSilkscreenText } from "./elements/pcb-silkscreen-text"
|
|
45
53
|
import { drawPcbSilkscreenRect } from "./elements/pcb-silkscreen-rect"
|
|
46
54
|
import { drawPcbSilkscreenCircle } from "./elements/pcb-silkscreen-circle"
|
|
@@ -148,8 +156,103 @@ export class CircuitToCanvasDrawer {
|
|
|
148
156
|
elements: AnyCircuitElement[],
|
|
149
157
|
options: DrawElementsOptions = {},
|
|
150
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
|
+
|
|
151
166
|
for (const element of elements) {
|
|
152
|
-
|
|
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
|
+
})
|
|
153
256
|
}
|
|
154
257
|
}
|
|
155
258
|
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED