circuit-to-canvas 0.0.1
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/.claude/settings.local.json +7 -0
- package/README.md +30 -0
- package/biome.json +93 -0
- package/bunfig.toml +6 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +375 -0
- package/lib/drawer/CircuitToCanvasDrawer.ts +119 -0
- package/lib/drawer/elements/index.ts +4 -0
- package/lib/drawer/elements/pcb-plated-hole.ts +168 -0
- package/lib/drawer/index.ts +5 -0
- package/lib/drawer/shapes/circle.ts +23 -0
- package/lib/drawer/shapes/index.ts +4 -0
- package/lib/drawer/shapes/oval.ts +34 -0
- package/lib/drawer/shapes/pill.ts +60 -0
- package/lib/drawer/shapes/rect.ts +69 -0
- package/lib/drawer/types.ts +125 -0
- package/lib/index.ts +1 -0
- package/lib/pcb/index.ts +5 -0
- package/package.json +25 -0
- package/tests/__snapshots__/svg.snap.svg +3 -0
- package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
- package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
- package/tests/elements/pcb-plated-hole.test.ts +90 -0
- package/tests/fixtures/png-matcher.ts +159 -0
- package/tests/fixtures/preload.ts +2 -0
- package/tests/shapes/__snapshots__/circle.snap.png +0 -0
- package/tests/shapes/__snapshots__/oval.snap.png +0 -0
- package/tests/shapes/__snapshots__/pill-vertical.snap.png +0 -0
- package/tests/shapes/__snapshots__/pill.snap.png +0 -0
- package/tests/shapes/__snapshots__/rect-rounded.snap.png +0 -0
- package/tests/shapes/__snapshots__/rect.snap.png +0 -0
- package/tests/shapes/circle.test.ts +24 -0
- package/tests/shapes/oval.test.ts +25 -0
- package/tests/shapes/pill.test.ts +47 -0
- package/tests/shapes/rect.test.ts +48 -0
- package/tests/svg.test.ts +11 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { AnyCircuitElement, PcbPlatedHole } from "circuit-json"
|
|
2
|
+
import { identity, compose, translate, scale } from "transformation-matrix"
|
|
3
|
+
import type { Matrix } from "transformation-matrix"
|
|
4
|
+
import {
|
|
5
|
+
type CanvasContext,
|
|
6
|
+
type PcbColorMap,
|
|
7
|
+
type DrawerConfig,
|
|
8
|
+
type CameraBounds,
|
|
9
|
+
DEFAULT_PCB_COLOR_MAP,
|
|
10
|
+
} from "./types"
|
|
11
|
+
import { drawPcbPlatedHole } from "./elements/pcb-plated-hole"
|
|
12
|
+
|
|
13
|
+
export interface DrawElementsOptions {
|
|
14
|
+
layers?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CanvasLike {
|
|
18
|
+
getContext(contextId: "2d"): CanvasContext | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CircuitToCanvasDrawer {
|
|
22
|
+
private ctx: CanvasContext
|
|
23
|
+
private colorMap: PcbColorMap
|
|
24
|
+
public realToCanvasMat: Matrix
|
|
25
|
+
|
|
26
|
+
constructor(canvasOrContext: CanvasLike | CanvasContext) {
|
|
27
|
+
// Check if it's a canvas element (works in both browser and Node.js)
|
|
28
|
+
if (
|
|
29
|
+
"getContext" in canvasOrContext &&
|
|
30
|
+
typeof canvasOrContext.getContext === "function"
|
|
31
|
+
) {
|
|
32
|
+
const ctx = canvasOrContext.getContext("2d")
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
throw new Error("Failed to get 2D rendering context from canvas")
|
|
35
|
+
}
|
|
36
|
+
this.ctx = ctx
|
|
37
|
+
} else {
|
|
38
|
+
this.ctx = canvasOrContext as CanvasContext
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.colorMap = { ...DEFAULT_PCB_COLOR_MAP }
|
|
42
|
+
this.realToCanvasMat = identity()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
configure(config: DrawerConfig): void {
|
|
46
|
+
if (config.colorOverrides) {
|
|
47
|
+
this.colorMap = {
|
|
48
|
+
...this.colorMap,
|
|
49
|
+
...config.colorOverrides,
|
|
50
|
+
copper: {
|
|
51
|
+
...this.colorMap.copper,
|
|
52
|
+
...config.colorOverrides.copper,
|
|
53
|
+
},
|
|
54
|
+
silkscreen: {
|
|
55
|
+
...this.colorMap.silkscreen,
|
|
56
|
+
...config.colorOverrides.silkscreen,
|
|
57
|
+
},
|
|
58
|
+
soldermask: {
|
|
59
|
+
...this.colorMap.soldermask,
|
|
60
|
+
...config.colorOverrides.soldermask,
|
|
61
|
+
},
|
|
62
|
+
soldermaskWithCopperUnderneath: {
|
|
63
|
+
...this.colorMap.soldermaskWithCopperUnderneath,
|
|
64
|
+
...config.colorOverrides.soldermaskWithCopperUnderneath,
|
|
65
|
+
},
|
|
66
|
+
soldermaskOverCopper: {
|
|
67
|
+
...this.colorMap.soldermaskOverCopper,
|
|
68
|
+
...config.colorOverrides.soldermaskOverCopper,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setCameraBounds(bounds: CameraBounds): void {
|
|
75
|
+
const canvas = this.ctx.canvas
|
|
76
|
+
const canvasWidth = canvas.width
|
|
77
|
+
const canvasHeight = canvas.height
|
|
78
|
+
|
|
79
|
+
const realWidth = bounds.maxX - bounds.minX
|
|
80
|
+
const realHeight = bounds.maxY - bounds.minY
|
|
81
|
+
|
|
82
|
+
const scaleX = canvasWidth / realWidth
|
|
83
|
+
const scaleY = canvasHeight / realHeight
|
|
84
|
+
const uniformScale = Math.min(scaleX, scaleY)
|
|
85
|
+
|
|
86
|
+
// Center the view
|
|
87
|
+
const offsetX = (canvasWidth - realWidth * uniformScale) / 2
|
|
88
|
+
const offsetY = (canvasHeight - realHeight * uniformScale) / 2
|
|
89
|
+
|
|
90
|
+
this.realToCanvasMat = compose(
|
|
91
|
+
translate(offsetX, offsetY),
|
|
92
|
+
scale(uniformScale, uniformScale),
|
|
93
|
+
translate(-bounds.minX, -bounds.minY),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
drawElements(
|
|
98
|
+
elements: AnyCircuitElement[],
|
|
99
|
+
options: DrawElementsOptions = {},
|
|
100
|
+
): void {
|
|
101
|
+
for (const element of elements) {
|
|
102
|
+
this.drawElement(element, options)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private drawElement(
|
|
107
|
+
element: AnyCircuitElement,
|
|
108
|
+
options: DrawElementsOptions,
|
|
109
|
+
): void {
|
|
110
|
+
if (element.type === "pcb_plated_hole") {
|
|
111
|
+
drawPcbPlatedHole({
|
|
112
|
+
ctx: this.ctx,
|
|
113
|
+
hole: element as PcbPlatedHole,
|
|
114
|
+
transform: this.realToCanvasMat,
|
|
115
|
+
colorMap: this.colorMap,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { PcbPlatedHole } 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 { drawOval } from "../shapes/oval"
|
|
7
|
+
import { drawPill } from "../shapes/pill"
|
|
8
|
+
|
|
9
|
+
export interface DrawPcbPlatedHoleParams {
|
|
10
|
+
ctx: CanvasContext
|
|
11
|
+
hole: PcbPlatedHole
|
|
12
|
+
transform: Matrix
|
|
13
|
+
colorMap: PcbColorMap
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
|
|
17
|
+
const { ctx, hole, transform, colorMap } = params
|
|
18
|
+
|
|
19
|
+
if (hole.shape === "circle") {
|
|
20
|
+
// Draw outer copper ring
|
|
21
|
+
drawCircle({
|
|
22
|
+
ctx,
|
|
23
|
+
center: { x: hole.x, y: hole.y },
|
|
24
|
+
radius: hole.outer_diameter / 2,
|
|
25
|
+
fill: colorMap.copper.top,
|
|
26
|
+
transform,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Draw inner drill hole
|
|
30
|
+
drawCircle({
|
|
31
|
+
ctx,
|
|
32
|
+
center: { x: hole.x, y: hole.y },
|
|
33
|
+
radius: hole.hole_diameter / 2,
|
|
34
|
+
fill: colorMap.drill,
|
|
35
|
+
transform,
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hole.shape === "oval") {
|
|
41
|
+
// Draw outer copper oval
|
|
42
|
+
drawOval({
|
|
43
|
+
ctx,
|
|
44
|
+
center: { x: hole.x, y: hole.y },
|
|
45
|
+
width: hole.outer_width,
|
|
46
|
+
height: hole.outer_height,
|
|
47
|
+
fill: colorMap.copper.top,
|
|
48
|
+
transform,
|
|
49
|
+
rotation: hole.ccw_rotation,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Draw inner drill hole
|
|
53
|
+
drawOval({
|
|
54
|
+
ctx,
|
|
55
|
+
center: { x: hole.x, y: hole.y },
|
|
56
|
+
width: hole.hole_width,
|
|
57
|
+
height: hole.hole_height,
|
|
58
|
+
fill: colorMap.drill,
|
|
59
|
+
transform,
|
|
60
|
+
rotation: hole.ccw_rotation,
|
|
61
|
+
})
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (hole.shape === "pill") {
|
|
66
|
+
// Draw outer copper pill
|
|
67
|
+
drawPill({
|
|
68
|
+
ctx,
|
|
69
|
+
center: { x: hole.x, y: hole.y },
|
|
70
|
+
width: hole.outer_width,
|
|
71
|
+
height: hole.outer_height,
|
|
72
|
+
fill: colorMap.copper.top,
|
|
73
|
+
transform,
|
|
74
|
+
rotation: hole.ccw_rotation,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Draw inner drill hole
|
|
78
|
+
drawPill({
|
|
79
|
+
ctx,
|
|
80
|
+
center: { x: hole.x, y: hole.y },
|
|
81
|
+
width: hole.hole_width,
|
|
82
|
+
height: hole.hole_height,
|
|
83
|
+
fill: colorMap.drill,
|
|
84
|
+
transform,
|
|
85
|
+
rotation: hole.ccw_rotation,
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hole.shape === "circular_hole_with_rect_pad") {
|
|
91
|
+
// Draw rectangular pad
|
|
92
|
+
drawRect({
|
|
93
|
+
ctx,
|
|
94
|
+
center: { x: hole.x, y: hole.y },
|
|
95
|
+
width: hole.rect_pad_width,
|
|
96
|
+
height: hole.rect_pad_height,
|
|
97
|
+
fill: colorMap.copper.top,
|
|
98
|
+
transform,
|
|
99
|
+
borderRadius: (hole as any).rect_border_radius ?? 0,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// 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)
|
|
105
|
+
drawCircle({
|
|
106
|
+
ctx,
|
|
107
|
+
center: { x: holeX, y: holeY },
|
|
108
|
+
radius: hole.hole_diameter / 2,
|
|
109
|
+
fill: colorMap.drill,
|
|
110
|
+
transform,
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (hole.shape === "pill_hole_with_rect_pad") {
|
|
116
|
+
// Draw rectangular pad
|
|
117
|
+
drawRect({
|
|
118
|
+
ctx,
|
|
119
|
+
center: { x: hole.x, y: hole.y },
|
|
120
|
+
width: hole.rect_pad_width,
|
|
121
|
+
height: hole.rect_pad_height,
|
|
122
|
+
fill: colorMap.copper.top,
|
|
123
|
+
transform,
|
|
124
|
+
borderRadius: (hole as any).rect_border_radius ?? 0,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 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)
|
|
130
|
+
drawPill({
|
|
131
|
+
ctx,
|
|
132
|
+
center: { x: holeX, y: holeY },
|
|
133
|
+
width: hole.hole_width,
|
|
134
|
+
height: hole.hole_height,
|
|
135
|
+
fill: colorMap.drill,
|
|
136
|
+
transform,
|
|
137
|
+
})
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (hole.shape === "rotated_pill_hole_with_rect_pad") {
|
|
142
|
+
// Draw rotated rectangular pad
|
|
143
|
+
drawRect({
|
|
144
|
+
ctx,
|
|
145
|
+
center: { x: hole.x, y: hole.y },
|
|
146
|
+
width: hole.rect_pad_width,
|
|
147
|
+
height: hole.rect_pad_height,
|
|
148
|
+
fill: colorMap.copper.top,
|
|
149
|
+
transform,
|
|
150
|
+
borderRadius: (hole as any).rect_border_radius ?? 0,
|
|
151
|
+
rotation: hole.rect_ccw_rotation,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 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)
|
|
157
|
+
drawPill({
|
|
158
|
+
ctx,
|
|
159
|
+
center: { x: holeX, y: holeY },
|
|
160
|
+
width: hole.hole_width,
|
|
161
|
+
height: hole.hole_height,
|
|
162
|
+
fill: colorMap.drill,
|
|
163
|
+
transform,
|
|
164
|
+
rotation: hole.hole_ccw_rotation,
|
|
165
|
+
})
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
import { applyToPoint } from "transformation-matrix"
|
|
3
|
+
import type { CanvasContext } from "../types"
|
|
4
|
+
|
|
5
|
+
export interface DrawCircleParams {
|
|
6
|
+
ctx: CanvasContext
|
|
7
|
+
center: { x: number; y: number }
|
|
8
|
+
radius: number
|
|
9
|
+
fill: string
|
|
10
|
+
transform: Matrix
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawCircle(params: DrawCircleParams): void {
|
|
14
|
+
const { ctx, center, radius, fill, transform } = params
|
|
15
|
+
|
|
16
|
+
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
17
|
+
const scaledRadius = radius * Math.abs(transform.a)
|
|
18
|
+
|
|
19
|
+
ctx.beginPath()
|
|
20
|
+
ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
|
|
21
|
+
ctx.fillStyle = fill
|
|
22
|
+
ctx.fill()
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
import { applyToPoint } from "transformation-matrix"
|
|
3
|
+
import type { CanvasContext } from "../types"
|
|
4
|
+
|
|
5
|
+
export interface DrawOvalParams {
|
|
6
|
+
ctx: CanvasContext
|
|
7
|
+
center: { x: number; y: number }
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
fill: string
|
|
11
|
+
transform: Matrix
|
|
12
|
+
rotation?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function drawOval(params: DrawOvalParams): void {
|
|
16
|
+
const { ctx, center, width, height, fill, transform, rotation = 0 } = params
|
|
17
|
+
|
|
18
|
+
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
19
|
+
const scaledWidth = width * Math.abs(transform.a)
|
|
20
|
+
const scaledHeight = height * Math.abs(transform.a)
|
|
21
|
+
|
|
22
|
+
ctx.save()
|
|
23
|
+
ctx.translate(cx, cy)
|
|
24
|
+
|
|
25
|
+
if (rotation !== 0) {
|
|
26
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ctx.beginPath()
|
|
30
|
+
ctx.ellipse(0, 0, scaledWidth / 2, scaledHeight / 2, 0, 0, Math.PI * 2)
|
|
31
|
+
ctx.fillStyle = fill
|
|
32
|
+
ctx.fill()
|
|
33
|
+
ctx.restore()
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
import { applyToPoint } from "transformation-matrix"
|
|
3
|
+
import type { CanvasContext } from "../types"
|
|
4
|
+
|
|
5
|
+
export interface DrawPillParams {
|
|
6
|
+
ctx: CanvasContext
|
|
7
|
+
center: { x: number; y: number }
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
fill: string
|
|
11
|
+
transform: Matrix
|
|
12
|
+
rotation?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function drawPill(params: DrawPillParams): void {
|
|
16
|
+
const { ctx, center, width, height, fill, transform, rotation = 0 } = params
|
|
17
|
+
|
|
18
|
+
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
19
|
+
const scaledWidth = width * Math.abs(transform.a)
|
|
20
|
+
const scaledHeight = height * Math.abs(transform.a)
|
|
21
|
+
|
|
22
|
+
ctx.save()
|
|
23
|
+
ctx.translate(cx, cy)
|
|
24
|
+
|
|
25
|
+
if (rotation !== 0) {
|
|
26
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ctx.beginPath()
|
|
30
|
+
|
|
31
|
+
if (scaledWidth > scaledHeight) {
|
|
32
|
+
// Horizontal pill
|
|
33
|
+
const radius = scaledHeight / 2
|
|
34
|
+
const straightLength = scaledWidth - scaledHeight
|
|
35
|
+
|
|
36
|
+
ctx.moveTo(-straightLength / 2, -radius)
|
|
37
|
+
ctx.lineTo(straightLength / 2, -radius)
|
|
38
|
+
ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2)
|
|
39
|
+
ctx.lineTo(-straightLength / 2, radius)
|
|
40
|
+
ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2)
|
|
41
|
+
} else if (scaledHeight > scaledWidth) {
|
|
42
|
+
// Vertical pill
|
|
43
|
+
const radius = scaledWidth / 2
|
|
44
|
+
const straightLength = scaledHeight - scaledWidth
|
|
45
|
+
|
|
46
|
+
ctx.moveTo(radius, -straightLength / 2)
|
|
47
|
+
ctx.lineTo(radius, straightLength / 2)
|
|
48
|
+
ctx.arc(0, straightLength / 2, radius, 0, Math.PI)
|
|
49
|
+
ctx.lineTo(-radius, -straightLength / 2)
|
|
50
|
+
ctx.arc(0, -straightLength / 2, radius, Math.PI, 0)
|
|
51
|
+
} else {
|
|
52
|
+
// Circle (width === height)
|
|
53
|
+
ctx.arc(0, 0, scaledWidth / 2, 0, Math.PI * 2)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ctx.closePath()
|
|
57
|
+
ctx.fillStyle = fill
|
|
58
|
+
ctx.fill()
|
|
59
|
+
ctx.restore()
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
import { applyToPoint } from "transformation-matrix"
|
|
3
|
+
import type { CanvasContext } from "../types"
|
|
4
|
+
|
|
5
|
+
export interface DrawRectParams {
|
|
6
|
+
ctx: CanvasContext
|
|
7
|
+
center: { x: number; y: number }
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
fill: string
|
|
11
|
+
transform: Matrix
|
|
12
|
+
borderRadius?: number
|
|
13
|
+
rotation?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function drawRect(params: DrawRectParams): void {
|
|
17
|
+
const {
|
|
18
|
+
ctx,
|
|
19
|
+
center,
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
fill,
|
|
23
|
+
transform,
|
|
24
|
+
borderRadius = 0,
|
|
25
|
+
rotation = 0,
|
|
26
|
+
} = params
|
|
27
|
+
|
|
28
|
+
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
29
|
+
const scaledWidth = width * Math.abs(transform.a)
|
|
30
|
+
const scaledHeight = height * Math.abs(transform.a)
|
|
31
|
+
const scaledRadius = borderRadius * Math.abs(transform.a)
|
|
32
|
+
|
|
33
|
+
ctx.save()
|
|
34
|
+
ctx.translate(cx, cy)
|
|
35
|
+
|
|
36
|
+
if (rotation !== 0) {
|
|
37
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ctx.beginPath()
|
|
41
|
+
|
|
42
|
+
if (scaledRadius > 0) {
|
|
43
|
+
const x = -scaledWidth / 2
|
|
44
|
+
const y = -scaledHeight / 2
|
|
45
|
+
const r = Math.min(scaledRadius, scaledWidth / 2, scaledHeight / 2)
|
|
46
|
+
|
|
47
|
+
ctx.moveTo(x + r, y)
|
|
48
|
+
ctx.lineTo(x + scaledWidth - r, y)
|
|
49
|
+
ctx.arcTo(x + scaledWidth, y, x + scaledWidth, y + r, r)
|
|
50
|
+
ctx.lineTo(x + scaledWidth, y + scaledHeight - r)
|
|
51
|
+
ctx.arcTo(
|
|
52
|
+
x + scaledWidth,
|
|
53
|
+
y + scaledHeight,
|
|
54
|
+
x + scaledWidth - r,
|
|
55
|
+
y + scaledHeight,
|
|
56
|
+
r,
|
|
57
|
+
)
|
|
58
|
+
ctx.lineTo(x + r, y + scaledHeight)
|
|
59
|
+
ctx.arcTo(x, y + scaledHeight, x, y + scaledHeight - r, r)
|
|
60
|
+
ctx.lineTo(x, y + r)
|
|
61
|
+
ctx.arcTo(x, y, x + r, y, r)
|
|
62
|
+
} else {
|
|
63
|
+
ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ctx.fillStyle = fill
|
|
67
|
+
ctx.fill()
|
|
68
|
+
ctx.restore()
|
|
69
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Matrix } from "transformation-matrix"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canvas context type that works with both browser and node-canvas.
|
|
5
|
+
* Uses a subset of CanvasRenderingContext2D methods that are common to both.
|
|
6
|
+
*/
|
|
7
|
+
export interface CanvasContext {
|
|
8
|
+
beginPath(): void
|
|
9
|
+
closePath(): void
|
|
10
|
+
arc(
|
|
11
|
+
x: number,
|
|
12
|
+
y: number,
|
|
13
|
+
radius: number,
|
|
14
|
+
startAngle: number,
|
|
15
|
+
endAngle: number,
|
|
16
|
+
): void
|
|
17
|
+
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
|
|
18
|
+
ellipse(
|
|
19
|
+
x: number,
|
|
20
|
+
y: number,
|
|
21
|
+
radiusX: number,
|
|
22
|
+
radiusY: number,
|
|
23
|
+
rotation: number,
|
|
24
|
+
startAngle: number,
|
|
25
|
+
endAngle: number,
|
|
26
|
+
): void
|
|
27
|
+
fill(): void
|
|
28
|
+
rect(x: number, y: number, w: number, h: number): void
|
|
29
|
+
lineTo(x: number, y: number): void
|
|
30
|
+
moveTo(x: number, y: number): void
|
|
31
|
+
save(): void
|
|
32
|
+
restore(): void
|
|
33
|
+
translate(x: number, y: number): void
|
|
34
|
+
rotate(angle: number): void
|
|
35
|
+
fillStyle: string | CanvasGradient | CanvasPattern
|
|
36
|
+
canvas: { width: number; height: number }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type CopperLayerName =
|
|
40
|
+
| "top"
|
|
41
|
+
| "bottom"
|
|
42
|
+
| "inner1"
|
|
43
|
+
| "inner2"
|
|
44
|
+
| "inner3"
|
|
45
|
+
| "inner4"
|
|
46
|
+
| "inner5"
|
|
47
|
+
| "inner6"
|
|
48
|
+
|
|
49
|
+
export type CopperColorMap = Record<CopperLayerName, string> & {
|
|
50
|
+
[layer: string]: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PcbColorMap {
|
|
54
|
+
copper: CopperColorMap
|
|
55
|
+
drill: string
|
|
56
|
+
silkscreen: {
|
|
57
|
+
top: string
|
|
58
|
+
bottom: string
|
|
59
|
+
}
|
|
60
|
+
boardOutline: string
|
|
61
|
+
soldermask: {
|
|
62
|
+
top: string
|
|
63
|
+
bottom: string
|
|
64
|
+
}
|
|
65
|
+
soldermaskWithCopperUnderneath: {
|
|
66
|
+
top: string
|
|
67
|
+
bottom: string
|
|
68
|
+
}
|
|
69
|
+
soldermaskOverCopper: {
|
|
70
|
+
top: string
|
|
71
|
+
bottom: string
|
|
72
|
+
}
|
|
73
|
+
substrate: string
|
|
74
|
+
courtyard: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const DEFAULT_PCB_COLOR_MAP: PcbColorMap = {
|
|
78
|
+
copper: {
|
|
79
|
+
top: "rgb(200, 52, 52)",
|
|
80
|
+
inner1: "rgb(255, 140, 0)",
|
|
81
|
+
inner2: "rgb(255, 215, 0)",
|
|
82
|
+
inner3: "rgb(50, 205, 50)",
|
|
83
|
+
inner4: "rgb(64, 224, 208)",
|
|
84
|
+
inner5: "rgb(138, 43, 226)",
|
|
85
|
+
inner6: "rgb(255, 105, 180)",
|
|
86
|
+
bottom: "rgb(77, 127, 196)",
|
|
87
|
+
},
|
|
88
|
+
soldermaskWithCopperUnderneath: {
|
|
89
|
+
top: "rgb(18, 82, 50)",
|
|
90
|
+
bottom: "rgb(77, 127, 196)",
|
|
91
|
+
},
|
|
92
|
+
soldermask: {
|
|
93
|
+
top: "rgb(12, 55, 33)",
|
|
94
|
+
bottom: "rgb(12, 55, 33)",
|
|
95
|
+
},
|
|
96
|
+
soldermaskOverCopper: {
|
|
97
|
+
top: "rgb(52, 135, 73)",
|
|
98
|
+
bottom: "rgb(52, 135, 73)",
|
|
99
|
+
},
|
|
100
|
+
substrate: "rgb(201, 162, 110)",
|
|
101
|
+
drill: "#FF26E2",
|
|
102
|
+
silkscreen: {
|
|
103
|
+
top: "#f2eda1",
|
|
104
|
+
bottom: "#5da9e9",
|
|
105
|
+
},
|
|
106
|
+
boardOutline: "rgba(255, 255, 255, 0.5)",
|
|
107
|
+
courtyard: "#FF00FF",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DrawerConfig {
|
|
111
|
+
colorOverrides?: Partial<PcbColorMap>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CameraBounds {
|
|
115
|
+
minX: number
|
|
116
|
+
maxX: number
|
|
117
|
+
minY: number
|
|
118
|
+
maxY: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DrawContext {
|
|
122
|
+
ctx: CanvasRenderingContext2D
|
|
123
|
+
transform: Matrix
|
|
124
|
+
colorMap: PcbColorMap
|
|
125
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./drawer"
|
package/lib/pcb/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "circuit-to-canvas",
|
|
3
|
+
"main": "dist/index.js",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
8
|
+
"format": "biome format --write .",
|
|
9
|
+
"format:check": "biome format --check ."
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@biomejs/biome": "^2.3.8",
|
|
13
|
+
"@types/bun": "latest",
|
|
14
|
+
"bun-match-svg": "^0.0.14",
|
|
15
|
+
"canvas": "^3.2.0",
|
|
16
|
+
"circuit-json": "^0.0.327",
|
|
17
|
+
"tsup": "^8.5.1"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"transformation-matrix": "^3.1.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|