circuit-to-canvas 0.0.21 → 0.0.23

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
@@ -300,34 +300,38 @@ interface DrawPcbSilkscreenTextParams {
300
300
  realToCanvasMat: Matrix;
301
301
  colorMap: PcbColorMap;
302
302
  }
303
+ declare function drawPcbSilkscreenText(params: DrawPcbSilkscreenTextParams): void;
304
+
303
305
  interface DrawPcbSilkscreenRectParams {
304
306
  ctx: CanvasContext;
305
307
  rect: PcbSilkscreenRect;
306
308
  realToCanvasMat: Matrix;
307
309
  colorMap: PcbColorMap;
308
310
  }
311
+ declare function drawPcbSilkscreenRect(params: DrawPcbSilkscreenRectParams): void;
312
+
309
313
  interface DrawPcbSilkscreenCircleParams {
310
314
  ctx: CanvasContext;
311
315
  circle: PcbSilkscreenCircle;
312
316
  realToCanvasMat: Matrix;
313
317
  colorMap: PcbColorMap;
314
318
  }
319
+ declare function drawPcbSilkscreenCircle(params: DrawPcbSilkscreenCircleParams): void;
320
+
315
321
  interface DrawPcbSilkscreenLineParams {
316
322
  ctx: CanvasContext;
317
323
  line: PcbSilkscreenLine;
318
324
  realToCanvasMat: Matrix;
319
325
  colorMap: PcbColorMap;
320
326
  }
327
+ declare function drawPcbSilkscreenLine(params: DrawPcbSilkscreenLineParams): void;
328
+
321
329
  interface DrawPcbSilkscreenPathParams {
322
330
  ctx: CanvasContext;
323
331
  path: PcbSilkscreenPath;
324
332
  realToCanvasMat: Matrix;
325
333
  colorMap: PcbColorMap;
326
334
  }
327
- declare function drawPcbSilkscreenText(params: DrawPcbSilkscreenTextParams): void;
328
- declare function drawPcbSilkscreenRect(params: DrawPcbSilkscreenRectParams): void;
329
- declare function drawPcbSilkscreenCircle(params: DrawPcbSilkscreenCircleParams): void;
330
- declare function drawPcbSilkscreenLine(params: DrawPcbSilkscreenLineParams): void;
331
335
  declare function drawPcbSilkscreenPath(params: DrawPcbSilkscreenPathParams): void;
332
336
 
333
337
  interface DrawPcbCutoutParams {
package/dist/index.js CHANGED
@@ -647,40 +647,170 @@ function drawPcbBoard(params) {
647
647
  }
648
648
  }
649
649
 
650
- // lib/drawer/elements/pcb-silkscreen.ts
650
+ // lib/drawer/elements/pcb-silkscreen-text.ts
651
+ import { applyToPoint as applyToPoint9 } from "transformation-matrix";
652
+
653
+ // lib/drawer/shapes/text/text.ts
654
+ import { lineAlphabet } from "@tscircuit/alphabet";
651
655
  import { applyToPoint as applyToPoint8 } from "transformation-matrix";
