@tscircuit/copper-pour-solver 0.0.1
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/.github/workflows/.replit +9 -0
- package/.github/workflows/bun-formatcheck.yml +26 -0
- package/.github/workflows/bun-pver-release.yml +71 -0
- package/.github/workflows/bun-test.yml +31 -0
- package/.github/workflows/bun-typecheck.yml +26 -0
- package/README.md +37 -0
- package/biome.json +93 -0
- package/bun.lock +164 -0
- package/bunfig.toml +5 -0
- package/cosmos.config.json +5 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +368 -0
- package/lib/circuit-json/convert-circuit-json-to-input-problem.ts +208 -0
- package/lib/circuit-json/convertCircuitJsonToInputProblem.ts +0 -0
- package/lib/index.ts +3 -0
- package/lib/solvers/CopperPourPipelineSolver.ts +51 -0
- package/lib/solvers/copper-pour/circle-to-polygon.ts +15 -0
- package/lib/solvers/copper-pour/generate-brep.ts +60 -0
- package/lib/solvers/copper-pour/get-board-polygon.ts +19 -0
- package/lib/solvers/copper-pour/process-obstacles.ts +116 -0
- package/lib/types.ts +46 -0
- package/package.json +28 -0
- package/site/Welcome.page.tsx +0 -0
- package/tests/__snapshots__/circuit-1.snap.svg +1 -0
- package/tests/__snapshots__/circuit-2.snap.svg +1 -0
- package/tests/__snapshots__/circuit-3.snap.svg +1 -0
- package/tests/__snapshots__/circuit-4.snap.svg +1 -0
- package/tests/__snapshots__/circuit-5.snap.svg +1 -0
- package/tests/__snapshots__/circuit-6.snap.svg +1 -0
- package/tests/assets/circuit-1.json +592 -0
- package/tests/assets/circuit-2.json +1424 -0
- package/tests/assets/circuit-3.json +1424 -0
- package/tests/assets/circuit-4.json +631 -0
- package/tests/assets/circuit-5.json +631 -0
- package/tests/assets/circuit-6.json +806 -0
- package/tests/circuit-1.test.ts +14 -0
- package/tests/circuit-2.test.ts +15 -0
- package/tests/circuit-3.test.ts +15 -0
- package/tests/circuit-4.test.ts +15 -0
- package/tests/circuit-5.test.ts +15 -0
- package/tests/circuit-6.test.ts +15 -0
- package/tests/fixtures/preload.ts +1 -0
- package/tests/utils/run-solver-and-render-to-svg.ts +64 -0
- package/tsconfig.json +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// lib/solvers/CopperPourPipelineSolver.ts
|
|
2
|
+
import { BasePipelineSolver } from "@tscircuit/solver-utils";
|
|
3
|
+
|
|
4
|
+
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
5
|
+
import Flatten from "@flatten-js/core";
|
|
6
|
+
var getBoardPolygon = (region) => {
|
|
7
|
+
if (region.outline && region.outline.length > 0) {
|
|
8
|
+
return new Flatten.Polygon(
|
|
9
|
+
region.outline.map((p) => Flatten.point(p.x, p.y))
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
const { bounds } = region;
|
|
13
|
+
return new Flatten.Polygon(
|
|
14
|
+
new Flatten.Box(
|
|
15
|
+
bounds.minX,
|
|
16
|
+
bounds.minY,
|
|
17
|
+
bounds.maxX,
|
|
18
|
+
bounds.maxY
|
|
19
|
+
).toPoints()
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// lib/solvers/CopperPourPipelineSolver.ts
|
|
24
|
+
import Flatten5 from "@flatten-js/core";
|
|
25
|
+
|
|
26
|
+
// lib/solvers/copper-pour/process-obstacles.ts
|
|
27
|
+
import Flatten3 from "@flatten-js/core";
|
|
28
|
+
|
|
29
|
+
// lib/solvers/copper-pour/circle-to-polygon.ts
|
|
30
|
+
import Flatten2 from "@flatten-js/core";
|
|
31
|
+
var circleToPolygon = (circle, numSegments = 32) => {
|
|
32
|
+
const points = [];
|
|
33
|
+
for (let i = 0; i < numSegments; i++) {
|
|
34
|
+
const angle = i / numSegments * 2 * Math.PI;
|
|
35
|
+
points.push(
|
|
36
|
+
new Flatten2.Point(
|
|
37
|
+
circle.center.x + circle.r * Math.cos(angle),
|
|
38
|
+
circle.center.y + circle.r * Math.sin(angle)
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return new Flatten2.Polygon(points);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// lib/solvers/copper-pour/process-obstacles.ts
|
|
46
|
+
var isRectPad = (pad) => pad.shape === "rect";
|
|
47
|
+
var isTracePad = (pad) => pad.shape === "trace";
|
|
48
|
+
var isCircularPad = (pad) => pad.shape === "circle";
|
|
49
|
+
var processObstaclesForPour = (pads, pourConnectivityKey, margins) => {
|
|
50
|
+
const polygonsToSubtract = [];
|
|
51
|
+
const { padMargin, traceMargin } = margins;
|
|
52
|
+
for (const pad of pads) {
|
|
53
|
+
const isOnNet = pad.connectivityKey === pourConnectivityKey;
|
|
54
|
+
if (isOnNet) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isCircularPad(pad)) {
|
|
58
|
+
const isHole = pad.connectivityKey.startsWith("hole:");
|
|
59
|
+
const margin = isHole ? 0 : padMargin;
|
|
60
|
+
const circle = new Flatten3.Circle(
|
|
61
|
+
new Flatten3.Point(pad.x, pad.y),
|
|
62
|
+
pad.radius + margin
|
|
63
|
+
);
|
|
64
|
+
polygonsToSubtract.push(circleToPolygon(circle));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isRectPad(pad)) {
|
|
68
|
+
const { bounds } = pad;
|
|
69
|
+
const b = new Flatten3.Box(
|
|
70
|
+
bounds.minX - padMargin,
|
|
71
|
+
bounds.minY - padMargin,
|
|
72
|
+
bounds.maxX + padMargin,
|
|
73
|
+
bounds.maxY + padMargin
|
|
74
|
+
);
|
|
75
|
+
polygonsToSubtract.push(new Flatten3.Polygon(b.toPoints()));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (isTracePad(pad)) {
|
|
79
|
+
for (const segment of pad.segments) {
|
|
80
|
+
const circle = new Flatten3.Circle(
|
|
81
|
+
new Flatten3.Point(segment.x, segment.y),
|
|
82
|
+
pad.width / 2 + traceMargin
|
|
83
|
+
);
|
|
84
|
+
polygonsToSubtract.push(circleToPolygon(circle));
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < pad.segments.length - 1; i++) {
|
|
87
|
+
const p1 = pad.segments[i];
|
|
88
|
+
const p2 = pad.segments[i + 1];
|
|
89
|
+
if (!p1 || !p2) continue;
|
|
90
|
+
const segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
|
|
91
|
+
if (segmentLength === 0) continue;
|
|
92
|
+
const enlargedWidth = pad.width + traceMargin * 2;
|
|
93
|
+
const centerX = (p1.x + p2.x) / 2;
|
|
94
|
+
const centerY = (p1.y + p2.y) / 2;
|
|
95
|
+
const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
|
|
96
|
+
const w2 = segmentLength / 2;
|
|
97
|
+
const h2 = enlargedWidth / 2;
|
|
98
|
+
const angleRad = rotationDeg * Math.PI / 180;
|
|
99
|
+
const cosAngle = Math.cos(angleRad);
|
|
100
|
+
const sinAngle = Math.sin(angleRad);
|
|
101
|
+
const corners = [
|
|
102
|
+
{ x: -w2, y: -h2 },
|
|
103
|
+
{ x: w2, y: -h2 },
|
|
104
|
+
{ x: w2, y: h2 },
|
|
105
|
+
{ x: -w2, y: h2 }
|
|
106
|
+
];
|
|
107
|
+
const rotatedCorners = corners.map((p) => ({
|
|
108
|
+
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
109
|
+
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
110
|
+
}));
|
|
111
|
+
polygonsToSubtract.push(
|
|
112
|
+
new Flatten3.Polygon(
|
|
113
|
+
rotatedCorners.map((p) => Flatten3.point(p.x, p.y))
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { polygonsToSubtract };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// lib/solvers/copper-pour/generate-brep.ts
|
|
124
|
+
import Flatten4 from "@flatten-js/core";
|
|
125
|
+
var faceToVertices = (face) => face.edges.map((e) => {
|
|
126
|
+
const pt = {
|
|
127
|
+
x: e.start.x,
|
|
128
|
+
y: e.start.y
|
|
129
|
+
};
|
|
130
|
+
if (e.isArc) {
|
|
131
|
+
const bulge = Math.tan(e.shape.sweep / 4);
|
|
132
|
+
if (Math.abs(bulge) > 1e-9) {
|
|
133
|
+
pt.bulge = bulge;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return pt;
|
|
137
|
+
});
|
|
138
|
+
var generateBRep = (pourPolygons) => {
|
|
139
|
+
const brep_shapes = [];
|
|
140
|
+
const polygons = Array.isArray(pourPolygons) ? pourPolygons : [pourPolygons];
|
|
141
|
+
for (const p of polygons) {
|
|
142
|
+
const islands = p.splitToIslands();
|
|
143
|
+
for (const island of islands) {
|
|
144
|
+
if (island.isEmpty()) continue;
|
|
145
|
+
const faces = [...island.faces];
|
|
146
|
+
const outer_face_ccw = faces.find(
|
|
147
|
+
(f) => f.orientation() === Flatten4.ORIENTATION.CCW
|
|
148
|
+
);
|
|
149
|
+
const inner_faces_cw = faces.filter(
|
|
150
|
+
(f) => f.orientation() === Flatten4.ORIENTATION.CW
|
|
151
|
+
);
|
|
152
|
+
if (!outer_face_ccw) continue;
|
|
153
|
+
outer_face_ccw.reverse();
|
|
154
|
+
const outer_ring_vertices = faceToVertices(outer_face_ccw);
|
|
155
|
+
const inner_rings = inner_faces_cw.map((f) => {
|
|
156
|
+
f.reverse();
|
|
157
|
+
return { vertices: faceToVertices(f) };
|
|
158
|
+
});
|
|
159
|
+
brep_shapes.push({
|
|
160
|
+
outer_ring: { vertices: outer_ring_vertices },
|
|
161
|
+
inner_rings
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return brep_shapes;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// lib/solvers/CopperPourPipelineSolver.ts
|
|
169
|
+
var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
170
|
+
constructor(input) {
|
|
171
|
+
super(input);
|
|
172
|
+
this.input = input;
|
|
173
|
+
}
|
|
174
|
+
pipelineDef = [];
|
|
175
|
+
getOutput() {
|
|
176
|
+
const brep_shapes = [];
|
|
177
|
+
for (const region of this.input.regionsForPour) {
|
|
178
|
+
const boardPolygon = getBoardPolygon(region);
|
|
179
|
+
const padsForLayer = this.input.pads.filter(
|
|
180
|
+
(p) => p.layer === region.layer
|
|
181
|
+
);
|
|
182
|
+
const { polygonsToSubtract } = processObstaclesForPour(
|
|
183
|
+
padsForLayer,
|
|
184
|
+
region.connectivityKey,
|
|
185
|
+
{
|
|
186
|
+
padMargin: region.padMargin,
|
|
187
|
+
traceMargin: region.traceMargin
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
let pourPolygons = boardPolygon;
|
|
191
|
+
for (const poly of polygonsToSubtract) {
|
|
192
|
+
pourPolygons = Flatten5.BooleanOperations.subtract(
|
|
193
|
+
pourPolygons,
|
|
194
|
+
poly
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const new_breps = generateBRep(pourPolygons);
|
|
198
|
+
brep_shapes.push(...new_breps);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
brep_shapes
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// lib/circuit-json/convert-circuit-json-to-input-problem.ts
|
|
207
|
+
var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
208
|
+
const source_ports = circuitJson.filter(
|
|
209
|
+
(e) => e.type === "source_port"
|
|
210
|
+
);
|
|
211
|
+
const pcb_ports = circuitJson.filter(
|
|
212
|
+
(e) => e.type === "pcb_port"
|
|
213
|
+
);
|
|
214
|
+
const source_traces = circuitJson.filter(
|
|
215
|
+
(e) => e.type === "source_trace"
|
|
216
|
+
);
|
|
217
|
+
const source_nets = circuitJson.filter(
|
|
218
|
+
(e) => e.type === "source_net"
|
|
219
|
+
);
|
|
220
|
+
const pcb_board = circuitJson.find((e) => e.type === "pcb_board");
|
|
221
|
+
if (!pcb_board) throw new Error("No pcb_board found in circuit json");
|
|
222
|
+
const sourcePortIdToConnectivityKey = Object.fromEntries(
|
|
223
|
+
source_ports.map((sp) => [
|
|
224
|
+
sp.source_port_id,
|
|
225
|
+
sp.subcircuit_connectivity_map_key
|
|
226
|
+
])
|
|
227
|
+
);
|
|
228
|
+
const pcbPortIdToConnectivityKey = Object.fromEntries(
|
|
229
|
+
pcb_ports.map((pp) => [
|
|
230
|
+
pp.pcb_port_id,
|
|
231
|
+
sourcePortIdToConnectivityKey[pp.source_port_id]
|
|
232
|
+
])
|
|
233
|
+
);
|
|
234
|
+
const pcbPlatedHoleIdToConnectivityKey = {};
|
|
235
|
+
for (const pcb_port of pcb_ports) {
|
|
236
|
+
if (pcb_port.pcb_port_id) {
|
|
237
|
+
pcbPlatedHoleIdToConnectivityKey[pcb_port.pcb_port_id] = pcbPortIdToConnectivityKey[pcb_port.pcb_port_id];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const sourceTraceIdToConnectivityKey = Object.fromEntries(
|
|
241
|
+
source_traces.map((st) => [
|
|
242
|
+
st.source_trace_id,
|
|
243
|
+
st.subcircuit_connectivity_map_key
|
|
244
|
+
])
|
|
245
|
+
);
|
|
246
|
+
const sourceNetIdToConnectivityKey = Object.fromEntries(
|
|
247
|
+
source_nets.map((sn) => [
|
|
248
|
+
sn.source_net_id,
|
|
249
|
+
sn.subcircuit_connectivity_map_key
|
|
250
|
+
])
|
|
251
|
+
);
|
|
252
|
+
const idToConnectivityKey = {
|
|
253
|
+
...sourceTraceIdToConnectivityKey,
|
|
254
|
+
...sourceNetIdToConnectivityKey
|
|
255
|
+
};
|
|
256
|
+
const pads = [];
|
|
257
|
+
for (const elm of circuitJson) {
|
|
258
|
+
if (elm.type === "pcb_smtpad") {
|
|
259
|
+
const smtpad = elm;
|
|
260
|
+
if (smtpad.layer !== options.layer) continue;
|
|
261
|
+
let connectivityKey;
|
|
262
|
+
if (smtpad.pcb_port_id) {
|
|
263
|
+
connectivityKey = pcbPortIdToConnectivityKey[smtpad.pcb_port_id];
|
|
264
|
+
}
|
|
265
|
+
if (!connectivityKey) {
|
|
266
|
+
connectivityKey = `unconnected:${smtpad.pcb_smtpad_id}`;
|
|
267
|
+
}
|
|
268
|
+
if (smtpad.shape === "rect") {
|
|
269
|
+
pads.push({
|
|
270
|
+
shape: "rect",
|
|
271
|
+
padId: smtpad.pcb_smtpad_id,
|
|
272
|
+
layer: smtpad.layer,
|
|
273
|
+
connectivityKey,
|
|
274
|
+
bounds: {
|
|
275
|
+
minX: smtpad.x - smtpad.width / 2,
|
|
276
|
+
minY: smtpad.y - smtpad.height / 2,
|
|
277
|
+
maxX: smtpad.x + smtpad.width / 2,
|
|
278
|
+
maxY: smtpad.y + smtpad.height / 2
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
} else if (smtpad.shape === "circle") {
|
|
282
|
+
pads.push({
|
|
283
|
+
shape: "circle",
|
|
284
|
+
padId: smtpad.pcb_smtpad_id,
|
|
285
|
+
layer: smtpad.layer,
|
|
286
|
+
connectivityKey,
|
|
287
|
+
x: smtpad.x,
|
|
288
|
+
y: smtpad.y,
|
|
289
|
+
radius: smtpad.radius
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} else if (elm.type === "pcb_plated_hole") {
|
|
293
|
+
const platedHole = elm;
|
|
294
|
+
if (platedHole.shape !== "circle") continue;
|
|
295
|
+
if (!platedHole.layers.includes(options.layer)) continue;
|
|
296
|
+
let connectivityKey = pcbPlatedHoleIdToConnectivityKey[platedHole.pcb_plated_hole_id];
|
|
297
|
+
if (!connectivityKey) {
|
|
298
|
+
connectivityKey = `unconnected-plated-hole:${platedHole.pcb_plated_hole_id}`;
|
|
299
|
+
}
|
|
300
|
+
pads.push({
|
|
301
|
+
shape: "circle",
|
|
302
|
+
padId: platedHole.pcb_plated_hole_id,
|
|
303
|
+
layer: options.layer,
|
|
304
|
+
connectivityKey,
|
|
305
|
+
x: platedHole.x,
|
|
306
|
+
y: platedHole.y,
|
|
307
|
+
radius: platedHole.outer_diameter / 2
|
|
308
|
+
});
|
|
309
|
+
} else if (elm.type === "pcb_hole") {
|
|
310
|
+
const hole = elm;
|
|
311
|
+
if (hole.hole_shape !== "circle") continue;
|
|
312
|
+
pads.push({
|
|
313
|
+
shape: "circle",
|
|
314
|
+
padId: hole.pcb_hole_id,
|
|
315
|
+
layer: options.layer,
|
|
316
|
+
// holes are through-all
|
|
317
|
+
connectivityKey: `hole:${hole.pcb_hole_id}`,
|
|
318
|
+
x: hole.x,
|
|
319
|
+
y: hole.y,
|
|
320
|
+
radius: hole.hole_diameter / 2
|
|
321
|
+
});
|
|
322
|
+
} else if (elm.type === "pcb_trace") {
|
|
323
|
+
const trace = elm;
|
|
324
|
+
const first_wire = trace.route.find(
|
|
325
|
+
(r) => r.route_type === "wire" && r.layer
|
|
326
|
+
);
|
|
327
|
+
if (!first_wire) continue;
|
|
328
|
+
if (first_wire.route_type === "via") continue;
|
|
329
|
+
if (first_wire.layer !== options.layer) continue;
|
|
330
|
+
if (!trace.source_trace_id) continue;
|
|
331
|
+
const connectivityKey = idToConnectivityKey[trace.source_trace_id];
|
|
332
|
+
if (!connectivityKey) continue;
|
|
333
|
+
pads.push({
|
|
334
|
+
shape: "trace",
|
|
335
|
+
padId: trace.pcb_trace_id,
|
|
336
|
+
layer: first_wire.layer,
|
|
337
|
+
connectivityKey,
|
|
338
|
+
segments: trace.route.map((r) => ({ x: r.x, y: r.y })),
|
|
339
|
+
width: first_wire.width
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const { width, height } = pcb_board;
|
|
344
|
+
const regionsForPour = [
|
|
345
|
+
{
|
|
346
|
+
shape: "rect",
|
|
347
|
+
layer: options.layer,
|
|
348
|
+
bounds: {
|
|
349
|
+
minX: -width / 2,
|
|
350
|
+
minY: -height / 2,
|
|
351
|
+
maxX: width / 2,
|
|
352
|
+
maxY: height / 2
|
|
353
|
+
},
|
|
354
|
+
outline: pcb_board.outline,
|
|
355
|
+
connectivityKey: options.pour_connectivity_key,
|
|
356
|
+
padMargin: options.pad_margin,
|
|
357
|
+
traceMargin: options.trace_margin
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
return {
|
|
361
|
+
pads,
|
|
362
|
+
regionsForPour
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
export {
|
|
366
|
+
CopperPourPipelineSolver,
|
|
367
|
+
convertCircuitJsonToInputProblem
|
|
368
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyCircuitElement,
|
|
3
|
+
LayerRef,
|
|
4
|
+
PcbBoard,
|
|
5
|
+
PcbHole,
|
|
6
|
+
PcbPlatedHole,
|
|
7
|
+
PcbPort,
|
|
8
|
+
PcbSmtPad,
|
|
9
|
+
PcbTrace,
|
|
10
|
+
SourceNet,
|
|
11
|
+
SourcePort,
|
|
12
|
+
SourceTrace,
|
|
13
|
+
} from "circuit-json"
|
|
14
|
+
import type {
|
|
15
|
+
InputCircularPad,
|
|
16
|
+
InputPad,
|
|
17
|
+
InputProblem,
|
|
18
|
+
InputRectPad,
|
|
19
|
+
InputTracePad,
|
|
20
|
+
} from "lib/types"
|
|
21
|
+
|
|
22
|
+
export const convertCircuitJsonToInputProblem = (
|
|
23
|
+
circuitJson: AnyCircuitElement[],
|
|
24
|
+
options: {
|
|
25
|
+
layer: LayerRef
|
|
26
|
+
pour_connectivity_key: string
|
|
27
|
+
pad_margin: number
|
|
28
|
+
trace_margin: number
|
|
29
|
+
},
|
|
30
|
+
): InputProblem => {
|
|
31
|
+
const source_ports = circuitJson.filter(
|
|
32
|
+
(e) => e.type === "source_port",
|
|
33
|
+
) as SourcePort[]
|
|
34
|
+
const pcb_ports = circuitJson.filter(
|
|
35
|
+
(e) => e.type === "pcb_port",
|
|
36
|
+
) as PcbPort[]
|
|
37
|
+
const source_traces = circuitJson.filter(
|
|
38
|
+
(e) => e.type === "source_trace",
|
|
39
|
+
) as SourceTrace[]
|
|
40
|
+
const source_nets = circuitJson.filter(
|
|
41
|
+
(e) => e.type === "source_net",
|
|
42
|
+
) as SourceNet[]
|
|
43
|
+
const pcb_board = circuitJson.find((e) => e.type === "pcb_board") as
|
|
44
|
+
| PcbBoard
|
|
45
|
+
| undefined
|
|
46
|
+
|
|
47
|
+
if (!pcb_board) throw new Error("No pcb_board found in circuit json")
|
|
48
|
+
|
|
49
|
+
const sourcePortIdToConnectivityKey = Object.fromEntries(
|
|
50
|
+
source_ports.map((sp) => [
|
|
51
|
+
sp.source_port_id,
|
|
52
|
+
sp.subcircuit_connectivity_map_key,
|
|
53
|
+
]),
|
|
54
|
+
)
|
|
55
|
+
const pcbPortIdToConnectivityKey: Record<string, string | undefined> =
|
|
56
|
+
Object.fromEntries(
|
|
57
|
+
pcb_ports.map((pp) => [
|
|
58
|
+
pp.pcb_port_id,
|
|
59
|
+
sourcePortIdToConnectivityKey[pp.source_port_id],
|
|
60
|
+
]),
|
|
61
|
+
)
|
|
62
|
+
const pcbPlatedHoleIdToConnectivityKey: Record<string, string | undefined> =
|
|
63
|
+
{}
|
|
64
|
+
for (const pcb_port of pcb_ports) {
|
|
65
|
+
if (pcb_port.pcb_port_id) {
|
|
66
|
+
pcbPlatedHoleIdToConnectivityKey[pcb_port.pcb_port_id] =
|
|
67
|
+
pcbPortIdToConnectivityKey[pcb_port.pcb_port_id]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sourceTraceIdToConnectivityKey = Object.fromEntries(
|
|
72
|
+
source_traces.map((st) => [
|
|
73
|
+
st.source_trace_id,
|
|
74
|
+
st.subcircuit_connectivity_map_key,
|
|
75
|
+
]),
|
|
76
|
+
)
|
|
77
|
+
const sourceNetIdToConnectivityKey = Object.fromEntries(
|
|
78
|
+
source_nets.map((sn) => [
|
|
79
|
+
sn.source_net_id,
|
|
80
|
+
sn.subcircuit_connectivity_map_key,
|
|
81
|
+
]),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const idToConnectivityKey = {
|
|
85
|
+
...sourceTraceIdToConnectivityKey,
|
|
86
|
+
...sourceNetIdToConnectivityKey,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pads: InputPad[] = []
|
|
90
|
+
|
|
91
|
+
for (const elm of circuitJson) {
|
|
92
|
+
if (elm.type === "pcb_smtpad") {
|
|
93
|
+
const smtpad = elm as PcbSmtPad
|
|
94
|
+
if (smtpad.layer !== options.layer) continue
|
|
95
|
+
|
|
96
|
+
let connectivityKey: string | undefined
|
|
97
|
+
if (smtpad.pcb_port_id) {
|
|
98
|
+
connectivityKey = pcbPortIdToConnectivityKey[smtpad.pcb_port_id]
|
|
99
|
+
}
|
|
100
|
+
if (!connectivityKey) {
|
|
101
|
+
connectivityKey = `unconnected:${smtpad.pcb_smtpad_id}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (smtpad.shape === "rect") {
|
|
105
|
+
pads.push({
|
|
106
|
+
shape: "rect",
|
|
107
|
+
padId: smtpad.pcb_smtpad_id,
|
|
108
|
+
layer: smtpad.layer,
|
|
109
|
+
connectivityKey,
|
|
110
|
+
bounds: {
|
|
111
|
+
minX: smtpad.x - smtpad.width! / 2,
|
|
112
|
+
minY: smtpad.y - smtpad.height! / 2,
|
|
113
|
+
maxX: smtpad.x + smtpad.width! / 2,
|
|
114
|
+
maxY: smtpad.y + smtpad.height! / 2,
|
|
115
|
+
},
|
|
116
|
+
} as InputRectPad)
|
|
117
|
+
} else if (smtpad.shape === "circle") {
|
|
118
|
+
pads.push({
|
|
119
|
+
shape: "circle",
|
|
120
|
+
padId: smtpad.pcb_smtpad_id,
|
|
121
|
+
layer: smtpad.layer,
|
|
122
|
+
connectivityKey,
|
|
123
|
+
x: smtpad.x,
|
|
124
|
+
y: smtpad.y,
|
|
125
|
+
radius: smtpad.radius!,
|
|
126
|
+
} as InputCircularPad)
|
|
127
|
+
}
|
|
128
|
+
} else if (elm.type === "pcb_plated_hole") {
|
|
129
|
+
const platedHole = elm as PcbPlatedHole
|
|
130
|
+
if (platedHole.shape !== "circle") continue
|
|
131
|
+
if (!platedHole.layers.includes(options.layer)) continue
|
|
132
|
+
|
|
133
|
+
// TODO better connectivity check
|
|
134
|
+
let connectivityKey =
|
|
135
|
+
pcbPlatedHoleIdToConnectivityKey[platedHole.pcb_plated_hole_id]
|
|
136
|
+
if (!connectivityKey) {
|
|
137
|
+
connectivityKey = `unconnected-plated-hole:${platedHole.pcb_plated_hole_id}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pads.push({
|
|
141
|
+
shape: "circle",
|
|
142
|
+
padId: platedHole.pcb_plated_hole_id,
|
|
143
|
+
layer: options.layer,
|
|
144
|
+
connectivityKey,
|
|
145
|
+
x: platedHole.x,
|
|
146
|
+
y: platedHole.y,
|
|
147
|
+
radius: platedHole.outer_diameter / 2,
|
|
148
|
+
} as InputCircularPad)
|
|
149
|
+
} else if (elm.type === "pcb_hole") {
|
|
150
|
+
const hole = elm as PcbHole
|
|
151
|
+
if (hole.hole_shape !== "circle") continue
|
|
152
|
+
|
|
153
|
+
pads.push({
|
|
154
|
+
shape: "circle",
|
|
155
|
+
padId: hole.pcb_hole_id,
|
|
156
|
+
layer: options.layer, // holes are through-all
|
|
157
|
+
connectivityKey: `hole:${hole.pcb_hole_id}`,
|
|
158
|
+
x: hole.x,
|
|
159
|
+
y: hole.y,
|
|
160
|
+
radius: hole.hole_diameter / 2,
|
|
161
|
+
} as InputCircularPad)
|
|
162
|
+
} else if (elm.type === "pcb_trace") {
|
|
163
|
+
const trace = elm as PcbTrace
|
|
164
|
+
const first_wire = trace.route.find(
|
|
165
|
+
(r: any) => r.route_type === "wire" && r.layer,
|
|
166
|
+
)
|
|
167
|
+
if (!first_wire) continue
|
|
168
|
+
if (first_wire.route_type === "via") continue
|
|
169
|
+
if (first_wire.layer !== options.layer) continue
|
|
170
|
+
|
|
171
|
+
if (!trace.source_trace_id) continue
|
|
172
|
+
const connectivityKey = idToConnectivityKey[trace.source_trace_id]
|
|
173
|
+
if (!connectivityKey) continue
|
|
174
|
+
|
|
175
|
+
pads.push({
|
|
176
|
+
shape: "trace",
|
|
177
|
+
padId: trace.pcb_trace_id,
|
|
178
|
+
layer: first_wire.layer!,
|
|
179
|
+
connectivityKey,
|
|
180
|
+
segments: trace.route.map((r: any) => ({ x: r.x, y: r.y })),
|
|
181
|
+
width: first_wire.width,
|
|
182
|
+
} as InputTracePad)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { width, height } = pcb_board
|
|
187
|
+
const regionsForPour = [
|
|
188
|
+
{
|
|
189
|
+
shape: "rect" as const,
|
|
190
|
+
layer: options.layer,
|
|
191
|
+
bounds: {
|
|
192
|
+
minX: -width / 2,
|
|
193
|
+
minY: -height / 2,
|
|
194
|
+
maxX: width / 2,
|
|
195
|
+
maxY: height / 2,
|
|
196
|
+
},
|
|
197
|
+
outline: pcb_board.outline,
|
|
198
|
+
connectivityKey: options.pour_connectivity_key,
|
|
199
|
+
padMargin: options.pad_margin,
|
|
200
|
+
traceMargin: options.trace_margin,
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
pads,
|
|
206
|
+
regionsForPour,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
File without changes
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BasePipelineSolver } from "@tscircuit/solver-utils"
|
|
2
|
+
import { getBoardPolygon } from "./copper-pour/get-board-polygon"
|
|
3
|
+
import Flatten from "@flatten-js/core"
|
|
4
|
+
import { processObstaclesForPour } from "./copper-pour/process-obstacles"
|
|
5
|
+
import { generateBRep } from "./copper-pour/generate-brep"
|
|
6
|
+
import type { BRepShape } from "circuit-json"
|
|
7
|
+
import type { InputProblem, PipelineOutput } from "lib/types"
|
|
8
|
+
|
|
9
|
+
export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
10
|
+
pipelineDef = []
|
|
11
|
+
constructor(public input: InputProblem) {
|
|
12
|
+
super(input)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override getOutput(): PipelineOutput {
|
|
16
|
+
const brep_shapes: BRepShape[] = []
|
|
17
|
+
|
|
18
|
+
for (const region of this.input.regionsForPour) {
|
|
19
|
+
const boardPolygon = getBoardPolygon(region)
|
|
20
|
+
|
|
21
|
+
const padsForLayer = this.input.pads.filter(
|
|
22
|
+
(p) => p.layer === region.layer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const { polygonsToSubtract } = processObstaclesForPour(
|
|
26
|
+
padsForLayer,
|
|
27
|
+
region.connectivityKey,
|
|
28
|
+
{
|
|
29
|
+
padMargin: region.padMargin,
|
|
30
|
+
traceMargin: region.traceMargin,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
let pourPolygons: Flatten.Polygon = boardPolygon
|
|
35
|
+
|
|
36
|
+
for (const poly of polygonsToSubtract) {
|
|
37
|
+
pourPolygons = Flatten.BooleanOperations.subtract(
|
|
38
|
+
pourPolygons,
|
|
39
|
+
poly,
|
|
40
|
+
) as Flatten.Polygon
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const new_breps = generateBRep(pourPolygons)
|
|
44
|
+
brep_shapes.push(...new_breps)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
brep_shapes,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Flatten from "@flatten-js/core"
|
|
2
|
+
|
|
3
|
+
export const circleToPolygon = (circle: Flatten.Circle, numSegments = 32) => {
|
|
4
|
+
const points: Flatten.Point[] = []
|
|
5
|
+
for (let i = 0; i < numSegments; i++) {
|
|
6
|
+
const angle = (i / numSegments) * 2 * Math.PI
|
|
7
|
+
points.push(
|
|
8
|
+
new Flatten.Point(
|
|
9
|
+
circle.center.x + circle.r * Math.cos(angle),
|
|
10
|
+
circle.center.y + circle.r * Math.sin(angle),
|
|
11
|
+
),
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return new Flatten.Polygon(points)
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Flatten from "@flatten-js/core"
|
|
2
|
+
import type { BRepShape } from "circuit-json"
|
|
3
|
+
|
|
4
|
+
const faceToVertices = (face: Flatten.Face) =>
|
|
5
|
+
face.edges.map((e) => {
|
|
6
|
+
const pt: { x: number; y: number; bulge?: number } = {
|
|
7
|
+
x: e.start.x,
|
|
8
|
+
y: e.start.y,
|
|
9
|
+
}
|
|
10
|
+
if (e.isArc) {
|
|
11
|
+
const bulge = Math.tan((e.shape as Flatten.Arc).sweep / 4)
|
|
12
|
+
if (Math.abs(bulge) > 1e-9) {
|
|
13
|
+
pt.bulge = bulge
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return pt
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const generateBRep = (
|
|
20
|
+
pourPolygons: Flatten.Polygon | Flatten.Polygon[],
|
|
21
|
+
): BRepShape[] => {
|
|
22
|
+
const brep_shapes: BRepShape[] = []
|
|
23
|
+
|
|
24
|
+
const polygons = Array.isArray(pourPolygons) ? pourPolygons : [pourPolygons]
|
|
25
|
+
|
|
26
|
+
for (const p of polygons) {
|
|
27
|
+
const islands = p.splitToIslands()
|
|
28
|
+
|
|
29
|
+
for (const island of islands) {
|
|
30
|
+
if (island.isEmpty()) continue
|
|
31
|
+
|
|
32
|
+
const faces = [...island.faces] as Flatten.Face[]
|
|
33
|
+
const outer_face_ccw = faces.find(
|
|
34
|
+
(f) => f.orientation() === Flatten.ORIENTATION.CCW,
|
|
35
|
+
)
|
|
36
|
+
const inner_faces_cw = faces.filter(
|
|
37
|
+
(f) => f.orientation() === Flatten.ORIENTATION.CW,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (!outer_face_ccw) continue
|
|
41
|
+
|
|
42
|
+
// BRep requires outer ring to be CW and inner rings to be CCW.
|
|
43
|
+
// Flatten-js provides outer face as CCW and inner faces as CW.
|
|
44
|
+
// We need to reverse them.
|
|
45
|
+
outer_face_ccw.reverse()
|
|
46
|
+
const outer_ring_vertices = faceToVertices(outer_face_ccw)
|
|
47
|
+
const inner_rings = inner_faces_cw.map((f) => {
|
|
48
|
+
f.reverse()
|
|
49
|
+
return { vertices: faceToVertices(f) }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
brep_shapes.push({
|
|
53
|
+
outer_ring: { vertices: outer_ring_vertices },
|
|
54
|
+
inner_rings,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return brep_shapes
|
|
60
|
+
}
|