@tscircuit/schematic-trace-solver 0.0.45 → 0.0.47

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 (27) hide show
  1. package/.github/workflows/bun-formatcheck.yml +1 -1
  2. package/.github/workflows/bun-pver-release.yml +1 -1
  3. package/.github/workflows/bun-test.yml +1 -1
  4. package/.github/workflows/bun-typecheck.yml +1 -1
  5. package/dist/index.d.ts +35 -9
  6. package/dist/index.js +543 -186
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +92 -50
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +16 -5
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +95 -75
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts +74 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +45 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts +71 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +162 -28
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/doesTraceStartOrEndInLabel.ts +58 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
  18. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +13 -1
  19. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +110 -60
  20. package/package.json +1 -1
  21. package/site/examples/example27.page.tsx +4 -0
  22. package/tests/assets/example27.json +168 -0
  23. package/tests/examples/__snapshots__/example03.snap.svg +67 -67
  24. package/tests/examples/__snapshots__/example16.snap.svg +3 -3
  25. package/tests/examples/__snapshots__/example30.snap.svg +258 -0
  26. package/tests/examples/example30.test.ts +12 -0
  27. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +4 -1
package/dist/index.js CHANGED
@@ -2260,12 +2260,134 @@ var NetLabelPlacementSolver = class extends BaseSolver {
2260
2260
  }
2261
2261
  };
2262
2262
 
