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
@@ -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
+ })
@@ -0,0 +1,62 @@
1
+ import { createCanvas } from "@napi-rs/canvas"
2
+ import type { AnyCircuitElement } from "circuit-json"
3
+ import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
4
+ import type { Bounds } from "@tscircuit/math-utils"
5
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
6
+ import { stackPngsVertically, svgToPng } from "./stackPngsVertically"
7
+ import { getBoundsOfPcbElements } from "@tscircuit/circuit-json-util"
8
+
9
+ export interface StackedPngSvgComparisonOptions {
10
+ width?: number
11
+ height?: number
12
+ padding?: number
13
+ }
14
+
15
+ /**
16
+ * Generate a stacked PNG comparison of circuit-to-canvas vs circuit-to-svg rendering.
17
+ *
18
+ * @param circuitJson - Array of circuit elements to render
19
+ * @param options - Optional configuration for width, height, and padding
20
+ * @returns Promise<Buffer> - Stacked PNG with both renderings labeled
21
+ */
22
+ export async function getStackedPngSvgComparison(
23
+ circuitJson: AnyCircuitElement[],
24
+ options: StackedPngSvgComparisonOptions = {},
25
+ ): Promise<Buffer> {
26
+ const { width = 400, height = 800, padding = 4 } = options
27
+
28
+ const bounds = getBoundsOfPcbElements(circuitJson)
29
+
30
+ // Generate circuit-to-canvas PNG
31
+ const canvas = createCanvas(width, height)
32
+ const ctx = canvas.getContext("2d")
33
+ const drawer = new CircuitToCanvasDrawer(ctx)
34
+
35
+ ctx.fillStyle = "#000000"
36
+ ctx.fillRect(0, 0, width, height)
37
+
38
+ drawer.setCameraBounds({
39
+ minX: bounds.minX,
40
+ maxX: bounds.maxX,
41
+ minY: bounds.minY,
42
+ maxY: bounds.maxY,
43
+ })
44
+ drawer.drawElements(circuitJson)
45
+
46
+ const canvasPng = canvas.toBuffer("image/png")
47
+
48
+ // Generate circuit-to-svg PNG
49
+ const svg = convertCircuitJsonToPcbSvg(circuitJson, {
50
+ width,
51
+ height,
52
+ })
53
+ const svgPng = svgToPng(svg)
54
+
55
+ // Stack both PNGs vertically with labels
56
+ const stackedPng = await stackPngsVertically([
57
+ { png: canvasPng, label: "circuit-to-canvas" },
58
+ { png: svgPng, label: "circuit-to-svg" },
59
+ ])
60
+
61
+ return stackedPng
62
+ }
@@ -0,0 +1,82 @@
1
+ import { createCanvas, loadImage } from "@napi-rs/canvas"
2
+ import { Resvg } from "@resvg/resvg-js"
3
+ import * as fs from "node:fs"
4
+ import * as path from "node:path"
5
+
6
+ // Pre-generated label PNGs for common labels
7
+ const labelPngCache: Map<string, Buffer> = new Map()
8
+ const assetsDir = path.join(__dirname, "assets")
9
+
10
+ const getPreGeneratedLabelPng = (label: string): Buffer => {
11
+ const cacheKey = label
12
+ if (labelPngCache.has(cacheKey)) {
13
+ return labelPngCache.get(cacheKey)!
14
+ }
15
+
16
+ const filename = `label-${label}.png`
17
+ const filepath = path.join(assetsDir, filename)
18
+ if (fs.existsSync(filepath)) {
19
+ const png = fs.readFileSync(filepath)
20
+ labelPngCache.set(cacheKey, png)
21
+ return png
22
+ }
23
+ throw new Error(`Label PNG not found for label: ${label}`)
24
+ }
25
+
26
+ export const stackPngsVertically = async (
27
+ pngs: Array<{ png: Buffer; label: string }>,
28
+ ): Promise<Buffer> => {
29
+ if (pngs.length === 0) {
30
+ throw new Error("No PNGs provided to stack")
31
+ }
32
+
33
+ if (pngs.length === 1) {
34
+ return pngs[0]!.png
35
+ }
36
+
37
+ // Load all images to get dimensions
38
+ const images = await Promise.all(
39
+ pngs.map(async ({ png }) => await loadImage(png)),
40
+ )
41
+
42
+ // Calculate the maximum width and total height
43
+ const maxWidth = Math.max(...images.map((img) => img.width))
44
+ const totalHeight = images.reduce((sum, img) => sum + img.height, 0)
45
+
46
+ // Create the final canvas
47
+ const canvas = createCanvas(maxWidth, totalHeight)
48
+ const ctx = canvas.getContext("2d")
49
+
50
+ // Fill with dark background
51
+ ctx.fillStyle = "#1a1a1a"
52
+ ctx.fillRect(0, 0, maxWidth, totalHeight)
53
+
54
+ // Draw each image and its label
55
+ let currentY = 0
56
+ for (let i = 0; i < pngs.length; i++) {
57
+ const { label } = pngs[i]!
58
+ const img = images[i]!
59
+ const width = img.width
60
+ const height = img.height
61
+
62
+ // Center horizontally if image is narrower than max width
63
+ const left = Math.floor((maxWidth - width) / 2)
64
+
65
+ // Draw the image
66
+ ctx.drawImage(img, left, currentY)
67
+
68
+ // Draw the label
69
+ const labelPng = getPreGeneratedLabelPng(label)
70
+ const labelImg = await loadImage(labelPng!)
71
+ ctx.drawImage(labelImg, 0, currentY)
72
+
73
+ currentY += height
74
+ }
75
+
76
+ return canvas.toBuffer("image/png")
77
+ }
78
+
79
+ export const svgToPng = (svg: string): Buffer => {
80
+ const resvg = new Resvg(svg)
81
+ return resvg.render().asPng()
82
+ }
package/tsconfig.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,
10
+ "resolveJsonModule": true,
10
11
 
11
12
  // Bundler mode
12
13
  "moduleResolution": "bundler",