circuit-to-canvas 0.0.46 → 0.0.47

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.js CHANGED
@@ -456,6 +456,90 @@ function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rota
456
456
  }
457
457
  ctx.restore();
458
458
  }
459
+ function offsetPolygonPoints(points, offset) {
460
+ if (points.length < 3 || offset === 0) return points;
461
+ let centerX = 0;
462
+ let centerY = 0;
463
+ for (const point of points) {
464
+ centerX += point.x;
465
+ centerY += point.y;
466
+ }
467
+ centerX /= points.length;
468
+ centerY /= points.length;
469
+ const result = [];
470
+ for (const point of points) {
471
+ const vectorX = point.x - centerX;
472
+ const vectorY = point.y - centerY;
473
+ const distance = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
474
+ if (distance > 0) {
475
+ const normalizedX = vectorX / distance;
476
+ const normalizedY = vectorY / distance;
477
+ result.push({
478
+ x: point.x + normalizedX * offset,
479
+ y: point.y + normalizedY * offset
480
+ });
481
+ } else {
482
+ result.push({
483
+ x: point.x + offset,
484
+ y: point.y
485
+ });
486
+ }
487
+ }
488
+ return result;
489
+ }
490
+ function drawSoldermaskRingForPolygon(ctx, points, margin, realToCanvasMat, soldermaskColor, padColor) {
491
+ if (points.length < 3 || margin >= 0) return;
492
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
493
+ const prevCompositeOp = ctx.globalCompositeOperation;
494
+ if (ctx.globalCompositeOperation !== void 0) {
495
+ ctx.globalCompositeOperation = "source-atop";
496
+ }
497
+ ctx.beginPath();
498
+ const canvasPoints = points.map(
499
+ (p) => applyToPoint6(realToCanvasMat, [p.x, p.y])
500
+ );
501
+ const firstPoint = canvasPoints[0];
502
+ if (firstPoint) {
503
+ const [firstX, firstY] = firstPoint;
504
+ ctx.moveTo(firstX, firstY);
505
+ for (let i = 1; i < canvasPoints.length; i++) {
506
+ const point = canvasPoints[i];
507
+ if (point) {
508
+ const [x, y] = point;
509
+ ctx.lineTo(x, y);
510
+ }
511
+ }
512
+ ctx.closePath();
513
+ ctx.fillStyle = soldermaskColor;
514
+ ctx.fill();
515
+ }
516
+ if (ctx.globalCompositeOperation !== void 0) {
517
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
518
+ }
519
+ const innerPoints = offsetPolygonPoints(points, margin);
520
+ if (innerPoints.length >= 3) {
521
+ ctx.beginPath();
522
+ const innerCanvasPoints = innerPoints.map(
523
+ (p) => applyToPoint6(realToCanvasMat, [p.x, p.y])
524
+ );
525
+ const firstInnerPoint = innerCanvasPoints[0];
526
+ if (firstInnerPoint) {
527
+ const [firstX, firstY] = firstInnerPoint;
528
+ ctx.moveTo(firstX, firstY);
529
+ for (let i = 1; i < innerCanvasPoints.length; i++) {
530
+ const point = innerCanvasPoints[i];
531
+ if (point) {
532
+ const [x, y] = point;
533
+ ctx.lineTo(x, y);
534
+ }
535
+ }
536
+ ctx.closePath();
537
+ ctx.fillStyle = padColor;
538
+ ctx.fill();
539
+ }
540
+ }
541
+ ctx.restore();
542
+ }
459
543
 
460
544
  // lib/drawer/elements/pcb-plated-hole.ts
461
545
  function getSoldermaskColor(layers, colorMap) {
@@ -643,7 +727,7 @@ function drawPcbPlatedHole(params) {
643
727
  height: hole.rect_pad_height + margin * 2,
644
728
  fill: positiveMarginColor,
645
729
  realToCanvasMat,
646
- borderRadius: (hole.rect_border_radius ?? 0) + margin
730
+ borderRadius: hole.rect_border_radius ?? 0
647
731
  });
648
732
  }
649
733
  drawRect({
@@ -702,7 +786,7 @@ function drawPcbPlatedHole(params) {
702
786
  height: hole.rect_pad_height + margin * 2,
703
787
  fill: positiveMarginColor,
704
788
  realToCanvasMat,
705
- borderRadius: (hole.rect_border_radius ?? 0) + margin
789
+ borderRadius: hole.rect_border_radius ?? 0
706
790
  });
707
791
  }
