circuit-to-canvas 0.0.3 → 0.0.5
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/README.md +1 -1
- package/dist/index.d.ts +82 -3
- package/dist/index.js +357 -4
- package/lib/drawer/CircuitToCanvasDrawer.ts +55 -0
- package/lib/drawer/elements/index.ts +25 -0
- package/lib/drawer/elements/pcb-copper-text.ts +99 -0
- package/lib/drawer/elements/pcb-fabrication-note-path.ts +41 -0
- package/lib/drawer/elements/pcb-fabrication-note-rect.ts +39 -0
- package/lib/drawer/elements/pcb-fabrication-note-text.ts +42 -0
- package/lib/drawer/elements/pcb-note-rect.ts +37 -0
- package/lib/drawer/shapes/index.ts +9 -0
- package/lib/drawer/shapes/rect.ts +28 -3
- package/lib/drawer/shapes/text.ts +218 -0
- package/lib/drawer/types.ts +8 -0
- package/package.json +3 -1
- package/tests/elements/__snapshots__/pcb-copper-text-knockout.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-copper-text.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path-custom-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path-thick-stroke.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-path.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-all-features.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-corner-radius.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-custom-color.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-dashed.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-fabrication-note-rect-default-color.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/pcb-copper-text.test.ts +102 -0
- package/tests/elements/pcb-fabrication-note-path-custom-color.test.ts +37 -0
- package/tests/elements/pcb-fabrication-note-path-thick-stroke.test.ts +37 -0
- package/tests/elements/pcb-fabrication-note-path.test.ts +36 -0
- package/tests/elements/pcb-fabrication-note-rect-all-features.test.ts +35 -0
- package/tests/elements/pcb-fabrication-note-rect-corner-radius.test.ts +33 -0
- package/tests/elements/pcb-fabrication-note-rect-custom-color.test.ts +32 -0
- package/tests/elements/pcb-fabrication-note-rect-dashed.test.ts +33 -0
- package/tests/elements/pcb-fabrication-note-rect-default-color.test.ts +32 -0
- package/tests/elements/pcb-fabrication-note-text-rgba-color.test.ts +34 -0
- package/tests/elements/pcb-fabrication-note-text-small.test.ts +33 -0
- 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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { PcbCopperText } 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 {
|
|
6
|
+
getAlphabetLayout,
|
|
7
|
+
strokeAlphabetText,
|
|
8
|
+
getTextStartPosition,
|
|
9
|
+
type AnchorAlignment,
|
|
10
|
+
} from "../shapes/text"
|
|
11
|
+
|
|
12
|
+
export interface DrawPcbCopperTextParams {
|
|
13
|
+
ctx: CanvasContext
|
|
14
|
+
text: PcbCopperText
|
|
15
|
+
transform: Matrix
|
|
16
|
+
colorMap: PcbColorMap
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 }
|
|
20
|
+
|
|
21
|
+
function layerToCopperColor(layer: string, colorMap: PcbColorMap): string {
|
|
22
|
+
return (
|
|
23
|
+
colorMap.copper[layer as keyof typeof colorMap.copper] ??
|
|
24
|
+
colorMap.copper.top
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mapAnchorAlignment(alignment?: string): AnchorAlignment {
|
|
29
|
+
if (!alignment) return "center"
|
|
30
|
+
if (alignment.includes("left")) return "left"
|
|
31
|
+
if (alignment.includes("right")) return "right"
|
|
32
|
+
return "center"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
|
|
36
|
+
const { ctx, text, transform, colorMap } = params
|
|
37
|
+
|
|
38
|
+
const content = text.text ?? ""
|
|
39
|
+
if (!content) return
|
|
40
|
+
|
|
41
|
+
const [x, y] = applyToPoint(transform, [
|
|
42
|
+
text.anchor_position.x,
|
|
43
|
+
text.anchor_position.y,
|
|
44
|
+
])
|
|
45
|
+
const scale = Math.abs(transform.a)
|
|
46
|
+
const fontSize = (text.font_size ?? 1) * scale
|
|
47
|
+
const rotation = text.ccw_rotation ?? 0
|
|
48
|
+
const padding = {
|
|
49
|
+
...DEFAULT_PADDING,
|
|
50
|
+
...text.knockout_padding,
|
|
51
|
+
}
|
|
52
|
+
const textColor = layerToCopperColor(text.layer, colorMap)
|
|
53
|
+
const layout = getAlphabetLayout(content, fontSize)
|
|
54
|
+
const totalWidth = layout.width + layout.strokeWidth
|
|
55
|
+
const totalHeight = layout.height + layout.strokeWidth
|
|
56
|
+
const alignment = mapAnchorAlignment(text.anchor_alignment)
|
|
57
|
+
const startPos = getTextStartPosition(alignment, layout)
|
|
58
|
+
// Copper text always centers vertically (startY=0), uses startPos.x for horizontal alignment
|
|
59
|
+
const startX = startPos.x
|
|
60
|
+
const startY = 0 // Centers vertically at y=0 (shared function calculates yOffset = startY + height/2)
|
|
61
|
+
|
|
62
|
+
ctx.save()
|
|
63
|
+
ctx.translate(x, y)
|
|
64
|
+
if (text.is_mirrored) ctx.scale(-1, 1)
|
|
65
|
+
if (rotation !== 0) ctx.rotate(-rotation * (Math.PI / 180))
|
|
66
|
+
|
|
67
|
+
ctx.lineWidth = layout.strokeWidth
|
|
68
|
+
ctx.lineCap = "round"
|
|
69
|
+
ctx.lineJoin = "round"
|
|
70
|
+
|
|
71
|
+
if (text.is_knockout) {
|
|
72
|
+
const paddingLeft = padding.left * scale
|
|
73
|
+
const paddingRight = padding.right * scale
|
|
74
|
+
const paddingTop = padding.top * scale
|
|
75
|
+
const paddingBottom = padding.bottom * scale
|
|
76
|
+
const xOffset = startX - paddingLeft
|
|
77
|
+
const yOffset = -(layout.height / 2) - layout.strokeWidth / 2 - paddingTop
|
|
78
|
+
const knockoutWidth = totalWidth + paddingLeft + paddingRight
|
|
79
|
+
const knockoutHeight = totalHeight + paddingTop + paddingBottom
|
|
80
|
+
|
|
81
|
+
ctx.fillStyle = textColor
|
|
82
|
+
ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight)
|
|
83
|
+
|
|
84
|
+
const previousCompositeOperation = ctx.globalCompositeOperation
|
|
85
|
+
ctx.globalCompositeOperation = "destination-out"
|
|
86
|
+
ctx.fillStyle = "rgba(0,0,0,1)"
|
|
87
|
+
ctx.strokeStyle = "rgba(0,0,0,1)"
|
|
88
|
+
strokeAlphabetText(ctx, content, layout, startX, startY)
|
|
89
|
+
if (previousCompositeOperation) {
|
|
90
|
+
ctx.globalCompositeOperation = previousCompositeOperation
|
|
91
|
+
} else {
|
|
92
|
+
ctx.globalCompositeOperation = "source-over"
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
ctx.strokeStyle = textColor
|
|
96
|
+
strokeAlphabetText(ctx, content, layout, startX, startY)
|
|
97
|
+
}
|
|
98
|
+
ctx.restore()
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PcbFabricationNotePath } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import type { PcbColorMap, CanvasContext } from "../types"
|
|
4
|
+
import { drawLine } from "../shapes/line"
|
|
5
|
+
|
|
6
|
+
export interface DrawPcbFabricationNotePathParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
path: PcbFabricationNotePath
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawPcbFabricationNotePath(
|
|
14
|
+
params: DrawPcbFabricationNotePathParams,
|
|
15
|
+
): void {
|
|
16
|
+
const { ctx, path, transform, colorMap } = params
|
|
17
|
+
|
|
18
|
+
// Use the color from the path if provided, otherwise use a default color
|
|
19
|
+
// Fabrication notes are typically shown in a distinct color
|
|
20
|
+
const defaultColor = "rgba(255,255,255,0.5)" // White color for fabrication notes
|
|
21
|
+
const color = path.color ?? defaultColor
|
|
22
|
+
|
|
23
|
+
if (!path.route || path.route.length < 2) return
|
|
24
|
+
|
|
25
|
+
// Draw each segment of the path
|
|
26
|
+
for (let i = 0; i < path.route.length - 1; i++) {
|
|
27
|
+
const start = path.route[i]
|
|
28
|
+
const end = path.route[i + 1]
|
|
29
|
+
|
|
30
|
+
if (!start || !end) continue
|
|
31
|
+
|
|
32
|
+
drawLine({
|
|
33
|
+
ctx,
|
|
34
|
+
start: { x: start.x, y: start.y },
|
|
35
|
+
end: { x: end.x, y: end.y },
|
|
36
|
+
strokeWidth: path.stroke_width ?? 0.1,
|
|
37
|
+
stroke: color,
|
|
38
|
+
transform,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PcbFabricationNoteRect } 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 DrawPcbFabricationNoteRectParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
rect: PcbFabricationNoteRect
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawPcbFabricationNoteRect(
|
|
14
|
+
params: DrawPcbFabricationNoteRectParams,
|
|
15
|
+
): void {
|
|
16
|
+
const { ctx, rect, transform, colorMap } = params
|
|
17
|
+
|
|
18
|
+
// Use the color from the rect if provided, otherwise use a default color
|
|
19
|
+
// Fabrication notes are typically shown in a distinct color
|
|
20
|
+
const defaultColor = "rgba(255,255,255,0.5)" // White color for fabrication notes
|
|
21
|
+
const color = rect.color ?? defaultColor
|
|
22
|
+
|
|
23
|
+
const isFilled = rect.is_filled ?? false
|
|
24
|
+
const hasStroke = rect.has_stroke ?? true
|
|
25
|
+
const isStrokeDashed = rect.is_stroke_dashed ?? false
|
|
26
|
+
|
|
27
|
+
drawRect({
|
|
28
|
+
ctx,
|
|
29
|
+
center: rect.center,
|
|
30
|
+
width: rect.width,
|
|
31
|
+
height: rect.height,
|
|
32
|
+
fill: isFilled ? color : undefined,
|
|
33
|
+
stroke: hasStroke ? color : undefined,
|
|
34
|
+
strokeWidth: hasStroke ? rect.stroke_width : undefined,
|
|
35
|
+
borderRadius: rect.corner_radius,
|
|
36
|
+
transform,
|
|
37
|
+
isStrokeDashed,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PcbFabricationNoteText } from "circuit-json"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import type { PcbColorMap, CanvasContext } from "../types"
|
|
4
|
+
import { drawText } from "../shapes/text"
|
|
5
|
+
|
|
6
|
+
export interface DrawPcbFabricationNoteTextParams {
|
|
7
|
+
ctx: CanvasContext
|
|
8
|
+
text: PcbFabricationNoteText
|
|
9
|
+
transform: Matrix
|
|
10
|
+
colorMap: PcbColorMap
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FABRICATION_NOTE_COLOR = "rgba(255,255,255,0.5)"
|
|
14
|
+
|
|
15
|
+
function layerToColor(layer: string, colorMap: PcbColorMap): string {
|
|
16
|
+
// For fabrication notes, we use a default color
|
|
17
|
+
// Could be extended to support per-layer colors in the future
|
|
18
|
+
return DEFAULT_FABRICATION_NOTE_COLOR
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function drawPcbFabricationNoteText(
|
|
22
|
+
params: DrawPcbFabricationNoteTextParams,
|
|
23
|
+
): void {
|
|
24
|
+
const { ctx, text, transform, colorMap } = params
|
|
25
|
+
|
|
26
|
+
const defaultColor = layerToColor(text.layer, colorMap)
|
|
27
|
+
const color = text.color ?? defaultColor
|
|
28
|
+
const fontSize = text.font_size
|
|
29
|
+
|
|
30
|
+
// Use @tscircuit/alphabet to draw text
|
|
31
|
+
// Pass real-world coordinates and let drawText apply the transform
|
|
32
|
+
drawText({
|
|
33
|
+
ctx,
|
|
34
|
+
text: text.text,
|
|
35
|
+
x: text.anchor_position.x,
|
|
36
|
+
y: text.anchor_position.y,
|
|
37
|
+
fontSize,
|
|
38
|
+
color,
|
|
39
|
+
transform,
|
|
40
|
+
anchorAlignment: text.anchor_alignment,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -5,3 +5,12 @@ 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 {
|
|
9
|
+
drawText,
|
|
10
|
+
type DrawTextParams,
|
|
11
|
+
getAlphabetLayout,
|
|
12
|
+
strokeAlphabetText,
|
|
13
|
+
getTextStartPosition,
|
|
14
|
+
type AlphabetLayout,
|
|
15
|
+
type AnchorAlignment,
|
|
16
|
+
} from "./text"
|
|
@@ -7,10 +7,13 @@ export interface DrawRectParams {
|
|
|
7
7
|
center: { x: number; y: number }
|
|
8
8
|
width: number
|
|
9
9
|
height: number
|
|
10
|
-
fill
|
|
10
|
+
fill?: string
|
|
11
11
|
transform: Matrix
|
|
12
12
|
borderRadius?: number
|
|
13
13
|
rotation?: number
|
|
14
|
+
stroke?: string
|
|
15
|
+
strokeWidth?: number
|
|
16
|
+
isStrokeDashed?: boolean
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function drawRect(params: DrawRectParams): void {
|
|
@@ -23,12 +26,18 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
23
26
|
transform,
|
|
24
27
|
borderRadius = 0,
|
|
25
28
|
rotation = 0,
|
|
29
|
+
stroke,
|
|
30
|
+
strokeWidth,
|
|
31
|
+
isStrokeDashed = false,
|
|
26
32
|
} = params
|
|
27
33
|
|
|
28
34
|
const [cx, cy] = applyToPoint(transform, [center.x, center.y])
|
|
29
35
|
const scaledWidth = width * Math.abs(transform.a)
|
|
30
36
|
const scaledHeight = height * Math.abs(transform.a)
|
|
31
37
|
const scaledRadius = borderRadius * Math.abs(transform.a)
|
|
38
|
+
const scaledStrokeWidth = strokeWidth
|
|
39
|
+
? strokeWidth * Math.abs(transform.a)
|
|
40
|
+
: undefined
|
|
32
41
|
|
|
33
42
|
ctx.save()
|
|
34
43
|
ctx.translate(cx, cy)
|
|
@@ -37,6 +46,13 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
37
46
|
ctx.rotate(-rotation * (Math.PI / 180))
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
// Set up dashed line if needed
|
|
50
|
+
if (isStrokeDashed && scaledStrokeWidth) {
|
|
51
|
+
ctx.setLineDash([scaledStrokeWidth * 2, scaledStrokeWidth * 2])
|
|
52
|
+
} else {
|
|
53
|
+
ctx.setLineDash([])
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
ctx.beginPath()
|
|
41
57
|
|
|
42
58
|
if (scaledRadius > 0) {
|
|
@@ -63,7 +79,16 @@ export function drawRect(params: DrawRectParams): void {
|
|
|
63
79
|
ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
if (fill) {
|
|
83
|
+
ctx.fillStyle = fill
|
|
84
|
+
ctx.fill()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stroke && scaledStrokeWidth) {
|
|
88
|
+
ctx.strokeStyle = stroke
|
|
89
|
+
ctx.lineWidth = scaledStrokeWidth
|
|
90
|
+
ctx.stroke()
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
ctx.restore()
|
|
69
94
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { lineAlphabet } from "@tscircuit/alphabet"
|
|
2
|
+
import type { Matrix } from "transformation-matrix"
|
|
3
|
+
import { applyToPoint } from "transformation-matrix"
|
|
4
|
+
import type { CanvasContext } from "../types"
|
|
5
|
+
|
|
6
|
+
const GLYPH_WIDTH_RATIO = 0.62
|
|
7
|
+
const LETTER_SPACING_RATIO = 0.3 // Letter spacing between characters (25% of glyph width)
|
|
8
|
+
const SPACE_WIDTH_RATIO = 1
|
|
9
|
+
const STROKE_WIDTH_RATIO = 0.13
|
|
10
|
+
const CURVED_GLYPHS = new Set(["O", "o", "0"])
|
|
11
|
+
|
|
12
|
+
export type AlphabetLayout = {
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
glyphWidth: number
|
|
16
|
+
letterSpacing: number
|
|
17
|
+
spaceWidth: number
|
|
18
|
+
strokeWidth: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAlphabetLayout(
|
|
22
|
+
text: string,
|
|
23
|
+
fontSize: number,
|
|
24
|
+
): AlphabetLayout {
|
|
25
|
+
const glyphWidth = fontSize * GLYPH_WIDTH_RATIO
|
|
26
|
+
const letterSpacing = glyphWidth * LETTER_SPACING_RATIO
|
|
27
|
+
const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO
|
|
28
|
+
const characters = Array.from(text)
|
|
29
|
+
|
|
30
|
+
let width = 0
|
|
31
|
+
characters.forEach((char, index) => {
|
|
32
|
+
const advance = char === " " ? spaceWidth : glyphWidth
|
|
33
|
+
width += advance
|
|
34
|
+
if (index < characters.length - 1) width += letterSpacing
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
width,
|
|
41
|
+
height: fontSize,
|
|
42
|
+
glyphWidth,
|
|
43
|
+
letterSpacing,
|
|
44
|
+
spaceWidth,
|
|
45
|
+
strokeWidth,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getGlyphLines = (char: string) =>
|
|
50
|
+
lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()]
|
|
51
|
+
|
|
52
|
+
export type AnchorAlignment =
|
|
53
|
+
| "center"
|
|
54
|
+
| "top_left"
|
|
55
|
+
| "top_right"
|
|
56
|
+
| "bottom_left"
|
|
57
|
+
| "bottom_right"
|
|
58
|
+
| "left"
|
|
59
|
+
| "right"
|
|
60
|
+
| "top"
|
|
61
|
+
| "bottom"
|
|
62
|
+
|
|
63
|
+
export function getTextStartPosition(
|
|
64
|
+
alignment: AnchorAlignment,
|
|
65
|
+
layout: AlphabetLayout,
|
|
66
|
+
): { x: number; y: number } {
|
|
67
|
+
const totalWidth = layout.width + layout.strokeWidth
|
|
68
|
+
const totalHeight = layout.height + layout.strokeWidth
|
|
69
|
+
|
|
70
|
+
let x = 0
|
|
71
|
+
let y = 0
|
|
72
|
+
|
|
73
|
+
// Horizontal alignment
|
|
74
|
+
if (alignment === "center") {
|
|
75
|
+
x = -totalWidth / 2
|
|
76
|
+
} else if (
|
|
77
|
+
alignment === "top_left" ||
|
|
78
|
+
alignment === "bottom_left" ||
|
|
79
|
+
alignment === "left"
|
|
80
|
+
) {
|
|
81
|
+
x = 0
|
|
82
|
+
} else if (
|
|
83
|
+
alignment === "top_right" ||
|
|
84
|
+
alignment === "bottom_right" ||
|
|
85
|
+
alignment === "right"
|
|
86
|
+
) {
|
|
87
|
+
x = -totalWidth
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Vertical alignment
|
|
91
|
+
if (alignment === "center") {
|
|
92
|
+
y = -totalHeight / 2
|
|
93
|
+
} else if (
|
|
94
|
+
alignment === "top_left" ||
|
|
95
|
+
alignment === "top_right" ||
|
|
96
|
+
alignment === "top"
|
|
97
|
+
) {
|
|
98
|
+
y = 0
|
|
99
|
+
} else if (
|
|
100
|
+
alignment === "bottom_left" ||
|
|
101
|
+
alignment === "bottom_right" ||
|
|
102
|
+
alignment === "bottom"
|
|
103
|
+
) {
|
|
104
|
+
y = -totalHeight
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { x, y }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function strokeAlphabetText(
|
|
111
|
+
ctx: CanvasContext,
|
|
112
|
+
text: string,
|
|
113
|
+
layout: AlphabetLayout,
|
|
114
|
+
startX: number,
|
|
115
|
+
startY: number,
|
|
116
|
+
): void {
|
|
117
|
+
const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout
|
|
118
|
+
const yOffset = startY + height / 2
|
|
119
|
+
const characters = Array.from(text)
|
|
120
|
+
let cursor = startX + strokeWidth / 2
|
|
121
|
+
|
|
122
|
+
characters.forEach((char, index) => {
|
|
123
|
+
const lines = getGlyphLines(char)
|
|
124
|
+
const advance = char === " " ? spaceWidth : glyphWidth
|
|
125
|
+
|
|
126
|
+
if (CURVED_GLYPHS.has(char)) {
|
|
127
|
+
const radiusX = Math.max(glyphWidth / 2 - strokeWidth / 2, strokeWidth)
|
|
128
|
+
const radiusY = Math.max(height / 2 - strokeWidth / 2, strokeWidth)
|
|
129
|
+
const centerY = yOffset - height / 2
|
|
130
|
+
ctx.beginPath()
|
|
131
|
+
ctx.ellipse(
|
|
132
|
+
cursor + glyphWidth / 2,
|
|
133
|
+
centerY,
|
|
134
|
+
radiusX,
|
|
135
|
+
radiusY,
|
|
136
|
+
0,
|
|
137
|
+
0,
|
|
138
|
+
Math.PI * 2,
|
|
139
|
+
)
|
|
140
|
+
ctx.stroke()
|
|
141
|
+
} else if (lines?.length) {
|
|
142
|
+
ctx.beginPath()
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const x1 = cursor + line.x1 * glyphWidth
|
|
145
|
+
const y1 = yOffset - line.y1 * height
|
|
146
|
+
const x2 = cursor + line.x2 * glyphWidth
|
|
147
|
+
const y2 = yOffset - line.y2 * height
|
|
148
|
+
ctx.moveTo(x1, y1)
|
|
149
|
+
ctx.lineTo(x2, y2)
|
|
150
|
+
}
|
|
151
|
+
ctx.stroke()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Move cursor by the character width
|
|
155
|
+
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
|
+
if (index < characters.length - 1) {
|
|
159
|
+
cursor += letterSpacing
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface DrawTextParams {
|
|
165
|
+
ctx: CanvasContext
|
|
166
|
+
text: string
|
|
167
|
+
x: number
|
|
168
|
+
y: number
|
|
169
|
+
fontSize: number
|
|
170
|
+
color: string
|
|
171
|
+
transform: Matrix
|
|
172
|
+
anchorAlignment: AnchorAlignment
|
|
173
|
+
rotation?: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function drawText(params: DrawTextParams): void {
|
|
177
|
+
const {
|
|
178
|
+
ctx,
|
|
179
|
+
text,
|
|
180
|
+
x,
|
|
181
|
+
y,
|
|
182
|
+
fontSize,
|
|
183
|
+
color,
|
|
184
|
+
transform,
|
|
185
|
+
anchorAlignment,
|
|
186
|
+
rotation = 0,
|
|
187
|
+
} = params
|
|
188
|
+
|
|
189
|
+
if (!text) return
|
|
190
|
+
|
|
191
|
+
const [transformedX, transformedY] = applyToPoint(transform, [x, y])
|
|
192
|
+
const scale = Math.abs(transform.a)
|
|
193
|
+
const scaledFontSize = fontSize * scale
|
|
194
|
+
const layout = getAlphabetLayout(text, scaledFontSize)
|
|
195
|
+
const startPos = getTextStartPosition(anchorAlignment, layout)
|
|
196
|
+
|
|
197
|
+
ctx.save()
|
|
198
|
+
ctx.translate(transformedX, transformedY)
|
|
199
|
+
|
|
200
|
+
if (rotation !== 0) {
|
|
201
|
+
ctx.rotate(-rotation * (Math.PI / 180))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ctx.lineWidth = layout.strokeWidth
|
|
205
|
+
ctx.lineCap = "round"
|
|
206
|
+
ctx.lineJoin = "round"
|
|
207
|
+
ctx.strokeStyle = color
|
|
208
|
+
|
|
209
|
+
strokeAlphabetText(
|
|
210
|
+
ctx,
|
|
211
|
+
text,
|
|
212
|
+
layout,
|
|
213
|
+
startPos.x,
|
|
214
|
+
startPos.y + layout.strokeWidth / 2,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
ctx.restore()
|
|
218
|
+
}
|
package/lib/drawer/types.ts
CHANGED
|
@@ -34,13 +34,21 @@ export interface CanvasContext {
|
|
|
34
34
|
translate(x: number, y: number): void
|
|
35
35
|
rotate(angle: number): void
|
|
36
36
|
scale(x: number, y: number): void
|
|
37
|
+
globalCompositeOperation?: string
|
|
37
38
|
fillStyle: string | CanvasGradient | CanvasPattern
|
|
38
39
|
strokeStyle: string | CanvasGradient | CanvasPattern
|
|
39
40
|
lineWidth: number
|
|
40
41
|
lineCap: "butt" | "round" | "square"
|
|
41
42
|
lineJoin: "bevel" | "round" | "miter"
|
|
43
|
+
setLineDash(segments: number[]): void
|
|
42
44
|
canvas: { width: number; height: number }
|
|
43
45
|
fillText(text: string, x: number, y: number): void
|
|
46
|
+
fillRect(x: number, y: number, width: number, height: number): void
|
|
47
|
+
measureText?: (text: string) => {
|
|
48
|
+
width: number
|
|
49
|
+
actualBoundingBoxAscent?: number
|
|
50
|
+
actualBoundingBoxDescent?: number
|
|
51
|
+
}
|
|
44
52
|
font: string
|
|
45
53
|
textAlign: "start" | "end" | "left" | "right" | "center"
|
|
46
54
|
textBaseline:
|
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.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@biomejs/biome": "^2.3.8",
|
|
13
|
+
"@tscircuit/alphabet": "^0.0.8",
|
|
13
14
|
"@types/bun": "latest",
|
|
14
15
|
"bun-match-svg": "^0.0.14",
|
|
15
16
|
"canvas": "^3.2.0",
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"tsup": "^8.5.1"
|
|
18
19
|
},
|
|
19
20
|
"peerDependencies": {
|
|
21
|
+
"@tscircuit/alphabet": "*",
|
|
20
22
|
"typescript": "^5"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|