@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.
@@ -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
+ }
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.6",
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
  }