@tscircuit/schematic-trace-solver 0.0.81 → 0.0.83

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.
@@ -146,6 +146,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
146
146
  candidate: EvaluatedCandidate,
147
147
  labelIndex: number,
148
148
  ) {
149
+ if (candidate.phase === "trace-anchor") return
150
+
149
151
  const tracePath = this.getCandidateConnectorTrace(label, candidate)
150
152
  if (tracePath.length < 2) return
151
153
 
@@ -187,6 +189,13 @@ export class AvailableNetOrientationSolver extends BaseSolver {
187
189
  )
188
190
  if (rotatedCandidate) return rotatedCandidate
189
191
 
192
+ const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
193
+ label,
194
+ orientations[0]!,
195
+ labelIndex,
196
+ )
197
+ if (traceAnchorCandidate) return traceAnchorCandidate
198
+
190
199
  const shiftedCandidate = this.findValidShiftedCandidate(
191
200
  label,
192
201
  orientations[0]!,
@@ -228,6 +237,57 @@ export class AvailableNetOrientationSolver extends BaseSolver {
228
237
  return null
229
238
  }
230
239
 
240
+ private findValidTraceAnchorCandidate(
241
+ label: NetLabelPlacement,
242
+ orientation: FacingDirection,
243
+ labelIndex: number,
244
+ ) {
245
+ const direction = dir(orientation)
246
+ const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
247
+ (a, b) => {
248
+ const aAlongDirection = a.x * direction.x + a.y * direction.y
249
+ const bAlongDirection = b.x * direction.x + b.y * direction.y
250
+ return bAlongDirection - aAlongDirection
251
+ },
252
+ )
253
+
254
+ for (const anchorPoint of candidatePoints) {
255
+ const candidate = this.createCandidate(label, anchorPoint, orientation)
256
+ const result = this.evaluateCandidate(
257
+ candidate,
258
+ label,
259
+ labelIndex,
260
+ "trace-anchor",
261
+ )
262
+ this.currentCandidateResults.push(result)
263
+
264
+ if (result.status === "valid") {
265
+ result.selected = true
266
+ return result
267
+ }
268
+ }
269
+
270
+ return null
271
+ }
272
+
273
+ private getTraceAnchorCandidatePoints(label: NetLabelPlacement) {
274
+ const seen = new Set<string>()
275
+ const points: Point[] = []
276
+
277
+ for (const traceId of label.mspConnectionPairIds ?? []) {
278
+ const trace = this.traceMap[traceId]
279
+ if (!trace) continue
280
+ for (const point of trace.tracePath) {
281
+ const key = `${point.x.toFixed(9)},${point.y.toFixed(9)}`
282
+ if (seen.has(key)) continue
283
+ seen.add(key)
284
+ points.push(point)
285
+ }
286
+ }
287
+
288
+ return points
289
+ }
290
+
231
291
  private getAvailableOrientations(label: NetLabelPlacement) {
232
292
  const effectiveNetId = label.netId ?? label.globalConnNetId
233
293
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
@@ -608,16 +668,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
608
668
  phase: CandidatePhase
609
669
  }) {
610
670
  const { candidate, label, labelIndex, phase } = params
671
+ const bounds = getRectBounds(
672
+ candidate.center,
673
+ candidate.width,
674
+ candidate.height,
675
+ )
611
676
  const boundsStatus = this.getBoundsStatus({
612
- bounds: getRectBounds(
613
- candidate.center,
614
- candidate.width,
615
- candidate.height,
616
- ),
677
+ bounds,
617
678
  labelIndex,
618
679
  label,
619
680
  })
620
- if (boundsStatus !== "valid") return boundsStatus
681
+ if (boundsStatus !== "valid") {
682
+ if (
683
+ phase !== "trace-anchor" ||
684
+ boundsStatus !== "chip-collision" ||
685
+ !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)
686
+ ) {
687
+ return boundsStatus
688
+ }
689
+
690
+ const nonChipBoundsStatus = this.getBoundsStatus({
691
+ bounds,
692
+ labelIndex,
693
+ label,
694
+ ignoreChipCollisions: true,
695
+ })
696
+ if (nonChipBoundsStatus !== "valid") return nonChipBoundsStatus
697
+ }
621
698
 
