@tscircuit/schematic-trace-solver 0.0.122 → 0.0.124

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.js CHANGED
@@ -5108,6 +5108,51 @@ var getVisibleTraceMetrics = (traces) => {
5108
5108
  var getVisibleTraceLength = (traces) => getVisibleTraceMetrics(traces).length;
5109
5109
  var getVisibleTraceSegmentCount = (traces) => getVisibleTraceMetrics(traces).segmentCount;
5110
5110
 
5111
+ // lib/solvers/TraceCleanupSolver/shortenExcessiveLabelPaddingDetour.ts
5112
+ var MIN_EXCESSIVE_DETOUR_LENGTH_RATIO = 2.5;
5113
+ var getTracePathLength = (tracePath) => {
5114
+ let pathLength = 0;
5115
+ for (let pointIndex = 1; pointIndex < tracePath.length; pointIndex++) {
5116
+ const previousPoint = tracePath[pointIndex - 1];
5117
+ const point = tracePath[pointIndex];
5118
+ pathLength += Math.abs(point.x - previousPoint.x) + Math.abs(point.y - previousPoint.y);
5119
+ }
5120
+ return pathLength;
5121
+ };
5122
+ var shortenExcessiveLabelPaddingDetour = ({
5123
+ inputProblem,
5124
+ trace
5125
+ }) => {
5126
+ const exactConnection = inputProblem.directConnections.find(
5127
+ (connection) => connection.pinIds.every((pinId) => trace.pinIds.includes(pinId))
5128
+ );
5129
+ if (!exactConnection) return trace;
5130
+ const chipMap = {};
5131
+ for (const chip of inputProblem.chips) chipMap[chip.chipId] = chip;
5132
+ const candidateSolver = new SchematicTraceSingleLineSolver2({
5133
+ pins: trace.pins,
5134
+ connectionPair: trace,
5135
+ inputProblem: {
5136
+ ...inputProblem,
5137
+ directConnections: [
5138
+ exactConnection,
5139
+ ...inputProblem.directConnections.filter(
5140
+ (connection) => connection !== exactConnection
5141
+ )
5142
+ ]
5143
+ },
5144
+ chipMap
5145
+ });
5146
+ candidateSolver.solve();
5147
+ if (!candidateSolver.solvedTracePath) return trace;
5148
+ const currentLength = getTracePathLength(trace.tracePath);
5149
+ const candidateLength = getTracePathLength(candidateSolver.solvedTracePath);
5150
+ if (currentLength <= candidateLength * MIN_EXCESSIVE_DETOUR_LENGTH_RATIO) {
5151
+ return trace;
5152
+ }
5153
+ return { ...trace, tracePath: candidateSolver.solvedTracePath };
5154
+ };
5155
+
5111
5156
  // lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts
5112
5157
  var minimizeTurnsWithFilteredLabels = ({
5113
5158
  targetMspConnectionPairId,
@@ -5117,12 +5162,16 @@ var minimizeTurnsWithFilteredLabels = ({
5117
5162
  mergedLabelNetIdMap,
5118
5163
  paddingBuffer
5119
5164
  }) => {
5120
- const targetTrace = traces.find(
5165
+ const originalTargetTrace = traces.find(
5121
5166
  (t) => t.mspPairId === targetMspConnectionPairId
5122
5167
  );
5123
- if (!targetTrace) {
5168
+ if (!originalTargetTrace) {
5124
5169
  throw new Error(`Target trace ${targetMspConnectionPairId} not found`);
5125
5170
  }
5171
+ const targetTrace = shortenExcessiveLabelPaddingDetour({
5172
+ inputProblem,
5173
+ trace: originalTargetTrace
5174
+ });
5126
5175
  const targetPinIds = new Set(targetTrace.pinIds);
5127
5176
  const otherTraces = traces.filter(
5128
5177
  (trace) => trace.mspPairId !== targetMspConnectionPairId
@@ -11181,6 +11230,7 @@ var UnroutedTraceRecoverySolver = class extends BaseSolver {
11181
11230
  };
11182
11231
 
11183
11232
  // lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel.ts
11233
+ var NET_LABEL_INTERIOR_INSET = 1e-6;
11184
11234
  var pathIntersectsAnyNetLabel = ({
11185
11235
  path,
11186
11236
  netLabelPlacements
@@ -11195,9 +11245,30 @@ var pathIntersectsAnyNetLabel = ({
11195
11245
  }
11196
11246
  return false;
11197
11247
  };
11248
+ var pathEntersAnyNetLabel = ({
11249
+ path,
11250
+ netLabelPlacements
11251
+ }) => {
11252
+ for (const label of netLabelPlacements) {
11253
+ const labelBounds = getRectBounds(label.center, label.width, label.height);
11254
+ const labelInteriorBounds = {
11255
+ minX: labelBounds.minX + NET_LABEL_INTERIOR_INSET,
11256
+ minY: labelBounds.minY + NET_LABEL_INTERIOR_INSET,
11257
+ maxX: labelBounds.maxX - NET_LABEL_INTERIOR_INSET,
11258
+ maxY: labelBounds.maxY - NET_LABEL_INTERIOR_INSET
11259
+ };
11260
+ for (let index = 0; index < path.length - 1; index++) {
11261
+ if (segmentIntersectsRect2(path[index], path[index + 1], labelInteriorBounds)) {
11262
+ return true;
11263
+ }
11264
+ }
11265
+ }
11266
+ return false;
11267
+ };
11198
11268
 
11199
11269
  // lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts
11200
11270
  var MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2;
11271
+ var MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2;
11201
11272
  var getSharedPin = ({
11202
11273
  donorTrace,
11203
11274
  branchTrace
@@ -11274,6 +11345,7 @@ var getAlignedBranchPath = ({
11274
11345
  };
11275
11346
  var candidateIsClear = ({
11276
11347
  candidateTrace,
11348
+ originalTrace,
11277
11349
  traces,
11278
11350
  inputProblem,
11279
11351
  netLabelPlacements
@@ -11288,10 +11360,34 @@ var candidateIsClear = ({
11288
11360
  if (doesPathCoincideWithTraces(candidateTrace.tracePath, otherNetTraces)) {
11289
11361
  return false;
11290
11362
  }
11291
- return !pathIntersectsAnyNetLabel({
11363
+ const otherNetLabelPlacements = netLabelPlacements.filter(
11364
+ (label) => label.globalConnNetId !== candidateTrace.globalConnNetId
11365
+ );
11366
+ if (pathIntersectsAnyNetLabel({
11292
11367
  path: candidateTrace.tracePath,
11293
- netLabelPlacements
11294
- });
11368
+ netLabelPlacements: otherNetLabelPlacements
11369
+ })) {
11370
+ return false;
11371
+ }
11372
+ const sameNetLabelPlacements = netLabelPlacements.filter(
11373
+ (label) => label.globalConnNetId === candidateTrace.globalConnNetId
11374
+ );
11375
+ if (!pathIntersectsAnyNetLabel({
11376
+ path: candidateTrace.tracePath,
11377
+ netLabelPlacements: sameNetLabelPlacements
11378
+ })) {
11379
+ return true;
11380
+ }
11381
+ if (pathEntersAnyNetLabel({
11382
+ path: candidateTrace.tracePath,
11383
+ netLabelPlacements: sameNetLabelPlacements
11384
+ })) {
11385
+ return false;
11386
+ }
11387
+ const originalRail = getLongestHorizontalSegment(originalTrace);
11388
+ const candidateRail = getLongestHorizontalSegment(candidateTrace);
11389
+ if (!originalRail || !candidateRail) return false;
11390
+ return Math.abs(originalRail.start.y - candidateRail.start.y) <= MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET;
11295
11391
  };
11296
11392
  var alignSameNetJunctions = ({
11297
11393
  inputProblem,
@@ -11324,6 +11420,7 @@ var alignSameNetJunctions = ({
11324
11420
  }
11325
11421
  if (!candidateIsClear({
11326
11422
  candidateTrace,
11423
+ originalTrace: branchTrace,
11327
11424
  traces: outputTraces,
11328
11425
  inputProblem,
11329
11426
  netLabelPlacements
@@ -12,7 +12,10 @@ import {
12
12
  } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
13
13
  import type { InputPin, InputProblem } from "lib/types/InputProblem"
14
14
  import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
15
- import { pathIntersectsAnyNetLabel } from "./pathIntersectsAnyNetLabel"
15
+ import {
16
+ pathEntersAnyNetLabel,
17
+ pathIntersectsAnyNetLabel,
18
+ } from "./pathIntersectsAnyNetLabel"
16
19
 
17
20
  interface AlignSameNetJunctionsInput {
18
21
  inputProblem: InputProblem
@@ -25,7 +28,10 @@ interface HorizontalSegment {
25
28
  end: Point
26
29
  }
27
30
 
31
+ // Only near-level load pins should be combined onto one horizontal rail.
28
32
  const MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2
33
+ // Limit label-boundary alignment to small corrections that cannot create spikes.
34
+ const MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2
29
35
 
30
36
  const getSharedPin = ({
31
37
  donorTrace,
@@ -135,11 +141,13 @@ const getAlignedBranchPath = ({
135
141
 
136
142
  const candidateIsClear = ({
137
143
  candidateTrace,
144
+ originalTrace,
138
145
  traces,
139
146
  inputProblem,
140
147
  netLabelPlacements,
141
148
  }: {
142
149
  candidateTrace: SolvedTracePath
150
+ originalTrace: SolvedTracePath
143
151
  traces: SolvedTracePath[]
144
152
  inputProblem: InputProblem
145
153
  netLabelPlacements: NetLabelPlacement[]
@@ -156,10 +164,46 @@ const candidateIsClear = ({
156
164
  return false
157
165
  }
158
166
 
159
- return !pathIntersectsAnyNetLabel({
160
- path: candidateTrace.tracePath,
161
- netLabelPlacements,
162
- })
167
+ const otherNetLabelPlacements = netLabelPlacements.filter(
168
+ (label) => label.globalConnNetId !== candidateTrace.globalConnNetId,
169
+ )
170
+ if (
171
+ pathIntersectsAnyNetLabel({
172
+ path: candidateTrace.tracePath,
173
+ netLabelPlacements: otherNetLabelPlacements,
174
+ })
175
+ ) {
176
+ return false
177
+ }
178
+
179
+ const sameNetLabelPlacements = netLabelPlacements.filter(
180
+ (label) => label.globalConnNetId === candidateTrace.globalConnNetId,
181
+ )
182
+ // A same-net rail may follow its label edge, but never enter the label body.
183
+ if (
184
+ !pathIntersectsAnyNetLabel({
185
+ path: candidateTrace.tracePath,
186
+ netLabelPlacements: sameNetLabelPlacements,
187
+ })
188
+ ) {
189
+ return true
190
+ }
191
+ if (
192
+ pathEntersAnyNetLabel({
193
+ path: candidateTrace.tracePath,
194
+ netLabelPlacements: sameNetLabelPlacements,
195
+ })
196
+ ) {
197
+ return false
198
+ }
199
+
200
+ const originalRail = getLongestHorizontalSegment(originalTrace)
201
+ const candidateRail = getLongestHorizontalSegment(candidateTrace)
202
+ if (!originalRail || !candidateRail) return false
203
+ return (
204
+ Math.abs(originalRail.start.y - candidateRail.start.y) <=
205
+ MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET
206
+ )
163
207
  }
164
208
 
165
209
  export const alignSameNetJunctions = ({
@@ -202,6 +246,7 @@ export const alignSameNetJunctions = ({
202
246
  if (
203
247
  !candidateIsClear({
204
248
  candidateTrace,
249
+ originalTrace: branchTrace,
205
250
  traces: outputTraces,
206
251
  inputProblem,
207
252
  netLabelPlacements,
@@ -3,6 +3,9 @@ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetL
3
3
  import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
4
4
  import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
5
 
6
+ // Insetting distinguishes harmless edge contact from entering the label body.
7
+ const NET_LABEL_INTERIOR_INSET = 1e-6
8
+
6
9
  export const pathIntersectsAnyNetLabel = ({
7
10
  path,
8
11
  netLabelPlacements,
@@ -20,3 +23,29 @@ export const pathIntersectsAnyNetLabel = ({
20
23
  }
21
24
  return false
22
25
  }
26
+
27
+ export const pathEntersAnyNetLabel = ({
28
+ path,
29
+ netLabelPlacements,
30
+ }: {
31
+ path: Point[]
32
+ netLabelPlacements: NetLabelPlacement[]
33
+ }) => {
34
+ for (const label of netLabelPlacements) {
35
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
36
+ const labelInteriorBounds = {
37
+ minX: labelBounds.minX + NET_LABEL_INTERIOR_INSET,
38
+ minY: labelBounds.minY + NET_LABEL_INTERIOR_INSET,
39
+ maxX: labelBounds.maxX - NET_LABEL_INTERIOR_INSET,
40
+ maxY: labelBounds.maxY - NET_LABEL_INTERIOR_INSET,
41
+ }
42
+ for (let index = 0; index < path.length - 1; index++) {
43
+ if (
44
+ segmentIntersectsRect(path[index], path[index + 1], labelInteriorBounds)
45
+ ) {
46
+ return true
47
+ }
48
+ }
49
+ }
50
+ return false
51
+ }
@@ -9,6 +9,7 @@ import {
9
9
  getVisibleTraceSegmentCount,
10
10
  RAIL_ALIGNMENT_EPSILON,
11
11
  } from "./sameNetRailAlignment/geometry"
12
+ import { shortenExcessiveLabelPaddingDetour } from "./shortenExcessiveLabelPaddingDetour"
12
13
 
13
14
  /**
14
15
  * Minimizes turns with a strict pass that treats every other trace as an
@@ -31,12 +32,16 @@ export const minimizeTurnsWithFilteredLabels = ({
31
32
  mergedLabelNetIdMap: Record<string, Set<string>>
32
33
  paddingBuffer: number
33
34
  }): SolvedTracePath => {
34
- const targetTrace = traces.find(
35
+ const originalTargetTrace = traces.find(
35
36
  (t) => t.mspPairId === targetMspConnectionPairId,
36
37
  )
37
- if (!targetTrace) {
38
+ if (!originalTargetTrace) {
38
39
  throw new Error(`Target trace ${targetMspConnectionPairId} not found`)
39
40
  }
41
+ const targetTrace = shortenExcessiveLabelPaddingDetour({
42
+ inputProblem,
43
+ trace: originalTargetTrace,
44
+ })
40
45
 
41
46
  const targetPinIds = new Set(targetTrace.pinIds)
42
47
  const otherTraces = traces.filter(
@@ -0,0 +1,64 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
3
+ import type { InputChip, InputProblem } from "lib/types/InputProblem"
4
+
5
+ const MIN_EXCESSIVE_DETOUR_LENGTH_RATIO = 2.5
6
+
7
+ const getTracePathLength = (
8
+ tracePath: SolvedTracePath["tracePath"],
9
+ ): number => {
10
+ let pathLength = 0
11
+ for (let pointIndex = 1; pointIndex < tracePath.length; pointIndex++) {
12
+ const previousPoint = tracePath[pointIndex - 1]!
13
+ const point = tracePath[pointIndex]!
14
+ pathLength +=
15
+ Math.abs(point.x - previousPoint.x) + Math.abs(point.y - previousPoint.y)
16
+ }
17
+ return pathLength
18
+ }
19
+
20
+ /**
21
+ * Retries an already-solved severe detour with its exact direct connection
22
+ * first, preventing a sibling branch's label width from inflating text padding.
23
+ * The shorter route is used only when it is less than 40% of the original.
24
+ */
25
+ export const shortenExcessiveLabelPaddingDetour = ({
26
+ inputProblem,
27
+ trace,
28
+ }: {
29
+ inputProblem: InputProblem
30
+ trace: SolvedTracePath
31
+ }): SolvedTracePath => {
32
+ const exactConnection = inputProblem.directConnections.find((connection) =>
33
+ connection.pinIds.every((pinId) => trace.pinIds.includes(pinId)),
34
+ )
35
+ if (!exactConnection) return trace
36
+
37
+ const chipMap: Record<string, InputChip> = {}
38
+ for (const chip of inputProblem.chips) chipMap[chip.chipId] = chip
39
+
40
+ const candidateSolver = new SchematicTraceSingleLineSolver2({
41
+ pins: trace.pins,
42
+ connectionPair: trace,
43
+ inputProblem: {
44
+ ...inputProblem,
45
+ directConnections: [
46
+ exactConnection,
47
+ ...inputProblem.directConnections.filter(
48
+ (connection) => connection !== exactConnection,
49
+ ),
50
+ ],
51
+ },
52
+ chipMap,
53
+ })
54
+ candidateSolver.solve()
55
+ if (!candidateSolver.solvedTracePath) return trace
56
+
57
+ const currentLength = getTracePathLength(trace.tracePath)
58
+ const candidateLength = getTracePathLength(candidateSolver.solvedTracePath)
59
+ if (currentLength <= candidateLength * MIN_EXCESSIVE_DETOUR_LENGTH_RATIO) {
60
+ return trace
61
+ }
62
+
63
+ return { ...trace, tracePath: candidateSolver.solvedTracePath }
64
+ }
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.122",
4
+ "version": "0.0.124",
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/bug-reports/bug-report-20260804T171919Z/bug-report-20260804T171919Z.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/bug-reports/bug-report-20260804T225912Z/bug-report-20260804T225912Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />