From 0f365cadbd9aa6c04173791002c49b5c231d8c1c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 18 Mar 2021 19:02:54 +1100 Subject: [PATCH] Experimental code for (currently slow) native faceted brep handling. See #841. --- .../blenderbim/bim/__init__.py | 2 + .../blenderbim/bim/import_ifc.py | 440 +++++++----------- 2 files changed, 162 insertions(+), 280 deletions(-) diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index c467131273..28a7dfdd2a 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -183,6 +183,7 @@ if bpy is not None: bpy.types.Collection.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) # Check if we need this bpy.types.Material.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties) bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) + bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties) bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) @@ -210,6 +211,7 @@ if bpy is not None: del bpy.types.Collection.BIMObjectProperties # Check if we need this del bpy.types.Material.BIMMaterialProperties del bpy.types.Mesh.BIMMeshProperties + del bpy.types.Curve.BIMMeshProperties del bpy.types.Camera.BIMMeshProperties del bpy.types.Camera.BIMCameraProperties del bpy.types.TextCurve.BIMTextProperties diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index 5f1a8cda42..b546d9c088 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -415,13 +415,33 @@ class IfcImporter: self.exclude_elements |= self.native_elements def is_native(self, element): - if not element.Representation or not element.Representation.Representations: + if not element.Representation or not element.Representation.Representations or element.HasOpenings: return - for representation in self.get_body_representations(element.Representation.Representations): - # Single swept disk solids (e.g. rebar) are better natively represented as beveled curves - if len(representation["raw"].Items) == 1 and representation["raw"].Items[0].is_a("IfcSweptDiskSolid"): - self.native_data[element.GlobalId] = {"type": "IfcSweptDiskSolid"} - return True + representations = self.get_transformed_body_representations(element.Representation.Representations) + + # Single swept disk solids (e.g. rebar) are better natively represented as beveled curves + if [r for r in representations if self.is_native_swept_disk_solid(r)]: + self.native_data[element.GlobalId] = { + "representations": representations, + "representation": self.get_body_representation(element.Representation.Representations), + "type": "IfcSweptDiskSolid", + } + return True + # FacetedBreps (without voids) are meshes. See #841. + # Commented out as seems currently too slow. + # if [r for r in representations if self.is_native_faceted_brep(r)]: + # self.native_data[element.GlobalId] = { + # "representations": representations, + # "representation": self.get_body_representation(element.Representation.Representations), + # "type": "IfcFacetedBrep", + # } + # return True + + def is_native_swept_disk_solid(self, representation): + return len(representation["raw"].Items) == 1 and representation["raw"].Items[0].is_a("IfcSweptDiskSolid") + + def is_native_faceted_brep(self, representation): + return bool([i for i in representation["raw"].Items if i.is_a() == "IfcFacetedBrep"]) def get_products_from_shape_representation(self, element): products = [pr.ShapeOfProduct[0] for pr in element.OfProductRepresentation] @@ -619,7 +639,28 @@ class IfcImporter: if total % 250 == 0: print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint)) checkpoint = time.time() - self.create_product(element, mesh=self.create_native_mesh(element)) + native_data = self.native_data[element.GlobalId] + representation = native_data["representation"] + context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 + mesh_name = f"{context_id}/{representation.id()}" + mesh = self.meshes.get(mesh_name) + if mesh is None: + if native_data["type"] == "IfcSweptDiskSolid": + mesh = self.create_native_swept_disk_solid(element, mesh_name) + elif native_data["type"] == "IfcFacetedBrep": + mesh = self.create_native_faceted_brep(element, mesh_name) + mesh.BIMMeshProperties.ifc_definition_id = representation.id() + self.meshes[mesh_name] = mesh + self.create_product(element, mesh=mesh) + if native_data["type"] == "IfcFacetedBrep": + # The current implementation doesn't reuse vertices, so we weld it after assigning materials. + # This welding isn't true to the representation, but is easy and seems inexpensive. + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) + bm.to_mesh(mesh) + bm.free() + print("Done creating geometry") def create_products(self): @@ -750,6 +791,9 @@ class IfcImporter: ) obj.matrix_world = self.apply_blender_offset_to_matrix(mat) self.material_creator.create(element, obj, mesh) + elif mesh: + obj.matrix_world = self.apply_blender_offset_to_matrix(self.get_element_matrix(element)) + 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)) @@ -759,224 +803,112 @@ class IfcImporter: obj.display_type = "WIRE" return obj - def create_native_mesh(self, element): - # TODO This should be split off into its own module for run-time native mesh conversion - 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.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 create_native_faceted_brep(self, element, mesh_name): + # TODO: georeferencing? + # Note: to make this algorithm simpler (it's already confusing) we don't reuse / weld verts + # co [x y z x y z x y z ...] + # vertex_index [i i i i i ...] + # loop_start [0 3 6 9 ...] (for tris) + # loop_total [3 3 3 3 ...] (for tris) + co = [] + vertex_index = [] + loop_start = [] + loop_total = [] + total_verts = 0 + total_polygons = 0 + materials = [] + material_ids = [] + item_index = 0 - 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 + for representation in self.native_data[element.GlobalId]["representations"]: + for item in representation["raw"].Items: + materials.append(self.get_representation_item_material_name(item) or "NULLMAT") + mesh = item.get_info_2(recursive=True) # See bug #841 + total_item_polygons = 0 + for face in mesh["Outer"]["CfsFaces"]: + # Blender cannot handle faces with holes. + if len(face["Bounds"]) > 1: + inner_bounds = [] + for bound in face["Bounds"]: + if bound["type"] == "IfcFaceOuterBound": + outer_bound = [[p["Coordinates"] for p in bound["Bound"]["Polygon"]]] + else: + inner_bounds.append([p["Coordinates"] for p in bound["Bound"]["Polygon"]]) + points = outer_bound[0].copy() + [points.extend(p) for p in inner_bounds] + tessellated_polygons = mathutils.geometry.tessellate_polygon(outer_bound + inner_bounds) + tessellated_faces = [[{"Coordinates": points[pi]} for pi in t] for t in tessellated_polygons] + else: + tessellated_faces = [face["Bounds"][0]["Bound"]["Polygon"]] + + for tessellated_face in tessellated_faces: + loop_start.append(total_verts) + loop_count = 0 + total_polygons += 1 + total_item_polygons += 1 + for point in tessellated_face: + co.extend( + representation["matrix"] + @ mathutils.Vector([c * self.unit_scale for c in point["Coordinates"]]) + ) + total_verts += 1 + loop_count += 1 + loop_total.append(loop_count) + vertex_index = range(0, total_verts) + if materials[item_index] == "NULLMAT": + # Magic number -1 represents no material, until this has a better approach + material_ids += [-1] * total_item_polygons else: - new_spline.points.add(1) - new_spline.points[-1].co = point.co - return a + material_ids += [item_index] * total_item_polygons + item_index += 1 + mesh = bpy.data.meshes.new("Tester") - def merge_bmeshes(self, a, b): - mesh = bpy.data.meshes.new("x") - b.to_mesh(mesh) - b.free() - a.from_mesh(mesh) - return a + mesh.vertices.add(total_verts) + mesh.vertices.foreach_set("co", co) + mesh.loops.add(total_verts) + mesh.loops.foreach_set("vertex_index", vertex_index) + mesh.polygons.add(total_polygons) + mesh.polygons.foreach_set("loop_start", loop_start) + mesh.polygons.foreach_set("loop_total", loop_total) + mesh.update() - 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 + mesh["ios_materials"] = materials + mesh["ios_material_ids"] = material_ids 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 create_native_swept_disk_solid(self, element, mesh_name): + # TODO: georeferencing? + curve = bpy.data.curves.new(mesh_name, type="CURVE") + curve.dimensions = "3D" + curve.resolution_u = 2 + polyline = curve.splines.new("POLY") - 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 + for representation in self.native_data[element.GlobalId]["representations"]: + for item in representation["raw"].Items: + # TODO: support inner radius, start param, and end param + geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix) + 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 = representation["matrix"] @ mathutils.Vector(v1) + v2 = vertices[edge[1]] + polyline.points.add(1) + polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2) - 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 + curve.bevel_depth = self.unit_scale * item.Radius + return curve def merge_by_class(self): merge_set = {} @@ -1273,11 +1205,6 @@ class IfcImporter: 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 @@ -1285,7 +1212,19 @@ class IfcImporter: result[2][3] *= self.unit_scale return result - def get_body_representations(self, representations, matrix=None): + def get_body_representation(self, representations): + for representation in representations: + if ( + representation.RepresentationIdentifier == "Body" + and representation.RepresentationType == "MappedRepresentation" + ): + if len(representation.Items) > 1: + return representation + return self.get_body_representation([representation.Items[0].MappingSource.MappedRepresentation]) + elif representation.RepresentationIdentifier == "Body": + return representation + + def get_transformed_body_representations(self, representations, matrix=None): if matrix is None: matrix = mathutils.Matrix() results = [] @@ -1300,7 +1239,9 @@ class IfcImporter: if item.MappingTarget: transform = transform @ self.get_cartesiantransformationoperator(item.MappingTarget) results.extend( - self.get_body_representations([item.MappingSource.MappedRepresentation], transform @ matrix) + self.get_transformed_body_representations( + [item.MappingSource.MappedRepresentation], transform @ matrix + ) ) elif representation.RepresentationIdentifier == "Body": results.append({"raw": representation, "matrix": self.scale_matrix(matrix)}) @@ -1348,16 +1289,13 @@ class IfcImporter: 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): + def create_mesh(self, element, shape): 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 @@ -1422,64 +1360,6 @@ class IfcImporter: 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))