├── .idea ├── .gitignore ├── vcs.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml ├── misc.xml └── ifcopenshell_examples.iml ├── 3D ├── images │ └── qt3d_minimal.png ├── README.md └── qt3d_minimal.py ├── GUI ├── images │ ├── ifc_treeviewer.png │ ├── ifc_schemaviewer.png │ ├── ifc_treeviewer_root.png │ ├── ifc_treeviewer2_empty.png │ ├── ifc_treeviewer2_prop.png │ ├── ifc_treeviewer_empty.png │ └── ifc_treeviewer2_complete.png ├── README.md ├── ifc_treeviewer.py ├── ifc_treeviewer2.py └── ifc_schemaviewer.py ├── Viewer ├── images │ ├── QIFCViewer_01.png │ ├── QIFCViewer_02.png │ ├── QIFCViewer_03.png │ ├── QIFCViewer_04.png │ ├── QIFCViewer_05.png │ └── QIFCViewer_06_win.png ├── README.md ├── QIFCViewer.py ├── IFCTreeWidget.py ├── IFCCustomDelegate.py ├── IFCListingWidget.py ├── IFCPropertyWidget.py └── IFCQt3DView.py ├── README.md ├── Console ├── README.md ├── minimal.py ├── geometry_minimal.py └── IFCOSPrintHierarchy.py ├── .gitignore └── LICENSE /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /3D/images/qt3d_minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/3D/images/qt3d_minimal.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer.png -------------------------------------------------------------------------------- /GUI/images/ifc_schemaviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_schemaviewer.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_01.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_02.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_03.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_04.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_05.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer_root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer_root.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer2_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer2_empty.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer2_prop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer2_prop.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer_empty.png -------------------------------------------------------------------------------- /Viewer/images/QIFCViewer_06_win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/Viewer/images/QIFCViewer_06_win.png -------------------------------------------------------------------------------- /GUI/images/ifc_treeviewer2_complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefkeB/ifcopenshell_examples/HEAD/GUI/images/ifc_treeviewer2_complete.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IfcOpenShell Examples 2 | 3 | Series of examples on how to use IfcOpenShell https://github.com/IfcOpenShell/IfcOpenShell as a general-purpose IFC library. 4 | 5 | Primary focus is on Python examples, with and without interface. 6 | See the Wiki for more in-depth explanation for the different examples. 7 | 8 | A second objective is to collect all examples into one, more comprehensive IFC-viewer. 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/ifcopenshell_examples.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /3D/README.md: -------------------------------------------------------------------------------- 1 | # 3D Geometry Examples 2 | 3 | These are basic examples of parsing the 3D geometry from an IFC file, using PyQt and IfcOpenShell. 4 | 5 | ## Qt3d_minimal.py 6 | 7 | This is a minimal example of a 3D widget, using the Qt3d-libraries from the QtSDK. 8 | 9 | It parses the IFC-file with the `geom` library from IfcOpenShell, which returns a polygonal representation for each object. This is then translated into `QEntity` items in a Qt3d scenegraph. It can be slow for certain objects (e.g., furniture objects with lots of vertices and different colors). 10 | 11 | ![result](images/qt3d_minimal.png) 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /Console/README.md: -------------------------------------------------------------------------------- 1 | # Console Examples 2 | 3 | ## Console Example (minimal.py) 4 | 5 | This is a very basic example of a minimal console IFC application, written in Python and using the IfcOpenShell library. 6 | 7 | It opens a file and returns the Spatial Hierachy, starting from the one and only `IfcProject` entity. 8 | 9 | ## Print Hierachy (IFCOSPrintHierachy.py) 10 | 11 | More advanced console example, which expands the minimal example with the full list of all properties, quantities and type. 12 | 13 | ## Geometry (geometry_minimal.py) 14 | 15 | A minimalistic example of parsing all geometric representations, using the iterator from the IfcOpenShell `geom` library, to get to the polygonal mesh representations. This uses the `OpenCascade` libraries to get to the individual vertices, edges, faces and normals. 16 | -------------------------------------------------------------------------------- /GUI/README.md: -------------------------------------------------------------------------------- 1 | # GUI Treeviewer Example 2 | 3 | These are basic examples of GUI IFC applications, written in Python and using the ifcopenshell library. The Graphical User Interface uses the Qt libraries, more in particular the PyQt5 library. As an alternative and with minimal code changes, it can also use the PySide2 libraries. 4 | 5 | 6 | ## ifc_treeviewer.py 7 | 8 | This is not a full viewer, but uses a basic `QTreeWidget` to display the Spatial hierarchy (decomposition and containment). 9 | 10 | ![result](images/ifc_treeviewer.png) 11 | 12 | 13 | ## ifc_treeviewer2.py 14 | 15 | This is a more advanced viewer, where a second tree is used to display type, properties, quantities and attributes of the object selected in the first tree. 16 | 17 | 18 | ![result](images/ifc_treeviewer2_complete.png) 19 | 20 | ## ifc_schemaviewer.py 21 | 22 | A Tree view of the IFC schema. You can switch between IFC2X3 and IFC4 and lower or increase the amount of columns to display. 23 | You can also toggle the display of the Entities, Types, Enumerations and Selects. 24 | 25 | ![result](images/ifc_schemaviewer.png) -------------------------------------------------------------------------------- /Console/minimal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import ifcopenshell 3 | 4 | 5 | # Our Print Hierarchy function (recursive) 6 | def print_hierarchy(entity, level): 7 | print("{0}{1} [{2}]".format('. ' * level, entity.Name, entity.is_a())) 8 | 9 | # using IfcRelAggregates to get spatial decomposition of spatial structure elements 10 | if entity.is_a('IfcObjectDefinition'): 11 | for rel in entity.IsDecomposedBy: 12 | related_objects = rel.RelatedObjects 13 | for item in related_objects: 14 | print_hierarchy(item, level + 1) 15 | 16 | # only spatial elements can contain building elements 17 | if entity.is_a('IfcSpatialStructureElement'): 18 | # using IfcRelContainedInSpatialElement to get contained elements 19 | for rel in entity.ContainsElements: 20 | contained_elements = rel.RelatedElements 21 | for element in contained_elements: 22 | print_hierarchy(element, level + 1) 23 | 24 | # Our Main function 25 | def main(): 26 | ifc_file = ifcopenshell.open(sys.argv[1]) 27 | items = ifc_file.by_type('IfcProject') 28 | print_hierarchy(items[0], 0) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() -------------------------------------------------------------------------------- /Console/geometry_minimal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import ifcopenshell 3 | import ifcopenshell.geom 4 | 5 | 6 | def parse_shape(shape): 7 | geometry = shape.geometry 8 | print("id(shape):", str(shape.id)) 9 | print("guid: ", str(shape.guid)) 10 | print("id(geom): ", str(geometry.id)) 11 | vertices = [geometry.verts[i: i + 3] for i in range(0, len(geometry.verts), 3)] 12 | print("vertices: ", vertices) 13 | edges = [geometry.edges[i: i + 2] for i in range(0, len(geometry.edges), 2)] 14 | print("edges: ", edges) 15 | faces = [geometry.faces[i: i + 3] for i in range(0, len(geometry.faces), 3)] 16 | print("faces: ", faces) 17 | normals = [geometry.normals[i: i + 3] for i in range(0, len(geometry.normals), 3)] 18 | print("normals: ", normals) 19 | mat_ids = [geometry.material_ids[i: i + 3] for i in range(0, len(geometry.material_ids), 3)] 20 | print("mat_ids: ", mat_ids) 21 | 22 | 23 | # iterating all geometric representations 24 | def geometry_iterator(ifc_file, settings): 25 | iterator = ifcopenshell.geom.iterator(settings, ifc_file) 26 | iterator.initialize() 27 | while True: 28 | shape = iterator.get() 29 | product = shape.product 30 | print("Product #" + str(product.id())) 31 | parse_shape(shape) 32 | if not iterator.next(): 33 | break 34 | 35 | 36 | # Our Main function 37 | def main(): 38 | ifc_file = ifcopenshell.open(sys.argv[1]) 39 | settings = ifcopenshell.geom.settings() 40 | settings.set(settings.WELD_VERTICES, False) # false = generate normals 41 | geometry_iterator(ifc_file, settings) 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /GUI/ifc_treeviewer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | try: 4 | from PyQt5.QtCore import * 5 | from PyQt5.QtGui import * 6 | from PyQt5.QtWidgets import * 7 | except Exception: 8 | from PySide2.QtGui import * 9 | from PySide2.QtCore import * 10 | from PySide2.QtWidgets import * 11 | import ifcopenshell 12 | 13 | 14 | class ViewTree(QWidget): 15 | def __init__(self): 16 | QWidget.__init__(self) 17 | # Prepare Tree Widgets in a stretchable layout 18 | vbox = QVBoxLayout() 19 | self.setLayout(vbox) 20 | # Object Tree 21 | self.object_tree = QTreeWidget() 22 | vbox.addWidget(self.object_tree) 23 | self.object_tree.setColumnCount(3) 24 | self.object_tree.setHeaderLabels(["Name", "Class", "ID"]) 25 | 26 | def load_file(self, filename): 27 | # Import the IFC File 28 | self.ifc_file = ifcopenshell.open(filename) 29 | root_item = QTreeWidgetItem( 30 | [self.ifc_file.wrapped_data.header.file_name.name, 'File', ""]) 31 | for item in self.ifc_file.by_type('IfcProject'): 32 | self.add_object_in_tree(item, root_item) 33 | # Finish the GUI 34 | self.object_tree.addTopLevelItem(root_item) 35 | self.object_tree.expandToDepth(3) 36 | 37 | def add_object_in_tree(self, ifc_object, parent_item): 38 | tree_item = QTreeWidgetItem([ifc_object.Name, ifc_object.is_a(), ifc_object.GlobalId]) 39 | parent_item.addChild(tree_item) 40 | if hasattr(ifc_object, 'ContainsElements'): 41 | for rel in ifc_object.ContainsElements: 42 | for element in rel.RelatedElements: 43 | self.add_object_in_tree(element, tree_item) 44 | if hasattr(ifc_object, 'IsDecomposedBy'): 45 | for rel in ifc_object.IsDecomposedBy: 46 | for related_object in rel.RelatedObjects: 47 | self.add_object_in_tree(related_object, tree_item) 48 | 49 | 50 | if __name__ == '__main__': 51 | app = 0 52 | if QApplication.instance(): 53 | app = QApplication.instance() 54 | else: 55 | app = QApplication(sys.argv) 56 | 57 | w = ViewTree() 58 | w.resize(600, 800) 59 | filename = sys.argv[1] 60 | if os.path.isfile(filename): 61 | w.load_file(filename) 62 | w.show() 63 | sys.exit(app.exec_()) 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .DS_Store 131 | .vscode/launch.json 132 | -------------------------------------------------------------------------------- /Viewer/README.md: -------------------------------------------------------------------------------- 1 | # Viewer for IFC Files 2 | 3 | ## QIFCViewer.py 4 | 5 | The main window of a basic IFC viewer. This collects all views, widgets and manages the loading of models in IFC-format. 6 | This class is responsible for file loading and linking the widgets together using the Qt Signals & Slots mechanism. 7 | 8 | You can also save and reload all files (e.g., after you edited some values). 9 | 10 | ![viewer](images/QIFCViewer_05.png) 11 | 12 | ## IFCQt3DView.py 13 | 14 | A 3D viewer widget, using the Qt3d-libraries from the QtSDK. 15 | It parses the IFC-file with the `geom` library from IfcOpenShell, which returns a polygonal representation for each object. This is then translated into `QEntity` items in a Qt3d scene-graph. It can be slow for certain objects (e.g., furniture objects with lots of vertices and different colors). 16 | In addition, due to performance reasons, also the `OCC` library (a Python wrapper for OpenCASCADE) is required. 17 | 18 | * IFC File Loading, geometry parsing & (very) basic navigation 19 | * Wireframe (edges) display, Origin and Axis 20 | * Object Picking + Selection syncing with other views 21 | * Basic Scene-Graph viewer (with Toggles to control visibility) 22 | 23 | ## IFCTreeWidget.py 24 | 25 | A widget to contain a Spatial Tree 26 | 27 | * Object Tree (with switchable model decomposition on/off) 28 | * Selection Syncing with other views 29 | * Editing object names 30 | * A dropdown to pick any of the classes in the file to use as the "root" of the tree 31 | 32 | ## IFCPropertyWidget.py 33 | 34 | A widget to display information about selected objects 35 | 36 | * Property Tree (attributes, inverse attributes, properties, quantities, type, associations, assignments) 37 | * File Header display (when selecting the top of the tree) 38 | * Configurable display (toggles + the "full" option to go really deep) 39 | * Editing of STRING, DOUBLE, INT and ENUMERATION values 40 | 41 | ## IFCListingWidget.py 42 | 43 | A Takeoff table, to display attributes, properties or quantities 44 | 45 | * Editable Dropdown to set the main class filter (can be abstract) 46 | * Default set of headers to get you started 47 | * Editor to adjust the headers and control the filter 48 | * Editing of STRING, DOUBLE, INT and ENUMERATION values for attributes and properties 49 | 50 | ## IFCCustomDelegate.py 51 | 52 | A series of convenience functions to support information extraction and editing 53 | 54 | * Extract properties, quantities and attributes by name 55 | * Make a summary text to display in tooltips 56 | * Make a "friendly" name from an IFC class, removing 'ifc' and splitting by capital letters (e.g. 'IfcRelAggregates' becomes 'Rel Aggregates') 57 | * Custom delegate class to edit individual attributes, using some hints when building up the trees 58 | 59 | # Example running on Windows 60 | 61 | ![viewer](images/QIFCViewer_06_win.png) -------------------------------------------------------------------------------- /Console/IFCOSPrintHierarchy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import ifcopenshell 3 | 4 | 5 | # Indent 6 | def indent(level): 7 | spacer = '. ' 8 | for i in range(level): 9 | print(spacer, end='') 10 | 11 | 12 | # PropertySet 13 | def print_element_properties(property_set, level): 14 | indent(level), print(property_set.Name) 15 | for prop in property_set.HasProperties: 16 | unit = str(prop.Unit) if hasattr(prop, 'Unit') else '' 17 | prop_value = '' 18 | if prop.is_a('IfcPropertySingleValue'): 19 | prop_value = str(prop.NominalValue.wrappedValue) 20 | indent(level + 1) 21 | print(str('{0} = {1} [{2}]').format(prop.Name, prop_value, unit)) 22 | 23 | 24 | # QuantitySet 25 | def print_element_quantities(quantity_set, level): 26 | indent(level), print(quantity_set.Name) 27 | # the individual quantities 28 | for quantity in quantity_set.Quantities: 29 | unit = str(quantity.Unit) if hasattr(quantity, 'Unit') else '' 30 | quantity_value = '' 31 | if quantity.is_a('IfcQuantityLength'): 32 | quantity_value = str(quantity.LengthValue) 33 | elif quantity.is_a('IfcQuantityArea'): 34 | quantity_value = str(quantity.AreaValue) 35 | elif quantity.is_a('IfcQuantityVolume'): 36 | quantity_value = str(quantity.VolumeValue) 37 | elif quantity.is_a('IfcQuantityCount'): 38 | quantity_value = str(quantity.CountValue) 39 | indent(level + 1) 40 | print(str('{0} = {1} [{2}]').format(quantity.Name, quantity_value, unit)) 41 | 42 | 43 | # Our Print Entity function (recursive) 44 | def print_entity(entity, level): 45 | indent(level), print('#' + str(entity.id()) + ' = ' + entity.is_a() 46 | + ' "' + str(entity.Name) + '" (' + entity.GlobalId + ')') 47 | if hasattr(entity, 'IsDefinedBy'): 48 | for definition in entity.IsDefinedBy: 49 | if definition.is_a('IfcRelDefinesByType'): 50 | print_entity(definition.RelatingType, level + 1) 51 | if definition.is_a('IfcRelDefinesByProperties'): 52 | related_data = definition.RelatingPropertyDefinition 53 | # the individual properties/quantities 54 | if related_data.is_a('IfcPropertySet'): 55 | print_element_properties(related_data, level + 1) 56 | elif related_data.is_a('IfcElementQuantity'): 57 | print_element_quantities(related_data, level + 1) 58 | 59 | # follow Containment relation 60 | if hasattr(entity, 'ContainsElements'): 61 | for rel in entity.ContainsElements: 62 | for child in rel.RelatedElements: 63 | print_entity(child, level + 1) 64 | 65 | # follow Aggregation/Decomposition Relation 66 | if hasattr(entity, 'IsDecomposedBy'): 67 | for rel in entity.IsDecomposedBy: 68 | for child in rel.RelatedObjects: 69 | print_entity(child, level + 1) 70 | 71 | # Our Main function 72 | def main(): 73 | ifc_file = ifcopenshell.open(sys.argv[1]) 74 | for item in ifc_file.by_type('IfcProject'): 75 | print_entity(item, 0) 76 | 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /GUI/ifc_treeviewer2.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | 4 | try: 5 | from PyQt5.QtCore import * 6 | from PyQt5.QtGui import * 7 | from PyQt5.QtWidgets import * 8 | except Exception: 9 | from PySide2.QtGui import * 10 | from PySide2.QtCore import * 11 | from PySide2.QtWidgets import * 12 | import ifcopenshell 13 | 14 | 15 | class ViewTree(QWidget): 16 | def __init__(self): 17 | QWidget.__init__(self) 18 | # Prepare Tree Widgets in a stretchable layout 19 | vbox = QVBoxLayout() 20 | self.setLayout(vbox) 21 | # Object Tree 22 | self.object_tree = QTreeWidget() 23 | vbox.addWidget(self.object_tree) 24 | self.object_tree.setColumnCount(3) 25 | self.object_tree.setHeaderLabels(["Name", "Class", "ID"]) 26 | self.object_tree.setSelectionMode(QAbstractItemView.ExtendedSelection) 27 | self.object_tree.selectionModel().selectionChanged.connect(self.add_data) 28 | # Property Tree 29 | self.property_tree = QTreeWidget() 30 | vbox.addWidget(self.property_tree) 31 | self.property_tree.setColumnCount(3) 32 | self.property_tree.setHeaderLabels(["Name", "Value", "ID/Type"]) 33 | 34 | def load_file(self, filename): 35 | # Import the IFC File 36 | self.ifc_file = ifcopenshell.open(filename) 37 | root_item = QTreeWidgetItem( 38 | [self.ifc_file.wrapped_data.header.file_name.name, 'File', ""]) 39 | for item in self.ifc_file.by_type('IfcProject'): 40 | self.add_object_in_tree(item, root_item) 41 | # Finish the GUI 42 | self.object_tree.addTopLevelItem(root_item) 43 | self.object_tree.expandToDepth(3) 44 | 45 | # Attributes 46 | def add_attributes_in_tree(self, ifc_object, parent_item): 47 | # the individual attributes 48 | for att_idx in range(0, len(ifc_object)): 49 | # https://github.com/jakob-beetz/IfcOpenShellScriptingTutorial/wiki/02:-Inspecting-IFC-instance-objects 50 | att_name = ifc_object.attribute_name(att_idx) 51 | att_value = str(ifc_object[att_idx]) 52 | att_type = ifc_object.attribute_type(att_name) 53 | attribute_item = QTreeWidgetItem([att_name, att_value, att_type]) 54 | parent_item.addChild(attribute_item) 55 | 56 | # PropertySet 57 | def add_properties_in_tree(self, property_set, parent_item): 58 | # the individual properties 59 | for prop in property_set.HasProperties: 60 | unit = str(prop.Unit) if hasattr(prop, 'Unit') else '' 61 | prop_value = '' 62 | if prop.is_a('IfcPropertySingleValue'): 63 | prop_value = str(prop.NominalValue.wrappedValue) 64 | parent_item.addChild(QTreeWidgetItem([prop.Name, prop_value, unit])) 65 | 66 | # QuantitySet 67 | def add_quantities_in_tree(self, quantity_set, parent_item): 68 | # the individual quantities 69 | for quantity in quantity_set.Quantities: 70 | unit = str(quantity.Unit) if hasattr(quantity, 'Unit') else '' 71 | quantity_value = '' 72 | if quantity.is_a('IfcQuantityLength'): 73 | quantity_value = str(quantity.LengthValue) 74 | elif quantity.is_a('IfcQuantityArea'): 75 | quantity_value = str(quantity.AreaValue) 76 | elif quantity.is_a('IfcQuantityVolume'): 77 | quantity_value = str(quantity.VolumeValue) 78 | elif quantity.is_a('IfcQuantityCount'): 79 | quantity_value = str(quantity.CountValue) 80 | parent_item.addChild(QTreeWidgetItem([quantity.Name, quantity_value, unit])) 81 | 82 | def add_data(self): 83 | self.property_tree.clear() 84 | items = self.object_tree.selectedItems() 85 | for item in items: 86 | # the GUID is in the third column 87 | buffer = item.text(2) 88 | if not buffer: 89 | break 90 | # find the related object in our IFC file 91 | ifc_object = self.ifc_file.by_guid(buffer) 92 | if ifc_object is None: 93 | break 94 | 95 | # Attributes 96 | attributes_item = QTreeWidgetItem(["Attributes", "", ifc_object.GlobalId]) 97 | self.property_tree.addTopLevelItem(attributes_item) 98 | self.add_attributes_in_tree(ifc_object, attributes_item) 99 | 100 | # Properties & Quantities 101 | if hasattr(ifc_object, 'IsDefinedBy'): 102 | for definition in ifc_object.IsDefinedBy: 103 | if definition.is_a('IfcRelDefinesByType'): 104 | type_object = definition.RelatingType 105 | type_item = QTreeWidgetItem([type_object.Name, 106 | type_object.is_a(), 107 | type_object.GlobalId]) 108 | self.property_tree.addTopLevelItem(type_item) 109 | if definition.is_a('IfcRelDefinesByProperties'): 110 | property_set = definition.RelatingPropertyDefinition 111 | # the individual properties/quantities 112 | if property_set.is_a('IfcPropertySet'): 113 | properties_item = QTreeWidgetItem([property_set.Name, 114 | property_set.is_a(), 115 | property_set.GlobalId]) 116 | self.property_tree.addTopLevelItem(properties_item) 117 | self.add_properties_in_tree(property_set, properties_item) 118 | elif property_set.is_a('IfcElementQuantity'): 119 | quantities_item = QTreeWidgetItem([property_set.Name, 120 | property_set.is_a(), 121 | property_set.GlobalId]) 122 | self.property_tree.addTopLevelItem(quantities_item) 123 | self.add_quantities_in_tree(property_set, quantities_item) 124 | 125 | self.property_tree.expandAll() 126 | 127 | def add_object_in_tree(self, ifc_object, parent_item): 128 | tree_item = QTreeWidgetItem([ifc_object.Name, ifc_object.is_a(), ifc_object.GlobalId]) 129 | parent_item.addChild(tree_item) 130 | if hasattr(ifc_object, 'ContainsElements'): 131 | for rel in ifc_object.ContainsElements: 132 | for element in rel.RelatedElements: 133 | self.add_object_in_tree(element, tree_item) 134 | if hasattr(ifc_object, 'IsDecomposedBy'): 135 | for rel in ifc_object.IsDecomposedBy: 136 | for related_object in rel.RelatedObjects: 137 | self.add_object_in_tree(related_object, tree_item) 138 | 139 | 140 | if __name__ == '__main__': 141 | app = 0 142 | if QApplication.instance(): 143 | app = QApplication.instance() 144 | else: 145 | app = QApplication(sys.argv) 146 | 147 | w = ViewTree() 148 | w.resize(600, 800) 149 | filename = sys.argv[1] 150 | if os.path.isfile(filename): 151 | w.load_file(filename) 152 | w.show() 153 | sys.exit(app.exec_()) 154 | -------------------------------------------------------------------------------- /GUI/ifc_schemaviewer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtWidgets import * 3 | import ifcopenshell 4 | 5 | 6 | class SchemaViewer(QWidget): 7 | """ 8 | Tree View to show the inheritance tree of IFC classes 9 | from the schema (choice between IFC2X3 and IFC4). 10 | - V1 = Single Tree (object tree with basic spatial hierarchy) 11 | - V2 = Add Types and Enumerations 12 | """ 13 | 14 | def __init__(self): 15 | QWidget.__init__(self) 16 | # Prepare Tree Widgets in a stretchable layout 17 | vbox = QVBoxLayout() 18 | self.setLayout(vbox) 19 | 20 | hbox = QHBoxLayout() 21 | vbox.addLayout(hbox) 22 | 23 | # Schema Chooser 24 | self.current_schema = 'IFC2X3' 25 | self.schema_chooser = QComboBox() 26 | self.schema_chooser.addItems(['IFC2X3', 'IFC4', 'IFC4x1','IFC4x2','IFC4x3_rc1','IFC4x3_rc2','IFC4x3_rc3','IFC4x3_rc4']) 27 | self.schema_chooser.activated[str].connect(self.set_schema) 28 | hbox.addWidget(self.schema_chooser) 29 | 30 | # Column Count Chooser 31 | self.columns = 2 32 | self.column_chooser = QSpinBox() 33 | self.column_chooser.setValue(self.columns) 34 | self.column_chooser.valueChanged.connect(self.set_columns) 35 | hbox.addWidget(self.column_chooser) 36 | 37 | # option show entities 38 | self.show_entities = True 39 | self.check_ent = QCheckBox("Entities") 40 | self.check_ent.setToolTip("Display all Entities") 41 | self.check_ent.setChecked(self.show_entities) 42 | self.check_ent.toggled.connect(self.toggle_show_entities) 43 | hbox.addWidget(self.check_ent) 44 | # option show types 45 | self.show_types = False 46 | self.check_types = QCheckBox("Types") 47 | self.check_types.setToolTip("Display all Types") 48 | self.check_types.setChecked(self.show_types) 49 | self.check_types.toggled.connect(self.toggle_show_types) 50 | hbox.addWidget(self.check_types) 51 | # option show enumerations 52 | self.show_enums = False 53 | self.check_enums = QCheckBox("Enumerations") 54 | self.check_enums.setToolTip("Display all Enumerations") 55 | self.check_enums.setChecked(self.show_enums) 56 | self.check_enums.toggled.connect(self.toggle_show_enums) 57 | hbox.addWidget(self.check_enums) 58 | # option show selects 59 | self.show_selects = False 60 | self.check_selects = QCheckBox("Selects") 61 | self.check_selects.setToolTip("Display all Select Types") 62 | self.check_selects.setChecked(self.show_selects) 63 | self.check_selects.toggled.connect(self.toggle_show_selects) 64 | hbox.addWidget(self.check_selects) 65 | 66 | # Stretchable Spacer 67 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 68 | hbox.addSpacerItem(spacer) 69 | 70 | # Object Tree 71 | self.object_tree = QTreeWidget() 72 | vbox.addWidget(self.object_tree) 73 | 74 | self.reload_schema() 75 | 76 | def set_columns(self, columns): 77 | self.columns = columns 78 | self.reload_schema() 79 | 80 | def set_schema(self, ifc_schema): 81 | self.current_schema = ifc_schema 82 | self.reload_schema() 83 | 84 | def toggle_show_entities(self): 85 | self.show_entities = not self.show_entities 86 | self.reload_schema() 87 | 88 | def toggle_show_types(self): 89 | self.show_types = not self.show_types 90 | self.reload_schema() 91 | 92 | def toggle_show_selects(self): 93 | self.show_selects = not self.show_selects 94 | self.reload_schema() 95 | 96 | def toggle_show_enums(self): 97 | self.show_enums = not self.show_enums 98 | self.reload_schema() 99 | 100 | def reload_schema(self): 101 | self.object_tree.setColumnCount(self.columns) 102 | labels = ['IFC Entity (class)'] 103 | for s in range(self.columns): 104 | labels.append(str(s + 1)) 105 | self.object_tree.setHeaderLabels(labels) 106 | 107 | self.object_tree.clear() 108 | # root_item = QTreeWidgetItem(["SCHEMA", ifc_schema]) 109 | # self.object_tree.addTopLevelItem(root_item) 110 | schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.current_schema) 111 | # for e in schema.entities(): 112 | # self.add_class_in_tree(e) 113 | 114 | for d in schema.declarations(): 115 | self.add_declaration_in_tree(d) 116 | 117 | self.object_tree.expandToDepth(0) 118 | # self.object_tree.expandAll() 119 | 120 | def add_declaration_in_tree(self, declaration, parent_item=None, level=0): 121 | d = declaration 122 | d_index = d.index_in_schema() 123 | d_name = d.name() 124 | 125 | if d.as_entity() is not None: 126 | e = d.as_entity() 127 | if self.show_entities: 128 | self.add_class_in_tree(e) 129 | 130 | if d.as_type_declaration() is not None: 131 | t = d.as_type_declaration() 132 | if self.show_types: # or self.show_selects and level > 0: 133 | t_val = str(t) 134 | t_tip = '' 135 | if t.declared_type().as_simple_type() is not None: 136 | t_val = str(t.declared_type().as_simple_type()) 137 | t_tip = 'type_declaration:simple_type' 138 | if t.declared_type().as_named_type() is not None: 139 | t_val = str(t.declared_type().as_named_type()) 140 | t_tip = 'type_declaration:name_type' 141 | if t.declared_type().as_aggregation_type() is not None: 142 | t_val = str(t.declared_type().as_aggregation_type()) 143 | t_tip = 'type_declaration:aggregation_type' 144 | item = QTreeWidgetItem([d_name, t_val, t_tip]) 145 | item.setToolTip(0, t_tip) 146 | if parent_item is not None: 147 | parent_item.addChild(item) 148 | else: 149 | self.object_tree.addTopLevelItem(item) 150 | 151 | if d.as_enumeration_type() is not None: 152 | e = d.as_enumeration_type() 153 | if self.show_enums: 154 | buffer = [d_name] 155 | for enum in e.enumeration_items(): 156 | buffer.append(enum) 157 | item = QTreeWidgetItem(buffer) 158 | item.setToolTip(0, 'enumeration_type:\n' + ', '.join(buffer[1:])) 159 | if parent_item is not None: 160 | parent_item.addChild(item) 161 | else: 162 | self.object_tree.addTopLevelItem(item) 163 | 164 | if d.as_select_type() is not None: 165 | s = d.as_select_type() 166 | if self.show_selects or level > 0: 167 | item = QTreeWidgetItem([d_name]) 168 | item.setToolTip(0, 'select_type') 169 | if parent_item is not None: 170 | parent_item.addChild(item) 171 | else: 172 | self.object_tree.addTopLevelItem(item) 173 | for sub in s.select_list(): 174 | self.add_declaration_in_tree(sub, item, level+1) 175 | 176 | def add_class_in_tree(self, entity, parent_item=None, level=0): 177 | """ 178 | Recursive function to fill the tree with entity declarations 179 | 180 | :param entity: IFC Entity Class 181 | :param parent_item: item to which our item is attached 182 | :type parent_item: QTreeWidgetItem 183 | :param: level: depth of the tree at this point 184 | :param: level: int 185 | :return: 186 | """ 187 | # At level 0, we only load non-rooted classes 188 | # At other levels, we dive deeper, recursively 189 | if level > 0 or (level == 0 and entity.supertype() is None): 190 | buffer = [str(entity.name())] 191 | attributes = entity.all_attributes() 192 | c = entity.attribute_count() 193 | for a in range(entity.attribute_count()): 194 | attribute = entity.attribute_by_index(a) 195 | a_name = attribute.name() if hasattr(attribute, 'name') else "" 196 | # a_type = attribute.type_of_attribute().declared_type().name() if hasattr(attribute.type_of_attribute(), 'declared_type') else "" 197 | # a_optional = str(attribute.optional()) if hasattr(attribute, 'optional') else "" 198 | buffer.append(a_name) 199 | 200 | item = QTreeWidgetItem(buffer) 201 | if parent_item is not None: 202 | parent_item.addChild(item) 203 | else: 204 | self.object_tree.addTopLevelItem(item) 205 | 206 | # sub types 207 | for s in entity.subtypes(): 208 | self.add_class_in_tree(s, item, level+1) 209 | 210 | 211 | if __name__ == '__main__': 212 | app = 0 213 | if QApplication.instance(): 214 | app = QApplication.instance() 215 | else: 216 | app = QApplication(sys.argv) 217 | 218 | w = SchemaViewer() 219 | w.resize(600, 800) 220 | w.show() 221 | sys.exit(app.exec_()) 222 | -------------------------------------------------------------------------------- /3D/qt3d_minimal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import os.path 4 | import struct 5 | import multiprocessing 6 | 7 | try: 8 | from PyQt5.QtCore import * 9 | from PyQt5.QtGui import * 10 | from PyQt5.QtWidgets import * 11 | from PyQt5.Qt3DCore import * 12 | from PyQt5.Qt3DExtras import * 13 | from PyQt5.Qt3DRender import * 14 | except Exception: 15 | from PySide2.QtGui import * 16 | from PySide2.QtCore import * 17 | from PySide2.QtWidgets import * 18 | from PySide2.Qt3DCore import * 19 | from PySide2.Qt3DExtras import * 20 | from PySide2.Qt3DRender import * 21 | 22 | import ifcopenshell 23 | import ifcopenshell.geom 24 | 25 | 26 | class View3D(QWidget): 27 | """ 28 | 3D View Widget 29 | - V1 = IFC File Loading, geometry parsing & basic navigation 30 | """ 31 | def __init__(self): 32 | QWidget.__init__(self) 33 | 34 | # variables 35 | self.ifc_file = None 36 | self.start = time.time() 37 | 38 | # 3D View 39 | self.view = Qt3DWindow() 40 | self.view.defaultFrameGraph().setClearColor(QColor("#4466ff")) 41 | self.container = self.createWindowContainer(self.view) 42 | self.container.setMinimumSize(QSize(200, 100)) 43 | self.container.setFocusPolicy(Qt.NoFocus) 44 | 45 | # Prepare our scene 46 | self.root = QEntity() 47 | self.material = QPerVertexColorMaterial() 48 | self.root.addComponent(self.material) 49 | self.material.setShareable(True) 50 | self.initialise_camera() 51 | self.view.setRootEntity(self.root) 52 | 53 | # Finish GUI 54 | layout = QHBoxLayout() 55 | layout.addWidget(self.container) 56 | self.setLayout(layout) 57 | 58 | def initialise_camera(self): 59 | # camera 60 | camera = self.view.camera() 61 | camera.lens().setPerspectiveProjection(45.0, 16.0 / 9.0, 0.1, 1000) 62 | camera.setPosition(QVector3D(0, 0, 40)) 63 | camera.setViewCenter(QVector3D(0, 0, 0)) 64 | 65 | # for camera control 66 | cam_controller = QOrbitCameraController(self.root) 67 | cam_controller.setLinearSpeed(50.0) 68 | cam_controller.setLookSpeed(180.0) 69 | cam_controller.setCamera(camera) 70 | 71 | def load_file(self, filename): 72 | if self.ifc_file is None: 73 | print("Importing IFC file ...") 74 | start = time.time() 75 | self.ifc_file = ifcopenshell.open(filename) 76 | print("Loaded in ", time.time() - start) 77 | 78 | print("Importing IFC geometrical information ...") 79 | self.start = time.time() 80 | settings = ifcopenshell.geom.settings() 81 | settings.set(settings.WELD_VERTICES, False) # false is needed to generate normals -- slower! 82 | settings.set(settings.USE_WORLD_COORDS, True) # true = ignore transformation 83 | 84 | # Two methods 85 | # self.parse_project(settings) # SLOWER - create geometry for each product 86 | self.parse_geometry(settings) # FASTER - iteration with parallel processing 87 | print("\nFinished in ", time.time() - self.start) 88 | 89 | def parse_geometry(self, settings): 90 | iterator = ifcopenshell.geom.iterator(settings, self.ifc_file, multiprocessing.cpu_count()) 91 | iterator.initialize() 92 | counter = 0 93 | while True: 94 | shape = iterator.get() 95 | # skip openings and spaces geometry 96 | if not shape.product.is_a('IfcOpeningElement') and not shape.product.is_a('IfcSpace'): 97 | try: 98 | self.generate_rendermesh(shape) 99 | print(str("Shape {0}\t[#{1}]\tin {2} seconds") 100 | .format(str(counter), str(shape.id), time.time() - self.start)) 101 | except Exception as e: 102 | print(str("Shape {0}\t[#{1}]\tERROR - {2} : {3}") 103 | .format(str(counter), str(shape.id), shape.product.is_a(), e)) 104 | pass 105 | counter += 1 106 | if not iterator.next(): 107 | break 108 | 109 | def parse_project(self, settings): 110 | # parse all products 111 | products = self.ifc_file.by_type('IfcProduct') 112 | counter = 0 113 | for product in products: 114 | if not product.is_a('IfcOpeningElement') and not product.is_a('IfcSpace'): 115 | if product.Representation: 116 | shape = ifcopenshell.geom.create_shape(settings, product) 117 | self.generate_rendermesh(shape) 118 | print(str("Product {0}\t[#{1}]\tin {2} seconds") 119 | .format(str(counter), str(product.id()), time.time() - self.start)) 120 | counter += 1 121 | 122 | def generate_rendermesh(self, shape): 123 | geometry = shape.geometry 124 | 125 | # buffer example https://stackoverflow.com/questions/49049828/numpy-array-via-qbuffer-to-qgeometry 126 | custom_mesh_renderer = QGeometryRenderer() 127 | custom_mesh_renderer.setPrimitiveType(QGeometryRenderer.Triangles) 128 | custom_geometry = QGeometry(custom_mesh_renderer) 129 | 130 | # Position Attribute 131 | position_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 132 | # position_data_buffer.setData(QByteArray(np.array(geometry.verts).astype(np.float32).tobytes())) 133 | position_data_buffer.setData(struct.pack('%sf' % len(geometry.verts), *geometry.verts)) 134 | position_attribute = QAttribute() 135 | position_attribute.setAttributeType(QAttribute.VertexAttribute) 136 | position_attribute.setBuffer(position_data_buffer) 137 | position_attribute.setVertexBaseType(QAttribute.Float) 138 | position_attribute.setVertexSize(3) # 3 floats 139 | position_attribute.setByteOffset(0) # start from first index 140 | position_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 141 | position_attribute.setCount(len(geometry.verts)) # vertices 142 | position_attribute.setName(QAttribute.defaultPositionAttributeName()) 143 | custom_geometry.addAttribute(position_attribute) 144 | 145 | # Normal Attribute 146 | if len(geometry.normals) > 0: 147 | normals_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 148 | # normals_data_buffer.setData(QByteArray(np.array(geometry.normals).astype(np.float32).tobytes())) 149 | normals_data_buffer.setData(struct.pack('%sf' % len(geometry.normals), *geometry.normals)) 150 | normal_attribute = QAttribute() 151 | normal_attribute.setAttributeType(QAttribute.VertexAttribute) 152 | normal_attribute.setBuffer(normals_data_buffer) 153 | normal_attribute.setVertexBaseType(QAttribute.Float) 154 | normal_attribute.setVertexSize(3) # 3 floats 155 | normal_attribute.setByteOffset(0) # start from first index 156 | normal_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 157 | normal_attribute.setCount(len(geometry.normals)) # vertices 158 | normal_attribute.setName(QAttribute.defaultNormalAttributeName()) 159 | custom_geometry.addAttribute(normal_attribute) 160 | 161 | # Collect the colors via the materials (1 color per vertex) 162 | color_list = [0.5] * len(geometry.verts) 163 | for material_index in range(0, len(geometry.material_ids)): 164 | # default color without material 165 | red = 0.5 166 | green = 1.0 167 | blue = 0.5 168 | # From material index we get the material reference ID 169 | mat_id = geometry.material_ids[material_index] 170 | # Beware... this id can be -1 - so use a default color instead 171 | if mat_id > -1: 172 | material = geometry.materials[mat_id] 173 | red = material.diffuse[0] 174 | green = material.diffuse[1] 175 | blue = material.diffuse[2] 176 | # get the 3 related vertices for this face (three indices in vertex array) 177 | for i in range(3): 178 | vertex = geometry.faces[material_index * 3 + i] 179 | color_list[vertex * 3] = red 180 | color_list[vertex * 3 + 1] = green 181 | color_list[vertex * 3 + 2] = blue 182 | 183 | # Color Attribute 184 | color_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 185 | # color_data_buffer.setData(QByteArray(np.array(color_list).astype(np.float32).tobytes())) 186 | color_data_buffer.setData(struct.pack('%sf' % len(color_list), *color_list)) 187 | color_attribute = QAttribute() 188 | color_attribute.setAttributeType(QAttribute.VertexAttribute) 189 | color_attribute.setBuffer(color_data_buffer) 190 | color_attribute.setVertexBaseType(QAttribute.Float) 191 | color_attribute.setVertexSize(3) # 3 floats 192 | color_attribute.setByteOffset(0) # start from first index 193 | color_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 194 | color_attribute.setCount(len(color_list)) # colors (per vertex) 195 | color_attribute.setName(QAttribute.defaultColorAttributeName()) 196 | custom_geometry.addAttribute(color_attribute) 197 | 198 | # Faces Index Attribute 199 | index_data_buffer = QBuffer(QBuffer.IndexBuffer, custom_geometry) 200 | # index_data_buffer.setData(QByteArray(np.array(geometry.faces).astype(np.uintc).tobytes())) 201 | index_data_buffer.setData(struct.pack("{}I".format(len(geometry.faces)), *geometry.faces)) 202 | index_attribute = QAttribute() 203 | index_attribute.setVertexBaseType(QAttribute.UnsignedInt) 204 | index_attribute.setAttributeType(QAttribute.IndexAttribute) 205 | index_attribute.setBuffer(index_data_buffer) 206 | index_attribute.setCount(len(geometry.faces)) 207 | custom_geometry.addAttribute(index_attribute) 208 | 209 | # ---------------------------------------------------------------------------- 210 | # make the geometry visible with a renderer 211 | custom_mesh_renderer.setGeometry(custom_geometry) 212 | custom_mesh_renderer.setInstanceCount(1) 213 | custom_mesh_renderer.setFirstVertex(0) 214 | custom_mesh_renderer.setFirstInstance(0) 215 | 216 | # add everything to the scene 217 | custom_mesh_entity = QEntity(self.root) 218 | custom_mesh_entity.addComponent(custom_mesh_renderer) 219 | transform = QTransform() 220 | transform.setRotationX(-90) 221 | custom_mesh_entity.addComponent(transform) 222 | custom_mesh_entity.addComponent(self.material) 223 | 224 | 225 | # Our Main function 226 | def main(): 227 | app = 0 228 | if QApplication.instance(): 229 | app = QApplication.instance() 230 | else: 231 | app = QApplication(sys.argv) 232 | 233 | w = View3D() 234 | w.resize(1280, 800) 235 | filename = sys.argv[1] 236 | if os.path.isfile(filename): 237 | w.load_file(filename) 238 | w.setWindowTitle("IFC Viewer - " + filename) 239 | w.show() 240 | sys.exit(app.exec_()) 241 | 242 | 243 | if __name__ == "__main__": 244 | main() 245 | -------------------------------------------------------------------------------- /Viewer/QIFCViewer.py: -------------------------------------------------------------------------------- 1 | # import time 2 | import os.path 3 | from IFCCustomDelegate import * 4 | from IFCQt3DView import * 5 | from IFCTreeWidget import * 6 | from IFCPropertyWidget import * 7 | from IFCListingWidget import * 8 | 9 | 10 | class QIFCViewer(QMainWindow): 11 | """ 12 | IFC Model Viewer 13 | - V1 = Loading the IFCTreeWidget and IFCQt3dView together (as-is) 14 | - V2 = Open + Save methods, Toolbar and Status Bar & Files in a Dictionary 15 | - V3 = Use separate Tree Views as two separate Dock Widgets and link them 16 | - V4 = Syncing updates & edits of values between Object and Property Tree 17 | - V5 = Supporting drag and drop of IFC files onto the app 18 | - V6 = Make the 3D view optional (switch) 19 | """ 20 | def __init__(self): 21 | QMainWindow.__init__(self) 22 | 23 | # A dictionary referring to our files, based on name 24 | self.ifc_files = {} 25 | self.setAcceptDrops(True) 26 | self.settings = QSettings() 27 | 28 | self.USE_3D = self.settings.value('USE_3D', True) 29 | 30 | # menu, actions and toolbar 31 | self.setUnifiedTitleAndToolBarOnMac(True) 32 | toolbar = QToolBar("My main toolbar") 33 | toolbar.setFloatable(False) 34 | toolbar.setMovable(False) 35 | self.addToolBar(toolbar) 36 | menu_bar = self.menuBar() 37 | file_menu = QMenu("&File", self) 38 | menu_bar.addMenu(file_menu) 39 | 40 | action_open = QAction("Open...", self) 41 | action_open.setShortcut("CTRL+O") 42 | action_open.setStatusTip("Open an IFC Model") 43 | action_open.triggered.connect(self.get_file) 44 | toolbar.addAction(action_open) 45 | file_menu.addAction(action_open) 46 | 47 | action_save = QAction("Save All", self) 48 | action_save.setShortcut("CTRL+S") 49 | action_save.setStatusTip("Confirm saving of all IFC Models") 50 | action_save.triggered.connect(self.save_files) 51 | toolbar.addAction(action_save) 52 | file_menu.addAction(action_save) 53 | 54 | action_reload = QAction("Reload All", self) 55 | action_reload.setShortcut("CTRL+R") 56 | action_reload.setStatusTip("Reload all IFC Models") 57 | action_reload.triggered.connect(self.reload_files) 58 | toolbar.addAction(action_reload) 59 | file_menu.addAction(action_reload) 60 | 61 | action_close = QAction("Close All", self) 62 | action_close.setShortcut("CTRL+W") 63 | action_close.setStatusTip("Close all IFC Models") 64 | action_close.triggered.connect(self.close_files) 65 | toolbar.addAction(action_close) 66 | file_menu.addAction(action_close) 67 | 68 | action_quit = QAction("Quit", self) 69 | action_quit.setShortcut("CTRL+Q") 70 | action_quit.setStatusTip("Quit the application") 71 | action_quit.triggered.connect(qApp.quit) 72 | file_menu.addAction(action_quit) 73 | 74 | # Option : USE 3D 75 | self.check_3d = QCheckBox("Use 3D") 76 | self.check_3d.setToolTip("Toggle the 3D Window (restart required!)") 77 | self.check_3d.setChecked(self.USE_3D) 78 | self.check_3d.toggled.connect(self.toggle_use_3d) 79 | toolbar.addWidget(self.check_3d) 80 | 81 | self.setStatusBar(QStatusBar(self)) 82 | 83 | # Main Widgets 84 | if self.USE_3D: 85 | self.view_3d = IFCQt3dView() 86 | self.view_tree = IFCTreeWidget() 87 | self.view_properties = IFCPropertyWidget() 88 | self.view_takeoff = IFCListingWidget() 89 | 90 | # Selection Syncing 91 | if self.USE_3D: 92 | self.view_tree.select_object.connect(self.view_3d.select_object_by_id) 93 | self.view_tree.deselect_object.connect(self.view_3d.deselect_object_by_id) 94 | self.view_3d.add_to_selected_entities.connect(self.view_tree.receive_selection) 95 | self.view_3d.add_to_selected_entities.connect(self.view_takeoff.receive_selection) 96 | self.view_takeoff.select_object.connect(self.view_3d.select_object_by_id) 97 | # self.view_takeoff.deselect_object.connect(self.view_3d.deselect_object_by_id) 98 | # from tree to other views 99 | self.view_tree.send_selection_set.connect(self.view_properties.set_from_selected_items) 100 | self.view_tree.select_object.connect(self.view_takeoff.receive_selection) 101 | # send from takeoff to other views 102 | self.view_takeoff.select_object.connect(self.view_tree.receive_selection) 103 | # self.view_takeoff.deselect_object.connect(self.view_tree.receive_selection) 104 | 105 | # Update Syncing 106 | self.view_properties.send_update_object.connect(self.view_tree.receive_object_update) 107 | 108 | # Docking Widgets 109 | if self.USE_3D is True: 110 | self.dock = QDockWidget('Model Tree', self) 111 | self.dock.setWidget(self.view_tree) 112 | self.dock.setFloating(False) 113 | self.addDockWidget(Qt.LeftDockWidgetArea, self.dock) 114 | 115 | self.dock2 = QDockWidget('Property Tree', self) 116 | self.dock2.setWidget(self.view_properties) 117 | self.dock2.setFloating(False) 118 | self.addDockWidget(Qt.LeftDockWidgetArea, self.dock2) 119 | 120 | self.dock3 = QDockWidget('Take off', self) 121 | self.dock3.setWidget(self.view_takeoff) 122 | self.dock3.setFloating(False) 123 | self.addDockWidget(Qt.BottomDockWidgetArea, self.dock3) 124 | 125 | # Main Widget = 3D View 126 | if self.USE_3D: 127 | self.setCentralWidget(self.view_3d) 128 | else: 129 | self.setCentralWidget(self.view_tree) 130 | 131 | # region File Methods 132 | 133 | def load_file(self, filename): 134 | """ 135 | Load an IFC file from the given path. If the file was already loaded, 136 | the user is asked if the file should be replaced. 137 | 138 | :param filename: full path to the IFC file 139 | :return: True if the file was loaded, False if cancelled. 140 | """ 141 | if filename in self.ifc_files: 142 | # Display warning that this model was already loaded. Replace or Cancel. 143 | dlg = QMessageBox(self.parent()) 144 | dlg.setWindowTitle("Model already loaded!") 145 | dlg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) 146 | dlg.setIcon(QMessageBox.Warning) 147 | dlg.setText(str("Do you want to replace the currently loaded model?\n" 148 | "{}").format(filename)) 149 | button = dlg.exec_() 150 | if button == QMessageBox.Cancel: 151 | return False 152 | 153 | start = time.time() 154 | ifc_file = ifcopenshell.open(filename) 155 | print("Loaded ", filename, " in ", time.time() - start, " seconds") 156 | self.ifc_files[filename] = ifc_file 157 | 158 | # print("Loading Views ...") 159 | start = time.time() 160 | self.view_tree.ifc_files[filename] = ifc_file 161 | self.view_tree.load_file(filename) 162 | 163 | if self.USE_3D: 164 | self.view_3d.ifc_files[filename] = ifc_file 165 | self.view_3d.load_file(filename) 166 | 167 | self.view_takeoff.ifc_files[filename] = ifc_file 168 | print("Loaded all views in ", time.time() - start) 169 | return True 170 | 171 | def dragEnterEvent(self, event): 172 | data = event.mimeData() 173 | urls = data.urls() 174 | if urls and urls[0].scheme() == 'file': 175 | event.acceptProposedAction() 176 | 177 | def dragMoveEvent(self, event): 178 | data = event.mimeData() 179 | urls = data.urls() 180 | if urls and urls[0].scheme() == 'file': 181 | event.acceptProposedAction() 182 | 183 | def dropEvent(self, event): 184 | data = event.mimeData() 185 | urls = data.urls() 186 | if urls and urls[0].scheme() == 'file': 187 | for f in urls: 188 | filepath = str(f.path()) 189 | # only .ifc files are acceptable 190 | if os.path.isfile(filepath) and os.path.splitext(filepath)[1] == '.ifc': 191 | self.load_file(filepath) 192 | else: 193 | dialog = QMessageBox() 194 | dialog.setWindowTitle("Error: Invalid File") 195 | dialog.setText(str("Only .ifc files are accepted.\nYou dragged {}").format(filepath)) 196 | dialog.setIcon(QMessageBox.Warning) 197 | dialog.exec_() 198 | 199 | def reload_files(self): 200 | """ 201 | Reload all currently open files 202 | """ 203 | for ifc_filename, ifc_file in self.ifc_files.items(): 204 | self.load_file(ifc_filename) 205 | 206 | def save_files(self): 207 | """ 208 | Save all currently loaded files 209 | """ 210 | for ifc_filename, ifc_file in self.ifc_files.items(): 211 | 212 | dlg = QMessageBox(self.parent()) 213 | dlg.setWindowTitle("Confirm file save") 214 | dlg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) 215 | dlg.setText(str("Do you want to save:\n{}?").format(ifc_filename)) 216 | dlg.setDetailedText(str("When you click 'OK', you still have the chance of" 217 | "selecting a new name or location.")) 218 | button = dlg.exec_() 219 | if button == QMessageBox.Ok: 220 | savepath = QFileDialog.getSaveFileName(self, caption="Save IFC File", 221 | directory=ifc_filename, 222 | filter="IFC files (*.ifc)") 223 | if savepath[0] != '': 224 | ifc_file.write(savepath[0]) 225 | 226 | def get_file(self): 227 | """ 228 | Get a File Open dialog to select IFC files to load in the project 229 | """ 230 | filenames, filter_string = QFileDialog.getOpenFileNames(self, caption="Open IFC File", 231 | filter="IFC files (*.ifc)") 232 | 233 | # self.setWindowTitle("IFC Viewer") 234 | for file in filenames: 235 | if os.path.isfile(file): 236 | if self.load_file(file): 237 | # Concatenate all file names 238 | title = "IFC Viewer" 239 | for filename, file in self.ifc_files.items(): 240 | title += " - " + os.path.basename(filename) 241 | if len(title) > 64: 242 | title = title[:64] + "..." 243 | self.setWindowTitle(title) 244 | 245 | def close_files(self): 246 | """ 247 | Close all loaded files (and clear the different views) 248 | """ 249 | self.ifc_files = {} 250 | self.view_tree.close_files() 251 | self.view_properties.reset() 252 | if self.USE_3D: 253 | self.view_3d.close_files() 254 | self.view_takeoff.close_files() 255 | self.setWindowTitle("IFC Viewer") 256 | 257 | def toggle_use_3d(self): 258 | self.USE_3D = not self.USE_3D 259 | self.settings.setValue("USE_3D", self.USE_3D) 260 | 261 | 262 | 263 | # Our Main function 264 | def main(): 265 | app = 0 266 | if QApplication.instance(): 267 | app = QApplication.instance() 268 | else: 269 | app = QApplication(sys.argv) 270 | app.setApplicationDisplayName("IFC Viewer") 271 | app.setOrganizationName("sbuild") 272 | app.setOrganizationDomain("sbuild.com") 273 | app.setApplicationName("QIFCViewer") 274 | 275 | w = QIFCViewer() 276 | w.setWindowTitle("IFC Viewer") 277 | w.resize(1280, 800) 278 | filename = sys.argv[1] if len(sys.argv) >= 2 else '' 279 | if os.path.isfile(filename): 280 | w.load_file(filename) 281 | w.setWindowTitle(w.windowTitle() + " - " + os.path.basename(filename)) 282 | w.show() 283 | sys.exit(app.exec_()) 284 | 285 | 286 | if __name__ == "__main__": 287 | main() 288 | -------------------------------------------------------------------------------- /Viewer/IFCTreeWidget.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | 4 | try: 5 | from PyQt5.QtCore import * 6 | from PyQt5.QtGui import * 7 | from PyQt5.QtWidgets import * 8 | except Exception: 9 | from PySide2.QtGui import * 10 | from PySide2.QtCore import * 11 | from PySide2.QtWidgets import * 12 | 13 | import ifcopenshell 14 | from IFCCustomDelegate import * 15 | 16 | 17 | class IFCTreeWidget(QWidget): 18 | """ 19 | Fifth version of the IFC Tree Widget/View 20 | - V1 = Single Tree (object + basic hierarchy) 21 | - V2 = Double Tree (object with type, properties/quantities + attributes) 22 | - V3 = Support for Selection Signals (to be linked to 3D view) 23 | - V4 = Object Tree with references + Property tree with Header data 24 | - V5 = Editing the name of objects + keeping multiple files in a dictionary 25 | - V6 = Spinning off the property tree into its own widget 26 | - V7 = Make the tree configurable (Decomposition, Root Class Chooser) 27 | """ 28 | def __init__(self): 29 | QWidget.__init__(self) 30 | # A dictionary referring to our files, based on name 31 | self.ifc_files = {} 32 | 33 | # Main Settings 34 | self.root_class = 'IfcProject' 35 | self.follow_associations = False 36 | self.follow_defines = False 37 | self.follow_decomposition = True 38 | 39 | # Prepare Tree Widgets in a stretchable layout 40 | vbox = QVBoxLayout() 41 | self.setLayout(vbox) 42 | 43 | # Series of buttons and check boxes in a horizontal layout 44 | buttons = QWidget() 45 | vbox.addWidget(buttons) 46 | hbox = QHBoxLayout() 47 | hbox.setContentsMargins(0, 0, 0, 0) 48 | buttons.setLayout(hbox) 49 | # Option : Decomposition 50 | self.check_c = QCheckBox("Decomposition") 51 | self.check_c.setToolTip("Display the Containment and Decomposition relations in the object tree") 52 | self.check_c.setChecked(self.follow_decomposition) 53 | self.check_c.toggled.connect(self.toggle_decomposition) 54 | hbox.addWidget(self.check_c) 55 | # Stretchable Spacer 56 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 57 | hbox.addSpacerItem(spacer) 58 | # Root Class Chooser 59 | self.root_class_chooser = QComboBox() 60 | self.root_class_chooser.setToolTip("Select the top level class to display in the tree") 61 | self.root_class_chooser.setMinimumWidth(80) 62 | self.root_class_chooser.addItem('IfcProject') 63 | self.root_class_chooser.addItem('IfcMaterial') 64 | self.root_class_chooser.addItem('IfcProduct') 65 | self.root_class_chooser.addItem('IfcRelationship') 66 | self.root_class_chooser.addItem('IfcPropertySet') 67 | self.root_class_chooser.setEditable(True) 68 | self.root_class_chooser.activated.connect(self.toggle_chooser) 69 | hbox.addWidget(self.root_class_chooser) 70 | 71 | # Object Tree 72 | self.object_tree = QTreeWidget() 73 | vbox.addWidget(self.object_tree) 74 | self.object_tree.setColumnCount(2) 75 | self.object_tree.setHeaderLabels(["Name", "Class"]) 76 | self.object_tree.setSelectionMode(QAbstractItemView.ExtendedSelection) 77 | self.object_tree.selectionModel().selectionChanged.connect(self.send_selection) 78 | self.object_tree.itemDoubleClicked.connect(self.check_object_name_edit) 79 | self.object_tree.itemChanged.connect(self.set_object_name_edit) 80 | 81 | # region Selection Methods 82 | 83 | select_object = pyqtSignal(object) 84 | deselect_object = pyqtSignal(object) 85 | send_selection_set = pyqtSignal(object) 86 | 87 | 88 | def send_selection(self, selected_items, deselected_items): 89 | items = self.object_tree.selectedItems() 90 | self.send_selection_set.emit(items) 91 | for item in items: 92 | entity = item.data(0, Qt.UserRole) 93 | if hasattr(entity, "GlobalId"): 94 | GlobalId = entity.GlobalId 95 | if GlobalId != '': 96 | self.select_object.emit(GlobalId) 97 | print("IFCTreeWidget.send_selection.select_object ", GlobalId) 98 | 99 | # send the deselected items as well 100 | for index in deselected_items.indexes(): 101 | if index.column() == 0: # only for first column, to avoid repeats 102 | item = self.object_tree.itemFromIndex(index) 103 | entity = item.data(0, Qt.UserRole) 104 | if hasattr(entity, "GlobalId"): 105 | GlobalId = entity.GlobalId 106 | if GlobalId != '': 107 | self.deselect_object.emit(GlobalId) 108 | print("IFCTreeWidget.send_selection.deselect_object ", GlobalId) 109 | 110 | def receive_selection(self, ids): 111 | print("IFCTreeWidget.receive_selection ", ids) 112 | self.object_tree.clearSelection() 113 | if not len(ids): 114 | return 115 | # TODO: this may take a while in large trees - should we have a cache? 116 | iterator = QTreeWidgetItemIterator(self.object_tree) 117 | while iterator.value(): 118 | item = iterator.value() 119 | entity = item.data(0, Qt.UserRole) 120 | if entity is not None and hasattr(entity, "GlobalId"): 121 | if entity.GlobalId == ids: 122 | item.setSelected(not item.isSelected()) 123 | index = self.object_tree.indexFromItem(item) 124 | self.object_tree.scrollTo(index) 125 | iterator += 1 126 | 127 | def receive_object_update(self, ifc_object): 128 | iterator = QTreeWidgetItemIterator(self.object_tree) 129 | while iterator.value(): 130 | item = iterator.value() 131 | entity = item.data(0, Qt.UserRole) 132 | if entity == ifc_object: 133 | # refresh my name 134 | item.setText(0, ifc_object.Name) 135 | iterator += 1 136 | 137 | # endregion 138 | 139 | # region File Methods 140 | 141 | def close_files(self): 142 | self.ifc_files.clear() 143 | self.object_tree.clear() 144 | self.prepare_chooser() 145 | 146 | def load_file(self, filename): 147 | """ 148 | Load the file passed as filename and builds the whole object tree. 149 | If it already exists, that branch is removed and recreated. 150 | 151 | :param filename: Full path to the IFC file 152 | """ 153 | ifc_file = None 154 | if filename in self.ifc_files: 155 | ifc_file = self.ifc_files[filename] 156 | for i in range(self.object_tree.topLevelItemCount()): 157 | toplevel_item = self.object_tree.topLevelItem(i) 158 | if toplevel_item is not None and filename == toplevel_item.text(0): 159 | root = self.object_tree.invisibleRootItem() 160 | root.removeChild(toplevel_item) 161 | else: # Load as new file 162 | ifc_file = ifcopenshell.open(filename) 163 | self.ifc_files[filename] = ifc_file 164 | 165 | self.prepare_chooser() 166 | self.add_objects(filename) 167 | 168 | # endregion 169 | 170 | # region Object Tree Methods 171 | 172 | def add_objects(self, filename): 173 | """Fill the Object Tree with TreeItems representing Entity Instances 174 | 175 | :param filename: The filename for a loaded IFC model (in the files dictionary) 176 | :type filename: str 177 | """ 178 | ifc_file = self.ifc_files[filename] 179 | root_item = QTreeWidgetItem([filename, 'File']) 180 | root_item.setData(0, Qt.UserRole, ifc_file) 181 | try: 182 | for item in ifc_file.by_type(self.root_class): 183 | self.add_object_in_tree(item, root_item) 184 | except: 185 | dlg = QMessageBox(self.parent()) 186 | dlg.setWindowTitle("Invalid IFC Class!") 187 | dlg.setStandardButtons(QMessageBox.Ok) 188 | dlg.setIcon(QMessageBox.Critical) 189 | dlg.setText(str("{} is not a valid class name.\nSuggestions are IfcProject or IfcWall.").format(self.root_class)) 190 | dlg.exec_() 191 | # Finish the GUI 192 | self.object_tree.addTopLevelItem(root_item) 193 | self.object_tree.expandToDepth(3) 194 | 195 | def add_object_in_tree(self, ifc_object, parent_item): 196 | """ 197 | Fill the Object Tree recursively with Objects and their 198 | children, as defined by the relationships 199 | 200 | :param ifc_object: an IFC entity instance 201 | :type ifc_object: entity_instance 202 | :param parent_item: the parent QTreeWidgetItem 203 | :type parent_item: QTreeWidgetItem 204 | """ 205 | my_name = ifc_object.Name if hasattr(ifc_object, "Name") else "" 206 | tree_item = QTreeWidgetItem([my_name, ifc_object.is_a()]) 207 | parent_item.addChild(tree_item) 208 | tree_item.setData(0, Qt.UserRole, ifc_object) 209 | tree_item.setToolTip(0, entity_summary(ifc_object)) 210 | 211 | if self.follow_decomposition: 212 | if hasattr(ifc_object, 'ContainsElements'): 213 | for rel in ifc_object.ContainsElements: 214 | for element in rel.RelatedElements: 215 | self.add_object_in_tree(element, tree_item) 216 | if hasattr(ifc_object, 'IsDecomposedBy'): 217 | for rel in ifc_object.IsDecomposedBy: 218 | for related_object in rel.RelatedObjects: 219 | self.add_object_in_tree(related_object, tree_item) 220 | if hasattr(ifc_object, 'IsGroupedBy'): 221 | for rel in ifc_object.IsGroupedBy: 222 | if hasattr(rel, 'RelatedObjects'): 223 | for related_object in rel.RelatedObjects: 224 | self.add_object_in_tree(related_object, tree_item) 225 | if hasattr(ifc_object, 'AssignedItems'): # objects on layers 226 | for rep in ifc_object.AssignedItems: 227 | # self.add_object_in_tree(rep, tree_item) 228 | # From Shape Representation to Product Definition Shape to Product? 229 | for prod_def_shape in rep.OfProductRepresentation: 230 | for prod in prod_def_shape.ShapeOfProduct: 231 | self.add_object_in_tree(prod, tree_item) 232 | 233 | def set_object_name_edit(self, item, column): 234 | """ 235 | Send the change back to the item 236 | 237 | :param item: QTreeWidgetItem 238 | :param int column: Column index 239 | :return: 240 | """ 241 | if item.text(1) == 'File': 242 | return 243 | ifc_object = item.data(0, Qt.UserRole) 244 | if ifc_object is not None: 245 | if hasattr(ifc_object, "Name"): 246 | ifc_object.Name = item.text(0) 247 | # warn other views/widgets 248 | items = self.object_tree.selectedItems() 249 | self.send_selection_set.emit(items) 250 | 251 | def check_object_name_edit(self, item, column): 252 | """ 253 | Check whether this item can be edited 254 | 255 | :param item: QTreeWidgetItem 256 | :param column: Column index 257 | :return: 258 | """ 259 | if item.text(1) == 'File': 260 | return 261 | 262 | tmp = item.flags() 263 | if column == 0: 264 | item.setFlags(tmp | Qt.ItemIsEditable) 265 | elif tmp & Qt.ItemIsEditable: 266 | item.setFlags(tmp ^ Qt.ItemIsEditable) 267 | 268 | # endregion 269 | 270 | # region UI Methods 271 | 272 | def toggle_decomposition(self): 273 | self.follow_decomposition = not self.follow_decomposition 274 | self.regenerate_tree() 275 | 276 | def toggle_chooser(self, text): 277 | self.root_class = self.root_class_chooser.currentText() 278 | self.regenerate_tree() 279 | 280 | def prepare_chooser(self): 281 | buffer = self.root_class_chooser.currentText() 282 | if buffer == '': 283 | buffer = 'IfcProject' 284 | self.root_class_chooser.clear() 285 | for _, file in self.ifc_files.items(): 286 | for t in file.wrapped_data.types(): 287 | if self.root_class_chooser.findText(t, Qt.MatchFixedString) == -1: # require exact matching! 288 | self.root_class_chooser.addItem(t) 289 | 290 | # Add all available classes in the Combobox 291 | self.root_class_chooser.setEditable(False) 292 | self.root_class_chooser.model().sort(0, Qt.AscendingOrder) 293 | self.root_class_chooser.setCurrentText(buffer) 294 | 295 | def regenerate_tree(self): 296 | self.object_tree.clear() 297 | for filename, file in self.ifc_files.items(): 298 | self.add_objects(filename) 299 | 300 | # endregion 301 | 302 | 303 | if __name__ == '__main__': 304 | app = 0 305 | if QApplication.instance(): 306 | app = QApplication.instance() 307 | else: 308 | app = QApplication(sys.argv) 309 | 310 | w = IFCTreeWidget() 311 | w.resize(600, 800) 312 | filename = sys.argv[1] 313 | if os.path.isfile(filename): 314 | w.load_file(filename) 315 | w.show() 316 | sys.exit(app.exec_()) 317 | -------------------------------------------------------------------------------- /Viewer/IFCCustomDelegate.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # import os.path 3 | import re 4 | 5 | try: 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | except Exception: 10 | from PySide2.QtGui import * 11 | from PySide2.QtCore import * 12 | from PySide2.QtWidgets import * 13 | 14 | import ifcopenshell 15 | 16 | 17 | # region Utility Methods 18 | 19 | def entity_summary(entity): 20 | """ 21 | Return a multi-line string containing a summary of information about 22 | the IFC entity instance (STEP Id, Name, Class and GlobalId). 23 | Can be used in a Tooltip. 24 | 25 | :param entity: The IFC entity instance 26 | """ 27 | myId = str(entity.id()) if hasattr(entity, "id") else "" 28 | myName = entity.Name if hasattr(entity, "Name") else "" 29 | myClass = entity.is_a() if hasattr(entity, "is_a") else "" 30 | myGlobalId = entity.GlobalId if hasattr(entity, "GlobalId") else "" 31 | return str("STEP id\t: #{}\nName\t: {}\nClass\t: {}\nGlobalId\t: {}").format( 32 | myId, myName, myClass, myGlobalId) 33 | 34 | 35 | def get_enums_from_object(ifc_object, att_name): 36 | """ 37 | Check the schema to get the list of enumerations 38 | for one particular attribute of a class. 39 | 40 | As we don't know the schema from the object (or do we?) 41 | we try both the IFC2x3 and IFC4 schemes. 42 | 43 | :param ifc_object: instance of an IFC object 44 | :type ifc_object: entity_instance 45 | :param att_name: name of the attribute 46 | :type att_name: str 47 | """ 48 | if hasattr(ifc_object, att_name): 49 | att_index = ifc_object.wrapped_data.get_argument_index(att_name) 50 | # att_value = ifc_object.wrapped_data.get_argument(att_index) 51 | # att_type = ifc_object.wrapped_data.get_argument_type(att_index) 52 | att_type = ifc_object.attribute_type(att_index) 53 | if att_type == 'ENUMERATION': 54 | try: 55 | schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name('IFC2x3') 56 | e_class = schema.declaration_by_name(ifc_object.is_a()) 57 | attribute = e_class.attribute_by_index(att_index) 58 | return attribute.type_of_attribute().declared_type().enumeration_items() 59 | except: 60 | pass 61 | try: 62 | schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name('IFC4') 63 | e_class = schema.declaration_by_name(ifc_object.is_a()) 64 | attribute = e_class.attribute_by_index(att_index) 65 | return attribute.type_of_attribute().declared_type().enumeration_items() 66 | except: 67 | pass 68 | 69 | 70 | def camel_case_split(string): 71 | return re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', string) 72 | 73 | 74 | def get_friendly_ifc_name(ifc_object): 75 | # Trick to split the IfcClass into separate words 76 | s = ' '.join(camel_case_split(str(ifc_object.is_a()))) 77 | s = s[4:] 78 | return s 79 | 80 | # endregion 81 | 82 | # region Delegates & Editing 83 | 84 | 85 | def str2bool(v): 86 | return str(v).lower() in ("yes", "y", "true", "t", ".t.", "1") 87 | 88 | 89 | class QCustomDelegate(QItemDelegate): 90 | # https://stackoverflow.com/questions/41207485/how-to-create-combo-box-qitemdelegate 91 | # https://stackoverflow.com/questions/18068439/pyqt-simplest-working-example-of-a-combobox-inside-qtableview 92 | 93 | send_update_object = pyqtSignal(object) 94 | 95 | def __init__(self, parent): 96 | QItemDelegate.__init__(self, parent) 97 | self.allowed_column = -1 98 | 99 | def set_allowed_column(self, col): 100 | self.allowed_column = col 101 | 102 | def createEditor(self, widget, option, index): 103 | """ 104 | Create a Combobox for enumerations 105 | Create a LineEdit for strings and double 106 | Create a Spinbox for integers 107 | Create a Checkbox for Bool 108 | But do nothing for other data types 109 | """ 110 | # model = index.model() 111 | # row = index.row() 112 | column = index.column() 113 | # parent = index.parent() 114 | if column == self.allowed_column or self.allowed_column == -1: 115 | att_current_value = index.data(Qt.DisplayRole) 116 | ifc_object = index.data(Qt.UserRole) 117 | att_name = index.data(Qt.UserRole + 1) 118 | att_value = index.data(Qt.UserRole + 2) 119 | att_type = index.data(Qt.UserRole + 3) 120 | att_index = index.data(Qt.UserRole + 4) 121 | ifc_sub_object = index.data(Qt.UserRole + 5) 122 | target = ifc_object 123 | if ifc_sub_object is not None: 124 | target = ifc_sub_object 125 | if target is not None and att_name not in ['GlobalId', 'id', 'type', 'class']: 126 | # Check Properties (even if we don't know the unit...) 127 | if target.is_a('IfcPropertySingleValue'): 128 | attribute = target.wrapped_data.get_argument('NominalValue') 129 | if attribute.is_a('IfcBoolean'): 130 | check = QCheckBox(widget) 131 | check.setText(target.wrapped_data.get_argument('Name')) # use the Name of the Property 132 | check.setAutoFillBackground(True) 133 | check.setChecked(str2bool(att_current_value)) 134 | return check 135 | else: 136 | return QItemDelegate.createEditor(self, widget, option, index) # default 137 | if att_type == 'ENTITY INSTANCE': 138 | return None 139 | if att_type in ['ENUMERATION']: 140 | enums = get_enums_from_object(target, att_name) 141 | combo = QComboBox(widget) 142 | combo.setAutoFillBackground(True) 143 | combo.addItems(enums) 144 | combo.setCurrentText(att_current_value) 145 | return combo 146 | if att_type in ['INT']: 147 | spin = QSpinBox(widget) 148 | spin.setRange(-1e6, 1e6) 149 | spin.setValue(int(att_current_value)) 150 | return spin 151 | if att_type in ['DOUBLE']: 152 | spin = QDoubleSpinBox(widget) 153 | spin.setRange(-1e10, 1e10) 154 | if att_current_value is None or att_current_value == 'None': 155 | spin.setValue(0.0) 156 | else: 157 | v = att_current_value 158 | v2 = float(att_current_value) 159 | spin.setValue(float(att_current_value)) 160 | return spin 161 | if att_type in ['BOOL']: 162 | check = QCheckBox(widget) 163 | check.setText(att_name) 164 | check.setAutoFillBackground(True) 165 | check.setChecked(str2bool(att_current_value)) 166 | return check 167 | if att_type in ['STRING']: 168 | return QItemDelegate.createEditor(self, widget, option, index) # default = QLineEdit 169 | 170 | # return QItemDelegate.createEditor(self, widget, option, index) # default 171 | return None 172 | 173 | def setModelData(self, editor, model, index): 174 | """This is the data stored into the field""" 175 | if isinstance(editor, QComboBox): 176 | model.setData(index, editor.itemText(editor.currentIndex())) 177 | if isinstance(editor, QLineEdit): 178 | model.setData(index, editor.text()) 179 | if isinstance(editor, QSpinBox) or isinstance(editor, QDoubleSpinBox): 180 | model.setData(index, editor.valueFromText(editor.cleanText())) 181 | if isinstance(editor, QCheckBox): 182 | model.setData(index, editor.isChecked()) 183 | 184 | column = index.column() 185 | if column == self.allowed_column or self.allowed_column == -1: 186 | att_new_value = index.data(Qt.DisplayRole) 187 | ifc_object = index.data(Qt.UserRole) 188 | att_name = index.data(Qt.UserRole + 1) 189 | att_value = index.data(Qt.UserRole + 2) 190 | att_type = index.data(Qt.UserRole + 3) 191 | att_index = index.data(Qt.UserRole + 4) 192 | ifc_sub_object = index.data(Qt.UserRole + 5) 193 | # TODO: editing property in the Listing View? Get property as sub_object 194 | target = ifc_object 195 | if ifc_sub_object is not None: 196 | target = ifc_sub_object 197 | if target is not None: 198 | # PropertySingleValue > wrapped value 199 | if target.is_a('IfcPropertySingleValue'): 200 | try: 201 | attribute = target.wrapped_data.get_argument('NominalValue') 202 | # attribute = getattr(ifc_object, 'NominalValue') 203 | if str(att_new_value) == '': 204 | # attribute.setArgumentAsNull(0) # crashes? 205 | target.setArgumentAsNull(3) 206 | else: 207 | if attribute.is_a('IfcAreaMeasure') or attribute.is_a('IfcLengthMeasure')\ 208 | or attribute.is_a('IfcVolumeMeasure'): 209 | attribute.setArgumentAsDouble(0, float(att_new_value)) 210 | elif attribute.is_a('IfcText') or attribute.is_a('IfcLabel'): 211 | attribute.setArgumentAsString(0, str(att_new_value)) 212 | elif attribute.is_a('IfcBoolean'): 213 | att_new_value = str2bool(att_new_value) 214 | attribute.setArgumentAsBool(0, att_new_value) 215 | else: 216 | setattr(attribute, 'wrappedValue', att_new_value) 217 | model.setData(index, att_new_value) 218 | except: 219 | print("Could not set Attribute :", att_name, " with value ", att_new_value) 220 | print("So we reset it to ", str(target.NominalValue.wrappedValue)) 221 | model.setData(index, str(att_value)) 222 | pass 223 | else: 224 | # regular attributes > based on attribute index 225 | print("Attribute", att_index, "current:", att_value, "new:", att_new_value) 226 | if att_value != att_new_value and hasattr(target, att_name): 227 | try: 228 | if att_type == 'ENTITY INSTANCE': 229 | print('Can not set instance from string') 230 | if att_type == 'INT': 231 | setattr(target, att_name, int(att_new_value)) 232 | if att_type == 'DOUBLE': 233 | setattr(target, att_name, float(att_new_value)) 234 | if att_type == 'BOOL': 235 | setattr(target, att_name, bool(att_new_value)) 236 | if att_type == 'ENUMERATION': 237 | enum = get_enums_from_object(target, att_name) 238 | if att_new_value in enum: 239 | print('Valid enum value') 240 | setattr(target, att_name, att_new_value) 241 | else: 242 | print('Invalid enum value') 243 | model.setData(index, str(att_value)) 244 | if att_type == 'STRING': 245 | setattr(target, att_name, att_new_value) 246 | except: 247 | print("Could not set Attribute :", att_name, " with value ", att_new_value) 248 | print("So we reset it to ", att_value) 249 | model.setData(index, str(att_value)) 250 | pass 251 | # Warn other views, but only needed if Name is changed 252 | if att_name == "Name": 253 | self.send_update_object.emit(target) 254 | 255 | else: 256 | QItemDelegate.setModelData(self, editor, model, index) 257 | 258 | def updateEditorGeometry(self, editor, option, index): 259 | editor.setGeometry(option.rect) 260 | 261 | def paint(self, painter, styleoptions, index): 262 | """ 263 | Add a greenish background color to indicate editable cells 264 | """ 265 | # model = index.model() 266 | row = index.row() 267 | column = index.column() 268 | # parent = index.parent() 269 | if column == self.allowed_column or self.allowed_column == -1: 270 | ifc_object = index.data(Qt.UserRole) 271 | att_name = index.data(Qt.UserRole + 1) 272 | # att_value = index.data(Qt.UserRole + 2) 273 | att_type = index.data(Qt.UserRole + 3) 274 | # att_index = index.data(Qt.UserRole + 4) 275 | ifc_sub_object = index.data(Qt.UserRole + 5) 276 | target = ifc_object 277 | if ifc_sub_object is not None: 278 | target = ifc_sub_object 279 | if target is not None: 280 | if att_name != 'GlobalId'\ 281 | and att_type in ['STRING', 'DOUBLE', 'ENUMERATION', 'INT', 'BOOL']\ 282 | and not target.is_a('IfcPhysicalQuantity'): 283 | # add a greenish background color to indicate editable cells 284 | painter.fillRect(styleoptions.rect, QColor(191, 222, 185, 30)) 285 | elif target.is_a('IfcPropertySingleValue'): 286 | painter.fillRect(styleoptions.rect, QColor(191, 185, 222, 30)) 287 | 288 | # But also do the regular paint 289 | QItemDelegate.paint(self, painter, styleoptions, index) 290 | 291 | # endregion 292 | -------------------------------------------------------------------------------- /Viewer/IFCListingWidget.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import csv 4 | 5 | try: 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | except Exception: 10 | from PySide2.QtGui import * 11 | from PySide2.QtCore import * 12 | from PySide2.QtWidgets import * 13 | 14 | import ifcopenshell 15 | from IFCCustomDelegate import * 16 | 17 | # region Utility Functions 18 | 19 | 20 | def get_type_name(element): 21 | """Retrieve name of the relating Type""" 22 | result = {} 23 | if hasattr(element, 'IsDefinedBy') is False: 24 | return result 25 | 26 | for definition in element.IsDefinedBy: 27 | if definition.is_a('IfcRelDefinesByType'): 28 | relating_type = definition.RelatingType 29 | # result['ifc_object'] = element 30 | result['ifc_sub_object'] = relating_type 31 | # result['att_name'] = 'Name' 32 | result['att_value'] = relating_type.Name 33 | result['att_type'] = relating_type.attribute_type(2) 34 | result['att_idx'] = 2 35 | result['IsEditable'] = True 36 | return result 37 | 38 | 39 | def get_attribute_by_name(element, attribute_name): 40 | """simple function to return the string value of an attribute""" 41 | result = {} 42 | # result['ifc_object'] = element 43 | result['ifc_sub_object'] = element 44 | # result['att_name'] = attribute_name 45 | result['IsEditable'] = False 46 | # exceptions for id, class, type 47 | if attribute_name == "id": 48 | # att = getattr(element, 'id') 49 | result['att_value'] = element.id() 50 | return result # str(element.id()), element 51 | elif attribute_name == "class": 52 | result['att_value'] = element.is_a() 53 | return result 54 | elif attribute_name == "type": 55 | return get_type_name(element) 56 | 57 | for att_idx in range(0, len(element)): 58 | att_name = element.attribute_name(att_idx) 59 | if att_name == attribute_name: 60 | try: 61 | att = element[att_idx] 62 | # att = element.wrapped_data.get_argument(att_name) 63 | result['att_value'] = str(element.wrap_value(att)) 64 | result['att_type'] = element.attribute_type(att_idx) 65 | result['att_idx'] = att_idx 66 | result['IsEditable'] = True 67 | return result 68 | except: 69 | return result 70 | 71 | 72 | def get_property_or_quantity_by_name(element, prop_or_quantity_name): 73 | """simple function to return the string value of a property or quantity""" 74 | result = {} 75 | # result['object'] = element 76 | # result['att_name'] = prop_or_quantity_name 77 | result['IsEditable'] = False 78 | if hasattr(element, 'IsDefinedBy') is False: 79 | return result 80 | 81 | # result['ifc_sub_object'] = element 82 | for definition in element.IsDefinedBy: 83 | if definition.is_a('IfcRelDefinesByProperties'): 84 | if hasattr(definition.RelatingPropertyDefinition, "HasProperties"): 85 | for prop in definition.RelatingPropertyDefinition.HasProperties: 86 | if prop.Name == prop_or_quantity_name and prop.is_a('IfcPropertySingleValue'): 87 | result['ifc_sub_object'] = prop 88 | result['att_value'] = prop.NominalValue.wrappedValue 89 | result['att_type'] = prop.attribute_type(2) 90 | result['att_idx'] = 2 # NominalValue 91 | result['IsEditable'] = True 92 | return result # str(prop.NominalValue.wrappedValue), prop 93 | if hasattr(definition.RelatingPropertyDefinition, "Quantities"): 94 | for quantity in definition.RelatingPropertyDefinition.Quantities: 95 | if quantity.Name == prop_or_quantity_name: 96 | result['ifc_sub_object'] = quantity 97 | result['att_type'] = quantity.attribute_type(3) 98 | result['att_idx'] = 3 # Quantity Value 99 | result['IsEditable'] = False 100 | if quantity.is_a('IfcQuantityLength'): 101 | result['att_value'] = quantity.LengthValue 102 | return result 103 | elif quantity.is_a('IfcQuantityArea'): 104 | result['att_value'] = quantity.AreaValue 105 | return result 106 | elif quantity.is_a('IfcQuantityVolume'): 107 | result['att_value'] = quantity.VolumeValue 108 | return result 109 | elif quantity.is_a('IfcQuantityCount'): 110 | result['att_value'] = quantity.CountValue 111 | return result 112 | else: 113 | return result 114 | 115 | 116 | def takeoff_element(element, header): 117 | """ 118 | Take off element attributes, properties or quantities, from a given header. 119 | Use the exact name (e.g., "GlobalId" for the GUID, "Name", "Description"). 120 | 121 | You can also state "id" for the STEP-ID, "class" to get the IFC entity class 122 | and "type" to get the name of a linked type. 123 | 124 | :param element: IFC Entity Reference 125 | :param header: list of strings to indicate attribute, property or quantity 126 | :return: List of dicts with the actual values 127 | """ 128 | columns = [] 129 | for search_value in header: 130 | # first try if it is an attribute 131 | result = get_attribute_by_name(element, search_value) 132 | if result is None: # maybe it is a property or quantity 133 | result = get_property_or_quantity_by_name(element, search_value) 134 | columns.append(result) 135 | return columns 136 | 137 | # endregion 138 | 139 | # region Header Editor 140 | 141 | 142 | class StringListEditor(QWidget): 143 | """ 144 | Generic List Widget to edit a list of Strings. 145 | Could be applied in different places. 146 | """ 147 | def __init__(self): 148 | QWidget.__init__(self) 149 | 150 | # Prepare Widgets in a stretchable layout 151 | vbox = QVBoxLayout() 152 | self.setLayout(vbox) 153 | 154 | # Series of buttons and check boxes in a horizontal layout 155 | buttons = QWidget() 156 | vbox.addWidget(buttons) 157 | hbox = QHBoxLayout() 158 | hbox.setContentsMargins(0, 0, 0, 0) 159 | buttons.setLayout(hbox) 160 | 161 | # Button : Apply 162 | apply = QPushButton("Apply") 163 | apply.setToolTip("Apply the List") 164 | apply.clicked.connect(self.apply) 165 | hbox.addWidget(apply) 166 | # Button : Insert 167 | insert = QPushButton("Insert") 168 | insert.setToolTip("Insert Item into the List\nid, class, type or any\nattribute, property or quantity name") 169 | insert.clicked.connect(self.insert_item) 170 | hbox.addWidget(insert) 171 | # Button : Remove 172 | remove = QPushButton("Remove") 173 | remove.setToolTip("Remove selected List Item") 174 | remove.clicked.connect(self.remove_item) 175 | hbox.addWidget(remove) 176 | # Stretchable Spacer 177 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 178 | hbox.addSpacerItem(spacer) 179 | 180 | # List of Strings 181 | self.label_list = QListWidget() 182 | vbox.addWidget(self.label_list) 183 | self.label_list.setDragDropMode(QListWidget.InternalMove) 184 | 185 | apply_labels = pyqtSignal(object) 186 | 187 | def set_labels(self, labels): 188 | # self.labels = labels 189 | self.label_list.clear() 190 | self.label_list.addItems(labels) 191 | for index in range(self.label_list.count()): 192 | item = self.label_list.item(index) 193 | item.setFlags(item.flags() | Qt.ItemIsEditable) 194 | 195 | def apply(self): 196 | labels = [] 197 | for item in range(self.label_list.count()): 198 | labels.append(self.label_list.item(item).text()) 199 | self.apply_labels.emit(labels) 200 | 201 | def insert_item(self): 202 | new_item = QListWidgetItem("") 203 | new_item.setFlags(new_item.flags() | Qt.ItemIsEditable) 204 | 205 | selection = self.label_list.selectedItems() 206 | if not selection: 207 | self.label_list.insertItem(0, new_item) 208 | for item in selection: 209 | position = self.label_list.row(item) 210 | self.label_list.insertItem(position, new_item) 211 | return 212 | 213 | def remove_item(self): 214 | selection = self.label_list.selectedItems() 215 | if not selection: return 216 | for item in selection: 217 | self.label_list.takeItem(self.label_list.row(item)) 218 | 219 | # endregion 220 | 221 | # region IFC Listing Widget 222 | 223 | 224 | class IFCListingWidget(QWidget): 225 | """ 226 | Fifth version of the IFC Tree Widget/View 227 | - V1 = Take off Table + Takeoff Button + CSV export + Header Editor 228 | - V2 = From Table Widget to Table View 229 | - V3 = Metadata into Takeoff Item to support editing with Delegate 230 | """ 231 | def __init__(self): 232 | QWidget.__init__(self) 233 | # A dictionary referring to our files, based on name 234 | self.ifc_files = {} 235 | 236 | # Main Settings 237 | self.root_class = 'IfcElement' 238 | self.header = [] # list of strings 239 | 240 | # Prepare Main Widgets in a stretchable layout 241 | vbox = QVBoxLayout() 242 | self.setLayout(vbox) 243 | 244 | # Series of buttons and check boxes in a horizontal layout 245 | buttons = QWidget() 246 | vbox.addWidget(buttons) 247 | hbox = QHBoxLayout() 248 | hbox.setContentsMargins(0, 0, 0, 0) 249 | buttons.setLayout(hbox) 250 | 251 | # Button : Take Off 252 | takeoff = QPushButton("Take Off") 253 | takeoff.setToolTip("Generate the Takeoff") 254 | takeoff.pressed.connect(self.take_off) 255 | hbox.addWidget(takeoff) 256 | # Button : Reset 257 | reset = QPushButton("Reset") 258 | reset.setToolTip("Reset the Takeoff") 259 | reset.pressed.connect(self.reset) 260 | hbox.addWidget(reset) 261 | # Button : Export to CSV 262 | export = QPushButton("Export") 263 | export.setToolTip("Export to CSV") 264 | export.pressed.connect(self.export) 265 | hbox.addWidget(export) 266 | # Stretchable Spacer 267 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 268 | hbox.addSpacerItem(spacer) 269 | # Root Class Chooser 270 | self.root_class_chooser = QComboBox() 271 | self.root_class_chooser.setToolTip("Select the IFC class to filter on") 272 | self.root_class_chooser.setMinimumWidth(80) 273 | self.root_class_chooser.addItem('IfcElement') 274 | self.root_class_chooser.addItem('IfcProduct') 275 | self.root_class_chooser.addItem('IfcWall') 276 | self.root_class_chooser.addItem('IfcWindow') 277 | self.root_class_chooser.setEditable(True) 278 | self.root_class_chooser.activated.connect(self.toggle_chooser) 279 | hbox.addWidget(self.root_class_chooser) 280 | # Button : Edit Header 281 | edit = QPushButton("Edit...") 282 | edit.setToolTip("Edit the Header Columns") 283 | edit.pressed.connect(self.edit) 284 | hbox.addWidget(edit) 285 | self.header_editor = None 286 | # Button : Default 287 | default = QPushButton("Default") 288 | default.setToolTip("Set default headers") 289 | default.pressed.connect(self.default_header) 290 | hbox.addWidget(default) 291 | 292 | # Listing Widget 293 | self.object_table = QTableView() 294 | self.model = None 295 | delegate = QCustomDelegate(self) 296 | self.object_table.setItemDelegate(delegate) 297 | vbox.addWidget(self.object_table) 298 | self.default_header() 299 | 300 | #region Files & UI methods 301 | 302 | def load_file(self, filename): 303 | """ 304 | Load the IFC file passed as filename. 305 | 306 | :param filename: Full path to the IFC file 307 | :type filename: str 308 | """ 309 | ifc_file = None 310 | if filename in self.ifc_files: 311 | ifc_file = self.ifc_files[filename] 312 | else: # Load as new file 313 | ifc_file = ifcopenshell.open(filename) 314 | self.ifc_files[filename] = ifc_file 315 | 316 | def close_files(self): 317 | self.ifc_files.clear() 318 | self.reset() 319 | 320 | def reset(self): 321 | if self.model is not None: 322 | self.model.clear() 323 | self.model = QStandardItemModel(0, len(self.header), self) 324 | self.object_table.setModel(self.model) 325 | for c, h in enumerate(self.header): 326 | self.model.setHeaderData(c, Qt.Horizontal, h) 327 | 328 | self.object_table.setSelectionMode(QAbstractItemView.SingleSelection) 329 | self.object_table.setSelectionBehavior(QAbstractItemView.SelectRows) 330 | self.object_table.selectionModel().selectionChanged.connect(self.send_selection) 331 | 332 | def edit(self): 333 | # open a dialog with a StringList edit widget 334 | if self.header_editor is None: 335 | self.header_editor = StringListEditor() 336 | self.header_editor.set_labels(self.header) 337 | self.header_editor.apply_labels.connect(self.set_headers) 338 | self.header_editor.show() 339 | 340 | def export(self): 341 | savepath = QFileDialog.getSaveFileName(self, caption="Save CSV File", 342 | filter="CSV files (*.csv)") 343 | if savepath[0] != '': 344 | # Write to CSV file 345 | csv_file = open(savepath[0], 'w') 346 | qto_writer = csv.writer(csv_file, delimiter=';') 347 | qto_writer.writerow(self.header) 348 | 349 | # take off if not done already 350 | if self.model.rowCount() == 0: 351 | self.take_off() 352 | # export table to CSV 353 | for r in range(self.model.rowCount()): 354 | record = [] 355 | for c in range(self.model.columnCount()): 356 | index = self.model.index(r, c) 357 | item_data = index.data(Qt.DisplayRole) 358 | record.append(str(item_data)) 359 | qto_writer.writerow(record) 360 | csv_file.close() 361 | 362 | def set_headers(self, labels): 363 | self.header = labels 364 | self.reset() 365 | 366 | def default_header(self): 367 | self.header.clear() 368 | # prepare takeoff by listing names of attributes (hardcoded) 369 | attributes = ["id", "GlobalId", "class", "PredefinedType", "ObjectType", "type", "Name"] 370 | # names of properties (anything goes) 371 | properties = ["LoadBearing", "IsExternal", "Reference"] 372 | # names of quantities (anything goes) 373 | quantities = ["Length", "Height", "Width", "Perimeter", "Area", "Volume", "Depth", 374 | "NetSideArea", "GrossArea", "NetArea", "GrossVolume", "NetVolume"] 375 | # concatenate 376 | self.header.extend(attributes + properties + quantities) 377 | self.reset() 378 | 379 | def toggle_chooser(self, text): 380 | self.root_class = self.root_class_chooser.currentText() 381 | self.reset() 382 | 383 | # endregion 384 | 385 | # region Take Off 386 | 387 | def take_off(self): 388 | self.reset() 389 | if len(self.header) == 0: 390 | return 391 | for _, file in self.ifc_files.items(): 392 | try: 393 | items = file.by_type(self.root_class) 394 | except: 395 | dlg = QMessageBox(self.parent()) 396 | dlg.setWindowTitle("Invalid IFC Class!") 397 | dlg.setStandardButtons(QMessageBox.Close) 398 | dlg.setIcon(QMessageBox.Critical) 399 | dlg.setText(str("{} is not a valid IFC class name.\n" 400 | "\nSuggestions are IfcElement or IfcWall.\n" 401 | "We will reset it to 'IfcElement'").format( 402 | self.root_class)) 403 | dlg.exec_() 404 | wrong_value = self.root_class 405 | index_of_wrong = self.root_class_chooser.findText(wrong_value) 406 | self.root_class_chooser.removeItem(index_of_wrong) 407 | self.root_class = 'IfcElement' 408 | self.root_class_chooser.setCurrentText(self.root_class) 409 | return 410 | for ifc_object in items: 411 | record = takeoff_element(ifc_object, self.header) 412 | # add an empty row 413 | row = [] 414 | # and fill it with QStandardItems 415 | for column, cell in enumerate(record): 416 | if cell is not None: 417 | # we receive a Dict with all required metadata 418 | # but some keys may not exist! 419 | # ifc_object = cell['ifc_object'] 420 | ifc_sub_object = cell['ifc_sub_object'] if 'ifc_sub_object' in cell.keys() else None 421 | att_name = self.header[column] # cell['att_name'] 422 | att_value = cell['att_value'] if 'att_value' in cell.keys() else None 423 | att_type = cell['att_type'] if 'att_type' in cell.keys() else None 424 | att_idx = cell['att_idx'] if 'att_idx' in cell.keys() else None 425 | if ifc_sub_object is not None and att_idx != '' and att_idx is not None: 426 | att_name = ifc_sub_object.attribute_name(int(att_idx)) 427 | editable = cell['IsEditable'] if 'IsEditable' in cell.keys() else False 428 | 429 | new_item = QStandardItem(str(att_value)) 430 | if not editable: 431 | new_item.setFlags(new_item.flags() ^ Qt.ItemIsEditable) # make uneditable 432 | else: 433 | new_item.setFlags(new_item.flags() | Qt.ItemIsEditable) # make editable 434 | new_item.setData(ifc_object, Qt.UserRole) 435 | new_item.setData(att_name, Qt.UserRole + 1) # name 436 | new_item.setData(att_value, Qt.UserRole + 2) # value 437 | new_item.setData(att_type, Qt.UserRole + 3) # type 438 | new_item.setData(att_idx, Qt.UserRole + 4) # index 439 | new_item.setData(ifc_sub_object, Qt.UserRole + 5) # sub object 440 | buffer = "ifc_object:\t#" + str(ifc_object.id()) 441 | buffer += "\natt_name:\t" + str(att_name) 442 | buffer += "\natt_value:\t" + str(att_value) 443 | buffer += "\natt_type:\t" + str(att_type) 444 | buffer += "\natt_idx:\t\t" + str(att_idx) 445 | new_item.setToolTip(entity_summary(ifc_object)) 446 | if column != 0: 447 | new_item.setToolTip(buffer) 448 | row.append(new_item) 449 | else: 450 | row.append(QStandardItem()) 451 | self.model.appendRow(row) 452 | 453 | # endregion 454 | 455 | # region Selection Methods 456 | 457 | select_object = pyqtSignal(object) 458 | deselect_object = pyqtSignal(object) 459 | send_selection_set = pyqtSignal(object) 460 | 461 | def send_selection(self, selected_items, deselected_items): 462 | # selection_model = self.object_table.selectionModel() 463 | # items = selection_model.selectedItems() 464 | # self.send_selection_set.emit(items) 465 | # for item in items: 466 | for index in selected_items.indexes(): 467 | if index.column() == 0: # only for first column, to avoid repeats 468 | entity = index.data(Qt.UserRole) 469 | if hasattr(entity, "GlobalId"): 470 | GlobalId = entity.GlobalId 471 | if GlobalId != '': 472 | self.select_object.emit(GlobalId) 473 | print("IFCListingWidget.send_selection.select_object ", GlobalId) 474 | 475 | # send the deselected items as well 476 | for index in deselected_items.indexes(): 477 | if index.column() == 0: # only for first column, to avoid repeats 478 | entity = index.data(Qt.UserRole) 479 | if hasattr(entity, "GlobalId"): 480 | GlobalId = entity.GlobalId 481 | if GlobalId != '': 482 | self.deselect_object.emit(GlobalId) 483 | print("IFCListingWidget.send_selection.deselect_object ", GlobalId) 484 | 485 | def receive_selection(self, ids): 486 | print("IFCListingWidget.receive_selection ", ids) 487 | selection_model = self.object_table.selectionModel() 488 | # check if already selected 489 | index = selection_model.currentIndex() 490 | entity = index.data(Qt.UserRole) 491 | if entity is not None and hasattr(entity, "GlobalId"): 492 | if entity.GlobalId == ids: 493 | return 494 | selection_model.clearSelection() 495 | if not len(ids): 496 | return 497 | 498 | for r in range(self.model.rowCount()): 499 | index = self.model.index(r, 0) # only for first column, to avoid repeats 500 | entity = index.data(Qt.UserRole) 501 | if entity is not None and hasattr(entity, "GlobalId"): 502 | if entity.GlobalId == ids: 503 | self.object_table.selectRow(r) 504 | # selection_model.select(index, QItemSelectionModel.Rows) 505 | self.object_table.scrollTo(index) 506 | 507 | # endregion 508 | 509 | # endregion 510 | 511 | 512 | if __name__ == '__main__': 513 | app = 0 514 | if QApplication.instance(): 515 | app = QApplication.instance() 516 | else: 517 | app = QApplication(sys.argv) 518 | 519 | w = IFCListingWidget() 520 | w.setWindowTitle('IFC Listing') 521 | w.resize(600, 800) 522 | # input 2 = CSV with headers 523 | if len(sys.argv) > 2: 524 | if os.path.isfile(sys.argv[2]): 525 | csv_file = open(sys.argv[2]) 526 | reader = csv.reader(csv_file, delimiter=';') 527 | row1 = next(reader) 528 | header = [] 529 | for e in row1: 530 | header.append(str(e)) 531 | w.set_headers(header) 532 | # input 1 = the IFC file to load 533 | if os.path.isfile(sys.argv[1]): 534 | w.load_file(sys.argv[1]) 535 | # finally display the window & execute the app 536 | w.show() 537 | sys.exit(app.exec_()) 538 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /Viewer/IFCPropertyWidget.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | # import re 4 | 5 | try: 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | except Exception: 10 | from PySide2.QtGui import * 11 | from PySide2.QtCore import * 12 | from PySide2.QtWidgets import * 13 | 14 | import ifcopenshell 15 | from IFCCustomDelegate import * 16 | 17 | 18 | class IFCPropertyWidget(QWidget): 19 | """ 20 | A Widget containing all information from one object from one file. 21 | This is concerned with attributes, properties, quantities 22 | and associated materials, classifications. 23 | - V1 = Spin-off from IFCTreeWidget 24 | - V2 = Configurable display options (Properties, Associations, Attributes + Full detail) 25 | - V3 = Refining, add Assignments 26 | - V4 = Editing Attributes (STRING, DOUBLE, INT and ENUMERATION) 27 | - V5 = QTreeWidget replaced with QTreeView 28 | - V6 = Inverse Attributes 29 | - V7 = Updated Delegate (shared with other views/widgets) 30 | """ 31 | 32 | send_update_object = pyqtSignal(object) 33 | 34 | # region Initialisation 35 | 36 | def __init__(self): 37 | QWidget.__init__(self) 38 | # The list of the currently loaded objects 39 | self.loaded_objects_and_files = [] 40 | 41 | # Main Settings 42 | self.follow_attributes = False 43 | self.follow_inverse_attributes = False 44 | self.follow_properties = True 45 | self.follow_associations = False 46 | self.follow_assignments = False 47 | self.follow_defines = False 48 | self.show_all = False 49 | 50 | # Widgets Setup 51 | vbox = QVBoxLayout() 52 | self.setLayout(vbox) 53 | 54 | # Series of buttons and check boxes in a horizontal layout 55 | buttons = QWidget() 56 | vbox.addWidget(buttons) 57 | hbox = QHBoxLayout() 58 | hbox.setContentsMargins(0, 0, 0, 0) 59 | buttons.setLayout(hbox) 60 | # Option: Display Attributes 61 | self.check_a = QCheckBox("Attr") 62 | self.check_a.setToolTip("Display all attributes") 63 | self.check_a.setChecked(self.follow_attributes) 64 | self.check_a.toggled.connect(self.toggle_attributes) 65 | hbox.addWidget(self.check_a) 66 | # Option: Display Inverse Attributes 67 | self.check_ia = QCheckBox("InvAttr") 68 | self.check_ia.setToolTip("Display all inverse attributes") 69 | self.check_ia.setChecked(self.follow_inverse_attributes) 70 | self.check_ia.toggled.connect(self.toggle_inverse_attributes) 71 | hbox.addWidget(self.check_ia) 72 | # Option: Display Properties 73 | self.check_p = QCheckBox("Props") 74 | self.check_p.setToolTip("Display DefinedByProperties (incl. quantities)") 75 | self.check_p.setChecked(self.follow_properties) 76 | self.check_p.toggled.connect(self.toggle_properties) 77 | hbox.addWidget(self.check_p) 78 | # Option: Display Associations 79 | check_associations = QCheckBox("Assoc") 80 | check_associations.setToolTip("Display associations, such as materials and classifications") 81 | check_associations.setChecked(self.follow_associations) 82 | check_associations.toggled.connect(self.toggle_associations) 83 | hbox.addWidget(check_associations) 84 | # Option: Display Assignments 85 | check_assignments = QCheckBox("Assign") 86 | check_assignments.setToolTip("Display assignments, such as control, actor, process, group") 87 | check_assignments.setChecked(self.follow_assignments) 88 | check_assignments.toggled.connect(self.toggle_assignments) 89 | hbox.addWidget(check_assignments) 90 | # Option: Display Full Detail 91 | check_full_detail = QCheckBox("Full") 92 | check_full_detail.setToolTip("Show the full attributes hierarchy") 93 | check_full_detail.setChecked(self.show_all) 94 | check_full_detail.toggled.connect(self.toggle_show_all) 95 | hbox.addWidget(check_full_detail) 96 | # Stretchable Spacer 97 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 98 | hbox.addSpacerItem(spacer) 99 | 100 | # Property Tree 101 | self.property_tree = QTreeView() 102 | delegate = QCustomDelegate(self) 103 | delegate.set_allowed_column(1) 104 | delegate.send_update_object.connect(self.send_update_object) # to warn name changes 105 | self.property_tree.setItemDelegate(delegate) 106 | self.property_tree.setEditTriggers(QAbstractItemView.CurrentChanged) # open editor upon first click 107 | # self.property_tree.setEditTriggers(QAbstractItemView.NoEditTriggers) # Not editable tree 108 | # self.property_tree.setItemDelegateForColumn(1, delegate) 109 | self.model = None 110 | self.reset() 111 | vbox.addWidget(self.property_tree) 112 | 113 | # endregion 114 | 115 | # region SelectionMethods 116 | 117 | def set_from_selected_items(self, items): 118 | """ 119 | Fill the Property Tree from a selection of QTreeWidgetItems. 120 | Can be used to link from the Object Tree Widget 121 | 122 | :param items: List of QTreeWidgetItems (containing data in column 1) 123 | """ 124 | self.reset() 125 | for item in items: 126 | # our very first item is the File, so show the Header only 127 | if item.text(1) == "File": 128 | model = item.data(0, Qt.UserRole) 129 | if model is None: 130 | break 131 | self.add_file_header(model) 132 | else: 133 | ifc_object = item.data(0, Qt.UserRole) 134 | if ifc_object is None: 135 | break 136 | self.add_object_data(ifc_object) 137 | 138 | # endregion 139 | 140 | # region InformationFilling 141 | 142 | def add_inverse_attributes_in_tree(self, ifc_object, parent_item, recursion=0): 143 | """ 144 | Fill the property tree with the inverse ttributes of an object. When the "show all" 145 | option is activated, this will enable recursive attribute display. This can 146 | potentially go very deep and take a long time with deep structures or 147 | large geometry. 148 | 149 | :param ifc_object: IFC entity 150 | :param parent_item: QStandardItem used to put attributes underneath 151 | :param recursion: To avoid infinite recursion, the recursion level is checked 152 | """ 153 | attributes = ifc_object.wrapped_data.get_inverse_attribute_names() 154 | for att_idx, att_name in enumerate(attributes): 155 | att_value = "" # str(ifc_object[att_idx]) 156 | att_type = "" # ifc_object.attribute_type(att_name) 157 | attribute_item0 = QStandardItem(att_name) 158 | attribute_item0.setData(ifc_object, Qt.UserRole) # remember the owner of this attribute 159 | attribute_item0.setData(att_name, Qt.UserRole + 1) # name 160 | attribute_item0.setData(att_value, Qt.UserRole + 2) # value 161 | attribute_item0.setData(att_type, Qt.UserRole + 3) # type 162 | attribute_item0.setData(att_idx, Qt.UserRole + 4) # index 163 | parent_item.appendRow([attribute_item0]) 164 | 165 | inv_attribute_tuple = getattr(ifc_object, att_name) 166 | if len(inv_attribute_tuple) > 0: 167 | for i, nested_att in enumerate(inv_attribute_tuple): 168 | att_class = get_friendly_ifc_name(nested_att) if nested_att is not None else '' 169 | nested_attribute_item0 = QStandardItem("[" + str(i) + "]") 170 | nested_attribute_item1 = QStandardItem(att_class) 171 | # nested_attribute_item1.setToolTip(att_value) 172 | # nested_attribute_item2 = QStandardItem(att_type) 173 | # nested_attribute_item1.setData(nested_att, Qt.UserRole) # remember the owner of this attribute 174 | # nested_attribute_item0.setData('', Qt.UserRole + 1) # name 175 | # nested_attribute_item0.setData('', Qt.UserRole + 2) # value 176 | # nested_attribute_item0.setData(att_class, Qt.UserRole + 3) # type 177 | # nested_attribute_item0.setData(i, Qt.UserRole + 4) # index 178 | attribute_item0.appendRow([nested_attribute_item0, nested_attribute_item1]) 179 | self.add_attributes_in_tree(nested_att, nested_attribute_item0) 180 | 181 | def add_attributes_in_tree(self, ifc_object, parent_item, recursion=0): 182 | """ 183 | Fill the property tree with the attributes of an object. When the "show all" 184 | option is activated, this will enable recursive attribute display. This can 185 | potentially go very deep and take a long time with deep structures or 186 | large geometry. 187 | 188 | :param ifc_object: IFC entity 189 | :param parent_item: QStandardItem used to put attributes underneath 190 | :param recursion: To avoid infinite recursion, the recursion level is checked 191 | """ 192 | for att_idx in range(0, len(ifc_object)): 193 | # https://github.com/jakob-beetz/IfcOpenShellScriptingTutorial/wiki/02:-Inspecting-IFC-instance-objects 194 | att_name = ifc_object.attribute_name(att_idx) 195 | att_value = str(ifc_object[att_idx]) 196 | att_type = ifc_object.attribute_type(att_idx) 197 | if not self.show_all and (att_type == ('ENTITY INSTANCE' or 'AGGREGATE OF ENTITY INSTANCE')): 198 | att_value = '' 199 | # but for properties, we can show a value 200 | if not self.show_all and ifc_object.is_a('IfcPropertySingleValue') and att_name == 'NominalValue': 201 | att_value = str(ifc_object.NominalValue.wrappedValue) 202 | attribute_item0 = QStandardItem(att_name) 203 | attribute_item1 = QStandardItem(att_value) 204 | attribute_item1.setToolTip(att_value) 205 | attribute_item1.setData(ifc_object, Qt.UserRole) # remember the owner of this attribute 206 | attribute_item1.setData(att_name, Qt.UserRole + 1) # name 207 | attribute_item1.setData(att_value, Qt.UserRole + 2) # value 208 | attribute_item1.setData(att_type, Qt.UserRole + 3) # type 209 | attribute_item1.setData(att_idx, Qt.UserRole + 4) # index 210 | if att_name == 'NominalValue': 211 | attribute_item1.setData(ifc_object.NominalValue, Qt.UserRole + 5) # sub_object 212 | attribute_item1.setEditable(True) 213 | attribute_item2 = QStandardItem(att_type) 214 | if att_type in ['STRING', 'DOUBLE', 'INT', 'ENUMERATION']: 215 | attribute_item1.setEditable(True) 216 | if att_type == 'ENUMERATION': 217 | enums = get_enums_from_object(ifc_object, att_name) 218 | attribute_item1.setStatusTip(' - '.join(enums)) 219 | attribute_item1.setToolTip(' - '.join(enums)) 220 | 221 | parent_item.appendRow([attribute_item0, attribute_item1, attribute_item2]) 222 | 223 | # Skip? 224 | if not self.show_all: 225 | if att_name == 'OwnerHistory' or 'Representation' or 'ObjectPlacement': 226 | continue 227 | 228 | # Recursive call to display the attributes of ENTITY INSTANCES and AGGREGATES 229 | attribute = ifc_object[att_idx] 230 | if attribute is not None and recursion < 20: 231 | if att_type == 'ENTITY INSTANCE': 232 | self.add_attributes_in_tree(attribute, attribute_item0, recursion + 1) 233 | if att_type == 'AGGREGATE OF DOUBLE': 234 | attribute_item0.setText(attribute_item0.text() + ' [' + str(len(attribute)) + ']') 235 | for counter, value in enumerate(attribute): 236 | nested_item0 = QStandardItem("[" + str(counter) + "]") 237 | nested_item1 = QStandardItem(str(value)) 238 | nested_item2 = QStandardItem("DOUBLE") 239 | # nested_item1.setData(nested_entity, Qt.UserRole) # remember the owner of this attribute 240 | # nested_item1.setData(att_name, Qt.UserRole + 1) # name 241 | # nested_item1.setData(att_value, Qt.UserRole + 2) # value 242 | # nested_item1.setData(att_type, Qt.UserRole + 3) # type 243 | # nested_item1.setData(att_idx, Qt.UserRole + 4) # index 244 | attribute_item0.appendRow([nested_item0, nested_item1, nested_item2]) 245 | if att_type == 'AGGREGATE OF ENTITY INSTANCE': 246 | attribute_item0.setText(attribute_item0.text() + ' [' + str(len(attribute)) + ']') 247 | for counter, nested_entity in enumerate(attribute): 248 | nested_item0 = QStandardItem("[" + str(counter) + "]") 249 | nested_item1 = QStandardItem(get_friendly_ifc_name(nested_entity)) 250 | nested_item2 = QStandardItem("#" + str(nested_entity.id())) 251 | nested_item1.setData(nested_entity, Qt.UserRole) # remember the owner of this attribute 252 | # nested_item1.setData(att_name, Qt.UserRole + 1) # name 253 | # nested_item1.setData(att_value, Qt.UserRole + 2) # value 254 | # nested_item1.setData(att_type, Qt.UserRole + 3) # type 255 | # nested_item1.setData(att_idx, Qt.UserRole + 4) # index 256 | attribute_item0.appendRow([nested_item0, nested_item1, nested_item2]) 257 | try: 258 | self.add_attributes_in_tree(nested_entity, nested_item0, 259 | recursion + 1) # forced high depth 260 | except: 261 | print('Except nested Entity Instance') 262 | pass 263 | 264 | def add_properties_in_tree(self, property_set, parent_item): 265 | """ 266 | Fill the property tree with the properties of a particular property set 267 | 268 | :param property_set: IfcPropertySet containing individual properties 269 | :param parent_item: QStandardItem used to put properties underneath 270 | """ 271 | for index, prop in enumerate(property_set.HasProperties): 272 | if self.show_all: 273 | prop_item0 = QStandardItem("[" + str(index) + "]") 274 | parent_item.appendRow([prop_item0]) 275 | self.add_attributes_in_tree(prop, prop_item0) 276 | else: 277 | unit = str(prop.Unit) if hasattr(prop, 'Unit') else '' 278 | prop_value = '' 279 | if prop.is_a('IfcPropertySingleValue'): 280 | prop_value = str(prop.NominalValue.wrappedValue) 281 | prop_item0 = QStandardItem(prop.Name) 282 | prop_item1 = QStandardItem(prop_value) 283 | prop_item2 = QStandardItem(unit) 284 | prop_item1.setData(prop, Qt.UserRole) # object 285 | prop_item1.setData(prop.Name, Qt.UserRole + 1) # name 286 | prop_item1.setData(prop_value, Qt.UserRole + 2) # value 287 | prop_item1.setData(unit, Qt.UserRole + 3) # type 288 | prop_item1.setData(index, Qt.UserRole + 4) # index 289 | prop_item1.setData(prop, Qt.UserRole + 5) # sub_object 290 | parent_item.appendRow([prop_item0, prop_item1, prop_item2]) 291 | elif prop.is_a('IfcComplexProperty'): 292 | property_item0 = QStandardItem(prop.Name) 293 | # property_item1 = QStandardItem('') 294 | property_item2 = QStandardItem(unit) 295 | parent_item.appendRow([property_item0, None, property_item2]) 296 | for nested_index, nested_prop in enumerate(prop.HasProperties): 297 | nested_name = nested_prop.Name 298 | nested_value = str(nested_prop.NominalValue.wrappedValue) 299 | nested_unit = str(nested_prop.Unit) if hasattr(nested_prop, 'Unit') else '' 300 | prop_nested_item0 = QStandardItem(nested_name) 301 | prop_nested_item1 = QStandardItem(nested_value) 302 | prop_nested_item2 = QStandardItem(nested_unit) 303 | prop_nested_item1.setData(nested_prop, Qt.UserRole) # object 304 | prop_nested_item1.setData(nested_name, Qt.UserRole + 1) # name 305 | prop_nested_item1.setData(nested_value, Qt.UserRole + 2) # value 306 | prop_nested_item1.setData(nested_unit, Qt.UserRole + 3) # type 307 | prop_nested_item1.setData(nested_index, Qt.UserRole + 4) # index 308 | prop_nested_item1.setData(nested_prop, Qt.UserRole + 5) # sub_object 309 | property_item0.appendRow([prop_nested_item0, prop_nested_item1, prop_nested_item2]) 310 | else: 311 | property_item0 = QStandardItem(prop.Name) 312 | property_item1 = QStandardItem(prop_value) 313 | property_item2 = QStandardItem(unit) 314 | property_item1.setData(prop, Qt.UserRole) 315 | property_item1.setData(prop.Name, Qt.UserRole + 1) # name 316 | property_item1.setData(prop_value, Qt.UserRole + 2) # value 317 | property_item1.setData(unit, Qt.UserRole + 3) # type 318 | property_item1.setData(index, Qt.UserRole + 4) # index 319 | property_item1.setData(prop, Qt.UserRole + 5) # sub_object 320 | parent_item.appendRow([property_item0, property_item1, property_item2]) 321 | 322 | def add_quantities_in_tree(self, quantity_set, parent_item): 323 | """ 324 | Fill the property tree with the quantities 325 | 326 | :param quantity_set: IfcQuantitySet containing individual quantities 327 | :param parent_item: QStandardItem used to put quantities underneath 328 | """ 329 | for quantity in quantity_set.Quantities: 330 | if self.show_all: 331 | self.add_attributes_in_tree(quantity, parent_item) 332 | else: 333 | unit = str(quantity.Unit) if hasattr(quantity, 'Unit') else '' 334 | quantity_value = '' 335 | if quantity.is_a('IfcQuantityLength'): 336 | quantity_value = str(quantity.LengthValue) 337 | elif quantity.is_a('IfcQuantityArea'): 338 | quantity_value = str(quantity.AreaValue) 339 | elif quantity.is_a('IfcQuantityVolume'): 340 | quantity_value = str(quantity.VolumeValue) 341 | elif quantity.is_a('IfcQuantityCount'): 342 | quantity_value = str(quantity.CountValue) 343 | 344 | prop_item0 = QStandardItem(quantity.Name) 345 | prop_item1 = QStandardItem(quantity_value) 346 | prop_item2 = QStandardItem(unit) 347 | parent_item.appendRow([prop_item0, prop_item1, prop_item2]) 348 | 349 | def add_file_header(self, ifc_file): 350 | """ 351 | Fill the property tree with the Header data from the file(s) 352 | which contains the current selected entities 353 | """ 354 | self.loaded_objects_and_files.append(ifc_file) 355 | header_item = QStandardItem("Header") 356 | header_item.setData(ifc_file, Qt.UserRole) 357 | self.model.invisibleRootItem().appendRow([header_item]) 358 | 359 | header = ifc_file.wrapped_data.header 360 | FILE_DESCRIPTION_item = QStandardItem("FILE_DESCRIPTION") 361 | header_item.appendRow([FILE_DESCRIPTION_item]) 362 | for desc in header.file_description.description: 363 | # desc = ...[...:...]" 364 | key = desc.split("[")[0] 365 | description = desc[len(key) + 1:-1] 366 | tree_item1 = QStandardItem(key) 367 | tree_item2 = QStandardItem(description) 368 | tree_item1.setToolTip(description) 369 | FILE_DESCRIPTION_item.appendRow([tree_item1, tree_item2]) 370 | FILE_DESCRIPTION_item.appendRow( 371 | [QStandardItem("implementation_level"), 372 | QStandardItem(str(header.file_description.implementation_level))]) 373 | 374 | FILE_NAME_item = QStandardItem("FILE_NAME") 375 | header_item.appendRow([FILE_NAME_item]) 376 | FILE_NAME_item.appendRow([QStandardItem("name"), QStandardItem(str(header.file_name.name))]) 377 | FILE_NAME_item.appendRow([QStandardItem("time_stamp"), QStandardItem(str(header.file_name.time_stamp))]) 378 | for author in header.file_name.author: 379 | FILE_NAME_item.appendRow([QStandardItem("author"), QStandardItem(str(author))]) 380 | for organization in header.file_name.organization: 381 | FILE_NAME_item.appendRow([QStandardItem("organization"), QStandardItem(str(organization))]) 382 | FILE_NAME_item.appendRow( 383 | [QStandardItem("preprocessor_version"), QStandardItem(str(header.file_name.preprocessor_version))]) 384 | FILE_NAME_item.appendRow( 385 | [QStandardItem("originating_system"), QStandardItem(str(header.file_name.originating_system))]) 386 | FILE_NAME_item.appendRow([QStandardItem("authorization"), QStandardItem(str(header.file_name.authorization))]) 387 | 388 | FILE_SCHEMA_item = QStandardItem("FILE_SCHEMA") 389 | header_item.appendRow([FILE_SCHEMA_item]) 390 | for schema_identifiers in header.file_schema.schema_identifiers: 391 | FILE_SCHEMA_item.appendRow( 392 | [QStandardItem("schema_identifiers"), QStandardItem(str(schema_identifiers))]) 393 | 394 | self.property_tree.expandAll() 395 | 396 | def add_object_data(self, ifc_object): 397 | """ 398 | Fill the property tree with all data from object 399 | 400 | :param ifc_object: The IFC Entity instance to show 401 | """ 402 | self.loaded_objects_and_files.append(ifc_object) 403 | 404 | # Attributes 405 | if self.follow_attributes: 406 | attributes_item0 = QStandardItem("Attributes [" + str(len(ifc_object)) + "]") 407 | attributes_item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 408 | self.model.invisibleRootItem().appendRow([attributes_item0, attributes_item1]) 409 | self.add_attributes_in_tree(ifc_object, attributes_item0) 410 | 411 | if self.follow_inverse_attributes: 412 | inv_attributes_item0 =\ 413 | QStandardItem("Inverse Attributes [" 414 | + str(len(ifc_object.wrapped_data.get_inverse_attribute_names())) + "]") 415 | inv_attributes_item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 416 | self.model.invisibleRootItem().appendRow([inv_attributes_item0, inv_attributes_item1]) 417 | self.add_inverse_attributes_in_tree(ifc_object, inv_attributes_item0) 418 | 419 | # Has Assignments 420 | if self.follow_assignments and hasattr(ifc_object, 'HasAssignments'): 421 | buffer = "HasAssignments [" + str(len(ifc_object.HasAssignments)) + "]" 422 | assignments_item0 = QStandardItem(buffer) 423 | assignments_item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 424 | self.model.invisibleRootItem().appendRow([assignments_item0, assignments_item1]) 425 | counter = 0 426 | for assignment in ifc_object.HasAssignments: 427 | ass_name = assignment.Name if assignment.Name is not None else '[' + str(counter) + ']' 428 | ass_item0 = QStandardItem(ass_name) 429 | ass_item0.setData(assignment, Qt.UserRole) 430 | # att_name = index.data(Qt.UserRole + 1) 431 | # att_value = index.data(Qt.UserRole + 2) 432 | # att_type = index.data(Qt.UserRole + 3) 433 | # att_index = index.data(Qt.UserRole + 4) 434 | # sub_object = index.data(Qt.UserRole + 5) 435 | ass_item1 = QStandardItem(get_friendly_ifc_name(assignment)) 436 | ass_item2 = QStandardItem(assignment.GlobalId) 437 | assignments_item0.appendRow([ass_item0, ass_item1, ass_item2]) 438 | self.add_attributes_in_tree(assignment, ass_item0) 439 | counter += 1 440 | 441 | # Defined By (for type, properties & quantities) 442 | # using attributes (show all) 443 | if self.show_all and (self.follow_defines or self.follow_properties) and hasattr(ifc_object, 444 | 'IsDefinedBy'): 445 | item0 = QStandardItem("IsDefinedBy [" + str(len(ifc_object.IsDefinedBy)) + "]") 446 | item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 447 | self.model.invisibleRootItem().appendRow([item0, item1]) 448 | counter = 0 449 | for definition in ifc_object.IsDefinedBy: 450 | if definition.is_a('IfcRelDefinesByProperties') and not self.follow_properties: 451 | continue 452 | if definition.is_a('IfcRelDefinesByType') and not self.follow_defines: 453 | continue 454 | def_name = definition.Name if definition.Name is not None else '[' + str(counter) + ']' 455 | def_item0 = QStandardItem(def_name) 456 | def_item0.setData(definition, Qt.UserRole) 457 | # att_name = index.data(Qt.UserRole + 1) 458 | # att_value = index.data(Qt.UserRole + 2) 459 | # att_type = index.data(Qt.UserRole + 3) 460 | # att_index = index.data(Qt.UserRole + 4) 461 | # sub_object = index.data(Qt.UserRole + 5) 462 | def_item1 = QStandardItem(get_friendly_ifc_name(definition)) 463 | def_item2 = QStandardItem(definition.GlobalId) 464 | item0.appendRow([def_item0, def_item1, def_item2]) 465 | self.add_attributes_in_tree(definition, def_item0) 466 | counter += 1 467 | # more streamlined display 468 | if not self.show_all and (self.follow_defines or self.follow_properties) and hasattr(ifc_object, 'IsDefinedBy'): 469 | buffer = "IsDefinedBy [" + str(len(ifc_object.IsDefinedBy)) + "]" 470 | defines_item0 = QStandardItem(buffer) 471 | defines_item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 472 | self.model.invisibleRootItem().appendRow([defines_item0, defines_item1]) 473 | for definition in ifc_object.IsDefinedBy: 474 | if self.follow_defines and definition.is_a('IfcRelDefinesByType'): 475 | type_object = definition.RelatingType 476 | s = get_friendly_ifc_name(type_object) 477 | type_item0 = QStandardItem(type_object.Name) 478 | type_item0.setData(type_item0, Qt.UserRole) 479 | # att_name = index.data(Qt.UserRole + 1) 480 | # att_value = index.data(Qt.UserRole + 2) 481 | # att_type = index.data(Qt.UserRole + 3) 482 | # att_index = index.data(Qt.UserRole + 4) 483 | type_item1 = QStandardItem(s) 484 | type_item2 = QStandardItem(type_object.GlobalId) 485 | type_item0.setData(type_object, Qt.UserRole) 486 | defines_item0.appendRow([type_item0, type_item1, type_item2]) 487 | if self.follow_properties and definition.is_a('IfcRelDefinesByProperties'): 488 | property_set = definition.RelatingPropertyDefinition 489 | prop_item0 = QStandardItem(property_set.Name) 490 | prop_item0.setData(property_set, Qt.UserRole) 491 | # att_name = index.data(Qt.UserRole + 1) 492 | # att_value = index.data(Qt.UserRole + 2) 493 | # att_type = index.data(Qt.UserRole + 3) 494 | # att_index = index.data(Qt.UserRole + 4) 495 | prop_item1 = QStandardItem(get_friendly_ifc_name(property_set)) 496 | prop_item2 = QStandardItem(property_set.GlobalId) 497 | defines_item0.appendRow([prop_item0, prop_item1, prop_item2]) 498 | # the individual properties/quantities 499 | if property_set.is_a('IfcPropertySet'): 500 | self.add_properties_in_tree(property_set, prop_item0) 501 | elif property_set.is_a('IfcElementQuantity'): 502 | self.add_quantities_in_tree(property_set, prop_item0) 503 | if self.follow_properties and hasattr(ifc_object, 'HasPropertySets'): 504 | buffer = "HasPropertySets [" + str(len(ifc_object.HasPropertySets)) + "]" 505 | defines_item0 = QStandardItem(buffer) 506 | defines_item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 507 | self.model.invisibleRootItem().appendRow([defines_item0, defines_item1]) 508 | for property_set in ifc_object.HasPropertySets: 509 | prop_item0 = QStandardItem(property_set.Name) 510 | prop_item0.setData(property_set, Qt.UserRole) 511 | prop_item1 = QStandardItem(get_friendly_ifc_name(property_set)) 512 | prop_item2 = QStandardItem(property_set.GlobalId) 513 | defines_item0.appendRow([prop_item0, prop_item1, prop_item2]) 514 | self.add_properties_in_tree(property_set, prop_item0) 515 | 516 | # Associations (Materials, Classification, ...) 517 | if self.follow_associations and hasattr(ifc_object, 'HasAssociations'): 518 | item0 = QStandardItem("Associations [" + str(len(ifc_object.HasAssociations)) + "]") 519 | item1 = QStandardItem(get_friendly_ifc_name(ifc_object)) 520 | self.model.invisibleRootItem().appendRow([item0, item1]) 521 | for association in ifc_object.HasAssociations: 522 | def_item0 = QStandardItem(association.Name) 523 | def_item0.setData(association, Qt.UserRole) 524 | # att_name = index.data(Qt.UserRole + 1) 525 | # att_value = index.data(Qt.UserRole + 2) 526 | # att_type = index.data(Qt.UserRole + 3) 527 | # att_index = index.data(Qt.UserRole + 4) 528 | def_item1 = QStandardItem(get_friendly_ifc_name(association)) 529 | def_item2 = QStandardItem(association.GlobalId) 530 | item0.appendRow([def_item0, def_item1, def_item2]) 531 | # and one step deeper 532 | if association.is_a('IfcRelAssociatesMaterial'): 533 | relating_material = association.RelatingMaterial 534 | # other (non Root) entities 535 | if not self.show_all: # streamlined display 536 | if relating_material.is_a('IfcMaterialList'): 537 | for mat in relating_material.Materials: 538 | mat_item0 = QStandardItem(mat.Name) 539 | mat_item0.setData(mat, Qt.UserRole) 540 | mat_item1 = QStandardItem(get_friendly_ifc_name(mat)) 541 | mat_item2 = QStandardItem(str('#' + str(mat.id()))) 542 | def_item0.appendRow([mat_item0, mat_item1, mat_item2]) 543 | self.add_attributes_in_tree(mat, mat_item0) 544 | elif relating_material.is_a('IfcMaterialLayerSet'): 545 | # self.add_attributes_in_tree(relating_material, item) 546 | for layer in relating_material.MaterialLayers: 547 | layer_name = layer.Name if hasattr(layer, 'Name') else '' 548 | mat_item0 = QStandardItem(layer_name) 549 | mat_item0.setData(layer, Qt.UserRole) 550 | mat_item1 = QStandardItem(get_friendly_ifc_name(layer)) 551 | mat_item2 = QStandardItem(str('#' + str(layer.id()))) 552 | def_item0.appendRow([mat_item0, mat_item1, mat_item2]) 553 | self.add_attributes_in_tree(layer, mat_item0) 554 | else: 555 | self.add_attributes_in_tree(relating_material, def_item0) 556 | else: # generic attributes following 557 | self.add_attributes_in_tree(relating_material, def_item0) 558 | elif association.is_a('IfcRelAssociatesClassification'): 559 | relating_classification = association.RelatingClassification 560 | self.add_attributes_in_tree(relating_classification, def_item0) 561 | 562 | self.property_tree.expandAll() 563 | 564 | # endregion 565 | 566 | # region Configuring the tree 567 | 568 | def reset(self): 569 | w0 = 200 570 | w1 = 200 571 | w2 = 50 572 | if self.model is not None: 573 | w0 = self.property_tree.columnWidth(0) 574 | w1 = self.property_tree.columnWidth(1) 575 | w2 = self.property_tree.columnWidth(2) 576 | self.model.clear() 577 | self.model = QStandardItemModel(0, 3, self) 578 | self.property_tree.setModel(self.model) 579 | self.property_tree.setColumnWidth(0, w0) 580 | self.property_tree.setColumnWidth(1, w1) 581 | self.property_tree.setColumnWidth(2, w2) 582 | self.model.setHeaderData(0, Qt.Horizontal, "Name") 583 | self.model.setHeaderData(1, Qt.Horizontal, "Value") 584 | self.model.setHeaderData(2, Qt.Horizontal, "ID/Type") 585 | self.loaded_objects_and_files.clear() 586 | 587 | def regenerate(self): 588 | buffer_list = self.loaded_objects_and_files[:] # copy items in new list 589 | self.reset() 590 | for item in buffer_list: 591 | # if file 592 | if hasattr(item, "id"): 593 | self.add_object_data(item) 594 | else: 595 | self.add_file_header(item) 596 | 597 | def toggle_attributes(self): 598 | self.follow_attributes = not self.follow_attributes 599 | self.regenerate() 600 | 601 | def toggle_inverse_attributes(self): 602 | self.follow_inverse_attributes = not self.follow_inverse_attributes 603 | self.regenerate() 604 | 605 | def toggle_properties(self): 606 | self.follow_properties = not self.follow_properties 607 | self.regenerate() 608 | 609 | def toggle_associations(self): 610 | self.follow_associations = not self.follow_associations 611 | self.regenerate() 612 | 613 | def toggle_assignments(self): 614 | self.follow_assignments = not self.follow_assignments 615 | self.regenerate() 616 | 617 | def toggle_defines(self): 618 | self.follow_defines = not self.follow_defines 619 | self.regenerate() 620 | 621 | def toggle_show_all(self): 622 | self.show_all = not self.show_all 623 | self.regenerate() 624 | 625 | # endregion 626 | 627 | 628 | if __name__ == '__main__': 629 | app = 0 630 | if QApplication.instance(): 631 | app = QApplication.instance() 632 | else: 633 | app = QApplication(sys.argv) 634 | 635 | w = IFCPropertyWidget() 636 | w.resize(600, 800) 637 | filename = sys.argv[1] 638 | if os.path.isfile(filename): 639 | ifc_file = ifcopenshell.open(filename) 640 | w.add_file_header(ifc_file) 641 | entities = ifc_file.by_type('IfcProject') 642 | for entity in entities: 643 | w.add_object_data(entity) 644 | w.show() 645 | sys.exit(app.exec_()) 646 | -------------------------------------------------------------------------------- /Viewer/IFCQt3DView.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | import sys 3 | import time 4 | import os.path 5 | import struct 6 | import multiprocessing 7 | 8 | try: 9 | from PyQt5.QtCore import * 10 | from PyQt5.QtGui import * 11 | from PyQt5.QtWidgets import * 12 | from PyQt5.Qt3DCore import * 13 | from PyQt5.Qt3DExtras import * 14 | from PyQt5.Qt3DRender import * 15 | except Exception: 16 | from PySide2.QtGui import * 17 | from PySide2.QtCore import * 18 | from PySide2.QtWidgets import * 19 | from PySide2.Qt3DCore import * 20 | from PySide2.Qt3DExtras import * 21 | from PySide2.Qt3DRender import * 22 | 23 | 24 | import ifcopenshell 25 | import ifcopenshell.geom 26 | # https://github.com/IfcOpenShell/IfcOpenShell/blob/master/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py#L147 27 | import ifcopenshell.geom.occ_utils 28 | import OCC 29 | import OCC.Core.gp 30 | import OCC.Core.Geom 31 | import OCC.Core.AIS 32 | 33 | import OCC.Core.Bnd 34 | import OCC.Core.BRepBndLib 35 | 36 | import OCC.Core.BRep 37 | import OCC.Core.BRepPrimAPI 38 | import OCC.Core.BRepAlgoAPI 39 | import OCC.Core.BRepBuilderAPI 40 | 41 | import OCC.Core.GProp 42 | import OCC.Core.BRepGProp 43 | 44 | import OCC.Core.TopoDS 45 | import OCC.Core.TopExp 46 | import OCC.Core.TopAbs 47 | 48 | from OCC.Core.Tesselator import ShapeTesselator 49 | 50 | from collections import namedtuple 51 | shape_tuple = namedtuple("shape_tuple", ("data", "geometry", "styles", "style_ids")) 52 | 53 | # endregion 54 | 55 | 56 | class IFCQt3dView(QWidget): 57 | """ 58 | 3D View Widget 59 | - V1 = IFC File Loading, geometry parsing & basic navigation 60 | - V2 = Adding Edges display 61 | - V3 = Draw Origin Axes & Grid 62 | - V4 = Object Picking & Selection Syncing (+ reorganise scenegraph) 63 | - V5 = Working with multiple files (+ reorganise scenegraph again) 64 | - V6 = Revised Wireframe setup, improved highlights, select also in scenegraph 65 | """ 66 | 67 | # Two signals to extend or shrink the selection 68 | add_to_selected_entities = pyqtSignal(str) 69 | remove_from_selected_entities = pyqtSignal(str) 70 | 71 | # region Initialisation 72 | 73 | def __init__(self): 74 | QWidget.__init__(self) 75 | 76 | # variables 77 | self.ifc_files = {} # from filename to IFC model 78 | self.model_nodes = {} # from filename to QEntity node 79 | self.start = time.time() 80 | 81 | # 3D View 82 | self.view = Qt3DWindow() 83 | self.view.defaultFrameGraph().setClearColor(QColor("#4466ff")) 84 | self.container = self.createWindowContainer(self.view) 85 | self.container.setMinimumSize(QSize(200, 100)) 86 | self.container.setFocusPolicy(Qt.NoFocus) 87 | 88 | # Prepare our scene 89 | self.root = QEntity() 90 | self.root.setObjectName("Root") 91 | self.scene = QEntity() 92 | self.scene.setObjectName("Scene") 93 | self.scene.setParent(self.root) 94 | self.grids = QEntity() 95 | self.grids.setObjectName("Grids") 96 | self.grids.setParent(self.scene) 97 | self.grids.setProperty("IsProduct", True) 98 | self.display_edges = True 99 | self.display_meshes = True 100 | 101 | self.files = QEntity() 102 | self.files.setObjectName("Models") 103 | self.files.setProperty("IsProduct", True) 104 | self.files.setParent(self.root) 105 | 106 | # Selection List & Shared Materials 107 | self.materials = QEntity() 108 | self.materials.setObjectName("Materials") 109 | self.materials.setProperty("IsProduct", True) 110 | self.materials.setParent(self.scene) 111 | self.selected = [] 112 | self.mat_highlight = QGoochMaterial() 113 | self.mat_highlight.setObjectName("Shared Highlight Material") 114 | self.mat_highlight.setShareable(True) 115 | self.mat_highlight.setDiffuse(QColor(50, 250, 50)) 116 | self.mat_highlight.setShininess(0.5) 117 | self.materials.addComponent(self.mat_highlight) 118 | 119 | self.material = QPerVertexColorMaterial() 120 | self.material.setObjectName("Shared Vertex Color Material") 121 | self.material.setShareable(True) 122 | self.materials.addComponent(self.material) 123 | 124 | self.transparent = QDiffuseSpecularMaterial() 125 | self.transparent.setObjectName("Shared Transparent Material") 126 | self.transparent.setShareable(True) 127 | self.transparent.setAlphaBlendingEnabled(True) 128 | self.transparent.setDiffuse(QColor(230, 230, 250, 150)) 129 | self.materials.addComponent(self.transparent) 130 | 131 | self.edge_material = QDiffuseSpecularMaterial() 132 | self.edge_material.setObjectName("Shared Lines Material") 133 | self.edge_material.setShareable(True) 134 | self.edge_material.setDiffuse(QColor(50, 50, 50)) 135 | self.materials.addComponent(self.edge_material) 136 | 137 | self.camera = None 138 | self.cam_controller = None 139 | self.initialise_camera() 140 | self.create_light() 141 | self.view.setRootEntity(self.root) 142 | 143 | # Axes & Grid 144 | self.generate_axis(5) 145 | self.generate_grid(10) 146 | 147 | # Scene Graph 148 | self.scene_graph = QTreeWidget() 149 | self.scene_graph.setColumnCount(2) 150 | self.scene_graph.setHeaderLabels(["Object Name", "Class"]) 151 | # self.scene_graph.selectionModel().selectionChanged.connect(self.toggle_visibility) 152 | # self.scene_graph.itemChanged.connect(self.toggle_visibility) # is emitted on almost everything! 153 | self.scene_graph.itemChanged[QTreeWidgetItem, int].connect(self.toggle_visibility) 154 | # self.scene_graph.itemPressed.connect(self.toggle_visibility) 155 | 156 | # picking 157 | self.picking_sphere = None 158 | picking_settings = self.view.renderSettings().pickingSettings() 159 | self.picker = QObjectPicker(self.scene) 160 | self.picker.setObjectName("Picker") 161 | self.picker.setProperty("IsProduct", True) 162 | picking_settings.setFaceOrientationPickingMode(QPickingSettings.FrontAndBackFace) 163 | # set QObjectPicker to PointPicking: 164 | picking_settings.setPickMethod(QPickingSettings.TrianglePicking) 165 | # picking_settings.setPickMethod(QPickingSettings.LinePicking) 166 | # picking_settings.setPickMethod(QPickingSettings.PointPicking) 167 | picking_settings.setPickResultMode(QPickingSettings.NearestPick) 168 | # picking_settings.setWorldSpaceTolerance(.5) 169 | # self.picker.setHoverEnabled(True) 170 | # self.picker.setDragEnabled(True) 171 | # self.picker.moved.connect(self.pick) 172 | # self.picker.pressed.connect(self.pick) 173 | self.picker.clicked.connect(self.pick) 174 | # self.picker.released.connect(self.pick) 175 | self.root.addComponent(self.picker) 176 | 177 | # Finish GUI 178 | layout = QHBoxLayout() 179 | # Splitter 180 | splitter = QSplitter(Qt.Horizontal) 181 | layout.addWidget(splitter) 182 | splitter.addWidget(self.container) 183 | # splitter.addWidget(self.scene_graph) 184 | 185 | vbox = QVBoxLayout() 186 | # Series of buttons and check boxes in a horizontal layout 187 | buttons = QWidget() 188 | hbox = QHBoxLayout() 189 | hbox.setContentsMargins(0, 0, 0, 0) 190 | buttons.setLayout(hbox) 191 | # Add buttons 192 | # Button : Reset Camera 193 | btn_camera_reset = QPushButton("Reset") 194 | btn_camera_reset.setToolTip("Reset the camera position") 195 | btn_camera_reset.pressed.connect(self.reset_camera) 196 | hbox.addWidget(btn_camera_reset) 197 | # Button : Store Camera 198 | btn_camera_store = QPushButton("Store") 199 | btn_camera_store.setToolTip("Store the camera position") 200 | btn_camera_store.pressed.connect(self.store_camera) 201 | hbox.addWidget(btn_camera_store) 202 | # Button : Rotate Camera 203 | btn_camera_rotate = QPushButton("Rotate") 204 | btn_camera_rotate.setToolTip("Turntable 30° around pick point") 205 | btn_camera_rotate.pressed.connect(self.rotate_around_position) 206 | hbox.addWidget(btn_camera_rotate) 207 | self.pick_position = QVector3D(0, 0, 0) 208 | # Button : Update Tree 209 | btn_update_tree = QPushButton("Update") 210 | btn_update_tree.setToolTip("Update Scenegraph Tree") 211 | btn_update_tree.pressed.connect(self.update_scene_graph_tree) 212 | hbox.addWidget(btn_update_tree) 213 | # Option: Display Meshes 214 | check_ms = QCheckBox("Meshes") 215 | check_ms.setToolTip("Display all meshes") 216 | check_ms.setChecked(self.display_meshes) 217 | check_ms.toggled.connect(self.toggle_meshes) 218 | hbox.addWidget(check_ms) 219 | # Option: Display Wireframe 220 | check_wf = QCheckBox("Wireframe") 221 | check_wf.setToolTip("Display all edges") 222 | check_wf.setChecked(self.display_edges) 223 | check_wf.toggled.connect(self.toggle_wireframe) 224 | hbox.addWidget(check_wf) 225 | btn_show_wireframe = QCheckBox("Wireframe") 226 | btn_camera_store.setToolTip("Store the camera position") 227 | btn_camera_store.pressed.connect(self.store_camera) 228 | hbox.addWidget(btn_camera_store) 229 | # Stretchable Spacer 230 | spacer = QSpacerItem(10, 10, QSizePolicy.Expanding) 231 | hbox.addSpacerItem(spacer) 232 | # Add Scenegraph 233 | scenegraph = QWidget() 234 | scenegraph.setLayout(vbox) 235 | vbox.addWidget(buttons) 236 | vbox.addWidget(self.scene_graph) 237 | splitter.addWidget(scenegraph) 238 | 239 | self.setLayout(layout) 240 | 241 | # endregion 242 | 243 | # region SelectionMethods 244 | 245 | def select_object_by_id(self, object_id): 246 | print("IFCQt3dView.select_object_by_id ", object_id) 247 | for f in self.files.children(): 248 | for e in f.children(): 249 | if e.objectName() == object_id: 250 | self.selected.append(e) 251 | self.set_highlight(e) 252 | return 253 | 254 | def deselect_object_by_id(self, object_id): 255 | print("IFCQt3dView.deselect_object_by_id ", object_id) 256 | for f in self.files.children(): 257 | for e in f.children(): 258 | if e.objectName() == object_id: 259 | self.set_highlight(e, False) 260 | if e in self.selected: 261 | self.selected.remove(e) 262 | return 263 | 264 | def toggle_entity(self, entity): 265 | print("IFCQt3dView.toggle_entity ", entity.objectName()) 266 | if entity in self.selected: 267 | self.selected.remove(entity) 268 | self.set_highlight(entity, False) 269 | else: 270 | self.selected.append(entity) 271 | self.set_highlight(entity) 272 | 273 | def set_highlight(self, entity, on=True): 274 | """ 275 | Set the given QEntity to the highlight material 276 | 277 | :param entity: the geometry to highlight 278 | :type entity: QEntity 279 | :param on: True = set highlight, False = remove 280 | :param on: bool 281 | """ 282 | # Switch the Material from our Mesh Child 283 | if on is False: 284 | for c in entity.children(): 285 | c.removeComponent(self.mat_highlight) 286 | if c.property("IsTransparent") is True: 287 | c.addComponent(self.transparent) 288 | else: 289 | c.addComponent(self.material) 290 | self.highlight_in_scene_graph(entity, False) 291 | else: 292 | for c in entity.children(): 293 | if c.property("IsTransparent") is True: 294 | c.removeComponent(self.transparent) 295 | else: 296 | c.removeComponent(self.material) 297 | if c.property("IsWireframe") is True: 298 | c.addComponent(self.material) 299 | else: 300 | c.addComponent(self.mat_highlight) 301 | self.highlight_in_scene_graph(entity) 302 | 303 | def select_exclusive_entity(self, entity): 304 | print("IFCQt3dView.select_exclusive_entity ", entity) 305 | for e in self.selected: 306 | if e is not entity: 307 | self.set_highlight(e, False) 308 | self.remove_from_selected_entities.emit(e.objectName()) 309 | self.selected.clear() 310 | self.selected.append(entity) 311 | self.set_highlight(entity) 312 | self.add_to_selected_entities.emit(entity.objectName()) 313 | 314 | def pick(self, e: QPickTriangleEvent): 315 | position = e.position() # screen space 316 | localPosition = e.localIntersection() # model space 317 | worldPosition = e.worldIntersection() # world space QVector3D 318 | self.pick_position = worldPosition 319 | 320 | entity = e.entity() 321 | if entity is None: 322 | return 323 | # Picked mesh is child of container entity "parent" 324 | parent = entity.parentEntity() 325 | GlobalId = parent.objectName() 326 | print("IFCQt3dView.pick (" + GlobalId + ")") 327 | 328 | if e.button() == Qt.LeftButton and e.modifiers() == Qt.ControlModifier: 329 | self.toggle_entity(parent) 330 | else: 331 | if e.button() == Qt.LeftButton and e.modifiers() == Qt.ShiftModifier: 332 | # up = self.view.camera().upVector() 333 | self.view.camera().setViewCenter(worldPosition) 334 | self.view.camera().setUpVector(QVector3D(0, 1, 0)) 335 | 336 | # Place the picking sphere at the Pick position 337 | if False: 338 | if self.picking_sphere is None: 339 | self.picking_sphere = QEntity(self.scene) 340 | self.picking_sphere.setObjectName("Picking Sphere") 341 | material = QPhongMaterial() 342 | material.setAmbient(QColor(100, 50, 50)) 343 | material.setDiffuse(QColor(200, 150, 150)) 344 | self.picking_sphere.addComponent(material) 345 | sphere_mesh = QSphereMesh() 346 | sphere_mesh.setRadius(0.1) 347 | self.picking_sphere.addComponent(sphere_mesh) 348 | sphere_position = QTransform() 349 | sphere_position.setTranslation(worldPosition) 350 | self.picking_sphere.addComponent(sphere_position) 351 | # self.generate_axis(5, worldPosition) 352 | elif e.button() == Qt.LeftButton: 353 | self.select_exclusive_entity(parent) 354 | 355 | # endregion 356 | 357 | def toggle_meshes(self): 358 | # Parse the whole Scenegraph and hide all branches with a Triangle Renderer 359 | 360 | # iterate over children 361 | for filename, model in self.model_nodes.items(): 362 | for ifc_entity in model.children(): 363 | for representation in ifc_entity.children(): 364 | # self.update_scene_graph_tree(item, node_item) 365 | if representation.property("IsWireframe") is None: 366 | representation.setEnabled(not representation.isEnabled()) 367 | 368 | def toggle_wireframe(self, enabled=True): 369 | # Parse the whole Scenegraph and hide all branches with a Lines Renderer 370 | 371 | # iterate over children 372 | for filename, model in self.model_nodes.items(): 373 | for ifc_entity in model.children(): 374 | for representation in ifc_entity.children(): 375 | # self.update_scene_graph_tree(item, node_item) 376 | if representation.property("IsWireframe") is True: 377 | representation.setEnabled(not representation.isEnabled()) 378 | 379 | # region SceneMethods 380 | 381 | def reset_camera(self): 382 | # self.cam_index = 0 383 | # self.cameras = [] 384 | # controlled_cam = self.cam_controller.camera() 385 | view_cam = self.view.camera() 386 | # view_cam.lens().setPerspectiveProjection(45.0, 16.0 / 9.0, 0.1, 200) # 16.0 / 9.0, 0.1, 200) 387 | # view_cam.setPosition(QVector3D(0, 0, 40)) 388 | # view_cam.setViewCenter(QVector3D(0, 0, 0)) 389 | # view_cam.setUpVector(QVector3D(0, 1, 0)) 390 | # view_cam.setFieldOfView(45.0) 391 | 392 | view_cam.setPosition(self.cam_pos) 393 | view_cam.setViewCenter(self.cam_viewcenter) 394 | view_cam.setFieldOfView(self.cam_fieldofview) 395 | view_cam.setUpVector(self.cam_upvector) 396 | 397 | # self.cameras.append(view_cam) 398 | # cam_ortho = QCamera() 399 | # cam_ortho.lens().setOrthographicProjection(5.0, 5.0, 5.0, 5.0, 0.0, 200.0) 400 | # cam_ortho.setPosition(QVector3D(30, 30, 30)) 401 | # cam_ortho.setViewCenter(QVector3D(0, 0, 0)) 402 | # self.cameras.append(cam_ortho) 403 | # 404 | # self.camera = self.cameras[1] 405 | 406 | def store_camera(self): 407 | # capture current camera in a list 408 | camera = self.view.camera() 409 | self.cam_pos = camera.position() 410 | self.cam_viewcenter = camera.viewCenter() 411 | self.cam_viewvector = camera.viewVector() 412 | self.cam_fieldofview = camera.fieldOfView() 413 | self.cam_upvector = camera.upVector() 414 | pass 415 | 416 | def rotate_around_position(self, position=QVector3D(0,0,0), angle=30, rotation_axis=QVector3D(0,1,0)): 417 | # capture current matrix 418 | matrix = self.view.camera().transform().matrix() 419 | # move to position 420 | matrix.translate(-self.pick_position) 421 | # do the rotation 422 | matrix.rotate(30, QVector3D(0, 1, 0)) 423 | # translate back 424 | matrix.translate(self.pick_position) 425 | # apply matrix 426 | self.view.camera().transform().setMatrix(matrix) 427 | pass 428 | 429 | def initialise_camera(self): 430 | # camera 431 | if self.camera is None: 432 | self.camera = self.view.camera() 433 | self.camera.setObjectName("Camera") 434 | ratio = self.view.width() / self.view.height() 435 | self.camera.lens().setPerspectiveProjection(45.0, ratio, 0.1, 200) # 16.0 / 9.0, 0.1, 200) 436 | self.camera.setPosition(QVector3D(0, 0, 40)) 437 | self.camera.setViewCenter(QVector3D(0, 0, 0)) 438 | self.camera.setUpVector(QVector3D(0, 1, 0)) 439 | self.camera.setFieldOfView(45.0) 440 | 441 | self.cam_pos = self.camera.position() 442 | self.cam_viewcenter = self.camera.viewCenter() 443 | self.cam_viewvector = self.camera.viewVector() 444 | self.cam_fieldofview = self.camera.fieldOfView() 445 | self.cam_upvector = self.camera.upVector() 446 | 447 | # for camera control 448 | if self.cam_controller is None: 449 | self.cam_controller = QOrbitCameraController(self.scene) 450 | # self.cam_controller = QFirstPersonCameraController(self.scene) 451 | self.cam_controller.setObjectName("Orbit Camera Controller") 452 | self.cam_controller.setLinearSpeed(50.0) 453 | self.cam_controller.setLookSpeed(180.0) 454 | self.cam_controller.setCamera(self.camera) 455 | 456 | def create_light(self): 457 | # Light 458 | self.lights = QEntity(self.scene) 459 | self.lights.setObjectName("Lights") 460 | self.lights.setProperty("IsProduct", True) 461 | 462 | # Light 1 463 | light_entity = QEntity(self.lights) 464 | light_entity.setObjectName("Light Entity 1") 465 | light_entity.setProperty("IsProduct", True) 466 | light = QPointLight(light_entity) 467 | light.setObjectName("Point Light") 468 | light.setColor(QColor.fromRgbF(1.0, 1.0, 1.0, 1.0)) 469 | light.setIntensity(1) 470 | light_entity.addComponent(light) 471 | light_transform = QTransform(light_entity) 472 | light_transform.setObjectName("Light Transform") 473 | light_transform.setTranslation(QVector3D(10.0, 40.0, 0.0)) 474 | light_entity.addComponent(light_transform) 475 | 476 | # Light 2 477 | light_entity2 = QEntity(self.lights) 478 | light_entity2.setObjectName("Light Entity 2") 479 | light_entity2.setProperty("IsProduct", True) 480 | light2 = QPointLight(light_entity2) 481 | light2.setObjectName("Point Light") 482 | light2.setColor(QColor.fromRgbF(0.8, 0.8, 1.0, 1.0)) 483 | light2.setIntensity(1) 484 | light_entity2.addComponent(light2) 485 | light_transform2 = QTransform(light_entity2) 486 | light_transform2.setObjectName("Light Transform") 487 | light_transform2.setTranslation(QVector3D(10.0, -40.0, 0.0)) 488 | light_entity2.addComponent(light_transform2) 489 | 490 | # endregion 491 | 492 | # region FileMethods 493 | 494 | def close_files(self): 495 | for child in self.files.children(): 496 | child.setParent(None) 497 | self.update_scene_graph_tree() 498 | self.model_nodes.clear() 499 | self.selected.clear() 500 | pass 501 | 502 | def load_file(self, filename): 503 | """ 504 | Load the file passed as filename and generates the geometry. 505 | If it already exists, the geometry is removed and recreated. 506 | 507 | :param filename: Full path to the IFC file 508 | """ 509 | ifc_file = None 510 | if filename in self.ifc_files: 511 | ifc_file = self.ifc_files[filename] 512 | else: # Load as new file 513 | print("Importing IFC file ...") 514 | start = time.time() 515 | ifc_file = ifcopenshell.open(filename) 516 | self.ifc_files[filename] = ifc_file 517 | print("Loaded in ", time.time() - start) 518 | 519 | model_node = None 520 | if filename in self.model_nodes: 521 | model_node = self.model_nodes[filename] 522 | for element in model_node.children(): 523 | element.setParent(None) 524 | else: 525 | model_node = QEntity(self.files) 526 | self.model_nodes[filename] = model_node 527 | model_node.setProperty("IsProduct", True) 528 | model_node.setObjectName(filename) 529 | 530 | print("Importing IFC geometrical information ...") 531 | self.start = time.time() 532 | settings = ifcopenshell.geom.settings() 533 | settings.set(settings.WELD_VERTICES, False) # false is needed to generate normals -- slower 534 | # settings.set(settings.NO_NORMALS, True) # disable generation of normals 535 | settings.set(settings.USE_WORLD_COORDS, True) # true = ignore transformation 536 | # settings.set(settings.SEW_SHELLS, True) # true default - slightly slower? 537 | # settings.set(settings.GENERATE_UVS, True) # true default 538 | # settings.set(settings.FASTER_BOOLEANS, True) # merge opening Booleans before subtracting 539 | # settings.set(settings.DISABLE_TRIANGULATION, True) # if using OCC formats 540 | # settings.set(settings.USE_BREP_DATA, True) # use OCC BREP data 541 | # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, True) 542 | # settings.set(settings.DISABLE_BOOLEAN_RESULT, True) # works 543 | # settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True) # works 544 | # settings.set(settings.STRICT_TOLERANCE, False) # default kernel tolerance to 1 = True 545 | # settings.set(settings.APPLY_LAYERSETS, True) # geometry for individual layers 546 | settings.set(settings.APPLY_DEFAULT_MATERIALS, True) # assign default material for elements without 547 | # settings.set_angular_tolerance(1) 548 | # settings.set_deflection_tolerance(1) # default = 1e-3 549 | 550 | settings.set(settings.USE_PYTHON_OPENCASCADE, True) 551 | 552 | # Two methods 553 | # self.parse_project(filename, settings) # SLOWER - create geometry for each product 554 | self.parse_geometry(filename, settings) # FASTER - iteration with parallel processing 555 | print("\nFinished in ", time.time() - self.start) 556 | 557 | self.update_scene_graph_tree() 558 | self.scene_graph.expandToDepth(1) 559 | 560 | # endregion 561 | 562 | # region SceneGraphMethods 563 | 564 | def toggle_visibility(self, tree_item, column): 565 | # get the widget item 566 | if tree_item is not None and column == 0: 567 | # get its user data 568 | entity = tree_item.data(0, Qt.UserRole) 569 | if entity is not None: 570 | if entity.property("IsProduct") is True: 571 | # TODO: this is the opposite of what we expect... 572 | entity.setEnabled(not entity.isEnabled()) 573 | # set visibility to reflect the check state 574 | if tree_item.checkState(0) == Qt.Checked: 575 | # if not entity.isEnabled(): 576 | # entity.setEnabled(True) 577 | var = 1 578 | if tree_item.checkState(0) == Qt.Unchecked: 579 | var = 2 580 | # if entity.isEnabled(): 581 | # entity.setEnabled(False) 582 | 583 | def highlight_in_scene_graph(self, entity, highlight=True): 584 | """ 585 | Highlight the entity in the scene graph 586 | 587 | :type entity: QEntity 588 | :type highlight: bool 589 | """ 590 | iterator = QTreeWidgetItemIterator(self.scene_graph) 591 | while iterator.value(): 592 | item = iterator.value() 593 | qentity = item.data(0, Qt.UserRole) 594 | if entity == qentity: 595 | item.setSelected(highlight) 596 | self.scene_graph.scrollToItem(item) 597 | iterator += 1 598 | 599 | def update_scene_graph_tree(self, node=None, parent=None): 600 | """ 601 | Recursive update of the tree representing the 3D scene graph. 602 | This only takes the scene_graph itself into account. 603 | The TreeItems carry a reference to the QEntity node. 604 | The nodes are named with their IFC Object GlobalId. 605 | Nodes which represent an IfcProduct (or some grouping), 606 | get a check box to toggle the visibility of the QEntity. 607 | 608 | :param node: current QEntity 609 | :type node: QEntity 610 | :param parent: parent QTreeWidgetItem 611 | :type parent: QTreeWidgetItem 612 | """ 613 | if node is None: 614 | node = self.root 615 | if parent is None: 616 | parent = self.scene_graph.invisibleRootItem() 617 | self.scene_graph.clear() 618 | 619 | node_item = QTreeWidgetItem([node.objectName(), node.metaObject().className()]) 620 | parent.addChild(node_item) 621 | 622 | # Add a reference to the QEntity 623 | if node.property("IsProduct") is True: 624 | node_item.setData(0, Qt.UserRole, node) 625 | node_item.setData(0, Qt.ToolTipRole, str("{} - {}").format(node.objectName(), "IsProduct")) 626 | 627 | # display checkbox for Enabled Items 628 | node_item.setFlags(node_item.flags() | Qt.ItemIsUserCheckable) 629 | # TODO: this is the opposite of what we expect... 630 | if node.isEnabled(): 631 | node_item.setCheckState(0, Qt.Unchecked) 632 | else: 633 | node_item.setCheckState(0, Qt.Checked) 634 | 635 | # iterate over children 636 | for item in node.children(): 637 | self.update_scene_graph_tree(item, node_item) 638 | 639 | # endregion 640 | 641 | # region GeometryMethods 642 | 643 | def parse_geometry(self, filename, settings): 644 | ifc_file = self.ifc_files[filename] 645 | iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count()) 646 | iterator.initialize() 647 | counter = 0 648 | while True: 649 | shape = iterator.get() 650 | # skip openings and spaces geometry 651 | if not shape.data.product.is_a('IfcOpeningElement') and not shape.data.product.is_a('IfcSpace'): 652 | try: 653 | self.generate_rendermesh(shape, self.model_nodes[filename]) 654 | print(str("Shape {0}\t[#{1}]\tin {2} seconds") 655 | .format(str(counter), str(shape.data.id), time.time() - self.start)) 656 | except Exception as e: 657 | print(str("Shape {0}\t[#{1}]\tERROR - {2} : {3}") 658 | .format(str(counter), str(shape.data.id), shape.data.product.is_a(), e)) 659 | pass 660 | counter += 1 661 | if not iterator.next(): 662 | break 663 | 664 | def parse_project(self, filename, settings): 665 | ifc_file = self.ifc_files[filename] 666 | # parse all products 667 | products = ifc_file.by_type('IfcProduct') 668 | counter = 0 669 | for product in products: 670 | if not product.is_a('IfcOpeningElement') and not product.is_a('IfcSpace'): 671 | if product.Representation: 672 | shape = ifcopenshell.geom.create_shape(settings, product) 673 | self.generate_rendermesh(shape, self.model_nodes[filename]) 674 | print(str("Product {0}\t[#{1}]\tin {2} seconds") 675 | .format(str(counter), str(product.id()), time.time() - self.start)) 676 | counter += 1 677 | 678 | def parse_shape(self, geometry): 679 | # compute the tessellation 680 | tess = ShapeTesselator(geometry) 681 | tess.Compute(compute_edges=True) 682 | # tess.Compute(compute_edges=False, mesh_quality=1.0, parallel=True) 683 | 684 | # get the vertices 685 | vertices = [] 686 | vertex_count = tess.ObjGetVertexCount() 687 | for i_vertex in range(0, vertex_count): 688 | i1, i2, i3 = tess.GetVertex(i_vertex) 689 | vertices.append(i1) 690 | vertices.append(i2) 691 | vertices.append(i3) 692 | 693 | # get the normals 694 | normals = [] 695 | normals_count = tess.ObjGetNormalCount() 696 | for i_normal in range(0, normals_count): 697 | i1, i2, i3 = tess.GetNormal(i_normal) 698 | normals.append(i1) 699 | normals.append(i2) 700 | normals.append(i3) 701 | 702 | # get the triangles 703 | triangles = [] 704 | triangle_count = tess.ObjGetTriangleCount() 705 | for i_triangle in range(0, triangle_count): 706 | i1, i2, i3 = tess.GetTriangleIndex(i_triangle) 707 | triangles.append(i1) 708 | triangles.append(i2) 709 | triangles.append(i3) 710 | 711 | # get the edges 712 | edges = [] 713 | edge_count = tess.ObjGetEdgeCount() 714 | for i_edge in range(0, edge_count): 715 | vertex_count = tess.ObjEdgeGetVertexCount(i_edge) 716 | # edge = [] 717 | for i_vertex in range(0, vertex_count): 718 | vertex = tess.GetEdgeVertex(i_edge, i_vertex) 719 | # edge.append(vertex) 720 | # edges.append(i_vertex) 721 | edges.append(vertex[0]) 722 | edges.append(vertex[1]) 723 | edges.append(vertex[2]) 724 | # edges.append(edge) 725 | 726 | return vertices, normals, triangles, edges 727 | 728 | def generate_rendermesh(self, shape, parent): 729 | """ 730 | Collecting the mesh geometry using OCC for the current TopoDS Shape. 731 | The vertices, edges, triangles and colors are used to create the 732 | Qt3D Entities & Nodes & Components for the 3D Representation. 733 | 734 | :param shape: TopoDS Shape (from OpenCASCADE) 735 | :param parent: QEntity parent Node (representing the File node) 736 | """ 737 | data = shape.data 738 | geometry = shape.geometry 739 | styles = shape.styles 740 | style_ids = shape.style_ids 741 | 742 | custom_mesh_entity = QEntity(parent) 743 | custom_mesh_entity.setObjectName(shape.data.guid) 744 | custom_mesh_entity.setProperty("IsProduct", True) 745 | custom_mesh_entity.setProperty("GlobalId", shape.data.guid) 746 | 747 | it = OCC.Core.TopoDS.TopoDS_Iterator(geometry) 748 | index = 0 749 | while it.More(): 750 | vertices, normals, triangles, edges = self.parse_shape(it.Value()) 751 | 752 | # ------ MESH -------------------------- 753 | custom_mesh_renderer = QGeometryRenderer() 754 | custom_mesh_renderer.setObjectName("Mesh Renderer") 755 | custom_mesh_renderer.setPrimitiveType(QGeometryRenderer.Triangles) 756 | custom_geometry = QGeometry(custom_mesh_renderer) 757 | custom_geometry.setObjectName("Custom Geometry") 758 | 759 | # Position Attribute 760 | position_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 761 | # position_data_buffer.setData(QByteArray(np.array(geometry.verts).astype(np.float32).tobytes())) 762 | position_data_buffer.setData(struct.pack('%sf' % len(vertices), *vertices)) 763 | position_attribute = QAttribute() 764 | position_attribute.setAttributeType(QAttribute.VertexAttribute) 765 | position_attribute.setBuffer(position_data_buffer) 766 | position_attribute.setVertexBaseType(QAttribute.Float) 767 | position_attribute.setVertexSize(3) # 3 floats 768 | position_attribute.setByteOffset(0) # start from first index 769 | position_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 770 | position_attribute.setCount(len(vertices)) # vertices 771 | position_attribute.setName(QAttribute.defaultPositionAttributeName()) 772 | position_attribute.setObjectName("Position Vertex Attribute") 773 | custom_geometry.addAttribute(position_attribute) 774 | 775 | # Normal Attribute 776 | if len(normals) > 0: 777 | normals_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 778 | # normals_data_buffer.setData(QByteArray(np.array(geometry.normals).astype(np.float32).tobytes())) 779 | normals_data_buffer.setData(struct.pack('%sf' % len(normals), *normals)) 780 | normal_attribute = QAttribute() 781 | normal_attribute.setAttributeType(QAttribute.VertexAttribute) 782 | normal_attribute.setBuffer(normals_data_buffer) 783 | normal_attribute.setVertexBaseType(QAttribute.Float) 784 | normal_attribute.setVertexSize(3) # 3 floats 785 | normal_attribute.setByteOffset(0) # start from first index 786 | normal_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 787 | normal_attribute.setCount(len(normals)) # vertices 788 | normal_attribute.setName(QAttribute.defaultNormalAttributeName()) 789 | normal_attribute.setObjectName("Normal Vertex Attribute") 790 | custom_geometry.addAttribute(normal_attribute) 791 | 792 | # Collect the colors via the materials (1 color per vertex) 793 | # we get a list of styles (ids) and surface styles (rgba values) 794 | # expressed per shape, not per vertex, so repeat them 795 | s_style = styles[index] 796 | r = s_style[0] 797 | g = s_style[1] 798 | b = s_style[2] 799 | a = s_style[3] 800 | color_list = [r, g, b] * int(len(vertices) / 3) 801 | 802 | # Color Attribute 803 | color_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 804 | # color_data_buffer.setData(QByteArray(np.array(color_list).astype(np.float32).tobytes())) 805 | color_data_buffer.setData(struct.pack('%sf' % len(color_list), *color_list)) 806 | color_attribute = QAttribute() 807 | color_attribute.setAttributeType(QAttribute.VertexAttribute) 808 | color_attribute.setBuffer(color_data_buffer) 809 | color_attribute.setVertexBaseType(QAttribute.Float) 810 | color_attribute.setVertexSize(3) # 3 floats 811 | color_attribute.setByteOffset(0) # start from first index 812 | color_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 813 | color_attribute.setCount(len(color_list)) # colors (per vertex) 814 | color_attribute.setName(QAttribute.defaultColorAttributeName()) 815 | color_attribute.setObjectName("Color Vertex Attribute") 816 | custom_geometry.addAttribute(color_attribute) 817 | 818 | # Faces Index Attribute 819 | index_data_buffer = QBuffer(QBuffer.IndexBuffer, custom_geometry) 820 | # index_data_buffer.setData(QByteArray(np.array(triangles).astype(np.uintc).tobytes())) 821 | index_data_buffer.setData(struct.pack("{}I".format(len(triangles)), *triangles)) 822 | index_data_buffer.setObjectName("Index Data Buffer") 823 | index_attribute = QAttribute() 824 | index_attribute.setVertexBaseType(QAttribute.UnsignedInt) 825 | index_attribute.setAttributeType(QAttribute.IndexAttribute) 826 | index_attribute.setBuffer(index_data_buffer) 827 | index_attribute.setCount(len(triangles)) 828 | index_attribute.setName("Indices") 829 | index_attribute.setObjectName("Index Unsigned Int Attribute") 830 | custom_geometry.addAttribute(index_attribute) 831 | 832 | # make the geometry visible with a renderer 833 | custom_mesh_renderer.setGeometry(custom_geometry) 834 | custom_mesh_renderer.setInstanceCount(1) 835 | custom_mesh_renderer.setFirstVertex(0) 836 | custom_mesh_renderer.setFirstInstance(0) 837 | 838 | # add everything to the scene 839 | custom_mesh_sub_entity = QEntity(custom_mesh_entity) 840 | custom_mesh_sub_entity.addComponent(custom_mesh_renderer) 841 | custom_mesh_sub_entity.setObjectName("Mesh") # ifc_object.GlobalId) 842 | transform = QTransform() 843 | transform.setObjectName("Rotate X -90°") 844 | transform.setRotationX(-90) 845 | custom_mesh_sub_entity.addComponent(transform) 846 | if a < 1.0: 847 | custom_mesh_sub_entity.addComponent(self.transparent) 848 | custom_mesh_sub_entity.setProperty("IsTransparent", True) 849 | else: 850 | custom_mesh_sub_entity.addComponent(self.material) 851 | 852 | # ------ EDGES -------------------------- 853 | custom_line_renderer = QGeometryRenderer() 854 | custom_line_renderer.setObjectName("Lines Renderer") 855 | custom_line_renderer.setPrimitiveType(QGeometryRenderer.Lines) 856 | custom_line_geometry = QGeometry(custom_line_renderer) 857 | custom_line_geometry.setObjectName("Custom Lines Geometry") 858 | 859 | # Position Attribute 860 | position_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_line_geometry) 861 | # position_data_buffer.setData(QByteArray(np.array(edges).astype(np.float32).tobytes())) 862 | position_data_buffer.setData(struct.pack('%sf' % len(edges), *edges)) 863 | position_attribute = QAttribute() 864 | position_attribute.setAttributeType(QAttribute.VertexAttribute) 865 | position_attribute.setBuffer(position_data_buffer) 866 | position_attribute.setVertexBaseType(QAttribute.Float) 867 | position_attribute.setVertexSize(3) # 3 floats 868 | position_attribute.setByteOffset(0) # start from first index 869 | position_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 870 | position_attribute.setCount(len(edges)) # vertices 871 | position_attribute.setName(QAttribute.defaultPositionAttributeName()) 872 | custom_line_geometry.addAttribute(position_attribute) 873 | 874 | # Edges Index Attribute 875 | indices_edges = list(range(int(len(edges) / 3))) 876 | index_data_buffer = QBuffer(QBuffer.IndexBuffer, custom_line_geometry) 877 | # index_data_buffer.setData(QByteArray(np.array(indices_edges).astype(np.uintc).tobytes())) 878 | index_data_buffer.setData(struct.pack("{}I".format(len(indices_edges)), *indices_edges)) 879 | index_data_buffer.setObjectName("Index Data Buffer") 880 | index_attribute = QAttribute() 881 | index_attribute.setVertexBaseType(QAttribute.UnsignedInt) 882 | index_attribute.setAttributeType(QAttribute.IndexAttribute) 883 | index_attribute.setBuffer(index_data_buffer) 884 | index_attribute.setCount(len(indices_edges)) 885 | index_attribute.setName("Indices") 886 | index_attribute.setObjectName("Index Unsigned Int Attribute") 887 | custom_line_geometry.addAttribute(index_attribute) 888 | 889 | # make the geometry visible with a renderer 890 | custom_line_renderer.setGeometry(custom_line_geometry) 891 | custom_line_renderer.setInstanceCount(1) 892 | custom_line_renderer.setFirstVertex(0) 893 | custom_line_renderer.setFirstInstance(0) 894 | 895 | # add everything to the scene 896 | custom_line_entity = QEntity(custom_mesh_entity) # TODO: rethink scenegraph 897 | custom_line_entity.setObjectName("Line") 898 | custom_line_entity.setProperty("IsWireframe", True) 899 | transform = QTransform() 900 | transform.setObjectName("Rotate X -90°") 901 | transform.setRotationX(-90) 902 | custom_line_entity.addComponent(transform) 903 | custom_line_entity.addComponent(custom_line_renderer) 904 | custom_line_entity.addComponent(self.edge_material) 905 | 906 | index += 1 907 | it.Next() 908 | 909 | def generate_line(self, start, end): 910 | vertices = start + end 911 | self.generate_primitive(vertices) 912 | 913 | def generate_axis(self, size, pos=None): 914 | if pos is None: 915 | pos = [0, 0, 0] 916 | elif type(pos) == QVector3D: 917 | pos = [pos.x(), pos.y(), pos.z()] 918 | x, y, z = pos[0], pos[1], pos[2] 919 | x_axis = [x, y, z, x + size, y, z] 920 | y_axis = [x, y, z, x, y + size, z] 921 | z_axis = [x, y, z, x, y, z + size] 922 | self.generate_primitive(x_axis, [1, 0, 0]) 923 | self.generate_primitive(y_axis, [0, 1, 0]) 924 | self.generate_primitive(z_axis, [0, 0, 1]) 925 | 926 | def generate_grid(self, extent, pos=None, step_size=1): 927 | if pos is None: 928 | pos = [0, 0, 0] 929 | elif type(pos) == QVector3D: 930 | pos = [pos.x(), pos.y(), pos.z()] 931 | x, y, z = pos[0], pos[1], pos[2] 932 | for h in range(-extent, extent + step_size, step_size): 933 | grid_line = [x + h, y - extent, z, x + h, y + extent, z] 934 | self.generate_primitive(grid_line) 935 | for v in range(-extent, extent + step_size, step_size): 936 | grid_line = [x - extent, y + v, z, x + extent, y + v, z] 937 | self.generate_primitive(grid_line) 938 | 939 | def generate_primitive(self, 940 | coordinates, 941 | colors=None, 942 | primitive=QGeometryRenderer.Lines): 943 | # coordinates = [x1, y1, z1, x2, y2, z2, ...] 944 | if colors is None: 945 | colors = [0.5, 0.5, 0.5] 946 | if len(colors) != len(coordinates): 947 | color_list = colors * int(len(coordinates) / len(colors)) 948 | else: 949 | color_list = colors 950 | 951 | custom_line_renderer = QGeometryRenderer() 952 | custom_line_renderer.setPrimitiveType(primitive) 953 | custom_geometry = QGeometry(custom_line_renderer) 954 | 955 | # Position Attribute 956 | position_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 957 | # position_data_buffer.setData(QByteArray(np.array(coordinates).astype(np.float32).tobytes())) 958 | position_data_buffer.setData(struct.pack('%sf' % len(coordinates), *coordinates)) 959 | position_attribute = QAttribute() 960 | # position_attribute.setAttributeType(QAttribute.VertexAttribute) 961 | position_attribute.setBuffer(position_data_buffer) 962 | # position_attribute.setVertexBaseType(QAttribute.Float) 963 | position_attribute.setVertexSize(3) # 3 floats 964 | # position_attribute.setByteOffset(0) # start from first index 965 | # position_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 966 | # position_attribute.setCount(len(coordinates)) # vertices 967 | position_attribute.setName(QAttribute.defaultPositionAttributeName()) 968 | custom_geometry.addAttribute(position_attribute) 969 | 970 | # Color Attribute 971 | color_data_buffer = QBuffer(QBuffer.VertexBuffer, custom_geometry) 972 | # color_data_buffer.setData(QByteArray(np.array(color_list).astype(np.float32).tobytes())) 973 | color_data_buffer.setData(struct.pack('%sf' % len(color_list), *color_list)) 974 | color_attribute = QAttribute() 975 | # color_attribute.setAttributeType(QAttribute.VertexAttribute) 976 | color_attribute.setBuffer(color_data_buffer) 977 | # color_attribute.setVertexBaseType(QAttribute.Float) 978 | color_attribute.setVertexSize(3) # 3 floats 979 | # color_attribute.setByteOffset(0) # start from first index 980 | # color_attribute.setByteStride(3 * 4) # 3 coordinates and 4 as length of float32 in bytes 981 | color_attribute.setCount(len(color_list)) # colors (per vertex) 982 | color_attribute.setName(QAttribute.defaultColorAttributeName()) 983 | custom_geometry.addAttribute(color_attribute) 984 | 985 | # ---------------------------------------------------------------------------- 986 | # make the geometry visible with a renderer 987 | custom_line_renderer.setGeometry(custom_geometry) 988 | custom_line_renderer.setInstanceCount(1) 989 | custom_line_renderer.setFirstVertex(0) 990 | custom_line_renderer.setFirstInstance(0) 991 | 992 | # add everything to the scene 993 | custom_line_entity = QEntity(self.grids) 994 | custom_line_entity.setObjectName("Line") 995 | transform = QTransform() 996 | transform.setObjectName("Rotate X -90°") 997 | transform.setRotationX(-90) 998 | custom_line_entity.addComponent(transform) 999 | custom_line_entity.addComponent(custom_line_renderer) 1000 | custom_line_entity.addComponent(self.material) 1001 | 1002 | # endregion 1003 | 1004 | 1005 | # Our Main function 1006 | def main(): 1007 | app = 0 1008 | if QApplication.instance(): 1009 | app = QApplication.instance() 1010 | else: 1011 | app = QApplication(sys.argv) 1012 | 1013 | w = IFCQt3dView() 1014 | w.resize(1280, 800) 1015 | filename = sys.argv[1] 1016 | if os.path.isfile(filename): 1017 | w.load_file(filename) 1018 | w.setWindowTitle("IFC Viewer - " + filename) 1019 | w.show() 1020 | sys.exit(app.exec_()) 1021 | 1022 | 1023 | if __name__ == "__main__": 1024 | main() 1025 | --------------------------------------------------------------------------------