circuit-to-canvas 0.0.51 → 0.0.52

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 (32) hide show
  1. package/dist/index.d.ts +5 -2
  2. package/dist/index.js +1750 -1743
  3. package/lib/drawer/CircuitToCanvasDrawer.ts +61 -60
  4. package/lib/drawer/elements/pcb-board.ts +26 -21
  5. package/lib/drawer/elements/pcb-plated-hole.ts +3 -3
  6. package/lib/drawer/elements/pcb-soldermask/hole.ts +4 -4
  7. package/lib/drawer/elements/pcb-soldermask/index.ts +9 -9
  8. package/lib/drawer/elements/pcb-soldermask/plated-hole.ts +4 -4
  9. package/lib/drawer/elements/pcb-soldermask/smt-pad.ts +4 -4
  10. package/package.json +1 -1
  11. package/tests/board-snapshot/usb-c-flashlight-board.test.ts +1 -1
  12. package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
  13. package/tests/elements/__snapshots__/brep-copper-pours.snap.png +0 -0
  14. package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
  15. package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
  16. package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
  17. package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
  18. package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
  19. package/tests/elements/__snapshots__/pcb-no-soldermask.snap.png +0 -0
  20. package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
  21. package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
  22. package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
  23. package/tests/elements/pcb-board.test.ts +2 -2
  24. package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +1 -1
  25. package/tests/elements/pcb-hole-soldermask-margin.test.ts +1 -1
  26. package/tests/elements/pcb-keepout-with-group-id.test.ts +1 -1
  27. package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +1 -1
  28. package/tests/elements/pcb-plated-hole.test.ts +3 -3
  29. package/tests/elements/pcb-smtpad-asymmetric-soldermask-margin.test.ts +1 -1
  30. package/tests/elements/pcb-smtpad-soldermask-coverage.test.ts +1 -1
  31. package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +1 -1
  32. package/tests/fixtures/getStackedPngSvgComparison.ts +5 -5
package/dist/index.js CHANGED
@@ -1,70 +1,51 @@
1
1
  // lib/drawer/CircuitToCanvasDrawer.ts
2
2
  import {
3
- identity,
4
3
  compose,
5
- translate,
6
- scale
4
+ identity,
5
+ scale,
6
+ translate
7
7
  } from "transformation-matrix";
8
8
 
9
- // lib/drawer/pcb-render-layer-filter.ts
10
- import { getElementRenderLayers } from "@tscircuit/circuit-json-util";
11
- function shouldDrawElement(element, options) {
12
- if (!options.layers || options.layers.length === 0) {
13
- return true;
14
- }
15
- const elementLayers = getElementRenderLayers(element);
16
- if (elementLayers.length === 0) {
17
- return true;
18
- }
19
- return elementLayers.some((layer) => options.layers.includes(layer));
20
- }
21
-
22
- // lib/drawer/types.ts
23
- var DEFAULT_PCB_COLOR_MAP = {
24
- copper: {
25
- top: "rgb(200, 52, 52)",
26
- inner1: "rgb(255, 140, 0)",
27
- inner2: "rgb(255, 215, 0)",
28
- inner3: "rgb(50, 205, 50)",
29
- inner4: "rgb(64, 224, 208)",
30
- inner5: "rgb(138, 43, 226)",
31
- inner6: "rgb(255, 105, 180)",
32
- bottom: "rgb(77, 127, 196)"
33
- },
34
- soldermaskWithCopperUnderneath: {
35
- top: "rgb(18, 82, 50)",
36
- bottom: "rgb(77, 127, 196)"
37
- },
38
- soldermask: {
39
- top: "rgb(12, 55, 33)",
40
- bottom: "rgb(12, 55, 33)"
41
- },
42
- soldermaskOverCopper: {
43
- top: "rgb(52, 135, 73)",
44
- bottom: "rgb(52, 135, 73)"
45
- },
46
- substrate: "rgb(201, 162, 110)",
47
- drill: "#FF26E2",
48
- silkscreen: {
49
- top: "#f2eda1",
50
- bottom: "#5da9e9"
51
- },
52
- boardOutline: "rgba(255, 255, 255, 0.5)",
53
- courtyard: "#FF00FF",
54
- keepout: "#FF6B6B"
55
- // Red color for keepout zones
56
- };
57
-
58
- // lib/drawer/shapes/circle.ts
9
+ // lib/drawer/shapes/path.ts
59
10
  import { applyToPoint } from "transformation-matrix";
60
- function drawCircle(params) {
61
- const { ctx, center, radius, fill, realToCanvasMat } = params;
62
- const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y]);
63
- const scaledRadius = radius * Math.abs(realToCanvasMat.a);
11
+ function drawPath(params) {
12
+ const {
13
+ ctx,
14
+ points,
15
+ fill,
16
+ stroke,
17
+ strokeWidth = 1,
18
+ realToCanvasMat,
19
+ closePath = false
20
+ } = params;
21
+ if (points.length < 2) return;
64
22
  ctx.beginPath();
65
- ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
66
- ctx.fillStyle = fill;
67
- ctx.fill();
23
+ const canvasPoints = points.map(
24
+ (p) => applyToPoint(realToCanvasMat, [p.x, p.y])
25
+ );
26
+ const firstPoint = canvasPoints[0];
27
+ if (!firstPoint) return;
28
+ const [firstX, firstY] = firstPoint;
29
+ ctx.moveTo(firstX, firstY);
30
+ for (let i = 1; i < canvasPoints.length; i++) {
31
+ const point = canvasPoints[i];
32
+ if (!point) continue;
33
+ const [x, y] = point;
34
+ ctx.lineTo(x, y);
35
+ }
36
+ if (closePath) {
37
+ ctx.closePath();
38
+ }
39
+ if (fill) {
40
+ ctx.fillStyle = fill;
41
+ ctx.fill();
42
+ }
43
+ if (stroke) {
44
+ const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
45
+ ctx.strokeStyle = stroke;
46
+ ctx.lineWidth = scaledStrokeWidth;
47
+ ctx.stroke();
48
+ }
68
49
  }
69
50
 
70
51
  // lib/drawer/shapes/rect.ts
@@ -133,107 +114,71 @@ function drawRect(params) {
133
114
  ctx.restore();
134
115
  }
135
116
 
