@tscircuit/copper-pour-solver 0.0.26 → 0.0.28
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 +3 -1
- package/dist/index.js +347 -205
- package/lib/index.ts +1 -0
- package/lib/solvers/CopperPourPipelineSolver.ts +20 -24
- package/lib/solvers/copper-pour/generate-brep.ts +35 -51
- package/lib/solvers/copper-pour/get-board-polygon.ts +10 -19
- package/lib/solvers/copper-pour/manifold-geometry-adapter.ts +159 -0
- package/lib/solvers/copper-pour/manifold-runtime.ts +51 -0
- package/lib/solvers/copper-pour/polygon-primitives.ts +57 -0
- package/lib/solvers/copper-pour/polygon-ring.ts +126 -0
- package/lib/solvers/copper-pour/process-obstacles.ts +48 -143
- 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/fixtures/preload.ts +3 -0
- package/tests/manifold-copper-pour-geometry.test.ts +194 -0
- package/tests/stm32f746g-disco.test.ts +16 -16
- package/lib/solvers/copper-pour/circle-to-polygon.ts +0 -15
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ declare class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem>
|
|
|
54
54
|
getOutput(): PipelineOutput;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
declare const initializeManifoldGeometry: () => Promise<void>;
|
|
58
|
+
|
|
57
59
|
declare const convertCircuitJsonToInputProblem: (circuitJson: AnyCircuitElement[], options: {
|
|
58
60
|
layer: LayerRef;
|
|
59
61
|
pour_connectivity_key: string;
|
|
@@ -64,4 +66,4 @@ declare const convertCircuitJsonToInputProblem: (circuitJson: AnyCircuitElement[
|
|
|
64
66
|
outline?: Point$1[];
|
|
65
67
|
}) => InputProblem;
|
|
66
68
|
|
|
67
|
-
export { type BaseInputPad, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPolygonPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem };
|
|
69
|
+
export { type BaseInputPad, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPolygonPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem, initializeManifoldGeometry };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,127 @@
|
|
|
1
1
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
2
2
|
import { BasePipelineSolver } from "@tscircuit/solver-utils";
|
|
3
3
|
|
|
4
|
+
// lib/solvers/copper-pour/polygon-ring.ts
|
|
5
|
+
var MANIFOLD_GEOMETRY_SCALE = 1e6;
|
|
6
|
+
var signedArea = (ring) => {
|
|
7
|
+
let area = 0;
|
|
8
|
+
for (let i = 0; i < ring.length; i++) {
|
|
9
|
+
const current = ring[i];
|
|
10
|
+
const next = ring[(i + 1) % ring.length];
|
|
11
|
+
area += current.x * next.y - next.x * current.y;
|
|
12
|
+
}
|
|
13
|
+
return area / 2;
|
|
14
|
+
};
|
|
15
|
+
var describeScaledPolygons = (polygons) => {
|
|
16
|
+
let pointCount = 0;
|
|
17
|
+
const bbox = {
|
|
18
|
+
minX: Number.POSITIVE_INFINITY,
|
|
19
|
+
minY: Number.POSITIVE_INFINITY,
|
|
20
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
21
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
22
|
+
};
|
|
23
|
+
for (const polygon of polygons) {
|
|
24
|
+
pointCount += polygon.length;
|
|
25
|
+
for (const [x, y] of polygon) {
|
|
26
|
+
bbox.minX = Math.min(bbox.minX, x);
|
|
27
|
+
bbox.minY = Math.min(bbox.minY, y);
|
|
28
|
+
bbox.maxX = Math.max(bbox.maxX, x);
|
|
29
|
+
bbox.maxY = Math.max(bbox.maxY, y);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
polygonCount: polygons.length,
|
|
34
|
+
pointCount,
|
|
35
|
+
bbox: pointCount > 0 ? bbox : null,
|
|
36
|
+
scale: MANIFOLD_GEOMETRY_SCALE
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
var assertFinitePoint = (point, operation) => {
|
|
40
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${operation} received non-finite point (${point.x}, ${point.y})`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var pointsEqual = (a, b) => a.x === b.x && a.y === b.y;
|
|
47
|
+
var normalizeRing = (ring, operation = "normalizeRing") => {
|
|
48
|
+
const normalized = [];
|
|
49
|
+
for (const point of ring) {
|
|
50
|
+
assertFinitePoint(point, operation);
|
|
51
|
+
const roundedPoint = {
|
|
52
|
+
x: Math.round(point.x * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
53
|
+
y: Math.round(point.y * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE
|
|
54
|
+
};
|
|
55
|
+
const previous = normalized[normalized.length - 1];
|
|
56
|
+
if (!previous || !pointsEqual(previous, roundedPoint)) {
|
|
57
|
+
normalized.push(roundedPoint);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (normalized.length > 1 && pointsEqual(normalized[0], normalized[normalized.length - 1])) {
|
|
61
|
+
normalized.pop();
|
|
62
|
+
}
|
|
63
|
+
const uniquePoints = new Set(normalized.map((p) => `${p.x},${p.y}`));
|
|
64
|
+
if (uniquePoints.size < 3 || Math.abs(signedArea(normalized)) < 1e-18) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
};
|
|
69
|
+
var toScaledManifoldPolygons = (polygons, operation = "toScaledManifoldPolygons") => {
|
|
70
|
+
const scaledPolygons = [];
|
|
71
|
+
for (const polygon of polygons) {
|
|
72
|
+
const normalized = normalizeRing(polygon, operation);
|
|
73
|
+
if (normalized.length < 3) continue;
|
|
74
|
+
const positiveRing = signedArea(normalized) < 0 ? [...normalized].reverse() : normalized;
|
|
75
|
+
scaledPolygons.push(
|
|
76
|
+
positiveRing.map((p) => [
|
|
77
|
+
Math.round(p.x * MANIFOLD_GEOMETRY_SCALE),
|
|
78
|
+
Math.round(p.y * MANIFOLD_GEOMETRY_SCALE)
|
|
79
|
+
])
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return scaledPolygons;
|
|
83
|
+
};
|
|
84
|
+
var fromScaledManifoldPolygons = (polygons) => polygons.map(
|
|
85
|
+
(polygon) => normalizeRing(
|
|
86
|
+
polygon.map(([x, y]) => ({
|
|
87
|
+
x: x / MANIFOLD_GEOMETRY_SCALE,
|
|
88
|
+
y: y / MANIFOLD_GEOMETRY_SCALE
|
|
89
|
+
})),
|
|
90
|
+
"fromScaledManifoldPolygons"
|
|
91
|
+
)
|
|
92
|
+
).filter((polygon) => polygon.length >= 3);
|
|
93
|
+
|
|
94
|
+
// lib/solvers/copper-pour/generate-brep.ts
|
|
95
|
+
var ensureAreaSign = (ring, desiredSign) => {
|
|
96
|
+
const area = signedArea(ring);
|
|
97
|
+
const shouldReverse = desiredSign === "positive" && area < 0 || desiredSign === "negative" && area > 0;
|
|
98
|
+
return shouldReverse ? [...ring].reverse() : ring;
|
|
99
|
+
};
|
|
100
|
+
var ringToVertices = (ring) => ring.map((point) => ({
|
|
101
|
+
x: point.x,
|
|
102
|
+
y: point.y
|
|
103
|
+
}));
|
|
104
|
+
var generateBRep = (pourIslands) => {
|
|
105
|
+
const brep_shapes = [];
|
|
106
|
+
for (const island of pourIslands) {
|
|
107
|
+
if (island.outerRing.length < 3) continue;
|
|
108
|
+
const outerRing = ensureAreaSign(island.outerRing, "negative");
|
|
109
|
+
const innerRings = island.innerRings.filter((ring) => ring.length >= 3).map((ring) => ensureAreaSign(ring, "positive"));
|
|
110
|
+
brep_shapes.push({
|
|
111
|
+
outer_ring: { vertices: ringToVertices(outerRing) },
|
|
112
|
+
inner_rings: innerRings.map((ring) => ({
|
|
113
|
+
vertices: ringToVertices(ring)
|
|
114
|
+
}))
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return brep_shapes;
|
|
118
|
+
};
|
|
119
|
+
|
|
4
120
|
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
5
|
-
import Flatten from "@flatten-js/core";
|
|
6
121
|
var getBoardPolygon = (region) => {
|
|
7
122
|
const board_edge_margin = region.board_edge_margin ?? 0;
|
|
8
123
|
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;
|
|
124
|
+
return normalizeRing(region.outline, "getBoardPolygon.outline");
|
|
16
125
|
}
|
|
17
126
|
const { bounds } = region;
|
|
18
127
|
const newBounds = {
|
|
@@ -22,38 +131,194 @@ var getBoardPolygon = (region) => {
|
|
|
22
131
|
maxY: bounds.maxY - board_edge_margin
|
|
23
132
|
};
|
|
24
133
|
if (newBounds.minX >= newBounds.maxX || newBounds.minY >= newBounds.maxY) {
|
|
25
|
-
return
|
|
134
|
+
return [];
|
|
26
135
|
}
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
).toPoints()
|
|
34
|
-
);
|
|
136
|
+
return [
|
|
137
|
+
{ x: newBounds.minX, y: newBounds.minY },
|
|
138
|
+
{ x: newBounds.maxX, y: newBounds.minY },
|
|
139
|
+
{ x: newBounds.maxX, y: newBounds.maxY },
|
|
140
|
+
{ x: newBounds.minX, y: newBounds.maxY }
|
|
141
|
+
];
|
|
35
142
|
};
|
|
36
143
|
|
|
37
|
-
// lib/solvers/
|
|
38
|
-
import
|
|
144
|
+
// lib/solvers/copper-pour/manifold-runtime.ts
|
|
145
|
+
import {
|
|
146
|
+
getManifoldModule,
|
|
147
|
+
getManifoldModuleSync
|
|
148
|
+
} from "manifold-3d/lib/wasm.js";
|
|
149
|
+
var manifoldModulePromise = null;
|
|
150
|
+
var initializeManifoldGeometry = async () => {
|
|
151
|
+
if (getManifoldModuleSync()) return;
|
|
152
|
+
manifoldModulePromise ??= getManifoldModule().catch((error) => {
|
|
153
|
+
manifoldModulePromise = null;
|
|
154
|
+
throw error;
|
|
155
|
+
});
|
|
156
|
+
await manifoldModulePromise;
|
|
157
|
+
};
|
|
158
|
+
var isManifoldGeometryInitialized = () => Boolean(getManifoldModuleSync());
|
|
159
|
+
var getCrossSection = () => {
|
|
160
|
+
const manifold = getManifoldModuleSync();
|
|
161
|
+
if (!manifold) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"Manifold geometry has not been initialized. Call initializeManifoldGeometry() before solving copper pours."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return manifold.CrossSection;
|
|
167
|
+
};
|
|
168
|
+
var runManifoldOperation = (operation, polygons, callback) => {
|
|
169
|
+
try {
|
|
170
|
+
return callback();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const details = describeScaledPolygons(polygons);
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
throw new Error(
|
|
175
|
+
`${operation} failed: ${message}; details=${JSON.stringify(details)}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
39
179
|
|
|
40
|
-
// lib/solvers/copper-pour/
|
|
41
|
-
|
|
180
|
+
// lib/solvers/copper-pour/manifold-geometry-adapter.ts
|
|
181
|
+
var DEFAULT_MIN_ISLAND_AREA = 1e-8;
|
|
182
|
+
var crossSectionFromPolygon = (polygon, fillRule = "Positive") => {
|
|
183
|
+
const scaledPolygons = toScaledManifoldPolygons(
|
|
184
|
+
[polygon],
|
|
185
|
+
"crossSectionFromPolygon"
|
|
186
|
+
);
|
|
187
|
+
const CrossSection = getCrossSection();
|
|
188
|
+
if (scaledPolygons.length === 0) {
|
|
189
|
+
return CrossSection.ofPolygons([]);
|
|
190
|
+
}
|
|
191
|
+
return runManifoldOperation(
|
|
192
|
+
"crossSectionFromPolygon",
|
|
193
|
+
scaledPolygons,
|
|
194
|
+
() => CrossSection.ofPolygons(scaledPolygons, fillRule)
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
var crossSectionFromPolygons = (polygons, fillRule = "Positive") => {
|
|
198
|
+
const scaledPolygons = toScaledManifoldPolygons(
|
|
199
|
+
polygons,
|
|
200
|
+
"crossSectionFromPolygons"
|
|
201
|
+
);
|
|
202
|
+
const CrossSection = getCrossSection();
|
|
203
|
+
if (scaledPolygons.length === 0) {
|
|
204
|
+
return CrossSection.ofPolygons([]);
|
|
205
|
+
}
|
|
206
|
+
return runManifoldOperation(
|
|
207
|
+
"crossSectionFromPolygons",
|
|
208
|
+
scaledPolygons,
|
|
209
|
+
() => CrossSection.ofPolygons(scaledPolygons, fillRule)
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
var composeCrossSections = (sections) => {
|
|
213
|
+
const nonEmptySections = sections.filter((section) => !section.isEmpty());
|
|
214
|
+
const CrossSection = getCrossSection();
|
|
215
|
+
if (nonEmptySections.length === 0) {
|
|
216
|
+
return CrossSection.ofPolygons([]);
|
|
217
|
+
}
|
|
218
|
+
return runManifoldOperation(
|
|
219
|
+
"composeCrossSections",
|
|
220
|
+
[],
|
|
221
|
+
() => CrossSection.compose(nonEmptySections)
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
var offsetPolygon = (polygon, margin, joinType = "Miter") => {
|
|
225
|
+
const scaledPolygons = toScaledManifoldPolygons([polygon], "offsetPolygon");
|
|
226
|
+
if (scaledPolygons.length === 0 || margin <= 0) {
|
|
227
|
+
return scaledPolygons.length === 0 ? [] : [normalizeRing(polygon)];
|
|
228
|
+
}
|
|
229
|
+
const scaledMargin = margin * MANIFOLD_GEOMETRY_SCALE;
|
|
230
|
+
const CrossSection = getCrossSection();
|
|
231
|
+
const section = runManifoldOperation(
|
|
232
|
+
"offsetPolygon.input",
|
|
233
|
+
scaledPolygons,
|
|
234
|
+
() => CrossSection.ofPolygons(scaledPolygons, "Positive")
|
|
235
|
+
);
|
|
236
|
+
const offset = runManifoldOperation(
|
|
237
|
+
"offsetPolygon.offset",
|
|
238
|
+
scaledPolygons,
|
|
239
|
+
() => section.offset(scaledMargin, joinType, 2, 32)
|
|
240
|
+
);
|
|
241
|
+
return fromScaledManifoldPolygons(offset.toPolygons());
|
|
242
|
+
};
|
|
243
|
+
var subtractBlockersFromPour = (pourPolygon, blockerPolygons) => {
|
|
244
|
+
const pourSection = crossSectionFromPolygon(pourPolygon);
|
|
245
|
+
const blockerSection = crossSectionFromPolygons(blockerPolygons);
|
|
246
|
+
if (pourSection.isEmpty() || blockerSection.isEmpty()) {
|
|
247
|
+
return pourSection;
|
|
248
|
+
}
|
|
249
|
+
const operationPolygons = [
|
|
250
|
+
...toScaledManifoldPolygons([pourPolygon], "subtractBlockersFromPour.pour"),
|
|
251
|
+
...toScaledManifoldPolygons(
|
|
252
|
+
blockerPolygons,
|
|
253
|
+
"subtractBlockersFromPour.blockers"
|
|
254
|
+
)
|
|
255
|
+
];
|
|
256
|
+
return runManifoldOperation(
|
|
257
|
+
"subtractBlockersFromPour",
|
|
258
|
+
operationPolygons,
|
|
259
|
+
() => pourSection.subtract(blockerSection)
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
var removeTinyIslands = (section, minArea = DEFAULT_MIN_ISLAND_AREA) => {
|
|
263
|
+
if (section.isEmpty()) return section;
|
|
264
|
+
const minScaledArea = minArea * MANIFOLD_GEOMETRY_SCALE * MANIFOLD_GEOMETRY_SCALE;
|
|
265
|
+
const islands = section.decompose().filter((island) => Math.abs(island.area()) >= minScaledArea);
|
|
266
|
+
return composeCrossSections(islands);
|
|
267
|
+
};
|
|
268
|
+
var crossSectionToCopperPourIslands = (section) => {
|
|
269
|
+
const islands = [];
|
|
270
|
+
for (const island of section.decompose()) {
|
|
271
|
+
const rings = fromScaledManifoldPolygons(island.toPolygons());
|
|
272
|
+
if (rings.length === 0) continue;
|
|
273
|
+
const outerRing = rings.reduce(
|
|
274
|
+
(largest, ring) => Math.abs(signedArea(ring)) > Math.abs(signedArea(largest)) ? ring : largest
|
|
275
|
+
);
|
|
276
|
+
const innerRings = rings.filter((ring) => ring !== outerRing);
|
|
277
|
+
islands.push({
|
|
278
|
+
outerRing,
|
|
279
|
+
innerRings
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return islands;
|
|
283
|
+
};
|
|
42
284
|
|
|
43
|
-
// lib/solvers/copper-pour/
|
|
44
|
-
|
|
45
|
-
var circleToPolygon = (circle, numSegments = 32) => {
|
|
285
|
+
// lib/solvers/copper-pour/polygon-primitives.ts
|
|
286
|
+
var circleToPolygon = (center, radius, numSegments = 32) => {
|
|
46
287
|
const points = [];
|
|
47
288
|
for (let i = 0; i < numSegments; i++) {
|
|
48
289
|
const angle = i / numSegments * 2 * Math.PI;
|
|
49
|
-
points.push(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
);
|
|
290
|
+
points.push({
|
|
291
|
+
x: center.x + radius * Math.cos(angle),
|
|
292
|
+
y: center.y + radius * Math.sin(angle)
|
|
293
|
+
});
|
|
55
294
|
}
|
|
56
|
-
return
|
|
295
|
+
return points;
|
|
296
|
+
};
|
|
297
|
+
var boxToPolygon = (minX, minY, maxX, maxY) => [
|
|
298
|
+
{ x: minX, y: minY },
|
|
299
|
+
{ x: maxX, y: minY },
|
|
300
|
+
{ x: maxX, y: maxY },
|
|
301
|
+
{ x: minX, y: maxY }
|
|
302
|
+
];
|
|
303
|
+
var segmentToPolygon = (start, end, width) => {
|
|
304
|
+
const segmentLength = Math.hypot(start.x - end.x, start.y - end.y);
|
|
305
|
+
if (segmentLength === 0) return [];
|
|
306
|
+
const centerX = (start.x + end.x) / 2;
|
|
307
|
+
const centerY = (start.y + end.y) / 2;
|
|
308
|
+
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
309
|
+
const cosAngle = Math.cos(angle);
|
|
310
|
+
const sinAngle = Math.sin(angle);
|
|
311
|
+
const halfLength = segmentLength / 2;
|
|
312
|
+
const halfWidth = width / 2;
|
|
313
|
+
return [
|
|
314
|
+
{ x: -halfLength, y: -halfWidth },
|
|
315
|
+
{ x: halfLength, y: -halfWidth },
|
|
316
|
+
{ x: halfLength, y: halfWidth },
|
|
317
|
+
{ x: -halfLength, y: halfWidth }
|
|
318
|
+
].map((point) => ({
|
|
319
|
+
x: centerX + point.x * cosAngle - point.y * sinAngle,
|
|
320
|
+
y: centerY + point.x * sinAngle + point.y * cosAngle
|
|
321
|
+
}));
|
|
57
322
|
};
|
|
58
323
|
|
|
59
324
|
// lib/solvers/copper-pour/process-obstacles.ts
|
|
@@ -65,61 +330,38 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
65
330
|
const polygonsToSubtract = [];
|
|
66
331
|
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins;
|
|
67
332
|
if (boardOutline && boardOutline.length > 0 && board_edge_margin && board_edge_margin > 0) {
|
|
68
|
-
const
|
|
69
|
-
boardOutline
|
|
333
|
+
const vertices = normalizeRing(
|
|
334
|
+
boardOutline,
|
|
335
|
+
"processObstacles.boardOutline"
|
|
70
336
|
);
|
|
71
|
-
if (boardPoly.area() < 0) {
|
|
72
|
-
boardPoly.reverse();
|
|
73
|
-
}
|
|
74
|
-
const vertices = boardPoly.vertices;
|
|
75
337
|
for (let i = 0; i < vertices.length; i++) {
|
|
76
338
|
const p1 = vertices[i === 0 ? vertices.length - 1 : i - 1];
|
|
77
339
|
const p2 = vertices[i];
|
|
78
340
|
const p3 = vertices[(i + 1) % vertices.length];
|
|
79
341
|
if (!p1 || !p2 || !p3) continue;
|
|
80
|
-
const v1 =
|
|
81
|
-
const v2 =
|
|
82
|
-
const crossProduct = v1.
|
|
83
|
-
|
|
84
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
342
|
+
const v1 = { x: p2.x - p1.x, y: p2.y - p1.y };
|
|
343
|
+
const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
|
344
|
+
const crossProduct = v1.x * v2.y - v1.y * v2.x;
|
|
345
|
+
polygonsToSubtract.push(circleToPolygon(p2, board_edge_margin));
|
|
85
346
|
if (crossProduct < 0) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
347
|
+
polygonsToSubtract.push(
|
|
348
|
+
boxToPolygon(
|
|
349
|
+
p2.x - board_edge_margin,
|
|
350
|
+
p2.y - board_edge_margin,
|
|
351
|
+
p2.x + board_edge_margin,
|
|
352
|
+
p2.y + board_edge_margin
|
|
353
|
+
)
|
|
91
354
|
);
|
|
92
|
-
polygonsToSubtract.push(new Flatten3.Polygon(box.toPoints()));
|
|
93
355
|
}
|
|
94
356
|
}
|
|
95
357
|
for (let i = 0; i < vertices.length; i++) {
|
|
96
358
|
const p1 = vertices[i];
|
|
97
359
|
const p2 = vertices[(i + 1) % vertices.length];
|
|
98
360
|
if (!p1 || !p2) continue;
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const centerY = (p1.y + p2.y) / 2;
|
|
104
|
-
const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
|
|
105
|
-
const w2 = segmentLength / 2;
|
|
106
|
-
const h2 = enlargedWidth / 2;
|
|
107
|
-
const angleRad = rotationDeg * Math.PI / 180;
|
|
108
|
-
const cosAngle = Math.cos(angleRad);
|
|
109
|
-
const sinAngle = Math.sin(angleRad);
|
|
110
|
-
const corners = [
|
|
111
|
-
{ x: -w2, y: -h2 },
|
|
112
|
-
{ x: w2, y: -h2 },
|
|
113
|
-
{ x: w2, y: h2 },
|
|
114
|
-
{ x: -w2, y: h2 }
|
|
115
|
-
];
|
|
116
|
-
const rotatedCorners = corners.map((p) => ({
|
|
117
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
118
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
119
|
-
}));
|
|
120
|
-
polygonsToSubtract.push(
|
|
121
|
-
new Flatten3.Polygon(rotatedCorners.map((p) => Flatten3.point(p.x, p.y)))
|
|
122
|
-
);
|
|
361
|
+
const segmentPolygon = segmentToPolygon(p1, p2, board_edge_margin * 2);
|
|
362
|
+
if (segmentPolygon.length > 0) {
|
|
363
|
+
polygonsToSubtract.push(segmentPolygon);
|
|
364
|
+
}
|
|
123
365
|
}
|
|
124
366
|
}
|
|
125
367
|
for (const pad of pads) {
|
|
@@ -130,23 +372,22 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
130
372
|
const isHoleOrCutout = pad.connectivityKey.startsWith("hole:") || pad.connectivityKey.startsWith("cutout:");
|
|
131
373
|
if (isCircularPad(pad)) {
|
|
132
374
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
pad.radius + margin
|
|
375
|
+
polygonsToSubtract.push(
|
|
376
|
+
circleToPolygon({ x: pad.x, y: pad.y }, pad.radius + margin)
|
|
136
377
|
);
|
|
137
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
138
378
|
continue;
|
|
139
379
|
}
|
|
140
380
|
if (isRectPad(pad)) {
|
|
141
381
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
142
382
|
const { bounds } = pad;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
383
|
+
polygonsToSubtract.push(
|
|
384
|
+
boxToPolygon(
|
|
385
|
+
bounds.minX - margin,
|
|
386
|
+
bounds.minY - margin,
|
|
387
|
+
bounds.maxX + margin,
|
|
388
|
+
bounds.maxY + margin
|
|
389
|
+
)
|
|
148
390
|
);
|
|
149
|
-
polygonsToSubtract.push(new Flatten3.Polygon(b.toPoints()));
|
|
150
391
|
continue;
|
|
151
392
|
}
|
|
152
393
|
if (isPolygonPad(pad)) {
|
|
@@ -161,132 +402,39 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
161
402
|
return true;
|
|
162
403
|
});
|
|
163
404
|
if (uniquePoints.length < 3) continue;
|
|
164
|
-
const polygon =
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
if (Math.abs(polygon.area()) < 1e-9) continue;
|
|
405
|
+
const polygon = normalizeRing(uniquePoints, "processObstacles.polygonPad");
|
|
406
|
+
if (polygon.length < 3) continue;
|
|
168
407
|
if (margin <= 0) {
|
|
169
408
|
polygonsToSubtract.push(polygon);
|
|
170
409
|
continue;
|
|
171
410
|
}
|
|
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
|
-
}
|
|
411
|
+
polygonsToSubtract.push(...offsetPolygon(polygon, margin));
|
|
199
412
|
continue;
|
|
200
413
|
}
|
|
201
414
|
if (isTracePad(pad)) {
|
|
202
415
|
for (const segment of pad.segments) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pad.width / 2 + traceMargin
|
|
416
|
+
polygonsToSubtract.push(
|
|
417
|
+
circleToPolygon(segment, pad.width / 2 + traceMargin)
|
|
206
418
|
);
|
|
207
|
-
polygonsToSubtract.push(circleToPolygon(circle));
|
|
208
419
|
}
|
|
209
420
|
for (let i = 0; i < pad.segments.length - 1; i++) {
|
|
210
421
|
const p1 = pad.segments[i];
|
|
211
422
|
const p2 = pad.segments[i + 1];
|
|
212
423
|
if (!p1 || !p2) continue;
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const centerY = (p1.y + p2.y) / 2;
|
|
218
|
-
const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
|
|
219
|
-
const w2 = segmentLength / 2;
|
|
220
|
-
const h2 = enlargedWidth / 2;
|
|
221
|
-
const angleRad = rotationDeg * Math.PI / 180;
|
|
222
|
-
const cosAngle = Math.cos(angleRad);
|
|
223
|
-
const sinAngle = Math.sin(angleRad);
|
|
224
|
-
const corners = [
|
|
225
|
-
{ x: -w2, y: -h2 },
|
|
226
|
-
{ x: w2, y: -h2 },
|
|
227
|
-
{ x: w2, y: h2 },
|
|
228
|
-
{ x: -w2, y: h2 }
|
|
229
|
-
];
|
|
230
|
-
const rotatedCorners = corners.map((p) => ({
|
|
231
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
232
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
233
|
-
}));
|
|
234
|
-
polygonsToSubtract.push(
|
|
235
|
-
new Flatten3.Polygon(
|
|
236
|
-
rotatedCorners.map((p) => Flatten3.point(p.x, p.y))
|
|
237
|
-
)
|
|
424
|
+
const segmentPolygon = segmentToPolygon(
|
|
425
|
+
p1,
|
|
426
|
+
p2,
|
|
427
|
+
pad.width + traceMargin * 2
|
|
238
428
|
);
|
|
429
|
+
if (segmentPolygon.length > 0) {
|
|
430
|
+
polygonsToSubtract.push(segmentPolygon);
|
|
431
|
+
}
|
|
239
432
|
}
|
|
240
433
|
}
|
|
241
434
|
}
|
|
242
435
|
return { polygonsToSubtract };
|
|
243
436
|
};
|
|
244
437
|
|
|
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
438
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
291
439
|
var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
292
440
|
constructor(input) {
|
|
@@ -299,6 +447,11 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
299
447
|
return "CopperPourPipelineSolver";
|
|
300
448
|
}
|
|
301
449
|
getOutput() {
|
|
450
|
+
if (!isManifoldGeometryInitialized()) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
"Manifold geometry has not been initialized. Call initializeManifoldGeometry() before solving copper pours."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
302
455
|
const brep_shapes = [];
|
|
303
456
|
for (const region of this.input.regionsForPour) {
|
|
304
457
|
const boardPolygon = getBoardPolygon(region);
|
|
@@ -316,23 +469,11 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
316
469
|
},
|
|
317
470
|
region.outline
|
|
318
471
|
);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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;
|
|
334
|
-
}
|
|
335
|
-
const new_breps = generateBRep(pourPolygons);
|
|
472
|
+
const finalPour = removeTinyIslands(
|
|
473
|
+
subtractBlockersFromPour(boardPolygon, polygonsToSubtract)
|
|
474
|
+
);
|
|
475
|
+
const pourIslands = crossSectionToCopperPourIslands(finalPour);
|
|
476
|
+
const new_breps = generateBRep(pourIslands);
|
|
336
477
|
brep_shapes.push(...new_breps);
|
|
337
478
|
}
|
|
338
479
|
return {
|
|
@@ -559,5 +700,6 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
559
700
|
};
|
|
560
701
|
export {
|
|
561
702
|
CopperPourPipelineSolver,
|
|
562
|
-
convertCircuitJsonToInputProblem
|
|
703
|
+
convertCircuitJsonToInputProblem,
|
|
704
|
+
initializeManifoldGeometry
|
|
563
705
|
};
|
package/lib/index.ts
CHANGED