circuit-to-canvas 0.0.110 → 0.0.112

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PcbVia, PcbTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval, PcbCutout, PCBKeepout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension, PcbFabricationNoteDimension, PcbCourtyardCircle } from 'circuit-json';
1
+ import { LayerRef, AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PcbVia, PcbCutout, PcbTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval, PCBKeepout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension, PcbFabricationNoteDimension, PcbCourtyardCircle } from 'circuit-json';
2
2
  import { Matrix } from 'transformation-matrix';
3
3
 
4
4
  /**
@@ -44,7 +44,7 @@ interface CanvasContext {
44
44
  font: string;
45
45
  textAlign: "start" | "end" | "left" | "right" | "center";
46
46
  }
47
- type CopperLayerName = "top" | "bottom" | "inner1" | "inner2" | "inner3" | "inner4" | "inner5" | "inner6";
47
+ type CopperLayerName = LayerRef;
48
48
  type CopperColorMap = Record<CopperLayerName, string> & {
49
49
  [layer: string]: string;
50
50
  };
@@ -333,7 +333,7 @@ interface DrawPcbPlatedHoleParams {
333
333
  colorMap: PcbColorMap;
334
334
  soldermaskMargin?: number;
335
335
  drawSoldermask?: boolean;
336
- layer?: "top" | "bottom";
336
+ layer?: LayerRef;
337
337
  }
338
338
  declare function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void;
339
339
 
@@ -342,7 +342,7 @@ interface DrawPcbViaParams {
342
342
  via: PCBVia;
343
343
  realToCanvasMat: Matrix;
344
344
  colorMap: PcbColorMap;
345
- layer?: "top" | "bottom";
345
+ layer?: LayerRef;
346
346
  }
347
347
  declare function drawPcbVia(params: DrawPcbViaParams): void;
348
348
 
@@ -364,6 +364,7 @@ interface DrawPcbSmtPadParams {
364
364
  holes?: PcbHole[];
365
365
  platedHoles?: PcbPlatedHole[];
366
366
  vias?: PcbVia[];
367
+ cutouts?: PcbCutout[];
367
368
  }
368
369
  declare function drawPcbSmtPad(params: DrawPcbSmtPadParams): void;
369
370
 
package/dist/index.js CHANGED
@@ -1637,22 +1637,24 @@ function drawPadWithDrillCutouts(params) {
1637
1637
  ctx.restore();
1638
1638
  }
1639
1639
  function getPadDrillCutouts(params) {
1640
- const { pad, holes = [], platedHoles = [], vias = [] } = params;
1640
+ const { pad, holes = [], platedHoles = [], vias = [], cutouts = [] } = params;
1641
1641
  const padBounds = getPadBounds(pad);
1642
1642
  if (!padBounds) return [];
1643
- const cutouts = [];
1644
- for (const hole of holes) {
1645
- const cutout = getHoleDrillCutout(hole);
1643
+ const drillCutouts = [];
1644
+ const addIfOverlapping = (cutout) => {
1646
1645
  if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
1647
- cutouts.push(cutout);
1646
+ drillCutouts.push(cutout);
1648
1647
  }
1648
+ };
1649
+ for (const cutout of cutouts) {
1650
+ addIfOverlapping(getBoardCutout(cutout));
1651
+ }
1652
+ for (const hole of holes) {
1653
+ addIfOverlapping(getHoleDrillCutout(hole));
1649
1654
  }
1650
1655
  for (const platedHole of platedHoles) {
1651
1656
  if (!layersIncludePadLayer(platedHole.layers, pad.layer)) continue;
1652
- const cutout = getPlatedHoleDrillCutout(platedHole);
1653
- if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
1654
- cutouts.push(cutout);
1655
- }
1657
+ addIfOverlapping(getPlatedHoleDrillCutout(platedHole));
1656
1658
  }
1657
1659
  for (const via of vias) {
1658
1660
  if (!layersIncludePadLayer(via.layers, pad.layer)) continue;
@@ -1664,11 +1666,9 @@ function getPadDrillCutouts(params) {
1664
1666
  y: via.y,
1665
1667
  radius
1666
1668
  };
1667
- if (boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
1668
- cutouts.push(cutout);
1669
- }
1669
+ addIfOverlapping(cutout);
1670
1670
  }
1671
- return cutouts;
1671
+ return drillCutouts;
1672
1672
  }
1673
1673
  function appendPadPath(ctx, pad, realToCanvasMat) {
1674
1674
  if (pad.shape === "rect" || pad.shape === "rotated_rect") {
@@ -1741,11 +1741,16 @@ function appendDrillCutoutPath(ctx, drillCutout, realToCanvasMat) {
1741
1741
  });
1742
1742
  return;
1743
1743
  }
1744
+ if (drillCutout.shape === "polygon") {
1745
+ appendPolygonPath(ctx, drillCutout.points, realToCanvasMat);
1746
+ return;
1747
+ }
1744
1748
  appendRectPath(ctx, {
1745
1749
  x: drillCutout.x,
1746
1750
  y: drillCutout.y,
1747
1751
  width: drillCutout.width,
1748
1752
  height: drillCutout.height,
1753
+ radius: drillCutout.radius,
1749
1754
  rotation: drillCutout.rotation,
1750
1755
  realToCanvasMat
1751
1756
  });
@@ -1883,6 +1888,9 @@ function getPadBounds(pad) {
1883
1888
  }
1884
1889
  }
1885
1890
  function getDrillCutoutBounds(cutout) {
1891
+ if (cutout.shape === "polygon") {
1892
+ return getPointBounds(cutout.points);
1893
+ }
1886
1894
  if (cutout.shape === "circle") {
1887
1895
  return boundsAroundCenter(cutout.x, cutout.y, cutout.radius, cutout.radius);
1888
1896
  }
@@ -1906,6 +1914,32 @@ function getDrillCutoutBounds(cutout) {
1906
1914
  }
1907
1915
  return boundsAroundCenter(cutout.x, cutout.y, halfWidth, halfHeight);
1908
1916
  }
1917
+ function getBoardCutout(cutout) {
1918
+ if (cutout.shape === "circle") {
1919
+ if (cutout.radius <= 0) return;
1920
+ return {
1921
+ shape: "circle",
1922
+ x: cutout.center.x,
1923
+ y: cutout.center.y,
1924
+ radius: cutout.radius
1925
+ };
1926
+ }
1927
+ if (cutout.shape === "rect") {
1928
+ if (cutout.width <= 0 || cutout.height <= 0) return;
1929
+ return {
1930
+ shape: "rect",
1931
+ x: cutout.center.x,
1932
+ y: cutout.center.y,
1933
+ width: cutout.width,
1934
+ height: cutout.height,
1935
+ radius: cutout.corner_radius,
1936
+ rotation: cutout.rotation
1937
+ };
1938
+ }
1939
+ if (cutout.shape === "polygon" && cutout.points.length >= 3) {
1940
+ return { shape: "polygon", points: cutout.points };
1941
+ }
1942
+ }
1909
1943
  function getPointBounds(points) {
1910
1944
  let minX = Infinity;
1911
1945
  let maxX = -Infinity;
@@ -5183,6 +5217,8 @@ var DEFAULT_PCB_COLOR_MAP = {
5183
5217
  inner4: "rgb(64, 224, 208)",
5184
5218
  inner5: "rgb(138, 43, 226)",
5185
5219
  inner6: "rgb(255, 105, 180)",
5220
+ inner7: "rgb(232, 178, 167)",
5221
+ inner8: "rgb(242, 237, 161)",
5186
5222
  bottom: "rgb(77, 127, 196)"
5187
5223
  },
5188
5224
  copperPour: {
@@ -5222,9 +5258,10 @@ var DEFAULT_PCB_COLOR_MAP = {
5222
5258
  // lib/drawer/CircuitToCanvasDrawer.ts
5223
5259
  function getCopperLayer(layers) {
5224
5260
  if (!layers || layers.length === 0) return "top";
5225
- const hasBottom = layers.some((l) => l.startsWith("bottom"));
5226
- return hasBottom ? "bottom" : "top";
5261
+ const copperLayer = layers.find((candidate) => candidate.endsWith("_copper"));
5262
+ return copperLayer ? copperLayer.slice(0, -"_copper".length) : "top";
5227
5263
  }
5264
+ var isOnCopperLayer = (element, layer) => element.layers?.includes(layer) ?? true;
5228
5265
  var CircuitToCanvasDrawer = class {
5229
5266
  ctx;
5230
5267
  colorMap;
@@ -5310,13 +5347,16 @@ var CircuitToCanvasDrawer = class {
5310
5347
  const renderTopSoldermask = drawSoldermask && (board !== void 0 || panel !== void 0) && (options.drawSoldermaskTop ?? !hasExplicitSoldermaskLayers);
5311
5348
  const renderBottomSoldermask = drawSoldermask && (board !== void 0 || panel !== void 0) && (options.drawSoldermaskBottom ?? false);
5312
5349
  const drawableVias = elements.filter(
5313
- (el) => shouldDrawElement(el, options) && el.type === "pcb_via"
5350
+ (el) => shouldDrawElement(el, options) && el.type === "pcb_via" && isOnCopperLayer(el, layer)
5314
5351
  );
5315
5352
  const drawableHoles = elements.filter(
5316
5353
  (el) => shouldDrawElement(el, options) && el.type === "pcb_hole"
5317
5354
  );
5318
5355
  const drawablePlatedHoles = elements.filter(
5319
- (el) => shouldDrawElement(el, options) && el.type === "pcb_plated_hole"
5356
+ (el) => shouldDrawElement(el, options) && el.type === "pcb_plated_hole" && isOnCopperLayer(el, layer)
5357
+ );
5358
+ const drawableCutouts = elements.filter(
5359
+ (el) => shouldDrawElement(el, options) && el.type === "pcb_cutout"
5320
5360
  );
5321
5361
  if (board) {
5322
5362
  drawPcbBoard({
@@ -5349,7 +5389,8 @@ var CircuitToCanvasDrawer = class {
5349
5389
  colorMap: this.colorMap,
5350
5390
  holes: drawableHoles,
5351
5391
  platedHoles: drawablePlatedHoles,
5352
- vias: drawableVias
5392
+ vias: drawableVias,
5393
+ cutouts: drawableCutouts
5353
5394
  });
5354
5395
  }
5355
5396
  if (element.type === "pcb_copper_text") {
@@ -5387,7 +5428,7 @@ var CircuitToCanvasDrawer = class {
5387
5428
  drawSoldermask: renderTopSoldermask
5388
5429
  });
5389
5430
  }
5390
- if (element.type === "pcb_plated_hole") {
5431
+ if (element.type === "pcb_plated_hole" && isOnCopperLayer(element, layer)) {
5391
5432
  drawPcbPlatedHole({
5392
5433
  ctx: this.ctx,
5393
5434
  hole: element,
@@ -5398,7 +5439,7 @@ var CircuitToCanvasDrawer = class {
5398
5439
  layer
5399
5440
  });
5400
5441
  }
5401
- if (element.type === "pcb_via") {
5442
+ if (element.type === "pcb_via" && isOnCopperLayer(element, layer)) {
5402
5443
  drawPcbVia({
5403
5444
  ctx: this.ctx,
5404
5445
  via: element,
@@ -5506,7 +5547,7 @@ var CircuitToCanvasDrawer = class {
5506
5547
  }
5507
5548
  for (const element of elements) {
5508
5549
  if (!shouldDrawElement(element, options)) continue;
5509
- if (element.type === "pcb_plated_hole" && !renderTopSoldermask) {
5550
+ if (element.type === "pcb_plated_hole" && !renderTopSoldermask && isOnCopperLayer(element, layer)) {
5510
5551
  drawPcbPlatedHole({
5511
5552
  ctx: this.ctx,
5512
5553
  hole: element,
@@ -5517,7 +5558,7 @@ var CircuitToCanvasDrawer = class {
5517
5558
  layer
5518
5559
  });
5519
5560
  }
5520
- if (element.type === "pcb_via" && !renderTopSoldermask) {
5561
+ if (element.type === "pcb_via" && !renderTopSoldermask && isOnCopperLayer(element, layer)) {
5521
5562
  drawPcbVia({
5522
5563
  ctx: this.ctx,
5523
5564
  via: element,
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AnyCircuitElement,
3
+ LayerRef,
3
4
  PCBKeepout,
4
5
  PcbBoard,
5
6
  PcbCopperPour,
@@ -94,12 +95,17 @@ interface CanvasLike {
94
95
  getContext(contextId: "2d"): CanvasContext | null
95
96
  }
96
97
 
97
- function getCopperLayer(layers?: PcbRenderLayer[]): "top" | "bottom" {
98
+ function getCopperLayer(layers?: PcbRenderLayer[]): LayerRef {
98
99
  if (!layers || layers.length === 0) return "top"
99
- const hasBottom = layers.some((l) => l.startsWith("bottom"))
100
- return hasBottom ? "bottom" : "top"
100
+ const copperLayer = layers.find((candidate) => candidate.endsWith("_copper"))
101
+ return copperLayer
102
+ ? (copperLayer.slice(0, -"_copper".length) as LayerRef)
103
+ : "top"
101
104
  }
102
105
 
106
+ const isOnCopperLayer = (element: PcbVia | PcbPlatedHole, layer: LayerRef) =>
107
+ element.layers?.includes(layer) ?? true
108
+
103
109
  export class CircuitToCanvasDrawer {
104
110
  private ctx: CanvasContext
105
111
  private colorMap: PcbColorMap
@@ -233,7 +239,9 @@ export class CircuitToCanvasDrawer {
233
239
  (options.drawSoldermaskBottom ?? false)
234
240
  const drawableVias = elements.filter(
235
241
  (el): el is PcbVia =>
236
- shouldDrawElement(el, options) && el.type === "pcb_via",
242
+ shouldDrawElement(el, options) &&
243
+ el.type === "pcb_via" &&
244
+ isOnCopperLayer(el, layer),
237
245
  )
238
246
  const drawableHoles = elements.filter(
239
247
  (el): el is PcbHole =>
@@ -241,7 +249,13 @@ export class CircuitToCanvasDrawer {
241
249
  )
242
250
  const drawablePlatedHoles = elements.filter(
243
251
  (el): el is PcbPlatedHole =>
244
- shouldDrawElement(el, options) && el.type === "pcb_plated_hole",
252
+ shouldDrawElement(el, options) &&
253
+ el.type === "pcb_plated_hole" &&
254
+ isOnCopperLayer(el, layer),
255
+ )
256
+ const drawableCutouts = elements.filter(
257
+ (el): el is PcbCutout =>
258
+ shouldDrawElement(el, options) && el.type === "pcb_cutout",
245
259
  )
246
260
 
247
261
  // Step 2: Draw board outline/material (inner board)
@@ -283,6 +297,7 @@ export class CircuitToCanvasDrawer {
283
297
  holes: drawableHoles,
284
298
  platedHoles: drawablePlatedHoles,
285
299
  vias: drawableVias,
300
+ cutouts: drawableCutouts,
286
301
  })
287
302
  }
288
303
 
@@ -327,7 +342,10 @@ export class CircuitToCanvasDrawer {
327
342
  })
328
343
  }
329
344
 
330
- if (element.type === "pcb_plated_hole") {
345
+ if (
346
+ element.type === "pcb_plated_hole" &&
347
+ isOnCopperLayer(element, layer)
348
+ ) {
331
349
  drawPcbPlatedHole({
332
350
  ctx: this.ctx,
333
351
  hole: element as PcbPlatedHole,
@@ -339,7 +357,7 @@ export class CircuitToCanvasDrawer {
339
357
  })
340
358
  }
341
359
 
342
- if (element.type === "pcb_via") {
360
+ if (element.type === "pcb_via" && isOnCopperLayer(element, layer)) {
343
361
  drawPcbVia({
344
362
  ctx: this.ctx,
345
363
  via: element as PcbVia,
@@ -469,7 +487,11 @@ export class CircuitToCanvasDrawer {
469
487
  for (const element of elements) {
470
488
  if (!shouldDrawElement(element, options)) continue
471
489
 
472
- if (element.type === "pcb_plated_hole" && !renderTopSoldermask) {
490
+ if (
491
+ element.type === "pcb_plated_hole" &&
492
+ !renderTopSoldermask &&
493
+ isOnCopperLayer(element, layer)
494
+ ) {
473
495
  drawPcbPlatedHole({
474
496
  ctx: this.ctx,
475
497
  hole: element as PcbPlatedHole,
@@ -483,7 +505,11 @@ export class CircuitToCanvasDrawer {
483
505
  })
484
506
  }
485
507
 
486
- if (element.type === "pcb_via" && !renderTopSoldermask) {
508
+ if (
509
+ element.type === "pcb_via" &&
510
+ !renderTopSoldermask &&
511
+ isOnCopperLayer(element, layer)
512
+ ) {
487
513
  drawPcbVia({
488
514
  ctx: this.ctx,
489
515
  via: element as PcbVia,
@@ -1,4 +1,10 @@
1
- import type { PcbHole, PcbPlatedHole, PcbSmtPad, PcbVia } from "circuit-json"
1
+ import type {
2
+ PcbCutout,
3
+ PcbHole,
4
+ PcbPlatedHole,
5
+ PcbSmtPad,
6
+ PcbVia,
7
+ } from "circuit-json"
2
8
  import type { Matrix } from "transformation-matrix"
3
9
  import { applyToPoint } from "transformation-matrix"
4
10
  import type { CanvasContext } from "../../types"
@@ -34,14 +40,21 @@ interface DrillCutoutRect {
34
40
  y: number
35
41
  width: number
36
42
  height: number
43
+ radius?: number
37
44
  rotation?: number
38
45
  }
39
46
 
47
+ interface DrillCutoutPolygon {
48
+ shape: "polygon"
49
+ points: Array<{ x: number; y: number }>
50
+ }
51
+
40
52
  type DrillCutout =
41
53
  | DrillCutoutCircle
42
54
  | DrillCutoutOval
43
55
  | DrillCutoutPill
44
56
  | DrillCutoutRect
57
+ | DrillCutoutPolygon
45
58
 
46
59
  interface Bounds {
47
60
  minX: number
@@ -55,6 +68,7 @@ export interface PcbSmtPadDrillCutoutParams {
55
68
  holes?: PcbHole[]
56
69
  platedHoles?: PcbPlatedHole[]
57
70
  vias?: PcbVia[]
71
+ cutouts?: PcbCutout[]
58
72
  }
59
73
 
60
74
  export function drawPadWithDrillCutouts(params: {
@@ -87,26 +101,28 @@ export function drawPadWithDrillCutouts(params: {
87
101
  export function getPadDrillCutouts(
88
102
  params: PcbSmtPadDrillCutoutParams,
89
103
  ): DrillCutout[] {
90
- const { pad, holes = [], platedHoles = [], vias = [] } = params
104
+ const { pad, holes = [], platedHoles = [], vias = [], cutouts = [] } = params
91
105
  const padBounds = getPadBounds(pad)
92
106
  if (!padBounds) return []
93
107
 
94
- const cutouts: DrillCutout[] = []
95
-
96
- for (const hole of holes) {
97
- const cutout = getHoleDrillCutout(hole)
108
+ const drillCutouts: DrillCutout[] = []
109
+ const addIfOverlapping = (cutout: DrillCutout | undefined): void => {
98
110
  if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
99
- cutouts.push(cutout)
111
+ drillCutouts.push(cutout)
100
112
  }
101
113
  }
102
114
 
115
+ for (const cutout of cutouts) {
116
+ addIfOverlapping(getBoardCutout(cutout))
117
+ }
118
+
119
+ for (const hole of holes) {
120
+ addIfOverlapping(getHoleDrillCutout(hole))
121
+ }
122
+
103
123
  for (const platedHole of platedHoles) {
104
124
  if (!layersIncludePadLayer(platedHole.layers, pad.layer)) continue
105
-
106
- const cutout = getPlatedHoleDrillCutout(platedHole)
107
- if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
108
- cutouts.push(cutout)
109
- }
125
+ addIfOverlapping(getPlatedHoleDrillCutout(platedHole))
110
126
  }
111
127
 
112
128
  for (const via of vias) {
@@ -120,12 +136,10 @@ export function getPadDrillCutouts(
120
136
  y: via.y,
121
137
  radius,
122
138
  }
123
- if (boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
124
- cutouts.push(cutout)
125
- }
139
+ addIfOverlapping(cutout)
126
140
  }
127
141
 
128
- return cutouts
142
+ return drillCutouts
129
143
  }
130
144
 
131
145
  function appendPadPath(
@@ -215,11 +229,17 @@ function appendDrillCutoutPath(
215
229
  return
216
230
  }
217
231
 
232
+ if (drillCutout.shape === "polygon") {
233
+ appendPolygonPath(ctx, drillCutout.points, realToCanvasMat)
234
+ return
235
+ }
236
+
218
237
  appendRectPath(ctx, {
219
238
  x: drillCutout.x,
220
239
  y: drillCutout.y,
221
240
  width: drillCutout.width,
222
241
  height: drillCutout.height,
242
+ radius: drillCutout.radius,
223
243
  rotation: drillCutout.rotation,
224
244
  realToCanvasMat,
225
245
  })
@@ -429,6 +449,10 @@ function getPadBounds(pad: PcbSmtPad): Bounds | undefined {
429
449
  }
430
450
 
431
451
  function getDrillCutoutBounds(cutout: DrillCutout): Bounds {
452
+ if (cutout.shape === "polygon") {
453
+ return getPointBounds(cutout.points)
454
+ }
455
+
432
456
  if (cutout.shape === "circle") {
433
457
  return boundsAroundCenter(cutout.x, cutout.y, cutout.radius, cutout.radius)
434
458
  }
@@ -455,6 +479,35 @@ function getDrillCutoutBounds(cutout: DrillCutout): Bounds {
455
479
  return boundsAroundCenter(cutout.x, cutout.y, halfWidth, halfHeight)
456
480
  }
457
481
 
482
+ function getBoardCutout(cutout: PcbCutout): DrillCutout | undefined {
483
+ if (cutout.shape === "circle") {
484
+ if (cutout.radius <= 0) return
485
+ return {
486
+ shape: "circle",
487
+ x: cutout.center.x,
488
+ y: cutout.center.y,
489
+ radius: cutout.radius,
490
+ }
491
+ }
492
+
493
+ if (cutout.shape === "rect") {
494
+ if (cutout.width <= 0 || cutout.height <= 0) return
495
+ return {
496
+ shape: "rect",
497
+ x: cutout.center.x,
498
+ y: cutout.center.y,
499
+ width: cutout.width,
500
+ height: cutout.height,
501
+ radius: cutout.corner_radius,
502
+ rotation: cutout.rotation,
503
+ }
504
+ }
505
+
506
+ if (cutout.shape === "polygon" && cutout.points.length >= 3) {
507
+ return { shape: "polygon", points: cutout.points }
508
+ }
509
+ }
510
+
458
511
  function getPointBounds(points: Array<{ x: number; y: number }>): Bounds {
459
512
  let minX = Infinity
460
513
  let maxX = -Infinity
@@ -1,4 +1,4 @@
1
- import type { PcbPlatedHole } from "circuit-json"
1
+ import type { LayerRef, PcbPlatedHole } from "circuit-json"
2
2
  import type { Matrix } from "transformation-matrix"
3
3
  import { drawCircle } from "../shapes/circle"
4
4
  import { drawOval } from "../shapes/oval"
@@ -15,7 +15,7 @@ export interface DrawPcbPlatedHoleParams {
15
15
  colorMap: PcbColorMap
16
16
  soldermaskMargin?: number
17
17
  drawSoldermask?: boolean
18
- layer?: "top" | "bottom"
18
+ layer?: LayerRef
19
19
  }
20
20
 
21
21
  export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
@@ -1,10 +1,16 @@
1
- import type { PcbHole, PcbPlatedHole, PcbSmtPad, PcbVia } from "circuit-json"
1
+ import type {
2
+ PcbCutout,
3
+ PcbHole,
4
+ PcbPlatedHole,
5
+ PcbSmtPad,
6
+ PcbVia,
7
+ } from "circuit-json"
2
8
  import type { Matrix } from "transformation-matrix"
3
- import type { PcbColorMap, CanvasContext } from "../types"
4
9
  import { drawCircle } from "../shapes/circle"
5
- import { drawRect } from "../shapes/rect"
6
10
  import { drawPill } from "../shapes/pill"
7
11
  import { drawPolygon } from "../shapes/polygon"
12
+ import { drawRect } from "../shapes/rect"
13
+ import type { CanvasContext, PcbColorMap } from "../types"
8
14
  import {
9
15
  drawPadWithDrillCutouts,
10
16
  getPadDrillCutouts,
@@ -18,6 +24,7 @@ export interface DrawPcbSmtPadParams {
18
24
  holes?: PcbHole[]
19
25
  platedHoles?: PcbPlatedHole[]
20
26
  vias?: PcbVia[]
27
+ cutouts?: PcbCutout[]
21
28
  }
22
29
 
23
30
  function layerToColor(layer: string, colorMap: PcbColorMap): string {
@@ -1,4 +1,4 @@
1
- import type { PCBVia } from "circuit-json"
1
+ import type { LayerRef, PCBVia } from "circuit-json"
2
2
  import type { Matrix } from "transformation-matrix"
3
3
  import { drawCircle } from "../shapes/circle"
4
4
  import type { CanvasContext, PcbColorMap } from "../types"
@@ -8,7 +8,7 @@ export interface DrawPcbViaParams {
8
8
  via: PCBVia
9
9
  realToCanvasMat: Matrix
10
10
  colorMap: PcbColorMap
11
- layer?: "top" | "bottom"
11
+ layer?: LayerRef
12
12
  }
13
13
 
14
14
  export function drawPcbVia(params: DrawPcbViaParams): void {
@@ -1,3 +1,4 @@
1
+ import type { LayerRef } from "circuit-json"
1
2
  import type { Matrix } from "transformation-matrix"
2
3
 
3
4
  /**
@@ -56,15 +57,7 @@ export interface CanvasContext {
56
57
  textAlign: "start" | "end" | "left" | "right" | "center"
57
58
  }
58
59
 
59
- export type CopperLayerName =
60
- | "top"
61
- | "bottom"
62
- | "inner1"
63
- | "inner2"
64
- | "inner3"
65
- | "inner4"
66
- | "inner5"
67
- | "inner6"
60
+ export type CopperLayerName = LayerRef
68
61
 
69
62
  export type CopperColorMap = Record<CopperLayerName, string> & {
70
63
  [layer: string]: string
@@ -115,6 +108,8 @@ export const DEFAULT_PCB_COLOR_MAP: PcbColorMap = {
115
108
  inner4: "rgb(64, 224, 208)",
116
109
  inner5: "rgb(138, 43, 226)",
117
110
  inner6: "rgb(255, 105, 180)",
111
+ inner7: "rgb(232, 178, 167)",
112
+ inner8: "rgb(242, 237, 161)",
118
113
  bottom: "rgb(77, 127, 196)",
119
114
  },
120
115
  copperPour: {
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.110",
4
+ "version": "0.0.112",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -13,13 +13,14 @@
13
13
  "@napi-rs/canvas": "^0.1.84",
14
14
  "@resvg/resvg-js": "^2.6.2",
15
15
  "@tscircuit/alphabet": "^0.0.23",
16
- "@tscircuit/circuit-json-util": "^0.0.91",
16
+ "@tscircuit/circuit-json-util": "^0.0.99",
17
17
  "@tscircuit/math-utils": "^0.0.36",
18
18
  "@types/bun": "latest",
19
19
  "bun-match-svg": "^0.0.14",
20
- "circuit-json": "^0.0.433",
20
+ "circuit-json": "^0.0.450",
21
21
  "circuit-json-to-connectivity-map": "^0.0.23",
22
- "circuit-to-svg": "^0.0.324",
22
+ "circuit-to-svg": "^0.0.390",
23
+ "format-si-unit": "^0.0.7",
23
24
  "looks-same": "^10.0.1",
24
25
  "schematic-symbols": "^0.0.202",
25
26
  "tsup": "^8.5.1"
@@ -1,9 +1,12 @@
1
1
  import { expect, test } from "bun:test"
2
2
  import { createCanvas } from "@napi-rs/canvas"
3
- import type { AnyCircuitElement } from "circuit-json"
3
+ import type { AnyCircuitElement, LayerRef, PcbRenderLayer } from "circuit-json"
4
4
  import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
5
 
6
- function renderCopperPourScene(elements: AnyCircuitElement[]): Buffer {
6
+ function renderCopperPourScene(
7
+ elements: AnyCircuitElement[],
8
+ layer: LayerRef,
9
+ ): Buffer {
7
10
  const canvas = createCanvas(100, 100)
8
11
  const ctx = canvas.getContext("2d")
9
12
  const drawer = new CircuitToCanvasDrawer(ctx)
@@ -11,7 +14,9 @@ function renderCopperPourScene(elements: AnyCircuitElement[]): Buffer {
11
14
  ctx.fillStyle = "#1a1a1a"
12
15
  ctx.fillRect(0, 0, 100, 100)
13
16
 
14
- drawer.drawElements(elements)
17
+ drawer.drawElements(elements, {
18
+ layers: [`${layer}_copper` as PcbRenderLayer],
19
+ })
15
20
 
16
21
  return canvas.toBuffer("image/png")
17
22
  }
@@ -23,6 +28,8 @@ const INNER_LAYERS = [
23
28
  "inner4",
24
29
  "inner5",
25
30
  "inner6",
31
+ "inner7",
32
+ "inner8",
26
33
  ] as const
27
34
 
28
35
  function createCopperPourWithTraceScene(
@@ -78,9 +85,10 @@ for (const layer of INNER_LAYERS) {
78
85
  test(`draw ${layer} copper pour with trace`, async () => {
79
86
  const elements = createCopperPourWithTraceScene(layer)
80
87
 
81
- const withTrace = renderCopperPourScene(elements)
88
+ const withTrace = renderCopperPourScene(elements, layer)
82
89
  const withoutTrace = renderCopperPourScene(
83
90
  elements.filter((element) => element.type !== "pcb_trace"),
91
+ layer,
84
92
  )
85
93
 
86
94
  expect(Buffer.compare(withTrace, withoutTrace)).not.toBe(0)
@@ -0,0 +1,39 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { PcbCutout, PcbSmtPad } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("cutout removes overlapping SMT pad copper", async () => {
7
+ const scale = 4
8
+ const canvas = createCanvas(100 * scale, 100 * scale)
9
+ const ctx = canvas.getContext("2d")
10
+ ctx.scale(scale, scale)
11
+ const drawer = new CircuitToCanvasDrawer(ctx)
12
+ drawer.configure({
13
+ colorOverrides: { drill: "rgba(0,0,0,0)" },
14
+ })
15
+
16
+ const pad: PcbSmtPad = {
17
+ type: "pcb_smtpad",
18
+ pcb_smtpad_id: "pad1",
19
+ shape: "rect",
20
+ layer: "top",
21
+ x: 50,
22
+ y: 50,
23
+ width: 40,
24
+ height: 20,
25
+ }
26
+ const cutout: PcbCutout = {
27
+ type: "pcb_cutout",
28
+ pcb_cutout_id: "cutout1",
29
+ shape: "circle",
30
+ center: { x: 50, y: 50 },
31
+ radius: 5,
32
+ }
33
+
34
+ drawer.drawElements([pad, cutout])
35
+
36
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
37
+ import.meta.path,
38
+ )
39
+ })
@@ -0,0 +1,36 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { PcbRenderLayer, PcbVia } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ const renderViaLayer = (via: PcbVia | undefined, layer: PcbRenderLayer) => {
7
+ const canvas = createCanvas(100, 100)
8
+ const ctx = canvas.getContext("2d")
9
+ const drawer = new CircuitToCanvasDrawer(ctx)
10
+
11
+ ctx.fillStyle = "#1a1a1a"
12
+ ctx.fillRect(0, 0, 100, 100)
13
+ drawer.drawElements(via ? [via] : [], { layers: [layer] })
14
+
15
+ return canvas.toBuffer("image/png")
16
+ }
17
+
18
+ test("draw buried via only on layers in its span", async () => {
19
+ const via: PcbVia = {
20
+ type: "pcb_via",
21
+ pcb_via_id: "via_inner8",
22
+ x: 50,
23
+ y: 50,
24
+ outer_diameter: 40,
25
+ hole_diameter: 20,
26
+ layers: ["inner7", "inner8"],
27
+ }
28
+
29
+ const inner8 = renderViaLayer(via, "inner8_copper")
30
+ const inner6 = renderViaLayer(via, "inner6_copper")
31
+ const blank = renderViaLayer(undefined, "inner6_copper")
32
+
33
+ expect(Buffer.compare(inner8, blank)).not.toBe(0)
34
+ expect(Buffer.compare(inner6, blank)).toBe(0)
35
+ await expect(inner8).toMatchPngSnapshot(import.meta.path)
36
+ })