circuit-to-canvas 0.0.46 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,10 +8,10 @@ import { Matrix } from 'transformation-matrix';
8
8
  interface CanvasContext {
9
9
  beginPath(): void;
10
10
  closePath(): void;
11
- arc(x: number, y: number, radius: number, startAngle: number, endAngle: number): void;
11
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
12
12
  arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
13
13
  ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number): void;
14
- fill(): void;
14
+ fill(fillRule?: "nonzero" | "evenodd"): void;
15
15
  stroke(): void;
16
16
  rect(x: number, y: number, w: number, h: number): void;
17
17
  lineTo(x: number, y: number): void;
@@ -25,6 +25,7 @@ interface CanvasContext {
25
25
  globalCompositeOperation?: string;
26
26
  fillStyle: string | CanvasGradient | CanvasPattern;
27
27
  strokeStyle: string | CanvasGradient | CanvasPattern;
28
+ globalAlpha: number;
28
29
  lineWidth: number;
29
30
  lineCap: "butt" | "round" | "square";
30
31
  lineJoin: "bevel" | "round" | "miter";
package/dist/index.js CHANGED
@@ -456,6 +456,90 @@ function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rota
456
456
  }
457
457
  ctx.restore();
458
458
  }
459
+ function offsetPolygonPoints(points, offset) {
460
+ if (points.length < 3 || offset === 0) return points;
461
+ let centerX = 0;
462
+ let centerY = 0;
463
+ for (const point of points) {
464
+ centerX += point.x;
465
+ centerY += point.y;
466
+ }
467
+ centerX /= points.length;
468
+ centerY /= points.length;
469
+ const result = [];
470
+ for (const point of points) {
471
+ const vectorX = point.x - centerX;
472
+ const vectorY = point.y - centerY;
473
+ const distance = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
474
+ if (distance > 0) {
475
+ const normalizedX = vectorX / distance;
476
+ const normalizedY = vectorY / distance;
477
+ result.push({
478
+ x: point.x + normalizedX * offset,
479
+ y: point.y + normalizedY * offset
480
+ });
481
+ } else {
482
+ result.push({
483
+ x: point.x + offset,
484
+ y: point.y
485
+ });
486
+ }
487
+ }
488
+ return result;
489
+ }
490
+ function drawSoldermaskRingForPolygon(ctx, points, margin, realToCanvasMat, soldermaskColor, padColor) {
491
+ if (points.length < 3 || margin >= 0) return;
492
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
493
+ const prevCompositeOp = ctx.globalCompositeOperation;
494
+ if (ctx.globalCompositeOperation !== void 0) {
495
+ ctx.globalCompositeOperation = "source-atop";
496
+ }
497
+ ctx.beginPath();
498
+ const canvasPoints = points.map(
499
+ (p) => applyToPoint6(realToCanvasMat, [p.x, p.y])
500
+ );
501
+ const firstPoint = canvasPoints[0];
502
+ if (firstPoint) {
503
+ const [firstX, firstY] = firstPoint;
504
+ ctx.moveTo(firstX, firstY);
505
+ for (let i = 1; i < canvasPoints.length; i++) {
506
+ const point = canvasPoints[i];
507
+ if (point) {
508
+ const [x, y] = point;
509
+ ctx.lineTo(x, y);
510
+ }
511
+ }
512
+ ctx.closePath();
513
+ ctx.fillStyle = soldermaskColor;
514
+ ctx.fill();
515
+ }
516
+ if (ctx.globalCompositeOperation !== void 0) {
517
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
518
+ }
519
+ const innerPoints = offsetPolygonPoints(points, margin);
520
+ if (innerPoints.length >= 3) {
521
+ ctx.beginPath();
522
+ const innerCanvasPoints = innerPoints.map(
523
+ (p) => applyToPoint6(realToCanvasMat, [p.x, p.y])
524
+ );
525
+ const firstInnerPoint = innerCanvasPoints[0];
526
+ if (firstInnerPoint) {
527
+ const [firstX, firstY] = firstInnerPoint;
528
+ ctx.moveTo(firstX, firstY);
529
+ for (let i = 1; i < innerCanvasPoints.length; i++) {
530
+ const point = innerCanvasPoints[i];
531
+ if (point) {
532
+ const [x, y] = point;
533
+ ctx.lineTo(x, y);
534
+ }
535
+ }
536
+ ctx.closePath();
537
+ ctx.fillStyle = padColor;
538
+ ctx.fill();
539
+ }
540
+ }
541
+ ctx.restore();
542
+ }
459
543
 
