Files
IfcOpenShell/src/ifcblenderexport/blenderbim/bim/import_ifc.py
T

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