diff --git a/lib/components/primitive-components/Trace.ts b/lib/components/primitive-components/Trace.ts index d4c3d5f..570ee55 100644 --- a/lib/components/primitive-components/Trace.ts +++ b/lib/components/primitive-components/Trace.ts @@ -1,7 +1,12 @@ import { traceProps } from "@tscircuit/props" import { PrimitiveComponent } from "../base-components/PrimitiveComponent" import type { Port } from "./Port" -import { IJumpAutorouter, autoroute } from "@tscircuit/infgrid-ijump-astar" +import { + IJumpAutorouter, + autoroute, + getObstaclesFromSoup, + markObstaclesAsConnected, +} from "@tscircuit/infgrid-ijump-astar" import type { AnySoupElement, PCBTrace, @@ -17,6 +22,20 @@ import { computeObstacleBounds } from "lib/utils/autorouting/computeObstacleBoun import { projectPointInDirection } from "lib/utils/projectPointInDirection" import type { TraceHint } from "./TraceHint" import { findPossibleTraceLayerCombinations } from "lib/utils/autorouting/findPossibleTraceLayerCombinations" +import { pairs } from "lib/utils/pairs" +import { mergeRoutes } from "lib/utils/autorouting/mergeRoutes" + +type PcbRouteObjective = + | RouteHintPoint + | { layers: string[]; x: number; y: number; via?: boolean } + +const portToObjective = (port: Port): PcbRouteObjective => { + const portPosition = port.getGlobalPcbPosition() + return { + ...portPosition, + layers: port.getAvailablePcbLayers(), + } +} export class Trace extends PrimitiveComponent { source_trace_id: string | null = null @@ -162,16 +181,89 @@ export class Trace extends PrimitiveComponent { // When we have hints, we have to order the hints then route between each // terminal of the trace and the hints // TODO order based on proximity to ports - const orderedHintsAndPorts: Array = [ - { layers: ports[0].port.getAvailablePcbLayers() }, + const orderedRouteObjectives: PcbRouteObjective[] = [ + portToObjective(ports[0].port), ...pcbRouteHints, - { layers: ports[1].port.getAvailablePcbLayers() }, + portToObjective(ports[1].port), ] - const candidateLayerCombinations = - findPossibleTraceLayerCombinations(orderedHintsAndPorts) + // Hints can indicate where there should be a via, but the layer is allowed + // to be unspecified, therefore we need to find possible layer combinations + // to go to each hint and still route to the start and end points + const candidateLayerCombinations = findPossibleTraceLayerCombinations( + orderedRouteObjectives, + ) + + if (candidateLayerCombinations.length === 0) { + this.renderError( + `Could not find a common layer (using hints) for trace ${this.getString()}`, + ) + } + + // Cache the PCB obstacles, they'll be needed for each segment between + // ports/hints + const obstacles = getObstaclesFromSoup(this.project!.db.toArray()) + markObstaclesAsConnected( + obstacles, + orderedRouteObjectives, + this.source_trace_id!, + ) + + // TODO explore all candidate layer combinations if one fails + const candidateLayerSelections = candidateLayerCombinations[0].layer_path + + /** + * Apply the candidate layer selections to the route objectives, now we + * have a set of points that have definite layers + */ + const orderedRoutePoints = orderedRouteObjectives.map((t, idx) => { + if (t.via) { + return { + ...t, + via_to_layer: candidateLayerSelections[idx], + } + } + return { ...t, layers: [candidateLayerSelections[idx]] } + }) + + const routes: PCBTrace["route"][] = [] + for (const [a, b] of pairs(orderedRoutePoints)) { + const BOUNDS_MARGIN = 2 //mm + const ijump = new IJumpAutorouter({ + input: { + obstacles, + connections: [ + { + name: this.source_trace_id!, + pointsToConnect: [a, b], + }, + ], + layerCount: 1, + bounds: { + minX: Math.min(a.x, b.x) - BOUNDS_MARGIN, + maxX: Math.max(a.x, b.x) + BOUNDS_MARGIN, + minY: Math.min(a.y, b.y) - BOUNDS_MARGIN, + maxY: Math.max(a.y, b.y) + BOUNDS_MARGIN, + }, + }, + }) + const traces = ijump.solveAndMapToTraces() + if (traces.length === 0) { + this.renderError( + `Could not find a route between ${a.x}, ${a.y} and ${b.x}, ${b.y}`, + ) + return + } + // TODO ijump returns multiple traces for some reason + const [trace] = traces as PCBTrace[] + routes.push(trace.route) + } - console.log({ candidateLayerCombinations }) + const pcb_trace = db.pcb_trace.insert({ + route: mergeRoutes(routes), + source_trace_id: this.source_trace_id!, + }) + this.pcb_trace_id = pcb_trace.pcb_trace_id } doInitialSchematicTraceRender(): void { diff --git a/lib/utils/autorouting/mergeRoutes.ts b/lib/utils/autorouting/mergeRoutes.ts new file mode 100644 index 0000000..476f5cf --- /dev/null +++ b/lib/utils/autorouting/mergeRoutes.ts @@ -0,0 +1,70 @@ +import type { PCBTrace } from "@tscircuit/soup" + +function pdist(a: any, b: any) { + return Math.hypot(a.x - b.x, a.y - b.y) +} + +/** + * Merge multiple routes into a single route. + * + * If the end of the next route is closer to the end of the previous route, + * reverse the next route and append it to the previous route. + */ +export const mergeRoutes = (routes: PCBTrace["route"][]) => { + // routes = routes.filter((route) => route.length > 0) + if (routes.some((r) => r.length === 0)) { + throw new Error("Cannot merge routes with zero length") + } + // for (const route of routes) { + // console.table(route) + // } + const merged: PCBTrace["route"] = [] + // const reverse_log: boolean[] = [] + + // Determine if the first route should be reversed + const first_route_fp = routes[0][0] + const first_route_lp = routes[0][routes[0].length - 1] + + const second_route_fp = routes[1][0] + const second_route_lp = routes[1][routes[1].length - 1] + + const best_reverse_dist = Math.min( + pdist(first_route_fp, second_route_fp), + pdist(first_route_fp, second_route_lp), + ) + + const best_normal_dist = Math.min( + pdist(first_route_lp, second_route_fp), + pdist(first_route_lp, second_route_lp), + ) + + if (best_reverse_dist < best_normal_dist) { + merged.push(...routes[0].reverse()) + // reverse_log.push(true) + } else { + merged.push(...routes[0]) + // reverse_log.push(false) + } + + for (let i = 1; i < routes.length; i++) { + const last_merged_point = merged[merged.length - 1] + const next_route = routes[i] + + const next_first_point = next_route[0] + const next_last_point = next_route[next_route.length - 1] + + const distance_to_first = pdist(last_merged_point, next_first_point) + const distance_to_last = pdist(last_merged_point, next_last_point) + + if (distance_to_first < distance_to_last) { + // reverse_log.push(false) + merged.push(...next_route) + } else { + // reverse_log.push(true) + merged.push(...next_route.reverse()) + } + } + // console.log(reverse_log) + // console.table(merged) + return merged +} diff --git a/lib/utils/pairs.ts b/lib/utils/pairs.ts new file mode 100644 index 0000000..5fefa65 --- /dev/null +++ b/lib/utils/pairs.ts @@ -0,0 +1,10 @@ +/** + * Return pairs of adjacent elements in an array. + */ +export function pairs(arr: Array): Array<[T, T]> { + const result: Array<[T, T]> = [] + for (let i = 0; i < arr.length - 1; i++) { + result.push([arr[i], arr[i + 1]]) + } + return result +} diff --git a/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg b/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg index b7ee15a..3e91265 100644 --- a/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg +++ b/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg @@ -24,6 +24,7 @@ + \ No newline at end of file