circuit-to-canvas 0.0.19 → 0.0.20

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.js CHANGED
@@ -647,37 +647,142 @@ function drawPcbBoard(params) {
647
647
  }
648
648
  }
649
649
 
650
- // lib/drawer/elements/pcb-silkscreen.ts
650
+ // lib/drawer/shapes/text/text.ts
651
+ import { lineAlphabet } from "@tscircuit/alphabet";
651
652
  import { applyToPoint as applyToPoint8 } from "transformation-matrix";
652
- function layerToSilkscreenColor(layer, colorMap) {
653
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
653
+
654
+ // lib/drawer/shapes/text/getAlphabetLayout.ts
655
+ var GLYPH_WIDTH_RATIO = 0.62;
656
+ var LETTER_SPACING_RATIO = 0.3;
657
+ var SPACE_WIDTH_RATIO = 1;
658
+ var STROKE_WIDTH_RATIO = 0.13;
659
+ function getAlphabetLayout(text, fontSize) {
660
+ const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
661
+ const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
662
+ const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
663
+ const characters = Array.from(text);
664
+ let width = 0;
665
+ characters.forEach((char, index) => {
666
+ const advance = char === " " ? spaceWidth : glyphWidth;
667
+ width += advance;
668
+ if (index < characters.length - 1) width += letterSpacing;
669
+ });
670
+ const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
671
+ return {
672
+ width,
673
+ height: fontSize,
674
+ glyphWidth,
675
+ letterSpacing,
676
+ spaceWidth,
677
+ strokeWidth
678
+ };
654
679
  }
655
- function mapAnchorAlignment(alignment) {
656
- if (!alignment) return "center";
657
- if (alignment.includes("left")) return "left";
658
- if (alignment.includes("right")) return "right";
659
- return "center";
680
+
681
+ // lib/drawer/shapes/text/getTextStartPosition.ts
682
+ function getTextStartPosition(alignment, layout) {
683
+ const totalWidth = layout.width + layout.strokeWidth;
684
+ const totalHeight = layout.height + layout.strokeWidth;
685
+ let x = 0;
686
+ let y = 0;
687
+ if (alignment === "center") {
688
+ x = -totalWidth / 2;
689
+ } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
690
+ x = 0;
691
+ } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
692
+ x = -totalWidth;
693
+ }
694
+ if (alignment === "center") {
695
+ y = -totalHeight / 2;
696
+ } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
697
+ y = 0;
698
+ } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
699
+ y = -totalHeight;
700
+ } else {
701
+ y = 0;
702
+ }
703
+ return { x, y };
660
704
  }
661
- function drawPcbSilkscreenText(params) {
662
- const { ctx, text, realToCanvasMat, colorMap } = params;
663
- const color = layerToSilkscreenColor(text.layer, colorMap);
664
- const [x, y] = applyToPoint8(realToCanvasMat, [
665
- text.anchor_position.x,
666
- text.anchor_position.y
667
- ]);
668
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a);
669
- const rotation = text.ccw_rotation ?? 0;
705
+
706
+ // lib/drawer/shapes/text/text.ts
707
+ var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
708
+ function strokeAlphabetText(ctx, text, layout, startX, startY) {
709
+ const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
710
+ const topY = startY;
711
+ const characters = Array.from(text);
712
+ let cursor = startX + strokeWidth / 2;
713
+ characters.forEach((char, index) => {
714
+ const lines = getGlyphLines(char);
715
+ const advance = char === " " ? spaceWidth : glyphWidth;
716
+ if (lines?.length) {
717
+ ctx.beginPath();
718
+ for (const line of lines) {
719
+ const x1 = cursor + line.x1 * glyphWidth;
720
+ const y1 = topY + (1 - line.y1) * height;
721
+ const x2 = cursor + line.x2 * glyphWidth;
722
+ const y2 = topY + (1 - line.y2) * height;
723
+ ctx.moveTo(x1, y1);
724
+ ctx.lineTo(x2, y2);
725
+ }
726
+ ctx.stroke();
727
+ }
728
+ cursor += advance;
729
+ if (index < characters.length - 1) {
730
+ cursor += letterSpacing;
731
+ }
732
+ });
733
+ }
734
+ function drawText(params) {
735
+ const {
736
+ ctx,
737
+ text,
738
+ x,
739
+ y,
740
+ fontSize,
741
+ color,
742
+ realToCanvasMat,
743
+ anchorAlignment,
744
+ rotation = 0
745
+ } = params;
746
+ if (!text) return;
747
+ const [canvasX, canvasY] = applyToPoint8(realToCanvasMat, [x, y]);
748
+ const scale2 = Math.abs(realToCanvasMat.a);
749
+ const scaledFontSize = fontSize * scale2;
750
+ const layout = getAlphabetLayout(text, scaledFontSize);
751
+ const startPos = getTextStartPosition(anchorAlignment, layout);
670
752
  ctx.save();
671
- ctx.translate(x, y);
753
+ ctx.translate(canvasX, canvasY);
672
754
  if (rotation !== 0) {
673
755
  ctx.rotate(-rotation * (Math.PI / 180));
674
756
  }
675
- ctx.font = `${fontSize}px sans-serif`;
676
- ctx.fillStyle = color;
677
- ctx.textAlign = mapAnchorAlignment(text.anchor_alignment);
678
- ctx.fillText(text.text, 0, 0);
757
+ ctx.lineWidth = layout.strokeWidth;
758
+ ctx.lineCap = "round";
759
+ ctx.lineJoin = "round";
760
+ ctx.strokeStyle = color;
761
+ strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
679
762
  ctx.restore();
680
763
  }
