calculate-packing 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ interface PackedComponent extends InputComponent {
43
43
  ccwRotationDegrees?: number;
44
44
  pads: OutputPad[];
45
45
  }
46
- type PackPlacementStrategy = "shortest_connection_along_outline" | "minimum_sum_distance_to_network" | "minimum_sum_squared_distance_to_network";
46
+ type PackPlacementStrategy = "shortest_connection_along_outline" | "minimum_sum_distance_to_network" | "minimum_sum_squared_distance_to_network" | "minimum_closest_sum_squared_distance";
47
47
  interface PackInput {
48
48
  components: InputComponent[];
49
49
  minGap: number;
@@ -304,6 +304,72 @@ declare class MultiOffsetIrlsSolver extends BaseSolver {
304
304
  visualize(): GraphicsObject;
305
305
  }
306
306
 
307
+ interface TwoPhaseIrlsSolverParams extends Omit<MultiOffsetIrlsSolverParams, "useSquaredDistance"> {
308
+ /** Phase 1 convergence tolerance */
309
+ phase1Epsilon?: number;
310
+ /** Phase 2 convergence tolerance */
311
+ phase2Epsilon?: number;
312
+ }
313
+ /**
314
+ * Two-Phase IRLS Solver that implements the "minimum_closest_sum_squared_distance" strategy.
315
+ *
316
+ * Phase 1: Solves for minimum sum squared distance (like regular squared distance optimization)
317
+ * Phase 2: From the Phase 1 solution, finds the closest point to any target and optimizes
318
+ * to minimize JUST the distance to that single closest point
319
+ */
320
+ declare class TwoPhaseIrlsSolver extends BaseSolver {
321
+ offsetPadPoints: MultiOffsetIrlsSolverParams["offsetPadPoints"];
322
+ targetPointMap: MultiOffsetIrlsSolverParams["targetPointMap"];
323
+ constraintFn?: MultiOffsetIrlsSolverParams["constraintFn"];
324
+ currentPosition: {
325
+ x: number;
326
+ y: number;
327
+ };
328
+ optimalPosition?: {
329
+ x: number;
330
+ y: number;
331
+ };
332
+ private readonly initialPosition;
333
+ private readonly phase1Epsilon;
334
+ private readonly phase2Epsilon;
335
+ private readonly maxIterations;
336
+ private phase1Solver?;
337
+ private phase2Solver?;
338
+ private currentPhase;
339
+ private phase1Position?;
340
+ private closestTargetPadId?;
341
+ private closestTargetPoint?;
342
+ constructor(params: TwoPhaseIrlsSolverParams);
343
+ getConstructorParams(): TwoPhaseIrlsSolverParams;
344
+ _setup(): void;
345
+ _step(): void;
346
+ private stepPhase1;
347
+ private setupPhase2;
348
+ private stepPhase2;
349
+ /**
350
+ * Get the current best position
351
+ */
352
+ getBestPosition(): {
353
+ x: number;
354
+ y: number;
355
+ };
356
+ /**
357
+ * Get the current absolute positions for all offset pad points
358
+ */
359
+ getOffsetPadPositions(): Map<string, {
360
+ x: number;
361
+ y: number;
362
+ }>;
363
+ /**
364
+ * Calculate total distance from current position to all assigned target points
365
+ */
366
+ getTotalDistance(position?: {
367
+ x: number;
368
+ y: number;
369
+ }): number;
370
+ visualize(): GraphicsObject;
371
+ }
372
+
307
373
  /**
308
374
  * Given a single segment on the outline, the component's rotation, compute the
309
375
  * optimal position for the rotated component (the position that minimizes the
@@ -319,18 +385,19 @@ declare class OutlineSegmentCandidatePointSolver extends BaseSolver {
319
385
  viableOutlineSegment: [Point$3, Point$3] | null;
320
386
  fullOutline: [Point$3, Point$3][];
321
387
  componentRotationDegrees: number;
322
- packStrategy: "minimum_sum_squared_distance_to_network" | "minimum_sum_distance_to_network";
388
+ packStrategy: "minimum_sum_squared_distance_to_network" | "minimum_sum_distance_to_network" | "minimum_closest_sum_squared_distance";
323
389
  minGap: number;
324
390
  packedComponents: PackedComponent[];
325
391
  componentToPack: InputComponent;
326
392
  viableBounds?: Bounds;
327
393
  optimalPosition?: Point$3;
328
394
  irlsSolver?: MultiOffsetIrlsSolver;
395
+ twoPhaseIrlsSolver?: TwoPhaseIrlsSolver;
329
396
  constructor(params: {
330
397
  outlineSegment: [Point$3, Point$3];
331
398
  fullOutline: [Point$3, Point$3][];
332
399
  componentRotationDegrees: number;
333
- packStrategy: "minimum_sum_squared_distance_to_network" | "minimum_sum_distance_to_network";
400
+ packStrategy: "minimum_sum_squared_distance_to_network" | "minimum_sum_distance_to_network" | "minimum_closest_sum_squared_distance";
334
401
  minGap: number;
335
402
  packedComponents: PackedComponent[];
336
403
  componentToPack: InputComponent;
package/dist/index.js CHANGED
@@ -2165,6 +2165,258 @@ var MultiOffsetIrlsSolver = class extends BaseSolver {
2165
2165
  }
2166
2166
  };
2167
2167
 
2168
+ // lib/solver-utils/TwoPhaseIrlsSolver.ts
2169
+ var TwoPhaseIrlsSolver = class extends BaseSolver {
2170
+ offsetPadPoints;
2171
+ targetPointMap;
2172
+ constraintFn;
2173
+ currentPosition;
2174
+ optimalPosition;
2175
+ initialPosition;
2176
+ phase1Epsilon;
2177
+ phase2Epsilon;
2178
+ maxIterations;
2179
+ phase1Solver;
2180
+ phase2Solver;
2181
+ currentPhase = 1;
2182
+ phase1Position;
2183
+ closestTargetPadId;
2184
+ closestTargetPoint;
2185
+ constructor(params) {
2186
+ super();
2187
+ this.offsetPadPoints = [...params.offsetPadPoints];
2188
+ this.targetPointMap = new Map(params.targetPointMap);
2189
+ this.initialPosition = { ...params.initialPosition };
2190
+ this.currentPosition = { ...params.initialPosition };
2191
+ this.constraintFn = params.constraintFn;
2192
+ this.phase1Epsilon = params.phase1Epsilon ?? params.epsilon ?? 1e-6;
2193
+ this.phase2Epsilon = params.phase2Epsilon ?? params.epsilon ?? 1e-6;
2194
+ this.maxIterations = params.maxIterations ?? 100;
2195
+ }
2196
+ getConstructorParams() {
2197
+ return {
2198
+ offsetPadPoints: this.offsetPadPoints.map((pad) => ({ ...pad })),
2199
+ targetPointMap: new Map(this.targetPointMap),
2200
+ initialPosition: this.initialPosition,
2201
+ constraintFn: this.constraintFn,
2202
+ phase1Epsilon: this.phase1Epsilon,
2203
+ phase2Epsilon: this.phase2Epsilon,
2204
+ maxIterations: this.maxIterations
2205
+ };
2206
+ }
2207
+ _setup() {
2208
+ this.currentPosition = { ...this.initialPosition };
2209
+ this.optimalPosition = void 0;
2210
+ this.currentPhase = 1;
2211
+ this.phase1Position = void 0;
2212
+ this.closestTargetPadId = void 0;
2213
+ this.closestTargetPoint = void 0;
2214
+ const hasTargets = Array.from(this.targetPointMap.values()).some(
2215
+ (targets) => targets.length > 0
2216
+ );
2217
+ if (!hasTargets || this.offsetPadPoints.length === 0) {
2218
+ this.optimalPosition = { ...this.currentPosition };
2219
+ this.solved = true;
2220
+ return;
2221
+ }
2222
+ this.phase1Solver = new MultiOffsetIrlsSolver({
2223
+ offsetPadPoints: this.offsetPadPoints,
2224
+ targetPointMap: this.targetPointMap,
2225
+ initialPosition: this.initialPosition,
2226
+ constraintFn: this.constraintFn,
2227
+ epsilon: this.phase1Epsilon,
2228
+ maxIterations: this.maxIterations,
2229
+ useSquaredDistance: true
2230
+ // Phase 1 uses squared distance
2231
+ });
2232
+ this.phase1Solver.setup();
2233
+ }
2234
+ _step() {
2235
+ if (this.currentPhase === 1) {
2236
+ this.stepPhase1();
2237
+ } else {
2238
+ this.stepPhase2();
2239
+ }
2240
+ }
2241
+ stepPhase1() {
2242
+ if (!this.phase1Solver) return;
2243
+ this.phase1Solver.step();
2244
+ this.currentPosition = this.phase1Solver.getBestPosition();
2245
+ if (this.phase1Solver.solved) {
2246
+ this.phase1Position = this.phase1Solver.getBestPosition();
2247
+ this.setupPhase2();
2248
+ } else if (this.phase1Solver.failed) {
2249
+ this.failed = true;
2250
+ this.error = `Phase 1 failed: ${this.phase1Solver.error}`;
2251
+ }
2252
+ }
2253
+ setupPhase2() {
2254
+ if (!this.phase1Position) return;
2255
+ let minDistance = Infinity;
2256
+ let closestPadId;
2257
+ let closestTarget;
2258
+ for (const pad of this.offsetPadPoints) {
2259
+ const targetPoints = this.targetPointMap.get(pad.id) || [];
2260
+ if (targetPoints.length === 0) continue;
2261
+ const padX = this.phase1Position.x + pad.offsetX;
2262
+ const padY = this.phase1Position.y + pad.offsetY;
2263
+ for (const targetPoint of targetPoints) {
2264
+ const dx = padX - targetPoint.x;
2265
+ const dy = padY - targetPoint.y;
2266
+ const distance = Math.sqrt(dx * dx + dy * dy);
2267
+ if (distance < minDistance) {
2268
+ minDistance = distance;
2269
+ closestPadId = pad.id;
2270
+ closestTarget = targetPoint;
2271
+ }
2272
+ }
2273
+ }
2274
+ this.closestTargetPadId = closestPadId;
2275
+ this.closestTargetPoint = closestTarget;
2276
+ if (!closestPadId || !closestTarget) {
2277
+ this.optimalPosition = this.phase1Position;
2278
+ this.solved = true;
2279
+ return;
2280
+ }
2281
+ const phase2TargetMap = /* @__PURE__ */ new Map();
2282
+ phase2TargetMap.set(closestPadId, [closestTarget]);
2283
+ this.phase2Solver = new MultiOffsetIrlsSolver({
2284
+ offsetPadPoints: this.offsetPadPoints,
2285
+ targetPointMap: phase2TargetMap,
2286
+ initialPosition: this.phase1Position,
2287
+ constraintFn: this.constraintFn,
2288
+ epsilon: this.phase2Epsilon,
2289
+ maxIterations: this.maxIterations,
2290
+ useSquaredDistance: false
2291
+ // Phase 2 uses regular distance
2292
+ });
2293
+ this.phase2Solver.setup();
2294
+ this.currentPhase = 2;
2295
+ }
2296
+ stepPhase2() {
2297
+ if (!this.phase2Solver) return;
2298
+ this.phase2Solver.step();
2299
+ this.currentPosition = this.phase2Solver.getBestPosition();
2300
+ if (this.phase2Solver.solved) {
2301
+ this.optimalPosition = this.phase2Solver.getBestPosition();
2302
+ this.solved = true;
2303
+ } else if (this.phase2Solver.failed) {
2304
+ this.failed = true;
2305
+ this.error = `Phase 2 failed: ${this.phase2Solver.error}`;
2306
+ }
2307
+ }
2308
+ /**
2309
+ * Get the current best position
2310
+ */
2311
+ getBestPosition() {
2312
+ return this.optimalPosition || this.currentPosition;
2313
+ }
2314
+ /**
2315
+ * Get the current absolute positions for all offset pad points
2316
+ */
2317
+ getOffsetPadPositions() {
2318
+ const currentPos = this.getBestPosition();
2319
+ const padPositions = /* @__PURE__ */ new Map();
2320
+ for (const pad of this.offsetPadPoints) {
2321
+ padPositions.set(pad.id, {
2322
+ x: currentPos.x + pad.offsetX,
2323
+ y: currentPos.y + pad.offsetY
2324
+ });
2325
+ }
2326
+ return padPositions;
2327
+ }
2328
+ /**
2329
+ * Calculate total distance from current position to all assigned target points
2330
+ */
2331
+ getTotalDistance(position) {
2332
+ const pos = position || this.getBestPosition();
2333
+ let totalDistance = 0;
2334
+ for (const pad of this.offsetPadPoints) {
2335
+ const padPosition = {
2336
+ x: pos.x + pad.offsetX,
2337
+ y: pos.y + pad.offsetY
2338
+ };
2339
+ const targetPoints = this.targetPointMap.get(pad.id) || [];
2340
+ for (const target of targetPoints) {
2341
+ const dx = padPosition.x - target.x;
2342
+ const dy = padPosition.y - target.y;
2343
+ totalDistance += dx * dx + dy * dy;
2344
+ }
2345
+ }
2346
+ return totalDistance;
2347
+ }
2348
+ visualize() {
2349
+ const graphics = {
2350
+ lines: [],
2351
+ points: [],
2352
+ rects: [],
2353
+ circles: []
2354
+ };
2355
+ graphics.points.push({
2356
+ ...this.currentPosition,
2357
+ color: this.currentPhase === 1 ? "#FF6B6B" : "#4ECDC4",
2358
+ label: `Phase ${this.currentPhase}`
2359
+ });
2360
+ if (this.phase1Position && this.currentPhase === 2) {
2361
+ graphics.points.push({
2362
+ ...this.phase1Position,
2363
+ color: "rgba(255, 107, 107, 0.5)",
2364
+ label: "Phase 1 result"
2365
+ });
2366
+ }
2367
+ if (this.closestTargetPoint && this.closestTargetPadId) {
2368
+ graphics.points.push({
2369
+ ...this.closestTargetPoint,
2370
+ color: "#FFA500",
2371
+ label: `Closest target (${this.closestTargetPadId})`
2372
+ });
2373
+ const closestPad = this.offsetPadPoints.find(
2374
+ (p) => p.id === this.closestTargetPadId
2375
+ );
2376
+ if (closestPad) {
2377
+ const currentPos = this.getBestPosition();
2378
+ const padPos = {
2379
+ x: currentPos.x + closestPad.offsetX,
2380
+ y: currentPos.y + closestPad.offsetY
2381
+ };
2382
+ graphics.lines.push({
2383
+ points: [padPos, this.closestTargetPoint],
2384
+ strokeColor: "#FFA500"
2385
+ });
2386
+ }
2387
+ }
2388
+ const activeSolver = this.currentPhase === 1 ? this.phase1Solver : this.phase2Solver;
2389
+ if (activeSolver) {
2390
+ const solverViz = activeSolver.visualize();
2391
+ const phaseColor = this.currentPhase === 1 ? "rgba(255, 107, 107, 0.7)" : "rgba(76, 205, 196, 0.7)";
2392
+ if (solverViz.lines) {
2393
+ const modifiedLines = solverViz.lines.map((line) => ({
2394
+ ...line,
2395
+ strokeColor: phaseColor
2396
+ }));
2397
+ graphics.lines.push(...modifiedLines);
2398
+ }
2399
+ if (solverViz.points) {
2400
+ graphics.points.push(...solverViz.points);
2401
+ }
2402
+ if (solverViz.rects) {
2403
+ graphics.rects.push(...solverViz.rects);
2404
+ }
2405
+ if (solverViz.circles) {
2406
+ graphics.circles.push(...solverViz.circles);
2407
+ }
2408
+ }
2409
+ if (this.optimalPosition) {
2410
+ graphics.points.push({
2411
+ ...this.optimalPosition,
2412
+ color: "rgba(76, 175, 80, 0.8)",
2413
+ label: "Final optimal position"
2414
+ });
2415
+ }
2416
+ return graphics;
2417
+ }
2418
+ };
2419
+
2168
2420
  // lib/geometry/pointInOutline.ts