2263
+ // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts
2264
+ var groupLabelsByChipAndOrientation = ({
2265
+ labels,
2266
+ chips
2267
+ }) => {
2268
+ const groupedLabels = {};
2269
+ for (const label of labels) {
2270
+ if (label.pinIds.length === 0) {
2271
+ continue;
2272
+ }
2273
+ const chipId = label.pinIds[0].split(".")[0];
2274
+ if (!chipId) {
2275
+ continue;
2276
+ }
2277
+ const key = `${chipId}-${label.orientation}`;
2278
+ if (!groupedLabels[key]) {
2279
+ groupedLabels[key] = [];
2280
+ }
2281
+ groupedLabels[key].push(label);
2282
+ }
2283
+ return groupedLabels;
2284
+ };
2285
+
2286
+ // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts
2287
+ var mergeLabelGroup = (group, groupKey) => {
2288
+ if (group.length === 0) {
2289
+ throw new Error("Cannot merge an empty group of labels.");
2290
+ }
2291
+ let minX = Infinity;
2292
+ let minY = Infinity;
2293
+ let maxX = -Infinity;
2294
+ let maxY = -Infinity;
2295
+ const allPinIds = /* @__PURE__ */ new Set();
2296
+ const allMspConnectionPairIds = /* @__PURE__ */ new Set();
2297
+ const originalNetIds = /* @__PURE__ */ new Set();
2298
+ for (const label of group) {
2299
+ const bounds = getRectBounds(label.center, label.width, label.height);
2300
+ minX = Math.min(minX, bounds.minX);
2301
+ minY = Math.min(minY, bounds.minY);
2302
+ maxX = Math.max(maxX, bounds.maxX);
2303
+ maxY = Math.max(maxY, bounds.maxY);
2304
+ label.pinIds.forEach((id) => allPinIds.add(id));
2305
+ label.mspConnectionPairIds.forEach((id) => allMspConnectionPairIds.add(id));
2306
+ originalNetIds.add(label.globalConnNetId);
2307
+ }
2308
+ const newWidth = maxX - minX;
2309
+ const newHeight = maxY - minY;
2310
+ const newCenter = { x: minX + newWidth / 2, y: minY + newHeight / 2 };
2311
+ const template = group[0];
2312
+ const syntheticId = `merged-group-${groupKey}`;
2313
+ const mergedLabel = {
2314
+ ...template,
2315
+ // Copy common properties
2316
+ globalConnNetId: syntheticId,
2317
+ center: newCenter,
2318
+ width: newWidth,
2319
+ height: newHeight,
2320
+ pinIds: Array.from(allPinIds),
2321
+ mspConnectionPairIds: Array.from(allMspConnectionPairIds)
2322
+ };
2323
+ return {
2324
+ mergedLabel,
2325
+ originalNetIds
2326
+ };
2327
+ };
2328
+
2329
+ // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts
2330
+ import { distance } from "@tscircuit/math-utils";
2331
+ var filterLabelsAtTraceEdges = ({
2332
+ labels,
2333
+ traces,
2334
+ distanceThreshold = 0.5
2335
+ // Example threshold
2336
+ }) => {
2337
+ const tracesByNetId = /* @__PURE__ */ new Map();
2338
+ if (!traces) {
2339
+ throw new Error("No traces provided to filterLabelsAtTraceEdges");
2340
+ }
2341
+ for (const trace of traces) {
2342
+ if (!trace.globalConnNetId) continue;
2343
+ if (!tracesByNetId.has(trace.globalConnNetId)) {
2344
+ tracesByNetId.set(trace.globalConnNetId, []);
2345
+ }
2346
+ tracesByNetId.get(trace.globalConnNetId).push(trace);
2347
+ }
2348
+ const filteredLabels = [];
2349
+ for (const label of labels) {
2350
+ if (label.mspConnectionPairIds.length === 0) {
2351
+ filteredLabels.push(label);
2352
+ continue;
2353
+ }
2354
+ const relevantTraces = tracesByNetId.get(label.globalConnNetId);
2355
+ let isNearTraceEdge = false;
2356
+ if (!relevantTraces || relevantTraces.length === 0) {
2357
+ continue;
2358
+ }
2359
+ for (const trace of relevantTraces) {
2360
+ if (trace.tracePath.length === 0) continue;
2361
+ const startPoint = trace.tracePath[0];
2362
+ const endPoint = trace.tracePath[trace.tracePath.length - 1];
2363
+ const startDist = distance(label.center, startPoint);
2364
+ const endDist = distance(label.center, endPoint);
2365
+ if (startDist <= distanceThreshold || endDist <= distanceThreshold) {
2366
+ isNearTraceEdge = true;
2367
+ break;
2368
+ }
2369
+ }
2370
+ if (isNearTraceEdge) {
2371
+ filteredLabels.push(label);
2372
+ }
2373
+ }
2374
+ return filteredLabels;
2375
+ };
2376
+
2263
2377
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts
2264
2378
  var MergedNetLabelObstacleSolver = class extends BaseSolver {
2265
2379
  input;
2266
2380
  output;
2267
2381
  inputProblem;
2268
2382
  traces;
2383
+ // State for the new pipeline
2384
+ pipelineStep = "filtering_labels";
2385
+ filteredLabels = [];
2386
+ labelGroups = {};
2387
+ groupKeysToProcess = [];
2388
+ finalPlacements = [];
2389
+ mergedLabelNetIdMap = {};
2390
+ activeMergingGroupKey = null;
2269
2391
  constructor(solverInput) {
2270
2392
  super();
2271
2393
  this.input = solverInput;
@@ -2277,67 +2399,58 @@ var MergedNetLabelObstacleSolver = class extends BaseSolver {
2277
2399
  };
2278
2400
  }
2279
2401
  _step() {
2280
- const originalLabels = this.input.netLabelPlacements;
2281
- const mergedLabelNetIdMap = {};
2282
- if (!originalLabels || originalLabels.length === 0) {
2283
- this.output = {
2284
- netLabelPlacements: [],
2285
- mergedLabelNetIdMap: {}
2286
- };
2287
- this.solved = true;
2288
- return;
2289
- }
2290
- const labelGroups = {};
2291
- for (const p of originalLabels) {
2292
- if (p.pinIds.length === 0) continue;
2293
- const chipId = p.pinIds[0].split(".")[0];
2294
- if (!chipId) continue;
2295
- const key = `${chipId}-${p.orientation}`;
2296
- if (!(key in labelGroups)) {
2297
- labelGroups[key] = [];
2298
- }
2299
- labelGroups[key].push(p);
2300
- }
2301
- const finalPlacements = [];
2302
- for (const [key, group] of Object.entries(labelGroups)) {
2303
- if (group.length <= 1) {
2304
- finalPlacements.push(...group);
2305
- continue;
2306
- }
2307
- let minX = Infinity;
2308
- let minY = Infinity;
2309
- let maxX = -Infinity;
2310
- let maxY = -Infinity;
2311
- for (const p of group) {
2312
- const bounds = getRectBounds(p.center, p.width, p.height);
2313
- minX = Math.min(minX, bounds.minX);
2314
- minY = Math.min(minY, bounds.minY);
2315
- maxX = Math.max(maxX, bounds.maxX);
2316
- maxY = Math.max(maxY, bounds.maxY);
2317
- }
2318
- const newWidth = maxX - minX;
2319
- const newHeight = maxY - minY;
2320
- const template = group[0];
2321
- const syntheticId = `merged-group-${key}`;
2322
- const originalNetIds = new Set(group.map((p) => p.globalConnNetId));
2323
- mergedLabelNetIdMap[syntheticId] = originalNetIds;
2324
- finalPlacements.push({
2325
- ...template,
2326
- globalConnNetId: syntheticId,
2327
- width: newWidth,
2328
- height: newHeight,
2329
- center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
2330
- pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
2331
- mspConnectionPairIds: [
2332
- ...new Set(group.flatMap((p) => p.mspConnectionPairIds))
2333
- ]
2334
- });
2402
+ switch (this.pipelineStep) {
2403
+ case "filtering_labels":
2404
+ this.filteredLabels = filterLabelsAtTraceEdges({
2405
+ labels: this.input.netLabelPlacements,
2406
+ traces: this.traces
2407
+ });
2408
+ this.pipelineStep = "grouping_labels";
2409
+ break;
2410
+ case "grouping_labels":
2411
+ this.labelGroups = groupLabelsByChipAndOrientation({
2412
+ labels: this.filteredLabels,
2413
+ chips: this.inputProblem.chips
2414
+ });
2415
+ this.groupKeysToProcess = Object.keys(this.labelGroups);
2416
+ this.pipelineStep = "merging_groups";
2417
+ break;
2418
+ case "merging_groups":
2419
+ if (this.groupKeysToProcess.length === 0) {
2420
+ this.pipelineStep = "finalizing";
2421
+ this.activeMergingGroupKey = null;
2422
+ break;
2423
+ }
2424
+ const groupKey = this.groupKeysToProcess.pop();
2425
+ this.activeMergingGroupKey = groupKey;
2426
+ const group = this.labelGroups[groupKey];
2427
+ if (group.length > 1) {
2428
+ const { mergedLabel, originalNetIds } = mergeLabelGroup(
2429
+ group,
2430
+ groupKey
2431
+ );
2432
+ this.finalPlacements.push(mergedLabel);
2433
+ this.mergedLabelNetIdMap[mergedLabel.globalConnNetId] = originalNetIds;
2434
+ } else {
2435
+ this.finalPlacements.push(...group);
2436
+ }
2437
+ break;
2438
+ case "finalizing":
2439
+ const processedOriginalIds = new Set(
2440
+ this.finalPlacements.flatMap(
2441
+ (p) => this.mergedLabelNetIdMap[p.globalConnNetId] ? [...this.mergedLabelNetIdMap[p.globalConnNetId]] : [p.globalConnNetId]
2442
+ )
2443
+ );
2444
+ const unprocessedLabels = this.input.netLabelPlacements.filter(
2445
+ (l) => !processedOriginalIds.has(l.globalConnNetId)
2446
+ );
2447
+ this.output = {
2448
+ netLabelPlacements: [...this.finalPlacements, ...unprocessedLabels],
2449
+ mergedLabelNetIdMap: this.mergedLabelNetIdMap
2450
+ };
2451
+ this.solved = true;
2452
+ break;
2335
2453
  }
2336
- this.output = {
2337
- netLabelPlacements: finalPlacements,
2338
- mergedLabelNetIdMap
2339
- };
2340
- this.solved = true;
2341
2454
  }
2342
2455
  getOutput() {
2343
2456
  return this.output;
@@ -2362,6 +2475,19 @@ var MergedNetLabelObstacleSolver = class extends BaseSolver {
2362
2475
  };
2363
2476
  graphics.lines.push(line);
2364
2477
  }
2478
+ if (this.activeMergingGroupKey && this.labelGroups[this.activeMergingGroupKey]) {
2479
+ const activeGroup = this.labelGroups[this.activeMergingGroupKey];
2480
+ for (const label of activeGroup) {
2481
+ graphics.rects.push({
2482
+ center: label.center,
2483
+ width: label.width,
2484
+ height: label.height,
2485
+ fill: "rgba(255, 165, 0, 0.5)",
2486
+ // Orange highlight
2487
+ stroke: "orange"
2488
+ });
2489
+ }
2490
+ }
2365
2491
  for (const finalLabel of this.output.netLabelPlacements) {
2366
2492
  const isMerged = finalLabel.globalConnNetId.startsWith("merged-group-");
2367
2493
  const color = getColorFromString(finalLabel.globalConnNetId);
@@ -2371,7 +2497,6 @@ var MergedNetLabelObstacleSolver = class extends BaseSolver {
2371
2497
  width: finalLabel.width,
2372
2498
  height: finalLabel.height,
2373
2499
  fill: color.replace(/, 1\)/, ", 0.2)"),
2374
- // semi-transparent
2375
2500
  stroke: color,
2376
2501
  label: finalLabel.globalConnNetId
2377
2502
  });
@@ -2417,14 +2542,17 @@ var MergedNetLabelObstacleSolver = class extends BaseSolver {
2417
2542
  };
2418
2543
 
2419
2544
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts
2420
- var detectTraceLabelOverlap = (traces, netLabels) => {
2545
+ var detectTraceLabelOverlap = ({
2546
+ traces,
2547
+ netLabels = []
2548
+ }) => {
2421
2549
  const overlaps = [];
2422
2550
  for (const trace of traces) {
2423
2551
  for (const label of netLabels) {
2424
2552
  const labelBounds = getRectBounds(label.center, label.width, label.height);
2425
- for (let i = 0; i < trace.tracePath.length - 1; i++) {
2426
- const p1 = trace.tracePath[i];
2427
- const p2 = trace.tracePath[i + 1];
2553
+ for (let j = 0; j < trace.tracePath.length - 1; j++) {
2554
+ const p1 = trace.tracePath[j];
2555
+ const p2 = trace.tracePath[j + 1];
2428
2556
  if (segmentIntersectsRect2(p1, p2, labelBounds)) {
2429
2557
  if (trace.globalConnNetId === label.globalConnNetId) {
2430
2558
  break;
@@ -2521,73 +2649,106 @@ var generateFourPointDetourCandidates = ({
2521
2649
  paddingBuffer,
2522
2650
  detourCount
2523
2651
  }) => {
2524
- let collidingSegIndex = -1;
2525
- for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
2526
- if (segmentIntersectsRect(
2527
- initialTrace.tracePath[i],
2528
- initialTrace.tracePath[i + 1],
2529
- labelBounds
2530
- )) {
2531
- collidingSegIndex = i;
2532
- break;
2652
+ const isMergedLabel = label.globalConnNetId.startsWith("merged-group-");
2653
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer;
2654
+ const paddedLabelBounds = {
2655
+ minX: labelBounds.minX - effectivePadding,
2656
+ maxX: labelBounds.maxX + effectivePadding,
2657
+ minY: labelBounds.minY - effectivePadding,
2658
+ maxY: labelBounds.maxY + effectivePadding
2659
+ };
2660
+ let entryPoint, exitPoint;
2661
+ let entryIndex, exitIndex;
2662
+ const isPointInRect = (p) => p.x >= paddedLabelBounds.minX && p.x <= paddedLabelBounds.maxX && p.y >= paddedLabelBounds.minY && p.y <= paddedLabelBounds.maxY;
2663
+ if (isMergedLabel) {
2664
+ let firstInsideIndex = -1;
2665
+ for (let i = 0; i < initialTrace.tracePath.length; i++) {
2666
+ if (isPointInRect(initialTrace.tracePath[i])) {
2667
+ firstInsideIndex = i;
2668
+ break;
2669
+ }
2670
+ }
2671
+ if (firstInsideIndex === -1) return [];
2672
+ entryIndex = Math.max(0, firstInsideIndex - 1);
2673
+ entryPoint = initialTrace.tracePath[entryIndex];
2674
+ let firstOutsideIndex = -1;
2675
+ for (let i = firstInsideIndex; i < initialTrace.tracePath.length; i++) {
2676
+ if (!isPointInRect(initialTrace.tracePath[i])) {
2677
+ firstOutsideIndex = i;
2678
+ break;
2679
+ }
2680
+ }
2681
+ if (firstOutsideIndex === -1) {
2682
+ exitIndex = initialTrace.tracePath.length - 1;
2683
+ } else {
2684
+ exitIndex = firstOutsideIndex;
2533
2685
  }
2686
+ exitPoint = initialTrace.tracePath[exitIndex];
2687
+ } else {
2688
+ let collidingSegIndex = -1;
2689
+ for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
2690
+ if (segmentIntersectsRect(
2691
+ initialTrace.tracePath[i],
2692
+ initialTrace.tracePath[i + 1],
2693
+ { ...paddedLabelBounds, chipId: "temp-obstacle" }
2694
+ )) {
2695
+ collidingSegIndex = i;
2696
+ break;
2697
+ }
2698
+ }
2699
+ if (collidingSegIndex === -1) return [];
2700
+ entryIndex = collidingSegIndex;
2701
+ exitIndex = collidingSegIndex + 1;
2702
+ entryPoint = initialTrace.tracePath[entryIndex];
2703
+ exitPoint = initialTrace.tracePath[exitIndex];
2534
2704
  }
2535
- if (collidingSegIndex === -1) return [];
2536
- const pA = initialTrace.tracePath[collidingSegIndex];
2537
- const pB = initialTrace.tracePath[collidingSegIndex + 1];
2538
- if (!pA || !pB) return [];
2705
+ if (!entryPoint || !exitPoint || entryIndex >= exitIndex) return [];
2539
2706
  const candidateDetours = [];
2540
- const paddedLabelBounds = getRectBounds(
2541
- label.center,
2542
- label.width,
2543
- label.height
2544
- );
2545
- const effectivePadding = paddingBuffer + detourCount * paddingBuffer;
2546
- if (isVertical(pA, pB)) {
2547
- const xCandidates = [
2548
- paddedLabelBounds.maxX + effectivePadding,
2549
- paddedLabelBounds.minX - effectivePadding
2550
- ];
2551
- for (const newX of xCandidates) {
2707
+ const dx = exitPoint.x - entryPoint.x;
2708
+ const dy = exitPoint.y - entryPoint.y;
2709
+ if (Math.abs(dx) > Math.abs(dy)) {
2710
+ const yCandidates = [paddedLabelBounds.maxY, paddedLabelBounds.minY];
2711
+ for (const newY of yCandidates) {
2552
2712
  candidateDetours.push(
2553
- pB.y > pA.y ? [
2554
- { x: pA.x, y: paddedLabelBounds.minY - effectivePadding },
2555
- { x: newX, y: paddedLabelBounds.minY - effectivePadding },
2556
- { x: newX, y: paddedLabelBounds.maxY + effectivePadding },
2557
- { x: pB.x, y: paddedLabelBounds.maxY + effectivePadding }
2713
+ dx > 0 ? [
2714
+ { x: paddedLabelBounds.minX, y: entryPoint.y },
2715
+ { x: paddedLabelBounds.minX, y: newY },
2716
+ { x: paddedLabelBounds.maxX, y: newY },
2717
+ { x: paddedLabelBounds.maxX, y: exitPoint.y }
2558
2718
  ] : [
2559
- { x: pA.x, y: paddedLabelBounds.maxY + effectivePadding },
2560
- { x: newX, y: paddedLabelBounds.maxY + effectivePadding },
2561
- { x: newX, y: paddedLabelBounds.minY - effectivePadding },
2562
- { x: pB.x, y: paddedLabelBounds.minY - effectivePadding }
2719
+ // Right-to-left
2720
+ { x: paddedLabelBounds.maxX, y: entryPoint.y },
2721
+ { x: paddedLabelBounds.maxX, y: newY },
2722
+ { x: paddedLabelBounds.minX, y: newY },
2723
+ { x: paddedLabelBounds.minX, y: exitPoint.y }
2563
2724
  ]
2564
2725
  );
2565
2726
  }
2566
2727
  } else {
2567
- const yCandidates = [
2568
- paddedLabelBounds.maxY + effectivePadding,
2569
- paddedLabelBounds.minY - effectivePadding
2570
- ];
2571
- for (const newY of yCandidates) {
2728
+ const xCandidates = [paddedLabelBounds.maxX, paddedLabelBounds.minX];
2729
+ for (const newX of xCandidates) {
2572
2730
  candidateDetours.push(
2573
- pB.x > pA.x ? [
2574
- { x: paddedLabelBounds.minX - effectivePadding, y: pA.y },
2575
- { x: paddedLabelBounds.minX - effectivePadding, y: newY },
2576
- { x: paddedLabelBounds.maxX + effectivePadding, y: newY },
2577
- { x: paddedLabelBounds.maxX + effectivePadding, y: pB.y }
2731
+ dy > 0 ? [
2732
+ { x: entryPoint.x, y: paddedLabelBounds.minY },
2733
+ { x: newX, y: paddedLabelBounds.minY },
2734
+ { x: newX, y: paddedLabelBounds.maxY },
2735
+ { x: exitPoint.x, y: paddedLabelBounds.maxY }
2578
2736
  ] : [
2579
- { x: paddedLabelBounds.maxX + effectivePadding, y: pA.y },
2580
- { x: paddedLabelBounds.maxX + effectivePadding, y: newY },
2581
- { x: paddedLabelBounds.minX - effectivePadding, y: newY },
2582
- { x: paddedLabelBounds.minX - effectivePadding, y: pB.y }
2737
+ // Bottom-to-top
2738
+ { x: entryPoint.x, y: paddedLabelBounds.maxY },
2739
+ { x: newX, y: paddedLabelBounds.maxY },
2740
+ { x: newX, y: paddedLabelBounds.minY },
2741
+ { x: exitPoint.x, y: paddedLabelBounds.minY }
2583
2742
  ]
2584
2743
  );
2585
2744
  }
2586
2745
  }
2587
2746
  return candidateDetours.map((detourPoints) => [
2588
- ...initialTrace.tracePath.slice(0, collidingSegIndex + 1),
2747
+ ...initialTrace.tracePath.slice(0, entryIndex),
2748
+ entryPoint,
2589
2749
  ...detourPoints,
2590
- ...initialTrace.tracePath.slice(collidingSegIndex + 1)
2750
+ exitPoint,
2751
+ ...initialTrace.tracePath.slice(exitIndex + 1)
2591
2752
  ]);
2592
2753
  };
2593
2754
 
@@ -2631,13 +2792,12 @@ var generateRerouteCandidates = ({
2631
2792
  if (trace.globalConnNetId === label.globalConnNetId) {
2632
2793
  return [initialTrace.tracePath];
2633
2794
  }
2634
- const labelPadding = paddingBuffer;
2635
2795
  const labelBoundsRaw = getRectBounds(label.center, label.width, label.height);
2636
2796
  const labelBounds = {
2637
- minX: labelBoundsRaw.minX - labelPadding,
2638
- minY: labelBoundsRaw.minY - labelPadding,
2639
- maxX: labelBoundsRaw.maxX + labelPadding,
2640
- maxY: labelBoundsRaw.maxY + labelPadding,
2797
+ minX: labelBoundsRaw.minX,
2798
+ minY: labelBoundsRaw.minY,
2799
+ maxX: labelBoundsRaw.maxX,
2800
+ maxY: labelBoundsRaw.maxY,
2641
2801
  chipId: `netlabel-${label.netId}`
2642
2802
  };
2643
2803
  const fourPointCandidates = generateFourPointDetourCandidates({
@@ -2663,6 +2823,7 @@ var generateRerouteCandidates = ({
2663
2823
  };
2664
2824
 
2665
2825
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts
2826
+ var MAX_TRIES = 5;
2666
2827
  var SingleOverlapSolver = class extends BaseSolver {
2667
2828
  queuedCandidatePaths;
2668
2829
  solvedTracePath = null;
@@ -2670,13 +2831,17 @@ var SingleOverlapSolver = class extends BaseSolver {
2670
2831
  problem;
2671
2832
  obstacles;
2672
2833
  label;
2834
+ _tried = 0;
2673
2835
  constructor(solverInput) {
2674
2836
  super();
2675
2837
  this.initialTrace = solverInput.trace;
2676
2838
  this.problem = solverInput.problem;
2677
2839
  this.label = solverInput.label;
2840
+ const effectivePadding = solverInput.paddingBuffer + solverInput.detourCount * solverInput.paddingBuffer;
2678
2841
  const candidates = generateRerouteCandidates({
2679
- ...solverInput
2842
+ ...solverInput,
2843
+ paddingBuffer: effectivePadding
2844
+ // Use the calculated, larger padding
2680
2845
  });
2681
2846
  const getPathLength = (pts) => {
2682
2847
  let len = 0;
@@ -2693,10 +2858,11 @@ var SingleOverlapSolver = class extends BaseSolver {
2693
2858
  this.obstacles = getObstacleRects(this.problem);
2694
2859
  }
2695
2860
  _step() {
2696
- if (this.queuedCandidatePaths.length === 0) {
2861
+ if (this.queuedCandidatePaths.length === 0 || this._tried >= MAX_TRIES) {
2697
2862
  this.failed = true;
2698
2863
  return;
2699
2864
  }
2865
+ this._tried++;
2700
2866
  const nextCandidatePath = this.queuedCandidatePaths.shift();
2701
2867
  const simplifiedPath = simplifyPath(nextCandidatePath);
2702
2868
  if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles)) {
@@ -2738,26 +2904,92 @@ var SingleOverlapSolver = class extends BaseSolver {
2738
2904
  }
2739
2905
  };
2740
2906
 
2907
+ // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/doesTraceStartOrEndInLabel.ts
2908
+ var doesTraceStartOrEndInLabel = ({ trace, label }) => {
2909
+ if (trace.tracePath.length < 2) {
2910
+ return false;
2911
+ }
2912
+ const firstSegmentTrace = {
2913
+ ...trace,
2914
+ tracePath: [trace.tracePath[0], trace.tracePath[1]]
2915
+ };
2916
+ const firstSegmentOverlap = detectTraceLabelOverlap({
2917
+ traces: [firstSegmentTrace],
2918
+ netLabels: [label]
2919
+ });
2920
+ if (firstSegmentOverlap.length > 0) {
2921
+ return true;
2922
+ }
2923
+ if (trace.tracePath.length > 2) {
2924
+ const lastPoint = trace.tracePath[trace.tracePath.length - 1];
2925
+ const secondToLastPoint = trace.tracePath[trace.tracePath.length - 2];
2926
+ const lastSegmentTrace = {
2927
+ ...trace,
2928
+ tracePath: [secondToLastPoint, lastPoint]
2929
+ };
2930
+ const lastSegmentOverlap = detectTraceLabelOverlap({
2931
+ traces: [lastSegmentTrace],
2932
+ netLabels: [label]
2933
+ });
2934
+ if (lastSegmentOverlap.length > 0) {
2935
+ return true;
2936
+ }
2937
+ }
2938
+ return false;
2939
+ };
2940
+
2941
+ // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts
2942
+ var visualizeDecomposition = (params) => {
2943
+ const { decomposedChildLabels, collidingTrace, mergedLabel, graphics } = params;
2944
+ if (!graphics.rects) graphics.rects = [];
2945
+ if (!graphics.texts) graphics.texts = [];
2946
+ for (const childLabel of decomposedChildLabels) {
2947
+ const isOwnLabel = childLabel.globalConnNetId === collidingTrace.globalConnNetId;
2948
+ graphics.rects.push({
2949
+ center: childLabel.center,
2950
+ width: childLabel.width,
2951
+ height: childLabel.height,
2952
+ fill: isOwnLabel ? "green" : "red"
2953
+ // Green for own label, red for others
2954
+ });
2955
+ }
2956
+ graphics.texts.push({
2957
+ x: mergedLabel.center.x,
2958
+ y: mergedLabel.center.y + mergedLabel.height / 2 + 0.5,
2959
+ text: `DECOMPOSITION: Trace ${collidingTrace.mspPairId} vs Merged Label ${mergedLabel.globalConnNetId}`,
2960
+ fontSize: 0.3,
2961
+ color: "blue"
2962
+ });
2963
+ return graphics;
2964
+ };
2965
+
2741
2966
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts
2742
2967
  var OverlapAvoidanceStepSolver = class extends BaseSolver {
2743
2968
  inputProblem;
2744
- netLabelPlacements;
2969
+ initialNetLabelPlacements;
2970
+ mergedNetLabelPlacements;
2745
2971
  mergedLabelNetIdMap;
2746
2972
  allTraces;
2747
2973
  modifiedTraces = [];
2748
- detourCountByLabel = {};
2749
2974
  PADDING_BUFFER = 0.1;
2975
+ detourCounts = /* @__PURE__ */ new Map();
2750
2976
  activeSubSolver = null;
2751
2977
  overlapQueue = [];
2752
2978
  recentlyFailed = /* @__PURE__ */ new Set();
2979
+ currentlyProcessingOverlap = null;
2980
+ decomposedChildLabels = null;
2753
2981
  constructor(solverInput) {
2754
2982
  super();
2755
2983
  this.inputProblem = solverInput.inputProblem;
2756
- this.netLabelPlacements = solverInput.netLabelPlacements;
2984
+ this.initialNetLabelPlacements = solverInput.initialNetLabelPlacements;
2985
+ this.mergedNetLabelPlacements = solverInput.mergedNetLabelPlacements;
2757
2986
  this.mergedLabelNetIdMap = solverInput.mergedLabelNetIdMap;
2758
2987
  this.allTraces = [...solverInput.traces];
2988
+ this.detourCounts = solverInput.detourCounts;
2759
2989
  }
2760
2990
  _step() {
2991
+ this.currentlyProcessingOverlap = null;
2992
+ this.decomposedChildLabels = null;
2761
2993
  if (this.activeSubSolver) {
2762
2994
  this.activeSubSolver.step();
2763
2995
  if (this.activeSubSolver.solved) {
@@ -2777,18 +3009,13 @@ var OverlapAvoidanceStepSolver = class extends BaseSolver {
2777
3009
  const overlapId = `${this.activeSubSolver.initialTrace.mspPairId}-${this.activeSubSolver.label.globalConnNetId}`;
2778
3010
  this.recentlyFailed.add(overlapId);
2779
3011
  this.activeSubSolver = null;
3012
+ } else {
2780
3013
  }
2781
3014
  return;
2782
3015
  }
2783
- const overlaps = detectTraceLabelOverlap(
2784
- this.allTraces,
2785
- this.netLabelPlacements
2786
- ).filter((o) => {
2787
- const originalNetIds = this.mergedLabelNetIdMap[o.label.globalConnNetId];
2788
- if (originalNetIds) {
2789
- return !originalNetIds.has(o.trace.globalConnNetId);
2790
- }
2791
- return o.trace.globalConnNetId !== o.label.globalConnNetId;
3016
+ const overlaps = detectTraceLabelOverlap({
3017
+ traces: this.allTraces,
3018
+ netLabels: this.mergedNetLabelPlacements
2792
3019
  });
2793
3020
  if (overlaps.length === 0) {
2794
3021
  this.solved = true;
@@ -2804,28 +3031,100 @@ var OverlapAvoidanceStepSolver = class extends BaseSolver {
2804
3031
  }
2805
3032
  this.overlapQueue = nonFailedOverlaps;
2806
3033
  const nextOverlap = this.overlapQueue.shift();
3034
+ this.currentlyProcessingOverlap = nextOverlap ?? null;
2807
3035
  if (nextOverlap) {
2808
3036
  const traceToFix = this.allTraces.find(
2809
3037
  (t) => t.mspPairId === nextOverlap.trace.mspPairId
2810
3038
  );
2811
- if (traceToFix) {
2812
- const labelId = nextOverlap.label.globalConnNetId;
2813
- const detourCount = this.detourCountByLabel[labelId] || 0;
2814
- this.detourCountByLabel[labelId] = detourCount + 1;
2815
- this.activeSubSolver = new SingleOverlapSolver({
2816
- trace: traceToFix,
2817
- label: nextOverlap.label,
2818
- problem: this.inputProblem,
2819
- paddingBuffer: this.PADDING_BUFFER,
2820
- detourCount
2821
- });
3039
+ const labelToAvoid = nextOverlap.label;
3040
+ const originalNetIds = this.mergedLabelNetIdMap[labelToAvoid.globalConnNetId];
3041
+ const isSelfOverlap = originalNetIds?.has(traceToFix.globalConnNetId);
3042
+ if (isSelfOverlap) {
3043
+ const childLabels = this.initialNetLabelPlacements.filter(
3044
+ (l) => originalNetIds.has(l.globalConnNetId)
3045
+ );
3046
+ this.decomposedChildLabels = childLabels;
3047
+ let actualOverlapLabel = null;
3048
+ for (const childLabel of childLabels) {
3049
+ const overlapsWithChild = detectTraceLabelOverlap({
3050
+ traces: [traceToFix],
3051
+ netLabels: [childLabel]
3052
+ });
3053
+ if (overlapsWithChild.length > 0) {
3054
+ actualOverlapLabel = childLabel;
3055
+ break;
3056
+ }
3057
+ }
3058
+ if (actualOverlapLabel) {
3059
+ const detourCount2 = this.detourCounts.get(actualOverlapLabel.globalConnNetId) ?? 0;
3060
+ this.detourCounts.set(
3061
+ actualOverlapLabel.globalConnNetId,
3062
+ detourCount2 + 1
3063
+ );
3064
+ this.activeSubSolver = new SingleOverlapSolver({
3065
+ trace: traceToFix,
3066
+ label: actualOverlapLabel,
3067
+ problem: this.inputProblem,
3068
+ paddingBuffer: this.PADDING_BUFFER,
3069
+ detourCount: detourCount2
3070
+ });
3071
+ } else {
3072
+ const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`;
3073
+ this.recentlyFailed.add(overlapId);
3074
+ }
3075
+ return;
3076
+ }
3077
+ if (originalNetIds && doesTraceStartOrEndInLabel({ trace: traceToFix, label: labelToAvoid })) {
3078
+ const childLabels = this.initialNetLabelPlacements.filter(
3079
+ (l) => originalNetIds.has(l.globalConnNetId)
3080
+ );
3081
+ this.decomposedChildLabels = childLabels;
3082
+ let actualOverlapLabel = null;
3083
+ for (const childLabel of childLabels) {
3084
+ const overlapsWithChild = detectTraceLabelOverlap({
3085
+ traces: [traceToFix],
3086
+ netLabels: [childLabel]
3087
+ });
3088
+ if (overlapsWithChild.length > 0) {
3089
+ actualOverlapLabel = childLabel;
3090
+ break;
3091
+ }
3092
+ }
3093
+ if (actualOverlapLabel) {
3094
+ const detourCount2 = this.detourCounts.get(actualOverlapLabel.globalConnNetId) ?? 0;
3095
+ this.detourCounts.set(
3096
+ actualOverlapLabel.globalConnNetId,
3097
+ detourCount2 + 1
3098
+ );
3099
+ this.activeSubSolver = new SingleOverlapSolver({
3100
+ trace: traceToFix,
3101
+ label: actualOverlapLabel,
3102
+ problem: this.inputProblem,
3103
+ paddingBuffer: this.PADDING_BUFFER,
3104
+ detourCount: detourCount2
3105
+ });
3106
+ } else {
3107
+ const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`;
3108
+ this.recentlyFailed.add(overlapId);
3109
+ }
3110
+ return;
2822
3111
  }
3112
+ const detourCount = this.detourCounts.get(labelToAvoid.globalConnNetId) ?? 0;
3113
+ this.detourCounts.set(labelToAvoid.globalConnNetId, detourCount + 1);
3114
+ this.activeSubSolver = new SingleOverlapSolver({
3115
+ trace: traceToFix,
3116
+ label: labelToAvoid,
3117
+ problem: this.inputProblem,
3118
+ paddingBuffer: this.PADDING_BUFFER,
3119
+ detourCount
3120
+ });
2823
3121
  }
2824
3122
  }
2825
3123
  getOutput() {
2826
3124
  return {
2827
3125
  allTraces: this.allTraces,
2828
- modifiedTraces: this.modifiedTraces
3126
+ modifiedTraces: this.modifiedTraces,
3127
+ detourCounts: this.detourCounts
2829
3128
  };
2830
3129
  }
2831
3130
  visualize() {
@@ -2840,6 +3139,37 @@ var OverlapAvoidanceStepSolver = class extends BaseSolver {
2840
3139
  strokeColor: "purple"
2841
3140
  });
2842
3141
  }
3142
+ if (this.currentlyProcessingOverlap) {
3143
+ const { trace, label } = this.currentlyProcessingOverlap;
3144
+ graphics.lines.push({
3145
+ points: trace.tracePath,
3146
+ strokeColor: "red"
3147
+ });
3148
+ if (this.decomposedChildLabels) {
3149
+ visualizeDecomposition({
3150
+ decomposedChildLabels: this.decomposedChildLabels,
3151
+ collidingTrace: trace,
3152
+ mergedLabel: label,
3153
+ graphics
3154
+ });
3155
+ } else {
3156
+ if (!graphics.rects) graphics.rects = [];
3157
+ graphics.rects.push({
3158
+ center: label.center,
3159
+ width: label.width,
3160
+ height: label.height,
3161
+ fill: "yellow"
3162
+ });
3163
+ if (!graphics.texts) graphics.texts = [];
3164
+ graphics.texts.push({
3165
+ x: label.center.x,
3166
+ y: label.center.y + label.height / 2 + 0.5,
3167
+ text: `COLLISION: Trace ${trace.mspPairId} vs Label ${label.globalConnNetId}`,
3168
+ fontSize: 0.3,
3169
+ color: "red"
3170
+ });
3171
+ }
3172
+ }
2843
3173
  return graphics;
2844
3174
  }
2845
3175
  };
@@ -2847,74 +3177,101 @@ var OverlapAvoidanceStepSolver = class extends BaseSolver {
2847
3177
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts
2848
3178
  var TraceLabelOverlapAvoidanceSolver = class extends BaseSolver {
2849
3179
  inputProblem;
2850
- traces;
2851
3180
  netLabelPlacements;
2852
- // sub-solver instances
3181
+ unprocessedTraces = [];
3182
+ cleanTraces = [];
3183
+ subSolvers = [];
3184
+ phase = "searching_for_overlaps";
3185
+ detourCounts = /* @__PURE__ */ new Map();
2853
3186
  labelMergingSolver;
2854
- overlapAvoidanceSolver;
2855
- pipelineStepIndex = 0;
2856
3187
  constructor(solverInput) {
2857
3188
  super();
2858
3189
  this.inputProblem = solverInput.inputProblem;
2859
- this.traces = solverInput.traces;
3190
+ this.unprocessedTraces = [...solverInput.traces];
2860
3191
  this.netLabelPlacements = solverInput.netLabelPlacements;
3192
+ this.cleanTraces = [];
3193
+ this.subSolvers = [];
2861
3194
  }
2862
3195
  _step() {
2863
- if (this.activeSubSolver) {
2864
- this.activeSubSolver.step();
2865
- if (this.activeSubSolver.solved) {
2866
- this.activeSubSolver = null;
2867
- this.pipelineStepIndex++;
2868
- } else if (this.activeSubSolver.failed) {
2869
- this.failed = true;
2870
- this.activeSubSolver = null;
3196
+ if (this.phase === "searching_for_overlaps") {
3197
+ if (this.unprocessedTraces.length === 0) {
3198
+ console.log(
3199
+ `Dispatch phase complete. Created ${this.subSolvers.length} sub-solvers.`
3200
+ );
3201
+ this.phase = "fixing_overlaps";
3202
+ return;
2871
3203
  }
2872
- return;
2873
- }
2874
- switch (this.pipelineStepIndex) {
2875
- case 0:
2876
- this.labelMergingSolver = new MergedNetLabelObstacleSolver({
2877
- netLabelPlacements: this.netLabelPlacements,
3204
+ const currentTargetTrace = this.unprocessedTraces.shift();
3205
+ const localOverlaps = detectTraceLabelOverlap({
3206
+ traces: [currentTargetTrace],
3207
+ netLabels: this.netLabelPlacements
3208
+ });
3209
+ if (localOverlaps.length === 0) {
3210
+ this.cleanTraces.push(currentTargetTrace);
3211
+ } else {
3212
+ const collidingLabels = localOverlaps.map((o) => o.label);
3213
+ const labelMerger = new MergedNetLabelObstacleSolver({
3214
+ netLabelPlacements: collidingLabels,
2878
3215
  inputProblem: this.inputProblem,
2879
- traces: this.traces
3216
+ traces: [currentTargetTrace]
2880
3217
  });
2881
- this.activeSubSolver = this.labelMergingSolver;
2882
- break;
2883
- case 1:
2884
- this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
3218
+ labelMerger.solve();
3219
+ const mergingOutput = labelMerger.getOutput();
3220
+ const subSolver = new OverlapAvoidanceStepSolver({
2885
3221
  inputProblem: this.inputProblem,
2886
- traces: this.traces,
2887
- netLabelPlacements: this.labelMergingSolver.getOutput().netLabelPlacements,
2888
- mergedLabelNetIdMap: this.labelMergingSolver.getOutput().mergedLabelNetIdMap
3222
+ traces: [currentTargetTrace],
3223
+ initialNetLabelPlacements: this.netLabelPlacements,
3224
+ mergedNetLabelPlacements: mergingOutput.netLabelPlacements,
3225
+ mergedLabelNetIdMap: mergingOutput.mergedLabelNetIdMap,
3226
+ detourCounts: this.detourCounts
2889
3227
  });
2890
- this.activeSubSolver = this.overlapAvoidanceSolver;
2891
- break;
2892
- default:
3228
+ this.subSolvers.push(subSolver);
3229
+ }
3230
+ } else if (this.phase === "fixing_overlaps") {
3231
+ if (this.subSolvers.every((s) => s.solved || s.failed)) {
3232
+ console.log("All sub-solvers finished.");
3233
+ if (!this.labelMergingSolver) {
3234
+ this.labelMergingSolver = new MergedNetLabelObstacleSolver({
3235
+ netLabelPlacements: this.netLabelPlacements,
3236
+ inputProblem: this.inputProblem,
3237
+ traces: this.getOutput().traces
3238
+ });
3239
+ this.labelMergingSolver.solve();
3240
+ }
2893
3241
  this.solved = true;
2894
- break;
3242
+ return;
3243
+ }
3244
+ for (const solver of this.subSolvers) {
3245
+ if (!solver.solved && !solver.failed) {
3246
+ solver.step();
3247
+ }
3248
+ }
2895
3249
  }
2896
3250
  }
2897
3251
  getOutput() {
3252
+ const solvedTraces = this.subSolvers.flatMap((s) => s.getOutput().allTraces);
2898
3253
  return {
2899
- traces: this.overlapAvoidanceSolver?.getOutput().allTraces ?? this.traces,
3254
+ traces: [...this.cleanTraces, ...solvedTraces],
2900
3255
  netLabelPlacements: this.labelMergingSolver?.getOutput().netLabelPlacements ?? this.netLabelPlacements
2901
3256
  };
2902
3257
  }
2903
3258
  visualize() {
2904
- if (this.activeSubSolver) {
2905
- return this.activeSubSolver.visualize();
2906
- }
2907
3259
  const graphics = visualizeInputProblem(this.inputProblem);
2908
3260
  if (!graphics.lines) graphics.lines = [];
2909
3261
  if (!graphics.rects) graphics.rects = [];
2910
- const output = this.getOutput();
2911
- for (const trace of output.traces) {
3262
+ for (const trace of this.cleanTraces) {
2912
3263
  graphics.lines.push({
2913
3264
  points: trace.tracePath,
2914
3265
  strokeColor: "purple"
2915
3266
  });
2916
3267
  }
2917
- for (const label of output.netLabelPlacements) {
3268
+ for (const solver of this.subSolvers) {
3269
+ const solverGraphics = solver.visualize();
3270
+ graphics.lines.push(...solverGraphics.lines ?? []);
3271
+ graphics.rects.push(...solverGraphics.rects ?? []);
3272
+ graphics.points.push(...solverGraphics.points ?? []);
3273
+ }
3274
+ for (const label of this.netLabelPlacements) {
2918
3275
  const color = getColorFromString(label.globalConnNetId, 0.3);
2919
3276
  graphics.rects.push({
2920
3277
  center: label.center,
@@ -3006,7 +3363,7 @@ function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
3006
3363
 
3007
3364
  // lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts
3008
3365
  var NEAREST_NEIGHBOR_COUNT = 3;
3009
- var distance = (p1, p2) => {
3366
+ var distance2 = (p1, p2) => {
3010
3367
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
3011
3368
  };
3012
3369
  var LongDistancePairSolver = class extends BaseSolver {
@@ -3047,7 +3404,7 @@ var LongDistancePairSolver = class extends BaseSolver {
3047
3404
  return [
3048
3405
  {
3049
3406
  pin: targetPin,
3050
- distance: distance(sourcePin, targetPin)
3407
+ distance: distance2(sourcePin, targetPin)
3051
3408
  }
3052
3409
  ];
3053
3410
  }).sort((a, b) => a.distance - b.distance).slice(0, NEAREST_NEIGHBOR_COUNT);