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 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 yOffset = startY + height / 2;
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 = yOffset - line.y1 * height;
896
+ const y1 = topY + (1 - line.y1) * height;
895
897
  const x2 = cursor + line.x2 * glyphWidth;
896
- const y2 = yOffset - line.y2 * height;
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 = 0;
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 = -(layout.height / 2) - layout.strokeWidth / 2 - paddingTop;
990
+ const yOffset = textBoxTop - paddingTop;
993
991
  const knockoutWidth = totalWidth + paddingLeft + paddingRight;
994
- const knockoutHeight = totalHeight + paddingTop + paddingBottom;
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 = 0 // Centers vertically at y=0 (shared function calculates yOffset = startY + height/2)
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 = -(layout.height / 2) - layout.strokeWidth / 2 - paddingTop
80
+ const yOffset = textBoxTop - paddingTop
78
81
  const knockoutWidth = totalWidth + paddingLeft + paddingRight
79
- const knockoutHeight = totalHeight + paddingTop + paddingBottom
82
+ const knockoutHeight = textBoxHeight + paddingTop + paddingBottom
80
83
 
81
84
  ctx.fillStyle = textColor
82
85
  ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight)
@@ -88,7 +88,6 @@ export function drawPcbSilkscreenText(
88
88
  ctx.font = `${fontSize}px sans-serif`
89
89
  ctx.fillStyle = color
90
90
  ctx.textAlign = mapAnchorAlignment(text.anchor_alignment)
91
- ctx.textBaseline = "middle"
92
91
  ctx.fillText(text.text, 0, 0)
93
92
  ctx.restore()
94
93
  }
@@ -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 yOffset = startY + height / 2
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 = yOffset - line.y1 * height
150
+ const y1 = topY + (1 - line.y1) * height
146
151
  const x2 = cursor + line.x2 * glyphWidth
147
- const y2 = yOffset - line.y2 * height
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
  }
@@ -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.5",
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.8",
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",
@@ -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: "T1",
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: "SMAL",
21
+ text: "Smapq876",
22
22
  anchor_position: { x: 50, y: 50 },
23
23
  anchor_alignment: "center",
24
24
  font: "tscircuit2024",