622
699
  const connectorTrace = this.getCandidateConnectorTrace(label, {
623
700
  anchorPoint: candidate.anchorPoint,
@@ -647,22 +724,59 @@ export class AvailableNetOrientationSolver extends BaseSolver {
647
724
  return "valid"
648
725
  }
649
726
 
727
+ private isAcceptableTraceAnchorChipCollision(
728
+ candidate: CandidateLabel,
729
+ label: NetLabelPlacement,
730
+ bounds: Bounds,
731
+ ) {
732
+ const collidingChips =
733
+ this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
734
+ if (collidingChips.length === 0) return false
735
+
736
+ const labelChipIds = new Set(
737
+ label.pinIds
738
+ .map((pinId) => this.pinMap[pinId]?.chipId)
739
+ .filter((chipId): chipId is string => Boolean(chipId)),
740
+ )
741
+
742
+ return collidingChips.every((chip) => {
743
+ if (!labelChipIds.has(chip.chipId)) return false
744
+
745
+ const { anchorPoint, orientation } = candidate
746
+ const chipBounds = chip.bounds
747
+ if (isYOrientation(orientation)) {
748
+ return (
749
+ anchorPoint.x < chipBounds.minX - EPS ||
750
+ anchorPoint.x > chipBounds.maxX + EPS
751
+ )
752
+ }
753
+ return (
754
+ anchorPoint.y < chipBounds.minY - EPS ||
755
+ anchorPoint.y > chipBounds.maxY + EPS
756
+ )
757
+ })
758
+ }
759
+
650
760
  private getBoundsStatus(candidateBoundsCheck: {
651
761
  bounds: Bounds
652
762
  labelIndex: number
653
763
  label: NetLabelPlacement
764
+ ignoreChipCollisions?: boolean
654
765
  }): CandidateStatus {
655
- const { bounds, labelIndex, label } = candidateBoundsCheck
766
+ const { bounds, labelIndex, label, ignoreChipCollisions } =
767
+ candidateBoundsCheck
656
768
 
657
- if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
658
- return "chip-collision"
769
+ if (!ignoreChipCollisions) {
770
+ if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
771
+ return "chip-collision"
772
+ }
773
+ if (this.sharesChipBoundary(bounds)) {
774
+ return "chip-collision"
775
+ }
659
776
  }
660
777
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
661
778
  return "text-collision"
662
779
  }
663
- if (this.sharesChipBoundary(bounds)) {
664
- return "chip-collision"
665
- }
666
780
  if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
667
781
  return "trace-collision"
668
782
  }
@@ -35,7 +35,11 @@ export type CandidateStatus =
35
35
  | "trace-clearance-violation"
36
36
  | "netlabel-collision"
37
37
 
38
- export type CandidatePhase = "rotate" | "shift" | "lateral-shift"
38
+ export type CandidatePhase =
39
+ | "rotate"
40
+ | "trace-anchor"
41
+ | "shift"
42
+ | "lateral-shift"
39
43
 
