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.
@@ -10,6 +10,7 @@ import type {
10
10
  PcbSilkscreenRect,
11
11
  PcbSilkscreenCircle,
12
12
  PcbSilkscreenLine,
13
+ PcbSilkscreenOval,
13
14
  PcbSilkscreenPath,
14
15
  PcbSilkscreenPill,
15
16
  PcbCutout,
@@ -25,8 +26,14 @@ import type {
25
26
  PcbNoteLine,
26
27
  PcbRenderLayer,
27
28
  } from "circuit-json"
29
+ import {
30
+ identity,
31
+ compose,
32
+ translate,
33
+ scale,
34
+ applyToPoint,
35
+ } from "transformation-matrix"
28
36
  import { shouldDrawElement } from "./pcb-render-layer-filter"
29
- import { identity, compose, translate, scale } from "transformation-matrix"
30
37
  import type { Matrix } from "transformation-matrix"
31
38
  import {
32
39
  type CanvasContext,
@@ -41,11 +48,14 @@ import { drawPcbHole } from "./elements/pcb-hole"
41
48
  import { drawPcbSmtPad } from "./elements/pcb-smtpad"
42
49
  import { drawPcbTrace } from "./elements/pcb-trace"
43
50
  import { drawPcbBoard } from "./elements/pcb-board"
51
+ import { drawPath } from "./shapes/path"
52
+ import { drawRect } from "./shapes/rect"
44
53
  import { drawPcbSilkscreenText } from "./elements/pcb-silkscreen-text"
45
54
  import { drawPcbSilkscreenRect } from "./elements/pcb-silkscreen-rect"
46
55
  import { drawPcbSilkscreenCircle } from "./elements/pcb-silkscreen-circle"
47
56
  import { drawPcbSilkscreenLine } from "./elements/pcb-silkscreen-line"
48
57
  import { drawPcbSilkscreenPath } from "./elements/pcb-silkscreen-path"
58
+ import { drawPcbSilkscreenOval } from "./elements/pcb-silkscreen-oval"
49
59
  import { drawPcbSilkscreenPill } from "./elements/pcb-silkscreen-pill"
50
60
  import { drawPcbCutout } from "./elements/pcb-cutout"
51
61
  import { drawPcbCopperPour } from "./elements/pcb-copper-pour"
@@ -148,8 +158,103 @@ export class CircuitToCanvasDrawer {
148
158
  elements: AnyCircuitElement[],
149
159
  options: DrawElementsOptions = {},
150
160
  ): void {
161
+ // Check if any pad has is_covered_with_solder_mask: true
162
+ const hasSoldermaskPads = elements.some(
163
+ (el) =>
164
+ el.type === "pcb_smtpad" &&
165
+ (el as PcbSmtPad).is_covered_with_solder_mask === true,
166
+ )
167
+
151
168
  for (const element of elements) {
152
- this.drawElement(element, options)
169
+ if (element.type === "pcb_board" && hasSoldermaskPads) {
170
+ // Draw board with soldermask fill when pads have soldermask
171
+ this.drawBoardWithSoldermask(element as PcbBoard)
172
+ } else {
173
+ this.drawElement(element, options)
174
+ }
175
+ }
176
+ }
177
+
178
+ private drawBoardWithSoldermask(board: PcbBoard): void {
179
+ const { width, height, center, outline } = board
180
+ const layer = "top" // Default to top layer for soldermask color
181
+
182
+ // If the board has a custom outline, draw it as a path with soldermask fill
183
+ if (outline && Array.isArray(outline) && outline.length >= 3) {
184
+ const soldermaskColor =
185
+ this.colorMap.soldermask[
186
+ layer as keyof typeof this.colorMap.soldermask
187
+ ] ?? this.colorMap.soldermask.top
188
+
189
+ // Draw filled path
190
+ const canvasPoints = outline.map((p) => {
191
+ const [x, y] = applyToPoint(this.realToCanvasMat, [p.x, p.y])
192
+ return { x, y }
193
+ })
194
+
195
+ this.ctx.beginPath()
196
+ const firstPoint = canvasPoints[0]
197
+ if (firstPoint) {
198
+ this.ctx.moveTo(firstPoint.x, firstPoint.y)
199
+ for (let i = 1; i < canvasPoints.length; i++) {
200
+ const point = canvasPoints[i]
201
+ if (point) {
202
+ this.ctx.lineTo(point.x, point.y)
203
+ }
204
+ }
205
+ this.ctx.closePath()
206
+ }
207
+
208
+ this.ctx.fillStyle = soldermaskColor
209
+ this.ctx.fill()
210
+
211
+ // Draw outline stroke
212
+ drawPath({
213
+ ctx: this.ctx,
214
+ points: outline.map((p) => ({ x: p.x, y: p.y })),
215
+ stroke: this.colorMap.boardOutline,
216
+ strokeWidth: 0.1,
217
+ realToCanvasMat: this.realToCanvasMat,
218
+ closePath: true,
219
+ })
220
+ return
221
+ }
222
+
223
+ // Otherwise draw a rectangle with soldermask fill
224
+ if (width !== undefined && height !== undefined && center) {
225
+ const soldermaskColor =
226
+ this.colorMap.soldermask[
227
+ layer as keyof typeof this.colorMap.soldermask
228
+ ] ?? this.colorMap.soldermask.top
229
+
230
+ // Draw filled rectangle
231
+ drawRect({
232
+ ctx: this.ctx,
233
+ center,
234
+ width,
235
+ height,
236
+ fill: soldermaskColor,
237
+ realToCanvasMat: this.realToCanvasMat,
238
+ })
239
+
240
+ // Draw the outline stroke separately using path
241
+ const halfWidth = width / 2
242
+ const halfHeight = height / 2
243
+ const corners = [
244
+ { x: center.x - halfWidth, y: center.y - halfHeight },
245
+ { x: center.x + halfWidth, y: center.y - halfHeight },
246
+ { x: center.x + halfWidth, y: center.y + halfHeight },
247
+ { x: center.x - halfWidth, y: center.y + halfHeight },
248
+ ]
249
+
250
+ drawPath({
251
+ ctx: this.ctx,
252
+ points: corners,
253
+ stroke: this.colorMap.boardOutline,
254
+ strokeWidth: 0.1,
255
+ realToCanvasMat: this.realToCanvasMat,
256
+ closePath: true,
257
+ })
153
258
  }
154
259
  }
155
260
 
@@ -270,6 +375,15 @@ export class CircuitToCanvasDrawer {
270
375
  })
271
376
  }
