calculate-packing 0.0.59 → 0.0.61

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/README.md CHANGED
@@ -22,6 +22,9 @@ Internally the algorithm:
22
22
  4. evaluates the four orthogonal rotations of the candidate component and
23
23
  chooses the cheapest non-overlapping one
24
24
 
25
+ <img width="4652" height="3508" alt="image" src="https://github.com/user-attachments/assets/a1c7f129-6e87-42d2-aa3e-3a0f39719469" />
26
+
27
+
25
28
  ## Installation
26
29
 
27
30
  ```bash
package/dist/index.d.ts CHANGED
@@ -142,19 +142,32 @@ declare class LargestRectOutsideOutlineFromPointSolver extends BaseSolver$1 {
142
142
  origin: Point$1;
143
143
  globalBounds: Bounds;
144
144
  largestRect: Rect | null;
145
+ /**
146
+ * Mode for finding rectangles:
147
+ * - "outside": Find rect outside the polygon (for CCW obstacle boundaries)
148
+ * - "inside": Find rect inside the polygon (for CW free space pockets)
149
+ */
150
+ mode: "outside" | "inside";
145
151
  constructor(params: {
146
152
  ccwFullOutline: Point$1[];
147
153
  origin: Point$1;
148
154
  globalBounds: Bounds;
155
+ /**
156
+ * Mode for finding rectangles:
157
+ * - "outside" (default): Find rect outside the polygon (for CCW obstacle boundaries)
158
+ * - "inside": Find rect inside the polygon (for CW free space pockets)
159
+ */
160
+ mode?: "outside" | "inside";
149
161
  });
150
162
  getConstructorParams(): {
151
163
  ccwFullOutline: Point$1[];
152
164
  origin: Point$1;
153
165
  globalBounds: Bounds;
166
+ mode: "outside" | "inside";
154
167
  };
155
168
  _setup(): void;
156
169
  _step(): void;
157
- private computeLargestRectOutside;
170
+ private computeLargestRect;
158
171
  private almostEqual;
159
172
  private makeEdges;
160
173
  private isVertical;
@@ -383,6 +396,9 @@ declare class OutlineSegmentCandidatePointSolver extends BaseSolver$1 {
383
396
  optimalPosition?: Point$2;
384
397
  irlsSolver?: MultiOffsetIrlsSolver;
385
398
  twoPhaseIrlsSolver?: TwoPhaseIrlsSolver;
399
+ largestRectBounds?: Bounds;
400
+ largestRectMidPoint?: Point$2;
401
+ largestRectOrigin?: Point$2;
386
402
  constructor(params: {
387
403
  outlineSegment: [Point$2, Point$2];
388
404
  ccwFullOutline: [Point$2, Point$2][];
package/dist/index.js CHANGED
@@ -59,6 +59,53 @@ function sortComponentQueue({
59
59
  // lib/constructOutlinesFromPackedComponents.ts
60
60
  import Flatten from "@flatten-js/core";
61
61
 
62
+ // lib/geometry/combineBounds.ts
63
+ var combineBounds = (bounds) => {
64
+ const minX = Math.min(...bounds.map((b) => b.minX));
65
+ const minY = Math.min(...bounds.map((b) => b.minY));
66
+ const maxX = Math.max(...bounds.map((b) => b.maxX));
67
+ const maxY = Math.max(...bounds.map((b) => b.maxY));
68
+ return { minX, minY, maxX, maxY };
69
+ };
70
+
71
+ // lib/geometry/getComponentBounds.ts
72
+ var getComponentBounds = (component, minGap = 0) => {
73
+ const bounds = {
74
+ minX: Infinity,
75
+ maxX: -Infinity,
76
+ minY: Infinity,
77
+ maxY: -Infinity
78
+ };
79
+ component.pads.forEach((pad) => {
80
+ const hw = pad.size.x / 2;
81
+ const hh = pad.size.y / 2;
82
+ const localCorners = [
83
+ { x: pad.offset.x - hw, y: pad.offset.y - hh },
84
+ { x: pad.offset.x + hw, y: pad.offset.y - hh },
85
+ { x: pad.offset.x + hw, y: pad.offset.y + hh },
86
+ { x: pad.offset.x - hw, y: pad.offset.y + hh }
87
+ ];
88
+ localCorners.forEach((corner) => {
89
+ const world = rotatePoint(
90
+ corner,
91
+ component.ccwRotationOffset * Math.PI / 180
92
+ );
93
+ const x = world.x + component.center.x;
94
+ const y = world.y + component.center.y;
95
+ bounds.minX = Math.min(bounds.minX, x);
96
+ bounds.maxX = Math.max(bounds.maxX, x);
97
+ bounds.minY = Math.min(bounds.minY, y);
98
+ bounds.maxY = Math.max(bounds.maxY, y);
99
+ });
100
+ });
101
+ return {
102
+ minX: bounds.minX - minGap,
103
+ maxX: bounds.maxX + minGap,
104
+ minY: bounds.minY - minGap,
105
+ maxY: bounds.maxY + minGap
106
+ };
107
+ };
108
+
62
109
  // lib/math/cross.ts
63
110
  var cross = (O, A, B) => (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
64
111
 
@@ -73,7 +120,7 @@ function simplifyCollinearSegments(outline, tolerance = 1e-10) {
73
120
  for (let i = 1; i < outline.length; i++) {
74
121
  const nextSegment = outline[i];
75
122
  const [nextStart, nextEnd] = nextSegment;
76
- const connectionTolerance = 1e-10;
123
+ const connectionTolerance = 1e-9;
77
124
  const isConnected = Math.abs(currentSegmentEnd.x - nextStart.x) < connectionTolerance && Math.abs(currentSegmentEnd.y - nextStart.y) < connectionTolerance;
78
125
  if (!isConnected) {
79
126
  simplified.push([currentSegmentStart, currentSegmentEnd]);
@@ -82,7 +129,16 @@ function simplifyCollinearSegments(outline, tolerance = 1e-10) {
82
129
  continue;
83
130
  }
84
131
  const crossProduct = cross(currentSegmentStart, currentSegmentEnd, nextEnd);
85
- if (Math.abs(crossProduct) < tolerance) {
132
+ const segLen1 = Math.hypot(
133
+ currentSegmentEnd.x - currentSegmentStart.x,
134
+ currentSegmentEnd.y - currentSegmentStart.y
135
+ );
136
+ const segLen2 = Math.hypot(
137
+ nextEnd.x - currentSegmentEnd.x,
138
+ nextEnd.y - currentSegmentEnd.y
139
+ );
140
+ const scaledTolerance = Math.max(tolerance, tolerance * segLen1 * segLen2);
141
+ if (Math.abs(crossProduct) < scaledTolerance) {
86
142
  currentSegmentEnd = nextEnd;
87
143
  } else {
88
144
  simplified.push([currentSegmentStart, currentSegmentEnd]);
@@ -97,7 +153,7 @@ function simplifyCollinearSegments(outline, tolerance = 1e-10) {
97
153
  currentSegmentEnd
98
154
  ];
99
155
  if (firstSegment && simplified.length > 0) {
100
- const connectionTolerance = 1e-10;
156
+ const connectionTolerance = 1e-9;
101
157
  const isLastConnectedToFirst = Math.abs(currentSegmentEnd.x - firstSegment[0].x) < connectionTolerance && Math.abs(currentSegmentEnd.y - firstSegment[0].y) < connectionTolerance;
102
158
  if (isLastConnectedToFirst) {
103
159
  const crossProduct = cross(
@@ -105,7 +161,19 @@ function simplifyCollinearSegments(outline, tolerance = 1e-10) {
105
161
  currentSegmentEnd,
106
162
  firstSegment[1]
107
163
  );
108
- if (Math.abs(crossProduct) < tolerance) {
164
+ const segLen1 = Math.hypot(
165
+ currentSegmentEnd.x - currentSegmentStart.x,
166
+ currentSegmentEnd.y - currentSegmentStart.y
167
+ );
168
+ const segLen2 = Math.hypot(
169
+ firstSegment[1].x - currentSegmentEnd.x,
170
+ firstSegment[1].y - currentSegmentEnd.y
171
+ );
172
+ const scaledTolerance = Math.max(
173
+ tolerance,
174
+ tolerance * segLen1 * segLen2
175
+ );
176
+ if (Math.abs(crossProduct) < scaledTolerance) {
109
177
  simplified[0] = [currentSegmentStart, firstSegment[1]];
110
178
  } else {
111
179
  simplified.push(lastSegmentCandidate);
@@ -122,52 +190,65 @@ function simplifyCollinearSegments(outline, tolerance = 1e-10) {
122
190
  return simplified;
123
191
  }
124
192
 
125
- // lib/geometry/getComponentBounds.ts
126
- var getComponentBounds = (component, minGap = 0) => {
127
- const bounds = {
128
- minX: Infinity,
129
- maxX: -Infinity,
130
- minY: Infinity,
131
- maxY: -Infinity
193
+ // lib/parseFlattenPolygonLoops.ts
194
+ function signedArea(points) {
195
+ let area = 0;
196
+ for (let i = 0; i < points.length; i++) {
197
+ const j = (i + 1) % points.length;
198
+ const pi = points[i];
199
+ const pj = points[j];
200
+ area += pi.x * pj.y;
201
+ area -= pj.x * pi.y;
202
+ }
203
+ return area / 2;
204
+ }
205
+ function extractFacePoints(face) {
206
+ const points = [];
207
+ let edge = face.first;
208
+ if (!edge) return points;
209
+ do {
210
+ const shp = edge.shape;
211
+ const ps = shp.start ?? shp.ps;
212
+ points.push({ x: ps.x, y: ps.y });
213
+ edge = edge.next;
214
+ } while (edge !== face.first);
215
+ return points;
216
+ }
217
+ function parseFlattenPolygonLoops(polygon) {
218
+ const obstacleFreeLoops = [];
219
+ const obstacleContainingLoops = [];
220
+ const faces = Array.from(polygon.faces);
221
+ for (const face of faces) {
222
+ const points = extractFacePoints(face);
223
+ if (points.length < 3) continue;
224
+ const area = signedArea(points);
225
+ if (area > 0) {
226
+ obstacleFreeLoops.push(points);
227
+ } else if (area < 0) {
228
+ obstacleContainingLoops.push(points);
229
+ }
230
+ }
231
+ return {
232
+ obstacleFreeLoops,
233
+ obstacleContainingLoops
132
234
  };
133
- component.pads.forEach((pad) => {
134
- const hw = pad.size.x / 2;
135
- const hh = pad.size.y / 2;
136
- const localCorners = [
137
- { x: pad.offset.x - hw, y: pad.offset.y - hh },
138
- { x: pad.offset.x + hw, y: pad.offset.y - hh },
139
- { x: pad.offset.x + hw, y: pad.offset.y + hh },
140
- { x: pad.offset.x - hw, y: pad.offset.y + hh }
141
- ];
142
- localCorners.forEach((corner) => {
143
- const world = rotatePoint(
144
- corner,
145
- component.ccwRotationOffset * Math.PI / 180
146
- );
147
- const x = world.x + component.center.x;
148
- const y = world.y + component.center.y;
149
- bounds.minX = Math.min(bounds.minX, x);
150
- bounds.maxX = Math.max(bounds.maxX, x);
151
- bounds.minY = Math.min(bounds.minY, y);
152
- bounds.maxY = Math.max(bounds.maxY, y);
153
- });
154
- });
235
+ }
236
+ function pointsToSegments(points) {
237
+ const segments = [];
238
+ for (let i = 0; i < points.length; i++) {
239
+ const p1 = points[i];
240
+ const p2 = points[(i + 1) % points.length];
241
+ segments.push([p1, p2]);
242
+ }
243
+ return segments;
244
+ }
245
+ function parseFlattenPolygonSegments(polygon) {
246
+ const loops = parseFlattenPolygonLoops(polygon);
155
247
  return {
156
- minX: bounds.minX - minGap,
157
- maxX: bounds.maxX + minGap,
158
- minY: bounds.minY - minGap,
159
- maxY: bounds.maxY + minGap
248
+ obstacleFreeLoops: loops.obstacleFreeLoops.map(pointsToSegments),
249
+ obstacleContainingLoops: loops.obstacleContainingLoops.map(pointsToSegments)
160
250
  };
161
- };
162
-
163
- // lib/geometry/combineBounds.ts
164
- var combineBounds = (bounds) => {
165
- const minX = Math.min(...bounds.map((b) => b.minX));
166
- const minY = Math.min(...bounds.map((b) => b.minY));
167
- const maxX = Math.max(...bounds.map((b) => b.maxX));
168
- const maxY = Math.max(...bounds.map((b) => b.maxY));
169
- return { minX, minY, maxX, maxY };
170
- };
251
+ }
171
252
 
172
253
  // lib/constructOutlinesFromPackedComponents.ts
173
254
  var createPadPolygons = (component, minGap) => {
@@ -247,40 +328,22 @@ var constructOutlinesFromPackedComponents = (components, opts = {}) => {
247
328
  const obstacleShapes = createObstaclePolygons(obstacles, minGap);
248
329
  allPadShapes.push(...obstacleShapes);
249
330
  if (allPadShapes.length === 0) return [];
250
- const areaOfBox = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
251
- const containsBox = (outer, inner, eps = 1e-9) => outer.minX - eps <= inner.minX && outer.minY - eps <= inner.minY && outer.maxX + eps >= inner.maxX && outer.maxY + eps >= inner.maxY;
252
- const sortedByAreaDesc = [...allPadShapes].sort(
253
- (a, b) => areaOfBox(b.bbox) - areaOfBox(a.bbox)
254
- );
255
- const filteredPadShapes = [];
256
- for (const shape of sortedByAreaDesc) {
257
- const w = shape.bbox.maxX - shape.bbox.minX;
258
- const h = shape.bbox.maxY - shape.bbox.minY;
259
- if (!(w > 1e-12 && h > 1e-12)) continue;
260
- let contained = false;
261
- for (const kept of filteredPadShapes) {
262
- if (containsBox(kept.bbox, shape.bbox)) {
263
- contained = true;
264
- break;
265
- }
266
- }
267
- if (!contained) filteredPadShapes.push(shape);
268
- }
331
+ const filteredPadShapes = filterPadShapes(allPadShapes);
269
332
  const keptPadPolys = filteredPadShapes.map((s) => s.poly);
270
333
  let A = new Flatten.Polygon(
271
334
  new Flatten.Box(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY)
272
335
  );
273
336
  const B = A.clone();
274
- for (let i = 0; i < keptPadPolys.length; i++) {
337
+ for (const poly of keptPadPolys) {
275
338
  try {
276
- A = Flatten.BooleanOperations.subtract(A, keptPadPolys[i]);
277
- } catch (e) {
339
+ A = Flatten.BooleanOperations.subtract(A, poly);
340
+ } catch {
278
341
  }
279
342
  }
280
343
  let union = null;
281
344
  try {
282
345
  union = Flatten.BooleanOperations.subtract(B, A);
283
- } catch (e) {
346
+ } catch {
284
347
  try {
285
348
  if (keptPadPolys.length > 0) {
286
349
  let U = keptPadPolys[0];
@@ -296,27 +359,36 @@ var constructOutlinesFromPackedComponents = (components, opts = {}) => {
296
359
  union = null;
297
360
  }
298
361
  }
299
- const outlines = [];
300
- for (const face of union.faces) {
301
- if (face.isHole) continue;
302
- const outline = [];
303
- let edge = face.first;
304
- if (!edge) continue;
305
- do {
306
- const shp = edge.shape;
307
- const ps = shp.start ?? shp.ps;
308
- const pe = shp.end ?? shp.pe;
309
- outline.push([
310
- { x: ps.x, y: ps.y },
311
- { x: pe.x, y: pe.y }
312
- ]);
313
- edge = edge.next;
314
- } while (edge !== face.first);
315
- const simplifiedOutline = simplifyCollinearSegments(outline);
316
- outlines.push(simplifiedOutline);
317
- }
318
- return outlines;
362
+ if (!union) return [];
363
+ const parsed = parseFlattenPolygonSegments(union);
364
+ const allOutlines = [
365
+ ...parsed.obstacleFreeLoops.map(simplifyCollinearSegments),
366
+ ...parsed.obstacleContainingLoops.map(simplifyCollinearSegments)
367
+ ];
368
+ return allOutlines.filter((outline) => outline.length >= 3);
319
369
  };
370
+ function filterPadShapes(allPadShapes) {
371
+ const areaOfBox = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
372
+ const containsBox = (outer, inner, eps = 1e-9) => outer.minX - eps <= inner.minX && outer.minY - eps <= inner.minY && outer.maxX + eps >= inner.maxX && outer.maxY + eps >= inner.maxY;
373
+ const sortedByAreaDesc = [...allPadShapes].sort(
374
+ (a, b) => areaOfBox(b.bbox) - areaOfBox(a.bbox)
375
+ );
376
+ const filtered = [];
377
+ for (const shape of sortedByAreaDesc) {
378
+ const w = shape.bbox.maxX - shape.bbox.minX;
379
+ const h = shape.bbox.maxY - shape.bbox.minY;
380
+ if (!(w > 1e-12 && h > 1e-12)) continue;
381
+ let contained = false;
382
+ for (const kept of filtered) {
383
+ if (containsBox(kept.bbox, shape.bbox)) {
384
+ contained = true;
385
+ break;
386
+ }
387
+ }
388
+ if (!contained) filtered.push(shape);
389
+ }
390
+ return filtered;
391
+ }
320
392
 
321
393
  // lib/OutlineSegmentCandidatePointSolver/OutlineSegmentCandidatePointSolver.ts
322
394
  import { clamp } from "@tscircuit/math-utils";
@@ -946,73 +1018,6 @@ var getColorForString = (string, alpha = 1) => {
946
1018
  return `hsl(${hash % 360}, 100%, 50%, ${alpha})`;
947
1019
  };
948
1020
 
949
- // lib/geometry/pointInOutline.ts
950
- var EPS = 1e-9;
951
- function cross2(ax, ay, bx, by) {
952
- return ax * by - ay * bx;
953
- }
954
- function isLeft(a, b, p) {
955
- return cross2(b.x - a.x, b.y - a.y, p.x - a.x, p.y - a.y);
956
- }
957
- function pointOnSegment(p, a, b, eps = EPS) {
958
- const area2 = isLeft(a, b, p);
959
- if (Math.abs(area2) > eps) return false;
960
- const minx = Math.min(a.x, b.x) - eps;
961
- const maxx = Math.max(a.x, b.x) + eps;
962
- const miny = Math.min(a.y, b.y) - eps;
963
- const maxy = Math.max(a.y, b.y) + eps;
964
- return p.x >= minx && p.x <= maxx && p.y >= miny && p.y <= maxy;
965
- }
966
- function pointInOutline(p, segments, rule = "even-odd") {
967
- let minx = Infinity;
968
- let miny = Infinity;
969
- let maxx = -Infinity;
970
- let maxy = -Infinity;
971
- for (const s of segments) {
972
- const [a, b] = s;
973
- minx = Math.min(minx, a.x, b.x);
974
- miny = Math.min(miny, a.y, b.y);
975
- maxx = Math.max(maxx, a.x, b.x);
976
- maxy = Math.max(maxy, a.y, b.y);
977
- }
978
- if (p.x < minx - EPS || p.x > maxx + EPS || p.y < miny - EPS || p.y > maxy + EPS) {
979
- return "outside";
980
- }
981
- for (const s of segments) {
982
- const [a, b] = s;
983
- if (pointOnSegment(p, a, b)) return "boundary";
984
- }
985
- if (rule === "even-odd") {
986
- let crossings = 0;
987
- for (const s of segments) {
988
- const [a, b] = s;
989
- const ay = a.y;
990
- const by = b.y;
991
- const ax = a.x;
992
- const bx = b.x;
993
- const cond = ay <= p.y && p.y < by || by <= p.y && p.y < ay;
994
- if (!cond) continue;
995
- const dy = by - ay;
996
- if (Math.abs(dy) < EPS) continue;
997
- const t = (p.y - ay) / dy;
998
- const xAtY = ax + t * (bx - ax);
999
- if (xAtY > p.x + EPS) crossings++;
1000
- }
1001
- return crossings % 2 === 1 ? "inside" : "outside";
1002
- } else {
1003
- let winding = 0;
1004
- for (const s of segments) {
1005
- const [a, b] = s;
1006
- if (a.y <= p.y) {
1007
- if (b.y > p.y && isLeft(a, b, p) > EPS) winding++;
1008
- } else {
1009
- if (b.y <= p.y && isLeft(a, b, p) < -EPS) winding--;
1010
- }
1011
- }
1012
- return winding !== 0 ? "inside" : "outside";
1013
- }
1014
- }
1015
-
1016
1021
  // lib/OutlineSegmentCandidatePointSolver/getOutwardNormal.ts
1017
1022
  function getOutwardNormal(outlineSegment, ccwFullOutline) {
1018
1023
  const [p1, p2] = outlineSegment;
@@ -1026,29 +1031,6 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1026
1031
  const dirY = dy / len;
1027
1032
  const left = { x: -dirY, y: dirX };
1028
1033
  const right = { x: dirY, y: -dirX };
1029
- const mid = {
1030
- x: (p1.x + p2.x) / 2,
1031
- y: (p1.y + p2.y) / 2
1032
- };
1033
- const bbox = getOutlineBoundsWithMargin(ccwFullOutline);
1034
- const scale = Math.max(bbox.maxX - bbox.minX, bbox.maxY - bbox.minY) || 1;
1035
- const testDistance = Math.max(1e-4, 1e-3 * scale);
1036
- const testLeft = {
1037
- x: mid.x + left.x * testDistance,
1038
- y: mid.y + left.y * testDistance
1039
- };
1040
- const testRight = {
1041
- x: mid.x + right.x * testDistance,
1042
- y: mid.y + right.y * testDistance
1043
- };
1044
- const locLeft = pointInOutline(testLeft, ccwFullOutline);
1045
- if (locLeft === "outside") {
1046
- return left;
1047
- }
1048
- const locRight = pointInOutline(testRight, ccwFullOutline);
1049
- if (locRight === "outside") {
1050
- return right;
1051
- }
1052
1034
  const verts = [];
1053
1035
  if (ccwFullOutline.length > 0) {
1054
1036
  verts.push(ccwFullOutline[0][0]);
@@ -1056,7 +1038,7 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1056
1038
  verts.push(seg[1]);
1057
1039
  }
1058
1040
  }
1059
- const signedArea = (() => {
1041
+ const signedArea2 = (() => {
1060
1042
  let a = 0;
1061
1043
  for (let i = 0; i < verts.length; i++) {
1062
1044
  const v1 = verts[i];
@@ -1065,35 +1047,12 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1065
1047
  }
1066
1048
  return a / 2;
1067
1049
  })();
1068
- if (Math.abs(signedArea) > 1e-12) {
1069
- return signedArea > 0 ? right : left;
1050
+ const geometricOutward = signedArea2 > 0 ? right : left;
1051
+ const isCW = signedArea2 < 0;
1052
+ if (isCW) {
1053
+ return { x: -geometricOutward.x, y: -geometricOutward.y };
1070
1054
  }
1071
- const center = {
1072
- x: (bbox.minX + bbox.maxX) / 2,
1073
- y: (bbox.minY + bbox.maxY) / 2
1074
- };
1075
- const away = { x: mid.x - center.x, y: mid.y - center.y };
1076
- const dotLeft = left.x * away.x + left.y * away.y;
1077
- const dotRight = right.x * away.x + right.y * away.y;
1078
- return dotRight >= dotLeft ? right : left;
1079
- }
1080
- function getOutlineBoundsWithMargin(ccwFullOutline, margin = 0) {
1081
- let minX = Infinity;
1082
- let minY = Infinity;
1083
- let maxX = -Infinity;
1084
- let maxY = -Infinity;
1085
- for (const [p1, p2] of ccwFullOutline) {
1086
- minX = Math.min(minX, p1.x, p2.x);
1087
- minY = Math.min(minY, p1.y, p2.y);
1088
- maxX = Math.max(maxX, p1.x, p2.x);
1089
- maxY = Math.max(maxY, p1.y, p2.y);
1090
- }
1091
- return {
1092
- minX: minX - margin,
1093
- minY: minY - margin,
1094
- maxX: maxX + margin,
1095
- maxY: maxY + margin
1096
- };
1055
+ return geometricOutward;
1097
1056
  }
1098
1057
 
1099
1058
  // lib/LargestRectOutsideOutlineFromPointSolver.ts
@@ -1103,26 +1062,34 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1103
1062
  origin;
1104
1063
  globalBounds;
1105
1064
  largestRect = null;
1065
+ /**
1066
+ * Mode for finding rectangles:
1067
+ * - "outside": Find rect outside the polygon (for CCW obstacle boundaries)
1068
+ * - "inside": Find rect inside the polygon (for CW free space pockets)
1069
+ */
1070
+ mode;
1106
1071
  constructor(params) {
1107
1072
  super();
1108
1073
  this.ccwFullOutline = params.ccwFullOutline;
1109
1074
  this.origin = params.origin;
1110
1075
  this.globalBounds = params.globalBounds;
1076
+ this.mode = params.mode ?? "outside";
1111
1077
  }
1112
1078
  getConstructorParams() {
1113
1079
  return {
1114
1080
  ccwFullOutline: this.ccwFullOutline,
1115
1081
  origin: this.origin,
1116
- globalBounds: this.globalBounds
1082
+ globalBounds: this.globalBounds,
1083
+ mode: this.mode
1117
1084
  };
1118
1085
  }
1119
1086
  _setup() {
1120
1087
  }
1121
1088
  _step() {
1122
- this.largestRect = this.computeLargestRectOutside();
1089
+ this.largestRect = this.computeLargestRect();
1123
1090
  this.solved = true;
1124
1091
  }
1125
- computeLargestRectOutside() {
1092
+ computeLargestRect() {
1126
1093
  const edges = this.makeEdges(this.ccwFullOutline);
1127
1094
  const bounds = {
1128
1095
  x: this.globalBounds.minX,
@@ -1134,7 +1101,7 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1134
1101
  edges,
1135
1102
  this.origin,
1136
1103
  bounds,
1137
- "outside"
1104
+ this.mode
1138
1105
  );
1139
1106
  }
1140
1107
  almostEqual(a, b, eps = 1e-9) {
@@ -1196,6 +1163,9 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1196
1163
  bx1,
1197
1164
  bx2
1198
1165
  );
1166
+ if (mode === "inside") {
1167
+ return inside;
1168
+ }
1199
1169
  const outs = [];
1200
1170
  let prev = bx1;
1201
1171
  for (const [L, R] of inside) {
@@ -1460,6 +1430,9 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1460
1430
  optimalPosition;
1461
1431
  irlsSolver;
1462
1432
  twoPhaseIrlsSolver;
1433
+ largestRectBounds;
1434
+ largestRectMidPoint;
1435
+ largestRectOrigin;
1463
1436
  constructor(params) {
1464
1437
  super();
1465
1438
  this.outlineSegment = params.outlineSegment;
@@ -1529,19 +1502,36 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1529
1502
  componentBounds.maxY - componentBounds.minY
1530
1503
  ) * 2 + this.minGap * 2
1531
1504
  });
1505
+ this.largestRectMidPoint = {
1506
+ x: (p1.x + p2.x) / 2,
1507
+ y: (p1.y + p2.y) / 2
1508
+ };
1509
+ this.largestRectOrigin = {
1510
+ x: this.largestRectMidPoint.x + outwardNormal.x * 1e-4,
1511
+ y: this.largestRectMidPoint.y + outwardNormal.y * 1e-4
1512
+ };
1513
+ const outlinePoints = this.ccwFullOutline.flatMap(([p]) => p);
1514
+ let signedArea2 = 0;
1515
+ for (let i = 0; i < outlinePoints.length; i++) {
1516
+ const p12 = outlinePoints[i];
1517
+ const p22 = outlinePoints[(i + 1) % outlinePoints.length];
1518
+ signedArea2 += p12.x * p22.y - p22.x * p12.y;
1519
+ }
1520
+ signedArea2 /= 2;
1521
+ const isCW = signedArea2 < 0;
1522
+ const rectSearchMode = isCW ? "inside" : "outside";
1532
1523
  const largestRectSolverParams = {
1533
- ccwFullOutline: this.ccwFullOutline.flatMap(([p]) => p),
1524
+ ccwFullOutline: outlinePoints,
1534
1525
  globalBounds: packedComponentBoundsWithMargin,
1535
- origin: {
1536
- x: (p1.x + p2.x) / 2 + outwardNormal.x * 1e-4,
1537
- y: (p1.y + p2.y) / 2 + outwardNormal.y * 1e-4
1538
- }
1526
+ origin: this.largestRectOrigin,
1527
+ mode: rectSearchMode
1539
1528
  };
1540
1529
  const largestRectSolver = new LargestRectOutsideOutlineFromPointSolver(
1541
1530
  largestRectSolverParams
1542
1531
  );
1543
1532
  largestRectSolver.solve();
1544
1533
  const largestRectBounds = largestRectSolver.getLargestRectBounds();
1534
+ this.largestRectBounds = largestRectBounds;
1545
1535
  const segmentNormAbs = {
1546
1536
  x: Math.abs(
1547
1537
  Math.sign(this.outlineSegment[1].x - this.outlineSegment[0].x)
@@ -1860,6 +1850,31 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1860
1850
  label: "Viable Bounds"
1861
1851
  });
1862
1852
  }
1853
+ if (this.largestRectBounds) {
1854
+ graphics.rects.push({
1855
+ center: {
1856
+ x: (this.largestRectBounds.minX + this.largestRectBounds.maxX) / 2,
1857
+ y: (this.largestRectBounds.minY + this.largestRectBounds.maxY) / 2
1858
+ },
1859
+ width: this.largestRectBounds.maxX - this.largestRectBounds.minX,
1860
+ height: this.largestRectBounds.maxY - this.largestRectBounds.minY,
1861
+ fill: "rgba(255,0,255,0.4)"
1862
+ });
1863
+ }
1864
+ if (this.largestRectMidPoint) {
1865
+ graphics.points.push({
1866
+ ...this.largestRectMidPoint,
1867
+ label: "Largest Rect Mid Point",
1868
+ color: "rgba(128,0,255,1)"
1869
+ });
1870
+ }
1871
+ if (this.largestRectOrigin) {
1872
+ graphics.points.push({
1873
+ ...this.largestRectOrigin,
1874
+ label: "Largest Rect Origin",
1875
+ color: "rgba(255,0,128,1)"
1876
+ });
1877
+ }
1863
1878
  if (this.viableOutlineSegment) {
1864
1879
  const [p1, p2] = this.viableOutlineSegment;
1865
1880
  graphics.lines.push({
@@ -2182,6 +2197,57 @@ var SingleComponentPackSolver = class extends BaseSolver4 {
2182
2197
  });
2183
2198
  }
2184
2199
  }
2200
+ if (this.boundaryOutline && this.boundaryOutline.length >= 3) {
2201
+ const boundarySegments = [];
2202
+ for (let i = 0; i < this.boundaryOutline.length; i++) {
2203
+ const p1 = this.boundaryOutline[i];
2204
+ const p2 = this.boundaryOutline[(i + 1) % this.boundaryOutline.length];
2205
+ boundarySegments.push([p1, p2]);
2206
+ }
2207
+ const boundaryOutlineIndex = this.outlines.length;
2208
+ for (let i = 0; i < boundarySegments.length; i++) {
2209
+ const segment = boundarySegments[i];
2210
+ this.queuedOutlineSegments.push({
2211
+ segment,
2212
+ availableRotations: [...availableRotations],
2213
+ segmentIndex: boundaryOutlineIndex * 1e3 + i,
2214
+ ccwFullOutline: boundarySegments
2215
+ });
2216
+ }
2217
+ }
2218
+ let obstacleOutlineIndex = this.outlines.length + 1;
2219
+ for (const obstacle of this.obstacles) {
2220
+ const hw = obstacle.width / 2 + this.minGap;
2221
+ const hh = obstacle.height / 2 + this.minGap;
2222
+ const cx = obstacle.absoluteCenter.x;
2223
+ const cy = obstacle.absoluteCenter.y;
2224
+ const obstacleCorners = [
2225
+ { x: cx - hw, y: cy - hh },
2226
+ { x: cx + hw, y: cy - hh },
2227
+ { x: cx + hw, y: cy + hh },
2228
+ { x: cx - hw, y: cy + hh }
2229
+ ];
2230
+ const obstacleSegments = [
2231
+ [obstacleCorners[0], obstacleCorners[1]],
2232
+ // bottom
2233
+ [obstacleCorners[1], obstacleCorners[2]],
2234
+ // right
2235
+ [obstacleCorners[2], obstacleCorners[3]],
2236
+ // top
2237
+ [obstacleCorners[3], obstacleCorners[0]]
2238
+ // left
2239
+ ];
2240
+ for (let i = 0; i < obstacleSegments.length; i++) {
2241
+ const segment = obstacleSegments[i];
2242
+ this.queuedOutlineSegments.push({
2243
+ segment,
2244
+ availableRotations: [...availableRotations],
2245
+ segmentIndex: obstacleOutlineIndex * 1e3 + i,
2246
+ ccwFullOutline: obstacleSegments
2247
+ });
2248
+ }
2249
+ obstacleOutlineIndex++;
2250
+ }
2185
2251
  this.currentPhase = "segment_candidate";
2186
2252
  this.currentSegmentIndex = 0;
2187
2253
  this.currentRotationIndex = 0;
@@ -2568,26 +2634,26 @@ function getPolygonCentroid(points) {
2568
2634
  const sumY = points.reduce((sum, p) => sum + p.y, 0);
2569
2635
  return { x: sumX / points.length, y: sumY / points.length };
2570
2636
  }
2571
- let signedArea = 0;
2637
+ let signedArea2 = 0;
2572
2638
  let cx = 0;
2573
2639
  let cy = 0;
2574
2640
  for (let i = 0; i < points.length; i++) {
2575
2641
  const p1 = points[i];
2576
2642
  const p2 = points[(i + 1) % points.length];
2577
2643
  const crossProduct = p1.x * p2.y - p2.x * p1.y;
2578
- signedArea += crossProduct;
2644
+ signedArea2 += crossProduct;
2579
2645
  cx += (p1.x + p2.x) * crossProduct;
2580
2646
  cy += (p1.y + p2.y) * crossProduct;
2581
2647
  }
2582
- signedArea *= 0.5;
2583
- const area = Math.abs(signedArea);
2648
+ signedArea2 *= 0.5;
2649
+ const area = Math.abs(signedArea2);
2584
2650
  if (area < 1e-10) {
2585
2651
  const sumX = points.reduce((sum, p) => sum + p.x, 0);
2586
2652
  const sumY = points.reduce((sum, p) => sum + p.y, 0);
2587
2653
  return { x: sumX / points.length, y: sumY / points.length };
2588
2654
  }
2589
- cx /= 6 * signedArea;
2590
- cy /= 6 * signedArea;
2655
+ cx /= 6 * signedArea2;
2656
+ cy /= 6 * signedArea2;
2591
2657
  return { x: cx, y: cy };
2592
2658
  }
2593
2659
 
@@ -2606,8 +2672,14 @@ var PackSolver2 = class extends BaseSolver5 {
2606
2672
  }
2607
2673
  _setup() {
2608
2674
  const { components, packOrderStrategy, packFirst = [] } = this.packInput;
2675
+ const validComponents = components.filter((component) => {
2676
+ if (component.pads.length === 0) return false;
2677
+ return component.pads.every(
2678
+ (pad) => Number.isFinite(pad.size.x) && Number.isFinite(pad.size.y) && pad.size.x > 0 && pad.size.y > 0
2679
+ );
2680
+ });
2609
2681
  this.unpackedComponentQueue = sortComponentQueue({
2610
- components,
2682
+ components: validComponents,
2611
2683
  packOrderStrategy,
2612
2684
  packFirst
2613
2685
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "calculate-packing",
3
3
  "main": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.0.59",
5
+ "version": "0.0.61",
6
6
  "description": "Calculate a packing layout with support for different strategy configurations",
7
7
  "scripts": {
8
8
  "start": "cosmos",
@@ -22,7 +22,7 @@
22
22
  "@tscircuit/circuit-json-util": "^0.0.66",
23
23
  "@tscircuit/footprinter": "^0.0.203",
24
24
  "@tscircuit/math-utils": "^0.0.25",
25
- "@tscircuit/solver-utils": "^0.0.3",
25
+ "@tscircuit/solver-utils": "^0.0.4",
26
26
  "@types/bun": "latest",
27
27
  "@types/react": "^19.1.8",
28
28
  "@types/react-dom": "^19.1.6",
@@ -35,6 +35,7 @@
35
35
  "react-cosmos": "^7.0.0",
36
36
  "react-cosmos-plugin-vite": "^7.0.0",
37
37
  "react-dom": "^19.1.0",
38
+ "stack-svgs": "^0.0.1",
38
39
  "tscircuit": "^0.0.693",
39
40
  "tsup": "^8.5.0",
40
41
  "vite": "^7.1.2"