136
- // lib/drawer/shapes/oval.ts
137
- import { applyToPoint as applyToPoint3 } from "transformation-matrix";
138
- function drawOval(params) {
139
- const {
140
- ctx,
141
- center,
142
- radius_x,
143
- radius_y,
144
- fill,
145
- stroke,
146
- strokeWidth = 0.1,
147
- realToCanvasMat,
148
- rotation = 0
149
- } = params;
150
- const [cx, cy] = applyToPoint3(realToCanvasMat, [center.x, center.y]);
151
- const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
152
- const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
153
- const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
154
- ctx.save();
155
- ctx.translate(cx, cy);
156
- if (rotation !== 0) {
157
- ctx.rotate(-rotation * (Math.PI / 180));
158
- }
159
- ctx.beginPath();
160
- ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
161
- if (fill) {
162
- ctx.fillStyle = fill;
163
- ctx.fill();
117
+ // lib/drawer/elements/pcb-board.ts
118
+ function drawPcbBoard(params) {
119
+ const { ctx, board, realToCanvasMat, colorMap, drawBoardMaterial } = params;
120
+ const { width, height, center, outline } = board;
121
+ if (outline && Array.isArray(outline) && outline.length >= 3) {
122
+ if (drawBoardMaterial) {
123
+ drawPath({
124
+ ctx,
125
+ points: outline.map((p) => ({ x: p.x, y: p.y })),
126
+ fill: colorMap.substrate,
127
+ realToCanvasMat,
128
+ closePath: true
129
+ });
130
+ }
131
+ drawPath({
132
+ ctx,
133
+ points: outline.map((p) => ({ x: p.x, y: p.y })),
134
+ stroke: colorMap.boardOutline,
135
+ strokeWidth: 0.1,
136
+ realToCanvasMat,
137
+ closePath: true
138
+ });
139
+ return;
164
140
  }
165
- if (stroke) {
166
- ctx.strokeStyle = stroke;
167
- ctx.lineWidth = scaledStrokeWidth;
168
- ctx.stroke();
141
+ if (width !== void 0 && height !== void 0 && center) {
142
+ if (drawBoardMaterial) {
143
+ drawRect({
144
+ ctx,
145
+ center,
146
+ width,
147
+ height,
148
+ fill: colorMap.substrate,
149
+ realToCanvasMat
150
+ });
151
+ }
152
+ const halfWidth = width / 2;
153
+ const halfHeight = height / 2;
154
+ const corners = [
155
+ { x: center.x - halfWidth, y: center.y - halfHeight },
156
+ { x: center.x + halfWidth, y: center.y - halfHeight },
157
+ { x: center.x + halfWidth, y: center.y + halfHeight },
158
+ { x: center.x - halfWidth, y: center.y + halfHeight }
159
+ ];
160
+ drawPath({
161
+ ctx,
162
+ points: corners,
163
+ stroke: colorMap.boardOutline,
164
+ strokeWidth: 0.1,
165
+ realToCanvasMat,
166
+ closePath: true
167
+ });
169
168
  }
170
- ctx.restore();
171
169
  }
172
170
 
173
- // lib/drawer/shapes/pill.ts
171
+ // lib/drawer/elements/pcb-copper-pour.ts
174
172
  import { applyToPoint as applyToPoint4 } from "transformation-matrix";
175
- function drawPill(params) {
176
- const {
177
- ctx,
178
- center,
179
- width,
180
- height,
181
- fill,
182
- realToCanvasMat,
183
- rotation = 0,
184
- stroke,
185
- strokeWidth
186
- } = params;
187
- const [cx, cy] = applyToPoint4(realToCanvasMat, [center.x, center.y]);
188
- const scaledWidth = width * Math.abs(realToCanvasMat.a);
189
- const scaledHeight = height * Math.abs(realToCanvasMat.a);
190
- const scaledStrokeWidth = strokeWidth ? strokeWidth * Math.abs(realToCanvasMat.a) : void 0;
191
- ctx.save();
192
- ctx.translate(cx, cy);
193
- if (rotation !== 0) {
194
- ctx.rotate(-rotation * (Math.PI / 180));
195
- }
196
- ctx.beginPath();
197
- if (scaledWidth > scaledHeight) {
198
- const radius = scaledHeight / 2;
199
- const straightLength = scaledWidth - scaledHeight;
200
- ctx.moveTo(-straightLength / 2, -radius);
201
- ctx.lineTo(straightLength / 2, -radius);
202
- ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
203
- ctx.lineTo(-straightLength / 2, radius);
204
- ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
205
- } else if (scaledHeight > scaledWidth) {
206
- const radius = scaledWidth / 2;
207
- const straightLength = scaledHeight - scaledWidth;
208
- ctx.moveTo(radius, -straightLength / 2);
209
- ctx.lineTo(radius, straightLength / 2);
210
- ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
211
- ctx.lineTo(-radius, -straightLength / 2);
212
- ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
213
- } else {
214
- ctx.arc(0, 0, scaledWidth / 2, 0, Math.PI * 2);
215
- }
216
- ctx.closePath();
217
- if (fill) {
218
- ctx.fillStyle = fill;
219
- ctx.fill();
220
- }
221
- if (stroke && scaledStrokeWidth) {
222
- ctx.strokeStyle = stroke;
223
- ctx.lineWidth = scaledStrokeWidth;
224
- ctx.stroke();
225
- }
226
- ctx.restore();
227
- }
228
173
 
229
174
  // lib/drawer/shapes/polygon.ts
230
- import { applyToPoint as applyToPoint5 } from "transformation-matrix";
175
+ import { applyToPoint as applyToPoint3 } from "transformation-matrix";
231
176
  function drawPolygon(params) {
232
177
  const { ctx, points, fill, realToCanvasMat } = params;
233
178
  if (points.length < 3) return;
234
179
  ctx.beginPath();
235
180
  const canvasPoints = points.map(
236
- (p) => applyToPoint5(realToCanvasMat, [p.x, p.y])
181
+ (p) => applyToPoint3(realToCanvasMat, [p.x, p.y])
237
182
  );
238
183
  const firstPoint = canvasPoints[0];
239
184
  if (!firstPoint) return;
@@ -250,452 +195,427 @@ function drawPolygon(params) {
250
195
  ctx.fill();
251
196
  }
252
197
 
253
- // lib/drawer/elements/soldermask-margin.ts
254
- import { applyToPoint as applyToPoint6 } from "transformation-matrix";
255
- function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor, asymmetricMargins) {
256
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
257
- const scaledWidth = width * Math.abs(realToCanvasMat.a);
258
- const scaledHeight = height * Math.abs(realToCanvasMat.a);
259
- const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a);
260
- const ml = asymmetricMargins?.left ?? margin;
261
- const mr = asymmetricMargins?.right ?? margin;
262
- const mt = asymmetricMargins?.top ?? margin;
263
- const mb = asymmetricMargins?.bottom ?? margin;
264
- const scaledThicknessL = Math.max(0, -ml) * Math.abs(realToCanvasMat.a);
265
- const scaledThicknessR = Math.max(0, -mr) * Math.abs(realToCanvasMat.a);
266
- const scaledThicknessT = Math.max(0, -mt) * Math.abs(realToCanvasMat.a);
267
- const scaledThicknessB = Math.max(0, -mb) * Math.abs(realToCanvasMat.a);
268
- ctx.save();
269
- ctx.translate(cx, cy);
270
- if (rotation !== 0) {
271
- ctx.rotate(-rotation * (Math.PI / 180));
198
+ // lib/drawer/elements/pcb-copper-pour.ts
199
+ function layerToColor(layer, colorMap) {
200
+ return colorMap.copper[layer] ?? colorMap.copper.top;
201
+ }
202
+ function computeArcFromBulge(startX, startY, endX, endY, bulge) {
203
+ if (Math.abs(bulge) < 1e-10) {
204
+ return null;
272
205
  }
273
- const prevCompositeOp = ctx.globalCompositeOperation;
274
- if (ctx.globalCompositeOperation !== void 0) {
275
- ctx.globalCompositeOperation = "source-atop";
206
+ const chordX = endX - startX;
207
+ const chordY = endY - startY;
208
+ const chordLength = Math.hypot(chordX, chordY);
209
+ if (chordLength < 1e-10) {
210
+ return null;
276
211
  }
277
- const outerWidth = scaledWidth;
278
- const outerHeight = scaledHeight;
279
- const outerRadius = scaledRadius;
280
- ctx.beginPath();
281
- if (outerRadius > 0) {
282
- const x = -outerWidth / 2;
283
- const y = -outerHeight / 2;
284
- const r = Math.min(outerRadius, outerWidth / 2, outerHeight / 2);
285
- ctx.moveTo(x + r, y);
286
- ctx.lineTo(x + outerWidth - r, y);
287
- ctx.arcTo(x + outerWidth, y, x + outerWidth, y + r, r);
288
- ctx.lineTo(x + outerWidth, y + outerHeight - r);
289
- ctx.arcTo(
290
- x + outerWidth,
291
- y + outerHeight,
292
- x + outerWidth - r,
293
- y + outerHeight,
294
- r
295
- );
296
- ctx.lineTo(x + r, y + outerHeight);
297
- ctx.arcTo(x, y + outerHeight, x, y + outerHeight - r, r);
298
- ctx.lineTo(x, y + r);
299
- ctx.arcTo(x, y, x + r, y, r);
300
- } else {
301
- ctx.rect(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight);
212
+ const sagitta = Math.abs(bulge) * (chordLength / 2);
213
+ const halfChord = chordLength / 2;
214
+ const radius = (sagitta * sagitta + halfChord * halfChord) / (2 * sagitta);
215
+ const distToCenter = radius - sagitta;
216
+ const midX = (startX + endX) / 2;
217
+ const midY = (startY + endY) / 2;
218
+ const perpX = -chordY / chordLength;
219
+ const perpY = chordX / chordLength;
220
+ const sign = bulge > 0 ? -1 : 1;
221
+ const centerX = midX + sign * perpX * distToCenter;
222
+ const centerY = midY + sign * perpY * distToCenter;
223
+ return { centerX, centerY, radius };
224
+ }
225
+ function drawArcFromBulge(ctx, realStartX, realStartY, realEndX, realEndY, bulge, realToCanvasMat) {
226
+ if (Math.abs(bulge) < 1e-10) {
227
+ const [endX, endY] = applyToPoint4(realToCanvasMat, [realEndX, realEndY]);
228
+ ctx.lineTo(endX, endY);
229
+ return;
302
230
  }
303
- ctx.fillStyle = soldermaskColor;
304
- ctx.fill();
305
- if (ctx.globalCompositeOperation !== void 0) {
306
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
231
+ const arc = computeArcFromBulge(
232
+ realStartX,
233
+ realStartY,
234
+ realEndX,
235
+ realEndY,
236
+ bulge
237
+ );
238
+ if (!arc) {
239
+ const [endX, endY] = applyToPoint4(realToCanvasMat, [realEndX, realEndY]);
240
+ ctx.lineTo(endX, endY);
241
+ return;
307
242
  }
308
- const innerWidth = Math.max(
309
- 0,
310
- scaledWidth - (scaledThicknessL + scaledThicknessR)
243
+ const [canvasStartX, canvasStartY] = applyToPoint4(realToCanvasMat, [
244
+ realStartX,
245
+ realStartY
246
+ ]);
247
+ const [canvasEndX, canvasEndY] = applyToPoint4(realToCanvasMat, [
248
+ realEndX,
249
+ realEndY
250
+ ]);
251
+ const [canvasCenterX, canvasCenterY] = applyToPoint4(realToCanvasMat, [
252
+ arc.centerX,
253
+ arc.centerY
254
+ ]);
255
+ const canvasRadius = Math.hypot(
256
+ canvasStartX - canvasCenterX,
257
+ canvasStartY - canvasCenterY
311
258
  );
312
- const innerHeight = Math.max(
313
- 0,
314
- scaledHeight - (scaledThicknessT + scaledThicknessB)
259
+ const startAngle = Math.atan2(
260
+ canvasStartY - canvasCenterY,
261
+ canvasStartX - canvasCenterX
315
262
  );
316
- const innerRadius = Math.max(
317
- 0,
318
- scaledRadius - (scaledThicknessL + scaledThicknessR + scaledThicknessT + scaledThicknessB) / 4
263
+ const endAngle = Math.atan2(
264
+ canvasEndY - canvasCenterY,
265
+ canvasEndX - canvasCenterX
319
266
  );
320
- if (innerWidth > 0 && innerHeight > 0) {
321
- ctx.beginPath();
322
- const x = -scaledWidth / 2 + scaledThicknessL;
323
- const y = -scaledHeight / 2 + scaledThicknessT;
324
- if (innerRadius > 0) {
325
- const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2);
326
- ctx.moveTo(x + r, y);
327
- ctx.lineTo(x + innerWidth - r, y);
328
- ctx.arcTo(x + innerWidth, y, x + innerWidth, y + r, r);
329
- ctx.lineTo(x + innerWidth, y + innerHeight - r);
330
- ctx.arcTo(
331
- x + innerWidth,
332
- y + innerHeight,
333
- x + innerWidth - r,
334
- y + innerHeight,
335
- r
336
- );
337
- ctx.lineTo(x + r, y + innerHeight);
338
- ctx.arcTo(x, y + innerHeight, x, y + innerHeight - r, r);
339
- ctx.lineTo(x, y + r);
340
- ctx.arcTo(x, y, x + r, y, r);
267
+ const det = realToCanvasMat.a * realToCanvasMat.d - realToCanvasMat.b * realToCanvasMat.c;
268
+ const isFlipped = det < 0;
269
+ const counterclockwise = bulge > 0 ? !isFlipped : isFlipped;
270
+ ctx.arc(
271
+ canvasCenterX,
272
+ canvasCenterY,
273
+ canvasRadius,
274
+ startAngle,
275
+ endAngle,
276
+ counterclockwise
277
+ );
278
+ }
279
+ function drawRing(ctx, ring, realToCanvasMat) {
280
+ if (ring.vertices.length < 2) return;
281
+ if (ring.vertices.length === 2) {
282
+ const v0 = ring.vertices[0];
283
+ const v1 = ring.vertices[1];
284
+ if (v0 && v1 && Math.abs((v0.bulge ?? 0) - 1) < 1e-10 && Math.abs((v1.bulge ?? 0) - 1) < 1e-10) {
285
+ const [x0, y0] = applyToPoint4(realToCanvasMat, [v0.x, v0.y]);
286
+ ctx.moveTo(x0, y0);
287
+ drawArcFromBulge(ctx, v0.x, v0.y, v1.x, v1.y, 1, realToCanvasMat);
288
+ drawArcFromBulge(ctx, v1.x, v1.y, v0.x, v0.y, 1, realToCanvasMat);
289
+ return;
290
+ }
291
+ }
292
+ const firstVertex = ring.vertices[0];
293
+ if (!firstVertex) return;
294
+ const [firstX, firstY] = applyToPoint4(realToCanvasMat, [
295
+ firstVertex.x,
296
+ firstVertex.y
297
+ ]);
298
+ ctx.moveTo(firstX, firstY);
299
+ for (let i = 0; i < ring.vertices.length; i++) {
300
+ const currentVertex = ring.vertices[i];
301
+ const nextIndex = (i + 1) % ring.vertices.length;
302
+ const nextVertex = ring.vertices[nextIndex];
303
+ if (!currentVertex || !nextVertex) continue;
304
+ const bulge = currentVertex.bulge ?? 0;
305
+ if (Math.abs(bulge) < 1e-10) {
306
+ const [nextX, nextY] = applyToPoint4(realToCanvasMat, [
307
+ nextVertex.x,
308
+ nextVertex.y
309
+ ]);
310
+ ctx.lineTo(nextX, nextY);
341
311
  } else {
342
- ctx.rect(x, y, innerWidth, innerHeight);
312
+ drawArcFromBulge(
313
+ ctx,
314
+ currentVertex.x,
315
+ currentVertex.y,
316
+ nextVertex.x,
317
+ nextVertex.y,
318
+ bulge,
319
+ realToCanvasMat
320
+ );
343
321
  }
344
- ctx.fillStyle = padColor;
345
- ctx.fill();
346
322
  }
347
- ctx.restore();
348
323
  }
349
- function drawSoldermaskRingForCircle(ctx, center, radius, margin, realToCanvasMat, soldermaskColor, padColor) {
350
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
351
- const scaledRadius = radius * Math.abs(realToCanvasMat.a);
352
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
324
+ function drawPcbCopperPour(params) {
325
+ const { ctx, pour, realToCanvasMat, colorMap } = params;
326
+ const color = layerToColor(pour.layer, colorMap);
353
327
  ctx.save();
354
- const prevCompositeOp = ctx.globalCompositeOperation;
355
- if (ctx.globalCompositeOperation !== void 0) {
356
- ctx.globalCompositeOperation = "source-atop";
328
+ if (pour.shape === "rect") {
329
+ const [cx, cy] = applyToPoint4(realToCanvasMat, [
330
+ pour.center.x,
331
+ pour.center.y
332
+ ]);
333
+ const scaledWidth = pour.width * Math.abs(realToCanvasMat.a);
334
+ const scaledHeight = pour.height * Math.abs(realToCanvasMat.a);
335
+ ctx.translate(cx, cy);
336
+ if (pour.rotation) {
337
+ ctx.rotate(-pour.rotation * (Math.PI / 180));
338
+ }
339
+ ctx.beginPath();
340
+ ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
341
+ ctx.fillStyle = color;
342
+ ctx.globalAlpha = 0.5;
343
+ ctx.fill();
344
+ ctx.restore();
345
+ return;
357
346
  }
358
- ctx.beginPath();
359
- ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
360
- ctx.fillStyle = soldermaskColor;
361
- ctx.fill();
362
- if (ctx.globalCompositeOperation !== void 0) {
363
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
347
+ if (pour.shape === "polygon") {
348
+ if (pour.points && pour.points.length >= 3) {
349
+ const canvasPoints = pour.points.map(
350
+ (p) => applyToPoint4(realToCanvasMat, [p.x, p.y])
351
+ );
352
+ const firstPoint = canvasPoints[0];
353
+ if (!firstPoint) {
354
+ ctx.restore();
355
+ return;
356
+ }
357
+ ctx.beginPath();
358
+ const [firstX, firstY] = firstPoint;
359
+ ctx.moveTo(firstX, firstY);
360
+ for (let i = 1; i < canvasPoints.length; i++) {
361
+ const point = canvasPoints[i];
362
+ if (!point) continue;
363
+ const [x, y] = point;
364
+ ctx.lineTo(x, y);
365
+ }
366
+ ctx.closePath();
367
+ ctx.fillStyle = color;
368
+ ctx.globalAlpha = 0.5;
369
+ ctx.fill();
370
+ }
371
+ ctx.restore();
372
+ return;
364
373
  }
365
- const innerRadius = Math.max(0, scaledRadius - scaledMargin);
366
- if (innerRadius > 0) {
374
+ if (pour.shape === "brep") {
367
375
  ctx.beginPath();
368
- ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
369
- ctx.fillStyle = padColor;
370
- ctx.fill();
376
+ drawRing(ctx, pour.brep_shape.outer_ring, realToCanvasMat);
377
+ if (pour.brep_shape.inner_rings) {
378
+ for (const innerRing of pour.brep_shape.inner_rings) {
379
+ drawRing(ctx, innerRing, realToCanvasMat);
380
+ }
381
+ }
382
+ ctx.fillStyle = color;
383
+ ctx.globalAlpha = 0.5;
384
+ ctx.fill("evenodd");
385
+ ctx.restore();
386
+ return;
371
387
  }
372
388
  ctx.restore();
373
389
  }
374
- function drawSoldermaskRingForPill(ctx, center, width, height, margin, rotation, realToCanvasMat, soldermaskColor, padColor) {
375
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
376
- const scaledWidth = width * Math.abs(realToCanvasMat.a);
377
- const scaledHeight = height * Math.abs(realToCanvasMat.a);
378
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
379
- ctx.save();
380
- ctx.translate(cx, cy);
381
- if (rotation !== 0) {
382
- ctx.rotate(-rotation * (Math.PI / 180));
383
- }
384
- const prevCompositeOp = ctx.globalCompositeOperation;
385
- if (ctx.globalCompositeOperation !== void 0) {
386
- ctx.globalCompositeOperation = "source-atop";
387
- }
388
- const outerWidth = scaledWidth;
389
- const outerHeight = scaledHeight;
390
- ctx.beginPath();
391
- if (outerWidth > outerHeight) {
392
- const radius = outerHeight / 2;
393
- const straightLength = outerWidth - outerHeight;
394
- ctx.moveTo(-straightLength / 2, -radius);
395
- ctx.lineTo(straightLength / 2, -radius);
396
- ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
397
- ctx.lineTo(-straightLength / 2, radius);
398
- ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
399
- } else if (outerHeight > outerWidth) {
400
- const radius = outerWidth / 2;
401
- const straightLength = outerHeight - outerWidth;
402
- ctx.moveTo(radius, -straightLength / 2);
403
- ctx.lineTo(radius, straightLength / 2);
404
- ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
405
- ctx.lineTo(-radius, -straightLength / 2);
406
- ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
407
- } else {
408
- ctx.arc(0, 0, outerWidth / 2, 0, Math.PI * 2);
390
+
391
+ // lib/drawer/elements/pcb-copper-text.ts
392
+ import { applyToPoint as applyToPoint6 } from "transformation-matrix";
393
+
394
+ // lib/drawer/shapes/text/text.ts
395
+ import { lineAlphabet } from "@tscircuit/alphabet";
396
+ import { applyToPoint as applyToPoint5 } from "transformation-matrix";
397
+
398
+ // lib/drawer/shapes/text/getAlphabetLayout.ts
399
+ var GLYPH_WIDTH_RATIO = 0.62;
400
+ var LETTER_SPACING_RATIO = 0.3;
401
+ var SPACE_WIDTH_RATIO = 1;
402
+ var STROKE_WIDTH_RATIO = 0.13;
403
+ function getAlphabetLayout(text, fontSize) {
404
+ const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
405
+ const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
406
+ const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
407
+ const characters = Array.from(text);
408
+ let width = 0;
409
+ characters.forEach((char, index) => {
410
+ const advance = char === " " ? spaceWidth : glyphWidth;
411
+ width += advance;
412
+ if (index < characters.length - 1) width += letterSpacing;
413
+ });
414
+ const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
415
+ return {
416
+ width,
417
+ height: fontSize,
418
+ glyphWidth,
419
+ letterSpacing,
420
+ spaceWidth,
421
+ strokeWidth
422
+ };
423
+ }
424
+
425
+ // lib/drawer/shapes/text/getTextStartPosition.ts
426
+ function getTextStartPosition(alignment, layout) {
427
+ const totalWidth = layout.width + layout.strokeWidth;
428
+ const totalHeight = layout.height + layout.strokeWidth;
429
+ let x = 0;
430
+ let y = 0;
431
+ if (alignment === "center") {
432
+ x = -totalWidth / 2;
433
+ } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
434
+ x = 0;
435
+ } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
436
+ x = -totalWidth;
409
437
  }
410
- ctx.fillStyle = soldermaskColor;
411
- ctx.fill();
412
- if (ctx.globalCompositeOperation !== void 0) {
413
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
438
+ if (alignment === "center") {
439
+ y = -totalHeight / 2;
440
+ } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
441
+ y = 0;
442
+ } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
443
+ y = -totalHeight;
444
+ } else {
445
+ y = 0;
414
446
  }
415
- const innerWidth = scaledWidth - scaledMargin * 2;
416
- const innerHeight = scaledHeight - scaledMargin * 2;
417
- if (innerWidth > 0 && innerHeight > 0) {
418
- ctx.beginPath();
419
- if (innerWidth > innerHeight) {
420
- const radius = innerHeight / 2;
421
- const straightLength = innerWidth - innerHeight;
422
- ctx.moveTo(-straightLength / 2, -radius);
423
- ctx.lineTo(straightLength / 2, -radius);
424
- ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
425
- ctx.lineTo(-straightLength / 2, radius);
426
- ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
427
- } else if (innerHeight > innerWidth) {
428
- const radius = innerWidth / 2;
429
- const straightLength = innerHeight - innerWidth;
430
- ctx.moveTo(radius, -straightLength / 2);
431
- ctx.lineTo(radius, straightLength / 2);
432
- ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
433
- ctx.lineTo(-radius, -straightLength / 2);
434
- ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
435
- } else {
436
- ctx.arc(0, 0, innerWidth / 2, 0, Math.PI * 2);
447
+ return { x, y };
448
+ }
449
+
450
+ // lib/drawer/shapes/text/text.ts
451
+ var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
452
+ function strokeAlphabetText(params) {
453
+ const { ctx, text, fontSize, startX, startY } = params;
454
+ const layout = getAlphabetLayout(text, fontSize);
455
+ const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
456
+ const topY = startY;
457
+ const characters = Array.from(text);
458
+ let cursor = startX + strokeWidth / 2;
459
+ characters.forEach((char, index) => {
460
+ const lines = getGlyphLines(char);
461
+ const advance = char === " " ? spaceWidth : glyphWidth;
462
+ if (lines?.length) {
463
+ ctx.beginPath();
464
+ for (const line of lines) {
465
+ const x1 = cursor + line.x1 * glyphWidth;
466
+ const y1 = topY + (1 - line.y1) * height;
467
+ const x2 = cursor + line.x2 * glyphWidth;
468
+ const y2 = topY + (1 - line.y2) * height;
469
+ ctx.moveTo(x1, y1);
470
+ ctx.lineTo(x2, y2);
471
+ }
472
+ ctx.stroke();
437
473
  }
438
- ctx.fillStyle = padColor;
439
- ctx.fill();
440
- }
441
- ctx.restore();
474
+ cursor += advance;
475
+ if (index < characters.length - 1) {
476
+ cursor += letterSpacing;
477
+ }
478
+ });
442
479
  }
443
- function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rotation, realToCanvasMat, soldermaskColor, holeColor) {
444
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
445
- const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
446
- const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
447
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
480
+ function drawText(params) {
481
+ const {
482
+ ctx,
483
+ text,
484
+ x,
485
+ y,
486
+ fontSize,
487
+ color,
488
+ realToCanvasMat,
489
+ anchorAlignment,
490
+ rotation = 0
491
+ } = params;
492
+ if (!text) return;
493
+ const [canvasX, canvasY] = applyToPoint5(realToCanvasMat, [x, y]);
494
+ const scale2 = Math.abs(realToCanvasMat.a);
495
+ const scaledFontSize = fontSize * scale2;
496
+ const layout = getAlphabetLayout(text, scaledFontSize);
497
+ const startPos = getTextStartPosition(anchorAlignment, layout);
448
498
  ctx.save();
449
- ctx.translate(cx, cy);
499
+ ctx.translate(canvasX, canvasY);
450
500
  if (rotation !== 0) {
451
501
  ctx.rotate(-rotation * (Math.PI / 180));
452
502
  }
453
- const prevCompositeOp = ctx.globalCompositeOperation;
454
- if (ctx.globalCompositeOperation !== void 0) {
455
- ctx.globalCompositeOperation = "source-atop";
503
+ ctx.lineWidth = layout.strokeWidth;
504
+ ctx.lineCap = "round";
505
+ ctx.lineJoin = "round";
506
+ ctx.strokeStyle = color;
507
+ strokeAlphabetText({
508
+ ctx,
509
+ text,
510
+ fontSize: scaledFontSize,
511
+ startX: startPos.x,
512
+ startY: startPos.y
513
+ });
514
+ ctx.restore();
515
+ }
516
+
517
+ // lib/drawer/elements/pcb-copper-text.ts
518
+ var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
519
+ function mapAnchorAlignment(alignment) {
520
+ if (!alignment) return "center";
521
+ if (alignment.includes("left")) return "center_left";
522
+ if (alignment.includes("right")) return "center_right";
523
+ return "center";
524
+ }
525
+ function drawPcbCopperText(params) {
526
+ const { ctx, text, realToCanvasMat, colorMap } = params;
527
+ const content = text.text ?? "";
528
+ if (!content) return;
529
+ const [x, y] = applyToPoint6(realToCanvasMat, [
530
+ text.anchor_position.x,
531
+ text.anchor_position.y
532
+ ]);
533
+ const scale2 = Math.abs(realToCanvasMat.a);
534
+ const fontSize = (text.font_size ?? 1) * scale2;
535
+ const rotation = text.ccw_rotation ?? 0;
536
+ const padding = {
537
+ ...DEFAULT_PADDING,
538
+ ...text.knockout_padding
539
+ };
540
+ const textColor = colorMap.copper[text.layer] ?? colorMap.copper.top;
541
+ const layout = getAlphabetLayout(content, fontSize);
542
+ const totalWidth = layout.width + layout.strokeWidth;
543
+ const alignment = mapAnchorAlignment(text.anchor_alignment);
544
+ const startPos = getTextStartPosition(alignment, layout);
545
+ ctx.save();
546
+ ctx.translate(x, y);
547
+ if (text.is_mirrored) ctx.scale(-1, 1);
548
+ if (rotation !== 0) ctx.rotate(-rotation * (Math.PI / 180));
549
+ ctx.lineWidth = layout.strokeWidth;
550
+ ctx.lineCap = "round";
551
+ ctx.lineJoin = "round";
552
+ if (text.is_knockout) {
553
+ const paddingLeft = padding.left * scale2;
554
+ const paddingRight = padding.right * scale2;
555
+ const paddingTop = padding.top * scale2;
556
+ const paddingBottom = padding.bottom * scale2;
557
+ const rectX = startPos.x - paddingLeft * 4;
558
+ const rectY = startPos.y - paddingTop * 4;
559
+ const rectWidth = totalWidth + paddingLeft * 2 + paddingRight * 2;
560
+ const rectHeight = layout.height + layout.strokeWidth + paddingTop * 2 + paddingBottom * 2;
561
+ ctx.fillStyle = textColor;
562
+ ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
563
+ } else {
564
+ ctx.strokeStyle = textColor;
456
565
  }
566
+ strokeAlphabetText({
567
+ ctx,
568
+ text: content,
569
+ fontSize,
570
+ startX: startPos.x,
571
+ startY: startPos.y
572
+ });
573
+ ctx.restore();
574
+ }
575
+
576
+ // lib/drawer/shapes/circle.ts
577
+ import { applyToPoint as applyToPoint7 } from "transformation-matrix";
578
+ function drawCircle(params) {
579
+ const { ctx, center, radius, fill, realToCanvasMat } = params;
580
+ const [cx, cy] = applyToPoint7(realToCanvasMat, [center.x, center.y]);
581
+ const scaledRadius = radius * Math.abs(realToCanvasMat.a);
457
582
  ctx.beginPath();
458
- ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
459
- ctx.fillStyle = soldermaskColor;
583
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
584
+ ctx.fillStyle = fill;
460
585
  ctx.fill();
461
- if (ctx.globalCompositeOperation !== void 0) {
462
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
463
- }
464
- const innerRadiusX = Math.max(0, scaledRadiusX - scaledMargin);
465
- const innerRadiusY = Math.max(0, scaledRadiusY - scaledMargin);
466
- if (innerRadiusX > 0 && innerRadiusY > 0) {
467
- ctx.beginPath();
468
- ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2);
469
- ctx.fillStyle = holeColor;
470
- ctx.fill();
471
- }
472
- ctx.restore();
473
- }
474
- function offsetPolygonPoints(points, offset) {
475
- if (points.length < 3 || offset === 0) return points;
476
- let centerX = 0;
477
- let centerY = 0;
478
- for (const point of points) {
479
- centerX += point.x;
480
- centerY += point.y;
481
- }
482
- centerX /= points.length;
483
- centerY /= points.length;
484
- const result = [];
485
- for (const point of points) {
486
- const vectorX = point.x - centerX;
487
- const vectorY = point.y - centerY;
488
- const distance = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
489
- if (distance > 0) {
490
- const normalizedX = vectorX / distance;
491
- const normalizedY = vectorY / distance;
492
- result.push({
493
- x: point.x + normalizedX * offset,
494
- y: point.y + normalizedY * offset
495
- });
496
- } else {
497
- result.push({
498
- x: point.x + offset,
499
- y: point.y
500
- });
501
- }
502
- }
503
- return result;
504
586
  }
505
587
 
506
- // lib/drawer/elements/pcb-plated-hole.ts
507
- function drawPcbPlatedHole(params) {
508
- const {
509
- ctx,
510
- hole,
511
- realToCanvasMat,
512
- colorMap,
513
- soldermaskMargin = 0,
514
- showSoldermask
515
- } = params;
516
- if (hole.is_covered_with_solder_mask === true && showSoldermask) {
517
- return;
518
- }
519
- const copperColor = colorMap.copper.top;
520
- const copperInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0;
521
- if (hole.shape === "circle") {
522
- drawCircle({
523
- ctx,
524
- center: { x: hole.x, y: hole.y },
525
- radius: hole.outer_diameter / 2 - copperInset,
526
- fill: copperColor,
527
- realToCanvasMat
528
- });
529
- drawCircle({
530
- ctx,
531
- center: { x: hole.x, y: hole.y },
532
- radius: hole.hole_diameter / 2,
533
- fill: colorMap.drill,
534
- realToCanvasMat
535
- });
536
- return;
537
- }
538
- if (hole.shape === "oval") {
539
- drawOval({
540
- ctx,
541
- center: { x: hole.x, y: hole.y },
542
- radius_x: hole.outer_width / 2 - copperInset,
543
- radius_y: hole.outer_height / 2 - copperInset,
544
- fill: copperColor,
545
- realToCanvasMat,
546
- rotation: hole.ccw_rotation
547
- });
548
- drawOval({
549
- ctx,
550
- center: { x: hole.x, y: hole.y },
551
- radius_x: hole.hole_width / 2,
552
- radius_y: hole.hole_height / 2,
553
- fill: colorMap.drill,
554
- realToCanvasMat,
555
- rotation: hole.ccw_rotation
556
- });
557
- return;
558
- }
559
- if (hole.shape === "pill") {
560
- drawPill({
561
- ctx,
562
- center: { x: hole.x, y: hole.y },
563
- width: hole.outer_width - copperInset * 2,
564
- height: hole.outer_height - copperInset * 2,
565
- fill: copperColor,
566
- realToCanvasMat,
567
- rotation: hole.ccw_rotation
568
- });
569
- drawPill({
588
+ // lib/drawer/elements/pcb-cutout.ts
589
+ function drawPcbCutout(params) {
590
+ const { ctx, cutout, realToCanvasMat, colorMap } = params;
591
+ if (cutout.shape === "rect") {
592
+ drawRect({
570
593
  ctx,
571
- center: { x: hole.x, y: hole.y },
572
- width: hole.hole_width,
573
- height: hole.hole_height,
594
+ center: cutout.center,
595
+ width: cutout.width,
596
+ height: cutout.height,
574
597
  fill: colorMap.drill,
575
598
  realToCanvasMat,
576
- rotation: hole.ccw_rotation
599
+ rotation: cutout.rotation ?? 0,
600
+ borderRadius: cutout.corner_radius ?? 0
577
601
  });
578
602
  return;
579
603
  }
580
- if (hole.shape === "circular_hole_with_rect_pad") {
581
- drawRect({
582
- ctx,
583
- center: { x: hole.x, y: hole.y },
584
- width: hole.rect_pad_width - copperInset * 2,
585
- height: hole.rect_pad_height - copperInset * 2,
586
- fill: copperColor,
587
- realToCanvasMat,
588
- borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0
589
- });
590
- const holeX = hole.x + (hole.hole_offset_x ?? 0);
591
- const holeY = hole.y + (hole.hole_offset_y ?? 0);
604
+ if (cutout.shape === "circle") {
592
605
  drawCircle({
593
606
  ctx,
594
- center: { x: holeX, y: holeY },
595
- radius: hole.hole_diameter / 2,
596
- fill: colorMap.drill,
597
- realToCanvasMat
598
- });
599
- return;
600
- }
601
- if (hole.shape === "pill_hole_with_rect_pad") {
602
- drawRect({
603
- ctx,
604
- center: { x: hole.x, y: hole.y },
605
- width: hole.rect_pad_width - copperInset * 2,
606
- height: hole.rect_pad_height - copperInset * 2,
607
- fill: copperColor,
608
- realToCanvasMat,
609
- borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0
610
- });
611
- const holeX = hole.x + (hole.hole_offset_x ?? 0);
612
- const holeY = hole.y + (hole.hole_offset_y ?? 0);
613
- drawPill({
614
- ctx,
615
- center: { x: holeX, y: holeY },
616
- width: hole.hole_width,
617
- height: hole.hole_height,
607
+ center: cutout.center,
608
+ radius: cutout.radius,
618
609
  fill: colorMap.drill,
619
610
  realToCanvasMat
620
611
  });
621
612
  return;
622
613
  }
623
- if (hole.shape === "rotated_pill_hole_with_rect_pad") {
624
- drawRect({
625
- ctx,
626
- center: { x: hole.x, y: hole.y },
627
- width: hole.rect_pad_width - copperInset * 2,
628
- height: hole.rect_pad_height - copperInset * 2,
629
- fill: copperColor,
630
- realToCanvasMat,
631
- borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0,
632
- rotation: hole.rect_ccw_rotation
633
- });
634
- const holeX = hole.x + (hole.hole_offset_x ?? 0);
635
- const holeY = hole.y + (hole.hole_offset_y ?? 0);
636
- drawPill({
637
- ctx,
638
- center: { x: holeX, y: holeY },
639
- width: hole.hole_width,
640
- height: hole.hole_height,
641
- fill: colorMap.drill,
642
- realToCanvasMat,
643
- rotation: hole.hole_ccw_rotation
644
- });
645
- return;
646
- }
647
- if (hole.shape === "hole_with_polygon_pad") {
648
- const padOutline = hole.pad_outline;
649
- if (padOutline && padOutline.length >= 3) {
650
- const padPoints = padOutline.map((point) => ({
651
- x: hole.x + point.x,
652
- y: hole.y + point.y
653
- }));
654
- const copperPoints = copperInset > 0 ? offsetPolygonPoints(padPoints, -copperInset) : padPoints;
655
- if (copperPoints.length >= 3) {
656
- drawPolygon({
657
- ctx,
658
- points: copperPoints,
659
- fill: copperColor,
660
- realToCanvasMat
661
- });
662
- }
663
- }
664
- const holeX = hole.x + (hole.hole_offset_x ?? 0);
665
- const holeY = hole.y + (hole.hole_offset_y ?? 0);
666
- const holeShape = hole.hole_shape;
667
- if (holeShape === "circle") {
668
- drawCircle({
669
- ctx,
670
- center: { x: holeX, y: holeY },
671
- radius: (hole.hole_diameter ?? 0) / 2,
672
- fill: colorMap.drill,
673
- realToCanvasMat
674
- });
675
- } else if (holeShape === "oval") {
676
- drawOval({
677
- ctx,
678
- center: { x: holeX, y: holeY },
679
- radius_x: (hole.hole_width ?? 0) / 2,
680
- radius_y: (hole.hole_height ?? 0) / 2,
681
- fill: colorMap.drill,
682
- realToCanvasMat
683
- });
684
- } else if (holeShape === "pill") {
685
- drawPill({
686
- ctx,
687
- center: { x: holeX, y: holeY },
688
- width: hole.hole_width ?? 0,
689
- height: hole.hole_height ?? 0,
690
- fill: colorMap.drill,
691
- realToCanvasMat
692
- });
693
- } else if (holeShape === "rotated_pill") {
694
- drawPill({
614
+ if (cutout.shape === "polygon") {
615
+ if (cutout.points && cutout.points.length >= 3) {
616
+ drawPolygon({
695
617
  ctx,
696
- center: { x: holeX, y: holeY },
697
- width: hole.hole_width ?? 0,
698
- height: hole.hole_height ?? 0,
618
+ points: cutout.points,
699
619
  fill: colorMap.drill,
700
620
  realToCanvasMat
701
621
  });
@@ -704,188 +624,213 @@ function drawPcbPlatedHole(params) {
704
624
  }
705
625
  }
706
626
 
707
- // lib/drawer/elements/pcb-via.ts
708
- function drawPcbVia(params) {
709
- const { ctx, via, realToCanvasMat, colorMap } = params;
710
- drawCircle({
711
- ctx,
712
- center: { x: via.x, y: via.y },
713
- radius: via.outer_diameter / 2,
714
- fill: colorMap.copper.top,
715
- realToCanvasMat
716
- });
717
- drawCircle({
718
- ctx,
719
- center: { x: via.x, y: via.y },
720
- radius: via.hole_diameter / 2,
721
- fill: colorMap.drill,
722
- realToCanvasMat
723
- });
724
- }
725
-
726
- // lib/drawer/elements/pcb-hole.ts
727
- function getRotation(hole) {
728
- if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
729
- return hole.ccw_rotation;
730
- }
731
- return 0;
627
+ // lib/drawer/shapes/dimension-line.ts
628
+ import { applyToPoint as applyToPoint8 } from "transformation-matrix";
629
+ var TEXT_OFFSET_MULTIPLIER = 1.5;
630
+ var CHARACTER_WIDTH_MULTIPLIER = 0.6;
631
+ var TEXT_INTERSECTION_PADDING_MULTIPLIER = 0.3;
632
+ function normalize(v) {
633
+ const len = Math.hypot(v.x, v.y) || 1;
634
+ return { x: v.x / len, y: v.y / len };
732
635
  }
733
- function drawPcbHole(params) {
734
- const { ctx, hole, realToCanvasMat, colorMap, soldermaskMargin = 0 } = params;
735
- if (hole.is_covered_with_solder_mask === true) {
736
- return;
737
- }
738
- const holeInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0;
739
- if (hole.hole_shape === "circle") {
740
- drawCircle({
741
- ctx,
742
- center: { x: hole.x, y: hole.y },
743
- radius: hole.hole_diameter / 2 - holeInset,
744
- fill: colorMap.drill,
745
- realToCanvasMat
746
- });
747
- return;
748
- }
749
- if (hole.hole_shape === "square") {
750
- const rotation = getRotation(hole);
751
- drawRect({
752
- ctx,
753
- center: { x: hole.x, y: hole.y },
754
- width: hole.hole_diameter - holeInset * 2,
755
- height: hole.hole_diameter - holeInset * 2,
756
- fill: colorMap.drill,
757
- realToCanvasMat,
758
- rotation
759
- });
760
- return;
761
- }
762
- if (hole.hole_shape === "oval") {
763
- const rotation = getRotation(hole);
764
- drawOval({
765
- ctx,
766
- center: { x: hole.x, y: hole.y },
767
- radius_x: hole.hole_width / 2 - holeInset,
768
- radius_y: hole.hole_height / 2 - holeInset,
769
- fill: colorMap.drill,
770
- realToCanvasMat,
771
- rotation
772
- });
773
- return;
774
- }
775
- if (hole.hole_shape === "rect") {
776
- const rotation = getRotation(hole);
777
- drawRect({
778
- ctx,
779
- center: { x: hole.x, y: hole.y },
780
- width: hole.hole_width - holeInset * 2,
781
- height: hole.hole_height - holeInset * 2,
782
- fill: colorMap.drill,
783
- realToCanvasMat,
784
- rotation
785
- });
786
- return;
787
- }
788
- if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
789
- const rotation = getRotation(hole);
790
- drawPill({
791
- ctx,
792
- center: { x: hole.x, y: hole.y },
793
- width: hole.hole_width - holeInset * 2,
794
- height: hole.hole_height - holeInset * 2,
795
- fill: colorMap.drill,
796
- realToCanvasMat,
797
- rotation
798
- });
799
- return;
800
- }
801
- }
802
-
803
- // lib/drawer/elements/pcb-smtpad.ts
804
- function layerToColor(layer, colorMap) {
805
- return colorMap.copper[layer] ?? colorMap.copper.top;
806
- }
807
- function getBorderRadius(pad) {
808
- if (pad.shape === "rect" || pad.shape === "rotated_rect") {
809
- return pad.corner_radius ?? pad.rect_border_radius ?? 0;
810
- }
811
- return 0;
812
- }
813
- function drawPcbSmtPad(params) {
814
- const { ctx, pad, realToCanvasMat, colorMap } = params;
815
- const color = layerToColor(pad.layer, colorMap);
816
- if (pad.shape === "rect") {
817
- drawRect({
818
- ctx,
819
- center: { x: pad.x, y: pad.y },
820
- width: pad.width,
821
- height: pad.height,
822
- fill: color,
823
- realToCanvasMat,
824
- borderRadius: getBorderRadius(pad)
825
- });
826
- return;
827
- }
828
- if (pad.shape === "rotated_rect") {
829
- drawRect({
830
- ctx,
831
- center: { x: pad.x, y: pad.y },
832
- width: pad.width,
833
- height: pad.height,
834
- fill: color,
835
- realToCanvasMat,
836
- borderRadius: getBorderRadius(pad),
837
- rotation: pad.ccw_rotation ?? 0
838
- });
839
- return;
840
- }
841
- if (pad.shape === "circle") {
842
- drawCircle({
843
- ctx,
844
- center: { x: pad.x, y: pad.y },
845
- radius: pad.radius,
846
- fill: color,
847
- realToCanvasMat
848
- });
849
- return;
850
- }
851
- if (pad.shape === "pill") {
852
- drawPill({
853
- ctx,
854
- center: { x: pad.x, y: pad.y },
855
- width: pad.width,
856
- height: pad.height,
857
- fill: color,
858
- realToCanvasMat
636
+ function drawDimensionLine(params) {
637
+ const {
638
+ ctx,
639
+ from,
640
+ to,
641
+ realToCanvasMat,
642
+ color,
643
+ fontSize,
644
+ arrowSize = 1,
645
+ strokeWidth: manualStrokeWidth,
646
+ text,
647
+ textRotation,
648
+ offset
649
+ } = params;
650
+ const direction = normalize({ x: to.x - from.x, y: to.y - from.y });
651
+ const perpendicular = { x: -direction.y, y: direction.x };
652
+ const hasOffsetDirection = offset?.direction && typeof offset.direction.x === "number" && typeof offset.direction.y === "number";
653
+ const normalizedOffsetDirection = hasOffsetDirection ? normalize(offset.direction) : { x: 0, y: 0 };
654
+ const offsetMagnitude = offset?.distance ?? 0;
655
+ const offsetVector = {
656
+ x: normalizedOffsetDirection.x * offsetMagnitude,
657
+ y: normalizedOffsetDirection.y * offsetMagnitude
658
+ };
659
+ const fromOffset = { x: from.x + offsetVector.x, y: from.y + offsetVector.y };
660
+ const toOffset = { x: to.x + offsetVector.x, y: to.y + offsetVector.y };
661
+ const fromBase = {
662
+ x: fromOffset.x + direction.x * arrowSize,
663
+ y: fromOffset.y + direction.y * arrowSize
664
+ };
665
+ const toBase = {
666
+ x: toOffset.x - direction.x * arrowSize,
667
+ y: toOffset.y - direction.y * arrowSize
668
+ };
669
+ const scaleValue = Math.abs(realToCanvasMat.a);
670
+ const strokeWidth = manualStrokeWidth ?? arrowSize / 5;
671
+ const lineColor = color || "rgba(255,255,255,0.5)";
672
+ const extensionDirection = hasOffsetDirection && (Math.abs(normalizedOffsetDirection.x) > Number.EPSILON || Math.abs(normalizedOffsetDirection.y) > Number.EPSILON) ? normalizedOffsetDirection : perpendicular;
673
+ const extensionLength = offsetMagnitude + 0.5;
674
+ const allPoints = [];
675
+ const getExtensionPoints = (anchor) => {
676
+ const endPoint = {
677
+ x: anchor.x + extensionDirection.x * extensionLength,
678
+ y: anchor.y + extensionDirection.y * extensionLength
679
+ };
680
+ const halfWidth = strokeWidth / 2;
681
+ const extPerpendicular = {
682
+ x: -extensionDirection.y,
683
+ y: extensionDirection.x
684
+ };
685
+ return [
686
+ {
687
+ x: anchor.x + extPerpendicular.x * halfWidth,
688
+ y: anchor.y + extPerpendicular.y * halfWidth
689
+ },
690
+ {
691
+ x: anchor.x - extPerpendicular.x * halfWidth,
692
+ y: anchor.y - extPerpendicular.y * halfWidth
693
+ },
694
+ {
695
+ x: endPoint.x - extPerpendicular.x * halfWidth,
696
+ y: endPoint.y - extPerpendicular.y * halfWidth
697
+ },
698
+ {
699
+ x: endPoint.x + extPerpendicular.x * halfWidth,
700
+ y: endPoint.y + extPerpendicular.y * halfWidth
701
+ }
702
+ ];
703
+ };
704
+ allPoints.push(fromOffset);
705
+ allPoints.push({
706
+ x: fromBase.x + perpendicular.x * (arrowSize / 2),
707
+ y: fromBase.y + perpendicular.y * (arrowSize / 2)
708
+ });
709
+ allPoints.push({
710
+ x: fromBase.x + perpendicular.x * (strokeWidth / 2),
711
+ y: fromBase.y + perpendicular.y * (strokeWidth / 2)
712
+ });
713
+ allPoints.push({
714
+ x: toBase.x + perpendicular.x * (strokeWidth / 2),
715
+ y: toBase.y + perpendicular.y * (strokeWidth / 2)
716
+ });
717
+ allPoints.push({
718
+ x: toBase.x + perpendicular.x * (arrowSize / 2),
719
+ y: toBase.y + perpendicular.y * (arrowSize / 2)
720
+ });
721
+ allPoints.push(toOffset);
722
+ allPoints.push({
723
+ x: toBase.x - perpendicular.x * (arrowSize / 2),
724
+ y: toBase.y - perpendicular.y * (arrowSize / 2)
725
+ });
726
+ allPoints.push({
727
+ x: toBase.x - perpendicular.x * (strokeWidth / 2),
728
+ y: toBase.y - perpendicular.y * (strokeWidth / 2)
729
+ });
730
+ allPoints.push({
731
+ x: fromBase.x - perpendicular.x * (strokeWidth / 2),
732
+ y: fromBase.y - perpendicular.y * (strokeWidth / 2)
733
+ });
734
+ allPoints.push({
735
+ x: fromBase.x - perpendicular.x * (arrowSize / 2),
736
+ y: fromBase.y - perpendicular.y * (arrowSize / 2)
737
+ });
738
+ allPoints.push(fromOffset);
739
+ const startPoint = allPoints[0];
740
+ const addTick = (anchor) => {
741
+ const pts = getExtensionPoints(anchor);
742
+ allPoints.push(startPoint);
743
+ allPoints.push(pts[0]);
744
+ allPoints.push(...pts);
745
+ allPoints.push(pts[0]);
746
+ allPoints.push(startPoint);
747
+ };
748
+ addTick(from);
749
+ addTick(to);
750
+ drawPolygon({
751
+ ctx,
752
+ points: allPoints,
753
+ fill: lineColor,
754
+ realToCanvasMat
755
+ });
756
+ if (text) {
757
+ const midPoint = {
758
+ x: (from.x + to.x) / 2 + offsetVector.x,
759
+ y: (from.y + to.y) / 2 + offsetVector.y
760
+ };
761
+ const [screenFromX, screenFromY] = applyToPoint8(realToCanvasMat, [
762
+ fromOffset.x,
763
+ fromOffset.y
764
+ ]);
765
+ const [screenToX, screenToY] = applyToPoint8(realToCanvasMat, [
766
+ toOffset.x,
767
+ toOffset.y
768
+ ]);
769
+ const screenDirection = normalize({
770
+ x: screenToX - screenFromX,
771
+ y: screenToY - screenFromY
859
772
  });
860
- return;
861
- }
862
- if (pad.shape === "rotated_pill") {
863
- drawPill({
773
+ let textAngle = Math.atan2(screenDirection.y, screenDirection.x) * 180 / Math.PI;
774
+ if (textAngle > 90 || textAngle < -90) {
775
+ textAngle += 180;
776
+ }
777
+ const finalTextAngle = typeof textRotation === "number" && Number.isFinite(textRotation) ? textAngle - textRotation : textAngle;
778
+ let additionalOffset = 0;
779
+ if (text && typeof textRotation === "number" && Number.isFinite(textRotation)) {
780
+ const textWidth = text.length * fontSize * CHARACTER_WIDTH_MULTIPLIER;
781
+ const textHeight = fontSize;
782
+ const rotationRad = textRotation * Math.PI / 180;
783
+ const sinRot = Math.abs(Math.sin(rotationRad));
784
+ const cosRot = Math.abs(Math.cos(rotationRad));
785
+ const halfWidth = textWidth / 2;
786
+ const halfHeight = textHeight / 2;
787
+ const maxExtension = halfWidth * sinRot + halfHeight * cosRot;
788
+ additionalOffset = maxExtension + fontSize * TEXT_INTERSECTION_PADDING_MULTIPLIER;
789
+ }
790
+ const textOffset = arrowSize * TEXT_OFFSET_MULTIPLIER + additionalOffset;
791
+ const textPoint = {
792
+ x: midPoint.x + perpendicular.x * textOffset,
793
+ y: midPoint.y + perpendicular.y * textOffset
794
+ };
795
+ drawText({
864
796
  ctx,
865
- center: { x: pad.x, y: pad.y },
866
- width: pad.width,
867
- height: pad.height,
868
- fill: color,
797
+ text,
798
+ x: textPoint.x,
799
+ y: textPoint.y,
800
+ fontSize,
801
+ color: lineColor,
869
802
  realToCanvasMat,
870
- rotation: pad.ccw_rotation ?? 0
803
+ anchorAlignment: "center",
804
+ rotation: -finalTextAngle
805
+ // drawText expects CCW rotation in degrees
871
806
  });
872
- return;
873
- }
874
- if (pad.shape === "polygon") {
875
- if (pad.points && pad.points.length >= 3) {
876
- drawPolygon({
877
- ctx,
878
- points: pad.points,
879
- fill: color,
880
- realToCanvasMat
881
- });
882
- }
883
- return;
884
807
  }
885
808
  }
886
809
 
810
+ // lib/drawer/elements/pcb-fabrication-note-dimension.ts
811
+ var DEFAULT_FABRICATION_NOTE_COLOR = "rgba(255,255,255,0.5)";
812
+ function drawPcbFabricationNoteDimension(params) {
813
+ const { ctx, pcbFabricationNoteDimension, realToCanvasMat } = params;
814
+ const color = pcbFabricationNoteDimension.color ?? DEFAULT_FABRICATION_NOTE_COLOR;
815
+ drawDimensionLine({
816
+ ctx,
817
+ from: pcbFabricationNoteDimension.from,
818
+ to: pcbFabricationNoteDimension.to,
819
+ realToCanvasMat,
820
+ color,
821
+ fontSize: pcbFabricationNoteDimension.font_size ?? 1,
822
+ arrowSize: pcbFabricationNoteDimension.arrow_size ?? 1,
823
+ text: pcbFabricationNoteDimension.text,
824
+ textRotation: pcbFabricationNoteDimension.text_ccw_rotation,
825
+ offset: pcbFabricationNoteDimension.offset_distance && pcbFabricationNoteDimension.offset_direction ? {
826
+ distance: pcbFabricationNoteDimension.offset_distance,
827
+ direction: pcbFabricationNoteDimension.offset_direction
828
+ } : void 0
829
+ });
830
+ }
831
+
887
832
  // lib/drawer/shapes/line.ts
888
- import { applyToPoint as applyToPoint7 } from "transformation-matrix";
833
+ import { applyToPoint as applyToPoint9 } from "transformation-matrix";
889
834
  function drawLine(params) {
890
835
  const {
891
836
  ctx,
@@ -896,8 +841,8 @@ function drawLine(params) {
896
841
  realToCanvasMat,
897
842
  lineCap = "round"
898
843
  } = params;
899
- const [x1, y1] = applyToPoint7(realToCanvasMat, [start.x, start.y]);
900
- const [x2, y2] = applyToPoint7(realToCanvasMat, [end.x, end.y]);
844
+ const [x1, y1] = applyToPoint9(realToCanvasMat, [start.x, start.y]);
845
+ const [x2, y2] = applyToPoint9(realToCanvasMat, [end.x, end.y]);
901
846
  const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
902
847
  ctx.beginPath();
903
848
  ctx.moveTo(x1, y1);
@@ -908,355 +853,381 @@ function drawLine(params) {
908
853
  ctx.stroke();
909
854
  }
910
855
 
911
- // lib/drawer/elements/pcb-trace.ts
912
- function layerToColor2(layer, colorMap) {
913
- return colorMap.copper[layer] ?? colorMap.copper.top;
914
- }
915
- function drawPcbTrace(params) {
916
- const { ctx, trace, realToCanvasMat, colorMap } = params;
917
- if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
918
- return;
919
- }
920
- for (let i = 0; i < trace.route.length - 1; i++) {
921
- const start = trace.route[i];
922
- const end = trace.route[i + 1];
856
+ // lib/drawer/elements/pcb-fabrication-note-path.ts
857
+ function drawPcbFabricationNotePath(params) {
858
+ const { ctx, path, realToCanvasMat, colorMap } = params;
859
+ const defaultColor = "rgba(255,255,255,0.5)";
860
+ const color = path.color ?? defaultColor;
861
+ if (!path.route || path.route.length < 2) return;
862
+ for (let i = 0; i < path.route.length - 1; i++) {
863
+ const start = path.route[i];
864
+ const end = path.route[i + 1];
923
865
  if (!start || !end) continue;
924
- const layer = "layer" in start ? start.layer : "layer" in end ? end.layer : null;
925
- if (!layer) continue;
926
- const color = layerToColor2(layer, colorMap);
927
- const traceWidth = "width" in start ? start.width : "width" in end ? end.width : 0.1;
928
866
  drawLine({
929
867
  ctx,
930
868
  start: { x: start.x, y: start.y },
931
869
  end: { x: end.x, y: end.y },
932
- strokeWidth: traceWidth,
870
+ strokeWidth: path.stroke_width ?? 0.1,
933
871
  stroke: color,
934
- realToCanvasMat,
935
- lineCap: "round"
872
+ realToCanvasMat
936
873
  });
937
874
  }
938
875
  }
939
876
 
940
- // lib/drawer/shapes/path.ts
941
- import { applyToPoint as applyToPoint8 } from "transformation-matrix";
942
- function drawPath(params) {
877
+ // lib/drawer/elements/pcb-fabrication-note-rect.ts
878
+ function drawPcbFabricationNoteRect(params) {
879
+ const { ctx, rect, realToCanvasMat, colorMap } = params;
880
+ const defaultColor = "rgba(255,255,255,0.5)";
881
+ const color = rect.color ?? defaultColor;
882
+ const isFilled = rect.is_filled ?? false;
883
+ const hasStroke = rect.has_stroke ?? true;
884
+ const isStrokeDashed = rect.is_stroke_dashed ?? false;
885
+ drawRect({
886
+ ctx,
887
+ center: rect.center,
888
+ width: rect.width,
889
+ height: rect.height,
890
+ fill: isFilled ? color : void 0,
891
+ stroke: hasStroke ? color : void 0,
892
+ strokeWidth: hasStroke ? rect.stroke_width : void 0,
893
+ borderRadius: rect.corner_radius,
894
+ realToCanvasMat,
895
+ isStrokeDashed
896
+ });
897
+ }
898
+
899
+ // lib/drawer/elements/pcb-fabrication-note-text.ts
900
+ var DEFAULT_FABRICATION_NOTE_COLOR2 = "rgba(255,255,255,0.5)";
901
+ function layerToColor2(layer, colorMap) {
902
+ return DEFAULT_FABRICATION_NOTE_COLOR2;
903
+ }
904
+ function drawPcbFabricationNoteText(params) {
905
+ const { ctx, text, realToCanvasMat, colorMap } = params;
906
+ const defaultColor = layerToColor2(text.layer, colorMap);
907
+ const color = text.color ?? defaultColor;
908
+ const fontSize = text.font_size;
909
+ drawText({
910
+ ctx,
911
+ text: text.text,
912
+ x: text.anchor_position.x,
913
+ y: text.anchor_position.y,
914
+ fontSize,
915
+ color,
916
+ realToCanvasMat,
917
+ anchorAlignment: text.anchor_alignment
918
+ });
919
+ }
920
+
921
+ // lib/drawer/shapes/oval.ts
922
+ import { applyToPoint as applyToPoint10 } from "transformation-matrix";
923
+ function drawOval(params) {
943
924
  const {
944
925
  ctx,
945
- points,
926
+ center,
927
+ radius_x,
928
+ radius_y,
946
929
  fill,
947
930
  stroke,
948
- strokeWidth = 1,
931
+ strokeWidth = 0.1,
949
932
  realToCanvasMat,
950
- closePath = false
933
+ rotation = 0
951
934
  } = params;
952
- if (points.length < 2) return;
935
+ const [cx, cy] = applyToPoint10(realToCanvasMat, [center.x, center.y]);
936
+ const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
937
+ const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
938
+ const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
939
+ ctx.save();
940
+ ctx.translate(cx, cy);
941
+ if (rotation !== 0) {
942
+ ctx.rotate(-rotation * (Math.PI / 180));
943
+ }
953
944
  ctx.beginPath();
954
- const canvasPoints = points.map(
955
- (p) => applyToPoint8(realToCanvasMat, [p.x, p.y])
956
- );
957
- const firstPoint = canvasPoints[0];
958
- if (!firstPoint) return;
959
- const [firstX, firstY] = firstPoint;
960
- ctx.moveTo(firstX, firstY);
961
- for (let i = 1; i < canvasPoints.length; i++) {
962
- const point = canvasPoints[i];
963
- if (!point) continue;
964
- const [x, y] = point;
965
- ctx.lineTo(x, y);
945
+ ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
946
+ if (fill) {
947
+ ctx.fillStyle = fill;
948
+ ctx.fill();
966
949
  }
967
- if (closePath) {
968
- ctx.closePath();
950
+ if (stroke) {
951
+ ctx.strokeStyle = stroke;
952
+ ctx.lineWidth = scaledStrokeWidth;
953
+ ctx.stroke();
954
+ }
955
+ ctx.restore();
956
+ }
957
+
958
+ // lib/drawer/shapes/pill.ts
959
+ import { applyToPoint as applyToPoint11 } from "transformation-matrix";
960
+ function drawPill(params) {
961
+ const {
962
+ ctx,
963
+ center,
964
+ width,
965
+ height,
966
+ fill,
967
+ realToCanvasMat,
968
+ rotation = 0,
969
+ stroke,
970
+ strokeWidth
971
+ } = params;
972
+ const [cx, cy] = applyToPoint11(realToCanvasMat, [center.x, center.y]);
973
+ const scaledWidth = width * Math.abs(realToCanvasMat.a);
974
+ const scaledHeight = height * Math.abs(realToCanvasMat.a);
975
+ const scaledStrokeWidth = strokeWidth ? strokeWidth * Math.abs(realToCanvasMat.a) : void 0;
976
+ ctx.save();
977
+ ctx.translate(cx, cy);
978
+ if (rotation !== 0) {
979
+ ctx.rotate(-rotation * (Math.PI / 180));
980
+ }
981
+ ctx.beginPath();
982
+ if (scaledWidth > scaledHeight) {
983
+ const radius = scaledHeight / 2;
984
+ const straightLength = scaledWidth - scaledHeight;
985
+ ctx.moveTo(-straightLength / 2, -radius);
986
+ ctx.lineTo(straightLength / 2, -radius);
987
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
988
+ ctx.lineTo(-straightLength / 2, radius);
989
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
990
+ } else if (scaledHeight > scaledWidth) {
991
+ const radius = scaledWidth / 2;
992
+ const straightLength = scaledHeight - scaledWidth;
993
+ ctx.moveTo(radius, -straightLength / 2);
994
+ ctx.lineTo(radius, straightLength / 2);
995
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
996
+ ctx.lineTo(-radius, -straightLength / 2);
997
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
998
+ } else {
999
+ ctx.arc(0, 0, scaledWidth / 2, 0, Math.PI * 2);
969
1000
  }
1001
+ ctx.closePath();
970
1002
  if (fill) {
971
1003
  ctx.fillStyle = fill;
972
1004
  ctx.fill();
973
1005
  }
974
- if (stroke) {
975
- const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1006
+ if (stroke && scaledStrokeWidth) {
976
1007
  ctx.strokeStyle = stroke;
977
1008
  ctx.lineWidth = scaledStrokeWidth;
978
1009
  ctx.stroke();
979
1010
  }
1011
+ ctx.restore();
980
1012
  }
981
1013
 
982
- // lib/drawer/elements/pcb-board.ts
983
- function drawPcbBoard(params) {
984
- const { ctx, board, realToCanvasMat, colorMap } = params;
985
- const { width, height, center, outline } = board;
986
- if (outline && Array.isArray(outline) && outline.length >= 3) {
987
- drawPath({
1014
+ // lib/drawer/elements/pcb-hole.ts
1015
+ function getRotation(hole) {
1016
+ if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
1017
+ return hole.ccw_rotation;
1018
+ }
1019
+ return 0;
1020
+ }
1021
+ function drawPcbHole(params) {
1022
+ const { ctx, hole, realToCanvasMat, colorMap, soldermaskMargin = 0 } = params;
1023
+ if (hole.is_covered_with_solder_mask === true) {
1024
+ return;
1025
+ }
1026
+ const holeInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0;
1027
+ if (hole.hole_shape === "circle") {
1028
+ drawCircle({
988
1029
  ctx,
989
- points: outline.map((p) => ({ x: p.x, y: p.y })),
990
- fill: colorMap.substrate,
1030
+ center: { x: hole.x, y: hole.y },
1031
+ radius: hole.hole_diameter / 2 - holeInset,
1032
+ fill: colorMap.drill,
1033
+ realToCanvasMat
1034
+ });
1035
+ return;
1036
+ }
1037
+ if (hole.hole_shape === "square") {
1038
+ const rotation = getRotation(hole);
1039
+ drawRect({
1040
+ ctx,
1041
+ center: { x: hole.x, y: hole.y },
1042
+ width: hole.hole_diameter - holeInset * 2,
1043
+ height: hole.hole_diameter - holeInset * 2,
1044
+ fill: colorMap.drill,
991
1045
  realToCanvasMat,
992
- closePath: true
1046
+ rotation
993
1047
  });
994
- drawPath({
1048
+ return;
1049
+ }
1050
+ if (hole.hole_shape === "oval") {
1051
+ const rotation = getRotation(hole);
1052
+ drawOval({
995
1053
  ctx,
996
- points: outline.map((p) => ({ x: p.x, y: p.y })),
997
- stroke: colorMap.boardOutline,
998
- strokeWidth: 0.1,
1054
+ center: { x: hole.x, y: hole.y },
1055
+ radius_x: hole.hole_width / 2 - holeInset,
1056
+ radius_y: hole.hole_height / 2 - holeInset,
1057
+ fill: colorMap.drill,
999
1058
  realToCanvasMat,
1000
- closePath: true
1059
+ rotation
1001
1060
  });
1002
1061
  return;
1003
1062
  }
1004
- if (width !== void 0 && height !== void 0 && center) {
1063
+ if (hole.hole_shape === "rect") {
1064
+ const rotation = getRotation(hole);
1005
1065
  drawRect({
1006
1066
  ctx,
1007
- center,
1008
- width,
1009
- height,
1010
- fill: colorMap.substrate,
1011
- realToCanvasMat
1067
+ center: { x: hole.x, y: hole.y },
1068
+ width: hole.hole_width - holeInset * 2,
1069
+ height: hole.hole_height - holeInset * 2,
1070
+ fill: colorMap.drill,
1071
+ realToCanvasMat,
1072
+ rotation
1012
1073
  });
1013
- const halfWidth = width / 2;
1014
- const halfHeight = height / 2;
1015
- const corners = [
1016
- { x: center.x - halfWidth, y: center.y - halfHeight },
1017
- { x: center.x + halfWidth, y: center.y - halfHeight },
1018
- { x: center.x + halfWidth, y: center.y + halfHeight },
1019
- { x: center.x - halfWidth, y: center.y + halfHeight }
1020
- ];
1021
- drawPath({
1074
+ return;
1075
+ }
1076
+ if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
1077
+ const rotation = getRotation(hole);
1078
+ drawPill({
1022
1079
  ctx,
1023
- points: corners,
1024
- stroke: colorMap.boardOutline,
1025
- strokeWidth: 0.1,
1080
+ center: { x: hole.x, y: hole.y },
1081
+ width: hole.hole_width - holeInset * 2,
1082
+ height: hole.hole_height - holeInset * 2,
1083
+ fill: colorMap.drill,
1026
1084
  realToCanvasMat,
1027
- closePath: true
1085
+ rotation
1028
1086
  });
1087
+ return;
1029
1088
  }
1030
1089
  }
1031
1090
 
1032
- // lib/drawer/elements/pcb-silkscreen-text.ts
1033
- import { applyToPoint as applyToPoint10 } from "transformation-matrix";
1034
-
1035
- // lib/drawer/shapes/text/text.ts
1036
- import { lineAlphabet } from "@tscircuit/alphabet";
1037
- import { applyToPoint as applyToPoint9 } from "transformation-matrix";
1038
-
1039
- // lib/drawer/shapes/text/getAlphabetLayout.ts
1040
- var GLYPH_WIDTH_RATIO = 0.62;
1041
- var LETTER_SPACING_RATIO = 0.3;
1042
- var SPACE_WIDTH_RATIO = 1;
1043
- var STROKE_WIDTH_RATIO = 0.13;
1044
- function getAlphabetLayout(text, fontSize) {
1045
- const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
1046
- const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
1047
- const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
1048
- const characters = Array.from(text);
1049
- let width = 0;
1050
- characters.forEach((char, index) => {
1051
- const advance = char === " " ? spaceWidth : glyphWidth;
1052
- width += advance;
1053
- if (index < characters.length - 1) width += letterSpacing;
1054
- });
1055
- const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
1056
- return {
1057
- width,
1058
- height: fontSize,
1059
- glyphWidth,
1060
- letterSpacing,
1061
- spaceWidth,
1062
- strokeWidth
1063
- };
1064
- }
1065
-
1066
- // lib/drawer/shapes/text/getTextStartPosition.ts
1067
- function getTextStartPosition(alignment, layout) {
1068
- const totalWidth = layout.width + layout.strokeWidth;
1069
- const totalHeight = layout.height + layout.strokeWidth;
1070
- let x = 0;
1071
- let y = 0;
1072
- if (alignment === "center") {
1073
- x = -totalWidth / 2;
1074
- } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
1075
- x = 0;
1076
- } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
1077
- x = -totalWidth;
1078
- }
1079
- if (alignment === "center") {
1080
- y = -totalHeight / 2;
1081
- } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
1082
- y = 0;
1083
- } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
1084
- y = -totalHeight;
1085
- } else {
1086
- y = 0;
1087
- }
1088
- return { x, y };
1091
+ // lib/drawer/elements/pcb-keepout.ts
1092
+ import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1093
+ function hexToRgba(hex, alpha) {
1094
+ const r = parseInt(hex.slice(1, 3), 16);
1095
+ const g = parseInt(hex.slice(3, 5), 16);
1096
+ const b = parseInt(hex.slice(5, 7), 16);
1097
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1089
1098
  }
1090
-
1091
- // lib/drawer/shapes/text/text.ts
1092
- var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
1093
- function strokeAlphabetText(params) {
1094
- const { ctx, text, fontSize, startX, startY } = params;
1095
- const layout = getAlphabetLayout(text, fontSize);
1096
- const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
1097
- const topY = startY;
1098
- const characters = Array.from(text);
1099
- let cursor = startX + strokeWidth / 2;
1100
- characters.forEach((char, index) => {
1101
- const lines = getGlyphLines(char);
1102
- const advance = char === " " ? spaceWidth : glyphWidth;
1103
- if (lines?.length) {
1099
+ function drawPcbKeepout(params) {
1100
+ const { ctx, keepout, realToCanvasMat, colorMap } = params;
1101
+ const strokeColor = colorMap.keepout;
1102
+ const fillColor = hexToRgba(colorMap.keepout, 0.2);
1103
+ const hatchSpacing = 1;
1104
+ if (keepout.shape === "rect") {
1105
+ const [cx, cy] = applyToPoint12(realToCanvasMat, [
1106
+ keepout.center.x,
1107
+ keepout.center.y
1108
+ ]);
1109
+ const scaledWidth = keepout.width * Math.abs(realToCanvasMat.a);
1110
+ const scaledHeight = keepout.height * Math.abs(realToCanvasMat.a);
1111
+ const rotation = keepout.rotation ?? 0;
1112
+ ctx.save();
1113
+ ctx.translate(cx, cy);
1114
+ if (rotation !== 0) {
1115
+ ctx.rotate(-rotation * (Math.PI / 180));
1116
+ }
1117
+ ctx.beginPath();
1118
+ ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
1119
+ ctx.fillStyle = fillColor;
1120
+ ctx.fill();
1121
+ ctx.beginPath();
1122
+ ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
1123
+ ctx.clip();
1124
+ const scaledSpacing = hatchSpacing * Math.abs(realToCanvasMat.a);
1125
+ const diagonal = Math.sqrt(
1126
+ scaledWidth * scaledWidth + scaledHeight * scaledHeight
1127
+ );
1128
+ const halfWidth = scaledWidth / 2;
1129
+ const halfHeight = scaledHeight / 2;
1130
+ ctx.strokeStyle = strokeColor;
1131
+ ctx.lineWidth = 0.15 * Math.abs(realToCanvasMat.a);
1132
+ ctx.setLineDash([]);
1133
+ for (let offset = -diagonal; offset < diagonal * 2; offset += scaledSpacing) {
1104
1134
  ctx.beginPath();
1105
- for (const line of lines) {
1106
- const x1 = cursor + line.x1 * glyphWidth;
1107
- const y1 = topY + (1 - line.y1) * height;
1108
- const x2 = cursor + line.x2 * glyphWidth;
1109
- const y2 = topY + (1 - line.y2) * height;
1110
- ctx.moveTo(x1, y1);
1111
- ctx.lineTo(x2, y2);
1112
- }
1135
+ const startX = -halfWidth + offset;
1136
+ const startY = -halfHeight;
1137
+ const endX = -halfWidth + offset + diagonal;
1138
+ const endY = -halfHeight + diagonal;
1139
+ ctx.moveTo(startX, startY);
1140
+ ctx.lineTo(endX, endY);
1113
1141
  ctx.stroke();
1114
1142
  }
1115
- cursor += advance;
1116
- if (index < characters.length - 1) {
1117
- cursor += letterSpacing;
1118
- }
1119
- });
1120
- }
1121
- function drawText(params) {
1122
- const {
1123
- ctx,
1124
- text,
1125
- x,
1126
- y,
1127
- fontSize,
1128
- color,
1129
- realToCanvasMat,
1130
- anchorAlignment,
1131
- rotation = 0
1132
- } = params;
1133
- if (!text) return;
1134
- const [canvasX, canvasY] = applyToPoint9(realToCanvasMat, [x, y]);
1135
- const scale2 = Math.abs(realToCanvasMat.a);
1136
- const scaledFontSize = fontSize * scale2;
1137
- const layout = getAlphabetLayout(text, scaledFontSize);
1138
- const startPos = getTextStartPosition(anchorAlignment, layout);
1139
- ctx.save();
1140
- ctx.translate(canvasX, canvasY);
1141
- if (rotation !== 0) {
1142
- ctx.rotate(-rotation * (Math.PI / 180));
1143
- }
1144
- ctx.lineWidth = layout.strokeWidth;
1145
- ctx.lineCap = "round";
1146
- ctx.lineJoin = "round";
1147
- ctx.strokeStyle = color;
1148
- strokeAlphabetText({
1149
- ctx,
1150
- text,
1151
- fontSize: scaledFontSize,
1152
- startX: startPos.x,
1153
- startY: startPos.y
1154
- });
1155
- ctx.restore();
1156
- }
1157
-
1158
- // lib/drawer/elements/pcb-silkscreen-text.ts
1159
- function layerToSilkscreenColor(layer, colorMap) {
1160
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1161
- }
1162
- function mapAnchorAlignment(alignment) {
1163
- if (!alignment) return "center";
1164
- return alignment;
1165
- }
1166
- function drawPcbSilkscreenText(params) {
1167
- const { ctx, text, realToCanvasMat, colorMap } = params;
1168
- const content = text.text ?? "";
1169
- if (!content) return;
1170
- const color = layerToSilkscreenColor(text.layer, colorMap);
1171
- const [x, y] = applyToPoint10(realToCanvasMat, [
1172
- text.anchor_position.x,
1173
- text.anchor_position.y
1174
- ]);
1175
- const scale2 = Math.abs(realToCanvasMat.a);
1176
- const fontSize = (text.font_size ?? 1) * scale2;
1177
- const rotation = text.ccw_rotation ?? 0;
1178
- const layout = getAlphabetLayout(content, fontSize);
1179
- const alignment = mapAnchorAlignment(text.anchor_alignment);
1180
- const startPos = getTextStartPosition(alignment, layout);
1181
- ctx.save();
1182
- ctx.translate(x, y);
1183
- if (rotation !== 0) {
1184
- ctx.rotate(-rotation * (Math.PI / 180));
1143
+ ctx.restore();
1144
+ return;
1185
1145
  }
1186
- if (text.layer === "bottom") {
1187
- ctx.scale(-1, 1);
1146
+ if (keepout.shape === "circle") {
1147
+ const [cx, cy] = applyToPoint12(realToCanvasMat, [
1148
+ keepout.center.x,
1149
+ keepout.center.y
1150
+ ]);
1151
+ const scaledRadius = keepout.radius * Math.abs(realToCanvasMat.a);
1152
+ const scaledSpacing = hatchSpacing * Math.abs(realToCanvasMat.a);
1153
+ ctx.save();
1154
+ ctx.translate(cx, cy);
1155
+ ctx.beginPath();
1156
+ ctx.arc(0, 0, scaledRadius, 0, Math.PI * 2);
1157
+ ctx.fillStyle = fillColor;
1158
+ ctx.fill();
1159
+ ctx.beginPath();
1160
+ ctx.arc(0, 0, scaledRadius, 0, Math.PI * 2);
1161
+ ctx.clip();
1162
+ const diagonal = scaledRadius * 2;
1163
+ ctx.strokeStyle = strokeColor;
1164
+ ctx.lineWidth = 0.15 * Math.abs(realToCanvasMat.a);
1165
+ ctx.setLineDash([]);
1166
+ for (let offset = -diagonal; offset < diagonal * 2; offset += scaledSpacing) {
1167
+ ctx.beginPath();
1168
+ ctx.moveTo(offset - diagonal, -diagonal);
1169
+ ctx.lineTo(offset + diagonal, diagonal);
1170
+ ctx.stroke();
1171
+ }
1172
+ ctx.restore();
1173
+ return;
1188
1174
  }
1189
- ctx.lineWidth = layout.strokeWidth;
1190
- ctx.lineCap = "round";
1191
- ctx.lineJoin = "round";
1192
- ctx.strokeStyle = color;
1193
- strokeAlphabetText({
1194
- ctx,
1195
- text: content,
1196
- fontSize,
1197
- startX: startPos.x,
1198
- startY: startPos.y
1199
- });
1200
- ctx.restore();
1201
- }
1202
-
1203
- // lib/drawer/elements/pcb-silkscreen-rect.ts
1204
- function layerToSilkscreenColor2(layer, colorMap) {
1205
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1206
- }
1207
- function drawPcbSilkscreenRect(params) {
1208
- const { ctx, rect, realToCanvasMat, colorMap } = params;
1209
- const color = layerToSilkscreenColor2(rect.layer, colorMap);
1210
- drawRect({
1211
- ctx,
1212
- center: rect.center,
1213
- width: rect.width,
1214
- height: rect.height,
1215
- fill: color,
1216
- realToCanvasMat
1217
- });
1218
1175
  }
1219
1176
 
1220
- // lib/drawer/elements/pcb-silkscreen-circle.ts
1221
- function layerToSilkscreenColor3(layer, colorMap) {
1222
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1223
- }
1224
- function drawPcbSilkscreenCircle(params) {
1225
- const { ctx, circle, realToCanvasMat, colorMap } = params;
1226
- const color = layerToSilkscreenColor3(circle.layer, colorMap);
1227
- drawCircle({
1177
+ // lib/drawer/elements/pcb-note-dimension.ts
1178
+ var DEFAULT_NOTE_COLOR = "rgba(255,255,255,0.5)";
1179
+ function drawPcbNoteDimension(params) {
1180
+ const { ctx, pcbNoteDimension, realToCanvasMat } = params;
1181
+ const color = pcbNoteDimension.color ?? DEFAULT_NOTE_COLOR;
1182
+ drawDimensionLine({
1228
1183
  ctx,
1229
- center: circle.center,
1230
- radius: circle.radius,
1231
- fill: color,
1232
- realToCanvasMat
1184
+ from: pcbNoteDimension.from,
1185
+ to: pcbNoteDimension.to,
1186
+ realToCanvasMat,
1187
+ color,
1188
+ fontSize: pcbNoteDimension.font_size,
1189
+ arrowSize: pcbNoteDimension.arrow_size,
1190
+ text: pcbNoteDimension.text,
1191
+ textRotation: pcbNoteDimension.text_ccw_rotation,
1192
+ offset: pcbNoteDimension.offset_distance && pcbNoteDimension.offset_direction ? {
1193
+ distance: pcbNoteDimension.offset_distance,
1194
+ direction: pcbNoteDimension.offset_direction
1195
+ } : void 0
1233
1196
  });
1234
1197
  }
1235
1198
 
1236
- // lib/drawer/elements/pcb-silkscreen-line.ts
1237
- function layerToSilkscreenColor4(layer, colorMap) {
1238
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1239
- }
1240
- function drawPcbSilkscreenLine(params) {
1199
+ // lib/drawer/elements/pcb-note-line.ts
1200
+ import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1201
+ function drawPcbNoteLine(params) {
1241
1202
  const { ctx, line, realToCanvasMat, colorMap } = params;
1242
- const color = layerToSilkscreenColor4(line.layer, colorMap);
1243
- drawLine({
1244
- ctx,
1245
- start: { x: line.x1, y: line.y1 },
1246
- end: { x: line.x2, y: line.y2 },
1247
- strokeWidth: line.stroke_width ?? 0.1,
1248
- stroke: color,
1249
- realToCanvasMat
1250
- });
1203
+ const defaultColor = "rgb(89, 148, 220)";
1204
+ const color = line.color ?? defaultColor;
1205
+ const strokeWidth = line.stroke_width ?? 0.1;
1206
+ const isDashed = line.is_dashed ?? false;
1207
+ const [x1, y1] = applyToPoint13(realToCanvasMat, [line.x1, line.y1]);
1208
+ const [x2, y2] = applyToPoint13(realToCanvasMat, [line.x2, line.y2]);
1209
+ const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1210
+ ctx.save();
1211
+ if (isDashed) {
1212
+ ctx.setLineDash([scaledStrokeWidth * 2, scaledStrokeWidth * 2]);
1213
+ } else {
1214
+ ctx.setLineDash([]);
1215
+ }
1216
+ ctx.beginPath();
1217
+ ctx.moveTo(x1, y1);
1218
+ ctx.lineTo(x2, y2);
1219
+ ctx.lineWidth = scaledStrokeWidth;
1220
+ ctx.strokeStyle = color;
1221
+ ctx.lineCap = "round";
1222
+ ctx.stroke();
1223
+ ctx.restore();
1251
1224
  }
1252
1225
 
1253
- // lib/drawer/elements/pcb-silkscreen-path.ts
1254
- function layerToSilkscreenColor5(layer, colorMap) {
1255
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1256
- }
1257
- function drawPcbSilkscreenPath(params) {
1226
+ // lib/drawer/elements/pcb-note-path.ts
1227
+ function drawPcbNotePath(params) {
1258
1228
  const { ctx, path, realToCanvasMat, colorMap } = params;
1259
- const color = layerToSilkscreenColor5(path.layer, colorMap);
1229
+ const defaultColor = "rgb(89, 148, 220)";
1230
+ const color = path.color ?? defaultColor;
1260
1231
  if (!path.route || path.route.length < 2) return;
1261
1232
  for (let i = 0; i < path.route.length - 1; i++) {
1262
1233
  const start = path.route[i];
@@ -1273,515 +1244,560 @@ function drawPcbSilkscreenPath(params) {
1273
1244
  }
1274
1245
  }
1275
1246
 
1276
- // lib/drawer/elements/pcb-silkscreen-oval.ts
1277
- function layerToSilkscreenColor6(layer, colorMap) {
1278
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1279
- }
1280
- function drawPcbSilkscreenOval(params) {
1281
- const { ctx, oval, realToCanvasMat, colorMap } = params;
1282
- const color = layerToSilkscreenColor6(oval.layer, colorMap);
1283
- drawOval({
1247
+ // lib/drawer/elements/pcb-note-rect.ts
1248
+ function drawPcbNoteRect(params) {
1249
+ const { ctx, rect, realToCanvasMat, colorMap } = params;
1250
+ const defaultColor = "rgb(89, 148, 220)";
1251
+ const color = rect.color ?? defaultColor;
1252
+ const isFilled = rect.is_filled ?? false;
1253
+ const hasStroke = rect.has_stroke ?? true;
1254
+ const isStrokeDashed = rect.is_stroke_dashed ?? false;
1255
+ drawRect({
1284
1256
  ctx,
1285
- center: oval.center,
1286
- radius_x: oval.radius_x,
1287
- radius_y: oval.radius_y,
1288
- stroke: color,
1289
- strokeWidth: 0.1,
1257
+ center: rect.center,
1258
+ width: rect.width,
1259
+ height: rect.height,
1260
+ fill: isFilled ? color : void 0,
1261
+ stroke: hasStroke ? color : void 0,
1262
+ strokeWidth: hasStroke ? rect.stroke_width : void 0,
1263
+ borderRadius: rect.corner_radius,
1290
1264
  realToCanvasMat,
1291
- rotation: oval.ccw_rotation ?? 0
1265
+ isStrokeDashed
1292
1266
  });
1293
1267
  }
1294
1268
 
1295
- // lib/drawer/elements/pcb-silkscreen-pill.ts
1296
- function layerToSilkscreenColor7(layer, colorMap) {
1297
- return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1298
- }
1299
- function drawPcbSilkscreenPill(params) {
1300
- const { ctx, pill, realToCanvasMat, colorMap } = params;
1301
- const color = layerToSilkscreenColor7(pill.layer, colorMap);
1302
- const strokeWidth = 0.2;
1303
- drawPill({
1269
+ // lib/drawer/elements/pcb-note-text.ts
1270
+ var DEFAULT_NOTE_TEXT_COLOR = "rgb(89, 148, 220)";
1271
+ function drawPcbNoteText(params) {
1272
+ const { ctx, text, realToCanvasMat, colorMap } = params;
1273
+ const defaultColor = DEFAULT_NOTE_TEXT_COLOR;
1274
+ const color = text.color ?? defaultColor;
1275
+ const fontSize = text.font_size ?? 1;
1276
+ drawText({
1304
1277
  ctx,
1305
- center: pill.center,
1306
- width: pill.width,
1307
- height: pill.height,
1308
- stroke: color,
1309
- strokeWidth,
1310
- realToCanvasMat
1278
+ text: text.text ?? "",
1279
+ x: text.anchor_position.x,
1280
+ y: text.anchor_position.y,
1281
+ fontSize,
1282
+ color,
1283
+ realToCanvasMat,
1284
+ anchorAlignment: text.anchor_alignment ?? "center"
1311
1285
  });
1312
1286
  }
1313
1287
 
1314
- // lib/drawer/elements/pcb-cutout.ts
1315
- function drawPcbCutout(params) {
1316
- const { ctx, cutout, realToCanvasMat, colorMap } = params;
1317
- if (cutout.shape === "rect") {
1318
- drawRect({
1319
- ctx,
1320
- center: cutout.center,
1321
- width: cutout.width,
1322
- height: cutout.height,
1323
- fill: colorMap.drill,
1324
- realToCanvasMat,
1325
- rotation: cutout.rotation ?? 0,
1326
- borderRadius: cutout.corner_radius ?? 0
1327
- });
1328
- return;
1288
+ // lib/drawer/elements/soldermask-margin.ts
1289
+ import { applyToPoint as applyToPoint14 } from "transformation-matrix";
1290
+ function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor, asymmetricMargins) {
1291
+ const [cx, cy] = applyToPoint14(realToCanvasMat, [center.x, center.y]);
1292
+ const scaledWidth = width * Math.abs(realToCanvasMat.a);
1293
+ const scaledHeight = height * Math.abs(realToCanvasMat.a);
1294
+ const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a);
1295
+ const ml = asymmetricMargins?.left ?? margin;
1296
+ const mr = asymmetricMargins?.right ?? margin;
1297
+ const mt = asymmetricMargins?.top ?? margin;
1298
+ const mb = asymmetricMargins?.bottom ?? margin;
1299
+ const scaledThicknessL = Math.max(0, -ml) * Math.abs(realToCanvasMat.a);
1300
+ const scaledThicknessR = Math.max(0, -mr) * Math.abs(realToCanvasMat.a);
1301
+ const scaledThicknessT = Math.max(0, -mt) * Math.abs(realToCanvasMat.a);
1302
+ const scaledThicknessB = Math.max(0, -mb) * Math.abs(realToCanvasMat.a);
1303
+ ctx.save();
1304
+ ctx.translate(cx, cy);
1305
+ if (rotation !== 0) {
1306
+ ctx.rotate(-rotation * (Math.PI / 180));
1329
1307
  }
1330
- if (cutout.shape === "circle") {
1331
- drawCircle({
1332
- ctx,
1333
- center: cutout.center,
1334
- radius: cutout.radius,
1335
- fill: colorMap.drill,
1336
- realToCanvasMat
1337
- });
1338
- return;
1308
+ const prevCompositeOp = ctx.globalCompositeOperation;
1309
+ if (ctx.globalCompositeOperation !== void 0) {
1310
+ ctx.globalCompositeOperation = "source-atop";
1339
1311
  }
1340
- if (cutout.shape === "polygon") {
1341
- if (cutout.points && cutout.points.length >= 3) {
1342
- drawPolygon({
1343
- ctx,
1344
- points: cutout.points,
1345
- fill: colorMap.drill,
1346
- realToCanvasMat
1347
- });
1312
+ const outerWidth = scaledWidth;
1313
+ const outerHeight = scaledHeight;
1314
+ const outerRadius = scaledRadius;
1315
+ ctx.beginPath();
1316
+ if (outerRadius > 0) {
1317
+ const x = -outerWidth / 2;
1318
+ const y = -outerHeight / 2;
1319
+ const r = Math.min(outerRadius, outerWidth / 2, outerHeight / 2);
1320
+ ctx.moveTo(x + r, y);
1321
+ ctx.lineTo(x + outerWidth - r, y);
1322
+ ctx.arcTo(x + outerWidth, y, x + outerWidth, y + r, r);
1323
+ ctx.lineTo(x + outerWidth, y + outerHeight - r);
1324
+ ctx.arcTo(
1325
+ x + outerWidth,
1326
+ y + outerHeight,
1327
+ x + outerWidth - r,
1328
+ y + outerHeight,
1329
+ r
1330
+ );
1331
+ ctx.lineTo(x + r, y + outerHeight);
1332
+ ctx.arcTo(x, y + outerHeight, x, y + outerHeight - r, r);
1333
+ ctx.lineTo(x, y + r);
1334
+ ctx.arcTo(x, y, x + r, y, r);
1335
+ } else {
1336
+ ctx.rect(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight);
1337
+ }
1338
+ ctx.fillStyle = soldermaskColor;
1339
+ ctx.fill();
1340
+ if (ctx.globalCompositeOperation !== void 0) {
1341
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
1342
+ }
1343
+ const innerWidth = Math.max(
1344
+ 0,
1345
+ scaledWidth - (scaledThicknessL + scaledThicknessR)
1346
+ );
1347
+ const innerHeight = Math.max(
1348
+ 0,
1349
+ scaledHeight - (scaledThicknessT + scaledThicknessB)
1350
+ );
1351
+ const innerRadius = Math.max(
1352
+ 0,
1353
+ scaledRadius - (scaledThicknessL + scaledThicknessR + scaledThicknessT + scaledThicknessB) / 4
1354
+ );
1355
+ if (innerWidth > 0 && innerHeight > 0) {
1356
+ ctx.beginPath();
1357
+ const x = -scaledWidth / 2 + scaledThicknessL;
1358
+ const y = -scaledHeight / 2 + scaledThicknessT;
1359
+ if (innerRadius > 0) {
1360
+ const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2);
1361
+ ctx.moveTo(x + r, y);
1362
+ ctx.lineTo(x + innerWidth - r, y);
1363
+ ctx.arcTo(x + innerWidth, y, x + innerWidth, y + r, r);
1364
+ ctx.lineTo(x + innerWidth, y + innerHeight - r);
1365
+ ctx.arcTo(
1366
+ x + innerWidth,
1367
+ y + innerHeight,
1368
+ x + innerWidth - r,
1369
+ y + innerHeight,
1370
+ r
1371
+ );
1372
+ ctx.lineTo(x + r, y + innerHeight);
1373
+ ctx.arcTo(x, y + innerHeight, x, y + innerHeight - r, r);
1374
+ ctx.lineTo(x, y + r);
1375
+ ctx.arcTo(x, y, x + r, y, r);
1376
+ } else {
1377
+ ctx.rect(x, y, innerWidth, innerHeight);
1348
1378
  }
1349
- return;
1379
+ ctx.fillStyle = padColor;
1380
+ ctx.fill();
1350
1381
  }
1382
+ ctx.restore();
1351
1383
  }
1352
-
1353
- // lib/drawer/elements/pcb-keepout.ts
1354
- import { applyToPoint as applyToPoint11 } from "transformation-matrix";
1355
- function hexToRgba(hex, alpha) {
1356
- const r = parseInt(hex.slice(1, 3), 16);
1357
- const g = parseInt(hex.slice(3, 5), 16);
1358
- const b = parseInt(hex.slice(5, 7), 16);
1359
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1360
- }
1361
- function drawPcbKeepout(params) {
1362
- const { ctx, keepout, realToCanvasMat, colorMap } = params;
1363
- const strokeColor = colorMap.keepout;
1364
- const fillColor = hexToRgba(colorMap.keepout, 0.2);
1365
- const hatchSpacing = 1;
1366
- if (keepout.shape === "rect") {
1367
- const [cx, cy] = applyToPoint11(realToCanvasMat, [
1368
- keepout.center.x,
1369
- keepout.center.y
1370
- ]);
1371
- const scaledWidth = keepout.width * Math.abs(realToCanvasMat.a);
1372
- const scaledHeight = keepout.height * Math.abs(realToCanvasMat.a);
1373
- const rotation = keepout.rotation ?? 0;
1374
- ctx.save();
1375
- ctx.translate(cx, cy);
1376
- if (rotation !== 0) {
1377
- ctx.rotate(-rotation * (Math.PI / 180));
1378
- }
1384
+ function drawSoldermaskRingForCircle(ctx, center, radius, margin, realToCanvasMat, soldermaskColor, padColor) {
1385
+ const [cx, cy] = applyToPoint14(realToCanvasMat, [center.x, center.y]);
1386
+ const scaledRadius = radius * Math.abs(realToCanvasMat.a);
1387
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
1388
+ ctx.save();
1389
+ const prevCompositeOp = ctx.globalCompositeOperation;
1390
+ if (ctx.globalCompositeOperation !== void 0) {
1391
+ ctx.globalCompositeOperation = "source-atop";
1392
+ }
1393
+ ctx.beginPath();
1394
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
1395
+ ctx.fillStyle = soldermaskColor;
1396
+ ctx.fill();
1397
+ if (ctx.globalCompositeOperation !== void 0) {
1398
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
1399
+ }
1400
+ const innerRadius = Math.max(0, scaledRadius - scaledMargin);
1401
+ if (innerRadius > 0) {
1379
1402
  ctx.beginPath();
1380
- ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
1381
- ctx.fillStyle = fillColor;
1403
+ ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
1404
+ ctx.fillStyle = padColor;
1382
1405
  ctx.fill();
1406
+ }
1407
+ ctx.restore();
1408
+ }
1409
+ function drawSoldermaskRingForPill(ctx, center, width, height, margin, rotation, realToCanvasMat, soldermaskColor, padColor) {
1410
+ const [cx, cy] = applyToPoint14(realToCanvasMat, [center.x, center.y]);
1411
+ const scaledWidth = width * Math.abs(realToCanvasMat.a);
1412
+ const scaledHeight = height * Math.abs(realToCanvasMat.a);
1413
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
1414
+ ctx.save();
1415
+ ctx.translate(cx, cy);
1416
+ if (rotation !== 0) {
1417
+ ctx.rotate(-rotation * (Math.PI / 180));
1418
+ }
1419
+ const prevCompositeOp = ctx.globalCompositeOperation;
1420
+ if (ctx.globalCompositeOperation !== void 0) {
1421
+ ctx.globalCompositeOperation = "source-atop";
1422
+ }
1423
+ const outerWidth = scaledWidth;
1424
+ const outerHeight = scaledHeight;
1425
+ ctx.beginPath();
1426
+ if (outerWidth > outerHeight) {
1427
+ const radius = outerHeight / 2;
1428
+ const straightLength = outerWidth - outerHeight;
1429
+ ctx.moveTo(-straightLength / 2, -radius);
1430
+ ctx.lineTo(straightLength / 2, -radius);
1431
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
1432
+ ctx.lineTo(-straightLength / 2, radius);
1433
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
1434
+ } else if (outerHeight > outerWidth) {
1435
+ const radius = outerWidth / 2;
1436
+ const straightLength = outerHeight - outerWidth;
1437
+ ctx.moveTo(radius, -straightLength / 2);
1438
+ ctx.lineTo(radius, straightLength / 2);
1439
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
1440
+ ctx.lineTo(-radius, -straightLength / 2);
1441
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
1442
+ } else {
1443
+ ctx.arc(0, 0, outerWidth / 2, 0, Math.PI * 2);
1444
+ }
1445
+ ctx.fillStyle = soldermaskColor;
1446
+ ctx.fill();
1447
+ if (ctx.globalCompositeOperation !== void 0) {
1448
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
1449
+ }
1450
+ const innerWidth = scaledWidth - scaledMargin * 2;
1451
+ const innerHeight = scaledHeight - scaledMargin * 2;
1452
+ if (innerWidth > 0 && innerHeight > 0) {
1383
1453
  ctx.beginPath();
1384
- ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
1385
- ctx.clip();
1386
- const scaledSpacing = hatchSpacing * Math.abs(realToCanvasMat.a);
1387
- const diagonal = Math.sqrt(
1388
- scaledWidth * scaledWidth + scaledHeight * scaledHeight
1389
- );
1390
- const halfWidth = scaledWidth / 2;
1391
- const halfHeight = scaledHeight / 2;
1392
- ctx.strokeStyle = strokeColor;
1393
- ctx.lineWidth = 0.15 * Math.abs(realToCanvasMat.a);
1394
- ctx.setLineDash([]);
1395
- for (let offset = -diagonal; offset < diagonal * 2; offset += scaledSpacing) {
1396
- ctx.beginPath();
1397
- const startX = -halfWidth + offset;
1398
- const startY = -halfHeight;
1399
- const endX = -halfWidth + offset + diagonal;
1400
- const endY = -halfHeight + diagonal;
1401
- ctx.moveTo(startX, startY);
1402
- ctx.lineTo(endX, endY);
1403
- ctx.stroke();
1454
+ if (innerWidth > innerHeight) {
1455
+ const radius = innerHeight / 2;
1456
+ const straightLength = innerWidth - innerHeight;
1457
+ ctx.moveTo(-straightLength / 2, -radius);
1458
+ ctx.lineTo(straightLength / 2, -radius);
1459
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
1460
+ ctx.lineTo(-straightLength / 2, radius);
1461
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
1462
+ } else if (innerHeight > innerWidth) {
1463
+ const radius = innerWidth / 2;
1464
+ const straightLength = innerHeight - innerWidth;
1465
+ ctx.moveTo(radius, -straightLength / 2);
1466
+ ctx.lineTo(radius, straightLength / 2);
1467
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
1468
+ ctx.lineTo(-radius, -straightLength / 2);
1469
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
1470
+ } else {
1471
+ ctx.arc(0, 0, innerWidth / 2, 0, Math.PI * 2);
1404
1472
  }
1405
- ctx.restore();
1406
- return;
1473
+ ctx.fillStyle = padColor;
1474
+ ctx.fill();
1407
1475
  }
1408
- if (keepout.shape === "circle") {
1409
- const [cx, cy] = applyToPoint11(realToCanvasMat, [
1410
- keepout.center.x,
1411
- keepout.center.y
1412
- ]);
1413
- const scaledRadius = keepout.radius * Math.abs(realToCanvasMat.a);
1414
- const scaledSpacing = hatchSpacing * Math.abs(realToCanvasMat.a);
1415
- ctx.save();
1416
- ctx.translate(cx, cy);
1476
+ ctx.restore();
1477
+ }
1478
+ function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rotation, realToCanvasMat, soldermaskColor, holeColor) {
1479
+ const [cx, cy] = applyToPoint14(realToCanvasMat, [center.x, center.y]);
1480
+ const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
1481
+ const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
1482
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
1483
+ ctx.save();
1484
+ ctx.translate(cx, cy);
1485
+ if (rotation !== 0) {
1486
+ ctx.rotate(-rotation * (Math.PI / 180));
1487
+ }
1488
+ const prevCompositeOp = ctx.globalCompositeOperation;
1489
+ if (ctx.globalCompositeOperation !== void 0) {
1490
+ ctx.globalCompositeOperation = "source-atop";
1491
+ }
1492
+ ctx.beginPath();
1493
+ ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
1494
+ ctx.fillStyle = soldermaskColor;
1495
+ ctx.fill();
1496
+ if (ctx.globalCompositeOperation !== void 0) {
1497
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
1498
+ }
1499
+ const innerRadiusX = Math.max(0, scaledRadiusX - scaledMargin);
1500
+ const innerRadiusY = Math.max(0, scaledRadiusY - scaledMargin);
1501
+ if (innerRadiusX > 0 && innerRadiusY > 0) {
1417
1502
  ctx.beginPath();
1418
- ctx.arc(0, 0, scaledRadius, 0, Math.PI * 2);
1419
- ctx.fillStyle = fillColor;
1503
+ ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2);
1504
+ ctx.fillStyle = holeColor;
1420
1505
  ctx.fill();
1421
- ctx.beginPath();
1422
- ctx.arc(0, 0, scaledRadius, 0, Math.PI * 2);
1423
- ctx.clip();
1424
- const diagonal = scaledRadius * 2;
1425
- ctx.strokeStyle = strokeColor;
1426
- ctx.lineWidth = 0.15 * Math.abs(realToCanvasMat.a);
1427
- ctx.setLineDash([]);
1428
- for (let offset = -diagonal; offset < diagonal * 2; offset += scaledSpacing) {
1429
- ctx.beginPath();
1430
- ctx.moveTo(offset - diagonal, -diagonal);
1431
- ctx.lineTo(offset + diagonal, diagonal);
1432
- ctx.stroke();
1506
+ }
1507
+ ctx.restore();
1508
+ }
1509
+ function offsetPolygonPoints(points, offset) {
1510
+ if (points.length < 3 || offset === 0) return points;
1511
+ let centerX = 0;
1512
+ let centerY = 0;
1513
+ for (const point of points) {
1514
+ centerX += point.x;
1515
+ centerY += point.y;
1516
+ }
1517
+ centerX /= points.length;
1518
+ centerY /= points.length;
1519
+ const result = [];
1520
+ for (const point of points) {
1521
+ const vectorX = point.x - centerX;
1522
+ const vectorY = point.y - centerY;
1523
+ const distance = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
1524
+ if (distance > 0) {
1525
+ const normalizedX = vectorX / distance;
1526
+ const normalizedY = vectorY / distance;
1527
+ result.push({
1528
+ x: point.x + normalizedX * offset,
1529
+ y: point.y + normalizedY * offset
1530
+ });
1531
+ } else {
1532
+ result.push({
1533
+ x: point.x + offset,
1534
+ y: point.y
1535
+ });
1433
1536
  }
1434
- ctx.restore();
1435
- return;
1436
1537
  }
1538
+ return result;
1437
1539
  }
1438
1540
 
1439
- // lib/drawer/elements/pcb-copper-pour.ts
1440
- import { applyToPoint as applyToPoint12 } from "transformation-matrix";
1441
- function layerToColor3(layer, colorMap) {
1442
- return colorMap.copper[layer] ?? colorMap.copper.top;
1443
- }
1444
- function computeArcFromBulge(startX, startY, endX, endY, bulge) {
1445
- if (Math.abs(bulge) < 1e-10) {
1446
- return null;
1541
+ // lib/drawer/elements/pcb-plated-hole.ts
1542
+ function drawPcbPlatedHole(params) {
1543
+ const {
1544
+ ctx,
1545
+ hole,
1546
+ realToCanvasMat,
1547
+ colorMap,
1548
+ soldermaskMargin = 0,
1549
+ drawSoldermask
1550
+ } = params;
1551
+ if (hole.is_covered_with_solder_mask === true && drawSoldermask) {
1552
+ return;
1447
1553
  }
1448
- const chordX = endX - startX;
1449
- const chordY = endY - startY;
1450
- const chordLength = Math.hypot(chordX, chordY);
1451
- if (chordLength < 1e-10) {
1452
- return null;
1554
+ const copperColor = colorMap.copper.top;
1555
+ const copperInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0;
1556
+ if (hole.shape === "circle") {
1557
+ drawCircle({
1558
+ ctx,
1559
+ center: { x: hole.x, y: hole.y },
1560
+ radius: hole.outer_diameter / 2 - copperInset,
1561
+ fill: copperColor,
1562
+ realToCanvasMat
1563
+ });
1564
+ drawCircle({
1565
+ ctx,
1566
+ center: { x: hole.x, y: hole.y },
1567
+ radius: hole.hole_diameter / 2,
1568
+ fill: colorMap.drill,
1569
+ realToCanvasMat
1570
+ });
1571
+ return;
1453
1572
  }
1454
- const sagitta = Math.abs(bulge) * (chordLength / 2);
1455
- const halfChord = chordLength / 2;
1456
- const radius = (sagitta * sagitta + halfChord * halfChord) / (2 * sagitta);
1457
- const distToCenter = radius - sagitta;
1458
- const midX = (startX + endX) / 2;
1459
- const midY = (startY + endY) / 2;
1460
- const perpX = -chordY / chordLength;
1461
- const perpY = chordX / chordLength;
1462
- const sign = bulge > 0 ? -1 : 1;
1463
- const centerX = midX + sign * perpX * distToCenter;
1464
- const centerY = midY + sign * perpY * distToCenter;
1465
- return { centerX, centerY, radius };
1466
- }
1467
- function drawArcFromBulge(ctx, realStartX, realStartY, realEndX, realEndY, bulge, realToCanvasMat) {
1468
- if (Math.abs(bulge) < 1e-10) {
1469
- const [endX, endY] = applyToPoint12(realToCanvasMat, [realEndX, realEndY]);
1470
- ctx.lineTo(endX, endY);
1573
+ if (hole.shape === "oval") {
1574
+ drawOval({
1575
+ ctx,
1576
+ center: { x: hole.x, y: hole.y },
1577
+ radius_x: hole.outer_width / 2 - copperInset,
1578
+ radius_y: hole.outer_height / 2 - copperInset,
1579
+ fill: copperColor,
1580
+ realToCanvasMat,
1581
+ rotation: hole.ccw_rotation
1582
+ });
1583
+ drawOval({
1584
+ ctx,
1585
+ center: { x: hole.x, y: hole.y },
1586
+ radius_x: hole.hole_width / 2,
1587
+ radius_y: hole.hole_height / 2,
1588
+ fill: colorMap.drill,
1589
+ realToCanvasMat,
1590
+ rotation: hole.ccw_rotation
1591
+ });
1471
1592
  return;
1472
1593
  }
1473
- const arc = computeArcFromBulge(
1474
- realStartX,
1475
- realStartY,
1476
- realEndX,
1477
- realEndY,
1478
- bulge
1479
- );
1480
- if (!arc) {
1481
- const [endX, endY] = applyToPoint12(realToCanvasMat, [realEndX, realEndY]);
1482
- ctx.lineTo(endX, endY);
1594
+ if (hole.shape === "pill") {
1595
+ drawPill({
1596
+ ctx,
1597
+ center: { x: hole.x, y: hole.y },
1598
+ width: hole.outer_width - copperInset * 2,
1599
+ height: hole.outer_height - copperInset * 2,
1600
+ fill: copperColor,
1601
+ realToCanvasMat,
1602
+ rotation: hole.ccw_rotation
1603
+ });
1604
+ drawPill({
1605
+ ctx,
1606
+ center: { x: hole.x, y: hole.y },
1607
+ width: hole.hole_width,
1608
+ height: hole.hole_height,
1609
+ fill: colorMap.drill,
1610
+ realToCanvasMat,
1611
+ rotation: hole.ccw_rotation
1612
+ });
1483
1613
  return;
1484
1614
  }
1485
- const [canvasStartX, canvasStartY] = applyToPoint12(realToCanvasMat, [
1486
- realStartX,
1487
- realStartY
1488
- ]);
1489
- const [canvasEndX, canvasEndY] = applyToPoint12(realToCanvasMat, [
1490
- realEndX,
1491
- realEndY
1492
- ]);
1493
- const [canvasCenterX, canvasCenterY] = applyToPoint12(realToCanvasMat, [
1494
- arc.centerX,
1495
- arc.centerY
1496
- ]);
1497
- const canvasRadius = Math.hypot(
1498
- canvasStartX - canvasCenterX,
1499
- canvasStartY - canvasCenterY
1500
- );
1501
- const startAngle = Math.atan2(
1502
- canvasStartY - canvasCenterY,
1503
- canvasStartX - canvasCenterX
1504
- );
1505
- const endAngle = Math.atan2(
1506
- canvasEndY - canvasCenterY,
1507
- canvasEndX - canvasCenterX
1508
- );
1509
- const det = realToCanvasMat.a * realToCanvasMat.d - realToCanvasMat.b * realToCanvasMat.c;
1510
- const isFlipped = det < 0;
1511
- const counterclockwise = bulge > 0 ? !isFlipped : isFlipped;
1512
- ctx.arc(
1513
- canvasCenterX,
1514
- canvasCenterY,
1515
- canvasRadius,
1516
- startAngle,
1517
- endAngle,
1518
- counterclockwise
1519
- );
1520
- }
1521
- function drawRing(ctx, ring, realToCanvasMat) {
1522
- if (ring.vertices.length < 2) return;
1523
- if (ring.vertices.length === 2) {
1524
- const v0 = ring.vertices[0];
1525
- const v1 = ring.vertices[1];
1526
- if (v0 && v1 && Math.abs((v0.bulge ?? 0) - 1) < 1e-10 && Math.abs((v1.bulge ?? 0) - 1) < 1e-10) {
1527
- const [x0, y0] = applyToPoint12(realToCanvasMat, [v0.x, v0.y]);
1528
- ctx.moveTo(x0, y0);
1529
- drawArcFromBulge(ctx, v0.x, v0.y, v1.x, v1.y, 1, realToCanvasMat);
1530
- drawArcFromBulge(ctx, v1.x, v1.y, v0.x, v0.y, 1, realToCanvasMat);
1531
- return;
1532
- }
1615
+ if (hole.shape === "circular_hole_with_rect_pad") {
1616
+ drawRect({
1617
+ ctx,
1618
+ center: { x: hole.x, y: hole.y },
1619
+ width: hole.rect_pad_width - copperInset * 2,
1620
+ height: hole.rect_pad_height - copperInset * 2,
1621
+ fill: copperColor,
1622
+ realToCanvasMat,
1623
+ borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0
1624
+ });
1625
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
1626
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
1627
+ drawCircle({
1628
+ ctx,
1629
+ center: { x: holeX, y: holeY },
1630
+ radius: hole.hole_diameter / 2,
1631
+ fill: colorMap.drill,
1632
+ realToCanvasMat
1633
+ });
1634
+ return;
1533
1635
  }
1534
- const firstVertex = ring.vertices[0];
1535
- if (!firstVertex) return;
1536
- const [firstX, firstY] = applyToPoint12(realToCanvasMat, [
1537
- firstVertex.x,
1538
- firstVertex.y
1539
- ]);
1540
- ctx.moveTo(firstX, firstY);
1541
- for (let i = 0; i < ring.vertices.length; i++) {
1542
- const currentVertex = ring.vertices[i];
1543
- const nextIndex = (i + 1) % ring.vertices.length;
1544
- const nextVertex = ring.vertices[nextIndex];
1545
- if (!currentVertex || !nextVertex) continue;
1546
- const bulge = currentVertex.bulge ?? 0;
1547
- if (Math.abs(bulge) < 1e-10) {
1548
- const [nextX, nextY] = applyToPoint12(realToCanvasMat, [
1549
- nextVertex.x,
1550
- nextVertex.y
1551
- ]);
1552
- ctx.lineTo(nextX, nextY);
1553
- } else {
1554
- drawArcFromBulge(
1555
- ctx,
1556
- currentVertex.x,
1557
- currentVertex.y,
1558
- nextVertex.x,
1559
- nextVertex.y,
1560
- bulge,
1561
- realToCanvasMat
1562
- );
1563
- }
1636
+ if (hole.shape === "pill_hole_with_rect_pad") {
1637
+ drawRect({
1638
+ ctx,
1639
+ center: { x: hole.x, y: hole.y },
1640
+ width: hole.rect_pad_width - copperInset * 2,
1641
+ height: hole.rect_pad_height - copperInset * 2,
1642
+ fill: copperColor,
1643
+ realToCanvasMat,
1644
+ borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0
1645
+ });
1646
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
1647
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
1648
+ drawPill({
1649
+ ctx,
1650
+ center: { x: holeX, y: holeY },
1651
+ width: hole.hole_width,
1652
+ height: hole.hole_height,
1653
+ fill: colorMap.drill,
1654
+ realToCanvasMat
1655
+ });
1656
+ return;
1564
1657
  }
1565
- }
1566
- function drawPcbCopperPour(params) {
1567
- const { ctx, pour, realToCanvasMat, colorMap } = params;
1568
- const color = layerToColor3(pour.layer, colorMap);
1569
- ctx.save();
1570
- if (pour.shape === "rect") {
1571
- const [cx, cy] = applyToPoint12(realToCanvasMat, [
1572
- pour.center.x,
1573
- pour.center.y
1574
- ]);
1575
- const scaledWidth = pour.width * Math.abs(realToCanvasMat.a);
1576
- const scaledHeight = pour.height * Math.abs(realToCanvasMat.a);
1577
- ctx.translate(cx, cy);
1578
- if (pour.rotation) {
1579
- ctx.rotate(-pour.rotation * (Math.PI / 180));
1580
- }
1581
- ctx.beginPath();
1582
- ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight);
1583
- ctx.fillStyle = color;
1584
- ctx.globalAlpha = 0.5;
1585
- ctx.fill();
1586
- ctx.restore();
1658
+ if (hole.shape === "rotated_pill_hole_with_rect_pad") {
1659
+ drawRect({
1660
+ ctx,
1661
+ center: { x: hole.x, y: hole.y },
1662
+ width: hole.rect_pad_width - copperInset * 2,
1663
+ height: hole.rect_pad_height - copperInset * 2,
1664
+ fill: copperColor,
1665
+ realToCanvasMat,
1666
+ borderRadius: hole.rect_border_radius ? Math.max(0, hole.rect_border_radius - copperInset) : 0,
1667
+ rotation: hole.rect_ccw_rotation
1668
+ });
1669
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
1670
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
1671
+ drawPill({
1672
+ ctx,
1673
+ center: { x: holeX, y: holeY },
1674
+ width: hole.hole_width,
1675
+ height: hole.hole_height,
1676
+ fill: colorMap.drill,
1677
+ realToCanvasMat,
1678
+ rotation: hole.hole_ccw_rotation
1679
+ });
1587
1680
  return;
1588
1681
  }
1589
- if (pour.shape === "polygon") {
1590
- if (pour.points && pour.points.length >= 3) {
1591
- const canvasPoints = pour.points.map(
1592
- (p) => applyToPoint12(realToCanvasMat, [p.x, p.y])
1593
- );
1594
- const firstPoint = canvasPoints[0];
1595
- if (!firstPoint) {
1596
- ctx.restore();
1597
- return;
1598
- }
1599
- ctx.beginPath();
1600
- const [firstX, firstY] = firstPoint;
1601
- ctx.moveTo(firstX, firstY);
1602
- for (let i = 1; i < canvasPoints.length; i++) {
1603
- const point = canvasPoints[i];
1604
- if (!point) continue;
1605
- const [x, y] = point;
1606
- ctx.lineTo(x, y);
1682
+ if (hole.shape === "hole_with_polygon_pad") {
1683
+ const padOutline = hole.pad_outline;
1684
+ if (padOutline && padOutline.length >= 3) {
1685
+ const padPoints = padOutline.map((point) => ({
1686
+ x: hole.x + point.x,
1687
+ y: hole.y + point.y
1688
+ }));
1689
+ const copperPoints = copperInset > 0 ? offsetPolygonPoints(padPoints, -copperInset) : padPoints;
1690
+ if (copperPoints.length >= 3) {
1691
+ drawPolygon({
1692
+ ctx,
1693
+ points: copperPoints,
1694
+ fill: copperColor,
1695
+ realToCanvasMat
1696
+ });
1607
1697
  }
1608
- ctx.closePath();
1609
- ctx.fillStyle = color;
1610
- ctx.globalAlpha = 0.5;
1611
- ctx.fill();
1612
1698
  }
1613
- ctx.restore();
1614
- return;
1615
- }
1616
- if (pour.shape === "brep") {
1617
- ctx.beginPath();
1618
- drawRing(ctx, pour.brep_shape.outer_ring, realToCanvasMat);
1619
- if (pour.brep_shape.inner_rings) {
1620
- for (const innerRing of pour.brep_shape.inner_rings) {
1621
- drawRing(ctx, innerRing, realToCanvasMat);
1622
- }
1699
+ const holeX = hole.x + (hole.hole_offset_x ?? 0);
1700
+ const holeY = hole.y + (hole.hole_offset_y ?? 0);
1701
+ const holeShape = hole.hole_shape;
1702
+ if (holeShape === "circle") {
1703
+ drawCircle({
1704
+ ctx,
1705
+ center: { x: holeX, y: holeY },
1706
+ radius: (hole.hole_diameter ?? 0) / 2,
1707
+ fill: colorMap.drill,
1708
+ realToCanvasMat
1709
+ });
1710
+ } else if (holeShape === "oval") {
1711
+ drawOval({
1712
+ ctx,
1713
+ center: { x: holeX, y: holeY },
1714
+ radius_x: (hole.hole_width ?? 0) / 2,
1715
+ radius_y: (hole.hole_height ?? 0) / 2,
1716
+ fill: colorMap.drill,
1717
+ realToCanvasMat
1718
+ });
1719
+ } else if (holeShape === "pill") {
1720
+ drawPill({
1721
+ ctx,
1722
+ center: { x: holeX, y: holeY },
1723
+ width: hole.hole_width ?? 0,
1724
+ height: hole.hole_height ?? 0,
1725
+ fill: colorMap.drill,
1726
+ realToCanvasMat
1727
+ });
1728
+ } else if (holeShape === "rotated_pill") {
1729
+ drawPill({
1730
+ ctx,
1731
+ center: { x: holeX, y: holeY },
1732
+ width: hole.hole_width ?? 0,
1733
+ height: hole.hole_height ?? 0,
1734
+ fill: colorMap.drill,
1735
+ realToCanvasMat
1736
+ });
1623
1737
  }
1624
- ctx.fillStyle = color;
1625
- ctx.globalAlpha = 0.5;
1626
- ctx.fill("evenodd");
1627
- ctx.restore();
1628
1738
  return;
1629
1739
  }
1630
- ctx.restore();
1631
1740
  }
1632
1741
 
1633
- // lib/drawer/elements/pcb-copper-text.ts
1634
- import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1635
- var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
1636
- function mapAnchorAlignment2(alignment) {
1637
- if (!alignment) return "center";
1638
- if (alignment.includes("left")) return "center_left";
1639
- if (alignment.includes("right")) return "center_right";
1640
- return "center";
1742
+ // lib/drawer/elements/pcb-silkscreen-circle.ts
1743
+ function layerToSilkscreenColor(layer, colorMap) {
1744
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1641
1745
  }
1642
- function drawPcbCopperText(params) {
1643
- const { ctx, text, realToCanvasMat, colorMap } = params;
1644
- const content = text.text ?? "";
1645
- if (!content) return;
1646
- const [x, y] = applyToPoint13(realToCanvasMat, [
1647
- text.anchor_position.x,
1648
- text.anchor_position.y
1649
- ]);
1650
- const scale2 = Math.abs(realToCanvasMat.a);
1651
- const fontSize = (text.font_size ?? 1) * scale2;
1652
- const rotation = text.ccw_rotation ?? 0;
1653
- const padding = {
1654
- ...DEFAULT_PADDING,
1655
- ...text.knockout_padding
1656
- };
1657
- const textColor = colorMap.copper[text.layer] ?? colorMap.copper.top;
1658
- const layout = getAlphabetLayout(content, fontSize);
1659
- const totalWidth = layout.width + layout.strokeWidth;
1660
- const alignment = mapAnchorAlignment2(text.anchor_alignment);
1661
- const startPos = getTextStartPosition(alignment, layout);
1662
- ctx.save();
1663
- ctx.translate(x, y);
1664
- if (text.is_mirrored) ctx.scale(-1, 1);
1665
- if (rotation !== 0) ctx.rotate(-rotation * (Math.PI / 180));
1666
- ctx.lineWidth = layout.strokeWidth;
1667
- ctx.lineCap = "round";
1668
- ctx.lineJoin = "round";
1669
- if (text.is_knockout) {
1670
- const paddingLeft = padding.left * scale2;
1671
- const paddingRight = padding.right * scale2;
1672
- const paddingTop = padding.top * scale2;
1673
- const paddingBottom = padding.bottom * scale2;
1674
- const rectX = startPos.x - paddingLeft * 4;
1675
- const rectY = startPos.y - paddingTop * 4;
1676
- const rectWidth = totalWidth + paddingLeft * 2 + paddingRight * 2;
1677
- const rectHeight = layout.height + layout.strokeWidth + paddingTop * 2 + paddingBottom * 2;
1678
- ctx.fillStyle = textColor;
1679
- ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
1680
- } else {
1681
- ctx.strokeStyle = textColor;
1682
- }
1683
- strokeAlphabetText({
1746
+ function drawPcbSilkscreenCircle(params) {
1747
+ const { ctx, circle, realToCanvasMat, colorMap } = params;
1748
+ const color = layerToSilkscreenColor(circle.layer, colorMap);
1749
+ drawCircle({
1684
1750
  ctx,
1685
- text: content,
1686
- fontSize,
1687
- startX: startPos.x,
1688
- startY: startPos.y
1751
+ center: circle.center,
1752
+ radius: circle.radius,
1753
+ fill: color,
1754
+ realToCanvasMat
1689
1755
  });
1690
- ctx.restore();
1691
1756
  }
1692
1757
 
1693
- // lib/drawer/elements/pcb-fabrication-note-text.ts
1694
- var DEFAULT_FABRICATION_NOTE_COLOR = "rgba(255,255,255,0.5)";
1695
- function layerToColor4(layer, colorMap) {
1696
- return DEFAULT_FABRICATION_NOTE_COLOR;
1697
- }
1698
- function drawPcbFabricationNoteText(params) {
1699
- const { ctx, text, realToCanvasMat, colorMap } = params;
1700
- const defaultColor = layerToColor4(text.layer, colorMap);
1701
- const color = text.color ?? defaultColor;
1702
- const fontSize = text.font_size;
1703
- drawText({
1704
- ctx,
1705
- text: text.text,
1706
- x: text.anchor_position.x,
1707
- y: text.anchor_position.y,
1708
- fontSize,
1709
- color,
1710
- realToCanvasMat,
1711
- anchorAlignment: text.anchor_alignment
1712
- });
1758
+ // lib/drawer/elements/pcb-silkscreen-line.ts
1759
+ function layerToSilkscreenColor2(layer, colorMap) {
1760
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1713
1761
  }
1714
-
1715
- // lib/drawer/elements/pcb-fabrication-note-rect.ts
1716
- function drawPcbFabricationNoteRect(params) {
1717
- const { ctx, rect, realToCanvasMat, colorMap } = params;
1718
- const defaultColor = "rgba(255,255,255,0.5)";
1719
- const color = rect.color ?? defaultColor;
1720
- const isFilled = rect.is_filled ?? false;
1721
- const hasStroke = rect.has_stroke ?? true;
1722
- const isStrokeDashed = rect.is_stroke_dashed ?? false;
1723
- drawRect({
1762
+ function drawPcbSilkscreenLine(params) {
1763
+ const { ctx, line, realToCanvasMat, colorMap } = params;
1764
+ const color = layerToSilkscreenColor2(line.layer, colorMap);
1765
+ drawLine({
1724
1766
  ctx,
1725
- center: rect.center,
1726
- width: rect.width,
1727
- height: rect.height,
1728
- fill: isFilled ? color : void 0,
1729
- stroke: hasStroke ? color : void 0,
1730
- strokeWidth: hasStroke ? rect.stroke_width : void 0,
1731
- borderRadius: rect.corner_radius,
1732
- realToCanvasMat,
1733
- isStrokeDashed
1767
+ start: { x: line.x1, y: line.y1 },
1768
+ end: { x: line.x2, y: line.y2 },
1769
+ strokeWidth: line.stroke_width ?? 0.1,
1770
+ stroke: color,
1771
+ realToCanvasMat
1734
1772
  });
1735
1773
  }
1736
1774
 
1737
- // lib/drawer/elements/pcb-note-rect.ts
1738
- function drawPcbNoteRect(params) {
1739
- const { ctx, rect, realToCanvasMat, colorMap } = params;
1740
- const defaultColor = "rgb(89, 148, 220)";
1741
- const color = rect.color ?? defaultColor;
1742
- const isFilled = rect.is_filled ?? false;
1743
- const hasStroke = rect.has_stroke ?? true;
1744
- const isStrokeDashed = rect.is_stroke_dashed ?? false;
1745
- drawRect({
1746
- ctx,
1747
- center: rect.center,
1748
- width: rect.width,
1749
- height: rect.height,
1750
- fill: isFilled ? color : void 0,
1751
- stroke: hasStroke ? color : void 0,
1752
- strokeWidth: hasStroke ? rect.stroke_width : void 0,
1753
- borderRadius: rect.corner_radius,
1754
- realToCanvasMat,
1755
- isStrokeDashed
1756
- });
1775
+ // lib/drawer/elements/pcb-silkscreen-oval.ts
1776
+ function layerToSilkscreenColor3(layer, colorMap) {
1777
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1757
1778
  }
1758
-
1759
- // lib/drawer/elements/pcb-fabrication-note-path.ts
1760
- function drawPcbFabricationNotePath(params) {
1761
- const { ctx, path, realToCanvasMat, colorMap } = params;
1762
- const defaultColor = "rgba(255,255,255,0.5)";
1763
- const color = path.color ?? defaultColor;
1764
- if (!path.route || path.route.length < 2) return;
1765
- for (let i = 0; i < path.route.length - 1; i++) {
1766
- const start = path.route[i];
1767
- const end = path.route[i + 1];
1768
- if (!start || !end) continue;
1769
- drawLine({
1770
- ctx,
1771
- start: { x: start.x, y: start.y },
1772
- end: { x: end.x, y: end.y },
1773
- strokeWidth: path.stroke_width ?? 0.1,
1774
- stroke: color,
1775
- realToCanvasMat
1776
- });
1777
- }
1779
+ function drawPcbSilkscreenOval(params) {
1780
+ const { ctx, oval, realToCanvasMat, colorMap } = params;
1781
+ const color = layerToSilkscreenColor3(oval.layer, colorMap);
1782
+ drawOval({
1783
+ ctx,
1784
+ center: oval.center,
1785
+ radius_x: oval.radius_x,
1786
+ radius_y: oval.radius_y,
1787
+ stroke: color,
1788
+ strokeWidth: 0.1,
1789
+ realToCanvasMat,
1790
+ rotation: oval.ccw_rotation ?? 0
1791
+ });
1778
1792
  }
1779
1793
 
1780
- // lib/drawer/elements/pcb-note-path.ts
1781
- function drawPcbNotePath(params) {
1794
+ // lib/drawer/elements/pcb-silkscreen-path.ts
1795
+ function layerToSilkscreenColor4(layer, colorMap) {
1796
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1797
+ }
1798
+ function drawPcbSilkscreenPath(params) {
1782
1799
  const { ctx, path, realToCanvasMat, colorMap } = params;
1783
- const defaultColor = "rgb(89, 148, 220)";
1784
- const color = path.color ?? defaultColor;
1800
+ const color = layerToSilkscreenColor4(path.layer, colorMap);
1785
1801
  if (!path.route || path.route.length < 2) return;
1786
1802
  for (let i = 0; i < path.route.length - 1; i++) {
1787
1803
  const start = path.route[i];
@@ -1798,279 +1814,172 @@ function drawPcbNotePath(params) {
1798
1814
  }
1799
1815
  }
1800
1816
 
1801
- // lib/drawer/elements/pcb-note-text.ts
1802
- var DEFAULT_NOTE_TEXT_COLOR = "rgb(89, 148, 220)";
1803
- function drawPcbNoteText(params) {
1804
- const { ctx, text, realToCanvasMat, colorMap } = params;
1805
- const defaultColor = DEFAULT_NOTE_TEXT_COLOR;
1806
- const color = text.color ?? defaultColor;
1807
- const fontSize = text.font_size ?? 1;
1808
- drawText({
1809
- ctx,
1810
- text: text.text ?? "",
1811
- x: text.anchor_position.x,
1812
- y: text.anchor_position.y,
1813
- fontSize,
1814
- color,
1815
- realToCanvasMat,
1816
- anchorAlignment: text.anchor_alignment ?? "center"
1817
- });
1818
- }
1819
-
1820
- // lib/drawer/shapes/dimension-line.ts
1821
- import { applyToPoint as applyToPoint14 } from "transformation-matrix";
1822
- var TEXT_OFFSET_MULTIPLIER = 1.5;
1823
- var CHARACTER_WIDTH_MULTIPLIER = 0.6;
1824
- var TEXT_INTERSECTION_PADDING_MULTIPLIER = 0.3;
1825
- function normalize(v) {
1826
- const len = Math.hypot(v.x, v.y) || 1;
1827
- return { x: v.x / len, y: v.y / len };
1817
+ // lib/drawer/elements/pcb-silkscreen-pill.ts
1818
+ function layerToSilkscreenColor5(layer, colorMap) {
1819
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1828
1820
  }
1829
- function drawDimensionLine(params) {
1830
- const {
1831
- ctx,
1832
- from,
1833
- to,
1834
- realToCanvasMat,
1835
- color,
1836
- fontSize,
1837
- arrowSize = 1,
1838
- strokeWidth: manualStrokeWidth,
1839
- text,
1840
- textRotation,
1841
- offset
1842
- } = params;
1843
- const direction = normalize({ x: to.x - from.x, y: to.y - from.y });
1844
- const perpendicular = { x: -direction.y, y: direction.x };
1845
- const hasOffsetDirection = offset?.direction && typeof offset.direction.x === "number" && typeof offset.direction.y === "number";
1846
- const normalizedOffsetDirection = hasOffsetDirection ? normalize(offset.direction) : { x: 0, y: 0 };
1847
- const offsetMagnitude = offset?.distance ?? 0;
1848
- const offsetVector = {
1849
- x: normalizedOffsetDirection.x * offsetMagnitude,
1850
- y: normalizedOffsetDirection.y * offsetMagnitude
1851
- };
1852
- const fromOffset = { x: from.x + offsetVector.x, y: from.y + offsetVector.y };
1853
- const toOffset = { x: to.x + offsetVector.x, y: to.y + offsetVector.y };
1854
- const fromBase = {
1855
- x: fromOffset.x + direction.x * arrowSize,
1856
- y: fromOffset.y + direction.y * arrowSize
1857
- };
1858
- const toBase = {
1859
- x: toOffset.x - direction.x * arrowSize,
1860
- y: toOffset.y - direction.y * arrowSize
1861
- };
1862
- const scaleValue = Math.abs(realToCanvasMat.a);
1863
- const strokeWidth = manualStrokeWidth ?? arrowSize / 5;
1864
- const lineColor = color || "rgba(255,255,255,0.5)";
1865
- const extensionDirection = hasOffsetDirection && (Math.abs(normalizedOffsetDirection.x) > Number.EPSILON || Math.abs(normalizedOffsetDirection.y) > Number.EPSILON) ? normalizedOffsetDirection : perpendicular;
1866
- const extensionLength = offsetMagnitude + 0.5;
1867
- const allPoints = [];
1868
- const getExtensionPoints = (anchor) => {
1869
- const endPoint = {
1870
- x: anchor.x + extensionDirection.x * extensionLength,
1871
- y: anchor.y + extensionDirection.y * extensionLength
1872
- };
1873
- const halfWidth = strokeWidth / 2;
1874
- const extPerpendicular = {
1875
- x: -extensionDirection.y,
1876
- y: extensionDirection.x
1877
- };
1878
- return [
1879
- {
1880
- x: anchor.x + extPerpendicular.x * halfWidth,
1881
- y: anchor.y + extPerpendicular.y * halfWidth
1882
- },
1883
- {
1884
- x: anchor.x - extPerpendicular.x * halfWidth,
1885
- y: anchor.y - extPerpendicular.y * halfWidth
1886
- },
1887
- {
1888
- x: endPoint.x - extPerpendicular.x * halfWidth,
1889
- y: endPoint.y - extPerpendicular.y * halfWidth
1890
- },
1891
- {
1892
- x: endPoint.x + extPerpendicular.x * halfWidth,
1893
- y: endPoint.y + extPerpendicular.y * halfWidth
1894
- }
1895
- ];
1896
- };
1897
- allPoints.push(fromOffset);
1898
- allPoints.push({
1899
- x: fromBase.x + perpendicular.x * (arrowSize / 2),
1900
- y: fromBase.y + perpendicular.y * (arrowSize / 2)
1901
- });
1902
- allPoints.push({
1903
- x: fromBase.x + perpendicular.x * (strokeWidth / 2),
1904
- y: fromBase.y + perpendicular.y * (strokeWidth / 2)
1905
- });
1906
- allPoints.push({
1907
- x: toBase.x + perpendicular.x * (strokeWidth / 2),
1908
- y: toBase.y + perpendicular.y * (strokeWidth / 2)
1909
- });
1910
- allPoints.push({
1911
- x: toBase.x + perpendicular.x * (arrowSize / 2),
1912
- y: toBase.y + perpendicular.y * (arrowSize / 2)
1913
- });
1914
- allPoints.push(toOffset);
1915
- allPoints.push({
1916
- x: toBase.x - perpendicular.x * (arrowSize / 2),
1917
- y: toBase.y - perpendicular.y * (arrowSize / 2)
1918
- });
1919
- allPoints.push({
1920
- x: toBase.x - perpendicular.x * (strokeWidth / 2),
1921
- y: toBase.y - perpendicular.y * (strokeWidth / 2)
1922
- });
1923
- allPoints.push({
1924
- x: fromBase.x - perpendicular.x * (strokeWidth / 2),
1925
- y: fromBase.y - perpendicular.y * (strokeWidth / 2)
1926
- });
1927
- allPoints.push({
1928
- x: fromBase.x - perpendicular.x * (arrowSize / 2),
1929
- y: fromBase.y - perpendicular.y * (arrowSize / 2)
1930
- });
1931
- allPoints.push(fromOffset);
1932
- const startPoint = allPoints[0];
1933
- const addTick = (anchor) => {
1934
- const pts = getExtensionPoints(anchor);
1935
- allPoints.push(startPoint);
1936
- allPoints.push(pts[0]);
1937
- allPoints.push(...pts);
1938
- allPoints.push(pts[0]);
1939
- allPoints.push(startPoint);
1940
- };
1941
- addTick(from);
1942
- addTick(to);
1943
- drawPolygon({
1821
+ function drawPcbSilkscreenPill(params) {
1822
+ const { ctx, pill, realToCanvasMat, colorMap } = params;
1823
+ const color = layerToSilkscreenColor5(pill.layer, colorMap);
1824
+ const strokeWidth = 0.2;
1825
+ drawPill({
1944
1826
  ctx,
1945
- points: allPoints,
1946
- fill: lineColor,
1827
+ center: pill.center,
1828
+ width: pill.width,
1829
+ height: pill.height,
1830
+ stroke: color,
1831
+ strokeWidth,
1947
1832
  realToCanvasMat
1948
1833
  });
1949
- if (text) {
1950
- const midPoint = {
1951
- x: (from.x + to.x) / 2 + offsetVector.x,
1952
- y: (from.y + to.y) / 2 + offsetVector.y
1953
- };
1954
- const [screenFromX, screenFromY] = applyToPoint14(realToCanvasMat, [
1955
- fromOffset.x,
1956
- fromOffset.y
1957
- ]);
1958
- const [screenToX, screenToY] = applyToPoint14(realToCanvasMat, [
1959
- toOffset.x,
1960
- toOffset.y
1961
- ]);
1962
- const screenDirection = normalize({
1963
- x: screenToX - screenFromX,
1964
- y: screenToY - screenFromY
1965
- });
1966
- let textAngle = Math.atan2(screenDirection.y, screenDirection.x) * 180 / Math.PI;
1967
- if (textAngle > 90 || textAngle < -90) {
1968
- textAngle += 180;
1969
- }
1970
- const finalTextAngle = typeof textRotation === "number" && Number.isFinite(textRotation) ? textAngle - textRotation : textAngle;
1971
- let additionalOffset = 0;
1972
- if (text && typeof textRotation === "number" && Number.isFinite(textRotation)) {
1973
- const textWidth = text.length * fontSize * CHARACTER_WIDTH_MULTIPLIER;
1974
- const textHeight = fontSize;
1975
- const rotationRad = textRotation * Math.PI / 180;
1976
- const sinRot = Math.abs(Math.sin(rotationRad));
1977
- const cosRot = Math.abs(Math.cos(rotationRad));
1978
- const halfWidth = textWidth / 2;
1979
- const halfHeight = textHeight / 2;
1980
- const maxExtension = halfWidth * sinRot + halfHeight * cosRot;
1981
- additionalOffset = maxExtension + fontSize * TEXT_INTERSECTION_PADDING_MULTIPLIER;
1982
- }
1983
- const textOffset = arrowSize * TEXT_OFFSET_MULTIPLIER + additionalOffset;
1984
- const textPoint = {
1985
- x: midPoint.x + perpendicular.x * textOffset,
1986
- y: midPoint.y + perpendicular.y * textOffset
1987
- };
1988
- drawText({
1989
- ctx,
1990
- text,
1991
- x: textPoint.x,
1992
- y: textPoint.y,
1993
- fontSize,
1994
- color: lineColor,
1995
- realToCanvasMat,
1996
- anchorAlignment: "center",
1997
- rotation: -finalTextAngle
1998
- // drawText expects CCW rotation in degrees
1999
- });
2000
- }
2001
- }
2002
-
2003
- // lib/drawer/elements/pcb-note-dimension.ts
2004
- var DEFAULT_NOTE_COLOR = "rgba(255,255,255,0.5)";
2005
- function drawPcbNoteDimension(params) {
2006
- const { ctx, pcbNoteDimension, realToCanvasMat } = params;
2007
- const color = pcbNoteDimension.color ?? DEFAULT_NOTE_COLOR;
2008
- drawDimensionLine({
2009
- ctx,
2010
- from: pcbNoteDimension.from,
2011
- to: pcbNoteDimension.to,
2012
- realToCanvasMat,
2013
- color,
2014
- fontSize: pcbNoteDimension.font_size,
2015
- arrowSize: pcbNoteDimension.arrow_size,
2016
- text: pcbNoteDimension.text,
2017
- textRotation: pcbNoteDimension.text_ccw_rotation,
2018
- offset: pcbNoteDimension.offset_distance && pcbNoteDimension.offset_direction ? {
2019
- distance: pcbNoteDimension.offset_distance,
2020
- direction: pcbNoteDimension.offset_direction
2021
- } : void 0
2022
- });
2023
1834
  }
2024
1835
 
2025
- // lib/drawer/elements/pcb-fabrication-note-dimension.ts
2026
- var DEFAULT_FABRICATION_NOTE_COLOR2 = "rgba(255,255,255,0.5)";
2027
- function drawPcbFabricationNoteDimension(params) {
2028
- const { ctx, pcbFabricationNoteDimension, realToCanvasMat } = params;
2029
- const color = pcbFabricationNoteDimension.color ?? DEFAULT_FABRICATION_NOTE_COLOR2;
2030
- drawDimensionLine({
1836
+ // lib/drawer/elements/pcb-silkscreen-rect.ts
1837
+ function layerToSilkscreenColor6(layer, colorMap) {
1838
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1839
+ }
1840
+ function drawPcbSilkscreenRect(params) {
1841
+ const { ctx, rect, realToCanvasMat, colorMap } = params;
1842
+ const color = layerToSilkscreenColor6(rect.layer, colorMap);
1843
+ drawRect({
2031
1844
  ctx,
2032
- from: pcbFabricationNoteDimension.from,
2033
- to: pcbFabricationNoteDimension.to,
2034
- realToCanvasMat,
2035
- color,
2036
- fontSize: pcbFabricationNoteDimension.font_size ?? 1,
2037
- arrowSize: pcbFabricationNoteDimension.arrow_size ?? 1,
2038
- text: pcbFabricationNoteDimension.text,
2039
- textRotation: pcbFabricationNoteDimension.text_ccw_rotation,
2040
- offset: pcbFabricationNoteDimension.offset_distance && pcbFabricationNoteDimension.offset_direction ? {
2041
- distance: pcbFabricationNoteDimension.offset_distance,
2042
- direction: pcbFabricationNoteDimension.offset_direction
2043
- } : void 0
1845
+ center: rect.center,
1846
+ width: rect.width,
1847
+ height: rect.height,
1848
+ fill: color,
1849
+ realToCanvasMat
2044
1850
  });
2045
1851
  }
2046
1852
 
2047
- // lib/drawer/elements/pcb-note-line.ts
1853
+ // lib/drawer/elements/pcb-silkscreen-text.ts
2048
1854
  import { applyToPoint as applyToPoint15 } from "transformation-matrix";
2049
- function drawPcbNoteLine(params) {
2050
- const { ctx, line, realToCanvasMat, colorMap } = params;
2051
- const defaultColor = "rgb(89, 148, 220)";
2052
- const color = line.color ?? defaultColor;
2053
- const strokeWidth = line.stroke_width ?? 0.1;
2054
- const isDashed = line.is_dashed ?? false;
2055
- const [x1, y1] = applyToPoint15(realToCanvasMat, [line.x1, line.y1]);
2056
- const [x2, y2] = applyToPoint15(realToCanvasMat, [line.x2, line.y2]);
2057
- const scaledStrokeWidth = strokeWidth * Math.abs(realToCanvasMat.a);
1855
+ function layerToSilkscreenColor7(layer, colorMap) {
1856
+ return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
1857
+ }
1858
+ function mapAnchorAlignment2(alignment) {
1859
+ if (!alignment) return "center";
1860
+ return alignment;
1861
+ }
1862
+ function drawPcbSilkscreenText(params) {
1863
+ const { ctx, text, realToCanvasMat, colorMap } = params;
1864
+ const content = text.text ?? "";
1865
+ if (!content) return;
1866
+ const color = layerToSilkscreenColor7(text.layer, colorMap);
1867
+ const [x, y] = applyToPoint15(realToCanvasMat, [
1868
+ text.anchor_position.x,
1869
+ text.anchor_position.y
1870
+ ]);
1871
+ const scale2 = Math.abs(realToCanvasMat.a);
1872
+ const fontSize = (text.font_size ?? 1) * scale2;
1873
+ const rotation = text.ccw_rotation ?? 0;
1874
+ const layout = getAlphabetLayout(content, fontSize);
1875
+ const alignment = mapAnchorAlignment2(text.anchor_alignment);
1876
+ const startPos = getTextStartPosition(alignment, layout);
2058
1877
  ctx.save();
2059
- if (isDashed) {
2060
- ctx.setLineDash([scaledStrokeWidth * 2, scaledStrokeWidth * 2]);
2061
- } else {
2062
- ctx.setLineDash([]);
1878
+ ctx.translate(x, y);
1879
+ if (rotation !== 0) {
1880
+ ctx.rotate(-rotation * (Math.PI / 180));
2063
1881
  }
2064
- ctx.beginPath();
2065
- ctx.moveTo(x1, y1);
2066
- ctx.lineTo(x2, y2);
2067
- ctx.lineWidth = scaledStrokeWidth;
2068
- ctx.strokeStyle = color;
1882
+ if (text.layer === "bottom") {
1883
+ ctx.scale(-1, 1);
1884
+ }
1885
+ ctx.lineWidth = layout.strokeWidth;
2069
1886
  ctx.lineCap = "round";
2070
- ctx.stroke();
1887
+ ctx.lineJoin = "round";
1888
+ ctx.strokeStyle = color;
1889
+ strokeAlphabetText({
1890
+ ctx,
1891
+ text: content,
1892
+ fontSize,
1893
+ startX: startPos.x,
1894
+ startY: startPos.y
1895
+ });
2071
1896
  ctx.restore();
2072
1897
  }
2073
1898
 
1899
+ // lib/drawer/elements/pcb-smtpad.ts
1900
+ function layerToColor3(layer, colorMap) {
1901
+ return colorMap.copper[layer] ?? colorMap.copper.top;
1902
+ }
1903
+ function getBorderRadius(pad) {
1904
+ if (pad.shape === "rect" || pad.shape === "rotated_rect") {
1905
+ return pad.corner_radius ?? pad.rect_border_radius ?? 0;
1906
+ }
1907
+ return 0;
1908
+ }
1909
+ function drawPcbSmtPad(params) {
1910
+ const { ctx, pad, realToCanvasMat, colorMap } = params;
1911
+ const color = layerToColor3(pad.layer, colorMap);
1912
+ if (pad.shape === "rect") {
1913
+ drawRect({
1914
+ ctx,
1915
+ center: { x: pad.x, y: pad.y },
1916
+ width: pad.width,
1917
+ height: pad.height,
1918
+ fill: color,
1919
+ realToCanvasMat,
1920
+ borderRadius: getBorderRadius(pad)
1921
+ });
1922
+ return;
1923
+ }
1924
+ if (pad.shape === "rotated_rect") {
1925
+ drawRect({
1926
+ ctx,
1927
+ center: { x: pad.x, y: pad.y },
1928
+ width: pad.width,
1929
+ height: pad.height,
1930
+ fill: color,
1931
+ realToCanvasMat,
1932
+ borderRadius: getBorderRadius(pad),
1933
+ rotation: pad.ccw_rotation ?? 0
1934
+ });
1935
+ return;
1936
+ }
1937
+ if (pad.shape === "circle") {
1938
+ drawCircle({
1939
+ ctx,
1940
+ center: { x: pad.x, y: pad.y },
1941
+ radius: pad.radius,
1942
+ fill: color,
1943
+ realToCanvasMat
1944
+ });
1945
+ return;
1946
+ }
1947
+ if (pad.shape === "pill") {
1948
+ drawPill({
1949
+ ctx,
1950
+ center: { x: pad.x, y: pad.y },
1951
+ width: pad.width,
1952
+ height: pad.height,
1953
+ fill: color,
1954
+ realToCanvasMat
1955
+ });
1956
+ return;
1957
+ }
1958
+ if (pad.shape === "rotated_pill") {
1959
+ drawPill({
1960
+ ctx,
1961
+ center: { x: pad.x, y: pad.y },
1962
+ width: pad.width,
1963
+ height: pad.height,
1964
+ fill: color,
1965
+ realToCanvasMat,
1966
+ rotation: pad.ccw_rotation ?? 0
1967
+ });
1968
+ return;
1969
+ }
1970
+ if (pad.shape === "polygon") {
1971
+ if (pad.points && pad.points.length >= 3) {
1972
+ drawPolygon({
1973
+ ctx,
1974
+ points: pad.points,
1975
+ fill: color,
1976
+ realToCanvasMat
1977
+ });
1978
+ }
1979
+ return;
1980
+ }
1981
+ }
1982
+
2074
1983
  // lib/drawer/elements/pcb-soldermask/board.ts
2075
1984
  import { applyToPoint as applyToPoint16 } from "transformation-matrix";
2076
1985
 
@@ -2228,10 +2137,10 @@ function processHoleSoldermask(params) {
2228
2137
  realToCanvasMat,
2229
2138
  colorMap,
2230
2139
  soldermaskOverCopperColor,
2231
- showSoldermask
2140
+ drawSoldermask
2232
2141
  } = params;
2233
- const isCoveredWithSoldermask = showSoldermask && hole.is_covered_with_solder_mask === true;
2234
- const margin = showSoldermask ? hole.soldermask_margin ?? 0 : 0;
2142
+ const isCoveredWithSoldermask = drawSoldermask && hole.is_covered_with_solder_mask === true;
2143
+ const margin = drawSoldermask ? hole.soldermask_margin ?? 0 : 0;
2235
2144
  if (isCoveredWithSoldermask) {
2236
2145
  ctx.fillStyle = soldermaskOverCopperColor;
2237
2146
  drawHoleShapePath({ ctx, hole, realToCanvasMat, margin: 0 });
@@ -2451,11 +2360,11 @@ function processPlatedHoleSoldermask(params) {
2451
2360
  colorMap,
2452
2361
  soldermaskOverCopperColor,
2453
2362
  layer,
2454
- showSoldermask
2363
+ drawSoldermask
2455
2364
  } = params;
2456
2365
  if (hole.layers && !hole.layers.includes(layer)) return;
2457
- const isCoveredWithSoldermask = showSoldermask && hole.is_covered_with_solder_mask === true;
2458
- const margin = showSoldermask ? hole.soldermask_margin ?? 0 : 0;
2366
+ const isCoveredWithSoldermask = drawSoldermask && hole.is_covered_with_solder_mask === true;
2367
+ const margin = drawSoldermask ? hole.soldermask_margin ?? 0 : 0;
2459
2368
  const copperColor = colorMap.copper.top;
2460
2369
  if (isCoveredWithSoldermask) {
2461
2370
  ctx.fillStyle = soldermaskOverCopperColor;
@@ -2714,11 +2623,11 @@ function processSmtPadSoldermask(params) {
2714
2623
  colorMap,
2715
2624
  soldermaskOverCopperColor,
2716
2625
  layer,
2717
- showSoldermask
2626
+ drawSoldermask
2718
2627
  } = params;
2719
2628
  if (pad.layer !== layer) return;
2720
- const isCoveredWithSoldermask = showSoldermask && pad.is_covered_with_solder_mask === true;
2721
- const margin = showSoldermask ? pad.soldermask_margin ?? 0 : 0;
2629
+ const isCoveredWithSoldermask = drawSoldermask && pad.is_covered_with_solder_mask === true;
2630
+ const margin = drawSoldermask ? pad.soldermask_margin ?? 0 : 0;
2722
2631
  let ml = margin;
2723
2632
  let mr = margin;
2724
2633
  let mt = margin;
@@ -2981,11 +2890,11 @@ function drawPcbSoldermask(params) {
2981
2890
  realToCanvasMat,
2982
2891
  colorMap,
2983
2892
  layer,
2984
- showSoldermask
2893
+ drawSoldermask
2985
2894
  } = params;
2986
2895
  const soldermaskColor = colorMap.soldermask[layer] ?? colorMap.soldermask.top;
2987
2896
  const soldermaskOverCopperColor = colorMap.soldermaskOverCopper[layer] ?? colorMap.soldermaskOverCopper.top;
2988
- if (showSoldermask) {
2897
+ if (drawSoldermask) {
2989
2898
  drawBoardSoldermask({ ctx, board, realToCanvasMat, soldermaskColor });
2990
2899
  }
2991
2900
  for (const element of elements) {
@@ -2996,7 +2905,7 @@ function drawPcbSoldermask(params) {
2996
2905
  colorMap,
2997
2906
  soldermaskOverCopperColor,
2998
2907
  layer,
2999
- showSoldermask
2908
+ drawSoldermask
3000
2909
  });
3001
2910
  }
3002
2911
  }
@@ -3008,7 +2917,7 @@ function processElementSoldermask(params) {
3008
2917
  colorMap,
3009
2918
  soldermaskOverCopperColor,
3010
2919
  layer,
3011
- showSoldermask
2920
+ drawSoldermask
3012
2921
  } = params;
3013
2922
  if (element.type === "pcb_smtpad") {
3014
2923
  processSmtPadSoldermask({
@@ -3018,7 +2927,7 @@ function processElementSoldermask(params) {
3018
2927
  colorMap,
3019
2928
  soldermaskOverCopperColor,
3020
2929
  layer,
3021
- showSoldermask
2930
+ drawSoldermask
3022
2931
  });
3023
2932
  } else if (element.type === "pcb_plated_hole") {
3024
2933
  processPlatedHoleSoldermask({
@@ -3028,7 +2937,7 @@ function processElementSoldermask(params) {
3028
2937
  colorMap,
3029
2938
  soldermaskOverCopperColor,
3030
2939
  layer,
3031
- showSoldermask
2940
+ drawSoldermask
3032
2941
  });
3033
2942
  } else if (element.type === "pcb_hole") {
3034
2943
  processHoleSoldermask({
@@ -3037,7 +2946,7 @@ function processElementSoldermask(params) {
3037
2946
  realToCanvasMat,
3038
2947
  colorMap,
3039
2948
  soldermaskOverCopperColor,
3040
- showSoldermask
2949
+ drawSoldermask
3041
2950
  });
3042
2951
  } else if (element.type === "pcb_via") {
3043
2952
  processViaSoldermask({
@@ -3056,6 +2965,103 @@ function processElementSoldermask(params) {
3056
2965
  }
3057
2966
  }
3058
2967
 
2968
+ // lib/drawer/elements/pcb-trace.ts
2969
+ function layerToColor4(layer, colorMap) {
2970
+ return colorMap.copper[layer] ?? colorMap.copper.top;
2971
+ }
2972
+ function drawPcbTrace(params) {
2973
+ const { ctx, trace, realToCanvasMat, colorMap } = params;
2974
+ if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
2975
+ return;
2976
+ }
2977
+ for (let i = 0; i < trace.route.length - 1; i++) {
2978
+ const start = trace.route[i];
2979
+ const end = trace.route[i + 1];
2980
+ if (!start || !end) continue;
2981
+ const layer = "layer" in start ? start.layer : "layer" in end ? end.layer : null;
2982
+ if (!layer) continue;
2983
+ const color = layerToColor4(layer, colorMap);
2984
+ const traceWidth = "width" in start ? start.width : "width" in end ? end.width : 0.1;
2985
+ drawLine({
2986
+ ctx,
2987
+ start: { x: start.x, y: start.y },
2988
+ end: { x: end.x, y: end.y },
2989
+ strokeWidth: traceWidth,
2990
+ stroke: color,
2991
+ realToCanvasMat,
2992
+ lineCap: "round"
2993
+ });
2994
+ }
2995
+ }
2996
+
2997
+ // lib/drawer/elements/pcb-via.ts
2998
+ function drawPcbVia(params) {
2999
+ const { ctx, via, realToCanvasMat, colorMap } = params;
3000
+ drawCircle({
3001
+ ctx,
3002
+ center: { x: via.x, y: via.y },
3003
+ radius: via.outer_diameter / 2,
3004
+ fill: colorMap.copper.top,
3005
+ realToCanvasMat
3006
+ });
3007
+ drawCircle({
3008
+ ctx,
3009
+ center: { x: via.x, y: via.y },
3010
+ radius: via.hole_diameter / 2,
3011
+ fill: colorMap.drill,
3012
+ realToCanvasMat
3013
+ });
3014
+ }
3015
+
3016
+ // lib/drawer/pcb-render-layer-filter.ts
3017
+ import { getElementRenderLayers } from "@tscircuit/circuit-json-util";
3018
+ function shouldDrawElement(element, options) {
3019
+ if (!options.layers || options.layers.length === 0) {
3020
+ return true;
3021
+ }
3022
+ const elementLayers = getElementRenderLayers(element);
3023
+ if (elementLayers.length === 0) {
3024
+ return true;
3025
+ }
3026
+ return elementLayers.some((layer) => options.layers.includes(layer));
3027
+ }
3028
+
3029
+ // lib/drawer/types.ts
3030
+ var DEFAULT_PCB_COLOR_MAP = {
3031
+ copper: {
3032
+ top: "rgb(200, 52, 52)",
3033
+ inner1: "rgb(255, 140, 0)",
3034
+ inner2: "rgb(255, 215, 0)",
3035
+ inner3: "rgb(50, 205, 50)",
3036
+ inner4: "rgb(64, 224, 208)",
3037
+ inner5: "rgb(138, 43, 226)",
3038
+ inner6: "rgb(255, 105, 180)",
3039
+ bottom: "rgb(77, 127, 196)"
3040
+ },
3041
+ soldermaskWithCopperUnderneath: {
3042
+ top: "rgb(18, 82, 50)",
3043
+ bottom: "rgb(77, 127, 196)"
3044
+ },
3045
+ soldermask: {
3046
+ top: "rgb(12, 55, 33)",
3047
+ bottom: "rgb(12, 55, 33)"
3048
+ },
3049
+ soldermaskOverCopper: {
3050
+ top: "rgb(52, 135, 73)",
3051
+ bottom: "rgb(52, 135, 73)"
3052
+ },
3053
+ substrate: "rgb(201, 162, 110)",
3054
+ drill: "#FF26E2",
3055
+ silkscreen: {
3056
+ top: "#f2eda1",
3057
+ bottom: "#5da9e9"
3058
+ },
3059
+ boardOutline: "rgba(255, 255, 255, 0.5)",
3060
+ courtyard: "#FF00FF",
3061
+ keepout: "#FF6B6B"
3062
+ // Red color for keepout zones
3063
+ };
3064
+
3059
3065
  // lib/drawer/CircuitToCanvasDrawer.ts
3060
3066
  var CircuitToCanvasDrawer = class {
3061
3067
  ctx;
@@ -3126,7 +3132,8 @@ var CircuitToCanvasDrawer = class {
3126
3132
  ctx: this.ctx,
3127
3133
  board,
3128
3134
  realToCanvasMat: this.realToCanvasMat,
3129
- colorMap: this.colorMap
3135
+ colorMap: this.colorMap,
3136
+ drawBoardMaterial: options.drawBoardMaterial ?? false
3130
3137
  });
3131
3138
  }
3132
3139
  for (const element of elements) {
@@ -3148,7 +3155,7 @@ var CircuitToCanvasDrawer = class {
3148
3155
  });
3149
3156
  }
3150
3157
  }
3151
- const showSoldermask = options.showSoldermask ?? false;
3158
+ const drawSoldermask = options.drawSoldermask ?? false;
3152
3159
  if (board) {
3153
3160
  drawPcbSoldermask({
3154
3161
  ctx: this.ctx,
@@ -3157,7 +3164,7 @@ var CircuitToCanvasDrawer = class {
3157
3164
  realToCanvasMat: this.realToCanvasMat,
3158
3165
  colorMap: this.colorMap,
3159
3166
  layer: "top",
3160
- showSoldermask
3167
+ drawSoldermask
3161
3168
  });
3162
3169
  }
3163
3170
  for (const element of elements) {
@@ -3246,8 +3253,8 @@ var CircuitToCanvasDrawer = class {
3246
3253
  hole: element,
3247
3254
  realToCanvasMat: this.realToCanvasMat,
3248
3255
  colorMap: this.colorMap,
3249
- soldermaskMargin: showSoldermask ? element.soldermask_margin : void 0,
3250
- showSoldermask
3256
+ soldermaskMargin: drawSoldermask ? element.soldermask_margin : void 0,
3257
+ drawSoldermask
3251
3258
  });
3252
3259
  }
3253
3260
  if (element.type === "pcb_via") {
@@ -3267,7 +3274,7 @@ var CircuitToCanvasDrawer = class {
3267
3274
  hole: element,
3268
3275
  realToCanvasMat: this.realToCanvasMat,
3269
3276
  colorMap: this.colorMap,
3270
- soldermaskMargin: showSoldermask ? element.soldermask_margin : void 0
3277
+ soldermaskMargin: drawSoldermask ? element.soldermask_margin : void 0
3271
3278
  });
3272
3279
  }
3273
3280
  if (element.type === "pcb_cutout") {