Skip to content

Commit

Permalink
Merge pull request #6 from tscircuit/tracehints
Browse files Browse the repository at this point in the history
Trace Hint Implementation (single layer)
  • Loading branch information
seveibar authored Aug 25, 2024
2 parents f2d42ac + 7f49c95 commit 6b5961b
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 13 deletions.
Binary file modified bun.lockb
Binary file not shown.
8 changes: 8 additions & 0 deletions lib/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,12 @@ export class Project {
computeGlobalPcbTransform(): Matrix {
return identity()
}

selectAll(selector: string): PrimitiveComponent[] {
return this.rootComponent?.selectAll(selector) ?? []
}

selectOne(selector: string): PrimitiveComponent | null {
return this.rootComponent?.selectOne(selector) ?? null
}
}
11 changes: 11 additions & 0 deletions lib/components/base-components/PrimitiveComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ export abstract class PrimitiveComponent<
return this.selectAll(selector)[0] ?? null
}

getAvailablePcbLayers(): string[] {
if (this.isPcbPrimitive) {
if (this.props.layer) return [this.props.layer]
if (this.componentName === "PlatedHole") {
return ["top", "bottom"] // TODO derive layers from parent
}
return []
}
return []
}

getDescendants(): PrimitiveComponent[] {
const descendants: PrimitiveComponent[] = []
for (const child of this.children) {
Expand Down
5 changes: 5 additions & 0 deletions lib/components/primitive-components/Port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export class Port extends PrimitiveComponent<typeof portProps> {
return `.${this.parent?.props.name} > port.${this.props.name}`
// return `#${this.props.id}`
}
getAvailablePcbLayers(): string[] {
return Array.from(
new Set(this.matchedComponents.flatMap((c) => c.getAvailablePcbLayers())),
)
}

doInitialSourceRender(): void {
const { db } = this.project!
Expand Down
145 changes: 138 additions & 7 deletions lib/components/primitive-components/Trace.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
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 type { AnySoupElement, SchematicTrace } from "@tscircuit/soup"
import {
IJumpAutorouter,
autoroute,
getObstaclesFromSoup,
markObstaclesAsConnected,
} from "@tscircuit/infgrid-ijump-astar"
import type {
AnySoupElement,
PCBTrace,
RouteHintPoint,
SchematicTrace,
} from "@tscircuit/soup"
import type {
Obstacle,
SimpleRouteConnection,
SimpleRouteJson,
} from "lib/utils/autorouting/SimpleRouteJson"
import { computeObstacleBounds } from "lib/utils/autorouting/computeObstacleBounds"
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<typeof traceProps> {
source_trace_id: string | null = null
Expand Down Expand Up @@ -124,14 +150,119 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {

const source_trace = db.source_trace.get(this.source_trace_id!)!

const { solution } = autoroute(pcbElements.concat([source_trace]))
const hints = ports.flatMap(({ port }) =>
port.matchedComponents.filter((c) => c.componentName === "TraceHint"),
) as TraceHint[]

const pcbRouteHints = (this._parsedProps.pcbRouteHints ?? []).concat(
hints.flatMap((h) => h.getPcbRouteHints()),
)

if (ports.length > 2) {
this.renderError(
`Trace has more than two ports (${ports
.map((p) => p.port.getString())
.join(
", ",
)}), routing between more than two ports for a single trace is not implemented`,
)
return
}

// TODO for some reason, the solution gets duplicated. Seems to be an issue
// with the ijump-astar function
const pcb_trace = solution[0]
if (pcbRouteHints.length === 0) {
const { solution } = autoroute(pcbElements.concat([source_trace]))
// TODO for some reason, the solution gets duplicated inside ijump-astar
const pcb_trace = solution[0]
db.pcb_trace.insert(pcb_trace)
this.pcb_trace_id = pcb_trace.pcb_trace_id
return
}

db.pcb_trace.insert(pcb_trace)
// 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 orderedRouteObjectives: PcbRouteObjective[] = [
portToObjective(ports[0].port),
...pcbRouteHints,
portToObjective(ports[1].port),
]

// 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)
}

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
}

Expand Down
60 changes: 58 additions & 2 deletions lib/components/primitive-components/TraceHint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,60 @@
import { PrimitiveComponent } from "lib/components/base-components/PrimitiveComponent"
import { traceProps } from "@tscircuit/props"
import { traceHintProps } from "@tscircuit/props"
import type { Port } from "./Port"
import type { RouteHintPoint } from "@tscircuit/soup"
import { applyToPoint } from "transformation-matrix"

export class TraceHint extends PrimitiveComponent<typeof traceProps> {}
export class TraceHint extends PrimitiveComponent<typeof traceHintProps> {
matchedPort: Port | null = null

doInitialPortMatching(): void {
const { db } = this.project!
const { _parsedProps: props, parent } = this

if (!parent) return

if (parent.componentName === "Trace") {
this.renderError(
`Port inference inside trace is not yet supported (${this})`,
)
return
}

if (!parent) throw new Error("TraceHint has no parent")

if (!props.for) {
this.renderError(`TraceHint has no for property (${this})`)
return
}

const port = parent.selectOne(props.for, { type: "port" }) as Port

if (!port) {
this.renderError(
`${this} could not find port for selector "${props.for}"`,
)
}

this.matchedPort = port
port.registerMatch(this)
}

getPcbRouteHints(): Array<RouteHintPoint> {
const { _parsedProps: props } = this

const offsets = props.offset ? [props.offset] : props.offsets

if (!offsets) return []

const globalTransform = this.computePcbGlobalTransform()

return offsets.map(
(offset): RouteHintPoint => ({
...applyToPoint(globalTransform, offset),
via: offset.via,
to_layer: offset.to_layer,
trace_width: offset.trace_width,
}),
)
}
}
102 changes: 102 additions & 0 deletions lib/utils/autorouting/findPossibleTraceLayerCombinations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
interface Hint {
via?: boolean
optional_via?: boolean
layers?: Array<string>
}

const LAYER_SELECTION_PREFERENCE = ["top", "bottom", "inner1", "inner2"]

/**
* ORDERING OF CANDIDATES: Example:
* top -> top -> bottom -> bottom
* bottom -> bottom -> top -> top
* top -> bottom -> bottom -> top
* bottom -> top -> top -> bottom
*/
interface CandidateTraceLayerCombination {
layer_path: string[]
}

/**
* Given a set of hints that contain layers or indication there should be a via,
* this function returns all combinations of layers that could be traversed.
*
* EXAMPLE 1:
* INPUT:
* [top,bottom] -> unspecified -> unspecified/via -> [top, bottom]
* OUTPUT:
* top -> top -> bottom -> bottom
* bottom -> bottom -> top -> top *
* EXAMPLE 2:
* INPUT:
* [top,bottom] -> unspecified -> unspecified/via -> unspecified/via -> [top, bottom]
* OUTPUT:
* top -> top -> bottom -> top -> top
* bottom -> bottom -> top-> bottom -> bottom
* bottom -> bottom -> inner-1 -> inner-1 -> bottom
*/
export const findPossibleTraceLayerCombinations = (
hints: Hint[],
layer_path: string[] = [],
): CandidateTraceLayerCombination[] => {
const candidates: CandidateTraceLayerCombination[] = []
if (layer_path.length === 0) {
const starting_layers = hints[0].layers!
for (const layer of starting_layers) {
candidates.push(
...findPossibleTraceLayerCombinations(hints.slice(1), [layer]),
)
}
return candidates
}

if (hints.length === 0) return []
const current_hint = hints[0]
const is_possibly_via = current_hint.via || current_hint.optional_via
const last_layer = layer_path[layer_path.length - 1]

if (hints.length === 1) {
const last_hint = current_hint
if (last_hint.layers && is_possibly_via) {
return last_hint.layers.map((layer) => ({
layer_path: [...layer_path, layer],
}))
}
if (last_hint.layers?.includes(last_layer)) {
return [{ layer_path: [...layer_path, last_layer] }]
}
return []
}

if (!is_possibly_via) {
if (current_hint.layers) {
if (!current_hint.layers.includes(last_layer)) {
return []
}
}

return findPossibleTraceLayerCombinations(
hints.slice(1),
layer_path.concat([last_layer]),
)
}

const candidate_next_layers = (
current_hint.optional_via
? LAYER_SELECTION_PREFERENCE
: LAYER_SELECTION_PREFERENCE.filter((layer) => layer !== last_layer)
).filter(
(layer) => !current_hint.layers || current_hint.layers?.includes(layer),
)

for (const candidate_next_layer of candidate_next_layers) {
candidates.push(
...findPossibleTraceLayerCombinations(
hints.slice(1),
layer_path.concat(candidate_next_layer),
),
)
}

return candidates
}
Loading

0 comments on commit 6b5961b

Please sign in to comment.