@tscircuit/copper-pour-solver 0.0.23 → 0.0.25
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.js +323 -169
- package/lib/solvers/CopperPourPipelineSolver.ts +35 -22
- package/lib/solvers/copper-pour/generate-brep.ts +45 -52
- package/lib/solvers/copper-pour/get-board-polygon.ts +11 -19
- package/lib/solvers/copper-pour/manifold-geometry-adapter.ts +306 -0
- package/lib/solvers/copper-pour/process-obstacles.ts +65 -87
- package/package.json +4 -1
- package/tests/__snapshots__/2-layers-bottom.snap.svg +1 -1
- package/tests/__snapshots__/2-layers-top.snap.svg +1 -1
- package/tests/__snapshots__/board-edge-margin-2.snap.svg +1 -1
- package/tests/__snapshots__/board-edge-margin.snap.svg +1 -1
- package/tests/__snapshots__/hole-and-cutouts.snap.svg +1 -1
- package/tests/__snapshots__/larger-trace-margin.snap.svg +1 -1
- package/tests/__snapshots__/multiple-pours.snap.svg +1 -1
- package/tests/__snapshots__/pad-margin.snap.svg +1 -1
- package/tests/__snapshots__/polygon-board-2.snap.svg +1 -1
- package/tests/__snapshots__/polygon-board.snap.svg +1 -1
- package/tests/__snapshots__/smaller-trace-margin.snap.svg +1 -1
- package/tests/__snapshots__/stm32f746g-disco.test.tsbottom.snap.svg +1 -0
- package/tests/__snapshots__/stm32f746g-disco.test.tstop.snap.svg +1 -0
- package/tests/__snapshots__/via.snap.svg +1 -1
- package/tests/assets/tsci_AnasSarkiz.STM32F746G-DISCO.circuit.json +67472 -0
- package/tests/manifold-copper-pour-geometry.test.ts +194 -0
- package/tests/stm32f746g-disco.test.ts +26 -0
- package/lib/solvers/copper-pour/circle-to-polygon.ts +0 -15
package/dist/index.js
CHANGED
|
@@ -1,18 +1,255 @@
|
|
|
1
1
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
2
2
|
import { BasePipelineSolver } from "@tscircuit/solver-utils";
|
|
3
3
|
|
|
4
|
+
// lib/solvers/copper-pour/generate-brep.ts
|
|
5
|
+
var signedArea = (ring) => {
|
|
6
|
+
let area = 0;
|
|
7
|
+
for (let i = 0; i < ring.length; i++) {
|
|
8
|
+
const current = ring[i];
|
|
9
|
+
const next = ring[(i + 1) % ring.length];
|
|
10
|
+
area += current.x * next.y - next.x * current.y;
|
|
11
|
+
}
|
|
12
|
+
return area / 2;
|
|
13
|
+
};
|
|
14
|
+
var ensureAreaSign = (ring, desiredSign) => {
|
|
15
|
+
const area = signedArea(ring);
|
|
16
|
+
const shouldReverse = desiredSign === "positive" && area < 0 || desiredSign === "negative" && area > 0;
|
|
17
|
+
return shouldReverse ? [...ring].reverse() : ring;
|
|
18
|
+
};
|
|
19
|
+
var ringToVertices = (ring) => ring.map((point) => ({
|
|
20
|
+
x: point.x,
|
|
21
|
+
y: point.y
|
|
22
|
+
}));
|
|
23
|
+
var generateBRep = (pourIslands) => {
|
|
24
|
+
const brep_shapes = [];
|
|
25
|
+
for (const island of pourIslands) {
|
|
26
|
+
if (island.outerRing.length < 3) continue;
|
|
27
|
+
const outerRing = ensureAreaSign(island.outerRing, "negative");
|
|
28
|
+
const innerRings = island.innerRings.filter((ring) => ring.length >= 3).map((ring) => ensureAreaSign(ring, "positive"));
|
|
29
|
+
brep_shapes.push({
|
|
30
|
+
outer_ring: { vertices: ringToVertices(outerRing) },
|
|
31
|
+
inner_rings: innerRings.map((ring) => ({
|
|
32
|
+
vertices: ringToVertices(ring)
|
|
33
|
+
}))
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return brep_shapes;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// lib/solvers/copper-pour/manifold-geometry-adapter.ts
|
|
40
|
+
var manifoldModule = await import("manifold-3d");
|
|
41
|
+
var manifoldFactory = manifoldModule.default;
|
|
42
|
+
var manifold = await manifoldFactory();
|
|
43
|
+
manifold.setup();
|
|
44
|
+
var { CrossSection } = manifold;
|
|
45
|
+
var MANIFOLD_GEOMETRY_SCALE = 1e6;
|
|
46
|
+
var DEFAULT_MIN_ISLAND_AREA = 1e-8;
|
|
47
|
+
var describePolygons = (polygons) => {
|
|
48
|
+
let pointCount = 0;
|
|
49
|
+
const bbox = {
|
|
50
|
+
minX: Number.POSITIVE_INFINITY,
|
|
51
|
+
minY: Number.POSITIVE_INFINITY,
|
|
52
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
53
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
54
|
+
};
|
|
55
|
+
for (const polygon of polygons) {
|
|
56
|
+
pointCount += polygon.length;
|
|
57
|
+
for (const [x, y] of polygon) {
|
|
58
|
+
bbox.minX = Math.min(bbox.minX, x);
|
|
59
|
+
bbox.minY = Math.min(bbox.minY, y);
|
|
60
|
+
bbox.maxX = Math.max(bbox.maxX, x);
|
|
61
|
+
bbox.maxY = Math.max(bbox.maxY, y);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
polygonCount: polygons.length,
|
|
66
|
+
pointCount,
|
|
67
|
+
bbox: pointCount > 0 ? bbox : null,
|
|
68
|
+
scale: MANIFOLD_GEOMETRY_SCALE
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
var assertFinitePoint = (point, operation) => {
|
|
72
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${operation} received non-finite point (${point.x}, ${point.y})`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var pointsEqual = (a, b) => a.x === b.x && a.y === b.y;
|
|
79
|
+
var signedArea2 = (ring) => {
|
|
80
|
+
let area = 0;
|
|
81
|
+
for (let i = 0; i < ring.length; i++) {
|
|
82
|
+
const current = ring[i];
|
|
83
|
+
const next = ring[(i + 1) % ring.length];
|
|
84
|
+
area += current.x * next.y - next.x * current.y;
|
|
85
|
+
}
|
|
86
|
+
return area / 2;
|
|
87
|
+
};
|
|
88
|
+
var normalizeRing = (ring, operation = "normalizeRing") => {
|
|
89
|
+
const normalized = [];
|
|
90
|
+
for (const point of ring) {
|
|
91
|
+
assertFinitePoint(point, operation);
|
|
92
|
+
const roundedPoint = {
|
|
93
|
+
x: Math.round(point.x * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
94
|
+
y: Math.round(point.y * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE
|
|
95
|
+
};
|
|
96
|
+
const previous = normalized[normalized.length - 1];
|
|
97
|
+
if (!previous || !pointsEqual(previous, roundedPoint)) {
|
|
98
|
+
normalized.push(roundedPoint);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (normalized.length > 1 && pointsEqual(normalized[0], normalized[normalized.length - 1])) {
|
|
102
|
+
normalized.pop();
|
|
103
|
+
}
|
|
104
|
+
const uniquePoints = new Set(normalized.map((p) => `${p.x},${p.y}`));
|
|
105
|
+
if (uniquePoints.size < 3 || Math.abs(signedArea2(normalized)) < 1e-18) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
return normalized;
|
|
109
|
+
};
|
|
110
|
+
var toScaledManifoldPolygons = (polygons, operation = "toScaledManifoldPolygons") => {
|
|
111
|
+
const scaledPolygons = [];
|
|
112
|
+
for (const polygon of polygons) {
|
|
113
|
+
const normalized = normalizeRing(polygon, operation);
|
|
114
|
+
if (normalized.length < 3) continue;
|
|
115
|
+
const positiveRing = signedArea2(normalized) < 0 ? [...normalized].reverse() : normalized;
|
|
116
|
+
scaledPolygons.push(
|
|
117
|
+
positiveRing.map((p) => [
|
|
118
|
+
Math.round(p.x * MANIFOLD_GEOMETRY_SCALE),
|
|
119
|
+
Math.round(p.y * MANIFOLD_GEOMETRY_SCALE)
|
|
120
|
+
])
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return scaledPolygons;
|
|
124
|
+
};
|
|
125
|
+
var fromScaledManifoldPolygons = (polygons) => polygons.map(
|
|
126
|
+
(polygon) => normalizeRing(
|
|
127
|
+
polygon.map(([x, y]) => ({
|
|
128
|
+
x: x / MANIFOLD_GEOMETRY_SCALE,
|
|
129
|
+
y: y / MANIFOLD_GEOMETRY_SCALE
|
|
130
|
+
})),
|
|
131
|
+
"fromScaledManifoldPolygons"
|
|
132
|
+
)
|
|
133
|
+
).filter((polygon) => polygon.length >= 3);
|
|
134
|
+
var runManifoldOperation = (operation, polygons, callback) => {
|
|
135
|
+
try {
|
|
136
|
+
return callback();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const details = describePolygons(polygons);
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
throw new Error(
|
|
141
|
+
`${operation} failed: ${message}; details=${JSON.stringify(details)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var crossSectionFromPolygon = (polygon, fillRule = "Positive") => {
|
|
146
|
+
const scaledPolygons = toScaledManifoldPolygons(
|
|
147
|
+
[polygon],
|
|
148
|
+
"crossSectionFromPolygon"
|
|
149
|
+
);
|
|
150
|
+
if (scaledPolygons.length === 0) {
|
|
151
|
+
return CrossSection.ofPolygons([]);
|
|
152
|
+
}
|
|
153
|
+
return runManifoldOperation(
|
|
154
|
+
"crossSectionFromPolygon",
|
|
155
|
+
scaledPolygons,
|
|
156
|
+
() => CrossSection.ofPolygons(scaledPolygons, fillRule)
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
var crossSectionFromPolygons = (polygons, fillRule = "Positive") => {
|
|
160
|
+
const scaledPolygons = toScaledManifoldPolygons(
|
|
161
|
+
polygons,
|
|
162
|
+
"crossSectionFromPolygons"
|
|
163
|
+
);
|
|
164
|
+
if (scaledPolygons.length === 0) {
|
|
165
|
+
return CrossSection.ofPolygons([]);
|
|
166
|
+
}
|
|
167
|
+
return runManifoldOperation(
|
|
168
|
+
"crossSectionFromPolygons",
|
|
169
|
+
scaledPolygons,
|
|
170
|
+
() => CrossSection.ofPolygons(scaledPolygons, fillRule)
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
var composeCrossSections = (sections) => {
|
|
174
|
+
const nonEmptySections = sections.filter((section) => !section.isEmpty());
|
|
175
|
+
if (nonEmptySections.length === 0) {
|
|
176
|
+
return CrossSection.ofPolygons([]);
|
|
177
|
+
}
|
|
178
|
+
return runManifoldOperation(
|
|
179
|
+
"composeCrossSections",
|
|
180
|
+
[],
|
|
181
|
+
() => CrossSection.compose(nonEmptySections)
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
var offsetPolygon = (polygon, margin, joinType = "Miter") => {
|
|
185
|
+
const scaledPolygons = toScaledManifoldPolygons([polygon], "offsetPolygon");
|
|
186
|
+
if (scaledPolygons.length === 0 || margin <= 0) {
|
|
187
|
+
return scaledPolygons.length === 0 ? [] : [normalizeRing(polygon)];
|
|
188
|
+
}
|
|
189
|
+
const scaledMargin = margin * MANIFOLD_GEOMETRY_SCALE;
|
|
190
|
+
const section = runManifoldOperation(
|
|
191
|
+
"offsetPolygon.input",
|
|
192
|
+
scaledPolygons,
|
|
193
|
+
() => CrossSection.ofPolygons(scaledPolygons, "Positive")
|
|
194
|
+
);
|
|
195
|
+
const offset = runManifoldOperation(
|
|
196
|
+
"offsetPolygon.offset",
|
|
197
|
+
scaledPolygons,
|
|
198
|
+
() => section.offset(scaledMargin, joinType, 2, 32)
|
|
199
|
+
);
|
|
200
|
+
return fromScaledManifoldPolygons(offset.toPolygons());
|
|
201
|
+
};
|
|
202
|
+
var subtractBlockersFromPour = (pourPolygon, blockerPolygons) => {
|
|
203
|
+
const pourSection = crossSectionFromPolygon(pourPolygon);
|
|
204
|
+
const blockerSection = crossSectionFromPolygons(blockerPolygons);
|
|
205
|
+
if (pourSection.isEmpty() || blockerSection.isEmpty()) {
|
|
206
|
+
return pourSection;
|
|
207
|
+
}
|
|
208
|
+
const operationPolygons = [
|
|
209
|
+
...toScaledManifoldPolygons([pourPolygon], "subtractBlockersFromPour.pour"),
|
|
210
|
+
...toScaledManifoldPolygons(
|
|
211
|
+
blockerPolygons,
|
|
212
|
+
"subtractBlockersFromPour.blockers"
|
|
213
|
+
)
|
|
214
|
+
];
|
|
215
|
+
return runManifoldOperation(
|
|
216
|
+
"subtractBlockersFromPour",
|
|
217
|
+
operationPolygons,
|
|
218
|
+
() => pourSection.subtract(blockerSection)
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
var removeTinyIslands = (section, minArea = DEFAULT_MIN_ISLAND_AREA) => {
|
|
222
|
+
if (section.isEmpty()) return section;
|
|
223
|
+
const minScaledArea = minArea * MANIFOLD_GEOMETRY_SCALE * MANIFOLD_GEOMETRY_SCALE;
|
|
224
|
+
const islands = section.decompose().filter((island) => Math.abs(island.area()) >= minScaledArea);
|
|
225
|
+
return composeCrossSections(islands);
|
|
226
|
+
};
|
|
227
|
+
var crossSectionToCopperPourIslands = (section) => {
|
|
228
|
+
const islands = [];
|
|
229
|
+
for (const island of section.decompose()) {
|
|
230
|
+
const rings = fromScaledManifoldPolygons(island.toPolygons());
|
|
231
|
+
if (rings.length === 0) continue;
|
|
232
|
+
const outerRing = rings.reduce(
|
|
233
|
+
(largest, ring) => Math.abs(signedArea2(ring)) > Math.abs(signedArea2(largest)) ? ring : largest
|
|
234
|
+
);
|
|
235
|
+
const innerRings = rings.filter((ring) => ring !== outerRing);
|
|
236
|
+
islands.push({
|
|
237
|
+
outerRing,
|
|
238
|
+
innerRings
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return islands;
|
|
242
|
+
};
|
|
243
|
+
var geometryDebugSummary = (label, polygons) => ({
|
|
244
|
+
label,
|
|
245
|
+
...describePolygons(toScaledManifoldPolygons(polygons, label))
|
|
246
|
+
});
|
|
247
|
+
|
|
4
248
|
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
5
|
-
import Flatten from "@flatten-js/core";
|
|
6
249
|
var getBoardPolygon = (region) => {
|
|
7
250
|
const board_edge_margin = region.board_edge_margin ?? 0;
|
|
8
251
|
if (region.outline && region.outline.length > 0) {
|
|
9
|
-
|
|
10
|
-
region.outline.map((p) => Flatten.point(p.x, p.y))
|
|
11
|
-
);
|
|
12
|
-
if (polygon.orientation() === Flatten.ORIENTATION.CW) {
|
|
13
|
-
polygon.reverse();
|
|
14
|
-
}
|
|
15
|
-
return polygon;
|
|
252
|
+
return normalizeRing(region.outline, "getBoardPolygon.outline");
|
|
16
253
|
}
|
|
17
254
|
const { bounds } = region;
|
|
18
255
|
const newBounds = {
|
|
@@ -22,74 +259,64 @@ var getBoardPolygon = (region) => {
|
|
|
22
259
|
maxY: bounds.maxY - board_edge_margin
|
|
23
260
|
};
|
|
24
261
|
if (newBounds.minX >= newBounds.maxX || newBounds.minY >= newBounds.maxY) {
|
|
25
|
-
return
|
|
262
|
+
return [];
|
|
26
263
|
}
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
).toPoints()
|
|
34
|
-
);
|
|
264
|
+
return [
|
|
265
|
+
{ x: newBounds.minX, y: newBounds.minY },
|
|
266
|
+
{ x: newBounds.maxX, y: newBounds.minY },
|
|
267
|
+
{ x: newBounds.maxX, y: newBounds.maxY },
|
|
268
|
+
{ x: newBounds.minX, y: newBounds.maxY }
|
|
269
|
+
];
|
|
35
270
|
};
|
|
36
271
|
|
|
37
|
-
// lib/solvers/CopperPourPipelineSolver.ts
|
|
38
|
-
import Flatten5 from "@flatten-js/core";
|
|
39
|
-
|
|
40
272
|
// lib/solvers/copper-pour/process-obstacles.ts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
var circleToPolygon = (
|
|
273
|
+
var isRectPad = (pad) => pad.shape === "rect";
|
|
274
|
+
var isTracePad = (pad) => pad.shape === "trace";
|
|
275
|
+
var isCircularPad = (pad) => pad.shape === "circle";
|
|
276
|
+
var isPolygonPad = (pad) => pad.shape === "polygon";
|
|
277
|
+
var circleToPolygon = (center, radius, numSegments = 32) => {
|
|
46
278
|
const points = [];
|
|
47
279
|
for (let i = 0; i < numSegments; i++) {
|
|
48
280
|
const angle = i / numSegments * 2 * Math.PI;
|
|
49
|
-
points.push(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
);
|
|
281
|
+
points.push({
|
|
282
|
+
x: center.x + radius * Math.cos(angle),
|
|
283
|
+
y: center.y + radius * Math.sin(angle)
|
|
284
|
+
});
|
|
55
285
|
}
|
|
56
|
-
return
|
|
286
|
+
return points;
|
|
57
287
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
288
|
+
var boxToPolygon = (minX, minY, maxX, maxY) => [
|
|
289
|
+
{ x: minX, y: minY },
|
|
290
|
+
{ x: maxX, y: minY },
|
|
291
|
+
{ x: maxX, y: maxY },
|
|
292
|
+
{ x: minX, y: maxY }
|
|
293
|
+
];
|
|
64
294
|
var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline) => {
|
|
65
295
|
const polygonsToSubtract = [];
|
|
66
296
|
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins;
|
|
67
297
|
if (boardOutline && boardOutline.length > 0 && board_edge_margin && board_edge_margin > 0) {
|
|
68
|
-
const
|
|
69
|
-
boardOutline
|
|
298
|
+
const vertices = normalizeRing(
|
|
299
|
+
boardOutline,
|
|
300
|
+
"processObstacles.boardOutline"
|
|
70
301
|
);
|
|
71
|
-
if (boardPoly.area() < 0) {
|
|
72
|
-
boardPoly.reverse();
|
|
73
|
-
}
|
|
74
|
-
const vertices = boardPoly.vertices;
|
|
75
302
|
for (let i = 0; i < vertices.length; i++) {
|
|
76
303
|
const p1 = vertices[i === 0 ? vertices.length - 1 : i - 1];
|
|
77
304
|
const p2 = vertices[i];
|
|
78
305
|
const p3 = vertices[(i + 1) % vertices.length];
|
|
79
306
|
if (!p1 || !p2 || !p3) continue;
|
|
80
|
-
const v1 =
|
|
81
|
-
const v2 =
|
|
82
|
-
const crossProduct = v1.
|
|
83
|
-
|
|
84
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
307
|
+
const v1 = { x: p2.x - p1.x, y: p2.y - p1.y };
|
|
308
|
+
const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
|
309
|
+
const crossProduct = v1.x * v2.y - v1.y * v2.x;
|
|
310
|
+
polygonsToSubtract.push(circleToPolygon(p2, board_edge_margin));
|
|
85
311
|
if (crossProduct < 0) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
312
|
+
polygonsToSubtract.push(
|
|
313
|
+
boxToPolygon(
|
|
314
|
+
p2.x - board_edge_margin,
|
|
315
|
+
p2.y - board_edge_margin,
|
|
316
|
+
p2.x + board_edge_margin,
|
|
317
|
+
p2.y + board_edge_margin
|
|
318
|
+
)
|
|
91
319
|
);
|
|
92
|
-
polygonsToSubtract.push(new Flatten3.Polygon(box.toPoints()));
|
|
93
320
|
}
|
|
94
321
|
}
|
|
95
322
|
for (let i = 0; i < vertices.length; i++) {
|
|
@@ -117,9 +344,7 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
117
344
|
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
118
345
|
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
119
346
|
}));
|
|
120
|
-
polygonsToSubtract.push(
|
|
121
|
-
new Flatten3.Polygon(rotatedCorners.map((p) => Flatten3.point(p.x, p.y)))
|
|
122
|
-
);
|
|
347
|
+
polygonsToSubtract.push(rotatedCorners);
|
|
123
348
|
}
|
|
124
349
|
}
|
|
125
350
|
for (const pad of pads) {
|
|
@@ -130,23 +355,22 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
130
355
|
const isHoleOrCutout = pad.connectivityKey.startsWith("hole:") || pad.connectivityKey.startsWith("cutout:");
|
|
131
356
|
if (isCircularPad(pad)) {
|
|
132
357
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
pad.radius + margin
|
|
358
|
+
polygonsToSubtract.push(
|
|
359
|
+
circleToPolygon({ x: pad.x, y: pad.y }, pad.radius + margin)
|
|
136
360
|
);
|
|
137
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
138
361
|
continue;
|
|
139
362
|
}
|
|
140
363
|
if (isRectPad(pad)) {
|
|
141
364
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
142
365
|
const { bounds } = pad;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
366
|
+
polygonsToSubtract.push(
|
|
367
|
+
boxToPolygon(
|
|
368
|
+
bounds.minX - margin,
|
|
369
|
+
bounds.minY - margin,
|
|
370
|
+
bounds.maxX + margin,
|
|
371
|
+
bounds.maxY + margin
|
|
372
|
+
)
|
|
148
373
|
);
|
|
149
|
-
polygonsToSubtract.push(new Flatten3.Polygon(b.toPoints()));
|
|
150
374
|
continue;
|
|
151
375
|
}
|
|
152
376
|
if (isPolygonPad(pad)) {
|
|
@@ -161,50 +385,20 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
161
385
|
return true;
|
|
162
386
|
});
|
|
163
387
|
if (uniquePoints.length < 3) continue;
|
|
164
|
-
const polygon =
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
if (Math.abs(polygon.area()) < 1e-9) continue;
|
|
388
|
+
const polygon = normalizeRing(uniquePoints, "processObstacles.polygonPad");
|
|
389
|
+
if (polygon.length < 3) continue;
|
|
168
390
|
if (margin <= 0) {
|
|
169
391
|
polygonsToSubtract.push(polygon);
|
|
170
392
|
continue;
|
|
171
393
|
}
|
|
172
|
-
|
|
173
|
-
polygon.reverse();
|
|
174
|
-
}
|
|
175
|
-
const offsetLines = [];
|
|
176
|
-
const polygonVertices = polygon.vertices;
|
|
177
|
-
for (let i = 0; i < polygonVertices.length; i++) {
|
|
178
|
-
const p1 = polygonVertices[i];
|
|
179
|
-
const p2 = polygonVertices[(i + 1) % polygonVertices.length];
|
|
180
|
-
const segment = Flatten3.segment(p1, p2);
|
|
181
|
-
if (segment.length === 0) continue;
|
|
182
|
-
const line = Flatten3.line(segment.start, segment.end);
|
|
183
|
-
const norm = line.norm;
|
|
184
|
-
const offsetLine = line.translate(norm.multiply(-margin));
|
|
185
|
-
offsetLines.push(offsetLine);
|
|
186
|
-
}
|
|
187
|
-
const newPolygonPoints = [];
|
|
188
|
-
for (let i = 0; i < offsetLines.length; i++) {
|
|
189
|
-
const line1 = offsetLines[i];
|
|
190
|
-
const line2 = offsetLines[(i + 1) % offsetLines.length];
|
|
191
|
-
const ip = line1.intersect(line2);
|
|
192
|
-
if (ip.length > 0) {
|
|
193
|
-
newPolygonPoints.push(ip[0]);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (newPolygonPoints.length >= 3) {
|
|
197
|
-
polygonsToSubtract.push(new Flatten3.Polygon(newPolygonPoints));
|
|
198
|
-
}
|
|
394
|
+
polygonsToSubtract.push(...offsetPolygon(polygon, margin));
|
|
199
395
|
continue;
|
|
200
396
|
}
|
|
201
397
|
if (isTracePad(pad)) {
|
|
202
398
|
for (const segment of pad.segments) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pad.width / 2 + traceMargin
|
|
399
|
+
polygonsToSubtract.push(
|
|
400
|
+
circleToPolygon(segment, pad.width / 2 + traceMargin)
|
|
206
401
|
);
|
|
207
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
208
402
|
}
|
|
209
403
|
for (let i = 0; i < pad.segments.length - 1; i++) {
|
|
210
404
|
const p1 = pad.segments[i];
|
|
@@ -231,62 +425,13 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
231
425
|
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
232
426
|
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
233
427
|
}));
|
|
234
|
-
polygonsToSubtract.push(
|
|
235
|
-
new Flatten3.Polygon(
|
|
236
|
-
rotatedCorners.map((p) => Flatten3.point(p.x, p.y))
|
|
237
|
-
)
|
|
238
|
-
);
|
|
428
|
+
polygonsToSubtract.push(rotatedCorners);
|
|
239
429
|
}
|
|
240
430
|
}
|
|
241
431
|
}
|
|
242
432
|
return { polygonsToSubtract };
|
|
243
433
|
};
|
|
244
434
|
|
|
245
|
-
// lib/solvers/copper-pour/generate-brep.ts
|
|
246
|
-
import Flatten4 from "@flatten-js/core";
|
|
247
|
-
var faceToVertices = (face) => face.edges.map((e) => {
|
|
248
|
-
const pt = {
|
|
249
|
-
x: e.start.x,
|
|
250
|
-
y: e.start.y
|
|
251
|
-
};
|
|
252
|
-
if (e.isArc) {
|
|
253
|
-
const bulge = Math.tan(e.shape.sweep / 4);
|
|
254
|
-
if (Math.abs(bulge) > 1e-9) {
|
|
255
|
-
pt.bulge = bulge;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return pt;
|
|
259
|
-
});
|
|
260
|
-
var generateBRep = (pourPolygons) => {
|
|
261
|
-
const brep_shapes = [];
|
|
262
|
-
const polygons = Array.isArray(pourPolygons) ? pourPolygons : [pourPolygons];
|
|
263
|
-
for (const p of polygons) {
|
|
264
|
-
const islands = p.splitToIslands();
|
|
265
|
-
for (const island of islands) {
|
|
266
|
-
if (island.isEmpty()) continue;
|
|
267
|
-
const faces = [...island.faces];
|
|
268
|
-
const outer_face_ccw = faces.find(
|
|
269
|
-
(f) => f.orientation() === Flatten4.ORIENTATION.CCW
|
|
270
|
-
);
|
|
271
|
-
const inner_faces_cw = faces.filter(
|
|
272
|
-
(f) => f.orientation() === Flatten4.ORIENTATION.CW
|
|
273
|
-
);
|
|
274
|
-
if (!outer_face_ccw) continue;
|
|
275
|
-
outer_face_ccw.reverse();
|
|
276
|
-
const outer_ring_vertices = faceToVertices(outer_face_ccw);
|
|
277
|
-
const inner_rings = inner_faces_cw.map((f) => {
|
|
278
|
-
f.reverse();
|
|
279
|
-
return { vertices: faceToVertices(f) };
|
|
280
|
-
});
|
|
281
|
-
brep_shapes.push({
|
|
282
|
-
outer_ring: { vertices: outer_ring_vertices },
|
|
283
|
-
inner_rings
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return brep_shapes;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
435
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
291
436
|
var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
292
437
|
constructor(input) {
|
|
@@ -316,23 +461,32 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
316
461
|
},
|
|
317
462
|
region.outline
|
|
318
463
|
);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
464
|
+
if (process.env.COPPER_POUR_DEBUG_GEOMETRY) {
|
|
465
|
+
console.info(
|
|
466
|
+
JSON.stringify([
|
|
467
|
+
geometryDebugSummary("input pour polygon", [boardPolygon]),
|
|
468
|
+
geometryDebugSummary("unioned blockers input", polygonsToSubtract)
|
|
469
|
+
])
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const finalPour = removeTinyIslands(
|
|
473
|
+
subtractBlockersFromPour(boardPolygon, polygonsToSubtract)
|
|
474
|
+
);
|
|
475
|
+
const pourIslands = crossSectionToCopperPourIslands(finalPour);
|
|
476
|
+
if (process.env.COPPER_POUR_DEBUG_GEOMETRY) {
|
|
477
|
+
console.info(
|
|
478
|
+
JSON.stringify(
|
|
479
|
+
geometryDebugSummary(
|
|
480
|
+
"final copper polygons",
|
|
481
|
+
pourIslands.flatMap((island) => [
|
|
482
|
+
island.outerRing,
|
|
483
|
+
...island.innerRings
|
|
484
|
+
])
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
);
|
|
334
488
|
}
|
|
335
|
-
const new_breps = generateBRep(
|
|
489
|
+
const new_breps = generateBRep(pourIslands);
|
|
336
490
|
brep_shapes.push(...new_breps);
|
|
337
491
|
}
|
|
338
492
|
return {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
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
2
|
import type { BRepShape } from "circuit-json"
|
|
7
3
|
import type { InputProblem, PipelineOutput } from "lib/types"
|
|
4
|
+
import { generateBRep } from "./copper-pour/generate-brep"
|
|
5
|
+
import { getBoardPolygon } from "./copper-pour/get-board-polygon"
|
|
6
|
+
import {
|
|
7
|
+
crossSectionToCopperPourIslands,
|
|
8
|
+
geometryDebugSummary,
|
|
9
|
+
removeTinyIslands,
|
|
10
|
+
subtractBlockersFromPour,
|
|
11
|
+
} from "./copper-pour/manifold-geometry-adapter"
|
|
12
|
+
import { processObstaclesForPour } from "./copper-pour/process-obstacles"
|
|
8
13
|
|
|
9
14
|
export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
10
15
|
pipelineDef = []
|
|
@@ -38,27 +43,35 @@ export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
|
38
43
|
region.outline,
|
|
39
44
|
)
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
if (process.env.COPPER_POUR_DEBUG_GEOMETRY) {
|
|
47
|
+
console.info(
|
|
48
|
+
JSON.stringify([
|
|
49
|
+
geometryDebugSummary("input pour polygon", [boardPolygon]),
|
|
50
|
+
geometryDebugSummary("unioned blockers input", polygonsToSubtract),
|
|
51
|
+
]),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const finalPour = removeTinyIslands(
|
|
56
|
+
subtractBlockersFromPour(boardPolygon, polygonsToSubtract),
|
|
57
|
+
)
|
|
58
|
+
const pourIslands = crossSectionToCopperPourIslands(finalPour)
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
pourPolygons = nextPolys
|
|
60
|
+
if (process.env.COPPER_POUR_DEBUG_GEOMETRY) {
|
|
61
|
+
console.info(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
geometryDebugSummary(
|
|
64
|
+
"final copper polygons",
|
|
65
|
+
pourIslands.flatMap((island) => [
|
|
66
|
+
island.outerRing,
|
|
67
|
+
...island.innerRings,
|
|
68
|
+
]),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
const new_breps = generateBRep(
|
|
74
|
+
const new_breps = generateBRep(pourIslands)
|
|
62
75
|
brep_shapes.push(...new_breps)
|
|
63
76
|
}
|
|
64
77
|
|