circuit-to-canvas 0.0.20 → 0.0.21

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,141 +647,36 @@ function drawPcbBoard(params) {
647
647
  }
648
648
  }
649
649
 
650
- // lib/drawer/shapes/text/text.ts
651
- import { lineAlphabet } from "@tscircuit/alphabet";
652
- import { applyToPoint as applyToPoint8 } from "transformation-matrix";
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
- };
679
- }
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 };
704
- }
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);
752
- ctx.save();
753
- ctx.translate(canvasX, canvasY);
754
- if (rotation !== 0) {
755
- ctx.rotate(-rotation * (Math.PI / 180));
756
- }
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);
762
- ctx.restore();
763
- }
764
-
765
650
  // lib/drawer/elements/pcb-silkscreen.ts
651
+ import { applyToPoint as applyToPoint8 } from "transformation-matrix";
766
652
  function layerToSilkscreenColor(layer, colorMap) {
767
653
  return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
768
654
  }
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";
660
+ }
769
661
  function drawPcbSilkscreenText(params) {
770
662
  const { ctx, text, realToCanvasMat, colorMap } = params;
771
663
  const color = layerToSilkscreenColor(text.layer, colorMap);
772
- const fontSize = text.font_size ?? 1;
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);
773
669
  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
- });
670
+ ctx.save();
671
+ ctx.translate(x, y);
672
+ if (rotation !== 0) {
673
+ ctx.rotate(-rotation * (Math.PI / 180));
674
+ }
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);
679
+ ctx.restore();
785
680
  }
