circuit-to-canvas 0.0.28 → 0.0.30

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, NinePointAnchor, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension } from 'circuit-json';
1
+ import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension } from 'circuit-json';
2
2
  import { Matrix } from 'transformation-matrix';
3
3
 
4
4
  /**
@@ -86,7 +86,7 @@ interface DrawContext {
86
86
  }
87
87
 
88
88
  interface DrawElementsOptions {
89
- layers?: string[];
89
+ layers?: PcbRenderLayer[];
90
90
  }
91
91
  interface CanvasLike {
92
92
  getContext(contextId: "2d"): CanvasContext | null;
package/dist/index.js CHANGED
@@ -1,3 +1,16 @@
1
+ // lib/drawer/pcb-render-layer-filter.ts
2
+ import { getElementRenderLayers } from "@tscircuit/circuit-json-util";
3
+ function shouldDrawElement(element, options) {
4
+ if (!options.layers || options.layers.length === 0) {
5
+ return true;
6
+ }
7
+ const elementLayers = getElementRenderLayers(element);
8
+ if (elementLayers.length === 0) {
9
+ return true;
10
+ }
11
+ return elementLayers.some((layer) => options.layers.includes(layer));
12
+ }
13
+
1
14
  // lib/drawer/CircuitToCanvasDrawer.ts
2
15
  import { identity, compose, translate, scale } from "transformation-matrix";
3
16
 
@@ -196,6 +209,30 @@ function drawPill(params) {
196
209
  ctx.restore();
197
210
  }
198
211
 
212
+ // lib/drawer/shapes/polygon.ts
213
+ import { applyToPoint as applyToPoint5 } from "transformation-matrix";
214
+ function drawPolygon(params) {
215
+ const { ctx, points, fill, realToCanvasMat } = params;
216
+ if (points.length < 3) return;
217
+ ctx.beginPath();
218
+ const canvasPoints = points.map(
219
+ (p) => applyToPoint5(realToCanvasMat, [p.x, p.y])
220
+ );
221
+ const firstPoint = canvasPoints[0];
222
+ if (!firstPoint) return;
223
+ const [firstX, firstY] = firstPoint;
224
+ ctx.moveTo(firstX, firstY);
225
+ for (let i = 1; i < canvasPoints.length; i++) {
226
+ const point = canvasPoints[i];
227
+ if (!point) continue;
228
+ const [x, y] = point;
229
+ ctx.lineTo(x, y);
230
+ }
231
+ ctx.closePath();
232
+ ctx.fillStyle = fill;
233
+ ctx.fill();
234
+ }
235
+
199
236
  // lib/drawer/elements/pcb-plated-hole.ts
200
237
  function drawPcbPlatedHole(params) {
201
238
  const { ctx, hole, realToCanvasMat, colorMap } = params;
@@ -325,6 +362,61 @@ function drawPcbPlatedHole(params) {
325
362
  });
326
363
  return;
327
364
  }
365
+ if (hole.shape === "hole_with_polygon_pad") {
366
+ const padOutline = hole.pad_outline;
367
+ if (padOutline && padOutline.length >= 3) {
368
+ const padPoints = padOutline.map((point) => ({
369
+ x: hole.x + point.x,
370
+ y: hole.y + point.y
371
+ }));
372
+ drawPolygon({
373
+ ctx,
374
+ points: padPoints,
375
+ fill: colorMap.copper.top,
376
+ realToCanvasMat
377
+ });
378
+ }
379
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
380
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
381
+ const holeShape = hole.hole_shape;
382
+ if (holeShape === "circle") {
383
+ drawCircle({
384
+ ctx,
385
+ center: { x: holeX, y: holeY },
386
+ radius: (hole.hole_diameter ?? 0) / 2,
387
+ fill: colorMap.drill,
388
+ realToCanvasMat
389
+ });
390
+ } else if (holeShape === "oval") {
391
+ drawOval({
392
+ ctx,
393
+ center: { x: holeX, y: holeY },
394
+ width: hole.hole_width ?? 0,
395
+ height: hole.hole_height ?? 0,
396
+ fill: colorMap.drill,
397
+ realToCanvasMat
398
+ });
399
+ } else if (holeShape === "pill") {
400
+ drawPill({
401
+ ctx,
402
+ center: { x: holeX, y: holeY },
403
+ width: hole.hole_width ?? 0,
404
+ height: hole.hole_height ?? 0,
405
+ fill: colorMap.drill,
406
+ realToCanvasMat
407
+ });
408
+ } else if (holeShape === "rotated_pill") {
409
+ drawPill({
410
+ ctx,
411
+ center: { x: holeX, y: holeY },
412
+ width: hole.hole_width ?? 0,
413
+ height: hole.hole_height ?? 0,
414
+ fill: colorMap.drill,
415
+ realToCanvasMat
416
+ });
417
+ }
418
+ return;
419
+ }
328
420
  }
329
421
 
330
422
  // lib/drawer/elements/pcb-via.ts
@@ -417,30 +509,6 @@ function drawPcbHole(params) {
417
509
  }
418
510
  }
419
511
 
420
- // lib/drawer/shapes/polygon.ts
421
- import { applyToPoint as applyToPoint5 } from "transformation-matrix";
422
- function drawPolygon(params) {
423
- const { ctx, points, fill, realToCanvasMat } = params;
424
- if (points.length < 3) return;
425
- ctx.beginPath();
426
- const canvasPoints = points.map(
427
- (p) => applyToPoint5(realToCanvasMat, [p.x, p.y])
428
- );
429
- const firstPoint = canvasPoints[0];
430
- if (!firstPoint) return;
431
- const [firstX, firstY] = firstPoint;
432
- ctx.moveTo(firstX, firstY);
433
- for (let i = 1; i < canvasPoints.length; i++) {
434
- const point = canvasPoints[i];
435
- if (!point) continue;
436
- const [x, y] = point;
437
- ctx.lineTo(x, y);
438
- }
439
- ctx.closePath();
440
- ctx.fillStyle = fill;
441
- ctx.fill();
442
- }
443
-
444
512
  // lib/drawer/elements/pcb-smtpad.ts
445
513
  function layerToColor(layer, colorMap) {
446
514
  return colorMap.copper[layer] ?? colorMap.copper.top;
@@ -1461,6 +1529,9 @@ var CircuitToCanvasDrawer = class {
1461
1529
  }
1462
1530
  }
1463
1531
  drawElement(element, options) {
1532
+ if (!shouldDrawElement(element, options)) {
1533
+ return;
1534
+ }
1464
1535
  if (element.type === "pcb_plated_hole") {
1465
1536
  drawPcbPlatedHole({
1466
1537
  ctx: this.ctx,
@@ -1,10 +1,10 @@
1
1
  import type {
2
2
  AnyCircuitElement,
3
3
  PcbPlatedHole,
4
- PCBVia,
5
- PCBHole,
4
+ PcbVia,
5
+ PcbHole,
6
6
  PcbSmtPad,
7
- PCBTrace,
7
+ PcbTrace,
8
8
  PcbBoard,
9
9
  PcbSilkscreenText,
10
10
  PcbSilkscreenRect,
@@ -23,7 +23,9 @@ import type {
23
23
  PcbNoteText,
24
24
  PcbNoteDimension,
25
25
  PcbNoteLine,
26
+ PcbRenderLayer,
26
27
  } from "circuit-json"
28
+ import { shouldDrawElement } from "./pcb-render-layer-filter"
27
29
  import { identity, compose, translate, scale } from "transformation-matrix"
28
30
  import type { Matrix } from "transformation-matrix"
29
31
  import {
@@ -58,7 +60,7 @@ import { drawPcbNoteDimension } from "./elements/pcb-note-dimension"
58
60
  import { drawPcbNoteLine } from "./elements/pcb-note-line"
59
61
 
60
62
  export interface DrawElementsOptions {
61
- layers?: string[]
63
+ layers?: PcbRenderLayer[]
62
64
  }
63
65
 
64
66
  interface CanvasLike {
@@ -155,6 +157,11 @@ export class CircuitToCanvasDrawer {
155
157
  element: AnyCircuitElement,
156
158
  options: DrawElementsOptions,
157
159
  ): void {
160
+ // Check if element should be drawn based on layer options
161
+ if (!shouldDrawElement(element, options)) {
162
+ return
163
+ }
164
+
158
165
  if (element.type === "pcb_plated_hole") {
159
166
  drawPcbPlatedHole({
160
167
  ctx: this.ctx,
@@ -167,7 +174,7 @@ export class CircuitToCanvasDrawer {
167
174
  if (element.type === "pcb_via") {
168
175
  drawPcbVia({
169
176
  ctx: this.ctx,
170
- via: element as PCBVia,
177
+ via: element as PcbVia,
171
178
  realToCanvasMat: this.realToCanvasMat,
172
179
  colorMap: this.colorMap,
173
180
  })
@@ -176,7 +183,7 @@ export class CircuitToCanvasDrawer {
176
183
  if (element.type === "pcb_hole") {
177
184
  drawPcbHole({
178
185
  ctx: this.ctx,
179
- hole: element as PCBHole,
186
+ hole: element as PcbHole,
180
187
  realToCanvasMat: this.realToCanvasMat,
181
188
  colorMap: this.colorMap,
182
189
  })
@@ -194,7 +201,7 @@ export class CircuitToCanvasDrawer {
194
201
  if (element.type === "pcb_trace") {
195
202
  drawPcbTrace({
196
203
  ctx: this.ctx,
197
- trace: element as PCBTrace,
204
+ trace: element as PcbTrace,
198
205
  realToCanvasMat: this.realToCanvasMat,
199
206
  colorMap: this.colorMap,
200
207
  })
@@ -5,6 +5,7 @@ import { drawCircle } from "../shapes/circle"
5
5
  import { drawRect } from "../shapes/rect"
6
6
  import { drawOval } from "../shapes/oval"
7
7
  import { drawPill } from "../shapes/pill"
8
+ import { drawPolygon } from "../shapes/polygon"
8
9
 
9
10
  export interface DrawPcbPlatedHoleParams {
10
11
  ctx: CanvasContext
@@ -96,12 +97,12 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
96
97
  height: hole.rect_pad_height,
97
98
  fill: colorMap.copper.top,
98
99
  realToCanvasMat,
99
- borderRadius: (hole as any).rect_border_radius ?? 0,
100
+ borderRadius: hole.rect_border_radius ?? 0,
100
101
  })
101
102
 
102
103
  // Draw circular drill hole (with offset)
103
- const holeX = hole.x + ((hole as any).hole_offset_x ?? 0)
104
- const holeY = hole.y + ((hole as any).hole_offset_y ?? 0)
104
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
105
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
105
106
  drawCircle({
106
107
  ctx,
107
108
  center: { x: holeX, y: holeY },
@@ -121,12 +122,12 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
121
122
  height: hole.rect_pad_height,
122
123
  fill: colorMap.copper.top,
123
124
  realToCanvasMat,
124
- borderRadius: (hole as any).rect_border_radius ?? 0,
125
+ borderRadius: hole.rect_border_radius ?? 0,
125
126
  })
126
127
 
127
128
  // Draw pill drill hole (with offset)
128
- const holeX = hole.x + ((hole as any).hole_offset_x ?? 0)
129
- const holeY = hole.y + ((hole as any).hole_offset_y ?? 0)
129
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
130
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
130
131
  drawPill({
131
132
  ctx,
132
133
  center: { x: holeX, y: holeY },
@@ -147,13 +148,13 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
147
148
  height: hole.rect_pad_height,
148
149
  fill: colorMap.copper.top,
149
150
  realToCanvasMat,
150
- borderRadius: (hole as any).rect_border_radius ?? 0,
151
+ borderRadius: hole.rect_border_radius ?? 0,
151
152
  rotation: hole.rect_ccw_rotation,
152
153
  })
153
154
 
154
155
  // Draw rotated pill drill hole (with offset)
155
- const holeX = hole.x + ((hole as any).hole_offset_x ?? 0)
156
- const holeY = hole.y + ((hole as any).hole_offset_y ?? 0)
156
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
157
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
157
158
  drawPill({
158
159
  ctx,
159
160
  center: { x: holeX, y: holeY },
@@ -165,4 +166,65 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
165
166
  })
166
167
  return
167
168
  }
169
+
170
+ if (hole.shape === "hole_with_polygon_pad") {
171
+ // Draw polygon pad
172
+ const padOutline = hole.pad_outline
173
+ if (padOutline && padOutline.length >= 3) {
174
+ // Transform pad_outline points to be relative to hole.x, hole.y
175
+ const padPoints = padOutline.map((point: { x: number; y: number }) => ({
176
+ x: hole.x + point.x,
177
+ y: hole.y + point.y,
178
+ }))
179
+ drawPolygon({
180
+ ctx,
181
+ points: padPoints,
182
+ fill: colorMap.copper.top,
183
+ realToCanvasMat,
184
+ })
185
+ }
186
+
187
+ // Draw drill hole (with offset)
188
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
189
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
190
+ const holeShape = hole.hole_shape
191
+
192
+ if (holeShape === "circle") {
193
+ drawCircle({
194
+ ctx,
195
+ center: { x: holeX, y: holeY },
196
+ radius: (hole.hole_diameter ?? 0) / 2,
197
+ fill: colorMap.drill,
198
+ realToCanvasMat,
199
+ })
200
+ } else if (holeShape === "oval") {
201
+ drawOval({
202
+ ctx,
203
+ center: { x: holeX, y: holeY },
204
+ width: hole.hole_width ?? 0,
205
+ height: hole.hole_height ?? 0,
206
+ fill: colorMap.drill,
207
+ realToCanvasMat,
208
+ })
209
+ } else if (holeShape === "pill") {
210
+ drawPill({
211
+ ctx,
212
+ center: { x: holeX, y: holeY },
213
+ width: hole.hole_width ?? 0,
214
+ height: hole.hole_height ?? 0,
215
+ fill: colorMap.drill,
216
+ realToCanvasMat,
217
+ })
218
+ } else if (holeShape === "rotated_pill") {
219
+ drawPill({
220
+ ctx,
221
+ center: { x: holeX, y: holeY },
222
+ width: hole.hole_width ?? 0,
223
+ height: hole.hole_height ?? 0,
224
+ fill: colorMap.drill,
225
+ realToCanvasMat,
226
+ })
227
+ }
228
+ return
229
+ }
168
230
  }
@@ -0,0 +1,30 @@
1
+ import type { AnyCircuitElement } from "circuit-json"
2
+ import type { DrawElementsOptions } from "./CircuitToCanvasDrawer"
3
+ import { getElementRenderLayers } from "@tscircuit/circuit-json-util"
4
+
5
+ /**
6
+ * Gets the render layer(s) for an element based on its type and layer property
7
+ */
8
+
9
+ /**
10
+ * Checks if an element should be drawn based on layer options
11
+ */
12
+ export function shouldDrawElement(
13
+ element: AnyCircuitElement,
14
+ options: DrawElementsOptions,
15
+ ): boolean {
16
+ // If no layers specified, draw all elements
17
+ if (!options.layers || options.layers.length === 0) {
18
+ return true
19
+ }
20
+
21
+ const elementLayers = getElementRenderLayers(element)
22
+
23
+ // If element has no layer info (board, holes, etc.), always draw
24
+ if (elementLayers.length === 0) {
25
+ return true
26
+ }
27
+
28
+ // Check if any of the element's layers match the requested layers
29
+ return elementLayers.some((layer) => options.layers!.includes(layer))
30
+ }
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.28",
4
+ "version": "0.0.30",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -13,11 +13,11 @@
13
13
  "@napi-rs/canvas": "^0.1.84",
