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 +8 -4
- package/dist/index.js +162 -134
- package/lib/drawer/CircuitToCanvasDrawer.ts +5 -7
- package/lib/drawer/elements/index.ts +17 -5
- package/lib/drawer/elements/pcb-silkscreen-circle.ts +33 -0
- package/lib/drawer/elements/pcb-silkscreen-line.ts +34 -0
- package/lib/drawer/elements/pcb-silkscreen-path.ts +44 -0
- package/lib/drawer/elements/pcb-silkscreen-rect.ts +34 -0
- package/lib/drawer/elements/pcb-silkscreen-text.ts +71 -0
- 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/lib/drawer/elements/pcb-silkscreen.ts +0 -170
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
|
-
|
|
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
|
}
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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] =
|
|
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) =>
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
-
}
|