764
+
765
+ // lib/drawer/elements/pcb-silkscreen.ts
766
+ function layerToSilkscreenColor(layer, colorMap) {
767
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
768
+ }
769
+ function drawPcbSilkscreenText(params) {
770
+ const { ctx, text, realToCanvasMat, colorMap } = params;
771
+ const color = layerToSilkscreenColor(text.layer, colorMap);
772
+ const fontSize = text.font_size ?? 1;
773
+ const rotation = text.ccw_rotation ?? 0;
774
+ drawText({
775
+ ctx,
776
+ text: text.text,
777
+ x: text.anchor_position.x,
778
+ y: text.anchor_position.y,
779
+ fontSize,
780
+ color,
781
+ realToCanvasMat,
782
+ anchorAlignment: text.anchor_alignment ?? "center",
783
+ rotation
784
+ });
785
+ }
681
786
  function drawPcbSilkscreenRect(params) {
682
787
  const { ctx, rect, realToCanvasMat, colorMap } = params;
683
788
  const color = layerToSilkscreenColor(rect.layer, colorMap);
@@ -829,129 +934,12 @@ function drawPcbCopperPour(params) {
829
934
  }
830
935
 
831
936
  // lib/drawer/elements/pcb-copper-text.ts
832
- import { applyToPoint as applyToPoint11 } from "transformation-matrix";
833
-
834
- // lib/drawer/shapes/text/text.ts
835
- import { lineAlphabet } from "@tscircuit/alphabet";
836
937
  import { applyToPoint as applyToPoint10 } from "transformation-matrix";
837
-
838
- // lib/drawer/shapes/text/getAlphabetLayout.ts
839
- var GLYPH_WIDTH_RATIO = 0.62;
840
- var LETTER_SPACING_RATIO = 0.3;
841
- var SPACE_WIDTH_RATIO = 1;
842
- var STROKE_WIDTH_RATIO = 0.13;
843
- function getAlphabetLayout(text, fontSize) {
844
- const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
845
- const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
846
- const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
847
- const characters = Array.from(text);
848
- let width = 0;
849
- characters.forEach((char, index) => {
850
- const advance = char === " " ? spaceWidth : glyphWidth;
851
- width += advance;
852
- if (index < characters.length - 1) width += letterSpacing;
853
- });
854
- const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
855
- return {
856
- width,
857
- height: fontSize,
858
- glyphWidth,
859
- letterSpacing,
860
- spaceWidth,
861
- strokeWidth
862
- };
863
- }
864
-
865
- // lib/drawer/shapes/text/getTextStartPosition.ts
866
- function getTextStartPosition(alignment, layout) {
867
- const totalWidth = layout.width + layout.strokeWidth;
868
- const totalHeight = layout.height + layout.strokeWidth;
869
- let x = 0;
870
- let y = 0;
871
- if (alignment === "center") {
872
- x = -totalWidth / 2;
873
- } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
874
- x = 0;
875
- } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
876
- x = -totalWidth;
877
- }
878
- if (alignment === "center") {
879
- y = -totalHeight / 2;
880
- } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
881
- y = 0;
882
- } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
883
- y = -totalHeight;
884
- } else {
885
- y = 0;
886
- }
887
- return { x, y };
888
- }
889
-
890
- // lib/drawer/shapes/text/text.ts
891
- var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
892
- function strokeAlphabetText(ctx, text, layout, startX, startY) {
893
- const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
894
- const topY = startY;
895
- const characters = Array.from(text);
896
- let cursor = startX + strokeWidth / 2;
897
- characters.forEach((char, index) => {
898
- const lines = getGlyphLines(char);
899
- const advance = char === " " ? spaceWidth : glyphWidth;
900
- if (lines?.length) {
901
- ctx.beginPath();
902
- for (const line of lines) {
903
- const x1 = cursor + line.x1 * glyphWidth;
904
- const y1 = topY + (1 - line.y1) * height;
905
- const x2 = cursor + line.x2 * glyphWidth;
906
- const y2 = topY + (1 - line.y2) * height;
907
- ctx.moveTo(x1, y1);
908
- ctx.lineTo(x2, y2);
909
- }
910
- ctx.stroke();
911
- }
912
- cursor += advance;
913
- if (index < characters.length - 1) {
914
- cursor += letterSpacing;
915
- }
916
- });
917
- }
918
- function drawText(params) {
919
- const {
920
- ctx,
921
- text,
922
- x,
923
- y,
924
- fontSize,
925
- color,
926
- realToCanvasMat,
927
- anchorAlignment,
928
- rotation = 0
929
- } = params;
930
- if (!text) return;
931
- const [canvasX, canvasY] = applyToPoint10(realToCanvasMat, [x, y]);
932
- const scale2 = Math.abs(realToCanvasMat.a);
933
- const scaledFontSize = fontSize * scale2;
934
- const layout = getAlphabetLayout(text, scaledFontSize);
935
- const startPos = getTextStartPosition(anchorAlignment, layout);
936
- ctx.save();
937
- ctx.translate(canvasX, canvasY);
938
- if (rotation !== 0) {
939
- ctx.rotate(-rotation * (Math.PI / 180));
940
- }
941
- ctx.lineWidth = layout.strokeWidth;
942
- ctx.lineCap = "round";
943
- ctx.lineJoin = "round";
944
- ctx.strokeStyle = color;
945
- strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
946
- ctx.restore();
947
- }
948
-
949
- // lib/drawer/elements/pcb-copper-text.ts
950
938
  var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
