@tscircuit/schematic-trace-solver 0.0.61 → 0.0.62

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,441 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import type { FacingDirection } from "lib/utils/dir"
8
+ import {
9
+ getDimsForOrientation,
10
+ getCenterFromAnchor,
11
+ getRectBounds,
12
+ } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
13
+ import { rectIntersectsAnyTrace } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
14
+ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
15
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
16
+ import { getColorFromString } from "lib/utils/getColorFromString"
17
+
18
+ type CandidateStatus =
19
+ | "ok"
20
+ | "chip-collision"
21
+ | "trace-collision"
22
+ | "label-collision"
23
+
24
+ const ANCHOR_TRACE_CLEARANCE = 1e-4
25
+ const SEGMENT_PARALLEL_EPS = 1e-6
26
+ const CANDIDATE_STEP = 0.1
27
+
28
+ const OUTWARD_DIR: Record<FacingDirection, { x: number; y: number }> = {
29
+ "x+": { x: 1, y: 0 },
30
+ "x-": { x: -1, y: 0 },
31
+ "y+": { x: 0, y: 1 },
32
+ "y-": { x: 0, y: -1 },
33
+ }
34
+
35
+ const CANDIDATE_STATUS_COLOR: Record<CandidateStatus, string> = {
36
+ ok: "green",
37
+ "label-collision": "orange",
38
+ "trace-collision": "darkorange",
39
+ "chip-collision": "red",
40
+ }
41
+
42
+ const CANDIDATE_STATUS_FILL: Record<CandidateStatus, string> = {
43
+ ok: "rgba(0, 200, 0, 0.25)",
44
+ "label-collision": "rgba(255, 160, 0, 0.2)",
45
+ "trace-collision": "rgba(200, 80, 0, 0.2)",
46
+ "chip-collision": "rgba(220, 0, 0, 0.15)",
47
+ }
48
+
49
+ type Candidate = {
50
+ orientation: FacingDirection
51
+ anchor: { x: number; y: number }
52
+ center: { x: number; y: number }
53
+ width: number
54
+ height: number
55
+ bounds: { minX: number; minY: number; maxX: number; maxY: number }
56
+ hostPairId?: MspConnectionPairId
57
+ hostSegIndex?: number
58
+ status: CandidateStatus | null
59
+ }
60
+
61
+ function boundsOverlap(
62
+ a: { minX: number; minY: number; maxX: number; maxY: number },
63
+ b: { minX: number; minY: number; maxX: number; maxY: number },
64
+ ): boolean {
65
+ return (
66
+ a.minX < b.maxX - 1e-9 &&
67
+ a.maxX > b.minX + 1e-9 &&
68
+ a.minY < b.maxY - 1e-9 &&
69
+ a.maxY > b.minY + 1e-9
70
+ )
71
+ }
72
+
73
+ function sampleAnchorsAlongSegment(
74
+ a: { x: number; y: number },
75
+ b: { x: number; y: number },
76
+ ): Array<{ x: number; y: number }> {
77
+ const dx = b.x - a.x
78
+ const dy = b.y - a.y
79
+ const len = Math.sqrt(dx * dx + dy * dy)
80
+ const steps = Math.max(1, Math.round(len / CANDIDATE_STEP))
81
+ const anchors: Array<{ x: number; y: number }> = []
82
+ for (let k = 0; k <= steps; k++) {
83
+ const t = k / steps
84
+ anchors.push({ x: a.x + t * dx, y: a.y + t * dy })
85
+ }
86
+ return anchors
87
+ }
88
+
89
+ export interface NetLabelNetLabelCollisionSolverParams {
90
+ inputProblem: InputProblem
91
+ traces: SolvedTracePath[]
92
+ netLabelPlacements: NetLabelPlacement[]
93
+ }
94
+
95
+ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
96
+ inputProblem: InputProblem
97
+ traces: SolvedTracePath[]
98
+ netLabelPlacements: NetLabelPlacement[]
99
+
100
+ outputNetLabelPlacements: NetLabelPlacement[]
101
+
102
+ currentCollision: [NetLabelPlacement, NetLabelPlacement] | null = null
103
+ currentLabelToMove: NetLabelPlacement | null = null
104
+ candidateResults: Candidate[] = []
105
+
106
+ private chipIndex: ChipObstacleSpatialIndex
107
+ private traceMap: Record<MspConnectionPairId, SolvedTracePath>
108
+ private skippedCollisionKeys = new Set<string>()
109
+
110
+ private labelsToTry: NetLabelPlacement[] = []
111
+ private candidateQueue: Candidate[] = []
112
+ private candidateIndex = 0
113
+
114
+ constructor(params: NetLabelNetLabelCollisionSolverParams) {
115
+ super()
116
+ this.inputProblem = params.inputProblem
117
+ this.traces = params.traces
118
+ this.netLabelPlacements = params.netLabelPlacements
119
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
120
+ this.chipIndex =
121
+ params.inputProblem._chipObstacleSpatialIndex ??
122
+ new ChipObstacleSpatialIndex(params.inputProblem.chips)
123
+ this.traceMap = Object.fromEntries(
124
+ params.traces.map((t) => [t.mspPairId, t]),
125
+ ) as Record<MspConnectionPairId, SolvedTracePath>
126
+ }
127
+
128
+ override getConstructorParams(): ConstructorParameters<
129
+ typeof NetLabelNetLabelCollisionSolver
130
+ >[0] {
131
+ return {
132
+ inputProblem: this.inputProblem,
133
+ traces: this.traces,
134
+ netLabelPlacements: this.netLabelPlacements,
135
+ }
136
+ }
137
+
138
+ getOutput() {
139
+ return { netLabelPlacements: this.outputNetLabelPlacements }
140
+ }
141
+
142
+ private labelBounds(label: NetLabelPlacement) {
143
+ return getRectBounds(label.center, label.width, label.height)
144
+ }
145
+
146
+ private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) {
147
+ return [a.globalConnNetId, b.globalConnNetId].sort().join("::")
148
+ }
149
+
150
+ private findNextCollidingPair():
151
+ | [NetLabelPlacement, NetLabelPlacement]
152
+ | null {
153
+ const labels = this.outputNetLabelPlacements
154
+ for (let i = 0; i < labels.length; i++) {
155
+ for (let j = i + 1; j < labels.length; j++) {
156
+ const a = labels[i]!
157
+ const b = labels[j]!
158
+ if (a.globalConnNetId === b.globalConnNetId) continue
159
+ if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue
160
+ if (boundsOverlap(this.labelBounds(a), this.labelBounds(b)))
161
+ return [a, b]
162
+ }
163
+ }
164
+ return null
165
+ }
166
+
167
+ private netLabelWidthOf(label: NetLabelPlacement): number | undefined {
168
+ if (label.orientation === "x+" || label.orientation === "x-")
169
+ return label.width
170
+ return label.height
171
+ }
172
+
173
+ private buildCandidatesForLabel(label: NetLabelPlacement): Candidate[] {
174
+ const netLabelWidth = this.netLabelWidthOf(label)
175
+ const candidates: Candidate[] = []
176
+
177
+ const buildCandidate = (
178
+ orientation: FacingDirection,
179
+ anchor: { x: number; y: number },
180
+ hostPairId?: MspConnectionPairId,
181
+ hostSegIndex?: number,
182
+ ): Candidate => {
183
+ const { width, height } = getDimsForOrientation({
184
+ orientation,
185
+ netLabelWidth,
186
+ })
187
+ const baseCenter = getCenterFromAnchor(anchor, orientation, width, height)
188
+ const outwardDir = OUTWARD_DIR[orientation]
189
+ const center = {
190
+ x: baseCenter.x + outwardDir.x * ANCHOR_TRACE_CLEARANCE,
191
+ y: baseCenter.y + outwardDir.y * ANCHOR_TRACE_CLEARANCE,
192
+ }
193
+ return {
194
+ orientation,
195
+ anchor,
196
+ center,
197
+ width,
198
+ height,
199
+ bounds: getRectBounds(center, width, height),
200
+ hostPairId,
201
+ hostSegIndex,
202
+ status: null,
203
+ }
204
+ }
205
+
206
+ const isPortOnly = label.mspConnectionPairIds.length === 0
207
+
208
+ if (isPortOnly) {
209
+ const allOrientations: FacingDirection[] = ["x+", "x-", "y+", "y-"]
210
+ const orderedOrientations = [
211
+ label.orientation,
212
+ ...allOrientations.filter((o) => o !== label.orientation),
213
+ ]
214
+ for (const orientation of orderedOrientations) {
215
+ candidates.push(buildCandidate(orientation, label.anchorPoint))
216
+ }
217
+ } else {
218
+ for (const mspPairId of label.mspConnectionPairIds) {
219
+ const trace = this.traceMap[mspPairId]
220
+ if (!trace) continue
221
+ const pts = trace.tracePath
222
+ for (let si = 0; si < pts.length - 1; si++) {
223
+ const segStart = pts[si]!
224
+ const segEnd = pts[si + 1]!
225
+ const isHorizontal =
226
+ Math.abs(segStart.y - segEnd.y) < SEGMENT_PARALLEL_EPS
227
+ const isVertical =
228
+ Math.abs(segStart.x - segEnd.x) < SEGMENT_PARALLEL_EPS
229
+ if (!isHorizontal && !isVertical) continue
230
+ let perpendicularOrientations: FacingDirection[]
231
+ if (isHorizontal) {
232
+ perpendicularOrientations = ["y+", "y-"]
233
+ } else {
234
+ perpendicularOrientations = ["x+", "x-"]
235
+ }
236
+ for (const anchor of sampleAnchorsAlongSegment(segStart, segEnd)) {
237
+ for (const orientation of perpendicularOrientations) {
238
+ candidates.push(
239
+ buildCandidate(
240
+ orientation,
241
+ anchor,
242
+ mspPairId as MspConnectionPairId,
243
+ si,
244
+ ),
245
+ )
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ return candidates
253
+ }
254
+
255
+ private checkCandidate(
256
+ candidate: Candidate,
257
+ movingLabelNetId: string,
258
+ obstacleLabels: NetLabelPlacement[],
259
+ ): CandidateStatus {
260
+ const { bounds, hostPairId, hostSegIndex } = candidate
261
+
262
+ if (this.chipIndex.getChipsInBounds(bounds).length > 0)
263
+ return "chip-collision"
264
+
265
+ if (
266
+ rectIntersectsAnyTrace(bounds, this.traceMap, hostPairId, hostSegIndex)
267
+ .hasIntersection
268
+ ) {
269
+ return "trace-collision"
270
+ }
271
+
272
+ for (const obstacle of obstacleLabels) {
273
+ if (obstacle.globalConnNetId === movingLabelNetId) continue
274
+ if (boundsOverlap(bounds, this.labelBounds(obstacle)))
275
+ return "label-collision"
276
+ }
277
+
278
+ return "ok"
279
+ }
280
+
281
+ private beginSearchForLabel(label: NetLabelPlacement) {
282
+ this.currentLabelToMove = label
283
+ this.candidateQueue = this.buildCandidatesForLabel(label)
284
+ this.candidateIndex = 0
285
+ this.candidateResults = []
286
+ }
287
+
288
+ private clearActiveSearch() {
289
+ this.currentCollision = null
290
+ this.currentLabelToMove = null
291
+ this.labelsToTry = []
292
+ this.candidateQueue = []
293
+ this.candidateIndex = 0
294
+ this.candidateResults = []
295
+ }
296
+
297
+ override _step() {
298
+ if (!this.currentCollision) {
299
+ const pair = this.findNextCollidingPair()
300
+ if (!pair) {
301
+ this.solved = true
302
+ return
303
+ }
304
+ this.currentCollision = pair
305
+ this.labelsToTry = [pair[1], pair[0]]
306
+ this.beginSearchForLabel(this.labelsToTry.shift()!)
307
+ return
308
+ }
309
+
310
+ if (this.candidateIndex >= this.candidateQueue.length) {
311
+ if (this.labelsToTry.length > 0) {
312
+ this.beginSearchForLabel(this.labelsToTry.shift()!)
313
+ } else {
314
+ this.skippedCollisionKeys.add(
315
+ this.collisionKey(this.currentCollision[0], this.currentCollision[1]),
316
+ )
317
+ this.clearActiveSearch()
318
+ }
319
+ return
320
+ }
321
+
322
+ const candidate = this.candidateQueue[this.candidateIndex++]!
323
+ const [labelA, labelB] = this.currentCollision
324
+ let fixedLabel: NetLabelPlacement
325
+ if (this.currentLabelToMove === labelB) {
326
+ fixedLabel = labelA
327
+ } else {
328
+ fixedLabel = labelB
329
+ }
330
+ const obstacleLabels = [
331
+ ...this.outputNetLabelPlacements.filter(
332
+ (l) => l !== labelA && l !== labelB,
333
+ ),
334
+ fixedLabel,
335
+ ]
336
+
337
+ const status = this.checkCandidate(
338
+ candidate,
339
+ this.currentLabelToMove!.globalConnNetId,
340
+ obstacleLabels,
341
+ )
342
+ candidate.status = status
343
+ this.candidateResults.push({ ...candidate })
344
+
345
+ if (status === "ok") {
346
+ const idx = this.outputNetLabelPlacements.indexOf(
347
+ this.currentLabelToMove!,
348
+ )
349
+ if (idx !== -1) {
350
+ this.outputNetLabelPlacements[idx] = {
351
+ ...this.currentLabelToMove!,
352
+ orientation: candidate.orientation,
353
+ anchorPoint: candidate.anchor,
354
+ width: candidate.width,
355
+ height: candidate.height,
356
+ center: candidate.center,
357
+ }
358
+ }
359
+ this.clearActiveSearch()
360
+ }
361
+ }
362
+
363
+ override visualize(): GraphicsObject {
364
+ const graphics = visualizeInputProblem(this.inputProblem)
365
+ if (!graphics.lines) graphics.lines = []
366
+ if (!graphics.rects) graphics.rects = []
367
+ if (!graphics.points) graphics.points = []
368
+
369
+ for (const trace of this.traces) {
370
+ graphics.lines.push({
371
+ points: trace.tracePath,
372
+ strokeColor: "purple",
373
+ } as any)
374
+ }
375
+
376
+ for (const label of this.outputNetLabelPlacements) {
377
+ const isInActiveCollision =
378
+ this.currentCollision != null &&
379
+ (label === this.currentCollision[0] ||
380
+ label === this.currentCollision[1])
381
+
382
+ let labelFill: string
383
+ let labelStroke: string
384
+ let labelText: string
385
+ let pointColor: string
386
+ if (isInActiveCollision) {
387
+ labelFill = "rgba(255, 0, 0, 0.2)"
388
+ labelStroke = "red"
389
+ labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}\n⚠ COLLIDING`
390
+ pointColor = "red"
391
+ } else {
392
+ labelFill = getColorFromString(label.globalConnNetId, 0.35)
393
+ labelStroke = getColorFromString(label.globalConnNetId, 0.9)
394
+ labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`
395
+ pointColor = getColorFromString(label.globalConnNetId, 0.9)
396
+ }
397
+
398
+ graphics.rects.push({
399
+ center: label.center,
400
+ width: label.width,
401
+ height: label.height,
402
+ fill: labelFill,
403
+ strokeColor: labelStroke,
404
+ label: labelText,
405
+ } as any)
406
+ graphics.points.push({
407
+ x: label.anchorPoint.x,
408
+ y: label.anchorPoint.y,
409
+ color: pointColor,
410
+ label: `anchorPoint\norientation: ${label.orientation}`,
411
+ } as any)
412
+ }
413
+
414
+ const movingNetId = this.currentLabelToMove
415
+ ? this.currentLabelToMove.netId
416
+ : "?"
417
+ for (const c of this.candidateResults) {
418
+ const statusColor = CANDIDATE_STATUS_COLOR[c.status!]
419
+ const statusFill = CANDIDATE_STATUS_FILL[c.status!]
420
+ let strokeDash: string | undefined
421
+ if (c.status !== "ok") strokeDash = "4 2"
422
+ graphics.rects.push({
423
+ center: c.center,
424
+ width: c.width,
425
+ height: c.height,
426
+ fill: statusFill,
427
+ strokeColor: statusColor,
428
+ strokeDash,
429
+ label: `candidate: ${c.status}\norientation: ${c.orientation}\nmoving: ${movingNetId}`,
430
+ } as any)
431
+ graphics.points.push({
432
+ x: c.anchor.x,
433
+ y: c.anchor.y,
434
+ color: statusColor,
435
+ label: `candidate anchor\n${c.status}`,
436
+ } as any)
437
+ }
438
+
439
+ return graphics
440
+ }
441
+ }
@@ -26,6 +26,7 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/
26
26
  import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
27
27
  import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
28
28
  import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
29
+ import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
29
30
 
30
31
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
31
32
  solverName: string
@@ -80,6 +81,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
80
81
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
81
82
  traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
82
83
  netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
84
+ netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
83
85
 
84
86
  startTimeOfPhase: Record<string, number>
85
87
  endTimeOfPhase: Record<string, number>
@@ -300,6 +302,19 @@ export class SchematicTracePipelineSolver extends BaseSolver {
300
302
  },
301
303
  ],
302
304
  ),
305
+ definePipelineStep(
306
+ "netLabelNetLabelCollisionSolver",
307
+ NetLabelNetLabelCollisionSolver,
308
+ (instance) => [
309
+ {
310
+ inputProblem: instance.inputProblem,
311
+ traces: instance.netLabelTraceCollisionSolver!.getOutput().traces,
312
+ netLabelPlacements:
313
+ instance.netLabelTraceCollisionSolver!.getOutput()
314
+ .netLabelPlacements,
315
+ },
316
+ ],
317
+ ),
303
318
  ]
