circuit-to-canvas 0.0.1 → 0.0.3

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.
Files changed (66) hide show
  1. package/.github/workflows/bun-formatcheck.yml +26 -0
  2. package/.github/workflows/bun-pver-release.yml +77 -0
  3. package/.github/workflows/bun-test.yml +31 -0
  4. package/.github/workflows/bun-typecheck.yml +26 -0
  5. package/LICENSE +21 -0
  6. package/README.md +66 -0
  7. package/dist/index.d.ts +146 -2
  8. package/dist/index.js +620 -0
  9. package/lib/drawer/CircuitToCanvasDrawer.ts +138 -1
  10. package/lib/drawer/elements/index.ts +30 -0
  11. package/lib/drawer/elements/pcb-board.ts +62 -0
  12. package/lib/drawer/elements/pcb-copper-pour.ts +84 -0
  13. package/lib/drawer/elements/pcb-cutout.ts +53 -0
  14. package/lib/drawer/elements/pcb-hole.ts +90 -0
  15. package/lib/drawer/elements/pcb-silkscreen.ts +171 -0
  16. package/lib/drawer/elements/pcb-smtpad.ts +108 -0
  17. package/lib/drawer/elements/pcb-trace.ts +59 -0
  18. package/lib/drawer/elements/pcb-via.ts +33 -0
  19. package/lib/drawer/shapes/index.ts +3 -0
  20. package/lib/drawer/shapes/line.ts +37 -0
  21. package/lib/drawer/shapes/path.ts +61 -0
  22. package/lib/drawer/shapes/polygon.ts +38 -0
  23. package/lib/drawer/types.ts +16 -0
  24. package/package.json +2 -2
  25. package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
  26. package/tests/elements/__snapshots__/bottom-layer-pad.snap.png +0 -0
  27. package/tests/elements/__snapshots__/bottom-layer-trace.snap.png +0 -0
  28. package/tests/elements/__snapshots__/circular-cutout.snap.png +0 -0
  29. package/tests/elements/__snapshots__/circular-pad.snap.png +0 -0
  30. package/tests/elements/__snapshots__/copper-pour-with-trace.snap.png +0 -0
  31. package/tests/elements/__snapshots__/custom-outline-board.snap.png +0 -0
  32. package/tests/elements/__snapshots__/multi-segment-trace.snap.png +0 -0
  33. package/tests/elements/__snapshots__/multiple-traces.snap.png +0 -0
  34. package/tests/elements/__snapshots__/multiple-vias.snap.png +0 -0
  35. package/tests/elements/__snapshots__/oval-hole.snap.png +0 -0
  36. package/tests/elements/__snapshots__/pcb-board.snap.png +0 -0
  37. package/tests/elements/__snapshots__/pcb-copper-pour.snap.png +0 -0
  38. package/tests/elements/__snapshots__/pcb-cutout.snap.png +0 -0
  39. package/tests/elements/__snapshots__/pcb-hole.snap.png +0 -0
  40. package/tests/elements/__snapshots__/pcb-silkscreen.snap.png +0 -0
  41. package/tests/elements/__snapshots__/pcb-smtpad.snap.png +0 -0
  42. package/tests/elements/__snapshots__/pcb-trace.snap.png +0 -0
  43. package/tests/elements/__snapshots__/pcb-via.snap.png +0 -0
  44. package/tests/elements/__snapshots__/pill-hole.snap.png +0 -0
  45. package/tests/elements/__snapshots__/pill-pad.snap.png +0 -0
  46. package/tests/elements/__snapshots__/polygon-copper-pour.snap.png +0 -0
  47. package/tests/elements/__snapshots__/polygon-cutout.snap.png +0 -0
  48. package/tests/elements/__snapshots__/polygon-pad.snap.png +0 -0
  49. package/tests/elements/__snapshots__/rect-pad-with-border-radius.snap.png +0 -0
  50. package/tests/elements/__snapshots__/rotated-rect-pad.snap.png +0 -0
  51. package/tests/elements/__snapshots__/silkscreen-circle.snap.png +0 -0
  52. package/tests/elements/__snapshots__/silkscreen-line.snap.png +0 -0
  53. package/tests/elements/__snapshots__/silkscreen-on-component.snap.png +0 -0
  54. package/tests/elements/__snapshots__/silkscreen-path.snap.png +0 -0
  55. package/tests/elements/__snapshots__/silkscreen-rect.snap.png +0 -0
  56. package/tests/elements/__snapshots__/silkscreen-text-bottom.snap.png +0 -0
  57. package/tests/elements/__snapshots__/square-hole.snap.png +0 -0
  58. package/tests/elements/pcb-board.test.ts +155 -0
  59. package/tests/elements/pcb-copper-pour.test.ts +120 -0
  60. package/tests/elements/pcb-cutout.test.ts +82 -0
  61. package/tests/elements/pcb-hole.test.ts +105 -0
  62. package/tests/elements/pcb-silkscreen.test.ts +245 -0
  63. package/tests/elements/pcb-smtpad.test.ts +198 -0
  64. package/tests/elements/pcb-trace.test.ts +116 -0
  65. package/tests/elements/pcb-via.test.ts +75 -0
  66. package/.claude/settings.local.json +0 -7
