circuit-to-canvas 0.0.1

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 (38) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +30 -0
  3. package/biome.json +93 -0
  4. package/bunfig.toml +6 -0
  5. package/dist/index.d.ts +151 -0
  6. package/dist/index.js +375 -0
  7. package/lib/drawer/CircuitToCanvasDrawer.ts +119 -0
  8. package/lib/drawer/elements/index.ts +4 -0
  9. package/lib/drawer/elements/pcb-plated-hole.ts +168 -0
  10. package/lib/drawer/index.ts +5 -0
  11. package/lib/drawer/shapes/circle.ts +23 -0
  12. package/lib/drawer/shapes/index.ts +4 -0
  13. package/lib/drawer/shapes/oval.ts +34 -0
  14. package/lib/drawer/shapes/pill.ts +60 -0
  15. package/lib/drawer/shapes/rect.ts +69 -0
  16. package/lib/drawer/types.ts +125 -0
  17. package/lib/index.ts +1 -0
  18. package/lib/pcb/index.ts +5 -0
  19. package/package.json +25 -0
  20. package/tests/__snapshots__/svg.snap.svg +3 -0
  21. package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
  22. package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
  23. package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
  24. package/tests/elements/pcb-plated-hole.test.ts +90 -0
  25. package/tests/fixtures/png-matcher.ts +159 -0
  26. package/tests/fixtures/preload.ts +2 -0
  27. package/tests/shapes/__snapshots__/circle.snap.png +0 -0
  28. package/tests/shapes/__snapshots__/oval.snap.png +0 -0
  29. package/tests/shapes/__snapshots__/pill-vertical.snap.png +0 -0
  30. package/tests/shapes/__snapshots__/pill.snap.png +0 -0
  31. package/tests/shapes/__snapshots__/rect-rounded.snap.png +0 -0
  32. package/tests/shapes/__snapshots__/rect.snap.png +0 -0
  33. package/tests/shapes/circle.test.ts +24 -0
  34. package/tests/shapes/oval.test.ts +25 -0
  35. package/tests/shapes/pill.test.ts +47 -0
  36. package/tests/shapes/rect.test.ts +48 -0
  37. package/tests/svg.test.ts +11 -0
  38. package/tsconfig.json +31 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": ["Bash(npm ls:*)", "Bash(bun add:*)"],
