Automatyczne wymiarowanie pomieszczeń w Dynamo

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.

Posts created 32

Dodaj komentarz

Twój adres email nie zostanie opublikowany.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top