circuit-to-canvas 0.0.51 → 0.0.53

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