@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.
@@ -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,11 @@
1
+ export function anchorsForSegment(
2
+ a: { x: number; y: number },
3
+ b: { x: number; y: number },
4
+ ) {
5
+ // Start, midpoint, end
6
+ return [
7
+ { x: a.x, y: a.y },
8
+ { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
9
+ { x: b.x, y: b.y },
10
+ ]
11
+ }
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.7",
4
+ "version": "0.0.8",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -95,9 +95,9 @@ const inputProblem: InputProblem = {
95
95
  },
96
96
  ],
97
97
  availableNetLabelOrientations: {
98
- VCC: ["y+", "y-"],
98
+ VCC: ["y+"],
99
99
  EN: ["x+", "x-"],
100
- GND: ["y+", "y-"],
100
+ GND: ["y-"],
101
101
  },
102
102
  maxMspPairDistance: 2,
103
103
  }