circuit-to-canvas 0.0.30 → 0.0.32
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 +41 -5
- package/dist/index.js +456 -41
- package/lib/drawer/CircuitToCanvasDrawer.ts +116 -2
- package/lib/drawer/elements/index.ts +10 -0
- package/lib/drawer/elements/pcb-hole.ts +2 -2
- package/lib/drawer/elements/pcb-plated-hole.ts +6 -6
- package/lib/drawer/elements/pcb-silkscreen-oval.ts +35 -0
- package/lib/drawer/elements/pcb-smtpad.ts +174 -0
- package/lib/drawer/elements/soldermask-margin.ts +266 -0
- package/lib/drawer/shapes/oval.ts +25 -10
- package/package.json +3 -3
- package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
- package/tests/elements/pcb-silkscreen-oval.test.ts +37 -0
- package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +163 -0
- package/tests/shapes/__snapshots__/oval.snap.png +0 -0
- package/tests/shapes/oval.test.ts +3 -2
|
@@ -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
|
+
}
|
|
@@ -5,9 +5,11 @@ import type { CanvasContext } from "../types"
|
|
|
5
5
|
export interface DrawOvalParams {
|
|
6
6
|
ctx: CanvasContext
|
|
7
7
|
center: { x: number; y: number }
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
fill
|
|
8
|
+
radius_x: number
|
|
9
|
+
radius_y: number
|
|
10
|
+
fill?: string
|
|
11
|
+
stroke?: string
|
|
12
|
+
strokeWidth?: number
|
|
11
13
|
realToCanvasMat: Matrix
|
|
12
14
|
rotation?: number
|
|
13
15
|
}
|
|
@@ -16,16 +18,19 @@ export function drawOval(params: DrawOvalParams): void {
|
|
|
16
18
|
const {
|
|
17
19
|
ctx,
|
|
18
20
|
center,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
radius_x,
|
|
22
|
+
radius_y,
|
|
21
23
|
fill,
|
|
24
|
+
stroke,
|
|
25
|
+
strokeWidth = 0.1,
|
|
22
26
|
realToCanvasMat,
|
|
23
27
|
rotation = 0,
|
|
24
28
|
} = params
|
|
25
29
|
|
|
26
30
|
const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
|
|
27
|
-
const
|
|
28
|
-
const
|
|
31
|
+
const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a)
|
|
32
|
+
const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a)
|
|
33
|
+
const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a)
|
|
29
34
|
|
|
30
35
|
ctx.save()
|
|
31
36
|
ctx.translate(cx, cy)
|
|
@@ -35,8 +40,18 @@ export function drawOval(params: DrawOvalParams): void {
|
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
ctx.beginPath()
|
|
38
|
-
ctx.ellipse(0, 0,
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2)
|
|
44
|
+
|
|
45
|
+
if (fill) {
|
|
46
|
+
ctx.fillStyle = fill
|
|
47
|
+
ctx.fill()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (stroke) {
|
|
51
|
+
ctx.strokeStyle = stroke
|
|
52
|
+
ctx.lineWidth = scaledStrokeWidth
|
|
53
|
+
ctx.stroke()
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
ctx.restore()
|
|
42
57
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circuit-to-canvas",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.32",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"@tscircuit/math-utils": "^0.0.29",
|
|
18
18
|
"@types/bun": "latest",
|
|
19
19
|
"bun-match-svg": "^0.0.14",
|
|
20
|
-
"circuit-json": "^0.0.
|
|
20
|
+
"circuit-json": "^0.0.348",
|
|
21
21
|
"circuit-json-to-connectivity-map": "^0.0.23",
|
|
22
|
-
"circuit-to-svg": "^0.0.
|
|
22
|
+
"circuit-to-svg": "^0.0.303",
|
|
23
23
|
"looks-same": "^10.0.1",
|
|
24
24
|
"schematic-symbols": "^0.0.202",
|
|
25
25
|
"tsup": "^8.5.1"
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import type { AnyCircuitElement, PcbSilkscreenOval } from "circuit-json"
|
|
3
|
+
import { getStackedPngSvgComparison } from "../fixtures/getStackedPngSvgComparison"
|
|
4
|
+
|
|
5
|
+
test("draw silkscreen oval", async () => {
|
|
6
|
+
const oval: PcbSilkscreenOval = {
|
|
7
|
+
type: "pcb_silkscreen_oval",
|
|
8
|
+
layer: "top" as const,
|
|
9
|
+
pcb_component_id: "pcb_component_1",
|
|
10
|
+
pcb_silkscreen_oval_id: "oval_1",
|
|
11
|
+
center: { x: 0, y: 0 },
|
|
12
|
+
radius_x: 2,
|
|
13
|
+
radius_y: 1,
|
|
14
|
+
ccw_rotation: 45,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const circuitJson: AnyCircuitElement[] = [
|
|
18
|
+
{
|
|
19
|
+
type: "pcb_board",
|
|
20
|
+
pcb_board_id: "board1",
|
|
21
|
+
center: { x: 0, y: 0 },
|
|
22
|
+
width: 10,
|
|
23
|
+
height: 10,
|
|
24
|
+
thickness: 1.6,
|
|
25
|
+
num_layers: 2,
|
|
26
|
+
material: "fr4",
|
|
27
|
+
},
|
|
28
|
+
oval,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const stackedPng = await getStackedPngSvgComparison(circuitJson, {
|
|
32
|
+
width: 400,
|
|
33
|
+
height: 800,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await expect(stackedPng).toMatchPngSnapshot(import.meta.path)
|
|
37
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
4
|
+
|
|
5
|
+
test("draw smt pads with positive and negative soldermask margins", async () => {
|
|
6
|
+
const canvas = createCanvas(800, 600)
|
|
7
|
+
const ctx = canvas.getContext("2d")
|
|
8
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
9
|
+
|
|
10
|
+
ctx.fillStyle = "#1a1a1a"
|
|
11
|
+
ctx.fillRect(0, 0, 800, 600)
|
|
12
|
+
|
|
13
|
+
const circuit: any = [
|
|
14
|
+
{
|
|
15
|
+
type: "pcb_board",
|
|
16
|
+
pcb_board_id: "board0",
|
|
17
|
+
center: { x: 0, y: 0 },
|
|
18
|
+
width: 14,
|
|
19
|
+
height: 10,
|
|
20
|
+
},
|
|
21
|
+
// Rectangle with positive margin (mask extends beyond pad)
|
|
22
|
+
{
|
|
23
|
+
type: "pcb_smtpad",
|
|
24
|
+
pcb_smtpad_id: "pad_rect_positive",
|
|
25
|
+
shape: "rect",
|
|
26
|
+
layer: "top",
|
|
27
|
+
x: -4,
|
|
28
|
+
y: 2,
|
|
29
|
+
width: 1.6,
|
|
30
|
+
height: 1.1,
|
|
31
|
+
is_covered_with_solder_mask: true,
|
|
32
|
+
soldermask_margin: 0.2,
|
|
33
|
+
},
|
|
34
|
+
// Rectangle with negative margin (spacing around copper, copper visible)
|
|
35
|
+
{
|
|
36
|
+
type: "pcb_smtpad",
|
|
37
|
+
pcb_smtpad_id: "pad_rect_negative",
|
|
38
|
+
shape: "rect",
|
|
39
|
+
layer: "top",
|
|
40
|
+
x: -4,
|
|
41
|
+
y: -2,
|
|
42
|
+
width: 1.6,
|
|
43
|
+
height: 1.1,
|
|
44
|
+
is_covered_with_solder_mask: true,
|
|
45
|
+
soldermask_margin: -0.15,
|
|
46
|
+
},
|
|
47
|
+
// Circle with positive margin
|
|
48
|
+
{
|
|
49
|
+
type: "pcb_smtpad",
|
|
50
|
+
pcb_smtpad_id: "pad_circle_positive",
|
|
51
|
+
shape: "circle",
|
|
52
|
+
layer: "top",
|
|
53
|
+
x: 0,
|
|
54
|
+
y: 2,
|
|
55
|
+
radius: 0.75,
|
|
56
|
+
is_covered_with_solder_mask: true,
|
|
57
|
+
soldermask_margin: 0.15,
|
|
58
|
+
},
|
|
59
|
+
// Circle with negative margin
|
|
60
|
+
{
|
|
61
|
+
type: "pcb_smtpad",
|
|
62
|
+
pcb_smtpad_id: "pad_circle_negative",
|
|
63
|
+
shape: "circle",
|
|
64
|
+
layer: "top",
|
|
65
|
+
x: 0,
|
|
66
|
+
y: -2,
|
|
67
|
+
radius: 0.75,
|
|
68
|
+
is_covered_with_solder_mask: true,
|
|
69
|
+
soldermask_margin: -0.2,
|
|
70
|
+
},
|
|
71
|
+
// Pill with positive margin
|
|
72
|
+
{
|
|
73
|
+
type: "pcb_smtpad",
|
|
74
|
+
pcb_smtpad_id: "pad_pill_positive",
|
|
75
|
+
shape: "pill",
|
|
76
|
+
layer: "top",
|
|
77
|
+
x: 4,
|
|
78
|
+
y: 2,
|
|
79
|
+
width: 2.4,
|
|
80
|
+
height: 1,
|
|
81
|
+
radius: 0.5,
|
|
82
|
+
is_covered_with_solder_mask: true,
|
|
83
|
+
soldermask_margin: 0.1,
|
|
84
|
+
},
|
|
85
|
+
// Pill with negative margin
|
|
86
|
+
{
|
|
87
|
+
type: "pcb_smtpad",
|
|
88
|
+
pcb_smtpad_id: "pad_pill_negative",
|
|
89
|
+
shape: "pill",
|
|
90
|
+
layer: "top",
|
|
91
|
+
x: 4,
|
|
92
|
+
y: -2,
|
|
93
|
+
width: 2.4,
|
|
94
|
+
height: 1,
|
|
95
|
+
radius: 0.5,
|
|
96
|
+
is_covered_with_solder_mask: true,
|
|
97
|
+
soldermask_margin: -0.12,
|
|
98
|
+
},
|
|
99
|
+
// Silkscreen labels for positive margin pads (top row)
|
|
100
|
+
{
|
|
101
|
+
type: "pcb_silkscreen_text",
|
|
102
|
+
pcb_silkscreen_text_id: "text_rect_pos",
|
|
103
|
+
layer: "top",
|
|
104
|
+
anchor_position: { x: -4, y: 3.2 },
|
|
105
|
+
anchor_alignment: "center",
|
|
106
|
+
text: "+0.2mm",
|
|
107
|
+
font_size: 0.4,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "pcb_silkscreen_text",
|
|
111
|
+
pcb_silkscreen_text_id: "text_circle_pos",
|
|
112
|
+
layer: "top",
|
|
113
|
+
anchor_position: { x: 0, y: 3.2 },
|
|
114
|
+
anchor_alignment: "center",
|
|
115
|
+
text: "+0.15mm",
|
|
116
|
+
font_size: 0.4,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "pcb_silkscreen_text",
|
|
120
|
+
pcb_silkscreen_text_id: "text_pill_pos",
|
|
121
|
+
layer: "top",
|
|
122
|
+
anchor_position: { x: 4, y: 3.2 },
|
|
123
|
+
anchor_alignment: "center",
|
|
124
|
+
text: "+0.1mm",
|
|
125
|
+
font_size: 0.4,
|
|
126
|
+
},
|
|
127
|
+
// Silkscreen labels for negative margin pads (bottom row)
|
|
128
|
+
{
|
|
129
|
+
type: "pcb_silkscreen_text",
|
|
130
|
+
pcb_silkscreen_text_id: "text_rect_neg",
|
|
131
|
+
layer: "top",
|
|
132
|
+
anchor_position: { x: -4, y: -3.2 },
|
|
133
|
+
anchor_alignment: "center",
|
|
134
|
+
text: "-0.15mm",
|
|
135
|
+
font_size: 0.4,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: "pcb_silkscreen_text",
|
|
139
|
+
pcb_silkscreen_text_id: "text_circle_neg",
|
|
140
|
+
layer: "top",
|
|
141
|
+
anchor_position: { x: 0, y: -3.2 },
|
|
142
|
+
anchor_alignment: "center",
|
|
143
|
+
text: "-0.2mm",
|
|
144
|
+
font_size: 0.4,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "pcb_silkscreen_text",
|
|
148
|
+
pcb_silkscreen_text_id: "text_pill_neg",
|
|
149
|
+
layer: "top",
|
|
150
|
+
anchor_position: { x: 4, y: -3.2 },
|
|
151
|
+
anchor_alignment: "center",
|
|
152
|
+
text: "-0.12mm",
|
|
153
|
+
font_size: 0.4,
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
drawer.setCameraBounds({ minX: -7, maxX: 7, minY: -5, maxY: 5 })
|
|
158
|
+
drawer.drawElements(circuit)
|
|
159
|
+
|
|
160
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
161
|
+
import.meta.path,
|
|
162
|
+
)
|
|
163
|
+
})
|
|
Binary file
|
|
@@ -13,10 +13,11 @@ test("draw oval", async () => {
|
|
|
13
13
|
drawOval({
|
|
14
14
|
ctx,
|
|
15
15
|
center: { x: 50, y: 50 },
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
radius_x: 50,
|
|
17
|
+
radius_y: 25,
|
|
18
18
|
fill: "#0000ff",
|
|
19
19
|
realToCanvasMat: identity(),
|
|
20
|
+
rotation: 45,
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|