Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
if (overlapsHorizontalConstraintMismatch) return true
}

if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false

// Even a correctly oriented label may need a connector to clear a neighbor;
// later collision resolution must not rotate it outside its constraints.
const bounds = getRectBounds(label.center, label.width, label.height)
return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
if (otherIndex === labelIndex) return false
Expand Down Expand Up @@ -578,11 +578,88 @@ export class AvailableNetOrientationSolver extends BaseSolver {
)
if (shiftedCandidate) return shiftedCandidate

return this.findValidLateralShiftedCandidate(
const lateralCandidate = this.findValidLateralShiftedCandidate(
label,
orientations[0]!,
labelIndex,
)
if (lateralCandidate) return lateralCandidate

return this.findValidBranchFromHostTrace(
label,
orientations[0]!,
labelIndex,
)
}

/**
* Searches connector source points in schematic world coordinates (mm,
* +X right, +Y up). Orientation vectors are directions in the same frame.
*/
private findValidBranchFromHostTrace(
label: NetLabelPlacement,
orientation: FacingDirection,
labelIndex: number,
): EvaluatedCandidate | null {
const direction = dir(orientation)
const perpendicular = { x: -direction.y, y: direction.x }
const maxDistance = this.getSearchDistanceLimit(label, orientation)
const sources: Point[] = []
for (const traceId of label.mspConnectionPairIds) {
const trace = this.traceMap[traceId]
if (!trace) continue
for (let i = 0; i < trace.tracePath.length - 1; i++) {
const start = trace.tracePath[i]!
const end = trace.tracePath[i + 1]!
const steps = Math.max(
1,
Math.ceil(
Math.hypot(end.x - start.x, end.y - start.y) / LABEL_SEARCH_STEP,
),
)
for (let step = 0; step <= steps; step++) {
sources.push({
x: start.x + ((end.x - start.x) * step) / steps,
y: start.y + ((end.y - start.y) * step) / steps,
})
}
}
}
sources.sort(
(a, b) =>
Math.hypot(a.x - label.anchorPoint.x, a.y - label.anchorPoint.y) -
Math.hypot(b.x - label.anchorPoint.x, b.y - label.anchorPoint.y),
)
// The original anchor can be trapped between labels. Search short branches
// from the existing wire while preserving both connectivity and orientation.
for (const connectorSource of sources) {
for (
let offset = 0;
offset <= maxDistance + EPS;
offset += LABEL_SEARCH_STEP
) {
for (const sign of offset === 0 ? [1] : [-1, 1]) {
const candidate = this.findValidCandidateInShiftColumn({
label,
labelIndex,
orientation,
direction,
baseAnchor: {
x: connectorSource.x + perpendicular.x * offset * sign,
y: connectorSource.y + perpendicular.y * offset * sign,
},
connectorSource,
maxSearchDistance: maxDistance,
outwardDistance: offset * sign,
phase: "lateral-shift",
startDistance: WICK_CLEARANCE,
stopOnTraceCollision: false,
})
if (candidate) return candidate
}
}
}
return null
}

private findValidCrowdedTopVerticalFanoutCandidate(
Expand Down Expand Up @@ -1519,14 +1596,15 @@ export class AvailableNetOrientationSolver extends BaseSolver {
)
}
if (orientation === "x-") {
// Labels outside the chip still need room for a short horizontal wick.
return Math.max(
0,
this.getSearchDistanceLimit(label, orientation),
...labelChips.map((chip) => baseAnchor.x - chip.bounds.minX),
)
}
if (orientation === "x+") {
return Math.max(
0,
this.getSearchDistanceLimit(label, orientation),
...labelChips.map((chip) => chip.bounds.maxX - baseAnchor.x),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatia
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
import { getColorFromString } from "lib/utils/getColorFromString"
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
import { getOrientationConstraint } from "lib/utils/getOrientationConstraint"

type CandidateStatus =
| "ok"
Expand Down Expand Up @@ -208,6 +209,11 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
const netLabelWidth = this.netLabelWidthOf(label)
const netLabelHeight = this.netLabelHeightOf(label)
const candidates: Candidate[] = []
const alongTraceCandidates: Candidate[] = []
const orientationConstraint = getOrientationConstraint(
this.inputProblem,
label,
)

const buildCandidate = (
orientation: FacingDirection,
Expand Down Expand Up @@ -295,12 +301,24 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
),
)
}
// A constrained label can also extend past a trace endpoint. Keep
// its host segment in collision checks so it cannot cover the wire.
for (const orientation of orientationConstraint ?? []) {
if (perpendicularOrientations.includes(orientation)) continue
alongTraceCandidates.push(
buildCandidate(orientation, anchor, mspPairId),
)
}
}
}
}
}

return candidates
return orientationConstraint === null
? candidates
: [...candidates, ...alongTraceCandidates].filter((candidate) =>
orientationConstraint.includes(candidate.orientation),
)
}

