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 +3 -2
- package/dist/index.js +272 -6
- package/lib/drawer/elements/pcb-copper-pour.ts +235 -2
- package/lib/drawer/elements/pcb-plated-hole.ts +40 -5
- package/lib/drawer/elements/pcb-smtpad.ts +29 -4
- package/lib/drawer/elements/soldermask-margin.ts +130 -0
- package/lib/drawer/types.ts +3 -1
- package/package.json +1 -1
- package/tests/elements/__snapshots__/brep-copper-pours.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-comprehensive-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
- package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +1034 -83
- package/tests/elements/pcb-copper-pour.test.ts +171 -0
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|