@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 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/generate-brep.ts
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 ensureAreaSign = (ring, desiredSign) => {
15
- const area = signedArea(ring);
16
- const shouldReverse = desiredSign === "positive" && area < 0 || desiredSign === "negative" && area > 0;
17
- return shouldReverse ? [...ring].reverse() : ring;
18
- };
19
- var ringToVertices = (ring) => ring.map((point) => ({
20
- x: point.x,
21
- y: point.y
22
- }));
23
- var generateBRep = (pourIslands) => {
24
- const brep_shapes = [];
25
- for (const island of pourIslands) {
26
- if (island.outerRing.length < 3) continue;
27
- const outerRing = ensureAreaSign(island.outerRing, "negative");
28
- const innerRings = island.innerRings.filter((ring) => ring.length >= 3).map((ring) => ensureAreaSign(ring, "positive"));
29
- brep_shapes.push({
30
- outer_ring: { vertices: ringToVertices(outerRing) },
31
- inner_rings: innerRings.map((ring) => ({
32
- vertices: ringToVertices(ring)
33
- }))
34
- });
35
- }
36
- return brep_shapes;
37
- };
38
-
39
- // lib/solvers/copper-pour/manifold-geometry-adapter.ts
40
- var manifoldModule = await import("manifold-3d");
41
- var manifoldFactory = manifoldModule.default;
42
- var manifold = await manifoldFactory();
43
- manifold.setup();
44
- var { CrossSection } = manifold;
45
- var MANIFOLD_GEOMETRY_SCALE = 1e6;
46
- var DEFAULT_MIN_ISLAND_AREA = 1e-8;
47
- var describePolygons = (polygons) => {
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(signedArea2(normalized)) < 1e-18) {
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 = signedArea2(normalized) < 0 ? [...normalized].reverse() : normalized;
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 = describePolygons(polygons);
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(signedArea2(ring)) > Math.abs(signedArea2(largest)) ? ring : largest
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/process-obstacles.ts
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
327
- if (segmentLength === 0) continue;
328
- const enlargedWidth = board_edge_margin * 2;
329
- const centerX = (p1.x + p2.x) / 2;
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
408
- if (segmentLength === 0) continue;
409
- const enlargedWidth = pad.width + traceMargin * 2;
410
- const centerX = (p1.x + p2.x) / 2;
411
- const centerY = (p1.y + p2.y) / 2;
412
- const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
413
- const w2 = segmentLength / 2;
414
- const h2 = enlargedWidth / 2;
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
@@ -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"
@@ -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, PolygonRing } from "./manifold-geometry-adapter"
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 { PolygonRing } from "./manifold-geometry-adapter"
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 { Point } from "@tscircuit/math-utils"
2
- import type {
3
- CrossSection as CrossSectionType,
4
- FillRule,
5
- SimplePolygon,
6
- } from "manifold-3d"
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
- ): CrossSectionType => {
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
- ): CrossSectionType => {
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: CrossSectionType[],
200
- ): CrossSectionType => {
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
- ): CrossSectionType => {
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: CrossSectionType,
122
+ section: CrossSection,
262
123
  minArea = DEFAULT_MIN_ISLAND_AREA,
263
- ): CrossSectionType => {
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: CrossSectionType,
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
- normalizeRing,
11
- offsetPolygon,
12
- type PolygonRing,
13
- } from "./manifold-geometry-adapter"
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y)
115
- if (segmentLength === 0) continue
116
-
117
- const enlargedWidth = board_edge_margin * 2
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 segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y)
222
- if (segmentLength === 0) continue
223
-
224
- const enlargedWidth = pad.width + traceMargin * 2
225
-
226
- const centerX = (p1.x + p2.x) / 2
227
- const centerY = (p1.y + p2.y) / 2
228
- const rotationDeg =
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/copper-pour-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.25",
4
+ "version": "0.0.27",
5
5
  "scripts": {
6
6
  "format": "biome format . --write",
7
7
  "format:check": "biome format .",
@@ -1 +1,4 @@
1
1
  import "bun-match-svg"
2
+ import { initializeManifoldGeometry } from "lib/index"
3
+
4
+ await initializeManifoldGeometry()
@@ -11,7 +11,7 @@ test("stm32f746g-disco top layer full board repro", () => {
11
11
  trace_margin: 0.2,
12
12
  })
13
13
 
14
- expect(svg).toMatchSvgSnapshot(import.meta.path+"top")
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
- expect(svg).toMatchSvgSnapshot(import.meta.path+"bottom")
25
+ expect(svg).toMatchSvgSnapshot(import.meta.path + "bottom")
26
26
  })