circuit-to-canvas 0.0.46 → 0.0.48

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.
@@ -11,6 +11,8 @@ import {
11
11
  drawSoldermaskRingForOval,
12
12
  drawSoldermaskRingForPill,
13
13
  drawSoldermaskRingForRect,
14
+ drawSoldermaskRingForPolygon,
15
+ offsetPolygonPoints,
14
16
  } from "./soldermask-margin"
15
17
 
16
18
  export interface DrawPcbPlatedHoleParams {
@@ -249,7 +251,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
249
251
  height: hole.rect_pad_height + margin * 2,
250
252
  fill: positiveMarginColor,
251
253
  realToCanvasMat,
252
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
254
+ borderRadius: hole.rect_border_radius ?? 0,
253
255
  })
254
256
  }
255
257
 
@@ -318,7 +320,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
318
320
  height: hole.rect_pad_height + margin * 2,
319
321
  fill: positiveMarginColor,
320
322
  realToCanvasMat,
321
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
323
+ borderRadius: hole.rect_border_radius ?? 0,
322
324
  })
323
325
  }
324
326
 
@@ -388,7 +390,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
388
390
  height: hole.rect_pad_height + margin * 2,
389
391
  fill: positiveMarginColor,
390
392
  realToCanvasMat,
391
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
393
+ borderRadius: hole.rect_border_radius ?? 0,
392
394
  rotation: hole.rect_ccw_rotation,
393
395
  })
394
396
  }
@@ -453,8 +455,6 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
453
455
  }
454
456
 
455
457
  if (hole.shape === "hole_with_polygon_pad") {
456
- // Note: Polygon pads don't support soldermask margins (similar to SMT polygon pads)
457
- // Draw polygon pad
458
458
  const padOutline = hole.pad_outline
459
459
  if (padOutline && padOutline.length >= 3) {
460
460
  // Transform pad_outline points to be relative to hole.x, hole.y
@@ -462,12 +462,47 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
462
462
  x: hole.x + point.x,
463
463
  y: hole.y + point.y,
464
464
  }))
465
+
466
+ // For positive margins, draw extended mask area first
467
+ if (hasSoldermask && margin > 0) {
468
+ const expandedPoints = offsetPolygonPoints(padPoints, margin)
469
+ drawPolygon({
470
+ ctx,
471
+ points: expandedPoints,
472
+ fill: positiveMarginColor,
473
+ realToCanvasMat,
474
+ })
475
+ }
476
+
477
+ // Draw polygon pad
465
478
  drawPolygon({
466
479
  ctx,
467
480
  points: padPoints,
468
481
  fill: copperColor,
469
482
  realToCanvasMat,
470
483
  })
484
+
485
+ // For negative margins, draw soldermask ring on top of the pad
486
+ if (hasSoldermask && margin < 0) {
487
+ drawSoldermaskRingForPolygon(
488
+ ctx,
489
+ padPoints,
490
+ margin,
491
+ realToCanvasMat,
492
+ soldermaskRingColor,
493
+ copperColor,
494
+ )
495
+ }
496
+
497
+ // If fully covered, draw soldermask overlay
498
+ if (isCoveredWithSoldermask) {
499
+ drawPolygon({
500
+ ctx,
501
+ points: padPoints,
502
+ fill: soldermaskRingColor,
503
+ realToCanvasMat,
504
+ })
505
+ }
471
506
  }
472
507
 
473
508
  // Draw drill hole (with offset, only if not fully covered with soldermask)
@@ -9,6 +9,8 @@ import {
9
9
  drawSoldermaskRingForRect,
10
10
  drawSoldermaskRingForCircle,
11
11
  drawSoldermaskRingForPill,
12
+ drawSoldermaskRingForPolygon,
13
+ offsetPolygonPoints,
12
14
  } from "./soldermask-margin"
13
15
 
14
16
  export interface DrawPcbSmtPadParams {
@@ -67,7 +69,7 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
67
69
  height: pad.height + margin * 2,
68
70
  fill: positiveMarginColor,
69
71
  realToCanvasMat,
70
- borderRadius: getBorderRadius(pad, margin),
72
+ borderRadius: getBorderRadius(pad),
71
73
  })
