circuit-to-canvas 0.0.36 → 0.0.38

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.
@@ -172,11 +172,17 @@ export class CircuitToCanvasDrawer {
172
172
  (el as PcbHole & { is_covered_with_solder_mask?: boolean })
173
173
  .is_covered_with_solder_mask === true,
174
174
  )
175
+ const hasSoldermaskPlatedHoles = elements.some(
176
+ (el) =>
177
+ el.type === "pcb_plated_hole" &&
178
+ (el as PcbPlatedHole & { is_covered_with_solder_mask?: boolean })
179
+ .is_covered_with_solder_mask === true,
180
+ )
175
181
 
176
182
  for (const element of elements) {
177
183
  if (
178
184
  element.type === "pcb_board" &&
179
- (hasSoldermaskPads || hasSoldermaskHoles)
185
+ (hasSoldermaskPads || hasSoldermaskHoles || hasSoldermaskPlatedHoles)
180
186
  ) {
181
187
  // Draw board with soldermask fill when pads or holes have soldermask
182
188
  this.drawBoardWithSoldermask(element as PcbBoard)
@@ -6,6 +6,12 @@ import { drawRect } from "../shapes/rect"
6
6
  import { drawOval } from "../shapes/oval"
7
7
  import { drawPill } from "../shapes/pill"
8
8
  import { drawPolygon } from "../shapes/polygon"
9
+ import {
10
+ drawSoldermaskRingForCircle,
11
+ drawSoldermaskRingForOval,
12
+ drawSoldermaskRingForPill,
13
+ drawSoldermaskRingForRect,
14
+ } from "./soldermask-margin"
9
15
 
10
16
  export interface DrawPcbPlatedHoleParams {
11
17
  ctx: CanvasContext
@@ -14,19 +20,64 @@ export interface DrawPcbPlatedHoleParams {
14
20
  colorMap: PcbColorMap
15
21
  }
16
22
 
23
+ function getSoldermaskColor(
24
+ layers: string[] | undefined,
25
+ colorMap: PcbColorMap,
26
+ ): string {
27
+ const layer = layers?.includes("top") ? "top" : "bottom"
28
+ return (
29
+ colorMap.soldermaskOverCopper[
30
+ layer as keyof typeof colorMap.soldermaskOverCopper
31
+ ] ?? colorMap.soldermaskOverCopper.top
32
+ )
33
+ }
34
+
17
35
  export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
18
36
  const { ctx, hole, realToCanvasMat, colorMap } = params
19
37
 
38
+ const hasSoldermask =
39
+ hole.is_covered_with_solder_mask === true &&
40
+ hole.soldermask_margin !== undefined &&
41
+ hole.soldermask_margin !== 0
42
+ const margin = hasSoldermask ? hole.soldermask_margin! : 0
43
+ const soldermaskRingColor = getSoldermaskColor(hole.layers, colorMap)
44
+ const positiveMarginColor = colorMap.substrate
45
+ const copperColor = colorMap.copper.top
46
+
20
47
  if (hole.shape === "circle") {
48
+ // For positive margins, draw extended mask area first
49
+ if (hasSoldermask && margin > 0) {
50
+ drawCircle({
51
+ ctx,
52
+ center: { x: hole.x, y: hole.y },
53
+ radius: hole.outer_diameter / 2 + margin,
54
+ fill: positiveMarginColor,
55
+ realToCanvasMat,
56
+ })
57
+ }
58
+
21
59
  // Draw outer copper ring
22
60
  drawCircle({
23
61
  ctx,
24
62
  center: { x: hole.x, y: hole.y },
25
63
  radius: hole.outer_diameter / 2,
26
- fill: colorMap.copper.top,
64
+ fill: copperColor,
27
65
  realToCanvasMat,
28
66
  })
29
67
 
68
+ // For negative margins, draw soldermask ring on top of the copper ring
69
+ if (hasSoldermask && margin < 0) {
70
+ drawSoldermaskRingForCircle(
71
+ ctx,
72
+ { x: hole.x, y: hole.y },
73
+ hole.outer_diameter / 2,
74
+ margin,
75
+ realToCanvasMat,
76
+ soldermaskRingColor,
77
+ copperColor,
78
+ )
79
+ }
80
+
30
81
  // Draw inner drill hole
31
82
  drawCircle({
32
83
  ctx,
@@ -39,17 +90,45 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
39
90
  }
40
91
 
41
92
  if (hole.shape === "oval") {
93
+ // For positive margins, draw extended mask area first
94
+ if (hasSoldermask && margin > 0) {
95
+ drawOval({
96
+ ctx,
97
+ center: { x: hole.x, y: hole.y },
98
+ radius_x: hole.outer_width / 2 + margin,
99
+ radius_y: hole.outer_height / 2 + margin,
100
+ fill: positiveMarginColor,
101
+ realToCanvasMat,
102
+ rotation: hole.ccw_rotation,
103
+ })
104
+ }
105
+
42
106
  // Draw outer copper oval
43
107
  drawOval({
44
108
  ctx,
45
109
  center: { x: hole.x, y: hole.y },
46
110
  radius_x: hole.outer_width / 2,
47
111
  radius_y: hole.outer_height / 2,
48
- fill: colorMap.copper.top,
112
+ fill: copperColor,
49
113
  realToCanvasMat,
50
114
  rotation: hole.ccw_rotation,
51
115
  })
52
116
 
117
+ // For negative margins, draw soldermask ring on top of the copper oval
118
+ if (hasSoldermask && margin < 0) {
119
+ drawSoldermaskRingForOval(
120
+ ctx,
121
+ { x: hole.x, y: hole.y },
122
+ hole.outer_width / 2,
123
+ hole.outer_height / 2,
124
+ margin,
125
+ hole.ccw_rotation ?? 0,
126
+ realToCanvasMat,
127
+ soldermaskRingColor,
128
+ copperColor,
129
+ )
130
+ }
131
+
53
132
  // Draw inner drill hole
54
133
  drawOval({
55
134
  ctx,
@@ -64,17 +143,45 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
64
143
  }
65
144
 
66
145
  if (hole.shape === "pill") {
146
+ // For positive margins, draw extended mask area first
147
+ if (hasSoldermask && margin > 0) {
148
+ drawPill({
149
+ ctx,
150
+ center: { x: hole.x, y: hole.y },
151
+ width: hole.outer_width + margin * 2,
152
+ height: hole.outer_height + margin * 2,
153
+ fill: positiveMarginColor,
154
+ realToCanvasMat,
155
+ rotation: hole.ccw_rotation,
156
+ })
157
+ }
158
+
67
159
  // Draw outer copper pill
68
160
  drawPill({
69
161
  ctx,
70
162
  center: { x: hole.x, y: hole.y },
71
163
  width: hole.outer_width,
72
164
  height: hole.outer_height,
73
- fill: colorMap.copper.top,
165
+ fill: copperColor,
74
166
  realToCanvasMat,
75
167
  rotation: hole.ccw_rotation,
76
168
  })
77
169
 
170
+ // For negative margins, draw soldermask ring on top of the copper pill
171
+ if (hasSoldermask && margin < 0) {
172
+ drawSoldermaskRingForPill(
173
+ ctx,
174
+ { x: hole.x, y: hole.y },
175
+ hole.outer_width,
176
+ hole.outer_height,
177
+ margin,
178
+ hole.ccw_rotation ?? 0,
179
+ realToCanvasMat,
180
+ soldermaskRingColor,
181
+ copperColor,
182
+ )
183
+ }
184
+
78
185
  // Draw inner drill hole
79
186
  drawPill({
80
187
  ctx,
@@ -89,17 +196,46 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
89
196
  }
90
197
 
91
198
  if (hole.shape === "circular_hole_with_rect_pad") {
199
+ // For positive margins, draw extended mask area first
200
+ if (hasSoldermask && margin > 0) {
201
+ drawRect({
202
+ ctx,
203
+ center: { x: hole.x, y: hole.y },
204
+ width: hole.rect_pad_width + margin * 2,
205
+ height: hole.rect_pad_height + margin * 2,
206
+ fill: positiveMarginColor,
207
+ realToCanvasMat,
208
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
209
+ })
210
+ }
211
+
92
212
  // Draw rectangular pad
93
213
  drawRect({
94
214
  ctx,
95
215
  center: { x: hole.x, y: hole.y },
96
216
  width: hole.rect_pad_width,
97
217
  height: hole.rect_pad_height,
98
- fill: colorMap.copper.top,
218
+ fill: copperColor,
99
219
  realToCanvasMat,
100
220
  borderRadius: hole.rect_border_radius ?? 0,
101
221
  })
102
222
 
223
+ // For negative margins, draw soldermask ring on top of the pad
224
+ if (hasSoldermask && margin < 0) {
225
+ drawSoldermaskRingForRect(
226
+ ctx,
227
+ { x: hole.x, y: hole.y },
228
+ hole.rect_pad_width,
229
+ hole.rect_pad_height,
230
+ margin,
231
+ hole.rect_border_radius ?? 0,
232
+ 0,
233
+ realToCanvasMat,
234
+ soldermaskRingColor,
235
+ copperColor,
236
+ )
237
+ }
238
+
103
239
  // Draw circular drill hole (with offset)
104
240
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
105
241
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -114,17 +250,46 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
114
250
  }
115
251
 
116
252
  if (hole.shape === "pill_hole_with_rect_pad") {
253
+ // For positive margins, draw extended mask area first
254
+ if (hasSoldermask && margin > 0) {
255
+ drawRect({
256
+ ctx,
257
+ center: { x: hole.x, y: hole.y },
258
+ width: hole.rect_pad_width + margin * 2,
259
+ height: hole.rect_pad_height + margin * 2,
260
+ fill: positiveMarginColor,
261
+ realToCanvasMat,
262
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
263
+ })
264
+ }
265
+
117
266
  // Draw rectangular pad
118
267
  drawRect({
119
268
  ctx,
120
269
  center: { x: hole.x, y: hole.y },
121
270
  width: hole.rect_pad_width,
122
271
  height: hole.rect_pad_height,
123
- fill: colorMap.copper.top,
272
+ fill: copperColor,
124
273
  realToCanvasMat,
125
274
  borderRadius: hole.rect_border_radius ?? 0,
126
275
  })
127
276
 
277
+ // For negative margins, draw soldermask ring on top of the pad
278
+ if (hasSoldermask && margin < 0) {
279
+ drawSoldermaskRingForRect(
280
+ ctx,
281
+ { x: hole.x, y: hole.y },
282
+ hole.rect_pad_width,
283
+ hole.rect_pad_height,
284
+ margin,
285
+ hole.rect_border_radius ?? 0,
286
+ 0,
287
+ realToCanvasMat,
288
+ soldermaskRingColor,
289
+ copperColor,
290
+ )
291
+ }
292
+
128
293
  // Draw pill drill hole (with offset)
129
294
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
130
295
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -140,18 +305,48 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
140
305
  }
141
306
 
142
307
  if (hole.shape === "rotated_pill_hole_with_rect_pad") {
308
+ // For positive margins, draw extended mask area first
309
+ if (hasSoldermask && margin > 0) {
310
+ drawRect({
311
+ ctx,
312
+ center: { x: hole.x, y: hole.y },
313
+ width: hole.rect_pad_width + margin * 2,
314
+ height: hole.rect_pad_height + margin * 2,
315
+ fill: positiveMarginColor,
316
+ realToCanvasMat,
317
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
318
+ rotation: hole.rect_ccw_rotation,
319
+ })
320
+ }
321
+
143
322
  // Draw rotated rectangular pad
144
323
  drawRect({
145
324
  ctx,
146
325
  center: { x: hole.x, y: hole.y },
147
326
  width: hole.rect_pad_width,
148
327
  height: hole.rect_pad_height,
149
- fill: colorMap.copper.top,
328
+ fill: copperColor,
150
329
  realToCanvasMat,
151
330
  borderRadius: hole.rect_border_radius ?? 0,
152
331
  rotation: hole.rect_ccw_rotation,
153
332
  })
154
333
 
334
+ // For negative margins, draw soldermask ring on top of the pad
335
+ if (hasSoldermask && margin < 0) {
336
+ drawSoldermaskRingForRect(
337
+ ctx,
338
+ { x: hole.x, y: hole.y },
339
+ hole.rect_pad_width,
340
+ hole.rect_pad_height,
341
+ margin,
342
+ hole.rect_border_radius ?? 0,
343
+ hole.rect_ccw_rotation ?? 0,
344
+ realToCanvasMat,
345
+ soldermaskRingColor,
346
+ copperColor,
347
+ )
348
+ }
349
+
155
350
  // Draw rotated pill drill hole (with offset)
156
351
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
157
352
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -168,6 +363,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
168
363
  }
169
364
 
170
365
  if (hole.shape === "hole_with_polygon_pad") {
366
+ // Note: Polygon pads don't support soldermask margins (similar to SMT polygon pads)
171
367
  // Draw polygon pad
172
368
  const padOutline = hole.pad_outline
173
369
  if (padOutline && padOutline.length >= 3) {
@@ -179,7 +375,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
179
375
  drawPolygon({
180
376
  ctx,
181
377
  points: padPoints,
182
- fill: colorMap.copper.top,
378
+ fill: copperColor,
183
379
  realToCanvasMat,
184
380
  })
185
381
  }
@@ -121,85 +121,80 @@ export function drawDimensionLine(params: DrawDimensionLineParams): void {
121
121
  ]
122
122
  }
123
123
 
124
- // Extension lines (ticks)
125
- const ext1 = getExtensionPoints(from)
126
- allPoints.push(...ext1, ext1[0]!)
127
-
128
- const ext2 = getExtensionPoints(to)
129
- allPoints.push(...ext2, ext2[0]!)
130
-
131
- // Main dimension line
132
- const halfWidth = strokeWidth / 2
133
- const mainLine = [
134
- {
135
- x: fromBase.x + perpendicular.x * halfWidth,
136
- y: fromBase.y + perpendicular.y * halfWidth,
137
- },
138
- {
139
- x: fromBase.x - perpendicular.x * halfWidth,
140
- y: fromBase.y - perpendicular.y * halfWidth,
141
- },
142
- {
143
- x: toBase.x - perpendicular.x * halfWidth,
144
- y: toBase.y - perpendicular.y * halfWidth,
145
- },
146
- {
147
- x: toBase.x + perpendicular.x * halfWidth,
148
- y: toBase.y + perpendicular.y * halfWidth,
149
- },
150
- ]
151
- allPoints.push(...mainLine, mainLine[0]!)
152
-
153
- // Arrows
154
- const arrow1 = [
155
- fromOffset,
156
- {
157
- x:
158
- fromOffset.x +
159
- direction.x * arrowSize +
160
- perpendicular.x * (arrowSize / 2),
161
- y:
162
- fromOffset.y +
163
- direction.y * arrowSize +
164
- perpendicular.y * (arrowSize / 2),
165
- },
166
- {
167
- x:
168
- fromOffset.x +
169
- direction.x * arrowSize -
170
- perpendicular.x * (arrowSize / 2),
171
- y:
172
- fromOffset.y +
173
- direction.y * arrowSize -
174
- perpendicular.y * (arrowSize / 2),
175
- },
176
- ]
177
- allPoints.push(...arrow1, arrow1[0]!)
178
-
179
- const arrow2 = [
180
- toOffset,
181
- {
182
- x:
183
- toOffset.x -
184
- direction.x * arrowSize +
185
- perpendicular.x * (arrowSize / 2),
186
- y:
187
- toOffset.y -
188
- direction.y * arrowSize +
189
- perpendicular.y * (arrowSize / 2),
190
- },
191
- {
192
- x:
193
- toOffset.x -
194
- direction.x * arrowSize -
195
- perpendicular.x * (arrowSize / 2),
196
- y:
197
- toOffset.y -
198
- direction.y * arrowSize -
199
- perpendicular.y * (arrowSize / 2),
200
- },
201
- ]
202
- allPoints.push(...arrow2, arrow2[0]!)
124
+ // Unified Perimeter Approach:
125
+ // Draw the main line and both arrows as a single continuous perimeter path.
126
+ // This eliminates self-intersections and winding issues.
127
+
128
+ // 1. Tip 1
129
+ allPoints.push(fromOffset)
130
+
131
+ // 2. Arrow 1 base corner 1
132
+ allPoints.push({
133
+ x: fromBase.x + perpendicular.x * (arrowSize / 2),
134
+ y: fromBase.y + perpendicular.y * (arrowSize / 2),
135
+ })
136
+
137
+ // 3. Main Line corner 1
138
+ allPoints.push({
139
+ x: fromBase.x + perpendicular.x * (strokeWidth / 2),
140
+ y: fromBase.y + perpendicular.y * (strokeWidth / 2),
141
+ })
142
+
143
+ // 4. Main Line corner 2 (at toBase)
144
+ allPoints.push({
145
+ x: toBase.x + perpendicular.x * (strokeWidth / 2),
146
+ y: toBase.y + perpendicular.y * (strokeWidth / 2),
147
+ })
148
+
149
+ // 5. Arrow 2 base corner 1
150
+ allPoints.push({
151
+ x: toBase.x + perpendicular.x * (arrowSize / 2),
152
+ y: toBase.y + perpendicular.y * (arrowSize / 2),
153
+ })
154
+
155
+ // 6. Tip 2
156
+ allPoints.push(toOffset)
157
+
158
+ // 7. Arrow 2 base corner 2
159
+ allPoints.push({
160
+ x: toBase.x - perpendicular.x * (arrowSize / 2),
161
+ y: toBase.y - perpendicular.y * (arrowSize / 2),
162
+ })
163
+
164
+ // 8. Main Line corner 3 (at toBase)
165
+ allPoints.push({
166
+ x: toBase.x - perpendicular.x * (strokeWidth / 2),
167
+ y: toBase.y - perpendicular.y * (strokeWidth / 2),
168
+ })
169
+
170
+ // 9. Main Line corner 4 (at fromBase)
171
+ allPoints.push({
172
+ x: fromBase.x - perpendicular.x * (strokeWidth / 2),
173
+ y: fromBase.y - perpendicular.y * (strokeWidth / 2),
174
+ })
175
+
176
+ // 10. Arrow 1 base corner 2
177
+ allPoints.push({
178
+ x: fromBase.x - perpendicular.x * (arrowSize / 2),
179
+ y: fromBase.y - perpendicular.y * (arrowSize / 2),
180
+ })
181
+
182
+ // 11. Back to Tip 1
183
+ allPoints.push(fromOffset)
184
+
185
+ // Bridge Ticks (Extension lines)
186
+ const startPoint = allPoints[0]!
187
+ const addTick = (anchor: { x: number; y: number }) => {
188
+ const pts = getExtensionPoints(anchor)
189
+ allPoints.push(startPoint)
190
+ allPoints.push(pts[0]!)
191
+ allPoints.push(...pts)
192
+ allPoints.push(pts[0]!)
193
+ allPoints.push(startPoint)
194
+ }
195
+
196
+ addTick(from)
197
+ addTick(to)
203
198
 
204
199
  drawPolygon({
205
200
  ctx,
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.36",
4
+ "version": "0.0.38",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",