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 +17 -1
- package/dist/index.js +301 -225
- package/package.json +3 -2
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
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
|
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 (
|
|
337
|
+
for (const poly of keptPadPolys) {
|
|
275
338
|
try {
|
|
276
|
-
A = Flatten.BooleanOperations.subtract(A,
|
|
277
|
-
} catch
|
|
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
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
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
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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.
|
|
1093
|
+
this.largestRect = this.computeLargestRect();
|
|
1123
1094
|
this.solved = true;
|
|
1124
1095
|
}
|
|
1125
|
-
|
|
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
|
-
|
|
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:
|
|
1528
|
+
ccwFullOutline: outlinePoints,
|
|
1534
1529
|
globalBounds: packedComponentBoundsWithMargin,
|
|
1535
|
-
origin:
|
|
1536
|
-
|
|
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
|
|
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
|
-
|
|
2648
|
+
signedArea2 += crossProduct;
|
|
2579
2649
|
cx += (p1.x + p2.x) * crossProduct;
|
|
2580
2650
|
cy += (p1.y + p2.y) * crossProduct;
|
|
2581
2651
|
}
|
|
2582
|
-
|
|
2583
|
-
const area = Math.abs(
|
|
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 *
|
|
2590
|
-
cy /= 6 *
|
|
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.
|
|
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.
|
|
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"
|