diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..dd9fc935367 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [aothms] diff --git a/src/ifcblender/io_import_scene_ifc/__init__.py b/src/ifcblender/io_import_scene_ifc/__init__.py index 2375d7bc6bb..8336dab8506 100644 --- a/src/ifcblender/io_import_scene_ifc/__init__.py +++ b/src/ifcblender/io_import_scene_ifc/__init__.py @@ -41,10 +41,17 @@ if "ifcopenshell" in locals(): importlib.reload(ifcopenshell) +from bpy.props import ( + BoolProperty, + IntProperty, + StringProperty, +) +from bpy_extras.io_utils import ImportHelper +from collections import defaultdict import bpy +import logging import mathutils -from bpy.props import StringProperty, IntProperty, BoolProperty -from bpy_extras.io_utils import ImportHelper +import os major, minor = bpy.app.version[0:2] transpose_matrices = minor >= 62 @@ -62,6 +69,25 @@ name="IFC Entity Type", description="The STEP Datatype keyword") +def _get_parent(instance): + """This is based on ifcopenshell.app.geom""" + if instance.is_a("IfcOpeningElement"): + # We skip opening elements as they are nameless. + # We use this function to get usable collections. + return _get_parent(instance.VoidsElements[0].RelatingBuildingElement) + if instance.is_a("IfcElement"): + fills = instance.FillsVoids + if len(fills): + return fills[0].RelatingOpeningElement + containments = instance.ContainedInStructure + if len(containments): + return containments[0].RelatingStructure + if instance.is_a("IfcObjectDefinition"): + decompositions = instance.Decomposes + if len(decompositions): + return decompositions[0].RelatingObject + + def import_ifc(filename, use_names, process_relations, blender_booleans): from . import ifcopenshell @@ -69,22 +95,59 @@ def import_ifc(filename, use_names, process_relations, blender_booleans): print(f"Reading {bpy.path.basename(filename)}...") settings = ifcopenshell_geom.settings() settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, blender_booleans) - iterator = ifcopenshell_geom.iterator(settings, filename) + assert os.path.exists(filename), filename + ifc_file = ifcopenshell.open(filename) + iterator = ifcopenshell_geom.iterator(settings, ifc_file) valid_file = iterator.initialize() if not valid_file: return False print("Done reading file") - id_to_object = {} + id_to_object = defaultdict(list) id_to_parent = {} id_to_matrix = {} openings = [] old_progress = -1 print("Creating geometry...") - collection = bpy.data.collections.new(f"{bpy.path.basename(filename)}") - bpy.context.scene.collection.children.link(collection) + root_collection = bpy.data.collections.new(f"{bpy.path.basename(filename)}") + bpy.context.scene.collection.children.link(root_collection) + + collections = { + 0: root_collection + } + def get_collection(cid): + if cid == 0: + return root_collection + + collection = collections.get(cid) + if collection is None: + try: + ifc_object = ifc_file.by_id(cid) + except Exception as exc: + logging.exception(exc) + ifc_object = None + + if ifc_object is not None: + # FIXME: I am really unsure if that is correct way to get parent object + ifc_parent_object = _get_parent(ifc_object) + parent_id = ifc_parent_object.id() if ifc_parent_object is not None else 0 + parent_collection = get_collection(parent_id) + name = ifc_object.Name or f'{ifc_object.is_a()}[{cid}]' + else: + parent_collection = get_collection(0) + name = f'unresolved_{cid}' + + collection = bpy.data.collections.new(name) + parent_collection.children.link(collection) + collections[cid] = collection + + return collection + if process_relations: rel_collection = bpy.data.collections.new("Relations") collection.children.link(rel_collection) + + project_meshes = dict() + while True: ob = iterator.get() @@ -97,16 +160,18 @@ def import_ifc(filename, use_names, process_relations, blender_booleans): nm = ob.name if len(ob.name) and use_names else ob.guid # MESH CREATION # Depending on version, geometry.id will be either int or str - mesh_name = 'mesh-%r' % ob.geometry.id - if mesh_name in bpy.data.meshes: - me = bpy.data.meshes[mesh_name] - else: + mesh_name = f'mesh-{ob.geometry.id}' + + me = project_meshes.get(mesh_name) + if me is None: verts = [[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)] me = bpy.data.meshes.new(mesh_name) + project_meshes[mesh_name] = me + me.from_pydata(verts, [], faces) me.validate() # MATERIAL CREATION @@ -163,7 +228,8 @@ def add_material(mname, props): id_to_matrix[ob.id] = mat else: bob.matrix_world = mat - collection.objects.link(bob) + + get_collection(ob.parent_id).objects.link(bob) bpy.context.view_layer.objects.active = bob bpy.ops.object.mode_set(mode='EDIT') @@ -178,8 +244,6 @@ def add_material(mname, props): bob.hide_viewport = bob.hide_render = True bob.display_type = 'WIRE' - if ob.id not in id_to_object: - id_to_object[ob.id] = [] id_to_object[ob.id].append(bob) if ob.parent_id > 0: @@ -201,45 +265,43 @@ def add_material(mname, props): if process_relations: print("Processing relations...") + while len(id_to_parent_temp): + id, parent_id = id_to_parent_temp.popitem() - while len(id_to_parent_temp) and process_relations: - id, parent_id = id_to_parent_temp.popitem() - - if parent_id in id_to_object: - bob = id_to_object[parent_id][0] - else: - parent_ob = iterator.getObject(parent_id) - if parent_ob.id == -1: - bob = None + if parent_id in id_to_object: + bob = id_to_object[parent_id][0] else: - m = parent_ob.transformation.matrix.data - nm = parent_ob.name if len(parent_ob.name) and use_names \ - else parent_ob.guid - bob = bpy.data.objects.new(nm, None) - - mat = mathutils.Matrix(( - [m[0], m[1], m[2], 0], - [m[3], m[4], m[5], 0], - [m[6], m[7], m[8], 0], - [m[9], m[10], m[11], 1])) - if transpose_matrices: - mat.transpose() - id_to_matrix[parent_ob.id] = mat - - rel_collection.objects.link(bob) - - bob.ifc_id = parent_ob.id - bob.ifc_name, bob.ifc_type, bob.ifc_guid = \ - parent_ob.name, parent_ob.type, parent_ob.guid - - if parent_ob.parent_id > 0: - id_to_parent[parent_id] = parent_ob.parent_id - id_to_parent_temp[parent_id] = parent_ob.parent_id - if parent_id not in id_to_object: id_to_object[parent_id] = [] - id_to_object[parent_id].append(bob) - if bob: - for ob in id_to_object[id]: - ob.parent = bob + parent_ob = iterator.getObject(parent_id) + if parent_ob.id == -1: + bob = None + else: + m = parent_ob.transformation.matrix.data + nm = parent_ob.name if len(parent_ob.name) and use_names \ + else parent_ob.guid + bob = bpy.data.objects.new(nm, None) + + mat = mathutils.Matrix(( + [m[0], m[1], m[2], 0], + [m[3], m[4], m[5], 0], + [m[6], m[7], m[8], 0], + [m[9], m[10], m[11], 1])) + if transpose_matrices: + mat.transpose() + id_to_matrix[parent_ob.id] = mat + + rel_collection.objects.link(bob) + + bob.ifc_id = parent_ob.id + bob.ifc_name, bob.ifc_type, bob.ifc_guid = \ + parent_ob.name, parent_ob.type, parent_ob.guid + + if parent_ob.parent_id > 0: + id_to_parent[parent_id] = parent_ob.parent_id + id_to_parent_temp[parent_id] = parent_ob.parent_id + id_to_object[parent_id].append(bob) + if bob: + for ob in id_to_object[id]: + ob.parent = bob id_to_matrix_temp = dict(id_to_matrix) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/__init__.py b/src/ifcopenshell-python/ifcopenshell/geom/__init__.py index 8281bf0aca4..04db49a6e2b 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/__init__.py @@ -21,5 +21,25 @@ from __future__ import division from __future__ import print_function -from . import occ_utils as utils +def _has_occ(): + try: + import OCC.Core.BRepTools + return True + except ImportError: + pass + + try: + import OCC.BRepTools + return True + except ImportError: + pass + + return False + + +has_occ = _has_occ() + +if has_occ: + from . import occ_utils as utils + from .main import * diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 92d57f005f9..d3a2161a8fa 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -28,16 +28,7 @@ from ..file import file from ..entity_instance import entity_instance - -def has_occ(): - try: - import OCC.BRepTools - except BaseException: - return False - return True - - -has_occ = has_occ() +from . import has_occ def wrap_shape_creation(settings, shape): @@ -47,6 +38,11 @@ def wrap_shape_creation(settings, shape): if has_occ: from . import occ_utils as utils + try: + from OCC.Core import TopoDS + except ImportError: + from OCC import TopoDS + def wrap_shape_creation(settings, shape): return utils.create_shape_from_serialization(shape) if getattr(settings, 'use_python_opencascade', False) else shape @@ -118,8 +114,7 @@ def unwrap(value): if isinstance(value, entity_instance): args.append(kwargs.get("completely_within", False)) elif has_occ: - import OCC.TopoDS - if isinstance(value, OCC.TopoDS.TopoDS_Shape): + if isinstance(value, TopoDS.TopoDS_Shape): args[1] = utils.serialize_shape(value) return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)] @@ -183,10 +178,8 @@ def entity_instance_or_none(e): return None if e is None else entity_instance(e) if has_occ: - import OCC.TopoDS - def _(string_or_shape, *args): - if isinstance(string_or_shape, OCC.TopoDS.TopoDS_Shape): + if isinstance(string_or_shape, TopoDS.TopoDS_Shape): string_or_shape = utils.serialize_shape(string_or_shape) return entity_instance_or_none(fn(string_or_shape, *args)) else: diff --git a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py index bba90da5bc3..8455d477329 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py @@ -23,8 +23,15 @@ import random import operator +import warnings + from collections import namedtuple, Iterable +try: + from OCC.Core import V3d, TopoDS, gp, AIS, Quantity, BRepTools, Graphic3d +except ImportError: + from OCC import V3d, TopoDS, gp, AIS, Quantity, BRepTools, Graphic3d + shape_tuple = namedtuple('shape_tuple', ('data', 'geometry', 'styles')) handle, main_loop, add_menu, add_function_to_menu = None, None, None, None @@ -46,7 +53,6 @@ def initialize_display(): - import OCC.V3d import OCC.Display.SimpleGui global handle, main_loop, add_menu, add_function_to_menu @@ -71,7 +77,7 @@ def lights(): viewer.DelLight(l) for dir in [(3, 2, 1), (-1, -2, -3)]: - light = OCC.V3d.V3d_DirectionalLight(viewer_handle) + light = V3d.V3d_DirectionalLight(viewer_handle) light.SetDirection(*dir) viewer.SetLightOn(light.GetHandle()) @@ -80,19 +86,13 @@ def lights(): def yield_subshapes(shape): - import OCC.TopoDS - - it = OCC.TopoDS.TopoDS_Iterator(shape) + it = TopoDS.TopoDS_Iterator(shape) while it.More(): yield it.Value() it.Next() def display_shape(shape, clr=None, viewer_handle=None): - import OCC.gp - import OCC.AIS - import OCC.Quantity - if viewer_handle is None: viewer_handle = handle @@ -101,7 +101,7 @@ def display_shape(shape, clr=None, viewer_handle=None): else: representation = None - material = OCC.Graphic3d.Graphic3d_MaterialAspect(OCC.Graphic3d.Graphic3d_NOM_PLASTER) + material = Graphic3d.Graphic3d_MaterialAspect(Graphic3d.Graphic3d_NOM_PLASTER) material.SetDiffuse(1) if representation and not clr: @@ -111,20 +111,20 @@ def display_shape(shape, clr=None, viewer_handle=None): clr = DEFAULT_STYLES.get(representation.data.type, DEFAULT_STYLES["DEFAULT"]) if clr: - ais = OCC.AIS.AIS_Shape(shape) + ais = AIS.AIS_Shape(shape) ais.SetMaterial(material) if isinstance(clr, str): - qclr = getattr(OCC.Quantity, "Quantity_NOC_%s" % clr.upper(), - getattr(OCC.Quantity, "Quantity_NOC_%s1" % clr.upper(), None)) + qclr = getattr(Quantity, "Quantity_NOC_%s" % clr.upper(), + getattr(Quantity, "Quantity_NOC_%s1" % clr.upper(), None)) if qclr is None: raise Exception("No color named '%s'" % clr.upper()) elif isinstance(clr, Iterable): clr = tuple(clr) - if len(clr) < 3 and len(clr) > 4: - raise Exception("Need 3 or 4 colour components. Got '%r'." % clr) - qclr = OCC.Quantity.Quantity_Color(clr[0], clr[1], clr[2], OCC.Quantity.Quantity_TOC_RGB) - elif isinstance(clr, OCC.Quantity.Quantity_Color): + if len(clr) < 3 or len(clr) > 4: + raise Exception("Need 3 or 4 color components. Got '%r'." % len(clr)) + qclr = Quantity.Quantity_Color(clr[0], clr[1], clr[2], Quantity.Quantity_TOC_RGB) + elif isinstance(clr, Quantity.Quantity_Color): qclr = clr else: raise Exception("Object of type %r cannot be used as a color." % type(clr)) @@ -133,23 +133,22 @@ def display_shape(shape, clr=None, viewer_handle=None): if isinstance(clr, tuple) and len(clr) == 4 and clr[3] < 1.: ais.SetTransparency(1. - clr[3]) - elif representation and hasattr(OCC.AIS, "AIS_MultipleConnectedShape"): + elif representation and hasattr(AIS, "AIS_MultipleConnectedShape"): default_style_applied = None - ais = OCC.AIS.AIS_MultipleConnectedShape(shape) + ais = AIS.AIS_MultipleConnectedShape(shape) subshapes = list(yield_subshapes(shape)) lens = len(representation.styles), len(subshapes) if lens[0] != lens[1]: - import warnings warnings.warn("Unable to assign styles to subshapes. Encountered %d styles for %d shapes." % lens) else: for shp, stl in zip(subshapes, representation.styles): - subshape = OCC.AIS.AIS_Shape(shp) + subshape = AIS.AIS_Shape(shp) if min(stl) < 0. or max(stl) > 1.: default_style_applied = stl = DEFAULT_STYLES.get(representation.data.type, DEFAULT_STYLES["DEFAULT"]) - subshape.SetColor(OCC.Quantity.Quantity_Color(stl[0], stl[1], stl[2], OCC.Quantity.Quantity_TOC_RGB)) + subshape.SetColor(Quantity.Quantity_Color(stl[0], stl[1], stl[2], Quantity.Quantity_TOC_RGB)) subshape.SetMaterial(material) if len(stl) == 4 and stl[3] < 1.: subshape.SetTransparency(1. - stl[3]) @@ -170,13 +169,13 @@ def display_shape(shape, clr=None, viewer_handle=None): ais.SetTransparency(1.) else: - ais = OCC.AIS.AIS_Shape(shape) + ais = AIS.AIS_Shape(shape) ais.SetMaterial(material) def r(): return random.random() * 0.3 + 0.7 - clr = OCC.Quantity.Quantity_Color(r(), r(), r(), OCC.Quantity.Quantity_TOC_RGB) + clr = Quantity.Quantity_Color(r(), r(), r(), Quantity.Quantity_TOC_RGB) ais.SetColor(clr) ais_handle = ais.GetHandle() @@ -190,25 +189,19 @@ def set_shape_transparency(ais, t): def get_bounding_box_center(bbox): - import OCC.gp - bbmin = [0.] * 3 bbmax = [0.] * 3 bbmin[0], bbmin[1], bbmin[2], bbmax[0], bbmax[1], bbmax[2] = bbox.Get() - return OCC.gp.gp_Pnt(*map(lambda xy: (xy[0] + xy[1]) / 2., zip(bbmin, bbmax))) + return gp.gp_Pnt(*map(lambda xy: (xy[0] + xy[1]) / 2., zip(bbmin, bbmax))) def serialize_shape(shape): - import OCC.BRepTools - - shapes = OCC.BRepTools.BRepTools_ShapeSet() + shapes = BRepTools.BRepTools_ShapeSet() shapes.Add(shape) return shapes.WriteToString() def create_shape_from_serialization(brep_object): - import OCC.BRepTools - brep_data, occ_shape, styles = None, None, () is_product_shape = True @@ -229,7 +222,7 @@ def create_shape_from_serialization(brep_object): return shape_tuple(brep_object, None, styles) try: - ss = OCC.BRepTools.BRepTools_ShapeSet() + ss = BRepTools.BRepTools_ShapeSet() ss.ReadFromString(brep_data) occ_shape = ss.Shape(ss.NbShapes()) except BaseException: diff --git a/src/ifcopenshell-python/ifcopenshell/template.py b/src/ifcopenshell-python/ifcopenshell/template.py index fdc34aec076..ec253ad4145 100644 --- a/src/ifcopenshell-python/ifcopenshell/template.py +++ b/src/ifcopenshell-python/ifcopenshell/template.py @@ -66,7 +66,7 @@ "application_version": lambda d: main.version, "project_globalid": lambda d: compress(uuid.uuid4().hex), "schema_identifier": lambda d: main.schema_identifier, - "timestamp": lambda d: time.time(), + "timestamp": lambda d: int(time.time()), "timestring": lambda d: time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(d.get('timestamp') or time.time())) }