@tscircuit/schematic-trace-solver 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -5
- package/dist/index.js +451 -338
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +199 -465
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +89 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/anchors.ts +11 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions.ts +45 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry.ts +48 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts +64 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +177 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +4 -0
- package/lib/solvers/SchematicTracePipelineSolver/expandChipsToFitPins.ts +41 -0
- package/package.json +1 -1
- package/site/examples/example01-basic.page.tsx +2 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
4
|
+
import { chooseHostTraceForGroup } from "./host"
|
|
5
|
+
import type { SingleNetLabelPlacementSolver } from "./SingleNetLabelPlacementSolver"
|
|
6
|
+
|
|
7
|
+
export function visualizeSingleNetLabelPlacementSolver(
|
|
8
|
+
solver: SingleNetLabelPlacementSolver,
|
|
9
|
+
): GraphicsObject {
|
|
10
|
+
const graphics = visualizeInputProblem(solver.inputProblem)
|
|
11
|
+
|
|
12
|
+
// Visualize the entire trace group for this net id
|
|
13
|
+
const groupId = solver.overlappingSameNetTraceGroup.globalConnNetId
|
|
14
|
+
const host = chooseHostTraceForGroup({
|
|
15
|
+
inputProblem: solver.inputProblem,
|
|
16
|
+
inputTraceMap: solver.inputTraceMap,
|
|
17
|
+
globalConnNetId: groupId,
|
|
18
|
+
fallbackTrace: solver.overlappingSameNetTraceGroup.overlappingTraces,
|
|
19
|
+
})
|
|
20
|
+
const groupStroke = getColorFromString(groupId, 0.9)
|
|
21
|
+
const groupFill = getColorFromString(groupId, 0.5)
|
|
22
|
+
|
|
23
|
+
for (const trace of Object.values(solver.inputTraceMap)) {
|
|
24
|
+
if (trace.globalConnNetId !== groupId) continue
|
|
25
|
+
const isHost = host ? trace.mspPairId === host.mspPairId : false
|
|
26
|
+
graphics.lines!.push({
|
|
27
|
+
points: trace.tracePath,
|
|
28
|
+
strokeColor: isHost ? groupStroke : groupFill,
|
|
29
|
+
strokeWidth: isHost ? 0.006 : 0.003,
|
|
30
|
+
strokeDash: isHost ? undefined : "4 2",
|
|
31
|
+
} as any)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Visualize all tested candidate rectangles with reason coloring
|
|
35
|
+
for (const c of solver.testedCandidates) {
|
|
36
|
+
const fill =
|
|
37
|
+
c.status === "ok"
|
|
38
|
+
? "rgba(0, 180, 0, 0.25)"
|
|
39
|
+
: c.status === "chip-collision"
|
|
40
|
+
? "rgba(220, 0, 0, 0.25)"
|
|
41
|
+
: c.status === "trace-collision"
|
|
42
|
+
? "rgba(220, 140, 0, 0.25)"
|
|
43
|
+
: "rgba(120, 120, 120, 0.15)"
|
|
44
|
+
const stroke =
|
|
45
|
+
c.status === "ok"
|
|
46
|
+
? "green"
|
|
47
|
+
: c.status === "chip-collision"
|
|
48
|
+
? "red"
|
|
49
|
+
: c.status === "trace-collision"
|
|
50
|
+
? "orange"
|
|
51
|
+
: "gray"
|
|
52
|
+
|
|
53
|
+
graphics.rects!.push({
|
|
54
|
+
center: {
|
|
55
|
+
x: (c.bounds.minX + c.bounds.maxX) / 2,
|
|
56
|
+
y: (c.bounds.minY + c.bounds.maxY) / 2,
|
|
57
|
+
},
|
|
58
|
+
width: c.width,
|
|
59
|
+
height: c.height,
|
|
60
|
+
fill,
|
|
61
|
+
strokeColor: stroke,
|
|
62
|
+
} as any)
|
|
63
|
+
|
|
64
|
+
graphics.points!.push({
|
|
65
|
+
x: c.anchor.x,
|
|
66
|
+
y: c.anchor.y,
|
|
67
|
+
color: stroke,
|
|
68
|
+
} as any)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Visualize the final accepted label (if any)
|
|
72
|
+
if (solver.netLabelPlacement) {
|
|
73
|
+
const p = solver.netLabelPlacement
|
|
74
|
+
graphics.rects!.push({
|
|
75
|
+
center: p.center,
|
|
76
|
+
width: p.width,
|
|
77
|
+
height: p.height,
|
|
78
|
+
fill: "rgba(0, 128, 255, 0.35)",
|
|
79
|
+
strokeColor: "blue",
|
|
80
|
+
} as any)
|
|
81
|
+
graphics.points!.push({
|
|
82
|
+
x: p.anchorPoint.x,
|
|
83
|
+
y: p.anchorPoint.y,
|
|
84
|
+
color: "blue",
|
|
85
|
+
} as any)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return graphics
|
|
89
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
|
|
4
|
+
export function segmentIntersectsRect(
|
|
5
|
+
p1: { x: number; y: number },
|
|
6
|
+
p2: { x: number; y: number },
|
|
7
|
+
rect: { minX: number; minY: number; maxX: number; maxY: number },
|
|
8
|
+
EPS = 1e-9,
|
|
9
|
+
): boolean {
|
|
10
|
+
const isVert = Math.abs(p1.x - p2.x) < EPS
|
|
11
|
+
const isHorz = Math.abs(p1.y - p2.y) < EPS
|
|
12
|
+
if (!isVert && !isHorz) return false
|
|
13
|
+
|
|
14
|
+
if (isVert) {
|
|
15
|
+
const x = p1.x
|
|
16
|
+
if (x < rect.minX - EPS || x > rect.maxX + EPS) return false
|
|
17
|
+
const segMinY = Math.min(p1.y, p2.y)
|
|
18
|
+
const segMaxY = Math.max(p1.y, p2.y)
|
|
19
|
+
const overlap = Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY)
|
|
20
|
+
return overlap > EPS
|
|
21
|
+
} else {
|
|
22
|
+
const y = p1.y
|
|
23
|
+
if (y < rect.minY - EPS || y > rect.maxY + EPS) return false
|
|
24
|
+
const segMinX = Math.min(p1.x, p2.x)
|
|
25
|
+
const segMaxX = Math.max(p1.x, p2.x)
|
|
26
|
+
const overlap = Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX)
|
|
27
|
+
return overlap > EPS
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rectIntersectsAnyTrace(
|
|
32
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number },
|
|
33
|
+
inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>,
|
|
34
|
+
hostPathId?: MspConnectionPairId,
|
|
35
|
+
hostSegIndex?: number,
|
|
36
|
+
): boolean {
|
|
37
|
+
for (const [pairId, solved] of Object.entries(inputTraceMap)) {
|
|
38
|
+
const pts = solved.tracePath
|
|
39
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
40
|
+
if (pairId === hostPathId && i === hostSegIndex) continue
|
|
41
|
+
if (segmentIntersectsRect(pts[i]!, pts[i + 1]!, bounds)) return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
2
|
+
|
|
3
|
+
export const NET_LABEL_HORIZONTAL_WIDTH = 0.4
|
|
4
|
+
export const NET_LABEL_HORIZONTAL_HEIGHT = 0.2
|
|
5
|
+
|
|
6
|
+
export function getDimsForOrientation(orientation: FacingDirection) {
|
|
7
|
+
if (orientation === "y+" || orientation === "y-") {
|
|
8
|
+
return {
|
|
9
|
+
width: NET_LABEL_HORIZONTAL_HEIGHT,
|
|
10
|
+
height: NET_LABEL_HORIZONTAL_WIDTH,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
width: NET_LABEL_HORIZONTAL_WIDTH,
|
|
15
|
+
height: NET_LABEL_HORIZONTAL_HEIGHT,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCenterFromAnchor(
|
|
20
|
+
anchor: { x: number; y: number },
|
|
21
|
+
orientation: FacingDirection,
|
|
22
|
+
width: number,
|
|
23
|
+
height: number,
|
|
24
|
+
) {
|
|
25
|
+
switch (orientation) {
|
|
26
|
+
case "x+":
|
|
27
|
+
return { x: anchor.x + width / 2, y: anchor.y }
|
|
28
|
+
case "x-":
|
|
29
|
+
return { x: anchor.x - width / 2, y: anchor.y }
|
|
30
|
+
case "y+":
|
|
31
|
+
return { x: anchor.x, y: anchor.y + height / 2 }
|
|
32
|
+
case "y-":
|
|
33
|
+
return { x: anchor.x, y: anchor.y - height / 2 }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getRectBounds(
|
|
38
|
+
center: { x: number; y: number },
|
|
39
|
+
w: number,
|
|
40
|
+
h: number,
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
minX: center.x - w / 2,
|
|
44
|
+
minY: center.y - h / 2,
|
|
45
|
+
maxX: center.x + w / 2,
|
|
46
|
+
maxY: center.y + h / 2,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
4
|
+
|
|
5
|
+
export function lengthOfTrace(path: SolvedTracePath): number {
|
|
6
|
+
let sum = 0
|
|
7
|
+
const pts = path.tracePath
|
|
8
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
9
|
+
sum +=
|
|
10
|
+
Math.abs(pts[i + 1]!.x - pts[i]!.x) + Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
11
|
+
}
|
|
12
|
+
return sum
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function chooseHostTraceForGroup(params: {
|
|
16
|
+
inputProblem: InputProblem
|
|
17
|
+
inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>
|
|
18
|
+
globalConnNetId: string
|
|
19
|
+
fallbackTrace?: SolvedTracePath
|
|
20
|
+
}): SolvedTracePath | undefined {
|
|
21
|
+
const { inputProblem, inputTraceMap, globalConnNetId, fallbackTrace } = params
|
|
22
|
+
|
|
23
|
+
const chipsById: Record<string, InputChip> = Object.fromEntries(
|
|
24
|
+
inputProblem.chips.map((c) => [c.chipId, c]),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const groupTraces = Object.values(inputTraceMap).filter(
|
|
28
|
+
(t) => t.globalConnNetId === globalConnNetId,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const chipIdsInGroup = new Set<string>()
|
|
32
|
+
for (const t of groupTraces) {
|
|
33
|
+
chipIdsInGroup.add(t.pins[0].chipId)
|
|
34
|
+
chipIdsInGroup.add(t.pins[1].chipId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let largestChipId: string | null = null
|
|
38
|
+
let largestPinCount = -1
|
|
39
|
+
for (const id of chipIdsInGroup) {
|
|
40
|
+
const chip = chipsById[id]
|
|
41
|
+
const count = chip?.pins?.length ?? 0
|
|
42
|
+
if (count > largestPinCount) {
|
|
43
|
+
largestPinCount = count
|
|
44
|
+
largestChipId = id
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hostCandidates =
|
|
49
|
+
largestChipId == null
|
|
50
|
+
? []
|
|
51
|
+
: groupTraces.filter(
|
|
52
|
+
(t) =>
|
|
53
|
+
t.pins[0].chipId === largestChipId ||
|
|
54
|
+
t.pins[1].chipId === largestChipId,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (hostCandidates.length > 0) {
|
|
58
|
+
return hostCandidates.reduce((a, b) =>
|
|
59
|
+
lengthOfTrace(a) >= lengthOfTrace(b) ? a : b,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return fallbackTrace
|
|
64
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
5
|
+
import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
|
|
6
|
+
import type {
|
|
7
|
+
NetLabelPlacement,
|
|
8
|
+
OverlappingSameNetTraceGroup,
|
|
9
|
+
} from "../NetLabelPlacementSolver"
|
|
10
|
+
import {
|
|
11
|
+
getDimsForOrientation,
|
|
12
|
+
getCenterFromAnchor,
|
|
13
|
+
getRectBounds,
|
|
14
|
+
} from "./geometry"
|
|
15
|
+
import { rectIntersectsAnyTrace } from "./collisions"
|
|
16
|
+
|
|
17
|
+
export function solveNetLabelPlacementForPortOnlyPin(params: {
|
|
18
|
+
inputProblem: InputProblem
|
|
19
|
+
inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>
|
|
20
|
+
chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
21
|
+
overlappingSameNetTraceGroup: OverlappingSameNetTraceGroup
|
|
22
|
+
availableOrientations: FacingDirection[]
|
|
23
|
+
}): {
|
|
24
|
+
placement: NetLabelPlacement | null
|
|
25
|
+
testedCandidates: Array<{
|
|
26
|
+
center: { x: number; y: number }
|
|
27
|
+
width: number
|
|
28
|
+
height: number
|
|
29
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
30
|
+
anchor: { x: number; y: number }
|
|
31
|
+
orientation: FacingDirection
|
|
32
|
+
status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
|
|
33
|
+
hostSegIndex: number
|
|
34
|
+
}>
|
|
35
|
+
error?: string
|
|
36
|
+
} {
|
|
37
|
+
const {
|
|
38
|
+
inputProblem,
|
|
39
|
+
inputTraceMap,
|
|
40
|
+
chipObstacleSpatialIndex,
|
|
41
|
+
overlappingSameNetTraceGroup,
|
|
42
|
+
availableOrientations,
|
|
43
|
+
} = params
|
|
44
|
+
|
|
45
|
+
const pinId = overlappingSameNetTraceGroup.portOnlyPinId
|
|
46
|
+
if (!pinId) {
|
|
47
|
+
return {
|
|
48
|
+
placement: null,
|
|
49
|
+
testedCandidates: [],
|
|
50
|
+
error: "No portOnlyPinId provided",
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find pin coordinates
|
|
55
|
+
let pin: { x: number; y: number } | null = null
|
|
56
|
+
for (const chip of inputProblem.chips) {
|
|
57
|
+
const p = chip.pins.find((pp) => pp.pinId === pinId)
|
|
58
|
+
if (p) {
|
|
59
|
+
pin = { x: p.x, y: p.y }
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!pin) {
|
|
64
|
+
return {
|
|
65
|
+
placement: null,
|
|
66
|
+
testedCandidates: [],
|
|
67
|
+
error: `Port-only pin not found: ${pinId}`,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const orientations =
|
|
72
|
+
availableOrientations.length > 0
|
|
73
|
+
? availableOrientations
|
|
74
|
+
: (["x+", "x-", "y+", "y-"] as FacingDirection[])
|
|
75
|
+
|
|
76
|
+
const anchor = { x: pin.x, y: pin.y }
|
|
77
|
+
const outwardOf = (o: FacingDirection) =>
|
|
78
|
+
o === "x+"
|
|
79
|
+
? { x: 1, y: 0 }
|
|
80
|
+
: o === "x-"
|
|
81
|
+
? { x: -1, y: 0 }
|
|
82
|
+
: o === "y+"
|
|
83
|
+
? { x: 0, y: 1 }
|
|
84
|
+
: { x: 0, y: -1 }
|
|
85
|
+
|
|
86
|
+
const testedCandidates: Array<{
|
|
87
|
+
center: { x: number; y: number }
|
|
88
|
+
width: number
|
|
89
|
+
height: number
|
|
90
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
91
|
+
anchor: { x: number; y: number }
|
|
92
|
+
orientation: FacingDirection
|
|
93
|
+
status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
|
|
94
|
+
hostSegIndex: number
|
|
95
|
+
}> = []
|
|
96
|
+
|
|
97
|
+
for (const orientation of orientations) {
|
|
98
|
+
const { width, height } = getDimsForOrientation(orientation)
|
|
99
|
+
// Place label fully outside the chip: shift center slightly outward
|
|
100
|
+
const baseCenter = getCenterFromAnchor(anchor, orientation, width, height)
|
|
101
|
+
const outward = outwardOf(orientation)
|
|
102
|
+
const offset = 1e-3
|
|
103
|
+
const center = {
|
|
104
|
+
x: baseCenter.x + outward.x * offset,
|
|
105
|
+
y: baseCenter.y + outward.y * offset,
|
|
106
|
+
}
|
|
107
|
+
const bounds = getRectBounds(center, width, height)
|
|
108
|
+
|
|
109
|
+
// Chip collision check
|
|
110
|
+
const chips = chipObstacleSpatialIndex.getChipsInBounds(bounds)
|
|
111
|
+
if (chips.length > 0) {
|
|
112
|
+
testedCandidates.push({
|
|
113
|
+
center,
|
|
114
|
+
width,
|
|
115
|
+
height,
|
|
116
|
+
bounds,
|
|
117
|
+
anchor,
|
|
118
|
+
orientation,
|
|
119
|
+
status: "chip-collision",
|
|
120
|
+
hostSegIndex: -1,
|
|
121
|
+
})
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Trace collision check
|
|
126
|
+
if (
|
|
127
|
+
rectIntersectsAnyTrace(
|
|
128
|
+
bounds,
|
|
129
|
+
inputTraceMap,
|
|
130
|
+
"" as MspConnectionPairId,
|
|
131
|
+
-1,
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
testedCandidates.push({
|
|
135
|
+
center,
|
|
136
|
+
width,
|
|
137
|
+
height,
|
|
138
|
+
bounds,
|
|
139
|
+
anchor,
|
|
140
|
+
orientation,
|
|
141
|
+
status: "trace-collision",
|
|
142
|
+
hostSegIndex: -1,
|
|
143
|
+
})
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Found a valid placement
|
|
148
|
+
testedCandidates.push({
|
|
149
|
+
center,
|
|
150
|
+
width,
|
|
151
|
+
height,
|
|
152
|
+
bounds,
|
|
153
|
+
anchor,
|
|
154
|
+
orientation,
|
|
155
|
+
status: "ok",
|
|
156
|
+
hostSegIndex: -1,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const placement: NetLabelPlacement = {
|
|
160
|
+
globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
|
|
161
|
+
dcConnNetId: undefined,
|
|
162
|
+
orientation,
|
|
163
|
+
anchorPoint: anchor,
|
|
164
|
+
width,
|
|
165
|
+
height,
|
|
166
|
+
center,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { placement, testedCandidates }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
placement: null,
|
|
174
|
+
testedCandidates,
|
|
175
|
+
error: "Could not place net label at port without collisions",
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -14,6 +14,7 @@ import { visualizeInputProblem } from "./visualizeInputProblem"
|
|
|
14
14
|
import { GuidelinesSolver } from "../GuidelinesSolver/GuidelinesSolver"
|
|
15
15
|
import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds"
|
|
16
16
|
import { correctPinsInsideChips } from "./correctPinsInsideChip"
|
|
17
|
+
import { expandChipsToFitPins } from "./expandChipsToFitPins"
|
|
17
18
|
|
|
18
19
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
19
20
|
solverName: string
|
|
@@ -153,6 +154,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
153
154
|
_chipObstacleSpatialIndex: undefined,
|
|
154
155
|
})
|
|
155
156
|
|
|
157
|
+
// First, expand chips so existing pin coordinates sit on or within their edges without shrinking.
|
|
158
|
+
expandChipsToFitPins(cloned)
|
|
159
|
+
// Then, for any remaining pins that are still inside due to mixed extremes, snap them to the nearest edge.
|
|
156
160
|
correctPinsInsideChips(cloned)
|
|
157
161
|
|
|
158
162
|
return cloned
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expands chip width/height (never shrinks) so that all existing pin coordinates
|
|
5
|
+
* are inside or on the chip boundary, and any pin at the furthest extent in X/Y
|
|
6
|
+
* lies exactly on an edge.
|
|
7
|
+
*
|
|
8
|
+
* Notes:
|
|
9
|
+
* - Center is preserved.
|
|
10
|
+
* - Only increases dimensions as needed.
|
|
11
|
+
* - Clears cached _facingDirection on pins since geometry changed.
|
|
12
|
+
*/
|
|
13
|
+
export const expandChipsToFitPins = (problem: InputProblem) => {
|
|
14
|
+
for (const chip of problem.chips) {
|
|
15
|
+
const halfWidth = chip.width / 2
|
|
16
|
+
const halfHeight = chip.height / 2
|
|
17
|
+
|
|
18
|
+
let maxDx = 0
|
|
19
|
+
let maxDy = 0
|
|
20
|
+
|
|
21
|
+
for (const pin of chip.pins) {
|
|
22
|
+
const dx = Math.abs(pin.x - chip.center.x)
|
|
23
|
+
const dy = Math.abs(pin.y - chip.center.y)
|
|
24
|
+
if (dx > maxDx) maxDx = dx
|
|
25
|
+
if (dy > maxDy) maxDy = dy
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const newHalfWidth = Math.max(halfWidth, maxDx)
|
|
29
|
+
const newHalfHeight = Math.max(halfHeight, maxDy)
|
|
30
|
+
|
|
31
|
+
if (newHalfWidth > halfWidth || newHalfHeight > halfHeight) {
|
|
32
|
+
chip.width = newHalfWidth * 2
|
|
33
|
+
chip.height = newHalfHeight * 2
|
|
34
|
+
|
|
35
|
+
// Clear any cached facing direction since geometry changed.
|
|
36
|
+
for (const pin of chip.pins) {
|
|
37
|
+
pin._facingDirection = undefined
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED