Est si on utilisait Dynamo pour visualiser un IFC? Et si on profitait des avantages des 2 moteurs Python ?
Bien que Dynamo ne soit pas un viewer Ifc, l'idée de charger la géométrie d'un Ifc complet dans Dynamo m'a toujours trotté dans la tête.
- Récupérer les données géométriques d'un IFC
Pour exemple, reprenons l'IFC 2x3 du projet RAC_basic_sample, nous récupérons les géométries (coordonnées des triangles) et les couleurs RGB des éléments via la librairie ifcopenshell et le moteur CPython3.
Ici, nous profitons du multiprocessing que permet la libraire ifcopenshell pour récupérer les informations géométriques (shape de chaque Element Ifc)
Ensuite, pour tracer les triangles (convertis en surfaces), on utilisera IronPython.
Pour transférer les donnes du nœud CPython3 au nœud IronPython, la solution la plus simple aurait été d'utiliser une liste, cependant cela représenterait plusieurs centaines de milliers d'items.
liste avec + de 2 millions items |
- Interopérabilité entre les moteurs Pythons (CPython3 et IronPython)
ma première idée (avec peu d'espoir) était d'utiliser une classe Python3 pour stocker la liste pour ensuite transférer un objet/instance vers le nœud IronPython, et comme je m'en doutais un peu, cela ne fonctionne pas.
Une solution de contournement est d'utiliser un objet .Net et non Itérable / Énumérable pour éviter qu'il soit converti en Liste par le wrapper Dynamo.
- Solution n°1 : une Classe .Net (CLR)
import sys
import clr
import System
from System.Collections.Generic import List
class ExampleClrClass(System.Object):
__namespace__ = "MyNameSpace_"
def __init__(self, lst):
super().__init__()
self.__lst = lst
@clr.clrmethod(List[System.Int32], [int])
def Multiply(self, x):
return List[System.Int32]([n * x for n in self.__lst])
@clr.clrproperty(List[System.Int32])
def GetList(self):
return List[System.Int32](self.__lst)
a = ExampleClrClass(list(range(10000)))
OUT = a
Néanmoins, sous Dynamo, pour une raison que j'ignore, PythonNet 2.5.x ne parvient pas à libérer toutes les ressources (Memory Leak) pour certains objets, les classes Net/CLR en font partie.- Solution n°2 : une DataTable
Une DataTable est un objet de type Table de données dont la structure est un peu similaire à un DataFrame Pandas ou une Table SQL, elle peut stocker plus de 16 millions de lignes.
Ce sera ici la solution retenue, on évite ainsi que la sortie du nœud Python (Dynamo wrapper) ait à traiter une immense liste (5 à 10 secondes de gagner).
Le code CPython3
import clr
import sys
import re
import System
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS
clr.AddReference('System.Data')
from System.Data import *
clr.AddReference('Python.Included')
import Python.Included as pyInc
path_py3_lib = pyInc.Installer.EmbeddedPythonHome
sys.path.append(path_py3_lib + r'\Lib\site-packages')
import time
import os
import traceback
import ifcopenshell
import ifcopenshell.geom
from ifcopenshell.util.placement import get_local_placement
import numpy as np
import multiprocessing
import math
class IfcUtils():
def __init__(self, file_path):
self._file_path = file_path
self.ifc_file = ifcopenshell.open(self._file_path)
self.errors = []
def ToGroupVerices(self):
""" search Cluster by location """
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
settings.set(settings.APPLY_DEFAULT_MATERIALS, True)
# get unit length
global_unit_assignments = self.ifc_file.by_type("IfcUnitAssignment")
# The global context defines 0 or more unit sets, each containing IFC unit definitions (using list comprehension):
global_length_unit_definition = [u for ua in global_unit_assignments for u in ua.Units if u.is_a() in ('IfcSIUnit', 'IfcConversionBasedUnit') and u.UnitType=='LENGTHUNIT'][-1]
#
products = self.ifc_file.by_type('IfcProduct')
triangles = []
#
### Start Processing ####
print (("nbr_processor", multiprocessing.cpu_count()))
t1 = time.time()
iterator = ifcopenshell.geom.iterator(settings, self.ifc_file, multiprocessing.cpu_count())
print("elapsed_time_iterator", time.time() - t1)
counter = 0
dt_triangle = DataTable("ifc_data")
dt_triangle.Columns.Add("r", int)
dt_triangle.Columns.Add("g", int)
dt_triangle.Columns.Add("b", int)
dt_triangle.Columns.Add("ptaX", float)
dt_triangle.Columns.Add("ptaY", float)
dt_triangle.Columns.Add("ptaZ", float)
dt_triangle.Columns.Add("ptbX", float)
dt_triangle.Columns.Add("ptbY", float)
dt_triangle.Columns.Add("ptbZ", float)
dt_triangle.Columns.Add("ptcX", float)
dt_triangle.Columns.Add("ptcY", float)
dt_triangle.Columns.Add("ptcZ", float)
if iterator.initialize():
while True:
counter += 1
shape = iterator.get()
# METHOD TO GET MATRIX if you don't use USE_WORLD_COORDS
# https://github.com/IfcOpenShell/IfcOpenShell/issues/1440
matrix_data = np.array(shape.transformation.matrix.data).reshape((3,4)).T
element = self.ifc_file.by_guid(shape.guid)
if not element.is_a('IfcSite') and not element.is_a('IfcOpeningElement') :
faces = shape.geometry.faces # Indices of vertices per triangle face e.g. [f1v1, f1v2, f1v3, f2v1, f2v2, f2v3, ...]
verts = shape.geometry.verts # X Y Z of vertices in flattened list e.g. [v1x, v1y, v1z, v2x, v2y, v2z, ...]
materials = shape.geometry.materials # Material names and colour style information that are relevant to this shape
material_ids = shape.geometry.material_ids # Indices of material applied per triangle face e.g. [f1m, f2m, ...]
# Since the lists are flattened, you may prefer to group them per face like so depending on your geometry kernel
grouped_verts = [[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]
#print(grouped_verts)
grouped_faces = [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
# get color
rgb_color = [200,200,200]
if len(materials) == 1:
elementMaterial = materials[0]
if elementMaterial.hasDiffuse():
rgb_color = [math.floor(i * 255) for i in elementMaterial.diffuse]
elif elementMaterial.hasSpecular():
rgb_color = [math.floor(i * 255) for i in elementMaterial.specular]
else:pass
#
for idx_a, idx_b, idx_c in grouped_faces:
coorda = grouped_verts[idx_a]
coordb = grouped_verts[idx_b]
coordc = grouped_verts[idx_c]
dt_triangle.Rows.Add(*rgb_color, *coorda, *coordb, *coordc)
#
if not iterator.next():
break
return dt_triangle
file_path = IN[0]
#
objIfc = IfcUtils(file_path)
dt_triangle = objIfc.ToGroupVerices()
OUT = dt_triangle
- Traitement et Affichage des Géométries IFC.
import clr
import time
import System
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS
clr.AddReference('GeometryColor')
from Modifiers import GeometryColor
clr.AddReference('DSCoreNodes')
import DSCore
from DSCore import Color as DSColor
clr.AddReference('System.Data')
clr.AddReference('System.Data.DataSetExtensions')
from System.Data import DataSet,DataTable,DataRow
from System.Data.DataTableExtensions import *
clr.ImportExtensions(System.Data.DataTableExtensions)
from System.Threading.Tasks import *
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
import traceback
def to_Surface(data_row):
colorRGB, pta, ptb, ptc, plg = None, None, None, None, None
try:
ds_color = DSColor.ByARGB(255, data_row['r'], data_row['g'], data_row['b'])
pta = DS.Point.ByCoordinates(data_row['ptaX'], data_row['ptaY'], data_row['ptaZ'])
ptb = DS.Point.ByCoordinates(data_row['ptbX'], data_row['ptbY'], data_row['ptbZ'])
ptc = DS.Point.ByCoordinates(data_row['ptcX'], data_row['ptcY'], data_row['ptcZ'])
#
plg = DS.Polygon.ByPoints([pta, ptb, ptc])
surface = plg.Patch()
pta.Dispose()
ptb.Dispose()
ptc.Dispose()
plg.Dispose()
return GeometryColor.ByGeometryColor(surface, ds_color)
except Exception as ex:
for i in [pta, ptb, ptc, plg]:
if i is not None:
i.Dispose()
return None
dt_triangle = IN[0]
threadResult = dt_triangle.Rows.AsParallel()\
.WithDegreeOfParallelism(System.Environment.ProcessorCount)\
.Select(lambda l: to_Surface(l))\
.ToList()
OUT = threadResult
🛈 Malgré toutes ces optimisations, un viewer IFC restera tout de même toujours plus rapide.
Résultat en vidéo
alors CPython3 ou IronPython2 ? les deux, mon capitaine
À ce jour les 2 moteurs Python ont chacun des avantages, ils ne sont pas concurrents...
Hi Cyril, This is a great piece of art. Would it also be possible to import only geometry of a chosen IFC-category?
RépondreSupprimerCe commentaire a été supprimé par l'auteur.
SupprimerHi, replace this line
Supprimerif not element.is_a('IfcSite') and not element.is_a('IfcOpeningElement') :
by
if element.is_a() in ['IfcSlab', 'IfcDoor']: