circuit-to-canvas 0.0.18 → 0.0.19

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
@@ -1144,16 +1144,166 @@ function drawPcbNoteText(params) {
1144
1144
  });
1145
1145
  }
1146
1146
 
1147
- // lib/drawer/elements/pcb-note-line.ts
1147
+ // lib/drawer/elements/pcb-note-dimension.ts
1148
1148
  import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1149
+
1150
+ // lib/drawer/shapes/arrow.ts
1151
+ function drawArrow(params) {
1152
+ const { ctx, x, y, angle, arrowSize, color, strokeWidth } = params;
1153
+ ctx.save();
1154
+ ctx.translate(x, y);
1155
+ ctx.rotate(angle);
1156
+ ctx.beginPath();
1157
+ ctx.moveTo(0, 0);
1158
+ ctx.lineTo(-arrowSize, -arrowSize / 2);
1159
+ ctx.moveTo(0, 0);
1160
+ ctx.lineTo(-arrowSize, arrowSize / 2);
1161
+ ctx.lineWidth = strokeWidth;
1162
+ ctx.strokeStyle = color;
1163
+ ctx.lineCap = "round";
1164
+ ctx.lineJoin = "round";
1165
+ ctx.stroke();
1166
+ ctx.restore();
1167
+ }
1168
+
1169
+ // lib/drawer/elements/pcb-note-dimension.ts
1170
+ var DEFAULT_NOTE_COLOR = "rgba(255,255,255,0.5)";
1171
+ function drawPcbNoteDimension(params) {
1172
+ const { ctx, pcbNoteDimension, realToCanvasMat } = params;
1173
+ const color = pcbNoteDimension.color ?? DEFAULT_NOTE_COLOR;
1174
+ const arrowSize = pcbNoteDimension.arrow_size;
1175
+ const realFromX = pcbNoteDimension.from.x;
1176
+ const realFromY = pcbNoteDimension.from.y;
1177
+ const realToX = pcbNoteDimension.to.x;
1178
+ const realToY = pcbNoteDimension.to.y;
1179
+ let fromX = realFromX;
1180
+ let fromY = realFromY;
1181
+ let toX = realToX;
1182
+ let toY = realToY;
1183
+ let hasOffset = false;
1184
+ let offsetX = 0;
1185
+ let offsetY = 0;
1186
+ if (pcbNoteDimension.offset_distance && pcbNoteDimension.offset_direction) {
1187
+ const dirX = pcbNoteDimension.offset_direction.x;
1188
+ const dirY = pcbNoteDimension.offset_direction.y;
1189
+ const length = Math.hypot(dirX, dirY);
1190
+ if (length > 0) {
1191
+ const normX = dirX / length;
1192
+ const normY = dirY / length;
1193
+ hasOffset = true;
1194
+ offsetX = pcbNoteDimension.offset_distance * normX;
1195
+ offsetY = pcbNoteDimension.offset_distance * normY;
1196
+ fromX += offsetX;
1197
+ fromY += offsetY;
1198
+ toX += offsetX;
1199
+ toY += offsetY;
1200
+ }
1201
+ }
1202
+ const STROKE_WIDTH_RATIO2 = 0.13;
1203
+ const strokeWidth = Math.max(
1204
+ pcbNoteDimension.font_size * STROKE_WIDTH_RATIO2,
1205
+ 0.35
1206
+ );
1207
+ if (hasOffset) {
1208
+ drawLine({
1209
+ ctx,
1210
+ start: { x: realFromX, y: realFromY },
1211
+ end: { x: fromX, y: fromY },
1212
+ strokeWidth,
1213
+ stroke: color,
1214
+ realToCanvasMat
1215
+ });
1216
+ drawLine({
1217
+ ctx,
1218
+ start: { x: realToX, y: realToY },
1219
+ end: { x: toX, y: toY },
1220
+ strokeWidth,
1221
+ stroke: color,
1222
+ realToCanvasMat
1223
+ });
1224
+ }
1225
+ drawLine({
1226
+ ctx,
1227
+ start: { x: fromX, y: fromY },
1228
+ end: { x: toX, y: toY },
1229
+ strokeWidth,
1230
+ stroke: color,
1231
+ realToCanvasMat
1232
+ });
1233
+ const [canvasFromX, canvasFromY] = applyToPoint12(realToCanvasMat, [
1234
+ fromX,
1235
+ fromY
1236
+ ]);
1237
+ const [canvasToX, canvasToY] = applyToPoint12(realToCanvasMat, [toX, toY]);
1238
+ const canvasDx = canvasToX - canvasFromX;
1239
+ const canvasDy = canvasToY - canvasFromY;
1240
+ const lineAngle = Math.atan2(canvasDy, canvasDx);
1241
+ const scale2 = Math.abs(realToCanvasMat.a);
1242
+ const scaledArrowSize = arrowSize * scale2;
1243
+ const scaledStrokeWidth = strokeWidth * scale2;
1244
+ drawArrow({
1245
+ ctx,
1246
+ x: canvasFromX,
1247
+ y: canvasFromY,
1248
+ angle: lineAngle + Math.PI,
1249
+ arrowSize: scaledArrowSize,
1250
+ color,
1251
+ strokeWidth: scaledStrokeWidth
1252
+ });
1253
+ drawArrow({
1254
+ ctx,
1255
+ x: canvasToX,
1256
+ y: canvasToY,
1257
+ angle: lineAngle,
1258
+ arrowSize: scaledArrowSize,
1259
+ color,
1260
+ strokeWidth: scaledStrokeWidth
1261
+ });
1262
+ if (pcbNoteDimension.text) {
1263
+ let textX = (fromX + toX) / 2;
1264
+ let textY = (fromY + toY) / 2;
1265
+ const perpX = toY - fromY;
1266
+ const perpY = -(toX - fromX);
1267
+ const perpLength = Math.sqrt(perpX * perpX + perpY * perpY);
1268
+ if (perpLength > 0) {
1269
+ const offsetDistance = pcbNoteDimension.font_size * 1.5;
1270
+ const normalizedPerpX = perpX / perpLength;
1271
+ const normalizedPerpY = perpY / perpLength;
1272
+ textX += normalizedPerpX * offsetDistance;
1273
+ textY += normalizedPerpY * offsetDistance;
1274
+ }
1275
+ const textRotation = -(() => {
1276
+ const raw = pcbNoteDimension.text_ccw_rotation ?? lineAngle * 180 / Math.PI;
1277
+ if (pcbNoteDimension.text_ccw_rotation !== void 0) return raw;
1278
+ let deg = (raw + 180) % 360 - 180;
1279
+ if (deg > 90) deg -= 180;
1280
+ if (deg < -90) deg += 180;
1281
+ return deg;
1282
+ })();
1283
+ drawText({
1284
+ ctx,
1285
+ text: pcbNoteDimension.text,
1286
+ x: textX,
1287
+ y: textY,
1288
+ fontSize: pcbNoteDimension.font_size,
1289
+ color,
1290
+ realToCanvasMat,
1291
+ anchorAlignment: "center",
1292
+ rotation: textRotation
1293
+ });
1294
+ }
1295
+ }
1296
+
1297
+ // lib/drawer/elements/pcb-note-line.ts
1298
+ import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1149
1299
  function drawPcbNoteLine(params) {
1150
1300
  const { ctx, line, realToCanvasMat, colorMap } = params;
1151
1301
  const defaultColor = "rgb(89, 148, 220)";
1152
1302
  const color = line.color ?? defaultColor;
1153
1303
  const strokeWidth = line.stroke_width ?? 0.1;
1154
1304
  const isDashed = line.is_dashed ?? false;
1155
- const [x1, y1] = applyToPoint12(realToCanvasMat, [line.x1, line.y1]);
1156
- const [x2, y2] = applyToPoint12(realToCanvasMat, [line.x2, line.y2]);
1305
+ const [x1, y1] = applyToPoint13(realToCanvasMat, [line.x1, line.y1]);
1306
+ const [x2, y2] = applyToPoint13(realToCanvasMat, [line.x2, line.y2]);
1157
1307
  const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1158
1308
  ctx.save();
1159
1309
  if (isDashed) {
@@ -1408,11 +1558,20 @@ var CircuitToCanvasDrawer = class {
1408
1558
  colorMap: this.colorMap
1409
1559
  });
1410
1560
  }
1561
+ if (element.type === "pcb_note_dimension") {
1562
+ drawPcbNoteDimension({
1563
+ ctx: this.ctx,
1564
+ pcbNoteDimension: element,
1565
+ realToCanvasMat: this.realToCanvasMat,
1566
+ colorMap: this.colorMap
1567
+ });
1568
+ }
1411
1569
  }
1412
1570
  };
1413
1571
  export {
1414
1572
  CircuitToCanvasDrawer,
1415
1573
  DEFAULT_PCB_COLOR_MAP,
1574
+ drawArrow,
1416
1575
  drawCircle,
1417
1576
  drawLine,
1418
1577
  drawOval,
@@ -1425,6 +1584,7 @@ export {
1425
1584
  drawPcbFabricationNoteRect,
1426
1585
  drawPcbFabricationNoteText,
1427
1586
  drawPcbHole,
1587
+ drawPcbNoteDimension,
1428
1588
  drawPcbNotePath,
1429
1589
  drawPcbNoteRect,
1430
1590
  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
+ }
@@ -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.19",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -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
+ })