272
377
 
378
+ if (element.type === "pcb_silkscreen_oval") {
379
+ drawPcbSilkscreenOval({
380
+ ctx: this.ctx,
381
+ oval: element as PcbSilkscreenOval,
382
+ realToCanvasMat: this.realToCanvasMat,
383
+ colorMap: this.colorMap,
384
+ })
385
+ }
386
+
273
387
  if (element.type === "pcb_cutout") {
274
388
  drawPcbCutout({
275
389
  ctx: this.ctx,
@@ -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
 
@@ -43,6 +48,11 @@ export {
43
48
  type DrawPcbSilkscreenPillParams,
44
49
  } from "./pcb-silkscreen-pill"
45
50
 
51
+ export {
52
+ drawPcbSilkscreenOval,
53
+ type DrawPcbSilkscreenOvalParams,
54
+ } from "./pcb-silkscreen-oval"
55
+
46
56
  export { drawPcbCutout, type DrawPcbCutoutParams } from "./pcb-cutout"
47
57
 
48
58
  export {
@@ -43,8 +43,8 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
43
43
  drawOval({
44
44
  ctx,
45
45
  center: { x: hole.x, y: hole.y },
46
- width: hole.hole_width,
47
- height: hole.hole_height,
46
+ radius_x: hole.hole_width / 2,
47
+ radius_y: hole.hole_height / 2,
48
48
  fill: colorMap.drill,
49
49
  realToCanvasMat,
50
50
  })
@@ -43,8 +43,8 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
43
43
  drawOval({
44
44
  ctx,
45
45
  center: { x: hole.x, y: hole.y },
46
- width: hole.outer_width,
47
- height: hole.outer_height,
46
+ radius_x: hole.outer_width / 2,
47
+ radius_y: hole.outer_height / 2,
48
48
  fill: colorMap.copper.top,
49
49
  realToCanvasMat,
50
50
  rotation: hole.ccw_rotation,
@@ -54,8 +54,8 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
54
54
  drawOval({
55
55
  ctx,
56
56
  center: { x: hole.x, y: hole.y },
57
- width: hole.hole_width,
58
- height: hole.hole_height,
57
+ radius_x: hole.hole_width / 2,
58
+ radius_y: hole.hole_height / 2,
59
59
  fill: colorMap.drill,
60
60
  realToCanvasMat,
61
61
  rotation: hole.ccw_rotation,
@@ -201,8 +201,8 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
201
201
  drawOval({
202
202
  ctx,
203
203
  center: { x: holeX, y: holeY },
204
- width: hole.hole_width ?? 0,
205
- height: hole.hole_height ?? 0,
204
+ radius_x: (hole.hole_width ?? 0) / 2,
205
+ radius_y: (hole.hole_height ?? 0) / 2,
206
206
  fill: colorMap.drill,
207
207
  realToCanvasMat,
208
208
  })
@@ -0,0 +1,35 @@
1
+ import type { PcbSilkscreenOval } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import type { PcbColorMap, CanvasContext } from "../types"
4
+ import { drawOval } from "../shapes/oval"
5
+
6
+ export interface DrawPcbSilkscreenOvalParams {
7
+ ctx: CanvasContext
8
+ oval: PcbSilkscreenOval
9
+ realToCanvasMat: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
14
+ return layer === "bottom"
15
+ ? colorMap.silkscreen.bottom
16
+ : colorMap.silkscreen.top
17
+ }
18
+
19
+ export function drawPcbSilkscreenOval(
20
+ params: DrawPcbSilkscreenOvalParams,
21
+ ): void {
22
+ const { ctx, oval, realToCanvasMat, colorMap } = params
23
+ const color = layerToSilkscreenColor(oval.layer, colorMap)
24
+
25
+ drawOval({
26
+ ctx,
27
+ center: oval.center,
28
+ radius_x: oval.radius_x,
29
+ radius_y: oval.radius_y,
30
+ stroke: color,
31
+ strokeWidth: 0.1,
32
+ realToCanvasMat,
33
+ rotation: oval.ccw_rotation ?? 0,
34
+ })
35
+ }
@@ -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