304
319
 
305
320
  constructor(inputProblem: InputProblem) {
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.61",
4
+ "version": "0.0.62",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example39.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example40.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example41.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,71 @@
1
+ {
2
+ "chips": [
3
+ {
4
+ "chipId": "schematic_component_0",
5
+ "center": {
6
+ "x": -6.5,
7
+ "y": -0.33
8
+ },
9
+ "width": 1.5,
10
+ "height": 0.8,
11
+ "pins": [
12
+ {
13
+ "pinId": "U3.24",
14
+ "x": -7.65,
15
+ "y": -0.22999999999999998
16
+ },
17
+ {
18
+ "pinId": "U3.25",
19
+ "x": -7.65,
20
+ "y": -0.43000000000000016
21
+ }
22
+ ],
23
+ "sectionId": "rp2040"
24
+ },
25
+ {
26
+ "chipId": "schematic_component_4",
27
+ "center": {
28
+ "x": -9.5,
29
+ "y": -0.4299999999999997
30
+ },
31
+ "width": 1.1,
32
+ "height": 0.388910699999999,
33
+ "pins": [
34
+ {
35
+ "pinId": "R5.1",
36
+ "x": -10.049999999999999,
37
+ "y": -0.4299999999999997
38
+ },
39
+ {
40
+ "pinId": "R5.2",
41
+ "x": -8.95,
42
+ "y": -0.4299999999999997
43
+ }
44
+ ],
45
+ "sectionId": "rp2040"
46
+ }
47
+ ],
48
+ "directConnections": [
49
+ {
50
+ "pinIds": ["U3.25", "R5.2"],
51
+ "netId": ".U3 > .pin25 to .R5 > .pin2"
52
+ }
53
+ ],
54
+ "netConnections": [
55
+ {
56
+ "netId": "SWCLK",
57
+ "pinIds": ["U3.24"],
58
+ "netLabelWidth": 0.72
59
+ },
60
+ {
61
+ "netId": "SWD",
62
+ "pinIds": ["U3.25", "R5.2"],
63
+ "netLabelWidth": 0.48
64
+ }
65
+ ],
66
+ "availableNetLabelOrientations": {
67
+ "SWCLK": ["x-", "x+"],
68
+ "SWD": ["x-", "x+"]
69
+ },
70
+ "maxMspPairDistance": 2.4
71
+ }