@tscircuit/schematic-trace-solver 0.0.55 → 0.0.56

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.d.ts CHANGED
@@ -820,6 +820,32 @@ declare class TraceAnchoredNetLabelOverlapSolver extends BaseSolver {
820
820
  visualize(): GraphicsObject;
821
821
  }
822
822
 
823
+ interface NetLabelTraceCollisionSolverParams {
824
+ inputProblem: InputProblem;
825
+ traces: SolvedTracePath[];
826
+ netLabelPlacements: NetLabelPlacement[];
827
+ }
828
+ declare class NetLabelTraceCollisionSolver extends BaseSolver {
829
+ inputProblem: InputProblem;
830
+ traces: SolvedTracePath[];
831
+ netLabelPlacements: NetLabelPlacement[];
832
+ outputTraces: SolvedTracePath[];
833
+ outputNetLabelPlacements: NetLabelPlacement[];
834
+ activeSubSolver: SingleOverlapSolver | null;
835
+ private recentlyFailed;
836
+ private detourCounts;
837
+ private currentOverlap;
838
+ constructor(params: NetLabelTraceCollisionSolverParams);
839
+ getConstructorParams(): ConstructorParameters<typeof NetLabelTraceCollisionSolver>[0];
840
+ _step(): void;
841
+ getOutput(): {
842
+ traces: SolvedTracePath[];
843
+ netLabelPlacements: NetLabelPlacement[];
844
+ };
845
+ private getOverlapKey;
846
+ visualize(): GraphicsObject;
847
+ }
848
+
823
849
  /**
824
850
  * Pipeline solver that runs a series of solvers to find the best schematic layout.
825
851
  * Coordinates the entire layout process from chip partitioning through final packing.
@@ -849,12 +875,13 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
849
875
  availableNetOrientationSolver?: AvailableNetOrientationSolver;
850
876
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver;
851
877
  traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver;
878
+ netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver;
852
879
  startTimeOfPhase: Record<string, number>;
853
880
  endTimeOfPhase: Record<string, number>;
854
881
  timeSpentOnPhase: Record<string, number>;
855
882
  firstIterationOfPhase: Record<string, number>;
856
883
  inputProblem: InputProblem;
857
- pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof VccNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver>)[];
884
+ pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof VccNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver> | PipelineStep<typeof NetLabelTraceCollisionSolver>)[];
858
885
  constructor(inputProblem: InputProblem);
859
886
  getConstructorParams(): ConstructorParameters<typeof SchematicTracePipelineSolver>[0];
860
887
  currentPipelineStepIndex: number;
package/dist/index.js CHANGED
@@ -7150,6 +7150,214 @@ var TraceAnchoredNetLabelOverlapSolver = class extends BaseSolver {
7150
7150
  }
7151
7151
  };
7152
7152
 
7153
+ // lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts
7154
+ function buildMergedObstacleLabel(seedLabel, allLabels) {
7155
+ const TOUCH_MARGIN = 0.01;
7156
+ const getBounds3 = (l) => getRectBounds(l.center, l.width, l.height);
7157
+ const touches = (a, b) => {
7158
+ const ba = getBounds3(a);
7159
+ const bb = getBounds3(b);
7160
+ return ba.minX <= bb.maxX + TOUCH_MARGIN && ba.maxX >= bb.minX - TOUCH_MARGIN && ba.minY <= bb.maxY + TOUCH_MARGIN && ba.maxY >= bb.minY - TOUCH_MARGIN;
7161
+ };
7162
+ const group = /* @__PURE__ */ new Set([seedLabel]);
7163
+ let changed = true;
7164
+ while (changed) {
7165
+ changed = false;
7166
+ for (const l of allLabels) {
7167
+ if (group.has(l)) continue;
7168
+ if ([...group].some((g) => touches(g, l))) {
7169
+ group.add(l);
7170
+ changed = true;
7171
+ }
7172
+ }
7173
+ }
7174
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
7175
+ for (const l of group) {
7176
+ const b = getBounds3(l);
7177
+ if (b.minX < minX) minX = b.minX;
7178
+ if (b.minY < minY) minY = b.minY;
7179
+ if (b.maxX > maxX) maxX = b.maxX;
7180
+ if (b.maxY > maxY) maxY = b.maxY;
7181
+ }
7182
+ const cx = (minX + maxX) / 2;
7183
+ const cy = (minY + maxY) / 2;
7184
+ const w = maxX - minX;
7185
+ const h = maxY - minY;
7186
+ return {
7187
+ ...seedLabel,
7188
+ center: { x: cx, y: cy },
7189
+ width: w,
7190
+ height: h
7191
+ };
7192
+ }
7193
+ function detectInnerTraceLabelOverlaps(traces, labels) {
7194
+ const overlaps = [];
7195
+ for (const trace of traces) {
7196
+ for (const label of labels) {
7197
+ if (trace.globalConnNetId === label.globalConnNetId) continue;
7198
+ const bounds = getRectBounds(label.center, label.width, label.height);
7199
+ const path = trace.tracePath;
7200
+ const innerStart = 1;
7201
+ const innerEnd = path.length - 2;
7202
+ for (let i = innerStart; i < innerEnd; i++) {
7203
+ if (segmentIntersectsRect2(path[i], path[i + 1], bounds)) {
7204
+ overlaps.push({ trace, label });
7205
+ break;
7206
+ }
7207
+ }
7208
+ }
7209
+ }
7210
+ return overlaps;
7211
+ }
7212
+ var PADDING_BUFFER = 0.1;
7213
+ var MAX_DETOUR_ATTEMPTS = 3;
7214
+ var NetLabelTraceCollisionSolver = class extends BaseSolver {
7215
+ inputProblem;
7216
+ traces;
7217
+ netLabelPlacements;
7218
+ outputTraces;
7219
+ outputNetLabelPlacements;
7220
+ activeSubSolver = null;
7221
+ recentlyFailed = /* @__PURE__ */ new Set();
7222
+ detourCounts = /* @__PURE__ */ new Map();
7223
+ currentOverlap = null;
7224
+ constructor(params) {
7225
+ super();
7226
+ this.inputProblem = params.inputProblem;
7227
+ this.traces = params.traces;
7228
+ this.netLabelPlacements = params.netLabelPlacements;
7229
+ this.outputTraces = [...params.traces];
7230
+ this.outputNetLabelPlacements = [...params.netLabelPlacements];
7231
+ }
7232
+ getConstructorParams() {
7233
+ return {
7234
+ inputProblem: this.inputProblem,
7235
+ traces: this.traces,
7236
+ netLabelPlacements: this.netLabelPlacements
7237
+ };
7238
+ }
7239
+ _step() {
7240
+ if (this.activeSubSolver) {
7241
+ this.activeSubSolver.step();
7242
+ if (this.activeSubSolver.solved) {
7243
+ const solvedPath = this.activeSubSolver.solvedTracePath;
7244
+ if (solvedPath) {
7245
+ const idx = this.outputTraces.findIndex(
7246
+ (t) => t.mspPairId === this.activeSubSolver.initialTrace.mspPairId
7247
+ );
7248
+ if (idx !== -1) {
7249
+ this.outputTraces[idx] = {
7250
+ ...this.outputTraces[idx],
7251
+ tracePath: solvedPath
7252
+ };
7253
+ }
7254
+ }
7255
+ this.activeSubSolver = null;
7256
+ this.currentOverlap = null;
7257
+ } else if (this.activeSubSolver.failed) {
7258
+ const key = this.getOverlapKey(
7259
+ this.activeSubSolver.initialTrace,
7260
+ this.activeSubSolver.label
7261
+ );
7262
+ this.recentlyFailed.add(key);
7263
+ this.activeSubSolver = null;
7264
+ this.currentOverlap = null;
7265
+ }
7266
+ return;
7267
+ }
7268
+ const overlaps = detectInnerTraceLabelOverlaps(
7269
+ this.outputTraces,
7270
+ this.outputNetLabelPlacements
7271
+ );
7272
+ const actionable = overlaps.filter((o) => {
7273
+ const key = this.getOverlapKey(o.trace, o.label);
7274
+ return !this.recentlyFailed.has(key);
7275
+ });
7276
+ if (actionable.length === 0) {
7277
+ this.solved = true;
7278
+ return;
7279
+ }
7280
+ const next = actionable[0];
7281
+ const traceToFix = this.outputTraces.find(
7282
+ (t) => t.mspPairId === next.trace.mspPairId
7283
+ );
7284
+ this.currentOverlap = next;
7285
+ const mergedLabel = buildMergedObstacleLabel(
7286
+ next.label,
7287
+ this.outputNetLabelPlacements.filter(
7288
+ (l) => l.globalConnNetId !== traceToFix.globalConnNetId
7289
+ )
7290
+ );
7291
+ const mergeKey = `${next.trace.mspPairId}::merged::${mergedLabel.center.x},${mergedLabel.center.y},${mergedLabel.width},${mergedLabel.height}`;
7292
+ const detourCount = this.detourCounts.get(mergeKey) ?? 0;
7293
+ if (detourCount >= MAX_DETOUR_ATTEMPTS) {
7294
+ this.recentlyFailed.add(this.getOverlapKey(next.trace, next.label));
7295
+ this.currentOverlap = null;
7296
+ return;
7297
+ }
7298
+ this.detourCounts.set(mergeKey, detourCount + 1);
7299
+ this.activeSubSolver = new SingleOverlapSolver({
7300
+ trace: traceToFix,
7301
+ label: mergedLabel,
7302
+ problem: this.inputProblem,
7303
+ paddingBuffer: PADDING_BUFFER,
7304
+ detourCount
7305
+ });
7306
+ }
7307
+ getOutput() {
7308
+ return {
7309
+ traces: this.outputTraces,
7310
+ netLabelPlacements: this.outputNetLabelPlacements
7311
+ };
7312
+ }
7313
+ getOverlapKey(trace, label) {
7314
+ return `${trace.mspPairId}::${label.globalConnNetId}`;
7315
+ }
7316
+ visualize() {
7317
+ if (this.activeSubSolver) return this.activeSubSolver.visualize();
7318
+ const graphics = visualizeInputProblem(this.inputProblem);
7319
+ if (!graphics.lines) graphics.lines = [];
7320
+ if (!graphics.rects) graphics.rects = [];
7321
+ if (!graphics.points) graphics.points = [];
7322
+ for (const trace of this.outputTraces) {
7323
+ graphics.lines.push({
7324
+ points: trace.tracePath,
7325
+ strokeColor: "purple"
7326
+ });
7327
+ }
7328
+ for (const label of this.outputNetLabelPlacements) {
7329
+ graphics.rects.push({
7330
+ center: label.center,
7331
+ width: label.width,
7332
+ height: label.height,
7333
+ fill: getColorFromString(label.globalConnNetId, 0.35),
7334
+ strokeColor: getColorFromString(label.globalConnNetId, 0.9),
7335
+ label: `netId: ${label.netId}
7336
+ globalConnNetId: ${label.globalConnNetId}`
7337
+ });
7338
+ graphics.points.push({
7339
+ x: label.anchorPoint.x,
7340
+ y: label.anchorPoint.y,
7341
+ color: getColorFromString(label.globalConnNetId, 0.9)
7342
+ });
7343
+ }
7344
+ if (this.currentOverlap) {
7345
+ graphics.lines.push({
7346
+ points: this.currentOverlap.trace.tracePath,
7347
+ strokeColor: "red"
7348
+ });
7349
+ graphics.rects.push({
7350
+ center: this.currentOverlap.label.center,
7351
+ width: this.currentOverlap.label.width,
7352
+ height: this.currentOverlap.label.height,
7353
+ fill: "rgba(255,0,0,0.3)",
7354
+ strokeColor: "red"
7355
+ });
7356
+ }
7357
+ return graphics;
7358
+ }
7359
+ };
7360
+
7153
7361
  // lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