708
792
  drawRect({
@@ -762,7 +846,7 @@ function drawPcbPlatedHole(params) {
762
846
  height: hole.rect_pad_height + margin * 2,
763
847
  fill: positiveMarginColor,
764
848
  realToCanvasMat,
765
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
849
+ borderRadius: hole.rect_border_radius ?? 0,
766
850
  rotation: hole.rect_ccw_rotation
767
851
  });
768
852
  }
@@ -824,12 +908,39 @@ function drawPcbPlatedHole(params) {
824
908
  x: hole.x + point.x,
825
909
  y: hole.y + point.y
826
910
  }));
911
+ if (hasSoldermask && margin > 0) {
912
+ const expandedPoints = offsetPolygonPoints(padPoints, margin);
913
+ drawPolygon({
914
+ ctx,
915
+ points: expandedPoints,
916
+ fill: positiveMarginColor,
917
+ realToCanvasMat
918
+ });
919
+ }
827
920
  drawPolygon({
828
921
  ctx,
829
922
  points: padPoints,
830
923
  fill: copperColor,
831
924
  realToCanvasMat
832
925
  });
926
+ if (hasSoldermask && margin < 0) {
927
+ drawSoldermaskRingForPolygon(
928
+ ctx,
929
+ padPoints,
930
+ margin,
931
+ realToCanvasMat,
932
+ soldermaskRingColor,
933
+ copperColor
934
+ );
935
+ }
936
+ if (isCoveredWithSoldermask) {
937
+ drawPolygon({
938
+ ctx,
939
+ points: padPoints,
940
+ fill: soldermaskRingColor,
941
+ realToCanvasMat
942
+ });
943
+ }
833
944
  }
834
945
  if (!isCoveredWithSoldermask) {
835
946
  const holeX = hole.x + (hole.hole_offset_x ?? 0);
@@ -1233,7 +1344,7 @@ function drawPcbSmtPad(params) {
1233
1344
  height: pad.height + margin * 2,
1234
1345
  fill: positiveMarginColor,
1235
1346
  realToCanvasMat,
1236
- borderRadius: getBorderRadius(pad, margin)
1347
+ borderRadius: getBorderRadius(pad)
1237
1348
  });
1238
1349
  }
1239
1350
  drawRect({
@@ -1281,7 +1392,7 @@ function drawPcbSmtPad(params) {
1281
1392
  height: pad.height + margin * 2,
1282
1393
  fill: positiveMarginColor,
1283
1394
  realToCanvasMat,
1284
- borderRadius: getBorderRadius(pad, margin),
1395
+ borderRadius: getBorderRadius(pad),
1285
1396
  rotation: pad.ccw_rotation ?? 0
1286
1397
  });
1287
1398
  }
@@ -1455,13 +1566,32 @@ function drawPcbSmtPad(params) {
1455
1566
  }
1456
1567
  if (pad.shape === "polygon") {
1457
1568
  if (pad.points && pad.points.length >= 3) {
1569
+ if (hasSoldermask && margin > 0) {
1570
+ const expandedPoints = offsetPolygonPoints(pad.points, margin);
1571
+ drawPolygon({
1572
+ ctx,
1573
+ points: expandedPoints,
1574
+ fill: positiveMarginColor,
1575
+ realToCanvasMat
1576
+ });
1577
+ }
1458
1578
  drawPolygon({
1459
1579
  ctx,
1460
1580
  points: pad.points,
1461
1581
  fill: color,
1462
1582
  realToCanvasMat
1463
1583
  });
1464
- if (isCoveredWithSoldermask && margin >= 0) {
1584
+ if (hasSoldermask && margin < 0) {
1585
+ drawSoldermaskRingForPolygon(
1586
+ ctx,
1587
+ pad.points,
1588
+ margin,
1589
+ realToCanvasMat,
1590
+ soldermaskRingColor,
1591
+ color
1592
+ );
1593
+ }
1594
+ if (isCoveredWithSoldermask && margin === 0) {
1465
1595
  drawPolygon({
1466
1596
  ctx,
1467
1597
  points: pad.points,
@@ -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
+ }
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.47",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",