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.
- package/dist/index.d.ts +32 -8
- package/dist/index.js +179 -13
- package/lib/drawer/CircuitToCanvasDrawer.ts +14 -2
- package/lib/drawer/elements/index.ts +5 -0
- package/lib/drawer/elements/pcb-copper-text.ts +2 -2
- package/lib/drawer/elements/pcb-note-dimension.ts +201 -0
- package/lib/drawer/shapes/arrow.ts +36 -0
- package/lib/drawer/shapes/index.ts +1 -0
- package/lib/drawer/shapes/text/getAlphabetLayout.ts +41 -0
- package/lib/drawer/shapes/text/getTextStartPosition.ts +53 -0
- package/lib/drawer/shapes/text/index.ts +3 -0
- package/lib/drawer/shapes/{text.ts → text/text.ts} +5 -104
- package/package.json +2 -1
- package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
- package/tests/board-snapshot/usb-c-flashlight-board.test.ts +15 -0
- package/tests/board-snapshot/usb-c-flashlight.json +2456 -0
- package/tests/elements/__snapshots__/fabrication-note-text-descenders.snap.png +0 -0
- package/tests/elements/__snapshots__/fabrication-note-text-full-charset.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-dimension-angled-and-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-basic.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-text-anchor-alignment.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-text-custom-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-text-small.snap.png +0 -0
- package/tests/elements/pcb-note-dimension-angled-and-vertical.test.ts +37 -0
- package/tests/elements/pcb-note-dimension-basic.test.ts +36 -0
- package/tests/elements/pcb-note-dimension-vertical.test.ts +42 -0
- package/tests/elements/pcb-note-dimension-with-offset.test.ts +38 -0
- package/tests/fixtures/assets/label-circuit-to-canvas.png +0 -0
- package/tests/fixtures/assets/label-circuit-to-svg.png +0 -0
- package/tests/fixtures/getStackedPngSvgComparison.ts +62 -0
- package/tests/fixtures/stackPngsVertically.ts +82 -0
- package/tests/shapes/__snapshots__/oval.snap.png +0 -0
- package/tsconfig.json +1 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
})
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|