4
+ "deny": [],
5
+ "ask": []
6
+ }
7
+ }
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # circuit-to-canvas
2
+
3
+ Draw [Circuit JSON](https://github.com/tscircuit/circuit-json) into a Canvas- works with any canvas object (Node/Vanilla)
4
+
5
+ ```tsx
6
+ const drawer = new CircuitToCanvasDrawer(canvasOrCanvasRenderingContext2d)
7
+
8
+ // Sets the internal transformation matrix for all operations
9
+ drawer.setCameraBounds({ minX: 0, maxX: 100, minY: 0, maxY: 100 })
10
+
11
+ drawer.configure({
12
+ colorOverrides: {
13
+ topCopper: "#ff0000"
14
+ }
15
+ })
16
+
17
+ // Accepts a circuit json array, by default draws on all layers
18
+ drawer.drawElements([pcbPlatedHole], {
19
+ layers: ["top_copper"]
20
+ })
21
+ ```
22
+
23
+ ## Implementation Notes
24
+
25
+ There are two "types" of layers:
26
+
27
+ - Specific drawing layers e.g. "top_copper"
28
+ - Layer groups "top" (includes "top_copper", "top_soldermask")
29
+
30
+ inner layers go by the name inner1, inner2 etc.
package/biome.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
3
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
4
+ "formatter": {
5
+ "enabled": true,
6
+ "indentStyle": "space"
7
+ },
8
+ "files": {
9
+ "includes": ["**", "!**/cosmos-export", "!**/dist", "!**/package.json"]
10
+ },
11
+ "javascript": {
12
+ "formatter": {
13
+ "jsxQuoteStyle": "double",
14
+ "quoteProperties": "asNeeded",
15
+ "trailingCommas": "all",
16
+ "semicolons": "asNeeded",
17
+ "arrowParentheses": "always",
18
+ "bracketSpacing": true,
19
+ "bracketSameLine": false
20
+ }
21
+ },
22
+ "linter": {
23
+ "enabled": true,
24
+ "rules": {
25
+ "recommended": true,
26
+ "suspicious": {
27
+ "noExplicitAny": "off"
28
+ },
29
+ "complexity": {
30
+ "noForEach": "error",
31
+ "useLiteralKeys": "off"
32
+ },
33
+ "a11y": {
34
+ "noAccessKey": "off",
35
+ "noAriaHiddenOnFocusable": "off",
36
+ "noAriaUnsupportedElements": "off",
37
+ "noAutofocus": "off",
38
+ "noDistractingElements": "off",
39
+ "noHeaderScope": "off",
40
+ "noInteractiveElementToNoninteractiveRole": "off",
41
+ "noLabelWithoutControl": "off",
42
+ "noNoninteractiveElementToInteractiveRole": "off",
43
+ "noNoninteractiveTabindex": "off",
44
+ "noPositiveTabindex": "off",
45
+ "noRedundantAlt": "off",
46
+ "noRedundantRoles": "off",
47
+ "noStaticElementInteractions": "off",
48
+ "noSvgWithoutTitle": "off",
49
+ "useAltText": "off",
50
+ "useAnchorContent": "off",
51
+ "useAriaActivedescendantWithTabindex": "off",
52
+ "useAriaPropsForRole": "off",
53
+ "useAriaPropsSupportedByRole": "off",
54
+ "useButtonType": "off",
55
+ "useFocusableInteractive": "off",
56
+ "useHeadingContent": "off",
57
+ "useHtmlLang": "off",
58
+ "useIframeTitle": "off",
59
+ "useKeyWithClickEvents": "off",
60
+ "useKeyWithMouseEvents": "off",
61
+ "useMediaCaption": "off",
62
+ "useSemanticElements": "off",
63
+ "useValidAnchor": "off",
64
+ "useValidAriaProps": "off",
65
+ "useValidAriaRole": "off",
66
+ "useValidAriaValues": "off",
67
+ "useValidAutocomplete": "off",
68
+ "useValidLang": "off"
69
+ },
70
+ "style": {
71
+ "useSingleVarDeclarator": "error",
72
+ "noParameterAssign": "off",
73
+ "noUselessElse": "off",
74
+ "noNonNullAssertion": "off",
75
+ "useNumberNamespace": "off",
76
+ "noUnusedTemplateLiteral": "off",
77
+ "useFilenamingConvention": {
78
+ "level": "error",
79
+ "options": {
80
+ "strictCase": true,
81
+ "requireAscii": true,
82
+ "filenameCases": ["kebab-case", "export"]
83
+ }
84
+ },
85
+ "useAsConstAssertion": "error",
86
+ "useDefaultParameterLast": "error",
87
+ "useEnumInitializers": "error",
88
+ "useSelfClosingElements": "error",
89
+ "noInferrableTypes": "error"
90
+ }
91
+ }
92
+ }
93
+ }
package/bunfig.toml ADDED
@@ -0,0 +1,6 @@
1
+ [test]
2
+ preload = ["./tests/fixtures/preload.ts"]
3
+ root = "./tests"
4
+
5
+ [install.lockfile]
6
+ save = false
@@ -0,0 +1,151 @@
1
+ import { AnyCircuitElement, PcbPlatedHole } from 'circuit-json';
2
+ import { Matrix } from 'transformation-matrix';
3
+
4
+ /**
5
+ * Canvas context type that works with both browser and node-canvas.
6
+ * Uses a subset of CanvasRenderingContext2D methods that are common to both.
7
+ */
8
+ interface CanvasContext {
9
+ beginPath(): void;
10
+ closePath(): void;
11
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number): void;
12
+ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
13
+ ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number): void;
14
+ fill(): void;
15
+ rect(x: number, y: number, w: number, h: number): void;
16
+ lineTo(x: number, y: number): void;
17
+ moveTo(x: number, y: number): void;
18
+ save(): void;
19
+ restore(): void;
20
+ translate(x: number, y: number): void;
21
+ rotate(angle: number): void;
22
+ fillStyle: string | CanvasGradient | CanvasPattern;
23
+ canvas: {
24
+ width: number;
25
+ height: number;
26
+ };
27
+ }
28
+ type CopperLayerName = "top" | "bottom" | "inner1" | "inner2" | "inner3" | "inner4" | "inner5" | "inner6";
29
+ type CopperColorMap = Record<CopperLayerName, string> & {
30
+ [layer: string]: string;
31
+ };
32
+ interface PcbColorMap {
33
+ copper: CopperColorMap;
34
+ drill: string;
35
+ silkscreen: {
36
+ top: string;
37
+ bottom: string;
38
+ };
39
+ boardOutline: string;
40
+ soldermask: {
41
+ top: string;
42
+ bottom: string;
43
+ };
44
+ soldermaskWithCopperUnderneath: {
45
+ top: string;
46
+ bottom: string;
47
+ };
48
+ soldermaskOverCopper: {
49
+ top: string;
50
+ bottom: string;
51
+ };
52
+ substrate: string;
53
+ courtyard: string;
54
+ }
55
+ declare const DEFAULT_PCB_COLOR_MAP: PcbColorMap;
56
+ interface DrawerConfig {
57
+ colorOverrides?: Partial<PcbColorMap>;
58
+ }
59
+ interface CameraBounds {
60
+ minX: number;
61
+ maxX: number;
62
+ minY: number;
63
+ maxY: number;
64
+ }
65
+ interface DrawContext {
66
+ ctx: CanvasRenderingContext2D;
67
+ transform: Matrix;
68
+ colorMap: PcbColorMap;
69
+ }
70
+
71
+ interface DrawElementsOptions {
72
+ layers?: string[];
73
+ }
74
+ interface CanvasLike {
75
+ getContext(contextId: "2d"): CanvasContext | null;
76
+ }
77
+ declare class CircuitToCanvasDrawer {
78
+ private ctx;
79
+ private colorMap;
80
+ realToCanvasMat: Matrix;
81
+ constructor(canvasOrContext: CanvasLike | CanvasContext);
82
+ configure(config: DrawerConfig): void;
83
+ setCameraBounds(bounds: CameraBounds): void;
84
+ drawElements(elements: AnyCircuitElement[], options?: DrawElementsOptions): void;
85
+ private drawElement;
86
+ }
87
+
88
+ interface DrawCircleParams {
89
+ ctx: CanvasContext;
90
+ center: {
91
+ x: number;
92
+ y: number;
93
+ };
94
+ radius: number;
95
+ fill: string;
96
+ transform: Matrix;
97
+ }
98
+ declare function drawCircle(params: DrawCircleParams): void;
99
+
100
+ interface DrawRectParams {
101
+ ctx: CanvasContext;
102
+ center: {
103
+ x: number;
104
+ y: number;
105
+ };
106
+ width: number;
107
+ height: number;
108
+ fill: string;
109
+ transform: Matrix;
110
+ borderRadius?: number;
111
+ rotation?: number;
112
+ }
113
+ declare function drawRect(params: DrawRectParams): void;
114
+
115
+ interface DrawOvalParams {
116
+ ctx: CanvasContext;
117
+ center: {
118
+ x: number;
119
+ y: number;
120
+ };
121
+ width: number;
122
+ height: number;
123
+ fill: string;
124
+ transform: Matrix;
125
+ rotation?: number;
126
+ }
127
+ declare function drawOval(params: DrawOvalParams): void;
128
+
129
+ interface DrawPillParams {
130
+ ctx: CanvasContext;
131
+ center: {
132
+ x: number;
133
+ y: number;
134
+ };
135
+ width: number;
136
+ height: number;
137
+ fill: string;
138
+ transform: Matrix;
139
+ rotation?: number;
140
+ }
141
+ declare function drawPill(params: DrawPillParams): void;
142
+
143
+ interface DrawPcbPlatedHoleParams {
144
+ ctx: CanvasContext;
145
+ hole: PcbPlatedHole;
146
+ transform: Matrix;
147
+ colorMap: PcbColorMap;
148
+ }
149
+ declare function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void;
150
+
151
+ export { type CameraBounds, type CanvasContext, CircuitToCanvasDrawer, type CopperColorMap, type CopperLayerName, DEFAULT_PCB_COLOR_MAP, type DrawCircleParams, type DrawContext, type DrawElementsOptions, type DrawOvalParams, type DrawPcbPlatedHoleParams, type DrawPillParams, type DrawRectParams, type DrawerConfig, type PcbColorMap, drawCircle, drawOval, drawPcbPlatedHole, drawPill, drawRect };
package/dist/index.js ADDED
@@ -0,0 +1,375 @@
1
+ // lib/drawer/CircuitToCanvasDrawer.ts
2
+ import { identity, compose, translate, scale } from "transformation-matrix";
3
+
4
+ // lib/drawer/types.ts
5
+ var DEFAULT_PCB_COLOR_MAP = {
6
+ copper: {
7
+ top: "rgb(200, 52, 52)",
8
+ inner1: "rgb(255, 140, 0)",
9
+ inner2: "rgb(255, 215, 0)",
10
+ inner3: "rgb(50, 205, 50)",
11
+ inner4: "rgb(64, 224, 208)",
12
+ inner5: "rgb(138, 43, 226)",
13
+ inner6: "rgb(255, 105, 180)",
14
+ bottom: "rgb(77, 127, 196)"
15
+ },
16
+ soldermaskWithCopperUnderneath: {
17
+ top: "rgb(18, 82, 50)",
18
+ bottom: "rgb(77, 127, 196)"
19
+ },
20
+ soldermask: {
21
+ top: "rgb(12, 55, 33)",
22
+ bottom: "rgb(12, 55, 33)"
23
+ },
24
+ soldermaskOverCopper: {
25
+ top: "rgb(52, 135, 73)",
26
+ bottom: "rgb(52, 135, 73)"
27
+ },
28
+ substrate: "rgb(201, 162, 110)",
29
+ drill: "#FF26E2",
30
+ silkscreen: {
31
+ top: "#f2eda1",
32
+ bottom: "#5da9e9"
33
+ },
34
+ boardOutline: "rgba(255, 255, 255, 0.5)",
35
+ courtyard: "#FF00FF"
36
+ };
37
+
38
+ // lib/drawer/shapes/circle.ts
39
+ import { applyToPoint } from "transformation-matrix";
40
+ function drawCircle(params) {
41
+ const { ctx, center, radius, fill, transform } = params;
42
+ const [cx, cy] = applyToPoint(transform, [center.x, center.y]);
43
+ const scaledRadius = radius * Math.abs(transform.a);
44
+ ctx.beginPath();
45
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
46
+ ctx.fillStyle = fill;
47
+ ctx.fill();
48
+ }
49
+
50
+ // lib/drawer/shapes/rect.ts
51
+ import { applyToPoint as applyToPoint2 } from "transformation-matrix";
52
+ function drawRect(params) {
53
+ const {
54
+ ctx,
55
+ center,
56
+ width,
57
+ height,
58
+ fill,
59
+ transform,
60
+ borderRadius = 0,
61
+ rotation = 0
62
+ } = params;
63
+ const [cx, cy] = applyToPoint2(transform, [center.x, center.y]);
64
+ const scaledWidth = width * Math.abs(transform.a);
65
+ const scaledHeight = height * Math.abs(transform.a);
66
+ const scaledRadius = borderRadius * Math.abs(transform.a);
67
+ ctx.save();
68
+ ctx.translate(cx, cy);
69
+ if (rotation !== 0) {
70
+ ctx.rotate(-rotation * (Math.PI / 180));
71
+ }
72
+ ctx.beginPath();
73
+ if (scaledRadius > 0) {
74
+ const x = -scaledWidth / 2;
75
+ const y = -scaledHeight / 2;
76
+ const r = Math.min(scaledRadius, scaledWidth / 2, scaledHeight / 2);
77
+ ctx.moveTo(x + r, y);
78
+ ctx.lineTo(x + scaledWidth - r, y);
79
+ ctx.arcTo(x + scaledWidth, y, x + scaledWidth, y + r, r);
80
+ ctx.lineTo(x + scaledWidth, y + scaledHeight - r);
81
+ ctx.arcTo(
82
+ x + scaledWidth,
83
+ y + scaledHeight,
84
+ x + scaledWidth - r,
85
+ y + scaledHeight,
86
+ r
87
+ );
88
+ ctx.lineTo(x + r, y + scaledHeight);
89
+ ctx.arcTo(x, y + scaledHeight, x, y + scaledHeight - r, r);
90
+ ctx.lineTo(x, y + r);
91
+ ctx.arcTo(x, y, x + r, y, r);
92
+ } else {
93
+ ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
94
+ }
95
+ ctx.fillStyle = fill;
96
+ ctx.fill();
97
+ ctx.restore();
98
+ }
99
+
100
+ // lib/drawer/shapes/oval.ts
101
+ import { applyToPoint as applyToPoint3 } from "transformation-matrix";
102
+ function drawOval(params) {
103
+ const { ctx, center, width, height, fill, transform, rotation = 0 } = params;
104
+ const [cx, cy] = applyToPoint3(transform, [center.x, center.y]);
105
+ const scaledWidth = width * Math.abs(transform.a);
106
+ const scaledHeight = height * Math.abs(transform.a);
107
+ ctx.save();
108
+ ctx.translate(cx, cy);
109
+ if (rotation !== 0) {
110
+ ctx.rotate(-rotation * (Math.PI / 180));
111
+ }
112
+ ctx.beginPath();
113
+ ctx.ellipse(0, 0, scaledWidth / 2, scaledHeight / 2, 0, 0, Math.PI * 2);
114
+ ctx.fillStyle = fill;
115
+ ctx.fill();
116
+ ctx.restore();
117
+ }
118
+
119
+ // lib/drawer/shapes/pill.ts
120
+ import { applyToPoint as applyToPoint4 } from "transformation-matrix";
121
+ function drawPill(params) {
122
+ const { ctx, center, width, height, fill, transform, rotation = 0 } = params;
123
+ const [cx, cy] = applyToPoint4(transform, [center.x, center.y]);
124
+ const scaledWidth = width * Math.abs(transform.a);
125
+ const scaledHeight = height * Math.abs(transform.a);
126
+ ctx.save();
127
+ ctx.translate(cx, cy);
128
+ if (rotation !== 0) {
129
+ ctx.rotate(-rotation * (Math.PI / 180));
130
+ }
131
+ ctx.beginPath();
132
+ if (scaledWidth > scaledHeight) {
133
+ const radius = scaledHeight / 2;
134
+ const straightLength = scaledWidth - scaledHeight;
135
+ ctx.moveTo(-straightLength / 2, -radius);
136
+ ctx.lineTo(straightLength / 2, -radius);
137
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
138
+ ctx.lineTo(-straightLength / 2, radius);
139
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
140
+ } else if (scaledHeight > scaledWidth) {
141
+ const radius = scaledWidth / 2;
142
+ const straightLength = scaledHeight - scaledWidth;
143
+ ctx.moveTo(radius, -straightLength / 2);
144
+ ctx.lineTo(radius, straightLength / 2);
145
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
146
+ ctx.lineTo(-radius, -straightLength / 2);
147
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
148
+ } else {
149
+ ctx.arc(0, 0, scaledWidth / 2, 0, Math.PI * 2);
150
+ }
151
+ ctx.closePath();
152
+ ctx.fillStyle = fill;
153
+ ctx.fill();
154
+ ctx.restore();
155
+ }
156
+
157
+ // lib/drawer/elements/pcb-plated-hole.ts
158
+ function drawPcbPlatedHole(params) {
159
+ const { ctx, hole, transform, colorMap } = params;
160
+ if (hole.shape === "circle") {
161
+ drawCircle({
162
+ ctx,
163
+ center: { x: hole.x, y: hole.y },
164
+ radius: hole.outer_diameter / 2,
165
+ fill: colorMap.copper.top,
166
+ transform
167
+ });
168
+ drawCircle({
169
+ ctx,
170
+ center: { x: hole.x, y: hole.y },
171
+ radius: hole.hole_diameter / 2,
172
+ fill: colorMap.drill,
173
+ transform
174
+ });
175
+ return;
176
+ }
177
+ if (hole.shape === "oval") {
178
+ drawOval({
179
+ ctx,
180
+ center: { x: hole.x, y: hole.y },
181
+ width: hole.outer_width,
182
+ height: hole.outer_height,
183
+ fill: colorMap.copper.top,
184
+ transform,
185
+ rotation: hole.ccw_rotation
186
+ });
187
+ drawOval({
188
+ ctx,
189
+ center: { x: hole.x, y: hole.y },
190
+ width: hole.hole_width,
191
+ height: hole.hole_height,
192
+ fill: colorMap.drill,
193
+ transform,
194
+ rotation: hole.ccw_rotation
195
+ });
196
+ return;
197
+ }
198
+ if (hole.shape === "pill") {
199
+ drawPill({
200
+ ctx,
201
+ center: { x: hole.x, y: hole.y },
202
+ width: hole.outer_width,
203
+ height: hole.outer_height,
204
+ fill: colorMap.copper.top,
205
+ transform,
206
+ rotation: hole.ccw_rotation
207
+ });
208
+ drawPill({
209
+ ctx,
210
+ center: { x: hole.x, y: hole.y },
211
+ width: hole.hole_width,
212
+ height: hole.hole_height,
213
+ fill: colorMap.drill,
214
+ transform,
215
+ rotation: hole.ccw_rotation
216
+ });
217
+ return;
218
+ }
219
+ if (hole.shape === "circular_hole_with_rect_pad") {
220
+ drawRect({
221
+ ctx,
222
+ center: { x: hole.x, y: hole.y },
223
+ width: hole.rect_pad_width,
224
+ height: hole.rect_pad_height,
225
+ fill: colorMap.copper.top,
226
+ transform,
227
+ borderRadius: hole.rect_border_radius ?? 0
228
+ });
229
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
230
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
231
+ drawCircle({
232
+ ctx,
233
+ center: { x: holeX, y: holeY },
234
+ radius: hole.hole_diameter / 2,
235
+ fill: colorMap.drill,
236
+ transform
237
+ });
238
+ return;
239
+ }
240
+ if (hole.shape === "pill_hole_with_rect_pad") {
241
+ drawRect({
242
+ ctx,
243
+ center: { x: hole.x, y: hole.y },
244
+ width: hole.rect_pad_width,
245
+ height: hole.rect_pad_height,
246
+ fill: colorMap.copper.top,
247
+ transform,
248
+ borderRadius: hole.rect_border_radius ?? 0
249
+ });
250
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
251
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
252
+ drawPill({
253
+ ctx,
254
+ center: { x: holeX, y: holeY },
255
+ width: hole.hole_width,
256
+ height: hole.hole_height,
257
+ fill: colorMap.drill,
258
+ transform
259
+ });
260
+ return;
261
+ }
262
+ if (hole.shape === "rotated_pill_hole_with_rect_pad") {
263
+ drawRect({
264
+ ctx,
265
+ center: { x: hole.x, y: hole.y },
266
+ width: hole.rect_pad_width,
267
+ height: hole.rect_pad_height,
268
+ fill: colorMap.copper.top,
269
+ transform,
270
+ borderRadius: hole.rect_border_radius ?? 0,
271
+ rotation: hole.rect_ccw_rotation
272
+ });
273
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
274
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
275
+ drawPill({
276
+ ctx,
277
+ center: { x: holeX, y: holeY },
278
+ width: hole.hole_width,
279
+ height: hole.hole_height,
280
+ fill: colorMap.drill,
281
+ transform,
282
+ rotation: hole.hole_ccw_rotation
283
+ });
284
+ return;
285
+ }
286
+ }
287
+
288
+ // lib/drawer/CircuitToCanvasDrawer.ts
289
+ var CircuitToCanvasDrawer = class {
290
+ ctx;
291
+ colorMap;
292
+ realToCanvasMat;
293
+ constructor(canvasOrContext) {
294
+ if ("getContext" in canvasOrContext && typeof canvasOrContext.getContext === "function") {
295
+ const ctx = canvasOrContext.getContext("2d");
296
+ if (!ctx) {
297
+ throw new Error("Failed to get 2D rendering context from canvas");
298
+ }
299
+ this.ctx = ctx;
300
+ } else {
301
+ this.ctx = canvasOrContext;
302
+ }
303
+ this.colorMap = { ...DEFAULT_PCB_COLOR_MAP };
304
+ this.realToCanvasMat = identity();
305
+ }
306
+ configure(config) {
307
+ if (config.colorOverrides) {
308
+ this.colorMap = {
309
+ ...this.colorMap,
310
+ ...config.colorOverrides,
311
+ copper: {
312
+ ...this.colorMap.copper,
313
+ ...config.colorOverrides.copper
314
+ },
315
+ silkscreen: {
316
+ ...this.colorMap.silkscreen,
317
+ ...config.colorOverrides.silkscreen
318
+ },
319
+ soldermask: {
320
+ ...this.colorMap.soldermask,
321
+ ...config.colorOverrides.soldermask
322
+ },
323
+ soldermaskWithCopperUnderneath: {
324
+ ...this.colorMap.soldermaskWithCopperUnderneath,
325
+ ...config.colorOverrides.soldermaskWithCopperUnderneath
326
+ },
327
+ soldermaskOverCopper: {
328
+ ...this.colorMap.soldermaskOverCopper,
329
+ ...config.colorOverrides.soldermaskOverCopper
330
+ }
331
+ };
332
+ }
333
+ }
334
+ setCameraBounds(bounds) {
335
+ const canvas = this.ctx.canvas;
336
+ const canvasWidth = canvas.width;
337
+ const canvasHeight = canvas.height;
338
+ const realWidth = bounds.maxX - bounds.minX;
339
+ const realHeight = bounds.maxY - bounds.minY;
340
+ const scaleX = canvasWidth / realWidth;
341
+ const scaleY = canvasHeight / realHeight;
342
+ const uniformScale = Math.min(scaleX, scaleY);
343
+ const offsetX = (canvasWidth - realWidth * uniformScale) / 2;
344
+ const offsetY = (canvasHeight - realHeight * uniformScale) / 2;
345
+ this.realToCanvasMat = compose(
346
+ translate(offsetX, offsetY),
347
+ scale(uniformScale, uniformScale),
348
+ translate(-bounds.minX, -bounds.minY)
349
+ );
350
+ }
351
+ drawElements(elements, options = {}) {
352
+ for (const element of elements) {
353
+ this.drawElement(element, options);
354
+ }
355
+ }
356
+ drawElement(element, options) {
357
+ if (element.type === "pcb_plated_hole") {
358
+ drawPcbPlatedHole({
359
+ ctx: this.ctx,
360
+ hole: element,
361
+ transform: this.realToCanvasMat,
362
+ colorMap: this.colorMap
363
+ });
364
+ }
365
+ }
366
+ };
367
+ export {
368
+ CircuitToCanvasDrawer,
369
+ DEFAULT_PCB_COLOR_MAP,
370
+ drawCircle,
371
+ drawOval,
372
+ drawPcbPlatedHole,
373
+ drawPill,
374
+ drawRect
375
+ };