circuit-to-canvas 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +24 -2
- package/dist/index.js +290 -142
- package/lib/drawer/CircuitToCanvasDrawer.ts +11 -0
- package/lib/drawer/elements/index.ts +5 -0
- package/lib/drawer/elements/pcb-note-dimension.ts +201 -0
- package/lib/drawer/elements/pcb-silkscreen.ts +14 -29
- package/lib/drawer/shapes/arrow.ts +36 -0
- package/lib/drawer/shapes/index.ts +1 -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-note-dimension-basic.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
- package/tests/elements/pcb-note-dimension-angled-and-vertical.test.ts +37 -0
- package/tests/elements/pcb-note-dimension-basic.test.ts +36 -0
- package/tests/elements/pcb-note-dimension-vertical.test.ts +42 -0
- package/tests/elements/pcb-note-dimension-with-offset.test.ts +38 -0
- package/tests/fixtures/stackPngsVertically.ts +43 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyCircuitElement, NinePointAnchor, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText } from 'circuit-json';
|
|
1
|
+
import { AnyCircuitElement, NinePointAnchor, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension } from 'circuit-json';
|
|
2
2
|
import { Matrix } from 'transformation-matrix';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -202,6 +202,20 @@ interface DrawPathParams {
|
|
|
202
202
|
}
|
|
203
203
|
declare function drawPath(params: DrawPathParams): void;
|
|
204
204
|
|
|
205
|
+
interface DrawArrowParams {
|
|
206
|
+
ctx: CanvasContext;
|
|
207
|
+
x: number;
|
|
208
|
+
y: number;
|
|
209
|
+
angle: number;
|
|
210
|
+
arrowSize: number;
|
|
211
|
+
color: string;
|
|
212
|
+
strokeWidth: number;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Draw an arrow at a point along a line
|
|
216
|
+
*/
|
|
217
|
+
declare function drawArrow(params: DrawArrowParams): void;
|
|
218
|
+
|
|
205
219
|
type AlphabetLayout = {
|
|
206
220
|
width: number;
|
|
207
221
|
height: number;
|
|
@@ -388,4 +402,12 @@ interface DrawPcbNoteTextParams {
|
|
|
388
402
|
}
|
|
389
403
|
declare function drawPcbNoteText(params: DrawPcbNoteTextParams): void;
|
|
390
404
|
|
|
391
|
-
|
|
405
|
+
interface DrawPcbNoteDimensionParams {
|
|
406
|
+
ctx: CanvasContext;
|
|
407
|
+
pcbNoteDimension: PcbNoteDimension;
|
|
408
|
+
realToCanvasMat: Matrix;
|
|
409
|
+
colorMap: PcbColorMap;
|
|
410
|
+
}
|
|
411
|
+
declare function drawPcbNoteDimension(params: DrawPcbNoteDimensionParams): void;
|
|
412
|
+
|
|
413
|
+
export { type AlphabetLayout, type AnchorAlignment, type CameraBounds, type CanvasContext, CircuitToCanvasDrawer, type CopperColorMap, type CopperLayerName, DEFAULT_PCB_COLOR_MAP, type DrawArrowParams, type DrawCircleParams, type DrawContext, type DrawElementsOptions, type DrawLineParams, type DrawOvalParams, type DrawPathParams, type DrawPcbBoardParams, type DrawPcbCopperPourParams, type DrawPcbCopperTextParams, type DrawPcbCutoutParams, type DrawPcbFabricationNotePathParams, type DrawPcbFabricationNoteRectParams, type DrawPcbFabricationNoteTextParams, type DrawPcbHoleParams, type DrawPcbNoteDimensionParams, type DrawPcbNotePathParams, type DrawPcbNoteRectParams, type DrawPcbNoteTextParams, type DrawPcbPlatedHoleParams, type DrawPcbSilkscreenCircleParams, type DrawPcbSilkscreenLineParams, type DrawPcbSilkscreenPathParams, type DrawPcbSilkscreenRectParams, type DrawPcbSilkscreenTextParams, type DrawPcbSmtPadParams, type DrawPcbTraceParams, type DrawPcbViaParams, type DrawPillParams, type DrawPolygonParams, type DrawRectParams, type DrawTextParams, type DrawerConfig, type PcbColorMap, drawArrow, drawCircle, drawLine, drawOval, drawPath, drawPcbBoard, drawPcbCopperPour, drawPcbCopperText, drawPcbCutout, drawPcbFabricationNotePath, drawPcbFabricationNoteRect, drawPcbFabricationNoteText, drawPcbHole, drawPcbNoteDimension, drawPcbNotePath, drawPcbNoteRect, drawPcbNoteText, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenPath, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
|
package/dist/index.js
CHANGED
|
@@ -647,37 +647,142 @@ function drawPcbBoard(params) {
|
|
|
647
647
|
}
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
// lib/drawer/
|
|
650
|
+
// lib/drawer/shapes/text/text.ts
|
|
651
|
+
import { lineAlphabet } from "@tscircuit/alphabet";
|
|
651
652
|
import { applyToPoint as applyToPoint8 } from "transformation-matrix";
|
|
652
|
-
|
|
653
|
-
|
|
653
|
+
|
|
654
|
+
// lib/drawer/shapes/text/getAlphabetLayout.ts
|
|
655
|
+
var GLYPH_WIDTH_RATIO = 0.62;
|
|
656
|
+
var LETTER_SPACING_RATIO = 0.3;
|
|
657
|
+
var SPACE_WIDTH_RATIO = 1;
|
|
658
|
+
var STROKE_WIDTH_RATIO = 0.13;
|
|
659
|
+
function getAlphabetLayout(text, fontSize) {
|
|
660
|
+
const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
|
|
661
|
+
const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
|
|
662
|
+
const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
|
|
663
|
+
const characters = Array.from(text);
|
|
664
|
+
let width = 0;
|
|
665
|
+
characters.forEach((char, index) => {
|
|
666
|
+
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
667
|
+
width += advance;
|
|
668
|
+
if (index < characters.length - 1) width += letterSpacing;
|
|
669
|
+
});
|
|
670
|
+
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
|
|
671
|
+
return {
|
|
672
|
+
width,
|
|
673
|
+
height: fontSize,
|
|
674
|
+
glyphWidth,
|
|
675
|
+
letterSpacing,
|
|
676
|
+
spaceWidth,
|
|
677
|
+
strokeWidth
|
|
678
|
+
};
|
|
654
679
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
680
|
+
|
|
681
|
+
// lib/drawer/shapes/text/getTextStartPosition.ts
|
|
682
|
+
function getTextStartPosition(alignment, layout) {
|
|
683
|
+
const totalWidth = layout.width + layout.strokeWidth;
|
|
684
|
+
const totalHeight = layout.height + layout.strokeWidth;
|
|
685
|
+
let x = 0;
|
|
686
|
+
let y = 0;
|
|
687
|
+
if (alignment === "center") {
|
|
688
|
+
x = -totalWidth / 2;
|
|
689
|
+
} else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
|
|
690
|
+
x = 0;
|
|
691
|
+
} else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
|
|
692
|
+
x = -totalWidth;
|
|
693
|
+
}
|
|
694
|
+
if (alignment === "center") {
|
|
695
|
+
y = -totalHeight / 2;
|
|
696
|
+
} else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
|
|
697
|
+
y = 0;
|
|
698
|
+
} else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
|
|
699
|
+
y = -totalHeight;
|
|
700
|
+
} else {
|
|
701
|
+
y = 0;
|
|
702
|
+
}
|
|
703
|
+
return { x, y };
|
|
660
704
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
705
|
+
|
|
706
|
+
// lib/drawer/shapes/text/text.ts
|
|
707
|
+
var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
|
|
708
|
+
function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
709
|
+
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
|
|
710
|
+
const topY = startY;
|
|
711
|
+
const characters = Array.from(text);
|
|
712
|
+
let cursor = startX + strokeWidth / 2;
|
|
713
|
+
characters.forEach((char, index) => {
|
|
714
|
+
const lines = getGlyphLines(char);
|
|
715
|
+
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
716
|
+
if (lines?.length) {
|
|
717
|
+
ctx.beginPath();
|
|
718
|
+
for (const line of lines) {
|
|
719
|
+
const x1 = cursor + line.x1 * glyphWidth;
|
|
720
|
+
const y1 = topY + (1 - line.y1) * height;
|
|
721
|
+
const x2 = cursor + line.x2 * glyphWidth;
|
|
722
|
+
const y2 = topY + (1 - line.y2) * height;
|
|
723
|
+
ctx.moveTo(x1, y1);
|
|
724
|
+
ctx.lineTo(x2, y2);
|
|
725
|
+
}
|
|
726
|
+
ctx.stroke();
|
|
727
|
+
}
|
|
728
|
+
cursor += advance;
|
|
729
|
+
if (index < characters.length - 1) {
|
|
730
|
+
cursor += letterSpacing;
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function drawText(params) {
|
|
735
|
+
const {
|
|
736
|
+
ctx,
|
|
737
|
+
text,
|
|
738
|
+
x,
|
|
739
|
+
y,
|
|
740
|
+
fontSize,
|
|
741
|
+
color,
|
|
742
|
+
realToCanvasMat,
|
|
743
|
+
anchorAlignment,
|
|
744
|
+
rotation = 0
|
|
745
|
+
} = params;
|
|
746
|
+
if (!text) return;
|
|
747
|
+
const [canvasX, canvasY] = applyToPoint8(realToCanvasMat, [x, y]);
|
|
748
|
+
const scale2 = Math.abs(realToCanvasMat.a);
|
|
749
|
+
const scaledFontSize = fontSize * scale2;
|
|
750
|
+
const layout = getAlphabetLayout(text, scaledFontSize);
|
|
751
|
+
const startPos = getTextStartPosition(anchorAlignment, layout);
|
|
670
752
|
ctx.save();
|
|
671
|
-
ctx.translate(
|
|
753
|
+
ctx.translate(canvasX, canvasY);
|
|
672
754
|
if (rotation !== 0) {
|
|
673
755
|
ctx.rotate(-rotation * (Math.PI / 180));
|
|
674
756
|
}
|
|
675
|
-
ctx.
|
|
676
|
-
ctx.
|
|
677
|
-
ctx.
|
|
678
|
-
ctx.
|
|
757
|
+
ctx.lineWidth = layout.strokeWidth;
|
|
758
|
+
ctx.lineCap = "round";
|
|
759
|
+
ctx.lineJoin = "round";
|
|
760
|
+
ctx.strokeStyle = color;
|
|
761
|
+
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
|
|
679
762
|
ctx.restore();
|
|
680
763
|
}
|
|
764
|
+
|
|
765
|
+
// lib/drawer/elements/pcb-silkscreen.ts
|
|
766
|
+
function layerToSilkscreenColor(layer, colorMap) {
|
|
767
|
+
return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
|
|
768
|
+
}
|
|
769
|
+
function drawPcbSilkscreenText(params) {
|
|
770
|
+
const { ctx, text, realToCanvasMat, colorMap } = params;
|
|
771
|
+
const color = layerToSilkscreenColor(text.layer, colorMap);
|
|
772
|
+
const fontSize = text.font_size ?? 1;
|
|
773
|
+
const rotation = text.ccw_rotation ?? 0;
|
|
774
|
+
drawText({
|
|
775
|
+
ctx,
|
|
776
|
+
text: text.text,
|
|
777
|
+
x: text.anchor_position.x,
|
|
778
|
+
y: text.anchor_position.y,
|
|
779
|
+
fontSize,
|
|
780
|
+
color,
|
|
781
|
+
realToCanvasMat,
|
|
782
|
+
anchorAlignment: text.anchor_alignment ?? "center",
|
|
783
|
+
rotation
|
|
784
|
+
});
|
|
785
|
+
}
|
|
681
786
|
function drawPcbSilkscreenRect(params) {
|
|
682
787
|
const { ctx, rect, realToCanvasMat, colorMap } = params;
|
|
683
788
|
const color = layerToSilkscreenColor(rect.layer, colorMap);
|
|
@@ -829,129 +934,12 @@ function drawPcbCopperPour(params) {
|
|
|
829
934
|
}
|
|
830
935
|
|
|
831
936
|
// lib/drawer/elements/pcb-copper-text.ts
|
|
832
|
-
import { applyToPoint as applyToPoint11 } from "transformation-matrix";
|
|
833
|
-
|
|
834
|
-
// lib/drawer/shapes/text/text.ts
|
|
835
|
-
import { lineAlphabet } from "@tscircuit/alphabet";
|
|
836
937
|
import { applyToPoint as applyToPoint10 } from "transformation-matrix";
|
|
837
|
-
|
|
838
|
-
// lib/drawer/shapes/text/getAlphabetLayout.ts
|
|
839
|
-
var GLYPH_WIDTH_RATIO = 0.62;
|
|
840
|
-
var LETTER_SPACING_RATIO = 0.3;
|
|
841
|
-
var SPACE_WIDTH_RATIO = 1;
|
|
842
|
-
var STROKE_WIDTH_RATIO = 0.13;
|
|
843
|
-
function getAlphabetLayout(text, fontSize) {
|
|
844
|
-
const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
|
|
845
|
-
const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
|
|
846
|
-
const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
|
|
847
|
-
const characters = Array.from(text);
|
|
848
|
-
let width = 0;
|
|
849
|
-
characters.forEach((char, index) => {
|
|
850
|
-
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
851
|
-
width += advance;
|
|
852
|
-
if (index < characters.length - 1) width += letterSpacing;
|
|
853
|
-
});
|
|
854
|
-
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
|
|
855
|
-
return {
|
|
856
|
-
width,
|
|
857
|
-
height: fontSize,
|
|
858
|
-
glyphWidth,
|
|
859
|
-
letterSpacing,
|
|
860
|
-
spaceWidth,
|
|
861
|
-
strokeWidth
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// lib/drawer/shapes/text/getTextStartPosition.ts
|
|
866
|
-
function getTextStartPosition(alignment, layout) {
|
|
867
|
-
const totalWidth = layout.width + layout.strokeWidth;
|
|
868
|
-
const totalHeight = layout.height + layout.strokeWidth;
|
|
869
|
-
let x = 0;
|
|
870
|
-
let y = 0;
|
|
871
|
-
if (alignment === "center") {
|
|
872
|
-
x = -totalWidth / 2;
|
|
873
|
-
} else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
|
|
874
|
-
x = 0;
|
|
875
|
-
} else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
|
|
876
|
-
x = -totalWidth;
|
|
877
|
-
}
|
|
878
|
-
if (alignment === "center") {
|
|
879
|
-
y = -totalHeight / 2;
|
|
880
|
-
} else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
|
|
881
|
-
y = 0;
|
|
882
|
-
} else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
|
|
883
|
-
y = -totalHeight;
|
|
884
|
-
} else {
|
|
885
|
-
y = 0;
|
|
886
|
-
}
|
|
887
|
-
return { x, y };
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// lib/drawer/shapes/text/text.ts
|
|
891
|
-
var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
|
|
892
|
-
function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
893
|
-
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
|
|
894
|
-
const topY = startY;
|
|
895
|
-
const characters = Array.from(text);
|
|
896
|
-
let cursor = startX + strokeWidth / 2;
|
|
897
|
-
characters.forEach((char, index) => {
|
|
898
|
-
const lines = getGlyphLines(char);
|
|
899
|
-
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
900
|
-
if (lines?.length) {
|
|
901
|
-
ctx.beginPath();
|
|
902
|
-
for (const line of lines) {
|
|
903
|
-
const x1 = cursor + line.x1 * glyphWidth;
|
|
904
|
-
const y1 = topY + (1 - line.y1) * height;
|
|
905
|
-
const x2 = cursor + line.x2 * glyphWidth;
|
|
906
|
-
const y2 = topY + (1 - line.y2) * height;
|
|
907
|
-
ctx.moveTo(x1, y1);
|
|
908
|
-
ctx.lineTo(x2, y2);
|
|
909
|
-
}
|
|
910
|
-
ctx.stroke();
|
|
911
|
-
}
|
|
912
|
-
cursor += advance;
|
|
913
|
-
if (index < characters.length - 1) {
|
|
914
|
-
cursor += letterSpacing;
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
function drawText(params) {
|
|
919
|
-
const {
|
|
920
|
-
ctx,
|
|
921
|
-
text,
|
|
922
|
-
x,
|
|
923
|
-
y,
|
|
924
|
-
fontSize,
|
|
925
|
-
color,
|
|
926
|
-
realToCanvasMat,
|
|
927
|
-
anchorAlignment,
|
|
928
|
-
rotation = 0
|
|
929
|
-
} = params;
|
|
930
|
-
if (!text) return;
|
|
931
|
-
const [canvasX, canvasY] = applyToPoint10(realToCanvasMat, [x, y]);
|
|
932
|
-
const scale2 = Math.abs(realToCanvasMat.a);
|
|
933
|
-
const scaledFontSize = fontSize * scale2;
|
|
934
|
-
const layout = getAlphabetLayout(text, scaledFontSize);
|
|
935
|
-
const startPos = getTextStartPosition(anchorAlignment, layout);
|
|
936
|
-
ctx.save();
|
|
937
|
-
ctx.translate(canvasX, canvasY);
|
|
938
|
-
if (rotation !== 0) {
|
|
939
|
-
ctx.rotate(-rotation * (Math.PI / 180));
|
|
940
|
-
}
|
|
941
|
-
ctx.lineWidth = layout.strokeWidth;
|
|
942
|
-
ctx.lineCap = "round";
|
|
943
|
-
ctx.lineJoin = "round";
|
|
944
|
-
ctx.strokeStyle = color;
|
|
945
|
-
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
|
|
946
|
-
ctx.restore();
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// lib/drawer/elements/pcb-copper-text.ts
|
|
950
938
|
var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
|
|
951
939
|
function layerToCopperColor(layer, colorMap) {
|
|
952
940
|
return colorMap.copper[layer] ?? colorMap.copper.top;
|
|
953
941
|
}
|
|
954
|
-
function
|
|
942
|
+
function mapAnchorAlignment(alignment) {
|
|
955
943
|
if (!alignment) return "center";
|
|
956
944
|
if (alignment.includes("left")) return "center_left";
|
|
957
945
|
if (alignment.includes("right")) return "center_right";
|
|
@@ -961,7 +949,7 @@ function drawPcbCopperText(params) {
|
|
|
961
949
|
const { ctx, text, realToCanvasMat, colorMap } = params;
|
|
962
950
|
const content = text.text ?? "";
|
|
963
951
|
if (!content) return;
|
|
964
|
-
const [x, y] =
|
|
952
|
+
const [x, y] = applyToPoint10(realToCanvasMat, [
|
|
965
953
|
text.anchor_position.x,
|
|
966
954
|
text.anchor_position.y
|
|
967
955
|
]);
|
|
@@ -975,7 +963,7 @@ function drawPcbCopperText(params) {
|
|
|
975
963
|
const textColor = layerToCopperColor(text.layer, colorMap);
|
|
976
964
|
const layout = getAlphabetLayout(content, fontSize);
|
|
977
965
|
const totalWidth = layout.width + layout.strokeWidth;
|
|
978
|
-
const alignment =
|
|
966
|
+
const alignment = mapAnchorAlignment(text.anchor_alignment);
|
|
979
967
|
const startPos = getTextStartPosition(alignment, layout);
|
|
980
968
|
const startX = startPos.x;
|
|
981
969
|
const startY = startPos.y;
|
|
@@ -1144,6 +1132,156 @@ function drawPcbNoteText(params) {
|
|
|
1144
1132
|
});
|
|
1145
1133
|
}
|
|
1146
1134
|
|
|
1135
|
+
// lib/drawer/elements/pcb-note-dimension.ts
|
|
1136
|
+
import { applyToPoint as applyToPoint11 } from "transformation-matrix";
|
|
1137
|
+
|
|
1138
|
+
// lib/drawer/shapes/arrow.ts
|
|
1139
|
+
function drawArrow(params) {
|
|
1140
|
+
const { ctx, x, y, angle, arrowSize, color, strokeWidth } = params;
|
|
1141
|
+
ctx.save();
|
|
1142
|
+
ctx.translate(x, y);
|
|
1143
|
+
ctx.rotate(angle);
|
|
1144
|
+
ctx.beginPath();
|
|
1145
|
+
ctx.moveTo(0, 0);
|
|
1146
|
+
ctx.lineTo(-arrowSize, -arrowSize / 2);
|
|
1147
|
+
ctx.moveTo(0, 0);
|
|
1148
|
+
ctx.lineTo(-arrowSize, arrowSize / 2);
|
|
1149
|
+
ctx.lineWidth = strokeWidth;
|
|
1150
|
+
ctx.strokeStyle = color;
|
|
1151
|
+
ctx.lineCap = "round";
|
|
1152
|
+
ctx.lineJoin = "round";
|
|
1153
|
+
ctx.stroke();
|
|
1154
|
+
ctx.restore();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// lib/drawer/elements/pcb-note-dimension.ts
|
|
1158
|
+
var DEFAULT_NOTE_COLOR = "rgba(255,255,255,0.5)";
|
|
1159
|
+
function drawPcbNoteDimension(params) {
|
|
1160
|
+
const { ctx, pcbNoteDimension, realToCanvasMat } = params;
|
|
1161
|
+
const color = pcbNoteDimension.color ?? DEFAULT_NOTE_COLOR;
|
|
1162
|
+
const arrowSize = pcbNoteDimension.arrow_size;
|
|
1163
|
+
const realFromX = pcbNoteDimension.from.x;
|
|
1164
|
+
const realFromY = pcbNoteDimension.from.y;
|
|
1165
|
+
const realToX = pcbNoteDimension.to.x;
|
|
1166
|
+
const realToY = pcbNoteDimension.to.y;
|
|
1167
|
+
let fromX = realFromX;
|
|
1168
|
+
let fromY = realFromY;
|
|
1169
|
+
let toX = realToX;
|
|
1170
|
+
let toY = realToY;
|
|
1171
|
+
let hasOffset = false;
|
|
1172
|
+
let offsetX = 0;
|
|
1173
|
+
let offsetY = 0;
|
|
1174
|
+
if (pcbNoteDimension.offset_distance && pcbNoteDimension.offset_direction) {
|
|
1175
|
+
const dirX = pcbNoteDimension.offset_direction.x;
|
|
1176
|
+
const dirY = pcbNoteDimension.offset_direction.y;
|
|
1177
|
+
const length = Math.hypot(dirX, dirY);
|
|
1178
|
+
if (length > 0) {
|
|
1179
|
+
const normX = dirX / length;
|
|
1180
|
+
const normY = dirY / length;
|
|
1181
|
+
hasOffset = true;
|
|
1182
|
+
offsetX = pcbNoteDimension.offset_distance * normX;
|
|
1183
|
+
offsetY = pcbNoteDimension.offset_distance * normY;
|
|
1184
|
+
fromX += offsetX;
|
|
1185
|
+
fromY += offsetY;
|
|
1186
|
+
toX += offsetX;
|
|
1187
|
+
toY += offsetY;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
const STROKE_WIDTH_RATIO2 = 0.13;
|
|
1191
|
+
const strokeWidth = Math.max(
|
|
1192
|
+
pcbNoteDimension.font_size * STROKE_WIDTH_RATIO2,
|
|
1193
|
+
0.35
|
|
1194
|
+
);
|
|
1195
|
+
if (hasOffset) {
|
|
1196
|
+
drawLine({
|
|
1197
|
+
ctx,
|
|
1198
|
+
start: { x: realFromX, y: realFromY },
|
|
1199
|
+
end: { x: fromX, y: fromY },
|
|
1200
|
+
strokeWidth,
|
|
1201
|
+
stroke: color,
|
|
1202
|
+
realToCanvasMat
|
|
1203
|
+
});
|
|
1204
|
+
drawLine({
|
|
1205
|
+
ctx,
|
|
1206
|
+
start: { x: realToX, y: realToY },
|
|
1207
|
+
end: { x: toX, y: toY },
|
|
1208
|
+
strokeWidth,
|
|
1209
|
+
stroke: color,
|
|
1210
|
+
realToCanvasMat
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
drawLine({
|
|
1214
|
+
ctx,
|
|
1215
|
+
start: { x: fromX, y: fromY },
|
|
1216
|
+
end: { x: toX, y: toY },
|
|
1217
|
+
strokeWidth,
|
|
1218
|
+
stroke: color,
|
|
1219
|
+
realToCanvasMat
|
|
1220
|
+
});
|
|
1221
|
+
const [canvasFromX, canvasFromY] = applyToPoint11(realToCanvasMat, [
|
|
1222
|
+
fromX,
|
|
1223
|
+
fromY
|
|
1224
|
+
]);
|
|
1225
|
+
const [canvasToX, canvasToY] = applyToPoint11(realToCanvasMat, [toX, toY]);
|
|
1226
|
+
const canvasDx = canvasToX - canvasFromX;
|
|
1227
|
+
const canvasDy = canvasToY - canvasFromY;
|
|
1228
|
+
const lineAngle = Math.atan2(canvasDy, canvasDx);
|
|
1229
|
+
const scale2 = Math.abs(realToCanvasMat.a);
|
|
1230
|
+
const scaledArrowSize = arrowSize * scale2;
|
|
1231
|
+
const scaledStrokeWidth = strokeWidth * scale2;
|
|
1232
|
+
drawArrow({
|
|
1233
|
+
ctx,
|
|
1234
|
+
x: canvasFromX,
|
|
1235
|
+
y: canvasFromY,
|
|
1236
|
+
angle: lineAngle + Math.PI,
|
|
1237
|
+
arrowSize: scaledArrowSize,
|
|
1238
|
+
color,
|
|
1239
|
+
strokeWidth: scaledStrokeWidth
|
|
1240
|
+
});
|
|
1241
|
+
drawArrow({
|
|
1242
|
+
ctx,
|
|
1243
|
+
x: canvasToX,
|
|
1244
|
+
y: canvasToY,
|
|
1245
|
+
angle: lineAngle,
|
|
1246
|
+
arrowSize: scaledArrowSize,
|
|
1247
|
+
color,
|
|
1248
|
+
strokeWidth: scaledStrokeWidth
|
|
1249
|
+
});
|
|
1250
|
+
if (pcbNoteDimension.text) {
|
|
1251
|
+
let textX = (fromX + toX) / 2;
|
|
1252
|
+
let textY = (fromY + toY) / 2;
|
|
1253
|
+
const perpX = toY - fromY;
|
|
1254
|
+
const perpY = -(toX - fromX);
|
|
1255
|
+
const perpLength = Math.sqrt(perpX * perpX + perpY * perpY);
|
|
1256
|
+
if (perpLength > 0) {
|
|
1257
|
+
const offsetDistance = pcbNoteDimension.font_size * 1.5;
|
|
1258
|
+
const normalizedPerpX = perpX / perpLength;
|
|
1259
|
+
const normalizedPerpY = perpY / perpLength;
|
|
1260
|
+
textX += normalizedPerpX * offsetDistance;
|
|
1261
|
+
textY += normalizedPerpY * offsetDistance;
|
|
1262
|
+
}
|
|
1263
|
+
const textRotation = -(() => {
|
|
1264
|
+
const raw = pcbNoteDimension.text_ccw_rotation ?? lineAngle * 180 / Math.PI;
|
|
1265
|
+
if (pcbNoteDimension.text_ccw_rotation !== void 0) return raw;
|
|
1266
|
+
let deg = (raw + 180) % 360 - 180;
|
|
1267
|
+
if (deg > 90) deg -= 180;
|
|
1268
|
+
if (deg < -90) deg += 180;
|
|
1269
|
+
return deg;
|
|
1270
|
+
})();
|
|
1271
|
+
drawText({
|
|
1272
|
+
ctx,
|
|
1273
|
+
text: pcbNoteDimension.text,
|
|
1274
|
+
x: textX,
|
|
1275
|
+
y: textY,
|
|
1276
|
+
fontSize: pcbNoteDimension.font_size,
|
|
1277
|
+
color,
|
|
1278
|
+
realToCanvasMat,
|
|
1279
|
+
anchorAlignment: "center",
|
|
1280
|
+
rotation: textRotation
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1147
1285
|
// lib/drawer/elements/pcb-note-line.ts
|
|
1148
1286
|
import { applyToPoint as applyToPoint12 } from "transformation-matrix";
|
|
1149
1287
|
function drawPcbNoteLine(params) {
|
|
@@ -1408,11 +1546,20 @@ var CircuitToCanvasDrawer = class {
|
|
|
1408
1546
|
colorMap: this.colorMap
|
|
1409
1547
|
});
|
|
1410
1548
|
}
|
|
1549
|
+
if (element.type === "pcb_note_dimension") {
|
|
1550
|
+
drawPcbNoteDimension({
|
|
1551
|
+
ctx: this.ctx,
|
|
1552
|
+
pcbNoteDimension: element,
|
|
1553
|
+
realToCanvasMat: this.realToCanvasMat,
|
|
1554
|
+
colorMap: this.colorMap
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1411
1557
|
}
|
|
1412
1558
|
};
|
|
1413
1559
|
export {
|
|
1414
1560
|
CircuitToCanvasDrawer,
|
|
1415
1561
|
DEFAULT_PCB_COLOR_MAP,
|
|
1562
|
+
drawArrow,
|
|
1416
1563
|
drawCircle,
|
|
1417
1564
|
drawLine,
|
|
1418
1565
|
drawOval,
|
|
@@ -1425,6 +1572,7 @@ export {
|
|
|
1425
1572
|
drawPcbFabricationNoteRect,
|
|
1426
1573
|
drawPcbFabricationNoteText,
|
|
1427
1574
|
drawPcbHole,
|
|
1575
|
+
drawPcbNoteDimension,
|
|
1428
1576
|
drawPcbNotePath,
|
|
1429
1577
|
drawPcbNoteRect,
|
|
1430
1578
|
drawPcbNoteText,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
PcbFabricationNotePath,
|
|
21
21
|
PcbNotePath,
|
|
22
22
|
PcbNoteText,
|
|
23
|
+
PcbNoteDimension,
|
|
23
24
|
PcbNoteLine,
|
|
24
25
|
} from "circuit-json"
|
|
25
26
|
import { identity, compose, translate, scale } from "transformation-matrix"
|
|
@@ -53,6 +54,7 @@ import { drawPcbNoteRect } from "./elements/pcb-note-rect"
|
|
|
53
54
|
import { drawPcbFabricationNotePath } from "./elements/pcb-fabrication-note-path"
|
|
54
55
|
import { drawPcbNotePath } from "./elements/pcb-note-path"
|
|
55
56
|
import { drawPcbNoteText } from "./elements/pcb-note-text"
|
|
57
|
+
import { drawPcbNoteDimension } from "./elements/pcb-note-dimension"
|
|
56
58
|
import { drawPcbNoteLine } from "./elements/pcb-note-line"
|
|
57
59
|
|
|
58
60
|
export interface DrawElementsOptions {
|
|
@@ -341,5 +343,14 @@ export class CircuitToCanvasDrawer {
|
|
|
341
343
|
colorMap: this.colorMap,
|
|
342
344
|
})
|
|
343
345
|
}
|
|
346
|
+
|
|
347
|
+
if (element.type === "pcb_note_dimension") {
|
|
348
|
+
drawPcbNoteDimension({
|
|
349
|
+
ctx: this.ctx,
|
|
350
|
+
pcbNoteDimension: element as PcbNoteDimension,
|
|
351
|
+
realToCanvasMat: this.realToCanvasMat,
|
|
352
|
+
colorMap: this.colorMap,
|
|
353
|
+
})
|
|
354
|
+
}
|
|
344
355
|
}
|
|
345
356
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { PcbNoteDimension } 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 { drawLine } from "../shapes/line"
|
|
6
|
+
import { drawText } from "../shapes/text"
|
|
7
|
+
import { drawArrow } from "../shapes/arrow"
|
|
8
|
+
|
|
9
|
+
export interface DrawPcbNoteDimensionParams {
|
|
10
|
+
ctx: CanvasContext
|
|
11
|
+
pcbNoteDimension: PcbNoteDimension
|
|
12
|
+
realToCanvasMat: Matrix
|
|
13
|
+
colorMap: PcbColorMap
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_NOTE_COLOR = "rgba(255,255,255,0.5)"
|
|
17
|
+
|
|
18
|
+
export function drawPcbNoteDimension(params: DrawPcbNoteDimensionParams): void {
|
|
19
|
+
const { ctx, pcbNoteDimension, realToCanvasMat } = params
|
|
20
|
+
|
|
21
|
+
const color = pcbNoteDimension.color ?? DEFAULT_NOTE_COLOR
|
|
22
|
+
const arrowSize = pcbNoteDimension.arrow_size
|
|
23
|
+
|
|
24
|
+
// Store real (model) endpoints for extension lines
|
|
25
|
+
const realFromX = pcbNoteDimension.from.x
|
|
26
|
+
const realFromY = pcbNoteDimension.from.y
|
|
27
|
+
const realToX = pcbNoteDimension.to.x
|
|
28
|
+
const realToY = pcbNoteDimension.to.y
|
|
29
|
+
|
|
30
|
+
// Calculate the dimension line endpoints (real/model coords)
|
|
31
|
+
let fromX = realFromX
|
|
32
|
+
let fromY = realFromY
|
|
33
|
+
let toX = realToX
|
|
34
|
+
let toY = realToY
|
|
35
|
+
|
|
36
|
+
// Track if we have an offset (for drawing extension lines)
|
|
37
|
+
let hasOffset = false
|
|
38
|
+
let offsetX = 0
|
|
39
|
+
let offsetY = 0
|
|
40
|
+
|
|
41
|
+
// Apply offset if provided
|
|
42
|
+
if (pcbNoteDimension.offset_distance && pcbNoteDimension.offset_direction) {
|
|
43
|
+
const dirX = pcbNoteDimension.offset_direction.x
|
|
44
|
+
const dirY = pcbNoteDimension.offset_direction.y
|
|
45
|
+
const length = Math.hypot(dirX, dirY)
|
|
46
|
+
if (length > 0) {
|
|
47
|
+
const normX = dirX / length
|
|
48
|
+
const normY = dirY / length
|
|
49
|
+
hasOffset = true
|
|
50
|
+
offsetX = pcbNoteDimension.offset_distance * normX
|
|
51
|
+
offsetY = pcbNoteDimension.offset_distance * normY
|
|
52
|
+
fromX += offsetX
|
|
53
|
+
fromY += offsetY
|
|
54
|
+
toX += offsetX
|
|
55
|
+
toY += offsetY
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Calculate stroke width to match text stroke width
|
|
60
|
+
// Text uses fontSize * STROKE_WIDTH_RATIO (0.13) with minimum 0.35
|
|
61
|
+
const STROKE_WIDTH_RATIO = 0.13
|
|
62
|
+
|
|
63
|
+
const strokeWidth = Math.max(
|
|
64
|
+
pcbNoteDimension.font_size * STROKE_WIDTH_RATIO,
|
|
65
|
+
0.35,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Draw extension lines if offset is provided
|
|
69
|
+
if (hasOffset) {
|
|
70
|
+
// Extension line from original 'from' point to offset 'from' point
|
|
71
|
+
drawLine({
|
|
72
|
+
ctx,
|
|
73
|
+
start: { x: realFromX, y: realFromY },
|
|
74
|
+
end: { x: fromX, y: fromY },
|
|
75
|
+
strokeWidth,
|
|
76
|
+
stroke: color,
|
|
77
|
+
realToCanvasMat: realToCanvasMat,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Extension line from original 'to' point to offset 'to' point
|
|
81
|
+
drawLine({
|
|
82
|
+
ctx,
|
|
83
|
+
start: { x: realToX, y: realToY },
|
|
84
|
+
end: { x: toX, y: toY },
|
|
85
|
+
strokeWidth,
|
|
86
|
+
stroke: color,
|
|
87
|
+
realToCanvasMat: realToCanvasMat,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Draw the dimension line
|
|
92
|
+
drawLine({
|
|
93
|
+
ctx,
|
|
94
|
+
start: { x: fromX, y: fromY },
|
|
95
|
+
end: { x: toX, y: toY },
|
|
96
|
+
strokeWidth,
|
|
97
|
+
stroke: color,
|
|
98
|
+
realToCanvasMat: realToCanvasMat,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Draw arrows at both ends
|
|
102
|
+
const [canvasFromX, canvasFromY] = applyToPoint(realToCanvasMat, [
|
|
103
|
+
fromX,
|
|
104
|
+
fromY,
|
|
105
|
+
])
|
|
106
|
+
const [canvasToX, canvasToY] = applyToPoint(realToCanvasMat, [toX, toY])
|
|
107
|
+
// Calculate angle for arrows in canvas coordinates
|
|
108
|
+
const canvasDx = canvasToX - canvasFromX
|
|
109
|
+
const canvasDy = canvasToY - canvasFromY
|
|
110
|
+
const lineAngle = Math.atan2(canvasDy, canvasDx)
|
|
111
|
+
const scale = Math.abs(realToCanvasMat.a)
|
|
112
|
+
const scaledArrowSize = arrowSize * scale
|
|
113
|
+
const scaledStrokeWidth = strokeWidth * scale
|
|
114
|
+
|
|
115
|
+
// Arrow at 'from' point (pointing outward, away from the line center)
|
|
116
|
+
// This means pointing in the direction opposite to 'to'
|
|
117
|
+
drawArrow({
|
|
118
|
+
ctx,
|
|
119
|
+
x: canvasFromX,
|
|
120
|
+
y: canvasFromY,
|
|
121
|
+
angle: lineAngle + Math.PI,
|
|
122
|
+
arrowSize: scaledArrowSize,
|
|
123
|
+
color,
|
|
124
|
+
strokeWidth: scaledStrokeWidth,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Arrow at 'to' point (pointing outward, away from the line center)
|
|
128
|
+
// This means pointing in the direction toward 'to' (away from 'from')
|
|
129
|
+
drawArrow({
|
|
130
|
+
ctx,
|
|
131
|
+
x: canvasToX,
|
|
132
|
+
y: canvasToY,
|
|
133
|
+
angle: lineAngle,
|
|
134
|
+
arrowSize: scaledArrowSize,
|
|
135
|
+
color,
|
|
136
|
+
strokeWidth: scaledStrokeWidth,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Draw text if provided
|
|
140
|
+
if (pcbNoteDimension.text) {
|
|
141
|
+
// Calculate text position (midpoint of the dimension line)
|
|
142
|
+
// The line endpoints are already offset if offset was provided
|
|
143
|
+
let textX = (fromX + toX) / 2
|
|
144
|
+
let textY = (fromY + toY) / 2
|
|
145
|
+
|
|
146
|
+
// Offset text perpendicular to the dimension line so it appears above/outside
|
|
147
|
+
// Calculate perpendicular vector (rotate line direction by 90 degrees CW)
|
|
148
|
+
// For a line from (fromX, fromY) to (toX, toY), perpendicular is (dy, -dx)
|
|
149
|
+
// This ensures text appears above horizontal lines and to the right of vertical lines
|
|
150
|
+
const perpX = toY - fromY
|
|
151
|
+
const perpY = -(toX - fromX)
|
|
152
|
+
const perpLength = Math.sqrt(perpX * perpX + perpY * perpY)
|
|
153
|
+
|
|
154
|
+
// Normalize and offset by font size (plus a small gap)
|
|
155
|
+
if (perpLength > 0) {
|
|
156
|
+
const offsetDistance = pcbNoteDimension.font_size * 1.5 // Offset by 1.5x font size
|
|
157
|
+
const normalizedPerpX = perpX / perpLength
|
|
158
|
+
const normalizedPerpY = perpY / perpLength
|
|
159
|
+
textX += normalizedPerpX * offsetDistance
|
|
160
|
+
textY += normalizedPerpY * offsetDistance
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Calculate rotation (displayed CCW degrees). If the caller provided
|
|
164
|
+
// `text_ccw_rotation` use that directly; otherwise align with the line
|
|
165
|
+
// angle and keep the text upright by folding into [-90, 90]. `drawText`
|
|
166
|
+
// expects a rotation value that it will negate internally, so we pass
|
|
167
|
+
// `-deg` below.
|
|
168
|
+
// Compute the displayed CCW degrees. Use the explicit `text_ccw_rotation`
|
|
169
|
+
// when provided; otherwise derive from the line angle and fold into
|
|
170
|
+
// [-90, 90] so text stays upright. Finally, `drawText` negates the
|
|
171
|
+
// provided rotation when applying it to the canvas, so pass the
|
|
172
|
+
// negative of the displayed CCW degrees.
|
|
173
|
+
const textRotation = -(() => {
|
|
174
|
+
const raw =
|
|
175
|
+
pcbNoteDimension.text_ccw_rotation ?? (lineAngle * 180) / Math.PI
|
|
176
|
+
|
|
177
|
+
if (pcbNoteDimension.text_ccw_rotation !== undefined) return raw
|
|
178
|
+
|
|
179
|
+
// Normalize to [-180, 180]
|
|
180
|
+
let deg = ((raw + 180) % 360) - 180
|
|
181
|
+
|
|
182
|
+
// Fold into [-90, 90]
|
|
183
|
+
if (deg > 90) deg -= 180
|
|
184
|
+
if (deg < -90) deg += 180
|
|
185
|
+
|
|
186
|
+
return deg
|
|
187
|
+
})()
|
|
188
|
+
|
|
189
|
+
drawText({
|
|
190
|
+
ctx,
|
|
191
|
+
text: pcbNoteDimension.text,
|
|
192
|
+
x: textX,
|
|
193
|
+
y: textY,
|
|
194
|
+
fontSize: pcbNoteDimension.font_size,
|
|
195
|
+
color,
|
|
196
|
+
realToCanvasMat: realToCanvasMat,
|
|
197
|
+
anchorAlignment: "center",
|
|
198
|
+
rotation: textRotation,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -6,12 +6,12 @@ import type {
|
|
|
6
6
|
PcbSilkscreenPath,
|
|
7
7
|
} from "circuit-json"
|
|
8
8
|
import type { Matrix } from "transformation-matrix"
|
|
9
|
-
import { applyToPoint } from "transformation-matrix"
|
|
10
9
|
import type { PcbColorMap, CanvasContext } from "../types"
|
|
11
10
|
import { drawRect } from "../shapes/rect"
|
|
12
11
|
import { drawCircle } from "../shapes/circle"
|
|
13
12
|
import { drawLine } from "../shapes/line"
|
|
14
13
|
import { drawPath } from "../shapes/path"
|
|
14
|
+
import { drawText } from "../shapes/text"
|
|
15
15
|
|
|
16
16
|
export interface DrawPcbSilkscreenTextParams {
|
|
17
17
|
ctx: CanvasContext
|
|
@@ -54,42 +54,27 @@ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
|
|
|
54
54
|
: colorMap.silkscreen.top
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function mapAnchorAlignment(
|
|
58
|
-
alignment?: string,
|
|
59
|
-
): "start" | "end" | "left" | "right" | "center" {
|
|
60
|
-
if (!alignment) return "center"
|
|
61
|
-
if (alignment.includes("left")) return "left"
|
|
62
|
-
if (alignment.includes("right")) return "right"
|
|
63
|
-
return "center"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
export function drawPcbSilkscreenText(
|
|
67
58
|
params: DrawPcbSilkscreenTextParams,
|
|
68
59
|
): void {
|
|
69
60
|
const { ctx, text, realToCanvasMat, colorMap } = params
|
|
70
61
|
|
|
71
62
|
const color = layerToSilkscreenColor(text.layer, colorMap)
|
|
72
|
-
const
|
|
73
|
-
text.anchor_position.x,
|
|
74
|
-
text.anchor_position.y,
|
|
75
|
-
])
|
|
76
|
-
|
|
77
|
-
const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a)
|
|
63
|
+
const fontSize = text.font_size ?? 1
|
|
78
64
|
const rotation = text.ccw_rotation ?? 0
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ctx.restore()
|
|
66
|
+
// Use @tscircuit/alphabet to draw text (font-independent, stroke-based rendering)
|
|
67
|
+
drawText({
|
|
68
|
+
ctx,
|
|
69
|
+
text: text.text,
|
|
70
|
+
x: text.anchor_position.x,
|
|
71
|
+
y: text.anchor_position.y,
|
|
72
|
+
fontSize,
|
|
73
|
+
color,
|
|
74
|
+
realToCanvasMat,
|
|
75
|
+
anchorAlignment: text.anchor_alignment ?? "center",
|
|
76
|
+
rotation,
|
|
77
|
+
})
|
|
93
78
|
}
|
|
94
79
|
|
|
95
80
|
export function drawPcbSilkscreenRect(
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CanvasContext } from "../types"
|
|
2
|
+
|
|
3
|
+
export interface DrawArrowParams {
|
|
4
|
+
ctx: CanvasContext
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
angle: number
|
|
8
|
+
arrowSize: number
|
|
9
|
+
color: string
|
|
10
|
+
strokeWidth: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Draw an arrow at a point along a line
|
|
15
|
+
*/
|
|
16
|
+
export function drawArrow(params: DrawArrowParams): void {
|
|
17
|
+
const { ctx, x, y, angle, arrowSize, color, strokeWidth } = params
|
|
18
|
+
|
|
19
|
+
ctx.save()
|
|
20
|
+
ctx.translate(x, y)
|
|
21
|
+
ctx.rotate(angle)
|
|
22
|
+
|
|
23
|
+
ctx.beginPath()
|
|
24
|
+
ctx.moveTo(0, 0)
|
|
25
|
+
ctx.lineTo(-arrowSize, -arrowSize / 2)
|
|
26
|
+
ctx.moveTo(0, 0)
|
|
27
|
+
ctx.lineTo(-arrowSize, arrowSize / 2)
|
|
28
|
+
|
|
29
|
+
ctx.lineWidth = strokeWidth
|
|
30
|
+
ctx.strokeStyle = color
|
|
31
|
+
ctx.lineCap = "round"
|
|
32
|
+
ctx.lineJoin = "round"
|
|
33
|
+
ctx.stroke()
|
|
34
|
+
|
|
35
|
+
ctx.restore()
|
|
36
|
+
}
|
|
@@ -5,6 +5,7 @@ export { drawPill, type DrawPillParams } from "./pill"
|
|
|
5
5
|
export { drawPolygon, type DrawPolygonParams } from "./polygon"
|
|
6
6
|
export { drawLine, type DrawLineParams } from "./line"
|
|
7
7
|
export { drawPath, type DrawPathParams } from "./path"
|
|
8
|
+
export { drawArrow, type DrawArrowParams } from "./arrow"
|
|
8
9
|
export {
|
|
9
10
|
drawText,
|
|
10
11
|
type DrawTextParams,
|
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.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@biomejs/biome": "^2.3.8",
|
|
13
13
|
"@napi-rs/canvas": "^0.1.84",
|
|
14
14
|
"@resvg/resvg-js": "^2.6.2",
|
|
15
|
-
"@tscircuit/alphabet": "^0.0.
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbNoteDimension } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note dimension - angled", async () => {
|
|
7
|
+
const width = 240
|
|
8
|
+
const height = 160
|
|
9
|
+
const dpr = 2
|
|
10
|
+
const canvas = createCanvas(width * dpr, height * dpr)
|
|
11
|
+
const ctx = canvas.getContext("2d")
|
|
12
|
+
ctx.scale(dpr, dpr)
|
|
13
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
14
|
+
|
|
15
|
+
// Background
|
|
16
|
+
ctx.fillStyle = "#1a1a1a"
|
|
17
|
+
ctx.fillRect(0, 0, width, height)
|
|
18
|
+
|
|
19
|
+
const angledDim: PcbNoteDimension = {
|
|
20
|
+
type: "pcb_note_dimension",
|
|
21
|
+
pcb_note_dimension_id: "note_dimension_angled_1",
|
|
22
|
+
from: { x: 40, y: 120 },
|
|
23
|
+
to: { x: 200, y: 40 }, // angled up-right
|
|
24
|
+
arrow_size: 6,
|
|
25
|
+
font_size: 8,
|
|
26
|
+
text: "sqrt( (160)^2 + (80)^2 )",
|
|
27
|
+
font: "tscircuit2024",
|
|
28
|
+
// slight offset so extension lines are visible and text sits off the line
|
|
29
|
+
offset_distance: 12,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
drawer.drawElements([angledDim])
|
|
33
|
+
|
|
34
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
35
|
+
import.meta.path,
|
|
36
|
+
)
|
|
37
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbNoteDimension } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note dimension - basic", async () => {
|
|
7
|
+
const width = 200
|
|
8
|
+
const height = 100
|
|
9
|
+
const dpr = 2
|
|
10
|
+
const canvas = createCanvas(width * dpr, height * dpr)
|
|
11
|
+
const ctx = canvas.getContext("2d")
|
|
12
|
+
ctx.scale(dpr, dpr)
|
|
13
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
14
|
+
|
|
15
|
+
// Background
|
|
16
|
+
ctx.fillStyle = "#1a1a1a"
|
|
17
|
+
// Use logical dimensions when filling background (canvas is scaled)
|
|
18
|
+
ctx.fillRect(0, 0, width, height)
|
|
19
|
+
|
|
20
|
+
const dim: PcbNoteDimension = {
|
|
21
|
+
type: "pcb_note_dimension",
|
|
22
|
+
pcb_note_dimension_id: "note_dimension_basic_1",
|
|
23
|
+
from: { x: 20, y: 50 },
|
|
24
|
+
to: { x: 180, y: 50 },
|
|
25
|
+
arrow_size: 4,
|
|
26
|
+
font_size: 6,
|
|
27
|
+
font: "tscircuit2024",
|
|
28
|
+
text: "160",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
drawer.drawElements([dim])
|
|
32
|
+
|
|
33
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
34
|
+
import.meta.path,
|
|
35
|
+
)
|
|
36
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbNoteDimension } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note dimension - vertical with rotation", async () => {
|
|
7
|
+
const width = 160
|
|
8
|
+
const height = 240
|
|
9
|
+
const dpr = 2
|
|
10
|
+
const canvas = createCanvas(width * dpr, height * dpr)
|
|
11
|
+
const ctx = canvas.getContext("2d")
|
|
12
|
+
ctx.scale(dpr, dpr)
|
|
13
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
14
|
+
|
|
15
|
+
// Background
|
|
16
|
+
ctx.fillStyle = "#1a1a1a"
|
|
17
|
+
ctx.fillRect(0, 0, width, height)
|
|
18
|
+
|
|
19
|
+
const verticalDim: PcbNoteDimension = {
|
|
20
|
+
type: "pcb_note_dimension",
|
|
21
|
+
pcb_note_dimension_id: "note_dimension_vertical_1",
|
|
22
|
+
from: { x: 80, y: 40 },
|
|
23
|
+
to: { x: 80, y: 200 }, // vertical line downwards
|
|
24
|
+
arrow_size: 6,
|
|
25
|
+
font_size: 9,
|
|
26
|
+
text: "160",
|
|
27
|
+
font: "tscircuit2024",
|
|
28
|
+
// Provide explicit text rotation (counter-clockwise degrees),
|
|
29
|
+
// which should align text along the vertical dimension.
|
|
30
|
+
text_ccw_rotation: 90,
|
|
31
|
+
// Offset horizontally so the dimension line sits right of the points,
|
|
32
|
+
// and extension lines from the points are drawn to the dimension line.
|
|
33
|
+
offset_distance: 14,
|
|
34
|
+
offset_direction: { x: 1, y: 0 },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
drawer.drawElements([verticalDim])
|
|
38
|
+
|
|
39
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
40
|
+
import.meta.path,
|
|
41
|
+
)
|
|
42
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbNoteDimension } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note dimension - with offset", async () => {
|
|
7
|
+
const width = 200
|
|
8
|
+
const height = 120
|
|
9
|
+
const dpr = 2
|
|
10
|
+
const canvas = createCanvas(width * dpr, height * dpr)
|
|
11
|
+
const ctx = canvas.getContext("2d")
|
|
12
|
+
ctx.scale(dpr, dpr)
|
|
13
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
14
|
+
|
|
15
|
+
// Background
|
|
16
|
+
ctx.fillStyle = "#1a1a1a"
|
|
17
|
+
ctx.fillRect(0, 0, width, height)
|
|
18
|
+
|
|
19
|
+
const dimWithOffset: PcbNoteDimension = {
|
|
20
|
+
type: "pcb_note_dimension",
|
|
21
|
+
pcb_note_dimension_id: "note_dimension_offset_1",
|
|
22
|
+
from: { x: 40, y: 70 },
|
|
23
|
+
to: { x: 160, y: 70 },
|
|
24
|
+
arrow_size: 5,
|
|
25
|
+
font_size: 7,
|
|
26
|
+
text: "120",
|
|
27
|
+
font: "tscircuit2024",
|
|
28
|
+
// Offset the dimension line along a custom direction, ensuring extension lines are drawn
|
|
29
|
+
offset_distance: 10,
|
|
30
|
+
offset_direction: { x: 0, y: -1 }, // offset upward by 10 units
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
drawer.drawElements([dimWithOffset])
|
|
34
|
+
|
|
35
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
36
|
+
import.meta.path,
|
|
37
|
+
)
|
|
38
|
+
})
|
|
@@ -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
|
}
|