@tscircuit/copper-pour-solver 0.0.9 → 0.0.11
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 +8 -2
- package/dist/index.js +188 -23
- package/lib/circuit-json/convert-circuit-json-to-input-problem.ts +115 -15
- package/lib/solvers/CopperPourPipelineSolver.ts +1 -0
- package/lib/solvers/copper-pour/process-obstacles.ts +109 -7
- package/lib/types.ts +11 -1
- package/package.json +1 -1
- package/tests/__snapshots__/hole-and-cutouts.snap.svg +1 -0
- package/tests/__snapshots__/via.snap.svg +1 -0
- package/tests/assets/hole-and-cutouts.json +772 -0
- package/tests/assets/via.json +716 -0
- package/tests/hole-and-cutouts.test.ts +16 -0
- package/tests/utils/run-solver-and-render-to-svg.ts +1 -0
- package/tests/via.test.ts +15 -0
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface InputPourRegion {
|
|
|
11
11
|
padMargin: number;
|
|
12
12
|
traceMargin: number;
|
|
13
13
|
board_edge_margin?: number;
|
|
14
|
+
cutout_margin?: number;
|
|
14
15
|
}
|
|
15
16
|
interface BaseInputPad {
|
|
16
17
|
padId: string;
|
|
@@ -32,7 +33,11 @@ interface InputTracePad extends BaseInputPad {
|
|
|
32
33
|
width: number;
|
|
33
34
|
segments: Point[];
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
interface InputPolygonPad extends BaseInputPad {
|
|
37
|
+
shape: "polygon";
|
|
38
|
+
points: Point[];
|
|
39
|
+
}
|
|
40
|
+
type InputPad = InputRectPad | InputCircularPad | InputTracePad | InputPolygonPad;
|
|
36
41
|
interface InputProblem {
|
|
37
42
|
regionsForPour: InputPourRegion[];
|
|
38
43
|
pads: InputPad[];
|
|
@@ -54,6 +59,7 @@ declare const convertCircuitJsonToInputProblem: (circuitJson: AnyCircuitElement[
|
|
|
54
59
|
pad_margin: number;
|
|
55
60
|
trace_margin: number;
|
|
56
61
|
board_edge_margin?: number;
|
|
62
|
+
cutout_margin?: number;
|
|
57
63
|
}) => InputProblem;
|
|
58
64
|
|
|
59
|
-
export { type BaseInputPad, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem };
|
|
65
|
+
export { type BaseInputPad, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPolygonPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem };
|
package/dist/index.js
CHANGED
|
@@ -57,9 +57,10 @@ var circleToPolygon = (circle, numSegments = 32) => {
|
|
|
57
57
|
var isRectPad = (pad) => pad.shape === "rect";
|
|
58
58
|
var isTracePad = (pad) => pad.shape === "trace";
|
|
59
59
|
var isCircularPad = (pad) => pad.shape === "circle";
|
|
60
|
+
var isPolygonPad = (pad) => pad.shape === "polygon";
|
|
60
61
|
var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline) => {
|
|
61
62
|
const polygonsToSubtract = [];
|
|
62
|
-
const { padMargin, traceMargin, board_edge_margin } = margins;
|
|
63
|
+
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins;
|
|
63
64
|
if (boardOutline && boardOutline.length > 0 && board_edge_margin && board_edge_margin > 0) {
|
|
64
65
|
const boardPoly = new Flatten3.Polygon(
|
|
65
66
|
boardOutline.map((p) => Flatten3.point(p.x, p.y))
|
|
@@ -123,9 +124,18 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
123
124
|
if (isOnNet) {
|
|
124
125
|
continue;
|
|
125
126
|
}
|
|
127
|
+
const isHoleOrCutout = pad.connectivityKey.startsWith("hole:") || pad.connectivityKey.startsWith("cutout:");
|
|
126
128
|
if (isCircularPad(pad)) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
+
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
130
|
+
if (isHoleOrCutout) {
|
|
131
|
+
console.log(
|
|
132
|
+
`Applying cutout margin to circular pad/hole/cutout: ${pad.padId}, margin: ${margin}`
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(
|
|
136
|
+
`Applying pad margin to circular pad: ${pad.padId}, margin: ${margin}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
129
139
|
const circle = new Flatten3.Circle(
|
|
130
140
|
new Flatten3.Point(pad.x, pad.y),
|
|
131
141
|
pad.radius + margin
|
|
@@ -134,16 +144,83 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
134
144
|
continue;
|
|
135
145
|
}
|
|
136
146
|
if (isRectPad(pad)) {
|
|
147
|
+
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
148
|
+
if (isHoleOrCutout) {
|
|
149
|
+
console.log(
|
|
150
|
+
`Applying cutout margin to rect pad/cutout: ${pad.padId}, margin: ${margin}`
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(
|
|
154
|
+
`Applying pad margin to rect pad: ${pad.padId}, margin: ${margin}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
137
157
|
const { bounds } = pad;
|
|
138
158
|
const b = new Flatten3.Box(
|
|
139
|
-
bounds.minX -
|
|
140
|
-
bounds.minY -
|
|
141
|
-
bounds.maxX +
|
|
142
|
-
bounds.maxY +
|
|
159
|
+
bounds.minX - margin,
|
|
160
|
+
bounds.minY - margin,
|
|
161
|
+
bounds.maxX + margin,
|
|
162
|
+
bounds.maxY + margin
|
|
143
163
|
);
|
|
144
164
|
polygonsToSubtract.push(new Flatten3.Polygon(b.toPoints()));
|
|
145
165
|
continue;
|
|
146
166
|
}
|
|
167
|
+
if (isPolygonPad(pad)) {
|
|
168
|
+
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : 0;
|
|
169
|
+
if (isHoleOrCutout) {
|
|
170
|
+
console.log(
|
|
171
|
+
`Applying cutout margin to polygon cutout: ${pad.padId}, margin: ${margin}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
175
|
+
const uniquePoints = pad.points.filter((p) => {
|
|
176
|
+
const key = `${p.x},${p.y}`;
|
|
177
|
+
if (seen.has(key)) {
|
|
178
|
+
console.log(
|
|
179
|
+
`Duplicate point detected and removed for ${pad.padId}: (${p.x}, ${p.y})`
|
|
180
|
+
);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
seen.add(key);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
if (uniquePoints.length < 3) continue;
|
|
187
|
+
const polygon = new Flatten3.Polygon(
|
|
188
|
+
uniquePoints.map((p) => Flatten3.point(p.x, p.y))
|
|
189
|
+
);
|
|
190
|
+
if (Math.abs(polygon.area()) < 1e-9) continue;
|
|
191
|
+
if (margin <= 0) {
|
|
192
|
+
polygonsToSubtract.push(polygon);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (polygon.area() > 0) {
|
|
196
|
+
polygon.reverse();
|
|
197
|
+
}
|
|
198
|
+
const offsetLines = [];
|
|
199
|
+
const polygonVertices = polygon.vertices;
|
|
200
|
+
for (let i = 0; i < polygonVertices.length; i++) {
|
|
201
|
+
const p1 = polygonVertices[i];
|
|
202
|
+
const p2 = polygonVertices[(i + 1) % polygonVertices.length];
|
|
203
|
+
const segment = Flatten3.segment(p1, p2);
|
|
204
|
+
if (segment.length === 0) continue;
|
|
205
|
+
const line = Flatten3.line(segment.start, segment.end);
|
|
206
|
+
const norm = line.norm;
|
|
207
|
+
const offsetLine = line.translate(norm.multiply(-margin));
|
|
208
|
+
offsetLines.push(offsetLine);
|
|
209
|
+
}
|
|
210
|
+
const newPolygonPoints = [];
|
|
211
|
+
for (let i = 0; i < offsetLines.length; i++) {
|
|
212
|
+
const line1 = offsetLines[i];
|
|
213
|
+
const line2 = offsetLines[(i + 1) % offsetLines.length];
|
|
214
|
+
const ip = line1.intersect(line2);
|
|
215
|
+
if (ip.length > 0) {
|
|
216
|
+
newPolygonPoints.push(ip[0]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (newPolygonPoints.length >= 3) {
|
|
220
|
+
polygonsToSubtract.push(new Flatten3.Polygon(newPolygonPoints));
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
147
224
|
if (isTracePad(pad)) {
|
|
148
225
|
for (const segment of pad.segments) {
|
|
149
226
|
const circle = new Flatten3.Circle(
|
|
@@ -254,7 +331,8 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
254
331
|
{
|
|
255
332
|
padMargin: region.padMargin,
|
|
256
333
|
traceMargin: region.traceMargin,
|
|
257
|
-
board_edge_margin: region.board_edge_margin
|
|
334
|
+
board_edge_margin: region.board_edge_margin,
|
|
335
|
+
cutoutMargin: region.cutout_margin
|
|
258
336
|
},
|
|
259
337
|
region.outline
|
|
260
338
|
);
|
|
@@ -294,6 +372,9 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
294
372
|
const source_traces = circuitJson.filter(
|
|
295
373
|
(e) => e.type === "source_trace"
|
|
296
374
|
);
|
|
375
|
+
const pcb_traces = circuitJson.filter(
|
|
376
|
+
(e) => e.type === "pcb_trace"
|
|
377
|
+
);
|
|
297
378
|
const source_nets = circuitJson.filter(
|
|
298
379
|
(e) => e.type === "source_net"
|
|
299
380
|
);
|
|
@@ -333,6 +414,14 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
333
414
|
...sourceTraceIdToConnectivityKey,
|
|
334
415
|
...sourceNetIdToConnectivityKey
|
|
335
416
|
};
|
|
417
|
+
const pcbTraceIdToConnectivityKey = Object.fromEntries(
|
|
418
|
+
pcb_traces.map(
|
|
419
|
+
(trace) => [
|
|
420
|
+
trace.pcb_trace_id,
|
|
421
|
+
trace.source_trace_id ? idToConnectivityKey[trace.source_trace_id] : void 0
|
|
422
|
+
]
|
|
423
|
+
).filter((entry) => Boolean(entry[1]))
|
|
424
|
+
);
|
|
336
425
|
const pads = [];
|
|
337
426
|
for (const elm of circuitJson) {
|
|
338
427
|
if (elm.type === "pcb_smtpad") {
|
|
@@ -399,25 +488,100 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
399
488
|
y: hole.y,
|
|
400
489
|
radius: hole.hole_diameter / 2
|
|
401
490
|
});
|
|
491
|
+
} else if (elm.type === "pcb_cutout") {
|
|
492
|
+
const cutout = elm;
|
|
493
|
+
console.log(
|
|
494
|
+
`Processing cutout: ${cutout.pcb_cutout_id} (shape: ${cutout.shape})`
|
|
495
|
+
);
|
|
496
|
+
if (cutout.shape === "rect") {
|
|
497
|
+
pads.push({
|
|
498
|
+
shape: "rect",
|
|
499
|
+
padId: cutout.pcb_cutout_id,
|
|
500
|
+
layer: options.layer,
|
|
501
|
+
// through-all
|
|
502
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
503
|
+
bounds: {
|
|
504
|
+
minX: cutout.center.x - cutout.width / 2,
|
|
505
|
+
minY: cutout.center.y - cutout.height / 2,
|
|
506
|
+
maxX: cutout.center.x + cutout.width / 2,
|
|
507
|
+
maxY: cutout.center.y + cutout.height / 2
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
} else if (cutout.shape === "circle") {
|
|
511
|
+
pads.push({
|
|
512
|
+
shape: "circle",
|
|
513
|
+
padId: cutout.pcb_cutout_id,
|
|
514
|
+
layer: options.layer,
|
|
515
|
+
// through-all
|
|
516
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
517
|
+
x: cutout.center.x,
|
|
518
|
+
y: cutout.center.y,
|
|
519
|
+
radius: cutout.radius
|
|
520
|
+
});
|
|
521
|
+
} else if (cutout.shape === "polygon") {
|
|
522
|
+
console.log(
|
|
523
|
+
`Polygon cutout points for ${cutout.pcb_cutout_id}: ${JSON.stringify(cutout.points)}`
|
|
524
|
+
);
|
|
525
|
+
pads.push({
|
|
526
|
+
shape: "polygon",
|
|
527
|
+
padId: cutout.pcb_cutout_id,
|
|
528
|
+
layer: options.layer,
|
|
529
|
+
// through-all
|
|
530
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
531
|
+
points: cutout.points
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
} else if (elm.type === "pcb_via") {
|
|
535
|
+
const via = elm;
|
|
536
|
+
if (!via.layers.includes(options.layer)) continue;
|
|
537
|
+
let connectivityKey;
|
|
538
|
+
if (via.pcb_trace_id) {
|
|
539
|
+
connectivityKey = pcbTraceIdToConnectivityKey[via.pcb_trace_id];
|
|
540
|
+
}
|
|
541
|
+
if (!connectivityKey) {
|
|
542
|
+
connectivityKey = `unconnected-via:${via.pcb_via_id}`;
|
|
543
|
+
}
|
|
544
|
+
pads.push({
|
|
545
|
+
shape: "circle",
|
|
546
|
+
padId: via.pcb_via_id,
|
|
547
|
+
layer: options.layer,
|
|
548
|
+
connectivityKey,
|
|
549
|
+
x: via.x,
|
|
550
|
+
y: via.y,
|
|
551
|
+
radius: via.outer_diameter / 2
|
|
552
|
+
});
|
|
402
553
|
} else if (elm.type === "pcb_trace") {
|
|
403
554
|
const trace = elm;
|
|
404
|
-
const first_wire = trace.route.find(
|
|
405
|
-
(r) => r.route_type === "wire" && r.layer
|
|
406
|
-
);
|
|
407
|
-
if (!first_wire) continue;
|
|
408
|
-
if (first_wire.route_type === "via") continue;
|
|
409
|
-
if (first_wire.layer !== options.layer) continue;
|
|
410
555
|
if (!trace.source_trace_id) continue;
|
|
411
556
|
const connectivityKey = idToConnectivityKey[trace.source_trace_id];
|
|
412
557
|
if (!connectivityKey) continue;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
558
|
+
let currentSegmentGroup = [];
|
|
559
|
+
let currentWidth = null;
|
|
560
|
+
const commitGroup = () => {
|
|
561
|
+
if (currentSegmentGroup.length > 1) {
|
|
562
|
+
pads.push({
|
|
563
|
+
shape: "trace",
|
|
564
|
+
padId: `${trace.pcb_trace_id}-${pads.length}`,
|
|
565
|
+
layer: options.layer,
|
|
566
|
+
connectivityKey,
|
|
567
|
+
segments: currentSegmentGroup,
|
|
568
|
+
width: currentWidth
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
currentSegmentGroup = [];
|
|
572
|
+
currentWidth = null;
|
|
573
|
+
};
|
|
574
|
+
for (const r of trace.route) {
|
|
575
|
+
const ri = r;
|
|
576
|
+
const isWireOnLayer = ri.route_type === "wire" && ri.layer === options.layer;
|
|
577
|
+
if (isWireOnLayer) {
|
|
578
|
+
if (currentWidth === null) currentWidth = ri.width;
|
|
579
|
+
currentSegmentGroup.push({ x: ri.x, y: ri.y });
|
|
580
|
+
} else {
|
|
581
|
+
commitGroup();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
commitGroup();
|
|
421
585
|
}
|
|
422
586
|
}
|
|
423
587
|
const { width, height } = pcb_board;
|
|
@@ -435,7 +599,8 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
435
599
|
connectivityKey: options.pour_connectivity_key,
|
|
436
600
|
padMargin: options.pad_margin,
|
|
437
601
|
traceMargin: options.trace_margin,
|
|
438
|
-
board_edge_margin: options.board_edge_margin ?? 0
|
|
602
|
+
board_edge_margin: options.board_edge_margin ?? 0,
|
|
603
|
+
cutout_margin: options.cutout_margin
|
|
439
604
|
}
|
|
440
605
|
];
|
|
441
606
|
return {
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
PcbPort,
|
|
8
8
|
PcbSmtPad,
|
|
9
9
|
PcbTrace,
|
|
10
|
+
PcbVia,
|
|
11
|
+
Point,
|
|
10
12
|
SourceNet,
|
|
11
13
|
SourcePort,
|
|
12
14
|
SourceTrace,
|
|
@@ -14,6 +16,7 @@ import type {
|
|
|
14
16
|
import type {
|
|
15
17
|
InputCircularPad,
|
|
16
18
|
InputPad,
|
|
19
|
+
InputPolygonPad,
|
|
17
20
|
InputProblem,
|
|
18
21
|
InputRectPad,
|
|
19
22
|
InputTracePad,
|
|
@@ -27,6 +30,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
27
30
|
pad_margin: number
|
|
28
31
|
trace_margin: number
|
|
29
32
|
board_edge_margin?: number
|
|
33
|
+
cutout_margin?: number
|
|
30
34
|
},
|
|
31
35
|
): InputProblem => {
|
|
32
36
|
const source_ports = circuitJson.filter(
|
|
@@ -38,6 +42,9 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
38
42
|
const source_traces = circuitJson.filter(
|
|
39
43
|
(e) => e.type === "source_trace",
|
|
40
44
|
) as SourceTrace[]
|
|
45
|
+
const pcb_traces = circuitJson.filter(
|
|
46
|
+
(e) => e.type === "pcb_trace",
|
|
47
|
+
) as PcbTrace[]
|
|
41
48
|
const source_nets = circuitJson.filter(
|
|
42
49
|
(e) => e.type === "source_net",
|
|
43
50
|
) as SourceNet[]
|
|
@@ -87,6 +94,21 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
87
94
|
...sourceNetIdToConnectivityKey,
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
const pcbTraceIdToConnectivityKey: Record<string, string> =
|
|
98
|
+
Object.fromEntries(
|
|
99
|
+
pcb_traces
|
|
100
|
+
.map(
|
|
101
|
+
(trace) =>
|
|
102
|
+
[
|
|
103
|
+
trace.pcb_trace_id,
|
|
104
|
+
trace.source_trace_id
|
|
105
|
+
? idToConnectivityKey[trace.source_trace_id]
|
|
106
|
+
: undefined,
|
|
107
|
+
] as const,
|
|
108
|
+
)
|
|
109
|
+
.filter((entry): entry is [string, string] => Boolean(entry[1])),
|
|
110
|
+
)
|
|
111
|
+
|
|
90
112
|
const pads: InputPad[] = []
|
|
91
113
|
|
|
92
114
|
for (const elm of circuitJson) {
|
|
@@ -160,27 +182,104 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
160
182
|
y: hole.y,
|
|
161
183
|
radius: hole.hole_diameter / 2,
|
|
162
184
|
} as InputCircularPad)
|
|
163
|
-
} else if (elm.type === "
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
} else if (elm.type === "pcb_cutout") {
|
|
186
|
+
const cutout = elm as any
|
|
187
|
+
console.log(
|
|
188
|
+
`Processing cutout: ${cutout.pcb_cutout_id} (shape: ${cutout.shape})`,
|
|
167
189
|
)
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
if (cutout.shape === "rect") {
|
|
191
|
+
pads.push({
|
|
192
|
+
shape: "rect",
|
|
193
|
+
padId: cutout.pcb_cutout_id,
|
|
194
|
+
layer: options.layer, // through-all
|
|
195
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
196
|
+
bounds: {
|
|
197
|
+
minX: cutout.center.x - cutout.width / 2,
|
|
198
|
+
minY: cutout.center.y - cutout.height / 2,
|
|
199
|
+
maxX: cutout.center.x + cutout.width / 2,
|
|
200
|
+
maxY: cutout.center.y + cutout.height / 2,
|
|
201
|
+
},
|
|
202
|
+
} as InputRectPad)
|
|
203
|
+
} else if (cutout.shape === "circle") {
|
|
204
|
+
pads.push({
|
|
205
|
+
shape: "circle",
|
|
206
|
+
padId: cutout.pcb_cutout_id,
|
|
207
|
+
layer: options.layer, // through-all
|
|
208
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
209
|
+
x: cutout.center.x,
|
|
210
|
+
y: cutout.center.y,
|
|
211
|
+
radius: cutout.radius,
|
|
212
|
+
} as InputCircularPad)
|
|
213
|
+
} else if (cutout.shape === "polygon") {
|
|
214
|
+
console.log(
|
|
215
|
+
`Polygon cutout points for ${cutout.pcb_cutout_id}: ${JSON.stringify(cutout.points)}`,
|
|
216
|
+
)
|
|
217
|
+
pads.push({
|
|
218
|
+
shape: "polygon",
|
|
219
|
+
padId: cutout.pcb_cutout_id,
|
|
220
|
+
layer: options.layer, // through-all
|
|
221
|
+
connectivityKey: `cutout:${cutout.pcb_cutout_id}`,
|
|
222
|
+
points: cutout.points,
|
|
223
|
+
} as InputPolygonPad)
|
|
224
|
+
}
|
|
225
|
+
} else if (elm.type === "pcb_via") {
|
|
226
|
+
const via = elm as PcbVia
|
|
227
|
+
if (!via.layers.includes(options.layer)) continue
|
|
228
|
+
|
|
229
|
+
let connectivityKey: string | undefined
|
|
230
|
+
if (via.pcb_trace_id) {
|
|
231
|
+
connectivityKey = pcbTraceIdToConnectivityKey[via.pcb_trace_id]
|
|
232
|
+
}
|
|
171
233
|
|
|
234
|
+
if (!connectivityKey) {
|
|
235
|
+
connectivityKey = `unconnected-via:${via.pcb_via_id}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
pads.push({
|
|
239
|
+
shape: "circle",
|
|
240
|
+
padId: via.pcb_via_id,
|
|
241
|
+
layer: options.layer,
|
|
242
|
+
connectivityKey,
|
|
243
|
+
x: via.x,
|
|
244
|
+
y: via.y,
|
|
245
|
+
radius: via.outer_diameter / 2,
|
|
246
|
+
} as InputCircularPad)
|
|
247
|
+
} else if (elm.type === "pcb_trace") {
|
|
248
|
+
const trace = elm as PcbTrace
|
|
172
249
|
if (!trace.source_trace_id) continue
|
|
173
250
|
const connectivityKey = idToConnectivityKey[trace.source_trace_id]
|
|
174
251
|
if (!connectivityKey) continue
|
|
175
252
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
253
|
+
let currentSegmentGroup: Point[] = []
|
|
254
|
+
let currentWidth: number | null = null
|
|
255
|
+
|
|
256
|
+
const commitGroup = () => {
|
|
257
|
+
if (currentSegmentGroup.length > 1) {
|
|
258
|
+
pads.push({
|
|
259
|
+
shape: "trace",
|
|
260
|
+
padId: `${trace.pcb_trace_id}-${pads.length}`,
|
|
261
|
+
layer: options.layer,
|
|
262
|
+
connectivityKey,
|
|
263
|
+
segments: currentSegmentGroup,
|
|
264
|
+
width: currentWidth!,
|
|
265
|
+
} as InputTracePad)
|
|
266
|
+
}
|
|
267
|
+
currentSegmentGroup = []
|
|
268
|
+
currentWidth = null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const r of trace.route) {
|
|
272
|
+
const ri = r as any
|
|
273
|
+
const isWireOnLayer =
|
|
274
|
+
ri.route_type === "wire" && ri.layer === options.layer
|
|
275
|
+
if (isWireOnLayer) {
|
|
276
|
+
if (currentWidth === null) currentWidth = ri.width
|
|
277
|
+
currentSegmentGroup.push({ x: ri.x, y: ri.y })
|
|
278
|
+
} else {
|
|
279
|
+
commitGroup()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
commitGroup()
|
|
184
283
|
}
|
|
185
284
|
}
|
|
186
285
|
|
|
@@ -200,6 +299,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
200
299
|
padMargin: options.pad_margin,
|
|
201
300
|
traceMargin: options.trace_margin,
|
|
202
301
|
board_edge_margin: options.board_edge_margin ?? 0,
|
|
302
|
+
cutout_margin: options.cutout_margin,
|
|
203
303
|
},
|
|
204
304
|
]
|
|
205
305
|
|
|
@@ -29,6 +29,7 @@ export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
|
29
29
|
padMargin: region.padMargin,
|
|
30
30
|
traceMargin: region.traceMargin,
|
|
31
31
|
board_edge_margin: region.board_edge_margin,
|
|
32
|
+
cutoutMargin: region.cutout_margin,
|
|
32
33
|
},
|
|
33
34
|
region.outline,
|
|
34
35
|
)
|
|
@@ -3,6 +3,7 @@ import type { Point } from "@tscircuit/math-utils"
|
|
|
3
3
|
import type {
|
|
4
4
|
InputCircularPad,
|
|
5
5
|
InputPad,
|
|
6
|
+
InputPolygonPad,
|
|
6
7
|
InputRectPad,
|
|
7
8
|
InputTracePad,
|
|
8
9
|
} from "lib/types"
|
|
@@ -17,6 +18,8 @@ const isTracePad = (pad: InputPad): pad is InputTracePad =>
|
|
|
17
18
|
pad.shape === "trace"
|
|
18
19
|
const isCircularPad = (pad: InputPad): pad is InputCircularPad =>
|
|
19
20
|
pad.shape === "circle"
|
|
21
|
+
const isPolygonPad = (pad: InputPad): pad is InputPolygonPad =>
|
|
22
|
+
pad.shape === "polygon"
|
|
20
23
|
|
|
21
24
|
export const processObstaclesForPour = (
|
|
22
25
|
pads: InputPad[],
|
|
@@ -25,12 +28,13 @@ export const processObstaclesForPour = (
|
|
|
25
28
|
padMargin: number
|
|
26
29
|
traceMargin: number
|
|
27
30
|
board_edge_margin?: number
|
|
31
|
+
cutoutMargin?: number
|
|
28
32
|
},
|
|
29
33
|
boardOutline?: Point[],
|
|
30
34
|
): ProcessedObstacles => {
|
|
31
35
|
const polygonsToSubtract: Flatten.Polygon[] = []
|
|
32
36
|
|
|
33
|
-
const { padMargin, traceMargin, board_edge_margin } = margins
|
|
37
|
+
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins
|
|
34
38
|
|
|
35
39
|
if (
|
|
36
40
|
boardOutline &&
|
|
@@ -120,9 +124,21 @@ export const processObstaclesForPour = (
|
|
|
120
124
|
continue
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
const isHoleOrCutout =
|
|
128
|
+
pad.connectivityKey.startsWith("hole:") ||
|
|
129
|
+
pad.connectivityKey.startsWith("cutout:")
|
|
130
|
+
|
|
123
131
|
if (isCircularPad(pad)) {
|
|
124
|
-
const
|
|
125
|
-
|
|
132
|
+
const margin = isHoleOrCutout ? (cutoutMargin ?? 0) : padMargin
|
|
133
|
+
if (isHoleOrCutout) {
|
|
134
|
+
console.log(
|
|
135
|
+
`Applying cutout margin to circular pad/hole/cutout: ${pad.padId}, margin: ${margin}`,
|
|
136
|
+
)
|
|
137
|
+
} else {
|
|
138
|
+
console.log(
|
|
139
|
+
`Applying pad margin to circular pad: ${pad.padId}, margin: ${margin}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
126
142
|
const circle = new Flatten.Circle(
|
|
127
143
|
new Flatten.Point(pad.x, pad.y),
|
|
128
144
|
pad.radius + margin,
|
|
@@ -132,17 +148,103 @@ export const processObstaclesForPour = (
|
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
if (isRectPad(pad)) {
|
|
151
|
+
const margin = isHoleOrCutout ? (cutoutMargin ?? 0) : padMargin
|
|
152
|
+
if (isHoleOrCutout) {
|
|
153
|
+
console.log(
|
|
154
|
+
`Applying cutout margin to rect pad/cutout: ${pad.padId}, margin: ${margin}`,
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
`Applying pad margin to rect pad: ${pad.padId}, margin: ${margin}`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
135
161
|
const { bounds } = pad
|
|
136
162
|
const b = new Flatten.Box(
|
|
137
|
-
bounds.minX -
|
|
138
|
-
bounds.minY -
|
|
139
|
-
bounds.maxX +
|
|
140
|
-
bounds.maxY +
|
|
163
|
+
bounds.minX - margin,
|
|
164
|
+
bounds.minY - margin,
|
|
165
|
+
bounds.maxX + margin,
|
|
166
|
+
bounds.maxY + margin,
|
|
141
167
|
)
|
|
142
168
|
polygonsToSubtract.push(new Flatten.Polygon(b.toPoints()))
|
|
143
169
|
continue
|
|
144
170
|
}
|
|
145
171
|
|
|
172
|
+
if (isPolygonPad(pad)) {
|
|
173
|
+
const margin = isHoleOrCutout ? (cutoutMargin ?? 0) : 0
|
|
174
|
+
if (isHoleOrCutout) {
|
|
175
|
+
console.log(
|
|
176
|
+
`Applying cutout margin to polygon cutout: ${pad.padId}, margin: ${margin}`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const seen = new Set<string>()
|
|
181
|
+
const uniquePoints = pad.points.filter((p) => {
|
|
182
|
+
const key = `${p.x},${p.y}`
|
|
183
|
+
if (seen.has(key)) {
|
|
184
|
+
console.log(
|
|
185
|
+
`Duplicate point detected and removed for ${pad.padId}: (${p.x}, ${p.y})`,
|
|
186
|
+
)
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
seen.add(key)
|
|
190
|
+
return true
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if (uniquePoints.length < 3) continue
|
|
194
|
+
|
|
195
|
+
const polygon = new Flatten.Polygon(
|
|
196
|
+
uniquePoints.map((p) => Flatten.point(p.x, p.y)),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if (Math.abs(polygon.area()) < 1e-9) continue
|
|
200
|
+
|
|
201
|
+
if (margin <= 0) {
|
|
202
|
+
polygonsToSubtract.push(polygon)
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Ensure polygon is CCW for consistent normal direction.
|
|
207
|
+
// In flatten-js, CCW corresponds to a negative area.
|
|
208
|
+
if (polygon.area() > 0) {
|
|
209
|
+
polygon.reverse()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const offsetLines: Flatten.Line[] = []
|
|
213
|
+
const polygonVertices = polygon.vertices
|
|
214
|
+
for (let i = 0; i < polygonVertices.length; i++) {
|
|
215
|
+
const p1 = polygonVertices[i]!
|
|
216
|
+
const p2 = polygonVertices[(i + 1) % polygonVertices.length]!
|
|
217
|
+
|
|
218
|
+
const segment = Flatten.segment(p1, p2)
|
|
219
|
+
|
|
220
|
+
if (segment.length === 0) continue
|
|
221
|
+
|
|
222
|
+
const line = Flatten.line(segment.start, segment.end)
|
|
223
|
+
|
|
224
|
+
// For a CCW polygon, the normal (rotated +90deg, i.e. "left") points inward.
|
|
225
|
+
// We must translate outward, so we use a negative margin.
|
|
226
|
+
const norm = line.norm
|
|
227
|
+
const offsetLine = line.translate(norm.multiply(-margin))
|
|
228
|
+
offsetLines.push(offsetLine)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const newPolygonPoints: Flatten.Point[] = []
|
|
232
|
+
for (let i = 0; i < offsetLines.length; i++) {
|
|
233
|
+
const line1 = offsetLines[i]!
|
|
234
|
+
const line2 = offsetLines[(i + 1) % offsetLines.length]!
|
|
235
|
+
|
|
236
|
+
const ip = line1.intersect(line2)
|
|
237
|
+
if (ip.length > 0) {
|
|
238
|
+
newPolygonPoints.push(ip[0]!)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (newPolygonPoints.length >= 3) {
|
|
243
|
+
polygonsToSubtract.push(new Flatten.Polygon(newPolygonPoints))
|
|
244
|
+
}
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
146
248
|
if (isTracePad(pad)) {
|
|
147
249
|
// Add circles for each vertex
|
|
148
250
|
for (const segment of pad.segments) {
|
package/lib/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface InputPourRegion {
|
|
|
10
10
|
padMargin: number
|
|
11
11
|
traceMargin: number
|
|
12
12
|
board_edge_margin?: number
|
|
13
|
+
cutout_margin?: number
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface BaseInputPad {
|
|
@@ -36,7 +37,16 @@ export interface InputTracePad extends BaseInputPad {
|
|
|
36
37
|
segments: Point[]
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export
|
|
40
|
+
export interface InputPolygonPad extends BaseInputPad {
|
|
41
|
+
shape: "polygon"
|
|
42
|
+
points: Point[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type InputPad =
|
|
46
|
+
| InputRectPad
|
|
47
|
+
| InputCircularPad
|
|
48
|
+
| InputTracePad
|
|
49
|
+
| InputPolygonPad
|
|
40
50
|
|
|
41
51
|
export interface InputProblem {
|
|
42
52
|
regionsForPour: InputPourRegion[]
|