calculate-packing 0.0.60 → 0.0.62

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
@@ -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,40 @@ 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(
366
+ (outline) => simplifyCollinearSegments(outline)
367
+ ),
368
+ ...parsed.obstacleContainingLoops.map(
369
+ (outline) => simplifyCollinearSegments(outline)
370
+ )
371
+ ];
372
+ return allOutlines.filter((outline) => outline.length >= 3);
319
373
  };
374
+ function filterPadShapes(allPadShapes) {
375
+ const areaOfBox = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
376
+ 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;
377
+ const sortedByAreaDesc = [...allPadShapes].sort(
378
+ (a, b) => areaOfBox(b.bbox) - areaOfBox(a.bbox)
379
+ );
380
+ const filtered = [];
381
+ for (const shape of sortedByAreaDesc) {
382
+ const w = shape.bbox.maxX - shape.bbox.minX;
383
+ const h = shape.bbox.maxY - shape.bbox.minY;
384
+ if (!(w > 1e-12 && h > 1e-12)) continue;
385
+ let contained = false;
386
+ for (const kept of filtered) {
387
+ if (containsBox(kept.bbox, shape.bbox)) {
388
+ contained = true;
389
+ break;
390
+ }
391
+ }
392
+ if (!contained) filtered.push(shape);
393
+ }
394
+ return filtered;
395
+ }
320
396
 
321
397
  // lib/OutlineSegmentCandidatePointSolver/OutlineSegmentCandidatePointSolver.ts
322
398
  import { clamp } from "@tscircuit/math-utils";
@@ -946,73 +1022,6 @@ var getColorForString = (string, alpha = 1) => {
946
1022
  return `hsl(${hash % 360}, 100%, 50%, ${alpha})`;
947
1023
  };
948
1024
 
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
1025
  // lib/OutlineSegmentCandidatePointSolver/getOutwardNormal.ts
1017
1026
  function getOutwardNormal(outlineSegment, ccwFullOutline) {
1018
1027
  const [p1, p2] = outlineSegment;
@@ -1026,29 +1035,6 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1026
1035
  const dirY = dy / len;
1027
1036
  const left = { x: -dirY, y: dirX };
1028
1037
  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
1038
  const verts = [];
1053
1039
  if (ccwFullOutline.length > 0) {
1054
1040
  verts.push(ccwFullOutline[0][0]);
@@ -1056,7 +1042,7 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1056
1042
  verts.push(seg[1]);
1057
1043
  }
1058
1044
  }
1059
- const signedArea = (() => {
1045
+ const signedArea2 = (() => {
1060
1046
  let a = 0;
1061
1047
  for (let i = 0; i < verts.length; i++) {
1062
1048
  const v1 = verts[i];
@@ -1065,35 +1051,12 @@ function getOutwardNormal(outlineSegment, ccwFullOutline) {
1065
1051
  }
1066
1052
  return a / 2;
1067
1053
  })();
1068
- if (Math.abs(signedArea) > 1e-12) {
1069
- return signedArea > 0 ? right : left;
1054
+ const geometricOutward = signedArea2 > 0 ? right : left;
1055
+ const isCW = signedArea2 < 0;
1056
+ if (isCW) {
1057
+ return { x: -geometricOutward.x, y: -geometricOutward.y };
1070
1058
  }
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
- };
1059
+ return geometricOutward;
1097
1060
  }
1098
1061
 
1099
1062
  // lib/LargestRectOutsideOutlineFromPointSolver.ts
@@ -1103,26 +1066,34 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1103
1066
  origin;
1104
1067
  globalBounds;
1105
1068
  largestRect = null;
1069
+ /**
1070
+ * Mode for finding rectangles:
1071
+ * - "outside": Find rect outside the polygon (for CCW obstacle boundaries)
1072
+ * - "inside": Find rect inside the polygon (for CW free space pockets)
1073
+ */
1074
+ mode;
1106
1075
  constructor(params) {
1107
1076
  super();
1108
1077
  this.ccwFullOutline = params.ccwFullOutline;
1109
1078
  this.origin = params.origin;
1110
1079
  this.globalBounds = params.globalBounds;
1080
+ this.mode = params.mode ?? "outside";
1111
1081
  }
1112
1082
  getConstructorParams() {
1113
1083
  return {
1114
1084
  ccwFullOutline: this.ccwFullOutline,
1115
1085
  origin: this.origin,
1116
- globalBounds: this.globalBounds
1086
+ globalBounds: this.globalBounds,
1087
+ mode: this.mode
1117
1088
  };
1118
1089
  }
1119
1090
  _setup() {
1120
1091
  }
1121
1092
  _step() {
1122
- this.largestRect = this.computeLargestRectOutside();
1093
+ this.largestRect = this.computeLargestRect();
1123
1094
  this.solved = true;
1124
1095
  }
1125
- computeLargestRectOutside() {
1096
+ computeLargestRect() {
1126
1097
  const edges = this.makeEdges(this.ccwFullOutline);
1127
1098
  const bounds = {
1128
1099
  x: this.globalBounds.minX,
@@ -1134,7 +1105,7 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1134
1105
  edges,
1135
1106
  this.origin,
1136
1107
  bounds,
1137
- "outside"
1108
+ this.mode
1138
1109
  );
1139
1110
  }
1140
1111
  almostEqual(a, b, eps = 1e-9) {
@@ -1196,6 +1167,9 @@ var LargestRectOutsideOutlineFromPointSolver = class extends BaseSolver2 {
1196
1167
  bx1,
1197
1168
  bx2
1198
1169
  );
1170
+ if (mode === "inside") {
1171
+ return inside;
1172
+ }
1199
1173
  const outs = [];
1200
1174
  let prev = bx1;
1201
1175
  for (const [L, R] of inside) {
@@ -1460,6 +1434,9 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1460
1434
  optimalPosition;
1461
1435
  irlsSolver;
1462
1436
  twoPhaseIrlsSolver;
1437
+ largestRectBounds;
1438
+ largestRectMidPoint;
1439
+ largestRectOrigin;
1463
1440
  constructor(params) {
1464
1441
  super();
1465
1442
  this.outlineSegment = params.outlineSegment;
@@ -1529,19 +1506,36 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1529
1506
  componentBounds.maxY - componentBounds.minY
1530
1507
  ) * 2 + this.minGap * 2
1531
1508
  });
1509
+ this.largestRectMidPoint = {
1510
+ x: (p1.x + p2.x) / 2,
1511
+ y: (p1.y + p2.y) / 2
1512
+ };
1513
+ this.largestRectOrigin = {
1514
+ x: this.largestRectMidPoint.x + outwardNormal.x * 1e-4,
1515
+ y: this.largestRectMidPoint.y + outwardNormal.y * 1e-4
1516
+ };
1517
+ const outlinePoints = this.ccwFullOutline.flatMap(([p]) => p);
1518
+ let signedArea2 = 0;
1519
+ for (let i = 0; i < outlinePoints.length; i++) {
1520
+ const p12 = outlinePoints[i];
1521
+ const p22 = outlinePoints[(i + 1) % outlinePoints.length];
1522
+ signedArea2 += p12.x * p22.y - p22.x * p12.y;
1523
+ }
1524
+ signedArea2 /= 2;
1525
+ const isCW = signedArea2 < 0;
1526
+ const rectSearchMode = isCW ? "inside" : "outside";
1532
1527
  const largestRectSolverParams = {
1533
- ccwFullOutline: this.ccwFullOutline.flatMap(([p]) => p),
1528
+ ccwFullOutline: outlinePoints,
1534
1529
  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
- }
1530
+ origin: this.largestRectOrigin,
1531
+ mode: rectSearchMode
1539
1532
  };
1540
1533
  const largestRectSolver = new LargestRectOutsideOutlineFromPointSolver(
1541
1534
  largestRectSolverParams
1542
1535
  );
1543
1536
  largestRectSolver.solve();
1544
1537
  const largestRectBounds = largestRectSolver.getLargestRectBounds();
1538
+ this.largestRectBounds = largestRectBounds;
1545
1539
  const segmentNormAbs = {
1546
1540
  x: Math.abs(
1547
1541
  Math.sign(this.outlineSegment[1].x - this.outlineSegment[0].x)
@@ -1860,6 +1854,31 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1860
1854
  label: "Viable Bounds"
1861
1855
  });
1862
1856
  }
1857
+ if (this.largestRectBounds) {
1858
+ graphics.rects.push({
1859
+ center: {
1860
+ x: (this.largestRectBounds.minX + this.largestRectBounds.maxX) / 2,
1861
+ y: (this.largestRectBounds.minY + this.largestRectBounds.maxY) / 2
1862
+ },
1863
+ width: this.largestRectBounds.maxX - this.largestRectBounds.minX,
1864
+ height: this.largestRectBounds.maxY - this.largestRectBounds.minY,
1865
+ fill: "rgba(255,0,255,0.4)"
1866
+ });
1867
+ }
1868
+ if (this.largestRectMidPoint) {
1869
+ graphics.points.push({
1870
+ ...this.largestRectMidPoint,
1871
+ label: "Largest Rect Mid Point",
1872
+ color: "rgba(128,0,255,1)"
1873
+ });
1874
+ }
1875
+ if (this.largestRectOrigin) {
1876
+ graphics.points.push({
1877
+ ...this.largestRectOrigin,
1878
+ label: "Largest Rect Origin",
1879
+ color: "rgba(255,0,128,1)"
1880
+ });
1881
+ }
1863
1882
  if (this.viableOutlineSegment) {
1864
1883
  const [p1, p2] = this.viableOutlineSegment;
1865
1884
  graphics.lines.push({
@@ -2182,6 +2201,57 @@ var SingleComponentPackSolver = class extends BaseSolver4 {
2182
2201
  });
2183
2202
  }
2184
2203
  }
2204
+ if (this.boundaryOutline && this.boundaryOutline.length >= 3) {
2205
+ const boundarySegments = [];
2206
+ for (let i = 0; i < this.boundaryOutline.length; i++) {
2207
+ const p1 = this.boundaryOutline[i];
2208
+ const p2 = this.boundaryOutline[(i + 1) % this.boundaryOutline.length];
2209
+ boundarySegments.push([p1, p2]);
2210
+ }
2211
+ const boundaryOutlineIndex = this.outlines.length;
2212
+ for (let i = 0; i < boundarySegments.length; i++) {
2213
+ const segment = boundarySegments[i];
2214
+ this.queuedOutlineSegments.push({
2215
+ segment,
2216
+ availableRotations: [...availableRotations],
2217
+ segmentIndex: boundaryOutlineIndex * 1e3 + i,
2218
+ ccwFullOutline: boundarySegments
2219
+ });
2220
+ }
2221
+ }
2222
+ let obstacleOutlineIndex = this.outlines.length + 1;
2223
+ for (const obstacle of this.obstacles) {
2224
+ const hw = obstacle.width / 2 + this.minGap;
2225
+ const hh = obstacle.height / 2 + this.minGap;
2226
+ const cx = obstacle.absoluteCenter.x;
2227
+ const cy = obstacle.absoluteCenter.y;
2228
+ const obstacleCorners = [
2229
+ { x: cx - hw, y: cy - hh },
2230
+ { x: cx + hw, y: cy - hh },
2231
+ { x: cx + hw, y: cy + hh },
2232
+ { x: cx - hw, y: cy + hh }
2233
+ ];
2234
+ const obstacleSegments = [
2235
+ [obstacleCorners[0], obstacleCorners[1]],
2236
+ // bottom
2237
+ [obstacleCorners[1], obstacleCorners[2]],
2238
+ // right
2239
+ [obstacleCorners[2], obstacleCorners[3]],
2240
+ // top
2241
+ [obstacleCorners[3], obstacleCorners[0]]
2242
+ // left
2243
+ ];
2244
+ for (let i = 0; i < obstacleSegments.length; i++) {
2245
+ const segment = obstacleSegments[i];
2246
+ this.queuedOutlineSegments.push({
2247
+ segment,
2248
+ availableRotations: [...availableRotations],
2249
+ segmentIndex: obstacleOutlineIndex * 1e3 + i,
2250
+ ccwFullOutline: obstacleSegments
2251
+ });
2252
+ }
2253
+ obstacleOutlineIndex++;
2254
+ }
2185
2255
  this.currentPhase = "segment_candidate";
2186
2256
  this.currentSegmentIndex = 0;
2187
2257
  this.currentRotationIndex = 0;
@@ -2568,26 +2638,26 @@ function getPolygonCentroid(points) {
2568
2638
  const sumY = points.reduce((sum, p) => sum + p.y, 0);
2569
2639
  return { x: sumX / points.length, y: sumY / points.length };
2570
2640
  }
2571
- let signedArea = 0;
2641
+ let signedArea2 = 0;
2572
2642
  let cx = 0;
2573
2643
  let cy = 0;
2574
2644
  for (let i = 0; i < points.length; i++) {
2575
2645
  const p1 = points[i];
2576
2646
  const p2 = points[(i + 1) % points.length];
2577
2647
  const crossProduct = p1.x * p2.y - p2.x * p1.y;
2578
- signedArea += crossProduct;
2648
+ signedArea2 += crossProduct;
2579
2649
  cx += (p1.x + p2.x) * crossProduct;
2580
2650
  cy += (p1.y + p2.y) * crossProduct;
2581
2651
  }
2582
- signedArea *= 0.5;
2583
- const area = Math.abs(signedArea);
2652
+ signedArea2 *= 0.5;
2653
+ const area = Math.abs(signedArea2);
2584
2654
  if (area < 1e-10) {
2585
2655
  const sumX = points.reduce((sum, p) => sum + p.x, 0);
2586
2656
  const sumY = points.reduce((sum, p) => sum + p.y, 0);
2587
2657
  return { x: sumX / points.length, y: sumY / points.length };
2588
2658
  }
2589
- cx /= 6 * signedArea;
2590
- cy /= 6 * signedArea;
2659
+ cx /= 6 * signedArea2;
2660
+ cy /= 6 * signedArea2;
2591
2661
  return { x: cx, y: cy };
2592
2662
  }
2593
2663
 
@@ -2606,8 +2676,14 @@ var PackSolver2 = class extends BaseSolver5 {
2606
2676
  }
2607
2677
  _setup() {
2608
2678
  const { components, packOrderStrategy, packFirst = [] } = this.packInput;
2679
+ const validComponents = components.filter((component) => {
2680
+ if (component.pads.length === 0) return false;
2681
+ return component.pads.every(
2682
+ (pad) => Number.isFinite(pad.size.x) && Number.isFinite(pad.size.y) && pad.size.x > 0 && pad.size.y > 0
2683
+ );
2684
+ });
2609
2685
  this.unpackedComponentQueue = sortComponentQueue({
2610
- components,
2686
+ components: validComponents,
2611
2687
  packOrderStrategy,
2612
2688
  packFirst
2613
2689
  });
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.60",
5
+ "version": "0.0.62",
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"