@tscircuit/schematic-trace-solver 0.0.7 → 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 +425 -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/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
|
+
}
|
package/package.json
CHANGED