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 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
- export { type AlphabetLayout, type AnchorAlignment, type CameraBounds, type CanvasContext, CircuitToCanvasDrawer, type CopperColorMap, type CopperLayerName, DEFAULT_PCB_COLOR_MAP, 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 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, drawCircle, drawLine, drawOval, drawPath, drawPcbBoard, drawPcbCopperPour, drawPcbCopperText, drawPcbCutout, drawPcbFabricationNotePath, drawPcbFabricationNoteRect, drawPcbFabricationNoteText, drawPcbHole, drawPcbNotePath, drawPcbNoteRect, drawPcbNoteText, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenPath, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
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/elements/pcb-silkscreen.ts
650
+ // lib/drawer/shapes/text/text.ts
651
+ import { lineAlphabet } from "@tscircuit/alphabet";
651
652
  import { applyToPoint as applyToPoint8 } from "transformation-matrix";
652
- function layerToSilkscreenColor(layer, colorMap) {
653
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
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
- function mapAnchorAlignment(alignment) {
656
- if (!alignment) return "center";
657
- if (alignment.includes("left")) return "left";
658
- if (alignment.includes("right")) return "right";
659
- return "center";
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
- function drawPcbSilkscreenText(params) {
662
- const { ctx, text, realToCanvasMat, colorMap } = params;
663
- const color = layerToSilkscreenColor(text.layer, colorMap);
664
- const [x, y] = applyToPoint8(realToCanvasMat, [
665
- text.anchor_position.x,
666
- text.anchor_position.y
667
- ]);
668
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a);
669
- const rotation = text.ccw_rotation ?? 0;
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(x, y);
753
+ ctx.translate(canvasX, canvasY);
672
754
  if (rotation !== 0) {
673
755
  ctx.rotate(-rotation * (Math.PI / 180));
674
756
  }
675
- ctx.font = `${fontSize}px sans-serif`;
676
- ctx.fillStyle = color;
677
- ctx.textAlign = mapAnchorAlignment(text.anchor_alignment);
678
- ctx.fillText(text.text, 0, 0);
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 mapAnchorAlignment2(alignment) {
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] = applyToPoint11(realToCanvasMat, [
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 = mapAnchorAlignment2(text.anchor_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
  }
@@ -67,3 +67,8 @@ export {
67
67
  drawPcbNoteText,
68
68
  type DrawPcbNoteTextParams,
69
69
  } from "./pcb-note-text"
70
+
71
+ export {
72
+ drawPcbNoteDimension,
73
+ type DrawPcbNoteDimensionParams,
74
+ } from "./pcb-note-dimension"
@@ -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 [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)
63
+ const fontSize = text.font_size ?? 1
78
64
  const rotation = text.ccw_rotation ?? 0
79
65
 
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()
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.18",
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.9",
15
+ "@tscircuit/alphabet": "^0.0.17",
16
16
  "@tscircuit/circuit-json-util": "^0.0.73",
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
@@ -5,7 +5,7 @@ import usbcFlashlightCircuit from "./usb-c-flashlight.json"
5
5
 
6
6
  const circuitElements = usbcFlashlightCircuit as AnyCircuitElement[]
7
7
 
8
- test.skip("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
8
+ test("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
9
9
  const stackedPng = await getStackedPngSvgComparison(circuitElements, {
10
10
  width: 400,
11
11
  height: 800,
@@ -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 = (svg: string): Buffer => {
80
- const resvg = new Resvg(svg)
81
- return resvg.render().asPng()
81
+ export const svgToPng = (svgString: string): Buffer => {
82
+ const fontBuffer = Buffer.from(tscircuitFont, "base64")
83
+
84
+ let tempFontPath: string | undefined
85
+ let cleanupFn: (() => void) | undefined
86
+
87
+ try {
88
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "resvg-font-"))
89
+ tempFontPath = path.join(tempDir, "tscircuit-font.ttf")
90
+ fs.writeFileSync(tempFontPath, fontBuffer)
91
+
92
+ cleanupFn = () => {
93
+ try {
94
+ fs.unlinkSync(tempFontPath!)
95
+ } catch {
96
+ // Ignore errors during cleanup
97
+ }
98
+ }
99
+
100
+ const opts: ResvgRenderOptions = {
101
+ font: {
102
+ fontFiles: [tempFontPath],
103
+ loadSystemFonts: false,
104
+ defaultFontFamily: "TscircuitAlphabet",
105
+ monospaceFamily: "TscircuitAlphabet",
106
+ sansSerifFamily: "TscircuitAlphabet",
107
+ },
108
+ }
109
+
110
+ const resvg = new Resvg(svgString, opts)
111
+ const pngData = resvg.render()
112
+ const pngBuffer = pngData.asPng()
113
+
114
+ return Buffer.from(pngBuffer)
115
+ } finally {
116
+ // Clean up temporary font file
117
+ if (cleanupFn) {
118
+ cleanupFn()
119
+ }
120
+ }
82
121
  }