7154
7362
  function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
7155
7363
  return {
@@ -7174,6 +7382,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
7174
7382
  availableNetOrientationSolver;
7175
7383
  vccNetLabelCornerPlacementSolver;
7176
7384
  traceAnchoredNetLabelOverlapSolver;
7385
+ netLabelTraceCollisionSolver;
7177
7386
  startTimeOfPhase;
7178
7387
  endTimeOfPhase;
7179
7388
  timeSpentOnPhase;
@@ -7354,6 +7563,17 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
7354
7563
  netLabelPlacements: instance.vccNetLabelCornerPlacementSolver.outputNetLabelPlacements
7355
7564
  }
7356
7565
  ]
7566
+ ),
7567
+ definePipelineStep(
7568
+ "netLabelTraceCollisionSolver",
7569
+ NetLabelTraceCollisionSolver,
7570
+ (instance) => [
7571
+ {
7572
+ inputProblem: instance.inputProblem,
7573
+ traces: instance.availableNetOrientationSolver.traces,
7574
+ netLabelPlacements: instance.traceAnchoredNetLabelOverlapSolver.outputNetLabelPlacements
7575
+ }
7576
+ ]
7357
7577
  )
7358
7578
  ];
7359
7579
  constructor(inputProblem) {
@@ -0,0 +1,294 @@
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 { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
6
+ import { SingleOverlapSolver } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver"
7
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
8
+ import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
9
+ import { getColorFromString } from "lib/utils/getColorFromString"
10
+ import type { InputProblem } from "lib/types/InputProblem"
11
+
12
+ /**
13
+ * Merges all labels that touch/overlap with each other (transitively) around
14
+ * a seed label into a single bounding-box label. This prevents the situation
15
+ * where fixing one label collision re-routes the trace into an adjacent label.
16
+ */
17
+ function buildMergedObstacleLabel(
18
+ seedLabel: NetLabelPlacement,
19
+ allLabels: NetLabelPlacement[],
20
+ ): NetLabelPlacement {
21
+ const TOUCH_MARGIN = 0.01
22
+
23
+ const getBounds = (l: NetLabelPlacement) =>
24
+ getRectBounds(l.center, l.width, l.height)
25
+
26
+ const touches = (a: NetLabelPlacement, b: NetLabelPlacement) => {
27
+ const ba = getBounds(a)
28
+ const bb = getBounds(b)
29
+ return (
30
+ ba.minX <= bb.maxX + TOUCH_MARGIN &&
31
+ ba.maxX >= bb.minX - TOUCH_MARGIN &&
32
+ ba.minY <= bb.maxY + TOUCH_MARGIN &&
33
+ ba.maxY >= bb.minY - TOUCH_MARGIN
34
+ )
35
+ }
36
+
37
+ const group = new Set<NetLabelPlacement>([seedLabel])
38
+ let changed = true
39
+ while (changed) {
40
+ changed = false
41
+ for (const l of allLabels) {
42
+ if (group.has(l)) continue
43
+ if ([...group].some((g) => touches(g, l))) {
44
+ group.add(l)
45
+ changed = true
46
+ }
47
+ }
48
+ }
49
+
50
+ let minX = Infinity,
51
+ minY = Infinity,
52
+ maxX = -Infinity,
53
+ maxY = -Infinity
54
+ for (const l of group) {
55
+ const b = getBounds(l)
56
+ if (b.minX < minX) minX = b.minX
57
+ if (b.minY < minY) minY = b.minY
58
+ if (b.maxX > maxX) maxX = b.maxX
59
+ if (b.maxY > maxY) maxY = b.maxY
60
+ }
61
+
62
+ const cx = (minX + maxX) / 2
63
+ const cy = (minY + maxY) / 2
64
+ const w = maxX - minX
65
+ const h = maxY - minY
66
+
67
+ return {
68
+ ...seedLabel,
69
+ center: { x: cx, y: cy },
70
+ width: w,
71
+ height: h,
72
+ }
73
+ }
74
+
75
+ interface TraceInnerLabelOverlap {
76
+ trace: SolvedTracePath
77
+ label: NetLabelPlacement
78
+ }
79
+
80
+ /**
81
+ * Detects overlaps where a trace's INTERIOR (non-endpoint) segments cross a label.
82
+ * Traces are allowed to boundary a label at their endpoints (pin locations),
83
+ * just not allowed to cross through it in the middle.
84
+ */
85
+ function detectInnerTraceLabelOverlaps(
86
+ traces: SolvedTracePath[],
87
+ labels: NetLabelPlacement[],
88
+ ): TraceInnerLabelOverlap[] {
89
+ const overlaps: TraceInnerLabelOverlap[] = []
90
+ for (const trace of traces) {
91
+ for (const label of labels) {
92
+ if (trace.globalConnNetId === label.globalConnNetId) continue
93
+ const bounds = getRectBounds(label.center, label.width, label.height)
94
+ const path = trace.tracePath
95
+ // Skip first and last segments — trace endpoints (pins) may legitimately
96
+ // sit inside or adjacent to labels of neighbouring nets.
97
+ const innerStart = 1
98
+ const innerEnd = path.length - 2
99
+ for (let i = innerStart; i < innerEnd; i++) {
100
+ if (segmentIntersectsRect(path[i]!, path[i + 1]!, bounds)) {
101
+ overlaps.push({ trace, label })
102
+ break
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return overlaps
108
+ }
109
+
110
+ export interface NetLabelTraceCollisionSolverParams {
111
+ inputProblem: InputProblem
112
+ traces: SolvedTracePath[]
113
+ netLabelPlacements: NetLabelPlacement[]
114
+ }
115
+
116
+ const PADDING_BUFFER = 0.1
117
+ const MAX_DETOUR_ATTEMPTS = 3
118
+
119
+ export class NetLabelTraceCollisionSolver extends BaseSolver {
120
+ inputProblem: InputProblem
121
+ traces: SolvedTracePath[]
122
+ netLabelPlacements: NetLabelPlacement[]
123
+
124
+ outputTraces: SolvedTracePath[]
125
+ outputNetLabelPlacements: NetLabelPlacement[]
126
+
127
+ override activeSubSolver: SingleOverlapSolver | null = null
128
+ private recentlyFailed = new Set<string>()
129
+ private detourCounts = new Map<string, number>()
130
+
131
+ private currentOverlap: {
132
+ trace: SolvedTracePath
133
+ label: NetLabelPlacement
134
+ } | null = null
135
+
136
+ constructor(params: NetLabelTraceCollisionSolverParams) {
137
+ super()
138
+ this.inputProblem = params.inputProblem
139
+ this.traces = params.traces
140
+ this.netLabelPlacements = params.netLabelPlacements
141
+ this.outputTraces = [...params.traces]
142
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
143
+ }
144
+
145
+ override getConstructorParams(): ConstructorParameters<
146
+ typeof NetLabelTraceCollisionSolver
147
+ >[0] {
148
+ return {
149
+ inputProblem: this.inputProblem,
150
+ traces: this.traces,
151
+ netLabelPlacements: this.netLabelPlacements,
152
+ }
153
+ }
154
+
155
+ override _step() {
156
+ if (this.activeSubSolver) {
157
+ this.activeSubSolver.step()
158
+
159
+ if (this.activeSubSolver.solved) {
160
+ const solvedPath = this.activeSubSolver.solvedTracePath
161
+ if (solvedPath) {
162
+ const idx = this.outputTraces.findIndex(
163
+ (t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
164
+ )
165
+ if (idx !== -1) {
166
+ this.outputTraces[idx] = {
167
+ ...this.outputTraces[idx],
168
+ tracePath: solvedPath,
169
+ }
170
+ }
171
+ }
172
+ this.activeSubSolver = null
173
+ this.currentOverlap = null
174
+ } else if (this.activeSubSolver.failed) {
175
+ const key = this.getOverlapKey(
176
+ this.activeSubSolver.initialTrace,
177
+ this.activeSubSolver.label,
178
+ )
179
+ this.recentlyFailed.add(key)
180
+ this.activeSubSolver = null
181
+ this.currentOverlap = null
182
+ }
183
+ return
184
+ }
185
+
186
+ const overlaps = detectInnerTraceLabelOverlaps(
187
+ this.outputTraces,
188
+ this.outputNetLabelPlacements,
189
+ )
190
+
191
+ const actionable = overlaps.filter((o) => {
192
+ const key = this.getOverlapKey(o.trace, o.label)
193
+ return !this.recentlyFailed.has(key)
194
+ })
195
+
196
+ if (actionable.length === 0) {
197
+ this.solved = true
198
+ return
199
+ }
200
+
201
+ const next = actionable[0]
202
+
203
+ const traceToFix = this.outputTraces.find(
204
+ (t) => t.mspPairId === next.trace.mspPairId,
205
+ )!
206
+
207
+ this.currentOverlap = next
208
+
209
+ // Merge all touching/overlapping labels into one obstacle so that fixing
210
+ // one collision doesn't immediately create another with an adjacent label.
211
+ const mergedLabel = buildMergedObstacleLabel(
212
+ next.label,
213
+ this.outputNetLabelPlacements.filter(
214
+ (l) => l.globalConnNetId !== traceToFix.globalConnNetId,
215
+ ),
216
+ )
217
+
218
+ const mergeKey = `${next.trace.mspPairId}::merged::${mergedLabel.center.x},${mergedLabel.center.y},${mergedLabel.width},${mergedLabel.height}`
219
+ const detourCount = this.detourCounts.get(mergeKey) ?? 0
220
+ if (detourCount >= MAX_DETOUR_ATTEMPTS) {
221
+ this.recentlyFailed.add(this.getOverlapKey(next.trace, next.label))
222
+ this.currentOverlap = null
223
+ return
224
+ }
225
+ this.detourCounts.set(mergeKey, detourCount + 1)
226
+
227
+ this.activeSubSolver = new SingleOverlapSolver({
228
+ trace: traceToFix,
229
+ label: mergedLabel,
230
+ problem: this.inputProblem,
231
+ paddingBuffer: PADDING_BUFFER,
232
+ detourCount,
233
+ })
234
+ }
235
+
236
+ getOutput() {
237
+ return {
238
+ traces: this.outputTraces,
239
+ netLabelPlacements: this.outputNetLabelPlacements,
240
+ }
241
+ }
242
+
243
+ private getOverlapKey(trace: SolvedTracePath, label: NetLabelPlacement) {
244
+ return `${trace.mspPairId}::${label.globalConnNetId}`
245
+ }
246
+
247
+ override visualize(): GraphicsObject {
248
+ if (this.activeSubSolver) return this.activeSubSolver.visualize()
249
+
250
+ const graphics = visualizeInputProblem(this.inputProblem)
251
+ if (!graphics.lines) graphics.lines = []
252
+ if (!graphics.rects) graphics.rects = []
253
+ if (!graphics.points) graphics.points = []
254
+
255
+ for (const trace of this.outputTraces) {
256
+ graphics.lines.push({
257
+ points: trace.tracePath,
258
+ strokeColor: "purple",
259
+ } as any)
260
+ }
261
+
262
+ for (const label of this.outputNetLabelPlacements) {
263
+ graphics.rects.push({
264
+ center: label.center,
265
+ width: label.width,
266
+ height: label.height,
267
+ fill: getColorFromString(label.globalConnNetId, 0.35),
268
+ strokeColor: getColorFromString(label.globalConnNetId, 0.9),
269
+ label: `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`,
270
+ } as any)
271
+ graphics.points.push({
272
+ x: label.anchorPoint.x,
273
+ y: label.anchorPoint.y,
274
+ color: getColorFromString(label.globalConnNetId, 0.9),
275
+ } as any)
276
+ }
277
+
278
+ if (this.currentOverlap) {
279
+ graphics.lines.push({
280
+ points: this.currentOverlap.trace.tracePath,
281
+ strokeColor: "red",
282
+ } as any)
283
+ graphics.rects.push({
284
+ center: this.currentOverlap.label.center,
285
+ width: this.currentOverlap.label.width,
286
+ height: this.currentOverlap.label.height,
287
+ fill: "rgba(255,0,0,0.3)",
288
+ strokeColor: "red",
289
+ } as any)
290
+ }
291
+
292
+ return graphics
293
+ }
294
+ }
@@ -25,6 +25,7 @@ import { Example28Solver } from "../Example28Solver/Example28Solver"
25
25
  import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver"
26
26
  import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
27
27
  import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
28
+ import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
28
29
 
29
30
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
30
31
  solverName: string
@@ -78,6 +79,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
78
79
  availableNetOrientationSolver?: AvailableNetOrientationSolver
79
80
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
80
81
  traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
82
+ netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
81
83
 
82
84
  startTimeOfPhase: Record<string, number>
83
85
  endTimeOfPhase: Record<string, number>
@@ -285,6 +287,19 @@ export class SchematicTracePipelineSolver extends BaseSolver {
285
287
  },
286
288
  ],
287
289
  ),
290
+ definePipelineStep(
291
+ "netLabelTraceCollisionSolver",
292
+ NetLabelTraceCollisionSolver,
293
+ (instance) => [
294
+ {
295
+ inputProblem: instance.inputProblem,
296
+ traces: instance.availableNetOrientationSolver!.traces,
297
+ netLabelPlacements:
298
+ instance.traceAnchoredNetLabelOverlapSolver!
299
+ .outputNetLabelPlacements,
300
+ },
301
+ ],
302
+ ),
288
303
  ]
289
304
 
290
305
  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.55",
4
+ "version": "0.0.56",
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/example33.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,132 @@
1
+ {
2
+ "chips": [
3
+ {
4
+ "chipId": "schematic_component_0",
5
+ "center": {
6
+ "x": -1.4,
7
+ "y": -6.6
8
+ },
9
+ "width": 1.1,
10
+ "height": 0.388910699999999,
11
+ "pins": [
12
+ {
13
+ "pinId": "R7.1",
14
+ "x": -1.95,
15
+ "y": -6.6
16
+ },
17
+ {
18
+ "pinId": "R7.2",
19
+ "x": -0.8499999999999999,
20
+ "y": -6.6
21
+ }
22
+ ],
23
+ "sectionId": "qspi"
24
+ },
25
+ {
26
+ "chipId": "schematic_component_1",
27
+ "center": {
28
+ "x": 0,
29
+ "y": -8.4
30
+ },
31
+ "width": 3.045,
32
+ "height": 1,
33
+ "pins": [
34
+ {
35
+ "pinId": "U5.1",
36
+ "x": -1.5225,
37
+ "y": -8.1
38
+ },
39
+ {
40
+ "pinId": "U5.2",
41
+ "x": -1.5225,
42
+ "y": -8.3
43
+ },
44
+ {
45
+ "pinId": "U5.3",
46
+ "x": -1.5225,
47
+ "y": -8.5
48
+ },
49
+ {
50
+ "pinId": "U5.4",
51
+ "x": -1.5225,
52
+ "y": -8.700000000000001
53
+ },
54
+ {
55
+ "pinId": "U5.5",
56
+ "x": 1.5225,
57
+ "y": -8.1
58
+ },
59
+ {
60
+ "pinId": "U5.6",
61
+ "x": 1.5225,
62
+ "y": -8.3
63
+ },
64
+ {
65
+ "pinId": "U5.7",
66
+ "x": 1.5225,
67
+ "y": -8.700000000000001
68
+ },
69
+ {
70
+ "pinId": "U5.8",
71
+ "x": 1.5225,
72
+ "y": -8.5
73
+ }
74
+ ],
75
+ "sectionId": "qspi"
76
+ }
77
+ ],
78
+ "directConnections": [],
79
+ "netConnections": [
80
+ {
81
+ "netId": "V3V3",
82
+ "pinIds": ["R7.1", "U5.8"],
83
+ "netLabelWidth": 0.4
84
+ },
85
+ {
86
+ "netId": "QSPI_CS",
87
+ "pinIds": ["R7.2", "U5.4"],
88
+ "netLabelWidth": 0.7
89
+ },
90
+ {
91
+ "netId": "QSPI_SCK",
92
+ "pinIds": ["U5.1"],
93
+ "netLabelWidth": 0.8
94
+ },
95
+ {
96
+ "netId": "QSPI_DATA0",
97
+ "pinIds": ["U5.2"],
98
+ "netLabelWidth": 1
99
+ },
100
+ {
101
+ "netId": "QSPI_DATA1",
102
+ "pinIds": ["U5.3"],
103
+ "netLabelWidth": 1
104
+ },
105
+ {
106
+ "netId": "QSPI_DATA2",
107
+ "pinIds": ["U5.5"],
108
+ "netLabelWidth": 1
109
+ },
110
+ {
111
+ "netId": "QSPI_DATA3",
112
+ "pinIds": ["U5.6"],
113
+ "netLabelWidth": 1
114
+ },
115
+ {
116
+ "netId": "GND",
117
+ "pinIds": ["U5.7"],
118
+ "netLabelWidth": 0.3
119
+ }
120
+ ],
121
+ "availableNetLabelOrientations": {
122
+ "V3V3": ["y+"],
123
+ "QSPI_CS": ["x-", "x+"],
124
+ "QSPI_SCK": ["x-", "x+"],
125
+ "QSPI_DATA0": ["x-", "x+"],
126
+ "QSPI_DATA1": ["x-", "x+"],
127
+ "QSPI_DATA2": ["x-", "x+"],
128
+ "QSPI_DATA3": ["x-", "x+"],
129
+ "GND": ["y-"]
130
+ },
131
+ "maxMspPairDistance": 2.4
132
+ }
@@ -0,0 +1,181 @@
1
+ <svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="100%" height="100%" fill="white" />
3
+ <g>
4
+ <circle data-type="point" data-label="R7.1
5
+ x-" data-x="-1.95" data-y="-6.6" cx="102.39751311443555" cy="200.26423159121816" r="3" fill="hsl(232, 100%, 50%, 0.8)" />
6
+ </g>
7
+ <g>
8
+ <circle data-type="point" data-label="R7.2
9
+ x+" data-x="-0.8499999999999999" data-y="-6.6" cx="222.07888090149598" cy="200.26423159121816" r="3" fill="hsl(233, 100%, 50%, 0.8)" />
10
+ </g>
11
+ <g>
12
+ <circle data-type="point" data-label="U5.1
13
+ x-" data-x="-1.5225" data-y="-8.1" cx="148.91004468622495" cy="363.4660967553915" r="3" fill="hsl(203, 100%, 50%, 0.8)" />
14
+ </g>
15
+ <g>
16
+ <circle data-type="point" data-label="U5.2
17
+ x-" data-x="-1.5225" data-y="-8.3" cx="148.91004468622495" cy="385.22634544394805" r="3" fill="hsl(204, 100%, 50%, 0.8)" />
18
+ </g>
19
+ <g>
20
+ <circle data-type="point" data-label="U5.3
21
+ x-" data-x="-1.5225" data-y="-8.5" cx="148.91004468622495" cy="406.98659413250437" r="3" fill="hsl(205, 100%, 50%, 0.8)" />
22
+ </g>
23
+ <g>
24
+ <circle data-type="point" data-label="U5.4
25
+ x-" data-x="-1.5225" data-y="-8.700000000000001" cx="148.91004468622495" cy="428.7468428210609" r="3" fill="hsl(206, 100%, 50%, 0.8)" />
26
+ </g>
27
+ <g>
28
+ <circle data-type="point" data-label="U5.5
29
+ x+" data-x="1.5225" data-y="-8.1" cx="480.2098309694967" cy="363.4660967553915" r="3" fill="hsl(207, 100%, 50%, 0.8)" />
30
+ </g>
31
+ <g>
32
+ <circle data-type="point" data-label="U5.6
33
+ x+" data-x="1.5225" data-y="-8.3" cx="480.2098309694967" cy="385.22634544394805" r="3" fill="hsl(208, 100%, 50%, 0.8)" />
34
+ </g>
35
+ <g>
36
+ <circle data-type="point" data-label="U5.7
37
+ x+" data-x="1.5225" data-y="-8.700000000000001" cx="480.2098309694967" cy="428.7468428210609" r="3" fill="hsl(209, 100%, 50%, 0.8)" />
38
+ </g>
39
+ <g>
40
+ <circle data-type="point" data-label="U5.8
41
+ x+" data-x="1.5225" data-y="-8.5" cx="480.2098309694967" cy="406.98659413250437" r="3" fill="hsl(210, 100%, 50%, 0.8)" />
42
+ </g>
43
+ <g>
44
+ <circle data-type="point" data-label="" data-x="-2.15" data-y="-6.6" cx="80.63726442587912" cy="200.26423159121816" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
45
+ </g>
46
+ <g>
47
+ <circle data-type="point" data-label="" data-x="-0.8499999999999999" data-y="-6.6" cx="222.07888090149598" cy="200.26423159121816" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
48
+ </g>
49
+ <g>
50
+ <circle data-type="point" data-label="" data-x="-1.5225" data-y="-8.700000000000001" cx="148.91004468622495" cy="428.7468428210609" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
51
+ </g>
52
+ <g>
53
+ <circle data-type="point" data-label="" data-x="-1.5225" data-y="-8.1" cx="148.91004468622495" cy="363.4660967553915" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
54
+ </g>
55
+ <g>
56
+ <circle data-type="point" data-label="" data-x="-1.5225" data-y="-8.3" cx="148.91004468622495" cy="385.22634544394805" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
57
+ </g>
58
+ <g>
59
+ <circle data-type="point" data-label="" data-x="-1.5225" data-y="-8.5" cx="148.91004468622495" cy="406.98659413250437" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
60
+ </g>
61
+ <g>
62
+ <circle data-type="point" data-label="" data-x="1.5225" data-y="-8.1" cx="480.2098309694967" cy="363.4660967553915" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
63
+ </g>
64
+ <g>
65
+ <circle data-type="point" data-label="" data-x="1.5225" data-y="-8.3" cx="480.2098309694967" cy="385.22634544394805" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
66
+ </g>
67
+ <g>
68
+ <circle data-type="point" data-label="" data-x="1.6235" data-y="-8.901" cx="491.19875655721773" cy="450.61589275306005" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
69
+ </g>
70
+ <g>
71
+ <polyline data-points="-1.95,-6.6 1.5225,-8.5" data-type="line" data-label="" points="102.39751311443555,200.26423159121816 480.2098309694967,406.98659413250437" fill="none" stroke="hsl(154, 100%, 50%, 0.8)" stroke-width="1" stroke-dasharray="4 2" />
72
+ </g>
73
+ <g>
74
+ <polyline data-points="-0.8499999999999999,-6.6 -1.5225,-8.700000000000001" data-type="line" data-label="" points="222.07888090149598,200.26423159121816 148.91004468622495,428.7468428210609" fill="none" stroke="hsl(172, 100%, 50%, 0.8)" stroke-width="1" stroke-dasharray="4 2" />
75
+ </g>
76
+ <g>
77
+ <polyline data-points="-1.95,-6.6 -2.15,-6.6 -2.15,-7.55 1.7225000000000004,-7.55 1.7225000000000004,-7.8999999999999995 2.6235,-7.8999999999999995 2.6235,-8.499999999999998 1.5225000000000002,-8.5" data-type="line" data-label="" points="102.39751311443555,200.26423159121816 80.63726442587912,200.26423159121816 80.63726442587912,303.62541286186126 501.97007965805324,303.62541286186126 501.97007965805324,341.70584806683496 600,341.70584806683496 600,406.98659413250414 480.20983096949675,406.98659413250437" fill="none" stroke="purple" stroke-width="1" />
78
+ </g>
79
+ <g>
80
+ <polyline data-points="1.5225,-8.700000000000001 1.6235,-8.700000000000001 1.6235,-8.901" data-type="line" data-label="" points="480.2098309694967,428.7468428210609 491.19875655721773,428.7468428210609 491.19875655721773,450.61589275306005" fill="none" stroke="purple" stroke-width="1" />
81
+ </g>
82
+ <g>
83
+ <rect data-type="rect" data-label="schematic_component_0" data-x="-1.4" data-y="-6.6" x="102.39751311443555" y="179.10724771711682" width="119.68136778706042" height="42.313967748202685" fill="hsl(24, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009191071428571429" />
84
+ </g>
85
+ <g>
86
+ <rect data-type="rect" data-label="schematic_component_1" data-x="0" data-y="-8.4" x="148.91004468622495" y="341.7058480668351" width="331.2997862832717" height="108.80124344278227" fill="hsl(24, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009191071428571429" />
87
+ </g>
88
+ <g>
89
+ <rect data-type="rect" data-label="netId: V3V3
90
+ globalConnNetId: connectivity_net0" data-x="-2.15" data-y="-6.3999999999999995" x="69.75714008160091" y="156.7437342141053" width="21.76024868855646" height="43.52049737711286" fill="#ef444466" stroke="#ef4444" stroke-width="0.009191071428571429" />
91
+ </g>
92
+ <g>
93
+ <rect data-type="rect" data-label="netId: QSPI_CS
94
+ globalConnNetId: connectivity_net1" data-x="-0.4989999999999999" data-y="-6.6" x="222.18768214493878" y="189.38410724693995" width="76.1608704099475" height="21.76024868855643" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
95
+ </g>
96
+ <g>
97
+ <rect data-type="rect" data-label="netId: QSPI_CS
98
+ globalConnNetId: connectivity_net1" data-x="-1.8735" data-y="-8.700000000000001" x="72.64037303283462" y="417.8667184767828" width="76.16087040994756" height="21.760248688556317" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
99
+ </g>
100
+ <g>
101
+ <rect data-type="rect" data-label="netId: QSPI_SCK
102
+ globalConnNetId: connectivity_net2" data-x="-1.9234999999999998" data-y="-8.1" x="61.76024868855643" y="352.5859724111133" width="87.04099475422575" height="21.760248688556317" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
103
+ </g>
104
+ <g>
105
+ <rect data-type="rect" data-label="netId: QSPI_DATA0
106
+ globalConnNetId: connectivity_net3" data-x="-2.0235" data-y="-8.3" x="40" y="374.34622109966983" width="108.80124344278218" height="21.760248688556317" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
107
+ </g>
108
+ <g>
109
+ <rect data-type="rect" data-label="netId: QSPI_DATA1
110
+ globalConnNetId: connectivity_net4" data-x="-2.0235" data-y="-8.5" x="40" y="396.10646978822615" width="108.80124344278218" height="21.76024868855643" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
111
+ </g>
112
+ <g>
113
+ <rect data-type="rect" data-label="netId: QSPI_DATA2
114
+ globalConnNetId: connectivity_net5" data-x="2.0235" data-y="-8.1" x="480.3186322129395" y="352.5859724111133" width="108.80124344278215" height="21.760248688556317" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
115
+ </g>
116
+ <g>
117
+ <rect data-type="rect" data-label="netId: QSPI_DATA3
118
+ globalConnNetId: connectivity_net6" data-x="2.0235" data-y="-8.3" x="480.3186322129395" y="374.34622109966983" width="108.80124344278215" height="21.760248688556317" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009191071428571429" />
119
+ </g>
120
+ <g>
121
+ <rect data-type="rect" data-label="netId: GND
122
+ globalConnNetId: connectivity_net7" data-x="1.6235" data-y="-9.051" x="480.3186322129395" y="450.61589275306005" width="21.76024868855643" height="32.64037303283476" fill="#00000066" stroke="#000000" stroke-width="0.009191071428571429" />
123
+ </g>
124
+ <g id="crosshair" style="display: none">
125
+ <line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5" />
126
+ <line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5" /><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text>
127
+ </g>
128
+ <script>
129
+ <![CDATA[
130
+ document.currentScript.parentElement.addEventListener('mousemove', (e) => {
131
+ const svg = e.currentTarget;
132
+ const rect = svg.getBoundingClientRect();
133
+ const x = e.clientX - rect.left;
134
+ const y = e.clientY - rect.top;
135
+ const crosshair = svg.getElementById('crosshair');
136
+ const h = svg.getElementById('crosshair-h');
137
+ const v = svg.getElementById('crosshair-v');
138
+ const coords = svg.getElementById('coordinates');
139
+
140
+ crosshair.style.display = 'block';
141
+ h.setAttribute('x1', '0');
142
+ h.setAttribute('x2', '640');
143
+ h.setAttribute('y1', y);
144
+ h.setAttribute('y2', y);
145
+ v.setAttribute('x1', x);
146
+ v.setAttribute('x2', x);
147
+ v.setAttribute('y1', '0');
148
+ v.setAttribute('y2', '640');
149
+
150
+ // Calculate real coordinates using inverse transformation
151
+ const matrix = {
152
+ "a": 108.8012434427822,
153
+ "c": 0,
154
+ "e": 314.55993782786084,
155
+ "b": 0,
156
+ "d": -108.8012434427822,
157
+ "f": -517.8239751311443
158
+ };
159
+ // Manually invert and apply the affine transform
160
+ // Since we only use translate and scale, we can directly compute:
161
+ // x' = (x - tx) / sx
162
+ // y' = (y - ty) / sy
163
+ const sx = matrix.a;
164
+ const sy = matrix.d;
165
+ const tx = matrix.e;
166
+ const ty = matrix.f;
167
+ const realPoint = {
168
+ x: (x - tx) / sx,
169
+ y: (y - ty) / sy // Flip y back since we used negative scale
170
+ }
171
+
172
+ coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
173
+ coords.setAttribute('x', (x + 5).toString());
174
+ coords.setAttribute('y', (y - 5).toString());
175
+ });
176
+ document.currentScript.parentElement.addEventListener('mouseleave', () => {
177
+ document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
178
+ });
179
+ ]]>
180
+ </script>
181
+ </svg>
@@ -0,0 +1,12 @@
1
+ import { test, expect } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import inputProblem from "../assets/example33.json"
4
+ import "tests/fixtures/matcher"
5
+
6
+ test("example33", () => {
7
+ const solver = new SchematicTracePipelineSolver(inputProblem as any)
8
+
9
+ solver.solve()
10
+
11
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
12
+ })