656
+
657
+ // lib/drawer/shapes/text/getAlphabetLayout.ts
658
+ var GLYPH_WIDTH_RATIO = 0.62;
659
+ var LETTER_SPACING_RATIO = 0.3;
660
+ var SPACE_WIDTH_RATIO = 1;
661
+ var STROKE_WIDTH_RATIO = 0.13;
662
+ function getAlphabetLayout(text, fontSize) {
663
+ const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
664
+ const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
665
+ const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
666
+ const characters = Array.from(text);
667
+ let width = 0;
668
+ characters.forEach((char, index) => {
669
+ const advance = char === " " ? spaceWidth : glyphWidth;
670
+ width += advance;
671
+ if (index < characters.length - 1) width += letterSpacing;
672
+ });
673
+ const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
674
+ return {
675
+ width,
676
+ height: fontSize,
677
+ glyphWidth,
678
+ letterSpacing,
679
+ spaceWidth,
680
+ strokeWidth
681
+ };
682
+ }
683
+
684
+ // lib/drawer/shapes/text/getTextStartPosition.ts
685
+ function getTextStartPosition(alignment, layout) {
686
+ const totalWidth = layout.width + layout.strokeWidth;
687
+ const totalHeight = layout.height + layout.strokeWidth;
688
+ let x = 0;
689
+ let y = 0;
690
+ if (alignment === "center") {
691
+ x = -totalWidth / 2;
692
+ } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
693
+ x = 0;
694
+ } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
695
+ x = -totalWidth;
696
+ }
697
+ if (alignment === "center") {
698
+ y = -totalHeight / 2;
699
+ } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
700
+ y = 0;
701
+ } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
702
+ y = -totalHeight;
703
+ } else {
704
+ y = 0;
705
+ }
706
+ return { x, y };
707
+ }
708
+
709
+ // lib/drawer/shapes/text/text.ts
710
+ var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
711
+ function strokeAlphabetText(ctx, text, layout, startX, startY) {
712
+ const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
713
+ const topY = startY;
714
+ const characters = Array.from(text);
715
+ let cursor = startX + strokeWidth / 2;
716
+ characters.forEach((char, index) => {
717
+ const lines = getGlyphLines(char);
718
+ const advance = char === " " ? spaceWidth : glyphWidth;
719
+ if (lines?.length) {
720
+ ctx.beginPath();
721
+ for (const line of lines) {
722
+ const x1 = cursor + line.x1 * glyphWidth;
723
+ const y1 = topY + (1 - line.y1) * height;
724
+ const x2 = cursor + line.x2 * glyphWidth;
725
+ const y2 = topY + (1 - line.y2) * height;
726
+ ctx.moveTo(x1, y1);
727
+ ctx.lineTo(x2, y2);
728
+ }
729
+ ctx.stroke();
730
+ }
731
+ cursor += advance;
732
+ if (index < characters.length - 1) {
733
+ cursor += letterSpacing;
734
+ }
735
+ });
736
+ }
737
+ function drawText(params) {
738
+ const {
739
+ ctx,
740
+ text,
741
+ x,
742
+ y,
743
+ fontSize,
744
+ color,
745
+ realToCanvasMat,
746
+ anchorAlignment,
747
+ rotation = 0
748
+ } = params;
749
+ if (!text) return;
750
+ const [canvasX, canvasY] = applyToPoint8(realToCanvasMat, [x, y]);
751
+ const scale2 = Math.abs(realToCanvasMat.a);
752
+ const scaledFontSize = fontSize * scale2;
753
+ const layout = getAlphabetLayout(text, scaledFontSize);
754
+ const startPos = getTextStartPosition(anchorAlignment, layout);
755
+ ctx.save();
756
+ ctx.translate(canvasX, canvasY);
757
+ if (rotation !== 0) {
758
+ ctx.rotate(-rotation * (Math.PI / 180));
759
+ }
760
+ ctx.lineWidth = layout.strokeWidth;
761
+ ctx.lineCap = "round";
762
+ ctx.lineJoin = "round";
763
+ ctx.strokeStyle = color;
764
+ strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
765
+ ctx.restore();
766
+ }
767
+
768
+ // lib/drawer/elements/pcb-silkscreen-text.ts
652
769
  function layerToSilkscreenColor(layer, colorMap) {
653
770
  return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
654
771
  }
655
772
  function mapAnchorAlignment(alignment) {
656
773
  if (!alignment) return "center";
657
- if (alignment.includes("left")) return "left";
658
- if (alignment.includes("right")) return "right";
659
- return "center";
774
+ return alignment;
660
775
  }
