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 +3 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +297 -225
- package/package.json +3 -2
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
|
|
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,36 @@ var constructOutlinesFromPackedComponents = (components, opts = {}) => {
|
|
|
296
359
|
union = null;
|
|
297
360
|
}
|
|
298
361
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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.
|
|
1089
|
+
this.largestRect = this.computeLargestRect();
|
|
1123
1090
|
this.solved = true;
|
|
1124
1091
|
}
|
|
1125
|
-
|
|
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
|
-
|
|
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:
|
|
1524
|
+
ccwFullOutline: outlinePoints,
|
|
1534
1525
|
globalBounds: packedComponentBoundsWithMargin,
|
|
1535
|
-
origin:
|
|
1536
|
-
|
|
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
|
|
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
|
-
|
|
2644
|
+
signedArea2 += crossProduct;
|
|
2579
2645
|
cx += (p1.x + p2.x) * crossProduct;
|
|
2580
2646
|
cy += (p1.y + p2.y) * crossProduct;
|
|
2581
2647
|
}
|
|
2582
|
-
|
|
2583
|
-
const area = Math.abs(
|
|
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 *
|
|
2590
|
-
cy /= 6 *
|
|
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.
|
|
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.
|
|
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"
|