72
74
  }
73
75
 
@@ -123,7 +125,7 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
123
125
  height: pad.height + margin * 2,
124
126
  fill: positiveMarginColor,
125
127
  realToCanvasMat,
126
- borderRadius: getBorderRadius(pad, margin),
128
+ borderRadius: getBorderRadius(pad),
127
129
  rotation: pad.ccw_rotation ?? 0,
128
130
  })
129
131
  }
@@ -328,6 +330,17 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
328
330
 
329
331
  if (pad.shape === "polygon") {
330
332
  if (pad.points && pad.points.length >= 3) {
333
+ // For positive margins, draw extended mask area first
334
+ if (hasSoldermask && margin > 0) {
335
+ const expandedPoints = offsetPolygonPoints(pad.points, margin)
336
+ drawPolygon({
337
+ ctx,
338
+ points: expandedPoints,
339
+ fill: positiveMarginColor,
340
+ realToCanvasMat,
341
+ })
342
+ }
343
+
331
344
  // Draw the copper pad
332
345
  drawPolygon({
333
346
  ctx,
@@ -336,8 +349,20 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
336
349
  realToCanvasMat,
337
350
  })
338
351
 
339
- // If covered with soldermask and margin >= 0, draw soldermaskOverCopper overlay
340
- if (isCoveredWithSoldermask && margin >= 0) {
352
+ // For negative margins, draw soldermask ring on top of the pad
353
+ if (hasSoldermask && margin < 0) {
354
+ drawSoldermaskRingForPolygon(
355
+ ctx,
356
+ pad.points,
357
+ margin,
358
+ realToCanvasMat,
359
+ soldermaskRingColor,
360
+ color,
361
+ )
362
+ }
363
+
364
+ // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
365
+ if (isCoveredWithSoldermask && margin === 0) {
341
366
  drawPolygon({
342
367
  ctx,
343
368
  points: pad.points,
@@ -324,3 +324,133 @@ export function drawSoldermaskRingForOval(
324
324
 
325
325
  ctx.restore()
326
326
  }
327
+
328
+ /**
329
+ * Offsets polygon points by a given distance (positive = outward, negative = inward)
330
+ */
331
+ export function offsetPolygonPoints(
332
+ points: Array<{ x: number; y: number }>,
333
+ offset: number,
334
+ ): Array<{ x: number; y: number }> {
335
+ if (points.length < 3 || offset === 0) return points
336
+
337
+ // Calculate polygon centroid
338
+ let centerX = 0
339
+ let centerY = 0
340
+ for (const point of points) {
341
+ centerX += point.x
342
+ centerY += point.y
343
+ }
344
+ centerX /= points.length
345
+ centerY /= points.length
346
+
347
+ const result: Array<{ x: number; y: number }> = []
348
+
349
+ for (const point of points) {
350
+ // Calculate vector from center to point
351
+ const vectorX = point.x - centerX
352
+ const vectorY = point.y - centerY
353
+
354
+ // Calculate distance from center
355
+ const distance = Math.sqrt(vectorX * vectorX + vectorY * vectorY)
356
+
357
+ if (distance > 0) {
358
+ // Normalize vector and offset
359
+ const normalizedX = vectorX / distance
360
+ const normalizedY = vectorY / distance
361
+
362
+ result.push({
363
+ x: point.x + normalizedX * offset,
364
+ y: point.y + normalizedY * offset,
365
+ })
366
+ } else {
367
+ // Point is at center, offset in arbitrary direction
368
+ result.push({
369
+ x: point.x + offset,
370
+ y: point.y,
371
+ })
372
+ }
373
+ }
374
+
375
+ return result
376
+ }
377
+
378
+ /**
379
+ * Draws a soldermask ring for polygon shapes with negative margin
380
+ * (soldermask appears inside the pad boundary)
381
+ */
382
+ export function drawSoldermaskRingForPolygon(
383
+ ctx: CanvasContext,
384
+ points: Array<{ x: number; y: number }>,
385
+ margin: number,
386
+ realToCanvasMat: Matrix,
387
+ soldermaskColor: string,
388
+ padColor: string,
389
+ ): void {
390
+ if (points.length < 3 || margin >= 0) return
391
+
392
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
393
+
394
+ // For negative margins, outer is pad boundary, inner is contracted by margin
395
+ // Use source-atop so the ring only appears on the pad
396
+ const prevCompositeOp = ctx.globalCompositeOperation
397
+ if (ctx.globalCompositeOperation !== undefined) {
398
+ ctx.globalCompositeOperation = "source-atop"
399
+ }
400
+
401
+ // Draw outer polygon filled (at pad boundary)
402
+ ctx.beginPath()
403
+ const canvasPoints = points.map((p) =>
404
+ applyToPoint(realToCanvasMat, [p.x, p.y]),
405
+ )
406
+
407
+ const firstPoint = canvasPoints[0]
408
+ if (firstPoint) {
409
+ const [firstX, firstY] = firstPoint
410
+ ctx.moveTo(firstX, firstY)
411
+
412
+ for (let i = 1; i < canvasPoints.length; i++) {
413
+ const point = canvasPoints[i]
414
+ if (point) {
415
+ const [x, y] = point
416
+ ctx.lineTo(x, y)
417
+ }
418
+ }
419
+ ctx.closePath()
420
+ ctx.fillStyle = soldermaskColor
421
+ ctx.fill()
422
+ }
423
+
424
+ // Reset composite operation and restore pad color in inner area
425
+ if (ctx.globalCompositeOperation !== undefined) {
426
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over"
427
+ }
428
+
429
+ // Restore pad color in inner polygon (contracted by margin)
430
+ const innerPoints = offsetPolygonPoints(points, margin)
431
+ if (innerPoints.length >= 3) {
432
+ ctx.beginPath()
433
+ const innerCanvasPoints = innerPoints.map((p) =>
434
+ applyToPoint(realToCanvasMat, [p.x, p.y]),
435
+ )
436
+
437
+ const firstInnerPoint = innerCanvasPoints[0]
438
+ if (firstInnerPoint) {
439
+ const [firstX, firstY] = firstInnerPoint
440
+ ctx.moveTo(firstX, firstY)
441
+
442
+ for (let i = 1; i < innerCanvasPoints.length; i++) {
443
+ const point = innerCanvasPoints[i]
444
+ if (point) {
445
+ const [x, y] = point
446
+ ctx.lineTo(x, y)
447
+ }
448
+ }
449
+ ctx.closePath()
450
+ ctx.fillStyle = padColor
451
+ ctx.fill()
452
+ }
453
+ }
454
+
455
+ ctx.restore()
456
+ }
@@ -13,6 +13,7 @@ export interface CanvasContext {
13
13
  radius: number,
14
14
  startAngle: number,
15
15
  endAngle: number,
16
+ counterclockwise?: boolean,
16
17
  ): void
17
18
  arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
18
19
  ellipse(
@@ -24,7 +25,7 @@ export interface CanvasContext {
24
25
  startAngle: number,
25
26
  endAngle: number,
26
27
  ): void
27
- fill(): void
28
+ fill(fillRule?: "nonzero" | "evenodd"): void
28
29
  stroke(): void
29
30
  rect(x: number, y: number, w: number, h: number): void
30
31
  lineTo(x: number, y: number): void
@@ -38,6 +39,7 @@ export interface CanvasContext {
38
39
  globalCompositeOperation?: string
39
40
  fillStyle: string | CanvasGradient | CanvasPattern
40
41
  strokeStyle: string | CanvasGradient | CanvasPattern
42
+ globalAlpha: number
41
43
  lineWidth: number
42
44
  lineCap: "butt" | "round" | "square"
43
45
  lineJoin: "bevel" | "round" | "miter"
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.46",
4
+ "version": "0.0.48",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",