diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index 11abc150e..4e7138822 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -20,6 +20,7 @@ import { isYOrientation, rangesOverlap, rectsOverlap, + simplifyOrthogonalPath, traceCrossesBoundsInterior, tracePathCrossesAnyBounds, tracePathCrossesAnyTrace, @@ -127,7 +128,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { private applyCandidate( label: NetLabelPlacement, - candidate: CandidateLabel, + candidate: EvaluatedCandidate, labelIndex: number, ) { this.outputNetLabelPlacements[labelIndex] = { @@ -139,14 +140,33 @@ export class AvailableNetOrientationSolver extends BaseSolver { private addConnectorTrace( label: NetLabelPlacement, - candidate: CandidateLabel, + candidate: EvaluatedCandidate, labelIndex: number, ) { - const tracePath = getConnectorTracePath( - label.anchorPoint, - candidate.anchorPoint, - candidate.orientation, - ) + let tracePath: Point[] + + if (candidate.phase === "lateral-shift") { + const orientDir = dir(candidate.orientation) + const kickedSource = { + x: label.anchorPoint.x - orientDir.x * LABEL_SEARCH_STEP, + y: label.anchorPoint.y - orientDir.y * LABEL_SEARCH_STEP, + } + tracePath = simplifyOrthogonalPath([ + label.anchorPoint, + ...getConnectorTracePath( + kickedSource, + candidate.anchorPoint, + candidate.orientation, + ), + ]) + } else { + tracePath = getConnectorTracePath( + label.anchorPoint, + candidate.anchorPoint, + candidate.orientation, + ) + } + if (tracePath.length < 2) return const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}` @@ -185,10 +205,19 @@ export class AvailableNetOrientationSolver extends BaseSolver { labelIndex, orientations, ) + if (rotatedCandidate) return rotatedCandidate + + const shiftedCandidate = this.findValidShiftedCandidate( + label, + orientations[0]!, + labelIndex, + ) + if (shiftedCandidate) return shiftedCandidate - return ( - rotatedCandidate ?? - this.findValidShiftedCandidate(label, orientations[0]!, labelIndex) + return this.findValidLateralShiftedCandidate( + label, + orientations[0]!, + labelIndex, ) } @@ -266,6 +295,63 @@ export class AvailableNetOrientationSolver extends BaseSolver { return null } + /** + * When all candidates fail for the current (unshifted) position, try + * shifting the label anchor laterally — x for y-orientations, y for + * x-orientations — and re-attempting the required orientation. + * + * Offsets are tried in alternating sign order: + * -1·step, +1·step, -2·step, +2·step, … + * so the nearest escape routes are tested first. + */ + private findValidLateralShiftedCandidate( + label: NetLabelPlacement, + orientation: FacingDirection, + labelIndex: number, + ): EvaluatedCandidate | null { + const direction = dir(orientation) + const initialBaseAnchor = this.getSearchStartAnchor(label, orientation) + + // Lateral axis: perpendicular to the orientation direction + const lateralDir: Point = { + x: isYOrientation(orientation) ? 1 : 0, + y: isXOrientation(orientation) ? 1 : 0, + } + + const maxSteps = Math.ceil(this.maxSearchDistance / LABEL_SEARCH_STEP) + + for (let step = 1; step <= maxSteps; step++) { + for (const sign of [-1, 1]) { + const lateralOffset = sign * step * LABEL_SEARCH_STEP + const baseAnchor = { + x: initialBaseAnchor.x + lateralDir.x * lateralOffset, + y: initialBaseAnchor.y + lateralDir.y * lateralOffset, + } + + const maxSearchDistance = this.getLateralColumnMaxDistance( + label, + orientation, + baseAnchor, + ) + + const candidate = this.findValidCandidateInShiftColumn({ + label, + labelIndex, + orientation, + direction, + baseAnchor, + maxSearchDistance, + outwardDistance: lateralOffset, + phase: "lateral-shift", + }) + + if (candidate) return candidate + } + } + + return null + } + private findValidCandidateInShiftColumn(params: { label: NetLabelPlacement labelIndex: number @@ -274,6 +360,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { baseAnchor: Point maxSearchDistance: number outwardDistance: number + phase?: CandidatePhase }) { const { label, @@ -283,6 +370,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { baseAnchor, maxSearchDistance, outwardDistance, + phase = "shift", } = params for ( @@ -299,7 +387,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { candidate, label, labelIndex, - "shift", + phase, distance, outwardDistance, ) @@ -412,6 +500,32 @@ export class AvailableNetOrientationSolver extends BaseSolver { return Math.min(this.maxSearchDistance, labelLength * 2) } + private getLateralColumnMaxDistance( + label: NetLabelPlacement, + orientation: FacingDirection, + baseAnchor: Point, + ) { + const chipId = label.pinIds + .map((pid) => this.pinMap[pid]?.chipId) + .find(Boolean) + const chip = chipId + ? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId) + : null + + if (chip) { + if (orientation === "y-") + return Math.max(0, baseAnchor.y - chip.bounds.minY) + if (orientation === "y+") + return Math.max(0, chip.bounds.maxY - baseAnchor.y) + if (orientation === "x-") + return Math.max(0, baseAnchor.x - chip.bounds.minX) + if (orientation === "x+") + return Math.max(0, chip.bounds.maxX - baseAnchor.x) + } + + return this.getSearchDistanceLimit(label, orientation) + } + private createCandidate( label: NetLabelPlacement, anchorPoint: Point, @@ -474,6 +588,12 @@ export class AvailableNetOrientationSolver extends BaseSolver { return "trace-collision" } + for (const chip of this.chipObstacleSpatialIndex.chips) { + if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) { + return "chip-collision" + } + } + for (let i = 0; i < this.outputNetLabelPlacements.length; i++) { if (i === labelIndex) continue const otherLabel = this.outputNetLabelPlacements[i]! diff --git a/lib/solvers/AvailableNetOrientationSolver/geometry.ts b/lib/solvers/AvailableNetOrientationSolver/geometry.ts index 87757e99f..929cfbea2 100644 --- a/lib/solvers/AvailableNetOrientationSolver/geometry.ts +++ b/lib/solvers/AvailableNetOrientationSolver/geometry.ts @@ -168,7 +168,7 @@ export const getConnectorTracePath = ( : [source, { x: source.x, y: target.y }, target], ) -const simplifyOrthogonalPath = (path: Point[]) => { +export const simplifyOrthogonalPath = (path: Point[]) => { const deduped = path.filter( (point, index) => index === 0 || !pointsEqual(point, path[index - 1]!), ) diff --git a/lib/solvers/AvailableNetOrientationSolver/types.ts b/lib/solvers/AvailableNetOrientationSolver/types.ts index 29433ef33..016688523 100644 --- a/lib/solvers/AvailableNetOrientationSolver/types.ts +++ b/lib/solvers/AvailableNetOrientationSolver/types.ts @@ -33,7 +33,7 @@ export type CandidateStatus = | "trace-collision" | "netlabel-collision" -export type CandidatePhase = "rotate" | "shift" +export type CandidatePhase = "rotate" | "shift" | "lateral-shift" export type EvaluatedCandidate = CandidateLabel & { status: CandidateStatus diff --git a/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts new file mode 100644 index 000000000..bfd905a3a --- /dev/null +++ b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts @@ -0,0 +1,441 @@ +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { FacingDirection } from "lib/utils/dir" +import { + getDimsForOrientation, + getCenterFromAnchor, + getRectBounds, +} from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry" +import { rectIntersectsAnyTrace } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions" +import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex" +import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +import { getColorFromString } from "lib/utils/getColorFromString" + +type CandidateStatus = + | "ok" + | "chip-collision" + | "trace-collision" + | "label-collision" + +const ANCHOR_TRACE_CLEARANCE = 1e-4 +const SEGMENT_PARALLEL_EPS = 1e-6 +const CANDIDATE_STEP = 0.1 + +const OUTWARD_DIR: Record = { + "x+": { x: 1, y: 0 }, + "x-": { x: -1, y: 0 }, + "y+": { x: 0, y: 1 }, + "y-": { x: 0, y: -1 }, +} + +const CANDIDATE_STATUS_COLOR: Record = { + ok: "green", + "label-collision": "orange", + "trace-collision": "darkorange", + "chip-collision": "red", +} + +const CANDIDATE_STATUS_FILL: Record = { + ok: "rgba(0, 200, 0, 0.25)", + "label-collision": "rgba(255, 160, 0, 0.2)", + "trace-collision": "rgba(200, 80, 0, 0.2)", + "chip-collision": "rgba(220, 0, 0, 0.15)", +} + +type Candidate = { + orientation: FacingDirection + anchor: { x: number; y: number } + center: { x: number; y: number } + width: number + height: number + bounds: { minX: number; minY: number; maxX: number; maxY: number } + hostPairId?: MspConnectionPairId + hostSegIndex?: number + status: CandidateStatus | null +} + +function boundsOverlap( + a: { minX: number; minY: number; maxX: number; maxY: number }, + b: { minX: number; minY: number; maxX: number; maxY: number }, +): boolean { + return ( + a.minX < b.maxX - 1e-9 && + a.maxX > b.minX + 1e-9 && + a.minY < b.maxY - 1e-9 && + a.maxY > b.minY + 1e-9 + ) +} + +function sampleAnchorsAlongSegment( + a: { x: number; y: number }, + b: { x: number; y: number }, +): Array<{ x: number; y: number }> { + const dx = b.x - a.x + const dy = b.y - a.y + const len = Math.sqrt(dx * dx + dy * dy) + const steps = Math.max(1, Math.round(len / CANDIDATE_STEP)) + const anchors: Array<{ x: number; y: number }> = [] + for (let k = 0; k <= steps; k++) { + const t = k / steps + anchors.push({ x: a.x + t * dx, y: a.y + t * dy }) + } + return anchors +} + +export interface NetLabelNetLabelCollisionSolverParams { + inputProblem: InputProblem + traces: SolvedTracePath[] + netLabelPlacements: NetLabelPlacement[] +} + +export class NetLabelNetLabelCollisionSolver extends BaseSolver { + inputProblem: InputProblem + traces: SolvedTracePath[] + netLabelPlacements: NetLabelPlacement[] + + outputNetLabelPlacements: NetLabelPlacement[] + + currentCollision: [NetLabelPlacement, NetLabelPlacement] | null = null + currentLabelToMove: NetLabelPlacement | null = null + candidateResults: Candidate[] = [] + + private chipIndex: ChipObstacleSpatialIndex + private traceMap: Record + private skippedCollisionKeys = new Set() + + private labelsToTry: NetLabelPlacement[] = [] + private candidateQueue: Candidate[] = [] + private candidateIndex = 0 + + constructor(params: NetLabelNetLabelCollisionSolverParams) { + super() + this.inputProblem = params.inputProblem + this.traces = params.traces + this.netLabelPlacements = params.netLabelPlacements + this.outputNetLabelPlacements = [...params.netLabelPlacements] + this.chipIndex = + params.inputProblem._chipObstacleSpatialIndex ?? + new ChipObstacleSpatialIndex(params.inputProblem.chips) + this.traceMap = Object.fromEntries( + params.traces.map((t) => [t.mspPairId, t]), + ) as Record + } + + override getConstructorParams(): ConstructorParameters< + typeof NetLabelNetLabelCollisionSolver + >[0] { + return { + inputProblem: this.inputProblem, + traces: this.traces, + netLabelPlacements: this.netLabelPlacements, + } + } + + getOutput() { + return { netLabelPlacements: this.outputNetLabelPlacements } + } + + private labelBounds(label: NetLabelPlacement) { + return getRectBounds(label.center, label.width, label.height) + } + + private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) { + return [a.globalConnNetId, b.globalConnNetId].sort().join("::") + } + + private findNextCollidingPair(): + | [NetLabelPlacement, NetLabelPlacement] + | null { + const labels = this.outputNetLabelPlacements + for (let i = 0; i < labels.length; i++) { + for (let j = i + 1; j < labels.length; j++) { + const a = labels[i]! + const b = labels[j]! + if (a.globalConnNetId === b.globalConnNetId) continue + if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue + if (boundsOverlap(this.labelBounds(a), this.labelBounds(b))) + return [a, b] + } + } + return null + } + + private netLabelWidthOf(label: NetLabelPlacement): number | undefined { + if (label.orientation === "x+" || label.orientation === "x-") + return label.width + return label.height + } + + private buildCandidatesForLabel(label: NetLabelPlacement): Candidate[] { + const netLabelWidth = this.netLabelWidthOf(label) + const candidates: Candidate[] = [] + + const buildCandidate = ( + orientation: FacingDirection, + anchor: { x: number; y: number }, + hostPairId?: MspConnectionPairId, + hostSegIndex?: number, + ): Candidate => { + const { width, height } = getDimsForOrientation({ + orientation, + netLabelWidth, + }) + const baseCenter = getCenterFromAnchor(anchor, orientation, width, height) + const outwardDir = OUTWARD_DIR[orientation] + const center = { + x: baseCenter.x + outwardDir.x * ANCHOR_TRACE_CLEARANCE, + y: baseCenter.y + outwardDir.y * ANCHOR_TRACE_CLEARANCE, + } + return { + orientation, + anchor, + center, + width, + height, + bounds: getRectBounds(center, width, height), + hostPairId, + hostSegIndex, + status: null, + } + } + + const isPortOnly = label.mspConnectionPairIds.length === 0 + + if (isPortOnly) { + const allOrientations: FacingDirection[] = ["x+", "x-", "y+", "y-"] + const orderedOrientations = [ + label.orientation, + ...allOrientations.filter((o) => o !== label.orientation), + ] + for (const orientation of orderedOrientations) { + candidates.push(buildCandidate(orientation, label.anchorPoint)) + } + } else { + for (const mspPairId of label.mspConnectionPairIds) { + const trace = this.traceMap[mspPairId] + if (!trace) continue + const pts = trace.tracePath + for (let si = 0; si < pts.length - 1; si++) { + const segStart = pts[si]! + const segEnd = pts[si + 1]! + const isHorizontal = + Math.abs(segStart.y - segEnd.y) < SEGMENT_PARALLEL_EPS + const isVertical = + Math.abs(segStart.x - segEnd.x) < SEGMENT_PARALLEL_EPS + if (!isHorizontal && !isVertical) continue + let perpendicularOrientations: FacingDirection[] + if (isHorizontal) { + perpendicularOrientations = ["y+", "y-"] + } else { + perpendicularOrientations = ["x+", "x-"] + } + for (const anchor of sampleAnchorsAlongSegment(segStart, segEnd)) { + for (const orientation of perpendicularOrientations) { + candidates.push( + buildCandidate( + orientation, + anchor, + mspPairId as MspConnectionPairId, + si, + ), + ) + } + } + } + } + } + + return candidates + } + + private checkCandidate( + candidate: Candidate, + movingLabelNetId: string, + obstacleLabels: NetLabelPlacement[], + ): CandidateStatus { + const { bounds, hostPairId, hostSegIndex } = candidate + + if (this.chipIndex.getChipsInBounds(bounds).length > 0) + return "chip-collision" + + if ( + rectIntersectsAnyTrace(bounds, this.traceMap, hostPairId, hostSegIndex) + .hasIntersection + ) { + return "trace-collision" + } + + for (const obstacle of obstacleLabels) { + if (obstacle.globalConnNetId === movingLabelNetId) continue + if (boundsOverlap(bounds, this.labelBounds(obstacle))) + return "label-collision" + } + + return "ok" + } + + private beginSearchForLabel(label: NetLabelPlacement) { + this.currentLabelToMove = label + this.candidateQueue = this.buildCandidatesForLabel(label) + this.candidateIndex = 0 + this.candidateResults = [] + } + + private clearActiveSearch() { + this.currentCollision = null + this.currentLabelToMove = null + this.labelsToTry = [] + this.candidateQueue = [] + this.candidateIndex = 0 + this.candidateResults = [] + } + + override _step() { + if (!this.currentCollision) { + const pair = this.findNextCollidingPair() + if (!pair) { + this.solved = true + return + } + this.currentCollision = pair + this.labelsToTry = [pair[1], pair[0]] + this.beginSearchForLabel(this.labelsToTry.shift()!) + return + } + + if (this.candidateIndex >= this.candidateQueue.length) { + if (this.labelsToTry.length > 0) { + this.beginSearchForLabel(this.labelsToTry.shift()!) + } else { + this.skippedCollisionKeys.add( + this.collisionKey(this.currentCollision[0], this.currentCollision[1]), + ) + this.clearActiveSearch() + } + return + } + + const candidate = this.candidateQueue[this.candidateIndex++]! + const [labelA, labelB] = this.currentCollision + let fixedLabel: NetLabelPlacement + if (this.currentLabelToMove === labelB) { + fixedLabel = labelA + } else { + fixedLabel = labelB + } + const obstacleLabels = [ + ...this.outputNetLabelPlacements.filter( + (l) => l !== labelA && l !== labelB, + ), + fixedLabel, + ] + + const status = this.checkCandidate( + candidate, + this.currentLabelToMove!.globalConnNetId, + obstacleLabels, + ) + candidate.status = status + this.candidateResults.push({ ...candidate }) + + if (status === "ok") { + const idx = this.outputNetLabelPlacements.indexOf( + this.currentLabelToMove!, + ) + if (idx !== -1) { + this.outputNetLabelPlacements[idx] = { + ...this.currentLabelToMove!, + orientation: candidate.orientation, + anchorPoint: candidate.anchor, + width: candidate.width, + height: candidate.height, + center: candidate.center, + } + } + this.clearActiveSearch() + } + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem) + if (!graphics.lines) graphics.lines = [] + if (!graphics.rects) graphics.rects = [] + if (!graphics.points) graphics.points = [] + + for (const trace of this.traces) { + graphics.lines.push({ + points: trace.tracePath, + strokeColor: "purple", + } as any) + } + + for (const label of this.outputNetLabelPlacements) { + const isInActiveCollision = + this.currentCollision != null && + (label === this.currentCollision[0] || + label === this.currentCollision[1]) + + let labelFill: string + let labelStroke: string + let labelText: string + let pointColor: string + if (isInActiveCollision) { + labelFill = "rgba(255, 0, 0, 0.2)" + labelStroke = "red" + labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}\n⚠ COLLIDING` + pointColor = "red" + } else { + labelFill = getColorFromString(label.globalConnNetId, 0.35) + labelStroke = getColorFromString(label.globalConnNetId, 0.9) + labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}` + pointColor = getColorFromString(label.globalConnNetId, 0.9) + } + + graphics.rects.push({ + center: label.center, + width: label.width, + height: label.height, + fill: labelFill, + strokeColor: labelStroke, + label: labelText, + } as any) + graphics.points.push({ + x: label.anchorPoint.x, + y: label.anchorPoint.y, + color: pointColor, + label: `anchorPoint\norientation: ${label.orientation}`, + } as any) + } + + const movingNetId = this.currentLabelToMove + ? this.currentLabelToMove.netId + : "?" + for (const c of this.candidateResults) { + const statusColor = CANDIDATE_STATUS_COLOR[c.status!] + const statusFill = CANDIDATE_STATUS_FILL[c.status!] + let strokeDash: string | undefined + if (c.status !== "ok") strokeDash = "4 2" + graphics.rects.push({ + center: c.center, + width: c.width, + height: c.height, + fill: statusFill, + strokeColor: statusColor, + strokeDash, + label: `candidate: ${c.status}\norientation: ${c.orientation}\nmoving: ${movingNetId}`, + } as any) + graphics.points.push({ + x: c.anchor.x, + y: c.anchor.y, + color: statusColor, + label: `candidate anchor\n${c.status}`, + } as any) + } + + return graphics + } +} diff --git a/lib/solvers/SameNetTraceConsolidationSolver/SameNetTraceConsolidationSolver.ts b/lib/solvers/SameNetTraceConsolidationSolver/SameNetTraceConsolidationSolver.ts new file mode 100644 index 000000000..3e4e75a05 --- /dev/null +++ b/lib/solvers/SameNetTraceConsolidationSolver/SameNetTraceConsolidationSolver.ts @@ -0,0 +1,475 @@ +import type { Point } from "@tscircuit/math-utils" +import { doSegmentsIntersect } from "@tscircuit/math-utils" +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { + isHorizontal, + isVertical, + segmentIntersectsRect, +} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" +import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" +import { getColorFromString } from "lib/utils/getColorFromString" + +type Axis = "horizontal" | "vertical" + +type SegmentRef = { + mspPairId: MspConnectionPairId + segmentIndex: number + axis: Axis + coord: number + min: number + max: number + length: number + stableKey: string +} + +export interface SameNetTraceConsolidationSolverInput { + inputProblem: InputProblem + inputTraces: SolvedTracePath[] + mergeDistance?: number + intervalGap?: number +} + +const DEFAULT_MERGE_DISTANCE = 0.12 +const DEFAULT_INTERVAL_GAP = 0.12 +const MAX_CONSOLIDATION_PASSES = 1000 +const EPS = 1e-6 + +const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({ + ...trace, + pins: [{ ...trace.pins[0] }, { ...trace.pins[1] }], + pinIds: [...trace.pinIds], + mspConnectionPairIds: [...trace.mspConnectionPairIds], + tracePath: trace.tracePath.map((p) => ({ ...p })), +}) + +const samePoint = (a: Point, b: Point) => + Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS + +const dedupePath = (path: Point[]): Point[] => { + const deduped: Point[] = [] + for (const point of path) { + if ( + deduped.length === 0 || + !samePoint(deduped[deduped.length - 1]!, point) + ) { + deduped.push(point) + } + } + return deduped +} + +const normalizePath = (path: Point[]) => + dedupePath(simplifyPath(dedupePath(path))) + +const intervalDistance = (a: SegmentRef, b: SegmentRef) => + Math.max(0, Math.max(a.min, b.min) - Math.min(a.max, b.max)) + +const segmentRefsCompatible = ( + a: SegmentRef, + b: SegmentRef, + mergeDistance: number, + intervalGap: number, +) => + a.axis === b.axis && + a.mspPairId !== b.mspPairId && + Math.abs(a.coord - b.coord) <= mergeDistance && + intervalDistance(a, b) <= intervalGap + +const compareSegmentRefs = (a: SegmentRef, b: SegmentRef) => + a.axis.localeCompare(b.axis) || + a.coord - b.coord || + a.min - b.min || + a.max - b.max || + a.mspPairId.localeCompare(b.mspPairId) || + a.segmentIndex - b.segmentIndex + +const chooseCanonicalSegment = (segments: SegmentRef[]) => + [...segments].sort( + (a, b) => + b.length - a.length || + a.stableKey.localeCompare(b.stableKey) || + a.segmentIndex - b.segmentIndex, + )[0]! + +const getSegmentRefs = (trace: SolvedTracePath): SegmentRef[] => { + const refs: SegmentRef[] = [] + const pts = trace.tracePath + + for (let i = 0; i < pts.length - 1; i++) { + const start = pts[i]! + const end = pts[i + 1]! + if (samePoint(start, end)) continue + + if (isHorizontal(start, end, EPS)) { + const min = Math.min(start.x, end.x) + const max = Math.max(start.x, end.x) + refs.push({ + mspPairId: trace.mspPairId, + segmentIndex: i, + axis: "horizontal", + coord: start.y, + min, + max, + length: max - min, + stableKey: `${trace.mspPairId}:${i}`, + }) + } else if (isVertical(start, end, EPS)) { + const min = Math.min(start.y, end.y) + const max = Math.max(start.y, end.y) + refs.push({ + mspPairId: trace.mspPairId, + segmentIndex: i, + axis: "vertical", + coord: start.x, + min, + max, + length: max - min, + stableKey: `${trace.mspPairId}:${i}`, + }) + } + } + + return refs +} + +const clusterSegments = ( + refs: SegmentRef[], + mergeDistance: number, + intervalGap: number, +) => { + const parent = refs.map((_, index) => index) + const find = (index: number): number => { + while (parent[index] !== index) { + parent[index] = parent[parent[index]!]! + index = parent[index]! + } + return index + } + const union = (a: number, b: number) => { + const rootA = find(a) + const rootB = find(b) + if (rootA !== rootB) parent[rootB] = rootA + } + + for (let i = 0; i < refs.length; i++) { + for (let j = i + 1; j < refs.length; j++) { + if ( + segmentRefsCompatible(refs[i]!, refs[j]!, mergeDistance, intervalGap) + ) { + union(i, j) + } + } + } + + const clusters = new Map() + for (let i = 0; i < refs.length; i++) { + const root = find(i) + if (!clusters.has(root)) clusters.set(root, []) + clusters.get(root)!.push(refs[i]!) + } + + return Array.from(clusters.values()).filter((cluster) => cluster.length > 1) +} + +const hasOnlyOrthogonalSegments = (path: Point[]) => { + for (let i = 0; i < path.length - 1; i++) { + const start = path[i]! + const end = path[i + 1]! + if (samePoint(start, end)) continue + if (!isHorizontal(start, end, EPS) && !isVertical(start, end, EPS)) { + return false + } + } + return true +} + +const countChipCollisions = (path: Point[], inputProblem: InputProblem) => { + const rects = getObstacleRects(inputProblem) + let count = 0 + for (let i = 0; i < path.length - 1; i++) { + const start = path[i]! + const end = path[i + 1]! + for (const rect of rects) { + if (segmentIntersectsRect(start, end, rect, EPS)) count++ + } + } + return count +} + +const countDifferentNetIntersections = ( + trace: SolvedTracePath, + path: Point[], + traces: SolvedTracePath[], +) => { + let count = 0 + for (let i = 0; i < path.length - 1; i++) { + const a = path[i]! + const b = path[i + 1]! + for (const otherTrace of traces) { + if ( + otherTrace.mspPairId === trace.mspPairId || + otherTrace.globalConnNetId === trace.globalConnNetId + ) { + continue + } + for (let j = 0; j < otherTrace.tracePath.length - 1; j++) { + const c = otherTrace.tracePath[j]! + const d = otherTrace.tracePath[j + 1]! + if (doSegmentsIntersect(a, b, c, d)) count++ + } + } + } + return count +} + +const snappedPathForSegment = ( + trace: SolvedTracePath, + segment: SegmentRef, + coord: number, +): Point[] | null => { + const pts = trace.tracePath.map((p) => ({ ...p })) + const segmentStart = pts[segment.segmentIndex]! + const segmentEnd = pts[segment.segmentIndex + 1]! + const lastIndex = pts.length - 1 + const isFirstSegment = segment.segmentIndex === 0 + const isLastSegment = segment.segmentIndex + 1 === lastIndex + + if (isFirstSegment && isLastSegment) return null + if (Math.abs(segment.coord - coord) < EPS) return null + + if (segment.axis === "horizontal") { + if (isFirstSegment) { + pts.splice( + 1, + 1, + { x: segmentStart.x, y: coord }, + { x: segmentEnd.x, y: coord }, + ) + } else if (isLastSegment) { + pts.splice( + segment.segmentIndex, + 1, + { x: segmentStart.x, y: coord }, + { x: segmentEnd.x, y: coord }, + ) + } else { + segmentStart.y = coord + segmentEnd.y = coord + } + } else { + if (isFirstSegment) { + pts.splice( + 1, + 1, + { x: coord, y: segmentStart.y }, + { x: coord, y: segmentEnd.y }, + ) + } else if (isLastSegment) { + pts.splice( + segment.segmentIndex, + 1, + { x: coord, y: segmentStart.y }, + { x: coord, y: segmentEnd.y }, + ) + } else { + segmentStart.x = coord + segmentEnd.x = coord + } + } + + return normalizePath(pts) +} + +export class SameNetTraceConsolidationSolver extends BaseSolver { + inputProblem: InputProblem + inputTraces: SolvedTracePath[] + mergeDistance: number + intervalGap: number + + outputTraces: SolvedTracePath[] + correctedTraceMap: Record + private consolidationPassCount = 0 + + constructor(params: SameNetTraceConsolidationSolverInput) { + super() + this.inputProblem = params.inputProblem + this.inputTraces = params.inputTraces + this.mergeDistance = params.mergeDistance ?? DEFAULT_MERGE_DISTANCE + this.intervalGap = params.intervalGap ?? DEFAULT_INTERVAL_GAP + + this.outputTraces = params.inputTraces.map(cloneTrace) + this.correctedTraceMap = Object.fromEntries( + this.outputTraces.map((trace) => [trace.mspPairId, trace]), + ) + } + + override getConstructorParams(): ConstructorParameters< + typeof SameNetTraceConsolidationSolver + >[0] { + return { + inputProblem: this.inputProblem, + inputTraces: this.inputTraces, + mergeDistance: this.mergeDistance, + intervalGap: this.intervalGap, + } + } + + override _step() { + const changed = this.applyNextConsolidationPass() + if (!changed) { + this.solved = true + return + } + + this.consolidationPassCount++ + if (this.consolidationPassCount >= MAX_CONSOLIDATION_PASSES) { + this.stats.consolidationPassLimitExceeded = true + this.stats.consolidationPassCount = this.consolidationPassCount + this.solved = true + } + } + + private applyNextConsolidationPass() { + const tracesByNet = new Map() + for (const trace of this.outputTraces) { + if (!tracesByNet.has(trace.globalConnNetId)) { + tracesByNet.set(trace.globalConnNetId, []) + } + tracesByNet.get(trace.globalConnNetId)!.push(trace) + } + + for (const globalConnNetId of [...tracesByNet.keys()].sort()) { + const netTraces = tracesByNet.get(globalConnNetId)! + for (const axis of ["horizontal", "vertical"] as const) { + const refs = netTraces + .flatMap(getSegmentRefs) + .filter((ref) => ref.axis === axis) + .sort(compareSegmentRefs) + + const clusters = clusterSegments( + refs, + this.mergeDistance, + this.intervalGap, + ).sort((a, b) => compareSegmentRefs(a[0]!, b[0]!)) + + for (const cluster of clusters) { + const canonical = chooseCanonicalSegment(cluster) + const targets = cluster + .filter((segment) => segment.stableKey !== canonical.stableKey) + .sort( + (a, b) => + a.length - b.length || a.stableKey.localeCompare(b.stableKey), + ) + + let changed = false + const updatedTraceIds = new Set() + for (const target of targets) { + if (updatedTraceIds.has(target.mspPairId)) continue + const trace = this.correctedTraceMap[target.mspPairId] + if (!trace) continue + const candidatePath = snappedPathForSegment( + trace, + target, + canonical.coord, + ) + if (!candidatePath) continue + if (!this.isCandidateSafe(trace, candidatePath)) continue + + const updatedTrace = { + ...trace, + tracePath: candidatePath, + } + this.correctedTraceMap[trace.mspPairId] = updatedTrace + this.outputTraces = this.outputTraces.map((existingTrace) => + existingTrace.mspPairId === trace.mspPairId + ? updatedTrace + : existingTrace, + ) + updatedTraceIds.add(trace.mspPairId) + changed = true + } + + if (changed) return true + } + } + } + + return false + } + + private isCandidateSafe(trace: SolvedTracePath, path: Point[]) { + if (path.length < 2) return false + if (!samePoint(path[0]!, trace.tracePath[0]!)) return false + if ( + !samePoint( + path[path.length - 1]!, + trace.tracePath[trace.tracePath.length - 1]!, + ) + ) { + return false + } + if (!hasOnlyOrthogonalSegments(path)) return false + + const originalChipCollisions = countChipCollisions( + trace.tracePath, + this.inputProblem, + ) + const candidateChipCollisions = countChipCollisions(path, this.inputProblem) + if (candidateChipCollisions > originalChipCollisions) return false + + const originalDifferentNetIntersections = countDifferentNetIntersections( + trace, + trace.tracePath, + this.outputTraces, + ) + const candidateDifferentNetIntersections = countDifferentNetIntersections( + trace, + path, + this.outputTraces, + ) + if ( + candidateDifferentNetIntersections > originalDifferentNetIntersections + ) { + return false + } + + return true + } + + getOutput() { + return { + traces: this.outputTraces, + correctedTraceMap: this.correctedTraceMap, + } + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + + for (const trace of this.inputTraces) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: "rgba(120,120,120,0.45)", + strokeDash: "4 2", + }) + } + + for (const trace of this.outputTraces) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: getColorFromString(trace.globalConnNetId, 0.9), + }) + } + + return graphics + } +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c1..1bb4da3f7 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -26,6 +26,8 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/ import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver" import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver" import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver" +import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver" +import { SameNetTraceConsolidationSolver } from "../SameNetTraceConsolidationSolver/SameNetTraceConsolidationSolver" type PipelineStep BaseSolver> = { solverName: string @@ -75,11 +77,13 @@ export class SchematicTracePipelineSolver extends BaseSolver { labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver traceCleanupSolver?: TraceCleanupSolver + sameNetTraceConsolidationSolver?: SameNetTraceConsolidationSolver example28Solver?: Example28Solver availableNetOrientationSolver?: AvailableNetOrientationSolver vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver + netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver startTimeOfPhase: Record endTimeOfPhase: Record @@ -217,11 +221,28 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ] }), + definePipelineStep( + "sameNetTraceConsolidationSolver", + SameNetTraceConsolidationSolver, + (instance) => { + const traces = + instance.traceCleanupSolver?.getOutput().traces ?? + instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces + + return [ + { + inputProblem: instance.inputProblem, + inputTraces: traces, + }, + ] + }, + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, (instance) => { const traces = + instance.sameNetTraceConsolidationSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -237,6 +258,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { ), definePipelineStep("example28Solver", Example28Solver, (instance) => { const traces = + instance.sameNetTraceConsolidationSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -300,6 +322,19 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ], ), + definePipelineStep( + "netLabelNetLabelCollisionSolver", + NetLabelNetLabelCollisionSolver, + (instance) => [ + { + inputProblem: instance.inputProblem, + traces: instance.netLabelTraceCollisionSolver!.getOutput().traces, + netLabelPlacements: + instance.netLabelTraceCollisionSolver!.getOutput() + .netLabelPlacements, + }, + ], + ), ] constructor(inputProblem: InputProblem) { diff --git a/package.json b/package.json index 216f1d8ac..ac2765b39 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tscircuit/schematic-trace-solver", "main": "dist/index.js", - "version": "0.0.60", + "version": "0.0.62", "type": "module", "scripts": { "start": "cosmos", diff --git a/site/examples/example37.page.tsx b/site/examples/example37.page.tsx new file mode 100644 index 000000000..eb5f22243 --- /dev/null +++ b/site/examples/example37.page.tsx @@ -0,0 +1,4 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example37.json" + +export default () => diff --git a/site/examples/example38.page.tsx b/site/examples/example38.page.tsx new file mode 100644 index 000000000..10e50c56a --- /dev/null +++ b/site/examples/example38.page.tsx @@ -0,0 +1,4 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example38.json" + +export default () => diff --git a/site/examples/example39.page.tsx b/site/examples/example39.page.tsx new file mode 100644 index 000000000..e6a3b51b9 --- /dev/null +++ b/site/examples/example39.page.tsx @@ -0,0 +1,4 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example39.json" + +export default () => diff --git a/site/examples/example40.page.tsx b/site/examples/example40.page.tsx new file mode 100644 index 000000000..3e708e688 --- /dev/null +++ b/site/examples/example40.page.tsx @@ -0,0 +1,4 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example40.json" + +export default () => diff --git a/site/examples/example41.page.tsx b/site/examples/example41.page.tsx new file mode 100644 index 000000000..2f8944108 --- /dev/null +++ b/site/examples/example41.page.tsx @@ -0,0 +1,4 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example41.json" + +export default () => diff --git a/site/examples/example42.page.tsx b/site/examples/example42.page.tsx new file mode 100644 index 000000000..4f5f5f1b4 --- /dev/null +++ b/site/examples/example42.page.tsx @@ -0,0 +1,13 @@ +import { useMemo } from "react" +import { SameNetTraceConsolidationSolver } from "lib/solvers/SameNetTraceConsolidationSolver/SameNetTraceConsolidationSolver" +import { GenericSolverDebugger } from "site/components/GenericSolverDebugger" +import inputData from "../../tests/assets/example42.json" + +export default () => { + const solver = useMemo( + () => new SameNetTraceConsolidationSolver(inputData as any), + [], + ) + + return +} diff --git a/tests/assets/example37.json b/tests/assets/example37.json new file mode 100644 index 000000000..5040711e3 --- /dev/null +++ b/tests/assets/example37.json @@ -0,0 +1,173 @@ +{ + "chips": [ + { + "chipId": "schematic_component_0", + "center": { + "x": -3.69, + "y": 1.7 + }, + "width": 1.1, + "height": 0.388910699999999, + "pins": [ + { + "pinId": "R1.1", + "x": -4.24, + "y": 1.7 + }, + { + "pinId": "R1.2", + "x": -3.14, + "y": 1.7 + } + ] + }, + { + "chipId": "schematic_component_1", + "center": { + "x": -6.19, + "y": 0.3 + }, + "width": 1.1, + "height": 0.84, + "pins": [ + { + "pinId": "C1.1", + "x": -6.74, + "y": 0.3 + }, + { + "pinId": "C1.2", + "x": -5.640000000000001, + "y": 0.3 + } + ] + }, + { + "chipId": "schematic_component_2", + "center": { + "x": 0, + "y": 0 + }, + "width": 0.4, + "height": 1, + "pins": [ + { + "pinId": "U1.1", + "x": -0.6000000000000001, + "y": 0.30000000000000004 + }, + { + "pinId": "U1.2", + "x": -0.6000000000000001, + "y": 0.10000000000000003 + }, + { + "pinId": "U1.3", + "x": -0.6000000000000001, + "y": -0.09999999999999998 + }, + { + "pinId": "U1.4", + "x": -0.6000000000000001, + "y": -0.30000000000000004 + }, + { + "pinId": "U1.5", + "x": 0.6000000000000001, + "y": -0.30000000000000004 + }, + { + "pinId": "U1.6", + "x": 0.6000000000000001, + "y": -0.10000000000000003 + }, + { + "pinId": "U1.7", + "x": 0.6000000000000001, + "y": 0.09999999999999998 + }, + { + "pinId": "U1.8", + "x": 0.6000000000000001, + "y": 0.30000000000000004 + } + ] + }, + { + "chipId": "schematic_component_3", + "center": { + "x": -2.92, + "y": -0.4 + }, + "width": 0.4, + "height": 1, + "pins": [ + { + "pinId": "U2.1", + "x": -3.52, + "y": -0.09999999999999998 + }, + { + "pinId": "U2.2", + "x": -3.52, + "y": -0.3 + }, + { + "pinId": "U2.3", + "x": -3.52, + "y": -0.5 + }, + { + "pinId": "U2.4", + "x": -3.52, + "y": -0.7000000000000001 + }, + { + "pinId": "U2.5", + "x": -2.32, + "y": -0.7000000000000001 + }, + { + "pinId": "U2.6", + "x": -2.32, + "y": -0.5 + }, + { + "pinId": "U2.7", + "x": -2.32, + "y": -0.30000000000000004 + }, + { + "pinId": "U2.8", + "x": -2.32, + "y": -0.09999999999999998 + } + ] + } + ], + "directConnections": [ + { + "pinIds": ["C1.1", "R1.1"], + "netId": ".C1 > .pin1 to R1.pin1" + }, + { + "pinIds": ["U1.1", "C1.2"], + "netId": ".U1 > .pin1 to C1.pin2" + }, + { + "pinIds": ["U1.2", "R1.2"], + "netId": ".U1 > .pin2 to R1.pin2" + }, + { + "pinIds": ["U2.1", "U1.3"], + "netId": ".U2 > .pin1 to U1.pin3" + }, + { + "pinIds": ["U2.2", "U1.4"], + "netId": ".U2 > .pin2 to U1.pin4" + } + ], + "netConnections": [], + "availableNetLabelOrientations": {}, + "maxMspPairDistance": 2.4 +} diff --git a/tests/assets/example38.json b/tests/assets/example38.json new file mode 100644 index 000000000..ce86496d6 --- /dev/null +++ b/tests/assets/example38.json @@ -0,0 +1,32 @@ +{ + "chips": [ + { + "chipId": "schematic_component_0", + "center": { + "x": 0, + "y": 0 + }, + "width": 0.2, + "height": 0.325, + "pins": [ + { + "pinId": "TP1.1", + "x": -1.2246467991473533e-17, + "y": 0.2 + } + ] + } + ], + "directConnections": [], + "netConnections": [ + { + "netId": "GND", + "pinIds": ["TP1.1"], + "netLabelWidth": 0.48 + } + ], + "availableNetLabelOrientations": { + "GND": ["y-"] + }, + "maxMspPairDistance": 2.4 +} diff --git a/tests/assets/example39.json b/tests/assets/example39.json new file mode 100644 index 000000000..cf466ff1b --- /dev/null +++ b/tests/assets/example39.json @@ -0,0 +1,71 @@ +{ + "chips": [ + { + "chipId": "schematic_component_0", + "center": { + "x": -6.5, + "y": -0.33 + }, + "width": 1.5, + "height": 0.8, + "pins": [ + { + "pinId": "U3.24", + "x": -7.65, + "y": -0.22999999999999998 + }, + { + "pinId": "U3.25", + "x": -7.65, + "y": -0.43000000000000016 + } + ], + "sectionId": "rp2040" + }, + { + "chipId": "schematic_component_4", + "center": { + "x": -9.5, + "y": -0.4299999999999997 + }, + "width": 1.1, + "height": 0.388910699999999, + "pins": [ + { + "pinId": "R5.1", + "x": -10.049999999999999, + "y": -0.4299999999999997 + }, + { + "pinId": "R5.2", + "x": -8.95, + "y": -0.4299999999999997 + } + ], + "sectionId": "rp2040" + } + ], + "directConnections": [ + { + "pinIds": ["U3.25", "R5.2"], + "netId": ".U3 > .pin25 to .R5 > .pin2" + } + ], + "netConnections": [ + { + "netId": "SWCLK", + "pinIds": ["U3.24"], + "netLabelWidth": 0.72 + }, + { + "netId": "SWD", + "pinIds": ["U3.25", "R5.2"], + "netLabelWidth": 0.48 + } + ], + "availableNetLabelOrientations": { + "SWCLK": ["x-", "x+"], + "SWD": ["x-", "x+"] + }, + "maxMspPairDistance": 2.4 +} diff --git a/tests/assets/example40.json b/tests/assets/example40.json new file mode 100644 index 000000000..c8ab68699 --- /dev/null +++ b/tests/assets/example40.json @@ -0,0 +1,157 @@ +{ + "chips": [ + { + "chipId": "J2", + "center": { + "x": 0, + "y": 0 + }, + "width": 0.65, + "height": 1.8, + "pins": [ + { + "pinId": "J2.1", + "x": 0.325, + "y": 0.8 + }, + { + "pinId": "J2.2", + "x": 0.325, + "y": 0.6 + }, + { + "pinId": "J2.3", + "x": 0.325, + "y": 0.4 + }, + { + "pinId": "J2.4", + "x": 0.325, + "y": 0.2 + }, + { + "pinId": "J2.5", + "x": 0.325, + "y": 0 + }, + { + "pinId": "J2.6", + "x": 0.325, + "y": -0.2 + }, + { + "pinId": "J2.7", + "x": 0.325, + "y": -0.4 + }, + { + "pinId": "J2.8", + "x": 0.325, + "y": -0.6 + }, + { + "pinId": "J2.9", + "x": 0.325, + "y": -0.8 + } + ] + }, + { + "chipId": "U1", + "center": { + "x": 4, + "y": 0.4 + }, + "width": 1, + "height": 2, + "pins": [ + { + "pinId": "U1.1", + "x": 3.5, + "y": 1.2 + }, + { + "pinId": "U1.2", + "x": 3.5, + "y": 0.8 + }, + { + "pinId": "U1.3", + "x": 3.5, + "y": 0.4 + }, + { + "pinId": "U1.4", + "x": 3.5, + "y": 0 + }, + { + "pinId": "U1.5", + "x": 3.5, + "y": -0.4 + }, + { + "pinId": "U1.6", + "x": 4.5, + "y": 1 + }, + { + "pinId": "U1.7", + "x": 4.5, + "y": 0.6 + }, + { + "pinId": "U1.8", + "x": 4.5, + "y": 0.2 + }, + { + "pinId": "U1.9", + "x": 4.5, + "y": -0.2 + } + ] + } + ], + "directConnections": [ + { + "pinIds": ["J2.1", "U1.1"], + "netId": "J2.pin1 to U1.pin1" + }, + { + "pinIds": ["J2.2", "U1.2"], + "netId": "J2.pin2 to U1.pin2" + }, + { + "pinIds": ["J2.3", "U1.3"], + "netId": "J2.pin3 to U1.pin3" + }, + { + "pinIds": ["J2.4", "U1.4"], + "netId": "J2.pin4 to U1.pin4" + }, + { + "pinIds": ["J2.5", "U1.5"], + "netId": "J2.pin5 to U1.pin5" + }, + { + "pinIds": ["J2.6", "U1.6"], + "netId": "J2.pin6 to U1.pin6" + }, + { + "pinIds": ["J2.7", "U1.7"], + "netId": "J2.pin7 to U1.pin7" + }, + { + "pinIds": ["J2.8", "U1.8"], + "netId": "J2.pin8 to U1.pin8" + }, + { + "pinIds": ["J2.9", "U1.9"], + "netId": "J2.pin9 to U1.pin9" + } + ], + "netConnections": [], + "availableNetLabelOrientations": {}, + "maxMspPairDistance": 5 +} diff --git a/tests/assets/example41.json b/tests/assets/example41.json new file mode 100644 index 000000000..9f65ee947 --- /dev/null +++ b/tests/assets/example41.json @@ -0,0 +1,81 @@ +{ + "chips": [ + { + "chipId": "schematic_component_0", + "center": { + "x": -6.5, + "y": -0.33 + }, + "width": 1.5, + "height": 1.5, + "pins": [ + { + "pinId": "U3.24", + "x": -7.65, + "y": -0.22999999999999998 + }, + { + "pinId": "U3.25", + "x": -7.65, + "y": -0.43000000000000016 + }, + { + "pinId": "U3.26", + "x": -7.65, + "y": -0.63000000000000016 + } + ], + "sectionId": "rp2040" + }, + { + "chipId": "schematic_component_4", + "center": { + "x": -9.5, + "y": -0.4299999999999997 + }, + "width": 1.1, + "height": 0.388910699999999, + "pins": [ + { + "pinId": "R5.1", + "x": -10.049999999999999, + "y": -0.4299999999999997 + }, + { + "pinId": "R5.2", + "x": -8.95, + "y": -0.4299999999999997 + } + ], + "sectionId": "rp2040" + } + ], + "directConnections": [ + { + "pinIds": ["U3.25", "R5.2"], + "netId": ".U3 > .pin25 to .R5 > .pin2" + } + ], + "netConnections": [ + { + "netId": "SWCLK", + "pinIds": ["U3.24"], + "netLabelWidth": 0.72 + }, + { + "netId": "SWD", + "pinIds": ["U3.25", "R5.2"], + "netLabelWidth": 0.48 + }, + { + "netId": "CLK", + "pinIds": ["U3.26"], + "netLabelWidth": 0.72 + } + ], + "availableNetLabelOrientations": { + "SWCLK": ["x-", "x+"], + "SWD": ["x-", "x+"] + }, + "maxMspPairDistance": 2.4 +} diff --git a/tests/assets/example42.json b/tests/assets/example42.json new file mode 100644 index 000000000..9cbd857f6 --- /dev/null +++ b/tests/assets/example42.json @@ -0,0 +1,67 @@ +{ + "inputProblem": { + "chips": [], + "directConnections": [], + "netConnections": [], + "availableNetLabelOrientations": {} + }, + "inputTraces": [ + { + "mspPairId": "same-net-trunk", + "dcConnNetId": "vcc", + "globalConnNetId": "vcc", + "userNetId": "VCC", + "pins": [ + { "chipId": "A", "pinId": "A.1", "x": 0, "y": 0 }, + { "chipId": "B", "pinId": "B.1", "x": 2.4, "y": 0 } + ], + "pinIds": ["A.1", "B.1"], + "mspConnectionPairIds": ["same-net-trunk"], + "tracePath": [ + { "x": 0, "y": 0 }, + { "x": 0.4, "y": 0 }, + { "x": 0.4, "y": 1 }, + { "x": 2.4, "y": 1 }, + { "x": 2.4, "y": 0 } + ] + }, + { + "mspPairId": "same-net-branch", + "dcConnNetId": "vcc", + "globalConnNetId": "vcc", + "userNetId": "VCC", + "pins": [ + { "chipId": "C", "pinId": "C.1", "x": 0, "y": 0.2 }, + { "chipId": "D", "pinId": "D.1", "x": 2, "y": 0.2 } + ], + "pinIds": ["C.1", "D.1"], + "mspConnectionPairIds": ["same-net-branch"], + "tracePath": [ + { "x": 0, "y": 0.2 }, + { "x": 0.4, "y": 0.2 }, + { "x": 0.4, "y": 1.08 }, + { "x": 2, "y": 1.08 }, + { "x": 2, "y": 0.2 } + ] + }, + { + "mspPairId": "different-net-nearby", + "dcConnNetId": "gnd", + "globalConnNetId": "gnd", + "userNetId": "GND", + "pins": [ + { "chipId": "E", "pinId": "E.1", "x": 0, "y": 0.4 }, + { "chipId": "F", "pinId": "F.1", "x": 2, "y": 0.4 } + ], + "pinIds": ["E.1", "F.1"], + "mspConnectionPairIds": ["different-net-nearby"], + "tracePath": [ + { "x": 0, "y": 0.4 }, + { "x": 0.4, "y": 0.4 }, + { "x": 0.4, "y": 1.16 }, + { "x": 2, "y": 1.16 }, + { "x": 2, "y": 0.4 } + ] + } + ] +} diff --git a/tests/examples/__snapshots__/example02.snap.svg b/tests/examples/__snapshots__/example02.snap.svg index 3815fdc0b..311efa946 100644 --- a/tests/examples/__snapshots__/example02.snap.svg +++ b/tests/examples/__snapshots__/example02.snap.svg @@ -58,7 +58,7 @@ orientation: y+" data-x="-1.4574283249999997" data-y="1.3024186000000004" cx="29 +orientation: y-" data-x="-1.5071549750000002" data-y="-0.2000000000000004" cx="288.00904304318556" cy="349.5798500586337" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> - + @@ -202,7 +202,7 @@ available orientations: y+" data-x="-1.4574283249999997" data-y="1.5274186000000 +available orientations: y-" data-x="-1.5071549750000002" data-y="-0.4250000000000004" x="279.54556663155927" y="349.5798500586337" width="16.92695282325252" height="38.085643852318185" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +y+" data-x="0.30397715550000004" data-y="0.5800832909999993" cx="386.41696544795997" cy="192.03773223990865" r="3" fill="hsl(315, 100%, 50%, 0.8)" /> +y-" data-x="0.31067575550000137" data-y="-0.5800832909999993" cx="388.6751471623335" cy="583.1443755831586" r="3" fill="hsl(316, 100%, 50%, 0.8)" /> +x-" data-x="-0.4467558855000001" data-y="-0.10250625000000019" cx="133.33548191977735" cy="422.1471909191105" r="3" fill="hsl(317, 100%, 50%, 0.8)" /> - + - + - + - + + + + +globalConnNetId: connectivity_net0" data-x="0.30397715550000004" data-y="0.8060832909999993" x="352.7057166142773" y="40" width="67.42249766736523" height="151.70061975157182" fill="#ef444466" stroke="#ef4444" stroke-width="0.0029663688964285694" /> +globalConnNetId: connectivity_net0" data-x="0.5606757555000014" data-y="-0.3040832909999993" x="439.2420204128574" y="414.25101892640873" width="67.42249766736518" height="151.70061975157182" fill="#ef444466" stroke="#ef4444" stroke-width="0.0029663688964285694" /> - + diff --git a/tests/examples/__snapshots__/example19.snap.svg b/tests/examples/__snapshots__/example19.snap.svg index ac5ba82db..8e54f6c76 100644 --- a/tests/examples/__snapshots__/example19.snap.svg +++ b/tests/examples/__snapshots__/example19.snap.svg @@ -98,10 +98,10 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 - + - + @@ -129,19 +129,23 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 +globalConnNetId: connectivity_net3 +available orientations: any" data-x="2.2284928" data-y="-0.46751595000000035" x="300.34928" y="375.9895275000001" width="45" height="19.999999999999943" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01" /> +globalConnNetId: connectivity_net0 +available orientations: any" data-x="1.6252733499999996" data-y="-0.30120930000000024" x="240.027335" y="359.3588625000001" width="45" height="20" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01" /> +globalConnNetId: connectivity_net1 +available orientations: any" data-x="0.7999316625000001" data-y="0.42500000000000004" x="169.99316625000006" y="274.23793250000006" width="20" height="45" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01" /> +globalConnNetId: connectivity_net2 +available orientations: any" data-x="3.3884680250000008" data-y="1.5247267500000008" x="428.8468025000001" y="164.26525749999996" width="20" height="45.00000000000003" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01" />