@tscircuit/pcb-viewer 1.0.0

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.
@@ -0,0 +1,15 @@
1
+ import { CanvasPrimitiveRenderer } from "./CanvasPrimitiveRenderer"
2
+ import { AnyElement } from "@tscircuit/builder"
3
+ import { useMemo } from "react"
4
+ import { convertElementToPrimitives } from "../lib/convert-element-to-primitive"
5
+
6
+ export interface CanvasElementsRendererProps {
7
+ elements: AnyElement[]
8
+ }
9
+
10
+ export const CanvasElementsRenderer = (props: CanvasElementsRendererProps) => {
11
+ const primitives = useMemo(() => {
12
+ return props.elements.flatMap((elm) => convertElementToPrimitives(elm))
13
+ }, [props.elements])
14
+ return <CanvasPrimitiveRenderer primitives={primitives} />
15
+ }
@@ -0,0 +1,25 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import { drawPrimitives } from "../lib/draw-primitives"
3
+ import { Drawer } from "../lib/Drawer"
4
+ import { Primitive } from "../lib/types"
5
+
6
+ interface Props {
7
+ primitives: Primitive[]
8
+ defaultUnit?: string
9
+ }
10
+
11
+ export const CanvasPrimitiveRenderer = ({ primitives }: Props) => {
12
+ const ref = useRef()
13
+ let [width, height] = [500, 500]
14
+ useEffect(() => {
15
+ const drawer = new Drawer(ref.current)
16
+ drawer.clear()
17
+ drawPrimitives(drawer, primitives)
18
+ }, [primitives])
19
+
20
+ return (
21
+ <div style={{ backgroundColor: "black" }}>
22
+ <canvas ref={ref} width={width} height={height}></canvas>
23
+ </div>
24
+ )
25
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./PCBViewer"
2
+ export { CanvasElementsRenderer } from "./components/CanvasElementsRenderer"
@@ -0,0 +1,168 @@
1
+ import { scaleOnly } from "./util/scale-only"
2
+ import {
3
+ identity,
4
+ Matrix,
5
+ applyToPoint,
6
+ translate,
7
+ compose,
8
+ } from "transformation-matrix"
9
+ import colors from "./colors"
10
+ import { convertTextToLines } from "./convert-text-to-lines"
11
+
12
+ export interface Aperture {
13
+ shape: "circle" | "square"
14
+ size: number
15
+ mode: "add" | "subtract"
16
+ fontSize: number
17
+ color: string
18
+ }
19
+
20
+ export const LAYER_NAME_TO_COLOR = {
21
+ // Standard colors, you shouldn't use these except for testing
22
+ red: "red",
23
+ black: "black",
24
+ green: "green",
25
+ // TODO more builtin html colors
26
+
27
+ // Common eagle names
28
+ top: colors.board.copper.f,
29
+ keepout: colors.board.background,
30
+ tkeepout: colors.board.b_crtyd,
31
+ tplace: colors.board.b_silks,
32
+
33
+ ...colors.board,
34
+ }
35
+
36
+ export const FILL_TYPES = {
37
+ 0: "Empty",
38
+ 1: "Solid",
39
+ 2: "Line",
40
+ 3: "LtSlash",
41
+ 4: "Slash",
42
+ 5: "BkSlash",
43
+ 6: "LtBkSlash",
44
+ 7: "Hatch",
45
+ 8: "XHatch",
46
+ 9: "Interleave",
47
+ 10: "WideDot",
48
+ 11: "CloseDot",
49
+ 12: "Stipple1",
50
+ 13: "Stipple2",
51
+ 14: "Stipple3",
52
+ 15: "Stipple4",
53
+ }
54
+
55
+ export class Drawer {
56
+ ctx: CanvasRenderingContext2D
57
+ aperture: Aperture
58
+ transform: Matrix
59
+ lastPoint: { x: number; y: number }
60
+
61
+ constructor(public canvas: HTMLCanvasElement) {
62
+ this.canvas = canvas
63
+ this.ctx = canvas.getContext("2d")
64
+ this.transform = identity()
65
+ // positive is up (cartesian)
66
+ this.transform.d *= -1
67
+ this.transform = compose(this.transform, translate(0, -500))
68
+ }
69
+
70
+ clear() {
71
+ const { ctx, canvas } = this
72
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
73
+ }
74
+
75
+ equip(aperature: Partial<Aperture>) {
76
+ this.aperture = {
77
+ fontSize: 0,
78
+ shape: "circle",
79
+ mode: "add",
80
+ size: 0,
81
+ color: "red",
82
+ ...aperature,
83
+ }
84
+ }
85
+
86
+ rect(x: number, y: number, w: number, h: number) {
87
+ const [x1$, y1$] = applyToPoint(this.transform, [x, y])
88
+ const [x2$, y2$] = applyToPoint(this.transform, [x + w, y + h])
89
+ this.applyAperture()
90
+ this.ctx.fillRect(x1$, y1$, x2$ - x1$, y2$ - y1$)
91
+ }
92
+
93
+ circle(x: number, y: number, r: number) {
94
+ const r$ = scaleOnly(this.transform, r)
95
+ const [x$, y$] = applyToPoint(this.transform, [x, y])
96
+ this.applyAperture()
97
+ this.ctx.beginPath()
98
+ this.ctx.arc(x$, y$, r$ * 2, 0, 2 * Math.PI)
99
+ this.ctx.fill()
100
+ this.ctx.closePath()
101
+ }
102
+
103
+ /* NOTE: This is not gerber compatible */
104
+ text(text: string, x: number, y: number) {
105
+ const [x$, y$] = applyToPoint(this.transform, [x, y])
106
+ this.applyAperture()
107
+
108
+ this.ctx.fillText(text, x$, y$)
109
+ }
110
+
111
+ applyAperture() {
112
+ const { ctx, transform, aperture } = this
113
+ const { size, mode, color, fontSize } = aperture
114
+ ctx.lineWidth = scaleOnly(transform, size)
115
+ ctx.lineCap = "round"
116
+ if (mode === "add") {
117
+ let colorString =
118
+ color[0] === "#" || color.startsWith("rgb")
119
+ ? color
120
+ : LAYER_NAME_TO_COLOR[color.toLowerCase()]
121
+ ? LAYER_NAME_TO_COLOR[color.toLowerCase()]
122
+ : null
123
+ if (colorString === null) {
124
+ console.warn(`Color mapping for "${color}" not found`)
125
+ colorString = "white"
126
+ }
127
+ ctx.fillStyle = colorString
128
+ ctx.strokeStyle = colorString
129
+ } else {
130
+ ctx.globalCompositeOperation = "destination-out"
131
+ ctx.fillStyle = "rgba(0,0,0,1)"
132
+ ctx.strokeStyle = "rgba(0,0,0,1)"
133
+ }
134
+ ctx.font = `${scaleOnly(transform, fontSize)}px sans-serif`
135
+ }
136
+
137
+ moveTo(x: number, y: number) {
138
+ this.lastPoint = { x, y }
139
+ }
140
+ lineTo(x: number, y: number) {
141
+ const [x$, y$] = applyToPoint(this.transform, [x, y])
142
+ const { size, shape, mode } = this.aperture
143
+ const size$ = scaleOnly(this.transform, size)
144
+ let { lastPoint, ctx } = this
145
+ const lastPoint$ = applyToPoint(this.transform, lastPoint)
146
+
147
+ this.applyAperture()
148
+
149
+ if (shape === "square")
150
+ ctx.fillRect(
151
+ lastPoint$.x - size$ / 2,
152
+ lastPoint$.y - size$ / 2,
153
+ size$,
154
+ size$
155
+ )
156
+ ctx.beginPath()
157
+ ctx.moveTo(lastPoint$.x, lastPoint$.y)
158
+ ctx.lineTo(x$, y$)
159
+
160
+ ctx.stroke()
161
+ ctx.closePath()
162
+
163
+ if (shape === "square")
164
+ ctx.fillRect(x$ - size$ / 2, y$ - size$ / 2, size$, size$)
165
+
166
+ this.lastPoint = { x, y }
167
+ }
168
+ }
@@ -0,0 +1,233 @@
1
+ // Mix of Eagle Dark for schematics and KiCAD 2020 for PCBs
2
+ // https://github.com/pointhi/kicad-color-schemes/blob/master/eagle-dark/eagle-dark.jsonO
3
+ // https://github.com/pointhi/kicad-color-schemes/blob/master/kicad-2020/kicad_2020.json
4
+ export default {
5
+ "3d_viewer": {
6
+ background_bottom: "rgb(102, 102, 128)",
7
+ background_top: "rgb(204, 204, 230)",
8
+ board: "rgb(51, 43, 23)",
9
+ copper: "rgb(179, 156, 0)",
10
+ silkscreen_bottom: "rgb(230, 230, 230)",
11
+ silkscreen_top: "rgb(230, 230, 230)",
12
+ soldermask: "rgb(20, 51, 36)",
13
+ solderpaste: "rgb(128, 128, 128)",
14
+ },
15
+ board: {
16
+ anchor: "rgb(255, 38, 226)",
17
+ aux_items: "rgb(255, 255, 255)",
18
+ b_adhes: "rgb(0, 0, 132)",
19
+ b_crtyd: "rgb(255, 38, 226)",
20
+ b_fab: "rgb(88, 93, 132)",
21
+ b_mask: "rgba(2, 255, 238, 0.400)",
22
+ b_paste: "rgb(0, 194, 194)",
23
+ b_silks: "rgb(232, 178, 167)",
24
+ background: "rgb(0, 16, 35)",
25
+ cmts_user: "rgb(89, 148, 220)",
26
+ copper: {
27
+ b: "rgb(77, 127, 196)",
28
+ f: "rgb(200, 52, 52)",
29
+ in1: "rgb(127, 200, 127)",
30
+ in10: "rgb(237, 124, 51)",
31
+ in11: "rgb(91, 195, 235)",
32
+ in12: "rgb(247, 111, 142)",
33
+ in13: "rgb(167, 165, 198)",
34
+ in14: "rgb(40, 204, 217)",
35
+ in15: "rgb(232, 178, 167)",
36
+ in16: "rgb(242, 237, 161)",
37
+ in17: "rgb(237, 124, 51)",
38
+ in18: "rgb(91, 195, 235)",
39
+ in19: "rgb(247, 111, 142)",
40
+ in2: "rgb(206, 125, 44)",
41
+ in20: "rgb(167, 165, 198)",
42
+ in21: "rgb(40, 204, 217)",
43
+ in22: "rgb(232, 178, 167)",
44
+ in23: "rgb(242, 237, 161)",
45
+ in24: "rgb(237, 124, 51)",
46
+ in25: "rgb(91, 195, 235)",
47
+ in26: "rgb(247, 111, 142)",
48
+ in27: "rgb(167, 165, 198)",
49
+ in28: "rgb(40, 204, 217)",
50
+ in29: "rgb(232, 178, 167)",
51
+ in3: "rgb(79, 203, 203)",
52
+ in30: "rgb(242, 237, 161)",
53
+ in4: "rgb(219, 98, 139)",
54
+ in5: "rgb(167, 165, 198)",
55
+ in6: "rgb(40, 204, 217)",
56
+ in7: "rgb(232, 178, 167)",
57
+ in8: "rgb(242, 237, 161)",
58
+ in9: "rgb(141, 203, 129)",
59
+ },
60
+ cursor: "rgb(255, 255, 255)",
61
+ drc: "rgb(194, 194, 194)",
62
+ drc_error: "rgba(215, 91, 107, 0.800)",
63
+ drc_exclusion: "rgb(255, 255, 255)",
64
+ drc_warning: "rgba(255, 208, 66, 0.902)",
65
+ dwgs_user: "rgb(194, 194, 194)",
66
+ eco1_user: "rgb(180, 219, 210)",
67
+ eco2_user: "rgb(216, 200, 82)",
68
+ edge_cuts: "rgb(208, 210, 205)",
69
+ f_adhes: "rgb(132, 0, 132)",
70
+ f_crtyd: "rgb(255, 0, 245)",
71
+ f_fab: "rgb(175, 175, 175)",
72
+ f_mask: "rgba(216, 100, 255, 0.400)",
73
+ f_paste: "rgba(180, 160, 154, 0.902)",
74
+ f_silks: "rgb(242, 237, 161)",
75
+ footprint_text_back: "rgb(0, 0, 132)",
76
+ footprint_text_front: "rgb(194, 194, 194)",
77
+ footprint_text_invisible: "rgb(132, 132, 132)",
78
+ grid: "rgb(132, 132, 132)",
79
+ grid_axes: "rgb(194, 194, 194)",
80
+ margin: "rgb(255, 38, 226)",
81
+ microvia: "rgb(0, 132, 132)",
82
+ no_connect: "rgb(0, 0, 132)",
83
+ pad_back: "rgb(77, 127, 196)",
84
+ pad_front: "rgb(200, 52, 52)",
85
+ pad_plated_hole: "rgb(194, 194, 0)",
86
+ pad_through_hole: "rgb(227, 183, 46)",
87
+ plated_hole: "rgb(26, 196, 210)",
88
+ ratsnest: "rgba(245, 255, 213, 0.702)",
89
+ select_overlay: "rgb(4, 255, 67)",
90
+ through_via: "rgb(236, 236, 236)",
91
+ user_1: "rgb(194, 194, 194)",
92
+ user_2: "rgb(89, 148, 220)",
93
+ user_3: "rgb(180, 219, 210)",
94
+ user_4: "rgb(216, 200, 82)",
95
+ user_5: "rgb(194, 194, 194)",
96
+ user_6: "rgb(89, 148, 220)",
97
+ user_7: "rgb(180, 219, 210)",
98
+ user_8: "rgb(216, 200, 82)",
99
+ user_9: "rgb(232, 178, 167)",
100
+ via_blind_buried: "rgb(187, 151, 38)",
101
+ via_hole: "rgb(227, 183, 46)",
102
+ via_micro: "rgb(0, 132, 132)",
103
+ via_through: "rgb(236, 236, 236)",
104
+ worksheet: "rgb(200, 114, 171)",
105
+ },
106
+ gerbview: {
107
+ axes: "rgb(0, 0, 132)",
108
+ background: "rgb(0, 0, 0)",
109
+ dcodes: "rgb(255, 255, 255)",
110
+ grid: "rgb(132, 132, 132)",
111
+ layers: [
112
+ "rgb(132, 0, 0)",
113
+ "rgb(194, 194, 0)",
114
+ "rgb(194, 0, 194)",
115
+ "rgb(194, 0, 0)",
116
+ "rgb(0, 132, 132)",
117
+ "rgb(0, 132, 0)",
118
+ "rgb(0, 0, 132)",
119
+ "rgb(132, 132, 132)",
120
+ "rgb(132, 0, 132)",
121
+ "rgb(194, 194, 194)",
122
+ "rgb(132, 0, 132)",
123
+ "rgb(132, 0, 0)",
124
+ "rgb(132, 132, 0)",
125
+ "rgb(194, 194, 194)",
126
+ "rgb(0, 0, 132)",
127
+ "rgb(0, 132, 0)",
128
+ "rgb(132, 0, 0)",
129
+ "rgb(194, 194, 0)",
130
+ "rgb(194, 0, 194)",
131
+ "rgb(194, 0, 0)",
132
+ "rgb(0, 132, 132)",
133
+ "rgb(0, 132, 0)",
134
+ "rgb(0, 0, 132)",
135
+ "rgb(132, 132, 132)",
136
+ "rgb(132, 0, 132)",
137
+ "rgb(194, 194, 194)",
138
+ "rgb(132, 0, 132)",
139
+ "rgb(132, 0, 0)",
140
+ "rgb(132, 132, 0)",
141
+ "rgb(194, 194, 194)",
142
+ "rgb(0, 0, 132)",
143
+ "rgb(0, 132, 0)",
144
+ "rgb(132, 0, 0)",
145
+ "rgb(194, 194, 0)",
146
+ "rgb(194, 0, 194)",
147
+ "rgb(194, 0, 0)",
148
+ "rgb(0, 132, 132)",
149
+ "rgb(0, 132, 0)",
150
+ "rgb(0, 0, 132)",
151
+ "rgb(132, 132, 132)",
152
+ "rgb(132, 0, 132)",
153
+ "rgb(194, 194, 194)",
154
+ "rgb(132, 0, 132)",
155
+ "rgb(132, 0, 0)",
156
+ "rgb(132, 132, 0)",
157
+ "rgb(194, 194, 194)",
158
+ "rgb(0, 0, 132)",
159
+ "rgb(0, 132, 0)",
160
+ "rgb(132, 0, 0)",
161
+ "rgb(194, 194, 0)",
162
+ "rgb(194, 0, 194)",
163
+ "rgb(194, 0, 0)",
164
+ "rgb(0, 132, 132)",
165
+ "rgb(0, 132, 0)",
166
+ "rgb(0, 0, 132)",
167
+ "rgb(132, 132, 132)",
168
+ "rgb(132, 0, 132)",
169
+ "rgb(194, 194, 194)",
170
+ "rgb(132, 0, 132)",
171
+ "rgb(132, 0, 0)",
172
+ ],
173
+ negative_objects: "rgb(132, 132, 132)",
174
+ worksheet: "rgb(0, 0, 132)",
175
+ },
176
+ palette: [
177
+ "rgb(132, 0, 0)",
178
+ "rgb(194, 194, 0)",
179
+ "rgb(194, 0, 194)",
180
+ "rgb(194, 0, 0)",
181
+ "rgb(0, 132, 132)",
182
+ "rgb(0, 132, 0)",
183
+ "rgb(0, 0, 132)",
184
+ "rgb(132, 132, 132)",
185
+ "rgb(132, 0, 132)",
186
+ "rgb(194, 194, 194)",
187
+ "rgb(132, 0, 132)",
188
+ "rgb(132, 0, 0)",
189
+ "rgb(132, 132, 0)",
190
+ "rgb(194, 194, 194)",
191
+ "rgb(0, 0, 132)",
192
+ "rgb(0, 132, 0)",
193
+ ],
194
+ schematic: {
195
+ anchor: "rgb(0, 0, 255)",
196
+ aux_items: "rgb(0, 0, 0)",
197
+ background: "rgb(33, 33, 33)",
198
+ brightened: "rgb(204, 204, 204)",
199
+ bus: "rgb(0, 96, 192)",
200
+ bus_junction: "rgb(0, 96, 192)",
201
+ component_body: "rgb(44, 44, 44)",
202
+ component_outline: "rgb(192, 0, 0)",
203
+ cursor: "rgb(224, 224, 224)",
204
+ erc_error: "rgba(192, 48, 48, 0.800)",
205
+ erc_warning: "rgba(192, 140, 0, 0.800)",
206
+ fields: "rgb(128, 0, 160)",
207
+ grid: "rgb(60, 60, 60)",
208
+ grid_axes: "rgb(60, 60, 60)",
209
+ hidden: "rgb(194, 194, 194)",
210
+ junction: "rgb(0, 160, 0)",
211
+ label_global: "rgb(0, 160, 224)",
212
+ label_hier: "rgb(160, 160, 0)",
213
+ label_local: "rgb(192, 192, 192)",
214
+ net_name: "rgb(224, 224, 224)",
215
+ no_connect: "rgb(97, 43, 224)",
216
+ note: "rgb(192, 192, 0)",
217
+ override_item_colors: false,
218
+ pin: "rgb(192, 0, 0)",
219
+ pin_name: "rgb(192, 192, 192)",
220
+ pin_number: "rgb(192, 0, 0)",
221
+ reference: "rgb(192, 192, 192)",
222
+ shadow: "rgba(102, 179, 255, 0.800)",
223
+ sheet: "rgb(128, 0, 160)",
224
+ sheet_background: "rgba(255, 255, 255, 0.000)",
225
+ sheet_fields: "rgb(160, 160, 0)",
226
+ sheet_filename: "rgb(160, 160, 0)",
227
+ sheet_label: "rgb(160, 160, 0)",
228
+ sheet_name: "rgb(0, 160, 204)",
229
+ value: "rgb(192, 192, 192)",
230
+ wire: "rgb(0, 160, 0)",
231
+ worksheet: "rgb(192, 0, 0)",
232
+ },
233
+ }
@@ -0,0 +1,31 @@
1
+ import { AnyElement } from "@tscircuit/builder"
2
+ import { Primitive } from "./types"
3
+
4
+ export const convertElementToPrimitives = (
5
+ element: AnyElement
6
+ ): Primitive[] => {
7
+ switch (element.type) {
8
+ case "pcb_smtpad": {
9
+ if (element.shape === "rect") {
10
+ const { shape, x, y, width, height, layer } = element
11
+
12
+ return [
13
+ {
14
+ pcb_drawing_type: "rect",
15
+ x,
16
+ y,
17
+ w: width,
18
+ h: height,
19
+ layer: layer,
20
+ },
21
+ ]
22
+ } else if (element.shape === "circle") {
23
+ console.warn(`Unsupported shape: ${element.shape} for pcb_smtpad`)
24
+ return []
25
+ }
26
+ }
27
+ }
28
+
29
+ console.warn(`Unsupported element type: ${element.type}`)
30
+ return []
31
+ }
@@ -0,0 +1,31 @@
1
+ import { lineAlphabet } from "../assets/alphabet"
2
+ import { Line, Text } from "./types"
3
+
4
+ export const convertTextToLines = (text: Text): Line[] => {
5
+ const strokeWidth = text.size / 8
6
+ const letterWidth = text.size * 0.6
7
+ const letterSpace = text.size * 0.2
8
+
9
+ const lines: Line[] = []
10
+ for (let letterIndex = 0; letterIndex < text.text.length; letterIndex++) {
11
+ const letter = text.text[letterIndex]
12
+ const letterLines = lineAlphabet[letter.toUpperCase()]
13
+ if (!letterLines) continue
14
+ for (const { x1, y1, x2, y2 } of letterLines) {
15
+ lines.push({
16
+ pcb_drawing_type: "line",
17
+ x1:
18
+ text.x + (letterWidth + letterSpace) * letterIndex + letterWidth * x1,
19
+ y1: text.y + text.size * y1,
20
+ x2:
21
+ text.x + (letterWidth + letterSpace) * letterIndex + letterWidth * x2,
22
+ y2: text.y + text.size * y2,
23
+ width: strokeWidth,
24
+ layer: text.layer,
25
+ unit: text.unit,
26
+ })
27
+ }
28
+ }
29
+
30
+ return lines
31
+ }
@@ -0,0 +1,45 @@
1
+ import { EagleJSON, Layer } from "@tscircuit/eagle-xml-converter"
2
+ import {
3
+ compose,
4
+ fromDefinition,
5
+ fromTransformAttribute,
6
+ } from "transformation-matrix"
7
+ import { Drawer } from "./Drawer"
8
+
9
+ export const drawEagle = (drawer: Drawer, eagle: EagleJSON) => {
10
+ const pkg = eagle.library.packages[0]
11
+
12
+ if (eagle.grid.unit === "inch") {
13
+ drawer.transform = compose(
14
+ fromDefinition(
15
+ fromTransformAttribute("translate(200, 200) scale(30,-30)")
16
+ )
17
+ )
18
+ }
19
+
20
+ const layerMap: Record<number, Layer> = {}
21
+ for (const layer of eagle.layers) {
22
+ layerMap[layer.number] = layer
23
+ }
24
+
25
+ for (const smd of pkg.smd || []) {
26
+ drawer.equip({
27
+ color: layerMap[smd.layer].name,
28
+ })
29
+ drawer.rect(smd.x - smd.dx / 2, smd.y - smd.dy / 2, smd.dx, smd.dy)
30
+ }
31
+ for (const wire of pkg.wire || []) {
32
+ drawer.equip({
33
+ size: wire.width,
34
+ shape: "circle",
35
+ color: layerMap[wire.layer].name,
36
+ })
37
+ drawer.moveTo(wire.x1, wire.y1)
38
+ drawer.lineTo(wire.x2, wire.y2)
39
+ }
40
+
41
+ for (const circle of pkg.circle || []) {
42
+ drawer.equip({ color: layerMap[circle.layer].name })
43
+ drawer.circle(circle.x, circle.y, circle.radius)
44
+ }
45
+ }
@@ -0,0 +1,69 @@
1
+ import { Primitive, Line, Text, Circle, Rect } from "./types"
2
+ import { Drawer } from "./Drawer"
3
+ import { convertTextToLines } from "./convert-text-to-lines"
4
+
5
+ export const drawLine = (drawer: Drawer, line: Line) => {
6
+ drawer.equip({
7
+ size: line.width,
8
+ shape: line.squareCap ? "square" : "circle",
9
+ color: line.layer.name,
10
+ })
11
+ drawer.moveTo(line.x1, line.y1)
12
+ drawer.lineTo(line.x2, line.y2)
13
+ }
14
+
15
+ export const drawText = (drawer: Drawer, text: Text) => {
16
+ drawer.equip({
17
+ fontSize: text.size,
18
+ color: text.layer.name,
19
+ })
20
+ // TODO handle align
21
+ if (text.align && text.align !== "top-left") {
22
+ console.warn("Unhandled text align", text.align)
23
+ }
24
+
25
+ // Non-gerber compatible
26
+ // drawer.text(text.text, text.x, text.y)
27
+
28
+ const lines = convertTextToLines(text)
29
+ for (const line of lines) {
30
+ drawLine(drawer, line)
31
+ }
32
+ }
33
+
34
+ export const drawRect = (drawer: Drawer, rect: Rect) => {
35
+ drawer.equip({
36
+ color: rect.layer.name,
37
+ })
38
+ if (rect.align && rect.align !== "center") {
39
+ console.warn("Unhandled rect align", rect.align)
40
+ }
41
+ drawer.rect(rect.x, rect.y, rect.w, rect.h)
42
+ }
43
+
44
+ export const drawCircle = (drawer: Drawer, circle: Circle) => {
45
+ drawer.equip({
46
+ color: circle.layer.name,
47
+ })
48
+ drawer.circle(circle.x, circle.y, circle.r)
49
+ }
50
+
51
+ export const drawPrimitive = (drawer: Drawer, primitive: Primitive) => {
52
+ switch (primitive.pcb_drawing_type) {
53
+ case "line":
54
+ return drawLine(drawer, primitive)
55
+ case "text":
56
+ return drawText(drawer, primitive)
57
+ case "rect":
58
+ return drawRect(drawer, primitive)
59
+ case "circle":
60
+ return drawCircle(drawer, primitive)
61
+ }
62
+ throw new Error(
63
+ `Unknown primitive type: ${(primitive as any).pcb_drawing_type}`
64
+ )
65
+ }
66
+
67
+ export const drawPrimitives = (drawer: Drawer, primitives: Primitive[]) => {
68
+ primitives.forEach((primitive) => drawPrimitive(drawer, primitive))
69
+ }
@@ -0,0 +1,54 @@
1
+ export type AlignString =
2
+ | "top-left"
3
+ | "top-center"
4
+ | "top-right"
5
+ | "center-left"
6
+ | "center"
7
+ | "center-right"
8
+ | "bottom-left"
9
+ | "bottom-center"
10
+ | "bottom-right"
11
+
12
+ export interface LayerRef {
13
+ name: string
14
+ }
15
+
16
+ export interface PCBDrawingObject {
17
+ layer: LayerRef
18
+ unit?: string
19
+ }
20
+
21
+ export interface Line extends PCBDrawingObject {
22
+ pcb_drawing_type: "line"
23
+ x1: number
24
+ y1: number
25
+ x2: number
26
+ y2: number
27
+ squareCap?: boolean
28
+ width: number
29
+ }
30
+ export interface Text extends PCBDrawingObject {
31
+ pcb_drawing_type: "text"
32
+ text: string
33
+ x: number
34
+ y: number
35
+ size: number
36
+ align?: AlignString
37
+ }
38
+ export interface Rect extends PCBDrawingObject {
39
+ pcb_drawing_type: "rect"
40
+ x: number
41
+ y: number
42
+ w: number
43
+ h: number
44
+ roundness?: number
45
+ align?: AlignString
46
+ }
47
+ export interface Circle extends PCBDrawingObject {
48
+ pcb_drawing_type: "circle"
49
+ x: number
50
+ y: number
51
+ r: number
52
+ }
53
+
54
+ export type Primitive = Line | Text | Rect | Circle
@@ -0,0 +1,7 @@
1
+ import { Matrix } from "transformation-matrix"
2
+
3
+ export const scaleOnly = (mat: Matrix, value: number) => {
4
+ if (Math.abs(mat.a) !== Math.abs(mat.d))
5
+ throw new Error("Cannot scale non-uniformly")
6
+ return value * Math.abs(mat.a)
7
+ }