circuit-to-canvas 0.0.17 → 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.
Files changed (37) hide show
  1. package/dist/index.d.ts +32 -8
  2. package/dist/index.js +179 -13
  3. package/lib/drawer/CircuitToCanvasDrawer.ts +14 -2
  4. package/lib/drawer/elements/index.ts +5 -0
  5. package/lib/drawer/elements/pcb-copper-text.ts +2 -2
  6. package/lib/drawer/elements/pcb-note-dimension.ts +201 -0
  7. package/lib/drawer/shapes/arrow.ts +36 -0
  8. package/lib/drawer/shapes/index.ts +1 -0
  9. package/lib/drawer/shapes/text/getAlphabetLayout.ts +41 -0
  10. package/lib/drawer/shapes/text/getTextStartPosition.ts +53 -0
  11. package/lib/drawer/shapes/text/index.ts +3 -0
  12. package/lib/drawer/shapes/{text.ts → text/text.ts} +5 -104
  13. package/package.json +2 -1
  14. package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
  15. package/tests/board-snapshot/usb-c-flashlight-board.test.ts +15 -0
  16. package/tests/board-snapshot/usb-c-flashlight.json +2456 -0
  17. package/tests/elements/__snapshots__/fabrication-note-text-descenders.snap.png +0 -0
  18. package/tests/elements/__snapshots__/fabrication-note-text-full-charset.snap.png +0 -0
  19. package/tests/elements/__snapshots__/pcb-fabrication-note-text-rgba-color.snap.png +0 -0
  20. package/tests/elements/__snapshots__/pcb-fabrication-note-text-small.snap.png +0 -0
  21. package/tests/elements/__snapshots__/pcb-note-dimension-angled-and-vertical.snap.png +0 -0
  22. package/tests/elements/__snapshots__/pcb-note-dimension-basic.snap.png +0 -0
  23. package/tests/elements/__snapshots__/pcb-note-dimension-vertical.snap.png +0 -0
  24. package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
  25. package/tests/elements/__snapshots__/pcb-note-text-anchor-alignment.snap.png +0 -0
  26. package/tests/elements/__snapshots__/pcb-note-text-custom-color.snap.png +0 -0
  27. package/tests/elements/__snapshots__/pcb-note-text-small.snap.png +0 -0
  28. package/tests/elements/pcb-note-dimension-angled-and-vertical.test.ts +37 -0
  29. package/tests/elements/pcb-note-dimension-basic.test.ts +36 -0
  30. package/tests/elements/pcb-note-dimension-vertical.test.ts +42 -0
  31. package/tests/elements/pcb-note-dimension-with-offset.test.ts +38 -0
  32. package/tests/fixtures/assets/label-circuit-to-canvas.png +0 -0
  33. package/tests/fixtures/assets/label-circuit-to-svg.png +0 -0
  34. package/tests/fixtures/getStackedPngSvgComparison.ts +62 -0
  35. package/tests/fixtures/stackPngsVertically.ts +82 -0
  36. package/tests/shapes/__snapshots__/oval.snap.png +0 -0
  37. package/tsconfig.json +1 -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, 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;
@@ -211,11 +225,7 @@ type AlphabetLayout = {
211
225
  strokeWidth: number;
212
226
  };
213
227
  declare function getAlphabetLayout(text: string, fontSize: number): AlphabetLayout;
214
- type AnchorAlignment = "center" | "top_left" | "top_right" | "bottom_left" | "bottom_right" | "left" | "right" | "top" | "bottom";
215
- declare function getTextStartPosition(alignment: AnchorAlignment, layout: AlphabetLayout): {
216
- x: number;
217
- y: number;
218
- };
228
+
219
229
  declare function strokeAlphabetText(ctx: CanvasContext, text: string, layout: AlphabetLayout, startX: number, startY: number): void;
220
230
  interface DrawTextParams {
221
231
  ctx: CanvasContext;
@@ -225,11 +235,17 @@ interface DrawTextParams {
225
235
  fontSize: number;
226
236
  color: string;
227
237
  realToCanvasMat: Matrix;
228
- anchorAlignment: AnchorAlignment;
238
+ anchorAlignment: NinePointAnchor;
229
239
  rotation?: number;
230
240
  }
231
241
  declare function drawText(params: DrawTextParams): void;
232
242
 
243
+ type AnchorAlignment = NinePointAnchor;
244
+ declare function getTextStartPosition(alignment: NinePointAnchor, layout: AlphabetLayout): {
245
+ x: number;
246
+ y: number;
247
+ };
248
+
233
249
  interface DrawPcbPlatedHoleParams {
234
250
  ctx: CanvasContext;
235
251
  hole: PcbPlatedHole;
@@ -386,4 +402,12 @@ interface DrawPcbNoteTextParams {
386
402
  }
387
403
  declare function drawPcbNoteText(params: DrawPcbNoteTextParams): void;
388
404
 
389
- 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
@@ -831,9 +831,11 @@ function drawPcbCopperPour(params) {
831
831
  // lib/drawer/elements/pcb-copper-text.ts
832
832
  import { applyToPoint as applyToPoint11 } from "transformation-matrix";
833
833
 
834
- // lib/drawer/shapes/text.ts
834
+ // lib/drawer/shapes/text/text.ts
835
835
  import { lineAlphabet } from "@tscircuit/alphabet";
836
836
  import { applyToPoint as applyToPoint10 } from "transformation-matrix";
837
+
838
+ // lib/drawer/shapes/text/getAlphabetLayout.ts
837
839
  var GLYPH_WIDTH_RATIO = 0.62;
838
840
  var LETTER_SPACING_RATIO = 0.3;
839
841
  var SPACE_WIDTH_RATIO = 1;
@@ -859,7 +861,8 @@ function getAlphabetLayout(text, fontSize) {
859
861
  strokeWidth
860
862
  };
861
863
  }
862
- var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
864
+
865
+ // lib/drawer/shapes/text/getTextStartPosition.ts
863
866
  function getTextStartPosition(alignment, layout) {
864
867
  const totalWidth = layout.width + layout.strokeWidth;
865
868
  const totalHeight = layout.height + layout.strokeWidth;
@@ -867,22 +870,25 @@ function getTextStartPosition(alignment, layout) {
867
870
  let y = 0;
868
871
  if (alignment === "center") {
869
872
  x = -totalWidth / 2;
870
- } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "left") {
873
+ } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
871
874
  x = 0;
872
- } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "right") {
875
+ } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
873
876
  x = -totalWidth;
874
877
  }
