@tscircuit/copper-pour-solver 0.0.26 → 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.
Files changed (30) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.js +347 -205
  3. package/lib/index.ts +1 -0
  4. package/lib/solvers/CopperPourPipelineSolver.ts +20 -24
  5. package/lib/solvers/copper-pour/generate-brep.ts +35 -51
  6. package/lib/solvers/copper-pour/get-board-polygon.ts +10 -19
  7. package/lib/solvers/copper-pour/manifold-geometry-adapter.ts +159 -0
  8. package/lib/solvers/copper-pour/manifold-runtime.ts +51 -0
  9. package/lib/solvers/copper-pour/polygon-primitives.ts +57 -0
  10. package/lib/solvers/copper-pour/polygon-ring.ts +126 -0
  11. package/lib/solvers/copper-pour/process-obstacles.ts +48 -143
  12. package/package.json +4 -1
  13. package/tests/__snapshots__/2-layers-bottom.snap.svg +1 -1
  14. package/tests/__snapshots__/2-layers-top.snap.svg +1 -1
  15. package/tests/__snapshots__/board-edge-margin-2.snap.svg +1 -1
  16. package/tests/__snapshots__/board-edge-margin.snap.svg +1 -1
  17. package/tests/__snapshots__/hole-and-cutouts.snap.svg +1 -1
  18. package/tests/__snapshots__/larger-trace-margin.snap.svg +1 -1
  19. package/tests/__snapshots__/multiple-pours.snap.svg +1 -1
  20. package/tests/__snapshots__/pad-margin.snap.svg +1 -1
  21. package/tests/__snapshots__/polygon-board-2.snap.svg +1 -1
  22. package/tests/__snapshots__/polygon-board.snap.svg +1 -1
  23. package/tests/__snapshots__/smaller-trace-margin.snap.svg +1 -1
  24. package/tests/__snapshots__/stm32f746g-disco.test.tsbottom.snap.svg +1 -0
  25. package/tests/__snapshots__/stm32f746g-disco.test.tstop.snap.svg +1 -0
  26. package/tests/__snapshots__/via.snap.svg +1 -1
  27. package/tests/fixtures/preload.ts +3 -0
  28. package/tests/manifold-copper-pour-geometry.test.ts +194 -0
  29. package/tests/stm32f746g-disco.test.ts +16 -16
  30. 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
- 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;
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 new Flatten.Polygon();
134
+ return [];
26
135
  }
27
- return new Flatten.Polygon(
28
- new Flatten.Box(
29
- newBounds.minX,
30
- newBounds.minY,
31
- newBounds.maxX,
32
- newBounds.maxY
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/CopperPourPipelineSolver.ts
38
- import Flatten5 from "@flatten-js/core";
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/process-obstacles.ts
41
- import Flatten3 from "@flatten-js/core";
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/circle-to-polygon.ts
44
- import Flatten2 from "@flatten-js/core";
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
- new Flatten2.Point(
51
- circle.center.x + circle.r * Math.cos(angle),
52
- circle.center.y + circle.r * Math.sin(angle)
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 new Flatten2.Polygon(points);
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 boardPoly = new Flatten3.Polygon(
69
- boardOutline.map((p) => Flatten3.point(p.x, p.y))
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 = 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));
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
- 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
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
100
- if (segmentLength === 0) continue;
101
- const enlargedWidth = board_edge_margin * 2;
102
- const centerX = (p1.x + p2.x) / 2;
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
- const circle = new Flatten3.Circle(
134
- new Flatten3.Point(pad.x, pad.y),
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
- const b = new Flatten3.Box(
144
- bounds.minX - margin,
145
- bounds.minY - margin,
146
- bounds.maxX + margin,
147
- bounds.maxY + margin
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 = new Flatten3.Polygon(
165
- uniquePoints.map((p) => Flatten3.point(p.x, p.y))
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
- 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
- }
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
- const circle = new Flatten3.Circle(
204
- new Flatten3.Point(segment.x, segment.y),
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
214
- if (segmentLength === 0) continue;
215
- const enlargedWidth = pad.width + traceMargin * 2;
216
- const centerX = (p1.x + p2.x) / 2;
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
- 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;
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
@@ -1,3 +1,4 @@
1
1
  export * from "./solvers/CopperPourPipelineSolver"
2
+ export { initializeManifoldGeometry } from "./solvers/copper-pour/manifold-runtime"
2
3
  export * from "./circuit-json/convert-circuit-json-to-input-problem"
3
4
  export * from "./types"