@tscircuit/copper-pour-solver 0.0.25 → 0.0.27
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 +135 -147
- package/lib/index.ts +1 -0
- package/lib/solvers/CopperPourPipelineSolver.ts +7 -24
- package/lib/solvers/copper-pour/generate-brep.ts +2 -11
- package/lib/solvers/copper-pour/get-board-polygon.ts +1 -2
- package/lib/solvers/copper-pour/manifold-geometry-adapter.ts +26 -173
- 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 +18 -91
- package/package.json +1 -1
- package/tests/fixtures/preload.ts +3 -0
- package/tests/stm32f746g-disco.test.ts +2 -2
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,7 +1,8 @@
|
|
|
1
1
|
// lib/solvers/CopperPourPipelineSolver.ts
|
|
2
2
|
import { BasePipelineSolver } from "@tscircuit/solver-utils";
|
|
3
3
|
|
|
4
|
-
// lib/solvers/copper-pour/
|
|
4
|
+
// lib/solvers/copper-pour/polygon-ring.ts
|
|
5
|
+
var MANIFOLD_GEOMETRY_SCALE = 1e6;
|
|
5
6
|
var signedArea = (ring) => {
|
|
6
7
|
let area = 0;
|
|
7
8
|
for (let i = 0; i < ring.length; i++) {
|
|
@@ -11,40 +12,7 @@ var signedArea = (ring) => {
|
|
|
11
12
|
}
|
|
12
13
|
return area / 2;
|
|
13
14
|
};
|
|
14
|
-
var
|
|
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) => {
|
|
15
|
+
var describeScaledPolygons = (polygons) => {
|
|
48
16
|
let pointCount = 0;
|
|
49
17
|
const bbox = {
|
|
50
18
|
minX: Number.POSITIVE_INFINITY,
|
|
@@ -76,15 +44,6 @@ var assertFinitePoint = (point, operation) => {
|
|
|
76
44
|
}
|
|
77
45
|
};
|
|
78
46
|
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
47
|
var normalizeRing = (ring, operation = "normalizeRing") => {
|
|
89
48
|
const normalized = [];
|
|
90
49
|
for (const point of ring) {
|
|
@@ -102,7 +61,7 @@ var normalizeRing = (ring, operation = "normalizeRing") => {
|
|
|
102
61
|
normalized.pop();
|
|
103
62
|
}
|
|
104
63
|
const uniquePoints = new Set(normalized.map((p) => `${p.x},${p.y}`));
|
|
105
|
-
if (uniquePoints.size < 3 || Math.abs(
|
|
64
|
+
if (uniquePoints.size < 3 || Math.abs(signedArea(normalized)) < 1e-18) {
|
|
106
65
|
return [];
|
|
107
66
|
}
|
|
108
67
|
return normalized;
|
|
@@ -112,7 +71,7 @@ var toScaledManifoldPolygons = (polygons, operation = "toScaledManifoldPolygons"
|
|
|
112
71
|
for (const polygon of polygons) {
|
|
113
72
|
const normalized = normalizeRing(polygon, operation);
|
|
114
73
|
if (normalized.length < 3) continue;
|
|
115
|
-
const positiveRing =
|
|
74
|
+
const positiveRing = signedArea(normalized) < 0 ? [...normalized].reverse() : normalized;
|
|
116
75
|
scaledPolygons.push(
|
|
117
76
|
positiveRing.map((p) => [
|
|
118
77
|
Math.round(p.x * MANIFOLD_GEOMETRY_SCALE),
|
|
@@ -131,22 +90,101 @@ var fromScaledManifoldPolygons = (polygons) => polygons.map(
|
|
|
131
90
|
"fromScaledManifoldPolygons"
|
|
132
91
|
)
|
|
133
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
|
+
|
|
120
|
+
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
121
|
+
var getBoardPolygon = (region) => {
|
|
122
|
+
const board_edge_margin = region.board_edge_margin ?? 0;
|
|
123
|
+
if (region.outline && region.outline.length > 0) {
|
|
124
|
+
return normalizeRing(region.outline, "getBoardPolygon.outline");
|
|
125
|
+
}
|
|
126
|
+
const { bounds } = region;
|
|
127
|
+
const newBounds = {
|
|
128
|
+
minX: bounds.minX + board_edge_margin,
|
|
129
|
+
minY: bounds.minY + board_edge_margin,
|
|
130
|
+
maxX: bounds.maxX - board_edge_margin,
|
|
131
|
+
maxY: bounds.maxY - board_edge_margin
|
|
132
|
+
};
|
|
133
|
+
if (newBounds.minX >= newBounds.maxX || newBounds.minY >= newBounds.maxY) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
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
|
+
];
|
|
142
|
+
};
|
|
143
|
+
|
|
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
|
+
};
|
|
134
168
|
var runManifoldOperation = (operation, polygons, callback) => {
|
|
135
169
|
try {
|
|
136
170
|
return callback();
|
|
137
171
|
} catch (error) {
|
|
138
|
-
const details =
|
|
172
|
+
const details = describeScaledPolygons(polygons);
|
|
139
173
|
const message = error instanceof Error ? error.message : String(error);
|
|
140
174
|
throw new Error(
|
|
141
175
|
`${operation} failed: ${message}; details=${JSON.stringify(details)}`
|
|
142
176
|
);
|
|
143
177
|
}
|
|
144
178
|
};
|
|
179
|
+
|
|
180
|
+
// lib/solvers/copper-pour/manifold-geometry-adapter.ts
|
|
181
|
+
var DEFAULT_MIN_ISLAND_AREA = 1e-8;
|
|
145
182
|
var crossSectionFromPolygon = (polygon, fillRule = "Positive") => {
|
|
146
183
|
const scaledPolygons = toScaledManifoldPolygons(
|
|
147
184
|
[polygon],
|
|
148
185
|
"crossSectionFromPolygon"
|
|
149
186
|
);
|
|
187
|
+
const CrossSection = getCrossSection();
|
|
150
188
|
if (scaledPolygons.length === 0) {
|
|
151
189
|
return CrossSection.ofPolygons([]);
|
|
152
190
|
}
|
|
@@ -161,6 +199,7 @@ var crossSectionFromPolygons = (polygons, fillRule = "Positive") => {
|
|
|
161
199
|
polygons,
|
|
162
200
|
"crossSectionFromPolygons"
|
|
163
201
|
);
|
|
202
|
+
const CrossSection = getCrossSection();
|
|
164
203
|
if (scaledPolygons.length === 0) {
|
|
165
204
|
return CrossSection.ofPolygons([]);
|
|
166
205
|
}
|
|
@@ -172,6 +211,7 @@ var crossSectionFromPolygons = (polygons, fillRule = "Positive") => {
|
|
|
172
211
|
};
|
|
173
212
|
var composeCrossSections = (sections) => {
|
|
174
213
|
const nonEmptySections = sections.filter((section) => !section.isEmpty());
|
|
214
|
+
const CrossSection = getCrossSection();
|
|
175
215
|
if (nonEmptySections.length === 0) {
|
|
176
216
|
return CrossSection.ofPolygons([]);
|
|
177
217
|
}
|
|
@@ -187,6 +227,7 @@ var offsetPolygon = (polygon, margin, joinType = "Miter") => {
|
|
|
187
227
|
return scaledPolygons.length === 0 ? [] : [normalizeRing(polygon)];
|
|
188
228
|
}
|
|
189
229
|
const scaledMargin = margin * MANIFOLD_GEOMETRY_SCALE;
|
|
230
|
+
const CrossSection = getCrossSection();
|
|
190
231
|
const section = runManifoldOperation(
|
|
191
232
|
"offsetPolygon.input",
|
|
192
233
|
scaledPolygons,
|
|
@@ -230,7 +271,7 @@ var crossSectionToCopperPourIslands = (section) => {
|
|
|
230
271
|
const rings = fromScaledManifoldPolygons(island.toPolygons());
|
|
231
272
|
if (rings.length === 0) continue;
|
|
232
273
|
const outerRing = rings.reduce(
|
|
233
|
-
(largest, ring) => Math.abs(
|
|
274
|
+
(largest, ring) => Math.abs(signedArea(ring)) > Math.abs(signedArea(largest)) ? ring : largest
|
|
234
275
|
);
|
|
235
276
|
const innerRings = rings.filter((ring) => ring !== outerRing);
|
|
236
277
|
islands.push({
|
|
@@ -240,40 +281,8 @@ var crossSectionToCopperPourIslands = (section) => {
|
|
|
240
281
|
}
|
|
241
282
|
return islands;
|
|
242
283
|
};
|
|
243
|
-
var geometryDebugSummary = (label, polygons) => ({
|
|
244
|
-
label,
|
|
245
|
-
...describePolygons(toScaledManifoldPolygons(polygons, label))
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// lib/solvers/copper-pour/get-board-polygon.ts
|
|
249
|
-
var getBoardPolygon = (region) => {
|
|
250
|
-
const board_edge_margin = region.board_edge_margin ?? 0;
|
|
251
|
-
if (region.outline && region.outline.length > 0) {
|
|
252
|
-
return normalizeRing(region.outline, "getBoardPolygon.outline");
|
|
253
|
-
}
|
|
254
|
-
const { bounds } = region;
|
|
255
|
-
const newBounds = {
|
|
256
|
-
minX: bounds.minX + board_edge_margin,
|
|
257
|
-
minY: bounds.minY + board_edge_margin,
|
|
258
|
-
maxX: bounds.maxX - board_edge_margin,
|
|
259
|
-
maxY: bounds.maxY - board_edge_margin
|
|
260
|
-
};
|
|
261
|
-
if (newBounds.minX >= newBounds.maxX || newBounds.minY >= newBounds.maxY) {
|
|
262
|
-
return [];
|
|
263
|
-
}
|
|
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
|
-
];
|
|
270
|
-
};
|
|
271
284
|
|
|
272
|
-
// lib/solvers/copper-pour/
|
|
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";
|
|
285
|
+
// lib/solvers/copper-pour/polygon-primitives.ts
|
|
277
286
|
var circleToPolygon = (center, radius, numSegments = 32) => {
|
|
278
287
|
const points = [];
|
|
279
288
|
for (let i = 0; i < numSegments; i++) {
|
|
@@ -291,6 +300,32 @@ var boxToPolygon = (minX, minY, maxX, maxY) => [
|
|
|
291
300
|
{ x: maxX, y: maxY },
|
|
292
301
|
{ x: minX, y: maxY }
|
|
293
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
|
+
}));
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// lib/solvers/copper-pour/process-obstacles.ts
|
|
325
|
+
var isRectPad = (pad) => pad.shape === "rect";
|
|
326
|
+
var isTracePad = (pad) => pad.shape === "trace";
|
|
327
|
+
var isCircularPad = (pad) => pad.shape === "circle";
|
|
328
|
+
var isPolygonPad = (pad) => pad.shape === "polygon";
|
|
294
329
|
var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline) => {
|
|
295
330
|
const polygonsToSubtract = [];
|
|
296
331
|
const { padMargin, traceMargin, board_edge_margin, cutoutMargin } = margins;
|
|
@@ -323,28 +358,10 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
323
358
|
const p1 = vertices[i];
|
|
324
359
|
const p2 = vertices[(i + 1) % vertices.length];
|
|
325
360
|
if (!p1 || !p2) continue;
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const centerY = (p1.y + p2.y) / 2;
|
|
331
|
-
const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
|
|
332
|
-
const w2 = segmentLength / 2;
|
|
333
|
-
const h2 = enlargedWidth / 2;
|
|
334
|
-
const angleRad = rotationDeg * Math.PI / 180;
|
|
335
|
-
const cosAngle = Math.cos(angleRad);
|
|
336
|
-
const sinAngle = Math.sin(angleRad);
|
|
337
|
-
const corners = [
|
|
338
|
-
{ x: -w2, y: -h2 },
|
|
339
|
-
{ x: w2, y: -h2 },
|
|
340
|
-
{ x: w2, y: h2 },
|
|
341
|
-
{ x: -w2, y: h2 }
|
|
342
|
-
];
|
|
343
|
-
const rotatedCorners = corners.map((p) => ({
|
|
344
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
345
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
346
|
-
}));
|
|
347
|
-
polygonsToSubtract.push(rotatedCorners);
|
|
361
|
+
const segmentPolygon = segmentToPolygon(p1, p2, board_edge_margin * 2);
|
|
362
|
+
if (segmentPolygon.length > 0) {
|
|
363
|
+
polygonsToSubtract.push(segmentPolygon);
|
|
364
|
+
}
|
|
348
365
|
}
|
|
349
366
|
}
|
|
350
367
|
for (const pad of pads) {
|
|
@@ -404,28 +421,14 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
404
421
|
const p1 = pad.segments[i];
|
|
405
422
|
const p2 = pad.segments[i + 1];
|
|
406
423
|
if (!p1 || !p2) continue;
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const angleRad = rotationDeg * Math.PI / 180;
|
|
416
|
-
const cosAngle = Math.cos(angleRad);
|
|
417
|
-
const sinAngle = Math.sin(angleRad);
|
|
418
|
-
const corners = [
|
|
419
|
-
{ x: -w2, y: -h2 },
|
|
420
|
-
{ x: w2, y: -h2 },
|
|
421
|
-
{ x: w2, y: h2 },
|
|
422
|
-
{ x: -w2, y: h2 }
|
|
423
|
-
];
|
|
424
|
-
const rotatedCorners = corners.map((p) => ({
|
|
425
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
426
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle
|
|
427
|
-
}));
|
|
428
|
-
polygonsToSubtract.push(rotatedCorners);
|
|
424
|
+
const segmentPolygon = segmentToPolygon(
|
|
425
|
+
p1,
|
|
426
|
+
p2,
|
|
427
|
+
pad.width + traceMargin * 2
|
|
428
|
+
);
|
|
429
|
+
if (segmentPolygon.length > 0) {
|
|
430
|
+
polygonsToSubtract.push(segmentPolygon);
|
|
431
|
+
}
|
|
429
432
|
}
|
|
430
433
|
}
|
|
431
434
|
}
|
|
@@ -444,6 +447,11 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
444
447
|
return "CopperPourPipelineSolver";
|
|
445
448
|
}
|
|
446
449
|
getOutput() {
|
|
450
|
+
if (!isManifoldGeometryInitialized()) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
"Manifold geometry has not been initialized. Call initializeManifoldGeometry() before solving copper pours."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
447
455
|
const brep_shapes = [];
|
|
448
456
|
for (const region of this.input.regionsForPour) {
|
|
449
457
|
const boardPolygon = getBoardPolygon(region);
|
|
@@ -461,31 +469,10 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
461
469
|
},
|
|
462
470
|
region.outline
|
|
463
471
|
);
|
|
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
472
|
const finalPour = removeTinyIslands(
|
|
473
473
|
subtractBlockersFromPour(boardPolygon, polygonsToSubtract)
|
|
474
474
|
);
|
|
475
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
|
-
);
|
|
488
|
-
}
|
|
489
476
|
const new_breps = generateBRep(pourIslands);
|
|
490
477
|
brep_shapes.push(...new_breps);
|
|
491
478
|
}
|
|
@@ -713,5 +700,6 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
713
700
|
};
|
|
714
701
|
export {
|
|
715
702
|
CopperPourPipelineSolver,
|
|
716
|
-
convertCircuitJsonToInputProblem
|
|
703
|
+
convertCircuitJsonToInputProblem,
|
|
704
|
+
initializeManifoldGeometry
|
|
717
705
|
};
|
package/lib/index.ts
CHANGED
|
@@ -5,10 +5,10 @@ import { generateBRep } from "./copper-pour/generate-brep"
|
|
|
5
5
|
import { getBoardPolygon } from "./copper-pour/get-board-polygon"
|
|
6
6
|
import {
|
|
7
7
|
crossSectionToCopperPourIslands,
|
|
8
|
-
geometryDebugSummary,
|
|
9
8
|
removeTinyIslands,
|
|
10
9
|
subtractBlockersFromPour,
|
|
11
10
|
} from "./copper-pour/manifold-geometry-adapter"
|
|
11
|
+
import { isManifoldGeometryInitialized } from "./copper-pour/manifold-runtime"
|
|
12
12
|
import { processObstaclesForPour } from "./copper-pour/process-obstacles"
|
|
13
13
|
|
|
14
14
|
export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
@@ -22,6 +22,12 @@ export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
override getOutput(): PipelineOutput {
|
|
25
|
+
if (!isManifoldGeometryInitialized()) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Manifold geometry has not been initialized. Call initializeManifoldGeometry() before solving copper pours.",
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
const brep_shapes: BRepShape[] = []
|
|
26
32
|
|
|
27
33
|
for (const region of this.input.regionsForPour) {
|
|
@@ -43,34 +49,11 @@ export class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem> {
|
|
|
43
49
|
region.outline,
|
|
44
50
|
)
|
|
45
51
|
|
|
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
52
|
const finalPour = removeTinyIslands(
|
|
56
53
|
subtractBlockersFromPour(boardPolygon, polygonsToSubtract),
|
|
57
54
|
)
|
|
58
55
|
const pourIslands = crossSectionToCopperPourIslands(finalPour)
|
|
59
56
|
|
|
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
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
57
|
const new_breps = generateBRep(pourIslands)
|
|
75
58
|
brep_shapes.push(...new_breps)
|
|
76
59
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { BRepShape } from "circuit-json"
|
|
2
|
-
import type { CopperPourIsland
|
|
3
|
-
|
|
4
|
-
const signedArea = (ring: PolygonRing) => {
|
|
5
|
-
let area = 0
|
|
6
|
-
for (let i = 0; i < ring.length; i++) {
|
|
7
|
-
const current = ring[i]!
|
|
8
|
-
const next = ring[(i + 1) % ring.length]!
|
|
9
|
-
area += current.x * next.y - next.x * current.y
|
|
10
|
-
}
|
|
11
|
-
return area / 2
|
|
12
|
-
}
|
|
2
|
+
import type { CopperPourIsland } from "./manifold-geometry-adapter"
|
|
3
|
+
import { signedArea, type PolygonRing } from "./polygon-ring"
|
|
13
4
|
|
|
14
5
|
const ensureAreaSign = (
|
|
15
6
|
ring: PolygonRing,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { InputPourRegion } from "lib/types"
|
|
2
|
-
import type
|
|
3
|
-
import { normalizeRing } from "./manifold-geometry-adapter"
|
|
2
|
+
import { normalizeRing, type PolygonRing } from "./polygon-ring"
|
|
4
3
|
|
|
5
4
|
export const getBoardPolygon = (region: InputPourRegion): PolygonRing => {
|
|
6
5
|
const board_edge_margin = region.board_edge_margin ?? 0
|
|
@@ -1,176 +1,34 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "manifold-
|
|
1
|
+
import type { FillRule } from "manifold-3d"
|
|
2
|
+
import {
|
|
3
|
+
getCrossSection,
|
|
4
|
+
runManifoldOperation,
|
|
5
|
+
type CrossSection,
|
|
6
|
+
} from "./manifold-runtime"
|
|
7
|
+
import {
|
|
8
|
+
fromScaledManifoldPolygons,
|
|
9
|
+
MANIFOLD_GEOMETRY_SCALE,
|
|
10
|
+
normalizeRing,
|
|
11
|
+
signedArea,
|
|
12
|
+
toScaledManifoldPolygons,
|
|
13
|
+
type PolygonRing,
|
|
14
|
+
} from "./polygon-ring"
|
|
7
15
|
|
|
8
|
-
const manifoldModule = await import("manifold-3d")
|
|
9
|
-
const manifoldFactory = manifoldModule.default as unknown as () => Promise<{
|
|
10
|
-
CrossSection: typeof CrossSectionType
|
|
11
|
-
setup: () => void
|
|
12
|
-
}>
|
|
13
|
-
const manifold = await manifoldFactory()
|
|
14
|
-
manifold.setup()
|
|
15
|
-
|
|
16
|
-
const { CrossSection } = manifold
|
|
17
|
-
|
|
18
|
-
export const MANIFOLD_GEOMETRY_SCALE = 1_000_000
|
|
19
16
|
export const DEFAULT_MIN_ISLAND_AREA = 1e-8
|
|
20
17
|
|
|
21
|
-
export type PolygonRing = Point[]
|
|
22
|
-
type ScaledPolygons = SimplePolygon[]
|
|
23
18
|
export type CopperPourIsland = {
|
|
24
19
|
outerRing: PolygonRing
|
|
25
20
|
innerRings: PolygonRing[]
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
const describePolygons = (polygons: ScaledPolygons) => {
|
|
29
|
-
let pointCount = 0
|
|
30
|
-
const bbox = {
|
|
31
|
-
minX: Number.POSITIVE_INFINITY,
|
|
32
|
-
minY: Number.POSITIVE_INFINITY,
|
|
33
|
-
maxX: Number.NEGATIVE_INFINITY,
|
|
34
|
-
maxY: Number.NEGATIVE_INFINITY,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const polygon of polygons) {
|
|
38
|
-
pointCount += polygon.length
|
|
39
|
-
for (const [x, y] of polygon) {
|
|
40
|
-
bbox.minX = Math.min(bbox.minX, x)
|
|
41
|
-
bbox.minY = Math.min(bbox.minY, y)
|
|
42
|
-
bbox.maxX = Math.max(bbox.maxX, x)
|
|
43
|
-
bbox.maxY = Math.max(bbox.maxY, y)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
polygonCount: polygons.length,
|
|
49
|
-
pointCount,
|
|
50
|
-
bbox: pointCount > 0 ? bbox : null,
|
|
51
|
-
scale: MANIFOLD_GEOMETRY_SCALE,
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const assertFinitePoint = (point: Point, operation: string) => {
|
|
56
|
-
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`${operation} received non-finite point (${point.x}, ${point.y})`,
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const pointsEqual = (a: Point, b: Point) => a.x === b.x && a.y === b.y
|
|
64
|
-
|
|
65
|
-
const signedArea = (ring: PolygonRing) => {
|
|
66
|
-
let area = 0
|
|
67
|
-
for (let i = 0; i < ring.length; i++) {
|
|
68
|
-
const current = ring[i]!
|
|
69
|
-
const next = ring[(i + 1) % ring.length]!
|
|
70
|
-
area += current.x * next.y - next.x * current.y
|
|
71
|
-
}
|
|
72
|
-
return area / 2
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const normalizeRing = (
|
|
76
|
-
ring: PolygonRing,
|
|
77
|
-
operation = "normalizeRing",
|
|
78
|
-
): PolygonRing => {
|
|
79
|
-
const normalized: PolygonRing = []
|
|
80
|
-
|
|
81
|
-
for (const point of ring) {
|
|
82
|
-
assertFinitePoint(point, operation)
|
|
83
|
-
const roundedPoint = {
|
|
84
|
-
x:
|
|
85
|
-
Math.round(point.x * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
86
|
-
y:
|
|
87
|
-
Math.round(point.y * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
88
|
-
}
|
|
89
|
-
const previous = normalized[normalized.length - 1]
|
|
90
|
-
if (!previous || !pointsEqual(previous, roundedPoint)) {
|
|
91
|
-
normalized.push(roundedPoint)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
normalized.length > 1 &&
|
|
97
|
-
pointsEqual(normalized[0]!, normalized[normalized.length - 1]!)
|
|
98
|
-
) {
|
|
99
|
-
normalized.pop()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const uniquePoints = new Set(normalized.map((p) => `${p.x},${p.y}`))
|
|
103
|
-
if (uniquePoints.size < 3 || Math.abs(signedArea(normalized)) < 1e-18) {
|
|
104
|
-
return []
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return normalized
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Manifold owns all robust 2D clipping/offsetting for copper-pour geometry.
|
|
111
|
-
// This adapter keeps scaling, ring normalization, errors, and output grouping
|
|
112
|
-
// out of the solver so the rest of the repo stays independent of WASM details.
|
|
113
|
-
export const toScaledManifoldPolygons = (
|
|
114
|
-
polygons: PolygonRing[],
|
|
115
|
-
operation = "toScaledManifoldPolygons",
|
|
116
|
-
): ScaledPolygons => {
|
|
117
|
-
const scaledPolygons: ScaledPolygons = []
|
|
118
|
-
|
|
119
|
-
for (const polygon of polygons) {
|
|
120
|
-
const normalized = normalizeRing(polygon, operation)
|
|
121
|
-
if (normalized.length < 3) continue
|
|
122
|
-
const positiveRing =
|
|
123
|
-
signedArea(normalized) < 0 ? [...normalized].reverse() : normalized
|
|
124
|
-
scaledPolygons.push(
|
|
125
|
-
positiveRing.map((p) => [
|
|
126
|
-
Math.round(p.x * MANIFOLD_GEOMETRY_SCALE),
|
|
127
|
-
Math.round(p.y * MANIFOLD_GEOMETRY_SCALE),
|
|
128
|
-
]),
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return scaledPolygons
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const fromScaledManifoldPolygons = (
|
|
136
|
-
polygons: SimplePolygon[],
|
|
137
|
-
): PolygonRing[] =>
|
|
138
|
-
polygons
|
|
139
|
-
.map((polygon) =>
|
|
140
|
-
normalizeRing(
|
|
141
|
-
polygon.map(([x, y]) => ({
|
|
142
|
-
x: x / MANIFOLD_GEOMETRY_SCALE,
|
|
143
|
-
y: y / MANIFOLD_GEOMETRY_SCALE,
|
|
144
|
-
})),
|
|
145
|
-
"fromScaledManifoldPolygons",
|
|
146
|
-
),
|
|
147
|
-
)
|
|
148
|
-
.filter((polygon) => polygon.length >= 3)
|
|
149
|
-
|
|
150
|
-
const runManifoldOperation = <T>(
|
|
151
|
-
operation: string,
|
|
152
|
-
polygons: ScaledPolygons,
|
|
153
|
-
callback: () => T,
|
|
154
|
-
): T => {
|
|
155
|
-
try {
|
|
156
|
-
return callback()
|
|
157
|
-
} catch (error) {
|
|
158
|
-
const details = describePolygons(polygons)
|
|
159
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
160
|
-
throw new Error(
|
|
161
|
-
`${operation} failed: ${message}; details=${JSON.stringify(details)}`,
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
23
|
export const crossSectionFromPolygon = (
|
|
167
24
|
polygon: PolygonRing,
|
|
168
25
|
fillRule: FillRule = "Positive",
|
|
169
|
-
):
|
|
26
|
+
): CrossSection => {
|
|
170
27
|
const scaledPolygons = toScaledManifoldPolygons(
|
|
171
28
|
[polygon],
|
|
172
29
|
"crossSectionFromPolygon",
|
|
173
30
|
)
|
|
31
|
+
const CrossSection = getCrossSection()
|
|
174
32
|
if (scaledPolygons.length === 0) {
|
|
175
33
|
return CrossSection.ofPolygons([])
|
|
176
34
|
}
|
|
@@ -182,11 +40,12 @@ export const crossSectionFromPolygon = (
|
|
|
182
40
|
export const crossSectionFromPolygons = (
|
|
183
41
|
polygons: PolygonRing[],
|
|
184
42
|
fillRule: FillRule = "Positive",
|
|
185
|
-
):
|
|
43
|
+
): CrossSection => {
|
|
186
44
|
const scaledPolygons = toScaledManifoldPolygons(
|
|
187
45
|
polygons,
|
|
188
46
|
"crossSectionFromPolygons",
|
|
189
47
|
)
|
|
48
|
+
const CrossSection = getCrossSection()
|
|
190
49
|
if (scaledPolygons.length === 0) {
|
|
191
50
|
return CrossSection.ofPolygons([])
|
|
192
51
|
}
|
|
@@ -196,9 +55,10 @@ export const crossSectionFromPolygons = (
|
|
|
196
55
|
}
|
|
197
56
|
|
|
198
57
|
export const composeCrossSections = (
|
|
199
|
-
sections:
|
|
200
|
-
):
|
|
58
|
+
sections: CrossSection[],
|
|
59
|
+
): CrossSection => {
|
|
201
60
|
const nonEmptySections = sections.filter((section) => !section.isEmpty())
|
|
61
|
+
const CrossSection = getCrossSection()
|
|
202
62
|
if (nonEmptySections.length === 0) {
|
|
203
63
|
return CrossSection.ofPolygons([])
|
|
204
64
|
}
|
|
@@ -218,6 +78,7 @@ export const offsetPolygon = (
|
|
|
218
78
|
}
|
|
219
79
|
|
|
220
80
|
const scaledMargin = margin * MANIFOLD_GEOMETRY_SCALE
|
|
81
|
+
const CrossSection = getCrossSection()
|
|
221
82
|
const section = runManifoldOperation(
|
|
222
83
|
"offsetPolygon.input",
|
|
223
84
|
scaledPolygons,
|
|
@@ -234,7 +95,7 @@ export const offsetPolygon = (
|
|
|
234
95
|
export const subtractBlockersFromPour = (
|
|
235
96
|
pourPolygon: PolygonRing,
|
|
236
97
|
blockerPolygons: PolygonRing[],
|
|
237
|
-
):
|
|
98
|
+
): CrossSection => {
|
|
238
99
|
const pourSection = crossSectionFromPolygon(pourPolygon)
|
|
239
100
|
const blockerSection = crossSectionFromPolygons(blockerPolygons)
|
|
240
101
|
|
|
@@ -258,9 +119,9 @@ export const subtractBlockersFromPour = (
|
|
|
258
119
|
}
|
|
259
120
|
|
|
260
121
|
export const removeTinyIslands = (
|
|
261
|
-
section:
|
|
122
|
+
section: CrossSection,
|
|
262
123
|
minArea = DEFAULT_MIN_ISLAND_AREA,
|
|
263
|
-
):
|
|
124
|
+
): CrossSection => {
|
|
264
125
|
if (section.isEmpty()) return section
|
|
265
126
|
|
|
266
127
|
const minScaledArea =
|
|
@@ -273,7 +134,7 @@ export const removeTinyIslands = (
|
|
|
273
134
|
}
|
|
274
135
|
|
|
275
136
|
export const crossSectionToCopperPourIslands = (
|
|
276
|
-
section:
|
|
137
|
+
section: CrossSection,
|
|
277
138
|
): CopperPourIsland[] => {
|
|
278
139
|
const islands: CopperPourIsland[] = []
|
|
279
140
|
|
|
@@ -296,11 +157,3 @@ export const crossSectionToCopperPourIslands = (
|
|
|
296
157
|
|
|
297
158
|
return islands
|
|
298
159
|
}
|
|
299
|
-
|
|
300
|
-
export const geometryDebugSummary = (
|
|
301
|
-
label: string,
|
|
302
|
-
polygons: PolygonRing[],
|
|
303
|
-
) => ({
|
|
304
|
-
label,
|
|
305
|
-
...describePolygons(toScaledManifoldPolygons(polygons, label)),
|
|
306
|
-
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CrossSection as CrossSectionType,
|
|
3
|
+
ManifoldToplevel,
|
|
4
|
+
} from "manifold-3d"
|
|
5
|
+
import {
|
|
6
|
+
getManifoldModule,
|
|
7
|
+
getManifoldModuleSync,
|
|
8
|
+
} from "manifold-3d/lib/wasm.js"
|
|
9
|
+
import { describeScaledPolygons, type ScaledPolygons } from "./polygon-ring"
|
|
10
|
+
|
|
11
|
+
let manifoldModulePromise: Promise<ManifoldToplevel> | null = null
|
|
12
|
+
|
|
13
|
+
export const initializeManifoldGeometry = async () => {
|
|
14
|
+
if (getManifoldModuleSync()) return
|
|
15
|
+
manifoldModulePromise ??= getManifoldModule().catch((error) => {
|
|
16
|
+
manifoldModulePromise = null
|
|
17
|
+
throw error
|
|
18
|
+
})
|
|
19
|
+
await manifoldModulePromise
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const isManifoldGeometryInitialized = () =>
|
|
23
|
+
Boolean(getManifoldModuleSync())
|
|
24
|
+
|
|
25
|
+
export const getCrossSection = () => {
|
|
26
|
+
const manifold = getManifoldModuleSync()
|
|
27
|
+
if (!manifold) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Manifold geometry has not been initialized. Call initializeManifoldGeometry() before solving copper pours.",
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return manifold.CrossSection
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const runManifoldOperation = <T>(
|
|
36
|
+
operation: string,
|
|
37
|
+
polygons: ScaledPolygons,
|
|
38
|
+
callback: () => T,
|
|
39
|
+
): T => {
|
|
40
|
+
try {
|
|
41
|
+
return callback()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const details = describeScaledPolygons(polygons)
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${operation} failed: ${message}; details=${JSON.stringify(details)}`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type CrossSection = CrossSectionType
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { PolygonRing } from "./polygon-ring"
|
|
3
|
+
|
|
4
|
+
export const circleToPolygon = (
|
|
5
|
+
center: Point,
|
|
6
|
+
radius: number,
|
|
7
|
+
numSegments = 32,
|
|
8
|
+
): PolygonRing => {
|
|
9
|
+
const points: PolygonRing = []
|
|
10
|
+
for (let i = 0; i < numSegments; i++) {
|
|
11
|
+
const angle = (i / numSegments) * 2 * Math.PI
|
|
12
|
+
points.push({
|
|
13
|
+
x: center.x + radius * Math.cos(angle),
|
|
14
|
+
y: center.y + radius * Math.sin(angle),
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return points
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const boxToPolygon = (
|
|
21
|
+
minX: number,
|
|
22
|
+
minY: number,
|
|
23
|
+
maxX: number,
|
|
24
|
+
maxY: number,
|
|
25
|
+
): PolygonRing => [
|
|
26
|
+
{ x: minX, y: minY },
|
|
27
|
+
{ x: maxX, y: minY },
|
|
28
|
+
{ x: maxX, y: maxY },
|
|
29
|
+
{ x: minX, y: maxY },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export const segmentToPolygon = (
|
|
33
|
+
start: Point,
|
|
34
|
+
end: Point,
|
|
35
|
+
width: number,
|
|
36
|
+
): PolygonRing => {
|
|
37
|
+
const segmentLength = Math.hypot(start.x - end.x, start.y - end.y)
|
|
38
|
+
if (segmentLength === 0) return []
|
|
39
|
+
|
|
40
|
+
const centerX = (start.x + end.x) / 2
|
|
41
|
+
const centerY = (start.y + end.y) / 2
|
|
42
|
+
const angle = Math.atan2(end.y - start.y, end.x - start.x)
|
|
43
|
+
const cosAngle = Math.cos(angle)
|
|
44
|
+
const sinAngle = Math.sin(angle)
|
|
45
|
+
const halfLength = segmentLength / 2
|
|
46
|
+
const halfWidth = width / 2
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
{ x: -halfLength, y: -halfWidth },
|
|
50
|
+
{ x: halfLength, y: -halfWidth },
|
|
51
|
+
{ x: halfLength, y: halfWidth },
|
|
52
|
+
{ x: -halfLength, y: halfWidth },
|
|
53
|
+
].map((point) => ({
|
|
54
|
+
x: centerX + point.x * cosAngle - point.y * sinAngle,
|
|
55
|
+
y: centerY + point.x * sinAngle + point.y * cosAngle,
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { SimplePolygon } from "manifold-3d"
|
|
3
|
+
|
|
4
|
+
export const MANIFOLD_GEOMETRY_SCALE = 1_000_000
|
|
5
|
+
|
|
6
|
+
export type PolygonRing = Point[]
|
|
7
|
+
export type ScaledPolygons = SimplePolygon[]
|
|
8
|
+
|
|
9
|
+
export const signedArea = (ring: PolygonRing) => {
|
|
10
|
+
let area = 0
|
|
11
|
+
for (let i = 0; i < ring.length; i++) {
|
|
12
|
+
const current = ring[i]!
|
|
13
|
+
const next = ring[(i + 1) % ring.length]!
|
|
14
|
+
area += current.x * next.y - next.x * current.y
|
|
15
|
+
}
|
|
16
|
+
return area / 2
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const describeScaledPolygons = (polygons: ScaledPolygons) => {
|
|
20
|
+
let pointCount = 0
|
|
21
|
+
const bbox = {
|
|
22
|
+
minX: Number.POSITIVE_INFINITY,
|
|
23
|
+
minY: Number.POSITIVE_INFINITY,
|
|
24
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
25
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const polygon of polygons) {
|
|
29
|
+
pointCount += polygon.length
|
|
30
|
+
for (const [x, y] of polygon) {
|
|
31
|
+
bbox.minX = Math.min(bbox.minX, x)
|
|
32
|
+
bbox.minY = Math.min(bbox.minY, y)
|
|
33
|
+
bbox.maxX = Math.max(bbox.maxX, x)
|
|
34
|
+
bbox.maxY = Math.max(bbox.maxY, y)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
polygonCount: polygons.length,
|
|
40
|
+
pointCount,
|
|
41
|
+
bbox: pointCount > 0 ? bbox : null,
|
|
42
|
+
scale: MANIFOLD_GEOMETRY_SCALE,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const assertFinitePoint = (point: Point, operation: string) => {
|
|
47
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${operation} received non-finite point (${point.x}, ${point.y})`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pointsEqual = (a: Point, b: Point) => a.x === b.x && a.y === b.y
|
|
55
|
+
|
|
56
|
+
export const normalizeRing = (
|
|
57
|
+
ring: PolygonRing,
|
|
58
|
+
operation = "normalizeRing",
|
|
59
|
+
): PolygonRing => {
|
|
60
|
+
const normalized: PolygonRing = []
|
|
61
|
+
|
|
62
|
+
for (const point of ring) {
|
|
63
|
+
assertFinitePoint(point, operation)
|
|
64
|
+
const roundedPoint = {
|
|
65
|
+
x:
|
|
66
|
+
Math.round(point.x * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
67
|
+
y:
|
|
68
|
+
Math.round(point.y * MANIFOLD_GEOMETRY_SCALE) / MANIFOLD_GEOMETRY_SCALE,
|
|
69
|
+
}
|
|
70
|
+
const previous = normalized[normalized.length - 1]
|
|
71
|
+
if (!previous || !pointsEqual(previous, roundedPoint)) {
|
|
72
|
+
normalized.push(roundedPoint)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
normalized.length > 1 &&
|
|
78
|
+
pointsEqual(normalized[0]!, normalized[normalized.length - 1]!)
|
|
79
|
+
) {
|
|
80
|
+
normalized.pop()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const uniquePoints = new Set(normalized.map((p) => `${p.x},${p.y}`))
|
|
84
|
+
if (uniquePoints.size < 3 || Math.abs(signedArea(normalized)) < 1e-18) {
|
|
85
|
+
return []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return normalized
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const toScaledManifoldPolygons = (
|
|
92
|
+
polygons: PolygonRing[],
|
|
93
|
+
operation = "toScaledManifoldPolygons",
|
|
94
|
+
): ScaledPolygons => {
|
|
95
|
+
const scaledPolygons: ScaledPolygons = []
|
|
96
|
+
|
|
97
|
+
for (const polygon of polygons) {
|
|
98
|
+
const normalized = normalizeRing(polygon, operation)
|
|
99
|
+
if (normalized.length < 3) continue
|
|
100
|
+
const positiveRing =
|
|
101
|
+
signedArea(normalized) < 0 ? [...normalized].reverse() : normalized
|
|
102
|
+
scaledPolygons.push(
|
|
103
|
+
positiveRing.map((p) => [
|
|
104
|
+
Math.round(p.x * MANIFOLD_GEOMETRY_SCALE),
|
|
105
|
+
Math.round(p.y * MANIFOLD_GEOMETRY_SCALE),
|
|
106
|
+
]),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return scaledPolygons
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const fromScaledManifoldPolygons = (
|
|
114
|
+
polygons: SimplePolygon[],
|
|
115
|
+
): PolygonRing[] =>
|
|
116
|
+
polygons
|
|
117
|
+
.map((polygon) =>
|
|
118
|
+
normalizeRing(
|
|
119
|
+
polygon.map(([x, y]) => ({
|
|
120
|
+
x: x / MANIFOLD_GEOMETRY_SCALE,
|
|
121
|
+
y: y / MANIFOLD_GEOMETRY_SCALE,
|
|
122
|
+
})),
|
|
123
|
+
"fromScaledManifoldPolygons",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
.filter((polygon) => polygon.length >= 3)
|
|
@@ -6,11 +6,13 @@ import type {
|
|
|
6
6
|
InputRectPad,
|
|
7
7
|
InputTracePad,
|
|
8
8
|
} from "lib/types"
|
|
9
|
+
import { offsetPolygon } from "./manifold-geometry-adapter"
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
11
|
+
boxToPolygon,
|
|
12
|
+
circleToPolygon,
|
|
13
|
+
segmentToPolygon,
|
|
14
|
+
} from "./polygon-primitives"
|
|
15
|
+
import { normalizeRing, type PolygonRing } from "./polygon-ring"
|
|
14
16
|
|
|
15
17
|
interface ProcessedObstacles {
|
|
16
18
|
polygonsToSubtract: PolygonRing[]
|
|
@@ -24,34 +26,6 @@ const isCircularPad = (pad: InputPad): pad is InputCircularPad =>
|
|
|
24
26
|
const isPolygonPad = (pad: InputPad): pad is InputPolygonPad =>
|
|
25
27
|
pad.shape === "polygon"
|
|
26
28
|
|
|
27
|
-
const circleToPolygon = (
|
|
28
|
-
center: Point,
|
|
29
|
-
radius: number,
|
|
30
|
-
numSegments = 32,
|
|
31
|
-
): PolygonRing => {
|
|
32
|
-
const points: PolygonRing = []
|
|
33
|
-
for (let i = 0; i < numSegments; i++) {
|
|
34
|
-
const angle = (i / numSegments) * 2 * Math.PI
|
|
35
|
-
points.push({
|
|
36
|
-
x: center.x + radius * Math.cos(angle),
|
|
37
|
-
y: center.y + radius * Math.sin(angle),
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
return points
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const boxToPolygon = (
|
|
44
|
-
minX: number,
|
|
45
|
-
minY: number,
|
|
46
|
-
maxX: number,
|
|
47
|
-
maxY: number,
|
|
48
|
-
): PolygonRing => [
|
|
49
|
-
{ x: minX, y: minY },
|
|
50
|
-
{ x: maxX, y: minY },
|
|
51
|
-
{ x: maxX, y: maxY },
|
|
52
|
-
{ x: minX, y: maxY },
|
|
53
|
-
]
|
|
54
|
-
|
|
55
29
|
export const processObstaclesForPour = (
|
|
56
30
|
pads: InputPad[],
|
|
57
31
|
pourConnectivityKey: string,
|
|
@@ -111,35 +85,10 @@ export const processObstaclesForPour = (
|
|
|
111
85
|
|
|
112
86
|
if (!p1 || !p2) continue
|
|
113
87
|
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const centerX = (p1.x + p2.x) / 2
|
|
120
|
-
const centerY = (p1.y + p2.y) / 2
|
|
121
|
-
const rotationDeg = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI
|
|
122
|
-
|
|
123
|
-
const w2 = segmentLength / 2
|
|
124
|
-
const h2 = enlargedWidth / 2
|
|
125
|
-
|
|
126
|
-
const angleRad = (rotationDeg * Math.PI) / 180
|
|
127
|
-
const cosAngle = Math.cos(angleRad)
|
|
128
|
-
const sinAngle = Math.sin(angleRad)
|
|
129
|
-
|
|
130
|
-
const corners = [
|
|
131
|
-
{ x: -w2, y: -h2 },
|
|
132
|
-
{ x: w2, y: -h2 },
|
|
133
|
-
{ x: w2, y: h2 },
|
|
134
|
-
{ x: -w2, y: h2 },
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
const rotatedCorners = corners.map((p) => ({
|
|
138
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
139
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle,
|
|
140
|
-
}))
|
|
141
|
-
|
|
142
|
-
polygonsToSubtract.push(rotatedCorners)
|
|
88
|
+
const segmentPolygon = segmentToPolygon(p1, p2, board_edge_margin * 2)
|
|
89
|
+
if (segmentPolygon.length > 0) {
|
|
90
|
+
polygonsToSubtract.push(segmentPolygon)
|
|
91
|
+
}
|
|
143
92
|
}
|
|
144
93
|
}
|
|
145
94
|
|
|
@@ -218,36 +167,14 @@ export const processObstaclesForPour = (
|
|
|
218
167
|
|
|
219
168
|
if (!p1 || !p2) continue
|
|
220
169
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
(Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI
|
|
230
|
-
|
|
231
|
-
const w2 = segmentLength / 2
|
|
232
|
-
const h2 = enlargedWidth / 2
|
|
233
|
-
|
|
234
|
-
const angleRad = (rotationDeg * Math.PI) / 180
|
|
235
|
-
const cosAngle = Math.cos(angleRad)
|
|
236
|
-
const sinAngle = Math.sin(angleRad)
|
|
237
|
-
|
|
238
|
-
const corners = [
|
|
239
|
-
{ x: -w2, y: -h2 },
|
|
240
|
-
{ x: w2, y: -h2 },
|
|
241
|
-
{ x: w2, y: h2 },
|
|
242
|
-
{ x: -w2, y: h2 },
|
|
243
|
-
]
|
|
244
|
-
|
|
245
|
-
const rotatedCorners = corners.map((p) => ({
|
|
246
|
-
x: centerX + p.x * cosAngle - p.y * sinAngle,
|
|
247
|
-
y: centerY + p.x * sinAngle + p.y * cosAngle,
|
|
248
|
-
}))
|
|
249
|
-
|
|
250
|
-
polygonsToSubtract.push(rotatedCorners)
|
|
170
|
+
const segmentPolygon = segmentToPolygon(
|
|
171
|
+
p1,
|
|
172
|
+
p2,
|
|
173
|
+
pad.width + traceMargin * 2,
|
|
174
|
+
)
|
|
175
|
+
if (segmentPolygon.length > 0) {
|
|
176
|
+
polygonsToSubtract.push(segmentPolygon)
|
|
177
|
+
}
|
|
251
178
|
}
|
|
252
179
|
}
|
|
253
180
|
}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ test("stm32f746g-disco top layer full board repro", () => {
|
|
|
11
11
|
trace_margin: 0.2,
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
expect(svg).toMatchSvgSnapshot(import.meta.path + "top")
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
test("stm32f746g-disco bottom layer full board repro", () => {
|
|
@@ -22,5 +22,5 @@ test("stm32f746g-disco bottom layer full board repro", () => {
|
|
|
22
22
|
trace_margin: 0.2,
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
expect(svg).toMatchSvgSnapshot(import.meta.path + "bottom")
|
|
26
26
|
})
|