460
544
  // lib/drawer/elements/pcb-plated-hole.ts
461
545
  function getSoldermaskColor(layers, colorMap) {
@@ -643,7 +727,7 @@ function drawPcbPlatedHole(params) {
643
727
  height: hole.rect_pad_height + margin * 2,
644
728
  fill: positiveMarginColor,
645
729
  realToCanvasMat,
646
- borderRadius: (hole.rect_border_radius ?? 0) + margin
730
+ borderRadius: hole.rect_border_radius ?? 0
647
731
  });
648
732
  }
649
733
  drawRect({
@@ -702,7 +786,7 @@ function drawPcbPlatedHole(params) {
702
786
  height: hole.rect_pad_height + margin * 2,
703
787
  fill: positiveMarginColor,
704
788
  realToCanvasMat,
705
- borderRadius: (hole.rect_border_radius ?? 0) + margin
789
+ borderRadius: hole.rect_border_radius ?? 0
706
790
  });
707
791
  }
708
792
  drawRect({
@@ -762,7 +846,7 @@ function drawPcbPlatedHole(params) {
762
846
  height: hole.rect_pad_height + margin * 2,
763
847
  fill: positiveMarginColor,
764
848
  realToCanvasMat,
765
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
849
+ borderRadius: hole.rect_border_radius ?? 0,
766
850
  rotation: hole.rect_ccw_rotation
767
851
  });
768
852
  }
@@ -824,12 +908,39 @@ function drawPcbPlatedHole(params) {
824
908
  x: hole.x + point.x,
825
909
  y: hole.y + point.y
826
910
  }));
911
+ if (hasSoldermask && margin > 0) {
912
+ const expandedPoints = offsetPolygonPoints(padPoints, margin);
913
+ drawPolygon({
914
+ ctx,
915
+ points: expandedPoints,
916
+ fill: positiveMarginColor,
917
+ realToCanvasMat
918
+ });
919
+ }
827
920
  drawPolygon({
828
921
  ctx,
829
922
  points: padPoints,
830
923
  fill: copperColor,
831
924
  realToCanvasMat
832
925
  });
926
+ if (hasSoldermask && margin < 0) {
927
+ drawSoldermaskRingForPolygon(
928
+ ctx,
929
+ padPoints,
930
+ margin,
931
+ realToCanvasMat,
932
+ soldermaskRingColor,
933
+ copperColor
934
+ );
935
+ }
936
+ if (isCoveredWithSoldermask) {
937
+ drawPolygon({
938
+ ctx,
939
+ points: padPoints,
940
+ fill: soldermaskRingColor,
941
+ realToCanvasMat
942
+ });
943
+ }
833
944
  }
834
945
  if (!isCoveredWithSoldermask) {
835
946
  const holeX = hole.x + (hole.hole_offset_x ?? 0);
@@ -1233,7 +1344,7 @@ function drawPcbSmtPad(params) {
1233
1344
  height: pad.height + margin * 2,
1234
1345
  fill: positiveMarginColor,
1235
1346
  realToCanvasMat,
1236
- borderRadius: getBorderRadius(pad, margin)
1347
+ borderRadius: getBorderRadius(pad)
1237
1348
  });
1238
1349
  }
1239
1350
  drawRect({
@@ -1281,7 +1392,7 @@ function drawPcbSmtPad(params) {
1281
1392
  height: pad.height + margin * 2,
1282
1393
  fill: positiveMarginColor,
1283
1394
  realToCanvasMat,
1284
- borderRadius: getBorderRadius(pad, margin),
1395
+ borderRadius: getBorderRadius(pad),
1285
1396
  rotation: pad.ccw_rotation ?? 0
1286
1397
  });
1287
1398
  }
