@tscircuit/schematic-trace-solver 0.0.174 → 0.0.176

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.
Files changed (63) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +759 -91
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +5 -1
  4. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +16 -1
  5. package/lib/solvers/InlineNetLabelSolver/restoreReroutesAroundSupersededLabels.ts +174 -0
  6. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +2 -30
  7. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +2 -0
  8. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +748 -15
  9. package/lib/solvers/SameNetJunctionAlignmentSolver/findNearestSharedPinExitRail.ts +140 -0
  10. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +12 -5
  11. package/lib/utils/doesPathCoincideWithTraces.ts +17 -4
  12. package/lib/utils/pathIntersectsRenderedLabel.ts +33 -0
  13. package/package.json +1 -1
  14. package/site/bug-reports/bug-report-20260901T055358Z.page.tsx +4 -0
  15. package/site/bug-reports/bug-report-20260901T064117Z.page.tsx +4 -0
  16. package/site/bug-reports/bug-report-20260901T134241Z.page.tsx +4 -0
  17. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
  18. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.test.ts +9 -0
  19. package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +2 -2
  21. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  23. package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.test.ts +8 -4
  24. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
  25. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +2 -2
  26. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +2 -2
  27. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +2 -2
  28. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +6 -6
  29. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +676 -0
  30. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.json +2467 -0
  31. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +59 -0
  32. package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +134 -0
  33. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.json +215 -0
  34. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +12 -0
  35. package/tests/bug-reports/bug-report-20260901T134241Z/__snapshots__/bug-report-20260901T134241Z.snap.svg +115 -0
  36. package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.json +359 -0
  37. package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.test.ts +60 -0
  38. package/tests/examples/__snapshots__/example51.snap.svg +2 -2
  39. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +10 -10
  40. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +2 -2
  41. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +2 -2
  42. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +10 -10
  43. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +2 -2
  44. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +18 -18
  45. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +18 -18
  46. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +10 -10
  47. package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +2 -2
  48. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +2 -2
  49. package/tests/repros/__snapshots__/repro-tida-01389-hbridge.snap.svg +2 -2
  50. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +2 -2
  51. package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +134 -0
  52. package/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg +164 -0
  53. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +2 -2
  54. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  55. package/tests/repros/assets/repro-trellis-core-gnd-net-label-overlap.input.json +178 -0
  56. package/tests/repros/assets/repro-wireless-mouse-charger-section.input.json +591 -0
  57. package/tests/repros/repro-isolated-rs485-isow7841.test.ts +4 -1
  58. package/tests/repros/repro-powerbank-3v-system-power.test.ts +24 -0
  59. package/tests/repros/repro-tida-01389-hbridge.test.ts +24 -0
  60. package/tests/repros/repro-trellis-core-gnd-net-label-overlap.test.ts +73 -0
  61. package/tests/repros/repro-wireless-mouse-charger-section.test.ts +16 -0
  62. package/tests/solvers/InlineNetLabelSolver/restore-reroutes-around-superseded-labels.test.ts +186 -0
  63. package/tests/solvers/SameNetJunctionAlignmentSolver/parallel-load-rails.test.ts +1 -0
@@ -1,11 +1,17 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
3
3
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
4
6
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
7
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
6
8
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
9
+ import { getMovedAnchorPointForReroute } from "lib/solvers/Example28Solver/getMovedAnchorPointForReroute"
7
10
  import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