661
776
  function drawPcbSilkscreenText(params) {
662
777
  const { ctx, text, realToCanvasMat, colorMap } = params;
778
+ const content = text.text ?? "";
779
+ if (!content) return;
663
780
  const color = layerToSilkscreenColor(text.layer, colorMap);
664
- const [x, y] = applyToPoint8(realToCanvasMat, [
781
+ const [x, y] = applyToPoint9(realToCanvasMat, [
665
782
  text.anchor_position.x,
666
783
  text.anchor_position.y
667
784
  ]);
668
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a);
785
+ const scale2 = Math.abs(realToCanvasMat.a);
786
+ const fontSize = (text.font_size ?? 1) * scale2;
669
787
  const rotation = text.ccw_rotation ?? 0;
788
+ const layout = getAlphabetLayout(content, fontSize);
789
+ const alignment = mapAnchorAlignment(text.anchor_alignment);
790
+ const startPos = getTextStartPosition(alignment, layout);
670
791
  ctx.save();
671
792
  ctx.translate(x, y);
672
793
  if (rotation !== 0) {
673
794
  ctx.rotate(-rotation * (Math.PI / 180));
674
795
  }
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);
796
+ if (text.layer === "bottom") {
797
+ ctx.scale(-1, 1);
798
+ }
799
+ ctx.lineWidth = layout.strokeWidth;
800
+ ctx.lineCap = "round";
801
+ ctx.lineJoin = "round";
802
+ ctx.strokeStyle = color;
803
+ strokeAlphabetText(ctx, content, layout, startPos.x, startPos.y);
679
804
  ctx.restore();
680
805
  }
