@tscircuit/schematic-trace-solver 0.0.182 → 0.0.183

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.
@@ -11,6 +11,7 @@ import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolve
11
11
  import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
12
12
  import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
13
13
  import type { Point } from "@tscircuit/math-utils"
14
+ import { getPinDirectionCandidates } from "./SchematicTraceSingleLineSolver/getPinDirection"
14
15
 
15
16
  const shouldPreferExteriorDetours = ({
16
17
  connectionPair,
@@ -62,6 +63,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
62
63
  chipMap: Record<string, InputChip>
63
64
 
64
65
  currentConnectionPair: MspConnectionPair | null = null
66
+ retryingWithoutNetLabelClearance = false
65
67
 
66
68
  solvedTracePaths: Array<SolvedTracePath> = []
67
69
  failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
@@ -101,6 +103,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
101
103
  if (this.activeSubSolver?.solved) {
102
104
  this.solvedTracePaths.push({
103
105
  ...this.currentConnectionPair!,
106
+ pins: this.activeSubSolver.pins,
104
107
  tracePath: this.activeSubSolver!.solvedTracePath!,
105
108
  mspConnectionPairIds: [this.currentConnectionPair!.mspPairId],
106
109
  pinIds: [
@@ -110,8 +113,33 @@ export class SchematicTraceLinesSolver extends BaseSolver {
110
113
  })
111
114
  this.activeSubSolver = null
112
115
  this.currentConnectionPair = null
116
+ this.retryingWithoutNetLabelClearance = false
113
117
  }
114
118
  if (this.activeSubSolver?.failed) {
119
+ if (
120
+ this.currentConnectionPair &&
121
+ !this.retryingWithoutNetLabelClearance &&
122
+ this.activeSubSolver.hasAmbiguousPinDirections
123
+ ) {
124
+ const connectionPair = this.currentConnectionPair
125
+ this.retryingWithoutNetLabelClearance = true
126
+ this.activeSubSolver = new SchematicTraceSingleLineSolver2({
127
+ inputProblem: this.inputProblem,
128
+ pins: connectionPair.pins.map((pin) => ({
129
+ ...pin,
130
+ })) as MspConnectionPair["pins"],
131
+ connectionPair,
132
+ chipMap: this.chipMap,
133
+ preferExteriorDetours: shouldPreferExteriorDetours({
134
+ connectionPair,
135
+ allConnectionPairs: this.mspConnectionPairs,
136
+ inputProblem: this.inputProblem,
137
+ }),
138
+ reserveNetLabelClearance: false,
139
+ })
140
+ return
141
+ }
142
+
115
143
  // Record the failure for this connection and continue to the next pair
116
144
  if (this.currentConnectionPair) {
117
145
  this.failedConnectionPairs.push({
@@ -121,6 +149,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
121
149
  }
122
150
  this.activeSubSolver = null
123
151
  this.currentConnectionPair = null
152
+ this.retryingWithoutNetLabelClearance = false
124
153
  // Do not fail the whole solver; proceed to schedule the next pair
125
154
  }
126
155
 
@@ -137,8 +166,22 @@ export class SchematicTraceLinesSolver extends BaseSolver {
137
166
  }
138
167
 
139
168
  this.currentConnectionPair = connectionPair
140
-
141
- const { pins } = connectionPair
169
+ this.retryingWithoutNetLabelClearance = false
170
+
171
+ // Corner pins can legitimately face either adjacent edge. Keep their
172
+ // inferred direction local to this route so solving one branch cannot
173
+ // constrain a later branch that shares the same corner pin. Pins with one
174
+ // geometric direction retain the established shared representation.
175
+ const hasAmbiguousCornerPin = connectionPair.pins.some((pin) => {
176
+ if (pin._facingDirection) return false
177
+ const chip = this.chipMap[pin.chipId]
178
+ return chip && getPinDirectionCandidates(pin, chip).length > 1
179
+ })
180
+ const pins = hasAmbiguousCornerPin
181
+ ? (connectionPair.pins.map((pin) => ({
182
+ ...pin,
183
+ })) as MspConnectionPair["pins"])
184
+ : connectionPair.pins
142
185
 
143
186
  this.activeSubSolver = new SchematicTraceSingleLineSolver2({
144
187
  inputProblem: this.inputProblem,
@@ -1,9 +1,12 @@
1
1
  import type { InputChip, InputPin } from "lib/types/InputProblem"
2
2
 
3
- export const getPinDirection = (
3
+ export type PinDirection = "x+" | "x-" | "y+" | "y-"
4
+
5
+ export const getPinDirectionCandidates = (
4
6
  pin: InputPin,
5
7
  chip: InputChip,
6
- ): "x+" | "x-" | "y+" | "y-" => {
8
+ connectedPin?: InputPin,
9
+ ): PinDirection[] => {
7
10
  // Determine what edge the pin lies on
8
11
  const { x, y } = pin
9
12
  const { center, width, height } = chip
@@ -26,17 +29,62 @@ export const getPinDirection = (
26
29
  xMinusDistance,
27
30
  )
28
31
 
29
- if (minDistance === yPlusDistance) {
30
- return "y+"
31
- }
32
+ // Preserve the established single-direction result as the primary choice.
33
+ // Candidate routing may treat nearly equal edge distances as an ambiguous
34
+ // corner, but context-free callers (such as visualization) should not change
35
+ // direction solely because the candidate comparison uses a tolerance.
36
+ const primaryDirection: PinDirection =
37
+ minDistance === yPlusDistance
38
+ ? "y+"
39
+ : minDistance === yMinusDistance
40
+ ? "y-"
41
+ : minDistance === xPlusDistance
42
+ ? "x+"
43
+ : "x-"
44
+
45
+ const matchingDirections = [
46
+ { direction: "y+" as const, distance: yPlusDistance },
47
+ { direction: "y-" as const, distance: yMinusDistance },
48
+ { direction: "x+" as const, distance: xPlusDistance },
49
+ { direction: "x-" as const, distance: xMinusDistance },
50
+ ]
51
+ .filter(({ distance }) => Math.abs(distance - minDistance) <= 1e-9)
52
+ .map(({ direction }) => direction)
53
+
54
+ const closestDirections = [
55
+ primaryDirection,
56
+ ...matchingDirections.filter((direction) => direction !== primaryDirection),
57
+ ]
32
58
 
33
- if (minDistance === yMinusDistance) {
34
- return "y-"
59
+ if (!connectedPin || closestDirections.length <= 1) {
60
+ return closestDirections
35
61
  }
36
62
 
37
- if (minDistance === xPlusDistance) {
38
- return "x+"
63
+ const xDistance = connectedPin.x - pin.x
64
+ const yDistance = connectedPin.y - pin.y
65
+ const directionTowardConnectedPin: PinDirection =
66
+ Math.abs(xDistance) >= Math.abs(yDistance)
67
+ ? xDistance >= 0
68
+ ? "x+"
69
+ : "x-"
70
+ : yDistance >= 0
71
+ ? "y+"
72
+ : "y-"
73
+
74
+ if (!closestDirections.includes(directionTowardConnectedPin)) {
75
+ return closestDirections
39
76
  }
40
77
 
41
- return "x-"
78
+ return [
79
+ directionTowardConnectedPin,
80
+ ...closestDirections.filter(
81
+ (direction) => direction !== directionTowardConnectedPin,
82
+ ),
83
+ ]
42
84
  }
85
+
86
+ export const getPinDirection = (
87
+ pin: InputPin,
88
+ chip: InputChip,
89
+ connectedPin?: InputPin,
90
+ ): PinDirection => getPinDirectionCandidates(pin, chip, connectedPin)[0]!
@@ -9,8 +9,12 @@ import type { InputChip, InputProblem } from "lib/types/InputProblem"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
10
  import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
11
11
  import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
12
- import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
13
- import { calculateDirectShortPath } from "./calculateDirectShortPath"
12
+ import { getPinDirectionCandidates } from "../SchematicTraceSingleLineSolver/getPinDirection"
13
+ import {
14
+ calculateDirectShortPath,
15
+ pathMatchesPinDirections,
16
+ segmentDirection,
17
+ } from "./calculateDirectShortPath"
14
18
  import {
15
19
  findFirstCollision,
16
20
  isHorizontal,
@@ -78,6 +82,55 @@ const pinsFaceEachOther = ({
78
82
  : pin1._facingDirection === "y-" && pin2._facingDirection === "y+"
79
83
  }
80
84
 
85
+ const getPathLength = (points: Point[]) => {
86
+ let length = 0
87
+ for (let i = 0; i < points.length - 1; i++) {
88
+ length +=
89
+ Math.abs(points[i + 1]!.x - points[i]!.x) +
90
+ Math.abs(points[i + 1]!.y - points[i]!.y)
91
+ }
92
+ return length
93
+ }
94
+
95
+ const getInitialPathForPins = ({
96
+ pins,
97
+ obstacles,
98
+ }: {
99
+ pins: MspConnectionPair["pins"]
100
+ obstacles: ObstacleRect[]
101
+ }): { path: Point[]; isDirectShortPath: boolean } => {
102
+ const [pin1, pin2] = pins
103
+ const directShortPath = calculateDirectShortPath(pin1, pin2)
104
+ const defaultElbow = calculateElbowForPins({
105
+ pin1,
106
+ pin2,
107
+ overshoot: 0.2,
108
+ })
109
+ const routingDistance = Math.abs(pin1.x - pin2.x) + Math.abs(pin1.y - pin2.y)
110
+ const adaptiveElbow = calculateElbowForPins({
111
+ pin1,
112
+ pin2,
113
+ overshoot: Math.min(0.2, Math.max(0.02, routingDistance / 4)),
114
+ })
115
+ const adaptiveElbowIsShorter =
116
+ getPathLength(adaptiveElbow) < getPathLength(defaultElbow)
117
+ const defaultElbowBacktracks =
118
+ getPathLength(defaultElbow) > routingDistance + 1e-9
119
+ const shouldUseAdaptiveElbow =
120
+ findFirstCollision(adaptiveElbow, obstacles) === null &&
121
+ ((pinsFaceEachOther({ pin1, pin2 }) &&
122
+ defaultElbowBacktracks &&
123
+ adaptiveElbowIsShorter) ||
124
+ findFirstCollision(defaultElbow, obstacles) !== null)
125
+
126
+ return {
127
+ path:
128
+ directShortPath ??
129
+ (shouldUseAdaptiveElbow ? adaptiveElbow : defaultElbow),
130
+ isDirectShortPath: directShortPath !== null,
131
+ }
132
+ }
133
+
81
134
  export class SchematicTraceSingleLineSolver2 extends BaseSolver {
82
135
  pins: MspConnectionPair["pins"]
83
136
  connectionPair?: MspConnectionPair
@@ -91,12 +144,18 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
91
144
 
92
145
  baseElbow: Point[]
93
146
  preferExteriorDetours: boolean
147
+ reserveNetLabelClearance: boolean
94
148
 
95
149
  solvedTracePath: Point[] | null = null
96
150
 
97
- private queue: Array<{ path: Point[]; collisionRects: Set<ObstacleRect> }> =
98
- []
151
+ private queue: Array<{
152
+ path: Point[]
153
+ collisionRects: Set<ObstacleRect>
154
+ directions: readonly [FacingDirection, FacingDirection]
155
+ }> = []
99
156
  private visited: Set<PathKey> = new Set()
157
+ private inferredPinIndexes = new Set<number>()
158
+ hasAmbiguousPinDirections = false
100
159
 
101
160
  constructor(params: {
102
161
  pins: MspConnectionPair["pins"]
@@ -104,6 +163,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
104
163
  inputProblem: InputProblem
105
164
  chipMap: Record<string, InputChip>
106
165
  preferExteriorDetours?: boolean
166
+ reserveNetLabelClearance?: boolean
107
167
  }) {
108
168
  super()
109
169
  this.pins = params.pins
@@ -111,19 +171,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
111
171
  this.inputProblem = params.inputProblem
112
172
  this.chipMap = params.chipMap
113
173
  this.preferExteriorDetours = params.preferExteriorDetours ?? true
114
-
115
- // Ensure facing directions are present
116
- for (const pin of this.pins) {
117
- if (!pin._facingDirection) {
118
- const chip = this.chipMap[pin.chipId]
119
- pin._facingDirection = getPinDirection(pin, chip)
120
- }
121
- }
174
+ this.reserveNetLabelClearance = params.reserveNetLabelClearance ?? true
122
175
 
123
176
  // Build obstacle rects from chips and schematic text boxes. Text boxes are
124
177
  // padded by the label footprint for this net so labels have clearance too.
125
178
  this.obstacles = getObstacleRects(this.inputProblem, {
126
- textBoxPadding: this.getTextBoxPaddingForConnectionPair(),
179
+ textBoxPadding: this.reserveNetLabelClearance
180
+ ? this.getTextBoxPaddingForConnectionPair()
181
+ : undefined,
127
182
  })
128
183
  this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle))
129
184
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
@@ -139,40 +194,71 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
139
194
  ),
140
195
  )
141
196
 
142
- const [pin1, pin2] = this.pins
143
- const directShortPath = calculateDirectShortPath(pin1, pin2)
144
- const defaultElbow = calculateElbowForPins({
145
- pin1,
146
- pin2,
147
- overshoot: 0.2,
148
- })
149
- const routingDistance =
150
- Math.abs(pin1.x - pin2.x) + Math.abs(pin1.y - pin2.y)
151
- const adaptiveElbow = calculateElbowForPins({
152
- pin1,
153
- pin2,
154
- overshoot: Math.min(0.2, Math.max(0.02, routingDistance / 4)),
197
+ const directionOptions = this.pins.map((pin, pinIndex) => {
198
+ if (pin._facingDirection) return [pin._facingDirection]
199
+ this.inferredPinIndexes.add(pinIndex)
200
+ const connectedPin = this.pins[pinIndex === 0 ? 1 : 0]
201
+ return getPinDirectionCandidates(
202
+ pin,
203
+ this.chipMap[pin.chipId],
204
+ connectedPin,
205
+ )
155
206
  })
156
- const adaptiveElbowIsShorter =
157
- this.pathLength(adaptiveElbow) < this.pathLength(defaultElbow)
158
- const defaultElbowBacktracks =
159
- this.pathLength(defaultElbow) > routingDistance + 1e-9
160
- const shouldUseAdaptiveElbow =
161
- findFirstCollision(adaptiveElbow, this.obstacles) === null &&
162
- ((pinsFaceEachOther({ pin1, pin2 }) &&
163
- defaultElbowBacktracks &&
164
- adaptiveElbowIsShorter) ||
165
- findFirstCollision(defaultElbow, this.obstacles) !== null)
166
-
167
- // Build initial elbow path
168
- this.baseElbow = defaultElbow
169
- if (shouldUseAdaptiveElbow) {
170
- this.baseElbow = adaptiveElbow
207
+ const directionPairs = directionOptions[0]!.flatMap(
208
+ (firstDirection, firstIndex) =>
209
+ directionOptions[1]!.map((secondDirection, secondIndex) => ({
210
+ directions: [firstDirection, secondDirection] as const,
211
+ preferenceIndex: firstIndex + secondIndex,
212
+ })),
213
+ )
214
+ this.hasAmbiguousPinDirections = directionOptions.some(
215
+ (directions) => directions.length > 1,
216
+ )
217
+ const rankedDirectionPairs = directionPairs
218
+ .map(({ directions, preferenceIndex }) => {
219
+ const candidatePins = this.pins.map((pin, index) => ({
220
+ ...pin,
221
+ _facingDirection: directions[index]!,
222
+ })) as MspConnectionPair["pins"]
223
+ const initialPath = getInitialPathForPins({
224
+ pins: candidatePins,
225
+ obstacles: this.obstacles,
226
+ })
227
+ return {
228
+ directions,
229
+ baseElbow: initialPath.path,
230
+ isDirectShortPath: initialPath.isDirectShortPath,
231
+ preferenceIndex,
232
+ collisionCount:
233
+ findFirstCollision(initialPath.path, this.obstacles) === null
234
+ ? 0
235
+ : 1,
236
+ pathLength: getPathLength(initialPath.path),
237
+ }
238
+ })
239
+ .sort(
240
+ (first, second) =>
241
+ first.collisionCount - second.collisionCount ||
242
+ first.pathLength - second.pathLength ||
243
+ first.preferenceIndex - second.preferenceIndex,
244
+ )
245
+
246
+ const preferredCandidate = rankedDirectionPairs[0]!
247
+ for (const [pinIndex, pin] of this.pins.entries()) {
248
+ pin._facingDirection = preferredCandidate.directions[pinIndex]
171
249
  }
172
- if (directShortPath) {
173
- this.baseElbow = directShortPath
250
+
251
+ const [pin1, pin2] = this.pins
252
+ this.baseElbow = preferredCandidate.baseElbow
253
+
254
+ // Short paths are generated by a dedicated routine that validates both
255
+ // endpoint directions. Preserve the established behavior of accepting
256
+ // that local connection directly instead of sending it through obstacle
257
+ // detour search, which can turn overlapping endpoint symbols into two
258
+ // fallback labels.
259
+ if (preferredCandidate.isDirectShortPath) {
260
+ this.solvedTracePath = preferredCandidate.baseElbow
174
261
  }
175
- this.solvedTracePath = directShortPath
176
262
 
177
263
  // Bounds defined by PA and PB
178
264
  this.aabb = aabbFromPoints(
@@ -180,9 +266,19 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
180
266
  { x: pin2.x, y: pin2.y },
181
267
  )
182
268
 
183
- // Seed search
184
- this.queue.push({ path: this.baseElbow, collisionRects: new Set() })
185
- this.visited.add(pathKey(this.baseElbow))
269
+ // A corner pin has more than one geometrically valid outward direction.
270
+ // Seed each distinct elbow so obstacle routing, rather than component type,
271
+ // decides which direction is usable.
272
+ for (const candidate of rankedDirectionPairs) {
273
+ const key = pathKey(candidate.baseElbow)
274
+ if (this.visited.has(key)) continue
275
+ this.visited.add(key)
276
+ this.queue.push({
277
+ path: candidate.baseElbow,
278
+ collisionRects: new Set(),
279
+ directions: candidate.directions,
280
+ })
281
+ }
186
282
  }
187
283
 
188
284
  override getConstructorParams(): ConstructorParameters<
@@ -194,6 +290,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
194
290
  connectionPair: this.connectionPair,
195
291
  inputProblem: this.inputProblem,
196
292
  preferExteriorDetours: this.preferExteriorDetours,
293
+ reserveNetLabelClearance: this.reserveNetLabelClearance,
197
294
  }
198
295
  }
199
296
 
@@ -268,13 +365,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
268
365
  }
269
366
 
270
367
  private pathLength(pts: Point[]): number {
271
- let sum = 0
272
- for (let i = 0; i < pts.length - 1; i++) {
273
- sum +=
274
- Math.abs(pts[i + 1]!.x - pts[i]!.x) +
275
- Math.abs(pts[i + 1]!.y - pts[i]!.y)
276
- }
277
- return sum
368
+ return getPathLength(pts)
278
369
  }
279
370
 
280
371
  private getPinBandPenalty(path: Point[]): number {
@@ -318,7 +409,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
318
409
  return
319
410
  }
320
411
 
321
- const { path, collisionRects } = state
412
+ const { path, collisionRects, directions } = state
322
413
 
323
414
  const [PA, PB] = this.pins
324
415
  const collision = findFirstCollision(path, this.obstacles, {
@@ -409,8 +500,21 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
409
500
  Math.abs(p.x - q.x) < EPS && Math.abs(p.y - q.y) < EPS
410
501
  if (
411
502
  samePoint(first, { x: PA.x, y: PA.y }) &&
412
- samePoint(last, { x: PB.x, y: PB.y })
503
+ samePoint(last, { x: PB.x, y: PB.y }) &&
504
+ (!this.hasAmbiguousPinDirections ||
505
+ pathMatchesPinDirections({
506
+ path,
507
+ pin1: { ...PA, _facingDirection: directions[0] },
508
+ pin2: { ...PB, _facingDirection: directions[1] },
509
+ }))
413
510
  ) {
511
+ for (const pinIndex of this.inferredPinIndexes) {
512
+ const direction =
513
+ pinIndex === 0
514
+ ? segmentDirection(path[0]!, path[1]!)
515
+ : segmentDirection(path.at(-1)!, path.at(-2)!)
516
+ if (direction) this.pins[pinIndex]!._facingDirection = direction
517
+ }
414
518
  this.solvedTracePath = path
415
519
  this.solved = true
416
520
  }
@@ -471,6 +575,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
471
575
  this.queue.push({
472
576
  path: detour,
473
577
  collisionRects: nextCollisionRects,
578
+ directions,
474
579
  })
475
580
  }
476
581
  return
@@ -630,7 +735,11 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
630
735
  (a, b) => a.length - b.length || a.pinBandPenalty - b.pinBandPenalty,
631
736
  )
632
737
  for (const st of newStates) {
633
- this.queue.push({ path: st.path, collisionRects: st.collisionRects })
738
+ this.queue.push({
739
+ path: st.path,
740
+ collisionRects: st.collisionRects,
741
+ directions,
742
+ })
634
743
  }
635
744
  }
636
745
 
@@ -1,16 +1,121 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
3
 
4
+ type LabelBounds = {
5
+ minX: number
6
+ maxX: number
7
+ minY: number
8
+ maxY: number
9
+ }
10
+
11
+ const EPSILON = 1e-9
12
+
13
+ const rangesMeetWithoutOverlap = (
14
+ firstMin: number,
15
+ firstMax: number,
16
+ secondMin: number,
17
+ secondMax: number,
18
+ ) =>
19
+ firstMax >= secondMin - EPSILON &&
20
+ secondMax >= firstMin - EPSILON &&
21
+ Math.min(firstMax, secondMax) - Math.max(firstMin, secondMin) <= EPSILON
22
+
23
+ const pointsEqual = (first: Point, second: Point) =>
24
+ Math.abs(first.x - second.x) <= EPSILON &&
25
+ Math.abs(first.y - second.y) <= EPSILON
26
+
27
+ const continuesExistingBoundarySegment = ({
28
+ start,
29
+ end,
30
+ label,
31
+ originalPath,
32
+ }: {
33
+ start: Point
34
+ end: Point
35
+ label: LabelBounds
36
+ originalPath: Point[]
37
+ }) => {
38
+ const isVertical = Math.abs(start.x - end.x) <= EPSILON
39
+ const isHorizontal = Math.abs(start.y - end.y) <= EPSILON
40
+ const isOnBoundary = isVertical
41
+ ? Math.abs(start.x - label.minX) <= EPSILON ||
42
+ Math.abs(start.x - label.maxX) <= EPSILON
43
+ : isHorizontal
44
+ ? Math.abs(start.y - label.minY) <= EPSILON ||
45
+ Math.abs(start.y - label.maxY) <= EPSILON
46
+ : false
47
+
48
+ if (!isOnBoundary) return false
49
+
50
+ for (let i = 0; i < originalPath.length - 1; i++) {
51
+ const isEndpointSegment = i === 0 || i === originalPath.length - 2
52
+ if (!isEndpointSegment) continue
53
+
54
+ const originalStart = originalPath[i]!
55
+ const originalEnd = originalPath[i + 1]!
56
+ const innerEndpoint = i === 0 ? originalEnd : originalStart
57
+ if (
58
+ !pointsEqual(start, innerEndpoint) &&
59
+ !pointsEqual(end, innerEndpoint)
60
+ ) {
61
+ continue
62
+ }
63
+ if (!segmentIntersectsRect(originalStart, originalEnd, label)) continue
64
+
65
+ if (
66
+ isVertical &&
67
+ Math.abs(originalStart.x - originalEnd.x) <= EPSILON &&
68
+ Math.abs(originalStart.x - start.x) <= EPSILON &&
69
+ rangesMeetWithoutOverlap(
70
+ Math.min(start.y, end.y),
71
+ Math.max(start.y, end.y),
72
+ Math.min(originalStart.y, originalEnd.y),
73
+ Math.max(originalStart.y, originalEnd.y),
74
+ )
75
+ ) {
76
+ return true
77
+ }
78
+
79
+ if (
80
+ isHorizontal &&
81
+ Math.abs(originalStart.y - originalEnd.y) <= EPSILON &&
82
+ Math.abs(originalStart.y - start.y) <= EPSILON &&
83
+ rangesMeetWithoutOverlap(
84
+ Math.min(start.x, end.x),
85
+ Math.max(start.x, end.x),
86
+ Math.min(originalStart.x, originalEnd.x),
87
+ Math.max(originalStart.x, originalEnd.x),
88
+ )
89
+ ) {
90
+ return true
91
+ }
92
+ }
93
+
94
+ return false
95
+ }
96
+
4
97
  export const hasCollisionsWithLabels = (
5
98
  pathSegments: Point[],
6
- labels: any[],
99
+ labels: LabelBounds[],
100
+ options: { originalPath?: Point[] } = {},
7
101
  ): boolean => {
8
102
  for (let i = 0; i < pathSegments.length - 1; i++) {
9
- const p1 = pathSegments[i]
10
- const p2 = pathSegments[i + 1]
103
+ const p1 = pathSegments[i]!
104
+ const p2 = pathSegments[i + 1]!
11
105
 
12
106
  for (const label of labels) {
13
107
  if (segmentIntersectsRect(p1, p2, label)) {
108
+ if (
109
+ options.originalPath &&
110
+ continuesExistingBoundarySegment({
111
+ start: p1,
112
+ end: p2,
113
+ label,
114
+ originalPath: options.originalPath,
115
+ })
116
+ ) {
117
+ continue
118
+ }
14
119
  return true
15
120
  }
16
121
  }