40
44
  export type EvaluatedCandidate = CandidateLabel & {
41
45
  status: CandidateStatus
@@ -1,18 +1,27 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
+ import {
3
+ getPinMap,
4
+ getTracePins,
5
+ } from "lib/solvers/AvailableNetOrientationSolver/traces"
2
6
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
7
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
- import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
8
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
9
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
6
10
  import {
7
11
  detectTraceLabelOverlap,
8
12
  type TraceLabelOverlap,
9
13
  } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
10
- import type { InputProblem } from "lib/types/InputProblem"
14
+ import type { InputPin, InputProblem } from "lib/types/InputProblem"
15
+ import { dir } from "lib/utils/dir"
11
16
  import { moveAttachedLabelsToReroutedTrace } from "./labelMovement"
12
17
  import { findBestReroutePath } from "./reroute"
13
18
  import type { Example28SolverParams, RerouteCandidateResult } from "./types"
14
19
  import { visualizeExample28Solver } from "./visualize"
15
20
 
21
+ const LABEL_OUTWARD_STEP = 0.1
22
+ const LABEL_MAX_OUTWARD_STEPS = 10
23
+ const LABEL_TRACE_CLEARANCE = 0.1
24
+
16
25
  export class Example28Solver extends BaseSolver {
17
26
  inputProblem: InputProblem
18
27
  traces: SolvedTracePath[]
@@ -25,6 +34,7 @@ export class Example28Solver extends BaseSolver {
25
34
  currentCandidateResults: RerouteCandidateResult[] = []
26
35
 
27
36
  private chipObstacles: ReturnType<typeof getObstacleRects>
37
+ private pinMap: Record<string, InputPin & { chipId: string }>
28
38
 
29
39
  constructor(params: Example28SolverParams) {
30
40
  super()
@@ -34,6 +44,7 @@ export class Example28Solver extends BaseSolver {
34
44
  this.outputTraces = [...params.traces]
35
45
  this.outputNetLabelPlacements = [...params.netLabelPlacements]
36
46
  this.chipObstacles = getObstacleRects(params.inputProblem)
47
+ this.pinMap = getPinMap(params.inputProblem)
37
48
  this.initializeQueuedOverlaps()
38
49
  this.currentOverlap = this.queuedOverlaps[0] ?? null
39
50
  }
@@ -144,6 +155,8 @@ export class Example28Solver extends BaseSolver {
144
155
  if (traceIndex === -1) return
145
156
 
146
157
  const currentTrace = this.outputTraces[traceIndex]!
158
+ if (this.tryMoveLabelOutward(overlap.label)) return
159
+
147
160
  const rerouteResult = findBestReroutePath({
148
161
  trace: currentTrace,
149
162
  obstacleLabel: overlap.label,
@@ -169,6 +182,79 @@ export class Example28Solver extends BaseSolver {
169
182
  })
170
183
  }
171
184
 
185
+ private tryMoveLabelOutward(labelToMove: NetLabelPlacement) {
186
+ const labelIndex = this.outputNetLabelPlacements.findIndex(
187
+ (label) =>
188
+ label.globalConnNetId === labelToMove.globalConnNetId &&
189
+ label.anchorPoint.x === labelToMove.anchorPoint.x &&
190
+ label.anchorPoint.y === labelToMove.anchorPoint.y &&
191
+ label.pinIds.join(",") === labelToMove.pinIds.join(","),
192
+ )
193
+ if (labelIndex === -1) return false
194
+
195
+ const label = this.outputNetLabelPlacements[labelIndex]!
196
+ if (label.mspConnectionPairIds.length > 0) return false
197
+ if (label.pinIds.length !== 1) return false
198
+ if (
199
+ this.outputNetLabelPlacements.filter(
200
+ (otherLabel) => otherLabel.globalConnNetId === label.globalConnNetId,
201
+ ).length !== 1
202
+ ) {
203
+ return false
204
+ }
205
+
206
+ const outward = dir(label.orientation)
207
+ if (outward.x === 0 && outward.y === 0) return false
208
+
209
+ for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
210
+ const distance = step * LABEL_OUTWARD_STEP
211
+ const candidate = {
212
+ ...label,
213
+ anchorPoint: {
214
+ x: label.anchorPoint.x + outward.x * distance,
215
+ y: label.anchorPoint.y + outward.y * distance,
216
+ },
217
+ center: {
218
+ x: label.center.x + outward.x * distance,
219
+ y: label.center.y + outward.y * distance,
220
+ },
221
+ }
222
+ const candidateWithClearance = {
223
+ ...candidate,
224
+ width: candidate.width + LABEL_TRACE_CLEARANCE * 2,
225
+ height: candidate.height + LABEL_TRACE_CLEARANCE * 2,
226
+ }
227
+ if (
228
+ detectTraceLabelOverlap({
229
+ traces: this.outputTraces,
230
+ netLabels: [candidateWithClearance],
231
+ }).length > 0
232
+ ) {
233
+ continue
234
+ }
235
+
236
+ const connectorTrace = createPortOnlyLabelConnectorTrace({
237
+ label,
238
+ movedLabel: candidate,
239
+ pinMap: this.pinMap,
240
+ })
241
+ if (
242
+ detectTraceLabelOverlap({
243
+ traces: [connectorTrace],
244
+ netLabels: this.outputNetLabelPlacements,
245
+ }).length > 0
246
+ ) {
247
+ continue
248
+ }
249
+
250
+ this.outputNetLabelPlacements[labelIndex] = candidate
251
+ this.outputTraces.push(connectorTrace)
252
+ return true
253
+ }
254
+
255
+ return false
256
+ }
257
+
172
258
  override visualize(): GraphicsObject {
173
259
  return visualizeExample28Solver({
174
260
  inputProblem: this.inputProblem,
@@ -180,3 +266,29 @@ export class Example28Solver extends BaseSolver {
180
266
  })
181
267
  }
182
268
  }
269
+
270
+ const getPortOnlyLabelConnectorMspPairId = (label: NetLabelPlacement) =>
271
+ `port-only-label-connector-${label.globalConnNetId}-${label.pinIds[0]}`
272
+
273
+ const createPortOnlyLabelConnectorTrace = ({
274
+ label,
275
+ movedLabel,
276
+ pinMap,
277
+ }: {
278
+ label: NetLabelPlacement
279
+ movedLabel: NetLabelPlacement
280
+ pinMap: Record<string, InputPin & { chipId: string }>
281
+ }): SolvedTracePath => {
282
+ const mspPairId = getPortOnlyLabelConnectorMspPairId(label)
283
+
284
+ return {
285
+ mspPairId,
286
+ dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
287
+ globalConnNetId: label.globalConnNetId,
288
+ userNetId: label.netId,
289
+ pins: getTracePins(label, pinMap),
290
+ tracePath: [label.anchorPoint, movedLabel.anchorPoint],
291
+ mspConnectionPairIds: [mspPairId],
292
+ pinIds: label.pinIds,
293
+ }
294
+ }
@@ -20,8 +20,7 @@ import type {
20
20
  TracePathScore,
21
21
  } from "./types"
22
22
 
23
- const LABEL_SIDE_CLEARANCE = 0.1
24
- const LABEL_HUG_CLEARANCE = 0.001
23
+ const LABEL_CLEARANCE = 0.1
25
24
 
26
25
  export const findBestReroutePath = ({
27
26
  trace,
@@ -102,7 +101,7 @@ export const generateRerouteCandidateResults = ({
102
101
  trace,
103
102
  label,
104
103
  problem: inputProblem,
105
- paddingBuffer: LABEL_SIDE_CLEARANCE,
104
+ paddingBuffer: LABEL_CLEARANCE,
106
105
  detourCount: 0,
107
106
  }),
108
107
  ...generateEndpointDetourCandidates(trace, label),
@@ -188,9 +187,11 @@ const generateEndpointDetourCandidates = (
188
187
  const start = trace.tracePath[0]
189
188
  const end = trace.tracePath[trace.tracePath.length - 1]
190
189
  if (!start || !end) return []
190
+ const startExit = trace.tracePath[1] ?? start
191
+ const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end
191
192
 
192
193
  const bounds = getRectBounds(label.center, label.width, label.height)
193
- const padding = LABEL_SIDE_CLEARANCE
194
+ const padding = LABEL_CLEARANCE
194
195
  const labelDirection = dir(label.orientation)
195
196
  const labelSideX =
196
197
  labelDirection.x < 0 ? bounds.minX - padding : bounds.maxX + padding
@@ -204,18 +205,22 @@ const generateEndpointDetourCandidates = (
204
205
  candidates.push(
205
206
  [
206
207
  start,
207
- { x: start.x, y: topY },
208
+ startExit,
209
+ { x: startExit.x, y: topY },
208
210
  { x: sideX, y: topY },
209
211
  { x: sideX, y: bottomY },
210
- { x: end.x, y: bottomY },
212
+ { x: endEntry.x, y: bottomY },
213
+ endEntry,
211
214
  end,
212
215
  ],
213
216
  [
214
217
  start,
215
- { x: start.x, y: bottomY },
218
+ startExit,
219
+ { x: startExit.x, y: bottomY },
216
220
  { x: sideX, y: bottomY },
217
221
  { x: sideX, y: topY },
218
- { x: end.x, y: topY },
222
+ { x: endEntry.x, y: topY },
223
+ endEntry,
219
224
  end,
220
225
  ],
221
226
  )
@@ -231,6 +236,8 @@ const generateLabelHugCandidates = (
231
236
  const start = trace.tracePath[0]
232
237
  const end = trace.tracePath[trace.tracePath.length - 1]
233
238
  if (!start || !end) return []
239
+ const startExit = trace.tracePath[1] ?? start
240
+ const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end
234
241
 
235
242
  const labelDirection = dir(label.orientation)
236
243
  if (labelDirection.x === 0) return []
@@ -238,29 +245,33 @@ const generateLabelHugCandidates = (
238
245
  const bounds = getRectBounds(label.center, label.width, label.height)
239
246
  const sideX =
240
247
  labelDirection.x < 0
241
- ? bounds.minX - LABEL_SIDE_CLEARANCE
242
- : bounds.maxX + LABEL_SIDE_CLEARANCE
243
- const topY = bounds.maxY + LABEL_HUG_CLEARANCE
244
- const bottomY = bounds.minY - LABEL_HUG_CLEARANCE
245
- const startsAboveEnd = start.y >= end.y
248
+ ? bounds.minX - LABEL_CLEARANCE
249
+ : bounds.maxX + LABEL_CLEARANCE
250
+ const topY = bounds.maxY + LABEL_CLEARANCE
251
+ const bottomY = bounds.minY - LABEL_CLEARANCE
252
+ const startsAboveEnd = startExit.y >= endEntry.y
246
253
  const firstY = startsAboveEnd ? topY : bottomY
247
254
  const secondY = startsAboveEnd ? bottomY : topY
248
255
 
249
256
  return [
250
257
  [
251
258
  start,
252
- { x: start.x, y: firstY },
259
+ startExit,
260
+ { x: startExit.x, y: firstY },
253
261
  { x: sideX, y: firstY },
254
262
  { x: sideX, y: secondY },
255
- { x: end.x, y: secondY },
263
+ { x: endEntry.x, y: secondY },
264
+ endEntry,
256
265
  end,
257
266
  ],
258
267
  [
259
268
  start,
260
- { x: start.x, y: secondY },
269
+ startExit,
270
+ { x: startExit.x, y: secondY },
261
271
  { x: sideX, y: secondY },
262
272
  { x: sideX, y: firstY },
263
- { x: end.x, y: firstY },
273
+ { x: endEntry.x, y: firstY },
274
+ endEntry,
264
275
  end,
265
276
  ],
266
277
  ]
@@ -341,9 +352,9 @@ const generateHorizontalSegmentPushCandidate = (
341
352
  return null
342
353
  }
343
354
 
344
- let segmentPushX = bounds.minX - LABEL_SIDE_CLEARANCE
355
+ let segmentPushX = bounds.minX - LABEL_CLEARANCE
345
356
  if (labelDirection.x > 0) {
346
- segmentPushX = bounds.maxX + LABEL_SIDE_CLEARANCE
357
+ segmentPushX = bounds.maxX + LABEL_CLEARANCE
347
358
  }
348
359
  const segmentPushStartY = getClearedHorizontalY({
349
360
  start: previousAnchor,
@@ -393,10 +404,10 @@ const getClearedHorizontalY = ({
393
404
 
394
405
  const labelCenterY = (labelBounds.minY + labelBounds.maxY) / 2
395
406
  if (start.y >= labelCenterY) {
396
- return labelBounds.maxY + LABEL_HUG_CLEARANCE
407
+ return labelBounds.maxY + LABEL_CLEARANCE
397
408
  }
398
409
 
399
- return labelBounds.minY - LABEL_HUG_CLEARANCE
410
+ return labelBounds.minY - LABEL_CLEARANCE
400
411
  }
401
412
 
402
413
  const markSelectedCandidate = (
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.81",
4
+ "version": "0.0.83",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",