806
+
807
+ // lib/drawer/elements/pcb-silkscreen-rect.ts
808
+ function layerToSilkscreenColor2(layer, colorMap) {
809
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
810
+ }
681
811
  function drawPcbSilkscreenRect(params) {
682
812
  const { ctx, rect, realToCanvasMat, colorMap } = params;
683
- const color = layerToSilkscreenColor(rect.layer, colorMap);
813
+ const color = layerToSilkscreenColor2(rect.layer, colorMap);
684
814
  drawRect({
685
815
  ctx,
686
816
  center: rect.center,
@@ -690,9 +820,14 @@ function drawPcbSilkscreenRect(params) {
690
820
  realToCanvasMat
691
821
  });
692
822
  }
823
+
824
+ // lib/drawer/elements/pcb-silkscreen-circle.ts
825
+ function layerToSilkscreenColor3(layer, colorMap) {
826
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
827
+ }
693
828
  function drawPcbSilkscreenCircle(params) {
694
829
  const { ctx, circle, realToCanvasMat, colorMap } = params;
695
- const color = layerToSilkscreenColor(circle.layer, colorMap);
830
+ const color = layerToSilkscreenColor3(circle.layer, colorMap);
696
831
  drawCircle({
697
832
  ctx,
698
833
  center: circle.center,
@@ -701,9 +836,14 @@ function drawPcbSilkscreenCircle(params) {
701
836
  realToCanvasMat
702
837
  });
703
838
  }
839
+
840
+ // lib/drawer/elements/pcb-silkscreen-line.ts
841
+ function layerToSilkscreenColor4(layer, colorMap) {
842
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
843
+ }
704
844
  function drawPcbSilkscreenLine(params) {
705
845
  const { ctx, line, realToCanvasMat, colorMap } = params;
706
- const color = layerToSilkscreenColor(line.layer, colorMap);
846
+ const color = layerToSilkscreenColor4(line.layer, colorMap);
707
847
  drawLine({
708
848
  ctx,
709
849
  start: { x: line.x1, y: line.y1 },
@@ -713,9 +853,14 @@ function drawPcbSilkscreenLine(params) {
713
853
  realToCanvasMat
714
854
  });
715
855
  }
856
+
857
+ // lib/drawer/elements/pcb-silkscreen-path.ts
858
+ function layerToSilkscreenColor5(layer, colorMap) {
859
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
860
+ }
716
861
  function drawPcbSilkscreenPath(params) {
717
862
  const { ctx, path, realToCanvasMat, colorMap } = params;
718
- const color = layerToSilkscreenColor(path.layer, colorMap);
863
+ const color = layerToSilkscreenColor5(path.layer, colorMap);
719
864
  if (!path.route || path.route.length < 2) return;
720
865
  for (let i = 0; i < path.route.length - 1; i++) {
721
866
  const start = path.route[i];
@@ -771,7 +916,7 @@ function drawPcbCutout(params) {
771
916
  }
772
917
 
773
918
  // lib/drawer/elements/pcb-copper-pour.ts
774
- import { applyToPoint as applyToPoint9 } from "transformation-matrix";
919
+ import { applyToPoint as applyToPoint10 } from "transformation-matrix";
775
920
  function layerToColor3(layer, colorMap) {
776
921
  return colorMap.copper[layer] ?? colorMap.copper.top;
777
922
  }
@@ -780,7 +925,7 @@ function drawPcbCopperPour(params) {
780
925
  const color = layerToColor3(pour.layer, colorMap);
781
926
  ctx.save();
782
927
  if (pour.shape === "rect") {
783
- const [cx, cy] = applyToPoint9(realToCanvasMat, [
928
+ const [cx, cy] = applyToPoint10(realToCanvasMat, [
784
929
  pour.center.x,
785
930
  pour.center.y
786
931
  ]);
@@ -801,7 +946,7 @@ function drawPcbCopperPour(params) {
801
946
  if (pour.shape === "polygon") {
802
947
  if (pour.points && pour.points.length >= 3) {
803
948
  const canvasPoints = pour.points.map(
804
- (p) => applyToPoint9(realToCanvasMat, [p.x, p.y])
949
+ (p) => applyToPoint10(realToCanvasMat, [p.x, p.y])
805
950
  );
806
951
  const firstPoint = canvasPoints[0];
807
952
  if (!firstPoint) {
@@ -830,123 +975,6 @@ function drawPcbCopperPour(params) {
830
975
 
831
976
  // lib/drawer/elements/pcb-copper-text.ts
832
977
  import { applyToPoint as applyToPoint11 } from "transformation-matrix";
833
-
834
- // lib/drawer/shapes/text/text.ts
835
- import { lineAlphabet } from "@tscircuit/alphabet";
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
950
978
  var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
951
979
  function layerToCopperColor(layer, colorMap) {
952
980
  return colorMap.copper[layer] ?? colorMap.copper.top;
@@ -38,13 +38,11 @@ import { drawPcbHole } from "./elements/pcb-hole"
38
38
  import { drawPcbSmtPad } from "./elements/pcb-smtpad"
39
39
  import { drawPcbTrace } from "./elements/pcb-trace"
40
40
  import { drawPcbBoard } from "./elements/pcb-board"
41
- import {
42
- drawPcbSilkscreenText,
43
- drawPcbSilkscreenRect,
44
- drawPcbSilkscreenCircle,
45
- drawPcbSilkscreenLine,
46
- drawPcbSilkscreenPath,
47
- } from "./elements/pcb-silkscreen"
41
+ import { drawPcbSilkscreenText } from "./elements/pcb-silkscreen-text"
42
+ import { drawPcbSilkscreenRect } from "./elements/pcb-silkscreen-rect"
43
+ import { drawPcbSilkscreenCircle } from "./elements/pcb-silkscreen-circle"
44
+ import { drawPcbSilkscreenLine } from "./elements/pcb-silkscreen-line"
45
+ import { drawPcbSilkscreenPath } from "./elements/pcb-silkscreen-path"
48
46
  import { drawPcbCutout } from "./elements/pcb-cutout"
49
47
  import { drawPcbCopperPour } from "./elements/pcb-copper-pour"
50
48
  import { drawPcbCopperText } from "./elements/pcb-copper-text"
@@ -15,16 +15,28 @@ export { drawPcbBoard, type DrawPcbBoardParams } from "./pcb-board"
15
15
 
16
16
  export {
17
17
  drawPcbSilkscreenText,
18
- drawPcbSilkscreenRect,
19
- drawPcbSilkscreenCircle,
20
- drawPcbSilkscreenLine,
21
- drawPcbSilkscreenPath,
22
18
  type DrawPcbSilkscreenTextParams,
19
+ } from "./pcb-silkscreen-text"
20
+
21
+ export {
22
+ drawPcbSilkscreenRect,
23
23
  type DrawPcbSilkscreenRectParams,
24
+ } from "./pcb-silkscreen-rect"
25
+
26
+ export {
27
+ drawPcbSilkscreenCircle,
24
28
  type DrawPcbSilkscreenCircleParams,
29
+ } from "./pcb-silkscreen-circle"
30
+
31
+ export {
32
+ drawPcbSilkscreenLine,
25
33
  type DrawPcbSilkscreenLineParams,
34
+ } from "./pcb-silkscreen-line"
35
+
36
+ export {
37
+ drawPcbSilkscreenPath,
26
38
  type DrawPcbSilkscreenPathParams,
27
- } from "./pcb-silkscreen"
39
+ } from "./pcb-silkscreen-path"
28
40
 
29
41
  export { drawPcbCutout, type DrawPcbCutoutParams } from "./pcb-cutout"
30
42
 
@@ -0,0 +1,33 @@
1
+ import type { PcbSilkscreenCircle } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import type { PcbColorMap, CanvasContext } from "../types"
4
+ import { drawCircle } from "../shapes/circle"
5
+
6
+ export interface DrawPcbSilkscreenCircleParams {
7
+ ctx: CanvasContext
8
+ circle: PcbSilkscreenCircle
9
+ realToCanvasMat: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
14
+ return layer === "bottom"
15
+ ? colorMap.silkscreen.bottom
16
+ : colorMap.silkscreen.top
17
+ }
18
+
19
+ export function drawPcbSilkscreenCircle(
20
+ params: DrawPcbSilkscreenCircleParams,
21
+ ): void {
22
+ const { ctx, circle, realToCanvasMat, colorMap } = params
23
+
24
+ const color = layerToSilkscreenColor(circle.layer, colorMap)
25
+
26
+ drawCircle({
27
+ ctx,
28
+ center: circle.center,
29
+ radius: circle.radius,
30
+ fill: color,
31
+ realToCanvasMat,
32
+ })
33
+ }
@@ -0,0 +1,34 @@
1
+ import type { PcbSilkscreenLine } 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 DrawPcbSilkscreenLineParams {
7
+ ctx: CanvasContext
8
+ line: PcbSilkscreenLine
9
+ realToCanvasMat: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
14
+ return layer === "bottom"
15
+ ? colorMap.silkscreen.bottom
16
+ : colorMap.silkscreen.top
17
+ }
18
+
19
+ export function drawPcbSilkscreenLine(
20
+ params: DrawPcbSilkscreenLineParams,
21
+ ): void {
22
+ const { ctx, line, realToCanvasMat, colorMap } = params
23
+
24
+ const color = layerToSilkscreenColor(line.layer, colorMap)
25
+
26
+ drawLine({
27
+ ctx,
28
+ start: { x: line.x1, y: line.y1 },
29
+ end: { x: line.x2, y: line.y2 },
30
+ strokeWidth: line.stroke_width ?? 0.1,
31
+ stroke: color,
32
+ realToCanvasMat,
33
+ })
34
+ }
@@ -0,0 +1,44 @@
1
+ import type { PcbSilkscreenPath } 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 DrawPcbSilkscreenPathParams {
7
+ ctx: CanvasContext
8
+ path: PcbSilkscreenPath
9
+ realToCanvasMat: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
14
+ return layer === "bottom"
15
+ ? colorMap.silkscreen.bottom
16
+ : colorMap.silkscreen.top
17
+ }
18
+
19
+ export function drawPcbSilkscreenPath(
20
+ params: DrawPcbSilkscreenPathParams,
21
+ ): void {
22
+ const { ctx, path, realToCanvasMat, colorMap } = params
23
+
24
+ const color = layerToSilkscreenColor(path.layer, colorMap)
25
+
26
+ if (!path.route || path.route.length < 2) return
27
+
28
+ // Draw each segment of the path
29
+ for (let i = 0; i < path.route.length - 1; i++) {
30
+ const start = path.route[i]
31
+ const end = path.route[i + 1]
32
+
33
+ if (!start || !end) continue
34
+
35
+ drawLine({
36
+ ctx,
37
+ start: { x: start.x, y: start.y },
38
+ end: { x: end.x, y: end.y },
39
+ strokeWidth: path.stroke_width ?? 0.1,
40
+ stroke: color,
41
+ realToCanvasMat,
42
+ })
43
+ }
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { PcbSilkscreenRect } 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 DrawPcbSilkscreenRectParams {
7
+ ctx: CanvasContext
8
+ rect: PcbSilkscreenRect
9
+ realToCanvasMat: Matrix
10
+ colorMap: PcbColorMap
11
+ }
12
+
13
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
14
+ return layer === "bottom"
15
+ ? colorMap.silkscreen.bottom
16
+ : colorMap.silkscreen.top
17
+ }
18
+
19
+ export function drawPcbSilkscreenRect(
20
+ params: DrawPcbSilkscreenRectParams,
21
+ ): void {
22
+ const { ctx, rect, realToCanvasMat, colorMap } = params
23
+
24
+ const color = layerToSilkscreenColor(rect.layer, colorMap)
25
+
26
+ drawRect({
27
+ ctx,
28
+ center: rect.center,
29
+ width: rect.width,
30
+ height: rect.height,
31
+ fill: color,
32
+ realToCanvasMat,
33
+ })
34
+ }
@@ -0,0 +1,71 @@
1
+ import type { PcbSilkscreenText } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import { applyToPoint } from "transformation-matrix"
4
+ import type { PcbColorMap, CanvasContext } from "../types"
5
+ import {
6
+ getAlphabetLayout,
7
+ strokeAlphabetText,
8
+ getTextStartPosition,
9
+ type AnchorAlignment,
10
+ } from "../shapes/text"
11
+
12
+ export interface DrawPcbSilkscreenTextParams {
13
+ ctx: CanvasContext
14
+ text: PcbSilkscreenText
15
+ realToCanvasMat: Matrix
16
+ colorMap: PcbColorMap
17
+ }
18
+
19
+ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
20
+ return layer === "bottom"
21
+ ? colorMap.silkscreen.bottom
22
+ : colorMap.silkscreen.top
23
+ }
24
+
25
+ function mapAnchorAlignment(alignment?: string): AnchorAlignment {
26
+ if (!alignment) return "center"
27
+ return alignment as AnchorAlignment
28
+ }
29
+
30
+ export function drawPcbSilkscreenText(
31
+ params: DrawPcbSilkscreenTextParams,
32
+ ): void {
33
+ const { ctx, text, realToCanvasMat, colorMap } = params
34
+
35
+ const content = text.text ?? ""
36
+ if (!content) return
37
+
38
+ const color = layerToSilkscreenColor(text.layer, colorMap)
39
+ const [x, y] = applyToPoint(realToCanvasMat, [
40
+ text.anchor_position.x,
41
+ text.anchor_position.y,
42
+ ])
43
+ const scale = Math.abs(realToCanvasMat.a)
44
+ const fontSize = (text.font_size ?? 1) * scale
45
+ const rotation = text.ccw_rotation ?? 0
46
+
47
+ const layout = getAlphabetLayout(content, fontSize)
48
+ const alignment = mapAnchorAlignment(text.anchor_alignment)
49
+ const startPos = getTextStartPosition(alignment, layout)
50
+
51
+ ctx.save()
52
+ ctx.translate(x, y)
53
+
54
+ // Apply rotation (CCW rotation in degrees)
55
+ if (rotation !== 0) {
56
+ ctx.rotate(-rotation * (Math.PI / 180))
57
+ }
58
+
59
+ if (text.layer === "bottom") {
60
+ ctx.scale(-1, 1)
61
+ }
62
+
63
+ ctx.lineWidth = layout.strokeWidth
64
+ ctx.lineCap = "round"
65
+ ctx.lineJoin = "round"
66
+ ctx.strokeStyle = color
67
+
68
+ strokeAlphabetText(ctx, content, layout, startPos.x, startPos.y)
69
+
70
+ ctx.restore()
71
+ }
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.21",
4
+ "version": "0.0.23",
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,
@@ -64,6 +64,116 @@ test("draw silkscreen text bottom layer", async () => {
64
64
  )
65
65
  })
66
66
 
67
+ test("draw silkscreen text with rotation", async () => {
68
+ const canvas = createCanvas(100, 100)
69
+ const ctx = canvas.getContext("2d")
70
+ const drawer = new CircuitToCanvasDrawer(ctx)
71
+
72
+ ctx.fillStyle = "#1a1a1a"
73
+ ctx.fillRect(0, 0, 100, 100)
74
+
75
+ const text: PcbSilkscreenText = {
76
+ type: "pcb_silkscreen_text",
77
+ pcb_silkscreen_text_id: "text1",
78
+ pcb_component_id: "component1",
79
+ layer: "top",
80
+ text: "ROT45",
81
+ anchor_position: { x: 50, y: 50 },
82
+ anchor_alignment: "center",
83
+ font: "tscircuit2024",
84
+ font_size: 8,
85
+ ccw_rotation: 45,
86
+ }
87
+
88
+ drawer.drawElements([text])
89
+
90
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
91
+ import.meta.path,
92
+ "silkscreen-text-rotated",
93
+ )
94
+ })
95
+
96
+ test("draw silkscreen text bottom layer with rotation - tests transform order", async () => {
97
+ const canvas = createCanvas(150, 150)
98
+ const ctx = canvas.getContext("2d")
99
+ const drawer = new CircuitToCanvasDrawer(ctx)
100
+
101
+ ctx.fillStyle = "#1a1a1a"
102
+ ctx.fillRect(0, 0, 150, 150)
103
+
104
+ // This test verifies the transform order (translate -> rotate -> scale) is correct
105
+ // by testing bottom layer text with various rotations
106
+ const texts: PcbSilkscreenText[] = [
107
+ {
108
+ type: "pcb_silkscreen_text",
109
+ pcb_silkscreen_text_id: "text1",
110
+ pcb_component_id: "component1",
111
+ layer: "bottom",
112
+ text: "0",
113
+ anchor_position: { x: 75, y: 30 },
114
+ anchor_alignment: "center",
115
+ font: "tscircuit2024",
116
+ font_size: 6,
117
+ ccw_rotation: 0,
118
+ },
119
+ {
120
+ type: "pcb_silkscreen_text",
121
+ pcb_silkscreen_text_id: "text2",
122
+ pcb_component_id: "component1",
123
+ layer: "bottom",
124
+ text: "90",
125
+ anchor_position: { x: 120, y: 75 },
126
+ anchor_alignment: "center",
127
+ font: "tscircuit2024",
128
+ font_size: 6,
129
+ ccw_rotation: 90,
130
+ },
131
+ {
132
+ type: "pcb_silkscreen_text",
133
+ pcb_silkscreen_text_id: "text3",
134
+ pcb_component_id: "component1",
135
+ layer: "bottom",
136
+ text: "180",
137
+ anchor_position: { x: 75, y: 120 },
138
+ anchor_alignment: "center",
139
+ font: "tscircuit2024",
140
+ font_size: 6,
141
+ ccw_rotation: 180,
142
+ },
143
+ {
144
+ type: "pcb_silkscreen_text",
145
+ pcb_silkscreen_text_id: "text4",
146
+ pcb_component_id: "component1",
147
+ layer: "bottom",
148
+ text: "270",
149
+ anchor_position: { x: 30, y: 75 },
150
+ anchor_alignment: "center",
151
+ font: "tscircuit2024",
152
+ font_size: 6,
153
+ ccw_rotation: 270,
154
+ },
155
+ {
156
+ type: "pcb_silkscreen_text",
157
+ pcb_silkscreen_text_id: "text5",
158
+ pcb_component_id: "component1",
159
+ layer: "bottom",
160
+ text: "BTM",
161
+ anchor_position: { x: 75, y: 75 },
162
+ anchor_alignment: "center",
163
+ font: "tscircuit2024",
164
+ font_size: 8,
165
+ ccw_rotation: 45,
166
+ },
167
+ ]
168
+
169
+ drawer.drawElements(texts)
170
+
171
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
172
+ import.meta.path,
173
+ "silkscreen-text-bottom-rotated",
174
+ )
175
+ })
176
+
67
177
  test("draw silkscreen rect", async () => {
68
178
  const canvas = createCanvas(100, 100)
69
179
  const ctx = canvas.getContext("2d")
@@ -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
  }
@@ -1,170 +0,0 @@
1
- import type {
2
- PcbSilkscreenText,
3
- PcbSilkscreenRect,
4
- PcbSilkscreenCircle,
5
- PcbSilkscreenLine,
6
- PcbSilkscreenPath,
7
- } from "circuit-json"
8
- import type { Matrix } from "transformation-matrix"
9
- import { applyToPoint } from "transformation-matrix"
10
- import type { PcbColorMap, CanvasContext } from "../types"
11
- import { drawRect } from "../shapes/rect"
12
- import { drawCircle } from "../shapes/circle"
13
- import { drawLine } from "../shapes/line"
14
- import { drawPath } from "../shapes/path"
15
-
16
- export interface DrawPcbSilkscreenTextParams {
17
- ctx: CanvasContext
18
- text: PcbSilkscreenText
19
- realToCanvasMat: Matrix
20
- colorMap: PcbColorMap
21
- }
22
-
23
- export interface DrawPcbSilkscreenRectParams {
24
- ctx: CanvasContext
25
- rect: PcbSilkscreenRect
26
- realToCanvasMat: Matrix
27
- colorMap: PcbColorMap
28
- }
29
-
30
- export interface DrawPcbSilkscreenCircleParams {
31
- ctx: CanvasContext
32
- circle: PcbSilkscreenCircle
33
- realToCanvasMat: Matrix
34
- colorMap: PcbColorMap
35
- }
36
-
37
- export interface DrawPcbSilkscreenLineParams {
38
- ctx: CanvasContext
39
- line: PcbSilkscreenLine
40
- realToCanvasMat: Matrix
41
- colorMap: PcbColorMap
42
- }
43
-
44
- export interface DrawPcbSilkscreenPathParams {
45
- ctx: CanvasContext
46
- path: PcbSilkscreenPath
47
- realToCanvasMat: Matrix
48
- colorMap: PcbColorMap
49
- }
50
-
51
- function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
52
- return layer === "bottom"
53
- ? colorMap.silkscreen.bottom
54
- : colorMap.silkscreen.top
55
- }
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
- export function drawPcbSilkscreenText(
67
- params: DrawPcbSilkscreenTextParams,
68
- ): void {
69
- const { ctx, text, realToCanvasMat, colorMap } = params
70
-
71
- 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)
78
- const rotation = text.ccw_rotation ?? 0
79
-
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()
93
- }
94
-
95
- export function drawPcbSilkscreenRect(
96
- params: DrawPcbSilkscreenRectParams,
97
- ): void {
98
- const { ctx, rect, realToCanvasMat, colorMap } = params
99
-
100
- const color = layerToSilkscreenColor(rect.layer, colorMap)
101
-
102
- drawRect({
103
- ctx,
104
- center: rect.center,
105
- width: rect.width,
106
- height: rect.height,
107
- fill: color,
108
- realToCanvasMat,
109
- })
110
- }
111
-
112
- export function drawPcbSilkscreenCircle(
113
- params: DrawPcbSilkscreenCircleParams,
114
- ): void {
115
- const { ctx, circle, realToCanvasMat, colorMap } = params
116
-
117
- const color = layerToSilkscreenColor(circle.layer, colorMap)
118
-
119
- drawCircle({
120
- ctx,
121
- center: circle.center,
122
- radius: circle.radius,
123
- fill: color,
124
- realToCanvasMat,
125
- })
126
- }
127
-
128
- export function drawPcbSilkscreenLine(
129
- params: DrawPcbSilkscreenLineParams,
130
- ): void {
131
- const { ctx, line, realToCanvasMat, colorMap } = params
132
-
133
- const color = layerToSilkscreenColor(line.layer, colorMap)
134
-
135
- drawLine({
136
- ctx,
137
- start: { x: line.x1, y: line.y1 },
138
- end: { x: line.x2, y: line.y2 },
139
- strokeWidth: line.stroke_width ?? 0.1,
140
- stroke: color,
141
- realToCanvasMat,
142
- })
143
- }
144
-
145
- export function drawPcbSilkscreenPath(
146
- params: DrawPcbSilkscreenPathParams,
147
- ): void {
148
- const { ctx, path, realToCanvasMat, colorMap } = params
149
-
150
- const color = layerToSilkscreenColor(path.layer, colorMap)
151
-
152
- if (!path.route || path.route.length < 2) return
153
-
154
- // Draw each segment of the path
155
- for (let i = 0; i < path.route.length - 1; i++) {
156
- const start = path.route[i]
157
- const end = path.route[i + 1]
158
-
159
- if (!start || !end) continue
160
-
161
- drawLine({
162
- ctx,
163
- start: { x: start.x, y: start.y },
164
- end: { x: end.x, y: end.y },
165
- strokeWidth: path.stroke_width ?? 0.1,
166
- stroke: color,
167
- realToCanvasMat,
168
- })
169
- }
170
- }