import bpy import csv import json import time from pathlib import Path from mathutils import Vector, Matrix from .helper import SIUnitHelper from . import ifcopenshell class ArrayModifier: count: int offset: Vector class QtoCalculator(): def get_units(self, o, vg_index): return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]]) def get_length(self, o, vg_index): length = 0 edges = [e for e in o.data.edges if ( vg_index in [g.group for g in o.data.vertices[e.vertices[0]].groups] and vg_index in [g.group for g in o.data.vertices[e.vertices[1]].groups] )] for e in edges: length += self.get_edge_distance(o, e) return length def get_edge_distance(self, obj, edge): return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length def get_area(self, o, vg_index): area = 0 vertices_in_vg = [v.index for v in o.data.vertices if vg_index in [g.group for g in v.groups]] for polygon in o.data.polygons: if self.is_polygon_in_vg(polygon, vertices_in_vg): area += polygon.area return area def is_polygon_in_vg(self, polygon, vertices_in_vg): for v in polygon.vertices: if v not in vertices_in_vg: return False return True def get_volume(self, o, vg_index): volume = 0 ob_mat = o.matrix_world me = o.data me.calc_loop_triangles() for tf in me.loop_triangles: tfv = tf.vertices if len(tf.vertices) == 3: tf_tris = (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]), else: tf_tris = (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]), \ (me.vertices[tfv[2]], me.vertices[tfv[3]], me.vertices[tfv[0]]) for tf_iter in tf_tris: v1 = ob_mat @ tf_iter[0].co v2 = ob_mat @ tf_iter[1].co v3 = ob_mat @ tf_iter[2].co volume += v1.dot(v2.cross(v3)) / 6.0 return volume class IfcSchema(): def __init__(self, ifc_export_settings): self.schema_dir = ifc_export_settings.schema_dir self.property_file = ifcopenshell.open(self.schema_dir + 'IFC4_ADD2.ifc') self.psets = {} self.qtos = {} self.load() with open(self.schema_dir + 'ifc_types_IFC4.json') as f: self.type_map = json.load(f) with open(self.schema_dir + 'ifc_elements_IFC4.json') as f: self.elements = json.load(f) def load(self): for property in self.property_file.by_type('IfcPropertySetTemplate'): if property.Name[0:4] == 'Qto_': # self.qtos.append({ }) pass else: self.psets[property.Name] = { 'HasPropertyTemplates': {p.Name: p for p in property.HasPropertyTemplates}} class IfcParser(): def __init__(self, ifc_export_settings): self.data_dir = ifc_export_settings.data_dir self.ifc_export_settings = ifc_export_settings self.selected_products = [] self.product_index = 0 self.product_name_index_map = {} self.units = {} self.people = [] self.organisations = [] self.psets = {} self.documents = {} self.classifications = [] self.classification_references = {} self.objectives = {} self.qtos = {} self.aggregates = {} self.materials = {} self.spatial_structure_elements = [] self.spatial_structure_elements_tree = [] self.rel_contained_in_spatial_structure = {} self.rel_nests = {} self.rel_space_boundaries = {} self.rel_defines_by_type = {} self.rel_defines_by_qto = {} self.rel_defines_by_pset = {} self.rel_associates_document_object = {} self.rel_associates_document_type = {} self.rel_associates_classification_object = {} self.rel_associates_classification_type = {} self.rel_associates_material = {} self.rel_associates_material_layer_set = {} self.rel_associates_material_constituent_set = {} self.rel_associates_constraint_objective_object = {} self.rel_associates_constraint_objective_type = {} self.rel_aggregates = {} self.rel_voids_elements = {} self.rel_fills_elements = {} self.representations = {} self.type_products = [] self.door_attributes = {} self.window_attributes = {} self.project = {} self.libraries = [] self.products = [] def parse(self): self.units = self.get_units() self.unit_scale = self.get_unit_scale() self.people = self.get_people() self.organisations = self.get_organisations() self.convert_selected_objects_into_products(bpy.context.selected_objects) self.psets = self.get_psets() self.documents = self.get_documents() self.classifications = self.get_classifications() self.classification_references = self.get_classification_references() self.objectives = self.get_objectives() self.representations = self.get_representations() self.materials = self.get_materials() self.styled_items = self.get_styled_items() self.qtos = self.get_qtos() self.spatial_structure_elements = self.get_spatial_structure_elements() self.collection_name_filter = [] self.project = self.get_project() self.libraries = self.get_libraries() self.door_attributes = self.get_door_attributes() self.window_attributes = self.get_window_attributes() self.type_products = self.get_type_products() self.get_products() self.resolve_boolean_modifiers() self.map_conversion = self.get_map_conversion() self.target_crs = self.get_target_crs() self.spatial_structure_elements_tree = self.get_spatial_structure_elements_tree( self.project['raw'].children, self.collection_name_filter) def get_units(self): return { 'length': { 'ifc': None, 'is_metric': bpy.context.scene.unit_settings.system == 'METRIC', 'raw': bpy.context.scene.unit_settings.length_unit }, 'area': { 'ifc': None, 'is_metric': bpy.context.scene.unit_settings.system == 'METRIC', 'raw': bpy.context.scene.unit_settings.length_unit }, 'volume': { 'ifc': None, 'is_metric': bpy.context.scene.unit_settings.system == 'METRIC', 'raw': bpy.context.scene.unit_settings.length_unit }} def get_unit_scale(self): unit_settings = bpy.context.scene.unit_settings conversions = { 'KILOMETERS': 1e3, 'CENTIMETERS': 1e-2, 'MILLIMETERS': 1e-3, 'MICROMETERS': 1e-6, 'FEET': 0.3048, 'INCHES': 0.0254} if unit_settings.system in {'METRIC', 'IMPERIAL'}: scale = unit_settings.scale_length if unit_settings.length_unit in conversions.keys(): scale *= conversions[unit_settings.length_unit] return scale return 1 def get_object_attributes(self, obj): attributes = {'Name': self.get_ifc_name(obj.name)} if obj.BIMObjectProperties.attributes.find('GlobalId') == -1: global_id = obj.BIMObjectProperties.attributes.add() global_id.name = 'GlobalId' global_id.string_value = ifcopenshell.guid.new() attributes.update({a.name: a.string_value for a in obj.BIMObjectProperties.attributes}) return attributes def get_products(self): for product in self.selected_products: obj = product['raw'] self.add_product(self.get_product(product)) self.resolve_array_modifier(product) def resolve_array_modifier(self, product): obj = product['raw'] instance_objects = [(obj, obj.matrix_world.translation)] global_id_index = 0 for instance in self.get_instances(obj): created_instances = [] for n in range(instance.count - 1): for o in instance_objects: location = o[1] + ((n + 1) * instance.offset) self.add_product( self.get_product( {'raw': o[0], 'metadata': product['metadata']}, {'location': location}, {'GlobalId': self.get_parametric_global_id(obj, global_id_index)} ) ) created_instances.append((o[0], location)) instance_objects.extend(created_instances) def resolve_boolean_modifiers(self): for product in self.products: obj = product['raw'] for m in obj.modifiers: if m.type == 'BOOLEAN' and m.object is not None: void = self.get_product_from_raw_name(m.object.name) if void is not None: if product['ifc'] not in self.rel_voids_elements: self.rel_voids_elements[product['ifc']] = [] self.rel_voids_elements[product['ifc']].append(void['ifc']) if m.object.parent: fill = self.get_product_from_raw_name(m.object.parent.name) if fill is not None: if void['ifc'] not in self.rel_fills_elements: self.rel_fills_elements[void['ifc']] = [] self.rel_fills_elements[void['ifc']].append(fill['ifc']) def get_axis(self, matrix, axis): return matrix.row[axis].to_3d().normalized() def get_parametric_global_id(self, obj, index): global_ids = obj.BIMObjectProperties.global_ids total_global_ids = len(global_ids) if index < total_global_ids: return global_ids[index].name global_id = obj.BIMObjectProperties.global_ids.add() global_id.name = ifcopenshell.guid.new() return global_id.name def add_product(self, product): self.products.append(product) self.product_name_index_map[product['raw'].name] = self.product_index self.product_index += 1 def get_product_index_from_raw_name(self, name): for index, product in enumerate(self.products): if product['raw'].name == name: return index def get_product_from_raw_name(self, name): for index, product in enumerate(self.products): if product['raw'].name == name: return product def get_product(self, selected_product, metadata_override={}, attribute_override={}): obj = selected_product['raw'] product = { 'ifc': None, 'raw': obj, 'location': obj.matrix_world.translation, 'up_axis': self.get_axis(obj.matrix_world, 2), 'forward_axis': self.get_axis(obj.matrix_world, 0), 'right_axis': self.get_axis(obj.matrix_world, 1), 'has_scale': obj.scale != Vector((1, 1, 1)), 'scale': obj.scale, 'class': self.get_ifc_class(obj.name), 'relating_structure': None, 'relating_host': None, 'relating_qtos_key': None, 'representations': self.get_object_representation_names(obj), 'attributes': self.get_object_attributes(obj) } product['attributes'].update(attribute_override) product.update(metadata_override) if obj.parent \ and self.is_a_type(self.get_ifc_class(obj.parent.name)): reference = self.get_type_product_reference(obj.parent.name) self.rel_defines_by_type.setdefault(reference, []).append(self.product_index) for collection in product['raw'].users_collection: self.parse_product_collection(product, collection) if 'IfcRelNests' in obj.constraints: parent_product_index = self.get_product_index_from_raw_name( obj.constraints['IfcRelNests'].target.name) self.rel_nests.setdefault(parent_product_index, []).append(product) product['relating_host'] = parent_product_index for name, constraint in obj.constraints.items(): if 'IfcRelSpaceBoundary' not in name: continue self.rel_space_boundaries.setdefault(self.product_index, []).append({ 'ifc': None, 'class': self.get_ifc_class(name), 'related_building_element_raw_name': constraint.target.name, 'connection_geometry_face_index': name.split('/')[1], 'attributes': { 'PhysicalOrVirtualBoundary': name.split('/')[2], 'InternalOrExternalBoundary': name.split('/')[3] } }) if obj.instance_type == 'COLLECTION' \ and self.is_a_rel_aggregates(self.get_ifc_class(obj.instance_collection.name)): self.rel_aggregates[self.product_index] = obj.name if 'rel_aggregates_relating_object' in selected_product['metadata']: relating_object = selected_product['metadata']['rel_aggregates_relating_object'] inverted = relating_object.matrix_world.inverted() product['location'] = inverted @ product['location'] product['up_axis'] = self.get_axis(inverted @ obj.matrix_world, 2) product['forward_axis'] = self.get_axis(inverted @ obj.matrix_world, 0) self.aggregates.setdefault(relating_object.name, []).append(self.product_index) if obj.name in self.qtos: self.rel_defines_by_qto.setdefault(obj.name, []).append(product) for pset in obj.BIMObjectProperties.psets: self.rel_defines_by_pset.setdefault( '{}/{}'.format(pset.name, pset.file), []).append(product) for document in obj.BIMObjectProperties.documents: self.rel_associates_document_object.setdefault( document.file, []).append(product) for classification in obj.BIMObjectProperties.classifications: self.rel_associates_classification_object.setdefault( classification.identification, []).append(product) for key in obj.keys(): if key[0:9] == 'Objective': self.rel_associates_constraint_objective_object.setdefault( obj[key], []).append(product) for slot in obj.material_slots: if slot.link == 'OBJECT': continue if 'IsMaterialLayerSet' in obj: self.rel_associates_material_layer_set.setdefault(self.product_index, []).append(slot.material.name) elif 'IsMaterialConstituentSet' in obj: self.rel_associates_material_constituent_set.setdefault(self.product_index, []).append( slot.material.name) else: self.rel_associates_material.setdefault(slot.material.name, []).append(product) return product def parse_product_collection(self, product, collection): if collection is None: return class_name = self.get_ifc_class(collection.name) if self.is_a_spatial_structure_element(class_name): reference = self.get_spatial_structure_element_reference(collection.name) self.rel_contained_in_spatial_structure.setdefault(reference, []).append(self.product_index) product['relating_structure'] = reference self.collection_name_filter.append(collection.name) elif self.is_a_rel_aggregates(class_name): pass else: self.parse_product_collection(product, self.get_parent_collection(collection)) def get_parent_collection(self, child_collection): for parent_collection in bpy.data.collections: for child in parent_collection.children: if child.name == child_collection.name: return parent_collection def get_instances(self, obj): instances = [] for m in obj.modifiers: if m.type == 'ARRAY': array = ArrayModifier() world_rotation = obj.matrix_world.decompose()[1] array.offset = world_rotation @ Vector( (m.constant_offset_displace[0], m.constant_offset_displace[1], m.constant_offset_displace[2])) if m.fit_type == 'FIXED_COUNT': array.count = m.count elif m.fit_type == 'FIT_LENGTH': array.count = int(m.fit_length / array.offset.length) instances.append(array) return instances def convert_selected_objects_into_products(self, objects_to_sort, metadata=None): if not metadata: metadata = {} for obj in objects_to_sort: if not self.is_a_library(self.get_ifc_class(obj.users_collection[0].name)): self.selected_products.append({'raw': obj, 'metadata': metadata}) if obj.instance_type == 'COLLECTION': self.convert_selected_objects_into_products( obj.instance_collection.objects, {'rel_aggregates_relating_object': obj} ) def get_psets(self): psets = {} for filename in Path(self.data_dir + 'pset/').glob('**/*.csv'): with open(filename, 'r') as f: name = filename.parts[-2] description = filename.stem psets['{}/{}'.format(name, description)] = { 'ifc': None, 'raw': {x[0]: x[1] for x in list(csv.reader(f))}, 'attributes': { 'Name': name, 'Description': description} } return psets def get_door_attributes(self): return self.get_predefined_attributes('door') def get_window_attributes(self): return self.get_predefined_attributes('window') def get_predefined_attributes(self, attr): results = {} for filename in Path(self.data_dir + attr + '/').glob('**/*.csv'): with open(filename, 'r') as f: type_name = filename.parts[-2] pset_name = filename.stem results.setdefault(type_name, []).append({ 'ifc': None, 'raw': {x[0]: x[1] for x in list(csv.reader(f))}, 'pset_name': pset_name.split('.')[0] }) return results def get_classifications(self): results = [] class_path = self.data_dir + 'class/' with open(class_path + 'classifications.csv', 'r') as f: data = list(csv.reader(f)) keys = data.pop(0) for row in data: row[-1] = json.loads(row[-1]) results.append({ 'ifc': None, 'raw': row, 'attributes': dict(zip(keys, row)) }) return results def get_classification_references(self): results = {} class_path = self.data_dir + 'class/' with open(class_path + 'references.csv', 'r') as f: data = list(csv.reader(f)) keys = data.pop(0) for row in data: results[row[0]] = { 'ifc': None, 'raw': row, 'referenced_source': int(row.pop()), 'attributes': dict(zip(keys, row)) } return results def get_objectives(self): results = {} class_path = self.data_dir + 'constraint/' with open(class_path + 'objectives.csv', 'r') as f: data = list(csv.reader(f)) keys = data.pop(0) for row in data: results[row[0]] = { 'ifc': None, 'raw': row, 'attributes': dict(zip(keys, row)) } return results def get_people(self): with open(self.data_dir + 'owner/person.json') as file: return json.load(file) def get_organisations(self): with open(self.data_dir + 'owner/organisation.json') as file: return json.load(file) def get_documents(self): documents = {} doc_path = self.data_dir + 'doc/' for filename in Path(doc_path).glob('**/*'): uri = str(filename.relative_to(doc_path).as_posix()) documents[uri] = { 'ifc': None, 'raw': filename, 'attributes': { 'Location': uri, 'Name': filename.stem }} return documents def get_project(self): for collection in bpy.data.collections: if self.is_a_project(self.get_ifc_class(collection.name)): return { 'ifc': None, 'raw': collection, 'class': self.get_ifc_class(collection.name), 'attributes': self.get_object_attributes(collection) } def get_libraries(self): results = [] for collection in self.project['raw'].children: if not self.is_a_library(self.get_ifc_class(collection.name)): continue results.append({ 'ifc': None, 'raw': collection, 'class': self.get_ifc_class(collection.name), 'rel_declares_type_products': [], 'attributes': self.get_object_attributes(collection) }) return results def get_map_conversion(self): scene = bpy.context.scene if not scene.BIMProperties.has_georeferencing: return {} return { 'ifc': None, 'attributes': { 'Eastings': float(scene.MapConversion.eastings), 'Northings': float(scene.MapConversion.northings), 'OrthogonalHeight': float(scene.MapConversion.orthogonal_height), 'XAxisAbscissa': float(scene.MapConversion.x_axis_abscissa), 'XAxisOrdinate': float(scene.MapConversion.x_axis_ordinate), 'Scale': float(scene.MapConversion.scale) } } def get_target_crs(self): scene = bpy.context.scene if not scene.BIMProperties.has_georeferencing: return {} return { 'ifc': None, 'attributes': { 'Name': scene.TargetCRS.name, 'Description': scene.TargetCRS.description, 'GeodeticDatum': scene.TargetCRS.geodetic_datum, 'VerticalDatum': scene.TargetCRS.vertical_datum, 'MapProjection': scene.TargetCRS.map_projection, 'MapZone': str(scene.TargetCRS.map_zone), 'MapUnit': scene.TargetCRS.map_unit } } def get_spatial_structure_elements(self): elements = [] for collection in bpy.data.collections: if self.is_a_spatial_structure_element(self.get_ifc_class(collection.name)): elements.append({ 'ifc': None, 'raw': collection, 'class': self.get_ifc_class(collection.name), 'attributes': self.get_object_attributes(collection) }) return elements def get_representations(self): results = {} if not self.ifc_export_settings.has_representations: return results for product in self.selected_products + self.type_products: obj = product['raw'] if obj.data is None or obj.data.name in results: continue self.append_default_representation(obj, results) self.append_representation_per_context(obj, results) return results def append_default_representation(self, obj, results): if not self.is_mesh_context_sensitive(obj.data.name): results['Model/Body/MODEL_VIEW/{}'.format(obj.data.name)] = self.get_representation( obj.data, obj, 'Model', 'Body', 'MODEL_VIEW') def append_representation_per_context(self, obj, results): name = self.get_ifc_representation_name(obj.data.name) for context in self.ifc_export_settings.context_tree: for subcontext in context['subcontexts']: for target_view in subcontext['target_views']: mesh_name = '/'.join([context['name'], subcontext['name'], target_view, name]) try: mesh = bpy.data.meshes[mesh_name] except: continue results[mesh_name] = self.get_representation( mesh, obj, context['name'], subcontext['name'], target_view) def get_representation(self, mesh, obj, context, subcontext, target_view, is_generated=False): return { 'ifc': None, 'raw': mesh, 'raw_object': obj, 'context': context, 'subcontext': subcontext, 'target_view': target_view, 'is_curve': isinstance(mesh, bpy.types.Curve), 'is_wireframe': mesh.BIMMeshProperties.is_wireframe if hasattr(mesh, 'BIMMeshProperties') else False, 'is_swept_solid': mesh.BIMMeshProperties.is_swept_solid if hasattr(mesh, 'BIMMeshProperties') else False, 'is_generated': is_generated, 'attributes': {'Name': mesh.name} } def is_mesh_context_sensitive(self, name): return '/' in name def get_ifc_representation_name(self, name): if self.is_mesh_context_sensitive(name): return name.split('/')[3] return name def get_materials(self): results = {} if not self.ifc_export_settings.has_representations: return results for product in self.selected_products + self.type_products: obj = product['raw'] if obj.data is None: continue for slot in obj.material_slots: if slot.material is None: continue if slot.material.name in results or slot.link == 'OBJECT': continue results[slot.material.name] = { 'ifc': None, 'layer_ifc': None, 'constituent_ifc': None, 'raw': slot.material, 'is_material_layer_set': 'IsMaterialLayerSet' in obj.keys(), 'is_material_constituent_set': 'IsMaterialConstituentSet' in obj.keys(), 'attributes': {'Name': slot.material.name}, 'layer_attributes': { key[3:]: slot.material[key] for key in slot.material.keys() if key[0:3] == 'Ifc' }, 'constituent_attributes': { key[3:]: slot.material[key] for key in slot.material.keys() if key[0:3] == 'Ifc' } } return results def get_styled_items(self): results = [] if not self.ifc_export_settings.has_representations: return results for product in self.selected_products + self.type_products: obj = product['raw'] if obj.data is None: continue for slot in obj.material_slots: if slot.material is None: continue if not self.ifc_export_settings.should_export_all_materials_as_styled_items and ( slot.material.name in results or slot.link == 'DATA'): continue results.append({ 'ifc': None, 'raw': slot.material, 'related_product_name': product['raw'].name, 'attributes': {'Name': slot.material.name}, }) return results def get_qtos(self): if not self.ifc_export_settings.has_quantities: return {} results = {} for product in self.selected_products + self.type_products: obj = product['raw'] if not obj.data: continue for property in obj.keys(): if property[0:4] != 'Qto_': continue results[obj.name] = { 'ifc': None, 'raw': obj, 'class': property, 'attributes': { 'Name': property, 'MethodOfMeasurement': obj[property] } } return results def get_type_products(self): results = [] index = 0 for library in self.libraries: for obj in library['raw'].objects: if not self.is_a_type(self.get_ifc_class(obj.name)): continue try: type_product = { 'ifc': None, 'raw': obj, 'location': obj.matrix_world.translation, 'up_axis': self.get_axis(obj.matrix_world, 2), 'forward_axis': self.get_axis(obj.matrix_world, 0), 'psets': ['{}/{}'.format(pset.name, pset.file) for pset in obj.BIMObjectProperties.psets], 'class': self.get_ifc_class(obj.name), 'representations': self.get_object_representation_names(obj), 'attributes': self.get_object_attributes(obj) } results.append(type_product) library['rel_declares_type_products'].append(index) for key in obj.keys(): if key[0:3] == 'Doc': self.rel_associates_document_type.setdefault( obj[key], []).append(type_product) elif key[0:5] == 'Class': self.rel_associates_classification_type.setdefault( obj[key], []).append(type_product) elif key[0:9] == 'Objective': self.rel_associates_constraint_objective_type.setdefault( obj[key], []).append(type_product) index += 1 except Exception as e: self.ifc_export_settings.logger.error( 'The type product "{}" could not be parsed: {}'.format(obj.name, e.args)) return results def get_object_representation_names(self, obj): names = [] if obj.data is None: return names if not self.is_mesh_context_sensitive(obj.data.name): names.append('Model/Body/MODEL_VIEW/{}'.format(obj.data.name)) name = self.get_ifc_representation_name(obj.data.name) for context in self.ifc_export_settings.context_tree: for subcontext in context['subcontexts']: for target_view in subcontext['target_views']: mesh_name = '/'.join([context['name'], subcontext['name'], target_view, name]) try: mesh = bpy.data.meshes[mesh_name] except: continue names.append(mesh_name) return names def get_spatial_structure_elements_tree(self, collections, name_filter): collection_tree = [] for collection in collections: if not self.is_a_spatial_structure_element(self.get_ifc_class(collection.name)): continue children = self.get_spatial_structure_elements_tree( collection.children, name_filter) if collection.name in name_filter \ or children: collection_tree.append({ 'reference': self.get_spatial_structure_element_reference(collection.name), 'children': children }) return collection_tree def get_spatial_structure_element_reference(self, name): return ['{}/{}'.format(e['class'], e['attributes']['Name']) for e in self.spatial_structure_elements].index(name) def get_type_product_reference(self, name): return [p['attributes']['Name'] for p in self.type_products].index(self.get_ifc_name(name)) def get_ifc_class(self, name): return name.split('/')[0] def get_ifc_name(self, name): try: return name.split('/')[1] except IndexError: self.ifc_export_settings.logger.error( 'Name "{}" does not follow the format of "IfcClass/Name"'.format(name)) def is_a_spatial_structure_element(self, class_name): # We assume that any collection we can't identify is a spatial structure return class_name[0:3] == 'Ifc' \ and not self.is_a_project(class_name) \ and not self.is_a_library(class_name) \ and not self.is_a_rel_aggregates(class_name) def is_a_rel_aggregates(self, class_name): return class_name == 'IfcRelAggregates' def is_a_project(self, class_name): return class_name == 'IfcProject' def is_a_library(self, class_name): return class_name == 'IfcProjectLibrary' def is_a_type(self, class_name): return class_name[0:3] == 'Ifc' and class_name[-4:] == 'Type' class IfcExporter(): def __init__(self, ifc_export_settings, ifc_schema, ifc_parser, qto_calculator): self.template_file = '{}template.ifc'.format(ifc_export_settings.schema_dir) self.ifc_export_settings = ifc_export_settings self.ifc_schema = ifc_schema self.ifc_parser = ifc_parser self.qto_calculator = qto_calculator def export(self): self.file = ifcopenshell.open(self.template_file) self.set_common_definitions() self.ifc_parser.parse() self.create_units() self.create_people() self.create_organisations() self.create_rep_context() self.create_project() self.create_documents() self.create_classifications() self.create_classification_references() self.create_objectives() self.create_psets() self.create_libraries() self.create_map_conversion() self.create_representations() self.create_materials() self.create_type_products() self.create_spatial_structure_elements(self.ifc_parser.spatial_structure_elements_tree) self.create_qtos() self.create_products() self.create_styled_items() self.relate_definitions_to_contexts() self.relate_objects_to_objects() self.relate_elements_to_spatial_structures() self.relate_nested_elements_to_hosted_elements() self.relate_objects_to_types() self.relate_objects_to_qtos() self.relate_objects_to_psets() self.relate_objects_to_opening_elements() self.relate_opening_elements_to_fillings() self.relate_objects_to_materials() self.relate_objects_to_material_layer_sets() self.relate_objects_to_material_constituent_sets() self.relate_spaces_to_boundary_elements() self.relate_to_documents(self.ifc_parser.rel_associates_document_object) self.relate_to_documents(self.ifc_parser.rel_associates_document_type) self.relate_to_classifications(self.ifc_parser.rel_associates_classification_object) self.relate_to_classifications(self.ifc_parser.rel_associates_classification_type) self.relate_to_objectives(self.ifc_parser.rel_associates_constraint_objective_object) self.relate_to_objectives(self.ifc_parser.rel_associates_constraint_objective_type) self.file.write(self.ifc_export_settings.output_file) def set_common_definitions(self): # Owner history doesn't actually work like this, but for now, it does :) self.origin = self.file.by_type('IfcAxis2Placement3D')[0] self.owner_history = self.file.by_type('IfcOwnerHistory')[0] def create_units(self): for unit_type, data in self.ifc_parser.units.items(): if data['is_metric']: data['ifc'] = self.create_metric_unit(unit_type, data) else: data['ifc'] = self.create_imperial_unit(unit_type, data) self.file.createIfcUnitAssignment([u['ifc'] for u in self.ifc_parser.units.values()]) def create_metric_unit(self, unit_type, data): type_prefix = '' if unit_type == 'area': type_prefix = 'SQUARE_' elif unit_type == 'volume': type_prefix = 'CUBIC_' return self.file.createIfcSIUnit( None, '{}UNIT'.format(unit_type.upper()), SIUnitHelper.get_prefix(data['raw']), type_prefix + SIUnitHelper.get_unit_name(data['raw']) ) def create_imperial_unit(self, unit_type, data): if unit_type == 'length': dimensional_exponents = self.file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0) name_prefix = '' elif unit_type == 'area': dimensional_exponents = self.file.createIfcDimensionalExponents(2, 0, 0, 0, 0, 0, 0) name_prefix = 'square' elif unit_type == 'volume': dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0) name_prefix = 'cubic' si_unit = self.file.createIfcSIUnit( None, '{}UNIT'.format(unit_type.upper()), None, '{}METRE'.format(name_prefix.upper() + '_' if name_prefix else '') ) if data['raw'] == 'INCHES': name = '{}inch'.format(name_prefix + ' ' if name_prefix else '') elif data['raw'] == 'FEET': name = '{}foot'.format(name_prefix + ' ' if name_prefix else '') value_component = self.file.create_entity( 'IfcReal', **{'wrappedValue': SIUnitHelper.si_conversions[name]} ) conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) return self.file.createIfcConversionBasedUnit( dimensional_exponents, '{}UNIT'.format(unit_type.upper()), name, conversion_factor ) def create_people(self): for person in self.ifc_parser.people: if person['Roles']: person['Roles'] = self.create_roles(person['Roles']) if person['Addresses']: person['Addresses'] = self.create_addresses(person['Addresses']) self.file.create_entity('IfcPerson', **person) def create_organisations(self): for organisation in self.ifc_parser.organisations: if organisation['Roles']: organisation['Roles'] = self.create_roles(organisation['Roles']) if organisation['Addresses']: organisation['Addresses'] = self.create_addresses(organisation['Addresses']) self.file.create_entity('IfcOrganization', **organisation) def create_roles(self, roles): results = [] for role in roles: results.append(self.file.create_entity('IfcActorRole', **role)) return results def create_addresses(self, addresses): results = [] for address in addresses: is_postal_address = False for key in ['InternalLocation', 'AddressLines', 'PostalBox', 'Town', 'Region', 'PostalCode', 'Country']: if key in address: is_postal_address = True if is_postal_address: results.append(self.file.create_entity('IfcPostalAddress', **address)) else: results.append(self.file.create_entity('IfcTelecomAddress', **address)) return results def create_documents(self): for document in self.ifc_parser.documents.values(): document['ifc'] = self.file.create_entity( 'IfcDocumentReference', **document['attributes']) self.file.createIfcRelAssociatesDocument( ifcopenshell.guid.new(), None, None, None, [self.ifc_parser.project['ifc']], document['ifc']) def create_classifications(self): for classification in self.ifc_parser.classifications: classification['ifc'] = self.file.create_entity( 'IfcClassification', **classification['attributes'] ) self.file.createIfcRelAssociatesClassification( ifcopenshell.guid.new(), None, None, None, [self.ifc_parser.project['ifc']], classification['ifc']) def create_classification_references(self): for reference in self.ifc_parser.classification_references.values(): reference['attributes']['ReferencedSource'] = \ self.ifc_parser.classifications[reference['referenced_source']]['ifc'] reference['ifc'] = self.file.create_entity( 'IfcClassificationReference', **reference['attributes']) def create_objectives(self): for objective in self.ifc_parser.objectives.values(): objective['ifc'] = self.file.create_entity( 'IfcObjective', **objective['attributes']) def create_psets(self): for pset in self.ifc_parser.psets.values(): properties = self.create_pset_properties(pset) if not properties: self.ifc_export_settings.logger.error( 'No properties could be detected for the pset {}/{}'.format( pset['attributes']['Name'], pset['attributes']['Description'])) continue pset['attributes'].update({ 'GlobalId': ifcopenshell.guid.new(), 'OwnerHistory': self.owner_history, 'HasProperties': properties }) pset['ifc'] = self.file.create_entity('IfcPropertySet', **pset['attributes']) def create_pset_properties(self, pset): if pset['attributes']['Name'] in self.ifc_schema.psets: return self.create_templated_pset_properties(pset) return self.create_custom_pset_properties(pset) def create_custom_pset_properties(self, pset): properties = [] for key, value in pset['raw'].items(): properties.append( self.file.create_entity('IfcPropertySingleValue', **{ 'Name': key, 'NominalValue': self.file.create_entity('IfcLabel', value) })) return properties def create_templated_pset_properties(self, pset): properties = [] templates = self.ifc_schema.psets[pset['attributes']['Name']]['HasPropertyTemplates'] for name, data in templates.items(): if name not in pset['raw']: continue if data.TemplateType == 'P_SINGLEVALUE': if data.PrimaryMeasureType: value_type = data.PrimaryMeasureType else: # The IFC spec is missing some, so we provide a fallback value_type = 'IfcLabel' nominal_value = self.file.create_entity( value_type, self.cast_to_base_type(value_type, pset['raw'][name])) properties.append( self.file.create_entity('IfcPropertySingleValue', **{ 'Name': name, 'NominalValue': nominal_value })) invalid_pset_keys = [k for k in pset['raw'].keys() if k not in templates.keys()] if invalid_pset_keys: self.ifc_export_settings.logger.error( 'One or more properties were invalid in the pset {}/{}: {}'.format( pset['attributes']['Name'], pset['attributes']['Description'], invalid_pset_keys)) return properties def cast_to_base_type(self, var_type, value): if var_type not in self.ifc_schema.type_map: return value elif self.ifc_schema.type_map[var_type] == 'float': return float(value) elif self.ifc_schema.type_map[var_type] == 'integer': return int(value) elif self.ifc_schema.type_map[var_type] == 'bool': return True if value.lower() in ['1', 't', 'true', 'yes', 'y', 'uh-huh'] else False return str(value) def create_rep_context(self): self.ifc_rep_context = {} self.ifc_rep_context['Model'] = { 'ifc': self.file.createIfcGeometricRepresentationContext( None, 'Model', 3, 1.0E-05, self.origin)} if 'Plan' in self.ifc_export_settings.contexts: self.ifc_rep_context['Plan'] = { 'ifc': self.file.createIfcGeometricRepresentationContext( None, 'Plan', 2, 1.0E-05, self.origin)} for context in self.ifc_export_settings.context_tree: for subcontext in context['subcontexts']: self.ifc_rep_context[context['name']][subcontext['name']] = {} for target_view in subcontext['target_views']: self.ifc_rep_context[context['name']][subcontext['name']][target_view] = { 'ifc': self.file.createIfcGeometricRepresentationSubContext( subcontext['name'], context['name'], None, None, None, None, self.ifc_rep_context[context['name']]['ifc'], None, target_view, None)} def create_project(self): self.ifc_parser.project['attributes'].update({ 'RepresentationContexts': [c['ifc'] for c in self.ifc_rep_context.values()], 'UnitsInContext': self.file.by_type("IfcUnitAssignment")[0] }) self.ifc_parser.project['ifc'] = self.file.create_entity( self.ifc_parser.project['class'], **self.ifc_parser.project['attributes']) def create_libraries(self): for library in self.ifc_parser.libraries: library['ifc'] = self.file.create_entity(library['class'], **library['attributes']) self.file.createIfcRelDeclares( ifcopenshell.guid.new(), self.owner_history, None, None, self.ifc_parser.project['ifc'], [l['ifc'] for l in self.ifc_parser.libraries]) def create_map_conversion(self): if not self.ifc_parser.map_conversion: return self.create_target_crs() # TODO should this be hardcoded? self.ifc_parser.map_conversion['attributes']['SourceCRS'] = self.ifc_rep_context['Model']['ifc'] self.ifc_parser.map_conversion['attributes']['TargetCRS'] = self.ifc_parser.target_crs['ifc'] self.ifc_parser.map_conversion['ifc'] = self.file.create_entity( 'IfcMapConversion', **self.ifc_parser.map_conversion['attributes'] ) def create_target_crs(self): self.ifc_parser.target_crs['attributes']['MapUnit'] = self.file.createIfcSIUnit( None, 'LENGTHUNIT', SIUnitHelper.get_prefix(self.ifc_parser.target_crs['attributes']['MapUnit']), SIUnitHelper.get_unit_name(self.ifc_parser.target_crs['attributes']['MapUnit']) ) self.ifc_parser.target_crs['ifc'] = self.file.create_entity( 'IfcProjectedCRS', **self.ifc_parser.target_crs['attributes'] ) def create_type_products(self): for product in self.ifc_parser.type_products: placement = self.create_ifc_axis_2_placement_3d( product['location'], product['up_axis'], product['forward_axis'] ) if product['representations']: maps = [] for representation in product['representations']: maps.append(self.file.createIfcRepresentationMap( placement, self.ifc_parser.representations[representation]['ifc'])) product['attributes']['RepresentationMaps'] = maps if product['psets']: product['attributes'].update({'HasPropertySets':[ self.ifc_parser.psets[pset]['ifc'] for pset in product['psets']] }) if product['class'] == 'IfcDoorType' \ and product['attributes']['Name'] in self.ifc_parser.door_attributes: self.add_predefined_attributes_to_type_product( product, self.ifc_parser.door_attributes[product['attributes']['Name']] ) elif product['class'] == 'IfcWindowType' \ and product['attributes']['Name'] in self.ifc_parser.window_attributes: self.add_predefined_attributes_to_type_product( product, self.ifc_parser.window_attributes[product['attributes']['Name']] ) try: product['ifc'] = self.file.create_entity(product['class'], **product['attributes']) except RuntimeError as e: self.ifc_export_settings.logger.error( 'The type product "{}/{}" could not be created: {}'.format( product['class'], product['attributes']['Name'], e.args ) ) def add_predefined_attributes_to_type_product(self, product, attributes): self.create_predefined_attributes(attributes) product['attributes'].setdefault('HasPropertySets', []) for attribute in attributes: product['attributes']['HasPropertySets'].append(attribute['ifc']) def create_predefined_attributes(self, attributes): for attribute in attributes: attribute['ifc'] = self.file.create_entity( attribute['pset_name'], **{k: float(v) if v.replace('.', '', 1).isdigit() else v for k, v in attribute['raw'].items()} ) def relate_definitions_to_contexts(self): for library in self.ifc_parser.libraries: self.file.createIfcRelDeclares( ifcopenshell.guid.new(), self.owner_history, None, None, library['ifc'], [self.ifc_parser.type_products[t]['ifc'] for t in library['rel_declares_type_products']]) def relate_objects_to_objects(self): for relating_object, related_objects_reference in self.ifc_parser.rel_aggregates.items(): relating_object = self.ifc_parser.products[relating_object] related_objects = [self.ifc_parser.products[o]['ifc'] for o in self.ifc_parser.aggregates[related_objects_reference]] self.file.createIfcRelAggregates( ifcopenshell.guid.new(), self.owner_history, relating_object['attributes']['Name'], None, relating_object['ifc'], related_objects) def create_spatial_structure_elements(self, element_tree, relating_object=None): if relating_object == None: relating_object = self.ifc_parser.project['ifc'] placement_rel_to = None else: placement_rel_to = relating_object.ObjectPlacement related_objects = [] for node in element_tree: element = self.ifc_parser.spatial_structure_elements[node['reference']] element['attributes'].update({ 'OwnerHistory': self.owner_history, # TODO: unhardcode 'ObjectPlacement': self.file.createIfcLocalPlacement(placement_rel_to, self.origin) }) element['ifc'] = self.file.create_entity(element['class'], **element['attributes']) related_objects.append(element['ifc']) self.create_spatial_structure_elements(node['children'], element['ifc']) if related_objects: self.file.createIfcRelAggregates( ifcopenshell.guid.new(), self.owner_history, None, None, relating_object, related_objects) def create_styled_items(self): for styled_item in self.ifc_parser.styled_items: product = self.ifc_parser.products[ self.ifc_parser.get_product_index_from_raw_name( styled_item['related_product_name'])]['ifc'] representation_items = [] if product.Representation: for representation in product.Representation.Representations: for item in representation.Items: representation_items.append(item) for representation_item in representation_items: styled_item['ifc'] = self.create_styled_item(styled_item, representation_item) def create_styled_item(self, item, representation_item=None): styles = [] styles.append(self.create_surface_style_rendering(item)) if item['raw'].BIMMaterialProperties.is_external: styles.append(self.file.create_entity('IfcExternallyDefinedSurfaceStyle', **self.get_material_external_definition(item['raw']))) # Name is filled out because Revit treats this incorrectly as the material name surface_style = self.file.createIfcSurfaceStyle(item['attributes']['Name'], 'BOTH', styles) if self.ifc_export_settings.should_use_presentation_style_assignment: surface_style = self.file.createIfcPresentationStyleAssignment([surface_style]) return self.file.createIfcStyledItem(representation_item, [surface_style], item['attributes']['Name']) def create_materials(self): for material in self.ifc_parser.materials.values(): styled_item = self.create_styled_item(material) styled_representation = self.file.createIfcStyledRepresentation( self.ifc_rep_context['Model']['Body']['MODEL_VIEW']['ifc'], None, None, [styled_item]) material['ifc'] = self.file.createIfcMaterial(material['raw'].name, None, None) self.file.createIfcMaterialDefinitionRepresentation( material['raw'].name, None, [styled_representation], material['ifc']) if material['is_material_layer_set']: material['layer_attributes']['Material'] = material['ifc'] material['layer_ifc'] = self.file.create_entity('IfcMaterialLayer', **material['layer_attributes']) elif material['is_material_constituent_set']: material['constituent_attributes']['Material'] = material['ifc'] material['constituent_ifc'] = self.file.create_entity('IfcMaterialConstituent', **material['constituent_attributes']) def create_surface_style_rendering(self, styled_item): surface_colour = self.create_colour_rgb(styled_item['raw'].diffuse_color) rendering_attributes = {'SurfaceColour': surface_colour} rendering_attributes.update(self.get_rendering_attributes(styled_item['raw'])) return self.file.create_entity('IfcSurfaceStyleRendering', **rendering_attributes) def get_rendering_attributes(self, material): if not hasattr(material.node_tree, 'nodes') \ or 'Principled BSDF' not in material.node_tree.nodes: return {} bsdf = material.node_tree.nodes['Principled BSDF'] return { 'Transparency': (bsdf.inputs['Alpha'].default_value - 1) * -1, 'DiffuseColour': self.create_colour_rgb(bsdf.inputs['Base Color'].default_value) } def get_material_external_definition(self, material): return { 'Location': material.BIMMaterialProperties.location, 'Identification': material.BIMMaterialProperties.identification if material.BIMMaterialProperties.identification else material.name, 'Name': material.BIMMaterialProperties.name if material.BIMMaterialProperties.name else material.name } def create_colour_rgb(self, colour): return self.file.createIfcColourRgb(None, colour[0], colour[1], colour[2]) def create_representations(self): for representation in self.ifc_parser.representations.values(): representation['ifc'] = self.create_representation(representation) def create_products(self): for product in self.ifc_parser.products: self.create_product(product) def create_qtos(self): for object_name, qto in self.ifc_parser.qtos.items(): quantities = self.calculate_quantities(qto['class'], qto['raw']) qto['attributes'].update({ 'GlobalId': ifcopenshell.guid.new(), 'OwnerHistory': self.owner_history, 'Quantities': quantities }) qto['ifc'] = self.file.create_entity('IfcElementQuantity', **qto['attributes']) def create_product(self, product): if product['relating_structure']: placement_rel_to = self.ifc_parser.spatial_structure_elements[product['relating_structure']][ 'ifc'].ObjectPlacement elif product['relating_host'] is not None: placement_rel_to = self.ifc_parser.products[product['relating_host']]['ifc'].ObjectPlacement else: placement_rel_to = None if product['has_scale']: placement = self.file.createIfcLocalPlacement(placement_rel_to, self.origin) else: placement = self.file.createIfcLocalPlacement(placement_rel_to, self.create_ifc_axis_2_placement_3d(product['location'], product['up_axis'], product['forward_axis'])) product['attributes'].update({ 'OwnerHistory': self.owner_history, # TODO: unhardcode 'ObjectPlacement': placement, 'Representation': self.get_product_shape(product) }) for key, value in product['attributes'].items(): var_type = self.get_product_attribute_type(product['class'], key) if var_type is None: continue product['attributes'][key] = self.cast_to_base_type(var_type, value) try: product['ifc'] = self.file.create_entity(product['class'], **product['attributes']) except RuntimeError as e: self.ifc_export_settings.logger.error( 'The product "{}/{}" could not be created: {}'.format( product['class'], product['attributes']['Name'], e.args) ) def get_product_attribute_type(self, product_class, attribute_name): element_schema = self.ifc_schema.elements[product_class] for a in element_schema['attributes']: if a['name'] == attribute_name: return a['type'] if element_schema['parent'] in self.ifc_schema.elements: return self.get_product_attribute_type(element_schema['parent'], attribute_name) return None def get_product_shape(self, product): try: shape = self.file.createIfcProductDefinitionShape(None, None, self.get_product_shape_representations(product)) except: shape = None return shape def get_product_shape_representations(self, product): results = [] for representation_name in product['representations']: shape_representation = self.ifc_parser.representations[representation_name]['ifc'] if product['has_scale']: results.append(self.get_product_mapped_geometry(product, shape_representation)) else: results.append(shape_representation) return results def get_product_mapped_geometry(self, product, shape_representation): mapping_source = self.file.createIfcRepresentationMap(self.origin, shape_representation) mapping_target = self.file.createIfcCartesianTransformationOperator3DnonUniform( self.create_direction(product['forward_axis']), self.create_direction(product['right_axis']), self.create_cartesian_point( product['location'].x, product['location'].y, product['location'].z ), product['scale'].x, self.create_direction(product['up_axis']), product['scale'].y, product['scale'].z) mapped_item = self.file.createIfcMappedItem(mapping_source, mapping_target) return self.file.createIfcShapeRepresentation( shape_representation.ContextOfItems, shape_representation.RepresentationIdentifier, shape_representation.RepresentationType, [mapped_item]) def calculate_quantities(self, qto_name, obj): quantities = [] for index, vg in enumerate(obj.vertex_groups): if qto_name not in vg.name: continue if 'length' in vg.name.lower(): quantity = float(self.qto_calculator.get_length(obj, index)) quantities.append(self.file.createIfcQuantityLength( vg.name.split('/')[1], None, self.ifc_parser.units['length']['ifc'], quantity)) elif 'area' in vg.name.lower(): quantity = float(self.qto_calculator.get_area(obj, index)) quantities.append(self.file.createIfcQuantityArea( vg.name.split('/')[1], None, self.ifc_parser.units['area']['ifc'], quantity)) elif 'volume' in vg.name.lower(): quantity = float(self.qto_calculator.get_volume(obj, index)) quantities.append(self.file.createIfcQuantityVolume( vg.name.split('/')[1], None, self.ifc_parser.units['volume']['ifc'], quantity)) if not quantity: self.ifc_export_settings.logger.warning('The calculated quantity {} for {} is zero.'.format( vg.name, obj.name)) return quantities def create_ifc_axis_2_placement_3d(self, point, up, forward): return self.file.createIfcAxis2Placement3D( self.create_cartesian_point(point.x, point.y, point.z), self.file.createIfcDirection((up.x, up.y, up.z)), self.file.createIfcDirection((forward.x, forward.y, forward.z))) def create_representation(self, representation): self.ifc_vertices = [] self.ifc_edges = [] self.ifc_faces = [] if representation['is_generated'] \ and representation['subcontext'] == 'Box': return self.create_box_representation(representation) elif representation['subcontext'] == 'CoG': return self.create_cog_representation(representation) elif representation['context'] == 'Plan' \ or representation['subcontext'] == 'Axis' \ or representation['is_wireframe']: return self.create_wireframe_representation(representation) elif representation['subcontext'] == 'SurveyPoints': return self.create_geometric_curve_set_representation(representation) elif representation['is_curve']: return self.create_curve_representation(representation) elif representation['is_swept_solid']: return self.create_swept_solid_representation(representation) else: return self.create_solid_representation(representation) def create_box_representation(self, representation): obj = representation['raw_object'] bounding_box = self.file.createIfcBoundingBox( self.create_cartesian_point( obj.bound_box[0][0], obj.bound_box[0][1], obj.bound_box[0][2] ), obj.dimensions[0], obj.dimensions[1], obj.dimensions[2] ) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'BoundingBox', [bounding_box]) def create_cog_representation(self, representation): mesh = representation['raw'] cog = self.create_cartesian_point( mesh.vertices[0].co.x, mesh.vertices[0].co.y, mesh.vertices[0].co.z) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'BoundingBox', [cog]) def create_wireframe_representation(self, representation): mesh = representation['raw'] self.create_vertices(mesh.vertices) for edge in mesh.edges: self.ifc_edges.append(self.file.createIfcPolyline([ self.ifc_vertices[v] for v in edge.vertices])) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'Curve', self.ifc_edges) def create_geometric_curve_set_representation(self, representation): mesh = representation['raw'] self.create_vertices(mesh.vertices) edges = list(mesh.edges) loop_vertices = [] loops = [] # Not a fast algorithm, but easy while edges: for i, edge in enumerate(edges): if edge.vertices[0] in loop_vertices \ and edge.vertices[1] in loop_vertices: del edges[i] loop_vertex_indices = self.get_loop_from_edges(edges) loop_vertices.extend(loop_vertex_indices) loops.append(self.file.createIfcPolyline([ self.ifc_vertices[i] for i in loop_vertex_indices])) geometric_curve_set = self.file.createIfcGeometricCurveSet(loops) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'GeometricCurveSet', [geometric_curve_set]) # https://medium.com/@behreajj/scripting-curves-in-blender-with-python-c487097efd13 # https://blender.stackexchange.com/questions/30597/python-up-vector-math-for-curve def bezier_tangent(self, pt0=Vector(), pt1=Vector(), pt2=Vector(), pt3=Vector(), step=0.5): # Return early if step is out of bounds [0, 1]. if step <= 0.0: return pt1 - pt0 if step >= 1.0: return pt3 - pt2 # Find coefficients. u = 1.0 - step ut6 = u * step * 6.0 tsq3 = step * step * 3.0 usq3 = u * u * 3.0 # Find tangent and return. return (pt1 - pt0) * usq3 + (pt2 - pt1) * ut6 + (pt3 - pt2) * tsq3 def create_curve_representation(self, representation): # TODO: support unclosed surfaces swept_area = self.file.createIfcArbitraryClosedProfileDef('AREA', None, self.create_curve(representation['raw'].bevel_object.data)) swept_area_solids = [] for spline in representation['raw'].splines: direction = spline.bezier_points[1].co - spline.bezier_points[0].co unit_direction = direction.normalized() # This can be used in the future when dealing with non vector curves # curr_point = spline.bezier_points[0] # next_point = spline.bezier_points[1] # j_percent = 0 # direction = self.bezier_tangent( # pt0=curr_point.co, # pt1=curr_point.handle_right, # pt2=next_point.handle_left, # pt3=next_point.co, # step=j_percent) tilt_matrix = Matrix.Rotation(-spline.bezier_points[0].tilt, 4, 'Z') x_axis = unit_direction.to_track_quat('-Y', 'Z') @ Vector((1, 0, 0)) @ tilt_matrix position = self.create_ifc_axis_2_placement_3d( spline.bezier_points[0].co, unit_direction, x_axis) swept_area_solids.append(self.file.createIfcExtrudedAreaSolid( swept_area, position, self.file.createIfcDirection((0., 0., 1.)), self.convert_si_to_unit(direction.length))) # TODO: support other types of swept areas # swept_area_solid = self.file.createIfcFixedReferenceSweptAreaSolid( # swept_area, self.origin, self.create_curve(representation['raw']), # 0., 1., self.file.createIfcDirection((0.0, -1.0, 0.0))) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'AdvancedSweptSolid', swept_area_solids) def create_curve(self, curve): # TODO: support interpolated curves, not just polylines points = [] for point in curve.splines[0].bezier_points: points.append(self.create_cartesian_point( point.co.x, point.co.y, point.co.z)) if curve.splines[0].use_cyclic_u: points.append(points[0]) return self.file.createIfcPolyline(points) def create_swept_solid_representation(self, representation): obj = representation['raw_object'] mesh = representation['raw'] items = [] for swept_solid in mesh.BIMMeshProperties.swept_solids: extrusion_edge = self.get_edges_in_v_indices(obj, json.loads(swept_solid.extrusion))[0] inner_curves = [] if swept_solid.inner_curves: for indices in json.loads(swept_solid.inner_curves): loop = self.get_loop_from_v_indices(obj, indices) curve_ucs = self.get_curve_profile_coordinate_system(obj, loop) inner_curves.append( self.create_polyline_from_loop(obj, loop, curve_ucs)) outer_curve_loop = self.get_loop_from_v_indices(obj, json.loads(swept_solid.outer_curve)) curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop) outer_curve = self.create_polyline_from_loop(obj, outer_curve_loop, curve_ucs) if inner_curves: curve = self.file.createIfcArbitraryProfileDefWithVoids('AREA', None, outer_curve, inner_curves) else: curve = self.file.createIfcArbitraryClosedProfileDef('AREA', None, outer_curve) direction = self.get_extrusion_direction(obj, outer_curve_loop, extrusion_edge, curve_ucs) unit_direction = direction.normalized() position = self.create_ifc_axis_2_placement_3d( curve_ucs['center'], curve_ucs['z_axis'], curve_ucs['x_axis']) items.append(self.file.createIfcExtrudedAreaSolid( curve, position, self.file.createIfcDirection(( unit_direction.x, unit_direction.y, unit_direction.z)), self.convert_si_to_unit(direction.length))) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'SweptSolid', items) def get_start_and_end_of_extrusion(self, profile_points, extrusion_edge): if extrusion_edge.vertices[0] in profile_points: return (extrusion_edge.vertices[0], extrusion_edge.vertices[1]) return (extrusion_edge.vertices[1], extrusion_edge.vertices[0]) def get_curve_profile_coordinate_system(self, obj, loop): profile_face = bpy.data.meshes.new('profile_face') profile_verts = [( obj.data.vertices[p].co.x, obj.data.vertices[p].co.y, obj.data.vertices[p].co.z ) for p in loop] profile_faces = [tuple(range(0, len(profile_verts)))] profile_face.from_pydata(profile_verts, [], profile_faces) center = profile_face.polygons[0].center x_axis = (obj.data.vertices[loop[0]].co - center).normalized() z_axis = profile_face.polygons[0].normal.normalized() y_axis = z_axis.cross(x_axis).normalized() matrix = Matrix((x_axis, y_axis, z_axis)) matrix.normalize() return { 'center': center, 'x_axis': x_axis, 'y_axis': y_axis, 'z_axis': z_axis, 'matrix': matrix.to_4x4() @ Matrix.Translation(-center) } def create_polyline_from_loop(self, obj, loop, curve_ucs): points = [] for point in loop: transformed_point = curve_ucs['matrix'] @ obj.data.vertices[point].co points.append(self.create_cartesian_point( transformed_point.x, transformed_point.y)) points.append(points[0]) return self.file.createIfcPolyline(points) def get_extrusion_direction(self, obj, outer_curve_loop, extrusion_edge, curve_ucs): start, end = self.get_start_and_end_of_extrusion(outer_curve_loop, extrusion_edge) return curve_ucs['matrix'] @ ( curve_ucs['center'] + (obj.data.vertices[end].co - obj.data.vertices[start].co)) def get_loop_from_v_indices(self, obj, indices): edges = self.get_edges_in_v_indices(obj, indices) loop = self.get_loop_from_edges(edges) loop.pop(-1) return loop def get_edges_in_v_indices(self, obj, indices): return [e for e in obj.data.edges if (e.vertices[0] in indices and e.vertices[1] in indices)] def get_loop_from_edges(self, edges): while edges: currentEdge = edges.pop() startVert = currentEdge.vertices[0] endVert = currentEdge.vertices[1] polyLine = [startVert, endVert] ok = 1 while ok: ok = 0 i = len(edges) while i: i -= 1 ed = edges[i] if ed.vertices[0] == endVert: polyLine.append(ed.vertices[1]) endVert = polyLine[-1] ok = 1 del edges[i] elif ed.vertices[1] == endVert: polyLine.append(ed.vertices[0]) endVert = polyLine[-1] ok = 1 del edges[i] elif ed.vertices[0] == startVert: polyLine.insert(0, ed.vertices[1]) startVert = polyLine[0] ok = 1 del edges[i] elif ed.vertices[1] == startVert: polyLine.insert(0, ed.vertices[0]) startVert = polyLine[0] ok = 1 del edges[i] return polyLine def create_solid_representation(self, representation): mesh = representation['raw'] self.create_vertices(mesh.vertices) for polygon in mesh.polygons: self.ifc_faces.append(self.file.createIfcFace([ self.file.createIfcFaceOuterBound( self.file.createIfcPolyLoop([self.ifc_vertices[vertice] for vertice in polygon.vertices]), True)])) return self.file.createIfcShapeRepresentation( self.ifc_rep_context[representation['context']][representation['subcontext']][ representation['target_view']]['ifc'], representation['subcontext'], 'Brep', [self.file.createIfcFacetedBrep(self.file.createIfcClosedShell(self.ifc_faces))]) def create_vertices(self, vertices): self.ifc_vertices.extend( [self.file.createIfcCartesianPoint(self.convert_si_to_unit(v.co)) for v in vertices] ) def create_cartesian_point(self, x, y, z=None): x = self.convert_si_to_unit(x) y = self.convert_si_to_unit(y) if z is None: return self.file.createIfcCartesianPoint((x, y)) z = self.convert_si_to_unit(z) return self.file.createIfcCartesianPoint((x, y, z)) def create_direction(self, vector): return self.file.createIfcDirection((vector.x, vector.y, vector.z)) def relate_objects_to_opening_elements(self): for relating_building_element, related_opening_elements in self.ifc_parser.rel_voids_elements.items(): for related_opening_element in related_opening_elements: self.file.createIfcRelVoidsElement( ifcopenshell.guid.new(), self.owner_history, None, None, related_building_element, related_opening_element ) def relate_opening_elements_to_fillings(self): for relating_opening_element, related_building_elements in self.ifc_parser.rel_fills_elements.items(): for related_building_element in related_building_elements: self.file.createIfcRelFillsElement( ifcopenshell.guid.new(), self.owner_history, None, None, relating_opening_element, related_building_element ) def relate_elements_to_spatial_structures(self): for relating_structure, related_elements in self.ifc_parser.rel_contained_in_spatial_structure.items(): self.file.createIfcRelContainedInSpatialStructure( ifcopenshell.guid.new(), self.owner_history, None, None, [self.ifc_parser.products[e]['ifc'] for e in related_elements], self.ifc_parser.spatial_structure_elements[relating_structure]['ifc']) def relate_nested_elements_to_hosted_elements(self): for relating_object, related_objects in self.ifc_parser.rel_nests.items(): self.file.createIfcRelNests( ifcopenshell.guid.new(), self.owner_history, None, None, self.ifc_parser.products[relating_object]['ifc'], [o['ifc'] for o in related_objects]) def relate_objects_to_types(self): for relating_type, related_objects in self.ifc_parser.rel_defines_by_type.items(): self.file.createIfcRelDefinesByType( ifcopenshell.guid.new(), self.owner_history, None, None, [self.ifc_parser.products[o]['ifc'] for o in related_objects], self.ifc_parser.type_products[relating_type]['ifc']) def relate_objects_to_qtos(self): for relating_property_key, related_objects in self.ifc_parser.rel_defines_by_qto.items(): self.file.createIfcRelDefinesByProperties( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], self.ifc_parser.qtos[relating_property_key]['ifc']) def relate_objects_to_psets(self): for relating_property_key, related_objects in self.ifc_parser.rel_defines_by_pset.items(): self.file.createIfcRelDefinesByProperties( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], self.ifc_parser.psets[relating_property_key]['ifc']) def relate_objects_to_materials(self): if not self.ifc_export_settings.has_representations: return for relating_material_key, related_objects in self.ifc_parser.rel_associates_material.items(): self.file.createIfcRelAssociatesMaterial( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], self.ifc_parser.materials[relating_material_key]['ifc']) def relate_objects_to_material_layer_sets(self): if not self.ifc_export_settings.has_representations: return for product_index, related_materials in self.ifc_parser.rel_associates_material_layer_set.items(): material_layer_set = self.file.create_entity('IfcMaterialLayerSet', **{ 'MaterialLayers': [self.ifc_parser.materials[m]['layer_ifc'] for m in related_materials] }) self.file.createIfcRelAssociatesMaterial( ifcopenshell.guid.new(), self.owner_history, None, None, [self.ifc_parser.products[product_index]['ifc']], material_layer_set) def relate_objects_to_material_constituent_sets(self): if not self.ifc_export_settings.has_representations: return for product_index, related_materials in self.ifc_parser.rel_associates_material_constituent_set.items(): material_constituent_set = self.file.create_entity('IfcMaterialConstituentSet', **{ 'MaterialConstituents': [self.ifc_parser.materials[m]['constituent_ifc'] for m in related_materials] }) self.file.createIfcRelAssociatesMaterial( ifcopenshell.guid.new(), self.owner_history, None, None, [self.ifc_parser.products[product_index]['ifc']], material_constituent_set) def relate_spaces_to_boundary_elements(self): for relating_space_index, relationships, in self.ifc_parser.rel_space_boundaries.items(): for relationship in relationships: relationship['attributes']['GlobalId'] = ifcopenshell.guid.new() relationship['attributes']['RelatedBuildingElement'] = self.ifc_parser.products[ self.ifc_parser.get_product_index_from_raw_name( relationship['related_building_element_raw_name'])]['ifc'] relationship['attributes']['RelatingSpace'] = self.ifc_parser.products[relating_space_index]['ifc'] relationship['attributes']['ConnectionGeometry'] = self.create_connection_geometry( self.ifc_parser.products[relating_space_index], relationship['connection_geometry_face_index']) self.file.create_entity(relationship['class'], **relationship['attributes']) def create_connection_geometry(self, product, face_index): mesh = product['raw'].data polygon = mesh.polygons[int(face_index)] vertex_on_polygon = mesh.vertices[polygon.vertices[0]].co center = polygon.center normal = polygon.normal forward = center - vertex_on_polygon return self.file.createIfcFaceSurface([self.file.createIfcFaceOuterBound( self.file.createIfcPolyLoop([ self.create_cartesian_point( mesh.vertices[vertice].co.x, mesh.vertices[vertice].co.y, mesh.vertices[vertice].co.z) for vertice in polygon.vertices]), True)], self.file.createIfcPlane(self.file.createIfcAxis2Placement3D( self.create_cartesian_point(center.x, center.y, center.z), self.file.createIfcDirection((normal.x, normal.y, normal.z)), self.file.createIfcDirection((forward.x, forward.y, forward.z)))), True) def relate_to_documents(self, relationships): for relating_document_key, related_objects in relationships.items(): self.file.createIfcRelAssociatesDocument( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], self.ifc_parser.documents[relating_document_key]['ifc']) def relate_to_classifications(self, relationships): for relating_key, related_objects in relationships.items(): self.file.createIfcRelAssociatesClassification( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], self.ifc_parser.classification_references[relating_key]['ifc']) def relate_to_objectives(self, relationships): for relating_key, related_objects in relationships.items(): self.file.createIfcRelAssociatesConstraint( ifcopenshell.guid.new(), self.owner_history, None, None, [o['ifc'] for o in related_objects], None, self.ifc_parser.objectives[relating_key]['ifc']) def convert_si_to_unit(self, co): return co / self.ifc_parser.unit_scale class IfcExportSettings: def __init__(self): self.logger = None self.schema_dir = None self.data_dir = None self.output_file = None self.has_representations = True self.has_quantities = True self.contexts = ['Model', 'Plan'] self.subcontexts = ['Axis', 'FootPrint', 'Reference', 'Body', 'Clearance', 'CoG', 'SurveyPoints'] self.generated_subcontexts = ['Box'] self.target_views = ['GRAPH_VIEW', 'SKETCH_VIEW', 'MODEL_VIEW', 'PLAN_VIEW', 'REFLECTED_PLAN_VIEW', 'SECTION_VIEW', 'ELEVATION_VIEW', 'USERDEFINED', 'NOTDEFINED'] self.should_export_all_materials_as_styled_items = False self.should_use_presentation_style_assignment = False # TODO make this configurable via UI self.context_tree = self.build_context_tree() def build_context_tree(self): tree = [] for context in self.contexts: subcontexts = [] for subcontext in self.subcontexts + self.generated_subcontexts: target_views = [] for target_view in self.target_views: if context == 'Model' \ and target_view != 'MODEL_VIEW': continue elif context == 'Plan' \ and target_view == 'MODEL_VIEW': continue target_views.append(target_view) subcontexts.append({ 'name': subcontext, 'target_views': target_views }) tree.append({ 'name': context, 'subcontexts': subcontexts }) return tree