@tscircuit/schematic-trace-solver 0.0.166 → 0.0.168

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))
@@ -11560,10 +11582,10 @@ function isPointOnPath(point, path) {
11560
11582
  }
11561
11583
  function buildMergedObstacleLabel(seedLabel, allLabels) {
11562
11584
  const TOUCH_MARGIN = 0.01;
11563
- const getBounds4 = (l) => getRectBounds(l.center, l.width, l.height);
11585
+ const getBounds3 = (l) => getRectBounds(l.center, l.width, l.height);
11564
11586
  const touches = (a, b) => {
11565
- const ba = getBounds4(a);
11566
- const bb = getBounds4(b);
11587
+ const ba = getBounds3(a);
11588
+ const bb = getBounds3(b);
11567
11589
  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;
11568
11590
  };
11569
11591
  const group = /* @__PURE__ */ new Set([seedLabel]);
@@ -11580,7 +11602,7 @@ function buildMergedObstacleLabel(seedLabel, allLabels) {
11580
11602
  }
11581
11603
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
11582
11604
  for (const l of group) {
11583
- const b = getBounds4(l);
11605
+ const b = getBounds3(l);
11584
11606
  if (b.minX < minX) minX = b.minX;
11585
11607
  if (b.minY < minY) minY = b.minY;
11586
11608
  if (b.maxX > maxX) maxX = b.maxX;
@@ -13814,12 +13836,28 @@ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
13814
13836
  return segments.sort((a, b) => b.length - a.length);
13815
13837
  };
13816
13838
 
13839
+ // lib/solvers/InlineNetLabelSolver/getAnchoredNetLabelRenderedBounds.ts
13840
+ var getAnchoredNetLabelRenderedBounds = (placement) => {
13841
+ if (placement.orientation !== "x-" && placement.orientation !== "x+") {
13842
+ return getRectBounds(placement.center, placement.width, placement.height);
13843
+ }
13844
+ const width = Math.max(placement.width, placement.height);
13845
+ const height = NET_LABEL_HORIZONTAL_HEIGHT;
13846
+ const center = getCenterFromAnchor(
13847
+ placement.anchorPoint,
13848
+ placement.orientation,
13849
+ width,
13850
+ height
13851
+ );
13852
+ return getRectBounds(center, width, height);
13853
+ };
13854
+
13817
13855
  // lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts
13818
13856
  var LABEL_CLEARANCE2 = 0.05;
13819
13857
  var POINT_EPSILON = 1e-6;
13820
13858
  var CONTIGUOUS_LABEL_GAP = 0.01;
13821
13859
  var MAX_OUTWARD_DISTANCE = 5;
13822
- var getBounds3 = (placement) => {
13860
+ var getInlineBounds = (placement) => {
13823
13861
  const renderedWidth = placement.axis === "y" ? placement.height : placement.width;
13824
13862
  const renderedHeight = placement.axis === "y" ? placement.width : placement.height;
13825
13863
  return {
@@ -13861,7 +13899,7 @@ var isPointOnPath2 = (point, path) => {
13861
13899
  return false;
13862
13900
  };
13863
13901
  var getRequiredOutwardDistance = (label, inlineBounds) => {
13864
- const labelBounds = getBounds3(label);
13902
+ const labelBounds = getAnchoredNetLabelRenderedBounds(label);
13865
13903
  if (label.orientation === "x-" || label.orientation === "x+") {
13866
13904
  const nearby2 = inlineBounds.filter(
13867
13905
  (bounds) => labelBounds.minY < bounds.maxY && labelBounds.maxY > bounds.minY
@@ -13988,8 +14026,8 @@ var getContiguousLabelGroup = ({
13988
14026
  if (group.has(labelIndex)) continue;
13989
14027
  if ([...group].some(
13990
14028
  (memberIndex) => boundsGapOnPerpendicularAxis(
13991
- getBounds3(labels[memberIndex]),
13992
- getBounds3(label),
14029
+ getAnchoredNetLabelRenderedBounds(labels[memberIndex]),
14030
+ getAnchoredNetLabelRenderedBounds(label),
13993
14031
  trigger.orientation
13994
14032
  ) <= CONTIGUOUS_LABEL_GAP
13995
14033
  )) {
@@ -14011,7 +14049,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14011
14049
  tracePath: trace.tracePath.map((point) => ({ ...point }))
14012
14050
  }));
14013
14051
  const outputLabels = netLabelPlacements.map((label) => ({ ...label }));
14014
- const inlineBounds = inlineNetLabelPlacements.map(getBounds3);
14052
+ const inlineBounds = inlineNetLabelPlacements.map(getInlineBounds);
14015
14053
  const pinMap = getPinMap(inputProblem);
14016
14054
  const chipIdByPinId = /* @__PURE__ */ new Map();
14017
14055
  for (const chip of inputProblem.chips) {
@@ -14034,7 +14072,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14034
14072
  for (let iteration = 0; iteration < outputLabels.length; iteration++) {
14035
14073
  let adjustedObstacle = false;
14036
14074
  for (const [movingIndex, movingDistance] of distances) {
14037
- const movingBounds = getBounds3(
14075
+ const movingBounds = getAnchoredNetLabelRenderedBounds(
14038
14076
  moveLabel(
14039
14077
  outputLabels[movingIndex],
14040
14078
  trigger.orientation,
@@ -14045,7 +14083,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14045
14083
  if (group.has(obstacleIndex)) continue;
14046
14084
  const obstacle = outputLabels[obstacleIndex];
14047
14085
  const existingObstacleDistance = distances.get(obstacleIndex) ?? 0;
14048
- const obstacleBounds = getBounds3(
14086
+ const obstacleBounds = getAnchoredNetLabelRenderedBounds(
14049
14087
  moveLabel(obstacle, trigger.orientation, existingObstacleDistance)
14050
14088
  );
14051
14089
  if (!boundsOverlap(movingBounds, obstacleBounds)) continue;
@@ -14054,7 +14092,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14054
14092
  break;
14055
14093
  }
14056
14094
  const shoveDistance = getDistanceToShoveBoundsPast(
14057
- getBounds3(obstacle),
14095
+ getAnchoredNetLabelRenderedBounds(obstacle),
14058
14096
  movingBounds,
14059
14097
  trigger.orientation
14060
14098
  );
@@ -14083,7 +14121,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14083
14121
  }
14084
14122
  const finalLabelAt = (labelIndex) => proposals.get(labelIndex) ?? outputLabels[labelIndex];
14085
14123
  for (const [labelIndex, movedLabel] of proposals) {
14086
- const movedBounds = getBounds3(movedLabel);
14124
+ const movedBounds = getAnchoredNetLabelRenderedBounds(movedLabel);
14087
14125
  if (inlineBounds.some((bounds) => boundsOverlap(movedBounds, bounds))) {
14088
14126
  failed = true;
14089
14127
  break;
@@ -14102,7 +14140,10 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14102
14140
  break;
14103
14141
  }
14104
14142
  if (outputLabels.some(
14105
- (_, otherIndex) => otherIndex !== labelIndex && boundsOverlap(movedBounds, getBounds3(finalLabelAt(otherIndex)))
14143
+ (_, otherIndex) => otherIndex !== labelIndex && boundsOverlap(
14144
+ movedBounds,
14145
+ getAnchoredNetLabelRenderedBounds(finalLabelAt(otherIndex))
14146
+ )
14106
14147
  )) {
14107
14148
  failed = true;
14108
14149
  break;
@@ -14147,7 +14188,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
14147
14188
  ) || outputLabels.some(
14148
14189
  (_, otherIndex) => otherIndex !== labelIndex && pathIntersectsBounds(
14149
14190
  connector.tracePath,
14150
- getBounds3(finalLabelAt(otherIndex))
14191
+ getAnchoredNetLabelRenderedBounds(finalLabelAt(otherIndex))
14151
14192
  )
14152
14193
  );
14153
14194
  if (connectorObstructed) {
@@ -14183,7 +14224,7 @@ var getStubDirection2 = (path) => {
14183
14224
  }
14184
14225
  return end.y >= start.y ? "y+" : "y-";
14185
14226
  };
14186
- var getLabelBounds3 = (placement) => {
14227
+ var getInlineLabelBounds = (placement) => {
14187
14228
  const renderedWidth = placement.axis === "y" ? placement.height : placement.width;
14188
14229
  const renderedHeight = placement.axis === "y" ? placement.width : placement.height;
14189
14230
  return {
@@ -14283,12 +14324,12 @@ var pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
14283
14324
  direction,
14284
14325
  distance7
14285
14326
  );
14286
- const shiftedBounds = getLabelBounds3(shiftedPlacement);
14327
+ const shiftedBounds = getInlineLabelBounds(shiftedPlacement);
14287
14328
  for (const anchoredPlacement of anchoredNetLabelPlacements) {
14288
14329
  if (anchoredPlacement.globalConnNetId === placement.globalConnNetId) {
14289
14330
  continue;
14290
14331
  }
14291
- const anchoredBounds = getLabelBounds3(anchoredPlacement);
14332
+ const anchoredBounds = getAnchoredNetLabelRenderedBounds(anchoredPlacement);
14292
14333
  if (!boundsOverlap(shiftedBounds, anchoredBounds)) continue;
14293
14334
  requiredAdditionalDistance = Math.max(
14294
14335
  requiredAdditionalDistance,
@@ -14312,13 +14353,13 @@ var pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
14312
14353
  const originalEnd = originalPlacement.stubTracePath[1];
14313
14354
  const shiftedEnd = proposal.stubTracePath[1];
14314
14355
  const addedStubSegment = [originalEnd, shiftedEnd];
14315
- const labelBounds = getLabelBounds3(proposal);
14356
+ const labelBounds = getInlineLabelBounds(proposal);
14316
14357
  const ownerChipId = group[proposalIndex].ownerChipId;
14317
14358
  if (anchoredNetLabelPlacements.some((anchoredPlacement) => {
14318
14359
  if (anchoredPlacement.globalConnNetId === proposal.globalConnNetId) {
14319
14360
  return false;
14320
14361
  }
14321
- const anchoredBounds = getLabelBounds3(anchoredPlacement);
14362
+ const anchoredBounds = getAnchoredNetLabelRenderedBounds(anchoredPlacement);
14322
14363
  return boundsOverlap(labelBounds, anchoredBounds) || doesPathIntersectBounds2(addedStubSegment, anchoredBounds);
14323
14364
  })) {
14324
14365
  return true;
@@ -14351,7 +14392,7 @@ var pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
14351
14392
  }
14352
14393
  }
14353
14394
  for (const fixedPlacement of fixedInlinePlacements) {
14354
- const fixedBounds = getLabelBounds3(fixedPlacement);
14395
+ const fixedBounds = getInlineLabelBounds(fixedPlacement);
14355
14396
  if (boundsOverlap(labelBounds, fixedBounds) || doesPathIntersectBounds2(addedStubSegment, fixedBounds) || fixedPlacement.stubTracePath && doesPathIntersectBounds2(fixedPlacement.stubTracePath, labelBounds)) {
14356
14397
  return true;
14357
14398
  }
@@ -14398,12 +14439,6 @@ var getInlinePlacementBounds = (placement) => {
14398
14439
  maxY: placement.center.y + renderedHeight / 2
14399
14440
  };
14400
14441
  };
14401
- var getAnchoredPlacementBounds = (placement) => ({
14402
- minX: placement.center.x - placement.width / 2,
14403
- maxX: placement.center.x + placement.width / 2,
14404
- minY: placement.center.y - placement.height / 2,
14405
- maxY: placement.center.y + placement.height / 2
14406
- });
14407
14442
  var InlineNetLabelSolver = class extends BaseSolver {
14408
14443
  inputProblem;
14409
14444
  traces;
@@ -14973,7 +15008,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
14973
15008
  if (anchoredPlacement.globalConnNetId === inlinePlacement.globalConnNetId) {
14974
15009
  continue;
14975
15010
  }
14976
- const anchoredBounds = getAnchoredPlacementBounds(anchoredPlacement);
15011
+ const anchoredBounds = getAnchoredNetLabelRenderedBounds(anchoredPlacement);
14977
15012
  if (boundsOverlap(inlineBounds, anchoredBounds) || inlinePlacement.stubTracePath && doesPathIntersectBounds3(
14978
15013
  inlinePlacement.stubTracePath,
14979
15014
  anchoredBounds
@@ -21,6 +21,7 @@ import {
21
21
  type AxisAlignedSegment,
22
22
  getAxisAlignedSegments,
23
23
  } from "./getAxisAlignedSegments"
24
+ import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
24
25
  import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
25
26
  import { pushInlineTerminalLabelsAwayFromAnchoredLabels } from "./pushInlineTerminalLabelsAwayFromAnchoredLabels"
26
27
 
@@ -157,13 +158,6 @@ const getInlinePlacementBounds = (
157
158
  }
158
159
  }
159
160
 
160
- const getAnchoredPlacementBounds = (placement: NetLabelPlacement): Bounds => ({
161
- minX: placement.center.x - placement.width / 2,
162
- maxX: placement.center.x + placement.width / 2,
163
- minY: placement.center.y - placement.height / 2,
164
- maxY: placement.center.y + placement.height / 2,
165
- })
166
-
167
161
  /**
168
162
  * Places "inline net labels" - net names drawn alongside the trace they belong
169
163
  * to - for connections that opted in via `allowInlineNetLabel`.
@@ -1007,7 +1001,8 @@ export class InlineNetLabelSolver extends BaseSolver {
1007
1001
  ) {
1008
1002
  continue
1009
1003
  }
1010
- const anchoredBounds = getAnchoredPlacementBounds(anchoredPlacement)
1004
+ const anchoredBounds =
1005
+ getAnchoredNetLabelRenderedBounds(anchoredPlacement)
1011
1006
  if (
1012
1007
  boundsOverlap(inlineBounds, anchoredBounds) ||
1013
1008
  (inlinePlacement.stubTracePath &&
@@ -0,0 +1,34 @@
1
+ import type { Bounds } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import {
4
+ getCenterFromAnchor,
5
+ getRectBounds,
6
+ NET_LABEL_HORIZONTAL_HEIGHT,
7
+ } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
8
+
9
+ /**
10
+ * Returns the visible schematic-world bounds of a conventional net label.
11
+ *
12
+ * Horizontal labels render as tags whose text remains horizontal. Rail inputs
13
+ * encode their vertical-symbol dimensions in width/height, so a rail label
14
+ * that later falls back to an x-facing tag can have those extents transposed.
15
+ * Normalize that tag to the renderer's horizontal envelope before using it as
16
+ * an obstacle.
17
+ */
18
+ export const getAnchoredNetLabelRenderedBounds = (
19
+ placement: NetLabelPlacement,
20
+ ): Bounds => {
21
+ if (placement.orientation !== "x-" && placement.orientation !== "x+") {
22
+ return getRectBounds(placement.center, placement.width, placement.height)
23
+ }
24
+
25
+ const width = Math.max(placement.width, placement.height)
26
+ const height = NET_LABEL_HORIZONTAL_HEIGHT
27
+ const center = getCenterFromAnchor(
28
+ placement.anchorPoint,
29
+ placement.orientation,
30
+ width,
31
+ height,
32
+ )
33
+ return getRectBounds(center, width, height)
34
+ }
@@ -8,6 +8,7 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
8
8
  import type { InputProblem } from "lib/types/InputProblem"
9
9
  import { dir, type FacingDirection } from "lib/utils/dir"
10
10
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
11
+ import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
11
12
  import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
12
13
 
13
14
  const LABEL_CLEARANCE = 0.05
@@ -15,12 +16,7 @@ const POINT_EPSILON = 1e-6
15
16
  const CONTIGUOUS_LABEL_GAP = 0.01
16
17
  const MAX_OUTWARD_DISTANCE = 5
17
18
 
18
- const getBounds = (placement: {
19
- center: Point
20
- width: number
21
- height: number
22
- axis?: "x" | "y"
23
- }): Bounds => {
19
+ const getInlineBounds = (placement: InlineNetLabelPlacement): Bounds => {
24
20
  const renderedWidth =
25
21
  placement.axis === "y" ? placement.height : placement.width
26
22
  const renderedHeight =
@@ -79,7 +75,7 @@ const getRequiredOutwardDistance = (
79
75
  label: NetLabelPlacement,
80
76
  inlineBounds: Bounds[],
81
77
  ) => {
82
- const labelBounds = getBounds(label)
78
+ const labelBounds = getAnchoredNetLabelRenderedBounds(label)
83
79
 
84
80
  if (label.orientation === "x-" || label.orientation === "x+") {
85
81
  const nearby = inlineBounds.filter(
@@ -272,8 +268,8 @@ const getContiguousLabelGroup = ({
272
268
  [...group].some(
273
269
  (memberIndex) =>
274
270
  boundsGapOnPerpendicularAxis(
275
- getBounds(labels[memberIndex]!),
276
- getBounds(label),
271
+ getAnchoredNetLabelRenderedBounds(labels[memberIndex]!),
272
+ getAnchoredNetLabelRenderedBounds(label),
277
273
  trigger.orientation,
278
274
  ) <= CONTIGUOUS_LABEL_GAP,
279
275
  )
@@ -316,7 +312,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
316
312
  tracePath: trace.tracePath.map((point) => ({ ...point })),
317
313
  }))
318
314
  const outputLabels = netLabelPlacements.map((label) => ({ ...label }))
319
- const inlineBounds = inlineNetLabelPlacements.map(getBounds)
315
+ const inlineBounds = inlineNetLabelPlacements.map(getInlineBounds)
320
316
  const pinMap = getPinMap(inputProblem)
321
317
  const chipIdByPinId = new Map<string, string>()
322
318
  for (const chip of inputProblem.chips) {
@@ -346,7 +342,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
346
342
  for (let iteration = 0; iteration < outputLabels.length; iteration++) {
347
343
  let adjustedObstacle = false
348
344
  for (const [movingIndex, movingDistance] of distances) {
349
- const movingBounds = getBounds(
345
+ const movingBounds = getAnchoredNetLabelRenderedBounds(
350
346
  moveLabel(
351
347
  outputLabels[movingIndex]!,
352
348
  trigger.orientation,
@@ -361,7 +357,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
361
357
  if (group.has(obstacleIndex)) continue
362
358
  const obstacle = outputLabels[obstacleIndex]!
363
359
  const existingObstacleDistance = distances.get(obstacleIndex) ?? 0
364
- const obstacleBounds = getBounds(
360
+ const obstacleBounds = getAnchoredNetLabelRenderedBounds(
365
361
  moveLabel(obstacle, trigger.orientation, existingObstacleDistance),
366
362
  )
367
363
  if (!boundsOverlap(movingBounds, obstacleBounds)) continue
@@ -374,7 +370,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
374
370
  break
375
371
  }
376
372
  const shoveDistance = getDistanceToShoveBoundsPast(
377
- getBounds(obstacle),
373
+ getAnchoredNetLabelRenderedBounds(obstacle),
378
374
  movingBounds,
379
375
  trigger.orientation,
380
376
  )
@@ -409,7 +405,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
409
405
  const finalLabelAt = (labelIndex: number) =>
410
406
  proposals.get(labelIndex) ?? outputLabels[labelIndex]!
411
407
  for (const [labelIndex, movedLabel] of proposals) {
412
- const movedBounds = getBounds(movedLabel)
408
+ const movedBounds = getAnchoredNetLabelRenderedBounds(movedLabel)
413
409
  if (inlineBounds.some((bounds) => boundsOverlap(movedBounds, bounds))) {
414
410
  failed = true
415
411
  break
@@ -434,7 +430,10 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
434
430
  outputLabels.some(
435
431
  (_, otherIndex) =>
436
432
  otherIndex !== labelIndex &&
437
- boundsOverlap(movedBounds, getBounds(finalLabelAt(otherIndex))),
433
+ boundsOverlap(
434
+ movedBounds,
435
+ getAnchoredNetLabelRenderedBounds(finalLabelAt(otherIndex)),
436
+ ),
438
437
  )
439
438
  ) {
440
439
  failed = true
@@ -503,7 +502,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
503
502
  otherIndex !== labelIndex &&
504
503
  pathIntersectsBounds(
505
504
  connector.tracePath,
506
- getBounds(finalLabelAt(otherIndex)),
505
+ getAnchoredNetLabelRenderedBounds(finalLabelAt(otherIndex)),
507
506
  ),
508
507
  )
509
508
  if (connectorObstructed) {
@@ -3,6 +3,7 @@ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetL
3
3
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
4
  import type { InputProblem } from "lib/types/InputProblem"
5
5
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
6
+ import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
6
7
  import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
7
8
 
8
9
  type StubDirection = "x+" | "x-" | "y+" | "y-"
@@ -18,12 +19,7 @@ const getStubDirection = (path: [Point, Point]): StubDirection => {
18
19
  return end.y >= start.y ? "y+" : "y-"
19
20
  }
20
21
 
21
- const getLabelBounds = (placement: {
22
- center: Point
23
- width: number
24
- height: number
25
- axis?: "x" | "y"
26
- }): Bounds => {
22
+ const getInlineLabelBounds = (placement: InlineNetLabelPlacement): Bounds => {
27
23
  const renderedWidth =
28
24
  placement.axis === "y" ? placement.height : placement.width
29
25
  const renderedHeight =
@@ -178,12 +174,13 @@ export const pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
178
174
  direction,
179
175
  distance,
180
176
  )
181
- const shiftedBounds = getLabelBounds(shiftedPlacement)
177
+ const shiftedBounds = getInlineLabelBounds(shiftedPlacement)
182
178
  for (const anchoredPlacement of anchoredNetLabelPlacements) {
183
179
  if (anchoredPlacement.globalConnNetId === placement.globalConnNetId) {
184
180
  continue
185
181
  }
186
- const anchoredBounds = getLabelBounds(anchoredPlacement)
182
+ const anchoredBounds =
183
+ getAnchoredNetLabelRenderedBounds(anchoredPlacement)
187
184
  if (!boundsOverlap(shiftedBounds, anchoredBounds)) continue
188
185
  requiredAdditionalDistance = Math.max(
189
186
  requiredAdditionalDistance,
@@ -209,7 +206,7 @@ export const pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
209
206
  const originalEnd = originalPlacement.stubTracePath![1]
210
207
  const shiftedEnd = proposal.stubTracePath![1]
211
208
  const addedStubSegment: [Point, Point] = [originalEnd, shiftedEnd]
212
- const labelBounds = getLabelBounds(proposal)
209
+ const labelBounds = getInlineLabelBounds(proposal)
213
210
  const ownerChipId = group[proposalIndex]!.ownerChipId
214
211
 
215
212
  if (
@@ -217,7 +214,8 @@ export const pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
217
214
  if (anchoredPlacement.globalConnNetId === proposal.globalConnNetId) {
218
215
  return false
219
216
  }
220
- const anchoredBounds = getLabelBounds(anchoredPlacement)
217
+ const anchoredBounds =
218
+ getAnchoredNetLabelRenderedBounds(anchoredPlacement)
221
219
  return (
222
220
  boundsOverlap(labelBounds, anchoredBounds) ||
223
221
  doesPathIntersectBounds(addedStubSegment, anchoredBounds)
@@ -267,7 +265,7 @@ export const pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
267
265
  }
268
266
 
269
267
  for (const fixedPlacement of fixedInlinePlacements) {
270
- const fixedBounds = getLabelBounds(fixedPlacement)
268
+ const fixedBounds = getInlineLabelBounds(fixedPlacement)
271
269
  if (
272
270
  boundsOverlap(labelBounds, fixedBounds) ||
273
271
  doesPathIntersectBounds(addedStubSegment, fixedBounds) ||
@@ -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
  }
@@ -98,7 +98,11 @@ export interface InputNetConnection {
98
98
  netId: string
99
99
  pinIds: Array<PinId>
100
100
 
101
- /** True when the authoritative source net is electrical ground. */
101
+ /**
102
+ * Preserve explicitly wired islands on this ground net, joining the islands
103
+ * electrically through labels instead of routing between them. Nets without
104
+ * explicit traces, or without this hint, retain automatic bus routing.
105
+ */
102
106
  isGround?: boolean
103
107
 
104
108
  /**
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.166",
8
+ "version": "0.0.168",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",