@tscircuit/copper-pour-solver 0.0.25 → 0.0.26
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 +169 -323
- package/lib/solvers/CopperPourPipelineSolver.ts +22 -35
- package/lib/solvers/copper-pour/circle-to-polygon.ts +15 -0
- package/lib/solvers/copper-pour/generate-brep.ts +52 -45
- package/lib/solvers/copper-pour/get-board-polygon.ts +19 -11
- package/lib/solvers/copper-pour/process-obstacles.ts +87 -65
- package/package.json +1 -4
- 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__/via.snap.svg +1 -1
- package/tests/stm32f746g-disco.test.ts +16 -16
- package/lib/solvers/copper-pour/manifold-geometry-adapter.ts +0 -306
- package/tests/__snapshots__/stm32f746g-disco.test.tsbottom.snap.svg +0 -1
- package/tests/__snapshots__/stm32f746g-disco.test.tstop.snap.svg +0 -1
- package/tests/manifold-copper-pour-geometry.test.ts +0 -194
package/dist/index.js
CHANGED
|
@@ -1,255 +1,18 @@
|
|
|
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
|
-
|
|
248
4
|
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
5
|
+
import Flatten from "@flatten-js/core";
|
|
249
6
|
var getBoardPolygon = (region) => {
|
|
250
7
|
const board_edge_margin = region.board_edge_margin ?? 0;
|
|
251
8
|
if (region.outline && region.outline.length > 0) {
|
|
252
|
-
|
|
9
|
+
const polygon = new Flatten.Polygon(
|
|
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;
|
|
253
16
|
}
|
|
254
17
|
const { bounds } = region;
|
|
255
18
|
const newBounds = {
|
|
@@ -259,64 +22,74 @@ var getBoardPolygon = (region) => {
|
|
|
259
22
|
maxY: bounds.maxY - board_edge_margin
|
|
260
23
|
};
|
|
261
24
|
if (newBounds.minX >= newBounds.maxX || newBounds.minY >= newBounds.maxY) {
|
|
262
|
-
return
|
|
25
|
+
return new Flatten.Polygon();
|
|
263
26
|
}
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
27
|
+
return new Flatten.Polygon(
|
|
28
|
+
new Flatten.Box(
|
|
29
|
+
newBounds.minX,
|
|
30
|
+
newBounds.minY,
|
|
31
|
+
newBounds.maxX,
|
|
32
|
+
newBounds.maxY
|
|
33
|
+
).toPoints()
|
|
34
|
+
);
|
|
270
35
|
};
|
|
271
36
|
|
|
37
|
+
// lib/solvers/CopperPourPipelineSolver.ts
|
|
38
|
+
import Flatten5 from "@flatten-js/core";
|
|
39
|
+
|
|
272
40
|
// lib/solvers/copper-pour/process-obstacles.ts
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
var circleToPolygon = (
|
|
41
|
+
import Flatten3 from "@flatten-js/core";
|
|
42
|
+
|
|
43
|
+
// lib/solvers/copper-pour/circle-to-polygon.ts
|
|
44
|
+
import Flatten2 from "@flatten-js/core";
|
|
45
|
+
var circleToPolygon = (circle, numSegments = 32) => {
|
|
278
46
|
const points = [];
|
|
279
47
|
for (let i = 0; i < numSegments; i++) {
|
|
280
48
|
const angle = i / numSegments * 2 * Math.PI;
|
|
281
|
-
points.push(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
49
|
+
points.push(
|
|
50
|
+
new Flatten2.Point(
|
|
51
|
+
circle.center.x + circle.r * Math.cos(angle),
|
|
52
|
+
circle.center.y + circle.r * Math.sin(angle)
|
|
53
|
+
)
|
|
54
|
+
);
|
|
285
55
|
}
|
|
286
|
-
return points;
|
|
56
|
+
return new Flatten2.Polygon(points);
|
|
287
57
|
};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
58
|
+
|
|
59
|
+
// lib/solvers/copper-pour/process-obstacles.ts
|
|
60
|
+
var isRectPad = (pad) => pad.shape === "rect";
|
|
61
|
+
var isTracePad = (pad) => pad.shape === "trace";
|
|
62
|
+
var isCircularPad = (pad) => pad.shape === "circle";
|
|
63
|
+
var isPolygonPad = (pad) => pad.shape === "polygon";
|
|
294
64
|
var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline) => {
|
|
295
65
|
const polygonsToSubtract = [];
|
|
296
66
|
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins;
|
|
297
67
|
if (boardOutline && boardOutline.length > 0 && board_edge_margin && board_edge_margin > 0) {
|
|
298
|
-
const
|
|
299
|
-
boardOutline,
|
|
300
|
-
"processObstacles.boardOutline"
|
|
68
|
+
const boardPoly = new Flatten3.Polygon(
|
|
69
|
+
boardOutline.map((p) => Flatten3.point(p.x, p.y))
|
|
301
70
|
);
|
|
71
|
+
if (boardPoly.area() < 0) {
|
|
72
|
+
boardPoly.reverse();
|
|
73
|
+
}
|
|
74
|
+
const vertices = boardPoly.vertices;
|
|
302
75
|
for (let i = 0; i < vertices.length; i++) {
|
|
303
76
|
const p1 = vertices[i === 0 ? vertices.length - 1 : i - 1];
|
|
304
77
|
const p2 = vertices[i];
|
|
305
78
|
const p3 = vertices[(i + 1) % vertices.length];
|
|
306
79
|
if (!p1 || !p2 || !p3) continue;
|
|
307
|
-
const v1 =
|
|
308
|
-
const v2 =
|
|
309
|
-
const crossProduct = v1.
|
|
310
|
-
|
|
80
|
+
const v1 = new Flatten3.Vector(p1, p2);
|
|
81
|
+
const v2 = new Flatten3.Vector(p2, p3);
|
|
82
|
+
const crossProduct = v1.cross(v2);
|
|
83
|
+
const circle = new Flatten3.Circle(p2, board_edge_margin);
|
|
84
|
+
polygonsToSubtract.push(circleToPolygon(circle));
|
|
311
85
|
if (crossProduct < 0) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
p2.y + board_edge_margin
|
|
318
|
-
)
|
|
86
|
+
const box = new Flatten3.Box(
|
|
87
|
+
p2.x - board_edge_margin,
|
|
88
|
+
p2.y - board_edge_margin,
|
|
89
|
+
p2.x + board_edge_margin,
|
|
90
|
+
p2.y + board_edge_margin
|
|
319
91
|
);
|
|
92
|
+
polygonsToSubtract.push(new Flatten3.Polygon(box.toPoints()));
|
|
320
93
|
}
|
|
321
94
|
}
|
|
322
95
|
for (let i = 0; i < vertices.length; i++) {
|
|
@@ -344,7 +117,9 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
344
117
|
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
345
118
|
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
346
119
|
}));
|
|
347
|
-
polygonsToSubtract.push(
|
|
120
|
+
polygonsToSubtract.push(
|
|
121
|
+
new Flatten3.Polygon(rotatedCorners.map((p) => Flatten3.point(p.x, p.y)))
|
|
122
|
+
);
|
|
348
123
|
}
|
|
349
124
|
}
|
|
350
125
|
for (const pad of pads) {
|
|
@@ -355,22 +130,23 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
355
130
|
const isHoleOrCutout = pad.connectivityKey.startsWith("hole:") || pad.connectivityKey.startsWith("cutout:");
|
|
356
131
|
if (isCircularPad(pad)) {
|
|
357
132
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
358
|
-
|
|
359
|
-
|
|
133
|
+
const circle = new Flatten3.Circle(
|
|
134
|
+
new Flatten3.Point(pad.x, pad.y),
|
|
135
|
+
pad.radius + margin
|
|
360
136
|
);
|
|
137
|
+
polygonsToSubtract.push(circleToPolygon(circle));
|
|
361
138
|
continue;
|
|
362
139
|
}
|
|
363
140
|
if (isRectPad(pad)) {
|
|
364
141
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
365
142
|
const { bounds } = pad;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
bounds.maxY + margin
|
|
372
|
-
)
|
|
143
|
+
const b = new Flatten3.Box(
|
|
144
|
+
bounds.minX - margin,
|
|
145
|
+
bounds.minY - margin,
|
|
146
|
+
bounds.maxX + margin,
|
|
147
|
+
bounds.maxY + margin
|
|
373
148
|
);
|
|
149
|
+
polygonsToSubtract.push(new Flatten3.Polygon(b.toPoints()));
|
|
374
150
|
continue;
|
|
375
151
|
}
|
|
376
152
|
if (isPolygonPad(pad)) {
|
|
@@ -385,20 +161,50 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
385
161
|
return true;
|
|
386
162
|
});
|
|
387
163
|
if (uniquePoints.length < 3) continue;
|
|
388
|
-
const polygon =
|
|
389
|
-
|
|
164
|
+
const polygon = new Flatten3.Polygon(
|
|
165
|
+
uniquePoints.map((p) => Flatten3.point(p.x, p.y))
|
|
166
|
+
);
|
|
167
|
+
if (Math.abs(polygon.area()) < 1e-9) continue;
|
|
390
168
|
if (margin <= 0) {
|
|
391
169
|
polygonsToSubtract.push(polygon);
|
|
392
170
|
continue;
|
|
393
171
|
}
|
|
394
|
-
|
|
172
|
+
if (polygon.area() > 0) {
|
|
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
|
+
}
|
|
395
199
|
continue;
|
|
396
200
|
}
|
|
397
201
|
if (isTracePad(pad)) {
|
|
398
202
|
for (const segment of pad.segments) {
|
|
399
|
-
|
|
400
|
-
|
|
203
|
+
const circle = new Flatten3.Circle(
|
|
204
|
+
new Flatten3.Point(segment.x, segment.y),
|
|
205
|
+
pad.width / 2 + traceMargin
|
|
401
206
|
);
|
|
207
|
+
polygonsToSubtract.push(circleToPolygon(circle));
|
|
402
208
|
}
|
|
403
209
|
for (let i = 0; i < pad.segments.length - 1; i++) {
|
|
404
210
|
const p1 = pad.segments[i];
|
|
@@ -425,13 +231,62 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
425
231
|
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
426
232
|
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
427
233
|
}));
|
|
428
|
-
polygonsToSubtract.push(
|
|
234
|
+
polygonsToSubtract.push(
|
|
235
|
+
new Flatten3.Polygon(
|
|
236
|
+
rotatedCorners.map((p) => Flatten3.point(p.x, p.y))
|
|
237
|
+
)
|
|
238
|
+
);
|
|
429
239
|
}
|
|
430
240
|
}
|
|
431
241
|
}
|
|
432
242
|
return { polygonsToSubtract };
|
|
433
243
|
};
|
|
434
244
|
|
|
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
|
+
|
|
435
290
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
436
291
|
var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
437
292
|
constructor(input) {
|
|
@@ -461,32 +316,23 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
461
316
|
},
|
|
462
317
|
region.outline
|
|
463
318
|
);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
geometryDebugSummary(
|
|
480
|
-
"final copper polygons",
|
|
481
|
-
pourIslands.flatMap((island) => [
|
|
482
|
-
island.outerRing,
|
|
483
|
-
...island.innerRings
|
|
484
|
-
])
|
|
485
|
-
)
|
|
486
|
-
)
|
|
487
|
-
);
|
|
319
|
+
let pourPolygons = boardPolygon;
|
|
320
|
+
for (const poly of polygonsToSubtract) {
|
|
321
|
+
const currentPolys = Array.isArray(pourPolygons) ? pourPolygons : [pourPolygons];
|
|
322
|
+
const nextPolys = [];
|
|
323
|
+
for (const p of currentPolys) {
|
|
324
|
+
const result = Flatten5.BooleanOperations.subtract(p, poly);
|
|
325
|
+
if (result) {
|
|
326
|
+
if (Array.isArray(result)) {
|
|
327
|
+
nextPolys.push(...result.filter((r) => !r.isEmpty()));
|
|
328
|
+
} else {
|
|
329
|
+
if (!result.isEmpty()) nextPolys.push(result);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
pourPolygons = nextPolys;
|
|
488
334
|
}
|
|
489
|
-
const new_breps = generateBRep(
|
|
335
|
+
const new_breps = generateBRep(pourPolygons);
|
|
490
336
|
brep_shapes.push(...new_breps);
|
|
491
337
|
}
|
|
492
338
|
return {
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { BasePipelineSolver } from "@tscircuit/solver-utils"
|
|
2
|
-
import type { BRepShape } from "circuit-json"
|
|
3
|
-
import type { InputProblem, PipelineOutput } from "lib/types"
|
|
4
|
-
import { generateBRep } from "./copper-pour/generate-brep"
|
|
5
2
|
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"
|
|
3
|
+
import Flatten from "@flatten-js/core"
|
|
12
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"
|
|
13
8
|
|
|
14
9
|
export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
15
10
|
pipelineDef = []
|
|
@@ -43,35 +38,27 @@ export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
|
43
38
|
region.outline,
|
|
44
39
|
)
|
|
45
40
|
|
|
46
|
-
|
|
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)
|
|
41
|
+
let pourPolygons: Flatten.Polygon | Flatten.Polygon[] = boardPolygon
|
|
59
42
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
43
|
+
for (const poly of polygonsToSubtract) {
|
|
44
|
+
const currentPolys = Array.isArray(pourPolygons)
|
|
45
|
+
? pourPolygons
|
|
46
|
+
: [pourPolygons]
|
|
47
|
+
const nextPolys: Flatten.Polygon[] = []
|
|
48
|
+
for (const p of currentPolys) {
|
|
49
|
+
const result = Flatten.BooleanOperations.subtract(p, poly)
|
|
50
|
+
if (result) {
|
|
51
|
+
if (Array.isArray(result)) {
|
|
52
|
+
nextPolys.push(...result.filter((r) => !r.isEmpty()))
|
|
53
|
+
} else {
|
|
54
|
+
if (!result.isEmpty()) nextPolys.push(result)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pourPolygons = nextPolys
|
|
72
59
|
}
|
|
73
60
|
|
|
74
|
-
const new_breps = generateBRep(
|
|
61
|
+
const new_breps = generateBRep(pourPolygons)
|
|
75
62
|
brep_shapes.push(...new_breps)
|
|
76
63
|
}
|
|
77
64
|
|
|
@@ -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
|
+
}
|