951
939
  function layerToCopperColor(layer, colorMap) {
952
940
  return colorMap.copper[layer] ?? colorMap.copper.top;
953
941
  }
954
- function mapAnchorAlignment2(alignment) {
942
+ function mapAnchorAlignment(alignment) {
955
943
  if (!alignment) return "center";
956
944
  if (alignment.includes("left")) return "center_left";
957
945
  if (alignment.includes("right")) return "center_right";
@@ -961,7 +949,7 @@ function drawPcbCopperText(params) {
961
949
  const { ctx, text, realToCanvasMat, colorMap } = params;
962
950
  const content = text.text ?? "";
963
951
  if (!content) return;
964
- const [x, y] = applyToPoint11(realToCanvasMat, [
952
+ const [x, y] = applyToPoint10(realToCanvasMat, [
965
953
  text.anchor_position.x,
966
954
  text.anchor_position.y
967
955
  ]);
@@ -975,7 +963,7 @@ function drawPcbCopperText(params) {
975
963
  const textColor = layerToCopperColor(text.layer, colorMap);
976
964
  const layout = getAlphabetLayout(content, fontSize);
977
965
  const totalWidth = layout.width + layout.strokeWidth;
978
- const alignment = mapAnchorAlignment2(text.anchor_alignment);
966
+ const alignment = mapAnchorAlignment(text.anchor_alignment);
979
967
  const startPos = getTextStartPosition(alignment, layout);
980
968
  const startX = startPos.x;
981
969
  const startY = startPos.y;
@@ -1145,7 +1133,7 @@ function drawPcbNoteText(params) {
1145
1133
  }
1146
1134
 
1147
1135
  // lib/drawer/elements/pcb-note-dimension.ts
1148
- import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1136
+ import { applyToPoint as applyToPoint11 } from "transformation-matrix";
1149
1137
 
1150
1138
  // lib/drawer/shapes/arrow.ts
1151
1139
  function drawArrow(params) {
@@ -1230,11 +1218,11 @@ function drawPcbNoteDimension(params) {
1230
1218
  stroke: color,
1231
1219
  realToCanvasMat
1232
1220
  });
1233
- const [canvasFromX, canvasFromY] = applyToPoint12(realToCanvasMat, [
1221
+ const [canvasFromX, canvasFromY] = applyToPoint11(realToCanvasMat, [
1234
1222
  fromX,
1235
1223
  fromY
1236
1224
  ]);
1237
- const [canvasToX, canvasToY] = applyToPoint12(realToCanvasMat, [toX, toY]);
1225
+ const [canvasToX, canvasToY] = applyToPoint11(realToCanvasMat, [toX, toY]);
1238
1226
  const canvasDx = canvasToX - canvasFromX;
1239
1227
  const canvasDy = canvasToY - canvasFromY;
1240
1228
  const lineAngle = Math.atan2(canvasDy, canvasDx);
@@ -1295,15 +1283,15 @@ function drawPcbNoteDimension(params) {
1295
1283
  }
1296
1284
 
1297
1285
  // lib/drawer/elements/pcb-note-line.ts
1298
- import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1286
+ import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1299
1287
  function drawPcbNoteLine(params) {
1300
1288
  const { ctx, line, realToCanvasMat, colorMap } = params;
1301
1289
  const defaultColor = "rgb(89, 148, 220)";
1302
1290
  const color = line.color ?? defaultColor;
1303
1291
  const strokeWidth = line.stroke_width ?? 0.1;
1304
1292
  const isDashed = line.is_dashed ?? false;
1305
- const [x1, y1] = applyToPoint13(realToCanvasMat, [line.x1, line.y1]);
1306
- const [x2, y2] = applyToPoint13(realToCanvasMat, [line.x2, line.y2]);
1293
+ const [x1, y1] = applyToPoint12(realToCanvasMat, [line.x1, line.y1]);
1294
+ const [x2, y2] = applyToPoint12(realToCanvasMat, [line.x2, line.y2]);
1307
1295
  const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1308
1296
  ctx.save();
1309
1297
  if (isDashed) {
@@ -6,12 +6,12 @@ import type {
6
6
  PcbSilkscreenPath,
7
7
  } from "circuit-json"
8
8
  import type { Matrix } from "transformation-matrix"
9
- import { applyToPoint } from "transformation-matrix"
10
9
  import type { PcbColorMap, CanvasContext } from "../types"
11
10
  import { drawRect } from "../shapes/rect"
12
11
  import { drawCircle } from "../shapes/circle"
13
12
  import { drawLine } from "../shapes/line"
14
13
  import { drawPath } from "../shapes/path"
14
+ import { drawText } from "../shapes/text"
15
15
 
16
16
  export interface DrawPcbSilkscreenTextParams {
17
17
  ctx: CanvasContext
@@ -54,42 +54,27 @@ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
54
54
  : colorMap.silkscreen.top
55
55
  }
56
56
 
57
- function mapAnchorAlignment(
58
- alignment?: string,
59
- ): "start" | "end" | "left" | "right" | "center" {
60
- if (!alignment) return "center"
61
- if (alignment.includes("left")) return "left"
62
- if (alignment.includes("right")) return "right"
63
- return "center"
64
- }
65
-
66
57
  export function drawPcbSilkscreenText(
67
58
  params: DrawPcbSilkscreenTextParams,
68
59
  ): void {
69
60
  const { ctx, text, realToCanvasMat, colorMap } = params
70
61
 
71
62
  const color = layerToSilkscreenColor(text.layer, colorMap)
72
- const [x, y] = applyToPoint(realToCanvasMat, [
73
- text.anchor_position.x,
74
- text.anchor_position.y,
75
- ])
76
-
77
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a)
63
+ const fontSize = text.font_size ?? 1
78
64
  const rotation = text.ccw_rotation ?? 0
79
65
 
80
- ctx.save()
81
- ctx.translate(x, y)
82
-
83
- // Apply rotation (CCW rotation in degrees)
84
- if (rotation !== 0) {
85
- ctx.rotate(-rotation * (Math.PI / 180))
86
- }
87
-
88
- ctx.font = `${fontSize}px sans-serif`
89
- ctx.fillStyle = color
90
- ctx.textAlign = mapAnchorAlignment(text.anchor_alignment)
91
- ctx.fillText(text.text, 0, 0)
92
- ctx.restore()
66
+ // Use @tscircuit/alphabet to draw text (font-independent, stroke-based rendering)
67
+ drawText({
68
+ ctx,
69
+ text: text.text,
70
+ x: text.anchor_position.x,
71
+ y: text.anchor_position.y,
72
+ fontSize,
73
+ color,
74
+ realToCanvasMat,
75
+ anchorAlignment: text.anchor_alignment ?? "center",
76
+ rotation,
77
+ })
93
78
  }
94
79
 
95
80
  export function drawPcbSilkscreenRect(
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.19",
4
+ "version": "0.0.20",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -12,7 +12,7 @@
12
12
  "@biomejs/biome": "^2.3.8",
13
13
  "@napi-rs/canvas": "^0.1.84",
14
14
  "@resvg/resvg-js": "^2.6.2",
15
- "@tscircuit/alphabet": "^0.0.9",
15
+ "@tscircuit/alphabet": "^0.0.17",
16
16
  "@tscircuit/circuit-json-util": "^0.0.73",
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
@@ -5,7 +5,7 @@ import usbcFlashlightCircuit from "./usb-c-flashlight.json"
5
5
 
6
6
  const circuitElements = usbcFlashlightCircuit as AnyCircuitElement[]
7
7
 
8
- test.skip("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
8
+ test("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
9
9
  const stackedPng = await getStackedPngSvgComparison(circuitElements, {
10
10
  width: 400,
11
11
  height: 800,
@@ -1,7 +1,9 @@
1
1
  import { createCanvas, loadImage } from "@napi-rs/canvas"
2
- import { Resvg } from "@resvg/resvg-js"
2
+ import { Resvg, type ResvgRenderOptions } from "@resvg/resvg-js"
3
3
  import * as fs from "node:fs"
4
+ import * as os from "node:os"
4
5
  import * as path from "node:path"
6
+ import tscircuitFont from "@tscircuit/alphabet/base64font"
5
7
 
6
8
  // Pre-generated label PNGs for common labels
7
9
  const labelPngCache: Map<string, Buffer> = new Map()
@@ -76,7 +78,44 @@ export const stackPngsVertically = async (
76
78
  return canvas.toBuffer("image/png")
77
79
  }
78
80
 
79
- export const svgToPng = (svg: string): Buffer => {
80
- const resvg = new Resvg(svg)
81
- return resvg.render().asPng()
81
+ export const svgToPng = (svgString: string): Buffer => {
82
+ const fontBuffer = Buffer.from(tscircuitFont, "base64")
83
+
84
+ let tempFontPath: string | undefined
85
+ let cleanupFn: (() => void) | undefined
86
+
87
+ try {
88
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "resvg-font-"))
89
+ tempFontPath = path.join(tempDir, "tscircuit-font.ttf")
90
+ fs.writeFileSync(tempFontPath, fontBuffer)
91
+
92
+ cleanupFn = () => {
93
+ try {
94
+ fs.unlinkSync(tempFontPath!)
95
+ } catch {
96
+ // Ignore errors during cleanup
97
+ }
98
+ }
99
+
100
+ const opts: ResvgRenderOptions = {
101
+ font: {
102
+ fontFiles: [tempFontPath],
103
+ loadSystemFonts: false,
104
+ defaultFontFamily: "TscircuitAlphabet",
105
+ monospaceFamily: "TscircuitAlphabet",
106
+ sansSerifFamily: "TscircuitAlphabet",
107
+ },
108
+ }
109
+
110
+ const resvg = new Resvg(svgString, opts)
111
+ const pngData = resvg.render()
112
+ const pngBuffer = pngData.asPng()
113
+
114
+ return Buffer.from(pngBuffer)
115
+ } finally {
116
+ // Clean up temporary font file
117
+ if (cleanupFn) {
118
+ cleanupFn()
119
+ }
120
+ }
82
121
  }