mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 18:21:59 +00:00
1570 lines
68 KiB
Python
1570 lines
68 KiB
Python
import ifcopenshell
|
|
import ifcopenshell.geom
|
|
import ifcopenshell.util.geolocation
|
|
import ifcopenshell.util.selector
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.unit
|
|
import bpy
|
|
import bmesh
|
|
import os
|
|
import re
|
|
import shutil
|
|
import threading
|
|
import json
|
|
import time
|
|
import mathutils
|
|
import math
|
|
import multiprocessing
|
|
import zipfile
|
|
import tempfile
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from itertools import cycle
|
|
from datetime import datetime
|
|
from blenderbim.bim.module.context.data import Data as ContextData
|
|
from blenderbim.bim.ifc import IfcStore
|
|
from . import schema
|
|
|
|
|
|
class FileCopy(threading.Thread):
|
|
def __init__(self, file_path, destination):
|
|
threading.Thread.__init__(self)
|
|
self.file_path = file_path
|
|
self.destination = destination
|
|
|
|
def run(self):
|
|
shutil.copy(self.file_path, self.destination)
|
|
|
|
|
|
class MaterialCreator:
|
|
def __init__(self, ifc_import_settings, ifc_importer):
|
|
self.mesh = None
|
|
self.materials = {}
|
|
self.parsed_meshes = set()
|
|
self.ifc_import_settings = ifc_import_settings
|
|
self.ifc_importer = ifc_importer
|
|
|
|
def create(self, element, obj, mesh):
|
|
self.obj = obj
|
|
self.mesh = mesh
|
|
self.parse_material(element)
|
|
if (hasattr(element, "Representation") and not element.Representation) or (
|
|
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
|
|
):
|
|
return
|
|
if not self.mesh or self.mesh.name in self.parsed_meshes:
|
|
return
|
|
self.parsed_meshes.add(self.mesh.name)
|
|
if self.parse_representations(element):
|
|
self.assign_material_slots_to_faces(obj)
|
|
|
|
def parse_representations(self, element):
|
|
has_parsed = False
|
|
if hasattr(element, "Representation"):
|
|
for representation in element.Representation.Representations:
|
|
if self.parse_representation(representation):
|
|
has_parsed = True
|
|
elif hasattr(element, "RepresentationMaps"):
|
|
for representation_map in element.RepresentationMaps:
|
|
if self.parse_representation(representation_map.MappedRepresentation):
|
|
has_parsed = True
|
|
return has_parsed
|
|
|
|
def parse_representation(self, representation):
|
|
has_parsed = False
|
|
representation_items = self.resolve_mapped_representation_items(representation)
|
|
for item in representation_items:
|
|
if self.parse_representation_item(item):
|
|
has_parsed = True
|
|
return has_parsed
|
|
|
|
def parse_representation_item(self, item):
|
|
if not item.StyledByItem:
|
|
return
|
|
|
|
item_id = self.mesh.BIMMeshProperties.ifc_item_ids.add()
|
|
item_id.name = str(item.id())
|
|
|
|
styled_item = item.StyledByItem[0] # Cardinality is S[0:1]
|
|
style_name = self.get_surface_style_name(styled_item)
|
|
|
|
if not style_name:
|
|
return
|
|
|
|
if self.mesh.materials.get(style_name):
|
|
item_id.slot_index = self.mesh.materials.find(style_name)
|
|
return True
|
|
|
|
style = bpy.data.materials.get(style_name)
|
|
if not style:
|
|
style = bpy.data.materials.new(style_name)
|
|
self.parse_styled_item(styled_item, style)
|
|
|
|
self.assign_style_to_mesh(style)
|
|
item_id.slot_index = len(self.mesh.materials) - 1
|
|
return True
|
|
|
|
def assign_material_slots_to_faces(self, obj):
|
|
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
|
|
return
|
|
if len(obj.material_slots) == 1:
|
|
return
|
|
material_to_slot = {}
|
|
for i, material in enumerate(self.mesh["ios_materials"]):
|
|
if material == "NULLMAT":
|
|
continue
|
|
elif "surface-style-" in material:
|
|
material = material.split("-")[2]
|
|
if len(bytes(material, "utf-8")) > 63: # Blender material names are up to 63 UTF-8 bytes
|
|
material = bytes(material, "utf-8")[0:63].decode("utf-8")
|
|
|
|
slot_index = obj.material_slots.find(material)
|
|
if slot_index == -1:
|
|
# If we can't find the material, it is possible that the
|
|
# material name is duplicated, and so a '.001' is added.
|
|
# The maximum characters for the material name is 59 in this
|
|
# scenario.
|
|
material = bytes(material, "utf-8")[0:59].decode("utf-8")
|
|
slot_index = [self.canonicalise_material_name(s.name) for s in obj.material_slots].index(material)
|
|
material_to_slot[i] = slot_index
|
|
|
|
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
|
|
material_index = [
|
|
(material_to_slot[mat_id] if mat_id != -1 else 0) for mat_id in self.mesh["ios_material_ids"]
|
|
]
|
|
self.mesh.polygons.foreach_set("material_index", material_index)
|
|
|
|
def canonicalise_material_name(self, name):
|
|
return re.sub(r"\.[0-9]{3}$", "", name)
|
|
|
|
def parse_material(self, element):
|
|
for association in element.HasAssociations:
|
|
if association.is_a("IfcRelAssociatesMaterial"):
|
|
material_select = association.RelatingMaterial
|
|
if material_select.is_a("IfcMaterialDefinition"):
|
|
self.create_definition(material_select)
|
|
elif material_select.is_a("IfcMaterialUsageDefinition"):
|
|
self.create_usage_definition(material_select)
|
|
elif material_select.is_a("IfcMaterialList"):
|
|
# Note that lists are deprecated
|
|
self.create_material_list(material_select)
|
|
# To support IFC2X3 equivalent of IfcMaterialDefinition
|
|
elif material_select.is_a("IfcMaterial") or material_select.is_a("IfcMaterialLayerSet"):
|
|
self.create_definition(material_select)
|
|
# To support IFC2X3 equivalent of IfcMaterialUsageDefinition
|
|
elif material_select.is_a("IfcMaterialLayerSetUsage"):
|
|
self.create_usage_definition(material_select)
|
|
# IFC2X3 supports assigning a material layer directly. This is silly.
|
|
elif material_select.is_a("IfcMaterialLayer"):
|
|
pass
|
|
|
|
def create_layer_set_usage(self, usage):
|
|
# TODO import rest of the layer set usage data
|
|
self.create_definition(usage.ForLayerSet)
|
|
|
|
def create_definition(self, material):
|
|
if material.is_a("IfcMaterial"):
|
|
self.create_single(material)
|
|
elif material.is_a("IfcMaterialConstituentSet"):
|
|
self.create_constituent_set(material)
|
|
elif material.is_a("IfcMaterialLayerSet"):
|
|
self.create_layer_set(material)
|
|
elif material.is_a("IfcMaterialProfileSet"):
|
|
self.create_profile_set(material)
|
|
|
|
def create_usage_definition(self, material):
|
|
if material.is_a("IfcMaterialLayerSetUsage"):
|
|
self.create_layer_set_usage(material)
|
|
elif material.is_a("IfcMaterialProfileSetUsage"):
|
|
pass # TODO
|
|
|
|
def create_single(self, material):
|
|
if material.Name not in self.materials:
|
|
self.create_new_single(material)
|
|
|
|
def create_layer_set(self, layer_set):
|
|
for layer in layer_set.MaterialLayers:
|
|
if layer.Material:
|
|
if layer.Material.Name not in self.materials:
|
|
self.create_new_single(layer.Material)
|
|
|
|
def create_constituent_set(self, constituent_set):
|
|
for constituent in constituent_set.MaterialConstituents:
|
|
if constituent.Material.Name not in self.materials:
|
|
self.create_new_single(constituent.Material)
|
|
|
|
def create_profile_set(self, profile_set):
|
|
for profile in profile_set.MaterialProfiles:
|
|
if profile.Material.Name not in self.materials:
|
|
self.create_new_single(profile.Material)
|
|
|
|
def create_material_list(self, material_list):
|
|
for material in material_list.Materials:
|
|
if material.Name not in self.materials:
|
|
self.create_new_single(material)
|
|
|
|
def create_new_single(self, material):
|
|
self.materials[material.Name] = obj = bpy.data.materials.new(material.Name)
|
|
obj.BIMObjectProperties.ifc_definition_id = int(material.id())
|
|
if not material.HasRepresentation or not material.HasRepresentation[0].Representations:
|
|
return
|
|
for representation in material.HasRepresentation[0].Representations:
|
|
if not representation.Items:
|
|
continue
|
|
for item in representation.Items:
|
|
if not item.is_a("IfcStyledItem"):
|
|
continue
|
|
self.parse_styled_item(item, obj)
|
|
|
|
def get_surface_style_name(self, styled_item):
|
|
if styled_item.Name:
|
|
return styled_item.Name
|
|
styles = self.get_styled_item_styles(styled_item)
|
|
for style in styles:
|
|
if not style.is_a("IfcSurfaceStyle"):
|
|
continue
|
|
if style.Name:
|
|
return style.Name
|
|
return str(style.id())
|
|
return None # We only support surface styles right now
|
|
|
|
def parse_styled_item(self, styled_item, material):
|
|
styles = self.get_styled_item_styles(styled_item)
|
|
for style in styles:
|
|
if not style.is_a("IfcSurfaceStyle"):
|
|
continue
|
|
material.BIMMaterialProperties.ifc_style_id = int(style.id())
|
|
for surface_style in style.Styles:
|
|
if surface_style.is_a("IfcSurfaceStyleShading"):
|
|
alpha = 1.0
|
|
# Transparency was added in IFC4
|
|
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
|
|
alpha = 1 - surface_style.Transparency
|
|
material.diffuse_color = (
|
|
surface_style.SurfaceColour.Red,
|
|
surface_style.SurfaceColour.Green,
|
|
surface_style.SurfaceColour.Blue,
|
|
alpha,
|
|
)
|
|
|
|
# IfcPresentationStyleAssignment is deprecated as of IFC4
|
|
# However it is still widely used thanks to Revit :(
|
|
def get_styled_item_styles(self, styled_item):
|
|
styles = []
|
|
for style in styled_item.Styles:
|
|
if style.is_a("IfcPresentationStyleAssignment"):
|
|
styles.extend(self.get_styled_item_styles(style))
|
|
else:
|
|
styles.append(style)
|
|
return styles
|
|
|
|
def resolve_mapped_representation_items(self, representation):
|
|
items = []
|
|
for item in representation.Items:
|
|
if item.is_a("IfcMappedItem"):
|
|
items.extend(item.MappingSource.MappedRepresentation.Items)
|
|
else:
|
|
items.append(item)
|
|
return items
|
|
|
|
def assign_style_to_mesh(self, material):
|
|
if not self.mesh:
|
|
return
|
|
self.mesh.materials.append(material)
|
|
|
|
|
|
class IfcImporter:
|
|
def __init__(self, ifc_import_settings):
|
|
self.ifc_import_settings = ifc_import_settings
|
|
self.diff = None
|
|
self.file = None
|
|
self.settings = ifcopenshell.geom.settings()
|
|
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
|
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
|
self.settings_native = ifcopenshell.geom.settings()
|
|
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
|
self.settings_2d = ifcopenshell.geom.settings()
|
|
self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
|
|
self.include_elements = []
|
|
self.exclude_elements = []
|
|
self.project = None
|
|
self.spatial_structure_elements = {}
|
|
self.elements = {}
|
|
self.type_collection = None
|
|
self.type_products = {}
|
|
self.openings = {}
|
|
self.meshes = {}
|
|
self.mesh_shapes = {}
|
|
self.time = 0
|
|
self.unit_scale = 1
|
|
self.added_data = {}
|
|
self.native_elements = {}
|
|
self.native_data = {}
|
|
self.aggregates = {}
|
|
self.aggregate_collections = {}
|
|
|
|
self.material_creator = MaterialCreator(ifc_import_settings, self)
|
|
|
|
def profile_code(self, message):
|
|
if not self.time:
|
|
self.time = time.time()
|
|
print("{} :: {:.2f}".format(message, time.time() - self.time))
|
|
self.time = time.time()
|
|
|
|
def execute(self):
|
|
self.profile_code("Starting import process")
|
|
self.load_diff()
|
|
self.profile_code("Load diff")
|
|
self.purge_diff()
|
|
self.profile_code("Purge diffs")
|
|
self.load_file()
|
|
self.profile_code("Loading file")
|
|
self.set_ifc_file()
|
|
self.profile_code("Setting file")
|
|
if self.ifc_import_settings.should_auto_set_workarounds:
|
|
self.auto_set_workarounds()
|
|
self.profile_code("Set vendor worksarounds")
|
|
self.calculate_unit_scale()
|
|
self.profile_code("Calculate unit scale")
|
|
self.calculate_model_offset()
|
|
self.profile_code("Calculate model offset")
|
|
self.set_units()
|
|
self.profile_code("Set units")
|
|
self.create_project()
|
|
self.profile_code("Create project")
|
|
self.create_spatial_hierarchy()
|
|
self.profile_code("Create spatial hierarchy")
|
|
self.create_aggregates()
|
|
self.profile_code("Create aggregates")
|
|
self.create_openings_collection()
|
|
self.profile_code("Create opening collection")
|
|
self.process_element_filter()
|
|
self.profile_code("Process element filter")
|
|
# TODO: Deprecate
|
|
# self.parse_native_elements()
|
|
# self.profile_code("Parsing native elements")
|
|
self.create_grids()
|
|
self.profile_code("Create grids")
|
|
# TODO: Deprecate
|
|
# self.create_native_products()
|
|
# self.profile_code("Create native products")
|
|
self.create_products()
|
|
self.profile_code("Create products")
|
|
self.create_type_products()
|
|
self.profile_code("Create type products")
|
|
self.create_annotation()
|
|
self.profile_code("Create annotation")
|
|
self.place_objects_in_spatial_tree()
|
|
self.profile_code("Placing objects in spatial tree")
|
|
if self.ifc_import_settings.should_merge_by_class:
|
|
self.merge_by_class()
|
|
self.profile_code("Merging by class")
|
|
elif self.ifc_import_settings.should_merge_by_material:
|
|
self.merge_by_material()
|
|
self.profile_code("Merging by material")
|
|
if self.ifc_import_settings.should_merge_materials_by_colour or (
|
|
self.ifc_import_settings.should_auto_set_workarounds and len(self.material_creator.materials) > 300
|
|
):
|
|
self.merge_materials_by_colour()
|
|
self.profile_code("Merging by colour")
|
|
self.add_project_to_scene()
|
|
self.profile_code("Add project to scene")
|
|
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
|
self.clean_mesh()
|
|
self.profile_code("Mesh cleaning")
|
|
self.set_default_context()
|
|
self.profile_code("Setting default context")
|
|
|
|
def auto_set_workarounds(self):
|
|
applications = self.file.by_type("IfcApplication")
|
|
if not applications:
|
|
return
|
|
if "prostructures" in applications[0].ApplicationFullName.lower():
|
|
self.ifc_import_settings.should_allow_non_element_aggregates = True
|
|
|
|
def is_element_far_away(self, element):
|
|
try:
|
|
return self.is_point_far_away(element.ObjectPlacement.RelativePlacement.Location)
|
|
except:
|
|
pass
|
|
|
|
def is_point_far_away(self, point):
|
|
# Arbitrary threshold based on experience
|
|
if hasattr(point, "Coordinates"):
|
|
return (
|
|
abs(point.Coordinates[0]) > 1000000
|
|
or abs(point.Coordinates[1]) > 1000000
|
|
or abs(point.Coordinates[2]) > 1000000
|
|
)
|
|
return abs(point[0]) > 1000000 or abs(point[1]) > 1000000 or abs(point[2]) > 1000000
|
|
|
|
def process_element_filter(self):
|
|
if not self.ifc_import_settings.ifc_selector:
|
|
return
|
|
self.include_elements = []
|
|
selector = ifcopenshell.util.selector.Selector()
|
|
elements = selector.parse(self.file, self.ifc_import_settings.ifc_selector)
|
|
if self.ifc_import_settings.ifc_import_filter == "WHITELIST":
|
|
self.include_elements = elements
|
|
elif self.ifc_import_settings.ifc_import_filter == "BLACKLIST":
|
|
self.exclude_elements = elements
|
|
|
|
def parse_native_elements(self):
|
|
self.parse_native_swept_disk_solid()
|
|
self.parse_native_extruded_area_solid()
|
|
self.parse_native_faceted_brep()
|
|
if self.include_elements:
|
|
include_global_ids = [e.GlobalId for e in self.include_elements]
|
|
filtered_native_elements = {}
|
|
for global_id in self.native_elements.keys():
|
|
if global_id in include_global_ids:
|
|
filtered_native_elements[global_id] = self.native_elements[global_id]
|
|
self.native_elements = filtered_native_elements
|
|
elif self.exclude_elements:
|
|
exclude_global_ids = [e.GlobalId for e in self.exclude_elements]
|
|
filtered_native_elements = {}
|
|
for global_id in self.native_elements.keys():
|
|
if global_id not in exclude_global_ids:
|
|
filtered_native_elements[global_id] = self.native_elements[global_id]
|
|
self.native_elements = filtered_native_elements
|
|
|
|
def parse_native_swept_disk_solid(self):
|
|
for element in self.file.by_type("IfcSweptDiskSolid"):
|
|
if [e for e in self.file.get_inverse(element) if e.is_a("IfcBooleanResult")]:
|
|
continue
|
|
self.swap_out_with_dummy_geometry(element)
|
|
|
|
def parse_native_extruded_area_solid(self):
|
|
for element in self.file.by_type("IfcExtrudedAreaSolid"):
|
|
if element.SweptArea.is_a() not in [
|
|
"IfcArbitraryClosedProfileDef",
|
|
"IfcRectangleProfileDef",
|
|
"IfcCircleProfileDef",
|
|
]:
|
|
continue
|
|
if [e for e in self.file.get_inverse(element) if e.is_a("IfcBooleanResult")]:
|
|
continue
|
|
self.swap_out_with_dummy_geometry(element)
|
|
|
|
def parse_native_faceted_brep(self):
|
|
for element in self.file.by_type("IfcFacetedBrep"):
|
|
if [e for e in self.file.get_inverse(element) if e.is_a("IfcBooleanResult")]:
|
|
continue
|
|
self.swap_out_with_dummy_geometry(element)
|
|
|
|
def swap_out_with_dummy_geometry(self, element):
|
|
dummy_geometry = self.get_dummy_geometry()
|
|
inverse_elements = self.file.get_inverse(element)
|
|
for inverse_element in inverse_elements:
|
|
if inverse_element.is_a("IfcShapeRepresentation"):
|
|
inverse_element.RepresentationType = "Curve"
|
|
for product in self.get_products_from_shape_representation(inverse_element):
|
|
self.native_elements.setdefault(product.GlobalId, {})[dummy_geometry.id()] = element
|
|
ifcopenshell.util.element.replace_attribute(inverse_element, element, dummy_geometry)
|
|
|
|
def get_dummy_geometry(self):
|
|
point = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
|
direction = self.file.createIfcVector(self.file.createIfcDirection((0.0, 0.0, 1.0)), 1000.0)
|
|
return self.file.createIfcLine(point, direction)
|
|
|
|
def get_products_from_shape_representation(self, element):
|
|
products = [pr.ShapeOfProduct[0] for pr in element.OfProductRepresentation]
|
|
for rep_map in element.RepresentationMap:
|
|
for usage in rep_map.MapUsage:
|
|
for inverse_element in self.file.get_inverse(usage):
|
|
if inverse_element.is_a("IfcShapeRepresentation"):
|
|
products.extend(self.get_products_from_shape_representation(inverse_element))
|
|
return products
|
|
|
|
def calculate_model_offset(self):
|
|
project = self.file.by_type("IfcProject")[0]
|
|
site = self.find_decomposed_ifc_class(project, "IfcSite")
|
|
if site and self.is_element_far_away(site[0]):
|
|
return self.guess_georeferencing(site[0])
|
|
building = self.find_decomposed_ifc_class(project, "IfcBuilding")
|
|
if building and self.is_element_far_away(building[0]):
|
|
return self.guess_georeferencing(building[0])
|
|
return self.guess_absolute_coordinate()
|
|
|
|
def guess_georeferencing(self, element):
|
|
if not element.ObjectPlacement.is_a("IfcLocalPlacement"):
|
|
return
|
|
placement = element.ObjectPlacement.RelativePlacement
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
props.blender_eastings = str(placement.Location.Coordinates[0])
|
|
props.blender_northings = str(placement.Location.Coordinates[1])
|
|
props.blender_orthogonal_height = str(placement.Location.Coordinates[2])
|
|
if placement.RefDirection:
|
|
props.blender_x_axis_abscissa = str(placement.RefDirection.DirectionRatios[0])
|
|
props.blender_x_axis_ordinate = str(placement.RefDirection.DirectionRatios[1])
|
|
props.has_blender_offset = True
|
|
props.blender_offset_type = "OBJECT_PLACEMENT"
|
|
|
|
def guess_absolute_coordinate(self):
|
|
# Civil BIM applications like to work in absolute coordinates, where the ObjectPlacement is 0,0,0 but each
|
|
# individual coordinate of the shape representation is in absolute values.
|
|
offset_point = self.get_offset_point()
|
|
if not offset_point:
|
|
return
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
props.blender_eastings = str(offset_point[0])
|
|
props.blender_northings = str(offset_point[1])
|
|
props.blender_orthogonal_height = str(offset_point[2])
|
|
props.has_blender_offset = True
|
|
props.blender_offset_type = "CARTESIAN_POINT"
|
|
|
|
def get_offset_point(self):
|
|
offset_point = None
|
|
elements_checked = 0
|
|
# If more than these points aren't far away, the file probably isn't absolutely positioned
|
|
element_checking_threshold = 100
|
|
try:
|
|
point_lists = self.file.by_type("IfcCartesianPointList3D")
|
|
except:
|
|
# IFC2X3 does not have IfcCartesianPointList3D
|
|
point_lists = []
|
|
for point_list in point_lists:
|
|
elements_checked += 1
|
|
if elements_checked > element_checking_threshold:
|
|
return
|
|
for i, point in enumerate(point_list.CoordList):
|
|
if len(point) == 3 and self.is_point_far_away(point):
|
|
return point[0]
|
|
|
|
for point in self.file.by_type("IfcCartesianPoint"):
|
|
elements_checked += 1
|
|
if elements_checked > element_checking_threshold:
|
|
return
|
|
if len(point.Coordinates) == 3 and self.is_point_far_away(point):
|
|
return point[0]
|
|
|
|
def apply_blender_offset_to_matrix(self, matrix):
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
if props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT":
|
|
return mathutils.Matrix(
|
|
ifcopenshell.util.geolocation.global2local(
|
|
matrix,
|
|
float(props.blender_eastings) * self.unit_scale,
|
|
float(props.blender_northings) * self.unit_scale,
|
|
float(props.blender_orthogonal_height) * self.unit_scale,
|
|
float(props.blender_x_axis_abscissa),
|
|
float(props.blender_x_axis_ordinate),
|
|
).tolist()
|
|
)
|
|
return mathutils.Matrix(matrix.tolist())
|
|
|
|
def find_decomposed_ifc_class(self, element, ifc_class):
|
|
results = []
|
|
rel_aggregates = element.IsDecomposedBy
|
|
if not rel_aggregates:
|
|
return results
|
|
for rel_aggregate in rel_aggregates:
|
|
for part in rel_aggregate.RelatedObjects:
|
|
if part.is_a(ifc_class):
|
|
results.append(part)
|
|
results.extend(self.find_decomposed_ifc_class(part, ifc_class))
|
|
return results
|
|
|
|
def create_grids(self):
|
|
grids = self.file.by_type("IfcGrid")
|
|
for grid in grids:
|
|
shape = None
|
|
if not grid.UAxes or not grid.VAxes:
|
|
# Revit can create invalid grids
|
|
self.ifc_import_settings.logger.error("An invalid grid was found %s", grid)
|
|
continue
|
|
if grid.Representation:
|
|
shape = ifcopenshell.geom.create_shape(self.settings_2d, grid)
|
|
grid_obj = self.create_product(grid, shape)
|
|
collection = bpy.data.collections.new(self.get_name(grid))
|
|
element_matrix = self.get_local_placement(grid.ObjectPlacement)
|
|
element_matrix[0][3] *= self.unit_scale
|
|
element_matrix[1][3] *= self.unit_scale
|
|
element_matrix[2][3] *= self.unit_scale
|
|
u_axes = bpy.data.collections.new("UAxes")
|
|
collection.children.link(u_axes)
|
|
v_axes = bpy.data.collections.new("VAxes")
|
|
collection.children.link(v_axes)
|
|
self.create_grid_axes(grid.UAxes, u_axes, element_matrix)
|
|
self.create_grid_axes(grid.VAxes, v_axes, element_matrix)
|
|
if grid.WAxes:
|
|
w_axes = bpy.data.collections.new("WAxes")
|
|
collection.children.link(w_axes)
|
|
self.create_grid_axes(grid.WAxes, w_axes, element_matrix)
|
|
|
|
def create_grid_axes(self, axes, grid, matrix_world):
|
|
for axis in axes:
|
|
shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve)
|
|
mesh = self.create_mesh(axis, shape)
|
|
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
|
|
obj.BIMObjectProperties.ifc_definition_id = axis.id()
|
|
obj.matrix_world = matrix_world
|
|
grid.objects.link(obj)
|
|
|
|
def create_type_products(self):
|
|
type_products = self.file.by_type("IfcTypeProduct")
|
|
for collection in self.project["blender"].children:
|
|
if collection.name == "Types":
|
|
self.type_collection = collection
|
|
break
|
|
if not self.type_collection:
|
|
self.type_collection = bpy.data.collections.new("Types")
|
|
self.project["blender"].children.link(self.type_collection)
|
|
for type_product in type_products:
|
|
self.create_type_product(type_product)
|
|
|
|
def create_type_product(self, element):
|
|
self.ifc_import_settings.logger.info("Creating object %s", element)
|
|
representation_map = self.get_type_product_body_representation_map(element)
|
|
mesh = None
|
|
if representation_map:
|
|
representation = representation_map.MappedRepresentation
|
|
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
|
|
mesh = self.meshes.get(mesh_name)
|
|
if mesh is None:
|
|
try:
|
|
shape = ifcopenshell.geom.create_shape(self.settings, representation_map.MappedRepresentation)
|
|
mesh = self.create_mesh(element, shape)
|
|
self.meshes[mesh_name] = mesh
|
|
except:
|
|
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
|
|
obj = bpy.data.objects.new(self.get_name(element), mesh)
|
|
self.link_element(element, obj)
|
|
self.material_creator.create(element, obj, mesh)
|
|
self.type_products[element.GlobalId] = obj
|
|
|
|
def get_type_product_body_representation_map(self, element):
|
|
if not element.RepresentationMaps:
|
|
return
|
|
for representation_map in element.RepresentationMaps:
|
|
context = representation_map.MappedRepresentation.ContextOfItems
|
|
if (
|
|
context.ContextType == "Model"
|
|
and context.ContextIdentifier == "Body"
|
|
and context.TargetView == "MODEL_VIEW"
|
|
):
|
|
return representation_map
|
|
|
|
def create_native_products(self):
|
|
if not self.native_elements:
|
|
return
|
|
# TODO: the iterator is kind of useless here, rewrite this
|
|
iterator = ifcopenshell.geom.iterator(
|
|
self.settings_native,
|
|
self.file,
|
|
multiprocessing.cpu_count(),
|
|
include=[self.file.by_guid(guid) for guid in self.native_elements.keys()] or None,
|
|
)
|
|
valid_file = iterator.initialize()
|
|
total = 0
|
|
checkpoint = time.time()
|
|
if not valid_file:
|
|
return False
|
|
while True:
|
|
total += 1
|
|
if total % 250 == 0:
|
|
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
|
checkpoint = time.time()
|
|
shape = iterator.get()
|
|
if shape:
|
|
self.create_product(self.file.by_id(shape.guid), shape)
|
|
if not iterator.next():
|
|
break
|
|
print("Done creating geometry")
|
|
|
|
def create_products(self):
|
|
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
|
iterator = ifcopenshell.geom.iterator(
|
|
self.settings,
|
|
self.file,
|
|
multiprocessing.cpu_count(),
|
|
include=self.include_elements or None,
|
|
exclude=self.exclude_elements or None,
|
|
)
|
|
else:
|
|
iterator = ifcopenshell.geom.iterator(
|
|
self.settings, self.file, include=self.include_elements or None, exclude=self.exclude_elements or None
|
|
)
|
|
valid_file = iterator.initialize()
|
|
if not valid_file:
|
|
return False
|
|
checkpoint = time.time()
|
|
total = 0
|
|
while True:
|
|
total += 1
|
|
if total % 250 == 0:
|
|
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
|
checkpoint = time.time()
|
|
shape = iterator.get()
|
|
if shape:
|
|
self.create_product(self.file.by_id(shape.guid), shape)
|
|
if not iterator.next():
|
|
break
|
|
print("Done creating geometry")
|
|
|
|
def create_annotation(self):
|
|
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
|
iterator = ifcopenshell.geom.iterator(
|
|
self.settings_2d,
|
|
self.file,
|
|
multiprocessing.cpu_count(),
|
|
include=self.file.by_type("IfcAnnotation")
|
|
)
|
|
else:
|
|
iterator = ifcopenshell.geom.iterator(
|
|
self.settings_2d, self.file, include=self.file.by_type("IfcAnnotation")
|
|
)
|
|
valid_file = iterator.initialize()
|
|
if not valid_file:
|
|
return False
|
|
checkpoint = time.time()
|
|
total = 0
|
|
while True:
|
|
total += 1
|
|
if total % 250 == 0:
|
|
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
|
checkpoint = time.time()
|
|
shape = iterator.get()
|
|
if shape:
|
|
self.create_product(self.file.by_id(shape.guid), shape)
|
|
if not iterator.next():
|
|
break
|
|
print("Done creating geometry")
|
|
|
|
|
|
def create_product(self, element, shape=None):
|
|
if element is None:
|
|
return
|
|
|
|
if not self.ifc_import_settings.should_import_spaces and element.is_a("IfcSpace"):
|
|
return
|
|
|
|
self.ifc_import_settings.logger.info("Creating object %s", element)
|
|
|
|
if shape:
|
|
mesh_name = self.get_mesh_name(shape.geometry)
|
|
mesh = self.meshes.get(mesh_name)
|
|
if mesh is None:
|
|
if element.GlobalId in self.native_elements:
|
|
mesh = self.create_native_mesh(element, shape)
|
|
if mesh is None:
|
|
mesh = self.create_mesh(element, shape)
|
|
if "-" in shape.geometry.id:
|
|
mesh.BIMMeshProperties.ifc_definition_id = int(shape.geometry.id.split("-")[0])
|
|
else:
|
|
mesh.BIMMeshProperties.ifc_definition_id = int(shape.geometry.id)
|
|
self.meshes[mesh_name] = mesh
|
|
else:
|
|
mesh = None
|
|
|
|
obj = bpy.data.objects.new(self.get_name(element), mesh)
|
|
self.link_element(element, obj)
|
|
|
|
if shape:
|
|
m = shape.transformation.matrix.data
|
|
# We use numpy here because Blender mathutils.Matrix is not accurate enough
|
|
mat = np.matrix(
|
|
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
|
|
)
|
|
obj.matrix_world = self.apply_blender_offset_to_matrix(mat)
|
|
self.material_creator.create(element, obj, mesh)
|
|
elif hasattr(element, "ObjectPlacement"):
|
|
obj.matrix_world = self.apply_blender_offset_to_matrix(self.get_element_matrix(element))
|
|
|
|
self.add_opening_relation(element, obj)
|
|
|
|
if element.is_a("IfcOpeningElement"):
|
|
obj.display_type = "WIRE"
|
|
return obj
|
|
|
|
def create_native_mesh(self, element, shape):
|
|
# TODO This should be split off into its own module for run-time native mesh conversion
|
|
data = self.native_elements[element.GlobalId]
|
|
materials = []
|
|
items = []
|
|
for representation in self.get_body_representations(element.Representation.Representations):
|
|
for item in representation["raw"].Items:
|
|
material_name = self.get_representation_item_material_name(item)
|
|
if not material_name:
|
|
# Magic string NULLMAT represents no material, unless this has a better approach
|
|
material_name = "NULLMAT"
|
|
materials.append(material_name)
|
|
if item.id() in data:
|
|
item = data[item.id()]
|
|
if item.is_a() == "IfcExtrudedAreaSolid":
|
|
native = self.create_native_extruded_area_solid(item, element)
|
|
if native:
|
|
bmesh.ops.transform(
|
|
native["blender"], matrix=representation["matrix"], verts=native["blender"].verts
|
|
)
|
|
items.append(native)
|
|
else:
|
|
items.append(None)
|
|
elif item.is_a("IfcSweptDiskSolid"):
|
|
items.append(
|
|
{
|
|
"blender": self.transform_curve(
|
|
self.create_native_swept_disk_solid(item, element), representation["matrix"]
|
|
),
|
|
"raw": item,
|
|
"subitems": [],
|
|
}
|
|
)
|
|
elif item.is_a("IfcFacetedBrep"):
|
|
bm = self.create_native_faceted_brep(item, element)
|
|
if bm:
|
|
bmesh.ops.transform(bm, matrix=representation["matrix"], verts=bm.verts)
|
|
items.append({"blender": bm, "raw": item, "subitems": []})
|
|
else:
|
|
items.append(None)
|
|
else:
|
|
items.append(None)
|
|
|
|
if not items:
|
|
return None
|
|
|
|
bevel_depth = None
|
|
merged_curve = None
|
|
merged_bm = bmesh.new()
|
|
material_ids = []
|
|
representation_items = []
|
|
for i, item in enumerate(items):
|
|
if not item:
|
|
continue
|
|
if isinstance(item["blender"], bpy.types.Curve):
|
|
if bevel_depth is None:
|
|
bevel_depth = item["blender"].bevel_depth
|
|
merged_curve = item["blender"]
|
|
elif item["blender"].bevel_depth == bevel_depth:
|
|
self.merge_curves(merged_curve, item["blender"])
|
|
else:
|
|
# TODO: handle if there are multiple different radiuses
|
|
# We don't have a choice but to meshify it
|
|
pass
|
|
elif isinstance(item["blender"], bmesh.types.BMesh):
|
|
representation_items.append(
|
|
{
|
|
"name": item["raw"].is_a(),
|
|
"total_vertices": len(item["blender"].verts),
|
|
"subitems": item["subitems"],
|
|
}
|
|
)
|
|
total_polygons = len(item["blender"].faces)
|
|
if merged_bm is None:
|
|
merged_bm = item["blender"]
|
|
else:
|
|
self.merge_bmeshes(merged_bm, item["blender"])
|
|
# Magic string NULLMAT represents no material, unless this has a better approach
|
|
if materials[i] == "NULLMAT":
|
|
# Magic number -1 represents no material, until this has a better approach
|
|
material_ids += [-1] * total_polygons
|
|
else:
|
|
material_ids += [i] * total_polygons
|
|
if merged_curve:
|
|
return merged_curve
|
|
# TODO: handle both curve and bmeshes combined
|
|
mesh = bpy.data.meshes.new("Native Mesh")
|
|
merged_bm.to_mesh(mesh)
|
|
merged_bm.free()
|
|
mesh["ios_materials"] = materials
|
|
mesh["ios_material_ids"] = material_ids
|
|
mesh["ios_items"] = representation_items
|
|
mesh.BIMMeshProperties.is_native = True
|
|
return mesh
|
|
|
|
def get_representation_item_material_name(self, item):
|
|
if not item.StyledByItem:
|
|
return
|
|
styled_item = item.StyledByItem[0]
|
|
return self.material_creator.get_surface_style_name(styled_item)
|
|
|
|
def transform_curve(self, curve, matrix):
|
|
for spline in curve.splines:
|
|
for point in spline.points:
|
|
point.co = matrix @ point.co
|
|
return curve
|
|
|
|
def merge_curves(self, a, b):
|
|
for spline in b.splines:
|
|
new_spline = a.splines.new("POLY")
|
|
is_first = True
|
|
for point in spline.points:
|
|
if is_first:
|
|
is_first = False
|
|
else:
|
|
new_spline.points.add(1)
|
|
new_spline.points[-1].co = point.co
|
|
return a
|
|
|
|
def merge_bmeshes(self, a, b):
|
|
mesh = bpy.data.meshes.new("x")
|
|
b.to_mesh(mesh)
|
|
b.free()
|
|
a.from_mesh(mesh)
|
|
return a
|
|
|
|
def create_native_faceted_brep(self, item, element):
|
|
vertex_map = {}
|
|
vertices = []
|
|
faces = []
|
|
vertex_index = 0
|
|
for face in item.Outer.CfsFaces:
|
|
if len(face.Bounds) > 1:
|
|
# TODO: implement tesselate_polygon
|
|
return None
|
|
for point in face.Bounds[0].Bound.Polygon:
|
|
if point.id() not in vertex_map:
|
|
vertices.append([c * self.unit_scale for c in point.Coordinates])
|
|
vertex_map[point.id()] = vertex_index
|
|
vertex_index += 1
|
|
faces.append([vertex_map[p.id()] for p in face.Bounds[0].Bound.Polygon])
|
|
return self.bmesh_from_pydata(vertices, [], faces)
|
|
|
|
def create_native_swept_disk_solid(self, item, element):
|
|
# TODO: support inner radius, start param, and end param
|
|
shape = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
|
|
mesh = self.create_mesh(element, shape, is_curve=True)
|
|
mesh.bevel_depth = self.unit_scale * item.Radius
|
|
return mesh
|
|
|
|
def create_native_extruded_area_solid(self, item, element):
|
|
# print(shape.materials)
|
|
subitems = []
|
|
if item.SweptArea.is_a() == "IfcArbitraryClosedProfileDef":
|
|
shape = ifcopenshell.geom.create_shape(self.settings_native, item.SweptArea.OuterCurve)
|
|
bm = self.bmesh_from_pydata(*self.shape_to_mesh(shape))
|
|
bm.faces.new([v for v in bm.verts])
|
|
bm.faces.ensure_lookup_table()
|
|
subitems.append({"name": item.SweptArea.is_a(), "vertices": range(0, len(bm.verts))})
|
|
elif item.SweptArea.is_a() == "IfcRectangleProfileDef":
|
|
bm = self.bmesh_from_rectangle(item.SweptArea.XDim, item.SweptArea.YDim)
|
|
if item.SweptArea.Position:
|
|
bmesh.ops.transform(bm, matrix=self.get_axis2placement(item.SweptArea.Position), verts=bm.verts)
|
|
bmesh.ops.transform(bm, matrix=mathutils.Matrix() * self.unit_scale, verts=bm.verts)
|
|
subitems.append({"name": item.SweptArea.is_a(), "vertices": [0, 1, 2, 3]})
|
|
elif item.SweptArea.is_a() == "IfcCircleProfileDef":
|
|
bm = self.bmesh_from_circle(item.SweptArea.Radius)
|
|
if item.SweptArea.Position:
|
|
bmesh.ops.transform(bm, matrix=self.get_axis2placement(item.SweptArea.Position), verts=bm.verts)
|
|
bmesh.ops.transform(bm, matrix=mathutils.Matrix() * self.unit_scale, verts=bm.verts)
|
|
subitems.append(
|
|
{
|
|
"name": item.SweptArea.is_a(),
|
|
# This strange vertice offset is due to a Blender quirk
|
|
"vertices": range(1, len(bm.verts) + 1),
|
|
}
|
|
)
|
|
else:
|
|
# TODO: what if we can't handle it?
|
|
return
|
|
results = bmesh.ops.extrude_face_region(bm, geom=[bm.faces[0]])
|
|
bm.faces.ensure_lookup_table()
|
|
offset = self.unit_scale * item.Depth * mathutils.Vector(item.ExtrudedDirection.DirectionRatios)
|
|
if item.SweptArea.is_a() == "IfcCircleProfileDef":
|
|
# Circle profiles have a quirk apparently in Blender
|
|
subitems.append({"name": "ExtrudedDirection", "vertices": [0, 1]})
|
|
else:
|
|
subitems.append({"name": "ExtrudedDirection", "vertices": [0, len(subitems[-1]["vertices"])]})
|
|
for geom in results["geom"]:
|
|
if isinstance(geom, bmesh.types.BMVert):
|
|
geom.co += offset
|
|
if item.Position:
|
|
bmesh.ops.transform(bm, matrix=self.scale_matrix(self.get_axis2placement(item.Position)), verts=bm.verts)
|
|
return {"blender": bm, "raw": item, "subitems": subitems}
|
|
# mesh['ios_material_ids'] = [0] * len(bm.faces)
|
|
|
|
def bmesh_from_rectangle(self, x, y):
|
|
bm = bmesh.new()
|
|
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=x / 2)
|
|
bm.verts.ensure_lookup_table()
|
|
diff_vector = mathutils.Vector((0.0, (x - y) / 2.0, 0.0))
|
|
bm.verts[0].co += diff_vector
|
|
bm.verts[1].co += diff_vector
|
|
bm.verts[2].co -= diff_vector
|
|
bm.verts[3].co -= diff_vector
|
|
bm.edges.ensure_lookup_table()
|
|
bm.faces.ensure_lookup_table()
|
|
return bm
|
|
|
|
def bmesh_from_circle(self, r):
|
|
bm = bmesh.new()
|
|
# Segments should be a multiple of 4 to easily measure the diameter
|
|
si_radius = r * self.unit_scale
|
|
# I'm arbitrarily deciding that 28 verts is enough for a 1m radius
|
|
closest_power_of_2 = int(math.log(si_radius, 2) + 0.5)
|
|
segments = (closest_power_of_2 * 4) + 28
|
|
bmesh.ops.create_circle(bm, cap_ends=True, segments=segments, radius=r)
|
|
bm.verts.ensure_lookup_table()
|
|
bm.edges.ensure_lookup_table()
|
|
bm.faces.ensure_lookup_table()
|
|
return bm
|
|
|
|
def merge_by_class(self):
|
|
merge_set = {}
|
|
for obj in self.added_data.values():
|
|
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
|
|
continue
|
|
merge_set.setdefault(obj.name.split("/")[0], []).append(obj)
|
|
self.merge_objects(merge_set)
|
|
|
|
def merge_by_material(self):
|
|
merge_set = {}
|
|
for obj in self.added_data.values():
|
|
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
|
|
continue
|
|
if not obj.material_slots:
|
|
merge_set.setdefault("no-material", []).append(obj)
|
|
else:
|
|
merge_set.setdefault(obj.material_slots[0].name, []).append(obj)
|
|
self.merge_objects(merge_set)
|
|
|
|
def merge_objects(self, merge_set):
|
|
for ifc_class, objs in merge_set.items():
|
|
context_override = {}
|
|
context_override["object"] = context_override["active_object"] = objs[0]
|
|
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
|
|
bpy.ops.object.join(context_override)
|
|
|
|
def merge_materials_by_colour(self):
|
|
cleaned_materials = {}
|
|
for m in bpy.data.materials:
|
|
key = "-".join([str(x) for x in m.diffuse_color])
|
|
cleaned_materials[key] = {"diffuse_color": m.diffuse_color}
|
|
|
|
for cleaned_material in cleaned_materials.values():
|
|
cleaned_material["material"] = bpy.data.materials.new("Merged Material")
|
|
cleaned_material["material"].diffuse_color = cleaned_material["diffuse_color"]
|
|
|
|
for obj in self.added_data.values():
|
|
if not hasattr(obj, "material_slots") or not obj.material_slots:
|
|
continue
|
|
for slot in obj.material_slots:
|
|
m = slot.material
|
|
key = "-".join([str(x) for x in m.diffuse_color])
|
|
slot.material = cleaned_materials[key]["material"]
|
|
|
|
for material in self.material_creator.materials.values():
|
|
bpy.data.materials.remove(material)
|
|
|
|
def add_project_to_scene(self):
|
|
try:
|
|
bpy.context.scene.collection.children.link(self.project["blender"])
|
|
except:
|
|
# Occurs when reloading a project
|
|
pass
|
|
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
|
|
project_collection.children[self.opening_collection.name].hide_viewport = True
|
|
project_collection.children[self.type_collection.name].hide_viewport = True
|
|
|
|
def clean_mesh(self):
|
|
obj = None
|
|
last_obj = None
|
|
for obj in self.added_data.values():
|
|
if obj.type == "MESH":
|
|
obj.select_set(True)
|
|
last_obj = obj
|
|
if not last_obj:
|
|
return
|
|
bpy.context.view_layer.objects.active = last_obj
|
|
context_override = {}
|
|
bpy.ops.object.editmode_toggle(context_override)
|
|
bpy.ops.mesh.remove_doubles(context_override)
|
|
bpy.ops.mesh.tris_convert_to_quads(context_override)
|
|
bpy.ops.mesh.normals_make_consistent(context_override)
|
|
bpy.ops.object.editmode_toggle(context_override)
|
|
|
|
def add_opening_relation(self, element, obj):
|
|
if not element.is_a("IfcOpeningElement"):
|
|
return
|
|
self.openings[element.GlobalId] = obj
|
|
|
|
def load_diff(self):
|
|
if not self.ifc_import_settings.diff_file:
|
|
return
|
|
with open(self.ifc_import_settings.diff_file, "r") as file:
|
|
self.diff = json.load(file)
|
|
|
|
def load_file(self):
|
|
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
|
|
extension = self.ifc_import_settings.input_file.split(".")[-1]
|
|
if extension.lower() == "ifczip":
|
|
with tempfile.TemporaryDirectory() as unzipped_path:
|
|
with zipfile.ZipFile(self.ifc_import_settings.input_file, "r") as zip_ref:
|
|
zip_ref.extractall(unzipped_path)
|
|
for filename in Path(unzipped_path).glob("**/*.ifc"):
|
|
self.file = ifcopenshell.open(filename)
|
|
break
|
|
elif extension.lower() == "ifcxml":
|
|
self.file = ifcopenshell.file(
|
|
ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(self.ifc_import_settings.input_file)
|
|
)
|
|
elif extension.lower() == "ifc":
|
|
self.file = ifcopenshell.open(self.ifc_import_settings.input_file)
|
|
IfcStore.file = self.file
|
|
|
|
def set_ifc_file(self):
|
|
bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
|
|
IfcStore.file = self.file
|
|
IfcStore.path = self.ifc_import_settings.input_file
|
|
|
|
def calculate_unit_scale(self):
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
|
|
|
def set_units(self):
|
|
units = self.file.by_type("IfcUnitAssignment")[0]
|
|
for unit in units.Units:
|
|
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
|
|
if unit.is_a("IfcSIUnit"):
|
|
bpy.context.scene.unit_settings.system = "METRIC"
|
|
if unit.Name == "METRE":
|
|
if not unit.Prefix:
|
|
bpy.context.scene.unit_settings.length_unit = "METERS"
|
|
else:
|
|
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
|
|
else:
|
|
bpy.context.scene.unit_settings.system = "IMPERIAL"
|
|
name = unit.Name.lower()
|
|
if name == "inch":
|
|
bpy.context.scene.unit_settings.length_unit = "INCHES"
|
|
elif name == "foot":
|
|
bpy.context.scene.unit_settings.length_unit = "FEET"
|
|
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
|
|
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
|
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
|
|
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
|
)
|
|
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
|
|
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
|
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
|
|
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
|
)
|
|
|
|
def create_project(self):
|
|
self.project = {"ifc": self.file.by_type("IfcProject")[0]}
|
|
self.project["blender"] = bpy.data.collections.new("IfcProject/{}".format(self.project["ifc"].Name))
|
|
obj = self.create_product(self.project["ifc"])
|
|
if obj:
|
|
self.project["blender"].objects.link(obj)
|
|
|
|
def create_spatial_hierarchy(self):
|
|
if self.project["ifc"].IsDecomposedBy:
|
|
for rel_aggregate in self.project["ifc"].IsDecomposedBy:
|
|
self.add_related_objects(self.project["blender"], rel_aggregate.RelatedObjects)
|
|
|
|
def add_related_objects(self, parent, related_objects):
|
|
for element in related_objects:
|
|
if element.is_a("IfcSpace"):
|
|
continue
|
|
global_id = element.GlobalId
|
|
collection = bpy.data.collections.new(self.get_name(element))
|
|
self.spatial_structure_elements[global_id] = {"blender": collection}
|
|
parent.children.link(collection)
|
|
obj = self.create_product(element)
|
|
if obj:
|
|
self.spatial_structure_elements[global_id]["blender_obj"] = obj
|
|
collection.objects.link(obj)
|
|
if element.IsDecomposedBy:
|
|
for rel_aggregate in element.IsDecomposedBy:
|
|
self.add_related_objects(collection, rel_aggregate.RelatedObjects)
|
|
|
|
def create_aggregates(self):
|
|
if self.ifc_import_settings.should_allow_non_element_aggregates:
|
|
if self.file.schema == "IFC2X3":
|
|
rel_aggregates = [
|
|
a
|
|
for a in self.file.by_type("IfcRelAggregates")
|
|
if not a.RelatingObject.is_a("IfcSpatialStructureElement")
|
|
]
|
|
else:
|
|
rel_aggregates = [
|
|
a for a in self.file.by_type("IfcRelAggregates") if not a.RelatingObject.is_a("IfcSpatialElement")
|
|
]
|
|
else:
|
|
rel_aggregates = [a for a in self.file.by_type("IfcRelAggregates") if a.RelatingObject.is_a("IfcElement")]
|
|
for rel_aggregate in rel_aggregates:
|
|
self.create_aggregate(rel_aggregate)
|
|
|
|
def create_aggregate(self, rel_aggregate):
|
|
element = rel_aggregate.RelatingObject
|
|
|
|
obj = bpy.data.objects.new("{}/{}".format(element.is_a(), element.Name), None)
|
|
self.link_element(element, obj)
|
|
self.place_object_in_spatial_tree(element, obj)
|
|
|
|
collection = bpy.data.collections.new(obj.name)
|
|
obj.users_collection[0].children.link(collection)
|
|
obj.users_collection[0].objects.unlink(obj)
|
|
collection.objects.link(obj)
|
|
|
|
self.aggregates[element.GlobalId] = obj
|
|
self.aggregate_collections[rel_aggregate.id()] = collection
|
|
|
|
def create_openings_collection(self):
|
|
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
|
|
self.project["blender"].children.link(self.opening_collection)
|
|
|
|
def get_name(self, element):
|
|
return "{}/{}".format(element.is_a(), element.Name)
|
|
|
|
def purge_diff(self):
|
|
if not self.diff:
|
|
return
|
|
objects_to_purge = []
|
|
for obj in bpy.data.objects:
|
|
if "GlobalId" not in obj.BIMObjectProperties.attributes:
|
|
continue
|
|
global_id = obj.BIMObjectProperties.attributes["GlobalId"].string_value
|
|
if global_id in self.diff["deleted"] or global_id in self.diff["changed"].keys():
|
|
objects_to_purge.append(obj)
|
|
bpy.ops.object.delete({"selected_objects": objects_to_purge})
|
|
|
|
def place_objects_in_spatial_tree(self):
|
|
for global_id, obj in self.added_data.items():
|
|
self.place_object_in_spatial_tree(self.file.by_guid(global_id), obj)
|
|
|
|
def place_object_in_spatial_tree(self, element, obj):
|
|
if element.is_a("IfcProject"):
|
|
return
|
|
elif element.is_a("IfcTypeObject"):
|
|
self.type_collection.objects.link(obj)
|
|
elif element.GlobalId in self.spatial_structure_elements:
|
|
if not obj.data:
|
|
return
|
|
# Since spatial structure elements are generated as empties, we'll replace it with the representation
|
|
spatial_obj = self.spatial_structure_elements[element.GlobalId]["blender_obj"]
|
|
spatial_collection = self.spatial_structure_elements[element.GlobalId]["blender"]
|
|
spatial_name = spatial_obj.name
|
|
spatial_collection.objects.link(obj)
|
|
bpy.data.objects.remove(spatial_obj)
|
|
obj.name = spatial_name
|
|
elif (
|
|
hasattr(element, "ContainedInStructure")
|
|
and element.ContainedInStructure
|
|
and element.ContainedInStructure[0].RelatingStructure
|
|
):
|
|
container = element.ContainedInStructure[0].RelatingStructure
|
|
if container.is_a("IfcSpace"):
|
|
return self.place_object_in_spatial_tree(container, obj)
|
|
elif element.is_a("IfcGrid"):
|
|
grid_collection = bpy.data.collections.get(obj.name)
|
|
if grid_collection: # Just in case we ran into invalid grids from Revit
|
|
self.spatial_structure_elements[container.GlobalId]["blender"].children.link(grid_collection)
|
|
grid_collection.objects.link(obj)
|
|
else:
|
|
self.spatial_structure_elements[container.GlobalId]["blender"].objects.link(obj)
|
|
elif hasattr(element, "Decomposes") and element.Decomposes:
|
|
collection = None
|
|
if element.Decomposes[0].RelatingObject.is_a("IfcProject"):
|
|
collection = self.project["blender"]
|
|
elif element.Decomposes[0].RelatingObject.is_a("IfcSpatialStructureElement"):
|
|
if element.is_a("IfcSpatialStructureElement") and not element.is_a("IfcSpace"):
|
|
global_id = element.GlobalId
|
|
else:
|
|
global_id = element.Decomposes[0].RelatingObject.GlobalId
|
|
if global_id in self.spatial_structure_elements:
|
|
if (
|
|
element.is_a("IfcSpatialStructureElement")
|
|
and not element.is_a("IfcSpace")
|
|
and "blender_obj" in self.spatial_structure_elements[global_id]
|
|
):
|
|
bpy.data.objects.remove(self.spatial_structure_elements[global_id]["blender_obj"])
|
|
collection = self.spatial_structure_elements[global_id]["blender"]
|
|
# This may occur if we are nesting an IfcSpace (which is special
|
|
# since it does not have a collection within an IfcSpace
|
|
if not collection:
|
|
return self.place_object_in_spatial_tree(element.Decomposes[0].RelatingObject, obj)
|
|
else:
|
|
collection = self.aggregate_collections[element.Decomposes[0].id()]
|
|
if collection:
|
|
collection.objects.link(obj)
|
|
else:
|
|
self.ifc_import_settings.logger.error("An element could not be placed in the spatial tree %s", element)
|
|
elif element.is_a("IfcOpeningElement"):
|
|
self.opening_collection.objects.link(obj)
|
|
else:
|
|
self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element)
|
|
bpy.context.scene.collection.objects.link(obj)
|
|
|
|
def cast_edge_case_attribute(self, ifc_class, key, value):
|
|
if key == "RefLatitude" or key == "RefLongitude":
|
|
return ifcopenshell.util.geolocation.dms2dd(*value)
|
|
return value
|
|
|
|
def get_element_matrix(self, element, mesh_name=None):
|
|
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
|
result[0][3] *= self.unit_scale
|
|
result[1][3] *= self.unit_scale
|
|
result[2][3] *= self.unit_scale
|
|
return result
|
|
|
|
def get_body_representations(self, representations, matrix=None):
|
|
if matrix is None:
|
|
matrix = mathutils.Matrix()
|
|
results = []
|
|
for representation in representations:
|
|
if (
|
|
representation.RepresentationIdentifier == "Body"
|
|
and representation.RepresentationType == "MappedRepresentation"
|
|
):
|
|
for item in representation.Items:
|
|
# TODO: Confirm if this transformation is right
|
|
transform = self.get_axis2placement(item.MappingSource.MappingOrigin)
|
|
if item.MappingTarget:
|
|
transform = transform @ self.get_cartesiantransformationoperator(item.MappingTarget)
|
|
results.extend(
|
|
self.get_body_representations([item.MappingSource.MappedRepresentation], transform @ matrix)
|
|
)
|
|
elif representation.RepresentationIdentifier == "Body":
|
|
results.append({"raw": representation, "matrix": self.scale_matrix(matrix)})
|
|
return results
|
|
|
|
def scale_matrix(self, matrix):
|
|
matrix[0][3] *= self.unit_scale
|
|
matrix[1][3] *= self.unit_scale
|
|
matrix[2][3] *= self.unit_scale
|
|
return matrix
|
|
|
|
def get_representation_id(self, element):
|
|
if not element.Representation:
|
|
return None
|
|
for representation in element.Representation.Representations:
|
|
if not representation.is_a("IfcShapeRepresentation"):
|
|
continue
|
|
if (
|
|
representation.RepresentationIdentifier == "Body"
|
|
and representation.RepresentationType != "MappedRepresentation"
|
|
):
|
|
return representation.id()
|
|
elif representation.RepresentationIdentifier == "Body":
|
|
return representation.Items[0].MappingSource.MappedRepresentation.id()
|
|
|
|
def get_representation_cartesian_transformation(self, element):
|
|
if not element.Representation:
|
|
return None
|
|
for representation in element.Representation.Representations:
|
|
if not representation.is_a("IfcShapeRepresentation"):
|
|
continue
|
|
if (
|
|
representation.RepresentationIdentifier == "Body"
|
|
and representation.RepresentationType == "MappedRepresentation"
|
|
):
|
|
return representation.Items[0].MappingTarget
|
|
|
|
def get_mesh_name(self, geometry):
|
|
representation_id = geometry.id
|
|
if "-" in representation_id:
|
|
representation_id = int(re.sub(r"\D", "", representation_id.split("-")[0]))
|
|
else:
|
|
representation_id = int(re.sub(r"\D", "", representation_id))
|
|
representation = self.file.by_id(representation_id)
|
|
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
|
|
return "{}/{}".format(context_id, representation_id)
|
|
|
|
def create_mesh(self, element, shape, is_curve=False):
|
|
try:
|
|
if hasattr(shape, "geometry"):
|
|
geometry = shape.geometry
|
|
else:
|
|
geometry = shape
|
|
|
|
if is_curve:
|
|
return self.create_curve(geometry)
|
|
|
|
mesh = bpy.data.meshes.new(self.get_mesh_name(geometry))
|
|
|
|
props = bpy.context.scene.BIMGeoreferenceProperties
|
|
if props.has_blender_offset and props.blender_offset_type == "CARTESIAN_POINT":
|
|
ordinate_index = 0
|
|
verts = [None] * len(geometry.verts)
|
|
offset_point = (
|
|
float(props.blender_eastings) * self.unit_scale,
|
|
float(props.blender_northings) * self.unit_scale,
|
|
float(props.blender_orthogonal_height) * self.unit_scale,
|
|
)
|
|
for i, vert in enumerate(geometry.verts):
|
|
if ordinate_index > 2:
|
|
ordinate_index = 0
|
|
verts[i] = vert - offset_point[ordinate_index]
|
|
ordinate_index += 1
|
|
else:
|
|
verts = geometry.verts
|
|
|
|
if geometry.faces:
|
|
num_vertices = len(verts) // 3
|
|
total_faces = len(geometry.faces)
|
|
loop_start = range(0, total_faces, 3)
|
|
num_loops = total_faces // 3
|
|
loop_total = [3] * num_loops
|
|
num_vertex_indices = len(geometry.faces)
|
|
|
|
mesh.vertices.add(num_vertices)
|
|
if self.ifc_import_settings.should_offset_model:
|
|
# Potentially, there is a smarter way to do this. See #1047
|
|
v_index = cycle((0, 1, 2))
|
|
verts = [v + self.ifc_import_settings.model_offset_coordinates[next(v_index)] for v in verts]
|
|
mesh.vertices.foreach_set("co", verts)
|
|
else:
|
|
mesh.vertices.foreach_set("co", verts)
|
|
mesh.loops.add(num_vertex_indices)
|
|
mesh.loops.foreach_set("vertex_index", geometry.faces)
|
|
mesh.polygons.add(num_loops)
|
|
mesh.polygons.foreach_set("loop_start", loop_start)
|
|
mesh.polygons.foreach_set("loop_total", loop_total)
|
|
mesh.update()
|
|
else:
|
|
e = geometry.edges
|
|
v = verts
|
|
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
|
|
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
|
mesh.from_pydata(vertices, edges, [])
|
|
|
|
ios_materials = []
|
|
for mat in geometry.materials:
|
|
if mat.original_name():
|
|
ios_materials.append(mat.original_name())
|
|
else:
|
|
ios_materials.append(mat.name)
|
|
mesh["ios_materials"] = ios_materials
|
|
mesh["ios_material_ids"] = geometry.material_ids
|
|
return mesh
|
|
except:
|
|
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
|
|
import traceback
|
|
|
|
print(traceback.format_exc())
|
|
|
|
def create_curve(self, geometry):
|
|
curve = bpy.data.curves.new(geometry.id, type="CURVE")
|
|
curve.dimensions = "3D"
|
|
curve.resolution_u = 2
|
|
polyline = curve.splines.new("POLY")
|
|
e = geometry.edges
|
|
v = geometry.verts
|
|
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
|
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
|
v2 = None
|
|
for edge in edges:
|
|
v1 = vertices[edge[0]]
|
|
if v1 != v2:
|
|
polyline = curve.splines.new("POLY")
|
|
polyline.points[-1].co = v1
|
|
v2 = vertices[edge[1]]
|
|
polyline.points.add(1)
|
|
polyline.points[-1].co = v2
|
|
return curve
|
|
|
|
def shape_to_mesh(self, shape):
|
|
if hasattr(shape, "geometry"):
|
|
geometry = shape.geometry
|
|
else:
|
|
geometry = shape
|
|
f = geometry.faces
|
|
e = geometry.edges
|
|
v = geometry.verts
|
|
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
|
|
faces = [[f[i], f[i + 1], f[i + 2]] for i in range(0, len(f), 3)]
|
|
if faces:
|
|
edges = []
|
|
else:
|
|
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
|
return (vertices, edges, faces)
|
|
|
|
def bmesh_from_pydata(self, verts=[], edges=[], faces=[]):
|
|
bm = bmesh.new()
|
|
[bm.verts.new(co) for co in verts]
|
|
bm.verts.index_update()
|
|
bm.verts.ensure_lookup_table()
|
|
if faces:
|
|
for face in faces:
|
|
bm.faces.new(tuple(bm.verts[i] for i in face))
|
|
bm.faces.index_update()
|
|
bm.faces.ensure_lookup_table()
|
|
if edges:
|
|
for edge in edges:
|
|
edge_seq = tuple(bm.verts[i] for i in edge)
|
|
try:
|
|
bm.edges.new(edge_seq)
|
|
except ValueError:
|
|
# edge exists!
|
|
pass
|
|
bm.edges.index_update()
|
|
bm.edges.ensure_lookup_table()
|
|
return bm
|
|
|
|
def a2p(self, o, z, x):
|
|
y = z.cross(x)
|
|
r = mathutils.Matrix((x, y, z, o))
|
|
r.resize_4x4()
|
|
r.transpose()
|
|
return r
|
|
|
|
def get_axis2placement(self, plc):
|
|
if plc.is_a("IfcAxis2Placement3D"):
|
|
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
|
|
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
|
|
o = plc.Location.Coordinates
|
|
else:
|
|
z = mathutils.Vector((0, 0, 1))
|
|
if plc.RefDirection:
|
|
x = mathutils.Vector(list(plc.RefDirection.DirectionRatios) + [0])
|
|
else:
|
|
x = mathutils.Vector((1, 0, 0))
|
|
o = list(plc.Location.Coordinates) + [0]
|
|
return self.a2p(o, z, x)
|
|
|
|
def get_cartesiantransformationoperator(self, plc):
|
|
x = mathutils.Vector(plc.Axis1.DirectionRatios if plc.Axis1 else (1, 0, 0))
|
|
z = x.cross(mathutils.Vector(plc.Axis2.DirectionRatios if plc.Axis2 else (0, 1, 0)))
|
|
o = plc.LocalOrigin.Coordinates
|
|
return self.a2p(o, z, x)
|
|
|
|
def get_local_placement(self, plc):
|
|
if plc is None:
|
|
return mathutils.Matrix()
|
|
if plc.PlacementRelTo is None:
|
|
parent = mathutils.Matrix()
|
|
else:
|
|
parent = self.get_local_placement(plc.PlacementRelTo)
|
|
return parent @ self.get_axis2placement(plc.RelativePlacement)
|
|
|
|
def set_default_context(self):
|
|
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
|
|
if subcontext.ContextIdentifier == "Body":
|
|
bpy.context.scene.BIMProperties.contexts = str(subcontext.id())
|
|
break
|
|
|
|
def link_element(self, element, obj):
|
|
self.added_data[element.GlobalId] = obj
|
|
IfcStore.link_element(element, obj)
|
|
|
|
|
|
class IfcImportSettings:
|
|
def __init__(self):
|
|
self.logger = None
|
|
self.input_file = None
|
|
self.diff_file = None
|
|
self.should_import_spaces = False
|
|
self.should_auto_set_workarounds = True
|
|
self.should_use_cpu_multiprocessing = True
|
|
self.should_merge_by_class = False
|
|
self.should_merge_by_material = False
|
|
self.should_merge_materials_by_colour = False
|
|
self.should_clean_mesh = True
|
|
self.deflection_tolerance = 0.001
|
|
self.angular_tolerance = 0.5
|
|
self.should_allow_non_element_aggregates = False
|
|
self.should_offset_model = False
|
|
self.model_offset_coordinates = (0, 0, 0)
|
|
self.ifc_import_filter = "NONE"
|
|
self.ifc_selector = ""
|
|
|
|
@staticmethod
|
|
def factory(context, input_file, logger):
|
|
scene_bim = context.scene.BIMProperties
|
|
scene_diff = context.scene.DiffProperties
|
|
settings = IfcImportSettings()
|
|
settings.input_file = input_file
|
|
settings.logger = logger
|
|
settings.diff_file = scene_diff.diff_json_file
|
|
return settings
|