@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.
Files changed (44) hide show
  1. package/.github/workflows/.replit +9 -0
  2. package/.github/workflows/bun-formatcheck.yml +26 -0
  3. package/.github/workflows/bun-pver-release.yml +71 -0
  4. package/.github/workflows/bun-test.yml +31 -0
  5. package/.github/workflows/bun-typecheck.yml +26 -0
  6. package/README.md +37 -0
  7. package/biome.json +93 -0
  8. package/bun.lock +164 -0
  9. package/bunfig.toml +5 -0
  10. package/cosmos.config.json +5 -0
  11. package/dist/index.d.ts +57 -0
  12. package/dist/index.js +368 -0
  13. package/lib/circuit-json/convert-circuit-json-to-input-problem.ts +208 -0
  14. package/lib/circuit-json/convertCircuitJsonToInputProblem.ts +0 -0
  15. package/lib/index.ts +3 -0
  16. package/lib/solvers/CopperPourPipelineSolver.ts +51 -0
  17. package/lib/solvers/copper-pour/circle-to-polygon.ts +15 -0
  18. package/lib/solvers/copper-pour/generate-brep.ts +60 -0
  19. package/lib/solvers/copper-pour/get-board-polygon.ts +19 -0
  20. package/lib/solvers/copper-pour/process-obstacles.ts +116 -0
  21. package/lib/types.ts +46 -0
  22. package/package.json +28 -0
  23. package/site/Welcome.page.tsx +0 -0
  24. package/tests/__snapshots__/circuit-1.snap.svg +1 -0
  25. package/tests/__snapshots__/circuit-2.snap.svg +1 -0
  26. package/tests/__snapshots__/circuit-3.snap.svg +1 -0
  27. package/tests/__snapshots__/circuit-4.snap.svg +1 -0
  28. package/tests/__snapshots__/circuit-5.snap.svg +1 -0
  29. package/tests/__snapshots__/circuit-6.snap.svg +1 -0
  30. package/tests/assets/circuit-1.json +592 -0
  31. package/tests/assets/circuit-2.json +1424 -0
  32. package/tests/assets/circuit-3.json +1424 -0
  33. package/tests/assets/circuit-4.json +631 -0
  34. package/tests/assets/circuit-5.json +631 -0
  35. package/tests/assets/circuit-6.json +806 -0
  36. package/tests/circuit-1.test.ts +14 -0
  37. package/tests/circuit-2.test.ts +15 -0
  38. package/tests/circuit-3.test.ts +15 -0
  39. package/tests/circuit-4.test.ts +15 -0
  40. package/tests/circuit-5.test.ts +15 -0
  41. package/tests/circuit-6.test.ts +15 -0
  42. package/tests/fixtures/preload.ts +1 -0
  43. package/tests/utils/run-solver-and-render-to-svg.ts +64 -0
  44. 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
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./solvers/CopperPourPipelineSolver"
2
+ export * from "./circuit-json/convert-circuit-json-to-input-problem"
3
+ export * from "./types"
@@ -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
+ }