8
- import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
11
+ import {
12
+ rectsOverlap,
13
+ tracePathContainsPoint,
14
+ } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
9
15
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
10
16
  import {
11
17
  getVisibleTraceLength,
@@ -20,11 +26,17 @@ import {
20
26
  pathEntersAnyNetLabel,
21
27
  pathIntersectsAnyNetLabel,
22
28
  } from "./pathIntersectsAnyNetLabel"
29
+ import {
30
+ findNearestSharedPinExitRail,
31
+ getPinFacingAxis,
32
+ isCoordinateOnPinFacingSide,
33
+ } from "./findNearestSharedPinExitRail"
23
34
 
24
35
  interface AlignSameNetJunctionsInput {
25
36
  inputProblem: InputProblem
26
37
  traces: SolvedTracePath[]
27
38
  netLabelPlacements: NetLabelPlacement[]
39
+ netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
28
40
  }
29
41
 
30
42
  interface HorizontalSegment {
@@ -39,8 +51,18 @@ const MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2
39
51
  // A shared vertical pin can anchor an existing rail when the rail is only a
40
52
  // symbol-stem correction away from the pin itself.
41
53
  const MAX_SHARED_PIN_RAIL_OFFSET = 0.05
54
+ // Nearby rails that leave the same junction can be combined even when the
55
+ // shared rail is slightly longer. The visible result has one continuous run
56
+ // instead of two parallel runs joined by a jog.
57
+ const MAX_SHARED_ENDPOINT_RAIL_OFFSET = 0.8
42
58
  const MIN_RETURN_STEM_LENGTH = 0.05
43
59
 
60
+ const getSegmentAxis = (start: Point, end: Point) => {
61
+ if (isHorizontal(start, end)) return "x" as const
62
+ if (isVertical(start, end)) return "y" as const
63
+ return null
64
+ }
65
+
44
66
  export const getSharedPin = ({
45
67
  donorTrace,
46
68
  branchTrace,
@@ -264,11 +286,591 @@ const getAlignedBranchPath = ({
264
286
  return [...sharedToOther].reverse()
265
287
  }
266
288
 
289
+ /**
290
+ * Reuse the row of an adjacent same-side pin for a branch leaving their
291
+ * shared outer stem. This removes the extra parallel jog produced when the
292
+ * branch initially routes beyond both pin rows before approaching a
293
+ * perpendicular target pin. The topology is detected from pin-facing axes
294
+ * and shared segments; the number and ordering of intermediate turns do not
295
+ * define eligibility.
296
+ */
297
+ const getAlignedParallelPinExitPath = ({
298
+ donorTrace,
299
+ branchTrace,
300
+ }: {
301
+ donorTrace: SolvedTracePath
302
+ branchTrace: SolvedTracePath
303
+ }): Point[] | null => {
304
+ const sharedPin = getSharedPin({ donorTrace, branchTrace })
305
+ if (!sharedPin) return null
306
+ const adjacentPin = getOtherPin({ trace: donorTrace, sharedPin })
307
+ const targetPin = getOtherPin({ trace: branchTrace, sharedPin })
308
+ if (!adjacentPin || !targetPin) return null
309
+
310
+ const facing = sharedPin._facingDirection
311
+ const departureAxis = getPinFacingAxis(sharedPin)
312
+ if (!facing || !departureAxis) return null
313
+ const stemAxis = departureAxis === "x" ? "y" : "x"
314
+ if (
315
+ adjacentPin.chipId !== sharedPin.chipId ||
316
+ adjacentPin._facingDirection !== facing ||
317
+ !nearlyEqual(adjacentPin[departureAxis], sharedPin[departureAxis]) ||
318
+ getPinFacingAxis(targetPin) !== stemAxis
319
+ ) {
320
+ return null
321
+ }
322
+ const adjacentPinOffset = Math.abs(
323
+ adjacentPin[stemAxis] - sharedPin[stemAxis],
324
+ )
325
+ if (
326
+ adjacentPinOffset > MAX_ALIGNED_LOAD_PIN_OFFSET &&
327
+ !nearlyEqual(adjacentPinOffset, MAX_ALIGNED_LOAD_PIN_OFFSET)
328
+ ) {
329
+ return null
330
+ }
331
+
332
+ const branchStartsAtSharedPin = branchTrace.pins[0].pinId === sharedPin.pinId
333
+ let sharedToTargetPath = simplifyPath(branchTrace.tracePath)
334
+ if (!branchStartsAtSharedPin) sharedToTargetPath.reverse()
335
+ if (sharedToTargetPath.length < 2) return null
336
+ const branchDepartureSegment: [Point, Point] = [
337
+ sharedToTargetPath[0]!,
338
+ sharedToTargetPath[1]!,
339
+ ]
340
+ const sharedExitRail = findNearestSharedPinExitRail({
341
+ donorPath: donorTrace.tracePath,
342
+ branchDepartureSegment,
343
+ sharedPin,
344
+ adjacentPin,
345
+ })
346
+ if (!sharedExitRail) return null
347
+
348
+ const alignedRailCoordinate = adjacentPin[stemAxis]
349
+ if (
350
+ !isCoordinateOnPinFacingSide({
351
+ coordinate: alignedRailCoordinate,
352
+ axis: stemAxis,
353
+ pin: targetPin,
354
+ })
355
+ ) {
356
+ return null
357
+ }
358
+ const alignedStemExit = {
359
+ ...sharedExitRail.sharedPinExit,
360
+ [stemAxis]: alignedRailCoordinate,
361
+ }
362
+ const targetApproach = {
363
+ x: targetPin.x,
364
+ y: targetPin.y,
365
+ [stemAxis]: alignedRailCoordinate,
366
+ }
367
+ const candidate = simplifyPath([
368
+ { x: sharedPin.x, y: sharedPin.y },
369
+ sharedExitRail.sharedPinExit,
370
+ alignedStemExit,
371
+ targetApproach,
372
+ { x: targetPin.x, y: targetPin.y },
373
+ ])
374
+ return branchStartsAtSharedPin ? candidate : candidate.reverse()
375
+ }
376
+
377
+ /**
378
+ * Move the first movable rail of a branch onto the first rail of another
379
+ * trace leaving the same endpoint. The two perpendicular rail intervals must
380
+ * touch after the move; the caller then verifies that the combined visible
381
+ * trace loses a segment and that the reroute is clear.
382
+ */
383
+ const getAlignedSharedEndpointRailPath = ({
384
+ donorTrace,
385
+ branchTrace,
386
+ netLabelConnectorTraceIds,
387
+ }: {
388
+ donorTrace: SolvedTracePath
389
+ branchTrace: SolvedTracePath
390
+ netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
391
+ }): Point[] | null => {
392
+ if (
393
+ netLabelConnectorTraceIds.has(donorTrace.mspPairId) ||
394
+ netLabelConnectorTraceIds.has(branchTrace.mspPairId)
395
+ ) {
396
+ return null
397
+ }
398
+
399
+ const sharedPin = getSharedPin({ donorTrace, branchTrace })
400
+ if (!sharedPin) return null
401
+
402
+ const pathFromShared = (trace: SolvedTracePath) => {
403
+ const path = simplifyPath(trace.tracePath)
404
+ return trace.pins[0]!.pinId === sharedPin.pinId ? path : [...path].reverse()
405
+ }
406
+ const donorPath = pathFromShared(donorTrace)
407
+ const branchPath = pathFromShared(branchTrace)
408
+ // The branch rail must be internal so moving it cannot move either pin.
409
+ if (donorPath.length < 3 || branchPath.length < 4) return null
410
+
411
+ const donorDepartureAxis = getSegmentAxis(donorPath[0]!, donorPath[1]!)
412
+ const branchDepartureAxis = getSegmentAxis(branchPath[0]!, branchPath[1]!)
413
+ if (!donorDepartureAxis || donorDepartureAxis !== branchDepartureAxis) {
414
+ return null
415
+ }
416
+ const donorDeparture =
417
+ donorPath[1]![donorDepartureAxis] - donorPath[0]![donorDepartureAxis]
418
+ const branchDeparture =
419
+ branchPath[1]![branchDepartureAxis] - branchPath[0]![branchDepartureAxis]
420
+ if (Math.sign(donorDeparture) !== Math.sign(branchDeparture)) return null
421
+
422
+ const donorRailAxis = getSegmentAxis(donorPath[1]!, donorPath[2]!)
423
+ const branchRailAxis = getSegmentAxis(branchPath[1]!, branchPath[2]!)
424
+ if (
425
+ !donorRailAxis ||
426
+ donorRailAxis !== branchRailAxis ||
427
+ donorRailAxis === donorDepartureAxis
428
+ ) {
429
+ return null
430
+ }
431
+ // A return bus has another rail with this orientation near its far
432
+ // endpoint. Moving only the first rail would partially rewrite a shape that
433
+ // must be evaluated as a whole by the endpoint-bus transformation.
434
+ let branchParallelRailCount = 0
435
+ for (let index = 1; index < branchPath.length - 2; index++) {
436
+ if (
437
+ getSegmentAxis(branchPath[index]!, branchPath[index + 1]!) ===
438
+ branchRailAxis
439
+ ) {
440
+ branchParallelRailCount++
441
+ }
442
+ }
443
+ if (branchParallelRailCount !== 1) return null
444
+
445
+ const railCoordinateAxis = donorRailAxis === "x" ? "y" : "x"
446
+ const donorCoordinate = donorPath[1]![railCoordinateAxis]
447
+ const branchCoordinate = branchPath[1]![railCoordinateAxis]
448
+ const railOffset = Math.abs(donorCoordinate - branchCoordinate)
449
+ if (
450
+ nearlyEqual(donorCoordinate, branchCoordinate) ||
451
+ railOffset > MAX_SHARED_ENDPOINT_RAIL_OFFSET
452
+ ) {
453
+ return null
454
+ }
455
+
456
+ const donorRange = [
457
+ donorPath[1]![donorRailAxis],
458
+ donorPath[2]![donorRailAxis],
459
+ ].sort((a, b) => a - b)
460
+ const branchRange = [
461
+ branchPath[1]![branchRailAxis],
462
+ branchPath[2]![branchRailAxis],
463
+ ].sort((a, b) => a - b)
464
+ const railsTouchAfterAlignment =
465
+ Math.min(donorRange[1]!, branchRange[1]!) >=
466
+ Math.max(donorRange[0]!, branchRange[0]!) - 1e-6
467
+ if (!railsTouchAfterAlignment) return null
468
+
469
+ const candidateFromShared = simplifyPath(
470
+ branchPath.map((point, index) =>
471
+ index === 1 || index === 2
472
+ ? { ...point, [railCoordinateAxis]: donorCoordinate }
473
+ : point,
474
+ ),
475
+ )
476
+ return branchTrace.pins[0]!.pinId === sharedPin.pinId
477
+ ? candidateFromShared
478
+ : candidateFromShared.reverse()
479
+ }
480
+
481
+ /**
482
+ * Remove the jog between a routed trace and an attached generated-label
483
+ * connector. The connector must initially run along one donor segment, then
484
+ * turn onto a nearby perpendicular rail whose interval touches the donor's
485
+ * adjacent rail after alignment. Only the generated connector is changed.
486
+ */
487
+ const getAlignedAttachedLabelConnectorPath = ({
488
+ donorTrace,
489
+ connectorTrace,
490
+ netLabelConnectorTraceIds,
491
+ }: {
492
+ donorTrace: SolvedTracePath
493
+ connectorTrace: SolvedTracePath
494
+ netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
495
+ }): Point[] | null => {
496
+ if (
497
+ netLabelConnectorTraceIds.has(donorTrace.mspPairId) ||
498
+ !netLabelConnectorTraceIds.has(connectorTrace.mspPairId)
499
+ ) {
500
+ return null
501
+ }
502
+
503
+ const findCandidate = (connectorPath: Point[]) => {
504
+ if (connectorPath.length < 3) return null
505
+ const junction = connectorPath[0]!
506
+ const turn = connectorPath[1]!
507
+ const railEnd = connectorPath[2]!
508
+ const departureAxis = getSegmentAxis(junction, turn)
509
+ const railAxis = getSegmentAxis(turn, railEnd)
510
+ if (!departureAxis || !railAxis || departureAxis === railAxis) return null
511
+
512
+ const donorSegmentIndex = donorTrace.tracePath.findIndex(
513
+ (point, index, path) => {
514
+ const next = path[index + 1]
515
+ return (
516
+ next &&
517
+ getSegmentAxis(point, next) === departureAxis &&
518
+ tracePathContainsPoint([point, next], junction) &&
519
+ tracePathContainsPoint([point, next], turn)
520
+ )
521
+ },
522
+ )
523
+ if (donorSegmentIndex < 0) return null
524
+
525
+ const donorSegmentStart = donorTrace.tracePath[donorSegmentIndex]!
526
+ const donorSegmentEnd = donorTrace.tracePath[donorSegmentIndex + 1]!
527
+ // A connector already attached at a donor corner has no offset junction
528
+ // to remove; moving its rail would rewrite the label's intended escape.
529
+ if (
530
+ (nearlyEqual(junction.x, donorSegmentStart.x) &&
531
+ nearlyEqual(junction.y, donorSegmentStart.y)) ||
532
+ (nearlyEqual(junction.x, donorSegmentEnd.x) &&
533
+ nearlyEqual(junction.y, donorSegmentEnd.y))
534
+ ) {
535
+ return null
536
+ }
537
+ const adjacentDonorRails = [
538
+ donorSegmentIndex > 0
539
+ ? [donorTrace.tracePath[donorSegmentIndex - 1]!, donorSegmentStart]
540
+ : null,
541
+ donorSegmentIndex + 2 < donorTrace.tracePath.length
542
+ ? [donorSegmentEnd, donorTrace.tracePath[donorSegmentIndex + 2]!]
543
+ : null,
544
+ ].flatMap((segment) =>
545
+ segment && getSegmentAxis(segment[0], segment[1]) === railAxis
546
+ ? [segment as [Point, Point]]
547
+ : [],
548
+ )
549
+ if (adjacentDonorRails.length === 0) return null
550
+
551
+ const railCoordinateAxis = railAxis === "x" ? "y" : "x"
552
+ const connectorRailCoordinate = turn[railCoordinateAxis]
553
+ const connectorRange = [turn[railAxis], railEnd[railAxis]].sort(
554
+ (a, b) => a - b,
555
+ )
556
+ const donorRail = adjacentDonorRails
557
+ .map((segment) => ({
558
+ segment,
559
+ coordinate: segment[0][railCoordinateAxis],
560
+ offset: Math.abs(
561
+ segment[0][railCoordinateAxis] - connectorRailCoordinate,
562
+ ),
563
+ }))
564
+ .filter(({ segment, offset }) => {
565
+ if (offset > MAX_SHARED_ENDPOINT_RAIL_OFFSET) return false
566
+ const donorRange = [segment[0][railAxis], segment[1][railAxis]].sort(
567
+ (a, b) => a - b,
568
+ )
569
+ const overlapStart = Math.max(donorRange[0]!, connectorRange[0]!)
570
+ const overlapEnd = Math.min(donorRange[1]!, connectorRange[1]!)
571
+ // Join two end-to-end rails. Do not collapse a connector onto a donor
572
+ // rail when their intervals already overlap, which would reposition a
573
+ // deliberate label branch rather than remove a jog.
574
+ return (
575
+ overlapEnd >= overlapStart - 1e-6 &&
576
+ Math.abs(overlapEnd - overlapStart) <= 1e-6
577
+ )
578
+ })
579
+ .sort((a, b) => a.offset - b.offset)[0]
580
+ if (!donorRail || nearlyEqual(donorRail.offset, 0)) return null
581
+
582
+ return simplifyPath(
583
+ connectorPath.map((point, index) =>
584
+ index === 1 || index === 2
585
+ ? { ...point, [railCoordinateAxis]: donorRail.coordinate }
586
+ : point,
587
+ ),
588
+ )
589
+ }
590
+
591
+ const forwardPath = simplifyPath(connectorTrace.tracePath)
592
+ const forwardCandidate = findCandidate(forwardPath)
593
+ if (forwardCandidate) return forwardCandidate
594
+ const reverseCandidate = findCandidate([...forwardPath].reverse())
595
+ return reverseCandidate ? reverseCandidate.reverse() : null
596
+ }
597
+
598
+ const pathsIntersect = (firstPath: Point[], secondPath: Point[]) => {
599
+ if (
600
+ firstPath.some((point) => tracePathContainsPoint(secondPath, point)) ||
601
+ secondPath.some((point) => tracePathContainsPoint(firstPath, point))
602
+ ) {
603
+ return true
604
+ }
605
+ for (let firstIndex = 1; firstIndex < firstPath.length; firstIndex++) {
606
+ for (let secondIndex = 1; secondIndex < secondPath.length; secondIndex++) {
607
+ if (
608
+ getSegmentIntersection(
609
+ firstPath[firstIndex - 1]!,
610
+ firstPath[firstIndex]!,
611
+ secondPath[secondIndex - 1]!,
612
+ secondPath[secondIndex]!,
613
+ )
614
+ ) {
615
+ return true
616
+ }
617
+ }
618
+ }
619
+ return false
620
+ }
621
+
622
+ /**
623
+ * Keep generated label connectors joined when their host trace moves. Only a
624
+ * connector endpoint is projected, and only along its existing first/last
625
+ * segment, so the label and the rest of its route stay fixed.
626
+ */
627
+ const preserveAttachedLabelConnectorJunctions = ({
628
+ originalTrace,
629
+ candidateTrace,
630
+ traces,
631
+ netLabelConnectorTraceIds,
632
+ }: {
633
+ originalTrace: SolvedTracePath
634
+ candidateTrace: SolvedTracePath
635
+ traces: SolvedTracePath[]
636
+ netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
637
+ }): SolvedTracePath[] | null => {
638
+ let outputTraces = traces.map((trace) =>
639
+ trace.mspPairId === originalTrace.mspPairId ? candidateTrace : trace,
640
+ )
641
+
642
+ for (const connector of traces) {
643
+ if (
644
+ connector.mspPairId === originalTrace.mspPairId ||
645
+ !netLabelConnectorTraceIds.has(connector.mspPairId) ||
646
+ connector.globalConnNetId !== originalTrace.globalConnNetId ||
647
+ !pathsIntersect(originalTrace.tracePath, connector.tracePath) ||
648
+ pathsIntersect(candidateTrace.tracePath, connector.tracePath)
649
+ ) {
650
+ continue
651
+ }
652
+
653
+ const endpointIndex = [0, connector.tracePath.length - 1].find((index) =>
654
+ tracePathContainsPoint(
655
+ originalTrace.tracePath,
656
+ connector.tracePath[index]!,
657
+ ),
658
+ )
659
+ if (endpointIndex === undefined) return null
660
+ const originalJunction = connector.tracePath[endpointIndex]!
661
+ const movedJunction = getMovedAnchorPointForReroute(
662
+ originalJunction,
663
+ originalTrace.tracePath,
664
+ candidateTrace.tracePath,
665
+ )
666
+ if (!movedJunction) return null
667
+
668
+ const neighborIndex = endpointIndex === 0 ? 1 : endpointIndex - 1
669
+ const neighbor = connector.tracePath[neighborIndex]
670
+ if (
671
+ !neighbor ||
672
+ getSegmentAxis(originalJunction, neighbor) !==
673
+ getSegmentAxis(movedJunction, neighbor)
674
+ ) {
675
+ return null
676
+ }
677
+
678
+ const movedConnector = {
679
+ ...connector,
680
+ tracePath: connector.tracePath.map((point, index) =>
681
+ index === endpointIndex ? movedJunction : point,
682
+ ),
683
+ }
684
+ if (!pathsIntersect(candidateTrace.tracePath, movedConnector.tracePath)) {
685
+ return null
686
+ }
687
+ outputTraces = outputTraces.map((trace) =>
688
+ trace.mspPairId === connector.mspPairId ? movedConnector : trace,
689
+ )
690
+ }
691
+
692
+ return outputTraces
693
+ }
694
+
695
+ type PerpendicularPinRail = {
696
+ railCoordinate: number
697
+ }
698
+
699
+ const getPerpendicularPinRail = ({
700
+ trace,
701
+ endpointPin,
702
+ }: {
703
+ trace: SolvedTracePath
704
+ endpointPin: InputPin
705
+ }): PerpendicularPinRail | null => {
706
+ const otherPin = getOtherPin({ trace, sharedPin: endpointPin })
707
+ if (!otherPin) return null
708
+
709
+ const startsAtEndpoint = trace.pins[0].pinId === endpointPin.pinId
710
+ let endpointToOtherPath = simplifyPath(trace.tracePath)
711
+ if (!startsAtEndpoint) endpointToOtherPath.reverse()
712
+ if (endpointToOtherPath.length !== 3) return null
713
+
714
+ const [endpoint, railPoint, other] = endpointToOtherPath
715
+ const endpointFacing = endpointPin._facingDirection
716
+ if (endpointFacing === "y+" || endpointFacing === "y-") {
717
+ if (
718
+ otherPin._facingDirection !== "x+" &&
719
+ otherPin._facingDirection !== "x-"
720
+ ) {
721
+ return null
722
+ }
723
+ if (
724
+ !isVertical(endpoint!, railPoint!) ||
725
+ !isHorizontal(railPoint!, other!) ||
726
+ !railIsOnFacingSide({ railY: railPoint!.y, pin: endpointPin })
727
+ ) {
728
+ return null
729
+ }
730
+ const approachesOtherFromFacingSide =
731
+ otherPin._facingDirection === "x-"
732
+ ? railPoint!.x < otherPin.x
733
+ : railPoint!.x > otherPin.x
734
+ return approachesOtherFromFacingSide
735
+ ? { railCoordinate: railPoint!.y }
736
+ : null
737
+ }
738
+
739
+ if (endpointFacing !== "x+" && endpointFacing !== "x-") return null
740
+ if (
741
+ otherPin._facingDirection !== "y+" &&
742
+ otherPin._facingDirection !== "y-"
743
+ ) {
744
+ return null
745
+ }
746
+ if (!isHorizontal(endpoint!, railPoint!) || !isVertical(railPoint!, other!)) {
747
+ return null
748
+ }
749
+ const railIsOnEndpointFacingSide =
750
+ endpointFacing === "x+"
751
+ ? railPoint!.x > endpointPin.x
752
+ : railPoint!.x < endpointPin.x
753
+ const approachesOtherFromFacingSide =
754
+ otherPin._facingDirection === "y+"
755
+ ? railPoint!.y > otherPin.y
756
+ : railPoint!.y < otherPin.y
757
+ return railIsOnEndpointFacingSide && approachesOtherFromFacingSide
758
+ ? { railCoordinate: railPoint!.x }
759
+ : null
760
+ }
761
+
762
+ /**
763
+ * Level a bus between two same-axis pins with the perpendicular rails that
764
+ * connect those endpoints to their loads. The candidate reuses both existing
765
+ * endpoint stubs, so a long bus does not meet each stub on a nearby parallel
766
+ * rail and leave a visible jog.
767
+ */
768
+ const getAlignedPerpendicularEndpointBusPath = ({
769
+ donorTrace,
770
+ branchTrace,
771
+ traces,
772
+ }: {
773
+ donorTrace: SolvedTracePath
774
+ branchTrace: SolvedTracePath
775
+ traces: SolvedTracePath[]
776
+ }): Point[] | null => {
777
+ const sharedPin = getSharedPin({ donorTrace, branchTrace })
778
+ if (!sharedPin) return null
779
+ const returnPin = getOtherPin({ trace: branchTrace, sharedPin })
780
+ if (!returnPin) return null
781
+
782
+ const getOtherSameNetTracesAtPin = (pin: InputPin) =>
783
+ traces.filter(
784
+ (trace) =>
785
+ trace.mspPairId !== branchTrace.mspPairId &&
786
+ trace.globalConnNetId === branchTrace.globalConnNetId &&
787
+ trace.pinIds.includes(pin.pinId),
788
+ )
789
+ const sharedDonorTraces = getOtherSameNetTracesAtPin(sharedPin)
790
+ const returnDonorTraces = getOtherSameNetTracesAtPin(returnPin)
791
+ if (
792
+ sharedDonorTraces.length !== 1 ||
793
+ sharedDonorTraces[0]!.mspPairId !== donorTrace.mspPairId ||
794
+ returnDonorTraces.length !== 1
795
+ ) {
796
+ return null
797
+ }
798
+
799
+ const sharedRail = getPerpendicularPinRail({
800
+ trace: donorTrace,
801
+ endpointPin: sharedPin,
802
+ })
803
+ if (!sharedRail) return null
804
+
805
+ const facing = sharedPin._facingDirection
806
+ const verticalEndpoints = facing === "y+" || facing === "y-"
807
+ const horizontalEndpoints = facing === "x+" || facing === "x-"
808
+ if (!verticalEndpoints && !horizontalEndpoints) return null
809
+ if (
810
+ verticalEndpoints !==
811
+ (returnPin._facingDirection === "y+" || returnPin._facingDirection === "y-")
812
+ ) {
813
+ return null
814
+ }
815
+
816
+ const branchStartsAtSharedPin = branchTrace.pins[0].pinId === sharedPin.pinId
817
+ let sharedToReturnPath = simplifyPath(branchTrace.tracePath)
818
+ if (!branchStartsAtSharedPin) sharedToReturnPath.reverse()
819
+ if (sharedToReturnPath.length !== 6) return null
820
+
821
+ const expectedSegmentOrientations = verticalEndpoints
822
+ ? ["V", "H", "V", "H", "V"]
823
+ : ["H", "V", "H", "V", "H"]
824
+ const actualSegmentOrientations = sharedToReturnPath
825
+ .slice(1)
826
+ .map((point, index) =>
827
+ isHorizontal(sharedToReturnPath[index]!, point)
828
+ ? "H"
829
+ : isVertical(sharedToReturnPath[index]!, point)
830
+ ? "V"
831
+ : "D",
832
+ )
833
+ if (
834
+ actualSegmentOrientations.some(
835
+ (orientation, index) =>
836
+ orientation !== expectedSegmentOrientations[index],
837
+ )
838
+ ) {
839
+ return null
840
+ }
841
+
842
+ const returnRail = getPerpendicularPinRail({
843
+ trace: returnDonorTraces[0]!,
844
+ endpointPin: returnPin,
845
+ })
846
+ if (!returnRail) return null
847
+
848
+ const candidate = verticalEndpoints
849
+ ? simplifyPath([
850
+ { x: sharedPin.x, y: sharedPin.y },
851
+ { x: sharedPin.x, y: sharedRail.railCoordinate },
852
+ { x: sharedToReturnPath[2]!.x, y: sharedRail.railCoordinate },
853
+ { x: sharedToReturnPath[3]!.x, y: returnRail.railCoordinate },
854
+ { x: returnPin.x, y: returnRail.railCoordinate },
855
+ { x: returnPin.x, y: returnPin.y },
856
+ ])
857
+ : simplifyPath([
858
+ { x: sharedPin.x, y: sharedPin.y },
859
+ { x: sharedRail.railCoordinate, y: sharedPin.y },
860
+ { x: sharedRail.railCoordinate, y: sharedToReturnPath[2]!.y },
861
+ { x: returnRail.railCoordinate, y: sharedToReturnPath[3]!.y },
862
+ { x: returnRail.railCoordinate, y: returnPin.y },
863
+ { x: returnPin.x, y: returnPin.y },
864
+ ])
865
+ return branchStartsAtSharedPin ? candidate : candidate.reverse()
866
+ }
867
+
267
868
  const candidateIsClear = ({
268
869
  candidateTrace,
269
870
  originalTrace,
270
871
  traces,
271
872
  inputProblem,
873
+ originalNetLabelPlacements,
272
874
  candidateNetLabelPlacements,
273
875
  attachedLabelIndexes,
274
876
  }: {
@@ -276,6 +878,7 @@ const candidateIsClear = ({
276
878
  originalTrace: SolvedTracePath
277
879
  traces: SolvedTracePath[]
278
880
  inputProblem: InputProblem
881
+ originalNetLabelPlacements: NetLabelPlacement[]
279
882
  candidateNetLabelPlacements: NetLabelPlacement[]
280
883
  attachedLabelIndexes: number[]
281
884
  }) => {
@@ -329,6 +932,71 @@ const candidateIsClear = ({
329
932
  return false
330
933
  }
331
934
 
935
+ // Moving an attached label is part of the reroute. It must not introduce a
936
+ // collision with an unchanged trace, component, or neighboring label.
937
+ const labelHitsTrace = (label: NetLabelPlacement, trace: SolvedTracePath) =>
938
+ pathIntersectsAnyNetLabel({
939
+ path: trace.tracePath,
940
+ netLabelPlacements: [label],
941
+ })
942
+ for (const labelIndex of attachedLabelIndexes) {
943
+ const originalLabel = originalNetLabelPlacements[labelIndex]!
944
+ const candidateLabel = candidateNetLabelPlacements[labelIndex]!
945
+ const originalBounds = getRectBounds(
946
+ originalLabel.center,
947
+ originalLabel.width,
948
+ originalLabel.height,
949
+ )
950
+ const candidateBounds = getRectBounds(
951
+ candidateLabel.center,
952
+ candidateLabel.width,
953
+ candidateLabel.height,
954
+ )
955
+
956
+ for (const trace of traces) {
957
+ if (trace.globalConnNetId === candidateLabel.globalConnNetId) continue
958
+ if (
959
+ labelHitsTrace(candidateLabel, trace) &&
960
+ !labelHitsTrace(originalLabel, trace)
961
+ ) {
962
+ return false
963
+ }
964
+ }
965
+ for (const obstacle of obstacles) {
966
+ if (
967
+ rectsOverlap(candidateBounds, obstacle) &&
968
+ !rectsOverlap(originalBounds, obstacle)
969
+ ) {
970
+ return false
971
+ }
972
+ }
973
+ for (
974
+ let otherIndex = 0;
975
+ otherIndex < candidateNetLabelPlacements.length;
976
+ otherIndex++
977
+ ) {
978
+ if (otherIndex === labelIndex) continue
979
+ const originalOther = originalNetLabelPlacements[otherIndex]!
980
+ const candidateOther = candidateNetLabelPlacements[otherIndex]!
981
+ const originalOtherBounds = getRectBounds(
982
+ originalOther.center,
983
+ originalOther.width,
984
+ originalOther.height,
985
+ )
986
+ const candidateOtherBounds = getRectBounds(
987
+ candidateOther.center,
988
+ candidateOther.width,
989
+ candidateOther.height,
990
+ )
991
+ if (
992
+ rectsOverlap(candidateBounds, candidateOtherBounds) &&
993
+ !rectsOverlap(originalBounds, originalOtherBounds)
994
+ ) {
995
+ return false
996
+ }
997
+ }
998
+ }
999
+
332
1000
  const otherNetLabelPlacements = candidateNetLabelPlacements.filter(
333
1001
  (label) => label.globalConnNetId !== candidateTrace.globalConnNetId,
334
1002
  )
@@ -456,15 +1124,16 @@ export const alignSameNetJunctions = ({
456
1124
  inputProblem,
457
1125
  traces,
458
1126
  netLabelPlacements,
1127
+ netLabelConnectorTraceIds,
459
1128
  }: AlignSameNetJunctionsInput) => {
460
1129
  let outputTraces = [...traces]
461
1130
  let outputNetLabelPlacements = [...netLabelPlacements]
462
1131
  let alignedJunctionCount = 0
1132
+ const alignedBranchTraceIds = new Set<string>()
463
1133
 
464
1134
  // First level the load rails, then attach return branches to those final
465
1135
  // rails. Doing this in one pass could attach a branch to a rail that moves later.
466
1136
  for (const alignReturnBranches of [false, true]) {
467
- const alignedBranchTraceIds = new Set<string>()
468
1137
  // Reuse each aligned branch as the rail for the next load in the chain. An
469
1138
  // aligned branch may already have had its donor turn, so queue it again when
470
1139
  // its geometry changes.
@@ -483,15 +1152,48 @@ export const alignSameNetJunctions = ({
483
1152
 
484
1153
  // Prefer the existing return stem. If extending the outer column
485
1154
  // meets another net, try shorter escapes without moving any pins.
486
- const candidatePaths = alignReturnBranches
487
- ? [1, 0.5, 0.25].map((returnStemScale) =>
488
- getAlignedReturnBranchPath({
489
- donorTrace,
490
- branchTrace,
491
- returnStemScale,
492
- }),
493
- )
494
- : [getAlignedBranchPath({ donorTrace, branchTrace })]
1155
+ const donorIsLabelConnector = netLabelConnectorTraceIds.has(
1156
+ donorTrace.mspPairId,
1157
+ )
1158
+ const branchIsLabelConnector = netLabelConnectorTraceIds.has(
1159
+ branchTrace.mspPairId,
1160
+ )
1161
+ const candidatePaths = branchIsLabelConnector
1162
+ ? alignReturnBranches && !donorIsLabelConnector
1163
+ ? [
1164
+ getAlignedAttachedLabelConnectorPath({
1165
+ donorTrace,
1166
+ connectorTrace: branchTrace,
1167
+ netLabelConnectorTraceIds,
1168
+ }),
1169
+ ]
1170
+ : []
1171
+ : donorIsLabelConnector
1172
+ ? []
1173
+ : alignReturnBranches
1174
+ ? [
1175
+ ...[1, 0.5, 0.25].map((returnStemScale) =>
1176
+ getAlignedReturnBranchPath({
1177
+ donorTrace,
1178
+ branchTrace,
1179
+ returnStemScale,
1180
+ }),
1181
+ ),
1182
+ getAlignedPerpendicularEndpointBusPath({
1183
+ donorTrace,
1184
+ branchTrace,
1185
+ traces: outputTraces,
1186
+ }),
1187
+ getAlignedSharedEndpointRailPath({
1188
+ donorTrace,
1189
+ branchTrace,
1190
+ netLabelConnectorTraceIds,
1191
+ }),
1192
+ ]
1193
+ : [
1194
+ getAlignedBranchPath({ donorTrace, branchTrace }),
1195
+ getAlignedParallelPinExitPath({ donorTrace, branchTrace }),
1196
+ ]
495
1197
  for (const candidatePath of candidatePaths) {
496
1198
  if (!candidatePath) continue
497
1199
  const candidateTrace = { ...branchTrace, tracePath: candidatePath }
@@ -520,12 +1222,20 @@ export const alignSameNetJunctions = ({
520
1222
  netLabelPlacements: outputNetLabelPlacements,
521
1223
  attachedLabelIndexes,
522
1224
  })
1225
+ const candidateTraces = preserveAttachedLabelConnectorJunctions({
1226
+ originalTrace: branchTrace,
1227
+ candidateTrace,
1228
+ traces: outputTraces,
1229
+ netLabelConnectorTraceIds,
1230
+ })
1231
+ if (!candidateTraces) continue
523
1232
  if (
524
1233
  !candidateIsClear({
525
1234
  candidateTrace,
526
1235
  originalTrace: branchTrace,
527
- traces: outputTraces,
1236
+ traces: candidateTraces,
528
1237
  inputProblem,
1238
+ originalNetLabelPlacements: outputNetLabelPlacements,
529
1239
  candidateNetLabelPlacements,
530
1240
  attachedLabelIndexes,
531
1241
  })
@@ -533,10 +1243,33 @@ export const alignSameNetJunctions = ({
533
1243
  continue
534
1244
  }
535
1245
 
536
- outputTraces = outputTraces.map((trace) => {
537
- if (trace.mspPairId === branchTrace.mspPairId) return candidateTrace
538
- return trace
1246
+ const movedConnectorsAreClear = candidateTraces.every((trace) => {
1247
+ if (
1248
+ trace.mspPairId === branchTrace.mspPairId ||
1249
+ !netLabelConnectorTraceIds.has(trace.mspPairId)
1250
+ ) {
1251
+ return true
1252
+ }
1253
+ const originalConnector = outputTraces.find(
1254
+ (originalTrace) => originalTrace.mspPairId === trace.mspPairId,
1255
+ )!
1256
+ if (originalConnector.tracePath === trace.tracePath) return true
1257
+ return candidateIsClear({
1258
+ candidateTrace: trace,
1259
+ originalTrace: originalConnector,
1260
+ traces: candidateTraces,
1261
+ inputProblem,
1262
+ originalNetLabelPlacements: outputNetLabelPlacements,
1263
+ candidateNetLabelPlacements,
1264
+ attachedLabelIndexes: getAttachedLabelIndexes(
1265
+ originalConnector,
1266
+ outputNetLabelPlacements,
1267
+ ),
1268
+ })
539
1269
  })
1270
+ if (!movedConnectorsAreClear) continue
1271
+
1272
+ outputTraces = candidateTraces
540
1273
  outputNetLabelPlacements = candidateNetLabelPlacements
541
1274
  alignedBranchTraceIds.add(branchTrace.mspPairId)
542
1275
  alignedJunctionCount++