diff --git a/algos/infinite-grid-ijump-astar/v2/index.ts b/algos/infinite-grid-ijump-astar/v2/index.ts index 9cc4092..4b0aed6 100644 --- a/algos/infinite-grid-ijump-astar/v2/index.ts +++ b/algos/infinite-grid-ijump-astar/v2/index.ts @@ -32,6 +32,7 @@ export function autorouteMultiMargin( const autorouter = new IJumpMultiMarginAutorouter({ input, + isRemovePathLoopsEnabled: true, }) const solution = autorouter.solveAndMapToTraces() diff --git a/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts b/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts index edaa34b..3e79fd4 100644 --- a/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts +++ b/algos/infinite-grid-ijump-astar/v2/lib/GeneralizedAstar.ts @@ -12,6 +12,7 @@ import type { } from "autorouting-dataset" import { getObstaclesFromRoute } from "autorouting-dataset/lib/solver-utils/getObstaclesFromRoute" import { ObstacleList } from "./ObstacleList" +import { removePathLoops } from "autorouting-dataset/lib/solver-postprocessing/remove-path-loops" const debug = Debug("autorouting-dataset:astar") @@ -40,7 +41,7 @@ export class GeneralizedAstarAutorouter { GRID_STEP: number OBSTACLE_MARGIN: number MAX_ITERATIONS: number - + isRemovePathLoopsEnabled: boolean /** * Setting this greater than 1 makes the algorithm find suboptimal paths and * act more greedy, but at greatly improves performance. @@ -58,6 +59,7 @@ export class GeneralizedAstarAutorouter { GRID_STEP?: number OBSTACLE_MARGIN?: number MAX_ITERATIONS?: number + isRemovePathLoopsEnabled?: boolean debug?: boolean }) { this.input = opts.input @@ -68,6 +70,7 @@ export class GeneralizedAstarAutorouter { this.OBSTACLE_MARGIN = opts.OBSTACLE_MARGIN ?? 0.15 this.MAX_ITERATIONS = opts.MAX_ITERATIONS ?? 100 this.debug = opts.debug ?? debug.enabled + this.isRemovePathLoopsEnabled = opts.isRemovePathLoopsEnabled ?? false if (this.debug) { debug.enabled = true } @@ -197,7 +200,7 @@ export class GeneralizedAstarAutorouter { const { solved, current } = this.solveOneStep() if (solved) { - const route: PointWithLayer[] = [] + let route: PointWithLayer[] = [] let node: Node | null = current while (node) { route.unshift({ @@ -213,6 +216,10 @@ export class GeneralizedAstarAutorouter { this.debugMessage += `t${this.debugTraceCount}: ${this.iterations} iterations\n` } + if (this.isRemovePathLoopsEnabled) { + route = removePathLoops(route) + } + return { solved: true, route, connectionName: connection.name } } diff --git a/algos/infinite-grid-ijump-astar/v2/lib/IJumpMultiMarginAutorouter.ts b/algos/infinite-grid-ijump-astar/v2/lib/IJumpMultiMarginAutorouter.ts index 5f0fb24..831a50d 100644 --- a/algos/infinite-grid-ijump-astar/v2/lib/IJumpMultiMarginAutorouter.ts +++ b/algos/infinite-grid-ijump-astar/v2/lib/IJumpMultiMarginAutorouter.ts @@ -34,11 +34,6 @@ export class IJumpMultiMarginAutorouter extends GeneralizedAstarAutorouter { enterCost: 0, travelCostFactor: 1, }, - // { - // margin: 0.25, - // enterCost: 5, - // travelCostFactor: 1.5, - // }, { margin: 0.15, enterCost: 10, diff --git a/module/lib/solver-postprocessing/remove-path-loops.ts b/module/lib/solver-postprocessing/remove-path-loops.ts new file mode 100644 index 0000000..5358ee9 --- /dev/null +++ b/module/lib/solver-postprocessing/remove-path-loops.ts @@ -0,0 +1,95 @@ +interface Point { + x: number + y: number +} + +export function removePathLoops(path: T[]): T[] { + if (path.length < 4) return path // No loops possible with less than 4 points + + const result: Point[] = [path[0]] + + for (let i = 1; i < path.length; i++) { + const currentSegment = { start: path[i - 1], end: path[i] } + + let intersectionFound = false + let intersectionPoint: Point | null = null + let intersectionIndex = -1 + + // Check for intersections with all previous segments + for (let j = 0; j < result.length - 1; j++) { + const previousSegment = { start: result[j], end: result[j + 1] } + const intersection = findIntersection(previousSegment, currentSegment) + + if (intersection) { + intersectionFound = true + intersectionPoint = intersection + intersectionIndex = j + break + } + } + + if (intersectionFound && intersectionPoint) { + // Remove the loop + result.splice(intersectionIndex + 1) + // Add the intersection point + result.push(intersectionPoint) + } + + // Add the current point if it's not the same as the last point in result + const lastPoint = result[result.length - 1] + if (lastPoint.x !== path[i].x || lastPoint.y !== path[i].y) { + result.push(path[i]) + } + } + + return result as T[] +} + +function findIntersection( + segment1: { start: Point; end: Point }, + segment2: { start: Point; end: Point }, +): Point | null { + // Check if segments are parallel + if ( + (segment1.start.x === segment1.end.x && + segment2.start.x === segment2.end.x) || + (segment1.start.y === segment1.end.y && segment2.start.y === segment2.end.y) + ) { + return null + } + + // Find intersection point + let intersectionPoint: Point + + if (segment1.start.x === segment1.end.x) { + const x = segment1.start.x + const y = segment2.start.y + intersectionPoint = { ...segment1.start, x, y } + } else { + const x = segment2.start.x + const y = segment1.start.y + intersectionPoint = { ...segment1.start, x, y } + } + + // Check if intersection point is within both segments + if ( + isPointInSegment(intersectionPoint, segment1) && + isPointInSegment(intersectionPoint, segment2) + ) { + return intersectionPoint + } + + return null +} + +function isPointInSegment( + point: Point, + segment: { start: Point; end: Point }, +): boolean { + return ( + point.x >= Math.min(segment.start.x, segment.end.x) && + point.x <= Math.max(segment.start.x, segment.end.x) && + point.y >= Math.min(segment.start.y, segment.end.y) && + point.y <= Math.max(segment.start.y, segment.end.y) + ) +} diff --git a/module/tests/solver-postprocessing/remove-path-loops.test.ts b/module/tests/solver-postprocessing/remove-path-loops.test.ts new file mode 100644 index 0000000..97eb916 --- /dev/null +++ b/module/tests/solver-postprocessing/remove-path-loops.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from "bun:test" +import { removePathLoops } from "../../lib/solver-postprocessing/remove-path-loops" +import type { Point } from "autorouting-dataset/lib/types" + +test("removePathLoops removes loops from path", () => { + const path: Point[] = [ + { x: 0, y: 0 }, + { x: 0, y: 5 }, + { x: -3, y: 5 }, + { x: -3, y: 3 }, + { x: 3, y: 3 }, + { x: 3, y: 6 }, + ] + + const shortenedPath = removePathLoops(path) + + expect(shortenedPath).toEqual([ + { x: 0, y: 0 }, + { x: 0, y: 3 }, + { x: 3, y: 3 }, + { x: 3, y: 6 }, + ]) +})