calculate-packing 0.0.12 → 0.0.13

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +49 -28
  2. package/dist/index.js +1090 -736
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1,92 +1,124 @@
1
- // lib/solver-utils/BaseSolver.ts
2
- var BaseSolver = class {
3
- MAX_ITERATIONS = 1e3;
4
- solved = false;
5
- failed = false;
6
- iterations = 0;
7
- progress = 0;
8
- error = null;
9
- activeSubSolver;
10
- failedSubSolvers;
11
- timeToSolve;
12
- stats = {};
13
- _setupDone = false;
14
- setup() {
15
- if (this._setupDone) return;
16
- this._setup();
17
- this._setupDone = true;
18
- }
19
- /** DO NOT OVERRIDE! Override _step() instead */
20
- step() {
21
- if (!this._setupDone) {
22
- this.setup();
23
- }
24
- if (this.solved) return;
25
- if (this.failed) return;
26
- this.iterations++;
27
- try {
28
- this._step();
29
- } catch (e) {
30
- this.error = `${this.constructor.name} error: ${e}`;
31
- console.error(this.error);
32
- this.failed = true;
33
- throw e;
34
- }
35
- if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
36
- this.tryFinalAcceptance();
37
- }
38
- if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
39
- this.error = `${this.constructor.name} ran out of iterations`;
40
- console.error(this.error);
41
- this.failed = true;
42
- }
43
- if ("computeProgress" in this) {
44
- this.progress = this.computeProgress();
1
+ // lib/PackSolver/checkOverlapWithPackedComponents.ts
2
+ function checkOverlapWithPackedComponents({
3
+ component,
4
+ packedComponents,
5
+ minGap
6
+ }) {
7
+ for (const componentPad of component.pads) {
8
+ for (const packedComponent of packedComponents) {
9
+ for (const packedPad of packedComponent.pads) {
10
+ const comp1Bounds = {
11
+ left: componentPad.absoluteCenter.x - componentPad.size.x / 2,
12
+ right: componentPad.absoluteCenter.x + componentPad.size.x / 2,
13
+ bottom: componentPad.absoluteCenter.y - componentPad.size.y / 2,
14
+ top: componentPad.absoluteCenter.y + componentPad.size.y / 2
15
+ };
16
+ const comp2Bounds = {
17
+ left: packedPad.absoluteCenter.x - packedPad.size.x / 2,
18
+ right: packedPad.absoluteCenter.x + packedPad.size.x / 2,
19
+ bottom: packedPad.absoluteCenter.y - packedPad.size.y / 2,
20
+ top: packedPad.absoluteCenter.y + packedPad.size.y / 2
21
+ };
22
+ const xOverlap = comp1Bounds.right > comp2Bounds.left && comp2Bounds.right > comp1Bounds.left;
23
+ const yOverlap = comp1Bounds.top > comp2Bounds.bottom && comp2Bounds.top > comp1Bounds.bottom;
24
+ if (xOverlap && yOverlap) {
25
+ return true;
26
+ }
27
+ if (!xOverlap || !yOverlap) {
28
+ const xGap = xOverlap ? 0 : Math.min(
29
+ Math.abs(comp1Bounds.left - comp2Bounds.right),
30
+ Math.abs(comp2Bounds.left - comp1Bounds.right)
31
+ );
32
+ const yGap = yOverlap ? 0 : Math.min(
33
+ Math.abs(comp1Bounds.bottom - comp2Bounds.top),
34
+ Math.abs(comp2Bounds.bottom - comp1Bounds.top)
35
+ );
36
+ const actualGap = Math.max(xGap, yGap);
37
+ if (actualGap < minGap) {
38
+ return true;
39
+ }
40
+ }
41
+ }
45
42
  }
46
43
  }
47
- _setup() {
48
- }
49
- _step() {
50
- }
51
- getConstructorParams() {
52
- throw new Error("getConstructorParams not implemented");
53
- }
54
- solve() {
55
- const startTime = Date.now();
56
- while (!this.solved && !this.failed) {
57
- this.step();
44
+ return false;
45
+ }
46
+
47
+ // lib/math/computeNearestPointOnSegmentForSegmentSet.ts
48
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
49
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
50
+ var mul = (a, s) => ({ x: a.x * s, y: a.y * s });
51
+ var dot = (a, b) => a.x * b.x + a.y * b.y;
52
+ var clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
53
+ function closestPointOnSegAToSegB(segA, segB) {
54
+ const [p, q] = segA;
55
+ const [r, s] = segB;
56
+ const u = sub(q, p);
57
+ const v = sub(s, r);
58
+ const w0 = sub(p, r);
59
+ const a = dot(u, u);
60
+ const b = dot(u, v);
61
+ const c = dot(v, v);
62
+ const d = dot(u, w0);
63
+ const e = dot(v, w0);
64
+ const EPS = 1e-12;
65
+ const D = a * c - b * b;
66
+ let sN;
67
+ let tN;
68
+ let sD = D;
69
+ let tD = D;
70
+ if (D < EPS) {
71
+ sN = 0;
72
+ sD = 1;
73
+ tN = e;
74
+ tD = c;
75
+ } else {
76
+ sN = b * e - c * d;
77
+ tN = a * e - b * d;
78
+ if (sN < 0) {
79
+ sN = 0;
80
+ tN = e;
81
+ tD = c;
82
+ } else if (sN > sD) {
83
+ sN = sD;
84
+ tN = e + b;
85
+ tD = c;
58
86
  }
59
- const endTime = Date.now();
60
- this.timeToSolve = endTime - startTime;
61
- }
62
- visualize() {
63
- return {
64
- lines: [],
65
- points: [],
66
- rects: [],
67
- circles: []
68
- };
69
87
  }
70
- /**
71
- * Called when the solver is about to fail, but we want to see if we have an
72
- * "acceptable" or "passable" solution. Mostly used for optimizers that
73
- * have an aggressive early stopping criterion.
74
- */
75
- tryFinalAcceptance() {
88
+ if (tN < 0) {
89
+ tN = 0;
90
+ sN = clamp(-d, 0, a);
91
+ sD = a;
92
+ } else if (tN > tD) {
93
+ tN = tD;
94
+ sN = clamp(-d + b, 0, a);
95
+ sD = a;
76
96
  }
77
- /**
78
- * A lightweight version of the visualize method that can be used to stream
79
- * progress
80
- */
81
- preview() {
82
- return {
83
- lines: [],
84
- points: [],
85
- rects: [],
86
- circles: []
87
- };
97
+ const sParam = sD > EPS ? sN / sD : 0;
98
+ const closestA = add(p, mul(u, sParam));
99
+ const tParam = tD > EPS ? tN / tD : 0;
100
+ const closestB = add(r, mul(v, tParam));
101
+ const diff = sub(closestA, closestB);
102
+ return { pointA: closestA, paramA: sParam, dist2: dot(diff, diff) };
103
+ }
104
+ function computeNearestPointOnSegmentForSegmentSet(segmentA, segmentSet) {
105
+ if (!segmentSet.length)
106
+ throw new Error("segmentSet must contain at least one segment");
107
+ let bestPoint = segmentA[0];
108
+ let bestDist2 = Number.POSITIVE_INFINITY;
109
+ for (const segB of segmentSet) {
110
+ const { pointA, dist2 } = closestPointOnSegAToSegB(segmentA, segB);
111
+ if (dist2 < bestDist2) {
112
+ bestDist2 = dist2;
113
+ bestPoint = pointA;
114
+ if (bestDist2 === 0) break;
115
+ }
88
116
  }
89
- };
117
+ return { nearestPoint: bestPoint, dist: Math.sqrt(bestDist2) };
118
+ }
119
+
120
+ // lib/constructOutlinesFromPackedComponents.ts
121
+ import Flatten from "@flatten-js/core";
90
122
 
91
123
  // lib/math/rotatePoint.ts
92
124
  var rotatePoint = (point, angle, origin = { x: 0, y: 0 }) => {
@@ -118,7 +150,10 @@ var getComponentBounds = (component, minGap = 0) => {
118
150
  { x: pad.offset.x - hw, y: pad.offset.y + hh }
119
151
  ];
120
152
  localCorners.forEach((corner) => {
121
- const world = rotatePoint(corner, component.ccwRotationOffset);
153
+ const world = rotatePoint(
154
+ corner,
155
+ component.ccwRotationOffset * Math.PI / 180
156
+ );
122
157
  const x = world.x + component.center.x;
123
158
  const y = world.y + component.center.y;
124
159
  bounds.minX = Math.min(bounds.minX, x);
@@ -136,7 +171,6 @@ var getComponentBounds = (component, minGap = 0) => {
136
171
  };
137
172
 
138
173
  // lib/constructOutlinesFromPackedComponents.ts
139
- import Flatten from "@flatten-js/core";
140
174
  var constructOutlinesFromPackedComponents = (components, opts = {}) => {
141
175
  const { minGap = 0 } = opts;
142
176
  if (components.length === 0) return [];
@@ -175,6 +209,107 @@ var constructOutlinesFromPackedComponents = (components, opts = {}) => {
175
209
  return outlines;
176
210
  };
177
211
 
212
+ // lib/PackSolver/computeSumDistanceForPosition.ts
213
+ function computeSumDistanceForPosition({
214
+ component,
215
+ position,
216
+ targetNetworkId,
217
+ packedComponents,
218
+ useSquaredDistance = false
219
+ }) {
220
+ const componentPadsOnNetwork = component.pads.filter(
221
+ (pad) => pad.networkId === targetNetworkId
222
+ );
223
+ if (componentPadsOnNetwork.length === 0) return 0;
224
+ const packedPadsOnNetwork = packedComponents.flatMap(
225
+ (component2) => component2.pads.filter((pad) => pad.networkId === targetNetworkId)
226
+ );
227
+ if (packedPadsOnNetwork.length === 0) return 0;
228
+ let sumDistance = 0;
229
+ for (const componentPad of componentPadsOnNetwork) {
230
+ const padPosition = {
231
+ x: position.x + componentPad.offset.x,
232
+ y: position.y + componentPad.offset.y
233
+ };
234
+ let minDistance = Number.POSITIVE_INFINITY;
235
+ for (const packedPad of packedPadsOnNetwork) {
236
+ const dx = padPosition.x - packedPad.absoluteCenter.x;
237
+ const dy = padPosition.y - packedPad.absoluteCenter.y;
238
+ const distance = useSquaredDistance ? dx * dx + dy * dy : Math.hypot(dx, dy);
239
+ if (distance < minDistance) {
240
+ minDistance = distance;
241
+ }
242
+ }
243
+ sumDistance += minDistance === Number.POSITIVE_INFINITY ? 0 : minDistance;
244
+ }
245
+ return sumDistance;
246
+ }
247
+
248
+ // lib/PackSolver/findOptimalPointOnSegment.ts
249
+ function findOptimalPointOnSegment({
250
+ p1,
251
+ p2,
252
+ component,
253
+ networkId,
254
+ packedComponents,
255
+ useSquaredDistance = false
256
+ }) {
257
+ const candidatePoints = [];
258
+ const tolerance = 1e-6;
259
+ let left = 0;
260
+ let right = 1;
261
+ const interpolatePoint = (t) => ({
262
+ x: p1.x + t * (p2.x - p1.x),
263
+ y: p1.y + t * (p2.y - p1.y)
264
+ });
265
+ const evaluateDistance = (t) => {
266
+ const point = interpolatePoint(t);
267
+ const distance = computeSumDistanceForPosition({
268
+ component,
269
+ position: point,
270
+ targetNetworkId: networkId,
271
+ packedComponents,
272
+ useSquaredDistance
273
+ });
274
+ candidatePoints.push({
275
+ ...point,
276
+ networkId,
277
+ distance
278
+ });
279
+ return distance;
280
+ };
281
+ while (right - left > tolerance) {
282
+ const leftThird = left + (right - left) / 3;
283
+ const rightThird = right - (right - left) / 3;
284
+ const leftDistance = evaluateDistance(leftThird);
285
+ const rightDistance = evaluateDistance(rightThird);
286
+ if (leftDistance > rightDistance) {
287
+ left = leftThird;
288
+ } else {
289
+ right = rightThird;
290
+ }
291
+ }
292
+ const optimalT = (left + right) / 2;
293
+ const optimalPoint = interpolatePoint(optimalT);
294
+ const optimalDistance = computeSumDistanceForPosition({
295
+ component,
296
+ position: optimalPoint,
297
+ targetNetworkId: networkId,
298
+ packedComponents,
299
+ useSquaredDistance
300
+ });
301
+ candidatePoints.push({
302
+ ...optimalPoint,
303
+ networkId,
304
+ distance: optimalDistance
305
+ });
306
+ return {
307
+ point: optimalPoint,
308
+ distance: optimalDistance,
309
+ candidatePoints
310
+ };
311
+ }
312
+
178
313
  // lib/testing/createColorMapFromStrings.ts
179
314
  var createColorMapFromStrings = (strings) => {
180
315
  const colorMap = {};
@@ -205,7 +340,7 @@ var getGraphicsFromPackOutput = (packOutput) => {
205
340
  fill: "rgba(0,0,0,0.25)",
206
341
  label: [
207
342
  component.componentId,
208
- `ccwRotationOffset: ${(component.ccwRotationOffset / Math.PI * 180).toFixed(1)}`
343
+ `ccwRotationOffset: ${component.ccwRotationOffset.toFixed(1)}\xB0`
209
344
  ].join("\n")
210
345
  };
211
346
  rects.push(rect);
@@ -241,20 +376,6 @@ var getGraphicsFromPackOutput = (packOutput) => {
241
376
  };
242
377
  };
243
378
 
244
- // lib/PackSolver/setPackedComponentPadCenters.ts
245
- var setPackedComponentPadCenters = (packedComponent) => {
246
- packedComponent.pads = packedComponent.pads.map((pad) => ({
247
- ...pad,
248
- absoluteCenter: (() => {
249
- const rotated = rotatePoint(pad.offset, packedComponent.ccwRotationOffset);
250
- return {
251
- x: packedComponent.center.x + rotated.x,
252
- y: packedComponent.center.y + rotated.y
253
- };
254
- })()
255
- }));
256
- };
257
-
258
379
  // lib/PackSolver/getSegmentsFromPad.ts
259
380
  var getSegmentsFromPad = (pad, { padding = 0 } = {}) => {
260
381
  const segments = [];
@@ -279,408 +400,494 @@ var getSegmentsFromPad = (pad, { padding = 0 } = {}) => {
279
400
  return segments;
280
401
  };
281
402
 
282
- // lib/math/computeNearestPointOnSegmentForSegmentSet.ts
283
- var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
284
- var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
285
- var mul = (a, s) => ({ x: a.x * s, y: a.y * s });
286
- var dot = (a, b) => a.x * b.x + a.y * b.y;
287
- var clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
288
- function closestPointOnSegAToSegB(segA, segB) {
289
- const [p, q] = segA;
290
- const [r, s] = segB;
291
- const u = sub(q, p);
292
- const v = sub(s, r);
293
- const w0 = sub(p, r);
294
- const a = dot(u, u);
295
- const b = dot(u, v);
296
- const c = dot(v, v);
297
- const d = dot(u, w0);
298
- const e = dot(v, w0);
299
- const EPS = 1e-12;
300
- const D = a * c - b * b;
301
- let sN;
302
- let tN;
303
- let sD = D;
304
- let tD = D;
305
- if (D < EPS) {
306
- sN = 0;
307
- sD = 1;
308
- tN = e;
309
- tD = c;
310
- } else {
311
- sN = b * e - c * d;
312
- tN = a * e - b * d;
313
- if (sN < 0) {
314
- sN = 0;
315
- tN = e;
316
- tD = c;
317
- } else if (sN > sD) {
318
- sN = sD;
319
- tN = e + b;
320
- tD = c;
321
- }
403
+ // lib/PackSolver/computeGlobalCenter.ts
404
+ function computeGlobalCenter(packedComponents) {
405
+ if (!packedComponents.length) return { x: 0, y: 0 };
406
+ const sum = packedComponents.reduce(
407
+ (acc, component) => ({
408
+ x: acc.x + component.center.x,
409
+ y: acc.y + component.center.y
410
+ }),
411
+ { x: 0, y: 0 }
412
+ );
413
+ return {
414
+ x: sum.x / packedComponents.length,
415
+ y: sum.y / packedComponents.length
416
+ };
417
+ }
418
+
419
+ // lib/PackSolver/findBestPointForDisconnected.ts
420
+ function findBestPointForDisconnected({
421
+ outlines,
422
+ direction,
423
+ packedComponents
424
+ }) {
425
+ const points = outlines.flatMap(
426
+ (outline) => outline.map(([p1, p2]) => ({
427
+ x: (p1.x + p2.x) / 2,
428
+ y: (p1.y + p2.y) / 2
429
+ }))
430
+ );
431
+ if (!points.length) return { x: 0, y: 0 };
432
+ if (direction !== "nearest_to_center") {
433
+ const extreme = direction === "left" || direction === "down" ? Math.min : Math.max;
434
+ const key = direction === "left" || direction === "right" ? "x" : "y";
435
+ const target = extreme(...points.map((p) => p[key]));
436
+ return points.find((p) => p[key] === target);
322
437
  }
323
- if (tN < 0) {
324
- tN = 0;
325
- sN = clamp(-d, 0, a);
326
- sD = a;
327
- } else if (tN > tD) {
328
- tN = tD;
329
- sN = clamp(-d + b, 0, a);
330
- sD = a;
331
- }
332
- const sParam = sD > EPS ? sN / sD : 0;
333
- const closestA = add(p, mul(u, sParam));
334
- const tParam = tD > EPS ? tN / tD : 0;
335
- const closestB = add(r, mul(v, tParam));
336
- const diff = sub(closestA, closestB);
337
- return { pointA: closestA, paramA: sParam, dist2: dot(diff, diff) };
338
- }
339
- function computeNearestPointOnSegmentForSegmentSet(segmentA, segmentSet) {
340
- if (!segmentSet.length)
341
- throw new Error("segmentSet must contain at least one segment");
342
- let bestPoint = segmentA[0];
343
- let bestDist2 = Number.POSITIVE_INFINITY;
344
- for (const segB of segmentSet) {
345
- const { pointA, dist2 } = closestPointOnSegAToSegB(segmentA, segB);
346
- if (dist2 < bestDist2) {
347
- bestDist2 = dist2;
348
- bestPoint = pointA;
349
- if (bestDist2 === 0) break;
350
- }
351
- }
352
- return { nearestPoint: bestPoint, dist: Math.sqrt(bestDist2) };
438
+ const center = computeGlobalCenter(packedComponents);
439
+ return points.reduce(
440
+ (best, point) => Math.hypot(point.x - center.x, point.y - center.y) < Math.hypot(best.x - center.x, best.y - center.y) ? point : best
441
+ );
353
442
  }
354
443
 
355
- // lib/PackSolver/PackSolver.ts
356
- import { computeDistanceBetweenBoxes } from "@tscircuit/math-utils";
357
-
358
- // lib/PackSolver/translationOptimizer.ts
359
- function computeTranslationBounds(component, initialCenter, packedComponents, minGap) {
360
- const componentBounds = getComponentBounds(component, 0);
361
- const componentWidth = componentBounds.maxX - componentBounds.minX;
362
- const componentHeight = componentBounds.maxY - componentBounds.minY;
363
- let minX = Number.NEGATIVE_INFINITY;
364
- let maxX = Number.POSITIVE_INFINITY;
365
- let minY = Number.NEGATIVE_INFINITY;
366
- let maxY = Number.POSITIVE_INFINITY;
367
- for (const packedComp of packedComponents) {
368
- const packedBounds = getComponentBounds(packedComp, 0);
369
- const packedWidth = packedBounds.maxX - packedBounds.minX;
370
- const packedHeight = packedBounds.maxY - packedBounds.minY;
371
- const minCenterDistanceX = minGap + (componentWidth + packedWidth) / 2;
372
- const minCenterDistanceY = minGap + (componentHeight + packedHeight) / 2;
373
- const packedCenterX = packedComp.center.x;
374
- const packedCenterY = packedComp.center.y;
375
- const leftBound = packedCenterX - minCenterDistanceX;
376
- const rightBound = packedCenterX + minCenterDistanceX;
377
- const bottomBound = packedCenterY - minCenterDistanceY;
378
- const topBound = packedCenterY + minCenterDistanceY;
379
- if (Math.abs(packedCenterX - initialCenter.x) > Math.abs(packedCenterY - initialCenter.y)) {
380
- if (packedCenterX < initialCenter.x) {
381
- minX = Math.max(minX, rightBound);
382
- } else {
383
- maxX = Math.min(maxX, leftBound);
384
- }
385
- } else {
386
- if (packedCenterY < initialCenter.y) {
387
- minY = Math.max(minY, topBound);
388
- } else {
389
- maxY = Math.min(maxY, bottomBound);
444
+ // lib/PackSolver/setPackedComponentPadCenters.ts
445
+ var setPackedComponentPadCenters = (packedComponent) => {
446
+ packedComponent.pads = packedComponent.pads.map((pad) => {
447
+ const rotated = rotatePoint(
448
+ pad.offset,
449
+ packedComponent.ccwRotationOffset * Math.PI / 180
450
+ );
451
+ const normalizedRotation = (packedComponent.ccwRotationOffset % 360 + 360) % 360;
452
+ const shouldSwapDimensions = normalizedRotation === 90 || normalizedRotation === 270;
453
+ return {
454
+ ...pad,
455
+ size: shouldSwapDimensions ? { x: pad.size.y, y: pad.size.x } : pad.size,
456
+ // Keep original dimensions for 0°/180° rotations
457
+ absoluteCenter: {
458
+ x: packedComponent.center.x + rotated.x,
459
+ y: packedComponent.center.y + rotated.y
390
460
  }
461
+ };
462
+ });
463
+ };
464
+
465
+ // lib/PackSolver/placeComponentAtPoint.ts
466
+ function placeComponentAtPoint({
467
+ component,
468
+ point,
469
+ candidateAngles,
470
+ checkOverlap
471
+ }) {
472
+ const evaluatedPositionShadows = [];
473
+ for (const angle of candidateAngles) {
474
+ const pads = component.pads.map((pad) => {
475
+ const rotatedOffset = rotatePoint(pad.offset, angle * Math.PI / 180);
476
+ const normalizedRotation = (angle % 360 + 360) % 360;
477
+ const shouldSwapDimensions = normalizedRotation === 90 || normalizedRotation === 270;
478
+ return {
479
+ ...pad,
480
+ size: shouldSwapDimensions ? { x: pad.size.y, y: pad.size.x } : pad.size,
481
+ absoluteCenter: {
482
+ x: point.x + rotatedOffset.x,
483
+ y: point.y + rotatedOffset.y
484
+ }
485
+ };
486
+ });
487
+ const candidate = {
488
+ ...component,
489
+ center: point,
490
+ ccwRotationOffset: angle,
491
+ pads
492
+ };
493
+ evaluatedPositionShadows.push(candidate);
494
+ if (!checkOverlap(candidate)) {
495
+ Object.assign(component, candidate);
496
+ setPackedComponentPadCenters(component);
497
+ return evaluatedPositionShadows;
391
498
  }
392
499
  }
393
- const maxTranslation = 5;
394
- minX = Math.max(minX, initialCenter.x - maxTranslation);
395
- maxX = Math.min(maxX, initialCenter.x + maxTranslation);
396
- minY = Math.max(minY, initialCenter.y - maxTranslation);
397
- maxY = Math.min(maxY, initialCenter.y + maxTranslation);
398
- return { minX, maxX, minY, maxY };
500
+ component.center = point;
501
+ component.ccwRotationOffset = 0;
502
+ setPackedComponentPadCenters(component);
503
+ return evaluatedPositionShadows;
399
504
  }
400
- function calculateSumDistance(component, candidateCenter, packedComponents) {
401
- const packedPads = packedComponents.flatMap((c) => c.pads);
402
- const connectedPads = component.pads.filter(
403
- (pad) => packedPads.some((pp) => pp.networkId === pad.networkId)
404
- );
405
- if (connectedPads.length === 0) return 0;
406
- let sumDistance = 0;
407
- for (const pad of connectedPads) {
408
- const padAbsolutePos = {
409
- x: candidateCenter.x + pad.offset.x,
410
- y: candidateCenter.y + pad.offset.y
411
- };
412
- const sameNetPads = packedPads.filter(
413
- (pp) => pp.networkId === pad.networkId
414
- );
415
- let minDistance = Number.POSITIVE_INFINITY;
416
- for (const packedPad of sameNetPads) {
417
- const distance = Math.hypot(
418
- padAbsolutePos.x - packedPad.absoluteCenter.x,
419
- padAbsolutePos.y - packedPad.absoluteCenter.y
505
+
506
+ // lib/PackSolver/placeComponentDisconnected.ts
507
+ function placeComponentDisconnected({
508
+ component,
509
+ outlines,
510
+ direction,
511
+ packedComponents,
512
+ candidateAngles,
513
+ checkOverlap
514
+ }) {
515
+ const targetPoint = findBestPointForDisconnected({
516
+ outlines,
517
+ direction,
518
+ packedComponents
519
+ });
520
+ return placeComponentAtPoint({
521
+ component,
522
+ point: targetPoint,
523
+ candidateAngles,
524
+ checkOverlap
525
+ });
526
+ }
527
+
528
+ // lib/PackSolver/RotationSelector.ts
529
+ function selectOptimalRotation(options) {
530
+ const {
531
+ component,
532
+ candidatePoints,
533
+ packedComponents,
534
+ minGap,
535
+ useSquaredDistance,
536
+ checkOverlap
537
+ } = options;
538
+ const candidateAngles = getCandidateAngles(component);
539
+ let globalBestCandidate = null;
540
+ for (const angle of candidateAngles) {
541
+ let bestForThisRotation = null;
542
+ const packedPads = packedComponents.flatMap((c) => c.pads);
543
+ for (const candidatePoint of candidatePoints) {
544
+ const networkId = candidatePoint.networkId;
545
+ const componentPadsOnNetwork = component.pads.filter(
546
+ (p) => p.networkId === networkId
420
547
  );
421
- if (distance < minDistance) {
422
- minDistance = distance;
548
+ if (componentPadsOnNetwork.length > 0) {
549
+ const firstPad = componentPadsOnNetwork[0];
550
+ const rotatedPadOffset = rotatePoint(
551
+ firstPad.offset,
552
+ angle * Math.PI / 180
553
+ );
554
+ const initialCenter = {
555
+ x: candidatePoint.x - rotatedPadOffset.x,
556
+ y: candidatePoint.y - rotatedPadOffset.y
557
+ };
558
+ const tempComponent = {
559
+ ...component,
560
+ center: initialCenter,
561
+ ccwRotationOffset: (angle % 360 + 360) % 360,
562
+ pads: component.pads.map((p) => ({
563
+ ...p,
564
+ absoluteCenter: { x: 0, y: 0 }
565
+ // Will be set by setPackedComponentPadCenters
566
+ }))
567
+ };
568
+ const transformedPads = tempComponent.pads.map((p) => {
569
+ const rotatedOffset = rotatePoint(p.offset, angle * Math.PI / 180);
570
+ const normalizedRotation = (angle % 360 + 360) % 360;
571
+ const shouldSwapDimensions = normalizedRotation === 90 || normalizedRotation === 270;
572
+ return {
573
+ ...p,
574
+ size: shouldSwapDimensions ? { x: p.size.y, y: p.size.x } : p.size,
575
+ absoluteCenter: {
576
+ x: initialCenter.x + rotatedOffset.x,
577
+ y: initialCenter.y + rotatedOffset.y
578
+ }
579
+ };
580
+ });
581
+ tempComponent.pads = transformedPads;
582
+ if (!checkOverlap(tempComponent)) {
583
+ let cost = 0;
584
+ for (const tp of transformedPads) {
585
+ const sameNetPads = packedPads.filter(
586
+ (pp) => pp.networkId === tp.networkId
587
+ );
588
+ if (!sameNetPads.length) continue;
589
+ let bestD = Infinity;
590
+ for (const pp of sameNetPads) {
591
+ const dx = tp.absoluteCenter.x - pp.absoluteCenter.x;
592
+ const dy = tp.absoluteCenter.y - pp.absoluteCenter.y;
593
+ const d = useSquaredDistance ? dx * dx + dy * dy : Math.hypot(dx, dy);
594
+ if (d < bestD) bestD = d;
595
+ }
596
+ cost += bestD === Infinity ? 0 : bestD;
597
+ }
598
+ if (!bestForThisRotation || cost < bestForThisRotation.cost) {
599
+ bestForThisRotation = {
600
+ center: initialCenter,
601
+ angle: (angle % 360 + 360) % 360,
602
+ cost,
603
+ pads: component.pads.map((p) => ({
604
+ ...p,
605
+ absoluteCenter: { x: 0, y: 0 }
606
+ }))
607
+ // Return original pads structure
608
+ };
609
+ }
610
+ }
611
+ }
612
+ const centerTrial = {
613
+ ...component,
614
+ center: { x: candidatePoint.x, y: candidatePoint.y },
615
+ ccwRotationOffset: (angle % 360 + 360) % 360,
616
+ pads: component.pads.map((p) => {
617
+ const rotatedOffset = rotatePoint(p.offset, angle * Math.PI / 180);
618
+ const normalizedRotation = (angle % 360 + 360) % 360;
619
+ const shouldSwapDimensions = normalizedRotation === 90 || normalizedRotation === 270;
620
+ return {
621
+ ...p,
622
+ size: shouldSwapDimensions ? { x: p.size.y, y: p.size.x } : p.size,
623
+ absoluteCenter: {
624
+ x: candidatePoint.x + rotatedOffset.x,
625
+ y: candidatePoint.y + rotatedOffset.y
626
+ }
627
+ };
628
+ })
629
+ };
630
+ if (!checkOverlap(centerTrial)) {
631
+ let centerCost = 0;
632
+ for (const tp of centerTrial.pads) {
633
+ const sameNetPads = packedPads.filter(
634
+ (pp) => pp.networkId === tp.networkId
635
+ );
636
+ if (!sameNetPads.length) continue;
637
+ let bestD = Infinity;
638
+ for (const pp of sameNetPads) {
639
+ const dx = tp.absoluteCenter.x - pp.absoluteCenter.x;
640
+ const dy = tp.absoluteCenter.y - pp.absoluteCenter.y;
641
+ const d = useSquaredDistance ? dx * dx + dy * dy : Math.hypot(dx, dy);
642
+ if (d < bestD) bestD = d;
643
+ }
644
+ centerCost += bestD === Infinity ? 0 : bestD;
645
+ }
646
+ if (!bestForThisRotation || centerCost < bestForThisRotation.cost) {
647
+ bestForThisRotation = {
648
+ center: centerTrial.center,
649
+ angle: (angle % 360 + 360) % 360,
650
+ cost: centerCost,
651
+ pads: component.pads.map((p) => ({
652
+ ...p,
653
+ absoluteCenter: { x: 0, y: 0 }
654
+ }))
655
+ // Return original pads structure
656
+ };
657
+ }
423
658
  }
424
659
  }
425
- sumDistance += minDistance === Number.POSITIVE_INFINITY ? 0 : minDistance;
426
- }
427
- return sumDistance;
428
- }
429
- function checkOverlap(component, candidateCenter, packedComponents, minGap) {
430
- const tempComponent = {
431
- ...component,
432
- center: candidateCenter,
433
- pads: component.pads.map((p) => ({
434
- ...p,
435
- absoluteCenter: {
436
- x: candidateCenter.x + p.offset.x,
437
- y: candidateCenter.y + p.offset.y
660
+ if (bestForThisRotation && (!globalBestCandidate || bestForThisRotation.cost < globalBestCandidate.cost)) {
661
+ if (component.componentId === "C6") {
662
+ console.log(
663
+ `C6 rotation ${angle}\xB0: best cost=${bestForThisRotation.cost.toFixed(3)} ${!globalBestCandidate ? "[FIRST ROTATION]" : "[NEW BEST ROTATION]"}`
664
+ );
438
665
  }
439
- }))
440
- };
441
- const componentBounds = getComponentBounds(tempComponent, 0);
442
- const componentBox = {
443
- center: {
444
- x: (componentBounds.minX + componentBounds.maxX) / 2,
445
- y: (componentBounds.minY + componentBounds.maxY) / 2
446
- },
447
- width: componentBounds.maxX - componentBounds.minX,
448
- height: componentBounds.maxY - componentBounds.minY
449
- };
450
- for (const packedComp of packedComponents) {
451
- const packedBounds = getComponentBounds(packedComp, 0);
452
- const packedBox = {
453
- center: {
454
- x: (packedBounds.minX + packedBounds.maxX) / 2,
455
- y: (packedBounds.minY + packedBounds.maxY) / 2
456
- },
457
- width: packedBounds.maxX - packedBounds.minX,
458
- height: packedBounds.maxY - packedBounds.minY
459
- };
460
- const centerDistance = Math.hypot(
461
- componentBox.center.x - packedBox.center.x,
462
- componentBox.center.y - packedBox.center.y
463
- );
464
- const minRequiredDistance = minGap + (componentBox.width + packedBox.width) / 2;
465
- if (centerDistance < minRequiredDistance) {
466
- return true;
666
+ globalBestCandidate = bestForThisRotation;
667
+ } else if (bestForThisRotation && component.componentId === "C6") {
668
+ console.log(
669
+ `C6 rotation ${angle}\xB0: cost=${bestForThisRotation.cost.toFixed(3)} [WORSE THAN ${globalBestCandidate?.cost.toFixed(3)}]`
670
+ );
467
671
  }
468
672
  }
469
- return false;
470
- }
471
- function clampToBounds(p, b) {
472
- return {
473
- x: Math.min(Math.max(p.x, b.minX), b.maxX),
474
- y: Math.min(Math.max(p.y, b.minY), b.maxY)
475
- };
673
+ return globalBestCandidate;
476
674
  }
477
- function dist(a, b) {
478
- const dx = a.x - b.x;
479
- const dy = a.y - b.y;
480
- return Math.hypot(dx, dy);
675
+ function getCandidateAngles(c) {
676
+ return (c.availableRotationDegrees ?? [0, 90, 180, 270]).map((d) => d % 360);
481
677
  }
482
- function backtrackToFeasible(from, to, isFeasible, maxTrials = 20) {
483
- let lo = 0;
484
- let hi = 1;
485
- let best = null;
486
- for (let i = 0; i < maxTrials; i++) {
487
- const t = (lo + hi) / 2;
488
- const cand = {
489
- x: from.x + (to.x - from.x) * t,
490
- y: from.y + (to.y - from.y) * t
491
- };
492
- if (isFeasible(cand)) {
493
- best = cand;
494
- lo = t;
495
- } else {
496
- hi = t;
678
+
679
+ // lib/PackSolver/sortComponentQueue.ts
680
+ function sortComponentQueue({
681
+ components,
682
+ packOrderStrategy,
683
+ packFirst = []
684
+ }) {
685
+ const packFirstMap = /* @__PURE__ */ new Map();
686
+ packFirst.forEach((componentId, index) => {
687
+ packFirstMap.set(componentId, index);
688
+ });
689
+ return [...components].sort((a, b) => {
690
+ const aPackFirstIndex = packFirstMap.get(a.componentId);
691
+ const bPackFirstIndex = packFirstMap.get(b.componentId);
692
+ if (aPackFirstIndex !== void 0 && bPackFirstIndex !== void 0) {
693
+ return aPackFirstIndex - bPackFirstIndex;
497
694
  }
498
- }
499
- return best ?? from;
695
+ if (aPackFirstIndex !== void 0) return -1;
696
+ if (bPackFirstIndex !== void 0) return 1;
697
+ if (packOrderStrategy === "largest_to_smallest") {
698
+ return b.pads.length - a.pads.length;
699
+ }
700
+ return a.pads.length - b.pads.length;
701
+ });
500
702
  }
501
- function geometricMedianConstrained(targets, start, bounds, maxIter = 100, tol = 1e-4) {
502
- let c = clampToBounds(start, bounds);
503
- const eps = 1e-9;
504
- for (let k = 0; k < maxIter; k++) {
505
- for (const p of targets) {
506
- if (dist(c, p) <= eps) return clampToBounds(p, bounds);
703
+
704
+ // lib/solver-utils/BaseSolver.ts
705
+ var BaseSolver = class {
706
+ MAX_ITERATIONS = 1e3;
707
+ solved = false;
708
+ failed = false;
709
+ iterations = 0;
710
+ progress = 0;
711
+ error = null;
712
+ activeSubSolver;
713
+ failedSubSolvers;
714
+ timeToSolve;
715
+ stats = {};
716
+ _setupDone = false;
717
+ setup() {
718
+ if (this._setupDone) return;
719
+ this._setup();
720
+ this._setupDone = true;
721
+ }
722
+ /** DO NOT OVERRIDE! Override _step() instead */
723
+ step() {
724
+ if (!this._setupDone) {
725
+ this.setup();
726
+ }
727
+ if (this.solved) return;
728
+ if (this.failed) return;
729
+ this.iterations++;
730
+ try {
731
+ this._step();
732
+ } catch (e) {
733
+ this.error = `${this.constructor.name} error: ${e}`;
734
+ console.error(this.error);
735
+ this.failed = true;
736
+ throw e;
737
+ }
738
+ if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
739
+ this.tryFinalAcceptance();
740
+ }
741
+ if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
742
+ this.error = `${this.constructor.name} ran out of iterations`;
743
+ console.error(this.error);
744
+ this.failed = true;
507
745
  }
508
- let numX = 0;
509
- let numY = 0;
510
- let den = 0;
511
- for (const p of targets) {
512
- const d = dist(c, p);
513
- const w = 1 / Math.max(d, eps);
514
- numX += p.x * w;
515
- numY += p.y * w;
516
- den += w;
746
+ if ("computeProgress" in this) {
747
+ this.progress = this.computeProgress();
517
748
  }
518
- if (den === 0) break;
519
- const next = clampToBounds({ x: numX / den, y: numY / den }, bounds);
520
- if (dist(next, c) < tol) return next;
521
- c = next;
522
749
  }
523
- return c;
524
- }
525
- function optimizeTranslationForMinimumSum(context) {
526
- const { component, initialCenter, packedComponents, minGap } = context;
527
- const bounds = computeTranslationBounds(
528
- component,
529
- initialCenter,
530
- packedComponents,
531
- minGap
532
- );
533
- if (bounds.minX >= bounds.maxX || bounds.minY >= bounds.maxY) {
534
- return initialCenter;
750
+ _setup() {
535
751
  }
536
- const packedPads = packedComponents.flatMap((c) => c.pads);
537
- const connectedPads = component.pads.filter(
538
- (pad) => packedPads.some((pp) => pp.networkId === pad.networkId)
539
- );
540
- if (connectedPads.length === 0) return initialCenter;
541
- const assignTargets = (center2) => {
542
- const results = [];
543
- for (const pad of connectedPads) {
544
- const padAbs = { x: center2.x + pad.offset.x, y: center2.y + pad.offset.y };
545
- let best = null;
546
- for (let j = 0; j < packedPads.length; j++) {
547
- const pp = packedPads[j];
548
- if (!pp || pp.networkId !== pad.networkId) continue;
549
- const d = Math.hypot(
550
- padAbs.x - pp.absoluteCenter.x,
551
- padAbs.y - pp.absoluteCenter.y
552
- );
553
- if (!best || d < dist(padAbs, best.p)) {
554
- best = {
555
- p: {
556
- x: pp.absoluteCenter.x - pad.offset.x,
557
- y: pp.absoluteCenter.y - pad.offset.y
558
- },
559
- key: `${pad.networkId}:${j}`
560
- };
561
- }
562
- }
563
- if (best) results.push({ targets: best.p, key: best.key });
752
+ _step() {
753
+ }
754
+ getConstructorParams() {
755
+ throw new Error("getConstructorParams not implemented");
756
+ }
757
+ solve() {
758
+ const startTime = Date.now();
759
+ while (!this.solved && !this.failed) {
760
+ this.step();
564
761
  }
565
- return results;
566
- };
567
- let center = clampToBounds(initialCenter, bounds);
568
- let prevAssignSignature = "";
569
- const maxOuterIters = 30;
570
- const tolMove = 1e-3;
571
- if (checkOverlap(component, center, packedComponents, minGap)) {
572
- const mid = {
573
- x: (bounds.minX + bounds.maxX) / 2,
574
- y: (bounds.minY + bounds.maxY) / 2
762
+ const endTime = Date.now();
763
+ this.timeToSolve = endTime - startTime;
764
+ }
765
+ visualize() {
766
+ return {
767
+ lines: [],
768
+ points: [],
769
+ rects: [],
770
+ circles: []
575
771
  };
576
- center = backtrackToFeasible(
577
- center,
578
- mid,
579
- (p) => !checkOverlap(component, p, packedComponents, minGap)
580
- );
581
772
  }
582
- for (let iter = 0; iter < maxOuterIters; iter++) {
583
- const assignments = assignTargets(center);
584
- if (assignments.length === 0) return initialCenter;
585
- const signature = assignments.map((a) => a.key).join("|");
586
- const targets = assignments.map((a) => a.targets);
587
- const proposed = geometricMedianConstrained(
588
- targets,
589
- center,
590
- bounds,
591
- 100,
592
- 1e-4
593
- );
594
- let next = proposed;
595
- if (checkOverlap(component, next, packedComponents, minGap)) {
596
- next = backtrackToFeasible(
597
- center,
598
- proposed,
599
- (p) => !checkOverlap(component, p, packedComponents, minGap)
600
- );
601
- }
602
- if (signature === prevAssignSignature && dist(next, center) < tolMove) {
603
- center = next;
604
- break;
605
- }
606
- prevAssignSignature = signature;
607
- center = next;
773
+ /**
774
+ * Called when the solver is about to fail, but we want to see if we have an
775
+ * "acceptable" or "passable" solution. Mostly used for optimizers that
776
+ * have an aggressive early stopping criterion.
777
+ */
778
+ tryFinalAcceptance() {
608
779
  }
609
- const initCost = calculateSumDistance(
610
- component,
611
- initialCenter,
612
- packedComponents
613
- );
614
- const finalCost = calculateSumDistance(component, center, packedComponents);
615
- if (finalCost > initCost && !checkOverlap(component, initialCenter, packedComponents, minGap)) {
616
- return initialCenter;
780
+ /**
781
+ * A lightweight version of the visualize method that can be used to stream
782
+ * progress
783
+ */
784
+ preview() {
785
+ return {
786
+ lines: [],
787
+ points: [],
788
+ rects: [],
789
+ circles: []
790
+ };
617
791
  }
618
- return center;
619
- }
792
+ };
620
793
 
621
- // lib/PackSolver/PackSolver.ts
622
- var PackSolver = class extends BaseSolver {
794
+ // lib/PackSolver/PhasedPackSolver.ts
795
+ var PhasedPackSolver = class extends BaseSolver {
623
796
  packInput;
624
797
  unpackedComponentQueue;
625
798
  packedComponents;
799
+ // Phase management
800
+ currentPhase = "idle";
801
+ currentComponent;
802
+ phaseData = {};
803
+ // Legacy compatibility
626
804
  lastBestPointsResult;
627
805
  lastEvaluatedPositionShadows;
806
+ lastCandidatePoints;
628
807
  constructor(input) {
629
808
  super();
630
809
  this.packInput = input;
631
810
  }
632
811
  _setup() {
633
812
  const { components, packOrderStrategy, packFirst = [] } = this.packInput;
634
- const packFirstMap = /* @__PURE__ */ new Map();
635
- packFirst.forEach((componentId, index) => {
636
- packFirstMap.set(componentId, index);
637
- });
638
- this.unpackedComponentQueue = [...components].sort((a, b) => {
639
- const aPackFirstIndex = packFirstMap.get(a.componentId);
640
- const bPackFirstIndex = packFirstMap.get(b.componentId);
641
- if (aPackFirstIndex !== void 0 && bPackFirstIndex !== void 0) {
642
- return aPackFirstIndex - bPackFirstIndex;
643
- }
644
- if (aPackFirstIndex !== void 0) return -1;
645
- if (bPackFirstIndex !== void 0) return 1;
646
- if (packOrderStrategy === "largest_to_smallest") {
647
- return b.pads.length - a.pads.length;
648
- }
649
- return a.pads.length - b.pads.length;
813
+ this.unpackedComponentQueue = sortComponentQueue({
814
+ components,
815
+ packOrderStrategy,
816
+ packFirst
650
817
  });
651
818
  this.packedComponents = [];
819
+ this.currentPhase = "idle";
820
+ this.phaseData = {};
652
821
  }
653
822
  _step() {
654
823
  if (this.solved) return;
824
+ switch (this.currentPhase) {
825
+ case "idle":
826
+ if (this.unpackedComponentQueue.length === 0) {
827
+ this.solved = true;
828
+ return;
829
+ }
830
+ this.currentComponent = this.unpackedComponentQueue.shift();
831
+ if (!this.currentComponent) {
832
+ this.solved = true;
833
+ return;
834
+ }
835
+ if (this.packedComponents.length === 0) {
836
+ this.placeFirstComponent();
837
+ this.currentComponent = void 0;
838
+ return;
839
+ }
840
+ this.currentPhase = "show_candidate_points";
841
+ this.computeCandidatePoints();
842
+ break;
843
+ case "show_candidate_points":
844
+ this.currentPhase = "show_rotations";
845
+ this.computeRotationTrials();
846
+ break;
847
+ case "show_rotations":
848
+ this.currentPhase = "show_final_placement";
849
+ this.selectBestRotation();
850
+ break;
851
+ case "show_final_placement":
852
+ this.finalizeComponentPlacement();
853
+ this.currentPhase = "idle";
854
+ this.phaseData = {};
855
+ break;
856
+ }
857
+ }
858
+ placeFirstComponent() {
859
+ if (!this.currentComponent) return;
860
+ const newPackedComponent = {
861
+ ...this.currentComponent,
862
+ center: { x: 0, y: 0 },
863
+ ccwRotationOffset: 0,
864
+ pads: this.currentComponent.pads.map((p) => ({
865
+ ...p,
866
+ absoluteCenter: { x: 0, y: 0 }
867
+ }))
868
+ };
869
+ const candidateAngles = this.getCandidateAngles(newPackedComponent);
870
+ newPackedComponent.ccwRotationOffset = ((candidateAngles[0] ?? 0) % 360 + 360) % 360;
871
+ setPackedComponentPadCenters(newPackedComponent);
872
+ this.packedComponents.push(newPackedComponent);
873
+ this.currentComponent = void 0;
874
+ }
875
+ computeCandidatePoints() {
876
+ if (!this.currentComponent) return;
655
877
  const {
656
878
  minGap = 0,
657
879
  disconnectedPackDirection = "nearest_to_center",
658
880
  packPlacementStrategy = "shortest_connection_along_outline"
659
881
  } = this.packInput;
660
- if (this.unpackedComponentQueue.length === 0) {
661
- this.solved = true;
662
- return;
663
- }
664
- const next = this.unpackedComponentQueue.shift();
665
- if (!next) {
666
- this.solved = true;
667
- return;
668
- }
669
882
  const newPackedComponent = {
670
- ...next,
883
+ ...this.currentComponent,
671
884
  center: { x: 0, y: 0 },
672
885
  ccwRotationOffset: 0,
673
- pads: next.pads.map((p) => ({
886
+ pads: this.currentComponent.pads.map((p) => ({
674
887
  ...p,
675
888
  absoluteCenter: { x: 0, y: 0 }
676
889
  }))
677
890
  };
678
- if (this.packedComponents.length === 0) {
679
- newPackedComponent.center = { x: 0, y: 0 };
680
- setPackedComponentPadCenters(newPackedComponent);
681
- this.packedComponents.push(newPackedComponent);
682
- return;
683
- }
684
891
  const padMargins = newPackedComponent.pads.map(
685
892
  (p) => Math.max(p.size.x, p.size.y) / 2
686
893
  );
@@ -689,6 +896,7 @@ var PackSolver = class extends BaseSolver {
689
896
  this.packedComponents,
690
897
  { minGap: minGap + additionalGap }
691
898
  );
899
+ this.phaseData.outlines = outlines;
692
900
  const networkIdsInPackedComponents = new Set(
693
901
  this.packedComponents.flatMap((c) => c.pads.map((p) => p.networkId))
694
902
  );
@@ -701,55 +909,86 @@ var PackSolver = class extends BaseSolver {
701
909
  )
702
910
  );
703
911
  if (sharedNetworkIds.size === 0) {
704
- this.placeComponentDisconnected(
705
- newPackedComponent,
912
+ this.phaseData.candidatePoints = [];
913
+ this.phaseData.goodCandidates = [];
914
+ const shadows = placeComponentDisconnected({
915
+ component: newPackedComponent,
706
916
  outlines,
707
- disconnectedPackDirection
708
- );
709
- this.packedComponents.push(newPackedComponent);
917
+ direction: disconnectedPackDirection,
918
+ packedComponents: this.packedComponents,
919
+ candidateAngles: this.getCandidateAngles(newPackedComponent),
920
+ checkOverlap: (comp) => this.checkOverlapWithPackedComponents(comp)
921
+ });
922
+ this.phaseData.selectedRotation = newPackedComponent;
923
+ this.phaseData.rotationTrials = shadows.map((s) => ({
924
+ ...s,
925
+ cost: 0,
926
+ anchorType: "center",
927
+ hasOverlap: false
928
+ }));
710
929
  return;
711
930
  }
931
+ const candidatePoints = [];
932
+ const goodCandidates = [];
933
+ let smallestDistance = Number.POSITIVE_INFINITY;
934
+ const segmentBestPoints = /* @__PURE__ */ new Map();
935
+ const getSegmentKey = (segment) => {
936
+ const [p1, p2] = segment;
937
+ return `${p1.x.toFixed(6)},${p1.y.toFixed(6)}-${p2.x.toFixed(6)},${p2.y.toFixed(6)}`;
938
+ };
712
939
  const networkIdToAlreadyPackedSegments = /* @__PURE__ */ new Map();
713
940
  for (const sharedNetworkId of sharedNetworkIds) {
714
- networkIdToAlreadyPackedSegments.set(sharedNetworkId, []);
941
+ const segments = [];
715
942
  for (const packedComponent of this.packedComponents) {
716
943
  for (const pad of packedComponent.pads) {
717
- if (pad.networkId !== sharedNetworkId) continue;
718
- const segments = getSegmentsFromPad(pad);
719
- networkIdToAlreadyPackedSegments.set(sharedNetworkId, segments);
944
+ if (pad.networkId === sharedNetworkId) {
945
+ segments.push(...getSegmentsFromPad(pad));
946
+ }
720
947
  }
721
948
  }
949
+ networkIdToAlreadyPackedSegments.set(sharedNetworkId, segments);
722
950
  }
723
- let smallestDistance = Number.POSITIVE_INFINITY;
724
- let bestPoints = [];
725
- if (packPlacementStrategy === "minimum_sum_distance_to_network") {
951
+ if (packPlacementStrategy === "minimum_sum_distance_to_network" || packPlacementStrategy === "minimum_sum_squared_distance_to_network") {
726
952
  for (const outline of outlines) {
727
953
  for (const outlineSegment of outline) {
728
- const samplePoints = [
729
- outlineSegment[0],
730
- {
731
- x: (outlineSegment[0].x + outlineSegment[1].x) / 2,
732
- y: (outlineSegment[0].y + outlineSegment[1].y) / 2
733
- },
734
- outlineSegment[1]
735
- ];
736
- for (const samplePoint of samplePoints) {
737
- for (const sharedNetworkId of sharedNetworkIds) {
738
- const sumDistance = this.computeSumDistanceForPosition(
739
- newPackedComponent,
740
- samplePoint,
741
- sharedNetworkId
742
- );
743
- if (sumDistance < smallestDistance + 1e-6) {
744
- if (sumDistance < smallestDistance - 1e-6) {
745
- bestPoints = [{ ...samplePoint, networkId: sharedNetworkId }];
746
- smallestDistance = sumDistance;
747
- } else {
748
- bestPoints.push({
749
- ...samplePoint,
750
- networkId: sharedNetworkId
751
- });
752
- }
954
+ const [p1, p2] = outlineSegment;
955
+ for (const sharedNetworkId of sharedNetworkIds) {
956
+ const {
957
+ point: optimalPoint,
958
+ distance: optimalDistance,
959
+ candidatePoints: searchPoints
960
+ } = findOptimalPointOnSegment({
961
+ p1,
962
+ p2,
963
+ component: newPackedComponent,
964
+ networkId: sharedNetworkId,
965
+ packedComponents: this.packedComponents,
966
+ useSquaredDistance: packPlacementStrategy === "minimum_sum_squared_distance_to_network"
967
+ });
968
+ for (const searchPoint of searchPoints) {
969
+ candidatePoints.push(searchPoint);
970
+ }
971
+ const segmentKey = getSegmentKey(outlineSegment);
972
+ const currentSegmentBest = segmentBestPoints.get(segmentKey);
973
+ if (!currentSegmentBest || optimalDistance < currentSegmentBest.distance) {
974
+ segmentBestPoints.set(segmentKey, {
975
+ point: { ...optimalPoint, networkId: sharedNetworkId },
976
+ distance: optimalDistance
977
+ });
978
+ }
979
+ if (optimalDistance < smallestDistance + 1e-6) {
980
+ if (optimalDistance < smallestDistance - 1e-6) {
981
+ goodCandidates.length = 0;
982
+ goodCandidates.push({
983
+ ...optimalPoint,
984
+ networkId: sharedNetworkId
985
+ });
986
+ smallestDistance = optimalDistance;
987
+ } else {
988
+ goodCandidates.push({
989
+ ...optimalPoint,
990
+ networkId: sharedNetworkId
991
+ });
753
992
  }
754
993
  }
755
994
  }
@@ -768,17 +1007,32 @@ var PackSolver = class extends BaseSolver {
768
1007
  outlineSegment,
769
1008
  alreadyPackedSegments
770
1009
  );
1010
+ candidatePoints.push({
1011
+ ...nearestPointOnOutlineToAlreadyPackedSegments,
1012
+ networkId: sharedNetworkId,
1013
+ distance: outlineToAlreadyPackedSegmentsDist
1014
+ });
1015
+ const segmentKey = getSegmentKey(outlineSegment);
1016
+ const currentSegmentBest = segmentBestPoints.get(segmentKey);
1017
+ if (!currentSegmentBest || outlineToAlreadyPackedSegmentsDist < currentSegmentBest.distance) {
1018
+ segmentBestPoints.set(segmentKey, {
1019
+ point: {
1020
+ ...nearestPointOnOutlineToAlreadyPackedSegments,
1021
+ networkId: sharedNetworkId
1022
+ },
1023
+ distance: outlineToAlreadyPackedSegmentsDist
1024
+ });
1025
+ }
771
1026
  if (outlineToAlreadyPackedSegmentsDist < smallestDistance + 1e-6) {
772
1027
  if (outlineToAlreadyPackedSegmentsDist < smallestDistance - 1e-6) {
773
- bestPoints = [
774
- {
775
- ...nearestPointOnOutlineToAlreadyPackedSegments,
776
- networkId: sharedNetworkId
777
- }
778
- ];
1028
+ goodCandidates.length = 0;
1029
+ goodCandidates.push({
1030
+ ...nearestPointOnOutlineToAlreadyPackedSegments,
1031
+ networkId: sharedNetworkId
1032
+ });
779
1033
  smallestDistance = outlineToAlreadyPackedSegmentsDist;
780
1034
  } else {
781
- bestPoints.push({
1035
+ goodCandidates.push({
782
1036
  ...nearestPointOnOutlineToAlreadyPackedSegments,
783
1037
  networkId: sharedNetworkId
784
1038
  });
@@ -788,212 +1042,216 @@ var PackSolver = class extends BaseSolver {
788
1042
  }
789
1043
  }
790
1044
  }
791
- this.lastBestPointsResult = {
792
- bestPoints,
793
- distance: smallestDistance
794
- };
795
- this.lastEvaluatedPositionShadows = [];
796
- for (const bestPoint of bestPoints) {
797
- const networkId = bestPoint.networkId;
798
- const newPadsConnectedToNetworkId = newPackedComponent.pads.filter(
799
- (p) => p.networkId === networkId
800
- );
801
- const candidateAngles = this.getCandidateAngles(newPackedComponent);
802
- let bestCandidate = null;
803
- const packedPads = this.packedComponents.flatMap((c) => c.pads);
804
- for (const angle of candidateAngles) {
805
- const firstPad = newPadsConnectedToNetworkId[0];
806
- if (!firstPad) continue;
807
- const rotatedOffset = rotatePoint(firstPad.offset, angle);
808
- const candidateCenter = {
809
- x: bestPoint.x - rotatedOffset.x,
810
- y: bestPoint.y - rotatedOffset.y
811
- };
812
- const transformedPads = newPackedComponent.pads.map((p) => {
813
- const ro = rotatePoint(p.offset, angle);
814
- return {
815
- ...p,
816
- absoluteCenter: {
817
- x: candidateCenter.x + ro.x,
818
- y: candidateCenter.y + ro.y
819
- }
820
- };
821
- });
822
- const tempComponent = {
823
- ...newPackedComponent,
824
- center: candidateCenter,
825
- ccwRotationOffset: angle,
826
- pads: transformedPads
827
- };
828
- this.lastEvaluatedPositionShadows?.push({ ...tempComponent });
829
- if (this.checkOverlapWithPackedComponents(tempComponent)) continue;
830
- let cost = 0;
831
- if (packPlacementStrategy === "minimum_sum_distance_to_network") {
832
- const optimizedCenter = optimizeTranslationForMinimumSum({
833
- component: tempComponent,
834
- initialCenter: candidateCenter,
835
- packedComponents: this.packedComponents,
836
- minGap
837
- });
838
- const optimizedTransformedPads = newPackedComponent.pads.map((p) => {
839
- const ro = rotatePoint(p.offset, angle);
840
- return {
841
- ...p,
842
- absoluteCenter: {
843
- x: optimizedCenter.x + ro.x,
844
- y: optimizedCenter.y + ro.y
845
- }
1045
+ for (const sharedNetworkId of sharedNetworkIds) {
1046
+ for (const outline of outlines) {
1047
+ for (const outlineSegment of outline) {
1048
+ const [p1, p2] = outlineSegment;
1049
+ for (let t = 0; t <= 1; t += 0.2) {
1050
+ const sampledPoint = {
1051
+ x: p1.x + t * (p2.x - p1.x),
1052
+ y: p1.y + t * (p2.y - p1.y)
846
1053
  };
847
- });
848
- tempComponent.center = optimizedCenter;
849
- tempComponent.pads = optimizedTransformedPads;
850
- if (optimizedCenter.x !== candidateCenter.x || optimizedCenter.y !== candidateCenter.y) {
851
- this.lastEvaluatedPositionShadows?.push({ ...tempComponent });
852
- }
853
- if (this.checkOverlapWithPackedComponents(tempComponent)) continue;
854
- for (const tp of optimizedTransformedPads) {
855
- const sameNetPads = packedPads.filter(
856
- (pp) => pp.networkId === tp.networkId
1054
+ let distance = 0;
1055
+ const componentPadsOnNetwork = newPackedComponent.pads.filter(
1056
+ (p) => p.networkId === sharedNetworkId
857
1057
  );
858
- if (!sameNetPads.length) continue;
859
- let bestD = Infinity;
860
- for (const pp of sameNetPads) {
861
- const dx = tp.absoluteCenter.x - pp.absoluteCenter.x;
862
- const dy = tp.absoluteCenter.y - pp.absoluteCenter.y;
863
- const d = Math.hypot(dx, dy);
864
- if (d < bestD) bestD = d;
1058
+ for (const componentPad of componentPadsOnNetwork) {
1059
+ let minDist = Number.POSITIVE_INFINITY;
1060
+ for (const packedComponent of this.packedComponents) {
1061
+ for (const packedPad of packedComponent.pads) {
1062
+ if (packedPad.networkId === sharedNetworkId) {
1063
+ const dx = sampledPoint.x + componentPad.offset.x - packedPad.absoluteCenter.x;
1064
+ const dy = sampledPoint.y + componentPad.offset.y - packedPad.absoluteCenter.y;
1065
+ const dist = Math.sqrt(dx * dx + dy * dy);
1066
+ minDist = Math.min(minDist, dist);
1067
+ }
1068
+ }
1069
+ }
1070
+ distance += minDist;
865
1071
  }
866
- cost += bestD === Infinity ? 0 : bestD;
867
- }
868
- } else {
869
- for (const tp of transformedPads) {
870
- const sameNetPads = packedPads.filter(
871
- (pp) => pp.networkId === tp.networkId
872
- );
873
- if (!sameNetPads.length) continue;
874
- let bestD = Infinity;
875
- for (const pp of sameNetPads) {
876
- const dx = tp.absoluteCenter.x - pp.absoluteCenter.x;
877
- const dy = tp.absoluteCenter.y - pp.absoluteCenter.y;
878
- const d = Math.hypot(dx, dy);
879
- if (d < bestD) bestD = d;
1072
+ const point = {
1073
+ ...sampledPoint,
1074
+ networkId: sharedNetworkId,
1075
+ distance
1076
+ };
1077
+ candidatePoints.push(point);
1078
+ if (distance < smallestDistance) {
1079
+ smallestDistance = distance;
1080
+ goodCandidates.length = 0;
1081
+ goodCandidates.push(point);
1082
+ } else if (distance === smallestDistance) {
1083
+ goodCandidates.push(point);
880
1084
  }
881
- cost += bestD;
882
1085
  }
883
1086
  }
884
- if (!bestCandidate || cost < bestCandidate.cost) {
885
- const finalCenter = packPlacementStrategy === "minimum_sum_distance_to_network" ? tempComponent.center : candidateCenter;
886
- bestCandidate = { center: finalCenter, angle, cost };
887
- }
888
1087
  }
889
- if (bestCandidate) {
890
- newPackedComponent.center = bestCandidate.center;
891
- newPackedComponent.ccwRotationOffset = bestCandidate.angle;
892
- } else {
893
- console.log("no valid rotation found");
894
- const firstPad = newPadsConnectedToNetworkId[0];
895
- const candidateCenter = {
896
- x: bestPoint.x - firstPad.offset.x,
897
- y: bestPoint.y - firstPad.offset.y
898
- };
899
- newPackedComponent.center = candidateCenter;
900
- newPackedComponent.ccwRotationOffset = 0;
1088
+ }
1089
+ for (const [, segmentBest] of segmentBestPoints) {
1090
+ const isAlreadyIncluded = goodCandidates.some(
1091
+ (gc) => Math.abs(gc.x - segmentBest.point.x) < 1e-6 && Math.abs(gc.y - segmentBest.point.y) < 1e-6 && gc.networkId === segmentBest.point.networkId
1092
+ );
1093
+ if (!isAlreadyIncluded) {
1094
+ goodCandidates.push(segmentBest.point);
901
1095
  }
902
- setPackedComponentPadCenters(newPackedComponent);
903
1096
  }
904
- setPackedComponentPadCenters(newPackedComponent);
905
- this.packedComponents.push(newPackedComponent);
906
- }
907
- getConstructorParams() {
908
- return [this.packInput];
1097
+ this.phaseData.candidatePoints = candidatePoints;
1098
+ this.phaseData.goodCandidates = goodCandidates;
1099
+ this.phaseData.bestDistance = smallestDistance;
1100
+ this.lastCandidatePoints = candidatePoints;
1101
+ this.lastBestPointsResult = { goodCandidates, distance: smallestDistance };
909
1102
  }
910
- /* ---------- small helpers ------------------------------------------------ */
911
- getCandidateAngles(c) {
912
- return (c.availableRotationDegrees ?? [0, 90, 180, 270]).map(
913
- (d) => d % 360 * Math.PI / 180
914
- );
915
- }
916
- checkOverlapWithPackedComponents(cand) {
917
- const b = getComponentBounds(cand, 0);
918
- const candBox = {
919
- center: { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 },
920
- width: b.maxX - b.minX,
921
- height: b.maxY - b.minY
1103
+ computeRotationTrials() {
1104
+ if (!this.currentComponent || !this.phaseData.goodCandidates) return;
1105
+ const newPackedComponent = {
1106
+ ...this.currentComponent,
1107
+ center: { x: 0, y: 0 },
1108
+ ccwRotationOffset: 0,
1109
+ pads: this.currentComponent.pads.map((p) => ({
1110
+ ...p,
1111
+ absoluteCenter: { x: 0, y: 0 }
1112
+ }))
922
1113
  };
923
- for (const pc of this.packedComponents) {
924
- for (const pad of pc.pads) {
925
- if (computeDistanceBetweenBoxes(
926
- {
927
- center: pad.absoluteCenter,
928
- width: pad.size.x,
929
- height: pad.size.y
930
- },
931
- candBox
932
- ).distance < this.packInput.minGap)
933
- return true;
1114
+ const rotationTrials = [];
1115
+ const candidateAngles = this.getCandidateAngles(newPackedComponent);
1116
+ const allCandidatePoints = [...this.phaseData.goodCandidates];
1117
+ if (this.phaseData.outlines) {
1118
+ for (const networkId of new Set(
1119
+ this.phaseData.goodCandidates.map((p) => p.networkId)
1120
+ )) {
1121
+ for (const outline of this.phaseData.outlines) {
1122
+ for (const outlineSegment of outline) {
1123
+ const [p1, p2] = outlineSegment;
1124
+ for (let t = 0; t <= 1; t += 0.2) {
1125
+ allCandidatePoints.push({
1126
+ x: p1.x + t * (p2.x - p1.x),
1127
+ y: p1.y + t * (p2.y - p1.y),
1128
+ networkId
1129
+ });
1130
+ }
1131
+ }
1132
+ }
934
1133
  }
935
1134
  }
936
- return false;
937
- }
938
- computeGlobalCenter() {
939
- if (!this.packedComponents.length) return { x: 0, y: 0 };
940
- const s = this.packedComponents.reduce(
941
- (a, c) => ({ x: a.x + c.center.x, y: a.y + c.center.y }),
942
- { x: 0, y: 0 }
943
- );
944
- return {
945
- x: s.x / this.packedComponents.length,
946
- y: s.y / this.packedComponents.length
947
- };
948
- }
949
- findBestPointForDisconnected(outlines, dir) {
950
- const pts = outlines.flatMap(
951
- (ol) => ol.map(([p1, p2]) => ({
952
- x: (p1.x + p2.x) / 2,
953
- y: (p1.y + p2.y) / 2
954
- }))
955
- );
956
- if (!pts.length) return { x: 0, y: 0 };
957
- if (dir !== "nearest_to_center") {
958
- const extreme = dir === "left" || dir === "down" ? Math.min : Math.max;
959
- const key = dir === "left" || dir === "right" ? "x" : "y";
960
- const target = extreme(...pts.map((p) => p[key]));
961
- return pts.find((p) => p[key] === target);
1135
+ const useSquaredDistance = this.packInput.packPlacementStrategy === "minimum_sum_squared_distance_to_network";
1136
+ const result = selectOptimalRotation({
1137
+ component: newPackedComponent,
1138
+ candidatePoints: allCandidatePoints,
1139
+ packedComponents: this.packedComponents,
1140
+ minGap: this.packInput.minGap ?? 0,
1141
+ useSquaredDistance,
1142
+ checkOverlap: (comp) => this.checkOverlapWithPackedComponents(comp)
1143
+ });
1144
+ for (const angle of candidateAngles) {
1145
+ for (const point of this.phaseData.goodCandidates) {
1146
+ const componentPadsOnNetwork = newPackedComponent.pads.filter(
1147
+ (p) => p.networkId === point.networkId
1148
+ );
1149
+ if (componentPadsOnNetwork.length > 0) {
1150
+ const firstPad = componentPadsOnNetwork[0];
1151
+ const rotatedPadOffset = rotatePoint(
1152
+ firstPad.offset,
1153
+ angle * Math.PI / 180
1154
+ );
1155
+ const componentCenter = {
1156
+ x: point.x - rotatedPadOffset.x,
1157
+ y: point.y - rotatedPadOffset.y
1158
+ };
1159
+ const trial = { ...newPackedComponent };
1160
+ trial.center = componentCenter;
1161
+ trial.ccwRotationOffset = (angle % 360 + 360) % 360;
1162
+ setPackedComponentPadCenters(trial);
1163
+ const hasOverlap = this.checkOverlapWithPackedComponents(trial);
1164
+ let cost = 0;
1165
+ for (const pad of trial.pads) {
1166
+ let minDist = Number.POSITIVE_INFINITY;
1167
+ for (const packedComp of this.packedComponents) {
1168
+ for (const packedPad of packedComp.pads) {
1169
+ if (packedPad.networkId === pad.networkId) {
1170
+ const dx = pad.absoluteCenter.x - packedPad.absoluteCenter.x;
1171
+ const dy = pad.absoluteCenter.y - packedPad.absoluteCenter.y;
1172
+ const dist = Math.sqrt(dx * dx + dy * dy);
1173
+ minDist = Math.min(minDist, dist);
1174
+ }
1175
+ }
1176
+ }
1177
+ if (minDist < Number.POSITIVE_INFINITY) {
1178
+ cost += useSquaredDistance ? minDist * minDist : minDist;
1179
+ }
1180
+ }
1181
+ rotationTrials.push({
1182
+ ...trial,
1183
+ cost,
1184
+ anchorType: "pad",
1185
+ anchorPadId: firstPad.padId,
1186
+ hasOverlap
1187
+ });
1188
+ }
1189
+ const centerTrial = { ...newPackedComponent };
1190
+ centerTrial.center = { x: point.x, y: point.y };
1191
+ centerTrial.ccwRotationOffset = (angle % 360 + 360) % 360;
1192
+ setPackedComponentPadCenters(centerTrial);
1193
+ const centerHasOverlap = this.checkOverlapWithPackedComponents(centerTrial);
1194
+ let centerCost = 0;
1195
+ for (const pad of centerTrial.pads) {
1196
+ let minDist = Number.POSITIVE_INFINITY;
1197
+ for (const packedComp of this.packedComponents) {
1198
+ for (const packedPad of packedComp.pads) {
1199
+ if (packedPad.networkId === pad.networkId) {
1200
+ const dx = pad.absoluteCenter.x - packedPad.absoluteCenter.x;
1201
+ const dy = pad.absoluteCenter.y - packedPad.absoluteCenter.y;
1202
+ const dist = Math.sqrt(dx * dx + dy * dy);
1203
+ minDist = Math.min(minDist, dist);
1204
+ }
1205
+ }
1206
+ }
1207
+ if (minDist < Number.POSITIVE_INFINITY) {
1208
+ centerCost += useSquaredDistance ? minDist * minDist : minDist;
1209
+ }
1210
+ }
1211
+ rotationTrials.push({
1212
+ ...centerTrial,
1213
+ cost: centerCost,
1214
+ anchorType: "center",
1215
+ hasOverlap: centerHasOverlap
1216
+ });
1217
+ }
962
1218
  }
963
- const center = this.computeGlobalCenter();
964
- return pts.reduce(
965
- (best, p) => Math.hypot(p.x - center.x, p.y - center.y) < Math.hypot(best.x - center.x, best.y - center.y) ? p : best
966
- );
1219
+ this.phaseData.rotationTrials = rotationTrials;
1220
+ if (result) {
1221
+ const selectedComponent = { ...newPackedComponent };
1222
+ selectedComponent.center = result.center;
1223
+ selectedComponent.ccwRotationOffset = result.angle;
1224
+ selectedComponent.pads = result.pads;
1225
+ setPackedComponentPadCenters(selectedComponent);
1226
+ this.phaseData.selectedRotation = selectedComponent;
1227
+ } else {
1228
+ this.phaseData.selectedRotation = void 0;
1229
+ }
1230
+ this.lastEvaluatedPositionShadows = rotationTrials;
967
1231
  }
968
- placeComponentAtPoint(comp, pt) {
969
- this.lastEvaluatedPositionShadows = [];
970
- for (const ang of this.getCandidateAngles(comp)) {
971
- const pads = comp.pads.map((p) => {
972
- const ro = rotatePoint(p.offset, ang);
973
- return { ...p, absoluteCenter: { x: pt.x + ro.x, y: pt.y + ro.y } };
974
- });
975
- const cand = {
976
- ...comp,
977
- center: pt,
978
- ccwRotationOffset: ang,
979
- pads
1232
+ selectBestRotation() {
1233
+ if (!this.phaseData.selectedRotation && this.currentComponent) {
1234
+ const newPackedComponent = {
1235
+ ...this.currentComponent,
1236
+ center: { x: 5, y: 5 },
1237
+ ccwRotationOffset: 0,
1238
+ pads: this.currentComponent.pads.map((p) => ({
1239
+ ...p,
1240
+ absoluteCenter: { x: 0, y: 0 }
1241
+ }))
980
1242
  };
981
- this.lastEvaluatedPositionShadows.push(cand);
982
- if (!this.checkOverlapWithPackedComponents(cand)) {
983
- Object.assign(comp, cand);
984
- setPackedComponentPadCenters(comp);
985
- return;
986
- }
1243
+ const candidateAngles = this.getCandidateAngles(newPackedComponent);
1244
+ newPackedComponent.ccwRotationOffset = ((candidateAngles[0] ?? 0) % 360 + 360) % 360;
1245
+ setPackedComponentPadCenters(newPackedComponent);
1246
+ this.phaseData.selectedRotation = newPackedComponent;
987
1247
  }
988
- comp.center = pt;
989
- comp.ccwRotationOffset = 0;
990
- setPackedComponentPadCenters(comp);
991
1248
  }
992
- placeComponentDisconnected(comp, outlines, dir) {
993
- const target = this.findBestPointForDisconnected(outlines, dir);
994
- this.placeComponentAtPoint(comp, target);
1249
+ finalizeComponentPlacement() {
1250
+ if (!this.phaseData.selectedRotation) return;
1251
+ this.packedComponents.push(this.phaseData.selectedRotation);
1252
+ this.currentComponent = void 0;
995
1253
  }
996
- /** Visualize the current packing state components are omitted, only the outline is shown. */
1254
+ /** Visualize the current packing state based on the current phase */
997
1255
  visualize() {
998
1256
  const graphics = getGraphicsFromPackOutput({
999
1257
  components: this.packedComponents ?? [],
@@ -1004,6 +1262,15 @@ var PackSolver = class extends BaseSolver {
1004
1262
  });
1005
1263
  graphics.points ??= [];
1006
1264
  graphics.lines ??= [];
1265
+ graphics.rects ??= [];
1266
+ if (graphics.rects) {
1267
+ for (const rect of graphics.rects) {
1268
+ if (rect.fill === "rgba(0,0,0,0.25)") {
1269
+ rect.fill = "rgba(100,100,100,0.5)";
1270
+ rect.stroke = "#333333";
1271
+ }
1272
+ }
1273
+ }
1007
1274
  const outlines = constructOutlinesFromPackedComponents(
1008
1275
  this.packedComponents ?? [],
1009
1276
  {
@@ -1020,76 +1287,161 @@ var PackSolver = class extends BaseSolver {
1020
1287
  )
1021
1288
  )
1022
1289
  );
1023
- if (!this.solved) {
1024
- for (const shadow of this.lastEvaluatedPositionShadows ?? []) {
1025
- const bounds = getComponentBounds(shadow, 0);
1026
- graphics.rects.push({
1027
- center: shadow.center,
1028
- width: bounds.maxX - bounds.minX,
1029
- height: bounds.maxY - bounds.minY,
1030
- fill: "rgba(0,255,255,0.2)",
1031
- label: (shadow.ccwRotationOffset / Math.PI * 180).toFixed(1)
1290
+ graphics.texts ??= [];
1291
+ graphics.texts.push({
1292
+ text: `Phase: ${this.currentPhase}`,
1293
+ x: 0,
1294
+ y: 5,
1295
+ fontSize: 0.3
1296
+ });
1297
+ if (this.currentComponent) {
1298
+ graphics.texts.push({
1299
+ text: `Packing: ${this.currentComponent.componentId}`,
1300
+ x: 0,
1301
+ y: 4.5,
1302
+ fontSize: 0.25
1303
+ });
1304
+ }
1305
+ switch (this.currentPhase) {
1306
+ case "show_candidate_points":
1307
+ this.visualizeCandidatePoints(graphics);
1308
+ break;
1309
+ case "show_rotations":
1310
+ this.visualizeRotationTrials(graphics);
1311
+ break;
1312
+ case "show_final_placement":
1313
+ this.visualizeFinalPlacement(graphics);
1314
+ break;
1315
+ case "idle":
1316
+ break;
1317
+ }
1318
+ return graphics;
1319
+ }
1320
+ visualizeCandidatePoints(graphics) {
1321
+ if (this.phaseData.candidatePoints) {
1322
+ for (const candidatePoint of this.phaseData.candidatePoints) {
1323
+ graphics.points.push({
1324
+ x: candidatePoint.x,
1325
+ y: candidatePoint.y,
1326
+ label: `d=${candidatePoint.distance.toFixed(3)}`,
1327
+ fill: "rgba(255,165,0,0.6)",
1328
+ // Orange color for candidate points
1329
+ radius: 0.02
1032
1330
  });
1033
- for (const shadowPad of shadow.pads) {
1034
- graphics.rects.push({
1035
- center: shadowPad.absoluteCenter,
1036
- width: shadowPad.size.x,
1037
- height: shadowPad.size.y,
1038
- fill: "rgba(0,0,255,0.5)"
1039
- });
1040
- }
1041
1331
  }
1042
- if (this.lastBestPointsResult) {
1043
- for (const bestPoint of this.lastBestPointsResult.bestPoints) {
1044
- graphics.points.push({
1045
- x: bestPoint.x,
1046
- y: bestPoint.y,
1047
- label: `bestPoint
1048
- networkId: ${bestPoint.networkId}
1049
- d=${this.lastBestPointsResult.distance}`
1050
- });
1051
- }
1332
+ }
1333
+ if (this.phaseData.goodCandidates) {
1334
+ for (const goodCandidate of this.phaseData.goodCandidates) {
1335
+ graphics.points.push({
1336
+ x: goodCandidate.x,
1337
+ y: goodCandidate.y,
1338
+ label: `BEST (d=${this.phaseData.bestDistance?.toFixed(3)})`,
1339
+ fill: "rgba(0,255,0,0.8)",
1340
+ // Green color for best points
1341
+ radius: 0.03
1342
+ });
1343
+ const crossSize = 0.2;
1344
+ graphics.lines.push(
1345
+ {
1346
+ points: [
1347
+ {
1348
+ x: goodCandidate.x - crossSize,
1349
+ y: goodCandidate.y - crossSize
1350
+ },
1351
+ {
1352
+ x: goodCandidate.x + crossSize,
1353
+ y: goodCandidate.y + crossSize
1354
+ }
1355
+ ],
1356
+ stroke: "#00AA00"
1357
+ },
1358
+ {
1359
+ points: [
1360
+ {
1361
+ x: goodCandidate.x - crossSize,
1362
+ y: goodCandidate.y + crossSize
1363
+ },
1364
+ {
1365
+ x: goodCandidate.x + crossSize,
1366
+ y: goodCandidate.y - crossSize
1367
+ }
1368
+ ],
1369
+ stroke: "#00AA00"
1370
+ }
1371
+ );
1052
1372
  }
1053
1373
  }
1054
- return graphics;
1374
+ }
1375
+ visualizeRotationTrials(graphics) {
1376
+ if (!this.phaseData.rotationTrials) return;
1377
+ for (const trial of this.phaseData.rotationTrials) {
1378
+ const rotationOffset = 0.02 * (trial.ccwRotationOffset / 90);
1379
+ const anchorInfo = trial.anchorType === "pad" ? `pad: ${trial.anchorPadId}` : "center";
1380
+ const overlapText = trial.hasOverlap ? "\nOVERLAP" : "";
1381
+ graphics.points.push({
1382
+ x: trial.center.x + rotationOffset,
1383
+ y: trial.center.y + rotationOffset,
1384
+ label: `${trial.ccwRotationOffset}\xB0 (cost: ${trial.cost.toFixed(3)}, anchor: ${anchorInfo})${overlapText}`,
1385
+ fill: "rgba(0,255,255,0.8)",
1386
+ radius: 0.05
1387
+ });
1388
+ for (const pad of trial.pads) {
1389
+ const padColor = trial.hasOverlap ? { fill: "rgba(255,165,0,0.15)", stroke: "rgba(255,165,0,0.4)" } : { fill: "rgba(0,0,255,0.15)", stroke: "rgba(0,0,255,0.4)" };
1390
+ graphics.rects.push({
1391
+ center: pad.absoluteCenter,
1392
+ width: pad.size.x,
1393
+ height: pad.size.y,
1394
+ fill: padColor.fill,
1395
+ stroke: padColor.stroke,
1396
+ strokeWidth: 0.01
1397
+ });
1398
+ }
1399
+ }
1400
+ }
1401
+ visualizeFinalPlacement(graphics) {
1402
+ if (!this.phaseData.selectedRotation) return;
1403
+ const component = this.phaseData.selectedRotation;
1404
+ const bounds = getComponentBounds(component, 0);
1405
+ graphics.rects.push({
1406
+ center: component.center,
1407
+ width: bounds.maxX - bounds.minX,
1408
+ height: bounds.maxY - bounds.minY,
1409
+ fill: "rgba(0,255,0,0.3)",
1410
+ stroke: "#00FF00",
1411
+ strokeWidth: 0.05,
1412
+ label: `PLACED at ${component.ccwRotationOffset}\xB0`
1413
+ });
1414
+ for (const pad of component.pads) {
1415
+ graphics.rects.push({
1416
+ center: pad.absoluteCenter,
1417
+ width: pad.size.x,
1418
+ height: pad.size.y,
1419
+ fill: "rgba(0,255,0,0.7)"
1420
+ });
1421
+ }
1422
+ }
1423
+ getConstructorParams() {
1424
+ return [this.packInput];
1055
1425
  }
1056
1426
  getResult() {
1057
1427
  return this.packedComponents;
1058
1428
  }
1059
- computeSumDistanceForPosition(component, position, targetNetworkId) {
1060
- const componentPadsOnNetwork = component.pads.filter(
1061
- (p) => p.networkId === targetNetworkId
1062
- );
1063
- if (componentPadsOnNetwork.length === 0) return 0;
1064
- const packedPadsOnNetwork = this.packedComponents.flatMap(
1065
- (c) => c.pads.filter((p) => p.networkId === targetNetworkId)
1066
- );
1067
- if (packedPadsOnNetwork.length === 0) return 0;
1068
- let sumDistance = 0;
1069
- for (const componentPad of componentPadsOnNetwork) {
1070
- const padPosition = {
1071
- x: position.x + componentPad.offset.x,
1072
- y: position.y + componentPad.offset.y
1073
- };
1074
- let minDistance = Number.POSITIVE_INFINITY;
1075
- for (const packedPad of packedPadsOnNetwork) {
1076
- const distance = Math.hypot(
1077
- padPosition.x - packedPad.absoluteCenter.x,
1078
- padPosition.y - packedPad.absoluteCenter.y
1079
- );
1080
- if (distance < minDistance) {
1081
- minDistance = distance;
1082
- }
1083
- }
1084
- sumDistance += minDistance === Number.POSITIVE_INFINITY ? 0 : minDistance;
1085
- }
1086
- return sumDistance;
1429
+ /* ---------- small helpers ------------------------------------------------ */
1430
+ getCandidateAngles(c) {
1431
+ return (c.availableRotationDegrees ?? [0, 90, 180, 270]).map((d) => d % 360);
1432
+ }
1433
+ checkOverlapWithPackedComponents(component) {
1434
+ return checkOverlapWithPackedComponents({
1435
+ component,
1436
+ packedComponents: this.packedComponents,
1437
+ minGap: this.packInput.minGap ?? 0
1438
+ });
1087
1439
  }
1088
1440
  };
1089
1441
 
1090
1442
  // lib/pack.ts
1091
1443
  var pack = (input) => {
1092
- const solver = new PackSolver(input);
1444
+ const solver = new PhasedPackSolver(input);
1093
1445
  solver.solve();
1094
1446
  return {
1095
1447
  ...input,
@@ -1322,6 +1674,8 @@ var convertCircuitJsonToPackOutput = (circuitJson, opts = {}) => {
1322
1674
  var convertPackOutputToPackInput = (packed) => {
1323
1675
  const strippedComponents = packed.components.map((pc) => ({
1324
1676
  componentId: pc.componentId,
1677
+ availableRotationDegrees: pc.availableRotationDegrees,
1678
+ // Preserve rotation constraints
1325
1679
  pads: pc.pads.map(({ absoluteCenter: _ac, ...rest }) => rest)
1326
1680
  }));
1327
1681
  return {
@@ -1331,7 +1685,7 @@ var convertPackOutputToPackInput = (packed) => {
1331
1685
  };
1332
1686
  };
1333
1687
  export {
1334
- PackSolver,
1688
+ PhasedPackSolver,
1335
1689
  convertCircuitJsonToPackOutput,
1336
1690
  convertPackOutputToPackInput,
1337
1691
  getGraphicsFromPackOutput,