circuit-to-canvas 0.0.4 → 0.0.6
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 +12 -2
- package/dist/index.js +77 -21
- package/lib/drawer/CircuitToCanvasDrawer.ts +11 -0
- package/lib/drawer/elements/index.ts +5 -0
- package/lib/drawer/elements/pcb-copper-text.ts +10 -5
- package/lib/drawer/elements/pcb-note-rect.ts +37 -0
- package/lib/drawer/shapes/text.ts +62 -20
- package/package.json +1 -1
- package/tests/elements/__snapshots__/fabrication-note-text-baseline-anchors.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-baseline.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-descenders.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-text-rgba-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-text-small.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-rect-all-features.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-rect-dashed-stroke.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-rect-filled-no-stroke.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-silkscreen.snap.png +0 -0
- package/tests/elements/__snapshots__/silkscreen-text-bottom.snap.png +0 -0
- package/tests/elements/pcb-fabrication-note-text-baseline-anchors.test.ts +80 -0
- package/tests/elements/pcb-fabrication-note-text-baseline.test.ts +43 -0
- package/tests/elements/pcb-fabrication-note-text-descenders.test.ts +43 -0
- package/tests/elements/pcb-fabrication-note-text-small.test.ts +1 -1
- package/tests/elements/pcb-note-rect-all-features.test.ts +38 -0
- package/tests/elements/pcb-note-rect-dashed-stroke.test.ts +32 -0
- package/tests/elements/pcb-note-rect-filled-no-stroke.test.ts +32 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyCircuitElement, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbFabricationNotePath } from 'circuit-json';
|
|
1
|
+
import { AnyCircuitElement, PcbPlatedHole, PCBVia, PCBHole, PcbSmtPad, PCBTrace, PcbBoard, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbCutout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath } from 'circuit-json';
|
|
2
2
|
import { Matrix } from 'transformation-matrix';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -210,6 +210,8 @@ type AlphabetLayout = {
|
|
|
210
210
|
letterSpacing: number;
|
|
211
211
|
spaceWidth: number;
|
|
212
212
|
strokeWidth: number;
|
|
213
|
+
baselineOffset: number;
|
|
214
|
+
descenderDepth: number;
|
|
213
215
|
};
|
|
214
216
|
declare function getAlphabetLayout(text: string, fontSize: number): AlphabetLayout;
|
|
215
217
|
type AnchorAlignment = "center" | "top_left" | "top_right" | "bottom_left" | "bottom_right" | "left" | "right" | "top" | "bottom";
|
|
@@ -355,6 +357,14 @@ interface DrawPcbFabricationNoteRectParams {
|
|
|
355
357
|
}
|
|
356
358
|
declare function drawPcbFabricationNoteRect(params: DrawPcbFabricationNoteRectParams): void;
|
|
357
359
|
|
|
360
|
+
interface DrawPcbNoteRectParams {
|
|
361
|
+
ctx: CanvasContext;
|
|
362
|
+
rect: PcbNoteRect;
|
|
363
|
+
transform: Matrix;
|
|
364
|
+
colorMap: PcbColorMap;
|
|
365
|
+
}
|
|
366
|
+
declare function drawPcbNoteRect(params: DrawPcbNoteRectParams): void;
|
|
367
|
+
|
|
358
368
|
interface DrawPcbFabricationNotePathParams {
|
|
359
369
|
ctx: CanvasContext;
|
|
360
370
|
path: PcbFabricationNotePath;
|
|
@@ -363,4 +373,4 @@ interface DrawPcbFabricationNotePathParams {
|
|
|
363
373
|
}
|
|
364
374
|
declare function drawPcbFabricationNotePath(params: DrawPcbFabricationNotePathParams): void;
|
|
365
375
|
|
|
366
|
-
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 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, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenPath, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
|
|
376
|
+
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 DrawPcbNoteRectParams, 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, drawPcbNoteRect, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenPath, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
|
package/dist/index.js
CHANGED
|
@@ -821,6 +821,16 @@ var LETTER_SPACING_RATIO = 0.3;
|
|
|
821
821
|
var SPACE_WIDTH_RATIO = 1;
|
|
822
822
|
var STROKE_WIDTH_RATIO = 0.13;
|
|
823
823
|
var CURVED_GLYPHS = /* @__PURE__ */ new Set(["O", "o", "0"]);
|
|
824
|
+
var LOWERCASE_BASELINE_OFFSET = (() => {
|
|
825
|
+
const referenceLetters = ["a", "c", "e", "m", "n", "o", "r", "s", "u", "x"];
|
|
826
|
+
const offsets = referenceLetters.map((letter) => lineAlphabet[letter]).filter(
|
|
827
|
+
(lines) => lines !== void 0 && lines.length > 0
|
|
828
|
+
).map(
|
|
829
|
+
(lines) => Math.min(...lines.map((line) => Math.min(line.y1, line.y2)))
|
|
830
|
+
);
|
|
831
|
+
return offsets.length > 0 ? Math.min(...offsets) : 0;
|
|
832
|
+
})();
|
|
833
|
+
var getBaselineOffsetForLetter = (letter) => letter >= "a" && letter <= "z" ? LOWERCASE_BASELINE_OFFSET : 0;
|
|
824
834
|
function getAlphabetLayout(text, fontSize) {
|
|
825
835
|
const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
|
|
826
836
|
const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
|
|
@@ -833,19 +843,24 @@ function getAlphabetLayout(text, fontSize) {
|
|
|
833
843
|
if (index < characters.length - 1) width += letterSpacing;
|
|
834
844
|
});
|
|
835
845
|
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
|
|
846
|
+
const hasLowercase = /[a-z]/.test(text);
|
|
847
|
+
const baselineOffset = hasLowercase ? (1 - LOWERCASE_BASELINE_OFFSET) * fontSize : fontSize;
|
|
848
|
+
const descenderDepth = hasLowercase ? LOWERCASE_BASELINE_OFFSET * fontSize : 0;
|
|
836
849
|
return {
|
|
837
850
|
width,
|
|
838
851
|
height: fontSize,
|
|
839
852
|
glyphWidth,
|
|
840
853
|
letterSpacing,
|
|
841
854
|
spaceWidth,
|
|
842
|
-
strokeWidth
|
|
855
|
+
strokeWidth,
|
|
856
|
+
baselineOffset,
|
|
857
|
+
descenderDepth
|
|
843
858
|
};
|
|
844
859
|
}
|
|
845
860
|
var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
|
|
846
861
|
function getTextStartPosition(alignment, layout) {
|
|
847
862
|
const totalWidth = layout.width + layout.strokeWidth;
|
|
848
|
-
const totalHeight = layout.height + layout.strokeWidth;
|
|
863
|
+
const totalHeight = layout.height + layout.descenderDepth + layout.strokeWidth;
|
|
849
864
|
let x = 0;
|
|
850
865
|
let y = 0;
|
|
851
866
|
if (alignment === "center") {
|
|
@@ -856,26 +871,38 @@ function getTextStartPosition(alignment, layout) {
|
|
|
856
871
|
x = -totalWidth;
|
|
857
872
|
}
|
|
858
873
|
if (alignment === "center") {
|
|
859
|
-
y = -
|
|
874
|
+
y = layout.baselineOffset - layout.height / 2;
|
|
860
875
|
} else if (alignment === "top_left" || alignment === "top_right" || alignment === "top") {
|
|
861
|
-
y =
|
|
876
|
+
y = layout.baselineOffset;
|
|
862
877
|
} else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom") {
|
|
863
|
-
y = -
|
|
878
|
+
y = -layout.descenderDepth;
|
|
879
|
+
} else {
|
|
880
|
+
y = 0;
|
|
864
881
|
}
|
|
865
882
|
return { x, y };
|
|
866
883
|
}
|
|
867
884
|
function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
868
|
-
const {
|
|
869
|
-
|
|
885
|
+
const {
|
|
886
|
+
glyphWidth,
|
|
887
|
+
letterSpacing,
|
|
888
|
+
spaceWidth,
|
|
889
|
+
height,
|
|
890
|
+
strokeWidth,
|
|
891
|
+
baselineOffset
|
|
892
|
+
} = layout;
|
|
893
|
+
const baselineY = startY;
|
|
870
894
|
const characters = Array.from(text);
|
|
871
895
|
let cursor = startX + strokeWidth / 2;
|
|
872
896
|
characters.forEach((char, index) => {
|
|
873
897
|
const lines = getGlyphLines(char);
|
|
874
898
|
const advance = char === " " ? spaceWidth : glyphWidth;
|
|
899
|
+
const charBaselineOffset = getBaselineOffsetForLetter(char);
|
|
875
900
|
if (CURVED_GLYPHS.has(char)) {
|
|
901
|
+
const normalizedCenterY = 0.5;
|
|
902
|
+
const adjustedCenterY = normalizedCenterY - charBaselineOffset;
|
|
903
|
+
const centerY = baselineY - adjustedCenterY * height;
|
|
876
904
|
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth);
|
|
877
905
|
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth);
|
|
878
|
-
const centerY = yOffset - height / 2;
|
|
879
906
|
ctx.beginPath();
|
|
880
907
|
ctx.ellipse(
|
|
881
908
|
cursor + glyphWidth / 2,
|
|
@@ -890,10 +917,12 @@ function strokeAlphabetText(ctx, text, layout, startX, startY) {
|
|
|
890
917
|
} else if (lines?.length) {
|
|
891
918
|
ctx.beginPath();
|
|
892
919
|
for (const line of lines) {
|
|
920
|
+
const adjusted_y1 = line.y1 - charBaselineOffset;
|
|
921
|
+
const adjusted_y2 = line.y2 - charBaselineOffset;
|
|
893
922
|
const x1 = cursor + line.x1 * glyphWidth;
|
|
894
|
-
const y1 =
|
|
923
|
+
const y1 = baselineY - adjusted_y1 * height;
|
|
895
924
|
const x2 = cursor + line.x2 * glyphWidth;
|
|
896
|
-
const y2 =
|
|
925
|
+
const y2 = baselineY - adjusted_y2 * height;
|
|
897
926
|
ctx.moveTo(x1, y1);
|
|
898
927
|
ctx.lineTo(x2, y2);
|
|
899
928
|
}
|
|
@@ -932,13 +961,7 @@ function drawText(params) {
|
|
|
932
961
|
ctx.lineCap = "round";
|
|
933
962
|
ctx.lineJoin = "round";
|
|
934
963
|
ctx.strokeStyle = color;
|
|
935
|
-
strokeAlphabetText(
|
|
936
|
-
ctx,
|
|
937
|
-
text,
|
|
938
|
-
layout,
|
|
939
|
-
startPos.x,
|
|
940
|
-
startPos.y + layout.strokeWidth / 2
|
|
941
|
-
);
|
|
964
|
+
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
|
|
942
965
|
ctx.restore();
|
|
943
966
|
}
|
|
944
967
|
|
|
@@ -971,11 +994,10 @@ function drawPcbCopperText(params) {
|
|
|
971
994
|
const textColor = layerToCopperColor(text.layer, colorMap);
|
|
972
995
|
const layout = getAlphabetLayout(content, fontSize);
|
|
973
996
|
const totalWidth = layout.width + layout.strokeWidth;
|
|
974
|
-
const totalHeight = layout.height + layout.strokeWidth;
|
|
975
997
|
const alignment = mapAnchorAlignment2(text.anchor_alignment);
|
|
976
998
|
const startPos = getTextStartPosition(alignment, layout);
|
|
977
999
|
const startX = startPos.x;
|
|
978
|
-
const startY =
|
|
1000
|
+
const startY = startPos.y;
|
|
979
1001
|
ctx.save();
|
|
980
1002
|
ctx.translate(x, y);
|
|
981
1003
|
if (text.is_mirrored) ctx.scale(-1, 1);
|
|
@@ -988,10 +1010,13 @@ function drawPcbCopperText(params) {
|
|
|
988
1010
|
const paddingRight = padding.right * scale2;
|
|
989
1011
|
const paddingTop = padding.top * scale2;
|
|
990
1012
|
const paddingBottom = padding.bottom * scale2;
|
|
1013
|
+
const textBoxTop = startY - layout.baselineOffset - layout.strokeWidth / 2;
|
|
1014
|
+
const textBoxBottom = startY + layout.descenderDepth + layout.strokeWidth / 2;
|
|
1015
|
+
const textBoxHeight = textBoxBottom - textBoxTop;
|
|
991
1016
|
const xOffset = startX - paddingLeft;
|
|
992
|
-
const yOffset =
|
|
1017
|
+
const yOffset = textBoxTop - paddingTop;
|
|
993
1018
|
const knockoutWidth = totalWidth + paddingLeft + paddingRight;
|
|
994
|
-
const knockoutHeight =
|
|
1019
|
+
const knockoutHeight = textBoxHeight + paddingTop + paddingBottom;
|
|
995
1020
|
ctx.fillStyle = textColor;
|
|
996
1021
|
ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight);
|
|
997
1022
|
const previousCompositeOperation = ctx.globalCompositeOperation;
|
|
@@ -1055,6 +1080,28 @@ function drawPcbFabricationNoteRect(params) {
|
|
|
1055
1080
|
});
|
|
1056
1081
|
}
|
|
1057
1082
|
|
|
1083
|
+
// lib/drawer/elements/pcb-note-rect.ts
|
|
1084
|
+
function drawPcbNoteRect(params) {
|
|
1085
|
+
const { ctx, rect, transform, colorMap } = params;
|
|
1086
|
+
const defaultColor = "rgb(89, 148, 220)";
|
|
1087
|
+
const color = rect.color ?? defaultColor;
|
|
1088
|
+
const isFilled = rect.is_filled ?? false;
|
|
1089
|
+
const hasStroke = rect.has_stroke ?? true;
|
|
1090
|
+
const isStrokeDashed = rect.is_stroke_dashed ?? false;
|
|
1091
|
+
drawRect({
|
|
1092
|
+
ctx,
|
|
1093
|
+
center: rect.center,
|
|
1094
|
+
width: rect.width,
|
|
1095
|
+
height: rect.height,
|
|
1096
|
+
fill: isFilled ? color : void 0,
|
|
1097
|
+
stroke: hasStroke ? color : void 0,
|
|
1098
|
+
strokeWidth: hasStroke ? rect.stroke_width : void 0,
|
|
1099
|
+
borderRadius: rect.corner_radius,
|
|
1100
|
+
transform,
|
|
1101
|
+
isStrokeDashed
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1058
1105
|
// lib/drawer/elements/pcb-fabrication-note-path.ts
|
|
1059
1106
|
function drawPcbFabricationNotePath(params) {
|
|
1060
1107
|
const { ctx, path, transform, colorMap } = params;
|
|
@@ -1273,6 +1320,14 @@ var CircuitToCanvasDrawer = class {
|
|
|
1273
1320
|
colorMap: this.colorMap
|
|
1274
1321
|
});
|
|
1275
1322
|
}
|
|
1323
|
+
if (element.type === "pcb_note_rect") {
|
|
1324
|
+
drawPcbNoteRect({
|
|
1325
|
+
transform: this.realToCanvasMat,
|
|
1326
|
+
colorMap: this.colorMap,
|
|
1327
|
+
ctx: this.ctx,
|
|
1328
|
+
rect: element
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1276
1331
|
if (element.type === "pcb_fabrication_note_path") {
|
|
1277
1332
|
drawPcbFabricationNotePath({
|
|
1278
1333
|
ctx: this.ctx,
|
|
@@ -1298,6 +1353,7 @@ export {
|
|
|
1298
1353
|
drawPcbFabricationNoteRect,
|
|
1299
1354
|
drawPcbFabricationNoteText,
|
|
1300
1355
|
drawPcbHole,
|
|
1356
|
+
drawPcbNoteRect,
|
|
1301
1357
|
drawPcbPlatedHole,
|
|
1302
1358
|
drawPcbSilkscreenCircle,
|
|
1303
1359
|
drawPcbSilkscreenLine,
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
PcbCopperText,
|
|
17
17
|
PcbFabricationNoteText,
|
|
18
18
|
PcbFabricationNoteRect,
|
|
19
|
+
PcbNoteRect,
|
|
19
20
|
PcbFabricationNotePath,
|
|
20
21
|
} from "circuit-json"
|
|
21
22
|
import { identity, compose, translate, scale } from "transformation-matrix"
|
|
@@ -45,6 +46,7 @@ import { drawPcbCopperPour } from "./elements/pcb-copper-pour"
|
|
|
45
46
|
import { drawPcbCopperText } from "./elements/pcb-copper-text"
|
|
46
47
|
import { drawPcbFabricationNoteText } from "./elements/pcb-fabrication-note-text"
|
|
47
48
|
import { drawPcbFabricationNoteRect } from "./elements/pcb-fabrication-note-rect"
|
|
49
|
+
import { drawPcbNoteRect } from "./elements/pcb-note-rect"
|
|
48
50
|
import { drawPcbFabricationNotePath } from "./elements/pcb-fabrication-note-path"
|
|
49
51
|
|
|
50
52
|
export interface DrawElementsOptions {
|
|
@@ -288,6 +290,15 @@ export class CircuitToCanvasDrawer {
|
|
|
288
290
|
})
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
if (element.type === "pcb_note_rect") {
|
|
294
|
+
drawPcbNoteRect({
|
|
295
|
+
transform: this.realToCanvasMat,
|
|
296
|
+
colorMap: this.colorMap,
|
|
297
|
+
ctx: this.ctx,
|
|
298
|
+
rect: element as PcbNoteRect,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
291
302
|
if (element.type === "pcb_fabrication_note_path") {
|
|
292
303
|
drawPcbFabricationNotePath({
|
|
293
304
|
ctx: this.ctx,
|
|
@@ -48,6 +48,11 @@ export {
|
|
|
48
48
|
type DrawPcbFabricationNoteRectParams,
|
|
49
49
|
} from "./pcb-fabrication-note-rect"
|
|
50
50
|
|
|
51
|
+
export {
|
|
52
|
+
drawPcbNoteRect,
|
|
53
|
+
type DrawPcbNoteRectParams,
|
|
54
|
+
} from "./pcb-note-rect"
|
|
55
|
+
|
|
51
56
|
export {
|
|
52
57
|
drawPcbFabricationNotePath,
|
|
53
58
|
type DrawPcbFabricationNotePathParams,
|
|
@@ -52,12 +52,10 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
|
|
|
52
52
|
const textColor = layerToCopperColor(text.layer, colorMap)
|
|
53
53
|
const layout = getAlphabetLayout(content, fontSize)
|
|
54
54
|
const totalWidth = layout.width + layout.strokeWidth
|
|
55
|
-
const totalHeight = layout.height + layout.strokeWidth
|
|
56
55
|
const alignment = mapAnchorAlignment(text.anchor_alignment)
|
|
57
56
|
const startPos = getTextStartPosition(alignment, layout)
|
|
58
|
-
// Copper text always centers vertically (startY=0), uses startPos.x for horizontal alignment
|
|
59
57
|
const startX = startPos.x
|
|
60
|
-
const startY =
|
|
58
|
+
const startY = startPos.y
|
|
61
59
|
|
|
62
60
|
ctx.save()
|
|
63
61
|
ctx.translate(x, y)
|
|
@@ -73,10 +71,17 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
|
|
|
73
71
|
const paddingRight = padding.right * scale
|
|
74
72
|
const paddingTop = padding.top * scale
|
|
75
73
|
const paddingBottom = padding.bottom * scale
|
|
74
|
+
// Calculate knockout rectangle to cover the text box
|
|
75
|
+
// startY is the baseline position, text box extends from (baseline - baselineOffset) to (baseline + descenderDepth)
|
|
76
|
+
const textBoxTop = startY - layout.baselineOffset - layout.strokeWidth / 2
|
|
77
|
+
const textBoxBottom =
|
|
78
|
+
startY + layout.descenderDepth + layout.strokeWidth / 2
|
|
79
|
+
const textBoxHeight = textBoxBottom - textBoxTop
|
|
80
|
+
|
|
76
81
|
const xOffset = startX - paddingLeft
|
|
77
|
-
const yOffset =
|
|
82
|
+
const yOffset = textBoxTop - paddingTop
|
|
78
83
|
const knockoutWidth = totalWidth + paddingLeft + paddingRight
|
|
79
|
-
const knockoutHeight =
|
|
84
|
+
const knockoutHeight = textBoxHeight + paddingTop + paddingBottom
|
|
80
85
|
|
|
81
86
|
ctx.fillStyle = textColor
|
|
82
87
|
ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PcbNoteRect } 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 DrawPcbNoteRectParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
rect: PcbNoteRect
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawPcbNoteRect(params: DrawPcbNoteRectParams): void {
|
|
14
|
+
const { ctx, rect, transform, colorMap } = params
|
|
15
|
+
|
|
16
|
+
// Use the color from the rect if provided, otherwise use a default color
|
|
17
|
+
// Notes are typically shown in a distinct color
|
|
18
|
+
const defaultColor = "rgb(89, 148, 220)" // White color for notes
|
|
19
|
+
const color = rect.color ?? defaultColor
|
|
20
|
+
|
|
21
|
+
const isFilled = rect.is_filled ?? false
|
|
22
|
+
const hasStroke = rect.has_stroke ?? true
|
|
23
|
+
const isStrokeDashed = rect.is_stroke_dashed ?? false
|
|
24
|
+
|
|
25
|
+
drawRect({
|
|
26
|
+
ctx,
|
|
27
|
+
center: rect.center,
|
|
28
|
+
width: rect.width,
|
|
29
|
+
height: rect.height,
|
|
30
|
+
fill: isFilled ? color : undefined,
|
|
31
|
+
stroke: hasStroke ? color : undefined,
|
|
32
|
+
strokeWidth: hasStroke ? rect.stroke_width : undefined,
|
|
33
|
+
borderRadius: rect.corner_radius,
|
|
34
|
+
transform,
|
|
35
|
+
isStrokeDashed,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -9,6 +9,25 @@ const SPACE_WIDTH_RATIO = 1
|
|
|
9
9
|
const STROKE_WIDTH_RATIO = 0.13
|
|
10
10
|
const CURVED_GLYPHS = new Set(["O", "o", "0"])
|
|
11
11
|
|
|
12
|
+
// Calculate baseline offset from reference lowercase letters (same as working implementation)
|
|
13
|
+
const LOWERCASE_BASELINE_OFFSET = (() => {
|
|
14
|
+
const referenceLetters = ["a", "c", "e", "m", "n", "o", "r", "s", "u", "x"]
|
|
15
|
+
const offsets = referenceLetters
|
|
16
|
+
.map((letter) => lineAlphabet[letter])
|
|
17
|
+
.filter(
|
|
18
|
+
(lines): lines is NonNullable<typeof lines> =>
|
|
19
|
+
lines !== undefined && lines.length > 0,
|
|
20
|
+
)
|
|
21
|
+
.map((lines) =>
|
|
22
|
+
Math.min(...lines.map((line) => Math.min(line.y1, line.y2))),
|
|
23
|
+
)
|
|
24
|
+
return offsets.length > 0 ? Math.min(...offsets) : 0
|
|
25
|
+
})()
|
|
26
|
+
|
|
27
|
+
// Get baseline offset for a specific letter (only lowercase letters get offset)
|
|
28
|
+
const getBaselineOffsetForLetter = (letter: string) =>
|
|
29
|
+
letter >= "a" && letter <= "z" ? LOWERCASE_BASELINE_OFFSET : 0
|
|
30
|
+
|
|
12
31
|
export type AlphabetLayout = {
|
|
13
32
|
width: number
|
|
14
33
|
height: number
|
|
@@ -16,6 +35,8 @@ export type AlphabetLayout = {
|
|
|
16
35
|
letterSpacing: number
|
|
17
36
|
spaceWidth: number
|
|
18
37
|
strokeWidth: number
|
|
38
|
+
baselineOffset: number // Distance from top to baseline
|
|
39
|
+
descenderDepth: number // Distance from baseline to bottom (for descenders)
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
export function getAlphabetLayout(
|
|
@@ -35,6 +56,15 @@ export function getAlphabetLayout(
|
|
|
35
56
|
})
|
|
36
57
|
|
|
37
58
|
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35)
|
|
59
|
+
// Calculate baseline offset: distance from top of text box to baseline
|
|
60
|
+
// In normalized coords: y=0 is bottom, y=1 is top
|
|
61
|
+
// LOWERCASE_BASELINE_OFFSET is the minimum y (baseline position) in normalized coords
|
|
62
|
+
const hasLowercase = /[a-z]/.test(text)
|
|
63
|
+
const baselineOffset = hasLowercase
|
|
64
|
+
? (1 - LOWERCASE_BASELINE_OFFSET) * fontSize
|
|
65
|
+
: fontSize
|
|
66
|
+
// Descender depth: distance from baseline to bottom (for descenders like g, j, p, q, y)
|
|
67
|
+
const descenderDepth = hasLowercase ? LOWERCASE_BASELINE_OFFSET * fontSize : 0
|
|
38
68
|
|
|
39
69
|
return {
|
|
40
70
|
width,
|
|
@@ -43,6 +73,8 @@ export function getAlphabetLayout(
|
|
|
43
73
|
letterSpacing,
|
|
44
74
|
spaceWidth,
|
|
45
75
|
strokeWidth,
|
|
76
|
+
baselineOffset,
|
|
77
|
+
descenderDepth,
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
80
|
|
|
@@ -65,7 +97,8 @@ export function getTextStartPosition(
|
|
|
65
97
|
layout: AlphabetLayout,
|
|
66
98
|
): { x: number; y: number } {
|
|
67
99
|
const totalWidth = layout.width + layout.strokeWidth
|
|
68
|
-
|
|
100
|
+
// Total height includes descender depth for proper vertical alignment
|
|
101
|
+
const totalHeight = layout.height + layout.descenderDepth + layout.strokeWidth
|
|
69
102
|
|
|
70
103
|
let x = 0
|
|
71
104
|
let y = 0
|
|
@@ -87,21 +120,24 @@ export function getTextStartPosition(
|
|
|
87
120
|
x = -totalWidth
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
// Vertical alignment
|
|
123
|
+
// Vertical alignment - positions relative to baseline
|
|
124
|
+
// Text extends from (baseline - baselineOffset) at top to (baseline + descenderDepth) at bottom
|
|
91
125
|
if (alignment === "center") {
|
|
92
|
-
y = -
|
|
126
|
+
y = layout.baselineOffset - layout.height / 2
|
|
93
127
|
} else if (
|
|
94
128
|
alignment === "top_left" ||
|
|
95
129
|
alignment === "top_right" ||
|
|
96
130
|
alignment === "top"
|
|
97
131
|
) {
|
|
98
|
-
y =
|
|
132
|
+
y = layout.baselineOffset
|
|
99
133
|
} else if (
|
|
100
134
|
alignment === "bottom_left" ||
|
|
101
135
|
alignment === "bottom_right" ||
|
|
102
136
|
alignment === "bottom"
|
|
103
137
|
) {
|
|
104
|
-
y = -
|
|
138
|
+
y = -layout.descenderDepth
|
|
139
|
+
} else {
|
|
140
|
+
y = 0
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
return { x, y }
|
|
@@ -114,19 +150,31 @@ export function strokeAlphabetText(
|
|
|
114
150
|
startX: number,
|
|
115
151
|
startY: number,
|
|
116
152
|
): void {
|
|
117
|
-
const {
|
|
118
|
-
|
|
153
|
+
const {
|
|
154
|
+
glyphWidth,
|
|
155
|
+
letterSpacing,
|
|
156
|
+
spaceWidth,
|
|
157
|
+
height,
|
|
158
|
+
strokeWidth,
|
|
159
|
+
baselineOffset,
|
|
160
|
+
} = layout
|
|
161
|
+
const baselineY = startY
|
|
119
162
|
const characters = Array.from(text)
|
|
120
163
|
let cursor = startX + strokeWidth / 2
|
|
121
164
|
|
|
122
165
|
characters.forEach((char, index) => {
|
|
123
166
|
const lines = getGlyphLines(char)
|
|
124
167
|
const advance = char === " " ? spaceWidth : glyphWidth
|
|
168
|
+
// Get normalized baseline offset for this specific character (0-1 range)
|
|
169
|
+
const charBaselineOffset = getBaselineOffsetForLetter(char)
|
|
125
170
|
|
|
126
171
|
if (CURVED_GLYPHS.has(char)) {
|
|
172
|
+
// For curved glyphs, adjust coordinates by baseline offset
|
|
173
|
+
const normalizedCenterY = 0.5
|
|
174
|
+
const adjustedCenterY = normalizedCenterY - charBaselineOffset
|
|
175
|
+
const centerY = baselineY - adjustedCenterY * height
|
|
127
176
|
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth)
|
|
128
177
|
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth)
|
|
129
|
-
const centerY = yOffset - height / 2
|
|
130
178
|
ctx.beginPath()
|
|
131
179
|
ctx.ellipse(
|
|
132
180
|
cursor + glyphWidth / 2,
|
|
@@ -141,20 +189,20 @@ export function strokeAlphabetText(
|
|
|
141
189
|
} else if (lines?.length) {
|
|
142
190
|
ctx.beginPath()
|
|
143
191
|
for (const line of lines) {
|
|
192
|
+
// Convert normalized y coordinates to canvas coordinates (inverted for canvas)
|
|
193
|
+
const adjusted_y1 = line.y1 - charBaselineOffset
|
|
194
|
+
const adjusted_y2 = line.y2 - charBaselineOffset
|
|
144
195
|
const x1 = cursor + line.x1 * glyphWidth
|
|
145
|
-
const y1 =
|
|
196
|
+
const y1 = baselineY - adjusted_y1 * height
|
|
146
197
|
const x2 = cursor + line.x2 * glyphWidth
|
|
147
|
-
const y2 =
|
|
198
|
+
const y2 = baselineY - adjusted_y2 * height
|
|
148
199
|
ctx.moveTo(x1, y1)
|
|
149
200
|
ctx.lineTo(x2, y2)
|
|
150
201
|
}
|
|
151
202
|
ctx.stroke()
|
|
152
203
|
}
|
|
153
204
|
|
|
154
|
-
// Move cursor by the character width
|
|
155
205
|
cursor += advance
|
|
156
|
-
// Add letter spacing after each character except the last one
|
|
157
|
-
// This spacing will be before the next character, creating visible gaps
|
|
158
206
|
if (index < characters.length - 1) {
|
|
159
207
|
cursor += letterSpacing
|
|
160
208
|
}
|
|
@@ -206,13 +254,7 @@ export function drawText(params: DrawTextParams): void {
|
|
|
206
254
|
ctx.lineJoin = "round"
|
|
207
255
|
ctx.strokeStyle = color
|
|
208
256
|
|
|
209
|
-
strokeAlphabetText(
|
|
210
|
-
ctx,
|
|
211
|
-
text,
|
|
212
|
-
layout,
|
|
213
|
-
startPos.x,
|
|
214
|
-
startPos.y + layout.strokeWidth / 2,
|
|
215
|
-
)
|
|
257
|
+
strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y)
|
|
216
258
|
|
|
217
259
|
ctx.restore()
|
|
218
260
|
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw text baseline alignment with different anchor positions", async () => {
|
|
7
|
+
const SCALE = 4
|
|
8
|
+
const canvas = createCanvas(300 * SCALE, 200 * SCALE)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(SCALE, SCALE)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
|
|
13
|
+
ctx.fillStyle = "#1a1a1a"
|
|
14
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
15
|
+
|
|
16
|
+
// Draw reference lines
|
|
17
|
+
ctx.strokeStyle = "#444444"
|
|
18
|
+
ctx.lineWidth = 0.5
|
|
19
|
+
// Top line
|
|
20
|
+
ctx.beginPath()
|
|
21
|
+
ctx.moveTo(10, 50)
|
|
22
|
+
ctx.lineTo(290, 50)
|
|
23
|
+
ctx.stroke()
|
|
24
|
+
// Center/baseline line
|
|
25
|
+
ctx.beginPath()
|
|
26
|
+
ctx.moveTo(10, 100)
|
|
27
|
+
ctx.lineTo(290, 100)
|
|
28
|
+
ctx.stroke()
|
|
29
|
+
// Bottom line
|
|
30
|
+
ctx.beginPath()
|
|
31
|
+
ctx.moveTo(10, 150)
|
|
32
|
+
ctx.lineTo(290, 150)
|
|
33
|
+
ctx.stroke()
|
|
34
|
+
|
|
35
|
+
// Test with top alignment
|
|
36
|
+
const textTop: PcbFabricationNoteText = {
|
|
37
|
+
type: "pcb_fabrication_note_text",
|
|
38
|
+
pcb_fabrication_note_text_id: "fab-note-top",
|
|
39
|
+
pcb_component_id: "component1",
|
|
40
|
+
layer: "top",
|
|
41
|
+
text: "gap",
|
|
42
|
+
anchor_position: { x: 50, y: 50 },
|
|
43
|
+
anchor_alignment: "top_left",
|
|
44
|
+
font: "tscircuit2024",
|
|
45
|
+
font_size: 16,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Test with center alignment (baseline at center)
|
|
49
|
+
const textCenter: PcbFabricationNoteText = {
|
|
50
|
+
type: "pcb_fabrication_note_text",
|
|
51
|
+
pcb_fabrication_note_text_id: "fab-note-center",
|
|
52
|
+
pcb_component_id: "component2",
|
|
53
|
+
layer: "top",
|
|
54
|
+
text: "gap",
|
|
55
|
+
anchor_position: { x: 150, y: 100 },
|
|
56
|
+
anchor_alignment: "center",
|
|
57
|
+
font: "tscircuit2024",
|
|
58
|
+
font_size: 16,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Test with bottom alignment
|
|
62
|
+
const textBottom: PcbFabricationNoteText = {
|
|
63
|
+
type: "pcb_fabrication_note_text",
|
|
64
|
+
pcb_fabrication_note_text_id: "fab-note-bottom",
|
|
65
|
+
pcb_component_id: "component3",
|
|
66
|
+
layer: "top",
|
|
67
|
+
text: "gap",
|
|
68
|
+
anchor_position: { x: 250, y: 150 },
|
|
69
|
+
anchor_alignment: "bottom_left",
|
|
70
|
+
font: "tscircuit2024",
|
|
71
|
+
font_size: 16,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
drawer.drawElements([textTop, textCenter, textBottom])
|
|
75
|
+
|
|
76
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
77
|
+
import.meta.path,
|
|
78
|
+
"fabrication-note-text-baseline-anchors",
|
|
79
|
+
)
|
|
80
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw text with baseline alignment and descenders", async () => {
|
|
7
|
+
const SCALE = 4
|
|
8
|
+
const canvas = createCanvas(200 * SCALE, 150 * SCALE)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(SCALE, SCALE)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
|
|
13
|
+
ctx.fillStyle = "#1a1a1a"
|
|
14
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
15
|
+
|
|
16
|
+
// Draw baseline reference line
|
|
17
|
+
ctx.strokeStyle = "#444444"
|
|
18
|
+
ctx.lineWidth = 0.5
|
|
19
|
+
ctx.beginPath()
|
|
20
|
+
ctx.moveTo(10, 75)
|
|
21
|
+
ctx.lineTo(190, 75)
|
|
22
|
+
ctx.stroke()
|
|
23
|
+
|
|
24
|
+
// Test text with lowercase letters and descenders (g, j, p, q, y)
|
|
25
|
+
const text: PcbFabricationNoteText = {
|
|
26
|
+
type: "pcb_fabrication_note_text",
|
|
27
|
+
pcb_fabrication_note_text_id: "fab-note-baseline",
|
|
28
|
+
pcb_component_id: "component1",
|
|
29
|
+
layer: "top",
|
|
30
|
+
text: "gap jqpy",
|
|
31
|
+
anchor_position: { x: 100, y: 75 },
|
|
32
|
+
anchor_alignment: "center",
|
|
33
|
+
font: "tscircuit2024",
|
|
34
|
+
font_size: 20,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
drawer.drawElements([text])
|
|
38
|
+
|
|
39
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
40
|
+
import.meta.path,
|
|
41
|
+
"fabrication-note-text-baseline",
|
|
42
|
+
)
|
|
43
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw lowercase text with descenders", async () => {
|
|
7
|
+
const SCALE = 4
|
|
8
|
+
const canvas = createCanvas(250 * SCALE, 100 * SCALE)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(SCALE, SCALE)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
|
|
13
|
+
ctx.fillStyle = "#1a1a1a"
|
|
14
|
+
ctx.fillRect(0, 0, canvas.width / SCALE, canvas.height / SCALE)
|
|
15
|
+
|
|
16
|
+
// Draw baseline reference line
|
|
17
|
+
ctx.strokeStyle = "#666666"
|
|
18
|
+
ctx.lineWidth = 0.5
|
|
19
|
+
ctx.beginPath()
|
|
20
|
+
ctx.moveTo(10, 50)
|
|
21
|
+
ctx.lineTo(240, 50)
|
|
22
|
+
ctx.stroke()
|
|
23
|
+
|
|
24
|
+
// Test all descender letters: g, j, p, q, y
|
|
25
|
+
const text: PcbFabricationNoteText = {
|
|
26
|
+
type: "pcb_fabrication_note_text",
|
|
27
|
+
pcb_fabrication_note_text_id: "fab-note-descenders",
|
|
28
|
+
pcb_component_id: "component1",
|
|
29
|
+
layer: "top",
|
|
30
|
+
text: "gjpqy",
|
|
31
|
+
anchor_position: { x: 125, y: 50 },
|
|
32
|
+
anchor_alignment: "center",
|
|
33
|
+
font: "tscircuit2024",
|
|
34
|
+
font_size: 24,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
drawer.drawElements([text])
|
|
38
|
+
|
|
39
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
40
|
+
import.meta.path,
|
|
41
|
+
"fabrication-note-text-descenders",
|
|
42
|
+
)
|
|
43
|
+
})
|
|
@@ -18,7 +18,7 @@ test("draw fabrication note text small size", async () => {
|
|
|
18
18
|
pcb_fabrication_note_text_id: "fab-note-small",
|
|
19
19
|
pcb_component_id: "component1",
|
|
20
20
|
layer: "top",
|
|
21
|
-
text: "
|
|
21
|
+
text: "Smapq876",
|
|
22
22
|
anchor_position: { x: 50, y: 50 },
|
|
23
23
|
anchor_alignment: "center",
|
|
24
24
|
font: "tscircuit2024",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbNoteRect } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note rect with all features", async () => {
|
|
7
|
+
const canvas = createCanvas(100, 100)
|
|
8
|
+
const ctx = canvas.getContext("2d")
|
|
9
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
10
|
+
|
|
11
|
+
ctx.fillStyle = "#1a1a1a"
|
|
12
|
+
ctx.fillRect(0, 0, 100, 100)
|
|
13
|
+
|
|
14
|
+
const rect: PcbNoteRect = {
|
|
15
|
+
type: "pcb_note_rect",
|
|
16
|
+
pcb_note_rect_id: "note_rect1",
|
|
17
|
+
pcb_component_id: "component1",
|
|
18
|
+
pcb_group_id: "group1",
|
|
19
|
+
subcircuit_id: "subcircuit1",
|
|
20
|
+
name: "Test Note",
|
|
21
|
+
text: "This is a test note",
|
|
22
|
+
center: { x: 50, y: 50 },
|
|
23
|
+
width: 60,
|
|
24
|
+
height: 40,
|
|
25
|
+
stroke_width: 2,
|
|
26
|
+
corner_radius: 5,
|
|
27
|
+
is_filled: true,
|
|
28
|
+
has_stroke: true,
|
|
29
|
+
is_stroke_dashed: true,
|
|
30
|
+
color: "#00FFFF", // Cyan color
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
drawer.drawElements([rect])
|
|
34
|
+
|
|
35
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
36
|
+
import.meta.path,
|
|
37
|
+
)
|
|
38
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbNoteRect } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note rect with dashed stroke and no fill", async () => {
|
|
7
|
+
const canvas = createCanvas(100, 100)
|
|
8
|
+
const ctx = canvas.getContext("2d")
|
|
9
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
10
|
+
|
|
11
|
+
ctx.fillStyle = "#1a1a1a"
|
|
12
|
+
ctx.fillRect(0, 0, 100, 100)
|
|
13
|
+
|
|
14
|
+
const rect: PcbNoteRect = {
|
|
15
|
+
type: "pcb_note_rect",
|
|
16
|
+
pcb_note_rect_id: "note_rect2",
|
|
17
|
+
center: { x: 50, y: 50 },
|
|
18
|
+
width: 60,
|
|
19
|
+
height: 40,
|
|
20
|
+
stroke_width: 2,
|
|
21
|
+
is_filled: false,
|
|
22
|
+
has_stroke: true,
|
|
23
|
+
is_stroke_dashed: true,
|
|
24
|
+
color: "#0000FF", // Blue color
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
drawer.drawElements([rect])
|
|
28
|
+
|
|
29
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
30
|
+
import.meta.path,
|
|
31
|
+
)
|
|
32
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "canvas"
|
|
3
|
+
import type { PcbNoteRect } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("draw pcb note rect with fill and no stroke", async () => {
|
|
7
|
+
const canvas = createCanvas(100, 100)
|
|
8
|
+
const ctx = canvas.getContext("2d")
|
|
9
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
10
|
+
|
|
11
|
+
ctx.fillStyle = "#1a1a1a"
|
|
12
|
+
ctx.fillRect(0, 0, 100, 100)
|
|
13
|
+
|
|
14
|
+
const rect: PcbNoteRect = {
|
|
15
|
+
type: "pcb_note_rect",
|
|
16
|
+
pcb_note_rect_id: "note_rect3",
|
|
17
|
+
center: { x: 50, y: 50 },
|
|
18
|
+
width: 60,
|
|
19
|
+
height: 40,
|
|
20
|
+
stroke_width: 2,
|
|
21
|
+
corner_radius: 8,
|
|
22
|
+
is_filled: true,
|
|
23
|
+
has_stroke: false,
|
|
24
|
+
is_stroke_dashed: false,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
drawer.drawElements([rect])
|
|
28
|
+
|
|
29
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
30
|
+
import.meta.path,
|
|
31
|
+
)
|
|
32
|
+
})
|