@@ -0,0 +1,108 @@
1
+ import type { PcbSmtPad } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import type { PcbColorMap, CanvasContext } from "../types"
4
+ import { drawCircle } from "../shapes/circle"
5
+ import { drawRect } from "../shapes/rect"
6
+ import { drawPill } from "../shapes/pill"
7
+ import { drawPolygon } from "../shapes/polygon"
8
+
9
+ export interface DrawPcbSmtPadParams {
10
+ ctx: CanvasContext
11
+ pad: PcbSmtPad
12
+ transform: Matrix
13
+ colorMap: PcbColorMap
14
+ }
15
+
16
+ function layerToColor(layer: string, colorMap: PcbColorMap): string {
17
+ return (
18
+ colorMap.copper[layer as keyof typeof colorMap.copper] ??
19
+ colorMap.copper.top
20
+ )
21
+ }
22
+
23
+ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
24
+ const { ctx, pad, transform, colorMap } = params
25
+
26
+ const color = layerToColor(pad.layer, colorMap)
27
+
28
+ if (pad.shape === "rect") {
29
+ drawRect({
30
+ ctx,
31
+ center: { x: pad.x, y: pad.y },
32
+ width: pad.width,
33
+ height: pad.height,
34
+ fill: color,
35
+ transform,
36
+ borderRadius:
37
+ (pad as { corner_radius?: number }).corner_radius ??
38
+ pad.rect_border_radius ??
39
+ 0,
40
+ })
41
+ return
42
+ }
43
+
44
+ if (pad.shape === "rotated_rect") {
45
+ drawRect({
46
+ ctx,
47
+ center: { x: pad.x, y: pad.y },
48
+ width: pad.width,
49
+ height: pad.height,
50
+ fill: color,
51
+ transform,
52
+ borderRadius:
53
+ (pad as { corner_radius?: number }).corner_radius ??
54
+ pad.rect_border_radius ??
55
+ 0,
56
+ rotation: pad.ccw_rotation ?? 0,
57
+ })
58
+ return
59
+ }
60
+
61
+ if (pad.shape === "circle") {
62
+ drawCircle({
63
+ ctx,
64
+ center: { x: pad.x, y: pad.y },
65
+ radius: pad.radius,
66
+ fill: color,
67
+ transform,
68
+ })
69
+ return
70
+ }
71
+
72
+ if (pad.shape === "pill") {
73
+ drawPill({
74
+ ctx,
75
+ center: { x: pad.x, y: pad.y },
76
+ width: pad.width,
77
+ height: pad.height,
78
+ fill: color,
79
+ transform,
80
+ })
81
+ return
82
+ }
83
+
84
+ if (pad.shape === "rotated_pill") {
85
+ drawPill({
86
+ ctx,
87
+ center: { x: pad.x, y: pad.y },
88
+ width: pad.width,
89
+ height: pad.height,
90
+ fill: color,
91
+ transform,
92
+ rotation: pad.ccw_rotation ?? 0,
93
+ })
94
+ return
95
+ }
96
+
97
+ if (pad.shape === "polygon") {
98
+ if (pad.points && pad.points.length >= 3) {
99
+ drawPolygon({
100
+ ctx,
101
+ points: pad.points,
102
+ fill: color,
103
+ transform,
104
+ })
105
+ }
106
+ return
107
+ }
108
+ }
@@ -0,0 +1,59 @@
1
+ import type { PCBTrace } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import type { PcbColorMap, CanvasContext } from "../types"
4
+ import { drawLine } from "../shapes/line"
5
+
6
+ export interface DrawPcbTraceParams {
7
+ ctx: CanvasContext
8
+ trace: PCBTrace
9
+ transform: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToColor(layer: string, colorMap: PcbColorMap): string {
14
+ return (
15
+ colorMap.copper[layer as keyof typeof colorMap.copper] ??
16
+ colorMap.copper.top
17
+ )
18
+ }
19
+
20
+ export function drawPcbTrace(params: DrawPcbTraceParams): void {
21
+ const { ctx, trace, transform, colorMap } = params
22
+
23
+ if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
24
+ return
25
+ }
26
+
27
+ // Draw each segment of the trace
28
+ for (let i = 0; i < trace.route.length - 1; i++) {
29
+ const start = trace.route[i]
30
+ const end = trace.route[i + 1]
31
+
32
+ if (!start || !end) continue
33
+
34
+ // Get the layer from either point
35
+ const layer =
36
+ "layer" in start
37
+ ? start.layer
38
+ : "layer" in end
39
+ ? (end as any).layer
40
+ : null
41
+ if (!layer) continue
42
+
43
+ const color = layerToColor(layer, colorMap)
44
+
45
+ // Get the trace width from either point
46
+ const traceWidth =
47
+ "width" in start ? start.width : "width" in end ? (end as any).width : 0.1
48
+
49
+ drawLine({
50
+ ctx,
51
+ start: { x: start.x, y: start.y },
52
+ end: { x: end.x, y: end.y },
53
+ strokeWidth: traceWidth,
54
+ stroke: color,
55
+ transform,
56
+ lineCap: "round",
57
+ })
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ import type { PCBVia } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import type { PcbColorMap, CanvasContext } from "../types"
4
+ import { drawCircle } from "../shapes/circle"
5
+
6
+ export interface DrawPcbViaParams {
7
+ ctx: CanvasContext
8
+ via: PCBVia
9
+ transform: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ export function drawPcbVia(params: DrawPcbViaParams): void {
14
+ const { ctx, via, transform, colorMap } = params
15
+
16
+ // Draw outer copper ring
17
+ drawCircle({
18
+ ctx,
19
+ center: { x: via.x, y: via.y },
20
+ radius: via.outer_diameter / 2,
21
+ fill: colorMap.copper.top,
22
+ transform,
23
+ })
24
+
25
+ // Draw inner drill hole
26
+ drawCircle({
27
+ ctx,
28
+ center: { x: via.x, y: via.y },
29
+ radius: via.hole_diameter / 2,
30
+ fill: colorMap.drill,
31
+ transform,
32
+ })
33
+ }
@@ -2,3 +2,6 @@ export { drawCircle, type DrawCircleParams } from "./circle"
2
2
  export { drawRect, type DrawRectParams } from "./rect"
3
3
  export { drawOval, type DrawOvalParams } from "./oval"
4
4
  export { drawPill, type DrawPillParams } from "./pill"
5
+ export { drawPolygon, type DrawPolygonParams } from "./polygon"
6
+ export { drawLine, type DrawLineParams } from "./line"
7
+ export { drawPath, type DrawPathParams } from "./path"
@@ -0,0 +1,37 @@
1
+ import type { Matrix } from "transformation-matrix"
2
+ import { applyToPoint } from "transformation-matrix"
3
+ import type { CanvasContext } from "../types"
4
+
5
+ export interface DrawLineParams {
6
+ ctx: CanvasContext
7
+ start: { x: number; y: number }
8
+ end: { x: number; y: number }
9
+ strokeWidth: number
10
+ stroke: string
11
+ transform: Matrix
12
+ lineCap?: "butt" | "round" | "square"
13
+ }
14
+
15
+ export function drawLine(params: DrawLineParams): void {
16
+ const {
17
+ ctx,
18
+ start,
19
+ end,
20
+ strokeWidth,
21
+ stroke,
22
+ transform,
23
+ lineCap = "round",
24
+ } = params
25
+
26
+ const [x1, y1] = applyToPoint(transform, [start.x, start.y])
27
+ const [x2, y2] = applyToPoint(transform, [end.x, end.y])
28
+ const scaledStrokeWidth = strokeWidth * Math.abs(transform.a)
29
+
30
+ ctx.beginPath()
31
+ ctx.moveTo(x1, y1)
32
+ ctx.lineTo(x2, y2)
33
+ ctx.lineWidth = scaledStrokeWidth
34
+ ctx.strokeStyle = stroke
35
+ ctx.lineCap = lineCap
36
+ ctx.stroke()
37
+ }
@@ -0,0 +1,61 @@
1
+ import type { Matrix } from "transformation-matrix"
2
+ import { applyToPoint } from "transformation-matrix"
3
+ import type { CanvasContext } from "../types"
4
+
5
+ export interface DrawPathParams {
6
+ ctx: CanvasContext
7
+ points: Array<{ x: number; y: number }>
8
+ fill?: string
9
+ stroke?: string
10
+ strokeWidth?: number
11
+ transform: Matrix
12
+ closePath?: boolean
13
+ }
14
+
15
+ export function drawPath(params: DrawPathParams): void {
16
+ const {
17
+ ctx,
18
+ points,
19
+ fill,
20
+ stroke,
21
+ strokeWidth = 1,
22
+ transform,
23
+ closePath = false,
24
+ } = params
25
+
26
+ if (points.length < 2) return
27
+
28
+ ctx.beginPath()
29
+
30
+ const transformedPoints = points.map((p) =>
31
+ applyToPoint(transform, [p.x, p.y]),
32
+ )
33
+
34
+ const firstPoint = transformedPoints[0]
35
+ if (!firstPoint) return
36
+ const [firstX, firstY] = firstPoint
37
+ ctx.moveTo(firstX, firstY)
38
+
39
+ for (let i = 1; i < transformedPoints.length; i++) {
40
+ const point = transformedPoints[i]
41
+ if (!point) continue
42
+ const [x, y] = point
43
+ ctx.lineTo(x, y)
44
+ }
45
+
46
+ if (closePath) {
47
+ ctx.closePath()
48
+ }
49
+
50
+ if (fill) {
51
+ ctx.fillStyle = fill
52
+ ctx.fill()
53
+ }
54
+
55
+ if (stroke) {
56
+ const scaledStrokeWidth = strokeWidth * Math.abs(transform.a)
57
+ ctx.strokeStyle = stroke
58
+ ctx.lineWidth = scaledStrokeWidth
59
+ ctx.stroke()
60
+ }
61
+ }
@@ -0,0 +1,38 @@
1
+ import type { Matrix } from "transformation-matrix"
2
+ import { applyToPoint } from "transformation-matrix"
3
+ import type { CanvasContext } from "../types"
4
+
5
+ export interface DrawPolygonParams {
6
+ ctx: CanvasContext
7
+ points: Array<{ x: number; y: number }>
8
+ fill: string
9
+ transform: Matrix
10
+ }
11
+
12
+ export function drawPolygon(params: DrawPolygonParams): void {
13
+ const { ctx, points, fill, transform } = params
14
+
15
+ if (points.length < 3) return
16
+
17
+ ctx.beginPath()
18
+
19
+ const transformedPoints = points.map((p) =>
20
+ applyToPoint(transform, [p.x, p.y]),
21
+ )
22
+
23
+ const firstPoint = transformedPoints[0]
24
+ if (!firstPoint) return
25
+ const [firstX, firstY] = firstPoint
26
+ ctx.moveTo(firstX, firstY)
27
+
28
+ for (let i = 1; i < transformedPoints.length; i++) {
29
+ const point = transformedPoints[i]
30
+ if (!point) continue
31
+ const [x, y] = point
32
+ ctx.lineTo(x, y)
33
+ }
34
+
35
+ ctx.closePath()
36
+ ctx.fillStyle = fill
37
+ ctx.fill()
38
+ }
@@ -25,6 +25,7 @@ export interface CanvasContext {
25
25
  endAngle: number,
26
26
  ): void
27
27
  fill(): void
28
+ stroke(): void
28
29
  rect(x: number, y: number, w: number, h: number): void
29
30
  lineTo(x: number, y: number): void
30
31
  moveTo(x: number, y: number): void
@@ -32,8 +33,23 @@ export interface CanvasContext {
32
33
  restore(): void
33
34
  translate(x: number, y: number): void
34
35
  rotate(angle: number): void
36
+ scale(x: number, y: number): void
35
37
  fillStyle: string | CanvasGradient | CanvasPattern
38
+ strokeStyle: string | CanvasGradient | CanvasPattern
39
+ lineWidth: number
40
+ lineCap: "butt" | "round" | "square"
41
+ lineJoin: "bevel" | "round" | "miter"
36
42
  canvas: { width: number; height: number }
43
+ fillText(text: string, x: number, y: number): void
44
+ font: string
45
+ textAlign: "start" | "end" | "left" | "right" | "center"
46
+ textBaseline:
47
+ | "top"
48
+ | "hanging"
49
+ | "middle"
50
+ | "alphabetic"
51
+ | "ideographic"
52
+ | "bottom"
37
53
  }
38
54
 
39
55
  export type CopperLayerName =
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "circuit-to-canvas",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
8
8
  "format": "biome format --write .",
9
- "format:check": "biome format --check ."
9
+ "format:check": "biome format ."
10
10
  },
11
11
  "devDependencies": {
12
12
  "@biomejs/biome": "^2.3.8",
@@ -0,0 +1,155 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "canvas"
3
+ import type { PcbBoard } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("draw rectangular board", 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 board: PcbBoard = {
15
+ type: "pcb_board",
16
+ pcb_board_id: "board1",
17
+ center: { x: 50, y: 50 },
18
+ width: 80,
19
+ height: 60,
20
+ thickness: 1.6,
21
+ num_layers: 2,
22
+ material: "fr4",
23
+ }
24
+
25
+ drawer.drawElements([board])
26
+
27
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
28
+ import.meta.path,
29
+ )
30
+ })
31
+
32
+ test("draw board with custom outline", async () => {
33
+ const canvas = createCanvas(100, 100)
34
+ const ctx = canvas.getContext("2d")
35
+ const drawer = new CircuitToCanvasDrawer(ctx)
36
+
37
+ ctx.fillStyle = "#1a1a1a"
38
+ ctx.fillRect(0, 0, 100, 100)
39
+
40
+ // L-shaped board outline
41
+ const board: PcbBoard = {
42
+ type: "pcb_board",
43
+ pcb_board_id: "board1",
44
+ center: { x: 50, y: 50 },
45
+ width: 80,
46
+ height: 80,
47
+ thickness: 1.6,
48
+ num_layers: 2,
49
+ material: "fr4",
50
+ outline: [
51
+ { x: 10, y: 10 },
52
+ { x: 90, y: 10 },
53
+ { x: 90, y: 50 },
54
+ { x: 50, y: 50 },
55
+ { x: 50, y: 90 },
56
+ { x: 10, y: 90 },
57
+ ],
58
+ }
59
+
60
+ drawer.drawElements([board])
61
+
62
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
63
+ import.meta.path,
64
+ "custom-outline-board",
65
+ )
66
+ })
67
+
68
+ test("draw board with elements", async () => {
69
+ const canvas = createCanvas(200, 150)
70
+ const ctx = canvas.getContext("2d")
71
+ const drawer = new CircuitToCanvasDrawer(ctx)
72
+
73
+ ctx.fillStyle = "#1a1a1a"
74
+ ctx.fillRect(0, 0, 200, 150)
75
+
76
+ const elements = [
77
+ {
78
+ type: "pcb_board" as const,
79
+ pcb_board_id: "board1",
80
+ center: { x: 100, y: 75 },
81
+ width: 180,
82
+ height: 130,
83
+ },
84
+ {
85
+ type: "pcb_smtpad" as const,
86
+ pcb_smtpad_id: "pad1",
87
+ shape: "rect" as const,
88
+ x: 50,
89
+ y: 50,
90
+ width: 20,
91
+ height: 10,
92
+ layer: "top" as const,
93
+ },
94
+ {
95
+ type: "pcb_smtpad" as const,
96
+ pcb_smtpad_id: "pad2",
97
+ shape: "rect" as const,
98
+ x: 150,
99
+ y: 50,
100
+ width: 20,
101
+ height: 10,
102
+ layer: "top" as const,
103
+ },
104
+ {
105
+ type: "pcb_trace" as const,
106
+ pcb_trace_id: "trace1",
107
+ route: [
108
+ {
109
+ route_type: "wire" as const,
110
+ x: 50,
111
+ y: 50,
112
+ width: 3,
113
+ layer: "top" as const,
114
+ },
115
+ {
116
+ route_type: "wire" as const,
117
+ x: 100,
118
+ y: 50,
119
+ width: 3,
120
+ layer: "top" as const,
121
+ },
122
+ {
123
+ route_type: "wire" as const,
124
+ x: 100,
125
+ y: 100,
126
+ width: 3,
127
+ layer: "top" as const,
128
+ },
129
+ {
130
+ route_type: "wire" as const,
131
+ x: 150,
132
+ y: 100,
133
+ width: 3,
134
+ layer: "top" as const,
135
+ },
136
+ ],
137
+ },
138
+ {
139
+ type: "pcb_via" as const,
140
+ pcb_via_id: "via1",
141
+ x: 100,
142
+ y: 75,
143
+ outer_diameter: 10,
144
+ hole_diameter: 5,
145
+ layers: ["top", "bottom"],
146
+ },
147
+ ]
148
+
149
+ drawer.drawElements(elements)
150
+
151
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
152
+ import.meta.path,
153
+ "board-with-elements",
154
+ )
155
+ })