2169
2421
  var EPS = 1e-9;
2170
2422
  function cross2(ax, ay, bx, by) {
@@ -2309,6 +2561,7 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver {
2309
2561
  viableBounds;
2310
2562
  optimalPosition;
2311
2563
  irlsSolver;
2564
+ twoPhaseIrlsSolver;
2312
2565
  constructor(params) {
2313
2566
  super();
2314
2567
  this.outlineSegment = params.outlineSegment;
@@ -2427,29 +2680,41 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver {
2427
2680
  x: (vp1.x + vp2.x) / 2,
2428
2681
  y: (vp1.y + vp2.y) / 2
2429
2682
  });
2430
- this.irlsSolver = new MultiOffsetIrlsSolver({
2431
- offsetPadPoints,
2432
- targetPointMap,
2433
- initialPosition,
2434
- constraintFn,
2435
- epsilon: 1e-6,
2436
- maxIterations: 50,
2437
- useSquaredDistance: this.packStrategy === "minimum_sum_squared_distance_to_network"
2438
- });
2683
+ if (this.packStrategy === "minimum_closest_sum_squared_distance") {
2684
+ this.twoPhaseIrlsSolver = new TwoPhaseIrlsSolver({
2685
+ offsetPadPoints,
2686
+ targetPointMap,
2687
+ initialPosition,
2688
+ constraintFn,
2689
+ epsilon: 1e-6,
2690
+ maxIterations: 50
2691
+ });
2692
+ } else {
2693
+ this.irlsSolver = new MultiOffsetIrlsSolver({
2694
+ offsetPadPoints,
2695
+ targetPointMap,
2696
+ initialPosition,
2697
+ constraintFn,
2698
+ epsilon: 1e-6,
2699
+ maxIterations: 50,
2700
+ useSquaredDistance: this.packStrategy === "minimum_sum_squared_distance_to_network"
2701
+ });
2702
+ }
2439
2703
  }
2440
2704
  _step() {
2441
- if (!this.irlsSolver) {
2705
+ const activeSolver = this.irlsSolver || this.twoPhaseIrlsSolver;
2706
+ if (!activeSolver) {
2442
2707
  this.solved = true;
2443
2708
  return;
2444
2709
  }
2445
- this.irlsSolver.step();
2446
- if (this.irlsSolver.solved) {
2447
- const rawPosition = this.irlsSolver.getBestPosition();
2710
+ activeSolver.step();
2711
+ if (activeSolver.solved) {
2712
+ const rawPosition = activeSolver.getBestPosition();
2448
2713
  this.optimalPosition = rawPosition;
2449
2714
  this.solved = true;
2450
- } else if (this.irlsSolver.failed) {
2715
+ } else if (activeSolver.failed) {
2451
2716
  this.failed = true;
2452
- this.error = this.irlsSolver.error;
2717
+ this.error = activeSolver.error;
2453
2718
  }
2454
2719
  }
2455
2720
  /**
@@ -2672,7 +2937,7 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver {
2672
2937
  });
2673
2938
  }
2674
2939
  }
2675
- const pos = this.optimalPosition ?? this.irlsSolver?.currentPosition ?? {
2940
+ const pos = this.optimalPosition ?? this.irlsSolver?.currentPosition ?? this.twoPhaseIrlsSolver?.currentPosition ?? {
2676
2941
  x: 0,
2677
2942
  y: 0
2678
2943
  };
@@ -2702,13 +2967,14 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver {
2702
2967
  }
2703
2968
  }
2704
2969
  }
2705
- if (this.irlsSolver) {
2706
- const currentPos = this.irlsSolver.currentPosition;
2970
+ const activeSolver = this.irlsSolver || this.twoPhaseIrlsSolver;
2971
+ if (activeSolver) {
2972
+ const currentPos = activeSolver.currentPosition;
2707
2973
  graphics.points.push({
2708
2974
  ...currentPos,
2709
2975
  color: "#f44336"
2710
2976
  });
2711
- const solverViz = this.irlsSolver.visualize();
2977
+ const solverViz = activeSolver.visualize();
2712
2978
  if (solverViz.lines) {
2713
2979
  graphics.lines.push(...solverViz.lines);
2714
2980
  }
@@ -2871,7 +3137,7 @@ var SingleComponentPackSolver = class extends BaseSolver {
2871
3137
  outlineSegment: queuedSegment.segment,
2872
3138
  fullOutline: queuedSegment.fullOutline,
2873
3139
  componentRotationDegrees: rotation,
2874
- packStrategy: "minimum_sum_squared_distance_to_network",
3140
+ packStrategy: this.packPlacementStrategy === "minimum_closest_sum_squared_distance" ? "minimum_closest_sum_squared_distance" : "minimum_sum_squared_distance_to_network",
2875
3141
  minGap: this.minGap,
2876
3142
  packedComponents: this.packedComponents,
2877
3143
  componentToPack: this.componentToPack
@@ -2900,7 +3166,7 @@ var SingleComponentPackSolver = class extends BaseSolver {
2900
3166
  calculateDistance(position, rotation) {
2901
3167
  const tempComponent = this.createPackedComponent(position, rotation);
2902
3168
  let totalDistance = 0;
2903
- const useSquaredDistance = this.packPlacementStrategy === "minimum_sum_squared_distance_to_network";
3169
+ const useSquaredDistance = this.packPlacementStrategy === "minimum_sum_squared_distance_to_network" || this.packPlacementStrategy === "minimum_closest_sum_squared_distance";
2904
3170
  for (const pad of tempComponent.pads) {
2905
3171
  let minDistanceToNetwork = Infinity;
2906
3172
  for (const packedComponent of this.packedComponents) {
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.29",
5
+ "version": "0.0.30",
6
6
  "description": "Calculate a packing layout with support for different strategy configurations",
7
7
  "scripts": {
8
8
  "start": "cosmos",