circuit-to-canvas 0.0.47 → 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 +136 -0
- package/lib/drawer/elements/pcb-copper-pour.ts +235 -2
- 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/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
|
@@ -2154,6 +2154,128 @@ import { applyToPoint as applyToPoint12 } from "transformation-matrix";
|
|
|
2154
2154
|
function layerToColor3(layer, colorMap) {
|
|
2155
2155
|
return colorMap.copper[layer] ?? colorMap.copper.top;
|
|
2156
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
|
+
}
|
|
2157
2279
|
function drawPcbCopperPour(params) {
|
|
2158
2280
|
const { ctx, pour, realToCanvasMat, colorMap } = params;
|
|
2159
2281
|
const color = layerToColor3(pour.layer, colorMap);
|
|
@@ -2204,6 +2326,20 @@ function drawPcbCopperPour(params) {
|
|
|
2204
2326
|
ctx.restore();
|
|
2205
2327
|
return;
|
|
2206
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
|
+
}
|
|
2207
2343
|
ctx.restore();
|
|
2208
2344
|
}
|
|
2209
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
|
}
|
package/lib/drawer/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface CanvasContext {
|
|
|
13
13
|
radius: number,
|
|
14
14
|
startAngle: number,
|
|
15
15
|
endAngle: number,
|
|
16
|
+
counterclockwise?: boolean,
|
|
16
17
|
): void
|
|
17
18
|
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
|
|
18
19
|
ellipse(
|
|
@@ -24,7 +25,7 @@ export interface CanvasContext {
|
|
|
24
25
|
startAngle: number,
|
|
25
26
|
endAngle: number,
|
|
26
27
|
): void
|
|
27
|
-
fill(): void
|
|
28
|
+
fill(fillRule?: "nonzero" | "evenodd"): void
|
|
28
29
|
stroke(): void
|
|
29
30
|
rect(x: number, y: number, w: number, h: number): void
|
|
30
31
|
lineTo(x: number, y: number): void
|
|
@@ -38,6 +39,7 @@ export interface CanvasContext {
|
|
|
38
39
|
globalCompositeOperation?: string
|
|
39
40
|
fillStyle: string | CanvasGradient | CanvasPattern
|
|
40
41
|
strokeStyle: string | CanvasGradient | CanvasPattern
|
|
42
|
+
globalAlpha: number
|
|
41
43
|
lineWidth: number
|
|
42
44
|
lineCap: "butt" | "round" | "square"
|
|
43
45
|
lineJoin: "bevel" | "round" | "miter"
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -119,3 +119,174 @@ test("draw copper pour with trace", async () => {
|
|
|
119
119
|
"copper-pour-with-trace",
|
|
120
120
|
)
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
test("draw brep copper pours", async () => {
|
|
124
|
+
const canvas = createCanvas(200, 200)
|
|
125
|
+
const ctx = canvas.getContext("2d")
|
|
126
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
127
|
+
|
|
128
|
+
// Set camera bounds to fit all elements
|
|
129
|
+
drawer.setCameraBounds({
|
|
130
|
+
minX: -100,
|
|
131
|
+
maxX: 100,
|
|
132
|
+
minY: -50,
|
|
133
|
+
maxY: 50,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
ctx.fillStyle = "#1a1a1a"
|
|
137
|
+
ctx.fillRect(0, 0, 200, 200)
|
|
138
|
+
|
|
139
|
+
const soup: any[] = [
|
|
140
|
+
{
|
|
141
|
+
type: "pcb_board",
|
|
142
|
+
pcb_board_id: "board1",
|
|
143
|
+
center: { x: 0, y: 0 },
|
|
144
|
+
width: 200,
|
|
145
|
+
height: 100,
|
|
146
|
+
material: "fr4",
|
|
147
|
+
num_layers: 2,
|
|
148
|
+
thickness: 1.6,
|
|
149
|
+
},
|
|
150
|
+
// pour_brep_1: square with rounded-square hole
|
|
151
|
+
{
|
|
152
|
+
type: "pcb_copper_pour",
|
|
153
|
+
pcb_copper_pour_id: "pour_brep_1",
|
|
154
|
+
layer: "top",
|
|
155
|
+
shape: "brep",
|
|
156
|
+
source_net_id: "net1",
|
|
157
|
+
brep_shape: {
|
|
158
|
+
outer_ring: {
|
|
159
|
+
vertices: [
|
|
160
|
+
{ x: -30, y: 30 },
|
|
161
|
+
{ x: -50, y: 30 },
|
|
162
|
+
{ x: -50, y: 10 },
|
|
163
|
+
{ x: -30, y: 10 },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
inner_rings: [
|
|
167
|
+
{
|
|
168
|
+
vertices: [
|
|
169
|
+
{ x: -35, y: 25, bulge: 0.5 },
|
|
170
|
+
{ x: -45, y: 25 },
|
|
171
|
+
{ x: -45, y: 15 },
|
|
172
|
+
{ x: -35, y: 15 },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
} as PcbCopperPour,
|
|
178
|
+
// pour_brep_2: Bulgy outer ring, two holes
|
|
179
|
+
{
|
|
180
|
+
type: "pcb_copper_pour",
|
|
181
|
+
pcb_copper_pour_id: "pour_brep_2",
|
|
182
|
+
layer: "top",
|
|
183
|
+
shape: "brep",
|
|
184
|
+
source_net_id: "net2",
|
|
185
|
+
brep_shape: {
|
|
186
|
+
outer_ring: {
|
|
187
|
+
vertices: [
|
|
188
|
+
{ x: 10, y: 30, bulge: -0.5 },
|
|
189
|
+
{ x: -10, y: 30 },
|
|
190
|
+
{ x: -10, y: 10, bulge: 0.5 },
|
|
191
|
+
{ x: 10, y: 10 },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
inner_rings: [
|
|
195
|
+
{
|
|
196
|
+
// square hole
|
|
197
|
+
vertices: [
|
|
198
|
+
{ x: -5, y: 25 },
|
|
199
|
+
{ x: -8, y: 25 },
|
|
200
|
+
{ x: -8, y: 22 },
|
|
201
|
+
{ x: -5, y: 22 },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
// triangular hole
|
|
206
|
+
vertices: [
|
|
207
|
+
{ x: 5, y: 25 },
|
|
208
|
+
{ x: 8, y: 22 },
|
|
209
|
+
{ x: 5, y: 22 },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
} as PcbCopperPour,
|
|
215
|
+
// pour_brep_3: Circular pour with square hole
|
|
216
|
+
{
|
|
217
|
+
type: "pcb_copper_pour",
|
|
218
|
+
pcb_copper_pour_id: "pour_brep_3",
|
|
219
|
+
layer: "top",
|
|
220
|
+
shape: "brep",
|
|
221
|
+
source_net_id: "net3",
|
|
222
|
+
brep_shape: {
|
|
223
|
+
outer_ring: {
|
|
224
|
+
vertices: [
|
|
225
|
+
{ x: 30, y: 20, bulge: 1 },
|
|
226
|
+
{ x: 50, y: 20, bulge: 1 },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
inner_rings: [
|
|
230
|
+
{
|
|
231
|
+
vertices: [
|
|
232
|
+
{ x: 38, y: 22 },
|
|
233
|
+
{ x: 42, y: 22 },
|
|
234
|
+
{ x: 42, y: 18 },
|
|
235
|
+
{ x: 38, y: 18 },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
} as PcbCopperPour,
|
|
241
|
+
// pour_brep_4: bottom layer pour
|
|
242
|
+
{
|
|
243
|
+
type: "pcb_copper_pour",
|
|
244
|
+
pcb_copper_pour_id: "pour_brep_4",
|
|
245
|
+
layer: "bottom",
|
|
246
|
+
shape: "brep",
|
|
247
|
+
source_net_id: "net4",
|
|
248
|
+
brep_shape: {
|
|
249
|
+
outer_ring: {
|
|
250
|
+
vertices: [
|
|
251
|
+
{ x: -30, y: -10 },
|
|
252
|
+
{ x: -50, y: -10 },
|
|
253
|
+
{ x: -50, y: -30 },
|
|
254
|
+
{ x: -30, y: -30, bulge: 0.5 },
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
} as PcbCopperPour,
|
|
259
|
+
// pour_rect_1: A rect pour with rotation
|
|
260
|
+
{
|
|
261
|
+
type: "pcb_copper_pour",
|
|
262
|
+
pcb_copper_pour_id: "pour_rect_1",
|
|
263
|
+
layer: "top",
|
|
264
|
+
shape: "rect",
|
|
265
|
+
source_net_id: "net5",
|
|
266
|
+
center: { x: 0, y: -20 },
|
|
267
|
+
width: 20,
|
|
268
|
+
height: 10,
|
|
269
|
+
rotation: 15,
|
|
270
|
+
} as PcbCopperPour,
|
|
271
|
+
// pour_polygon_1: A polygon pour (triangle)
|
|
272
|
+
{
|
|
273
|
+
type: "pcb_copper_pour",
|
|
274
|
+
pcb_copper_pour_id: "pour_polygon_1",
|
|
275
|
+
layer: "top",
|
|
276
|
+
shape: "polygon",
|
|
277
|
+
source_net_id: "net6",
|
|
278
|
+
points: [
|
|
279
|
+
{ x: 30, y: -10 },
|
|
280
|
+
{ x: 50, y: -30 },
|
|
281
|
+
{ x: 30, y: -30 },
|
|
282
|
+
],
|
|
283
|
+
} as PcbCopperPour,
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
drawer.drawElements(soup)
|
|
287
|
+
|
|
288
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
289
|
+
import.meta.path,
|
|
290
|
+
"brep-copper-pours",
|
|
291
|
+
)
|
|
292
|
+
})
|