Когда мы программируем под AutoCAD – то перед нами часто возникает задача быстрого пространственного поиска по набору примитивов. Лучше всего подобный поиск реализуется с помощью R-дерева.
Для примера будем парсить «рисованные» таблицы (это которые нарисованы отрезками и текстом) и создавать по ним ACAD-таблицы (это которые создаются командой _table)
Возьмём готовую реализацию R-дерева, его использование очень просто, но нам понадобится класс с которым оно будет работать. Он будет называется MyCell. Тогда создание дерева:
Me.wTree = New RTree(Of MyCell)()
Добавление примитва
Me.wTree.Add(nCell.GetRectangle, nCell)
Здесь Rectangle это MBR. При работе с базой данных чертежа мы получаем объекты DBObject, которые наследуют класс Drawable у которого есть свойство Bounds As Autodesk.AutoCAD.DatabaseServices.Extents3d?, которое, к удивлению иногда может возвращать Nothing – это потому, что если мы взглянем в описание DrawableType мы увидим там значения со словами «Light» и «Background» — у этих слов проблемы с границами. Но если мы будем работать с DrawableType.Geometry – у нас будут и границы, хотя стоит присмотреться к XLine и Ray…
После создания и заполнения R-дерева, мы переходим к поиску в нём – для этого существует два метода:
Получаем список объектов внутри MBR
Public Function Intersects(r As RTree.Rectangle) As System.Collections.Generic.List(Of T)
Получаем список объектов около точки поиска
Public Function Nearest(p As RTree.Point, furthestDistance As Single) As System.Collections.Generic.List(Of T)
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.Geometry
Imports RTree
Public Class MyCell
Public Box As Line 'Размер ячейки
Public Col As Integer
Public Row As Integer
Public Value As String
Public Sub New()
Box = Nothing
Col = 0
Row = 0
Value = ""
End Sub
Public Sub New(nBox As Line, wCol As Integer, wRow As Integer, nValue As String)
Box = nBox
Col = wCol
Row = wRow
Value = nValue
End Sub
Public Function GetH() As Double
Return Box.EndPoint.Y - Box.StartPoint.Y
End Function
Public Function GetW() As Double
Return Box.EndPoint.X - Box.StartPoint.X
End Function
Public Function GetRectangle() As Rectangle
Return New Rectangle(Box.StartPoint.X, Box.StartPoint.Y, Box.EndPoint.X, Box.EndPoint.Y, 0, 0)
End Function
End Class
Imports MyAcAs = Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.Geometry
Imports RTree
Public Class MyTable
'Public Shared MinColW As Double = 1
'Public Shared MinRowH As Double = 1
Private vert, horz As List(Of Line) 'Список линий формирующих таблицу
Public Cells(,) As MyCell 'Массив ячеек
Friend wTree As RTree(Of MyCell) 'Дерево для поиска ячеек
Public Enum Orent
Vert ' вертикальна
Horz ' горизонтална
None ' неопределенна
End Enum
Public Shared Function isOrto(wL As Line) As Orent
'Определяем орентацию линии - вертикальна или горизонтална
Dim wValue As Double = wL.Angle / Math.PI
Dim delta As Double = 0.05
wValue = wValue - Math.Truncate(wValue + delta / 2)
If Math.Abs(wValue) <= delta Then
Return Orent.Horz
ElseIf (Math.Abs(wValue) < 0.5 + delta) And (Math.Abs(wValue) > 0.5 - delta) Then
Return Orent.Vert
Else
Return Orent.None
End If
End Function
Private Shared Function CompareByX(l1 As Line, l2 As Line) As Integer
If l1.StartPoint.X > l2.StartPoint.X Then
Return 1
ElseIf l1.StartPoint.X = l2.StartPoint.X Then
Return 0
Else
Return -1
End If
End Function
Private Shared Function CompareByY(l1 As Line, l2 As Line) As Integer
If l1.StartPoint.Y > l2.StartPoint.Y Then
Return 1
ElseIf l1.StartPoint.Y = l2.StartPoint.Y Then
Return 0
Else
Return -1
End If
End Function
Private Shared Function GetSelect(ed As Editor) As ObjectId()
'Получаем от пользователя набор данных для парсинга
Dim PSResult As PromptSelectionResult
Dim wTV() As TypedValue = {New TypedValue(DxfCode.Operator, "<or"), _
New TypedValue(DxfCode.Start, "LINE"), _
New TypedValue(DxfCode.Start, "LWPOLYLINE"), _
New TypedValue(DxfCode.Start, "TEXT"), _
New TypedValue(DxfCode.Start, "MTEXT"), _
New TypedValue(DxfCode.Operator, "or>")}
Dim wSF As New SelectionFilter(wTV)
PSResult = ed.GetSelection(wSF)
If PSResult.Status = PromptStatus.OK Then
Return PSResult.Value.GetObjectIds()
Else
Return Nothing
End If
End Function
Private Shared Function PolyToLine(pl As Polyline) As List(Of Line)
Dim wList As New List(Of Line)
Dim wL As Line
For i = 0 To pl.NumberOfVertices - 2
wL = New Line(pl.GetPoint3dAt(i), pl.GetPoint3dAt(i + 1))
wList.Add(wL)
Next
Return wList
End Function
Private Sub New(nvert As List(Of Line), nhorz As List(Of Line))
'Формируем "пустую" таблицу из линий
Me.vert = nvert
Me.horz = nhorz
Dim CC, RC As Integer
CC = Me.GetCols()
RC = Me.GetRows()
ReDim Me.Cells(CC, RC)
Me.wTree = New RTree(Of MyCell)()
Dim wLine As Line
Dim nCell As MyCell
For i = 0 To CC - 1
For j = 0 To RC - 1
wLine = Me.GetCellBox(i, j)
nCell = New MyCell(wLine, i, j, "")
Me.Cells(i, j) = nCell
Me.wTree.Add(nCell.GetRectangle, nCell)
Next
Next
End Sub
Public Sub SetValue(wt As DBText)
'Заполняем таблицу
If wt.Bounds IsNot Nothing Then
Dim tExtent As Extents3d = wt.Bounds
Dim X, Y As Double
X = (tExtent.MaxPoint.X + tExtent.MinPoint.X) / 2
Y = (tExtent.MaxPoint.Y + tExtent.MinPoint.Y) / 2
Dim wP As New Point(X, Y, 0)
Dim wList As List(Of MyCell) = Me.wTree.Nearest(wP, wt.Height / 2)
If wList IsNot Nothing Then
If wList.Count > 0 Then wList(0).Value = wt.TextString
End If
End If
End Sub
Public Sub SetValue(wt As MText)
'Заполняем таблицу
If wt.Bounds IsNot Nothing Then
Dim tExtent As Extents3d = wt.Bounds
Dim X, Y As Double
X = (tExtent.MaxPoint.X + tExtent.MinPoint.X) / 2
Y = (tExtent.MaxPoint.Y + tExtent.MinPoint.Y) / 2
Dim wP As New Point(X, Y, 0)
Dim wList As List(Of MyCell) = Me.wTree.Nearest(wP, 1)
If wList IsNot Nothing Then
If wList.Count > 0 Then wList(0).Value = wt.Text
End If
End If
End Sub
Private Shared Function CrTbl(wList As List(Of Line)) As MyTable
'Формируем "пустую" таблицу из линий
Dim nvert, nhorz, overt, ohorz As List(Of Line)
nvert = wList.FindAll(Function(l) isOrto(l) = Orent.Vert)
nvert.Sort(AddressOf CompareByX)
nhorz = wList.FindAll(Function(l) isOrto(l) = Orent.Horz)
nhorz.Sort(AddressOf CompareByY)
'
Dim MinColW, MinRowH As Double
MinColW = Math.Abs(nvert(0).StartPoint.X - nvert(nvert.Count - 1).StartPoint.X) * 0.01
MinRowH = Math.Abs(nhorz(0).StartPoint.Y - nhorz(nhorz.Count - 1).StartPoint.Y) * 0.01
'
Dim ol As Line = Nothing
overt = New List(Of Line)
For Each l In nvert
If ol Is Nothing Then
ol = l
overt.Add(l)
Else
If Math.Abs(l.StartPoint.X - ol.StartPoint.X) > MinColW Then
ol = l
overt.Add(l)
End If
End If
Next
'
ohorz = New List(Of Line)
For Each l In nhorz
If ol Is Nothing Then
ol = l
ohorz.Add(l)
Else
If Math.Abs(l.StartPoint.Y - ol.StartPoint.Y) > MinRowH Then
ol = l
ohorz.Add(l)
End If
End If
Next
Return New MyTable(overt, ohorz)
End Function
Public Shared Function CrTbl(acDoc As MyAcAs.Document) As MyTable
'Создаём таблицу
Dim ed As Editor = acDoc.Editor
Dim objIdArray() As ObjectId = MyTable.GetSelect(ed) 'Получаем от пользователя набор данных для парсинга
If objIdArray IsNot Nothing Then
Dim dbObj As DBObject
Dim wList As New List(Of Line)
Dim wTList As New List(Of DBText)
Dim wMTList As New List(Of MText)
Using tr As Transaction = acDoc.Database.TransactionManager.StartTransaction
Try
For Each objId As ObjectId In objIdArray
dbObj = tr.GetObject(objId, OpenMode.ForRead)
'Сортируем полученные объекты
Select Case True
Case TypeOf dbObj Is Line
wList.Add(dbObj)
Case TypeOf dbObj Is Polyline
wList.AddRange(MyTable.PolyToLine(dbObj))
Case TypeOf dbObj Is DBText
wTList.Add(dbObj)
Case TypeOf dbObj Is MText
wMTList.Add(dbObj)
End Select
Next
tr.Commit()
Catch ex As Exception
ed.WriteMessage(ex.ToString())
tr.Abort()
End Try
End Using
'
Dim wMTbl As MyTable = MyTable.CrTbl(wList)
'Заполняем текстом
For Each wt In wTList
wMTbl.SetValue(wt)
Next
For Each wmt In wMTList
wMTbl.SetValue(wmt)
Next
Return wMTbl
Else
Return Nothing
End If
End Function
Public Function GetCols() As Integer
Return vert.Count - 1
End Function
Public Function GetColW(i As Integer) As Double
Dim res As Double = Math.Abs(vert(i + 1).StartPoint.X - vert(i).StartPoint.X)
If res = 0 Then res = 1 '?!
Return res
End Function
Public Function GetRows() As Integer
Return horz.Count - 1
End Function
Public Function GetRowH(j As Integer) As Double
Dim res As Double = Math.Abs(horz(j + 1).StartPoint.Y - horz(j).StartPoint.Y)
If res = 0 Then res = 1 '?!
Return res
End Function
Public Function GetCellBox(i As Integer, j As Integer) As Line
'Получаем диагональную линию в нужной ячейке (размер)
Dim p1, p2 As Point3d
p1 = New Point3d(vert(i).StartPoint.X, horz(j).StartPoint.Y, 0)
p2 = New Point3d(vert(i + 1).StartPoint.X, horz(j + 1).StartPoint.Y, 0)
Return New Line(p1, p2)
End Function
Public Function CrTbl(ip As Point3d) As Table
'Создаём ACAD-таблицу
Dim res As New Table()
Dim Rs, Cs As Integer
Rs = Me.GetRows()
Cs = Me.GetCols()
res.SetSize(Rs, Cs)
res.Position = ip
For i = 0 To Cs - 1
res.Columns(i).Width = Me.GetColW(i)
For j = 0 To Rs - 1
res.Rows(j).Height = Me.GetRowH(j)
res.Cells(Rs - j - 1, i).TextString = Me.Cells(i, j).Value
Next
Next
res.GenerateLayout() '!?
Return res
End Function
End Class
Imports Autodesk.AutoCAD.Runtime
Imports MyAcAs = Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.DatabaseServices
Public Class AcadWork
<CommandMethod("TblParse")> _
Public Sub TblParse()
Dim acDoc As MyAcAs.Document = MyAcAs.Application.DocumentManager.MdiActiveDocument
Dim ed As Editor = acDoc.Editor
Dim wMTbl As MyTable = MyTable.CrTbl(acDoc)
'
Dim PPResult As PromptPointResult
PPResult = ed.GetPoint("Точка вставки")
If PPResult.Status = PromptStatus.OK Then
Dim nTbl As Table = wMTbl.CrTbl(PPResult.Value)
Using tr As Transaction = acDoc.Database.TransactionManager.StartTransaction
Try
Dim bt As BlockTable = tr.GetObject(acDoc.Database.BlockTableId, OpenMode.ForRead)
Dim btr As BlockTableRecord = tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite)
btr.AppendEntity(nTbl)
tr.AddNewlyCreatedDBObject(nTbl, True)
tr.Commit()
Catch ex As Exception
ed.WriteMessage(ex.ToString())
tr.Abort()
End Try
End Using
End If
End Sub
End Class
Заключение
Использование R-дерева позволяет очень быстро выполнять поиск по нужному набору примитивов, без использования методов AutoCAD'а.
Автор: trir