14
14
  "@resvg/resvg-js": "^2.6.2",
15
15
  "@tscircuit/alphabet": "^0.0.17",
16
- "@tscircuit/circuit-json-util": "^0.0.73",
16
+ "@tscircuit/circuit-json-util": "^0.0.75",
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
19
19
  "bun-match-svg": "^0.0.14",
20
- "circuit-json": "^0.0.342",
20
+ "circuit-json": "^0.0.346",
21
21
  "circuit-json-to-connectivity-map": "^0.0.23",
22
22
  "circuit-to-svg": "^0.0.297",
23
23
  "looks-same": "^10.0.1",
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { PcbSmtPad } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("layer filter only draws elements on specified layers", async () => {
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
+
14
+ const topPad: PcbSmtPad = {
15
+ type: "pcb_smtpad",
16
+ pcb_smtpad_id: "pad1",
17
+ shape: "rect",
18
+ x: 30,
19
+ y: 50,
20
+ width: 20,
21
+ height: 20,
22
+ layer: "top",
23
+ }
24
+
25
+ const bottomPad: PcbSmtPad = {
26
+ type: "pcb_smtpad",
27
+ pcb_smtpad_id: "pad2",
28
+ shape: "rect",
29
+ x: 70,
30
+ y: 50,
31
+ width: 20,
32
+ height: 20,
33
+ layer: "bottom",
34
+ }
35
+
36
+ // Draw only top layer - should only show top pad (left side)
37
+ drawer.drawElements([topPad, bottomPad], { layers: ["top_copper"] })
38
+
39
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
40
+ import.meta.path,
41
+ )
42
+ })
@@ -0,0 +1,111 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { PcbPlatedHole } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("draw hole with polygon pad - circle hole", async () => {
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
+
14
+ const hole: PcbPlatedHole = {
15
+ type: "pcb_plated_hole",
16
+ pcb_plated_hole_id: "hole1",
17
+ shape: "hole_with_polygon_pad",
18
+ x: 50,
19
+ y: 50,
20
+ hole_shape: "circle",
21
+ hole_diameter: 15,
22
+ pad_outline: [
23
+ { x: -20, y: -20 },
24
+ { x: 20, y: -20 },
25
+ { x: 25, y: 0 },
26
+ { x: 20, y: 20 },
27
+ { x: -20, y: 20 },
28
+ { x: -25, y: 0 },
29
+ ],
30
+ layers: ["top", "bottom"],
31
+ } as any
32
+
33
+ drawer.drawElements([hole])
34
+
35
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
36
+ import.meta.path,
37
+ "hole-with-polygon-pad-circle",
38
+ )
39
+ })
40
+
41
+ test("draw hole with polygon pad - pill hole with offset", async () => {
42
+ const canvas = createCanvas(100, 100)
43
+ const ctx = canvas.getContext("2d")
44
+ const drawer = new CircuitToCanvasDrawer(ctx)
45
+
46
+ ctx.fillStyle = "#1a1a1a"
47
+ ctx.fillRect(0, 0, 100, 100)
48
+
49
+ const hole: PcbPlatedHole = {
50
+ type: "pcb_plated_hole",
51
+ pcb_plated_hole_id: "hole1",
52
+ shape: "hole_with_polygon_pad",
53
+ x: 50,
54
+ y: 50,
55
+ hole_shape: "pill",
56
+ hole_width: 12,
57
+ hole_height: 8,
58
+ hole_offset_x: 5,
59
+ hole_offset_y: -3,
60
+ pad_outline: [
61
+ { x: -25, y: -15 },
62
+ { x: 25, y: -15 },
63
+ { x: 30, y: 0 },
64
+ { x: 25, y: 15 },
65
+ { x: -25, y: 15 },
66
+ { x: -30, y: 0 },
67
+ ],
68
+ layers: ["top", "bottom"],
69
+ } as any
70
+
71
+ drawer.drawElements([hole])
72
+
73
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
74
+ import.meta.path,
75
+ "hole-with-polygon-pad-pill-offset",
76
+ )
77
+ })
78
+
79
+ test("draw hole with polygon pad - pill hole", async () => {
80
+ const canvas = createCanvas(100, 100)
81
+ const ctx = canvas.getContext("2d")
82
+ const drawer = new CircuitToCanvasDrawer(ctx)
83
+
84
+ ctx.fillStyle = "#1a1a1a"
85
+ ctx.fillRect(0, 0, 100, 100)
86
+
87
+ const hole: PcbPlatedHole = {
88
+ type: "pcb_plated_hole",
89
+ pcb_plated_hole_id: "hole1",
90
+ shape: "hole_with_polygon_pad",
91
+ x: 50,
92
+ y: 50,
93
+ hole_shape: "rotated_pill",
94
+ hole_width: 15,
95
+ hole_height: 10,
96
+ pad_outline: [
97
+ { x: -20, y: -20 },
98
+ { x: 20, y: -20 },
99
+ { x: 20, y: 20 },
100
+ { x: -20, y: 20 },
101
+ ],
102
+ layers: ["top", "bottom"],
103
+ } as any
104
+
105
+ drawer.drawElements([hole])
106
+
107
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
108
+ import.meta.path,
109
+ "hole-with-polygon-pad-pill",
110
+ )
111
+ })