@@ -1455,13 +1566,32 @@ function drawPcbSmtPad(params) {
1455
1566
  }
1456
1567
  if (pad.shape === "polygon") {
1457
1568
  if (pad.points && pad.points.length >= 3) {
1569
+ if (hasSoldermask && margin > 0) {
1570
+ const expandedPoints = offsetPolygonPoints(pad.points, margin);
1571
+ drawPolygon({
1572
+ ctx,
1573
+ points: expandedPoints,
1574
+ fill: positiveMarginColor,
1575
+ realToCanvasMat
1576
+ });
1577
+ }
1458
1578
  drawPolygon({
1459
1579
  ctx,
1460
1580
  points: pad.points,
1461
1581
  fill: color,
1462
1582
  realToCanvasMat
1463
1583
  });
1464
- if (isCoveredWithSoldermask && margin >= 0) {
1584
+ if (hasSoldermask && margin < 0) {
1585
+ drawSoldermaskRingForPolygon(
1586
+ ctx,
1587
+ pad.points,
1588
+ margin,
1589
+ realToCanvasMat,
1590
+ soldermaskRingColor,
1591
+ color
1592
+ );
1593
+ }
1594
+ if (isCoveredWithSoldermask && margin === 0) {
1465
1595
  drawPolygon({
1466
1596
  ctx,
1467
1597
  points: pad.points,
@@ -2024,6 +2154,128 @@ import { applyToPoint as applyToPoint12 } from "transformation-matrix";
2024
2154
  function layerToColor3(layer, colorMap) {
2025
2155
  return colorMap.copper[layer] ?? colorMap.copper.top;
2026
2156
  }
2157
+ function computeArcFromBulge(startX, startY, endX, endY, bulge) {
2158
+ if (Math.abs(bulge) < 1e-10) {
2159
+ return null;
2160
+ }
2161
+ const chordX = endX - startX;
2162
+ const chordY = endY - startY;
2163
+ const chordLength = Math.hypot(chordX, chordY);
2164
+ if (chordLength < 1e-10) {
2165
+ return null;
2166
+ }
2167
+ const sagitta = Math.abs(bulge) * (chordLength / 2);
2168
+ const halfChord = chordLength / 2;
2169
+ const radius = (sagitta * sagitta + halfChord * halfChord) / (2 * sagitta);
2170
+ const distToCenter = radius - sagitta;
2171
+ const midX = (startX + endX) / 2;
2172
+ const midY = (startY + endY) / 2;
2173
+ const perpX = -chordY / chordLength;
2174
+ const perpY = chordX / chordLength;
2175
+ const sign = bulge > 0 ? -1 : 1;
2176
+ const centerX = midX + sign * perpX * distToCenter;
2177
+ const centerY = midY + sign * perpY * distToCenter;
2178
+ return { centerX, centerY, radius };
2179
+ }
2180
+ function drawArcFromBulge(ctx, realStartX, realStartY, realEndX, realEndY, bulge, realToCanvasMat) {
2181
+ if (Math.abs(bulge) < 1e-10) {
2182
+ const [endX, endY] = applyToPoint12(realToCanvasMat, [realEndX, realEndY]);
2183
+ ctx.lineTo(endX, endY);
2184
+ return;
2185
+ }
2186
+ const arc = computeArcFromBulge(
2187
+ realStartX,
2188
+ realStartY,
2189
+ realEndX,
2190
+ realEndY,
2191
+ bulge
2192
+ );
2193
+ if (!arc) {
2194
+ const [endX, endY] = applyToPoint12(realToCanvasMat, [realEndX, realEndY]);
2195
+ ctx.lineTo(endX, endY);
2196
+ return;
2197
+ }
2198
+ const [canvasStartX, canvasStartY] = applyToPoint12(realToCanvasMat, [
2199
+ realStartX,
2200
+ realStartY
2201
+ ]);
2202
+ const [canvasEndX, canvasEndY] = applyToPoint12(realToCanvasMat, [
2203
+ realEndX,
2204
+ realEndY
2205
+ ]);
2206
+ const [canvasCenterX, canvasCenterY] = applyToPoint12(realToCanvasMat, [
2207
+ arc.centerX,
2208
+ arc.centerY
2209
+ ]);
2210
+ const canvasRadius = Math.hypot(
2211
+ canvasStartX - canvasCenterX,
2212
+ canvasStartY - canvasCenterY
2213
+ );
2214
+ const startAngle = Math.atan2(
2215
+ canvasStartY - canvasCenterY,
2216
+ canvasStartX - canvasCenterX
2217
+ );
2218
+ const endAngle = Math.atan2(
2219
+ canvasEndY - canvasCenterY,
2220
+ canvasEndX - canvasCenterX
2221
+ );
2222
+ const det = realToCanvasMat.a * realToCanvasMat.d - realToCanvasMat.b * realToCanvasMat.c;
2223
+ const isFlipped = det < 0;
2224
+ const counterclockwise = bulge > 0 ? !isFlipped : isFlipped;
2225
+ ctx.arc(
2226
+ canvasCenterX,
2227
+ canvasCenterY,
2228
+ canvasRadius,
2229
+ startAngle,
2230
+ endAngle,
2231
+ counterclockwise
2232
+ );
2233
+ }
2234
+ function drawRing(ctx, ring, realToCanvasMat) {
2235
+ if (ring.vertices.length < 2) return;
2236
+ if (ring.vertices.length === 2) {
2237
+ const v0 = ring.vertices[0];
2238
+ const v1 = ring.vertices[1];
2239
+ if (v0 && v1 && Math.abs((v0.bulge ?? 0) - 1) < 1e-10 && Math.abs((v1.bulge ?? 0) - 1) < 1e-10) {
2240
+ const [x0, y0] = applyToPoint12(realToCanvasMat, [v0.x, v0.y]);
2241
+ ctx.moveTo(x0, y0);
2242
+ drawArcFromBulge(ctx, v0.x, v0.y, v1.x, v1.y, 1, realToCanvasMat);
2243
+ drawArcFromBulge(ctx, v1.x, v1.y, v0.x, v0.y, 1, realToCanvasMat);
2244
+ return;
2245
+ }
2246
+ }
2247
+ const firstVertex = ring.vertices[0];
2248
+ if (!firstVertex) return;
2249
+ const [firstX, firstY] = applyToPoint12(realToCanvasMat, [
2250
+ firstVertex.x,
2251
+ firstVertex.y
2252
+ ]);
2253
+ ctx.moveTo(firstX, firstY);
2254
+ for (let i = 0; i < ring.vertices.length; i++) {
2255
+ const currentVertex = ring.vertices[i];
2256
+ const nextIndex = (i + 1) % ring.vertices.length;
2257
+ const nextVertex = ring.vertices[nextIndex];
2258
+ if (!currentVertex || !nextVertex) continue;
2259
+ const bulge = currentVertex.bulge ?? 0;
2260
+ if (Math.abs(bulge) < 1e-10) {
2261
+ const [nextX, nextY] = applyToPoint12(realToCanvasMat, [
2262
+ nextVertex.x,
2263
+ nextVertex.y
2264
+ ]);
2265
+ ctx.lineTo(nextX, nextY);
2266
+ } else {
2267
+ drawArcFromBulge(
2268
+ ctx,
2269
+ currentVertex.x,
2270
+ currentVertex.y,
2271
+ nextVertex.x,
2272
+ nextVertex.y,
2273
+ bulge,
2274
+ realToCanvasMat
2275
+ );
2276
+ }
2277
+ }
2278
+ }
2027
2279
  function drawPcbCopperPour(params) {
2028
2280
  const { ctx, pour, realToCanvasMat, colorMap } = params;
2029
2281
  const color = layerToColor3(pour.layer, colorMap);
@@ -2074,6 +2326,20 @@ function drawPcbCopperPour(params) {
2074
2326
  ctx.restore();
2075
2327
  return;
2076
2328
  }
2329
+ if (pour.shape === "brep") {
2330
+ ctx.beginPath();
2331
+ drawRing(ctx, pour.brep_shape.outer_ring, realToCanvasMat);
2332
+ if (pour.brep_shape.inner_rings) {
2333
+ for (const innerRing of pour.brep_shape.inner_rings) {
2334
+ drawRing(ctx, innerRing, realToCanvasMat);
2335
+ }
2336
+ }
2337
+ ctx.fillStyle = color;
2338
+ ctx.globalAlpha = 0.5;
2339
+ ctx.fill("evenodd");
2340
+ ctx.restore();
2341
+ return;
2342
+ }
2077
2343
  ctx.restore();
2078
2344
  }
2079
2345
 
@@ -4,6 +4,7 @@ import { applyToPoint } from "transformation-matrix"
4
4
  import type { PcbColorMap, CanvasContext } from "../types"
5
5
  import { drawRect } from "../shapes/rect"
6
6
  import { drawPolygon } from "../shapes/polygon"
7
+ import type { Ring } from "circuit-json"
7
8
 
8
9
  export interface DrawPcbCopperPourParams {
9
10
  ctx: CanvasContext
@@ -19,6 +20,219 @@ function layerToColor(layer: string, colorMap: PcbColorMap): string {
19
20
  )
20
21
  }
21
22
 
23
+ /**
24
+ * Compute arc center and radius from two points and a bulge value.
25
+ * Bulge is the tangent of 1/4 of the included angle.
26
+ * - bulge = 0: straight line
27
+ * - bulge = 1: semicircle (180 degrees)
28
+ * - bulge > 0: arc bulges to the left of the direction from start to end
29
+ * - bulge < 0: arc bulges to the right
30
+ */
31
+ function computeArcFromBulge(
32
+ startX: number,
33
+ startY: number,
34
+ endX: number,
35
+ endY: number,
36
+ bulge: number,
37
+ ): { centerX: number; centerY: number; radius: number } | null {
38
+ if (Math.abs(bulge) < 1e-10) {
39
+ return null
40
+ }
41
+
42
+ // Calculate chord vector and length
43
+ const chordX = endX - startX
44
+ const chordY = endY - startY
45
+ const chordLength = Math.hypot(chordX, chordY)
46
+
47
+ if (chordLength < 1e-10) {
48
+ return null
49
+ }
50
+
51
+ // The sagitta (bulge height) is: s = bulge * (chord_length / 2)
52
+ // This is because bulge = tan(theta/4) and s = r - r*cos(theta/2)
53
+ // After simplification: s = (chord/2) * tan(theta/4) = (chord/2) * bulge
54
+ const sagitta = Math.abs(bulge) * (chordLength / 2)
55
+
56
+ // Calculate radius from sagitta and chord:
57
+ // From geometry: r = (s^2 + (chord/2)^2) / (2*s)
58
+ const halfChord = chordLength / 2
59
+ const radius = (sagitta * sagitta + halfChord * halfChord) / (2 * sagitta)
60
+
61
+ // Distance from chord midpoint to center
62
+ const distToCenter = radius - sagitta
63
+
64
+ // Midpoint of the chord
65
+ const midX = (startX + endX) / 2
66
+ const midY = (startY + endY) / 2
67
+
68
+ // Unit vector perpendicular to chord
69
+ // "Left" of the direction from start to end (in standard Y-up coords)
70
+ const perpX = -chordY / chordLength
71
+ const perpY = chordX / chordLength
72
+
73
+ // For positive bulge, arc bulges left, so center is to the RIGHT of chord direction
74
+ // For negative bulge, arc bulges right, so center is to the LEFT of chord direction
75
+ const sign = bulge > 0 ? -1 : 1
76
+ const centerX = midX + sign * perpX * distToCenter
77
+ const centerY = midY + sign * perpY * distToCenter
78
+
79
+ return { centerX, centerY, radius }
80
+ }
81
+
82
+ /**
83
+ * Draws an arc between two points given a bulge value.
84
+ * Coordinates are in canvas space, but we need the original real-space points
85
+ * to correctly compute the arc direction.
86
+ */
87
+ function drawArcFromBulge(
88
+ ctx: CanvasContext,
89
+ realStartX: number,
90
+ realStartY: number,
91
+ realEndX: number,
92
+ realEndY: number,
93
+ bulge: number,
94
+ realToCanvasMat: Matrix,
95
+ ): void {
96
+ if (Math.abs(bulge) < 1e-10) {
97
+ const [endX, endY] = applyToPoint(realToCanvasMat, [realEndX, realEndY])
98
+ ctx.lineTo(endX, endY)
99
+ return
100
+ }
101
+
102
+ // Compute arc in real coordinates
103
+ const arc = computeArcFromBulge(
104
+ realStartX,
105
+ realStartY,
106
+ realEndX,
107
+ realEndY,
108
+ bulge,
109
+ )
110
+
111
+ if (!arc) {
112
+ const [endX, endY] = applyToPoint(realToCanvasMat, [realEndX, realEndY])
113
+ ctx.lineTo(endX, endY)
114
+ return
115
+ }
116
+
117
+ // Transform all points to canvas coordinates
118
+ const [canvasStartX, canvasStartY] = applyToPoint(realToCanvasMat, [
119
+ realStartX,
120
+ realStartY,
121
+ ])
122
+ const [canvasEndX, canvasEndY] = applyToPoint(realToCanvasMat, [
123
+ realEndX,
124
+ realEndY,
125
+ ])
126
+ const [canvasCenterX, canvasCenterY] = applyToPoint(realToCanvasMat, [
127
+ arc.centerX,
128
+ arc.centerY,
129
+ ])
130
+
131
+ // Calculate radius in canvas space (may be scaled)
132
+ const canvasRadius = Math.hypot(
133
+ canvasStartX - canvasCenterX,
134
+ canvasStartY - canvasCenterY,
135
+ )
136
+
137
+ // Calculate start and end angles in canvas space
138
+ const startAngle = Math.atan2(
139
+ canvasStartY - canvasCenterY,
140
+ canvasStartX - canvasCenterX,
141
+ )
142
+ const endAngle = Math.atan2(
143
+ canvasEndY - canvasCenterY,
144
+ canvasEndX - canvasCenterX,
145
+ )
146
+
147
+ // Determine arc direction
148
+ // In real coords: positive bulge = counterclockwise (left-hand rule)
149
+ // The transformation may flip Y, which reverses the apparent direction
150
+ // Check if transformation flips by looking at the determinant (a*d - b*c)
151
+ const det =
152
+ realToCanvasMat.a * realToCanvasMat.d -
153
+ realToCanvasMat.b * realToCanvasMat.c
154
+ const isFlipped = det < 0
155
+
156
+ // Positive bulge in real coords = counterclockwise in real coords
157
+ // If flipped, counterclockwise becomes clockwise in canvas coords
158
+ const counterclockwise = bulge > 0 ? !isFlipped : isFlipped
159
+
160
+ ctx.arc(
161
+ canvasCenterX,
162
+ canvasCenterY,
163
+ canvasRadius,
164
+ startAngle,
165
+ endAngle,
166
+ counterclockwise,
167
+ )
168
+ }
169
+
170
+ function drawRing(
171
+ ctx: CanvasContext,
172
+ ring: Ring,
173
+ realToCanvasMat: Matrix,
174
+ ): void {
175
+ if (ring.vertices.length < 2) return
176
+
177
+ // Special case: a circle defined by 2 vertices with bulge=1 each
178
+ if (ring.vertices.length === 2) {
179
+ const v0 = ring.vertices[0]
180
+ const v1 = ring.vertices[1]
181
+ if (
182
+ v0 &&
183
+ v1 &&
184
+ Math.abs((v0.bulge ?? 0) - 1) < 1e-10 &&
185
+ Math.abs((v1.bulge ?? 0) - 1) < 1e-10
186
+ ) {
187
+ // This is a full circle - draw two semicircles
188
+ const [x0, y0] = applyToPoint(realToCanvasMat, [v0.x, v0.y])
189
+
190
+ ctx.moveTo(x0, y0)
191
+ drawArcFromBulge(ctx, v0.x, v0.y, v1.x, v1.y, 1, realToCanvasMat)
192
+ drawArcFromBulge(ctx, v1.x, v1.y, v0.x, v0.y, 1, realToCanvasMat)
193
+ return
194
+ }
195
+ }
196
+
197
+ const firstVertex = ring.vertices[0]
198
+ if (!firstVertex) return
199
+ const [firstX, firstY] = applyToPoint(realToCanvasMat, [
200
+ firstVertex.x,
201
+ firstVertex.y,
202
+ ])
203
+ ctx.moveTo(firstX, firstY)
204
+
205
+ for (let i = 0; i < ring.vertices.length; i++) {
206
+ const currentVertex = ring.vertices[i]
207
+ const nextIndex = (i + 1) % ring.vertices.length
208
+ const nextVertex = ring.vertices[nextIndex]
209
+
210
+ if (!currentVertex || !nextVertex) continue
211
+
212
+ const bulge = currentVertex.bulge ?? 0
213
+
214
+ if (Math.abs(bulge) < 1e-10) {
215
+ // No bulge, draw straight line
216
+ const [nextX, nextY] = applyToPoint(realToCanvasMat, [
217
+ nextVertex.x,
218
+ nextVertex.y,
219
+ ])
220
+ ctx.lineTo(nextX, nextY)
221
+ } else {
222
+ // Draw arc based on bulge value (pass real coordinates)
223
+ drawArcFromBulge(
224
+ ctx,
225
+ currentVertex.x,
226
+ currentVertex.y,
227
+ nextVertex.x,
228
+ nextVertex.y,
229
+ bulge,
230
+ realToCanvasMat,
231
+ )
232
+ }
233
+ }
234
+ }
235
+
22
236
  export function drawPcbCopperPour(params: DrawPcbCopperPourParams): void {
23
237
  const { ctx, pour, realToCanvasMat, colorMap } = params
24
238
 
@@ -45,7 +259,7 @@ export function drawPcbCopperPour(params: DrawPcbCopperPourParams): void {
45
259
  ctx.beginPath()
46
260
  ctx.rect(-scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight)
47
261
  ctx.fillStyle = color
48
- ;(ctx as any).globalAlpha = 0.5
262
+ ctx.globalAlpha = 0.5
49
263
  ctx.fill()
50
264
  ctx.restore()
51
265
  return
@@ -76,12 +290,31 @@ export function drawPcbCopperPour(params: DrawPcbCopperPourParams): void {
76
290
 
77
291
  ctx.closePath()
78
292
  ctx.fillStyle = color
79
- ;(ctx as any).globalAlpha = 0.5
293
+ ctx.globalAlpha = 0.5
80
294
  ctx.fill()
81
295
  }
82
296
  ctx.restore()
83
297
  return
84
298
  }
85
299
 
300
+ if (pour.shape === "brep") {
301
+ ctx.beginPath()
302
+ // Draw outer ring
303
+ drawRing(ctx, pour.brep_shape.outer_ring, realToCanvasMat)
304
+
305
+ // Draw inner rings (holes) - use evenodd fill rule to create holes
306
+ if (pour.brep_shape.inner_rings) {
307
+ for (const innerRing of pour.brep_shape.inner_rings) {
308
+ drawRing(ctx, innerRing, realToCanvasMat)
309
+ }
310
+ }
311
+
312
+ ctx.fillStyle = color
313
+ ctx.globalAlpha = 0.5
314
+ ctx.fill("evenodd")
315
+ ctx.restore()
316
+ return
317
+ }
318
+
86
319
  ctx.restore()
87
320
  }