private checkCandidate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ export const generateRerouteCandidates = ({
label,
paddingBuffer,
detourCount,
includeCornerDetours = false,
}: {
trace: SolvedTracePath
label: NetLabelPlacement
problem: InputProblem
paddingBuffer: number
detourCount: number
includeCornerDetours?: boolean
}): Point[][] => {
const initialTrace = { ...trace, tracePath: simplifyPath(trace.tracePath) }

Expand Down Expand Up @@ -73,6 +75,7 @@ export const generateRerouteCandidates = ({
initialTrace,
firstInsideIndex,
lastInsideIndex,
includeCornerDetours,
labelBounds,
paddingBuffer,
detourCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSol
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
import { getCombinedLabelObstacle } from "./getCombinedLabelObstacle"
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
import { pathEntersAnyNetLabel } from "lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel"

interface SingleOverlapSolverInput {
trace: SolvedTracePath
Expand Down Expand Up @@ -64,50 +67,116 @@ export class SingleOverlapSolver extends BaseSolver {
this.netLabelPlacements = solverInput.netLabelPlacements ?? [
solverInput.label,
]
// Attached labels move with this trace. Protect the interiors of other
// same-net labels that the original route already clears.
const sameNetLabelsToAvoid = this.netLabelPlacements.filter(
(label) =>
label.globalConnNetId === this.initialTrace.globalConnNetId &&
!label.mspConnectionPairIds.includes(this.initialTrace.mspPairId) &&
!tracePathContainsPoint(
this.initialTrace.tracePath,
label.anchorPoint,
) &&
!pathEntersAnyNetLabel({
path: this.initialTrace.tracePath,
netLabelPlacements: [label],
}),
)
this.obstacles = getObstacleRects(this.problem)

// Calculate an effective padding for this specific run based on the detourCount.
const effectivePadding =
solverInput.paddingBuffer +
solverInput.detourCount * solverInput.paddingBuffer

const candidates = generateRerouteCandidates({
let candidates = generateRerouteCandidates({
...solverInput,
paddingBuffer: effectivePadding, // Use the calculated, larger padding
})

// A full-segment shift can hit a remote label on the same net. In that
// case, also try a local turn around the target label's corner.
if (
candidates.some((path) =>
pathEntersAnyNetLabel({
path,
netLabelPlacements: sameNetLabelsToAvoid,
}),
)
) {
candidates = generateRerouteCandidates({
...solverInput,
paddingBuffer: effectivePadding,
includeCornerDetours: true,
})
}

// If local detours cannot clear a neighboring same-net label, also
// generate routes around both label bodies. Merely rejecting those
// detours can leave the trace crossing the original target label.
const blockingLabels = sameNetLabelsToAvoid.filter((label) =>
candidates.some((path) =>
pathEntersAnyNetLabel({ path, netLabelPlacements: [label] }),
),
)
let fallbackCandidates: Point[][] = []
if (blockingLabels.length > 0) {
fallbackCandidates = generateRerouteCandidates({
...solverInput,
label: getCombinedLabelObstacle(this.label, blockingLabels),
paddingBuffer: effectivePadding,
includeCornerDetours: true,
})
}

const getLabelOverlapCount = (path: Point[]) =>
detectTraceLabelOverlap({
traces: [{ ...this.initialTrace, tracePath: path }],
netLabels: this.netLabelPlacements,
}).length

const candidateByPath = new Map<string, Point[]>()
for (const candidate of candidates) {
const simplifiedCandidate = simplifyPath(candidate)
candidateByPath.set(
simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"),
simplifiedCandidate,
)
}

this.queuedCandidatePaths = [...candidateByPath.values()].sort((a, b) => {
const pathLengthDifference = getPathLength(a) - getPathLength(b)
if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) {
return pathLengthDifference
const getQueuedCandidates = (paths: Point[][]) => {
const candidateByPath = new Map<string, Point[]>()
for (const candidate of paths) {
const simplifiedCandidate = simplifyPath(candidate)
if (
pathEntersAnyNetLabel({
path: simplifiedCandidate,
netLabelPlacements: sameNetLabelsToAvoid,
})
)
continue
candidateByPath.set(
simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"),
simplifiedCandidate,
)
}

const overlapCountDifference =
getLabelOverlapCount(a) - getLabelOverlapCount(b)
if (overlapCountDifference !== 0) return overlapCountDifference
return [...candidateByPath.values()].sort((a, b) => {
const pathLengthDifference = getPathLength(a) - getPathLength(b)
if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) {
return pathLengthDifference
}

return a.length - b.length
})
const overlapCountDifference =
getLabelOverlapCount(a) - getLabelOverlapCount(b)
if (overlapCountDifference !== 0) return overlapCountDifference

return a.length - b.length
})
}
// Preserve the normal search first, then exhaust the finite set of wider
// detours. Applying the five-candidate limit to both groups would leave
// those fallback routes untried in crowded power-label layouts.
this.queuedCandidatePaths = [
...getQueuedCandidates(candidates).slice(0, MAX_TRIES),
...getQueuedCandidates(fallbackCandidates),
]
}

override _step() {
// Failure conditions: no more candidates or exceeded max tries
if (this.queuedCandidatePaths.length === 0 || this._tried >= MAX_TRIES) {
// Both the local routes and any wider fallback routes have been tried.
if (this.queuedCandidatePaths.length === 0) {
this.failed = true
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"

/**
* Builds a temporary obstacle for detour generation without moving any label.
* Centers and bounds are points in schematic-world millimeters, with +X right
* and +Y up in the right-handed XY plane. Width and height are axis extents.
*/
export const getCombinedLabelObstacle = (
targetLabel: NetLabelPlacement,
blockingLabels: NetLabelPlacement[],
): NetLabelPlacement => {
const bounds = [targetLabel, ...blockingLabels].map((label) =>
getRectBounds(label.center, label.width, label.height),
)
const minX = Math.min(...bounds.map((labelBounds) => labelBounds.minX))
const maxX = Math.max(...bounds.map((labelBounds) => labelBounds.maxX))
const minY = Math.min(...bounds.map((labelBounds) => labelBounds.minY))
const maxY = Math.max(...bounds.map((labelBounds) => labelBounds.maxY))
return {
...targetLabel,
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
width: maxX - minX,
height: maxY - minY,
}
}
Loading
Loading