circuit-to-canvas 0.0.5 → 0.0.7
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 +0 -1
- package/dist/index.js +14 -16
- package/lib/drawer/elements/pcb-copper-text.ts +8 -5
- package/lib/drawer/elements/pcb-silkscreen.ts +0 -1
- package/lib/drawer/shapes/text.ts +10 -14
- package/lib/drawer/types.ts +0 -7
- package/package.json +2 -2
- package/tests/elements/__snapshots__/fabrication-note-text-baseline-anchors.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-baseline.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-descenders.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-full-charset.snap.png +0 -0
- 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-text-rgba-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-text-small.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-silkscreen.snap.png +0 -0
- package/tests/elements/__snapshots__/silkscreen-text-bottom.snap.png +0 -0
- package/tests/elements/pcb-copper-text.test.ts +1 -1
- package/tests/elements/pcb-fabrication-note-text-descenders.test.ts +43 -0
- package/tests/elements/pcb-fabrication-note-text-full-charset.test.ts +84 -0
- package/tests/elements/pcb-fabrication-note-text-small.test.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,6 @@ interface CanvasContext {
|
|
|
41
41
|
};
|
|
42
42
|
font: string;
|
|
43
43
|
textAlign: "start" | "end" | "left" | "right" | "center";
|
|
44
|
-
textBaseline: "top" | "hanging" | "middle" | "alphabetic" | "ideographic" | "bottom";
|
|
45
44
|
}
|
|
46
45
|
type CopperLayerName = "top" | "bottom" | "inner1" | "inner2" | "inner3" | "inner4" | "inner5" | "inner6";
|
|
47
46
|
type CopperColorMap = Record<CopperLayerName, string> & {
|
package/dist/index.js
CHANGED
|
@@ -659,7 +659,6 @@ function drawPcbSilkscreenText(params) {
|
|
|
659
659
|
ctx.font = `${fontSize}px sans-serif`;
|
|
660
660
|
ctx.fillStyle = color;
|
|
661
661
|
ctx.textAlign = mapAnchorAlignment(text.anchor_alignment);
|
|
662
|
-
ctx.textBaseline = "middle";
|
|
663
662
|
ctx.fillText(text.text, 0, 0);
|
|
664
663
|
ctx.restore();
|
|
665
664
|
}
|
|
@@ -861,21 +860,24 @@ function getTextStartPosition(alignment, layout) {
|
|
|
861
860
|
y = 0;
|
|
862
861
|
} else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom") {
|
|
863
862
|
y = -totalHeight;
|
|
863
|
+
} else {
|
|
864
|
+
y = 0;
|
|
864
865
|
}
|
|
865
866
|
return { x, y };
|
|
866
867
|
}
|
|
867
868
|
function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
868
869
|
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
|
|
869
|
-
const
|
|
870
|
+
const topY = startY;
|
|
870
871
|
const characters = Array.from(text);
|
|
871
872
|
let cursor = startX + strokeWidth / 2;
|
|
872
873
|
characters.forEach((char, index) => {
|
|
873
874
|
const lines = getGlyphLines(char);
|
|
874
875
|
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
875
876
|
if (CURVED_GLYPHS.has(char)) {
|
|
877
|
+
const normalizedCenterY = 0.5;
|
|
878
|
+
const centerY = topY + normalizedCenterY * height;
|
|
876
879
|
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth);
|
|
877
880
|
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth);
|
|
878
|
-
const centerY = yOffset - height / 2;
|
|
879
881
|
ctx.beginPath();
|
|
880
882
|
ctx.ellipse(
|
|
881
883
|
cursor + glyphWidth / 2,
|
|
@@ -891,9 +893,9 @@ function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
|
891
893
|
ctx.beginPath();
|
|
892
894
|
for (const line of lines) {
|
|
893
895
|
const x1 = cursor + line.x1 * glyphWidth;
|
|
894
|
-
const y1 =
|
|
896
|
+
const y1 = topY + (1 - line.y1) * height;
|
|
895
897
|
const x2 = cursor + line.x2 * glyphWidth;
|
|
896
|
-
const y2 =
|
|
898
|
+
const y2 = topY + (1 - line.y2) * height;
|
|
897
899
|
ctx.moveTo(x1, y1);
|
|
898
900
|
ctx.lineTo(x2, y2);
|
|
899
901
|
}
|
|
@@ -932,13 +934,7 @@ function drawText(params) {
|
|
|
932
934
|
ctx.lineCap = "round";
|
|
933
935
|
ctx.lineJoin = "round";
|
|
934
936
|
ctx.strokeStyle = color;
|
|
935
|
-
strokeAlphabetText(
|
|
936
|
-
ctx,
|
|
937
|
-
text,
|
|
938
|
-
layout,
|
|
939
|
-
startPos.x,
|
|
940
|
-
startPos.y + layout.strokeWidth / 2
|
|
941
|
-
);
|
|
937
|
+
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
|
|
942
938
|
ctx.restore();
|
|
943
939
|
}
|
|
944
940
|
|
|
@@ -971,11 +967,10 @@ function drawPcbCopperText(params) {
|
|
|
971
967
|
const textColor = layerToCopperColor(text.layer, colorMap);
|
|
972
968
|
const layout = getAlphabetLayout(content, fontSize);
|
|
973
969
|
const totalWidth = layout.width + layout.strokeWidth;
|
|
974
|
-
const totalHeight = layout.height + layout.strokeWidth;
|
|
975
970
|
const alignment = mapAnchorAlignment2(text.anchor_alignment);
|
|
976
971
|
const startPos = getTextStartPosition(alignment, layout);
|
|
977
972
|
const startX = startPos.x;
|
|
978
|
-
const startY =
|
|
973
|
+
const startY = startPos.y;
|
|
979
974
|
ctx.save();
|
|
980
975
|
ctx.translate(x, y);
|
|
981
976
|
if (text.is_mirrored) ctx.scale(-1, 1);
|
|
@@ -988,10 +983,13 @@ function drawPcbCopperText(params) {
|
|
|
988
983
|
const paddingRight = padding.right * scale2;
|
|
989
984
|
const paddingTop = padding.top * scale2;
|
|
990
985
|
const paddingBottom = padding.bottom * scale2;
|
|
986
|
+
const textBoxTop = startY - layout.strokeWidth / 2;
|
|
987
|
+
const textBoxBottom = startY + layout.height + layout.strokeWidth / 2;
|
|
988
|
+
const textBoxHeight = textBoxBottom - textBoxTop;
|
|
991
989
|
const xOffset = startX - paddingLeft;
|
|
992
|
-
const yOffset =
|
|
990
|
+
const yOffset = textBoxTop - paddingTop;
|
|
993
991
|
const knockoutWidth = totalWidth + paddingLeft + paddingRight;
|
|
994
|
-
const knockoutHeight =
|
|
992
|
+
const knockoutHeight = textBoxHeight + paddingTop + paddingBottom;
|
|
995
993
|
ctx.fillStyle = textColor;
|
|
996
994
|
ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight);
|
|
997
995
|
const previousCompositeOperation = ctx.globalCompositeOperation;
|
|
@@ -52,12 +52,10 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
|
|
|
52
52
|
const textColor = layerToCopperColor(text.layer, colorMap)
|
|
53
53
|
const layout = getAlphabetLayout(content, fontSize)
|
|
54
54
|
const totalWidth = layout.width + layout.strokeWidth
|
|
55
|
-
const totalHeight = layout.height + layout.strokeWidth
|
|
56
55
|
const alignment = mapAnchorAlignment(text.anchor_alignment)
|
|
57
56
|
const startPos = getTextStartPosition(alignment, layout)
|
|
58
|
-
// Copper text always centers vertically (startY=0), uses startPos.x for horizontal alignment
|
|
59
57
|
const startX = startPos.x
|
|
60
|
-
const startY =
|
|
58
|
+
const startY = startPos.y
|
|
61
59
|
|
|
62
60
|
ctx.save()
|
|
63
61
|
ctx.translate(x, y)
|
|
@@ -73,10 +71,15 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
|
|
|
73
71
|
const paddingRight = padding.right * scale
|
|
74
72
|
const paddingTop = padding.top * scale
|
|
75
73
|
const paddingBottom = padding.bottom * scale
|
|
74
|
+
// Calculate knockout rectangle to cover the text box
|
|
75
|
+
const textBoxTop = startY - layout.strokeWidth / 2
|
|
76
|
+
const textBoxBottom = startY + layout.height + layout.strokeWidth / 2
|
|
77
|
+
const textBoxHeight = textBoxBottom - textBoxTop
|
|
78
|
+
|
|
76
79
|
const xOffset = startX - paddingLeft
|
|
77
|
-
const yOffset =
|
|
80
|
+
const yOffset = textBoxTop - paddingTop
|
|
78
81
|
const knockoutWidth = totalWidth + paddingLeft + paddingRight
|
|
79
|
-
const knockoutHeight =
|
|
82
|
+
const knockoutHeight = textBoxHeight + paddingTop + paddingBottom
|
|
80
83
|
|
|
81
84
|
ctx.fillStyle = textColor
|
|
82
85
|
ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight)
|
|
@@ -102,6 +102,8 @@ export function getTextStartPosition(
|
|
|
102
102
|
alignment === "bottom"
|
|
103
103
|
) {
|
|
104
104
|
y = -totalHeight
|
|
105
|
+
} else {
|
|
106
|
+
y = 0
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
return { x, y }
|
|
@@ -115,7 +117,7 @@ export function strokeAlphabetText(
|
|
|
115
117
|
startY: number,
|
|
116
118
|
): void {
|
|
117
119
|
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout
|
|
118
|
-
const
|
|
120
|
+
const topY = startY
|
|
119
121
|
const characters = Array.from(text)
|
|
120
122
|
let cursor = startX + strokeWidth / 2
|
|
121
123
|
|
|
@@ -124,9 +126,10 @@ export function strokeAlphabetText(
|
|
|
124
126
|
const advance = char === " " ? spaceWidth : glyphWidth
|
|
125
127
|
|
|
126
128
|
if (CURVED_GLYPHS.has(char)) {
|
|
129
|
+
const normalizedCenterY = 0.5
|
|
130
|
+
const centerY = topY + normalizedCenterY * height
|
|
127
131
|
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth)
|
|
128
132
|
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth)
|
|
129
|
-
const centerY = yOffset - height / 2
|
|
130
133
|
ctx.beginPath()
|
|
131
134
|
ctx.ellipse(
|
|
132
135
|
cursor + glyphWidth / 2,
|
|
@@ -141,20 +144,19 @@ export function strokeAlphabetText(
|
|
|
141
144
|
} else if (lines?.length) {
|
|
142
145
|
ctx.beginPath()
|
|
143
146
|
for (const line of lines) {
|
|
147
|
+
// Convert normalized y coordinates to canvas coordinates (inverted for canvas)
|
|
148
|
+
// In normalized coords: y=0 is bottom, y=1 is top
|
|
144
149
|
const x1 = cursor + line.x1 * glyphWidth
|
|
145
|
-
const y1 =
|
|
150
|
+
const y1 = topY + (1 - line.y1) * height
|
|
146
151
|
const x2 = cursor + line.x2 * glyphWidth
|
|
147
|
-
const y2 =
|
|
152
|
+
const y2 = topY + (1 - line.y2) * height
|
|
148
153
|
ctx.moveTo(x1, y1)
|
|
149
154
|
ctx.lineTo(x2, y2)
|
|
150
155
|
}
|
|
151
156
|
ctx.stroke()
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
// Move cursor by the character width
|
|
155
159
|
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
160
|
if (index < characters.length - 1) {
|
|
159
161
|
cursor += letterSpacing
|
|
160
162
|
}
|
|
@@ -206,13 +208,7 @@ export function drawText(params: DrawTextParams): void {
|
|
|
206
208
|
ctx.lineJoin = "round"
|
|
207
209
|
ctx.strokeStyle = color
|
|
208
210
|
|
|
209
|
-
strokeAlphabetText(
|
|
210
|
-
ctx,
|
|
211
|
-
text,
|
|
212
|
-
layout,
|
|
213
|
-
startPos.x,
|
|
214
|
-
startPos.y + layout.strokeWidth / 2,
|
|
215
|
-
)
|
|
211
|
+
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y)
|
|
216
212
|
|
|
217
213
|
ctx.restore()
|
|
218
214
|
}
|
package/lib/drawer/types.ts
CHANGED
|
@@ -51,13 +51,6 @@ export interface CanvasContext {
|
|
|
51
51
|
}
|
|
52
52
|
font: string
|
|
53
53
|
textAlign: "start" | "end" | "left" | "right" | "center"
|
|
54
|
-
textBaseline:
|
|
55
|
-
| "top"
|
|
56
|
-
| "hanging"
|
|
57
|
-
| "middle"
|
|
58
|
-
| "alphabetic"
|
|
59
|
-
| "ideographic"
|
|
60
|
-
| "bottom"
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
export type CopperLayerName =
|
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.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@biomejs/biome": "^2.3.8",
|
|
13
|
-
"@tscircuit/alphabet": "^0.0.
|
|
13
|
+
"@tscircuit/alphabet": "^0.0.9",
|
|
14
14
|
"@types/bun": "latest",
|
|
15
15
|
"bun-match-svg": "^0.0.14",
|
|
16
16
|
"canvas": "^3.2.0",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -20,7 +20,7 @@ test("draw copper text", async () => {
|
|
|
20
20
|
pcb_copper_text_id: "copper-text-1",
|
|
21
21
|
pcb_component_id: "component1",
|
|
22
22
|
layer: "top",
|
|
23
|
-
text: "
|
|
23
|
+
text: "AabcbCdde",
|
|
24
24
|
anchor_position: { x: 40, y: 40 },
|
|
25
25
|
anchor_alignment: "center",
|
|
26
26
|
font: "tscircuit2024",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw lowercase text with descenders", async () => {
|
|
7
|
+
const SCALE = 4
|
|
8
|
+
const canvas = createCanvas(250 * SCALE, 100 * SCALE)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(SCALE, SCALE)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
|
|
13
|
+
ctx.fillStyle = "#1a1a1a"
|
|
14
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
15
|
+
|
|
16
|
+
// Draw reference line
|
|
17
|
+
ctx.strokeStyle = "#666666"
|
|
18
|
+
ctx.lineWidth = 0.5
|
|
19
|
+
ctx.beginPath()
|
|
20
|
+
ctx.moveTo(10, 50)
|
|
21
|
+
ctx.lineTo(240, 50)
|
|
22
|
+
ctx.stroke()
|
|
23
|
+
|
|
24
|
+
// Test all descender letters: g, j, p, q, y
|
|
25
|
+
const text: PcbFabricationNoteText = {
|
|
26
|
+
type: "pcb_fabrication_note_text",
|
|
27
|
+
pcb_fabrication_note_text_id: "fab-note-descenders",
|
|
28
|
+
pcb_component_id: "component1",
|
|
29
|
+
layer: "top",
|
|
30
|
+
text: "gjpqy",
|
|
31
|
+
anchor_position: { x: 125, y: 50 },
|
|
32
|
+
anchor_alignment: "center",
|
|
33
|
+
font: "tscircuit2024",
|
|
34
|
+
font_size: 24,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
drawer.drawElements([text])
|
|
38
|
+
|
|
39
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
40
|
+
import.meta.path,
|
|
41
|
+
"fabrication-note-text-descenders",
|
|
42
|
+
)
|
|
43
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw full character set", async () => {
|
|
7
|
+
const SCALE = 4
|
|
8
|
+
const canvas = createCanvas(800 * SCALE, 300 * SCALE)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(SCALE, SCALE)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
|
|
13
|
+
ctx.fillStyle = "#1a1a1a"
|
|
14
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
15
|
+
|
|
16
|
+
const fontSize = 20
|
|
17
|
+
const lineHeight = 40
|
|
18
|
+
let y = 30
|
|
19
|
+
|
|
20
|
+
// Lowercase letters
|
|
21
|
+
const lowercaseText: PcbFabricationNoteText = {
|
|
22
|
+
type: "pcb_fabrication_note_text",
|
|
23
|
+
pcb_fabrication_note_text_id: "fab-note-lowercase",
|
|
24
|
+
pcb_component_id: "component1",
|
|
25
|
+
layer: "top",
|
|
26
|
+
text: "abcdefghijklmnopqrstuvwxyz",
|
|
27
|
+
anchor_position: { x: 400, y },
|
|
28
|
+
anchor_alignment: "center",
|
|
29
|
+
font: "tscircuit2024",
|
|
30
|
+
font_size: fontSize,
|
|
31
|
+
}
|
|
32
|
+
drawer.drawElements([lowercaseText])
|
|
33
|
+
y += lineHeight
|
|
34
|
+
|
|
35
|
+
// Uppercase letters
|
|
36
|
+
const uppercaseText: PcbFabricationNoteText = {
|
|
37
|
+
type: "pcb_fabrication_note_text",
|
|
38
|
+
pcb_fabrication_note_text_id: "fab-note-uppercase",
|
|
39
|
+
pcb_component_id: "component2",
|
|
40
|
+
layer: "top",
|
|
41
|
+
text: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
42
|
+
anchor_position: { x: 400, y },
|
|
43
|
+
anchor_alignment: "center",
|
|
44
|
+
font: "tscircuit2024",
|
|
45
|
+
font_size: fontSize,
|
|
46
|
+
}
|
|
47
|
+
drawer.drawElements([uppercaseText])
|
|
48
|
+
y += lineHeight
|
|
49
|
+
|
|
50
|
+
// Numbers
|
|
51
|
+
const numbersText: PcbFabricationNoteText = {
|
|
52
|
+
type: "pcb_fabrication_note_text",
|
|
53
|
+
pcb_fabrication_note_text_id: "fab-note-numbers",
|
|
54
|
+
pcb_component_id: "component3",
|
|
55
|
+
layer: "top",
|
|
56
|
+
text: "0123456789",
|
|
57
|
+
anchor_position: { x: 400, y },
|
|
58
|
+
anchor_alignment: "center",
|
|
59
|
+
font: "tscircuit2024",
|
|
60
|
+
font_size: fontSize,
|
|
61
|
+
}
|
|
62
|
+
drawer.drawElements([numbersText])
|
|
63
|
+
y += lineHeight
|
|
64
|
+
|
|
65
|
+
// Common symbols
|
|
66
|
+
const symbolsText: PcbFabricationNoteText = {
|
|
67
|
+
type: "pcb_fabrication_note_text",
|
|
68
|
+
pcb_fabrication_note_text_id: "fab-note-symbols",
|
|
69
|
+
pcb_component_id: "component4",
|
|
70
|
+
layer: "top",
|
|
71
|
+
text: "()!@#$%^&*",
|
|
72
|
+
anchor_position: { x: 400, y },
|
|
73
|
+
anchor_alignment: "center",
|
|
74
|
+
font: "tscircuit2024",
|
|
75
|
+
font_size: fontSize,
|
|
76
|
+
}
|
|
77
|
+
drawer.drawElements([symbolsText])
|
|
78
|
+
y += lineHeight
|
|
79
|
+
|
|
80
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
81
|
+
import.meta.path,
|
|
82
|
+
"fabrication-note-text-full-charset",
|
|
83
|
+
)
|
|
84
|
+
})
|
|
@@ -18,7 +18,7 @@ test("draw fabrication note text small size", async () => {
|
|
|
18
18
|
pcb_fabrication_note_text_id: "fab-note-small",
|
|
19
19
|
pcb_component_id: "component1",
|
|
20
20
|
layer: "top",
|
|
21
|
-
text: "
|
|
21
|
+
text: "Smapq876",
|
|
22
22
|
anchor_position: { x: 50, y: 50 },
|
|
23
23
|
anchor_alignment: "center",
|
|
24
24
|
font: "tscircuit2024",
|