This is a pretty unique problem to solve but based on my approach noted above I've devised something that works for all of the lines in your sample file. Luckily your lines are split at junctions otherwise it would not work.
I think there is room for improvement in terms of the order of execution and better filtering order to limit number of comparisons required (for speed) but it works. There are certain situations where it would not work:
- Closely spaced beams where gap between is smaller than beam width
- Line where both ends are not within the limits of the other line i.e. staggered, one starts before the start of the other but stops before the end (as shown below).
There may be a quicker direct approach but to be honest there is a lot to consider in terms of the different forms of nearby parallel line ends. So I don't believe there is a perfect mathematical approach just an object orientated approach where you pick different braches of a decision tree based upon the situation for each set of related end points you find.
There are some constants to be aware of at the top of each class, these relate to tolerance of comparison.

Public Function NearestParallelV1(ByVal commandData As Autodesk.Revit.UI.ExternalCommandData, _
ByRef message As String, ByVal elements As Autodesk.Revit.DB.ElementSet)
If commandData.Application.ActiveUIDocument Is Nothing Then Return Result.Succeeded Else
Dim IntUIDoc As UIDocument = commandData.Application.ActiveUIDocument
Dim IntDoc As Document = IntUIDoc.Document
Dim LinesAlreadyPaired As New List(Of AdjustedLineAndID)
Dim NewCentreLines As New List(Of AdjustedLineAndID)
Dim FEC As New FilteredElementCollector(IntDoc, IntDoc.ActiveView.Id)
Dim ECF As New ElementCategoryFilter(BuiltInCategory.OST_Lines)
Dim Lines As ModelLine() = FEC.WherePasses(ECF).WhereElementIsNotElementType.WhereElementIsViewIndependent. _
ToElements.Cast(Of ModelLine).ToArray
For Each item As ModelLine In Lines
Dim LinesToProcess As New List(Of AdjustedLineAndID)
If item.GeometryCurve.GetType <> GetType(Line) Then
Continue For
End If
Dim EP0 As XYZ = item.GeometryCurve.GetEndPoint(0)
Dim EP1 As XYZ = item.GeometryCurve.GetEndPoint(1)
Dim NV As XYZ = (EP1 - EP0).Normalize
Dim T As Transform = Transform.Identity
T.BasisX = NV
T.BasisY = New XYZ(-NV.Y, NV.X, 0)
T.BasisZ = New XYZ(0, 0, 1)
T.Origin = EP0
Dim MainLine As New AdjustedLineAndID(item, T)
'Re-creating set below for each iteration is time consuming
'If we group lines into direction groups first we can iterate those directions instead
'and deduct 'item' co-ords from 'other.item' co-ords.
'i.e. no need to compare each line against each line but instead compare each line against only the lines it is parallel to
For Each Otheritem As ModelLine In Lines
LinesToProcess.Add(New AdjustedLineAndID(Otheritem, T))
Next
Dim LineSet As New AdjustedLineAndIDSet(MainLine, LinesToProcess, LinesAlreadyPaired)
If LineSet.IsEmpty Then Continue For Else
LinesAlreadyPaired.AddRange(LineSet.OriginalLinesInSet)
NewCentreLines.AddRange(LineSet.NewCentreLines)
Next
If NewCentreLines.Count = 0 Then Return Result.Cancelled Else
Using Tx As New Transaction(IntDoc, "Add centre lines")
If Tx.Start = TransactionStatus.Started Then
For Each item As AdjustedLineAndID In NewCentreLines
Dim NL As Line = Line.CreateBound(item.NewEnds(0), item.NewEnds(1))
IntDoc.Create.NewDetailCurve(IntDoc.ActiveView, NL)
Next
Tx.Commit()
End If
End Using
Return Result.Succeeded
End Function
Private Class AdjustedLineAndIDSet
Const mm_to_ft As Double = 1 / 304.8
'Max parallel distance to search for nearest neighbour e.g. max beam width
Private Shared MaxWidth As Double = 400 * mm_to_ft
Private IntMainLine As AdjustedLineAndID
Private IntLinesWithinLimits As New List(Of AdjustedLineAndID)
Public ReadOnly Property IsEmpty As Boolean
Get
Return IntLinesWithinLimits.Count = 0
End Get
End Property
Public ReadOnly Property OriginalLinesInSet As List(Of AdjustedLineAndID)
Get
If IsEmpty Then Return New List(Of AdjustedLineAndID) Else
Dim Out As New List(Of AdjustedLineAndID)
Out.Add(IntMainLine)
Out.AddRange(IntLinesWithinLimits)
Return Out
End Get
End Property
Public ReadOnly Property NewCentreLines As List(Of AdjustedLineAndID)
Get
Return GetNewCentreLines()
End Get
End Property
Public Sub New(MainLine As AdjustedLineAndID, OtherLines As List(Of AdjustedLineAndID), _
Optional Excluded As List(Of AdjustedLineAndID) = Nothing)
If Excluded Is Nothing Then Excluded = New List(Of AdjustedLineAndID)
If MainLine Is Nothing Then Exit Sub Else
IntMainLine = MainLine
If OtherLines.Contains(MainLine) Then
OtherLines.Remove(MainLine)
End If
Dim Filtered As List(Of AdjustedLineAndID) = OtherLines.FindAll(Function(j) Excluded.Contains(j) = False AndAlso _
IntMainLine.GetEndsRelationToOther(j) = _
AdjustedLineAndID.EndsRelationToOther.ParallelAndBothEndsWithin)
Dim dist_ft As Double = 0
Dim NPL As AdjustedLineAndID = MainLine.NearestParallelLine(Filtered, dist_ft)
If NPL Is Nothing Then Exit Sub Else
dist_ft += AdjustedLineAndID.DistanceTolerance
If dist_ft > MaxWidth Then Exit Sub Else
Dim Filtered1 As List(Of AdjustedLineAndID) = Filtered.FindAll(Function(j) IntMainLine.IsNearToOther(j, dist_ft))
IntLinesWithinLimits = Filtered1
End Sub
Private Function GetNewCentreLines() As List(Of AdjustedLineAndID)
Dim Out As New List(Of AdjustedLineAndID)
For Each item As AdjustedLineAndID In IntLinesWithinLimits
Dim Y As Double = item.AverageLocalY / 2
Dim E1 As New XYZ(item.NewEnds(0).X, Y, 0)
Dim E2 As New XYZ(item.NewEnds(1).X, Y, 0)
Out.Add(New AdjustedLineAndID(New XYZ() {E1, E2}, IntMainLine.TransformUsed.Inverse))
Next
Return Out
End Function
End Class
Private Class AdjustedLineAndID
Implements IEquatable(Of AdjustedLineAndID)
Const mm_to_ft As Double = 1 / 304.8
'When comparing position of lines what tolerance to use
Public Shared Property DistanceTolerance As Double = 5 * mm_to_ft
'When comparing end extents of parallel lines how much beyond strict limit to still count as within limits
Public Shared Property EndExtentsTolerance As Double = 150 * mm_to_ft
'General tolerance used for rounding comparisons of doubles
Public Shared Property DecimalPlaces As Integer = 8
Private IntNewEnds As XYZ()
Private IntOriginalEnds As XYZ()
Private IntLocalAvY As Double
Private IntTransform As Transform
Public ReadOnly Property TransformUsed() As Transform
Get
Return IntTransform
End Get
End Property
Public ReadOnly Property AverageLocalY() As Double
Get
Return IntLocalAvY
End Get
End Property
Public ReadOnly Property NewEnds([End] As Integer) As XYZ
Get
Return IntNewEnds([End])
End Get
End Property
Public ReadOnly Property OriginalEnds([End] As Integer) As XYZ
Get
Return IntOriginalEnds([End])
End Get
End Property
Public Sub New(Line As ModelLine, T As Transform)
MyClass.New(Line.GeometryCurve, T)
End Sub
Public Sub New(Line As DetailLine, T As Transform)
MyClass.New(Line.GeometryCurve, T)
End Sub
Public Sub New(Line As Line, T As Transform)
MyClass.New(New XYZ() {Line.GetEndPoint(0), Line.GetEndPoint(1)}, T)
End Sub
Public Sub New(EndPoints As XYZ(), T As Transform)
If EndPoints.Length <> 2 Then Throw New ArgumentException("EndPoints must be an array containing 2 elements.")
IntOriginalEnds = EndPoints
IntNewEnds = New XYZ() {T.Inverse.OfPoint(EndPoints(0)), _
T.Inverse.OfPoint(EndPoints(1))}
IntLocalAvY = (IntNewEnds(0).Y + IntNewEnds(1).Y) / 2
IntTransform = T
End Sub
Public Function IsParallelToOther(other As AdjustedLineAndID) As Boolean
Dim XYZ1adj As XYZ = IntTransform.Inverse.OfPoint(other.IntOriginalEnds(0))
Dim XYZ2adj As XYZ = IntTransform.Inverse.OfPoint(other.IntOriginalEnds(1))
Dim Y1 As Double = TruncateDbl(XYZ1adj.Y)
Dim Y2 As Double = TruncateDbl(XYZ2adj.Y)
Return Y1 = Y2
End Function
Private Shared Function TruncateDbl(V As Double) As Double
Return CDbl(V.ToString("F" & CStr(DecimalPlaces)))
End Function
Public Function NearestParallelLineOLD(others As List(Of AdjustedLineAndID), ByRef ParallelDistance As Double) As AdjustedLineAndID
Dim Jx As List(Of AdjustedLineAndID) = others.FindAll(Function(x) IsParallelToOther(x) = True _
AndAlso Math.Abs(x.IntLocalAvY) > 0)
If Jx.Count = 0 Then Return Nothing Else
Dim ParallelDist = Jx.Min(Function(x) Math.Abs(x.IntLocalAvY))
ParallelDistance = ParallelDist
Dim LN As AdjustedLineAndID = Jx.Find(Function(x) Math.Abs(x.IntLocalAvY) = ParallelDist)
Return LN
End Function
'Same as above but assumes input set are already parallel to this line
Public Function NearestParallelLine(ParallelLines As List(Of AdjustedLineAndID), ByRef ParallelDistance As Double) As AdjustedLineAndID
Dim Jx As List(Of AdjustedLineAndID) = ParallelLines.FindAll(Function(x) Math.Abs(x.IntLocalAvY) > 0)
If Jx.Count = 0 Then Return Nothing Else
Dim ParallelDist = Jx.Min(Function(x) Math.Abs(x.IntLocalAvY))
ParallelDistance = ParallelDist
Dim LN As AdjustedLineAndID = Jx.Find(Function(x) Math.Abs(x.IntLocalAvY) = ParallelDist)
Return LN
End Function
Private Function NumberOfEndsOfOtherWithinMyLimits(other As AdjustedLineAndID, ByRef BothToOneSide As Boolean) As Byte 'number of ends
Dim MinX As Double
Dim MaxX As Double
If Me.IntNewEnds(0).X < Me.IntNewEnds(1).X Then
MinX = Me.IntNewEnds(0).X
MaxX = Me.IntNewEnds(1).X
Else
MinX = Me.IntNewEnds(1).X
MaxX = Me.IntNewEnds(0).X
End If
Dim Lim0 As Double = MinX - EndExtentsTolerance
Dim Lim1 As Double = MaxX + EndExtentsTolerance
Dim Out As Byte = 0
If other.IntNewEnds(0).X >= Lim0 AndAlso other.IntNewEnds(0).X <= Lim1 Then Out += 1
If other.IntNewEnds(1).X >= Lim0 AndAlso other.IntNewEnds(1).X <= Lim1 Then Out += 1
If Out = 0 Then
If other.IntNewEnds(0).X < Lim0 AndAlso other.IntNewEnds(1).X < Lim0 Then BothToOneSide = True Else
If other.IntNewEnds(0).X > Lim1 AndAlso other.IntNewEnds(1).X > Lim1 Then BothToOneSide = True Else
End If
Return Out
End Function
Public Function IsNearToOther(other As AdjustedLineAndID, DistanceTolerance As Double)
Dim Y1 As Double = TruncateDbl(Math.Abs(Me.IntLocalAvY))
Dim Y2 As Double = TruncateDbl(Math.Abs(other.IntLocalAvY))
If Y1 > Y2 Then
Return Y1 - Y2 <= TruncateDbl(DistanceTolerance)
Else
Return Y2 - Y1 <= TruncateDbl(DistanceTolerance)
End If
End Function
Public Enum EndsRelationToOther As Byte
None = 0
ParallelAndBothEndsWithin = 1
ParallelAndOneEndWithin = 2
ParallelAndBothEndsToOneSide = 3
ParallelAndEndsEitherSideSide = 4
NotParallelTo = 5
End Enum
Public Function GetEndsRelationToOther(other As AdjustedLineAndID) As EndsRelationToOther
If IsParallelToOther(other) Then
Dim BothToOneSide As Boolean = False
Dim E As Byte = NumberOfEndsOfOtherWithinMyLimits(other, BothToOneSide)
Select Case E
Case Is = 0
If BothToOneSide = True Then
Return EndsRelationToOther.ParallelAndBothEndsToOneSide
Else
Return EndsRelationToOther.ParallelAndEndsEitherSideSide
End If
Case Is = 1
Return EndsRelationToOther.ParallelAndOneEndWithin
Case Is = 2
Return EndsRelationToOther.ParallelAndBothEndsWithin
End Select
Else
Return EndsRelationToOther.NotParallelTo
End If
End Function
Public Overloads Function Equals(other As AdjustedLineAndID) As Boolean Implements IEquatable(Of AdjustedLineAndID).Equals
Dim a1 As XYZ = other.IntOriginalEnds(0)
Dim a2 As XYZ = other.IntOriginalEnds(1)
Dim b1 As XYZ = Me.IntOriginalEnds(0)
Dim b2 As XYZ = Me.IntOriginalEnds(1)
If a1.DistanceTo(b1) <= DistanceTolerance Then
Return a2.DistanceTo(b2) <= DistanceTolerance
Else
Return a1.DistanceTo(b2) <= DistanceTolerance AndAlso a2.DistanceTo(b1) <= DistanceTolerance
End If
End Function
End Class