@tscircuit/schematic-trace-solver 0.0.165 → 0.0.167

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
@@ -141,7 +141,11 @@ interface InputDirectConnection {
141
141
  interface InputNetConnection {
142
142
  netId: string;
143
143
  pinIds: Array<PinId>;
144
- /** True when the authoritative source net is electrical ground. */
144
+ /**
145
+ * Preserve explicitly wired islands on this ground net, joining the islands
146
+ * electrically through labels instead of routing between them. Nets without
147
+ * explicit traces, or without this hint, retain automatic bus routing.
148
+ */
145
149
  isGround?: boolean;
146
150
  /**
147
151
  * User-facing text to render for this net label. `netId` remains the stable
package/dist/index.js CHANGED
@@ -369,10 +369,17 @@ var MAX_LOCAL_GROUND_BRANCH_OFFSET = 1;
369
369
  var getGroundConnectionPolicy = (inputProblem) => {
370
370
  const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem);
371
371
  const groundNetId = netConnMap.getNetConnectedToId("GND");
372
- if (!groundNetId) return () => true;
373
372
  const physicalConnMap = new ConnectivityMap2({});
373
+ const explicitlyWiredGroundNetIds = /* @__PURE__ */ new Set();
374
374
  for (const connection of inputProblem.directConnections) {
375
375
  physicalConnMap.addConnections([connection.pinIds]);
376
+ const globalNetId = netConnMap.getNetConnectedToId(connection.pinIds[0]);
377
+ const isMarkedGround = inputProblem.netConnections.some(
378
+ (net) => net.isGround && netConnMap.getNetConnectedToId(net.netId) === globalNetId
379
+ );
380
+ if (globalNetId && isMarkedGround) {
381
+ explicitlyWiredGroundNetIds.add(globalNetId);
382
+ }
376
383
  }
377
384
  const pins = new Map(
378
385
  inputProblem.chips.flatMap(
@@ -388,13 +395,28 @@ var getGroundConnectionPolicy = (inputProblem) => {
388
395
  ({ pin: b }) => a.pinId !== b.pinId && Math.abs(a.y - b.y) < 1e-6 && Math.abs(a.x - b.x) > 1e-6 && physicalConnMap.getNetConnectedToId(b.pinId) === group
389
396
  );
390
397
  });
391
- if (!hasExplicitParallelGroundRail) return () => true;
398
+ const areSeparateExplicitGroundIslands = (firstPinId, secondPinId) => {
399
+ const firstGlobalNetId = netConnMap.getNetConnectedToId(firstPinId);
400
+ const secondGlobalNetId = netConnMap.getNetConnectedToId(secondPinId);
401
+ if (!firstGlobalNetId || firstGlobalNetId !== secondGlobalNetId || !explicitlyWiredGroundNetIds.has(firstGlobalNetId)) {
402
+ return false;
403
+ }
404
+ const firstPhysicalGroup = physicalConnMap.getNetConnectedToId(firstPinId) ?? `pin:${firstPinId}`;
405
+ const secondPhysicalGroup = physicalConnMap.getNetConnectedToId(secondPinId) ?? `pin:${secondPinId}`;
406
+ return firstPhysicalGroup !== secondPhysicalGroup;
407
+ };
408
+ if (!hasExplicitParallelGroundRail) {
409
+ return (firstPinId, secondPinId) => !areSeparateExplicitGroundIslands(firstPinId, secondPinId);
410
+ }
392
411
  const isGroundFacingTerminal = (pinId) => {
393
412
  const entry = pins.get(pinId);
394
413
  return entry?.chip.pins.length === 2 && !physicalConnMap.getNetConnectedToId(pinId) && (entry.pin._facingDirection ?? getPinDirection(entry.pin, entry.chip)) === "y-";
395
414
  };
396
415
  return (firstPinId, secondPinId) => {
397
- if (!groundNetId || netConnMap.getNetConnectedToId(firstPinId) !== groundNetId || netConnMap.getNetConnectedToId(secondPinId) !== groundNetId) {
416
+ const firstGlobalNetId = netConnMap.getNetConnectedToId(firstPinId);
417
+ const secondGlobalNetId = netConnMap.getNetConnectedToId(secondPinId);
418
+ if (areSeparateExplicitGroundIslands(firstPinId, secondPinId)) return false;
419
+ if (!groundNetId || firstGlobalNetId !== groundNetId || secondGlobalNetId !== groundNetId) {
398
420
  return true;
399
421
  }
400
422
  if (!isGroundFacingTerminal(firstPinId) && !isGroundFacingTerminal(secondPinId))
@@ -13009,6 +13031,214 @@ var alignSameNetJunctions = ({
13009
13031
  };
13010
13032
  };
13011
13033
 
13034
+ // lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts
13035
+ import { getSegmentIntersection as getSegmentIntersection4 } from "@tscircuit/math-utils/line-intersections";
13036
+ var TRACE_LENGTH_EPSILON = 1e-6;
13037
+ var getTracePathStartingAtPin = (trace, pin) => {
13038
+ const pathStart = trace.tracePath[0];
13039
+ const pathEnd = trace.tracePath.at(-1);
13040
+ if (!pathStart || !pathEnd) return null;
13041
+ if (pointsEqual(pathStart, pin)) return trace.tracePath;
13042
+ if (pointsEqual(pathEnd, pin)) return [...trace.tracePath].reverse();
13043
+ return null;
13044
+ };
13045
+ var getPerpendicularPathCrossings = (targetPath, donorPath) => {
13046
+ const crossings = [];
13047
+ const sharedEndpoint = targetPath[0];
13048
+ for (let targetSegmentIndex = 0; targetSegmentIndex < targetPath.length - 1; targetSegmentIndex++) {
13049
+ const targetStart = targetPath[targetSegmentIndex];
13050
+ const targetEnd = targetPath[targetSegmentIndex + 1];
13051
+ const targetOrientation = getRailOrientation(targetStart, targetEnd);
13052
+ if (!targetOrientation) continue;
13053
+ for (let donorSegmentIndex = 0; donorSegmentIndex < donorPath.length - 1; donorSegmentIndex++) {
13054
+ const donorStart = donorPath[donorSegmentIndex];
13055
+ const donorEnd = donorPath[donorSegmentIndex + 1];
13056
+ const donorOrientation = getRailOrientation(donorStart, donorEnd);
13057
+ if (!donorOrientation || donorOrientation === targetOrientation) continue;
13058
+ const intersectionPoint = getSegmentIntersection4(
13059
+ targetStart,
13060
+ targetEnd,
13061
+ donorStart,
13062
+ donorEnd
13063
+ );
13064
+ if (!intersectionPoint || pointsEqual(intersectionPoint, sharedEndpoint)) {
13065
+ continue;
13066
+ }
13067
+ crossings.push({
13068
+ intersectionPoint,
13069
+ targetSegmentIndex,
13070
+ donorSegmentIndex
13071
+ });
13072
+ }
13073
+ }
13074
+ return crossings;
13075
+ };
13076
+ var buildCollapsedCyclePath = ({
13077
+ targetTrace,
13078
+ targetPath,
13079
+ donorPath,
13080
+ crossing
13081
+ }) => {
13082
+ const pathFromSharedPin = simplifyPath([
13083
+ ...donorPath.slice(0, crossing.donorSegmentIndex + 1),
13084
+ crossing.intersectionPoint,
13085
+ ...targetPath.slice(crossing.targetSegmentIndex + 1)
13086
+ ]);
13087
+ if (pointsEqual(targetTrace.tracePath[0], targetPath[0])) {
13088
+ return pathFromSharedPin;
13089
+ }
13090
+ return pathFromSharedPin.reverse();
13091
+ };
13092
+ var getNetLabelPlacementsForCycleCollapse = ({
13093
+ targetTrace,
13094
+ tracePath,
13095
+ netLabelPlacements
13096
+ }) => {
13097
+ const candidateNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
13098
+ trace: targetTrace,
13099
+ originalTracePath: targetTrace.tracePath,
13100
+ reroutedTracePath: tracePath,
13101
+ netLabelPlacements
13102
+ });
13103
+ for (let labelIndex = 0; labelIndex < netLabelPlacements.length; labelIndex++) {
13104
+ const label = netLabelPlacements[labelIndex];
13105
+ if (label.globalConnNetId !== targetTrace.globalConnNetId) continue;
13106
+ if (!tracePathContainsPoint(targetTrace.tracePath, label.anchorPoint)) {
13107
+ continue;
13108
+ }
13109
+ const candidateLabel = candidateNetLabelPlacements[labelIndex];
13110
+ if (!tracePathContainsPoint(tracePath, candidateLabel.anchorPoint)) {
13111
+ return null;
13112
+ }
13113
+ }
13114
+ const otherNetLabelPlacements = candidateNetLabelPlacements.filter(
13115
+ (label) => label.globalConnNetId !== targetTrace.globalConnNetId
13116
+ );
13117
+ if (pathIntersectsAnyNetLabel({
13118
+ path: tracePath,
13119
+ netLabelPlacements: otherNetLabelPlacements
13120
+ })) {
13121
+ return null;
13122
+ }
13123
+ const sameNetLabelPlacements = candidateNetLabelPlacements.filter(
13124
+ (label) => label.globalConnNetId === targetTrace.globalConnNetId
13125
+ );
13126
+ if (pathEntersAnyNetLabel({
13127
+ path: tracePath,
13128
+ netLabelPlacements: sameNetLabelPlacements
13129
+ })) {
13130
+ return null;
13131
+ }
13132
+ return candidateNetLabelPlacements;
13133
+ };
13134
+ var candidateIsBetter = (candidate, bestCandidate) => {
13135
+ if (!bestCandidate) return true;
13136
+ const visibleLengthDelta = candidate.netVisibleLength - bestCandidate.netVisibleLength;
13137
+ if (Math.abs(visibleLengthDelta) > TRACE_LENGTH_EPSILON) {
13138
+ return visibleLengthDelta < 0;
13139
+ }
13140
+ return candidate.netVisibleSegmentCount < bestCandidate.netVisibleSegmentCount;
13141
+ };
13142
+ var getBestCycleCollapseCandidate = ({
13143
+ targetTrace,
13144
+ traces,
13145
+ netLabelPlacements
13146
+ }) => {
13147
+ const sameNetTraces = traces.filter(
13148
+ (trace) => trace.globalConnNetId === targetTrace.globalConnNetId
13149
+ );
13150
+ const baselineNetVisibleLength = getVisibleTraceLength(sameNetTraces);
13151
+ const baselineTargetVisibleLength = getVisibleTraceLength([targetTrace]);
13152
+ let bestCandidate = null;
13153
+ for (const donorTrace of sameNetTraces) {
13154
+ if (donorTrace.mspPairId === targetTrace.mspPairId) continue;
13155
+ const sharedPin = getSharedPin({
13156
+ donorTrace,
13157
+ branchTrace: targetTrace
13158
+ });
13159
+ if (!sharedPin) continue;
13160
+ const targetPath = getTracePathStartingAtPin(targetTrace, sharedPin);
13161
+ const donorPath = getTracePathStartingAtPin(donorTrace, sharedPin);
13162
+ if (!targetPath || !donorPath) continue;
13163
+ const crossings = getPerpendicularPathCrossings(targetPath, donorPath);
13164
+ for (const crossing of crossings) {
13165
+ const tracePath = buildCollapsedCyclePath({
13166
+ targetTrace,
13167
+ targetPath,
13168
+ donorPath,
13169
+ crossing
13170
+ });
13171
+ const candidateTrace = { ...targetTrace, tracePath };
13172
+ const candidateTargetVisibleLength = getVisibleTraceLength([
13173
+ candidateTrace
13174
+ ]);
13175
+ if (candidateTargetVisibleLength > baselineTargetVisibleLength + TRACE_LENGTH_EPSILON) {
13176
+ continue;
13177
+ }
13178
+ const candidateNetLabelPlacements = getNetLabelPlacementsForCycleCollapse(
13179
+ {
13180
+ targetTrace,
13181
+ tracePath,
13182
+ netLabelPlacements
13183
+ }
13184
+ );
13185
+ if (!candidateNetLabelPlacements) continue;
13186
+ const candidateNetTraces = sameNetTraces.map((trace) => {
13187
+ if (trace.mspPairId === targetTrace.mspPairId) return candidateTrace;
13188
+ return trace;
13189
+ });
13190
+ const netVisibleLength = getVisibleTraceLength(candidateNetTraces);
13191
+ if (netVisibleLength >= baselineNetVisibleLength - TRACE_LENGTH_EPSILON) {
13192
+ continue;
13193
+ }
13194
+ const netVisibleSegmentCount = getVisibleTraceSegmentCount(candidateNetTraces);
13195
+ const candidate = {
13196
+ tracePath,
13197
+ netLabelPlacements: candidateNetLabelPlacements,
13198
+ netVisibleLength,
13199
+ netVisibleSegmentCount
13200
+ };
13201
+ if (candidateIsBetter(candidate, bestCandidate)) {
13202
+ bestCandidate = candidate;
13203
+ }
13204
+ }
13205
+ }
13206
+ return bestCandidate;
13207
+ };
13208
+ var collapseSameNetCycles = ({
13209
+ traces,
13210
+ netLabelPlacements
13211
+ }) => {
13212
+ const outputTraces = [...traces];
13213
+ let outputNetLabelPlacements = [...netLabelPlacements];
13214
+ let collapsedCycleCount = 0;
13215
+ let traceIndex = 0;
13216
+ while (traceIndex < outputTraces.length) {
13217
+ const targetTrace = outputTraces[traceIndex];
13218
+ const candidate = getBestCycleCollapseCandidate({
13219
+ targetTrace,
13220
+ traces: outputTraces,
13221
+ netLabelPlacements: outputNetLabelPlacements
13222
+ });
13223
+ if (!candidate) {
13224
+ traceIndex++;
13225
+ continue;
13226
+ }
13227
+ outputTraces[traceIndex] = {
13228
+ ...targetTrace,
13229
+ tracePath: candidate.tracePath
13230
+ };
13231
+ outputNetLabelPlacements = candidate.netLabelPlacements;
13232
+ collapsedCycleCount++;
13233
+ traceIndex = 0;
13234
+ }
13235
+ return {
13236
+ traces: outputTraces,
13237
+ netLabelPlacements: outputNetLabelPlacements,
13238
+ collapsedCycleCount
13239
+ };
13240
+ };
13241
+
13012
13242
  // lib/solvers/SameNetJunctionAlignmentSolver/placeGroundRailLabelsAtOuterEnd.ts
13013
13243
  var placeGroundRailLabelsAtOuterEnd = ({
13014
13244
  inputProblem,
@@ -13090,14 +13320,19 @@ var SameNetJunctionAlignmentSolver = class extends BaseSolver {
13090
13320
  this.outputNetLabelPlacements = input.netLabelPlacements;
13091
13321
  }
13092
13322
  _step() {
13093
- const result = alignSameNetJunctions(this.input);
13094
- this.outputTraces = result.traces;
13323
+ const alignment = alignSameNetJunctions(this.input);
13324
+ const cycleCollapse = collapseSameNetCycles({
13325
+ traces: alignment.traces,
13326
+ netLabelPlacements: alignment.netLabelPlacements
13327
+ });
13328
+ this.outputTraces = cycleCollapse.traces;
13095
13329
  this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
13096
13330
  inputProblem: this.input.inputProblem,
13097
- traces: result.traces,
13098
- netLabelPlacements: result.netLabelPlacements
13331
+ traces: cycleCollapse.traces,
13332
+ netLabelPlacements: cycleCollapse.netLabelPlacements
13099
13333
  });
13100
- this.stats.alignedJunctionCount = result.alignedJunctionCount;
13334
+ this.stats.alignedJunctionCount = alignment.alignedJunctionCount;
13335
+ this.stats.collapsedCycleCount = cycleCollapse.collapsedCycleCount;
13101
13336
  this.solved = true;
13102
13337
  }
13103
13338
  getOutput() {
@@ -11,12 +11,21 @@ const MAX_LOCAL_GROUND_BRANCH_OFFSET = 1
11
11
  export const getGroundConnectionPolicy = (inputProblem: InputProblem) => {
12
12
  const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem)
13
13
  const groundNetId = netConnMap.getNetConnectedToId("GND")
14
- if (!groundNetId) return () => true
15
14
  // Net identifiers do not constitute physical edges: two separate direct
16
15
  // connections may have the same netId without requesting a wire between them.
17
16
  const physicalConnMap = new ConnectivityMap({})
17
+ const explicitlyWiredGroundNetIds = new Set<string>()
18
18
  for (const connection of inputProblem.directConnections) {
19
19
  physicalConnMap.addConnections([connection.pinIds])
20
+ const globalNetId = netConnMap.getNetConnectedToId(connection.pinIds[0])
21
+ const isMarkedGround = inputProblem.netConnections.some(
22
+ (net) =>
23
+ net.isGround &&
24
+ netConnMap.getNetConnectedToId(net.netId) === globalNetId,
25
+ )
26
+ if (globalNetId && isMarkedGround) {
27
+ explicitlyWiredGroundNetIds.add(globalNetId)
28
+ }
20
29
  }
21
30
  const pins = new Map(
22
31
  inputProblem.chips.flatMap((chip) =>
@@ -46,7 +55,29 @@ export const getGroundConnectionPolicy = (inputProblem: InputProblem) => {
46
55
  )
47
56
  )
48
57
  })
49
- if (!hasExplicitParallelGroundRail) return () => true
58
+ const areSeparateExplicitGroundIslands = (
59
+ firstPinId: PinId,
60
+ secondPinId: PinId,
61
+ ) => {
62
+ const firstGlobalNetId = netConnMap.getNetConnectedToId(firstPinId)
63
+ const secondGlobalNetId = netConnMap.getNetConnectedToId(secondPinId)
64
+ if (
65
+ !firstGlobalNetId ||
66
+ firstGlobalNetId !== secondGlobalNetId ||
67
+ !explicitlyWiredGroundNetIds.has(firstGlobalNetId)
68
+ ) {
69
+ return false
70
+ }
71
+ const firstPhysicalGroup =
72
+ physicalConnMap.getNetConnectedToId(firstPinId) ?? `pin:${firstPinId}`
73
+ const secondPhysicalGroup =
74
+ physicalConnMap.getNetConnectedToId(secondPinId) ?? `pin:${secondPinId}`
75
+ return firstPhysicalGroup !== secondPhysicalGroup
76
+ }
77
+ if (!hasExplicitParallelGroundRail) {
78
+ return (firstPinId: PinId, secondPinId: PinId) =>
79
+ !areSeparateExplicitGroundIslands(firstPinId, secondPinId)
80
+ }
50
81
  const isGroundFacingTerminal = (pinId: PinId) => {
51
82
  const entry = pins.get(pinId)
52
83
  return (
@@ -58,10 +89,13 @@ export const getGroundConnectionPolicy = (inputProblem: InputProblem) => {
58
89
  }
59
90
 
60
91
  return (firstPinId: PinId, secondPinId: PinId): boolean => {
92
+ const firstGlobalNetId = netConnMap.getNetConnectedToId(firstPinId)
93
+ const secondGlobalNetId = netConnMap.getNetConnectedToId(secondPinId)
94
+ if (areSeparateExplicitGroundIslands(firstPinId, secondPinId)) return false
61
95
  if (
62
96
  !groundNetId ||
63
- netConnMap.getNetConnectedToId(firstPinId) !== groundNetId ||
64
- netConnMap.getNetConnectedToId(secondPinId) !== groundNetId
97
+ firstGlobalNetId !== groundNetId ||
98
+ secondGlobalNetId !== groundNetId
65
99
  ) {
66
100
  return true
67
101
  }
@@ -6,6 +6,7 @@ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/
6
6
  import type { InputProblem } from "lib/types/InputProblem"
7
7
  import { getColorFromString } from "lib/utils/getColorFromString"
8
8
  import { alignSameNetJunctions } from "./alignSameNetJunctions"
9
+ import { collapseSameNetCycles } from "./collapseSameNetCycles"
9
10
  import { placeGroundRailLabelsAtOuterEnd } from "./placeGroundRailLabelsAtOuterEnd"
10
11
 
11
12
  interface SameNetJunctionAlignmentSolverInput {
@@ -28,14 +29,19 @@ export class SameNetJunctionAlignmentSolver extends BaseSolver {
28
29
  }
29
30
 
30
31
  override _step() {
31
- const result = alignSameNetJunctions(this.input)
32
- this.outputTraces = result.traces
32
+ const alignment = alignSameNetJunctions(this.input)
33
+ const cycleCollapse = collapseSameNetCycles({
34
+ traces: alignment.traces,
35
+ netLabelPlacements: alignment.netLabelPlacements,
36
+ })
37
+ this.outputTraces = cycleCollapse.traces
33
38
  this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
34
39
  inputProblem: this.input.inputProblem,
35
- traces: result.traces,
36
- netLabelPlacements: result.netLabelPlacements,
40
+ traces: cycleCollapse.traces,
41
+ netLabelPlacements: cycleCollapse.netLabelPlacements,
37
42
  })
38
- this.stats.alignedJunctionCount = result.alignedJunctionCount
43
+ this.stats.alignedJunctionCount = alignment.alignedJunctionCount
44
+ this.stats.collapsedCycleCount = cycleCollapse.collapsedCycleCount
39
45
  this.solved = true
40
46
  }
41
47
 
@@ -41,7 +41,7 @@ const MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2
41
41
  const MAX_SHARED_PIN_RAIL_OFFSET = 0.05
42
42
  const MIN_RETURN_STEM_LENGTH = 0.05
43
43
 
44
- const getSharedPin = ({
44
+ export const getSharedPin = ({
45
45
  donorTrace,
46
46
  branchTrace,
47
47
  }: {