circuit-to-canvas 0.0.21 → 0.0.22
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 +137 -129
- package/lib/drawer/elements/pcb-silkscreen.ts +28 -12
- package/package.json +2 -2
- package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
- package/tests/board-snapshot/usb-c-flashlight-board.test.ts +1 -1
- package/tests/elements/__snapshots__/pcb-note-dimension-angled-and-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-silkscreen.snap.png +0 -0
- package/tests/elements/__snapshots__/silkscreen-text-bottom-rotated.snap.png +0 -0
- package/tests/elements/__snapshots__/silkscreen-text-bottom.snap.png +0 -0
- package/tests/elements/__snapshots__/silkscreen-text-rotated.snap.png +0 -0
- package/tests/elements/pcb-silkscreen.test.ts +110 -0
- package/tests/fixtures/stackPngsVertically.ts +43 -4
package/dist/index.js
CHANGED
|
@@ -648,34 +648,159 @@ function drawPcbBoard(params) {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
// lib/drawer/elements/pcb-silkscreen.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.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
|
-
|
|
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] =
|
|
781
|
+
const [x, y] = applyToPoint9(realToCanvasMat, [
|
|
665
782
|
text.anchor_position.x,
|
|
666
783
|
text.anchor_position.y
|
|
667
784
|
]);
|
|
668
|
-
const
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
ctx.
|
|
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
|
}
|
|
681
806
|
function drawPcbSilkscreenRect(params) {
|
|
@@ -771,7 +896,7 @@ function drawPcbCutout(params) {
|
|
|
771
896
|
}
|
|
772
897
|
|
|
773
898
|
// lib/drawer/elements/pcb-copper-pour.ts
|
|
774
|
-
import { applyToPoint as
|
|
899
|
+
import { applyToPoint as applyToPoint10 } from "transformation-matrix";
|
|
775
900
|
function layerToColor3(layer, colorMap) {
|
|
776
901
|
return colorMap.copper[layer] ?? colorMap.copper.top;
|
|
777
902
|
}
|
|
@@ -780,7 +905,7 @@ function drawPcbCopperPour(params) {
|
|
|
780
905
|
const color = layerToColor3(pour.layer, colorMap);
|
|
781
906
|
ctx.save();
|
|
782
907
|
if (pour.shape === "rect") {
|
|
783
|
-
const [cx, cy] =
|
|
908
|
+
const [cx, cy] = applyToPoint10(realToCanvasMat, [
|
|
784
909
|
pour.center.x,
|
|
785
910
|
pour.center.y
|
|
786
911
|
]);
|
|
@@ -801,7 +926,7 @@ function drawPcbCopperPour(params) {
|
|
|
801
926
|
if (pour.shape === "polygon") {
|
|
802
927
|
if (pour.points && pour.points.length >= 3) {
|
|
803
928
|
const canvasPoints = pour.points.map(
|
|
804
|
-
(p) =>
|
|
929
|
+
(p) => applyToPoint10(realToCanvasMat, [p.x, p.y])
|
|
805
930
|
);
|
|
806
931
|
const firstPoint = canvasPoints[0];
|
|
807
932
|
if (!firstPoint) {
|
|
@@ -830,123 +955,6 @@ function drawPcbCopperPour(params) {
|
|
|
830
955
|
|
|
831
956
|
// lib/drawer/elements/pcb-copper-text.ts
|
|
832
957
|
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
958
|
var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
|
|
951
959
|
function layerToCopperColor(layer, colorMap) {
|
|
952
960
|
return colorMap.copper[layer] ?? colorMap.copper.top;
|
|
@@ -12,6 +12,12 @@ import { drawRect } from "../shapes/rect"
|
|
|
12
12
|
import { drawCircle } from "../shapes/circle"
|
|
13
13
|
import { drawLine } from "../shapes/line"
|
|
14
14
|
import { drawPath } from "../shapes/path"
|
|
15
|
+
import {
|
|
16
|
+
getAlphabetLayout,
|
|
17
|
+
strokeAlphabetText,
|
|
18
|
+
getTextStartPosition,
|
|
19
|
+
type AnchorAlignment,
|
|
20
|
+
} from "../shapes/text"
|
|
15
21
|
|
|
16
22
|
export interface DrawPcbSilkscreenTextParams {
|
|
17
23
|
ctx: CanvasContext
|
|
@@ -54,13 +60,9 @@ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
|
|
|
54
60
|
: colorMap.silkscreen.top
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function mapAnchorAlignment(
|
|
58
|
-
alignment?: string,
|
|
59
|
-
): "start" | "end" | "left" | "right" | "center" {
|
|
63
|
+
function mapAnchorAlignment(alignment?: string): AnchorAlignment {
|
|
60
64
|
if (!alignment) return "center"
|
|
61
|
-
|
|
62
|
-
if (alignment.includes("right")) return "right"
|
|
63
|
-
return "center"
|
|
65
|
+
return alignment as AnchorAlignment
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export function drawPcbSilkscreenText(
|
|
@@ -68,15 +70,22 @@ export function drawPcbSilkscreenText(
|
|
|
68
70
|
): void {
|
|
69
71
|
const { ctx, text, realToCanvasMat, colorMap } = params
|
|
70
72
|
|
|
73
|
+
const content = text.text ?? ""
|
|
74
|
+
if (!content) return
|
|
75
|
+
|
|
71
76
|
const color = layerToSilkscreenColor(text.layer, colorMap)
|
|
72
77
|
const [x, y] = applyToPoint(realToCanvasMat, [
|
|
73
78
|
text.anchor_position.x,
|
|
74
79
|
text.anchor_position.y,
|
|
75
80
|
])
|
|
76
|
-
|
|
77
|
-
const fontSize = (text.font_size ?? 1) *
|
|
81
|
+
const scale = Math.abs(realToCanvasMat.a)
|
|
82
|
+
const fontSize = (text.font_size ?? 1) * scale
|
|
78
83
|
const rotation = text.ccw_rotation ?? 0
|
|
79
84
|
|
|
85
|
+
const layout = getAlphabetLayout(content, fontSize)
|
|
86
|
+
const alignment = mapAnchorAlignment(text.anchor_alignment)
|
|
87
|
+
const startPos = getTextStartPosition(alignment, layout)
|
|
88
|
+
|
|
80
89
|
ctx.save()
|
|
81
90
|
ctx.translate(x, y)
|
|
82
91
|
|
|
@@ -85,10 +94,17 @@ export function drawPcbSilkscreenText(
|
|
|
85
94
|
ctx.rotate(-rotation * (Math.PI / 180))
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
if (text.layer === "bottom") {
|
|
98
|
+
ctx.scale(-1, 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ctx.lineWidth = layout.strokeWidth
|
|
102
|
+
ctx.lineCap = "round"
|
|
103
|
+
ctx.lineJoin = "round"
|
|
104
|
+
ctx.strokeStyle = color
|
|
105
|
+
|
|
106
|
+
strokeAlphabetText(ctx, content, layout, startPos.x, startPos.y)
|
|
107
|
+
|
|
92
108
|
ctx.restore()
|
|
93
109
|
}
|
|
94
110
|
|
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.22",
|
|
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.
|
|
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",
|
|
Binary file
|
|
@@ -5,7 +5,7 @@ import usbcFlashlightCircuit from "./usb-c-flashlight.json"
|
|
|
5
5
|
|
|
6
6
|
const circuitElements = usbcFlashlightCircuit as AnyCircuitElement[]
|
|
7
7
|
|
|
8
|
-
test
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 = (
|
|
80
|
-
const
|
|
81
|
-
|
|
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
|
}
|