@tscircuit/schematic-trace-solver 0.0.81 → 0.0.83
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/biome.json +5 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +253 -85
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +126 -12
- package/lib/solvers/AvailableNetOrientationSolver/types.ts +5 -1
- package/lib/solvers/Example28Solver/Example28Solver.ts +114 -2
- package/lib/solvers/Example28Solver/reroute.ts +32 -21
- package/package.json +1 -1
- package/tests/examples/__snapshots__/example03.snap.svg +64 -64
- package/tests/examples/__snapshots__/example28.snap.svg +21 -18
- package/tests/examples/__snapshots__/example32.snap.svg +3 -3
- package/tests/examples/__snapshots__/example48.snap.svg +30 -30
- package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +177 -0
- package/tests/repros/assets/repro51-overlap-junction-crossing.input.json +129 -0
- package/tests/repros/repro51-overlap-junction-crossing.test.ts +105 -0
- package/tests/repros/rotated-components-rail-label.test.ts +68 -0
|
@@ -146,6 +146,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
146
146
|
candidate: EvaluatedCandidate,
|
|
147
147
|
labelIndex: number,
|
|
148
148
|
) {
|
|
149
|
+
if (candidate.phase === "trace-anchor") return
|
|
150
|
+
|
|
149
151
|
const tracePath = this.getCandidateConnectorTrace(label, candidate)
|
|
150
152
|
if (tracePath.length < 2) return
|
|
151
153
|
|
|
@@ -187,6 +189,13 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
187
189
|
)
|
|
188
190
|
if (rotatedCandidate) return rotatedCandidate
|
|
189
191
|
|
|
192
|
+
const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
|
|
193
|
+
label,
|
|
194
|
+
orientations[0]!,
|
|
195
|
+
labelIndex,
|
|
196
|
+
)
|
|
197
|
+
if (traceAnchorCandidate) return traceAnchorCandidate
|
|
198
|
+
|
|
190
199
|
const shiftedCandidate = this.findValidShiftedCandidate(
|
|
191
200
|
label,
|
|
192
201
|
orientations[0]!,
|
|
@@ -228,6 +237,57 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
228
237
|
return null
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
private findValidTraceAnchorCandidate(
|
|
241
|
+
label: NetLabelPlacement,
|
|
242
|
+
orientation: FacingDirection,
|
|
243
|
+
labelIndex: number,
|
|
244
|
+
) {
|
|
245
|
+
const direction = dir(orientation)
|
|
246
|
+
const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
|
|
247
|
+
(a, b) => {
|
|
248
|
+
const aAlongDirection = a.x * direction.x + a.y * direction.y
|
|
249
|
+
const bAlongDirection = b.x * direction.x + b.y * direction.y
|
|
250
|
+
return bAlongDirection - aAlongDirection
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
for (const anchorPoint of candidatePoints) {
|
|
255
|
+
const candidate = this.createCandidate(label, anchorPoint, orientation)
|
|
256
|
+
const result = this.evaluateCandidate(
|
|
257
|
+
candidate,
|
|
258
|
+
label,
|
|
259
|
+
labelIndex,
|
|
260
|
+
"trace-anchor",
|
|
261
|
+
)
|
|
262
|
+
this.currentCandidateResults.push(result)
|
|
263
|
+
|
|
264
|
+
if (result.status === "valid") {
|
|
265
|
+
result.selected = true
|
|
266
|
+
return result
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getTraceAnchorCandidatePoints(label: NetLabelPlacement) {
|
|
274
|
+
const seen = new Set<string>()
|
|
275
|
+
const points: Point[] = []
|
|
276
|
+
|
|
277
|
+
for (const traceId of label.mspConnectionPairIds ?? []) {
|
|
278
|
+
const trace = this.traceMap[traceId]
|
|
279
|
+
if (!trace) continue
|
|
280
|
+
for (const point of trace.tracePath) {
|
|
281
|
+
const key = `${point.x.toFixed(9)},${point.y.toFixed(9)}`
|
|
282
|
+
if (seen.has(key)) continue
|
|
283
|
+
seen.add(key)
|
|
284
|
+
points.push(point)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return points
|
|
289
|
+
}
|
|
290
|
+
|
|
231
291
|
private getAvailableOrientations(label: NetLabelPlacement) {
|
|
232
292
|
const effectiveNetId = label.netId ?? label.globalConnNetId
|
|
233
293
|
return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
|
|
@@ -608,16 +668,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
608
668
|
phase: CandidatePhase
|
|
609
669
|
}) {
|
|
610
670
|
const { candidate, label, labelIndex, phase } = params
|
|
671
|
+
const bounds = getRectBounds(
|
|
672
|
+
candidate.center,
|
|
673
|
+
candidate.width,
|
|
674
|
+
candidate.height,
|
|
675
|
+
)
|
|
611
676
|
const boundsStatus = this.getBoundsStatus({
|
|
612
|
-
bounds
|
|
613
|
-
candidate.center,
|
|
614
|
-
candidate.width,
|
|
615
|
-
candidate.height,
|
|
616
|
-
),
|
|
677
|
+
bounds,
|
|
617
678
|
labelIndex,
|
|
618
679
|
label,
|
|
619
680
|
})
|
|
620
|
-
if (boundsStatus !== "valid")
|
|
681
|
+
if (boundsStatus !== "valid") {
|
|
682
|
+
if (
|
|
683
|
+
phase !== "trace-anchor" ||
|
|
684
|
+
boundsStatus !== "chip-collision" ||
|
|
685
|
+
!this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)
|
|
686
|
+
) {
|
|
687
|
+
return boundsStatus
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const nonChipBoundsStatus = this.getBoundsStatus({
|
|
691
|
+
bounds,
|
|
692
|
+
labelIndex,
|
|
693
|
+
label,
|
|
694
|
+
ignoreChipCollisions: true,
|
|
695
|
+
})
|
|
696
|
+
if (nonChipBoundsStatus !== "valid") return nonChipBoundsStatus
|
|
697
|
+
}
|
|
621
698
|
|
|
622
699
|
const connectorTrace = this.getCandidateConnectorTrace(label, {
|
|
623
700
|
anchorPoint: candidate.anchorPoint,
|
|
@@ -647,22 +724,59 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
647
724
|
return "valid"
|
|
648
725
|
}
|
|
649
726
|
|
|
727
|
+
private isAcceptableTraceAnchorChipCollision(
|
|
728
|
+
candidate: CandidateLabel,
|
|
729
|
+
label: NetLabelPlacement,
|
|
730
|
+
bounds: Bounds,
|
|
731
|
+
) {
|
|
732
|
+
const collidingChips =
|
|
733
|
+
this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
|
|
734
|
+
if (collidingChips.length === 0) return false
|
|
735
|
+
|
|
736
|
+
const labelChipIds = new Set(
|
|
737
|
+
label.pinIds
|
|
738
|
+
.map((pinId) => this.pinMap[pinId]?.chipId)
|
|
739
|
+
.filter((chipId): chipId is string => Boolean(chipId)),
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
return collidingChips.every((chip) => {
|
|
743
|
+
if (!labelChipIds.has(chip.chipId)) return false
|
|
744
|
+
|
|
745
|
+
const { anchorPoint, orientation } = candidate
|
|
746
|
+
const chipBounds = chip.bounds
|
|
747
|
+
if (isYOrientation(orientation)) {
|
|
748
|
+
return (
|
|
749
|
+
anchorPoint.x < chipBounds.minX - EPS ||
|
|
750
|
+
anchorPoint.x > chipBounds.maxX + EPS
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
return (
|
|
754
|
+
anchorPoint.y < chipBounds.minY - EPS ||
|
|
755
|
+
anchorPoint.y > chipBounds.maxY + EPS
|
|
756
|
+
)
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
650
760
|
private getBoundsStatus(candidateBoundsCheck: {
|
|
651
761
|
bounds: Bounds
|
|
652
762
|
labelIndex: number
|
|
653
763
|
label: NetLabelPlacement
|
|
764
|
+
ignoreChipCollisions?: boolean
|
|
654
765
|
}): CandidateStatus {
|
|
655
|
-
const { bounds, labelIndex, label } =
|
|
766
|
+
const { bounds, labelIndex, label, ignoreChipCollisions } =
|
|
767
|
+
candidateBoundsCheck
|
|
656
768
|
|
|
657
|
-
if (
|
|
658
|
-
|
|
769
|
+
if (!ignoreChipCollisions) {
|
|
770
|
+
if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
|
|
771
|
+
return "chip-collision"
|
|
772
|
+
}
|
|
773
|
+
if (this.sharesChipBoundary(bounds)) {
|
|
774
|
+
return "chip-collision"
|
|
775
|
+
}
|
|
659
776
|
}
|
|
660
777
|
if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
|
|
661
778
|
return "text-collision"
|
|
662
779
|
}
|
|
663
|
-
if (this.sharesChipBoundary(bounds)) {
|
|
664
|
-
return "chip-collision"
|
|
665
|
-
}
|
|
666
780
|
if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
|
|
667
781
|
return "trace-collision"
|
|
668
782
|
}
|
|
@@ -35,7 +35,11 @@ export type CandidateStatus =
|
|
|
35
35
|
| "trace-clearance-violation"
|
|
36
36
|
| "netlabel-collision"
|
|
37
37
|
|
|
38
|
-
export type CandidatePhase =
|
|
38
|
+
export type CandidatePhase =
|
|
39
|
+
| "rotate"
|
|
40
|
+
| "trace-anchor"
|
|
41
|
+
| "shift"
|
|
42
|
+
| "lateral-shift"
|
|
39
43
|
|
|
40
44
|
export type EvaluatedCandidate = CandidateLabel & {
|
|
41
45
|
status: CandidateStatus
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import {
|
|
3
|
+
getPinMap,
|
|
4
|
+
getTracePins,
|
|
5
|
+
} from "lib/solvers/AvailableNetOrientationSolver/traces"
|
|
2
6
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
3
7
|
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
-
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
8
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
9
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
6
10
|
import {
|
|
7
11
|
detectTraceLabelOverlap,
|
|
8
12
|
type TraceLabelOverlap,
|
|
9
13
|
} from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
10
|
-
import type { InputProblem } from "lib/types/InputProblem"
|
|
14
|
+
import type { InputPin, InputProblem } from "lib/types/InputProblem"
|
|
15
|
+
import { dir } from "lib/utils/dir"
|
|
11
16
|
import { moveAttachedLabelsToReroutedTrace } from "./labelMovement"
|
|
12
17
|
import { findBestReroutePath } from "./reroute"
|
|
13
18
|
import type { Example28SolverParams, RerouteCandidateResult } from "./types"
|
|
14
19
|
import { visualizeExample28Solver } from "./visualize"
|
|
15
20
|
|
|
21
|
+
const LABEL_OUTWARD_STEP = 0.1
|
|
22
|
+
const LABEL_MAX_OUTWARD_STEPS = 10
|
|
23
|
+
const LABEL_TRACE_CLEARANCE = 0.1
|
|
24
|
+
|
|
16
25
|
export class Example28Solver extends BaseSolver {
|
|
17
26
|
inputProblem: InputProblem
|
|
18
27
|
traces: SolvedTracePath[]
|
|
@@ -25,6 +34,7 @@ export class Example28Solver extends BaseSolver {
|
|
|
25
34
|
currentCandidateResults: RerouteCandidateResult[] = []
|
|
26
35
|
|
|
27
36
|
private chipObstacles: ReturnType<typeof getObstacleRects>
|
|
37
|
+
private pinMap: Record<string, InputPin & { chipId: string }>
|
|
28
38
|
|
|
29
39
|
constructor(params: Example28SolverParams) {
|
|
30
40
|
super()
|
|
@@ -34,6 +44,7 @@ export class Example28Solver extends BaseSolver {
|
|
|
34
44
|
this.outputTraces = [...params.traces]
|
|
35
45
|
this.outputNetLabelPlacements = [...params.netLabelPlacements]
|
|
36
46
|
this.chipObstacles = getObstacleRects(params.inputProblem)
|
|
47
|
+
this.pinMap = getPinMap(params.inputProblem)
|
|
37
48
|
this.initializeQueuedOverlaps()
|
|
38
49
|
this.currentOverlap = this.queuedOverlaps[0] ?? null
|
|
39
50
|
}
|
|
@@ -144,6 +155,8 @@ export class Example28Solver extends BaseSolver {
|
|
|
144
155
|
if (traceIndex === -1) return
|
|
145
156
|
|
|
146
157
|
const currentTrace = this.outputTraces[traceIndex]!
|
|
158
|
+
if (this.tryMoveLabelOutward(overlap.label)) return
|
|
159
|
+
|
|
147
160
|
const rerouteResult = findBestReroutePath({
|
|
148
161
|
trace: currentTrace,
|
|
149
162
|
obstacleLabel: overlap.label,
|
|
@@ -169,6 +182,79 @@ export class Example28Solver extends BaseSolver {
|
|
|
169
182
|
})
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
private tryMoveLabelOutward(labelToMove: NetLabelPlacement) {
|
|
186
|
+
const labelIndex = this.outputNetLabelPlacements.findIndex(
|
|
187
|
+
(label) =>
|
|
188
|
+
label.globalConnNetId === labelToMove.globalConnNetId &&
|
|
189
|
+
label.anchorPoint.x === labelToMove.anchorPoint.x &&
|
|
190
|
+
label.anchorPoint.y === labelToMove.anchorPoint.y &&
|
|
191
|
+
label.pinIds.join(",") === labelToMove.pinIds.join(","),
|
|
192
|
+
)
|
|
193
|
+
if (labelIndex === -1) return false
|
|
194
|
+
|
|
195
|
+
const label = this.outputNetLabelPlacements[labelIndex]!
|
|
196
|
+
if (label.mspConnectionPairIds.length > 0) return false
|
|
197
|
+
if (label.pinIds.length !== 1) return false
|
|
198
|
+
if (
|
|
199
|
+
this.outputNetLabelPlacements.filter(
|
|
200
|
+
(otherLabel) => otherLabel.globalConnNetId === label.globalConnNetId,
|
|
201
|
+
).length !== 1
|
|
202
|
+
) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const outward = dir(label.orientation)
|
|
207
|
+
if (outward.x === 0 && outward.y === 0) return false
|
|
208
|
+
|
|
209
|
+
for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
|
|
210
|
+
const distance = step * LABEL_OUTWARD_STEP
|
|
211
|
+
const candidate = {
|
|
212
|
+
...label,
|
|
213
|
+
anchorPoint: {
|
|
214
|
+
x: label.anchorPoint.x + outward.x * distance,
|
|
215
|
+
y: label.anchorPoint.y + outward.y * distance,
|
|
216
|
+
},
|
|
217
|
+
center: {
|
|
218
|
+
x: label.center.x + outward.x * distance,
|
|
219
|
+
y: label.center.y + outward.y * distance,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
const candidateWithClearance = {
|
|
223
|
+
...candidate,
|
|
224
|
+
width: candidate.width + LABEL_TRACE_CLEARANCE * 2,
|
|
225
|
+
height: candidate.height + LABEL_TRACE_CLEARANCE * 2,
|
|
226
|
+
}
|
|
227
|
+
if (
|
|
228
|
+
detectTraceLabelOverlap({
|
|
229
|
+
traces: this.outputTraces,
|
|
230
|
+
netLabels: [candidateWithClearance],
|
|
231
|
+
}).length > 0
|
|
232
|
+
) {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const connectorTrace = createPortOnlyLabelConnectorTrace({
|
|
237
|
+
label,
|
|
238
|
+
movedLabel: candidate,
|
|
239
|
+
pinMap: this.pinMap,
|
|
240
|
+
})
|
|
241
|
+
if (
|
|
242
|
+
detectTraceLabelOverlap({
|
|
243
|
+
traces: [connectorTrace],
|
|
244
|
+
netLabels: this.outputNetLabelPlacements,
|
|
245
|
+
}).length > 0
|
|
246
|
+
) {
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.outputNetLabelPlacements[labelIndex] = candidate
|
|
251
|
+
this.outputTraces.push(connectorTrace)
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
172
258
|
override visualize(): GraphicsObject {
|
|
173
259
|
return visualizeExample28Solver({
|
|
174
260
|
inputProblem: this.inputProblem,
|
|
@@ -180,3 +266,29 @@ export class Example28Solver extends BaseSolver {
|
|
|
180
266
|
})
|
|
181
267
|
}
|
|
182
268
|
}
|
|
269
|
+
|
|
270
|
+
const getPortOnlyLabelConnectorMspPairId = (label: NetLabelPlacement) =>
|
|
271
|
+
`port-only-label-connector-${label.globalConnNetId}-${label.pinIds[0]}`
|
|
272
|
+
|
|
273
|
+
const createPortOnlyLabelConnectorTrace = ({
|
|
274
|
+
label,
|
|
275
|
+
movedLabel,
|
|
276
|
+
pinMap,
|
|
277
|
+
}: {
|
|
278
|
+
label: NetLabelPlacement
|
|
279
|
+
movedLabel: NetLabelPlacement
|
|
280
|
+
pinMap: Record<string, InputPin & { chipId: string }>
|
|
281
|
+
}): SolvedTracePath => {
|
|
282
|
+
const mspPairId = getPortOnlyLabelConnectorMspPairId(label)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
mspPairId,
|
|
286
|
+
dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
|
|
287
|
+
globalConnNetId: label.globalConnNetId,
|
|
288
|
+
userNetId: label.netId,
|
|
289
|
+
pins: getTracePins(label, pinMap),
|
|
290
|
+
tracePath: [label.anchorPoint, movedLabel.anchorPoint],
|
|
291
|
+
mspConnectionPairIds: [mspPairId],
|
|
292
|
+
pinIds: label.pinIds,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -20,8 +20,7 @@ import type {
|
|
|
20
20
|
TracePathScore,
|
|
21
21
|
} from "./types"
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
const LABEL_HUG_CLEARANCE = 0.001
|
|
23
|
+
const LABEL_CLEARANCE = 0.1
|
|
25
24
|
|
|
26
25
|
export const findBestReroutePath = ({
|
|
27
26
|
trace,
|
|
@@ -102,7 +101,7 @@ export const generateRerouteCandidateResults = ({
|
|
|
102
101
|
trace,
|
|
103
102
|
label,
|
|
104
103
|
problem: inputProblem,
|
|
105
|
-
paddingBuffer:
|
|
104
|
+
paddingBuffer: LABEL_CLEARANCE,
|
|
106
105
|
detourCount: 0,
|
|
107
106
|
}),
|
|
108
107
|
...generateEndpointDetourCandidates(trace, label),
|
|
@@ -188,9 +187,11 @@ const generateEndpointDetourCandidates = (
|
|
|
188
187
|
const start = trace.tracePath[0]
|
|
189
188
|
const end = trace.tracePath[trace.tracePath.length - 1]
|
|
190
189
|
if (!start || !end) return []
|
|
190
|
+
const startExit = trace.tracePath[1] ?? start
|
|
191
|
+
const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end
|
|
191
192
|
|
|
192
193
|
const bounds = getRectBounds(label.center, label.width, label.height)
|
|
193
|
-
const padding =
|
|
194
|
+
const padding = LABEL_CLEARANCE
|
|
194
195
|
const labelDirection = dir(label.orientation)
|
|
195
196
|
const labelSideX =
|
|
196
197
|
labelDirection.x < 0 ? bounds.minX - padding : bounds.maxX + padding
|
|
@@ -204,18 +205,22 @@ const generateEndpointDetourCandidates = (
|
|
|
204
205
|
candidates.push(
|
|
205
206
|
[
|
|
206
207
|
start,
|
|
207
|
-
|
|
208
|
+
startExit,
|
|
209
|
+
{ x: startExit.x, y: topY },
|
|
208
210
|
{ x: sideX, y: topY },
|
|
209
211
|
{ x: sideX, y: bottomY },
|
|
210
|
-
{ x:
|
|
212
|
+
{ x: endEntry.x, y: bottomY },
|
|
213
|
+
endEntry,
|
|
211
214
|
end,
|
|
212
215
|
],
|
|
213
216
|
[
|
|
214
217
|
start,
|
|
215
|
-
|
|
218
|
+
startExit,
|
|
219
|
+
{ x: startExit.x, y: bottomY },
|
|
216
220
|
{ x: sideX, y: bottomY },
|
|
217
221
|
{ x: sideX, y: topY },
|
|
218
|
-
{ x:
|
|
222
|
+
{ x: endEntry.x, y: topY },
|
|
223
|
+
endEntry,
|
|
219
224
|
end,
|
|
220
225
|
],
|
|
221
226
|
)
|
|
@@ -231,6 +236,8 @@ const generateLabelHugCandidates = (
|
|
|
231
236
|
const start = trace.tracePath[0]
|
|
232
237
|
const end = trace.tracePath[trace.tracePath.length - 1]
|
|
233
238
|
if (!start || !end) return []
|
|
239
|
+
const startExit = trace.tracePath[1] ?? start
|
|
240
|
+
const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end
|
|
234
241
|
|
|
235
242
|
const labelDirection = dir(label.orientation)
|
|
236
243
|
if (labelDirection.x === 0) return []
|
|
@@ -238,29 +245,33 @@ const generateLabelHugCandidates = (
|
|
|
238
245
|
const bounds = getRectBounds(label.center, label.width, label.height)
|
|
239
246
|
const sideX =
|
|
240
247
|
labelDirection.x < 0
|
|
241
|
-
? bounds.minX -
|
|
242
|
-
: bounds.maxX +
|
|
243
|
-
const topY = bounds.maxY +
|
|
244
|
-
const bottomY = bounds.minY -
|
|
245
|
-
const startsAboveEnd =
|
|
248
|
+
? bounds.minX - LABEL_CLEARANCE
|
|
249
|
+
: bounds.maxX + LABEL_CLEARANCE
|
|
250
|
+
const topY = bounds.maxY + LABEL_CLEARANCE
|
|
251
|
+
const bottomY = bounds.minY - LABEL_CLEARANCE
|
|
252
|
+
const startsAboveEnd = startExit.y >= endEntry.y
|
|
246
253
|
const firstY = startsAboveEnd ? topY : bottomY
|
|
247
254
|
const secondY = startsAboveEnd ? bottomY : topY
|
|
248
255
|
|
|
249
256
|
return [
|
|
250
257
|
[
|
|
251
258
|
start,
|
|
252
|
-
|
|
259
|
+
startExit,
|
|
260
|
+
{ x: startExit.x, y: firstY },
|
|
253
261
|
{ x: sideX, y: firstY },
|
|
254
262
|
{ x: sideX, y: secondY },
|
|
255
|
-
{ x:
|
|
263
|
+
{ x: endEntry.x, y: secondY },
|
|
264
|
+
endEntry,
|
|
256
265
|
end,
|
|
257
266
|
],
|
|
258
267
|
[
|
|
259
268
|
start,
|
|
260
|
-
|
|
269
|
+
startExit,
|
|
270
|
+
{ x: startExit.x, y: secondY },
|
|
261
271
|
{ x: sideX, y: secondY },
|
|
262
272
|
{ x: sideX, y: firstY },
|
|
263
|
-
{ x:
|
|
273
|
+
{ x: endEntry.x, y: firstY },
|
|
274
|
+
endEntry,
|
|
264
275
|
end,
|
|
265
276
|
],
|
|
266
277
|
]
|
|
@@ -341,9 +352,9 @@ const generateHorizontalSegmentPushCandidate = (
|
|
|
341
352
|
return null
|
|
342
353
|
}
|
|
343
354
|
|
|
344
|
-
let segmentPushX = bounds.minX -
|
|
355
|
+
let segmentPushX = bounds.minX - LABEL_CLEARANCE
|
|
345
356
|
if (labelDirection.x > 0) {
|
|
346
|
-
segmentPushX = bounds.maxX +
|
|
357
|
+
segmentPushX = bounds.maxX + LABEL_CLEARANCE
|
|
347
358
|
}
|
|
348
359
|
const segmentPushStartY = getClearedHorizontalY({
|
|
349
360
|
start: previousAnchor,
|
|
@@ -393,10 +404,10 @@ const getClearedHorizontalY = ({
|
|
|
393
404
|
|
|
394
405
|
const labelCenterY = (labelBounds.minY + labelBounds.maxY) / 2
|
|
395
406
|
if (start.y >= labelCenterY) {
|
|
396
|
-
return labelBounds.maxY +
|
|
407
|
+
return labelBounds.maxY + LABEL_CLEARANCE
|
|
397
408
|
}
|
|
398
409
|
|
|
399
|
-
return labelBounds.minY -
|
|
410
|
+
return labelBounds.minY - LABEL_CLEARANCE
|
|
400
411
|
}
|
|
401
412
|
|
|
402
413
|
const markSelectedCandidate = (
|