786
681
  function drawPcbSilkscreenRect(params) {
787
682
  const { ctx, rect, realToCanvasMat, colorMap } = params;
@@ -934,12 +829,129 @@ function drawPcbCopperPour(params) {
934
829
  }
935
830
 
936
831
  // 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";
937
836
  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
938
950
  var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
939
951
  function layerToCopperColor(layer, colorMap) {
940
952
  return colorMap.copper[layer] ?? colorMap.copper.top;
941
953
  }
942
- function mapAnchorAlignment(alignment) {
954
+ function mapAnchorAlignment2(alignment) {
943
955
  if (!alignment) return "center";
944
956
  if (alignment.includes("left")) return "center_left";
945
957
  if (alignment.includes("right")) return "center_right";
@@ -949,7 +961,7 @@ function drawPcbCopperText(params) {
949
961
  const { ctx, text, realToCanvasMat, colorMap } = params;
950
962
  const content = text.text ?? "";
951
963
  if (!content) return;
952
- const [x, y] = applyToPoint10(realToCanvasMat, [
964
+ const [x, y] = applyToPoint11(realToCanvasMat, [
953
965
  text.anchor_position.x,
954
966
  text.anchor_position.y
955
967
  ]);
@@ -963,7 +975,7 @@ function drawPcbCopperText(params) {
963
975
  const textColor = layerToCopperColor(text.layer, colorMap);
964
976
  const layout = getAlphabetLayout(content, fontSize);
965
977
  const totalWidth = layout.width + layout.strokeWidth;
966
- const alignment = mapAnchorAlignment(text.anchor_alignment);
978
+ const alignment = mapAnchorAlignment2(text.anchor_alignment);
967
979
  const startPos = getTextStartPosition(alignment, layout);
968
980
  const startX = startPos.x;
969
981
  const startY = startPos.y;
@@ -1133,7 +1145,7 @@ function drawPcbNoteText(params) {
1133
1145
  }
1134
1146
 
1135
1147
  // lib/drawer/elements/pcb-note-dimension.ts
1136
- import { applyToPoint as applyToPoint11 } from "transformation-matrix";
1148
+ import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1137
1149
 
1138
1150
  // lib/drawer/shapes/arrow.ts
1139
1151
  function drawArrow(params) {
@@ -1218,11 +1230,11 @@ function drawPcbNoteDimension(params) {
1218
1230
  stroke: color,
1219
1231
  realToCanvasMat
1220
1232
  });
1221
- const [canvasFromX, canvasFromY] = applyToPoint11(realToCanvasMat, [
1233
+ const [canvasFromX, canvasFromY] = applyToPoint12(realToCanvasMat, [
1222
1234
  fromX,
1223
1235
  fromY
1224
1236
  ]);
1225
- const [canvasToX, canvasToY] = applyToPoint11(realToCanvasMat, [toX, toY]);
1237
+ const [canvasToX, canvasToY] = applyToPoint12(realToCanvasMat, [toX, toY]);
1226
1238
  const canvasDx = canvasToX - canvasFromX;
1227
1239
  const canvasDy = canvasToY - canvasFromY;
1228
1240
  const lineAngle = Math.atan2(canvasDy, canvasDx);
@@ -1283,15 +1295,15 @@ function drawPcbNoteDimension(params) {
1283
1295
  }
1284
1296
 
1285
1297
  // lib/drawer/elements/pcb-note-line.ts
1286
- import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1298
+ import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1287
1299
  function drawPcbNoteLine(params) {
1288
1300
  const { ctx, line, realToCanvasMat, colorMap } = params;
1289
1301
  const defaultColor = "rgb(89, 148, 220)";
1290
1302
  const color = line.color ?? defaultColor;
1291
1303
  const strokeWidth = line.stroke_width ?? 0.1;
1292
1304
  const isDashed = line.is_dashed ?? false;
1293
- const [x1, y1] = applyToPoint12(realToCanvasMat, [line.x1, line.y1]);
1294
- const [x2, y2] = applyToPoint12(realToCanvasMat, [line.x2, line.y2]);
1305
+ const [x1, y1] = applyToPoint13(realToCanvasMat, [line.x1, line.y1]);
1306
+ const [x2, y2] = applyToPoint13(realToCanvasMat, [line.x2, line.y2]);
1295
1307
  const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1296
1308
  ctx.save();
1297
1309
  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"
9
10
  import type { PcbColorMap, CanvasContext } from "../types"
10
11
  import { drawRect } from "../shapes/rect"
11
12
  import { drawCircle } from "../shapes/circle"
12
13
  import { drawLine } from "../shapes/line"
13
14
  import { drawPath } from "../shapes/path"
14
- import { drawText } from "../shapes/text"
15
15
 
16
16
  export interface DrawPcbSilkscreenTextParams {
17
17
  ctx: CanvasContext
@@ -54,27 +54,42 @@ 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
+
57
66
  export function drawPcbSilkscreenText(
58
67
  params: DrawPcbSilkscreenTextParams,
59
68
  ): void {
60
69
  const { ctx, text, realToCanvasMat, colorMap } = params
61
70
 
62
71
  const color = layerToSilkscreenColor(text.layer, colorMap)
63
- const fontSize = text.font_size ?? 1
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)
64
78
  const rotation = text.ccw_rotation ?? 0
65
79
 
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
- })
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()
78
93
  }
79
94
 
80
95
  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.20",
4
+ "version": "0.0.21",
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.17",
15
+ "@tscircuit/alphabet": "^0.0.9",
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("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
8
+ test.skip("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,9 +1,7 @@
1
1
  import { createCanvas, loadImage } from "@napi-rs/canvas"
2
- import { Resvg, type ResvgRenderOptions } from "@resvg/resvg-js"
2
+ import { Resvg } from "@resvg/resvg-js"
3
3
  import * as fs from "node:fs"
4
- import * as os from "node:os"
5
4
  import * as path from "node:path"
6
- import tscircuitFont from "@tscircuit/alphabet/base64font"
7
5
 
8
6
  // Pre-generated label PNGs for common labels
9
7
  const labelPngCache: Map<string, Buffer> = new Map()
@@ -78,44 +76,7 @@ export const stackPngsVertically = async (
78
76
  return canvas.toBuffer("image/png")
79
77
  }
80
78
 
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
- }
79
+ export const svgToPng = (svg: string): Buffer => {
80
+ const resvg = new Resvg(svg)
81
+ return resvg.render().asPng()
121
82
  }