875
878
  if (alignment === "center") {
876
879
  y = -totalHeight / 2;
877
- } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top") {
880
+ } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
878
881
  y = 0;
879
- } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom") {
882
+ } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
880
883
  y = -totalHeight;
881
884
  } else {
882
885
  y = 0;
883
886
  }
884
887
  return { x, y };
885
888
  }
889
+
890
+ // lib/drawer/shapes/text/text.ts
891
+ var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
886
892
  function strokeAlphabetText(ctx, text, layout, startX, startY) {
887
893
  const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
888
894
  const topY = startY;
@@ -947,8 +953,8 @@ function layerToCopperColor(layer, colorMap) {
947
953
  }
948
954
  function mapAnchorAlignment2(alignment) {
949
955
  if (!alignment) return "center";
950
- if (alignment.includes("left")) return "left";
951
- if (alignment.includes("right")) return "right";
956
+ if (alignment.includes("left")) return "center_left";
957
+ if (alignment.includes("right")) return "center_right";
952
958
  return "center";
953
959
  }
954
960
  function drawPcbCopperText(params) {
@@ -1138,16 +1144,166 @@ function drawPcbNoteText(params) {
1138
1144
  });
1139
1145
  }
1140
1146
 
1141
- // lib/drawer/elements/pcb-note-line.ts
1147
+ // lib/drawer/elements/pcb-note-dimension.ts
1142
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";
1143
1299
  function drawPcbNoteLine(params) {
1144
1300
  const { ctx, line, realToCanvasMat, colorMap } = params;
1145
1301
  const defaultColor = "rgb(89, 148, 220)";
1146
1302
  const color = line.color ?? defaultColor;
1147
1303
  const strokeWidth = line.stroke_width ?? 0.1;
1148
1304
  const isDashed = line.is_dashed ?? false;
1149
- const [x1, y1] = applyToPoint12(realToCanvasMat, [line.x1, line.y1]);
1150
- 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]);
1151
1307
  const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1152
1308
  ctx.save();
1153
1309
  if (isDashed) {
@@ -1224,8 +1380,8 @@ var CircuitToCanvasDrawer = class {
1224
1380
  const offsetY = (canvasHeight - realHeight * uniformScale) / 2;
1225
1381
  this.realToCanvasMat = compose(
1226
1382
  translate(offsetX, offsetY),
1227
- scale(uniformScale, uniformScale),
1228
- translate(-bounds.minX, -bounds.minY)
1383
+ scale(uniformScale, -uniformScale),
1384
+ translate(-bounds.minX, -bounds.maxY)
1229
1385
  );
1230
1386
  }
1231
1387
  drawElements(elements, options = {}) {
@@ -1402,11 +1558,20 @@ var CircuitToCanvasDrawer = class {
1402
1558
  colorMap: this.colorMap
1403
1559
  });
1404
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
+ }
1405
1569
  }
1406
1570
  };
1407
1571
  export {
1408
1572
  CircuitToCanvasDrawer,
1409
1573
  DEFAULT_PCB_COLOR_MAP,
1574
+ drawArrow,
1410
1575
  drawCircle,
1411
1576
  drawLine,
1412
1577
  drawOval,
@@ -1419,6 +1584,7 @@ export {
1419
1584
  drawPcbFabricationNoteRect,
1420
1585
  drawPcbFabricationNoteText,
1421
1586
  drawPcbHole,
1587
+ drawPcbNoteDimension,
1422
1588
  drawPcbNotePath,
1423
1589
  drawPcbNoteRect,
1424
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 {
@@ -132,10 +134,11 @@ export class CircuitToCanvasDrawer {
132
134
  const offsetX = (canvasWidth - realWidth * uniformScale) / 2
133
135
  const offsetY = (canvasHeight - realHeight * uniformScale) / 2
134
136
 
137
+ // Flip Y axis: PCB uses Y-up, canvas uses Y-down
135
138
  this.realToCanvasMat = compose(
136
139
  translate(offsetX, offsetY),
137
- scale(uniformScale, uniformScale),
138
- translate(-bounds.minX, -bounds.minY),
140
+ scale(uniformScale, -uniformScale),
141
+ translate(-bounds.minX, -bounds.maxY),
139
142
  )
140
143
  }
141
144
 
@@ -340,5 +343,14 @@ export class CircuitToCanvasDrawer {
340
343
  colorMap: this.colorMap,
341
344
  })
342
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
+ }
343
355
  }
344
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"
@@ -27,8 +27,8 @@ function layerToCopperColor(layer: string, colorMap: PcbColorMap): string {
27
27
 
28
28
  function mapAnchorAlignment(alignment?: string): AnchorAlignment {
29
29
  if (!alignment) return "center"
30
- if (alignment.includes("left")) return "left"
31
- if (alignment.includes("right")) return "right"
30
+ if (alignment.includes("left")) return "center_left"
31
+ if (alignment.includes("right")) return "center_right"
32
32
  return "center"
33
33
  }
34
34
 
@@ -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,