circuit-to-canvas 0.0.3 → 0.0.4
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/README.md +1 -1
- package/dist/index.d.ts +74 -3
- package/dist/index.js +326 -4
- package/lib/drawer/CircuitToCanvasDrawer.ts +44 -0
- package/lib/drawer/elements/index.ts +20 -0
- package/lib/drawer/elements/pcb-copper-text.ts +99 -0
- package/lib/drawer/elements/pcb-fabrication-note-path.ts +41 -0
- package/lib/drawer/elements/pcb-fabrication-note-rect.ts +39 -0
- package/lib/drawer/elements/pcb-fabrication-note-text.ts +42 -0
- package/lib/drawer/shapes/index.ts +9 -0
- package/lib/drawer/shapes/rect.ts +28 -3
- package/lib/drawer/shapes/text.ts +218 -0
- package/lib/drawer/types.ts +8 -0
- package/package.json +3 -1
- package/tests/elements/__snapshots__/pcb-copper-text-knockout.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-copper-text.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path-custom-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path-thick-stroke.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-all-features.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-corner-radius.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-custom-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-dashed.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-default-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-text-rgba-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-text-small.snap.png +0 -0
- package/tests/elements/pcb-copper-text.test.ts +102 -0
- package/tests/elements/pcb-fabrication-note-path-custom-color.test.ts +37 -0
- package/tests/elements/pcb-fabrication-note-path-thick-stroke.test.ts +37 -0
- package/tests/elements/pcb-fabrication-note-path.test.ts +36 -0
- package/tests/elements/pcb-fabrication-note-rect-all-features.test.ts +35 -0
- package/tests/elements/pcb-fabrication-note-rect-corner-radius.test.ts +33 -0
- package/tests/elements/pcb-fabrication-note-rect-custom-color.test.ts +32 -0
- package/tests/elements/pcb-fabrication-note-rect-dashed.test.ts +33 -0
- package/tests/elements/pcb-fabrication-note-rect-default-color.test.ts +32 -0
- package/tests/elements/pcb-fabrication-note-text-rgba-color.test.ts +34 -0
- package/tests/elements/pcb-fabrication-note-text-small.test.ts +33 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PcbFabricationNotePath } 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 DrawPcbFabricationNotePathParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
path: PcbFabricationNotePath
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawPcbFabricationNotePath(
|
|
14
|
+
params: DrawPcbFabricationNotePathParams,
|
|
15
|
+
): void {
|
|
16
|
+
const { ctx, path, transform, colorMap } = params
|
|
17
|
+
|
|
18
|
+
// Use the color from the path if provided, otherwise use a default color
|
|
19
|
+
// Fabrication notes are typically shown in a distinct color
|
|
20
|
+
const defaultColor = "rgba(255,255,255,0.5)" // White color for fabrication notes
|
|
21
|
+
const color = path.color ?? defaultColor
|
|
22
|
+
|
|
23
|
+
if (!path.route || path.route.length < 2) return
|
|
24
|
+
|
|
25
|
+
// Draw each segment of the path
|
|
26
|
+
for (let i = 0; i < path.route.length - 1; i++) {
|
|
27
|
+
const start = path.route[i]
|
|
28
|
+
const end = path.route[i + 1]
|
|
29
|
+
|
|
30
|
+
if (!start || !end) continue
|
|
31
|
+
|
|
32
|
+
drawLine({
|
|
33
|
+
ctx,
|
|
34
|
+
start: { x: start.x, y: start.y },
|
|
35
|
+
end: { x: end.x, y: end.y },
|
|
36
|
+
strokeWidth: path.stroke_width ?? 0.1,
|
|
37
|
+
stroke: color,
|
|
38
|
+
transform,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PcbFabricationNoteRect } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import type { PcbColorMap, CanvasContext } from "../types"
|
|
4
|
+
import { drawRect } from "../shapes/rect"
|
|
5
|
+
|
|
6
|
+
export interface DrawPcbFabricationNoteRectParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
rect: PcbFabricationNoteRect
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawPcbFabricationNoteRect(
|
|
14
|
+
params: DrawPcbFabricationNoteRectParams,
|
|
15
|
+
): void {
|
|
16
|
+
const { ctx, rect, transform, colorMap } = params
|
|
17
|
+
|
|
18
|
+
// Use the color from the rect if provided, otherwise use a default color
|
|
19
|
+
// Fabrication notes are typically shown in a distinct color
|
|
20
|
+
const defaultColor = "rgba(255,255,255,0.5)" // White color for fabrication notes
|
|
21
|
+
const color = rect.color ?? defaultColor
|
|
22
|
+
|
|
23
|
+
const isFilled = rect.is_filled ?? false
|
|
24
|
+
const hasStroke = rect.has_stroke ?? true
|
|
25
|
+
const isStrokeDashed = rect.is_stroke_dashed ?? false
|
|
26
|
+
|
|
27
|
+
drawRect({
|
|
28
|
+
ctx,
|
|
29
|
+
center: rect.center,
|
|
30
|
+
width: rect.width,
|
|
31
|
+
height: rect.height,
|
|
32
|
+
fill: isFilled ? color : undefined,
|
|
33
|
+
stroke: hasStroke ? color : undefined,
|
|
34
|
+
strokeWidth: hasStroke ? rect.stroke_width : undefined,
|
|
35
|
+
borderRadius: rect.corner_radius,
|
|
36
|
+
transform,
|
|
37
|
+
isStrokeDashed,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import type { PcbColorMap, CanvasContext } from "../types"
|
|
4
|
+
import { drawText } from "../shapes/text"
|
|
5
|
+
|
|
6
|
+
export interface DrawPcbFabricationNoteTextParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
text: PcbFabricationNoteText
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FABRICATION_NOTE_COLOR = "rgba(255,255,255,0.5)"
|
|
14
|
+
|
|
15
|
+
function layerToColor(layer: string, colorMap: PcbColorMap): string {
|
|
16
|
+
// For fabrication notes, we use a default color
|
|
17
|
+
// Could be extended to support per-layer colors in the future
|
|
18
|
+
return DEFAULT_FABRICATION_NOTE_COLOR
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function drawPcbFabricationNoteText(
|
|
22
|
+
params: DrawPcbFabricationNoteTextParams,
|
|
23
|
+
): void {
|
|
24
|
+
const { ctx, text, transform, colorMap } = params
|
|
25
|
+
|
|
26
|
+
const defaultColor = layerToColor(text.layer, colorMap)
|
|
27
|
+
const color = text.color ?? defaultColor
|
|
28
|
+
const fontSize = text.font_size
|
|
29
|
+
|
|
30
|
+
// Use @tscircuit/alphabet to draw text
|
|
31
|
+
// Pass real-world coordinates and let drawText apply the transform
|
|
32
|
+
drawText({
|
|
33
|
+
ctx,
|
|
34
|
+
text: text.text,
|
|
35
|
+
x: text.anchor_position.x,
|
|
36
|
+
y: text.anchor_position.y,
|
|
37
|
+
fontSize,
|
|
38
|
+
color,
|
|
39
|
+
transform,
|
|
40
|
+
anchorAlignment: text.anchor_alignment,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -5,3 +5,12 @@ export { drawPill, type DrawPillParams } from "./pill"
|
|
|
5
5
|
export { drawPolygon, type DrawPolygonParams } from "./polygon"
|
|
6
6
|
export { drawLine, type DrawLineParams } from "./line"
|
|
7
7
|
export { drawPath, type DrawPathParams } from "./path"
|
|
8
|
+
export {
|
|
9
|
+
drawText,
|
|
10
|
+
type DrawTextParams,
|
|
11
|
+
getAlphabetLayout,
|
|
12
|
+
strokeAlphabetText,
|
|
13
|
+
getTextStartPosition,
|
|
14
|
+
type AlphabetLayout,
|
|
15
|
+
type AnchorAlignment,
|
|
16
|
+
} from "./text"
|
|
@@ -7,10 +7,13 @@ export interface DrawRectParams {
|
|
|
7
7
|
center: { x: number; y: number }
|
|
8
8
|
width: number
|
|
9
9
|
height: number
|
|
10
|
-
fill
|
|
10
|
+
fill?: string
|
|
11
11
|
transform: Matrix
|
|
12
12
|
borderRadius?: number
|
|
13
13
|
rotation?: number
|
|
14
|
+
stroke?: string
|
|
15
|
+
strokeWidth?: number
|
|
16
|
+
isStrokeDashed?: boolean
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function drawRect(params: DrawRectParams): void {
|
|
@@ -23,12 +26,18 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
23
26
|
transform,
|
|
24
27
|
borderRadius = 0,
|
|
25
28
|
rotation = 0,
|
|
29
|
+
stroke,
|
|
30
|
+
strokeWidth,
|
|
31
|
+
isStrokeDashed = false,
|
|
26
32
|
} = params
|
|
27
33
|
|
|
28
34
|
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
29
35
|
const scaledWidth = width * Math.abs(transform.a)
|
|
30
36
|
const scaledHeight = height * Math.abs(transform.a)
|
|
31
37
|
const scaledRadius = borderRadius * Math.abs(transform.a)
|
|
38
|
+
const scaledStrokeWidth = strokeWidth
|
|
39
|
+
? strokeWidth * Math.abs(transform.a)
|
|
40
|
+
: undefined
|
|
32
41
|
|
|
33
42
|
ctx.save()
|
|
34
43
|
ctx.translate(cx, cy)
|
|
@@ -37,6 +46,13 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
37
46
|
ctx.rotate(-rotation * (Math.PI / 180))
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
// Set up dashed line if needed
|
|
50
|
+
if (isStrokeDashed && scaledStrokeWidth) {
|
|
51
|
+
ctx.setLineDash([scaledStrokeWidth * 2, scaledStrokeWidth * 2])
|
|
52
|
+
} else {
|
|
53
|
+
ctx.setLineDash([])
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
ctx.beginPath()
|
|
41
57
|
|
|
42
58
|
if (scaledRadius > 0) {
|
|
@@ -63,7 +79,16 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
63
79
|
ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
if (fill) {
|
|
83
|
+
ctx.fillStyle = fill
|
|
84
|
+
ctx.fill()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stroke && scaledStrokeWidth) {
|
|
88
|
+
ctx.strokeStyle = stroke
|
|
89
|
+
ctx.lineWidth = scaledStrokeWidth
|
|
90
|
+
ctx.stroke()
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
ctx.restore()
|
|
69
94
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { lineAlphabet } from "@tscircuit/alphabet"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import { applyToPoint } from "transformation-matrix"
|
|
4
|
+
import type { CanvasContext } from "../types"
|
|
5
|
+
|
|
6
|
+
const GLYPH_WIDTH_RATIO = 0.62
|
|
7
|
+
const LETTER_SPACING_RATIO = 0.3 // Letter spacing between characters (25% of glyph width)
|
|
8
|
+
const SPACE_WIDTH_RATIO = 1
|
|
9
|
+
const STROKE_WIDTH_RATIO = 0.13
|
|
10
|
+
const CURVED_GLYPHS = new Set(["O", "o", "0"])
|
|
11
|
+
|
|
12
|
+
export type AlphabetLayout = {
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
glyphWidth: number
|
|
16
|
+
letterSpacing: number
|
|
17
|
+
spaceWidth: number
|
|
18
|
+
strokeWidth: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAlphabetLayout(
|
|
22
|
+
text: string,
|
|
23
|
+
fontSize: number,
|
|
24
|
+
): AlphabetLayout {
|
|
25
|
+
const glyphWidth = fontSize * GLYPH_WIDTH_RATIO
|
|
26
|
+
const letterSpacing = glyphWidth * LETTER_SPACING_RATIO
|
|
27
|
+
const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO
|
|
28
|
+
const characters = Array.from(text)
|
|
29
|
+
|
|
30
|
+
let width = 0
|
|
31
|
+
characters.forEach((char, index) => {
|
|
32
|
+
const advance = char === " " ? spaceWidth : glyphWidth
|
|
33
|
+
width += advance
|
|
34
|
+
if (index < characters.length - 1) width += letterSpacing
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
width,
|
|
41
|
+
height: fontSize,
|
|
42
|
+
glyphWidth,
|
|
43
|
+
letterSpacing,
|
|
44
|
+
spaceWidth,
|
|
45
|
+
strokeWidth,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getGlyphLines = (char: string) =>
|
|
50
|
+
lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()]
|
|
51
|
+
|
|
52
|
+
export type AnchorAlignment =
|
|
53
|
+
| "center"
|
|
54
|
+
| "top_left"
|
|
55
|
+
| "top_right"
|
|
56
|
+
| "bottom_left"
|
|
57
|
+
| "bottom_right"
|
|
58
|
+
| "left"
|
|
59
|
+
| "right"
|
|
60
|
+
| "top"
|
|
61
|
+
| "bottom"
|
|
62
|
+
|
|
63
|
+
export function getTextStartPosition(
|
|
64
|
+
alignment: AnchorAlignment,
|
|
65
|
+
layout: AlphabetLayout,
|
|
66
|
+
): { x: number; y: number } {
|
|
67
|
+
const totalWidth = layout.width + layout.strokeWidth
|
|
68
|
+
const totalHeight = layout.height + layout.strokeWidth
|
|
69
|
+
|
|
70
|
+
let x = 0
|
|
71
|
+
let y = 0
|
|
72
|
+
|
|
73
|
+
// Horizontal alignment
|
|
74
|
+
if (alignment === "center") {
|
|
75
|
+
x = -totalWidth / 2
|
|
76
|
+
} else if (
|
|
77
|
+
alignment === "top_left" ||
|
|
78
|
+
alignment === "bottom_left" ||
|
|
79
|
+
alignment === "left"
|
|
80
|
+
) {
|
|
81
|
+
x = 0
|
|
82
|
+
} else if (
|
|
83
|
+
alignment === "top_right" ||
|
|
84
|
+
alignment === "bottom_right" ||
|
|
85
|
+
alignment === "right"
|
|
86
|
+
) {
|
|
87
|
+
x = -totalWidth
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Vertical alignment
|
|
91
|
+
if (alignment === "center") {
|
|
92
|
+
y = -totalHeight / 2
|
|
93
|
+
} else if (
|
|
94
|
+
alignment === "top_left" ||
|
|
95
|
+
alignment === "top_right" ||
|
|
96
|
+
alignment === "top"
|
|
97
|
+
) {
|
|
98
|
+
y = 0
|
|
99
|
+
} else if (
|
|
100
|
+
alignment === "bottom_left" ||
|
|
101
|
+
alignment === "bottom_right" ||
|
|
102
|
+
alignment === "bottom"
|
|
103
|
+
) {
|
|
104
|
+
y = -totalHeight
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { x, y }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function strokeAlphabetText(
|
|
111
|
+
ctx: CanvasContext,
|
|
112
|
+
text: string,
|
|
113
|
+
layout: AlphabetLayout,
|
|
114
|
+
startX: number,
|
|
115
|
+
startY: number,
|
|
116
|
+
): void {
|
|
117
|
+
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout
|
|
118
|
+
const yOffset = startY + height / 2
|
|
119
|
+
const characters = Array.from(text)
|
|
120
|
+
let cursor = startX + strokeWidth / 2
|
|
121
|
+
|
|
122
|
+
characters.forEach((char, index) => {
|
|
123
|
+
const lines = getGlyphLines(char)
|
|
124
|
+
const advance = char === " " ? spaceWidth : glyphWidth
|
|
125
|
+
|
|
126
|
+
if (CURVED_GLYPHS.has(char)) {
|
|
127
|
+
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth)
|
|
128
|
+
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth)
|
|
129
|
+
const centerY = yOffset - height / 2
|
|
130
|
+
ctx.beginPath()
|
|
131
|
+
ctx.ellipse(
|
|
132
|
+
cursor + glyphWidth / 2,
|
|
133
|
+
centerY,
|
|
134
|
+
radiusX,
|
|
135
|
+
radiusY,
|
|
136
|
+
0,
|
|
137
|
+
0,
|
|
138
|
+
Math.PI * 2,
|
|
139
|
+
)
|
|
140
|
+
ctx.stroke()
|
|
141
|
+
} else if (lines?.length) {
|
|
142
|
+
ctx.beginPath()
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const x1 = cursor + line.x1 * glyphWidth
|
|
145
|
+
const y1 = yOffset - line.y1 * height
|
|
146
|
+
const x2 = cursor + line.x2 * glyphWidth
|
|
147
|
+
const y2 = yOffset - line.y2 * height
|
|
148
|
+
ctx.moveTo(x1, y1)
|
|
149
|
+
ctx.lineTo(x2, y2)
|
|
150
|
+
}
|
|
151
|
+
ctx.stroke()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Move cursor by the character width
|
|
155
|
+
cursor += advance
|
|
156
|
+
// Add letter spacing after each character except the last one
|
|
157
|
+
// This spacing will be before the next character, creating visible gaps
|
|
158
|
+
if (index < characters.length - 1) {
|
|
159
|
+
cursor += letterSpacing
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface DrawTextParams {
|
|
165
|
+
ctx: CanvasContext
|
|
166
|
+
text: string
|
|
167
|
+
x: number
|
|
168
|
+
y: number
|
|
169
|
+
fontSize: number
|
|
170
|
+
color: string
|
|
171
|
+
transform: Matrix
|
|
172
|
+
anchorAlignment: AnchorAlignment
|
|
173
|
+
rotation?: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function drawText(params: DrawTextParams): void {
|
|
177
|
+
const {
|
|
178
|
+
ctx,
|
|
179
|
+
text,
|
|
180
|
+
x,
|
|
181
|
+
y,
|
|
182
|
+
fontSize,
|
|
183
|
+
color,
|
|
184
|
+
transform,
|
|
185
|
+
anchorAlignment,
|
|
186
|
+
rotation = 0,
|
|
187
|
+
} = params
|
|
188
|
+
|
|
189
|
+
if (!text) return
|
|
190
|
+
|
|
191
|
+
const [transformedX, transformedY] = applyToPoint(transform, [x, y])
|
|
192
|
+
const scale = Math.abs(transform.a)
|
|
193
|
+
const scaledFontSize = fontSize * scale
|
|
194
|
+
const layout = getAlphabetLayout(text, scaledFontSize)
|
|
195
|
+
const startPos = getTextStartPosition(anchorAlignment, layout)
|
|
196
|
+
|
|
197
|
+
ctx.save()
|
|
198
|
+
ctx.translate(transformedX, transformedY)
|
|
199
|
+
|
|
200
|
+
if (rotation !== 0) {
|
|
201
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ctx.lineWidth = layout.strokeWidth
|
|
205
|
+
ctx.lineCap = "round"
|
|
206
|
+
ctx.lineJoin = "round"
|
|
207
|
+
ctx.strokeStyle = color
|
|
208
|
+
|
|
209
|
+
strokeAlphabetText(
|
|
210
|
+
ctx,
|
|
211
|
+
text,
|
|
212
|
+
layout,
|
|
213
|
+
startPos.x,
|
|
214
|
+
startPos.y + layout.strokeWidth / 2,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
ctx.restore()
|
|
218
|
+
}
|
package/lib/drawer/types.ts
CHANGED
|
@@ -34,13 +34,21 @@ export interface CanvasContext {
|
|
|
34
34
|
translate(x: number, y: number): void
|
|
35
35
|
rotate(angle: number): void
|
|
36
36
|
scale(x: number, y: number): void
|
|
37
|
+
globalCompositeOperation?: string
|
|
37
38
|
fillStyle: string | CanvasGradient | CanvasPattern
|
|
38
39
|
strokeStyle: string | CanvasGradient | CanvasPattern
|
|
39
40
|
lineWidth: number
|
|
40
41
|
lineCap: "butt" | "round" | "square"
|
|
41
42
|
lineJoin: "bevel" | "round" | "miter"
|
|
43
|
+
setLineDash(segments: number[]): void
|
|
42
44
|
canvas: { width: number; height: number }
|
|
43
45
|
fillText(text: string, x: number, y: number): void
|
|
46
|
+
fillRect(x: number, y: number, width: number, height: number): void
|
|
47
|
+
measureText?: (text: string) => {
|
|
48
|
+
width: number
|
|
49
|
+
actualBoundingBoxAscent?: number
|
|
50
|
+
actualBoundingBoxDescent?: number
|
|
51
|
+
}
|
|
44
52
|
font: string
|
|
45
53
|
textAlign: "start" | "end" | "left" | "right" | "center"
|
|
46
54
|
textBaseline:
|
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.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@biomejs/biome": "^2.3.8",
|
|
13
|
+
"@tscircuit/alphabet": "^0.0.8",
|
|
13
14
|
"@types/bun": "latest",
|
|
14
15
|
"bun-match-svg": "^0.0.14",
|
|
15
16
|
"canvas": "^3.2.0",
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"tsup": "^8.5.1"
|
|
18
19
|
},
|
|
19
20
|
"peerDependencies": {
|
|
21
|
+
"@tscircuit/alphabet": "*",
|
|
20
22
|
"typescript": "^5"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbCopperText } from "circuit-json"
|
|
4
|
+
import { scale } from "transformation-matrix"
|
|
5
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
6
|
+
|
|
7
|
+
test("draw copper text", async () => {
|
|
8
|
+
const SCALE = 4
|
|
9
|
+
const canvas = createCanvas(100 * SCALE, 100 * SCALE)
|
|
10
|
+
const ctx = canvas.getContext("2d")
|
|
11
|
+
ctx.scale(SCALE, SCALE)
|
|
12
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
13
|
+
|
|
14
|
+
ctx.fillStyle = "#1a1a1a"
|
|
15
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
16
|
+
|
|
17
|
+
const text: PcbCopperText[] = [
|
|
18
|
+
{
|
|
19
|
+
type: "pcb_copper_text",
|
|
20
|
+
pcb_copper_text_id: "copper-text-1",
|
|
21
|
+
pcb_component_id: "component1",
|
|
22
|
+
layer: "top",
|
|
23
|
+
text: "T1",
|
|
24
|
+
anchor_position: { x: 40, y: 40 },
|
|
25
|
+
anchor_alignment: "center",
|
|
26
|
+
font: "tscircuit2024",
|
|
27
|
+
font_size: 8,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
type: "pcb_copper_text",
|
|
32
|
+
pcb_copper_text_id: "copper-text-2",
|
|
33
|
+
pcb_component_id: "component1",
|
|
34
|
+
layer: "bottom",
|
|
35
|
+
text: "T2",
|
|
36
|
+
anchor_position: { x: 60, y: 60 },
|
|
37
|
+
anchor_alignment: "center",
|
|
38
|
+
font: "tscircuit2024",
|
|
39
|
+
font_size: 6,
|
|
40
|
+
is_mirrored: true,
|
|
41
|
+
ccw_rotation: 90,
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
drawer.drawElements(text)
|
|
46
|
+
|
|
47
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
48
|
+
import.meta.path,
|
|
49
|
+
"pcb-copper-text",
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("draw copper text knockout mirrored with padding", async () => {
|
|
54
|
+
const SCALE = 4
|
|
55
|
+
const canvas = createCanvas(100 * SCALE, 60 * SCALE)
|
|
56
|
+
const ctx = canvas.getContext("2d")
|
|
57
|
+
ctx.scale(SCALE, SCALE)
|
|
58
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
59
|
+
|
|
60
|
+
ctx.fillStyle = "#1a1a1a"
|
|
61
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
62
|
+
|
|
63
|
+
drawer.realToCanvasMat = scale(2, 2)
|
|
64
|
+
|
|
65
|
+
const text: PcbCopperText[] = [
|
|
66
|
+
{
|
|
67
|
+
type: "pcb_copper_text",
|
|
68
|
+
pcb_copper_text_id: "copper-text-2",
|
|
69
|
+
pcb_component_id: "component1",
|
|
70
|
+
layer: "bottom",
|
|
71
|
+
text: "KO1",
|
|
72
|
+
anchor_position: { x: 25, y: 15 },
|
|
73
|
+
anchor_alignment: "center",
|
|
74
|
+
font: "tscircuit2024",
|
|
75
|
+
font_size: 10,
|
|
76
|
+
is_knockout: true,
|
|
77
|
+
is_mirrored: true,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
type: "pcb_copper_text",
|
|
82
|
+
pcb_copper_text_id: "copper-text-3",
|
|
83
|
+
pcb_component_id: "component1",
|
|
84
|
+
layer: "top",
|
|
85
|
+
text: "KO2",
|
|
86
|
+
anchor_position: { x: 40, y: 20 },
|
|
87
|
+
anchor_alignment: "center",
|
|
88
|
+
font: "tscircuit2024",
|
|
89
|
+
font_size: 8,
|
|
90
|
+
is_knockout: true,
|
|
91
|
+
is_mirrored: false,
|
|
92
|
+
ccw_rotation: 45,
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
drawer.drawElements(text)
|
|
97
|
+
|
|
98
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
99
|
+
import.meta.path,
|
|
100
|
+
"pcb-copper-text-knockout",
|
|
101
|
+
)
|
|
102
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNotePath } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw fabrication note path with custom color", 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 path: PcbFabricationNotePath = {
|
|
15
|
+
type: "pcb_fabrication_note_path",
|
|
16
|
+
pcb_fabrication_note_path_id: "path1",
|
|
17
|
+
pcb_component_id: "component1",
|
|
18
|
+
layer: "top",
|
|
19
|
+
route: [
|
|
20
|
+
{ x: 10, y: 50 },
|
|
21
|
+
{ x: 30, y: 20 },
|
|
22
|
+
{ x: 70, y: 20 },
|
|
23
|
+
{ x: 90, y: 50 },
|
|
24
|
+
{ x: 70, y: 80 },
|
|
25
|
+
{ x: 30, y: 80 },
|
|
26
|
+
{ x: 10, y: 50 },
|
|
27
|
+
],
|
|
28
|
+
stroke_width: 2,
|
|
29
|
+
color: "#FF0000", // Red color
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
drawer.drawElements([path])
|
|
33
|
+
|
|
34
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
35
|
+
import.meta.path,
|
|
36
|
+
)
|
|
37
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNotePath } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw fabrication note path with thick stroke", 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 path: PcbFabricationNotePath = {
|
|
15
|
+
type: "pcb_fabrication_note_path",
|
|
16
|
+
pcb_fabrication_note_path_id: "path1",
|
|
17
|
+
pcb_component_id: "component1",
|
|
18
|
+
layer: "top",
|
|
19
|
+
route: [
|
|
20
|
+
{ x: 10, y: 50 },
|
|
21
|
+
{ x: 30, y: 20 },
|
|
22
|
+
{ x: 70, y: 20 },
|
|
23
|
+
{ x: 90, y: 50 },
|
|
24
|
+
{ x: 70, y: 80 },
|
|
25
|
+
{ x: 30, y: 80 },
|
|
26
|
+
{ x: 10, y: 50 },
|
|
27
|
+
],
|
|
28
|
+
stroke_width: 5, // Thick stroke
|
|
29
|
+
color: "#00FFFF", // Cyan color
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
drawer.drawElements([path])
|
|
33
|
+
|
|
34
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
35
|
+
import.meta.path,
|
|
36
|
+
)
|
|
37
|
+
})
|