W niniejszym poście (mam nadzieję początku serii) chciałbym przekazać kilka ostatnich przemyśleń dotyczących możliwości automatyzacji wymiarowania pomieszczeń w Revicie. Nie jest to banalne zadanie, zwłaszcza że nie ma jednego słusznego sposobu lokalizowania wymiarów. Dzisiaj pokażę najprostszy chyba wariant – utworzenie wymiarów wzdłuż głównych kierunków pomieszczeń.
Czyli w naszym przykładowym układzie postawimy wymiary odpowiednio pomiędzy niebieskimi i czerwonymi liniami.

Zdaję sobie sprawę, że nie jest to idealny rezultat wymagający uporządkowania, ale wykracza to poza temat tego posta:)
Kod skryptu będzie dość skomplikowany dlatego zdecydowałem się go podzielić na kilka modułów:

Zacznijmy więc z pierwszym, najprostszym, węzłem. Jego zadaniem jest tylko wydobycie segmentów obrysu pomieszczenia (Boundary Segments). Wydaje mi się, że kod jest dość łatwy do zrozumienia:
import clr clr.AddReference('RevitAPI') import Autodesk from Autodesk.Revit.DB import * #The inputs to this node will be stored as a list in the IN variables. rooms = UnwrapElement(IN[0]) output = [] options = SpatialElementBoundaryOptions() options.SpatialElementBoundaryLocation = SpatialElementBoundaryLocation.Finish for r in rooms: segments = [] loops = r.GetBoundarySegments(options) for loop in loops: for segment in loop: segments.append(segment) output.append(segments) #Assign your output to the OUT variable. OUT = output
Drugi węzeł jest odrobinę bardziej skomplikowany. Kod składa się z dwóch głównych pętli. Pierwsza z nich grupuje otrzymane segmenty obrysu według ich kierunku. W większości wypadków utworzy po prostu dwie listy – jedną dla segmentów pionowych drugą dla poziomych.
#group parallel segments together for rs in room_segments: directions = [] segment_groups = [] for segment in rs: l = segment.GetCurve() d = l.GetEndPoint(1)-l.GetEndPoint(0) idx = -1 for i in range(len(directions)): if isParallel(d,directions[i]): idx = i break if idx!=-1: segment_groups[idx].append(segment) else: directions.append(d) new_group = [] new_group.append(segment) segment_groups.append(new_group) sets.append(segment_groups)
Druga pętla grupuje segmenty które leżą na jednej linii. W Revicie często zdarza się, że linia obrysu budynku składa się z kilku mniejszych segmentów. Dzieje się tak np. gdy ściana została podzielona lub zawiera zagnieżdżony słup. Np. w naszym przykładzie powyżej możemy zauważyć, że w prawym pomieszczeniu górna krawędź składa się z dwóch oddzielnych ścian. Także w wyniku naszego pierwszego węzła kodu otrzymamy dwa oddzielne segmenty dla tej krawędzi pomieszczenia. I teraz musimy je zgrupować.
#split groups into collinear sets for rs in sets: room_output = [] for set in rs: csets = [] for s in set: for cs in csets: if len(cs)>0: l0 = s.GetCurve() l1=cs[0].GetCurve() if isCollinear(l0,l1): cs.append(s) break else: new_set = [s] csets.append(new_set) room_output.append(csets) output.append(room_output)
Drugi węzeł zawiera również kilka funkcji pomocniczych, głównie dotyczących obliczeń wektorowych. Aby nie przedłużać posta nie będę ich tu objaśniał. Pełny kod węzła wygląda tak:
import clr clr.AddReference('RevitAPI') import Autodesk from Autodesk.Revit.DB import * def isParallel(v1,v2): return v1.CrossProduct(v2).IsAlmostEqualTo(XYZ(0,0,0)) def isCollinear(l0,l1): a = l0.GetEndPoint(0) b = l0.GetEndPoint(1) c = l1.GetEndPoint(0) d = l1.GetEndPoint(1) return (b-a).CrossProduct(c-a).IsAlmostEqualTo((b-a).CrossProduct(d-a)) and (b-a).CrossProduct(c-a).IsAlmostEqualTo(XYZ(0,0,0)) #The inputs to this node will be stored as a list in the IN variables. room_segments = UnwrapElement(IN[0]) sets = [] #group parallel segments together for rs in room_segments: directions = [] segment_groups = [] for segment in rs: l = segment.GetCurve() d = l.GetEndPoint(1)-l.GetEndPoint(0) idx = -1 for i in range(len(directions)): if isParallel(d,directions[i]): idx = i break if idx!=-1: segment_groups[idx].append(segment) else: directions.append(d) new_group = [] new_group.append(segment) segment_groups.append(new_group) sets.append(segment_groups) output = [] #split groups into collinear sets for rs in sets: room_output = [] for set in rs: csets = [] for s in set: for cs in csets: if len(cs)>0: l0 = s.GetCurve() l1=cs[0].GetCurve() if isCollinear(l0,l1): cs.append(s) break else: new_set = [s] csets.append(new_set) room_output.append(csets) output.append(room_output) #Assign your output to the OUT variable. OUT = output
Zadaniem ostatniego węzła kodu jest stworzenie wymiarów, po jednym dla każdego kierunku głównego w każdym z pomieszczeń. Musimy teraz zadecydować które dokładnie segmenty obrysu zamierzamy zwymiarować. Jednym z prostszych rozwiązań, tak jak w przykładzie na początku posta, będzie postawienie wymiaru pomiędzy najdalszymi segmentami. W ten sposób otrzymamy maksymalne długości i szerokości pomieszczeń.
Na początku kod sortuje segmenty w każdej z utworzonych grup wg długości. Nie jest to konieczne, ale zakładam że najdłuższy odcinek w każdej grupie jest najistotniejszy. Następnie utworzymy listę zawierającą pary naszych grup. Każda para będzie też zawierała informację o odległości pomiędzy segmentami. W ten sposób możemy znaleźć parę najbardziej oddalonych od siebie segmentów.
Kiedy posortujemy już listę par wg dystansu, wybieramy pierwszy jej element. Następnie pobieramy referencje do najdłuższych segmentów z każdej grupy w parze. Tworzymy również prostopadłą do jednego z segmentów linię, przechodzącą przez jego środek. To będzie lokalizacja naszego wymiaru.
for rs in room_sets: for dir in rs: for set in dir: set=sorted(set, key=lambda x: x.GetCurve().Length, reverse = True) set_pairs = [] for s0 in dir: for s1 in dir: if s0!=s1: c = s0[0].GetCurve() c.MakeUnbound() d = c.Distance(s1[0].GetCurve().GetEndPoint(0)) set_pairs.append([d,s0[0],s1[0]]) sorted_by_distance = sorted(set_pairs, key = lambda x:x[0], reverse=True) first = sorted_by_distance[0][1] second = sorted_by_distance[0][2] nl = normal_line(first) refArray = ReferenceArray() refArray.Append(segment_reference(first)) refArray.Append(segment_reference(second)) d = doc.Create.NewDimension(view, nl, refArray) dims.append(d)
Trzeci węzeł również zawiera kilka dodatkowych funkcji pomocniczych. Jedna z nich umożliwia pobranie referencji (dla linii wymiarowej) z segmentów. Poniższy kod obsługuje tylko dwa rodzaje wydzielających elementów – ściany i linie room separator.
def segment_reference(s): se = doc.GetElement(s.ElementId) #if model line (room separator) if isinstance(se, Autodesk.Revit.DB.ModelLine): return se.GeometryCurve.Reference #if wall if isinstance(se,Autodesk.Revit.DB.Wall): rExt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Exterior)[0] rInt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Interior)[0] fExt = doc.GetElement(rExt).GetGeometryObjectFromReference(rExt) fInt = doc.GetElement(rInt).GetGeometryObjectFromReference(rInt) if fExt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fExt.Intersect(s.GetCurve())==SetComparisonResult.Subset: return rExt if fInt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fInt.Intersect(s.GetCurve())==SetComparisonResult.Subset: return rInt return None
Całość kodu trzeciego węzła wygląda tak:
import clr clr.AddReference("RevitAPI") from Autodesk.Revit.DB import * import Autodesk clr.AddReference("RevitServices") import RevitServices from RevitServices.Transactions import TransactionManager from RevitServices.Persistence import DocumentManager def normal_line(s): l = s.GetCurve() d = l.Direction n = XYZ(-d.Y,d.X,0) m = l.GetEndPoint(0) + (l.GetEndPoint(1)-l.GetEndPoint(0))/2 nl = Line.CreateBound(m, m+n) return nl def segment_reference(s): se = doc.GetElement(s.ElementId) #if model line (room separator) if isinstance(se, Autodesk.Revit.DB.ModelLine): return se.GeometryCurve.Reference #if wall if isinstance(se,Autodesk.Revit.DB.Wall): rExt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Exterior)[0] rInt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Interior)[0] fExt = doc.GetElement(rExt).GetGeometryObjectFromReference(rExt) fInt = doc.GetElement(rInt).GetGeometryObjectFromReference(rInt) if fExt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fExt.Intersect(s.GetCurve())==SetComparisonResult.Subset: return rExt if fInt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fInt.Intersect(s.GetCurve())==SetComparisonResult.Subset: return rInt return None doc = DocumentManager.Instance.CurrentDBDocument #The inputs to this node will be stored as a list in the IN variables. room_sets = UnwrapElement(IN[0]) view = UnwrapElement(IN[1]) dims = [] TransactionManager.Instance.EnsureInTransaction(doc) for rs in room_sets: for dir in rs: for set in dir: set=sorted(set, key=lambda x: x.GetCurve().Length, reverse = True) set_pairs = [] for s0 in dir: for s1 in dir: if s0!=s1: c = s0[0].GetCurve() c.MakeUnbound() d = c.Distance(s1[0].GetCurve().GetEndPoint(0)) set_pairs.append([d,s0[0],s1[0]]) sorted_by_distance = sorted(set_pairs, key = lambda x:x[0], reverse=True) first = sorted_by_distance[0][1] second = sorted_by_distance[0][2] nl = normal_line(first) refArray = ReferenceArray() refArray.Append(segment_reference(first)) refArray.Append(segment_reference(second)) d = doc.Create.NewDimension(view, nl, refArray) dims.append(d) TransactionManager.Instance.TransactionTaskDone() #Assign your output to the OUT variable. OUT = dims
Powyższy kod, pomimo że jest już całkiem złożony, wciąż ma ograniczone możliwości. Planuję napisać dalsze posty, w których pokażę jak można go rozwinąć. Przykładowo w prawym pomieszczeniu z przykładu powyżej należałoby dodatkowo zwymiarować zaułek. Również przydatna była możliwość pobrania referencji ze słupów wydzielających pomieszczenia.
Poniżej załączam plik z definicją w Dynamo 2.0.3.